front/src/api/order/index.ts
zzh 6fb1f4fd66 feat: 接入企业微信客服,并同步工作台与下单页改动
官网/H5 支持企微客服跳转;用户资料新增 WecomKfUrl;工作台与下单相关体验一并更新。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 19:09:34 +08:00

2357 lines
77 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'
import { extractDraftFailFromBody, parseOmsDraftMarker } from '@/utils/parcelCreateFailGuide'
function orderUrl(path: string): string {
return resolveAdminApiPath(path)
}
export interface OrderListFieldFilter {
field: string
op: string
value?: string
value2?: string
values?: string[]
}
export interface OrderListRequest {
page: number
pageSize: number
/** all 或各状态英文 key,与文档一致 */
status?: string
/** 子状态:all / pending / processed(问题件、已理赔 Tab) */
handleStatus?: string
/** 订单来源:all / manual / pickup 等 */
orderSource?: string
/** 履约类型:all / direct / warehouse */
fulfillmentType?: string
/** 绑定状态聚合:all / none / bound / deducted */
stockBindStatus?: string
/** 代发进度:pendingBind / domesticInTransit / arrivedPendingPick / outboundDone */
fulfillProgress?: string
/** 发货仓 ID(拣货队列等) */
warehouseId?: number
/** 管理端可选客户筛选;不传则查全部 */
customerId?: number
keyword?: string
createTimeStart?: string
createTimeEnd?: string
/** 表头字段级筛选(白名单) */
filters?: OrderListFieldFilter[]
sortField?: string
sortOrder?: 'asc' | 'desc' | 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>
fulfillProgressCounts?: Record<string, number>
}
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.orderSource?.trim()) {
body.orderSource = params.orderSource.trim()
}
if (params.fulfillmentType?.trim()) {
body.fulfillmentType = params.fulfillmentType.trim()
}
if (params.stockBindStatus?.trim()) {
body.stockBindStatus = params.stockBindStatus.trim()
}
if (params.fulfillProgress?.trim()) {
body.fulfillProgress = params.fulfillProgress.trim()
}
if (params.warehouseId != null && params.warehouseId > 0) {
body.warehouseId = params.warehouseId
}
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()
}
if (params.filters?.length) {
body.filters = params.filters
}
if (params.sortField?.trim()) {
body.sortField = params.sortField.trim()
}
if (params.sortOrder?.trim()) {
body.sortOrder = params.sortOrder.trim()
}
const res = await axiosInstance.post(orderUrl('/order/list'), body)
return parseOrderListPayload(res.data)
}
/** 各状态订单数量聚合 GET /order/status-counts(单次查询,非逐状态分页) */
export async function getOrderStatusCounts(params?: {
createTimeStart?: string
createTimeEnd?: string
fulfillmentType?: string
}): Promise<OrderStatusCountsResult> {
const res = await axiosInstance.get(orderUrl('/order/status-counts'), {
params: {
createTimeStart: params?.createTimeStart || undefined,
createTimeEnd: params?.createTimeEnd || undefined,
fulfillmentType: params?.fulfillmentType || undefined,
},
})
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 fulfillProgressCountsRaw = inner.fulfillProgressCounts ?? inner.FulfillProgressCounts ?? {}
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 : {},
fulfillProgressCounts:
typeof fulfillProgressCountsRaw === 'object' && fulfillProgressCountsRaw !== null
? fulfillProgressCountsRaw
: {},
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
fulfillmentType?: string
fulfillProgress?: string
keyword?: string
createTimeStart?: string
createTimeEnd?: string
filters?: OrderListFieldFilter[]
sortField?: string
sortOrder?: 'asc' | 'desc' | 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 async function postOrderPreDeclare(body: OrderIdRequest): Promise<void> {
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<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
downstreamServiceNo?: string
trackNo?: 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,
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<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',
reallocate: '/order/batch/reallocate',
}
/** 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; 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<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: ParsedOrderBatchJob
items: Array<ParsedOrderBatchJobItem>
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<number | string>
issueTemplateId?: number | string
issueType?: string
title?: string
content?: string
remark?: string
handlerNames?: string
}
export interface OrderExceptionEventItem {
id: number
orderId: number
issueType?: string
issueTypeName?: string
title?: string
content?: string
notifyMethod?: string
notifyMethodNames?: string
handlerNames?: string
handlerType?: string
handlerTypeName?: string
handlerUserId?: number
handleStatus?: string
handleStatusName?: string
createBy?: string
createTime?: string
processedBy?: string
processedTime?: string
remark?: string
}
export interface OrderExceptionEventListResult {
items: OrderExceptionEventItem[]
pendingCount: number
processedCount: number
}
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,
handlerNames: body.handlerNames,
})
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)
}
function mapExceptionEventItem(raw: any): OrderExceptionEventItem {
return {
id: Number(raw?.id ?? raw?.Id ?? 0),
orderId: Number(raw?.orderId ?? raw?.OrderId ?? 0),
issueType: raw?.issueType ?? raw?.IssueType,
issueTypeName: raw?.issueTypeName ?? raw?.IssueTypeName,
title: raw?.title ?? raw?.Title,
content: raw?.content ?? raw?.Content,
notifyMethod: raw?.notifyMethod ?? raw?.NotifyMethod,
notifyMethodNames: raw?.notifyMethodNames ?? raw?.NotifyMethodNames,
handlerNames: raw?.handlerNames ?? raw?.HandlerNames,
handlerType: raw?.handlerType ?? raw?.HandlerType,
handlerTypeName: raw?.handlerTypeName ?? raw?.HandlerTypeName,
handlerUserId: raw?.handlerUserId ?? raw?.HandlerUserId,
handleStatus: raw?.handleStatus ?? raw?.HandleStatus,
handleStatusName: raw?.handleStatusName ?? raw?.HandleStatusName,
createBy: raw?.createBy ?? raw?.CreateBy,
createTime: raw?.createTime ?? raw?.CreateTime,
processedBy: raw?.processedBy ?? raw?.ProcessedBy,
processedTime: raw?.processedTime ?? raw?.ProcessedTime,
remark: raw?.remark ?? raw?.Remark,
}
}
/** GET /order/exception-events/list */
export async function getOrderExceptionEventList(orderId: number | string): Promise<OrderExceptionEventListResult> {
const res = await axiosInstance.get(orderUrl('/order/exception-events/list'), {
params: { orderId: Number(orderId) },
})
const data = unwrapData<any>(res.data) ?? {}
const itemsRaw = data?.items ?? data?.Items ?? []
const items = Array.isArray(itemsRaw) ? itemsRaw.map(mapExceptionEventItem) : []
return {
items,
pendingCount: Number(data?.pendingCount ?? data?.PendingCount ?? 0),
processedCount: Number(data?.processedCount ?? data?.ProcessedCount ?? 0),
}
}
/** POST /order/exception-event/process */
export async function postOrderExceptionEventProcess(body: { id: number | string; remark?: string }) {
const res = await axiosInstance.post(orderUrl('/order/exception-event/process'), {
id: Number(body.id),
remark: body.remark,
})
assertBusinessOk(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',
fulfillmentType: params.fulfillmentType?.trim() || undefined,
fulfillProgress: params.fulfillProgress?.trim() || undefined,
keyword: params.keyword ?? '',
createTimeStart: params.createTimeStart ?? '',
createTimeEnd: params.createTimeEnd ?? '',
filters: params.filters,
sortField: params.sortField,
sortOrder: params.sortOrder,
})
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<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[]
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<string, unknown>
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<string, unknown>
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<PriceCheckShippingGroup[]> {
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<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 + senderAddressId */
export async function postOrderUpload(
file: File,
templateId: number,
senderAddressId?: number,
customerId?: 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))
}
if (customerId != null && customerId > 0) {
form.append('customerId', String(customerId))
}
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
skillKey?: string
}): Promise<OrderReceiverParseAddressResult> {
const res = await axiosInstance.post(orderUrl('/order/receiver/parse-address'), body)
assertBusinessOk(res.data)
const data = unwrapApiData<any>(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<string, any>): Promise<any> {
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<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/additional-services/update — 未付款订单更新附加服务 */
export async function postOrderAdditionalServicesUpdate(body: {
id: number | string
additionalServices: string[]
}): Promise<any> {
const res = await axiosInstance.post(orderUrl('/order/additional-services/update'), {
id: Number(body.id),
additionalServices: body.additionalServices ?? [],
})
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
}
/** POST /order/fee/rerun-inbound-billing — 重跑入库计费(应收+排货应付) */
export async function postOrderRerunInboundBilling(body: OrderIdRequest): Promise<any> {
const res = await axiosInstance.post(orderUrl('/order/fee/rerun-inbound-billing'), { id: body.id })
assertBusinessOk(res.data)
return res.data?.data ?? res.data?.Data ?? res.data
}
export type OrderReallocateScheme = 'costLowest' | 'timeFastest' | 'overallBest' | 'designated'
/** POST /order/fee/reallocate — 单票重新排货 */
export async function postOrderReallocate(body: {
id: number | string
scheme: OrderReallocateScheme
serviceChannelId?: number
}): Promise<any> {
const res = await axiosInstance.post(orderUrl('/order/fee/reallocate'), {
id: Number(body.id),
scheme: body.scheme,
serviceChannelId: body.serviceChannelId,
})
assertBusinessOk(res.data)
return res.data?.data ?? res.data?.Data ?? res.data
}
/** POST /order/fee/reallocate-channel-options — 指定渠道可选交集 */
export async function postOrderReallocateChannelOptions(body: {
orderIds: Array<number | string>
}): Promise<Array<{ serviceChannelId: number; channelCode: string; channelName?: string }>> {
const res = await axiosInstance.post(orderUrl('/order/fee/reallocate-channel-options'), {
orderIds: body.orderIds.map((id) => Number(id)),
})
assertBusinessOk(res.data)
const raw = res.data?.data ?? res.data?.Data ?? res.data
const items = raw?.items ?? raw?.Items ?? (Array.isArray(raw) ? raw : [])
return (items || []).map((row: any) => ({
serviceChannelId: Number(row.serviceChannelId ?? row.ServiceChannelId),
channelCode: String(row.channelCode ?? row.ChannelCode ?? ''),
channelName: row.channelName ?? row.ChannelName,
}))
}
/** POST /order/fee/reallocate-batch — 批量重新排货(同步兜底) */
export async function postOrderReallocateBatch(body: {
orderIds: Array<number | string>
scheme: OrderReallocateScheme
serviceChannelId?: number
}): Promise<{ successCount: number; failures: Array<{ orderId: number; serviceNo?: string; reason: string }> }> {
const res = await axiosInstance.post(orderUrl('/order/fee/reallocate-batch'), {
orderIds: body.orderIds.map((id) => Number(id)),
scheme: body.scheme,
serviceChannelId: body.serviceChannelId,
})
assertBusinessOk(res.data)
const raw = res.data?.data ?? res.data?.Data ?? res.data
return {
successCount: Number(raw?.successCount ?? raw?.SuccessCount ?? 0),
failures: (raw?.failures ?? raw?.Failures ?? []).map((f: any) => ({
orderId: Number(f.orderId ?? f.OrderId),
serviceNo: f.serviceNo ?? f.ServiceNo,
reason: String(f.reason ?? f.Reason ?? ''),
})),
}
}
export interface OrderAllocationCandidate {
serviceChannelId: number
channelCode: string
channelName?: string
totalPayableCost: number
minTime?: number | null
weight: number
selected: boolean
feeLines: Array<{ costItem?: string; amount: number; currency?: string }>
}
export interface OrderAllocationTrace {
orderId: number
fromSnapshot: boolean
scheme?: string
schemeName?: string
schemeSource?: string
schemeSourceName?: string
chargeableWeight?: number | null
chargingStage?: string
chargingStageName?: string
selectedChannelCode?: string
selectedChannelName?: string
failReason?: string
traceTime?: string
candidates: OrderAllocationCandidate[]
skipped: Array<{
serviceChannelId?: number
channelCode?: string
channelName?: string
reason: string
}>
}
/** GET /order/allocation-trace */
export async function getOrderAllocationTrace(params: {
id: number | string
live?: boolean
}): Promise<OrderAllocationTrace> {
const res = await axiosInstance.get(orderUrl('/order/allocation-trace'), {
params: { id: Number(params.id), live: params.live ? true : undefined },
})
assertBusinessOk(res.data)
const raw = res.data?.data ?? res.data?.Data ?? res.data
const mapCandidate = (c: any): OrderAllocationCandidate => ({
serviceChannelId: Number(c.serviceChannelId ?? c.ServiceChannelId ?? 0),
channelCode: String(c.channelCode ?? c.ChannelCode ?? ''),
channelName: c.channelName ?? c.ChannelName,
totalPayableCost: Number(c.totalPayableCost ?? c.TotalPayableCost ?? 0),
minTime: c.minTime ?? c.MinTime ?? null,
weight: Number(c.weight ?? c.Weight ?? 0),
selected: Boolean(c.selected ?? c.Selected),
feeLines: (c.feeLines ?? c.FeeLines ?? []).map((f: any) => ({
costItem: f.costItem ?? f.CostItem,
amount: Number(f.amount ?? f.Amount ?? 0),
currency: f.currency ?? f.Currency,
})),
})
return {
orderId: Number(raw?.orderId ?? raw?.OrderId ?? params.id),
fromSnapshot: Boolean(raw?.fromSnapshot ?? raw?.FromSnapshot),
scheme: raw?.scheme ?? raw?.Scheme,
schemeName: raw?.schemeName ?? raw?.SchemeName,
schemeSource: raw?.schemeSource ?? raw?.SchemeSource,
schemeSourceName: raw?.schemeSourceName ?? raw?.SchemeSourceName,
chargeableWeight: raw?.chargeableWeight ?? raw?.ChargeableWeight,
chargingStage: raw?.chargingStage ?? raw?.ChargingStage,
chargingStageName: raw?.chargingStageName ?? raw?.ChargingStageName,
selectedChannelCode: raw?.selectedChannelCode ?? raw?.SelectedChannelCode,
selectedChannelName: raw?.selectedChannelName ?? raw?.SelectedChannelName,
failReason: raw?.failReason ?? raw?.FailReason,
traceTime: raw?.traceTime ?? raw?.TraceTime,
candidates: (raw?.candidates ?? raw?.Candidates ?? []).map(mapCandidate),
skipped: (raw?.skipped ?? raw?.Skipped ?? []).map((s: any) => ({
serviceChannelId: s.serviceChannelId ?? s.ServiceChannelId,
channelCode: s.channelCode ?? s.ChannelCode,
channelName: s.channelName ?? s.ChannelName,
reason: String(s.reason ?? s.Reason ?? ''),
})),
}
}
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
}
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<T = any>(body: any): T {
return (body?.data ?? body?.Data ?? body) as T
}
/** GET /order/stock-bind/detail */
export async function getOrderStockBindDetail(orderId: number): Promise<OrderStockBindDetail> {
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<any>(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<any>(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<OrderStockBindDetail> {
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<OrderStockBindDetail> {
const res = await axiosInstance.post(orderUrl('/order/stock-bind/clear'), { orderId })
assertBusinessOk(res.data)
return unwrapData(res.data)
}
/** POST /order/stock-bind/confirm-deduct */
export async function postOrderStockBindConfirmDeduct(orderId: number): Promise<OrderStockBindDetail> {
const res = await axiosInstance.post(orderUrl('/order/stock-bind/confirm-deduct'), { 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<string, OrderFileItem[]>
}
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<OrderFileGroupedResult> {
const res = await axiosInstance.get(orderUrl('/order/files'), { params: { orderId } })
assertBusinessOk(res.data)
const data = unwrapData<any>(res.data)
const rawGroups = data?.groups ?? data?.Groups ?? {}
const groups: Record<string, OrderFileItem[]> = {}
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<OrderFileItem[]> {
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<any>(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<void> {
const res = await axiosInstance.post(orderUrl('/order/files/delete'), { id })
assertBusinessOk(res.data)
}