# 考试模块实现设计文档 ## 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:** ```typescript 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 创建与构建考试 1. **创建**: 教师输入基本信息(标题、科目)。数据库创建记录(草稿状态)。 2. **构建**: - 教师打开“构建”页面。 - 服务器从数据库 Hydrate(注水)`initialStructure`。 - 教师从题库拖拽题目到结构树。 - 教师创建章节(分组)。 - **保存**: 同时提交 `questionsJson`(扁平化,用于索引)和 `structureJson`(树状,用于布局)到 `updateExamAction`。 3. **发布**: 状态变更为 `published`。 ### 4.2 阅卷/批改流程(迁移到 Homework) 教师批改统一在 Homework 模块完成: - 提交列表:`/teacher/homework/submissions` - 批改页:`/teacher/homework/submissions/[submissionId]` ### 4.3 考试管理(All Exams Actions) 位于 `/teacher/exams/all` 的表格行级菜单。 1. **Publish / Move to Draft / Archive** - 客户端组件 `ExamActions` 触发 `updateExamAction`,传入 `examId` 与目标 `status`。 - 服务器更新 `exams.status`,并对 `/teacher/exams/all` 执行缓存再验证。 2. **Duplicate** - 客户端组件 `ExamActions` 触发 `duplicateExamAction`,传入 `examId`。 - 服务器复制 `exams` 记录并复制关联的 `exam_questions`。 - 新考试以 `draft` 状态创建,复制结构(`exams.structure`),并清空排期信息(`startTime/endTime`,以及 description 中的 `scheduledAt`)。 - 成功后跳转到新考试的构建页 `/teacher/exams/[id]/build`。 3. **Delete** - 客户端组件 `ExamActions` 触发 `deleteExamAction`,传入 `examId`。 - 服务器删除 `exams` 记录;相关表(如 `exam_questions`、`exam_submissions`、`submission_answers`)通过外键级联删除。 - 成功后刷新列表。 4. **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: - `createExamAction` - `updateExamAction` - `duplicateExamAction` - `deleteExamAction` ## 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` 置空;同时尝试从 `description` JSON 中移除 `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` 因当前存储在 `description` JSON 中,采用内存过滤 - `getExamById(id)`: 查询 exam 及其 `exam_questions`,并返回 `structure` 以用于构建器 Hydrate ### 6.5 `getExamPreviewAction` (新增) - **入参**: `examId` (string) - **行为**: - 查询指定 exam 及其关联的 questions (通过 `exam_questions` 关系)。 - 返回完整的 `structure` (JSON 树) 和扁平化的 `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。