2026-08-06 11:49:51 +08:00
SET NAMES utf8mb4 ;
USE baiyue_firstleg ;
/* 三期分柜与预装清单;独立于现有测量、计费表,可重复部署。 */
CREATE TABLE IF NOT EXISTS fl_preload_plan (
id BIGINT UNSIGNED NOT NULL COMMENT '预装清单ID' ,
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID' ,
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单/订单ID' ,
status VARCHAR ( 16 ) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/CONFIRMING/CONFIRMED' ,
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本' ,
items_json JSON NOT NULL COMMENT '可编辑分柜明细快照' ,
remark VARCHAR ( 1000 ) NULL COMMENT '预装备注' ,
created_by BIGINT UNSIGNED NOT NULL ,
created_by_name VARCHAR ( 64 ) NULL ,
updated_by BIGINT UNSIGNED NOT NULL ,
updated_by_name VARCHAR ( 64 ) NULL ,
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 ( id ),
UNIQUE KEY uk_preload_tenant_inbound ( tenant_id , inbound_id ),
KEY idx_preload_status ( tenant_id , status , updated_at ),
CONSTRAINT fk_preload_inbound FOREIGN KEY ( inbound_id ) REFERENCES fl_inbound_order ( id )
) ENGINE = InnoDB COMMENT = '订单分柜预装清单' ;
CREATE TABLE IF NOT EXISTS fl_split_operation (
id BIGINT UNSIGNED NOT NULL COMMENT '幂等记录ID' ,
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID' ,
inbound_id BIGINT UNSIGNED NOT NULL ,
operator_id BIGINT UNSIGNED NOT NULL ,
operation VARCHAR ( 40 ) NOT NULL ,
idempotency_key VARCHAR ( 128 ) NOT NULL ,
status VARCHAR ( 16 ) NOT NULL DEFAULT 'PROCESSING' COMMENT 'PROCESSING/SUCCESS' ,
result_json JSON NULL ,
created_at DATETIME ( 3 ) NOT NULL DEFAULT CURRENT_TIMESTAMP ( 3 ),
PRIMARY KEY ( id ),
UNIQUE KEY uk_split_operation ( tenant_id , inbound_id , operator_id , operation , idempotency_key ),
KEY idx_split_operation_time ( tenant_id , inbound_id , created_at ),
CONSTRAINT fk_split_operation_inbound FOREIGN KEY ( inbound_id ) REFERENCES fl_inbound_order ( id )
) ENGINE = InnoDB COMMENT = '分柜操作幂等与审计索引' ;
2026-08-06 17:48:50 +08:00
/*
* 部分装柜拆单契约:
2026-08-10 12:22:29 +08:00
* 本文件仅保留历史部分锁定明细;主单、LOADED/REMAINDER 多分片及层级约束
* 统一由 phase3-inbound-parent-child-schema.sql 维护。
2026-08-06 17:48:50 +08:00
*/
DROP PROCEDURE IF EXISTS phase3_split_add_column ;
DELIMITER $$
CREATE PROCEDURE phase3_split_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 @ phase3_split_ddl = CONCAT ( 'ALTER TABLE `' , p_table_name , '` ADD COLUMN `' ,
p_column_name , '` ' , p_definition );
PREPARE phase3_split_stmt FROM @ phase3_split_ddl ;
EXECUTE phase3_split_stmt ;
DEALLOCATE PREPARE phase3_split_stmt ;
END IF ;
END $$
DELIMITER ;
CALL phase3_split_add_column ( 'fl_inbound_order' , 'parent_inbound_id' ,
'BIGINT UNSIGNED NULL COMMENT ''生成本单的主入库单ID;仅二级子单有值'' AFTER id' );
CALL phase3_split_add_column ( 'fl_inbound_order' , 'split_level' ,
'TINYINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN parent_inbound_id IS NULL THEN 0 ELSE 1 END) STORED COMMENT ''拆单层级:0主单、1二级子单'' AFTER parent_inbound_id' );
CALL phase3_split_add_column ( 'fl_inbound_order' , 'split_status' ,
'VARCHAR(24) NOT NULL DEFAULT ''NOT_SPLIT'' COMMENT ''NOT_SPLIT/PARTIALLY_SPLIT/FULLY_SPLIT'' AFTER split_level' );
DROP PROCEDURE IF EXISTS phase3_split_add_column ;
SET @ phase3_split_index_exists = (
SELECT COUNT ( * ) FROM information_schema . statistics
WHERE table_schema = DATABASE () AND table_name = 'fl_inbound_order'
AND index_name = 'uk_inbound_one_secondary'
);
2026-08-10 12:22:29 +08:00
SET @ phase3_split_ddl = IF ( @ phase3_split_index_exists > 0 ,
'ALTER TABLE fl_inbound_order DROP INDEX uk_inbound_one_secondary' ,
2026-08-06 17:48:50 +08:00
'SELECT 1' );
PREPARE phase3_split_stmt FROM @ phase3_split_ddl ;
EXECUTE phase3_split_stmt ;
DEALLOCATE PREPARE phase3_split_stmt ;
SET @ phase3_split_fk_exists = (
SELECT COUNT ( * ) FROM information_schema . table_constraints
WHERE constraint_schema = DATABASE () AND table_name = 'fl_inbound_order'
AND constraint_name = 'fk_inbound_parent' AND constraint_type = 'FOREIGN KEY'
);
2026-08-10 12:22:29 +08:00
SET @ phase3_split_ddl = IF ( @ phase3_split_fk_exists > 0 ,
'ALTER TABLE fl_inbound_order DROP FOREIGN KEY fk_inbound_parent' ,
2026-08-06 17:48:50 +08:00
'SELECT 1' );
PREPARE phase3_split_stmt FROM @ phase3_split_ddl ;
EXECUTE phase3_split_stmt ;
DEALLOCATE PREPARE phase3_split_stmt ;
CREATE TABLE IF NOT EXISTS fl_inbound_split_detail (
id BIGINT UNSIGNED NOT NULL COMMENT '部分锁定明细ID' ,
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID' ,
parent_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '主入库单ID' ,
child_inbound_id BIGINT UNSIGNED NULL COMMENT '完成拆单后生成的二级子入库单ID' ,
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '原订单货物明细ID,不建立跨库外键' ,
source_package_count INT UNSIGNED NOT NULL COMMENT '拆分前该明细总箱数快照' ,
locked_package_count INT UNSIGNED NOT NULL COMMENT '本次装柜锁定箱数' ,
remaining_package_count INT UNSIGNED GENERATED ALWAYS AS (
source_package_count - locked_package_count
) STORED COMMENT '生成二级子单的剩余箱数' ,
status VARCHAR ( 16 ) NOT NULL DEFAULT 'LOCKED' COMMENT 'LOCKED/COMPLETED' ,
operation_id VARCHAR ( 128 ) NOT NULL COMMENT '部分锁定操作幂等号' ,
created_by BIGINT UNSIGNED NOT NULL ,
created_by_name VARCHAR ( 64 ) NULL ,
completed_by BIGINT UNSIGNED NULL ,
completed_by_name VARCHAR ( 64 ) NULL ,
completed_at DATETIME ( 3 ) NULL ,
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 ),
version INT UNSIGNED NOT NULL DEFAULT 0 ,
PRIMARY KEY ( id ),
UNIQUE KEY uk_inbound_split_parent_detail ( tenant_id , parent_inbound_id , inbound_detail_id ),
UNIQUE KEY uk_inbound_split_child_detail ( tenant_id , child_inbound_id , inbound_detail_id ),
UNIQUE KEY uk_inbound_split_operation ( operation_id ),
KEY idx_inbound_split_status ( tenant_id , parent_inbound_id , status ),
CONSTRAINT fk_inbound_split_detail_parent FOREIGN KEY ( parent_inbound_id ) REFERENCES fl_inbound_order ( id ),
CONSTRAINT fk_inbound_split_detail_child FOREIGN KEY ( child_inbound_id ) REFERENCES fl_inbound_order ( id ),
CONSTRAINT chk_inbound_split_partial CHECK (
source_package_count > 1 AND locked_package_count > 0
AND locked_package_count < source_package_count
),
CONSTRAINT chk_inbound_split_status CHECK ( status IN ( 'LOCKED' , 'COMPLETED' )),
CONSTRAINT chk_inbound_split_completion CHECK (
( status = 'LOCKED' AND child_inbound_id IS NULL AND completed_at IS NULL )
OR ( status = 'COMPLETED' AND child_inbound_id IS NOT NULL AND completed_at IS NOT NULL )
)
) ENGINE = InnoDB COMMENT = '按箱数部分锁定及剩余箱数生成二级子入库单的不可变契约' ;
DROP TRIGGER IF EXISTS trg_inbound_split_before_insert ;
DROP TRIGGER IF EXISTS trg_inbound_split_before_update ;
DROP TRIGGER IF EXISTS trg_inbound_split_detail_before_insert ;
DROP TRIGGER IF EXISTS trg_inbound_split_detail_before_update ;
DROP TRIGGER IF EXISTS trg_inbound_split_detail_before_delete ;
DELIMITER $$
CREATE TRIGGER trg_inbound_split_detail_before_insert
BEFORE INSERT ON fl_inbound_split_detail
FOR EACH ROW
BEGIN
DECLARE v_parent_tenant BIGINT UNSIGNED ;
DECLARE v_parent_parent BIGINT UNSIGNED ;
SELECT tenant_id , parent_inbound_id INTO v_parent_tenant , v_parent_parent
FROM fl_inbound_order WHERE id = NEW . parent_inbound_id AND deleted = 0 ;
IF v_parent_tenant <> NEW . tenant_id OR v_parent_parent IS NOT NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '部分锁定只能发生在客户所属主入库单' ;
END IF ;
IF NEW . status <> 'LOCKED' OR NEW . child_inbound_id IS NOT NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '部分锁定创建时不能提前绑定二级子入库单' ;
END IF ;
END $$
CREATE TRIGGER trg_inbound_split_detail_before_update
BEFORE UPDATE ON fl_inbound_split_detail
FOR EACH ROW
BEGIN
DECLARE v_child_tenant BIGINT UNSIGNED ;
DECLARE v_child_parent BIGINT UNSIGNED ;
IF NEW . tenant_id <> OLD . tenant_id OR NEW . parent_inbound_id <> OLD . parent_inbound_id
OR NEW . inbound_detail_id <> OLD . inbound_detail_id
OR NEW . source_package_count <> OLD . source_package_count
OR NEW . locked_package_count <> OLD . locked_package_count
OR NEW . operation_id <> OLD . operation_id THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '部分锁定的归属和箱数创建后不可修改' ;
END IF ;
IF OLD . status <> 'LOCKED' OR NEW . status <> 'COMPLETED' OR NEW . child_inbound_id IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '部分锁定只允许完成并绑定二级子入库单' ;
END IF ;
SELECT tenant_id , parent_inbound_id INTO v_child_tenant , v_child_parent
FROM fl_inbound_order WHERE id = NEW . child_inbound_id AND deleted = 0 ;
IF v_child_tenant <> NEW . tenant_id OR v_child_parent <> NEW . parent_inbound_id THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '剩余箱数必须绑定主单唯一二级子入库单' ;
END IF ;
END $$
CREATE TRIGGER trg_inbound_split_detail_before_delete
BEFORE DELETE ON fl_inbound_split_detail
FOR EACH ROW
BEGIN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '部分锁定与拆单明细不可删除' ;
END $$
DELIMITER ;
2026-08-10 12:22:29 +08:00
-- 主子关系及不可回退规则由 phase3-inbound-parent-child-schema.sql 统一维护。
-- 旧触发器要求“一父一子”,会阻止 LOADED/REMAINDER 多分片,迁移完成后必须移除。
DROP TRIGGER IF EXISTS trg_inbound_split_before_insert ;
DROP TRIGGER IF EXISTS trg_inbound_split_before_update ;
2026-08-06 17:48:50 +08:00
DROP PROCEDURE IF EXISTS phase3_split_assert_contract ;
DELIMITER $$
CREATE PROCEDURE phase3_split_assert_contract ()
BEGIN
IF ( SELECT COUNT ( * ) FROM information_schema . columns
WHERE table_schema = DATABASE () AND table_name = 'fl_inbound_order'
AND column_name IN ( 'parent_inbound_id' , 'split_level' , 'split_status' )) <> 3 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆单迁移失败:入库单主子关系字段未完整创建' ;
END IF ;
IF ( SELECT COUNT ( * ) FROM information_schema . tables
WHERE table_schema = DATABASE () AND table_name = 'fl_inbound_split_detail' ) <> 1 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆单迁移失败:部分锁定明细表未创建' ;
END IF ;
IF ( SELECT COUNT ( DISTINCT index_name ) FROM information_schema . statistics
WHERE table_schema = DATABASE ()
2026-08-10 12:22:29 +08:00
AND index_name IN ( 'uk_inbound_split_parent_detail' ,
'uk_inbound_split_child_detail' , 'uk_inbound_split_operation' )) <> 3 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆单迁移失败:部分锁定幂等索引缺失' ;
2026-08-06 17:48:50 +08:00
END IF ;
IF ( SELECT COUNT ( * ) FROM information_schema . triggers
WHERE trigger_schema = DATABASE ()
2026-08-10 12:22:29 +08:00
AND trigger_name IN ( 'trg_inbound_split_detail_before_insert' ,
2026-08-06 17:48:50 +08:00
'trg_inbound_split_detail_before_update' ,
2026-08-10 12:22:29 +08:00
'trg_inbound_split_detail_before_delete' )) <> 3 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拆单迁移失败:部分锁定触发器未完整创建' ;
2026-08-06 17:48:50 +08:00
END IF ;
END $$
DELIMITER ;
CALL phase3_split_assert_contract ();
DROP PROCEDURE IF EXISTS phase3_split_assert_contract ;