2026-10-20 21:47:51 +08:00
import { mkdir , readFile , writeFile } from 'node:fs/promises'
import { resolve , dirname } from 'node:path'
import { randomUUID , createHash } from 'node:crypto'
// 仅针对显式指定的本机统一网关;业务事实全部使用正式 HTTP 创建。
const gateway = String ( process . env . ADMIN_TEST_GATEWAY || '' ). replace ( /\/$/ , '' )
if ( ! gateway || ! [ 'localhost' , '127.0.0.1' ]. includes ( new URL ( gateway ). hostname )) throw new Error ( '必须显式指定本机 Gateway' )
if ( ! process . env . ADMIN_TEST_USERNAME || ! process . env . ADMIN_TEST_PASSWORD || ! process . env . LOCAL_TRANSFER_PDA_PASSWORD ) throw new Error ( '缺少显式验收凭据' )
const output = resolve ( process . env . TRANSPORT_ACCEPTANCE_OUTPUT || 'reports/local-dispatch-transport.json' )
const report = process . env . TRANSPORT_ACCEPTANCE_RESUME ? JSON . parse ( await readFile ( output , 'utf8' )) : { marker : `DT- ${ new Date (). toISOString (). replace ( /\D/g , '' ). slice ( 0 , 14 ) } ` , evidence : 'SYNTHETIC_TEST_PNG_NOT_REAL_PHYSICAL_TRANSPORT' , checks : [], documents : {}, requests : {}, intents : [] }
if ( report . passed ) throw new Error ( '本批次已通过,请直接查看报告' )
const marker = report . marker , base = '/api/platform/v1/last-leg/dispatches/transport-tasks' , transfers = '/api/platform/v1/last-leg/warehouse-transfers' , trucks = '/api/platform/v1/last-leg/us-truck-orders'
const png = Buffer . from ( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=' , 'base64' )
let admin , pda
async function save () { await mkdir ( dirname ( output ), { recursive : true }); await writeFile ( output , JSON . stringify ( report , null , 2 )) }
function check ( value , label ) { report . checks . push ({ label , passed : Boolean ( value ) }); if ( ! value ) throw new Error ( label ) }
function image ( name ) { const body = new FormData (); body . append ( 'file' , new Blob ([ png ], { type : 'image/png' }), ` ${ marker } - ${ name } -TEST.png` ); return body }
async function api ( token , method , path , body , expected = '200' ) {
const form = body instanceof FormData
if ( method !== 'GET' && ! path . startsWith ( '/api/auth/' )) { report . intents . push ({ method , path , idempotencyKey : body ? . idempotencyKey , hash : form ? undefined : createHash ( 'sha256' ). update ( JSON . stringify ( body ?? null )). digest ( 'hex' ) }); await save () }
const response = await fetch ( ` ${ gateway }${ path } ` , { method , signal : AbortSignal . timeout ( 45000 ), headers : { ...( token ? { Authorization : `Bearer ${ token } ` } : {}), ...( body !== undefined && ! form ? { 'Content-Type' : 'application/json' } : {}) }, body : body === undefined ? undefined : form ? body : JSON . stringify ( body ) })
const value = await response . json (). catch (() => null ), code = String ( value ? . code )
const passed = response . status === 200 && code === expected
report . checks . push ({ method , path , code , http : response . status , expected , passed , message : code === '200' ? undefined : value ? . message , requestId : response . headers . get ( 'x-request-id' ) || value ? . requestId , traceId : response . headers . get ( 'x-trace-id' ) || value ? . traceId }); await save ()
if ( ! passed ) throw new Error ( ` ${ method } ${ path } : ${ response . status } / ${ code } ${ value ? . message || '' } ` )
return expected === '200' ? value . data : value
}
const call = ( method , path , body , expected ) => api ( admin , method , path , body , expected )
const request = ( key , body ) => report . requests [ key ] ||= { ... body , idempotencyKey : randomUUID () }
const read = number => call ( 'GET' , ` ${ base } / ${ number } ` )
const command = detail => ({ recordVersion : detail . execution . trip . recordVersion , idempotencyKey : randomUUID (), reason : ` ${ marker } 本机运输流程验收` })
async function act ( detail , suffix , extra = {}, expected ) { return call ( 'POST' , ` ${ base } / ${ detail . dispatchNo } / ${ suffix } ` , { ... command ( detail ), ... extra }, expected ) }
async function transfer ( customer , warehouse , key ) {
if ( report . documents [ key ]) return report . documents [ key ]
const body = request ( key , { operatingWarehouseId : warehouse . id , destinationType : 'PRIVATE' , privateDestinationAddress : '100 TEST Distribution Road, Ontario, CA' , privateDestinationPostalCode : '91761' , privateDestinationContactName : '本机验收收货员' , privateDestinationContactPhone : '0000000000' , deliveryAddress : `1580 TEST Pickup Ave, Ontario, CA ( ${ marker } - ${ key } )` , pickupContactName : '本机验收取货员' , pickupContactPhone : '0000000000' , packageCount : 3 , volumeCbm : 0 , deliveryMethod : 'WAREHOUSE_PICKUP' , estimatedArrivalAt : new Date ( Date . now () + 86400000 ). toISOString (). slice ( 0 , 19 ), estimatedShipAt : new Date ( Date . now () + 172800000 ). toISOString (). slice ( 0 , 19 ), remark : marker , needRelabel : false , items : [{ skuCode : ` ${ marker } - ${ key } ` , productNameZh : '本机验收收纳盒' , quantity : 24 , packageCount : 3 , shippingMark : ` ${ marker } - ${ key } ` }] })
await save ()
let detail = await call ( 'POST' , ` ${ transfers } ?customerId= ${ customer . id } ` , body )
detail = await call ( 'POST' , ` ${ transfers } / ${ detail . transferNo } /review?customerId= ${ customer . id } ` , { decision : 'APPROVE' , remark : marker })
report . documents [ key ] = { customerId : customer . id , transferNo : detail . transferNo , packageCount : detail . packageCount , cartonMarks : detail . cartons . map ( row => row . cartonMark ) }; await save (); return report . documents [ key ]
}
async function pickup ( sources , key ) {
if ( report . documents [ key ]) return read ( report . documents [ key ]. dispatchNo )
const body = request ( key , { sources : sources . map (({ customerId , transferNo })=>({ customerId , transferNo })), plannedDepartureAt : new Date ( Date . now () - 300000 ). toISOString (), expectedDriverEndAt : new Date ( Date . now () + 86400000 ). toISOString (), remark : marker })
await save (); const detail = await call ( 'POST' , ` ${ base } /pickup` , body )
report . documents [ key ] = { dispatchNo : detail . dispatchNo , sources : body . sources }; await save ()
const replay = await call ( 'POST' , ` ${ base } /pickup` , body ); check ( replay . dispatchNo === detail . dispatchNo , '到仓取货重放返回同一排车号' ); return detail
}
async function resources ( warehouse ) {
if ( report . resources ) return report . resources
const driverBase = '/api/platform/v1/last-leg/fleet/drivers'
let driver = await call ( 'POST' , driverBase , { name : ` ${ marker } 专用司机` , phone : '0000000000' , ownership : 'OWNED' , region : 'OVERSEAS' , licenseNumber : marker , licenseType : 'Class A' , licenseZone : 'UTC' , licenseExpiresOn : new Date ( Date . now () + 365 * 86400000 ). toISOString (). slice ( 0 , 10 ), remark : ` ${ marker } TEST licence image` , profile : { warehouseId : warehouse . id , vehicleModels : '26ft Box Truck' , languagesAndArea : 'Ontario TEST' } })
driver = await call ( 'POST' , ` ${ driverBase } / ${ driver . id } /license?recordVersion= ${ driver . recordVersion } ` , image ( 'licence' ))
driver = await call ( 'POST' , ` ${ driverBase } / ${ driver . id } /review` , { recordVersion : driver . recordVersion , decision : 'APPROVED' , reason : '本机模拟资质审核' })
const vehicle = await call ( 'POST' , '/api/platform/v1/last-leg/master-data/vehicles' , { plateNumber : `TEST-DT- ${ marker . slice ( - 10 ) } ` , model : '26ft Box Truck' , fuelConsumptionPer100Km : 22 , mileageKm : 18000 , driverOrDispatchCompany : marker , phone : '0000000000' , region : 'OVERSEAS' , ownership : 'OWNED' , status : 'IN_SERVICE' , remark : marker , maxPalletSpaces : 12 , maxLoadKg : 8000 , maxVolumeCbm : 45 })
report . resources = { vehicleId : vehicle . id , driverId : driver . id }; await save (); return report . resources
}
async function assign ( detail , resource ) { if ( detail . execution . trip . vehicleId ) return detail ; return call ( 'POST' , ` ${ base } / ${ detail . dispatchNo } /assign` , { recordVersion : detail . execution . trip . recordVersion , ... resource , reason : marker }) }
async function release ( detail ) {
for ( const kind of [ 'VEHICLE' , 'DRIVER' ]) {
detail = await read ( detail . dispatchNo )
if ( detail . execution . trip [ kind === 'VEHICLE' ? 'vehicleRelease' : 'driverRelease' ]) continue
await act ( detail , 'release' ,{ resourceKind : kind , actualEndAt : new Date (). toISOString (), endLocation : 'Ontario TEST warehouse' , vehicleStatus : kind === 'VEHICLE' ? 'IN_SERVICE' : null })
}
return read ( detail . dispatchNo )
}
async function truck ( customer , key ) {
if ( report . documents [ key ]) return call ( 'GET' , ` ${ trucks } / ${ report . documents [ key ]. orderNo } ?customerId= ${ customer . id } ` )
const body = request ( key ,{ vehicleType : 'PALLET_15' , origin : 'TEST取货地' , originAddress : '100 TEST Origin Road, Ontario, CA' , destination : 'TEST送货地' , destinationAddress : '200 TEST Destination Road, Ontario, CA' , packageCount : 4 , totalWeightKg : 40 , totalVolumeCbm : 1 , budgetAmount : 100 , currency : 'USD' , originContactName : '本机验收员' , originContactPhone : '0000000000' , destinationContactName : '本机验收员' , destinationContactPhone : '0000000000' , pickupAt : new Date (). toISOString (). slice ( 0 , 19 ), deliveryAt : new Date ( Date . now () + 86400000 ). toISOString (). slice ( 0 , 19 ), remark : marker })
await save (); let detail = await call ( 'POST' , ` ${ trucks } ?customerId= ${ customer . id } ` , body )
report . documents [ key ] = { customerId : customer . id , orderNo : detail . summary . orderNo }; await save (); return detail
}
async function truckReady ( detail ) {
const path = ` ${ trucks } / ${ detail . summary . orderNo } ` , query = `?customerId= ${ detail . summary . customerId } `
const step = async ( suffix , body = {})=> call ( 'POST' , ` ${ path } / ${ suffix }${ query } ` ,{ version : detail . summary . version , idempotencyKey : randomUUID (), remark : marker ,... body })
if ( detail . summary . status === 'PENDING_REVIEW' ) detail = await step ( 'review' ,{ decision : 'APPROVE' })
if ( detail . summary . status === 'WAITING_INBOUND' ) detail = await step ( 'receive-pallets' ,{ actualPalletCount : 1 , packageCount : 4 , palletPackageCounts : [ 4 ]})
if ( detail . summary . status === 'WAITING_CONSOLIDATION' ) detail = await step ( 'allocate-pallets' ,{ palletIds : detail . pallets . map ( row => row . id )})
if ( detail . summary . status === 'ALLOCATED' ) detail = await step ( 'prepare-dispatch' )
check ( detail . summary . status === 'PENDING_DISPATCH' , '本土卡车原单审核、实收和分配后进入待派车' ); return detail
}
async function truckPlan ( source , warehouse , key ) {
if ( report . documents [ key ]) return read ( report . documents [ key ]. dispatchNo )
const body = request ( key ,{ customerId : source . summary . customerId , orderNo : source . summary . orderNo , warehouseId : warehouse . id , plannedDepartureAt : new Date ( Date . now () - 300000 ). toISOString (), expectedDriverEndAt : new Date ( Date . now () + 86400000 ). toISOString ()}); await save ()
const detail = await call ( 'POST' , ` ${ base } /truck` , body ); report . documents [ key ] = { dispatchNo : detail . dispatchNo , orderNo : body . orderNo }; await save ()
check (( await call ( 'POST' , ` ${ base } /truck` , body )). dispatchNo === detail . dispatchNo , '本土卡车安排幂等重放' ); return detail
}
async function measure ( source ) {
const path = `/api/pda/v1/last-leg/warehouse-transfers/ ${ source . transferNo } `
let detail = await api ( pda , 'GET' , path )
if ( detail . status === 'WAIT_CUSTOMER_CONFIRMATION' ) return
if ( ! detail . pallets . length ) detail = await api ( pda , 'POST' , ` ${ path } /pallets/drafts` ,{ idempotencyKey : randomUUID ()})
let pallet = detail . pallets [ 0 ]
if ( pallet . status === 'DRAFT' ) detail = await api ( pda , 'POST' , ` ${ path } /pallets/ ${ pallet . id } /generate` ,{ idempotencyKey : randomUUID ()})
pallet = detail . pallets . find ( row => row . id === pallet . id )
if ( pallet . status !== 'BOUND' ) detail = await api ( pda , 'POST' , ` ${ path } /pallets/ ${ pallet . id } /bind` ,{ idempotencyKey : randomUUID (), palletCode : pallet . palletNo })
const boxes = detail . cartons . map ( box =>({ cartonMark : box . cartonMark , transferPalletId : pallet . id , length : 40 , width : 30 , height : 25 , weight : 4 , items : [{ skuCode : detail . items [ 0 ]. skuCode , quantity : 8 }]}))
if ( ! detail . cartons . some ( row => row . countedAt )) detail = await api ( pda , 'POST' , ` ${ path } /cartons/count` ,{ idempotencyKey : randomUUID (), forecastPackageCount : detail . packageCount , boxes })
const photo = await api ( pda , 'POST' , ` ${ path } /measurement-photos/files` , image ( 'warehouse-count' ))
detail = await api ( pda , 'POST' , ` ${ path } /palletize` ,{ idempotencyKey : randomUUID (), photoFileIds : [ photo . fileId ]})
check ( detail . status === 'WAIT_CUSTOMER_CONFIRMATION' && detail . orderStatus === 'INBOUND_COMPLETED' , '取货到仓后仓库独立清点、测量、拍照完成入库' ); source . palletNo = pallet . palletNo ; await save ()
}
try {
const login = await api ( '' , 'POST' , '/api/auth/login' ,{ loginId : process . env . ADMIN_TEST_USERNAME , password : process . env . ADMIN_TEST_PASSWORD }); admin = login . accessToken
2026-10-27 17:44:41 +08:00
const required = [ 'list' , 'create' , 'edit' , 'depart' , 'cancel' ]. map ( key => `platform:overseas-warehouse:dispatch: ${ key } ` )
check ( Array . isArray ( login . authorities ) && required . every ( code => login . authorities . includes ( code )), '固定平台账号缺少正式业务权限,不自动补权限或创建角色' )
report . operatorUsername = process . env . ADMIN_TEST_USERNAME ; await save ()
2026-10-20 21:47:51 +08:00
const customers = ( await call ( 'GET' , '/api/platform/v1/iam/tenants?page=1&size=200' )). records . filter ( row => row . status === 'ACTIVE' )
check ( customers . length >= 2 , '有两个有效客户验证同仓跨客户取货' )
const warehouses = ( await call ( 'GET' , '/api/platform/v1/warehouse-admin?page=1&size=100&warehouseType=OVERSEAS&status=ACTIVE' )). records
const warehouse = warehouses . find ( row => row . warehouseCode === 'US-LAX-01' ) || warehouses [ 0 ]; report . warehouseId = warehouse . id
2026-10-27 17:44:41 +08:00
report . pdaUsername = process . env . LOCAL_TRANSFER_PDA_USERNAME
check ( Boolean ( report . pdaUsername ), '必须配置既有 LOCAL_TRANSFER_PDA_USERNAME,不自动创建PDA账号' )
2026-10-20 21:47:51 +08:00
pda = ( await api ( '' , 'POST' , '/api/auth/login' ,{ loginId : report . pdaUsername , password : process . env . LOCAL_TRANSFER_PDA_PASSWORD })). accessToken
const a = await transfer ( customers [ 0 ], warehouse , 'pickupA' ), b = await transfer ( customers [ 1 ], warehouse , 'pickupB' ), c = await transfer ( customers [ 0 ], warehouse , 'pickupCancelSource' )
let shared = await pickup ([ a , b ], 'pickupCompleted' ), cancelled = await pickup ([ c ], 'pickupCancelled' )
const resource = await resources ( warehouse ); shared = await assign ( shared , resource )
if ( cancelled . execution . trip . status === 'PLANNED' ){
await call ( 'POST' , ` ${ base } / ${ cancelled . dispatchNo } /assign` ,{ recordVersion : cancelled . execution . trip . recordVersion ,... resource , reason : marker }, 'A0409' )
await act ( cancelled , 'cancel' ); cancelled = await read ( cancelled . dispatchNo )
}
check ( cancelled . execution . trip . status === 'CANCELLED' , '取消取货排车生效' )
const replanned = await pickup ([ c ], 'pickupPendingSample' ); check ( replanned . dispatchNo !== cancelled . dispatchNo , '取货取消后可重新安排新排车,保留待操作样单' )
if ( shared . execution . trip . status === 'PLANNED' ){
await api ( pda , 'POST' , `/api/pda/v1/last-leg/warehouse-transfers/ ${ a . transferNo } /pallets/drafts` ,{ idempotencyKey : randomUUID ()}, 'A0409' )
await call ( 'POST' , ` ${ base } /pickup` ,{... report . requests . pickupCompleted , idempotencyKey : randomUUID ()}, 'A0409' )
await act ( shared , 'pickup/depart' ); shared = await read ( shared . dispatchNo )
await act ( shared , 'pickup/complete' ,{}, 'A0409' )
}
for ( const source of [ a , b ]){
shared = await read ( shared . dispatchNo ); let task = shared . pickups . find ( row => row . transferNo === source . transferNo )
const suffix = `pickup/ ${ source . customerId } / ${ source . transferNo } `
if ( task . status === 'ASSIGNED' ){
if ( source === b && report . pickupEvidenceA ) await act ( shared , ` ${ suffix } /picked` ,{ actualPackages : 3 , evidenceFileIds : [ report . pickupEvidenceA ]}, 'A0400' )
const file = await call ( 'POST' , ` ${ base } / ${ shared . dispatchNo } / ${ suffix } /files` , image ( source . transferNo ))
if ( source === a ){ report . pickupEvidenceA = file . fileId ; await save ()}
const body = {... command ( shared ), actualPackages : 3 , evidenceFileIds : [ file . fileId ], remark : marker }
await call ( 'POST' , ` ${ base } / ${ shared . dispatchNo } / ${ suffix } /picked` , body ); await call ( 'POST' , ` ${ base } / ${ shared . dispatchNo } / ${ suffix } /picked` , body )
await call ( 'POST' , ` ${ base } / ${ shared . dispatchNo } / ${ suffix } /picked` ,{... body , actualPackages : 4 }, 'A0409' )
const original = await call ( 'GET' , ` ${ transfers } / ${ source . transferNo } ?customerId= ${ source . customerId } ` )
check ( original . packageCount === 3 && original . status === 'WAIT_PDA_RECEIVING' && JSON . stringify ( original . cartons . map ( row => row . cartonMark )) === JSON . stringify ( source . cartonMarks ), '取货事实不篡改预报箱数、箱唛及仓库收货状态' )
}
shared = await read ( shared . dispatchNo ); task = shared . pickups . find ( row => row . transferNo === source . transferNo )
if ( task . status === 'PICKED' ) await act ( shared , ` ${ suffix } /arrive` )
}
shared = await read ( shared . dispatchNo ); if ( shared . execution . trip . status === 'IN_TRANSIT' ) await act ( shared , 'pickup/complete' )
shared = await release ( await read ( shared . dispatchNo )); check ( shared . execution . trip . status === 'COMPLETED' , '到仓取货排车完成并分别释放司机车辆' )
await measure ( a )
let source = await truck ( customers [ 0 ], 'truckOriginal' )
if ( ! report . documents . truckCancelled ) source = await truckReady ( source )
let native = await truckPlan ( source , warehouse , 'truckCancelled' )
if ( native . execution . trip . status === 'PLANNED' ){ await act ( native , 'cancel' ); native = await read ( native . dispatchNo )}
source = await call ( 'GET' , ` ${ trucks } / ${ source . summary . orderNo } ?customerId= ${ source . summary . customerId } ` )
if ( ! report . documents . truckCompleted ) check ( source . summary . status === 'PENDING_DISPATCH' , '取消本土卡车排车回退原单待派车' )
native = await truckPlan ( source , warehouse , 'truckCompleted' )
await call ( 'POST' , ` ${ base } /truck` ,{... report . requests . truckCompleted , plannedDepartureAt : new Date (). toISOString ()}, 'A0409' )
if ( native . execution . trip . status === 'PLANNED' ){
await call ( 'POST' , ` ${ trucks } / ${ source . summary . orderNo } /start-transit?customerId= ${ source . summary . customerId } ` ,{ version : source . summary . version , idempotencyKey : randomUUID (), remark : marker }, 'A0409' )
native = await assign ( native , resource )
await act ( native , 'truck/depart' ,{ recordVersion : native . execution . trip . recordVersion - 1 }, 'A0409' )
await act ( native , 'truck/depart' ); native = await read ( native . dispatchNo )
}
if ( native . execution . trip . status === 'IN_TRANSIT' ){
await act ( native , 'truck/complete' ,{ evidenceFileIds : []}, 'A0409' )
const file = await call ( 'POST' , ` ${ base } / ${ native . dispatchNo } /truck/files` , image ( 'truck-completion' ))
const body = {... command ( native ), evidenceFileIds : [ file . fileId ]}; await call ( 'POST' , ` ${ base } / ${ native . dispatchNo } /truck/complete` , body ); await call ( 'POST' , ` ${ base } / ${ native . dispatchNo } /truck/complete` , body )
await call ( 'POST' , ` ${ base } / ${ native . dispatchNo } /truck/complete` ,{... body , evidenceFileIds : []}, 'A0409' )
}
native = await release ( await read ( native . dispatchNo ))
source = await call ( 'GET' , ` ${ trucks } / ${ source . summary . orderNo } ?customerId= ${ source . summary . customerId } ` )
check ( native . execution . trip . status === 'COMPLETED' && source . summary . status === 'COMPLETED' && native . completionEvidence . length > 0 , '本土卡车排车完成、凭证持久化、原单同步完成' )
for ( const doc of [ shared , native , replanned ]){
const root = await call ( 'GET' , `/api/platform/v1/last-leg/dispatches/ ${ doc . dispatchNo } ` )
check ( root . taskType === doc . taskType , '同一个现有排车详情返回正确任务类型' )
}
report . final = { pickup : shared . dispatchNo , truck : native . dispatchNo , pending : replanned . dispatchNo , truckOriginal : source . summary . orderNo }; report . passed = true ; report . completedAt = new Date (). toISOString (); await save (); console . log ( JSON . stringify ({ passed : true , output ,... report . final }))
} catch ( error ){ report . passed = false ; report . lastError = String ( error ); await save (); console . error ( String ( error )); process . exitCode = 1 }