Bladeren bron

docs: add web permission system design docs

Add plan and design spec for web permission system refactoring.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Sisyphus 3 maanden geleden
bovenliggende
commit
28dc7dd7c0

+ 1091 - 0
docs/superpowers/plans/2026-06-20-web-permission-system.md

@@ -0,0 +1,1091 @@
+# 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<String> roles` 替代 `String role`,同时写入 `role`(单角色,向后兼容)和 `roles`(列表)两个 claim:
+
+```java
+// JwtConfig.java 修改 generateToken 方法
+public String generateToken(Long userId, List<String> roles) {
+    Map<String, Object> 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<String> roles = (List<String>) 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<String> 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<String> roles = userService.getAllRoles(user.getId());
+String token = jwtConfig.generateToken(user.getId(), roles);
+
+Map<String, Object> 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<String> 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 中添加权限计算**
+
+在 `<script>` 中引入权限工具,添加 `effectivePerms` 计算属性:
+
+```javascript
+// Layout.vue, 在 import 区域添加
+import { getEffectivePermissions, hasPermission, getRoleLabel } from '@/utils/permissions'
+
+// 在 computed 中添加
+effectivePerms() {
+  const rolesStr = localStorage.getItem('roles')
+  const roles = rolesStr ? JSON.parse(rolesStr) : [this.currentRole]
+  return getEffectivePermissions(roles)
+},
+displayRoles() {
+  const rolesStr = localStorage.getItem('roles')
+  const roles = rolesStr ? JSON.parse(rolesStr) : [this.currentRole]
+  return roles.map(r => getRoleLabel(r)).join(' / ')
+}
+```
+
+- [ ] **Step 4.2: 添加 hasPerm 方法**
+
+```javascript
+// Layout.vue methods 中添加
+hasPerm(required) {
+  return hasPermission(this.effectivePerms, required)
+}
+```
+
+- [ ] **Step 4.3: 替换模板 — 用 v-for 渲染菜单**
+
+将 `<el-menu>` 内的全部内容(现有第13-213行)替换为统一的菜单数组 + `v-for`:
+
+```javascript
+// 在 data() 中新增 menuItems 定义
+data() {
+  return {
+    menuItems: [
+      // 全局通用
+      { path: '/dashboard',                label: '首页',       icon: 'el-icon-s-home', perm: 'dashboard' },
+
+      // admin 专属
+      { title: '家庭管理', icon: 'el-icon-s-home', perm: 'family:*',
+        children: [
+          { path: '/families',             label: '家庭列表',    icon: 'el-icon-s-home',     perm: 'family:list' },
+          { path: '/children',             label: '孩子管理',    icon: 'el-icon-s-custom',   perm: 'family:children' },
+          { path: '/points',               label: '积分管理',    icon: 'el-icon-s-money',    perm: 'family:points' },
+        ]},
+      { title: '任务管理', icon: 'el-icon-s-order', perm: 'task:*',
+        children: [
+          { path: '/tasks',                label: '任务列表',    icon: 'el-icon-s-order',    perm: 'task:list' },
+          { path: '/task-templates',       label: '任务模板',    icon: 'el-icon-document',   perm: 'task:templates' },
+        ]},
+      { title: '奖励管理', icon: 'el-icon-s-goods', perm: 'reward:*',
+        children: [
+          { path: '/rewards',              label: '奖励列表',    icon: 'el-icon-s-goods',    perm: 'reward:list' },
+        ]},
+
+      // teacher + nutritionist 共享
+      { title: '我的家庭', icon: 'el-icon-s-home', perm: 'service:family',
+        children: [
+          { path: '/my-families',          label: '授权家庭',    icon: 'el-icon-s-home',     perm: 'service:family' },
+        ]},
+
+      // teacher 专属
+      { path: '/growth-records',          label: '成长记录',    icon: 'el-icon-document',   perm: 'growth:records' },
+      { path: '/growth-plans',            label: '成长计划',    icon: 'el-icon-document',   perm: 'growth:plans' },
+      { title: '服务管理', icon: 'el-icon-s-home', perm: 'service:*',
+        children: [
+          { path: '/teacher-families',     label: '我的家庭',    icon: 'el-icon-s-home',     perm: 'service:families' },
+          { path: '/teacher-team',         label: '我的团队',    icon: 'el-icon-s-custom',   perm: 'service:team' },
+        ]},
+      { title: '业务管理', icon: 'el-icon-s-marketing', perm: 'biz:*',
+        children: [
+          { path: '/teacher-packages',     label: '套餐管理',    icon: 'el-icon-s-marketing', perm: 'biz:packages' },
+          { path: '/teacher-orders',       label: '订单佣金',    icon: 'el-icon-s-order',    perm: 'biz:orders' },
+        ]},
+      { title: '测评管理', icon: 'el-icon-edit', perm: 'assessment:*',
+        children: [
+          { path: '/teacher-assessment',   label: 'DAN测评',     icon: 'el-icon-edit',       perm: 'assessment:dan' },
+          { path: '/teacher-consult',      label: '家长咨询',    icon: 'el-icon-chat-dot-round', perm: 'assessment:consult' },
+        ]},
+      { path: '/teacher-messages',        label: '消息中心',    icon: 'el-icon-chat-dot-round', perm: 'messages' },
+
+      // nutritionist 专属
+      { title: '健康档案', icon: 'el-icon-document', perm: 'health:*',
+        children: [
+          { path: '/health-reports',       label: '健康报告',    icon: 'el-icon-document',   perm: 'health:reports' },
+          { path: '/health-indicators',    label: '健康指标',    icon: 'el-icon-data-line',  perm: 'health:indicators' },
+          { path: '/health-checkins',      label: '健康打卡',    icon: 'el-icon-s-order',    perm: 'health:checkins' },
+        ]},
+      { path: '/energy-sandbox',          label: '五维能量',    icon: 'el-icon-data-line',  perm: 'energy' },
+
+      // article_manager 专属
+      { title: '文章管理', icon: 'el-icon-document', perm: 'articles:*',
+        children: [
+          { path: '/article-categories',   label: '文章分类',    icon: 'el-icon-document',   perm: 'articles:categories' },
+          { path: '/article-manage',       label: '文章管理',    icon: 'el-icon-document',   perm: 'articles:manage' },
+        ]},
+
+      // activity_manager 专属
+      { title: '活动管理', icon: 'el-icon-s-home', perm: 'activity:*',
+        children: [
+          { path: '/activities',           label: '活动列表',    icon: 'el-icon-s-home',     perm: 'activity:list' },
+          { path: '/activity-review',      label: '活动审核',    icon: 'el-icon-s-check',    perm: 'activity:review' },
+        ]},
+
+      // system 管理 (仅 admin)
+      { title: '系统管理', icon: 'el-icon-s-tools', perm: 'system:*',
+        children: [
+          { path: '/users',                label: '用户管理',    icon: 'el-icon-user',       perm: 'system:users' },
+          { path: '/package-audit',        label: '套餐审核',    icon: 'el-icon-s-check',    perm: 'system:audit' },
+          { path: '/guide-audit',          label: '规划师审核',  icon: 'el-icon-s-check',    perm: 'system:audit' },
+          { path: '/vendor-review',        label: '服务商审核',  icon: 'el-icon-s-check',    perm: 'system:audit' },
+          { path: '/operation-logs',       label: '操作日志',    icon: 'el-icon-s-order',    perm: 'system:logs' },
+          { path: '/product-manage',       label: '商品管理',    icon: 'el-icon-s-goods',    perm: 'system:products' },
+          { path: '/order-manage',         label: '订单管理',    icon: 'el-icon-s-order',    perm: 'system:orders' },
+          { path: '/danshop-category',     label: 'DanShop分类', icon: 'el-icon-s-goods',   perm: 'system:danshop' },
+          { path: '/danshop-profit-rule',  label: '分润规则',    icon: 'el-icon-s-marketing', perm: 'system:danshop' },
+          { path: '/danshop-logistics',    label: '物流管理',    icon: 'el-icon-s-order',    perm: 'system:danshop' },
+          { path: '/sys-config',           label: '系统配置',    icon: 'el-icon-s-tools',    perm: 'system:config' },
+          { path: '/energy-sandbox',       label: '五维能量',    icon: 'el-icon-data-line',  perm: 'system:energy' },
+          { path: '/education-systems',    label: '教育体系',    icon: 'el-icon-s-management', perm: 'system:education' },
+          { path: '/zodiac-configs',       label: '星座配置',    icon: 'el-icon-s-management', perm: 'system:config' },
+          { path: '/bazi-configs',         label: '八字配置',    icon: 'el-icon-s-management', perm: 'system:config' },
+          { path: '/blood-type-configs',   label: '血型配置',    icon: 'el-icon-s-management', perm: 'system:config' },
+        ]},
+    ],
+    // ... 原有 data
+  }
+}
+```
+
+模板替换为:
+
+```html
+<el-menu
+  :default-active="activeMenu"
+  class="sidebar-menu"
+  background-color="transparent"
+  text-color="#64748B"
+  active-text-color="#6366F1"
+  @select="handleMenuSelect"
+>
+  <template v-for="item in menuItems">
+    <!-- 有子菜单 -->
+    <el-submenu v-if="item.children && hasPerm(item.perm)" :key="item.title" :index="item.title">
+      <template slot="title">
+        <i :class="item.icon"></i>
+        <span>{{ item.label || item.title }}</span>
+      </template>
+      <el-menu-item
+        v-for="child in item.children"
+        :key="child.path"
+        :index="child.path"
+        v-if="hasPerm(child.perm)"
+      >
+        <i :class="child.icon"></i>
+        <span slot="title">{{ child.label }}</span>
+      </el-menu-item>
+    </el-submenu>
+    <!-- 无子菜单(直接菜单项) -->
+    <el-menu-item v-else-if="hasPerm(item.perm)" :key="item.path" :index="item.path">
+      <i :class="item.icon"></i>
+      <span slot="title">{{ item.label }}</span>
+    </el-menu-item>
+  </template>
+</el-menu>
+```
+
+保留 header 区域当前显示用户名/角色的逻辑,可改为显示 `displayRoles`:
+
+```html
+<span class="el-dropdown-link username">
+  {{ displayUserLabel }} ({{ displayRoles }})
+</span>
+```
+
+- [ ] **Step 4.4: 移除不再需要的 computed 属性**
+
+删除 `isAdmin`, `isTeacher`, `isVendor`, `isPlanner` computed 属性(如果不再被其他部分引用)。保留 `currentRole` 和 `vendorType` 以防被路由守卫使用。
+
+---
+
+### Task 5: Router 守卫 — 权限检查
+
+**Files:**
+- Modify: `cfc-web/src/router/index.js`
+
+- [ ] **Step 5.1: 给所有路由 meta 添加 perm 字段**
+
+在 `routes` 定义中,为每个路由的 `meta` 添加 `perm` 字段:
+
+```javascript
+{
+  path: 'dashboard',
+  component: () => import('@/views/Dashboard.vue'),
+  meta: { title: '首页', perm: 'dashboard' }
+},
+{
+  path: 'families',
+  component: () => import('@/views/Families.vue'),
+  meta: { title: '家庭列表', perm: 'family:list' }
+},
+// ... 所有路由都加上对应 perm
+```
+
+未授权用户可访问的路由(如 `/login`、`/403`)不加 `perm` 或设为 `null`。
+
+- [ ] **Step 5.2: 在 beforeEach 中添加权限检查**
+
+在路由守卫最后,`return next()` 之前加入权限检查:
+
+```javascript
+// router/index.js beforeEach 末尾,在现有检查之后
+
+// 6. 权限检查(新增)
+const requiredPerm = to.meta?.perm
+if (requiredPerm) {
+  const rolesStr = localStorage.getItem('roles')
+  const roles = rolesStr ? JSON.parse(rolesStr) : [localStorage.getItem('role')]
+  const perms = getEffectivePermissions(roles)
+  if (!hasPermission(perms, requiredPerm)) {
+    console.warn('Access denied: insufficient permissions for', to.name)
+    return next({ path: '/dashboard', query: { accessDenied: 'no-permission' }, replace: true })
+  }
+}
+
+return next()
+```
+
+顶部引入权限工具:
+
+```javascript
+import { getEffectivePermissions, hasPermission } from '@/utils/permissions'
+```
+
+- [ ] **Step 5.3: 可选的 /403 路由**
+
+在路由表中添加 403 页面(可选):
+
+```javascript
+{
+  path: '/403',
+  name: 'Forbidden',
+  component: { template: '<div style="text-align:center;padding:100px"><h1>403</h1><p>权限不足</p><router-link to="/">返回首页</router-link></div>' },
+  meta: { title: '权限不足' }
+}
+```
+
+---
+
+### Task 6: 新建页面骨架 — 我的家庭 + 成长档案
+
+**Files:**
+- Create: `cfc-web/src/views/teacher/FamilyList.vue`(复用部分现有 TeacherFamilies.vue 逻辑)
+- Create: `cfc-web/src/views/teacher/GrowthRecords.vue`
+- Create: `cfc-web/src/views/teacher/GrowthPlans.vue`
+
+- [ ] **Step 6.1: 创建 FamilyList.vue 骨架**
+
+```vue
+<template>
+  <div class="family-list">
+    <el-card>
+      <div slot="header">
+        <span>我的家庭</span>
+      </div>
+      <el-table :data="families" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="80" />
+        <el-table-column prop="familyName" label="家庭名称" />
+        <el-table-column prop="memberCount" label="成员数" width="100" />
+        <el-table-column prop="createdAt" label="绑定时间" width="180" />
+        <el-table-column label="操作" width="200">
+          <template slot-scope="{ row }">
+            <el-button size="mini" type="primary" @click="viewFamily(row)">查看详情</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'FamilyList',
+  data() {
+    return { loading: false, families: [] }
+  },
+  created() { this.loadFamilies() },
+  methods: {
+    async loadFamilies() {
+      this.loading = true
+      try {
+        const token = localStorage.getItem('token')
+        const baseURL = process.env.VUE_APP_BASE_API || ''
+        const res = await this.$axios || this.$http || axios.create({ baseURL }).post('/api/guide/family/list')
+        this.families = res.data?.data || []
+      } catch (e) {
+        this.$message?.error?.('加载家庭列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    viewFamily(row) {
+      this.$router.push(`/my-families/${row.id}`)
+    }
+  }
+}
+</script>
+```
+
+> 注意:实际 API 调用方式需与项目现有 axios 封装一致(参考 `src/api/` 目录下的封装模式)。
+
+- [ ] **Step 6.2: 创建 GrowthRecords.vue 骨架**
+
+```vue
+<template>
+  <div class="growth-records">
+    <el-card>
+      <div slot="header"><span>成长记录</span></div>
+      <el-table :data="records" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="80" />
+        <el-table-column prop="childName" label="孩子" width="120" />
+        <el-table-column prop="recordType" label="记录类型" width="120" />
+        <el-table-column prop="content" label="内容" />
+        <el-table-column prop="createdAt" label="记录时间" width="180" />
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'GrowthRecords',
+  data() { return { loading: false, records: [] } },
+  created() { this.loadRecords() },
+  methods: {
+    async loadRecords() {
+      this.loading = true
+      try {
+        const baseURL = process.env.VUE_APP_BASE_API || ''
+        const res = await this.$axios.post(`${baseURL}/api/growth/record/list`)
+        this.records = res.data?.data || []
+      } catch (e) {
+        this.$message?.error?.('加载成长记录失败')
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+```
+
+- [ ] **Step 6.3: 创建 GrowthPlans.vue 骨架**
+
+```vue
+<template>
+  <div class="growth-plans">
+    <el-card>
+      <div slot="header"><span>成长计划</span></div>
+      <el-table :data="plans" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="80" />
+        <el-table-column prop="childName" label="孩子" width="120" />
+        <el-table-column prop="title" label="计划标题" />
+        <el-table-column prop="status" label="状态" width="100" />
+        <el-table-column prop="createdAt" label="创建时间" width="180" />
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'GrowthPlans',
+  data() { return { loading: false, plans: [] } },
+  created() { this.loadPlans() },
+  methods: {
+    async loadPlans() {
+      this.loading = true
+      try {
+        const baseURL = process.env.VUE_APP_BASE_API || ''
+        const res = await this.$axios.post(`${baseURL}/api/growth/plan/list`)
+        this.plans = res.data?.data || []
+      } catch (e) {
+        this.$message?.error?.('加载成长计划失败')
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+```
+
+---
+
+### 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
+<template>
+  <div class="health-reports">
+    <el-card>
+      <div slot="header"><span>健康报告</span></div>
+      <el-table :data="reports" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="80" />
+        <el-table-column prop="childName" label="孩子" width="120" />
+        <el-table-column prop="reportType" label="报告类型" width="120" />
+        <el-table-column prop="summary" label="摘要" />
+        <el-table-column prop="createdAt" label="报告日期" width="180" />
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'HealthReports',
+  data() { return { loading: false, reports: [] } },
+  created() { this.loadReports() },
+  methods: {
+    async loadReports() {
+      this.loading = true
+      try {
+        const baseURL = process.env.VUE_APP_BASE_API || ''
+        const res = await this.$axios.post(`${baseURL}/api/health/report/list`)
+        this.reports = res.data?.data || []
+      } catch (e) {
+        this.$message?.error?.('加载健康报告失败')
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+```
+
+- [ ] **Step 7.3: 创建 HealthIndicators.vue 骨架**
+
+```vue
+<template>
+  <div class="health-indicators">
+    <el-card>
+      <div slot="header"><span>健康指标</span></div>
+      <el-table :data="indicators" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="80" />
+        <el-table-column prop="childName" label="孩子" width="120" />
+        <el-table-column prop="indicatorName" label="指标名称" width="120" />
+        <el-table-column prop="value" label="数值" width="100" />
+        <el-table-column prop="recordedAt" label="记录时间" width="180" />
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'HealthIndicators',
+  data() { return { loading: false, indicators: [] } },
+  created() { this.loadIndicators() },
+  methods: {
+    async loadIndicators() {
+      this.loading = true
+      try {
+        const baseURL = process.env.VUE_APP_BASE_API || ''
+        const res = await this.$axios.post(`${baseURL}/api/health/indicator/list`)
+        this.indicators = res.data?.data || []
+      } catch (e) {
+        this.$message?.error?.('加载健康指标失败')
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+```
+
+- [ ] **Step 7.4: 创建 HealthCheckins.vue 骨架**
+
+```vue
+<template>
+  <div class="health-checkins">
+    <el-card>
+      <div slot="header"><span>健康打卡</span></div>
+      <el-table :data="checkins" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="80" />
+        <el-table-column prop="childName" label="孩子" width="120" />
+        <el-table-column prop="checkinType" label="打卡类型" width="120" />
+        <el-table-column prop="note" label="备注" />
+        <el-table-column prop="createdAt" label="打卡时间" width="180" />
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'HealthCheckins',
+  data() { return { loading: false, checkins: [] } },
+  created() { this.loadCheckins() },
+  methods: {
+    async loadCheckins() {
+      this.loading = true
+      try {
+        const baseURL = process.env.VUE_APP_BASE_API || ''
+        const res = await this.$axios.post(`${baseURL}/api/health/checkin/list`)
+        this.checkins = res.data?.data || []
+      } catch (e) {
+        this.$message?.error?.('加载健康打卡失败')
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+```
+
+---
+
+### 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
+<template>
+  <div class="activities">
+    <el-card>
+      <div slot="header">
+        <span>活动管理</span>
+        <el-button type="primary" size="mini" style="float:right" @click="showCreate = true">创建活动</el-button>
+      </div>
+      <el-table :data="activities" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="80" />
+        <el-table-column prop="title" label="活动标题" />
+        <el-table-column prop="status" label="状态" width="100" />
+        <el-table-column prop="startTime" label="开始时间" width="180" />
+        <el-table-column prop="endTime" label="结束时间" width="180" />
+        <el-table-column label="操作" width="150">
+          <template slot-scope="{ row }">
+            <el-button size="mini" @click="editActivity(row)">编辑</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Activities',
+  data() { return { loading: false, activities: [], showCreate: false } },
+  created() { this.loadActivities() },
+  methods: {
+    async loadActivities() {
+      this.loading = true
+      try {
+        const baseURL = process.env.VUE_APP_BASE_API || ''
+        const res = await this.$axios.post(`${baseURL}/api/activity/list`)
+        this.activities = res.data?.data || []
+      } catch (e) {
+        this.$message?.error?.('加载活动列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    editActivity(row) {
+      this.$message.info('活动编辑功能待实现')
+    }
+  }
+}
+</script>
+```
+
+- [ ] **Step 8.2: 创建 ActivityReview.vue 骨架**
+
+```vue
+<template>
+  <div class="activity-review">
+    <el-card>
+      <div slot="header"><span>活动审核</span></div>
+      <el-table :data="pendingActivities" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="80" />
+        <el-table-column prop="title" label="活动标题" />
+        <el-table-column prop="applicant" label="申请人" width="120" />
+        <el-table-column prop="createdAt" label="申请时间" width="180" />
+        <el-table-column label="操作" width="200">
+          <template slot-scope="{ row }">
+            <el-button size="mini" type="success" @click="approve(row)">通过</el-button>
+            <el-button size="mini" type="danger" @click="reject(row)">驳回</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ActivityReview',
+  data() { return { loading: false, pendingActivities: [] } },
+  created() { this.loadPending() },
+  methods: {
+    async loadPending() {
+      this.loading = true
+      try {
+        const baseURL = process.env.VUE_APP_BASE_API || ''
+        const res = await this.$axios.post(`${baseURL}/api/activity/list`, { status: 'pending' })
+        this.pendingActivities = res.data?.data || []
+      } catch (e) {
+        this.$message?.error?.('加载待审核活动失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    approve(row) { this.$message.info('审核通过功能待实现') },
+    reject(row) { this.$message.info('驳回功能待实现') }
+  }
+}
+</script>
+```
+
+- [ ] **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
+<!-- Users.vue 操作列新增 -->
+<el-button size="mini" type="warning" @click="editRoles(row)">编辑角色</el-button>
+```
+
+新增 dialog:
+
+```vue
+<el-dialog title="编辑用户角色" :visible.sync="roleDialogVisible" width="400px">
+  <el-checkbox-group v-model="editingRoles">
+    <el-checkbox v-for="role in allRoles" :key="role.value" :label="role.value">
+      {{ role.label }}
+    </el-checkbox>
+  </el-checkbox-group>
+  <span slot="footer">
+    <el-button @click="roleDialogVisible = false">取消</el-button>
+    <el-button type="primary" @click="saveRoles" :loading="saving">保存</el-button>
+  </span>
+</el-dialog>
+```
+
+- [ ] **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<Boolean> updateUserRoles(@PathVariable Long userId,
+                                        @RequestBody Map<String, List<String>> body,
+                                        @RequestAttribute("userId") Long adminId) {
+    List<String> 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 等主要修改文件
+```

+ 283 - 0
docs/superpowers/specs/2026-06-20-web-permission-system-design.md

@@ -0,0 +1,283 @@
+# Web 管理端权限系统设计
+
+**日期:** 2026-06-20
+**状态:** 设计稿
+
+## 1. 目标
+
+将 Web 管理端从单角色互斥权限模式(`v-if="isPlanner"/v-else-if="isVendor"/v-else`)升级为多角色权限合并模式,支持以下角色:
+
+| 角色 | 标识 | 职责 |
+|------|------|------|
+| 管理员 | `admin` | 系统全功能管理 |
+| 成长规划师 | `teacher` | 家庭教育相关服务 |
+| 营养师 | `nutritionist` | 家庭健康相关服务 |
+| 文章管理员 | `article_manager` | 文章内容管理 |
+| 活动管理员 | `activity_manager` | 活动发布和管理 |
+
+**核心约束:** 一个用户可以拥有多个角色(如同时是营养师 + 文章管理员),其有效权限为所有角色权限的并集。
+
+## 2. 现有状态分析
+
+### 2.1 后端现状
+
+- `User` 实体已有 `roles` 字段(JSON 数组格式,支持多角色存储)
+- `UserService.hasRole()` 已支持解析多角色
+- 但 JWT 只存单 `role` 字段,登录仅返回单角色
+- 后端 Controller 对应关系:
+  - `GrowthRecordController` / `GrowthPlanController` — `/api/growth/*`(成长档案)
+  - `HealthReportController` — `/api/health/*`(健康报告)
+  - `HealthCheckinController` — `/api/health/checkin/*`(健康打卡)
+  - `ActivityController` — `/api/activity/*`(活动管理)
+  - 其余管理端 API 均可用
+
+### 2.2 前端现状
+
+- **Layout.vue:** 三选一互斥分支(`v-if="isPlanner"` / `v-else-if="isVendor"` / `v-else`)
+- **Router:** 硬编码 `teacherRoutes` / `adminRoutes` 名单判断
+- **认证:** `localStorage` 存单 `role` 字段
+- **现有页面:** 文章管理页面已有;成长档案/健康档案/活动管理页面需新建
+
+### 2.3 需新建的前端页面
+
+| 页面 | 路径 | 对应角色 | 后端 API |
+|------|------|---------|---------|
+| 我的家庭 | `/my-families` | teacher, nutritionist | 现有 `/api/guide/*` |
+| 成长记录 | `/growth-records` | teacher | `GrowthRecordController` |
+| 成长计划 | `/growth-plans` | teacher | `GrowthPlanController` |
+| 健康报告 | `/health-reports` | nutritionist | `HealthReportController` |
+| 健康指标 | `/health-indicators` | nutritionist | `HealthReportController` |
+| 健康打卡 | `/health-checkins` | nutritionist | `HealthCheckinController` |
+| 活动列表 | `/activities` | activity_manager | `ActivityController` |
+| 活动审核 | `/activity-review` | activity_manager | `ActivityController` |
+
+## 3. 架构设计
+
+### 3.1 权限模型
+
+采用 **Permission-based** 方案。定义两层权限标识:
+
+- **通配级:** `'assessment:*'` 匹配所有 `assessment:` 开头的权限
+- **原子级:** `'articles:manage'` 精确匹配单个功能
+
+#### 角色→权限映射
+
+```javascript
+// src/utils/permissions.js
+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:*'],
+}
+```
+
+#### 菜单→权限映射
+
+```
+首页 /dashboard                              → dashboard
+家庭管理 (system) /families /children /points  → family:*
+任务管理 /tasks /task-templates               → task:*
+奖励管理 /rewards                             → reward:*
+系统管理 /users /sys-config /...               → system:*
+我的家庭 /my-families                         → service:family
+成长记录 /growth-records                      → growth:records
+成长计划 /growth-plans                        → growth:plans
+套餐管理 /teacher-packages                    → biz:packages
+订单佣金 /teacher-orders                      → biz:orders
+DAN测评 /teacher-assessment                   → assessment:dan
+家长咨询 /teacher-consult                     → assessment:consult
+消息中心 /teacher-messages                    → messages
+健康报告 /health-reports                      → health:reports
+健康指标 /health-indicators                   → health:indicators
+健康打卡 /health-checkins                     → health:checkins
+五维能量 /energy-sandbox                      → energy
+文章分类 /article-categories                  → articles:categories
+文章管理 /article-manage                      → articles:manage
+文章编辑 /article-edit                        → articles:edit
+活动列表 /activities                          → activity:list
+活动审核 /activity-review                     → activity:review
+```
+
+### 3.2 后端变更
+
+#### JWT 变更
+
+**当前:** `generateToken(userId, role)` → JWT payload 含 `role: "admin"`
+
+**改为:** `generateToken(userId, roles)` → JWT payload 含 `roles: ["admin", "article_manager"]`
+
+#### API 变更
+
+`POST /api/admin-auth/login` 响应增加 `roles` 数组:
+
+```json
+{
+  "code": 200,
+  "data": {
+    "token": "...",
+    "adminId": 1,
+    "username": "admin",
+    "realName": "管理员",
+    "role": "admin",
+    "roles": ["admin", "article_manager"]
+  }
+}
+```
+
+`POST /api/admin-auth/info` 响应同样增加 `roles` 数组。
+
+**无需迁移:** User 表 `roles` 字段已存在,仅前端新增的角色值(`nutritionist`, `article_manager`, `activity_manager`)需要在用户管理后台可设置。
+
+### 3.3 前端变更
+
+#### 3.3.1 权限工具模块
+
+新增 `src/utils/permissions.js`:
+
+```javascript
+// 角色→权限映射表
+const ROLE_PERMISSIONS = { ... }
+
+// 菜单定义(每个菜单项带权限标识)
+const MENU_DEFINITIONS = [ ... ]
+
+// 核心函数
+function getEffectivePermissions(roles) { /* 合并所有角色的权限 */ }
+function hasPermission(userPerms, required) { /* 前缀匹配 + 通配 * 支持 */ }
+```
+
+#### 3.3.2 状态管理
+
+`Login.vue` 登录后存储:
+
+```javascript
+localStorage.setItem('roles', JSON.stringify(res.data.roles))
+```
+
+新增 Vuex store 或全局 computed 读取 `roles`。
+
+#### 3.3.3 Layout.vue 菜单渲染
+
+从目前的硬编码三层分支改为:
+
+```
+<template v-for="item in menuItems">
+  <el-submenu v-if="item.children && hasPerm(item.perm)">
+    ...子项统一渲染...
+  </el-submenu>
+  <el-menu-item v-else-if="hasPerm(item.perm)">
+    ...
+  </el-menu-item>
+</template>
+```
+
+#### 3.3.4 Router 守卫
+
+路由 meta 增加 `perm` 字段:
+
+```javascript
+{
+  path: 'article-manage',
+  component: ...,
+  meta: { title: '文章管理', perm: 'articles:manage' }
+}
+```
+
+路由守卫检查逻辑:
+
+```javascript
+if (to.meta.perm) {
+  const perms = getEffectivePermissions(userRoles)
+  if (!hasPermission(perms, to.meta.perm)) {
+    return next({ path: '/403', replace: true })
+  }
+}
+```
+
+保留现有 `requiresTeacher` 兼容过渡,逐步替换为 `perm`。
+
+## 4. 菜单结构(最终效果)
+
+### 管理员登录
+
+```
+首页 | 家庭管理 | 任务管理 | 奖励管理 | 系统管理(全部子项)
+```
+
+### 成长规划师登录
+
+```
+首页
+我的家庭 → 家庭详情 → 成长档案tab / 成长计划tab / 家庭成员tab
+成长记录
+成长计划
+业务管理 → 套餐管理 / 订单佣金
+测评管理 → DAN测评 / 家长咨询
+消息中心
+```
+
+### 营养师登录
+
+```
+首页
+我的家庭 → 家庭详情 → 健康报告tab / 健康指标tab / 健康打卡tab
+健康报告
+健康指标
+健康打卡
+五维能量
+```
+
+### 文章管理员登录
+
+```
+首页
+文章分类
+文章管理
+```
+
+### 活动管理员登录
+
+```
+首页
+活动列表
+活动审核
+```
+
+### 多角色合并(示例:营养师 + 文章管理员)
+
+```
+首页
+我的家庭 | 健康报告 | 健康指标 | 健康打卡 | 五维能量
+文章分类 | 文章管理
+```
+
+## 5. 新建页面骨架
+
+每个新建页面仅创建 Vue 文件 + 路由注册,内容为一个带 el-table 骨架的占位页:
+
+- `/my-families` — 授权家庭列表(复用教师端现有 `TeacherFamilies.vue` 逻辑)
+- `/growth-records` — 成长记录列表,依赖 `GrowthRecordController`
+- `/growth-plans` — 成长计划列表,依赖 `GrowthPlanController`
+- `/health-reports` — 健康报告列表,依赖 `HealthReportController`
+- `/health-indicators` — 健康指标管理,依赖 `HealthReportController`
+- `/health-checkins` — 健康打卡列表,依赖 `HealthCheckinController`
+- `/activities` — 活动列表管理,依赖 `ActivityController`
+- `/activity-review` — 活动审核,依赖 `ActivityController`
+
+## 6. 实施步骤
+
+1. 后端:JWT 及 API 多角色支持
+2. 新增 `src/utils/permissions.js` 权限引擎
+3. 重构 Layout.vue 菜单渲染(权限驱动)
+4. 重构 Router 守卫(权限检查)
+5. Login.vue 适配多角色存储
+6. 新建 8 个页面骨架 + 路由注册
+7. 验证:admin/teacher 现有功能不受影响
+
+## 7. 未纳入范围
+
+- 用户管理后台的角色编辑界面(已有 `User.roles` 字段,未来可通过管理接口直接编辑)
+- 「我的家庭」进入后的 tab 式详情页设计(本期只搭路由和菜单入口)
+- 小程序端权限(仅 Web 管理端)