Files
admin-web/src/modules/unified-orders/workflows/api.ts
T
2026-09-01 20:07:40 +08:00

534 lines
20 KiB
TypeScript

import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
const inboundBasePath = '/platform/v1/inbounds'
export interface OutboundGateItem { code: string; name: string; passed: boolean; actual?: string | null; requirement?: string | null }
export interface OutboundDispatchAggregate {
orderId: string | null
orderNo: string | null
inboundId: string | null
inboundCode: string | null
containerBookingId: string | null
containerNo: string | null
loadingPlanId: string | null
loadingPlanStatus: string | null
oceanBookingId: string | null
oceanBookingStatus: string | null
truckAppointmentId: string | null
truckAppointmentStatus: string | null
customsStatus?: string | null
sailingStatus?: string | null
gate: { passed: boolean; items: OutboundGateItem[] } | null
}
export interface ContainerBooking {
id: string
containerNo: string | null
cabinetNo: string | null
warehouseId: string | null
warehouseName: string | null
destinationWarehouseCode: string | null
destinationWarehouseName: string | null
capacityCbm: number | null
maxWeightKg: number | null
version: number
}
export interface SmartLoadingCandidate {
accountId: string
inboundId: string
inboundDetailId: string
orderId: string
orderNo: string | null
inboundCode: string | null
warehouseId: string
destinationWarehouseId: string | null
destinationWarehouseCode: string | null
destinationWarehouseName: string | null
customerName: string | null
goodsName: string | null
availablePackageCount: number
unitWeightKg: number
unitVolumeCbm: number
}
export interface SmartLoadingPlanItem {
id: string
loadingPlanId: string
accountId: string
customerName: string | null
inboundId: string
inboundDetailId: string
orderId: string
orderNo: string | null
inboundCode: string | null
destinationWarehouseCode: string | null
destinationWarehouseName: string | null
skuCode: string | null
productNameZh: string | null
packageCount: number
grossWeightKg: number
volumeCbm: number
loadSequence: number | null
version: number
}
export interface SmartLoadingPlan {
id: string
planNo: string | null
status: string
bookingId: string
containerNo: string | null
voyageNo: string | null
version: number
items: SmartLoadingPlanItem[]
}
export interface CreateSmartLoadingPlanRequest { bookingId: string; remark?: string }
export interface AssignSmartLoadingItemRequest {
inboundId: string
inboundDetailId: string
packageCount: number
expectedVersion: number
operationId: string
}
export interface RecommendSmartLoadingRequest { expectedVersion: number; operationId: string }
export interface AdjustSmartLoadingItemRequest {
packageCount: number
expectedPlanVersion: number
expectedItemVersion: number
operationId: string
}
export interface DeleteSmartLoadingItemRequest {
expectedPlanVersion: number
expectedItemVersion: number
operationId: string
}
export type PreloadStatus = 'PENDING' | 'CONFIRMING' | 'CONFIRMED' | string
/** 五类装柜取证。旧接口仍使用 PHOTO_STEP_* / SEAL 编码,页面统一映射为业务名称。 */
export type EvidenceType = 'PHOTO_STEP_1' | 'PHOTO_STEP_2' | 'PHOTO_STEP_3' | 'PHOTO_STEP_4' | 'SEAL'
export interface PreloadItem {
containerCode: string | null
cargoReference: string | null
packageCount: number | null
volumeCbm: number | null
remark: string | null
}
export interface PreloadPlan {
id: string
inboundId: string
tenantId: string
status: PreloadStatus
version: number
items: PreloadItem[]
remark: string | null
createdAt: string | null
updatedAt: string | null
}
export interface SplitActionResponse {
mainStatus: string
preloadPlan: PreloadPlan | null
}
export interface PickingTask {
id: number | string
inboundId: number | string
status: string
version: number
expectedCount: number | null
pickedCount: number | null
remark: string | null
}
export interface Evidence {
id: number | string
inboundId: number | string
tenantId: number | string
evidenceType: EvidenceType
fileId: number | string | null
fileName: string | null
sealNo: string | null
remark: string | null
version: number
status: 'ACTIVE' | 'VOIDED' | string
createdAt: string | null
updatedAt: string | null
voidReason?: string | null
voidedBy?: number | string | null
voidedByName?: string | null
voidedAt?: string | null
}
export interface EvidenceList {
inboundId: number | string
evidences: Evidence[]
}
export interface LoadingActionResponse {
mainStatus: string
evidences: EvidenceList | null
}
export interface PickingRequest {
expectedVersion: number
expectedCount: number
pickedCount: number
remark?: string
}
export interface SmartSplitRequest {
expectedVersion: number
items: PreloadItem[]
remark?: string
}
export interface UpsertEvidenceRequest {
expectedVersion: number
evidenceType: EvidenceType
fileId?: number | string
fileName?: string
sealNo?: string
remark?: string
}
export interface UploadEvidenceResult {
fileId: number | string
fileName: string | null
contentType?: string | null
sizeBytes?: number | null
}
export interface VoidEvidenceRequest {
expectedVersion: number
reason: string
}
export interface CompleteLoadingRequest {
expectedVersion: number
remark?: string
}
function query(tenantId: string | number) {
return { tenantId: String(tenantId) }
}
function idempotencyKey(operation: string, inboundId: string | number) {
return `admin-${operation}-${inboundId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
}
function operationId(operation: string, resourceId: string | number) {
return `admin-${operation}-${resourceId}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
}
function nullableId(value: number | string | null | undefined) {
return value === null || value === undefined || value === '' ? null : String(value)
}
function numberValue(value: number | string | null | undefined, fallback = 0) {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : fallback
}
function normalizeContainer(raw: Record<string, unknown>): ContainerBooking {
return {
id: String(raw.id),
containerNo: raw.containerNo == null ? null : String(raw.containerNo),
cabinetNo: raw.cabinetNo == null ? null : String(raw.cabinetNo),
warehouseId: nullableId(raw.warehouseId as number | string | null),
warehouseName: raw.warehouseName == null ? null : String(raw.warehouseName),
destinationWarehouseCode: raw.destinationWarehouseCode == null ? null : String(raw.destinationWarehouseCode),
destinationWarehouseName: raw.destinationWarehouseName == null ? null : String(raw.destinationWarehouseName),
capacityCbm: raw.capacityCbm == null ? null : numberValue(raw.capacityCbm as number | string),
maxWeightKg: raw.maxWeightKg == null ? null : numberValue(raw.maxWeightKg as number | string),
version: numberValue(raw.version as number | string),
}
}
function normalizeCandidate(raw: Record<string, unknown>): SmartLoadingCandidate {
return {
accountId: String(raw.accountId), inboundId: String(raw.inboundId), inboundDetailId: String(raw.inboundDetailId),
orderId: String(raw.orderId), orderNo: raw.orderNo == null ? null : String(raw.orderNo),
inboundCode: raw.inboundCode == null ? null : String(raw.inboundCode), warehouseId: String(raw.warehouseId),
destinationWarehouseId: nullableId(raw.destinationWarehouseId as number | string | null),
destinationWarehouseCode: raw.destinationWarehouseCode == null ? null : String(raw.destinationWarehouseCode),
destinationWarehouseName: raw.destinationWarehouseName == null ? null : String(raw.destinationWarehouseName),
customerName: raw.customerName == null ? null : String(raw.customerName),
goodsName: raw.goodsName == null ? null : String(raw.goodsName),
availablePackageCount: numberValue(raw.availablePackageCount as number | string),
unitWeightKg: numberValue(raw.unitWeightKg as number | string), unitVolumeCbm: numberValue(raw.unitVolumeCbm as number | string),
}
}
function normalizeSmartLoadingItem(raw: Record<string, unknown>): SmartLoadingPlanItem {
return {
id: String(raw.id), loadingPlanId: String(raw.loadingPlanId), accountId: String(raw.accountId),
customerName: raw.customerName == null ? null : String(raw.customerName), inboundId: String(raw.inboundId),
inboundDetailId: String(raw.inboundDetailId), orderId: String(raw.orderId),
orderNo: raw.orderNo == null ? null : String(raw.orderNo), inboundCode: raw.inboundCode == null ? null : String(raw.inboundCode),
destinationWarehouseCode: raw.destinationWarehouseCode == null ? null : String(raw.destinationWarehouseCode),
destinationWarehouseName: raw.destinationWarehouseName == null ? null : String(raw.destinationWarehouseName),
skuCode: raw.skuCode == null ? null : String(raw.skuCode), productNameZh: raw.productNameZh == null ? null : String(raw.productNameZh),
packageCount: numberValue(raw.packageCount as number | string), grossWeightKg: numberValue(raw.grossWeightKg as number | string),
volumeCbm: numberValue(raw.volumeCbm as number | string), loadSequence: raw.loadSequence == null ? null : numberValue(raw.loadSequence as number | string),
version: numberValue(raw.version as number | string),
}
}
function normalizeSmartLoadingPlan(raw: Record<string, unknown>): SmartLoadingPlan {
return {
id: String(raw.id), planNo: raw.planNo == null ? null : String(raw.planNo), status: String(raw.status || ''),
bookingId: String(raw.bookingId), containerNo: raw.containerNo == null ? null : String(raw.containerNo),
voyageNo: raw.voyageNo == null ? null : String(raw.voyageNo), version: numberValue(raw.version as number | string),
items: Array.isArray(raw.items) ? raw.items.map((item) => normalizeSmartLoadingItem(item as Record<string, unknown>)) : [],
}
}
function normalizeOutboundDispatch(raw: Record<string, unknown>): OutboundDispatchAggregate {
return {
orderId: nullableId(raw.orderId as number | string | null),
orderNo: raw.orderNo == null ? null : String(raw.orderNo),
inboundId: nullableId(raw.inboundId as number | string | null),
inboundCode: raw.inboundCode == null ? null : String(raw.inboundCode),
containerBookingId: nullableId(raw.containerBookingId as number | string | null),
containerNo: raw.containerNo == null ? null : String(raw.containerNo),
loadingPlanId: nullableId(raw.loadingPlanId as number | string | null),
loadingPlanStatus: raw.loadingPlanStatus == null ? null : String(raw.loadingPlanStatus),
oceanBookingId: nullableId(raw.oceanBookingId as number | string | null),
oceanBookingStatus: raw.oceanBookingStatus == null ? null : String(raw.oceanBookingStatus),
truckAppointmentId: nullableId(raw.truckAppointmentId as number | string | null),
truckAppointmentStatus: raw.truckAppointmentStatus == null ? null : String(raw.truckAppointmentStatus),
customsStatus: raw.customsStatus == null ? null : String(raw.customsStatus),
sailingStatus: raw.sailingStatus == null ? null : String(raw.sailingStatus),
gate: raw.gate == null ? null : raw.gate as OutboundDispatchAggregate['gate'],
}
}
function normalizePreloadPlan(raw: Record<string, unknown>): PreloadPlan {
return {
id: String(raw.id), inboundId: String(raw.inboundId), tenantId: String(raw.tenantId),
status: String(raw.status || ''), version: numberValue(raw.version as number | string),
items: Array.isArray(raw.items) ? raw.items.map((item) => ({ ...(item as PreloadItem) })) : [],
remark: raw.remark == null ? null : String(raw.remark),
createdAt: raw.createdAt == null ? null : String(raw.createdAt),
updatedAt: raw.updatedAt == null ? null : String(raw.updatedAt),
}
}
function normalizeSplitAction(raw: Record<string, unknown>): SplitActionResponse {
return {
mainStatus: String(raw.mainStatus || ''),
preloadPlan: raw.preloadPlan == null ? null : normalizePreloadPlan(raw.preloadPlan as Record<string, unknown>),
}
}
const smartLoadingBasePath = '/platform/v1/first-leg/smart-loading'
export async function getContainerBooking(bookingId: string | number) {
const response = await http.get<ApiEnvelope<Record<string, unknown>>, ApiEnvelope<Record<string, unknown>>>(
`/platform/v1/containers/${bookingId}`,
)
return normalizeContainer(unwrapApiResponse(response))
}
export async function listSmartLoadingCandidates(bookingId: string | number, warehouseId: string | number) {
const response = await http.get<ApiEnvelope<Record<string, unknown>[]>, ApiEnvelope<Record<string, unknown>[]>>(
`${smartLoadingBasePath}/candidates`, { params: { bookingId: String(bookingId), warehouseId: String(warehouseId) } },
)
return unwrapApiResponse(response).map(normalizeCandidate)
}
export async function getSmartLoadingPlan(planId: string | number) {
const response = await http.get<ApiEnvelope<Record<string, unknown>>, ApiEnvelope<Record<string, unknown>>>(
`${smartLoadingBasePath}/plans/${planId}`,
)
return normalizeSmartLoadingPlan(unwrapApiResponse(response))
}
export async function getSmartLoadingPlanByBooking(bookingId: string | number) {
const response = await http.get<ApiEnvelope<Record<string, unknown>>, ApiEnvelope<Record<string, unknown>>>(
`${smartLoadingBasePath}/plans/by-booking/${bookingId}`,
)
return normalizeSmartLoadingPlan(unwrapApiResponse(response))
}
export async function assignSmartLoadingItem(planId: string | number, request: Omit<AssignSmartLoadingItemRequest, 'operationId'>) {
const response = await http.post<ApiEnvelope<Record<string, unknown>>, ApiEnvelope<Record<string, unknown>>>(
`${smartLoadingBasePath}/plans/${planId}/assignments`,
{ ...request, operationId: operationId('smart-loading-assign', planId) },
)
return normalizeSmartLoadingPlan(unwrapApiResponse(response))
}
export async function recommendSmartLoading(planId: string | number, expectedVersion: number) {
const response = await http.post<ApiEnvelope<Record<string, unknown>>, ApiEnvelope<Record<string, unknown>>>(
`${smartLoadingBasePath}/plans/${planId}/recommend`,
{ expectedVersion, operationId: operationId('smart-loading-recommend', planId) },
)
return normalizeSmartLoadingPlan(unwrapApiResponse(response))
}
export async function adjustSmartLoadingItem(
planId: string | number,
itemId: string | number,
request: Omit<AdjustSmartLoadingItemRequest, 'operationId'>,
) {
const response = await http.patch<ApiEnvelope<Record<string, unknown>>, ApiEnvelope<Record<string, unknown>>>(
`${smartLoadingBasePath}/plans/${planId}/items/${itemId}`,
{ ...request, operationId: operationId('smart-loading-adjust', itemId) },
)
return normalizeSmartLoadingPlan(unwrapApiResponse(response))
}
export async function deleteSmartLoadingItem(
planId: string | number,
itemId: string | number,
request: Omit<DeleteSmartLoadingItemRequest, 'operationId'>,
) {
const response = await http.delete<ApiEnvelope<Record<string, unknown>>, ApiEnvelope<Record<string, unknown>>>(
`${smartLoadingBasePath}/plans/${planId}/items/${itemId}`,
{ data: { ...request, operationId: operationId('smart-loading-delete', itemId) } },
)
return normalizeSmartLoadingPlan(unwrapApiResponse(response))
}
function writeOptions(operation: string, inboundId: string | number, tenantId: string | number) {
return { params: query(tenantId), headers: { 'Idempotency-Key': idempotencyKey(operation, inboundId) } }
}
export async function getPreload(tenantId: string | number, inboundId: string | number) {
const response = await http.get<ApiEnvelope<Record<string, unknown>>, ApiEnvelope<Record<string, unknown>>>(
`${inboundBasePath}/${inboundId}/preload`, { params: query(tenantId) },
)
return normalizePreloadPlan(unwrapApiResponse(response))
}
export async function getOutboundDispatch(tenantId: string | number, orderId?: string | number, inboundId?: string | number) {
const response = await http.get<ApiEnvelope<Record<string, unknown>[]>, ApiEnvelope<Record<string, unknown>[]>>(
'/platform/v1/first-leg/outbound-dispatches',
{ params: { tenantId: String(tenantId), ...(orderId ? { orderId: String(orderId) } : {}), ...(inboundId ? { inboundId: String(inboundId) } : {}) } },
)
const result = unwrapApiResponse(response)
return result?.[0] ? normalizeOutboundDispatch(result[0]) : null
}
export async function smartSplit(
tenantId: string | number,
inboundId: string | number,
request: SmartSplitRequest,
) {
const response = await http.post<ApiEnvelope<Record<string, unknown>>, ApiEnvelope<Record<string, unknown>>>(
`${inboundBasePath}/${inboundId}/split/smart`, request, writeOptions('smart-split', inboundId, tenantId),
)
return normalizeSplitAction(unwrapApiResponse(response))
}
export async function getPicking(tenantId: string | number, inboundId: string | number) {
const response = await http.get<ApiEnvelope<PickingTask>, ApiEnvelope<PickingTask>>(
`${inboundBasePath}/${inboundId}/loading/picking`, { params: query(tenantId) },
)
return unwrapApiResponse(response)
}
export async function startPicking(
tenantId: string | number,
inboundId: string | number,
request: PickingRequest,
) {
const response = await http.post<ApiEnvelope<PickingTask>, ApiEnvelope<PickingTask>>(
`${inboundBasePath}/${inboundId}/loading/picking/start`, request,
writeOptions('picking-start', inboundId, tenantId),
)
return unwrapApiResponse(response)
}
export async function completePicking(
tenantId: string | number,
inboundId: string | number,
request: PickingRequest,
) {
const response = await http.post<ApiEnvelope<PickingTask>, ApiEnvelope<PickingTask>>(
`${inboundBasePath}/${inboundId}/loading/picking/complete`, request,
writeOptions('picking-complete', inboundId, tenantId),
)
return unwrapApiResponse(response)
}
export async function getEvidence(tenantId: string | number, inboundId: string | number) {
const response = await http.get<ApiEnvelope<EvidenceList>, ApiEnvelope<EvidenceList>>(
`${inboundBasePath}/${inboundId}/loading/evidence`, { params: query(tenantId) },
)
return unwrapApiResponse(response)
}
export async function upsertEvidence(
tenantId: string | number,
inboundId: string | number,
request: UpsertEvidenceRequest,
) {
const response = await http.post<ApiEnvelope<LoadingActionResponse>, ApiEnvelope<LoadingActionResponse>>(
`${inboundBasePath}/${inboundId}/loading/evidence`, request,
writeOptions(`evidence-${request.evidenceType}`, inboundId, tenantId),
)
return unwrapApiResponse(response)
}
/** 真实 File 服务上传;返回的 fileId 才能提交证据,不接受本地文件名冒充。 */
export async function uploadEvidenceFile(
tenantId: string | number,
inboundId: string | number,
evidenceType: EvidenceType,
file: File,
) {
const body = new FormData()
body.append('evidenceType', evidenceType)
body.append('file', file)
const response = await http.post<ApiEnvelope<UploadEvidenceResult>, ApiEnvelope<UploadEvidenceResult>>(
`/platform/v1/files/loading-evidence/${tenantId}/${inboundId}`,
body,
{ headers: { 'Content-Type': 'multipart/form-data' } },
)
return unwrapApiResponse(response)
}
export function downloadEvidenceFile(
tenantId: string | number,
inboundId: string | number,
fileId: string | number,
) {
return http.get<Blob, Blob>(
`/platform/v1/files/loading-evidence/${encodeURIComponent(String(tenantId))}/${encodeURIComponent(String(inboundId))}/${encodeURIComponent(String(fileId))}`,
{ responseType: 'blob' },
)
}
export async function voidEvidence(
tenantId: string | number,
inboundId: string | number,
evidenceId: string | number,
request: VoidEvidenceRequest,
) {
const response = await http.post<ApiEnvelope<LoadingActionResponse>, ApiEnvelope<LoadingActionResponse>>(
`${inboundBasePath}/${inboundId}/loading/evidence/${evidenceId}/void`,
request,
writeOptions('evidence-void', inboundId, tenantId),
)
return unwrapApiResponse(response)
}
export async function completeLoading(
tenantId: string | number,
inboundId: string | number,
request: CompleteLoadingRequest,
) {
const response = await http.post<ApiEnvelope<LoadingActionResponse>, ApiEnvelope<LoadingActionResponse>>(
`${inboundBasePath}/${inboundId}/loading/complete`, request,
writeOptions('loading-complete', inboundId, tenantId),
)
return unwrapApiResponse(response)
}