# 考试与试卷数据模型重构方案
## 一、问题回顾
当前系统中 **考试(Exam)** 和 **试卷(Paper)** 共用同一数据对象 `et_exam_exampaper_and_editexampaper`,导致:
- 同一份试卷不能用于多次考试
- 考试时间属性绑定在试卷上
- 业务边界模糊,难以维护
---
## 二、试卷完整业务操作清单
> **重要**:确保数据拆分后不影响以下所有业务流程
### 2.1 试卷管理操作(17项)
| 操作类型 | 操作名称 | 涉及的数据库表/SQL | 影响分析 |
|---------|---------|-------------------|---------|
| **CRUD** | 试卷列表查询 | `et_exam_exampaper_and_editexampaper` | 需拆分为 paper + examinations |
| **CRUD** | 试卷新增 | INSERT 到视图 | 需改为 INSERT 到 `et_exam_paper` |
| **CRUD** | 试卷编辑 | UPDATE 视图 | 需改为 UPDATE `et_exam_paper` |
| **CRUD** | 试卷删除 | DELETE + 级联删除 | 需保留级联逻辑 |
| **CRUD** | 试卷详情查看 | JOIN 查询 | 需重新设计 JOIN |
| **预览** | 试卷预览 | `preview()` / `previewPage()` | 不涉及业务拆分 |
| **导入** | 试卷导入 | `fileImport()` | 不涉及业务拆分 |
### 2.2 试卷题目操作(12项)
| 操作类型 | 操作名称 | 涉及的SQL ID | 影响分析 |
|---------|---------|-------------|---------|
| **题目查询** | 获取试卷题目列表 | `getExampaperEditList` | 需改为 JOIN `et_exam_paper_question` |
| **题目统计** | 获取各题型数量 | `getCount` | 需改为统计 `et_exam_paper_question` |
| **历史问卷** | 保存历史问卷 | `insertHistoryExam` | 需复制到新试卷 |
| **随机组卷** | 随机添加试题 | `insertRandExam` | 需 INSERT 到 `et_exam_paper_question` |
| **序号更新** | 更新题目序号 | `updateRandExamNum` | 需更新 `et_exam_paper_question.num` |
| **题目查询** | 返回试卷信息 | `getExampaperReturn` | 需查询 `et_exam_paper` |
| **题目数量** | 获取题目数量 | `getQuestionNum` | 需 COUNT `et_exam_paper_question` |
| **题型数量** | 获取各题型数量 | `getNumber` | 需按 type_num 统计 |
| **序号修改** | 修改题目序号 | `updateOtherNumber` | 需更新 `et_exam_paper_question.num` |
| **单题修改** | 修改单题序号 | `updateNewNumber` | 需更新 `et_exam_paper_question.num` |
| **题目删除** | 删除试卷题目 | `DeleteExamQuestion` | 需 DELETE `et_exam_paper_question` |
### 2.3 发放范围(limitation)操作(16项)🔴 关键
> **这是与班级/课程关联的核心模块,必须完整保留**
| 操作类型 | 操作名称 | 涉及的SQL/URL | 关键字段 |
|---------|---------|--------------|---------|
| **发放范围** | 查询发放范围列表 | `limitationUrl` | `exam_id`, `limitation` |
| **全所发放** | 设置全所发放 | `saveLimitationUrl` (limitation=1) | 需关联 examination |
| **部门发放** | 添加部门发放范围 | `departmentAddUrl` | 需关联 examination |
| **人员发放** | 添加人员发放范围 | `saveLimitationUrl` (limitation=3) | 需关联 examination |
| **班级发放** | 添加班级发放范围 | `saveClassUrl` | `et_exam_limitation_class` |
| **课程发放** | 添加课程发放范围 | `saveCourseUrl` | `et_exam_limitation_course` |
| **删除发放** | 删除发放范围 | `deleteLimitationUrl` | 需 DELETE + 级联 |
| **删除其他** | 删除其他发放范围 | `deleteOtherUrl` | 需 DELETE + 级联 |
| **班级人员** | 获取班级人员列表 | `classComboxUrl` | 需关联 `v_class_user` |
| **课程人员** | 获取课程人员列表 | `courseComboxUrl` | 需关联课程学员 |
| **更新班级** | 更新班级对应人的exam_id | `updateClassUrl` | 需关联 examination |
| **更新课程** | 更新课程对应人的exam_id | `updateCourseUrl` | 需关联 examination |
| **更新limitation** | 更新limitation的exam_id | `updateLimitationUrl` | 需关联 examination |
| **删除limitation** | 依据试卷id删除limitation | `deleteLimitationExampaperUrl` | 需关联 examination |
| **删除人员** | 删除课程班级对应的人 | `deletePersonUrl` | 需关联 examination |
| **人员导入** | 发放范围人员导入 | `personImportUrl` | 需关联 examination |
### 2.4 考试执行操作(12项)
| 操作类型 | 操作名称 | 涉及的SQL/URL | 影响分析 |
|---------|---------|-------------|---------|
| **答题查询** | 获取考试题目列表 | `getExamTestList` | 需改为 JOIN examination |
| **成绩查询** | 获取考试成绩列表 | `getExamResultList` | 需 JOIN examination |
| **判卷列表** | 获取待判卷列表 | `getExamMarkList` | 需 JOIN examination |
| **成绩统计** | 获取个人成绩统计 | `getExamScoreList` | 需 JOIN examination |
| **提交查询** | 查询未提交用户 | `getIsNotSubmitList` | 需关联 examination |
| **机构详情** | 获取机构学员详情 | `getInstitutionDetail` | 需关联 examination |
| **部门详情** | 获取部门学员详情 | `getDepartmentDetail` | 需关联 examination |
| **个人详情** | 获取个人考试详情 | `getPersonDetail` | 需关联 examination |
| **班级详情** | 获取班级考试详情 | `getClassDetail` | 需关联 examination |
| **课程详情** | 获取课程考试详情 | `getCourseDetail` | 需关联 examination |
| **自动判卷** | 自动评分 | `updateUserScore` | 需 JOIN examination |
| **补考推送** | 推送补考人员 | `insertInstituteResit` 等 | 需关联 examination |
### 2.5 补考/重考操作(8项)
| 操作类型 | 操作名称 | 涉及的SQL ID | 关键逻辑 |
|---------|---------|-------------|---------|
| **插入补考** | 插入机构补考 | `insertInstituteResit` | 需创建新 examination |
| **插入补考** | 插入部门补考 | `insertDepartmentResit` | 需创建新 examination |
| **插入补考** | 插入个人补考 | `insertPersonResit` | 需创建新 examination |
| **插入补考** | 插入班级补考 | `insertClassResit` | 需创建新 examination |
| **插入补考** | 插入课程补考 | `insertCourseResit` | 需创建新 examination |
| **保存补考试卷** | 保存原试卷试题至补考 | `saveResitPaper` | 需复制 paper_question |
| **删除补考** | 删除临时试卷 | `deleteTemporaryExam` | 需关联 examination |
| **更新补考ID** | 更新补考exam_id | `updateLimitationExamId` | 需关联 examination |
### 2.6 发放范围类型详解
```
limitation 字段含义:
├── 1 = 全所(所有人员)
├── 2 = 部门(指定部门及下属人员)
├── 3 = 人员(指定个人)
├── 4 = 班级(指定班级的人员)→ 关联 et_exam_limitation_class
└── 5 = 课程(指定课程的人员)→ 关联 et_exam_limitation_course
```
### 2.7 关键业务流程依赖关系
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 试卷发放范围业务流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 用户操作 后端处理 数据库操作 │
│ ───────── ──────── ────────── │
│ │
│ 选择"班级发放" ──────► saveLimitationUrl ──────► INSERT et_exam_limitation │
│ │ │ (limitation=4) │
│ ▼ ▼ │
│ 选择班级 ─────────► saveClassUrl ──────► INSERT et_exam_limitation_class │
│ │ │ (展开班级人员) │
│ ▼ ▼ │
│ 点击发布 ─────────► updateClassUrl ──────► UPDATE et_exam_limitation_class │
│ │ SET exam_id = 正式ID │
│ ▼ │
│ 发布成功 ──────► 班级学员可看到考试 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
### 3.1 核心表结构设计
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 重构后数据模型 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ et_exam_paper │ │ et_exam_examination │ │
│ │ (试卷主表) │◄───────│ (考试主表) │ │
│ ├─────────────────────┤ paper_id├─────────────────────┤ │
│ │ id (PK) │ │ id (PK) │ │
│ │ name │ │ paper_id (FK) │ │
│ │ description │ │ name │ │
│ │ category │ │ starttime │ │
│ │ categoryid │ │ endtime │ │
│ │ passpoint │ │ duration │ │
│ │ totalpoints │ │ shouldjoin │ │
│ │ creatperson │ │ realjoin │ │
│ │ creatpersonid │ │ leader │ │
│ │ createdepartment │ │ leaderid │ │
│ │ edittime │ │ createtime │ │
│ │ state │ │ state │ │
│ │ pg (1考试/2问卷) │ │ pg (1考试/2问卷) │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │ │
│ │ exam_id │
│ ▼ │
│ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────┐ │
│ │ et_exam_question │ │ et_exam_paper_question│ │et_exam_usertest│ │
│ │ (题目题库) │◄───│ (试卷题目关联) │◄───│ (用户答题) │ │
│ ├─────────────────────┤ ├─────────────────────┤ ├─────────────────┤ │
│ │ id (PK) │ │ id (PK) │ │ id (PK) │ │
│ │ type │ │ paper_id (FK) │ │ exam_id (FK) │ │
│ │ subject │ │ question_id (FK) │ │ question_id(FK) │ │
│ │ answer │ │ score │ │ user_id │ │
│ │ optionA~F │ │ ismust │ │ user_answer │ │
│ │ ... │ │ num │ │ user_score │ │
│ └─────────────────────┘ └─────────────────────┘ │ mark_teacher │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### 3.2 新旧表字段对照
| 新表 et_exam_paper | 来自 | 新表 et_exam_examinations | 来自 |
|-------------------|------|--------------------------|------|
| id | - | id | - |
| name | exampaper.name | paper_id | exampaper.id |
| description | exampaper.description | name | 关联 paper.name + "第N次考试" |
| category | exampaper.category | starttime | exampaper.startdate |
| categoryid | exampaper.categoryid | endtime | exampaper.enddate |
| passpoint | exampaper.passpoint | duration | exampaper.sc |
| totalpoints | 计算得出 | shouldjoin | limitation.count |
| creatperson | exampaper.creatperson | realjoin | 计算得出 |
| creatpersonid | exampaper.creatpersonid | leader | exampaper.leader |
| createdepartment | exampaper.createdepartment | leaderid | exampaper.leaderid |
| edittime | exampaper.edittime | createtime | exampaper.edittime |
| state | exampaper.state | state | exampaper.state |
| pg | exampaper.pg | pg | exampaper.pg |
---
## 四、详细重构方案
### 4.1 第一阶段:数据库层重构
#### 3.1.1 创建新表
```sql
-- 1. 创建试卷主表
CREATE TABLE `et_exam_paper` (
`id` varchar(32) NOT NULL COMMENT '试卷ID',
`name` varchar(50) NOT NULL COMMENT '试卷名称',
`description` varchar(500) DEFAULT NULL COMMENT '试卷描述',
`category` varchar(20) DEFAULT NULL COMMENT '所属知识点',
`categoryid` varchar(8) DEFAULT NULL COMMENT '知识点编码',
`passpoint` int(8) DEFAULT NULL COMMENT '及格分数',
`totalpoints` int(8) DEFAULT NULL COMMENT '试卷总分',
`creatperson` varchar(10) DEFAULT NULL COMMENT '创建人',
`creatpersonid` varchar(32) DEFAULT NULL COMMENT '创建人ID',
`createdepartment` varchar(50) DEFAULT NULL COMMENT '部门',
`edittime` datetime(6) DEFAULT NULL COMMENT '最后编辑时间',
`state` int(8) DEFAULT 1 COMMENT '状态(1草稿/2已发布)',
`pg` int(1) DEFAULT 1 COMMENT '类型(1考试/2问卷)',
`isdeleted` int(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 2. 创建考试主表
CREATE TABLE `et_exam_examinations` (
`id` varchar(32) NOT NULL COMMENT '考试ID',
`paper_id` varchar(32) NOT NULL COMMENT '关联试卷ID',
`name` varchar(50) NOT NULL COMMENT '考试名称',
`starttime` datetime(6) DEFAULT NULL COMMENT '开始时间',
`endtime` datetime(6) DEFAULT NULL COMMENT '结束时间',
`duration` int(8) DEFAULT NULL COMMENT '时长(分钟)',
`shouldjoin` int(8) DEFAULT 0 COMMENT '应考人数',
`realjoin` int(8) DEFAULT 0 COMMENT '实考人数',
`leader` varchar(20) DEFAULT NULL COMMENT '负责人',
`leaderid` varchar(32) DEFAULT NULL COMMENT '负责人ID',
`createtime` datetime(6) DEFAULT NULL COMMENT '创建时间',
`state` varchar(20) DEFAULT '未开始' COMMENT '状态',
`pg` int(1) DEFAULT 1 COMMENT '类型(1考试/2问卷)',
`isdeleted` int(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_paper_id` (`paper_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 3. 创建试卷题目关联表
CREATE TABLE `et_exam_paper_question` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`paper_id` varchar(32) NOT NULL COMMENT '试卷ID',
`question_id` varchar(32) NOT NULL COMMENT '题目ID',
`score` int(8) DEFAULT NULL COMMENT '分值',
`ismust` int(1) DEFAULT 1 COMMENT '是否必答',
`num` int(8) DEFAULT NULL COMMENT '题目序号',
`type` varchar(20) DEFAULT NULL COMMENT '题型',
`type_num` int(8) DEFAULT NULL COMMENT '题型序号',
PRIMARY KEY (`id`),
KEY `idx_paper_id` (`paper_id`),
KEY `idx_question_id` (`question_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 4. 创建考试限制表(新)
CREATE TABLE `et_exam_examination_limitation` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`exam_id` varchar(32) NOT NULL COMMENT '考试ID',
`limitation` int(1) DEFAULT NULL COMMENT '发放范围类型',
`user` varchar(20) DEFAULT NULL COMMENT '用户名',
`user_id` varchar(32) DEFAULT NULL COMMENT '用户ID',
PRIMARY KEY (`id`),
KEY `idx_exam_id` (`exam_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 5. 修改用户答题表
ALTER TABLE `et_exam_usertest`
ADD COLUMN `exam_id` varchar(32) NOT NULL COMMENT '考试ID' AFTER `id`,
DROP COLUMN `name`,
DROP COLUMN `department`,
MODIFY COLUMN `user_id` varchar(32) NOT NULL COMMENT '用户ID',
ADD INDEX `idx_exam_id` (`exam_id`);
```
#### 3.1.2 数据迁移
```sql
-- =============================================
-- 数据迁移脚本
-- =============================================
-- 迁移试卷数据
INSERT INTO et_exam_paper (
id, name, description, category, categoryid,
passpoint, totalpoints, creatperson, creatpersonid,
createdepartment, edittime, state, pg
)
SELECT
id,
name,
description,
category,
categoryid,
passpoint,
(SELECT SUM(score) FROM et_exam_editexampaper WHERE edit_id = exampaper.id) AS totalpoints,
creatperson,
creatpersonid,
createdepartment,
edittime,
state,
pg
FROM et_exam_exampaper_and_editexampaper
WHERE pg IN (1, 2);
-- 迁移考试数据(每次唯一的试卷创建一条考试记录)
INSERT INTO et_exam_examinations (
id, paper_id, name, starttime, endtime,
duration, createtime, state, pg
)
SELECT
id, -- 考试ID(复用原试卷ID)
id AS paper_id, -- 关联试卷ID(自关联,因为现在是1:1)
CONCAT(name, '-首次考试') AS name,
startdate,
enddate,
sc,
edittime,
state,
pg
FROM et_exam_exampaper_and_editexampaper
WHERE pg IN (1, 2);
-- 迁移试卷题目关联
INSERT INTO et_exam_paper_question (
id, paper_id, question_id, score, ismust, num, type, type_num
)
SELECT
md5(UUID()) AS id,
edit_id AS paper_id,
id AS question_id,
score,
ismust,
num,
type,
type_num
FROM et_exam_editexampaper;
-- 迁移考试限制数据
INSERT INTO et_exam_examination_limitation (
id, exam_id, limitation, user, user_id
)
SELECT
id,
exam_id,
limitation,
user,
user_id
FROM et_exam_limitation;
-- 迁移用户答题数据
UPDATE et_exam_usertest ut
INNER JOIN et_exam_editexampaper eq ON ut.question_id = eq.id
SET ut.exam_id = eq.edit_id;
```
---
### 4.2 第二阶段:SQL 映射层重构
#### 3.2.1 新建映射文件
创建 `et_exam_paper.map.xml`:
```xml
| 选择试卷: | |
|---|---|
| 考试名称: | |
| 考试时间: | - |
| 考试时长: | 分钟 |