# Web 管理端权限系统重构 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 将 Web 管理端从单角色互斥权限升级为多角色 permission-based 权限系统,支持 admin/teacher/nutritionist/article_manager/activity_manager 五种角色及任意角色组合。 **Architecture:** Permission-based 方案。定义两层权限标识(通配级 `assessment:*` + 原子级 `articles:manage`),每个角色映射一组权限,用户有效权限 = 所有角色权限的并集。Layout.vue 菜单渲染和 Router 守卫统一检查 `hasPermission()`。 **Tech Stack:** Java 8 + Spring Boot 2.7 (后端 JWT), Vue 2 + Element UI (前端), JWT io.jsonwebtoken **Spec:** `docs/superpowers/specs/2026-06-20-web-permission-system-design.md` --- ### Task 1: 后端 JWT 及 API 多角色支持 **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/JwtConfig.java` - Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java` - Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminAuthController.java` - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java` - [ ] **Step 1.1: JwtConfig — generateToken 支持多角色** 修改 `generateToken` 方法,接收 `List roles` 替代 `String role`,同时写入 `role`(单角色,向后兼容)和 `roles`(列表)两个 claim: ```java // JwtConfig.java 修改 generateToken 方法 public String generateToken(Long userId, List roles) { Map claims = new HashMap<>(); claims.put("userId", userId); claims.put("role", roles.isEmpty() ? "" : roles.get(0)); // 向后兼容:第一个角色为主角色 claims.put("roles", roles); // 新增:完整角色列表 return createToken(claims, userId.toString()); } ``` 保留原有单参数重载 `generateToken(Long userId, String role)` 作为便捷方法(内部转为 `Collections.singletonList(role)`): ```java public String generateToken(Long userId, String role) { return generateToken(userId, Collections.singletonList(role)); } ``` - [ ] **Step 1.2: JwtInterceptor — 提取 roles 列表** 在 JWT 解析后,同时提取 `roles` claim: ```java // JwtInterceptor.java preHandle() 方法,在 Claims claims = jwtConfig.parseToken(token); 之后 Claims claims = jwtConfig.parseToken(token); Long userId = Long.parseLong(claims.getSubject()); String role = (String) claims.get("role"); @SuppressWarnings("unchecked") List roles = (List) claims.get("roles"); if (roles == null) { roles = role != null ? Collections.singletonList(role) : Collections.emptyList(); } request.setAttribute("userId", userId); request.setAttribute("role", role); request.setAttribute("roles", roles); ``` - [ ] **Step 1.3: UserService — 添加 getAllRoles 方法** 在 `UserService` 中添加公开方法,返回用户所有角色: ```java // UserService.java 新增 public List getAllRoles(Long userId) { User user = getById(userId); if (user == null) return Collections.emptyList(); return parseRoles(user.getRoles()); } ``` > `parseRoles` 已是私有方法(第 556 行),可直接复用。注意它解析 `User.roles` 字段(JSON 或逗号分隔)。 - [ ] **Step 1.4: AdminAuthController — login 返回 roles** 在 `login()` 和 `loginByPassword()` 方法中,获取用户所有角色并写入 JWT 和响应: ```java // login() 方法,在 User user = ... 之后 List roles = userService.getAllRoles(user.getId()); String token = jwtConfig.generateToken(user.getId(), roles); Map result = new HashMap<>(); result.put("token", token); result.put("adminId", user.getId()); result.put("username", ...); result.put("realName", ...); result.put("role", role); result.put("roles", roles); // 新增 ``` 同样修改 `loginByPassword()` 方法。 - [ ] **Step 1.5: AdminAuthController — /info 返回 roles** 在 `getAdminInfo()` 方法中增加 `roles` 返回: ```java // getAdminInfo(),在 result.put("role", user.getRole()); 之后 List roles = userService.getAllRoles(user.getId()); result.put("roles", roles); ``` - [ ] **Step 1.6: 编译验证** ```bash cd /sc-data/cfc/cfc-backend && mvn clean compile -Dmaven.test.skip=true ``` Expected: BUILD SUCCESS --- ### Task 2: 前端权限引擎 — permissions.js **Files:** - Create: `cfc-web/src/utils/permissions.js` - [ ] **Step 2.1: 创建权限工具模块** ```javascript // src/utils/permissions.js /** * 角色→权限映射表 * 每个角色映射一组权限标识,支持通配符 '*' 和前缀通配 'prefix:*' */ const ROLE_PERMISSIONS = { admin: ['*'], teacher: ['dashboard', 'service:*', 'growth:*', 'biz:*', 'assessment:*', 'messages'], nutritionist: ['dashboard', 'service:family', 'health:*', 'assessment:dan', 'energy'], article_manager: ['dashboard', 'articles:*'], activity_manager: ['dashboard', 'activity:*'], } /** * 获取用户有效权限列表 * @param {string[]} roles - 用户拥有的角色列表 * @returns {string[]} - 去重后的权限标识列表 */ export function getEffectivePermissions(roles) { if (!roles || roles.length === 0) return [] const permSet = new Set() for (const role of roles) { const perms = ROLE_PERMISSIONS[role] if (perms) { for (const p of perms) { permSet.add(p) } } } return Array.from(permSet) } /** * 判断是否拥有指定权限 * @param {string[]} userPerms - 用户的有效权限列表 * @param {string} required - 需要的权限标识(支持 'prefix:*' 通配匹配) * @returns {boolean} * * 匹配规则: * - 如果 userPerms 包含 '*' → 返回 true * - 如果 required 是 'prefix:sub' 且 userPerms 包含 'prefix:*' → 返回 true * - 如果 userPerms 包含 required → 返回 true * - 否则返回 false */ export function hasPermission(userPerms, required) { if (!userPerms || !required) return false if (userPerms.includes('*')) return true // 精确匹配 if (userPerms.includes(required)) return true // 前缀通配匹配: required = 'articles:manage', 检查 'articles:*' 是否在 userPerms 中 const colonIndex = required.indexOf(':') if (colonIndex > 0) { const prefix = required.substring(0, colonIndex) + ':*' if (userPerms.includes(prefix)) return true } return false } /** * 获取角色中文名 */ export function getRoleLabel(role) { const labels = { admin: '管理员', teacher: '成长规划师', nutritionist: '营养师', article_manager: '文章管理员', activity_manager: '活动管理员', } return labels[role] || role } ``` - [ ] **Step 2.2: 验证文件创建** ```bash rtk read /sc-data/cfc/cfc-web/src/utils/permissions.js | head -5 ``` Expected: File exists with content --- ### Task 3: Login.vue — 多角色存储适配 **Files:** - Modify: `cfc-web/src/views/Login.vue` - [ ] **Step 3.1: 读取 roles 并存储** 在 Login.vue 中找到登录成功后的处理逻辑,增加 `roles` 存储: ```javascript // Login.vue,在登录成功的回调中 .then(res => { const data = res.data localStorage.setItem('token', data.token) localStorage.setItem('role', data.role) localStorage.setItem('roles', JSON.stringify(data.roles || [data.role])) localStorage.setItem('adminId', data.adminId) localStorage.setItem('adminName', data.username) // ... 后续路由跳转 }) ``` - [ ] **Step 3.2: 验证** ```bash rtk grep -n 'localStorage.setItem.*role' /sc-data/cfc/cfc-web/src/views/Login.vue ``` Expected: Shows both `role` and `roles` storage lines --- ### Task 4: Layout.vue — 权限驱动菜单渲染 **Files:** - Modify: `cfc-web/src/views/Layout.vue` 这是最大的改动。将当前三层 `v-if/v-else-if/v-else` 互斥结构替换为统一菜单定义 + `hasPermission` 驱动渲染。 - [ ] **Step 4.1: 在 script 中添加权限计算** 在 ` ``` > 注意:实际 API 调用方式需与项目现有 axios 封装一致(参考 `src/api/` 目录下的封装模式)。 - [ ] **Step 6.2: 创建 GrowthRecords.vue 骨架** ```vue ``` - [ ] **Step 6.3: 创建 GrowthPlans.vue 骨架** ```vue ``` --- ### Task 7: 新建页面骨架 — 健康档案 **Files:** - Create: `cfc-web/src/views/nutritionist/HealthReports.vue` - Create: `cfc-web/src/views/nutritionist/HealthIndicators.vue` - Create: `cfc-web/src/views/nutritionist/HealthCheckins.vue` - [ ] **Step 7.1: 创建目录和文件** ```bash mkdir -p /sc-data/cfc/cfc-web/src/views/nutritionist ``` - [ ] **Step 7.2: 创建 HealthReports.vue 骨架** ```vue ``` - [ ] **Step 7.3: 创建 HealthIndicators.vue 骨架** ```vue ``` - [ ] **Step 7.4: 创建 HealthCheckins.vue 骨架** ```vue ``` --- ### Task 8: 新建页面骨架 — 活动管理 + 注册路由 **Files:** - Create: `cfc-web/src/views/admin/Activities.vue` - Create: `cfc-web/src/views/admin/ActivityReview.vue` - Modify: `cfc-web/src/router/index.js`(注册所有新路由) - [ ] **Step 8.1: 创建 Activities.vue 骨架** ```vue ``` - [ ] **Step 8.2: 创建 ActivityReview.vue 骨架** ```vue ``` - [ ] **Step 8.3: 注册所有新建路由** 在 `router/index.js` 的 `routes` 中添加: ```javascript // 我的家庭(teacher + nutritionist 共享) { path: 'my-families', name: 'MyFamilies', component: () => import('@/views/teacher/FamilyList.vue'), meta: { title: '我的家庭', perm: 'service:family' } }, // 成长档案(teacher) { path: 'growth-records', name: 'GrowthRecords', component: () => import('@/views/teacher/GrowthRecords.vue'), meta: { title: '成长记录', perm: 'growth:records' } }, { path: 'growth-plans', name: 'GrowthPlans', component: () => import('@/views/teacher/GrowthPlans.vue'), meta: { title: '成长计划', perm: 'growth:plans' } }, // 健康档案(nutritionist) { path: 'health-reports', name: 'HealthReports', component: () => import('@/views/nutritionist/HealthReports.vue'), meta: { title: '健康报告', perm: 'health:reports' } }, { path: 'health-indicators', name: 'HealthIndicators', component: () => import('@/views/nutritionist/HealthIndicators.vue'), meta: { title: '健康指标', perm: 'health:indicators' } }, { path: 'health-checkins', name: 'HealthCheckins', component: () => import('@/views/nutritionist/HealthCheckins.vue'), meta: { title: '健康打卡', perm: 'health:checkins' } }, // 活动管理(activity_manager) { path: 'activities', name: 'Activities', component: () => import('@/views/admin/Activities.vue'), meta: { title: '活动列表', perm: 'activity:list' } }, { path: 'activity-review', name: 'ActivityReview', component: () => import('@/views/admin/ActivityReview.vue'), meta: { title: '活动审核', perm: 'activity:review' } }, ``` --- ### Task 9: 用户管理 — 角色编辑功能 **Files:** - Modify: `cfc-web/src/views/Users.vue` - [ ] **Step 9.1: 在用户列表中添加角色编辑功能** 在用户管理页面的表格操作列中增加"编辑角色"按钮,弹窗显示多选 checkbox 角色列表: ```vue 编辑角色 ``` 新增 dialog: ```vue {{ role.label }} 取消 保存 ``` - [ ] **Step 9.2: 添加 data 和方法** ```javascript data() { return { // ... 原有 data roleDialogVisible: false, editingUserId: null, editingRoles: [], saving: false, allRoles: [ { value: 'admin', label: '管理员' }, { value: 'teacher', label: '成长规划师' }, { value: 'nutritionist', label: '营养师' }, { value: 'article_manager', label: '文章管理员' }, { value: 'activity_manager', label: '活动管理员' }, ], } }, methods: { editRoles(row) { this.editingUserId = row.id this.editingRoles = row.roles || [row.role].filter(Boolean) this.roleDialogVisible = true }, async saveRoles() { this.saving = true try { const baseURL = process.env.VUE_APP_BASE_API || '' const token = localStorage.getItem('token') const axios = this.$axios || require('axios') await axios.post(`${baseURL}/api/admin/user/${this.editingUserId}/roles`, { roles: this.editingRoles }, { headers: { Authorization: `Bearer ${token}` } }) this.$message.success('角色更新成功') this.roleDialogVisible = false this.loadUsers() // 刷新列表 } catch (e) { this.$message.error('保存失败') } finally { this.saving = false } }, } ``` 后端需新增接口: ```java // AdminController.java 新增 @PostMapping("/user/{userId}/roles") public Result updateUserRoles(@PathVariable Long userId, @RequestBody Map> body, @RequestAttribute("userId") Long adminId) { List roles = body.get("roles"); if (roles == null) return Result.error("roles不能为空"); User user = userMapper.selectById(userId); if (user == null) return Result.error("用户不存在"); // 限制:只有 admin 可以修改角色 User admin = userMapper.selectById(adminId); if (admin == null || !"admin".equals(admin.getRole())) { return Result.error("无权限"); } // 更新 roles 字段 String rolesStr = String.join(",", roles); user.setRoles(rolesStr); // 如果更新后的 roles 不再包含当前 role,更新 role 为主角色 if (!roles.contains(user.getRole())) { user.setRole(roles.isEmpty() ? "" : roles.get(0)); } user.setUpdatedAt(new Date()); userMapper.updateById(user); return Result.success(true); } ``` --- ### Task 10: 验证与构建 - [ ] **Step 10.1: 后端编译** ```bash cd /sc-data/cfc/cfc-backend && mvn clean compile -Dmaven.test.skip=true ``` - [ ] **Step 10.2: 前端构建** ```bash cd /sc-data/cfc/cfc-web && npm run build 2>&1 | tail -10 ``` - [ ] **Step 10.3: 检查 LSP 诊断** ```bash # 在 IDE 或使用 LSP 工具检查 Layout.vue router/index.js 等主要修改文件 ```