Files
gym-manage/docs/superpowers/specs/2026-04-08-permission-system-enhancement-design.md

16 KiB
Raw Permalink Blame History

权限系统增强设计文档

日期: 2026-04-08
作者: 张翔
版本: 1.0
状态: 待审查

1. 概述

1.1 背景

当前系统已完成基础的路由权限控制,但存在以下优化空间:

  1. 菜单硬编码 - 菜单在前端硬编码,无法根据用户角色动态显示
  2. 权限数据分散 - 角色和权限信息存储在 localStorage,缺乏统一管理
  3. 缺少按钮级权限控制 - 无法控制按钮级别的权限
  4. 缺少 API 权限检查 - 前端调用 API 前未检查权限,可能发送无效请求

1.2 目标

实现完整的权限系统增强,包括:

  1. 动态菜单渲染 - 从后端获取菜单数据,根据用户权限动态渲染
  2. 权限缓存优化 - 使用 Pinia 统一管理权限数据,localStorage 持久化
  3. 权限指令 - 提供 v-permission 指令实现按钮级权限控制
  4. API 权限检查 - 前端调用 API 前检查权限,减少无效请求

1.3 范围

包含:

  • Permission Store (Pinia)
  • v-permission 指令
  • 动态菜单渲染
  • API 权限检查工具
  • 相关单元测试

不包含:

  • 后端权限系统修改(仅需新增 API
  • 数据库权限表结构调整
  • 其他业务功能开发

2. 架构设计

2.1 整体架构

┌─────────────────────────────────────────────────────────────┐
│                        前端应用                              │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │  路由守卫     │  │  权限指令     │  │  动态菜单     │      │
│  │  (已完成)     │  │ v-permission │  │  渲染         │      │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘      │
│         │                  │                  │               │
│         └──────────────────┼──────────────────┘               │
│                            │                                  │
│                   ┌────────▼────────┐                        │
│                   │  Permission     │                        │
│                   │  Store (Pinia)  │                        │
│                   └────────┬────────┘                        │
│                            │                                  │
│                   ┌────────▼────────┐                        │
│                   │  localStorage   │                        │
│                   │  (持久化)        │                        │
│                   └─────────────────┘                        │
│                                                               │
└───────────────────────────┬─────────────────────────────────┘
                            │
                    HTTP API │
                            │
┌───────────────────────────▼─────────────────────────────────┐
│                        后端服务                              │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ /auth/login  │  │ /menus/user  │  │ /permissions │      │
│  │ (已存在)      │  │ (新增)        │  │ (已存在)      │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
│                                                               │
│  ┌──────────────────────────────────────────────────┐      │
│  │  RBAC 权限系统 (角色-权限-菜单)                    │      │
│  └──────────────────────────────────────────────────┘      │
│                                                               │
└─────────────────────────────────────────────────────────────┘

2.2 数据流

登录成功
  ↓
解析 JWT token 获取角色
  ↓
调用 fetchUserMenus() 获取菜单和权限
  ↓
存入 Store + localStorage
  ↓
页面刷新时从 localStorage 恢复

3. 详细设计

3.1 Permission Store

文件位置: src/stores/permission.ts

状态定义:

interface PermissionState {
  roles: string[]           // 用户角色
  permissions: string[]     // 用户权限码
  menus: MenuItem[]         // 用户菜单
  loaded: boolean           // 是否已加载
}

interface MenuItem {
  id: number
  name: string
  path: string
  icon?: string
  parentId?: number
  sort: number
  children?: MenuItem[]
}

核心 Actions:

// 初始化权限数据(从 localStorage 恢复)
initFromStorage(): void

// 登录后设置权限数据
setPermissionData(data: {
  roles: string[]
  permissions: string[]
  menus: MenuItem[]
}): void

// 从后端刷新权限数据
async fetchUserMenus(): Promise<void>

// 清除权限数据(退出登录)
clearPermissionData(): void

// 权限检查方法
hasRole(role: string | string[]): boolean
hasPermission(permission: string | string[]): boolean

持久化策略:

  • 登录时:将角色、权限、菜单数据存入 localStorage
  • 页面刷新:Pinia 从 localStorage 恢复数据,立即渲染菜单
  • 权限变更:提供刷新机制,同步更新 localStorage 和 Pinia
  • 退出登录:清除所有数据

3.2 v-permission 指令

文件位置: src/directives/permission.ts

用法:

<!-- 角色检查 -->
<button v-permission:role="'admin'">管理员按钮</button>

<!-- 权限码检查 -->
<button v-permission:permission="'user:delete'">删除用户</button>

<!-- 支持数组满足任一条件 -->
<button v-permission:role="['admin', 'manager']">导出数据</button>
<button v-permission:permission="['user:create', 'user:update']">编辑用户</button>

<!-- 简写形式默认权限检查 -->
<button v-permission="'user:delete'">删除</button>

实现逻辑:

export const permissionDirective = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const permissionStore = usePermissionStore()
    
    const { arg, value } = binding
    const checkType = arg || 'permission' // 默认权限检查
    
    let hasAccess = false
    
    if (checkType === 'role') {
      hasAccess = permissionStore.hasRole(value)
    } else if (checkType === 'permission') {
      hasAccess = permissionStore.hasPermission(value)
    }
    
    if (!hasAccess) {
      el.style.display = 'none'
    }
  }
}

注册方式:

// src/main.ts
import { permissionDirective } from '@/directives/permission'

app.directive('permission', permissionDirective)

3.3 动态菜单渲染

后端 API:

GET /api/menus/user

请求头:
Authorization: Bearer <token>

响应:
{
  "code": 200,
  "data": {
    "menus": [
      {
        "id": 1,
        "name": "仪表盘",
        "path": "/dashboard",
        "icon": "Odometer",
        "parentId": null,
        "sort": 1
      },
      {
        "id": 2,
        "name": "系统管理",
        "path": "/system",
        "icon": "Setting",
        "parentId": null,
        "sort": 2,
        "children": [
          {
            "id": 3,
            "name": "用户管理",
            "path": "/users",
            "icon": null,
            "parentId": 2,
            "sort": 1
          }
        ]
      }
    ],
    "permissions": [
      "user:read",
      "user:create",
      "user:update",
      "user:delete"
    ]
  }
}

前端组件:

<!-- src/layouts/DefaultLayout.vue -->
<template>
  <el-menu
    :default-active="activeMenu"
    class="menu"
    :collapse="collapsed"
    router
  >
    <menu-item
      v-for="menu in menuTree"
      :key="menu.id"
      :menu="menu"
    />
  </el-menu>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { usePermissionStore } from '@/stores/permission'
import MenuItem from '@/components/MenuItem.vue'

const permissionStore = usePermissionStore()
const menuTree = computed(() => permissionStore.menus)
</script>

递归菜单组件:

<!-- src/components/MenuItem.vue -->
<template>
  <!-- 有子菜单 -->
  <el-sub-menu
    v-if="menu.children && menu.children.length > 0"
    :index="String(menu.id)"
  >
    <template #title>
      <el-icon v-if="menu.icon">
        <component :is="menu.icon" />
      </el-icon>
      <span>{{ menu.name }}</span>
    </template>
    <menu-item
      v-for="child in menu.children"
      :key="child.id"
      :menu="child"
    />
  </el-sub-menu>
  
  <!-- 无子菜单 -->
  <el-menu-item
    v-else
    :index="menu.path"
  >
    <el-icon v-if="menu.icon">
      <component :is="menu.icon" />
    </el-icon>
    <span>{{ menu.name }}</span>
  </el-menu-item>
</template>

3.4 API 权限检查

文件位置: src/utils/permission-check.ts

权限映射配置:

const apiPermissionMap: Record<string, { permission: string; method: string }> = {
  '/api/users:GET': { permission: 'user:read', method: 'GET' },
  '/api/users:POST': { permission: 'user:create', method: 'POST' },
  '/api/users/*:PUT': { permission: 'user:update', method: 'PUT' },
  '/api/users/*:DELETE': { permission: 'user:delete', method: 'DELETE' },
  '/api/roles:GET': { permission: 'role:read', method: 'GET' },
  // ... 更多映射
}

检查函数:

export function canAccessApi(path: string, method: string): boolean {
  const permissionStore = usePermissionStore()
  
  const required = findRequiredPermission(path, method, apiPermissionMap)
  
  if (!required) {
    return true // 未定义权限要求的 API 默认允许
  }
  
  return permissionStore.hasPermission(required.permission)
}

集成到请求拦截器:

// src/utils/request.ts
import { canAccessApi } from './permission-check'

request.interceptors.request.use(
  (config) => {
    // 权限检查
    const path = config.url || ''
    const method = config.method?.toUpperCase() || 'GET'
    
    if (!canAccessApi(path, method)) {
      return Promise.reject(new Error('无权限访问此 API'))
    }
    
    // 原有的 token 和签名逻辑
    // ...
    
    return config
  }
)

4. 测试策略

4.1 测试覆盖范围

  1. Permission Store 单元测试

    • 测试权限数据的存储和恢复
    • 测试 hasRole 和 hasPermission 方法
    • 测试 localStorage 持久化
    • 测试数据清除功能
  2. v-permission 指令测试

    • 测试角色检查功能
    • 测试权限码检查功能
    • 测试数组参数处理
    • 测试元素隐藏/显示逻辑
  3. 动态菜单测试

    • 测试菜单数据获取
    • 测试菜单树渲染
    • 测试菜单缓存机制
    • 测试菜单权限过滤
  4. API 权限检查测试

    • 测试权限映射匹配
    • 测试通配符匹配
    • 测试请求拦截逻辑

4.2 测试文件结构

src/
├── stores/
│   └── __tests__/
│       └── permission.test.ts
├── directives/
│   └── __tests__/
│       └── permission.test.ts
├── components/
│   └── __tests__/
│       └── MenuItem.test.ts
└── utils/
    └── __tests__/
        └── permission-check.test.ts

5. 实施计划

5.1 实施顺序

第 1 步:Permission Store1-2 小时)

  • 创建 src/stores/permission.ts
  • 实现 localStorage 持久化
  • 编写单元测试
  • 集成到登录流程

第 2 步:v-permission 指令(1-2 小时)

  • 创建 src/directives/permission.ts
  • 注册全局指令
  • 编写单元测试
  • 在现有页面应用示例

第 3 步:后端 API 开发(2-3 小时)

  • 新增 GET /api/menus/user 接口
  • 根据用户角色返回菜单树
  • 返回用户权限列表
  • 编写后端测试

第 4 步:动态菜单渲染(2-3 小时)

  • 创建 src/components/MenuItem.vue
  • 修改 DefaultLayout.vue
  • 集成 Permission Store
  • 编写组件测试

第 5 步:API 权限检查(1-2 小时)

  • 创建 src/utils/permission-check.ts
  • 集成到请求拦截器
  • 编写单元测试
  • 优化性能

5.2 后端 API 需求

接口: GET /api/menus/user

功能: 获取当前登录用户可访问的菜单和权限

业务逻辑:

  1. 从 token 获取用户 ID
  2. 查询用户角色
  3. 根据角色查询菜单和权限
  4. 构建菜单树结构
  5. 返回菜单和权限列表

预估时间: 7-12 小时

6. 风险和约束

6.1 技术风险

  1. 后端 API 开发时间 - 需要后端配合开发新 API
  2. 菜单数据迁移 - 需要将硬编码菜单迁移到数据库
  3. 权限数据同步 - 前后端权限数据需要保持一致

6.2 约束条件

  1. 向后兼容 - 需要兼容现有的路由守卫逻辑
  2. 性能要求 - 菜单加载不能影响页面首屏渲染速度
  3. 测试覆盖 - 所有新增代码需要单元测试覆盖

7. 验收标准

7.1 功能验收

  • Permission Store 正确管理权限数据
  • v-permission 指令正确控制按钮显示
  • 动态菜单根据用户权限正确渲染
  • API 权限检查正确拦截无权限请求

7.2 质量验收

  • 所有单元测试通过
  • 代码覆盖率 ≥ 80%
  • TypeScript 类型检查通过
  • ESLint 检查通过

7.3 性能验收

  • 菜单加载时间 < 500ms
  • localStorage 读写不影响页面性能
  • 权限检查不影响 API 请求速度

8. 后续优化

8.1 短期优化

  1. 权限缓存过期 - 添加权限数据过期机制
  2. 权限变更通知 - 实现权限变更后的实时通知
  3. 权限日志 - 记录权限检查日志,便于调试

8.2 长期优化

  1. 权限可视化配置 - 提供权限配置界面
  2. 权限审计 - 记录用户权限变更历史
  3. 权限模板 - 提供常用权限模板,简化配置

9. 参考资料