Files
training-system/training/context/03-功能模块/04-考试模块.md
2026-05-12 12:24:11 +08:00

254 lines
11 KiB
Markdown
Raw 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.

# 考试模块exam
> 证据来源:[SRC-FEAT-01] [SRC-FEAT-02] [SRC-SQL-01] [SRC-API-01] [SRC-CODE-01module/exam/]
---
## 1. 模块定位
- **模块目标**:管理从出题到考试全链路:题目→试卷→考试→作答记录,支持在线作答、自动判分、成绩排名。
- **解决问题**:替代纸质考试,提升出卷效率(自动组卷),实现客观题自动判分,并对考试行为(时间窗口、次数限制、超时交卷)进行严格管控。
---
## 2. 功能清单
[SRC-FEAT-01模块三/四/五] [SRC-API-014.4 在线考试接口]
| 功能名称 | 功能描述 | 输入 | 输出 | 依赖模块 |
|---------|---------|------|------|---------|
| 题目分类管理 | 按分类管理题目,多级目录,按部门隔离 | QuestionCategoryDTO | QuestionCategoryVO | system |
| 题目 CRUD | 创建/修改/删除题目,支持单选/多选/判断三种题型,必须填写解析 | QuestionDTOtype, content, options, answer, analysis | QuestionVO | - |
| 题目状态管理 | 草稿→发布→下架,下架前检查引用 | questionId, action | - | - |
| 手动组卷 | 讲师从题库手动选题,配置每题分值 | PaperDTOtitle, questions[] | PaperVO | - |
| 自动组卷 | 按规则单选X题+多选Y题+判断Z题随机抽题 | AutoPaperDTO规则配置 | PaperVO | - |
| 试卷配置 | 设置总分、各题分值、考试时长、及格分 | PaperDTO | PaperVO | - |
| 试卷预览 | 组卷后预览试卷效果(随机题目顺序) | paperId | 完整试卷预览 | - |
| 试卷状态管理 | 草稿→发布→下架 | paperId, action | - | - |
| 创建考试 | 选择试卷,设置考试名称、时间窗口、最大次数 | ExamQueryDTO + ExamTarget | ExamVO | - |
| 指定考试对象 | 指定部门/小组/个人参加考试 | exam_id, target_type, target_id[] | - | system |
| 发布考试 | 将考试状态从 NOT_STARTED 转为 IN_PROGRESS时间到自动 | examId | - | - |
| 学员-我的考试列表 | 查询分配给当前学员的考试(在时间窗口内) | - | PageResult<ExamVO + 我的记录> | auth |
| 学员-开始考试 | 校验资格,创建 exam_record返回试题 | examId | 试题 + 时长 + recordId | - |
| 学员-断点续考 | 恢复进行中的考试记录,返回已保存答案 | examId | 试题 + 已保存答案 + 剩余时间 | - |
| 学员-保存答案 | 每30秒自动保存答案增量保存 | recordId, answers(JSON) | - | - |
| 学员-提交试卷 | 自动判分,保存成绩,返回排名+解析 | recordId, answers(JSON) | 成绩 + 排名 + 答案解析 | - |
| 超时自动交卷 | 定时任务每分钟扫描超时记录,系统代提交 | @Scheduled | - | - |
| 查看考试结果 | 查看某次考试的详细成绩和答案解析 | recordId | ExamResultVO | - |
| 成绩/排名查询 | 讲师查看某考试的所有学员成绩 | examId | PageResult<成绩列表> | - |
---
## 3. 核心逻辑
### 3.1 业务规则
[SRC-FEAT-014.3 考试规则] [SRC-API-015.2/5.3]
**题型规则**
- 单选题SINGLE=0options 为 JSON 数组answer 为单字母A/B/C/D
- 多选题MULTIPLE=1answer 为多字母组合(如"AB"/"BCD"),按字母排序存储
- 判断题JUDGE=2options 为空answer 为"T"(正确)或"F"(错误)
- 每道题必须填写 `analysis`(答案解析),否则不允许发布
**考试时间窗口**
- 只有在 `exam.start_time ≤ NOW() ≤ exam.end_time` 时,学员才能进入考试
- 考试到期后,`exam.status` 自动(或手动)变为 ENDED
**次数限制**
- 达到 `max_attempts` 后,学员不可再次开始考试
- 历史成绩取**最高分**作为最终成绩
- 每次考试创建 `exam_record.attempt_no = 历史次数 + 1`
**自动判分规则**
- 仅客观题(单选/多选/判断)自动判分
- 单选/判断:答案完全匹配得满分
- 多选:全部选项匹配(顺序无关)得满分;部分正确得 0 分V1 不分项给分)
- 总分 = 各题得分之和
**超时自动交卷**
- `@Scheduled(cron="0 */1 * * * ?")` 每分钟扫描
- 查询:`ex_exam_record.status=IN_PROGRESS``start_time + paper.duration 分钟 < NOW()`
- 使用乐观锁(`version` 字段)防止与用户主动交卷并发冲突 [SRC-API-015.2]
**排名计算**
- 计算时机:用户交卷后立即计算本次提交对应的排名
- 统计范围:同一 `exam_id` 下,所有 `status=SUBMITTED` 记录,取每用户最高分
- 排名规则:`DENSE_RANK() OVER (ORDER BY MAX(score) DESC, MIN(submit_time) ASC)`(同分按提交时间早排前)[SRC-API-015.3]
**题目顺序**
- 自动组卷时,返回给学员的题目顺序随机打乱(前端处理或后端随机返回)
### 3.2 校验逻辑
| 校验项 | 规则 | 失败响应 |
|-------|------|---------|
| 题目 analysis 必填 | 发布时 analysis 不可为空 | 400请填写答案解析后再发布 |
| 开始考试-时间窗口 | NOW() 在 [start_time, end_time] 范围内 | 400考试尚未开始 / 考试已结束 |
| 开始考试-次数限制 | 已有考试记录数 < max_attempts | 400已达到最大考试次数 |
| 开始考试-状态 | exam.status = IN_PROGRESS1 | 400状态不符 |
| 开始考试-进行中记录 | 若已有 IN_PROGRESS 记录返回续考而非新建 | 自动转入断点续考流程 |
| 答案格式 | 多选题答案字母需排序后匹配 | 自动处理不返回错误 |
| 超时交卷并发 | 乐观锁冲突时用户已提交忽略定时任务提交 | 静默忽略 OptimisticLockException |
| 试卷引用保护 | 已被考试引用的试卷下架前需确认警告 | 400 + 警告 + 引用考试数量 |
| 题目引用保护 | 已被试卷引用的题目下架前需确认警告 | 400 + 警告 + 引用试卷数量 |
### 3.3 状态流转
**内容状态(题目/试卷)**
```mermaid
stateDiagram-v2
[*] --> DRAFT : 创建
DRAFT --> PUBLISHED : 发布(题目需有 analysis
PUBLISHED --> OFFLINE : 下架(引用保护确认)
OFFLINE --> PUBLISHED : 重新上架
DRAFT --> [*] : 逻辑删除
```
**考试状态**
```mermaid
stateDiagram-v2
[*] --> NOT_STARTED : 创建考试start_time 未到)
NOT_STARTED --> IN_PROGRESS : 到达 start_time自动或手动
IN_PROGRESS --> ENDED : 到达 end_time自动或手动结束
ENDED --> [*] : 逻辑删除
```
**考试记录状态**
```mermaid
stateDiagram-v2
[*] --> IN_PROGRESS : 学员点击"开始考试"创建记录
IN_PROGRESS --> SUBMITTED : 学员交卷submit_source=USER
IN_PROGRESS --> SUBMITTED : 定时任务超时submit_source=SYSTEM_TIMEOUT
SUBMITTED --> [*] : 永久保留
```
---
## 4. 数据结构
[SRC-SQL-01考试模块]
### 4.1 涉及数据表7张
**ex_question_category**题目分类同知识库分类结构 department_id 隔离
**ex_question**
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 主键 |
| type | TINYINT | 0-单选 / 1-多选 / 2-判断 |
| content | TEXT NOT NULL | 题干 |
| options | JSON | 选项数组判断题为 NULL |
| answer | VARCHAR(50) NOT NULL | 正确答案单选/判断单字母多选合并字母 |
| analysis | TEXT | 答案解析发布前必填 |
| category_id | BIGINT | 所属分类 |
| department_id | BIGINT NOT NULL | 部门隔离键 |
| status | TINYINT | 0-草稿 / 1-发布 / 2-下架 |
| creator_id | BIGINT NOT NULL | 创建人 |
**ex_paper**
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 主键 |
| title | VARCHAR(200) NOT NULL | 试卷标题 |
| total_score | INT DEFAULT 100 | 总分 |
| duration | INT DEFAULT 60 | 考试时长分钟 |
| pass_score | INT DEFAULT 60 | 及格分 |
| department_id | BIGINT NOT NULL | 部门隔离键 |
| status | TINYINT | 0-草稿 / 1-发布 / 2-下架 |
**ex_paper_question**
| 字段 | 类型 | 说明 |
|------|------|------|
| paper_id | BIGINT NOT NULL | 试卷ID |
| question_id | BIGINT NOT NULL | 题目ID |
| score | INT DEFAULT 5 | 该题分值 |
| sort_order | INT | 排序大题号顺序 |
**ex_exam**
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 主键 |
| title | VARCHAR(200) NOT NULL | 考试标题 |
| paper_id | BIGINT NOT NULL | 关联试卷 |
| start_time / end_time | DATETIME | 时间窗口 |
| max_attempts | INT DEFAULT 1 | 最大考试次数 |
| department_id | BIGINT NOT NULL | 部门隔离键 |
| status | TINYINT | 0-未开始 / 1-进行中 / 2-已结束 |
**ex_exam_target**
| 字段 | 类型 | 说明 |
|------|------|------|
| exam_id | BIGINT NOT NULL | 考试ID |
| target_type | TINYINT | 0-部门 / 1-小组 / 2-个人 |
| target_id | BIGINT NOT NULL | 对应 dept/group/user 的ID |
**ex_exam_record** V1.0.1 扩展字段
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 主键 |
| exam_id | BIGINT NOT NULL | 考试ID |
| user_id | BIGINT NOT NULL | 学员ID |
| attempt_no | INT NOT NULL | 第几次考试 |
| score | INT | 得分 |
| passed | TINYINT | 0-未通过 / 1-通过 |
| start_time | DATETIME NOT NULL | 开始时间 |
| submit_time | DATETIME | 提交时间 |
| answers | JSON | 答题详情 |
| status | VARCHAR(20) | IN_PROGRESS / SUBMITTED |
| last_save_time | DATETIME | 最后保存时间 |
| version | INT DEFAULT 0 | 乐观锁版本号 |
| submit_source | VARCHAR(20) | USER / SYSTEM_TIMEOUT |
### 4.2 数据关联
- `ex_paper_question.paper_id` `ex_paper.id`
- `ex_paper_question.question_id` `ex_question.id`
- `ex_exam.paper_id` `ex_paper.id`
- `ex_exam_target` 多态关联target_type=0 `sys_department.id`=1 `sys_group.id`=2 `sys_user.id`
- `ex_exam_record.exam_id` `ex_exam.id``user_id` `sys_user.id`
---
## 5. 对外接口
[SRC-API-014.4]
| API 名称 | 方法 | 路径 | 权限 |
|---------|------|------|------|
| 题目分类 CRUD | GET/POST/PUT/DELETE | /api/exam/question-category/** | LECTURER/ADMIN |
| 题目 CRUD | GET/POST/PUT/DELETE | /api/exam/question/** | LECTURER/ADMIN |
| 试卷 CRUD + 自动组卷 | GET/POST/PUT/DELETE | /api/exam/paper/** + /api/exam/paper/auto | LECTURER/ADMIN |
| 考试 CRUD | GET/POST/PUT/DELETE | /api/exam/** | LECTURER/ADMIN |
| 我的考试列表 | GET | /api/student/exam | STUDENT |
| 考试详情 | GET | /api/student/exam/{id} | STUDENT |
| 开始考试 | POST | /api/student/exam/{id}/start | STUDENT |
| 断点续考 | GET | /api/student/exam/{id}/continue | STUDENT |
| 保存答案 | POST | /api/student/exam/{id}/save | STUDENT |
| 提交试卷 | POST | /api/student/exam/{id}/submit | STUDENT |
| 查看考试结果 | GET | /api/student/exam/record/{recordId} | STUDENT |
---
## 6. 异常与边界处理
| 场景 | 处理方式 |
|------|---------|
| 学员在时间窗口外访问考试 | 400 + 明确提示是"未开始"还是"已结束" |
| 次数用完后再次开始 | 400已达最大考试次数如有异议请联系讲师 |
| 交卷时 exam_record 已是 SUBMITTED并发/重复提交 | 乐观锁冲突或状态判断幂等返回已有成绩不重复判分 |
| 定时任务超时提交时用户同时提交 | 乐观锁 version 冲突定时任务忽略用户提交成功 |
| 试卷总分与题目分值之和不一致 | [ASSUMPTION] PRD 未明确建议前端组卷时实时计算并提示差额 |
| 自动组卷时题库数量不足 | 400题库中相应题型数量不足请手动添加题目或降低要求 |
| 学员中途关闭浏览器 | answers 按最后一次 save 记录恢复重新进入走断点续考流程 |