# ADR-0011: 统一权限资源树(菜单/权限点管理) ## Status Accepted ## Context 现有菜单管理(`SysMenu` + `SystemController` 的 `/api/system/menus/*`)是一个初级 CRUD 脚手架,与 PRD `0002-menu-permission-management` 的「统一权限资源树」差距巨大: 1. **按钮权限码体系缺失**:`SysMenu` 无 `perms`/`denyBehavior`/`apiUrl`/`status`/`description` 字段,按钮节点只是"菜单子节点",无法支撑按钮级 RBAC 鉴权。 2. **零层级约束**:`saveMenu` 直接 `saveOrUpdate`,不校验父子类型组合、不锁编辑 type、不查字段必填。 3. **删除无防护**:`deleteMenu` 直接 `removeById`,不检查子节点、不级联清理 `sys_role_menu` 中的授权引用,留孤儿授权。 4. **无全量资源树接口**:只有按当前用户角色过滤的可见菜单树(`/menus/tree`),管理员无法拿到全量树维护。 5. **无图标上传**。 6. **无权限码并集**:`PermissionServiceImpl` 只建菜单树,JWT 只填 `roleCode`,按钮级 `hasAuthority` 不可用。 ## Decision ### 1. 数据模型:同表补字段 + 枚举映射 `SysMenu` 同表新增 5 个字段(按 type 区分适用字段,空值表示不适用): | 字段 | 类型 | 适用节点 | |---|---|---| | `description` | varchar(100) | 全部 | | `perms` | varchar(100) | button | | `denyBehavior` | varchar(16) | button(hide/disable) | | `status` | varchar(16) | button(enabled/disabled,缺省 enabled) | | `apiUrl` | varchar(200) | button | DB `menuType` 保持 `tinyint`(1/2/3)不变,兼容现有数据;代码层引入 `MenuType` 枚举(`CATALOG(1)`/`MENU(2)`/`BUTTON(3)`)映射 PRD 的 catalog/menu/button。 ### 2. API 路径:新建 /api/resources + 保留 /menus/tree - **管理端**(全量资源树 CRUD + 图标上传):新建 `ResourceController`,路径 `/api/resources`,入参 JSON body(`ResourceNode` DTO)。 - **用户端**(当前用户可见菜单树):保留现有 `GET /api/system/menus/tree`,登录后渲染侧边栏用,不破坏现有链路。 两者职责分离:管理端给管理员维护全量资源,用户端给登录用户取可见菜单。 ### 3. 层级约束(服务端强制校验) | 父节点类型 | 允许子节点类型 | |---|---| | 根(parentId = null/0) | catalog、menu | | catalog | menu | | menu | button | | button | 无(叶子) | - 创建/编辑时校验父子类型组合(6 种组合正反例)。 - 编辑时 `type` 不可更改(编辑态锁定)。 - 字段必填:menu 必填 `route`;button 必填 `perms`(非空)、`denyBehavior`、`apiUrl`、`status`。 - `perms` 格式不强制校验(只校验非空)——格式错只影响该按钮自己的鉴权匹配,不污染其他权限码。 ### 4. 删除防护 + 级联清理 - 删除前检查子节点:有子节点则拒绝。 - 删除时级联清理 `sys_role_menu` 中对该节点的授权引用。 ### 5. 权限码并集注入(按钮级鉴权) 在 `PermissionServiceImpl.initDataScopeContext`(每请求算数据范围时)顺带算权限码并集: - 查用户所有角色 → 查角色授权的菜单 → 过滤 `type=button 且 status=enabled` → 收集 `perms` 字符串。 - 与 `roleCode` 一起填 Spring Security authority,使 `@PreAuthorize("hasAuthority('crm:xxx:yyy')")` 生效。 - 停用的权限点(status=disabled)不进并集——全局断路,无需逐角色取消授权。 ### 6. 图标上传:复用现有 FileApi `POST /api/resources/icon/upload`(multipart)调 crm-file 的 `FileApi` 上传到 MinIO,返回 URL 存入节点 `icon` 字段。校验 png/jpg/gif/svg/webp + ≤500KB。 ## Consequences - 现有 `sys_menu` 表需加 5 列(`ddl-auto: update` 自动加)。 - 现有 `/api/system/menus/save` 和 `/delete` 保留向后兼容(或逐步迁移到 `/api/resources`),不破坏前端现有调用。 - 按钮级鉴权启用后,业务接口可逐步从 `hasRole` 迁移到 `hasAuthority('crm:xxx:yyy')`,但迁移非阻断性,可渐进。 - `perms` 格式不强制校验,依赖管理员保证格式正确——错了该按钮鉴权永远拒绝,联调时暴露。