SET NAMES utf8mb4; USE baiyue_firstleg; /* * 三期主子入仓单结构迁移。 * 仅建立结构和数据库保护,不自动拆分或回填存量业务数据。 * 新写入采用“来源单承接实装、仅创建余量子单”;LOADED 子单和 loaded_child_inbound_id * 仅为历史双子单读取保留,不删除、不清洗存量记录。 * tenant_id 继续表示客户隔离键;数量统一使用 package_count。 */ DROP PROCEDURE IF EXISTS phase3_inbound_add_column; DELIMITER $$ CREATE PROCEDURE phase3_inbound_add_column( IN p_table_name VARCHAR(64), IN p_column_name VARCHAR(64), IN p_definition TEXT ) BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = p_table_name AND column_name = p_column_name ) THEN SET @ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD COLUMN `', p_column_name, '` ', p_definition); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; END IF; END$$ DELIMITER ; CALL phase3_inbound_add_column( 'fl_inbound_order', 'inbound_role', "VARCHAR(16) NOT NULL DEFAULT 'MASTER' COMMENT '入仓单层级:MASTER主单、CHILD子单' AFTER inbound_code"); CALL phase3_inbound_add_column( 'fl_inbound_order', 'parent_inbound_id', "BIGINT UNSIGNED NULL COMMENT '直属主入仓单ID;主单为空' AFTER inbound_role"); CALL phase3_inbound_add_column( 'fl_inbound_order', 'root_inbound_id', "BIGINT UNSIGNED NULL COMMENT '根主入仓单ID;存量主单可为空,按自身ID解释' AFTER parent_inbound_id"); CALL phase3_inbound_add_column( 'fl_inbound_order', 'split_sequence', "INT UNSIGNED NULL COMMENT '同一主单下的子单顺序号' AFTER root_inbound_id"); CALL phase3_inbound_add_column( 'fl_inbound_order', 'split_status', "VARCHAR(24) NOT NULL DEFAULT 'NOT_SPLIT' COMMENT '拆分状态:NOT_SPLIT、PARTIALLY_SPLIT、FULLY_SPLIT' AFTER split_sequence"); CALL phase3_inbound_add_column( 'fl_inbound_order', 'package_count', "INT UNSIGNED NULL COMMENT '入仓单箱数;三期唯一数量口径' AFTER total_package_count"); CALL phase3_inbound_add_column( 'fl_inbound_order', 'split_purpose', "VARCHAR(16) NOT NULL DEFAULT 'MASTER' COMMENT '履约分片用途:MASTER、LOADED、REMAINDER' AFTER package_count"); CALL phase3_inbound_add_column( 'fl_inbound_order', 'master_order_id', "BIGINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN inbound_role = 'MASTER' AND deleted = 0 THEN order_id ELSE NULL END) STORED COMMENT '仅用于约束有效主单订单唯一性'"); -- 旧实现只生成未装余量子单,可无歧义映射为 REMAINDER。 UPDATE fl_inbound_order SET split_purpose = 'REMAINDER' WHERE inbound_role = 'CHILD' AND split_purpose = 'MASTER'; CALL phase3_inbound_add_column( 'fl_inbound_order', 'active_remainder_root_id', "BIGINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN inbound_role = 'CHILD' AND split_purpose = 'REMAINDER' AND split_status = 'NOT_SPLIT' AND deleted = 0 THEN root_inbound_id ELSE NULL END) STORED COMMENT '有效余量唯一约束键;历史已完成分片为空'"); DROP PROCEDURE IF EXISTS phase3_inbound_add_column; DROP PROCEDURE IF EXISTS phase3_inbound_add_index; DELIMITER $$ CREATE PROCEDURE phase3_inbound_add_index( IN p_table_name VARCHAR(64), IN p_index_name VARCHAR(64), IN p_definition TEXT ) BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = p_table_name AND index_name = p_index_name ) THEN SET @ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD ', p_definition); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; END IF; END$$ DELIMITER ; CALL phase3_inbound_add_index( 'fl_inbound_order', 'uk_inbound_master_order', 'UNIQUE KEY `uk_inbound_master_order` (`tenant_id`, `master_order_id`)'); CALL phase3_inbound_add_index( 'fl_inbound_order', 'idx_inbound_root_role', 'KEY `idx_inbound_root_role` (`tenant_id`, `root_inbound_id`, `inbound_role`, `created_at`, `id`)'); DROP PROCEDURE IF EXISTS phase3_assert_active_remainder_unique_ready; DELIMITER $$ CREATE PROCEDURE phase3_assert_active_remainder_unique_ready() BEGIN IF EXISTS ( SELECT 1 FROM fl_inbound_order WHERE inbound_role = 'CHILD' AND split_purpose = 'REMAINDER' AND split_status = 'NOT_SPLIT' AND deleted = 0 AND root_inbound_id IS NOT NULL GROUP BY tenant_id, root_inbound_id HAVING COUNT(*) > 1 ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '同一根入仓单存在多个有效余量,请先人工核对历史数据'; END IF; END$$ DELIMITER ; CALL phase3_assert_active_remainder_unique_ready(); DROP PROCEDURE IF EXISTS phase3_assert_active_remainder_unique_ready; CALL phase3_inbound_add_index( 'fl_inbound_order', 'uk_inbound_one_active_remainder', 'UNIQUE KEY `uk_inbound_one_active_remainder` (`tenant_id`, `active_remainder_root_id`)'); DROP PROCEDURE IF EXISTS phase3_inbound_drop_legacy_index; DELIMITER $$ CREATE PROCEDURE phase3_inbound_drop_legacy_index() BEGIN IF EXISTS ( SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_order' AND index_name = 'uk_inbound_tenant_order' ) THEN ALTER TABLE fl_inbound_order DROP INDEX uk_inbound_tenant_order; END IF; IF EXISTS ( SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_order' AND index_name = 'uk_inbound_parent_sequence' ) THEN ALTER TABLE fl_inbound_order DROP INDEX uk_inbound_parent_sequence; END IF; IF EXISTS ( SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_order' AND index_name = 'uk_inbound_one_child' ) THEN ALTER TABLE fl_inbound_order DROP INDEX uk_inbound_one_child; END IF; IF EXISTS ( SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_order' AND index_name = 'uk_inbound_one_secondary' ) THEN ALTER TABLE fl_inbound_order DROP INDEX uk_inbound_one_secondary; END IF; END$$ DELIMITER ; CALL phase3_inbound_drop_legacy_index(); DROP PROCEDURE IF EXISTS phase3_inbound_drop_legacy_index; CALL phase3_inbound_add_index( 'fl_inbound_order', 'uk_inbound_parent_split_sequence', 'UNIQUE KEY `uk_inbound_parent_split_sequence` (`tenant_id`, `parent_inbound_id`, `split_sequence`)'); DROP PROCEDURE IF EXISTS phase3_inbound_add_index; CREATE TABLE IF NOT EXISTS fl_inbound_split_batch ( id BIGINT UNSIGNED NOT NULL COMMENT '拆分批次ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', parent_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '主入仓单ID', operation_id VARCHAR(64) NOT NULL COMMENT '业务操作幂等号', idempotency_key VARCHAR(128) NOT NULL COMMENT '请求幂等键', expected_parent_version INT NOT NULL COMMENT '操作时主单乐观锁版本', package_count INT UNSIGNED NOT NULL COMMENT '本批次拆分箱数', status VARCHAR(16) NOT NULL DEFAULT 'PREPARED' COMMENT '拆分批次状态:PREPARED已准备、APPLIED已生效、CANCELLED已取消', remark VARCHAR(500) NULL COMMENT '拆分原因或备注', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '拆分批次创建时间', applied_at DATETIME(3) NULL COMMENT '批次生效时间', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', PRIMARY KEY (id), UNIQUE KEY uk_inbound_split_operation (tenant_id, operation_id), UNIQUE KEY uk_inbound_split_idempotency (tenant_id, parent_inbound_id, idempotency_key), KEY idx_inbound_split_parent (tenant_id, parent_inbound_id, created_at, id), CONSTRAINT chk_inbound_split_package CHECK (package_count > 0), CONSTRAINT chk_inbound_split_status CHECK (status IN ('PREPARED', 'APPLIED', 'CANCELLED')), CONSTRAINT fk_inbound_split_parent FOREIGN KEY (parent_inbound_id) REFERENCES fl_inbound_order (id) ) ENGINE=InnoDB COMMENT='主入仓单拆分批次;数量口径仅为箱数'; CREATE TABLE IF NOT EXISTS fl_inbound_split_item ( id BIGINT UNSIGNED NOT NULL COMMENT '拆分明细ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', split_batch_id BIGINT UNSIGNED NOT NULL COMMENT '拆分批次ID', parent_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '主入仓单ID', child_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '子入仓单ID', line_no INT UNSIGNED NOT NULL COMMENT '同一拆分批次内的行号', source_inbound_detail_id BIGINT UNSIGNED NULL COMMENT '订单货物明细逻辑ID', source_measurement_id BIGINT UNSIGNED NOT NULL COMMENT '来源测量批次ID', source_measurement_item_id BIGINT UNSIGNED NOT NULL COMMENT '来源测量箱规明细ID', package_count INT UNSIGNED NOT NULL COMMENT '分配给子单的箱数', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '拆分数量台账创建时间', PRIMARY KEY (id), UNIQUE KEY uk_inbound_split_batch_line (tenant_id, split_batch_id, line_no), UNIQUE KEY uk_inbound_split_batch_source_child (tenant_id, split_batch_id, source_measurement_item_id, child_inbound_id), KEY idx_inbound_split_detail (tenant_id, source_inbound_detail_id, created_at), CONSTRAINT chk_inbound_split_item_package CHECK (package_count > 0), CONSTRAINT fk_inbound_split_item_batch FOREIGN KEY (split_batch_id) REFERENCES fl_inbound_split_batch (id), CONSTRAINT fk_inbound_split_item_parent FOREIGN KEY (parent_inbound_id) REFERENCES fl_inbound_order (id), CONSTRAINT fk_inbound_split_item_child FOREIGN KEY (child_inbound_id) REFERENCES fl_inbound_order (id), CONSTRAINT fk_inbound_split_item_measurement FOREIGN KEY (source_measurement_id) REFERENCES fl_inbound_measurement (id), CONSTRAINT fk_inbound_split_item_measure_item FOREIGN KEY (source_measurement_item_id) REFERENCES fl_inbound_measurement_item (id) ) ENGINE=InnoDB COMMENT='主子入仓单拆分数量台账'; CREATE TABLE IF NOT EXISTS fl_inbound_split_history ( id BIGINT UNSIGNED NOT NULL COMMENT '拆分历史ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', split_batch_id BIGINT UNSIGNED NOT NULL COMMENT '拆分批次ID', operation_id VARCHAR(64) NOT NULL COMMENT '历史操作幂等号', action_type VARCHAR(24) NOT NULL COMMENT '拆分动作:CREATE创建批次、APPLY生效、CANCEL取消', before_snapshot JSON NULL COMMENT '操作前快照', after_snapshot JSON NOT NULL COMMENT '操作后快照', operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID', operator_name VARCHAR(64) NULL COMMENT '操作人名称快照', operation_reason VARCHAR(500) NULL COMMENT '操作原因', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '不可变拆分历史创建时间', PRIMARY KEY (id), UNIQUE KEY uk_inbound_split_history_operation (tenant_id, operation_id), KEY idx_inbound_split_history (tenant_id, split_batch_id, created_at, id), CONSTRAINT chk_inbound_split_action CHECK (action_type IN ('CREATE', 'APPLY', 'CANCEL')), CONSTRAINT fk_inbound_split_history_batch FOREIGN KEY (split_batch_id) REFERENCES fl_inbound_split_batch (id) ) ENGINE=InnoDB COMMENT='不可变主子入仓单拆分审计历史'; DROP PROCEDURE IF EXISTS phase3_inbound_add_column; DELIMITER $$ CREATE PROCEDURE phase3_inbound_add_column( IN p_table_name VARCHAR(64), IN p_column_name VARCHAR(64), IN p_definition TEXT ) BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = p_table_name AND column_name = p_column_name ) THEN SET @ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD COLUMN `', p_column_name, '` ', p_definition); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; END IF; END$$ DELIMITER ; CALL phase3_inbound_add_column( 'fl_inbound_measurement', 'split_batch_id', "BIGINT UNSIGNED NULL COMMENT '产生该测量批次的入仓拆分批次ID' AFTER inbound_id"); CALL phase3_inbound_add_column( 'fl_inbound_measurement', 'source_measurement_id', "BIGINT UNSIGNED NULL COMMENT '子单继承或复核时的来源测量批次ID' AFTER split_batch_id"); CALL phase3_inbound_add_column( 'fl_inbound_measurement_item', 'split_item_id', "BIGINT UNSIGNED NULL COMMENT '产生该子单测量明细的拆分台账ID' AFTER measurement_id"); CALL phase3_inbound_add_column( 'fl_inbound_measurement_item', 'source_measurement_item_id', "BIGINT UNSIGNED NULL COMMENT '被拆分继承的来源测量箱规明细ID' AFTER split_item_id"); CALL phase3_inbound_add_column( 'fl_inbound_split_item', 'line_no', "INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '同一拆分批次内的行号' AFTER child_inbound_id"); ALTER TABLE fl_inbound_split_item MODIFY source_measurement_id BIGINT UNSIGNED NOT NULL COMMENT '来源测量批次ID', MODIFY source_measurement_item_id BIGINT UNSIGNED NOT NULL COMMENT '来源测量箱规明细ID'; CALL phase3_inbound_add_column( 'fl_inbound_split_batch', 'loaded_child_inbound_id', "BIGINT UNSIGNED NULL COMMENT '本批次实装子单ID' AFTER package_count"); CALL phase3_inbound_add_column( 'fl_inbound_split_batch', 'source_inbound_id', "BIGINT UNSIGNED NULL COMMENT '本批次实际消耗的MASTER或REMAINDER分片ID' AFTER parent_inbound_id"); CALL phase3_inbound_add_column( 'fl_inbound_split_batch', 'remainder_child_inbound_id', "BIGINT UNSIGNED NULL COMMENT '本批次余量子单ID' AFTER loaded_child_inbound_id"); CALL phase3_inbound_add_column( 'fl_inbound_split_batch', 'loaded_package_count', "INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '本批次实装箱数' AFTER remainder_child_inbound_id"); CALL phase3_inbound_add_column( 'fl_inbound_split_batch', 'remaining_package_count', "INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '本批次未装箱数' AFTER loaded_package_count"); CALL phase3_inbound_add_column( 'fl_inbound_split_batch', 'loading_plan_id', "BIGINT UNSIGNED NULL COMMENT '关联装柜计划/柜次' AFTER remaining_package_count"); CALL phase3_inbound_add_column( 'fl_inbound_split_batch', 'container_booking_id', "BIGINT UNSIGNED NULL COMMENT '关联柜次标识(兼容查询)' AFTER loading_plan_id"); DROP PROCEDURE IF EXISTS phase3_inbound_add_column; DROP PROCEDURE IF EXISTS phase3_inbound_add_relation; DELIMITER $$ CREATE PROCEDURE phase3_inbound_add_relation( IN p_table_name VARCHAR(64), IN p_constraint_name VARCHAR(64), IN p_definition TEXT ) BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema = DATABASE() AND table_name = p_table_name AND constraint_name = p_constraint_name ) THEN SET @ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD CONSTRAINT `', p_constraint_name, '` ', p_definition); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; END IF; END$$ DELIMITER ; CALL phase3_inbound_add_relation( 'fl_inbound_order', 'fk_inbound_parent_order', 'FOREIGN KEY (`parent_inbound_id`) REFERENCES `fl_inbound_order` (`id`)'); CALL phase3_inbound_add_relation( 'fl_inbound_order', 'fk_inbound_root_order', 'FOREIGN KEY (`root_inbound_id`) REFERENCES `fl_inbound_order` (`id`)'); CALL phase3_inbound_add_relation( 'fl_inbound_split_batch', 'fk_inbound_split_loaded_child', 'FOREIGN KEY (`loaded_child_inbound_id`) REFERENCES `fl_inbound_order` (`id`)'); CALL phase3_inbound_add_relation( 'fl_inbound_split_batch', 'fk_inbound_split_source', 'FOREIGN KEY (`source_inbound_id`) REFERENCES `fl_inbound_order` (`id`)'); CALL phase3_inbound_add_relation( 'fl_inbound_split_batch', 'fk_inbound_split_remainder_child', 'FOREIGN KEY (`remainder_child_inbound_id`) REFERENCES `fl_inbound_order` (`id`)'); CALL phase3_inbound_add_relation( 'fl_inbound_measurement', 'fk_measure_split_batch', 'FOREIGN KEY (`split_batch_id`) REFERENCES `fl_inbound_split_batch` (`id`)'); CALL phase3_inbound_add_relation( 'fl_inbound_measurement', 'fk_measure_source_batch', 'FOREIGN KEY (`source_measurement_id`) REFERENCES `fl_inbound_measurement` (`id`)'); CALL phase3_inbound_add_relation( 'fl_inbound_measurement_item', 'fk_measure_item_split_item', 'FOREIGN KEY (`split_item_id`) REFERENCES `fl_inbound_split_item` (`id`)'); CALL phase3_inbound_add_relation( 'fl_inbound_measurement_item', 'fk_measure_item_source_item', 'FOREIGN KEY (`source_measurement_item_id`) REFERENCES `fl_inbound_measurement_item` (`id`)'); DROP PROCEDURE IF EXISTS phase3_inbound_add_relation; DROP PROCEDURE IF EXISTS phase3_inbound_add_index; DELIMITER $$ CREATE PROCEDURE phase3_inbound_add_index( IN p_table_name VARCHAR(64), IN p_index_name VARCHAR(64), IN p_definition TEXT ) BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = p_table_name AND index_name = p_index_name ) THEN SET @ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD ', p_definition); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; END IF; END$$ DELIMITER ; CALL phase3_inbound_add_index( 'fl_inbound_measurement', 'idx_measure_split_source', 'KEY `idx_measure_split_source` (`tenant_id`, `split_batch_id`, `source_measurement_id`, `created_at`)'); CALL phase3_inbound_add_index( 'fl_inbound_measurement_item', 'idx_measure_item_split_source', 'KEY `idx_measure_item_split_source` (`tenant_id`, `split_item_id`, `source_measurement_item_id`)'); CALL phase3_inbound_add_index( 'fl_inbound_split_item', 'uk_inbound_split_batch_line', 'UNIQUE KEY `uk_inbound_split_batch_line` (`tenant_id`, `split_batch_id`, `line_no`)'); CALL phase3_inbound_add_index( 'fl_inbound_split_batch', 'idx_inbound_split_loading_plan', 'KEY `idx_inbound_split_loading_plan` (`tenant_id`, `loading_plan_id`, `container_booking_id`, `status`, `id`)'); CALL phase3_inbound_add_index( 'fl_inbound_split_batch', 'uk_inbound_split_source_once', 'UNIQUE KEY `uk_inbound_split_source_once` (`tenant_id`, `source_inbound_id`)'); DROP PROCEDURE IF EXISTS phase3_inbound_drop_split_legacy_index; DELIMITER $$ CREATE PROCEDURE phase3_inbound_drop_split_legacy_index() BEGIN IF EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_split_item' AND index_name = 'uk_inbound_split_child') THEN ALTER TABLE fl_inbound_split_item DROP INDEX uk_inbound_split_child; END IF; IF EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_split_item' AND index_name = 'uk_inbound_split_batch_child') THEN ALTER TABLE fl_inbound_split_item DROP INDEX uk_inbound_split_batch_child; END IF; IF EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_split_item' AND index_name = 'uk_inbound_split_batch_source_item') THEN ALTER TABLE fl_inbound_split_item DROP INDEX uk_inbound_split_batch_source_item; END IF; END$$ DELIMITER ; CALL phase3_inbound_drop_split_legacy_index(); DROP PROCEDURE IF EXISTS phase3_inbound_drop_split_legacy_index; CALL phase3_inbound_add_index( 'fl_inbound_split_item', 'uk_inbound_split_batch_source_child', 'UNIQUE KEY `uk_inbound_split_batch_source_child` (`tenant_id`, `split_batch_id`, `source_measurement_item_id`, `child_inbound_id`)'); DROP PROCEDURE IF EXISTS phase3_inbound_add_index; DROP TRIGGER IF EXISTS trg_inbound_order_parent_before_insert; DROP TRIGGER IF EXISTS trg_inbound_order_parent_before_update; DROP TRIGGER IF EXISTS trg_inbound_split_item_before_insert; DROP TRIGGER IF EXISTS trg_inbound_split_batch_before_insert; DROP TRIGGER IF EXISTS trg_inbound_split_batch_before_update; DROP TRIGGER IF EXISTS trg_inbound_split_item_before_update; DROP TRIGGER IF EXISTS trg_inbound_split_item_before_delete; DROP TRIGGER IF EXISTS trg_inbound_split_history_before_update; DROP TRIGGER IF EXISTS trg_inbound_split_history_before_delete; DELIMITER $$ CREATE TRIGGER trg_inbound_order_parent_before_insert BEFORE INSERT ON fl_inbound_order FOR EACH ROW BEGIN IF NEW.inbound_role NOT IN ('MASTER', 'CHILD') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '入仓单层级非法'; END IF; IF NEW.split_status NOT IN ('NOT_SPLIT', 'PARTIALLY_SPLIT', 'FULLY_SPLIT') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '入仓单拆分状态非法'; END IF; IF NEW.split_purpose NOT IN ('MASTER', 'LOADED', 'REMAINDER') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '入仓单分片用途非法'; END IF; IF NEW.package_count IS NOT NULL AND NEW.package_count = 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '入仓单箱数必须大于0'; END IF; IF NEW.inbound_role = 'MASTER' AND NEW.parent_inbound_id IS NOT NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '主入仓单不能绑定父单'; END IF; IF NEW.inbound_role = 'MASTER' AND NEW.split_purpose <> 'MASTER' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '主入仓单用途必须为MASTER'; END IF; IF NEW.inbound_role = 'CHILD' THEN IF NEW.parent_inbound_id IS NULL OR NEW.root_inbound_id IS NULL OR NEW.split_sequence IS NULL OR NEW.package_count IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '子入仓单缺少主单、顺序号或箱数'; END IF; IF NEW.split_purpose <> 'REMAINDER' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '新建子入仓单只能作为余量分片'; END IF; IF NOT EXISTS ( SELECT 1 FROM fl_inbound_order parent_order WHERE parent_order.id = NEW.parent_inbound_id AND parent_order.tenant_id = NEW.tenant_id AND parent_order.inbound_role = 'MASTER' AND parent_order.deleted = 0 ) OR NEW.root_inbound_id <> NEW.parent_inbound_id THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '子入仓单必须绑定同一客户的有效主单'; END IF; END IF; END$$ CREATE TRIGGER trg_inbound_order_parent_before_update BEFORE UPDATE ON fl_inbound_order FOR EACH ROW BEGIN IF NOT (OLD.tenant_id <=> NEW.tenant_id) OR NOT (OLD.inbound_role <=> NEW.inbound_role) OR NOT (OLD.parent_inbound_id <=> NEW.parent_inbound_id) OR NOT (OLD.root_inbound_id <=> NEW.root_inbound_id) OR NOT (OLD.split_sequence <=> NEW.split_sequence) OR NOT (OLD.split_purpose <=> NEW.split_purpose) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '入仓单主子归属创建后不可修改'; END IF; IF NEW.split_purpose NOT IN ('MASTER', 'LOADED', 'REMAINDER') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '入仓单分片用途非法'; END IF; IF NEW.inbound_role = 'CHILD' AND (NEW.package_count IS NULL OR NEW.package_count = 0) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '子入仓单箱数必须大于0'; END IF; IF NEW.split_status NOT IN ('NOT_SPLIT', 'PARTIALLY_SPLIT', 'FULLY_SPLIT') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '入仓单拆分状态非法'; END IF; IF (NEW.inbound_role = 'CHILD' AND NEW.split_purpose = 'MASTER') OR (NEW.inbound_role = 'MASTER' AND NEW.split_purpose <> 'MASTER') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '入仓单层级与用途不一致'; END IF; END$$ CREATE TRIGGER trg_inbound_split_item_before_insert BEFORE INSERT ON fl_inbound_split_item FOR EACH ROW BEGIN IF NEW.source_measurement_id IS NULL OR NEW.source_measurement_item_id IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆分明细必须绑定真实测量来源'; END IF; IF NOT EXISTS ( SELECT 1 FROM fl_inbound_split_batch batch JOIN fl_inbound_order parent_order ON parent_order.id = batch.parent_inbound_id JOIN fl_inbound_order child_order ON child_order.id = NEW.child_inbound_id WHERE batch.id = NEW.split_batch_id AND batch.tenant_id = NEW.tenant_id AND batch.parent_inbound_id = NEW.parent_inbound_id AND batch.status = 'PREPARED' AND batch.loaded_child_inbound_id IS NULL AND batch.remainder_child_inbound_id IS NOT NULL AND NEW.child_inbound_id = batch.remainder_child_inbound_id AND parent_order.tenant_id = NEW.tenant_id AND parent_order.inbound_role = 'MASTER' AND child_order.tenant_id = NEW.tenant_id AND child_order.inbound_role = 'CHILD' AND child_order.split_purpose = 'REMAINDER' AND child_order.parent_inbound_id = NEW.parent_inbound_id AND child_order.package_count >= NEW.package_count AND child_order.deleted = 0 AND EXISTS ( SELECT 1 FROM fl_inbound_measurement_item source_item JOIN fl_inbound_measurement source_batch ON source_batch.id = source_item.measurement_id WHERE source_item.id = NEW.source_measurement_item_id AND source_item.tenant_id = NEW.tenant_id AND source_item.measurement_id = NEW.source_measurement_id AND source_batch.inbound_id = COALESCE(batch.source_inbound_id, batch.parent_inbound_id) ) ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆分明细与批次、主单或子单不一致'; END IF; END$$ CREATE TRIGGER trg_inbound_split_batch_before_insert BEFORE INSERT ON fl_inbound_split_batch FOR EACH ROW BEGIN IF NEW.status <> 'PREPARED' OR NEW.applied_at IS NOT NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆分批次必须从PREPARED创建'; END IF; IF NEW.loaded_child_inbound_id IS NOT NULL OR NEW.loaded_package_count = 0 OR NEW.loaded_package_count + NEW.remaining_package_count <> NEW.package_count OR (NEW.remaining_package_count = 0 AND NEW.remainder_child_inbound_id IS NOT NULL) OR (NEW.remaining_package_count > 0 AND NEW.remainder_child_inbound_id IS NULL) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '新拆分批次必须由来源单承接实装且最多创建一个余量子单'; END IF; IF NOT EXISTS ( SELECT 1 FROM fl_inbound_order parent_order WHERE parent_order.id = NEW.parent_inbound_id AND parent_order.tenant_id = NEW.tenant_id AND parent_order.inbound_role = 'MASTER' AND parent_order.deleted = 0 ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆分批次主单不存在或客户归属不一致'; END IF; IF NEW.source_inbound_id IS NULL OR NOT EXISTS ( SELECT 1 FROM fl_inbound_order source_order WHERE source_order.id = NEW.source_inbound_id AND source_order.tenant_id = NEW.tenant_id AND source_order.deleted = 0 AND ((source_order.id = NEW.parent_inbound_id AND source_order.inbound_role = 'MASTER' AND ((source_order.split_status = 'NOT_SPLIT' AND COALESCE(source_order.package_count, source_order.total_package_count) = NEW.package_count) OR (source_order.split_status = 'FULLY_SPLIT' AND source_order.package_count = NEW.loaded_package_count))) OR (source_order.inbound_role = 'CHILD' AND source_order.parent_inbound_id = NEW.parent_inbound_id AND source_order.split_purpose = 'REMAINDER' AND ((source_order.split_status = 'NOT_SPLIT' AND source_order.package_count = NEW.package_count) OR (source_order.split_status = 'FULLY_SPLIT' AND source_order.package_count = NEW.loaded_package_count)))) ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆分批次来源必须是箱数守恒的根主单或余量分片'; END IF; IF NEW.loaded_package_count > 0 AND ( NEW.loading_plan_id IS NULL OR NEW.container_booking_id IS NULL OR NOT EXISTS ( SELECT 1 FROM fl_loading_plan plan WHERE plan.id = NEW.loading_plan_id AND plan.container_booking_id = NEW.container_booking_id AND EXISTS (SELECT 1 FROM fl_loading_plan_item plan_item WHERE plan_item.loading_plan_id = plan.id AND plan_item.account_id = NEW.tenant_id AND plan_item.inbound_id = NEW.source_inbound_id AND plan_item.deleted = 0) ) ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '实装拆分批次必须绑定匹配的装柜计划和柜次'; END IF; IF NEW.remainder_child_inbound_id IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM fl_inbound_order remainder_order WHERE remainder_order.id = NEW.remainder_child_inbound_id AND remainder_order.tenant_id = NEW.tenant_id AND remainder_order.parent_inbound_id = NEW.parent_inbound_id AND remainder_order.root_inbound_id = NEW.parent_inbound_id AND remainder_order.inbound_role = 'CHILD' AND remainder_order.split_purpose = 'REMAINDER' AND remainder_order.split_status = 'NOT_SPLIT' AND remainder_order.package_count = NEW.remaining_package_count AND remainder_order.deleted = 0 ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '余量子入仓单与拆分批次数量或根归属不一致'; END IF; END$$ CREATE TRIGGER trg_inbound_split_batch_before_update BEFORE UPDATE ON fl_inbound_split_batch FOR EACH ROW BEGIN IF OLD.status IN ('APPLIED', 'CANCELLED') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '已结束拆分批次不可修改'; END IF; IF NOT (OLD.parent_inbound_id <=> NEW.parent_inbound_id) OR NOT (OLD.source_inbound_id <=> NEW.source_inbound_id) OR NOT (OLD.loaded_child_inbound_id <=> NEW.loaded_child_inbound_id) OR NOT (OLD.remainder_child_inbound_id <=> NEW.remainder_child_inbound_id) OR NOT (OLD.package_count <=> NEW.package_count) OR NOT (OLD.loaded_package_count <=> NEW.loaded_package_count) OR NOT (OLD.remaining_package_count <=> NEW.remaining_package_count) OR NOT (OLD.loading_plan_id <=> NEW.loading_plan_id) OR NOT (OLD.container_booking_id <=> NEW.container_booking_id) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆分批次来源、子单、柜次与数量创建后不可修改'; END IF; IF NEW.status = 'APPLIED' THEN IF NEW.applied_at IS NULL OR (NEW.loaded_package_count > 0 AND ( NEW.loading_plan_id IS NULL OR NEW.container_booking_id IS NULL OR NOT EXISTS ( SELECT 1 FROM fl_loading_plan plan WHERE plan.id = NEW.loading_plan_id AND plan.container_booking_id = NEW.container_booking_id AND EXISTS (SELECT 1 FROM fl_loading_plan_item plan_item WHERE plan_item.loading_plan_id = plan.id AND plan_item.account_id = NEW.tenant_id AND plan_item.inbound_id = NEW.source_inbound_id AND plan_item.deleted = 0)))) OR NOT EXISTS ( SELECT 1 FROM fl_inbound_order parent_order WHERE parent_order.id = NEW.parent_inbound_id AND parent_order.tenant_id = NEW.tenant_id AND parent_order.inbound_role = 'MASTER' AND parent_order.version > NEW.expected_parent_version ) OR NOT EXISTS ( SELECT 1 FROM fl_inbound_order source_order WHERE source_order.id = NEW.source_inbound_id AND source_order.tenant_id = NEW.tenant_id AND source_order.split_status = 'FULLY_SPLIT' AND source_order.package_count = NEW.loaded_package_count ) OR (SELECT COALESCE(SUM(item.package_count), 0) FROM fl_inbound_split_item item WHERE item.tenant_id = NEW.tenant_id AND item.split_batch_id = NEW.id) <> NEW.remaining_package_count OR (SELECT COUNT(DISTINCT item.child_inbound_id) FROM fl_inbound_split_item item WHERE item.tenant_id = NEW.tenant_id AND item.split_batch_id = NEW.id) <> CASE WHEN NEW.remaining_package_count > 0 THEN 1 ELSE 0 END OR (NEW.loaded_child_inbound_id IS NOT NULL) OR NEW.loaded_package_count + NEW.remaining_package_count <> NEW.package_count OR (NEW.remaining_package_count > 0 AND NEW.remainder_child_inbound_id IS NULL) OR (NEW.remainder_child_inbound_id IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM fl_inbound_order c WHERE c.id = NEW.remainder_child_inbound_id AND c.parent_inbound_id = NEW.parent_inbound_id AND c.root_inbound_id = NEW.parent_inbound_id AND c.split_purpose = 'REMAINDER' AND c.split_status = 'NOT_SPLIT' AND c.package_count = NEW.remaining_package_count AND c.deleted = 0)) OR EXISTS (SELECT 1 FROM fl_inbound_split_item item WHERE item.tenant_id = NEW.tenant_id AND item.split_batch_id = NEW.id AND item.child_inbound_id <> NEW.remainder_child_inbound_id) OR EXISTS (SELECT 1 FROM fl_inbound_split_item item JOIN fl_inbound_order child_order ON child_order.id = item.child_inbound_id WHERE item.tenant_id = NEW.tenant_id AND item.split_batch_id = NEW.id GROUP BY child_order.id, child_order.package_count HAVING child_order.package_count <> SUM(item.package_count)) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二级子入仓单或拆分明细箱数合计不一致'; END IF; IF EXISTS ( SELECT 1 FROM fl_inbound_split_item split_item JOIN fl_inbound_measurement_item source_item ON source_item.id = split_item.source_measurement_item_id WHERE split_item.tenant_id = NEW.tenant_id AND split_item.split_batch_id = NEW.id GROUP BY split_item.source_measurement_item_id, source_item.package_count HAVING SUM(split_item.package_count) > source_item.package_count ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '子单承接箱数超过来源测量箱规箱数'; END IF; IF NOT EXISTS ( SELECT 1 FROM fl_inbound_order root_order WHERE root_order.id = NEW.parent_inbound_id AND root_order.tenant_id = NEW.tenant_id AND root_order.inbound_role = 'MASTER' AND root_order.deleted = 0 AND root_order.total_package_count = COALESCE(root_order.package_count, 0) + COALESCE((SELECT SUM(child.package_count) FROM fl_inbound_order child WHERE child.tenant_id = NEW.tenant_id AND child.root_inbound_id = root_order.id AND child.inbound_role = 'CHILD' AND child.deleted = 0), 0) ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '根入仓单原始箱数与已完成分片及当前余量不守恒'; END IF; ELSEIF NEW.status = 'CANCELLED' THEN IF EXISTS ( SELECT 1 FROM fl_inbound_split_item item WHERE item.tenant_id = NEW.tenant_id AND item.split_batch_id = NEW.id ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '已有子单明细的拆分批次不可取消'; END IF; ELSEIF NEW.status <> 'PREPARED' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆分批次状态流转非法'; END IF; END$$ CREATE TRIGGER trg_inbound_split_item_before_update BEFORE UPDATE ON fl_inbound_split_item FOR EACH ROW BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆分数量台账不可修改'; END$$ CREATE TRIGGER trg_inbound_split_item_before_delete BEFORE DELETE ON fl_inbound_split_item FOR EACH ROW BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆分数量台账不可删除'; END$$ CREATE TRIGGER trg_inbound_split_history_before_update BEFORE UPDATE ON fl_inbound_split_history FOR EACH ROW BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆分历史不可修改'; END$$ CREATE TRIGGER trg_inbound_split_history_before_delete BEFORE DELETE ON fl_inbound_split_history FOR EACH ROW BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆分历史不可删除'; END$$ DELIMITER ; DROP PROCEDURE IF EXISTS phase3_assert_inbound_parent_child_schema; DELIMITER $$ CREATE PROCEDURE phase3_assert_inbound_parent_child_schema() BEGIN IF (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name IN ('fl_inbound_split_batch', 'fl_inbound_split_item', 'fl_inbound_split_history')) <> 3 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '主子入仓单拆分台账未完整创建'; END IF; IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_order' AND column_name IN ('inbound_role', 'parent_inbound_id', 'root_inbound_id', 'split_sequence', 'split_status', 'package_count', 'master_order_id', 'split_purpose', 'active_remainder_root_id')) <> 9 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '主子入仓单关系字段未完整创建'; END IF; IF (SELECT COUNT(DISTINCT index_name) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_order' AND index_name IN ('uk_inbound_master_order', 'uk_inbound_parent_split_sequence', 'uk_inbound_one_active_remainder')) <> 3 OR (SELECT COUNT(DISTINCT index_name) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_split_batch' AND index_name IN ('uk_inbound_split_operation', 'uk_inbound_split_idempotency', 'uk_inbound_split_source_once')) <> 3 OR (SELECT COUNT(DISTINCT index_name) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_split_item' AND index_name IN ('uk_inbound_split_batch_line', 'uk_inbound_split_batch_source_child')) <> 2 OR (SELECT COUNT(DISTINCT index_name) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_split_history' AND index_name = 'uk_inbound_split_history_operation') <> 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '主子入仓单唯一约束未完整创建'; END IF; IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_split_batch' AND column_name IN ('source_inbound_id', 'loaded_child_inbound_id', 'remainder_child_inbound_id', 'loaded_package_count', 'remaining_package_count', 'loading_plan_id', 'container_booking_id')) <> 7 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆分批次来源、子单、数量或柜次字段未完整创建'; END IF; IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_split_item' AND column_name IN ('source_measurement_id', 'source_measurement_item_id') AND is_nullable = 'NO') <> 2 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆分数量台账必须绑定非空测量来源'; END IF; IF (SELECT COUNT(*) FROM information_schema.table_constraints WHERE constraint_schema = DATABASE() AND constraint_name IN ('fk_inbound_parent_order', 'fk_inbound_root_order', 'fk_inbound_split_source', 'fk_inbound_split_loaded_child', 'fk_inbound_split_remainder_child', 'fk_measure_split_batch', 'fk_measure_source_batch', 'fk_measure_item_split_item', 'fk_measure_item_source_item')) <> 9 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '主子入仓单与测量关联约束未完整创建'; END IF; IF (SELECT COUNT(*) FROM information_schema.triggers WHERE trigger_schema = DATABASE() AND trigger_name IN ('trg_inbound_order_parent_before_insert', 'trg_inbound_order_parent_before_update', 'trg_inbound_split_item_before_insert', 'trg_inbound_split_batch_before_insert', 'trg_inbound_split_batch_before_update', 'trg_inbound_split_item_before_update', 'trg_inbound_split_item_before_delete', 'trg_inbound_split_history_before_update', 'trg_inbound_split_history_before_delete')) <> 9 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '主子入仓单数据库保护触发器未完整创建'; END IF; IF EXISTS ( SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_order' AND index_name IN ('uk_inbound_tenant_order', 'uk_inbound_one_child', 'uk_inbound_one_secondary', 'uk_inbound_parent_sequence') ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '旧订单唯一索引仍阻止创建子入仓单'; END IF; IF EXISTS ( SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_split_item' AND index_name IN ('uk_inbound_split_child', 'uk_inbound_split_batch_child', 'uk_inbound_split_batch_source_item') ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '旧拆分明细唯一索引仍阻止数量守恒分配'; END IF; END$$ DELIMITER ; CALL phase3_assert_inbound_parent_child_schema(); DROP PROCEDURE IF EXISTS phase3_assert_inbound_parent_child_schema;