import axios from 'axios' import { http, unwrapApiResponse, ApiResponseError, type ApiEnvelope } from '@/shared/api/http' import { outsourcedBusinessDecimal, outsourcedBusinessVersion, outsourcedBusinessPhotoRoles, type OutsourcedBusinessOperations, type OutsourcedBusinessRow, type OutsourcedBusinessDetail, type OutsourcedBusinessList, type OutsourcedBusinessQuery, type OutsourcedBusinessSave, type OutsourcedBusinessBind, type OutsourcedBusinessPhoto, type OutsourcedBusinessPhotoSlot, type OutsourcedBusinessCommand, type OutsourcedBusinessHistory, type OutsourcedBusinessPage } from '../outsourcedBusinessTypes' const base = '/platform/v1/last-leg/fleet/outsourced-business' const path = (no: string) => `${base}/${encodeURIComponent(no)}` async function get(url: string, params?: object): Promise { return unwrapApiResponse(await http.get, ApiEnvelope>(url, { params })) } function count(value: unknown): number { const text = outsourcedBusinessVersion(value); const result = Number(text); if (!Number.isSafeInteger(result)) throw new Error('运营记录数量超出精确范围'); return result } function decimal(value: string | null, scale: number, precision: number): string | null { if (value == null) return null if (typeof value !== 'string' || outsourcedBusinessDecimal(value, scale, precision) !== value) throw new Error('运营金额或测量必须使用精确十进制字符串') return value } function operations(value: OutsourcedBusinessOperations): OutsourcedBusinessOperations { if ('actualDistanceMiles' in value || 'fuelConsumptionLitresPer100Km' in value || 'actualFuelLitres' in value) throw new Error('外配运营信息不支持里程或油耗') if (value.money && !['CNY','USD'].includes(value.money.currency)) throw new Error('运营金额币种无效') const amount = value.money ? decimal(value.money.amount, 2, 18) : null if (value.money && amount == null) throw new Error('承运金额与币种必须同时登记') return { money: value.money ? { currency: value.money.currency, amount: amount! } : null, arrivals: value.arrivals } } function requireSlot(slot: OutsourcedBusinessPhotoSlot) { if (!outsourcedBusinessPhotoRoles.some(role => role.slot === slot)) throw new Error('外配任务不支持该照片分类') } function row(value: OutsourcedBusinessRow): OutsourcedBusinessRow { if (!value.dispatchNo || !['AFTER_LOADING','DELIVERING','DELIVERED'].includes(value.stage) || !['OWNED','DISPATCHED'].includes(value.vehicle?.ownership)) throw new Error('外配排车来源或阶段不完整,请重新读取') return { ...value, operations: operations(value.operations), unitPrice: decimal(value.unitPrice,2,18), recordVersion: outsourcedBusinessVersion(value.recordVersion), dispatchRecordVersion: outsourcedBusinessVersion(value.dispatchRecordVersion), actualPalletCount: value.actualPalletCount == null ? null : count(value.actualPalletCount), photoSummary: { loadingCount: count(value.photoSummary.loadingCount) } } } function detail(value: OutsourcedBusinessDetail): OutsourcedBusinessDetail { if (value.row.dispatchNo !== value.dispatch.dispatchNo) throw new Error('运营资料与原排车单不一致'); return { ...value, row: row(value.row) } } function command(value: OutsourcedBusinessCommand): OutsourcedBusinessCommand { return { ...value, recordVersion: value.recordVersion == null ? null : outsourcedBusinessVersion(value.recordVersion) } } export async function listOutsourcedBusiness(query: OutsourcedBusinessQuery): Promise { const value = await get(base, query) return { ...value, records: value.records.map(row), page: count(value.page), size: count(value.size), total: count(value.total), summary: { dispatchCount: count(value.summary.dispatchCount), afterLoadingCount: count(value.summary.afterLoadingCount), deliveringCount: count(value.summary.deliveringCount), deliveredCount: count(value.summary.deliveredCount) } } } export async function getOutsourcedBusiness(no: string) { const value = detail(await get(path(no))); if (value.row.dispatchNo !== no) throw new Error('运营资料与当前排车业务号不一致'); return value } export async function listOutsourcedBusinessHistory(no: string, page: number, size: number): Promise> { const value = await get>(`${path(no)}/history`, { page, size }) return { ...value, records: value.records.map(record => ({ ...record, operations: record.operations ? operations(record.operations) : null, recordVersion: record.recordVersion == null ? null : outsourcedBusinessVersion(record.recordVersion), dispatchRecordVersion: record.dispatchRecordVersion == null ? null : outsourcedBusinessVersion(record.dispatchRecordVersion) })), page: count(value.page), size: count(value.size), total: count(value.total) } } export const getOutsourcedBusinessCommand = (no: string, key: string) => get(`${path(no)}/commands/${encodeURIComponent(key)}`).then(command) export async function saveOutsourcedBusinessOperations(no: string, body: OutsourcedBusinessSave) { outsourcedBusinessVersion(body.recordVersion) operations(body.operations) const value = detail(unwrapApiResponse(await http.put, ApiEnvelope>(`${path(no)}/operations`, body))) if (value.row.dispatchNo !== no) throw new Error('运营保存结果与原排车单不一致') return value } export async function uploadOutsourcedBusinessPhoto(no: string, slot: OutsourcedBusinessPhotoSlot, file: File, key: string) { requireSlot(slot) if (!['image/jpeg','image/png'].includes(file.type) || file.size <= 0) throw new Error('请选择有效的JPG或PNG图片') const form = new FormData(); form.append('file', file); form.append('idempotencyKey', key) const value = unwrapApiResponse(await http.post, ApiEnvelope>(`${path(no)}/photos/${slot}/uploads`, form)) if (value.slot !== slot || !value.fileId) throw new Error('上传文件与原照片分类不一致') return value } export async function bindOutsourcedBusinessPhoto(no: string, slot: OutsourcedBusinessPhotoSlot, body: OutsourcedBusinessBind) { requireSlot(slot) outsourcedBusinessVersion(body.recordVersion) const value = detail(unwrapApiResponse(await http.put, ApiEnvelope>(`${path(no)}/photos/${slot}`, body))) if (value.row.dispatchNo !== no) throw new Error('照片绑定结果与原排车单不一致') return value } async function downloadError(payload: unknown, status: number) { let value = payload if (payload instanceof Blob && payload.type.includes('json')) { try { value = JSON.parse(await payload.text()) as unknown } catch { throw new Error('图片接口返回了无法识别的错误') } } if (value && typeof value === 'object' && 'code' in value) { const error = value as ApiEnvelope; throw new ApiResponseError(error.message || '图片读取失败', error.code, status, error.data, error.requestId, error.traceId) } } export async function getOutsourcedBusinessPhotoContent(no: string, photo: OutsourcedBusinessPhoto, revisionVersion?: string): Promise { requireSlot(photo.slot) if (revisionVersion != null && BigInt(outsourcedBusinessVersion(revisionVersion)) < 1n) throw new Error('历史图片版本必须大于零') try { const blob = await http.get(`${path(no)}/photos/${photo.slot}/files/${encodeURIComponent(photo.fileId)}/content`, { responseType: 'blob', params: revisionVersion == null ? undefined : { revisionVersion: outsourcedBusinessVersion(revisionVersion) } }) await downloadError(blob, 200) if (!(blob instanceof Blob) || !['image/jpeg','image/png'].includes(blob.type) || !blob.size) throw new Error('图片内容为空或格式无效') return blob } catch (cause) { if (axios.isAxiosError(cause)) await downloadError(cause.response?.data, cause.response?.status || 0); throw cause } }