feat(admin): bind booking documents and truck containers

This commit is contained in:
2026-08-31 19:54:37 +08:00
parent 0f4f635686
commit a759eb1b6b
24 changed files with 691 additions and 179 deletions
@@ -1,7 +1,9 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { OceanBookingAgentOption, OceanBookingCarrierCode, OceanBookingDetail, OceanBookingLinkCandidatePage, OceanBookingLinkCandidateQuery, OceanBookingPage, OceanBookingQuery, OceanBookingResource, OceanBookingSaveRequest, OceanBookingStatus, OceanBookingReleaseRequest } from '../types'
import axios from 'axios'
import { ApiResponseError, http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { OceanBookingAgentOption, OceanBookingCarrierCode, OceanBookingDetail, OceanBookingDocument, OceanBookingDocumentInvalidation, OceanBookingDocumentMutation, OceanBookingDocumentType, OceanBookingLinkCandidatePage, OceanBookingLinkCandidateQuery, OceanBookingPage, OceanBookingPrintType, OceanBookingQuery, OceanBookingResource, OceanBookingSaveRequest, OceanBookingStatus, OceanBookingReleaseRequest } from '../types'
const basePath = '/platform/v1/ocean-bookings'
const maxErrorEnvelopeBytes = 1024 * 1024
export interface OceanLogisticsChannel {
channelCode: string
@@ -51,6 +53,7 @@ function detail(raw: OceanBookingDetail): OceanBookingDetail {
capacityCbm: Number(container.capacityCbm),
maxWeightKg: Number(container.maxWeightKg),
})),
documents: (raw.documents || []).map(document),
histories,
statusHistory: histories,
}
@@ -87,7 +90,74 @@ export async function createOceanBooking(payload: OceanBookingSaveRequest) { con
export async function updateOceanBookingDraft(id: string, payload: OceanBookingSaveRequest) { const response = await http.put<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}`, payload, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
export async function deleteOceanBookingDraft(id: string, expectedVersion: number) { await http.delete(`${basePath}/${encodeURIComponent(id)}`, { data: { expectedVersion }, headers: { 'Idempotency-Key': idempotencyKey() } }) }
export async function releaseOceanBooking(id: string, request: OceanBookingReleaseRequest) { const response = await http.post<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}/release`, request, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
export async function printOceanBooking(id: string, type: 'SHIPPING_INSTRUCTION' | 'BILL_CONFIRMATION') { return http.get<Blob, Blob>(`${basePath}/${encodeURIComponent(id)}/print`, { params: { type }, responseType: 'blob' }) }
const printPaths: Record<OceanBookingPrintType, string> = {
SHIPPING_INSTRUCTION: 'shipping-instruction',
BILL_OF_LADING_CONFIRMATION: 'bill-of-lading-confirmation',
}
function document(raw: OceanBookingDocument): OceanBookingDocument {
return {
...raw,
id: String(raw.id),
oceanBookingId: String(raw.oceanBookingId),
fileId: String(raw.fileId),
sizeBytes: raw.sizeBytes == null ? null : Number(raw.sizeBytes),
uploadedBy: String(raw.uploadedBy),
invalidatedBy: raw.invalidatedBy == null ? null : String(raw.invalidatedBy),
}
}
export async function downloadOceanBookingPrint(id: string, type: OceanBookingPrintType) {
return requestBlob(`${basePath}/${encodeURIComponent(id)}/prints/${printPaths[type]}`)
}
async function parseBlobError(blob: Blob, status?: number) {
if (!blob.type.toLowerCase().includes('json')) return null
if (blob.size > maxErrorEnvelopeBytes) return new ApiResponseError('文件接口返回了超出限制的 JSON 响应', 'INVALID_FILE_RESPONSE', status)
try {
const payload = JSON.parse(await blob.text()) as Partial<ApiEnvelope<unknown>>
return new ApiResponseError(payload.code === '200' ? '文件接口未返回文件内容' : payload.message || '文件请求失败', payload.code === '200' ? 'INVALID_FILE_RESPONSE' : payload.code || (status ? `HTTP_${status}` : 'FILE_REQUEST_FAILED'), status, payload.data, payload.requestId, payload.traceId)
} catch {
return new ApiResponseError('文件接口返回了无法识别的 JSON 响应', 'INVALID_FILE_RESPONSE', status)
}
}
async function requestBlob(path: string) {
try {
const blob = await http.get<Blob, Blob>(path, { responseType: 'blob' })
const responseError = await parseBlobError(blob, 200)
if (responseError) throw responseError
return blob
} catch (error) {
if (axios.isAxiosError(error) && error.response?.data instanceof Blob) {
const responseError = await parseBlobError(error.response.data, error.response.status)
if (responseError) throw responseError
}
throw error
}
}
export async function listOceanBookingDocuments(id: string) {
const response = await http.get<ApiEnvelope<OceanBookingDocument[]>, ApiEnvelope<OceanBookingDocument[]>>(`${basePath}/${encodeURIComponent(id)}/documents`)
return (unwrapApiResponse(response) || []).map(document)
}
export async function referenceOceanBookingDocument(id: string, expectedVersion: number, fileId: string, documentType: OceanBookingDocumentType) {
const response = await http.post<ApiEnvelope<OceanBookingDocumentMutation>, ApiEnvelope<OceanBookingDocumentMutation>>(`${basePath}/${encodeURIComponent(id)}/documents`, { expectedVersion, fileId, documentType }, { headers: { 'Idempotency-Key': idempotencyKey() } })
const result = unwrapApiResponse(response)
return { document: document(result.document), bookingVersion: Number(result.bookingVersion) }
}
export async function uploadOceanBookingDocument(id: string, expectedVersion: number, documentType: OceanBookingDocumentType, file: File) {
const form = new FormData()
form.append('expectedVersion', String(expectedVersion))
form.append('documentType', documentType)
form.append('file', file)
const response = await http.post<ApiEnvelope<OceanBookingDocumentMutation>, ApiEnvelope<OceanBookingDocumentMutation>>(`${basePath}/${encodeURIComponent(id)}/documents/upload`, form, { headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': idempotencyKey() } })
const result = unwrapApiResponse(response)
return { document: document(result.document), bookingVersion: Number(result.bookingVersion) }
}
export async function invalidateOceanBookingDocument(id: string, documentId: string, expectedVersion: number) {
const response = await http.delete<ApiEnvelope<OceanBookingDocumentInvalidation>, ApiEnvelope<OceanBookingDocumentInvalidation>>(`${basePath}/${encodeURIComponent(id)}/documents/${encodeURIComponent(documentId)}`, { data: { expectedVersion }, headers: { 'Idempotency-Key': idempotencyKey() } })
const result = unwrapApiResponse(response)
return { documentId: String(result.documentId), bookingVersion: Number(result.bookingVersion) }
}
export async function downloadOceanBookingDocument(id: string, documentId: string) {
return requestBlob(`${basePath}/${encodeURIComponent(id)}/documents/${encodeURIComponent(documentId)}/download`)
}
export async function listWarehouseLogisticsChannels(warehouseId: string): Promise<OceanLogisticsChannel[]> {
const response = await http.get<ApiEnvelope<OceanLogisticsChannel[]>, ApiEnvelope<OceanLogisticsChannel[]>>(`${basePath}/channels`, { params: { originWarehouseId: warehouseId } })
return (unwrapApiResponse(response) || []).map((item) => ({ channelCode: String(item.channelCode), channelName: String(item.channelName) }))