This commit is contained in:
2026-08-14 10:09:46 +08:00
parent 0bc752a2be
commit 66089f9b99
31 changed files with 719 additions and 141 deletions
@@ -116,25 +116,52 @@ function snapshotMatches(tree, original, loaded, remaining, activeSuffix) {
&& activeMatches && quantityConserved(tree)
}
function containerPayload(marker, sequence, warehouseId, candidate) {
function bookingPayload(marker, sequence, source) {
const suffix = `${marker}-${sequence}`.replace(/[^A-Za-z0-9]/g, '').slice(-18)
return {
containerNo: `SPLIT-${suffix}`.slice(0, 64), cabinetNo: `SI-${suffix}`.slice(0, 64),
warehouseId, warehouseName: null, billOfLadingNo: `BL-${suffix}`.slice(0, 64),
vesselName: `SPLIT-VESSEL-${suffix}`.slice(0, 128), voyageNo: `V-${suffix}`.slice(0, 64),
containerType: '40HQ', shippingCompanyId: null, shippingCompanyName: `SPLIT-SHIPPING-${suffix}`.slice(0, 128),
customerId: source.tenantId, customerName: source.customerName,
channelCode: source.channelCode, channelName: source.channelName,
warehouseId: source.warehouseId, warehouseName: source.warehouseName,
shippingCompanyId: null, shippingCompanyName: `SPLIT-SHIPPING-${suffix}`.slice(0, 128),
bookingAgentId: null, bookingAgentName: `SPLIT-AGENT-${suffix}`.slice(0, 128),
departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
destinationWarehouseCode: candidate.destinationWarehouseCode || null,
destinationWarehouseName: candidate.destinationWarehouseName || null,
capacityCbm: 100, maxWeightKg: 100000, terminalTruckingCompany: `SPLIT-TRUCK-${suffix}`.slice(0, 128),
loadingAt: '2026-09-10T08:00:00', estimatedSailingAt: '2026-09-11T08:00:00',
estimatedPortArrivalAt: '2026-09-28T08:00:00', estimatedWarehouseArrivalAt: '2026-09-29T08:00:00',
placeOfDelivery: 'LAX', serviceTerm: 'CY/CY', freightTerm: 'PREPAID', remark: marker,
parties: [{ partyType: 'SHIPPER', partyName: `SPLIT-SHIPPER-${suffix}` },
{ partyType: 'CONSIGNEE', partyName: `SPLIT-CONSIGNEE-${suffix}` },
{ partyType: 'NOTIFY_PARTY', partyName: `SPLIT-NOTIFY-${suffix}` }],
containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', capacityCbm: 100,
maxWeightKg: 100000, quantity: 1 }],
}
}
async function loadOneFragment(context, state) {
const { tenantId, rootOrderId, sourceOrderId, sourceOrderNo, warehouseId, sourcePackageCount, sequence } = state
const candidatesResponse = await context.request('GET', `${SMART}/candidates?warehouseId=${encodeURIComponent(warehouseId)}`)
const operationBase = `${context.marker}-chain-${sequence}`.slice(0, 100)
const bookingCreate = await request(context, 'POST', '/api/platform/v1/ocean-bookings', {
expectedVersion: 0, data: bookingPayload(context.marker, sequence, state),
}, { 'Idempotency-Key': `${operationBase}-booking-create` })
const bookingCreated = context.apiSuccess(MODULE, `为 ${sourceOrderNo} 创建渠道仓库一致的订舱`, bookingCreate, '', OPTIONS)
const oceanBookingId = id(bookingCreated?.oceanBooking?.id)
if (!oceanBookingId) return null
context.createdResources.phase3OrderSplitting.retainedAuditRecordIds.push(oceanBookingId)
const bookingSubmit = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${oceanBookingId}/submit`, {
expectedVersion: Number(bookingCreated?.oceanBooking?.version), reason: context.marker,
}, { 'Idempotency-Key': `${operationBase}-booking-submit` })
const bookingSubmitted = context.apiSuccess(MODULE, `提交 ${sourceOrderNo} 的订舱`, bookingSubmit, '', OPTIONS)
const bookingConfirm = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${oceanBookingId}/confirm`, {
expectedVersion: Number(bookingSubmitted?.oceanBooking?.version), reason: context.marker,
}, { 'Idempotency-Key': `${operationBase}-booking-confirm` })
const bookingConfirmed = context.apiSuccess(MODULE, `确认 ${sourceOrderNo} 的订舱并自动分配柜次`, bookingConfirm, '', OPTIONS)
const containerId = id(bookingConfirmed?.containerAllocations?.[0]?.containerBookingId)
context.assert(MODULE, `${sourceOrderNo} 订舱确认后取得稳定柜次`, bookingConfirm,
isPositiveId(containerId) && id(bookingConfirmed?.oceanBooking?.id) === oceanBookingId,
`oceanBookingId=${oceanBookingId}, containerBookingId=${containerId || 'missing'}`, OPTIONS)
if (!containerId) return null
context.createdResources.phase3OrderSplitting.retainedAuditRecordIds.push(containerId)
const candidatesPath = `${SMART}/candidates?bookingId=${encodeURIComponent(containerId)}&warehouseId=${encodeURIComponent(warehouseId)}`
const candidatesResponse = await context.request('GET', candidatesPath)
const candidates = context.apiSuccess(MODULE, `查询余量 ${sourceOrderNo} 的真实装柜候选`, candidatesResponse, '', OPTIONS)
const candidate = Array.isArray(candidates)
? candidates.find((row) => id(row.orderId) === sourceOrderId && id(row.accountId) === tenantId)
@@ -145,20 +172,12 @@ async function loadOneFragment(context, state) {
`orderId=${sourceOrderId}, available=${candidate?.availablePackageCount ?? 'missing'}`, OPTIONS)
if (!candidate?.inboundId || !candidate?.inboundDetailId) return null
const createContainer = await request(context, 'POST', CONTAINERS,
containerPayload(context.marker, sequence, warehouseId, candidate))
const container = context.apiSuccess(MODULE, `为 ${sourceOrderNo} 创建隔离柜次`, createContainer, '', OPTIONS)
const containerId = id(container?.id)
if (!containerId) return null
context.createdResources.phase3OrderSplitting.retainedAuditRecordIds.push(containerId)
const createPlan = await request(context, 'POST', `${SMART}/plans`, { bookingId: containerId, remark: context.marker })
const plan = context.apiSuccess(MODULE, `为 ${sourceOrderNo} 创建装柜计划`, createPlan, '', OPTIONS)
const planId = id(plan?.id)
if (!planId) return null
context.createdResources.phase3OrderSplitting.retainedAuditRecordIds.push(planId)
const operationBase = `${context.marker}-chain-${sequence}`.slice(0, 100)
const assignPayload = {
inboundId: id(candidate.inboundId), inboundDetailId: id(candidate.inboundDetailId), packageCount: 1,
expectedVersion: Number(plan.version), operationId: `${operationBase}-assign`,
@@ -180,16 +199,31 @@ async function loadOneFragment(context, state) {
const push = await request(context, 'POST', `${SMART}/plans/${planId}/push-preload`,
{ expectedVersion: Number(assigned.version) }, { 'Idempotency-Key': `${operationBase}-push` })
const pushed = context.apiSuccess(MODULE, `推送 ${sourceOrderNo} 的真实 PDA 任务`, push, '', OPTIONS)
if (pushed?.status !== 'LOADING') return null
const pushed = context.apiSuccess(MODULE, `推送 ${sourceOrderNo} 的预装计划`, push, '', OPTIONS)
context.assert(MODULE, `${sourceOrderNo} 未补真实柜号时计划保持 EDITING`, push,
pushed?.status === 'EDITING', `status=${pushed?.status || 'missing'}`, OPTIONS)
if (pushed?.status !== 'EDITING') return null
const containerDetailResponse = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}`)
const containerDetail = context.apiSuccess(MODULE, `回查 ${sourceOrderNo} 的自动柜次`, containerDetailResponse, '', OPTIONS)
const realContainerNo = `SPLIT-${operationBase.replace(/[^A-Za-z0-9]/g, '').slice(-18)}`.slice(0, 64)
const transportResponse = await request(context, 'PATCH', `${CONTAINERS}/${encodeURIComponent(containerId)}/transport`, {
containerNo: realContainerNo, voyageNo: `V-${sequence}`,
version: Number(containerDetail?.version), reason: context.marker,
})
const transported = context.apiSuccess(MODULE, `为 ${sourceOrderNo} 补充真实柜号并开放 PDA`, transportResponse, '', OPTIONS)
context.assert(MODULE, `${sourceOrderNo} 真实柜号已绑定到稳定柜次`, transportResponse,
transported?.containerNo === realContainerNo,
`containerBookingId=${containerId}, containerNo=${transported?.containerNo || 'missing'}`, OPTIONS)
const taskResponse = await context.request('GET',
`${PDA}/tasks?containerNo=${encodeURIComponent(container.containerNo)}&warehouseId=${encodeURIComponent(warehouseId)}`)
`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const task = context.apiSuccess(MODULE, `读取 ${sourceOrderNo} 的 PDA 任务`, taskResponse, '', OPTIONS)
const taskId = id(task?.id)
const pickItem = task?.items?.find?.((row) => id(row.loadingPlanItemId) === id(planItem.id))
context.assert(MODULE, `余量 ${sourceOrderNo} 不继承已拣数量`, taskResponse,
Boolean(taskId) && Boolean(pickItem?.id) && Number(pickItem?.plannedPackageCount) === 1
Boolean(taskId) && id(task?.loadingPlanId) === planId && id(task?.containerBookingId) === containerId
&& task?.currentContainerNo === realContainerNo && Boolean(pickItem?.id) && Number(pickItem?.plannedPackageCount) === 1
&& Number(pickItem?.pickedPackageCount) === 0,
`taskId=${taskId || 'missing'}, picked=${pickItem?.pickedPackageCount ?? 'missing'}, planned=${pickItem?.plannedPackageCount ?? 'missing'}`, OPTIONS)
if (!taskId || !pickItem?.id) return null
@@ -237,13 +271,13 @@ async function loadOneFragment(context, state) {
const treeResponse = await context.request('GET', treePath(rootOrderId, tenantId))
const tree = context.apiSuccess(MODULE, `回查 ${sourceOrderNo} 拆分后的根聚合`, treeResponse, '', OPTIONS)
const candidatesAfter = await context.request('GET', `${SMART}/candidates?warehouseId=${encodeURIComponent(warehouseId)}`)
const candidatesAfter = await context.request('GET', candidatesPath)
const candidateRowsAfter = context.apiSuccess(MODULE, `回查 ${sourceOrderNo} 不可重复消费`, candidatesAfter, '', OPTIONS)
context.assert(MODULE, `来源 ${sourceOrderNo} 同时最多成功拆分一次`, candidatesAfter,
Array.isArray(candidateRowsAfter) && !candidateRowsAfter.some((row) => id(row.orderId) === sourceOrderId),
`sourceOrderId=${sourceOrderId}, stillCandidate=${Array.isArray(candidateRowsAfter) && candidateRowsAfter.some((row) => id(row.orderId) === sourceOrderId)}`, OPTIONS)
return { tree, treeResponse, containerId, planId, taskId, sourceInboundId: id(candidate.inboundId) }
return { tree, treeResponse, oceanBookingId, containerId, planId, taskId, sourceInboundId: id(candidate.inboundId) }
}
export async function run(context) {
@@ -263,10 +297,15 @@ export async function run(context) {
const inbound = context.apiSuccess(MODULE, '读取本轮来源入库单', inboundResponse,
`tenantId=${tenantId}, inboundId=${inboundId}`, OPTIONS)
const orderId = id(inbound?.summary?.orderId)
const channelCode = id(inbound?.summary?.channelCode || fixture?.channelCode)
const channelName = inbound?.summary?.channelName || fixture?.channelName || channelCode
const warehouseName = inbound?.summary?.warehouseName || fixture?.warehouseName
const customerName = inbound?.summary?.customerName || `验收客户-${tenantId}`
context.assert(MODULE, '来源入库单归属本轮客户与根订单', inboundResponse,
isPositiveId(orderId) && id(inbound?.summary?.tenantId) === tenantId,
`orderId=${orderId || 'missing'}, tenantId=${id(inbound?.summary?.tenantId) || 'missing'}`, OPTIONS)
if (!orderId) return
isPositiveId(orderId) && id(inbound?.summary?.tenantId) === tenantId
&& Boolean(channelCode && channelName && warehouseName),
`orderId=${orderId || 'missing'}, tenantId=${id(inbound?.summary?.tenantId) || 'missing'}, channelCode=${channelCode || 'missing'}, warehouseName=${warehouseName || 'missing'}`, OPTIONS)
if (!orderId || !channelCode || !channelName || !warehouseName) return
const firstTreeResponse = await context.request('GET', treePath(orderId, tenantId))
const firstTree = context.apiSuccess(MODULE, '查询首次拆分后的根履约树', firstTreeResponse,
@@ -340,7 +379,8 @@ export async function run(context) {
const result = await loadOneFragment(context, {
tenantId, rootOrderId: orderId, sourceOrderId: id(source.orderId), sourceOrderNo: source.orderNo,
warehouseId, sourcePackageCount: step.sourcePackageCount, sequence: step.sourceSuffix,
customerName, channelCode, channelName, warehouseId, warehouseName,
sourcePackageCount: step.sourcePackageCount, sequence: step.sourceSuffix,
})
if (!result?.tree) return
tree = result.tree