# 超管端权限系统设计文档 > 整理日期: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 过滤,直接返回所有菜单 |