# 超管端权限系统设计文档
> 整理日期:2026-04-01
> 目的:为后端 Java 转写提供权限逻辑参考,涵盖机构管理、用户中心、系统设置三大模块
---
## 1. 整体架构
### 1.1 多租户 RBAC 模型
系统采用 **多租户 + 角色权限(RBAC)** 架构,核心数据模型关系如下:
```
Tenant (租户)
├── TenantMenu (M2M) ──→ Menu (菜单模板,全局共享)
├── User (用户)
│ └── UserRole (M2M) ──→ Role (角色,租户隔离)
│ └── RolePermission (M2M) ──→ Permission (权限,租户隔离)
└── Config (系统配置,租户隔离)
```
### 1.2 核心模型字段
#### Tenant(租户)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Int (PK) | 主键 |
| code | String (UNIQUE) | 租户编码,用于登录 URL |
| domain | String (UNIQUE) | 子域名访问 |
| isSuper | Int | 0=普通租户,1=超级租户 |
| validState | Int | 1=启用,2=禁用 |
| creator / modifier | String | 审计字段 |
#### User(用户)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Int (PK) | 主键 |
| tenantId | Int (FK) | 所属租户 |
| username | String | 租户内唯一 |
| email / phone | String | 租户内唯一 |
| userType | String | adult / child |
| userSource | String | admin_created / self_registered / child_migrated |
| validState | Int | 1=启用,2=禁用 |
#### Role(角色)—— 租户隔离
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Int (PK) | 主键 |
| tenantId | Int (FK) | 所属租户 |
| code | String | 租户内唯一,`super_admin` 为特殊角色 |
| name | String | 角色名称 |
#### Permission(权限)—— 租户隔离
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Int (PK) | 主键 |
| tenantId | Int (FK) | 所属租户 |
| code | String | 权限码,格式:`{resource}:{action}` |
| resource | String | 资源标识 |
| action | String | 操作标识 |
唯一约束:`(tenantId, resource, action)`
#### Menu(菜单)—— 全局共享
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Int (PK) | 主键 |
| parentId | Int | 父级菜单,支持多级嵌套 |
| permission | String | 关联的权限码,控制前端可见性 |
| validState | Int | 1=启用,2=禁用 |
#### TenantMenu(租户-菜单关联)—— 中间表
| 字段 | 类型 | 说明 |
|------|------|------|
| tenantId | Int (FK) | 租户 ID |
| menuId | Int (FK) | 菜单 ID |
---
## 2. 鉴权链路
每个请求依次经过四层守卫,任一层失败即拒绝:
```
请求进入
│
▼
┌─────────────────────────────────────────────┐
│ ① JwtAuthGuard │
│ 校验 JWT Token,解出 {userId, tenantId} │
│ 文件: auth/guards/jwt-auth.guard.ts │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ ② TenantGuard │
│ 提取租户上下文(优先级从高到低): │
│ 1. Header: x-tenant-id(直接 ID) │
│ 2. Header: x-tenant-code(按 code 查询) │
│ 3. 子域名解析 │
│ 4. JWT 中的 tenantId │
│ 校验:租户存在 && validState = 1 │
│ 注入:request.tenantId, request.tenant │
│ 文件: tenants/guards/tenant.guard.ts │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ ③ RolesGuard(可选,按需装饰) │
│ @Roles('super_admin', 'tenant_admin') │
│ OR 逻辑:用户拥有任一指定角色即通过 │
│ 文件: auth/guards/roles.guard.ts │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ ④ PermissionsGuard(可选,按需装饰) │
│ @RequirePermission('tenant:create') │
│ OR 逻辑:用户拥有任一指定权限即通过 │
│ ⚠️ 重要:super_admin 角色直接放行, │
│ 不检查具体权限 │
│ 文件: auth/guards/permissions.guard.ts │
└─────────────────────────────────────────────┘
```
### 权限解析路径
```
User → UserRole → Role
├── Role.code == 'super_admin' → 跳过权限检查,直接放行
└── RolePermission → Permission.code → 与接口要求的权限码比对
```
---
## 3. 模块一:机构管理
### 3.1 文件位置
| 层级 | 文件路径 |
|------|----------|
| Controller | `backend/src/tenants/tenants.controller.ts` |
| Service | `backend/src/tenants/tenants.service.ts` |
| 前端页面 | `frontend/src/views/system/tenants/Index.vue` |
| 前端 API | `frontend/src/api/tenants.ts` |
### 3.2 接口权限矩阵
| 接口路径 | 方法 | 权限码 | 额外校验 |
|----------|------|--------|----------|
| `/tenants` | GET | `tenant:read` | 列表自动排除内部租户 |
| `/tenants` | POST | `tenant:create` | `checkSuperTenant()` |
| `/tenants/:id` | GET | `tenant:read` | — |
| `/tenants/:id` | PATCH | `tenant:update` | `checkSuperTenant()` |
| `/tenants/:id/status` | PATCH | `tenant:update` | `checkSuperTenant()` + 禁止禁用超级租户自身 |
| `/tenants/:id/menus` | GET | `tenant:read` | 查看该租户已分配的菜单树 |
| `/tenants/:id` | DELETE | `tenant:delete` | `checkSuperTenant()` + 禁止删除超级租户 |
### 3.3 核心逻辑
#### checkSuperTenant()
所有写操作(创建、编辑、删除、启禁用)前,必须校验当前登录用户所属租户的 `isSuper = 1`。非超级租户用户无法执行任何机构管理写操作。
```
伪代码:
function checkSuperTenant(tenantId):
tenant = findById(tenantId)
if tenant.isSuper != 1:
throw ForbiddenException("仅超级租户可执行此操作")
```
#### 内部租户隐藏
列表查询自动排除以下 code 的租户,这些是系统内部使用的特殊租户:
- `super` — 超级租户
- `public` — 公众端
- `school` — 学校
- `teacher` — 教师
- `student` — 学生
- `judge` — 评委
#### 菜单分配
创建或编辑机构时,可通过 `TenantMenu` 中间表选择该机构可使用的菜单。前端以 checkbox 树形组件展示所有菜单模板供勾选。
#### 安全兜底
- 禁止禁用超级租户自身(防止系统锁死)
- 禁止删除超级租户
---
## 4. 模块二:用户中心
### 4.1 文件位置
| 层级 | 文件路径 |
|------|----------|
| Controller | `backend/src/users/users.controller.ts` |
| Service | `backend/src/users/users.service.ts` |
| 前端页面 | `frontend/src/views/system/users/Index.vue` |
| 前端 API | `frontend/src/api/users.ts` |
### 4.2 用户分类(超管跨租户视角)
超管端通过 `userType` 查询参数,按租户类型将用户分为四类:
| userType 参数值 | 租户筛选条件 | 含义 |
|----------------|-------------|------|
| `platform` | `Tenant.isSuper = 1` | 运营团队(超级租户下的用户) |
| `org` | `Tenant.isSuper = 0 AND code NOT IN ('public', 'judge')` | 机构用户 |
| `judge` | `Tenant.code = 'judge'` | 评委用户 |
| `public` | `Tenant.code = 'public'` | 公众用户 |
对应实现:`users.service.ts` 的 `buildTenantConditionByUserType()` 方法。
### 4.3 接口权限说明
用户中心接口**未使用 `@RequirePermission` 装饰器**,权限校验在 Service 层隐式完成:
| 操作 | 权限控制方式 | 说明 |
|------|-------------|------|
| 跨租户查看用户列表 | Service 层判断 `isSuper` | 仅超级租户用户可跨租户查询 |
| 用户统计 | Service 层判断 `isSuper` | 仅超级租户用户 |
| 创建用户 | Service 层校验 | 超管可在任意租户创建用户 |
| 编辑用户 | Service 层校验 | 超管可编辑任意租户的用户 |
| 启/禁用用户 | Service 层校验 | 禁止禁用租户内最后一个管理员 |
| 删除用户 | Service 层校验 | 超管可删除任意租户用户 |
| 分配角色 | 创建/编辑时指定 | 角色来源:目标用户所属租户的角色列表 |
### 4.4 核心逻辑
#### 跨租户查询
超级租户用户(`isSuper = 1`)可查看所有租户的用户,普通租户用户仅能查看和管理本租户用户。
#### 账号保护
禁止禁用某个租户内的最后一个管理员账号,防止该租户被锁死无法登录管理。
```
伪代码:
function disableUser(userId):
user = findById(userId)
adminCount = countAdminsInTenant(user.tenantId)
if adminCount <= 1 && user.isAdmin:
throw BadRequestException("不能禁用租户内最后一个管理员")
```
#### 角色分配
创建或编辑用户时,角色选择列表来源于**目标用户所属租户**的角色,而非当前操作者所属租户。即超管在给某机构创建用户时,可分配的角色是该机构下的角色。
---
## 5. 模块三:系统设置
系统设置包含多个子模块,权限策略各不相同。
### 5.1 菜单管理
| 层级 | 文件路径 |
|------|----------|
| Controller | `backend/src/menus/menus.controller.ts` |
| Service | `backend/src/menus/menus.service.ts` |
| 前端页面 | `frontend/src/views/system/menus/Index.vue` |
**特点:菜单是全局模板,不按租户隔离,仅超管可管理。**
#### 用户菜单加载逻辑(findUserMenus)
```
输入:userId, tenantId
│
├── 用户角色包含 super_admin?
│ ├── 是 → 返回所有有效菜单
│ └── 否 ↓
│
├── 获取用户所有权限码列表
│ User → UserRole → Role → RolePermission → Permission.code
│
├── 获取租户已分配的菜单 ID 列表
│ TenantMenu WHERE tenantId = ?
│
└── 过滤逻辑:
菜单在租户分配范围内
AND (菜单无 permission 字段 OR 用户拥有该 permission)
AND (菜单有 path OR 存在可见的子菜单)
```
### 5.2 角色管理
| 层级 | 文件路径 |
|------|----------|
| Controller | `backend/src/roles/roles.controller.ts` |
| Service | `backend/src/roles/roles.service.ts` |
| 前端页面 | `frontend/src/views/system/roles/Index.vue` |
**特点:角色按租户隔离,所有操作限定在当前租户内。**
| 操作 | 守卫 | 说明 |
|------|------|------|
| 查看角色列表 | JwtAuthGuard | 仅返回当前租户的角色 |
| 创建角色 | JwtAuthGuard | 在当前租户内创建 |
| 编辑角色 | JwtAuthGuard | 更新基本信息 + RolePermission 关联 |
| 删除角色 | JwtAuthGuard | 同时清理 UserRole、RolePermission 关联记录 |
### 5.3 权限管理
| 层级 | 文件路径 |
|------|----------|
| Controller | `backend/src/permissions/permissions.controller.ts` |
| Service | `backend/src/permissions/permissions.service.ts` |
| 前端页面 | `frontend/src/views/system/permissions/Index.vue` |
**特点:仅 `super_admin` 角色可创建/编辑/删除权限。**
| 操作 | 守卫 | 角色要求 |
|------|------|---------|
| 查看权限列表 | JwtAuthGuard | 所有登录用户可查看 |
| 创建权限 | `@Roles('super_admin')` | 仅 super_admin |
| 编辑权限 | `@Roles('super_admin')` | 仅 super_admin |
| 删除权限 | `@Roles('super_admin')` | 仅 super_admin |
#### 权限码规范
格式:`{resource}:{action}`
| 资源 | 权限码 |
|------|--------|
| 租户 | `tenant:create` `tenant:read` `tenant:update` `tenant:delete` |
| 用户 | `user:create` `user:read` `user:update` `user:delete` `user:password:update` |
| 角色 | `role:create` `role:read` `role:update` `role:delete` `role:assign` |
| 菜单 | `menu:create` `menu:read` `menu:update` `menu:delete` |
| 字典 | `dict:create` `dict:read` `dict:update` `dict:delete` |
| 配置 | `config:create` `config:read` `config:update` `config:delete` |
| 日志 | `log:read` `log:delete` |
| 活动 | `contest:*` |
| 报名 | `registration:*` |
| 作品 | `work:*` |
| 评审 | `review:*` |
| 成果 | `result:*` |
| 公告 | `notice:*` |
### 5.4 其他超管专属子模块
| 子模块 | 权限码前缀 | 说明 |
|--------|-----------|------|
| 数据字典 | `dict:*` | 全局字典维护 |
| 系统配置 | `config:*` | 按租户隔离的配置项 |
| 日志记录 | `log:*` | 系统操作日志 |
---
## 6. 前端权限控制
### 6.1 Auth Store
文件:`frontend/src/stores/auth.ts`
| 方法 | 说明 |
|------|------|
| `isSuperAdmin()` | 判断当前用户角色列表是否包含 `super_admin` |
| `hasPermission(code)` | super_admin 直接返回 true;否则检查 `user.permissions[]` |
| `hasAnyPermission(codes[])` | OR 逻辑,满足任一权限即可 |
| `hasRole(role)` | 检查角色列表 |
| `fetchUserMenus()` | 调用 `GET /menus/user-menus`,后端按权限过滤后返回菜单树 |
### 6.2 权限指令 v-permission
文件:`frontend/src/directives/permission.ts`
```html
新建机构
删除
操作
高级操作
```
| 修饰符 | 行为 |
|--------|------|
| (无) | 禁用元素,灰显不可点击 |
| `.hide` | 从 DOM 隐藏 |
| `.all` | 要求满足全部权限(默认为 OR) |
---
## 7. 菜单可见性分配
### 超级租户可见菜单
- 活动监管
- 内容管理
- 活动管理
- 机构管理
- 用户中心
- 系统设置(全部子项)
- 我的评审
### 普通租户默认可见菜单
- 工作台
- 学校管理
- 我的评审
- 活动管理
- 系统设置(仅:用户管理、角色管理)
---
## 8. 数据初始化脚本
位于 `backend/scripts/`,Java 端需等价实现:
| 脚本 | 执行顺序 | 功能 |
|------|---------|------|
| `init-menus.ts` | 1 | 从 `data/menus.json` 创建菜单树,超级租户分配全部菜单 |
| `init-super-tenant.ts` | 2 | 创建超级租户(code=super, isSuper=1)+ 管理员账号 |
| `init-admin-permissions.ts` | 3 | 创建全部权限记录(~90+),赋给 super_admin 角色 |
### 初始超管账号
| 字段 | 值 |
|------|-----|
| 租户 Code | `super` |
| 用户名 | `admin` |
| 密码 | `admin@super`(bcrypt 加密,10 轮 salt) |
| 角色 | `super_admin` |
---
## 9. Java 转写注意事项
| 序号 | 要点 | 说明 |
|------|------|------|
| 1 | **super_admin 跳过权限检查** | PermissionsGuard 中判断角色包含 `super_admin` 即放行,不查权限表。Java 端的拦截器/AOP 需等价实现 |
| 2 | **租户上下文必须注入每个请求** | TenantGuard 从 header → subdomain → JWT 多来源提取 tenantId,校验租户有效后注入请求上下文 |
| 3 | **checkSuperTenant 是额外校验** | 机构管理的写接口除了权限码校验外,还需 Service 层验证操作者所属租户 `isSuper=1` |
| 4 | **菜单全局 + TenantMenu 分配** | Menu 表不带 tenantId,通过 TenantMenu 中间表控制各租户的菜单范围 |
| 5 | **权限和角色按租户隔离** | Permission 和 Role 表都有 tenantId,查询时必须带租户条件 |
| 6 | **用户中心无装饰器权限** | 用户管理接口未使用 `@RequirePermission`,权限在 Service 层隐式判断,Java 端需在 Service 层实现相同逻辑 |
| 7 | **账号保护** | 禁止禁用租户最后一个管理员、禁止删除/禁用超级租户自身 |
| 8 | **密码加密** | bcrypt,10 轮 salt,Java 端使用 `BCryptPasswordEncoder` |
| 9 | **权限码格式** | 统一使用 `resource:action` 格式,与前端指令对应 |
| 10 | **菜单加载有 super_admin 快速路径** | super_admin 跳过 TenantMenu 和 permission 过滤,直接返回所有菜单 |