fix: align audit and dispatch workflows and release failed requests

This commit is contained in:
2026-10-22 23:10:09 +08:00
parent 9cdcb9c077
commit 4a3456756f
124 changed files with 1296 additions and 2120 deletions
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import { ApiResponseError, http } from '@/shared/api/http'
import { createDeclarationBatch, submitDeclarationBatch, recordClearanceResult, type DeclarationBatch } from './platformContainerCustoms'
import { completeAllBuyContainerCustoms, downloadContainerCustomsReceipt, downloadContainerCustomsDocument, getContainerCustoms, sendContainerCustomsToBroker, startContainerCustoms, uploadContainerCustomsReceipt } from './platformContainerCustoms'
import { completeAllBuyContainerCustoms, downloadContainerCustomsReceipt, downloadContainerCustomsDocument, downloadContainerCustomsFinal, getContainerCustoms, sendContainerCustomsToBroker, startContainerCustoms, uploadContainerCustomsReceipt } from './platformContainerCustoms'
const customs = {
aggregateId: 2092438843630391401n, loadingPlanId: 2092438843630391402n, containerBookingId: 2092438843630391403n,
@@ -99,7 +99,42 @@ describe('platform container customs API', () => {
expect(get.mock.calls).toEqual([
[`/platform/v1/customs/loading-plans/documents/${documentId}/download`, { responseType: 'blob' }],
[`/platform/v1/customs/loading-plans/documents/${documentId}/receipt/download`, { responseType: 'blob' }],
[`/platform/v1/customs/loading-plans/documents/${documentId}/completion/download`, { responseType: 'blob' }],
])
})
it.each([downloadContainerCustomsDocument, downloadContainerCustomsReceipt, downloadContainerCustomsFinal])('拒绝 HTTP 200 中的真实 JSON Blob 业务错误 %#', async (download) => {
const body = new Blob([JSON.stringify({ code: 'A0404', message: '预录单不存在', data: null, requestId: 'request-customs-blob', traceId: 'trace-customs-blob' })], { type: 'application/json;charset=UTF-8' })
vi.spyOn(http, 'get').mockResolvedValue(body)
const error: unknown = await download('2092438843630391407').catch((reason: unknown) => reason)
expect(error).toBeInstanceOf(ApiResponseError)
expect(error).toMatchObject({ code: 'A0404', message: '预录单不存在', status: 200, requestId: 'request-customs-blob', traceId: 'trace-customs-blob' })
})
it('非 2xx 的 JSON Blob 也保留业务错误和诊断信息', async () => {
const body = new Blob([JSON.stringify({ code: 'A0403', message: '无权下载预录单', data: null, requestId: 'request-forbidden', traceId: 'trace-forbidden' })], { type: 'application/json' })
vi.spyOn(http, 'get').mockRejectedValue({ isAxiosError: true, response: { status: 403, data: body } })
await expect(downloadContainerCustomsReceipt('7')).rejects.toMatchObject({ code: 'A0403', message: '无权下载预录单', status: 403, requestId: 'request-forbidden', traceId: 'trace-forbidden' })
})
it.each([
['application/pdf', '%PDF-1.7 原文件'],
['application/octet-stream', '原始二进制'],
['application/json', '{"product":"报关资料","code":"ITEM-1"}'],
['application/json', '{invalid-json'],
])('保留原文件内容与对象:%s', async (type, contents) => {
const body = new Blob([contents], { type })
vi.spyOn(http, 'get').mockResolvedValue(body)
await expect(downloadContainerCustomsFinal('7')).resolves.toBe(body)
})
it('非 JSON 的传输错误沿用原始错误', async () => {
const error = { isAxiosError: true, response: { status: 502, data: new Blob(['bad gateway'], { type: 'text/html' }) } }
vi.spyOn(http, 'get').mockRejectedValue(error)
await expect(downloadContainerCustomsReceipt('7')).rejects.toBe(error)
})
})
@@ -1,4 +1,5 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import axios from 'axios'
import { ApiResponseError, http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export type ContainerCustomsStatus = 'WAITING_CUSTOMER_DOCUMENTS' | 'READY_TO_START' | 'DECLARING' | 'COMPLETED'
export type ContainerCustomsTaxMode = 'TAX_REFUND' | 'BUY_DECLARATION'
@@ -210,16 +211,45 @@ export async function getContainerCustoms(loadingPlanId: string) {
return value ? normalize(value) : null
}
// 报关资料允许下载原始 JSON 文件,仅把统一业务错误信封识别为失败。
async function customsDownloadError(blob: Blob, status: number): Promise<ApiResponseError | null> {
if (!blob.type.toLowerCase().includes('json')) return null
let payload: unknown
try { payload = JSON.parse(await blob.text()) } catch { return null }
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null
const envelope = payload as Record<string, unknown>
if (typeof envelope.code !== 'string' || envelope.code === '200'
|| typeof envelope.message !== 'string' || !('data' in envelope)) return null
return new ApiResponseError(envelope.message, envelope.code, status, envelope.data,
typeof envelope.requestId === 'string' ? envelope.requestId : undefined,
typeof envelope.traceId === 'string' ? envelope.traceId : undefined)
}
async function downloadCustomsFile(path: string) {
try {
const blob = await http.get<Blob, Blob>(path, { responseType: 'blob' })
const error = await customsDownloadError(blob, 200)
if (error) throw error
return blob
} catch (error) {
if (axios.isAxiosError(error) && error.response?.data instanceof Blob) {
const downloadError = await customsDownloadError(error.response.data, error.response.status)
if (downloadError) throw downloadError
}
throw error
}
}
export function downloadContainerCustomsDocument(documentId: string) {
return http.get<Blob, Blob>(`${basePath}/documents/${encodeURIComponent(documentId)}/download`, { responseType: 'blob' })
return downloadCustomsFile(`${basePath}/documents/${encodeURIComponent(documentId)}/download`)
}
export function downloadContainerCustomsReceipt(documentId: string) {
return http.get<Blob, Blob>(`${basePath}/documents/${encodeURIComponent(documentId)}/receipt/download`, { responseType: 'blob' })
return downloadCustomsFile(`${basePath}/documents/${encodeURIComponent(documentId)}/completion/download`)
}
export function downloadContainerCustomsFinal(documentId: string) {
return http.get<Blob, Blob>(`${basePath}/documents/${encodeURIComponent(documentId)}/final/download`, { responseType: 'blob' })
return downloadCustomsFile(`${basePath}/documents/${encodeURIComponent(documentId)}/final/download`)
}
async function action(loadingPlanId: string, name: 'start' | 'send-to-broker' | 'submit-to-broker' | 'complete-all-buy', expectedVersion: number, remark?: string) {