考试与试卷数据模型重构方案
一、问题回顾
当前系统中 考试(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 创建新表
-- 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 数据迁移
-- =============================================
-- 数据迁移脚本
-- =============================================
-- 迁移试卷数据
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 version="1.0" encoding="utf-8"?>
<dynamic-sql>
<!-- 获取试卷列表 -->
<sql id="getPaperList"><![CDATA[
SELECT * FROM et_exam_paper
WHERE isdeleted = 0 AND pg = ?
<@p p=" AND name LIKE ?">name</@p>
<@p p=" AND category LIKE ?">category</@p>
<@p p=" AND createdepartment LIKE ?">createdepartment</@p>
]]></sql>
<!-- 获取试卷详情(含题目) -->
<sql id="getPaperDetail"><![CDATA[
SELECT p.*, q.id AS ques_id, q.subject, q.type, q.score, q.num, q.ismust
FROM et_exam_paper p
INNER JOIN et_exam_paper_question pq ON p.id = pq.paper_id
INNER JOIN et_exam_question q ON pq.question_id = q.id
WHERE p.id = ?
ORDER BY pq.num
]]></sql>
</dynamic-sql>
创建 et_exam_examinations.map.xml:
<?xml version="1.0" encoding="utf-8"?>
<dynamic-sql>
<!-- 获取考试列表 -->
<sql id="getExamList"><![CDATA[
SELECT e.*, p.name AS paper_name, p.description AS paper_desc
FROM et_exam_examinations e
INNER JOIN et_exam_paper p ON e.paper_id = p.id
WHERE e.isdeleted = 0 AND e.pg = ?
<@p p=" AND e.name LIKE ?">name</@p>
]]></sql>
<!-- 获取考试学员列表 -->
<sql id="getExamUserList"><![CDATA[
SELECT ut.*, u.username, u.orgname AS department
FROM et_exam_usertest ut
INNER JOIN et_train_baseuser u ON ut.user_id = u.usercode
WHERE ut.exam_id = ?
<@p p=" AND ut.user LIKE ?">name</@p>
]]></sql>
</dynamic-sql>
3.2.2 修改现有映射文件
修改 et_exam_usertest.map.xml:
| 原 SQL |
修改为 |
from et_exam_exampaper_and_editexampaper |
from et_exam_examinations e INNER JOIN et_exam_paper p ON e.paper_id = p.id |
exampaper.id |
e.id |
exampaper.name |
p.name |
exampaper.sc |
e.duration |
et_exam_editexampaper.edit_id |
et_exam_paper_question.paper_id |
修改 et_exam_limitation.map.xml:
| 原 SQL |
修改为 |
et_exam_limitation.exam_id |
et_exam_examination_limitation.exam_id |
et_exam_exampaper_and_editexampaper.id |
et_exam_examinations.paper_id |
4.3 第三阶段:Java 控制器层重构
3.3.1 新增控制器
// 新增 ExamExaminationController.java
@Controller
@RequestMapping("/exam/examination")
public class ExamExaminationController {
// 考试管理 CRUD
// 考试发布/关闭
// 考试时间修改
// 考试统计
}
3.3.2 修改现有控制器
ExampaperController.java 修改:
- 移除考试时间相关字段 (startdate, enddate, sc)
- 新增
paper_id 关联
- 保留试卷内容管理
ExamController.java 增强:
- 分离考试管理和试卷管理
- 新增"基于试卷创建考试"功能
4.4 第四阶段:前端页面重构
3.4.1 页面拆分
原结构:
exam/exampaper.ftl → 试卷+考试混用
├── exampaper_list.ftl → 试卷列表
├── exampaper_edit.ftl → 试卷+考试信息编辑
重构后:
exam/paper/
├── paper_list.ftl → 试卷列表
├── paper_edit.ftl → 试卷编辑(仅内容)
├── paper_detail.ftl → 试卷预览
exam/examination/
├── exam_list.ftl → 考试列表
├── exam_edit.ftl → 考试编辑(仅时间/人员)
├── exam_start.ftl → 开始答题
└── exam_result.ftl → 考试成绩
3.4.2 新增"创建考试"功能
<!-- exam/examination/exam_create.ftl -->
<div class="formTitle"><span class="icon icon_menu"></span>创建考试</div>
<table class="fromTable">
<tr>
<th>选择试卷:</th>
<td>
<select id="paperSelector" name="paper_id">
<!-- 从 et_exam_paper 加载 -->
</select>
</td>
</tr>
<tr>
<th>考试名称:</th>
<td><input id="examName" name="name" type="text"/></td>
</tr>
<tr>
<th>考试时间:</th>
<td>
<input id="starttime" name="starttime"/> -
<input id="endtime" name="endtime"/>
</td>
</tr>
<tr>
<th>考试时长:</th>
<td><input id="duration" name="duration" type="number"/> 分钟</td>
</tr>
</table>
四、影响范围分析
4.1 涉及文件清单
| 类型 |
文件数 |
主要文件 |
| SQL 映射 |
6 |
et_exam_*.map.xml |
| FTL 模板 |
18 |
exam/*.ftl |
| Java 控制器 |
6 |
Exam*Controller.class |
| Java Service |
4 |
ExamService.class |
| JavaScript |
3 |
exam/*.js |
| 报表 |
3 |
reportlets/exam*.cpt |
4.2 业务功能影响
| 功能模块 |
影响程度 |
说明 |
| 试卷管理 |
中 |
需分离考试时间字段 |
| 考试管理 |
高 |
完全重构 |
| 题库管理 |
低 |
无变化 |
| 答题功能 |
高 |
需关联考试ID |
| 成绩管理 |
中 |
需调整查询逻辑 |
| 统计分析 |
中 |
需新增统计维度 |
| 培训计划关联 |
高 |
需调整关联方式 |
五、重构风险评估
5.1 风险矩阵
| 风险项 |
可能性 |
影响 |
等级 |
缓解措施 |
| 历史数据丢失 |
中 |
高 |
🔴 高 |
完整备份 + 试运行环境验证 |
| 业务逻辑遗漏 |
高 |
高 |
🔴 高 |
逐模块测试 + 功能清单核对 |
| 性能下降 |
低 |
中 |
🟡 中 |
SQL 优化 + 索引添加 |
| 用户体验变化 |
中 |
中 |
🟡 中 |
保持 UI 一致 + 充分培训 |
| 报表不可用 |
中 |
中 |
🟡 中 |
同步更新 FineReport 模板 |
| 回滚困难 |
中 |
高 |
🔴 高 |
蓝绿部署 + 快速回滚脚本 |
5.2 回滚方案
-- 紧急回滚脚本(保留原表备份)
RENAME TABLE
et_exam_paper TO et_exam_paper_backup_20260416,
et_exam_examinations TO et_exam_examinations_backup_20260416,
et_exam_paper_question TO et_exam_paper_question_backup_20260416,
et_exam_examination_limitation TO et_exam_examination_limitation_backup_20260416;
-- 恢复旧表
RENAME TABLE
et_exam_exampaper_backup TO et_exam_exampaper_and_editexampaper;
六、数据迁移详细方案
6.1 迁移前准备
-- 1. 完整备份(必须)
mysqldump -u root -p --single-transaction \
--databases etms \
> backup_etms_20260416.sql
-- 2. 创建迁移日志表
CREATE TABLE et_exam_migration_log (
id INT AUTO_INCREMENT PRIMARY KEY,
step_name VARCHAR(100),
status VARCHAR(20),
records_count INT,
error_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
6.2 分步迁移
-- Step 1: 创建新表结构(无数据)
CREATE TABLE `et_exam_paper` (...) -- 如上述
-- Step 2: 迁移试卷数据
INSERT INTO et_exam_migration_log (step_name, status, records_count)
VALUES ('migrate_paper', 'starting', 0);
INSERT INTO et_exam_paper (...) -- 如上述
INSERT INTO et_exam_migration_log (step_name, status, records_count)
SELECT 'migrate_paper', 'completed', COUNT(*) FROM et_exam_paper;
-- Step 3: 迁移考试数据
INSERT INTO et_exam_migration_log (step_name, status, records_count)
VALUES ('migrate_examination', 'starting', 0);
INSERT INTO et_exam_examinations (...) -- 如上述
INSERT INTO et_exam_migration_log (step_name, status, records_count)
SELECT 'migrate_examination', 'completed', COUNT(*) FROM et_exam_examinations;
-- Step 4: 迁移题目关联
INSERT INTO et_exam_paper_question (...)
SELECT ... FROM et_exam_editexampaper;
-- Step 5: 迁移用户答题数据
UPDATE et_exam_usertest ut
INNER JOIN et_exam_editexampaper eq ON ut.question_id = eq.id
SET ut.exam_id = eq.edit_id;
-- Step 6: 数据验证
SELECT
(SELECT COUNT(*) FROM et_exam_paper) AS paper_count,
(SELECT COUNT(*) FROM et_exam_examinations) AS exam_count,
(SELECT COUNT(*) FROM et_exam_paper_question) AS question_count,
(SELECT COUNT(*) FROM et_exam_usertest WHERE exam_id IS NOT NULL) AS user_answer_count;
6.3 发放范围迁移(班级/课程关联)🔴 重点
这是用户特别关心的业务场景,必须确保迁移后不影响现有功能
6.3.1 发放范围数据结构
当前结构(存在问题):
et_exam_limitation (exam_id 实际是试卷ID)
├── et_exam_limitation_class (exam_id 关联到试卷)
└── et_exam_limitation_course (exam_id 关联到试卷)
重构后结构:
et_exam_examination_limitation (exam_id 关联到考试ID)
├── et_exam_limitation_class (exam_id 关联到考试ID)
└── et_exam_limitation_course (exam_id 关联到考试ID)
6.3.2 迁移 SQL
-- 1. 迁移 et_exam_limitation 表
-- 注意:exam_id 在原表中指向试卷,现在需要改为指向考试
-- 由于原表中 exam_id = 试卷ID,重构后 exam_id = 考试ID
-- 而考试ID = 试卷ID(首次迁移时1:1映射)
INSERT INTO et_exam_examination_limitation (
id, exam_id, limitation, user, user_id
)
SELECT
id,
exam_id, -- 考试ID(原试卷ID)
limitation, -- 发放范围类型
user, -- 用户/部门名称
user_id -- 用户ID/部门代码
FROM et_exam_limitation
WHERE exam_id IS NOT NULL;
-- 2. 迁移 et_exam_limitation_class 表
-- 展开班级人员到 et_exam_examination_limitation
INSERT INTO et_exam_examination_limitation (
id, exam_id, limitation, user, user_id
)
SELECT
md5(UUID()) AS id,
exam_id AS exam_id, -- 考试ID(原试卷ID)
4 AS limitation, -- 班级类型
class_person AS user, -- 班级人员姓名
class_person_id AS user_id -- 班级人员ID
FROM et_exam_limitation_class
WHERE exam_id IS NOT NULL;
-- 3. 迁移 et_exam_limitation_course 表
-- 展开课程人员到 et_exam_examination_limitation
INSERT INTO et_exam_examination_limitation (
id, exam_id, limitation, user, user_id
)
SELECT
md5(UUID()) AS id,
exam_id AS exam_id, -- 考试ID(原试卷ID)
5 AS limitation, -- 课程类型
course_person AS user, -- 课程人员姓名
course_person_id AS user_id -- 课程人员ID
FROM et_exam_limitation_course
WHERE exam_id IS NOT NULL;
6.3.3 迁移验证 SQL
-- 验证1:发放范围数据完整性
SELECT
'原limitation' AS source_table,
COUNT(*) AS record_count
FROM et_exam_limitation
UNION ALL
SELECT
'原limitation_class' AS source_table,
COUNT(*) AS record_count
FROM et_exam_limitation_class
UNION ALL
SELECT
'原limitation_course' AS source_table,
COUNT(*) AS record_count
FROM et_exam_limitation_course
UNION ALL
SELECT
'新examination_limitation' AS source_table,
COUNT(*) AS record_count
FROM et_exam_examination_limitation;
-- 验证2:各类型发放范围数量
SELECT
limitation AS 类型,
CASE limitation
WHEN 1 THEN '全所'
WHEN 2 THEN '部门'
WHEN 3 THEN '人员'
WHEN 4 THEN '班级'
WHEN 5 THEN '课程'
END AS 类型名称,
COUNT(*) AS 数量
FROM et_exam_examination_limitation
GROUP BY limitation
ORDER BY limitation;
-- 验证3:检查 exam_id 是否都能关联到有效的考试记录
SELECT
el.exam_id,
COUNT(*) AS limitation_count,
CASE
WHEN e.id IS NOT NULL THEN '有效'
ELSE '孤立数据'
END AS status
FROM et_exam_examination_limitation el
LEFT JOIN et_exam_examinations e ON el.exam_id = e.id
GROUP BY el.exam_id, e.id;
6.4 迁移后验证
-- 1. 数据完整性检查
SELECT '试卷数据' AS check_item, COUNT(*) AS count FROM et_exam_paper
UNION ALL SELECT '考试数据', COUNT(*) FROM et_exam_examinations
UNION ALL SELECT '题目关联', COUNT(*) FROM et_exam_paper_question
UNION ALL SELECT '用户答题', COUNT(*) FROM et_exam_usertest
UNION ALL SELECT '答题关联', COUNT(*) FROM et_exam_usertest WHERE exam_id IS NOT NULL;
-- 2. 外键关系验证
SELECT '孤立考试' AS issue, COUNT(*) AS count
FROM et_exam_examinations e
LEFT JOIN et_exam_paper p ON e.paper_id = p.id
WHERE p.id IS NULL;
-- 3. 分数汇总验证
SELECT
p.id, p.name,
p.totalpoints AS stored_total,
COALESCE(SUM(pq.score), 0) AS calculated_total,
IF(p.totalpoints = COALESCE(SUM(pq.score), 0), 'OK', 'MISMATCH') AS check_result
FROM et_exam_paper p
LEFT JOIN et_exam_paper_question pq ON p.id = pq.paper_id
GROUP BY p.id;
七、实施计划
7.1 时间估算
| 阶段 |
工作内容 |
工期 |
累计 |
| 第一阶段 |
数据库设计与建表 |
1天 |
1天 |
| 第二阶段 |
数据迁移脚本开发 |
2天 |
3天 |
| 第三阶段 |
SQL 映射层重构 |
3天 |
6天 |
| 第四阶段 |
Java 控制器重构 |
4天 |
10天 |
| 第五阶段 |
前端页面重构 |
3天 |
13天 |
| 第六阶段 |
报表适配 |
2天 |
15天 |
| 第七阶段 |
集成测试 |
3天 |
18天 |
| 第八阶段 |
性能测试与优化 |
2天 |
20天 |
| 第九阶段 |
培训与文档 |
2天 |
22天 |
| 第十阶段 |
灰度发布与上线 |
3天 |
25天 |
总工期:约 25 个工作日(5 周)
7.2 里程碑
| 里程碑 |
日期 |
交付物 |
| M1 - 数据库完成 |
第1天 |
新表结构 + 迁移脚本 |
| M2 - 后端完成 |
第10天 |
重构后的 Java 代码 |
| M3 - 前端完成 |
第13天 |
新页面 |
| M4 - 测试完成 |
第18天 |
测试报告 |
| M5 - 上线 |
第25天 |
正式环境 |
八、测试方案
8.1 测试用例清单
| 模块 |
测试项 |
优先级 |
| 试卷管理 |
创建试卷 |
P0 |
| 试卷管理 |
编辑试卷内容 |
P0 |
| 试卷管理 |
删除试卷 |
P1 |
| 考试管理 |
基于试卷创建考试 |
P0 |
| 考试管理 |
修改考试时间 |
P0 |
| 考试管理 |
发布/关闭考试 |
P0 |
| 答题功能 |
学员答题 |
P0 |
| 答题功能 |
答题记录关联考试 |
P0 |
| 成绩管理 |
成绩统计 |
P0 |
| 统计分析 |
考试通过率 |
P1 |
| 复用场景 |
同一试卷创建多次考试 |
P0 |
8.2 回归测试重点
九、部署方案
9.1 环境规划
| 环境 |
用途 |
数据 |
| 开发环境 |
开发调试 |
脱敏测试数据 |
| 测试环境 |
功能测试 |
生产数据副本(脱敏) |
| 预生产环境 |
灰度验证 |
生产数据副本 |
| 生产环境 |
正式运行 |
生产数据 |
9.2 部署步骤
# 1. 备份生产数据库
./backup_prod.sh
# 2. 执行数据迁移脚本
mysql -u root -p etms < migrate_exam_paper.sql
# 3. 部署应用
./deploy.sh --env=prod --module=exam
# 4. 健康检查
curl http://exam-api/health
# 5. 监控告警
# 检查 error.log, slow_query_log
十、附录
10.1 字段类型对照表
| 原字段 |
新位置 |
新字段 |
类型变化 |
| exampaper.name |
试卷表 |
name |
保留 |
| exampaper.startdate |
考试表 |
starttime |
datetime |
| exampaper.enddate |
考试表 |
endtime |
datetime |
| exampaper.sc |
考试表 |
duration |
int |
| exampaper.state |
试卷表 |
state |
保留 |
| limitation.exam_id |
新表 |
exam_id |
重命名 |
| usertest.exam_id |
同表 |
exam_id |
新增 |
10.2 SQL 变更清单
-- 需要执行的 DDL(按顺序)
1. CREATE TABLE et_exam_paper
2. CREATE TABLE et_exam_examinations
3. CREATE TABLE et_exam_paper_question
4. CREATE TABLE et_exam_examination_limitation
5. ALTER TABLE et_exam_usertest ADD exam_id
6. INSERT INTO et_exam_paper (...)
7. INSERT INTO et_exam_examinations (...)
8. INSERT INTO et_exam_paper_question (...)
9. INSERT INTO et_exam_examination_limitation (...)
10. UPDATE et_exam_usertest SET exam_id = ...
11. CREATE INDEX idx_paper_id ON et_exam_paper_question(paper_id)
12. CREATE INDEX idx_exam_id ON et_exam_usertest(exam_id)
文档版本:v1.0
创建时间:2026-04-16
作者:AI 助手