优化
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 id(value) { return value == null ? '' : String(value) }
|
||||||
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
|
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) {
|
function evidenceSignature(data) {
|
||||||
return JSON.stringify((data?.evidences || []).map((item) => ({
|
return JSON.stringify((data?.evidences || []).map((item) => ({
|
||||||
id: id(item?.id), type: item?.evidenceType || null, fileId: id(item?.fileId),
|
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)
|
&& id(concurrentCompleteOperations[0]?.operatorId) && Boolean(concurrentCompleteOperations[0]?.createdAt)
|
||||||
&& completion.task?.taskStatus === 'COMPLETED' && completion.task?.workflowStatus === 'COMPLETED'
|
&& completion.task?.taskStatus === 'COMPLETED' && completion.task?.workflowStatus === 'COMPLETED'
|
||||||
const completeResponse = completion.task?.taskStatus === 'COMPLETED'
|
const completeResponse = completion.task?.taskStatus === 'COMPLETED'
|
||||||
? await request(context, 'POST', `${taskPath}/complete`, completeBody,
|
? await replayCompletedCommand(() => request(context, 'POST', `${taskPath}/complete`, completeBody,
|
||||||
{ 'Idempotency-Key': completeKey })
|
{ 'Idempotency-Key': completeKey }), failed => {
|
||||||
|
context.createdResources.loadingCompleteReplayTransportFailures ??= []
|
||||||
|
context.createdResources.loadingCompleteReplayTransportFailures.push({ taskId,
|
||||||
|
operationId: completeBody.operationId, http: failed.http, error: failed.raw || 'transport failure' })
|
||||||
|
})
|
||||||
: acceptedCompleteResponse
|
: acceptedCompleteResponse
|
||||||
context.assert(MODULE, '两个不同幂等键并发 complete 仅一成功且只写一条完成审计',
|
context.assert(MODULE, '两个不同幂等键并发 complete 仅一成功且只写一条完成审计',
|
||||||
completeResponse, concurrentCompleteVerified,
|
completeResponse, concurrentCompleteVerified,
|
||||||
|
|||||||
@@ -254,6 +254,20 @@ function previewGroupsSorted(groups) {
|
|||||||
|
|
||||||
async function convertContainerPlans(context, producer, details) {
|
async function convertContainerPlans(context, producer, details) {
|
||||||
const bookingId = id(producer.containerBookingId)
|
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 previewPath = query(`${PLANS}/container-preview`, { sourceContainerBookingId: bookingId })
|
||||||
const preview = await api(context, 'GET', '预览整柜待生成入仓计划清单', previewPath)
|
const preview = await api(context, 'GET', '预览整柜待生成入仓计划清单', previewPath)
|
||||||
const groups = Array.isArray(preview.data?.groups) ? preview.data.groups : []
|
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)
|
const merchantToken = await loginMerchant(context, customerId)
|
||||||
if (!merchantToken) return
|
if (!merchantToken) return
|
||||||
|
|
||||||
|
const beforeEdit = await platform(context, 'GET', '编辑车辆前读取服务端目的仓线路', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`)
|
||||||
|
if (!success(beforeEdit.response) || beforeEdit.data?.dispatchNo !== dispatchNo) return
|
||||||
const updateBody = {
|
const updateBody = {
|
||||||
vehiclePlate: `DEV-${context.marker.slice(-8)}`.slice(0, 64),
|
vehiclePlate: `DEV-${context.marker.slice(-8)}`.slice(0, 64),
|
||||||
driverName: `五期验收司机-${context.marker.slice(-6)}`,
|
driverName: `五期验收司机-${context.marker.slice(-6)}`,
|
||||||
driverPhone: '19900000000', carrierName: '五期开发验收车队',
|
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', '补齐本轮排车车辆司机与线路',
|
const updated = await platform(context, 'PUT', '补齐本轮排车车辆司机与线路',
|
||||||
`${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`, updateBody)
|
`${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`, updateBody)
|
||||||
context.assert(MODULE, '排车编辑字段完整回显', updated.response,
|
context.assert(MODULE, '排车编辑字段完整回显', updated.response,
|
||||||
updated.data?.dispatchNo === dispatchNo && updated.data?.vehiclePlate === updateBody.vehiclePlate
|
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)
|
trace(updated.response), OPTIONS)
|
||||||
if (!success(updated.response)) return
|
if (!success(updated.response)) return
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user