29 KiB
考试(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 Action(create/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 第 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 导入
submissionAnswers - data-access-cross-module.ts
getExamSubmissionWithAnswers直查submissionAnswers - data-access-error-collection.ts 导入并查询
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 | "Lvl"、"min"、"pts"、"Questions" |
| components/exam-filters.tsx | "Search exams..."、"Status"、"Any Status"、"Draft"、"Published"、"Archived"、"Difficulty"、"Easy (1)" 等 |
| components/exam-preview-dialog.tsx | "Section"、"未命名题目"、"未命名子题"、"Exam Preview"、"Generating preview..."、"完整试卷预览"、"题 · 科目 · 年级 · 分钟 · 总分"、"No preview available"、"Confirm & Create" |
| components/exam-viewer.tsx | "Section"、"Group"、"Score:"、"No questions available." |
| components/question-options-editor.tsx | 选项编辑器中文硬编码 |
| editor/extensions/blank-node.tsx | aria-label="填空" |
| editor/extensions/group-block.tsx | placeholder 与统计文案硬编码 |
| editor/extensions/question-block.tsx | 题型 <option> 与 "分" 硬编码 |
| 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 | "标题不能为空" |
| actions-rich-editor.ts:49,55 | "试卷内容不能为空" |
| actions-rich-editor.ts:123,202 | "试卷内容格式无效"(safeJsonParse 兜底参数) |
| actions-rich-editor.ts:125,204 | "试卷内容解析失败" |
| actions-rich-editor.ts:169 | "试卷草稿已创建" |
| actions-rich-editor.ts:211 | "只能更新自己创建的试卷" |
| actions-rich-editor.ts:278 | "试卷已更新" |
| actions.ts:161,250,465 | "题目数据格式无效"(safeJsonParse 兜底) |
| actions.ts:466 | "试卷结构数据格式无效" |
| ai-pipeline/auto-mark.ts:30 | "试卷文本不能为空"(schema message) |
| ai-pipeline/auto-mark.ts:386 | "AI 自动标记完成" |
| stats-service.ts:136 | "(无题目文本)" |
| actions-helpers.ts:65 | "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/
| 路由 | 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 | : "single_choice") as RichQuestionType |
字符串字面量断言为联合类型 |
| editor/exam-nodes-to-editor-doc.ts:38 | 同上 | 同上 |
| editor/selection-toolbar.tsx:213,215 | slice.content.toJSON() as JSONContent[] |
ProseMirror→Tiptap 类型 |
| editor/exam-rich-editor.tsx:158,174 | editor.getJSON() as EditorJSONContent |
Tiptap 内部类型断言 |
| components/exam-data-table.tsx:39 | params as Record<...> |
不安全参数断言 |
| components/exam-form.tsx:40 | zodResolver(formSchema) as Resolver<ExamFormValues> |
zodResolver 返回类型断言 |
| actions-rich-editor.ts:147,230 | `q.type as "single_choice" | "multiple_choice" |
| edit-rich/page.tsx:64 | 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-richexam-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 页用 FileText,analytics 页也用 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 个组件未接入 i18n(exam-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.tsx(189 行),导出 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.ts(95 行,12 方法契约)+ services/exam-service-context.tsx(72 行,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.ts(192 行),四角色默认配置 + 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 需补充
-
exams 模块文件清单补全:
- 新增
data-access-error-collection.ts行(当前 004 未单独列出) - 标注
data-access-cross-module.ts中getExamForGradeEntry存在 P0 跨模块 JOIN 违规(待修复后改为 ✅ 已修复)
- 新增
-
permission 补全:
- 005 已有 EXAM_PROCTOR/EXAM_PROCTOR_READ,但 004 第 2.2 节 exams 权限点列表未完整列出
-
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 同样依赖,需补)
-
被依赖关系补全:
homework/data-access-utils.getQuestionText被exams/stats-service.ts调用,005 JSON 中 homework 模块 exports 的 usedBy 需补exams/stats-service
005_architecture_data.json 需补充
-
modules.exams.exports数组补:data-access-error-collection.ts(含getExamErrorCollectionForExam等接口)getExamForGradeEntry(标注跨模块 JOIN 待修复)
-
modules.homework.exports中getQuestionText的usedBy补"exams/stats-service" -
modules.questions.exports新增getQuestionTypeMapByIds(修复 P0-1 后) -
architectureOverview.violations数组新增当前未记录的违规项,修复后改为 ✅ 标记
附:重构方案设计要点(落地架构)
以下为 P2-4/P2-5 的具体设计方向,作为中长期重构蓝图。本次实施将先完成 P0/P1,P2 仅落地基础接口与配置骨架。
A. 完全解耦:ExamServicePort + Context 注入
// 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 配置驱动
// 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 翻译文件结构示例(新增键)
{
"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