fix: make unified database migrations converge

This commit is contained in:
2026-09-10 23:12:37 +08:00
parent 56635ad4e4
commit 89573fd1f0
5 changed files with 80 additions and 7 deletions
@@ -18580,6 +18580,20 @@ DROP PROCEDURE IF EXISTS migrate_fin_wallet_credit;
DELIMITER $$
CREATE PROCEDURE migrate_fin_wallet_credit()
BEGIN
DECLARE v_legacy_wallet_constraint BOOLEAN DEFAULT FALSE;
DECLARE v_credit_expiry_missing BOOLEAN DEFAULT FALSE;
SET v_legacy_wallet_constraint = EXISTS (
SELECT 1 FROM information_schema.CHECK_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA='baiyue_finance'
AND CONSTRAINT_NAME='chk_fin_wallet_account_balance'
AND CHECK_CLAUSE LIKE '%actual_balance%'
);
SET v_credit_expiry_missing = NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='baiyue_finance'
AND TABLE_NAME='fin_wallet_account'
AND COLUMN_NAME='credit_expires_on'
);
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='baiyue_finance' AND TABLE_NAME='fin_wallet_account' AND COLUMN_NAME='credit_limit') THEN
ALTER TABLE fin_wallet_account
ADD COLUMN credit_limit DECIMAL(20,2) NOT NULL DEFAULT 0 COMMENT '授信额度' AFTER available_balance,
@@ -18588,15 +18602,17 @@ BEGIN
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='baiyue_finance' AND TABLE_NAME='fin_wallet_account' AND COLUMN_NAME='credit_expires_on') THEN
ALTER TABLE fin_wallet_account ADD COLUMN credit_expires_on DATE NULL COMMENT '授信到期日,当天仍有效;次日失效' AFTER credit_frozen;
UPDATE fin_wallet_account
SET available_balance=actual_balance-credit_used-frozen_balance;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA='baiyue_finance' AND TABLE_NAME='fin_wallet_account' AND CONSTRAINT_NAME='chk_fin_wallet_account_balance' AND CONSTRAINT_TYPE='CHECK') THEN
ALTER TABLE fin_wallet_account DROP CHECK chk_fin_wallet_account_balance;
END IF;
IF v_legacy_wallet_constraint OR v_credit_expiry_missing THEN
UPDATE fin_wallet_account
SET actual_balance=actual_balance-credit_used,
credit_used=0,
credit_expires_on=NULL;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA='baiyue_finance' AND TABLE_NAME='fin_wallet_account' AND CONSTRAINT_NAME='chk_fin_wallet_account_balance' AND CONSTRAINT_TYPE='CHECK') THEN
ALTER TABLE fin_wallet_account DROP CHECK chk_fin_wallet_account_balance;
UPDATE fin_wallet_account
SET available_balance=actual_balance-credit_used-frozen_balance;
END IF;
ALTER TABLE fin_wallet_account ADD CONSTRAINT chk_fin_wallet_account_balance
CHECK (frozen_balance >= 0 AND credit_limit >= 0 AND credit_used >= 0 AND credit_frozen >= 0 AND frozen_balance >= credit_frozen);
+11 -2
View File
@@ -1793,6 +1793,10 @@ CREATE TABLE IF NOT EXISTS ll_relabel_item (
KEY idx_ll_relabel_item_destination (customer_id,destination_warehouse_id,application_id)
) ENGINE=InnoDB COMMENT='换标货物冻结明细';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='destination_fba_warehouse_code'), 'SELECT 1', 'ALTER TABLE ll_relabel_item ADD COLUMN destination_fba_warehouse_code VARCHAR(128) NULL COMMENT ''可选FBA目的仓编码(与海外操作仓分离)''')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='source_inventory_balance_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_item ADD COLUMN source_inventory_balance_id BIGINT NULL COMMENT ''原库存余额ID(预入仓换标任务可为空)''')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='source_inventory_position_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_item ADD COLUMN source_inventory_position_id BIGINT NULL COMMENT ''原库存库位ID(预入仓换标任务可为空)''')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='source_inbound_order_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_item ADD COLUMN source_inbound_order_id BIGINT NULL COMMENT ''原入仓单ID(预入仓换标任务可为空)''')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name IN ('shipment_no','destination_fba_warehouse_code','source_inventory_balance_id','source_inventory_position_id','source_inbound_order_id') AND is_nullable='NO'), 'ALTER TABLE ll_relabel_item MODIFY COLUMN shipment_no VARCHAR(128) NULL COMMENT ''货件编号(无FBA去向时为空)'', MODIFY COLUMN destination_fba_warehouse_code VARCHAR(128) NULL COMMENT ''可选FBA目的仓编码(与海外操作仓分离)'', MODIFY COLUMN source_inventory_balance_id BIGINT NULL COMMENT ''原库存余额ID(预入仓换标任务可为空)'', MODIFY COLUMN source_inventory_position_id BIGINT NULL COMMENT ''原库存库位ID(预入仓换标任务可为空)'', MODIFY COLUMN source_inbound_order_id BIGINT NULL COMMENT ''原入仓单ID(预入仓换标任务可为空)''', 'SELECT 1'));
PREPARE relabel_pre_inbound_stmt FROM @ddl; EXECUTE relabel_pre_inbound_stmt; DEALLOCATE PREPARE relabel_pre_inbound_stmt;
@@ -4172,7 +4176,7 @@ BEGIN
AND aggregate_version=0 AND last_event_id='baseline-platform-admin-authorization-v0'
AND (JSON_LENGTH(roles_json) <> 1
OR NOT JSON_CONTAINS(roles_json, JSON_QUOTE('PLATFORM_ADMIN'))
OR JSON_LENGTH(authorities_json) <> 109
OR JSON_LENGTH(authorities_json) <> 105
OR NOT JSON_CONTAINS(authorities_json, JSON_QUOTE('platform:overseas-warehouse:last-mile-order:list'))
OR NOT JSON_CONTAINS(authorities_json, JSON_QUOTE('platform:overseas-warehouse:inbound-order:list'))
OR NOT JSON_CONTAINS(authorities_json, JSON_QUOTE('platform:overseas-warehouse:dispatch:list'))
@@ -4740,7 +4744,12 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
PREPARE ll_delivery_order_bol_no_stmt FROM @ddl; EXECUTE ll_delivery_order_bol_no_stmt; DEALLOCATE PREPARE ll_delivery_order_bol_no_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_order' AND column_name='lastleg_order_id'), 'SELECT 1', 'ALTER TABLE ll_return_order ADD COLUMN lastleg_order_id BIGINT NULL COMMENT ''关联尾程主订单ID'' AFTER customer_reference_no'));
PREPARE ll_return_order_lastleg_id_stmt FROM @ddl; EXECUTE ll_return_order_lastleg_id_stmt; DEALLOCATE PREPARE ll_return_order_lastleg_id_stmt;
UPDATE ll_return_order r JOIN ll_order o ON o.source_order_id = CAST(r.id AS CHAR) AND o.source_order_no = r.return_no AND o.order_type = 'RETURN_RELABEL' SET r.lastleg_order_id = o.id WHERE r.lastleg_order_id IS NULL;
UPDATE ll_return_order r JOIN ll_order o
ON o.source_order_id COLLATE utf8mb4_unicode_ci = CAST(r.id AS CHAR) COLLATE utf8mb4_unicode_ci
AND o.source_order_no COLLATE utf8mb4_unicode_ci = r.return_no COLLATE utf8mb4_unicode_ci
AND o.order_type = 'RETURN_RELABEL'
SET r.lastleg_order_id = o.id
WHERE r.lastleg_order_id IS NULL;
-- 柜级入库计划跨客户批量托盘预留、确认与失败补偿。
USE baiyue_lastlag_warehouse;
@@ -9,6 +9,21 @@ import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class FinanceWalletContractTest {
@Test
void unifiedMigrationDropsTheLegacyBalanceConstraintBeforeRewritingBalances() throws Exception {
Path migration = Path.of("deploy/sql/头程数据库统一增量-MySQL8.sql");
if (!Files.exists(migration)) migration = Path.of("..").resolve(migration);
String sql = Files.readString(migration, StandardCharsets.UTF_8);
int procedure = sql.indexOf("CREATE PROCEDURE migrate_fin_wallet_credit()");
int dropConstraint = sql.indexOf("ALTER TABLE fin_wallet_account DROP CHECK chk_fin_wallet_account_balance", procedure);
int rewriteBalance = sql.indexOf("SET actual_balance=actual_balance-credit_used", procedure);
assertThat(procedure).isGreaterThanOrEqualTo(0);
assertThat(sql.substring(procedure, rewriteBalance))
.contains("CHECK_CLAUSE LIKE '%actual_balance%'", "v_legacy_wallet_constraint OR v_credit_expiry_missing");
assertThat(dropConstraint).isBetween(procedure, rewriteBalance - 1);
}
@Test
void schemaKeepsBalancesConsistentAndRechargeReviewTraceable() throws Exception {
String sql = Files.readString(Path.of("src/main/resources/db/finance.sql"), StandardCharsets.UTF_8);
@@ -1700,6 +1700,10 @@ CREATE TABLE IF NOT EXISTS ll_relabel_item (
KEY idx_ll_relabel_item_destination (customer_id,destination_warehouse_id,application_id)
) ENGINE=InnoDB COMMENT='换标货物冻结明细';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='destination_fba_warehouse_code'), 'SELECT 1', 'ALTER TABLE ll_relabel_item ADD COLUMN destination_fba_warehouse_code VARCHAR(128) NULL COMMENT ''可选FBA目的仓编码(与海外操作仓分离)''')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='source_inventory_balance_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_item ADD COLUMN source_inventory_balance_id BIGINT NULL COMMENT ''原库存余额ID(预入仓换标任务可为空)''')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='source_inventory_position_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_item ADD COLUMN source_inventory_position_id BIGINT NULL COMMENT ''原库存库位ID(预入仓换标任务可为空)''')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='source_inbound_order_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_item ADD COLUMN source_inbound_order_id BIGINT NULL COMMENT ''原入仓单ID(预入仓换标任务可为空)''')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name IN ('shipment_no','destination_fba_warehouse_code','source_inventory_balance_id','source_inventory_position_id','source_inbound_order_id') AND is_nullable='NO'), 'ALTER TABLE ll_relabel_item MODIFY COLUMN shipment_no VARCHAR(128) NULL COMMENT ''货件编号(无FBA去向时为空)'', MODIFY COLUMN destination_fba_warehouse_code VARCHAR(128) NULL COMMENT ''可选FBA目的仓编码(与海外操作仓分离)'', MODIFY COLUMN source_inventory_balance_id BIGINT NULL COMMENT ''原库存余额ID(预入仓换标任务可为空)'', MODIFY COLUMN source_inventory_position_id BIGINT NULL COMMENT ''原库存库位ID(预入仓换标任务可为空)'', MODIFY COLUMN source_inbound_order_id BIGINT NULL COMMENT ''原入仓单ID(预入仓换标任务可为空)''', 'SELECT 1'));
PREPARE relabel_pre_inbound_stmt FROM @ddl; EXECUTE relabel_pre_inbound_stmt; DEALLOCATE PREPARE relabel_pre_inbound_stmt;
@@ -51,6 +51,14 @@ class Phase5OverseasUpgradeSqlContractTest {
"ALTER TABLE ll_dropship_order_profile ADD COLUMN source_pallet_no",
"ADD UNIQUE KEY uk_ll_dropship_sku_inventory(profile_id,sku_code)",
"MODIFY COLUMN source_pallet_no VARCHAR(128) NOT NULL");
int relabelCreateEnd = overseas.indexOf(") ENGINE=InnoDB COMMENT='换标货物冻结明细';");
int relabelModify = overseas.indexOf("MODIFY COLUMN destination_fba_warehouse_code", relabelCreateEnd);
for (String column : List.of("destination_fba_warehouse_code", "source_inventory_balance_id",
"source_inventory_position_id", "source_inbound_order_id")) {
assertThat(overseas.indexOf("ALTER TABLE ll_relabel_item ADD COLUMN " + column, relabelCreateEnd))
.as(column + " converges before grouped MODIFY")
.isBetween(relabelCreateEnd, relabelModify - 1);
}
int start = overseas.indexOf("-- 五期正式结构必须由海外固定入口自行补齐");
int end = overseas.indexOf("USE baiyue_lastleg;", start);
String phase5 = overseas.substring(start, end);
@@ -69,6 +77,27 @@ class Phase5OverseasUpgradeSqlContractTest {
}
}
@Test
void overseasFixedAdminProjectionAssertionMatchesItsAuthorityArray() throws Exception {
String overseas = repositoryFile("deploy/sql/海外区域统一增量-MySQL8.sql");
int start = overseas.indexOf("SET @platform_admin_overseas_authorities = JSON_ARRAY(");
int end = overseas.indexOf(");", start);
String authorityArray = overseas.substring(start, end);
int authorityCount = count(authorityArray, "'platform:");
assertThat(authorityCount).isPositive();
assertThat(overseas).contains("JSON_LENGTH(authorities_json) <> " + authorityCount);
}
@Test
void overseasReturnOrderBackfillNormalizesLegacyColumnCollations() throws Exception {
String overseas = repositoryFile("deploy/sql/海外区域统一增量-MySQL8.sql");
assertThat(overseas).contains(
"o.source_order_id COLLATE utf8mb4_unicode_ci = CAST(r.id AS CHAR) COLLATE utf8mb4_unicode_ci",
"o.source_order_no COLLATE utf8mb4_unicode_ci = r.return_no COLLATE utf8mb4_unicode_ci");
}
@Test
void domesticFixedEntryKeepsIamPermissionsButOwnsNoLastlegBusinessSchema() throws Exception {
String domestic = repositoryFile("deploy/sql/头程数据库统一增量-MySQL8.sql");