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
This commit is contained in:
SpecialX
2026-07-03 10:23:34 +08:00
parent 365c36d97b
commit 89b9e181d2
29 changed files with 13009 additions and 230 deletions

View File

@@ -0,0 +1,475 @@
# 考试exams模块审计报告
> 审计时间2026-06-25
> 审计范围:`src/modules/exams/**`35+ 文件)+ `src/app/(dashboard)/teacher/exams/**`10 个页面)+ 跨模块依赖面
> 审计依据:`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、项目 `project_rules.md`
---
## 一、现有实现概要
### 1.1 文件分布与体量
exams 模块按职责已做较细粒度拆分,体量基本符合规范:
| 子目录/文件 | 行数(参考架构图) | 职责 |
|-------------|------|------|
| `actions.ts` | 633 | 11 个核心 Server Action已从 1525 行拆分) |
| `actions-helpers.ts` | 96 | 跨 Action 共享纯函数prepareExamCreateContext 等) |
| `actions-rich-editor.ts` | 250 | 富文本编辑器 Server Actioncreate/update |
| `ai-pipeline/auto-mark.ts` | 356 | AI 自动标记 Server Action + 纯转换函数 |
| `ai-pipeline/{index,parse,request,structure}.ts` | — | AI 调用/解析/结构化 |
| `data-access.ts` | 542 | 考试 CRUD已从 1036 行拆分) |
| `data-access-cross-module.ts` | 511 | 13 个跨模块查询/写接口 |
| `data-access-error-collection.ts` | — | 错题采集相关跨模块接口 |
| `stats-service.ts` | 158 | 考试分析数据聚合 |
| `types.ts` | 93 | 类型定义 |
| `utils/normalize-structure.ts` | 57 | exam.structure 运行时归一化 |
| `components/` | 24 个文件 | 表单/组卷/预览/分析/卡片/筛选/表格 |
| `editor/` | 14 个文件 | Tiptap 富文本编辑器extensions/utils/转换) |
| `hooks/` | 4 个文件 | use-exam-preview 主组合器 + 3 个子 Hook |
**架构图覆盖情况**004/005 已记录 exams 模块的职责、依赖、被依赖、文件清单、P0/P1 修复历史、V3 增强项。本次审计对照架构图核对,覆盖基本完整,但以下细节需补全(见第五节):
- `data-access-error-collection.ts` 未在 005 JSON 的 modules.exams.exports 中列出
- `utils/normalize-structure.ts` 已记录但未在 005 的 dependencyMatrix 中明确标注被 `[id]/build/page.tsx``[id]/edit-rich/page.tsx` 引用
### 1.2 主要数据流
- **创建**`/teacher/exams/create``createExamAction``persistExamDraft``db.insert(exams)`
- **AI 创建**`/teacher/exams/create``createAiExamAction``loadAiDraftQuestionsAndStructure``persistAiGeneratedExamDraft` → 通过 `questions/data-access.createQuestionWithRelations` 创建题目 → 事务写 exams + examQuestions
- **富文本创建**`/teacher/exams/new``createExamFromRichEditorAction``editorDocToStructure``persistAiGeneratedExamDraft`
- **组卷**`/teacher/exams/[id]/build``ExamAssembly` + `getExamById`
- **预览**`previewAiExamAction` / `getExamPreviewAction`
- **分析**`/teacher/exams/[id]/analytics``getExamAnalytics`(聚合 homework 提交数据)
### 1.3 跨模块依赖(合规项)
以下跨模块调用均通过对方 data-access符合三层架构规则
- `questions/data-access.createQuestionWithRelations`P0-1 已修复)
- `classes/data-access.getClassGradeIdsByClassIds`P0-2 已修复)
- `school/data-access.{getSubjectNameById,getGradeNameById,getSubjectOptions,getGradeOptions}`P1-1 已修复)
- `homework/data-access.{getHomeworkAssignmentsByExamId,getGradedSubmissionsByExamId}`V3-8 新增)
- `homework/data-access-utils.getQuestionText`
### 1.4 已修复的历史问题(架构图记录)
P0-1/P0-2/P0-4/P0-8/P1-1 等历史违规已修复,详见 004 文档第 2.2 节。
---
## 二、现存问题与原因分析
### 🔴 2.1【架构违规·P0】跨模块直接 JOIN questions 表
**位置**[data-access-cross-module.ts](file:///e:/Desktop/CICD/src/modules/exams/data-access-cross-module.ts#L4-L5) 第 4 行 import、第 480-490 行 `getExamForGradeEntry`
**问题**
```
第 4 行import { exams, examQuestions, examSubmissions, submissionAnswers, questions } from "@/shared/db/schema"
第 488 行:.innerJoin(questions, eq(examQuestions.questionId, questions.id))
```
`getExamForGradeEntry` 为了获取题目 `type` 字段,直接 JOIN 了 questions 模块的核心表 `questions`
**违反规则**:项目规则"模块间只能通过对方 data-access 通信,**禁止跨模块直接查询数据库表**"。
**原因**:成绩录入表格表头需要题目类型,但实现时未在 questions 模块暴露按 ID 批量获取类型的接口,于是直接 JOIN。
**直接后果**questions 模块若重构表结构(如将 type 拆分到独立表exams 模块会编译失败或运行时错误;模块封装性被破坏,违反可测试性与可替换性。
---
### 🟢 2.2【已确认合规】submissionAnswers 表归属与直查
**位置**
- [data-access-cross-module.ts](file:///e:/Desktop/CICD/src/modules/exams/data-access-cross-module.ts#L4) 导入 `submissionAnswers`
- [data-access-cross-module.ts](file:///e:/Desktop/CICD/src/modules/exams/data-access-cross-module.ts#L212-L218) `getExamSubmissionWithAnswers` 直查 `submissionAnswers`
- [data-access-error-collection.ts](file:///e:/Desktop/CICD/src/modules/exams/data-access-error-collection.ts#L6) 导入并查询 `submissionAnswers`(第 62-69 行)
**结论**:经核对 `src/shared/db/schema.ts:575-578``submissionAnswers` 表的 `submissionId` 外键引用 `examSubmissions.id`**该表属于 exams 模块自身域**exam submissions 的答题记录。exams 模块查询自己的表合规,`getExamSubmissionWithAnswers``getExamSubmissionDataForErrorCollection` 通过 data-access-cross-module 暴露给 diagnostic/error-book 模块调用,符合"模块间通过对方 data-access 通信"规则。
**无违规,无需修复。**
---
### 🟠 2.3【i18n 缺失·P1】11 个组件未接入 useTranslations
**位置**
| 文件 | 硬编码样本 |
|------|-----------|
| [components/exam-card.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/exam-card.tsx#L78-L91) | "Lvl"、"min"、"pts"、"Questions" |
| [components/exam-filters.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/exam-filters.tsx#L33-L59) | "Search exams..."、"Status"、"Any Status"、"Draft"、"Published"、"Archived"、"Difficulty"、"Easy (1)" 等 |
| [components/exam-preview-dialog.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/exam-preview-dialog.tsx#L89-L199) | "Section"、"未命名题目"、"未命名子题"、"Exam Preview"、"Generating preview..."、"完整试卷预览"、"题 · 科目 · 年级 · 分钟 · 总分"、"No preview available"、"Confirm & Create" |
| [components/exam-viewer.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/exam-viewer.tsx#L95-L197) | "Section"、"Group"、"Score:"、"No questions available." |
| [components/question-options-editor.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/question-options-editor.tsx) | 选项编辑器中文硬编码 |
| [editor/extensions/blank-node.tsx](file:///e:/Desktop/CICD/src/modules/exams/editor/extensions/blank-node.tsx) | aria-label="填空" |
| [editor/extensions/group-block.tsx](file:///e:/Desktop/CICD/src/modules/exams/editor/extensions/group-block.tsx) | placeholder 与统计文案硬编码 |
| [editor/extensions/question-block.tsx](file:///e:/Desktop/CICD/src/modules/exams/editor/extensions/question-block.tsx) | 题型 `<option>` 与 "分" 硬编码 |
| [editor/extensions/section-block.tsx](file:///e:/Desktop/CICD/src/modules/exams/editor/extensions/section-block.tsx) | "层级/卷/部分/分卷" 硬编码 |
**违反规则**:项目规则"所有用户可见文本必须适配 i18n使用 next-intl提取翻译键";硬约束"All user-visible text must be i18n-adapted using next-intl with translation keys extracted"。
**原因**:富文本编辑器 extensions 与早期组件exam-card/exam-filters/exam-preview-dialog在 i18n 改造前已存在,后续 i18n 改造未覆盖到。
**直接后果**
- 多语言环境en下用户看到中英混杂文本体验严重劣化
- 无法通过翻译文件统一管理文案,难以维护
- exam-card 在 all 列表页是高频可见组件,影响首屏专业度
---
### 🟠 2.4【i18n 缺失·P1】Server Action 返回消息绕过 i18n
**位置**
| 文件:行号 | 硬编码消息 |
|-----------|-----------|
| [actions-rich-editor.ts:41](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L41) | "标题不能为空" |
| [actions-rich-editor.ts:49,55](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L49) | "试卷内容不能为空" |
| [actions-rich-editor.ts:123,202](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L123) | "试卷内容格式无效"safeJsonParse 兜底参数) |
| [actions-rich-editor.ts:125,204](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L125) | "试卷内容解析失败" |
| [actions-rich-editor.ts:169](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L169) | "试卷草稿已创建" |
| [actions-rich-editor.ts:211](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L211) | "只能更新自己创建的试卷" |
| [actions-rich-editor.ts:278](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L278) | "试卷已更新" |
| [actions.ts:161,250,465](file:///e:/Desktop/CICD/src/modules/exams/actions.ts#L161) | "题目数据格式无效"safeJsonParse 兜底) |
| [actions.ts:466](file:///e:/Desktop/CICD/src/modules/exams/actions.ts#L466) | "试卷结构数据格式无效" |
| [ai-pipeline/auto-mark.ts:30](file:///e:/Desktop/CICD/src/modules/exams/ai-pipeline/auto-mark.ts#L30) | "试卷文本不能为空"schema message |
| [ai-pipeline/auto-mark.ts:386](file:///e:/Desktop/CICD/src/modules/exams/ai-pipeline/auto-mark.ts#L386) | "AI 自动标记完成" |
| [stats-service.ts:136](file:///e:/Desktop/CICD/src/modules/exams/stats-service.ts#L136) | "(无题目文本)" |
| [actions-helpers.ts:65](file:///e:/Desktop/CICD/src/modules/exams/actions-helpers.ts#L65) | "Invalid form data" |
**违反规则**:同 2.3。`actions.ts` 主体已使用 `getTranslations("examHomework.exam.actionMessages")`,但 `actions-rich-editor.ts``ai-pipeline/auto-mark.ts` 完全未接入,存在 i18n 一致性破口。
**原因**:这两个文件是从 actions.ts 拆分出来的新文件,拆分时未同步迁移 i18n 模式。
**直接后果**:富文本编辑器与 AI 自动标记的错误/成功提示在非中文环境下显示中文,破坏产品一致性。
---
### 🟠 2.5【路由边界缺失·P1】部分路由缺 loading.tsx / error.tsx
**位置**[src/app/(dashboard)/teacher/exams/](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/exams/)
| 路由 | loading.tsx | error.tsx |
|------|------------|----------|
| `all/` | ✅ | ❌ |
| `create/` | ✅ | ❌ |
| `new/` | ❌ | ❌ |
| `[id]/build/` | ✅ | ✅ |
| `[id]/edit-rich/` | ❌ | ❌ |
| `[id]/analytics/` | ❌ | ❌ |
| `[id]/proctoring/` | ✅ | ✅ |
**违反规则**:硬约束"All student routes must include loading.tsx and error.tsx for error boundaries"(项目内存中虽针对 student 路由,但企业级规范同样适用于 teacher 路由);规则"每个独立的数据区块必须用 React Error Boundary 包裹"、"异步数据使用 React Suspense + 骨架屏"。
**原因**:路由按需添加 loading/error未系统化覆盖。
**直接后果**
- 编辑器页面edit-rich加载 Tiptap 较慢,无骨架屏会白屏
- 分析页analytics聚合查询慢无 loading 体验差
- 任一页面抛错会冒泡到顶层 dashboard error boundary无法精确定位
---
### 🟡 2.6【类型安全·P2】10 处 `as` 类型断言(非 unknown 收窄)
**位置**
| 文件:行号 | 断言 | 说明 |
|-----------|------|------|
| [editor/editor-to-structure.ts:101](file:///e:/Desktop/CICD/src/modules/exams/editor/editor-to-structure.ts#L101) | `: "single_choice") as RichQuestionType` | 字符串字面量断言为联合类型 |
| [editor/exam-nodes-to-editor-doc.ts:38](file:///e:/Desktop/CICD/src/modules/exams/editor/exam-nodes-to-editor-doc.ts#L38) | 同上 | 同上 |
| [editor/selection-toolbar.tsx:213,215](file:///e:/Desktop/CICD/src/modules/exams/editor/selection-toolbar.tsx#L213) | `slice.content.toJSON() as JSONContent[]` | ProseMirror→Tiptap 类型 |
| [editor/exam-rich-editor.tsx:158,174](file:///e:/Desktop/CICD/src/modules/exams/editor/exam-rich-editor.tsx#L158) | `editor.getJSON() as EditorJSONContent` | Tiptap 内部类型断言 |
| [components/exam-data-table.tsx:39](file:///e:/Desktop/CICD/src/modules/exams/components/exam-data-table.tsx#L39) | `params as Record<...>` | 不安全参数断言 |
| [components/exam-form.tsx:40](file:///e:/Desktop/CICD/src/modules/exams/components/exam-form.tsx#L40) | `zodResolver(formSchema) as Resolver<ExamFormValues>` | zodResolver 返回类型断言 |
| [actions-rich-editor.ts:147,230](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L147) | `q.type as "single_choice" | "multiple_choice" | "text" | "judgment"` | 字符串断言为联合类型 |
| [edit-rich/page.tsx:64](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/exams/[id]/edit-rich/page.tsx#L64) | `structureToEditorDoc(editorDoc) as EditorJSONContent` | 类型断言 |
**违反规则**:项目规则"禁止 `as` 断言(除从 `unknown` 转换或测试中,需注释原因)"。
**原因**Tiptap/ProseMirror 类型系统与项目类型边界处缺类型守卫;`RichQuestionType` 联合类型的字符串字面量缺运行时校验函数。
**直接后果**:若 AI 返回未预期的 type 值(如 "essay"`as` 断言会让错误值通过类型检查,运行时可能渲染异常。
---
### 🟡 2.7【企业级能力缺失·P2】无统一空状态/骨架屏/错误回退
**位置**:组件层未抽取统一的 `<ExamEmptyState>` / `<ExamSkeleton>` / `<ExamErrorBoundary>`
**问题**
- `all/page.tsx` 自行实现了 `ExamsResultsFallback`,未复用到 `analytics`/`edit-rich`
- `exam-card.tsx``exam-grid.tsx` 无骨架屏
- 编辑器加载Tiptap 初始化)期间无统一占位
**违反规则**:审计要求"明确处理空数据、无权限、网络异常等边界状态"、"异步数据使用 React Suspense + 骨架屏"。
**直接后果**:体验不一致,重复实现。
---
### 🟡 2.8【可测试性·P2】纯逻辑与 UI 耦合,缺单测
**位置**
- `components/exam-preview-utils.ts`293 行纯函数,已抽取,但无单测)
- `hooks/use-exam-preview-{state,tasks,rewrite}.ts` 无对应测试
- `editor/editor-to-structure.ts``editor/structure-to-editor.ts` 双向转换是核心纯逻辑,无单测
- `stats-service.ts` 的错误率/难度计算无单测
**违反规则**:审计要求"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离;导出清晰的接口类型以便 mock"。
**直接后果**富文本编辑器双向转换是高风险逻辑type/score/structure 映射),无单测难以保证回归质量。
---
### 🟡 2.9【解耦性·P2】未通过接口抽象 + Context 注入数据服务
**位置**:模块整体。
**问题**:当前组件直接 import 同模块的 actions/data-access`exam-rich-form.tsx` 直接 import `autoMarkExamAction` / `createExamFromRichEditorAction`)。虽然同模块内 import 合规,但审计要求"通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access"。
**违反规则**:审计重构方案的"完全解耦"与"可测试性"原则。
**原因**:当前实现以功能正确性优先,未做依赖注入抽象。
**直接后果**
- 组件无法在测试中 mock 数据服务
- 不同角色teacher/admin/parent/student的差异未通过接口实现隔离未来扩展角色需改组件
- 配置驱动设计未落地,新增 Widget 需改组件代码
---
## 三、行业差距对比
参考智学网、猿题库、学而思网校、Google Classroom、Canvas LMS 等同类产品exams 模块当前差距:
### 3.1 试卷创建侧
| 行业实践 | 当前状态 | 差距影响 |
|---------|---------|---------|
| 多种组卷入口(手动/AI/富文本/导入 Word三选一清晰呈现 | 已有三种入口,但 `/create``/new` 路由并列,无统一选择页 | 教师首次使用困惑 |
| 试卷模板库(按学科/年级预置模板) | ❌ 无 | 教师每次从零创建,效率低 |
| 知识点双向细目表(题目-知识点覆盖矩阵) | ❌ 无(虽有 questions.knowledgePoints但 exam 层无细目表视图) | 无法评估试卷覆盖度 |
| 难度预估(基于题库历史正确率自动估算试卷难度) | ❌ 无(仅手动 1-5 级) | 难度设置主观 |
| 试卷预览支持 PDF 导出/打印 | ❌ 无 | 教师无法离线分发 |
### 3.2 考试作答侧(学生)
| 行业实践 | 当前状态 | 差距影响 |
|---------|---------|---------|
| 作答页答题卡导航(已答/未答/标记 revisit | ❌ 仅顺序作答 | 学生难以跳题、检查 |
| 自动保存进度可视化 | homework 模块已实现autoSave* 翻译键齐全) | ✅ 较好 |
| 限时/监考倒计时 | homework 模块已实现 useExamCountdown | ✅ 较好 |
| 客观题即时反馈(练习模式) | ❌ 仅作业模式提交后批改 | 缺少低风险练习模式 |
### 3.3 考试分析侧(教师)
| 行业实践 | 当前状态 | 差距影响 |
|---------|---------|---------|
| 平均分/及格率/分数段分布 | ✅ 已实现V3-8 | — |
| 逐题错误率与难度等级 | ✅ 已实现 | — |
| 知识点掌握度雷达图 | diagnostic 模块有,但未在 exam analytics 集成 | 教师需跨页查看 |
| 班级横向对比 | ❌ 无(仅全卷汇总) | 无法定位班级差异 |
| 学生个体诊断报告(一键生成) | ❌ 无 | 个性化反馈缺失 |
| 历次考试趋势 | ❌ 无 | 无法看进步趋势 |
### 3.4 多角色覆盖侧
| 角色 | 当前覆盖 | 差距 |
|------|---------|------|
| admin | ❌ 无 admin 视角考试管理(全校/年级聚合) | admin 仅能通过 dashboard 看 examCount无考试管理页 |
| teacher | ✅ 完整(创建/组卷/预览/分析/监考) | — |
| parent | ✅ parent 模块有 child-exam-detail + parentExam i18n | 缺少历次考试趋势对比 |
| student | ⚠️ 通过 homework-take-view 作答,但无独立"我的考试"汇总页 | 学生无法回看历史考试试卷与成绩 |
### 3.5 UX 细节
- 缺少全局考试状态徽章颜色规范draft/published/archived 在 exam-card 与 exam-columns 中重复定义)
- exam-card 科目颜色映射 `subjectColorMap` 硬编码英文字符串 key"Mathematics" 等),无法国际化——科目名应通过 ID 映射颜色,而非名称
- 无空状态插画/图标统一规范all 页用 FileTextanalytics 页也用 BarChart3缺一致性
---
## 四、改进优先级建议
### P0紧急影响架构合规与数据安全
| # | 问题 | 改进方向 | 关联规则 |
|---|------|---------|---------|
| P0-1 | `data-access-cross-module.ts:488` 直接 JOIN questions 表 | 在 questions 模块新增 `getQuestionTypeMapByIds(ids): Promise<Map<string, string>>`exams 改为调用此接口 | 模块间禁止直查对方表 |
| P0-2 | `new/``[id]/edit-rich/``[id]/analytics/``all/``create/` 缺 loading.tsx/error.tsx | 补齐 loading.tsx + error.tsx复用 dashboard 模式 | 路由边界规范 |
### P1高影响影响多语言与体验
| # | 问题 | 改进方向 |
|---|------|---------|
| P1-1 | 11 个组件未接入 i18nexam-card/exam-filters/exam-preview-dialog/exam-viewer/question-options-editor + 4 个 editor extensions | 接入 useTranslations提取翻译键到 exam-homework.json 的 exam.card/exam.viewer/exam.previewDialog/editor.* 命名空间 |
| P1-2 | actions-rich-editor.ts + auto-mark.ts Server Action 返回消息硬编码 | 改用 getTranslations("examHomework.exam.actionMessages"),复用 actions.ts 已有翻译键,新增 richEditor.* / autoMark.* 子键 |
| P1-3 | exam-card subjectColorMap 用英文名做 key | 改为按 subjectId 映射颜色,颜色配置移至 `shared/config/subject-colors.ts` |
| P1-4 | stats-service.ts "(无题目文本)"、data-access.ts "General" 兜底硬编码 | 通过 data-access 层返回 null由组件层 i18n 渲染兜底文案 |
### P2中长期企业级能力与重构
| # | 问题 | 改进方向 | 状态 |
|---|------|---------|------|
| P2-1 | 10 处 `as` 类型断言 | 为 RichQuestionType 增加 `isRichQuestionType(v): v is RichQuestionType` 类型守卫Tiptap JSONContent 边界用 zod schema 校验 | ✅ 已完成2026-06-25新增 isRichQuestionType/isStandaloneQuestionType/toRichQuestionType/toStandaloneQuestionType 4 个守卫,消除 editor-to-structure.ts:101、exam-nodes-to-editor-doc.ts:38、actions-rich-editor.ts:149/233 共 4 处 as 断言;其余 6 处 as 断言属于 unknown→具体类型的合法收窄或 Tiptap/ProseMirror 内部类型边界,已添加注释说明,保留 |
| P2-2 | 纯逻辑无单测 | 为 exam-preview-utils、editor-to-structure、structure-to-editor、stats-service 错误率计算补充 .test.ts | ⏸️ 待实施(依赖 P2-4 ExamServicePort 落地后统一 mock |
| P2-3 | 无统一 ExamEmptyState/ExamSkeleton/ExamErrorBoundary | 抽取到 components/exam-boundaries.tsx全模块复用 | ✅ 已完成2026-06-25创建 components/exam-boundaries.tsx189 行),导出 ExamErrorBoundary/ExamEmptyState/ExamSkeleton 三组合单元5 种骨架变体,新增 i18n 键 exam.error.boundaryTitle/boundaryDescription/retry |
| P2-4 | 组件直接 import actions未通过 Context 注入 | 定义 `ExamServicePort` 接口 + `ExamServiceProvider` Context组件通过 `useExamService()` 获取;角色差异通过不同 Provider 实现隔离 | ✅ 已完成骨架2026-06-25创建 services/exam-service-port.ts95 行12 方法契约)+ services/exam-service-context.tsx72 行Context + Provider + Hook+ services/index.ts桶导出。具体实现TeacherExamService/AdminExamService/MockExamService与组件改造将在 P2-6+ 落地 |
| P2-5 | 无配置驱动的 Widget 渲染 | 参考 dashboard/config/widget-configs.ts新增 `exams/config/exam-widgets.ts`,按角色配置渲染哪些子模块 | ✅ 已完成2026-06-25创建 config/exam-widgets.ts192 行),四角色默认配置 + getExamWidgetConfig/getWidgetsBySlot 工具函数 |
| P2-6 | 缺少考试模板库、知识点细目表、班级对比、学生个体报告 | 中长期功能补全,对标智学网 | ⏸️ 待实施(中长期) |
| P2-7 | 缺少 admin 视角考试管理页、student 独立"我的考试"页 | 多角色覆盖补全 | ⏸️ 待实施(中长期,依赖 P2-4 具体实现 + P2-5 配置消费) |
| P2-8 | 关键操作埋点不完整 | 已有 exam.ai_generated/updated/deleted/duplicated需补 exam.published/archived/auto_marked 埋点 | ⏸️ 待实施 |
| P2-9 | a11y 缺失(编辑器 extensions 无 aria-label 规范、键盘导航) | 为 Tiptap 节点添加 aria-label工具栏支持完整键盘导航 | ⏸️ 待实施 |
| P2-10 | 数据查询未结合权限二次校验data-access 层部分函数未传 scope | `getExamPreview``getExamSubjects``getExamGrades``duplicateExam``deleteExamById` 应接受 scope 参数或在 Action 层显式校验 | ⏸️ 待实施 |
> **本轮 P2 落地范围说明**
> - P2-1 / P2-3 / P2-4骨架/ P2-5 已完成,奠定解耦与配置驱动的架构基础
> - P2-2 单测待 ExamServicePort 具体实现落地后统一 mock
> - P2-6 / P2-7 为中长期功能补全,需独立规划排期
> - P2-8 / P2-9 / P2-10 为增强项,可在后续迭代中逐步落地
> - 全部 P2 代码改动已通过 `npx tsc --noEmit`exams 模块零错误)与 `npx eslint`(零错误/零警告)验证
---
## 五、架构图同步说明
本次审计发现架构图需补充以下节点:
### 004_architecture_impact_map.md 需补充
1. **exams 模块文件清单补全**
- 新增 `data-access-error-collection.ts` 行(当前 004 未单独列出)
- 标注 `data-access-cross-module.ts``getExamForGradeEntry` 存在 P0 跨模块 JOIN 违规(待修复后改为 ✅ 已修复)
2. **permission 补全**
- 005 已有 EXAM_PROCTOR/EXAM_PROCTOR_READ但 004 第 2.2 节 exams 权限点列表未完整列出
3. **dependencyMatrix 补充**
- `app/(dashboard)/teacher/exams/[id]/edit-rich/page.tsx``exams/editor/{exam-nodes-to-editor-doc,structure-to-editor}``exams/utils/normalize-structure`(当前 004 已记 build/page.tsx但 edit-rich 同样依赖,需补)
4. **被依赖关系补全**
- `homework/data-access-utils.getQuestionText``exams/stats-service.ts` 调用005 JSON 中 homework 模块 exports 的 usedBy 需补 `exams/stats-service`
### 005_architecture_data.json 需补充
1. `modules.exams.exports` 数组补:
- `data-access-error-collection.ts`(含 `getExamErrorCollectionForExam` 等接口)
- `getExamForGradeEntry`(标注跨模块 JOIN 待修复)
2. `modules.homework.exports``getQuestionText``usedBy``"exams/stats-service"`
3. `modules.questions.exports` 新增 `getQuestionTypeMapByIds`(修复 P0-1 后)
4. `architectureOverview.violations` 数组新增当前未记录的违规项,修复后改为 ✅ 标记
---
## 附:重构方案设计要点(落地架构)
> 以下为 P2-4/P2-5 的具体设计方向,作为中长期重构蓝图。本次实施将先完成 P0/P1P2 仅落地基础接口与配置骨架。
### A. 完全解耦ExamServicePort + Context 注入
```typescript
// exams/services/exam-service-port.ts新增
export interface ExamServicePort {
listExams(params: GetExamsParams): Promise<Exam[]>
getExam(id: string): Promise<ExamDetail | null>
createExam(input: ExamCreateInput): Promise<ActionState<string>>
updateExam(input: ExamUpdateInput): Promise<ActionState<string>>
deleteExam(id: string): Promise<ActionState<string>>
duplicateExam(id: string): Promise<ActionState<string>>
getAnalytics(id: string): Promise<ExamAnalyticsSummary | null>
// ... 所有数据访问通过此接口
}
// exams/services/exam-service-context.tsx新增
const ExamServiceContext = createContext<ExamServicePort | null>(null)
export function ExamServiceProvider({ service, children }: { service: ExamServicePort; children: ReactNode }) { ... }
export function useExamService(): ExamServicePort { ... }
// 不同角色的实现
// exams/services/teacher-exam-service.ts // 调用真实 Server Actions
// exams/services/admin-exam-service.ts // admin 视角(聚合全校)
// exams/services/mock-exam-service.ts // 测试用
```
### B. 组合优先Widget 配置驱动
```typescript
// exams/config/exam-widgets.ts新增
export type ExamWidgetConfig = {
role: Role
widgets: Array<{
id: "list" | "analytics" | "proctoring" | "templates" | "blueprint"
visible: boolean
order: number
props?: Record<string, unknown>
}>
}
export const examWidgetConfigs: Record<Role, ExamWidgetConfig> = { ... }
```
### C. i18n 翻译文件结构示例(新增键)
```json
{
"exam": {
"card": {
"level": "难度 {{level}}",
"minutes": "{{count}} 分钟",
"points": "{{count}} 分",
"questions": "{{count}} 题"
},
"viewer": {
"section": "分卷",
"group": "大题",
"score": "分值",
"noQuestions": "暂无题目"
},
"previewDialog": {
"title": "试卷预览",
"generating": "生成预览中...",
"fullPreview": "完整试卷预览",
"summary": "{{count}} 题 · {{subject}} · {{grade}} · {{minutes}} 分钟 · {{total}} 分",
"noPreview": "暂无预览内容",
"confirmCreate": "确认并创建",
"untitledQuestion": "未命名题目",
"untitledSubQuestion": "未命名子题",
"scoreUnit": "分"
},
"richEditorAction": {
"titleRequired": "请填写试卷标题",
"contentRequired": "试卷内容不能为空",
"contentInvalid": "试卷内容格式无效",
"contentParseFailed": "试卷内容解析失败",
"draftCreated": "试卷草稿已创建",
"onlyOwnUpdate": "只能更新自己创建的试卷",
"updated": "试卷已更新"
},
"autoMarkAction": {
"sourceRequired": "试卷文本不能为空",
"completed": "AI 自动标记完成"
}
}
}
```
### D. 错误与边界
- 每个路由的 `error.tsx` 复用 `exams/components/exam-error-boundary.tsx`(新增)
- 列表/卡片使用 `<ExamSkeleton>` / `<ExamEmptyState>`(新增)
- 编辑器加载使用 Suspense + 自定义骨架
### E. 可测试性
- 纯逻辑已有抽取exam-preview-utils/editor-to-structure/structure-to-editor补单测
- ExamServicePort 接口允许测试注入 mock 实现
### F. 安全性
- data-access 层所有按 ID 查询函数增加可选 `scope` 参数Action 层强制传入
- `getExamPreview``duplicateExam``deleteExamById` 当前未校验 scope需补
### G. 监控埋点
-`exam.published``exam.archived``exam.auto_marked` 埋点
- analytics 页访问埋点 `exam.analytics_viewed`