Files
gongxue-base/docs/superpowers/specs/2026-07-02-rbac-refactor-design.md

29 KiB
Raw Blame History

comet_change, role, canonical_spec, archived-with, status
comet_change role canonical_spec archived-with status
rbac-refactor technical-design openspec 2026-07-03-rbac-refactor final

RBAC 鉴权重构技术设计

  • 日期2026-07-02
  • 变更rbac-refactor
  • 阶段Design → Build

1. 架构总览

                          ┌─────────────────────────┐
                          │       Request            │
                          └───────────┬─────────────┘
                                      │
                                      ▼
                          ┌─────────────────────────┐
                          │    JwtAuthGuard          │
                          │    (Token 有效性校验)      │
                          └───────────┬─────────────┘
                                      │
                          ┌───────────▼─────────────┐
                          │    PermissionGuard       │  ← 全局 APP_GUARD
                          │    (从 req.user 读        │
                          │     permissions 数组)     │
                          └───────────┬─────────────┘
                                      │
                    ┌─────────────────┼─────────────────┐
                    │                 │                 │
                    ▼                 ▼                 ▼
              @Public()         @RequirePermission  无装饰器
              放行              ('student:create')    → 403
                                      │
                                      ▼
                              ┌───────────────┐
                              │  Controller   │
                              └───────────────┘

核心原则

  • 认证Authentication授权Authorization 分离:AuthModule 只负责登录/JWT/profile新建 RbacModule 负责角色/权限/用户-角色关联。
  • 默认拒绝:全局 PermissionGuard 要求所有接口显式声明权限公开接口login通过 @Public() 豁免。
  • JWT 携带权限:登录时将 User→Role→Permission 链展开为 permissions: string[] 打入 JWT payload避免每次请求查库。

2. 数据模型

2.1 ER 图

┌──────────────┐       ┌──────────────────┐       ┌──────────────┐
│     User     │       │      Role        │       │  Permission  │
├──────────────┤       ├──────────────────┤       ├──────────────┤
│ id (PK)      │       │ id (PK)          │       │ id (PK)      │
│ username(UK) │       │ name (UK)        │       │ code (UK)    │
│ passwordHash │       │ description      │       │ name         │
│ name         │       │ isSystem         │       │ group        │
│ isActive     │       │ status           │       │ description  │
│ lastLoginAt  │       │ createdAt        │       └──────┬───────┘
│ createdAt    │       │ updatedAt        │              │
│ updatedAt    │       └────────┬─────────┘              │
└──────┬───────┘                │                        │
       │                        │                        │
       │ N:M                    │ M:N                    │
       ▼                        ▼                        ▼
┌──────────────┐       ┌──────────────────┐
│  UserRole    │       │ RolePermission   │
├──────────────┤       ├──────────────────┤
│ userId (FK)  │       │ roleId (FK)      │
│ roleId (FK)  │       │ permissionId(FK) │
└──────────────┘       └──────────────────┘

2.2 实体定义

Permissionpermissions 表)

字段 类型 说明
id INT PK AUTO_INCREMENT
code VARCHAR(50) UNIQUE 权限码,格式 module:action
name VARCHAR(50) 中文名称
group VARCHAR(30) 分组(对应模块)
description VARCHAR(200) 可选说明

Roleroles 表)

字段 类型 说明
id INT PK AUTO_INCREMENT
name VARCHAR(30) UNIQUE 角色名称
description VARCHAR(200) 角色描述
isSystem BOOLEAN DEFAULT FALSE 系统预置角色,不可删除/改名
status TINYINT DEFAULT 1 1=启用, 0=禁用
createdAt DATETIME
updatedAt DATETIME

User 实体变更

操作 字段 说明
移除 role VARCHAR(20) 原 admin/operator 硬编码
移除 allowedMenus TEXT 原 JSON 数组,前端菜单可见性
新增 roles @ManyToMany → Role 通过 user_roles 关联表

2.3 权限点清单42 个)

Group 权限码 名称
dashboard dashboard:view 查看数据面板
student student:view 查看学生
student student:create 新增学生
student student:edit 编辑学生
student student:delete 删除学生
student student:import 导入学生
student student:export 导出学生
room room:view 查看宿舍
room room:create 新增宿舍
room room:edit 编辑宿舍
room room:delete 删除宿舍
occupancy occupancy:view 查看入住
occupancy occupancy:checkin 办理入住
occupancy occupancy:checkout 办理退宿
occupancy occupancy:transfer 调换宿舍
expense expense:view 查看费用
expense expense:create 录入费用
expense expense:edit 编辑费用
expense expense:delete 删除费用
bill bill:view 查看账单
bill bill:generate 生成账单
bill bill:confirm 确认账单
bill bill:delete 删除账单
bill bill:export-excel 导出 Excel
bill bill:export-pdf 导出 PDF
deposit deposit:view 查看押金
deposit deposit:create 新增押金
deposit deposit:edit 编辑押金
deposit deposit:delete 删除押金
classroom classroom:view 查看教室
classroom classroom:create 新增教室
classroom classroom:edit 编辑教室
classroom classroom:delete 删除教室
tenant tenant:view 查看租赁方
tenant tenant:create 新增租赁方
tenant tenant:edit 编辑租赁方
tenant tenant:delete 删除租赁方
rental rental:view 查看租赁订单
rental rental:create 新增租赁订单
rental rental:edit 编辑租赁订单
rental rental:delete 删除租赁订单
log log:view 查看操作日志
user user:view 查看用户
user user:create 创建用户
user user:edit 编辑用户
user user:delete 删除用户
user user:reset-password 重置密码
role role:view 查看角色
role role:create 创建角色
role role:edit 编辑角色
role role:delete 删除角色

2.4 预置角色与权限映射

角色 code isSystem 权限范围
超管 super_admin true 全部 42 个权限点
机构负责人 institution_head true 预留角色,暂分配教室/租赁相关 view 权限
老师 teacher true 预留角色,暂分配学生 view 权限
宿管老师 dormitory_supervisor true 学生/宿舍/入住/费用/账单/押金/日志的全部权限 + dashboard:view替代原 operator

注意:机构负责人和老师为占位角色。其实际业务权限点(课表、考勤等)在当前系统中尚不存在,后续 change 再补充。

2.5 设计决策:为什么不用 JSON 字段?

  • JSON 字段无法做数据库级外键约束和联表查询
  • "某权限被哪些角色拥有" → 关联表索引查询 O(log n)JSON 需全表扫描 O(n)
  • 后续数据级权限(机构→班级→学生)扩展时,关联表可直接扩展 Role + 数据策略组合

3. 后端模块设计

3.1 模块拆分

AuthModule瘦身                    RbacModule新建
├── AuthController                   ├── RbacController
│   ├── POST /auth/login             │   ├── GET    /rbac/roles
│   └── GET  /auth/profile           │   ├── GET    /rbac/roles/:id
│                                    │   ├── POST   /rbac/roles
├── AuthService                      │   ├── PUT    /rbac/roles/:id
│   ├── login()  ← 改:查询权限      │   ├── DELETE /rbac/roles/:id
│   ├── validateUser()               │   ├── GET    /rbac/permissions
│   └── initAdmin() ← 转移          │   ├── GET    /rbac/users
│                                    │   ├── POST   /rbac/users
├── JwtStrategy                      │   ├── PUT    /rbac/users/:id
│   └── validate() ← 改:返回权限    │   ├── DELETE /rbac/users/:id
│                                    │   └── PUT    /rbac/users/:id/password
└── JwtAuthGuard不变              │
                                      ├── RbacService
                                      │   ├── getRoles / CRUD
                                      │   ├── getPermissions / getPermissionTree
                                      │   ├── getUserPermissions(userId): string[]
                                      │   ├── createUser / updateUser / deleteUser
                                      │   └── seedData() ← 幂等种子数据
                                      │
                                      └── 导入 TypeOrmModule.forFeature([
                                            User, Role, Permission
                                          ])

关键改动

  1. AuthService.login() → 登录成功后调用 RbacService.getUserPermissions(userId),将结果打入 JWT payload
  2. AuthService.initAdmin() → 移到 RbacService.seedData() 中,因为超管角色和权限种子数据是 RBAC 层的职责
  3. AuthModule 需要 imports: [RbacModule] 或使用 forwardRef 避免循环依赖
  4. /auth/register/auth/users CRUD 全部迁移到 RbacController,变为 /rbac/users

3.2 循环依赖处理

AuthModuleRbacModule 存在双向依赖:

  • AuthService.login() 需要 RbacService.getUserPermissions()
  • RbacController 的用户 CRUD 接口需要 JwtAuthGuard
// auth.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    forwardRef(() => RbacModule),  // 延迟解析
    PassportModule,
    JwtModule.registerAsync({...}),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})

// rbac.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([User, Role, Permission]),
    forwardRef(() => AuthModule),  // 延迟解析(获取 JwtAuthGuard
  ],
  controllers: [RbacController],
  providers: [RbacService],
  exports: [RbacService],
})

4. 权限守卫设计

4.1 装饰器

// backend/src/auth/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

// backend/src/auth/decorators/permission.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const PERMISSION_KEY = 'permissions';
export const RequirePermission = (...permissions: string[]) =>
  SetMetadata(PERMISSION_KEY, permissions);

语义约定

用法 语义
@RequirePermission('student:create') 需要此权限
@RequirePermission('bill:export-excel', 'bill:export-pdf') 满足任一即可OR
@RequirePermission('bill:view') + @RequirePermission('bill:delete') 两个都需满足AND

装饰器多次 SetMetadata 时 NestJS 自动合并为数组,Reflector.get('permissions') 返回 [['bill:view'], ['bill:delete']],以此区分 AND 还是 OR 调用。

4.2 PermissionGuard

// backend/src/auth/guards/permission.guard.ts
@Injectable()
export class PermissionGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // 1. @Public() 豁免
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(), context.getClass(),
    ]);
    if (isPublic) return true;

    // 2. 获取所需权限
    const requiredPermissions = this.reflector.getAllAndOverride<string[][]>(
      PERMISSION_KEY,
      [context.getHandler(), context.getClass()],
    );
    // 无装饰器 = 默认拒绝
    if (!requiredPermissions) return false;

    // 3. 从 JWT payload 获取用户权限
    const { user } = context.switchToHttp().getRequest();
    if (!user?.permissions) return false;

    // 4. 匹配逻辑:外层 AND内层 OR
    return requiredPermissions.every(group =>
      group.some(p => user.permissions.includes(p))
    );
  }
}

匹配算法

装饰器层级:  @ReqPerm(A) @ReqPerm(B) @ReqPerm(C, D)
                            ↓
Reflector 返回: [ [A], [B], [C, D] ]
                            ↓
PermissionGuard: [A]     ∧ [B]      ∧ [C, D]
                  A 满足  ∧ B 满足   ∧ (C 或 D 满足)

超管拥有全部权限 → user.permissions.includes(p) 始终 true → 全部放行。

4.3 全局注册

// app.module.ts — providers 数组新增
{ provide: APP_GUARD, useClass: PermissionGuard },

注意:APP_GUARD 的顺序即为执行顺序。ThrottlerGuard 在前、JwtAuthGuard 在各模块注册、PermissionGuard 全局。

5. JWT 变更

5.1 Payload 结构

变更前: { sub: number, username: string, role: string }
变更后: { sub: number, username: string, permissions: string[] }
  • 移除 role 字段(用户不再有单一角色)
  • 新增 permissions 数组(登录时计算,有效期跟随 JWT 过期,默认 4h

5.2 登录流程

POST /auth/login { username, password }
  → AuthService.login()
    → 验证凭证
    → RbacService.getUserPermissions(userId)
      → SELECT DISTINCT p.code
        FROM permission p
        JOIN role_permission rp ON p.id = rp.permissionId
        JOIN user_role ur ON rp.roleId = ur.roleId
        WHERE ur.userId = ?
    → JWT sign({ sub, username, permissions })
    → 返回 { access_token, user: { id, username, name, roles, permissions } }

5.3 JwtStrategy.validate()

async validate(payload: any) {
  return {
    id: payload.sub,
    username: payload.username,
    permissions: payload.permissions || [],
  };
}

req.user 后续被 PermissionGuard 消费,permissions 数组供匹配。

5.4 设计决策:为什么权限不入库实时查询?

  • 性能:避免每次请求都做 4 表 JOIN
  • JWT 无状态:不依赖数据库连接,适合水平扩展
  • 折中:权限变更在下次登录生效。对极高安全要求的操作(账单确认/删除),可在 Controller 内额外做一次实时校验
  • 体积42 个短字符串约增加 500 bytes在可接受范围

6. 前端架构

6.1 权限基础设施

frontend/src/
├── hooks/
│   └── usePermission.ts        ← 新建
├── components/
│   ├── PermissionButton.tsx    ← 新建
│   └── PermissionRoute.tsx     ← 新建
└── pages/
    ├── Roles/                  ← 新建(角色管理)
    ├── Permissions/            ← 新建(权限一览)
    └── Users/                  ← 重构

6.2 usePermission Hook

// hooks/usePermission.ts
export function usePermission() {
  const permissions: string[] = JSON.parse(
    localStorage.getItem('permissions') || '[]'
  );

  const hasPermission = (code: string) => permissions.includes(code);
  const hasAnyPermission = (...codes: string[]) =>
    codes.some(c => permissions.includes(c));
  const hasAllPermissions = (...codes: string[]) =>
    codes.every(c => permissions.includes(c));

  return { permissions, hasPermission, hasAnyPermission, hasAllPermissions };
}

6.3 PermissionButton

// components/PermissionButton.tsx
// 无权限时 return null隐藏不占界面空间
const PermissionButton: React.FC<{
  permission: string;
  children: React.ReactNode;
} & ButtonProps> = ({ permission, children, ...btnProps }) => {
  const { hasPermission } = usePermission();
  if (!hasPermission(permission)) return null;
  return <Button {...btnProps}>{children}</Button>;
};

6.4 PermissionRoute

// components/PermissionRoute.tsx
// 无权限渲染 403 页面
const PermissionRoute: React.FC<{
  permission: string;
  children: React.ReactNode;
}> = ({ permission, children }) => {
  const { hasPermission } = usePermission();
  if (!hasPermission(permission)) {
    return <Result status="403" title="无权访问" subTitle="您没有访问此页面的权限" />;
  }
  return <>{children}</>;
};

6.5 Login 页面变更

// 登录成功后
localStorage.setItem('token', res.access_token);
localStorage.setItem('user', JSON.stringify(res.user));
localStorage.setItem('permissions', JSON.stringify(res.user.permissions));

6.6 404/403 处理

// api/index.ts — axios 拦截器
// 401 → 跳登录(不变)
// 403 → 不跳转,显示 "权限不足" 提示
if (error.response?.status === 403) {
  message.error('权限不足');
  return Promise.reject(error);
}

6.7 菜单过滤

MainLayout 每项菜单新增 permission 字段:

const menuItems = [
  { key: '/dashboard', icon: <DashboardOutlined />, label: '数据面板', permission: 'dashboard:view' },
  { key: '/students', icon: <TeamOutlined />, label: '学生管理', permission: 'student:view' },
  // ...
];

// 按权限过滤
const { hasPermission } = usePermission();
const visibleItems = menuItems.filter(item => !item.permission || hasPermission(item.permission));

6.8 角色管理页面设计

┌─────────────────────────────────────────────────────────────┐
│  角色管理                                        [+ 新增角色] │
├─────────────────────────────────────────────────────────────┤
│  ID │ 名称     │ 描述         │ 权限标签    │ 系统 │ 操作    │
├─────┼──────────┼──────────────┼─────────────┼──────┼─────────┤
│  1  │ 超管     │ 全部权限     │ 42个权限    │ ✓   │ -       │
│  2  │ 宿管老师 │ 宿舍相关管理 │ 查看/管理   │ ✓   │ 编辑    │
│  3  │ 老师     │ 学生查看     │ 学生:view   │ ✓   │ 编辑    │
│  4  │ 机构负责人│ 教室管理     │ 教室:view   │ ✓   │ 编辑    │
└─────────────────────────────────────────────────────────────┘

点击编辑 → Modal 弹窗:

  • 基本信息:名称(系统角色禁用编辑)、描述
  • 权限勾选区域:按 group 分组,每组一个 Card内含 Checkbox.Group
  • 每组支持全选/取消全选

6.9 用户管理页面变更

变更项 原设计 新设计
角色列 单一 Tagadmin/operator 多角色 Tag 列表
编辑弹窗-角色 Select 单选admin/operator Select mode="multiple"(所有活跃角色)
编辑弹窗-菜单 Checkbox.Group14 项菜单) 移除
新增弹窗 默认 operator 必选角色列表
API 端点 /auth/register/auth/users /rbac/users

7. 迁移策略

7.1 TypeORM Migration 配置

// backend/package.json 新增
{
  "scripts": {
    "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
    "migration:generate": "npm run typeorm -- migration:generate -d src/data-source.ts",
    "migration:run": "npm run typeorm -- migration:run -d src/data-source.ts",
    "migration:revert": "npm run typeorm -- migration:revert -d src/data-source.ts"
  }
}

7.2 迁移脚本步骤

-- 在同一事务中执行
BEGIN;

-- Step 1: 创建新表
CREATE TABLE permissions (...);
CREATE TABLE roles (...);
CREATE TABLE role_permissions (...);
CREATE TABLE user_roles (...);

-- Step 2: 插入种子数据
-- 42 个权限点
INSERT INTO permissions (code, name, `group`) VALUES
  ('dashboard:view', '查看数据面板', 'dashboard'),
  ...

-- 4 个预置角色
INSERT INTO roles (name, description, isSystem) VALUES
  ('超管', '系统超级管理员,拥有全部权限', 1),
  ('宿管老师', '管理宿舍相关业务', 1),
  ('老师', '查看和管理本班学生', 1),
  ('机构负责人', '管理机构教室和课程', 1);

-- 角色-权限关联
INSERT INTO role_permissions (roleId, permissionId)
  SELECT r.id, p.id FROM roles r, permissions p WHERE r.name = '超管';

-- 宿管老师:学生/宿舍/入住/费用/账单/押金/日志/dashboard
INSERT INTO role_permissions (roleId, permissionId)
  SELECT r.id, p.id FROM roles r, permissions p
  WHERE r.name = '宿管老师' AND p.`group` IN ('student', 'room', 'occupancy', 'expense', 'bill', 'deposit', 'log', 'dashboard');

-- Step 3: 迁移现有用户
-- admin → 超管
INSERT INTO user_roles (userId, roleId)
  SELECT u.id, r.id FROM users u, roles r
  WHERE u.role = 'admin' AND r.name = '超管';

-- operator → 宿管老师
INSERT INTO user_roles (userId, roleId)
  SELECT u.id, r.id FROM users u, roles r
  WHERE u.role = 'operator' AND r.name = '宿管老师';

-- Step 4: 删除旧字段
ALTER TABLE users DROP COLUMN role;
ALTER TABLE users DROP COLUMN allowed_menus;

COMMIT;

7.3 回滚策略

BEGIN;
-- 1. 恢复 users 表旧字段(从备份临时表)
ALTER TABLE users ADD COLUMN role VARCHAR(20) DEFAULT 'operator';
ALTER TABLE users ADD COLUMN allowed_menus TEXT;

UPDATE users SET role = COALESCE(
  (SELECT CASE WHEN r.name = '超管' THEN 'admin' ELSE 'operator' END
   FROM user_roles ur JOIN roles r ON ur.roleId = r.id
   WHERE ur.userId = users.id LIMIT 1),
  'operator'
);

-- 2. 删除新表
DROP TABLE IF EXISTS user_roles;
DROP TABLE IF EXISTS role_permissions;
DROP TABLE IF EXISTS roles;
DROP TABLE IF EXISTS permissions;
COMMIT;

7.4 synchronize → migration 切换

AppModule 中 TypeORM 配置将 synchronize: true 改为 synchronize: false(仅对迁移后的环境生效,首次迁移前可保留),migrations: [...] 指向迁移目录。

8. 种子数据初始化

RbacModule.onModuleInit() 中调用 RbacService.seedData()

async seedData() {
  // 1. 权限点INSERT ... WHERE NOT EXISTS (幂等)
  for (const perm of PRESET_PERMISSIONS) {
    await this.permRepo
      .createQueryBuilder()
      .insert().into(Permission)
      .values(perm)
      .orIgnore()  // SQLite: OR IGNORE; MySQL: ON DUPLICATE KEY
      .execute();
  }

  // 2. 角色:同上
  // 3. 角色-权限关联:先查角色和权限,批量 INSERT IGNORE
  // 4. 初始 admin 用户(复用原 initAdmin 逻辑)
}

幂等性保证

  • 权限点:code UNIQUE 约束 + orIgnore()
  • 角色:name UNIQUE 约束 + orIgnore()
  • 角色-权限关联:(roleId, permissionId) 联合唯一约束
  • 重复启动不会重复插入

9. 测试策略

9.1 单元测试

PermissionGuard5 用例)

# 场景 预期
1 @Public() 装饰的接口 放行
2 无装饰器 403
3 @RequirePermission('a'),用户有 'a' 放行
4 @RequirePermission('a'),用户无 'a' 403
5 @ReqPerm('a') + @ReqPerm('b'),用户有 'a' 无 'b' 403AND 不满足)

RbacService.getUserPermissions3 用例)

# 场景 预期
1 单角色用户 返回该角色全部权限
2 多角色用户 返回并集去重
3 无角色用户 返回空数组

9.2 e2e 测试

# 场景 步骤 预期
1 超管全通链路 登录 → GET /students → POST /students 全部 200
2 宿管老师受限链路 登录 → GET /students → 200POST /rbac/roles → 403 学生可查,角色管理拒绝

9.3 迁移验证

  • SQLite 环境:npm run migration:run → 表结构正确 → 种子数据完整
  • MySQL 环境:同上
  • 现有 admin 用户:迁移后登录 → 拥有 super_admin 角色 → 全部权限可访问

10. 风险与缓解

风险 等级 缓解措施
users 表结构变更破坏现有业务 迁移在事务中执行,失败自动回滚;部署前 staging 验证
JWT payload 增大 ~42 个短字符串约 500 bytes可接受后续若权限点过多可改用压缩编码
权限变更在下次登录才生效 关键操作可额外做实时校验;后续可用 Redis 黑名单强制下线
前端改造面大 分步迁移:先建组件 → 逐页替换 → 每步可编译可运行
synchronize → migration 切换有学习成本 保留 synchronize: true 作为本地开发模式migration 仅用于生产部署

11. 后续扩展预留

本次 RBAC 基础设施为以下扩展预留架构空间:

当前阶段(本 change               后续阶段
══════════════════════════          ══════════════════════════
User ↔ Role ↔ Permission            User ↔ Organization数据级
操作级student:create              数据级student:view:org-1
                                    → RBAC + 数据策略组合
  • Permission.code 保持纯粹的操作定义(module:action),不混入数据范围
  • 后续数据级权限通过新增 DataPolicy 实体 + @DataScope 装饰器实现
  • Role 实体预留扩展空间,可关联数据策略

12. 实现偏差记录 (Implementation Divergence)

本节记录 build 阶段验证时发现的、与原始设计不一致但经确认可接受的实现偏差。

12.1 PermissionGuard AND/OR 语义简化

设计 (Design Doc §4.2):

// 嵌套 AND/OR: getAllAndOverride<string[][]> → 返回 [['A'], ['B','C']]
// 外层 AND (every),内层 OR (some)
requiredPermissions.every(group => group.some(p => user.permissions.includes(p)))

实现 (PermissionGuard):

// 扁平 OR: getAllAndMerge<string[]> → 返回 ['A', 'B', 'C']
// 仅 OR 匹配
requiredPermissions.some(p => user.permissions.includes(p))

偏差原因: 当前所有接口均为单一 @RequirePermission(...) 调用,不涉及多次装饰器叠加的 AND 场景。扁平 OR 语义足以覆盖"一个接口需要多个权限中的任一即可访问"的需求,同时简化了匹配逻辑和调试成本。装饰器注释已标注"不支持 AND 语义:多次调用装饰器会被全局 PermissionGuard 合并为扁平数组"。

影响: 无。当前无接口需要 AND 权限组合。若未来业务需要"同时满足多个权限才可访问",届时再升级匹配算法(将 getAllAndMerge 改为 getAllAndOverride + 双层循环),向后兼容。

12.2 权限点数量

设计文档声称 42 个权限点,实际实现 52 个。差异来自:

  • 设计文档手动列举时遗漏 occupancy:delete 等权限点
  • 设计文档实际列举 51 个而非 42 个(计数偏差)

所有 52 个权限点均符合 module:action 命名规范且与模块目录结构一致,实现正确。