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
@@ -207,11 +207,95 @@ function completeManualNodes(marker) {
})).map((node, index) => index === 3 ? { ...node, nodeAt: null } : node)
}
function previewGroupRank(group) {
return ({ FBA: 0, ONE_CLICK_DROPSHIP: 1, PRIVATE_ADDRESS: 2 })[group?.destinationType] ?? 99
}
function previewGroupsSorted(groups) {
const rows = Array.isArray(groups) ? groups : []
return rows.every((group, index) => {
if (index === 0) return true
const previous = rows[index - 1]
const rankDifference = previewGroupRank(previous) - previewGroupRank(group)
return rankDifference < 0 || (rankDifference === 0
&& String(previous?.destinationWarehouseCode || '').localeCompare(
String(group?.destinationWarehouseCode || '')) <= 0)
})
}
async function convertContainerPlans(context, producer, details) {
const bookingId = id(producer.containerBookingId)
const previewPath = query(`${PLANS}/container-preview`, { sourceContainerBookingId: bookingId })
const preview = await api(context, 'GET', '预览整柜待生成入仓计划清单', previewPath)
const groups = Array.isArray(preview.data?.groups) ? preview.data.groups : []
const previewOrders = groups.flatMap((group) => Array.isArray(group?.orders) ? group.orders : [])
const pendingPalletCount = Number(preview.data?.pendingPalletCount)
context.assert(MODULE, '柜级预览列清尾程单、入仓码、目的仓、业务类型与建议托数', preview.response,
preview.data?.ready === true && id(preview.data?.sourceContainerBookingId) === bookingId
&& previewOrders.length === details.length && pendingPalletCount > 0
&& previewOrders.every((order) => nonBlank(order?.businessOrderNo) && nonBlank(order?.inboundCode)
&& nonBlank(order?.destinationType) && nonBlank(order?.destinationWarehouseCode)
&& Number(order?.suggestedPalletCount) > 0 && order?.readiness === 'READY'
&& Array.isArray(order?.blockers) && order.blockers.length === 0),
`orders=${previewOrders.length}, pendingPallets=${pendingPalletCount}, groups=${groups.length}; ${trace(preview.response)}`,
OPTIONS)
context.assert(MODULE, '柜级预览按FBA、一件代发、私人地址及目的仓稳定排序', preview.response,
previewGroupsSorted(groups), groups.map((group) => `${group.destinationType}:${group.destinationWarehouseCode}`).join(','), OPTIONS)
context.assert(MODULE, '一件代发的FBA目的仓快照保留给自动托盘', preview.response,
previewOrders.filter((order) => order?.destinationType === 'ONE_CLICK_DROPSHIP'
&& nonBlank(order?.destinationWarehouseCode)).every((order) => nonBlank(order?.fbaWarehouseCode)),
`dropshipFba=${previewOrders.filter((order) => nonBlank(order?.fbaWarehouseCode)).length}`, OPTIONS)
if (!success(preview.response) || preview.data?.ready !== true || pendingPalletCount <= 0) return null
const body = { sourceContainerBookingId: bookingId, snapshotVersion: Number(preview.data.snapshotVersion),
snapshotHash: preview.data.snapshotHash }
const key = `${context.marker}-container-plan-confirm`.slice(0, 128)
const converted = await api(context, 'POST', '按柜确认并自动生成入仓计划与实体托盘',
`${PLANS}/convert-container`, body, { 'Idempotency-Key': key })
const plans = Array.isArray(converted.data) ? converted.data : []
const pallets = plans.flatMap((plan) => Array.isArray(plan?.pallets) ? plan.pallets.map((pallet) => ({ plan, pallet })) : [])
context.assert(MODULE, '按柜确认逐票生成独立计划且托盘总数与预览一致', converted.response,
plans.length === details.length && pallets.length === pendingPalletCount
&& plans.every((plan) => plan?.creationMode === 'AUTO' && nonBlank(plan?.planNo)
&& Array.isArray(plan?.lines) && plan.lines.length === 1)
&& pallets.every(({ pallet }) => nonBlank(pallet?.palletNo) && id(pallet?.warehousePalletId)
&& id(pallet?.warehouseLocationId) && pallet?.assignmentStatus === 'RESERVED'),
`plans=${plans.length}, pallets=${pallets.length}, expectedPallets=${pendingPalletCount}; ${trace(converted.response)}`,
OPTIONS)
if (!success(converted.response)) return null
for (const { plan, pallet } of pallets) {
const line = plan.lines[0]
const palletPath = query(`/api/platform/v1/warehouse-admin/${encodeURIComponent(id(preview.data.overseasWarehouseId))}/pallets`, {
page: 1, size: 10, keyword: pallet.palletNo,
})
const listed = await api(context, 'GET', `回查自动托盘${pallet.palletNo}`, palletPath)
const stored = records(listed.data).find((row) => row?.palletCode === pallet.palletNo)
context.assert(MODULE, '自动托盘进入托盘列表并保留FBA目的仓', listed.response,
id(stored?.id) === id(pallet.warehousePalletId) && stored?.status === 'OCCUPIED'
&& (!nonBlank(line?.fbaWarehouseCode) || stored?.fbaWarehouseCode === line.fbaWarehouseCode),
`pallet=${pallet.palletNo}, expectedFba=${line?.fbaWarehouseCode || 'none'}, actualFba=${stored?.fbaWarehouseCode || 'none'}; ${trace(listed.response)}`,
OPTIONS)
}
const replay = await api(context, 'POST', '按柜确认同键同快照幂等重放',
`${PLANS}/convert-container`, body, { 'Idempotency-Key': key })
context.assert(MODULE, '按柜确认重放不新增计划或托盘', replay.response,
JSON.stringify(replay.data) === JSON.stringify(converted.data), trace(replay.response), OPTIONS)
const after = await api(context, 'GET', '回查整柜计划与托盘均已生成', previewPath)
context.assert(MODULE, '确认后预览待生成票数和托数归零', after.response,
Number(after.data?.pendingOrderCount) === 0 && Number(after.data?.pendingPalletCount) === 0
&& Number(after.data?.totalPlannedPalletCount) >= pendingPalletCount,
`pendingOrders=${after.data?.pendingOrderCount}, pendingPallets=${after.data?.pendingPalletCount}, totalPlannedPallets=${after.data?.totalPlannedPalletCount}; ${trace(after.response)}`,
OPTIONS)
return plans
}
export async function run(context) {
context.createdResources.phase4TailOrderControls = {
marker: context.marker, containerNo: null, orderIds: [], planNos: [], representativeOrderId: null,
finalStatus: null,
cleanup: 'Manual inbound plans remain as the marker-owned prerequisites consumed by overseas PDA receiving',
cleanup: 'Automatically generated container inbound plans and pallets remain as marker-owned prerequisites consumed by overseas PDA receiving',
}
const producer = targetProducer(context)
if (!producer?.containerNo || !producer?.tenantId) {
@@ -299,38 +383,13 @@ export async function run(context) {
nodesRead.data?.manualOverrideEnabled === true && nodesRead.data?.nodes?.length === 8
&& nodesRead.data.nodes.every((node) => node?.source === 'MANUAL'), trace(nodesRead.response), OPTIONS)
const plans = []
for (const original of refreshedDetails) {
const refreshed = await detail(context, original, `创建人工计划前刷新订单${id(original.id)}版本`)
const order = refreshed.data?.order
if (!success(refreshed.response) || !order) return
const body = manualPlanBody(order, order.version, context.marker)
const key = `${context.marker}-manual-plan-${id(order.id)}`.slice(0, 128)
const created = await api(context, 'POST', `为本轮尾程订单${id(order.id)}创建人工计划`, `${PLANS}/manual`, body,
{ 'Idempotency-Key': key })
context.assert(MODULE, '人工计划按订单稳定接柜仓回读当前主档并冻结入仓码、托数和操作标志', created.response,
Boolean(created.data?.planNo) && created.data?.creationMode === 'MANUAL'
&& Boolean(created.data?.overseasWarehouseCode) && Boolean(created.data?.overseasWarehouseName)
&& created.data?.plannedArrivalDate === body.plannedArrivalDate
&& created.data?.palletType === body.palletType && created.data?.urgent === true
&& Number(created.data?.palletCount) === body.palletCount
&& created.data?.lines?.some((line) => id(line?.orderId) === id(order.id) && line?.inboundCode === order.inboundCode),
`orderId=${id(order.id)}, planNo=${created.data?.planNo || 'missing'}; ${trace(created.response)}`, OPTIONS)
if (!success(created.response) || !created.data?.planNo) return
const replay = await api(context, 'POST', `人工计划${created.data.planNo}幂等重放`, `${PLANS}/manual`, body,
{ 'Idempotency-Key': key })
context.assert(MODULE, '人工计划重放复用同一计划且零新增', replay.response,
replay.data?.planNo === created.data.planNo, trace(replay.response), OPTIONS)
if (plans.length === 0) {
const hashConflict = await context.requestOverseas('POST', `${PLANS}/manual`,
{ ...body, remark: `${context.marker}-DIFFERENT` }, { 'Idempotency-Key': key }, context.overseasToken)
context.addStep(MODULE, '人工计划同键异载荷返回A0409', hashConflict, conflict(hashConflict), trace(hashConflict), OPTIONS)
}
plans.push(created.data.planNo)
}
const generated = await convertContainerPlans(context, producer, refreshedDetails)
if (!generated) return
context.createdResources.phase4TailOrderControls = {
...context.createdResources.phase4TailOrderControls,
containerNo: producer.containerNo, orderIds: refreshedDetails.map((order) => id(order.id)),
planNos: plans, representativeOrderId: id(representative.id), finalStatus: 'MANUAL_PLANS_READY',
planNos: generated.map((plan) => plan.planNo), representativeOrderId: id(representative.id),
generatedPlans: generated,
finalStatus: 'AUTO_CONTAINER_PLANS_READY',
}
}