-- ============================================================================== -- 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 $$;