/** * OMS 订单接口(Swagger:OMS-Order,与 admin 同源) */ import { axiosInstance } from '@/api/rbac' import { resolveAdminApiPath } from '@/utils/adminApi' import { extractDraftFailFromBody, parseOmsDraftMarker } from '@/utils/parcelCreateFailGuide' function orderUrl(path: string): string { return resolveAdminApiPath(path) } export interface OrderListRequest { page: number pageSize: number /** all 或各状态英文 key,与文档一致 */ status?: string /** 子状态:all / pending / processed(问题件、已理赔 Tab) */ handleStatus?: string /** 订单来源:all / manual / pickup 等 */ orderSource?: string /** 管理端可选客户筛选;不传则查全部 */ customerId?: number keyword?: string createTimeStart?: string createTimeEnd?: string } export interface OrderHandleStatusCounts { pending: number processed: number total?: number } export interface OrderListResult { items: any[] total: number } export interface OrderStatusCountsResult { total: number counts: Record handleCounts?: Record } function assertBusinessOk(body: any) { if (body == null) throw new Error('空响应') const code = body.code ?? body.Code const ok = code === undefined || code === null || code === 0 || code === 200 || code === '0' || code === '200' || body.success === true || body.Success === true if (!ok) { throw new Error(body.msg ?? body.Msg ?? body.message ?? body.Message ?? '请求失败') } } /** 兼容 data 包裹或顶层直出(camel/Pascal) */ function unwrapApiData>(body: any): T { if (body == null) return {} as T const wrapped = body.data ?? body.Data if (wrapped !== undefined && wrapped !== null) { return wrapped as T } return body as T } /** 取出分页载荷(兼容 data 包裹或顶层直出、items/list/records、camel/Pascal) */ function parseOrderListPayload(resBody: any): OrderListResult { assertBusinessOk(resBody) const inner = resBody.data ?? resBody.Data ?? resBody if (Array.isArray(inner)) { return { items: inner, total: inner.length } } const payload = inner && typeof inner === 'object' && !Array.isArray(inner) ? inner : resBody const items = payload.items ?? payload.Items ?? payload.list ?? payload.List ?? payload.records ?? payload.Records ?? payload.rows ?? payload.Rows ?? (Array.isArray(payload) ? payload : []) const totalRaw = payload.total ?? payload.Total ?? payload.totalCount ?? payload.TotalCount ?? payload.count ?? payload.Count const list = Array.isArray(items) ? items : [] const total = typeof totalRaw === 'number' ? totalRaw : totalRaw != null && totalRaw !== '' ? Number(totalRaw) : list.length return { items: list, total: Number.isFinite(total) ? total : 0, } } /** 分页查询订单列表 POST /order/list */ export async function postOrderList(params: OrderListRequest): Promise { const body: OrderListRequest = { page: params.page, pageSize: params.pageSize, status: params.status ?? 'all', keyword: params.keyword ?? '', } if (params.handleStatus?.trim()) { body.handleStatus = params.handleStatus.trim() } if (params.orderSource?.trim()) { body.orderSource = params.orderSource.trim() } if (params.customerId != null && params.customerId > 0) { body.customerId = params.customerId } if (params.createTimeStart?.trim()) { body.createTimeStart = params.createTimeStart.trim() } if (params.createTimeEnd?.trim()) { body.createTimeEnd = params.createTimeEnd.trim() } const res = await axiosInstance.post(orderUrl('/order/list'), body) return parseOrderListPayload(res.data) } /** 各状态订单数量聚合 GET /order/status-counts(单次查询,非逐状态分页) */ export async function getOrderStatusCounts(): Promise { const res = await axiosInstance.get(orderUrl('/order/status-counts')) assertBusinessOk(res.data) const inner = res.data?.data ?? res.data?.Data ?? res.data ?? {} const totalRaw = inner.total ?? inner.Total ?? 0 const counts = inner.counts ?? inner.Counts ?? {} const handleCountsRaw = inner.handleCounts ?? inner.HandleCounts ?? {} const handleCounts: Record = {} if (handleCountsRaw && typeof handleCountsRaw === 'object') { for (const [key, value] of Object.entries(handleCountsRaw as Record)) { handleCounts[key] = { pending: Number(value?.pending ?? value?.Pending ?? 0) || 0, processed: Number(value?.processed ?? value?.Processed ?? 0) || 0, total: Number(value?.total ?? value?.Total ?? 0) || undefined, } } } return { total: Number(totalRaw) || 0, counts: typeof counts === 'object' && counts !== null ? counts : {}, handleCounts, } } /** 订单详情 GET /order/detail?id= */ export async function getOrderDetail(id: number | string): Promise { const res = await axiosInstance.get(orderUrl('/order/detail'), { params: { id }, }) assertBusinessOk(res.data) return res.data?.data ?? res.data?.Data ?? res.data } export interface OrderIdRequest { id: number | string } export interface OrderExportRequest { status?: string keyword?: string createTimeStart?: string createTimeEnd?: string } export interface OrderPrintResult { printUrl?: string } export interface OrderExportResult { downloadUrl?: string expireAt?: string } async function postOrderById(path: string, body: OrderIdRequest): Promise { const res = await axiosInstance.post(orderUrl(path), { id: body.id }) assertBusinessOk(res.data) } /** POST /order/pay */ export function postOrderPay(body: OrderIdRequest) { return postOrderById('/order/pay', body) } /** POST /order/pre-declare */ export async function postOrderPreDeclare(body: OrderIdRequest): Promise { try { const res = await axiosInstance.post(orderUrl('/order/pre-declare'), { id: body.id }) const resBody = res.data if (!isBusinessOkBody(resBody)) { throwParcelCreateBusinessError(resBody, '预报失败') } const extracted = extractDraftFailFromBody(resBody) if (extracted.meta?.draftSaved) { throwParcelCreateBusinessError(resBody, extracted.cleanMessage || '预报失败') } } catch (e: any) { if (e instanceof ParcelCreateBusinessError) throw e const data = e?.response?.data if (data) throwParcelCreateBusinessError(data, e?.message || '预报失败') throw e } } /** POST /order/intercept */ export function postOrderIntercept(body: OrderIdRequest) { return postOrderById('/order/intercept', body) } /** POST /order/cancel-intercept */ export function postOrderCancelIntercept(body: OrderIdRequest) { return postOrderById('/order/cancel-intercept', body) } /** POST /order/delete */ export function postOrderDelete(body: OrderIdRequest) { return postOrderById('/order/delete', body) } export interface OrderTrackUpsertResult { skipped?: boolean skipReason?: string success?: boolean url?: string requestJson?: string curlCommand?: string statusCode?: number responseBody?: string } /** POST /order/track-upsert-small-bag → 同步调用轨迹 UpsertSmallBag(联调/手动触发) */ export async function postOrderTrackUpsertSmallBag(body: OrderIdRequest): Promise { const res = await axiosInstance.post(orderUrl('/order/track-upsert-small-bag'), { id: body.id }) assertBusinessOk(res.data) const d = res.data?.data ?? res.data?.Data ?? res.data ?? {} return { skipped: d.skipped ?? d.Skipped, skipReason: d.skipReason ?? d.SkipReason, success: d.success ?? d.Success, url: d.url ?? d.Url, requestJson: d.requestJson ?? d.RequestJson, curlCommand: d.curlCommand ?? d.CurlCommand, statusCode: d.statusCode ?? d.StatusCode, responseBody: d.responseBody ?? d.ResponseBody, } } export interface OrderDownstreamForwardResult { success?: boolean supplierCode?: string url?: string requestJson?: string statusCode?: number responseBody?: string downstreamServiceNo?: string trackNo?: string } /** POST /order/downstream-forward-create-one → 后端查订单表组装并转发 createOrderOne */ export async function postOrderDownstreamForwardCreateOne( body: OrderIdRequest ): Promise { const res = await axiosInstance.post(orderUrl('/order/downstream-forward-create-one'), { id: body.id }) assertBusinessOk(res.data) const d = res.data?.data ?? res.data?.Data ?? res.data ?? {} return { success: d.success ?? d.Success, supplierCode: d.supplierCode ?? d.SupplierCode, url: d.url ?? d.Url, requestJson: d.requestJson ?? d.RequestJson, statusCode: d.statusCode ?? d.StatusCode, responseBody: d.responseBody ?? d.ResponseBody, downstreamServiceNo: d.downstreamServiceNo ?? d.DownstreamServiceNo, trackNo: d.trackNo ?? d.TrackNo, } } export interface OrderBatchActionFailure { orderId?: number OrderId?: number serviceNo?: string ServiceNo?: string reason?: string Reason?: string } export interface OrderBatchActionResult { successCount?: number SuccessCount?: number failures?: OrderBatchActionFailure[] Failures?: OrderBatchActionFailure[] } export interface OrderBatchJobOutput { jobId?: number JobId?: number action?: string Action?: string status?: string Status?: string total?: number Total?: number successCount?: number SuccessCount?: number failCount?: number FailCount?: number pendingCount?: number PendingCount?: number runningCount?: number RunningCount?: number cancelledCount?: number CancelledCount?: number startedAt?: string StartedAt?: string finishedAt?: string FinishedAt?: string createTime?: string CreateTime?: string } export interface OrderBatchJobItemOutput { id?: number Id?: number orderId?: number OrderId?: number serviceNo?: string ServiceNo?: string clientNo?: string ClientNo?: string rowNumber?: number RowNumber?: number action?: string Action?: string status?: string Status?: string failReason?: string FailReason?: string resultJson?: string ResultJson?: string resultMessage?: string ResultMessage?: string retryCount?: number RetryCount?: number startedAt?: string StartedAt?: string finishedAt?: string FinishedAt?: string } export type ParsedOrderBatchJob = { jobId: number action: string status: string total: number successCount: number failCount: number pendingCount: number runningCount: number cancelledCount: number templateId?: number taskName?: string startedAt?: string finishedAt?: string createTime?: string } export type ParsedOrderBatchJobItem = { id: number orderId: number serviceNo?: string clientNo?: string rowNumber?: number action?: string status: string failReason?: string resultJson?: string resultMessage?: string retryCount: number startedAt?: string finishedAt?: string } function parseOrderBatchJob(raw: any): ParsedOrderBatchJob { const d = unwrapApiData(raw) const total = Number(d.total ?? d.Total ?? 0) const successCount = Number(d.successCount ?? d.SuccessCount ?? 0) const failCount = Number(d.failCount ?? d.FailCount ?? 0) const pendingCount = Number(d.pendingCount ?? d.PendingCount ?? 0) const runningCount = Number(d.runningCount ?? d.RunningCount ?? 0) const cancelledCount = Number(d.cancelledCount ?? d.CancelledCount ?? 0) return { jobId: Number(d.jobId ?? d.JobId ?? 0), action: String(d.action ?? d.Action ?? ''), status: String(d.status ?? d.Status ?? ''), total, successCount, failCount, pendingCount, runningCount, cancelledCount, templateId: (() => { const raw = d.templateId ?? d.TemplateId return raw != null && raw !== '' ? Number(raw) : undefined })(), taskName: d.taskName ?? d.TaskName, startedAt: d.startedAt ?? d.StartedAt, finishedAt: d.finishedAt ?? d.FinishedAt, createTime: d.createTime ?? d.CreateTime, } } function parseBatchJobItemClientNo(raw: any): string | undefined { const direct = raw.clientNo ?? raw.ClientNo if (direct) return String(direct).trim() || undefined const resultJson = raw.resultJson ?? raw.ResultJson if (!resultJson) return undefined try { const obj = JSON.parse(resultJson) const value = obj.clientNo ?? obj.ClientNo return value ? String(value).trim() : undefined } catch { return undefined } } function parseOrderBatchJobItem(raw: any): ParsedOrderBatchJobItem { const rowNumberRaw = raw.rowNumber ?? raw.RowNumber const retryCountRaw = raw.retryCount ?? raw.RetryCount const resultMessage = raw.resultMessage ?? raw.ResultMessage return { id: Number(raw.id ?? raw.Id ?? 0), orderId: Number(raw.orderId ?? raw.OrderId ?? 0), serviceNo: raw.serviceNo ?? raw.ServiceNo, clientNo: parseBatchJobItemClientNo(raw), rowNumber: rowNumberRaw != null && rowNumberRaw !== '' ? Number(rowNumberRaw) : undefined, action: raw.action ?? raw.Action, status: String(raw.status ?? raw.Status ?? ''), failReason: raw.failReason ?? raw.FailReason, resultJson: raw.resultJson ?? raw.ResultJson, resultMessage: resultMessage ? String(resultMessage) : undefined, retryCount: Number(retryCountRaw ?? 0) || 0, startedAt: raw.startedAt ?? raw.StartedAt, finishedAt: raw.finishedAt ?? raw.FinishedAt, } } export function buildBatchJobProgressText(job: { successCount: number failCount: number total: number pendingCount?: number runningCount?: number cancelledCount?: number }): string { const parts = [`成功 ${job.successCount}/${job.total}`] if (job.failCount > 0) parts.push(`失败 ${job.failCount}`) if ((job.pendingCount ?? 0) > 0) parts.push(`待处理 ${job.pendingCount}`) if ((job.runningCount ?? 0) > 0) parts.push(`执行中 ${job.runningCount}`) if ((job.cancelledCount ?? 0) > 0) parts.push(`已取消 ${job.cancelledCount}`) return parts.join(',') } export function formatBatchJobSummaryMessage( job: ParsedOrderBatchJob, options?: { timedOut?: boolean; jobId?: number }, ): string { const progress = buildBatchJobProgressText(job) if (options?.timedOut) { const jobRef = options.jobId ?? job.jobId return `批量任务 #${jobRef} 等待超时,当前进度:${progress}。请稍后在批量任务页查看完整结果。` } if (job.failCount > 0) { return `完成:${progress}` } return `全部成功:${progress}` } export function getBatchJobItemDisplayNo( item: { clientNo?: string; serviceNo?: string; orderId?: number }, _action?: string, ): string { if (item.clientNo) return item.clientNo if (item.serviceNo) return item.serviceNo return item.orderId ? String(item.orderId) : '-' } export function resolveBatchJobItemRowLabel( row: any, item: { orderId: number; clientNo?: string; serviceNo?: string }, ): string { return getBatchJobItemDisplayNo({ clientNo: item.clientNo || row?.customerOrderNo || row?.clientNo, serviceNo: item.serviceNo || row?.waybillNo || row?.serviceNo, orderId: item.orderId, }) } export function parseBatchJobPrintUrl(item: { resultJson?: string }): string { if (!item.resultJson) return '' try { const obj = JSON.parse(item.resultJson) return String(obj.printUrl ?? obj.PrintUrl ?? '').trim() } catch { return '' } } export type ParsedBatchTrackUpsertPayload = { success?: boolean message?: string trackUpsert?: { skipped?: boolean skipReason?: string requestUrl?: string requestBody?: unknown responseStatusCode?: number responseBody?: unknown responseBodyRaw?: string } downstreamForward?: ParsedBatchDownstreamForwardDetail } export type ParsedBatchDownstreamForwardDetail = { supplierCode?: string requestUrl?: string requestBody?: unknown requestBodyRaw?: string responseStatusCode?: number responseBody?: unknown responseBodyRaw?: string businessCode?: string businessMessage?: string downstreamServiceNo?: string trackNo?: string } export function parseBatchJobTrackUpsertPayload(item: { resultJson?: string }): ParsedBatchTrackUpsertPayload | null { if (!item.resultJson) return null try { return JSON.parse(item.resultJson) as ParsedBatchTrackUpsertPayload } catch { return null } } function stringifyJsonBlock(value: unknown): string { if (value == null) return '' if (typeof value === 'string') return value try { return JSON.stringify(value, null, 2) } catch { return String(value) } } function formatDownstreamForwardDetailLines(forward: ParsedBatchDownstreamForwardDetail): string[] { const lines: string[] = [] if (forward.supplierCode) lines.push(`supplierCode:${forward.supplierCode}`) if (forward.downstreamServiceNo) lines.push(`服务商单号:${forward.downstreamServiceNo}`) if (forward.trackNo) lines.push(`跟踪号:${forward.trackNo}`) if (forward.requestUrl) lines.push(`请求:${forward.requestUrl}`) const requestBody = stringifyJsonBlock(forward.requestBody) || forward.requestBodyRaw || '' if (requestBody) lines.push(`请求体:\n${requestBody}`) if (forward.responseStatusCode != null) lines.push(`HTTP:${forward.responseStatusCode}`) if (forward.businessCode) lines.push(`业务码:${forward.businessCode}`) if (forward.businessMessage) lines.push(`业务消息:${forward.businessMessage}`) const responseBody = stringifyJsonBlock(forward.responseBody) || forward.responseBodyRaw || '' if (responseBody) lines.push(`响应:\n${responseBody}`) return lines } /** 单条批量下游预报的 resultJson 展示文本 */ export function formatBatchJobDownstreamForwardItemText(label: string, item: ParsedOrderBatchJobItem): string { const parsed = parseBatchJobTrackUpsertPayload(item) const lines = [`【${label}】`] if (item.status === 'failed') { lines.push(`失败:${item.failReason || parsed?.message || '未知原因'}`) } else if (item.status === 'success') { lines.push(parsed?.message || item.resultMessage || '成功') } else { lines.push(item.resultMessage || item.status || '处理中') } const forward = parsed?.downstreamForward if (forward) { lines.push(...formatDownstreamForwardDetailLines(forward)) return lines.join('\n') } if (parsed) { lines.push(stringifyJsonBlock(parsed)) } else if (item.resultJson) { lines.push(item.resultJson) } return lines.join('\n') } /** 批量预报下游完成后的汇总展示(含请求/响应报文) */ export function formatBatchJobDownstreamForwardResultText( title: string, job: { jobId: number; status: string; total: number; successCount: number; failCount: number }, items: ParsedOrderBatchJobItem[], rows: any[], resolveLabel?: (row: any, item: ParsedOrderBatchJobItem) => string, ): string { const resolve = resolveLabel ?? ((row, item) => resolveBatchJobItemRowLabel(row, item)) const lines = [ `【${title}】任务 #${job.jobId}`, `状态:${job.status},合计 ${job.total},成功 ${job.successCount},失败 ${job.failCount}`, '', ] for (const item of items) { const row = rows.find((r) => { const rowId = Number(r?.id ?? r?.orderId ?? r?.Id ?? r?.OrderId ?? 0) return rowId === item.orderId }) lines.push(formatBatchJobDownstreamForwardItemText(resolve(row, item), item)) lines.push('') } return lines.join('\n').trimEnd() } export function getBatchJobDownstreamForwardResultSummary(item: ParsedOrderBatchJobItem): string { const parsed = parseBatchJobTrackUpsertPayload(item) const forward = parsed?.downstreamForward if (!forward) { return item.resultMessage || parsed?.message || item.failReason || '' } const code = forward.businessCode ? `code=${forward.businessCode}` : forward.responseStatusCode != null ? `HTTP ${forward.responseStatusCode}` : '' const msg = forward.businessMessage || '' const body = stringifyJsonBlock(forward.responseBody) || forward.responseBodyRaw || '' const preview = (msg || body).replace(/\s+/g, ' ').slice(0, 120) return [code, preview].filter(Boolean).join(' · ') || item.resultMessage || item.failReason || '' } export function formatBatchJobDownstreamForwardDetailText(item: ParsedOrderBatchJobItem): string { const label = getBatchJobItemDisplayNo(item) return formatBatchJobDownstreamForwardItemText(label, item) } /** 单条批量轨迹任务的 resultJson 展示文本 */ export function formatBatchJobTrackUpsertItemText(label: string, item: ParsedOrderBatchJobItem): string { const parsed = parseBatchJobTrackUpsertPayload(item) const lines = [`【${label}】`] if (item.status === 'failed') { lines.push(`失败:${item.failReason || parsed?.message || '未知原因'}`) } else if (item.status === 'success') { lines.push(parsed?.message || item.resultMessage || '成功') } else { lines.push(item.resultMessage || item.status || '处理中') } const track = parsed?.trackUpsert if (!track) { if (parsed) { lines.push(stringifyJsonBlock(parsed)) } else if (item.resultJson) { lines.push(item.resultJson) } return lines.join('\n') } if (track.skipped) { lines.push(`跳过:${track.skipReason || '未知原因'}`) } if (track.requestUrl) lines.push(`请求:${track.requestUrl}`) const requestBody = stringifyJsonBlock(track.requestBody) if (requestBody) lines.push(`请求体:\n${requestBody}`) if (track.responseStatusCode != null) lines.push(`HTTP:${track.responseStatusCode}`) const responseBody = stringifyJsonBlock(track.responseBody) || track.responseBodyRaw if (responseBody) lines.push(`响应:\n${responseBody}`) return lines.join('\n') } /** 批量创建轨迹订单完成后的汇总展示(含 resultJson 报文) */ export function formatBatchJobTrackUpsertResultText( title: string, job: { jobId: number; status: string; total: number; successCount: number; failCount: number }, items: ParsedOrderBatchJobItem[], rows: any[], resolveLabel?: (row: any, item: ParsedOrderBatchJobItem) => string, ): string { const resolve = resolveLabel ?? ((row, item) => resolveBatchJobItemRowLabel(row, item)) const lines = [ `【${title}】任务 #${job.jobId}`, `状态:${job.status},合计 ${job.total},成功 ${job.successCount},失败 ${job.failCount}`, '', ] for (const item of items) { const row = rows.find((r) => { const rowId = Number(r?.id ?? r?.orderId ?? r?.Id ?? r?.OrderId ?? 0) return rowId === item.orderId }) lines.push(formatBatchJobTrackUpsertItemText(resolve(row, item), item)) lines.push('') } return lines.join('\n').trimEnd() } /** 批量任务详情抽屉:轨迹响应摘要(单行) */ export function getBatchJobTrackUpsertResultSummary(item: ParsedOrderBatchJobItem): string { const parsed = parseBatchJobTrackUpsertPayload(item) const track = parsed?.trackUpsert if (!track) { return item.resultMessage || parsed?.message || '' } if (track.skipped) { return `跳过:${track.skipReason || '未知原因'}` } const code = track.responseStatusCode != null ? `HTTP ${track.responseStatusCode}` : '' const body = stringifyJsonBlock(track.responseBody) || track.responseBodyRaw || '' const preview = body.replace(/\s+/g, ' ').slice(0, 120) return [code, preview].filter(Boolean).join(' · ') || item.resultMessage || '成功' } /** 批量任务详情抽屉:轨迹响应详情(多行) */ export function formatBatchJobTrackUpsertDetailText(item: ParsedOrderBatchJobItem): string { const label = getBatchJobItemDisplayNo(item) return formatBatchJobTrackUpsertItemText(label, item) } const ORDER_BATCH_ACTION_ROUTES: Record = { pay: '/order/batch/pay', 'pre-declare': '/order/batch/pre-declare', intercept: '/order/batch/intercept', 'cancel-intercept': '/order/batch/cancel-intercept', delete: '/order/batch/delete', print: '/order/batch/print', 'mark-problematic': '/order/batch/mark-problematic', 'mark-claimed': '/order/batch/mark-claimed', 'handle-status-update': '/order/batch/handle-status-update', 'track-upsert-small-bag': '/order/batch/track-upsert', 'downstream-forward-create-one': '/order/batch/downstream-forward', } /** POST /order/batch/jobs 或按 action 路由的批量任务 */ export async function postOrderBatchJob(body: { action: string orderIds: Array payloadJson?: string requestId?: string }): Promise> { const route = ORDER_BATCH_ACTION_ROUTES[body.action] ?? '/order/batch/jobs' const payload = { action: body.action, orderIds: body.orderIds.map((id) => Number(id)), payloadJson: body.payloadJson, requestId: body.requestId, } const res = await axiosInstance.post(orderUrl(route), payload) assertBusinessOk(res.data) return parseOrderBatchJob(res.data) } export function formatBatchJobFailureMessage( job: { successCount: number; failCount: number; action?: string; total?: number }, items: Array<{ orderId: number serviceNo?: string clientNo?: string rowNumber?: number status: string failReason?: string resultMessage?: string }>, rows?: any[], getRowLabel?: (row: any, item: { orderId: number; serviceNo?: string; clientNo?: string }) => string ): string { const failed = items.filter((item) => item.status === 'failed') const header = job.total != null ? `完成:成功 ${job.successCount}/${job.total} 条,失败 ${job.failCount} 条` : `完成:成功 ${job.successCount} 条,失败 ${job.failCount} 条` if (!failed.length) { return header.replace(/,失败 0 条$/, '') } const lines = failed.slice(0, 8).map((item) => { const row = rows?.find((r) => Number(r?.id ?? r?.Id) === item.orderId) const label = getRowLabel?.(row, item) ?? getBatchJobItemDisplayNo(item, job.action) ?? String(item.orderId) const rowPrefix = item.rowNumber ? `第 ${item.rowNumber} 行 ` : '' const reason = item.resultMessage || item.failReason || '失败' return `【${rowPrefix}${label}】${reason}` }) const suffix = failed.length > 8 ? [`… 另有 ${failed.length - 8} 条失败,请在批量任务页查看`] : [] return [header, ...lines, ...suffix].join('\n') } const BATCH_JOB_TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled']) export interface OrderBatchDlxStats { enabled: boolean deadLetterMessageCount: number dlxAlertThreshold: number alert: boolean } export interface OrderBatchJobManageResult { jobId: number retriedCount: number cancelledCount: number status: string } function parseOrderBatchDlxStats(raw: any): OrderBatchDlxStats { const d = raw?.data ?? raw?.Data ?? raw ?? {} return { enabled: Boolean(d.enabled ?? d.Enabled), deadLetterMessageCount: Number(d.deadLetterMessageCount ?? d.DeadLetterMessageCount ?? 0), dlxAlertThreshold: Number(d.dlxAlertThreshold ?? d.DlxAlertThreshold ?? 0), alert: Boolean(d.alert ?? d.Alert), } } function parseOrderBatchJobManage(raw: any): OrderBatchJobManageResult { const d = raw?.data ?? raw?.Data ?? raw ?? {} return { jobId: Number(d.jobId ?? d.JobId ?? 0), retriedCount: Number(d.retriedCount ?? d.RetriedCount ?? 0), cancelledCount: Number(d.cancelledCount ?? d.CancelledCount ?? 0), status: String(d.status ?? d.Status ?? ''), } } /** GET /order/batch/jobs */ export async function getOrderBatchJobList(params: { page?: number pageSize?: number status?: string action?: string jobId?: number | string }): Promise<{ items: Array>; total: number }> { const res = await axiosInstance.get(orderUrl('/order/batch/jobs'), { params: { Page: params.page ?? 1, PageSize: params.pageSize ?? 30, Status: params.status || undefined, Action: params.action || undefined, JobId: params.jobId ? Number(params.jobId) : undefined, }, }) const parsed = parseOrderListPayload(res.data) return { items: (parsed.items || []).map(parseOrderBatchJob), total: parsed.total, } } /** GET /order/batch/dlx/stats */ export async function getOrderBatchDlxStats(): Promise { const res = await axiosInstance.get(orderUrl('/order/batch/dlx/stats')) assertBusinessOk(res.data) return parseOrderBatchDlxStats(res.data) } /** POST /order/batch/jobs/{id}/retry-failed */ export async function postOrderBatchJobRetryFailed( jobId: number | string, body?: { itemIds?: number[] } ): Promise { const res = await axiosInstance.post(orderUrl(`/order/batch/jobs/${jobId}/retry-failed`), body ?? {}) assertBusinessOk(res.data) return parseOrderBatchJobManage(res.data) } /** POST /order/batch/jobs/{id}/cancel */ export async function postOrderBatchJobCancel(jobId: number | string): Promise { const res = await axiosInstance.post(orderUrl(`/order/batch/jobs/${jobId}/cancel`)) assertBusinessOk(res.data) return parseOrderBatchJobManage(res.data) } function sleep(ms: number) { return new Promise((resolve) => { setTimeout(resolve, ms) }) } /** POST /order/batch/track-upsert */ export async function postOrderBatchTrackUpsert(body: { orderIds: Array requestId?: string }): Promise> { const res = await axiosInstance.post(orderUrl('/order/batch/track-upsert'), { orderIds: body.orderIds.map((id) => Number(id)), requestId: body.requestId, }) assertBusinessOk(res.data) return parseOrderBatchJob(res.data) } /** POST /order/batch/downstream-forward */ export async function postOrderBatchDownstreamForward(body: { orderIds: Array requestId?: string }): Promise> { const res = await axiosInstance.post(orderUrl('/order/batch/downstream-forward'), { orderIds: body.orderIds.map((id) => Number(id)), requestId: body.requestId, }) assertBusinessOk(res.data) return parseOrderBatchJob(res.data) } /** GET /order/batch/jobs/{id} */ export async function getOrderBatchJob(jobId: number | string): Promise> { const res = await axiosInstance.get(orderUrl(`/order/batch/jobs/${jobId}`)) assertBusinessOk(res.data) return parseOrderBatchJob(res.data) } /** GET /order/batch/jobs/{id}/items */ export async function getOrderBatchJobItems( jobId: number | string ): Promise>> { const res = await axiosInstance.get(orderUrl(`/order/batch/jobs/${jobId}/items`)) assertBusinessOk(res.data) const payload = unwrapApiData(res.data) const list = Array.isArray(payload) ? payload : payload.items ?? payload.Items ?? [] return (Array.isArray(list) ? list : []).map(parseOrderBatchJobItem) } /** 轮询批量任务直至结束或超时 */ export async function waitOrderBatchJob( jobId: number | string, options?: { intervalMs?: number; timeoutMs?: number } ): Promise<{ job: ParsedOrderBatchJob items: Array timedOut?: boolean }> { const intervalMs = options?.intervalMs ?? 1500 const timeoutMs = options?.timeoutMs ?? 300_000 const started = Date.now() while (Date.now() - started < timeoutMs) { const job = await getOrderBatchJob(jobId) if (BATCH_JOB_TERMINAL_STATUSES.has(job.status.toLowerCase())) { const items = await getOrderBatchJobItems(jobId) return { job, items } } await sleep(intervalMs) } const job = await getOrderBatchJob(jobId) const items = await getOrderBatchJobItems(jobId) return { job, items, timedOut: true } } export function isOrderBatchQueueDisabledError(error: unknown): boolean { const msg = String((error as any)?.message ?? (error as any)?.msg ?? '') return msg.includes('未启用') || msg.toLowerCase().includes('rabbitmq') } export interface OrderMarkProblematicRequest { orderIds: Array issueTemplateId?: number | string issueType?: string title?: string content?: string remark?: string } export interface OrderMarkClaimedRequest { orderIds: Array claimType?: string claimAmount?: number claimCurrency?: string claimReason?: string remark?: string } export interface OrderHandleStatusUpdateRequest { orderIds: Array handleStatus?: string remark?: string } function parseBatchActionResult(raw: any): { successCount: number; failures: Array<{ orderId: number; serviceNo?: string; reason: string }> } { const d = raw?.data ?? raw?.Data ?? raw ?? {} const failuresRaw = d.failures ?? d.Failures ?? [] const failures = (Array.isArray(failuresRaw) ? failuresRaw : []).map((item: any) => ({ orderId: Number(item.orderId ?? item.OrderId ?? 0), serviceNo: item.serviceNo ?? item.ServiceNo, reason: String(item.reason ?? item.Reason ?? ''), })) return { successCount: Number(d.successCount ?? d.SuccessCount ?? 0), failures, } } /** POST /order/mark-problematic */ export async function postOrderMarkProblematic(body: OrderMarkProblematicRequest): Promise<{ successCount: number; failures: Array<{ orderId: number; serviceNo?: string; reason: string }> }> { const res = await axiosInstance.post(orderUrl('/order/mark-problematic'), { orderIds: body.orderIds.map((id) => Number(id)), issueTemplateId: body.issueTemplateId != null ? Number(body.issueTemplateId) : undefined, issueType: body.issueType, title: body.title, content: body.content, remark: body.remark, }) assertBusinessOk(res.data) return parseBatchActionResult(res.data) } /** POST /order/mark-claimed */ export async function postOrderMarkClaimed(body: OrderMarkClaimedRequest) { const res = await axiosInstance.post(orderUrl('/order/mark-claimed'), { orderIds: body.orderIds.map((id) => Number(id)), claimType: body.claimType, claimAmount: body.claimAmount, claimCurrency: body.claimCurrency, claimReason: body.claimReason, remark: body.remark, }) assertBusinessOk(res.data) return parseBatchActionResult(res.data) } /** POST /order/handle-status/update */ export async function postOrderHandleStatusUpdate(body: OrderHandleStatusUpdateRequest) { const res = await axiosInstance.post(orderUrl('/order/handle-status/update'), { orderIds: body.orderIds.map((id) => Number(id)), handleStatus: body.handleStatus ?? 'processed', remark: body.remark, }) assertBusinessOk(res.data) return parseBatchActionResult(res.data) } /** POST /order/print → data.printUrl */ export async function postOrderPrint(body: OrderIdRequest): Promise { const res = await axiosInstance.post(orderUrl('/order/print'), { id: body.id }) assertBusinessOk(res.data) const d = res.data?.data ?? res.data?.Data ?? {} return { printUrl: d.printUrl ?? d.PrintUrl ?? '', } } /** POST /order/export → data.downloadUrl、expireAt */ export async function postOrderExport(params: OrderExportRequest): Promise { const res = await axiosInstance.post(orderUrl('/order/export'), { status: params.status ?? 'all', keyword: params.keyword ?? '', createTimeStart: params.createTimeStart ?? '', createTimeEnd: params.createTimeEnd ?? '', }) assertBusinessOk(res.data) const d = res.data?.data ?? res.data?.Data ?? {} return { downloadUrl: d.downloadUrl ?? d.DownloadUrl ?? '', expireAt: d.expireAt ?? d.ExpireAt ?? '', } } /** POST /order/price-check → data.items + data.failures + data.queryHint + priceBreakdown */ export interface PriceCheckRequest { destinationCountry: string shippingMethod: string cargoWeight: string length?: string width?: string height?: string /** @deprecated 请使用 cargoTypes */ cargoType?: string /** 货物类型(多选,字典 cargo_attribute 编码) */ cargoTypes?: string[] /** 仓库主键(多选;为空则遍历全部仓库) */ warehouseIds?: number[] destinationProvince?: string postalCode?: string /** 选用的优惠券实例 Id */ couponInstanceId?: number } export interface MarketingPriceBreakdown { receivableSubtotal?: number ReceivableSubtotal?: number levelDiscountAmount?: number LevelDiscountAmount?: number amountAfterLevel?: number AmountAfterLevel?: number couponDiscountAmount?: number CouponDiscountAmount?: number payableAmount?: number PayableAmount?: number memberLevelId?: number MemberLevelId?: number memberLevelName?: string MemberLevelName?: string priceTierKey?: string PriceTierKey?: string couponInstanceId?: number CouponInstanceId?: number } export interface CouponAvailableOutput { id?: number Id?: number templateName?: string TemplateName?: string faceValue?: number FaceValue?: number minOrderAmount?: number MinOrderAmount?: number unavailableReason?: string UnavailableReason?: string available?: boolean Available?: boolean } export interface PriceCheckQueryHint { quoteLevel?: 'exact' | 'estimated' missingFieldCodes?: string[] disclaimers?: string[] } export interface PriceCheckFailure { productId?: number productCode?: string productName?: string step?: string reason?: string cargoTypeName?: string warehouseName?: string } function normalizePriceCheckFailure(raw: unknown): PriceCheckFailure | null { if (raw == null) return null if (typeof raw === 'string') { const text = raw.trim() return text ? { reason: text } : null } if (typeof raw !== 'object') return null const row = raw as Record const reason = String( row.reason ?? row.Reason ?? row.message ?? row.Message ?? row.msg ?? row.Msg ?? '', ).trim() if (!reason && row.step == null && row.Step == null) return null return { productId: (row.productId ?? row.ProductId) as number | undefined, productCode: (row.productCode ?? row.ProductCode) as string | undefined, productName: (row.productName ?? row.ProductName) as string | undefined, step: (row.step ?? row.Step) as string | undefined, reason: reason || undefined, cargoTypeName: (row.cargoTypeName ?? row.CargoTypeName) as string | undefined, warehouseName: (row.warehouseName ?? row.WarehouseName) as string | undefined, } } /** 解析查价响应(兼容 data 包裹 / 直出、camel/Pascal、failures 在根或 data 内) */ export function parsePriceCheckResponse(resBody: any): { items: any[] failures: PriceCheckFailure[] queryHint: PriceCheckQueryHint | null priceBreakdown: MarketingPriceBreakdown | null availableCoupons: CouponAvailableOutput[] } { assertBusinessOk(resBody) const root = resBody ?? {} const inner = root.data ?? root.Data ?? root if (Array.isArray(inner)) { return { items: inner, failures: [], queryHint: null, priceBreakdown: null, availableCoupons: [] } } const payload = inner && typeof inner === 'object' && !Array.isArray(inner) ? inner : root const items = (payload as any).items ?? (payload as any).Items ?? (payload as any).list ?? (payload as any).List ?? [] const rawFailures = (payload as any).failures ?? (payload as any).Failures ?? root.failures ?? root.Failures ?? [] const rawHint = (payload as any).queryHint ?? (payload as any).QueryHint ?? null const queryHint = normalizePriceCheckQueryHint(rawHint) const failureRows = Array.isArray(rawFailures) ? rawFailures : rawFailures && typeof rawFailures === 'object' ? [rawFailures] : [] const failures = failureRows .map(normalizePriceCheckFailure) .filter((f): f is PriceCheckFailure => f != null && Boolean(f.reason?.trim())) const itemList = Array.isArray(items) ? items : [] const rawBreakdown = (payload as any).priceBreakdown ?? (payload as any).PriceBreakdown ?? null let priceBreakdown = rawBreakdown && typeof rawBreakdown === 'object' ? (rawBreakdown as MarketingPriceBreakdown) : null if (!priceBreakdown && itemList.length > 0) { const firstItem = itemList[0] as Record const itemBreakdown = firstItem?.priceBreakdown ?? firstItem?.PriceBreakdown if (itemBreakdown && typeof itemBreakdown === 'object') { priceBreakdown = itemBreakdown as MarketingPriceBreakdown } } const rawCoupons = (payload as any).availableCoupons ?? (payload as any).AvailableCoupons ?? [] const availableCoupons = (Array.isArray(rawCoupons) ? rawCoupons : []).map((row: any) => ({ ...row, id: row.id ?? row.Id ?? row.couponInstanceId ?? row.CouponInstanceId, available: row.available ?? row.Available ?? row.canUse ?? row.CanUse, })) if (!itemList.length && !failures.length) { const msg = String( root.msg ?? root.Msg ?? root.message ?? root.Message ?? (payload as any).msg ?? (payload as any).Msg ?? (payload as any).message ?? (payload as any).Message ?? '', ).trim() if (msg) { failures.push({ reason: msg }) } } return { items: itemList, failures, queryHint, priceBreakdown, availableCoupons } } function normalizePriceCheckQueryHint(raw: unknown): PriceCheckQueryHint | null { if (raw == null || typeof raw !== 'object') return null const row = raw as Record const quoteLevelRaw = String(row.quoteLevel ?? row.QuoteLevel ?? '').trim().toLowerCase() const quoteLevel = quoteLevelRaw === 'estimated' || quoteLevelRaw === 'exact' ? (quoteLevelRaw as 'exact' | 'estimated') : undefined const missingFieldCodes = ( (row.missingFieldCodes ?? row.MissingFieldCodes ?? []) as unknown[] ) .map((x) => String(x ?? '').trim()) .filter(Boolean) const disclaimers = ((row.disclaimers ?? row.Disclaimers ?? []) as unknown[]) .map((x) => String(x ?? '').trim()) .filter(Boolean) if (!quoteLevel && !missingFieldCodes.length && !disclaimers.length) return null return { quoteLevel, missingFieldCodes, disclaimers } } export type PriceCheckShippingOption = { productId: number productCode: string productName: string label: string } export type PriceCheckShippingGroup = { groupCode: string groupName: string products: PriceCheckShippingOption[] } function normalizeShippingOption(raw: any): PriceCheckShippingOption | null { const productCode = String(raw?.productCode ?? raw?.ProductCode ?? '').trim() if (!productCode) return null const productName = String(raw?.productName ?? raw?.ProductName ?? '').trim() const label = String(raw?.label ?? raw?.Label ?? '').trim() return { productId: Number(raw?.productId ?? raw?.ProductId ?? 0), productCode, productName, label: label || (productName ? `${productName} ${productCode}` : productCode), } } function normalizeShippingGroup(raw: any): PriceCheckShippingGroup | null { const groupCode = String(raw?.groupCode ?? raw?.GroupCode ?? '').trim() const groupName = String(raw?.groupName ?? raw?.GroupName ?? '').trim() const productsRaw = raw?.products ?? raw?.Products ?? [] const products = (Array.isArray(productsRaw) ? productsRaw : []) .map(normalizeShippingOption) .filter((item): item is PriceCheckShippingOption => Boolean(item)) if (!groupCode && !groupName && products.length === 0) return null return { groupCode: groupCode || groupName, groupName: groupName || groupCode, products, } } export async function getOrderPriceCheckShippingOptions(): Promise { const res = await axiosInstance.get(orderUrl('/order/price-check/shipping-options')) const body = res.data assertBusinessOk(body) const payload = body?.data ?? body?.Data ?? body const groupsRaw = payload?.groups ?? payload?.Groups ?? [] return (Array.isArray(groupsRaw) ? groupsRaw : []) .map(normalizeShippingGroup) .filter((item): item is PriceCheckShippingGroup => Boolean(item)) .filter((item) => item.products.length > 0) } export async function postOrderPriceCheck(params: PriceCheckRequest): Promise<{ items: any[] failures: PriceCheckFailure[] queryHint: PriceCheckQueryHint | null priceBreakdown: MarketingPriceBreakdown | null availableCoupons: CouponAvailableOutput[] }> { const res = await axiosInstance.post(orderUrl('/order/price-check'), params) return parsePriceCheckResponse(res.data) } /** GET /order/template/list */ export async function getOrderTemplateList(params: { page?: number pageSize?: number type?: 'standard' | 'custom' | string keyword?: string }): Promise { const res = await axiosInstance.get(orderUrl('/order/template/list'), { params: { page: params.page ?? 1, pageSize: params.pageSize ?? 50, type: params.type ?? '', keyword: params.keyword ?? '', }, }) return parseOrderListPayload(res.data) } /** 从 axios 响应中解析二进制下载内容 */ function resolveDownloadBlob(response: { data?: unknown headers?: Record }): { blob: Blob; contentType: string } { const headers = response?.headers ?? {} const contentType = String(headers['content-type'] ?? headers['Content-Type'] ?? '') const raw = response?.data if (raw instanceof Blob) { return { blob: raw, contentType: contentType || raw.type } } if (raw instanceof ArrayBuffer) { return { blob: new Blob([raw], { type: contentType || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }), contentType, } } if (raw instanceof Uint8Array) { return { blob: new Blob([raw], { type: contentType || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }), contentType, } } if (raw && typeof raw === 'object') { const body = raw as Record throw new Error( String(body.msg ?? body.Msg ?? body.message ?? body.Message ?? '下载失败'), ) } throw new Error('下载响应无效') } async function parseDownloadErrorBlob(blob: Blob): Promise { const text = await blob.text() try { const json = JSON.parse(text) throw new Error(String(json.msg ?? json.message ?? json.Message ?? '下载失败')) } catch (error: unknown) { if (error instanceof Error && error.message !== '下载失败') { throw error } throw new Error(text || '下载失败') } } function triggerBrowserFileDownload(blob: Blob, fileName: string) { const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = fileName.endsWith('.xlsx') ? fileName : `${fileName}.xlsx` a.style.display = 'none' document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } /** GET /order/template/download?templateId= → Excel 文件流 */ export async function downloadOrderTemplate(templateId: number, fileName: string) { const response = await axiosInstance.get(orderUrl('/order/template/download'), { params: { templateId }, responseType: 'arraybuffer', }) const { blob, contentType } = resolveDownloadBlob(response) const ct = contentType || blob.type || '' if (ct.includes('application/json') || ct.includes('text/json')) { await parseDownloadErrorBlob(blob) } if (!blob.size) { throw new Error('下载文件为空') } triggerBrowserFileDownload(blob, fileName) } export interface OrderTemplateFieldSchemaItem { fieldKey: string label: string required: boolean dataType: string dictType?: string group: string example: string } export interface OrderTemplateCustomDetail { id: number name: string type: string standardTemplateKey?: string mappingStatus?: string sourceFileName?: string sourceHeaders: string[] mapping?: { standardTemplateKey?: string headerRowIndex?: number dataStartRowIndex?: number mappings?: Array<{ standardFieldKey: string; sourceColumn: string; required?: boolean }> unmappedSourceColumns?: string[] } standardFields: OrderTemplateFieldSchemaItem[] } /** GET /order/template/field-schema */ export async function getOrderTemplateFieldSchema(templateKey?: string) { const res = await axiosInstance.get(orderUrl('/order/template/field-schema'), { params: { templateKey: templateKey ?? '' }, }) assertBusinessOk(res.data) return (res.data?.data ?? res.data?.Data ?? res.data) as { templateKey: string templateName: string fields: OrderTemplateFieldSchemaItem[] } } /** POST /order/template/custom/upload */ export async function postOrderTemplateCustomUpload(name: string, file: File) { const form = new FormData() form.append('name', name) form.append('file', file) const res = await axiosInstance.post(orderUrl('/order/template/custom/upload'), form) assertBusinessOk(res.data) const d = unwrapApiData(res.data) const templateId = Number(d.templateId ?? d.TemplateId ?? d.id ?? d.Id) if (!Number.isFinite(templateId) || templateId <= 0) { throw new Error('上传成功但未返回模板 ID') } return { templateId, name: String(d.name ?? d.Name ?? name), mappingStatus: String(d.mappingStatus ?? d.MappingStatus ?? 'draft'), sourceHeaders: (d.sourceHeaders ?? d.SourceHeaders ?? []) as string[], } } /** GET /order/template/custom/detail */ export async function getOrderTemplateCustomDetail(templateId: number): Promise { const res = await axiosInstance.get(orderUrl('/order/template/custom/detail'), { params: { templateId }, }) assertBusinessOk(res.data) const d = unwrapApiData(res.data) return { id: Number(d.id ?? d.Id ?? templateId), name: String(d.name ?? d.Name ?? ''), type: String(d.type ?? d.Type ?? 'custom'), standardTemplateKey: d.standardTemplateKey ?? d.StandardTemplateKey, mappingStatus: d.mappingStatus ?? d.MappingStatus, sourceFileName: d.sourceFileName ?? d.SourceFileName, sourceHeaders: (d.sourceHeaders ?? d.SourceHeaders ?? []) as string[], mapping: d.mapping ?? d.Mapping, standardFields: (d.standardFields ?? d.StandardFields ?? []) as OrderTemplateFieldSchemaItem[], } } /** POST /order/template/custom/save-mapping */ export async function postOrderTemplateSaveMapping(body: { templateId: number standardTemplateKey?: string headerRowIndex?: number dataStartRowIndex?: number mappings: Array<{ standardFieldKey: string; sourceColumn: string }> }) { const templateId = Number(body.templateId) if (!Number.isFinite(templateId) || templateId <= 0) { throw new Error('模板 ID 无效') } const payload = { TemplateId: templateId, StandardTemplateKey: body.standardTemplateKey, HeaderRowIndex: body.headerRowIndex ?? 1, DataStartRowIndex: body.dataStartRowIndex ?? 2, Mappings: body.mappings.map((item) => ({ StandardFieldKey: item.standardFieldKey, SourceColumn: item.sourceColumn, })), } const res = await axiosInstance.post(orderUrl('/order/template/custom/save-mapping'), payload) assertBusinessOk(res.data) const d = unwrapApiData(res.data) return { templateId: Number(d.templateId ?? d.TemplateId ?? templateId), mappingStatus: String(d.mappingStatus ?? d.MappingStatus ?? 'draft'), missingRequiredFields: (d.missingRequiredFields ?? d.MissingRequiredFields ?? []) as string[], } } /** DELETE /order/template/custom/{id} */ export async function deleteOrderTemplateCustom(templateId: number) { const res = await axiosInstance.delete(orderUrl(`/order/template/custom/${templateId}`)) const body = res?.data if (body == null || body === '') { return } assertBusinessOk(body) } /** POST /order/upload multipart: file + templateId + senderAddressId */ export async function postOrderUpload( file: File, templateId: number, senderAddressId?: number ): Promise<{ jobId: number; async: boolean }> { const form = new FormData() form.append('file', file) form.append('templateId', String(templateId)) if (senderAddressId != null && senderAddressId > 0) { form.append('senderAddressId', String(senderAddressId)) } const res = await axiosInstance.post(orderUrl('/order/upload'), form) assertBusinessOk(res.data) const d = unwrapApiData(res.data) const asyncFlag = d.async ?? d.Async return { jobId: Number(d.jobId ?? d.JobId ?? 0), async: asyncFlag === true || asyncFlag === 'true', } } /** 小包创建/预报业务失败(可能已落草稿) */ export class ParcelCreateBusinessError extends Error { draftSaved?: boolean orderId?: number serviceNo?: string raw?: any constructor( message: string, extra?: { draftSaved?: boolean orderId?: number serviceNo?: string raw?: any }, ) { super(message) this.name = 'ParcelCreateBusinessError' this.draftSaved = extra?.draftSaved this.orderId = extra?.orderId this.serviceNo = extra?.serviceNo this.raw = extra?.raw } } function isBusinessOkBody(body: any): boolean { if (body == null) return false const code = body.code ?? body.Code return ( code === undefined || code === null || code === 0 || code === 200 || code === '0' || code === '200' || body.success === true || body.Success === true ) } function throwParcelCreateBusinessError(body: any, fallbackMsg: string): never { const extracted = extractDraftFailFromBody(body) const fallbackParsed = parseOmsDraftMarker( String(body?.msg ?? body?.Msg ?? body?.message ?? body?.Message ?? fallbackMsg ?? '请求失败'), ) const cleanMessage = extracted.cleanMessage || fallbackParsed.cleanMessage || fallbackMsg || '请求失败' const draftSaved = Boolean(extracted.meta?.draftSaved || fallbackParsed.meta?.draftSaved) const orderId = extracted.meta?.orderId ?? fallbackParsed.meta?.orderId const serviceNo = extracted.meta?.serviceNo ?? fallbackParsed.meta?.serviceNo throw new ParcelCreateBusinessError(cleanMessage, { draftSaved, orderId, serviceNo, raw: body, }) } export interface OrderReceiverParseAddressResult { receiver: { recipientsName?: string companyName?: string phone?: string telePhone?: string email?: string countryCode?: string province?: string city?: string streetNumber?: string streetAddress1?: string streetAddress2?: string streetAddress3?: string zipCode?: string } isComplete: boolean missingRequired: string[] missingRecommended: string[] warnings: string[] } /** POST /order/receiver/parse-address — 粘贴地址 AI 解析 */ export async function postOrderReceiverParseAddress(body: { rawText: string providerKey?: string model?: string }): Promise { const res = await axiosInstance.post(orderUrl('/order/receiver/parse-address'), body) assertBusinessOk(res.data) const data = unwrapApiData(res.data) const receiver = data?.receiver ?? data?.Receiver ?? {} const pick = (a: string, b: string) => String(receiver?.[a] ?? receiver?.[b] ?? '').trim() || undefined return { receiver: { recipientsName: pick('recipientsName', 'RecipientsName'), companyName: pick('companyName', 'CompanyName'), phone: pick('phone', 'Phone'), telePhone: pick('telePhone', 'TelePhone'), email: pick('email', 'Email'), countryCode: pick('countryCode', 'CountryCode'), province: pick('province', 'Province'), city: pick('city', 'City'), streetNumber: pick('streetNumber', 'StreetNumber'), streetAddress1: pick('streetAddress1', 'StreetAddress1'), streetAddress2: pick('streetAddress2', 'StreetAddress2'), streetAddress3: pick('streetAddress3', 'StreetAddress3'), zipCode: pick('zipCode', 'ZipCode'), }, isComplete: Boolean(data?.isComplete ?? data?.IsComplete), missingRequired: (data?.missingRequired ?? data?.MissingRequired ?? []) as string[], missingRecommended: (data?.missingRecommended ?? data?.MissingRecommended ?? []) as string[], warnings: (data?.warnings ?? data?.Warnings ?? []) as string[], } } /** POST /order/parcel/create */ export async function postOrderParcelCreate(body: Record): Promise { try { const res = await axiosInstance.post(orderUrl('/order/parcel/create'), body) const resBody = res.data if (!isBusinessOkBody(resBody)) { throwParcelCreateBusinessError(resBody, '请求失败') } // Fake 可能返回 { message, details } 且无 code:若含草稿失败标记仍按失败处理 const extracted = extractDraftFailFromBody(resBody) if (extracted.meta?.draftSaved) { throwParcelCreateBusinessError(resBody, extracted.cleanMessage || '预报未完成') } return resBody?.data ?? resBody?.Data ?? {} } catch (e: any) { if (e instanceof ParcelCreateBusinessError) throw e const data = e?.response?.data if (data) throwParcelCreateBusinessError(data, e?.message || '请求失败') throw e } } export interface OrderParcelBatchCreateResult { batchNo?: string BatchNo?: string jobId?: number JobId?: number async?: boolean Async?: boolean total?: number Total?: number successCount?: number SuccessCount?: number failCount?: number FailCount?: number items?: Array<{ index?: number Index?: number requestId?: string RequestId?: string clientNo?: string ClientNo?: string serviceNo?: string ServiceNo?: string trackNo?: string TrackNo?: string labelUrl?: string LabelUrl?: string success?: boolean Success?: boolean message?: string Message?: string }> Items?: OrderParcelBatchCreateResult['items'] } function parseOrderParcelBatchCreateResult(raw: any): { batchNo: string jobId?: number async: boolean total: number successCount: number failCount: number items: Array<{ index: number clientNo?: string serviceNo?: string success: boolean message?: string }> } { const d = raw?.data ?? raw?.Data ?? raw ?? {} const itemsRaw = d.items ?? d.Items ?? [] const items = (Array.isArray(itemsRaw) ? itemsRaw : []).map((item: any, idx: number) => ({ index: Number(item.index ?? item.Index ?? idx + 1), clientNo: item.clientNo ?? item.ClientNo, serviceNo: item.serviceNo ?? item.ServiceNo, success: item.success ?? item.Success ?? false, message: item.message ?? item.Message, })) return { batchNo: String(d.batchNo ?? d.BatchNo ?? ''), jobId: d.jobId != null || d.JobId != null ? Number(d.jobId ?? d.JobId) : undefined, async: d.async === true || d.Async === true, total: Number(d.total ?? d.Total ?? items.length), successCount: Number(d.successCount ?? d.SuccessCount ?? 0), failCount: Number(d.failCount ?? d.FailCount ?? 0), items, } } /** POST /order/parcel/batch-create */ export async function postOrderParcelBatchCreate( bodies: Array> ): Promise> { const res = await axiosInstance.post(orderUrl('/order/parcel/batch-create'), bodies) assertBusinessOk(res.data) return parseOrderParcelBatchCreateResult(res.data) } /** POST /order/update */ export async function postOrderUpdate(body: Record): Promise { const res = await axiosInstance.post(orderUrl('/order/update'), body) assertBusinessOk(res.data) return res.data?.data ?? res.data?.Data ?? res.data } /** POST /order/weight/update */ export async function postOrderWeightUpdate(body: { id: number | string weights: Array<{ type: string length?: number width?: number height?: number actualWeight?: number }> }): Promise { const res = await axiosInstance.post(orderUrl('/order/weight/update'), body) assertBusinessOk(res.data) return res.data?.data ?? res.data?.Data ?? res.data } /** POST /order/fee/recalculate-receivable */ export async function postOrderRecalculateReceivable(body: OrderIdRequest): Promise { const res = await axiosInstance.post(orderUrl('/order/fee/recalculate-receivable'), { id: body.id }) assertBusinessOk(res.data) return res.data?.data ?? res.data?.Data ?? res.data } /** POST /order/fee/recalculate-payable */ export async function postOrderRecalculatePayable(body: OrderIdRequest): Promise { const res = await axiosInstance.post(orderUrl('/order/fee/recalculate-payable'), { id: body.id }) assertBusinessOk(res.data) return res.data?.data ?? res.data?.Data ?? res.data } export interface OrderFeeAddRequest { id: number | string type: 'receivable' | 'payable' costItem: string amount: number currency: string chargingStage: string downstreamProvider?: string priceNo?: string priceEffectiveTime?: string } /** POST /order/fee/add */ export async function postOrderFeeAdd(body: OrderFeeAddRequest): Promise { const res = await axiosInstance.post(orderUrl('/order/fee/add'), body) assertBusinessOk(res.data) return res.data?.data ?? res.data?.Data ?? res.data } export interface OrderStockBindItem { id?: number bindType: 'inventory' | 'parcelItem' | string skuId?: number skuCode?: string skuName?: string inventoryId?: number binLocationId?: number binCode?: string parcelId?: number parcelItemId?: number trackingNo?: string productName?: string attributeText?: string imageUrl?: string bindQty: number availableQty?: number status?: string } export interface OrderStockBindDetail { orderId: number warehouseId?: number warehouseName?: string bindStatus: string items: OrderStockBindItem[] } function unwrapData(body: any): T { return (body?.data ?? body?.Data ?? body) as T } /** GET /order/stock-bind/detail */ export async function getOrderStockBindDetail(orderId: number): Promise { const res = await axiosInstance.get(orderUrl('/order/stock-bind/detail'), { params: { orderId } }) assertBusinessOk(res.data) return unwrapData(res.data) } /** POST /order/stock-bind/inventory-candidates(orderId=0 为创建前草稿选仓) */ export async function postOrderStockBindInventoryCandidates(body: { orderId: number warehouseId?: number customerId?: number keyword?: string page?: number pageSize?: number }): Promise<{ items: any[]; total: number }> { const res = await axiosInstance.post(orderUrl('/order/stock-bind/inventory-candidates'), body) assertBusinessOk(res.data) const data = unwrapData(res.data) return { items: data?.items ?? data?.Items ?? [], total: data?.total ?? data?.Total ?? 0, } } /** POST /order/stock-bind/parcel-candidates(orderId=0 为创建前草稿选仓) */ export async function postOrderStockBindParcelCandidates(body: { orderId: number warehouseId?: number customerId?: number lifecycleStatus?: string keyword?: string page?: number pageSize?: number }): Promise<{ items: any[]; total: number }> { const res = await axiosInstance.post(orderUrl('/order/stock-bind/parcel-candidates'), body) assertBusinessOk(res.data) const data = unwrapData(res.data) return { items: data?.items ?? data?.Items ?? [], total: data?.total ?? data?.Total ?? 0, } } /** POST /order/stock-bind/save */ export async function postOrderStockBindSave(body: { orderId: number items: Array<{ bindType: string skuId?: number inventoryId?: number parcelId?: number parcelItemId?: number bindQty: number }> }): Promise { const res = await axiosInstance.post(orderUrl('/order/stock-bind/save'), body) assertBusinessOk(res.data) return unwrapData(res.data) } /** POST /order/stock-bind/clear */ export async function postOrderStockBindClear(orderId: number): Promise { const res = await axiosInstance.post(orderUrl('/order/stock-bind/clear'), { orderId }) assertBusinessOk(res.data) return unwrapData(res.data) } export interface OrderFileItem { id: number orderId: number fileType: string fileName?: string fileUrl: string contentType?: string fileSize?: number sort?: number source?: string remark?: string createTime?: string } export interface OrderFileGroupedResult { orderId: number groups: Record } function normalizeOrderFileItem(raw: any): OrderFileItem { return { id: Number(raw?.id ?? raw?.Id ?? 0), orderId: Number(raw?.orderId ?? raw?.OrderId ?? 0), fileType: String(raw?.fileType ?? raw?.FileType ?? ''), fileName: raw?.fileName ?? raw?.FileName, fileUrl: String(raw?.fileUrl ?? raw?.FileUrl ?? ''), contentType: raw?.contentType ?? raw?.ContentType, fileSize: raw?.fileSize ?? raw?.FileSize, sort: raw?.sort ?? raw?.Sort, source: raw?.source ?? raw?.Source, remark: raw?.remark ?? raw?.Remark, createTime: raw?.createTime ?? raw?.CreateTime, } } /** GET /order/files */ export async function getOrderFiles(orderId: number): Promise { const res = await axiosInstance.get(orderUrl('/order/files'), { params: { orderId } }) assertBusinessOk(res.data) const data = unwrapData(res.data) const rawGroups = data?.groups ?? data?.Groups ?? {} const groups: Record = {} Object.keys(rawGroups || {}).forEach((key) => { const list = Array.isArray(rawGroups[key]) ? rawGroups[key] : [] groups[key] = list.map(normalizeOrderFileItem) }) return { orderId: Number(data?.orderId ?? data?.OrderId ?? orderId), groups, } } /** POST /order/files/upload */ export async function uploadOrderFiles( orderId: number, fileType: string, files: File[] ): Promise { const form = new FormData() form.append('orderId', String(orderId)) form.append('fileType', fileType) files.forEach((file) => form.append('files', file)) const res = await axiosInstance.post(orderUrl('/order/files/upload'), form) assertBusinessOk(res.data) const data = unwrapData(res.data) const list = Array.isArray(data) ? data : data?.items ?? data?.Items ?? [] return list.map(normalizeOrderFileItem) } /** POST /order/files/delete */ export async function deleteOrderFile(id: number): Promise { const res = await axiosInstance.post(orderUrl('/order/files/delete'), { id }) assertBusinessOk(res.data) }