优化
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { replayCompletedCommand } from './phase3-loading-finalize.mjs'
|
||||
|
||||
test('completed-command replay retries one connection failure and keeps its diagnostic', async () => {
|
||||
const failure = { http: 0, raw: 'fetch failed' }
|
||||
const success = { http: 200, body: { code: '200' } }
|
||||
const responses = [failure, success]
|
||||
const diagnostics = []
|
||||
let calls = 0
|
||||
assert.equal(await replayCompletedCommand(async () => { calls++; return responses.shift() }, r => diagnostics.push(r)), success)
|
||||
assert.equal(calls, 2)
|
||||
assert.deepEqual(diagnostics, [failure])
|
||||
})
|
||||
test('business failures are never retried; a second connection failure remains failure', async () => {
|
||||
for (const response of [{ http: 200, body: { code: 'A0409' } }, { http: 503 }, { http: 0 }]) {
|
||||
let calls = 0
|
||||
const result = await replayCompletedCommand(async () => { calls++; return response }, () => {})
|
||||
assert.equal(result, response)
|
||||
assert.equal(calls, response.http === 0 ? 2 : 1)
|
||||
}
|
||||
})
|
||||
@@ -3,6 +3,15 @@ const OPTIONS = { scenario: 'phase3-loading-hard-gate-finalize', deploymentBlock
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
|
||||
|
||||
export async function replayCompletedCommand(send, recordTransportFailure) {
|
||||
const first = await send()
|
||||
if (first.http !== 0) return first
|
||||
// Used only after authoritative COMPLETED + operation history have been read.
|
||||
// The closure retains the exact original body and idempotency key.
|
||||
recordTransportFailure(first)
|
||||
return send()
|
||||
}
|
||||
function evidenceSignature(data) {
|
||||
return JSON.stringify((data?.evidences || []).map((item) => ({
|
||||
id: id(item?.id), type: item?.evidenceType || null, fileId: id(item?.fileId),
|
||||
@@ -268,8 +277,12 @@ export async function run(context) {
|
||||
&& id(concurrentCompleteOperations[0]?.operatorId) && Boolean(concurrentCompleteOperations[0]?.createdAt)
|
||||
&& completion.task?.taskStatus === 'COMPLETED' && completion.task?.workflowStatus === 'COMPLETED'
|
||||
const completeResponse = completion.task?.taskStatus === 'COMPLETED'
|
||||
? await request(context, 'POST', `${taskPath}/complete`, completeBody,
|
||||
{ 'Idempotency-Key': completeKey })
|
||||
? await replayCompletedCommand(() => request(context, 'POST', `${taskPath}/complete`, completeBody,
|
||||
{ 'Idempotency-Key': completeKey }), failed => {
|
||||
context.createdResources.loadingCompleteReplayTransportFailures ??= []
|
||||
context.createdResources.loadingCompleteReplayTransportFailures.push({ taskId,
|
||||
operationId: completeBody.operationId, http: failed.http, error: failed.raw || 'transport failure' })
|
||||
})
|
||||
: acceptedCompleteResponse
|
||||
context.assert(MODULE, '两个不同幂等键并发 complete 仅一成功且只写一条完成审计',
|
||||
completeResponse, concurrentCompleteVerified,
|
||||
|
||||
@@ -254,6 +254,20 @@ function previewGroupsSorted(groups) {
|
||||
|
||||
async function convertContainerPlans(context, producer, details) {
|
||||
const bookingId = id(producer.containerBookingId)
|
||||
const arrival = await api(context, 'POST', '按柜转换前读取预计到仓时间',
|
||||
`${ORDERS}/container-arrival-times/query`, { sourceContainerBookingIds: [bookingId] })
|
||||
const currentArrival = Array.isArray(arrival.data)
|
||||
? arrival.data.find(row => id(row.sourceContainerBookingId) === bookingId) : null
|
||||
if (!success(arrival.response) || !currentArrival || !Number.isInteger(Number(currentArrival.version))) return null
|
||||
if (!nonBlank(currentArrival.estimatedWarehouseArrivalAt)) {
|
||||
const expected = await api(context, 'PUT', '按柜转换前补齐预计到仓时间',
|
||||
`${ORDERS}/container-summaries/${encodeURIComponent(bookingId)}/expected-arrival`, {
|
||||
estimatedWarehouseArrivalAt: new Date(Date.now() + 2 * 86400000).toISOString().slice(0, 19),
|
||||
expectedVersion: Number(currentArrival.version), remark: `${context.marker}-container-plan-arrival`,
|
||||
}, { 'Idempotency-Key': `${context.marker}-container-plan-arrival`.slice(0, 128) })
|
||||
if (!success(expected.response) || id(expected.data?.sourceContainerBookingId) !== bookingId
|
||||
|| !nonBlank(expected.data?.estimatedWarehouseArrivalAt)) return null
|
||||
}
|
||||
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 : []
|
||||
|
||||
@@ -254,17 +254,19 @@ export async function run(context) {
|
||||
const merchantToken = await loginMerchant(context, customerId)
|
||||
if (!merchantToken) return
|
||||
|
||||
const beforeEdit = await platform(context, 'GET', '编辑车辆前读取服务端目的仓线路', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`)
|
||||
if (!success(beforeEdit.response) || beforeEdit.data?.dispatchNo !== dispatchNo) return
|
||||
const updateBody = {
|
||||
vehiclePlate: `DEV-${context.marker.slice(-8)}`.slice(0, 64),
|
||||
driverName: `五期验收司机-${context.marker.slice(-6)}`,
|
||||
driverPhone: '19900000000', carrierName: '五期开发验收车队',
|
||||
route: `${context.marker}-PHASE5-ROUTE`, estimatedArrivalAt: new Date(Date.now() + 86_400_000).toISOString(),
|
||||
estimatedArrivalAt: new Date(Date.now() + 86_400_000).toISOString(),
|
||||
}
|
||||
const updated = await platform(context, 'PUT', '补齐本轮排车车辆司机与线路',
|
||||
`${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`, updateBody)
|
||||
context.assert(MODULE, '排车编辑字段完整回显', updated.response,
|
||||
updated.data?.dispatchNo === dispatchNo && updated.data?.vehiclePlate === updateBody.vehiclePlate
|
||||
&& updated.data?.driverName === updateBody.driverName && updated.data?.route === updateBody.route,
|
||||
&& updated.data?.driverName === updateBody.driverName && updated.data?.route === beforeEdit.data.route,
|
||||
trace(updated.response), OPTIONS)
|
||||
if (!success(updated.response)) return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user