chore(deploy): synchronize phase3 unified database migration

This commit is contained in:
2026-08-10 13:31:42 +08:00
parent 88390f9401
commit ac085e0f53
6 changed files with 618 additions and 296 deletions
+446 -292
View File
@@ -6820,8 +6820,191 @@ WHERE TRUE
ON DUPLICATE KEY UPDATE id = id;
-- ============================================================================
-- 来源:order/src/main/resources/db/phase3-order-parent-child-schema.sql
SET NAMES utf8mb4;
USE baiyue_order;
DROP PROCEDURE IF EXISTS phase3_order_add_column;
DELIMITER $$
CREATE PROCEDURE phase3_order_add_column(IN p_table VARCHAR(64), IN p_column 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 AND column_name=p_column) THEN
SET @ddl = CONCAT('ALTER TABLE `', p_table, '` ADD COLUMN `', p_column, '` ', p_definition);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
END IF;
END$$
DELIMITER ;
CALL phase3_order_add_column('ord_order','order_role',"VARCHAR(12) NOT NULL DEFAULT 'MASTER' COMMENT '订单层级:MASTER、CHILD'");
CALL phase3_order_add_column('ord_order','parent_order_id','BIGINT UNSIGNED NULL COMMENT ''直属父订单ID;禁止子订单嵌套''');
CALL phase3_order_add_column('ord_order','root_order_id','BIGINT UNSIGNED NULL COMMENT ''根主订单ID''');
CALL phase3_order_add_column('ord_order','split_sequence','INT UNSIGNED NULL COMMENT ''同一父订单下的同级顺序''');
DROP PROCEDURE IF EXISTS phase3_order_add_index;
DELIMITER $$
CREATE PROCEDURE phase3_order_add_index(IN p_table VARCHAR(64), IN p_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 AND index_name=p_name) THEN
SET @ddl = CONCAT('ALTER TABLE `',p_table,'` ADD ',p_definition);
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
END IF;
END$$
DELIMITER ;
CALL phase3_order_add_index('ord_order','uk_order_parent_sequence',
'UNIQUE KEY `uk_order_parent_sequence` (`tenant_id`,`parent_order_id`,`split_sequence`)');
CALL phase3_order_add_index('ord_order','idx_order_root_role',
'KEY `idx_order_root_role` (`tenant_id`,`root_order_id`,`order_role`,`created_at`,`id`)');
DROP PROCEDURE IF EXISTS phase3_order_parent_fk;
DELIMITER $$
CREATE PROCEDURE phase3_order_parent_fk()
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE()
AND table_name='ord_order' AND constraint_name='fk_order_parent') THEN
ALTER TABLE ord_order DROP FOREIGN KEY fk_order_parent;
END IF;
ALTER TABLE ord_order ADD CONSTRAINT fk_order_parent FOREIGN KEY (parent_order_id) REFERENCES ord_order(id);
END$$
DELIMITER ;
CALL phase3_order_parent_fk();
DROP PROCEDURE IF EXISTS phase3_order_parent_fk;
CREATE TABLE IF NOT EXISTS ord_order_fulfillment_split (
id BIGINT UNSIGNED NOT NULL,
tenant_id BIGINT UNSIGNED NOT NULL,
parent_order_id BIGINT UNSIGNED NOT NULL,
source_order_id BIGINT UNSIGNED NOT NULL,
child_order_id BIGINT UNSIGNED NOT NULL,
split_role VARCHAR(12) NOT NULL COMMENT 'LOADED或REMAINDER',
loading_plan_id BIGINT UNSIGNED NULL,
container_booking_id BIGINT UNSIGNED NULL,
source_package_count INT UNSIGNED NOT NULL,
loaded_package_count INT UNSIGNED NOT NULL,
remaining_package_count INT UNSIGNED NOT NULL,
source_detail_json JSON NULL,
operation_id VARCHAR(64) NOT NULL,
idempotency_key VARCHAR(128) NOT NULL,
request_hash CHAR(64) NOT NULL,
expected_parent_version INT NOT NULL,
created_by BIGINT UNSIGNED NOT NULL,
created_by_name VARCHAR(64) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
version INT NOT NULL DEFAULT 0,
PRIMARY KEY(id),
UNIQUE KEY uk_order_split_operation (tenant_id,operation_id,split_role),
UNIQUE KEY uk_order_split_idempotency (tenant_id,parent_order_id,idempotency_key,split_role),
UNIQUE KEY uk_order_split_child (tenant_id,child_order_id),
KEY idx_order_split_parent (tenant_id,parent_order_id,created_at,id),
KEY idx_order_split_source (tenant_id,source_order_id,split_role),
KEY idx_order_split_container (tenant_id,container_booking_id,created_at,id),
CONSTRAINT fk_order_split_parent FOREIGN KEY(parent_order_id) REFERENCES ord_order(id),
CONSTRAINT fk_order_split_child FOREIGN KEY(child_order_id) REFERENCES ord_order(id),
CONSTRAINT chk_order_split_quantity CHECK (source_package_count > 0 AND loaded_package_count > 0
AND loaded_package_count + remaining_package_count = source_package_count)
) ENGINE=InnoDB COMMENT='父订单→子订单→柜次数量台账';
DROP TRIGGER IF EXISTS trg_ord_order_parent_insert;
DROP TRIGGER IF EXISTS trg_ord_order_parent_update;
DELIMITER $$
CREATE TRIGGER trg_ord_order_parent_insert BEFORE INSERT ON ord_order FOR EACH ROW
BEGIN
IF NEW.order_role='MASTER' THEN
SET NEW.parent_order_id=NULL;
IF NEW.root_order_id IS NULL THEN SET NEW.root_order_id=NEW.id; END IF;
ELSEIF NEW.order_role='CHILD' THEN
IF NEW.parent_order_id IS NULL OR NEW.root_order_id IS NULL OR NEW.split_sequence IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='子订单必须有父订单、根订单和顺序号';
END IF;
IF NEW.root_order_id<>NEW.parent_order_id OR EXISTS(
SELECT 1 FROM ord_order p WHERE p.id=NEW.parent_order_id AND p.order_role<>'MASTER') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='子订单只允许直接挂在主订单下';
END IF;
IF NOT EXISTS(SELECT 1 FROM ord_order p WHERE p.id=NEW.parent_order_id AND p.tenant_id=NEW.tenant_id AND p.deleted=0) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='父订单不存在或客户归属不一致';
END IF;
ELSE
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='订单层级非法';
END IF;
END$$
CREATE TRIGGER trg_ord_order_parent_update BEFORE UPDATE ON ord_order FOR EACH ROW
BEGIN
IF NEW.order_role<>OLD.order_role OR NOT (NEW.parent_order_id<=>OLD.parent_order_id)
OR NOT (NEW.root_order_id<=>OLD.root_order_id) OR NOT (NEW.split_sequence<=>OLD.split_sequence) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='订单父子关系不可修改';
END IF;
END$$
DELIMITER ;
CALL phase3_order_add_column('ord_order_fulfillment_split','split_role',"VARCHAR(12) NOT NULL DEFAULT 'LOADED' COMMENT 'LOADED或REMAINDER'");
CALL phase3_order_add_column('ord_order_fulfillment_split','source_order_id','BIGINT UNSIGNED NULL COMMENT ''实际被消费的来源订单ID''');
CALL phase3_order_add_column('ord_order_fulfillment_split','request_hash',"CHAR(64) NULL COMMENT '请求内容SHA-256;存量记录允许为空'");
-- 旧版台账没有来源字段;当时只允许从根主订单拆分,因此可无歧义回填为父订单。
UPDATE ord_order_fulfillment_split
SET source_order_id = parent_order_id
WHERE source_order_id IS NULL;
-- 旧记录没有请求哈希。生成稳定哨兵哈希可保持非空约束,并让旧幂等键重放明确冲突。
UPDATE ord_order_fulfillment_split
SET request_hash = SHA2(CONCAT('LEGACY:', tenant_id, ':', id), 256)
WHERE request_hash IS NULL OR request_hash = '';
ALTER TABLE ord_order_fulfillment_split
MODIFY source_order_id BIGINT UNSIGNED NOT NULL COMMENT '实际被消费的来源订单ID',
MODIFY request_hash CHAR(64) NOT NULL COMMENT '请求内容SHA-256';
CALL phase3_order_add_index('ord_order_fulfillment_split','idx_order_split_source',
'KEY `idx_order_split_source` (`tenant_id`,`source_order_id`,`split_role`)');
DROP PROCEDURE IF EXISTS phase3_order_drop_split_index;
DELIMITER $$
CREATE PROCEDURE phase3_order_drop_split_index()
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE()
AND table_name='ord_order_fulfillment_split' AND index_name='uk_order_split_operation') THEN
ALTER TABLE ord_order_fulfillment_split DROP INDEX uk_order_split_operation;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE()
AND table_name='ord_order_fulfillment_split' AND index_name='uk_order_split_idempotency') THEN
ALTER TABLE ord_order_fulfillment_split DROP INDEX uk_order_split_idempotency;
END IF;
END$$
DELIMITER ;
CALL phase3_order_drop_split_index();
DROP PROCEDURE IF EXISTS phase3_order_drop_split_index;
CALL phase3_order_add_index('ord_order_fulfillment_split','uk_order_split_operation',
'UNIQUE KEY `uk_order_split_operation` (`tenant_id`,`operation_id`,`split_role`)');
CALL phase3_order_add_index('ord_order_fulfillment_split','uk_order_split_idempotency',
'UNIQUE KEY `uk_order_split_idempotency` (`tenant_id`,`parent_order_id`,`idempotency_key`,`split_role`)');
DROP PROCEDURE IF EXISTS phase3_order_add_column;
DROP PROCEDURE IF EXISTS phase3_order_add_index;
DROP PROCEDURE IF EXISTS phase3_order_assert;
DELIMITER $$
CREATE PROCEDURE phase3_order_assert()
BEGIN
IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='ord_order' AND column_name IN ('order_role','parent_order_id','root_order_id','split_sequence'))<>4 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='订单父子字段未完整创建';
END IF;
IF (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE()
AND table_name='ord_order_fulfillment_split')<>1 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='订单拆分台账未创建';
END IF;
IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='ord_order_fulfillment_split'
AND column_name IN ('source_order_id','request_hash') AND is_nullable='NO')<>2 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='ord_order_fulfillment_split'
AND index_name IN ('idx_order_split_source','uk_order_split_operation',
'uk_order_split_idempotency'))<>3 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='订单拆分来源或幂等索引未完整创建';
END IF;
END$$
DELIMITER ;
CALL phase3_order_assert();
DROP PROCEDURE IF EXISTS phase3_order_assert;
-- 来源:firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql
-- ============================================================================
SET NAMES utf8mb4;
USE baiyue_firstleg;
@@ -6871,10 +7054,19 @@ CALL phase3_inbound_add_column(
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';
DROP PROCEDURE IF EXISTS phase3_inbound_add_column;
DROP PROCEDURE IF EXISTS phase3_inbound_add_index;
@@ -6902,9 +7094,6 @@ 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', 'uk_inbound_one_child',
'UNIQUE KEY `uk_inbound_one_child` (`tenant_id`, `parent_inbound_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`)');
@@ -6929,10 +7118,29 @@ BEGIN
) 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 (
@@ -6967,13 +7175,13 @@ CREATE TABLE IF NOT EXISTS fl_inbound_split_item (
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 NULL COMMENT '来源测量批次ID',
source_measurement_item_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),
PRIMARY KEY (id),
UNIQUE KEY uk_inbound_split_batch_line (tenant_id, split_batch_id, line_no),
UNIQUE KEY uk_inbound_split_batch_source_item (tenant_id, split_batch_id, source_measurement_item_id),
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),
@@ -7039,6 +7247,30 @@ CALL phase3_inbound_add_column(
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;
@@ -7069,6 +7301,15 @@ CALL phase3_inbound_add_relation(
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`)');
@@ -7114,9 +7355,11 @@ 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_item', 'uk_inbound_split_batch_source_item',
'UNIQUE KEY `uk_inbound_split_batch_source_item` (`tenant_id`, `split_batch_id`, `source_measurement_item_id`)');
DROP PROCEDURE IF EXISTS 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 $$
@@ -7130,10 +7373,18 @@ BEGIN
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;
@@ -7156,19 +7407,25 @@ BEGIN
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_status <> 'NOT_SPLIT' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '主子仅两级,子入仓单不可继续拆分';
IF NEW.split_purpose = 'MASTER' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '子入仓单必须标记LOADED或REMAINDER';
END IF;
IF NOT EXISTS (
SELECT 1 FROM fl_inbound_order parent_order
@@ -7186,12 +7443,17 @@ CREATE TRIGGER trg_inbound_order_parent_before_update
BEFORE UPDATE ON fl_inbound_order
FOR EACH ROW
BEGIN
IF NOT (OLD.inbound_role <=> NEW.inbound_role)
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) THEN
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';
@@ -7199,8 +7461,9 @@ BEGIN
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_status <> 'NOT_SPLIT' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '主子仅两级,子入仓单不可继续拆分';
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;
IF NEW.inbound_role = 'MASTER'
AND ((NEW.package_count = 0 AND NEW.split_status <> 'FULLY_SPLIT')
@@ -7213,6 +7476,9 @@ 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
@@ -7226,18 +7492,19 @@ BEGIN
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.parent_inbound_id = NEW.parent_inbound_id
AND child_order.package_count >= NEW.package_count
AND child_order.deleted = 0
AND (NEW.source_measurement_item_id IS NULL OR EXISTS (
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 = NEW.parent_inbound_id
))
AND source_batch.inbound_id = COALESCE(batch.source_inbound_id, batch.parent_inbound_id)
)
) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆分明细与批次、主单或子单不一致';
END IF;
@@ -7259,6 +7526,36 @@ BEGIN
) 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.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
(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'))
) 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;
END$$
CREATE TRIGGER trg_inbound_split_batch_before_update
@@ -7268,16 +7565,40 @@ 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.package_count > 0
AND parent_order.split_status = 'PARTIALLY_SPLIT'
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'
) OR
(SELECT COALESCE(SUM(item.package_count), 0)
FROM fl_inbound_split_item item
@@ -7286,7 +7607,26 @@ BEGIN
(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) <> 1 OR
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
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
EXISTS (SELECT 1
FROM fl_inbound_split_item item
JOIN fl_inbound_order child_order ON child_order.id = item.child_inbound_id
@@ -7365,21 +7705,45 @@ 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')) <> 7 THEN
'master_order_id', 'split_purpose')) <> 8 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '主子入仓单关系字段未完整创建';
END IF;
IF (SELECT COUNT(DISTINCT index_name) FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND index_name IN ('uk_inbound_master_order', 'uk_inbound_one_child',
'uk_inbound_split_operation', 'uk_inbound_split_idempotency',
'uk_inbound_split_history_operation')) <> 5 THEN
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_order'
AND index_name IN ('uk_inbound_master_order', 'uk_inbound_parent_split_sequence')) <> 2
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')) <> 6 THEN
'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
@@ -7399,248 +7763,25 @@ BEGIN
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'fl_inbound_order'
AND index_name = 'uk_inbound_tenant_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 (SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = 'baiyue_firstleg'
AND table_name = 'fl_inbound_carton_label') <> 1 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:稳定箱号映射表未创建';
END IF;
IF (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg'
AND table_name = 'fl_inbound_measurement_item'
AND column_name IN ('box_code', 'machine_no', 'device_measured_at', 'updated_at')) <> 4 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:单箱当前测量字段未完整创建';
END IF;
IF (SELECT COUNT(DISTINCT CONCAT(table_name, '.', index_name))
FROM information_schema.statistics
WHERE table_schema = 'baiyue_firstleg'
AND ((table_name = 'fl_inbound_carton_label'
AND index_name IN ('uk_carton_label_box_code', 'uk_carton_label_index'))
OR (table_name = 'fl_inbound_measurement_item'
AND index_name = 'uk_measure_box_code'))
AND non_unique = 0) <> 3 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:稳定箱号或单箱测量唯一约束未完整创建';
END IF;
IF (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg'
AND table_name = 'fl_mojia_measurement_receipt'
AND column_name IN ('id', 'tenant_id', 'subject_id', 'box_code', 'length_cm', 'width_cm',
'scanned_shipment_id', 'height_cm', 'weight_kg', 'machine_no', 'measured_at', 'image_file_id',
'status', 'match_failure_code', 'match_failure_reason', 'request_id',
'trace_id', 'created_at', 'updated_at')) <> 19 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:墨家未匹配测量收件箱字段未完整创建';
END IF;
IF (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg'
AND table_name = 'fl_inbound_measurement_item'
AND column_name = 'scanned_shipment_id'
AND character_maximum_length = 64) <> 1 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:箱级测量实扫Shipment ID字段未创建';
END IF;
IF (SELECT character_maximum_length FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg'
AND table_name = 'fl_inbound_measurement_item'
AND column_name = 'calculation_rule_version') <> 64 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:测量计算规则版本字段长度不是64';
END IF;
IF (SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = 'baiyue_firstleg'
AND table_name = 'fl_customer_dimension_config') <> 1 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:客户尺寸计算配置表未创建';
END IF;
IF (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = 'baiyue_firstleg'
AND table_name = 'fl_customer_dimension_config'
AND index_name = 'uk_customer_dimension_config_tenant_box'
AND non_unique = 0
AND ((seq_in_index = 1 AND column_name = 'tenant_id')
OR (seq_in_index = 2 AND column_name = 'box_type'))) <> 2 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:客户四箱规尺寸配置唯一约束未创建';
END IF;
IF (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_customer_dimension_config'
AND column_name IN ('id', 'tenant_id', 'box_type', 'inner_length_cm', 'inner_width_cm',
'inner_height_cm', 'edge_addition_cm', 'remark', 'initialization_key',
'created_by', 'created_at', 'updated_by', 'updated_at', 'deleted', 'version')) <> 15 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:客户四箱规尺寸配置字段不完整';
END IF;
IF EXISTS (
SELECT tenant.id
FROM baiyue_iam.iam_tenant tenant
LEFT JOIN baiyue_firstleg.fl_customer_dimension_config config
ON config.tenant_id = tenant.id AND config.deleted = 0
WHERE tenant.tenant_type = 'MERCHANT' AND tenant.deleted = 0 AND tenant.id > 0
GROUP BY tenant.id
HAVING COUNT(config.id) <> 4
OR COUNT(DISTINCT CASE
WHEN config.box_type IN ('SMALL', 'MEDIUM', 'LARGE', 'EXTRA_LARGE')
THEN config.box_type END) <> 4
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;
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission
WHERE id = 5100010 AND tenant_id = 0 AND app_code = 'PLATFORM'
AND permission_code = 'platform:customer:list' AND permission_name = '企业客户'
AND status = 'ACTIVE' AND deleted = 0) <> 1 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:客户尺寸计算配置权限父节点企业客户不存在';
END IF;
IF (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_auth' AND table_name = 'auth_account'
AND column_name = 'password_change_required') <> 1 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:客户管理员强制改密字段未创建';
END IF;
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission
WHERE tenant_id = 0 AND app_code = 'PLATFORM' AND permission_type = 'BUTTON'
AND parent_id = 5100010 AND status = 'ACTIVE' AND deleted = 0
AND ((id = 5100016 AND permission_code = 'platform:customer:reset-password')
OR (id = 5100017 AND permission_code = 'platform:customer:delete')
OR (id = 5100018 AND permission_code = 'platform:customer:impersonate'))) <> 3 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:平台客户操作权限未完整创建';
END IF;
IF (SELECT COUNT(DISTINCT p.permission_code)
FROM baiyue_iam.iam_role_permission rp
JOIN baiyue_iam.iam_role r ON r.id = rp.role_id AND r.tenant_id = rp.tenant_id
JOIN baiyue_iam.iam_permission p ON p.id = rp.permission_id AND p.tenant_id = rp.tenant_id
WHERE r.tenant_id = 0 AND r.app_code = 'PLATFORM' AND r.role_code = 'PLATFORM_ADMIN'
AND p.permission_code IN ('platform:customer:reset-password',
'platform:customer:delete',
'platform:customer:impersonate')) <> 3 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:平台管理员未获得客户操作权限';
END IF;
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission
WHERE tenant_id = 0 AND app_code = 'PLATFORM' AND permission_type = 'BUTTON'
AND parent_id = 5100010 AND status = 'ACTIVE' AND deleted = 0
AND ((id = 5102111 AND permission_code = 'firstleg:dimension-config:view')
OR (id = 5102112 AND permission_code = 'firstleg:dimension-config:edit'))) <> 2 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:客户尺寸计算配置权限码、固定ID或父节点不正确';
END IF;
IF (SELECT COUNT(DISTINCT p.permission_code)
FROM baiyue_iam.iam_role_permission rp
JOIN baiyue_iam.iam_role r ON r.id = rp.role_id AND r.tenant_id = rp.tenant_id
JOIN baiyue_iam.iam_permission p ON p.id = rp.permission_id AND p.tenant_id = rp.tenant_id
WHERE r.tenant_id = 0 AND r.app_code = 'PLATFORM' AND r.role_code = 'PLATFORM_ADMIN'
AND p.permission_code IN ('firstleg:dimension-config:view', 'firstleg:dimension-config:edit')) <> 2 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:平台管理员未获得客户尺寸计算配置读写权限';
END IF;
IF (SELECT COUNT(DISTINCT p.permission_code)
FROM baiyue_iam.iam_role_permission rp
JOIN baiyue_iam.iam_role r ON r.id = rp.role_id AND r.tenant_id = rp.tenant_id
JOIN baiyue_iam.iam_permission p ON p.id = rp.permission_id AND p.tenant_id = rp.tenant_id
WHERE r.tenant_id = 0 AND r.app_code = 'PLATFORM' AND r.role_code = 'ORDER_OPERATOR'
AND p.permission_code = 'firstleg:dimension-config:view') <> 1
OR (SELECT COUNT(*)
FROM baiyue_iam.iam_role_permission rp
JOIN baiyue_iam.iam_role r ON r.id = rp.role_id AND r.tenant_id = rp.tenant_id
JOIN baiyue_iam.iam_permission p ON p.id = rp.permission_id AND p.tenant_id = rp.tenant_id
WHERE r.tenant_id = 0 AND r.app_code = 'PLATFORM' AND r.role_code = 'ORDER_OPERATOR'
AND p.permission_code = 'firstleg:dimension-config:edit') <> 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:订单运营角色的客户尺寸计算配置权限不是只读';
END IF;
IF (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task'
AND column_name IN ('loading_plan_id', 'container_no', 'task_status')) <> 3
OR (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task'
AND column_name IN ('tenant_id', 'inbound_id', 'status', 'expected_count', 'picked_count')) <> 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:智能装柜拣货任务表型不正确';
END IF;
IF (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_loading_pick_task'
AND column_name IN ('tenant_id', 'inbound_id', 'status', 'expected_count', 'picked_count')) <> 5
OR (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_loading_pick_task'
AND column_name IN ('loading_plan_id', 'container_no', 'task_status')) <> 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:入库汇总拣货任务表型不正确';
END IF;
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission
WHERE tenant_id = 0 AND app_code = 'PLATFORM'
AND ((id = 5103040 AND permission_code = 'platform:download:list'
AND permission_name = '下载中心' AND route_path = '/downloads')
OR (id = 5103041 AND permission_code = 'platform:download:all' AND parent_id = 5103040))) <> 2
OR (SELECT COUNT(*) FROM baiyue_iam.iam_permission
WHERE tenant_id = 0 AND app_code = 'PLATFORM'
AND ((id = 5103060 AND permission_code = 'platform:location:list' AND parent_id = 5103000)
OR (id = 5103061 AND permission_code = 'platform:location:create' AND parent_id = 5103060)
OR (id = 5103062 AND permission_code = 'platform:location:edit' AND parent_id = 5103060)
OR (id = 5103063 AND permission_code = 'platform:location:status' AND parent_id = 5103060)
OR (id = 5103064 AND permission_code = 'platform:location:delete' AND parent_id = 5103060))) <> 5 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:下载中心或库位权限语义未修复';
END IF;
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission
WHERE tenant_id = 0 AND app_code = 'PLATFORM'
AND id BETWEEN 5102121 AND 5102129
AND permission_code IN ('firstleg:inbound:split', 'firstleg:inbound:preload',
'firstleg:inbound:preload-view', 'platform:firstleg:inbound:loading:edit',
'platform:firstleg:inbound:loading:view', 'platform:firstleg:inbound:loading:complete',
'platform:firstleg:inbound:delivery:complete', 'platform:firstleg:inbound:picking:edit',
'platform:firstleg:inbound:picking:view')) <> 9 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:三期入库装柜权限 ID 未完成迁移';
END IF;
IF (SELECT COUNT(DISTINCT CONCAT(role.role_code, '|', permission.permission_code))
FROM baiyue_iam.iam_role_permission role_permission
JOIN baiyue_iam.iam_role role
ON role.id = role_permission.role_id AND role.tenant_id = role_permission.tenant_id
JOIN baiyue_iam.iam_permission permission
ON permission.id = role_permission.permission_id
AND permission.tenant_id = role_permission.tenant_id
WHERE role.tenant_id = 0
AND role.role_code IN ('PLATFORM_ADMIN', 'ORDER_OPERATOR')
AND permission.permission_code IN (
'firstleg:inbound:split', 'firstleg:inbound:preload',
'firstleg:inbound:preload-view', 'platform:firstleg:inbound:loading:edit',
'platform:firstleg:inbound:loading:view', 'platform:firstleg:inbound:loading:complete',
'platform:firstleg:inbound:delivery:complete', 'platform:firstleg:inbound:picking:edit',
'platform:firstleg:inbound:picking:view')) <> 18 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:三期入库装柜角色授权未完整写入';
END IF;
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission
WHERE tenant_id = 0 AND app_code = 'PLATFORM'
AND ((id = 5102091 AND permission_code = 'platform:firstleg:container:view' AND parent_id = 5102090)
OR (id = 5102092 AND permission_code = 'platform:firstleg:container:create' AND parent_id = 5102090)
OR (id = 5102093 AND permission_code = 'platform:firstleg:container:edit' AND parent_id = 5102090)
OR (id = 5102094 AND permission_code = 'platform:firstleg:smart-loading:list' AND parent_id = 5102000)
OR (id = 5102096 AND permission_code = 'platform:firstleg:smart-loading:create' AND parent_id = 5102094)
OR (id = 5102097 AND permission_code = 'platform:firstleg:smart-loading:assign' AND parent_id = 5102094)
OR (id = 5102098 AND permission_code = 'platform:firstleg:smart-loading:delete' AND parent_id = 5102094)
OR (id = 5102099 AND permission_code = 'platform:firstleg:smart-loading:push' AND parent_id = 5102094))) <> 8 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:智能装柜权限 ID 或父子关系不正确';
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;
CALL assert_loading_pick_task_shapes(1);
DROP PROCEDURE IF EXISTS assert_loading_pick_task_shapes;
-- ============================================================
-- 三期服务中心订舱与拖车预约
-- 来源:firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql
-- 来源:firstleg/src/main/resources/db/phase3-truck-appointment-schema.sql
-- ============================================================
@@ -8585,7 +8726,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_operation (
UNIQUE KEY uk_loading_pick_operation_id (operation_id),
KEY idx_loading_pick_operation_task (pick_task_id, created_at),
CONSTRAINT fk_loading_pick_operation_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id),
CONSTRAINT chk_loading_pick_operation_action CHECK (action_type IN ('CLAIM', 'ASSIGN', 'REVIEW'))
CONSTRAINT chk_loading_pick_operation_action CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW'))
) ENGINE=InnoDB COMMENT='柜次PDA任务级幂等审计';
DROP TRIGGER IF EXISTS trg_loading_pick_review_before_update;
@@ -8715,43 +8856,57 @@ DROP PROCEDURE IF EXISTS assert_phase3_final_schema;
-- PDA 部分拣货释放策略:NULL/AUTO 按比例释放,MANUAL + [] 表示人工明确不释放。
USE baiyue_firstleg;
DROP PROCEDURE IF EXISTS phase3_pda_add_column;
ALTER TABLE fl_loading_pick_item
ADD COLUMN IF NOT EXISTS release_mode VARCHAR(10) NULL COMMENT 'AUTO按比例释放,MANUAL按人工选择释放' AFTER item_status,
ADD COLUMN IF NOT EXISTS release_location_ids JSON NULL COMMENT '人工选择释放库位ID;空数组表示明确释放0个' AFTER release_mode;
-- PDA 多人协作:领取粒度由整柜任务下沉为单条拣货明细。
CREATE TABLE IF NOT EXISTS fl_loading_pick_item_assignment (
pick_item_id BIGINT UNSIGNED NOT NULL,
assignee_id BIGINT UNSIGNED NOT NULL,
assignee_name VARCHAR(64) NOT NULL,
assigned_by BIGINT UNSIGNED NOT NULL,
assigned_by_name VARCHAR(64) NULL,
assignment_type VARCHAR(16) NOT NULL COMMENT 'CLAIMED/ASSIGNED',
version INT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (pick_item_id),
KEY idx_loading_pick_item_assignee (assignee_id, updated_at),
CONSTRAINT fk_loading_pick_item_assignment_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id),
CONSTRAINT chk_loading_pick_item_assignment_type CHECK (assignment_type IN ('CLAIMED', 'ASSIGNED'))
) ENGINE=InnoDB COMMENT='PDA拣货明细领取与作业员分配';
ALTER TABLE fl_loading_pick_operation
DROP CHECK IF EXISTS chk_loading_pick_operation_action,
ADD CONSTRAINT chk_loading_pick_operation_action
CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW'));
DROP PROCEDURE IF EXISTS assert_phase3_pda_item_assignment_schema;
DELIMITER $$
CREATE PROCEDURE phase3_pda_add_column(IN p_table VARCHAR(64), IN p_column VARCHAR(64), IN p_ddl TEXT)
CREATE PROCEDURE assert_phase3_pda_item_assignment_schema()
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = p_table AND column_name = p_column
) THEN
SET @phase3_pda_ddl = CONCAT('ALTER TABLE `', p_table, '` ADD COLUMN ', p_ddl);
PREPARE phase3_pda_stmt FROM @phase3_pda_ddl;
EXECUTE phase3_pda_stmt;
DEALLOCATE PREPARE phase3_pda_stmt;
IF (SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_pick_item_assignment') <> 1
OR (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_pick_item_assignment'
AND index_name='idx_loading_pick_item_assignee') <> 2
OR (SELECT COUNT(*) FROM information_schema.referential_constraints
WHERE constraint_schema='baiyue_firstleg'
AND constraint_name='fk_loading_pick_item_assignment_item') <> 1 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='PDA明细级多人分配表创建失败';
END IF;
END$$
DELIMITER ;
CALL assert_phase3_pda_item_assignment_schema();
DROP PROCEDURE IF EXISTS assert_phase3_pda_item_assignment_schema;
CALL phase3_pda_add_column('fl_loading_pick_item', 'release_mode',
'`release_mode` VARCHAR(10) NULL COMMENT ''AUTO按比例释放,MANUAL按人工选择释放'' AFTER `item_status`');
CALL phase3_pda_add_column('fl_loading_pick_item', 'release_location_ids',
'`release_location_ids` JSON NULL COMMENT ''人工选择释放库位ID;空数组表示明确释放0个'' AFTER `release_mode`');
CALL phase3_pda_add_column('fl_inbound_pallet_allocation', 'reservation_id',
'`reservation_id` BIGINT UNSIGNED NULL COMMENT ''仓库预占资源唯一ID,A0共享场景用于区分托盘'' AFTER `location_code`');
ALTER TABLE fl_inbound_pallet_allocation
ADD COLUMN IF NOT EXISTS reservation_id BIGINT UNSIGNED NULL COMMENT '仓库预占资源唯一ID,A0共享场景用于区分托盘' AFTER location_code;
SET @phase3_pda_index_ddl = IF(
EXISTS (SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_pallet_allocation'
AND index_name = 'idx_allocation_reservation'),
'SELECT 1',
'ALTER TABLE fl_inbound_pallet_allocation ADD INDEX idx_allocation_reservation (reservation_id)');
PREPARE phase3_pda_index_stmt FROM @phase3_pda_index_ddl;
EXECUTE phase3_pda_index_stmt;
DEALLOCATE PREPARE phase3_pda_index_stmt;
DROP PROCEDURE IF EXISTS phase3_pda_add_column;
/* Phase 3: six-carrier schedule query tasks, truthful manual fallback and three-day rate limit. */
-- 来源:firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql
SET NAMES utf8mb4;
USE baiyue_firstleg;
CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query (
id BIGINT UNSIGNED NOT NULL,
query_no VARCHAR(40) NOT NULL,
@@ -8976,7 +9131,7 @@ DELIMITER ;
CALL assert_carrier_schedule_query_schema();
DROP PROCEDURE IF EXISTS assert_carrier_schedule_query_schema;
-- 三期仓库常用角色初始化。模块来源:iam/src/main/resources/db/phase3-warehouse-roles.sql。
-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql
-- 三期仓库常用系统角色:仓库操作员、仓库拣货员、仓库理货员。
-- 本脚本只组合已经存在的权限资源,需在三期 Firstleg/IAM 权限脚本之后执行,可重复部署。
USE baiyue_iam;
@@ -9195,6 +9350,5 @@ DROP PROCEDURE assert_phase3_warehouse_roles_ready;
DROP TEMPORARY TABLE IF EXISTS tmp_phase3_warehouse_role_permission;
-- 二、三期对外统一使用 INBOUNDED;历史 WAIT_SPLIT 仅由查询适配为 INBOUNDED,统一迁移不改写主表或历史状态。
-- 必须保持为整份脚本最后一个结果,避免后续增量失败却被部署端误判为成功。
SELECT 'PASS' AS migration_status,
'统一增量全部执行完成;PDA多人协作、船司查询和仓库角色最小权限等全部增量及断言通过' AS message;
'统一增量全部执行完成;父子订单、履约分片、装柜门禁、订舱拖车、报关船期、船司查询、仓库角色、费用、公共资源及可靠审计结构校验通过' AS message;
@@ -119,6 +119,14 @@ BEGIN
) 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();
@@ -50,4 +50,74 @@ class Phase3FinalUnifiedSqlContractTest {
.contains("role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')")
.contains("角色授权未完整写入");
}
@Test
void phase3ModuleSqlBlocksAreContiguousAndSynchronised() throws Exception {
String unified = normalise(Files.readString(resolveUnified()));
assertBlock(unified, "-- 来源:order/src/main/resources/db/phase3-order-parent-child-schema.sql",
"-- 来源:firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql",
"order/src/main/resources/db/phase3-order-parent-child-schema.sql");
assertBlock(unified, "-- 来源:firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql",
"-- 来源:firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql",
"firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql");
assertBlock(unified, "-- 来源:firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql",
"-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql",
"firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql");
assertBlock(unified, "-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql",
"-- 二、三期对外统一使用 INBOUNDED;历史 WAIT_SPLIT 仅由查询适配为 INBOUNDED,统一迁移不改写主表或历史状态。",
"iam/src/main/resources/db/phase3-warehouse-roles.sql");
}
@Test
void passIsUniqueAndOnlyTheFinalExecutableStatement() throws Exception {
String sql = normalise(Files.readString(resolveUnified()));
String pass = "SELECT 'PASS' AS migration_status";
assertThat(count(sql, pass)).isEqualTo(1);
int passIndex = sql.lastIndexOf(pass);
assertThat(passIndex).isGreaterThan(sql.lastIndexOf("USE "))
.isGreaterThan(sql.lastIndexOf("ALTER TABLE "))
.isGreaterThan(sql.lastIndexOf("CREATE TABLE "))
.isGreaterThan(sql.lastIndexOf("CALL "));
}
@Test
void legacySplitIndexesAreOnlyDroppedAndNeverRecreated() throws Exception {
String sql = normalise(Files.readString(resolveUnified()));
assertThat(sql)
.contains("DROP INDEX uk_inbound_one_child")
.contains("DROP INDEX uk_inbound_one_secondary")
.doesNotContain("UNIQUE KEY `uk_inbound_one_child`")
.doesNotContain("UNIQUE KEY `uk_inbound_one_secondary`")
.doesNotContain("UNIQUE KEY `uk_inbound_split_batch_source_item`");
}
private static void assertBlock(String unified, String marker, String nextMarker, String modulePath)
throws Exception {
int start = unified.indexOf(marker);
int next = unified.indexOf(nextMarker, start + marker.length());
assertThat(start).as("missing marker %s", marker).isGreaterThanOrEqualTo(0);
assertThat(next).as("missing next marker %s", nextMarker).isGreaterThan(start);
String module = normalise(Files.readString(resolve(modulePath))).trim();
String block = unified.substring(start + marker.length(), next).trim();
assertThat(block).as("module block drift: %s", modulePath)
.startsWith(module);
}
private static Path resolveUnified() {
return resolve("deploy/sql/头程数据库统一增量-MySQL8.sql");
}
private static Path resolve(String path) {
Path direct = Path.of(path);
if (Files.exists(direct)) return direct;
return Path.of("..", path).normalize();
}
private static String normalise(String value) {
return value.replace("\r\n", "\n").replace("\uFEFF", "");
}
private static int count(String value, String token) {
return value.split(java.util.regex.Pattern.quote(token), -1).length - 1;
}
}
@@ -19,7 +19,7 @@ class WarehouseRoleSqlContractTest {
void unifiedDeploymentContainsTheSameWarehouseRoleContract() throws Exception {
String sql = unifiedSql();
assertWarehouseRoleContract(sql);
assertThat(sql)
assertThat(roleMigrationBlock(sql))
.doesNotContain("\n ADD COLUMN IF NOT EXISTS")
.doesNotContain("DROP CHECK IF EXISTS");
assertThat(sql.lastIndexOf("SELECT 'PASS' AS migration_status"))
@@ -97,6 +97,15 @@ class WarehouseRoleSqlContractTest {
return sql.substring(start, end > start ? end : sql.length());
}
private static String roleMigrationBlock(String sql) {
String marker = "-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql";
String endMarker = "-- 二、三期对外统一使用 INBOUNDED;历史 WAIT_SPLIT 仅由查询适配为 INBOUNDED,统一迁移不改写主表或历史状态。";
int start = sql.indexOf(marker);
int end = sql.indexOf(endMarker, start + marker.length());
if (start < 0 || end < 0) throw new IllegalStateException("统一入口缺少仓库角色连续区块");
return sql.substring(start, end);
}
private static String moduleSql() throws Exception {
return new ClassPathResource("db/phase3-warehouse-roles.sql")
.getContentAsString(StandardCharsets.UTF_8);
@@ -16,7 +16,6 @@ CALL phase3_order_add_column('ord_order','order_role',"VARCHAR(12) NOT NULL DEFA
CALL phase3_order_add_column('ord_order','parent_order_id','BIGINT UNSIGNED NULL COMMENT ''直属父订单ID;禁止子订单嵌套''');
CALL phase3_order_add_column('ord_order','root_order_id','BIGINT UNSIGNED NULL COMMENT ''根主订单ID''');
CALL phase3_order_add_column('ord_order','split_sequence','INT UNSIGNED NULL COMMENT ''同一父订单下的同级顺序''');
DROP PROCEDURE IF EXISTS phase3_order_add_column;
DROP PROCEDURE IF EXISTS phase3_order_add_index;
DELIMITER $$
@@ -33,7 +32,6 @@ CALL phase3_order_add_index('ord_order','uk_order_parent_sequence',
'UNIQUE KEY `uk_order_parent_sequence` (`tenant_id`,`parent_order_id`,`split_sequence`)');
CALL phase3_order_add_index('ord_order','idx_order_root_role',
'KEY `idx_order_root_role` (`tenant_id`,`root_order_id`,`order_role`,`created_at`,`id`)');
DROP PROCEDURE IF EXISTS phase3_order_add_index;
DROP PROCEDURE IF EXISTS phase3_order_parent_fk;
DELIMITER $$
@@ -118,7 +116,14 @@ DELIMITER ;
CALL phase3_order_add_column('ord_order_fulfillment_split','split_role',"VARCHAR(12) NOT NULL DEFAULT 'LOADED' COMMENT 'LOADED或REMAINDER'");
CALL phase3_order_add_column('ord_order_fulfillment_split','source_order_id','BIGINT UNSIGNED NULL COMMENT ''实际被消费的来源订单ID''');
CALL phase3_order_add_column('ord_order_fulfillment_split','request_hash',"CHAR(64) NULL COMMENT '请求内容SHA-256;存量记录允许为空'");
-- 存量空来源必须先由发布前数据核对补齐;P0 契约不允许继续写无来源台账。
-- 旧版台账没有来源字段;当时只允许从根主订单拆分,因此可无歧义回填为父订单。
UPDATE ord_order_fulfillment_split
SET source_order_id = parent_order_id
WHERE source_order_id IS NULL;
-- 旧记录没有请求哈希。生成稳定哨兵哈希可保持非空约束,并让旧幂等键重放明确冲突。
UPDATE ord_order_fulfillment_split
SET request_hash = SHA2(CONCAT('LEGACY:', tenant_id, ':', id), 256)
WHERE request_hash IS NULL OR request_hash = '';
ALTER TABLE ord_order_fulfillment_split
MODIFY source_order_id BIGINT UNSIGNED NOT NULL COMMENT '实际被消费的来源订单ID',
MODIFY request_hash CHAR(64) NOT NULL COMMENT '请求内容SHA-256';
@@ -145,6 +150,8 @@ CALL phase3_order_add_index('ord_order_fulfillment_split','uk_order_split_operat
'UNIQUE KEY `uk_order_split_operation` (`tenant_id`,`operation_id`,`split_role`)');
CALL phase3_order_add_index('ord_order_fulfillment_split','uk_order_split_idempotency',
'UNIQUE KEY `uk_order_split_idempotency` (`tenant_id`,`parent_order_id`,`idempotency_key`,`split_role`)');
DROP PROCEDURE IF EXISTS phase3_order_add_column;
DROP PROCEDURE IF EXISTS phase3_order_add_index;
DROP PROCEDURE IF EXISTS phase3_order_assert;
DELIMITER $$
@@ -158,6 +165,17 @@ BEGIN
AND table_name='ord_order_fulfillment_split')<>1 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='订单拆分台账未创建';
END IF;
IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='ord_order_fulfillment_split'
AND column_name IN ('source_order_id','request_hash') AND is_nullable='NO')<>2 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='ord_order_fulfillment_split'
AND index_name IN ('idx_order_split_source','uk_order_split_operation',
'uk_order_split_idempotency'))<>3 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='订单拆分来源或幂等索引未完整创建';
END IF;
END$$
DELIMITER ;
CALL phase3_order_assert();
@@ -0,0 +1,63 @@
package com.baiyue.order.repository;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class OrderParentChildSqlContractTest {
@Test
void sourceAndRequestHashAreRequiredAndHelpersLiveUntilFinalCall() throws Exception {
String sql = read(resolveModule());
assertThat(sql)
.contains("source_order_id BIGINT UNSIGNED NOT NULL")
.contains("KEY idx_order_split_source (tenant_id,source_order_id,split_role)")
.contains("MODIFY source_order_id BIGINT UNSIGNED NOT NULL")
.contains("MODIFY request_hash CHAR(64) NOT NULL")
.contains("UPDATE ord_order_fulfillment_split")
.contains("SHA2(CONCAT('LEGACY:', tenant_id, ':', id), 256)");
assertThat(sql.lastIndexOf("DROP PROCEDURE IF EXISTS phase3_order_add_column"))
.isGreaterThan(sql.lastIndexOf("CALL phase3_order_add_column"));
assertThat(sql.lastIndexOf("DROP PROCEDURE IF EXISTS phase3_order_add_index"))
.isGreaterThan(sql.lastIndexOf("CALL phase3_order_add_index"));
}
@Test
void unifiedEntryContainsTheExactOrderBlockBeforeFirstleg() throws Exception {
String unified = read(resolve("deploy/sql/头程数据库统一增量-MySQL8.sql"));
String module = read(resolveModule()).trim();
String marker = "-- 来源:order/src/main/resources/db/phase3-order-parent-child-schema.sql";
String next = "-- 来源:firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql";
int start = unified.indexOf(marker);
int end = unified.indexOf(next, start + marker.length());
assertThat(start).isGreaterThanOrEqualTo(0);
assertThat(end).isGreaterThan(start);
assertThat(unified.substring(start + marker.length(), end).trim()).startsWith(module);
assertThat(unified.lastIndexOf("SELECT 'PASS' AS migration_status"))
.isGreaterThan(end);
}
private static Path resolve(String path) {
Path direct = Path.of(path);
return Files.exists(direct) ? direct : Path.of("..", path).normalize();
}
private static Path resolveModule() {
Path direct = Path.of("src/main/resources/db/phase3-order-parent-child-schema.sql");
return Files.exists(direct)
? direct
: Path.of("order/src/main/resources/db/phase3-order-parent-child-schema.sql");
}
private static String read(Path path) throws Exception {
return Files.readString(path, StandardCharsets.UTF_8)
.replace("\r\n", "\n")
.replace("\uFEFF", "");
}
}