- 更新 CLAUDE.md 中 HTTP 客户端说明(apiFetch -> http) - 更新 docs/web/DESIGN.md 技术栈说明 - 添加任务工作流文档 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
15 KiB
15 KiB
菜单及路由管理体系实施文档
需求确认
| 项目 | 选择 |
|---|---|
| 菜单来源 | 混合模式(核心菜单静态 + 扩展菜单数据库) |
| 权限粒度 | 按钮级别 |
| 权限模型 | RBAC(角色-权限模型) |
| 超级管理员 | 需要内置 |
| 权限编码 | 资源:操作 格式(如 user:create) |
| 前端权限 | Hook + 组件 + 指令 |
| 路由守卫 | 自动拦截(跳转 403) |
一、数据库模型设计
新增模型
// 角色表
model Role {
id String @id @default(cuid(2))
code String @unique // 角色编码: admin, user
name String // 角色名称
description String?
isSystem Boolean @default(false) // 系统内置角色不可删
isEnabled Boolean @default(true)
sort Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
users UserRole[]
permissions RolePermission[]
menus RoleMenu[]
}
// 权限表
model Permission {
id String @id @default(cuid(2))
code String @unique // 权限编码: user:create
name String
description String?
resource String // 资源: user
action String // 操作: create
isEnabled Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
roles RolePermission[]
}
// 菜单表
model Menu {
id String @id @default(cuid(2))
parentId String?
code String @unique
name String
type String @default("menu") // dir / menu / button
path String?
icon String? // Lucide 图标名
component String?
permission String? // 关联权限编码
isExternal Boolean @default(false)
isHidden Boolean @default(false)
isEnabled Boolean @default(true)
isStatic Boolean @default(true) // 静态/动态菜单
sort Int @default(0)
meta Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
parent Menu? @relation("MenuTree", fields: [parentId], references: [id])
children Menu[] @relation("MenuTree")
roles RoleMenu[]
}
// 关联表
model UserRole { ... }
model RolePermission { ... }
model RoleMenu { ... }
// User 模型扩展
model User {
// ... 现有字段
isSuperAdmin Boolean @default(false) // 超级管理员标记
roles UserRole[]
}
超级管理员设计
User.isSuperAdmin字段标记- 超管拥有所有权限,跳过权限检查
- 超管标记仅允许数据库直接修改
二、共享类型定义
文件: packages/shared/src/types/permission.ts
枚举常量
export const MenuType = { DIR: 'dir', MENU: 'menu', BUTTON: 'button' } as const;
export const SystemRoleCode = { SUPER_ADMIN: 'super_admin', ADMIN: 'admin', USER: 'user' } as const;
export const PermissionAction = { CREATE: 'create', READ: 'read', UPDATE: 'update', DELETE: 'delete' } as const;
接口类型
RoleResponse/RoleDetailResponse- 角色响应PermissionResponse- 权限响应MenuResponse/MenuTreeNode- 菜单响应AuthUserWithPermissions- 带权限的认证用户UserMenusAndPermissionsResponse- 用户菜单和权限响应
三、后端模块设计
目录结构
apps/api/src/permission/
├── decorators/
│ ├── index.ts
│ └── require-permission.decorator.ts
├── guards/
│ ├── index.ts
│ └── permission.guard.ts
├── dto/
│ ├── index.ts
│ ├── permission.dto.ts
│ ├── role.dto.ts
│ └── menu.dto.ts
├── services/
│ ├── index.ts
│ ├── permission.service.ts
│ ├── role.service.ts
│ └── menu.service.ts
├── controllers/
│ ├── index.ts
│ ├── permission.controller.ts
│ ├── role.controller.ts
│ └── menu.controller.ts
├── index.ts
└── permission.module.ts
核心实现
权限装饰器 @RequirePermission(...permissions)
export const RequirePermission = (...permissions: string[]) =>
SetMetadata(PERMISSION_KEY, permissions);
权限守卫 PermissionGuard
canActivate(context) {
const permissions = reflector.get(PERMISSION_KEY, handler);
if (!permissions) return true;
const user = request.user;
if (user.isSuperAdmin) return true; // 超管直接通过
return permissions.some(p => user.permissions.includes(p));
}
JWT 策略更新 - 返回用户权限信息
validate(payload) {
const user = await prisma.user.findUnique({
include: { roles: { include: { role: { include: { permissions } } } } }
});
return { ...user, roles: [...], roleIds: [...], permissions: [...] };
}
API 端点
| 模块 | 端点 | 权限 |
|---|---|---|
| 角色 | GET/POST /roles |
role:read/create |
| 角色 | GET/PATCH/DELETE /roles/:id |
role:read/update/delete |
| 权限 | GET /permissions |
permission:read |
| 菜单 | GET/POST /menus |
menu:read/create |
| 菜单 | PATCH/DELETE /menus/:id |
menu:update/delete |
| 当前用户 | GET /auth/menus |
登录即可 |
四、前端权限系统设计
目录结构
apps/web/src/
├── stores/permissionStore.ts # 权限状态
├── hooks/usePermission.ts # 权限检查 Hook
├── components/permission/
│ ├── index.ts
│ ├── PermissionGuard.tsx # 权限守卫组件
│ └── WithPermission.tsx # 权限 HOC
├── services/permission.service.ts # 权限服务
├── config/route-permissions.ts # 路由权限映射
├── lib/icons.ts # 图标映射
└── app/403/page.tsx # 无权限页面
权限 Store
interface PermissionState {
menus: MenuTreeNode[];
permissions: string[];
isSuperAdmin: boolean;
isLoaded: boolean;
}
interface PermissionActions {
setPermissionData: (data) => void;
clearPermissionData: () => void;
hasPermission: (p: string | string[]) => boolean;
hasAllPermissions: (permissions: string[]) => boolean;
}
usePermission Hook
function usePermission() {
const { permissions, isSuperAdmin } = usePermissionStore();
const hasPermission = (permission: string | string[]) => {
if (isSuperAdmin) return true;
if (Array.isArray(permission)) return permission.some(p => permissions.includes(p));
return permissions.includes(permission);
};
return { hasPermission, hasAnyPermission, hasAllPermissions };
}
PermissionGuard 组件
<PermissionGuard permission="user:create">
<CreateUserButton />
</PermissionGuard>
<PermissionGuard permission={['user:create', 'user:update']} fallback={<span>无权限</span>}>
<UserForm />
</PermissionGuard>
<PermissionGuard permission={['user:create', 'user:update']} mode="all">
<UserForm />
</PermissionGuard>
路由守卫
在 Dashboard Layout 中实现:
- 登录后获取用户菜单和权限
- 存入 permissionStore
- 路由变化时检查权限
- 无权限跳转 403 页面
五、文件清单
后端文件
| 优先级 | 文件路径 | 说明 |
|---|---|---|
| P0 | apps/api/prisma/schema.prisma |
数据库模型 |
| P0 | apps/api/prisma/seed.ts |
种子数据 |
| P0 | apps/api/src/permission/guards/permission.guard.ts |
权限守卫 |
| P0 | apps/api/src/permission/decorators/require-permission.decorator.ts |
权限装饰器 |
| P0 | apps/api/src/auth/strategies/jwt.strategy.ts |
JWT 策略(已更新) |
| P0 | apps/api/src/auth/auth.controller.ts |
认证控制器(已更新) |
| P0 | apps/api/src/auth/auth.module.ts |
认证模块(已更新) |
| P1 | apps/api/src/permission/services/*.ts |
服务层 |
| P1 | apps/api/src/permission/controllers/*.ts |
控制器层 |
| P1 | apps/api/src/permission/dto/*.ts |
DTO 定义 |
| P1 | apps/api/src/permission/permission.module.ts |
权限模块 |
| P1 | apps/api/src/app.module.ts |
应用模块(已更新) |
| P1 | apps/api/src/prisma/prisma.service.ts |
Prisma 服务(已更新) |
共享包文件
| 文件路径 | 说明 |
|---|---|
packages/shared/src/types/permission.ts |
权限相关类型定义 |
packages/shared/src/types/index.ts |
类型导出(已更新) |
前端文件
| 优先级 | 文件路径 | 说明 |
|---|---|---|
| P0 | apps/web/src/stores/permissionStore.ts |
权限状态管理 |
| P0 | apps/web/src/app/(dashboard)/layout.tsx |
Dashboard 布局(已更新) |
| P1 | apps/web/src/hooks/usePermission.ts |
权限检查 Hook |
| P1 | apps/web/src/components/permission/PermissionGuard.tsx |
权限守卫组件 |
| P1 | apps/web/src/components/permission/WithPermission.tsx |
权限 HOC |
| P1 | apps/web/src/components/layout/Sidebar.tsx |
侧边栏(已更新) |
| P1 | apps/web/src/lib/icons.ts |
图标映射工具 |
| P1 | apps/web/src/config/route-permissions.ts |
路由权限配置 |
| P1 | apps/web/src/config/constants.ts |
常量配置(已更新) |
| P1 | apps/web/src/services/permission.service.ts |
权限服务 |
| P1 | apps/web/src/app/403/page.tsx |
403 页面 |
| P2 | apps/web/src/components/ui/collapsible.tsx |
折叠组件 |
六、使用指南
后端使用
1. 保护接口
@Controller('users')
@UseGuards(JwtAuthGuard, PermissionGuard)
@ApiBearerAuth()
export class UserController {
@Get()
@RequirePermission('user:read')
findAll() { ... }
@Post()
@RequirePermission('user:create')
create() { ... }
@Delete(':id')
@RequirePermission('user:delete')
delete() { ... }
}
2. 获取当前用户权限
@Get('profile')
getProfile(@CurrentUser() user: AuthUserWithPermissions) {
// user.isSuperAdmin - 是否超管
// user.roles - 角色编码列表
// user.permissions - 权限编码列表
}
前端使用
1. 权限守卫组件
import { PermissionGuard } from '@/components/permission';
// 单个权限
<PermissionGuard permission="user:create">
<Button>创建用户</Button>
</PermissionGuard>
// 多个权限(OR 关系)
<PermissionGuard permission={['user:create', 'user:update']}>
<UserForm />
</PermissionGuard>
// 多个权限(AND 关系)
<PermissionGuard permission={['user:create', 'user:update']} mode="all">
<UserForm />
</PermissionGuard>
// 带 fallback
<PermissionGuard permission="user:delete" fallback={<span>无权限</span>}>
<DeleteButton />
</PermissionGuard>
2. usePermission Hook
import { usePermission } from '@/hooks';
function MyComponent() {
const { hasPermission, isSuperAdmin } = usePermission();
return (
<div>
{hasPermission('user:create') && <CreateButton />}
{hasPermission(['user:update', 'user:delete']) && <ActionMenu />}
</div>
);
}
3. 路由权限配置
// config/route-permissions.ts
export const routePermissions: Record<string, string | string[]> = {
'/users': 'user:read',
'/users/create': 'user:create',
'/roles': 'role:read',
};
export const publicRoutes = [
'/dashboard',
'/profile',
'/settings',
];
七、部署步骤
1. 数据库迁移
# 启动数据库
docker compose -f deploy/docker-compose.yml up -d
# 执行数据库迁移
cd apps/api && pnpm db:migrate
# 初始化种子数据
cd apps/api && pnpm db:seed
2. 验证
-
使用超级管理员账号登录
- 邮箱:
admin@seclusion.dev - 密码:
admin123
- 邮箱:
-
访问 Swagger 文档查看新增的权限管理 API
http://localhost:4000/api/docs
-
验证菜单根据权限动态显示
-
验证无权限路由跳转到 403 页面
八、预置数据
角色
| 编码 | 名称 | 说明 |
|---|---|---|
super_admin |
超级管理员 | 系统内置,拥有所有权限 |
admin |
管理员 | 系统内置,拥有大部分权限 |
user |
普通用户 | 系统内置,基础权限 |
权限
| 编码 | 名称 | 资源 | 操作 |
|---|---|---|---|
user:create |
创建用户 | user | create |
user:read |
查看用户 | user | read |
user:update |
更新用户 | user | update |
user:delete |
删除用户 | user | delete |
role:create |
创建角色 | role | create |
role:read |
查看角色 | role | read |
role:update |
更新角色 | role | update |
role:delete |
删除角色 | role | delete |
permission:read |
查看权限 | permission | read |
menu:create |
创建菜单 | menu | create |
menu:read |
查看菜单 | menu | read |
menu:update |
更新菜单 | menu | update |
menu:delete |
删除菜单 | menu | delete |
菜单
| 编码 | 名称 | 类型 | 路径 | 图标 | 权限 |
|---|---|---|---|---|---|
dashboard |
仪表盘 | menu | /dashboard | LayoutDashboard | - |
user-management |
用户管理 | menu | /users | Users | user:read |
system |
系统管理 | dir | - | Settings | - |
role-management |
角色管理 | menu | /roles | Shield | role:read |
menu-management |
菜单管理 | menu | /menus | Menu | menu:read |
profile |
个人中心 | menu | /profile | User | - |
settings |
系统设置 | menu | /settings | Settings | - |
九、阶段四:管理界面(已完成)
角色管理
| 文件路径 | 说明 |
|---|---|
apps/web/src/app/(dashboard)/roles/page.tsx |
角色管理页面 |
apps/web/src/components/roles/RolesTable.tsx |
角色列表表格组件 |
apps/web/src/components/roles/RoleEditDialog.tsx |
角色编辑弹窗组件 |
apps/web/src/hooks/useRoles.ts |
角色相关 Hooks |
apps/web/src/services/role.service.ts |
角色服务层 |
菜单管理
| 文件路径 | 说明 |
|---|---|
apps/web/src/app/(dashboard)/menus/page.tsx |
菜单管理页面 |
apps/web/src/components/menus/MenusTable.tsx |
菜单列表表格组件 |
apps/web/src/components/menus/MenuEditDialog.tsx |
菜单编辑弹窗组件 |
apps/web/src/hooks/useMenus.ts |
菜单相关 Hooks |
apps/web/src/services/menu.service.ts |
菜单服务层 |
功能说明
-
角色管理:
- 角色列表展示(支持分页、搜索、排序)
- 新建/编辑角色
- 为角色分配权限
- 删除角色(系统内置角色不可删除)
-
菜单管理:
- 菜单列表展示(支持分页、搜索、排序)
- 新建/编辑菜单
- 支持目录、菜单、按钮三种类型
- 删除菜单(静态菜单不可删除)
十、后续扩展建议
- 数据权限 - 支持数据行级权限控制
- 权限缓存 - Redis 缓存用户权限减少数据库查询
- 权限继承 - 支持角色继承
- 操作日志 - 记录权限变更日志
- 批量导入导出 - 支持权限配置的批量操作