Files
baiyue-firstleg/firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql
T
2026-08-11 18:06:26 +08:00

845 lines
41 KiB
SQL

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;