# 用户权限模块审计报告

> 审计范围：用户权限（RBAC）模块，包括 `shared/types/permissions.ts`、`shared/lib/permissions.ts`、`shared/lib/auth-guard.ts`、`shared/hooks/use-permission.ts`、`shared/lib/role-utils.ts`、`shared/lib/session.ts`、`auth.ts`、`modules/rbac/*`、`modules/users/*`（权限相关部分）、`app/(dashboard)/admin/{roles,permissions,users}/*`。
> 审计时间：2026-06-24
> 审计依据：项目规则 `.trae/rules/project_rules.md`、架构影响地图 `004/005`。

---

## 一、现有实现概要

### 1.1 文件分布与行数

| 层 | 文件 | 行数 | 职责 |
|---|---|---|---|
| shared/types | `permissions.ts` | 223 | 67 个权限点常量 + `Role`/`BuiltinRole`/`Permission`/`DataScope`/`AuthContext` 类型 |
| shared/lib | `permissions.ts` | 301 | `ROLE_PERMISSIONS_SEED`（6 内置角色种子）+ `resolvePermissions()`（DB 查询 + 种子兜底） |
| shared/lib | `auth-guard.ts` | 176 | `getAuthContext()` / `requirePermission()` / `checkPermission()` / `resolveDataScope()` / `requireAuth()` |
| shared/lib | `role-utils.ts` | 34 | `normalizeRole()` / `resolvePrimaryRole()` 纯函数 |
| shared/lib | `session.ts` | 35 | `getSession()`（动态 import `@/auth` 避免循环依赖） |
| shared/hooks | `use-permission.ts` | 53 | 客户端 `usePermission()` Hook（`hasPermission`/`hasAnyPermission`/`hasAllPermissions`/`hasRole`） |
| app/root | `auth.ts` | 234 | NextAuth v5 配置（Credentials + JWT + Session 回调 + 登录事件） |
| modules/rbac | `data-access.ts` | 215 | 角色 CRUD + `getRolePermissions`/`setRolePermissions` + `ADMIN_ROLE_NAME` 常量 |
| modules/rbac | `data-access-assignments.ts` | 161 | 用户-角色分配（`getUserRoleNames`/`assignRolesToUser`/`getUserRoleAssignments`） |
| modules/rbac | `actions.ts` | 382 | 8 个 Server Action（含 `requirePermission` + Zod + 审计日志） |
| modules/rbac | `schema.ts` | 42 | 4 个 Zod schema |
| modules/rbac | `types.ts` | 56 | `RoleRecord`/`RoleWithStats`/`RoleDetail`/`CreateRoleInput`/`UpdateRoleInput`/`UserRoleAssignment`/`PaginatedResult` |
| modules/rbac/lib | `permission-catalog.ts` | 268 | `PERMISSION_CATALOG`（24 分组）+ `getAllPermissionMetas`/`getPermissionMeta` |
| modules/rbac/components | 7 个文件 | ~600 | `RoleList`/`RoleFormDialog`/`RolePermissionMatrix`/`PermissionCatalogView`/`UserRoleAssignDialog`/`RoleManagementView`/`RoleDetailEditButton` |
| modules/users | `data-access.ts` | 424 | 用户查询 + `getCurrentStudentUser` + `getAdminUsers`/`getAdminUserRoles` |
| modules/users | `actions.ts` | 218 | `updateUserProfile`/`importUsersAction`/`exportUsersAction`/`updateUserRoleAction`（空实现）/`deleteUserAction` |
| app/(dashboard)/admin | 4 个 page.tsx | ~200 | 角色列表/角色详情/权限目录/用户管理页面 |

### 1.2 数据流

```
登录 (auth.ts authorize)
  └─▶ db.query.users → 校验密码 → 查询 usersToRoles → resolvePrimaryRole
  └─▶ JWT callback: resolvePermissions(allRoles) → 写入 token.permissions
  └─▶ Session callback: 透传到 session.user.permissions

请求 (Server Component / Server Action)
  └─▶ getAuthContext() → getSession() → resolveDataScope(userId, roles) → AuthContext
  └─▶ requirePermission(p) → 校验 ctx.permissions.includes(p)

客户端 (Client Component)
  └─▶ usePermission() → useSession() → hasPermission(p)
```

### 1.3 架构图记录情况

架构影响地图 `004` 和数据 JSON `005` 已记录 `rbac` 模块（JSON 第 16204-16603 行），包含：
- 8 个 actions、11 个 data-access 函数、3 个 lib 常量/函数、8 个组件
- 依赖关系：`shared/db`、`shared/types/permissions`、`shared/lib/auth-guard`、`shared/lib/audit-logger`、`shared/lib/change-logger`
- 被使用方：`admin/roles`、`admin/permissions`、`admin/users`、`modules/users`
- i18n 命名空间 `rbac`，文件 `zh-CN/rbac.json` + `en/rbac.json`

**架构图遗漏**：
- `shared/lib/permissions.ts`（`resolvePermissions`）未在 rbac 模块依赖中显式列出（实际被 `auth.ts` 使用）
- `shared/lib/auth-guard.ts` 的 `resolveDataScope` 内部直接查询 `classes`/`classEnrollments`/`classSubjectTeachers`/`grades`/`parentStudentRelations` 表，属于跨模块数据访问，未在架构图中标注
- `modules/users/data-access.ts` 的 `getCurrentStudentUser` 直接调用 `auth()` 而非 `getAuthContext()`，未在架构图中标注此异常依赖
- `app/(dashboard)/admin/permissions/page.tsx` 直接查询 `rolePermissions`/`roles` 表，未走 data-access，架构图未标注此违规
- `modules/users/actions.ts` 的 `deleteUserAction` 直接 `db.delete(users)`，未走 data-access，架构图未标注
- `app/(dashboard)/admin/roles`、`admin/permissions`、`admin/users`（非 import 子目录）缺少 `loading.tsx`/`error.tsx`，架构图未标注

---

## 二、现存问题与原因分析

### 2.1 架构分层违规（P0）

#### 问题 2.1.1 — `app/` 直接访问数据库
- **位置**：[admin/permissions/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/permissions/page.tsx) 第 28-32 行
- **问题**：页面组件直接 `db.select(...).from(rolePermissions).innerJoin(roles, ...)` 查询数据库，绕过 data-access 层。
- **违反规则**：`app/ 只能调用 modules/ 的 Server Actions 和 data-access，不直接访问数据库`（项目规则「架构分层规则」）。
- **后果**：权限统计查询逻辑散落在页面中，无法复用、无法测试、无法统一加缓存；若 `rolePermissions` 表结构变更需修改多处。

#### 问题 2.1.2 — Server Action 直接访问数据库
- **位置**：[users/actions.ts](file:///e:/Desktop/CICD/src/modules/users/actions.ts) 第 208 行 `await db.delete(users).where(eq(users.id, userId))`
- **问题**：`deleteUserAction` 直接在 actions 层执行 DB 删除，未下沉到 data-access。
- **违反规则**：`app/ 只能调用 modules/ 的 Server Actions 和 data-access` + `严格三层架构，依赖方向单向`（项目规则「架构分层规则」）。
- **后果**：删除用户逻辑（应包含级联清理 sessions、usersToRoles、passwordSecurity 等）散落在 actions 层，易遗漏级联清理，造成孤儿数据。

#### 问题 2.1.3 — 客户端组件直接调用 fetch API
- **位置**：[admin-users-view.tsx](file:///e:/Desktop/CICD/src/modules/users/components/admin-users-view.tsx) 第 120 行 `fetch("/api/admin/users/" + deleteUserId, { method: "DELETE" })`
- **问题**：客户端组件通过 `fetch` 调用 API 路由删除用户，而非调用 Server Action。
- **违反规则**：`app/ 只能调用 modules/ 的 Server Actions 和 data-access`（项目规则「架构分层规则」）。
- **后果**：绕过 Server Action 的权限校验、Zod 验证、审计日志；存在未授权调用风险；删除逻辑出现两套（Server Action + API 路由）。

#### 问题 2.1.4 — `getCurrentStudentUser` 使用 `auth()` 而非 `getAuthContext()`
- **位置**：[users/data-access.ts](file:///e:/Desktop/CICD/src/modules/users/data-access.ts) 第 232 行 `const session = await auth()`
- **问题**：直接调用 `auth()` 获取 session，而非使用项目统一的 `getAuthContext()`。
- **违反规则**：`Authentication must use getAuthContext() and getCurrentStudentUser(); avoid mixing with other auth methods`（项目记忆 Hard Constraints）。
- **后果**：认证入口不统一，`getAuthContext` 提供的 `dataScope` 等上下文信息丢失；后续若在 session 校验逻辑中增加 IP 限制、租户隔离等，此处不会自动生效。

#### 问题 2.1.5 — `resolveDataScope` 跨模块直接查询数据库表
- **位置**：[auth-guard.ts](file:///e:/Desktop/CICD/src/shared/lib/auth-guard.ts) 第 70-163 行
- **问题**：`resolveDataScope` 直接查询 `classes`、`classEnrollments`、`classSubjectTeachers`、`grades`、`parentStudentRelations` 表，这些表属于 `school`、`classes`、`parent` 等模块。
- **违反规则**：`模块间只能通过对方 data-access 通信，禁止跨模块直接查询数据库表`（项目规则「架构分层规则」）。
- **后果**：`shared/lib` 反向依赖业务模块的表结构；若 `classes` 模块重构表结构，`auth-guard` 会编译报错；`shared/` 不得反向依赖业务模块的硬约束被破坏。

### 2.2 权限校验缺失或硬编码（P0/P1）

#### 问题 2.2.1 — `updateUserRoleAction` 是空实现
- **位置**：[users/actions.ts](file:///e:/Desktop/CICD/src/modules/users/actions.ts) 第 177-196 行
- **问题**：`updateUserRoleAction` 接收 `userId` 和 `role` 参数后，仅 `void userId; void role;` 直接返回成功，未执行任何实际操作。
- **违反规则**：`所有 Server Action 必须调用 requirePermission() 进行权限校验`（虽已调用 `requirePermission`，但逻辑为空）+ 功能完整性。
- **后果**：管理员在用户管理页点击「编辑」试图修改角色时，系统提示成功但实际未变更，属于功能性 Bug。

#### 问题 2.2.2 — `resolveDataScope` 角色硬编码
- **位置**：[auth-guard.ts](file:///e:/Desktop/CICD/src/shared/lib/auth-guard.ts) 第 64、69、81、113、136 行
- **问题**：使用 `roleNames.includes("admin")`、`roleNames.includes("grade_head")`、`roleNames.includes("teacher")`、`roleNames.includes("student")`、`roleNames.includes("parent")` 硬编码角色名。
- **违反规则**：`前端权限判断统一使用 usePermission().hasPermission()，严禁出现 role === "xxx" 硬编码`（项目规则虽针对前端，但服务端 DataScope 解析也应避免硬编码角色名，以支持动态角色）。
- **后果**：新增自定义角色无法正确解析 DataScope；`grade_head`/`teaching_head` 被合并处理，无法差异化授权。

#### 问题 2.2.3 — `role-utils.ts` 将 `grade_head`/`teaching_head` 折叠为 `teacher`
- **位置**：[role-utils.ts](file:///e:/Desktop/CICD/src/shared/lib/role-utils.ts) 第 17 行
- **问题**：`normalizeRole` 将 `grade_head`/`teaching_head` 映射为 `teacher`，丢失了角色差异。
- **后果**：`auth.ts` JWT 回调中 `token.role = resolvePrimaryRole(allRoles)` 后，年级主任/教务主任的 `session.user.role` 变为 `teacher`，前端若依赖 `role` 字段做路由跳转会丢失差异。

### 2.3 国际化遗漏（P1）

#### 问题 2.3.1 — RBAC 组件大量硬编码英文
- **位置**：
  - [role-list.tsx](file:///e:/Desktop/CICD/src/modules/rbac/components/role-list.tsx) 第 84、88、98、102、140、151、153、173、179、190、216、227 行（"No roles yet"、"Create Role"、"Roles"、"System"、"Custom"、"Enabled"、"Disabled"、"Edit"、"Enable"、"Disable"、"Delete"、"Deleting..."）
  - [role-form-dialog.tsx](file:///e:/Desktop/CICD/src/modules/rbac/components/role-form-dialog.tsx) 第 74、78-79、89、99、109、125 行（"Create new role"、"Edit role"、"Role name"、"Description"、"Cancel"、"Saving..."）
  - [user-role-assign-dialog.tsx](file:///e:/Desktop/CICD/src/modules/rbac/components/user-role-assign-dialog.tsx) 第 102、104-107、113、137、151、153、173 行（"Assign roles"、"Save roles"、"Cancel"、"Saving..."）
  - [permission-catalog-view.tsx](file:///e:/Desktop/CICD/src/modules/rbac/components/permission-catalog-view.tsx) 第 35、44 行（"Permission Catalog"、"All permission points..."）
  - [admin/roles/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/roles/page.tsx) 第 28-31 行
  - [admin/roles/[id]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/roles/[id]/page.tsx) 第 53、56、63、65、78-80 行
  - [admin/permissions/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/permissions/page.tsx) 第 43-44 行
- **违反规则**：`所有用户可见文本必须适配 i18n（使用 next-intl），提取翻译键`（项目规则「安全规范」+ 项目记忆 Hard Constraints）。
- **后果**：中文用户看到英文界面，体验不一致；无法切换语言。

#### 问题 2.3.2 — `admin-users-view.tsx` 大量硬编码中文
- **位置**：[admin-users-view.tsx](file:///e:/Desktop/CICD/src/modules/users/components/admin-users-view.tsx) 第 142、148、159、169、172、180、190、203、211-216、222、227、247、252、260、273、284、291、305、307、311、317 行
- **问题**：与 2.3.1 相反，此处全部硬编码中文（"用户管理"、"批量导入"、"搜索姓名或邮箱..."、"所有角色"、"搜索"、"重置"、"暂无用户"、"姓名"、"邮箱"、"角色"、"手机"、"注册时间"、"操作"、"编辑"、"分配角色"、"删除"、"确认删除用户？"等）。
- **违反规则**：同 2.3.1。
- **后果**：英文用户看到中文界面；与 RBAC 其他组件语言不一致（一半英文一半中文）。

#### 问题 2.3.3 — `rbac.json` 缺少权限点标签
- **位置**：[zh-CN/rbac.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/rbac.json) + [en/rbac.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/en/rbac.json)
- **问题**：`PERMISSION_CATALOG` 中每个权限点都定义了 `labelKey`（如 `rbac:permissions.exam.create`）和 `descriptionKey`，但 i18n 文件中只有 `permissions.group.*` 分组标签，缺少 `permissions.exam.create`、`permissions.exam.create.desc` 等具体权限点的翻译。
- **后果**：`RolePermissionMatrix` 和 `PermissionCatalogView` 中 `t.has(labelKey) ? t(labelKey) : perm.key` 回退到显示原始 key（如 `EXAM_CREATE`），用户无法理解权限含义。

### 2.4 TypeScript 类型不安全（P1）

#### 问题 2.4.1 — `as` 断言违规
- **位置**：
  - [permissions.ts](file:///e:/Desktop/CICD/src/shared/lib/permissions.ts) 第 279、296 行 `row.permission as Permission`
  - [auth-guard.ts](file:///e:/Desktop/CICD/src/shared/lib/auth-guard.ts) 第 27-28 行 `session.user.roles as Role[]`、`session.user.permissions as Permission[]`
  - [auth.ts](file:///e:/Desktop/CICD/src/auth.ts) 第 146 行 `user as { id: string; ... }`、第 196 行 `token.permissions as typeof token.permissions`、第 217-223 行 `message as { ... }`
  - [data-access.ts](file:///e:/Desktop/CICD/src/modules/rbac/data-access.ts) 第 81、186 行 `row.permission as Permission`
  - [data-access-assignments.ts](file:///e:/Desktop/CICD/src/modules/rbac/data-access-assignments.ts) 第 149 行 `params.role as string`
  - [admin/permissions/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/permissions/page.tsx) 第 36 行 `row.permission as Permission`
  - [users/actions.ts](file:///e:/Desktop/CICD/src/modules/users/actions.ts) 第 183-184、207 行 `formData.get(...) as string`
  - [admin-users-view.tsx](file:///e:/Desktop/CICD/src/modules/users/components/admin-users-view.tsx) 第 128 行 `e as Error`
- **违反规则**：`禁止 as 断言（除非从 unknown 转换或测试中，需注释原因）`（项目规则「TypeScript 规则」）。
- **后果**：类型安全被绕过，若 DB schema 或 session 结构变更，编译期不报错，运行时才暴露。

### 2.5 错误处理与边界缺失（P1）

#### 问题 2.5.1 — 缺少 `loading.tsx` / `error.tsx`
- **位置**：
  - `app/(dashboard)/admin/roles/` — 无 `loading.tsx`、`error.tsx`
  - `app/(dashboard)/admin/roles/[id]/` — 无 `loading.tsx`、`error.tsx`
  - `app/(dashboard)/admin/permissions/` — 无 `loading.tsx`、`error.tsx`
  - `app/(dashboard)/admin/users/` — 无 `loading.tsx`、`error.tsx`（仅 `users/import/` 有）
- **违反规则**：`All student routes must include loading.tsx and error.tsx for error boundaries`（项目记忆 Hard Constraints，虽针对 student 路由，但 admin 路由也应遵循）。
- **后果**：页面加载时无骨架屏，体验差；`requirePermission` 抛出 `PermissionDeniedError` 时无友好错误页，显示 Next.js 默认错误。

#### 问题 2.5.2 — 无 React Error Boundary 包裹独立数据区块
- **位置**：所有 RBAC 组件
- **问题**：`RoleList`、`RolePermissionMatrix`、`PermissionCatalogView` 等组件未用 Error Boundary 包裹，单个组件抛错会导致整页崩溃。
- **后果**：权限矩阵加载失败时，角色列表也无法显示。

#### 问题 2.5.3 — 无空数据/无权限/网络异常边界状态
- **位置**：
  - `RolePermissionMatrix` 无空权限组处理
  - `UserRoleAssignDialog` 无网络异常重试
  - `PermissionCatalogView` 无加载骨架
- **后果**：异常状态下用户看到空白或卡顿。

### 2.6 组件不可复用 / 逻辑未抽取（P2）

#### 问题 2.6.1 — `admin-users-view.tsx` 与 RBAC 组件重复实现
- **位置**：[admin-users-view.tsx](file:///e:/Desktop/CICD/src/modules/users/components/admin-users-view.tsx)
- **问题**：`AdminUsersView` 自行实现了删除确认对话框、分页、搜索、角色筛选，未复用 RBAC 模块的 `EmptyState`、分页组件等；同时硬编码了 `UserRoleAssignDialog` 的调用。
- **后果**：UI 不一致，维护成本高。

#### 问题 2.6.2 — `admin-users-view.tsx` 包含死代码
- **位置**：第 246-248 行「编辑」菜单项
- **问题**：`DropdownMenuItem` 点击无任何行为，未绑定 `onClick`。
- **后果**：用户点击「编辑」无反应，体验差。

#### 问题 2.6.3 — 纯逻辑与 UI 未分离
- **位置**：`RolePermissionMatrix` 中的 `setsEqual` 函数、选中状态计算逻辑
- **问题**：`setsEqual` 工具函数内联在组件文件底部，未抽取到 hooks 或 utils；选中状态 diff 逻辑未抽取。
- **后果**：无法单测，无法复用。

### 2.7 可访问性缺失（P2）

#### 问题 2.7.1 — 缺少 ARIA 属性与键盘导航
- **位置**：所有 RBAC 组件
- **问题**：
  - `RoleList` 表格无 `<caption>` 描述
  - `RolePermissionMatrix` 的 checkbox 分组无 `fieldset`/`legend` 语义
  - `UserRoleAssignDialog` 的角色列表无 `role="list"`
  - 图标按钮（如 `MoreHorizontal` 触发器）虽有 `sr-only` 但无 `aria-label`
- **违反规则**：`可访问性（a11y）：语义化标签、ARIA 属性、键盘导航`（项目规则「企业级补充」）。
- **后果**：屏幕阅读器用户无法理解页面结构；键盘用户操作困难。

### 2.8 性能与监控缺失（P2）

#### 问题 2.8.1 — 无 React Server Components 流式渲染
- **位置**：所有 admin 页面
- **问题**：页面使用 `export const dynamic = "force-dynamic"` 但未使用 `loading.tsx` 或 Suspense 流式渲染，整页阻塞等待所有数据。
- **后果**：首屏白屏时间长。

#### 问题 2.8.2 — 无关键操作埋点
- **位置**：`assignUserRolesAction`、`setRolePermissionsAction`
- **问题**：虽有 `logAudit` 审计日志，但无前端交互埋点（如权限变更成功率、耗时、失败原因分布）。
- **后果**：无法监控权限管理操作的健康度。

### 2.9 安全性隐患（P1）

#### 问题 2.9.1 — `deleteUserAction` 未校验目标用户是否为内置角色持有者
- **位置**：[users/actions.ts](file:///e:/Desktop/CICD/src/modules/users/actions.ts) 第 201-217 行
- **问题**：删除用户时未检查该用户是否为最后一个 admin 角色持有者，可能导致系统无管理员。
- **后果**：管理员误删后无法恢复。

#### 问题 2.9.2 — `assignRolesToUser` 未校验角色是否为内置且未禁用
- **位置**：[data-access-assignments.ts](file:///e:/Desktop/CICD/src/modules/rbac/data-access-assignments.ts) 第 42-73 行
- **问题**：`assignRolesToUser` 仅校验角色名存在，未校验角色是否 `isEnabled`，可将禁用角色分配给用户（虽 `resolvePermissions` 会过滤禁用角色，但数据不一致）。
- **后果**：用户被分配禁用角色后，UI 显示已分配但实际无权限，造成困惑。

---

## 三、行业差距对比

### 3.1 权限模型维度

| 能力 | 行业优秀实践（如 PowerSchool、Veracross、Alma） | 当前实现 | 差距影响 |
|---|---|---|---|
| 权限继承 | 支持权限继承（如 admin 继承 teacher 所有权限） | 扁平权限，无继承 | 新增角色需手动勾选所有权限，易遗漏 |
| 权限模板 | 提供角色模板（如「班主任」「学科组长」）一键创建 | 无模板，从零配置 | 管理员配置成本高 |
| 权限预览 | 保存前预览「该角色将获得哪些菜单/操作」 | 仅显示权限点列表 | 管理员无法直观理解权限效果 |
| 权限差异对比 | 对比两个角色的权限差异 | 无 | 无法评估角色调整影响 |
| 权限使用统计 | 统计每个权限点被多少角色/用户使用 | `PermissionCatalogView` 仅统计角色数，未统计用户数 | 无法识别「僵尸权限」 |
| 数据范围权限 | 支持「只能看自己班级」「只能看本年级」等行级权限 | `DataScope` 已实现但硬编码角色 | 自定义角色无法配置数据范围 |
| 权限生效时间 | 支持权限定时生效/过期 | 无 | 临时授权需手动撤销 |

### 3.2 UI/UX 维度

| 能力 | 行业优秀实践 | 当前实现 | 差距影响 |
|---|---|---|---|
| 权限矩阵搜索 | 支持按权限名/模块搜索过滤 | 无搜索，67 个权限点全展示 | 管理员查找特定权限困难 |
| 权限分组折叠 | 支持折叠/展开权限分组 | 分组固定展开 | 页面过长，滚动疲劳 |
| 批量角色分配 | 支持批量给多个用户分配角色 | 仅单个用户 | 批量入职时效率低 |
| 角色克隆 | 复制现有角色创建新角色 | 无 | 相似角色需重新勾选 |
| 权限变更审计可视化 | 时间轴展示角色权限变更历史 | 仅审计日志文本 | 无法直观追溯权限演进 |
| 权限影响范围分析 | 显示「修改此角色将影响 N 个用户」 | `RoleList` 显示 userCount 但编辑时不提示 | 管理员不知修改影响范围 |
| 实时权限校验 | 前端实时显示当前用户是否有权限操作 | `usePermission` 已实现但未在 UI 普遍使用 | 用户点击后才报无权限 |

### 3.3 多角色支持维度

| 能力 | 行业优秀实践 | 当前实现 | 差距影响 |
|---|---|---|---|
| 多角色叠加 | 用户多角色权限取并集 | `resolvePermissions` 已合并 | ✅ 已实现 |
| 主角色判定 | 支持配置主角色（用于路由/默认视图） | `resolvePrimaryRole` 硬编码优先级 | 无法自定义主角色 |
| 角色冲突检测 | 检测互斥角色（如 teacher + student） | 无 | 用户可同时拥有冲突角色 |
| 角色有效期 | 角色分配支持起止时间 | 无 | 临时角色无法自动撤销 |

### 3.4 安全合规维度

| 能力 | 行业优秀实践 | 当前实现 | 差距影响 |
|---|---|---|---|
| 最小权限原则校验 | 提示「该角色权限过大」 | 无 | 易授予过度权限 |
| 敏感权限二次确认 | 修改 admin/删除角色需二次确认 | `RoleList` 有删除确认，但改权限无 | 误操作风险 |
| 权限变更通知 | 权限变更通知受影响用户 | 无 | 用户不知权限被调整 |
| 权限分离审计 | 审计日志记录操作者 IP/UA | `logAudit` 未记录 IP/UA | 无法追溯操作来源 |

---

## 四、改进优先级建议

### P0 — 紧急（安全/架构违规，必须立即修复）

| 编号 | 问题 | 改进方向 |
|---|---|---|
| P0-1 | `admin/permissions/page.tsx` 直查 DB（问题 2.1.1） | 将权限统计查询下沉到 `modules/rbac/data-access.ts`，新增 `getPermissionRoleCounts()` 函数 |
| P0-2 | `deleteUserAction` 直查 DB（问题 2.1.2） | 将删除逻辑下沉到 `modules/users/data-access.ts`，新增 `deleteUserById()` 并处理级联清理 |
| P0-3 | `admin-users-view.tsx` 用 fetch 删除用户（问题 2.1.3） | 改为调用 `deleteUserAction` Server Action |
| P0-4 | `getCurrentStudentUser` 用 `auth()`（问题 2.1.4） | 改为调用 `getAuthContext()` 获取 userId |
| P0-5 | `updateUserRoleAction` 空实现（问题 2.2.1） | 删除此死代码，角色分配统一走 `rbac/actions.ts` 的 `assignUserRolesAction` |
| P0-6 | 缺少 `loading.tsx`/`error.tsx`（问题 2.5.1） | 为 `admin/roles`、`admin/roles/[id]`、`admin/permissions`、`admin/users` 新增 |

### P1 — 高优先级（i18n/类型安全/错误处理）

| 编号 | 问题 | 改进方向 |
|---|---|---|
| P1-1 | RBAC 组件硬编码英文（问题 2.3.1） | 提取翻译键到 `rbac.json`，组件改用 `useTranslations` |
| P1-2 | `admin-users-view.tsx` 硬编码中文（问题 2.3.2） | 提取翻译键到 `users.json` |
| P1-3 | `rbac.json` 缺权限点标签（问题 2.3.3） | 补全 67 个权限点的 `label` + `desc` 翻译 |
| P1-4 | `as` 断言违规（问题 2.4.1） | 用类型守卫替代，如 `isPermission(value): value is Permission` |
| P1-5 | `resolveDataScope` 跨模块查表（问题 2.1.5） | 改为调用 `classes/data-access`、`parent/data-access` 等模块的查询函数 |
| P1-6 | `resolveDataScope` 角色硬编码（问题 2.2.2） | 改为基于权限点判断（如 `hasPermission(DASHBOARD_ADMIN_READ)` → `all` scope）或配置驱动 |
| P1-7 | 无 Error Boundary（问题 2.5.2） | 为 `RoleList`、`RolePermissionMatrix`、`PermissionCatalogView` 包裹 Error Boundary |
| P1-8 | `deleteUserAction` 未保护最后 admin（问题 2.9.1） | 删除前校验目标用户是否为最后一个 admin |
| P1-9 | `assignRolesToUser` 未校验角色启用状态（问题 2.9.2） | 分配前过滤 `isEnabled = false` 的角色 |

### P2 — 中长期（体验/性能/可扩展性）

| 编号 | 问题 | 改进方向 |
|---|---|---|
| P2-1 | 权限矩阵无搜索/折叠 | 新增搜索框 + 分组折叠交互 |
| P2-2 | 无角色模板 | 预置「班主任」「学科组长」等模板 |
| P2-3 | 无权限变更影响提示 | 编辑权限时显示「将影响 N 个用户」 |
| P2-4 | 无权限使用统计 | `PermissionCatalogView` 增加用户数统计 |
| P2-5 | 无 a11y 属性 | 补充 ARIA、`caption`、`fieldset` |
| P2-6 | 无 RSC 流式渲染 | 引入 Suspense + 骨架屏 |
| P2-7 | 无关键操作埋点 | 预留 `trackPermissionChange()` 接口 |
| P2-8 | `role-utils.ts` 折叠角色（问题 2.2.3） | 保留 `grade_head`/`teaching_head` 差异 |
| P2-9 | 无权限继承 | 支持角色继承父角色权限 |
| P2-10 | 无权限变更通知 | 权限变更后通知受影响用户 |

---

## 五、架构图同步说明

本次审计发现架构图存在以下遗漏/不一致，需同步更新：

### 5.1 `004_architecture_impact_map.md` 需补充

1. **`shared/lib/auth-guard.ts` 的 `resolveDataScope` 跨模块依赖**：
   - 当前依赖 `classes`、`classEnrollments`、`classSubjectTeachers`、`grades`、`parentStudentRelations` 表
   - 应标注为「待重构：改为调用各模块 data-access」

2. **`modules/users/data-access.ts` 的 `getCurrentStudentUser` 异常依赖**：
   - 直接调用 `auth()` 而非 `getAuthContext()`
   - 应标注为「待修复：P0-4」

3. **`app/(dashboard)/admin/permissions/page.tsx` 直查 DB 违规**：
   - 应标注为「待修复：P0-1」

4. **`modules/users/actions.ts` 的 `deleteUserAction` 直查 DB**：
   - 应标注为「待修复：P0-2」

5. **缺失的 `loading.tsx`/`error.tsx`**：
   - `admin/roles`、`admin/roles/[id]`、`admin/permissions`、`admin/users` 四个路由

### 5.2 `005_architecture_data.json` 需补充

1. `modules.rbac.dependencies.dependsOn` 增加 `shared/lib/permissions`（`resolvePermissions` 被 `auth.ts` 使用，但 rbac 模块本身不直接依赖，需在 `auth` 节点标注）
2. `modules.rbac.exports.dataAccess` 增加 `getPermissionRoleCounts`（P0-1 新增函数）
3. `modules.users.exports.dataAccess` 增加 `deleteUserById`（P0-2 新增函数）
4. `modules.users.exports.actions` 标注 `updateUserRoleAction` 为 deprecated/删除
5. `app` 路由节点增加缺失的 `loading.tsx`/`error.tsx` 文件

---

## 六、重构方案设计

### 6.1 目标架构

```
app/(dashboard)/admin/
├─ roles/
│  ├─ page.tsx              # RSC: 调用 rbac/data-access.getRoles()
│  ├─ loading.tsx          # 骨架屏
│  ├─ error.tsx            # 错误边界
│  └─ [id]/
│     ├─ page.tsx           # RSC: 调用 rbac/data-access.getRoleById()
│     ├─ loading.tsx
│     └─ error.tsx
├─ permissions/
│  ├─ page.tsx              # RSC: 调用 rbac/data-access.getPermissionRoleCounts()
│  ├─ loading.tsx
│  └─ error.tsx
└─ users/
   ├─ page.tsx              # RSC: 调用 users/data-access.getAdminUsers()
   ├─ loading.tsx
   └─ error.tsx

modules/rbac/
├─ actions.ts               # Server Actions（编排层）
├─ data-access.ts           # 角色 CRUD + 权限统计
├─ data-access-assignments.ts # 用户-角色分配
├─ data-access-permissions.ts # 权限查询（新增 getPermissionRoleCounts）
├─ schema.ts                # Zod 验证
├─ types.ts                 # 类型定义
├─ lib/
│  ├─ permission-catalog.ts # 权限目录常量
│  └─ permission-diff.ts    # 权限差异计算纯函数（新增）
├─ hooks/
│  ├─ use-role-permissions.ts # 选中状态管理 Hook（新增）
│  └─ use-permission-search.ts # 权限搜索过滤 Hook（新增）
└─ components/
   ├─ role-list.tsx
   ├─ role-form-dialog.tsx
   ├─ role-permission-matrix.tsx
   ├─ permission-catalog-view.tsx
   ├─ user-role-assign-dialog.tsx
   ├─ role-management-view.tsx
   ├─ role-detail-edit-button.tsx
   ├─ permission-search-bar.tsx  # 新增
   ├─ permission-impact-badge.tsx # 新增：显示影响用户数
   └─ error-boundary.tsx        # 新增：RBAC 专用错误边界

modules/users/
├─ actions.ts               # 删除 updateUserRoleAction，deleteUserAction 改调 data-access
├─ data-access.ts           # 新增 deleteUserById
└─ components/
   └─ admin-users-view.tsx  # i18n 化，改用 Server Action

shared/lib/
├─ auth-guard.ts            # resolveDataScope 改调各模块 data-access
├─ permissions.ts           # 用类型守卫替代 as
├─ role-utils.ts            # 保留 grade_head/teaching_head 差异
└─ type-guards.ts           # 新增 isPermission/isRole 类型守卫
```

### 6.2 数据服务接口抽象（解耦）

```typescript
// modules/rbac/types.ts 新增

/** 角色数据服务接口 — 供依赖注入使用 */
export interface RoleDataService {
  getRoles(): Promise<RoleWithStats[]>
  getRoleById(id: string): Promise<RoleDetail | null>
  createRole(input: CreateRoleInput): Promise<RoleRecord>
  updateRole(id: string, input: UpdateRoleInput): Promise<RoleRecord>
  deleteRole(id: string): Promise<void>
  setRoleEnabled(id: string, enabled: boolean): Promise<RoleRecord>
  getRolePermissions(roleId: string): Promise<Permission[]>
  setRolePermissions(roleId: string, permissions: Permission[]): Promise<void>
  getPermissionRoleCounts(): Promise<Record<string, number>>
}

/** 用户-角色分配数据服务接口 */
export interface UserRoleAssignmentService {
  getUserRoleNames(userId: string): Promise<string[]>
  assignRolesToUser(userId: string, roleNames: string[]): Promise<void>
  getUserRoleAssignments(params?: {
    page?: number
    pageSize?: number
    search?: string
    role?: string
  }): Promise<PaginatedResult<UserRoleAssignment>>
}

// modules/rbac/data-access.ts 实现 RoleDataService
// modules/rbac/data-access-assignments.ts 实现 UserRoleAssignmentService
```

### 6.3 组合优先的 UI 设计

```tsx
// modules/rbac/components/role-permission-matrix.tsx 重构后

"use client"

import { useRolePermissions } from "../hooks/use-role-permissions"
import { usePermissionSearch } from "../hooks/use-permission-search"
import { PermissionSearchBar } from "./permission-search-bar"
import { PermissionImpactBadge } from "./permission-impact-badge"
import { ErrorBoundary } from "./error-boundary"

interface RolePermissionMatrixProps {
  roleId: string
  roleName: string
  currentPermissions: Permission[]
  userCount: number  // 新增：用于影响提示
  isLocked: boolean
}

export function RolePermissionMatrix({
  roleId,
  roleName,
  currentPermissions,
  userCount,
  isLocked,
}: RolePermissionMatrixProps) {
  const { selected, hasChanges, toggle, toggleGroup, reset } = useRolePermissions(currentPermissions)
  const { query, filteredCatalog, setQuery } = usePermissionSearch(PERMISSION_CATALOG)

  return (
    <ErrorBoundary fallback={<PermissionMatrixError />}>
      <Card>
        <CardHeader>
          <PermissionMatrixHeader
            roleName={roleName}
            isLocked={isLocked}
            selectedCount={selected.size}
            hasChanges={hasChanges}
            userCount={userCount}
          />
          {!isLocked && hasChanges && (
            <PermissionImpactBadge count={userCount} />
          )}
        </CardHeader>
        <CardContent>
          <PermissionSearchBar value={query} onChange={setQuery} />
          <PermissionGroups
            catalog={filteredCatalog}
            selected={selected}
            isLocked={isLocked}
            onToggle={toggle}
            onToggleGroup={toggleGroup}
          />
          <PermissionMatrixActions
            isLocked={isLocked}
            hasChanges={hasChanges}
            onReset={reset}
            onSave={() => handleSave(roleId, selected)}
          />
        </CardContent>
      </Card>
    </ErrorBoundary>
  )
}
```

### 6.4 i18n 翻译文件结构

```json
// shared/i18n/messages/zh-CN/rbac.json（补全后）
{
  "roles": { ... },
  "permissions": {
    "title": "权限目录",
    "description": "系统中定义的所有权限点，按模块分组。",
    "group": { ... },
    "exam": {
      "create": { "label": "创建考试", "desc": "允许创建新考试" },
      "read": { "label": "查看考试", "desc": "允许查看考试列表和详情" }
      // ... 67 个权限点
    }
  },
  "matrix": {
    "search": "搜索权限...",
    "selected": "已选 {count} 项",
    "impact": "修改将影响 {count} 个用户",
    "save": "保存更改",
    "reset": "重置"
  },
  "errors": {
    "loadFailed": "加载失败，请重试",
    "saveFailed": "保存失败：{message}"
  }
}
```

### 6.5 错误与边界处理

```tsx
// modules/rbac/components/error-boundary.tsx

"use client"

import { Component, type ReactNode } from "react"
import { Button } from "@/shared/components/ui/button"
import { AlertCircle } from "lucide-react"

interface Props {
  children: ReactNode
  fallback?: ReactNode
}

interface State {
  hasError: boolean
  error?: Error
}

export class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  render(): ReactNode {
    if (this.state.hasError) {
      return this.props.fallback ?? <DefaultErrorFallback error={this.state.error} />
    }
    return this.props.children
  }
}

function DefaultErrorFallback({ error }: { error?: Error }): ReactNode {
  return (
    <div role="alert" className="rounded-md border border-destructive/50 p-4">
      <AlertCircle className="h-5 w-5 text-destructive" aria-hidden="true" />
      <p className="mt-2 text-sm text-destructive">
        {error?.message ?? "加载失败"}
      </p>
      <Button variant="outline" size="sm" className="mt-2" onClick={() => window.location.reload()}>
        重试
      </Button>
    </div>
  )
}
```

```tsx
// app/(dashboard)/admin/roles/loading.tsx

import { Skeleton } from "@/shared/components/ui/skeleton"

export default function Loading(): JSX.Element {
  return (
    <div className="flex h-full flex-col space-y-6 p-8">
      <div className="space-y-2">
        <Skeleton className="h-8 w-48" />
        <Skeleton className="h-4 w-72" />
      </div>
      <Skeleton className="h-96 w-full" />
    </div>
  )
}
```

```tsx
// app/(dashboard)/admin/roles/error.tsx

"use client"

import { useEffect } from "react"
import { Button } from "@/shared/components/ui/button"
import { ShieldAlert } from "lucide-react"

export default function Error({ error, reset }: {
  error: Error & { digest?: string }
  reset: () => void
}): JSX.Element {
  useEffect(() => {
    console.error("Roles page error:", error)
  }, [error])

  return (
    <div className="flex h-full flex-col items-center justify-center space-y-4 p-8">
      <ShieldAlert className="h-12 w-12 text-destructive" aria-hidden="true" />
      <h2 className="text-xl font-semibold">角色管理加载失败</h2>
      <p className="text-sm text-muted-foreground">{error.message}</p>
      <Button onClick={reset}>重试</Button>
    </div>
  )
}
```

### 6.6 可测试性设计

```typescript
// modules/rbac/lib/permission-diff.ts（纯函数，可单测）

import type { Permission } from "@/shared/types/permissions"

export interface PermissionDiff {
  added: Permission[]
  removed: Permission[]
  unchanged: Permission[]
}

export function diffPermissions(
  before: Permission[],
  after: Permission[]
): PermissionDiff {
  const beforeSet = new Set(before)
  const afterSet = new Set(after)

  return {
    added: after.filter((p) => !beforeSet.has(p)),
    removed: before.filter((p) => !afterSet.has(p)),
    unchanged: after.filter((p) => beforeSet.has(p)),
  }
}

export function arePermissionsEqual(
  a: Permission[],
  b: Permission[]
): boolean {
  if (a.length !== b.length) return false
  const set = new Set(a)
  return b.every((p) => set.has(p))
}
```

```typescript
// modules/rbac/hooks/use-role-permissions.ts（纯逻辑 Hook）

"use client"

import { useCallback, useMemo, useState } from "react"
import type { Permission } from "@/shared/types/permissions"
import { arePermissionsEqual } from "../lib/permission-diff"

export function useRolePermissions(initial: Permission[]) {
  const [selected, setSelected] = useState<Set<string>>(new Set(initial))

  const hasChanges = useMemo(
    () => !setsEqual(selected, new Set(initial)),
    [selected, initial]
  )

  const toggle = useCallback((permission: string, checked: boolean) => {
    setSelected((prev) => {
      const next = new Set(prev)
      if (checked) next.add(permission)
      else next.delete(permission)
      return next
    })
  }, [])

  const toggleGroup = useCallback((permissions: string[], checked: boolean) => {
    setSelected((prev) => {
      const next = new Set(prev)
      for (const p of permissions) {
        if (checked) next.add(p)
        else next.delete(p)
      }
      return next
    })
  }, [])

  const reset = useCallback(() => setSelected(new Set(initial)), [initial])

  return { selected, hasChanges, toggle, toggleGroup, reset }
}

function setsEqual<T>(a: Set<T>, b: Set<T>): boolean {
  if (a.size !== b.size) return false
  for (const v of a) if (!b.has(v)) return false
  return true
}
```

### 6.7 配置驱动的可扩展设计

```typescript
// modules/rbac/lib/role-templates.ts（新增）

import type { Permission } from "@/shared/types/permissions"
import { Permissions } from "@/shared/types/permissions"

export interface RoleTemplate {
  id: string
  nameKey: string
  descriptionKey: string
  permissions: Permission[]
}

export const ROLE_TEMPLATES: RoleTemplate[] = [
  {
    id: "homeroom_teacher",
    nameKey: "rbac:templates.homeroom_teacher.name",
    descriptionKey: "rbac:templates.homeroom_teacher.desc",
    permissions: [
      Permissions.EXAM_READ,
      Permissions.HOMEWORK_CREATE,
      Permissions.HOMEWORK_GRADE,
      Permissions.CLASS_READ,
      Permissions.ATTENDANCE_MANAGE,
      Permissions.MESSAGE_SEND,
      Permissions.DASHBOARD_TEACHER_READ,
    ],
  },
  {
    id: "subject_teacher",
    nameKey: "rbac:templates.subject_teacher.name",
    descriptionKey: "rbac:templates.subject_teacher.desc",
    permissions: [
      Permissions.EXAM_CREATE,
      Permissions.QUESTION_CREATE,
      Permissions.HOMEWORK_GRADE,
      Permissions.GRADE_RECORD_MANAGE,
    ],
  },
  // ... 更多模板
]
```

### 6.8 监控埋点接口

```typescript
// shared/lib/analytics.ts（预留接口）

export interface PermissionChangeMetrics {
  action: "role.create" | "role.update" | "role.delete" | "role.set_permissions" | "user.assign_roles"
  targetId: string
  targetType: "role" | "user"
  changes?: {
    added?: number
    removed?: number
  }
  affectedUsers?: number
  durationMs: number
  success: boolean
  errorMessage?: string
}

export async function trackPermissionChange(metrics: PermissionChangeMetrics): Promise<void> {
  // 预留实现：可接入 Sentry / PostHog / 自建埋点
  if (process.env.NODE_ENV === "development") {
    console.log("[permission-change]", metrics)
  }
}
```

---

## 七、实施计划

### 7.1 第一阶段（P0 — 立即修复）

1. **P0-1**：`admin/permissions/page.tsx` 改用 data-access
   - 在 `modules/rbac/data-access-permissions.ts` 新增 `getPermissionRoleCounts()`
   - 页面改为 `import { getPermissionRoleCounts } from "@/modules/rbac/data-access-permissions"`

2. **P0-2**：`deleteUserAction` 改用 data-access
   - 在 `modules/users/data-access.ts` 新增 `deleteUserById(userId: string): Promise<void>`
   - 包含级联清理 sessions、usersToRoles、passwordSecurity
   - actions.ts 改调 `deleteUserById`

3. **P0-3**：`admin-users-view.tsx` 改用 Server Action
   - 删除 `fetch("/api/admin/users/...")` 逻辑
   - 改用 `useActionMutation` + `deleteUserAction`

4. **P0-4**：`getCurrentStudentUser` 改用 `getAuthContext()`
   - `const ctx = await getAuthContext()` → `ctx.userId`

5. **P0-5**：删除 `updateUserRoleAction` 空实现

6. **P0-6**：新增 `loading.tsx`/`error.tsx`
   - 4 个路由各 2 个文件，共 8 个文件

### 7.2 第二阶段（P1 — i18n + 类型安全）

1. **P1-1/P1-2/P1-3**：i18n 化
   - 补全 `rbac.json` 的 67 个权限点标签
   - RBAC 组件改用 `useTranslations`
   - `admin-users-view.tsx` 改用 `useTranslations`

2. **P1-4**：类型守卫
   - `shared/lib/type-guards.ts` 新增 `isPermission(value: unknown): value is Permission`
   - 替换所有 `as Permission` 断言

3. **P1-5**：`resolveDataScope` 改调模块 data-access
   - `classes` 模块新增 `getClassIdsForTeacher(userId)`、`getClassIdsForStudent(userId)`
   - `parent` 模块新增 `getChildrenIdsForParent(userId)`
   - `school` 模块新增 `getGradeIdsForHead(userId)`

4. **P1-6**：`resolveDataScope` 改为配置驱动
   - 新增 `shared/lib/data-scope-resolver.ts`
   - 基于权限点而非角色名判断 scope

5. **P1-7**：Error Boundary
   - 新增 `modules/rbac/components/error-boundary.tsx`
   - 包裹 `RoleList`、`RolePermissionMatrix`、`PermissionCatalogView`

6. **P1-8/P1-9**：安全性增强
   - `deleteUserAction` 校验最后 admin
   - `assignRolesToUser` 过滤禁用角色

### 7.3 第三阶段（P2 — 体验/性能/可扩展）

1. **P2-1**：权限矩阵搜索 + 折叠
2. **P2-2**：角色模板
3. **P2-3**：权限变更影响提示
4. **P2-4**：权限使用统计
5. **P2-5**：a11y 增强
6. **P2-6**：Suspense 流式渲染
7. **P2-7**：监控埋点
8. **P2-8**：`role-utils.ts` 保留角色差异
9. **P2-9**：权限继承
10. **P2-10**：权限变更通知

---

## 八、合规性检查

| 约束 | 当前状态 | 重构后 |
|---|---|---|
| 三层架构 `app → modules → shared` | ❌ app 直查 DB | ✅ 全部走 data-access |
| `app/` 不直接访问数据库 | ❌ 3 处违规 | ✅ 修复 |
| 模块间通过 data-access 通信 | ❌ `auth-guard` 跨模块查表 | ✅ 改调 data-access |
| Server Action 调用 `requirePermission()` | ✅ 已实现 | ✅ 保持 |
| 前端用 `usePermission().hasPermission()` | ✅ 已实现 | ✅ 保持 |
| i18n 适配 | ❌ 大量硬编码 | ✅ 全部提取翻译键 |
| TypeScript 严格模式（无 `any`/`as`） | ❌ 多处 `as` | ✅ 用类型守卫替代 |
| 单文件行数 ≤ 500/800/1000 | ✅ 当前均未超标 | ✅ 保持 |
| `loading.tsx`/`error.tsx` | ❌ 缺失 | ✅ 补全 |
| 架构图同步 | ❌ 有遗漏 | ✅ 同步更新 |

---

## 九、结论

用户权限模块的核心基础设施（`Permissions` 常量、`resolvePermissions`、`requirePermission`、`usePermission`）设计合理，RBAC 模块的 actions/data-access 分层清晰。但存在 **6 个 P0 级架构违规**（app 直查 DB、Server Action 直查 DB、客户端 fetch、认证入口不统一、空实现 Action、缺 loading/error）、**9 个 P1 级问题**（i18n 遗漏、类型断言、跨模块查表、角色硬编码、错误边界缺失、安全隐患）和 **10 个 P2 级改进**（搜索/模板/统计/a11y/性能/监控等）。

建议按 P0 → P1 → P2 顺序实施，P0 必须立即修复以消除安全与架构风险，P1 在本迭代内完成，P2 纳入后续迭代规划。
