-- 生产专用:修复“biz_tunnel 已存在、日期已升级、关联业务表仍为旧结构”的状态。 -- 依据用户提供的 shierfeng-fastapi / MySQL 8.0.46 SHOW CREATE TABLE;不是通用幂等迁移。 -- 仅生成,未在本地或生产执行。必须先停写、完整备份、在隔离恢复实例试跑,并经 DBA 审核。 -- 保留现有 biz_tunnel(包括 ORM 生成的外键/索引名称)和所有物料日期,不重建表、不补造历史。 -- 本补丁成功后:20261008_shipment_receipt_mysql.sql -> 20261008_usage_fifo_mysql.sql。 -- 不执行原 20261005_tunnel_material_flow_mysql.sql,也不重复执行日期/全量初始化脚本。 -- 在同一连接中显式 SET @shierfeng_prod_migration_approved = 1 后才能改业务表。 -- 该变量是人工确认门,不是环境识别器;同名开发库也必须禁止执行本交付。 -- 使用 mysql 批处理、禁用 --force;任一错误立即停止,不启动应用、不盲目重跑。 -- DDL 隐式提交,异常 handler 的 ROLLBACK 只能回滚菜单 DML,不能回滚之前的 ALTER。 DELIMITER $$ CREATE PROCEDURE repair_prod_existing_tunnel_20261008() BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; IF COALESCE(@shierfeng_prod_migration_approved, 0) <> 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Production maintenance approval and verified backup are required'; END IF; IF DATABASE() IS NULL OR DATABASE() <> 'shierfeng-fastapi' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Select the reviewed shierfeng-fastapi database on the production server'; END IF; IF SUBSTRING_INDEX(VERSION(), '.', 1) <> '8' OR (CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(VERSION(), '.', 2), '.', -1) AS UNSIGNED) = 0 AND CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(VERSION(), '.', 3), '.', -1) AS UNSIGNED) < 16) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'MySQL 8.0.16 or newer MySQL 8 is required'; END IF; -- 所有前置检查必须在首个业务表 ALTER 之前;不修改不符合快照的数据库。 IF (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND engine = 'InnoDB' AND table_name IN ('biz_tunnel', 'biz_test_batch', 'biz_customer_site', 'biz_usage_record', 'biz_return_item', 'biz_pallet_inventory', 'biz_pallet_event', 'biz_filling_record', 'biz_shipment_order', 'sys_menu', 'sys_role_menu', 'sys_user')) <> 12 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Required reviewed InnoDB tables are missing'; END IF; IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'biz_tunnel' AND column_name IN ('tunnel_id', 'test_batch_id', 'site_id', 'tunnel_code', 'tunnel_name', 'current_trace_no', 'current_trace_started_at', 'status', 'create_by', 'create_time', 'update_by', 'update_time', 'remark')) <> 13 OR NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'biz_tunnel' AND column_name = 'tunnel_id' AND column_type = 'bigint' AND is_nullable = 'NO') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Existing tunnel table does not match the reviewed structure'; END IF; -- 只校验外键语义,不要求 ORM 外键名等于原手写脚本的名称。 IF NOT EXISTS (SELECT 1 FROM information_schema.key_column_usage WHERE table_schema = DATABASE() AND table_name = 'biz_tunnel' AND column_name = 'site_id' AND referenced_table_schema = DATABASE() AND referenced_table_name = 'biz_customer_site' AND referenced_column_name = 'site_id') OR NOT EXISTS (SELECT 1 FROM information_schema.key_column_usage WHERE table_schema = DATABASE() AND table_name = 'biz_tunnel' AND column_name = 'test_batch_id' AND referenced_table_schema = DATABASE() AND referenced_table_name = 'biz_test_batch' AND referenced_column_name = 'batch_id') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Existing tunnel foreign keys must be reviewed'; END IF; IF NOT EXISTS (SELECT index_name FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'biz_tunnel' AND non_unique = 0 GROUP BY index_name HAVING GROUP_CONCAT(column_name ORDER BY seq_in_index) = 'site_id,tunnel_code') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Existing tunnel site and code unique key is missing'; END IF; IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'biz_filling_record' AND column_name IN ('production_date', 'expiry_date') AND data_type = 'date' AND is_nullable = 'YES') <> 2 OR NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema = DATABASE() AND table_name = 'biz_filling_record' AND constraint_name = 'ck_biz_filling_record_material_dates' AND constraint_type = 'CHECK' AND enforced = 'YES') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'The reviewed material date migration must already be complete'; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'biz_usage_record' AND column_name = 'line_id' AND column_type = 'bigint' AND is_nullable = 'NO') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Usage line_id is not in the reviewed pre-migration state'; END IF; IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND ((table_name = 'biz_usage_record' AND column_name IN ('tunnel_id', 'trace_no', 'batch_no_snapshot')) OR (table_name = 'biz_return_item' AND column_name IN ('tunnel_id_snapshot', 'trace_no_snapshot', 'batch_no_snapshot')) OR (table_name = 'biz_pallet_inventory' AND column_name IN ('tunnel_id', 'trace_no', 'site_arrived_at')) OR (table_name = 'biz_pallet_event' AND column_name IN ('before_tunnel_id', 'after_tunnel_id', 'trace_no', 'batch_no_snapshot')) OR (table_name = 'biz_shipment_order' AND column_name IN ('receipt_state', 'received_by', 'received_at', 'receipt_idempotency_key', 'receipt_request_hash')) OR (table_name IN ('biz_usage_record', 'biz_pallet_event') AND column_name IN ('fifo_skip_reason', 'fifo_recommendation')))) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Target columns already exist: partial or completed migration; do not rerun'; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.check_constraints WHERE constraint_schema = DATABASE() AND constraint_name = 'ck_biz_pallet_event_event_type' AND UPPER(check_clause) LIKE '%USAGE_RECORDED%' AND UPPER(check_clause) NOT LIKE '%TUNNEL_ENTERED%' AND UPPER(check_clause) NOT LIKE '%SHIPMENT_RECEIVED%') OR NOT EXISTS (SELECT 1 FROM information_schema.check_constraints WHERE constraint_schema = DATABASE() AND constraint_name = 'ck_biz_pallet_inventory_flow_state' AND UPPER(check_clause) LIKE '%AT_SITE_UNUSED%' AND UPPER(check_clause) NOT LIKE '%IN_TRANSIT%') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Event or inventory CHECK differs from the reviewed legacy state'; END IF; IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema = DATABASE() AND constraint_name IN ('fk_biz_usage_record_tunnel', 'fk_biz_return_item_tunnel', 'fk_biz_pallet_inventory_tunnel', 'fk_biz_pallet_event_before_tunnel', 'fk_biz_pallet_event_after_tunnel', 'uq_biz_shipment_receipt_site_key', 'fk_biz_shipment_order_received_by', 'ck_biz_shipment_order_receipt_state')) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'New constraint names already exist; review schema before migration'; END IF; IF EXISTS (SELECT 1 FROM information_schema.routines WHERE routine_schema = DATABASE() AND routine_name IN ('migrate_shipment_receipt_20261008', 'migrate_usage_fifo_20261008')) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'A previous receipt or FIFO migration routine remains; inspect before proceeding'; END IF; IF EXISTS (SELECT 1 FROM information_schema.triggers WHERE trigger_schema = DATABASE() AND event_object_table IN ('biz_usage_record', 'biz_return_item', 'biz_pallet_inventory', 'biz_pallet_event', 'biz_shipment_order', 'sys_menu')) OR EXISTS (SELECT 1 FROM information_schema.events WHERE event_schema = DATABASE() AND status = 'ENABLED') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Custom triggers or enabled events require separate DBA review'; END IF; IF NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 121 AND parent_id = 0 AND menu_type = 'M') OR NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 510 AND parent_id = 122 AND menu_type = 'C' AND perms = 'business:shipment:list') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Existing parent menus do not match the reviewed deployment'; END IF; IF EXISTS (SELECT 1 FROM sys_menu WHERE menu_id IN (520, 1116, 1117, 1118, 1119) OR perms IN ('master:tunnel:list', 'master:tunnel:query', 'master:tunnel:add', 'master:tunnel:edit', 'business:shipment:receive')) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Tunnel or receipt menu ID/permission conflict; do not overwrite'; END IF; IF EXISTS (SELECT 1 FROM sys_role_menu WHERE menu_id IN (520, 1116, 1117, 1118, 1119)) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Orphan role grants would become active; review before inserting menus'; END IF; -- BEGIN DDL ALTER TABLE biz_usage_record ADD COLUMN tunnel_id BIGINT NULL COMMENT '隧道ID;历史记录可能为空' AFTER site_id, ADD COLUMN trace_no VARCHAR(64) NULL COMMENT '隧道物料内部溯源号;历史记录可能为空' AFTER tunnel_id, ADD COLUMN batch_no_snapshot VARCHAR(64) NULL COMMENT '生产批次快照;历史记录可能为空' AFTER trace_no, MODIFY COLUMN line_id BIGINT NULL COMMENT '产线ID(可选)', ADD CONSTRAINT fk_biz_usage_record_tunnel FOREIGN KEY (tunnel_id) REFERENCES biz_tunnel (tunnel_id); ALTER TABLE biz_return_item ADD COLUMN tunnel_id_snapshot BIGINT NULL COMMENT '申请时隧道ID快照' AFTER active_filling_id_snapshot, ADD COLUMN trace_no_snapshot VARCHAR(64) NULL COMMENT '申请时物料溯源号快照' AFTER tunnel_id_snapshot, ADD COLUMN batch_no_snapshot VARCHAR(64) NULL COMMENT '申请时生产批次快照' AFTER trace_no_snapshot, ADD CONSTRAINT fk_biz_return_item_tunnel FOREIGN KEY (tunnel_id_snapshot) REFERENCES biz_tunnel (tunnel_id); ALTER TABLE biz_pallet_inventory ADD COLUMN tunnel_id BIGINT NULL COMMENT '当前隧道ID' AFTER site_id, ADD COLUMN trace_no VARCHAR(64) NULL COMMENT '当前隧道物料溯源号' AFTER tunnel_id, ADD CONSTRAINT fk_biz_pallet_inventory_tunnel FOREIGN KEY (tunnel_id) REFERENCES biz_tunnel (tunnel_id); ALTER TABLE biz_pallet_event ADD COLUMN before_tunnel_id BIGINT NULL COMMENT '变更前隧道ID' AFTER before_site_id, ADD COLUMN after_tunnel_id BIGINT NULL COMMENT '变更后隧道ID' AFTER after_site_id, ADD COLUMN trace_no VARCHAR(64) NULL COMMENT '事件关联物料溯源号' AFTER after_remaining_packages, ADD COLUMN batch_no_snapshot VARCHAR(64) NULL COMMENT '事件生产批次快照' AFTER trace_no, ADD CONSTRAINT fk_biz_pallet_event_before_tunnel FOREIGN KEY (before_tunnel_id) REFERENCES biz_tunnel (tunnel_id), ADD CONSTRAINT fk_biz_pallet_event_after_tunnel FOREIGN KEY (after_tunnel_id) REFERENCES biz_tunnel (tunnel_id); ALTER TABLE biz_pallet_event DROP CHECK ck_biz_pallet_event_event_type, ADD CONSTRAINT ck_biz_pallet_event_event_type CHECK ( event_type IN ('INBOUND_CONFIRMED', 'FILLING_CONFIRMED', 'SHIPMENT_CONFIRMED', 'TUNNEL_ENTERED', 'USAGE_RECORDED', 'RETURN_REQUESTED', 'RETURN_CONFIRMED') ); -- END DDL -- 菜单只新增已核实空闲的 ID;不修改 sys_role_menu、不自动授予普通角色。 START TRANSACTION; -- BEGIN MENU INSERT INTO sys_menu ( menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark ) VALUES (520, '隧道资料', 121, 7, 'tunnel', 'master/tunnel/index', '', '', 1, 0, 'C', '0', '0', 'master:tunnel:list', '#', 'admin', SYSDATE(), '', NULL, '隧道资料菜单'), (1116, '隧道查询', 520, 1, '#', '', '', '', 1, 0, 'F', '0', '0', 'master:tunnel:query', '#', 'admin', SYSDATE(), '', NULL, ''), (1117, '隧道新增', 520, 2, '#', '', '', '', 1, 0, 'F', '0', '0', 'master:tunnel:add', '#', 'admin', SYSDATE(), '', NULL, ''), (1118, '隧道修改', 520, 3, '#', '', '', '', 1, 0, 'F', '0', '0', 'master:tunnel:edit', '#', 'admin', SYSDATE(), '', NULL, ''); -- END MENU COMMIT; END$$ DELIMITER ; CALL repair_prod_existing_tunnel_20261008(); DROP PROCEDURE repair_prod_existing_tunnel_20261008; SET @shierfeng_prod_migration_approved = NULL; -- 返回12个新增隧道字段、line_id=YES及4行隧道菜单;随后仍须执行收货/FIFO两份补丁。 SELECT table_name, column_name, column_type, is_nullable FROM information_schema.columns WHERE table_schema = DATABASE() AND ((table_name = 'biz_usage_record' AND column_name IN ('tunnel_id', 'trace_no', 'batch_no_snapshot', 'line_id')) OR (table_name = 'biz_return_item' AND column_name IN ('tunnel_id_snapshot', 'trace_no_snapshot', 'batch_no_snapshot')) OR (table_name = 'biz_pallet_inventory' AND column_name IN ('tunnel_id', 'trace_no')) OR (table_name = 'biz_pallet_event' AND column_name IN ('before_tunnel_id', 'after_tunnel_id', 'trace_no', 'batch_no_snapshot'))) ORDER BY table_name, ordinal_position; SELECT menu_id, parent_id, perms FROM sys_menu WHERE menu_id IN (520, 1116, 1117, 1118) ORDER BY menu_id;