官网/H5 支持企微客服跳转;用户资料新增 WecomKfUrl;工作台与下单相关体验一并更新。 Co-authored-by: Cursor <cursoragent@cursor.com>
2357 lines
77 KiB
TypeScript
2357 lines
77 KiB
TypeScript
/**
|
||
* 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)
|
||
}
|