fix: treat missing return records as empty results

This commit is contained in:
2026-09-11 15:01:17 +08:00
parent f7d3a9a73f
commit fdd9302c4d
6 changed files with 69 additions and 11 deletions
+11 -1
View File
@@ -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) {
+18 -2
View File
@@ -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) {
+1 -2
View File
@@ -2,7 +2,6 @@
<view v-if="user" class="page-shell portal-page work-page">
<PortalHeader title="退货合箱" :user="user" fallback-url="/pages/module/module?key=inbound" />
<scroll-view class="work-scroll" scroll-y><view class="portal-content work-content">
<view class="work-heading"><text class="work-title">退货合箱</text><text class="work-subtitle">仅可合并当前仓、同一客户、已完成签收的退货单库存</text></view>
<view v-if="!result" class="work-card"><text class="card-title">添加退货单</text><ScanTargetInput name="returnNo" v-model="searchCode" label="退货单号" placeholder="扫描或输入完整退货单号" :active="true" @submit="search" /><button class="primary" :disabled="!searchCode.trim() || busy" @tap="search(searchCode)">{{ busy ? '查询中…' : '查询可合箱库存' }}</button></view>
<view v-if="candidates !== null && !result" class="section-gap"><view v-if="!candidates.length" class="empty-box"><text class="empty-copy">没有匹配的已完成退货单或可用库存</text></view><view v-else class="task-list"><button v-for="item in candidates" :key="item.returnNo" class="task-card candidate-card" @tap="addOrder(item)"><view class="row-head"><view><text class="business-code">{{ item.returnNo }}</text><text class="meta-line">客户参考号:{{ item.customerReferenceNo || '—' }}</text></view><text class="select-link">添加 ›</text></view></button></view></view>
@@ -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))])) },
+3 -6
View File
@@ -2,8 +2,6 @@
<view v-if="user" class="page-shell portal-page work-page">
<PortalHeader title="退货入库" :user="user" fallback-url="/pages/module/module?key=inbound" />
<scroll-view class="work-scroll" scroll-y><view class="portal-content work-content">
<view class="work-heading"><text class="work-title">退货入库</text><text class="work-subtitle">当前仓:{{ warehouseName }};每次提交生成一个签收箱和仓储计费批次</text></view>
<view v-if="!current && !result" class="work-card">
<text class="card-title">选择查询方式</text>
<view class="mode-row"><button class="mode-button" :class="{ active: entryMode === 'RETURN_NO' }" @tap="switchMode('RETURN_NO')">退货单号</button><button class="mode-button" :class="{ active: entryMode === 'SKU' }" @tap="switchMode('SKU')">SKU</button></view>
@@ -13,7 +11,7 @@
<view v-if="!current && candidates !== null && !result" class="section-gap">
<view class="row-head candidate-heading"><text class="card-title">匹配退货单</text><text class="meta-line">{{ candidates.length }} 个候选</text></view>
<view v-if="!candidates.length" class="empty-box"><text class="empty-copy">当前绑定仓没有包含该 SKU 的待签收退货单</text></view>
<view v-if="!candidates.length" class="empty-box"><text class="empty-copy">{{ entryMode === 'SKU' ? '当前绑定仓没有包含该 SKU 的待签收退货单' : '当前绑定仓没有匹配的待签收或签收中退货单' }}</text></view>
<view v-else class="task-list"><button v-for="item in candidates" :key="item.returnNo" class="task-card candidate-card" @tap="selectCandidate(item)"><view class="row-head"><text class="business-code">{{ item.returnNo }}</text><text class="status-pill">{{ statusLabel(item.status) }}</text></view><text class="meta-line">客户参考号:{{ item.customerReferenceNo || '—' }}</text><view class="metric-row"><view class="metric"><strong>{{ item.skuReceivedQuantity }}/{{ item.skuExpectedQuantity }}</strong><text>该 SKU</text></view><view class="metric"><strong>{{ item.orderReceivedQuantity }}/{{ item.orderExpectedQuantity }}</strong><text>整单进度</text></view><text class="select-link">选择 ›</text></view></button></view>
</view>
@@ -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)) },
+12
View File
@@ -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')
})
+24
View File
@@ -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')
})