This commit is contained in:
2026-09-15 14:30:47 +08:00
parent a474ca4d93
commit d1a00b8e01
18 changed files with 141 additions and 30 deletions
+2
View File
@@ -37,6 +37,8 @@ export async function run(context) {
## 可用上下文
混柜收货的每一步均区分同键同载荷重放、同键异载荷冲突和同货物新键再次扫码:普通海外仓重复箱返回冲突,FBA 相同累计数和代发同托新键返回零增量;随后回查完整会话,确保三票事实均不变。
`mixed-container-receiving.mjs` 通过总入口 `--modules mixed-container-receiving` 执行,依赖客户资质准备。新建一票双托一件代发、一票普通 FBA、一票普通海外仓订单,固定同一起运仓和渠道,经正式接口混装同柜、完成国内装柜和船节点、按柜生成海外计划。使用本轮 PDA 身份交替执行 FBA 累计收货、代发整托收货、普通海外仓逐箱收货;每步核对全部票进度、同键重放和未齐正常完成拒绝。终点为 `MIXED_CONTAINER_INBOUND_COMPLETED`,不创建后续出库或派送,不代表仓储转运单混装。报告复用 `createdResources.dropshipPalletMatrix`;失败保留阻断,成功数据保留,finally 停用本轮 PDA 操作员。真实运行须环境授权,离线合同 `mixed-container-receiving-contract.test.mjs` 不能代替真实 HTTP。
一件代发矩阵出库前运行 `dropship-outbound-edge-cases.mjs` 的候选查询探针:先模拟平台首次打开页面只传海外仓,再传客户筛选,要求本轮每票全部托盘、箱数、仓位及 SKU 事实一致,并核对仓库与指定客户隔离。缺托、错误客户/仓库、A0400 或筛选后仓位漂移均阻断;候选达到服务端上限导致本轮数据不可见也不能按空场景通过。该探针不证明跨客户混柜造数已覆盖。
@@ -334,16 +334,19 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
return data
}
let summary
let summaryResponse
for (let attempt = 0; attempt < 60; attempt++) {
const response = await context.requestOverseas('GET', `${base}/orders/container-summaries/${bookingId}`)
summaryResponse = response
summary = response?.body?.code === '200' ? response.body.data : null
if (summary?.orders?.length === group.length) break
if (attempt < 59) await new Promise((resolve) => setTimeout(resolve, 1000))
}
check(`${label}跨区事件逐票建单完整`, null,
check(`${label}跨区事件逐票建单完整`, summaryResponse,
summary?.orders?.length === group.length && group.every((record) =>
summary.orders.some((order) => order.inboundCode === record.source.inboundCode
&& String(order.customerId) === String(record.order.tenantId))))
&& String(order.customerId) === String(record.order.tenantId))),
`containerNo=${loading.containerNo}, expectedOrders=${group.length}, actualOrders=${summary?.orders?.length ?? 'unavailable'}`)
const preview = await call('入库计划预览', 'GET', `${base}/inbound-plans/container-preview?sourceContainerBookingId=${bookingId}`)
check(`${label}全柜入库计划就绪`, null, preview.ready === true && Number(preview.totalOrderCount) === group.length)
const body = { sourceContainerBookingId: bookingId, snapshotVersion: Number(preview.snapshotVersion),
@@ -37,7 +37,8 @@ test('schedule interleaves three modes and uses FBA cumulative count rather than
assert.throws(() => mixedReceivingSchedule(fixtures().slice(0, 2)))
})
function driver({ earlyComplete = false, wrongModeAccepted = false, drift = false } = {}) {
function driver({ earlyComplete = false, wrongModeAccepted = false, drift = false, repeatIncrement = false,
changedPayloadAccepted = false } = {}) {
const group = fixtures()
const expected = new Map(group.map(r => [r.source.inboundCode, 0]))
const schedule = mixedReceivingSchedule(group)
@@ -50,7 +51,14 @@ function driver({ earlyComplete = false, wrongModeAccepted = false, drift = fals
inbounds: [...expected].map(([inboundCode, receivedPackages]) => ({ inboundCode, totalPackages: 4,
receivedPackages, pendingPackages: 4 - receivedPackages })) })
return { group, marker: 'MIXED-TEST', check: (name, _response, passed) => assert.ok(passed, name),
request: async (_method, path) => ({ body: { code: (path.endsWith('/complete') ? earlyComplete : wrongModeAccepted) ? '200' : 'A0409' } }),
request: async (_method, path, body) => {
if (body.idempotencyKey.endsWith('-duplicate')) {
return { body: path.endsWith('/carton') ? { code: 'A0409' }
: { code: '200', data: { result: 'ALREADY_RECEIVED', acceptedPackages: repeatIncrement ? 1 : 0 } } }
}
if (seen.has(body.idempotencyKey)) return { body: { code: changedPayloadAccepted ? '200' : 'A0409' } }
return { body: { code: (path.endsWith('/complete') ? earlyComplete : wrongModeAccepted) ? '200' : 'A0409' } }
},
call: async (_name, method, path, body) => {
if (method === 'GET') return snapshot()
if (seen.has(body.idempotencyKey)) return structuredClone(seen.get(body.idempotencyKey))
@@ -82,3 +90,9 @@ test('mixed receiving fails if overseas or dropship can bypass pallet/carton rec
test('mixed receiving detects correct aggregate count allocated to the wrong inbound', async () => {
await assert.rejects(receiveMixedContainer(driver({ drift: true })))
})
test('mixed receiving rejects a new-key rescan that accepts another box', async () => {
await assert.rejects(receiveMixedContainer(driver({ repeatIncrement: true })))
})
test('mixed receiving rejects same-key different-payload success', async () => {
await assert.rejects(receiveMixedContainer(driver({ changedPayloadAccepted: true })))
})
@@ -58,6 +58,18 @@ export async function receiveMixedContainer({ group, call, check, request, marke
expected.find(row => row.inboundCode === step.record.source.inboundCode).received += step.count
const after = await call('混柜每步后逐票回查', 'GET', base)
check('混柜交替收货不串票不重计', null, receivingProgressMatches(after, expected))
const changedBody = step.path === 'pallet' ? { ...body, sourcePalletNo: `${body.sourcePalletNo}-CHANGED` }
: step.path === 'bulk' ? { ...body, packageCount: body.packageCount + 1 }
: { ...body, palletCode: `${body.palletCode}-CHANGED` }
const changed = await request('POST', path, changedBody)
check('混柜收货同键异载荷必须冲突', changed, changed.body?.code === 'A0409')
const duplicate = await request('POST', path, { ...body, idempotencyKey: `${body.idempotencyKey}-duplicate` })
check('混柜同货物新键重复操作不重复收货', duplicate, step.path === 'carton'
? duplicate.body?.code === 'A0409'
: duplicate.body?.code === '200' && duplicate.body?.data?.result === 'ALREADY_RECEIVED'
&& Number(duplicate.body?.data?.acceptedPackages) === 0)
const afterRetries = await call('混柜重复和冲突请求后回查全部票', 'GET', base)
check('混柜冲突及新键重复后全柜事实不变', null, JSON.stringify(afterRetries) === JSON.stringify(after))
}
const body = { allowDifference: false, idempotencyKey: `${marker}-mixed-complete` }
const completed = await call('混柜全部核平后正常完成入库', 'POST', `${base}/complete`, body)
@@ -64,8 +64,10 @@ test('current loading event binds each pallet carton group to an explicit source
const producer = fs.readFileSync(path.join(here, '../../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java'), 'utf8')
assert.match(model, /record SkuPackageSnapshot\(String sourceInboundDetailId, String skuCode/)
assert.match(model, /sourceInboundDetailId == null \|\| sourceInboundDetailId\.isBlank\(\)/)
assert.match(producer, /SkuPackageSnapshot\(detail\.detailId\(\)\.toString\(\), detail\.skuCode\(\)/)
assert.match(producer, /SkuPackageSnapshot\(detail\.getSourceInboundDetailId\(\)\.toString\(\), detail\.getSkuCode\(\)/)
assert.match(producer, /groupingBy\(PalletRow::getSourceInboundDetailId/)
assert.match(producer, /map\(PalletRow::getActualSkuQuantity\)/)
assert.match(producer, /cartonQuantities\(detailRows\.getValue\(\), marks\)/)
const measurement = fs.readFileSync(path.join(here, 'inbound-measurement.mjs'), 'utf8')
const loading = fs.readFileSync(path.join(here, 'phase3-loading.mjs'), 'utf8')
const finalize = fs.readFileSync(path.join(here, 'phase3-loading-finalize.mjs'), 'utf8')