Files
NextEdu/docs/architecture/audit/shared-audit.md

216 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Shared 基础设施层审查报告
> 审查日期2026-06-17
> 审查范围:`src/shared/`db、lib、hooks、components、types+ `src/auth.ts` + `src/proxy.ts`
> 审查依据:职责单一性、函数复杂度、模块间耦合、架构文档完整性
## 概览
- 文件总数69不含测试文件
- `db/`3
- `lib/`17新增 4 个 auth 拆分文件)
- `lib/ai/`6新增目录P2-2 拆分)
- `hooks/`7
- `components/ui/`34
- `components/a11y/`4
- `components/`顶层4
- `types/`2
- `src/auth.ts``src/proxy.ts`2
- 发现问题数15
- 严重程度分布:高 3 / 中 9 / 低 3
- 已修复问题4auth.ts 拆分、ai.ts 拆分、循环依赖、反向依赖)
- 待修复问题11含 schema.ts 拆分 P2-1、onboarding-gate、global-search、proxy.ts 等)
---
## 职责单一性问题
### 1. `src/shared/db/schema.ts`
- **问题**:单个文件包含 54 张表定义,共 1111 行,**超过项目规则中"任何文件不超过 1000 行"的硬性上限**。文件涵盖用户、认证、题库、教学、学校、班级、考试、作业、AI、公告、审计、成绩、文件、课程计划、消息、考勤、排课、选课、监考、学情诊断等十余个业务域。此外分节编号混乱section 12Parent-Student Relations行 958出现在 section 14bNotification Preferences行 934之后P2 段落与主编号交错。
- **严重程度**:高
- **建议**:按业务域拆分为多个 schema 文件(如 `schema/auth.ts``schema/academic.ts``schema/exam.ts``schema/audit.ts` 等),通过 `schema/index.ts` 聚合导出。同时修正分节编号。
### 2. `src/auth.ts` ✅ 已修复
- **问题**~~293 行,混合了多种职责~~
1. NextAuth 配置providers、callbacks、events
2. 密码安全 DB 操作(`getOrCreatePasswordSecurity``recordFailedLogin``resetFailedLogin`,行 56-130
3. 角色规范化工具(`normalizeRole``resolvePrimaryRole`,行 13-27
4. bcrypt 哈希规范化(`normalizeBcryptHash`,行 29-33
5. IP 解析(`resolveClientIp`,行 39-51
6. `authorize` 回调内联了限流、锁定检查、密码比对、日志记录全流程86 行)
- **严重程度**:高
- **修复状态**2026-06-17 已完成拆分P1-3。auth.ts 从 293 行降至 193 行4 类职责分别迁移到 shared/lib 下的独立文件:
- `password-security-service.ts`84 行)- 密码安全 DB 操作
- `role-utils.ts`31 行)- 角色规范化
- `bcrypt-utils.ts`18 行)- bcrypt 哈希规范化
- `http-utils.ts`27 行)- IP 解析(与三个 logger 共用)
### 3. `src/shared/lib/ai.ts` ✅ 已修复
- **问题**~~218 行,混合了 5 类职责~~
1. 请求负载解析与校验(`parseAiChatPayload`,行 70-96
2. API Key 加密/解密(`encryptAiApiKey`/`decryptAiApiKey`,行 104-124
3. Provider 配置 DB 查询(`getAiProviderConfig`,行 126-179
4. AI 客户端创建与调用(`getAiClient``createAiChatCompletion``testAiProviderConfig``testAiProviderById`
5. 错误格式化(`getAiErrorMessage`
- **严重程度**:中
- **修复状态**2026-06-17 已完成拆分P2-2commit 6588f74。原 `ai.ts`218 行)拆分为 `src/shared/lib/ai/` 目录 6 个文件:
- `payload-parser.ts`96 行)- 请求负载解析
- `api-key-crypto.ts`34 行)- API Key 加密/解密
- `provider-config.ts`66 行)- Provider 配置查询
- `client.ts`67 行)- AI 客户端创建与调用
- `errors.ts`9 行)- 错误格式化
- `index.ts`7 行)- 聚合导出
`ai.ts` 保留为向后兼容的重导出文件9 行),调用方无需修改 import 路径。
### 4. `src/shared/components/onboarding-gate.tsx`
- **问题**312 行组件,混合了:
1. 多步表单 UI
2. 角色推断业务逻辑(行 90-94通过权限反推角色`isAdmin`/`isTeacher`/`isStudent`/`isParent`),逻辑脆弱且未使用 `usePermission().hasRole()`
3. 硬编码教学学科列表(`TEACHER_SUBJECTS`,行 20——业务数据固化在 shared 基础设施
4. 直接 `fetch("/api/onboarding/status")``fetch("/api/onboarding/complete")`——耦合特定 API 路由
- **严重程度**:中
- **建议**:角色判断改用 `usePermission().hasRole()` 或 session 的 `role` 字段;学科列表迁移到 modules 层配置或 DBAPI 调用通过 Server Action 封装。组件本身可考虑按步骤拆分子组件。
### 5. `src/shared/components/global-search.tsx`
- **问题**221 行组件,混合了:
1. 搜索 UI 与下拉渲染
2. 硬编码业务类型(`ResultType = "question" | "textbook" | "exam" | "announcement"`,行 12与图标/标签映射(行 30-42——业务知识泄漏到 shared 层
3. 直接 `fetch("/api/search?...")`——耦合特定 API 路由与查询协议
4. 快捷键、点击外部、键盘导航等交互逻辑内联
- **严重程度**:中
- **建议**:搜索结果类型与图标映射应由 API 返回或从 modules 层注入API 调用抽取为独立 hook`useGlobalSearch`);交互逻辑可拆为 `useSearchKeyboard` 等。
### 6. `src/proxy.ts`
- **问题**75 行,硬编码了路由-权限映射(`ROUTE_PERMISSIONS``API_PERMISSIONS`,行 8-19使用原始字符串如 `"school:manage"``"exam:read"`**未复用 `Permissions` 常量**,违反项目规则"前端组件禁止硬编码 role/权限"的精神。`resolveDefaultPath`(行 21-27将角色到默认路径的业务映射硬编码在代理中。
- **严重程度**:中
- **建议**:权限字符串改用 `Permissions.SCHOOL_MANAGE` 等常量;路由权限映射迁移到 `shared/lib/route-permissions.ts` 配置文件;角色-路径映射迁移到 modules 层或路由配置。
### 7. `src/shared/lib/a11y.ts`
- **问题**`useA11yId`(行 7-10是一个 React Hook但放置在 `lib/` 目录而非 `hooks/` 目录。项目约定 `hooks/` 存放所有自定义 Hook`lib/` 存放纯工具函数。该文件其余函数(`mergeA11yProps``describeInput``loadingAria`)是纯函数,放置正确。
- **严重程度**:低
- **建议**:将 `useA11yId` 迁移到 `shared/hooks/use-a11y-id.ts``a11y.ts` 保留纯函数。
---
## 过耦合函数
### 1. `resolveDataScope` @ `src/shared/lib/auth-guard.ts:64-130`
- **行数**67
- **参数数**2`userId: string`, `roleNames: string[]`
- **问题**:单个函数内根据角色分支查询 4 张不同的表(`grades``classes``classSubjectTeachers``parentStudentRelations`),将权限范围解析与数据访问混合。每个角色分支的查询逻辑独立,新增角色需修改此函数,违反开闭原则。
- **建议**:将各角色的数据范围查询拆为独立函数(如 `resolveTeacherScope``resolveParentScope`),或迁移到各模块的 data-access 层,`resolveDataScope` 仅做分发。
### 2. `getAiProviderConfig` @ `src/shared/lib/ai.ts:126-179`
- **行数**53
- **参数数**1`providerId?: string`
- **问题**:函数内有三段几乎相同的 DB 查询分支(按 providerId、按 isDefault、fallback每段都 select 相同的字段、解密 apiKey、返回相同结构存在明显代码重复。
- **建议**:提取公共 `mapProviderRow(row)` 函数,三个分支简化为查询条件不同。或合并为单查询带 OR 条件 + 排序优先级。
### 3. `authorize`NextAuth Credentials 回调)@ `src/auth.ts:143-229`
- **行数**86
- **参数数**1`credentials`
- **问题**:单函数内串联了:邮箱密码校验 → 速率限制 → DB 用户查询 → 账户锁定检查 → 密码比对 → 失败计数 → 成功重置 → 角色查询 → 返回。流程长且混合了限流、安全策略、认证、日志多个关注点。内部还使用 `Promise.all` + 动态 `import`(行 165-168加载 `@/shared/db` 和 schema写法不寻常。
- **建议**:将流程拆分为 `checkRateLimit``checkAccountLockout``verifyPassword``loadUserRoles` 等步骤函数,`authorize` 仅编排。动态 import 改为静态 import。
### 4. `OnboardingGate` 组件 @ `src/shared/components/onboarding-gate.tsx:27-312`
- **行数**285组件函数体
- **参数数**0无 props内部消费 session
- **问题**单组件承担了状态检查、4 步表单、角色推断、API 提交、路由跳转。组件内 9 个 `useState`3 个 `useEffect`,逻辑密集。
- **建议**:按步骤拆分为 `OnboardingRoleStep``OnboardingProfileStep``OnboardingRoleDetailStep``OnboardingCompleteStep` 子组件;提取 `useOnboarding` hook 封装状态与提交逻辑。
### 5. `GlobalSearch` 组件 @ `src/shared/components/global-search.tsx:49-221`
- **行数**172组件函数体
- **参数数**2`className?`, `placeholder?`
- **问题**单组件承担了输入控制、防抖搜索、快捷键监听、点击外部关闭、键盘导航、结果渲染。6 个 `useState`3 个 `useEffect`
- **建议**:提取 `useGlobalSearch(query)` hook 封装搜索请求与状态;提取 `useSearchKeyboard` 封装快捷键与导航。组件仅负责渲染。
---
## 模块间依赖问题
### 1. shared 层与 `@/auth` 的循环依赖 ✅ 已修复
- **涉及模块**`shared/lib/{audit-logger, change-logger, auth-guard}``@/auth``shared/lib/{login-logger, permissions, password-policy, rate-limit}` + `shared/db`
- **问题类型**:循环依赖
- **问题详情**
- `shared/lib/audit-logger.ts`(原行 7`import { auth } from "@/auth"`
- `shared/lib/change-logger.ts`(原行 6`import { auth } from "@/auth"`
- `shared/lib/auth-guard.ts`(原行 1`import { auth } from "@/auth"`
-`src/auth.ts` 反向依赖 `shared/lib/permissions``shared/lib/login-logger``shared/lib/password-policy``shared/lib/rate-limit``shared/db`
这构成了 `shared/lib/*``auth``shared/lib/*` 的循环。
- **修复状态**2026-06-17 已完成P0-3。3 个文件audit-logger.ts、change-logger.ts、auth-guard.ts将静态 `import { auth } from "@/auth"` 改为动态 `const { auth } = await import("@/auth")`,打破模块级静态循环依赖。运行时调用链保持不变,但模块加载图无环。
### 2. shared 层对根模块 `@/auth` 的反向依赖 ✅ 已修复
- **涉及模块**`shared/lib/*``@/auth`(根模块)
- **问题类型**:反向依赖
- **问题详情**`src/auth.ts` 位于项目根目录,属于应用层(非 shared 层。shared 层应是被依赖方不应依赖应用层模块。三个文件audit-logger、change-logger、auth-guard直接 import `@/auth`,使 shared 层无法独立测试或复用。
- **修复状态**2026-06-17 已完成P0-3。通过动态 import 打破静态反向依赖。shared 层不再有对 `@/auth` 的静态 import仅保留运行时动态调用用于获取 session
### 3. 三个 logger 重复实现 IP/Header 提取 ✅ 部分修复
- **涉及模块**`shared/lib/audit-logger``shared/lib/change-logger``shared/lib/login-logger``src/auth.ts`
- **问题类型**过度耦合DRY 违反)
- **问题详情**:三个 logger 各自重复实现相同的 IP/User-Agent 提取逻辑:
- `audit-logger.ts`(行 27-32`headerList.get("x-forwarded-for") ?? headerList.get("x-real-ip") ?? "unknown"`
- `change-logger.ts`(行 27-31相同逻辑
- `login-logger.ts`(行 26-31相同逻辑
- `auth.ts`(原行 39-51`resolveClientIp` 也是类似逻辑(取 `x-forwarded-for` 第一段)
四处实现略有差异auth.ts 取逗号分隔第一段,其他取全值),存在不一致风险。
- **修复状态**2026-06-17 部分修复P1-3`src/auth.ts``resolveClientIp` 已迁移到 `shared/lib/http-utils.ts`27 行auth.ts 改为从该文件导入。但三个 logger 文件内部的 IP/Header 提取逻辑尚未统一到 `http-utils.ts`P2 待处理)。
---
## 架构文档改进建议
1. **补充依赖关系图**:当前 004 文档以函数/常量为粒度列举导出,但缺少模块间依赖方向的可视化图。建议在 005 JSON 中增加 `dependencyMatrix` 节点,记录 `shared/lib/* → @/auth``@/auth → shared/lib/*` 等依赖边,并在 004 Markdown 中用 Mermaid 图渲染。本次审查发现的循环依赖shared ↔ auth在当前文档中完全不可见。
2. **标注循环依赖与反向依赖**004/005 文档应明确标注 `shared/lib/{audit-logger, change-logger, auth-guard}``@/auth` 的依赖,以及这与 `@/auth``shared/lib/*` 的依赖构成的循环。当前文档将 `auth` 模块与 `shared` 模块分别描述,未揭示二者双向依赖。
3. **修正 schema.ts 分节编号**004 文档的"数据库表"章节按表名平铺列举,未反映 schema.ts 源文件中的分节结构。建议文档增加 schema.ts 分节映射表,并修正源文件中 section 12 出现在 section 14b 之后的编号混乱。
4. **增加 shared 层边界说明**004 文档应明确 shared 层"不应依赖应用层模块(如 `@/auth`"的架构约束,以及哪些文件属于 shared 层的对外公共 API。当前文档未说明 shared 与根模块auth.ts、proxy.ts的边界。
5. **补充函数复杂度标注**005 JSON 中每个函数已有签名记录,但缺少行数与参数数量字段。建议增加 `"lines"``"paramCount"` 字段,便于自动识别过耦合函数(如本次发现的 `authorize` 86 行、`resolveDataScope` 67 行)。
6. **记录 proxy.ts 的路由权限映射**004 文档未记录 `proxy.ts` 中的 `ROUTE_PERMISSIONS``API_PERMISSIONS` 硬编码映射,也未说明这些映射与 `Permissions` 常量的关系。建议在 005 JSON 的 `routes` 节点中补充代理层权限规则。
---
## 附:审查范围文件清单
| 目录 | 文件数 | 最大文件(行数) | 备注 |
|------|--------|------------------|------|
| `src/shared/db/` | 3 | schema.ts (1111) | **超过 1000 行硬性上限**P2-1 待拆分) |
| `src/shared/lib/` | 17 | password-security-service.ts (84) | ai.ts 已拆分为 ai/ 目录P2-2新增 4 个 auth 拆分文件P1-3 |
| `src/shared/lib/ai/` | 6 | payload-parser.ts (96) | 新增目录P2-2 拆分) |
| `src/shared/hooks/` | 7 | use-aria-live.ts (88) | |
| `src/shared/components/ui/` | 34 | chart.tsx (329) | 多为标准 shadcn/ui 组件 |
| `src/shared/components/a11y/` | 4 | focus-trap.tsx (110) | |
| `src/shared/components/`(顶层) | 4 | onboarding-gate.tsx (312) | |
| `src/shared/types/` | 2 | permissions.ts (92) | |
| `src/auth.ts` | 1 | auth.ts (193) | ✅ 已从 293 行降至 193 行P1-3 拆分) |
| `src/proxy.ts` | 1 | proxy.ts (75) | |
> 注:`components/ui/` 下 34 个文件多为 shadcn/ui 标准生成组件(基于 Radix UI职责单一未发现结构性问题故未逐一列入问题清单。`chart.tsx`329 行)为标准 shadcn chart 组件,行数较高但属框架约定,可接受。
>
> **变更说明**
> - `src/shared/lib/ai.ts`(原 218 行)已拆分为 `ai/` 目录 6 个文件,原文件保留为 9 行重导出P2-2
> - `src/auth.ts`(原 293 行)已拆分出 4 个 shared/lib 文件,自身降至 193 行P1-3
> - `src/shared/lib/` 新增:`password-security-service.ts`84 行)、`role-utils.ts`31 行)、`bcrypt-utils.ts`18 行)、`http-utils.ts`27 行)