library-picturebook-activity/docs/migration/permission-system.md
2026-04-01 19:30:33 +08:00

17 KiB
Raw Blame History

超管端权限系统设计文档

整理日期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.tsbuildTenantConditionByUserType() 方法。

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

<!-- 默认模式:无权限则禁用按钮(灰显不可点击) -->
<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@superbcrypt 加密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 密码加密 bcrypt10 轮 saltJava 端使用 BCryptPasswordEncoder
9 权限码格式 统一使用 resource:action 格式,与前端指令对应
10 菜单加载有 super_admin 快速路径 super_admin 跳过 TenantMenu 和 permission 过滤,直接返回所有菜单