Commit 596263f4177868ef6594a2d6fdf6e29889350a93

Authored by shaofan
1 parent 402685bc

Add multi-tenant support for roles and menus

src/main/java/com/diligrp/rider/controller/AdminSubstationRoleController.java 0 → 100644
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.exception.BizException;
  4 +import com.diligrp.rider.common.result.Result;
  5 +import com.diligrp.rider.dto.AdminRoleMenuAssignDTO;
  6 +import com.diligrp.rider.dto.AdminRoleSaveDTO;
  7 +import com.diligrp.rider.service.SystemRoleService;
  8 +import com.diligrp.rider.service.impl.SystemRoleMenuServiceImpl;
  9 +import com.diligrp.rider.vo.AdminRoleMenuTreeVO;
  10 +import com.diligrp.rider.vo.AdminRoleVO;
  11 +import jakarta.servlet.http.HttpServletRequest;
  12 +import jakarta.validation.Valid;
  13 +import lombok.RequiredArgsConstructor;
  14 +import org.springframework.web.bind.annotation.*;
  15 +
  16 +import java.util.List;
  17 +
  18 +/**
  19 + * 分站管理员侧角色管理接口(/api/admin/system/role/**)
  20 + * 每个分站只能看到/操作自己 cityId 下的角色,菜单只能分配 SUBSTATION/BOTH scope
  21 + */
  22 +@RestController
  23 +@RequestMapping("/api/admin/system/role")
  24 +@RequiredArgsConstructor
  25 +public class AdminSubstationRoleController {
  26 +
  27 + private final SystemRoleService systemRoleService;
  28 + private final SystemRoleMenuServiceImpl systemRoleMenuService;
  29 +
  30 + @GetMapping("/list")
  31 + public Result<List<AdminRoleVO>> list(HttpServletRequest request) {
  32 + Long cityId = resolveCityId(request);
  33 + return Result.success(systemRoleService.listByCityId(cityId));
  34 + }
  35 +
  36 + @PostMapping("/add")
  37 + public Result<Void> add(@Valid @RequestBody AdminRoleSaveDTO dto, HttpServletRequest request) {
  38 + Long cityId = resolveCityId(request);
  39 + systemRoleService.addForCity(dto, cityId);
  40 + return Result.success();
  41 + }
  42 +
  43 + @PutMapping("/edit")
  44 + public Result<Void> edit(@Valid @RequestBody AdminRoleSaveDTO dto, HttpServletRequest request) {
  45 + Long cityId = resolveCityId(request);
  46 + systemRoleService.editForCity(dto, cityId);
  47 + return Result.success();
  48 + }
  49 +
  50 + @PostMapping("/ban")
  51 + public Result<Void> ban(@RequestParam Long id, HttpServletRequest request) {
  52 + Long cityId = resolveCityId(request);
  53 + systemRoleService.banForCity(id, cityId);
  54 + return Result.success();
  55 + }
  56 +
  57 + @PostMapping("/cancelBan")
  58 + public Result<Void> cancelBan(@RequestParam Long id, HttpServletRequest request) {
  59 + Long cityId = resolveCityId(request);
  60 + systemRoleService.cancelBanForCity(id, cityId);
  61 + return Result.success();
  62 + }
  63 +
  64 + @DeleteMapping("/del")
  65 + public Result<Void> del(@RequestParam Long id, HttpServletRequest request) {
  66 + Long cityId = resolveCityId(request);
  67 + systemRoleService.delForCity(id, cityId);
  68 + return Result.success();
  69 + }
  70 +
  71 + @GetMapping("/{roleId}/menu-tree")
  72 + public Result<List<AdminRoleMenuTreeVO>> menuTree(@PathVariable Long roleId,
  73 + HttpServletRequest request) {
  74 + Long cityId = resolveCityId(request);
  75 + return Result.success(systemRoleMenuService.getRoleMenuTreeForCity(roleId, cityId));
  76 + }
  77 +
  78 + @PostMapping("/{roleId}/menus")
  79 + public Result<Void> assignMenus(@PathVariable Long roleId,
  80 + @Valid @RequestBody AdminRoleMenuAssignDTO dto,
  81 + HttpServletRequest request) {
  82 + Long cityId = resolveCityId(request);
  83 + systemRoleMenuService.assignMenusForCity(roleId, dto, cityId);
  84 + return Result.success();
  85 + }
  86 +
  87 + private Long resolveCityId(HttpServletRequest request) {
  88 + Long cityId = (Long) request.getAttribute("cityId");
  89 + if (cityId == null || cityId < 1) {
  90 + throw new BizException("当前账号未绑定有效城市");
  91 + }
  92 + return cityId;
  93 + }
  94 +}
src/main/java/com/diligrp/rider/controller/AdminSubstationUserController.java 0 → 100644
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.enums.AdminRoleScopeEnum;
  6 +import com.diligrp.rider.common.exception.BizException;
  7 +import com.diligrp.rider.common.result.Result;
  8 +import com.diligrp.rider.dto.ChangePasswordDTO;
  9 +import com.diligrp.rider.entity.Substation;
  10 +import com.diligrp.rider.mapper.SubstationMapper;
  11 +import com.diligrp.rider.service.RoleScopeGuardService;
  12 +import jakarta.servlet.http.HttpServletRequest;
  13 +import jakarta.validation.Valid;
  14 +import lombok.RequiredArgsConstructor;
  15 +import org.springframework.util.DigestUtils;
  16 +import org.springframework.web.bind.annotation.*;
  17 +
  18 +import java.nio.charset.StandardCharsets;
  19 +import java.util.List;
  20 +
  21 +/**
  22 + * 分站管理员侧子账号管理接口(/api/admin/substation-user/**)
  23 + * 分站管理员只能管理本 cityId 下的账号
  24 + */
  25 +@RestController
  26 +@RequestMapping("/api/admin/substation-user")
  27 +@RequiredArgsConstructor
  28 +public class AdminSubstationUserController {
  29 +
  30 + private final SubstationMapper substationMapper;
  31 + private final RoleScopeGuardService roleScopeGuardService;
  32 +
  33 + @GetMapping("/list")
  34 + public Result<List<Substation>> list(@RequestParam(required = false) String keyword,
  35 + HttpServletRequest request) {
  36 + Long cityId = resolveCityId(request);
  37 + Long selfId = (Long) request.getAttribute("adminId");
  38 + LambdaQueryWrapper<Substation> wrapper = new LambdaQueryWrapper<Substation>()
  39 + .eq(Substation::getCityId, cityId)
  40 + .ne(Substation::getId, selfId) // 不返回自己
  41 + .orderByDesc(Substation::getId);
  42 + if (keyword != null && !keyword.isBlank()) {
  43 + wrapper.and(w -> w.like(Substation::getUserLogin, keyword)
  44 + .or().like(Substation::getUserNickname, keyword)
  45 + .or().like(Substation::getMobile, keyword));
  46 + }
  47 + List<Substation> list = substationMapper.selectList(wrapper);
  48 + // 隐藏密码字段
  49 + list.forEach(s -> s.setUserPass(null));
  50 + return Result.success(list);
  51 + }
  52 +
  53 + @PostMapping("/add")
  54 + public Result<Void> add(@RequestBody Substation substation, HttpServletRequest request) {
  55 + Long cityId = resolveCityId(request);
  56 + substation.setCityId(cityId);
  57 +
  58 + Long loginExists = substationMapper.selectCount(new LambdaQueryWrapper<Substation>()
  59 + .eq(Substation::getUserLogin, substation.getUserLogin()));
  60 + if (loginExists > 0) {
  61 + throw new BizException("账号已存在,请更换");
  62 + }
  63 + // 校验角色必须是 SUBSTATION scope 且属于本租户或全局
  64 + roleScopeGuardService.requireRole(substation.getRoleId(), AdminRoleScopeEnum.SUBSTATION.name(), cityId);
  65 +
  66 + substation.setUserPass(encryptPass(substation.getUserPass()));
  67 + substation.setUserStatus(1);
  68 + substation.setCreateTime(System.currentTimeMillis() / 1000);
  69 + substationMapper.insert(substation);
  70 + return Result.success();
  71 + }
  72 +
  73 + @PutMapping("/edit")
  74 + public Result<Void> edit(@RequestBody Substation substation, HttpServletRequest request) {
  75 + Long cityId = resolveCityId(request);
  76 + Substation existing = requireOwned(substation.getId(), cityId);
  77 +
  78 + // 校验角色归属
  79 + roleScopeGuardService.requireRole(substation.getRoleId(), AdminRoleScopeEnum.SUBSTATION.name(), cityId);
  80 +
  81 + substation.setCityId(existing.getCityId()); // 不允许修改城市
  82 + if (substation.getUserPass() == null || substation.getUserPass().isBlank()) {
  83 + substation.setUserPass(null);
  84 + } else {
  85 + substation.setUserPass(encryptPass(substation.getUserPass()));
  86 + }
  87 + substationMapper.updateById(substation);
  88 + return Result.success();
  89 + }
  90 +
  91 + @PostMapping("/ban")
  92 + public Result<Void> ban(@RequestParam Long id, HttpServletRequest request) {
  93 + Long cityId = resolveCityId(request);
  94 + requireOwned(id, cityId);
  95 + substationMapper.update(null, new LambdaUpdateWrapper<Substation>()
  96 + .eq(Substation::getId, id).set(Substation::getUserStatus, 0));
  97 + return Result.success();
  98 + }
  99 +
  100 + @PostMapping("/cancelBan")
  101 + public Result<Void> cancelBan(@RequestParam Long id, HttpServletRequest request) {
  102 + Long cityId = resolveCityId(request);
  103 + requireOwned(id, cityId);
  104 + substationMapper.update(null, new LambdaUpdateWrapper<Substation>()
  105 + .eq(Substation::getId, id).set(Substation::getUserStatus, 1));
  106 + return Result.success();
  107 + }
  108 +
  109 + @DeleteMapping("/del")
  110 + public Result<Void> del(@RequestParam Long id, HttpServletRequest request) {
  111 + Long cityId = resolveCityId(request);
  112 + requireOwned(id, cityId);
  113 + substationMapper.deleteById(id);
  114 + return Result.success();
  115 + }
  116 +
  117 + @PostMapping("/changePassword")
  118 + public Result<Void> changePassword(@RequestParam Long id,
  119 + @Valid @RequestBody ChangePasswordDTO dto,
  120 + HttpServletRequest request) {
  121 + Long cityId = resolveCityId(request);
  122 + Substation sub = requireOwned(id, cityId);
  123 + if (!encryptPass(dto.getOldPassword()).equals(sub.getUserPass())) {
  124 + throw new BizException("原密码不正确");
  125 + }
  126 + if (encryptPass(dto.getNewPassword()).equals(sub.getUserPass())) {
  127 + throw new BizException("新密码不能与原密码相同");
  128 + }
  129 + substationMapper.update(null, new LambdaUpdateWrapper<Substation>()
  130 + .eq(Substation::getId, id)
  131 + .set(Substation::getUserPass, encryptPass(dto.getNewPassword())));
  132 + return Result.success();
  133 + }
  134 +
  135 + private Substation requireOwned(Long id, Long cityId) {
  136 + Substation sub = substationMapper.selectById(id);
  137 + if (sub == null) {
  138 + throw new BizException("账号不存在");
  139 + }
  140 + if (!cityId.equals(sub.getCityId())) {
  141 + throw new BizException("无权操作其他租户的账号");
  142 + }
  143 + return sub;
  144 + }
  145 +
  146 + private Long resolveCityId(HttpServletRequest request) {
  147 + Long cityId = (Long) request.getAttribute("cityId");
  148 + if (cityId == null || cityId < 1) {
  149 + throw new BizException("当前账号未绑定有效城市");
  150 + }
  151 + return cityId;
  152 + }
  153 +
  154 + private String encryptPass(String pass) {
  155 + return DigestUtils.md5DigestAsHex(pass.getBytes(StandardCharsets.UTF_8));
  156 + }
  157 +}
src/main/java/com/diligrp/rider/entity/SysRole.java
@@ -18,6 +18,9 @@ public class SysRole { @@ -18,6 +18,9 @@ public class SysRole {
18 18
19 private String roleScope; 19 private String roleScope;
20 20
  21 + /** 所属租户ID:0=平台全局角色,>0=分站租户专属角色 */
  22 + private Long cityId;
  23 +
21 private Integer status; 24 private Integer status;
22 25
23 private Long createTime; 26 private Long createTime;
src/main/java/com/diligrp/rider/service/RoleScopeGuardService.java
@@ -4,4 +4,7 @@ import com.diligrp.rider.entity.SysRole; @@ -4,4 +4,7 @@ import com.diligrp.rider.entity.SysRole;
4 4
5 public interface RoleScopeGuardService { 5 public interface RoleScopeGuardService {
6 SysRole requireRole(Long roleId, String requiredScope); 6 SysRole requireRole(Long roleId, String requiredScope);
  7 +
  8 + /** 分站侧调用:额外校验角色属于该租户或是全局角色 */
  9 + SysRole requireRole(Long roleId, String requiredScope, Long cityId);
7 } 10 }
src/main/java/com/diligrp/rider/service/SystemRoleService.java
@@ -6,15 +6,36 @@ import com.diligrp.rider.vo.AdminRoleVO; @@ -6,15 +6,36 @@ import com.diligrp.rider.vo.AdminRoleVO;
6 import java.util.List; 6 import java.util.List;
7 7
8 public interface SystemRoleService { 8 public interface SystemRoleService {
  9 + /** 平台侧调用:只返回平台全局角色(city_id=0) */
9 List<AdminRoleVO> list(boolean includeDisabled); 10 List<AdminRoleVO> list(boolean includeDisabled);
10 11
  12 + /** 分站侧调用:只返回该租户自己的角色(city_id=cityId) */
  13 + List<AdminRoleVO> listByCityId(Long cityId);
  14 +
  15 + /** 平台侧新增角色(city_id=0) */
11 void add(AdminRoleSaveDTO dto); 16 void add(AdminRoleSaveDTO dto);
12 17
  18 + /** 分站侧新增角色(city_id=cityId) */
  19 + void addForCity(AdminRoleSaveDTO dto, Long cityId);
  20 +
  21 + /** 平台侧编辑(只能编辑 city_id=0 的角色) */
13 void edit(AdminRoleSaveDTO dto); 22 void edit(AdminRoleSaveDTO dto);
14 23
  24 + /** 分站侧编辑(只能编辑本 cityId 的角色) */
  25 + void editForCity(AdminRoleSaveDTO dto, Long cityId);
  26 +
15 void ban(Long id); 27 void ban(Long id);
16 28
  29 + /** 分站侧禁用(校验 cityId 归属) */
  30 + void banForCity(Long id, Long cityId);
  31 +
17 void cancelBan(Long id); 32 void cancelBan(Long id);
18 33
  34 + /** 分站侧启用(校验 cityId 归属) */
  35 + void cancelBanForCity(Long id, Long cityId);
  36 +
19 void del(Long id); 37 void del(Long id);
  38 +
  39 + /** 分站侧删除(只能删除本 cityId 的角色) */
  40 + void delForCity(Long id, Long cityId);
20 } 41 }
src/main/java/com/diligrp/rider/service/impl/AdminMenuServiceImpl.java
@@ -47,8 +47,10 @@ public class AdminMenuServiceImpl implements AdminMenuService { @@ -47,8 +47,10 @@ public class AdminMenuServiceImpl implements AdminMenuService {
47 role = sysRoleMapper.selectById(roleId); 47 role = sysRoleMapper.selectById(roleId);
48 } 48 }
49 if (role == null) { 49 if (role == null) {
  50 + // Fallback:找全局内置角色(city_id=0)
50 role = sysRoleMapper.selectOne(new LambdaQueryWrapper<SysRole>() 51 role = sysRoleMapper.selectOne(new LambdaQueryWrapper<SysRole>()
51 .eq(SysRole::getCode, fallbackCode) 52 .eq(SysRole::getCode, fallbackCode)
  53 + .eq(SysRole::getCityId, 0L)
52 .eq(SysRole::getStatus, 1) 54 .eq(SysRole::getStatus, 1)
53 .last("LIMIT 1")); 55 .last("LIMIT 1"));
54 } 56 }
src/main/java/com/diligrp/rider/service/impl/AdminUserManageServiceImpl.java
@@ -40,7 +40,7 @@ public class AdminUserManageServiceImpl implements AdminUserManageService { @@ -40,7 +40,7 @@ public class AdminUserManageServiceImpl implements AdminUserManageService {
40 if (exists > 0) { 40 if (exists > 0) {
41 throw new BizException("账号已存在,请更换"); 41 throw new BizException("账号已存在,请更换");
42 } 42 }
43 - roleScopeGuardService.requireRole(adminUser.getRoleId(), AdminRoleScopeEnum.PLATFORM.name()); 43 + roleScopeGuardService.requireRole(adminUser.getRoleId(), AdminRoleScopeEnum.PLATFORM.name(), 0L);
44 adminUser.setUserPass(encryptPass(adminUser.getUserPass())); 44 adminUser.setUserPass(encryptPass(adminUser.getUserPass()));
45 adminUser.setUserStatus(1); 45 adminUser.setUserStatus(1);
46 adminUser.setCreateTime(System.currentTimeMillis() / 1000); 46 adminUser.setCreateTime(System.currentTimeMillis() / 1000);
@@ -53,7 +53,7 @@ public class AdminUserManageServiceImpl implements AdminUserManageService { @@ -53,7 +53,7 @@ public class AdminUserManageServiceImpl implements AdminUserManageService {
53 if (existing == null) { 53 if (existing == null) {
54 throw new BizException("平台账号不存在"); 54 throw new BizException("平台账号不存在");
55 } 55 }
56 - roleScopeGuardService.requireRole(adminUser.getRoleId(), AdminRoleScopeEnum.PLATFORM.name()); 56 + roleScopeGuardService.requireRole(adminUser.getRoleId(), AdminRoleScopeEnum.PLATFORM.name(), 0L);
57 if (adminUser.getUserPass() == null || adminUser.getUserPass().isBlank()) { 57 if (adminUser.getUserPass() == null || adminUser.getUserPass().isBlank()) {
58 adminUser.setUserPass(null); 58 adminUser.setUserPass(null);
59 } else { 59 } else {
src/main/java/com/diligrp/rider/service/impl/MenuBootstrapServiceImpl.java
@@ -60,10 +60,12 @@ public class MenuBootstrapServiceImpl implements MenuBootstrapService { @@ -60,10 +60,12 @@ public class MenuBootstrapServiceImpl implements MenuBootstrapService {
60 defaults.add(menu("dashboard", "工作台", "MENU", "/dashboard", "HomeOutlined", 0L, MenuScopeEnum.BOTH, 10)); 60 defaults.add(menu("dashboard", "工作台", "MENU", "/dashboard", "HomeOutlined", 0L, MenuScopeEnum.BOTH, 10));
61 defaults.add(menu("city.manage", "租户管理", "MENU", "/city", "GlobalOutlined", 0L, MenuScopeEnum.PLATFORM, 20)); 61 defaults.add(menu("city.manage", "租户管理", "MENU", "/city", "GlobalOutlined", 0L, MenuScopeEnum.PLATFORM, 20));
62 defaults.add(menu("substation.manage", "分站管理", "MENU", "/substation", "ApartmentOutlined", 0L, MenuScopeEnum.PLATFORM, 30)); 62 defaults.add(menu("substation.manage", "分站管理", "MENU", "/substation", "ApartmentOutlined", 0L, MenuScopeEnum.PLATFORM, 30));
  63 + defaults.add(menu("substation.user", "分站账号", "MENU", "/substation/user", "TeamOutlined", 0L, MenuScopeEnum.SUBSTATION, 31));
63 defaults.add(menu("merchant.root", "商家管理", "DIR", "", "ShopOutlined", 0L, MenuScopeEnum.PLATFORM, 40)); 64 defaults.add(menu("merchant.root", "商家管理", "DIR", "", "ShopOutlined", 0L, MenuScopeEnum.PLATFORM, 40));
64 defaults.add(menu("merchant.enter", "入驻申请", "MENU", "/merchant/enter", "", 0L, MenuScopeEnum.PLATFORM, 41)); 65 defaults.add(menu("merchant.enter", "入驻申请", "MENU", "/merchant/enter", "", 0L, MenuScopeEnum.PLATFORM, 41));
65 defaults.add(menu("merchant.store", "店铺管理", "MENU", "/merchant/store", "", 0L, MenuScopeEnum.PLATFORM, 42)); 66 defaults.add(menu("merchant.store", "店铺管理", "MENU", "/merchant/store", "", 0L, MenuScopeEnum.PLATFORM, 42));
66 defaults.add(menu("rider.list", "骑手管理", "MENU", "/rider", "UserOutlined", 0L, MenuScopeEnum.BOTH, 50)); 67 defaults.add(menu("rider.list", "骑手管理", "MENU", "/rider", "UserOutlined", 0L, MenuScopeEnum.BOTH, 50));
  68 + defaults.add(menu("rider.level", "骑手等级", "MENU", "/rider/level", "TrophyOutlined", 0L, MenuScopeEnum.BOTH, 55));
67 defaults.add(menu("rider.evaluate", "骑手评价", "MENU", "/rider/evaluate", "StarOutlined", 0L, MenuScopeEnum.BOTH, 60)); 69 defaults.add(menu("rider.evaluate", "骑手评价", "MENU", "/rider/evaluate", "StarOutlined", 0L, MenuScopeEnum.BOTH, 60));
68 defaults.add(menu("order.root", "订单管理", "DIR", "", "UnorderedListOutlined", 0L, MenuScopeEnum.BOTH, 70)); 70 defaults.add(menu("order.root", "订单管理", "DIR", "", "UnorderedListOutlined", 0L, MenuScopeEnum.BOTH, 70));
69 defaults.add(menu("order.list", "订单列表", "MENU", "/order", "", 0L, MenuScopeEnum.BOTH, 71)); 71 defaults.add(menu("order.list", "订单列表", "MENU", "/order", "", 0L, MenuScopeEnum.BOTH, 71));
@@ -80,6 +82,10 @@ public class MenuBootstrapServiceImpl implements MenuBootstrapService { @@ -80,6 +82,10 @@ public class MenuBootstrapServiceImpl implements MenuBootstrapService {
80 defaults.add(menu("system.role", "角色管理", "MENU", "/system/role", "", 0L, MenuScopeEnum.PLATFORM, 102)); 82 defaults.add(menu("system.role", "角色管理", "MENU", "/system/role", "", 0L, MenuScopeEnum.PLATFORM, 102));
81 defaults.add(menu("system.role_menu", "角色菜单", "MENU", "/system/role-menu", "", 0L, MenuScopeEnum.PLATFORM, 103)); 83 defaults.add(menu("system.role_menu", "角色菜单", "MENU", "/system/role-menu", "", 0L, MenuScopeEnum.PLATFORM, 103));
82 defaults.add(menu("admin.user", "平台账号", "MENU", "/admin-user", "", 0L, MenuScopeEnum.PLATFORM, 104)); 84 defaults.add(menu("admin.user", "平台账号", "MENU", "/admin-user", "", 0L, MenuScopeEnum.PLATFORM, 104));
  85 + // 分站管理员专属:站点管理目录
  86 + defaults.add(menu("system.sub_root", "站点管理", "DIR", "", "SettingOutlined", 0L, MenuScopeEnum.SUBSTATION, 110));
  87 + defaults.add(menu("system.sub_role", "角色管理", "MENU", "/substation/role", "", 0L, MenuScopeEnum.SUBSTATION, 111));
  88 + defaults.add(menu("system.sub_role_menu", "角色菜单", "MENU", "/substation/role-menu", "", 0L, MenuScopeEnum.SUBSTATION, 112));
83 89
84 Map<String, SysMenu> persisted = new LinkedHashMap<>(); 90 Map<String, SysMenu> persisted = new LinkedHashMap<>();
85 for (SysMenu menu : defaults) { 91 for (SysMenu menu : defaults) {
@@ -111,6 +117,11 @@ public class MenuBootstrapServiceImpl implements MenuBootstrapService { @@ -111,6 +117,11 @@ public class MenuBootstrapServiceImpl implements MenuBootstrapService {
111 persisted.get("system.role_menu").setListOrder(103); 117 persisted.get("system.role_menu").setListOrder(103);
112 persisted.get("admin.user").setParentId(persisted.get("system.root").getId()); 118 persisted.get("admin.user").setParentId(persisted.get("system.root").getId());
113 persisted.get("admin.user").setListOrder(104); 119 persisted.get("admin.user").setListOrder(104);
  120 + // 分站专属菜单父节点
  121 + persisted.get("system.sub_role").setParentId(persisted.get("system.sub_root").getId());
  122 + persisted.get("system.sub_role").setListOrder(111);
  123 + persisted.get("system.sub_role_menu").setParentId(persisted.get("system.sub_root").getId());
  124 + persisted.get("system.sub_role_menu").setListOrder(112);
114 125
115 for (SysMenu menu : persisted.values()) { 126 for (SysMenu menu : persisted.values()) {
116 sysMenuMapper.updateById(menu); 127 sysMenuMapper.updateById(menu);
src/main/java/com/diligrp/rider/service/impl/RoleScopeGuardServiceImpl.java
@@ -16,6 +16,11 @@ public class RoleScopeGuardServiceImpl implements RoleScopeGuardService { @@ -16,6 +16,11 @@ public class RoleScopeGuardServiceImpl implements RoleScopeGuardService {
16 16
17 @Override 17 @Override
18 public SysRole requireRole(Long roleId, String requiredScope) { 18 public SysRole requireRole(Long roleId, String requiredScope) {
  19 + return requireRole(roleId, requiredScope, null);
  20 + }
  21 +
  22 + @Override
  23 + public SysRole requireRole(Long roleId, String requiredScope, Long cityId) {
19 if (roleId == null || roleId < 1) { 24 if (roleId == null || roleId < 1) {
20 throw new BizException("角色不能为空"); 25 throw new BizException("角色不能为空");
21 } 26 }
@@ -29,6 +34,14 @@ public class RoleScopeGuardServiceImpl implements RoleScopeGuardService { @@ -29,6 +34,14 @@ public class RoleScopeGuardServiceImpl implements RoleScopeGuardService {
29 if (!requiredScope.equals(role.getRoleScope())) { 34 if (!requiredScope.equals(role.getRoleScope())) {
30 throw new BizException("角色归属不匹配"); 35 throw new BizException("角色归属不匹配");
31 } 36 }
  37 + // cityId != null 时,要求角色属于该租户或是全局角色(city_id=0)
  38 + if (cityId != null) {
  39 + boolean isGlobal = role.getCityId() == null || role.getCityId() == 0L;
  40 + boolean isOwned = cityId.equals(role.getCityId());
  41 + if (!isGlobal && !isOwned) {
  42 + throw new BizException("角色不属于当前租户");
  43 + }
  44 + }
32 return role; 45 return role;
33 } 46 }
34 } 47 }
src/main/java/com/diligrp/rider/service/impl/SubstationServiceImpl.java
@@ -44,7 +44,7 @@ public class SubstationServiceImpl implements SubstationService { @@ -44,7 +44,7 @@ public class SubstationServiceImpl implements SubstationService {
44 .eq(Substation::getUserLogin, substation.getUserLogin())); 44 .eq(Substation::getUserLogin, substation.getUserLogin()));
45 if (loginExists > 0) throw new BizException("账号已存在,请更换"); 45 if (loginExists > 0) throw new BizException("账号已存在,请更换");
46 46
47 - roleScopeGuardService.requireRole(substation.getRoleId(), AdminRoleScopeEnum.SUBSTATION.name()); 47 + roleScopeGuardService.requireRole(substation.getRoleId(), AdminRoleScopeEnum.SUBSTATION.name(), substation.getCityId());
48 substation.setUserPass(encryptPass(substation.getUserPass())); 48 substation.setUserPass(encryptPass(substation.getUserPass()));
49 substation.setUserStatus(1); 49 substation.setUserStatus(1);
50 substation.setCreateTime(System.currentTimeMillis() / 1000); 50 substation.setCreateTime(System.currentTimeMillis() / 1000);
@@ -55,7 +55,7 @@ public class SubstationServiceImpl implements SubstationService { @@ -55,7 +55,7 @@ public class SubstationServiceImpl implements SubstationService {
55 public void edit(Substation substation) { 55 public void edit(Substation substation) {
56 Substation existing = substationMapper.selectById(substation.getId()); 56 Substation existing = substationMapper.selectById(substation.getId());
57 if (existing == null) throw new BizException("分站管理员不存在"); 57 if (existing == null) throw new BizException("分站管理员不存在");
58 - roleScopeGuardService.requireRole(substation.getRoleId(), AdminRoleScopeEnum.SUBSTATION.name()); 58 + roleScopeGuardService.requireRole(substation.getRoleId(), AdminRoleScopeEnum.SUBSTATION.name(), existing.getCityId());
59 // 密码为空则不更新 59 // 密码为空则不更新
60 if (substation.getUserPass() == null || substation.getUserPass().isBlank()) { 60 if (substation.getUserPass() == null || substation.getUserPass().isBlank()) {
61 substation.setUserPass(null); 61 substation.setUserPass(null);
src/main/java/com/diligrp/rider/service/impl/SystemRoleMenuServiceImpl.java
@@ -30,10 +30,47 @@ public class SystemRoleMenuServiceImpl implements SystemRoleMenuService { @@ -30,10 +30,47 @@ public class SystemRoleMenuServiceImpl implements SystemRoleMenuService {
30 private final SysRoleMenuMapper sysRoleMenuMapper; 30 private final SysRoleMenuMapper sysRoleMenuMapper;
31 private final SystemMenuServiceImpl systemMenuService; 31 private final SystemMenuServiceImpl systemMenuService;
32 32
  33 + // ----------------------------------------------------------------
  34 + // 平台侧:不限菜单 scope(平台管理员可分配所有菜单)
  35 + // ----------------------------------------------------------------
  36 +
33 @Override 37 @Override
34 public List<AdminRoleMenuTreeVO> getRoleMenuTree(Long roleId) { 38 public List<AdminRoleMenuTreeVO> getRoleMenuTree(Long roleId) {
35 - SysRole role = requireRole(roleId); 39 + SysRole role = requireRole(roleId, null);
  40 + List<SysMenu> allowedMenus = filterMenusByScope(systemMenuService.listAllMenus(), role);
  41 + return buildCheckedTree(roleId, allowedMenus);
  42 + }
  43 +
  44 + @Override
  45 + @Transactional
  46 + public void assignMenus(Long roleId, AdminRoleMenuAssignDTO dto) {
  47 + SysRole role = requireRole(roleId, null);
36 List<SysMenu> allowedMenus = filterMenusByScope(systemMenuService.listAllMenus(), role); 48 List<SysMenu> allowedMenus = filterMenusByScope(systemMenuService.listAllMenus(), role);
  49 + doAssignMenus(roleId, dto, allowedMenus);
  50 + }
  51 +
  52 + // ----------------------------------------------------------------
  53 + // 分站侧:仅允许 SUBSTATION / BOTH scope 的菜单,且校验角色归属 cityId
  54 + // ----------------------------------------------------------------
  55 +
  56 + public List<AdminRoleMenuTreeVO> getRoleMenuTreeForCity(Long roleId, Long cityId) {
  57 + SysRole role = requireRole(roleId, cityId);
  58 + List<SysMenu> allowedMenus = filterSubstationMenus(systemMenuService.listAllMenus());
  59 + return buildCheckedTree(roleId, allowedMenus);
  60 + }
  61 +
  62 + @Transactional
  63 + public void assignMenusForCity(Long roleId, AdminRoleMenuAssignDTO dto, Long cityId) {
  64 + SysRole role = requireRole(roleId, cityId);
  65 + List<SysMenu> allowedMenus = filterSubstationMenus(systemMenuService.listAllMenus());
  66 + doAssignMenus(roleId, dto, allowedMenus);
  67 + }
  68 +
  69 + // ----------------------------------------------------------------
  70 + // 私有工具
  71 + // ----------------------------------------------------------------
  72 +
  73 + private List<AdminRoleMenuTreeVO> buildCheckedTree(Long roleId, List<SysMenu> allowedMenus) {
37 List<SysRoleMenu> assigned = sysRoleMenuMapper.selectList(new LambdaQueryWrapper<SysRoleMenu>() 74 List<SysRoleMenu> assigned = sysRoleMenuMapper.selectList(new LambdaQueryWrapper<SysRoleMenu>()
38 .eq(SysRoleMenu::getRoleId, roleId)); 75 .eq(SysRoleMenu::getRoleId, roleId));
39 Set<Long> assignedIds = assigned.stream().map(SysRoleMenu::getMenuId).collect(Collectors.toSet()); 76 Set<Long> assignedIds = assigned.stream().map(SysRoleMenu::getMenuId).collect(Collectors.toSet());
@@ -44,11 +81,7 @@ public class SystemRoleMenuServiceImpl implements SystemRoleMenuService { @@ -44,11 +81,7 @@ public class SystemRoleMenuServiceImpl implements SystemRoleMenuService {
44 return systemMenuService.buildTree(allowedMenus, checkedMap, false); 81 return systemMenuService.buildTree(allowedMenus, checkedMap, false);
45 } 82 }
46 83
47 - @Override  
48 - @Transactional  
49 - public void assignMenus(Long roleId, AdminRoleMenuAssignDTO dto) {  
50 - SysRole role = requireRole(roleId);  
51 - List<SysMenu> allowedMenus = filterMenusByScope(systemMenuService.listAllMenus(), role); 84 + private void doAssignMenus(Long roleId, AdminRoleMenuAssignDTO dto, List<SysMenu> allowedMenus) {
52 Set<Long> allowedIds = allowedMenus.stream().map(SysMenu::getId).collect(Collectors.toSet()); 85 Set<Long> allowedIds = allowedMenus.stream().map(SysMenu::getId).collect(Collectors.toSet());
53 List<Long> menuIds = dto.getMenuIds() == null ? List.of() : dto.getMenuIds(); 86 List<Long> menuIds = dto.getMenuIds() == null ? List.of() : dto.getMenuIds();
54 for (Long menuId : menuIds) { 87 for (Long menuId : menuIds) {
@@ -56,7 +89,6 @@ public class SystemRoleMenuServiceImpl implements SystemRoleMenuService { @@ -56,7 +89,6 @@ public class SystemRoleMenuServiceImpl implements SystemRoleMenuService {
56 throw new BizException("存在不允许分配的菜单"); 89 throw new BizException("存在不允许分配的菜单");
57 } 90 }
58 } 91 }
59 -  
60 sysRoleMenuMapper.delete(new LambdaQueryWrapper<SysRoleMenu>() 92 sysRoleMenuMapper.delete(new LambdaQueryWrapper<SysRoleMenu>()
61 .eq(SysRoleMenu::getRoleId, roleId)); 93 .eq(SysRoleMenu::getRoleId, roleId));
62 long now = System.currentTimeMillis() / 1000; 94 long now = System.currentTimeMillis() / 1000;
@@ -69,11 +101,15 @@ public class SystemRoleMenuServiceImpl implements SystemRoleMenuService { @@ -69,11 +101,15 @@ public class SystemRoleMenuServiceImpl implements SystemRoleMenuService {
69 } 101 }
70 } 102 }
71 103
72 - private SysRole requireRole(Long roleId) { 104 + private SysRole requireRole(Long roleId, Long cityId) {
73 SysRole role = sysRoleMapper.selectById(roleId); 105 SysRole role = sysRoleMapper.selectById(roleId);
74 if (role == null || role.getStatus() == null || role.getStatus() != 1) { 106 if (role == null || role.getStatus() == null || role.getStatus() != 1) {
75 throw new BizException("角色不存在或已禁用"); 107 throw new BizException("角色不存在或已禁用");
76 } 108 }
  109 + // cityId != null 时校验归属(分站侧调用)
  110 + if (cityId != null && !cityId.equals(role.getCityId())) {
  111 + throw new BizException("无权操作其他租户的角色");
  112 + }
77 return role; 113 return role;
78 } 114 }
79 115
@@ -81,6 +117,14 @@ public class SystemRoleMenuServiceImpl implements SystemRoleMenuService { @@ -81,6 +117,14 @@ public class SystemRoleMenuServiceImpl implements SystemRoleMenuService {
81 return menus.stream().filter(menu -> isScopeAllowed(role, menu)).collect(Collectors.toList()); 117 return menus.stream().filter(menu -> isScopeAllowed(role, menu)).collect(Collectors.toList());
82 } 118 }
83 119
  120 + /** 分站侧:只允许 SUBSTATION 或 BOTH scope 的菜单 */
  121 + private List<SysMenu> filterSubstationMenus(List<SysMenu> menus) {
  122 + return menus.stream().filter(menu ->
  123 + MenuScopeEnum.SUBSTATION.name().equals(menu.getMenuScope())
  124 + || MenuScopeEnum.BOTH.name().equals(menu.getMenuScope())
  125 + ).collect(Collectors.toList());
  126 + }
  127 +
84 private boolean isScopeAllowed(SysRole role, SysMenu menu) { 128 private boolean isScopeAllowed(SysRole role, SysMenu menu) {
85 if (MenuScopeEnum.BOTH.name().equals(menu.getMenuScope())) { 129 if (MenuScopeEnum.BOTH.name().equals(menu.getMenuScope())) {
86 return true; 130 return true;
src/main/java/com/diligrp/rider/service/impl/SystemRoleServiceImpl.java
@@ -27,6 +27,7 @@ import java.util.Set; @@ -27,6 +27,7 @@ import java.util.Set;
27 @RequiredArgsConstructor 27 @RequiredArgsConstructor
28 public class SystemRoleServiceImpl implements SystemRoleService { 28 public class SystemRoleServiceImpl implements SystemRoleService {
29 29
  30 + /** 内置角色编码(city_id=0 且 code 在此集合内,不允许编辑/删除) */
30 private static final Set<String> BUILT_IN_CODES = Set.of("platform_admin", "substation_admin"); 31 private static final Set<String> BUILT_IN_CODES = Set.of("platform_admin", "substation_admin");
31 32
32 private final SysRoleMapper sysRoleMapper; 33 private final SysRoleMapper sysRoleMapper;
@@ -34,38 +35,33 @@ public class SystemRoleServiceImpl implements SystemRoleService { @@ -34,38 +35,33 @@ public class SystemRoleServiceImpl implements SystemRoleService {
34 private final AdminUserMapper adminUserMapper; 35 private final AdminUserMapper adminUserMapper;
35 private final SubstationMapper substationMapper; 36 private final SubstationMapper substationMapper;
36 37
  38 + // ----------------------------------------------------------------
  39 + // 平台侧:只看/操作 city_id=0 的全局角色
  40 + // ----------------------------------------------------------------
  41 +
37 @Override 42 @Override
38 public List<AdminRoleVO> list(boolean includeDisabled) { 43 public List<AdminRoleVO> list(boolean includeDisabled) {
39 List<SysRole> roles = sysRoleMapper.selectList(new LambdaQueryWrapper<SysRole>() 44 List<SysRole> roles = sysRoleMapper.selectList(new LambdaQueryWrapper<SysRole>()
  45 + .eq(SysRole::getCityId, 0L)
40 .eq(!includeDisabled, SysRole::getStatus, 1) 46 .eq(!includeDisabled, SysRole::getStatus, 1)
41 .orderByAsc(SysRole::getId)); 47 .orderByAsc(SysRole::getId));
42 - List<AdminRoleVO> result = new ArrayList<>();  
43 - for (SysRole role : roles) {  
44 - result.add(toVO(role));  
45 - }  
46 - return result; 48 + return toVOList(roles);
47 } 49 }
48 50
49 @Override 51 @Override
50 public void add(AdminRoleSaveDTO dto) { 52 public void add(AdminRoleSaveDTO dto) {
51 validateScope(dto.getRoleScope()); 53 validateScope(dto.getRoleScope());
52 - ensureUniqueCode(dto.getCode(), null);  
53 -  
54 - SysRole role = new SysRole();  
55 - role.setCode(dto.getCode().trim());  
56 - role.setName(dto.getName().trim());  
57 - role.setRoleScope(dto.getRoleScope());  
58 - role.setStatus(1);  
59 - role.setCreateTime(System.currentTimeMillis() / 1000); 54 + ensureUniqueCode(dto.getCode(), null, 0L);
  55 + SysRole role = buildRole(dto, 0L);
60 sysRoleMapper.insert(role); 56 sysRoleMapper.insert(role);
61 } 57 }
62 58
63 @Override 59 @Override
64 public void edit(AdminRoleSaveDTO dto) { 60 public void edit(AdminRoleSaveDTO dto) {
65 - if (dto.getId() == null || dto.getId() < 1) {  
66 - throw new BizException("角色ID不能为空");  
67 - }  
68 SysRole role = requireRole(dto.getId()); 61 SysRole role = requireRole(dto.getId());
  62 + if (!role.getCityId().equals(0L)) {
  63 + throw new BizException("平台侧不允许编辑分站专属角色");
  64 + }
69 if (isBuiltIn(role)) { 65 if (isBuiltIn(role)) {
70 throw new BizException("内置角色不允许编辑"); 66 throw new BizException("内置角色不允许编辑");
71 } 67 }
@@ -73,7 +69,7 @@ public class SystemRoleServiceImpl implements SystemRoleService { @@ -73,7 +69,7 @@ public class SystemRoleServiceImpl implements SystemRoleService {
73 if (!role.getRoleScope().equals(dto.getRoleScope())) { 69 if (!role.getRoleScope().equals(dto.getRoleScope())) {
74 throw new BizException("角色范围不允许修改"); 70 throw new BizException("角色范围不允许修改");
75 } 71 }
76 - ensureUniqueCode(dto.getCode(), role.getId()); 72 + ensureUniqueCode(dto.getCode(), role.getId(), 0L);
77 role.setCode(dto.getCode().trim()); 73 role.setCode(dto.getCode().trim());
78 role.setName(dto.getName().trim()); 74 role.setName(dto.getName().trim());
79 sysRoleMapper.updateById(role); 75 sysRoleMapper.updateById(role);
@@ -82,27 +78,34 @@ public class SystemRoleServiceImpl implements SystemRoleService { @@ -82,27 +78,34 @@ public class SystemRoleServiceImpl implements SystemRoleService {
82 @Override 78 @Override
83 public void ban(Long id) { 79 public void ban(Long id) {
84 SysRole role = requireRole(id); 80 SysRole role = requireRole(id);
  81 + if (!role.getCityId().equals(0L)) {
  82 + throw new BizException("平台侧不允许操作分站专属角色");
  83 + }
85 if (isBuiltIn(role)) { 84 if (isBuiltIn(role)) {
86 throw new BizException("内置角色不允许禁用"); 85 throw new BizException("内置角色不允许禁用");
87 } 86 }
88 ensureNotBound(role.getId(), "当前角色已绑定账号,不能禁用"); 87 ensureNotBound(role.getId(), "当前角色已绑定账号,不能禁用");
89 sysRoleMapper.update(null, new LambdaUpdateWrapper<SysRole>() 88 sysRoleMapper.update(null, new LambdaUpdateWrapper<SysRole>()
90 - .eq(SysRole::getId, id)  
91 - .set(SysRole::getStatus, 0)); 89 + .eq(SysRole::getId, id).set(SysRole::getStatus, 0));
92 } 90 }
93 91
94 @Override 92 @Override
95 public void cancelBan(Long id) { 93 public void cancelBan(Long id) {
96 - requireRole(id); 94 + SysRole role = requireRole(id);
  95 + if (!role.getCityId().equals(0L)) {
  96 + throw new BizException("平台侧不允许操作分站专属角色");
  97 + }
97 sysRoleMapper.update(null, new LambdaUpdateWrapper<SysRole>() 98 sysRoleMapper.update(null, new LambdaUpdateWrapper<SysRole>()
98 - .eq(SysRole::getId, id)  
99 - .set(SysRole::getStatus, 1)); 99 + .eq(SysRole::getId, id).set(SysRole::getStatus, 1));
100 } 100 }
101 101
102 @Override 102 @Override
103 @Transactional 103 @Transactional
104 public void del(Long id) { 104 public void del(Long id) {
105 SysRole role = requireRole(id); 105 SysRole role = requireRole(id);
  106 + if (!role.getCityId().equals(0L)) {
  107 + throw new BizException("平台侧不允许删除分站专属角色");
  108 + }
106 if (isBuiltIn(role)) { 109 if (isBuiltIn(role)) {
107 throw new BizException("内置角色不允许删除"); 110 throw new BizException("内置角色不允许删除");
108 } 111 }
@@ -112,6 +115,72 @@ public class SystemRoleServiceImpl implements SystemRoleService { @@ -112,6 +115,72 @@ public class SystemRoleServiceImpl implements SystemRoleService {
112 sysRoleMapper.deleteById(role.getId()); 115 sysRoleMapper.deleteById(role.getId());
113 } 116 }
114 117
  118 + // ----------------------------------------------------------------
  119 + // 分站侧:只看/操作本 cityId 的角色
  120 + // ----------------------------------------------------------------
  121 +
  122 + @Override
  123 + public List<AdminRoleVO> listByCityId(Long cityId) {
  124 + List<SysRole> roles = sysRoleMapper.selectList(new LambdaQueryWrapper<SysRole>()
  125 + .eq(SysRole::getCityId, cityId)
  126 + .eq(SysRole::getStatus, 1)
  127 + .orderByAsc(SysRole::getId));
  128 + return toVOList(roles);
  129 + }
  130 +
  131 + @Override
  132 + public void addForCity(AdminRoleSaveDTO dto, Long cityId) {
  133 + // 分站角色只能是 SUBSTATION scope
  134 + if (!AdminRoleScopeEnum.SUBSTATION.name().equals(dto.getRoleScope())) {
  135 + throw new BizException("分站角色范围只能是 SUBSTATION");
  136 + }
  137 + ensureUniqueCode(dto.getCode(), null, cityId);
  138 + SysRole role = buildRole(dto, cityId);
  139 + sysRoleMapper.insert(role);
  140 + }
  141 +
  142 + @Override
  143 + public void editForCity(AdminRoleSaveDTO dto, Long cityId) {
  144 + SysRole role = requireRole(dto.getId());
  145 + requireOwnedByCity(role, cityId);
  146 + ensureUniqueCode(dto.getCode(), role.getId(), cityId);
  147 + role.setCode(dto.getCode().trim());
  148 + role.setName(dto.getName().trim());
  149 + sysRoleMapper.updateById(role);
  150 + }
  151 +
  152 + @Override
  153 + public void banForCity(Long id, Long cityId) {
  154 + SysRole role = requireRole(id);
  155 + requireOwnedByCity(role, cityId);
  156 + ensureNotBoundForCity(role.getId(), cityId, "当前角色已绑定账号,不能禁用");
  157 + sysRoleMapper.update(null, new LambdaUpdateWrapper<SysRole>()
  158 + .eq(SysRole::getId, id).set(SysRole::getStatus, 0));
  159 + }
  160 +
  161 + @Override
  162 + public void cancelBanForCity(Long id, Long cityId) {
  163 + SysRole role = requireRole(id);
  164 + requireOwnedByCity(role, cityId);
  165 + sysRoleMapper.update(null, new LambdaUpdateWrapper<SysRole>()
  166 + .eq(SysRole::getId, id).set(SysRole::getStatus, 1));
  167 + }
  168 +
  169 + @Override
  170 + @Transactional
  171 + public void delForCity(Long id, Long cityId) {
  172 + SysRole role = requireRole(id);
  173 + requireOwnedByCity(role, cityId);
  174 + ensureNotBoundForCity(role.getId(), cityId, "当前角色已绑定账号,不能删除");
  175 + sysRoleMenuMapper.delete(new LambdaQueryWrapper<SysRoleMenu>()
  176 + .eq(SysRoleMenu::getRoleId, role.getId()));
  177 + sysRoleMapper.deleteById(role.getId());
  178 + }
  179 +
  180 + // ----------------------------------------------------------------
  181 + // 私有工具方法
  182 + // ----------------------------------------------------------------
  183 +
115 private SysRole requireRole(Long id) { 184 private SysRole requireRole(Long id) {
116 if (id == null || id < 1) { 185 if (id == null || id < 1) {
117 throw new BizException("角色ID不能为空"); 186 throw new BizException("角色ID不能为空");
@@ -123,6 +192,12 @@ public class SystemRoleServiceImpl implements SystemRoleService { @@ -123,6 +192,12 @@ public class SystemRoleServiceImpl implements SystemRoleService {
123 return role; 192 return role;
124 } 193 }
125 194
  195 + private void requireOwnedByCity(SysRole role, Long cityId) {
  196 + if (!cityId.equals(role.getCityId())) {
  197 + throw new BizException("无权操作其他租户的角色");
  198 + }
  199 + }
  200 +
126 private void validateScope(String scope) { 201 private void validateScope(String scope) {
127 for (AdminRoleScopeEnum value : AdminRoleScopeEnum.values()) { 202 for (AdminRoleScopeEnum value : AdminRoleScopeEnum.values()) {
128 if (value.name().equals(scope)) { 203 if (value.name().equals(scope)) {
@@ -132,9 +207,10 @@ public class SystemRoleServiceImpl implements SystemRoleService { @@ -132,9 +207,10 @@ public class SystemRoleServiceImpl implements SystemRoleService {
132 throw new BizException("角色范围不合法"); 207 throw new BizException("角色范围不合法");
133 } 208 }
134 209
135 - private void ensureUniqueCode(String code, Long excludeId) { 210 + private void ensureUniqueCode(String code, Long excludeId, Long cityId) {
136 SysRole duplicate = sysRoleMapper.selectOne(new LambdaQueryWrapper<SysRole>() 211 SysRole duplicate = sysRoleMapper.selectOne(new LambdaQueryWrapper<SysRole>()
137 .eq(SysRole::getCode, code.trim()) 212 .eq(SysRole::getCode, code.trim())
  213 + .eq(SysRole::getCityId, cityId)
138 .ne(excludeId != null, SysRole::getId, excludeId) 214 .ne(excludeId != null, SysRole::getId, excludeId)
139 .last("LIMIT 1")); 215 .last("LIMIT 1"));
140 if (duplicate != null) { 216 if (duplicate != null) {
@@ -148,6 +224,34 @@ public class SystemRoleServiceImpl implements SystemRoleService { @@ -148,6 +224,34 @@ public class SystemRoleServiceImpl implements SystemRoleService {
148 } 224 }
149 } 225 }
150 226
  227 + private void ensureNotBoundForCity(Long roleId, Long cityId, String message) {
  228 + Long count = substationMapper.selectCount(new LambdaQueryWrapper<Substation>()
  229 + .eq(Substation::getRoleId, roleId)
  230 + .eq(Substation::getCityId, cityId));
  231 + if (count != null && count > 0) {
  232 + throw new BizException(message);
  233 + }
  234 + }
  235 +
  236 + private SysRole buildRole(AdminRoleSaveDTO dto, Long cityId) {
  237 + SysRole role = new SysRole();
  238 + role.setCode(dto.getCode().trim());
  239 + role.setName(dto.getName().trim());
  240 + role.setRoleScope(dto.getRoleScope());
  241 + role.setCityId(cityId);
  242 + role.setStatus(1);
  243 + role.setCreateTime(System.currentTimeMillis() / 1000);
  244 + return role;
  245 + }
  246 +
  247 + private List<AdminRoleVO> toVOList(List<SysRole> roles) {
  248 + List<AdminRoleVO> result = new ArrayList<>();
  249 + for (SysRole role : roles) {
  250 + result.add(toVO(role));
  251 + }
  252 + return result;
  253 + }
  254 +
151 private AdminRoleVO toVO(SysRole role) { 255 private AdminRoleVO toVO(SysRole role) {
152 AdminRoleVO vo = new AdminRoleVO(); 256 AdminRoleVO vo = new AdminRoleVO();
153 vo.setId(role.getId()); 257 vo.setId(role.getId());
@@ -163,7 +267,9 @@ public class SystemRoleServiceImpl implements SystemRoleService { @@ -163,7 +267,9 @@ public class SystemRoleServiceImpl implements SystemRoleService {
163 } 267 }
164 268
165 private boolean isBuiltIn(SysRole role) { 269 private boolean isBuiltIn(SysRole role) {
166 - return BUILT_IN_CODES.contains(role.getCode()); 270 + // 只有全局角色(city_id=0)且编码匹配才是内置角色
  271 + return role.getCityId() != null && role.getCityId() == 0L
  272 + && BUILT_IN_CODES.contains(role.getCode());
167 } 273 }
168 274
169 private long countAdminUsers(Long roleId) { 275 private long countAdminUsers(Long roleId) {
src/main/resources/schema.sql
@@ -379,10 +379,12 @@ CREATE TABLE `sys_role` ( @@ -379,10 +379,12 @@ CREATE TABLE `sys_role` (
379 `code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '角色编码', 379 `code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '角色编码',
380 `name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '角色名称', 380 `name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '角色名称',
381 `role_scope` VARCHAR(32) NOT NULL DEFAULT 'PLATFORM' COMMENT '角色归属:PLATFORM/SUBSTATION', 381 `role_scope` VARCHAR(32) NOT NULL DEFAULT 'PLATFORM' COMMENT '角色归属:PLATFORM/SUBSTATION',
  382 + `city_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '所属租户ID:0=平台全局角色,>0=分站租户专属角色',
382 `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=正常', 383 `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=正常',
383 `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0, 384 `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
384 PRIMARY KEY (`id`), 385 PRIMARY KEY (`id`),
385 - UNIQUE KEY `uk_role_code` (`code`) 386 + UNIQUE KEY `uk_role_code` (`code`, `city_id`),
  387 + KEY `idx_city_scope` (`city_id`, `role_scope`)
386 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台菜单角色表'; 388 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台菜单角色表';
387 389
388 CREATE TABLE `sys_menu` ( 390 CREATE TABLE `sys_menu` (