From c791e23f3e99331bc8a81e798e1ef59a70903ac2 Mon Sep 17 00:00:00 2001 From: wangzhibiao Date: Mon, 14 Sep 2026 02:13:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(db):=20=E6=96=B0=E5=A2=9EKingbase=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93V2=E8=BF=81=E7=A7=BB=E4=B8=8E=E5=9B=9E?= =?UTF-8?q?=E6=BB=9A=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- RuoYi-Vue/sql/train/v2/000_preflight.sql | 435 +++++++++++++++ .../sql/train/v2/000_tx_capability_probe.sql | 38 ++ .../train/v2/001_backup_current_schema.sql | 103 ++++ .../sql/train/v2/002_create_new_tables.sql | 213 ++++++++ .../sql/train/v2/003_alter_master_tables.sql | 251 +++++++++ .../train/v2/004_rebuild_business_tables.sql | 215 ++++++++ RuoYi-Vue/sql/train/v2/005_constraints.sql | 245 +++++++++ RuoYi-Vue/sql/train/v2/006_indexes.sql | 82 +++ RuoYi-Vue/sql/train/v2/007_dicts.sql | 335 ++++++++++++ .../sql/train/v2/008_migrate_teacher.sql | 57 ++ .../sql/train/v2/009_drop_legacy_score.sql | 46 ++ RuoYi-Vue/sql/train/v2/010_verify.sql | 500 ++++++++++++++++++ .../v2/rollback/000_phase05_exact_schema.sql | 356 +++++++++++++ .../v2/rollback/001_rollback_new_tables.sql | 45 ++ .../rollback/002_rollback_modified_tables.sql | 337 ++++++++++++ .../v2/rollback/003_restore_legacy_score.sql | 37 ++ RuoYi-Vue/sql/train/v2/rollback/README.md | 91 ++++ 17 files changed, 3386 insertions(+) create mode 100644 RuoYi-Vue/sql/train/v2/000_preflight.sql create mode 100644 RuoYi-Vue/sql/train/v2/000_tx_capability_probe.sql create mode 100644 RuoYi-Vue/sql/train/v2/001_backup_current_schema.sql create mode 100644 RuoYi-Vue/sql/train/v2/002_create_new_tables.sql create mode 100644 RuoYi-Vue/sql/train/v2/003_alter_master_tables.sql create mode 100644 RuoYi-Vue/sql/train/v2/004_rebuild_business_tables.sql create mode 100644 RuoYi-Vue/sql/train/v2/005_constraints.sql create mode 100644 RuoYi-Vue/sql/train/v2/006_indexes.sql create mode 100644 RuoYi-Vue/sql/train/v2/007_dicts.sql create mode 100644 RuoYi-Vue/sql/train/v2/008_migrate_teacher.sql create mode 100644 RuoYi-Vue/sql/train/v2/009_drop_legacy_score.sql create mode 100644 RuoYi-Vue/sql/train/v2/010_verify.sql create mode 100644 RuoYi-Vue/sql/train/v2/rollback/000_phase05_exact_schema.sql create mode 100644 RuoYi-Vue/sql/train/v2/rollback/001_rollback_new_tables.sql create mode 100644 RuoYi-Vue/sql/train/v2/rollback/002_rollback_modified_tables.sql create mode 100644 RuoYi-Vue/sql/train/v2/rollback/003_restore_legacy_score.sql create mode 100644 RuoYi-Vue/sql/train/v2/rollback/README.md diff --git a/RuoYi-Vue/sql/train/v2/000_preflight.sql b/RuoYi-Vue/sql/train/v2/000_preflight.sql new file mode 100644 index 000000000..5d7c180b6 --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/000_preflight.sql @@ -0,0 +1,435 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 迁移前置检查: 000_preflight.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 严格只读核验当前数据库完全符合 Phase 0.5 精确结构骨架与数据基线 +-- 设计依据: rollback/000_phase05_exact_schema.sql 与 13_live_database_snapshot.md +-- 验收机制: +-- 1. 纯只读探测,零写入,零结构修改 +-- 2. 基于预期元数据集合 (VALUES CTE) 进行全集合差异断言,杜绝数量对冲假象 +-- 3. 精确校验: 14旧业务表PK + 24旧业务FK + 7组业务UNIQUE + 6个identity主键 + 9个触发器 +-- 4. 任何 missing / unexpected / definition mismatch 立即抛出 EXCEPTION 中止后续迁移 +-- ============================================================================== + +DO $$ +DECLARE + v_schema TEXT; + v_cnt INT; + v_diff_text TEXT := ''; + r RECORD; +BEGIN + -- ========================================================================== + -- 1. 会话 Schema 校验 (必须显式为 public) + -- ========================================================================== + SELECT current_schema() INTO v_schema; + IF v_schema <> 'public' THEN + RAISE EXCEPTION '>>> [Preflight-FATAL] 当前会话 schema 为 "%",必须显式为 "public"', v_schema; + END IF; + + -- ========================================================================== + -- 2. 14 张 Phase 0.5 历史基线表双向集合差异检查 (精确存在且仅存在这 14 张 train_* 表) + -- ========================================================================== + v_diff_text := ''; + FOR r IN ( + WITH expected_tables(table_name) AS ( + VALUES + ('train_teacher'), ('train_student'), ('train_course'), ('train_class_plan'), + ('train_dormitory_info'), ('train_dorm_auth'), ('train_classroom_info'), ('train_training_site'), + ('train_class_report'), ('train_electronic_board'), ('train_student_checkin'), ('train_score_info'), + ('train_study_duration'), ('train_student_portrait') + ), + actual_tables AS ( + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' AND table_name LIKE 'train_%' + ) + SELECT 'MISSING_TABLE' AS diff_type, e.table_name + FROM expected_tables e + LEFT JOIN actual_tables a ON e.table_name = a.table_name + WHERE a.table_name IS NULL + UNION ALL + SELECT 'UNEXPECTED_TABLE' AS diff_type, a.table_name + FROM actual_tables a + LEFT JOIN expected_tables e ON a.table_name = e.table_name + WHERE e.table_name IS NULL + ) LOOP + v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '; '; + END LOOP; + + IF v_diff_text <> '' THEN + RAISE EXCEPTION '>>> [Preflight-FATAL] Phase 0.5 历史基线表集合全等校验失败: %', v_diff_text; + END IF; + + -- ========================================================================== + -- 3. 检查 8 张 V2 新增表均不得存在 (杜绝半迁移脏环境) + -- ========================================================================== + v_diff_text := ''; + FOR r IN ( + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name IN ( + 'train_resource', 'train_course_resource', 'train_plan_course', 'train_enrollment', + 'train_lesson', 'train_accommodation', 'train_score_item', 'train_student_score' + ) + ) LOOP + v_diff_text := v_diff_text || r.table_name || ', '; + END LOOP; + + IF v_diff_text <> '' THEN + RAISE EXCEPTION '>>> [Preflight-FATAL] 检测到已存在 V2 新表,当前非纯净 Phase 0.5 基线: %', v_diff_text; + END IF; + + -- ========================================================================== + -- 4. 数据行数基线: train_teacher 精确 1 行,其余 13 张旧表精确 0 行 + -- ========================================================================== + SELECT count(*) INTO v_cnt FROM train_teacher; + IF v_cnt <> 1 THEN + RAISE EXCEPTION '>>> [Preflight-FATAL] train_teacher 行数期望为 1,实际为 %', v_cnt; + END IF; + + SELECT count(*) INTO v_cnt FROM train_student; + IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_student 非空,包含 % 行', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_course; + IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_course 非空,包含 % 行', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_class_plan; + IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_class_plan 非空,包含 % 行', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_dormitory_info; + IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_dormitory_info 非空,包含 % 行', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_dorm_auth; + IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_dorm_auth 非空,包含 % 行', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_classroom_info; + IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_classroom_info 非空,包含 % 行', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_training_site; + IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_training_site 非空,包含 % 行', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_class_report; + IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_class_report 非空,包含 % 行', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_electronic_board; + IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_electronic_board 非空,包含 % 行', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_student_checkin; + IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_student_checkin 非空,包含 % 行', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_score_info; + IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_score_info 非空,包含 % 行', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_study_duration; + IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_study_duration 非空,包含 % 行', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_student_portrait; + IF v_cnt <> 0 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] train_student_portrait 非空,包含 % 行', v_cnt; END IF; + + -- ========================================================================== + -- 5. 14 个 Phase 0.5 旧业务主键 (PK) 精确骨架校验 + -- ========================================================================== + v_diff_text := ''; + FOR r IN ( + WITH expected_pk(table_name, constraint_name, column_name) AS ( + VALUES + ('train_teacher', 'pk_train_teacher', 't_id'), + ('train_student', 'pk_train_student', 's_id'), + ('train_course', 'pk_train_course', 'c_id'), + ('train_class_plan', 'pk_train_class_plan', 'plan_id'), + ('train_dormitory_info', 'pk_train_dormitory_info', 'dorm_id'), + ('train_dorm_auth', 'pk_train_dorm_auth', 'auth_id'), + ('train_classroom_info', 'pk_train_classroom_info', 'class_room_id'), + ('train_training_site', 'pk_train_training_site', 'site_id'), + ('train_class_report', 'pk_train_class_report', 'report_id'), + ('train_electronic_board', 'pk_train_electronic_board', 'board_id'), + ('train_student_checkin', 'pk_train_student_checkin', 'checkin_id'), + ('train_score_info', 'pk_train_score_info', 'score_id'), + ('train_study_duration', 'pk_train_study_duration', 'dur_id'), + ('train_student_portrait', 'pk_train_student_portrait', 'portrait_id') + ), + actual_pk AS ( + SELECT tc.table_name, tc.constraint_name, kcu.column_name + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + WHERE tc.constraint_type = 'PRIMARY KEY' + AND tc.table_schema = 'public' + AND tc.table_name LIKE 'train_%' + ) + SELECT 'MISSING_PK' AS diff_type, e.table_name, e.constraint_name, e.column_name + FROM expected_pk e + LEFT JOIN actual_pk a + ON e.table_name = a.table_name AND e.constraint_name = a.constraint_name AND e.column_name = a.column_name + WHERE a.table_name IS NULL + UNION ALL + SELECT 'UNEXPECTED_PK' AS diff_type, a.table_name, a.constraint_name, a.column_name + FROM actual_pk a + LEFT JOIN expected_pk e + ON a.table_name = e.table_name AND a.constraint_name = e.constraint_name AND a.column_name = e.column_name + WHERE e.table_name IS NULL + ) LOOP + v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '.' || r.constraint_name || '(' || r.column_name || '); '; + END LOOP; + + IF v_diff_text <> '' THEN + RAISE EXCEPTION '>>> [Preflight-FATAL] Phase 0.5 主键 (PK) 骨架不匹配: %', v_diff_text; + END IF; + + -- ========================================================================== + -- 6. 6 个 Phase 0.5 Identity 自增主键精确校验 + -- ========================================================================== + v_diff_text := ''; + FOR r IN ( + WITH expected_identity(table_name, column_name) AS ( + VALUES + ('train_dorm_auth', 'auth_id'), + ('train_class_report', 'report_id'), + ('train_student_checkin', 'checkin_id'), + ('train_score_info', 'score_id'), + ('train_study_duration', 'dur_id'), + ('train_student_portrait', 'portrait_id') + ), + actual_identity AS ( + SELECT table_name, column_name + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name LIKE 'train_%' + AND is_identity = 'YES' + ) + SELECT 'MISSING_IDENTITY' AS diff_type, e.table_name, e.column_name + FROM expected_identity e + LEFT JOIN actual_identity a + ON e.table_name = a.table_name AND e.column_name = a.column_name + WHERE a.table_name IS NULL + UNION ALL + SELECT 'UNEXPECTED_IDENTITY' AS diff_type, a.table_name, a.column_name + FROM actual_identity a + LEFT JOIN expected_identity e + ON a.table_name = e.table_name AND a.column_name = e.column_name + WHERE e.table_name IS NULL + ) LOOP + v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '.' || r.column_name || '; '; + END LOOP; + + IF v_diff_text <> '' THEN + RAISE EXCEPTION '>>> [Preflight-FATAL] Phase 0.5 Identity 属性不匹配: %', v_diff_text; + END IF; + + -- ========================================================================== + -- 7. 7 组 Phase 0.5 历史业务唯一约束 (UNIQUE) 骨架精确校验 + -- ========================================================================== + v_diff_text := ''; + FOR r IN ( + WITH expected_unique(table_name, constraint_name, column_list) AS ( + VALUES + ('train_dormitory_info', 'uq_dorm_building_room', 'dorm_building, dorm_room'), + ('train_dorm_auth', 'uq_dormauth_stu_plan', 'student_id, plan_id'), + ('train_class_report', 'uq_classreport_plan_stu', 'plan_id, student_id'), + ('train_electronic_board', 'uq_board_sn', 'board_sn'), + ('train_student_checkin', 'uq_checkin_lesson_stu', 'plan_id, c_id, student_id, lesson_start_time'), + ('train_score_info', 'uq_scoreinfo_plan_c_stu', 'plan_id, c_id, student_id'), + ('train_student_portrait', 'uq_portrait_stu_plan', 'student_id, plan_id') + ), + actual_unique AS ( + SELECT + tc.table_name, + tc.constraint_name, + string_agg(kcu.column_name, ', ' ORDER BY kcu.ordinal_position) AS column_list + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + WHERE tc.constraint_type = 'UNIQUE' + AND tc.table_schema = 'public' + AND tc.table_name LIKE 'train_%' + GROUP BY tc.table_name, tc.constraint_name + ) + SELECT 'MISSING_UNIQUE' AS diff_type, e.table_name, e.constraint_name, e.column_list + FROM expected_unique e + LEFT JOIN actual_unique a + ON e.table_name = a.table_name AND e.constraint_name = a.constraint_name AND e.column_list = a.column_list + WHERE a.table_name IS NULL + UNION ALL + SELECT 'UNEXPECTED_UNIQUE' AS diff_type, a.table_name, a.constraint_name, a.column_list + FROM actual_unique a + LEFT JOIN expected_unique e + ON a.table_name = e.table_name AND a.constraint_name = e.constraint_name AND a.column_list = e.column_list + WHERE e.table_name IS NULL + ) LOOP + v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '.' || r.constraint_name || '(' || r.column_list || '); '; + END LOOP; + + IF v_diff_text <> '' THEN + RAISE EXCEPTION '>>> [Preflight-FATAL] Phase 0.5 唯一约束 (UNIQUE) 骨架不匹配: %', v_diff_text; + END IF; + + -- ========================================================================== + -- 8. 24 组 Phase 0.5 历史业务外键约束 (FK) 骨架精确校验 (名称+所属表+源列+引用表+引用列) + -- ========================================================================== + v_diff_text := ''; + FOR r IN ( + WITH expected_fk(table_name, constraint_name, column_name, foreign_table_name, foreign_column_name) AS ( + VALUES + ('train_class_plan', 'fk_classplan_course', 'c_id', 'train_course', 'c_id'), + ('train_class_plan', 'fk_classplan_teacher', 't_id', 'train_teacher', 't_id'), + ('train_dorm_auth', 'fk_dormauth_stu', 'student_id', 'train_student', 's_id'), + ('train_dorm_auth', 'fk_dormauth_plan', 'plan_id', 'train_class_plan', 'plan_id'), + ('train_dorm_auth', 'fk_dormauth_dorm', 'dorm_id', 'train_dormitory_info', 'dorm_id'), + ('train_class_report', 'fk_classreport_plan', 'plan_id', 'train_class_plan', 'plan_id'), + ('train_class_report', 'fk_classreport_stu', 'student_id', 'train_student', 's_id'), + ('train_class_report', 'fk_classreport_dorm', 'alloc_dorm_id', 'train_dormitory_info', 'dorm_id'), + ('train_class_report', 'fk_classreport_classroom', 'alloc_classroom_id', 'train_classroom_info', 'class_room_id'), + ('train_class_report', 'fk_classreport_site', 'alloc_site_id', 'train_training_site', 'site_id'), + ('train_electronic_board', 'fk_board_classroom', 'bind_classroom_id', 'train_classroom_info', 'class_room_id'), + ('train_electronic_board', 'fk_board_plan', 'bind_plan_id', 'train_class_plan', 'plan_id'), + ('train_student_checkin', 'fk_checkin_plan', 'plan_id', 'train_class_plan', 'plan_id'), + ('train_student_checkin', 'fk_checkin_course', 'c_id', 'train_course', 'c_id'), + ('train_student_checkin', 'fk_checkin_stu', 'student_id', 'train_student', 's_id'), + ('train_student_checkin', 'fk_checkin_board', 'board_id', 'train_electronic_board', 'board_id'), + ('train_score_info', 'fk_scoreinfo_plan', 'plan_id', 'train_class_plan', 'plan_id'), + ('train_score_info', 'fk_scoreinfo_course', 'c_id', 'train_course', 'c_id'), + ('train_score_info', 'fk_scoreinfo_stu', 'student_id', 'train_student', 's_id'), + ('train_study_duration', 'fk_studydur_stu', 'student_id', 'train_student', 's_id'), + ('train_study_duration', 'fk_studydur_plan', 'plan_id', 'train_class_plan', 'plan_id'), + ('train_study_duration', 'fk_studydur_course', 'c_id', 'train_course', 'c_id'), + ('train_student_portrait', 'fk_portrait_stu', 'student_id', 'train_student', 's_id'), + ('train_student_portrait', 'fk_portrait_plan', 'plan_id', 'train_class_plan', 'plan_id') + ), + actual_fk AS ( + SELECT + tc.table_name, + tc.constraint_name, + kcu.column_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage ccu + ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema + WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema = 'public' + AND tc.table_name LIKE 'train_%' + ) + SELECT 'MISSING_FK' AS diff_type, e.table_name, e.constraint_name, e.column_name, e.foreign_table_name, e.foreign_column_name + FROM expected_fk e + LEFT JOIN actual_fk a + ON e.table_name = a.table_name + AND e.constraint_name = a.constraint_name + AND e.column_name = a.column_name + AND e.foreign_table_name = a.foreign_table_name + AND e.foreign_column_name = a.foreign_column_name + WHERE a.table_name IS NULL + UNION ALL + SELECT 'UNEXPECTED_FK' AS diff_type, a.table_name, a.constraint_name, a.column_name, a.foreign_table_name, a.foreign_column_name + FROM actual_fk a + LEFT JOIN expected_fk e + ON a.table_name = e.table_name + AND a.constraint_name = e.constraint_name + AND a.column_name = e.column_name + AND a.foreign_table_name = e.foreign_table_name + AND a.foreign_column_name = e.foreign_column_name + WHERE e.table_name IS NULL + ) LOOP + v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '.' || r.constraint_name || ' (' || r.column_name || ' -> ' || r.foreign_table_name || '.' || r.foreign_column_name || '); '; + END LOOP; + + IF v_diff_text <> '' THEN + RAISE EXCEPTION '>>> [Preflight-FATAL] Phase 0.5 外键约束 (FK) 骨架不匹配: %', v_diff_text; + END IF; + + -- ========================================================================== + -- 9. 9 个 Phase 0.5 历史 update_time 触发器精确校验 + -- ========================================================================== + v_diff_text := ''; + FOR r IN ( + WITH expected_triggers(table_name, trigger_name) AS ( + VALUES + ('train_teacher', 'trig_train_teacher_modify'), + ('train_student', 'trig_train_student_modify'), + ('train_course', 'trig_train_course_modify'), + ('train_classroom_info', 'trig_train_classroom_modify'), + ('train_training_site', 'trig_train_trainsite_modify'), + ('train_electronic_board', 'trig_train_board_modify'), + ('train_dormitory_info', 'trig_train_dorminfo_modify'), + ('train_class_plan', 'trig_train_classplan_modify'), + ('train_dorm_auth', 'trig_train_dormauth_modify') + ), + actual_triggers AS ( + SELECT event_object_table AS table_name, trigger_name + FROM information_schema.triggers + WHERE trigger_schema = 'public' + AND event_object_table LIKE 'train_%' + ) + SELECT 'MISSING_TRIGGER' AS diff_type, e.table_name, e.trigger_name + FROM expected_triggers e + LEFT JOIN actual_triggers a + ON e.table_name = a.table_name AND e.trigger_name = a.trigger_name + WHERE a.table_name IS NULL + UNION ALL + SELECT 'UNEXPECTED_TRIGGER' AS diff_type, a.table_name, a.trigger_name + FROM actual_triggers a + LEFT JOIN expected_triggers e + ON a.table_name = e.table_name AND a.trigger_name = e.trigger_name + WHERE e.table_name IS NULL + ) LOOP + v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '.' || r.trigger_name || '; '; + END LOOP; + + IF v_diff_text <> '' THEN + RAISE EXCEPTION '>>> [Preflight-FATAL] Phase 0.5 触发器骨架不匹配: %', v_diff_text; + END IF; + + -- ========================================================================== + -- 10. 历史旧字段特征校验 (确认是旧模型结构,未提前被执行过 V2) + -- ========================================================================== + SELECT count(*) INTO v_cnt FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'train_class_plan' AND column_name IN ('c_id', 't_id'); + IF v_cnt <> 2 THEN + RAISE EXCEPTION '>>> [Preflight-FATAL] train_class_plan 未包含旧 c_id 与 t_id 列,结构已被篡改'; + END IF; + + SELECT count(*) INTO v_cnt FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'train_student_checkin' AND column_name = 'lesson_start_time'; + IF v_cnt <> 1 THEN + RAISE EXCEPTION '>>> [Preflight-FATAL] train_student_checkin 结构异常,未发现旧 lesson_start_time 列'; + END IF; + + SELECT count(*) INTO v_cnt FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'train_score_info' + AND column_name IN ('course_score', 'train_score', 'exam_score'); + IF v_cnt <> 3 THEN + RAISE EXCEPTION '>>> [Preflight-FATAL] train_score_info 结构异常,未包含旧成绩列'; + END IF; + + SELECT count(*) INTO v_cnt FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'train_class_report' + AND column_name IN ('alloc_dorm_id', 'alloc_classroom_id', 'alloc_site_id'); + IF v_cnt <> 3 THEN + RAISE EXCEPTION '>>> [Preflight-FATAL] train_class_report 结构异常,未包含旧预分配列'; + END IF; + + -- ========================================================================== + -- 11. public.sys_* 系统基础表零受损基线核验 + -- ========================================================================== + SELECT count(*) INTO v_cnt FROM public.sys_user; + IF v_cnt <> 1 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] 基线 sys_user 行数受损,当前为 %', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM public.sys_menu; + IF v_cnt <> 106 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] 基线 sys_menu 行数受损,当前为 %', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM public.sys_dept; + IF v_cnt <> 15 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] 基线 sys_dept 行数受损,当前为 %', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM public.sys_dict_type; + IF v_cnt <> 44 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] 基线 sys_dict_type 行数受损,当前为 %', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM public.sys_dict_data; + IF v_cnt <> 500 THEN RAISE EXCEPTION '>>> [Preflight-FATAL] 基线 sys_dict_data 行数受损,当前为 %', v_cnt; END IF; + + RAISE NOTICE '=============================================================================='; + RAISE NOTICE '>>> [Preflight-PASS] Phase 0.5 精确骨架校验全部通过!'; + RAISE NOTICE '>>> 核验项: 14旧表 + 14 PK + 24 FK + 7 UNIQUE + 6 Identity + 9 Triggers + 行数基线'; + RAISE NOTICE '>>> 判定结果: 当前数据库处于纯净 Phase 0.5 历史基线状态,允许执行 V2 迁移。'; + RAISE NOTICE '=============================================================================='; +END $$; diff --git a/RuoYi-Vue/sql/train/v2/000_tx_capability_probe.sql b/RuoYi-Vue/sql/train/v2/000_tx_capability_probe.sql new file mode 100644 index 000000000..10bb12471 --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/000_tx_capability_probe.sql @@ -0,0 +1,38 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 迁移安全门禁 A: 000_tx_capability_probe.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public, database_mode: mysql) +-- 执行目标: 在正式执行迁移前,实证探测当前数据库实例是否支持 DDL 事务回滚能力 +-- 约束原则: +-- 1. 本文件为【前置能力探针】,绝不读取或修改任何 train_* 业务表与 sys_* 系统表 +-- 2. 仅在临时对象 public.__railway_v2_tx_probe 上执行可逆测试 +-- 3. 若 ROLLBACK 后探针表仍旧残留,表明 DDL 隐式自动提交,立即强行熔断后续迁移 +-- ============================================================================== + +-- Phase A: 启动显式事务,创建并修改探针临时表,随后主动 ROLLBACK +BEGIN; + +CREATE TABLE public.__railway_v2_tx_probe ( + id INTEGER +); + +ALTER TABLE public.__railway_v2_tx_probe +ADD COLUMN test_col INTEGER; + +ROLLBACK; + +-- Phase B: 只读断言验证探针表是否被完整撤销 +DO $$ +DECLARE + v_probe_exists INT; +BEGIN + SELECT count(*) INTO v_probe_exists + FROM information_schema.tables + WHERE table_schema = 'public' AND table_name = '__railway_v2_tx_probe'; + + IF v_probe_exists > 0 THEN + -- 发现探针表未被回滚,说明数据库引擎在执行 DDL 时发生了隐式提交 + RAISE EXCEPTION '>>> [Probe-FATAL] 当前 Kingbase 实例 DDL 不满足 V2 迁移事务假设 (DDL 在 ROLLBACK 后依然残留),禁止继续执行 001~010 迁移!'; + ELSE + RAISE NOTICE '>>> [Probe-PASS] 数据库 DDL 事务原子性验证通过:Kingbase 实例支持 DDL 事务回滚,符合 V2 迁移安全前提。'; + END IF; +END $$; diff --git a/RuoYi-Vue/sql/train/v2/001_backup_current_schema.sql b/RuoYi-Vue/sql/train/v2/001_backup_current_schema.sql new file mode 100644 index 000000000..95e2a5f4a --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/001_backup_current_schema.sql @@ -0,0 +1,103 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 迁移脚本: 001_backup_current_schema.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 备份当前 14 张培训旧表的数据与列结构快照 +-- 事务边界: 全程在显式事务 BEGIN ... COMMIT 中执行 +-- 安全原则: +-- 1. 禁止覆盖已有备份: 执行前检查若存在任意 bak_v1_* 表,立即抛出 EXCEPTION 强行阻断 +-- 2. 严禁使用 DROP TABLE IF EXISTS bak_v1_* 自动抹除已有备份 +-- 3. 严禁触碰任何 sys_* 系统表 +-- 4. 说明: CREATE TABLE AS SELECT 在 KingbaseES 中仅复制列名、类型与数据, +-- 不承担完整 DDL 快照职责 (完整结构备份由 rollback/000_phase05_exact_schema.sql 承担) +-- ============================================================================== + +BEGIN; + +-- 步骤 1: 强防重与防覆盖检查 (杜绝二次执行覆盖原始 V1 快照) +DO $$ +DECLARE + v_bak_cnt INT; + v_existing_baks TEXT := ''; + r RECORD; +BEGIN + FOR r IN ( + SELECT table_name + FROM information_schema.tables + WHERE table_schema = current_schema() AND table_name LIKE 'bak_v1_%' + ) LOOP + v_existing_baks := v_existing_baks || r.table_name || ', '; + END LOOP; + + IF v_existing_baks <> '' THEN + RAISE EXCEPTION '>>> [001-FATAL] 检测到既有备份表已经存在: [%]。为防止意外覆盖真实 V1 历史数据,严禁重复执行 001 备份!如需重新备份,请由 DBA 手动确认后归档清理既有 bak_v1_* 表。', v_existing_baks; + END IF; +END $$; + +-- 步骤 2: 备份具备真实数据的核心主数据表: train_teacher (当前环境精确 1 行) +CREATE TABLE bak_v1_train_teacher AS +SELECT * FROM train_teacher; +COMMENT ON TABLE bak_v1_train_teacher IS 'Phase 0.5 历史备份: 教师主档 (含唯一真实数据)'; + +-- 步骤 3: 备份其余 13 张现有空表 (当前行数均为 0,备份数据与字段基线) +CREATE TABLE bak_v1_train_student AS SELECT * FROM train_student; +COMMENT ON TABLE bak_v1_train_student IS 'Phase 0.5 历史备份: 学员表快照'; + +CREATE TABLE bak_v1_train_course AS SELECT * FROM train_course; +COMMENT ON TABLE bak_v1_train_course IS 'Phase 0.5 历史备份: 课程表快照'; + +CREATE TABLE bak_v1_train_class_plan AS SELECT * FROM train_class_plan; +COMMENT ON TABLE bak_v1_train_class_plan IS 'Phase 0.5 历史备份: 开班计划快照'; + +CREATE TABLE bak_v1_train_dormitory_info AS SELECT * FROM train_dormitory_info; +COMMENT ON TABLE bak_v1_train_dormitory_info IS 'Phase 0.5 历史备份: 宿舍信息快照'; + +CREATE TABLE bak_v1_train_dorm_auth AS SELECT * FROM train_dorm_auth; +COMMENT ON TABLE bak_v1_train_dorm_auth IS 'Phase 0.5 历史备份: 宿舍门禁快照'; + +CREATE TABLE bak_v1_train_classroom_info AS SELECT * FROM train_classroom_info; +COMMENT ON TABLE bak_v1_train_classroom_info IS 'Phase 0.5 历史备份: 教室信息快照'; + +CREATE TABLE bak_v1_train_training_site AS SELECT * FROM train_training_site; +COMMENT ON TABLE bak_v1_train_training_site IS 'Phase 0.5 历史备份: 实训场地快照'; + +CREATE TABLE bak_v1_train_class_report AS SELECT * FROM train_class_report; +COMMENT ON TABLE bak_v1_train_class_report IS 'Phase 0.5 历史备份: 报到记录快照'; + +CREATE TABLE bak_v1_train_electronic_board AS SELECT * FROM train_electronic_board; +COMMENT ON TABLE bak_v1_train_electronic_board IS 'Phase 0.5 历史备份: 电子班牌快照'; + +CREATE TABLE bak_v1_train_student_checkin AS SELECT * FROM train_student_checkin; +COMMENT ON TABLE bak_v1_train_student_checkin IS 'Phase 0.5 历史备份: 考勤签到快照'; + +CREATE TABLE bak_v1_train_score_info AS SELECT * FROM train_score_info; +COMMENT ON TABLE bak_v1_train_score_info IS 'Phase 0.5 历史备份: 旧成绩表快照 (待淘汰)'; + +CREATE TABLE bak_v1_train_study_duration AS SELECT * FROM train_study_duration; +COMMENT ON TABLE bak_v1_train_study_duration IS 'Phase 0.5 历史备份: 学习时长快照'; + +CREATE TABLE bak_v1_train_student_portrait AS SELECT * FROM train_student_portrait; +COMMENT ON TABLE bak_v1_train_student_portrait IS 'Phase 0.5 历史备份: 学员画像快照'; + +-- 步骤 4: 校验备份完整性 +DO $$ +DECLARE + v_teacher_cnt INT; + v_bak_total INT; +BEGIN + SELECT count(*) INTO v_teacher_cnt FROM bak_v1_train_teacher; + IF v_teacher_cnt <> 1 THEN + RAISE EXCEPTION '>>> [001-FATAL] 备份校验失败: bak_v1_train_teacher 期望 1 条数据,实际为 % 条', v_teacher_cnt; + END IF; + + SELECT count(*) INTO v_bak_total + FROM information_schema.tables + WHERE table_schema = current_schema() AND table_name LIKE 'bak_v1_%'; + + IF v_bak_total <> 14 THEN + RAISE EXCEPTION '>>> [001-FATAL] 备份表总数异常: 期望 14 张备份表,实际生成 % 张', v_bak_total; + END IF; + + RAISE NOTICE '>>> [001-PASS] 14 张表快照备份全部完成,教师唯一真实数据已安全封存。'; +END $$; + +COMMIT; diff --git a/RuoYi-Vue/sql/train/v2/002_create_new_tables.sql b/RuoYi-Vue/sql/train/v2/002_create_new_tables.sql new file mode 100644 index 000000000..db53eb5b3 --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/002_create_new_tables.sql @@ -0,0 +1,213 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 迁移脚本: 002_create_new_tables.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 创建数据库 V2 新增的 8 张核心培训业务表 +-- 事务边界: 全程在显式事务 BEGIN ... COMMIT 中执行 +-- 设计依据: RailwaySystem 数据库 V2 字段设计与 Kingbase DDL 规范 V1.0 及 Round 1 审查要求 +-- 规范要求: +-- 1. 主键为 bigint GENERATED BY DEFAULT AS IDENTITY 或 varchar(32) +-- 2. 时间类型统一使用 timestamp,禁用 DATETIME / ON UPDATE CURRENT_TIMESTAMP +-- 3. 状态与布尔统一使用 smallint +-- 4. train_lesson.t_id 设为 NOT NULL +-- 5. train_student_score.score 设为 NOT NULL (删除 DEFAULT 0) 并增加 score >= 0 CHECK +-- 6. train_lesson 增加 venue_type 与教室/场地的单行互斥 CHECK +-- 7. 字典语义严格对齐 V2 冻结稿 +-- ============================================================================== + +BEGIN; + +-- 1. 统一教学资源主表 +CREATE TABLE train_resource ( + resource_id VARCHAR(32) NOT NULL, + resource_name VARCHAR(150) NOT NULL, + resource_type SMALLINT NOT NULL, + resource_url TEXT NOT NULL, + description TEXT, + status SMALLINT NOT NULL DEFAULT 1, + create_by VARCHAR(64), + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_by VARCHAR(64), + update_time TIMESTAMP, + remark VARCHAR(500), + del_flag CHAR(1) NOT NULL DEFAULT '0', + CONSTRAINT pk_train_resource PRIMARY KEY (resource_id) +); +COMMENT ON TABLE train_resource IS '统一教学资源表 (文档、视频、3D模型、VR资源、外部教学链接、其他)'; +COMMENT ON COLUMN train_resource.resource_id IS '资源唯一ID'; +COMMENT ON COLUMN train_resource.resource_name IS '资源名称'; +COMMENT ON COLUMN train_resource.resource_type IS '资源类型 (1:文档, 2:视频, 3:3D模型, 4:VR资源, 5:外部教学链接, 6:其他)'; +COMMENT ON COLUMN train_resource.resource_url IS '资源存储地址/在线链接'; +COMMENT ON COLUMN train_resource.description IS '资源描述/使用说明'; +COMMENT ON COLUMN train_resource.status IS '状态 (1:正常, 0:停用)'; +COMMENT ON COLUMN train_resource.del_flag IS '删除标志 (0:存在, 2:删除)'; + +-- 2. 课程与资源多对多关联表 +CREATE TABLE train_course_resource ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY, + c_id VARCHAR(32) NOT NULL, + resource_id VARCHAR(32) NOT NULL, + sort_no INTEGER DEFAULT 0, + create_by VARCHAR(64), + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT pk_train_course_resource PRIMARY KEY (id) +); +COMMENT ON TABLE train_course_resource IS '课程与教学资源关联表 (N:M)'; +COMMENT ON COLUMN train_course_resource.id IS '自增主键'; +COMMENT ON COLUMN train_course_resource.c_id IS '课程ID'; +COMMENT ON COLUMN train_course_resource.resource_id IS '资源ID'; +COMMENT ON COLUMN train_course_resource.sort_no IS '排序号'; + +-- 3. 培训班与课程关联表 +CREATE TABLE train_plan_course ( + plan_course_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + plan_id VARCHAR(32) NOT NULL, + c_id VARCHAR(32) NOT NULL, + sort_no INTEGER DEFAULT 0, + create_by VARCHAR(64), + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(500), + CONSTRAINT pk_train_plan_course PRIMARY KEY (plan_course_id) +); +COMMENT ON TABLE train_plan_course IS '培训班课程关联表 (一期培训包含哪些课程)'; +COMMENT ON COLUMN train_plan_course.plan_course_id IS '培训课程实例ID (核心ID之一)'; +COMMENT ON COLUMN train_plan_course.plan_id IS '培训班ID'; +COMMENT ON COLUMN train_plan_course.c_id IS '课程ID'; +COMMENT ON COLUMN train_plan_course.sort_no IS '排课先后顺序号'; + +-- 4. 参训学员登记表 (系统最核心关系: 谁参加哪个班) +CREATE TABLE train_enrollment ( + enrollment_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + plan_id VARCHAR(32) NOT NULL, + student_id VARCHAR(32) NOT NULL, + source_type SMALLINT NOT NULL, + enrollment_status SMALLINT NOT NULL DEFAULT 0, + accommodation_required SMALLINT NOT NULL, + join_time TIMESTAMP, + complete_time TIMESTAMP, + create_by VARCHAR(64), + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_by VARCHAR(64), + update_time TIMESTAMP, + remark VARCHAR(500), + CONSTRAINT pk_train_enrollment PRIMARY KEY (enrollment_id) +); +COMMENT ON TABLE train_enrollment IS '参训学员登记表 (学员参加培训班全生命周期主表)'; +COMMENT ON COLUMN train_enrollment.enrollment_id IS '参训档案ID (核心ID之一)'; +COMMENT ON COLUMN train_enrollment.plan_id IS '培训班ID'; +COMMENT ON COLUMN train_enrollment.student_id IS '学员长期主档ID引用'; +COMMENT ON COLUMN train_enrollment.source_type IS '参训名单来源,具体字典值由 train_enrollment_source 定义'; +COMMENT ON COLUMN train_enrollment.enrollment_status IS '参训状态 (0:待报到, 1:已报到, 2:请假, 3:放弃, 4:已完成)'; +COMMENT ON COLUMN train_enrollment.accommodation_required IS '是否需要住宿 (0:否, 1:是)'; +COMMENT ON COLUMN train_enrollment.join_time IS '入班报到时间'; +COMMENT ON COLUMN train_enrollment.complete_time IS '培训完成/结课时间'; + +-- 5. 教学活动排课表 (排课课次必要包含授课教师,且场地类型与教室/实训场地严格互斥) +CREATE TABLE train_lesson ( + lesson_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + plan_course_id BIGINT NOT NULL, + t_id VARCHAR(32) NOT NULL, + start_time TIMESTAMP NOT NULL, + end_time TIMESTAMP NOT NULL, + venue_type SMALLINT NOT NULL, + class_room_id VARCHAR(32), + site_id VARCHAR(32), + create_by VARCHAR(64), + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_by VARCHAR(64), + update_time TIMESTAMP, + remark VARCHAR(500), + CONSTRAINT pk_train_lesson PRIMARY KEY (lesson_id), + CONSTRAINT chk_lesson_venue CHECK ( + (venue_type = 1 AND class_room_id IS NOT NULL AND site_id IS NULL) OR + (venue_type = 2 AND site_id IS NOT NULL AND class_room_id IS NULL) + ) +); +COMMENT ON TABLE train_lesson IS '课次教学活动表 (具体一次上课/实训安排)'; +COMMENT ON COLUMN train_lesson.lesson_id IS '课次ID (核心ID之一)'; +COMMENT ON COLUMN train_lesson.plan_course_id IS '所属培训课程实例ID'; +COMMENT ON COLUMN train_lesson.t_id IS '授课教师ID (课次必要组成)'; +COMMENT ON COLUMN train_lesson.start_time IS '课次开始时间'; +COMMENT ON COLUMN train_lesson.end_time IS '课次结束时间'; +COMMENT ON COLUMN train_lesson.venue_type IS '场地类型 (1:普通教室, 2:实训场地)'; +COMMENT ON COLUMN train_lesson.class_room_id IS '普通教室ID (当场地类型为教室时必填且场地为空)'; +COMMENT ON COLUMN train_lesson.site_id IS '实训场地ID (当场地类型为实训场地时必填且教室为空)'; + +-- 6. 学员住宿安排表 (房间必填 + 床位可选) +CREATE TABLE train_accommodation ( + accommodation_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + enrollment_id BIGINT NOT NULL, + dorm_id VARCHAR(32) NOT NULL, + bed_no VARCHAR(20), + assigned_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + accommodation_status SMALLINT NOT NULL DEFAULT 0, + create_by VARCHAR(64), + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_by VARCHAR(64), + update_time TIMESTAMP, + remark VARCHAR(500), + CONSTRAINT pk_train_accommodation PRIMARY KEY (accommodation_id) +); +COMMENT ON TABLE train_accommodation IS '学员住宿安排表 (本次参训住宿分配)'; +COMMENT ON COLUMN train_accommodation.accommodation_id IS '住宿安排ID'; +COMMENT ON COLUMN train_accommodation.enrollment_id IS '参训档案ID'; +COMMENT ON COLUMN train_accommodation.dorm_id IS '分配宿舍房间ID'; +COMMENT ON COLUMN train_accommodation.bed_no IS '分配床位号 (可选)'; +COMMENT ON COLUMN train_accommodation.assigned_time IS '分配时间'; +COMMENT ON COLUMN train_accommodation.accommodation_status IS '住宿状态 (0:预分配, 1:已入住, 2:已退宿)'; + +-- 7. 培训班考核成绩项配置表 (用户自由配置分值与权重) +CREATE TABLE train_score_item ( + score_item_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + plan_id VARCHAR(32) NOT NULL, + plan_course_id BIGINT, + item_name VARCHAR(100) NOT NULL, + item_type SMALLINT NOT NULL DEFAULT 1, + weight NUMERIC(5,2) NOT NULL DEFAULT 0.00, + full_score NUMERIC(5,2) NOT NULL DEFAULT 100.00, + sort_no INTEGER DEFAULT 0, + create_by VARCHAR(64), + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_by VARCHAR(64), + update_time TIMESTAMP, + remark VARCHAR(500), + CONSTRAINT pk_train_score_item PRIMARY KEY (score_item_id) +); +COMMENT ON TABLE train_score_item IS '考核成绩项配置表 (某培训班的成绩构成规则)'; +COMMENT ON COLUMN train_score_item.score_item_id IS '成绩项ID'; +COMMENT ON COLUMN train_score_item.plan_id IS '培训班ID'; +COMMENT ON COLUMN train_score_item.plan_course_id IS '关联培训课程ID (为空表示全班综合项)'; +COMMENT ON COLUMN train_score_item.item_name IS '考核项目名称'; +COMMENT ON COLUMN train_score_item.item_type IS '项目类型 (1:理论考试, 2:实操演练, 3:出勤考核, 4:综合评价)'; +COMMENT ON COLUMN train_score_item.weight IS '所占综合权重百分比 (0~100)'; +COMMENT ON COLUMN train_score_item.full_score IS '满分设定 (默认 100)'; +COMMENT ON COLUMN train_score_item.sort_no IS '展示顺序'; + +-- 8. 学员单项得分明细表 (参训学员在某考核项上的实际成绩,无默认0,有行即有分且非负) +CREATE TABLE train_student_score ( + score_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + enrollment_id BIGINT NOT NULL, + score_item_id BIGINT NOT NULL, + score NUMERIC(7,2) NOT NULL, + source_type SMALLINT NOT NULL, + source_system VARCHAR(100), + source_event_id VARCHAR(100), + import_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + create_by VARCHAR(64), + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_by VARCHAR(64), + update_time TIMESTAMP, + remark VARCHAR(500), + CONSTRAINT pk_train_student_score PRIMARY KEY (score_id), + CONSTRAINT chk_student_score_nonneg CHECK (score >= 0.00) +); +COMMENT ON TABLE train_student_score IS '学员成绩明细表 (某学员某成绩项得分,未录入成绩不生成记录)'; +COMMENT ON COLUMN train_student_score.score_id IS '成绩明细ID'; +COMMENT ON COLUMN train_student_score.enrollment_id IS '参训档案ID'; +COMMENT ON COLUMN train_student_score.score_item_id IS '关联成绩项ID'; +COMMENT ON COLUMN train_student_score.score IS '实际得分 (非负,未录成绩不建行,0为真实0分)'; +COMMENT ON COLUMN train_student_score.source_type IS '录入来源类型 (1:人工录入, 2:理论考试系统, 3:实操设备, 4:外部导入)'; +COMMENT ON COLUMN train_student_score.source_system IS '来源外部系统名称'; +COMMENT ON COLUMN train_student_score.source_event_id IS '来源外部事件/单号ID (用于幂等)'; +COMMENT ON COLUMN train_student_score.import_time IS '成绩写入/同步时间'; + +COMMIT; diff --git a/RuoYi-Vue/sql/train/v2/003_alter_master_tables.sql b/RuoYi-Vue/sql/train/v2/003_alter_master_tables.sql new file mode 100644 index 000000000..464a5894b --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/003_alter_master_tables.sql @@ -0,0 +1,251 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 迁移脚本: 003_alter_master_tables.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 调整 8 张主数据表结构至 V2 规范模型 +-- 事务边界: 全程在显式事务 BEGIN ... COMMIT 中执行 +-- 设计依据: RailwaySystem 数据库 V2 字段设计与 Kingbase DDL 规范 V1.0 及 Round 1 审查要求 +-- 包含表: +-- 1. train_teacher (保留1行数据,先加新列,旧列待008迁移并校验后设NOT NULL并删除旧列) +-- 2. train_student (0行,删除高校学籍旧列,增加企业主档与加密列,规范source_type语义) +-- 3. train_course (0行,删除旧附件字段,增加课程编码与审计列) +-- 4. train_classroom_info (0行,对齐公共审计字段) +-- 5. train_training_site (0行,对齐公共审计字段) +-- 6. train_electronic_board (0行,解绑计划ID,board_name 必填无默认空串) +-- 7. train_dormitory_info (0行,重构为容量与性别属性,删除预测默认值,增加 capacity > 0 CHECK) +-- 8. train_class_plan (0行,解绑单课程c_id与单教师t_id,升级为多课程架构) +-- ============================================================================== + +BEGIN; + +-- ============================================================================== +-- 步骤 1: 移除主数据表上绑定的旧 update_time 触发器 +-- ============================================================================== +DROP TRIGGER IF EXISTS trig_train_teacher_modify ON train_teacher; +DROP TRIGGER IF EXISTS trig_train_student_modify ON train_student; +DROP TRIGGER IF EXISTS trig_train_course_modify ON train_course; +DROP TRIGGER IF EXISTS trig_train_classroom_modify ON train_classroom_info; +DROP TRIGGER IF EXISTS trig_train_trainsite_modify ON train_training_site; +DROP TRIGGER IF EXISTS trig_train_board_modify ON train_electronic_board; +DROP TRIGGER IF EXISTS trig_train_dorminfo_modify ON train_dormitory_info; +DROP TRIGGER IF EXISTS trig_train_classplan_modify ON train_class_plan; + +-- ============================================================================== +-- 步骤 2: 显式解除阻塞主数据列删除或变更的旧外键约束 (不使用 CASCADE) +-- ============================================================================== +-- train_class_plan 上的旧外键 (指向 train_course 和 train_teacher) +ALTER TABLE train_class_plan DROP CONSTRAINT IF EXISTS fk_classplan_course; +ALTER TABLE train_class_plan DROP CONSTRAINT IF EXISTS fk_classplan_teacher; + +-- train_electronic_board 上的旧外键 (指向 train_class_plan) +ALTER TABLE train_electronic_board DROP CONSTRAINT IF EXISTS fk_board_plan; + +-- ============================================================================== +-- 步骤 3: 调整 train_teacher (有 1 行真实数据,暂不删除旧列,留待 008 迁移数据) +-- ============================================================================== +ALTER TABLE train_teacher + ADD COLUMN user_id BIGINT, + ADD COLUMN org_name VARCHAR(100), + ADD COLUMN job_title VARCHAR(50), + ADD COLUMN status SMALLINT DEFAULT 1, + ADD COLUMN create_by VARCHAR(64), + ADD COLUMN update_by VARCHAR(64), + ADD COLUMN del_flag CHAR(1) DEFAULT '0'; + +ALTER TABLE train_teacher + ALTER COLUMN t_name TYPE VARCHAR(50), + ALTER COLUMN t_gender TYPE CHAR(1), + ALTER COLUMN t_email TYPE VARCHAR(100), + ALTER COLUMN remark TYPE VARCHAR(500); + +COMMENT ON COLUMN train_teacher.user_id IS 'sys_user登录账号逻辑关联ID'; +COMMENT ON COLUMN train_teacher.org_name IS '所属单位/部门名称 (由旧 t_department 迁移)'; +COMMENT ON COLUMN train_teacher.job_title IS '职务/职称 (由旧 t_title 迁移)'; +COMMENT ON COLUMN train_teacher.status IS '状态 (1:正常, 0:停用)'; +COMMENT ON COLUMN train_teacher.del_flag IS '删除标志 (0:存在, 2:删除)'; + +-- ============================================================================== +-- 步骤 4: 调整 train_student (当前 0 行) +-- ============================================================================== +-- 4.1 淘汰高校模式旧字段与旧审计字段 +ALTER TABLE train_student + DROP COLUMN IF EXISTS s_department, + DROP COLUMN IF EXISTS s_major, + DROP COLUMN IF EXISTS s_class, + DROP COLUMN IF EXISTS create_user; + +-- 4.2 增加企业培训新主档字段、加密字段与公共审计字段 +ALTER TABLE train_student + ADD COLUMN user_id BIGINT, + ADD COLUMN org_name VARCHAR(100), + ADD COLUMN s_email VARCHAR(100), + ADD COLUMN id_card_ciphertext TEXT, + ADD COLUMN id_card_hash VARCHAR(128), + ADD COLUMN id_card_last4 VARCHAR(4), + ADD COLUMN face_ref VARCHAR(255), + ADD COLUMN source_type SMALLINT DEFAULT 1 NOT NULL, + ADD COLUMN status SMALLINT DEFAULT 1 NOT NULL, + ADD COLUMN create_by VARCHAR(64), + ADD COLUMN update_by VARCHAR(64), + ADD COLUMN del_flag CHAR(1) DEFAULT '0' NOT NULL; + +ALTER TABLE train_student + ALTER COLUMN s_name TYPE VARCHAR(50), + ALTER COLUMN s_gender TYPE CHAR(1), + ALTER COLUMN remark TYPE VARCHAR(500); + +COMMENT ON COLUMN train_student.user_id IS 'sys_user账号逻辑关联'; +COMMENT ON COLUMN train_student.org_name IS '所属单位/部门'; +COMMENT ON COLUMN train_student.id_card_ciphertext IS '身份证可逆加密密文'; +COMMENT ON COLUMN train_student.id_card_hash IS '身份证精确匹配Hash (防重)'; +COMMENT ON COLUMN train_student.id_card_last4 IS '身份证后四位展示'; +COMMENT ON COLUMN train_student.face_ref IS '人脸特征或图像凭证引用'; +COMMENT ON COLUMN train_student.source_type IS '人员主档进入系统的来源 (1:管理员录入, 2:Excel批量导入, 3:铁路内部业务平台同步)'; +COMMENT ON COLUMN train_student.status IS '主档状态 (1:正常, 0:停用)'; +COMMENT ON COLUMN train_student.del_flag IS '删除标志 (0:存在, 2:删除)'; + +-- ============================================================================== +-- 步骤 5: 调整 train_course (当前 0 行) +-- ============================================================================== +-- 5.1 淘汰旧集中附件字段 (已解耦迁入 train_resource 与 train_course_resource) +ALTER TABLE train_course + DROP COLUMN IF EXISTS attach_doc, + DROP COLUMN IF EXISTS attach_video, + DROP COLUMN IF EXISTS attach_model_link, + DROP COLUMN IF EXISTS model_resource_ids, + DROP COLUMN IF EXISTS create_user; + +-- 5.2 增加课程编码与标准审计字段 +ALTER TABLE train_course + ADD COLUMN c_code VARCHAR(50), + ADD COLUMN status SMALLINT DEFAULT 1 NOT NULL, + ADD COLUMN create_by VARCHAR(64), + ADD COLUMN update_by VARCHAR(64), + ADD COLUMN del_flag CHAR(1) DEFAULT '0' NOT NULL; + +ALTER TABLE train_course + ALTER COLUMN remark TYPE VARCHAR(500); + +COMMENT ON COLUMN train_course.c_code IS '课程统一业务编码'; +COMMENT ON COLUMN train_course.status IS '课程状态 (1:正常, 0:停用)'; +COMMENT ON COLUMN train_course.del_flag IS '删除标志 (0:存在, 2:删除)'; + +-- ============================================================================== +-- 步骤 6: 调整 train_classroom_info (当前 0 行) +-- ============================================================================== +ALTER TABLE train_classroom_info + DROP COLUMN IF EXISTS create_user; + +ALTER TABLE train_classroom_info + ADD COLUMN create_by VARCHAR(64), + ADD COLUMN update_by VARCHAR(64), + ADD COLUMN del_flag CHAR(1) DEFAULT '0' NOT NULL; + +ALTER TABLE train_classroom_info + ALTER COLUMN remark TYPE VARCHAR(500), + ALTER COLUMN room_status SET NOT NULL; + +COMMENT ON COLUMN train_classroom_info.del_flag IS '删除标志 (0:存在, 2:删除)'; + +-- ============================================================================== +-- 步骤 7: 调整 train_training_site (当前 0 行) +-- ============================================================================== +ALTER TABLE train_training_site + DROP COLUMN IF EXISTS create_user; + +ALTER TABLE train_training_site + ADD COLUMN create_by VARCHAR(64), + ADD COLUMN update_by VARCHAR(64), + ADD COLUMN del_flag CHAR(1) DEFAULT '0' NOT NULL; + +ALTER TABLE train_training_site + ALTER COLUMN remark TYPE VARCHAR(500), + ALTER COLUMN site_status SET NOT NULL; + +COMMENT ON COLUMN train_training_site.del_flag IS '删除标志 (0:存在, 2:删除)'; + +-- ============================================================================== +-- 步骤 8: 调整 train_electronic_board (当前 0 行) +-- ============================================================================== +-- 8.1 移除计划绑定列与旧播放字段 +ALTER TABLE train_electronic_board + DROP COLUMN IF EXISTS bind_plan_id, + DROP COLUMN IF EXISTS board_content, + DROP COLUMN IF EXISTS board_playmode, + DROP COLUMN IF EXISTS create_user; + +-- 8.2 增加设备名称 (必填,严禁空串默认)、API端点与审计字段 +ALTER TABLE train_electronic_board + ADD COLUMN board_name VARCHAR(100) NOT NULL, + ADD COLUMN api_endpoint VARCHAR(255), + ADD COLUMN create_by VARCHAR(64), + ADD COLUMN update_by VARCHAR(64), + ADD COLUMN del_flag CHAR(1) DEFAULT '0' NOT NULL; + +ALTER TABLE train_electronic_board + ALTER COLUMN support_checkin_type TYPE VARCHAR(50), + ALTER COLUMN remark TYPE VARCHAR(500), + ALTER COLUMN board_status SET NOT NULL; + +COMMENT ON COLUMN train_electronic_board.board_name IS '电子班牌名称 (必填)'; +COMMENT ON COLUMN train_electronic_board.api_endpoint IS '设备交互/推送接口地址'; +COMMENT ON COLUMN train_electronic_board.bind_classroom_id IS '绑定的物理教室ID'; +COMMENT ON COLUMN train_electronic_board.del_flag IS '删除标志 (0:存在, 2:删除)'; + +-- ============================================================================== +-- 步骤 9: 调整 train_dormitory_info (当前 0 行) +-- ============================================================================== +-- 9.1 移除静态床位统计列 +ALTER TABLE train_dormitory_info + DROP COLUMN IF EXISTS bed_total, + DROP COLUMN IF EXISTS bed_used, + DROP COLUMN IF EXISTS create_user; + +-- 9.2 增加楼层、房间额定容量、适用性别 (杜绝业务默认值猜测,增加容量正数 CHECK) +ALTER TABLE train_dormitory_info + ADD COLUMN dorm_floor VARCHAR(20) NOT NULL, + ADD COLUMN capacity INTEGER NOT NULL, + ADD COLUMN gender_type SMALLINT DEFAULT 0 NOT NULL, + ADD COLUMN create_by VARCHAR(64), + ADD COLUMN update_by VARCHAR(64), + ADD COLUMN del_flag CHAR(1) DEFAULT '0' NOT NULL; + +ALTER TABLE train_dormitory_info + ALTER COLUMN dorm_building SET NOT NULL, + ALTER COLUMN dorm_room SET NOT NULL, + ALTER COLUMN remark TYPE VARCHAR(500), + ALTER COLUMN dorm_status SET NOT NULL; + +ALTER TABLE train_dormitory_info DROP CONSTRAINT IF EXISTS chk_dorm_capacity; +ALTER TABLE train_dormitory_info ADD CONSTRAINT chk_dorm_capacity CHECK (capacity > 0); + +COMMENT ON COLUMN train_dormitory_info.dorm_floor IS '所在楼层 (管理员真实录入)'; +COMMENT ON COLUMN train_dormitory_info.capacity IS '房间核定床位容量 (正整数,无默认值)'; +COMMENT ON COLUMN train_dormitory_info.gender_type IS '适用性别 (0:不限, 1:男, 2:女)'; +COMMENT ON COLUMN train_dormitory_info.del_flag IS '删除标志 (0:存在, 2:删除)'; + +-- ============================================================================== +-- 步骤 10: 调整 train_class_plan (当前 0 行) +-- ============================================================================== +-- 10.1 彻底剥离旧单课 c_id 和旧单教师 t_id +ALTER TABLE train_class_plan + DROP COLUMN IF EXISTS c_id, + DROP COLUMN IF EXISTS t_id, + DROP COLUMN IF EXISTS create_user; + +-- 10.2 增加报到截止时间与标准审计列 +ALTER TABLE train_class_plan + ADD COLUMN report_deadline TIMESTAMP, + ADD COLUMN create_by VARCHAR(64), + ADD COLUMN update_by VARCHAR(64), + ADD COLUMN del_flag CHAR(1) DEFAULT '0' NOT NULL; + +ALTER TABLE train_class_plan + ALTER COLUMN plan_name TYPE VARCHAR(150), + ALTER COLUMN train_address TYPE VARCHAR(200), + ALTER COLUMN remark TYPE VARCHAR(500), + ALTER COLUMN plan_status SET NOT NULL; + +COMMENT ON COLUMN train_class_plan.report_deadline IS '学员现场报到截止时间'; +COMMENT ON COLUMN train_class_plan.del_flag IS '删除标志 (0:存在, 2:删除)'; + +COMMIT; diff --git a/RuoYi-Vue/sql/train/v2/004_rebuild_business_tables.sql b/RuoYi-Vue/sql/train/v2/004_rebuild_business_tables.sql new file mode 100644 index 000000000..2ea547d98 --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/004_rebuild_business_tables.sql @@ -0,0 +1,215 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 迁移脚本: 004_rebuild_business_tables.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 重构 5 张需彻底解耦旧关系的空业务过程表 +-- 事务边界: 全程在显式事务 BEGIN ... COMMIT 中执行 +-- 设计依据: RailwaySystem 数据库 V2 字段设计与 Kingbase DDL 规范 V1.0 及 Round 1 审查要求 +-- 规范要求: +-- 1. 校验确认各表必须为 0 行方可重建 +-- 2. 显式解除旧依赖,杜绝无边界使用 CASCADE +-- 3. 为 train_study_duration 增加时间有效性与单号/系统匹配 CHECK +-- ============================================================================== + +BEGIN; + +-- 步骤 1: 安全阻断校验 (确保各表确为 0 行,杜绝误删未备份数据) +DO $$ +DECLARE + v_cnt INT; +BEGIN + SELECT count(*) INTO v_cnt FROM train_dorm_auth; + IF v_cnt > 0 THEN RAISE EXCEPTION '>>> [004-FATAL] train_dorm_auth 包含 % 行数据,禁止重建', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_class_report; + IF v_cnt > 0 THEN RAISE EXCEPTION '>>> [004-FATAL] train_class_report 包含 % 行数据,禁止重建', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_student_checkin; + IF v_cnt > 0 THEN RAISE EXCEPTION '>>> [004-FATAL] train_student_checkin 包含 % 行数据,禁止重建', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_study_duration; + IF v_cnt > 0 THEN RAISE EXCEPTION '>>> [004-FATAL] train_study_duration 包含 % 行数据,禁止重建', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM train_student_portrait; + IF v_cnt > 0 THEN RAISE EXCEPTION '>>> [004-FATAL] train_student_portrait 包含 % 行数据,禁止重建', v_cnt; END IF; +END $$; + +-- 步骤 2: 卸载旧触发器 +DROP TRIGGER IF EXISTS trig_train_dormauth_modify ON train_dorm_auth; + +-- 步骤 3: 显式卸载已知旧外键约束 (杜绝使用 CASCADE 掩盖未预期依赖) +ALTER TABLE train_dorm_auth DROP CONSTRAINT IF EXISTS fk_dormauth_stu; +ALTER TABLE train_dorm_auth DROP CONSTRAINT IF EXISTS fk_dormauth_plan; +ALTER TABLE train_dorm_auth DROP CONSTRAINT IF EXISTS fk_dormauth_dorm; + +ALTER TABLE train_class_report DROP CONSTRAINT IF EXISTS fk_classreport_plan; +ALTER TABLE train_class_report DROP CONSTRAINT IF EXISTS fk_classreport_stu; +ALTER TABLE train_class_report DROP CONSTRAINT IF EXISTS fk_classreport_dorm; +ALTER TABLE train_class_report DROP CONSTRAINT IF EXISTS fk_classreport_classroom; +ALTER TABLE train_class_report DROP CONSTRAINT IF EXISTS fk_classreport_site; + +ALTER TABLE train_student_checkin DROP CONSTRAINT IF EXISTS fk_checkin_plan; +ALTER TABLE train_student_checkin DROP CONSTRAINT IF EXISTS fk_checkin_course; +ALTER TABLE train_student_checkin DROP CONSTRAINT IF EXISTS fk_checkin_stu; +ALTER TABLE train_student_checkin DROP CONSTRAINT IF EXISTS fk_checkin_board; + +ALTER TABLE train_study_duration DROP CONSTRAINT IF EXISTS fk_studydur_stu; +ALTER TABLE train_study_duration DROP CONSTRAINT IF EXISTS fk_studydur_plan; +ALTER TABLE train_study_duration DROP CONSTRAINT IF EXISTS fk_studydur_course; + +ALTER TABLE train_student_portrait DROP CONSTRAINT IF EXISTS fk_portrait_stu; +ALTER TABLE train_student_portrait DROP CONSTRAINT IF EXISTS fk_portrait_plan; + +-- 步骤 4: 执行安全常规 DROP TABLE (无 CASCADE) +DROP TABLE IF EXISTS train_dorm_auth; +DROP TABLE IF EXISTS train_class_report; +DROP TABLE IF EXISTS train_student_checkin; +DROP TABLE IF EXISTS train_study_duration; +DROP TABLE IF EXISTS train_student_portrait; + +-- 步骤 5: 按照 V2 规范重建表结构 + +-- 5.1 门禁授权同步表 +CREATE TABLE train_dorm_auth ( + auth_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + accommodation_id BIGINT NOT NULL, + auth_start TIMESTAMP, + auth_end TIMESTAMP, + face_sync_status SMALLINT NOT NULL DEFAULT 0, + retry_count INTEGER NOT NULL DEFAULT 0, + last_error VARCHAR(500), + external_auth_id VARCHAR(100), + last_sync_time TIMESTAMP, + create_by VARCHAR(64), + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_by VARCHAR(64), + update_time TIMESTAMP, + remark VARCHAR(500), + CONSTRAINT pk_train_dorm_auth PRIMARY KEY (auth_id) +); +COMMENT ON TABLE train_dorm_auth IS '宿舍门禁权限下发与同步表'; +COMMENT ON COLUMN train_dorm_auth.auth_id IS '门禁授权记录ID'; +COMMENT ON COLUMN train_dorm_auth.accommodation_id IS '住宿安排ID (一对一关联)'; +COMMENT ON COLUMN train_dorm_auth.auth_start IS '权限生效开始时间'; +COMMENT ON COLUMN train_dorm_auth.auth_end IS '权限失效结束时间'; +COMMENT ON COLUMN train_dorm_auth.face_sync_status IS '同步状态 (0:待下发, 1:已下发, 2:下发失败, 3:已注销)'; +COMMENT ON COLUMN train_dorm_auth.retry_count IS '同步失败重试次数'; +COMMENT ON COLUMN train_dorm_auth.last_error IS '最后一次下发失败错误信息'; +COMMENT ON COLUMN train_dorm_auth.external_auth_id IS '外部门禁硬件系统下发唯一授权凭证ID'; +COMMENT ON COLUMN train_dorm_auth.last_sync_time IS '最近一次同步请求时间'; + +-- 5.2 报到事件记录表 +CREATE TABLE train_class_report ( + report_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + enrollment_id BIGINT NOT NULL, + report_way SMALLINT NOT NULL, + report_device_sn VARCHAR(64), + report_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + verify_result SMALLINT NOT NULL, + verify_note VARCHAR(500), + create_by VARCHAR(64), + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(500), + CONSTRAINT pk_train_class_report PRIMARY KEY (report_id) +); +COMMENT ON TABLE train_class_report IS '培训班学员现场报到事件表'; +COMMENT ON COLUMN train_class_report.report_id IS '报到流水ID'; +COMMENT ON COLUMN train_class_report.enrollment_id IS '参训档案ID (一对一关联)'; +COMMENT ON COLUMN train_class_report.report_way IS '报到核验方式 (1:人脸识别, 2:身份证刷卡, 3:二维码, 4:人工录入)'; +COMMENT ON COLUMN train_class_report.report_device_sn IS '核验设备硬件序列号'; +COMMENT ON COLUMN train_class_report.report_time IS '报到完成时间'; +COMMENT ON COLUMN train_class_report.verify_result IS '核验结果 (1:核验通过, 2:核验不匹配, 3:人工放行)'; +COMMENT ON COLUMN train_class_report.verify_note IS '核验说明/异常原因'; + +-- 5.3 考勤签到明细表 +CREATE TABLE train_student_checkin ( + checkin_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + enrollment_id BIGINT NOT NULL, + lesson_id BIGINT NOT NULL, + board_id VARCHAR(32), + checkin_way SMALLINT NOT NULL, + sign_in_time TIMESTAMP, + sign_out_time TIMESTAMP, + attendance_status SMALLINT NOT NULL DEFAULT 0, + manual_flag SMALLINT NOT NULL DEFAULT 0, + create_by VARCHAR(64), + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_by VARCHAR(64), + update_time TIMESTAMP, + remark VARCHAR(500), + CONSTRAINT pk_train_student_checkin PRIMARY KEY (checkin_id) +); +COMMENT ON TABLE train_student_checkin IS '课次考勤打卡与最终考勤结果表'; +COMMENT ON COLUMN train_student_checkin.checkin_id IS '考勤记录ID'; +COMMENT ON COLUMN train_student_checkin.enrollment_id IS '参训档案ID'; +COMMENT ON COLUMN train_student_checkin.lesson_id IS '具体教学活动课次ID'; +COMMENT ON COLUMN train_student_checkin.board_id IS '打卡电子班牌ID'; +COMMENT ON COLUMN train_student_checkin.checkin_way IS '打卡方式 (1:人脸, 2:刷卡, 3:扫码, 4:教师代签)'; +COMMENT ON COLUMN train_student_checkin.sign_in_time IS '签到时间'; +COMMENT ON COLUMN train_student_checkin.sign_out_time IS '签退时间'; +COMMENT ON COLUMN train_student_checkin.attendance_status IS '考勤结论 (0:缺勤, 1:正常出勤, 2:迟到, 3:早退, 4:请假)'; +COMMENT ON COLUMN train_student_checkin.manual_flag IS '是否人工补录/修正 (0:自动, 1:人工补卡)'; + +-- 5.4 学习时长记录表 (自主/外部系统对接) +CREATE TABLE train_study_duration ( + dur_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + enrollment_id BIGINT NOT NULL, + plan_course_id BIGINT, + resource_id VARCHAR(32), + learning_content VARCHAR(200), + source_system VARCHAR(100), + source_event_id VARCHAR(100), + start_time TIMESTAMP, + end_time TIMESTAMP, + study_seconds INTEGER NOT NULL DEFAULT 0, + sync_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + create_by VARCHAR(64), + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(500), + CONSTRAINT pk_train_study_duration PRIMARY KEY (dur_id), + CONSTRAINT chk_study_seconds CHECK (study_seconds >= 0), + CONSTRAINT chk_study_time CHECK (end_time IS NULL OR start_time IS NULL OR end_time >= start_time), + CONSTRAINT chk_study_source_system CHECK (source_event_id IS NULL OR source_system IS NOT NULL) +); +COMMENT ON TABLE train_study_duration IS '学员自主学习与仿真实训时长上报表'; +COMMENT ON COLUMN train_study_duration.dur_id IS '学习时长记录ID'; +COMMENT ON COLUMN train_study_duration.enrollment_id IS '参训档案ID'; +COMMENT ON COLUMN train_study_duration.plan_course_id IS '关联培训课程ID (可选)'; +COMMENT ON COLUMN train_study_duration.resource_id IS '学习资源ID (可选)'; +COMMENT ON COLUMN train_study_duration.learning_content IS '学习主题/章节内容'; +COMMENT ON COLUMN train_study_duration.source_system IS '上报来源系统 (如 VR仿真系统, 在线学习端)'; +COMMENT ON COLUMN train_study_duration.source_event_id IS '来源系统上报事件ID (用于幂等防重)'; +COMMENT ON COLUMN train_study_duration.start_time IS '学习开始时间'; +COMMENT ON COLUMN train_study_duration.end_time IS '学习结束时间'; +COMMENT ON COLUMN train_study_duration.study_seconds IS '有效学习时长 (秒)'; +COMMENT ON COLUMN train_study_duration.sync_time IS '系统入库时间'; + +-- 5.5 学员参训综合画像表 +CREATE TABLE train_student_portrait ( + portrait_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + enrollment_id BIGINT NOT NULL, + attendance_rate NUMERIC(5,2), + theory_score NUMERIC(7,2), + practical_score NUMERIC(7,2), + total_study_hour NUMERIC(8,2), + label_tag TEXT, + portrait_content TEXT, + generated_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + create_by VARCHAR(64), + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_by VARCHAR(64), + update_time TIMESTAMP, + remark VARCHAR(500), + CONSTRAINT pk_train_student_portrait PRIMARY KEY (portrait_id) +); +COMMENT ON TABLE train_student_portrait IS '学员参训全过程综合评估与画像表'; +COMMENT ON COLUMN train_student_portrait.portrait_id IS '画像记录ID'; +COMMENT ON COLUMN train_student_portrait.enrollment_id IS '参训档案ID (单期唯一画像)'; +COMMENT ON COLUMN train_student_portrait.attendance_rate IS '出勤率百分比 (0~100)'; +COMMENT ON COLUMN train_student_portrait.theory_score IS '理论考试综合分'; +COMMENT ON COLUMN train_student_portrait.practical_score IS '实操演练综合分'; +COMMENT ON COLUMN train_student_portrait.total_study_hour IS '累计在线与自主学习总学时'; +COMMENT ON COLUMN train_student_portrait.label_tag IS '特征标签集合 (JSON或逗号分隔文本)'; +COMMENT ON COLUMN train_student_portrait.portrait_content IS '综合评价文本/导师评语'; +COMMENT ON COLUMN train_student_portrait.generated_time IS '画像生成时间'; + +COMMIT; diff --git a/RuoYi-Vue/sql/train/v2/005_constraints.sql b/RuoYi-Vue/sql/train/v2/005_constraints.sql new file mode 100644 index 000000000..f49ddd6a1 --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/005_constraints.sql @@ -0,0 +1,245 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 迁移脚本: 005_constraints.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 集中创建外键约束 (FK)、唯一约束 (UNIQUE) 与单行一致性检查约束 (CHECK) +-- 事务边界: 全程在显式事务 BEGIN ... COMMIT 中执行 +-- 设计依据: RailwaySystem 数据库 V2 字段设计与 Kingbase DDL 规范 V1.0 及 Round 1 审查要求 +-- 核心原则: +-- 1. 所有外键严格使用 ON DELETE NO ACTION,严禁使用 CASCADE +-- 2. 唯一约束保护核心领域关系防重 +-- 3. 检查约束全面覆盖单行时间、场地互斥、分值非负、容量合法及系统单号联动逻辑 +-- ============================================================================== + +BEGIN; + +-- ============================================================================== +-- 一、唯一约束 (UNIQUE CONSTRAINTS - 共 13 组) +-- ============================================================================== + +-- 1. 学员身份证哈希唯一 (允许 NULL,防重) +ALTER TABLE train_student DROP CONSTRAINT IF EXISTS uq_student_idcard_hash; +ALTER TABLE train_student ADD CONSTRAINT uq_student_idcard_hash UNIQUE (id_card_hash); + +-- 2. 课程编码唯一 (允许 NULL) +ALTER TABLE train_course DROP CONSTRAINT IF EXISTS uq_course_code; +ALTER TABLE train_course ADD CONSTRAINT uq_course_code UNIQUE (c_code); + +-- 3. 电子班牌硬件序列号全局唯一 +ALTER TABLE train_electronic_board DROP CONSTRAINT IF EXISTS uq_board_sn; +ALTER TABLE train_electronic_board ADD CONSTRAINT uq_board_sn UNIQUE (board_sn); + +-- 4. 宿舍房间唯一 (楼栋 + 房号) +ALTER TABLE train_dormitory_info DROP CONSTRAINT IF EXISTS uq_dorm_building_room; +ALTER TABLE train_dormitory_info ADD CONSTRAINT uq_dorm_building_room UNIQUE (dorm_building, dorm_room); + +-- 5. 课程与教学资源绑定唯一 +ALTER TABLE train_course_resource DROP CONSTRAINT IF EXISTS uq_course_resource; +ALTER TABLE train_course_resource ADD CONSTRAINT uq_course_resource UNIQUE (c_id, resource_id); + +-- 6. 培训班与课程关联唯一 (一个班不能重复加入同一门课) +ALTER TABLE train_plan_course DROP CONSTRAINT IF EXISTS uq_plan_course; +ALTER TABLE train_plan_course ADD CONSTRAINT uq_plan_course UNIQUE (plan_id, c_id); + +-- 7. 参训学员档案唯一 (同一学员不能重复加入同一培训班) +ALTER TABLE train_enrollment DROP CONSTRAINT IF EXISTS uq_enrollment; +ALTER TABLE train_enrollment ADD CONSTRAINT uq_enrollment UNIQUE (plan_id, student_id); + +-- 8. 住宿安排唯一 (一次参训保留一个有效住宿记录) +ALTER TABLE train_accommodation DROP CONSTRAINT IF EXISTS uq_accommodation_enroll; +ALTER TABLE train_accommodation ADD CONSTRAINT uq_accommodation_enroll UNIQUE (enrollment_id); + +-- 9. 门禁授权唯一 (一次住宿安排对应一条门禁同步主记录) +ALTER TABLE train_dorm_auth DROP CONSTRAINT IF EXISTS uq_dorm_auth_accomm; +ALTER TABLE train_dorm_auth ADD CONSTRAINT uq_dorm_auth_accomm UNIQUE (accommodation_id); + +-- 10. 报到记录唯一 (一次参训对应一次报到结果) +ALTER TABLE train_class_report DROP CONSTRAINT IF EXISTS uq_class_report_enroll; +ALTER TABLE train_class_report ADD CONSTRAINT uq_class_report_enroll UNIQUE (enrollment_id); + +-- 11. 课次考勤打卡唯一 (一个学员在一堂课只有一条考勤主结果) +ALTER TABLE train_student_checkin DROP CONSTRAINT IF EXISTS uq_student_checkin_enroll_lesson; +ALTER TABLE train_student_checkin ADD CONSTRAINT uq_student_checkin_enroll_lesson UNIQUE (enrollment_id, lesson_id); + +-- 12. 学员考核项成绩唯一 (一个学员在某考核项只有一条最终得分) +ALTER TABLE train_student_score DROP CONSTRAINT IF EXISTS uq_student_score_enroll_item; +ALTER TABLE train_student_score ADD CONSTRAINT uq_student_score_enroll_item UNIQUE (enrollment_id, score_item_id); + +-- 13. 学员参训综合画像唯一 (一次参训生成一份画像) +ALTER TABLE train_student_portrait DROP CONSTRAINT IF EXISTS uq_student_portrait_enroll; +ALTER TABLE train_student_portrait ADD CONSTRAINT uq_student_portrait_enroll UNIQUE (enrollment_id); + + +-- ============================================================================== +-- 二、外键约束 (FOREIGN KEY CONSTRAINTS - 共 26 组,全部 ON DELETE NO ACTION) +-- ============================================================================== + +-- 1. train_course_resource (2 FK) +ALTER TABLE train_course_resource DROP CONSTRAINT IF EXISTS fk_cr_course; +ALTER TABLE train_course_resource ADD CONSTRAINT fk_cr_course + FOREIGN KEY (c_id) REFERENCES train_course (c_id) ON DELETE NO ACTION; + +ALTER TABLE train_course_resource DROP CONSTRAINT IF EXISTS fk_cr_resource; +ALTER TABLE train_course_resource ADD CONSTRAINT fk_cr_resource + FOREIGN KEY (resource_id) REFERENCES train_resource (resource_id) ON DELETE NO ACTION; + +-- 2. train_electronic_board (1 FK) +ALTER TABLE train_electronic_board DROP CONSTRAINT IF EXISTS fk_board_classroom; +ALTER TABLE train_electronic_board ADD CONSTRAINT fk_board_classroom + FOREIGN KEY (bind_classroom_id) REFERENCES train_classroom_info (class_room_id) ON DELETE NO ACTION; + +-- 3. train_plan_course (2 FK) +ALTER TABLE train_plan_course DROP CONSTRAINT IF EXISTS fk_plancourse_plan; +ALTER TABLE train_plan_course ADD CONSTRAINT fk_plancourse_plan + FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION; + +ALTER TABLE train_plan_course DROP CONSTRAINT IF EXISTS fk_plancourse_course; +ALTER TABLE train_plan_course ADD CONSTRAINT fk_plancourse_course + FOREIGN KEY (c_id) REFERENCES train_course (c_id) ON DELETE NO ACTION; + +-- 4. train_enrollment (2 FK) +ALTER TABLE train_enrollment DROP CONSTRAINT IF EXISTS fk_enrollment_plan; +ALTER TABLE train_enrollment ADD CONSTRAINT fk_enrollment_plan + FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION; + +ALTER TABLE train_enrollment DROP CONSTRAINT IF EXISTS fk_enrollment_student; +ALTER TABLE train_enrollment ADD CONSTRAINT fk_enrollment_student + FOREIGN KEY (student_id) REFERENCES train_student (s_id) ON DELETE NO ACTION; + +-- 5. train_lesson (4 FK) +ALTER TABLE train_lesson DROP CONSTRAINT IF EXISTS fk_lesson_plancourse; +ALTER TABLE train_lesson ADD CONSTRAINT fk_lesson_plancourse + FOREIGN KEY (plan_course_id) REFERENCES train_plan_course (plan_course_id) ON DELETE NO ACTION; + +ALTER TABLE train_lesson DROP CONSTRAINT IF EXISTS fk_lesson_teacher; +ALTER TABLE train_lesson ADD CONSTRAINT fk_lesson_teacher + FOREIGN KEY (t_id) REFERENCES train_teacher (t_id) ON DELETE NO ACTION; + +ALTER TABLE train_lesson DROP CONSTRAINT IF EXISTS fk_lesson_classroom; +ALTER TABLE train_lesson ADD CONSTRAINT fk_lesson_classroom + FOREIGN KEY (class_room_id) REFERENCES train_classroom_info (class_room_id) ON DELETE NO ACTION; + +ALTER TABLE train_lesson DROP CONSTRAINT IF EXISTS fk_lesson_site; +ALTER TABLE train_lesson ADD CONSTRAINT fk_lesson_site + FOREIGN KEY (site_id) REFERENCES train_training_site (site_id) ON DELETE NO ACTION; + +-- 6. train_accommodation (2 FK) +ALTER TABLE train_accommodation DROP CONSTRAINT IF EXISTS fk_accomm_enrollment; +ALTER TABLE train_accommodation ADD CONSTRAINT fk_accomm_enrollment + FOREIGN KEY (enrollment_id) REFERENCES train_enrollment (enrollment_id) ON DELETE NO ACTION; + +ALTER TABLE train_accommodation DROP CONSTRAINT IF EXISTS fk_accomm_dorm; +ALTER TABLE train_accommodation ADD CONSTRAINT fk_accomm_dorm + FOREIGN KEY (dorm_id) REFERENCES train_dormitory_info (dorm_id) ON DELETE NO ACTION; + +-- 7. train_dorm_auth (1 FK) +ALTER TABLE train_dorm_auth DROP CONSTRAINT IF EXISTS fk_dormauth_accomm; +ALTER TABLE train_dorm_auth ADD CONSTRAINT fk_dormauth_accomm + FOREIGN KEY (accommodation_id) REFERENCES train_accommodation (accommodation_id) ON DELETE NO ACTION; + +-- 8. train_class_report (1 FK) +ALTER TABLE train_class_report DROP CONSTRAINT IF EXISTS fk_report_enrollment; +ALTER TABLE train_class_report ADD CONSTRAINT fk_report_enrollment + FOREIGN KEY (enrollment_id) REFERENCES train_enrollment (enrollment_id) ON DELETE NO ACTION; + +-- 9. train_student_checkin (3 FK) +ALTER TABLE train_student_checkin DROP CONSTRAINT IF EXISTS fk_checkin_enrollment; +ALTER TABLE train_student_checkin ADD CONSTRAINT fk_checkin_enrollment + FOREIGN KEY (enrollment_id) REFERENCES train_enrollment (enrollment_id) ON DELETE NO ACTION; + +ALTER TABLE train_student_checkin DROP CONSTRAINT IF EXISTS fk_checkin_lesson; +ALTER TABLE train_student_checkin ADD CONSTRAINT fk_checkin_lesson + FOREIGN KEY (lesson_id) REFERENCES train_lesson (lesson_id) ON DELETE NO ACTION; + +ALTER TABLE train_student_checkin DROP CONSTRAINT IF EXISTS fk_checkin_board; +ALTER TABLE train_student_checkin ADD CONSTRAINT fk_checkin_board + FOREIGN KEY (board_id) REFERENCES train_electronic_board (board_id) ON DELETE NO ACTION; + +-- 10. train_score_item (2 FK) +ALTER TABLE train_score_item DROP CONSTRAINT IF EXISTS fk_scoreitem_plan; +ALTER TABLE train_score_item ADD CONSTRAINT fk_scoreitem_plan + FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION; + +ALTER TABLE train_score_item DROP CONSTRAINT IF EXISTS fk_scoreitem_plancourse; +ALTER TABLE train_score_item ADD CONSTRAINT fk_scoreitem_plancourse + FOREIGN KEY (plan_course_id) REFERENCES train_plan_course (plan_course_id) ON DELETE NO ACTION; + +-- 11. train_student_score (2 FK) +ALTER TABLE train_student_score DROP CONSTRAINT IF EXISTS fk_score_enrollment; +ALTER TABLE train_student_score ADD CONSTRAINT fk_score_enrollment + FOREIGN KEY (enrollment_id) REFERENCES train_enrollment (enrollment_id) ON DELETE NO ACTION; + +ALTER TABLE train_student_score DROP CONSTRAINT IF EXISTS fk_score_item; +ALTER TABLE train_student_score ADD CONSTRAINT fk_score_item + FOREIGN KEY (score_item_id) REFERENCES train_score_item (score_item_id) ON DELETE NO ACTION; + +-- 12. train_study_duration (3 FK) +ALTER TABLE train_study_duration DROP CONSTRAINT IF EXISTS fk_study_enrollment; +ALTER TABLE train_study_duration ADD CONSTRAINT fk_study_enrollment + FOREIGN KEY (enrollment_id) REFERENCES train_enrollment (enrollment_id) ON DELETE NO ACTION; + +ALTER TABLE train_study_duration DROP CONSTRAINT IF EXISTS fk_study_plancourse; +ALTER TABLE train_study_duration ADD CONSTRAINT fk_study_plancourse + FOREIGN KEY (plan_course_id) REFERENCES train_plan_course (plan_course_id) ON DELETE NO ACTION; + +ALTER TABLE train_study_duration DROP CONSTRAINT IF EXISTS fk_study_resource; +ALTER TABLE train_study_duration ADD CONSTRAINT fk_study_resource + FOREIGN KEY (resource_id) REFERENCES train_resource (resource_id) ON DELETE NO ACTION; + +-- 13. train_student_portrait (1 FK) +ALTER TABLE train_student_portrait DROP CONSTRAINT IF EXISTS fk_portrait_enrollment; +ALTER TABLE train_student_portrait ADD CONSTRAINT fk_portrait_enrollment + FOREIGN KEY (enrollment_id) REFERENCES train_enrollment (enrollment_id) ON DELETE NO ACTION; + + +-- ============================================================================== +-- 三、单行检查约束 (CHECK CONSTRAINTS) +-- ============================================================================== + +-- 1. 排课时间逻辑检查: 开始时间必须早于结束时间 +ALTER TABLE train_lesson DROP CONSTRAINT IF EXISTS chk_lesson_time; +ALTER TABLE train_lesson ADD CONSTRAINT chk_lesson_time + CHECK (start_time < end_time); + +-- 2. 排课场地类型单行互斥约束 +ALTER TABLE train_lesson DROP CONSTRAINT IF EXISTS chk_lesson_venue; +ALTER TABLE train_lesson ADD CONSTRAINT chk_lesson_venue CHECK ( + (venue_type = 1 AND class_room_id IS NOT NULL AND site_id IS NULL) OR + (venue_type = 2 AND site_id IS NOT NULL AND class_room_id IS NULL) +); + +-- 3. 成绩项目单项权重范围: 0 ~ 100 +ALTER TABLE train_score_item DROP CONSTRAINT IF EXISTS chk_score_item_weight; +ALTER TABLE train_score_item ADD CONSTRAINT chk_score_item_weight + CHECK (weight >= 0.00 AND weight <= 100.00); + +-- 4. 成绩项目满分设定: 必须大于 0 +ALTER TABLE train_score_item DROP CONSTRAINT IF EXISTS chk_score_item_full; +ALTER TABLE train_score_item ADD CONSTRAINT chk_score_item_full + CHECK (full_score > 0.00); + +-- 5. 学员得分非负约束 (未录成绩不建行,录入则非负) +ALTER TABLE train_student_score DROP CONSTRAINT IF EXISTS chk_student_score_nonneg; +ALTER TABLE train_student_score ADD CONSTRAINT chk_student_score_nonneg + CHECK (score >= 0.00); + +-- 6. 学习时长非负检查 +ALTER TABLE train_study_duration DROP CONSTRAINT IF EXISTS chk_study_seconds; +ALTER TABLE train_study_duration ADD CONSTRAINT chk_study_seconds + CHECK (study_seconds >= 0); + +-- 7. 学习记录时间逻辑检查 +ALTER TABLE train_study_duration DROP CONSTRAINT IF EXISTS chk_study_time; +ALTER TABLE train_study_duration ADD CONSTRAINT chk_study_time + CHECK (end_time IS NULL OR start_time IS NULL OR end_time >= start_time); + +-- 8. 学习记录外部单号与来源系统一致性检查 +ALTER TABLE train_study_duration DROP CONSTRAINT IF EXISTS chk_study_source_system; +ALTER TABLE train_study_duration ADD CONSTRAINT chk_study_source_system + CHECK (source_event_id IS NULL OR source_system IS NOT NULL); + +-- 9. 宿舍核定容量正数检查 +ALTER TABLE train_dormitory_info DROP CONSTRAINT IF EXISTS chk_dorm_capacity; +ALTER TABLE train_dormitory_info ADD CONSTRAINT chk_dorm_capacity + CHECK (capacity > 0); + +COMMIT; diff --git a/RuoYi-Vue/sql/train/v2/006_indexes.sql b/RuoYi-Vue/sql/train/v2/006_indexes.sql new file mode 100644 index 000000000..06dce72ae --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/006_indexes.sql @@ -0,0 +1,82 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 迁移脚本: 006_indexes.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 创建业务核心查询索引、外键索引与部分唯一索引 +-- 事务边界: 全程在显式事务 BEGIN ... COMMIT 中执行 +-- 设计依据: RailwaySystem 数据库 V2 字段设计与 Kingbase DDL 规范 V1.0 及 Round 1 审查要求 +-- 核心原则: +-- 1. 主键与普通 UNIQUE 自动建立的唯一索引不重复创建 +-- 2. 为所有外键关联列建立独立索引,杜绝全表扫描与锁表 +-- 3. 排课多维冲突检测建立针对性复合时间索引 +-- 4. 学习记录来源事件使用双非空过滤的部分唯一索引防逃逸 +-- ============================================================================== + +BEGIN; + +-- 1. train_teacher 教师表索引 +CREATE INDEX idx_teacher_user_id ON train_teacher (user_id); +CREATE INDEX idx_teacher_name ON train_teacher (t_name); + +-- 2. train_student 学员表索引 +CREATE INDEX idx_student_name ON train_student (s_name); +CREATE INDEX idx_student_org ON train_student (org_name); +CREATE INDEX idx_student_user_id ON train_student (user_id); + +-- 3. train_course 课程表索引 +CREATE INDEX idx_course_name ON train_course (c_name); +CREATE INDEX idx_course_status ON train_course (status); + +-- 4. train_resource 教学资源表索引 +CREATE INDEX idx_resource_type ON train_resource (resource_type); +CREATE INDEX idx_resource_status ON train_resource (status); + +-- 5. train_course_resource 课程资源关联表索引 +CREATE INDEX idx_course_resource_course ON train_course_resource (c_id); +CREATE INDEX idx_course_resource_resource ON train_course_resource (resource_id); + +-- 6. train_electronic_board 电子班牌外键索引 +CREATE INDEX idx_board_classroom ON train_electronic_board (bind_classroom_id); + +-- 7. train_plan_course 培训班课程表索引 +CREATE INDEX idx_plan_course_plan ON train_plan_course (plan_id); +CREATE INDEX idx_plan_course_course ON train_plan_course (c_id); + +-- 8. train_enrollment 参训学员表索引 +CREATE INDEX idx_enrollment_plan ON train_enrollment (plan_id); +CREATE INDEX idx_enrollment_student ON train_enrollment (student_id); +CREATE INDEX idx_enrollment_status ON train_enrollment (enrollment_status); + +-- 9. train_lesson 排课表多维时间冲突检索复合索引 +CREATE INDEX idx_lesson_plan_course ON train_lesson (plan_course_id); +CREATE INDEX idx_lesson_teacher_time ON train_lesson (t_id, start_time, end_time); +CREATE INDEX idx_lesson_classroom_time ON train_lesson (class_room_id, start_time, end_time); +CREATE INDEX idx_lesson_site_time ON train_lesson (site_id, start_time, end_time); + +-- 10. train_accommodation 住宿安排表索引 +CREATE INDEX idx_accommodation_dorm ON train_accommodation (dorm_id); + +-- 11. train_student_checkin 考勤打卡表索引 +CREATE INDEX idx_checkin_lesson ON train_student_checkin (lesson_id); +CREATE INDEX idx_checkin_enrollment ON train_student_checkin (enrollment_id); +CREATE INDEX idx_checkin_board ON train_student_checkin (board_id); + +-- 12. train_score_item 成绩项配置表索引 +CREATE INDEX idx_score_item_plan ON train_score_item (plan_id); +CREATE INDEX idx_score_item_plan_course ON train_score_item (plan_course_id); + +-- 13. train_student_score 学员成绩明细表索引 +CREATE INDEX idx_student_score_enrollment ON train_student_score (enrollment_id); +CREATE INDEX idx_student_score_item ON train_student_score (score_item_id); + +-- 14. train_study_duration 学习时长表索引 +CREATE INDEX idx_study_enrollment ON train_study_duration (enrollment_id); +CREATE INDEX idx_study_plan_course ON train_study_duration (plan_course_id); +CREATE INDEX idx_study_resource ON train_study_duration (resource_id); +CREATE INDEX idx_study_sync ON train_study_duration (sync_time); + +-- 15. train_study_duration 幂等防重部分唯一索引 (必须 source_system 与 source_event_id 均非空方防重) +CREATE UNIQUE INDEX uq_idx_study_source_event + ON train_study_duration (source_system, source_event_id) + WHERE source_system IS NOT NULL AND source_event_id IS NOT NULL; + +COMMIT; diff --git a/RuoYi-Vue/sql/train/v2/007_dicts.sql b/RuoYi-Vue/sql/train/v2/007_dicts.sql new file mode 100644 index 000000000..6c3699fb0 --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/007_dicts.sql @@ -0,0 +1,335 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 业务字典脚本: 007_dicts.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 准备 RailwaySystem 一期 16 组培训业务字典类型与字典数据 +-- 设计依据: RailwaySystem 数据库 V2 字段设计与 Kingbase DDL 规范 V1.0 及 Round 1 审查要求 +-- 核心警示: +-- 1. 【本文件在当前数据库 V2 结构迁移阶段暂不执行!】留待后续 API/权限设计阶段统一接入。 +-- 2. 本文件不硬编码 dict_id / dict_code 数字主键,避免与已有 500+ 条系统字典数据冲突。 +-- 3. 采用 NOT EXISTS 幂等判断,保证即使重复执行也不会重复插入相同字典项。 +-- ============================================================================== + +BEGIN; + +-- ============================================================================== +-- 一、注册 16 组业务字典类型 (sys_dict_type) +-- ============================================================================== + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '培训系统通用状态', 'train_common_status', '0', 'admin', CURRENT_TIMESTAMP, '通用启用/停用状态 (1:正常, 0:停用)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_common_status'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '开班计划状态', 'train_plan_status', '0', 'admin', CURRENT_TIMESTAMP, '培训班全生命周期状态 (0:未开始, 1:进行中, 2:已结束, 3:已取消)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_plan_status'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '学员参训状态', 'train_enrollment_status', '0', 'admin', CURRENT_TIMESTAMP, '学员参训全过程状态 (0:待报到, 1:已报到, 2:请假, 3:放弃, 4:已完成)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_enrollment_status'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '学员主档来源类型', 'train_student_source', '0', 'admin', CURRENT_TIMESTAMP, '人员主档进入系统的来源 (1:管理员录入, 2:Excel批量导入, 3:铁路内部业务平台同步)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_student_source'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '教学资源类型', 'train_resource_type', '0', 'admin', CURRENT_TIMESTAMP, '资源载体分类 (1:文档, 2:视频, 3:3D模型, 4:VR资源, 5:外部教学链接, 6:其他)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_resource_type'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '教室状态', 'train_room_status', '0', 'admin', CURRENT_TIMESTAMP, '教室使用状态 (1:可用, 0:维护中)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_room_status'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '宿舍房间状态', 'train_dorm_status', '0', 'admin', CURRENT_TIMESTAMP, '宿舍可用状态 (1:可用, 0:维修中)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_dorm_status'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '宿舍适用性别', 'train_gender_type', '0', 'admin', CURRENT_TIMESTAMP, '宿舍男女分配属性 (0:不限, 1:男, 2:女)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_gender_type'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '住宿安排状态', 'train_accommodation_status', '0', 'admin', CURRENT_TIMESTAMP, '住宿分配状态 (0:预分配, 1:已入住, 2:已退宿)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_accommodation_status'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '门禁授权同步状态', 'train_auth_sync_status', '0', 'admin', CURRENT_TIMESTAMP, '外部硬件权限下发状态 (0:待同步, 1:已下发, 2:下发失败, 3:已注销)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_auth_sync_status'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '报到核验方式', 'train_report_way', '0', 'admin', CURRENT_TIMESTAMP, '现场报到凭据方式 (1:人脸识别, 2:身份证刷卡, 3:二维码, 4:人工确认)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_report_way'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '报到核验结果', 'train_verify_result', '0', 'admin', CURRENT_TIMESTAMP, '报到比对判定结论 (1:通过, 2:失败, 3:人工放行)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_verify_result'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '考勤打卡方式', 'train_checkin_way', '0', 'admin', CURRENT_TIMESTAMP, '课次签到签退打卡媒介 (1:人脸识别, 2:刷卡, 3:扫码, 4:教师代签)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_checkin_way'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '课次考勤结论', 'train_attendance_status', '0', 'admin', CURRENT_TIMESTAMP, '考勤状态 (0:缺勤, 1:正常出勤, 2:迟到, 3:早退, 4:请假)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_attendance_status'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '成绩考核项类型', 'train_score_item_type', '0', 'admin', CURRENT_TIMESTAMP, '成绩组成项类型 (1:理论考试, 2:实操演练, 3:出勤考核, 4:综合评价)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_score_item_type'); + +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT '成绩录入来源', 'train_score_source', '0', 'admin', CURRENT_TIMESTAMP, '成绩产生源头 (1:人工录入, 2:理论考试系统, 3:实操设备, 4:外部导入)' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_score_source'); + + +-- ============================================================================== +-- 二、注册各字典项数据 (sys_dict_data) +-- ============================================================================== + +-- 1. train_common_status +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '正常', '1', 'train_common_status', 'primary', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '正常启用' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_common_status' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '停用', '0', 'train_common_status', 'danger', 'N', '0', 'admin', CURRENT_TIMESTAMP, '停用不可用' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_common_status' AND dict_value = '0'); + +-- 2. train_plan_status +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '未开始', '0', 'train_plan_status', 'info', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '班级已建立,未到开班时间' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_plan_status' AND dict_value = '0'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '进行中', '1', 'train_plan_status', 'primary', 'N', '0', 'admin', CURRENT_TIMESTAMP, '培训班正在开展' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_plan_status' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 3, '已结束', '2', 'train_plan_status', 'success', 'N', '0', 'admin', CURRENT_TIMESTAMP, '培训完成已结业' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_plan_status' AND dict_value = '2'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 4, '已取消', '3', 'train_plan_status', 'danger', 'N', '0', 'admin', CURRENT_TIMESTAMP, '班级已撤销' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_plan_status' AND dict_value = '3'); + +-- 3. train_enrollment_status +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '待报到', '0', 'train_enrollment_status', 'info', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '已入班名单,待现场核验报到' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_enrollment_status' AND dict_value = '0'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '已报到', '1', 'train_enrollment_status', 'primary', 'N', '0', 'admin', CURRENT_TIMESTAMP, '已到场核验完成' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_enrollment_status' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 3, '请假', '2', 'train_enrollment_status', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '全程请假或特殊事假' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_enrollment_status' AND dict_value = '2'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 4, '放弃', '3', 'train_enrollment_status', 'danger', 'N', '0', 'admin', CURRENT_TIMESTAMP, '未到或中途主动退训' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_enrollment_status' AND dict_value = '3'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 5, '已完成', '4', 'train_enrollment_status', 'success', 'N', '0', 'admin', CURRENT_TIMESTAMP, '参训合格并顺利结业' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_enrollment_status' AND dict_value = '4'); + +-- 4. train_student_source (人员主档来源) +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '管理员录入', '1', 'train_student_source', 'primary', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '管理员单条或批量手工建立主档' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_student_source' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, 'Excel批量导入', '2', 'train_student_source', 'info', 'N', '0', 'admin', CURRENT_TIMESTAMP, '线下花名册表格批量导入' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_student_source' AND dict_value = '2'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 3, '铁路内部业务平台同步', '3', 'train_student_source', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '从国铁/路局内部HR或教务系统自动下发' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_student_source' AND dict_value = '3'); + +-- 5. train_resource_type (教学资源类型: 严格对齐 V2 冻结稿) +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '文档', '1', 'train_resource_type', 'primary', 'Y', '0', 'admin', CURRENT_TIMESTAMP, 'PDF/Word/PPT等电子课件' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_resource_type' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '视频', '2', 'train_resource_type', 'info', 'N', '0', 'admin', CURRENT_TIMESTAMP, 'MP4录播视频' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_resource_type' AND dict_value = '2'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 3, '3D模型', '3', 'train_resource_type', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '3D数字化实物机车/工务模型' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_resource_type' AND dict_value = '3'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 4, 'VR资源', '4', 'train_resource_type', 'success', 'N', '0', 'admin', CURRENT_TIMESTAMP, '虚拟仿真头盔或交互式实训课件' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_resource_type' AND dict_value = '4'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 5, '外部教学链接', '5', 'train_resource_type', 'danger', 'N', '0', 'admin', CURRENT_TIMESTAMP, '第三方外部实训/考试网址URL' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_resource_type' AND dict_value = '5'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 6, '其他', '6', 'train_resource_type', 'default', 'N', '0', 'admin', CURRENT_TIMESTAMP, '其他类型教学资料' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_resource_type' AND dict_value = '6'); + +-- 6. train_room_status +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '正常可用', '1', 'train_room_status', 'primary', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '教室可正常排课' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_room_status' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '维护保养中', '0', 'train_room_status', 'danger', 'N', '0', 'admin', CURRENT_TIMESTAMP, '检修维护,暂不可排课' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_room_status' AND dict_value = '0'); + +-- 7. train_dorm_status +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '正常可用', '1', 'train_dorm_status', 'primary', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '房间正常分配' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_dorm_status' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '维修停用', '0', 'train_dorm_status', 'danger', 'N', '0', 'admin', CURRENT_TIMESTAMP, '房间设施报修中' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_dorm_status' AND dict_value = '0'); + +-- 8. train_gender_type +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '不限', '0', 'train_gender_type', 'info', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '混合或不限' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_gender_type' AND dict_value = '0'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '男', '1', 'train_gender_type', 'primary', 'N', '0', 'admin', CURRENT_TIMESTAMP, '男生宿舍' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_gender_type' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 3, '女', '2', 'train_gender_type', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '女生宿舍' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_gender_type' AND dict_value = '2'); + +-- 9. train_accommodation_status +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '预分配', '0', 'train_accommodation_status', 'info', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '报到前系统预先分配' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_accommodation_status' AND dict_value = '0'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '已入住', '1', 'train_accommodation_status', 'primary', 'N', '0', 'admin', CURRENT_TIMESTAMP, '报到确认并已入住' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_accommodation_status' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 3, '已退宿', '2', 'train_accommodation_status', 'success', 'N', '0', 'admin', CURRENT_TIMESTAMP, '培训结束已离宿' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_accommodation_status' AND dict_value = '2'); + +-- 10. train_auth_sync_status +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '待下发', '0', 'train_auth_sync_status', 'info', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '未与外部门禁系统握手' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_auth_sync_status' AND dict_value = '0'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '已下发', '1', 'train_auth_sync_status', 'success', 'N', '0', 'admin', CURRENT_TIMESTAMP, '门禁硬件下发生效' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_auth_sync_status' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 3, '下发失败', '2', 'train_auth_sync_status', 'danger', 'N', '0', 'admin', CURRENT_TIMESTAMP, '网络超时或凭据错误' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_auth_sync_status' AND dict_value = '2'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 4, '已注销', '3', 'train_auth_sync_status', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '已退宿并回收门禁权限' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_auth_sync_status' AND dict_value = '3'); + +-- 11. train_report_way +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '人脸识别', '1', 'train_report_way', 'primary', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '通过人脸比对报到' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_report_way' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '身份证刷卡', '2', 'train_report_way', 'info', 'N', '0', 'admin', CURRENT_TIMESTAMP, '二代身份证读卡器' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_report_way' AND dict_value = '2'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 3, '手机扫码', '3', 'train_report_way', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '扫班级签到二维码' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_report_way' AND dict_value = '3'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 4, '人工确认', '4', 'train_report_way', 'success', 'N', '0', 'admin', CURRENT_TIMESTAMP, '班主任后台手动报到' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_report_way' AND dict_value = '4'); + +-- 12. train_verify_result +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '通过', '1', 'train_verify_result', 'success', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '身份信息核验一致' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_verify_result' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '失败', '2', 'train_verify_result', 'danger', 'N', '0', 'admin', CURRENT_TIMESTAMP, '比对不符或未在名单' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_verify_result' AND dict_value = '2'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 3, '人工放行', '3', 'train_verify_result', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '现场特殊人工核验放行' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_verify_result' AND dict_value = '3'); + +-- 13. train_checkin_way +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '班牌人脸识别', '1', 'train_checkin_way', 'primary', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '教室门口电子班牌刷脸' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_checkin_way' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '班牌刷卡', '2', 'train_checkin_way', 'info', 'N', '0', 'admin', CURRENT_TIMESTAMP, '学员卡刷班牌打卡' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_checkin_way' AND dict_value = '2'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 3, '手机扫码打卡', '3', 'train_checkin_way', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '扫班牌动态二维码' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_checkin_way' AND dict_value = '3'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 4, '教师代签/补卡', '4', 'train_checkin_way', 'success', 'N', '0', 'admin', CURRENT_TIMESTAMP, '授课老师端人工代签' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_checkin_way' AND dict_value = '4'); + +-- 14. train_attendance_status +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '缺勤', '0', 'train_attendance_status', 'danger', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '课次无打卡记录' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_attendance_status' AND dict_value = '0'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '正常出勤', '1', 'train_attendance_status', 'primary', 'N', '0', 'admin', CURRENT_TIMESTAMP, '准时签到且准时签退' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_attendance_status' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 3, '迟到', '2', 'train_attendance_status', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '超过上课设定宽限时间' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_attendance_status' AND dict_value = '2'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 4, '早退', '3', 'train_attendance_status', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '下课前提早离开' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_attendance_status' AND dict_value = '3'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 5, '请假', '4', 'train_attendance_status', 'info', 'N', '0', 'admin', CURRENT_TIMESTAMP, '当堂课次有效请假' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_attendance_status' AND dict_value = '4'); + +-- 15. train_score_item_type +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '理论考试', '1', 'train_score_item_type', 'primary', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '理论闭卷/开卷机考' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_score_item_type' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '实操演练', '2', 'train_score_item_type', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '现场设备实操/VR仿真测评' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_score_item_type' AND dict_value = '2'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 3, '出勤考核', '3', 'train_score_item_type', 'info', 'N', '0', 'admin', CURRENT_TIMESTAMP, '全期考勤率折算成绩' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_score_item_type' AND dict_value = '3'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 4, '综合评价', '4', 'train_score_item_type', 'success', 'N', '0', 'admin', CURRENT_TIMESTAMP, '教师日常考评或答辩' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_score_item_type' AND dict_value = '4'); + +-- 16. train_score_source +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, '人工录入', '1', 'train_score_source', 'primary', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '管理员或教师手工登记' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_score_source' AND dict_value = '1'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, '理论机考对接', '2', 'train_score_source', 'info', 'N', '0', 'admin', CURRENT_TIMESTAMP, '从在线考试平台接口抓取' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_score_source' AND dict_value = '2'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 3, '实操设备回传', '3', 'train_score_source', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '从VR/实训台硬件自动计算上报' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_score_source' AND dict_value = '3'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) +SELECT 4, 'Excel批量导入', '4', 'train_score_source', 'success', 'N', '0', 'admin', CURRENT_TIMESTAMP, '线下成绩表格批量导入' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_score_source' AND dict_value = '4'); + +COMMIT; diff --git a/RuoYi-Vue/sql/train/v2/008_migrate_teacher.sql b/RuoYi-Vue/sql/train/v2/008_migrate_teacher.sql new file mode 100644 index 000000000..2bc2ed61d --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/008_migrate_teacher.sql @@ -0,0 +1,57 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 迁移脚本: 008_migrate_teacher.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 迁移并保留 train_teacher 唯一样本数据,完成旧列向新列平滑过渡并安全清理 +-- 事务边界: 全程在显式事务 BEGIN ... COMMIT 中执行 +-- 设计依据: RailwaySystem 数据库 V2 字段设计与 Kingbase DDL 规范 V1.0 及 Round 1 审查要求 +-- 安全规范: +-- 1. 严禁在脚本中出现真实教师姓名、电话、身份证、邮箱等明文数据! +-- 2. 完全采用纯 DML 数据映射: t_department -> org_name, t_title -> job_title, create_user -> create_by +-- 3. 执行后验证迁移完整性,再将 status 与 del_flag 设为 NOT NULL 并安全移除旧列 +-- ============================================================================== + +BEGIN; + +-- 步骤 1: 数据平滑映射 (仅更新新字段为空的行,保证幂等) +UPDATE train_teacher +SET + org_name = COALESCE(org_name, t_department), + job_title = COALESCE(job_title, t_title), + create_by = COALESCE(create_by, create_user), + status = COALESCE(status, 1), + del_flag = COALESCE(del_flag, '0') +WHERE t_department IS NOT NULL OR t_title IS NOT NULL OR create_user IS NOT NULL; + +-- 步骤 2: 校验迁移正确性 (数据行数精确为1,且新字段全部填充完成) +DO $$ +DECLARE + v_total_cnt INT; + v_unmigrated_cnt INT; +BEGIN + SELECT count(*) INTO v_total_cnt FROM train_teacher; + SELECT count(*) INTO v_unmigrated_cnt + FROM train_teacher + WHERE org_name IS NULL OR status IS NULL OR del_flag IS NULL; + + IF v_total_cnt <> 1 THEN + RAISE EXCEPTION '>>> [008-FATAL] 教师数据校验异常: 期望精确 1 条数据,实际检测到 % 条', v_total_cnt; + END IF; + + IF v_unmigrated_cnt > 0 THEN + RAISE EXCEPTION '>>> [008-FATAL] 教师数据迁移失败: 存在 % 条记录的新主档字段尚未正确填充', v_unmigrated_cnt; + END IF; + + RAISE NOTICE '>>> [008-PASS] 教师唯一数据迁移核验通过,准备固化 NOT NULL 约束与清理旧列。'; +END $$; + +-- 步骤 3: 固化新列 NOT NULL 约束 +ALTER TABLE train_teacher ALTER COLUMN status SET NOT NULL; +ALTER TABLE train_teacher ALTER COLUMN del_flag SET NOT NULL; + +-- 步骤 4: 安全删除已成功迁移的旧列 (杜绝 CASCADE) +ALTER TABLE train_teacher + DROP COLUMN IF EXISTS t_department, + DROP COLUMN IF EXISTS t_title, + DROP COLUMN IF EXISTS create_user; + +COMMIT; diff --git a/RuoYi-Vue/sql/train/v2/009_drop_legacy_score.sql b/RuoYi-Vue/sql/train/v2/009_drop_legacy_score.sql new file mode 100644 index 000000000..9d977eacf --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/009_drop_legacy_score.sql @@ -0,0 +1,46 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 迁移脚本: 009_drop_legacy_score.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 淘汰旧模型表 train_score_info (由 train_score_item + train_student_score 取代) +-- 事务边界: 全程在显式事务 BEGIN ... COMMIT 中执行 +-- 设计依据: RailwaySystem 数据库 V2 字段设计与 Kingbase DDL 规范 V1.0 及 Round 1 审查要求 +-- 安全规范: +-- 1. 【强阻断要求】执行前必须严格断言 train_score_info 确为 0 行,非 0 立即异常中止! +-- 2. 显式解除自身约束,使用普通 DROP TABLE (杜绝使用 CASCADE 掩盖未预期依赖) +-- ============================================================================== + +BEGIN; + +-- 步骤 1: 强安全阻断校验 (非 0 行立即阻断并中止事务) +DO $$ +DECLARE + v_table_exists INT; + v_row_cnt INT; +BEGIN + SELECT count(*) INTO v_table_exists + FROM information_schema.tables + WHERE table_schema = current_schema() AND table_name = 'train_score_info'; + + IF v_table_exists > 0 THEN + EXECUTE 'SELECT count(*) FROM train_score_info' INTO v_row_cnt; + IF v_row_cnt > 0 THEN + RAISE EXCEPTION '>>> [009-FATAL] train_score_info 当前包含 % 行真实数据!禁止执行 DROP 操作!', v_row_cnt; + ELSE + RAISE NOTICE '>>> [009-PASS] 安全探测通过: train_score_info 精确为 0 行,允许执行淘汰下线。'; + END IF; + END IF; +END $$; + +-- 步骤 2: 卸载旧触发器 (若存在) +DROP TRIGGER IF EXISTS trig_train_scoreinfo_modify ON train_score_info; + +-- 步骤 3: 显式卸载已知历史约束 +ALTER TABLE train_score_info DROP CONSTRAINT IF EXISTS fk_scoreinfo_plan; +ALTER TABLE train_score_info DROP CONSTRAINT IF EXISTS fk_scoreinfo_course; +ALTER TABLE train_score_info DROP CONSTRAINT IF EXISTS fk_scoreinfo_stu; +ALTER TABLE train_score_info DROP CONSTRAINT IF EXISTS uq_scoreinfo_plan_c_stu; + +-- 步骤 4: 安全常规 DROP TABLE (无 CASCADE) +DROP TABLE IF EXISTS train_score_info; + +COMMIT; diff --git a/RuoYi-Vue/sql/train/v2/010_verify.sql b/RuoYi-Vue/sql/train/v2/010_verify.sql new file mode 100644 index 000000000..ec7904f57 --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/010_verify.sql @@ -0,0 +1,500 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 迁移验收脚本: 010_verify.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 实施“定义级深度验收”,核验数据库 V2 迁移后对象存在性与真实结构定义 +-- 设计依据: RailwaySystem 数据库 V2 字段设计与 Kingbase DDL 规范 V1.0 及 Round 2 审查要求 +-- 核心原则: +-- 1. 【失败即失败】所有验证逻辑均通过 PL/pgSQL DO 块强制断言,出现任何 FAIL 立即抛出 EXCEPTION +-- 2. 【定义级验收】拒绝仅查名字,必须校验: 表名、列列表、目标外键与删除规则、CHECK定义、索引定义 +-- 3. 【默认值校验】强断言 accommodation_status 默认值为 0,强断言业务选择型字段无 DEFAULT +-- 4. 【系统零破坏】显式核验 public.sys_* 系统表行数与基线完全吻合 +-- ============================================================================== + +-- 步骤 1: 打印 21 张表状态清单 (供人工审计屏幕阅读) +SELECT t.table_name, + CASE WHEN c.table_name IS NOT NULL THEN 'EXISTS' ELSE 'MISSING' END AS table_status +FROM ( + VALUES + ('train_teacher'), ('train_student'), ('train_course'), ('train_resource'), + ('train_course_resource'), ('train_classroom_info'), ('train_training_site'), + ('train_electronic_board'), ('train_dormitory_info'), ('train_class_plan'), + ('train_plan_course'), ('train_enrollment'), ('train_lesson'), + ('train_accommodation'), ('train_dorm_auth'), ('train_class_report'), + ('train_student_checkin'), ('train_score_item'), ('train_student_score'), + ('train_study_duration'), ('train_student_portrait') +) AS t(table_name) +LEFT JOIN information_schema.tables c + ON c.table_schema = 'public' AND c.table_name = t.table_name +ORDER BY t.table_name; + + +-- 步骤 2: 核心自动化强断言块 (任何项不满足立即抛出 EXCEPTION 中止) +DO $$ +DECLARE + v_cnt INT; + v_diff_text TEXT := ''; + r RECORD; +BEGIN + -- ========================================================================== + -- 1. 逐名断言 21 张正式业务表完全就绪且无冗余 train_* 杂表 + -- ========================================================================== + FOR r IN ( + WITH expected_tables(table_name) AS ( + VALUES + ('train_teacher'), ('train_student'), ('train_course'), ('train_resource'), + ('train_course_resource'), ('train_classroom_info'), ('train_training_site'), + ('train_electronic_board'), ('train_dormitory_info'), ('train_class_plan'), + ('train_plan_course'), ('train_enrollment'), ('train_lesson'), + ('train_accommodation'), ('train_dorm_auth'), ('train_class_report'), + ('train_student_checkin'), ('train_score_item'), ('train_student_score'), + ('train_study_duration'), ('train_student_portrait') + ), + actual_tables AS ( + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name LIKE 'train_%' + AND table_name NOT LIKE 'bak_%' + ) + SELECT 'MISSING_TABLE' AS diff_type, e.table_name + FROM expected_tables e + LEFT JOIN actual_tables a ON e.table_name = a.table_name + WHERE a.table_name IS NULL + UNION ALL + SELECT 'UNEXPECTED_TABLE' AS diff_type, a.table_name + FROM actual_tables a + LEFT JOIN expected_tables e ON a.table_name = e.table_name + WHERE e.table_name IS NULL + ) LOOP + v_diff_text := v_diff_text || r.diff_type || ':' || r.table_name || '; '; + END LOOP; + + IF v_diff_text <> '' THEN + RAISE EXCEPTION '>>> [Verify-FATAL] 21 张正式业务表集合不匹配: %', v_diff_text; + END IF; + + -- 确认旧淘汰表 train_score_info 彻底不存在 + SELECT count(*) INTO v_cnt FROM information_schema.tables + WHERE table_schema = 'public' AND table_name = 'train_score_info'; + IF v_cnt > 0 THEN + RAISE EXCEPTION '>>> [Verify-FATAL] train_score_info 仍残留在 public schema 中,淘汰下线失败!'; + END IF; + + -- ========================================================================== + -- 2. 数据行数基线强断言 (教师精确 1 行,新字段全部填充,其余 20 表精确 0 行) + -- ========================================================================== + SELECT count(*) INTO v_cnt FROM train_teacher; + IF v_cnt <> 1 THEN + RAISE EXCEPTION '>>> [Verify-FATAL] train_teacher 行数期望为 1,实际为 % 行', v_cnt; + END IF; + + SELECT count(*) INTO v_cnt + FROM train_teacher + WHERE org_name IS NULL OR status IS NULL OR del_flag IS NULL; + IF v_cnt > 0 THEN + RAISE EXCEPTION '>>> [Verify-FATAL] train_teacher 记录中新主档字段存在非法 NULL 值'; + END IF; + + SELECT count(*) INTO v_cnt + FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'train_teacher' + AND column_name IN ('t_department', 't_title', 'create_user'); + IF v_cnt > 0 THEN + RAISE EXCEPTION '>>> [Verify-FATAL] train_teacher 仍残留旧字段 (t_department/t_title/create_user)'; + END IF; + + FOR r IN ( + SELECT unnest(ARRAY[ + 'train_student', 'train_course', 'train_resource', 'train_course_resource', + 'train_classroom_info', 'train_training_site', 'train_electronic_board', + 'train_dormitory_info', 'train_class_plan', 'train_plan_course', + 'train_enrollment', 'train_lesson', 'train_accommodation', + 'train_dorm_auth', 'train_class_report', 'train_student_checkin', + 'train_score_item', 'train_student_score', 'train_study_duration', + 'train_student_portrait' + ]) AS tbl + ) LOOP + EXECUTE 'SELECT count(*) FROM ' || r.tbl INTO v_cnt; + IF v_cnt <> 0 THEN + RAISE EXCEPTION '>>> [Verify-FATAL] 表 % 包含非预期数据,当前行数为 % (期望 0 行)', r.tbl, v_cnt; + END IF; + END LOOP; + + -- ========================================================================== + -- 3. 13 组核心唯一约束 (UNIQUE) 定义级校验 (表名 + 约束名 + 组成列列表) + -- ========================================================================== + v_diff_text := ''; + FOR r IN ( + WITH expected_unique(table_name, constraint_name, column_list) AS ( + VALUES + ('train_student', 'uq_student_idcard_hash', 'id_card_hash'), + ('train_course', 'uq_course_code', 'c_code'), + ('train_electronic_board', 'uq_board_sn', 'board_sn'), + ('train_dormitory_info', 'uq_dorm_building_room', 'dorm_building, dorm_room'), + ('train_course_resource', 'uq_course_resource', 'c_id, resource_id'), + ('train_plan_course', 'uq_plan_course', 'plan_id, c_id'), + ('train_enrollment', 'uq_enrollment', 'plan_id, student_id'), + ('train_accommodation', 'uq_accommodation_enroll', 'enrollment_id'), + ('train_dorm_auth', 'uq_dorm_auth_accomm', 'accommodation_id'), + ('train_class_report', 'uq_class_report_enroll', 'enrollment_id'), + ('train_student_checkin', 'uq_student_checkin_enroll_lesson', 'enrollment_id, lesson_id'), + ('train_student_score', 'uq_student_score_enroll_item', 'enrollment_id, score_item_id'), + ('train_student_portrait', 'uq_student_portrait_enroll', 'enrollment_id') + ), + actual_unique AS ( + SELECT + tc.table_name, + tc.constraint_name, + string_agg(kcu.column_name, ', ' ORDER BY kcu.ordinal_position) AS column_list + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + WHERE tc.constraint_type = 'UNIQUE' + AND tc.table_schema = 'public' + AND tc.table_name LIKE 'train_%' + GROUP BY tc.table_name, tc.constraint_name + ) + SELECT 'MISSING_UQ' AS diff_type, e.table_name, e.constraint_name, e.column_list + FROM expected_unique e + LEFT JOIN actual_unique a + ON e.table_name = a.table_name + AND e.constraint_name = a.constraint_name + AND e.column_list = a.column_list + WHERE a.table_name IS NULL + UNION ALL + SELECT 'UNEXPECTED_UQ' AS diff_type, a.table_name, a.constraint_name, a.column_list + FROM actual_unique a + LEFT JOIN expected_unique e + ON a.table_name = e.table_name + AND a.constraint_name = e.constraint_name + AND a.column_list = e.column_list + WHERE e.table_name IS NULL + ) LOOP + v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '.' || r.constraint_name || '(' || r.column_list || '); '; + END LOOP; + + IF v_diff_text <> '' THEN + RAISE EXCEPTION '>>> [Verify-FATAL] 13 组唯一约束 (UNIQUE) 定义不匹配: %', v_diff_text; + END IF; + + -- ========================================================================== + -- 4. 26 组外键约束 (FK) 定义级校验 (表名 + 约束名 + 本地列 + 目标表 + 目标列 + 删除规则) + -- ========================================================================== + v_diff_text := ''; + FOR r IN ( + WITH expected_fk(table_name, constraint_name, column_name, foreign_table_name, foreign_column_name) AS ( + VALUES + ('train_course_resource', 'fk_cr_course', 'c_id', 'train_course', 'c_id'), + ('train_course_resource', 'fk_cr_resource', 'resource_id', 'train_resource', 'resource_id'), + ('train_electronic_board', 'fk_board_classroom', 'bind_classroom_id', 'train_classroom_info', 'class_room_id'), + ('train_plan_course', 'fk_plancourse_plan', 'plan_id', 'train_class_plan', 'plan_id'), + ('train_plan_course', 'fk_plancourse_course', 'c_id', 'train_course', 'c_id'), + ('train_enrollment', 'fk_enrollment_plan', 'plan_id', 'train_class_plan', 'plan_id'), + ('train_enrollment', 'fk_enrollment_student', 'student_id', 'train_student', 's_id'), + ('train_lesson', 'fk_lesson_plancourse', 'plan_course_id', 'train_plan_course', 'plan_course_id'), + ('train_lesson', 'fk_lesson_teacher', 't_id', 'train_teacher', 't_id'), + ('train_lesson', 'fk_lesson_classroom', 'class_room_id', 'train_classroom_info', 'class_room_id'), + ('train_lesson', 'fk_lesson_site', 'site_id', 'train_training_site', 'site_id'), + ('train_accommodation', 'fk_accomm_enrollment', 'enrollment_id', 'train_enrollment', 'enrollment_id'), + ('train_accommodation', 'fk_accomm_dorm', 'dorm_id', 'train_dormitory_info', 'dorm_id'), + ('train_dorm_auth', 'fk_dormauth_accomm', 'accommodation_id', 'train_accommodation', 'accommodation_id'), + ('train_class_report', 'fk_report_enrollment', 'enrollment_id', 'train_enrollment', 'enrollment_id'), + ('train_student_checkin', 'fk_checkin_enrollment', 'enrollment_id', 'train_enrollment', 'enrollment_id'), + ('train_student_checkin', 'fk_checkin_lesson', 'lesson_id', 'train_lesson', 'lesson_id'), + ('train_student_checkin', 'fk_checkin_board', 'board_id', 'train_electronic_board', 'board_id'), + ('train_score_item', 'fk_scoreitem_plan', 'plan_id', 'train_class_plan', 'plan_id'), + ('train_score_item', 'fk_scoreitem_plancourse', 'plan_course_id', 'train_plan_course', 'plan_course_id'), + ('train_student_score', 'fk_score_enrollment', 'enrollment_id', 'train_enrollment', 'enrollment_id'), + ('train_student_score', 'fk_score_item', 'score_item_id', 'train_score_item', 'score_item_id'), + ('train_study_duration', 'fk_study_enrollment', 'enrollment_id', 'train_enrollment', 'enrollment_id'), + ('train_study_duration', 'fk_study_plancourse', 'plan_course_id', 'train_plan_course', 'plan_course_id'), + ('train_study_duration', 'fk_study_resource', 'resource_id', 'train_resource', 'resource_id'), + ('train_student_portrait', 'fk_portrait_enrollment', 'enrollment_id', 'train_enrollment', 'enrollment_id') + ), + actual_fk AS ( + SELECT + tc.table_name, + tc.constraint_name, + kcu.column_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name, + rc.delete_rule + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage ccu + ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema + JOIN information_schema.referential_constraints rc + ON tc.constraint_name = rc.constraint_name AND tc.table_schema = rc.constraint_schema + WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema = 'public' + AND tc.table_name LIKE 'train_%' + ) + SELECT 'MISSING_FK' AS diff_type, e.table_name, e.constraint_name, e.column_name, e.foreign_table_name, e.foreign_column_name + FROM expected_fk e + LEFT JOIN actual_fk a + ON e.table_name = a.table_name + AND e.constraint_name = a.constraint_name + AND e.column_name = a.column_name + AND e.foreign_table_name = a.foreign_table_name + AND e.foreign_column_name = a.foreign_column_name + AND a.delete_rule = 'NO ACTION' + WHERE a.table_name IS NULL + UNION ALL + SELECT 'UNEXPECTED_FK' AS diff_type, a.table_name, a.constraint_name, a.column_name, a.foreign_table_name, a.foreign_column_name + FROM actual_fk a + LEFT JOIN expected_fk e + ON a.table_name = e.table_name + AND a.constraint_name = e.constraint_name + AND a.column_name = e.column_name + AND a.foreign_table_name = e.foreign_table_name + AND a.foreign_column_name = e.foreign_column_name + WHERE e.table_name IS NULL + ) LOOP + v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '.' || r.constraint_name || '; '; + END LOOP; + + IF v_diff_text <> '' THEN + RAISE EXCEPTION '>>> [Verify-FATAL] 26 组外键约束 (FK) 定义或删除规则不匹配: %', v_diff_text; + END IF; + + -- ========================================================================== + -- 5. 9 组单行检查约束 (CHECK) 定义逻辑校验 (逐名检查并核验包含预期核心表达式) + -- ========================================================================== + FOR r IN ( + SELECT + cc.constraint_name, + tc.table_name, + cc.check_clause + FROM information_schema.check_constraints cc + JOIN information_schema.table_constraints tc + ON cc.constraint_name = tc.constraint_name AND cc.constraint_schema = tc.constraint_schema + WHERE tc.table_schema = 'public' AND tc.table_name LIKE 'train_%' + ) LOOP + CASE r.constraint_name + WHEN 'chk_lesson_time' THEN + IF r.table_name <> 'train_lesson' OR r.check_clause NOT LIKE '%start_time < end_time%' THEN + RAISE EXCEPTION '>>> [Verify-FATAL] chk_lesson_time 定义不合预期: %', r.check_clause; + END IF; + WHEN 'chk_lesson_venue' THEN + IF r.table_name <> 'train_lesson' OR (r.check_clause NOT LIKE '%venue_type = 1%' OR r.check_clause NOT LIKE '%venue_type = 2%') THEN + RAISE EXCEPTION '>>> [Verify-FATAL] chk_lesson_venue 场地互斥定义不合预期: %', r.check_clause; + END IF; + WHEN 'chk_score_item_weight' THEN + IF r.table_name <> 'train_score_item' OR (r.check_clause NOT LIKE '%weight >= 0%' OR r.check_clause NOT LIKE '%weight <= 100%') THEN + RAISE EXCEPTION '>>> [Verify-FATAL] chk_score_item_weight 权重范围定义不合预期: %', r.check_clause; + END IF; + WHEN 'chk_score_item_full' THEN + IF r.table_name <> 'train_score_item' OR r.check_clause NOT LIKE '%full_score > 0%' THEN + RAISE EXCEPTION '>>> [Verify-FATAL] chk_score_item_full 满分定义不合预期: %', r.check_clause; + END IF; + WHEN 'chk_student_score_nonneg' THEN + IF r.table_name <> 'train_student_score' OR r.check_clause NOT LIKE '%score >= 0%' THEN + RAISE EXCEPTION '>>> [Verify-FATAL] chk_student_score_nonneg 成绩非负定义不合预期: %', r.check_clause; + END IF; + WHEN 'chk_study_seconds' THEN + IF r.table_name <> 'train_study_duration' OR r.check_clause NOT LIKE '%study_seconds >= 0%' THEN + RAISE EXCEPTION '>>> [Verify-FATAL] chk_study_seconds 时长非负定义不合预期: %', r.check_clause; + END IF; + WHEN 'chk_study_time' THEN + IF r.table_name <> 'train_study_duration' OR r.check_clause NOT LIKE '%end_time >= start_time%' THEN + RAISE EXCEPTION '>>> [Verify-FATAL] chk_study_time 时间先后定义不合预期: %', r.check_clause; + END IF; + WHEN 'chk_study_source_system' THEN + IF r.table_name <> 'train_study_duration' OR r.check_clause NOT LIKE '%source_system IS NOT NULL%' THEN + RAISE EXCEPTION '>>> [Verify-FATAL] chk_study_source_system 来源系统一致性定义不合预期: %', r.check_clause; + END IF; + WHEN 'chk_dorm_capacity' THEN + IF r.table_name <> 'train_dormitory_info' OR r.check_clause NOT LIKE '%capacity > 0%' THEN + RAISE EXCEPTION '>>> [Verify-FATAL] chk_dorm_capacity 容量正数定义不合预期: %', r.check_clause; + END IF; + ELSE + NULL; -- 忽略其他系统生成的非空检查 + END CASE; + END LOOP; + + -- 确认 9 个显式命名的 CHECK 约束一个不少 + SELECT count(*) INTO v_cnt + FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND constraint_type = 'CHECK' + AND constraint_name IN ( + 'chk_lesson_time', 'chk_lesson_venue', 'chk_score_item_weight', + 'chk_score_item_full', 'chk_student_score_nonneg', 'chk_study_seconds', + 'chk_study_time', 'chk_study_source_system', 'chk_dorm_capacity' + ); + IF v_cnt <> 9 THEN + RAISE EXCEPTION '>>> [Verify-FATAL] 9 个核心 CHECK 检查约束总数不匹配,实际发现 % 个', v_cnt; + END IF; + + -- ========================================================================== + -- 6. 12 张业务表 Identity 自增主键精确校验 (表名 + 指定列名 + is_identity = 'YES') + -- ========================================================================== + v_diff_text := ''; + FOR r IN ( + WITH expected_identity(table_name, column_name) AS ( + VALUES + ('train_course_resource', 'id'), + ('train_plan_course', 'plan_course_id'), + ('train_enrollment', 'enrollment_id'), + ('train_lesson', 'lesson_id'), + ('train_accommodation', 'accommodation_id'), + ('train_score_item', 'score_item_id'), + ('train_student_score', 'score_id'), + ('train_dorm_auth', 'auth_id'), + ('train_class_report', 'report_id'), + ('train_student_checkin', 'checkin_id'), + ('train_study_duration', 'dur_id'), + ('train_student_portrait', 'portrait_id') + ), + actual_identity AS ( + SELECT table_name, column_name + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name LIKE 'train_%' + AND is_identity = 'YES' + ) + SELECT 'MISSING_IDENTITY' AS diff_type, e.table_name, e.column_name + FROM expected_identity e + LEFT JOIN actual_identity a + ON e.table_name = a.table_name AND e.column_name = a.column_name + WHERE a.table_name IS NULL + UNION ALL + SELECT 'UNEXPECTED_IDENTITY' AS diff_type, a.table_name, a.column_name + FROM actual_identity a + LEFT JOIN expected_identity e + ON a.table_name = e.table_name AND a.column_name = e.column_name + WHERE e.table_name IS NULL + ) LOOP + v_diff_text := v_diff_text || r.diff_type || ': ' || r.table_name || '.' || r.column_name || '; '; + END LOOP; + + IF v_diff_text <> '' THEN + RAISE EXCEPTION '>>> [Verify-FATAL] 12 张表 Identity 自增主键属性不匹配: %', v_diff_text; + END IF; + + -- ========================================================================== + -- 7. 关键复合索引与部分唯一索引定义级校验 + -- ========================================================================== + -- 7.1 uq_idx_study_source_event 必须为 UNIQUE 且具有双非空 WHERE 条件 + SELECT count(*) INTO v_cnt FROM pg_indexes + WHERE schemaname = 'public' + AND tablename = 'train_study_duration' + AND indexname = 'uq_idx_study_source_event' + AND indexdef LIKE '%UNIQUE INDEX%' + AND indexdef LIKE '%(source_system, source_event_id)%' + AND indexdef LIKE '%source_system IS NOT NULL%' + AND indexdef LIKE '%source_event_id IS NOT NULL%'; + IF v_cnt <> 1 THEN + RAISE EXCEPTION '>>> [Verify-FATAL] 部分唯一索引 uq_idx_study_source_event 定义不合要求'; + END IF; + + -- 7.2 排课表三大时间检索复合索引 + SELECT count(*) INTO v_cnt FROM pg_indexes + WHERE schemaname = 'public' AND tablename = 'train_lesson' + AND indexname = 'idx_lesson_teacher_time' AND indexdef LIKE '%(t_id, start_time, end_time)%'; + IF v_cnt <> 1 THEN RAISE EXCEPTION '>>> [Verify-FATAL] 索引 idx_lesson_teacher_time 复合列定义不合要求'; END IF; + + SELECT count(*) INTO v_cnt FROM pg_indexes + WHERE schemaname = 'public' AND tablename = 'train_lesson' + AND indexname = 'idx_lesson_classroom_time' AND indexdef LIKE '%(class_room_id, start_time, end_time)%'; + IF v_cnt <> 1 THEN RAISE EXCEPTION '>>> [Verify-FATAL] 索引 idx_lesson_classroom_time 复合列定义不合要求'; END IF; + + SELECT count(*) INTO v_cnt FROM pg_indexes + WHERE schemaname = 'public' AND tablename = 'train_lesson' + AND indexname = 'idx_lesson_site_time' AND indexdef LIKE '%(site_id, start_time, end_time)%'; + IF v_cnt <> 1 THEN RAISE EXCEPTION '>>> [Verify-FATAL] 索引 idx_lesson_site_time 复合列定义不合要求'; END IF; + + -- ========================================================================== + -- 8. 默认值严格校验 (R2-01 住宿默认0预分配; R2-02 业务选择型字段严禁有DEFAULT) + -- ========================================================================== + -- 8.1 accommodation_status 默认值必须为 0 (预分配) + SELECT count(*) INTO v_cnt + FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'train_accommodation' + AND column_name = 'accommodation_status' AND column_default LIKE '%0%'; + IF v_cnt <> 1 THEN + RAISE EXCEPTION '>>> [Verify-FATAL] train_accommodation.accommodation_status 默认值必须为 0 (预分配)'; + END IF; + + -- 8.2 业务选择型字段严禁存在默认值 (column_default 必须为 NULL) + FOR r IN ( + SELECT table_name, column_name, column_default + FROM information_schema.columns + WHERE table_schema = 'public' AND ( + (table_name = 'train_enrollment' AND column_name IN ('source_type', 'accommodation_required')) OR + (table_name = 'train_lesson' AND column_name = 'venue_type') OR + (table_name = 'train_student_score' AND column_name = 'source_type') OR + (table_name = 'train_student_checkin' AND column_name = 'checkin_way') OR + (table_name = 'train_class_report' AND column_name = 'verify_result') + ) + ) LOOP + IF r.column_default IS NOT NULL THEN + RAISE EXCEPTION '>>> [Verify-FATAL] 业务选择型字段 %.% 非法配置了默认值: %,必须取消!', r.table_name, r.column_name, r.column_default; + END IF; + END LOOP; + + -- ========================================================================== + -- 9. 核心领域架构解耦列特征强断言 + -- ========================================================================== + -- train_class_plan 必须无 c_id 与 t_id + SELECT count(*) INTO v_cnt FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'train_class_plan' AND column_name IN ('c_id', 't_id'); + IF v_cnt > 0 THEN + RAISE EXCEPTION '>>> [Verify-FATAL] train_class_plan 仍包含旧单课或单教师外键列 c_id/t_id'; + END IF; + + -- train_student_checkin 必须包含 enrollment_id 与 lesson_id,且无旧冗余列 + SELECT count(*) INTO v_cnt FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'train_student_checkin' + AND column_name IN ('enrollment_id', 'lesson_id'); + IF v_cnt <> 2 THEN + RAISE EXCEPTION '>>> [Verify-FATAL] train_student_checkin 未包含核心列 enrollment_id 与 lesson_id'; + END IF; + + SELECT count(*) INTO v_cnt FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'train_student_checkin' + AND column_name IN ('plan_id', 'c_id', 'student_id', 'lesson_start_time', 'lesson_end_time'); + IF v_cnt > 0 THEN + RAISE EXCEPTION '>>> [Verify-FATAL] train_student_checkin 仍残留历史冗余列'; + END IF; + + -- train_class_report 必须无 alloc_* 旧预分配列 + SELECT count(*) INTO v_cnt FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'train_class_report' + AND column_name IN ('alloc_dorm_id', 'alloc_classroom_id', 'alloc_site_id'); + IF v_cnt > 0 THEN + RAISE EXCEPTION '>>> [Verify-FATAL] train_class_report 仍残留旧预分配列'; + END IF; + + -- train_lesson.t_id 必须为 NOT NULL + SELECT count(*) INTO v_cnt FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'train_lesson' + AND column_name = 't_id' AND is_nullable = 'NO'; + IF v_cnt <> 1 THEN + RAISE EXCEPTION '>>> [Verify-FATAL] train_lesson.t_id 必须为 NOT NULL'; + END IF; + + -- ========================================================================== + -- 10. public.sys_* 系统基线表行数与零受损核验 + -- ========================================================================== + SELECT count(*) INTO v_cnt FROM public.sys_user; + IF v_cnt <> 1 THEN RAISE EXCEPTION '>>> [Verify-FATAL] public.sys_user 行数受损,当前为 %', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM public.sys_menu; + IF v_cnt <> 106 THEN RAISE EXCEPTION '>>> [Verify-FATAL] public.sys_menu 行数受损,当前为 %', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM public.sys_dept; + IF v_cnt <> 15 THEN RAISE EXCEPTION '>>> [Verify-FATAL] public.sys_dept 行数受损,当前为 %', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM public.sys_dict_type; + IF v_cnt <> 44 THEN RAISE EXCEPTION '>>> [Verify-FATAL] public.sys_dict_type 行数受损,当前为 %', v_cnt; END IF; + + SELECT count(*) INTO v_cnt FROM public.sys_dict_data; + IF v_cnt <> 500 THEN RAISE EXCEPTION '>>> [Verify-FATAL] public.sys_dict_data 行数受损,当前为 %', v_cnt; END IF; + + RAISE NOTICE '=============================================================================='; + RAISE NOTICE '>>> [Verify-PASS] RailwaySystem 数据库 V2 定义级验收全量通过!'; + RAISE NOTICE '>>> 验收内容: 21表 + 13 UNIQUE(定义) + 26 FK(定义+NO ACTION) + 9 CHECK(表达式) + 12 Identity + 关键复合索引 + 默认值严格断言 + sys_*零破坏'; + RAISE NOTICE '=============================================================================='; +END $$; diff --git a/RuoYi-Vue/sql/train/v2/rollback/000_phase05_exact_schema.sql b/RuoYi-Vue/sql/train/v2/rollback/000_phase05_exact_schema.sql new file mode 100644 index 000000000..eb4a4f4b2 --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/rollback/000_phase05_exact_schema.sql @@ -0,0 +1,356 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 回滚基线: 000_phase05_exact_schema.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 精确定义 Phase 0.5 历史基线 14 张培训业务表的完整物理 DDL +-- 依据来源: codebase-audit 13_live_database_snapshot.md 真实元数据基线 +-- 包含内容: +-- 1. 14 张表物理字段定义、NOT NULL、DEFAULT +-- 2. 14 个 PRIMARY KEY (8 个 varchar(32), 6 个 bigint GENERATED BY DEFAULT AS IDENTITY) +-- 3. 7 组历史业务 UNIQUE 约束 +-- 4. 24 组历史业务 FOREIGN KEY 约束 +-- 5. 9 个 update_time 触发器 (绑定 func_train_update_modifytime) +-- 说明: 本文件为 Phase 0.5 原始结构标准,作为回滚操作的唯一结构依据,杜绝 CTAS 推断结构 +-- ============================================================================== + +-- 1. train_teacher 教师主档 +CREATE TABLE train_teacher ( + t_id VARCHAR(32) NOT NULL, + t_name VARCHAR(30) NOT NULL, + t_gender CHAR(2), + t_birth DATE, + t_department VARCHAR(50) NOT NULL, + t_title VARCHAR(30), + t_phone VARCHAR(20), + t_email VARCHAR(50), + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_teacher PRIMARY KEY (t_id) +); + +-- 2. train_student 学员表 +CREATE TABLE train_student ( + s_id VARCHAR(32) NOT NULL, + s_name VARCHAR(30) NOT NULL, + s_gender CHAR(2), + s_birth DATE, + s_department VARCHAR(50) NOT NULL, + s_major VARCHAR(50) NOT NULL, + s_class VARCHAR(30), + s_phone VARCHAR(20), + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_student PRIMARY KEY (s_id) +); + +-- 3. train_course 课程表 +CREATE TABLE train_course ( + c_id VARCHAR(32) NOT NULL, + c_name VARCHAR(100) NOT NULL, + c_type VARCHAR(30), + c_duration INTEGER, + teaching_objective TEXT, + course_content TEXT, + attach_doc TEXT, + attach_video TEXT, + attach_model_link TEXT, + model_resource_ids TEXT, + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_course PRIMARY KEY (c_id) +); + +-- 4. train_class_plan 开班计划表 +CREATE TABLE train_class_plan ( + plan_id VARCHAR(32) NOT NULL, + plan_name VARCHAR(100) NOT NULL, + c_id VARCHAR(32) NOT NULL, + start_time TIMESTAMP NOT NULL, + end_time TIMESTAMP NOT NULL, + train_address VARCHAR(100), + t_id VARCHAR(32) NOT NULL, + recruit_num INTEGER, + apply_condition TEXT, + plan_status SMALLINT DEFAULT 0, + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_class_plan PRIMARY KEY (plan_id) +); + +-- 5. train_dormitory_info 宿舍信息表 +CREATE TABLE train_dormitory_info ( + dorm_id VARCHAR(32) NOT NULL, + dorm_building VARCHAR(50), + dorm_room VARCHAR(30) NOT NULL, + bed_total INTEGER, + bed_used INTEGER DEFAULT 0, + lock_device_sn VARCHAR(64), + dorm_status SMALLINT DEFAULT 1, + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_dormitory_info PRIMARY KEY (dorm_id), + CONSTRAINT uq_dorm_building_room UNIQUE (dorm_building, dorm_room) +); + +-- 6. train_dorm_auth 宿舍门禁表 +CREATE TABLE train_dorm_auth ( + auth_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + student_id VARCHAR(32) NOT NULL, + plan_id VARCHAR(32) NOT NULL, + dorm_id VARCHAR(32) NOT NULL, + bed_no VARCHAR(20), + face_sync_status SMALLINT DEFAULT 0, + auth_start TIMESTAMP, + auth_end TIMESTAMP, + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_dorm_auth PRIMARY KEY (auth_id), + CONSTRAINT uq_dormauth_stu_plan UNIQUE (student_id, plan_id) +); + +-- 7. train_classroom_info 教室信息表 +CREATE TABLE train_classroom_info ( + class_room_id VARCHAR(32) NOT NULL, + room_name VARCHAR(60) NOT NULL, + room_location VARCHAR(100), + capacity INTEGER, + room_status SMALLINT DEFAULT 1, + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_classroom_info PRIMARY KEY (class_room_id) +); + +-- 8. train_training_site 实训场地表 +CREATE TABLE train_training_site ( + site_id VARCHAR(32) NOT NULL, + site_name VARCHAR(80) NOT NULL, + site_location VARCHAR(100), + capacity INTEGER, + site_device TEXT, + site_status SMALLINT DEFAULT 1, + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_training_site PRIMARY KEY (site_id) +); + +-- 9. train_class_report 报到记录表 +CREATE TABLE train_class_report ( + report_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + plan_id VARCHAR(32) NOT NULL, + student_id VARCHAR(32) NOT NULL, + report_way SMALLINT, + report_device_sn VARCHAR(64), + report_time TIMESTAMP, + report_status SMALLINT DEFAULT 0, + alloc_dorm_id VARCHAR(32), + alloc_classroom_id VARCHAR(32), + alloc_site_id VARCHAR(32), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT pk_train_class_report PRIMARY KEY (report_id), + CONSTRAINT uq_classreport_plan_stu UNIQUE (plan_id, student_id) +); + +-- 10. train_electronic_board 电子班牌表 +CREATE TABLE train_electronic_board ( + board_id VARCHAR(32) NOT NULL, + board_sn VARCHAR(64) NOT NULL, + bind_classroom_id VARCHAR(32), + bind_plan_id VARCHAR(32), + board_ip VARCHAR(50), + board_content VARCHAR(500), + board_playmode SMALLINT DEFAULT 1, + support_checkin_type VARCHAR(30), + board_status SMALLINT DEFAULT 1, + last_heartbeat TIMESTAMP, + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_electronic_board PRIMARY KEY (board_id), + CONSTRAINT uq_board_sn UNIQUE (board_sn) +); + +-- 11. train_student_checkin 考勤签到表 +CREATE TABLE train_student_checkin ( + checkin_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + plan_id VARCHAR(32) NOT NULL, + c_id VARCHAR(32) NOT NULL, + student_id VARCHAR(32) NOT NULL, + board_id VARCHAR(32), + checkin_way SMALLINT, + checkin_time TIMESTAMP, + checkin_status SMALLINT, + lesson_start_time TIMESTAMP, + lesson_end_time TIMESTAMP, + CONSTRAINT pk_train_student_checkin PRIMARY KEY (checkin_id), + CONSTRAINT uq_checkin_lesson_stu UNIQUE (plan_id, c_id, student_id, lesson_start_time) +); + +-- 12. train_score_info 成绩信息表 (历史旧表) +CREATE TABLE train_score_info ( + score_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + plan_id VARCHAR(32), + c_id VARCHAR(32), + student_id VARCHAR(32), + course_score NUMERIC(5,2), + train_score NUMERIC(5,2), + exam_score NUMERIC(5,2), + source_type SMALLINT, + source_system VARCHAR(50), + import_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT pk_train_score_info PRIMARY KEY (score_id), + CONSTRAINT uq_scoreinfo_plan_c_stu UNIQUE (plan_id, c_id, student_id) +); + +-- 13. train_study_duration 学习时长表 +CREATE TABLE train_study_duration ( + dur_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + student_id VARCHAR(32), + plan_id VARCHAR(32), + c_id VARCHAR(32), + device_type VARCHAR(50), + start_time TIMESTAMP, + end_time TIMESTAMP, + study_seconds INTEGER, + sync_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT pk_train_study_duration PRIMARY KEY (dur_id) +); + +-- 14. train_student_portrait 学员画像表 +CREATE TABLE train_student_portrait ( + portrait_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + student_id VARCHAR(32) NOT NULL, + plan_id VARCHAR(32) NOT NULL, + total_study_hour NUMERIC(8,2), + avg_score NUMERIC(5,2), + checkin_rate NUMERIC(5,2), + label_tag TEXT, + portrait_content TEXT, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT pk_train_student_portrait PRIMARY KEY (portrait_id), + CONSTRAINT uq_portrait_stu_plan UNIQUE (student_id, plan_id) +); + + +-- ============================================================================== +-- 历史 24 个外键约束 (FOREIGN KEY CONSTRAINTS - 全部 ON DELETE NO ACTION) +-- ============================================================================== + +-- train_class_plan (2 FK) +ALTER TABLE train_class_plan ADD CONSTRAINT fk_classplan_course + FOREIGN KEY (c_id) REFERENCES train_course (c_id) ON DELETE NO ACTION; +ALTER TABLE train_class_plan ADD CONSTRAINT fk_classplan_teacher + FOREIGN KEY (t_id) REFERENCES train_teacher (t_id) ON DELETE NO ACTION; + +-- train_dorm_auth (3 FK) +ALTER TABLE train_dorm_auth ADD CONSTRAINT fk_dormauth_stu + FOREIGN KEY (student_id) REFERENCES train_student (s_id) ON DELETE NO ACTION; +ALTER TABLE train_dorm_auth ADD CONSTRAINT fk_dormauth_plan + FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION; +ALTER TABLE train_dorm_auth ADD CONSTRAINT fk_dormauth_dorm + FOREIGN KEY (dorm_id) REFERENCES train_dormitory_info (dorm_id) ON DELETE NO ACTION; + +-- train_class_report (5 FK) +ALTER TABLE train_class_report ADD CONSTRAINT fk_classreport_plan + FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION; +ALTER TABLE train_class_report ADD CONSTRAINT fk_classreport_stu + FOREIGN KEY (student_id) REFERENCES train_student (s_id) ON DELETE NO ACTION; +ALTER TABLE train_class_report ADD CONSTRAINT fk_classreport_dorm + FOREIGN KEY (alloc_dorm_id) REFERENCES train_dormitory_info (dorm_id) ON DELETE NO ACTION; +ALTER TABLE train_class_report ADD CONSTRAINT fk_classreport_classroom + FOREIGN KEY (alloc_classroom_id) REFERENCES train_classroom_info (class_room_id) ON DELETE NO ACTION; +ALTER TABLE train_class_report ADD CONSTRAINT fk_classreport_site + FOREIGN KEY (alloc_site_id) REFERENCES train_training_site (site_id) ON DELETE NO ACTION; + +-- train_electronic_board (2 FK) +ALTER TABLE train_electronic_board ADD CONSTRAINT fk_board_classroom + FOREIGN KEY (bind_classroom_id) REFERENCES train_classroom_info (class_room_id) ON DELETE NO ACTION; +ALTER TABLE train_electronic_board ADD CONSTRAINT fk_board_plan + FOREIGN KEY (bind_plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION; + +-- train_student_checkin (4 FK) +ALTER TABLE train_student_checkin ADD CONSTRAINT fk_checkin_plan + FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION; +ALTER TABLE train_student_checkin ADD CONSTRAINT fk_checkin_course + FOREIGN KEY (c_id) REFERENCES train_course (c_id) ON DELETE NO ACTION; +ALTER TABLE train_student_checkin ADD CONSTRAINT fk_checkin_stu + FOREIGN KEY (student_id) REFERENCES train_student (s_id) ON DELETE NO ACTION; +ALTER TABLE train_student_checkin ADD CONSTRAINT fk_checkin_board + FOREIGN KEY (board_id) REFERENCES train_electronic_board (board_id) ON DELETE NO ACTION; + +-- train_score_info (3 FK) +ALTER TABLE train_score_info ADD CONSTRAINT fk_scoreinfo_plan + FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION; +ALTER TABLE train_score_info ADD CONSTRAINT fk_scoreinfo_course + FOREIGN KEY (c_id) REFERENCES train_course (c_id) ON DELETE NO ACTION; +ALTER TABLE train_score_info ADD CONSTRAINT fk_scoreinfo_stu + FOREIGN KEY (student_id) REFERENCES train_student (s_id) ON DELETE NO ACTION; + +-- train_study_duration (3 FK) +ALTER TABLE train_study_duration ADD CONSTRAINT fk_studydur_stu + FOREIGN KEY (student_id) REFERENCES train_student (s_id) ON DELETE NO ACTION; +ALTER TABLE train_study_duration ADD CONSTRAINT fk_studydur_plan + FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION; +ALTER TABLE train_study_duration ADD CONSTRAINT fk_studydur_course + FOREIGN KEY (c_id) REFERENCES train_course (c_id) ON DELETE NO ACTION; + +-- train_student_portrait (2 FK) +ALTER TABLE train_student_portrait ADD CONSTRAINT fk_portrait_stu + FOREIGN KEY (student_id) REFERENCES train_student (s_id) ON DELETE NO ACTION; +ALTER TABLE train_student_portrait ADD CONSTRAINT fk_portrait_plan + FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION; + + +-- ============================================================================== +-- 历史 9 个 update_time 触发器 +-- ============================================================================== +CREATE OR REPLACE TRIGGER trig_train_teacher_modify + BEFORE UPDATE ON train_teacher + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_student_modify + BEFORE UPDATE ON train_student + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_course_modify + BEFORE UPDATE ON train_course + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_classroom_modify + BEFORE UPDATE ON train_classroom_info + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_trainsite_modify + BEFORE UPDATE ON train_training_site + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_board_modify + BEFORE UPDATE ON train_electronic_board + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_dorminfo_modify + BEFORE UPDATE ON train_dormitory_info + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_classplan_modify + BEFORE UPDATE ON train_class_plan + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_dormauth_modify + BEFORE UPDATE ON train_dorm_auth + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); diff --git a/RuoYi-Vue/sql/train/v2/rollback/001_rollback_new_tables.sql b/RuoYi-Vue/sql/train/v2/rollback/001_rollback_new_tables.sql new file mode 100644 index 000000000..17b49fed7 --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/rollback/001_rollback_new_tables.sql @@ -0,0 +1,45 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 回滚脚本: 001_rollback_new_tables.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 安全下线并删除数据库 V2 新增的 8 张培训业务表 +-- 事务边界: 全程在显式事务 BEGIN ... COMMIT 中执行 +-- 说明: 先显式解除外键约束,然后按依赖反序执行常规 DROP TABLE (无 CASCADE) +-- ============================================================================== + +BEGIN; + +-- 步骤 1: 显式解除 V2 新增表之间的外键约束 +ALTER TABLE train_student_score DROP CONSTRAINT IF EXISTS fk_score_enrollment; +ALTER TABLE train_student_score DROP CONSTRAINT IF EXISTS fk_score_item; + +ALTER TABLE train_score_item DROP CONSTRAINT IF EXISTS fk_scoreitem_plan; +ALTER TABLE train_score_item DROP CONSTRAINT IF EXISTS fk_scoreitem_plancourse; + +ALTER TABLE train_lesson DROP CONSTRAINT IF EXISTS fk_lesson_plancourse; +ALTER TABLE train_lesson DROP CONSTRAINT IF EXISTS fk_lesson_teacher; +ALTER TABLE train_lesson DROP CONSTRAINT IF EXISTS fk_lesson_classroom; +ALTER TABLE train_lesson DROP CONSTRAINT IF EXISTS fk_lesson_site; + +ALTER TABLE train_accommodation DROP CONSTRAINT IF EXISTS fk_accomm_enrollment; +ALTER TABLE train_accommodation DROP CONSTRAINT IF EXISTS fk_accomm_dorm; + +ALTER TABLE train_enrollment DROP CONSTRAINT IF EXISTS fk_enrollment_plan; +ALTER TABLE train_enrollment DROP CONSTRAINT IF EXISTS fk_enrollment_student; + +ALTER TABLE train_plan_course DROP CONSTRAINT IF EXISTS fk_plancourse_plan; +ALTER TABLE train_plan_course DROP CONSTRAINT IF EXISTS fk_plancourse_course; + +ALTER TABLE train_course_resource DROP CONSTRAINT IF EXISTS fk_cr_course; +ALTER TABLE train_course_resource DROP CONSTRAINT IF EXISTS fk_cr_resource; + +-- 步骤 2: 按依赖拓扑反序执行常规 DROP TABLE +DROP TABLE IF EXISTS train_student_score; +DROP TABLE IF EXISTS train_score_item; +DROP TABLE IF EXISTS train_lesson; +DROP TABLE IF EXISTS train_accommodation; +DROP TABLE IF EXISTS train_enrollment; +DROP TABLE IF EXISTS train_plan_course; +DROP TABLE IF EXISTS train_course_resource; +DROP TABLE IF EXISTS train_resource; + +COMMIT; diff --git a/RuoYi-Vue/sql/train/v2/rollback/002_rollback_modified_tables.sql b/RuoYi-Vue/sql/train/v2/rollback/002_rollback_modified_tables.sql new file mode 100644 index 000000000..637860d23 --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/rollback/002_rollback_modified_tables.sql @@ -0,0 +1,337 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 回滚脚本: 002_rollback_modified_tables.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 严格基于 000_phase05_exact_schema.sql 结构与 bak_v1_* 数据还原 13 张表 +-- 事务边界: 全程在显式事务 BEGIN ... COMMIT 中执行 +-- 规范要求: +-- 1. 彻底摒弃 CTAS 丢失约束/默认值/触发器的不可靠方案 +-- 2. 精确采用 Phase 0.5 原始 DDL 重建表结构 +-- 3. 从 bak_v1_* 恢复真实数据并严格校验行数 +-- ============================================================================== + +BEGIN; + +-- 步骤 1: 显式解除既有 V2 外键约束与旧外键 +ALTER TABLE train_dorm_auth DROP CONSTRAINT IF EXISTS uq_dorm_auth_accomm; +ALTER TABLE train_dorm_auth DROP CONSTRAINT IF EXISTS fk_dormauth_accomm; +ALTER TABLE train_class_report DROP CONSTRAINT IF EXISTS uq_class_report_enroll; +ALTER TABLE train_class_report DROP CONSTRAINT IF EXISTS fk_report_enrollment; +ALTER TABLE train_student_checkin DROP CONSTRAINT IF EXISTS uq_student_checkin_enroll_lesson; +ALTER TABLE train_student_checkin DROP CONSTRAINT IF EXISTS fk_checkin_enrollment; +ALTER TABLE train_student_checkin DROP CONSTRAINT IF EXISTS fk_checkin_lesson; +ALTER TABLE train_student_checkin DROP CONSTRAINT IF EXISTS fk_checkin_board; +ALTER TABLE train_study_duration DROP CONSTRAINT IF EXISTS fk_study_enrollment; +ALTER TABLE train_study_duration DROP CONSTRAINT IF EXISTS fk_study_plancourse; +ALTER TABLE train_study_duration DROP CONSTRAINT IF EXISTS fk_study_resource; +ALTER TABLE train_student_portrait DROP CONSTRAINT IF EXISTS uq_student_portrait_enroll; +ALTER TABLE train_student_portrait DROP CONSTRAINT IF EXISTS fk_portrait_enrollment; +ALTER TABLE train_electronic_board DROP CONSTRAINT IF EXISTS fk_board_classroom; + +-- 步骤 2: 常规 DROP 13 张表 +DROP TABLE IF EXISTS train_student_portrait; +DROP TABLE IF EXISTS train_study_duration; +DROP TABLE IF EXISTS train_student_checkin; +DROP TABLE IF EXISTS train_class_report; +DROP TABLE IF EXISTS train_dorm_auth; +DROP TABLE IF EXISTS train_electronic_board; +DROP TABLE IF EXISTS train_dormitory_info; +DROP TABLE IF EXISTS train_class_plan; +DROP TABLE IF EXISTS train_training_site; +DROP TABLE IF EXISTS train_classroom_info; +DROP TABLE IF EXISTS train_course; +DROP TABLE IF EXISTS train_student; +DROP TABLE IF EXISTS train_teacher; + +-- 步骤 3: 严格依据 Phase 0.5 原始 DDL 重建 13 张表物理结构 + +CREATE TABLE train_teacher ( + t_id VARCHAR(32) NOT NULL, + t_name VARCHAR(30) NOT NULL, + t_gender CHAR(2), + t_birth DATE, + t_department VARCHAR(50) NOT NULL, + t_title VARCHAR(30), + t_phone VARCHAR(20), + t_email VARCHAR(50), + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_teacher PRIMARY KEY (t_id) +); + +CREATE TABLE train_student ( + s_id VARCHAR(32) NOT NULL, + s_name VARCHAR(30) NOT NULL, + s_gender CHAR(2), + s_birth DATE, + s_department VARCHAR(50) NOT NULL, + s_major VARCHAR(50) NOT NULL, + s_class VARCHAR(30), + s_phone VARCHAR(20), + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_student PRIMARY KEY (s_id) +); + +CREATE TABLE train_course ( + c_id VARCHAR(32) NOT NULL, + c_name VARCHAR(100) NOT NULL, + c_type VARCHAR(30), + c_duration INTEGER, + teaching_objective TEXT, + course_content TEXT, + attach_doc TEXT, + attach_video TEXT, + attach_model_link TEXT, + model_resource_ids TEXT, + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_course PRIMARY KEY (c_id) +); + +CREATE TABLE train_classroom_info ( + class_room_id VARCHAR(32) NOT NULL, + room_name VARCHAR(60) NOT NULL, + room_location VARCHAR(100), + capacity INTEGER, + room_status SMALLINT DEFAULT 1, + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_classroom_info PRIMARY KEY (class_room_id) +); + +CREATE TABLE train_training_site ( + site_id VARCHAR(32) NOT NULL, + site_name VARCHAR(80) NOT NULL, + site_location VARCHAR(100), + capacity INTEGER, + site_device TEXT, + site_status SMALLINT DEFAULT 1, + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_training_site PRIMARY KEY (site_id) +); + +CREATE TABLE train_class_plan ( + plan_id VARCHAR(32) NOT NULL, + plan_name VARCHAR(100) NOT NULL, + c_id VARCHAR(32) NOT NULL, + start_time TIMESTAMP NOT NULL, + end_time TIMESTAMP NOT NULL, + train_address VARCHAR(100), + t_id VARCHAR(32) NOT NULL, + recruit_num INTEGER, + apply_condition TEXT, + plan_status SMALLINT DEFAULT 0, + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_class_plan PRIMARY KEY (plan_id), + CONSTRAINT fk_classplan_course FOREIGN KEY (c_id) REFERENCES train_course (c_id) ON DELETE NO ACTION, + CONSTRAINT fk_classplan_teacher FOREIGN KEY (t_id) REFERENCES train_teacher (t_id) ON DELETE NO ACTION +); + +CREATE TABLE train_dormitory_info ( + dorm_id VARCHAR(32) NOT NULL, + dorm_building VARCHAR(50), + dorm_room VARCHAR(30) NOT NULL, + bed_total INTEGER, + bed_used INTEGER DEFAULT 0, + lock_device_sn VARCHAR(64), + dorm_status SMALLINT DEFAULT 1, + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_dormitory_info PRIMARY KEY (dorm_id), + CONSTRAINT uq_dorm_building_room UNIQUE (dorm_building, dorm_room) +); + +CREATE TABLE train_electronic_board ( + board_id VARCHAR(32) NOT NULL, + board_sn VARCHAR(64) NOT NULL, + bind_classroom_id VARCHAR(32), + bind_plan_id VARCHAR(32), + board_ip VARCHAR(50), + board_content VARCHAR(500), + board_playmode SMALLINT DEFAULT 1, + support_checkin_type VARCHAR(30), + board_status SMALLINT DEFAULT 1, + last_heartbeat TIMESTAMP, + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_electronic_board PRIMARY KEY (board_id), + CONSTRAINT uq_board_sn UNIQUE (board_sn), + CONSTRAINT fk_board_classroom FOREIGN KEY (bind_classroom_id) REFERENCES train_classroom_info (class_room_id) ON DELETE NO ACTION, + CONSTRAINT fk_board_plan FOREIGN KEY (bind_plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION +); + +CREATE TABLE train_dorm_auth ( + auth_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + student_id VARCHAR(32) NOT NULL, + plan_id VARCHAR(32) NOT NULL, + dorm_id VARCHAR(32) NOT NULL, + bed_no VARCHAR(20), + face_sync_status SMALLINT DEFAULT 0, + auth_start TIMESTAMP, + auth_end TIMESTAMP, + create_user VARCHAR(32) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + remark VARCHAR(200), + CONSTRAINT pk_train_dorm_auth PRIMARY KEY (auth_id), + CONSTRAINT uq_dormauth_stu_plan UNIQUE (student_id, plan_id), + CONSTRAINT fk_dormauth_stu FOREIGN KEY (student_id) REFERENCES train_student (s_id) ON DELETE NO ACTION, + CONSTRAINT fk_dormauth_plan FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION, + CONSTRAINT fk_dormauth_dorm FOREIGN KEY (dorm_id) REFERENCES train_dormitory_info (dorm_id) ON DELETE NO ACTION +); + +CREATE TABLE train_class_report ( + report_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + plan_id VARCHAR(32) NOT NULL, + student_id VARCHAR(32) NOT NULL, + report_way SMALLINT, + report_device_sn VARCHAR(64), + report_time TIMESTAMP, + report_status SMALLINT DEFAULT 0, + alloc_dorm_id VARCHAR(32), + alloc_classroom_id VARCHAR(32), + alloc_site_id VARCHAR(32), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT pk_train_class_report PRIMARY KEY (report_id), + CONSTRAINT uq_classreport_plan_stu UNIQUE (plan_id, student_id), + CONSTRAINT fk_classreport_plan FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION, + CONSTRAINT fk_classreport_stu FOREIGN KEY (student_id) REFERENCES train_student (s_id) ON DELETE NO ACTION, + CONSTRAINT fk_classreport_dorm FOREIGN KEY (alloc_dorm_id) REFERENCES train_dormitory_info (dorm_id) ON DELETE NO ACTION, + CONSTRAINT fk_classreport_classroom FOREIGN KEY (alloc_classroom_id) REFERENCES train_classroom_info (class_room_id) ON DELETE NO ACTION, + CONSTRAINT fk_classreport_site FOREIGN KEY (alloc_site_id) REFERENCES train_training_site (site_id) ON DELETE NO ACTION +); + +CREATE TABLE train_student_checkin ( + checkin_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + plan_id VARCHAR(32) NOT NULL, + c_id VARCHAR(32) NOT NULL, + student_id VARCHAR(32) NOT NULL, + board_id VARCHAR(32), + checkin_way SMALLINT, + checkin_time TIMESTAMP, + checkin_status SMALLINT, + lesson_start_time TIMESTAMP, + lesson_end_time TIMESTAMP, + CONSTRAINT pk_train_student_checkin PRIMARY KEY (checkin_id), + CONSTRAINT uq_checkin_lesson_stu UNIQUE (plan_id, c_id, student_id, lesson_start_time), + CONSTRAINT fk_checkin_plan FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION, + CONSTRAINT fk_checkin_course FOREIGN KEY (c_id) REFERENCES train_course (c_id) ON DELETE NO ACTION, + CONSTRAINT fk_checkin_stu FOREIGN KEY (student_id) REFERENCES train_student (s_id) ON DELETE NO ACTION, + CONSTRAINT fk_checkin_board FOREIGN KEY (board_id) REFERENCES train_electronic_board (board_id) ON DELETE NO ACTION +); + +CREATE TABLE train_study_duration ( + dur_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + student_id VARCHAR(32), + plan_id VARCHAR(32), + c_id VARCHAR(32), + device_type VARCHAR(50), + start_time TIMESTAMP, + end_time TIMESTAMP, + study_seconds INTEGER, + sync_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT pk_train_study_duration PRIMARY KEY (dur_id), + CONSTRAINT fk_studydur_stu FOREIGN KEY (student_id) REFERENCES train_student (s_id) ON DELETE NO ACTION, + CONSTRAINT fk_studydur_plan FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION, + CONSTRAINT fk_studydur_course FOREIGN KEY (c_id) REFERENCES train_course (c_id) ON DELETE NO ACTION +); + +CREATE TABLE train_student_portrait ( + portrait_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + student_id VARCHAR(32) NOT NULL, + plan_id VARCHAR(32) NOT NULL, + total_study_hour NUMERIC(8,2), + avg_score NUMERIC(5,2), + checkin_rate NUMERIC(5,2), + label_tag TEXT, + portrait_content TEXT, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT pk_train_student_portrait PRIMARY KEY (portrait_id), + CONSTRAINT uq_portrait_stu_plan UNIQUE (student_id, plan_id), + CONSTRAINT fk_portrait_stu FOREIGN KEY (student_id) REFERENCES train_student (s_id) ON DELETE NO ACTION, + CONSTRAINT fk_portrait_plan FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION +); + +-- 步骤 4: 重建 9 个历史触发器 +CREATE OR REPLACE TRIGGER trig_train_teacher_modify + BEFORE UPDATE ON train_teacher + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_student_modify + BEFORE UPDATE ON train_student + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_course_modify + BEFORE UPDATE ON train_course + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_classroom_modify + BEFORE UPDATE ON train_classroom_info + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_trainsite_modify + BEFORE UPDATE ON train_training_site + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_board_modify + BEFORE UPDATE ON train_electronic_board + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_dorminfo_modify + BEFORE UPDATE ON train_dormitory_info + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_classplan_modify + BEFORE UPDATE ON train_class_plan + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +CREATE OR REPLACE TRIGGER trig_train_dormauth_modify + BEFORE UPDATE ON train_dorm_auth + FOR EACH ROW EXECUTE FUNCTION func_train_update_modifytime(); + +-- 步骤 5: 从 bak_v1_* 物理快照还原数据 +INSERT INTO train_teacher SELECT * FROM bak_v1_train_teacher; +INSERT INTO train_student SELECT * FROM bak_v1_train_student; +INSERT INTO train_course SELECT * FROM bak_v1_train_course; +INSERT INTO train_classroom_info SELECT * FROM bak_v1_train_classroom_info; +INSERT INTO train_training_site SELECT * FROM bak_v1_train_training_site; +INSERT INTO train_class_plan SELECT * FROM bak_v1_train_class_plan; +INSERT INTO train_dormitory_info SELECT * FROM bak_v1_train_dormitory_info; +INSERT INTO train_electronic_board SELECT * FROM bak_v1_train_electronic_board; +INSERT INTO train_dorm_auth SELECT * FROM bak_v1_train_dorm_auth; +INSERT INTO train_class_report SELECT * FROM bak_v1_train_class_report; +INSERT INTO train_student_checkin SELECT * FROM bak_v1_train_student_checkin; +INSERT INTO train_study_duration SELECT * FROM bak_v1_train_study_duration; +INSERT INTO train_student_portrait SELECT * FROM bak_v1_train_student_portrait; + +-- 步骤 6: 校验教师唯一数据还原结果 +DO $$ +DECLARE + v_cnt INT; +BEGIN + SELECT count(*) INTO v_cnt FROM train_teacher; + IF v_cnt <> 1 THEN + RAISE EXCEPTION '>>> [Rollback-FATAL] 教师数据还原失败: 期望 1 行,实际为 % 行', v_cnt; + END IF; + RAISE NOTICE '>>> [Rollback-PASS] 13 张表结构与教师数据已严格依据 Phase 0.5 标准 DDL 恢复完毕。'; +END $$; + +COMMIT; diff --git a/RuoYi-Vue/sql/train/v2/rollback/003_restore_legacy_score.sql b/RuoYi-Vue/sql/train/v2/rollback/003_restore_legacy_score.sql new file mode 100644 index 000000000..e1294974b --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/rollback/003_restore_legacy_score.sql @@ -0,0 +1,37 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 回滚脚本: 003_restore_legacy_score.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: 严格基于 000_phase05_exact_schema.sql 结构恢复旧表 train_score_info +-- 事务边界: 全程在显式事务 BEGIN ... COMMIT 中执行 +-- 说明: 彻底摒弃 CTAS 伪造结构,严格恢复原字段、默认值、外键与唯一约束 +-- ============================================================================== + +BEGIN; + +DROP TABLE IF EXISTS train_score_info; + +-- 步骤 1: 依据 Phase 0.5 原始标准物理 DDL 重建表结构 +CREATE TABLE train_score_info ( + score_id BIGINT GENERATED BY DEFAULT AS IDENTITY, + plan_id VARCHAR(32), + c_id VARCHAR(32), + student_id VARCHAR(32), + course_score NUMERIC(5,2), + train_score NUMERIC(5,2), + exam_score NUMERIC(5,2), + source_type SMALLINT, + source_system VARCHAR(50), + import_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT pk_train_score_info PRIMARY KEY (score_id), + CONSTRAINT uq_scoreinfo_plan_c_stu UNIQUE (plan_id, c_id, student_id), + CONSTRAINT fk_scoreinfo_plan FOREIGN KEY (plan_id) REFERENCES train_class_plan (plan_id) ON DELETE NO ACTION, + CONSTRAINT fk_scoreinfo_course FOREIGN KEY (c_id) REFERENCES train_course (c_id) ON DELETE NO ACTION, + CONSTRAINT fk_scoreinfo_stu FOREIGN KEY (student_id) REFERENCES train_student (s_id) ON DELETE NO ACTION +); + +COMMENT ON TABLE train_score_info IS 'Phase 0.5 历史成绩表 (应急回滚精准恢复)'; + +-- 步骤 2: 从 bak_v1_train_score_info 快照表恢复数据 +INSERT INTO train_score_info SELECT * FROM bak_v1_train_score_info; + +COMMIT; diff --git a/RuoYi-Vue/sql/train/v2/rollback/README.md b/RuoYi-Vue/sql/train/v2/rollback/README.md new file mode 100644 index 000000000..334cd509b --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/rollback/README.md @@ -0,0 +1,91 @@ +# RailwaySystem 数据库 V2 应急回滚指南 (Round 1 修订版) + +> **技术基线**:KingbaseES V009R003C018 (`schema=public`) +> **回滚目标**:安全撤销 V2 数据库结构变更,将数据模型与物理约束 100% 精确还原至 Phase 0.5 历史基线。 + +--- + +## 1. 核心架构升级:解耦结构与数据 + +在 Round 1 审查中,彻底废除了使用 `CREATE TABLE ... AS SELECT * FROM bak_v1_*`(CTAS)推断历史结构的危险方案: + +```text +┌─────────────────────────────────────────────────────────┐ +│ Phase 0.5 精确物理结构 (表定义/列类型/PK/FK/UNIQUE/触发器) │ +│ 来源: rollback/000_phase05_exact_schema.sql │ +└───────────────────────────┬─────────────────────────────┘ + │ (先精确重建物理表结构) + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Phase 0.5 真实数据 (唯一教师行 + 各表数据) │ +│ 来源: 001 备份生成的 bak_v1_* 快照表 │ +└───────────────────────────┬─────────────────────────────┘ + │ (再通过 INSERT INTO 导入数据) + ▼ + 【Phase 0.5 绝对还原】 +``` + +- **`000_phase05_exact_schema.sql`**:作为唯一的物理结构基准,确保回滚后恢复所有 24 个旧外键、7 组旧业务唯一约束、NOT NULL、DEFAULT、IDENTITY 序列以及 9 个 `update_time` 触发器; +- **`bak_v1_*` 快照表**:仅承担纯粹的“数据暂存”职责。 + +--- + +## 2. 回滚脚本执行顺序 + +若在测试迁移或灰度验证阶段发现重大异常,需按照以下严格顺序执行回滚流水线: + +```text +步骤 1: 执行 001_rollback_new_tables.sql + -> 显式解除外键依赖,安全下线并 DROP 掉 V2 新增的 8 张业务表 + +步骤 2: 执行 002_rollback_modified_tables.sql + -> 基于 000_phase05_exact_schema.sql 的精确结构重建 13 张表,并从 bak_v1_* 物理快照还原数据,严格断言教师数据为 1 行 + +步骤 3: 执行 003_restore_legacy_score.sql + -> 基于 000_phase05_exact_schema.sql 的精确结构重建 train_score_info 及 3 个历史外键与唯一约束 + +步骤 4: 人工核验 Phase 0.5 状态 + -> 执行 000_preflight.sql 校验基线恢复,确认 14 张表、触发器与约束 100% 吻合 +``` + +--- + +## 3. 强前置依赖与数据来源声明 + +> [!CAUTION] +> **回滚操作必须满足以下前置条件:** + +1. **依赖 001 备份快照**: + - 步骤 2 与步骤 3 的数据还原依赖迁移前由 `001_backup_current_schema.sql` 生成的 `bak_v1_*` 快照表。 + - 若 `bak_v1_*` 表意外被删,**严禁盲目执行数据还原语句**。 +2. **唯一教师数据的物理还原**: + - 教师主档 `train_teacher` 的数据通过 `INSERT INTO train_teacher SELECT * FROM bak_v1_train_teacher;` 完整回灌。回滚脚本会严格校验恢复后的教师行数必须等于 1。 +3. **事务安全性**: + - 每一个回滚 SQL 文件内部均由显式 `BEGIN ... COMMIT` 包裹,出现任何错误自动整单回滚,不产生中间半回滚状态。 + +--- + +## 4. 必须人工介入确认的场景 + +### 场景 A:V2 阶段已产生新业务数据 +如果系统在执行 V2 DDL 之后,业务端或测试端已经在 `train_enrollment`、`train_lesson`、`train_student_score` 等表中写入了新数据: +- **直接执行回滚将导致这些新业务数据彻底物理清除!** +- **必须人工介入**:先通过数据导出工具将 V2 新增业务数据导出为 CSV/JSON 归档,确认业务无异议后方可执行回滚。 + +### 场景 B:bak_v1_* 备份表意外缺失 +若 `bak_v1_*` 表不存在: +- 必须暂停脚本执行; +- 从数据库物理备份集(如 `sys_backup` / 逻辑转储快照)中恢复 14 张旧表数据。 + +--- + +## 5. 回滚后验收确认 Checklist + +回滚完成后,在数据库客户端中直接运行 `000_preflight.sql` 即可自动化完成全量反向验收: + +- [ ] 1. 运行 `000_preflight.sql` 成功输出 `[Preflight-PASS]`; +- [ ] 2. 确认已无任何 V2 独有表(如 `train_plan_course` 等); +- [ ] 3. 确认旧表 `train_score_info` 已恢复,且具备旧外键; +- [ ] 4. 确认 `train_teacher` 精确包含 1 条数据,且旧字段 `t_department` 存在且非空; +- [ ] 5. 确认 9 个旧触发器全部重新挂载生效; +- [ ] 6. 确认系统表 `sys_*` 完好无损。