import { mkdir, open, readFile, rename, unlink } from 'node:fs/promises' import { dirname } from 'node:path' import { fingerprint } from './prod-release-order-stage.mjs' const check = (ok, message) => { if (!ok) throw new Error(message) } const longId = value => typeof value === 'string' && /^[1-9]\d*$/.test(value) const segment = value => typeof value === 'string' && /^[A-Za-z0-9_-]{1,80}$/.test(value) const number = value => Number.isSafeInteger(Number(value)) ? Number(value) : NaN export function validateDeliveryInput(input) { check(/^[A-Za-z0-9][A-Za-z0-9_-]{7,47}$/.test(input.batch || ''), '无效验收批次') for (const key of ['orderId', 'tailOrderId', 'warehouseId']) check(longId(input[key]), `无效 ${key}`) for (const key of ['orderNo', 'inboundCode', 'dispatchNo', 'outboundNo', 'deliveryNo']) check(segment(input[key]), `无效 ${key}`) check([1, 2].includes(input.packageCount), '只允许 1~2 箱单票验收') } // Separate journal: never overwrite the order-stage checkpoint or imply earlier stages were automated. export async function withDeliveryJournal(path, binding, action) { await mkdir(dirname(path), { recursive: true }) const lock = await open(`${path}.lock`, 'wx', 0o600) try { let state try { state = JSON.parse(await readFile(path, 'utf8')) } catch (error) { if (error.code !== 'ENOENT') throw new Error('签收检查点损坏,禁止重新上传') state = { version: 1, binding, steps: {} } } check(state.version === 1 && state.binding === binding && state.steps, '签收检查点与环境、账号或输入不符') const save = async patch => { const next = { ...state, ...patch } const file = await open(`${path}.next`, 'wx', 0o600) try { await file.writeFile(JSON.stringify(next, null, 2)); await file.sync() } finally { await file.close() } await rename(`${path}.next`, path) Object.assign(state, next) } return await action(state, save) } finally { await lock.close(); await unlink(`${path}.lock`) } } export function verifyOutbound(input, dispatch, detail, tail) { check(dispatch?.dispatchNo === input.dispatchNo && dispatch.overseasWarehouseId === input.warehouseId && dispatch.status === 'DEPARTED' && number(dispatch.packageCount) === input.packageCount && number(dispatch.shippedPackageCount) === input.packageCount && number(dispatch.pickedPackageCount) === input.packageCount && number(dispatch.remainingPackageCount) === 0, '排车身份、发车状态或数量不符') check(dispatch.items?.length > 0 && dispatch.items.every(item => item.inboundCode === input.inboundCode), '排车包含其他票') check(detail?.outbound?.outboundNo === input.outboundNo && detail.outbound.dispatchNo === input.dispatchNo && detail.outbound.overseasWarehouseId === input.warehouseId && detail.outbound.status === 'OUTBOUNDED' && number(detail.outbound.packageCount) === input.packageCount, '出库单身份或状态不符') check(detail.deliveries?.length === 1 && detail.deliveries[0].deliveryNo === input.deliveryNo, '不是本票唯一派送单') check(tail?.order?.id === input.tailOrderId && tail.order.sourceOrderNo === input.orderNo && tail.order.inboundCode === input.inboundCode && tail.order.overseasWarehouseId === input.warehouseId, '尾程与国内订单来源不符') const blockers = [] if (!detail.pallets?.length || detail.pallets.some(item => item.inboundCode !== input.inboundCode || number(item.pickedPackageCount) !== number(item.packageCount) || item.status === 'WAIT_PICK')) { blockers.push('OUTBOUND_PALLET_PICK_STATE_STALE') } if (String(tail.order.subOrderStatus).includes('OUTBOUND:WAIT_OUTBOUND')) blockers.push('TAIL_OUTBOUND_SUB_STATUS_STALE') return blockers } export async function runDeliveryStage({ input, customerId, state, save, request, upload, wait = ms => new Promise(r => setTimeout(r, ms)) }) { validateDeliveryInput(input) const root = '/platform/v1/last-leg' const deliveryPath = `${root}/delivery-orders/${input.deliveryNo}` const scoped = path => `${path}?customerId=${customerId}` const readDelivery = async () => { const detail = await request('overseas', 'GET', scoped(deliveryPath)) check(detail?.order?.customerId === customerId && detail.order.deliveryNo === input.deliveryNo && detail.order.dispatchNo === input.dispatchNo && detail.order.outboundNo === input.outboundNo && detail.order.remark === `RELEASE-ACCEPTANCE:${input.batch}`, '派送单身份或验收标记不符') return detail } const dispatch = await request('overseas', 'GET', `${root}/dispatches/${input.dispatchNo}`) const outbound = await request('overseas', 'GET', `${root}/outbound-orders/${input.outboundNo}`) const tail = await request('overseas', 'GET', scoped(`${root}/orders/${input.tailOrderId}`)) check(tail?.order?.customerId === customerId, '尾程客户不符') const blockers = verifyOutbound(input, dispatch, outbound, tail) let delivery = await readDelivery() if (delivery.order.status === 'WAIT_DELIVERY') { check(!state.startIntent, '配送中操作结果不确定,禁止重发;请核对服务端状态') await save({ startIntent: true }) await request('overseas', 'PUT', scoped(`${deliveryPath}/status`), { status: 'DELIVERING', expectedVersion: number(delivery.order.version), remark: `RELEASE-ACCEPTANCE:${input.batch}`, }) delivery = await readDelivery() } check(['DELIVERING', 'DELIVERED'].includes(delivery.order.status), '派送状态不能继续签收') const documentsPath = scoped(`${deliveryPath}/documents`) let documents = await request('overseas', 'GET', documentsPath) if (delivery.order.status !== 'DELIVERED') { if (!state.fileId) { check(!state.uploadIntent, '签收上传结果不确定;保留检查点,禁止重复上传') check(!documents.categories?.some(c => c.category === 'RECEIPT' && c.files?.length), '存在未登记的签收文件,需核对') await save({ uploadIntent: true }) const file = await upload(scoped(`${deliveryPath}/documents/RECEIPT/files`), `${input.batch}-receipt.txt`, `RELEASE ACCEPTANCE ONLY - NOT A REAL PROOF OF DELIVERY\n${input.batch}\n${input.deliveryNo}\n`) check(longId(file?.fileId), '上传未返回精确文件 ID') await save({ fileId: file.fileId }) } if (!state.saveBody) { check(Number.isSafeInteger(number(documents.version)) && number(documents.version) >= 0, '签收资料版本缺失') const existing = (documents.categories || []).filter(c => c.category !== 'RECEIPT' && c.files?.length) .map(c => ({ category: c.category, fileIds: c.files.map(f => f.fileId) })) await save({ saveBody: { expectedVersion: number(documents.version), idempotencyKey: `prod-receipt-${fingerprint({ batch: input.batch, delivery: input.deliveryNo }).slice(0, 40)}`, bolNo: documents.bolNo || null, documents: [...existing, { category: 'RECEIPT', fileIds: [state.fileId] }] } }) } await request('overseas', 'PUT', documentsPath, state.saveBody) delivery = await readDelivery() documents = await request('overseas', 'GET', documentsPath) } check(delivery.order.status === 'DELIVERED' && delivery.operations?.some(o => o.operationNode === 'RECEIPT_AUTO_COMPLETE'), '签收未自动完成派送') check(documents.categories?.some(c => c.category === 'RECEIPT' && c.status === 'UPLOADED' && c.files?.some(f => f.fileId === state.fileId)), '签收文件没有绑定本次上传结果') let inbound, order for (let attempt = 0; attempt < 30; attempt++) { const page = await request('cn', 'GET', `/platform/inbounds?tenantId=${customerId}&inboundCode=${input.inboundCode}&page=1&size=20`) inbound = page?.records?.find(row => row.inboundCode === input.inboundCode && row.orderId === input.orderId) order = await request('cn', 'GET', `/platform/v1/orders/${input.orderId}?tenantId=${customerId}`) if (inbound?.processStatus === 'DELIVERED' && order?.summary?.mainStatus === 'COMPLETED') break if (attempt < 29) await wait(1000) } const closure = inbound?.processStatus === 'DELIVERED' && order?.summary?.mainStatus === 'COMPLETED' if (!closure) blockers.push('CN_DELIVERY_EVENT_NOT_RECONCILED') const finalOutbound = await request('overseas', 'GET', `${root}/outbound-orders/${input.outboundNo}`) const finalTail = await request('overseas', 'GET', scoped(`${root}/orders/${input.tailOrderId}`)) const finalBlockers = verifyOutbound(input, dispatch, finalOutbound, finalTail) const result = { status: closure && !finalBlockers.length ? 'DELIVERY_STAGE_PASSED' : 'DELIVERY_STAGE_WITH_BLOCKERS', batch: input.batch, orderNo: input.orderNo, inboundCode: input.inboundCode, dispatchNo: input.dispatchNo, outboundNo: input.outboundNo, deliveryNo: input.deliveryNo, deliveryStatus: delivery.order.status, domesticInboundStatus: inbound?.processStatus || null, domesticOrderStatus: order?.summary?.mainStatus || null, deliveryClosureVerified: closure, fullFlowVerified: false, blockers: [...new Set([...finalBlockers, ...blockers.filter(b => b === 'CN_DELIVERY_EVENT_NOT_RECONCILED')])], packageCount: input.packageCount, receipt: 'SYNTHETIC_ACCEPTANCE_FILE', checkedAt: new Date().toISOString(), } await save({ result }) return result } export async function executeDelivery(settings, input, journalPath, fetcher = fetch) { validateDeliveryInput(input) const binding = fingerprint({ input, cn: settings.cn, overseas: settings.overseas, customer: settings.customer, admin: settings.credentials.ADMIN.loginId }) return withDeliveryJournal(journalPath, binding, async (state, save) => { const { client } = await import('./prod-release-acceptance.mjs') const login = await client(settings, fetcher).request('cn', 'POST', '/auth/login', null, settings.credentials.ADMIN) check(login?.accessToken && login?.overseasAccessToken, '缺少国内或海外令牌') const root = '/platform/v1/last-leg' const delivery = `${root}/delivery-orders/${input.deliveryNo}` const scoped = path => `${path}?customerId=${settings.customer.id}` const reads = new Set([`${root}/dispatches/${input.dispatchNo}`, `${root}/outbound-orders/${input.outboundNo}`, scoped(`${root}/orders/${input.tailOrderId}`), scoped(delivery), scoped(`${delivery}/documents`)]) const writes = new Set([scoped(`${delivery}/status`), scoped(`${delivery}/documents`)]) const cnReads = new Set([`/platform/inbounds?tenantId=${settings.customer.id}&inboundCode=${input.inboundCode}&page=1&size=20`, `/platform/v1/orders/${input.orderId}?tenantId=${settings.customer.id}`]) const http = client(settings, fetcher, (method, path, region) => region === 'cn' ? method === 'GET' && cnReads.has(path) : region === 'overseas' && (method === 'GET' && reads.has(path) || method === 'PUT' && writes.has(path))) return runDeliveryStage({ input, customerId: settings.customer.id, state, save, request: (region, method, path, body) => http.request(region, method, path, region === 'cn' ? login.accessToken : login.overseasAccessToken, body), upload: async (path, name, text) => { check(path === scoped(`${delivery}/documents/RECEIPT/files`), '上传路径不符') const form = new FormData(); form.append('file', new Blob([text], { type: 'text/plain' }), name) let response try { response = await fetcher(settings.overseas + path, { method: 'POST', redirect: 'error', signal: AbortSignal.timeout(20_000), headers: { Authorization: `Bearer ${login.overseasAccessToken}` }, body: form }) } catch { throw new Error('签收上传连接失败或超时;未自动重试') } const data = await response.json() const safe = value => /^[\w-]{1,100}$/.test(String(value)) ? value : 'unavailable' check(response.ok && data?.code === '200', `签收上传 HTTP=${response.status}; code=${safe(data?.code)}; requestId=${safe(data?.requestId)}; traceId=${safe(data?.traceId)}`) return data.data }, }) }) }