import { overseasRequest, overseasUpload, overseasDownload } from './overseas.js' const DISPATCH_BASE = '/pda/v1/last-leg/dispatches' const OUTBOUND_BASE = '/pda/v1/last-leg/outbound-tasks' const CARGO_DISCARD_BASE = '/pda/v1/last-leg/cargo-discards' const encoded = (value) => encodeURIComponent(String(value || '').trim()) function asList(value) { if (Array.isArray(value)) return value if (Array.isArray(value?.records)) return value.records if (Array.isArray(value?.items)) return value.items return [] } function count(value) { const number = Number(value || 0) return Number.isFinite(number) ? Math.max(0, number) : 0 } export function requireDispatchVersion(value) { if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) return value if (typeof value === 'string' && /^\d+$/.test(value)) return value throw new Error('排车操作版本缺失或无效,请刷新任务') } export function dispatchVersionAtLeast(current, confirmed) { const latest = String(requireDispatchVersion(current)).replace(/^0+(?=\d)/, '') const original = String(requireDispatchVersion(confirmed)).replace(/^0+(?=\d)/, '') return latest.length > original.length || latest.length === original.length && latest >= original } function dispatchCount(value) { if (typeof value !== 'number' && (typeof value !== 'string' || !/^(?:0|[1-9]\d*)$/.test(value))) throw new Error('排车数量不是有效非负整数,请重新读取任务') const number = typeof value === 'number' ? value : Number(value) if (!Number.isSafeInteger(number) || number < 0) throw new Error('排车数量超出安全整数范围或无效,请重新读取任务') return number } export function normalizeDispatchTask(value) { if (!value || !value.dispatchNo || !Array.isArray(value.items)) throw new Error('排车任务返回结构不完整') return { ...value, taskNo: String(value.taskNo || ''), dispatchNo: String(value.dispatchNo), recordVersion: value.recordVersion, pickingReconciled: value.pickingReconciled === true, actions: Object.fromEntries(Object.entries(value.actions || {}).map(([key, gate]) => [key, { ...gate, allowed: gate?.allowed === true }])), items: value.items.map(item => ({ ...item, dispatchItemId: String(item.dispatchItemId || ''), sourceKey: String(item.sourceKey || ''), sourceBusinessType: item.sourceBusinessType == null ? null : String(item.sourceBusinessType), sourceBusinessNo: item.sourceBusinessNo == null ? null : String(item.sourceBusinessNo), palletNo: String(item.palletNo || ''), scheduledPackages: dispatchCount(item.scheduledPackages), pickedPackages: dispatchCount(item.pickedPackages), returnCartons: (item.returnCartons || []).map(carton => ({ ...carton, returnCartonId: String(carton.returnCartonId || ''), cartonMark: String(carton.cartonMark || ''), productQuantity: dispatchCount(carton.productQuantity) })), positions: (item.positions || []).map(position => ({ ...position, positionId: String(position.positionId || ''), balanceId: String(position.balanceId || ''), availablePackages: dispatchCount(position.availablePackages), allocatedPackages: dispatchCount(position.allocatedPackages), pickedPackages: dispatchCount(position.pickedPackages) })) })) } } export function normalizeOutboundTask(value) { const header = value?.task || value || {} return { ...header, taskNo: String(header?.taskNo || ''), outboundNo: String(header?.outboundNo || ''), outboundType: String(header?.outboundType || ''), status: String(header?.status || ''), items: asList(value?.items).map((item) => ({ ...item, outboundPalletItemId: String(item?.outboundPalletItemId || ''), palletNo: String(item?.palletNo || ''), plannedPackages: count(item?.packageCount), pickedPackages: count(item?.pickedPackageCount), positions: asList(item?.positions).map((position) => ({ ...position, inventoryPositionId: String(position?.inventoryPositionId || ''), palletNo: String(position?.palletNo || item?.palletNo || ''), cartonMark: String(position?.cartonMark || ''), skuCode: String(position?.skuCode || position?.sku || ''), inboundCode: String(position?.inboundCode || ''), availablePackages: count(position?.availablePackages), pickedPackages: count(position?.pickedPackages) })) })) } } export async function listDispatchTasks() { const result = await overseasRequest('GET', DISPATCH_BASE) if (!Array.isArray(result)) throw new Error('排车任务列表返回结构无效') return result.map(normalizeDispatchTask) } export async function getDispatchTask(dispatchNo) { return normalizeDispatchTask(await overseasRequest('GET', `${DISPATCH_BASE}/${encoded(dispatchNo)}`)) } export function getDispatchLoading(dispatchNo) { return overseasRequest('GET', `${DISPATCH_BASE}/${encoded(dispatchNo)}/loading`) } export function getDispatchCommand(dispatchNo, key) { return overseasRequest('GET', `${DISPATCH_BASE}/${encoded(dispatchNo)}/commands/${encoded(key)}`) } function dispatchWrite(path, request) { requireDispatchVersion(request?.recordVersion) return overseasRequest('POST', path, request) } // 库存命令与仓租预校验共用本次请求的防重标识。 function requireStockCommandIdentity(request) { if (!request?.idempotencyKey) throw new Error('出库请求标识缺失,请重新进入任务') } export async function pickDispatchItem(dispatchNo, dispatchItemId, request) { requireDispatchVersion(request?.recordVersion) requireStockCommandIdentity(request) await prepareStockRent({ mode: 'DISPATCH', businessNo: dispatchNo, itemId: dispatchItemId, positionId: request.positionId, packages: request.packages, cartonMark: request.cartonMark || null, idempotencyKey: request.idempotencyKey, productQuantity: request.productQuantity || 0 }) return dispatchWrite(`${DISPATCH_BASE}/${encoded(dispatchNo)}/items/${encoded(dispatchItemId)}/pick`, request) } export function completeDispatchLoading(dispatchNo, request) { return dispatchWrite(`${DISPATCH_BASE}/${encoded(dispatchNo)}/loading-complete`, request) } export function reviewDispatchLoading(dispatchNo, request) { return dispatchWrite(`${DISPATCH_BASE}/${encoded(dispatchNo)}/loading-review`, request) } export async function listOutboundTasks(status = '') { const suffix = status ? `?status=${encoded(status)}` : '' return asList(await overseasRequest('GET', `${OUTBOUND_BASE}${suffix}`)).map(normalizeOutboundTask) } export async function getOutboundTask(taskNo) { return normalizeOutboundTask(await overseasRequest('GET', `${OUTBOUND_BASE}/${encoded(taskNo)}`)) } export function resolveOutboundTask(businessNo) { return overseasRequest('GET', `${OUTBOUND_BASE}/resolve/${encoded(businessNo)}`) } export async function pickOutboundTask(taskNo, request) { requireStockCommandIdentity(request) return overseasRequest('POST', `${OUTBOUND_BASE}/${encoded(taskNo)}/pick`, request) } export async function pickOutboundPallet(taskNo, palletNo, request) { requireStockCommandIdentity(request) return overseasRequest('POST', `${OUTBOUND_BASE}/${encoded(taskNo)}/pallets/${encoded(palletNo)}/pick`, request) } export function outboundPickPayload(item, position, packages, scannedCode = '') { const skuScan = item.skuCode && scannedCode === item.skuCode return { outboundPalletItemId: item.outboundPalletItemId, inventoryPositionId: position.inventoryPositionId || null, packages: Number(packages), ...(item.returnSkuQuantity != null ? { productQuantity: Number(item.returnSkuQuantity) } : {}), cartonMarks: skuScan ? [] : position.cartonMark ? [position.cartonMark] : [], ...(skuScan ? { skuCode: item.skuCode } : {}) } } export function completeOutboundTask(taskNo, request) { return overseasRequest('POST', `${OUTBOUND_BASE}/${encoded(taskNo)}/complete`, request) } export function normalizeCargoDiscardTask(value) { const task = value || {} return { ...task, remark: String(task?.remark || ''), requestedPackages: task?.requestedPackages == null ? null : count(task.requestedPackages), remainingPackages: task?.remainingPackages == null ? null : count(task.remainingPackages), discardNo: String(task?.discardNo || ''), outboundNo: String(task?.outboundNo || ''), palletNo: String(task?.palletNo || ''), scanMode: task?.scanMode === 'CARTON' ? 'CARTON' : 'PALLET', cartonMarks: asList(task?.cartonMarks).map(String), status: String(task?.status || 'PENDING'), skus: asList(task?.skus).map((sku) => ({ ...sku, itemId: String(sku?.itemId || ''), skuCode: String(sku?.skuCode || ''), productName: String(sku?.productName || ''), quantityUnit: sku?.quantityUnit === '件' ? '件' : '箱', plannedPackages: count(sku?.plannedPackages), actualPackages: sku?.actualPackages == null ? null : count(sku.actualPackages), actualQuantityEditable: sku?.actualQuantityEditable === true, allowedCartonMarks: asList(sku?.allowedCartonMarks).map(String), status: String(sku?.status || 'PENDING') })) } } export async function listCargoDiscardTasks() { return asList(await overseasRequest('GET', CARGO_DISCARD_BASE)).map(normalizeCargoDiscardTask) } export async function resolveCargoDiscardPallet(palletNo, discardNo) { return normalizeCargoDiscardTask(await overseasRequest('POST', `${CARGO_DISCARD_BASE}/resolve`, { palletNo, ...(discardNo ? { discardNo } : {}) })) } export async function confirmCargoDiscard(discardNo, palletNo, lines, photoFileIds, scannedCartonMarks = [], confirmAllCartons = false, request = {}) { const body = { lines, photoFileIds, scannedCartonMarks, confirmAllCartons, idempotencyKey: request.idempotencyKey } requireStockCommandIdentity(request) // 出库前按同一份货物、照片和幂等键校验仓租,资金就绪后才提交库存扣减。 await prepareStockRent({ mode: 'DISCARD', businessNo: discardNo, palletNo, ...body }) return normalizeCargoDiscardTask(await overseasRequest('POST', `${CARGO_DISCARD_BASE}/${encoded(discardNo)}/pallets/${encoded(palletNo)}/confirm`, body)) } /** 本次点击内只查询资金回执,不缓存或自动重放失败的出库命令。 */ async function prepareStockRent(body) { const base = '/pda/v1/last-leg/storage-rent/preparations' let preparationId try { let preparation = await overseasRequest('POST', base, body) preparationId = preparation?.preparationId for (let attempt = 0; preparation?.ready !== true && preparation?.status === 'FREEZE_PENDING' && preparationId && attempt < 20; attempt++) { await new Promise(resolve => setTimeout(resolve, 500)) preparation = await overseasRequest('GET', `${base}/${encoded(preparationId)}`) } if (preparation?.ready !== true) { const message = preparation?.message ? `仓租校验未通过:${preparation.message}` : '仓租资金预校验尚未通过,请稍后重新提交' throw Object.assign(new Error(message), { code: 'A0409' }) } } catch (error) { // 尚未发送出库命令时释放已知的未使用预占,释放失败保留原错误诊断。 if (preparationId) { try { await overseasRequest('DELETE', `${base}/${encoded(preparationId)}`) } catch (releaseError) { console.error('[Overseas PDA stock] rent preparation release failed', { code: releaseError?.code, requestId: releaseError?.requestId, traceId: releaseError?.traceId }) } } throw error } } const discardPhotoPath = (discardNo, palletNo) => `${CARGO_DISCARD_BASE}/${encoded(discardNo)}/pallets/${encoded(palletNo)}/photos/files` export const listCargoDiscardPhotos = (discardNo, palletNo) => overseasRequest('GET', discardPhotoPath(discardNo, palletNo)) export const uploadCargoDiscardPhoto = (discardNo, palletNo, filePath) => overseasUpload(discardPhotoPath(discardNo, palletNo), filePath) export const downloadCargoDiscardPhoto = (discardNo, palletNo, fileId) => overseasDownload(`${discardPhotoPath(discardNo, palletNo)}/${encoded(fileId)}/download`, { expectedContentTypePrefix: 'image/' })