chore(sync): 同步当前工作区全部变更(学员学号可编辑并留空自动生成等)

This commit is contained in:
buliping committed 2026-11-10 13:34:14 +08:00
1 parent 01e949093a
commit ee629f2f3e
534 files changed
+72978 -2720

No files matched your search

+5 -5
View File
@@ -6,8 +6,8 @@
BEGIN;
-- 1. 教师主档
UPDATE public.train_teacher SET org_name = '沈阳房建公寓段安全培训科', job_title = '首席技师/教授级高工', t_phone = '18800000000', t_email = 'zhaodm@sict.railway.cn', status = 1, del_flag = '0' WHERE t_id = 'T001';
INSERT INTO public.train_teacher (t_id, t_name, t_gender, t_birth, org_name, job_title, t_phone, t_email, status, del_flag, user_id, create_by, create_time, remark)
UPDATE public.train_teacher SET dept_name = '沈阳房建公寓段安全培训科', job_title = '首席技师/教授级高工', t_phone = '18800000000', t_email = 'zhaodm@sict.railway.cn', status = 1, del_flag = '0' WHERE t_id = 'T001';
INSERT INTO public.train_teacher (t_id, t_name, t_gender, t_birth, dept_name, job_title, t_phone, t_email, status, del_flag, user_id, create_by, create_time, remark)
VALUES
('T101', '崔宏伟', '0', '1982-05-14', '沈阳房建公寓段安全培训科', '高级工程师', '18800000011', 'cuihw@sict.railway.cn', 1, '0', 11, 'admin', CURRENT_TIMESTAMP, '演示主讲教师(绑定账号demo_teacher)'),
('T102', '张建国', '0', '1985-11-20', '沈阳房建公寓段技术科', '副高级工程师', '18800000012', 'zhangjg@sict.railway.cn', 1, '0', 12, 'admin', CURRENT_TIMESTAMP, '技术专家讲师(绑定账号demo_teacher2)'),
@@ -16,7 +16,7 @@ VALUES
('T105', '陈广林', '0', '1976-12-09', '大连项目部', '特级技师', '18800000015', 'chengl@sict.railway.cn', 1, '0', NULL, 'admin', CURRENT_TIMESTAMP, '防汛抢险实训专家'),
('T106', '马志强', '0', '1990-07-18', '丹东项目部', '工程师', '18800000016', 'mazhq@sict.railway.cn', 1, '0', NULL, 'admin', CURRENT_TIMESTAMP, '电气防火与智能化安全监控讲师')
ON CONFLICT (t_id) DO UPDATE SET
t_name = EXCLUDED.t_name, org_name = EXCLUDED.org_name, job_title = EXCLUDED.job_title, user_id = EXCLUDED.user_id, status = EXCLUDED.status, del_flag = '0';
t_name = EXCLUDED.t_name, dept_name = EXCLUDED.dept_name, job_title = EXCLUDED.job_title, user_id = EXCLUDED.user_id, status = EXCLUDED.status, del_flag = '0';
-- 2. 课程标准库
INSERT INTO public.train_course (c_id, c_name, c_code, c_type, c_duration, teaching_objective, course_content, status, del_flag, create_by, create_time, remark)
@@ -86,7 +86,7 @@ ON CONFLICT (dorm_id) DO UPDATE SET
dorm_building = EXCLUDED.dorm_building, dorm_floor = EXCLUDED.dorm_floor, dorm_room = EXCLUDED.dorm_room, capacity = EXCLUDED.capacity, dorm_status = EXCLUDED.dorm_status, del_flag = '0';
-- 7. 学员主档 (35名)
INSERT INTO public.train_student (s_id, s_name, s_gender, s_birth, org_name, s_phone, s_email, id_card_hash, id_card_last4, source_type, status, del_flag, user_id, create_by, create_time, remark)
INSERT INTO public.train_student (s_id, s_name, s_gender, s_birth, dept_name, s_phone, s_email, id_card_hash, id_card_last4, source_type, status, del_flag, user_id, create_by, create_time, remark)
VALUES
('S101', '李强', '0', '1990-01-01', '沈阳房建公寓段沈阳车间', '18800000101', 's101@sict.railway.cn', '57ecc1436745329b620ccbfaebbed22cda4ba9b33ec44de468d2e3acd61f63c2', '1000', 1, 1, '0', 21, 'admin', CURRENT_TIMESTAMP, '演示学员样本1'),
('S102', '王芳', '1', '1991-02-02', '沈阳房建公寓段大连车间', '18800000102', 's102@sict.railway.cn', 'f6d29eac73f7ea39158b909683cc36f50a2ea9298be8a453bf8922488e499b29', '1001', 1, 1, '0', 22, 'admin', CURRENT_TIMESTAMP, '演示学员样本2'),
@@ -124,7 +124,7 @@ VALUES
('S134', '吕梁', '1', '1991-10-06', '沈阳房建公寓段安全培训科', '18800000134', 's134@sict.railway.cn', '65a78d3839aef33f4059ea214e8faac94125c6c82cfa3001580fa208430b0ad3', '1033', 1, 1, '0', NULL, 'admin', CURRENT_TIMESTAMP, '演示学员样本34'),
('S135', '卢伟', '0', '1992-11-07', '沈阳房建公寓段技术科', '18800000135', 's135@sict.railway.cn', '2df5ae45334850de9e4ba87333f2388e2b71f5d75ead96fdd7c8734dd73f7147', '1034', 1, 1, '0', NULL, 'admin', CURRENT_TIMESTAMP, '演示学员样本35')
ON CONFLICT (s_id) DO UPDATE SET
s_name = EXCLUDED.s_name, org_name = EXCLUDED.org_name, s_phone = EXCLUDED.s_phone, s_email = EXCLUDED.s_email,
s_name = EXCLUDED.s_name, dept_name = EXCLUDED.dept_name, s_phone = EXCLUDED.s_phone, s_email = EXCLUDED.s_email,
id_card_hash = EXCLUDED.id_card_hash, id_card_last4 = EXCLUDED.id_card_last4, user_id = EXCLUDED.user_id, status = EXCLUDED.status, del_flag = '0';
COMMIT;
@@ -0,0 +1,390 @@
-- ==============================================================================
-- RailwaySystem 演示/测试数据: 005_demo_theory_exam_score.sql
-- 目标表: train_theory_exam_score(班级学员理论考试成绩表,先执行 026 建表脚本)
-- 数据库: MySQL 5.7+(脚本使用 INSERT ... SELECT / INSERT IGNORE / 派生表,5.7 完全兼容)
-- ==============================================================================
-- 一、依赖的既有数据(脚本不新建,全部从库中实时读取,保证外键与业务一致性)
-- train_student(s_id) —— 学员主档
-- train_class_plan(plan_id) —— 开班计划
-- train_plan_course(plan_course_id) —— 培训班所开课程实例(本脚本的核心关联依据)
-- train_enrollment(enrollment_id) —— 参训档案(学员 × 班级)
-- train_course(c_id) —— 课程主档(由 train_plan_course 带出)
-- train_exam_subject(subject_code) —— 科目字典(本脚本不写入,subject_code 置 NULL,避免无谓依赖)
-- train_score_item(score_item_id) —— 综合成绩项(本脚本不写入,score_item_id 置 NULL)
--
-- 二、生成逻辑
-- · 每一行 = 一个 (参训档案 enrollment_id, 班级课程 plan_course_id, 考试次数 retake_no) 组合,
-- 因此天然满足唯一键 uk_theory_enroll_course_retake;
-- · 同一学员同一班级若存在多条参训档案,取 MIN(enrollment_id) 去重,
-- 避免触发 uk_theory_final(每课程每学员仅一条最终成绩);
-- · 使用 ORDER BY + LIMIT/OFFSET 把"学员×课程"笛卡尔集切成互不重叠的区间,
-- 分别构造正常、满分、零分、缺考、免修、补考、锁定/归档等场景,区间之间不会撞唯一键;
-- · 使用 INSERT IGNORE,重复执行不会报错也不会产生重复数据;
-- · 若库中可用的"学员×课程"组合不足,靠后的场景会少插或插 0 行(不报错),
-- 执行前可先跑"第 0 步"的体检 SQL 查看可用组合数。
--
-- 三、字段写入约定
-- enrollment_id / plan_id / student_id ← train_enrollment
-- plan_course_id / c_id ← train_plan_course
-- exam_start_time / exam_end_time ← NOW() 回溯 1~30 天,开始时间比结束早 90 分钟
-- duration_seconds ← 5400(90 分钟)
-- retake / retake_no ← 正考 0/0;补考 1/1、1/2
-- exam_status ← NORMAL 正常 / ABSENT 缺考 / EXEMPT 免修
-- score ← 正常考试 0~满分;缺考/免修必须为 NULL(CHECK 约束要求)
-- full_score ← 100.00
-- pass_flag ← 60 分及格(与后端 60% 默认判定一致)
-- is_final ← 正考/补考最终成绩为 1,过程性补考为 0
-- score_status ← RECEIVED / ARCHIVED / LOCKED
-- source_type ← 1 手工录入 / 3 批量或外部导入(演示来源差异)
-- del_flag ← '0'
-- create_by ← 'test-data'(便于识别与清理)
-- ==============================================================================
SET NAMES utf8mb4;
-- ------------------------------------------------------------------------------
-- 第 0 步:体检(只读,不影响数据)
-- 查看当前可用的"学员 × 课程"组合数量,决定各场景实际能插入多少行
-- ------------------------------------------------------------------------------
SELECT COUNT(*) AS available_student_course_pairs
FROM (
SELECT e.student_id, pc.plan_course_id
FROM (SELECT student_id, plan_id, MIN(enrollment_id) AS enrollment_id
FROM train_enrollment
GROUP BY student_id, plan_id) e
JOIN train_plan_course pc ON pc.plan_id = e.plan_id
JOIN train_class_plan cp ON cp.plan_id = e.plan_id
) p;
-- ==============================================================================
-- 场景 1:正常分布(正考 0 次补考、成绩 60~100、通过、最终成绩)
-- 覆盖区间:偏移 0 ~ 11,共 12 行
-- ==============================================================================
INSERT IGNORE INTO train_theory_exam_score
(enrollment_id, plan_id, plan_course_id, c_id, student_id, subject_code,
exam_start_time, exam_end_time, duration_seconds, retake, retake_no,
exam_status, score, full_score, pass_flag, is_final, score_status,
source_type, source_system, source_event_id, score_item_id, remark,
del_flag, create_by, create_time)
SELECT t.enrollment_id, t.plan_id, t.plan_course_id, t.c_id, t.student_id, NULL,
DATE_SUB(t.end_time, INTERVAL 90 MINUTE), t.end_time, 5400, 0, 0,
'NORMAL',
CAST(60 + MOD(CRC32(CONCAT(t.enrollment_id, '-', t.plan_course_id)), 41) AS DECIMAL(7,2)),
100.00, 1, 1, 'RECEIVED',
1, NULL, NULL, NULL, '测试数据-正常通过(60~100分)',
'0', 'test-data', NOW()
FROM (
SELECT e.enrollment_id, e.plan_id, e.student_id, pc.plan_course_id, pc.c_id,
DATE_SUB(NOW(), INTERVAL (MOD(CRC32(CONCAT(e.enrollment_id, '-', pc.plan_course_id)), 30) + 1) DAY) AS end_time
FROM (SELECT student_id, plan_id, MIN(enrollment_id) AS enrollment_id
FROM train_enrollment
GROUP BY student_id, plan_id) e
JOIN train_plan_course pc ON pc.plan_id = e.plan_id
JOIN train_class_plan cp ON cp.plan_id = e.plan_id
ORDER BY e.enrollment_id, pc.plan_course_id
LIMIT 12 OFFSET 0
) t;
-- ==============================================================================
-- 场景 2:边界值-满分(score = full_score = 100.00)
-- 覆盖区间:偏移 12 ~ 15,共 4 行
-- ==============================================================================
INSERT IGNORE INTO train_theory_exam_score
(enrollment_id, plan_id, plan_course_id, c_id, student_id, subject_code,
exam_start_time, exam_end_time, duration_seconds, retake, retake_no,
exam_status, score, full_score, pass_flag, is_final, score_status,
source_type, source_system, source_event_id, score_item_id, remark,
del_flag, create_by, create_time)
SELECT t.enrollment_id, t.plan_id, t.plan_course_id, t.c_id, t.student_id, NULL,
DATE_SUB(t.end_time, INTERVAL 90 MINUTE), t.end_time, 5400, 0, 0,
'NORMAL', CAST(100.00 AS DECIMAL(7,2)), 100.00, 1, 1, 'RECEIVED',
1, NULL, NULL, NULL, '测试数据-边界值:满分100',
'0', 'test-data', NOW()
FROM (
SELECT e.enrollment_id, e.plan_id, e.student_id, pc.plan_course_id, pc.c_id,
DATE_SUB(NOW(), INTERVAL (MOD(CRC32(CONCAT(e.enrollment_id, '-', pc.plan_course_id)), 30) + 1) DAY) AS end_time
FROM (SELECT student_id, plan_id, MIN(enrollment_id) AS enrollment_id
FROM train_enrollment
GROUP BY student_id, plan_id) e
JOIN train_plan_course pc ON pc.plan_id = e.plan_id
JOIN train_class_plan cp ON cp.plan_id = e.plan_id
ORDER BY e.enrollment_id, pc.plan_course_id
LIMIT 4 OFFSET 12
) t;
-- ==============================================================================
-- 场景 3:边界值-零分(score = 0.00,未通过)
-- 覆盖区间:偏移 16 ~ 19,共 4 行
-- ==============================================================================
INSERT IGNORE INTO train_theory_exam_score
(enrollment_id, plan_id, plan_course_id, c_id, student_id, subject_code,
exam_start_time, exam_end_time, duration_seconds, retake, retake_no,
exam_status, score, full_score, pass_flag, is_final, score_status,
source_type, source_system, source_event_id, score_item_id, remark,
del_flag, create_by, create_time)
SELECT t.enrollment_id, t.plan_id, t.plan_course_id, t.c_id, t.student_id, NULL,
DATE_SUB(t.end_time, INTERVAL 90 MINUTE), t.end_time, 5400, 0, 0,
'NORMAL', CAST(0.00 AS DECIMAL(7,2)), 100.00, 0, 1, 'RECEIVED',
1, NULL, NULL, NULL, '测试数据-边界值:零分',
'0', 'test-data', NOW()
FROM (
SELECT e.enrollment_id, e.plan_id, e.student_id, pc.plan_course_id, pc.c_id,
DATE_SUB(NOW(), INTERVAL (MOD(CRC32(CONCAT(e.enrollment_id, '-', pc.plan_course_id)), 30) + 1) DAY) AS end_time
FROM (SELECT student_id, plan_id, MIN(enrollment_id) AS enrollment_id
FROM train_enrollment
GROUP BY student_id, plan_id) e
JOIN train_plan_course pc ON pc.plan_id = e.plan_id
JOIN train_class_plan cp ON cp.plan_id = e.plan_id
ORDER BY e.enrollment_id, pc.plan_course_id
LIMIT 4 OFFSET 16
) t;
-- ==============================================================================
-- 场景 4:缺考(ABSENT,score 必须为 NULL,pass_flag=0,仍作为该课程最终成绩)
-- 覆盖区间:偏移 20 ~ 23,共 4 行
-- ==============================================================================
INSERT IGNORE INTO train_theory_exam_score
(enrollment_id, plan_id, plan_course_id, c_id, student_id, subject_code,
exam_start_time, exam_end_time, duration_seconds, retake, retake_no,
exam_status, score, full_score, pass_flag, is_final, score_status,
source_type, source_system, source_event_id, score_item_id, remark,
del_flag, create_by, create_time)
SELECT t.enrollment_id, t.plan_id, t.plan_course_id, t.c_id, t.student_id, NULL,
DATE_SUB(t.end_time, INTERVAL 90 MINUTE), t.end_time, NULL, 0, 0,
'ABSENT', NULL, 100.00, 0, 1, 'RECEIVED',
1, NULL, NULL, NULL, '测试数据-缺考(无成绩)',
'0', 'test-data', NOW()
FROM (
SELECT e.enrollment_id, e.plan_id, e.student_id, pc.plan_course_id, pc.c_id,
DATE_SUB(NOW(), INTERVAL (MOD(CRC32(CONCAT(e.enrollment_id, '-', pc.plan_course_id)), 30) + 1) DAY) AS end_time
FROM (SELECT student_id, plan_id, MIN(enrollment_id) AS enrollment_id
FROM train_enrollment
GROUP BY student_id, plan_id) e
JOIN train_plan_course pc ON pc.plan_id = e.plan_id
JOIN train_class_plan cp ON cp.plan_id = e.plan_id
ORDER BY e.enrollment_id, pc.plan_course_id
LIMIT 4 OFFSET 20
) t;
-- ==============================================================================
-- 场景 5:免修(EXEMPT,score 为 NULL,按通过计)
-- 覆盖区间:偏移 24 ~ 26,共 3 行
-- ==============================================================================
INSERT IGNORE INTO train_theory_exam_score
(enrollment_id, plan_id, plan_course_id, c_id, student_id, subject_code,
exam_start_time, exam_end_time, duration_seconds, retake, retake_no,
exam_status, score, full_score, pass_flag, is_final, score_status,
source_type, source_system, source_event_id, score_item_id, remark,
del_flag, create_by, create_time)
SELECT t.enrollment_id, t.plan_id, t.plan_course_id, t.c_id, t.student_id, NULL,
NULL, NULL, NULL, 0, 0,
'EXEMPT', NULL, 100.00, 1, 1, 'ARCHIVED',
1, NULL, NULL, NULL, '测试数据-免修(不计分,计通过)',
'0', 'test-data', NOW()
FROM (
SELECT e.enrollment_id, e.plan_id, e.student_id, pc.plan_course_id, pc.c_id,
DATE_SUB(NOW(), INTERVAL 7 DAY) AS end_time
FROM (SELECT student_id, plan_id, MIN(enrollment_id) AS enrollment_id
FROM train_enrollment
GROUP BY student_id, plan_id) e
JOIN train_plan_course pc ON pc.plan_id = e.plan_id
JOIN train_class_plan cp ON cp.plan_id = e.plan_id
ORDER BY e.enrollment_id, pc.plan_course_id
LIMIT 3 OFFSET 24
) t;
-- ==============================================================================
-- 场景 6:补考过程记录(第 1 次补考,45 分未通过,is_final=0)
-- 覆盖区间:偏移 27 ~ 32,共 6 行;与场景 7 使用同一区间,形成"正考/补考"两次成绩
-- ==============================================================================
INSERT IGNORE INTO train_theory_exam_score
(enrollment_id, plan_id, plan_course_id, c_id, student_id, subject_code,
exam_start_time, exam_end_time, duration_seconds, retake, retake_no,
exam_status, score, full_score, pass_flag, is_final, score_status,
source_type, source_system, source_event_id, score_item_id, remark,
del_flag, create_by, create_time)
SELECT t.enrollment_id, t.plan_id, t.plan_course_id, t.c_id, t.student_id, NULL,
DATE_SUB(t.end_time, INTERVAL 90 MINUTE), t.end_time, 5400, 1, 1,
'NORMAL', CAST(45.00 AS DECIMAL(7,2)), 100.00, 0, 0, 'RECEIVED',
1, NULL, NULL, NULL, '测试数据-补考第1次未通过(非最终成绩)',
'0', 'test-data', NOW()
FROM (
SELECT e.enrollment_id, e.plan_id, e.student_id, pc.plan_course_id, pc.c_id,
DATE_SUB(NOW(), INTERVAL 15 DAY) AS end_time
FROM (SELECT student_id, plan_id, MIN(enrollment_id) AS enrollment_id
FROM train_enrollment
GROUP BY student_id, plan_id) e
JOIN train_plan_course pc ON pc.plan_id = e.plan_id
JOIN train_class_plan cp ON cp.plan_id = e.plan_id
ORDER BY e.enrollment_id, pc.plan_course_id
LIMIT 6 OFFSET 27
) t;
-- ==============================================================================
-- 场景 7:补考后最终成绩(第 2 次补考,65~95 分通过,is_final=1)
-- 覆盖区间:与场景 6 相同(偏移 27 ~ 32),retake_no=2,因此不违反唯一键
-- ==============================================================================
INSERT IGNORE INTO train_theory_exam_score
(enrollment_id, plan_id, plan_course_id, c_id, student_id, subject_code,
exam_start_time, exam_end_time, duration_seconds, retake, retake_no,
exam_status, score, full_score, pass_flag, is_final, score_status,
source_type, source_system, source_event_id, score_item_id, remark,
del_flag, create_by, create_time)
SELECT t.enrollment_id, t.plan_id, t.plan_course_id, t.c_id, t.student_id, NULL,
DATE_SUB(t.end_time, INTERVAL 90 MINUTE), t.end_time, 4800, 1, 2,
'NORMAL',
CAST(65 + MOD(CRC32(CONCAT('retake-', t.enrollment_id, '-', t.plan_course_id)), 31) AS DECIMAL(7,2)),
100.00, 1, 1, 'RECEIVED',
1, NULL, NULL, NULL, '测试数据-补考第2次通过(最终成绩)',
'0', 'test-data', NOW()
FROM (
SELECT e.enrollment_id, e.plan_id, e.student_id, pc.plan_course_id, pc.c_id,
DATE_SUB(NOW(), INTERVAL 7 DAY) AS end_time
FROM (SELECT student_id, plan_id, MIN(enrollment_id) AS enrollment_id
FROM train_enrollment
GROUP BY student_id, plan_id) e
JOIN train_plan_course pc ON pc.plan_id = e.plan_id
JOIN train_class_plan cp ON cp.plan_id = e.plan_id
ORDER BY e.enrollment_id, pc.plan_course_id
LIMIT 6 OFFSET 27
) t;
-- ==============================================================================
-- 场景 8:已锁定成绩(LOCKED,禁止再改分数;来源为手工录入)
-- 覆盖区间:偏移 33 ~ 34,共 2 行
-- ==============================================================================
INSERT IGNORE INTO train_theory_exam_score
(enrollment_id, plan_id, plan_course_id, c_id, student_id, subject_code,
exam_start_time, exam_end_time, duration_seconds, retake, retake_no,
exam_status, score, full_score, pass_flag, is_final, score_status,
source_type, source_system, source_event_id, score_item_id, remark,
del_flag, create_by, create_time)
SELECT t.enrollment_id, t.plan_id, t.plan_course_id, t.c_id, t.student_id, NULL,
DATE_SUB(t.end_time, INTERVAL 90 MINUTE), t.end_time, 5400, 0, 0,
'NORMAL', CAST(88.00 AS DECIMAL(7,2)), 100.00, 1, 1, 'LOCKED',
1, NULL, NULL, NULL, '测试数据-已锁定成绩',
'0', 'test-data', NOW()
FROM (
SELECT e.enrollment_id, e.plan_id, e.student_id, pc.plan_course_id, pc.c_id,
DATE_SUB(NOW(), INTERVAL 10 DAY) AS end_time
FROM (SELECT student_id, plan_id, MIN(enrollment_id) AS enrollment_id
FROM train_enrollment
GROUP BY student_id, plan_id) e
JOIN train_plan_course pc ON pc.plan_id = e.plan_id
JOIN train_class_plan cp ON cp.plan_id = e.plan_id
ORDER BY e.enrollment_id, pc.plan_course_id
LIMIT 2 OFFSET 33
) t;
-- ==============================================================================
-- 场景 9:外部批量导入 + 已归档(source_type=3,带导入幂等键)
-- 覆盖区间:偏移 35 ~ 36,共 2 行
-- ==============================================================================
INSERT IGNORE INTO train_theory_exam_score
(enrollment_id, plan_id, plan_course_id, c_id, student_id, subject_code,
exam_start_time, exam_end_time, duration_seconds, retake, retake_no,
exam_status, score, full_score, pass_flag, is_final, score_status,
source_type, source_system, source_event_id, score_item_id, remark,
del_flag, create_by, create_time)
SELECT t.enrollment_id, t.plan_id, t.plan_course_id, t.c_id, t.student_id, NULL,
DATE_SUB(t.end_time, INTERVAL 90 MINUTE), t.end_time, 5400, 0, 0,
'NORMAL', CAST(72.50 AS DECIMAL(7,2)), 100.00, 1, 1, 'ARCHIVED',
3, 'TMS-EXAM', CONCAT('IMP-', t.enrollment_id, '-', t.plan_course_id), NULL,
'测试数据-外部导入并归档',
'0', 'test-data', NOW()
FROM (
SELECT e.enrollment_id, e.plan_id, e.student_id, pc.plan_course_id, pc.c_id,
DATE_SUB(NOW(), INTERVAL 3 DAY) AS end_time
FROM (SELECT student_id, plan_id, MIN(enrollment_id) AS enrollment_id
FROM train_enrollment
GROUP BY student_id, plan_id) e
JOIN train_plan_course pc ON pc.plan_id = e.plan_id
JOIN train_class_plan cp ON cp.plan_id = e.plan_id
ORDER BY e.enrollment_id, pc.plan_course_id
LIMIT 2 OFFSET 35
) t;
-- ==============================================================================
-- 第 10 步:结果校验(只读)
-- 10.1 场景分布统计
-- 10.2 关联一致性检查(均应为 0 行)
-- 10.3 唯一性检查(均应为 0 行)
-- 10.4 分数与状态合法性检查(均应为 0 行)
-- ==============================================================================
-- 10.1 场景分布
SELECT exam_status, score_status, retake, is_final, COUNT(*) AS row_count
FROM train_theory_exam_score
WHERE create_by = 'test-data'
GROUP BY exam_status, score_status, retake, is_final
ORDER BY exam_status, score_status, retake, is_final;
-- 10.2 关联一致性:学员 / 班级计划 / 课程实例 / 课程主档 是否都能对上
SELECT t.theory_score_id, '学员不存在' AS problem
FROM train_theory_exam_score t
LEFT JOIN train_student s ON s.s_id = t.student_id
WHERE t.create_by = 'test-data' AND s.s_id IS NULL
UNION ALL
SELECT t.theory_score_id, '班级计划不存在'
FROM train_theory_exam_score t
LEFT JOIN train_class_plan cp ON cp.plan_id = t.plan_id
WHERE t.create_by = 'test-data' AND cp.plan_id IS NULL
UNION ALL
SELECT t.theory_score_id, '课程实例不存在或与计划/课程不匹配'
FROM train_theory_exam_score t
LEFT JOIN train_plan_course pc
ON pc.plan_course_id = t.plan_course_id
AND pc.plan_id = t.plan_id
AND pc.c_id = t.c_id
WHERE t.create_by = 'test-data' AND pc.plan_course_id IS NULL
UNION ALL
SELECT t.theory_score_id, '参训档案与学员/班级不匹配'
FROM train_theory_exam_score t
LEFT JOIN train_enrollment e
ON e.enrollment_id = t.enrollment_id
AND e.student_id = t.student_id
AND e.plan_id = t.plan_id
WHERE t.create_by = 'test-data' AND e.enrollment_id IS NULL;
-- 10.3 唯一性:同一参训档案 + 课程 + 考试次数重复
SELECT enrollment_id, plan_course_id, retake_no, COUNT(*) AS cnt
FROM train_theory_exam_score
WHERE del_flag = '0'
GROUP BY enrollment_id, plan_course_id, retake_no
HAVING COUNT(*) > 1;
-- 10.3.1 唯一性:同一课程同一学员出现多条最终成绩
SELECT plan_course_id, student_id, COUNT(*) AS final_cnt
FROM train_theory_exam_score
WHERE del_flag = '0' AND is_final = 1
GROUP BY plan_course_id, student_id
HAVING COUNT(*) > 1;
-- 10.4 合法性:缺考/免修携带分数、正常考试无分数、分数越界
SELECT theory_score_id, exam_status, score, full_score, '分数状态不匹配' AS problem
FROM train_theory_exam_score
WHERE create_by = 'test-data'
AND (
(exam_status = 'NORMAL' AND score IS NULL)
OR (exam_status IN ('ABSENT', 'EXEMPT') AND score IS NOT NULL)
OR (score IS NOT NULL AND (score < 0 OR score > full_score))
);
-- ==============================================================================
-- 清理(如需删除本脚本产生的全部数据,取消注释后执行)
-- 注意:逻辑删除会释放 uk_theory_final 的占用(生成列 final_flag 随 del_flag 变为 NULL)
-- ==============================================================================
-- DELETE FROM train_theory_exam_score WHERE create_by = 'test-data';
-- ==============================================================================
-- 附:无法通过 DB 约束落库的"异常场景"(仅作验证说明,脚本故意不插入)
-- 1) 重复录入:(enrollment_id, plan_course_id, retake_no) 重复
-- → 被 uk_theory_enroll_course_retake 拦截(INSERT IGNORE 也会被忽略)
-- 2) 同一课程同一学员两条 is_final=1
-- → 被 uk_theory_final(生成列 final_flag)拦截
-- 3) 缺考/免修携带分数,或正常考试分数为空
-- → 被 chk_theory_score_filled / chk_theory_score_range 拦截(MySQL 8.0 强制,5.7 由应用层校验)
-- 4) 考试结束时间早于开始时间
-- → 被 chk_theory_exam_time 拦截
-- 5) 引用不存在的学员/课程
-- → 被外键 fk_theory_student / fk_theory_plan_course 拦截
-- ==============================================================================
@@ -0,0 +1,359 @@
-- ==============================================================================
-- RailwaySystem 演示数据: 006_demo_theory_score_test01.sql(关系型重建版)
-- 用途: 为“测试01班”构建一套完整的班级—学员—选课—理论成绩关联模型,并填充演示数据
-- 数据库: MySQL 5.7+(无 CTE / 窗口函数 / CHECK 约束;全部为普通表 + 外键,5.7 完全兼容)
-- 字符集: utf8mb4 / utf8mb4_general_ci(支持中文班级名与姓名)
--
-- 数据模型(7 张表,演示专用,前缀 demo_,不触碰正式业务表 train_*):
--
-- demo_class_plan 开班计划(1) ──┬── demo_plan_course 计划开设课程(N)
-- │ │
-- demo_course 课程字典(1) ──┘ │
-- │ │
-- demo_student 学生主档(1) ── demo_class_member 班级成员(N) [计划 × 学生]
-- │
-- ├── demo_student_course 学生选课 [计划 × 学生 × 课程]
-- │ │
-- └── demo_theory_score 理论成绩
-- [计划 × 学生 × 课程 × 考试类型]
--
-- 关联一致性由数据库外键强制保证:
-- 1) 班级成员必须属于已存在的开班计划与已存在的学生;
-- 2) 学生选课必须是“该班成员 + 该班已开设课程”的交集(双外键);
-- 3) 理论成绩必须落在学生已选的课程上(外键指向 demo_student_course),
-- 因此不可能出现“成绩挂在未选课/非本班/未开课程上”的脏数据。
--
-- 数据规模:1 个开班计划、3 门课程、24 名学生、24 条班级成员、
-- 39 条选课记录、39 条正考成绩 + 2 条补考 = 41 条成绩
-- 边界/异常:满分 100、零分 0、及格线 60、59.5 差 0.5 分、缺考(成绩 NULL)、补考提升
--
-- 可重复执行:脚本开头统一 DROP(仅 demo_ 前缀表),如需保留请注释 DROP 段
-- ==============================================================================
SET NAMES utf8mb4;
-- ------------------------------------------------------------------------------
-- 0. 清理旧演示表(按外键依赖倒序)
-- ------------------------------------------------------------------------------
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `demo_theory_score`;
DROP TABLE IF EXISTS `demo_student_course`;
DROP TABLE IF EXISTS `demo_class_member`;
DROP TABLE IF EXISTS `demo_student`;
DROP TABLE IF EXISTS `demo_plan_course`;
DROP TABLE IF EXISTS `demo_course`;
DROP TABLE IF EXISTS `demo_class_plan`;
SET FOREIGN_KEY_CHECKS = 1;
-- ------------------------------------------------------------------------------
-- 1. 开班计划
-- ------------------------------------------------------------------------------
CREATE TABLE `demo_class_plan` (
`plan_id` VARCHAR(32) NOT NULL COMMENT '开班计划ID',
`plan_name` VARCHAR(150) NOT NULL COMMENT '开班计划名称',
`class_name` VARCHAR(50) NOT NULL COMMENT '班级名称',
`start_time` DATETIME NOT NULL COMMENT '开班时间',
`end_time` DATETIME NOT NULL COMMENT '结班时间',
`plan_status` TINYINT NOT NULL DEFAULT 1 COMMENT '计划状态(1进行中 0已结束)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`plan_id`),
UNIQUE KEY `uk_plan_class_name` (`class_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='开班计划演示表';
INSERT INTO `demo_class_plan` (`plan_id`, `plan_name`, `class_name`, `start_time`, `end_time`, `plan_status`) VALUES
('PLAN-TEST01', '2026年接触网安全理论培训班(测试01班)', '测试01班', '2026-10-08 08:30:00', '2026-12-31 18:00:00', 1);
-- ------------------------------------------------------------------------------
-- 2. 课程字典
-- ------------------------------------------------------------------------------
CREATE TABLE `demo_course` (
`c_id` VARCHAR(32) NOT NULL COMMENT '课程ID',
`c_name` VARCHAR(100) NOT NULL COMMENT '课程名称',
`c_type` VARCHAR(30) DEFAULT NULL COMMENT '课程类型',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`c_id`),
UNIQUE KEY `uk_course_name` (`c_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='课程字典演示表';
INSERT INTO `demo_course` (`c_id`, `c_name`, `c_type`) VALUES
('C01', '接触网安全理论', '理论'),
('C02', '铁路电力安全规程', '理论'),
('C03', '应急处置与救援', '理论');
-- ------------------------------------------------------------------------------
-- 3. 开班计划课程(计划开设了哪些课)
-- ------------------------------------------------------------------------------
CREATE TABLE `demo_plan_course` (
`plan_id` VARCHAR(32) NOT NULL COMMENT '开班计划ID',
`c_id` VARCHAR(32) NOT NULL COMMENT '课程ID',
`sort_no` INT NOT NULL DEFAULT 0 COMMENT '排课顺序',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`plan_id`, `c_id`),
KEY `idx_plan_course_course` (`c_id`),
CONSTRAINT `fk_plan_course_plan` FOREIGN KEY (`plan_id`) REFERENCES `demo_class_plan` (`plan_id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_plan_course_course` FOREIGN KEY (`c_id`) REFERENCES `demo_course` (`c_id`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='开班计划课程演示表';
INSERT INTO `demo_plan_course` (`plan_id`, `c_id`, `sort_no`) VALUES
('PLAN-TEST01', 'C01', 1),
('PLAN-TEST01', 'C02', 2),
('PLAN-TEST01', 'C03', 3);
-- ------------------------------------------------------------------------------
-- 4. 学生主档(学号 + 姓名)
-- ------------------------------------------------------------------------------
CREATE TABLE `demo_student` (
`student_no` VARCHAR(32) NOT NULL COMMENT '学号',
`student_name` VARCHAR(50) NOT NULL COMMENT '姓名',
`gender` VARCHAR(4) DEFAULT NULL COMMENT '性别',
`dept_name` VARCHAR(100) DEFAULT NULL COMMENT '所属单位',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`student_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='学生主档演示表';
INSERT INTO `demo_student` (`student_no`, `student_name`, `gender`, `dept_name`) VALUES
('TEST01-001', '张伟', '男', '供电段'),
('TEST01-002', '王芳', '女', '供电段'),
('TEST01-003', '李强', '男', '供电段'),
('TEST01-004', '刘洋', '男', '电务段'),
('TEST01-005', '陈静', '女', '电务段'),
('TEST01-006', '杨帆', '男', '电务段'),
('TEST01-007', '赵敏', '女', '工务段'),
('TEST01-008', '黄磊', '男', '工务段'),
('TEST01-009', '周涛', '男', '工务段'),
('TEST01-010', '吴倩', '女', '供电段'),
('TEST01-011', '徐鹏', '男', '供电段'),
('TEST01-012', '孙丽', '女', '供电段'),
('TEST01-013', '马超', '男', '电务段'),
('TEST01-014', '朱婷', '女', '电务段'),
('TEST01-015', '胡军', '男', '电务段'),
('TEST01-016', '郭鹏', '男', '工务段'),
('TEST01-017', '何梅', '女', '工务段'),
('TEST01-018', '高翔', '男', '工务段'),
('TEST01-019', '林芳', '女', '供电段'),
('TEST01-020', '罗文', '男', '供电段'),
('TEST01-021', '梁静', '女', '供电段'),
('TEST01-022', '宋波', '男', '电务段'),
('TEST01-023', '谢娜', '女', '电务段'),
('TEST01-024', '韩雪', '女', '工务段');
-- ------------------------------------------------------------------------------
-- 5. 班级成员(计划 × 学生,明确“谁在这个班”)
-- ------------------------------------------------------------------------------
CREATE TABLE `demo_class_member` (
`plan_id` VARCHAR(32) NOT NULL COMMENT '开班计划ID',
`student_no` VARCHAR(32) NOT NULL COMMENT '学号',
`join_time` DATETIME NOT NULL COMMENT '入班时间',
`member_status` TINYINT NOT NULL DEFAULT 1 COMMENT '成员状态(1在班 0已退班)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`plan_id`, `student_no`),
KEY `idx_member_student` (`student_no`),
CONSTRAINT `fk_member_plan` FOREIGN KEY (`plan_id`) REFERENCES `demo_class_plan` (`plan_id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_member_student` FOREIGN KEY (`student_no`) REFERENCES `demo_student` (`student_no`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='班级成员演示表';
INSERT INTO `demo_class_member` (`plan_id`, `student_no`, `join_time`, `member_status`)
SELECT 'PLAN-TEST01', `student_no`, '2026-10-08 09:00:00', 1
FROM `demo_student`;
-- ------------------------------------------------------------------------------
-- 6. 学生选课(计划 × 学生 × 课程;必须同时是班级成员且课程为计划已开设课程)
-- 选课规则:全部 24 人必选 C01;002~013 选 C02;003/009/015 选 C03(共 39 条)
-- ------------------------------------------------------------------------------
CREATE TABLE `demo_student_course` (
`plan_id` VARCHAR(32) NOT NULL COMMENT '开班计划ID',
`student_no` VARCHAR(32) NOT NULL COMMENT '学号',
`c_id` VARCHAR(32) NOT NULL COMMENT '课程ID',
`select_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间',
PRIMARY KEY (`plan_id`, `student_no`, `c_id`),
KEY `idx_sc_course` (`plan_id`, `c_id`),
CONSTRAINT `fk_sc_member` FOREIGN KEY (`plan_id`, `student_no`) REFERENCES `demo_class_member` (`plan_id`, `student_no`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_sc_plan_course` FOREIGN KEY (`plan_id`, `c_id`) REFERENCES `demo_plan_course` (`plan_id`, `c_id`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='学生选课演示表';
-- 6.1 全员必选 C01
INSERT INTO `demo_student_course` (`plan_id`, `student_no`, `c_id`)
SELECT 'PLAN-TEST01', `student_no`, 'C01'
FROM `demo_student`;
-- 6.2 002~013 加选 C02
INSERT INTO `demo_student_course` (`plan_id`, `student_no`, `c_id`)
SELECT 'PLAN-TEST01', `student_no`, 'C02'
FROM `demo_student`
WHERE `student_no` BETWEEN 'TEST01-002' AND 'TEST01-013';
-- 6.3 003/009/015 加选 C03
INSERT INTO `demo_student_course` (`plan_id`, `student_no`, `c_id`) VALUES
('PLAN-TEST01', 'TEST01-003', 'C03'),
('PLAN-TEST01', 'TEST01-009', 'C03'),
('PLAN-TEST01', 'TEST01-015', 'C03');
-- ------------------------------------------------------------------------------
-- 7. 理论成绩(计划 × 学生 × 课程 × 考试类型)
-- 外键指向选课表,确保“成绩只能落在已选课程上”;唯一键防重复录入
-- ------------------------------------------------------------------------------
CREATE TABLE `demo_theory_score` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`plan_id` VARCHAR(32) NOT NULL COMMENT '开班计划ID',
`student_no` VARCHAR(32) NOT NULL COMMENT '学号',
`c_id` VARCHAR(32) NOT NULL COMMENT '课程ID',
`exam_type` VARCHAR(16) NOT NULL DEFAULT '正考' COMMENT '考试类型(正考/补考)',
`exam_status` VARCHAR(16) NOT NULL DEFAULT '正常' COMMENT '考试状态(正常/缺考/免修)',
`score` DECIMAL(5,2) DEFAULT NULL COMMENT '理论成绩(0-100,缺考为NULL)',
`full_score` DECIMAL(5,2) NOT NULL DEFAULT 100.00 COMMENT '满分',
`pass_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '是否通过(1通过 0未通过)',
`exam_time` DATETIME DEFAULT NULL COMMENT '考试时间',
`remark` VARCHAR(200) DEFAULT NULL COMMENT '备注(边界/异常样本)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_score_member_course_exam` (`plan_id`, `student_no`, `c_id`, `exam_type`),
KEY `idx_score_student` (`student_no`),
KEY `idx_score_course` (`plan_id`, `c_id`),
CONSTRAINT `fk_score_student_course` FOREIGN KEY (`plan_id`, `student_no`, `c_id`)
REFERENCES `demo_student_course` (`plan_id`, `student_no`, `c_id`)
ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='学生理论成绩演示表(测试01班)';
-- 7.1 正考成绩:按“学生 × 已选课程”自动生成,保证与选课/班级/计划完全一致
-- 分数规则:001=100(满分)、013=0(零分)、019=59.5(差0.5分)、021=60(及格线)、
-- 024 的 C01 为缺考(NULL),其余 45~100 之间按学号+课程确定性分布
INSERT INTO `demo_theory_score`
(`plan_id`, `student_no`, `c_id`, `exam_type`, `exam_status`, `score`, `full_score`, `pass_flag`, `exam_time`, `remark`)
SELECT t.plan_id,
t.student_no,
t.c_id,
'正考',
t.exam_status,
t.score,
100.00,
CASE WHEN t.score IS NOT NULL AND t.score >= 60 THEN 1 ELSE 0 END,
CASE t.c_id
WHEN 'C01' THEN '2026-10-20 09:00:00'
WHEN 'C02' THEN '2026-10-22 09:00:00'
ELSE '2026-10-24 14:00:00'
END,
t.remark
FROM (
SELECT sc.plan_id,
sc.student_no,
sc.c_id,
CASE
WHEN sc.student_no = 'TEST01-024' AND sc.c_id = 'C01' THEN NULL
WHEN sc.student_no = 'TEST01-001' THEN 100.00
WHEN sc.student_no = 'TEST01-013' THEN 0.00
WHEN sc.student_no = 'TEST01-019' THEN 59.50
WHEN sc.student_no = 'TEST01-021' THEN 60.00
ELSE CAST(45 + MOD(CRC32(CONCAT(sc.student_no, '-', sc.c_id)), 56) AS DECIMAL(5,2))
END AS score,
CASE
WHEN sc.student_no = 'TEST01-024' AND sc.c_id = 'C01' THEN '缺考'
ELSE '正常'
END AS exam_status,
CASE
WHEN sc.student_no = 'TEST01-001' THEN '边界值:满分'
WHEN sc.student_no = 'TEST01-013' THEN '边界值:零分'
WHEN sc.student_no = 'TEST01-019' THEN '边界值:差0.5分未通过'
WHEN sc.student_no = 'TEST01-021' THEN '边界值:及格线'
WHEN sc.student_no = 'TEST01-024' AND sc.c_id = 'C01' THEN '异常样本:缺考无成绩'
ELSE NULL
END AS remark
FROM `demo_student_course` sc
) t;
-- 7.2 补考成绩:019、022 的 C01 正考未通过后补考通过(与正考类型不同,不违反唯一键)
INSERT INTO `demo_theory_score`
(`plan_id`, `student_no`, `c_id`, `exam_type`, `exam_status`, `score`, `full_score`, `pass_flag`, `exam_time`, `remark`)
VALUES
('PLAN-TEST01', 'TEST01-019', 'C01', '补考', '正常', 72.00, 100.00, 1, '2026-11-03 14:00:00', '异常样本:补考成绩提升'),
('PLAN-TEST01', 'TEST01-022', 'C01', '补考', '正常', 78.00, 100.00, 1, '2026-11-03 14:00:00', '异常样本:正考未通过后补考通过');
-- ==============================================================================
-- 8. 结果校验(只读)
-- ==============================================================================
-- 8.1 数据规模总览
SELECT '开班计划' AS table_name, COUNT(*) AS row_count FROM `demo_class_plan`
UNION ALL SELECT '课程', COUNT(*) FROM `demo_course`
UNION ALL SELECT '计划课程', COUNT(*) FROM `demo_plan_course`
UNION ALL SELECT '学生', COUNT(*) FROM `demo_student`
UNION ALL SELECT '班级成员', COUNT(*) FROM `demo_class_member`
UNION ALL SELECT '学生选课', COUNT(*) FROM `demo_student_course`
UNION ALL SELECT '理论成绩', COUNT(*) FROM `demo_theory_score`;
-- 8.2 班级成员与选课明细(含学生姓名、课程名、班级名)
SELECT p.class_name, m.student_no, s.student_name, c.c_name, sc.select_time
FROM `demo_class_member` m
JOIN `demo_class_plan` p ON p.plan_id = m.plan_id
JOIN `demo_student` s ON s.student_no = m.student_no
JOIN `demo_student_course` sc ON sc.plan_id = m.plan_id AND sc.student_no = m.student_no
JOIN `demo_course` c ON c.c_id = sc.c_id
ORDER BY m.student_no, c.c_id;
-- 8.3 学生成绩明细(正考/补考、含班级与课程)
SELECT p.class_name, s.student_no, s.student_name, c.c_name,
t.exam_type, t.exam_status, t.score, t.full_score, t.pass_flag, t.exam_time, t.remark
FROM `demo_theory_score` t
JOIN `demo_student` s ON s.student_no = t.student_no
JOIN `demo_course` c ON c.c_id = t.c_id
JOIN `demo_class_plan` p ON p.plan_id = t.plan_id
ORDER BY t.student_no, t.c_id, t.exam_type;
-- 8.4 课程维度统计:参考人数、平均分、最高分、最低分、及格率
SELECT c.c_name,
COUNT(t.score) AS scored_count,
ROUND(AVG(t.score), 2) AS avg_score,
MAX(t.score) AS max_score,
MIN(t.score) AS min_score,
ROUND(SUM(t.pass_flag) / COUNT(*) * 100, 2) AS pass_rate_percent
FROM `demo_theory_score` t
JOIN `demo_course` c ON c.c_id = t.c_id
WHERE t.exam_type = '正考'
GROUP BY c.c_name;
-- 8.5 关联一致性自检(三项均应返回 0 行)
-- a) 成绩存在、但对应选课关系不存在(外键已保护,此处为双重验证)
SELECT t.id, '成绩缺少选课关系' AS problem
FROM `demo_theory_score` t
LEFT JOIN `demo_student_course` sc
ON sc.plan_id = t.plan_id AND sc.student_no = t.student_no AND sc.c_id = t.c_id
WHERE sc.student_no IS NULL
-- b) 班级成员不在学生主档 / 不在开班计划
UNION ALL
SELECT m.student_no, '班级成员缺少学生或计划'
FROM `demo_class_member` m
LEFT JOIN `demo_student` s ON s.student_no = m.student_no
LEFT JOIN `demo_class_plan` p ON p.plan_id = m.plan_id
WHERE s.student_no IS NULL OR p.plan_id IS NULL
-- c) 选课不是本班已开设课程
UNION ALL
SELECT sc.student_no, '选课不在计划课程内'
FROM `demo_student_course` sc
LEFT JOIN `demo_plan_course` pc ON pc.plan_id = sc.plan_id AND pc.c_id = sc.c_id
WHERE pc.c_id IS NULL;
-- 8.6 成绩合法性自检(应返回 0 行)
SELECT id, student_no, c_id, exam_type, exam_status, score, full_score, pass_flag
FROM `demo_theory_score`
WHERE (score IS NOT NULL AND (score < 0 OR score > 100 OR score > full_score))
OR (exam_status = '正常' AND score IS NULL)
OR (score IS NOT NULL AND pass_flag <> IF(score >= 60, 1, 0));
-- ------------------------------------------------------------------------------
-- 9. 清理(如需删除全部演示数据,取消注释执行;按依赖倒序删除)
-- ------------------------------------------------------------------------------
-- SET FOREIGN_KEY_CHECKS = 0;
-- DROP TABLE IF EXISTS `demo_theory_score`;
-- DROP TABLE IF EXISTS `demo_student_course`;
-- DROP TABLE IF EXISTS `demo_class_member`;
-- DROP TABLE IF EXISTS `demo_student`;
-- DROP TABLE IF EXISTS `demo_plan_course`;
-- DROP TABLE IF EXISTS `demo_course`;
-- DROP TABLE IF EXISTS `demo_class_plan`;
-- SET FOREIGN_KEY_CHECKS = 1;
@@ -0,0 +1,330 @@
-- ==============================================================================
-- RailwaySystem 测试数据: 007_demo_theory_score_kbjh2026_11.sql
-- 目标表: train_theory_exam_score(班级学员理论考试成绩表,需先执行 026 建表脚本)
-- 指定班级: 开班计划名称 = 'KBJH2026-11现场测试开班'
-- 数据库: MySQL 5.7+(仅使用派生表 / 临时表 / INSERT...SELECT / INSERT IGNORE,未用 CTE、窗口函数)
-- ==============================================================================
-- 一、依赖表结构与字段说明(均需已存在,本脚本不建业务表)
-- ------------------------------------------------------------------------------
-- 1) train_student(学员主档,本脚本只读)
-- s_id VARCHAR(32) PK 学员ID
-- s_name VARCHAR(50) 姓名(仅用于结果核对,不参与写入)
-- del_flag CHAR(1) 删除标志(0存在;若该列不存在请忽略相关注释)
--
-- 2) train_class_plan(开班计划,本脚本只读)
-- plan_id VARCHAR(32) PK 开班计划ID
-- plan_name VARCHAR(150) 计划名称 —— 本脚本按 'KBJH2026-11现场测试开班' 过滤
--
-- 3) train_plan_course(班级开设课程,本脚本只读)
-- plan_course_id BIGINT PK 培训课程实例ID(成绩表的课程维度键)
-- plan_id VARCHAR(32) 所属开班计划
-- c_id VARCHAR(32) 课程主档ID
-- UNIQUE (plan_id, c_id)
--
-- 4) train_enrollment(参训档案,本脚本只读)
-- enrollment_id BIGINT PK 参训档案ID(学员 × 班级唯一事实)
-- plan_id VARCHAR(32) 开班计划ID
-- student_id VARCHAR(32) 学员ID
-- —— 无 del_flag 列
--
-- 5) train_theory_exam_score(写入目标,字段含义见 026 脚本)
-- enrollment_id / plan_id / plan_course_id / c_id / student_id —— 维度
-- exam_start_time / exam_end_time / duration_seconds —— 考试过程
-- retake / retake_no —— 正考(0/0) / 补考(1/1..n)
-- exam_status (NORMAL/ABSENT/EXEMPT) —— 考试状态
-- score DECIMAL(7,2) NULL / full_score / pass_flag —— 成绩
-- is_final (1/0) —— 是否该课程最终成绩
-- score_status (RECEIVED/ARCHIVED/LOCKED) —— 成绩状态
-- source_type (1/2/3) / source_system / source_event_id —— 来源
-- del_flag ('0'/'2')
--
-- 二、生成逻辑
-- · 事实源 = 该计划下“学员 × 该班已开设课程”的笛卡尔组合:
-- train_enrollment (按 plan_id + student_id 取 MIN(enrollment_id) 去重)
-- JOIN train_class_plan (plan_name = 'KBJH2026-11现场测试开班')
-- JOIN train_plan_course (pc.plan_id = e.plan_id)
-- JOIN train_student (保证学员主档存在)
-- 由此保证 student_id / plan_id / plan_course_id / c_id 四者同源一致;
-- · 组合先落到临时表 tmp_pair,避免重复计算,也保证同一组合只生成一次;
-- · 再从 tmp_pair 中按 (student_id, plan_course_id) 升序取前 8 个组合,
-- 分别打上 FULL / ZERO / ABSENT / PASS_LINE / NEAR_LINE / EXEMPT / RETAKE1 / RETAKE2 场景标签,
-- 确保边界与异常样本**一定存在**(组合数 ≥ 8 时全部覆盖,不足时自动降级);
-- · 其余组合按成绩 45~100 的确定性分布生成(CRC32(学号+课程) 取模);
-- · 补考:对 RETAKE1 / RETAKE2 两个组合额外生成一条补考记录(retake=1, retake_no=1, is_final=1),
-- 其正考记录 is_final=0,既展示“补考后成为最终成绩”,又不违反 uk_theory_final;
-- · 全程 INSERT IGNORE:重复执行不报错、不产生重复数据(不会掩盖外键错误)。
--
-- 三、数据量
-- 正考行数 = 该计划“学员数 × 开设课程数”(本脚本生成全部组合)
-- 补考行数 = 2(RETAKE1、RETAKE2 两个组合)
-- 例如 20 名学员 × 3 门课程 → 60 条正考 + 2 条补考 = 62 行
-- 若该计划不存在或无学员/无课程,则插入 0 行(第 0 步的体检查询可确认)
-- ==============================================================================
SET NAMES utf8mb4;
-- 目标班级(如需换班,只改这一行)
SET @plan_name = 'KBJH2026-11现场测试开班';
-- ------------------------------------------------------------------------------
-- 第 0 步:体检(只读)
-- 0.1 计划是否存在
-- 0.2 该计划的学员数 / 课程数 / 可生成组合数
-- ------------------------------------------------------------------------------
SELECT plan_id, plan_name, class_name, start_time, end_time, plan_status
FROM train_class_plan
WHERE plan_name = @plan_name;
SELECT COUNT(*) AS available_pairs
FROM (
SELECT e.plan_id, e.student_id, pc.plan_course_id
FROM (SELECT plan_id, student_id, MIN(enrollment_id) AS enrollment_id
FROM train_enrollment
GROUP BY plan_id, student_id) e
JOIN train_class_plan cp ON cp.plan_id = e.plan_id AND cp.plan_name = @plan_name
JOIN train_plan_course pc ON pc.plan_id = e.plan_id
JOIN train_student s ON s.s_id = e.student_id
) p;
-- ------------------------------------------------------------------------------
-- 第 1 步:构建“学员 × 课程”组合临时表
-- ------------------------------------------------------------------------------
DROP TEMPORARY TABLE IF EXISTS tmp_pair;
CREATE TEMPORARY TABLE tmp_pair (
enrollment_id BIGINT NOT NULL,
plan_id VARCHAR(32) NOT NULL,
student_id VARCHAR(32) NOT NULL,
plan_course_id BIGINT NOT NULL,
c_id VARCHAR(32) NOT NULL,
PRIMARY KEY (plan_course_id, student_id),
KEY idx_tmp_pair_student (student_id)
) ENGINE=InnoDB;
INSERT INTO tmp_pair (enrollment_id, plan_id, student_id, plan_course_id, c_id)
SELECT e.enrollment_id, e.plan_id, e.student_id, pc.plan_course_id, pc.c_id
FROM (SELECT plan_id, student_id, MIN(enrollment_id) AS enrollment_id
FROM train_enrollment
GROUP BY plan_id, student_id) e
JOIN train_class_plan cp ON cp.plan_id = e.plan_id AND cp.plan_name = @plan_name
JOIN train_plan_course pc ON pc.plan_id = e.plan_id
JOIN train_course c ON c.c_id = pc.c_id
JOIN train_student s ON s.s_id = e.student_id;
-- ------------------------------------------------------------------------------
-- 第 2 步:为前 8 个组合打场景标签(边界 / 异常样本)
-- FULL 满分 100
-- ZERO 零分 0
-- ABSENT 缺考(score = NULL)
-- PASS_LINE 及格线 60
-- NEAR_LINE 59.5(差 0.5 分未通过)
-- EXEMPT 免修(score = NULL,计通过)
-- RETAKE1 正考 45 分未通过,后续补考 72 分通过
-- RETAKE2 正考 50 分未通过,后续补考 78 分通过
-- ------------------------------------------------------------------------------
DROP TEMPORARY TABLE IF EXISTS tmp_seed;
CREATE TEMPORARY TABLE tmp_seed (
student_id VARCHAR(32) NOT NULL,
plan_course_id BIGINT NOT NULL,
scenario VARCHAR(16) NOT NULL,
PRIMARY KEY (student_id, plan_course_id)
) ENGINE=InnoDB;
INSERT INTO tmp_seed (student_id, plan_course_id, scenario)
SELECT student_id, plan_course_id, 'FULL'
FROM tmp_pair ORDER BY student_id, plan_course_id LIMIT 1 OFFSET 0;
INSERT INTO tmp_seed (student_id, plan_course_id, scenario)
SELECT student_id, plan_course_id, 'ZERO'
FROM tmp_pair ORDER BY student_id, plan_course_id LIMIT 1 OFFSET 1;
INSERT INTO tmp_seed (student_id, plan_course_id, scenario)
SELECT student_id, plan_course_id, 'ABSENT'
FROM tmp_pair ORDER BY student_id, plan_course_id LIMIT 1 OFFSET 2;
INSERT INTO tmp_seed (student_id, plan_course_id, scenario)
SELECT student_id, plan_course_id, 'PASS_LINE'
FROM tmp_pair ORDER BY student_id, plan_course_id LIMIT 1 OFFSET 3;
INSERT INTO tmp_seed (student_id, plan_course_id, scenario)
SELECT student_id, plan_course_id, 'NEAR_LINE'
FROM tmp_pair ORDER BY student_id, plan_course_id LIMIT 1 OFFSET 4;
INSERT INTO tmp_seed (student_id, plan_course_id, scenario)
SELECT student_id, plan_course_id, 'EXEMPT'
FROM tmp_pair ORDER BY student_id, plan_course_id LIMIT 1 OFFSET 5;
INSERT INTO tmp_seed (student_id, plan_course_id, scenario)
SELECT student_id, plan_course_id, 'RETAKE1'
FROM tmp_pair ORDER BY student_id, plan_course_id LIMIT 1 OFFSET 6;
INSERT INTO tmp_seed (student_id, plan_course_id, scenario)
SELECT student_id, plan_course_id, 'RETAKE2'
FROM tmp_pair ORDER BY student_id, plan_course_id LIMIT 1 OFFSET 7;
-- ------------------------------------------------------------------------------
-- 第 3 步:生成正考成绩(覆盖该计划全部“学员 × 课程”组合)
-- RETAKE1 / RETAKE2 组合的 is_final = 0(其最终成绩由补考记录承担)
-- ------------------------------------------------------------------------------
INSERT IGNORE INTO train_theory_exam_score
(enrollment_id, plan_id, plan_course_id, c_id, student_id, subject_code,
exam_start_time, exam_end_time, duration_seconds, retake, retake_no,
exam_status, score, full_score, pass_flag, is_final, score_status,
source_type, source_system, source_event_id, score_item_id, remark,
del_flag, create_by, create_time)
SELECT x.enrollment_id, x.plan_id, x.plan_course_id, x.c_id, x.student_id, NULL,
DATE_SUB(x.end_time, INTERVAL 90 MINUTE), x.end_time, 5400, 0, 0,
x.exam_status, x.score, 100.00,
CASE
WHEN x.scenario = 'EXEMPT' THEN 1 -- 免修不计分,按通过计
WHEN x.score IS NOT NULL AND x.score >= 60 THEN 1
ELSE 0
END,
CASE WHEN x.scenario IN ('RETAKE1', 'RETAKE2') THEN 0 ELSE 1 END,
'RECEIVED',
1, NULL, NULL, NULL,
CASE x.scenario
WHEN 'FULL' THEN '测试数据-边界值:满分100'
WHEN 'ZERO' THEN '测试数据-边界值:零分0'
WHEN 'ABSENT' THEN '测试数据-异常:缺考无成绩'
WHEN 'PASS_LINE' THEN '测试数据-边界值:及格线60'
WHEN 'NEAR_LINE' THEN '测试数据-边界值:59.5差0.5分未通过'
WHEN 'EXEMPT' THEN '测试数据-异常:免修不计分'
WHEN 'RETAKE1' THEN '测试数据-异常:正考45分未通过(待补考)'
WHEN 'RETAKE2' THEN '测试数据-异常:正考50分未通过(待补考)'
ELSE NULL
END,
'0', 'test-data', NOW()
FROM (
SELECT p.enrollment_id, p.plan_id, p.plan_course_id, p.c_id, p.student_id,
COALESCE(sd.scenario, 'NORMAL') AS scenario,
CASE COALESCE(sd.scenario, 'NORMAL')
WHEN 'FULL' THEN CAST(100.00 AS DECIMAL(7,2))
WHEN 'ZERO' THEN CAST(0.00 AS DECIMAL(7,2))
WHEN 'ABSENT' THEN NULL
WHEN 'EXEMPT' THEN NULL
WHEN 'PASS_LINE' THEN CAST(60.00 AS DECIMAL(7,2))
WHEN 'NEAR_LINE' THEN CAST(59.50 AS DECIMAL(7,2))
WHEN 'RETAKE1' THEN CAST(45.00 AS DECIMAL(7,2))
WHEN 'RETAKE2' THEN CAST(50.00 AS DECIMAL(7,2))
ELSE CAST(45 + MOD(CRC32(CONCAT(p.student_id, '-', p.plan_course_id)), 56) AS DECIMAL(7,2))
END AS score,
CASE WHEN COALESCE(sd.scenario, 'NORMAL') IN ('ABSENT', 'EXEMPT') THEN
CASE WHEN sd.scenario = 'EXEMPT' THEN 'EXEMPT' ELSE 'ABSENT' END
ELSE 'NORMAL'
END AS exam_status,
DATE_SUB(NOW(), INTERVAL (MOD(CRC32(CONCAT(p.student_id, '-', p.plan_course_id)), 20) + 1) DAY) AS end_time
FROM tmp_pair p
LEFT JOIN tmp_seed sd
ON sd.student_id = p.student_id AND sd.plan_course_id = p.plan_course_id
) x;
-- ------------------------------------------------------------------------------
-- 第 4 步:生成补考成绩(RETAKE1 → 72 分,RETAKE2 → 78 分,均为最终成绩)
-- ------------------------------------------------------------------------------
INSERT IGNORE INTO train_theory_exam_score
(enrollment_id, plan_id, plan_course_id, c_id, student_id, subject_code,
exam_start_time, exam_end_time, duration_seconds, retake, retake_no,
exam_status, score, full_score, pass_flag, is_final, score_status,
source_type, source_system, source_event_id, score_item_id, remark,
del_flag, create_by, create_time)
SELECT p.enrollment_id, p.plan_id, p.plan_course_id, p.c_id, p.student_id, NULL,
DATE_SUB(NOW(), INTERVAL 3 DAY),
DATE_ADD(DATE_SUB(NOW(), INTERVAL 3 DAY), INTERVAL 90 MINUTE),
5400, 1, 1,
'NORMAL',
CASE sd.scenario WHEN 'RETAKE1' THEN CAST(72.00 AS DECIMAL(7,2)) ELSE CAST(78.00 AS DECIMAL(7,2)) END,
100.00, 1, 1, 'RECEIVED',
1, NULL, NULL, NULL, '测试数据-异常:补考通过(最终成绩)',
'0', 'test-data', NOW()
FROM tmp_pair p
JOIN tmp_seed sd
ON sd.student_id = p.student_id AND sd.plan_course_id = p.plan_course_id
WHERE sd.scenario IN ('RETAKE1', 'RETAKE2');
-- ==============================================================================
-- 第 5 步:结果校验(只读)
-- ==============================================================================
-- 5.1 该计划成绩总量与场景分布
SELECT t.exam_status, t.retake, t.is_final, COUNT(*) AS row_count
FROM train_theory_exam_score t
JOIN train_class_plan cp ON cp.plan_id = t.plan_id
WHERE cp.plan_name = @plan_name AND t.create_by = 'test-data'
GROUP BY t.exam_status, t.retake, t.is_final
ORDER BY t.exam_status, t.retake, t.is_final;
-- 5.2 成绩明细(含学员姓名、课程名、计划名)
SELECT cp.plan_name, s.s_id, s.s_name, c.c_name,
t.retake, t.retake_no, t.exam_status, t.score, t.full_score, t.pass_flag,
t.is_final, t.exam_end_time, t.remark
FROM train_theory_exam_score t
JOIN train_class_plan cp ON cp.plan_id = t.plan_id
JOIN train_student s ON s.s_id = t.student_id
JOIN train_course c ON c.c_id = t.c_id
WHERE cp.plan_name = @plan_name AND t.create_by = 'test-data'
ORDER BY s.s_id, c.c_id, t.retake_no;
-- 5.3 关联一致性自检(学员 / 课程实例 / 参训档案三项,均应 0 行)
SELECT t.theory_score_id, '学员不存在' AS problem
FROM train_theory_exam_score t
JOIN train_class_plan cp ON cp.plan_id = t.plan_id
LEFT JOIN train_student s ON s.s_id = t.student_id
WHERE cp.plan_name = @plan_name AND s.s_id IS NULL
UNION ALL
SELECT t.theory_score_id, '课程实例与计划/课程不匹配'
FROM train_theory_exam_score t
JOIN train_class_plan cp ON cp.plan_id = t.plan_id
LEFT JOIN train_plan_course pc
ON pc.plan_course_id = t.plan_course_id
AND pc.plan_id = t.plan_id
AND pc.c_id = t.c_id
WHERE cp.plan_name = @plan_name AND pc.plan_course_id IS NULL
UNION ALL
SELECT t.theory_score_id, '参训档案与学员/班级不匹配'
FROM train_theory_exam_score t
JOIN train_class_plan cp ON cp.plan_id = t.plan_id
LEFT JOIN train_enrollment e
ON e.enrollment_id = t.enrollment_id
AND e.student_id = t.student_id
AND e.plan_id = t.plan_id
WHERE cp.plan_name = @plan_name AND e.enrollment_id IS NULL;
-- 5.4 唯一性自检(均应为 0 行)
SELECT enrollment_id, plan_course_id, retake_no, COUNT(*) AS cnt
FROM train_theory_exam_score
WHERE del_flag = '0'
GROUP BY enrollment_id, plan_course_id, retake_no
HAVING COUNT(*) > 1;
SELECT plan_course_id, student_id, COUNT(*) AS final_cnt
FROM train_theory_exam_score
WHERE del_flag = '0' AND is_final = 1
GROUP BY plan_course_id, student_id
HAVING COUNT(*) > 1;
-- 5.5 成绩合法性自检(应 0 行)
SELECT t.theory_score_id, t.exam_status, t.score, t.full_score, t.pass_flag
FROM train_theory_exam_score t
JOIN train_class_plan cp ON cp.plan_id = t.plan_id
WHERE cp.plan_name = @plan_name AND t.create_by = 'test-data'
AND (
(t.exam_status = 'NORMAL' AND t.score IS NULL)
OR (t.exam_status IN ('ABSENT', 'EXEMPT') AND t.score IS NOT NULL)
OR (t.score IS NOT NULL AND (t.score < 0 OR t.score > t.full_score))
OR (t.score IS NOT NULL AND t.pass_flag <> IF(t.score >= 60, 1, 0))
);
-- ------------------------------------------------------------------------------
-- 第 6 步:清理(仅删除本脚本为该计划生成的数据,取消注释后执行)
-- ------------------------------------------------------------------------------
-- DELETE t FROM train_theory_exam_score t
-- JOIN train_class_plan cp ON cp.plan_id = t.plan_id
-- WHERE cp.plan_name = @plan_name AND t.create_by = 'test-data';
-- ==============================================================================
-- 附:无法落库的异常场景(DB 约束拦截,脚本故意不插,可用于负向验证)
-- 1) 同一 (enrollment_id, plan_course_id, retake_no) 重复 → uk_theory_enroll_course_retake
-- 2) 同一课程同一学员两条 is_final = 1 → uk_theory_final(生成列 final_flag)
-- 3) 缺考/免修带分数、正常考试无分数 → chk_theory_score_filled(MySQL 8.0 强制)
-- 4) 考试结束时间早于开始时间 → chk_theory_exam_time
-- 5) 引用不存在的学员/课程/计划 → 外键拦截
-- ==============================================================================