超管端权限系统设计文档
整理日期: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)
| 字段 |
类型 |
说明 |
| id |
Int (PK) |
主键 |
| parentId |
Int |
父级菜单,支持多级嵌套 |
| permission |
String |
关联的权限码,控制前端可见性 |
| validState |
Int |
1=启用,2=禁用 |
| 字段 |
类型 |
说明 |
| 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 |
特点:菜单是全局模板,不按租户隔离,仅超管可管理。
输入: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
<!-- 默认模式:无权限则禁用按钮(灰显不可点击) -->
<a-button v-permission="'tenant:create'">新建机构</a-button>
<!-- hide 模式:无权限则隐藏元素(display:none) -->
<a-button v-permission.hide="'tenant:delete'">删除</a-button>
<!-- 多权限 OR:满足任一权限即可 -->
<a-button v-permission="['tenant:update', 'tenant:delete']">操作</a-button>
<!-- 多权限 AND:必须同时满足所有权限 -->
<a-button v-permission.all="['role:create', 'permission:assign']">高级操作</a-button>
| 修饰符 |
行为 |
| (无) |
禁用元素,灰显不可点击 |
.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 过滤,直接返回所有菜单 |