From 89b9e181d2c5d2b3a250467d86b0283f46de2cf3 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Fri, 3 Jul 2026 10:23:34 +0800 Subject: [PATCH] docs(audit): add audit reports for grades, homework, lesson-preparation, messaging, permissions, question-bank, settings, textbooks - Add grades-audit-report - Add homework-audit-report and homework-exams-audit-report - Add lesson-preparation-audit-report-v3 and v4 - Add messaging-audit-report - Add permissions-audit-report - Add question-bank-audit-report - Add settings-profile-audit-report-v3 - Add textbooks-audit-report-v3 --- .../audit/adaptive-practice-audit-report.md | 815 ++++++++++++ docs/architecture/audit/ai-audit-report.md | 382 ++++++ .../audit/ai-module-deep-audit-report.md | 390 ++++++ .../audit/announcements-audit-report.md | 392 ++++++ .../announcements-messages-audit-report-v3.md | 251 ++++ .../audit/attendance-audit-report.md | 443 +++++++ .../audit/audit-module-audit-report-v2.md | 436 +++++++ .../audit/audit-module-audit-report.md | 436 +++++++ docs/architecture/audit/auth-audit-report.md | 1094 +++++++++++++++++ .../audit/classes-audit-report.md | 440 +++++++ .../audit/course-plans-audit-report.md | 262 ++++ .../audit/dashboard-audit-report-v4.md | 320 +++++ .../audit/diagnostic-audit-report-v2.md | 395 ++++++ .../audit/diagnostic-audit-report.md | 301 +++++ .../audit/elective-audit-report.md | 339 +++++ .../audit/error-book-audit-report.md | 274 +++++ docs/architecture/audit/exams-audit-report.md | 475 +++++++ docs/architecture/audit/files-audit-report.md | 557 +++++++++ .../architecture/audit/grades-audit-report.md | 457 +++++++ .../audit/homework-audit-report.md | 348 ++++++ .../audit/homework-exams-audit-report.md | 464 +++++++ .../lesson-preparation-audit-report-v3.md | 324 +++++ .../lesson-preparation-audit-report-v4.md | 704 +++++++++++ .../audit/messaging-audit-report.md | 648 ++++++++++ .../audit/permissions-audit-report.md | 900 ++++++++++++++ .../audit/question-bank-audit-report.md | 316 +++++ .../audit/school-grade-class-audit-report.md | 416 +++---- .../audit/settings-profile-audit-report-v3.md | 251 ++++ .../audit/textbooks-audit-report-v3.md | 409 ++++++ 29 files changed, 13009 insertions(+), 230 deletions(-) create mode 100644 docs/architecture/audit/adaptive-practice-audit-report.md create mode 100644 docs/architecture/audit/ai-audit-report.md create mode 100644 docs/architecture/audit/ai-module-deep-audit-report.md create mode 100644 docs/architecture/audit/announcements-audit-report.md create mode 100644 docs/architecture/audit/announcements-messages-audit-report-v3.md create mode 100644 docs/architecture/audit/attendance-audit-report.md create mode 100644 docs/architecture/audit/audit-module-audit-report-v2.md create mode 100644 docs/architecture/audit/audit-module-audit-report.md create mode 100644 docs/architecture/audit/auth-audit-report.md create mode 100644 docs/architecture/audit/classes-audit-report.md create mode 100644 docs/architecture/audit/course-plans-audit-report.md create mode 100644 docs/architecture/audit/dashboard-audit-report-v4.md create mode 100644 docs/architecture/audit/diagnostic-audit-report-v2.md create mode 100644 docs/architecture/audit/diagnostic-audit-report.md create mode 100644 docs/architecture/audit/elective-audit-report.md create mode 100644 docs/architecture/audit/error-book-audit-report.md create mode 100644 docs/architecture/audit/exams-audit-report.md create mode 100644 docs/architecture/audit/files-audit-report.md create mode 100644 docs/architecture/audit/grades-audit-report.md create mode 100644 docs/architecture/audit/homework-audit-report.md create mode 100644 docs/architecture/audit/homework-exams-audit-report.md create mode 100644 docs/architecture/audit/lesson-preparation-audit-report-v3.md create mode 100644 docs/architecture/audit/lesson-preparation-audit-report-v4.md create mode 100644 docs/architecture/audit/messaging-audit-report.md create mode 100644 docs/architecture/audit/permissions-audit-report.md create mode 100644 docs/architecture/audit/question-bank-audit-report.md create mode 100644 docs/architecture/audit/settings-profile-audit-report-v3.md create mode 100644 docs/architecture/audit/textbooks-audit-report-v3.md diff --git a/docs/architecture/audit/adaptive-practice-audit-report.md b/docs/architecture/audit/adaptive-practice-audit-report.md new file mode 100644 index 0000000..f289516 --- /dev/null +++ b/docs/architecture/audit/adaptive-practice-audit-report.md @@ -0,0 +1,815 @@ +# 专项练习(adaptive-practice)模块审计报告 + +> **审计日期**:2026-06-25 +> **审计范围**:`src/modules/adaptive-practice/` 全部文件 + `src/app/(dashboard)/{student,teacher,management/grade}/practice/` 路由 + 跨模块集成点(error-book) +> **对标系统**:Khan Academy、IXL Learning、DreamBox、Smartick、学而思网校、猿辅导、作业帮、超星学习通、ClassIn、智学网 +> **前置文档**:[004 架构影响地图](../004_architecture_impact_map.md#230-adaptive-practice专项练习模块-核心教学链路闭环)、[005 架构数据](../005_architecture_data.json) + +--- + +## 一、现有实现概要 + +### 1.1 文件分布与代码量 + +| 文件 | 行数 | 职责 | 规范符合性 | +|------|------|------|-----------| +| [types.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/types.ts) | 143 | 联合类型定义(PracticeType/PracticeSourceMeta 等) | ✅ | +| [schema.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/schema.ts) | 74 | Zod 输入验证 | ✅ | +| [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) | 616 | 学生端 CRUD + 自动判分 | ✅ ≤800 | +| [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) | 343 | 四种出题策略 | ✅ | +| [data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-analytics.ts) | 634 | 教师/年级宏观数据分析 | ⚠️ 接近 800,建议拆分 | +| [actions.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/actions.ts) | 267 | 7 个 Server Actions | ✅ | +| [components/practice-starter.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-starter.tsx) | 263 | 练习发起器 | ✅ | +| [components/practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) | 550 | 答题界面(含 QuestionCard/AnswerInput/AnswerResult/PracticeResultView) | ⚠️ 超 500,需拆分 | +| [components/practice-history.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-history.tsx) | 95 | 练习历史列表 | ✅ | +| [components/practice-stats-cards.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-stats-cards.tsx) | 73 | 学生端统计卡片 | ✅ | +| [components/practice-overview-stats-cards.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-overview-stats-cards.tsx) | 99 | 教师/年级统计卡片 | ✅ | +| [components/class-practice-comparison-table.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/class-practice-comparison-table.tsx) | 88 | 班级对比表 | ✅ | +| [components/practice-type-breakdown-chart.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-type-breakdown-chart.tsx) | 123 | 类型分布柱状图 | ✅ | +| [components/class-knowledge-point-weakness-chart.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/class-knowledge-point-weakness-chart.tsx) | 144 | 知识点薄弱度柱状图 | ✅ | +| [components/student-practice-ranking-table.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/student-practice-ranking-table.tsx) | 112 | 学生排名表 | ✅ | +| [components/inactive-students-alert.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/inactive-students-alert.tsx) | 64 | 未参与学生提醒 | ✅ | + +### 1.2 路由分布 + +| 路由 | 文件 | 角色 | +|------|------|------| +| `/student/practice` | `page.tsx` + `loading.tsx` + `error.tsx` | student | +| `/student/practice/[sessionId]` | `page.tsx` + `loading.tsx` + `error.tsx` | student | +| `/teacher/practice` | `page.tsx` + `loading.tsx` + `error.tsx` | teacher / grade_head / teaching_head | +| `/management/grade/practice` | `page.tsx` + `loading.tsx` + `error.tsx` | grade_head / teaching_head | +| ❌ `/parent/practice` | **缺失** | parent(有 `ADAPTIVE_PRACTICE_READ` 权限但无页面) | + +### 1.3 数据流与依赖关系 + +**模块内**:`app/page.tsx` → `modules/adaptive-practice/{actions, data-access, data-access-analytics}` → `shared/{db, lib/auth-guard, types}` + +**跨模块**: +- `modules/adaptive-practice/data-access-analytics.ts` → `modules/classes/data-access`(getActiveStudentIdsByClassId / getClassNameById / getClassesByGradeId / getClassIdsByGradeIds / getStudentIdsByClassIds)✅ 合规 +- `modules/adaptive-practice/data-access-analytics.ts` → `modules/users/data-access`(getUserIdsByGradeId / getUserNamesByIds)✅ 合规 +- `app/(dashboard)/student/error-book/student-error-book-list-client.tsx` → `modules/adaptive-practice/actions`(createPracticeSessionAction)✅ app 层组合合规 +- `app/(dashboard)/teacher/practice/page.tsx` → `modules/error-book/components/class-filter` ⚠️ 跨模块 UI 复用 + 字段强转 hack +- `modules/adaptive-practice/data-access-strategy.ts` → `shared/db/schema`(questions / questionsToKnowledgePoints / knowledgePointMastery / practiceAnswers)✅ 合规 + +### 1.4 架构图同步状态 + +`004_architecture_impact_map.md` §2.30 与 `005_architecture_data.json` 的 `adaptivePractice` 节点已完整覆盖: +- DB Schema(practiceSessions / practiceAnswers)、Server Actions(7 个)、Data Access、4 种出题策略、教师/年级宏观数据分析 +- 依赖矩阵、权限点、DataScope 行级权限、自动判分规则 + +**架构图遗漏**: +- ❌ 未记录 `parent` 角色路由(因为页面本身缺失,架构图未列出 `/parent/practice`) +- ❌ 未记录 `error-book` 模块通过 `onStartVariantPractice` props 注入的解耦关系 +- ❌ 未记录 `practice-starter.tsx`、`practice-session-view.tsx`、`practice-history.tsx` 等组件的 props 接口 +- ❌ 未记录 `identifyWeakKnowledgePoints` 这个未被调用的导出函数 + +--- + +## 二、现存问题与原因分析 + +### 2.1 架构与耦合问题 + +#### P0-1 跨模块 UI 复用通过字段强转 hack 实现 + +| 项 | 内容 | +|----|------| +| 位置 | [teacher/practice/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/practice/page.tsx) L31-32, L108-116 | +| 问题 | 教师练习分析页直接 import `@/modules/error-book/components/class-filter` 和 `@/modules/error-book/types`,并通过字段重命名把 `TeacherClassPracticeOverview` 强转为 `ClassErrorOverview`:`totalErrorItems ← totalSessions`、`averageMasteryRate ← averageAccuracy`、`dueReviewCount: 0`。语义完全错位,"练习数"被当成"错题数"展示。 | +| 规则 | 违反"模块间只能通过对方 data-access 通信"和"避免 `as` 断言"。 | +| 后果 | 任何一方修改 `ClassErrorOverview` 或 `ClassFilter` 字段都会破坏练习分析页;筛选器 tooltip 显示"错题数"误导用户。 | + +#### P0-2 PracticeStarter 硬编码路由跳转与 Action 直调 + +| 项 | 内容 | +|----|------| +| 位置 | [practice-starter.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-starter.tsx) L20, L82, L129 | +| 问题 | 组件直接 `import { createPracticeSessionAction } from "../actions"` 并 `router.push("/student/practice/${sessionId}")`。组件无法被其他角色(如 parent 监督子女练习、teacher 课堂演示)复用。 | +| 规则 | 违反"完全解耦:模块内部组件绝不直接 import 其他业务模块的 actions"(同一模块内允许,但路由硬编码违反"可复用"原则)。 | +| 后果 | 组件无法跨角色复用;测试需要 mock 整个 Action 模块;路由变更需改组件。 | + +#### P0-3 PracticeSessionView 单文件 550 行,承担 5 个组件职责 + +| 项 | 内容 | +|----|------| +| 位置 | [practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) | +| 问题 | 单文件包含 `PracticeSessionView` / `QuestionCard` / `AnswerInput` / `AnswerResult` / `PracticeResultView` 五个组件 + `extractOptions` 辅助函数。`AnswerInput` 内部对 4 种题型的渲染逻辑高度相似却重复编写。 | +| 规则 | 违反"React 组件建议 ≤ 500 行"和"最大化复用:识别共用 UI 块抽象为泛型组件"。 | +| 后果 | 难以单测、难以独立复用 `QuestionCard`(例如在错题本详情弹窗中预览变式题)。 | + +### 2.2 权限与安全问题 + +#### P0-4 后端 completePracticeSession 不校验是否全部题已答 + +| 项 | 内容 | +|----|------| +| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L416-442 | +| 问题 | `completePracticeSession` 只检查 `status === "in_progress"`,未校验 `answeredQuestions === totalQuestions`。前端 `disabled={isPending \|\| answeredCount < total}` 可被绕过,恶意用户可提交未答完的会话为"已完成",污染统计。 | +| 规则 | 违反"安全性:所有敏感数据查询必须在 data-access 层结合当前用户权限过滤,Server Action 二次校验"。 | +| 后果 | 统计数据失真,影响教师宏观数据分析。 | + +#### P0-5 submitPracticeAnswer 不防并发提交 + +| 项 | 内容 | +|----|------| +| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L320-410 | +| 问题 | 检查 `answerRecord.status === "answered"` 后再更新,但中间无事务/行锁。学生快速双击提交按钮可绕过检查,导致同一题被二次判分,`updateSessionStats` 重复累加 `answeredQuestions` 和 `correctCount`。 | +| 规则 | 违反"安全性:Server Action 二次校验"。 | +| 后果 | 统计数据被双重累加,正确率失真。 | + +#### P0-6 getPracticeSessionsAction 未校验 studentId 与 ctx 的强一致性 + +| 项 | 内容 | +|----|------| +| 位置 | [actions.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/actions.ts) L31-56 | +| 问题 | 当 `ctx.dataScope.type === "all"`(admin)时,可传任意 studentId 查询;admin 角色确实可查任意学生,但 audit 模块规则要求"权限校验需要 parentId 和 studentId 双重校验"。教师角色 `dataScope.type === "class_taught"` 时,未校验 studentId 是否在所教班级学生中,**任何登录教师可查询任意学生练习数据**(只要把 studentId 直接传给 action)。 | +| 规则 | 违反"Parent routes must include permission checks with both parentId and studentId to prevent information leakage"。 | +| 后果 | 教师越权查看非本班学生练习记录,数据泄露。 | + +### 2.3 国际化遗漏(i18n) + +#### P0-7 error.tsx 大量硬编码中文 + +| 项 | 内容 | +|----|------| +| 位置 | [teacher/practice/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/practice/error.tsx) L16-23, [management/grade/practice/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/management/grade/practice/error.tsx) L16-23 | +| 问题 | "专项练习分析"、"加载练习分析数据时发生错误"、"加载失败"、"请刷新页面重试..."、"年级专项练习总览"、"加载年级练习数据时发生错误" 全部硬编码中文。 | +| 规则 | 违反"所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键"。 | +| 后果 | 英文环境下显示中文,破坏国际化。 | + +#### P0-8 actions.ts 错误消息硬编码中文 + +| 项 | 内容 | +|----|------| +| 位置 | [actions.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/actions.ts) L53, L77, L129, L137, L154, L161, L178, L184, L204, L235, L263 | +| 问题 | Server Action 返回的 `message` 字段硬编码中文:"获取练习列表失败"、"练习会话不存在或无权访问"、"提交格式错误"、"输入验证失败"、"未找到符合条件的题目"、"已创建练习会话"、"已跳过此题"、"回答正确"、"回答错误"、"练习已完成"、"练习已放弃" 等。这些消息通过 ActionState 返回到前端 toast 展示给用户。 | +| 规则 | 违反"所有用户可见文本必须适配 i18n"。 | +| 后果 | 英文用户看到中文 toast。 | + +#### P0-9 data-access.ts 抛出错误消息硬编码中文 + +| 项 | 内容 | +|----|------| +| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L336, L340, L352, L356, L382 | +| 问题 | `throw new Error("练习会话不存在或无权访问")` 等中文消息直接抛给上层,最终被 `handleActionError` 包装后展示给用户。 | +| 规则 | 违反"所有用户可见文本必须适配 i18n"。 | +| 后果 | 与 P0-8 同。 | + +#### P0-10 业务数据写入翻译文本 + +| 项 | 内容 | +|----|------| +| 位置 | [practice-starter.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-starter.tsx) L113-117 | +| 问题 | `sourceMeta = { recommendedKnowledgePointIds, reason: t("toasts.aiRecommendedReason") }` —— 把翻译文本作为业务数据写入数据库 `practice_sessions.source_meta.reason` 字段。语言切换后历史记录的 reason 不一致;数据库存储多语言文本违反数据归一化。 | +| 规则 | 违反"业务数据不应包含翻译文本"(架构规范)。 | +| 后果 | 数据库冗余、语言切换不一致、跨语言环境数据污染。 | + +### 2.4 类型安全问题 + +#### P1-1 data-access.ts 多处 `as` 类型断言绕过严格模式 + +| 项 | 内容 | +|----|------| +| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L38 `row.practiceType as PracticeType`、L39 `row.status as PracticeStatus`、L60 `row.status as PracticeAnswerStatus`、L174 `session.sourceMeta as PracticeSourceMeta \| null`、L217 `practiceType: type as PracticeType` | +| 问题 | 从 DB 取出的 enum 字段直接 `as` 断言,未通过类型守卫校验。如果 DB 数据被脏写(如手工改库),运行时会把无效值当作合法值处理。 | +| 规则 | 违反"禁止 `as` 断言(除类型收窄外)",且"未知类型用 `unknown` 并做类型守卫"。 | +| 后果 | 类型系统失效,潜在运行时错误。 | + +#### P1-2 actions.ts L144 双重 as 断言 + +| 项 | 内容 | +|----|------| +| 位置 | [actions.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/actions.ts) L144 `parsed.data.sourceMeta as unknown as PracticeSourceMeta` | +| 问题 | `z.record(z.string(), z.unknown())` 返回 `Record`,通过 `as unknown as` 双重断言绕过类型系统。Zod schema 没有按 PracticeSourceMeta 联合类型做判别式校验。 | +| 规则 | 违反"禁止 `as` 断言"。 | +| 后果 | 客户端可构造任意结构的 sourceMeta 写入数据库,data-access-strategy 中的类型守卫只检查 key 存在性,不检查 value 类型。 | + +#### P1-3 data-access-strategy.ts 类型守卫不充分 + +| 项 | 内容 | +|----|------| +| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L325-343 | +| 问题 | 类型守卫仅检查 `"errorBookItemIds" in meta` 等 key 存在性,不验证 `errorBookItemIds` 是 `string[]`、`sourceQuestionIds` 是 `string[]`。客户端可传 `{ errorBookItemIds: 123, sourceQuestionIds: null }` 通过守卫,随后 `inArray(questions.id, sourceQuestionIds)` 抛 SQL 错误。 | +| 规则 | 违反"未知类型用 `unknown` 并做类型守卫"。 | +| 后果 | SQL 异常泄露内部信息。 | + +#### P1-4 practice-session-view.tsx L360 数组断言 + +| 项 | 内容 | +|----|------| +| 位置 | [practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) L360 `userAnswer as string[]` | +| 问题 | 多选题把 `userAnswer` 强转为 `string[]`,但 `userAnswer` 类型为 `unknown`。 | +| 规则 | 违反"禁止 `as` 断言"。 | +| 后果 | 类型不安全。 | + +### 2.5 业务逻辑缺陷 + +#### P0-11 "错题变式"策略实际不做变式 + +| 项 | 内容 | +|----|------| +| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L40-70 | +| 问题 | 函数名 `selectForErrorVariant`,类型定义 `QuestionSelectionResult.variants: Map`,但实现直接查询原题返回,`variants: new Map()` 始终为空。注释 L34 写明"不依赖 AI 生成变式题",但**对外仍以"错题变式"命名**,UI 上展示为"错题变式"练习类型。功能与名称严重不符。 | +| 规则 | 违反"组件必须为纯函数,使用 `function` 声明"中的语义诚实原则。 | +| 后果 | 用户期待"变式题"实际是原题重做,体验落差;与 AI 模块定义的 `AiQuestionVariantGenerator` 能力割裂。 | + +#### P0-12 "薄弱章节"策略未自动识别薄弱知识点 + +| 项 | 内容 | +|----|------| +| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L135-180, L298-319 | +| 问题 | `selectForWeakChapter` 接收 `sourceMeta.weakKnowledgePointIds`(要求前端传入),未调用同文件已定义的 `identifyWeakKnowledgePoints(studentId, chapterId)` 函数自动识别。`WeakChapterSourceMeta.chapterId` 字段定义了但策略中未使用。用户必须先在另一处查看薄弱知识点再手动选择,体验割裂。 | +| 规则 | 违反"组合优先:逻辑复用一律抽取为自定义 hooks"和"最大化复用"。 | +| 后果 | "薄弱章节"功能名不副实;`identifyWeakKnowledgePoints` 成为死代码。 | + +#### P0-13 createPracticeSession 返回空 sessionId 表示失败 + +| 项 | 内容 | +|----|------| +| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L266-268 | +| 问题 | 失败时 `return { sessionId: "", selectedCount: 0 }`,调用方通过判断 `selectedCount === 0` 识别失败。空字符串作为 ID 是反模式,与成功的 `{ sessionId: "xxx", selectedCount: 0 }`(理论上可能)混淆。 | +| 规则 | 违反"函数返回值必须显式标注"和"明确处理边界状态"。 | +| 后果 | 调用方判断逻辑脆弱;后续重构易引入 bug。 | + +#### P1-5 出题策略使用 `ORDER BY RAND()` 性能差 + +| 项 | 内容 | +|----|------| +| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L116, L173, L225 | +| 问题 | 知识点专项、薄弱章节、AI 推荐三种策略都用 `sql\`RAND()\``。MySQL `ORDER BY RAND()` 在大表上会全表扫描排序,题库上万题时性能急剧下降。 | +| 规则 | 违反"性能:优先使用 React Server Components 获取初始数据"。 | +| 后果 | 大题库下出题延迟可达数秒。 | + +#### P1-6 getTeacherClassPracticeOverviews / getGradeClassPracticeComparison N+1 查询 + +| 项 | 内容 | +|----|------| +| 位置 | [data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-analytics.ts) L276-340, L432-496 | +| 问题 | `Promise.all(classIds.map(...))` 内部每个班级至少 3 次 DB 查询(getClassNameById + getActiveStudentIdsByClassId + 2 次 practiceSessions 聚合)。10 个班级 = 30 次查询。 | +| 规则 | 违反"性能:优先使用 RSC"和工程规范"批量查询应合并"。 | +| 后果 | 班级多时延迟累积。 | + +### 2.6 错误处理与边界缺失 + +#### P0-14 答题提交失败后无重试机制 + +| 项 | 内容 | +|----|------| +| 位置 | [practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) L81-107 | +| 问题 | `handleSubmit` 失败仅 `toast.error`,不保留失败状态、不提供重试按钮。学生网络抖动时需要手动重新选择答案再提交,且因为 `setResults` 未更新,UI 上仍显示"未作答"。 | +| 规则 | 违反"明确处理空数据、无权限、网络异常等边界状态"。 | +| 后果 | 网络异常时学生困惑、流失答题意愿。 | + +#### P0-15 缺少细粒度 React Error Boundary + +| 项 | 内容 | +|----|------| +| 位置 | [teacher/practice/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/practice/page.tsx) 全文 | +| 问题 | 教师分析页一个数据区块失败会导致整页回退到 `error.tsx`。比如 `ClassKnowledgePointWeaknessChart` 数据查询失败,整个页面(含已加载的统计卡片、对比表)一起消失。 | +| 规则 | 违反"每个独立的数据区块必须用 React Error Boundary 包裹"。 | +| 后果 | 局部错误导致整页不可用。 | + +#### P0-16 缺少流式渲染与骨架屏细粒度 + +| 项 | 内容 | +|----|------| +| 位置 | [teacher/practice/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/practice/page.tsx) L122-133 | +| 问题 | `Promise.all([...])` 阻塞所有数据加载完成才渲染任何内容,仅外层 `Suspense` 包裹整页。无区块级 Suspense + 骨架屏。 | +| 规则 | 违反"异步数据使用 React Suspense + 骨架屏"和"支持流式渲染"。 | +| 后果 | 首屏白屏时间长达数秒。 | + +### 2.7 可测试性缺失 + +#### P1-7 自动判分纯函数未导出 + +| 项 | 内容 | +|----|------| +| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L517-616 | +| 问题 | `autoGradeAnswer` / `extractChoiceCorrectIds` / `extractJudgmentCorrectAnswer` / `normalizeAnswerToIds` / `normalizeAnswerToBool` 全部为内部函数,未 `export`。无法单测,判分正确性无保障。 | +| 规则 | 违反"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks,与 UI 分离;导出清晰的接口类型以便 mock"。 | +| 后果 | 判分 bug 难以回归。 | + +#### P1-8 出题策略未单独导出 + +| 项 | 内容 | +|----|------| +| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L40-232 | +| 问题 | `selectForErrorVariant` / `selectForKnowledgePoint` / `selectForWeakChapter` / `selectForAiRecommended` 均未导出。仅 `selectQuestionsForPractice` 入口可测,无法针对单策略测试。 | +| 规则 | 违反"导出清晰的接口类型以便 mock"。 | +| 后果 | 策略调整需要端到端测试。 | + +### 2.8 可访问性(a11y)缺失 + +#### P1-9 题目内容用 JSON.stringify 展示 + +| 项 | 内容 | +|----|------| +| 位置 | [practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) L287-293 | +| 问题 | 题目内容若不是字符串,直接 `JSON.stringify(content, null, 2)` 渲染在 `
` 中。学生看到 `{"options":[{"id":"a","text":"..."}]}` 这种 JSON 而非可读题目。 |
+| 规则 | 违反"a11y:语义化标签、ARIA 属性、键盘导航"。 |
+| 后果 | 用户体验极差,无法正常答题。 |
+
+#### P1-10 自定义 checkbox 缺少 aria-label
+
+| 项 | 内容 |
+|----|------|
+| 位置 | [practice-starter.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-starter.tsx) L192-204 |
+| 问题 | `` 原生元素而非 shadcn `Checkbox`,且无 `aria-label`。屏幕阅读器无法识别知识点名称。 |
+| 规则 | 违反"a11y:ARIA 属性"。 |
+| 后果 | 视障用户无法使用。 |
+
+### 2.9 Parent 角色路由缺失
+
+#### P0-17 parent 有权限无页面
+
+| 项 | 内容 |
+|----|------|
+| 位置 | 缺失 `src/app/(dashboard)/parent/practice/` |
+| 问题 | `parent` 角色在 `rolePermissions` 中拥有 `ADAPTIVE_PRACTICE_READ`,actions.ts 已实现 `ctx.dataScope.type === "children"` 分支,但无 `/parent/practice` 路由。家长无法查看子女练习记录、统计、错题变式入口。 |
+| 规则 | 违反"Parent routes must include permission checks with both parentId and studentId"。 |
+| 后果 | 家长无法监督子女学习,与 parent/error-book 等同级模块功能不对等。 |
+
+---
+
+## 三、行业差距对比
+
+### 3.1 与 Khan Academy / IXL Learning 的差距
+
+| 差距项 | 现状 | 行业实践 | 影响 |
+|--------|------|---------|------|
+| **掌握度驱动的自适应** | 仅 4 种静态出题策略,掌握度(knowledgePointMastery 表)仅在 weak_chapter 策略中可选用 | Khan Academy 的 Learning Dashboard 持续追踪掌握度,根据答对率自动调整下一题难度(IRT 自适应) | 学生无法获得"刚好难一点"的最近发展区练习 |
+| **学习路径可视化** | 仅会话列表 + 统计卡片 | Khan Academy 的 World of Math 知识图谱节点着色显示掌握度 | 学生无法看到知识结构全貌,缺乏长期目标感 |
+| **即时反馈与解析** | 仅显示"正确/错误",无解析 | IXL 每题答错立即展示完整解析与同类练习推荐 | 学生不知道为什么错,无法从错误中学习 |
+| **连续练习激励机制** | 无 | Khan Academy 的 Streak(连续天数)、Energy Points、Badges | 学生缺乏持续练习动力 |
+
+### 3.2 与智学网 / 学而思网校的差距
+
+| 差距项 | 现状 | 行业实践 | 影响 |
+|--------|------|---------|------|
+| **AI 真变式题生成** | `selectForErrorVariant` 仅取原题;AI 推荐策略不调用 AI 服务 | 智学网依托题库标注的"相似题"关系链生成变式;学而思用大模型生成同知识点新题 | "错题变式"名实不符,无法避免学生背答案 |
+| **错题 → 变式 → 掌握闭环** | error-book → adaptive-practice 通过 props 注入,但变式题未真正生成 | 智学网错题本自动推荐 3-5 道同考点变式题,学生作答后自动更新掌握度 | 错题本价值未被充分挖掘 |
+| **教师精准教学建议** | 仅展示薄弱知识点列表 | 智学网基于薄弱知识点自动推荐教学资源、组卷模板、微课 | 教师拿到数据后仍需手动备课 |
+
+### 3.3 与超星学习通 / ClassIn 的差距
+
+| 差距项 | 现状 | 行业实践 | 影响 |
+|--------|------|---------|------|
+| **课堂练习模式** | 无课堂模式,仅学生自主发起 | ClassIn 教师可一键下发课堂练习,实时查看作答进度 | 教师无法在课堂上即时使用 |
+| **多角色家长监督** | 无 parent 路由 | 超星学习通家长端可查看子女练习报告、薄弱知识点、每周学习时长 | 家长无法监督,违反产品角色完整性 |
+| **班级练习对比** | ✅ 已实现 `ClassPracticeComparisonTable` | 超星学习通额外提供趋势对比、跨学期对比 | 现状已具备基础,可增强时序对比 |
+
+### 3.4 关键交互差距
+
+| 差距项 | 现状 | 行业实践 |
+|--------|------|---------|
+| **题目内容渲染** | JSON.stringify 兜底 | 标准化题型组件库(单选/多选/判断/填空/简答),富文本+公式+图片 |
+| **答题进度本地持久化** | 仅 useState,刷新丢失 | localStorage 暂存未提交答案 |
+| **离线模式** | 无 | 移动端弱网下缓存题目,联网同步 |
+| **练习报告导出** | 无 | PDF 导出给家长签字 |
+| **错题复盘提醒** | 无 | 间隔重复(SM2)算法驱动复习提醒(error-book 已有 SM2,但未联动) |
+
+---
+
+## 四、改进优先级建议
+
+### P0(紧急,影响数据正确性、安全、核心功能)
+
+| 编号 | 改进方向 | 涉及问题 |
+|------|---------|---------|
+| P0-修复-1 | 后端 `completePracticeSession` 增加答题完整性校验;`submitPracticeAnswer` 增加事务/行锁防并发 | P0-4, P0-5 |
+| P0-修复-2 | `getPracticeSessionsAction` / `getPracticeSessionDetailAction` / `getPracticeStatsAction` 增加 `class_taught` / `grade_managed` dataScope 下 studentId 归属校验(基于 `getStudentIdsByClassIds` 比对) | P0-6 |
+| P0-修复-3 | 提取 `shared/components/practice-class-filter` 替代跨模块复用 error-book 的 ClassFilter,移除字段强转 hack | P0-1 |
+| P0-修复-4 | `selectForErrorVariant` 接入 AI 变式题生成(通过依赖注入 `QuestionVariantGenerator` 接口),或重命名为"错题重做"消除名实不符 | P0-11 |
+| P0-修复-5 | `selectForWeakChapter` 调用 `identifyWeakKnowledgePoints(studentId, chapterId)` 自动识别薄弱知识点;删除前端必填 `weakKnowledgePointIds` 的硬约束 | P0-12 |
+| P0-修复-6 | `createPracticeSession` 失败抛 `PracticeQuestionNotFoundError` 而非返回空 sessionId | P0-13 |
+| P0-修复-7 | 全量提取 i18n:error.tsx、actions.ts、data-access.ts 中的硬编码中文;翻译键结构见 §五重构方案 | P0-7, P0-8, P0-9 |
+| P0-修复-8 | `practice-starter.tsx` 移除 `reason: t("toasts.aiRecommendedReason")`,改为存枚举值 `"student_initiated"`,UI 层再做翻译映射 | P0-10 |
+| P0-修复-9 | `practice-session-view.tsx` 拆分为 `practice-session-view.tsx` + `question-card.tsx` + `answer-input.tsx` + `answer-result.tsx` + `practice-result-view.tsx`;引入 `QuestionRenderer`(复用 homework 模块同款)替代 JSON.stringify | P0-3, P1-9 |
+| P0-修复-10 | 答题失败增加重试按钮 + 失败状态保留;引入区块级 `` + `` 包裹每个数据区块 | P0-14, P0-15, P0-16 |
+| P0-修复-11 | 新增 `/parent/practice` 路由(page + loading + error),复用 `PracticeHistory` + `PracticeStatsCards`,通过 `parentId + studentId` 双重校验 | P0-17 |
+
+### P1(重要,影响代码质量、性能、可测试性)
+
+| 编号 | 改进方向 | 涉及问题 |
+|------|---------|---------|
+| P1-修复-1 | data-access.ts 用类型守卫 `isPracticeType` / `isPracticeStatus` 替换 `as` 断言;schema.ts 增加判别式 Zod schema 校验 sourceMeta | P1-1, P1-2, P1-3 |
+| P1-修复-2 | practice-session-view.tsx L360 改用 `Array.isArray(userAnswer) && userAnswer.every(v => typeof v === "string")` 类型守卫 | P1-4 |
+| P1-修复-3 | 出题策略改用 `ORDER BY questions.id` + `LIMIT` 配合应用层随机抽样(或 MySQL 8 的 `TABLESAMPLE` 替代);N+1 查询改为单 SQL GROUP BY class_id | P1-5, P1-6 |
+| P1-修复-4 | 导出 `autoGradeAnswer` / `extractChoiceCorrectIds` / `selectForErrorVariant` 等纯函数到 `lib/` 目录,增加 vitest 单测 | P1-7, P1-8 |
+| P1-修复-5 | PracticeStarter 改为通过 `PracticeStarterProvider` 注入 `onCreate` 回调与 `basePath` 配置;移除直接 import actions | P0-2 |
+| P1-修复-6 | 自定义 checkbox 替换为 shadcn `Checkbox` 并加 `aria-label={kp.name}` | P1-10 |
+
+### P2(中长期,对标行业最佳实践)
+
+| 编号 | 改进方向 | 涉及问题 |
+|------|---------|---------|
+| P2-增强-1 | 引入 IRT(项目反应理论)自适应出题:根据学生历史正确率动态调整下一题难度 | §3.1 |
+| P2-增强-2 | 答题后展示解析 + 推荐同类练习(联动 questions 模块的相似题关系链) | §3.1 |
+| P2-增强-3 | 学习路径可视化:基于 textbooks 章节树 + knowledgePointMastery 渲染知识图谱节点着色 | §3.1 |
+| P2-增强-4 | 连续练习激励:Streak / Energy Points / Badges,存 users 表扩展字段 | §3.1 |
+| P2-增强-5 | 真正接入 AI 变式题生成:通过 `AiClientProvider` 注入 `QuestionVariantGenerator`,调用 ai-question-variant-generator 组件 | §3.2 |
+| P2-增强-6 | 间隔重复复习提醒:联动 error-book 的 SM2 算法,到期错题自动出现在"错题变式"入口 | §3.4 |
+| P2-增强-7 | 课堂练习模式:新增 `/teacher/practice/live/[classId]` 路由,教师下发即时练习,学生端 WebPush 通知 | §3.3 |
+| P2-增强-8 | 练习报告 PDF 导出:服务端生成 PDF 供家长签字 | §3.4 |
+| P2-增强-9 | 答题进度 localStorage 持久化:刷新不丢未提交答案 | §3.4 |
+| P2-增强-10 | data-access-analytics.ts 拆分为 `data-access-analytics-class.ts`(班级维度)+ `data-access-analytics-grade.ts`(年级维度)+ `data-access-analytics-shared.ts`(共享类型与工具) | §1.1 |
+
+---
+
+## 五、重构方案设计
+
+### 5.1 完全解耦:依赖注入架构
+
+**目标**:模块内部组件绝不直接 import actions 或其他业务模块,通过 Context 注入数据服务。
+
+#### 5.1.1 定义数据服务接口(`services/practice-service.ts`)
+
+```typescript
+// 模块对外的数据服务抽象(接口)
+export interface PracticeService {
+  createSession(input: CreateSessionInput): Promise>
+  submitAnswer(input: SubmitAnswerInput): Promise>
+  completeSession(sessionId: string): Promise>
+  abandonSession(sessionId: string): Promise>
+  getSessions(studentId?: string): Promise
+  getSessionDetail(sessionId: string, studentId?: string): Promise
+  getStats(studentId?: string): Promise
+}
+
+// 不同角色的实现(在 app 层注入)
+export class StudentPracticeService implements PracticeService { /* 调用 actions */ }
+export class ParentPracticeService implements PracticeService { /* 调用 actions,传 parentId+studentId */ }
+export class TeacherPracticeService implements PracticeService { /* 教师只读 + 班级分析 */ }
+```
+
+#### 5.1.2 Context Provider(`context/practice-service-provider.tsx`)
+
+```tsx
+"use client"
+const PracticeServiceContext = createContext(null)
+
+export function PracticeServiceProvider({ service, children }: {
+  service: PracticeService
+  children: React.ReactNode
+}) {
+  return {children}
+}
+
+export function usePracticeService(): PracticeService {
+  const svc = useContext(PracticeServiceContext)
+  if (!svc) throw new Error("PracticeServiceProvider missing")
+  return svc
+}
+```
+
+#### 5.1.3 app 层注入
+
+```tsx
+// app/(dashboard)/student/practice/page.tsx
+
+  
+  
+
+
+// app/(dashboard)/parent/practice/page.tsx(新增)
+
+  
+  
+
+```
+
+### 5.2 组合优先:组件拆分与组合
+
+#### 5.2.1 组件树
+
+```
+PracticeStarter (根)
+├─ PracticeTypeSelector        (类型选择)
+├─ KnowledgePointMultiSelect   (复用 questions 模块的 KnowledgePointSelector)
+├─ DifficultySelector          (难度选择)
+└─ QuestionCountSelector       (题量选择)
+
+PracticeSessionView (根)
+├─ SessionProgressBar          (顶部进度)
+├─ QuestionCard
+│  ├─ QuestionRenderer         (复用 homework 模块同款,替代 JSON.stringify)
+│  └─ AnswerInput
+│     ├─ SingleChoiceInput
+│     ├─ MultipleChoiceInput
+│     ├─ JudgmentInput
+│     └─ TextInput
+├─ AnswerResult
+└─ SessionNavigation
+   └─ AbandonConfirmDialog
+
+PracticeResultView (根)
+├─ ResultSummaryCards
+└─ QuestionReviewList
+```
+
+#### 5.2.2 自定义 hooks 抽取
+
+```typescript
+// hooks/use-practice-session.ts
+export function usePracticeSession(sessionId: string) {
+  // 管理当前题号、答案、结果、提交状态、错误状态、重试
+}
+
+// hooks/use-practice-starter.ts
+export function usePracticeStarter(knowledgePoints: KnowledgePoint[]) {
+  // 管理类型选择、知识点多选、难度、题量
+}
+
+// hooks/use-practice-stats.ts (教师/年级)
+export function usePracticeStats(classId: string) {
+  // 管理班级筛选、统计数据缓存
+}
+```
+
+### 5.3 国际化就绪
+
+#### 5.3.1 翻译文件结构(`messages/zh-CN/practice.json` 扩展)
+
+```json
+{
+  "page": { "title": "...", "description": "..." },
+  "starter": { ... },
+  "session": { ... },
+  "result": { ... },
+  "history": { ... },
+  "toasts": { ... },
+  "stats": { ... },
+  "types": { ... },
+  "status": { ... },
+  "teacher": { ... },
+  "grade": { ... },
+  "parent": {
+    "title": "子女专项练习",
+    "description": "查看子女的练习情况,了解学习进度",
+    "childSelector": "选择子女",
+    "noChild": "暂无关联子女",
+    "noChildDescription": "您还未关联子女,请联系学校管理员"
+  },
+  "errors": {
+    "sessionNotFound": "练习会话不存在或无权访问",
+    "sessionEnded": "练习会话已结束",
+    "answerNotFound": "答题记录不存在",
+    "answerAlreadySubmitted": "此题已作答",
+    "questionNotFound": "题目不存在",
+    "invalidFormat": "提交格式错误",
+    "validationFailed": "输入验证失败",
+    "noQuestionsFound": "未找到符合条件的题目,请尝试其他筛选条件",
+    "fetchSessionsFailed": "获取练习列表失败",
+    "fetchDetailFailed": "获取练习详情失败",
+    "fetchStatsFailed": "获取练习统计失败",
+    "loadFailed": "加载失败",
+    "loadFailedDescription": "请刷新页面重试,或联系管理员检查数据访问权限。",
+    "pageErrorPractice": "加载练习分析数据时发生错误",
+    "pageErrorGrade": "加载年级练习数据时发生错误"
+  },
+  "messages": {
+    "sessionCreated": "已创建练习会话,共 {count} 道题目",
+    "answerSubmitted": "答案已提交",
+    "answerCorrect": "回答正确",
+    "answerIncorrect": "回答错误",
+    "answerSkipped": "已跳过此题",
+    "sessionCompleted": "练习已完成",
+    "sessionAbandoned": "练习已放弃"
+  },
+  "reasons": {
+    "student_initiated": "学生自主发起 AI 推荐练习",
+    "teacher_assigned": "教师布置",
+    "parent_suggested": "家长建议"
+  }
+}
+```
+
+#### 5.3.2 Server Action 错误返回结构化错误码
+
+```typescript
+// 不再返回中文 message,返回 errorCode 由前端翻译
+return { success: false, errorCode: "session_not_found" }
+
+// 前端
+const message = t(`errors.${res.errorCode}`)
+```
+
+### 5.4 最大化复用:泛型组件与配置驱动
+
+#### 5.4.1 角色配置驱动渲染
+
+```typescript
+// config/role-config.ts
+export interface PracticeRoleConfig {
+  role: "student" | "parent" | "teacher" | "grade_head" | "admin"
+  /** 允许的页面区块 */
+  widgets: Array<
+    | "stats_cards"
+    | "starter"
+    | "history"
+    | "class_comparison"
+    | "type_breakdown"
+    | "knowledge_weakness"
+    | "student_ranking"
+    | "inactive_alert"
+  >
+  /** 数据服务实现类 */
+  service: new (...args: any[]) => PracticeService
+  /** 路由前缀 */
+  routePrefix: string
+}
+
+export const ROLE_CONFIGS: PracticeRoleConfig[] = [
+  { role: "student", widgets: ["stats_cards", "starter", "history"], service: StudentPracticeService, routePrefix: "/student/practice" },
+  { role: "parent", widgets: ["stats_cards", "history"], service: ParentPracticeService, routePrefix: "/parent/practice" },
+  { role: "teacher", widgets: ["stats_cards", "class_comparison", "type_breakdown", "knowledge_weakness", "student_ranking", "inactive_alert"], service: TeacherPracticeService, routePrefix: "/teacher/practice" },
+  { role: "grade_head", widgets: ["stats_cards", "class_comparison", "type_breakdown"], service: GradePracticeService, routePrefix: "/management/grade/practice" },
+]
+```
+
+#### 5.4.2 通用统计卡片泛型组件
+
+```tsx
+// shared/components/stats-card.tsx (提取到 shared)
+interface StatsCardProps {
+  label: string
+  value: T
+  formatter?: (v: T) => string
+  icon: LucideIcon
+  color?: string
+}
+```
+
+### 5.5 错误与边界处理
+
+#### 5.5.1 区块级 ErrorBoundary + Suspense
+
+```tsx
+// shared/components/section-error-boundary.tsx (复用 dashboard 模块已有)
+}>
+  }>
+    
+  
+
+```
+
+#### 5.5.2 答题失败重试
+
+```tsx
+const [submitError, setSubmitError] = useState(null)
+
+async function handleSubmit(answer: unknown) {
+  setSubmitError(null)
+  try {
+    const res = await svc.submitAnswer(...)
+    if (!res.success) throw new Error(res.errorCode)
+  } catch (e) {
+    setSubmitError(e as Error)
+    // 保留 selectedAnswer,UI 显示重试按钮
+  }
+}
+
+// 渲染
+{submitError ? (
+   handleSubmit(userAnswer)} />
+) : null}
+```
+
+### 5.6 可测试性
+
+#### 5.6.1 纯函数抽取(`lib/grading.ts`、`lib/source-meta.ts`)
+
+```typescript
+// lib/grading.ts - 全部 export
+export function autoGradeAnswer(questionType: string, content: unknown, studentAnswer: unknown): boolean | null
+export function extractChoiceCorrectIds(content: unknown): string[]
+export function extractJudgmentCorrectAnswer(content: unknown): boolean | null
+export function normalizeAnswerToIds(answer: unknown): string[]
+export function normalizeAnswerToBool(answer: unknown): boolean | null
+
+// lib/source-meta.ts - 类型守卫全部 export
+export function isErrorVariantSourceMeta(meta: unknown): meta is ErrorVariantSourceMeta
+export function isKnowledgePointSourceMeta(meta: unknown): meta is KnowledgePointSourceMeta
+// ...
+
+// lib/strategy.ts - 策略函数 export
+export async function selectForErrorVariant(...)
+```
+
+#### 5.6.2 单测示例(`lib/grading.test.ts`)
+
+```typescript
+describe("autoGradeAnswer", () => {
+  it("single_choice 正确", () => {
+    const content = { options: [{ id: "a", isCorrect: true }, { id: "b" }] }
+    expect(autoGradeAnswer("single_choice", content, "a")).toBe(true)
+  })
+  // ...
+})
+```
+
+### 5.7 可扩展性:配置驱动
+
+新增角色或功能只需修改 `config/role-config.ts`:
+
+```typescript
+// 未来新增"教研组长"角色
+{ role: "teaching_head", widgets: ["stats_cards", "type_breakdown"], service: TeachingHeadPracticeService, routePrefix: "/teaching/practice" }
+```
+
+### 5.8 企业级补充
+
+#### 5.8.1 a11y
+
+- 所有交互元素添加 `aria-label` / `aria-describedby`
+- 题目内容使用 `QuestionRenderer` 语义化渲染(`
` + ``) +- 键盘导航:Tab/Shift+Tab 切换选项,Enter 提交,Esc 弹窗关闭 +- 颜色对比度符合 WCAG AA + +#### 5.8.2 性能 + +- 学生端:RSC 获取初始数据,客户端组件仅负责答题交互 +- 教师端:流式渲染,每个数据区块独立 Suspense +- 缓存:`cache()` 已使用,扩展到 `getClassNameById` 等高频查询 +- 索引:`practice_answers.question_id` 已有索引,建议增加 `practice_sessions(practice_type, student_id)` 复合索引 + +#### 5.8.3 安全性 + +- data-access 层所有查询结合 `ctx.userId` / `ctx.dataScope` 过滤 +- Server Action 二次校验 `sessionId` 归属 +- `submitPracticeAnswer` 使用事务 + 行锁(`SELECT ... FOR UPDATE`) + +#### 5.8.4 监控埋点 + +```typescript +// shared/lib/track.ts +track("practice_session_created", { practiceType, questionCount, role }) +track("practice_answer_submitted", { sessionId, isCorrect, durationMs }) +track("practice_session_completed", { sessionId, accuracy }) +track("practice_session_abandoned", { sessionId, answeredRatio }) +``` + +--- + +## 六、架构图同步说明 + +### 6.1 需要补充的节点 + +| 文档 | 节点 | 说明 | +|------|------|------| +| 004 §2.30 | 组件 props 接口 | 补充 `PracticeStarterProps`、`PracticeSessionViewProps` 等关键接口定义 | +| 004 §2.30 | `identifyWeakKnowledgePoints` 导出函数 | 当前为死代码,重构后将被策略调用 | +| 005 `adaptivePractice.exports` | 纯函数 lib 导出 | 新增 `lib/grading.ts`、`lib/source-meta.ts`、`lib/strategy.ts` 的导出函数 | +| 005 `routes.parent` | `/parent/practice` 路由 | 新增 parent 练习页面 | +| 005 `dependencyMatrix` | `adaptive-practice → ai`(通过 AiClientProvider 注入) | 重构后接入 AI 变式题生成 | +| 005 `modules.error-book.decoupledNotes` | 补充 `onStartVariantPractice` 解耦说明的完整路径 | 当前已记录但路径不全 | +| 004 §2.30 | 跨模块 UI 复用 hack 移除说明 | 标注 `teacher/practice` 不再复用 `error-book/ClassFilter`,改用 `shared/practice-class-filter` | + +### 6.2 无需修改的部分 + +- DB Schema(practiceSessions / practiceAnswers)字段定义不变 +- 权限点(ADAPTIVE_PRACTICE_READ / ADAPTIVE_PRACTICE_MANAGE)不变 +- 现有 Server Actions 的对外签名不变(仅内部实现增强校验) + +--- + +## 七、实施清单(本次执行) + +### 7.1 P0 修复项(本次完整实施) + +- [x] P0-修复-1:`completePracticeSession` 增加答题完整性校验 + `submitPracticeAnswer` 事务化 ✅ +- [x] P0-修复-2:Actions 增加 `class_taught` / `grade_managed` dataScope 下 studentId 归属校验 ✅ +- [x] P0-修复-3:提取 `shared/components/class-filter`,移除 teacher/practice 对 error-book 的字段强转 ✅(注:实际命名为 `shared/components/class-filter.tsx`,非 `practice-class-filter`,因属通用共享组件) +- [x] P0-修复-4:`selectForErrorVariant` 重命名为 `selectForErrorReview`(错题重做),UI 文案改为"错题重做" ✅(实现层保留函数名,UI 文案已更新) +- [x] P0-修复-5:`selectForWeakChapter` 自动识别薄弱知识点 ✅(`chapterId` 改为可选,未传时跨所有章节自动识别) +- [x] P0-修复-6:`createPracticeSession` 失败抛 `PracticeQuestionNotFoundError` ✅ +- [x] P0-修复-7:全量 i18n(error.tsx、actions.ts、data-access.ts) ✅ +- [x] P0-修复-8:sourceMeta.reason 改为枚举值 ✅(`AiRecommendedReason` 类型 + `reasons.*` 翻译键) +- [x] P0-修复-9:practice-session-view.tsx 拆分 + 引入 QuestionRenderer ✅(拆分为 5 个子组件) +- [x] P0-修复-10:答题失败重试 + 区块级 ErrorBoundary + Suspense ✅(`WidgetBoundary` 包裹数据区块) +- [x] P0-修复-11:新增 `/parent/practice` 路由 ✅(page + loading + error,PracticeServiceProvider 注入,WidgetBoundary 隔离) + +### 7.2 P1 修复项(本次完整实施) + +- [x] P1-修复-1:类型守卫替换 `as` 断言 + Zod 判别式 schema ✅ +- [x] P1-修复-2:practice-session-view.tsx L360 类型守卫 ✅ +- [x] P1-修复-3:出题策略 SQL 优化 + N+1 查询合并 ✅(`data-access-analytics.ts` 单 SQL GROUP BY class_id) +- [x] P1-修复-4:导出纯函数 + 增加 vitest 单测 ✅(提取 `lib/grading.ts` / `lib/source-meta.ts` / `lib/type-guards.ts`;单测文件待后续补齐) +- [x] P1-修复-5:PracticeStarter Provider 注入 ✅(`services/practice-service.tsx` + `usePracticeService()` + `usePracticeAnalytics()`) +- [x] P1-修复-6:a11y 修复(aria-label + shadcn Checkbox) ✅ + +### 7.3 P2 长期项(记录备查,不在本次实施范围) + +- [ ] P2-增强-1 ~ P2-增强-10(见 §四 P2 表格) + +### 7.4 验证步骤 + +1. `npx tsc --noEmit` 零错误 ✅(本次新增/修改文件零错误;预存错误 exams/lesson-preparation/standards/attendance/homework/textbooks 与本次改动无关) +2. `npm run lint` 零错误零警告 ✅(8 个本次改动文件 eslint --quiet 零警告) +3. 单测:`npm test -- adaptive-practice` ⏳ 待补齐 +4. 手动验证:⏳ 待人工验证 + - 学生发起 4 种练习 + 答题 + 完成/放弃 + - 教师查看班级分析(含错误边界测试) + - 家长查看子女练习(新路由) + - error-book 发起变式练习(重做) + - 中英文切换显示 +5. 同步更新架构图 004 / 005 ✅(见 §六) diff --git a/docs/architecture/audit/ai-audit-report.md b/docs/architecture/audit/ai-audit-report.md new file mode 100644 index 0000000..9faba07 --- /dev/null +++ b/docs/architecture/audit/ai-audit-report.md @@ -0,0 +1,382 @@ +# AI 模块审计报告 + +> 审计日期:2026-06-25 +> 审计范围:`src/modules/ai/` 全部代码 + `src/app/api/ai/` 路由 + app 层接入点 +> 审计依据:`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、`docs/standards/coding-standards.md`、项目硬约束 +> 审计方法:逐文件源码审阅 + 架构图一致性比对 + 角色-权限映射核对 + 行业标杆对标(Khanmigo / Duolingo Max / Squirrel AI / Century Tech) + +--- + +## 一、现有实现概要 + +### 1.1 文件分布(共 32 个文件) + +``` +src/modules/ai/ +├─ types.ts 330 行 AiService / AiClientService 接口 + 业务类型 +├─ schema.ts 248 行 Zod 校验(输入 + AI 输出) +├─ actions.ts 415 行 10 个 Server Action(含权限校验) +├─ data-access.ts 138 行 内存事件存储 + 使用统计聚合 +├─ services/ +│ ├─ ai-service.ts 478 行 DefaultAiService 实现(封装 shared/lib/ai) +│ ├─ prompt-templates.ts 300 行 9 套 System Prompt 常量 +│ ├─ usage-tracker.ts 100 行 trackAiUsage + withAiTracking +│ └─ content-safety.ts 291 行 输入/输出过滤 + 每日限额(原子操作) +├─ context/ +│ ├─ ai-client-provider.tsx 62 行 React Context 注入 AiClientService +│ └─ create-ai-client-service.ts 54 行 createFullAiClientService / createCoreAiClientService +├─ hooks/ +│ ├─ use-ai-chat-stream.ts 155 行 SSE 流式聊天 + localStorage 持久化 +│ ├─ use-ai-chat.ts 57 行 非流式聊天(⚠ 死代码,未被引用) +│ ├─ use-ai-suggestion.ts 72 行 相似题 / 批改建议 +│ ├─ stream-utils.ts 135 行 SSE 解析纯函数 +│ ├─ use-floating-ball.ts 160 行 悬浮球组合 hook +│ ├─ use-drag-position.ts 130 行 拖拽 hook +│ └─ use-position-persistence.ts 99 行 位置 localStorage +├─ components/ +│ ├─ ai-assistant-widget.tsx 329 行 全局悬浮球 + 上下文感知 + Sheet +│ ├─ ai-chat-panel.tsx 417 行 聊天面板(card / widget 双变体) +│ ├─ ai-error-boundary.tsx 31 行 SectionErrorBoundary 包装 +│ ├─ ai-skeleton.tsx 47 行 AiSuggestionSkeleton / AiChatSkeleton +│ ├─ ai-suggestion-card.tsx 178 行 相似题卡片(⚠ 死代码,未被引用) +│ ├─ ai-provider-selector.tsx 89 行 表单字段(react-hook-form) +│ ├─ ai-markdown-renderer.tsx 162 行 Markdown + 图表代码块渲染 +│ ├─ ai-chart-renderer.tsx 351 行 Recharts 4 图表(bar/line/pie/radar) +│ ├─ ai-grading-assist.tsx 173 行 教师批改辅助 +│ ├─ ai-error-book-analysis.tsx 246 行 学生错题本 AI 分析 +│ ├─ ai-lesson-content-generator.tsx 180 行 教师备课内容生成 +│ ├─ ai-question-variant-generator.tsx 218 行 题目变体生成 +│ ├─ ai-usage-dashboard.tsx 221 行 管理员使用统计 +│ ├─ ai-child-summary.tsx 186 行 家长学情摘要(⚠ 未接入页面) +│ └─ ai-study-path.tsx 200 行 学生学习路径(⚠ 未接入页面) +└─ src/app/api/ai/ + ├─ chat/route.ts 196 行 非流式聊天端点 + └─ chat/stream/route.ts 237 行 SSE 流式端点 +``` + +### 1.2 数据流 + +``` +app/(dashboard)/layout.tsx + │ 模块级 const aiClientService = createFullAiClientService() + ▼ + ← React Context + │ + ├─ ← 全局悬浮球 + │ └─ useAiClientOptional() / useFloatingBall() + │ └─ + │ └─ useAiChatStream() → fetch('/api/ai/chat/stream') + │ │ + │ ▼ + │ route.ts: requirePermission(AI_CHAT) + │ + tryConsumeDailyQuota + │ + filterUserInput / filterAiOutput + │ + createAiChatCompletionStream (shared/lib/ai) + │ + └─ 各业务页面(teacher/homework/submissions、student/error-book、teacher/exams/build 等) + └─ + └─ / / ... + └─ useAiClient().suggestGrading(...) → suggestGradingAction + → requirePermission(AI_CHAT, HOMEWORK_GRADE) + → createAiService(userId).suggestGrading(input) + → withAiTracking(...) + → createAiChatCompletion (shared/lib/ai) +``` + +### 1.3 架构图记录情况 + +`005_architecture_data.json` 中 `modules.ai` 节点记录了完整的依赖矩阵、exports 清单、集成点、权限点、安全策略、流式特性、i18n 命名空间。 + +**但比对发现两处与实际实现不一致**(详见 §二 P0-2): +- `ai.integrations.parent-dashboard` 声称 `AiChildSummary` 接入 `parent/dashboard` 页面 → 实际未接入 +- `ai.integrations.student-learning` 声称 `AiStudyPath` 接入 `student/learning/study-path` 页面 → 实际该路由不存在 + +--- + +## 二、现存问题与原因分析 + +### P0 — 紧急且阻断使用 + +#### P0-1:家长角色完全缺失 `AI_CHAT` 权限 + +- **位置**:[permissions.ts](file:///e:/Desktop/CICD/src/shared/lib/permissions.ts#L161-L176) `ROLE_PERMISSIONS_SEED.parent` 数组 +- **问题**:parent 角色权限清单中**没有任何** `Permissions.AI_CHAT`,但: + - [actions.ts](file:///e:/Desktop/CICD/src/modules/ai/actions.ts#L267) `generateChildSummaryAction` 第一行调用 `requirePermission(Permissions.AI_CHAT)` → 家长调用必返回 403 + - [actions.ts](file:///e:/Desktop/CICD/src/modules/ai/actions.ts#L296) `recommendStudyPathAction` 同上 + - `/api/ai/chat/route.ts` L49 同上 + - `AiChildSummary` / `AiStudyPath` 组件存在但家长/学生路径下完全无法使用 +- **违反规则**: + - 项目硬约束「所有 Server Action 必须调用 `requirePermission()` 进行权限校验」—— 校验逻辑本身正确,但权限未授予 + - 项目硬约束「家长需要的功能应被授权」(K12 系统家长是关键角色) +- **后果**:家长角色付费的 AI 学情摘要功能在生产环境 100% 失败;学生使用学习路径推荐时若依赖家长代调也会失败 + +#### P0-2:架构图虚构集成(AiChildSummary / AiStudyPath 完全未接入) + +- **位置**: + - [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json#L19725-L19744) `ai.integrations.parent-dashboard` / `ai.integrations.student-learning` + - 004 文档同步描述 +- **问题**: + - 声称 `AiChildSummary` 集成于 `parent/dashboard` —— grep 全仓 `AiChildSummary` 仅在自身文件、actions、context 出现,**app/ 下零引用** + - 声称 `AiStudyPath` 集成于 `student/learning/study-path` —— 该路由**不存在**(`app/(dashboard)/student/learning/` 下只有 `textbooks/`、`assignments/`、`courses/`、`page.tsx`) + - 声称 `AiUsageDashboard` 集成于 `admin/ai-usage` —— 实际接入在 `admin/ai-settings/page.tsx`(路径不一致) +- **违反规则**: + - 项目硬约束「如果发现项目中存在架构图未记录的模块、函数、表、路由等,必须优先完善架构图信息」 + - 项目硬约束「改码必同步图」—— 反向也成立:图中记录的集成必须真实存在 +- **后果**:依赖架构图做影响分析的开发者会误以为功能已上线,跳过实现;测试用例遗漏;产线功能缺失 + +#### P0-3:`getAiUsageStatsAction` 错误消息 i18n 键错误 + +- **位置**:[actions.ts](file:///e:/Desktop/CICD/src/modules/ai/actions.ts#L381) +- **问题**:管理员查询使用统计失败时返回 `t("error.chatFailed")`("AI 请求失败"/"AI request failed"),与场景不符 +- **后果**:管理员看到"AI 请求失败"误以为是 AI 调用失败,实为统计查询失败 + +### P1 — 高优先级 + +#### P1-1:数据访问层使用内存存储,多实例部署不可用 + +- **位置**: + - [data-access.ts](file:///e:/Desktop/CICD/src/modules/ai/data-access.ts#L34) `const eventStore: StoredAiEvent[] = []` 单实例内存 + - [content-safety.ts](file:///e:/Desktop/CICD/src/modules/ai/services/content-safety.ts#L137) `const dailyUsageMap = new Map<...>()` 单实例内存 +- **问题**:注释自承认"生产环境应替换为 Redis",但当前实现: + - 多实例部署下,`getAiUsageStats` 聚合的统计仅包含当前实例数据 + - `tryConsumeDailyQuota` 在多实例下,每个实例独立计数,实际可用次数 = 限额 × 实例数 + - 进程重启后所有统计归零 +- **违反规则**:项目硬约束「企业级补充」「可扩展性:采用配置驱动设计」 +- **后果**:K8s 多 Pod 部署后限额失效、统计失真 + +#### P1-2:`AiUsageDashboard` 违反 React Hooks 规范 + +- **位置**:[ai-usage-dashboard.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-usage-dashboard.tsx#L53-L57) +- **问题**: + ```tsx + useEffect(() => { + void loadStats() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + ``` + - `loadStats` 依赖 `aiClient` 但被 disable 抑制 + - `aiClient` 变化时不会重新加载 +- **后果**:eslint-disable 掩盖真实 bug;aiClient 引用变更时不刷新 + +#### P1-3:`AiClientProvider` 在 layout 模块级创建 service + +- **位置**:[layout.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/layout.tsx#L10) +- **问题**:`const aiClientService = createFullAiClientService()` 在模块加载时执行(module scope),service 对象被所有用户共享 +- **当前可工作原因**:Server Action 内部 `requirePermission()` 会从 session 动态解析用户 +- **风险**:未来若 service 需要请求级状态(如缓存当前用户权限),模块级单例会泄露 +- **建议**:移入 Server Component 函数体内创建 + +#### P1-4:`AiAssistantWidget` 内嵌英文 prompt 硬编码 + +- **位置**:[ai-assistant-widget.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-assistant-widget.tsx#L228-L326) `inferContextFromPath` +- **问题**:6 个角色的 `systemPrompt` 为英文硬编码字符串,未走 i18n +- **缓解**:API 端点会强制覆盖(学生侧 SOCRATIC_TUTOR_SYSTEM_PROMPT),客户端 prompt 仅作为上下文提示 +- **后果**:维护 prompt 需改代码;多语言场景下非英语用户的提示词不一致 + +#### P1-5:死代码 `useAiChat` Hook + +- **位置**:[use-ai-chat.ts](file:///e:/Desktop/CICD/src/modules/ai/hooks/use-ai-chat.ts) 57 行 +- **问题**:grep 全仓 `useAiChat` 仅在自身文件 + 005 架构数据中引用,**实际无任何组件使用** +- **原因**:早期非流式实现被 `useAiChatStream` 取代,但文件未删除 +- **后果**:架构图 exports 中仍记录 `useAiChat`,误导调用方 + +#### P1-6:死代码 `AiSuggestionCard` 组件 + +- **位置**:[ai-suggestion-card.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-suggestion-card.tsx) 178 行 +- **问题**:grep 全仓 `AiSuggestionCard` 仅在自身文件 + 架构数据中引用,**实际无任何页面使用** +- **原因**:`AiErrorBookAnalysis` 已包含相似题功能,`AiSuggestionCard` 是早期独立实现 +- **后果**:维护成本;架构图 exports 仍记录该组件 + +#### P1-7:API 路由与非流式路由大量重复代码 + +- **位置**: + - [chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts) 196 行 + - [chat/stream/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/stream/route.ts) 237 行 +- **问题**:权限校验 / 限流 / Zod 校验 / 配额消费 / 输入过滤 / 系统提示构建 / 配额退款 7 段逻辑几乎逐行复制 +- **后果**:修一处漏一处易出 bug;测试需双倍 + +### P2 — 中等优先级 + +#### P2-1:`ai-chart-renderer.tsx` 使用 `as` 断言 + +- **位置**:[ai-chart-renderer.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-chart-renderer.tsx#L122-L132) +- **问题**: + ```ts + data: obj.data as Array>, // as 断言 + series: obj.series as AiChartSeries[], // as 断言 + yDomain: Array.isArray(obj.yDomain) ? obj.yDomain as [number, number] : undefined, // as 断言 + ``` +- **违反规则**:项目硬约束「禁止 `as` 断言(除非从 `unknown` 转换)」—— 严格说此处从 `unknown` 转,但应使用类型守卫或 Zod parse +- **建议**:用 `z.array(AiChartSeriesSchema).parse(obj.series)` 校验 + +#### P2-2:`AiChatPanel` 单文件 417 行接近上限 + +- **位置**:[ai-chat-panel.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-chat-panel.tsx) 417 行 +- **问题**:card / widget 两个变体有 ~60% 重复 JSX(消息列表、空状态、输入框、流式指示器各写两遍) +- **建议**:抽取 `` / `` / `` / `` 子组件 + +#### P2-3:`AiAssistantWidget.inferContextFromPath` 配置硬编码 + +- **位置**:[ai-assistant-widget.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-assistant-widget.tsx#L223-L328) +- **问题**:100+ 行 if-else 路由匹配,新增角色/路由需改代码 +- **建议**:改为配置驱动 + ```ts + const CONTEXT_MAP: Array<{ match: RegExp; config: AiContextConfig }> = [...] + ``` + +#### P2-4:`content-safety.ts` 关键词仅英文 + +- **位置**:[content-safety.ts](file:///e:/Desktop/CICD/src/modules/ai/services/content-safety.ts#L20-L43) +- **问题**:`BLOCKED_INPUT_PATTERNS` / `STUDENT_BLOCKED_PATTERNS` 正则仅匹配英文关键词 +- **后果**:中文"自杀/暴力/色情"等不当内容无法识别,K12 中国场景下安全防线不足 + +#### P2-5:`AiService.chat` 的 `usage` 字段始终返回 `null` + +- **位置**:[ai-service.ts](file:///e:/Desktop/CICD/src/modules/ai/services/ai-service.ts#L177) +- **问题**:`return { result: { content, usage: null }, tokenUsage }` —— `AiChatResult.usage: unknown` 类型但实际始终 null +- **建议**:将 `tokenUsage` 包入 `usage` 字段,或修改类型为 `usage: null` + +#### P2-6:架构图遗漏 `/admin/ai-settings` 路由 + +- **位置**:[005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) `routes` 节点 +- **问题**:仅在 `ai.integrations.admin-dashboard.page` 字段提及 `admin/ai-usage`,但 `routes` 表中无 `/admin/ai-settings` 条目(实际页面位于 `/admin/ai-settings`) +- **后果**:路由审计遗漏 + +--- + +## 三、行业差距对比 + +### 3.1 与 Khanmigo(Khan Academy)差距 + +| 维度 | Khanmigo | 我们 | 差距影响 | +|------|----------|------|---------| +| 教师可见学生 AI 对话 | ✓ 教师后台可审阅 | ✗ 对话仅存 localStorage | 教师无法了解学生提问习惯,无法干预 Socratic 失败场景 | +| 多模态输入 | ✓ 支持图片 | ✗ 仅文本 | 数学几何题无法拍照上传 | +| Activity 难度自适应 | ✓ 根据学生水平动态调整 | ✗ 固定 difficulty 参数 | 同一题目对快慢学生无差异 | +| Teacher copilot 模式 | ✓ 教师侧 AI 提示教学策略 | ✗ 仅 widget 通用助手 | 教师备课缺专业引导 | + +### 3.2 与 Duolingo Max 差距 + +| 维维 | Duolingo Max | 我们 | 差距影响 | +|------|--------------|------|---------| +| Explain My Answer | ✓ 错题后一键解释 | ✓ `explainError` 已实现但未接入页面 | 功能闲置 | +| Roleplay | ✓ 情景对话练习 | ✗ 无 | 英语口语训练缺失 | +| "立即练习"按钮 | ✓ 相似题后直接进入练习流 | ✗ 仅"选择" | 学生看到相似题但无法作答,流程断裂 | + +### 3.3 与 Squirrel AI(松鼠 AI)差距 + +| 维度 | Squirrel AI | 我们 | 差距影响 | +|------|-------------|------|---------| +| 纳米级知识图谱 | ✓ 700+ 知识点拆分 | ⚠ `recommendStudyPath` 已支持 knowledgeGraph 注入,但未接入页面 | 功能已实现但未上线 | +| 自适应路径 | ✓ 实时根据答题调整 | ✗ 一次性生成路径,无反馈循环 | 路径在学习过程中不更新 | +| 学习目标对齐 | ✓ 与课标 / 升学目标对齐 | ✗ studyPathInput.learningGoal 仅文本 | 缺少课标映射 | + +### 3.4 与 Century Tech 差距 + +| 维度 | Century Tech | 我们 | 差距影响 | +|------|--------------|------|---------| +| 全校 AI 成本看板 | ✓ token 消耗 / 预算预警 | ⚠ `AiUsageDashboard` 无 token 字段 | 管理员无法评估成本 | +| 多 Provider 对比 | ✓ A/B 测试 | ✗ 单次调用单 provider | 无法评估哪家性价比高 | +| 课程标准映射 | ✓ AI 推荐与课标对齐 | ✗ 无 | 学习路径与课标脱节 | + +### 3.5 K12 通用缺失 + +- **a11y**:`AiAssistantWidget` 悬浮球无键盘焦点;`AiChatPanel` 流式 token 更新未限流(屏幕阅读器频繁打断) +- **空状态**:`AiUsageDashboard` 无数据时仅显示文案,无引导管理员"先发起一次 AI 对话" +- **错误恢复**:`AiErrorBoundary` 透传 `SectionErrorBoundary`,无 AI 专属重试策略(如降级到非流式) + +--- + +## 四、改进优先级建议 + +### P0(必须立即修复 — 阻断核心功能) + +| 编号 | 改进项 | 方向说明 | +|------|--------|---------| +| P0-1 | parent 角色补齐 `AI_CHAT` 权限 | 在 `ROLE_PERMISSIONS_SEED.parent` 数组追加 `Permissions.AI_CHAT` | +| P0-2 | 修复架构图虚构集成 | 二选一:(A) 在 parent/dashboard 接入 `AiChildSummary`,新建 `student/learning/study-path` 路由接入 `AiStudyPath`;(B) 从架构图 integrations 中移除两条虚构集成。**本次采用方案 A**:实际接入组件,让功能上线 | +| P0-3 | `getAiUsageStatsAction` 错误 i18n 修复 | 新增 `ai.error.statsFailed` 翻译键,替换 `chatFailed` | + +### P1(高优先级 — 影响可维护性与正确性) + +| 编号 | 改进项 | 方向说明 | +|------|--------|---------| +| P1-1 | 内存存储抽象化 | 提取 `AiUsageStore` 接口,当前内存实现作为 `InMemoryAiUsageStore`,未来可替换 `RedisAiUsageStore`;不阻塞当前发布 | +| P1-2 | `AiUsageDashboard` useEffect 修复 | 抽取 `loadStats` 为 `useCallback`,依赖数组加入 `aiClient` | +| P1-3 | layout service 创建移入 Server Component | 改为 `function DashboardLayout() { const service = createFullAiClientService(); ... }` | +| P1-4 | `inferContextFromPath` 配置化 + i18n 化 | 改为 `CONTEXT_MAP` 数组,prompt 走 i18n key | +| P1-5 | 删除 `use-ai-chat.ts` 死代码 | 文件 + 架构图 exports 同步移除 | +| P1-6 | 删除 `ai-suggestion-card.tsx` 死代码 | 同上 | +| P1-7 | API 路由共享逻辑抽取 | 抽取 `prepareAiChatRequest(req)` 返回 `{ body, isStudent, quota, limitResult }` | + +### P2(中等优先级 — 代码质量与扩展性) + +| 编号 | 改进项 | 方向说明 | +|------|--------|---------| +| P2-1 | `ai-chart-renderer` `as` 断言替换为 Zod parse | 用 `AiChartSpecSchema.parse()` 校验 | +| P2-2 | `AiChatPanel` 拆分子组件 | 抽取 `ChatMessages` / `ChatInput` / `ChatEmptyState` | +| P2-3 | `content-safety` 增加中文关键词 | 扩展正则至中文场景 | +| P2-4 | `AiService.chat.usage` 修正 | 返回实际 tokenUsage 或改类型为 `null` | +| P2-5 | 架构图补齐 `/admin/ai-settings` 路由 | 005 routes 节点新增 | + +### 中长期方向(不在本次实施范围) + +- 接入 Redis 替换内存存储(需运维配合) +- 多模态输入(需 OCR/视觉模型) +- 教师 AI 对话审阅后台(需新增 DB 表) +- 多 Provider A/B 测试(需扩展 Provider 模型) +- 课程标准映射(需课标数据源) + +--- + +## 五、架构图同步说明 + +本次审计发现架构图需更新如下节点: + +### 5.1 `005_architecture_data.json` 同步项 + +1. **`modules.ai.exports.hooks`** 移除 `useAiChat`(死代码已删除) +2. **`modules.ai.exports.components`** 移除 `AiSuggestionCard`(死代码已删除) +3. **`modules.ai.integrations.parent-dashboard`** 更新 `page` 字段为真实接入路径 +4. **`modules.ai.integrations.student-learning`** 更新 `page` 字段为真实接入路径 `student/learning/study-path` +5. **`modules.ai.integrations.admin-dashboard`** 更正 `page` 为 `admin/ai-settings`(原误记为 `admin/ai-usage`) +6. **`routes./admin/ai-settings`** 新增节点(若 routes 表中确实缺失) +7. **`rolePermissionsSeed.parent`** 追加 `ai:chat` 权限 +8. **`modules.ai.i18n.v2Keys`** 追加 `error.statsFailed` 翻译键 + +### 5.2 `004_architecture_impact_map.md` 同步项 + +1. AI 模块章节的「集成点」表格更新实际接入路径 +2. 移除已删除组件的引用 + +--- + +## 六、本次实施清单 + +### 已实施(本次审计直接修复) + +| 编号 | 类型 | 改动 | +|------|------|------| +| P0-1 | 代码 | `permissions.ts` parent 角色追加 `AI_CHAT` | +| P0-2 | 代码 + 架构图 | 接入 `AiChildSummary` 到 parent/dashboard(`ParentDashboard` 新增 `aiSummarySlot`,page.tsx 为每个子女渲染 `AiChildSummary`);新建 `student/learning/study-path` 路由(page.tsx + loading.tsx + error.tsx)接入 `AiStudyPath`;同步 004/005 文档集成点与路由表 | +| P0-3 | 代码 + i18n | `actions.ts` 修复错误键;`ai.json` (zh/en) 新增 `error.statsFailed` | +| P1-2 | 代码 | `ai-usage-dashboard.tsx` 修复 useEffect | +| P1-3 | 代码 | `layout.tsx` service 移入函数体 | +| P1-5 | 代码 | 删除 `use-ai-chat.ts`;架构图 exports.hooks 移除 `useAiChat` | +| P1-6 | 代码 | 删除 `ai-suggestion-card.tsx`;架构图 exports.components 移除 `AiSuggestionCard` | +| P2-1 | 代码 | `ai-chart-renderer.tsx` 替换 `as` 为 Zod parse(新增 `AiChartSpecSchema`) | +| P2-4 | 代码 | `ai-service.ts` 修正 `usage` 字段返回实际 tokenUsage | +| 架构图 | 文档 | 004/005 同步上述改动;新增 `student.studyPath` i18n 块(zh/en) | +| 权限 | 文档 | `005` 的 `rolePermissionsSeed.parent` 追加 `AI_CHAT` | +| i18n | 文档 | `005` 的 `modules.ai.i18n.v2Keys` 追加 `error.statsFailed` | +| 路由 | 文档 | `005` 的 `routes` 表新增 `/student/learning/study-path`;更新 `/parent/dashboard` 描述含 AI 摘要集成;`/admin/ai-settings` 校正为 admin 集成路径(原误记为 `admin/ai-usage`) | + +### 未实施(中长期,需独立任务) + +| 编号 | 原因 | +|------|------| +| P1-1 Redis 替换内存 | 需运维提供 Redis 实例 | +| P1-4 prompt 完全 i18n 化 | 客户端 prompt 已被服务端覆盖,影响小 | +| P1-7 API 路由共享逻辑抽取 | 涉及测试回归,单独 PR | +| P2-2 AiChatPanel 拆分 | 涉及大量回归,单独 PR | +| P2-3 中文关键词扩展 | 需安全策略评审 | diff --git a/docs/architecture/audit/ai-module-deep-audit-report.md b/docs/architecture/audit/ai-module-deep-audit-report.md new file mode 100644 index 0000000..246564d --- /dev/null +++ b/docs/architecture/audit/ai-module-deep-audit-report.md @@ -0,0 +1,390 @@ +# AI 模块审计报告 V3 — 架构解耦与全角色对标 + +> 审计范围:基于 V1(`ai-module-audit-report.md`)与 V2(`ai-module-audit-report-v2.md`)已完成实现,进行第三轮深度架构审计。 +> 审计日期:2026-06-24 +> 审计方法:全文件逐行扫描 + 三层架构合规性矩阵 + 跨模块依赖图 + K12 行业标杆对标(Khanmigo / Duolingo Max / Squirrel AI / Century Tech / MagicSchool AI) +> 审计依据:`docs/standards/coding-standards.md`、`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、项目规则 + +--- + +## 一、现有实现概要 + +### 1.1 文件分布(30 个文件) + +| 层 | 文件 | 行数 | 职责 | +|------|------|------|------| +| **types** | `modules/ai/types.ts` | 295 | AiService/AiClientService 接口 + 8 个业务场景类型 | +| **schema** | `modules/ai/schema.ts` | 227 | Zod 验证(8 输入 + 8 输出) | +| **actions** | `modules/ai/actions.ts` | 381 | 9 个 Server Actions(含权限校验) | +| **data-access** | `modules/ai/data-access.ts` | 138 | AI 事件内存存储 + 统计聚合 | +| **services** | `modules/ai/services/ai-service.ts` | 439 | DefaultAiService 实现(8 方法) | +| **services** | `modules/ai/services/prompt-templates.ts` | 277 | 10 个系统提示词模板 | +| **services** | `modules/ai/services/usage-tracker.ts` | 99 | AI 使用量埋点 | +| **services** | `modules/ai/services/content-safety.ts` | 291 | 内容安全过滤(输入/输出/配额/Socratic) | +| **context** | `modules/ai/context/ai-client-provider.tsx` | 62 | React Context Provider + Hooks | +| **hooks** | `modules/ai/hooks/use-ai-chat-stream.ts` | 155 | 流式 AI 对话 Hook | +| **hooks** | `modules/ai/hooks/use-ai-chat.ts` | 57 | 非流式 AI 对话 Hook | +| **hooks** | `modules/ai/hooks/use-ai-suggestion.ts` | 72 | AI 建议 Hook | +| **hooks** | `modules/ai/hooks/use-floating-ball.ts` | 243 | 悬浮球拖拽 Hook | +| **hooks** | `modules/ai/hooks/stream-utils.ts` | 135 | SSE 流解析工具 | +| **components** | `modules/ai/components/ai-chat-panel.tsx` | 417 | AI 对话面板 | +| **components** | `modules/ai/components/ai-assistant-widget.tsx` | 329 | 全局 AI 助手悬浮球 | +| **components** | `modules/ai/components/ai-markdown-renderer.tsx` | 143 | Markdown 渲染器 | +| **components** | `modules/ai/components/ai-chart-renderer.tsx` | 329 | 图表渲染器 | +| **components** | `modules/ai/components/ai-error-boundary.tsx` | 88 | AI 错误边界 | +| **components** | `modules/ai/components/ai-skeleton.tsx` | 47 | 骨架屏 | +| **components** | `modules/ai/components/ai-provider-selector.tsx` | 89 | 服务商选择器 | +| **components** | `modules/ai/components/ai-grading-assist.tsx` | 173 | AI 批改辅助 | +| **components** | `modules/ai/components/ai-error-book-analysis.tsx` | 246 | 错题本 AI 分析 | +| **components** | `modules/ai/components/ai-lesson-content-generator.tsx` | 180 | 备课内容生成器 | +| **components** | `modules/ai/components/ai-question-variant-generator.tsx` | 218 | 题目变体生成器 | +| **components** | `modules/ai/components/ai-child-summary.tsx` | 186 | 家长学情摘要 | +| **components** | `modules/ai/components/ai-usage-dashboard.tsx` | 221 | 管理员使用统计 | +| **components** | `modules/ai/components/ai-study-path.tsx` | 200 | 学生学习路径 | +| **components** | `modules/ai/components/ai-suggestion-card.tsx` | 164 | 相似题建议卡片 | +| **api** | `app/api/ai/chat/route.ts` | 48 | 非流式聊天端点 | +| **api** | `app/api/ai/chat/stream/route.ts` | 237 | SSE 流式端点 | + +### 1.2 数据流 + +``` +客户端组件 + └─▶ useAiClient() / useAiClientOptional() + └─▶ AiClientProvider (app/layout.tsx 或子页面注入) + └─▶ Server Actions (modules/ai/actions.ts) + └─▶ createAiService(userId) → DefaultAiService + └─▶ createAiChatCompletion (shared/lib/ai) + └─▶ OpenAI SDK + ai_providers 表 + +SSE 流式端点(独立路径): + app/api/ai/chat/stream/route.ts + └─▶ requirePermission(AI_CHAT) + └─▶ content-safety (filterUserInput / tryConsumeDailyQuota) + └─▶ createAiChatCompletionStream (shared/lib/ai) + └─▶ content-safety (filterAiOutput / validateSocraticOutput) +``` + +### 1.3 架构图记录情况 + +- `004_architecture_impact_map.md` 第 2.29 节完整记录了 AI 模块(V2/V3/V4 变更) +- `005_architecture_data.json` 包含 `modules.ai` 节点(exports/dependencies/integrations/safety/streaming/i18n) +- **结论:架构图对 AI 模块的记录基本完整**,但未记录跨模块违规依赖(见 2.1.1)。 + +--- + +## 二、现存问题与原因分析 + +### 2.1 架构分层问题 + +#### 问题 2.1.1:5 处跨模块直接依赖 `shared/lib/ai`(绕过 modules/ai) + +- **位置**: + - [ai-suggest.ts:5](file:///e:/Desktop/CICD/src/modules/lesson-preparation/ai-suggest.ts#L5) — `import { createAiChatCompletion } from "@/shared/lib/ai"` + - [settings/actions.ts:14](file:///e:/Desktop/CICD/src/modules/settings/actions.ts#L14) — `import { encryptAiApiKey, getAiErrorMessage, testAiProviderById, testAiProviderConfig } from "@/shared/lib/ai"` + - [exams/actions.ts:987](file:///e:/Desktop/CICD/src/modules/exams/actions.ts#L987) — 动态 `import("@/shared/lib/ai")` + - [exams/ai-pipeline/request.ts:11](file:///e:/Desktop/CICD/src/modules/exams/ai-pipeline/request.ts#L11) — `import { createAiChatCompletion, getAiErrorMessage } from "@/shared/lib/ai"` + - [exams/ai-pipeline/parse.ts:11](file:///e:/Desktop/CICD/src/modules/exams/ai-pipeline/parse.ts#L11) — `import { createAiChatCompletion } from "@/shared/lib/ai"` +- **原因**:AI 模块在 V2 重构后才形成独立模块,但这些历史调用点未同步迁移。 +- **后果**:绕过 `modules/ai` 的内容安全过滤、每日配额、Socratic 模式、使用量埋点等保护机制;AI 调用无法统一治理。 +- **违反规则**:`项目规则 → 架构分层规则 → 模块间只能通过对方 data-access 通信`;`项目规则 → Server Action 规范 → 每个 Action 必须调用 requirePermission()`。 + +#### 问题 2.1.2:非流式聊天端点绕过 modules/ai + +- **位置**:[app/api/ai/chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts) +- **现状**:直接调用 `shared/lib/ai` 的 `createAiChatCompletion`,未走 `aiChatAction`,缺失内容安全过滤、每日配额、Socratic 模式等保护。 +- **后果**:与非流式端点(`/api/ai/chat/stream`)形成安全策略不一致;学生可通过非流式端点绕过 Socratic 模式获取直接答案。 +- **违反规则**:`项目规则 → 安全规范`;`项目规则 → Server Action 规范`。 + +#### 问题 2.1.3:4 个子页面重复创建 AiClientService + +- **位置**: + - [teacher/lesson-plans/[planId]/edit/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/lesson-plans/[planId]/edit/page.tsx) + - [teacher/homework/submissions/[submissionId]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/homework/submissions/[submissionId]/page.tsx) + - [student/error-book/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/error-book/page.tsx) + - [teacher/exams/[id]/build/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/exams/[id]/build/page.tsx) +- **现状**:每个页面各自创建只含 6 个 Action 的 `AiClientService`,覆盖 layout.tsx 的全局 Provider(含 9 个 Action)。 +- **后果**:代码重复;`generateChildSummary`、`recommendStudyPath`、`getAiUsageStats` 在这些页面内为 `undefined`,若未来组件调用将运行时错误。 +- **违反规则**:`项目规则 → 工程约定 → 最大化复用`。 + +### 2.2 权限问题 + +#### 问题 2.2.1:非流式聊天端点未走 requirePermission 体系 + +- **位置**:[app/api/ai/chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts) +- **现状**:虽然调用了 `requirePermission(Permissions.AI_CHAT)`,但绕过了 `modules/ai/actions.ts` 的 `aiChatAction`,导致内容安全过滤、每日配额、Socratic 模式等保护机制缺失。 +- **后果**:权限校验通过但安全策略不一致。 +- **违反规则**:`项目规则 → Server Action 规范 → 每个 Action 必须调用 requirePermission()`(虽调用但绕过 Action 编排层)。 + +### 2.3 国际化问题 + +#### 问题 2.3.1:ai-chart-renderer.tsx 硬编码中文 + +- **位置**:[ai-chart-renderer.tsx:147](file:///e:/Desktop/CICD/src/modules/ai/components/ai-chart-renderer.tsx#L147) +- **代码**:`图表数据格式错误,无法渲染` +- **后果**:无法切换语言。 +- **违反规则**:`项目规则 → 所有用户可见文本必须适配 i18n`。 + +#### 问题 2.3.2:ai-assistant-widget.tsx systemPrompt 硬编码英文 + +- **位置**:[ai-assistant-widget.tsx:230-326](file:///e:/Desktop/CICD/src/modules/ai/components/ai-assistant-widget.tsx#L230-L326) +- **现状**:`inferContextFromPath` 函数中所有 `systemPrompt` 和 `contextMessage` 为硬编码英文。 +- **判定**:systemPrompt 是发送给 AI 的指令(非用户可见文本),可保留英文(模型兼容性最佳);但 `contextMessage` 显示在 UI 中,应 i18n 化。 +- **后果**:contextMessage 无法国际化。 +- **违反规则**:`项目规则 → 所有用户可见文本必须适配 i18n`。 + +### 2.4 类型安全问题 + +#### 问题 2.4.1:ai-markdown-renderer.tsx 使用 as 断言 + +- **位置**:[ai-markdown-renderer.tsx:92](file:///e:/Desktop/CICD/src/modules/ai/components/ai-markdown-renderer.tsx#L92) +- **代码**:`lang.slice(CHART_LANG_PREFIX.length) as AiChartType` +- **后果**:若 lang 不在 AiChartType 枚举内,类型不安全。 +- **违反规则**:`项目规则 → TypeScript 规则 → 禁止 as 断言`。 + +#### 问题 2.4.2:ai-provider-selector.tsx 使用 as 断言 + +- **位置**:[ai-provider-selector.tsx:66](file:///e:/Desktop/CICD/src/modules/ai/components/ai-provider-selector.tsx#L66) +- **代码**:`field.value as string` +- **后果**:react-hook-form 的 field.value 类型应为泛型,此处强转。 +- **违反规则**:`项目规则 → TypeScript 规则 → 禁止 as 断言`。 + +#### 问题 2.4.3:ai-chart-renderer.tsx 使用 as 断言 + +- **位置**:[ai-chart-renderer.tsx:117](file:///e:/Desktop/CICD/src/modules/ai/components/ai-chart-renderer.tsx#L117) +- **代码**:`JSON.parse(data) as AiChartSpec` +- **后果**:JSON 结构不可信,直接断言可能运行时错误。 +- **违反规则**:`项目规则 → TypeScript 规则 → 禁止 as 断言`。 + +### 2.5 错误处理问题 + +#### 问题 2.5.1:ai-suggestion-card.tsx 未包裹 Error Boundary + +- **位置**:[ai-suggestion-card.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-suggestion-card.tsx) +- **现状**:组件内部有 try/catch 处理异步错误,但未用 `AiErrorBoundary` 包裹渲染期错误。 +- **后果**:若 `result.data` 结构异常或 `questions.map` 渲染时抛错,整页崩溃。 +- **违反规则**:`审计要求 → 每个独立数据区块必须用 React Error Boundary 包裹`。 + +#### 问题 2.5.2:非流式聊天端点错误处理不完整 + +- **位置**:[app/api/ai/chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts) +- **现状**:`getStatusFromError` 基于错误消息字符串匹配状态码,脆弱且不可靠。 +- **后果**:错误分类不准确。 +- **违反规则**:`项目规则 → 错误处理`。 + +### 2.6 文件大小问题 + +#### 问题 2.6.1:use-floating-ball.ts 超出 Hook 行数限制 + +- **位置**:[use-floating-ball.ts](file:///e:/Desktop/CICD/src/modules/ai/hooks/use-floating-ball.ts) +- **现状**:243 行,超出 Hook 文件 80 行建议上限 163 行。 +- **后果**:职责过多(位置加载/保存、拖拽事件、边缘吸附、半隐藏),难以维护和测试。 +- **违反规则**:`项目规则 → 单文件行数 → 自定义 Hook:建议 ≤ 80 行`。 + +### 2.7 可复用性问题 + +#### 问题 2.7.1:4 个子页面重复创建 AiClientService + +- 见问题 2.1.3。 + +#### 问题 2.7.2:ai-suggestion-card.tsx 未被使用 + +- **位置**:[ai-suggestion-card.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-suggestion-card.tsx) +- **现状**:组件已实现但无任何引用。 +- **后果**:死代码,维护负担。 +- **违反规则**:`审计要求 → 最大化复用`(应集成到错题本等页面)。 + +### 2.8 监控问题 + +#### 问题 2.8.1:非流式聊天端点无使用量埋点 + +- **位置**:[app/api/ai/chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts) +- **现状**:未调用 `trackAiUsage`,调用数据不入统计。 +- **后果**:管理员仪表盘数据不完整。 +- **违反规则**:`审计要求 → 监控`。 + +--- + +## 三、行业差距对比 + +### 3.1 与 Khanmigo(Khan Academy)的差距 + +| 功能 | Khanmigo | 本项目 | 差距 | +|------|----------|--------|------| +| 学生 Socratic 模式 | ✅ 强制引导式 | ✅ 已实现 | 无 | +| 教师备课助手 | ✅ 课程计划生成 | ✅ 已实现 | 无 | +| 多语言支持 | ✅ 20+ 语言 | ⚠️ 仅中英文 | 缺少多语言 Prompt | +| 学生情绪识别 | ✅ 检测挫败感 | ❌ 未实现 | 缺少情绪分析 | +| 家长沟通建议 | ✅ 家庭教育指导 | ✅ 已实现 | 无 | + +### 3.2 与 Duolingo Max 的差距 + +| 功能 | Duolingo Max | 本项目 | 差距 | +|------|-------------|--------|------| +| 解释我的答案 | ✅ AI 解释错误原因 | ❌ 未实现 | 缺少错题 AI 解释 | +| 角色扮演练习 | ✅ 情景对话练习 | ❌ 未实现 | 缺少口语/情景练习 | +| 个性化复习 | ✅ 基于遗忘曲线 | ⚠️ 仅 SM2 算法 | AI 未参与复习规划 | + +### 3.3 与 Squirrel AI 的差距 + +| 功能 | Squirrel AI | 本项目 | 差距 | +|------|-------------|--------|------| +| 纳米级知识图谱 | ✅ 10000+ 知识点 | ⚠️ V3 已集成 | 知识图谱粒度较粗 | +| 自适应学习路径 | ✅ 实时调整 | ✅ V3 已实现 | 无 | +| 多模态学习 | ✅ 视频+图文+音频 | ❌ 仅文本 | 缺少多模态 | +| 学习风格识别 | ✅ VARK 模型 | ❌ 未实现 | 缺少学习风格分析 | + +### 3.4 与 MagicSchool AI 的差距 + +| 功能 | MagicSchool AI | 本项目 | 差距 | +|------|----------------|--------|------| +| 50+ AI 工具 | ✅ 丰富工具集 | ⚠️ 8 个能力 | 工具数量不足 | +| IEP 生成 | ✅ 特殊教育计划 | ❌ 未实现 | 缺少 IEP | +| 家校沟通模板 | ✅ 邮件/通知模板 | ❌ 未实现 | 缺少沟通模板 | +| 跨学科项目设计 | ✅ PBL 项目设计 | ❌ 未实现 | 缺少 PBL | + +### 3.5 关键差距总结 + +1. **AI 能力数量不足**:仅 8 个能力,行业平均 15-20 个 +2. **多模态缺失**:仅支持文本,缺少图像/语音/视频输入 +3. **学习风格识别缺失**:未识别学生 VARK 学习风格 +4. **情绪识别缺失**:未检测学生挫败感/兴奋度 +5. **IEP/PBL 缺失**:未支持特殊教育和项目式学习 +6. **跨模块数据联动不足**:AI 未与考勤、行为、心理等数据联动分析 + +--- + +## 四、改进优先级建议 + +### P0(紧急 — 安全/架构合规) + +| 编号 | 问题 | 改进方向 | +|------|------|---------| +| P0-1 | 5 处跨模块直接依赖 shared/lib/ai | 迁移至通过 modules/ai 的 data-access 或 actions 调用 | +| P0-2 | 非流式聊天端点绕过 modules/ai | 重构为调用 aiChatAction 或迁移安全策略 | +| P0-3 | ai-chart-renderer.tsx 硬编码中文 | 迁移至 i18n | +| P0-4 | 3 处 as 断言 | 改用类型守卫函数 | + +### P1(重要 — 规范/复用) + +| 编号 | 问题 | 改进方向 | +|------|------|---------| +| P1-1 | 4 个子页面重复创建 AiClientService | 提取 createAiClientService 工厂函数 | +| P1-2 | use-floating-ball.ts 超出行数限制 | 拆分为 use-drag-position + use-edge-snap | +| P1-3 | ai-suggestion-card.tsx 未包裹 Error Boundary | 集成时用 AiErrorBoundary 包裹 | +| P1-4 | ai-assistant-widget.tsx contextMessage 硬编码 | 迁移至 i18n | +| P1-5 | 非流式端点无使用量埋点 | 集成 trackAiUsage | + +### P2(增强 — 行业对标) + +| 编号 | 问题 | 改进方向 | +|------|------|---------| +| P2-1 | 缺少错题 AI 解释 | 新增 explainErrorAction | +| P2-2 | 缺少学习风格识别 | 新增 VARK 评估 | +| P2-3 | 缺少 IEP 生成 | 新增特殊教育模块 | +| P2-4 | 缺少家校沟通模板 | 新增沟通模板生成 | +| P2-5 | 缺少多模态输入 | 支持图像/语音输入 | +| P2-6 | 缺少情绪识别 | 集成情绪分析 API | + +--- + +## 五、架构图同步说明 + +### 5.1 需补充的节点 + +1. **跨模块违规依赖**:在 `005_architecture_data.json` 的 `dependencyMatrix` 中标注 `lesson-preparation`、`settings`、`exams` 对 `shared/lib/ai` 的违规依赖(应改为通过 `modules/ai`)。 +2. **非流式端点安全策略缺失**:在 `004_architecture_impact_map.md` 的 AI 模块安全机制章节补充非流式端点的安全策略差距。 +3. **ai-suggestion-card.tsx 未使用**:在文件清单中标注该组件为"已实现未集成"。 + +### 5.2 需修改的节点 + +1. **V4 超时优化**:已在 V4 中记录(`shared/lib/ai/client.ts` 按场景分离超时)。 +2. **V4 规范修复**:已在 V4 中记录(ai-service.ts 消除 as 断言、actions.ts 消除非空断言、ai-assistant-widget.tsx i18n 修复)。 + +--- + +## 六、实施记录 + +### 6.1 P0 改进实施 + +#### P0-1:迁移跨模块依赖(部分) + +**已实施**: +- 修复 `ai-chart-renderer.tsx` 硬编码中文(P0-3) +- 修复 `ai-markdown-renderer.tsx` as 断言(P0-4 部分) +- 修复 `ai-provider-selector.tsx` as 断言(P0-4 部分) +- 修复 `ai-chart-renderer.tsx` as 断言(P0-4 部分) + +**未实施(需中长期计划)**: +- 5 处跨模块直接依赖 shared/lib/ai 的迁移(涉及 exams/lesson-preparation/settings 三个模块的重构,影响范围大,需单独排期) + +#### P0-2:非流式聊天端点安全策略重构(已实施) + +**实施**:重构 `app/api/ai/chat/route.ts`,与流式端点 `/api/ai/chat/stream` 安全策略完全对齐: +- Zod 校验输入(`AiChatInputSchema`,限制消息数 50/长度 8000) +- `tryConsumeDailyQuota` 原子化每日限额(防 TOCTOU 竞态) +- `filterUserInput` 输入安全过滤 +- `filterAiOutput` 输出安全过滤 +- 学生侧 Socratic 模式(服务端强制 `SOCRATIC_TUTOR_SYSTEM_PROMPT`,忽略客户端 systemPrompt) +- `validateSocraticOutput` 苏格拉底式输出校验 +- 过滤/失败时 `refundDailyQuota`(不惩罚用户) +- `trackEvent` 使用量埋点(成功/失败均记录) + +#### P0-3:ai-chart-renderer.tsx 硬编码中文(已实施) + +**实施**:添加 i18n 键 `ai.chart.parseError`,替换硬编码中文。 + +#### P0-4:as 断言修复(已实施) + +**实施**: +- `ai-markdown-renderer.tsx`:改用类型守卫函数 `isAiChartType` +- `ai-provider-selector.tsx`:改用 `String(field.value ?? "")` +- `ai-chart-renderer.tsx`:改用 Zod schema 校验 + +### 6.2 P1 改进实施 + +#### P1-1:提取 createAiClientService 工厂(已实施) + +**实施**:新建 `modules/ai/context/create-ai-client-service.ts`,导出 `createFullAiClientService`(含全部 9 个 Action)和 `createCoreAiClientService`(仅 6 个常用 Action)两个工厂函数。`app/(dashboard)/layout.tsx` 使用前者,4 个子页面(error-book、lesson-plans、homework、exams)使用后者,消除重复代码。 + +#### P1-2:拆分 use-floating-ball.ts(已实施) + +**实施**:将 243 行的 `use-floating-ball.ts` 拆分为 3 个文件: +- `use-position-persistence.ts`:Position 类型、常量、clamp/load/save 纯函数、位置状态 Hook(localStorage + resize 校正) +- `use-drag-position.ts`:拖拽状态 + pointer 事件处理 Hook(通过回调委托业务逻辑) +- `use-floating-ball.ts`:主组合 Hook(边缘吸附 + 半隐藏 + hovered 状态 + show/resetPosition) + +#### P1-3:ai-suggestion-card.tsx Error Boundary(已实施) + +**实施**:将原组件重命名为 `AiSuggestionCardInner`,新建 `AiSuggestionCard` 包装器用 `AiErrorBoundary` 包裹内部组件,保持公开 API 不变。 + +#### P1-4:ai-assistant-widget.tsx contextMessage i18n(已实施) + +**实施**:在 `en/ai.json` 和 `zh-CN/ai.json` 中添加 `chat.contextMessage.*` 翻译键(7 个场景:teacherGrading/teacherLesson/teacherExam/studentErrorBook/studentHomework/parent/admin),替换 `inferContextFromPath` 中 7 处硬编码英文。 + +#### P1-5:非流式端点使用量埋点(已实施) + +**实施**:在 `app/api/ai/chat/route.ts` 中集成 `trackEvent`(事件名 `ai.chat`),成功时记录 durationMs/tokenCount,失败时记录 errorMessage/durationMs。与流式端点(`ai.chat_stream`)对齐。 + +### 6.3 P2 改进(中长期计划) + +#### P2-1:错题 AI 解释(已实施) + +**实施**: +- 新增 `ExplainErrorInput`/`ExplainErrorResult` 类型(`modules/ai/types.ts`) +- 新增 `ExplainErrorInputSchema`/`ExplainErrorResultSchema` Zod 校验(`modules/ai/schema.ts`) +- 新增 `EXPLAIN_ERROR_SYSTEM_PROMPT` 提示词(`modules/ai/services/prompt-templates.ts`) +- 新增 `explainError` 服务方法(`modules/ai/services/ai-service.ts`) +- 新增 `explainErrorAction` Server Action(`modules/ai/actions.ts`,权限:AI_CHAT + ERROR_BOOK_READ) +- 更新 `AiService`/`AiClientService` 接口,添加 `explainError` 方法 +- 更新 `createFullAiClientService` 工厂函数包含 `explainError` +- 更新 `AiCapability` 类型添加 `"explain-error"` +- 更新 `AiUsageEvent`/`AI_EVENT_MAP` 添加 `explain_error` 埋点 +- 更新 i18n 翻译键 `capability.explainError`(en/zh-CN) +- 更新架构文档 004/005 + +**未实施(需后续排期)**: +- P2-2:VARK 学习风格评估(需新增评估模块 + DB 表) +- P2-3:IEP 特殊教育计划生成(需新增特殊教育模块) +- P2-4:家校沟通模板生成(需新增模板管理模块) +- P2-5:多模态输入支持(需接入图像/语音 API) +- P2-6:情绪识别(需接入情绪分析 API) diff --git a/docs/architecture/audit/announcements-audit-report.md b/docs/architecture/audit/announcements-audit-report.md new file mode 100644 index 0000000..a3a9071 --- /dev/null +++ b/docs/architecture/audit/announcements-audit-report.md @@ -0,0 +1,392 @@ +# 公告(announcements)模块审计报告 + +> 审查日期:2026-06-25 +> 审查范围:`src/modules/announcements/**`、`src/app/(dashboard)/announcements/**`、`src/app/(dashboard)/admin/announcements/**`、`src/shared/i18n/messages/{zh-CN,en}/announcements.json` +> 架构图参考:`docs/architecture/004_architecture_impact_map.md` §2.16、`docs/architecture/005_architecture_data.json#announcements` +> 关联报告:`announcements-messages-audit-report.md`(合并版,2026-06-22,已不再维护,本文为公告模块的独立深度审计) + +--- + +## 一、现有实现概要 + +### 1.1 文件分布 + +| 层 | 路径 | 文件 | 行数 | 说明 | +|----|------|------|------|------| +| 路由 · 用户端 | `src/app/(dashboard)/announcements/page.tsx` | 1 | 36 | 列表页(所有非管理角色共用) | +| 路由 · 用户端 | `src/app/(dashboard)/announcements/[id]/page.tsx` | 1 | 41 | 详情页(只读) | +| 路由 · 用户端 | `src/app/(dashboard)/announcements/{loading,error,[id]/error}.tsx` | 3 | 60 | 骨架屏 + 错误边界 | +| 路由 · 管理端 | `src/app/(dashboard)/admin/announcements/page.tsx` | 1 | 45 | 管理列表页 | +| 路由 · 管理端 | `src/app/(dashboard)/admin/announcements/[id]/page.tsx` | 1 | 47 | 编辑页(直接渲染表单,无详情视图) | +| 路由 · 管理端 | `src/app/(dashboard)/admin/announcements/{loading,error,[id]/error}.tsx` | 3 | 64 | 骨架屏 + 错误边界 | +| 模块 | `src/modules/announcements/actions.ts` | 1 | 403 | 9 个 Server Action + 通知编排 + 埋点 | +| 模块 | `src/modules/announcements/data-access.ts` | 1 | 413 | CRUD + 发布/归档 + 置顶/已读 + 3 个页面编排函数 | +| 模块 | `src/modules/announcements/types.ts` | 1 | 75 | 类型定义 | +| 模块 | `src/modules/announcements/schema.ts` | 1 | 95 | Zod 校验 + `refineAudience` 条件校验 | +| 模块 · 组件 | `src/modules/announcements/components/announcement-list.tsx` | 1 | 126 | 列表(纯服务端过滤) | +| 模块 · 组件 | `src/modules/announcements/components/announcement-card.tsx` | 1 | 125 | 卡片 + 置顶切换 | +| 模块 · 组件 | `src/modules/announcements/components/announcement-detail.tsx` | 1 | 267 | 详情 + 管理操作 + 自动已读 | +| 模块 · 组件 | `src/modules/announcements/components/announcement-form.tsx` | 1 | 230 | 创建/编辑表单 | +| 模块 · 组件 | `src/modules/announcements/components/admin-announcements-view.tsx` | 1 | 67 | 管理端视图(列表 + 创建 Dialog) | +| i18n | `src/shared/i18n/messages/{zh-CN,en}/announcements.json` | 2 | 103/103 | 11 命名空间翻译字典 | +| 测试 | — | 0 | 0 | **零测试文件** | + +文件大小均在规范内(组件 ≤500 行,actions/data-access ≤800 行)。 + +### 1.2 数据流 + +``` +[Route] /announcements/page.tsx + └─▶ announcements/data-access.getUserAnnouncementsPageData(userId, dataScope) + ├─▶ resolveAudience(userId, dataScope) // 内部函数 + │ └─▶ classes/data-access.{getClassGradeId | getStudentActiveClassId | getStudentActiveGradeId} + └─▶ getAnnouncements({ status: "published", audience }) + +[Route] /announcements/[id]/page.tsx ⚠️ 未做受众/状态过滤 + ├─▶ announcements/data-access.getAnnouncementById(id) + └─▶ announcements/data-access.isAnnouncementReadByUser(id, userId) + +[Route] /admin/announcements/page.tsx + └─▶ announcements/data-access.getAdminAnnouncementsPageData(status) + ├─▶ getAnnouncements({ status }) + ├─▶ school/data-access.getGrades() + └─▶ classes/data-access.getAdminClasses() + +[Route] /admin/announcements/[id]/page.tsx + └─▶ announcements/data-access.getEditAnnouncementPageData(id) + ├─▶ getAnnouncementById(id) + └─▶ school/data-access.getGrades() + +[Action] createAnnouncementAction / updateAnnouncementAction / publishAnnouncementAction + └─▶ notifyAnnouncementPublished(announcement) + ├─▶ resolveTargetUserIds(announcement) // ⚠️ 纯业务逻辑在 actions.ts + │ ├─▶ users/data-access.{getAllUserIds | getUserIdsByGradeId} + │ └─▶ classes/data-access.{getStudentIdsByClassId | getTeacherIdsByClassIds} + └─▶ notifications.sendBatchNotifications(payloads) +``` + +### 1.3 架构图记录情况 + +`004_architecture_impact_map.md` §2.16 对 announcements 模块的记录较为完整: +- ✅ 导出函数(9 个 Action + 14 个 data-access 函数)记录准确 +- ✅ 依赖关系(`shared/*`、`@/auth`、`school`、`classes`、`users`、`notifications`)记录准确 +- ✅ 已修复问题清单(P1-2/P1-5/P1-6/V2-P0-2/V2-P1-1/V2-P1-4/V2-P2-13d/V3-P0-2)记录详实 +- ✅ 文件清单与组件清单行数准确 + +**但架构图存在以下遗漏/不一致**(详见第五章): +1. 未记录 `AnnouncementDetail` 组件中 `canManage=true` 分支为**死代码**(无任何页面使用) +2. 未记录 `getAnnouncementReadStatusAction` 为**死代码**(无任何调用方) +3. 未记录 `/announcements/[id]` 路由层存在的**安全越权风险**(无受众过滤) +4. 未记录 `resolveAudience` 内部函数的**多孩子/多年级数据截断 Bug** +5. 未记录 actions 返回的英文字符串未走 i18n 的问题 + +--- + +## 二、现存问题与原因分析 + +### 2.1 【P0 · 安全越权】用户端详情页无受众/状态过滤 + +- **位置**:[src/app/(dashboard)/announcements/[id]/page.tsx:26-27](file:///e:/Desktop/CICD/src/app/(dashboard)/announcements/[id]/page.tsx) +- **问题**:详情页直接调用 `getAnnouncementById(id)`,未传入 `audience` 也未校验 `status === "published"`。 +- **后果**:任意持有 `ANNOUNCEMENT_READ` 权限的登录用户,只要知道/猜到公告 ID(cuid2),即可读取: + - 草稿(`status="draft"`)公告——提前泄露未发布内容 + - 已归档(`status="archived"`)公告——绕过归档语义 + - 其他年级/班级的定向公告——跨班级信息泄露(如某班处分通知被外班学生读到) +- **违反规则**:项目规则"安全性:所有敏感数据查询必须在 data-access 层结合当前用户权限过滤" + "Parent routes must include permission checks with both `parentId` and `studentId` to prevent information leakage"。 +- **根因**:`getAnnouncementById` 设计为通用读取函数,未提供"按受众过滤"重载;路由层也未在读取后做二次校验。 + +### 2.2 【P0 · 数据截断】`resolveAudience` 仅取首个 gradeId / classId / childId + +- **位置**:[src/modules/announcements/data-access.ts:351-395](file:///e:/Desktop/CICD/src/modules/announcements/data-access.ts) +- **问题**: + ```ts + if (dataScope.type === "grade_managed") { + const gradeId = dataScope.gradeIds[0] // ⚠️ 仅取第一个 + } + if (dataScope.type === "class_members" || dataScope.type === "class_taught") { + const classId = dataScope.classIds[0] // ⚠️ 仅取第一个 + } + if (dataScope.type === "children") { + const childId = dataScope.childrenIds[0] // ⚠️ 仅取第一个孩子 + } + ``` +- **后果**: + - **家长**有多个孩子在不同班级/年级时,只能看到第一个孩子的定向公告,第二个孩子的班主任通知完全不可见——直接违反 K12 家长端核心诉求。 + - **年级主任**管理多个年级时,只能看到第一个年级的公告。 + - **教师**任课多个班级时,只能看到第一个班级的公告。 +- **违反规则**:项目规则"Parent routes must include permission checks with both `parentId` and `studentId`" 与"data-access 层结合当前用户权限过滤"。 +- **根因**:`getAnnouncements` 的 `audience` 参数设计为单值 `{ gradeId?, classId? }`,不支持多值;`resolveAudience` 为迁就该签名做了截断。 + +### 2.3 【P0 · 越权写】置顶/已读 Action 缺少资源所有权二次校验 + +- **位置**:[src/modules/announcements/actions.ts:335-376](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts) +- **问题**: + - `toggleAnnouncementPinAction` 仅校验 `ANNOUNCEMENT_MANAGE`,未校验公告是否存在、未校验调用者是否为该公告作者或管理员范围。 + - `markAnnouncementAsReadAction` 仅校验 `ANNOUNCEMENT_READ`,未校验该公告是否对当前用户可见(即未结合 2.1 的受众过滤)。任意用户可对任意公告 ID(包括草稿、他人班级公告)写入已读记录,污染 `announcement_reads` 表。 +- **后果**:数据库完整性被破坏;统计 `readCount` 失真;为后续基于已读率的分析埋下错误数据。 +- **违反规则**:项目规则"Server Action 二次校验"。 +- **根因**:Action 层信任了 `requirePermission` 的角色校验,未做资源级(resource-level)授权。 + +### 2.4 【P1 · i18n 违规】Actions 返回英文硬编码消息 + +- **位置**:[src/modules/announcements/actions.ts](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts) 全文 +- **问题**:所有 Action 返回的 `ActionState.message` 均为英文字符串: + - `"Announcement created"` / `"Announcement updated"` / `"Announcement deleted"` + - `"Announcement published"` / `"Announcement archived"` + - `"Announcement not found"` / `"Invalid form data"` / `"Unexpected error"` + - `"Pin status toggled"` / `"Announcement marked as read"` +- 这些 message 通过 `toast.success(res.message)` / `toast.error(res.message)` 直接展示给用户(见 [announcement-detail.tsx:80,100,115](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-detail.tsx) 与 [announcement-form.tsx:80,88](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-form.tsx))。 +- **后果**:中文用户在创建/发布/删除公告后看到英文 Toast,i18n 字典中已定义的 `messages.created` / `messages.updated` 等翻译键完全未使用。 +- **违反规则**:项目规则"所有用户可见文本必须适配 i18n" + "Server Action 返回值统一采用 `ActionState` 类型"(隐含 message 应可本地化)。 +- **根因**:Actions 在 try 块内同步返回字符串,未通过 `getTranslations("announcements")` 获取本地化文案;i18n 字典定义了键但 Action 未消费。 + +### 2.5 【P1 · 死代码】`AnnouncementDetail` 管理分支与 `getAnnouncementReadStatusAction` 无调用方 + +- **位置**: + - [src/modules/announcements/components/announcement-detail.tsx:165-200](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-detail.tsx)(`canManage` 为 true 时的发布/归档/删除/置顶/编辑按钮组) + - [src/modules/announcements/actions.ts:381-391](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts)(`getAnnouncementReadStatusAction`) +- **问题**: + - 全仓搜索 `AnnouncementDetail` 的使用方,仅 [src/app/(dashboard)/announcements/[id]/page.tsx:34-38](file:///e:/Desktop/CICD/src/app/(dashboard)/announcements/[id]/page.tsx) 一处,且 `canManage={false}`。管理端 `/admin/announcements/[id]` 直接渲染 `AnnouncementForm`(编辑模式),**没有管理端详情页**。 + - 全仓搜索 `getAnnouncementReadStatusAction`,**零调用方**。该 Action 返回 `Record`,本应用于列表页批量标记已读/未读,但列表页从未调用。 +- **后果**: + - 管理员无法在 UI 中执行发布/归档/删除/置顶操作(除非进入编辑表单),严重限制了管理端可用性。 + - `announcement.readCount` 字段在 `AnnouncementDetail` 中展示,但因 `canManage` 永远为 false,**用户永远看不到已读人数**——已读统计功能在 UI 层完全不可见。 + - 列表页公告卡片没有"已读/未读"视觉区分,已读回执的数据无法驱动 UI。 +- **违反规则**:项目规则"避免 backwards-compatibility hacks ... 如果确定未使用,应完全删除" + "识别四个角色共用的 UI 块"。 +- **根因**:管理端路由设计遗漏了详情视图;已读状态查询 Action 未被列表组件消费。 + +### 2.6 【P1 · 耦合】组件直接 import actions,未通过 Context/Provider 注入 + +- **位置**: + - [announcement-card.tsx:13](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-card.tsx):`import { toggleAnnouncementPinAction } from "../actions"` + - [announcement-detail.tsx:25-31](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-detail.tsx):`import { archiveAnnouncementAction, deleteAnnouncementAction, markAnnouncementAsReadAction, publishAnnouncementAction, toggleAnnouncementPinAction } from "../actions"` + - [announcement-form.tsx:21](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-form.tsx):`import { createAnnouncementAction, updateAnnouncementAction } from "../actions"` +- **问题**:组件硬编码依赖具体 Server Action,无法在不修改组件代码的前提下替换为 mock 实现。 +- **后果**: + - 组件不可单元测试(必须 mock 整个 `../actions` 模块)。 + - 无法为不同角色注入不同实现(如家长端只读、教师端可编辑班级公告)。 + - 未来若要将公告组件复用于"班级空间"或"家长端聚合页",必须重写组件。 +- **违反规则**:用户要求"完全解耦:通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access"。 +- **根因**:组件设计未遵循依赖注入原则。 + +### 2.7 【P1 · 耦合】`AnnouncementForm` 硬编码路由跳转 + +- **位置**:[announcement-form.tsx:82,217](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-form.tsx) +- **问题**:表单提交成功后 `router.push("/admin/announcements")`,取消按钮也跳转到 `/admin/announcements`。 +- **后果**:表单无法在管理端以外的场景复用(如教师端发布班级公告、嵌入到班级详情页的快速发布公告入口)。 +- **违反规则**:用户要求"组合优先 ... 逻辑复用一律抽取为自定义 hooks" + "最大化复用"。 +- **根因**:表单未通过 `onSuccess` / `onCancel` 回调或 `successHref` prop 解耦导航。 + +### 2.8 【P1 · 业务逻辑位置】`resolveTargetUserIds` 放在 actions.ts + +- **位置**:[src/modules/announcements/actions.ts:48-66](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts) +- **问题**:受众解析 + 用户 ID 聚合是纯业务逻辑(无 I/O 副作用之外的逻辑),却放在 Server Action 文件中,与 Action 编排逻辑混杂。 +- **后果**: + - 无法独立单元测试(必须 mock `getAllUserIds` / `getStudentIdsByClassId` 等跨模块 data-access)。 + - 与 `data-access.ts` 中的 `resolveAudience` 形成两套受众解析逻辑,职责重叠。 +- **违反规则**:项目规则"可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks,与 UI 分离" + "Server Actions / Data Access 模块:建议 ≤ 800 行 ... 超过应考虑拆分"。 +- **根因**:actions.ts 既承担 HTTP 编排又承担业务规则,未分离 service 层。 + +### 2.9 【P1 · 性能】`toggleAnnouncementPin` 与 `markAnnouncementAsRead` 非原子操作 + +- **位置**:[src/modules/announcements/data-access.ts:210-224,234-250](file:///e:/Desktop/CICD/src/modules/announcements/data-access.ts) +- **问题**: + - `toggleAnnouncementPin`:先 `SELECT isPinned`,再 `UPDATE`。两次 DB 往返,且在并发场景下存在 lost update(两个管理员同时切换会得到错误结果)。 + - `markAnnouncementAsRead`:先 `SELECT id`,再 `INSERT`。已有唯一索引保证幂等,但多一次 SELECT 浪费往返。 +- **后果**:高并发时数据不一致;DB 负载翻倍。 +- **违反规则**:项目规则"性能:优先使用 React Server Components"(隐含高效数据访问)。 +- **根因**:未使用 Drizzle 的 `sql` 表达式或 `onDuplicateKeyUpdate`/`INSERT IGNORE` 语义。 + +### 2.10 【P1 · 错误处理】`handleActionError` 吞错误上下文 + +- **位置**:[src/modules/announcements/actions.ts:34-40](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts) +- **问题**: + ```ts + function handleActionError(e: unknown): ActionState { + if (e instanceof PermissionDeniedError) return { success: false, message: e.message } + if (e instanceof Error) return { success: false, message: e.message } + return { success: false, message: "Unexpected error" } + } + ``` + - 未 `console.error` 记录错误堆栈,生产环境无法定位故障。 + - 直接把 `e.message` 返回给前端,可能泄露内部错误信息(如 SQL 错误)。 + - "Unexpected error" 为英文硬编码。 +- **后果**:可观测性差;安全信息泄露风险。 +- **违反规则**:项目规则"错误与边界处理" + "i18n 就绪"。 +- **根因**:错误处理未与日志/埋点/i18n 集成。 + +### 2.11 【P2 · a11y】置顶按钮嵌套在 `` 内的键盘交互问题 + +- **位置**:[announcement-card.tsx:80-92,116-121](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-card.tsx) +- **问题**:`AnnouncementCard` 在有 `href` 时用 `` 包裹整个卡片,同时卡片内的"置顶"按钮是一个 `