feat(firstleg): converge phase3 loading models and pda

This commit is contained in:
2026-08-09 11:21:38 +08:00
parent 9b88fb4b9e
commit ed5b9fe77f
46 changed files with 2028 additions and 96 deletions
@@ -83,6 +83,52 @@ export async function run(context) {
const base = `/api/platform/v1/inbounds/${inboundId}`
const suffix = context.marker.replace(/\D/g, '').slice(-10)
const warehouseId = id(initial?.summary?.warehouseId)
const smartLoadingBase = '/api/platform/v1/first-leg/smart-loading'
const pdaBase = '/api/platform/v1/first-leg/pda-loading'
const candidateResponse = await context.request('GET', `${smartLoadingBase}/candidates?warehouseId=${encodeURIComponent(warehouseId)}`)
const candidates = context.apiSuccess(MODULE, '查询本轮入库单柜次装柜候选', candidateResponse, '', OPTIONS)
const candidate = Array.isArray(candidates) ? candidates.find((item) => id(item.inboundId) === inboundId
&& Number(item.availablePackageCount) > 0) : null
context.assert(MODULE, '柜次装柜候选与入库单、客户和仓库一致', candidateResponse,
Boolean(candidate?.inboundDetailId) && id(candidate?.accountId) === tenantId
&& id(candidate?.warehouseId) === warehouseId,
`candidate=${Boolean(candidate)}, warehouseId=${warehouseId || 'missing'}`, OPTIONS)
if (!candidate?.inboundDetailId || !warehouseId) return
const containerNo = `P3B-${suffix}`
const containerResponse = await request(context, 'POST', '/api/platform/v1/containers', {
containerNo, cabinetNo: `P3BI-${suffix}`, warehouseId,
warehouseName: initial?.summary?.warehouseName || null, billOfLadingNo: `P3BL-${suffix}`,
vesselName: `P3-BRIDGE-${suffix}`, voyageNo: `P3V-${suffix}`, containerType: '40HQ',
shippingCompanyName: `P3-SHIPPING-${suffix}`, departurePortCode: 'CNSZX', departurePortName: '深圳',
destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
destinationWarehouseCode: candidate.destinationWarehouseCode || null,
destinationWarehouseName: candidate.destinationWarehouseName || null,
capacityCbm: 100, maxWeightKg: 100000, loadingAt: '2026-09-02T08:00:00',
estimatedSailingAt: '2026-09-03T08:00:00', estimatedPortArrivalAt: '2026-09-20T08:00:00',
estimatedWarehouseArrivalAt: '2026-09-21T08:00:00',
})
const container = context.apiSuccess(MODULE, '创建与入库预装串联的柜次', containerResponse, '', OPTIONS)
const containerBookingId = id(container?.id)
if (!containerBookingId) return
const loadingPlanResponse = await request(context, 'POST', `${smartLoadingBase}/plans`, {
bookingId: containerBookingId, remark: context.marker,
})
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`, {
inboundId, inboundDetailId: id(candidate.inboundDetailId), packageCount: 1,
expectedVersion: Number(loadingPlan.version), operationId: `${context.marker}-bridge-assign`.slice(0, 128),
})
const assignedLoadingPlan = context.apiSuccess(MODULE, '将本轮入库货物分配到柜次计划', assignmentResponse, '', OPTIONS)
context.assert(MODULE, '柜次计划明细和版本真实持久化', assignmentResponse,
assignedLoadingPlan?.items?.some?.((item) => id(item.inboundId) === inboundId && Number(item.packageCount) === 1)
&& Number(assignedLoadingPlan?.version) === 1,
`loadingPlanId=${loadingPlanId}, version=${assignedLoadingPlan?.version ?? 'missing'}`, OPTIONS)
const smart = await request(context, 'POST', `${base}/split/smart?tenantId=${encodeURIComponent(tenantId)}`, {
expectedVersion: 0,
items: [{ containerCode: `P3-${suffix}`, cargoReference: `REF-${suffix}`, packageCount: 1, volumeCbm: 0.01, remark: context.marker }],
@@ -104,11 +150,31 @@ export async function run(context) {
&& replay.body?.data?.preloadPlan?.items?.length === 1,
`HTTP=${replay.http}, code=${codeOf(replay)}, planId=${id(replay.body?.data?.preloadPlan?.id) || 'missing'}`, OPTIONS)
const bindResponse = await request(context, 'POST',
`${base}/preload/loading-plans/${loadingPlanId}?tenantId=${encodeURIComponent(tenantId)}`, {
expectedPreloadVersion: Number(smartData.preloadPlan.version),
expectedLoadingPlanVersion: Number(assignedLoadingPlan.version),
}, { 'Idempotency-Key': `${context.marker}-bridge-bind`.slice(0, 128) })
const link = context.apiSuccess(MODULE, '原子关联入库预装与柜次装柜计划', bindResponse, '', OPTIONS)
context.assert(MODULE, '桥接双版本递增且关联归属唯一', bindResponse,
id(link?.inboundId) === inboundId && id(link?.loadingPlanId) === loadingPlanId
&& id(link?.preloadPlanId) === planId && Number(link?.allocatedPackageCount) === 1
&& Number(link?.preloadVersion) === 1 && Number(link?.loadingPlanVersion) === 2,
`linkId=${id(link?.id) || 'missing'}, preloadVersion=${link?.preloadVersion ?? 'missing'}, loadingVersion=${link?.loadingPlanVersion ?? 'missing'}`, OPTIONS)
if (!link?.id) return
const pushResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/push-preload`,
{ expectedVersion: Number(link.loadingPlanVersion) },
{ 'Idempotency-Key': `${context.marker}-bridge-push`.slice(0, 128) })
const pushedLoadingPlan = context.apiSuccess(MODULE, '桥接后推送柜次计划并生成真实 PDA 任务', pushResponse, '', OPTIONS)
context.assert(MODULE, '关联柜次计划进入 LOADING', pushResponse,
pushedLoadingPlan?.status === 'LOADING', `status=${pushedLoadingPlan?.status || 'missing'}`, OPTIONS)
const confirming = await request(context, 'POST', `${base}/preload/confirming?tenantId=${encodeURIComponent(tenantId)}`,
{ expectedVersion: Number(smartData.preloadPlan.version) }, { 'Idempotency-Key': `${context.marker}-p3-confirming`.slice(0, 128) })
{ expectedVersion: Number(link.preloadVersion) }, { 'Idempotency-Key': `${context.marker}-p3-confirming`.slice(0, 128) })
const confirmingData = context.apiSuccess(MODULE, '提交预装清单确认', confirming, '', OPTIONS)
context.assert(MODULE, '预装清单进入 CONFIRMING 且版本递增', confirming,
confirmingData?.preloadPlan?.status === 'CONFIRMING' && Number(confirmingData?.preloadPlan?.version) === 1,
confirmingData?.preloadPlan?.status === 'CONFIRMING' && Number(confirmingData?.preloadPlan?.version) === 2,
`status=${confirmingData?.preloadPlan?.status || 'missing'}, version=${confirmingData?.preloadPlan?.version ?? 'missing'}`, OPTIONS)
if (confirmingData?.preloadPlan?.status !== 'CONFIRMING') return
@@ -117,7 +183,7 @@ export async function run(context) {
const confirmed = context.apiSuccess(MODULE, '确认预装并进入装柜中', confirm, '', OPTIONS)
context.assert(MODULE, '预装确认推进主状态 LOADING 和计划 CONFIRMED', confirm,
confirmed?.mainStatus === 'LOADING' && confirmed?.preloadPlan?.status === 'CONFIRMED'
&& Number(confirmed?.preloadPlan?.version) === 2,
&& Number(confirmed?.preloadPlan?.version) === 3,
`mainStatus=${confirmed?.mainStatus || 'missing'}, planStatus=${confirmed?.preloadPlan?.status || 'missing'}, version=${confirmed?.preloadPlan?.version ?? 'missing'}`, OPTIONS)
if (confirmed?.mainStatus !== 'LOADING') return
@@ -142,13 +208,58 @@ export async function run(context) {
context.addStep(MODULE, '未完成 PDA 拣货时拒绝完成装柜', blockedBeforePicking, failure(blockedBeforePicking),
`HTTP=${blockedBeforePicking.http}, code=${codeOf(blockedBeforePicking)}`, OPTIONS)
const canonicalTaskResponse = await context.request('GET',
`${pdaBase}/tasks?containerNo=${encodeURIComponent(containerNo)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const canonicalTask = context.apiSuccess(MODULE, '读取柜次级 PDA 唯一事实任务', canonicalTaskResponse, '', OPTIONS)
const canonicalTaskId = id(canonicalTask?.id)
const canonicalItem = canonicalTask?.items?.find?.((item) => id(item.inboundId) === inboundId)
context.assert(MODULE, '柜次 PDA 任务与桥接入库明细一致', canonicalTaskResponse,
Boolean(canonicalTaskId) && id(canonicalTask?.loadingPlanId) === loadingPlanId
&& canonicalTask?.workflowStatus === 'UNASSIGNED' && Boolean(canonicalItem?.id),
`taskId=${canonicalTaskId || 'missing'}, itemId=${id(canonicalItem?.id) || 'missing'}`, OPTIONS)
if (!canonicalTaskId || !canonicalItem?.id) return
const claim = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/claim`, {
expectedAssignmentVersion: 0, operationId: `${context.marker}-bridge-claim`.slice(0, 128),
})
const claimed = context.apiSuccess(MODULE, '领取柜次 PDA 任务', claim, '', OPTIONS)
context.assert(MODULE, '柜次 PDA 任务绑定当前作业员', claim,
claimed?.workflowStatus === 'IN_PROGRESS' && Boolean(claimed?.assignment?.assigneeId),
`workflow=${claimed?.workflowStatus || 'missing'}`, OPTIONS)
const canonicalPick = await request(context, 'POST',
`${pdaBase}/tasks/${canonicalTaskId}/items/${id(canonicalItem.id)}/pick`, {
warehouseId, pickedPackageCount: 1, locationIds: [], version: Number(canonicalItem.version),
operationId: `${context.marker}-bridge-pick`.slice(0, 128),
})
const pickedCanonical = context.apiSuccess(MODULE, '执行柜次 PDA 明细拣货', canonicalPick, '', OPTIONS)
context.assert(MODULE, '柜次 PDA 明细完成后等待复核', canonicalPick,
pickedCanonical?.workflowStatus === 'AWAITING_REVIEW' && pickedCanonical?.taskStatus === 'LOADING',
`workflow=${pickedCanonical?.workflowStatus || 'missing'}`, OPTIONS)
const canonicalReview = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/review`, {
expectedTaskVersion: Number(pickedCanonical?.version),
operationId: `${context.marker}-bridge-review`.slice(0, 128), remark: context.marker,
})
const reviewedCanonical = context.apiSuccess(MODULE, '复核柜次 PDA 并完成柜次计划', canonicalReview, '', OPTIONS)
context.assert(MODULE, '柜次 PDA 复核成为装柜完成真实门禁', canonicalReview,
reviewedCanonical?.taskStatus === 'COMPLETED' && reviewedCanonical?.workflowStatus === 'COMPLETED'
&& reviewedCanonical?.review?.status === 'APPROVED',
`taskStatus=${reviewedCanonical?.taskStatus || 'missing'}`, OPTIONS)
const forgedPickingStart = await request(context, 'POST', `${base}/loading/picking/start?tenantId=${encodeURIComponent(tenantId)}`,
{ expectedVersion: 0, expectedCount: 1, pickedCount: 0, remark: 'forged-client-count' },
{ 'Idempotency-Key': `${context.marker}-p3-picking-forged-start`.slice(0, 128) })
context.addStep(MODULE, '入库级 PDA 汇总拒绝客户端伪造数量', forgedPickingStart, failure(forgedPickingStart),
`HTTP=${forgedPickingStart.http}, code=${codeOf(forgedPickingStart)}`, OPTIONS)
const pickingStart = await request(context, 'POST', `${base}/loading/picking/start?tenantId=${encodeURIComponent(tenantId)}`,
{ expectedVersion: 0, expectedCount: 1, pickedCount: 0, remark: context.marker },
{ expectedVersion: 0, expectedCount: 1, pickedCount: 1, remark: context.marker },
{ 'Idempotency-Key': `${context.marker}-p3-picking-start`.slice(0, 128) })
const pickingStarted = context.apiSuccess(MODULE, '开始本轮 PDA 拣货任务', pickingStart, '', OPTIONS)
context.assert(MODULE, 'PDA 拣货任务处于 IN_PROGRESS 且计划数量正确', pickingStart,
pickingStarted?.status === 'IN_PROGRESS' && Number(pickingStarted?.version) === 0
&& Number(pickingStarted?.expectedCount) === 1 && Number(pickingStarted?.pickedCount) === 0,
&& Number(pickingStarted?.expectedCount) === 1 && Number(pickingStarted?.pickedCount) === 1,
`status=${pickingStarted?.status || 'missing'}, version=${pickingStarted?.version ?? 'missing'}`, OPTIONS)
const pickingMismatch = await request(context, 'POST', `${base}/loading/picking/complete?tenantId=${encodeURIComponent(tenantId)}`,
@@ -240,6 +351,7 @@ export async function run(context) {
`status=${finalData?.summary?.processStatus || 'missing'}, version=${finalData?.summary?.version ?? 'missing'}, histories=${finalData?.statusHistory?.length ?? 'n/a'}`, OPTIONS)
context.createdResources.phase3Loading = {
tenantId, inboundId, preloadPlanId: planId, evidenceIds: evidenceItems.map((item) => id(item.id)),
loadingPlanId, containerBookingId, containerNo, warehouseId, canonicalPdaTaskId: canonicalTaskId,
pickingTaskId: id(pickingCompleted?.id), marker: context.marker, finalStatus: 'LOADED',
}