diff --git a/deploy/sql/migrations/cn/baiyue_file/V1__align_schema_baseline.sql b/deploy/sql/migrations/cn/baiyue_file/V1__align_schema_baseline.sql index f80f99e07..da1641e0b 100644 --- a/deploy/sql/migrations/cn/baiyue_file/V1__align_schema_baseline.sql +++ b/deploy/sql/migrations/cn/baiyue_file/V1__align_schema_baseline.sql @@ -8,13 +8,17 @@ SET NAMES utf8mb4; SET @guard = IF( EXISTS (SELECT 1 FROM file_object WHERE CHAR_LENGTH(business_type) > 32 LIMIT 1), - 'SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''file_object.business_type exceeds 32 characters''', + 'SELECT 1 FROM migration_blocked_file_business_type_exceeds_32_chars', 'SELECT 1' ); PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; -ALTER TABLE file_object - MODIFY COLUMN business_type varchar(32) DEFAULT NULL COMMENT '关联业务类型'; +SET @ddl = IF( + EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'file_object' AND column_name = 'business_type' AND (column_type <> 'varchar(32)' OR is_nullable <> 'YES')), + 'ALTER TABLE file_object MODIFY COLUMN business_type varchar(32) DEFAULT NULL COMMENT ''关联业务类型''', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; CREATE TABLE IF NOT EXISTS file_pda_release_channel ( app_code varchar(16) NOT NULL, diff --git a/deploy/sql/migrations/cn/baiyue_firstleg/V1__align_schema_baseline.sql b/deploy/sql/migrations/cn/baiyue_firstleg/V1__align_schema_baseline.sql index f63d365d5..3ae25fb1d 100644 --- a/deploy/sql/migrations/cn/baiyue_firstleg/V1__align_schema_baseline.sql +++ b/deploy/sql/migrations/cn/baiyue_firstleg/V1__align_schema_baseline.sql @@ -8,10 +8,14 @@ SET NAMES utf8mb4; SET @guard = IF( EXISTS (SELECT 1 FROM fl_dropship_pallet WHERE pallet_no IS NULL LIMIT 1), - 'SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''fl_dropship_pallet.pallet_no contains NULL''', + 'SELECT 1 FROM migration_blocked_firstleg_pallet_no_contains_null', 'SELECT 1' ); PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; -ALTER TABLE fl_dropship_pallet - MODIFY COLUMN pallet_no varchar(32) NOT NULL COMMENT '现场扫描的系统DP托盘码'; +SET @ddl = IF( + EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'fl_dropship_pallet' AND column_name = 'pallet_no' AND (column_type <> 'varchar(32)' OR is_nullable <> 'NO')), + 'ALTER TABLE fl_dropship_pallet MODIFY COLUMN pallet_no varchar(32) NOT NULL COMMENT ''现场扫描的系统DP托盘码''', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/deploy/sql/migrations/cn/baiyue_iam/V1__align_schema_baseline.sql b/deploy/sql/migrations/cn/baiyue_iam/V1__align_schema_baseline.sql index eee539c55..e6005ed14 100644 --- a/deploy/sql/migrations/cn/baiyue_iam/V1__align_schema_baseline.sql +++ b/deploy/sql/migrations/cn/baiyue_iam/V1__align_schema_baseline.sql @@ -8,28 +8,36 @@ SET NAMES utf8mb4; SET @guard = IF( EXISTS (SELECT 1 FROM iam_tenant WHERE customer_nickname IS NULL LIMIT 1), - 'SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''iam_tenant.customer_nickname contains NULL''', + 'SELECT 1 FROM migration_blocked_iam_customer_nickname_contains_null', 'SELECT 1' ); PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @guard = IF( EXISTS (SELECT 1 FROM iam_tenant WHERE CHAR_LENGTH(tenant_code) > 8 LIMIT 1), - 'SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''iam_tenant.tenant_code exceeds 8 characters''', + 'SELECT 1 FROM migration_blocked_iam_tenant_code_exceeds_8_chars', 'SELECT 1' ); PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @guard = IF( EXISTS (SELECT 1 FROM iam_tenant_qualification WHERE original_file_name IS NULL LIMIT 1), - 'SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''iam_tenant_qualification.original_file_name contains NULL''', + 'SELECT 1 FROM migration_blocked_iam_original_file_name_contains_null', 'SELECT 1' ); PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; -ALTER TABLE iam_tenant - MODIFY COLUMN tenant_code varchar(8) NOT NULL COMMENT '客户编号:企业客户固定五位数字,平台技术主体为PLATFORM', - MODIFY COLUMN customer_nickname varchar(32) NOT NULL COMMENT '客户昵称:创建时默认取公司名称中文拼音大写首字母,可修改'; +SET @ddl = IF( + EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'iam_tenant' AND column_name = 'tenant_code' AND (column_type <> 'varchar(8)' OR is_nullable <> 'NO')) + OR EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'iam_tenant' AND column_name = 'customer_nickname' AND (column_type <> 'varchar(32)' OR is_nullable <> 'NO')), + 'ALTER TABLE iam_tenant MODIFY COLUMN tenant_code varchar(8) NOT NULL COMMENT ''客户编号:企业客户固定五位数字,平台技术主体为PLATFORM'', MODIFY COLUMN customer_nickname varchar(32) NOT NULL COMMENT ''客户昵称:创建时默认取公司名称中文拼音大写首字母,可修改''', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; -ALTER TABLE iam_tenant_qualification - MODIFY COLUMN original_file_name varchar(255) NOT NULL COMMENT '上传时原始文件名'; +SET @ddl = IF( + EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'iam_tenant_qualification' AND column_name = 'original_file_name' AND (column_type <> 'varchar(255)' OR is_nullable <> 'NO')), + 'ALTER TABLE iam_tenant_qualification MODIFY COLUMN original_file_name varchar(255) NOT NULL COMMENT ''上传时原始文件名''', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/deploy/sql/migrations/overseas/baiyue_lastleg/V2__align_schema_baseline.sql b/deploy/sql/migrations/overseas/baiyue_lastleg/V2__align_schema_baseline.sql index 610f04578..b8369fedf 100644 --- a/deploy/sql/migrations/overseas/baiyue_lastleg/V2__align_schema_baseline.sql +++ b/deploy/sql/migrations/overseas/baiyue_lastleg/V2__align_schema_baseline.sql @@ -6,6 +6,22 @@ SET NAMES utf8mb4; +-- 收紧测试库漂移字段前先阻断不满足目标定义的数据,不执行自动回填或清理。 +SET @guard = IF(EXISTS (SELECT 1 FROM ll_delivery_order WHERE delivery_group_key IS NULL LIMIT 1), 'SELECT 1 FROM migration_blocked_lastleg_delivery_group_key_null', 'SELECT 1'); +PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @guard = IF(EXISTS (SELECT 1 FROM ll_local_looseboard_order WHERE container_type IS NULL LIMIT 1), 'SELECT 1 FROM migration_blocked_lastleg_looseboard_container_type_null', 'SELECT 1'); +PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @guard = IF(EXISTS (SELECT 1 FROM ll_us_truck_order WHERE container_type IS NULL LIMIT 1), 'SELECT 1 FROM migration_blocked_lastleg_truck_container_type_null', 'SELECT 1'); +PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @guard = IF(EXISTS (SELECT 1 FROM ll_outbound_pda_pick WHERE inventory_position_id IS NULL LIMIT 1), 'SELECT 1 FROM migration_blocked_lastleg_inventory_position_id_null', 'SELECT 1'); +PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @guard = IF(EXISTS (SELECT 1 FROM ll_relabel_item WHERE new_sku_code IS NULL LIMIT 1), 'SELECT 1 FROM migration_blocked_lastleg_new_sku_code_null', 'SELECT 1'); +PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @guard = IF(EXISTS (SELECT 1 FROM ll_relabel_item WHERE source_warehouse_id IS NULL LIMIT 1), 'SELECT 1 FROM migration_blocked_lastleg_source_warehouse_id_null', 'SELECT 1'); +PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @guard = IF(EXISTS (SELECT 1 FROM ll_relabel_pda_task WHERE CHAR_LENGTH(review_decision) > 16 LIMIT 1), 'SELECT 1 FROM migration_blocked_lastleg_review_decision_exceeds_16_chars', 'SELECT 1'); +PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; + -- 测试环境当前缺失、生产已存在的 PDA 废弃作业字段。 SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'll_cargo_discard_item' AND column_name = 'actual_discard_packages'), 'ALTER TABLE ll_cargo_discard_item ADD COLUMN actual_discard_packages int NULL COMMENT ''PDA确认实际废弃箱数'' AFTER discard_packages', 'SELECT 1'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; @@ -28,35 +44,23 @@ PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'll_return_receipt' AND column_name = 'pallet_code'), 'ALTER TABLE ll_return_receipt ADD COLUMN pallet_code varchar(128) COLLATE utf8mb4_unicode_ci NULL COMMENT ''本次签收绑定的托盘码'' AFTER carton_mark', 'SELECT 1'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; --- 收紧测试库漂移字段前先阻断不满足目标定义的数据,不执行自动回填或清理。 -SET @guard = IF(EXISTS (SELECT 1 FROM ll_delivery_order WHERE delivery_group_key IS NULL LIMIT 1), 'SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''ll_delivery_order.delivery_group_key contains NULL''', 'SELECT 1'); -PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @guard = IF(EXISTS (SELECT 1 FROM ll_local_looseboard_order WHERE container_type IS NULL LIMIT 1), 'SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''ll_local_looseboard_order.container_type contains NULL''', 'SELECT 1'); -PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @guard = IF(EXISTS (SELECT 1 FROM ll_us_truck_order WHERE container_type IS NULL LIMIT 1), 'SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''ll_us_truck_order.container_type contains NULL''', 'SELECT 1'); -PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @guard = IF(EXISTS (SELECT 1 FROM ll_outbound_pda_pick WHERE inventory_position_id IS NULL LIMIT 1), 'SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''ll_outbound_pda_pick.inventory_position_id contains NULL''', 'SELECT 1'); -PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @guard = IF(EXISTS (SELECT 1 FROM ll_relabel_item WHERE new_sku_code IS NULL LIMIT 1), 'SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''ll_relabel_item.new_sku_code contains NULL''', 'SELECT 1'); -PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @guard = IF(EXISTS (SELECT 1 FROM ll_relabel_item WHERE source_warehouse_id IS NULL LIMIT 1), 'SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''ll_relabel_item.source_warehouse_id contains NULL''', 'SELECT 1'); -PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @guard = IF(EXISTS (SELECT 1 FROM ll_relabel_pda_task WHERE CHAR_LENGTH(review_decision) > 16 LIMIT 1), 'SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''ll_relabel_pda_task.review_decision exceeds 16 characters''', 'SELECT 1'); -PREPARE stmt FROM @guard; EXECUTE stmt; DEALLOCATE PREPARE stmt; - -ALTER TABLE ll_delivery_order - MODIFY COLUMN delivery_group_key varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '同一出库单内按目的FBA仓拆单的稳定分组键'; -ALTER TABLE ll_local_looseboard_order - MODIFY COLUMN container_type varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '40HQ' COMMENT '平台统一柜型:40GP、40HQ、45HQ'; -ALTER TABLE ll_us_truck_order - MODIFY COLUMN container_type varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '40HQ' COMMENT '平台统一柜型:40GP、40HQ、45HQ'; -ALTER TABLE ll_outbound_pda_pick - MODIFY COLUMN inventory_position_id bigint NOT NULL COMMENT '库存仓位ID'; -ALTER TABLE ll_relabel_item - MODIFY COLUMN new_sku_code varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '换标后SKU编码', - MODIFY COLUMN source_warehouse_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '原海外中转仓ID'; -ALTER TABLE ll_relabel_pda_task - MODIFY COLUMN review_decision varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '最近复核结论:APPROVE或REWORK'; +SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'll_delivery_order' AND column_name = 'delivery_group_key' AND (column_type <> 'varchar(255)' OR is_nullable <> 'NO' OR collation_name <> 'utf8mb4_unicode_ci')), 'ALTER TABLE ll_delivery_order MODIFY COLUMN delivery_group_key varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT ''同一出库单内按目的FBA仓拆单的稳定分组键''', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'll_local_looseboard_order' AND column_name = 'container_type' AND (column_type <> 'varchar(32)' OR is_nullable <> 'NO' OR column_default <> '40HQ' OR collation_name <> 'utf8mb4_unicode_ci')), 'ALTER TABLE ll_local_looseboard_order MODIFY COLUMN container_type varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT ''40HQ'' COMMENT ''平台统一柜型:40GP、40HQ、45HQ''', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'll_us_truck_order' AND column_name = 'container_type' AND (column_type <> 'varchar(32)' OR is_nullable <> 'NO' OR column_default <> '40HQ' OR collation_name <> 'utf8mb4_unicode_ci')), 'ALTER TABLE ll_us_truck_order MODIFY COLUMN container_type varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT ''40HQ'' COMMENT ''平台统一柜型:40GP、40HQ、45HQ''', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'll_outbound_pda_pick' AND column_name = 'inventory_position_id' AND (column_type <> 'bigint' OR is_nullable <> 'NO')), 'ALTER TABLE ll_outbound_pda_pick MODIFY COLUMN inventory_position_id bigint NOT NULL COMMENT ''库存仓位ID''', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl = IF( + EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'll_relabel_item' AND column_name = 'new_sku_code' AND (column_type <> 'varchar(128)' OR is_nullable <> 'NO' OR collation_name <> 'utf8mb4_unicode_ci')) + OR EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'll_relabel_item' AND column_name = 'source_warehouse_id' AND (column_type <> 'varchar(128)' OR is_nullable <> 'NO' OR collation_name <> 'utf8mb4_unicode_ci')), + 'ALTER TABLE ll_relabel_item MODIFY COLUMN new_sku_code varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT ''换标后SKU编码'', MODIFY COLUMN source_warehouse_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT ''原海外中转仓ID''', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'll_relabel_pda_task' AND column_name = 'review_decision' AND (column_type <> 'varchar(16)' OR is_nullable <> 'YES' OR collation_name <> 'utf8mb4_unicode_ci')), 'ALTER TABLE ll_relabel_pda_task MODIFY COLUMN review_decision varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ''最近复核结论:APPROVE或REWORK''', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @ddl = IF( (SELECT table_collation FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'll_warehouse_transfer_measurement_photo') <> 'utf8mb4_unicode_ci', diff --git a/scripts/admin-test-env-modules/phase4-overseas-return-followups.mjs b/scripts/admin-test-env-modules/phase4-overseas-return-followups.mjs index 3778f0f7b..2d95f89e4 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-return-followups.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-return-followups.mjs @@ -235,12 +235,15 @@ export async function completeReturnRelabel(context, merchantToken, pdaToken, re for (const [index, source] of sources.entries()) { const target = await command(pdaToken, 'POST', '生成本轮带目的仓换标托盘', `${pallets}/relabel-pallet-label`, { relabelNo, idempotencyKey: key(context, `RETURN-PALLET-${index}`) }) - const from = await command(pdaToken, 'POST', '回查来源托盘版本', `${pallets}/resolve`, { code: source.palletCode }) - const to = await command(pdaToken, 'POST', '回查目标托盘版本', `${pallets}/resolve`, { code: target.palletCode }) + const from = await command(pdaToken, 'POST', '回查来源托盘版本', `${pallets}/pallets/resolve`, { code: source.palletCode }) + const to = await command(pdaToken, 'POST', '回查目标托盘版本', `${pallets}/pallets/resolve`, { code: target.palletCode }) require('组托仅使用本轮任务箱唛', source.cartonMarks?.length > 0 && source.cartonMarks.every(mark => task.items.some(item => item.cartonMark === mark))) const body = { relabelNo, source: { palletCode: from.palletCode, expectedVersion: Number(from.palletVersion) }, target: { palletCode: to.palletCode, expectedVersion: Number(to.palletVersion) }, + actualSource: { positionId: source.positionId, expectedPositionVersion: Number(source.positionVersion), + packages: task.items.filter(item => source.cartonMarks.includes(item.cartonMark)) + .reduce((sum, item) => sum + Number(item.expectedPackages), 0) }, cartonMarks: source.cartonMarks, idempotencyKey: key(context, `RETURN-BIND-${index}`), reason: context.marker } const bound = await command(pdaToken, 'POST', '退货换标按实际数量绑定目标托盘', `${pallets}/relabel-disassemble`, body) const replay = await command(pdaToken, 'POST', '退货换标组托原键重放', `${pallets}/relabel-disassemble`, body) diff --git a/scripts/admin-test-env-modules/phase4-overseas-return-receipt-contract.test.mjs b/scripts/admin-test-env-modules/phase4-overseas-return-receipt-contract.test.mjs index cef04d8e8..d32d4054b 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-return-receipt-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-return-receipt-contract.test.mjs @@ -17,10 +17,16 @@ test('return relabel lifecycle requires review, quantities, binding, photo and c calls.push({ method, path, body, token }) let data = task if (path.endsWith('/status')) data = { application: { relabelNo: 'RL-1', status: 'PENDING_PROCESSING' } } - if (path.endsWith('/relabel-sources')) data = [{ palletCode: 'OLD', cartonMarks: ['BOX'] }] + if (path.endsWith('/relabel-sources')) data = [{ positionId: '12', positionVersion: 0, palletCode: 'OLD', cartonMarks: ['BOX'] }] if (path.endsWith('/relabel-pallet-label')) data = { palletCode: 'NEW' } - if (path.endsWith('/resolve')) data = { palletCode: body.code, palletVersion: 1 } - if (path.endsWith('/relabel-disassemble')) data = { status: 'COMPLETED', operationId: '9', replayed: true } + if (path.endsWith('/resolve')) { + assert.equal(path, '/api/pda/v1/last-leg/pallet-operations/pallets/resolve') + data = { palletCode: body.code, palletVersion: 1 } + } + if (path.endsWith('/relabel-disassemble')) { + assert.deepEqual(body.actualSource, { positionId: '12', expectedPositionVersion: 0, packages: 1 }) + data = { status: 'COMPLETED', operationId: '9', replayed: true } + } if (path.endsWith('/RL-1/complete')) data = { applicationStatus: 'CUSTOMER_CONFIRM_PENDING' } if (path.endsWith('/confirm-completion')) data = { application: { relabelNo: 'RL-1', status: 'COMPLETED' } } if (path.endsWith('/return-orders/RT-1')) data = { order: { status: 'COMPLETED', relabelNo: 'RL-1', relabelStatus: 'COMPLETED' } } diff --git a/scripts/database-schema-alignment-migrations-contract.test.mjs b/scripts/database-schema-alignment-migrations-contract.test.mjs new file mode 100644 index 000000000..6dd584fa6 --- /dev/null +++ b/scripts/database-schema-alignment-migrations-contract.test.mjs @@ -0,0 +1,47 @@ +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' +import path from 'node:path' +import test from 'node:test' +import { fileURLToPath } from 'node:url' + +const repositoryRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url))) +const migrations = { + iam: 'deploy/sql/migrations/cn/baiyue_iam/V1__align_schema_baseline.sql', + firstleg: 'deploy/sql/migrations/cn/baiyue_firstleg/V1__align_schema_baseline.sql', + file: 'deploy/sql/migrations/cn/baiyue_file/V1__align_schema_baseline.sql', + lastleg: 'deploy/sql/migrations/overseas/baiyue_lastleg/V2__align_schema_baseline.sql', +} + +async function sources() { + return Object.fromEntries(await Promise.all(Object.entries(migrations).map(async ([name, relative]) => ( + [name, await readFile(path.join(repositoryRoot, relative), 'utf8')] + )))) +} + +test('schema alignment migrations contain no destructive statements', async () => { + for (const [name, source] of Object.entries(await sources())) { + assert.doesNotMatch(source, /^\s*(?:DROP|DELETE|UPDATE|TRUNCATE)\b/im, `${name} 迁移不得删除、清理或回填数据`) + assert.doesNotMatch(source, /\bSIGNAL\s+SQLSTATE\b/i, `${name} 迁移不得使用 MySQL 不支持的动态 SIGNAL`) + } +}) + +test('schema alignment migrations cover the verified drift', async () => { + const source = await sources() + for (const field of ['tenant_code', 'customer_nickname', 'original_file_name']) assert.match(source.iam, new RegExp(`\\b${field}\\b`)) + assert.match(source.firstleg, /\bpallet_no\b/) + for (const table of ['file_pda_release_channel', 'file_pda_release']) assert.match(source.file, new RegExp(`\\b${table}\\b`)) + for (const field of [ + 'actual_discard_packages', 'pda_status', 'pda_operator_id', 'pda_completed_at', + 'quantity', 'pallet_code', 'discrepancy_reason', 'manually_completed_at', + ]) assert.match(source.lastleg, new RegExp(`\\b${field}\\b`)) +}) + +test('constraint tightening is guarded before any alter table', async () => { + for (const [name, source] of Object.entries(await sources())) { + const firstAlter = source.search(/\bALTER TABLE\b/i) + const firstGuard = source.search(/SET @guard\s*=\s*IF/i) + if (name === 'file' || name === 'iam' || name === 'firstleg' || name === 'lastleg') { + assert.ok(firstGuard >= 0 && firstGuard < firstAlter, `${name} 必须先验证存量数据再执行 DDL`) + } + } +})