feat(phase3): 收口父子单与全链路验收

This commit is contained in:
2026-08-10 12:23:43 +08:00
parent b294695169
commit 75d5fd4e26
55 changed files with 1110 additions and 115 deletions
@@ -6,6 +6,30 @@ const TRUCK = '/api/platform/v1/truck-appointments'
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
async function actionRequest(context, path, endpoint, version) {
try {
const response = await fetch(`${context.options.gateway}${path}/${endpoint}`, {
method: 'POST',
headers: {
Accept: 'application/json', Authorization: `Bearer ${context.token}`,
'Content-Type': 'application/json; charset=utf-8',
'Idempotency-Key': `${context.marker}-flow-${endpoint}`.slice(0, 128),
},
body: JSON.stringify({ expectedVersion: version, reason: context.marker }),
signal: AbortSignal.timeout(60_000),
})
const raw = await response.text(); let payload = null
try { payload = raw ? JSON.parse(raw) : null } catch { /* bounded raw evidence */ }
return { method: 'POST', path: `${path}/${endpoint}`, http: response.status, body: payload,
raw: payload === null ? raw.slice(0, 500) : null,
requestId: payload?.requestId || response.headers.get('x-request-id'),
traceId: payload?.traceId || response.headers.get('x-trace-id') }
} catch (error) {
return { method: 'POST', path: `${path}/${endpoint}`, http: 0, body: null,
raw: String(error), requestId: null, traceId: null }
}
}
export async function run(context) {
const ocean = context.createdResources?.oceanBooking
const containerFlow = context.createdResources?.phase3Loading
@@ -76,27 +100,42 @@ export async function run(context) {
appointmentId, oceanBookingId: ocean.id,
containerBookingId: linkContainer ? id(containerFlow.containerBookingId) : null,
loadingPlanId: linkContainer ? id(containerFlow.loadingPlanId) : null,
marker: context.marker, finalStatus: 'CANCELLED',
marker: context.marker, finalStatus: 'COMPLETED',
}
const path = `${TRUCK}/${appointmentId}`
let version = Number(appointment.version)
for (const [name, endpoint, status] of [
['提交跨链路预约', 'submit', 'PENDING_DISPATCH'],
['派发跨链路拖车', 'dispatch', 'DISPATCHED'],
['跨链路开始提货', 'pickup', 'PICKUP_IN_PROGRESS'],
['跨链路拖车到仓', 'arrive', 'ARRIVED_WAREHOUSE'],
['跨链路开始装货', 'load', 'LOADING'],
['跨链路拖车离仓', 'depart', 'DEPARTED'],
['完成跨链路拖车', 'complete', 'COMPLETED'],
]) {
const action = await actionRequest(context, path, endpoint, version)
const changed = context.apiSuccess(MODULE, name, action, '', OPTIONS)
context.assert(MODULE, `${name}状态正确`, action, changed?.status === status,
`status=${changed?.status || 'missing'}, version=${changed?.version ?? 'missing'}`, OPTIONS)
if (!changed) return
version = Number(changed.version)
}
const action = await context.request('POST', `${TRUCK}/${appointmentId}/cancel`, { expectedVersion: Number(appointment.version), reason: context.marker })
const cancelled = context.apiSuccess(MODULE, '取消跨服务链路拖车预约', action, '', OPTIONS)
context.assert(MODULE, '跨服务链路安全终态为 CANCELLED', action, cancelled?.status === 'CANCELLED', `status=${cancelled?.status || 'missing'}`, OPTIONS)
const detail = await context.request('GET', `${TRUCK}/${appointmentId}`)
const final = context.apiSuccess(MODULE, '回查跨服务链路安全终态', detail, '', OPTIONS)
context.assert(MODULE, '跨服务链路终态持久化且关联订舱未漂移', detail,
final?.status === 'CANCELLED' && id(final?.oceanBookingId) === id(ocean.id)
final?.status === 'COMPLETED' && id(final?.oceanBookingId) === id(ocean.id)
&& (!linkContainer || id(final?.containerBookingId) === id(containerFlow.containerBookingId)
&& id(final?.loadingPlanId) === id(containerFlow.loadingPlanId)),
`status=${final?.status || 'missing'}, oceanBookingId=${final?.oceanBookingId || 'missing'}, containerBookingId=${final?.containerBookingId || 'n/a'}, loadingPlanId=${final?.loadingPlanId || 'n/a'}`, OPTIONS)
const historyResponse = await context.request('GET', `${TRUCK}/${appointmentId}/history`)
const history = context.apiSuccess(MODULE, '查询跨服务链路拖车预约状态历史', historyResponse, '', OPTIONS)
const cancel = Array.isArray(history) ? history.find((item) => item?.action === 'CANCEL') : null
context.assert(MODULE, '跨服务链路历史包含归属、状态、操作者、时间和备注', historyResponse,
Boolean(cancel) && id(cancel?.appointmentId) === appointmentId
&& cancel?.fromStatus === 'DRAFT' && cancel?.toStatus === 'CANCELLED'
&& Boolean(cancel?.operatorId) && Boolean(cancel?.operatorName) && Boolean(cancel?.createdAt)
&& cancel?.reason === context.marker,
`history=${history?.length ?? 'n/a'}, cancelFrom=${cancel?.fromStatus || 'missing'}, cancelTo=${cancel?.toStatus || 'missing'}`,
const actions = Array.isArray(history) ? history.filter((item) => item?.action !== 'CREATE') : []
const expectedActions = ['SUBMIT', 'DISPATCH', 'PICKUP', 'ARRIVE', 'LOAD', 'DEPART', 'COMPLETE']
context.assert(MODULE, '跨服务链路历史包含完整状态链、操作者、时间和备注', historyResponse,
expectedActions.every((action) => actions.some((item) => item.action === action
&& id(item.appointmentId) === appointmentId && Boolean(item.operatorId)
&& Boolean(item.operatorName) && Boolean(item.createdAt) && item.reason === context.marker)),
`history=${history?.length ?? 'n/a'}, actions=${actions.map((item) => item.action).join(',')}`,
OPTIONS)
}