docs: 添加权限系统增强设计文档

This commit is contained in:
张翔
2026-04-08 06:55:18 +08:00
parent 39be801e6e
commit d9edd24483
@@ -0,0 +1,538 @@
# 权限系统增强设计文档
**日期**: 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)