优化
This commit is contained in:
@@ -890,6 +890,7 @@ CREATE TABLE IF NOT EXISTS wh_partner (
|
||||
data_scope_id BIGINT UNSIGNED 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',
|
||||
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_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 '区县',
|
||||
@@ -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 '乐观锁版本号',
|
||||
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),
|
||||
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='平台统一维护、供全部客户共享的往来单位主数据';
|
||||
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',
|
||||
@@ -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_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', '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_email', 'VARCHAR(128) NULL');
|
||||
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', 'deleted', 'TINYINT(1) 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 建表;此段补齐早期部署缺列。
|
||||
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', 'tenant_id', '仓储公共资源域,固定为0');
|
||||
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', 'contact_email', '联系邮箱');
|
||||
CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'contact_name', '联系人姓名');
|
||||
|
||||
@@ -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。
|
||||
|
||||
海外 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
|
||||
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', () => {
|
||||
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)
|
||||
for (const token of ['/inventory/resolve', '/locations/resolve', '/pallets/resolve', '/operations?page=1&size=50']) {
|
||||
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, /key\(context, `fba-bulk-\$\{index\}`\)/)
|
||||
assert.match(source, /packageCount = Number\(resolved\.data\?\.pendingPackages \|\| 0\)/)
|
||||
assert.match(source, /if \(packageCount <= 0\) continue/)
|
||||
assert.match(source, /packageCount = Number\(inbound\.pendingPackages \|\| 0\)/)
|
||||
assert.match(source, /if \(packageCount <= 0\) return true/)
|
||||
assert.match(source, /key\(context, `overseas-carton-\$\{index\}`\)/)
|
||||
assert.match(source, /模式硬拒绝/)
|
||||
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, /preferredProducer\?\.cartonMarks\?\.length/)
|
||||
assert.match(source, /const cartonCandidates =/)
|
||||
assert.match(source, /candidateExcluded = notFound\(response\)/)
|
||||
assert.match(source, /if \(candidateExcluded\) continue/)
|
||||
assert.match(source, /const fixturesConserved = cartonFixtures\.length === expectedPending/)
|
||||
assert.doesNotMatch(source, /current\/skus\/resolve/)
|
||||
assert.doesNotMatch(source, /skuCode:/)
|
||||
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, /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.match(source, /last-leg\/orders\?page=1&size=200&containerNo=/)
|
||||
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, /awaitProducerConsumption\(context, allProducers\)/)
|
||||
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, /missingPickupReady=/)
|
||||
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/)
|
||||
})
|
||||
|
||||
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', () => {
|
||||
const orderedTokens = [
|
||||
"'查询当前客户待收柜号'", "'选择柜号并建立WAIT_SIGN会话'",
|
||||
|
||||
@@ -161,7 +161,7 @@ async function createPallet(context, inbound, index) {
|
||||
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', '完成入库后按入仓码现场查询库存',
|
||||
'/api/pda/v1/last-leg/inventory/resolve', { code: inboundCode })
|
||||
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
|
||||
&& id(item?.locationId) === pallet.locationId && Number(item?.availablePackages) > 0),
|
||||
`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', '完成入库后扫码查询库位',
|
||||
'/api/pda/v1/last-leg/locations/resolve', { code: pallet.locationCode })
|
||||
@@ -245,9 +250,9 @@ async function awaitProducerConsumption(context, producers) {
|
||||
}
|
||||
for (const containerNo of expected) {
|
||||
const response = lastResponses.get(containerNo) || null
|
||||
context.addStep(MODULE, `跨区消费生成尾程订单 ${containerNo}`, response,
|
||||
context.addStep(MODULE, `跨区广播消费观察 ${containerNo}`, response,
|
||||
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))
|
||||
}
|
||||
@@ -288,110 +293,76 @@ async function awaitPickupReadyCandidates(context, token, producers, allProducer
|
||||
return ready
|
||||
}
|
||||
|
||||
function skuFixtures(context, producer) {
|
||||
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 = []) {
|
||||
async function resolveCartonFixtures(context, token, inbounds, producerCartonMarks = []) {
|
||||
const cartonCandidates = [...new Set((Array.isArray(producerCartonMarks) ? producerCartonMarks : [producerCartonMarks])
|
||||
.map(id).filter(nonBlank))]
|
||||
if (!cartonCandidates.length) {
|
||||
blockerGap(context, 'OVERSEAS逐箱链', '缺少本轮PDA任务冻结箱唛,逐箱实收不能使用伪造箱号')
|
||||
return false
|
||||
blockerGap(context, '统一箱码解析链', '缺少本轮PDA任务冻结箱码,FBA和OVERSEAS均不能使用伪造箱号')
|
||||
return null
|
||||
}
|
||||
const expectedPending = inbounds.reduce((total, inbound) => total + Number(inbound.pendingPackages || 0), 0)
|
||||
const cartonFixtures = []
|
||||
let lastResponse = null
|
||||
for (const [index, cartonMark] of cartonCandidates.entries()) {
|
||||
const response = await context.requestOverseas('POST',
|
||||
'/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 inbound = inbounds.find((item) => item.inboundCode === resolved?.inboundCode)
|
||||
const pallet = inbound ? pallets.get(inbound.inboundCode) : null
|
||||
if (!inbound || !pallet) {
|
||||
blockerGap(context, `OVERSEAS箱唛${cartonMark}归属`,
|
||||
`冻结箱唛未解析到带真实托盘的本柜入仓码;inboundCode=${resolved?.inboundCode || 'missing'}`,
|
||||
const matches = inbounds.filter((item) => item.inboundCode === resolved?.inboundCode)
|
||||
const unique = success(response) && matches.length === 1
|
||||
&& ['FBA', 'OVERSEAS'].includes(resolved?.destinationType)
|
||||
&& matches[0]?.destinationType === resolved?.destinationType
|
||||
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)
|
||||
return false
|
||||
return null
|
||||
}
|
||||
cartonFixtures.push({ cartonMark, pallet })
|
||||
cartonFixtures.push({ cartonMark, inbound: matches[0], resolved })
|
||||
}
|
||||
const fixturesConserved = cartonFixtures.length === expectedPending
|
||||
context.assert(MODULE, 'OVERSEAS实装箱唛与待收箱数守恒', lastResponse, fixturesConserved,
|
||||
`resolvedFixtures=${cartonFixtures.length}, pendingPackages=${expectedPending}; ${trace(lastResponse)}`, OPTIONS)
|
||||
if (!fixturesConserved) {
|
||||
blockerGap(context, 'OVERSEAS逐箱冻结事实',
|
||||
`候选池未完整覆盖本柜真实冻结箱唛;resolvedFixtures=${cartonFixtures.length}, pendingPackages=${expectedPending}`,
|
||||
'fixture-mismatch', lastResponse)
|
||||
return cartonFixtures
|
||||
}
|
||||
|
||||
async function receiveFba(context, token, inbound, pallet, cartonFixtures, index) {
|
||||
const resolvedCartons = cartonFixtures.filter((item) => item.inbound.inboundCode === inbound.inboundCode
|
||||
&& item.resolved.destinationType === 'FBA')
|
||||
if (!resolvedCartons.length) {
|
||||
blockerGap(context, `FBA入仓码${inbound.inboundCode}箱码解析`, '没有冻结箱码以200/code 200唯一解析到当前FBA入仓码')
|
||||
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 body = { cartonMark, palletCode: pallet.palletCode,
|
||||
idempotencyKey: key(context, `overseas-carton-${index}`) }
|
||||
@@ -482,6 +453,10 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
|
||||
{ ...signBody, photos: [...photos].reverse() }, conflict)
|
||||
const current = await pda(context, token, 'GET', '签收后按账号恢复当前柜', '/api/pda/v1/last-leg/receiving-containers/current')
|
||||
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()
|
||||
for (const [index, inbound] of current.data.inbounds.entries()) {
|
||||
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')
|
||||
if (fbaInbounds.length && fbaInbounds.every((item) => pallets.has(item.inboundCode))) {
|
||||
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', {
|
||||
cartonMark: context.createdResources?.inboundMeasurement?.boxCode || 'MODE-REJECT-FBA',
|
||||
cartonMark: fbaCarton?.cartonMark,
|
||||
palletCode: pallets.get(firstFba.inboundCode).palletCode, idempotencyKey: key(context, 'fba-carton-forbidden'),
|
||||
}, (response) => conflictMessage(response, /FBA.*bulk|bulk.*FBA/i))
|
||||
for (const inbound of fbaInbounds) {
|
||||
await receiveFba(context, token, inbound, pallets.get(inbound.inboundCode), preferredProducer)
|
||||
}, (response) => a0409Message(response, /^FBA destination must use bulk count$/))
|
||||
for (const [index, inbound] of fbaInbounds.entries()) {
|
||||
await receiveFba(context, token, inbound, pallets.get(inbound.inboundCode), cartonFixtures, index)
|
||||
}
|
||||
} 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))) {
|
||||
const firstOverseas = overseasInbounds[0]
|
||||
const pallet = pallets.get(firstOverseas.inboundCode)
|
||||
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'),
|
||||
}, (response) => conflictMessage(response, /OVERSEAS.*carton|bulk.*forbidden/i))
|
||||
await receiveOverseas(context, token, overseasInbounds, pallets,
|
||||
preferredProducer?.cartonMarks?.length ? preferredProducer.cartonMarks : [preferredProducer?.cartonMark])
|
||||
}, (response) => a0409Message(response, /^OVERSEAS destination must use carton-by-carton scanning; bulk count is forbidden$/))
|
||||
await receiveOverseas(context, token, overseasInbounds, pallets, cartonFixtures)
|
||||
} else if (preferredProducer?.destinationType !== 'FBA') {
|
||||
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)
|
||||
for (const inbound of current.data.inbounds.filter((item) => pallets.has(item.inboundCode))) {
|
||||
const warehouseFacts = await verifyWarehouseQueries(context, token, inbound.inboundCode,
|
||||
pallets.get(inbound.inboundCode))
|
||||
pallets.get(inbound.inboundCode), inbound.destinationType)
|
||||
if (warehouseFacts && context.context?.phase4OverseasPdaOperations) {
|
||||
context.context.phase4OverseasPdaOperations.completedFixtures.push({
|
||||
containerNo, order, ...warehouseFacts,
|
||||
@@ -596,11 +576,7 @@ export async function run(context) {
|
||||
const producers = [dropship || matrix?.byDestinationType?.FBA?.[0], matrix?.byDestinationType?.OVERSEAS?.[0]]
|
||||
.filter((item) => nonBlank(item?.containerNo))
|
||||
const allProducers = Array.isArray(matrix?.containers) ? matrix.containers : producers
|
||||
if (allProducers.length && !(await awaitProducerConsumption(context, allProducers))) {
|
||||
blockerGap(context, '跨区消费生产矩阵',
|
||||
'三期已发布的四柜尚未全部由海外消费者生成尾程订单,禁止把PICKUP_READY缺口与事件消费混为一谈')
|
||||
return
|
||||
}
|
||||
if (allProducers.length) await awaitProducerConsumption(context, allProducers)
|
||||
if (producers.length && !(await awaitPickupReadyCandidates(context, primary.token, producers, allProducers))) {
|
||||
blockerGap(context, '实际处理柜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_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', '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_email', 'VARCHAR(128) NULL');
|
||||
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', 'deleted', 'TINYINT(1) 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 建表;此段补齐早期部署缺列。
|
||||
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 '所属业务数据范围',
|
||||
partner_code VARCHAR(50) NOT NULL COMMENT '平台内唯一往来单位编码', partner_name VARCHAR(128) NOT NULL COMMENT '往来单位名称',
|
||||
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_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 '区县',
|
||||
@@ -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 '乐观锁版本号',
|
||||
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),
|
||||
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='平台统一维护、供全部客户共享的往来单位主数据';
|
||||
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',
|
||||
|
||||
+10
-1
@@ -25,11 +25,12 @@ class WarehouseSchemaContractTest {
|
||||
void shouldHandleCase02() throws IOException {
|
||||
String sql = read("warehouse-partner.sql");
|
||||
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");
|
||||
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_partner_type")
|
||||
.contains("idx_wh_partner_type_scope")
|
||||
.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'))")
|
||||
.doesNotContain("CUSTOMS_BROKER", "SUPPLIER或BOTH");
|
||||
assertBaseColumns(sql);
|
||||
@@ -100,6 +101,8 @@ class WarehouseSchemaContractTest {
|
||||
.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_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("DROP PROCEDURE IF EXISTS add_column_if_missing")
|
||||
.doesNotContain("ADD COLUMN IF NOT EXISTS")
|
||||
@@ -150,7 +153,13 @@ class WarehouseSchemaContractTest {
|
||||
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"));
|
||||
assertThat(tableDefinition(unified, "wh_partner"))
|
||||
.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");
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user