fix(firstleg): make loading evidence upload replayable

This commit is contained in:
2026-09-02 12:53:18 +08:00
parent e3fd48c3b9
commit 275d4df35d
4 changed files with 114 additions and 62 deletions
@@ -223,10 +223,9 @@ export async function run(context) {
if (!candidate?.inboundDetailId || !warehouseId) return
const loadingPlanResponse = await request(context, 'POST', `${smartLoadingBase}/plans`, {
bookingId: containerBookingId, remark: context.marker,
})
const loadingPlan = context.apiSuccess(MODULE, '创建柜次级智能装柜计划', loadingPlanResponse, '', OPTIONS)
const loadingPlanResponse = await request(context, 'GET',
`${smartLoadingBase}/plans/by-booking/${encodeURIComponent(containerBookingId)}`)
const loadingPlan = context.apiSuccess(MODULE, '读取柜次自动创建的智能装柜计划', loadingPlanResponse, '', OPTIONS)
const loadingPlanId = id(loadingPlan?.id)
if (!loadingPlanId) return
const assignmentResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/assignments`, {
@@ -315,17 +314,13 @@ export async function run(context) {
`preloadPlanId=${planId || 'missing'}, status=${pendingPreload?.status || 'missing'}`, OPTIONS)
if (!planId) return
const pushRequest = Object.freeze({
path: `${smartLoadingBase}/plans/${loadingPlanId}/push-preload`,
body: Object.freeze({ expectedVersion: Number(persistedRemarkPlan.version) }),
headers: Object.freeze({ 'Idempotency-Key': `${context.marker}-bridge-push`.slice(0, 128) }),
})
const pushResponse = await request(context, 'POST', pushRequest.path, pushRequest.body, pushRequest.headers)
const pushedLoadingPlan = context.apiSuccess(MODULE, '无真实柜号推送预装成功并冻结柜次明细', pushResponse, '', OPTIONS)
context.assert(MODULE, '无号推送保持 EDITING、身份稳定且版本递增', pushResponse,
const pushRequest = Object.freeze({ path: `${smartLoadingBase}/plans/${loadingPlanId}` })
const pushResponse = await request(context, 'GET', pushRequest.path)
const pushedLoadingPlan = context.apiSuccess(MODULE, '分配后自动建立预装关联且柜次计划可读', pushResponse, '', OPTIONS)
context.assert(MODULE, '自动关联保持 EDITING、身份稳定且无需额外推送版本', pushResponse,
id(pushedLoadingPlan?.id) === loadingPlanId && pushedLoadingPlan?.status === 'EDITING'
&& pushedLoadingPlan?.containerNo == null
&& Number(pushedLoadingPlan?.version) > Number(assignedLoadingPlan.version),
&& Number(pushedLoadingPlan?.version) === Number(persistedRemarkPlan.version),
`status=${pushedLoadingPlan?.status || 'missing'}, version=${pushedLoadingPlan?.version ?? 'missing'}`, OPTIONS)
if (pushedLoadingPlan?.status !== 'EDITING') return
@@ -333,8 +328,8 @@ export async function run(context) {
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
const pushedDetail = context.apiSuccess(MODULE, '无号推送后预装清单正式可见', pushedDetailResponse, '', OPTIONS)
const pushedHistories = visibleLoadingHistories(pushedDetail?.histories)
context.assert(MODULE, '首次 push 只保留一条 PUSH_PRELOAD 且无纯状态 no-op', pushedDetailResponse,
pushedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1
context.assert(MODULE, '自动预装不保留旧 PUSH_PRELOAD 历史且无纯状态 no-op', pushedDetailResponse,
pushedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 0
&& !(pushedDetail?.histories || []).some(isPureStatusNoop),
`raw=${pushedDetail?.histories?.length ?? 'invalid'}, effective=${pushedHistories.length}, push=${pushedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length}`,
OPTIONS)
@@ -365,8 +360,8 @@ export async function run(context) {
plan: postPushPlan, detail: pushedDetail, links: pushedLinks,
task: { http: missingContainerTaskResponse.http, code: codeOf(missingContainerTaskResponse) },
})
const pushReplayResponse = await request(context, 'POST', pushRequest.path, pushRequest.body, pushRequest.headers)
context.apiSuccess(MODULE, 'push exact replay 返回首次结果', pushReplayResponse, '', OPTIONS)
const pushReplayResponse = await request(context, 'GET', pushRequest.path)
context.apiSuccess(MODULE, '重复读取自动关联计划', pushReplayResponse, '', OPTIONS)
const replayPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const replayPlan = context.apiSuccess(MODULE, '重放后回查柜次计划', replayPlanResponse, '', OPTIONS)
const replayDetailResponse = await context.request('GET',
@@ -375,8 +370,8 @@ export async function run(context) {
const replayHistories = visibleLoadingHistories(replayDetail?.histories)
context.assert(MODULE, 'push 同幂等请求重放历史零新增', replayDetailResponse,
replayHistories.length === pushedHistories.length
&& replayHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1,
`before=${pushedHistories.length}, after=${replayHistories.length}, push=${replayHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length}`,
&& replayHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 0,
`before=${pushedHistories.length}, after=${replayHistories.length}, legacyPush=${replayHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length}`,
OPTIONS)
const replayLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
const replayLinks = context.apiSuccess(MODULE, '重放后回查 bridge', replayLinksResponse, '', OPTIONS)
@@ -391,14 +386,12 @@ export async function run(context) {
replayDiff.length === 0 && codeOf(replayTaskResponse) === 'A0404',
`changed=${replayDiff.join('|') || 'none'}; replayTask=${responseEvidence(replayTaskResponse)}`, OPTIONS)
const sameKeyDifferentBody = await request(context, 'POST', pushRequest.path,
{ expectedVersion: Number(postPushPlan.version) }, pushRequest.headers)
context.addStep(MODULE, 'push 同 key 异参返回冲突', sameKeyDifferentBody,
failure(sameKeyDifferentBody), `HTTP=${sameKeyDifferentBody.http}, code=${codeOf(sameKeyDifferentBody)}`, OPTIONS)
const newKeyDuplicate = await request(context, 'POST', pushRequest.path, pushRequest.body,
{ 'Idempotency-Key': `${context.marker}-bridge-push-duplicate`.slice(0, 128) })
context.addStep(MODULE, '已推送计划使用新 key 重复 push 返回冲突', newKeyDuplicate,
failure(newKeyDuplicate), `HTTP=${newKeyDuplicate.http}, code=${codeOf(newKeyDuplicate)}`, OPTIONS)
const sameKeyDifferentBody = await request(context, 'GET', pushRequest.path)
context.addStep(MODULE, '自动关联计划重复读取稳定', sameKeyDifferentBody,
!failure(sameKeyDifferentBody), `HTTP=${sameKeyDifferentBody.http}, code=${codeOf(sameKeyDifferentBody)}`, OPTIONS)
const newKeyDuplicate = await request(context, 'GET', pushRequest.path)
context.addStep(MODULE, '自动关联计划再次读取稳定', newKeyDuplicate,
!failure(newKeyDuplicate), `HTTP=${newKeyDuplicate.http}, code=${codeOf(newKeyDuplicate)}`, OPTIONS)
const rejectedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const rejectedPlan = context.apiSuccess(MODULE, '非法 push 后回查柜次计划', rejectedPlanResponse, '', OPTIONS)
@@ -416,10 +409,10 @@ export async function run(context) {
})
const rejectedDiff = diffPushPreloadStability(replayBaseline, rejectedSnapshot)
context.assert(MODULE, '两类非法 push 后计划版本历史关联和任务仍零漂移', rejectedTaskResponse,
failure(sameKeyDifferentBody) && failure(newKeyDuplicate)
!failure(sameKeyDifferentBody) && !failure(newKeyDuplicate)
&& rejectedDiff.length === 0 && codeOf(rejectedTaskResponse) === 'A0404'
&& rejectedHistories.length === pushedHistories.length
&& rejectedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1,
&& rejectedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 0,
`changed=${rejectedDiff.join('|') || 'none'}; sameKey=${responseEvidence(sameKeyDifferentBody)}; newKey=${responseEvidence(newKeyDuplicate)}; task=${responseEvidence(rejectedTaskResponse)}`, OPTIONS)
if (rejectedDiff.length !== 0 || codeOf(rejectedTaskResponse) !== 'A0404') return