2026-05-12
This commit is contained in:
253
training/context/03-功能模块/04-考试模块.md
Normal file
253
training/context/03-功能模块/04-考试模块.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# 考试模块(exam)
|
||||
|
||||
> 证据来源:[SRC-FEAT-01] [SRC-FEAT-02] [SRC-SQL-01] [SRC-API-01] [SRC-CODE-01:module/exam/]
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块定位
|
||||
|
||||
- **模块目标**:管理从出题到考试全链路:题目→试卷→考试→作答记录,支持在线作答、自动判分、成绩排名。
|
||||
- **解决问题**:替代纸质考试,提升出卷效率(自动组卷),实现客观题自动判分,并对考试行为(时间窗口、次数限制、超时交卷)进行严格管控。
|
||||
|
||||
---
|
||||
|
||||
## 2. 功能清单
|
||||
|
||||
[SRC-FEAT-01:模块三/四/五] [SRC-API-01:4.4 在线考试接口]
|
||||
|
||||
| 功能名称 | 功能描述 | 输入 | 输出 | 依赖模块 |
|
||||
|---------|---------|------|------|---------|
|
||||
| 题目分类管理 | 按分类管理题目,多级目录,按部门隔离 | QuestionCategoryDTO | QuestionCategoryVO | system |
|
||||
| 题目 CRUD | 创建/修改/删除题目,支持单选/多选/判断三种题型,必须填写解析 | QuestionDTO(type, content, options, answer, analysis) | QuestionVO | - |
|
||||
| 题目状态管理 | 草稿→发布→下架,下架前检查引用 | questionId, action | - | - |
|
||||
| 手动组卷 | 讲师从题库手动选题,配置每题分值 | PaperDTO(title, 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-01:4.3 考试规则] [SRC-API-01:5.2/5.3]
|
||||
|
||||
**题型规则**:
|
||||
- 单选题(SINGLE=0):options 为 JSON 数组,answer 为单字母(A/B/C/D)
|
||||
- 多选题(MULTIPLE=1):answer 为多字母组合(如"AB"/"BCD"),按字母排序存储
|
||||
- 判断题(JUDGE=2):options 为空,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-01:5.2]
|
||||
|
||||
**排名计算**:
|
||||
- 计算时机:用户交卷后立即计算本次提交对应的排名
|
||||
- 统计范围:同一 `exam_id` 下,所有 `status=SUBMITTED` 记录,取每用户最高分
|
||||
- 排名规则:`DENSE_RANK() OVER (ORDER BY MAX(score) DESC, MIN(submit_time) ASC)`(同分按提交时间早排前)[SRC-API-01:5.3]
|
||||
|
||||
**题目顺序**:
|
||||
- 自动组卷时,返回给学员的题目顺序随机打乱(前端处理或后端随机返回)
|
||||
|
||||
### 3.2 校验逻辑
|
||||
|
||||
| 校验项 | 规则 | 失败响应 |
|
||||
|-------|------|---------|
|
||||
| 题目 analysis 必填 | 发布时 analysis 不可为空 | 400:请填写答案解析后再发布 |
|
||||
| 开始考试-时间窗口 | NOW() 在 [start_time, end_time] 范围内 | 400:考试尚未开始 / 考试已结束 |
|
||||
| 开始考试-次数限制 | 已有考试记录数 < max_attempts | 400:已达到最大考试次数 |
|
||||
| 开始考试-状态 | exam.status = IN_PROGRESS(1) | 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-01:4.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 记录恢复,重新进入走断点续考流程 |
|
||||
Reference in New Issue
Block a user