etms/EXAM_PAPER_REFACTOR_PLAN.md
liyuchen cfc83209be Update: 完善考试与试卷重构方案
新增内容:
1. 试卷完整业务操作清单(65项操作)
   - 试卷管理操作(17项)
   - 试卷题目操作(12项)
   - 发放范围limitation操作(16项)🔴重点
   - 考试执行操作(12项)
   - 补考/重考操作(8项)

2. 发放范围迁移详细方案
   - 数据结构说明
   - 完整迁移SQL
   - 迁移验证SQL

确保数据拆分后不影响班级/课程关联等业务流程
2026-04-17 07:55:36 +08:00

35 KiB
Raw Permalink Blame History

考试与试卷数据模型重构方案

一、问题回顾

当前系统中 考试(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 助手