fix(lastleg): complete generated pallet receiving flow

This commit is contained in:
2026-09-09 02:54:09 +08:00
parent 86646ccd69
commit c5862db98d
20 changed files with 367 additions and 225 deletions
@@ -163,29 +163,42 @@ export async function disablePicker(context, picker, index) {
context.addStep(MODULE, `finally停用海外PDA仓库操作员${index}`, response, disabled, trace(response), OPTIONS)
}
async function createPallet(context, inbound, index) {
const warehouseId = id(inbound?.overseasWarehouseId)
if (!longId(warehouseId)) return null
const suffix = markerSuffix(context)
const locationCode = `PDA-${suffix}-${index + 1}`.slice(0, 64)
const location = await context.requestOverseas('POST', `/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/locations`, {
zoneCode: `PDA-${index + 1}`, locationCode,
locationType: 'STORAGE', maxLoadKg: 5000, status: 'IDLE', remark: `${context.marker}-receiving`,
})
context.addStep(MODULE, `创建当前柜实收库位${index + 1}`, location, success(location), trace(location), OPTIONS)
const locationId = id(location?.body?.data)
if (!longId(locationId)) return null
context.createdResources.phase4OverseasPda.locationIds.push(locationId)
const palletCode = `PDA-${suffix}-${index + 1}`.slice(0, 128)
const unitPrice = Number((1.01 + index).toFixed(2))
const pallet = await context.requestOverseas('POST', `/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets`, {
palletCode, palletType: '木托盘', capacityCbm: 10, unitPrice, status: 'IDLE', locationId,
remark: `${context.marker}-receiving-retained`,
fbaWarehouseCode: inbound.destinationType === 'FBA' ? inbound.fbaWarehouseCode : null,
})
context.addStep(MODULE, `创建当前柜实收托盘${index + 1}`, pallet, success(pallet), trace(pallet), OPTIONS)
const palletId = id(pallet?.body?.data)
return longId(palletId) ? { warehouseId, locationId, locationCode, palletId, palletCode, unitPrice, retained: true } : null
function rememberGeneratedPlans(context, plans) {
if (!Array.isArray(plans)) return
for (const plan of plans) {
const inboundCode = id(plan?.lines?.[0]?.inboundCode)
if (!nonBlank(inboundCode)) continue
const generated = Array.isArray(plan?.pallets) ? plan.pallets : []
context.createdResources.phase4OverseasPda.generatedPalletsByInbound[inboundCode] = generated.map((pallet) => ({
warehouseId: id(plan?.overseasWarehouseId),
locationId: id(pallet?.warehouseLocationId),
palletId: id(pallet?.warehousePalletId),
palletCode: id(pallet?.palletNo),
unitPrice: Number(pallet?.unitPrice || 0),
retained: true,
}))
}
}
async function resolveGeneratedPallet(context, inbound) {
const candidates = context.createdResources.phase4OverseasPda.generatedPalletsByInbound[inbound.inboundCode] || []
const pallet = candidates[0]
if (!pallet || !longId(pallet.palletId) || !nonBlank(pallet.palletCode)) return null
const warehouseId = id(inbound?.overseasWarehouseId || pallet.warehouseId)
const listed = await context.requestOverseas('GET',
`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets?page=1&size=10&keyword=${encodeURIComponent(pallet.palletCode)}`)
const stored = records(listed?.body?.data).find((row) => id(row?.id) === pallet.palletId && row?.palletCode === pallet.palletCode)
const locations = stored && longId(stored?.locationId)
? await context.requestOverseas('GET',
`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/locations?page=1&size=200`)
: null
const location = records(locations?.body?.data).find((row) => id(row?.id) === id(stored?.locationId))
const resolved = success(listed) && success(locations) && stored && longId(stored?.locationId) && nonBlank(location?.locationCode)
context.addStep(MODULE, `复用入仓码${inbound.inboundCode}自动计划托盘`, listed, resolved,
`palletCode=${pallet.palletCode}, palletId=${pallet.palletId}, locationId=${id(stored?.locationId) || 'missing'}, locationCode=${location?.locationCode || 'missing'}; ${trace(listed)}`, OPTIONS)
if (!resolved) return null
return { ...pallet, warehouseId, locationId: id(stored.locationId), locationCode: location.locationCode,
unitPrice: Number(stored.unitPrice || pallet.unitPrice || 0) }
}
async function verifyWarehouseQueries(context, token, inboundCode, pallet, destinationType) {
@@ -306,34 +319,59 @@ async function convertReceivingContainers(context, producers) {
convertible,
`sourceContainerBookingId=${id(summary?.sourceContainerBookingId) || 'missing'}, allPlansReady=${summary?.allPlansReady === true}, orders=${totalOrders}, inboundCodes=${inboundCodes}, plans=${existingPlans}, pending=${pendingPlans}; ${trace(summaryResponse)}`, OPTIONS)
if (!convertible) return false
const body = { sourceContainerBookingId: id(summary.sourceContainerBookingId) }
const key = `${context.marker}-pda-convert-${containerNo}`.slice(0, 128)
const convertedResponse = await context.requestOverseas('POST',
'/api/platform/v1/last-leg/inbound-plans/convert-container', body,
{ 'Idempotency-Key': key }, context.overseasToken)
const plans = success(convertedResponse) && Array.isArray(convertedResponse.body?.data)
? convertedResponse.body.data : []
const convertedInboundCodes = new Set(plans
.flatMap((item) => Array.isArray(item?.lines) ? item.lines : [])
.map((line) => id(line?.inboundCode)).filter(nonBlank))
context.addStep(MODULE, `整柜转换为逐入仓码独立计划 ${containerNo}`, convertedResponse,
plans.length === inboundCodes
&& plans.every((item) => Array.isArray(item?.lines) && item.lines.length > 0)
&& convertedInboundCodes.size === inboundCodes,
`plans=${plans.length}, inboundCodes=${convertedInboundCodes.size}, expected=${inboundCodes}; ${trace(convertedResponse)}`, OPTIONS)
if (plans.length !== inboundCodes) return false
const replayResponse = await context.requestOverseas('POST',
'/api/platform/v1/last-leg/inbound-plans/convert-container', body,
{ 'Idempotency-Key': key }, context.overseasToken)
context.addStep(MODULE, `整柜转换幂等重放零新增 ${containerNo}`, replayResponse,
success(replayResponse) && JSON.stringify(replayResponse.body?.data) === JSON.stringify(plans),
trace(replayResponse), OPTIONS)
if (!success(replayResponse)) return false
const bookingId = id(summary.sourceContainerBookingId)
const previewPath = `/api/platform/v1/last-leg/inbound-plans/container-preview?sourceContainerBookingId=${encodeURIComponent(bookingId)}`
const previewResponse = await context.requestOverseas('GET', previewPath,
undefined, {}, context.overseasToken)
const preview = success(previewResponse) ? previewResponse.body?.data : null
context.addStep(MODULE, `读取PDA前柜级计划与自动托盘预览 ${containerNo}`, previewResponse,
preview?.ready === true && Number(preview?.totalOrderCount) === totalOrders
&& Number(preview?.pendingOrderCount) === pendingPlans,
`orders=${preview?.totalOrderCount ?? 'missing'}, pendingOrders=${preview?.pendingOrderCount ?? 'missing'}, pendingPallets=${preview?.pendingPalletCount ?? 'missing'}; ${trace(previewResponse)}`,
OPTIONS)
if (preview?.ready !== true) return false
if (pendingPlans > 0) {
const body = { sourceContainerBookingId: bookingId,
snapshotVersion: Number(preview.snapshotVersion), snapshotHash: preview.snapshotHash }
const key = `${context.marker}-pda-convert-${containerNo}`.slice(0, 128)
const convertedResponse = await context.requestOverseas('POST',
'/api/platform/v1/last-leg/inbound-plans/convert-container', body,
{ 'Idempotency-Key': key }, context.overseasToken)
const plans = success(convertedResponse) && Array.isArray(convertedResponse.body?.data)
? convertedResponse.body.data : []
const convertedInboundCodes = new Set(plans
.flatMap((item) => Array.isArray(item?.lines) ? item.lines : [])
.map((line) => id(line?.inboundCode)).filter(nonBlank))
context.addStep(MODULE, `整柜转换为逐入仓码独立计划及实体托盘 ${containerNo}`, convertedResponse,
plans.length === inboundCodes
&& plans.every((item) => Array.isArray(item?.lines) && item.lines.length > 0
&& Array.isArray(item?.pallets) && item.pallets.length > 0
&& item.pallets.every((pallet) => nonBlank(pallet?.palletNo) && longId(id(pallet?.warehousePalletId))))
&& convertedInboundCodes.size === inboundCodes,
`plans=${plans.length}, inboundCodes=${convertedInboundCodes.size}, expected=${inboundCodes}; ${trace(convertedResponse)}`, OPTIONS)
if (plans.length !== inboundCodes) return false
rememberGeneratedPlans(context, plans)
const replayResponse = await context.requestOverseas('POST',
'/api/platform/v1/last-leg/inbound-plans/convert-container', body,
{ 'Idempotency-Key': key }, context.overseasToken)
context.addStep(MODULE, `整柜转换幂等重放零新增 ${containerNo}`, replayResponse,
success(replayResponse) && JSON.stringify(replayResponse.body?.data) === JSON.stringify(plans),
trace(replayResponse), OPTIONS)
if (!success(replayResponse)) return false
} else {
const retainedPlans = context.createdResources?.phase4TailOrderControls?.generatedPlans
rememberGeneratedPlans(context, retainedPlans)
context.addStep(MODULE, `复用尾程控制已生成的整柜计划与实体托盘 ${containerNo}`, previewResponse,
summary?.allPlansReady === true && existingPlans === totalOrders
&& Number(preview?.existingPlannedPalletCount) > 0,
`plans=${existingPlans}, plannedPallets=${preview?.existingPlannedPalletCount ?? 'missing'}; ${trace(previewResponse)}`,
OPTIONS)
}
const progressResponse = await context.requestOverseas('GET',
`/api/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(summary.sourceContainerBookingId)}`,
undefined, {}, context.overseasToken)
const progress = success(progressResponse) ? progressResponse.body?.data?.summary : null
context.addStep(MODULE, `整柜转换后计划完整度闭环 ${containerNo}`, progressResponse,
context.addStep(MODULE, `PDA前整柜计划完整度闭环 ${containerNo}`, progressResponse,
progress?.allPlansReady === true && Number(progress?.planCount) === totalOrders
&& Number(progress?.pendingPlanCount) === 0,
`allPlansReady=${progress?.allPlansReady === true}, plans=${progress?.planCount ?? 'missing'}, pending=${progress?.pendingPlanCount ?? 'missing'}, plannedArrivalAt=${progress?.plannedArrivalAt || 'missing'}; ${trace(progressResponse)}`, OPTIONS)
@@ -598,10 +636,10 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
if (!success(afterResolve.response) || receivingSnapshot(afterResolve.data) !== beforeResolve) return
const pallets = new Map()
for (const [index, inbound] of current.data.inbounds.entries()) {
const pallet = await createPallet(context, inbound, index)
const pallet = await resolveGeneratedPallet(context, inbound)
if (!pallet) {
blockerGap(context, `${inbound.destinationType}入仓码${inbound.inboundCode}实物托盘`,
`无法在实际海外收货仓 ${inbound.overseasWarehouseId || 'missing'} 创建可解析真实托盘;目的仓 ${inbound.destinationWarehouseId || 'missing'} 不得代替物理仓`, 'warehouse-resource')
blockerGap(context, `${inbound.destinationType}入仓码${inbound.inboundCode}自动计划托盘`,
`按柜确认没有为该入仓码留下可解析的实体托盘;禁止在PDA收货时临时新建脱离计划的托盘`, 'warehouse-resource')
continue
}
pallets.set(inbound.inboundCode, pallet)
@@ -684,6 +722,7 @@ export async function run(context) {
palletIds: [], locationIds: [], finalStatus: null, pickerMembershipIds: [], completedContainers: [],
completedContainerFacts: [],
overseasWarehouseId: null, overseasWarehouseName: null,
generatedPalletsByInbound: {},
cleanup: 'Shared warehouse, pallets and locations used by completed inventory are retained as acceptance evidence',
}
const merchantToken = await loginMerchantForNegativeProbe(context)
@@ -727,7 +766,7 @@ export async function run(context) {
if (allProducers.length) await awaitProducerConsumption(context, allProducers)
if (producers.length && !(await convertReceivingContainers(context, producers))) {
blockerGap(context, '实际处理柜整柜转换门禁',
'目标柜未能通过正式平台convert-container逐入仓码生成独立计划')
'目标柜未能通过正式柜级预览与确认形成逐入仓码计划及实体托盘')
return
}
if (producers.length && !(await awaitPickupReadyCandidates(context, primary.token, producers, allProducers))) {