This commit is contained in:
2026-08-21 16:04:16 +08:00
parent dceec6cbed
commit 0e7df9052a
68 changed files with 3985 additions and 267 deletions
@@ -2,11 +2,13 @@ const MODULE = 'phase4-carrier-nodes'
const OPTIONS = { scenario: 'phase4-carrier-nodes-lifecycle', deploymentBlocker: true }
const BASE = '/api/platform/v1/carrier-schedule-queries'
export const QUERY_STATUSES = Object.freeze(['READY', 'MANUAL_REQUIRED', 'ARRIVED', 'PICKUP_READY', 'STOPPED'])
export const QUERY_STATUSES = Object.freeze(['READY', 'MANUAL_REQUIRED', 'STOPPED'])
export const STANDARD_NODES = Object.freeze([
'DEPARTED_WAREHOUSE', 'AT_ORIGIN_PORT', 'DEPARTED_ORIGIN_PORT',
'IN_TRANSIT', 'ARRIVED', 'PICKUP_READY',
'DEPARTED_WAREHOUSE', 'AT_ORIGIN_PORT', 'LOADED_ON_VESSEL', 'DEPARTED_ORIGIN_PORT',
'IN_TRANSIT', 'ARRIVED_DESTINATION_PORT', 'DISCHARGED_FROM_VESSEL', 'PICKUP_READY',
'PICKED_UP', 'EMPTY_RETURNED',
])
export const EXCEPTION_CODES = Object.freeze(['CUSTOMS_HOLD', 'TERMINAL_HOLD', 'DOCUMENT_HOLD', 'UNKNOWN_HOLD'])
function id(value) { return value == null ? '' : String(value) }
function number(value) { return Number.isFinite(Number(value)) }
@@ -18,21 +20,32 @@ function validTask(task) {
return Boolean(id(task?.id) && task?.queryNo && id(task?.customerId) && id(task?.oceanBookingId))
&& QUERY_STATUSES.includes(task?.status)
&& nullableId(task?.containerBookingId)
&& nullableText(task?.latestStandardNode)
&& (task?.latestStandardNode == null || STANDARD_NODES.includes(task.latestStandardNode))
&& ['latestNodeObservedAt', 'lastSuccessAt', 'sourceUpdatedAt', 'expiresAt', 'stoppedAt']
&& nullableText(task?.latestActualNode)
&& (task?.latestActualNode == null || STANDARD_NODES.includes(task.latestActualNode))
&& number(task?.version)
&& ['latestActualAt', 'lastSuccessAt', 'sourceUpdatedAt', 'expiresAt', 'stoppedAt']
.every((field) => nullableIso(task?.[field]))
&& nullableText(task?.stopReason)
}
function validNode(node) {
return Boolean(id(node?.id) && node?.providerCode && node?.providerNodeCode)
return Boolean(id(node?.id) && node?.providerCode && node?.providerEventId && node?.providerNodeCode)
&& STANDARD_NODES.includes(node?.standardNode)
&& number(node?.nodeSequence) && Number(node.nodeSequence) > 0
&& nullableIso(node?.observedAt) && node?.observedAt != null
&& nullableIso(node?.estimatedAt) && nullableIso(node?.actualAt)
&& nullableText(node?.locationCode) && nullableText(node?.locationName)
&& nullableText(node?.sourceReference) && nullableIso(node?.createdAt)
&& typeof node?.rawPayloadHash === 'string' && /^[0-9a-f]{64}$/.test(node.rawPayloadHash)
}
function validException(value) {
return Boolean(id(value?.id) && value?.providerCode && value?.providerEventId && value?.providerStatus)
&& EXCEPTION_CODES.includes(value?.exceptionCode) && typeof value?.active === 'boolean'
&& nullableIso(value?.observedAt) && value?.observedAt != null
&& nullableText(value?.locationCode) && nullableText(value?.locationName)
&& nullableText(value?.sourceReference) && nullableIso(value?.createdAt)
&& typeof value?.rawPayloadHash === 'string' && /^[0-9a-f]{64}$/.test(value.rawPayloadHash)
}
function validHistory(item) {
@@ -71,12 +84,33 @@ export async function run(context) {
const detailResponse = await context.request('GET', `${BASE}/${encodeURIComponent(queryId)}`)
const detail = context.apiSuccess(MODULE, '查询真实船节点任务详情', detailResponse, `queryId=${queryId}`, OPTIONS)
const nodes = Array.isArray(detail?.nodes) ? detail.nodes : []
const exceptions = Array.isArray(detail?.exceptions) ? detail.exceptions : []
const histories = Array.isArray(detail?.histories) ? detail.histories : []
context.assert(MODULE, '详情返回Task、标准节点与业务历史', detailResponse,
validTask(detail?.query?.query) && typeof detail?.query?.expired === 'boolean'
&& Array.isArray(detail?.nodes) && nodes.every(validNode)
&& Array.isArray(detail?.exceptions) && exceptions.every(validException)
&& Array.isArray(detail?.histories) && histories.every(validHistory),
`status=${detail?.query?.query?.status || 'missing'}, nodes=${nodes.length}, histories=${histories.length}`, OPTIONS)
`status=${detail?.query?.query?.status || 'missing'}, nodes=${nodes.length}, exceptions=${exceptions.length}, histories=${histories.length}`, OPTIONS)
const task = detail.query.query
const observedAt = task.latestActualAt || '2026-08-21T00:00:00'
const manualResponse = await context.request('POST', `${BASE}/${encodeURIComponent(queryId)}/manual-nodes`, {
expectedVersion: task.version,
standardNode: task.latestActualNode || 'DEPARTED_WAREHOUSE',
observedAt,
estimatedAt: observedAt,
actualAt: null,
locationCode: null,
locationName: null,
reason: '四期船节点人工预计事实验收',
}, context.token, { 'Idempotency-Key': `${context.marker || 'phase4'}-carrier-manual-${queryId}`.slice(0, 128) })
const manualDetail = context.apiSuccess(MODULE, '人工补录预计节点事实', manualResponse, `queryId=${queryId}`, OPTIONS)
context.assert(MODULE, '人工预计事实可见且不改变任务操作态', manualResponse,
QUERY_STATUSES.includes(manualDetail?.query?.query?.status)
&& manualDetail?.nodes?.some((item) => item?.providerCode === 'MANUAL'
&& item?.estimatedAt != null && item?.actualAt == null),
`status=${manualDetail?.query?.query?.status || 'missing'}, nodes=${manualDetail?.nodes?.length ?? 'missing'}`, OPTIONS)
const historyResponse = await context.request('GET', `${BASE}/${encodeURIComponent(queryId)}/history`)
const history = context.apiSuccess(MODULE, '查询真实船节点任务历史', historyResponse, `queryId=${queryId}`, OPTIONS)