优化
This commit is contained in:
@@ -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;跨区消费已独立验证,不回退处理历史柜')
|
||||
|
||||
Reference in New Issue
Block a user