From 4e6d397d8ea0fffb10f27deabe865dee9ec94509 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:00:00 +0800 Subject: [PATCH] docs(architecture): sync logging refactor to 004/005 and known-issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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、客户端错误上报)。 --- .../004_architecture_impact_map.md | 213 ++++++- docs/architecture/005_architecture_data.json | 525 +++++++++++++++++- docs/troubleshooting/known-issues.md | 224 +++++++- 3 files changed, 933 insertions(+), 29 deletions(-) diff --git a/docs/architecture/004_architecture_impact_map.md b/docs/architecture/004_architecture_impact_map.md index 594cb8c..399d7d8 100644 --- a/docs/architecture/004_architecture_impact_map.md +++ b/docs/architecture/004_architecture_impact_map.md @@ -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`;`getRequestContext()` 读取当前上下文;仅 Node.js Runtime | +| `shared/lib/with-request-context.ts` | `withRequestContext(fn)` 高阶函数;从 `headers()` 读取 `x-request-id` 或生成 UUID,注入 AsyncLocalStorage | +| `shared/hooks/use-error-report.ts` | 客户端错误上报 Hook;navigator.sendBeacon 上报到 /api/client-error;sessionStorage 节流(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 Runtime(V8 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 store(zustand),集中管理 modal registry。命令式 API + Hook 双入口。与 sonner 的分工:toast 走 notify,modal 走本 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 store(zustand),集中管理 modal registry。命令式 API + Hook 双入口。**V5 Phase 4 新增 `ModalRoot` 组件**:监听 ui-store modals 状态渲染已注册 modal,路由变化时自动 `closeAll`。与 sonner 的分工:toast 走 notify,modal 走本 store | 待迁移:新增跨组件触发 modal 推荐使用此模式;存量 Dialog 保持 useState | | `createServiceProvider` | `lib/create-service-provider.tsx` | `createServiceProvider(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` 常量:`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-error;sessionStorage 节流:同一 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`,消除 12+ 个文件重复定义;含 grades/lesson-preparation 等模块的类型守卫 `isGradeDraftData`/`isExerciseBlockData`/`isPermission` 等) | | `lib/track-event.ts` | - | 监控埋点接口(server-only,no-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 新增:年级仪表盘维度3,exams 表有直接 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/children,group 节点含 title/instruction/children) +- Lib(✅ 类型守卫专项重构 2026-07-07 新增 `lib/type-guards.ts`):`isRecord`(unknown → `Record`,消除 `as Record` 断言)/ `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`(状态子 Hook,67 行,10 个 useState + 3 个更新函数)/ `usePreviewBackgroundTasks`(后台任务子 Hook,80 行,PQueue 队列 + localStorage 持久化 + 入队/打开任务)/ `usePreviewRewrite`(AI 重写子 Hook,79 行,handleRewriteSelectedQuestion 调用 regenerateAiQuestionAction) - Stats-service(V3-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 修改:题目块中图片将原生 `` 替换为 `next/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` / `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-access,P0-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 + 跨模块查询接口 + 资源归属校验 + 数据范围过滤 + 前置依赖 CRUD(v3 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/MasteryLevel;v6 审计修复新增:KpGraphNode/KpGraphLink 从 force-graph.tsx 提取至此,供 lib/type-guards.ts 引用) | | `schema.ts` | 56 | Zod 校验(含 CreatePrerequisiteSchema/DeletePrerequisiteSchema;v2 修复: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 P2:O(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 | 知识点操作门面 Hook(v2 拆分:组合 useKpDialogState + useKpCrud,对外 API 不变) | | `hooks/use-kp-dialog-state.ts` | 33 | 知识点对话框状态管理 Hook(v2 新增:从 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 中 setState;v2 修复:区分 isLoading/isRefreshing 避免 UI 闪烁) | | `components/knowledge-graph.tsx` | 408 | 知识图谱主组件(✅ Task 10/15 重写:全书视图 + 搜索高亮 + 关联节点高亮 + 章节着色;v2 新增:添加/删除前置依赖 Dialog;v4 新增:hierarchical/force 布局模式切换,根据布局模式渲染分层 ReactFlow 或 Obsidian 风格力导向图,状态/详情面板/依赖增删对话框共享) | -| `components/graph-kp-node.tsx` | 92 | React Flow 自定义节点(✅ Task 9 新增:知识点名称+题目数徽章+掌握度进度条;v2 修复:节点宽度常量化 NODE_WIDTH;v3 修复:类型守卫替代 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_WIDTH;v3 修复:类型守卫替代 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-force;v5 视觉对齐 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-force;v5 视觉对齐 Obsidian Graph View:小圆点节点+极细半透明边线+标签右侧带背景+hover/选中邻居高亮+紫色外圈焦点环;通过 next/dynamic ssr:false + import type 加载避免 SSR canvas/window 报错;v6 审计修复:KpGraphNode/KpGraphLink 接口迁移至 types.ts,6 处 `as` 断言改用 lib/type-guards.isKpGraphNode/isKpGraphLink 守卫;保留 Line 80 `as unknown as ComponentType` 第三方库类型限制) | | `components/graph-node-detail-panel.tsx` | 171 | 节点详情侧边面板(✅ Task 13 新增:描述/掌握度/关联题目/前置后置列表;v2 修复:移除未使用 textbookId prop;v3 修复:aria-label) | | `components/chapter-sidebar-list.tsx` | 325 | 章节侧边栏列表(v2 修复:canEdit 默认 false + orderUpdateFailed i18n key;v3 修复:键盘导航) | | `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-05,Phase 4.5 — attendance-sheet.tsx 提交状态改用 useOptimistic): > - **attendance-sheet.tsx**:`isSubmitting` useState → `useOptimistic`。保留 `
` 模式 + `useFormStatus` SubmitButton(progressive 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-9:sendMessageAction 增加 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 实时推送 Hook(EventSource + 轮询降级) | | `hooks/use-desktop-notifications.ts` | ~100 | ✅ V2-P2-13c 新增:浏览器桌面推送 Hook(Notification API + 权限管理 + 点击跳转) | +| `lib/type-guards.ts` | - | ✅ as 断言修复新增(2026-07-07):`isStringRecord` 守卫,替代 channels/wechat-channel.ts 中 `metadata.wechatKeywords as Record` 断言 | **组件清单**: | 组件 | 职责 | @@ -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) - Types(V3-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 - ⚠️ P1:3 个 parent 组件(`parent-attendance-warning.tsx`/`parent-attendance-rate-card.tsx`/`parent-attendance-calendar.tsx`)直接 import `@/modules/attendance/types`,违反模块解耦原则(应通过 data-access 接口或 shared 类型抽象) - ⚠️ P1:parent-attendance-calendar.tsx 重新定义 `STATUS_DOT`/`STATUS_LABEL` 常量,与 attendance 模块重复 - ⚠️ P1:3 个 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+ | 类型定义(含 JSDoc,v4 新增 `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 + useSettingsService(Context 注入服务接口) | | `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` 类型常量(重命名 `ROUTE_PERMISSIONS`→`ROUTE_PREFIX_PERMISSIONS`、`API_PERMISSIONS`→`API_ROUTE_PERMISSIONS` 以更准确表达语义)。proxy.ts 从此文件 import,新增路由权限只需修改配置文件无需改 proxy.ts 逻辑;所有映射 value 类型从 `string` 收紧为 `Permission`,消除 `as Permission` 断言 +- ✅ logging 重构 Task 5:proxy.ts 注入 `x-request-id` 请求头。每个请求入口通过 Web Crypto API `crypto.randomUUID()` 生成 requestId(Edge 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/actions),8 字符安全字母表(剔除 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 API(k-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`(Zod,v3 重构为 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 新增:表单状态与处理逻辑 Hook(step/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 = { --- +## 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 审计概况 + +| 指标 | 数值 | +|---|---| +| 审计文件 | 101(86 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.ts,3 页面直访 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+1:questions(deleteQuestionRecursive/Batch)、lesson-preparation(deleteComment)、textbooks(reorderChapters) +- 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.messaging(auditNotes 已更新) +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:模块间依赖矩阵 > 行表示使用方,列表示被使用方。`✅` 合理依赖,`❌` 违规直查,`⟳` 循环依赖。 diff --git a/docs/architecture/005_architecture_data.json b/docs/architecture/005_architecture_data.json index 2cebeed..feda1d9 100644 --- a/docs/architecture/005_architecture_data.json +++ b/docs/architecture/005_architecture_data.json @@ -5,9 +5,13 @@ "generatedAt": "2026-06-17", "formatVersion": "1.1", "rule": "每次文件修改后须同步更新本文件", - "lastUpdate": "性能预算重构专项 Phase 1-4 完成(2026-07-05 审计 + 2026-07-07 补漏):Phase 1 配置基线:(1.1) next.config.ts 新增 experimental.optimizePackageImports 覆盖 lucide-react/recharts/@xyflow/react/@tiptap/* /@radix-ui/*/date-fns;(1.2) serverExternalPackages 追加 tencentcloud-sdk-nodejs + exceljs;(1.3) providers.tsx 注入 WebVitalsReporter 闭环 RUM 上报链路;(1.4) lighthouse.yml + lighthouserc.json CI 性能门槛(LCP<=3000ms/CLS<=0.1 error 级)。Phase 2 Bundle 预算优化:(2.1) TipTap 三实例(shared/ui/rich-text-editor + exams/editor/exam-rich-editor + lesson-preparation/blocks/rich-text-block)全部改 next/dynamic ssr:false + xxx-inner.tsx 拆分模式;(2.2) KnowledgeGraph 改 ReactFlowProvider 同步壳 + next/dynamic lazy inner;(2.3) exams/editor/types.ts barrel 减少服务端消费方拉入 @tiptap/* 运行时;(2.4) chart.tsx 改 recharts 具名导入(ResponsiveContainer/Tooltip/Legend)替代 import * as RechartsPrimitive barrel;(2.5) status-badge 移除多余 use client;(2.6) teacher/student/parent 角色路由补齐 loading.tsx。Phase 3 数据层优化:(3.1) getSession/getAuthContext/resolveDataScope/getSessionTeacherId 全部 React cache() 包裹实现请求级去重;(3.2) copyCoursePlanToClasses 改单事务 + 2 次 batch INSERT;(3.3) gradeHomeworkAnswers 改 UPDATE CASE WHEN 单次批量更新;(3.4) searchQuestions 改 FULLTEXT INDEX + MATCH AGAINST BOOLEAN MODE + toBooleanModeQuery 工具;(3.5) getAllUserIds 加默认 LIMIT 1000 + 分页参数;(3.6) announcements/notifications fan-out 改 createNotifications 批量 INSERT + sendBatchNotifications 并行收集;(3.7) 高 LIMIT 默认值调低(attendance/adaptive-practice/questions data-access 1000-5000→100)。Phase 4 组件渲染优化:(4.1) AssignmentCard/EmptyState/StatusBadge React.memo;(4.2) lesson-plan-editor/text-study-block/exercise-block/paper-context-menu 拆分 Zustand 细粒度 selector(避免整体订阅);(4.3) message-detail/message-list/grade-record-list/attendance-sheet 改 useOptimistic + useTransition 乐观更新;(4.4) question-data-table/exam-data-table 接入 @tanstack/react-virtual 列表虚拟化;(4.5) sidebar-provider resize 改 debounce + useMemo;(4.6) 12 个 recharts 图表组件 margin props 提取到模块级 CHART_MARGIN 常量避免 inline 重建(parent/grades/dashboard/homework 12 文件);(4.7) scan-image-viewer aspect-ratio;(4.8) AiAssistantWidget inner 拆分 + 动态 import;(4.9) layout.tsx metadata 增强(metadataBase/openGraph/twitter/robots/authors/creator)+ Promise.all 并行获取 locale/messages/session。架构决策:(1) 动态 import 标准模式 xxx-inner.tsx + lazy wrapper;(2) 请求级去重 > 跨请求 cache(React cache() not unstable_cache);(3) 批量 SQL > 循环 SQL(INSERT batch + UPDATE CASE WHEN);(4) React 19 useOptimistic 替代手动 isPending;(5) 细粒度 Zustand selector > useShallow 多字段 selector;(6) recharts 具名导入 + optimizePackageImports 替代 barrel 导入。未完成项:React Compiler(babel-plugin-react-compiler RC 状态暂不引入)、force-dynamic 96 处评估(待后续专项)、Playwright 性能断言(待 e2e 基础设施完善)。验证:npx tsc --noEmit 零新增错误;npm run lint 零新增错误。", + "lastUpdate": "性能预算重构专项 Phase 1-4 完成(2026-07-05 审计 + 2026-07-07 补漏):Phase 1 配置基线:(1.1) next.config.ts 新增 experimental.optimizePackageImports 覆盖 lucide-react/recharts/@xyflow/react/@tiptap/* /@radix-ui/*/date-fns;(1.2) serverExternalPackages 追加 tencentcloud-sdk-nodejs + exceljs;(1.3) providers.tsx 注入 WebVitalsReporter 闭环 RUM 上报链路;(1.4) lighthouse.yml + lighthouserc.json CI 性能门槛(LCP<=3000ms/CLS<=0.1 error 级)。Phase 2 Bundle 预算优化:(2.1) TipTap 三实例(shared/ui/rich-text-editor + exams/editor/exam-rich-editor + lesson-preparation/blocks/rich-text-block)全部改 next/dynamic ssr:false + xxx-inner.tsx 拆分模式;(2.2) KnowledgeGraph 改 ReactFlowProvider 同步壳 + next/dynamic lazy inner;(2.3) exams/editor/types.ts barrel 减少服务端消费方拉入 @tiptap/* 运行时;(2.4) chart.tsx 改 recharts 具名导入(ResponsiveContainer/Tooltip/Legend)替代 import * as RechartsPrimitive barrel;(2.5) status-badge 移除多余 use client;(2.6) teacher/student/parent 角色路由补齐 loading.tsx。Phase 3 数据层优化:(3.1) getSession/getAuthContext/resolveDataScope/getSessionTeacherId 全部 React cache() 包裹实现请求级去重;(3.2) copyCoursePlanToClasses 改单事务 + 2 次 batch INSERT;(3.3) gradeHomeworkAnswers 改 UPDATE CASE WHEN 单次批量更新;(3.4) searchQuestions 改 FULLTEXT INDEX + MATCH AGAINST BOOLEAN MODE + toBooleanModeQuery 工具;(3.5) getAllUserIds 加默认 LIMIT 1000 + 分页参数;(3.6) announcements/notifications fan-out 改 createNotifications 批量 INSERT + sendBatchNotifications 并行收集;(3.7) 高 LIMIT 默认值调低(attendance/adaptive-practice/questions data-access 1000-5000→100)。Phase 4 组件渲染优化:(4.1) AssignmentCard/EmptyState/StatusBadge React.memo;(4.2) lesson-plan-editor/text-study-block/exercise-block/paper-context-menu 拆分 Zustand 细粒度 selector(避免整体订阅);(4.3) message-detail/message-list/grade-record-list/attendance-sheet 改 useOptimistic + useTransition 乐观更新;(4.4) question-data-table/exam-data-table 接入 @tanstack/react-virtual 列表虚拟化;(4.5) sidebar-provider resize 改 debounce + useMemo;(4.6) 12 个 recharts 图表组件 margin props 提取到模块级 CHART_MARGIN 常量避免 inline 重建(parent/grades/dashboard/homework 12 文件);(4.7) scan-image-viewer aspect-ratio;(4.8) AiAssistantWidget inner 拆分 + 动态 import;(4.9) layout.tsx metadata 增强(metadataBase/openGraph/twitter/robots/authors/creator)+ Promise.all 并行获取 locale/messages/session。架构决策:(1) 动态 import 标准模式 xxx-inner.tsx + lazy wrapper;(2) 请求级去重 > 跨请求 cache(React cache() not unstable_cache);(3) 批量 SQL > 循环 SQL(INSERT batch + UPDATE CASE WHEN);(4) React 19 useOptimistic 替代手动 isPending;(5) 细粒度 Zustand selector > useShallow 多字段 selector;(6) recharts 具名导入 + optimizePackageImports 替代 barrel 导入。未完成项:React Compiler(babel-plugin-react-compiler RC 状态暂不引入)、force-dynamic 96 处评估(待后续专项)、Playwright 性能断言(待 e2e 基础设施完善)。验证:npx tsc --noEmit 零新增错误;npm run lint 零新增错误。| V5 状态管理统一专项 Phase 4 完成(2026-07-07 补漏重执行批量替换):Phase 4-2 sonner→notify 批量替换重新执行:(1) 修复 PowerShell 脚本语法错误(单引号数组字面量转义问题);(2) 执行 scripts/replace-sonner-v2.ps1 替换 131 个业务文件(含 import 语句 + toast.X(...) → notify.X(...) 方法调用),覆盖 exams/grades/lesson-preparation/settings/classes/scheduling/textbooks/ai/announcements/attendance/audit/auth/classes/course-plans/diagnostic/elective/error-book/files/homework/invitation-codes/leave-requests/messaging/notifications/onboarding/parent/questions/school/student/users 共 28 个模块 + shared/hooks + app/(dashboard) 路由;(3) paper-context-menu.tsx 手动重构:移除 `const { toast } = await import('sonner')` 和 `void import('sonner').then(...)` 动态 import 模式,改为静态 `import { notify } from '@/shared/lib/notify'` + `notify.success/error/info(...)`;(4) action-utils.ts 和 resolve-action-error.ts 注释示例同步更新为 notify。验证:仅 notify.ts(实现层)+ sonner.tsx(Toaster 组件)保留 `from 'sonner'` import;tsc --noEmit 0 errors;npm run lint 0 errors 0 warnings。| 数据库访问层审计专项 v1 完成(2026-07-07):纯深读审计 86 个 data-access*.ts + 15 个 actions.ts,输出 230 条问题(17 P0/48 P1/105 P2/60 P3)覆盖 4 维度 38 条规则(P/F/A/S 系列)。报告:docs/architecture/audit/data-access-audit-v1.md + data-access-audit-v1-data.json + 5 个分组 JSON(g1-g5)。5 阶段治理路线图:Phase 0 紧急安全(6 项 P0)→ 1 P0 治理 → 2 P1 性能+结构 → 3 P2 模式标准化 → 4 P3 风格优化。本次同步 005 文档:(1) permissions 补记 AUDIT_LOG_PURGE / AUDIT_RETENTION_MANAGE 两个审计建议新增权限点;(2) parent 模块添加 auditNotes 标注缺失 actions.ts 异常;(3) onboarding dependencyMatrix 添加 auditNote 标注 actions.ts 直查 DB 违规(A-09);(4) messaging 模块添加 auditNotes 标注 data-access.ts 976 行接近 1000 硬限,待治理阶段拆分;(5) shared 模块清单添加 auditNote 标注 date-utils.ts 待治理阶段新增。所有标注均为审计发现的事实记录,治理阶段实施后需更新对应 exports/permissions/dependencyMatrix。| 审计 v1 Phase 0 P0 修复完成(2026-07-07):6 项 P0 全部落地,tsc + lint 0 errors。本次同步 005 文档:(1) rolePermissions.admin 追加 AUDIT_LOG_PURGE / AUDIT_RETENTION_MANAGE;(2) _permissionsAuditNote 改为「已实施」状态;(3) modules.audit.exports.actions 中 purgeAuditLogsAction 权限改为 AUDIT_LOG_PURGE、saveAuditRetentionConfigAction 改为 AUDIT_RETENTION_MANAGE;(4) modules.onboarding.exports.dataAccess 新增 getUserOnboardedAtRaw / getUserOnboardedAt(cacheFn 包装) / markUserOnboarded 三个函数;onboarding dependencyMatrix.auditNote 改为「已实施」状态;(5) modules.parent.exports 新增 actions 数组(6 个 Server Action:getChildrenAction / getChildBasicInfoAction / getChildDashboardDataAction / getParentDashboardDataAction / getChildNameListAction / verifyParentChildRelationAction);parent.auditNotes 改为「已实施」状态;(6) modules.messaging.auditNotes 改为「已实施」状态,记录 5 个子文件拆分详情;(7) modules.shared.exports.functions 新增 serializeDate / serializeDateRequired(lib/date-utils.ts,S-04 DRY 修复,10+ data-access 文件迁移至此共享 util)。", "lastUpdated": "2026-07-07" }, + "_loggingRefactorSync": { + "syncedAt": "2026-07-07", + "scope": "日志系统重构专项架构图同步:(1) shared.exports.files 新增 lib/logger.ts(logger/createModuleLogger/Logger type,基于 pino + AsyncLocalStorage mixin)、lib/request-context.ts(requestContextStorage/getRequestContext/RequestContext type,基于 node:async_hooks)、lib/with-request-context.ts(withRequestContext HOF,注入 requestId);(2) shared.exports.hooks 新增 useErrorReport(客户端错误上报 Hook,sendBeacon→/api/client-error,sessionStorage 1 分钟节流);(3) apiRoutes 新增 /api/client-error(POST,createModuleLogger('client-error') 写入 pino);(4) dependencyMatrix 新增 5 个文件级依赖节点(shared/lib/logger→request-context+@/env.mjs+pino、shared/lib/request-context→node:async_hooks、shared/lib/with-request-context→request-context+next/headers+node:crypto、shared/hooks/use-error-report→无服务端依赖、src/app/api/client-error/route.ts→shared/lib/logger)" + }, "architectureOverview": { "layers": [ { @@ -141,8 +145,11 @@ "LESSON_PLAN_SUBSTITUTE_MANAGE": "lesson_plan:substitute_manage", "STANDARD_READ": "standard:read", "STANDARD_MANAGE": "standard:manage", - "STANDARD_LINK": "standard:link" + "STANDARD_LINK": "standard:link", + "AUDIT_LOG_PURGE": "audit_log:purge", + "AUDIT_RETENTION_MANAGE": "audit_retention:manage" }, + "_permissionsAuditNote": "✅ 审计 v1 Phase 0 P0-3 已实施(2026-07-07):(1) AUDIT_LOG_PURGE / audit_log:purge —— 审计日志清理操作权限,已新增至 permissions 常量、ROLE_PERMISSIONS_SEED admin 角色、permission-bitmap.ts 注册、permission-catalog.ts audit 分组、i18n rbac.json(zh-CN/en)翻译键;purgeAuditLogsAction 改用此权限点(原 AUDIT_LOG_READ 提权漏洞已修复,审计 ID: G4-003,规则 A-04)。(2) AUDIT_RETENTION_MANAGE / audit_retention:manage —— 审计日志保留策略管理权限,已全链路同步;saveAuditRetentionConfigAction 改用此权限点。两个权限点均已添加至 rolePermissions.admin 数组。", "rolePermissions": { "admin": [ "EXAM_CREATE", @@ -202,7 +209,9 @@ "ADAPTIVE_PRACTICE_READ", "LEAVE_REQUEST_CREATE", "LEAVE_REQUEST_READ", - "LEAVE_REQUEST_REVIEW" + "LEAVE_REQUEST_REVIEW", + "AUDIT_LOG_PURGE", + "AUDIT_RETENTION_MANAGE" ], "teacher": [ "EXAM_CREATE", @@ -448,6 +457,52 @@ "dashboard/components/teacher-dashboard/teacher-dashboard-header.tsx" ] }, + { + "name": "serializeDate", + "file": "lib/date-utils.ts", + "signature": "serializeDate(date: Date | null | undefined): string | null", + "params": { + "date": "Date 对象或 null/undefined" + }, + "purpose": "✅ 审计 v1 P0-6 新增(S-04 DRY 修复):日期序列化工具,将 Date 转为 ISO string,null/undefined 透传返回 null。从 messaging / course-plans / announcements / elective / audit / files / school / notifications 等 10+ data-access 文件中重复定义的 `const toIso = (d: Date) => d.toISOString()` 抽取", + "deps": [], + "usedBy": [ + "messaging/data-access-core.ts", + "messaging/data-access-templates.ts", + "course-plans/data-access.ts", + "announcements/data-access.ts", + "elective/data-access.ts", + "elective/data-access-selections.ts", + "audit/data-access.ts", + "files/data-access.ts", + "school/data-access.ts", + "notifications/preferences.ts", + "notifications/data-access.ts" + ] + }, + { + "name": "serializeDateRequired", + "file": "lib/date-utils.ts", + "signature": "serializeDateRequired(date: Date): string", + "params": { + "date": "Date 对象(必填)" + }, + "purpose": "✅ 审计 v1 P0-6 新增(S-04 DRY 修复):日期序列化工具(必填版本),将 Date 转为 ISO string,无 null 透传。用于已知非空 Date 字段的序列化场景", + "deps": [], + "usedBy": [ + "messaging/data-access-core.ts", + "messaging/data-access-templates.ts", + "course-plans/data-access.ts", + "announcements/data-access.ts", + "elective/data-access.ts", + "elective/data-access-selections.ts", + "audit/data-access.ts", + "files/data-access.ts", + "school/data-access.ts", + "notifications/preferences.ts", + "notifications/data-access.ts" + ] + }, { "name": "getParam", "file": "lib/search-params.ts", @@ -1664,6 +1719,16 @@ "usedBy": [ "待迁移:users/user-import-dialog.tsx、textbooks/textbook-form-dialog.tsx、error-book/error-book-detail-dialog.tsx 等 30+ 处 Dialog 组件" ] + }, + { + "name": "useErrorReport", + "file": "hooks/use-error-report.ts", + "signature": "useErrorReport(error: (Error & { digest?: string }) | null): void", + "purpose": "日志系统重构新增:客户端错误上报 Hook(\"use client\")。在 error.tsx 中使用,将客户端错误通过 navigator.sendBeacon 上报到 /api/client-error 端点,服务端经 createModuleLogger('client-error') 写入 pino 日志流。节流策略:同一 error digest 在 1 分钟内只上报一次(基于 sessionStorage),sendBeacon 不可用时降级 fetch keepalive,上报失败静默处理不影响用户体验", + "deps": [], + "usedBy": [ + "app/**/error.tsx(各路由错误边界)" + ] } ], "stores": [ @@ -2325,6 +2390,56 @@ "api/ai/chat/stream", "api/notifications/stream" ] + }, + { + "path": "lib/logger.ts", + "description": "日志系统重构新增:基于 pino 的全局 logger 与模块级子 logger 工厂。生产环境 JSON 输出到 stdout(docker logs 友好),开发环境 pino-pretty 彩色文本。通过 mixin 自动从 AsyncLocalStorage 混入 requestId/userId(若存在)。createModuleLogger(module) 返回 logger.child({ module }) 绑定 module 字段。LOG_LEVEL 由 @/env.mjs 注入。", + "exports": [ + "logger", + "createModuleLogger", + "Logger (type)" + ], + "deps": [ + "pino", + "@/env.mjs", + "shared/lib/request-context.getRequestContext" + ], + "usedBy": [ + "app/api/client-error/route.ts", + "其他服务端模块(按需接入)" + ] + }, + { + "path": "lib/request-context.ts", + "description": "日志系统重构新增:基于 node:async_hooks AsyncLocalStorage 的请求上下文存储。仅在 Node.js Runtime 可用(proxy.ts 是 Edge Runtime,不导入此模块)。RequestContext 含 requestId/userId?/module?,通过 withRequestContext 高阶函数注入。getRequestContext() 在调用栈外返回空对象,pino logger.mixin 自动调用此函数混入 requestId。", + "exports": [ + "requestContextStorage", + "getRequestContext", + "RequestContext (type)" + ], + "deps": [ + "node:async_hooks" + ], + "usedBy": [ + "shared/lib/logger.ts", + "shared/lib/with-request-context.ts" + ] + }, + { + "path": "lib/with-request-context.ts", + "description": "日志系统重构新增:HOF 包装 Server Action / Route Handler,注入 requestId 到 AsyncLocalStorage。工作流程:(1) 通过 next/headers() 读取 proxy.ts 注入的 x-request-id;(2) 若无则 node:crypto.randomUUID() 生成;(3) requestContextStorage.run() 注入上下文;(4) 调用栈内所有 logger 调用自动获得 requestId。", + "exports": [ + "withRequestContext" + ], + "deps": [ + "shared/lib/request-context.requestContextStorage", + "shared/lib/request-context.RequestContext (type)", + "next/headers", + "node:crypto" + ], + "usedBy": [ + "Server Actions / Route Handlers(按需包装)" + ] } ], "types": [ @@ -3368,6 +3483,9 @@ "description": "设计令牌专项重构(2026-07-04):Primitive + Semantic 双层架构,HEX→HSL,明暗双份,@theme inline 暴露为 Tailwind 类。详见 docs/architecture/004_architecture_impact_map.md 1.1.2 章节" } }, + "sharedAuditNotes": [ + "⚠️ 数据库访问层审计 v1(2026-07-07)建议新增文件:src/shared/lib/date-utils.ts。当前多个 data-access 文件(如 attendance/data-access.ts 的 serializeDate helper、classes/data-access-class-schedule.ts 等)存在重复的 Date→ISO string 序列化逻辑,违反 DRY 原则(规则 S-04)。治理阶段(Phase 3 模式标准化)需抽取统一的日期工具函数到 shared/lib/date-utils.ts,导出 serializeDate(date: Date | null): string | null、serializeDateArray(items: T[], dateField: keyof T): T[] 等纯函数。新增后需在本节点 exports.functions 中补记,并在 dependencyMatrix.shared 中更新 lib.date-utils 依赖引用。当前 shared/lib/ 实际文件清单(截至 2026-07-07):a11y/action-utils/ai/api-response/audit-logger/auth-guard/bcrypt-utils/breached-password/change-logger/create-service-provider/download/errors/excel/export-utils/file-storage/http-utils/logger/login-logger/notify/password-policy/password-security-service/permission-bitmap/permissions/query-client/query-keys/question-content/redis-client/request-context/resolve-action-error/role-utils/route-permissions/route-resolver/search-params/session/sse/storage-provider/track-event/type-guards/upstash-modules/utils/with-request-context(含 ai/cache/rate-limit 三个子目录),无 date-utils.ts。" + ], "auth": { "path": "src/auth.ts", "description": "用户认证:NextAuth配置(handlers/auth/signIn/signOut)、JWT/Session callbacks、events回调(登录日志)。集成密码安全策略(账户锁定、失败登录追踪)和登录速率限制。P1-3 拆分后,辅助函数已迁移至 shared/lib/{role-utils,bcrypt-utils,http-utils,password-security-service}。audit-P0 重构(2026-06-25):authorize 中的 2FA 校验改用同模块 modules/auth/services/two-factor-service(不再跨模块依赖 modules/settings/actions-security);jwt/session/signOut 中 3 处 `as` 断言通过 next-auth.d.ts 扩展 User 接口 + `\"token\" in message` 类型收窄消除", @@ -3445,7 +3563,7 @@ "middleware": { "file": "src/proxy.ts", "signature": "proxy(request: NextRequest): Promise", - "purpose": "基于权限点的路由守卫(Next.js 16 将 middleware 重命名为 proxy)。audit-P1-7:使用 hasPermissionInBitmap() 直接检查 token.permissionsBitmap 位图,不再解码完整权限数组(边缘运行时每请求都经过,性能优化)。audit-P1-10 + audit-P1-12:4 个权限映射常量抽取至 shared/lib/route-permissions.ts 配置文件(配置驱动设计),新增路由权限只需修改配置文件无需改 proxy.ts 逻辑;SPECIFIC_ROUTE_PERMISSIONS 扩展至 15 个条目覆盖 /admin/* 子路径细粒度权限", + "purpose": "基于权限点的路由守卫(Next.js 16 将 middleware 重命名为 proxy)。audit-P1-7:使用 hasPermissionInBitmap() 直接检查 token.permissionsBitmap 位图,不再解码完整权限数组(边缘运行时每请求都经过,性能优化)。audit-P1-10 + audit-P1-12:4 个权限映射常量抽取至 shared/lib/route-permissions.ts 配置文件(配置驱动设计),新增路由权限只需修改配置文件无需改 proxy.ts 逻辑;SPECIFIC_ROUTE_PERMISSIONS 扩展至 15 个条目覆盖 /admin/* 子路径细粒度权限。logging 重构 Task 5:注入 x-request-id(Web Crypto API crypto.randomUUID,Edge Runtime 兼容),通过 injectRequestId() 内部助手将请求头转发至下游 RSC,最终 response.headers.set('x-request-id', requestId) 暴露给客户端;不导入 node:async_hooks/request-context.ts(Edge Runtime 限制)", "permissionConfigSource": "shared/lib/route-permissions.ts (audit-P1-12 抽取)", "specificRoutePermissions": { "/admin/ai-settings": "ai:chat", @@ -4679,6 +4797,30 @@ ] } ], + "lib": [ + { + "name": "isRecord / isSimpleTextContent / isQuestionContentObj / isJSONContent / isJSONContentArray / toSortableId / isQuestionBlockAttrs", + "file": "lib/type-guards.ts", + "type": "type-guard-functions", + "purpose": "✅ 类型守卫专项重构(2026-07-07)新增:集中存放 exams 模块的类型守卫,消除 ai-pipeline/components/editor 子目录内 13 个文件中的违规 as 断言。isRecord 收窄 unknown → Record;isSimpleTextContent / isQuestionContentObj 用于题目内容解析;isJSONContent / isJSONContentArray 用于 Tiptap JSONContent;toSortableId 将 dnd-kit UniqueIdentifier(string|number)安全转字符串;isQuestionBlockAttrs 校验 QuestionBlock 节点 attrs 结构。合规 as:从 unknown 转换(as unknown as)与字面量常量(as const)保留未改。", + "deps": [ + "@tiptap/react.JSONContent" + ], + "usedBy": [ + "exams/ai-pipeline/request.ts", + "exams/components/exam-preview-utils.ts", + "exams/components/exam-actions.tsx", + "exams/components/assembly/structure-editor.tsx", + "exams/components/assembly/question-bank-list.tsx", + "exams/components/assembly/exam-paper-preview.tsx", + "exams/editor/selection-toolbar.tsx", + "exams/editor/exam-nodes-to-editor-doc.ts", + "exams/editor/exam-rich-editor-inner.tsx", + "exams/editor/extensions/question-block.tsx", + "exams/components/exam-rich-form.tsx" + ] + } + ], "statsService": [ { "name": "getExamAnalytics", @@ -6977,6 +7119,26 @@ "components/graph-kp-node.tsx" ] }, + { + "name": "KpGraphNode", + "type": "interface", + "definition": "extends NodeObject { id: string, name: string, kp: KpWithRelations, mastery: MasteryInfo | null, val: number, chapterColor: string }", + "purpose": "v6 审计修复新增:力导向图谱节点(react-force-graph-2d NodeObject 业务扩展)。从 force-graph.tsx 提取至 types.ts,供 lib/type-guards.ts 类型守卫引用", + "usedBy": [ + "components/force-graph.tsx", + "lib/type-guards.isKpGraphNode" + ] + }, + { + "name": "KpGraphLink", + "type": "interface", + "definition": "extends LinkObject { source: string | KpGraphNode, target: string | KpGraphNode, edgeType: 'parent' | 'prerequisite' }", + "purpose": "v6 审计修复新增:力导向图谱边(react-force-graph-2d LinkObject 业务扩展)。从 force-graph.tsx 提取至 types.ts,供 lib/type-guards.ts 类型守卫引用", + "usedBy": [ + "components/force-graph.tsx", + "lib/type-guards.isKpGraphLink" + ] + }, { "name": "CreatePrerequisiteInput", "type": "type", @@ -7087,6 +7249,79 @@ "purpose": "知识图谱布局计算纯函数(Task 8:dagre 集成,输入 KpWithRelations[],输出 React Flow Node[]/Edge[] + 画布尺寸)" } ], + "lib": [ + { + "name": "isKpWithRelations", + "file": "lib/type-guards.ts", + "signature": "(data: unknown) => data is KpWithRelations", + "purpose": "v6 审计修复新增:判断未知值是否为合法 KpWithRelations。校验关键运行时字段(id/name/questionCount/prerequisiteIds),替代 components/graph-kp-node.tsx 中 `data.kp as KpWithRelations` 断言", + "deps": [], + "usedBy": [ + "components/graph-kp-node.tsx", + "lib/type-guards.isGraphNodeData", + "lib/type-guards.isGraphLayoutNodeData" + ] + }, + { + "name": "isGraphNodeData", + "file": "lib/type-guards.ts", + "signature": "(data: unknown) => data is GraphNodeData", + "purpose": "v6 审计修复新增:判断未知值是否为合法 GraphNodeData(React Flow 节点 data)。替代 components/graph-kp-node.tsx 中 `data.graphData as GraphNodeData | undefined` 断言", + "deps": [ + "lib/type-guards.isKpWithRelations" + ], + "usedBy": [ + "components/graph-kp-node.tsx" + ] + }, + { + "name": "isGraphLayoutNodeData", + "file": "lib/type-guards.ts", + "signature": "(data: unknown) => data is GraphLayoutNodeData", + "purpose": "v6 审计修复新增:判断未知值是否为合法 GraphLayoutNodeData(dagre 布局节点 data,带索引签名)。替代 components/knowledge-graph-node.tsx 中 `node.data as GraphLayoutNodeData` 断言", + "deps": [ + "lib/type-guards.isKpWithRelations", + "graph-layout.GraphLayoutNodeData" + ], + "usedBy": [ + "components/knowledge-graph-node.tsx" + ] + }, + { + "name": "isGraphEdgeData", + "file": "lib/type-guards.ts", + "signature": "(data: unknown) => data is GraphEdgeData", + "purpose": "v6 审计修复新增:判断未知值是否为合法 GraphEdgeData(React Flow 边 data)。替代 components/graph-prerequisite-edge.tsx 中 `data as unknown as GraphEdgeData | undefined` 双重断言", + "deps": [], + "usedBy": [ + "components/graph-prerequisite-edge.tsx" + ] + }, + { + "name": "isKpGraphNode", + "file": "lib/type-guards.ts", + "signature": "(node: NodeObject) => node is KpGraphNode", + "purpose": "v6 审计修复新增:判断 react-force-graph-2d NodeObject 是否为业务 KpGraphNode。校验 id/name/val/chapterColor/kp/mastery 字段,替代 components/force-graph.tsx 中 4 处 `node as KpGraphNode` 断言(nodeCanvasObject/handleNodeClick/handleNodeHover/linkCanvasObject source-target)", + "deps": [ + "types.KpGraphNode" + ], + "usedBy": [ + "components/force-graph.tsx" + ] + }, + { + "name": "isKpGraphLink", + "file": "lib/type-guards.ts", + "signature": "(link: LinkObject) => link is KpGraphLink", + "purpose": "v6 审计修复新增:判断 react-force-graph-2d LinkObject 是否为业务 KpGraphLink。校验 edgeType 字段(source/target 在力导向模拟过程中会被库内部从 string id 替换为节点对象引用,故不校验其具体类型),替代 components/force-graph.tsx 中 `link as KpGraphLink` 断言", + "deps": [ + "types.KpGraphLink" + ], + "usedBy": [ + "components/force-graph.tsx" + ] + } + ], "analytics": [ { "name": "TextbookAnalytics", @@ -10265,6 +10500,21 @@ ] } ], + "lib": [ + { + "name": "isRecord / isMessageBody / isFileUploadResult", + "file": "lib/type-guards.ts", + "type": "type-guard-functions", + "signature": "(value: unknown) => value is Record / MessageBody / FileUploadResult", + "purpose": "as 断言修复新增(2026-07-07):校验 /api/upload 的 HTTP 响应体结构,替代 components/avatar-upload.tsx 中的 `await response.json() as FileUploadResult` 断言。HTTP 响应不可信,必须做字段校验", + "deps": [ + "files/types.FileUploadResult" + ], + "usedBy": [ + "components/avatar-upload.tsx" + ] + } + ], "components": [ { "name": "AiProviderSettingsCard", @@ -11663,10 +11913,10 @@ }, { "name": "saveAuditRetentionConfigAction", - "permission": "AUDIT_LOG_READ", + "permission": "AUDIT_RETENTION_MANAGE", "signature": "(config: AuditRetentionConfig) => Promise>", "file": "actions.ts", - "purpose": "保存审计日志保留策略配置(✅ P2-5 新增,trackEvent 埋点)", + "purpose": "保存审计日志保留策略配置(✅ P2-5 新增,trackEvent 埋点;✅ 审计 v1 P0-3:权限点由 AUDIT_LOG_READ 提升为 AUDIT_RETENTION_MANAGE,修复读权限执行写操作的提权漏洞,规则 A-04)", "deps": [ "requirePermission", "retention.saveAuditRetentionConfig", @@ -11678,10 +11928,10 @@ }, { "name": "purgeAuditLogsAction", - "permission": "AUDIT_LOG_READ", + "permission": "AUDIT_LOG_PURGE", "signature": "(retentionDays?: number, loginLogRetentionDays?: number) => Promise>", "file": "actions.ts", - "purpose": "手动清理过期审计日志(✅ P2-5 新增,trackEvent 埋点;✅ audit-P2-5 新增 loginLogRetentionDays 参数支持 login_logs 独立保留期)", + "purpose": "手动清理过期审计日志(✅ P2-5 新增,trackEvent 埋点;✅ audit-P2-5 新增 loginLogRetentionDays 参数支持 login_logs 独立保留期;✅ 审计 v1 P0-3:权限点由 AUDIT_LOG_READ 提升为 AUDIT_LOG_PURGE,修复读权限执行物理删除的提权漏洞,规则 A-04)", "deps": [ "requirePermission", "retention.getAuditRetentionConfig", @@ -15458,6 +15708,98 @@ "path": "src/modules/parent", "description": "家长端仪表盘:聚合家长关联子女的学习数据(课表、作业、成绩、班级),支持多子女切换查看。家长通过 parentStudentRelations 表关联子女,DataScope 解析为 children 类型", "exports": { + "actions": [ + { + "name": "getChildrenAction", + "permission": null, + "signature": "() => Promise>", + "file": "actions.ts", + "purpose": "✅ 审计 v1 P0-4 新增:获取当前登录家长的子女列表。调用 requireAuth() 获取 parentId 后调用 data-access.getChildren,替代 app/ 页面直接 import data-access(规则 A-08 修复)", + "deps": [ + "shared.lib.auth-guard.requireAuth", + "data-access.getChildren" + ], + "usedBy": [ + "parent/children/[studentId]/page.tsx", + "parent/leave/page.tsx", + "parent/elective/page.tsx" + ] + }, + { + "name": "getChildBasicInfoAction", + "permission": null, + "signature": "(studentId: string) => Promise>", + "file": "actions.ts", + "purpose": "✅ 审计 v1 P0-4 新增:获取子女基本信息。内部调用 verifyParentChildRelation 做亲子关系校验,未通过返回 FORBIDDEN", + "deps": [ + "shared.lib.auth-guard.requireAuth", + "data-access.verifyParentChildRelation", + "data-access.getChildBasicInfo" + ], + "usedBy": [ + "parent/children/[studentId]/page.tsx" + ] + }, + { + "name": "getChildDashboardDataAction", + "permission": null, + "signature": "(studentId: string) => Promise>", + "file": "actions.ts", + "purpose": "✅ 审计 v1 P0-4 新增:获取子女仪表盘数据(聚合 课表/作业/成绩/考试)。内部调用 verifyParentChildRelation 做亲子关系校验", + "deps": [ + "shared.lib.auth-guard.requireAuth", + "data-access.verifyParentChildRelation", + "data-access.getChildDashboardData" + ], + "usedBy": [ + "parent/children/[studentId]/page.tsx" + ] + }, + { + "name": "getParentDashboardDataAction", + "permission": null, + "signature": "() => Promise>", + "file": "actions.ts", + "purpose": "✅ 审计 v1 P0-4 新增:获取家长仪表盘数据(聚合所有子女摘要)", + "deps": [ + "shared.lib.auth-guard.requireAuth", + "data-access.getParentDashboardData" + ], + "usedBy": [ + "parent/dashboard/page.tsx" + ] + }, + { + "name": "getChildNameListAction", + "permission": null, + "signature": "() => Promise>>", + "file": "actions.ts", + "purpose": "✅ 审计 v1 P0-4 新增:获取家长所有子女姓名列表(用于详情页头部多子女切换器,一次批量查询避免 N+1)", + "deps": [ + "shared.lib.auth-guard.requireAuth", + "data-access.getChildNameList" + ], + "usedBy": [ + "parent/children/[studentId]/page.tsx" + ] + }, + { + "name": "verifyParentChildRelationAction", + "permission": null, + "signature": "(studentId: string) => Promise>", + "file": "actions.ts", + "purpose": "✅ 审计 v1 P0-4 新增:校验当前家长与指定子女的亲子关系,返回 studentId 或 FORBIDDEN", + "deps": [ + "shared.lib.auth-guard.requireAuth", + "data-access.verifyParentChildRelation" + ], + "usedBy": [ + "parent/children/[studentId]/page.tsx", + "parent/leave/page.tsx", + "parent/elective/page.tsx" + ] + } + ], "dataAccess": [ { "name": "getChildren", @@ -15470,7 +15812,7 @@ ], "usedBy": [ "getParentDashboardData (内部)", - "parent/children/[studentId]/page.tsx" + "getChildrenAction" ] }, { @@ -15769,7 +16111,10 @@ ] } ] - } + }, + "auditNotes": [ + "✅ 审计 v1 Phase 0 P0-4 已实施(2026-07-07):原缺失 actions.ts 文件已新建(src/modules/parent/actions.ts),封装 6 个 Server Action(getChildrenAction / getChildBasicInfoAction / getChildDashboardDataAction / getParentDashboardDataAction / getChildNameListAction / verifyParentChildRelationAction)。每个 Action 调用 requireAuth() + 内部 verifyParentChildRelation 亲子关系校验。3 个页面(children/[studentId]/page.tsx / leave/page.tsx / elective/page.tsx)改为调用对应 Server Action,不再直接 import data-access(规则 A-08 修复,审计 ID: G4-002)。" + ] }, "messaging": { "path": "src/modules/messaging", @@ -16925,7 +17270,10 @@ ] } ] - } + }, + "auditNotes": [ + "✅ 审计 v1 Phase 0 P0-5 已实施(2026-07-07):原 data-access.ts 单文件 1089 行超 1000 行硬限(审计 ID: G4-001,规则 S-01),混合 CRUD/批量/群组/模板/举报多职责。已拆分为 5 个子文件:(1) data-access-core.ts —— helpers(resolveUserNames / toIso via shared util)+ 核心 CRUD(getMessages/getMessageById/getMessageThread/createMessage/markMessageAsRead/deleteMessage/toggleMessageStar/recallMessage/getUnreadMessageCount/getRecipients/isReceiverAllowed/getMessageDetailPageData);(2) data-access-bulk.ts —— 批量操作(bulkMarkMessagesAsRead/bulkDeleteMessages/bulkToggleMessagesStar);(3) data-access-group.ts —— 群组消息 fan-out(sendGroupMessage + SendGroupMessageInput/SendGroupMessageResult 接口);(4) data-access-templates.ts —— 草稿 + 模板 CRUD(getMessageDrafts/createMessageDraft/updateMessageDraft/deleteMessageDraft/getMessageDraftById + getMessageTemplates/createMessageTemplate/updateMessageTemplate/deleteMessageTemplate);(5) data-access-reports.ts —— 举报 + 屏蔽 + 附件(reportMessage/hasUserReportedMessage/blockUser/unblockUser/isUserBlocked/isEitherUserBlocked/getBlockedUserIds/getMessageReports/getUserBlocks/getMessageAttachments + MESSAGE_ATTACHMENT_TARGET_TYPE)。原 data-access.ts 改为 barrel 文件 `export * from \"./data-access-*\"` 保持向后兼容。同时迁移至 shared/lib/date-utils.ts 的 serializeDate 统一日期序列化(规则 S-04 DRY)。exports.dataAccess 节点保持原文件名 data-access.ts 不变以兼容现有 import 路径,实际实现已分散到 5 个子文件。" + ] }, "notifications": { "path": "src/modules/notifications", @@ -17464,6 +17812,19 @@ ] } ], + "lib": [ + { + "name": "isStringRecord", + "file": "lib/type-guards.ts", + "type": "type-guard-function", + "signature": "(value: unknown) => value is Record", + "purpose": "as 断言修复新增(2026-07-07):校验 metadata.wechatKeywords 是否为 string→unknown 的记录对象,替代 channels/wechat-channel.ts 中的 `metadata.wechatKeywords as Record` 断言", + "deps": [], + "usedBy": [ + "channels/wechat-channel.ts" + ] + } + ], "components": [ { "name": "NotificationList", @@ -18253,6 +18614,13 @@ ] } ], + "lib": [ + { + "name": "type-guards.isAttendancePeriod", + "signature": "(value: unknown) => value is AttendancePeriod", + "notes": "类型守卫,替代 as AttendancePeriod 断言" + } + ], "services": [ { "name": "AttendanceReadService", @@ -18581,6 +18949,28 @@ ] } ], + "lib": [ + { + "name": "type-guards.isLeaveType", + "signature": "(value: unknown) => value is LeaveType", + "notes": "类型守卫,替代 as LeaveType 断言" + }, + { + "name": "type-guards.toLeaveType", + "signature": "(value: unknown, fallback?) => LeaveType", + "notes": "安全转换,非法值回退到 fallback" + }, + { + "name": "type-guards.isLeaveStatus", + "signature": "(value: unknown) => value is LeaveStatus", + "notes": "类型守卫,替代 as LeaveStatus 断言" + }, + { + "name": "type-guards.toLeaveStatus", + "signature": "(value: unknown, fallback?) => LeaveStatus", + "notes": "安全转换,非法值回退到 fallback" + } + ], "components": [ { "name": "LeaveRequestForm", @@ -22313,6 +22703,18 @@ "file": "actions.ts", "purpose": "取消课案与课标的关联" } + ], + "lib": [ + { + "name": "type-guards.isStandardLevel", + "signature": "(value: unknown) => value is StandardLevel", + "notes": "类型守卫,替代 as StandardLevel 断言" + }, + { + "name": "type-guards.toStandardLevel", + "signature": "(value: unknown, fallback?) => StandardLevel", + "notes": "安全转换,非法值回退到 fallback" + } ] }, "permissions": [ @@ -23003,6 +23405,21 @@ "name": "bindParentToChild", "file": "data-access.ts", "purpose": "家长绑定子女(v3 三因子验证:邮箱+生日+手机号后4位,对标 PowerSchool Access ID+Password)" + }, + { + "name": "getUserOnboardedAtRaw", + "file": "data-access.ts", + "purpose": "✅ 审计 v1 P0-1/P0-2 新增:读取 users.onboardedAt 原始函数(未缓存),供 actions 层调用替代直查 DB(规则 A-09 修复)" + }, + { + "name": "getUserOnboardedAt", + "file": "data-access.ts", + "purpose": "✅ 审计 v1 P0-2 新增:getUserOnboardedAtRaw 的 cacheFn 包装版本,请求级 memoization" + }, + { + "name": "markUserOnboarded", + "file": "data-access.ts", + "purpose": "✅ 审计 v1 P0-2 新增:标记用户 onboarding 完成(写 users.onboardedAt),供 completeOnboardingAction 调用替代 db.transaction + tx.update 直查" } ], "schema": [ @@ -23176,6 +23593,16 @@ "name": "generateBatchId", "signature": "() => string", "notes": "batch__" + }, + { + "name": "type-guards.isInvitationRole", + "signature": "(value: unknown) => value is InvitationRole", + "notes": "类型守卫,替代 as InvitationRole 断言" + }, + { + "name": "type-guards.toInvitationRole", + "signature": "(value: unknown, fallback?) => InvitationRole", + "notes": "安全转换,非法值回退到 fallback" } ], "schema": [ @@ -24334,7 +24761,8 @@ "data-access.enrollStudentByInvitationCode", "data-access.enrollTeacherByInvitationCode" ] - } + }, + "auditNote": "✅ 审计 v1 Phase 0 P0-1/P0-2 已实施(2026-07-07):原 actions.ts L15-L16 直接 import { db } from '@/shared/db' 与 import { users } from '@/shared/db/schema',并在 completeOnboardingAction 内 L72-L76 直接执行 db.select({ onboardedAt: users.onboardedAt }).from(users).where(eq(users.id, userId)).limit(1)(违反三层架构规则 A-09,审计 ID: G5-002);同时 data-access.ts L1 缺少 import 'server-only' 文件头(P-01,审计 ID: G5-001)。修复方案已落地:(1) data-access.ts 文件头补 import 'server-only';(2) data-access.ts 新增 getUserOnboardedAtRaw / getUserOnboardedAt(cacheFn 包装) / markUserOnboarded 三个函数;(3) actions.ts 移除 db.select / db.transaction / tx.update 直查与 eq / db / users schema 导入,改为调用 data-access 函数。" }, "modules/invitation-codes": { "dependsOn": [ @@ -24813,6 +25241,63 @@ "dependsOn": [ "shared" ] + }, + "shared/lib/logger": { + "dependsOn": [ + "shared/lib/request-context", + "@/env.mjs", + "pino" + ], + "uses": { + "shared/lib/request-context": [ + "getRequestContext" + ], + "external": [ + "pino", + "@/env.mjs (env.LOG_LEVEL, env.NODE_ENV)" + ] + } + }, + "shared/lib/request-context": { + "dependsOn": [], + "uses": { + "external": [ + "node:async_hooks (AsyncLocalStorage)" + ] + }, + "notes": "仅在 Node.js Runtime 可用;proxy.ts 是 Edge Runtime,不导入此模块" + }, + "shared/lib/with-request-context": { + "dependsOn": [ + "shared/lib/request-context", + "next/headers", + "node:crypto" + ], + "uses": { + "shared/lib/request-context": [ + "requestContextStorage", + "RequestContext (type)" + ], + "external": [ + "next/headers (headers())", + "node:crypto (randomUUID)" + ] + } + }, + "shared/hooks/use-error-report": { + "dependsOn": [], + "uses": {}, + "notes": "客户端 Hook(\"use client\"),无服务端依赖。通过 navigator.sendBeacon / fetch keepalive 上报到 /api/client-error 端点" + }, + "src/app/api/client-error/route.ts": { + "dependsOn": [ + "shared/lib/logger" + ], + "uses": { + "shared/lib/logger": [ + "createModuleLogger" + ] + } } }, "moduleDependencyGraph": { @@ -27116,6 +27601,22 @@ "description": "接收 Excel 文件,调用 parseExcel 返回 sheets 预览数据(实际导入由 users/actions.importUsersAction 完成)", "errorHandling": "ValidationError + apiError;修复原 401→403(权限不足应为 403 而非 401)", "envelope": "✅ 2026-07-04 API 规范化重构:统一响应信封 { success, message?, errorCode?, data? }" + }, + "/api/client-error": { + "methods": [ + "POST" + ], + "handler": "src/app/api/client-error/route.ts (POST)", + "auth": "public", + "module": "shared", + "file": "src/app/api/client-error/route.ts", + "description": "日志系统重构新增:客户端错误上报端点,接收 error.tsx 通过 useErrorReport Hook 上报的错误,用 createModuleLogger('client-error') 记录到 pino 日志流,统一与 server-side 错误一起排查。请求体 { message, stack?, digest?, path, userAgent, timestamp }。JSON 解析失败返回 400,成功记录后返回 { ok: true }", + "deps": [ + "shared/lib/logger.createModuleLogger" + ], + "usedBy": [ + "shared/hooks/use-error-report.ts" + ] } }, "devops": { diff --git a/docs/troubleshooting/known-issues.md b/docs/troubleshooting/known-issues.md index 574156e..eeb5e3d 100644 --- a/docs/troubleshooting/known-issues.md +++ b/docs/troubleshooting/known-issues.md @@ -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` | `interface NodeContentUpdate { data: Record }`(诚实反映 Zod `z.record(z.string(), z.unknown())` 校验后的 untrusted 输出,强制调用方走 `mergeBlockDataPatch`) | `interface NodeContentUpdate { data: Partial }`(伪装成可信类型,调用方直接 `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>`(next/dynamic 返回 `ComponentType` 与库默认导出类型不兼容,需 `as unknown as` 保留完整 props 类型) | 无(库类型限制无法用守卫替代) | +| DB JSON `unknown` 字段收窄用 `isRecord` | `if (!isRecord(value)) return value; const record = value` | `const record = value as Record` | +| 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` | `t(key, params as Record \| undefined)` | +| react-hook-form + zodResolver 协变差异保留 `as unknown as` | `zodResolver(formSchema) as unknown as Resolver`(库协变差异需 `as unknown as`,注释说明原因) | `zodResolver(formSchema) as Resolver`(直接 `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` 改用 `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 = {}` | `{} as Record` | +| Set.has(value) 需改 Set | `const SET = new Set([...]); 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>` | `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 分钟内不重复上报 | 每次渲染都上报 | + + +