diff --git a/EXAM_PAPER_REFACTOR_PLAN.md b/EXAM_PAPER_REFACTOR_PLAN.md new file mode 100644 index 0000000..a7d7b51 --- /dev/null +++ b/EXAM_PAPER_REFACTOR_PLAN.md @@ -0,0 +1,679 @@ +# 考试与试卷数据模型重构方案 + +## 一、问题回顾 + +当前系统中 **考试(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 创建新表 + +```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; +``` + +--- + +### 3.2 第二阶段:SQL 映射层重构 + +#### 3.2.1 新建映射文件 + +创建 `et_exam_paper.map.xml`: + +```xml + + + + name + <@p p=" AND category LIKE ?">category + <@p p=" AND createdepartment LIKE ?">createdepartment + ]]> + + + + +``` + +创建 `et_exam_examinations.map.xml`: + +```xml + + + + name + ]]> + + + name + ]]> + +``` + +#### 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 新增控制器 + +```java +// 新增 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 新增"创建考试"功能 + +```html + +
创建考试
+ + + + + + + + + + + + + + + + + +
选择试卷: + +
考试名称:
考试时间: + - + +
考试时长: 分钟
+``` + +--- + +## 四、影响范围分析 + +### 4.1 涉及文件清单 + +| 类型 | 文件数 | 主要文件 | +|------|--------|----------| +| SQL 映射 | 6 | et_exam_*.map.xml | +| FTL 模板 | 18 | exam/*.ftl | +| Java 控制器 | 6 | Exam*Controller.class | +| Java Service | 4 | Exam*Service*.class | +| JavaScript | 3 | exam/*.js | +| 报表 | 3 | reportlets/exam*.cpt | + +### 4.2 业务功能影响 + +| 功能模块 | 影响程度 | 说明 | +|----------|----------|------| +| 试卷管理 | 中 | 需分离考试时间字段 | +| 考试管理 | 高 | 完全重构 | +| 题库管理 | 低 | 无变化 | +| 答题功能 | 高 | 需关联考试ID | +| 成绩管理 | 中 | 需调整查询逻辑 | +| 统计分析 | 中 | 需新增统计维度 | +| 培训计划关联 | 高 | 需调整关联方式 | + +--- + +## 五、重构风险评估 + +### 5.1 风险矩阵 + +| 风险项 | 可能性 | 影响 | 等级 | 缓解措施 | +|--------|--------|------|------|----------| +| 历史数据丢失 | 中 | 高 | 🔴 高 | 完整备份 + 试运行环境验证 | +| 业务逻辑遗漏 | 高 | 高 | 🔴 高 | 逐模块测试 + 功能清单核对 | +| 性能下降 | 低 | 中 | 🟡 中 | SQL 优化 + 索引添加 | +| 用户体验变化 | 中 | 中 | 🟡 中 | 保持 UI 一致 + 充分培训 | +| 报表不可用 | 中 | 中 | 🟡 中 | 同步更新 FineReport 模板 | +| 回滚困难 | 中 | 高 | 🔴 高 | 蓝绿部署 + 快速回滚脚本 | + +### 5.2 回滚方案 + +```sql +-- 紧急回滚脚本(保留原表备份) +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 迁移前准备 + +```sql +-- 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 分步迁移 + +```sql +-- 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 迁移后验证 + +```sql +-- 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 部署步骤 + +```bash +# 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 变更清单 + +```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 助手*