Files
CICD/docs/design/005_exam_module_implementation.md
SpecialX 13e91e628d
Some checks failed
CI / build-and-test (push) Failing after 3m34s
CI / deploy (push) Has been skipped
Merge exams grading into homework
Redirect /teacher/exams/grading* to /teacher/homework/submissions; remove exam grading UI/actions/data-access; add homework student workflow and update design docs.
2025-12-31 11:59:03 +08:00

162 lines
7.5 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.

# 考试模块实现设计文档
## 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
## 7. 变更记录(合并 Homework
**日期**2025-12-31
- 移除 Exams grading 入口与实现:删除阅卷 UI、server action、data-access 查询。
- Exams grading 路由改为重定向到 Homework submissions。