Files
NextEdu/docs/architecture/audit/archive/exams-audit-report.md

476 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 考试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`