You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

13 KiB

角色管理模块

Status: ready-for-agent

Problem Statement

现有角色管理散落在 SystemController/api/system/roles/* 端点中,仅实现了基础 CRUD + 菜单分配,缺少祖先补全(ADR-0005)、校验规则、权限控制、角色详情接口和内置角色保护。ADR-0005 要求前端只传用户勾选的叶子节点 ID,后端保存前沿 parentId 向上遍历补全全部祖先 catalog/menu ID,确保菜单可见性链路完整。当前 SysRoleServiceImpl.assignMenus 直接先删后插,不做任何祖先补全,导致如果前端只传按钮节点,权限解析引擎建菜单树时祖先不在授权集合中,用户虽有按钮权限却看不到所在页面。

Solution

新建独立的 RoleController/api/roles/*),实现完整的角色管理闭环:分页查询、详情(含已补全的授权集合)、保存(带校验)、删除(级联清理)、权限分配(带祖先补全)。采用纯 hasAuthority 权限控制,通过幂等数据初始化器在权限资源树中创建角色管理的 button 权限点并分配给管理员角色。从 SystemController 移除全部角色端点。

User Stories

  1. 作为管理员,我希望分页查看角色列表,以便浏览和管理系统中的角色
  2. 作为管理员,我希望通过关键词搜索角色名称,以便快速定位特定角色
  3. 作为管理员,我希望查看角色详情包含基本信息和已补全的授权集合,以便了解角色当前持有哪些权限资源
  4. 作为管理员,我希望创建新角色并填写基本信息(名称、编码、数据范围、排序、备注),以便定义新的可分配角色
  5. 作为管理员,我希望编辑现有角色的基本信息,以便更新角色属性
  6. 作为管理员,我希望删除不再使用的角色,以便保持角色列表整洁
  7. 作为管理员,我希望通过勾选权限资源树节点来分配功能权限,以便控制该角色持有哪些权限点
  8. 作为管理员,我希望后端在保存功能权限时自动补全祖先节点,以便菜单可见性链路不会因遗漏祖先而断裂
  9. 作为管理员,我希望角色详情返回的授权集合是已补全的完整集合,以便前端直接渲染勾选状态无需二次补全
  10. 作为管理员,我希望被阻止删除内置角色,以便不会意外锁定系统导致不可恢复
  11. 作为管理员,我希望被阻止修改内置角色的编码,以便系统关键角色标识保持稳定
  12. 作为管理员,我希望角色编码唯一性被强制校验,以便两个角色不会共享同一编码导致权限解析异常
  13. 作为管理员,我希望数据范围值被校验在合法范围内,以便非法值不会导致角色被静默跳过
  14. 作为管理员,我希望删除角色时同时清理用户-角色关联,以便不留下指向不存在角色的孤儿关联记录
  15. 作为持有 crm:role:list 权限的非管理员用户,我希望能够查看角色列表,以便拥有只读角色访问能力
  16. 作为持有 crm:role:save 权限的非管理员用户,我希望能够保存角色信息,以便在无法删除角色的情况下仍可编辑
  17. 作为持有 crm:role:assign 权限的非管理员用户,我希望能够分配角色权限,以便管理角色授权而无需完整管理员权限
  18. 作为持有 crm:role:delete 权限的非管理员用户,我希望能够删除角色,以便管理角色生命周期
  19. 作为前端开发者,我希望权限分配端点只接收我勾选的原始节点 ID,以便不需要在前端实现祖先补全逻辑
  20. 作为前端开发者,我希望角色详情返回的授权集合是完整子树,以便直接用于设置勾选框状态
  21. 作为前端开发者,我希望权限资源树查询接口仍然可用,以便渲染勾选树的 UI 结构
  22. 作为前端开发者,我希望用户可见菜单树接口不受影响,以便登录后侧边栏渲染链路不被破坏
  23. 作为开发者,我希望数据初始化器是幂等的,以便首次启动和已有系统都能正确初始化权限数据
  24. 作为开发者,我希望内置角色标记由数据初始化器设置而非通过 API,以便防止通过接口将任意角色标记为内置
  25. 作为开发者,我希望角色管理的权限码被种子化到权限资源树中,以便部署后 hasAuthority 检查即可生效
  26. 作为开发者,我希望祖先补全逻辑作为通用能力放在菜单服务上,以便角色服务和资源服务都可以复用
  27. 作为开发者,我希望删除角色时在事务保护下同时清理三张表(角色本身、角色-资源关联、用户-角色关联),以便操作原子性有保障
  28. 作为开发者,我希望角色列表返回 builtin 标记,以便前端可以标识内置角色并隐藏删除按钮
  29. 作为开发者,我希望创建角色时不能设置 builtin=true,以便只有数据初始化器能创建内置角色
  30. 作为开发者,我希望现有 /api/system/roles/* 端点被移除而非保留兼容,以便不产生 API 冗余

Implementation Decisions

架构决策(ADR-0012 完整记录)

  • 独立 RoleController,路径 /api/roles/*:从 SystemController 移除全部角色端点,新建独立 Controller。与 ResourceController 的独立模式一致。旧路径 /api/system/roles/* 不再可用。
  • 分步保存:基本信息保存(POST /api/roles/saveOrUpdate)与权限分配(POST /api/roles/assign-resources)分离,各自独立调用。角色编辑页 Tab 分页各自保存。角色详情(GET /api/roles/detail)返回基本信息 + 授权集合。
  • 术语统一 resourceIds:API 层统一使用 resourceIds,与 ADR-0011 权限资源树术语体系一致。底层表名 sys_role_menu 不变,存储层命名与 API 层术语解耦。
  • 祖先补全放在 ISysMenuService:新增 getAncestorIds(Collection<Long> menuIds) 方法。沿 parentId 向上遍历是 sys_menu 表的固有操作。实现方式:批量查全表构建 ID→parentId 映射,内存中遍历,避免逐节点查 DB。SysRoleServiceImpl.assignResources 调用它拿到补全后的集合再写库。
  • 纯 hasAuthority + 数据种子:角色管理端点使用 @PreAuthorize("hasAuthority('crm:role:xxx')")。权限码通过 button 节点的 perms 字段注入(ADR-0011 权限码并集机制)。引导问题通过幂等数据初始化器解决。
  • 内置角色保护:SysRole.builtin 字段SysRole 新增 Boolean builtin 字段。builtin=true 的角色禁止删除、禁止改 roleCode。创建角色接口不允许设 builtin=true
  • 校验规则:roleCode 唯一性(保存时查 DB,已存在且 id 非当前记录则拒绝);内置角色保护(builtin=true 禁止删除和改 code);dataScope 范围校验(必须在 1-4 范围内)。
  • 删除级联deleteRoleCascade 同时清理 sys_role_menusys_user_role。事务保护,三表操作原子性。
  • 数据初始化器幂等化:将"整体跳过"改为"逐项检查"。ADMIN 角色已存在就只补 builtin=true,button 权限点不存在就创建,角色-权限绑定不存在就补建。

API 契约

遵循全局接口契约(非严格 RESTful):

端点 方法 权限码 说明
/api/roles/page POST crm:role:list 分页查询,current/size/keyword 表单参数
/api/roles/detail GET crm:role:detail 角色详情,roleId query 参数,返回 RoleDetailVO
/api/roles/saveOrUpdate POST crm:role:save 保存基本信息,表单参数(id 可选、roleName、roleCode、dataScope、sort、remark)
/api/roles/delete POST crm:role:delete 删除角色,id 表单参数,级联清理
/api/roles/assign-resources POST crm:role:assign 分配权限,roleId + resourceIds(逗号分隔字符串)表单参数,后端补全祖先

DTO 设计

  • RoleDetailVOid(Long,JSON 序列化为 String)、roleNameroleCodedataScopesortremarkbuiltin(Boolean)、resourceIds(List,已补全的完整授权集合,JSON 序列化为 List)
  • 分页列表返回 PageResult<SysRole>(实体含 builtin 字段,无需额外 DTO)

权限码种子数据

在权限资源树的"角色管理"menu 节点下创建 5 个 button 权限点:

名称 perms apiUrl denyBehavior status
角色列表 crm:role:list /api/roles/page hide enabled
角色详情 crm:role:detail /api/roles/detail hide enabled
角色保存 crm:role:save /api/roles/saveOrUpdate hide enabled
角色删除 crm:role:delete /api/roles/delete hide enabled
权限分配 crm:role:assign /api/roles/assign-resources hide enabled

数据模型变更

  • SysRole 新增 Boolean builtin 字段(@Column(columnDefinition = "boolean default false")),ddl-auto: update 自动加列。
  • sys_role_menu 表名不变,sys_user_role 表名不变。

涉及模块

  • crm-auth:RoleController(新建)、ISysRoleService / SysRoleServiceImpl(增强)、ISysMenuService / SysMenuServiceImpl(新增祖先补全)、RoleDetailVO(新建 DTO)、DataInitializer(幂等化重构)、SystemController(移除角色端点)、SysRole(加 builtin 字段)

Testing Decisions

测试理念

只测外部行为,不测实现细节。测试通过服务接口验证业务逻辑的正确性,不测私有方法、不测 mock 的调用次数、不测内部分支路径。测试名用自然语言描述场景,断言用 AssertJ。

测试 Seam:服务层 Mockito 单元测试

单一 seam,复用 ResourceServiceImplTest 的模式(@ExtendWith(MockitoExtension.class) + @Mock mapper 依赖 + @InjectMocks 服务)。

SysMenuServiceImplTest(新建)— 祖先补全逻辑:

  • 单链向上遍历:button(100) → menu(10) → catalog(1) → root(0),传入 {100},返回 {100, 10, 1}
  • 多链汇合:两条独立链的叶子节点,返回包含两条链全部祖先的并集
  • 根节点:传入 parentId=0 的 catalog,返回它自己(无祖先可补)
  • 空输入:传入空集合,返回空集合
  • 已含祖先:传入 {100, 10, 1},返回 {100, 10, 1}(幂等,不重复)

SysRoleServiceImplTest(新建)— 角色服务增强:

  • assignResources 调用祖先补全后写库(验证写入的 menuIds 包含祖先)
  • assignResources 先删后插全量替换(验证旧数据被清除)
  • deleteRoleCascade 同时清理 sys_role_menu 和 sys_user_role
  • 保存时 roleCode 唯一性校验(已存在同 code 不同 id → 拒绝)
  • 保存时 roleCode 唯一性校验(同 code 同 id → 通过,编辑场景)
  • 保存时 builtin 角色禁止改 roleCode
  • 保存时 dataScope 超范围 → 拒绝
  • 保存时 dataScope 合法范围(1-4)→ 通过
  • 删除时 builtin=true 的角色 → 拒绝
  • 删除时 builtin=false 的角色 → 通过
  • 创建角色时 builtin=true → 拒绝(不允许通过 API 设置 builtin)
  • getRoleDetail 返回 RoleDetailVO 含基本信息 + resourceIds

Prior Art

  • ResourceServiceImplTest(599 行):Mockito + AssertJ,覆盖资源树查询、新增/编辑校验、层级约束、字段必填。是本功能测试的直接模板。
  • DataVisibilityTest:纯领域逻辑测试,无 mock,用于验证档位折算规则。

Out of Scope

  • ResourceControllerhasRole('ADMIN') 迁移到 hasAuthority —— 暂不迁移,作为后续渐进迁移的对照。
  • 前端角色管理页面的实现 —— 本 spec 只覆盖后端。
  • 用户-角色分配界面(/api/system/users/assign-roles)—— 现有端点不变,不在本 spec 范围内。
  • 权限资源树的 CRUD 管理 —— 已由 ResourceController(ADR-0011)实现,不重复。
  • SystemController 中菜单树、部门、用户相关端点的迁移或重构 —— 只移除角色端点,其他不动。
  • 数据库迁移工具(Flyway/Liquibase)的引入 —— 使用 ddl-auto: update + 幂等初始化器,不引入迁移工具。

Further Notes

  • ADR-0005(原型侧)决定祖先补全由后端负责。ADR-0012(本仓库)记录了角色管理模块的完整架构决策。
  • CONTEXT.md 已新增三个领域术语:角色授权集合、祖先补全、内置角色。
  • 现有 DataInitializer 中管理员角色的 roleCode 为 ROLE_ADMIN,Spring Security 的 hasRole('ADMIN') 会自动加 ROLE_ 前缀匹配。迁移到 hasAuthority 后,权限码通过 button 节点的 perms 字段直接注入,不经过 ROLE_ 前缀归一。
  • PermissionResolverImplauthorizedMenus 方法已批量查 sys_role_menu + sys_menu,祖先补全逻辑的批量查询模式与之一致。
  • assign-resources 端点的 resourceIds 参数使用逗号分隔字符串(与现有 assign-menusmenuIds 参数格式一致),符合全局接口契约中"复杂字段前端 JSON.stringify 后作为普通表单字段传递"的规范。