feat(firstleg): enforce single remainder split schema
This commit is contained in:
@@ -7399,6 +7399,8 @@ USE baiyue_firstleg;
|
||||
/*
|
||||
* 三期主子入仓单结构迁移。
|
||||
* 仅建立结构和数据库保护,不自动拆分或回填存量业务数据。
|
||||
* 新写入采用“来源单承接实装、仅创建余量子单”;LOADED 子单和 loaded_child_inbound_id
|
||||
* 仅为历史双子单读取保留,不删除、不清洗存量记录。
|
||||
* tenant_id 继续表示客户隔离键;数量统一使用 package_count。
|
||||
*/
|
||||
DROP PROCEDURE IF EXISTS phase3_inbound_add_column;
|
||||
@@ -7455,6 +7457,10 @@ UPDATE fl_inbound_order
|
||||
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;
|
||||
@@ -7486,6 +7492,31 @@ 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()
|
||||
@@ -7812,8 +7843,8 @@ BEGIN
|
||||
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 = 'MASTER' THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '子入仓单必须标记LOADED或REMAINDER';
|
||||
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
|
||||
@@ -7853,11 +7884,6 @@ BEGIN
|
||||
OR (NEW.inbound_role = 'MASTER' AND NEW.split_purpose <> 'MASTER') THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '入仓单层级与用途不一致';
|
||||
END IF;
|
||||
IF NEW.inbound_role = 'MASTER'
|
||||
AND ((NEW.package_count = 0 AND NEW.split_status <> 'FULLY_SPLIT')
|
||||
OR (NEW.split_status = 'FULLY_SPLIT' AND COALESCE(NEW.package_count, 0) <> 0)) THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '主单箱数归零必须与完全拆分状态同时提交';
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
CREATE TRIGGER trg_inbound_split_item_before_insert
|
||||
@@ -7876,11 +7902,14 @@ BEGIN
|
||||
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 IN ('LOADED', 'REMAINDER')
|
||||
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
|
||||
@@ -7905,6 +7934,13 @@ 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
|
||||
@@ -7919,16 +7955,21 @@ BEGIN
|
||||
WHERE source_order.id = NEW.source_inbound_id
|
||||
AND source_order.tenant_id = NEW.tenant_id
|
||||
AND source_order.deleted = 0
|
||||
AND COALESCE(source_order.package_count, source_order.total_package_count) = NEW.package_count
|
||||
AND ((source_order.id = NEW.parent_inbound_id
|
||||
AND source_order.inbound_role = 'MASTER'
|
||||
AND source_order.split_status = 'NOT_SPLIT') OR
|
||||
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.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 = '拆分批次来源必须是箱数匹配且未消费的根主单或余量子单';
|
||||
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 (
|
||||
@@ -7944,6 +7985,20 @@ BEGIN
|
||||
) 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
|
||||
@@ -7987,34 +8042,28 @@ BEGIN
|
||||
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.package_count OR
|
||||
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) NOT BETWEEN 1 AND 2 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
|
||||
AND item.child_inbound_id = NEW.loaded_child_inbound_id) <> 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
|
||||
AND item.child_inbound_id = NEW.remainder_child_inbound_id) <> NEW.remaining_package_count OR
|
||||
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.loaded_package_count > 0 AND NEW.loaded_child_inbound_id IS NULL) OR
|
||||
(NEW.remaining_package_count > 0 AND NEW.remainder_child_inbound_id IS NULL) OR
|
||||
(NEW.loaded_child_inbound_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM fl_inbound_order c WHERE c.id = NEW.loaded_child_inbound_id
|
||||
AND c.parent_inbound_id = NEW.parent_inbound_id AND c.split_purpose = 'LOADED')) 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.split_purpose = 'REMAINDER')) OR
|
||||
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
|
||||
@@ -8036,6 +8085,24 @@ BEGIN
|
||||
) 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
|
||||
@@ -8093,12 +8160,13 @@ BEGIN
|
||||
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')) <> 8 THEN
|
||||
'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')) <> 2
|
||||
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',
|
||||
|
||||
@@ -4,6 +4,8 @@ USE baiyue_firstleg;
|
||||
/*
|
||||
* 三期主子入仓单结构迁移。
|
||||
* 仅建立结构和数据库保护,不自动拆分或回填存量业务数据。
|
||||
* 新写入采用“来源单承接实装、仅创建余量子单”;LOADED 子单和 loaded_child_inbound_id
|
||||
* 仅为历史双子单读取保留,不删除、不清洗存量记录。
|
||||
* tenant_id 继续表示客户隔离键;数量统一使用 package_count。
|
||||
*/
|
||||
DROP PROCEDURE IF EXISTS phase3_inbound_add_column;
|
||||
@@ -60,6 +62,10 @@ UPDATE fl_inbound_order
|
||||
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;
|
||||
@@ -91,6 +97,31 @@ 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()
|
||||
@@ -417,8 +448,8 @@ BEGIN
|
||||
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 = 'MASTER' THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '子入仓单必须标记LOADED或REMAINDER';
|
||||
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
|
||||
@@ -458,11 +489,6 @@ BEGIN
|
||||
OR (NEW.inbound_role = 'MASTER' AND NEW.split_purpose <> 'MASTER') THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '入仓单层级与用途不一致';
|
||||
END IF;
|
||||
IF NEW.inbound_role = 'MASTER'
|
||||
AND ((NEW.package_count = 0 AND NEW.split_status <> 'FULLY_SPLIT')
|
||||
OR (NEW.split_status = 'FULLY_SPLIT' AND COALESCE(NEW.package_count, 0) <> 0)) THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '主单箱数归零必须与完全拆分状态同时提交';
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
CREATE TRIGGER trg_inbound_split_item_before_insert
|
||||
@@ -481,11 +507,14 @@ BEGIN
|
||||
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 IN ('LOADED', 'REMAINDER')
|
||||
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
|
||||
@@ -510,6 +539,13 @@ 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
|
||||
@@ -524,16 +560,21 @@ BEGIN
|
||||
WHERE source_order.id = NEW.source_inbound_id
|
||||
AND source_order.tenant_id = NEW.tenant_id
|
||||
AND source_order.deleted = 0
|
||||
AND COALESCE(source_order.package_count, source_order.total_package_count) = NEW.package_count
|
||||
AND ((source_order.id = NEW.parent_inbound_id
|
||||
AND source_order.inbound_role = 'MASTER'
|
||||
AND source_order.split_status = 'NOT_SPLIT') OR
|
||||
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.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 = '拆分批次来源必须是箱数匹配且未消费的根主单或余量子单';
|
||||
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 (
|
||||
@@ -549,6 +590,20 @@ BEGIN
|
||||
) 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
|
||||
@@ -592,34 +647,28 @@ BEGIN
|
||||
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.package_count OR
|
||||
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) NOT BETWEEN 1 AND 2 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
|
||||
AND item.child_inbound_id = NEW.loaded_child_inbound_id) <> 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
|
||||
AND item.child_inbound_id = NEW.remainder_child_inbound_id) <> NEW.remaining_package_count OR
|
||||
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.loaded_package_count > 0 AND NEW.loaded_child_inbound_id IS NULL) OR
|
||||
(NEW.remaining_package_count > 0 AND NEW.remainder_child_inbound_id IS NULL) OR
|
||||
(NEW.loaded_child_inbound_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM fl_inbound_order c WHERE c.id = NEW.loaded_child_inbound_id
|
||||
AND c.parent_inbound_id = NEW.parent_inbound_id AND c.split_purpose = 'LOADED')) 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.split_purpose = 'REMAINDER')) OR
|
||||
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
|
||||
@@ -641,6 +690,24 @@ BEGIN
|
||||
) 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
|
||||
@@ -698,12 +765,13 @@ BEGIN
|
||||
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')) <> 8 THEN
|
||||
'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')) <> 2
|
||||
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',
|
||||
|
||||
+15
-4
@@ -22,10 +22,13 @@ class InboundParentChildSqlContractTest {
|
||||
.contains("'fl_inbound_order', 'package_count'")
|
||||
.contains("'fl_inbound_order', 'split_purpose'")
|
||||
.contains("SET split_purpose = 'REMAINDER'")
|
||||
.contains("active_remainder_root_id")
|
||||
.contains("GENERATED ALWAYS AS (CASE WHEN inbound_role = 'MASTER'")
|
||||
.contains("GENERATED ALWAYS AS (CASE WHEN inbound_role = 'CHILD'")
|
||||
.contains("DROP INDEX uk_inbound_tenant_order")
|
||||
.contains("uk_inbound_master_order")
|
||||
.contains("uk_inbound_parent_split_sequence")
|
||||
.contains("uk_inbound_one_active_remainder")
|
||||
.contains("DROP INDEX uk_inbound_parent_sequence");
|
||||
}
|
||||
|
||||
@@ -54,17 +57,23 @@ class InboundParentChildSqlContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void protectsOwnershipAndTotals() {
|
||||
void protectsSingleRemainderWritesAndConservation() {
|
||||
assertThat(sql)
|
||||
.contains("子入仓单必须绑定同一客户的有效主单")
|
||||
.contains("parent_order.version > NEW.expected_parent_version")
|
||||
.contains("NEW.loaded_package_count")
|
||||
.contains("NEW.remaining_package_count")
|
||||
.contains("NEW.loaded_package_count + NEW.remaining_package_count <> NEW.package_count")
|
||||
.contains("NEW.loaded_child_inbound_id IS NOT NULL")
|
||||
.contains("NEW.child_inbound_id = batch.remainder_child_inbound_id")
|
||||
.contains("source_order.package_count = NEW.loaded_package_count")
|
||||
.contains("item.child_inbound_id <> NEW.remainder_child_inbound_id")
|
||||
.contains("<> NEW.remaining_package_count")
|
||||
.contains("根入仓单原始箱数与已完成分片及当前余量不守恒")
|
||||
.contains("COALESCE(source_order.package_count, source_order.total_package_count) = NEW.package_count")
|
||||
.contains("AND plan_item.deleted = 0))))\n OR NOT EXISTS (")
|
||||
.contains("source_batch.inbound_id = COALESCE(batch.source_inbound_id, batch.parent_inbound_id)")
|
||||
.contains("拆分批次来源必须是箱数匹配且未消费的根主单或余量子单")
|
||||
.contains("拆分批次来源必须是箱数守恒的根主单或余量分片")
|
||||
.contains("SUM(split_item.package_count) > source_item.package_count")
|
||||
.contains("子单承接箱数超过来源测量箱规箱数")
|
||||
.contains("child_order.package_count >= NEW.package_count")
|
||||
@@ -75,13 +84,14 @@ class InboundParentChildSqlContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesMultiChildAndMultiSkuTotals() {
|
||||
void validatesSingleRemainderAndMultiSkuTotals() {
|
||||
assertThat(sql)
|
||||
.contains("uk_inbound_parent_split_sequence")
|
||||
.contains("uk_inbound_one_active_remainder")
|
||||
.contains("uk_inbound_split_batch_line")
|
||||
.contains("uk_inbound_split_batch_source_child")
|
||||
.contains("COUNT(DISTINCT item.child_inbound_id)")
|
||||
.contains("NOT BETWEEN 1 AND 2")
|
||||
.contains("<> CASE WHEN NEW.remaining_package_count > 0 THEN 1 ELSE 0 END")
|
||||
.contains("child_order.package_count <> SUM(item.package_count)");
|
||||
}
|
||||
|
||||
@@ -108,6 +118,7 @@ class InboundParentChildSqlContractTest {
|
||||
assertThat(sql)
|
||||
.contains("table_name = 'fl_inbound_split_batch'")
|
||||
.contains("uk_inbound_split_source_once")
|
||||
.contains("uk_inbound_one_active_remainder")
|
||||
.contains("table_name = 'fl_inbound_split_item'")
|
||||
.contains("uk_inbound_split_batch_source_child")
|
||||
.contains("is_nullable = 'NO'")
|
||||
|
||||
+16
@@ -107,6 +107,22 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
.doesNotContain("UNIQUE KEY `uk_inbound_split_batch_source_item`");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedEntryEnforcesSingleRemainderWritesAndKeepsLegacyColumns() throws Exception {
|
||||
String sql = normalise(Files.readString(resolveUnified()));
|
||||
|
||||
assertThat(sql)
|
||||
.contains("'fl_inbound_order', 'active_remainder_root_id'")
|
||||
.contains("uk_inbound_one_active_remainder")
|
||||
.contains("uk_inbound_split_source_once")
|
||||
.contains("NEW.loaded_child_inbound_id IS NOT NULL")
|
||||
.contains("NEW.child_inbound_id = batch.remainder_child_inbound_id")
|
||||
.contains("source_order.package_count = NEW.loaded_package_count")
|
||||
.contains("根入仓单原始箱数与已完成分片及当前余量不守恒")
|
||||
.contains("'fl_inbound_split_batch', 'loaded_child_inbound_id'")
|
||||
.contains("fk_inbound_split_loaded_child");
|
||||
}
|
||||
|
||||
private static void assertBlock(String unified, String marker, String nextMarker, String modulePath)
|
||||
throws Exception {
|
||||
int start = unified.indexOf(marker);
|
||||
|
||||
+3
-2
@@ -16,7 +16,7 @@ class SplitPreloadSqlContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void limitsSecondaryInboundDepth() throws Exception {
|
||||
void limitsNewChildrenToRootBasedRemainders() throws Exception {
|
||||
String sql = moduleMigration();
|
||||
assertThat(sql)
|
||||
.contains("parent_inbound_id")
|
||||
@@ -25,7 +25,8 @@ class SplitPreloadSqlContractTest {
|
||||
.contains("split_status")
|
||||
.contains("uk_inbound_parent_split_sequence")
|
||||
.contains("fk_inbound_parent_order")
|
||||
.contains("子入仓单必须标记LOADED或REMAINDER")
|
||||
.contains("新建子入仓单只能作为余量分片")
|
||||
.contains("uk_inbound_one_active_remainder")
|
||||
.contains("入仓单主子归属创建后不可修改");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user