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

539 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 权限系统增强设计文档
**日期**: 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`
**状态定义**:
```typescript
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**:
```typescript
// 初始化权限数据(从 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`
**用法**:
```vue
<!-- 角色检查 -->
<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>
```
**实现逻辑**:
```typescript
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'
}
}
}
```
**注册方式**:
```typescript
// 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"
]
}
}
```
**前端组件**:
```vue
<!-- 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>
```
**递归菜单组件**:
```vue
<!-- 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`
**权限映射配置**:
```typescript
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' },
// ... 更多映射
}
```
**检查函数**:
```typescript
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)
}
```
**集成到请求拦截器**:
```typescript
// 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. 参考资料
- [Vue 3 官方文档](https://vuejs.org/)
- [Pinia 官方文档](https://pinia.vuejs.org/)
- [Element Plus 文档](https://element-plus.org/)
- [RBAC 权限模型](https://en.wikipedia.org/wiki/Role-based_access_control)