# Shared 基础设施层审查报告 > 审查日期:2026-06-17 > 审查范围:`src/shared/`(db、lib、hooks、components、types)+ `src/auth.ts` + `src/proxy.ts` > 审查依据:职责单一性、函数复杂度、模块间耦合、架构文档完整性 ## 概览 - 文件总数:69(不含测试文件) - `db/`:3 - `lib/`:13 - `hooks/`:7 - `components/ui/`:34 - `components/a11y/`:4 - `components/`(顶层):4 - `types/`:2 - `src/auth.ts`、`src/proxy.ts`:2 - 发现问题数:15 - 严重程度分布:高 3 / 中 9 / 低 3 --- ## 职责单一性问题 ### 1. `src/shared/db/schema.ts` - **问题**:单个文件包含 54 张表定义,共 1111 行,**超过项目规则中"任何文件不超过 1000 行"的硬性上限**。文件涵盖用户、认证、题库、教学、学校、班级、考试、作业、AI、公告、审计、成绩、文件、课程计划、消息、考勤、排课、选课、监考、学情诊断等十余个业务域。此外分节编号混乱:section 12(Parent-Student Relations,行 958)出现在 section 14b(Notification 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 行) - **严重程度**:高 - **建议**:将密码安全 DB 操作迁移到 `shared/lib/password-security-service.ts`(与纯函数的 `password-policy.ts` 区分);角色规范化迁移到 `shared/lib/permissions.ts` 或新建 `shared/lib/role-utils.ts`;`resolveClientIp` 迁移到 `shared/lib/http-utils.ts`(与三个 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`) 其中加密/解密与 Provider 配置查询属于数据层,与 AI 调用本身是不同关注点。 - **严重程度**:中 - **建议**:将加密/解密拆到 `shared/lib/crypto.ts`(通用加密工具);将 Provider 配置查询拆到 `shared/lib/ai-provider-repo.ts`(数据访问)。`ai.ts` 保留负载解析与 AI 调用编排。 ### 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 层配置或 DB;API 调用通过 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/*` 的循环。虽然 NextAuth 的 `auth()` 函数是运行时调用而非模块级副作用,目前不会导致运行时错误,但架构上 shared 基础设施层不应反向依赖业务层的认证入口。 - **建议**:将 `auth()` 的 session 获取抽象为接口或通过参数注入。logger 函数改为接收 `session` 参数(由调用方传入),而非内部调用 `auth()`。或创建 `shared/lib/session.ts` 封装 session 获取,`auth.ts` 和 logger 都依赖它,打破循环。 ### 2. shared 层对根模块 `@/auth` 的反向依赖 - **涉及模块**:`shared/lib/*` → `@/auth`(根模块) - **问题类型**:反向依赖 - **问题详情**:`src/auth.ts` 位于项目根目录,属于应用层(非 shared 层)。shared 层应是被依赖方,不应依赖应用层模块。三个文件(audit-logger、change-logger、auth-guard)直接 import `@/auth`,使 shared 层无法独立测试或复用。 - **建议**:同上,通过依赖注入或提取 `shared/lib/session.ts` 解耦。 ### 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 取逗号分隔第一段,其他取全值),存在不一致风险。 - **建议**:提取 `shared/lib/http-utils.ts`,导出 `getClientIp()` 和 `getUserAgent()` 统一复用。 --- ## 架构文档改进建议 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 行硬性上限** | | `src/shared/lib/` | 13 | ai.ts (218) | | | `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 (293) | | | `src/proxy.ts` | 1 | proxy.ts (75) | | > 注:`components/ui/` 下 34 个文件多为 shadcn/ui 标准生成组件(基于 Radix UI),职责单一,未发现结构性问题,故未逐一列入问题清单。`chart.tsx`(329 行)为标准 shadcn chart 组件,行数较高但属框架约定,可接受。