front/src/api/order/index.ts
zzh 6a7f8f1bcb feat: 用户扩展、Nana双Tab、仓库与客服入口优化
扩展用户管理字段与组织编码生成,顶部改为仓库下拉;Nana 分 AI 与专属顾问 Tab,仓库页移除客服字段,并完善扫描语音与订单相关页面。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 11:23:54 +08:00

1220 lines
37 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* OMS 订单接口(Swagger:OMS-Order,与 admin 同源)
*/
import { axiosInstance } from '@/api/rbac'
import { resolveAdminApiPath } from '@/utils/adminApi'
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
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<string, number>
handleCounts?: Record<string, OrderHandleStatusCounts>
}
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<T = Record<string, any>>(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<OrderListResult> {
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.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<OrderStatusCountsResult> {
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<string, OrderHandleStatusCounts> = {}
if (handleCountsRaw && typeof handleCountsRaw === 'object') {
for (const [key, value] of Object.entries(handleCountsRaw as Record<string, any>)) {
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<any> {
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<void> {
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 function postOrderPreDeclare(body: OrderIdRequest) {
return postOrderById('/order/pre-declare', body)
}
/** 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<OrderTrackUpsertResult> {
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
}
/** POST /order/downstream-forward-create-one → 后端查订单表组装并转发 createOrderOne */
export async function postOrderDownstreamForwardCreateOne(
body: OrderIdRequest
): Promise<OrderDownstreamForwardResult> {
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,
}
}
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
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
status?: string
Status?: string
failReason?: string
FailReason?: string
startedAt?: string
StartedAt?: string
finishedAt?: string
FinishedAt?: string
}
function parseOrderBatchJob(raw: any): {
jobId: number
action: string
status: string
total: number
successCount: number
failCount: number
startedAt?: string
finishedAt?: string
createTime?: string
} {
const d = unwrapApiData(raw)
return {
jobId: Number(d.jobId ?? d.JobId ?? 0),
action: String(d.action ?? d.Action ?? ''),
status: String(d.status ?? d.Status ?? ''),
total: Number(d.total ?? d.Total ?? 0),
successCount: Number(d.successCount ?? d.SuccessCount ?? 0),
failCount: Number(d.failCount ?? d.FailCount ?? 0),
startedAt: d.startedAt ?? d.StartedAt,
finishedAt: d.finishedAt ?? d.FinishedAt,
createTime: d.createTime ?? d.CreateTime,
}
}
function parseOrderBatchJobItem(raw: any): {
id: number
orderId: number
serviceNo?: string
status: string
failReason?: string
resultJson?: string
} {
return {
id: Number(raw.id ?? raw.Id ?? 0),
orderId: Number(raw.orderId ?? raw.OrderId ?? 0),
serviceNo: raw.serviceNo ?? raw.ServiceNo,
status: String(raw.status ?? raw.Status ?? ''),
failReason: raw.failReason ?? raw.FailReason,
resultJson: raw.resultJson ?? raw.ResultJson,
}
}
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 ''
}
}
const ORDER_BATCH_ACTION_ROUTES: Record<string, string> = {
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<number | string>
payloadJson?: string
requestId?: string
}): Promise<ReturnType<typeof parseOrderBatchJob>> {
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 },
items: Array<{ orderId: number; serviceNo?: string; status: string; failReason?: string }>,
rows?: any[],
getRowLabel?: (row: any, item: { orderId: number; serviceNo?: string }) => string
): string {
const failed = items.filter((item) => item.status === 'failed')
if (!failed.length) {
return `完成:成功 ${job.successCount} 条`
}
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) ?? item.serviceNo ?? String(item.orderId)
return `【${label}】${item.failReason || '失败'}`
})
return [`完成:成功 ${job.successCount} 条,失败 ${job.failCount} 条`, ...lines].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<ReturnType<typeof parseOrderBatchJob>>; 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<OrderBatchDlxStats> {
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<OrderBatchJobManageResult> {
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<OrderBatchJobManageResult> {
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<void>((resolve) => {
setTimeout(resolve, ms)
})
}
/** POST /order/batch/track-upsert */
export async function postOrderBatchTrackUpsert(body: {
orderIds: Array<number | string>
requestId?: string
}): Promise<ReturnType<typeof parseOrderBatchJob>> {
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<number | string>
requestId?: string
}): Promise<ReturnType<typeof parseOrderBatchJob>> {
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<ReturnType<typeof parseOrderBatchJob>> {
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<Array<ReturnType<typeof parseOrderBatchJobItem>>> {
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: ReturnType<typeof parseOrderBatchJob>
items: Array<ReturnType<typeof parseOrderBatchJobItem>>
}> {
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)
}
throw new Error('批量任务执行超时,请稍后在任务列表中查看进度')
}
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<number | string>
issueTemplateId?: number | string
issueType?: string
title?: string
content?: string
remark?: string
}
export interface OrderMarkClaimedRequest {
orderIds: Array<number | string>
claimType?: string
claimAmount?: number
claimCurrency?: string
claimReason?: string
remark?: string
}
export interface OrderHandleStatusUpdateRequest {
orderIds: Array<number | string>
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<OrderPrintResult> {
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<OrderExportResult> {
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 */
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
}
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<string, unknown>
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[]
} {
assertBusinessOk(resBody)
const root = resBody ?? {}
const inner = root.data ?? root.Data ?? root
if (Array.isArray(inner)) {
return { items: inner, failures: [] }
}
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 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 : []
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 }
}
export async function postOrderPriceCheck(params: PriceCheckRequest): Promise<{
items: any[]
failures: PriceCheckFailure[]
}> {
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<OrderListResult> {
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<string, string | undefined>
}): { 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<string, unknown>
throw new Error(
String(body.msg ?? body.Msg ?? body.message ?? body.Message ?? '下载失败'),
)
}
throw new Error('下载响应无效')
}
async function parseDownloadErrorBlob(blob: Blob): Promise<never> {
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<OrderTemplateCustomDetail> {
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 */
export async function postOrderUpload(
file: File,
templateId: number
): Promise<{ taskId?: number; jobId?: number; async?: boolean }> {
const form = new FormData()
form.append('file', file)
form.append('templateId', String(templateId))
const res = await axiosInstance.post(orderUrl('/order/upload'), form)
assertBusinessOk(res.data)
const d = unwrapApiData(res.data)
const taskId = d.taskId ?? d.TaskId
const jobId = d.jobId ?? d.JobId
const asyncFlag = d.async ?? d.Async
return {
taskId: taskId != null ? Number(taskId) : undefined,
jobId: jobId != null ? Number(jobId) : undefined,
async: asyncFlag === true || asyncFlag === 'true',
}
}
/** GET /order/task/list */
export async function getOrderTaskList(params: {
page?: number
pageSize?: number
status?: string
keyword?: string
}): Promise<OrderListResult> {
const res = await axiosInstance.get(orderUrl('/order/task/list'), {
params: {
page: params.page ?? 1,
pageSize: params.pageSize ?? 20,
status: params.status ?? '',
keyword: params.keyword ?? '',
},
})
return parseOrderListPayload(res.data)
}
/** GET /order/task/detail?taskId= */
export async function getOrderTaskDetail(taskId: number | string): Promise<any> {
const res = await axiosInstance.get(orderUrl('/order/task/detail'), {
params: { taskId },
})
assertBusinessOk(res.data)
return res.data?.data ?? res.data?.Data ?? {}
}
/** POST /order/parcel/create */
export async function postOrderParcelCreate(body: Record<string, any>): Promise<any> {
const res = await axiosInstance.post(orderUrl('/order/parcel/create'), body)
assertBusinessOk(res.data)
return res.data?.data ?? res.data?.Data ?? {}
}
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<Record<string, any>>
): Promise<ReturnType<typeof parseOrderParcelBatchCreateResult>> {
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<string, any>): Promise<any> {
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<any> {
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<any> {
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<any> {
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<any> {
const res = await axiosInstance.post(orderUrl('/order/fee/add'), body)
assertBusinessOk(res.data)
return res.data?.data ?? res.data?.Data ?? res.data
}