fix(pda): preserve packing plan retry keys and surface missing API
This commit is contained in:
@@ -76,12 +76,9 @@ export function normalizeReturnPackingPlan(value = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function listReturnPackingPlans() {
|
export async function listReturnPackingPlans() {
|
||||||
try {
|
const value = await overseasRequest('GET', `${BASE}/packing/plans`)
|
||||||
const value = await overseasRequest('GET', `${BASE}/packing/plans`)
|
const records = Array.isArray(value) ? value : Array.isArray(value?.records) ? value.records : []
|
||||||
const records = Array.isArray(value) ? value : Array.isArray(value?.records) ? value.records : []
|
return records.map(normalizeReturnPackingPlan)
|
||||||
return records.map(normalizeReturnPackingPlan)
|
|
||||||
}
|
|
||||||
catch (error) { if (isNotFound(error)) return []; throw error }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReturnPackingPlan(planNo) {
|
export async function getReturnPackingPlan(planNo) {
|
||||||
|
|||||||
@@ -56,7 +56,24 @@
|
|||||||
removePlanBox(index) { if (this.planBoxes.length > 1) this.planBoxes.splice(index, 1) },
|
removePlanBox(index) { if (this.planBoxes.length > 1) this.planBoxes.splice(index, 1) },
|
||||||
addPlanItem(box) { box.items.push({ skuCode: '', quantity: 0 }) },
|
addPlanItem(box) { box.items.push({ skuCode: '', quantity: 0 }) },
|
||||||
removePlanItem(box, index) { if (box.items.length > 1) box.items.splice(index, 1) },
|
removePlanItem(box, index) { if (box.items.length > 1) box.items.splice(index, 1) },
|
||||||
async completePlan() { if (!this.canCompletePlan || this.busy || !this.activePlan) return; const payload = { boxes: this.planBoxes.map((box, index) => ({ boxNo: index + 1, items: box.items.filter((item) => String(item.skuCode || '').trim() && Number(item.quantity) > 0).map((item) => ({ skuCode: String(item.skuCode).trim(), quantity: Number(item.quantity) })), length: Number(box.length), width: Number(box.width), height: Number(box.height), weight: Number(box.weight) })), idempotencyKey: warehouseIdempotencyKey('returnPackingPlan', 'complete', { planNo: this.activePlan.planNo, boxes: this.planBoxes }).value }; await this.run(async () => { this.result = await mergeReturnPackingPlan(this.activePlan.planNo, payload); this.activePlan = null; this.planBoxes = []; await this.loadPlans(); playSuccessSound() }, '合箱计划执行失败') },
|
async completePlan() {
|
||||||
|
if (!this.canCompletePlan || this.busy || !this.activePlan) return
|
||||||
|
await this.run(async () => {
|
||||||
|
const planNo = this.activePlan.planNo
|
||||||
|
const boxes = this.planBoxes.map((box, index) => ({
|
||||||
|
boxNo: index + 1,
|
||||||
|
items: box.items.filter((item) => String(item.skuCode || '').trim() && Number(item.quantity) > 0).map((item) => ({ skuCode: String(item.skuCode).trim(), quantity: Number(item.quantity) })),
|
||||||
|
length: Number(box.length), width: Number(box.width), height: Number(box.height), weight: Number(box.weight)
|
||||||
|
}))
|
||||||
|
const key = warehouseIdempotencyKey('returnPackingPlan', 'complete', { planNo, boxes })
|
||||||
|
this.result = await mergeReturnPackingPlan(planNo, { boxes, idempotencyKey: key.value })
|
||||||
|
releaseWarehouseKey('returnPackingPlan', key.signature)
|
||||||
|
this.activePlan = null
|
||||||
|
this.planBoxes = []
|
||||||
|
await this.loadPlans()
|
||||||
|
playSuccessSound()
|
||||||
|
}, '合箱计划执行失败')
|
||||||
|
},
|
||||||
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => { if (!this.busy) { this.searchCode = value; this.search(value) } }, onError: (error) => this.setError(error, '扫码广播注册失败') }) },
|
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 },
|
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) 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() }, '查询可合箱退货单失败') },
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const blankState = () => ({
|
|||||||
relabel: { currentNo: '', oldLabel: '', newLabel: '', action: 'RELABEL', reason: '', remark: '', photoFileIds: [], photoNames: [], pendingKeys: {} },
|
relabel: { currentNo: '', oldLabel: '', newLabel: '', action: 'RELABEL', reason: '', remark: '', photoFileIds: [], photoNames: [], pendingKeys: {} },
|
||||||
returnReceipt: { entryMode: 'SKU', lookupCode: '', currentReturnNo: '', startedBy: 'SKU', trackingNo: '', palletCode: '', quantities: {}, boxCounts: {}, pendingKeys: {} },
|
returnReceipt: { entryMode: 'SKU', lookupCode: '', currentReturnNo: '', startedBy: 'SKU', trackingNo: '', palletCode: '', quantities: {}, boxCounts: {}, pendingKeys: {} },
|
||||||
returnPacking: { searchCode: '', selectedOrders: [], quantities: {}, length: '', width: '', height: '', weight: '', pendingKeys: {} },
|
returnPacking: { searchCode: '', selectedOrders: [], quantities: {}, length: '', width: '', height: '', weight: '', pendingKeys: {} },
|
||||||
|
returnPackingPlan: { pendingKeys: {} },
|
||||||
warehouseTransfer: { currentNo: '', pendingKeys: {} },
|
warehouseTransfer: { currentNo: '', pendingKeys: {} },
|
||||||
query: { inventoryCode: '', locationCode: '', palletCode: '' }
|
query: { inventoryCode: '', locationCode: '', palletCode: '' }
|
||||||
})
|
})
|
||||||
@@ -40,6 +41,7 @@ export function restoreWarehouseWork(scopeKey) {
|
|||||||
stocktake: domain(saved.stocktake, fresh.stocktake), move: domain(saved.move, fresh.move),
|
stocktake: domain(saved.stocktake, fresh.stocktake), move: domain(saved.move, fresh.move),
|
||||||
relabel: domain(saved.relabel, fresh.relabel), returnReceipt: domain(saved.returnReceipt, fresh.returnReceipt),
|
relabel: domain(saved.relabel, fresh.relabel), returnReceipt: domain(saved.returnReceipt, fresh.returnReceipt),
|
||||||
returnPacking: domain(saved.returnPacking, fresh.returnPacking),
|
returnPacking: domain(saved.returnPacking, fresh.returnPacking),
|
||||||
|
returnPackingPlan: domain(saved.returnPackingPlan, fresh.returnPackingPlan),
|
||||||
warehouseTransfer: domain(saved.warehouseTransfer, fresh.warehouseTransfer),
|
warehouseTransfer: domain(saved.warehouseTransfer, fresh.warehouseTransfer),
|
||||||
query: { ...fresh.query, ...(saved.query || {}) }
|
query: { ...fresh.query, ...(saved.query || {}) }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
import { findPackingOrders, getReturnCartonLabel, mergeReturnCartons, normalizePackingPreview } from '../src/api/return-packing.js'
|
import { findPackingOrders, getReturnCartonLabel, listReturnPackingPlans, mergeReturnCartons, normalizePackingPreview } from '../src/api/return-packing.js'
|
||||||
import { session } from '../src/stores/session.js'
|
import { session } from '../src/stores/session.js'
|
||||||
|
|
||||||
test('packing preview keeps customer scope internal and exposes stable order references', () => {
|
test('packing preview keeps customer scope internal and exposes stable order references', () => {
|
||||||
@@ -40,3 +40,11 @@ test('return packing lookup still rejects real service errors', async () => {
|
|||||||
globalThis.uni = { request(options) { options.success({ statusCode: 500, data: { code: 'B0500', message: '服务暂不可用' }, header: {} }) } }
|
globalThis.uni = { request(options) { options.success({ statusCode: 500, data: { code: 'B0500', message: '服务暂不可用' }, header: {} }) } }
|
||||||
await assert.rejects(() => findPackingOrders('RT-1'), (error) => error.code === 'B0500')
|
await assert.rejects(() => findPackingOrders('RT-1'), (error) => error.code === 'B0500')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('plan list distinguishes an empty success from a missing API and preserves diagnostics', async () => {
|
||||||
|
session.apiBase = '/api'; session.token = 'packing-token'
|
||||||
|
globalThis.uni = { request(options) { options.success({ statusCode: 200, data: { code: '200', data: [] }, header: {} }) } }
|
||||||
|
assert.deepEqual(await listReturnPackingPlans(), [])
|
||||||
|
globalThis.uni = { request(options) { options.success({ statusCode: 404, data: { code: 'A0404', message: '计划接口不存在', requestId: 'missing-plan-api' }, header: {} }) } }
|
||||||
|
await assert.rejects(() => listReturnPackingPlans(), error => error.code === 'A0404' && error.requestId === 'missing-plan-api')
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import vm from 'node:vm'
|
||||||
|
|
||||||
|
function setup(merge) {
|
||||||
|
const saved = new Map()
|
||||||
|
const context = vm.createContext({ reactive: value => value, PortalHeader: {}, ScanTargetInput: {},
|
||||||
|
playSuccessSound() {}, playErrorSound() {}, mergeReturnPackingPlan: merge,
|
||||||
|
uni: { setStorageSync: (key, value) => saved.set(key, structuredClone(value)), getStorageSync: key => saved.get(key), removeStorageSync: key => saved.delete(key) } })
|
||||||
|
const store = readFileSync(new URL('../src/stores/warehouse-work.js', import.meta.url), 'utf8')
|
||||||
|
vm.runInContext(store.replace(/^import .*$/mg, '').replace(/export /g, ''), context)
|
||||||
|
const source = readFileSync(new URL('../src/pages/return-packing/return-packing.vue', import.meta.url), 'utf8')
|
||||||
|
const script = source.match(/<script[^>]*>([\s\S]*?)<\/script>/)[1]
|
||||||
|
const component = vm.runInContext(script.replace(/^\s*import .*$/mg, '').replace('export default', 'globalThis.component ='), context)
|
||||||
|
vm.runInContext("restoreWarehouseWork('operator:warehouse')", context)
|
||||||
|
const page = Object.assign({}, component.methods, { canCompletePlan: true, busy: false,
|
||||||
|
activePlan: { planNo: 'PLAN-1' }, planBoxes: [{ items: [{ skuCode: ' SKU-1 ', quantity: '2' }], length: 10, width: 20, height: 30, weight: 1 }],
|
||||||
|
async loadPlans() {} })
|
||||||
|
return { page, restore: scope => vm.runInContext(`restoreWarehouseWork(${JSON.stringify(scope)})`, context),
|
||||||
|
keys: () => vm.runInContext('warehouseWork.returnPackingPlan.pendingKeys', context) }
|
||||||
|
}
|
||||||
|
|
||||||
|
test('plan completion reaches API and preserves the key across lost responses and draft restore', async () => {
|
||||||
|
const calls = []; let fail = true
|
||||||
|
const fixture = setup(async (plan, payload) => { calls.push({ plan, payload }); if (fail) throw new Error('response lost'); return { labels: [] } })
|
||||||
|
await fixture.page.completePlan()
|
||||||
|
assert.equal(calls.length, 1)
|
||||||
|
assert.equal(fixture.page.error.message, 'response lost')
|
||||||
|
assert.equal(fixture.page.busy, false)
|
||||||
|
fixture.restore('operator:warehouse')
|
||||||
|
fail = false
|
||||||
|
await fixture.page.completePlan()
|
||||||
|
assert.equal(calls.length, 2)
|
||||||
|
assert.equal(calls[0].payload.idempotencyKey, calls[1].payload.idempotencyKey)
|
||||||
|
assert.equal(calls[1].payload.boxes[0].items[0].skuCode, 'SKU-1')
|
||||||
|
assert.equal(fixture.page.activePlan, null)
|
||||||
|
assert.equal(Object.keys(fixture.keys()).length, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('plan completion keys are not reused by another operator or warehouse', async () => {
|
||||||
|
const keys = []
|
||||||
|
const fixture = setup(async (_plan, payload) => { keys.push(payload.idempotencyKey); throw new Error('response lost') })
|
||||||
|
await fixture.page.completePlan()
|
||||||
|
fixture.restore('other-operator:warehouse')
|
||||||
|
await fixture.page.completePlan()
|
||||||
|
assert.equal(keys.length, 2)
|
||||||
|
assert.notEqual(keys[0], keys[1])
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user