etms/EXAM_PAPER_REFACTOR_PLAN.md
liyuchen 2779536eb3 Add: 考试与试卷数据模型重构详细方案
包含:
1. 目标数据模型设计(3NF规范化)
2. 数据库层重构方案(新建4张表)
3. SQL映射层重构(6个文件修改)
4. Java控制器层重构
5. 前端页面重构方案
6. 影响范围分析(18+文件)
7. 风险评估矩阵(6大风险)
8. 数据迁移详细方案(含验证SQL)
9. 实施计划(25个工作日,5周)
10. 测试方案与部署方案
2026-04-16 17:42:29 +08:00

23 KiB
Raw Blame History

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

一、问题回顾

当前系统中 考试(Exam)试卷(Paper) 共用同一数据对象 et_exam_exampaper_and_editexampaper,导致:

  • 同一份试卷不能用于多次考试
  • 考试时间属性绑定在试卷上
  • 业务边界模糊,难以维护

二、目标数据模型

2.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    │  │
│                                                         └─────────────────┘  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

2.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

三、详细重构方案

3.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;

3.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

3.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 增强:

  • 分离考试管理和试卷管理
  • 新增"基于试卷创建考试"功能

3.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 迁移后验证

-- 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 助手