优化
This commit is contained in:
@@ -43,14 +43,14 @@ async function command(context, module, method, path, body, label, token, expect
|
||||
return data
|
||||
}
|
||||
|
||||
// 排车公开详情未提供tripNo,按本轮唯一新车牌分页,再核对真实资源及唯一绑定。
|
||||
// 按审核后权威车辆快照分页,再核对真实资源及本轮排车唯一绑定。
|
||||
async function discoverTrip(context, module, proof) {
|
||||
// 正式车辆主档将车牌转为大写;保留原marker及命令键,仅按该权威规则生成精确查询身份。
|
||||
const vehiclePlate = proof.marker.toUpperCase()
|
||||
const vehiclePlate = proof.vehicle?.plate
|
||||
if (typeof vehiclePlate !== 'string' || !vehiclePlate.trim()) throw Error('审核后的车辆快照缺少真实车牌')
|
||||
const rows = [], seen = new Set(); let total
|
||||
for (let page = 1; ; page++) {
|
||||
const query = new URLSearchParams({ page: String(page), size: '200', keyword: vehiclePlate, warehouseId: proof.warehouseId })
|
||||
const data = await command(context, module, 'GET', `${BASE}/trips?${query}`, undefined, '按本轮唯一车牌读取自动资源记录')
|
||||
const data = await command(context, module, 'GET', `${BASE}/trips?${query}`, undefined, '按实际车牌读取自动资源记录')
|
||||
if (!Array.isArray(data.records) || !Number.isSafeInteger(Number(data.total)) || Number(data.total) < 0
|
||||
|| Number(data.page) !== page || Number(data.size) !== 200) throw new Error('车次分页不完整,不能证明唯一关联')
|
||||
total ??= Number(data.total)
|
||||
@@ -79,7 +79,25 @@ async function discoverTrip(context, module, proof) {
|
||||
Object.assign(proof, { tripNo: detail.trip.tripNo, trip: detail.trip, stops: detail.trip.stops, fleetRecordVersion: detail.trip.recordVersion })
|
||||
}
|
||||
|
||||
/** 本轮TEST主档经正式API建立;主单创建时已自动建立Fleet资源记录。 */
|
||||
export async function reuseFleetResources(context, module, proof, arrival) {
|
||||
const pairs = context.options.fixedFleetResources
|
||||
const busy = (context.createdResources.fleetTrips || []).filter(item => item !== proof
|
||||
&& item.closure?.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED')
|
||||
const pair = Array.isArray(pairs) && pairs.find(item => /^[1-9][0-9]*$/.test(id(item?.vehicleId))
|
||||
&& /^[1-9][0-9]*$/.test(id(item?.driverId))
|
||||
&& !busy.some(trip => id(trip.vehicleId) === id(item.vehicleId) || id(trip.driverId) === id(item.driverId)))
|
||||
if (!pair) throw Error('缺少本轮可用的固定车辆司机组合;禁止自动新建主档或证照')
|
||||
proof.expectedDriverEndAt = new Date(Math.max(Date.now() + 48 * 3600000, arrival ? Date.parse(arrival) + 6 * 3600000 : 0)).toISOString()
|
||||
const vehicle = await command(context, module, 'GET', `/api/platform/v1/last-leg/master-data/vehicles/${id(pair.vehicleId)}`, undefined, '核对固定车辆状态')
|
||||
const driver = await command(context, module, 'GET', `${BASE}/drivers/${id(pair.driverId)}?expectedEnd=${encodeURIComponent(proof.expectedDriverEndAt)}`, undefined, '核对固定司机至任务结束的资格')
|
||||
if (id(vehicle.id) !== id(pair.vehicleId) || vehicle.status !== 'IN_SERVICE' || vehicle.region !== 'OVERSEAS' || vehicle.ownership !== 'OWNED'
|
||||
|| id(driver.id) !== id(pair.driverId) || driver.active !== true || driver.reviewStatus !== 'APPROVED'
|
||||
|| driver.details?.ownership !== 'OWNED' || driver.qualification?.canDispatch !== true || driver.qualification?.reason !== 'ELIGIBLE')
|
||||
throw Error('固定车辆司机状态或资格不符;不创建、启用或修复主档')
|
||||
Object.assign(proof, { vehicleId: id(vehicle.id), driverId: id(driver.id), reusedMasterData: true })
|
||||
}
|
||||
|
||||
/** 固定客户复用已配置主档;非固定生命周期场景才建立专用主档。 */
|
||||
export async function prepareFleetTrip(context, module, dispatchNo, warehouseId) {
|
||||
if (!dispatchNo || !warehouseId) throw new Error('车次验收缺少本轮排车单或实际起运仓')
|
||||
const proof = { dispatchNo, warehouseId: id(warehouseId), marker: `FT-${randomUUID().slice(0, 12)}`,
|
||||
@@ -90,6 +108,9 @@ export async function prepareFleetTrip(context, module, dispatchNo, warehouseId)
|
||||
version(before.recordVersion)
|
||||
if (before.dispatchNo !== dispatchNo || before.status !== 'ACTIVE' || safePackageCount(before.counts?.pickedPackageCount) !== 0
|
||||
|| safePackageCount(before.counts?.shippedPackageCount) !== 0) throw new Error('仅本轮有效零扫码排车可准备车辆和审核')
|
||||
const confirmedTimes = (before.dispatchInfo?.fbaAppointments || []).filter(row => row.status === 'SUCCESS' && row.confirmedAt).map(row => row.confirmedAt).sort()
|
||||
const arrival = before.dispatchInfo?.estimatedArrivalAt ?? confirmedTimes[0] ?? null
|
||||
if (context.options?.fixedCustomerId) await reuseFleetResources(context, module, proof, arrival)
|
||||
if (before.stage === 'LOADING') {
|
||||
proof.returnRequest = { recordVersion: before.recordVersion, idempotencyKey: key(proof, 'RETURN_TO_REVIEW'), reason: 'TEST零扫码明确退回装车前后调整资源' }
|
||||
await command(context, module, 'POST', `${path}/return-to-review`, proof.returnRequest, '零扫码排车显式退回装车前')
|
||||
@@ -97,6 +118,7 @@ export async function prepareFleetTrip(context, module, dispatchNo, warehouseId)
|
||||
}
|
||||
if (before.stage !== 'BEFORE_LOADING' || !['DRAFT', 'DISPATCH_REJECTED', 'LOADING_REJECTED'].includes(before.reviewState))
|
||||
throw new Error('排车未处于可编辑装车前阶段,不能隐式撤销审核或改派')
|
||||
if (!context.options?.fixedCustomerId) {
|
||||
const vehicle = await command(context, module, 'POST', '/api/platform/v1/last-leg/master-data/vehicles', {
|
||||
plateNumber: proof.marker, model: 'Box Truck', fuelConsumptionPer100Km: 12.5, mileageKm: 100,
|
||||
region: 'OVERSEAS', ownership: 'OWNED', status: 'IN_SERVICE', maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90,
|
||||
@@ -104,9 +126,8 @@ export async function prepareFleetTrip(context, module, dispatchNo, warehouseId)
|
||||
}, '建立本轮TEST车辆主档及容量')
|
||||
proof.vehicleId = id(vehicle.id)
|
||||
if (!proof.vehicleId) throw new Error('车辆主档没有返回真实资源ID')
|
||||
const confirmedTimes = (before.dispatchInfo?.fbaAppointments || []).filter(row => row.status === 'SUCCESS' && row.confirmedAt).map(row => row.confirmedAt).sort()
|
||||
const arrival = before.dispatchInfo?.estimatedArrivalAt ?? confirmedTimes[0] ?? null
|
||||
await prepareQualifiedDriver(context, module, proof, arrival)
|
||||
}
|
||||
proof.updateRequest = { recordVersion: before.recordVersion, idempotencyKey: key(proof, 'EDIT'),
|
||||
publishLooseboardDemand: before.publishLooseboardDemand,
|
||||
vehicle: { mode: 'MASTER', vehicleId: proof.vehicleId },
|
||||
@@ -222,17 +243,69 @@ async function verifyPreDepartureReassignment(context, module, proof) {
|
||||
state.status = 'REJECTED_WITHOUT_MUTATION'
|
||||
}
|
||||
|
||||
export const LOADING_PHOTO_SLOTS = ['LOADING_SCENE', 'LOADING_LIST', 'PRE_DEPARTURE_CHECK', 'DRIVER_LICENSE', 'RECEIPT']
|
||||
|
||||
export async function ensureFleetLoadingDocuments(context, module, proof, token) {
|
||||
const path = `${token ? PDA : DISPATCH}/${encodeURIComponent(proof.dispatchNo)}/loading-documents`
|
||||
const read = () => command(context, module, 'GET', path, undefined, '回查本车装车资料', token)
|
||||
let documents = await read()
|
||||
const initialStage = documents.stage
|
||||
const complete = value => LOADING_PHOTO_SLOTS.every(slot => value.photos?.some(photo => photo.slot === slot && id(photo.fileId)))
|
||||
const validate = value => {
|
||||
if (value.dispatchNo !== proof.dispatchNo || !['LOADING', 'AFTER_LOADING', 'DELIVERING'].includes(value.stage)
|
||||
|| value.complete !== complete(value)) throw Error('本车装车资料身份、阶段或五框完整性不匹配')
|
||||
}
|
||||
validate(documents)
|
||||
proof.loadingDocuments ??= { syntheticTestMaterials: true, uploads: [] }
|
||||
for (const slot of LOADING_PHOTO_SLOTS) {
|
||||
if (documents.photos?.some(photo => photo.slot === slot && id(photo.fileId))) continue
|
||||
if (!documents.editable || typeof context.requestMultipart !== 'function') throw Error('缺少本车装车照片写入资格或上传能力')
|
||||
const content = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64')
|
||||
const upload = { slot, path: `${path}/${slot}/uploads?idempotencyKey=${key(proof, `PHOTO_UPLOAD_${slot}`)}`,
|
||||
name: `${proof.marker}-TEST-${slot}.png`, sha256: createHash('sha256').update(content).digest('hex'), status: 'PREPARED' }
|
||||
if (proof.loadingDocuments.uploads.some(item => item.slot === slot)) throw Error('该照片上传已开始,保留原键和结果排查,不自动重发')
|
||||
proof.loadingDocuments.uploads.push(upload); await context.checkpoint?.()
|
||||
let response
|
||||
try { response = await context.requestMultipart('POST', upload.path, { name: upload.name, type: 'image/png', content }, token) }
|
||||
catch (error) { upload.status = 'UNKNOWN'; await context.checkpoint?.(); throw error }
|
||||
Object.assign(upload, { status: response?.http === 200 && response?.body?.code ? 'RESPONDED' : 'UNKNOWN',
|
||||
http: response?.http, code: response?.body?.code, requestId: response?.body?.requestId, traceId: response?.body?.traceId })
|
||||
await context.checkpoint?.()
|
||||
const photo = context.apiSuccess(module, '上传本轮TEST合成装车照片', response, '', OPTIONS)
|
||||
if (response?.http !== 200 || String(response?.body?.code) !== '200' || photo?.slot !== slot || !id(photo.fileId)) throw Error('装车照片上传未确认,停止绑定及发车')
|
||||
const body = { recordVersion: documents.recordVersion, idempotencyKey: key(proof, `PHOTO_BIND_${slot}`), fileId: id(photo.fileId) }
|
||||
const bound = await command(context, module, 'PUT', `${path}/${slot}`, body, '保存本车装车照片', token)
|
||||
validate(bound)
|
||||
check(context, module, '绑定回执保留本槽文件', bound.photos.some(item => item.slot === slot && id(item.fileId) === id(photo.fileId)))
|
||||
const replay = await command(context, module, 'PUT', `${path}/${slot}`, body, '原键重放装车照片绑定', token)
|
||||
check(context, module, '照片绑定重放恢复原回执', JSON.stringify(replay) === JSON.stringify(bound))
|
||||
documents = await read(); validate(documents)
|
||||
check(context, module, '装车资料未齐不得提前发车', documents.stage === (initialStage === 'AFTER_LOADING' && documents.complete ? 'DELIVERING' : initialStage))
|
||||
check(context, module, '装车照片已持久化', documents.photos.some(item => item.slot === slot && id(item.fileId) === id(photo.fileId)))
|
||||
}
|
||||
check(context, module, '五个装车照片框均已保存', documents.complete && complete(documents))
|
||||
proof.loadingDocuments.result = documents
|
||||
await context.checkpoint?.()
|
||||
return documents
|
||||
}
|
||||
|
||||
export async function confirmFleetLoading(context, module, proof, pdaToken, actualMeasurements, beforeComplete) {
|
||||
if (!proof?.dispatchNo || !proof.tripNo) throw new Error('缺少本轮原生排车及唯一资源记录证明')
|
||||
if (proof.loadingCommand) throw new Error('本轮装车命令已开始;保留原请求排查,不自动重试')
|
||||
const path = `${DISPATCH}/${encodeURIComponent(proof.dispatchNo)}`, pdaPath = `${PDA}/${encodeURIComponent(proof.dispatchNo)}`
|
||||
const preview = await command(context, module, 'GET', `${pdaToken ? pdaPath : path}/loading`, undefined, '读取真实整车拣货及实载证据', pdaToken)
|
||||
let preview = await command(context, module, 'GET', `${pdaToken ? pdaPath : path}/loading`, undefined, '读取真实整车拣货及实载证据', pdaToken)
|
||||
if (preview.dispatchNo !== proof.dispatchNo || preview.status !== 'ACTIVE' || preview.stage !== 'LOADING' || preview.pickingReconciled !== true
|
||||
|| preview.loadingComplete?.allowed !== true || !preview.manifestHash) throw new Error(`整车未就绪:${preview.loadingComplete?.blockReason || '真实拣货未核平或阶段不符'}`)
|
||||
if (actualMeasurements && (actualMeasurements.environment !== 'TEST' || actualMeasurements.marker !== context.marker
|
||||
|| actualMeasurements.dispatchNo !== proof.dispatchNo || !Array.isArray(actualMeasurements.pallets))) throw new Error('实测回执不属于本轮TEST排车')
|
||||
const measurements = preview.pallets.map(p => ({ warehousePalletId: id(p.warehousePalletId), palletSpaces: 1, weightKg: p.weightKg ?? null, volumeCbm: p.volumeCbm ?? null, deliveryIndex: p.deliveryIndex }))
|
||||
await verifyPreDepartureReassignment(context, module, proof)
|
||||
await ensureFleetLoadingDocuments(context, module, proof, pdaToken)
|
||||
const refreshed = await command(context, module, 'GET', `${pdaToken ? pdaPath : path}/loading`, undefined, '照片保存后回查装车资格', pdaToken)
|
||||
check(context, module, '资料保存不提前装车或改变实载清单', refreshed.dispatchNo === proof.dispatchNo
|
||||
&& refreshed.status === 'ACTIVE' && refreshed.stage === 'LOADING' && refreshed.pickingReconciled === true
|
||||
&& refreshed.loadingComplete?.allowed === true && refreshed.manifestHash === preview.manifestHash)
|
||||
preview = refreshed
|
||||
proof.loadingPreview = preview; proof.loadingMeasurementReceipt = actualMeasurements
|
||||
proof.loadingCommand = { path: `${pdaToken ? pdaPath : path}/loading-complete`, body: { recordVersion: preview.recordVersion,
|
||||
idempotencyKey: key(proof, 'LOADING_COMPLETE'), manifestHash: preview.manifestHash, remark: 'TEST本轮实际读取测量证据,不使用计费重量或固定值' } }
|
||||
@@ -310,7 +383,7 @@ export async function finishFleetTrip(context, module, proof) {
|
||||
state.nextDispatchResult = await command(context, module, 'POST', DISPATCH, state.nextDispatchRequest, '建立本轮原生空排车占用已结束资源')
|
||||
if (!state.nextDispatchResult.dispatchNo || state.nextDispatchResult.dispatchNo === proof.dispatchNo) throw new Error('原生空排车未返回独立真实业务号')
|
||||
state.nextDispatch = { dispatchNo: state.nextDispatchResult.dispatchNo, warehouseId: proof.warehouseId,
|
||||
marker: proof.marker, vehicleId: proof.vehicleId, driverId: proof.driverId }
|
||||
marker: proof.marker, vehicleId: proof.vehicleId, driverId: proof.driverId, vehicle: proof.vehicle }
|
||||
await discoverTrip(context, module, state.nextDispatch)
|
||||
if (state.nextDispatch.tripNo === proof.tripNo) throw new Error('新空排车未建立独立资源记录')
|
||||
await occupancy('VEHICLE', state.nextDispatch.tripNo); await occupancy('DRIVER', state.nextDispatch.tripNo)
|
||||
|
||||
Reference in New Issue
Block a user