# 权限系统增强设计文档

**日期**: 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 Store（1-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)
