diff --git a/src/api/return-packing.js b/src/api/return-packing.js index be8afd5..9b43e69 100644 --- a/src/api/return-packing.js +++ b/src/api/return-packing.js @@ -4,6 +4,11 @@ const BASE = '/pda/v1/last-leg/return-inventory' const encoded = (value) => encodeURIComponent(String(value || '').trim()) const count = (value) => Number.isFinite(Number(value)) ? Math.max(0, Number(value)) : 0 +function isNotFound(error) { + const code = String(error?.code || '').toUpperCase() + return code === 'A0404' || code === '404' +} + function normalizeItems(items) { return Array.isArray(items) ? items.map((item) => ({ skuCode: String(item.skuCode || ''), productNameZh: String(item.productNameZh || ''), @@ -34,7 +39,12 @@ export function normalizeReturnLabel(value = {}) { export async function findPackingOrders(returnNo = '') { const query = String(returnNo || '').trim() - return normalizePackingPreview(await overseasRequest('GET', `${BASE}/packing/orders${query ? `?returnNo=${encoded(query)}` : ''}`)) + try { + return normalizePackingPreview(await overseasRequest('GET', `${BASE}/packing/orders${query ? `?returnNo=${encoded(query)}` : ''}`)) + } catch (error) { + if (isNotFound(error)) return normalizePackingPreview() + throw error + } } export async function mergeReturnCartons(request) { diff --git a/src/api/return-receipt.js b/src/api/return-receipt.js index 2cc9de8..f18fcb1 100644 --- a/src/api/return-receipt.js +++ b/src/api/return-receipt.js @@ -16,6 +16,11 @@ function normalizeItem(item = {}) { } } +function isNotFound(error) { + const code = String(error?.code || '').toUpperCase() + return code === 'A0404' || code === '404' +} + export function normalizeReturnReceiptStart(value = {}) { return { returnNo: String(value.returnNo || ''), @@ -54,11 +59,22 @@ export function normalizeReturnReceiptResult(value = {}) { } export async function startReturnReceipt(returnNo) { - return normalizeReturnReceiptStart(await overseasRequest('POST', `${BASE}/start`, { returnNo: String(returnNo || '').trim() })) + try { + return normalizeReturnReceiptStart(await overseasRequest('POST', `${BASE}/start`, { returnNo: String(returnNo || '').trim() })) + } catch (error) { + if (isNotFound(error)) return null + throw error + } } export async function findReturnSkuCandidates(skuCode) { - return normalizeReturnSkuCandidates(await overseasRequest('POST', `${BASE}/sku-candidates`, { skuCode: String(skuCode || '').trim() })) + const normalizedSkuCode = String(skuCode || '').trim() + try { + return normalizeReturnSkuCandidates(await overseasRequest('POST', `${BASE}/sku-candidates`, { skuCode: normalizedSkuCode })) + } catch (error) { + if (isNotFound(error)) return normalizeReturnSkuCandidates({ skuCode: normalizedSkuCode, candidates: [] }) + throw error + } } export async function confirmReturnReceipt(request) { diff --git a/src/pages/return-packing/return-packing.vue b/src/pages/return-packing/return-packing.vue index 3b3b855..835c67b 100644 --- a/src/pages/return-packing/return-packing.vue +++ b/src/pages/return-packing/return-packing.vue @@ -2,7 +2,6 @@ - 退货合箱仅可合并当前仓、同一客户、已完成签收的退货单库存 添加退货单 没有匹配的已完成退货单或可用库存 @@ -46,7 +45,7 @@ methods: { startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => { if (!this.busy) { this.searchCode = value; this.search(value) } }, onError: (error) => this.setError(error, '扫码广播注册失败') }) }, stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null }, - async search(raw) { const value = String(raw?.detail?.value ?? raw ?? this.searchCode).trim(); if (!value || this.busy) return; this.searchCode = value; await this.run(async () => { const preview = await findPackingOrders(value); this.candidates = preview.orders.filter((item) => !this.selectedOrders.some((selected) => selected.returnNo === item.returnNo)); if (!this.candidates.length) playErrorSound(); else playSuccessSound() }, '查询可合箱退货单失败') }, + async search(raw) { const value = String(raw?.detail?.value ?? raw ?? this.searchCode).trim(); if (!value || this.busy) return; this.searchCode = value; await this.run(async () => { const preview = await findPackingOrders(value); this.candidates = preview.orders.filter((item) => !this.selectedOrders.some((selected) => selected.returnNo === item.returnNo)); if (this.candidates.length) playSuccessSound() }, '查询可合箱退货单失败') }, async addOrder(item) { if (this.busy || !item?.returnNo) return; await this.run(async () => { const preview = await findPackingOrders(item.returnNo); const exact = preview.orders.filter((order) => order.returnNo === item.returnNo); if (exact.length !== 1 || !preview.customerKey) throw new Error('请扫描完整退货单号后重试'); const customerKey = this.selectedOrders[0]?.customerKey; if (customerKey && customerKey !== preview.customerKey) throw new Error('不同客户的退货单不能合箱'); const selected = [...this.selectedOrders, { ...exact[0], customerKey: preview.customerKey }]; this.previews = { ...this.previews, [item.returnNo]: preview }; updateWarehouseWork('returnPacking', { selectedOrders: selected, searchCode: '', quantities: this.validQuantities(selected, this.previews) }); this.candidates = null; playSuccessSound() }, '添加退货单失败') }, removeOrder(returnNo) { const selected = this.selectedOrders.filter((item) => item.returnNo !== returnNo); const previews = { ...this.previews }; delete previews[returnNo]; this.previews = previews; updateWarehouseWork('returnPacking', { selectedOrders: selected, quantities: this.validQuantities(selected, previews) }) }, validQuantities(selected, previews) { const available = new Map(); for (const order of selected) for (const item of previews[order.returnNo]?.skus || []) available.set(item.skuCode, (available.get(item.skuCode) || 0) + Number(item.availableQuantity || 0)); return Object.fromEntries([...available].map(([sku, maximum]) => [sku, Math.min(maximum, this.quantity(sku))])) }, diff --git a/src/pages/return-receipt/return-receipt.vue b/src/pages/return-receipt/return-receipt.vue index f9edb66..4745265 100644 --- a/src/pages/return-receipt/return-receipt.vue +++ b/src/pages/return-receipt/return-receipt.vue @@ -2,8 +2,6 @@ - 退货入库当前仓:{{ warehouseName }};每次提交生成一个签收箱和仓储计费批次 - 选择查询方式 @@ -13,7 +11,7 @@ 匹配退货单{{ candidates.length }} 个候选 - 当前绑定仓没有包含该 SKU 的待签收退货单 + {{ entryMode === 'SKU' ? '当前绑定仓没有包含该 SKU 的待签收退货单' : '当前绑定仓没有匹配的待签收或签收中退货单' }} @@ -52,7 +50,6 @@ components: { PortalHeader, ScanTargetInput }, data() { return { user: session.currentUser, current: null, candidates: null, result: null, activeTarget: 'lookup', busy: false, error: null } }, computed: { - warehouseName() { return String(this.user?.warehouseName || '').trim() || '未关联海外仓' }, entryMode: { get() { return warehouseWork.returnReceipt.entryMode }, set(value) { updateWarehouseWork('returnReceipt', { entryMode: value }) } }, lookupCode: { get() { return warehouseWork.returnReceipt.lookupCode }, set(value) { updateWarehouseWork('returnReceipt', { lookupCode: value }) } }, startedBy: { get() { return warehouseWork.returnReceipt.startedBy }, set(value) { updateWarehouseWork('returnReceipt', { startedBy: value }) } }, @@ -68,9 +65,9 @@ stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null }, handleScan(value) { if (this.busy) return; if (!this.current) { this.lookupCode = value; this.lookup(value); return } if (this.activeTarget === 'carton') this.acceptCarton(value); else this.acceptTracking(value) }, switchMode(mode) { if (this.busy) return; this.entryMode = mode; this.lookupCode = ''; this.candidates = null; this.error = null; this.activeTarget = 'lookup' }, - async lookup(raw) { const value = String(raw?.detail?.value ?? raw ?? this.lookupCode).trim(); if (!value || this.busy) return; this.lookupCode = value; this.error = null; this.result = null; if (this.entryMode === 'SKU') await this.run(async () => { const response = await findReturnSkuCandidates(value); this.candidates = response.candidates; playSuccessSound() }, '查询 SKU 候选失败'); else await this.openOrder(value, 'RETURN_NO') }, + async lookup(raw) { const value = String(raw?.detail?.value ?? raw ?? this.lookupCode).trim(); if (!value || this.busy) return; this.lookupCode = value; this.error = null; this.result = null; if (this.entryMode === 'SKU') await this.run(async () => { const response = await findReturnSkuCandidates(value); this.candidates = response.candidates; if (this.candidates.length) playSuccessSound() }, '查询 SKU 候选失败'); else await this.openOrder(value, 'RETURN_NO') }, async selectCandidate(item) { if (!item?.returnNo || this.busy) return; await this.openOrder(item.returnNo, 'SKU') }, - async openOrder(returnNo, startedBy, feedback = true) { await this.run(async () => { const value = await startReturnReceipt(returnNo); this.current = value; this.candidates = null; this.startedBy = startedBy; updateWarehouseWork('returnReceipt', { currentReturnNo: value.returnNo, startedBy, lookupCode: value.returnNo, quantities: this.validQuantities(value) }); this.activeTarget = 'tracking'; if (feedback) playSuccessSound() }, '读取退货单失败') }, + async openOrder(returnNo, startedBy, feedback = true) { await this.run(async () => { const value = await startReturnReceipt(returnNo); if (!value) { this.current = null; this.candidates = []; updateWarehouseWork('returnReceipt', { currentReturnNo: '', quantities: {} }); this.activeTarget = 'lookup'; return } this.current = value; this.candidates = null; this.startedBy = startedBy; updateWarehouseWork('returnReceipt', { currentReturnNo: value.returnNo, startedBy, lookupCode: value.returnNo, quantities: this.validQuantities(value) }); this.activeTarget = 'tracking'; if (feedback) playSuccessSound() }, '读取退货单失败') }, validQuantities(order) { const source = warehouseWork.returnReceipt.quantities || {}; return Object.fromEntries(order.expectedItems.map((item) => [item.skuCode, Math.min(Math.max(0, Number(source[item.skuCode]) || 0), this.remaining(item))])) }, quantity(skuCode) { return Math.max(0, Number(warehouseWork.returnReceipt.quantities?.[skuCode]) || 0) }, remaining(item) { return Math.max(0, Number(item.expectedQuantity || 0) - Number(item.receivedQuantity || 0)) }, diff --git a/tests/return-packing-api.test.mjs b/tests/return-packing-api.test.mjs index 1791c69..0de4e23 100644 --- a/tests/return-packing-api.test.mjs +++ b/tests/return-packing-api.test.mjs @@ -28,3 +28,15 @@ test('return packing API uses exact PDA inventory routes without submitting cust assert.ok(calls.every((item) => item.authorization === 'Bearer packing-token')) assert.ok(calls.every((item) => !Object.hasOwn(item.data || {}, 'customerId') && !Object.hasOwn(item.data || {}, 'warehouseId'))) }) + +test('return packing lookup treats not found as an empty preview', async () => { + session.apiBase = '/overseas-api'; session.token = 'packing-token' + globalThis.uni = { request(options) { options.success({ statusCode: 404, data: { code: 'A0404', message: '没有匹配的可合箱库存' }, header: {} }) } } + assert.deepEqual(await findPackingOrders('MISSING-RETURN'), { customerKey: '', orders: [], skus: [] }) +}) + +test('return packing lookup still rejects real service errors', async () => { + session.apiBase = '/overseas-api'; session.token = 'packing-token' + globalThis.uni = { request(options) { options.success({ statusCode: 500, data: { code: 'B0500', message: '服务暂不可用' }, header: {} }) } } + await assert.rejects(() => findPackingOrders('RT-1'), (error) => error.code === 'B0500') +}) diff --git a/tests/return-receipt-api.test.mjs b/tests/return-receipt-api.test.mjs index 7080344..12ea263 100644 --- a/tests/return-receipt-api.test.mjs +++ b/tests/return-receipt-api.test.mjs @@ -50,3 +50,27 @@ test('return receipt API calls only the dedicated PDA routes and preserves the r assert.ok(calls.every((item) => item.authorization === 'Bearer return-token')) assert.ok(calls.every((item) => !Object.hasOwn(item.data || {}, 'customerId') && !Object.hasOwn(item.data || {}, 'warehouseId'))) }) + +test('return receipt lookup treats A0404 and HTTP 404 as empty results', async () => { + session.apiBase = '/overseas-api'; session.token = 'return-token' + let requestCount = 0 + globalThis.uni = { + request(options) { + requestCount += 1 + if (requestCount === 1) options.success({ statusCode: 404, data: { code: 'A0404', message: '当前仓没有待签收或签收中的退货单' }, header: {} }) + else options.success({ statusCode: 404, data: {}, header: {} }) + } + } + assert.equal(await startReturnReceipt('MISSING-RETURN'), null) + assert.deepEqual(await findReturnSkuCandidates('MISSING-SKU'), { skuCode: 'MISSING-SKU', candidates: [] }) +}) + +test('return receipt lookup still rejects real service errors', async () => { + session.apiBase = '/overseas-api'; session.token = 'return-token' + globalThis.uni = { + request(options) { + options.success({ statusCode: 500, data: { code: 'B0500', message: '服务暂不可用' }, header: {} }) + } + } + await assert.rejects(() => startReturnReceipt('RT-001'), (error) => error.code === 'B0500') +})