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

29 KiB
Raw Blame History

考试exams模块审计报告

审计时间2026-06-25 审计范围:src/modules/exams/**35+ 文件)+ src/app/(dashboard)/teacher/exams/**10 个页面)+ 跨模块依赖面 审计依据:docs/architecture/004_architecture_impact_map.mddocs/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/createcreateExamActionpersistExamDraftdb.insert(exams)
  • AI 创建/teacher/exams/createcreateAiExamActionloadAiDraftQuestionsAndStructurepersistAiGeneratedExamDraft → 通过 questions/data-access.createQuestionWithRelations 创建题目 → 事务写 exams + examQuestions
  • 富文本创建/teacher/exams/newcreateExamFromRichEditorActioneditorDocToStructurepersistAiGeneratedExamDraft
  • 组卷/teacher/exams/[id]/buildExamAssembly + getExamById
  • 预览previewAiExamAction / getExamPreviewAction
  • 分析/teacher/exams/[id]/analyticsgetExamAnalytics(聚合 homework 提交数据)

1.3 跨模块依赖(合规项)

以下跨模块调用均通过对方 data-access符合三层架构规则

  • questions/data-access.createQuestionWithRelationsP0-1 已修复)
  • classes/data-access.getClassGradeIdsByClassIdsP0-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 表归属与直查

位置

结论:经核对 src/shared/db/schema.ts:575-578submissionAnswers 表的 submissionId 外键引用 examSubmissions.id该表属于 exams 模块自身域exam submissions 的答题记录。exams 模块查询自己的表合规,getExamSubmissionWithAnswersgetExamSubmissionDataForErrorCollection 通过 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.tsai-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-rich
  • exam-card.tsxexam-grid.tsx 无骨架屏
  • 编辑器加载Tiptap 初始化)期间无统一占位

违反规则:审计要求"明确处理空数据、无权限、网络异常等边界状态"、"异步数据使用 React Suspense + 骨架屏"。

直接后果:体验不一致,重复实现。


🟡 2.8【可测试性·P2】纯逻辑与 UI 耦合,缺单测

位置

  • components/exam-preview-utils.ts293 行纯函数,已抽取,但无单测)
  • hooks/use-exam-preview-{state,tasks,rewrite}.ts 无对应测试
  • editor/editor-to-structure.tseditor/structure-to-editor.ts 双向转换是核心纯逻辑,无单测
  • stats-service.ts 的错误率/难度计算无单测

违反规则:审计要求"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离;导出清晰的接口类型以便 mock"。

直接后果富文本编辑器双向转换是高风险逻辑type/score/structure 映射),无单测难以保证回归质量。


🟡 2.9【解耦性·P2】未通过接口抽象 + Context 注入数据服务

位置:模块整体。

问题:当前组件直接 import 同模块的 actions/data-accessexam-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 getExamPreviewgetExamSubjectsgetExamGradesduplicateExamdeleteExamById 应接受 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 --noEmitexams 模块零错误)与 npx eslint(零错误/零警告)验证

五、架构图同步说明

本次审计发现架构图需补充以下节点:

004_architecture_impact_map.md 需补充

  1. exams 模块文件清单补全

    • 新增 data-access-error-collection.ts 行(当前 004 未单独列出)
    • 标注 data-access-cross-module.tsgetExamForGradeEntry 存在 P0 跨模块 JOIN 违规(待修复后改为 已修复)
  2. permission 补全

    • 005 已有 EXAM_PROCTOR/EXAM_PROCTOR_READ但 004 第 2.2 节 exams 权限点列表未完整列出
  3. dependencyMatrix 补充

    • app/(dashboard)/teacher/exams/[id]/edit-rich/page.tsxexams/editor/{exam-nodes-to-editor-doc,structure-to-editor}exams/utils/normalize-structure(当前 004 已记 build/page.tsx但 edit-rich 同样依赖,需补)
  4. 被依赖关系补全

    • homework/data-access-utils.getQuestionTextexams/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.exportsgetQuestionTextusedBy"exams/stats-service"

  3. modules.questions.exports 新增 getQuestionTypeMapByIds(修复 P0-1 后)

  4. architectureOverview.violations 数组新增当前未记录的违规项,修复后改为 标记


附:重构方案设计要点(落地架构)

以下为 P2-4/P2-5 的具体设计方向,作为中长期重构蓝图。本次实施将先完成 P0/P1P2 仅落地基础接口与配置骨架。

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 层强制传入
  • getExamPreviewduplicateExamdeleteExamById 当前未校验 scope需补

G. 监控埋点

  • exam.publishedexam.archivedexam.auto_marked 埋点
  • analytics 页访问埋点 exam.analytics_viewed