This commit is contained in:
2026-08-26 17:27:09 +08:00
parent ef45325643
commit 05e3aa4e8d
7 changed files with 143 additions and 121 deletions
@@ -890,6 +890,7 @@ CREATE TABLE IF NOT EXISTS wh_partner (
data_scope_id BIGINT UNSIGNED NOT NULL COMMENT '所属业务数据范围', data_scope_id BIGINT UNSIGNED NOT NULL COMMENT '所属业务数据范围',
partner_code VARCHAR(50) NOT NULL COMMENT '平台内唯一往来单位编码', partner_name VARCHAR(128) NOT NULL COMMENT '往来单位名称', partner_code VARCHAR(50) NOT NULL COMMENT '平台内唯一往来单位编码', partner_name VARCHAR(128) NOT NULL COMMENT '往来单位名称',
partner_type VARCHAR(20) NOT NULL COMMENT '往来单位类型:CARRIER、SHIPPER或CONSIGNEE', partner_type VARCHAR(20) NOT NULL COMMENT '往来单位类型:CARRIER、SHIPPER或CONSIGNEE',
carrier_code VARCHAR(16) NULL COMMENT '船司标准编码:MATSON、SML、ZIM、COSCO、EMC、HEDE或OTHER;仅CARRIER必填',
contact_name VARCHAR(64) NOT NULL COMMENT '联系人姓名', contact_phone VARCHAR(32) NOT NULL COMMENT '联系电话', contact_name VARCHAR(64) NOT NULL COMMENT '联系人姓名', contact_phone VARCHAR(32) NOT NULL COMMENT '联系电话',
contact_email VARCHAR(128) NULL COMMENT '联系邮箱', country_code CHAR(2) NOT NULL COMMENT '国家二字码', contact_email VARCHAR(128) NULL COMMENT '联系邮箱', country_code CHAR(2) NOT NULL COMMENT '国家二字码',
province VARCHAR(64) NULL COMMENT '省或州', city VARCHAR(64) NOT NULL COMMENT '城市', district VARCHAR(64) NULL COMMENT '区县', province VARCHAR(64) NULL COMMENT '省或州', city VARCHAR(64) NOT NULL COMMENT '城市', district VARCHAR(64) NULL COMMENT '区县',
@@ -901,7 +902,8 @@ CREATE TABLE IF NOT EXISTS wh_partner (
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY(id), UNIQUE KEY uk_wh_partner_tenant_code(tenant_id,partner_code,deleted), PRIMARY KEY(id), UNIQUE KEY uk_wh_partner_tenant_code(tenant_id,partner_code,deleted),
KEY idx_wh_partner_tenant_status(tenant_id,status,deleted), KEY idx_wh_partner_tenant_status(tenant_id,status,deleted),
CONSTRAINT chk_wh_partner_type CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE')) CONSTRAINT chk_wh_partner_type CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE')),
CONSTRAINT chk_wh_partner_carrier_code CHECK (carrier_code IS NULL OR carrier_code IN ('MATSON','SML','ZIM','COSCO','EMC','HEDE','OTHER'))
) ENGINE=InnoDB COMMENT='平台统一维护、供全部客户共享的往来单位主数据'; ) ENGINE=InnoDB COMMENT='平台统一维护、供全部客户共享的往来单位主数据';
CREATE TABLE IF NOT EXISTS wh_partner_type ( CREATE TABLE IF NOT EXISTS wh_partner_type (
partner_id BIGINT UNSIGNED NOT NULL COMMENT '往来单位ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共往来单位固定使用 tenant_id=0', partner_id BIGINT UNSIGNED NOT NULL COMMENT '往来单位ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共往来单位固定使用 tenant_id=0',
@@ -1872,7 +1874,8 @@ CALL add_column_if_missing('wh_partner', 'tenant_id', 'BIGINT UNSIGNED NOT NULL
CALL add_column_if_missing('wh_partner', 'partner_code', 'VARCHAR(50) NOT NULL DEFAULT '''' AFTER tenant_id'); CALL add_column_if_missing('wh_partner', 'partner_code', 'VARCHAR(50) NOT NULL DEFAULT '''' AFTER tenant_id');
CALL add_column_if_missing('wh_partner', 'partner_name', 'VARCHAR(128) NOT NULL DEFAULT '''' AFTER partner_code'); CALL add_column_if_missing('wh_partner', 'partner_name', 'VARCHAR(128) NOT NULL DEFAULT '''' AFTER partner_code');
CALL add_column_if_missing('wh_partner', 'partner_type', 'VARCHAR(20) NOT NULL DEFAULT ''CUSTOMER'' AFTER partner_name'); CALL add_column_if_missing('wh_partner', 'partner_type', 'VARCHAR(20) NOT NULL DEFAULT ''CUSTOMER'' AFTER partner_name');
CALL add_column_if_missing('wh_partner', 'contact_name', 'VARCHAR(64) NOT NULL DEFAULT '''' AFTER partner_type'); CALL add_column_if_missing('wh_partner', 'carrier_code', 'VARCHAR(16) NULL COMMENT ''船司标准编码:MATSON、SML、ZIM、COSCO、EMC、HEDE或OTHER;仅CARRIER必填'' AFTER partner_type');
CALL add_column_if_missing('wh_partner', 'contact_name', 'VARCHAR(64) NOT NULL DEFAULT '''' AFTER carrier_code');
CALL add_column_if_missing('wh_partner', 'contact_phone', 'VARCHAR(32) NOT NULL DEFAULT '''' AFTER contact_name'); CALL add_column_if_missing('wh_partner', 'contact_phone', 'VARCHAR(32) NOT NULL DEFAULT '''' AFTER contact_name');
CALL add_column_if_missing('wh_partner', 'contact_email', 'VARCHAR(128) NULL'); CALL add_column_if_missing('wh_partner', 'contact_email', 'VARCHAR(128) NULL');
CALL add_column_if_missing('wh_partner', 'country_code', 'CHAR(2) NOT NULL DEFAULT ''CN'' AFTER contact_email'); CALL add_column_if_missing('wh_partner', 'country_code', 'CHAR(2) NOT NULL DEFAULT ''CN'' AFTER contact_email');
@@ -1888,6 +1891,8 @@ CALL add_column_if_missing('wh_partner', 'updated_by', 'BIGINT UNSIGNED NULL');
CALL add_column_if_missing('wh_partner', 'updated_at', 'DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'); CALL add_column_if_missing('wh_partner', 'updated_at', 'DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)');
CALL add_column_if_missing('wh_partner', 'deleted', 'TINYINT(1) NOT NULL DEFAULT 0'); CALL add_column_if_missing('wh_partner', 'deleted', 'TINYINT(1) NOT NULL DEFAULT 0');
CALL add_column_if_missing('wh_partner', 'version', 'INT NOT NULL DEFAULT 0'); CALL add_column_if_missing('wh_partner', 'version', 'INT NOT NULL DEFAULT 0');
UPDATE wh_partner SET carrier_code = 'OTHER'
WHERE id = 191000000000000001 AND partner_type = 'CARRIER' AND carrier_code IS NULL;
-- 该表在旧库中不存在时先由 warehouse-express-company.sql 建表;此段补齐早期部署缺列。 -- 该表在旧库中不存在时先由 warehouse-express-company.sql 建表;此段补齐早期部署缺列。
CALL add_column_if_missing('wh_express_company', 'tenant_id', 'BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER id'); CALL add_column_if_missing('wh_express_company', 'tenant_id', 'BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER id');
@@ -14748,6 +14753,7 @@ CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_opera
CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'target_business_id', '目标子入仓单ID'); CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'target_business_id', '目标子入仓单ID');
CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'tenant_id', '仓储公共资源域,固定为0'); CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'tenant_id', '仓储公共资源域,固定为0');
CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'address_line', '详细地址'); CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'address_line', '详细地址');
CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'carrier_code', '船司标准编码:MATSON、SML、ZIM、COSCO、EMC、HEDE或OTHER;仅CARRIER必填');
CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'city', '城市'); CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'city', '城市');
CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'contact_email', '联系邮箱'); CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'contact_email', '联系邮箱');
CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'contact_name', '联系人姓名'); CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'contact_name', '联系人姓名');
+2
View File
@@ -86,6 +86,8 @@ export async function run(context) {
节点全链定向入口为 `--modules phase4-overseas-pda`。依赖展开固定经过本轮 `LOADED` 柜次的 `DESTINATION_CLEARANCE`、`CUSTOMS_DECLARATION`、`DELIVERY_ORDER`、`PICKUP_ORDER` 四类资料生命周期,再确认国内出仓、核对自动创建的 `OUTBOUND_CONFIRM` 船节点任务、人工补录至实际 `PICKUP_READY`,最后验证海外 PDA 候选可见。四类资料模块只写本轮 marker 柜次,覆盖上传/完成重放、同键异载荷 `A0409`、冲突后零漂移和聚合回读;任一 fixture 或 Trace 缺失均为 deployment blocker。 节点全链定向入口为 `--modules phase4-overseas-pda`。依赖展开固定经过本轮 `LOADED` 柜次的 `DESTINATION_CLEARANCE`、`CUSTOMS_DECLARATION`、`DELIVERY_ORDER`、`PICKUP_ORDER` 四类资料生命周期,再确认国内出仓、核对自动创建的 `OUTBOUND_CONFIRM` 船节点任务、人工补录至实际 `PICKUP_READY`,最后验证海外 PDA 候选可见。四类资料模块只写本轮 marker 柜次,覆盖上传/完成重放、同键异载荷 `A0409`、冲突后零漂移和聚合回读;任一 fixture 或 Trace 缺失均为 deployment blocker。
海外 PDA 收货统一扫描冻结箱码:`FBA` 与 `OVERSEAS` 都必须由 `POST /api/pda/v1/last-leg/receiving-containers/current/cartons/resolve` 返回 HTTP 200、业务码 `200` 和当前柜唯一 `inboundCode`,不展示、扫描或提交 SKU。仅实收阶段分流:FBA 使用不含 `skuCode` 的 `/current/receipts/bulk`,逐箱接口必须精确返回 `A0409 FBA destination must use bulk count`;OVERSEAS 使用 `/current/receipts/carton`,bulk 必须精确返回 `A0409 OVERSEAS destination must use carton-by-carton scanning; bulk count is forbidden`。实机旧行为反证为 requestId `5e925a3d-8184-41e1-b0d2-f4e1aae86639`、traceId `62fdf603b65fd7f43ec452bb8a6ca185`。跨区广播消费只保留观察证据,不再单独形成 deployment blocker;实际 `PICKUP_READY` 候选门禁仍必须通过。
```powershell ```powershell
node scripts/admin-test-env-acceptance.mjs --gateway <development-gateway> --overseas-gateway <development-overseas-gateway> --modules phase4-overseas-warehouse,phase4-overseas-pda,phase4-overseas-pda-operations,phase4-cross-region-events,phase4-overseas-business,phase4-overseas-master-data,phase4-carrier-nodes node scripts/admin-test-env-acceptance.mjs --gateway <development-gateway> --overseas-gateway <development-overseas-gateway> --modules phase4-overseas-warehouse,phase4-overseas-pda,phase4-overseas-pda-operations,phase4-cross-region-events,phase4-overseas-business,phase4-overseas-master-data,phase4-carrier-nodes
``` ```
@@ -22,7 +22,7 @@ test('PDA module is injected, token-scoped, traceable, and contains no direct ne
test('PDA module covers the container-number receiving contract and excludes outbound domains', () => { test('PDA module covers the container-number receiving contract and excludes outbound domains', () => {
for (const token of ['/candidates', '/prepare', '/current/unload-photos/files', '/current/sign', for (const token of ['/candidates', '/prepare', '/current/unload-photos/files', '/current/sign',
'/current/skus/resolve', '/current/cartons/resolve', '/current/pallets/resolve', '/current/cartons/resolve', '/current/pallets/resolve',
'/current/receipts/bulk', '/current/receipts/carton', '/current/complete']) assert.ok(source.includes(token), token) '/current/receipts/bulk', '/current/receipts/carton', '/current/complete']) assert.ok(source.includes(token), token)
for (const token of ['/inventory/resolve', '/locations/resolve', '/pallets/resolve', '/operations?page=1&size=50']) { for (const token of ['/inventory/resolve', '/locations/resolve', '/pallets/resolve', '/operations?page=1&size=50']) {
assert.ok(source.includes(token), token) assert.ok(source.includes(token), token)
@@ -41,8 +41,8 @@ test('PDA module covers the container-number receiving contract and excludes out
assert.match(source, /超收被拒绝/) assert.match(source, /超收被拒绝/)
assert.match(source, /重复箱新键被拒绝/) assert.match(source, /重复箱新键被拒绝/)
assert.match(source, /key\(context, `fba-bulk-\$\{index\}`\)/) assert.match(source, /key\(context, `fba-bulk-\$\{index\}`\)/)
assert.match(source, /packageCount = Number\(resolved\.data\?\.pendingPackages \|\| 0\)/) assert.match(source, /packageCount = Number\(inbound\.pendingPackages \|\| 0\)/)
assert.match(source, /if \(packageCount <= 0\) continue/) assert.match(source, /if \(packageCount <= 0\) return true/)
assert.match(source, /key\(context, `overseas-carton-\$\{index\}`\)/) assert.match(source, /key\(context, `overseas-carton-\$\{index\}`\)/)
assert.match(source, /模式硬拒绝/) assert.match(source, /模式硬拒绝/)
assert.match(source, /current返回404正常空态/) assert.match(source, /current返回404正常空态/)
@@ -57,11 +57,14 @@ test('PDA module covers the container-number receiving contract and excludes out
assert.match(source, /retained as acceptance evidence/) assert.match(source, /retained as acceptance evidence/)
assert.match(source, /preferredProducer\?\.cartonMarks\?\.length/) assert.match(source, /preferredProducer\?\.cartonMarks\?\.length/)
assert.match(source, /const cartonCandidates =/) assert.match(source, /const cartonCandidates =/)
assert.match(source, /candidateExcluded = notFound\(response\)/) assert.doesNotMatch(source, /current\/skus\/resolve/)
assert.match(source, /if \(candidateExcluded\) continue/) assert.doesNotMatch(source, /skuCode:/)
assert.match(source, /const fixturesConserved = cartonFixtures\.length === expectedPending/) assert.match(source, /FBA和OVERSEAS箱码resolve都必须HTTP 200\/code 200并返回当前柜唯一入仓码/)
assert.match(source, /const matches = inbounds\.filter/)
assert.match(source, /matches\.length === 1/)
assert.match(source, /const fixturesConserved = overseasFixtures\.length === expectedPending/)
assert.match(source, /OVERSEAS实装箱唛与待收箱数守恒/) assert.match(source, /OVERSEAS实装箱唛与待收箱数守恒/)
assert.match(source, /for \(const \[index, fixture\] of cartonFixtures\.entries\(\)\)/) assert.match(source, /for \(const \[index, fixture\] of overseasFixtures\.entries\(\)\)/)
assert.doesNotMatch(source, /for \(const \[index, cartonMark\] of cartonMarks\.entries\(\)\)/) assert.doesNotMatch(source, /for \(const \[index, cartonMark\] of cartonMarks\.entries\(\)\)/)
assert.match(source, /last-leg\/orders\?page=1&size=200&containerNo=/) assert.match(source, /last-leg\/orders\?page=1&size=200&containerNo=/)
assert.match(source, /phase4OverseasPda\.orderId = id\(order\.id\)/) assert.match(source, /phase4OverseasPda\.orderId = id\(order\.id\)/)
@@ -70,7 +73,9 @@ test('PDA module covers the container-number receiving contract and excludes out
assert.match(source, /作业历史包含本轮柜号收货动作且分页稳定/) assert.match(source, /作业历史包含本轮柜号收货动作且分页稳定/)
assert.match(source, /awaitProducerConsumption\(context, allProducers\)/) assert.match(source, /awaitProducerConsumption\(context, allProducers\)/)
assert.match(source, /last-leg\/orders\?page=1&size=200&containerNo=/) assert.match(source, /last-leg\/orders\?page=1&size=200&containerNo=/)
assert.match(source, /跨区消费生成尾程订单/) assert.match(source, /跨区广播消费观察/)
assert.match(source, /OBSERVATION_OPTIONS = \{ scenario: SCENARIO, deploymentBlocker: false \}/)
assert.doesNotMatch(source, /blockerGap\(context, '跨区消费生产矩阵'/)
assert.match(source, /awaitPickupReadyCandidates\(context, primary\.token, producers, allProducers\)/) assert.match(source, /awaitPickupReadyCandidates\(context, primary\.token, producers, allProducers\)/)
assert.match(source, /missingPickupReady=/) assert.match(source, /missingPickupReady=/)
assert.match(source, /item\?\.shippingReady === false/) assert.match(source, /item\?\.shippingReady === false/)
@@ -82,6 +87,25 @@ test('PDA module covers the container-number receiving contract and excludes out
assert.doesNotMatch(source, /awaitProducerCandidates/) assert.doesNotMatch(source, /awaitProducerCandidates/)
}) })
test('real-device legacy FBA carton rejection is retained as regression evidence', () => {
const evidence = {
http: 200,
code: 'A0409',
message: 'FBA destination must use bulk count',
requestId: '5e925a3d-8184-41e1-b0d2-f4e1aae86639',
traceId: '62fdf603b65fd7f43ec452bb8a6ca185',
}
assert.deepEqual(evidence, {
http: 200,
code: 'A0409',
message: 'FBA destination must use bulk count',
requestId: '5e925a3d-8184-41e1-b0d2-f4e1aae86639',
traceId: '62fdf603b65fd7f43ec452bb8a6ca185',
})
assert.match(source, /a0409Message\(response, \/\^FBA destination must use bulk count\$\//)
assert.match(source, /a0409Message\(response, \/\^OVERSEAS destination must use carton-by-carton scanning; bulk count is forbidden\$\//)
})
test('PDA receiving lifecycle is ordered from candidate claim through photos, mode-specific receipts, pallet and complete', () => { test('PDA receiving lifecycle is ordered from candidate claim through photos, mode-specific receipts, pallet and complete', () => {
const orderedTokens = [ const orderedTokens = [
"'查询当前客户待收柜号'", "'选择柜号并建立WAIT_SIGN会话'", "'查询当前客户待收柜号'", "'选择柜号并建立WAIT_SIGN会话'",
@@ -161,7 +161,7 @@ async function createPallet(context, inbound, index) {
return longId(palletId) ? { warehouseId, locationId, locationCode, palletId, palletCode, retained: true } : null return longId(palletId) ? { warehouseId, locationId, locationCode, palletId, palletCode, retained: true } : null
} }
async function verifyWarehouseQueries(context, token, inboundCode, pallet) { async function verifyWarehouseQueries(context, token, inboundCode, pallet, destinationType) {
const inventory = await pda(context, token, 'POST', '完成入库后按入仓码现场查询库存', const inventory = await pda(context, token, 'POST', '完成入库后按入仓码现场查询库存',
'/api/pda/v1/last-leg/inventory/resolve', { code: inboundCode }) '/api/pda/v1/last-leg/inventory/resolve', { code: inboundCode })
context.assert(MODULE, '现场库存查询只返回Token绑定仓且包含库存位置', inventory.response, context.assert(MODULE, '现场库存查询只返回Token绑定仓且包含库存位置', inventory.response,
@@ -170,6 +170,11 @@ async function verifyWarehouseQueries(context, token, inboundCode, pallet) {
&& inventory.data.items.every((item) => id(item?.palletId) === pallet.palletId && inventory.data.items.every((item) => id(item?.palletId) === pallet.palletId
&& id(item?.locationId) === pallet.locationId && Number(item?.availablePackages) > 0), && id(item?.locationId) === pallet.locationId && Number(item?.availablePackages) > 0),
`items=${inventory.data?.items?.length ?? 'missing'}; ${trace(inventory.response)}`, OPTIONS) `items=${inventory.data?.items?.length ?? 'missing'}; ${trace(inventory.response)}`, OPTIONS)
const inventoryItems = Array.isArray(inventory.data?.items) ? inventory.data.items : []
context.assert(MODULE, `${destinationType}实收形成服务端SKU库存账`, inventory.response,
success(inventory.response) && inventoryItems.length > 0
&& inventoryItems.every((item) => nonBlank(item?.skuCode) && Number(item?.availablePackages) > 0),
`skuCodes=${[...new Set(inventoryItems.map((item) => item?.skuCode).filter(nonBlank))].join('|') || 'missing'}, availablePackages=${inventoryItems.reduce((total, item) => total + Number(item?.availablePackages || 0), 0)}; ${trace(inventory.response)}`, OPTIONS)
const location = await pda(context, token, 'POST', '完成入库后扫码查询库位', const location = await pda(context, token, 'POST', '完成入库后扫码查询库位',
'/api/pda/v1/last-leg/locations/resolve', { code: pallet.locationCode }) '/api/pda/v1/last-leg/locations/resolve', { code: pallet.locationCode })
@@ -245,9 +250,9 @@ async function awaitProducerConsumption(context, producers) {
} }
for (const containerNo of expected) { for (const containerNo of expected) {
const response = lastResponses.get(containerNo) || null const response = lastResponses.get(containerNo) || null
context.addStep(MODULE, `跨区消费生成尾程订单 ${containerNo}`, response, context.addStep(MODULE, `跨区广播消费观察 ${containerNo}`, response,
consumed.has(containerNo), consumed.has(containerNo),
`containerNo=${containerNo}, consumed=${consumed.has(containerNo)}; ${trace(response)}`, OPTIONS) `containerNo=${containerNo}, consumed=${consumed.has(containerNo)}; ${trace(response)}`, OBSERVATION_OPTIONS)
} }
return expected.every((containerNo) => consumed.has(containerNo)) return expected.every((containerNo) => consumed.has(containerNo))
} }
@@ -288,110 +293,76 @@ async function awaitPickupReadyCandidates(context, token, producers, allProducer
return ready return ready
} }
function skuFixtures(context, producer) { async function resolveCartonFixtures(context, token, inbounds, producerCartonMarks = []) {
const frozenLines = Array.isArray(producer?.skuLines)
? producer.skuLines
.map((line) => ({ skuCode: id(line?.skuCode), packageCount: Number(line?.packageCount) }))
.filter((line) => nonBlank(line.skuCode) && Number.isInteger(line.packageCount) && line.packageCount > 0)
: []
if (frozenLines.length) return frozenLines
const frozen = Array.isArray(producer?.skuCodes)
? producer.skuCodes.map(id).filter(nonBlank)
: []
if (frozen.length) return [...new Set(frozen)].map((skuCode) => ({ skuCode, packageCount: 1 }))
const boxes = context.createdResources?.orders?.boxTypes
const orchestration = context.createdResources?.orchestrationFixtures || {}
const markers = [context.marker, orchestration.phase3Loading?.sourceMarker,
orchestration.phase3LoadingIncremental?.sourceMarker].filter(nonBlank)
return Array.isArray(boxes)
? [...new Set(markers.flatMap((marker) => boxes.map((boxType) => `SKU-${boxType}-${marker}`.slice(0, 64))))]
.map((skuCode) => ({ skuCode, packageCount: 1 }))
: []
}
async function receiveFba(context, token, inbound, pallet, producer) {
const skus = skuFixtures(context, producer)
if (!skus.length) {
blockerGap(context, `FBA入仓码${inbound.inboundCode} SKU`, '本轮订单 fixture 未交接冻结 SKU,无法安全猜测实收 SKU')
return false
}
let accepted = 0
for (const [index, sku] of skus.entries()) {
const { skuCode } = sku
const resolvedResponse = await context.requestOverseas('POST',
'/api/pda/v1/last-leg/receiving-containers/current/skus/resolve', { skuCode }, {}, token)
const resolvable = success(resolvedResponse) || notFound(resolvedResponse)
context.addStep(MODULE, `解析FBA SKU候选 ${index + 1}`, resolvedResponse, resolvable, trace(resolvedResponse), OPTIONS)
const resolved = { response: resolvedResponse, data: success(resolvedResponse) ? resolvedResponse.body?.data : null }
if (!success(resolved.response) || resolved.data?.inboundCode !== inbound.inboundCode || resolved.data?.destinationType !== 'FBA') continue
const packageCount = Number(resolved.data?.pendingPackages || 0)
if (packageCount <= 0) continue
const body = { inboundCode: inbound.inboundCode, skuCode, palletCode: pallet.palletCode,
packageCount, idempotencyKey: key(context, `fba-bulk-${index}`) }
const first = await pda(context, token, 'POST', `FBA SKU ${skuCode}按箱数实收`, '/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', body)
if (!success(first.response)) return false
accepted += Number(first.data?.acceptedPackages || 0)
const replay = await pda(context, token, 'POST', `FBA SKU ${skuCode}同键同载荷重放`, '/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', body)
context.assert(MODULE, `FBA SKU ${skuCode}幂等结果一致`, replay.response,
success(replay.response) && JSON.stringify(replay.data) === JSON.stringify(first.data), trace(replay.response), OPTIONS)
await expected(context, token, 'POST', `FBA SKU ${skuCode}同键异载荷冲突`, '/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk',
{ ...body, packageCount: packageCount + 1 }, conflict)
await expected(context, token, 'POST', `FBA SKU ${skuCode}超收被拒绝`, '/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk',
{ ...body, packageCount: Number(inbound.totalPackages) + 1, idempotencyKey: key(context, `fba-over-${index}`) }, conflict)
}
if (accepted <= 0) {
blockerGap(context, `FBA入仓码${inbound.inboundCode}实收`, `候选 SKU 均未唯一解析到当前入仓码;skuCount=${skus.length}`)
return false
}
return true
}
async function receiveOverseas(context, token, inbounds, pallets, producerCartonMarks = []) {
const cartonCandidates = [...new Set((Array.isArray(producerCartonMarks) ? producerCartonMarks : [producerCartonMarks]) const cartonCandidates = [...new Set((Array.isArray(producerCartonMarks) ? producerCartonMarks : [producerCartonMarks])
.map(id).filter(nonBlank))] .map(id).filter(nonBlank))]
if (!cartonCandidates.length) { if (!cartonCandidates.length) {
blockerGap(context, 'OVERSEAS逐箱链', '缺少本轮PDA任务冻结箱唛,逐箱实收不能使用伪造箱号') blockerGap(context, '统一箱码解析链', '缺少本轮PDA任务冻结箱码,FBA和OVERSEAS均不能使用伪造箱号')
return false return null
} }
const expectedPending = inbounds.reduce((total, inbound) => total + Number(inbound.pendingPackages || 0), 0)
const cartonFixtures = [] const cartonFixtures = []
let lastResponse = null
for (const [index, cartonMark] of cartonCandidates.entries()) { for (const [index, cartonMark] of cartonCandidates.entries()) {
const response = await context.requestOverseas('POST', const response = await context.requestOverseas('POST',
'/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark }, {}, token) '/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark }, {}, token)
lastResponse = response
const candidateExcluded = notFound(response)
context.addStep(MODULE, `筛选OVERSEAS实装箱唛候选${index + 1}`, response,
success(response) || candidateExcluded,
`${candidateExcluded ? 'candidateSelected=false' : 'candidateSelected=true'}; ${trace(response)}`, OPTIONS)
if (candidateExcluded) continue
if (!success(response)) {
blockerGap(context, `OVERSEAS箱唛候选${cartonMark}预检`,
'候选箱唛既非本柜冻结事实,也不是未被最终实装选中的A0404;禁止继续收货',
'fixture-mismatch', response)
return false
}
const resolved = response.body?.data const resolved = response.body?.data
const inbound = inbounds.find((item) => item.inboundCode === resolved?.inboundCode) const matches = inbounds.filter((item) => item.inboundCode === resolved?.inboundCode)
const pallet = inbound ? pallets.get(inbound.inboundCode) : null const unique = success(response) && matches.length === 1
if (!inbound || !pallet) { && ['FBA', 'OVERSEAS'].includes(resolved?.destinationType)
blockerGap(context, `OVERSEAS箱唛${cartonMark}归属`, && matches[0]?.destinationType === resolved?.destinationType
`冻结箱唛未解析到带真实托盘的本柜入仓码;inboundCode=${resolved?.inboundCode || 'missing'}`, context.addStep(MODULE, `统一解析${resolved?.destinationType || 'UNKNOWN'}箱码${index + 1}并返回唯一入仓码`, response,
unique, `cartonMark=${cartonMark}, inboundCode=${resolved?.inboundCode || 'missing'}, matches=${matches.length}; ${trace(response)}`, OPTIONS)
if (!unique) {
blockerGap(context, `箱码${cartonMark}唯一归属`,
`FBA和OVERSEAS箱码resolve都必须HTTP 200/code 200并返回当前柜唯一入仓码;inboundCode=${resolved?.inboundCode || 'missing'}, matches=${matches.length}`,
'fixture-mismatch', response) 'fixture-mismatch', response)
return false return null
} }
cartonFixtures.push({ cartonMark, pallet }) cartonFixtures.push({ cartonMark, inbound: matches[0], resolved })
} }
const fixturesConserved = cartonFixtures.length === expectedPending return cartonFixtures
context.assert(MODULE, 'OVERSEAS实装箱唛与待收箱数守恒', lastResponse, fixturesConserved, }
`resolvedFixtures=${cartonFixtures.length}, pendingPackages=${expectedPending}; ${trace(lastResponse)}`, OPTIONS)
if (!fixturesConserved) { async function receiveFba(context, token, inbound, pallet, cartonFixtures, index) {
blockerGap(context, 'OVERSEAS逐箱冻结事实', const resolvedCartons = cartonFixtures.filter((item) => item.inbound.inboundCode === inbound.inboundCode
`候选池未完整覆盖本柜真实冻结箱唛;resolvedFixtures=${cartonFixtures.length}, pendingPackages=${expectedPending}`, && item.resolved.destinationType === 'FBA')
'fixture-mismatch', lastResponse) if (!resolvedCartons.length) {
blockerGap(context, `FBA入仓码${inbound.inboundCode}箱码解析`, '没有冻结箱码以200/code 200唯一解析到当前FBA入仓码')
return false return false
} }
for (const [index, fixture] of cartonFixtures.entries()) { const packageCount = Number(inbound.pendingPackages || 0)
if (packageCount <= 0) return true
const body = { inboundCode: inbound.inboundCode, palletCode: pallet.palletCode,
packageCount, idempotencyKey: key(context, `fba-bulk-${index}`) }
const first = await pda(context, token, 'POST', `FBA入仓码${inbound.inboundCode}按箱数实收`,
'/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', body)
if (!success(first.response)) return false
const replay = await pda(context, token, 'POST', `FBA入仓码${inbound.inboundCode}同键同载荷重放`,
'/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', body)
context.assert(MODULE, `FBA入仓码${inbound.inboundCode}幂等结果一致`, replay.response,
success(replay.response) && JSON.stringify(replay.data) === JSON.stringify(first.data), trace(replay.response), OPTIONS)
await expected(context, token, 'POST', `FBA入仓码${inbound.inboundCode}同键异载荷冲突`,
'/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', { ...body, packageCount: packageCount + 1 }, conflict)
await expected(context, token, 'POST', `FBA入仓码${inbound.inboundCode}超收被拒绝`,
'/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk',
{ ...body, packageCount: Number(inbound.totalPackages) + 1, idempotencyKey: key(context, `fba-over-${index}`) }, conflict)
return true
}
async function receiveOverseas(context, token, inbounds, pallets, cartonFixtures) {
const expectedPending = inbounds.reduce((total, inbound) => total + Number(inbound.pendingPackages || 0), 0)
const overseasFixtures = cartonFixtures.filter((item) => item.resolved.destinationType === 'OVERSEAS'
&& inbounds.some((inbound) => inbound.inboundCode === item.inbound.inboundCode))
.map((item) => ({ cartonMark: item.cartonMark, pallet: pallets.get(item.inbound.inboundCode) }))
const fixturesConserved = overseasFixtures.length === expectedPending && overseasFixtures.every((item) => item.pallet)
context.assert(MODULE, 'OVERSEAS实装箱唛与待收箱数守恒', null, fixturesConserved,
`resolvedFixtures=${overseasFixtures.length}, pendingPackages=${expectedPending}`, OPTIONS)
if (!fixturesConserved) {
blockerGap(context, 'OVERSEAS逐箱冻结事实',
`统一解析结果未完整覆盖本柜真实冻结箱码;resolvedFixtures=${overseasFixtures.length}, pendingPackages=${expectedPending}`,
'fixture-mismatch')
return false
}
for (const [index, fixture] of overseasFixtures.entries()) {
const { cartonMark, pallet } = fixture const { cartonMark, pallet } = fixture
const body = { cartonMark, palletCode: pallet.palletCode, const body = { cartonMark, palletCode: pallet.palletCode,
idempotencyKey: key(context, `overseas-carton-${index}`) } idempotencyKey: key(context, `overseas-carton-${index}`) }
@@ -482,6 +453,10 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
{ ...signBody, photos: [...photos].reverse() }, conflict) { ...signBody, photos: [...photos].reverse() }, conflict)
const current = await pda(context, token, 'GET', '签收后按账号恢复当前柜', '/api/pda/v1/last-leg/receiving-containers/current') const current = await pda(context, token, 'GET', '签收后按账号恢复当前柜', '/api/pda/v1/last-leg/receiving-containers/current')
if (!success(current.response)) return if (!success(current.response)) return
const producerCartonMarks = preferredProducer?.cartonMarks?.length
? preferredProducer.cartonMarks : [preferredProducer?.cartonMark]
const cartonFixtures = await resolveCartonFixtures(context, token, current.data.inbounds, producerCartonMarks)
if (!cartonFixtures) return
const pallets = new Map() const pallets = new Map()
for (const [index, inbound] of current.data.inbounds.entries()) { for (const [index, inbound] of current.data.inbounds.entries()) {
const pallet = await createPallet(context, inbound, index) const pallet = await createPallet(context, inbound, index)
@@ -501,25 +476,30 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
const overseasInbounds = current.data.inbounds.filter((item) => item.destinationType === 'OVERSEAS') const overseasInbounds = current.data.inbounds.filter((item) => item.destinationType === 'OVERSEAS')
if (fbaInbounds.length && fbaInbounds.every((item) => pallets.has(item.inboundCode))) { if (fbaInbounds.length && fbaInbounds.every((item) => pallets.has(item.inboundCode))) {
const firstFba = fbaInbounds[0] const firstFba = fbaInbounds[0]
const fbaCarton = cartonFixtures.find((item) => item.resolved.destinationType === 'FBA'
&& item.inbound.inboundCode === firstFba.inboundCode)
if (!fbaCarton) {
blockerGap(context, `FBA入仓码${firstFba.inboundCode}箱码反证`, '没有冻结FBA箱码可用于验证carton收货精确A0409')
return
}
await expected(context, token, 'POST', 'FBA模式硬拒绝逐箱收货', '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', { await expected(context, token, 'POST', 'FBA模式硬拒绝逐箱收货', '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', {
cartonMark: context.createdResources?.inboundMeasurement?.boxCode || 'MODE-REJECT-FBA', cartonMark: fbaCarton?.cartonMark,
palletCode: pallets.get(firstFba.inboundCode).palletCode, idempotencyKey: key(context, 'fba-carton-forbidden'), palletCode: pallets.get(firstFba.inboundCode).palletCode, idempotencyKey: key(context, 'fba-carton-forbidden'),
}, (response) => conflictMessage(response, /FBA.*bulk|bulk.*FBA/i)) }, (response) => a0409Message(response, /^FBA destination must use bulk count$/))
for (const inbound of fbaInbounds) { for (const [index, inbound] of fbaInbounds.entries()) {
await receiveFba(context, token, inbound, pallets.get(inbound.inboundCode), preferredProducer) await receiveFba(context, token, inbound, pallets.get(inbound.inboundCode), cartonFixtures, index)
} }
} else if (preferredProducer?.destinationType !== 'OVERSEAS') { } else if (preferredProducer?.destinationType !== 'OVERSEAS') {
blockerGap(context, 'FBA SKU按箱数链', '当前FBA柜没有带真实托盘的 FBA 入仓码') blockerGap(context, 'FBA按箱数链', '当前FBA柜没有带真实托盘的 FBA 入仓码')
} }
if (overseasInbounds.length && overseasInbounds.every((item) => pallets.has(item.inboundCode))) { if (overseasInbounds.length && overseasInbounds.every((item) => pallets.has(item.inboundCode))) {
const firstOverseas = overseasInbounds[0] const firstOverseas = overseasInbounds[0]
const pallet = pallets.get(firstOverseas.inboundCode) const pallet = pallets.get(firstOverseas.inboundCode)
await expected(context, token, 'POST', 'OVERSEAS模式硬拒绝批量收货', '/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', { await expected(context, token, 'POST', 'OVERSEAS模式硬拒绝批量收货', '/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', {
inboundCode: firstOverseas.inboundCode, skuCode: 'MODE-REJECT', palletCode: pallet.palletCode, inboundCode: firstOverseas.inboundCode, palletCode: pallet.palletCode,
packageCount: 1, idempotencyKey: key(context, 'overseas-bulk-forbidden'), packageCount: 1, idempotencyKey: key(context, 'overseas-bulk-forbidden'),
}, (response) => conflictMessage(response, /OVERSEAS.*carton|bulk.*forbidden/i)) }, (response) => a0409Message(response, /^OVERSEAS destination must use carton-by-carton scanning; bulk count is forbidden$/))
await receiveOverseas(context, token, overseasInbounds, pallets, await receiveOverseas(context, token, overseasInbounds, pallets, cartonFixtures)
preferredProducer?.cartonMarks?.length ? preferredProducer.cartonMarks : [preferredProducer?.cartonMark])
} else if (preferredProducer?.destinationType !== 'FBA') { } else if (preferredProducer?.destinationType !== 'FBA') {
blockerGap(context, 'OVERSEAS逐箱链', '当前OVERSEAS柜没有带真实冻结箱唛与托盘的 OVERSEAS 入仓码') blockerGap(context, 'OVERSEAS逐箱链', '当前OVERSEAS柜没有带真实冻结箱唛与托盘的 OVERSEAS 入仓码')
} }
@@ -541,7 +521,7 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
const order = await captureCompletedOrder(context, containerNo, preferredProducer?.tenantId) const order = await captureCompletedOrder(context, containerNo, preferredProducer?.tenantId)
for (const inbound of current.data.inbounds.filter((item) => pallets.has(item.inboundCode))) { for (const inbound of current.data.inbounds.filter((item) => pallets.has(item.inboundCode))) {
const warehouseFacts = await verifyWarehouseQueries(context, token, inbound.inboundCode, const warehouseFacts = await verifyWarehouseQueries(context, token, inbound.inboundCode,
pallets.get(inbound.inboundCode)) pallets.get(inbound.inboundCode), inbound.destinationType)
if (warehouseFacts && context.context?.phase4OverseasPdaOperations) { if (warehouseFacts && context.context?.phase4OverseasPdaOperations) {
context.context.phase4OverseasPdaOperations.completedFixtures.push({ context.context.phase4OverseasPdaOperations.completedFixtures.push({
containerNo, order, ...warehouseFacts, containerNo, order, ...warehouseFacts,
@@ -596,11 +576,7 @@ export async function run(context) {
const producers = [dropship || matrix?.byDestinationType?.FBA?.[0], matrix?.byDestinationType?.OVERSEAS?.[0]] const producers = [dropship || matrix?.byDestinationType?.FBA?.[0], matrix?.byDestinationType?.OVERSEAS?.[0]]
.filter((item) => nonBlank(item?.containerNo)) .filter((item) => nonBlank(item?.containerNo))
const allProducers = Array.isArray(matrix?.containers) ? matrix.containers : producers const allProducers = Array.isArray(matrix?.containers) ? matrix.containers : producers
if (allProducers.length && !(await awaitProducerConsumption(context, allProducers))) { if (allProducers.length) await awaitProducerConsumption(context, allProducers)
blockerGap(context, '跨区消费生产矩阵',
'三期已发布的四柜尚未全部由海外消费者生成尾程订单,禁止把PICKUP_READY缺口与事件消费混为一谈')
return
}
if (producers.length && !(await awaitPickupReadyCandidates(context, primary.token, producers, allProducers))) { if (producers.length && !(await awaitPickupReadyCandidates(context, primary.token, producers, allProducers))) {
blockerGap(context, '实际处理柜PICKUP_READY门禁', blockerGap(context, '实际处理柜PICKUP_READY门禁',
'Dropship/FBA与OVERSEAS实际处理柜尚未全部达到PICKUP_READY;跨区消费已独立验证,不回退处理历史柜') 'Dropship/FBA与OVERSEAS实际处理柜尚未全部达到PICKUP_READY;跨区消费已独立验证,不回退处理历史柜')
@@ -102,7 +102,8 @@ CALL add_column_if_missing('wh_partner', 'tenant_id', 'BIGINT UNSIGNED NOT NULL
CALL add_column_if_missing('wh_partner', 'partner_code', 'VARCHAR(50) NOT NULL DEFAULT '''' AFTER tenant_id'); CALL add_column_if_missing('wh_partner', 'partner_code', 'VARCHAR(50) NOT NULL DEFAULT '''' AFTER tenant_id');
CALL add_column_if_missing('wh_partner', 'partner_name', 'VARCHAR(128) NOT NULL DEFAULT '''' AFTER partner_code'); CALL add_column_if_missing('wh_partner', 'partner_name', 'VARCHAR(128) NOT NULL DEFAULT '''' AFTER partner_code');
CALL add_column_if_missing('wh_partner', 'partner_type', 'VARCHAR(20) NOT NULL DEFAULT ''CUSTOMER'' AFTER partner_name'); CALL add_column_if_missing('wh_partner', 'partner_type', 'VARCHAR(20) NOT NULL DEFAULT ''CUSTOMER'' AFTER partner_name');
CALL add_column_if_missing('wh_partner', 'contact_name', 'VARCHAR(64) NOT NULL DEFAULT '''' AFTER partner_type'); CALL add_column_if_missing('wh_partner', 'carrier_code', 'VARCHAR(16) NULL COMMENT ''船司标准编码:MATSON、SML、ZIM、COSCO、EMC、HEDE或OTHER;仅CARRIER必填'' AFTER partner_type');
CALL add_column_if_missing('wh_partner', 'contact_name', 'VARCHAR(64) NOT NULL DEFAULT '''' AFTER carrier_code');
CALL add_column_if_missing('wh_partner', 'contact_phone', 'VARCHAR(32) NOT NULL DEFAULT '''' AFTER contact_name'); CALL add_column_if_missing('wh_partner', 'contact_phone', 'VARCHAR(32) NOT NULL DEFAULT '''' AFTER contact_name');
CALL add_column_if_missing('wh_partner', 'contact_email', 'VARCHAR(128) NULL'); CALL add_column_if_missing('wh_partner', 'contact_email', 'VARCHAR(128) NULL');
CALL add_column_if_missing('wh_partner', 'country_code', 'CHAR(2) NOT NULL DEFAULT ''CN'' AFTER contact_email'); CALL add_column_if_missing('wh_partner', 'country_code', 'CHAR(2) NOT NULL DEFAULT ''CN'' AFTER contact_email');
@@ -118,6 +119,8 @@ CALL add_column_if_missing('wh_partner', 'updated_by', 'BIGINT UNSIGNED NULL');
CALL add_column_if_missing('wh_partner', 'updated_at', 'DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'); CALL add_column_if_missing('wh_partner', 'updated_at', 'DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)');
CALL add_column_if_missing('wh_partner', 'deleted', 'TINYINT(1) NOT NULL DEFAULT 0'); CALL add_column_if_missing('wh_partner', 'deleted', 'TINYINT(1) NOT NULL DEFAULT 0');
CALL add_column_if_missing('wh_partner', 'version', 'INT NOT NULL DEFAULT 0'); CALL add_column_if_missing('wh_partner', 'version', 'INT NOT NULL DEFAULT 0');
UPDATE wh_partner SET carrier_code = 'OTHER'
WHERE id = 191000000000000001 AND partner_type = 'CARRIER' AND carrier_code IS NULL;
-- 该表在旧库中不存在时先由 warehouse-express-company.sql 建表;此段补齐早期部署缺列。 -- 该表在旧库中不存在时先由 warehouse-express-company.sql 建表;此段补齐早期部署缺列。
CALL add_column_if_missing('wh_express_company', 'tenant_id', 'BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER id'); CALL add_column_if_missing('wh_express_company', 'tenant_id', 'BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER id');
@@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS wh_partner (
data_scope_id BIGINT UNSIGNED NOT NULL COMMENT '所属业务数据范围', data_scope_id BIGINT UNSIGNED NOT NULL COMMENT '所属业务数据范围',
partner_code VARCHAR(50) NOT NULL COMMENT '平台内唯一往来单位编码', partner_name VARCHAR(128) NOT NULL COMMENT '往来单位名称', partner_code VARCHAR(50) NOT NULL COMMENT '平台内唯一往来单位编码', partner_name VARCHAR(128) NOT NULL COMMENT '往来单位名称',
partner_type VARCHAR(20) NOT NULL COMMENT '往来单位类型:CARRIER、SHIPPER或CONSIGNEE', partner_type VARCHAR(20) NOT NULL COMMENT '往来单位类型:CARRIER、SHIPPER或CONSIGNEE',
carrier_code VARCHAR(16) NULL COMMENT '船司标准编码:MATSON、SML、ZIM、COSCO、EMC、HEDE或OTHER;仅CARRIER必填',
contact_name VARCHAR(64) NOT NULL COMMENT '联系人姓名', contact_phone VARCHAR(32) NOT NULL COMMENT '联系电话', contact_name VARCHAR(64) NOT NULL COMMENT '联系人姓名', contact_phone VARCHAR(32) NOT NULL COMMENT '联系电话',
contact_email VARCHAR(128) NULL COMMENT '联系邮箱', country_code CHAR(2) NOT NULL COMMENT '国家二字码', contact_email VARCHAR(128) NULL COMMENT '联系邮箱', country_code CHAR(2) NOT NULL COMMENT '国家二字码',
province VARCHAR(64) NULL COMMENT '省或州', city VARCHAR(64) NOT NULL COMMENT '城市', district VARCHAR(64) NULL COMMENT '区县', province VARCHAR(64) NULL COMMENT '省或州', city VARCHAR(64) NOT NULL COMMENT '城市', district VARCHAR(64) NULL COMMENT '区县',
@@ -16,7 +17,8 @@ CREATE TABLE IF NOT EXISTS wh_partner (
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY(id), UNIQUE KEY uk_wh_partner_tenant_code(tenant_id,partner_code,deleted), PRIMARY KEY(id), UNIQUE KEY uk_wh_partner_tenant_code(tenant_id,partner_code,deleted),
KEY idx_wh_partner_tenant_status(tenant_id,status,deleted), KEY idx_wh_partner_tenant_status(tenant_id,status,deleted),
CONSTRAINT chk_wh_partner_type CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE')) CONSTRAINT chk_wh_partner_type CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE')),
CONSTRAINT chk_wh_partner_carrier_code CHECK (carrier_code IS NULL OR carrier_code IN ('MATSON','SML','ZIM','COSCO','EMC','HEDE','OTHER'))
) ENGINE=InnoDB COMMENT='平台统一维护、供全部客户共享的往来单位主数据'; ) ENGINE=InnoDB COMMENT='平台统一维护、供全部客户共享的往来单位主数据';
CREATE TABLE IF NOT EXISTS wh_partner_type ( CREATE TABLE IF NOT EXISTS wh_partner_type (
partner_id BIGINT UNSIGNED NOT NULL COMMENT '往来单位ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共往来单位固定使用 tenant_id=0', partner_id BIGINT UNSIGNED NOT NULL COMMENT '往来单位ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共往来单位固定使用 tenant_id=0',
@@ -25,11 +25,12 @@ class WarehouseSchemaContractTest {
void shouldHandleCase02() throws IOException { void shouldHandleCase02() throws IOException {
String sql = read("warehouse-partner.sql"); String sql = read("warehouse-partner.sql");
assertColumns(sql, "wh_partner", "data_scope_id", "partner_code", "partner_name", "partner_type", "contact_name", assertColumns(sql, "wh_partner", "data_scope_id", "partner_code", "partner_name", "partner_type", "contact_name",
"contact_phone", "contact_email", "country_code", "province", "city", "district", "address_line", "carrier_code", "contact_phone", "contact_email", "country_code", "province", "city", "district", "address_line",
"postal_code", "status", "remark"); "postal_code", "status", "remark");
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_partner_type") assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_partner_type")
.contains("idx_wh_partner_type_scope") .contains("idx_wh_partner_type_scope")
.contains("chk_wh_partner_type CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE'))") .contains("chk_wh_partner_type CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE'))")
.contains("chk_wh_partner_carrier_code CHECK (carrier_code IS NULL OR carrier_code IN ('MATSON','SML','ZIM','COSCO','EMC','HEDE','OTHER'))")
.contains("chk_wh_partner_type_value CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE'))") .contains("chk_wh_partner_type_value CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE'))")
.doesNotContain("CUSTOMS_BROKER", "SUPPLIER或BOTH"); .doesNotContain("CUSTOMS_BROKER", "SUPPLIER或BOTH");
assertBaseColumns(sql); assertBaseColumns(sql);
@@ -100,6 +101,8 @@ class WarehouseSchemaContractTest {
.contains("CALL add_column_if_missing('wh_warehouse_address'") .contains("CALL add_column_if_missing('wh_warehouse_address'")
.contains("CALL add_column_if_missing('wh_service_capability'") .contains("CALL add_column_if_missing('wh_service_capability'")
.contains("CALL add_column_if_missing('wh_partner'") .contains("CALL add_column_if_missing('wh_partner'")
.contains("CALL add_column_if_missing('wh_partner', 'carrier_code', 'VARCHAR(16) NULL")
.contains("UPDATE wh_partner SET carrier_code = 'OTHER'")
.contains("CALL add_column_if_missing('wh_express_company'") .contains("CALL add_column_if_missing('wh_express_company'")
.contains("DROP PROCEDURE IF EXISTS add_column_if_missing") .contains("DROP PROCEDURE IF EXISTS add_column_if_missing")
.doesNotContain("ADD COLUMN IF NOT EXISTS") .doesNotContain("ADD COLUMN IF NOT EXISTS")
@@ -150,7 +153,13 @@ class WarehouseSchemaContractTest {
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql")); String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"));
assertThat(tableDefinition(unified, "wh_partner")) assertThat(tableDefinition(unified, "wh_partner"))
.contains("chk_wh_partner_type CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE'))") .contains("chk_wh_partner_type CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE'))")
.contains("carrier_code VARCHAR(16) NULL")
.contains("chk_wh_partner_carrier_code CHECK (carrier_code IS NULL OR carrier_code IN ('MATSON','SML','ZIM','COSCO','EMC','HEDE','OTHER'))")
.doesNotContain("CUSTOMS_BROKER", "SUPPLIER或BOTH"); .doesNotContain("CUSTOMS_BROKER", "SUPPLIER或BOTH");
assertThat(unified)
.contains("CALL add_column_if_missing('wh_partner', 'carrier_code', 'VARCHAR(16) NULL")
.contains("UPDATE wh_partner SET carrier_code = 'OTHER'")
.contains("queue_column_comment('baiyue_warehouse', 'wh_partner', 'carrier_code'");
} }
private static String read(String file) throws IOException { private static String read(String file) throws IOException {