import { createHash, randomUUID } from 'node:crypto' const BASE = '/api/platform/v1/last-leg/fleet' const OPTIONS = { scenario: 'fleet-physical-trip-departure', deploymentBlocker: true } const id = value => value == null ? '' : String(value) const version = value => { 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) 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. */ 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 }) } 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 } async function prepareQualifiedDriver(context, module, marker, proof) { 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`, }, '建立本轮司机证照档案') 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('测试证照上传失败,不允许伪造已审核资格') 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('测试司机未审核通过,停止派车验证') } 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 }) } if (!pallets.size) throw new Error('没有实际装载托盘') return [...pallets.values()] } export async function confirmFleetLoading(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 } 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('改派后调度审核未通过') } state.reviewedDispatches = reviewed.dispatches proof.vehicleId = state.replacement.vehicleId; proof.driverId = state.replacement.driverId proof.licenseFileId = state.replacement.licenseFileId } 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. */ 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, '核对本车所有货物结案条件') 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 first = await probe(1) const assign = { recordVersion: first.evidence.recordVersion, vehicleId: proof.vehicleId, driverId: proof.driverId, reason: '验证本轮资源占用' } await expectOccupied(context, module, first.path, assign, '车辆', '货物结案后车辆仍不能被另一车次占用') 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 }