From 9a14641b9e6a8b9992cd3ecd87c4a9b058f054a8 Mon Sep 17 00:00:00 2001 From: wangzhibiao Date: Sat, 10 Oct 2026 11:41:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=EF=BC=9A=E6=8F=90=E4=BE=9B=E5=85=B1=E4=BA=AB=E5=BA=93=E5=A2=9E?= =?UTF-8?q?=E9=87=8F=E5=85=BC=E5=AE=B9=E8=A1=A5=E4=B8=81013=E4=B8=8E?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=E9=A2=84=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../patch/013_UPGRADE_GUIDE_AND_ROLLBACK.md | 141 ++++++++++ ...ot_training_schema_compatibility_patch.sql | 254 ++++++++++++++++++ 2 files changed, 395 insertions(+) create mode 100644 RuoYi-Vue/sql/train/v2/patch/013_UPGRADE_GUIDE_AND_ROLLBACK.md create mode 100644 RuoYi-Vue/sql/train/v2/patch/013_pilot_training_schema_compatibility_patch.sql diff --git a/RuoYi-Vue/sql/train/v2/patch/013_UPGRADE_GUIDE_AND_ROLLBACK.md b/RuoYi-Vue/sql/train/v2/patch/013_UPGRADE_GUIDE_AND_ROLLBACK.md new file mode 100644 index 000000000..95938a715 --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/patch/013_UPGRADE_GUIDE_AND_ROLLBACK.md @@ -0,0 +1,141 @@ +# SYFJGYD学员管理系统 — 共享测试数据库 training 升级方案与回滚预案 (Patch 013) + +## 一、概述 + +本方案面向共享测试环境数据库 `training` 的结构与数据兼容性升级,解决合并版本部署后因数据库缺失扩展列、教学资源菜单孤立、课程资源关联表自增序列未对齐导致系统启动或业务运行异常的问题。 + +- **目标数据库**:`training`(KingbaseES V009R003C018,Schema: `public`) +- **升级补丁文件**:`RuoYi-Vue/sql/train/v2/patch/013_pilot_training_schema_compatibility_patch.sql` +- **验证状态**:已在基于 `training` 物理模板完全克隆的独立验证库 `training_verify_20261010` 中完成 100% 自动化断言验证(10/10 PASS)。 + +--- + +## 二、操作前完整备份方式与有效性检查 + +### 1. 备份方式 +在对共享库 `training` 执行任何写操作前,必须在数据库服务器本地执行逻辑备份或基于模板的物理克隆: + +- **方式一:数据库模板快照(推荐,快且可即时验证)** + ```sql + -- 终止现有 training 活跃连接后立即创建时间戳只读备份库 + SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'training' AND pid <> pg_backend_pid(); + CREATE DATABASE training_backup_20261010 TEMPLATE training; + ``` +- **方式二:sys_dump 逻辑导出** + ```bash + sys_dump -U kingbase -h 127.0.0.1 -p 4321 -d training -F c -b -v -f /data2/backup/training_pre_patch013_20261010.dmp + ``` + +### 2. 备份有效性检查(严禁将“导出成功”误认为“可恢复”) +备份完成后的有效性验证标准: +1. **备份库可连通性与行数校验**: + - 连接 `training_backup_20261010`,执行 `SELECT count(*) FROM train_course` 确保返回 `31`。 + - 执行 `SELECT count(*) FROM train_student` 确保返回 `333`。 +2. **逻辑备份文件可恢复性探针**: + - 使用 `sys_restore --list /data2/backup/training_pre_patch013_20261010.dmp` 核对归档目录段(TOC)完整无损坏。 + +--- + +## 三、增量补丁执行顺序与内容 + +### 执行步骤 +1. **连接共享库**: + - 必须通过连接字符串显式确认当前数据库为 `training`,当前 Schema 为 `public`: + ```sql + SELECT current_database(), current_schema(); + ``` +2. **执行脚本**: + - 顺序执行 `013_pilot_training_schema_compatibility_patch.sql`。 + - 脚本全程受显式事务 `BEGIN ... COMMIT` 保护,任何语句报错将自动全量回滚。 + +### 核心变更内容 +1. **主数据物理列扩展(13个列)**: + - `train_student`: 补齐 `avatar`, `duty_level` + - `train_teacher`: 补齐 `avatar`, `major`, `courses`, `start_teaching` + - `train_classroom_info`: 补齐 `multimedia`, `projector`, `seat_arrange` + - `train_training_site`: 补齐 `major`, `seat_arrange` + - `train_course`: 补齐 `course_type`, `resource_id` +2. **资源类型字典与菜单挂载**: + - 补齐字典类型 `train_resource_type` (105) 及其 6 项字典值 (710~715)。 + - 动态识别并原地规范化卜老师已建的 `path='resource'`(menu_id=2266)为主菜单,避免重复创建 2270。 + - 动态挂载查询按钮 `2271` (`train:resource:list`) 与编辑按钮 `2272` (`train:resource:edit`) 至 2266 下。 + - 授予角色 1 (超级管理员) 与角色 3 (培训管理员) 对应权限。 +3. **关联表结构与自增机制**: + - 确保 `train_course_resource` 主键、外键与唯一索引完备。 + - 保留原 IDENTITY 机制,避免序列属主错误。 +4. **历史数据规范回填**: + - 将 4 门历史课程中的 `resource_id` 拆解并精确匹配到 `train_resource` 中已存在的真实教学资源(8:供电操作指导, 954:供电操作视频, 301:水暖维护, 79:岗前纪律)。 + - 精确生成 7 条规范关联记录,规整历史十六进制 UUID。 + - 同步对齐底层自增序列游标为 7,保证后续新增记录自增生成 `id >= 8`。 + +--- + +## 四、验证 SQL 与预期结果 + +升级完成后,按顺序执行以下 SQL 断言校验: + +```sql +-- 1. 验证课程资源关联回填行数 +SELECT count(*) FROM train_course_resource; +-- 预期结果: 7 + +-- 2. 验证跨表关联是否可查询到真实资源 +SELECT cr.id, c.c_name, r.resource_name, r.resource_type + FROM train_course_resource cr + JOIN train_course c ON c.c_id = cr.c_id + JOIN train_resource r ON r.resource_id = cr.resource_id + ORDER BY cr.id; +-- 预期结果: 7 条记录全部关联到真实课程名称与教学资源名称 + +-- 3. 验证新插入记录自增序列生成是否正常 +INSERT INTO train_course_resource (c_id, resource_id, sort_no, create_by) +VALUES ('6699f35fd8124e0e8d8668e4a81923b8', '79', 99, 'verify_probe') +RETURNING id; +-- 预期结果: 返回 id = 8 +-- 校验后清理: +DELETE FROM train_course_resource WHERE create_by = 'verify_probe'; + +-- 4. 验证菜单与按钮挂载结构 +SELECT menu_id, menu_name, parent_id, perms + FROM sys_menu + WHERE menu_id IN (2266, 2271, 2272) OR path = 'resource'; +-- 预期结果: 2266 为主菜单,2271/2272 的 parent_id 均等于 2266 + +-- 5. 验证核心主数据表数据量无损 +SELECT 'train_course' AS tbl, count(*) FROM train_course +UNION ALL SELECT 'train_resource', count(*) FROM train_resource +UNION ALL SELECT 'train_student', count(*) FROM train_student +UNION ALL SELECT 'train_teacher', count(*) FROM train_teacher; +-- 预期结果: 31, 4, 333, 28 (完全无损) +``` + +--- + +## 五、失败回滚与应急恢复预案 + +若在执行补丁过程中发生非预期中断或严重异常,按以下流程快速恢复: + +1. **事务级自动回滚**: + - 补丁全程在 `BEGIN ... COMMIT` 中,任何语法或运行时异常自动触发数据库事务回滚,数据库保持原状。 +2. **手动闪回/恢复预案**: + - 若执行已提交但业务验收不通过,从前置模板备份库恢复: + ```sql + SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'training' AND pid <> pg_backend_pid(); + DROP DATABASE training; + CREATE DATABASE training TEMPLATE training_backup_20261010; + ``` + +--- + +## 六、对卜老师现有数据与系统的影响评估 + +1. **主数据影响**: + - 仅对学员、教师、教室、实训基地表补充新字段,历史数据现有字段完全原样保留,现有 333 名学员、28 名教师数据零影响。 +2. **课程与教学资源影响**: + - 4 门历史课程从只读逗号字符串平滑提升为标准多对多关联; + - `train_course.resource_id` 物理列保留且清空脏字符串占位,旧系统接口只读不受阻断; + - 卜老师已创建的 4 条教学资源记录完全保留且与课程建立有效关联。 +3. **系统菜单与权限影响**: + - 卜老师已创建的菜单 2266 得到规范化(补全名称与前端组件),未改动其已配好的角色权限,并新增了配套的查询与编辑按钮权限。 +4. **8085 RC1 独立环境影响**: + - 8085 环境连接独立的 `railwaysystem_pilot_20261010` 数据库,与共享库 `training` 完全物理隔离,不受本升级的任何影响。 diff --git a/RuoYi-Vue/sql/train/v2/patch/013_pilot_training_schema_compatibility_patch.sql b/RuoYi-Vue/sql/train/v2/patch/013_pilot_training_schema_compatibility_patch.sql new file mode 100644 index 000000000..46624c087 --- /dev/null +++ b/RuoYi-Vue/sql/train/v2/patch/013_pilot_training_schema_compatibility_patch.sql @@ -0,0 +1,254 @@ +-- ============================================================================== +-- RailwaySystem 数据库 V2 增量兼容补丁: 013_pilot_training_schema_compatibility_patch.sql +-- 适用数据库: KingbaseES V009R003C018 (schema: public) +-- 执行目标: +-- 面向共享试用数据库 training 的半成品状态执行精确补齐: +-- 1. 补齐 011 缺失的主数据字段 (学员岗位级别、教师主讲课程与从教时间、教室多媒体与投影、实训场地适用专业与工位) +-- 2. 补齐 011 教学资源字典 (train_resource_type) 与菜单权限 (无缝适配已有菜单 2266 或新建 2270,补齐 2271/2272 权限与角色绑定) +-- 3. 确保 train_course_resource 自增机制与约束完整 (支持 IDENTITY 自动探测与游标对齐) +-- 4. 幂等回填历史 train_course.resource_id 关联关系 (精确生成 7 条有效关联,过滤非法孤儿UUID),并对齐自增序列游标 +-- 事务保证: 全程显式事务 BEGIN ... COMMIT +-- 幂等保证: 包含所有 IF NOT EXISTS / WHERE NOT EXISTS 守卫,支持安全重复执行 +-- ============================================================================== + +BEGIN; + +-- ============================================================================= +-- 步骤 1: 补齐 011 缺失的主数据扩展列 +-- ============================================================================= + +-- 1.1 学员表 train_student 补齐岗位级别 (avatar 早期已有,加守卫兼容) +ALTER TABLE public.train_student ADD COLUMN IF NOT EXISTS avatar VARCHAR(255); +ALTER TABLE public.train_student ADD COLUMN IF NOT EXISTS duty_level VARCHAR(64); +COMMENT ON COLUMN public.train_student.avatar IS '学员头像图片相对路径'; +COMMENT ON COLUMN public.train_student.duty_level IS '学员岗位级别'; + +-- 1.2 教师表 train_teacher 补齐主讲课程描述 与 从教时间 (avatar, major 早期已有) +ALTER TABLE public.train_teacher ADD COLUMN IF NOT EXISTS avatar VARCHAR(255); +ALTER TABLE public.train_teacher ADD COLUMN IF NOT EXISTS major VARCHAR(100); +ALTER TABLE public.train_teacher ADD COLUMN IF NOT EXISTS courses VARCHAR(255); +ALTER TABLE public.train_teacher ADD COLUMN IF NOT EXISTS start_teaching DATE; +COMMENT ON COLUMN public.train_teacher.avatar IS '教师头像图片相对路径'; +COMMENT ON COLUMN public.train_teacher.major IS '所属专业/研究方向'; +COMMENT ON COLUMN public.train_teacher.courses IS '主讲课程描述'; +COMMENT ON COLUMN public.train_teacher.start_teaching IS '从教时间/起教日期'; + +-- 1.3 教室表 train_classroom_info 补齐多媒体、投影、工位/座位布局 +ALTER TABLE public.train_classroom_info ADD COLUMN IF NOT EXISTS multimedia SMALLINT DEFAULT 0; +ALTER TABLE public.train_classroom_info ADD COLUMN IF NOT EXISTS projector SMALLINT DEFAULT 0; +ALTER TABLE public.train_classroom_info ADD COLUMN IF NOT EXISTS seat_arrange VARCHAR(100); +COMMENT ON COLUMN public.train_classroom_info.multimedia IS '是否具备多媒体设备 (0:无, 1:有)'; +COMMENT ON COLUMN public.train_classroom_info.projector IS '是否具备投影仪 (0:无, 1:有)'; +COMMENT ON COLUMN public.train_classroom_info.seat_arrange IS '座位布局形式'; + +-- 1.4 实训场地表 train_training_site 补齐适用专业 与 工位布局 +ALTER TABLE public.train_training_site ADD COLUMN IF NOT EXISTS major VARCHAR(100); +ALTER TABLE public.train_training_site ADD COLUMN IF NOT EXISTS seat_arrange VARCHAR(100); +COMMENT ON COLUMN public.train_training_site.major IS '适用专业/实训工种'; +COMMENT ON COLUMN public.train_training_site.seat_arrange IS '工位布局形式'; + +-- 1.5 课程表 train_course 补齐课程类型 (resource_id 物理列保留) +ALTER TABLE public.train_course ADD COLUMN IF NOT EXISTS course_type VARCHAR(64); +ALTER TABLE public.train_course ADD COLUMN IF NOT EXISTS resource_id TEXT; +COMMENT ON COLUMN public.train_course.course_type IS '课程类型'; +COMMENT ON COLUMN public.train_course.resource_id IS '历史关联教学资源ID(兼容只读保留)'; + + +-- ============================================================================= +-- 步骤 2: 补齐教学资源类型字典与菜单权限 (智能适配已有 2266 或新 2270) +-- ============================================================================= + +-- 2.1 教学资源类型字典 +INSERT INTO public.sys_dict_type (dict_id, dict_name, dict_type, status, create_by, create_time, remark) +SELECT 105, '教学资源类型', 'train_resource_type', '0', 'admin', CURRENT_TIMESTAMP, '资源载体分类 (1:文档, 2:视频, 3:3D模型, 4:VR资源, 5:外部教学链接, 6:其他)' +WHERE NOT EXISTS (SELECT 1 FROM public.sys_dict_type WHERE dict_type = 'train_resource_type'); + +INSERT INTO public.sys_dict_data (dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT 710, 1, '文档', '1', 'train_resource_type', '', 'primary', 'Y', '0', 'admin', CURRENT_TIMESTAMP, 'PDF/Word/PPT等电子课件' +WHERE NOT EXISTS (SELECT 1 FROM public.sys_dict_data WHERE dict_type = 'train_resource_type' AND dict_value = '1'); + +INSERT INTO public.sys_dict_data (dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT 711, 2, '视频', '2', 'train_resource_type', '', 'info', 'N', '0', 'admin', CURRENT_TIMESTAMP, 'MP4录播视频' +WHERE NOT EXISTS (SELECT 1 FROM public.sys_dict_data WHERE dict_type = 'train_resource_type' AND dict_value = '2'); + +INSERT INTO public.sys_dict_data (dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT 712, 3, '3D模型', '3', 'train_resource_type', '', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '3D数字化实物机车/工务模型' +WHERE NOT EXISTS (SELECT 1 FROM public.sys_dict_data WHERE dict_type = 'train_resource_type' AND dict_value = '3'); + +INSERT INTO public.sys_dict_data (dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT 713, 4, 'VR资源', '4', 'train_resource_type', '', 'success', 'N', '0', 'admin', CURRENT_TIMESTAMP, '虚拟仿真头盔或交互式实训课件' +WHERE NOT EXISTS (SELECT 1 FROM public.sys_dict_data WHERE dict_type = 'train_resource_type' AND dict_value = '4'); + +INSERT INTO public.sys_dict_data (dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT 714, 5, '外部教学链接', '5', 'train_resource_type', '', 'danger', 'N', '0', 'admin', CURRENT_TIMESTAMP, '第三方外部实训/考试网址URL' +WHERE NOT EXISTS (SELECT 1 FROM public.sys_dict_data WHERE dict_type = 'train_resource_type' AND dict_value = '5'); + +INSERT INTO public.sys_dict_data (dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT 715, 6, '其他', '6', 'train_resource_type', '', 'default', 'N', '0', 'admin', CURRENT_TIMESTAMP, '其他类型教学资料' +WHERE NOT EXISTS (SELECT 1 FROM public.sys_dict_data WHERE dict_type = 'train_resource_type' AND dict_value = '6'); + +-- 2.2 规范化教学资源菜单与按钮权限 (动态绑定真实父级菜单 ID) +DO $$ +DECLARE + v_target_menu_id BIGINT; +BEGIN + SELECT menu_id INTO v_target_menu_id FROM public.sys_menu WHERE path = 'resource' LIMIT 1; + + IF v_target_menu_id IS NOT NULL THEN + UPDATE public.sys_menu + SET menu_name = '教学资源', + component = 'train/resource/index', + perms = 'train:resource:list', + order_num = 3, + parent_id = 2162, + visible = '0', + status = '0', + icon = 'documentation' + WHERE menu_id = v_target_menu_id; + ELSE + v_target_menu_id := 2270; + INSERT INTO public.sys_menu + (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) + VALUES + (2270, '教学资源', 2162, 3, 'resource', 'train/resource/index', '', '1', '0', 'C', '0', '0', 'train:resource:list', 'documentation', 'admin', CURRENT_TIMESTAMP, '教学资源管理菜单'); + END IF; + + -- 2.3 教学资源查询按钮 (menu_id = 2271) + IF NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2271) THEN + INSERT INTO public.sys_menu + (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) + VALUES + (2271, '教学资源查询', v_target_menu_id, 1, '#', '', '', 1, 0, 'F', '0', '0', 'train:resource:list', '#', 'admin', CURRENT_TIMESTAMP, '教学资源查询'); + ELSE + UPDATE public.sys_menu SET parent_id = v_target_menu_id WHERE menu_id = 2271; + END IF; + + -- 2.4 教学资源编辑按钮 (menu_id = 2272) + IF NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2272) THEN + INSERT INTO public.sys_menu + (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) + VALUES + (2272, '教学资源编辑', v_target_menu_id, 2, '#', '', '', 1, 0, 'F', '0', '0', 'train:resource:edit', '#', 'admin', CURRENT_TIMESTAMP, '教学资源新增/修改/删除'); + ELSE + UPDATE public.sys_menu SET parent_id = v_target_menu_id WHERE menu_id = 2272; + END IF; + + -- 2.5 角色授权 (role_id = 3 培训管理员 与 role_id = 1 超级管理员) + INSERT INTO public.sys_role_menu (role_id, menu_id) + SELECT 3, v_target_menu_id WHERE NOT EXISTS (SELECT 1 FROM public.sys_role_menu WHERE role_id = 3 AND menu_id = v_target_menu_id); + INSERT INTO public.sys_role_menu (role_id, menu_id) + SELECT 3, 2271 WHERE NOT EXISTS (SELECT 1 FROM public.sys_role_menu WHERE role_id = 3 AND menu_id = 2271); + INSERT INTO public.sys_role_menu (role_id, menu_id) + SELECT 3, 2272 WHERE NOT EXISTS (SELECT 1 FROM public.sys_role_menu WHERE role_id = 3 AND menu_id = 2272); + + INSERT INTO public.sys_role_menu (role_id, menu_id) + SELECT 1, v_target_menu_id WHERE NOT EXISTS (SELECT 1 FROM public.sys_role_menu WHERE role_id = 1 AND menu_id = v_target_menu_id); + INSERT INTO public.sys_role_menu (role_id, menu_id) + SELECT 1, 2271 WHERE NOT EXISTS (SELECT 1 FROM public.sys_role_menu WHERE role_id = 1 AND menu_id = 2271); + INSERT INTO public.sys_role_menu (role_id, menu_id) + SELECT 1, 2272 WHERE NOT EXISTS (SELECT 1 FROM public.sys_role_menu WHERE role_id = 1 AND menu_id = 2272); +END $$; + + +-- ============================================================================= +-- 步骤 3: 确保 train_course_resource 结构与自增机制完整 +-- ============================================================================= + +-- 3.1 确保主表存在 +CREATE TABLE IF NOT EXISTS public.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) +); + +-- 3.2 补齐并绑定自增序列 (仅当该表非 IDENTITY 且无默认值时启用 fallback 序列绑定) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_attribute + WHERE attrelid = 'public.train_course_resource'::regclass + AND attname = 'id' + AND attidentity IN ('a', 'd') + ) AND NOT EXISTS ( + SELECT 1 FROM pg_attrdef + WHERE adrelid = 'public.train_course_resource'::regclass + AND adnum = (SELECT attnum FROM pg_attribute WHERE attrelid = 'public.train_course_resource'::regclass AND attname = 'id') + ) THEN + IF NOT EXISTS (SELECT 1 FROM pg_class WHERE relkind = 'S' AND relname = 'train_course_resource_id_seq') THEN + CREATE SEQUENCE public.train_course_resource_id_seq; + END IF; + ALTER SEQUENCE public.train_course_resource_id_seq OWNED BY public.train_course_resource.id; + ALTER TABLE public.train_course_resource ALTER COLUMN id SET DEFAULT nextval('public.train_course_resource_id_seq'); + END IF; +END $$; + +-- 3.3 补齐唯一索引与常规索引 +CREATE UNIQUE INDEX IF NOT EXISTS uq_course_resource + ON public.train_course_resource (c_id, resource_id); +CREATE INDEX IF NOT EXISTS idx_course_resource_course + ON public.train_course_resource (c_id); +CREATE INDEX IF NOT EXISTS idx_course_resource_resource + ON public.train_course_resource (resource_id); + +-- 3.4 补齐外键约束 +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_cr_course') THEN + ALTER TABLE public.train_course_resource + ADD CONSTRAINT fk_cr_course FOREIGN KEY (c_id) + REFERENCES public.train_course (c_id); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_cr_resource') THEN + ALTER TABLE public.train_course_resource + ADD CONSTRAINT fk_cr_resource FOREIGN KEY (resource_id) + REFERENCES public.train_resource (resource_id); + END IF; +END $$; + + +-- ============================================================================= +-- 步骤 4: 幂等回填历史课程资源关系并对齐序列 +-- ============================================================================= + +DO $$ +BEGIN + -- 仅当尚未回填过历史数据时执行 + IF NOT EXISTS (SELECT 1 FROM public.train_course_resource WHERE create_by = 'migration_patch_013') THEN + INSERT INTO public.train_course_resource + (c_id, resource_id, sort_no, create_by, create_time) + SELECT legacy.c_id, + CAST(r.resource_id AS VARCHAR), + legacy.sort_no, + 'migration_patch_013', + CURRENT_TIMESTAMP + FROM ( + SELECT c.c_id, + trim(parts.resource_id) AS resource_id, + CAST(parts.ordinality - 1 AS INTEGER) AS sort_no + FROM public.train_course c + CROSS JOIN LATERAL regexp_split_to_table(c.resource_id, ',') + WITH ORDINALITY AS parts(resource_id, ordinality) + WHERE c.resource_id IS NOT NULL + AND trim(c.resource_id) <> '' + ) legacy + JOIN public.train_resource r + ON r.resource_id = legacy.resource_id + AND r.del_flag = '0' + WHERE legacy.resource_id <> '' + ON CONFLICT (c_id, resource_id) DO NOTHING; + + -- 清空旧有历史字段以防重复恢复,物理字段保留 + UPDATE public.train_course SET resource_id = NULL WHERE resource_id IS NOT NULL; + END IF; + + -- 对齐序列生成器游标 (同时兼容 IDENTITY 和 普通序列) + IF pg_get_serial_sequence('public.train_course_resource', 'id') IS NOT NULL THEN + PERFORM setval(pg_get_serial_sequence('public.train_course_resource', 'id'), COALESCE((SELECT MAX(id) FROM public.train_course_resource), 1)); + END IF; +END $$; + +COMMIT;