9.2 KiB
考试模块实现设计文档
1. 概述
考试模块用于教师侧的“试卷制作与管理”,覆盖创建考试、组卷(支持嵌套分组)、发布/归档等流程。
说明(合并调整):与“作业(Homework)”模块合并后,考试模块不再提供“阅卷/评分(grading)”与提交流转;教师批改统一在 Homework 的 submissions 中完成。
2. 数据架构
2.1 核心实体
- Exams: 根实体,包含元数据(标题、时间安排)和结构信息。
- ExamQuestions: 关系链接,用于查询题目的使用情况(扁平化表示)。
- ExamSubmissions: (历史/保留)学生的考试尝试记录;当前 UI/路由不再使用。
- SubmissionAnswers: (历史/保留)链接到特定题目的单个答案;当前 UI/路由不再使用。
2.2 structure 字段
为了支持层级布局(如章节/分组),我们在 exams 表中引入了一个 JSON 列 structure。它作为“布局/呈现层”的单一事实来源(Source of Truth),用于渲染分组与排序;而 exam_questions 仍然承担题目关联、外键完整性与索引查询职责。
JSON Schema:
type ExamNode = {
id: string; // 节点的唯一 UUID
type: 'group' | 'question';
title?: string; // 'group' 类型必填
questionId?: string; // 'question' 类型必填
score?: number; // 在此考试上下文中的分值
children?: ExamNode[]; // 'group' 类型的递归子节点
}
2.3 description 元数据字段(当前实现)
当前版本将部分元数据(如 subject/grade/difficulty/totalScore/durationMin/tags/scheduledAt)以 JSON 字符串形式存入 exams.description,并在数据访问层解析后提供给列表页展示与筛选。
3. 组件架构
3.1 组卷(构建器)
位于 /teacher/exams/[id]/build。
ExamAssembly(客户端组件)- 管理
structure状态树。 - 处理“添加题目”、“添加章节”、“移除”和“重新排序”操作。
- 实时计算总分和进度。
- 管理
StructureEditor(客户端组件)- 基于
@dnd-kit构建。 - 提供嵌套的可排序(Sortable)界面。
- 支持在组内/组间拖拽题目(当前优化为 2 层深度)。
- 基于
QuestionBankList- 可搜索/筛选的可用题目列表。
- “添加”操作将节点追加到结构树中。
3.2 阅卷界面(已下线)
原阅卷路由 /teacher/exams/grading 与 /teacher/exams/grading/[submissionId] 已移除业务能力并重定向到 Homework:
/teacher/exams/grading*→/teacher/homework/submissions
3.3 列表页(All Exams)
位于 /teacher/exams/all。
- Page (RSC): 负责解析 query(
q/status/difficulty)并调用数据访问层获取 exams。 ExamFilters(客户端组件): 使用 URL query 驱动筛选条件。ExamDataTable(客户端组件): 基于 TanStack Table 渲染列表,并在 actions 列中渲染ExamActions。
4. 关键工作流
4.1 创建与构建考试
- 创建: 教师输入基本信息(标题、科目)。数据库创建记录(草稿状态)。
- 构建:
- 教师打开“构建”页面。
- 服务器从数据库 Hydrate(注水)
initialStructure。 - 教师从题库拖拽题目到结构树。
- 教师创建章节(分组)。
- 保存: 同时提交
questionsJson(扁平化,用于索引)和structureJson(树状,用于布局)到updateExamAction。
- 发布: 状态变更为
published。
4.2 阅卷/批改流程(迁移到 Homework)
教师批改统一在 Homework 模块完成:
- 提交列表:
/teacher/homework/submissions - 批改页:
/teacher/homework/submissions/[submissionId]
4.3 考试管理(All Exams Actions)
位于 /teacher/exams/all 的表格行级菜单。
-
Publish / Move to Draft / Archive
- 客户端组件
ExamActions触发updateExamAction,传入examId与目标status。 - 服务器更新
exams.status,并对/teacher/exams/all执行缓存再验证。
- 客户端组件
-
Duplicate
- 客户端组件
ExamActions触发duplicateExamAction,传入examId。 - 服务器复制
exams记录并复制关联的exam_questions。 - 新考试以
draft状态创建,复制结构(exams.structure),并清空排期信息(startTime/endTime,以及 description 中的scheduledAt)。 - 成功后跳转到新考试的构建页
/teacher/exams/[id]/build。
- 客户端组件
-
Delete
- 客户端组件
ExamActions触发deleteExamAction,传入examId。 - 服务器删除
exams记录;相关表(如exam_questions、exam_submissions、submission_answers)通过外键级联删除。 - 成功后刷新列表。
- 客户端组件
-
Edit / Build
- 当前统一跳转到
/teacher/exams/[id]/build。
- 当前统一跳转到
5. 技术决策
5.1 混合存储策略
我们在存储考试题目时采用了 混合方法:
- 关系型 (
exam_questions): 用于“查找所有使用题目 X 的考试”查询和外键约束。 - 文档型 (
exams.structure): 用于渲染嵌套 UI 和保留任意排序/分组。 理由: 这结合了 SQL 的完整性和 NoSQL 在 UI 布局上的灵活性。
5.2 拖拽功能
使用 @dnd-kit 代替旧库,因为:
- 更好的无障碍支持(键盘支持)。
- 模块化架构(Sensors, Modifiers)。
- 面向未来(现代 React Hooks 模式)。
5.3 Server Actions
所有变更操作(保存草稿、发布、复制、删除)均使用 Next.js Server Actions,以确保类型安全并自动重新验证缓存。
已落地的 Server Actions:
createExamActionupdateExamActionduplicateExamActiondeleteExamAction
6. 接口与数据影响
6.1 updateExamAction
- 入参(FormData):
examId(必填),status(可选:draft/published/archived),questionsJson(可选),structureJson(可选) - 行为:
- 若传入
questionsJson:先清空exam_questions再批量写入,order由数组顺序决定;未传入则不触碰exam_questions - 若传入
structureJson:写入exams.structure;未传入则不更新该字段 - 若传入
status:写入exams.status
- 若传入
- 缓存:
revalidatePath("/teacher/exams/all")
6.2 duplicateExamAction
- 入参(FormData):
examId(必填) - 行为:
- 复制一条
exams(新 id、新 title:追加 “(Copy)”、status强制为draft) startTime/endTime置空;同时尝试从descriptionJSON 中移除scheduledAt- 复制
exam_questions(保留 questionId/score/order) - 复制
exams.structure
- 复制一条
- 缓存:
revalidatePath("/teacher/exams/all")
6.3 deleteExamAction
- 入参(FormData):
examId(必填) - 行为:
- 删除
exams记录 - 依赖外键级联清理关联数据:
exam_questions、exam_submissions、submission_answers
- 删除
- 缓存:
revalidatePath("/teacher/exams/all")
6.4 数据访问层(Data Access)
位于 src/modules/exams/data-access.ts,对外提供与页面/组件解耦的查询函数。
getExams(params): 支持按q/status在数据库侧过滤;difficulty因当前存储在descriptionJSON 中,采用内存过滤getExamById(id): 查询 exam 及其exam_questions,并返回structure以用于构建器 Hydrate
6.5 getExamPreviewAction (新增)
- 入参:
examId(string) - 行为:
- 查询指定 exam 及其关联的 questions (通过
exam_questions关系)。 - 返回完整的
structure(JSON 树) 和扁平化的questions列表。 - 用于预览弹窗的数据加载。
- 查询指定 exam 及其关联的 questions (通过
7. 变更记录
日期:2026-01-12 (当前)
-
列表页优化 (
/teacher/exams/all):- 移除了冗余的 "All Exams" 页面标题和描述。
- 重构了表格列 (
ExamColumns):- 合并标题、标签、科目、年级为 "Exam Info" 列。
- 合并题目数、总分、时长为 "Stats" 列。
- 合并创建时间和预定时间为 "Date" 列。
- 优化了状态 (Status) 和难度 (Difficulty) 的视觉样式 (Badge, Progress bar)。
- 优化了表格分页和布局 (
ExamDataTable)。
-
预览功能增强:
- 新增直接预览功能:在操作列添加了 "View" (眼睛图标) 按钮。
- 点击 "View" 触发
getExamPreviewAction获取完整试卷数据。 - 弹窗 (
Dialog) 直接展示试卷内容 (ExamPaperPreview),移除了冗余的头部描述,优化了滚动体验。 - 修复了可访问性问题 (DialogTitle)。
-
组卷页面升级 (
/teacher/exams/[id]/build):- 布局重构: 扩展工作区高度,调整左右面板比例 (2:1),优化头部信息展示和进度可视化。
- 题库增强: 实现了基于 Server Action (
getQuestionsAction) 的分页加载和服务器端筛选,提升大数据量下的性能;优化了搜索和筛选器 UI。 - 预览优化: 移除了内联预览,改为通过 "Preview" 按钮触发弹窗预览,避免干扰编辑流。
- 视觉降噪: 移除了页面顶部冗余的标题和描述。
日期:2025-12-31
- 移除 Exams grading 入口与实现:删除阅卷 UI、server action、data-access 查询。
- Exams grading 路由改为重定向到 Homework submissions。