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 18  
19 19 private String roleScope;
20 20  
  21 + /** 所属租户ID:0=平台全局角色,>0=分站租户专属角色 */
  22 + private Long cityId;
  23 +
21 24 private Integer status;
22 25  
23 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  
5 5 public interface RoleScopeGuardService {
6 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 6 import java.util.List;
7 7  
8 8 public interface SystemRoleService {
  9 + /** 平台侧调用:只返回平台全局角色(city_id=0) */
9 10 List<AdminRoleVO> list(boolean includeDisabled);
10 11  
  12 + /** 分站侧调用:只返回该租户自己的角色(city_id=cityId) */
  13 + List<AdminRoleVO> listByCityId(Long cityId);
  14 +
  15 + /** 平台侧新增角色(city_id=0) */
11 16 void add(AdminRoleSaveDTO dto);
12 17  
  18 + /** 分站侧新增角色(city_id=cityId) */
  19 + void addForCity(AdminRoleSaveDTO dto, Long cityId);
  20 +
  21 + /** 平台侧编辑(只能编辑 city_id=0 的角色) */
13 22 void edit(AdminRoleSaveDTO dto);
14 23  
  24 + /** 分站侧编辑(只能编辑本 cityId 的角色) */
  25 + void editForCity(AdminRoleSaveDTO dto, Long cityId);
  26 +
15 27 void ban(Long id);
16 28  
  29 + /** 分站侧禁用(校验 cityId 归属) */
  30 + void banForCity(Long id, Long cityId);
  31 +
17 32 void cancelBan(Long id);
18 33  
  34 + /** 分站侧启用(校验 cityId 归属) */
  35 + void cancelBanForCity(Long id, Long cityId);
  36 +
19 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 47 role = sysRoleMapper.selectById(roleId);
48 48 }
49 49 if (role == null) {
  50 + // Fallback:找全局内置角色(city_id=0)
50 51 role = sysRoleMapper.selectOne(new LambdaQueryWrapper<SysRole>()
51 52 .eq(SysRole::getCode, fallbackCode)
  53 + .eq(SysRole::getCityId, 0L)
52 54 .eq(SysRole::getStatus, 1)
53 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 40 if (exists > 0) {
41 41 throw new BizException("账号已存在,请更换");
42 42 }
43   - roleScopeGuardService.requireRole(adminUser.getRoleId(), AdminRoleScopeEnum.PLATFORM.name());
  43 + roleScopeGuardService.requireRole(adminUser.getRoleId(), AdminRoleScopeEnum.PLATFORM.name(), 0L);
44 44 adminUser.setUserPass(encryptPass(adminUser.getUserPass()));
45 45 adminUser.setUserStatus(1);
46 46 adminUser.setCreateTime(System.currentTimeMillis() / 1000);
... ... @@ -53,7 +53,7 @@ public class AdminUserManageServiceImpl implements AdminUserManageService {
53 53 if (existing == null) {
54 54 throw new BizException("平台账号不存在");
55 55 }
56   - roleScopeGuardService.requireRole(adminUser.getRoleId(), AdminRoleScopeEnum.PLATFORM.name());
  56 + roleScopeGuardService.requireRole(adminUser.getRoleId(), AdminRoleScopeEnum.PLATFORM.name(), 0L);
57 57 if (adminUser.getUserPass() == null || adminUser.getUserPass().isBlank()) {
58 58 adminUser.setUserPass(null);
59 59 } else {
... ...
src/main/java/com/diligrp/rider/service/impl/MenuBootstrapServiceImpl.java
... ... @@ -60,10 +60,12 @@ public class MenuBootstrapServiceImpl implements MenuBootstrapService {
60 60 defaults.add(menu("dashboard", "工作台", "MENU", "/dashboard", "HomeOutlined", 0L, MenuScopeEnum.BOTH, 10));
61 61 defaults.add(menu("city.manage", "租户管理", "MENU", "/city", "GlobalOutlined", 0L, MenuScopeEnum.PLATFORM, 20));
62 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 64 defaults.add(menu("merchant.root", "商家管理", "DIR", "", "ShopOutlined", 0L, MenuScopeEnum.PLATFORM, 40));
64 65 defaults.add(menu("merchant.enter", "入驻申请", "MENU", "/merchant/enter", "", 0L, MenuScopeEnum.PLATFORM, 41));
65 66 defaults.add(menu("merchant.store", "店铺管理", "MENU", "/merchant/store", "", 0L, MenuScopeEnum.PLATFORM, 42));
66 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 69 defaults.add(menu("rider.evaluate", "骑手评价", "MENU", "/rider/evaluate", "StarOutlined", 0L, MenuScopeEnum.BOTH, 60));
68 70 defaults.add(menu("order.root", "订单管理", "DIR", "", "UnorderedListOutlined", 0L, MenuScopeEnum.BOTH, 70));
69 71 defaults.add(menu("order.list", "订单列表", "MENU", "/order", "", 0L, MenuScopeEnum.BOTH, 71));
... ... @@ -80,6 +82,10 @@ public class MenuBootstrapServiceImpl implements MenuBootstrapService {
80 82 defaults.add(menu("system.role", "角色管理", "MENU", "/system/role", "", 0L, MenuScopeEnum.PLATFORM, 102));
81 83 defaults.add(menu("system.role_menu", "角色菜单", "MENU", "/system/role-menu", "", 0L, MenuScopeEnum.PLATFORM, 103));
82 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 90 Map<String, SysMenu> persisted = new LinkedHashMap<>();
85 91 for (SysMenu menu : defaults) {
... ... @@ -111,6 +117,11 @@ public class MenuBootstrapServiceImpl implements MenuBootstrapService {
111 117 persisted.get("system.role_menu").setListOrder(103);
112 118 persisted.get("admin.user").setParentId(persisted.get("system.root").getId());
113 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 126 for (SysMenu menu : persisted.values()) {
116 127 sysMenuMapper.updateById(menu);
... ...
src/main/java/com/diligrp/rider/service/impl/RoleScopeGuardServiceImpl.java
... ... @@ -16,6 +16,11 @@ public class RoleScopeGuardServiceImpl implements RoleScopeGuardService {
16 16  
17 17 @Override
18 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 24 if (roleId == null || roleId < 1) {
20 25 throw new BizException("角色不能为空");
21 26 }
... ... @@ -29,6 +34,14 @@ public class RoleScopeGuardServiceImpl implements RoleScopeGuardService {
29 34 if (!requiredScope.equals(role.getRoleScope())) {
30 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 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 44 .eq(Substation::getUserLogin, substation.getUserLogin()));
45 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 48 substation.setUserPass(encryptPass(substation.getUserPass()));
49 49 substation.setUserStatus(1);
50 50 substation.setCreateTime(System.currentTimeMillis() / 1000);
... ... @@ -55,7 +55,7 @@ public class SubstationServiceImpl implements SubstationService {
55 55 public void edit(Substation substation) {
56 56 Substation existing = substationMapper.selectById(substation.getId());
57 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 60 if (substation.getUserPass() == null || substation.getUserPass().isBlank()) {
61 61 substation.setUserPass(null);
... ...
src/main/java/com/diligrp/rider/service/impl/SystemRoleMenuServiceImpl.java
... ... @@ -30,10 +30,47 @@ public class SystemRoleMenuServiceImpl implements SystemRoleMenuService {
30 30 private final SysRoleMenuMapper sysRoleMenuMapper;
31 31 private final SystemMenuServiceImpl systemMenuService;
32 32  
  33 + // ----------------------------------------------------------------
  34 + // 平台侧:不限菜单 scope(平台管理员可分配所有菜单)
  35 + // ----------------------------------------------------------------
  36 +
33 37 @Override
34 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 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 74 List<SysRoleMenu> assigned = sysRoleMenuMapper.selectList(new LambdaQueryWrapper<SysRoleMenu>()
38 75 .eq(SysRoleMenu::getRoleId, roleId));
39 76 Set<Long> assignedIds = assigned.stream().map(SysRoleMenu::getMenuId).collect(Collectors.toSet());
... ... @@ -44,11 +81,7 @@ public class SystemRoleMenuServiceImpl implements SystemRoleMenuService {
44 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 85 Set<Long> allowedIds = allowedMenus.stream().map(SysMenu::getId).collect(Collectors.toSet());
53 86 List<Long> menuIds = dto.getMenuIds() == null ? List.of() : dto.getMenuIds();
54 87 for (Long menuId : menuIds) {
... ... @@ -56,7 +89,6 @@ public class SystemRoleMenuServiceImpl implements SystemRoleMenuService {
56 89 throw new BizException("存在不允许分配的菜单");
57 90 }
58 91 }
59   -
60 92 sysRoleMenuMapper.delete(new LambdaQueryWrapper<SysRoleMenu>()
61 93 .eq(SysRoleMenu::getRoleId, roleId));
62 94 long now = System.currentTimeMillis() / 1000;
... ... @@ -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 105 SysRole role = sysRoleMapper.selectById(roleId);
74 106 if (role == null || role.getStatus() == null || role.getStatus() != 1) {
75 107 throw new BizException("角色不存在或已禁用");
76 108 }
  109 + // cityId != null 时校验归属(分站侧调用)
  110 + if (cityId != null && !cityId.equals(role.getCityId())) {
  111 + throw new BizException("无权操作其他租户的角色");
  112 + }
77 113 return role;
78 114 }
79 115  
... ... @@ -81,6 +117,14 @@ public class SystemRoleMenuServiceImpl implements SystemRoleMenuService {
81 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 128 private boolean isScopeAllowed(SysRole role, SysMenu menu) {
85 129 if (MenuScopeEnum.BOTH.name().equals(menu.getMenuScope())) {
86 130 return true;
... ...
src/main/java/com/diligrp/rider/service/impl/SystemRoleServiceImpl.java
... ... @@ -27,6 +27,7 @@ import java.util.Set;
27 27 @RequiredArgsConstructor
28 28 public class SystemRoleServiceImpl implements SystemRoleService {
29 29  
  30 + /** 内置角色编码(city_id=0 且 code 在此集合内,不允许编辑/删除) */
30 31 private static final Set<String> BUILT_IN_CODES = Set.of("platform_admin", "substation_admin");
31 32  
32 33 private final SysRoleMapper sysRoleMapper;
... ... @@ -34,38 +35,33 @@ public class SystemRoleServiceImpl implements SystemRoleService {
34 35 private final AdminUserMapper adminUserMapper;
35 36 private final SubstationMapper substationMapper;
36 37  
  38 + // ----------------------------------------------------------------
  39 + // 平台侧:只看/操作 city_id=0 的全局角色
  40 + // ----------------------------------------------------------------
  41 +
37 42 @Override
38 43 public List<AdminRoleVO> list(boolean includeDisabled) {
39 44 List<SysRole> roles = sysRoleMapper.selectList(new LambdaQueryWrapper<SysRole>()
  45 + .eq(SysRole::getCityId, 0L)
40 46 .eq(!includeDisabled, SysRole::getStatus, 1)
41 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 51 @Override
50 52 public void add(AdminRoleSaveDTO dto) {
51 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 56 sysRoleMapper.insert(role);
61 57 }
62 58  
63 59 @Override
64 60 public void edit(AdminRoleSaveDTO dto) {
65   - if (dto.getId() == null || dto.getId() < 1) {
66   - throw new BizException("角色ID不能为空");
67   - }
68 61 SysRole role = requireRole(dto.getId());
  62 + if (!role.getCityId().equals(0L)) {
  63 + throw new BizException("平台侧不允许编辑分站专属角色");
  64 + }
69 65 if (isBuiltIn(role)) {
70 66 throw new BizException("内置角色不允许编辑");
71 67 }
... ... @@ -73,7 +69,7 @@ public class SystemRoleServiceImpl implements SystemRoleService {
73 69 if (!role.getRoleScope().equals(dto.getRoleScope())) {
74 70 throw new BizException("角色范围不允许修改");
75 71 }
76   - ensureUniqueCode(dto.getCode(), role.getId());
  72 + ensureUniqueCode(dto.getCode(), role.getId(), 0L);
77 73 role.setCode(dto.getCode().trim());
78 74 role.setName(dto.getName().trim());
79 75 sysRoleMapper.updateById(role);
... ... @@ -82,27 +78,34 @@ public class SystemRoleServiceImpl implements SystemRoleService {
82 78 @Override
83 79 public void ban(Long id) {
84 80 SysRole role = requireRole(id);
  81 + if (!role.getCityId().equals(0L)) {
  82 + throw new BizException("平台侧不允许操作分站专属角色");
  83 + }
85 84 if (isBuiltIn(role)) {
86 85 throw new BizException("内置角色不允许禁用");
87 86 }
88 87 ensureNotBound(role.getId(), "当前角色已绑定账号,不能禁用");
89 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 92 @Override
95 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 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 102 @Override
103 103 @Transactional
104 104 public void del(Long id) {
105 105 SysRole role = requireRole(id);
  106 + if (!role.getCityId().equals(0L)) {
  107 + throw new BizException("平台侧不允许删除分站专属角色");
  108 + }
106 109 if (isBuiltIn(role)) {
107 110 throw new BizException("内置角色不允许删除");
108 111 }
... ... @@ -112,6 +115,72 @@ public class SystemRoleServiceImpl implements SystemRoleService {
112 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 184 private SysRole requireRole(Long id) {
116 185 if (id == null || id < 1) {
117 186 throw new BizException("角色ID不能为空");
... ... @@ -123,6 +192,12 @@ public class SystemRoleServiceImpl implements SystemRoleService {
123 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 201 private void validateScope(String scope) {
127 202 for (AdminRoleScopeEnum value : AdminRoleScopeEnum.values()) {
128 203 if (value.name().equals(scope)) {
... ... @@ -132,9 +207,10 @@ public class SystemRoleServiceImpl implements SystemRoleService {
132 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 211 SysRole duplicate = sysRoleMapper.selectOne(new LambdaQueryWrapper<SysRole>()
137 212 .eq(SysRole::getCode, code.trim())
  213 + .eq(SysRole::getCityId, cityId)
138 214 .ne(excludeId != null, SysRole::getId, excludeId)
139 215 .last("LIMIT 1"));
140 216 if (duplicate != null) {
... ... @@ -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 255 private AdminRoleVO toVO(SysRole role) {
152 256 AdminRoleVO vo = new AdminRoleVO();
153 257 vo.setId(role.getId());
... ... @@ -163,7 +267,9 @@ public class SystemRoleServiceImpl implements SystemRoleService {
163 267 }
164 268  
165 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 275 private long countAdminUsers(Long roleId) {
... ...
src/main/resources/schema.sql
... ... @@ -379,10 +379,12 @@ CREATE TABLE `sys_role` (
379 379 `code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '角色编码',
380 380 `name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '角色名称',
381 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 383 `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=正常',
383 384 `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
384 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 388 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台菜单角色表';
387 389  
388 390 CREATE TABLE `sys_menu` (
... ...