docs(architecture): sync logging refactor to 004/005 and known-issues

Task 15: 004 架构影响地图新增 1.1.7 日志系统重构章节(架构图、核心组件表、Request ID 贯穿链路、Edge Runtime 限制、替换范围、环境变量),shared/lib 清单新增 logger.ts/request-context.ts/with-request-context.ts/track-event.ts 更新,hooks 清单新增 use-error-report.ts。

Task 16: 005 架构数据 JSON 新增 3 个 shared/lib 文件节点、1 个 hooks 节点、1 个 apiRoute(/api/client-error)、5 个 dependencyMatrix 依赖关系,JSON 有效性验证通过。

Task 17: known-issues.md 新增二十八、日志系统规则章节,含 5 个规则表(pino 使用、Edge Runtime 限制、Request ID 贯穿、ESLint no-console、客户端错误上报)。
This commit is contained in:
SpecialX
2026-07-07 13:00:00 +08:00
parent 7c1b764b59
commit 4e6d397d8e
3 changed files with 933 additions and 29 deletions

View File

@@ -396,6 +396,83 @@ V5 状态管理统一的后续阶段,针对两类高频性能问题进行细
---
## 1.1.7 日志系统重构2026-07-07 新增)
### 目标
引入 pino 结构化日志、Request ID 贯穿、错误边界上报、静默失败修复、track-event 去重,统一全项目日志基础设施。仅应用层 SDK 改造,不部署外部可观测性服务。
### 架构
```
proxy.ts (Edge Runtime) Node.js Runtime
│ 生成 requestId (crypto.randomUUID) │
│ 注入 x-request-id 请求头 │ headers() 读取 x-request-id
▼ ▼
NextResponse.next({ request: { headers } }) withRequestContext(fn)
│ requestContextStorage.run({ requestId })
createModuleLogger("module")
│ pino mixin: getRequestContext()
stdout (JSON / pino-pretty)
客户端 error.tsx 服务端 /api/client-error
│ useErrorReport(error) │ createModuleLogger("client-error")
│ navigator.sendBeacon │ log.error({ message, stack, digest, path })
▼ ▼
/api/client-error (POST) pino 日志流
```
### 核心组件
| 文件 | 职责 |
|------|------|
| `shared/lib/logger.ts` | pino 实例 + `createModuleLogger(module)` 工厂;`mixin` 自动混入 requestId/userId开发环境 pino-pretty生产环境 JSON |
| `shared/lib/request-context.ts` | `AsyncLocalStorage<RequestContext>``getRequestContext()` 读取当前上下文;仅 Node.js Runtime |
| `shared/lib/with-request-context.ts` | `withRequestContext(fn)` 高阶函数;从 `headers()` 读取 `x-request-id` 或生成 UUID注入 AsyncLocalStorage |
| `shared/hooks/use-error-report.ts` | 客户端错误上报 Hooknavigator.sendBeacon 上报到 /api/client-errorsessionStorage 节流1 分钟/digest |
| `src/proxy.ts` | Edge Runtime生成/复用 requestId通过 `NextResponse.next({ request: { headers } })` 注入 x-request-id |
| `src/app/api/client-error/route.ts` | 接收客户端错误,用 `createModuleLogger("client-error")` 记录到 pino 日志流 |
### Request ID 贯穿链路
1. `proxy.ts`Edge Runtime生成 `requestId = crypto.randomUUID()`Web Crypto API非 Node.js crypto
2. 通过 `NextResponse.next({ request: { headers: injectRequestId(request, requestId) } })` 注入到下游请求
3. `withRequestContext(fn)` 在 Server Action / Route Handler 中从 `headers()` 读取 `x-request-id`
4. `requestContextStorage.run({ requestId }, () => fn(...))` 注入 AsyncLocalStorage
5. `pino` 的 `mixin: () => getRequestContext()` 自动将 requestId 混入每条日志
### Edge Runtime 限制
- `proxy.ts` 和原 `web-vitals/route.ts` 声明 `runtime = "edge"`
- Edge RuntimeV8 Isolate不支持 `node:async_hooks`AsyncLocalStorage和 `node:stream`pino 依赖)
- **解决方案**
- `proxy.ts` 仅用 Web Crypto API`crypto.randomUUID()`),不导入 Node.js 模块
- `web-vitals/route.ts` 从 `runtime = "edge"` 改为 `runtime = "nodejs"`pino 依赖 Node.js stream
- 客户端组件不能导入服务端 pino logger`env.LOG_LEVEL` 是 server-only
### 替换范围
- **34 个服务端 .ts 文件**86 处 `console.*` 替换为 `createModuleLogger("module")` 的 `log.*`
- **129 个 error.tsx**:接入 `useErrorReport(error)` Hook移除 7 处 `useEffect + console.error`
- **3 个 audit-logger**catch 块 `/* Silently fail */` → `log.warn({ err, action, module }, "...")`
- **4 个 track-event**`console.*` 替换为 `log.*`(客户端 track-event 改为纯 no-op
- **ESLint `no-console: error`**:强制服务端 .ts 使用 logger客户端 .tsx/hooks 暂时豁免(留待客户端错误上报机制处理)
### 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `LOG_LEVEL` | `info` | 日志级别debug/info/warn/error |
### 验证结果
- `npx tsc --noEmit`:零错误
- `npm run lint`零错误no-console 规则启用)
---
## 1.2 模块依赖关系图
下图展示模块间的实际依赖关系,**标注依赖类型与合规性**。
@@ -754,6 +831,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- `exportToExcel()` / `parseExcel()` / `generateTemplate()` — Excel 工具
- `cn()` / `formatDate()` / `formatFileSize()` — 通用工具
- `getInitials(name)` / `formatDateForFile(d?)` — 通用工具P1-c / P1-a 重构新增:从 parent/lib/utils.ts、grades/export-button.tsx 等多处重复实现抽取)
- `serializeDate(date)` / `serializeDateRequired(date)` — 日期序列化工具(✅ 审计 v1 P0-6 新增:位于 `lib/date-utils.ts`S-04 DRY 修复;`serializeDate(date: Date | null | undefined): string | null` 与 `serializeDateRequired(date: Date): string`messaging / course-plans / announcements / elective / audit / files / school / notifications 等 10+ data-access 文件迁移至共享 util消除各模块重复定义 `const toIso = (d: Date) => d.toISOString()`
- `downloadBase64File(base64, filename, mimeType?)` / `downloadBlob(blob, filename)` — 客户端文件下载P1-c 重构新增:从 grades/export-button、users/user-import-dialog、audit/audit-log-export-button 三处重复实现抽取,位于 `lib/download.ts`
- `handleActionError(e)` / `safeActionCall(action, options?)` / `safeJsonParse(json, msg)` / `safeParseDate(v, field)` / `safeParseNumber(v, field)` / `escapeLikePattern(input)` — Server Action 错误处理与客户端调用包装2026-06-23 审计修复新增:位于 `lib/action-utils.ts`,统一所有 Server Action catch 块错误处理,避免内部错误消息暴露给客户端)
- `BusinessError` / `NotFoundError` / `ValidationError` — 错误类2026-06-23 审计修复新增:位于 `lib/action-utils.ts`已知业务错误基类message 可安全返回客户端)
@@ -809,11 +887,11 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| 资产 | 文件 | 签名 | 用途 | 后续消费方 |
|------|------|------|------|--------|
| `createQueryClient` | `lib/query-client.ts` | `createQueryClient(): QueryClient` | TanStack Query 工厂,默认 staleTime 30s / retry 1 / refetchOnWindowFocus false / mutations 全局 onError 兜底 | `app/providers.tsx` |
| `configureNotify` + `notify` + `rawToast` | `lib/notify.ts` | `configureNotify(resolver); notify.{success,error,info,warning,promise}; rawToast` | 统一 toast 接口,包装 sonner 并内置 i18n key 解析。`configureNotify` 由 `Providers` 的 `NotifyConfigurator` 注入 next-intl `t` 函数 | `query-client.ts` 全局兜底 + 业务组件迁移后由各模块使用(替换 100+ 处 `import { toast } from "sonner"` |
| `notify` + `rawToast` | `lib/notify.ts` | `notify.{success,error,info,warning,message,promise}(message: string \| undefined, opts?: ExternalToast); rawToast` | 统一 toast 接口,包装 sonner。**V5 Phase 4 重写**:移除 `configureNotify` + i18n resolver 注入,简化为直接接受 `string \| undefined`(兼容 `result.message`),通过 `ExternalToast` options 转发 sonner 高级特性description/duration/action。i18n 由调用方通过 `t()` 控制。**已完成 131 文件批量替换**(业务代码 `import { toast } from "sonner"` → `import { notify } from "@/shared/lib/notify"` + `toast.X(...)` → `notify.X(...)` | `query-client.ts` 全局兜底 + 所有业务模块exams/grades/lesson-preparation/settings/classes 等 131 文件 |
| `useDialogState` | `hooks/use-dialog-state.ts` | `useDialogState(): readonly [boolean, () => void, () => void, () => void]` | Dialog/Sheet 开关状态 Hook替换 30+ 处 `const [open, setOpen] = useState(false)` 模式 | 待迁移30+ 处 Dialog 组件 |
| `useUiStore` + `useModal` | `stores/ui-store.ts` | `useUiStore: { modals, openModal, closeModal, isOpen, closeAll }; useModal(name)` | 全局 UI storezustand集中管理 modal registry。命令式 API + Hook 双入口。与 sonner 的分工toast 走 notifymodal 走本 store | 待迁移:30+ 处 Dialog 组件切换为 `useModal(name)` |
| `useUiStore` + `useModal` + `ModalRoot` | `stores/ui-store.ts` + `components/modal-root.tsx` | `useUiStore: { modals, openModal, closeModal, isOpen, closeAll }; useModal(name); ModalRoot({ registry }): ReactNode` | 全局 UI storezustand集中管理 modal registry。命令式 API + Hook 双入口。**V5 Phase 4 新增 `ModalRoot` 组件**:监听 ui-store modals 状态渲染已注册 modal路由变化时自动 `closeAll`。与 sonner 的分工toast 走 notifymodal 走本 store | 待迁移:新增跨组件触发 modal 推荐使用此模式;存量 Dialog 保持 useState |
| `createServiceProvider` | `lib/create-service-provider.tsx` | `createServiceProvider<T>(displayName): { Provider, useService, Context }` | Service DI Provider 工厂,替换 14 处重复的 `createContext(null)` + Provider + useService 抛错模板 | 待迁移announcements / attendance / audit / dashboard / messaging / settings 等 14 处 Service DI Provider |
| `Providers` | `app/providers.tsx` | `Providers({ children, locale, messages, session }): React.JSX.Element` | 客户端 Provider 聚合。嵌套顺序QueryClientProvider → NextIntlClientProvider → NotifyConfigurator → ThemeProvider → SessionProvider → NuqsAdapter → children + Toaster。DevTools 仅 dev 环境 | `app/layout.tsx` |
| `Providers` | `app/providers.tsx` | `Providers({ children, locale, messages, session }): React.JSX.Element` | 客户端 Provider 聚合。嵌套顺序QueryClientProvider → NextIntlClientProvider → ThemeProvider → SessionProvider → NuqsAdapter → children + Toaster。**V5 Phase 4 调整**:移除 `NotifyConfigurator`notify.ts 简化为直接接受 string不再依赖 i18n resolver 注入)。DevTools 仅 dev 环境 | `app/layout.tsx` |
**V5 状态分层模型**5 层):
@@ -877,9 +955,14 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `lib/route-resolver.ts` | 21 | 角色默认路径解析纯函数 `resolveDefaultPath(roles: string[]): string`(✅ audit-P0-6 新增:合并 `proxy.ts` 与 `onboarding/data-access.resolveDefaultPathByRoles` 重复实现;纯函数无 `server-only`,可在 edge/proxy 运行时使用;优先级 admin>grade_head/teaching_head>teacher>student>parent>fallback `/dashboard` |
| `lib/route-permissions.ts` | 82 | 路由权限配置常量(✅ audit-P1-10 + audit-P1-12 新增:从 proxy.ts 抽取,配置驱动设计;导出 4 个 `Record<string, Permission>` 常量:`SPECIFIC_ROUTE_PERMISSIONS`15 个精确路由权限audit-P1-10 扩展自 3 个至 15 个覆盖 `/admin/*` 子路径)/ `ROUTE_PREFIX_PERMISSIONS`5 个角色路由前缀权限)/ `DASHBOARD_ROUTE_PERMISSIONS`4 个仪表盘细粒度权限防跨角色访问)/ `API_ROUTE_PERMISSIONS`1 个 API 路由权限);纯常量无 `server-only`,新增路由权限只需修改此文件无需改 proxy.ts 逻辑) |
| `lib/permission-bitmap.ts` | 222 | 权限位图编解码(✅ audit-P1-7 新增:将 67 个权限点字符串数组约 1.1KB 压缩为约 13 字符 base36 字符串,体积减少 99%`PERMISSION_BITMAP_ORDER` 顺序固定不可变,新增权限只能追加末尾;纯函数无 `server-only`,可在 edge runtime 使用;导出 `encodePermissionsBitmap(permissions)` / `decodePermissionsBitmap(bitmap)` / `hasPermissionInBitmap(bitmap, permission)`BigInt 精度安全:手工按位累加 base36 → BigInt避免 `parseInt(bitmap, 36)` 的 Number 53 bit 精度丢失) |
| `lib/logger.ts` | 32 | pino 结构化日志实例(✅ 2026-07-07 日志重构新增:`createModuleLogger(module)` 工厂创建模块级 child logger开发环境 pino-pretty 彩色输出,生产环境 JSON 到 stdout`mixin` 自动从 AsyncLocalStorage 混入 requestId/userId`LOG_LEVEL` 环境变量控制级别server-only不兼容 Edge Runtime |
| `lib/request-context.ts` | 22 | AsyncLocalStorage 请求上下文(✅ 2026-07-07 日志重构新增:`RequestContext` 接口含 requestId/userId/module`requestContextStorage` 存储;`getRequestContext()` 读取当前上下文;仅 Node.js Runtime 可用proxy.ts 不导入) |
| `lib/with-request-context.ts` | 16 | Server Action 高阶函数包装(✅ 2026-07-07 日志重构新增:`withRequestContext(fn)` 从 `headers()` 读取 `x-request-id` 或生成 UUID注入 AsyncLocalStorage保留泛型类型推断 |
| `lib/track-event.ts` | - | 监控埋点接口(✅ 2026-07-07 日志重构:`console.info` 替换为 `createModuleLogger("track")` 的 `log.info`EventName 联合类型覆盖 11 个模块事件) |
| `lib/excel.ts` | - | Excel 导入导出 |
| `lib/file-storage.ts` | - | 文件存储抽象 |
| `hooks/use-permission.ts` | - | 客户端权限 Hook |
| `hooks/use-error-report.ts` | 72 | 客户端错误上报 Hook✅ 2026-07-07 日志重构新增:在 error.tsx 中使用,通过 navigator.sendBeacon 上报到 /api/client-errorsessionStorage 节流:同一 digest 1 分钟内只上报一次;降级到 fetch keepalive |
| `components/ui/*` | 35 文件 | shadcn/ui 标准组件(✅ P2-3 新增 alert.tsx |
| `components/ui/stat-card.tsx` | 95 | StatCard 统计卡片P1-a 新增) |
| `components/ui/stat-item.tsx` | 38 | StatItem 紧凑统计项P1-a 新增) |
@@ -907,6 +990,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `lib/type-guards.ts` | - | 通用类型守卫集合P1-6 新增 `isRecord(v): v is Record<string, unknown>`,消除 12+ 个文件重复定义;含 grades/lesson-preparation 等模块的类型守卫 `isGradeDraftData`/`isExerciseBlockData`/`isPermission` 等) |
| `lib/track-event.ts` | - | 监控埋点接口server-onlyno-op console.info 实现EventName 联合类型覆盖 11 个模块事件P3 新增 `homework.excellent_viewed`、`homework.remind_unsubmitted`audit-P1-9 新增 auth 模块 8 个事件 + `trackAuthEvent` 便捷函数) |
| `lib/utils.ts` | - | 通用工具P1-a/P1-c 新增 getInitials + formatDateForFile |
| `lib/date-utils.ts` | - | ✅ 审计 v1 P0-6 新增S-04 DRY日期序列化工具 `serializeDate(date: Date \| null \| undefined): string \| null` + `serializeDateRequired(date: Date): string`messaging / course-plans / announcements / elective / audit / files / school / notifications 等 10+ data-access 文件迁移至此共享 util |
| ~~`components/onboarding-gate.tsx`~~ | ~~312~~ | ✅ audit-P0-4 已删除(引导流程业务逻辑已迁移至 `modules/onboarding/`,原文件无任何 import 引用,删除以避免文件膨胀) |
| `components/global-search.tsx` | 221 | 全局搜索(业务泄漏) |
| `types/permissions.ts` | 157 | 67 个权限点常量 + Role/DataScope/AuthContext 类型(✅ rbac 新增:`Role` 类型从固定联合类型改为 `string` 支持动态角色;新增 `BUILTIN_ROLES` 常量、`BuiltinRole` 类型、`isBuiltinRole()` 类型守卫;`isRole()` 标记为 deprecated |
@@ -924,6 +1008,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- Data-access`getExams` / `getExamById` / `persistExamDraft` / `persistAiGeneratedExamDraft` / `buildExamDescription` / `resolveSubjectGradeNames` / `getExamCreatorId` / `updateExamWithQuestions` / `deleteExamById` / `duplicateExam` / `getExamPreview` / `getExamSubjects` / `getExamGrades` / `getExamsByGradeId`(✅ v4-P2-7 新增年级仪表盘维度3exams 表有直接 gradeId 字段,配合 examSubmissions 聚合提交数/已评分数/平均分,支持 scope 行级过滤)(后 8 个为 P1-2 新增)/ `getExamsForGradeEntry`(✅ 2026-06-24 新增:按 scope 过滤试卷列表,只返回有题目的试卷,供成绩录入页试卷选择器使用,返回 id/title/subjectName/gradeName/questionCount/totalScore/ `getExamForGradeEntry`(✅ 2026-06-24 新增获取单个试卷详情含题目列表innerJoin questions 获取 type含 scope 校验,返回 id/title/subjectId/gradeId/totalScore/questions[{id,order,score,type}],供 grades 模块按试卷录入成绩使用)/ `updateExamWithRichEditorData`(✅ 2026-06-24 新增P0-4 修复:从富文本编辑器更新已有试卷的 DB 事务,单事务内更新 exams.title/structure + 删除旧 examQuestions + 插入新 examQuestions原 updateExamFromRichEditorAction 直接 db.transaction 的逻辑下沉至此)
- AI Pipeline`generateAiCreateDraftFromSource` / `generateAiPreviewData` / `regenerateAiQuestionByInstruction` / `loadAiDraftQuestionsAndStructure`(✅ 2026-06-24 新增:从 actions.ts 下沉,加载 AI 草稿题目与结构,优先从 rawAiQuestions JSON 解析,否则从源文本生成)
- Utils`normalizeStructure`v3 新增:将持久化的 `exam.structure` unknown JSON 运行时校验并归一化为类型安全的 `ExamNode[]`,类型守卫模式无 `as` 断言,从 `teacher/exams/[id]/build/page.tsx` 提取;✅ 2026-06-24 升级:支持 section/group/question 三级层级递归校验section 节点含 title/level/childrengroup 节点含 title/instruction/children
- Lib✅ 类型守卫专项重构 2026-07-07 新增 `lib/type-guards.ts``isRecord`unknown → `Record<string, unknown>`,消除 `as Record<string, unknown>` 断言)/ `isSimpleTextContent`(校验可选 text 字段)/ `isQuestionContentObj`(校验题目内容 text+options 结构options 内仅校验 id/text 字段)/ `isJSONContent` + `isJSONContentArray`Tiptap JSONContent 守卫,返回类型为 `v is JSONContent`/ `toSortableId`dnd-kit `UniqueIdentifier`string|number→ string消除 `as string` 断言)/ `isQuestionBlockAttrs`QuestionBlock 节点 attrs 守卫 { questionId, type, score })。**用途**:消除 ai-pipeline/components/editor 子目录 13 个文件中的违规 `as` 断言;合规 `as``as unknown as` 从 unknown 转换、`as const` 字面量常量)保留未改。**使用者**`ai-pipeline/request.ts`、`components/exam-preview-utils.ts`、`components/exam-actions.tsx`、`components/assembly/{structure-editor,question-bank-list,exam-paper-preview}.tsx`、`editor/selection-toolbar.tsx`、`editor/exam-nodes-to-editor-doc.ts`、`editor/exam-rich-editor-inner.tsx`、`editor/extensions/question-block.tsx`、`components/exam-rich-form.tsx`
- Preview Utils`components/exam-preview-utils.ts`,✅ 2026-06-24 补充记录):`buildPreviewNodes` / `parseEditableContent` / `flattenPreviewQuestions` / `findPreviewQuestionNode` / `updatePreviewQuestionNodeInList` / `buildPreviewSignature` / `buildPreviewPayload` / `buildPreviewRequestData`(以上 8 个为预览纯函数,供 Hook 与组件复用)+ ✅ 2026-06-24 新增 5 个(从 use-exam-preview-tasks Hook 提取以控制行数):`buildTaskFormValues`(提取表单字段子集供任务回填)/ `isPreviewBackgroundTask`(类型守卫,校验 localStorage 恢复数据)/ `persistPreviewTasksToStorage`(持久化任务到 localStorage/ `parseStoredPreviewTasks`(解析 localStorage 任务queued/running 标记为 failed 防僵尸)/ `executeBackgroundPreviewTask`(执行单个后台预览任务的纯函数)
- Hooks✅ 2026-06-24 拆分记录):`useExamPreview`主组合器68 行,组合三个子 Hook + handlePreview 编排,返回 API 与拆分前一致)/ `usePreviewState`(状态子 Hook67 行10 个 useState + 3 个更新函数)/ `usePreviewBackgroundTasks`(后台任务子 Hook80 行PQueue 队列 + localStorage 持久化 + 入队/打开任务)/ `usePreviewRewrite`AI 重写子 Hook79 行handleRewriteSelectedQuestion 调用 regenerateAiQuestionAction
- Stats-serviceV3-8 新增):`getExamAnalytics`cache 包装,聚合考试所有作业的已批改提交,计算平均分/及格率/分数段分布/逐题错误率与难度等级,对标智学网考试分析)+ `ExamAnalyticsSummary` 类型
@@ -985,6 +1070,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `components/exam-preview-utils.ts` | 293 | 预览纯函数工具集buildPreviewNodes/parseEditableContent/flattenPreviewQuestions/findPreviewQuestionNode/updatePreviewQuestionNodeInList/buildPreviewSignature/buildPreviewPayload/buildPreviewRequestData + ✅ 2026-06-24 新增 5 个buildTaskFormValues/isPreviewBackgroundTask/persistPreviewTasksToStorage/parseStoredPreviewTasks/executeBackgroundPreviewTask从 use-exam-preview-tasks Hook 提取以控制行数) |
| `components/exam-preview.tsx` | - | 试卷结构预览组件use client从 exam-rich-form.tsx 提取,渲染试卷富文本编辑器的结构预览,支持分卷(section)/大题分组(group)/题目(question)三级嵌套递归渲染。✅ P3 修改:题目块中图片将原生 `<img>` 替换为 `next/image` 的 `<Image>` 组件(自动优化、懒加载、防布局偏移),新增导入 `next/image`;使用 `useTranslations('examHomework')` 国际化 section/group 标题与统计文案 |
| `utils/normalize-structure.ts` | 57 | v3 新增exam.structure 运行时校验与归一化(从 build/page.tsx 提取) |
| `lib/type-guards.ts` | 80 | ✅ 类型守卫专项重构2026-07-07新增exams 模块通用类型守卫集合。导出 `isRecord` / `isSimpleTextContent` / `isQuestionContentObj` / `isJSONContent` / `isJSONContentArray` / `toSortableId` / `isQuestionBlockAttrs` 共 7 个守卫/转换函数。消除 ai-pipeline/components/editor 子目录 13 个文件中的违规 `as` 断言(`as Record<string, unknown>` / `as { text?: string }` / `as JSONContent[]` / `as string` / `as QuestionBlockAttrs` 等),保留合规 `as unknown as`(从 unknown 转换)与 `as const`(字面量常量)未改 |
| `components/exam-analytics-dashboard.tsx` | - | V3-8 新增:考试分析仪表盘组件 |
| `components/exam-actions.tsx` | - | V3-5/V3-8/V3-12 增强:角色化菜单+analytics 链接+移动端触摸优化 |
| `components/exam-rich-form.tsx` | - | 2026-06-24 新增:富文本编辑器试卷创建/编辑表单ResizablePanel 左编辑右预览 + AI 自动标记 + 保存草稿)。✅ 2026-06-24 拆分后import 路径直连 `ai-pipeline/auto-mark.ts`autoMarkExamAction和 `actions-rich-editor.ts`createExamFromRichEditorAction/updateExamFromRichEditorAction支持 create/edit 双模式 |
@@ -1165,11 +1251,12 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- ✅ v1 测试修复:`SUBJECT_COLORS` 新增 `Chinese` 主题色rose
- Utils✅ 新增,纯函数 + 单测):`sortChapters` / `buildChapterTree` / `buildChapterIndex` / `findChapterParent` / `filterKnowledgePointsByChapter` / `normalizeOptional` / `highlightKnowledgePoints` / `hasCycleAfterAddingEdge`Task 4 新增循环依赖检测DFS 算法)/ `findChapterById`V3 续审计新增:递归查找章节树中指定 ID 的章节,替代各模块本地重复实现)
- Graph-layout✅ 新增,纯函数 + 单测):`computeGraphLayout`
- Lib✅ v6 审计修复新增,类型守卫集合):`isKpWithRelations` / `isGraphNodeData` / `isGraphLayoutNodeData` / `isGraphEdgeData` / `isKpGraphNode` / `isKpGraphLink`(位于 `lib/type-guards.ts`,替代 components/* 中针对 React Flow `node.data` 与 react-force-graph-2d `NodeObject`/`LinkObject` 的 `as` 断言,从 unknown 安全收窄到具体业务类型)
- Components✅ Task 13 新增):`GraphNodeDetailPanel` — 知识图谱节点详情侧边面板,展示知识点名称/描述/掌握度/关联题目/前置后置知识点,支持跳转与前置关系增删
- Analytics✅ 新增):`TextbookAnalytics` / `TextbookAnalyticsProvider` / `useTextbookAnalytics`
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`@xyflow/react`(知识图谱可视化)、`@dagrejs/dagre`(图谱分层布局算法)
- 依赖:`shared/*`、`@/auth`、`@xyflow/react`(知识图谱可视化)、`@dagrejs/dagre`(图谱分层布局算法)、`react-force-graph-2d`v4 新增Obsidian 风格力导向图v6 types.ts 导入其 NodeObject/LinkObject 类型供 KpGraphNode/KpGraphLink 接口继承)
- 被依赖:`questions`(✅ P1-1 已修复:通过 textbooks data-access、`exams`(通过类型)、`dashboard`(通过 data-accessP0-4 已修复)
- ✅ UI 层跨模块依赖已解耦:`textbooks/components/knowledge-point-dialogs.tsx` 不再直接 import questions 模块,改为通过 render prop 注入创建题目入口
@@ -1214,6 +1301,10 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- ✅ v3 P2 修复2026-06-24`textbook-content-panel.tsx` 17 个 props 按 `chapter`/`editing`/`selection`/`highlight` 四组重构,提升可读性
- ✅ v3 P2 修复2026-06-24`textbook-reader.tsx` 移除命令式 `classList` 操作,视觉高亮改为 `isHighlighted` 状态驱动,仅保留 `scrollIntoView`
- ✅ v3 P2 修复2026-06-24`use-kp-crud.ts`122 行)拆分为 `use-kp-create`/`use-kp-delete`/`use-kp-update` 三个子 Hook + 门面,均在 80 行限制内
- ✅ v6 审计修复2026-07-07`graph-kp-node.tsx` 2 处 `as` 断言(`data.kp as KpWithRelations`、`data.graphData as GraphNodeData | undefined`)改用 `lib/type-guards.isKpWithRelations`/`isGraphNodeData` 守卫,`extractNodeData` 返回 `| null`,校验失败组件渲染 null
- ✅ v6 审计修复2026-07-07`graph-prerequisite-edge.tsx` `data as unknown as GraphEdgeData | undefined` 双重断言改用 `lib/type-guards.isGraphEdgeData` 守卫
- ✅ v6 审计修复2026-07-07`knowledge-graph-node.tsx` `node.data as GraphLayoutNodeData` 断言改用 `lib/type-guards.isGraphLayoutNodeData` 守卫
- ✅ v6 审计修复2026-07-07`force-graph.tsx` 6 处 `as` 断言(`node as KpGraphNode` ×3、`link as KpGraphLink`、`kpLink.source as KpGraphNode`、`kpLink.target as KpGraphNode`)改用 `lib/type-guards.isKpGraphNode`/`isKpGraphLink` 守卫;`KpGraphNode`/`KpGraphLink` 接口从 force-graph.tsx 迁移至 types.ts 供守卫模块引用;保留 Line 80 `as unknown as ComponentType` 第三方库next/dynamic类型限制并加注释
- ⚠️ i18n 覆盖率约 99%v3 新增 `notFound`/`noClassMasteryPermission`/`close`/`prerequisiteAddFailed`/`prerequisiteRemoveFailed` 等键data-access 异常文案已错误码化)
- ⚠️ P2 图谱方向键导航未实现
- ⚠️ P2 data-access 函数未导出非缓存版本,单测难以 mock
@@ -1224,11 +1315,12 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `actions.ts` | 502 | 14 个 Server Action写操作含 Zod 校验 + 资源归属校验 + 知识图谱查询/前置依赖管理 + class-mastery 视图) |
| `data-access.ts` | 593 | 教材/章节/知识点 CRUD + 跨模块查询接口 + 资源归属校验 + 数据范围过滤 + 前置依赖 CRUDv3 P2硬编码异常改为 NotFoundError |
| `data-access-graph.ts` | 188 | 知识图谱只读查询(✅ Task 5 新增:知识点关联聚合、学生/班级掌握度、前置后置知识点,标记 `server-only`v2 修复:前置依赖双向 IN 过滤防跨教材污染) |
| `types.ts` | 94 | 类型定义含知识图谱类型GraphViewMode/GraphLayoutMode/MasteryInfo/KpWithRelations/GraphNodeData/GraphEdgeData/KnowledgeGraphData/MasteryLevel |
| `types.ts` | 133 | 类型定义含知识图谱类型GraphViewMode/GraphLayoutMode/MasteryInfo/KpWithRelations/GraphNodeData/GraphEdgeData/KnowledgeGraphData/MasteryLevelv6 审计修复新增KpGraphNode/KpGraphLink 从 force-graph.tsx 提取至此,供 lib/type-guards.ts 引用 |
| `schema.ts` | 56 | Zod 校验(含 CreatePrerequisiteSchema/DeletePrerequisiteSchemav2 修复refine 消息改为英文v3 修复:移除死代码 ReorderChaptersSchema |
| `constants.ts` | 91 | 学科/年级常量与颜色映射(✅ 新增v1 测试修复:新增 Chinese/Grade 1/Grade 2 |
| `utils.ts` | 226 | 章节树构建/排序/查找等纯函数 + 循环检测(✅ 新增,含单测) |
| `graph-layout.ts` | 109 | 知识图谱布局计算纯函数(✅ Task 8 重写为 dagre 集成,输出 React Flow 格式含单测v3 P2O(n²) 改为 Set 查找) |
| `lib/type-guards.ts` | 122 | v6 审计修复新增教材模块类型守卫集合isKpWithRelations/isGraphNodeData/isGraphLayoutNodeData/isGraphEdgeData/isKpGraphNode/isKpGraphLink替代 components/* 中针对 React Flow `node.data` 与 react-force-graph-2d `NodeObject`/`LinkObject` 的 `as` 断言 |
| `analytics.tsx` | 37 | 教材分析 Context/Provider/Hook✅ 新增) |
| `hooks/use-knowledge-point-actions.ts` | 45 | 知识点操作门面 Hookv2 拆分:组合 useKpDialogState + useKpCrud对外 API 不变) |
| `hooks/use-kp-dialog-state.ts` | 33 | 知识点对话框状态管理 Hookv2 新增:从 use-knowledge-point-actions 拆分) |
@@ -1239,10 +1331,10 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `hooks/use-text-selection.ts` | 49 | 文本选区捕获 Hook |
| `hooks/use-graph-data.ts` | 65 | 知识图谱数据加载 Hook✅ Task 11 新增:派生值模式避免 effect 中 setStatev2 修复:区分 isLoading/isRefreshing 避免 UI 闪烁) |
| `components/knowledge-graph.tsx` | 408 | 知识图谱主组件(✅ Task 10/15 重写:全书视图 + 搜索高亮 + 关联节点高亮 + 章节着色v2 新增:添加/删除前置依赖 Dialogv4 新增hierarchical/force 布局模式切换,根据布局模式渲染分层 ReactFlow 或 Obsidian 风格力导向图,状态/详情面板/依赖增删对话框共享) |
| `components/graph-kp-node.tsx` | 92 | React Flow 自定义节点(✅ Task 9 新增:知识点名称+题目数徽章+掌握度进度条v2 修复:节点宽度常量化 NODE_WIDTHv3 修复:类型守卫替代 as 断言) |
| `components/graph-prerequisite-edge.tsx` | 43 | React Flow 自定义边(✅ Task 9 新增:虚线+箭头表示前置依赖v2 修复GraphEdgeData 类型安全转换) |
| `components/graph-kp-node.tsx` | 105 | React Flow 自定义节点(✅ Task 9 新增:知识点名称+题目数徽章+掌握度进度条v2 修复:节点宽度常量化 NODE_WIDTHv3 修复:类型守卫替代 as 断言v6 审计修复extractNodeData 改用 lib/type-guards.isKpWithRelations/isGraphNodeData 守卫,校验失败返回 null |
| `components/graph-prerequisite-edge.tsx` | 47 | React Flow 自定义边(✅ Task 9 新增:虚线+箭头表示前置依赖v2 修复GraphEdgeData 类型安全转换v6 审计修复:改用 lib/type-guards.isGraphEdgeData 守卫替代 `as unknown as` 双重断言 |
| `components/graph-toolbar.tsx` | 121 | 图谱工具栏(✅ Task 9 新增:视图模式切换 + 搜索 + 重置视图v2 新增isRefreshing 轻量指示器v4 新增hierarchical/force 布局模式切换 Select |
| `components/force-graph.tsx` | 451 | Obsidian 风格力导向图谱组件v4 新增:基于 react-force-graph-2d + d3-forcev5 视觉对齐 Obsidian Graph View小圆点节点+极细半透明边线+标签右侧带背景+hover/选中邻居高亮+紫色外圈焦点环;通过 next/dynamic ssr:false + import type 加载避免 SSR canvas/window 报错) |
| `components/force-graph.tsx` | 446 | Obsidian 风格力导向图谱组件v4 新增:基于 react-force-graph-2d + d3-forcev5 视觉对齐 Obsidian Graph View小圆点节点+极细半透明边线+标签右侧带背景+hover/选中邻居高亮+紫色外圈焦点环;通过 next/dynamic ssr:false + import type 加载避免 SSR canvas/window 报错v6 审计修复KpGraphNode/KpGraphLink 接口迁移至 types.ts6 处 `as` 断言改用 lib/type-guards.isKpGraphNode/isKpGraphLink 守卫;保留 Line 80 `as unknown as ComponentType` 第三方库类型限制 |
| `components/graph-node-detail-panel.tsx` | 171 | 节点详情侧边面板(✅ Task 13 新增:描述/掌握度/关联题目/前置后置列表v2 修复:移除未使用 textbookId propv3 修复aria-label |
| `components/chapter-sidebar-list.tsx` | 325 | 章节侧边栏列表v2 修复canEdit 默认 false + orderUpdateFailed i18n keyv3 修复:键盘导航) |
| `components/section-error-boundary.tsx` | 53 | 章节内容错误边界v2 修复:默认值改为空字符串 + 条件渲染v3 修复componentDidCatch + a11y |
@@ -1710,6 +1802,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `correlation-compute.ts` | ~90 | L-9 纯函数: Pearson 相关系数 + 风险分级 + 汇总 (与 IO 解耦) |
| `data-access-correlation.ts` | ~130 | L-9 跨模块关联分析: 委托 classes/users/grades data-access |
| `components/attendance-grade-correlation-card.tsx` | ~220 | L-9 散点图 + 风险表格 (recharts ScatterChart) |
| `lib/type-guards.ts` | - | ✅ as 断言修复新增2026-07-07`isAttendancePeriod` 守卫,替代 components/attendance-sheet.tsx 中 Select `onValueChange` 的 `as AttendancePeriod` 断言 |
> 架构变更2026-07-05Phase 4.5 — attendance-sheet.tsx 提交状态改用 useOptimistic
> - **attendance-sheet.tsx**`isSubmitting` useState → `useOptimistic<boolean>`。保留 `<form action={handleSubmit}>` 模式 + `useFormStatus` SubmitButtonprogressive enhancement`setOptimisticSubmitting(true)` 在 form action 内调用action 完成后 `optimisticSubmitting` 自动恢复 false移除 `finally` 块
@@ -1857,6 +1950,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- 被依赖:`notifications`(✅ 已消除反向依赖)、`settings`(通知偏好表单)、`layout`(✅ P1-4 已修复:通知下拉组件改为从 notifications 模块导入)
**已知问题**
- ✅ 审计 v1 P0-5 已修复:~~`data-access.ts` 1089 行超 1000 行硬限(规则 S-01混合 CRUD/批量/群组/模板/举报多职责~~ 拆分为 5 个子文件(`data-access-core.ts` / `data-access-bulk.ts` / `data-access-group.ts` / `data-access-templates.ts` / `data-access-reports.ts``data-access.ts` 改为 barrel 文件 `export * from "./data-access-*"` 保持向后兼容;同时迁移至 `shared/lib/date-utils.ts` 的 `serializeDate` 统一日期序列化(规则 S-04 DRY
- ✅ P0-4 已修复:~~`sendMessageAction` 绕过 notifications dispatcher 直接调用 `createNotification`~~ 改为调用 `notifications.sendNotification`,通知 CRUD 已迁移至 notifications 模块
- ✅ P0 已修复:~~与 notifications 双向依赖 + 职责重叠~~ 通知相关表messageNotifications / notificationPreferences所有权已移交 notifications 模块messaging 仅保留 messages 表
- ✅ P1-5 已修复:~~同时管理 3 类数据messages + messageNotifications + notificationPreferences~~ 仅管理 messages 表,通知相关数据由 notifications 模块管理
@@ -1915,7 +2009,12 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | ~360 | 11 个私信 Server Action✅ 审计 V1-P0-4删除 `getMessageDetailAction` 死代码;✅ 审计 V1-P1-3新增 `getMessageThreadAction`;✅ 审计 V1-P0-1/P0-6/P1-9sendMessageAction 增加 receiverId 二次校验 + parentMessageId 校验 + 失败埋点;✅ V3-P1-3通知偏好 Action 已迁移至 notifications✅ V3-P0-1`saveMessageDraftAction` 使用 Zod 校验;✅ V2-P0-2通知标题 i18n 化) |
| `data-access.ts` | ~430 | 私信 CRUD + ✅ 审计 V1-P0-1 新增 `isReceiverAllowed` + ✅ 审计 V1-P0-2 `getMessageThread` 增加 userId 校验 + ✅ 审计 V1-P0-3 删除 `getMessagesPageData` 编排迁出至页面层 + `getMessageDetailPageData` 编排函数(✅ P1-5 新增;✅ V3-P1-6内联类型导入改为顶部 import type |
| `data-access.ts` | - | ✅ 审计 v1 P0-5 拆分为 barrel 文件:`export * from "./data-access-*"`,向后兼容所有现有 import 路径(原单文件 1089 行超 1000 硬限,规则 S-01 |
| `data-access-core.ts` | - | ✅ 审计 v1 P0-5 新增helpers`resolveUserNames` / `toIso` via shared util+ 核心 CRUD`getMessages` / `getMessageById` / `getMessageThread` / `createMessage` / `markMessageAsRead` / `deleteMessage` / `toggleMessageStar` / `recallMessage` / `getUnreadMessageCount` / `getRecipients` / `isReceiverAllowed` / `getMessageDetailPageData` |
| `data-access-bulk.ts` | - | ✅ 审计 v1 P0-5 新增:批量操作(`bulkMarkMessagesAsRead` / `bulkDeleteMessages` / `bulkToggleMessagesStar` |
| `data-access-group.ts` | - | ✅ 审计 v1 P0-5 新增:群组消息 fan-out`sendGroupMessage` + `SendGroupMessageInput` / `SendGroupMessageResult` 接口) |
| `data-access-templates.ts` | - | ✅ 审计 v1 P0-5 新增:草稿 + 模板 CRUD`getMessageDrafts` / `createMessageDraft` / `updateMessageDraft` / `deleteMessageDraft` / `getMessageDraftById` + `getMessageTemplates` / `createMessageTemplate` / `updateMessageTemplate` / `deleteMessageTemplate` |
| `data-access-reports.ts` | - | ✅ 审计 v1 P0-5 新增:举报 + 屏蔽 + 附件(`reportMessage` / `hasUserReportedMessage` / `blockUser` / `unblockUser` / `isUserBlocked` / `isEitherUserBlocked` / `getBlockedUserIds` / `getMessageReports` / `getUserBlocks` / `getMessageAttachments` + `MESSAGE_ATTACHMENT_TARGET_TYPE` |
| `schema.ts` | 40 | 私信发送校验 + messageId 校验 + 消息草稿校验(✅ V3-P0-1 新增 `MessageDraftSchema`;✅ V3-P1-3`UpdateNotificationPreferencesSchema` 已迁移至 notifications/schema.ts |
| `types.ts` | 85 | 私信类型 + ✅ 审计 V1-P1-6 新增 `RecipientRole` 类型 + re-export 通知类型(向后兼容) |
| `hooks/use-message-search.ts` | ~60 | ✅ P1-7 新增:消息搜索 hook防抖 + 请求竞态取消) |
@@ -1996,6 +2095,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `components/notification-dropdown.tsx` | ~180 | ✅ P1-4 新增(从 messaging 迁移):通知下拉菜单组件;✅ V2-P3改用 SSE 实时推送 + 轮询降级;✅ V2-P2-13c集成 useDesktopNotifications 桌面推送 |
| `hooks/use-notification-stream.ts` | ~195 | ✅ V2-P3 新增SSE 实时推送 HookEventSource + 轮询降级) |
| `hooks/use-desktop-notifications.ts` | ~100 | ✅ V2-P2-13c 新增:浏览器桌面推送 HookNotification API + 权限管理 + 点击跳转) |
| `lib/type-guards.ts` | - | ✅ as 断言修复新增2026-07-07`isStringRecord` 守卫,替代 channels/wechat-channel.ts 中 `metadata.wechatKeywords as Record<string, unknown>` 断言 |
**组件清单**
| 组件 | 职责 |
@@ -2015,7 +2115,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**职责**:操作日志 + 登录日志 + 数据变更日志查询与导出 + 审计概览仪表盘 + 数据保留策略(含 i18n + Error Boundary + 骨架屏 + 埋点 + Service 接口抽象)。
**导出函数**
- Actions`getDataChangeLogsAction` / `exportAuditLogsAction` / `exportLoginLogsAction` / `exportDataChangeLogsAction` / `getAuditRetentionConfigAction` / `saveAuditRetentionConfigAction` / `purgeAuditLogsAction`✅ 审计重构:导出逻辑下沉到 `export.ts`actions 仅编排;新增 `trackEvent` 埋点 `audit.exported`;✅ P2-5 新增保留策略 3 个 Action✅ v2 修正:保留策略操作 event 改为 `audit.retention`;✅ v2 新增 `revalidatePath` 精确刷新缓存)
- Actions`getDataChangeLogsAction` / `exportAuditLogsAction` / `exportLoginLogsAction` / `exportDataChangeLogsAction` / `getAuditRetentionConfigAction` / `saveAuditRetentionConfigAction`(✅ 审计 v1 P0-3权限点由 `AUDIT_LOG_READ` 提升为 `AUDIT_RETENTION_MANAGE`,修复读权限执行写操作的提权漏洞)/ `purgeAuditLogsAction`(✅ 审计 v1 P0-3权限点由 `AUDIT_LOG_READ` 提升为 `AUDIT_LOG_PURGE`,修复读权限执行物理删除的提权漏洞;✅ 审计重构:导出逻辑下沉到 `export.ts`actions 仅编排;新增 `trackEvent` 埋点 `audit.exported`;✅ P2-5 新增保留策略 3 个 Action✅ v2 修正:保留策略操作 event 改为 `audit.retention`;✅ v2 新增 `revalidatePath` 精确刷新缓存)
- Data-access`getAuditLogs` / `getLoginLogs` / `getDataChangeLogs` / `getAuditModuleOptions` / `getDataChangeStats` / `getDataChangeTableOptions` / `getAuditLogsForExport` / `getLoginLogsForExport` / `getDataChangeLogsForExport` / `getAuditOverviewStats` / `getAuditTrend` / `getDataChangeActionStats`(✅ P2-4 新增概览统计 3 个函数)
- Export✅ 审计重构新增):`buildAuditLogExport` / `buildLoginLogExport` / `buildDataChangeLogExport`Excel 列定义 + 行映射纯函数,从 actions 抽取)
- Retention✅ P2-5 新增):`getAuditRetentionConfig` / `saveAuditRetentionConfig` / `purgeExpiredAuditLogs` / `calculateRetentionThreshold` / `isValidRetentionDays`(保留策略配置读写 + 清理 + 纯函数)
@@ -2027,6 +2127,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- 被依赖:无
**已知问题**
- ✅ 审计 v1 P0-3 已修复:~~`purgeAuditLogsAction` 与 `saveAuditRetentionConfigAction` 用读权限 `AUDIT_LOG_READ` 执行物理删除/写配置(权限提权漏洞,规则 A-04~~ 新增独立权限点 `AUDIT_LOG_PURGE`"audit_log:purge")与 `AUDIT_RETENTION_MANAGE`"audit_retention:manage"),两个 Action 改用对应专属权限点;权限点已全链路同步:`permissions.ts` 常量 → `ROLE_PERMISSIONS_SEED` admin 角色 → `permission-bitmap.ts` 注册 → `permission-catalog.ts` audit 分组 → i18n `rbac.json`zh-CN/en翻译键
- ✅ 审计重构 P0 已修复:~~Excel 导出逻辑内联在 actions 层~~ 已抽取到 `export.ts`144 行actions.ts 从 212 行精简到 139 行
- ✅ 审计重构 P0 已修复:~~三个导出 Action 结构高度重复~~ 列定义 + 行映射统一到 export.ts 的 `buildXxxExport` 函数
- ✅ 审计重构 P0 已修复:~~全模块零 i18n~~ 所有组件接入 `useTranslations("audit")` + `useLocale()`formatDate 从硬编码 `zh-CN` 改为 locale 变量;新增 `src/shared/i18n/messages/{zh-CN,en}/audit.json`(含 table/filter/empty/export/error/detail/overview/retention 命名空间)
@@ -2060,7 +2161,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 210 | 7 个 Server Action编排层3 导出 + 3 保留策略 + 1 查询;✅ v2 新增 `revalidatePath` + `audit.retention` 埋点;✅ audit-P2-5 `purgeAuditLogsAction` 新增 `loginLogRetentionDays` 参数 + trackEvent 埋点) |
| `actions.ts` | 210 | 7 个 Server Action编排层3 导出 + 3 保留策略 + 1 查询;✅ v2 新增 `revalidatePath` + `audit.retention` 埋点;✅ audit-P2-5 `purgeAuditLogsAction` 新增 `loginLogRetentionDays` 参数 + trackEvent 埋点;✅ 审计 v1 P0-3`purgeAuditLogsAction` 改用 `AUDIT_LOG_PURGE``saveAuditRetentionConfigAction` 改用 `AUDIT_RETENTION_MANAGE`,修复读权限执行写/删操作的提权漏洞 |
| `data-access.ts` | 387 | 日志查询12 个函数3 查询 + 3 导出 + 3 选项/统计 + 3 概览统计;✅ v2 P1-4 错误处理统一为 throw |
| `export.ts` | 144 | Excel 导出纯函数(列定义 + 行映射 + buildXxxExport |
| `retention.ts` | 181 | 保留策略(配置读写 + 清理 + 纯函数,✅ P2-5 新增;✅ audit-P2-5 新增 `DEFAULT_LOGIN_LOG_RETENTION_DAYS=365` + `loginLogRetentionDays` 配置 + `purgeExpiredAuditLogs` 第二可选参数) |
@@ -2307,6 +2408,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**职责**:家长视角的子女数据聚合与展示。
**导出函数**
- Actions✅ 审计 v1 P0-4 新增 `actions.ts``getChildrenAction` / `getChildBasicInfoAction` / `getChildDashboardDataAction` / `getParentDashboardDataAction` / `getChildNameListAction` / `verifyParentChildRelationAction`6 个 Server Action每个调用 `requireAuth()` + 内部通过 `verifyParentChildRelation` 做亲子关系校验,替代 app/ 页面直接 import data-access
- Data-access`getChildren` / `getChildBasicInfo` / `getChildDashboardData`V3-11 增强:并行调用 `homework/data-access.getStudentExamResults` 获取考试结果)/ `getParentDashboardData` / `verifyParentChildRelation` / `getChildNameList`(✅ v4 新增:用于详情页头部多子女切换器,一次批量查询避免 N+1
- TypesV3-11 新增):`ChildExamResultItem`单条考试结果submissionId/examId/examTitle/assignmentId/assignmentTitle/score/maxScore/submittedAt/status`ChildDashboardData` 扩展 `examResults: ChildExamResultItem[]` 字段
- Components`ParentDashboard` / `ChildCard` / `ChildDetailHeader` / `ChildDetailPanel`V3-11 增强:新增 exams Tab/ `SiblingSwitcher` / `ChildHomeworkSummary` / `ChildHomeworkDetail`v4 新增)/ `ChildGradeSummary` / `ChildGradeDetail`v4 新增)/ `ChildScheduleCard` / `ChildExamDetail`V3-11 新增:子女考试详情视图,汇总卡片+考试列表)/ `ParentChildrenDataPage` / `ParentNoChildrenPage` / `ParentAttentionBanner`v4 新增)/ `ParentAttendanceWarning`v4 新增)/ `ParentAttendanceRateCard`v4 新增)/ `ParentAttendanceCalendar`v4 新增)/ `ParentExportButton`v4 新增)
@@ -2359,6 +2461,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- 被依赖:无
**已知问题**
- ✅ 审计 v1 P0-4 已修复:~~parent 模块缺失 `actions.ts`3 个页面(`children/[studentId]/page.tsx` / `leave/page.tsx` / `elective/page.tsx`)直接 import `@/modules/parent/data-access`,绕过 Server Action 权限边界(规则 A-08~~ 新建 `src/modules/parent/actions.ts`,封装 6 个 Server Action`getChildrenAction` / `getChildBasicInfoAction` / `getChildDashboardDataAction` / `getParentDashboardDataAction` / `getChildNameListAction` / `verifyParentChildRelationAction`),每个 Action 调用 `requireAuth()` + 内部 `verifyParentChildRelation` 亲子关系校验3 个页面改为调用对应 Server Action
- ⚠️ P13 个 parent 组件(`parent-attendance-warning.tsx`/`parent-attendance-rate-card.tsx`/`parent-attendance-calendar.tsx`)直接 import `@/modules/attendance/types`,违反模块解耦原则(应通过 data-access 接口或 shared 类型抽象)
- ⚠️ P1parent-attendance-calendar.tsx 重新定义 `STATUS_DOT`/`STATUS_LABEL` 常量,与 attendance 模块重复
- ⚠️ P13 个 parent 组件的纯函数(`buildWarnings`/`aggregate`/`rateTone`/`formatDateKey`/`parseDateKey`/`buildCalendarDays`/`isSameDay`)未导出,无法单测
@@ -2379,6 +2482,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | - | ✅ 审计 v1 P0-4 新增6 个 Server Action`getChildrenAction` / `getChildBasicInfoAction` / `getChildDashboardDataAction` / `getParentDashboardDataAction` / `getChildNameListAction` / `verifyParentChildRelationAction`),每个调用 `requireAuth()` + `verifyParentChildRelation` 亲子关系校验,替代 app/ 页面直接 import data-access |
| `data-access.ts` | 243+ | 子女关系 + 仪表盘数据聚合 + 关系校验 + 子女姓名列表v4 新增 `getChildNameList` + `buildWeeklySchedule`V3-11 `getChildDashboardData` 并行调用 `getStudentExamResults` |
| `types.ts` | 79+ | 类型定义(含 JSDocv4 新增 `ChildWeeklyScheduleItem`V3-11 新增 `ChildExamResultItem` + `ChildDashboardData.examResults` |
| `components/parent-dashboard.tsx` | 110 | 仪表盘v4 重构:待办横幅 + 宫格快捷入口 + 移动端水平滑动) |
@@ -2714,6 +2818,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `types.ts` | 60 | 类型定义AiProviderSummary + SettingsService/ProfileService/NotificationPreferenceService 接口) |
| `config/role-settings-config.tsx` | 85 | 角色设置页配置驱动映射ROLE_SETTINGS_CONFIG + resolveRoleSettingsConfig |
| `lib/student-overview-data.ts` | 150 | 学生概览纯数据计算buildStudentOverviewData + computeStudentStats 等,便于单测) |
| `lib/type-guards.ts` | - | ✅ as 断言修复新增2026-07-07`isRecord` / `isMessageBody` / `isFileUploadResult` 守卫,校验 /api/upload 的 HTTP 响应体结构,替代 components/avatar-upload.tsx 中 `await response.json() as FileUploadResult` 断言 |
| `components/settings-view.tsx` | 236 | SettingsView 统一设置页布局5 标签页 + Error Boundary + Suspense + i18n + SecurityCenterCard 集成) |
| `components/settings-service-context.tsx` | 39 | SettingsServiceProvider + useSettingsServiceContext 注入服务接口) |
| `components/settings-section-error-boundary.tsx` | 64 | 分区 Error Boundary局部失败不影响整页 |
@@ -2761,6 +2866,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- ✅ audit-P1-9 已修复:~~auth 模块未定义任何 trackEvent 事件,登录成功/失败/登出/注册/2FA 启用/禁用/账户锁定/限流触发均无业务埋点~~ `shared/lib/track-event.ts` EventName 新增 8 个 auth 事件(`auth.signin_success`/`auth.signin_failure`/`auth.signout`/`auth.signup`/`auth.2fa_enabled`/`auth.2fa_disabled`/`auth.account_locked`/`auth.rate_limited`+ `trackAuthEvent` 便捷函数;`auth.ts` events.signIn/signOut 调用 `trackAuthEvent``login-service.ts` 在限流/账户锁定/密码错误/2FA 失败 4 个失败分支调用 `trackAuthEvent``modules/auth/actions.ts` `registerAction` 成功后调用 `trackAuthEvent("auth.signup")``modules/settings/actions-security.ts` `verifyTwoFactorAction`/`disableTwoFactorAction` 调用 `trackAuthEvent("auth.2fa_enabled/disabled")`
- ✅ audit-P1-10 已修复:~~proxy.ts `SPECIFIC_ROUTE_PERMISSIONS` 仅覆盖 3 个 `/admin/*` 子路径ai-settings/roles/permissions其他 `/admin/announcements`/`/admin/audit-logs`/`/admin/users` 等子路径全部走 `/admin` 前缀匹配 SCHOOL_MANAGE 权限,导致拥有 ANNOUNCEMENT_MANAGE/AUDIT_LOG_READ/USER_MANAGE 等细粒度权限的非管理员角色(如教师、年级主任)无法访问这些页面~~ 扩展 `SPECIFIC_ROUTE_PERMISSIONS` 至 15 个条目,新增 `/admin/announcements`(ANNOUNCEMENT_MANAGE)、`/admin/audit-logs` + 3 子路径(AUDIT_LOG_READ)、`/admin/elective`(ELECTIVE_MANAGE)、`/admin/questions`(QUESTION_READ)、`/admin/users`(USER_MANAGE)、`/admin/error-book`(ERROR_BOOK_ANALYTICS_READ)、`/admin/lesson-plans`(LESSON_PLAN_READ)、`/admin/course-plans`(COURSE_PLAN_READ)。精确路由优先级高于前缀匹配,使非管理员角色可直接访问对应权限的 `/admin/*` 页面
- ✅ audit-P1-12 已修复:~~proxy.ts 内联 4 个权限映射常量SPECIFIC_ROUTE_PERMISSIONS / ROUTE_PERMISSIONS / DASHBOARD_ROUTE_PERMISSIONS / API_PERMISSIONS新增路由或权限点时必须修改 proxy.ts 逻辑代码,违反配置驱动设计~~ 抽取 `shared/lib/route-permissions.ts` 配置文件,导出 4 个 `Record<string, Permission>` 类型常量(重命名 `ROUTE_PERMISSIONS`→`ROUTE_PREFIX_PERMISSIONS`、`API_PERMISSIONS`→`API_ROUTE_PERMISSIONS` 以更准确表达语义。proxy.ts 从此文件 import新增路由权限只需修改配置文件无需改 proxy.ts 逻辑;所有映射 value 类型从 `string` 收紧为 `Permission`,消除 `as Permission` 断言
- ✅ logging 重构 Task 5proxy.ts 注入 `x-request-id` 请求头。每个请求入口通过 Web Crypto API `crypto.randomUUID()` 生成 requestIdEdge Runtime 兼容,不导入 `node:async_hooks`/`request-context.ts`),复用上游传入的 `x-request-id` 请求头(若有)。通过内部助手 `injectRequestId(request, requestId)` 构造新 Headers 对象,经 `NextResponse.next({ request: { headers } })` 转发给下游 RSC最终 `response.headers.set("x-request-id", requestId)` 暴露给客户端便于日志关联。函数签名未变,`injectRequestId` 为非导出内部函数
- ✅ audit-P2-3 已实施:~~系统开放注册缺少预分配角色/班级的机制,管理员无法批量引导特定用户(如新教师/家长)注册~~ 新增 `invitation_codes` 表 + `modules/invitation-codes` 模块types/schema/lib/code-generator/data-access/actions8 字符安全字母表(剔除 I/O/0/1+ Set 去重 + 批量插入admin 可批量生成邀请码1-100 张/批)并指定角色/限定邮箱/绑定班级/设置过期/备注,生成结果仅此一次返回明文供复制;注册流程集成 `invitationCode` 字段(可选),`registerAction` 调用 `validateInvitationCode()` → `consumeInvitationCode()`(乐观锁 `UPDATE ... WHERE usedBy IS NULL`),邀请码角色覆盖默认 student 角色admin 管理页 `/admin/invitation-codes`USER_MANAGE 权限)含统计卡片/表格/生成对话框/删除确认/复制i18n `invitationCodes` 命名空间已注册zh-CN + en
- ✅ audit-P2-4 已修复:~~注册流程未检测密码是否在已知数据泄露中出现,弱密码(如 "Pass1234" 等热门泄露密码)可通过 Zod 最小长度校验~~ 新增 `shared/lib/breached-password.ts` 接入 Have I Been Pwned APIk-anonymity 范围查询:仅发送 SHA-1 前 5 字符,本地比对完整哈希后缀,不泄露用户密码);`registerAction` 在邮箱可用性检查后、`createUser` 前调用 `checkBreachedPassword(data.password)`,命中则返回新错误码 `BREACHED_PASSWORD` + 写审计日志i18n 键 `auth.register.errors.BREACHED_PASSWORD` 已添加zh-CN + en。fail-open 策略HIBP API 不可用时(超时/网络错误)返回 `checkSkipped=true` 跳过检查,不阻断注册流程
- ✅ audit-P2-6 已修复:~~`AuthLayout` 学校品牌信息(学校名/logo/标语/作者)硬编码在组件内,无法由管理员动态配置~~ `AuthLayout` 接受 `brand?: BrandConfig` prop`app/(auth)/layout.tsx` 改为 async Server Component调用 `modules/settings/data-access-brand.getBrandConfig()` 读取品牌配置并注入;数据库不可用时 try/catch fail-open 使用 `DEFAULT_BRAND_CONFIG` 默认值,不阻断认证页面渲染。品牌配置由 `modules/settings` 提供完整 CRUD 链路(详见 settings 模块)
@@ -2793,8 +2899,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**导出函数**
- UI 组件:`OnboardingStepper` / `ParentChildrenForm`
- Hook`useOnboardingForm` / `UseOnboardingFormReturn`(类型)
- Server Actions`actions.ts``getOnboardingStatusAction` / `completeOnboardingAction`
- Data-access`data-access.ts``getOnboardingStatus` / `updateUserProfile` / `bindParentToChild`
- Server Actions`actions.ts``getOnboardingStatusAction` / `completeOnboardingAction`(✅ 审计 v1 P0 修复actions 层不再直查 DB改为调用 data-access 的 `getUserOnboardedAt` / `markUserOnboarded`
- Data-access`data-access.ts``getOnboardingStatus` / `updateUserProfile` / `bindParentToChild` / `getUserOnboardedAtRaw` / `getUserOnboardedAt`cacheFn 包装)/ `markUserOnboarded`(✅ 审计 v1 P0-1/P0-2 新增P-01 文件头补 `import "server-only"`
- Schema`schema.ts``OnboardingSchema`Zodv3 重构为 children 数组)
- Types`types.ts``OnboardingRoleInfo` / `OnboardingStatus` / `OnboardingCompleteData` / `OnboardingFailureItem` / `BindParentToChildParams` / `ChildRow` / `EMPTY_CHILD_ROW`
@@ -2803,6 +2909,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- 被依赖:`app/onboarding/page.tsx`、`proxy.ts`(读 `token.onboarded`
**已知问题**
- ✅ 审计 v1 P0-1/P0-2 已修复:~~`onboarding/data-access.ts` 缺 `import "server-only"``onboarding/actions.ts` 直查 DB违反三层架构 A-09~~ data-access.ts 文件头补 `import "server-only"`;新增 `getUserOnboardedAtRaw` / `getUserOnboardedAt`cacheFn 包装) / `markUserOnboarded` 三个 data-access 函数actions.ts 移除 `db.select` / `db.transaction` / `tx.update` 直查,改为调用 data-access 函数;移除 `eq` / `db` / `users` schema 导入
- ✅ audit-P1-2 已修复:~~`onboarding-stepper.tsx` 451 行接近 500 上限~~ 拆分为 Hook + 子组件 + 编排器三层,主文件降至 259 行
- ✅ audit-P1-8 已修复:~~`bindParentToChild` 未做独立速率限制,单次 Action 内可循环调用 10 次,三因子(生日 × 手机后4枚举攻击窗口~~ 在 `completeOnboardingAction` 中针对家长角色且 `children.length > 0` 时添加独立速率限制 key `onboarding:bind:{parentId}`,限制每小时 5 次5 × 10 = 50 次尝试/小时,远低于 3.65M 组合枚举所需量级);触发限流时写审计日志 `onboarding.bind_rate_limited` 并返回中文提示i18n 待后续迁移)
- ✅ audit-P1-13 已修复:~~步骤列表与角色过滤逻辑硬编码在 `use-onboarding-form.ts``STEPS_KEYS` 常量 + `isAdmin ? filter : identity` 三元表达式),新增/调整步骤必须修改 Hook 逻辑~~ 抽取配置驱动设计:在 `types.ts` 新增 `OnboardingStepId` / `OnboardingStepConfig` 类型 + `ONBOARDING_STEPS` 配置数组 + `getOnboardingStepsForRole(role)` 纯函数过滤步骤;`use-onboarding-form.ts` 消费配置派生 `stepConfigs`/`stepKeys`/`currentStepId`/`canSkip`,不再内联步骤定义;`onboarding-stepper.tsx` 按 `currentStepId` 渲染内容(替代数字下标 `step === N`),跳过按钮可见性由配置 `canSkip` 驱动。**附带修复**:原 `grade_head`/`teaching_head` 在 roleInfo 步骤无内容却无法跳过(`canSkipStep2 = isAdmin || isStudent || isTeacher` 未包含他们),现配置 `applicableRoles: ["teacher","student","parent"]` 使他们直接跳过该步骤
@@ -2811,8 +2918,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | - | Server Actions`getOnboardingStatusAction` + `completeOnboardingAction`,含幂等检查/局部错误收集/教师多科目循环绑定/审计日志/audit-P1-8 家长绑定子女独立速率限制 `onboarding:bind:{userId}` 5次/小时) |
| `data-access.ts` | - | 数据访问(`getOnboardingStatus` + `updateUserProfile` + `bindParentToChild` v3 三因子验证) |
| `actions.ts` | - | Server Actions`getOnboardingStatusAction` + `completeOnboardingAction`,含幂等检查/局部错误收集/教师多科目循环绑定/审计日志/audit-P1-8 家长绑定子女独立速率限制 `onboarding:bind:{userId}` 5次/小时;✅ 审计 v1 P0-2移除 `db.select` / `db.transaction` / `tx.update` 直查,改为调用 `getUserOnboardedAt()` / `markUserOnboarded()` data-access 函数 |
| `data-access.ts` | - | 数据访问(`getOnboardingStatus` + `updateUserProfile` + `bindParentToChild` v3 三因子验证;✅ 审计 v1 P0-1文件头补 `import "server-only"`;新增 `getUserOnboardedAtRaw` / `getUserOnboardedAt`cacheFn 包装)/ `markUserOnboarded` |
| `schema.ts` | - | Zod `OnboardingSchema`(含 children 数组,支持多子女) |
| `types.ts` | - | 类型定义(含 `ChildRow` / `EMPTY_CHILD_ROW`audit-P1-2 新增) |
| `hooks/use-onboarding-form.ts` | 208 | audit-P1-2 新增:表单状态与处理逻辑 Hookstep/name/phone/children 状态 + 导航 + URL query 持久化 + completeOnboardingAction 调用) |
@@ -3209,6 +3316,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| schema.ts | Zod 验证 |
| data-access.ts | 课标 CRUD + 树形查询 + 课案关联 |
| actions.ts | 10 个 Server Actions |
| lib/type-guards.ts | ✅ as 断言修复新增2026-07-07`isStandardLevel` / `toStandardLevel` 守卫 + 安全窄化函数,替代 data-access.ts 中 `row.level as StandardLevel` 断言 |
---
@@ -3741,6 +3849,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `schema.ts` | 45 | Zod 验证GenerateInvitationCodesSchema + InvitationCodeFieldSchema + INVITATION_ROLE_VALUES |
| `types.ts` | 65 | 类型定义InvitationRole / InvitationCodeRecord / Generate/Validate/Consume 接口) |
| `lib/code-generator.ts` | 67 | 邀请码生成器8 字符安全字母表 + Set 去重 + 批次 ID |
| `lib/type-guards.ts` | - | ✅ as 断言修复新增2026-07-07`isInvitationRole` / `toInvitationRole` 守卫 + 安全窄化函数,替代 data-access.ts 中 `row.role as InvitationRole` 断言与 generate-invitation-codes-dialog.tsx 中 Select `onValueChange` 的 `as InvitationRole` 断言 |
| `components/invitation-codes-view.tsx` | 302 | 管理视图(统计卡片 + 表格 + 删除确认 + 生成对话框入口) |
| `components/generate-invitation-codes-dialog.tsx` | 387 | 生成对话框form/result 两态,结果可复制全部/单个) |
@@ -3808,6 +3917,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `data-access.ts` | ~330 | 数据访问层CRUD + 分页 + DataScope 过滤 + 日期重叠校验 + 批量名称查询) |
| `schema.ts` | ~50 | Zod 校验CreateLeaveRequestSchema + ReviewLeaveRequestSchema |
| `types.ts` | ~70 | 类型定义LeaveType/Status/Request/ListItem/Query/Result/ReviewInput |
| `lib/type-guards.ts` | - | ✅ as 断言修复新增2026-07-07`isLeaveType` / `toLeaveType` / `isLeaveStatus` / `toLeaveStatus` 守卫 + 安全窄化函数,替代 data-access.ts 中 `row.leaveType as LeaveType` 与 `row.status as LeaveStatus` 断言 |
| `components/leave-request-form.tsx` | ~190 | 请假表单useActionState + 家长/学生双模式) |
| `components/leave-request-list.tsx` | ~120 | 只读列表 + 状态徽章 |
| `components/leave-review-dialog.tsx` | ~150 | 审批对话框 |
@@ -4305,6 +4415,79 @@ type ApiResponse<T = unknown> = {
---
## 3.8 数据库访问层审计专项2026-07-07
> 完整审计报告:[data-access-audit-v1.md](./audit/data-access-audit-v1.md)
> 结构化数据:[data-access-audit-v1-data.json](./audit/data-access-audit-v1-data.json)
> 速查手册:[known-issues.md 第二十七节](../troubleshooting/known-issues.md)
### 3.8.1 审计概况
| 指标 | 数值 |
|---|---|
| 审计文件 | 10186 data-access + 15 actions 辅查) |
| 问题总数 | 230 |
| P0 Critical | 17 |
| P1 High | 48 |
| P2 Medium | 105 |
| P3 Low | 60 |
### 3.8.2 高风险模块P0+P1 ≥ 3
| 模块 | P0 | P1 | 主要问题 |
|---|---|---|---|
| messaging | 1 | 4 | 单文件 1089 行超硬限 + 业务逻辑嵌入 |
| classes | 1 | 4 | cacheFn 全缺失 + 邀请码状态机 + N+1 |
| school | 0 | 5 | 938 行 + 30+ 导出 + 12 console.error + 角色判断 |
| lesson-preparation | 1 | 3 | LIKE 全表扫描 + N+1 + 无 LIMIT |
| scheduling | 1 | 2 | 跨模块 schema 直查 + 业务逻辑嵌入 |
| adaptive-practice | 1 | 2 | 2N+1 循环查询 |
| elective | 0 | 3 | 抽签算法 + 冲突检测 + i18n 通知嵌入 data-access |
| textbooks | 2 | 1 | 跨模块 schema 直查 + N+1 |
### 3.8.3 P0 关键问题17 条)
#### 安全漏洞4 条)
- `audit/actions.ts` purgeAuditLogsAction 用读权限执行物理删除(权限提权)
- `parent/` 模块缺失 actions.ts3 页面直访 data-access绕过权限校验
- `exams/data-access.ts` 与 `onboarding/data-access.ts` 缺 `import "server-only"`DB 逻辑泄露风险)
#### 架构硬违规5 条)
- `textbooks/data-access-graph.ts` 直查 questions + diagnostic 模块表
- `scheduling/data-access.ts` 直查 classes/users/subjects 三模块表
- `scheduling/data-access-class-schedule.ts` 含校验+状态机业务逻辑
- `onboarding/actions.ts` 直查 DB违反三层架构
- `messaging/data-access.ts` 1089 行超 1000 硬限
#### 必定性能问题8 条)
- 4 处递归/循环 N+1questionsdeleteQuestionRecursive/Batch、lesson-preparationdeleteComment、textbooksreorderChapters
- classes 24+ 读函数未走 cacheFn
- classes getTeacherClassesRaw 2N+1
- course-plans reorderCoursePlanItems N+1 + 未包裹事务
- adaptive-practice getTeacherClassPracticeOverviewsRaw 2N+1
### 3.8.4 治理路线图
| Phase | 范围 | 工作量 | 完成标准 |
|---|---|---|---|
| Phase 0 | P0 安全漏洞 | XS-S | server-only 补齐 + parent actions 新建 + audit 权限点新增 |
| Phase 1 | P0 架构+性能 | M-L | 跨模块 schema 治理 + messaging 拆分 + N+1 修复 + 业务逻辑下移 |
| Phase 2 | P1 性能+结构 | M-L | LIKE 治理 + 超长文件拆分 + school 重构 + 事务修复 |
| Phase 3 | P2 模式标准化 | S-M | cacheFn 补齐 + SELECT * 改显式 + helper 提取 + JSDoc |
| Phase 4 | P3 风格优化 | XS | as 断言 + 非空断言 + export * + 死代码 |
### 3.8.5 架构图待补记项
✅ Phase 0 P0 修复已完成2026-07-07以下待补记项已全部同步至 004/005 架构文档:
1. ✅ parent 模块缺失 actions.ts —— 已新建 `src/modules/parent/actions.ts`6 个 Server Action 已补记至 004 §2.19 与 005 modules.parent.exports.actions
2. ✅ onboarding/actions.ts 直查 DB —— 已改为调用 data-access 函数dependencyMatrix 已修正005 onboarding.auditNote 已更新)
3. ✅ messaging/data-access.ts 拆分后 —— 5 个子文件已补记至 004 §2.13 文件清单与 005 modules.messagingauditNotes 已更新)
4. ✅ 新增权限点 AUDIT_LOG_PURGE / AUDIT_RETENTION_MANAGE —— 已补记至 004 §2.15 与 005 permissions + rolePermissions.admin
5. ✅ 新增 shared/lib/date-utils.ts —— 已补记至 004 §2.1 导出函数与文件清单、005 modules.shared.exports.functions
---
# 附录 A模块间依赖矩阵
> 行表示使用方,列表示被使用方。`` 合理依赖,`` 违规直查,`` 循环依赖。

File diff suppressed because one or more lines are too long

View File

@@ -72,6 +72,7 @@ const label = t(question.type) // t("single_choice") → 找不
|------|------|
| Node.js 服务端驱动标记为外部包 | `serverExternalPackages: ["mysql2"]` |
| 输出模式 | `output: "standalone"` |
| Tailwind v4 排除非源码目录防止误扫描 | `@source not "../../docs"; @source not "../../scripts"; @source not "../../tests";` 置于 `globals.css` | 不配置docs 中的 `[length:var(--space-N)]` 字符串被扫描成类,生成 `length: var(--*)` 致 CSS 解析失败 `Unexpected token Delim('*')` |
---
@@ -570,6 +571,20 @@ export function AnnouncementPagination({ page, pageSize, total, basePath, status
| AI 输出 patch 类型用 `Record<string, unknown>` | `interface NodeContentUpdate { data: Record<string, unknown> }`(诚实反映 Zod `z.record(z.string(), z.unknown())` 校验后的 untrusted 输出,强制调用方走 `mergeBlockDataPatch` | `interface NodeContentUpdate { data: Partial<BlockData> }`(伪装成可信类型,调用方直接 `as BlockData` 断言) |
| switch 分发用类型守卫而非 `as` | `case "objective": return isObjectiveBlockData(data) ? flattenObjective(data, t) : []` | `case "objective": return flattenObjective(data as ObjectiveBlockData, t)` |
| TextbookContentNode 分支用 `unknown` 中间变量 | `const merged: unknown = { ...n, ...patch }; return merged as TextbookContentNode`(结构类型不兼容 Block.data 联合,从 unknown 收窄需 `as` | `return { ...n, ...patch } as unknown as TextbookContentNode`(双重断言) |
| React Flow `node.data` 收窄用类型守卫 | `const nodeData = isGraphLayoutNodeData(node.data) ? node.data : undefined` | `node.data as GraphLayoutNodeData` |
| React Flow `EdgeProps.data` 双重断言用类型守卫 | `const edgeData = isGraphEdgeData(data) ? data : undefined` | `data as unknown as GraphEdgeData | undefined` |
| react-force-graph-2d `NodeObject` 收窄用类型守卫 | `if (!isKpGraphNode(node)) return; const kpNode = node` | `const kpNode = node as KpGraphNode` |
| react-force-graph-2d `LinkObject` 收窄用类型守卫 | `if (!isKpGraphLink(link)) return; const kpLink = link` | `const kpLink = link as KpGraphLink` |
| 力导向边 source/target 字符串排除后守卫 | `if (typeof source === "string") return; if (!isKpGraphNode(source)) return` | `kpLink.source as KpGraphNode` |
| next/dynamic 第三方库类型限制保留 `as` | `as unknown as ComponentType<React.ComponentProps<typeof ForceGraph2DType>>`next/dynamic 返回 `ComponentType` 与库默认导出类型不兼容,需 `as unknown as` 保留完整 props 类型) | 无(库类型限制无法用守卫替代) |
| DB JSON `unknown` 字段收窄用 `isRecord` | `if (!isRecord(value)) return value; const record = value` | `const record = value as Record<string, unknown>` |
| Tiptap `JSONContent` 收窄用守卫 | `isJSONContent(v) ? v : fallback` / `isJSONContentArray(arr) ? arr : []` | `slice.content.toJSON() as JSONContent[]` / `node as JSONContent` |
| dnd-kit `UniqueIdentifier` 转 string 用 `toSortableId` | `toSortableId(event.active.id)` / `toSortableId(over.id)` | `event.active.id as string` / `over.id as string` |
| Tiptap NodeView `node.attrs` 收窄用守卫 | `const attrs = isQuestionBlockAttrs(node.attrs) ? node.attrs : { questionId: "", type: "single_choice", score: 0 }` | `node.attrs as QuestionBlockAttrs` |
| AI 接口返回 `doc` 字段守卫失败显式报错 | `if (!isJSONContent(result.data.doc)) { toast.error(t("...")); return }` | `result.data.doc as EditorJSONContent` |
| next-intl `t()` params 类型放宽 | `type TranslationFn = (key: string, params?: Record<string, string \| number \| Date>) => string` | `t(key, params as Record<string, string \| number \| Date> \| undefined)` |
| react-hook-form + zodResolver 协变差异保留 `as unknown as` | `zodResolver(formSchema) as unknown as Resolver<ExamFormValues>`(库协变差异需 `as unknown as`,注释说明原因) | `zodResolver(formSchema) as Resolver<ExamFormValues>`(直接 `as` 违规) |
| 复杂 RichQuestionContent 结构守卫失败用 isRecord 兜底 | `if (!isRecord(parsed)) return fallback; const content = parsed as EditorQuestion["content"]`(结构校验由下游 Zod schema 保证,注释说明) | `parsed as EditorQuestion["content"]`(直接断言无守卫) |
严重违规双重断言:
- `structure-tree.tsx:72` `{ ...textbookNode, type: "textbook_content" } as unknown as Block`
@@ -587,6 +602,31 @@ export function AnnouncementPagination({ page, pageSize, total, basePath, status
- `messaging/data-access.ts` — 5 处字面量 `as RecipientRole` 移除 + 2 处 `mapMessageReport` 改用 `toXxx` 辅助
- `messaging/components/message-report-block.tsx` — `setReason(v as MessageReportReason)` 替换为 `if (isMessageReportReason(v)) setReason(v)`
**教材模块类型守卫修复2026-07-07涉及文件**
- `textbooks/lib/type-guards.ts`(新建)— `isKpWithRelations` / `isGraphNodeData` / `isGraphLayoutNodeData` / `isGraphEdgeData` / `isKpGraphNode` / `isKpGraphLink` 6 个守卫
- `textbooks/types.ts` — `KpGraphNode` / `KpGraphLink` 接口从 force-graph.tsx 迁移至此(供守卫模块引用,避免组件文件被守卫模块反向依赖)
- `textbooks/components/graph-kp-node.tsx` — 2 处 `as``data.kp as KpWithRelations`、`data.graphData as GraphNodeData | undefined`)改用守卫;`extractNodeData` 返回 `| null`,校验失败组件渲染 null
- `textbooks/components/graph-prerequisite-edge.tsx` — `data as unknown as GraphEdgeData | undefined` 双重断言改用 `isGraphEdgeData` 守卫
- `textbooks/components/knowledge-graph-node.tsx` — `node.data as GraphLayoutNodeData` 改用 `isGraphLayoutNodeData` 守卫
- `textbooks/components/force-graph.tsx` — 6 处 `as``node as KpGraphNode` ×3、`link as KpGraphLink`、`kpLink.source as KpGraphNode`、`kpLink.target as KpGraphNode`)改用 `isKpGraphNode`/`isKpGraphLink` 守卫;保留 Line 80 `as unknown as ComponentType`next/dynamic 第三方库类型限制)
**exams 模块类型守卫修复2026-07-07涉及文件**
- `exams/lib/type-guards.ts`(新建)— `isRecord` / `isSimpleTextContent` / `isQuestionContentObj` / `isJSONContent` / `isJSONContentArray` / `toSortableId` / `isQuestionBlockAttrs` 共 7 个守卫/转换函数;`isJSONContent` 返回 `v is JSONContent`(依赖 `@tiptap/react` 类型)
- `exams/ai-pipeline/request.ts` — 2 处 `as Record<string, unknown>` 改用 `isRecord` 守卫;`as const` 字面量断言保留
- `exams/components/exam-preview-utils.ts` — `isPreviewBackgroundTask` 内 `as` 改用 `isRecord`
- `exams/components/exam-actions.tsx` — `isRawStructureNode` 守卫用 `isRecord` 替代 `typeof === "object"`
- `exams/components/assembly/structure-editor.tsx` — 9 处 `as` 修复(`as string` ×4 改 `toSortableId``as QuestionContent` ×2 改 `isQuestionContentObj``as Record` 改 `isRecord`
- `exams/components/assembly/question-bank-list.tsx` — `extractQuestionText` 用 `isRecord``parsedContent` 用 `isSimpleTextContent`
- `exams/components/assembly/exam-paper-preview.tsx` — `as QuestionContent` ×2 改用 `isQuestionContentObj` + 显式映射 ChoiceOption
- `exams/components/exam-columns.tsx` — `TranslationFn` 类型签名放宽 params 类型匹配 next-intl
- `exams/components/exam-data-table.tsx` — 移除 `t(key, params as ...)` 断言,类型由 TranslationFn 保证
- `exams/components/exam-form.tsx` — `zodResolver(formSchema) as Resolver` → `as unknown as Resolver`(合规:库协变差异,加注释)
- `exams/editor/selection-toolbar.tsx` — `as JSONContent[]` / `as JSONContent` 改用 `isJSONContentArray` + `.filter(isJSONContent)`
- `exams/editor/exam-nodes-to-editor-doc.ts` — `typeof raw === "object"` 改 `isRecord`;保留 `as EditorQuestion["content"]` 加注释(结构由下游 Zod 保证)
- `exams/editor/exam-rich-editor-inner.tsx` — 移除 `editor.getJSON() as EditorJSONContent` ×2`node.attrs.type as QuestionBlockType` 改用 `isRichQuestionType` 守卫
- `exams/editor/extensions/question-block.tsx` — `node.attrs as QuestionBlockAttrs` 改用 `isQuestionBlockAttrs` 守卫,失败回退默认 attrs
- `exams/components/exam-rich-form.tsx` — `result.data.doc as EditorJSONContent` 改用 `isJSONContent` 守卫,失败 toast 报错并 return
### 非空断言 `!.` 规则
| 规则 | 正确写法 | 错误写法 |
@@ -601,9 +641,13 @@ export function AnnouncementPagination({ page, pageSize, total, basePath, status
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| effect 中读 localStorage 用 `useEffectEvent` 或加依赖 | `useEffect(() => { setRecentIds(readRecentTextbookIds()) }, [])` 改用 `useSyncExternalStore` 或初始化函数 | `useEffect(() => { setRecentIds(readRecentTextbookIds()) }, [])` 触发 set-state-in-effect |
| 禁止 `eslint-disable-next-line` | 修正依赖数组或用 `useCallback` 包裹 | `// eslint-disable-next-line react-hooks/exhaustive-deps` |
| 挂载时从 localStorage 恢复 + 含表单副作用可豁免 | `// eslint-disable-next-line react-hooks/set-state-in-effect -- 挂载时恢复 localStorage` + `setXxx(data)` | 裸 `setXxx(data)` 触发 set-state-in-effect |
| 禁止 `eslint-disable-next-line`(仅限 exhaustive-deps | 修正依赖数组或用 `useCallback` 包裹 | `// eslint-disable-next-line react-hooks/exhaustive-deps` |
| ref 禁止渲染期访问,改用 `useState` 惰性初始化 | `const [q] = useState(() => new PQueue({...}))` | `const r = useRef(null); if (!r.current) r.current = new PQueue(...)` |
| useEffect 依赖数组须含闭包引用变量 | `useEffect(() => { f(t) }, [t])` | `useEffect(() => { f(t) }, [])` 漏 `t` |
| 闭包捕获的 state 恒为初始值,条件判断无意义需移除 | `if (active && usingSSE) { fetch() }`(移除 `count === 0` | `if (active && usingSSE && count === 0)` 在 `[]` effect 内恒真 |
涉及文件:`template-picker.tsx:72`error、`schedule-dialog.tsx:53`warning、`lesson-plan-editor.tsx:81`disable
涉及文件:`template-picker.tsx:72`error、`schedule-dialog.tsx:53`warning、`lesson-plan-editor.tsx:81`disable、`use-exam-preview-tasks.ts:28,36`refs+setState、`unread-message-badge.tsx:92`(闭包陷阱)、`practice-result-view.tsx:26`unused、`seed-grade5-chinese.ts`unused imports
### Tailwind 任意值规则
@@ -1377,4 +1421,180 @@ if (announcement.type === "school") {
| i18n 键同步新增 | SectionErrorBoundary 的 namespace 需有 `error.boundaryTitle` 等键 | 新增 namespace 但不补 i18n 键 |
| 架构文档同步 004/005 | 每批闭环后更新 exports/lastUpdate | 全部完成后才更新 |
## 二十六、`as` 断言违规修复规则2026-07-07
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 禁止 `as` 断言(除 unknown 转换/测试) | 类型守卫 / `as unknown as T` + 注释 | `value as T` |
| JSON.parse 结果必须经 unknown 中转 | `const raw: unknown = JSON.parse(x); const d = raw as T` | `JSON.parse(x) as T` |
| localStorage 数据需字段校验 | `isRecord` + 逐字段 `typeof` 校验 | `JSON.parse(x) as MyType` |
| URL search param 需类型守卫 | `isTimeRange(v) ? v : "today"` | `v as TimeRange` |
| HTTP 响应需类型守卫 | `isFileUploadResult(raw) ? raw : fallback` | `(await res.json()) as Result` |
| DB enum 字段需守卫转换 | `toLeaveType(row.leaveType)` | `row.leaveType as LeaveType` |
| Select onValueChange 需守卫 | `isAttendancePeriod(v) ? v : "full_day"` | `v as AttendancePeriod` |
| 表单 event.currentTarget 需 instanceof | `if (e.currentTarget instanceof HTMLFormElement)` | `e.currentTarget as HTMLFormElement` |
| 空对象赋值需显式标注 | `const empty: Record<string, string> = {}` | `{} as Record<string, string>` |
| Set<X>.has(value) 需改 Set<string> | `const SET = new Set<string>([...]); SET.has(value)` | `SET.has(value as X)` |
| readonly X[] 转 readonly string[] | `const ARR: readonly string[] = [...]` | `ARR.includes(value as X)` |
| 联合类型收窄用 filter 守卫 | `.filter((n): n is 1\|2\|3 => n>=1 && n<=3)` | `.map(Number) as Array<1\|2\|3>` |
| Drizzle or() 返回 SQL\|undefined | `const r = or(...); return r ?? null` | `or(...) as SQL` |
| Zod schema + 类型守卫组合 | `parsed.data.filter(isPermission)` | `parsed.data as Permission[]` |
| 模块私有类型守卫放 lib/type-guards.ts | `lib/type-guards.ts` 导出 `isX` / `toX` | 在组件内联 `as` 断言 |
## 二十七、数据库访问层审计治理规则2026-07-07 v1 审计)
> 来源:[data-access-audit-v1.md](../architecture/audit/data-access-audit-v1.md)
### server-only 文件头规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| data-access 首行必须 `import "server-only"` | `import "server-only"\nimport { db } from ...` | `import { db } from ...`(首行缺失) |
### cacheFn 包装规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 读函数走 Raw + Wrapper 配对 | `getXxxRaw = async () => {...}; getXxx = cacheFn(getXxxRaw, {tags, ttl, keyParts})` | `getXxx = async () => { /* 直查 DB */ }` |
| 权限校验函数不缓存 | `verifyTeacherOwnsClass` 直查(避免缓存权限提升) | `verifyTeacherOwnsClass = cacheFn(...)` |
### N+1 循环 SQL 规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 递归删除改批量收集 + inArray | 收集后代 ID 数组 → `inArray(ids)` 单次删除 | 递归内逐个 SELECT + DELETE |
| 循环 UPDATE 改 CASE WHEN | `UPDATE ... SET order = CASE id WHEN ... THEN ... END WHERE id IN (...)` | `for (item) await db.update(...).set(...).where(eq(id, item.id))` |
| 跨模块循环调用改批量接口 | `getActiveStudentIdsByClassIds(classIds)` 一次查询 | `Promise.all(classIds.map(id => getActiveStudentIdsByClassId(id)))` |
| 内存 filter 改 SQL WHERE | `inArray(field, ids)` 下推到 SQL | 拉全表后 `rows.filter(r => ids.includes(r.id))` |
### LIKE 全表扫描规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 前导通配符禁止 | `LIKE 'xxx%'`(可走索引)或 `MATCH AGAINST IN BOOLEAN MODE` | `LIKE '%xxx%'` |
| JSON 列禁止 LIKE | 关联表存储提取关系 + `inArray` 等值查询 | `LIKE('%id%', JSON列)` |
| LOWER+CAST+LIKE 三重杀手 | FULLTEXT 索引 + 生成列 `content_text` | `LOWER(CAST(content AS CHAR)) LIKE '%x%'` |
### 跨模块 schema 引用规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 模块间走对方 data-access | `import { getClassNamesByIds } from "@/modules/classes/data-access"` | `import { classes } from "@/shared/db/schema"` + JOIN |
| 跨模块批量接口 | `getGradeNamesByIds(gradeIds): Promise<Map<string,string>>` | `Promise.all(gradeIds.map(id => getGradeNameById(id)))` |
### data-access 职责边界规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| data-access 不含业务逻辑 | data-access 仅 CRUD + 映射 | 时间校验/状态机/抽签算法/角色判断 |
| 状态机移至 actions 或 lib | `actions.ts` 编排校验+状态迁移,调用 data-access 原子函数 | data-access 内 `if (status === 'pending') { ... } else if ...` |
| 纯计算函数移至 lib | `lib/schedule-conflict.ts` 导出 `isScheduleConflict` | data-access 内定义 `function isScheduleConflict(...)` |
| 跨模块编排移至 actions | actions 调用多个 data-access + 纯计算 | data-access 内调用 `getGradeRecords` + 归一化计算 |
### 事务规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 多步写必须包裹事务 | `await db.transaction(async (tx) => { ... })` | 循环内独立 `db.update(...)` 无事务 |
| 事务内用 tx 而非 db | `await tx.update(...)` | `await db.update(...)`(在 transaction 回调内) |
### 错误处理规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| data-access 层 throw 上抛 | `throw new Error("...")` 让 actions 处理 | `try { ... } catch { console.error(...); return null/[] }` |
| actions 层用 ActionState | `try { ... } catch (e) { return { success: false, message: handleActionError(e) } }` | actions 层 throw 不捕获 |
| 禁止 console.error 调试 | `throw new Error("...")` | `console.error("xxx failed:", error); return []` |
### 权限校验规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 每个模块必须有 actions.ts | `src/modules/xxx/actions.ts` 包装 Server Action | app/ 页面直接 import data-access |
| 破坏性操作用专属权限点 | `requirePermission(Permissions.AUDIT_LOG_PURGE)` | `requirePermission(Permissions.AUDIT_LOG_READ)` 执行删除 |
| 读权限不执行写操作 | 写操作用 `XXX_MANAGE`/`XXX_WRITE` 权限 | `XXX_READ` 权限执行 INSERT/UPDATE/DELETE |
### 文件结构规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| data-access 单文件 ≤ 800 行 | 超过时拆分为 `data-access-{职责}.ts` | 单文件 1000+ 行混合多类职责 |
| 单文件导出函数 ≤ 20 | 按职责拆分CRUD/跨模块/导入导出) | 单文件导出 40+ 函数 |
| 重复 helper 提取到 shared/lib | `import { toISODateString } from "@/shared/lib/date-utils"` | 每个模块自定义 `serializeDate`/`toIso` |
| export * 改显式 re-export | `export { getClassNamesByIds, getClassExists } from "./data-access-queries"` | `export * from "./data-access-queries"` |
### SELECT 列枚举规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 显式枚举所需列 | `db.select({ id: users.id, name: users.name }).from(users)` | `db.select().from(users)`SELECT * |
### 无 LIMIT 保护规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 列表查询加默认 LIMIT | `.limit(pageSize).offset(offset)` 或 `.limit(500)` | 无 LIMIT 的全表查询 |
| 统计走 SQL 聚合 | `SELECT COUNT(*), SUM(CASE WHEN ...) FROM ... WHERE ...` | 拉全表后内存循环统计 |
### Phase 0 P0 修复规则2026-07-07 落地)
> 来源:审计 v1 Phase 0 P0 修复6 项 P0 全部落地tsc + lint 0 errors
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| A-09 actions 禁止直查 DB | actions.ts 调用 data-access 函数(如 `getUserOnboardedAt()` / `markUserOnboarded()` | actions.ts 中 `db.select` / `db.update` / `db.transaction` + `import { db } from '@/shared/db'` |
| 权限点注册全链路同步 | `permissions.ts` 常量 → `permissions.ts` ROLE_PERMISSIONS_SEED → `permission-bitmap.ts` → `permission-catalog.ts` → i18n `rbac.json`zh-CN/en五处同步 | 仅在 `permissions.ts` 添加常量 |
| 权限提权修复 | 破坏性操作用独立权限点 `requirePermission(Permissions.AUDIT_LOG_PURGE)` | 用低权限 `AUDIT_LOG_READ` 覆盖清理操作(`purgeAuditLogsAction` |
| A-08 缺失 actions.ts | app/ 调用 `@/modules/parent/actions` 的 Server Action | app/ 直接 import `@/modules/parent/data-access` |
| S-01 文件超 1000 行拆分 | 拆分为 `data-access-{core,bulk,group,templates,reports}.ts` + barrel `export * from "./data-access-*"` | 单文件超过 1000 行(如 messaging/data-access.ts 1089 行) |
| S-04 DRY 日期序列化 | `import { serializeDate as toIso, serializeDateRequired as toIsoRequired } from "@/shared/lib/date-utils"` | 各模块重复定义 `const toIso = (d: Date) => d.toISOString()` |
| P-01 server-only 强制 | data-access.ts 文件头 `import "server-only"` | data-access.ts 缺少 `import "server-only"`DB 逻辑泄露风险) |
---
## 二十八、日志系统规则2026-07-07 重构)
### pino logger 使用
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 服务端日志统一用 pino | `import { createModuleLogger } from "@/shared/lib/logger"; const log = createModuleLogger("module-name"); log.info({...}, "msg")` | `console.log/error/warn/info(...)` |
| 模块级 logger 命名 | `createModuleLogger("audit")` / `createModuleLogger("exams")` / `createModuleLogger("client-error")` | `createModuleLogger("Audit")` / `createModuleLogger("a")` |
| 日志参数顺序 | `log.error({ err: error, userId }, "操作描述")` | `log.error("操作描述", error)` |
| 错误对象字段名 | `log.error({ err: error }, "msg")` | `log.error({ error }, "msg")` |
### Edge Runtime 限制
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| proxy.ts 不能导入 Node.js 模块 | `const requestId = crypto.randomUUID()` (Web Crypto API) | `import { randomUUID } from "node:crypto"` |
| Edge Runtime 路由不能导入 pino | `export const runtime = "nodejs"` + `import { createModuleLogger }` | `export const runtime = "edge"` + `import { createModuleLogger }` |
| 客户端组件不能导入服务端 logger | 客户端 track-event 改为纯 no-op | 客户端 .tsx 中 `import { createModuleLogger }` |
### Request ID 贯穿
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| Server Action 包装 | `export const myAction = withRequestContext(async (args) => {...})` | 直接在 Server Action 中 `requestContextStorage.run(...)` |
| Request ID 注入 | proxy.ts 通过 `NextResponse.next({ request: { headers } })` 注入 | proxy.ts 中 `requestContextStorage.run(...)` (Edge Runtime 不支持) |
| 读取 Request ID | `getRequestContext().requestId` (从 AsyncLocalStorage) | `headers().get("x-request-id")` (重复读取) |
### ESLint no-console 规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 服务端 .ts 文件 | `import { createModuleLogger } from "@/shared/lib/logger"` | `console.error(...)` (ESLint error) |
| 客户端 .tsx 文件 | 暂时豁免(留待客户端错误上报机制处理) | - |
| scripts/ 和 tests/ | 允许 console脚本/测试场景) | - |
### 客户端错误上报
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| error.tsx 错误上报 | `useErrorReport(error)` (Hook 自动 sendBeacon) | `useEffect(() => { console.error(error) }, [error])` |
| 上报端点 | POST /api/client-error (Route Handler 用 pino 记录) | 自定义 console 输出 |
| 节流策略 | sessionStorage 记录 digest,1 分钟内不重复上报 | 每次渲染都上报 |