diff --git a/src/modules/first-leg/utils/customsDocumentsReader.ts b/src/modules/first-leg/utils/customsDocumentsReader.ts new file mode 100644 index 0000000..8e77b83 --- /dev/null +++ b/src/modules/first-leg/utils/customsDocumentsReader.ts @@ -0,0 +1,52 @@ +import { getMerchantUnifiedOrders } from '../api/unifiedOrders' +import { getMerchantCustomsForOrder, isMerchantCustomsUnavailable, type MerchantCustomsContainer } from '../api/customs' +import { isCustomsOrderCandidate } from './customsEligibility' + +export interface CustomsDocumentRow { + order: { id: string; orderNo: string | null; businessSummary: { inboundCode: string } } + detail: MerchantCustomsContainer +} + +// Isolated from the shared order store: scan sequential pages, with at most +// four document requests in flight. Inconsistent pagination must be visible. +export async function readCustomsDocuments() { + const records: CustomsDocumentRow[] = [] + const seen = new Set() + let expectedTotal: number | undefined + let failedCount = 0 + const size = 200 + for (let page = 1; ; page += 1) { + const response = await getMerchantUnifiedOrders({ page, size }) + const total = Number(response.total) + const pages = Number(response.pages) + if (!Number.isSafeInteger(total) || total < 0 || Number(response.page) !== page + || Number(response.size) !== size || pages !== Math.ceil(total / size) + || (expectedTotal !== undefined && total !== expectedTotal) + || response.records.length !== Math.min(size, Math.max(0, total - (page - 1) * size))) { + throw new Error('订单分页数据不完整或已变化,请重新加载报关资料') + } + expectedTotal = total + for (const row of response.records) { + const id = String(row.orderId) + if (seen.has(id)) throw new Error('订单分页存在重复记录,请重新加载报关资料') + seen.add(id) + } + const candidates = response.records.filter(row => isCustomsOrderCandidate({ status: row.mainStatus, taxMode: row.taxMode ?? 'BUY_DECLARATION' })) + for (let offset = 0; offset < candidates.length; offset += 4) { + const results = await Promise.all(candidates.slice(offset, offset + 4).map(async row => { + const id = String(row.orderId) + try { + const detail = await getMerchantCustomsForOrder(id) + if (!detail.inbounds.some(item => item.orderId === id && item.taxMode === 'TAX_REFUND')) return null + return { order: { id, orderNo: row.orderNo, businessSummary: { inboundCode: row.inboundCode ?? '' } }, detail } + } catch (error) { + if (!isMerchantCustomsUnavailable(error)) failedCount += 1 + return null + } + })) + for (const result of results) if (result) records.push(result) + } + if (page >= pages) break + } + return { records, failedCount } +} diff --git a/src/modules/first-leg/views/CustomsDocumentsView.test.mjs b/src/modules/first-leg/views/CustomsDocumentsView.test.mjs index 269b13f..4f62422 100644 --- a/src/modules/first-leg/views/CustomsDocumentsView.test.mjs +++ b/src/modules/first-leg/views/CustomsDocumentsView.test.mjs @@ -17,19 +17,90 @@ const eligibility = evaluate(readFileSync(new URL('../utils/customsEligibility.t const order = (id, status = 'LOADED', taxMode = 'TAX_REFUND') => ({ id, status, taxMode, orderNo: `ORDER-${id}`, businessSummary: { inboundCode: `IN-${id}` } }) const detail = id => ({ status: 'COMPLETED', merchantStatus: 'CUSTOMS_COMPLETED', inbounds: [{ orderId: id, taxMode: 'TAX_REFUND' }] }) function harness(t, orders, getDetail = async id => detail(id)) { - const store = { orders, loadError: '', load: async () => {} }, calls = [] + const store = { loadError: '', failPage: 0, transformPage: value => value }, calls = [], pageCalls = [] + const reader = evaluate(readFileSync(new URL('../utils/customsDocumentsReader.ts', import.meta.url), 'utf8'), { + './customsEligibility': eligibility, + '../api/unifiedOrders': { getMerchantUnifiedOrders: async ({ page, size }) => { + pageCalls.push({ page, size }) + if (store.loadError || store.failPage === page) throw new Error(store.loadError || '第二页订单查询失败') + return store.transformPage({ page, size, total: orders.length, pages: Math.ceil(orders.length / size), + records: orders.slice((page - 1) * size, page * size).map(row => ({ orderId: row.id, orderNo: row.orderNo, mainStatus: row.status, taxMode: row.taxMode, inboundCode: row.businessSummary.inboundCode })) }) + } }, + '../api/customs': { getMerchantCustomsForOrder: id => { calls.push(id); return getDetail(id) }, isMerchantCustomsUnavailable: error => error?.code === 'A0404' }, + }) const source = readFileSync(new URL('./CustomsDocumentsView.vue', import.meta.url), 'utf8').match(/