feat(dispatch): unify whole vehicle scheduling, source ownership and staged execution

This commit is contained in:
2026-10-19 23:40:42 +08:00
parent e0677e7ae7
commit 9dd34791f7
346 changed files with 29245 additions and 12602 deletions
@@ -1,296 +1,316 @@
import { createHash, randomUUID } from 'node:crypto'
const BASE = '/api/platform/v1/last-leg/fleet'
const DISPATCH = '/api/platform/v1/last-leg/dispatches'
const PDA = '/api/pda/v1/last-leg/dispatches'
const OPTIONS = { scenario: 'fleet-physical-trip-departure', deploymentBlocker: true }
const id = value => value == null ? '' : String(value)
const id = value => {
if (typeof value === 'number' && !Number.isSafeInteger(value)) throw new Error('服务端资源ID超出安全整数范围,必须保留原始字符串')
return value == null ? '' : String(value)
}
const key = (proof, action) => createHash('sha256').update(`${proof.marker}:${proof.dispatchNo}:${action}`).digest('hex')
const version = value => {
if ((typeof value === 'number' && !Number.isSafeInteger(value)) || !/^(0|[1-9]\d*)$/.test(String(value)))
throw new Error('服务端车次版本无效')
if ((typeof value === 'number' && !Number.isSafeInteger(value)) || !/^(0|[1-9]\d*)$/.test(String(value))) throw new Error('服务端排车版本无效')
return BigInt(String(value))
}
async function command(context, module, method, path, body, label) {
const response = await context.request(method, path, body)
function check(context, module, label, condition, evidence = '') {
context.assert(module, label, null, condition, evidence, OPTIONS)
if (!condition) throw new Error(`${module}: ${label}失败`)
}
// 写前冻结原请求;未知结果停写,不能换键重造或自动重发。
async function command(context, module, method, path, body, label, token) {
let entry
if (method !== 'GET') {
context.createdResources ??= {}; context.createdResources.fleetTripCommands ??= []
entry = { method, path, body: structuredClone(body), status: 'PREPARED' }
context.createdResources.fleetTripCommands.push(entry); await context.checkpoint?.()
}
let response
try { response = await context.request(method, path, body, token) }
catch (error) { if (entry) { entry.status = 'UNKNOWN'; await context.checkpoint?.() }; throw error }
if (entry) {
Object.assign(entry, { status: 'RESPONDED', http: response?.http, code: response?.body?.code, requestId: response?.body?.requestId, traceId: response?.body?.traceId })
await context.checkpoint?.()
}
const data = context.apiSuccess(module, label, response, '', OPTIONS)
if (response?.http !== 200 || String(response?.body?.code) !== '200' || !data) throw new Error(`${module}: ${label}失败`)
return data
}
/** Test-only resources are created through real APIs and retained in the run report. No business SQL. */
// 排车公开详情未提供tripNo,按本轮唯一新车牌分页,再核对真实资源及唯一绑定。
async function discoverTrip(context, module, proof) {
const rows = [], seen = new Set(); let total
for (let page = 1; ; page++) {
const query = new URLSearchParams({ page: String(page), size: '200', keyword: proof.marker, warehouseId: proof.warehouseId })
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)
if (Number(data.total) !== total || data.records.length > 200 || (!data.records.length && rows.length < total)) throw new Error('车次分页变化或缺页')
for (const row of data.records) {
if (!row.tripNo || seen.has(row.tripNo)) throw new Error('车次分页业务号缺失或重复')
seen.add(row.tripNo); rows.push(row)
}
if (rows.length >= total) break
}
if (rows.length !== total) throw new Error('车次分页总数不一致')
const matches = rows.filter(row => row.vehiclePlate === proof.marker && id(row.vehicleId) === proof.vehicleId
&& id(row.driverId) === proof.driverId && id(row.warehouseId) === proof.warehouseId && row.tripPurpose === 'DELIVERY')
const bound = []
for (const candidate of matches) {
const detail = await command(context, module, 'GET', `${BASE}/trips/${encodeURIComponent(candidate.tripNo)}`, undefined, '核对排车与资源记录唯一绑定')
if (detail.trip?.tripNo !== candidate.tripNo || id(detail.trip.vehicleId) !== proof.vehicleId || id(detail.trip.driverId) !== proof.driverId)
throw new Error('真实车次候选资源发生变化')
if (detail.dispatches?.some(row => row.dispatchNo === proof.dispatchNo)) {
if (detail.dispatches.length !== 1) throw new Error('原生排车资源记录仍关联多张子单')
bound.push(detail)
}
}
if (bound.length !== 1) throw new Error(`本轮排车自动资源记录不是唯一绑定:${bound.length}`)
const [detail] = bound
Object.assign(proof, { tripNo: detail.trip.tripNo, trip: detail.trip, stops: detail.trip.stops, fleetRecordVersion: detail.trip.recordVersion })
}
/** 本轮TEST主档经正式API建立;主单创建时已自动建立Fleet资源记录。 */
export async function prepareFleetTrip(context, module, dispatchNo, warehouseId) {
if (!dispatchNo || !warehouseId) throw new Error('车次验收缺少本轮排车单或实际起运仓')
const marker = `FT-${randomUUID().slice(0, 12)}`
const proof = { dispatchNo, warehouseId: id(warehouseId), marker, reassignment: { status: 'PENDING' } }
context.createdResources.fleetTrips ??= []
context.createdResources.fleetTrips.push(proof)
const vehicle = await command(context, module, 'POST', '/api/platform/v1/last-leg/master-data/vehicles', {
plateNumber: marker, model: 'Box Truck', fuelConsumptionPer100Km: 12.5, mileageKm: 100,
region: 'OVERSEAS', ownership: 'OWNED', status: 'IN_SERVICE', maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90,
remark: `${context.marker} controlled TEST vehicle`,
}, '建立本轮真实车辆档案及容量')
proof.vehicleId = id(vehicle.id)
await prepareQualifiedDriver(context, module, marker, proof)
const plannedDepartureAt = new Date(Date.now() + 3600000).toISOString()
const expectedDriverEndAt = new Date(Date.now() + 48 * 3600000).toISOString()
const createKey = createHash('sha256').update(`${context.marker}:${dispatchNo}`).digest('hex')
let detail = await command(context, module, 'POST', `${BASE}/trips`, {
warehouseId, plannedDepartureAt, expectedDriverEndAt, idempotencyKey: createKey,
stops: [{ name: '测试起运仓' }, { name: '待匹配货物目的地' }], remark: context.marker,
}, '创建本轮共享运输车次')
proof.tripNo = detail.trip.tripNo
const path = `${BASE}/trips/${encodeURIComponent(proof.tripNo)}`
detail = await command(context, module, 'POST', `${path}/assignment`, {
recordVersion: detail.trip.recordVersion, vehicleId: proof.vehicleId, driverId: proof.driverId, reason: '本轮测试派车',
}, '正式分配车辆司机并核验证照')
detail = await command(context, module, 'POST', `${path}/dispatches/bind`, {
recordVersion: detail.trip.recordVersion, dispatchNo, reason: '关联本轮客户执行单据',
}, '将客户排车单关联至车次')
const loading = await command(context, module, 'GET', `${path}/loading`, undefined, '读取真实货物目的地匹配键')
const destinations = new Map()
for (const cargo of loading.cargo || []) {
if (!cargo.destinationKey) throw new Error('实际排车货物缺少目的地,不能伪造装载站点')
destinations.set(cargo.destinationKey, { name: cargo.destinationLabel, destinationKey: cargo.destinationKey })
const proof = { dispatchNo, warehouseId: id(warehouseId), marker: `FT-${randomUUID().slice(0, 12)}`,
syntheticTestMaterials: true, preparation: 'CHECKING', reassignment: { status: 'PENDING' } }
context.createdResources.fleetTrips ??= []; context.createdResources.fleetTrips.push(proof)
const path = `${DISPATCH}/${encodeURIComponent(dispatchNo)}`
let before = await command(context, module, 'GET', path, undefined, '读取原生排车主单版本和阶段')
version(before.recordVersion)
if (before.dispatchNo !== dispatchNo || before.status !== 'ACTIVE' || safePackageCount(before.counts?.pickedPackageCount) !== 0
|| safePackageCount(before.counts?.shippedPackageCount) !== 0) throw new Error('仅本轮有效零扫码排车可准备车辆和审核')
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, '零扫码排车显式退回装车前')
before = await command(context, module, 'GET', path, undefined, '回查显式退回后的审核状态')
}
if (!destinations.size) throw new Error('实际排车没有货物')
detail = await command(context, module, 'PUT', `${path}/plan`, {
recordVersion: detail.trip.recordVersion, plannedDepartureAt, expectedDriverEndAt,
stops: [{ name: '测试起运仓' }, ...destinations.values()], reason: '按实际货物目的地冻结路线',
}, '在拣货前匹配真实卸货站')
proof.stops = detail.trip.stops
detail = await command(context, module, 'POST', `${path}/dispatches/${encodeURIComponent(dispatchNo)}/review`, {
recordVersion: detail.trip.recordVersion, decision: 'APPROVED',
}, '拣货前通过本轮客户排车调度审核')
proof.dispatchReview = detail.dispatches.find(row => row.dispatchNo === dispatchNo)?.review
if (proof.dispatchReview?.status !== 'APPROVED') throw new Error('客户排车调度审核未通过,停止拣货准备')
return proof
if (before.stage !== 'BEFORE_LOADING' || !['DRAFT', 'DISPATCH_REJECTED', 'LOADING_REJECTED'].includes(before.reviewState))
throw new Error('排车未处于可编辑装车前阶段,不能隐式撤销审核或改派')
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,
remark: `${context.marker} TEST合成车辆主档,容量不是实载测量`,
}, '建立本轮TEST车辆主档及容量')
proof.vehicleId = id(vehicle.id)
if (!proof.vehicleId) throw new Error('车辆主档没有返回真实资源ID')
await prepareQualifiedDriver(context, module, proof)
proof.updateRequest = { recordVersion: before.recordVersion, idempotencyKey: key(proof, 'EDIT'),
vehicle: { mode: 'MASTER', vehicleId: proof.vehicleId },
driver: { mode: 'MASTER', driverId: proof.driverId, evidence: { expectedTaskEndAt: proof.expectedDriverEndAt } },
plannedDepartureAt: new Date(Date.now() + 3600000).toISOString(), estimatedArrivalAt: before.dispatchInfo?.estimatedArrivalAt ?? null,
remark: `${context.marker} TEST合成资料正常履约` }
let result = await command(context, module, 'PUT', path, proof.updateRequest, '通过排车主单编辑正式分配车辆司机')
for (const action of ['submit-review', 'dispatch-review', 'loading-review']) {
const body = { recordVersion: result.recordVersion, idempotencyKey: key(proof, action),
...(action === 'submit-review' ? { remark: '本轮TEST整车提交审核' } : { decision: 'APPROVED', reason: '本轮TEST整车审核' }) }
result = await command(context, module, 'POST', `${path}/${action}`, body, `原生整车${action}`)
}
const detail = await command(context, module, 'GET', path, undefined, '审核后读取原生资源快照')
check(context, module, '两级审核通过才进入装车中', detail.status === 'ACTIVE' && detail.stage === 'LOADING'
&& detail.reviewState === 'APPROVED' && detail.reviews?.dispatchReview?.status === 'APPROVED'
&& detail.reviews?.loadingReview?.status === 'APPROVED' && safePackageCount(detail.counts?.pickedPackageCount) === 0
&& version(detail.recordVersion) === version(result.recordVersion))
Object.assign(proof, { vehicleInfo: detail.vehicleInfo, vehicle: detail.vehicleInfo?.vehicle, driver: detail.vehicleInfo?.driver,
dispatchReview: detail.reviews.dispatchReview, loadingReview: detail.reviews.loadingReview, recordVersion: detail.recordVersion })
await discoverTrip(context, module, proof)
proof.preparation = 'VERIFIED'; await context.checkpoint?.(); return proof
}
async function prepareQualifiedDriver(context, module, marker, proof) {
async function prepareQualifiedDriver(context, module, proof) {
proof.expectedDriverEndAt = new Date(Date.now() + 48 * 3600000).toISOString()
let driver = await command(context, module, 'POST', `${BASE}/drivers`, {
name: `${marker}验收司机`, phone: '19900000000', ownership: 'OWNED', region: 'OVERSEAS',
licenseNumber: marker, licenseType: 'Class A', licenseZone: 'UTC',
licenseExpiresOn: new Date(Date.now() + 365 * 86400000).toISOString().slice(0, 10),
remark: `${context.marker} TEST credential fixture; not a real driving licence`,
}, '建立本轮司机证照档案')
name: `${proof.marker}验收司机`, phone: '19900000000', ownership: 'OWNED', region: 'OVERSEAS',
licenseNumber: proof.marker, licenseType: 'Class A', licenseZone: 'UTC',
licenseExpiresOn: new Date(Date.now() + 365 * 86400000).toISOString().slice(0, 10), remark: `${context.marker} TEST合成证照资料,不是真实驾驶证`,
}, '建立本轮TEST司机档案')
proof.driverId = id(driver.id)
const uploaded = await context.requestMultipart('POST', `${BASE}/drivers/${proof.driverId}/license?recordVersion=${driver.recordVersion}`, {
name: `${marker}-TEST-LICENSE.png`, type: 'image/png',
content: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64'),
})
driver = context.apiSuccess(module, '上传本轮测试证照文件', uploaded, '', OPTIONS)
if (!driver?.licenseFileId) throw new Error('测试证照上传失败,不允许伪造已审核资格')
if (!proof.driverId) throw new Error('司机主档没有返回真实资源ID')
proof.licenseUpload = { path: `${BASE}/drivers/${proof.driverId}/license?recordVersion=${driver.recordVersion}`,
name: `${proof.marker}-TEST-LICENSE.png`, syntheticTestMaterials: true, status: 'PREPARED' }
await context.checkpoint?.()
let uploaded
try {
uploaded = await context.requestMultipart('POST', proof.licenseUpload.path, { name: proof.licenseUpload.name, type: 'image/png',
content: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64') })
} catch (error) { proof.licenseUpload.status = 'UNKNOWN'; await context.checkpoint?.(); throw error }
Object.assign(proof.licenseUpload, { status: 'RESPONDED', http: uploaded?.http, code: uploaded?.body?.code }); await context.checkpoint?.()
driver = context.apiSuccess(module, '上传TEST合成证照文件', uploaded, '', OPTIONS)
if (uploaded?.http !== 200 || String(uploaded?.body?.code) !== '200' || !driver?.licenseFileId) throw new Error('TEST证照上传失败,不能伪造资格')
proof.licenseFileId = id(driver.licenseFileId)
const reviewed = await command(context, module, 'POST', `${BASE}/drivers/${proof.driverId}/review`, {
recordVersion: driver.recordVersion, decision: 'APPROVED', reason: '仅用于本轮受控测试司机',
}, '通过正式命令审核测试司机')
if (reviewed.reviewStatus !== 'APPROVED') throw new Error('测试司机未审核通过,停止派车验证')
recordVersion: driver.recordVersion, decision: 'APPROVED', reason: '仅本轮TEST合成证照验收',
}, '正式审核本轮TEST司机')
if (reviewed.reviewStatus !== 'APPROVED') throw new Error('TEST司机未审核通过')
}
export function controlledPalletMeasurements(cargo, stops) {
const pallets = new Map()
for (const item of cargo) {
if (!item.warehousePalletId || !item.palletNo || !item.destinationKey) throw new Error('装载货物缺少真实托盘或目的地')
const deliveryIndex = stops.findIndex(stop => stop.destinationKey === item.destinationKey)
if (deliveryIndex < 1) throw new Error('货物目的地不在车次路线')
const key = id(item.warehousePalletId)
if (pallets.has(key) && pallets.get(key).deliveryIndex !== deliveryIndex) throw new Error('混合目的地实物托盘须先拆托')
// Controlled test measurement, deliberately separate from source billing/allocated mass.
pallets.set(key, { warehousePalletId: key, palletSpaces: 1, weightKg: 100, volumeCbm: 1, deliveryIndex })
const decimal = value => String(value).replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '')
function evidenceFor(row, field, value) {
return row?.evidence?.some(e => e.field === field && e.actual === true && e.sourceType && e.sourceBusinessNo
&& Number.isFinite(Date.parse(e.observedAt)) && decimal(e.value) === decimal(value))
}
function safePackageCount(value) {
if ((typeof value !== 'number' && typeof value !== 'string')
|| !/^(0|[1-9]\d*)$/.test(String(value)) || !Number.isSafeInteger(Number(value)))
throw new Error('实际托盘箱数缺失或精度无效')
return Number(value)
}
/** 保留导出名称供直接调用方定位;任何实际量均须来自实测证据。 */
export function controlledPalletMeasurements(pallets, stops, actualPallets = []) {
if (!Array.isArray(pallets) || !pallets.length || !Array.isArray(stops)) throw new Error('没有实际装载托盘或真实路线')
const supplied = new Map(), seen = new Set()
for (const row of actualPallets) {
if (!row.warehousePalletId || supplied.has(id(row.warehousePalletId))) throw new Error('生产实测回执托盘缺失或重复')
supplied.set(id(row.warehousePalletId), row)
}
if (!pallets.size) throw new Error('没有实际装载托盘')
return [...pallets.values()]
const result = pallets.map(row => {
const palletId = id(row.warehousePalletId)
if (!palletId || !row.palletNo || seen.has(palletId)) throw new Error('实际预览托盘缺失或重复')
seen.add(palletId)
const scheduled = safePackageCount(row.scheduledPackages), picked = safePackageCount(row.pickedPackages)
if (scheduled <= 0 || picked !== scheduled) throw new Error('实际托盘拣货尚未核平')
const receipt = supplied.get(palletId), output = { warehousePalletId: palletId }
for (const field of ['palletSpaces', 'weightKg', 'volumeCbm']) {
const source = row[field] == null ? receipt : row, value = source?.[field]
if (!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(String(value)) || !Number.isFinite(Number(value)) || Number(value) <= 0) throw new Error(`实际测量缺少有效${field}`)
if (!evidenceFor(source, field, value)) throw new Error(`实际测量${field}缺少真实来源证据`)
if (field === 'palletSpaces' && !Number.isSafeInteger(value)) throw new Error('实际托位必须为正整数')
if (row[field] != null && receipt?.[field] != null && decimal(row[field]) !== decimal(receipt[field])) throw new Error(`生产回执与预览${field}不一致`)
output[field] = value
}
const index = row.deliveryIndex ?? receipt?.deliveryIndex
if (!Number.isSafeInteger(index) || index < 1 || !stops[index]?.destinationKey) throw new Error('实际卸货站缺失或不在车次路线')
if (row.deliveryIndex != null && receipt?.deliveryIndex != null && row.deliveryIndex !== receipt.deliveryIndex) throw new Error('生产回执与实际卸货站不一致')
output.deliveryIndex = index; return output
})
if ([...supplied.keys()].some(palletId => !seen.has(palletId))) throw new Error('生产实测回执包含本车以外托盘')
return result
}
export async function confirmFleetLoading(context, module, proof) {
// 已扫码单只能用原生编辑拒绝证明,不能再走旧Fleet强行改派。
async function verifyPreDepartureReassignment(context, module, proof) {
const state = proof.reassignment
if (state && state.status !== 'PENDING') throw new Error('本轮改派检查已开始;保留原请求排查,不自动重复写入')
if (state) state.status = 'CHECKING'
const confirmed = await confirmActualLoad(context, module, proof)
if (!state) return approveLoading(context, module, proof, confirmed.recordVersion)
state.originalConfirmation = confirmed
await verifyPreDepartureReassignment(context, module, proof, confirmed)
const reconfirmed = await confirmActualLoad(context, module, proof)
check(context, module, '重新确认仍覆盖改派前同批货物', reconfirmed.manifestHash === confirmed.manifestHash
&& JSON.stringify(reconfirmed.cargo) === JSON.stringify(confirmed.cargo)
&& version(reconfirmed.recordVersion) > version(state.assignedTrip.recordVersion))
state.finalConfirmation = reconfirmed
const reviewedVersion = await approveLoading(context, module, proof, reconfirmed.recordVersion)
state.status = 'VERIFIED'
return reviewedVersion
if (!state) return
if (state.status !== 'PENDING') throw new Error('本轮改派拒绝检查已开始;保留原请求排查,不自动重试')
state.status = 'CHECKING'
const path = `${DISPATCH}/${encodeURIComponent(proof.dispatchNo)}`
state.before = await command(context, module, 'GET', path, undefined, '已扫码拒绝前保存主单')
state.itemsBefore = await command(context, module, 'GET', `${path}/items`, undefined, '保存真实扫码与来源')
if (state.before.status !== 'ACTIVE' || state.before.stage !== 'LOADING' || safePackageCount(state.before.counts?.pickedPackageCount) <= 0) throw new Error('拒绝检查缺少本轮真实扫码事实')
state.request = { ...proof.updateRequest, recordVersion: state.before.recordVersion, idempotencyKey: key(proof, 'SCANNED_EDIT_MUST_REJECT'),
plannedDepartureAt: new Date(Date.parse(proof.updateRequest.plannedDepartureAt) + 60000).toISOString(), remark: 'TEST已扫码后编辑资源计划必须拒绝' }
await context.checkpoint?.()
const response = await context.request('PUT', path, state.request)
state.response = { http: response?.http, code: response?.body?.code, message: response?.body?.message }
check(context, module, '已扫码原生排车编辑被服务端拒绝', response?.http === 409 || (response?.http === 200 && ['409', 'A0409'].includes(String(response?.body?.code))))
state.after = await command(context, module, 'GET', path, undefined, '拒绝后回查主单')
state.itemsAfter = await command(context, module, 'GET', `${path}/items`, undefined, '拒绝后回查真实货物')
check(context, module, '已扫码拒绝后版本资源货物数量不变', JSON.stringify(state.before) === JSON.stringify(state.after) && JSON.stringify(state.itemsBefore) === JSON.stringify(state.itemsAfter))
state.status = 'REJECTED_WITHOUT_MUTATION'
}
async function approveLoading(context, module, proof, recordVersion) {
const detail = await command(context, module, 'POST', `${BASE}/trips/${encodeURIComponent(proof.tripNo)}/loading-review`, {
recordVersion, decision: 'APPROVED',
}, '实际装载确认后通过整车理货审核')
proof.loadingReview = detail.loadingReview
if (detail.loadingReview?.status !== 'APPROVED') throw new Error('整车理货审核未通过,不允许发车')
proof.recordVersion = detail.trip.recordVersion
return proof.recordVersion
}
async function confirmActualLoad(context, module, proof) {
const path = `${BASE}/trips/${encodeURIComponent(proof.tripNo)}/loading`
const loading = await command(context, module, 'GET', path, undefined, '拣货后读取整车实际装载依据')
if (loading.blockReason) throw new Error(`整车未就绪:${loading.blockReason}`)
const pallets = controlledPalletMeasurements(loading.cargo, proof.stops)
const confirmed = await command(context, module, 'POST', path, {
recordVersion: loading.recordVersion, manifestHash: loading.manifestHash, pallets,
reason: '受控测试测量:每个实物托盘1托位、100kg、1m³;非生产测量',
}, '通过正式命令确认受控测试装载')
proof.confirmedLoad = confirmed.confirmedLoad
proof.recordVersion = confirmed.recordVersion
check(context, module, '装载确认返回当前明细与新版本', confirmed.confirmedLoad?.manifestHash === loading.manifestHash
&& version(confirmed.recordVersion) > version(loading.recordVersion)
&& JSON.stringify(confirmed.confirmedLoad?.pallets) === JSON.stringify([...pallets].sort((a, b) => a.warehousePalletId.localeCompare(b.warehousePalletId))))
return confirmed
}
function dispatchWorkSnapshot(row) {
return JSON.stringify({ dispatchNo: row.dispatchNo, warehouseId: row.overseasWarehouseId,
status: row.status, packageCount: row.packageCount, pickedPackageCount: row.pickedPackageCount,
shippedPackageCount: row.shippedPackageCount, remainingPackageCount: row.remainingPackageCount,
estimatedArrivalAt: row.estimatedArrivalAt, items: row.items })
}
async function verifyPreDepartureReassignment(context, module, proof, confirmed) {
const state = proof.reassignment
const path = `${BASE}/trips/${encodeURIComponent(proof.tripNo)}`
const dispatchPath = `/api/platform/v1/last-leg/dispatches/${encodeURIComponent(proof.dispatchNo)}`
const original = await command(context, module, 'GET', path, undefined, '改派前读取车次资源和计划')
const before = await command(context, module, 'GET', dispatchPath, undefined, '改派前保留实际拣货与货物明细')
check(context, module, '改派仅针对本轮已拣完但未发车的车次', original.trip?.status === 'PLANNED'
&& version(original.trip.recordVersion) === version(confirmed.recordVersion) && before.status === 'READY_TO_DEPART'
&& before.dispatchNo === proof.dispatchNo && before.pickedPackageCount > 0 && before.shippedPackageCount === 0
&& before.pickedPackageCount === before.packageCount && before.items?.length > 0
&& id(original.trip.vehicleId) === proof.vehicleId && id(original.trip.driverId) === proof.driverId)
state.originalTrip = original.trip; state.dispatchBefore = before
const marker = `SWAP-${randomUUID().slice(0, 12)}`
const vehicle = await command(context, module, 'POST', '/api/platform/v1/last-leg/master-data/vehicles', {
plateNumber: marker, model: 'Box Truck', fuelConsumptionPer100Km: 12.5, mileageKm: 0,
region: 'OVERSEAS', ownership: 'OWNED', status: 'IN_SERVICE', maxPalletSpaces: original.trip.maxPalletSpaces,
maxLoadKg: original.trip.maxLoadKg, maxVolumeCbm: original.trip.maxVolumeCbm, remark: `${context.marker} 发车前改派测试`,
}, '创建本轮等容量替换车辆')
state.replacement = { vehicleId: id(vehicle.id) }
await prepareQualifiedDriver(context, module, marker, state.replacement)
state.assignmentRequest = { recordVersion: confirmed.recordVersion, vehicleId: state.replacement.vehicleId,
driverId: state.replacement.driverId, reason: '已拣完发车前换车换司机并重新核验装载' }
const assigned = await command(context, module, 'POST', `${path}/assignment`, state.assignmentRequest, '已拣货车次正式更换车辆和司机')
state.assignedTrip = assigned.trip
check(context, module, '改派完成且保持路线与客户关联', id(assigned.trip?.vehicleId) === state.replacement.vehicleId
&& id(assigned.trip?.driverId) === state.replacement.driverId && assigned.trip.status === 'PLANNED'
&& version(assigned.trip.recordVersion) > version(confirmed.recordVersion) && JSON.stringify(assigned.trip.stops) === JSON.stringify(original.trip.stops)
&& JSON.stringify(assigned.dispatches.map(({ review, ...row }) => row)) === JSON.stringify(original.dispatches.map(({ review, ...row }) => row))
&& assigned.dispatches.every(row => row.review?.status === 'PENDING'))
const after = await command(context, module, 'GET', dispatchPath, undefined, '改派后回查实际拣货与货物明细')
state.dispatchAfter = after
check(context, module, '改派只更新车辆司机并保留拣货货量到达时间', dispatchWorkSnapshot(after) === dispatchWorkSnapshot(before)
&& after.vehiclePlate === assigned.trip.vehiclePlate && after.driverName === assigned.trip.driverName && after.driverPhone === assigned.trip.driverPhone)
const loading = await command(context, module, 'GET', `${path}/loading`, undefined, '回查改派后原装载确认失效')
check(context, module, '原装载失效但货物和客户归属保持一致', loading.confirmedLoad == null
&& version(loading.recordVersion) === version(assigned.trip.recordVersion) && loading.manifestHash === confirmed.manifestHash
&& JSON.stringify(loading.cargo) === JSON.stringify(confirmed.cargo))
state.staleLoadRequest = { recordVersion: confirmed.recordVersion, manifestHash: confirmed.manifestHash,
pallets: confirmed.confirmedLoad.pallets, reason: '旧改派前页面提交必须被拒绝' }
const stale = await context.request('POST', `${path}/loading`, state.staleLoadRequest)
const conflict = stale?.http === 409 || (stale?.http === 200 && ['409', 'A0409'].includes(String(stale?.body?.code)))
state.staleLoadResult = { http: stale?.http, code: stale?.body?.code, message: stale?.body?.message }
check(context, module, '旧版本不能恢复旧装载确认', conflict && /已被修改/.test(String(stale?.body?.message || '')))
const unchanged = await command(context, module, 'GET', `${path}/loading`, undefined, '拒绝旧版本后回查装载未发生写入')
check(context, module, '旧版本拒绝后仍未确认且版本保持不变', version(unchanged.recordVersion) === version(assigned.trip.recordVersion)
&& unchanged.confirmedLoad == null && unchanged.manifestHash === loading.manifestHash)
let reviewed = assigned
for (const binding of assigned.dispatches) {
reviewed = await command(context, module, 'POST', `${path}/dispatches/${encodeURIComponent(binding.dispatchNo)}/review`, {
recordVersion: reviewed.trip.recordVersion, decision: 'APPROVED',
}, '换车换司机后重新通过调度审核')
if (reviewed.dispatches.find(row => row.dispatchNo === binding.dispatchNo)?.review?.status !== 'APPROVED') throw new Error('改派后调度审核未通过')
export async function confirmFleetLoading(context, module, proof, pdaToken, actualMeasurements) {
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)
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 = controlledPalletMeasurements(preview.pallets, proof.stops, actualMeasurements?.pallets)
await verifyPreDepartureReassignment(context, module, proof)
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, measurements, remark: 'TEST本轮实际读取测量证据,不使用计费重量或固定值' } }
const confirmed = await command(context, module, 'POST', proof.loadingCommand.path, proof.loadingCommand.body, '原生显式装车完成', pdaToken)
check(context, module, '显式装车才进入装车后', confirmed.dispatchNo === proof.dispatchNo && confirmed.status === 'ACTIVE' && confirmed.stage === 'AFTER_LOADING'
&& confirmed.result?.confirmedLoad?.manifestHash === preview.manifestHash && version(confirmed.recordVersion) > version(preview.recordVersion))
const returned = confirmed.result.confirmedLoad.pallets
check(context, module, '实载回执逐托保留原测量且不增加货量', returned?.length === measurements.length
&& measurements.every(m => returned.some(r => id(r.warehousePalletId) === m.warehousePalletId
&& r.palletSpaces === m.palletSpaces && decimal(r.weightKg) === decimal(m.weightKg)
&& decimal(r.volumeCbm) === decimal(m.volumeCbm) && r.deliveryIndex === m.deliveryIndex)))
proof.loadingCommand.result = confirmed; proof.confirmedLoad = confirmed.result.confirmedLoad
if (pdaToken) {
const replay = await command(context, module, 'POST', `${path}/loading-complete`, proof.loadingCommand.body, '平台原键原载荷重放PDA装车')
check(context, module, '双端装车共用原结果且不增加版本', replay.replayed === true && replay.dispatchNo === confirmed.dispatchNo
&& version(replay.recordVersion) === version(confirmed.recordVersion) && JSON.stringify(replay.result) === JSON.stringify(confirmed.result))
proof.loadingCommand.platformReplay = replay
}
state.reviewedDispatches = reviewed.dispatches
proof.vehicleId = state.replacement.vehicleId; proof.driverId = state.replacement.driverId
proof.licenseFileId = state.replacement.licenseFileId
proof.recordVersion = confirmed.recordVersion; await context.checkpoint?.(); return proof.recordVersion
}
function check(context, module, label, condition, evidence = '') {
context.assert(module, label, null, condition, evidence, OPTIONS)
if (!condition) throw new Error(`${module}: ${label}失败`)
}
async function expectOccupied(context, module, path, body, resource, label) {
const response = await context.request('POST', `${path}/assignment`, body)
const conflict = response?.http === 409 || (response?.http === 200 && ['409', 'A0409'].includes(String(response?.body?.code)))
const message = String(response?.body?.message || '')
const valid = conflict && message.includes(resource) && message.includes('占用')
context.assert(module, label, response, valid, `expected occupied ${resource}`, OPTIONS)
if (!valid) throw new Error(`${module}: ${label}未证明资源占用;停止并保留本轮资源`)
}
/** Fresh-run closure proof only. Retain all commands on proof for diagnostics; do not repair old fixtures. */
/** 货物结案与车辆司机结束分别执行;不再创建退役Fleet空计划作为探针。 */
export async function finishFleetTrip(context, module, proof) {
if (!proof?.tripNo || !proof.vehicleId || !proof.driverId || !proof.warehouseId) throw new Error('缺少本轮车次与资源证明')
if (proof.closure) throw new Error('本轮车次结案检查已开始;保留原请求排查,不自动重造或改写请求')
const state = proof.closure = { status: 'CHECKING', probes: [] }
const path = `${BASE}/trips/${encodeURIComponent(proof.tripNo)}`
const inspection = await command(context, module, 'GET', `${path}/completion`, undefined, '核对本车所有货物结案条件')
if (proof.closure) throw new Error('本轮车次结案检查已开始;保留原请求排查,不自动重造')
const state = proof.closure = { status: 'CHECKING', occupancy: [] }, path = `${BASE}/trips/${encodeURIComponent(proof.tripNo)}`
const inspection = await command(context, module, 'GET', `${path}/completion`, undefined, '核对本车货物结案条件')
check(context, module, '全部货物具有本车结案依据', inspection.canComplete === true && inspection.outcomes?.length > 0, inspection.blockReason || '')
state.completeRequest = { recordVersion: inspection.recordVersion, idempotencyKey: `${proof.tripNo}:COMPLETE`, reason: '本轮实际签收及退回终态核对' }
const completed = await command(context, module, 'POST', `${path}/complete`, state.completeRequest, '正式结案车次货物')
check(context, module, '结案记录原运输完成时间且尚未声明资源释放', completed.trip?.status === 'COMPLETED'
&& Boolean(completed.trip.completedAt) && completed.trip.vehicleRelease == null && completed.trip.driverRelease == null)
const replay = await command(context, module, 'POST', `${path}/complete`, state.completeRequest, '同键重放货物结案')
check(context, module, '结案重放没有新增版本或改变完成时间', replay.trip?.recordVersion === completed.trip.recordVersion && replay.trip.completedAt === completed.trip.completedAt)
async function probe(index) {
const request = { warehouseId: proof.warehouseId, idempotencyKey: `${proof.tripNo}:RELEASE-PROBE:${index}`,
plannedDepartureAt: new Date(Date.now() + 3600000).toISOString(), expectedDriverEndAt: new Date(Date.now() + 48 * 3600000).toISOString(),
stops: proof.stops, remark: '本轮资源占用验收计划,不关联货物或执行发车' }
const evidence = { createRequest: request }; state.probes.push(evidence)
const created = await command(context, module, 'POST', `${BASE}/trips`, request, `建立资源占用核对计划${index}`)
evidence.tripNo = created.trip.tripNo; evidence.recordVersion = created.trip.recordVersion
return { evidence, path: `${BASE}/trips/${encodeURIComponent(evidence.tripNo)}` }
const completed = await command(context, module, 'POST', `${path}/complete`, state.completeRequest, '正式结案货物')
check(context, module, '结案未自动结束车辆或司机', completed.trip?.status === 'COMPLETED' && Boolean(completed.trip.completedAt) && completed.trip.vehicleRelease == null && completed.trip.driverRelease == null)
const replay = await command(context, module, 'POST', `${path}/complete`, state.completeRequest, '原键重放结案')
check(context, module, '结案重放不增加版本或改变完成时间', version(replay.trip.recordVersion) === version(completed.trip.recordVersion) && replay.trip.completedAt === completed.trip.completedAt)
async function occupancy(kind, expectedOwner) {
const resourceId = kind === 'VEHICLE' ? proof.vehicleId : proof.driverId
const rows = await command(context, module, 'GET', `${path}/resource-occupancy?${new URLSearchParams({ kind, resourceIds: resourceId })}`, undefined, `读取${kind}独立占用`)
state.occupancy.push({ kind, rows })
// available对本车为true,必须核对occupiedTripNo的真实拥有者。
check(context, module, `${kind}占用拥有者符合独立结束阶段`, rows.length === 1 && id(rows[0].resourceId) === id(resourceId) && (rows[0].occupiedTripNo ?? null) === expectedOwner)
}
const first = await probe(1)
const assign = { recordVersion: first.evidence.recordVersion, vehicleId: proof.vehicleId, driverId: proof.driverId, reason: '验证本轮资源占用' }
await expectOccupied(context, module, first.path, assign, '车辆', '货物结案后车辆仍不能被另一车次占用')
await occupancy('VEHICLE', proof.tripNo); await occupancy('DRIVER', proof.tripNo)
const endLocation = proof.stops?.at(-1)?.name
check(context, module, '测试任务具有明确结束地点', Boolean(endLocation))
// These controlled fixtures end at the signed final stop and contain no return-driving task.
state.vehicleReleaseRequest = { recordVersion: completed.trip.recordVersion, resourceKind: 'VEHICLE',
actualEndAt: completed.trip.completedAt, endLocation, vehicleStatus: 'IN_SERVICE',
idempotencyKey: `${proof.tripNo}:RELEASE-VEHICLE`, reason: '受控测试车辆在末站结束,无返程安排' }
const vehicleReleased = await command(context, module, 'POST', `${path}/resources/release`, state.vehicleReleaseRequest, '单独结束测试车辆任务')
check(context, module, '只释放车辆仍保留司机任务', Boolean(vehicleReleased.trip?.vehicleRelease) && vehicleReleased.trip.driverRelease == null)
await expectOccupied(context, module, first.path, assign, '司机', '车辆已释放但司机仍不能被另一车次占用')
state.driverReleaseRequest = { recordVersion: vehicleReleased.trip.recordVersion, resourceKind: 'DRIVER',
actualEndAt: completed.trip.completedAt, endLocation, vehicleStatus: null,
idempotencyKey: `${proof.tripNo}:RELEASE-DRIVER`, reason: '受控测试司机在末站结束,无返程安排' }
const driverReleased = await command(context, module, 'POST', `${path}/resources/release`, state.driverReleaseRequest, '单独结束测试司机任务')
check(context, module, '车辆和司机各自保留真实任务结束记录', Boolean(driverReleased.trip?.vehicleRelease && driverReleased.trip.driverRelease))
const assigned = await command(context, module, 'POST', `${first.path}/assignment`, assign, '释放后正式再次分配相同车辆司机')
first.evidence.recordVersion = assigned.trip.recordVersion
check(context, module, '释放后的两项资源可被下一计划使用', id(assigned.trip?.vehicleId) === id(proof.vehicleId) && id(assigned.trip?.driverId) === id(proof.driverId))
const oldVehicleReplay = await command(context, module, 'POST', `${path}/resources/release`, state.vehicleReleaseRequest, '资源再分配后重放旧车辆结束请求')
const oldDriverReplay = await command(context, module, 'POST', `${path}/resources/release`, state.driverReleaseRequest, '资源再分配后重放旧司机结束请求')
check(context, module, '原结束请求重放不增加旧车次版本', oldVehicleReplay.trip.recordVersion === driverReleased.trip.recordVersion && oldDriverReplay.trip.recordVersion === driverReleased.trip.recordVersion)
const second = await probe(2)
await expectOccupied(context, module, second.path, { ...assign, recordVersion: second.evidence.recordVersion }, '车辆', '旧结束重放不能释放下一计划的车辆占用')
// A different free fixture vehicle isolates the driver claim from the vehicle claim.
const spare = await command(context, module, 'POST', '/api/platform/v1/last-leg/master-data/vehicles', {
plateNumber: `PROBE-${randomUUID().slice(0,12)}`, model: 'Box Truck', fuelConsumptionPer100Km: 12.5, mileageKm: 0,
region: 'OVERSEAS', ownership: 'OWNED', status: 'IN_SERVICE', maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90, remark: `${context.marker} 仅用于本轮司机占用负向验证`,
}, '建立司机占用隔离用测试车辆')
state.spareVehicleId = id(spare.id)
await expectOccupied(context, module, second.path, { ...assign, vehicleId: state.spareVehicleId, recordVersion: second.evidence.recordVersion }, '司机', '旧结束重放不能释放下一计划的司机占用')
for (const item of [second, first]) {
const cancelled = await command(context, module, 'POST', `${item.path}/cancel`, { recordVersion: item.evidence.recordVersion, reason: '资源占用核对完成,取消无货测试计划' }, '正式取消无货核对计划')
check(context, module, '核对计划取消且保留历史', cancelled.trip?.status === 'CANCELLED')
item.evidence.finalStatus = cancelled.trip.status
}
const final = await command(context, module, 'GET', path, undefined, '回查最终车次结案与资源结束记录')
check(context, module, '车次结案及两项资源结束保持一致', final.trip?.status === 'COMPLETED' && Boolean(final.trip.vehicleRelease && final.trip.driverRelease))
state.status = 'CARGO_COMPLETED_AND_RESOURCES_RELEASED'
state.finalTrip = final.trip
return state
if (!endLocation) throw new Error('TEST运输缺少明确末站结束地点')
state.vehicleReleaseRequest = { recordVersion: completed.trip.recordVersion, resourceKind: 'VEHICLE', actualEndAt: completed.trip.completedAt,
endLocation, vehicleStatus: 'IN_SERVICE', idempotencyKey: `${proof.tripNo}:RELEASE-VEHICLE`, reason: 'TEST车辆末站任务独立结束,无返程安排' }
const vehicleReleased = await command(context, module, 'POST', `${path}/resources/release`, state.vehicleReleaseRequest, '独立结束TEST车辆任务')
check(context, module, '只结束车辆仍保留司机任务', Boolean(vehicleReleased.trip?.vehicleRelease) && vehicleReleased.trip.driverRelease == null)
await occupancy('VEHICLE', null); await occupancy('DRIVER', proof.tripNo)
state.driverReleaseRequest = { recordVersion: vehicleReleased.trip.recordVersion, resourceKind: 'DRIVER', actualEndAt: completed.trip.completedAt,
endLocation, vehicleStatus: null, idempotencyKey: `${proof.tripNo}:RELEASE-DRIVER`, reason: 'TEST司机末站任务独立结束,无返程安排' }
const released = await command(context, module, 'POST', `${path}/resources/release`, state.driverReleaseRequest, '独立结束TEST司机任务')
check(context, module, '两项资源各自保留结束记录', Boolean(released.trip?.vehicleRelease && released.trip.driverRelease))
await occupancy('VEHICLE', null); await occupancy('DRIVER', null)
// 正式空排车是新资源拥有者;不恢复已退役的独立Fleet建单或手工关联。
state.nextDispatchRequest = { idempotencyKey: key(proof, 'NEXT_NATIVE_DISPATCH'), warehouseId: proof.warehouseId,
vehicle: { mode: 'MASTER', vehicleId: proof.vehicleId },
driver: { mode: 'MASTER', driverId: proof.driverId, evidence: { expectedTaskEndAt: new Date(Date.now() + 48 * 3600000).toISOString() } },
plannedDepartureAt: new Date(Date.now() + 3600000).toISOString(), estimatedArrivalAt: null,
remark: `${context.marker} TEST本轮空排车验证旧结束键不能释放新占用`, cargo: [], batchAllocations: [] }
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 }
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)
const oldVehicle = await command(context, module, 'POST', `${path}/resources/release`, state.vehicleReleaseRequest, '原键重放车辆结束')
const oldDriver = await command(context, module, 'POST', `${path}/resources/release`, state.driverReleaseRequest, '原键重放司机结束')
check(context, module, '独立结束重放不增加版本', version(oldVehicle.trip.recordVersion) === version(released.trip.recordVersion) && version(oldDriver.trip.recordVersion) === version(released.trip.recordVersion))
await occupancy('VEHICLE', state.nextDispatch.tripNo); await occupancy('DRIVER', state.nextDispatch.tripNo)
const nextPath = `${DISPATCH}/${encodeURIComponent(state.nextDispatch.dispatchNo)}`
state.nextDispatchBeforeCancel = await command(context, module, 'GET', nextPath, undefined, '取消前核齐本轮新空排车当前版本和零实物')
state.nextDispatchItems = await command(context, module, 'GET', `${nextPath}/items`, undefined, '取消前核齐本轮新空排车零货物来源')
const next = state.nextDispatchBeforeCancel
check(context, module, '仅取消本轮已确定且从未执行的原生空单', next.dispatchNo === state.nextDispatch.dispatchNo
&& next.status === 'ACTIVE' && next.stage === 'BEFORE_LOADING' && safePackageCount(next.counts?.itemCount) === 0
&& safePackageCount(next.counts?.packageCount) === 0 && safePackageCount(next.counts?.pickedPackageCount) === 0 && safePackageCount(next.counts?.shippedPackageCount) === 0
&& version(next.recordVersion) === version(state.nextDispatchResult.recordVersion)
&& state.nextDispatchItems.dispatchNo === next.dispatchNo && state.nextDispatchItems.items?.length === 0
&& version(state.nextDispatchItems.recordVersion) === version(next.recordVersion))
state.nextDispatchCancelRequest = { recordVersion: next.recordVersion,
idempotencyKey: key(proof, 'CANCEL_NEXT_NATIVE_DISPATCH'), reason: 'TEST原生空排车占用核对完成,保留取消历史' }
const cancelled = await command(context, module, 'POST', `${nextPath}/cancel`,
state.nextDispatchCancelRequest, '正式取消本轮未执行空排车')
check(context, module, '空排车取消并保留历史', cancelled.dispatchNo === state.nextDispatch.dispatchNo && cancelled.status === 'CANCELLED')
state.nextDispatch.finalStatus = cancelled.status
await occupancy('VEHICLE', null); await occupancy('DRIVER', null)
state.finalTrip = (await command(context, module, 'GET', path, undefined, '回查结案及资源结束记录')).trip
check(context, module, '最终结案和资源结束保持一致', state.finalTrip?.status === 'COMPLETED' && Boolean(state.finalTrip.vehicleRelease && state.finalTrip.driverRelease))
state.subsequentTripClaimProtection = 'VERIFIED_NATIVE_EMPTY_DISPATCH'
state.status = 'CARGO_COMPLETED_AND_RESOURCES_RELEASED'; await context.checkpoint?.(); return state
}