This commit is contained in:
2026-08-14 10:57:05 +08:00
parent 66089f9b99
commit 91e781b3ff
16 changed files with 219 additions and 119 deletions
@@ -232,43 +232,83 @@ async function loadOneFragment(context, state) {
const claim = await request(context, 'POST', `${PDA}/tasks/${taskId}/claim`, {
expectedAssignmentVersion: 0, operationId: `${operationBase}-claim`,
})
context.apiSuccess(MODULE, `领取 ${sourceOrderNo} 的 PDA 任务`, claim, '', OPTIONS)
const claimed = context.apiSuccess(MODULE, `领取 ${sourceOrderNo} 的 PDA 任务`, claim, '', OPTIONS)
const claimedItem = claimed?.items?.find?.((row) => id(row.id) === id(pickItem.id)) || pickItem
const inventoryResponse = await context.request('GET',
`${PDA}/tasks/${taskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`)
const inventory = context.apiSuccess(MODULE, `冻结 ${sourceOrderNo} 的 PDA 库存快照`, inventoryResponse, '', OPTIONS)
context.assert(MODULE, `${sourceOrderNo} 库存快照绑定稳定任务身份`, inventoryResponse,
id(inventory?.taskId) === taskId && id(inventory?.loadingPlanId) === planId
&& id(inventory?.containerBookingId) === containerId && id(inventory?.warehouseId) === warehouseId
&& Number(inventory?.taskVersion) === Number(claimed?.version)
&& typeof inventory?.snapshotToken === 'string' && inventory.snapshotToken.length === 64,
`taskId=${id(inventory?.taskId) || 'missing'}, taskVersion=${inventory?.taskVersion ?? 'missing'}, loadingPlanId=${id(inventory?.loadingPlanId) || 'missing'}, containerBookingId=${id(inventory?.containerBookingId) || 'missing'}`, OPTIONS)
if (!inventory?.snapshotToken) return null
const stalePick = await request(context, 'POST', `${PDA}/tasks/${taskId}/items/${id(pickItem.id)}/pick`, {
warehouseId, pickedPackageCount: 1, locationIds: [], version: Number(pickItem.version) + 1,
operationId: `${operationBase}-pick-stale`,
})
context.addStep(MODULE, `余量 ${sourceOrderNo} 的旧明细版本被拒绝`, stalePick,
isConflict(stalePick) && hasTrace(stalePick),
`HTTP=${stalePick.http}, code=${codeOf(stalePick)}, requestId=${stalePick.requestId || 'missing'}, traceId=${stalePick.traceId || 'missing'}`, OPTIONS)
const pickPayload = {
warehouseId, pickedPackageCount: 1, locationIds: [], version: Number(pickItem.version),
operationId: `${operationBase}-pick`,
const batchPath = `${PDA}/tasks/${taskId}/batch`
const batchKey = `${operationBase}-batch`.slice(0, 128)
const batchPayload = {
warehouseId, loadingPlanId: planId, containerBookingId: containerId,
expectedVersion: Number(inventory.taskVersion), inventorySnapshotToken: inventory.snapshotToken,
items: [{
itemId: id(claimedItem.id), action: 'PICK', pickedPackageCount: 1,
version: Number(claimedItem.version), releaseMode: 'MANUAL', locationIds: [],
}],
}
const pick = await request(context, 'POST', `${PDA}/tasks/${taskId}/items/${id(pickItem.id)}/pick`, pickPayload)
const picked = context.apiSuccess(MODULE, `真实拣货并拆分 ${sourceOrderNo}`, pick, '', OPTIONS)
context.assert(MODULE, `${sourceOrderNo} 本分片只承接1箱`, pick,
picked?.items?.some?.((row) => id(row.id) === id(pickItem.id)
&& Number(row.pickedPackageCount) === 1 && Number(row.plannedPackageCount) === 1),
const staleBatch = await request(context, 'POST', batchPath, {
...batchPayload, expectedVersion: Number(inventory.taskVersion) + 1,
}, { 'Idempotency-Key': `${operationBase}-batch-stale`.slice(0, 128) })
context.addStep(MODULE, `余量 ${sourceOrderNo} 的旧任务版本被拒绝且不结算`, staleBatch,
isConflict(staleBatch) && hasTrace(staleBatch),
`HTTP=${staleBatch.http}, code=${codeOf(staleBatch)}, requestId=${staleBatch.requestId || 'missing'}, traceId=${staleBatch.traceId || 'missing'}`, OPTIONS)
const batch = await request(context, 'POST', batchPath, batchPayload, { 'Idempotency-Key': batchKey })
const picked = context.apiSuccess(MODULE, `原子提交 ${sourceOrderNo} 的完整 PDA 草稿`, batch, '', OPTIONS)
context.assert(MODULE, `${sourceOrderNo} 批次拣出1箱并等待人工确认`, batch,
picked?.taskStatus === 'LOADING' && picked?.workflowStatus === 'AWAITING_REVIEW'
&& picked?.items?.some?.((row) => id(row.id) === id(claimedItem.id)
&& Number(row.pickedPackageCount) === 1 && Number(row.plannedPackageCount) === 1),
`workflow=${picked?.workflowStatus || 'missing'}, taskStatus=${picked?.taskStatus || 'missing'}`, OPTIONS)
const replay = await request(context, 'POST', `${PDA}/tasks/${taskId}/items/${id(pickItem.id)}/pick`, pickPayload)
const replayed = context.apiSuccess(MODULE, `${sourceOrderNo} 拆分幂等重放`, replay, '', OPTIONS)
context.assert(MODULE, `${sourceOrderNo} 幂等重放不重复累计`, replay,
id(replayed?.id) === taskId && replayed?.items?.some?.((row) => id(row.id) === id(pickItem.id)
&& Number(row.pickedPackageCount) === 1),
`taskId=${id(replayed?.id) || 'missing'}, picked=${replayed?.items?.find?.((row) => id(row.id) === id(pickItem.id))?.pickedPackageCount ?? 'missing'}`, OPTIONS)
const replay = await request(context, 'POST', batchPath, batchPayload, { 'Idempotency-Key': batchKey })
const replayed = context.apiSuccess(MODULE, `${sourceOrderNo} PDA 批次幂等重放`, replay, '', OPTIONS)
context.assert(MODULE, `${sourceOrderNo} 批次重放不重复累计`, replay,
id(replayed?.id) === taskId && Number(replayed?.version) === Number(picked?.version)
&& replayed?.items?.some?.((row) => id(row.id) === id(claimedItem.id)
&& Number(row.pickedPackageCount) === 1),
`taskId=${id(replayed?.id) || 'missing'}, taskVersion=${replayed?.version ?? 'missing'}`, OPTIONS)
if (sourcePackageCount > 1) {
const differentReplay = await request(context, 'POST', `${PDA}/tasks/${taskId}/items/${id(pickItem.id)}/pick`, {
...pickPayload, pickedPackageCount: 2,
})
const differentReplay = await request(context, 'POST', batchPath, {
...batchPayload,
items: [{ ...batchPayload.items[0], pickedPackageCount: 2 }],
}, { 'Idempotency-Key': batchKey })
context.addStep(MODULE, `${sourceOrderNo} 同幂等键异参被拒绝`, differentReplay,
isConflict(differentReplay) && hasTrace(differentReplay),
`HTTP=${differentReplay.http}, code=${codeOf(differentReplay)}, requestId=${differentReplay.requestId || 'missing'}, traceId=${differentReplay.traceId || 'missing'}`, OPTIONS)
}
const completePath = `${PDA}/tasks/${taskId}/complete`
const completePayload = {
expectedTaskVersion: Number(picked?.version), operationId: `${operationBase}-complete`.slice(0, 128),
remark: context.marker,
}
const complete = await request(context, 'POST', completePath, completePayload)
const completed = context.apiSuccess(MODULE, `人工确认 ${sourceOrderNo} 装柜完成并结算余量`, complete, '', OPTIONS)
context.assert(MODULE, `${sourceOrderNo} 只在 complete 后进入完成态`, complete,
completed?.taskStatus === 'COMPLETED' && completed?.workflowStatus === 'COMPLETED'
&& completed?.items?.some?.((row) => id(row.id) === id(claimedItem.id)
&& Number(row.pickedPackageCount) === 1 && Number(row.plannedPackageCount) === 1),
`workflow=${completed?.workflowStatus || 'missing'}, taskStatus=${completed?.taskStatus || 'missing'}`, OPTIONS)
if (completed?.taskStatus !== 'COMPLETED' || completed?.workflowStatus !== 'COMPLETED') return null
const completeReplay = await request(context, 'POST', completePath, completePayload)
const completedReplay = context.apiSuccess(MODULE, `${sourceOrderNo} PDA 完成幂等重放`, completeReplay, '', OPTIONS)
context.assert(MODULE, `${sourceOrderNo} 完成重放保持同一已结算任务`, completeReplay,
id(completedReplay?.id) === taskId && Number(completedReplay?.version) === Number(completed?.version)
&& completedReplay?.taskStatus === 'COMPLETED' && completedReplay?.workflowStatus === 'COMPLETED',
`taskId=${id(completedReplay?.id) || 'missing'}, taskVersion=${completedReplay?.version ?? 'missing'}`, OPTIONS)
const treeResponse = await context.request('GET', treePath(rootOrderId, tenantId))
const tree = context.apiSuccess(MODULE, `回查 ${sourceOrderNo} 拆分后的根聚合`, treeResponse, '', OPTIONS)
const candidatesAfter = await context.request('GET', candidatesPath)