fix(pda): drop stale sealed pallets from task workspace
This commit is contained in:
@@ -88,7 +88,7 @@
|
|||||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||||
import { bindDropshipPendingCartons, claimDropshipPalletizationTask, countDropshipCarton, getDropshipPalletizationTask, moveDropshipCarton, resolveDropshipWorkspaceCarton, resolveDropshipWorkspacePallet, sealDropshipPallet, unsealDropshipPallet } from '@/api/pda-dropship.js'
|
import { bindDropshipPendingCartons, claimDropshipPalletizationTask, countDropshipCarton, getDropshipPalletizationTask, moveDropshipCarton, resolveDropshipWorkspaceCarton, resolveDropshipWorkspacePallet, sealDropshipPallet, unsealDropshipPallet } from '@/api/pda-dropship.js'
|
||||||
import { session, requireSession } from '@/stores/session.js'
|
import { session, requireSession } from '@/stores/session.js'
|
||||||
import { formatApiError } from '@/utils/user-facing.js'
|
import { sanitizeUserMessage } from '@/utils/user-facing.js'
|
||||||
import { stableOperationId } from '@/utils/idempotency.js'
|
import { stableOperationId } from '@/utils/idempotency.js'
|
||||||
import { isOnline } from '@/utils/network.js'
|
import { isOnline } from '@/utils/network.js'
|
||||||
import { clearDropshipPalletizingDraft, restoreDropshipPalletizingDraft, saveDropshipPalletizingDraft } from '@/stores/dropship-palletizing.js'
|
import { clearDropshipPalletizingDraft, restoreDropshipPalletizingDraft, saveDropshipPalletizingDraft } from '@/stores/dropship-palletizing.js'
|
||||||
@@ -119,6 +119,10 @@
|
|||||||
onHide() { this.stopScanner() }, onUnload() { this.stopScanner() }, beforeUnmount() { this.stopScanner() },
|
onHide() { this.stopScanner() }, onUnload() { this.stopScanner() }, beforeUnmount() { this.stopScanner() },
|
||||||
onBackPress() { if (!this.unsealOpen) return false; this.closeUnseal(); return true },
|
onBackPress() { if (!this.unsealOpen) return false; this.closeUnseal(); return true },
|
||||||
methods: {
|
methods: {
|
||||||
|
formatOperationError(error, fallback) {
|
||||||
|
console.error('[PDA dropship inbound]', { code: error?.code, requestId: error?.requestId, traceId: error?.traceId })
|
||||||
|
return sanitizeUserMessage(error?.message, fallback)
|
||||||
|
},
|
||||||
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleScan(value), onError: () => { this.error = '扫码头连接失败,可使用输入框继续作业' } }) },
|
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleScan(value), onError: () => { this.error = '扫码头连接失败,可使用输入框继续作业' } }) },
|
||||||
stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null },
|
stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null },
|
||||||
handleScan(value) { if (this.loading || this.submitting) return; if (!this.palletSetupReady) { this.error = this.palletSetupMessage; return } const plan = planDropshipScan(createDropshipScanState({ online: isOnline() }), value, { unboundCartons: this.task?.unboundCartons }); if ([DROPSHIP_SCAN_INTENT.BLOCKED, DROPSHIP_SCAN_INTENT.INVALID].includes(plan.intent)) { this.error = plan.message; return } const code = plan.palletNo || plan.boxCode; if (plan.palletNo) { this.palletCode = code; this.selectPallet(code) } else { this.boxCode = code; this.resolveCarton() } },
|
handleScan(value) { if (this.loading || this.submitting) return; if (!this.palletSetupReady) { this.error = this.palletSetupMessage; return } const plan = planDropshipScan(createDropshipScanState({ online: isOnline() }), value, { unboundCartons: this.task?.unboundCartons }); if ([DROPSHIP_SCAN_INTENT.BLOCKED, DROPSHIP_SCAN_INTENT.INVALID].includes(plan.intent)) { this.error = plan.message; return } const code = plan.palletNo || plan.boxCode; if (plan.palletNo) { this.palletCode = code; this.selectPallet(code) } else { this.boxCode = code; this.resolveCarton() } },
|
||||||
@@ -127,7 +131,7 @@
|
|||||||
if (!this.taskId || !this.warehouseId) { this.error = '缺少合托任务或仓库信息,请返回任务列表'; return }
|
if (!this.taskId || !this.warehouseId) { this.error = '缺少合托任务或仓库信息,请返回任务列表'; return }
|
||||||
this.loading = true; this.resetMessages()
|
this.loading = true; this.resetMessages()
|
||||||
try { this.task = await getDropshipPalletizationTask(this.taskId, this.warehouseId); await this.refreshPallet({ restorePallet }); if (this.task.taskStatus === 'COMPLETED') clearDropshipPalletizingDraft(this.draftIdentity()) }
|
try { this.task = await getDropshipPalletizationTask(this.taskId, this.warehouseId); await this.refreshPallet({ restorePallet }); if (this.task.taskStatus === 'COMPLETED') clearDropshipPalletizingDraft(this.draftIdentity()) }
|
||||||
catch (error) { this.error = formatApiError(error, '一件代发入库任务加载失败') }
|
catch (error) { this.error = this.formatOperationError(error, '一件代发入库任务加载失败') }
|
||||||
finally { this.loading = false }
|
finally { this.loading = false }
|
||||||
},
|
},
|
||||||
async claimTask() { await this.runMutation(() => claimDropshipPalletizationTask(this.taskId, this.warehouseId), '任务领取成功') },
|
async claimTask() { await this.runMutation(() => claimDropshipPalletizationTask(this.taskId, this.warehouseId), '任务领取成功') },
|
||||||
@@ -137,7 +141,7 @@
|
|||||||
const code = String(value || this.boxCode || '').trim().toUpperCase(); if (!code || this.loading) return
|
const code = String(value || this.boxCode || '').trim().toUpperCase(); if (!code || this.loading) return
|
||||||
this.boxCode = code; this.loading = true; this.resetMessages(); this.carton = null
|
this.boxCode = code; this.loading = true; this.resetMessages(); this.carton = null
|
||||||
try { const resolved = await resolveDropshipWorkspaceCarton(this.warehouseId, code); this.scanTask = resolved.task; this.carton = resolved.carton; this.skuCounts = (this.carton.skuLines || []).map((line) => ({ ...line, actualQuantity: String(line.actualQuantity ?? line.expectedQuantity ?? ''), discrepancyRemark: line.discrepancyRemark || '' })) }
|
try { const resolved = await resolveDropshipWorkspaceCarton(this.warehouseId, code); this.scanTask = resolved.task; this.carton = resolved.carton; this.skuCounts = (this.carton.skuLines || []).map((line) => ({ ...line, actualQuantity: String(line.actualQuantity ?? line.expectedQuantity ?? ''), discrepancyRemark: line.discrepancyRemark || '' })) }
|
||||||
catch (error) { this.error = formatApiError(error, '箱码解析失败') }
|
catch (error) { this.error = this.formatOperationError(error, '箱码解析失败') }
|
||||||
finally { this.loading = false }
|
finally { this.loading = false }
|
||||||
},
|
},
|
||||||
async countCarton() {
|
async countCarton() {
|
||||||
@@ -167,7 +171,7 @@
|
|||||||
this.palletCode = code
|
this.palletCode = code
|
||||||
this.loading = true; this.resetMessages(); this.workspace = null
|
this.loading = true; this.resetMessages(); this.workspace = null
|
||||||
try { this.workspace = await resolveDropshipWorkspacePallet(this.warehouseId, code); this.selectedPalletNo = code; this.palletCode = code; this.selectedPalletId = String(this.workspace.pallet.palletId); this.activeScan = 'boxCode'; saveDropshipPalletizingDraft(this.draftIdentity(), { activePalletNo: code }) }
|
try { this.workspace = await resolveDropshipWorkspacePallet(this.warehouseId, code); this.selectedPalletNo = code; this.palletCode = code; this.selectedPalletId = String(this.workspace.pallet.palletId); this.activeScan = 'boxCode'; saveDropshipPalletizingDraft(this.draftIdentity(), { activePalletNo: code }) }
|
||||||
catch (error) { this.selectedPalletNo = ''; this.error = formatApiError(error, '托盘校验失败'); return }
|
catch (error) { this.selectedPalletNo = ''; this.error = this.formatOperationError(error, '托盘校验失败'); return }
|
||||||
finally { this.loading = false }
|
finally { this.loading = false }
|
||||||
const pallet = this.currentPallet
|
const pallet = this.currentPallet
|
||||||
if (pallet?.status === 'SEALED') { this.notice = `托盘 ${code} 已封托,可在托盘操作区解除封托`; return }
|
if (pallet?.status === 'SEALED') { this.notice = `托盘 ${code} 已封托,可在托盘操作区解除封托`; return }
|
||||||
@@ -235,10 +239,10 @@
|
|||||||
? result
|
? result
|
||||||
: await getDropshipPalletizationTask(this.taskId, this.warehouseId)
|
: await getDropshipPalletizationTask(this.taskId, this.warehouseId)
|
||||||
this.notice = message
|
this.notice = message
|
||||||
try { await this.refreshPallet() } catch (error) { this.error = formatApiError(error, '操作已成功,托盘汇总刷新失败,请刷新后继续') }
|
try { await this.refreshPallet() } catch (error) { this.error = this.formatOperationError(error, '操作已成功,托盘汇总刷新失败,请刷新后继续') }
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
catch (error) { this.error = formatApiError(error, '操作失败'); return null }
|
catch (error) { this.error = this.formatOperationError(error, '操作失败'); return null }
|
||||||
finally { this.submitting = false }
|
finally { this.submitting = false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import test from 'node:test'
|
|||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
import { readFile } from 'node:fs/promises'
|
import { readFile } from 'node:fs/promises'
|
||||||
import vm from 'node:vm'
|
import vm from 'node:vm'
|
||||||
|
import { sanitizeUserMessage } from '../utils/user-facing.js'
|
||||||
|
|
||||||
const source = await readFile(new URL('../pages/dropship-palletization/dropship-palletization.vue', import.meta.url), 'utf8')
|
const source = await readFile(new URL('../pages/dropship-palletization/dropship-palletization.vue', import.meta.url), 'utf8')
|
||||||
const script = source.match(/<script>([\s\S]*?)<\/script>/)[1].replace(/^\s*import .*$/gm, '').replace('export default', 'globalThis.page =')
|
const script = source.match(/<script>([\s\S]*?)<\/script>/)[1].replace(/^\s*import .*$/gm, '').replace('export default', 'globalThis.page =')
|
||||||
@@ -18,7 +19,7 @@ function harness(overrides = {}) {
|
|||||||
countDropshipCarton: async (id, payload) => { calls.push(['count', id, payload]); return { carton: {} } },
|
countDropshipCarton: async (id, payload) => { calls.push(['count', id, payload]); return { carton: {} } },
|
||||||
moveDropshipCarton: async (id, cartonId, payload) => { calls.push(['move', id, cartonId, payload]); return { ...target, taskId: id } },
|
moveDropshipCarton: async (id, cartonId, payload) => { calls.push(['move', id, cartonId, payload]); return { ...target, taskId: id } },
|
||||||
saveDropshipPalletizingDraft: () => {}, clearDropshipPalletizingDraft: () => {}, stableOperationId: () => 'stable-key',
|
saveDropshipPalletizingDraft: () => {}, clearDropshipPalletizingDraft: () => {}, stableOperationId: () => 'stable-key',
|
||||||
isOnline: () => true, formatApiError: (error) => error.message, uni: { redirectTo: (value) => calls.push(['redirect', value]) },
|
isOnline: () => true, sanitizeUserMessage, console: { error: (...args) => calls.push(['diagnostic', ...args]) }, uni: { redirectTo: (value) => calls.push(['redirect', value]) },
|
||||||
...overrides
|
...overrides
|
||||||
}
|
}
|
||||||
vm.runInNewContext(script, context)
|
vm.runInNewContext(script, context)
|
||||||
@@ -28,6 +29,19 @@ function harness(overrides = {}) {
|
|||||||
return { state, calls, pallet, target }
|
return { state, calls, pallet, target }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('scan failures show the business message and retain correlation evidence in diagnostics', async () => {
|
||||||
|
const failure = { message: '箱码不属于当前一件代发入仓单', code: 'A0404', requestId: 'request-test', traceId: 'trace-test' }
|
||||||
|
const { state, calls } = harness({ resolveDropshipWorkspaceCarton: async () => { throw failure } })
|
||||||
|
await state.resolveCarton('WRONG-BOX')
|
||||||
|
assert.equal(state.error, failure.message)
|
||||||
|
assert.equal(state.loading, false)
|
||||||
|
const evidence = calls.find(call => call[0] === 'diagnostic')[2]
|
||||||
|
assert.equal(evidence.code, failure.code)
|
||||||
|
assert.equal(evidence.requestId, failure.requestId)
|
||||||
|
assert.equal(evidence.traceId, failure.traceId)
|
||||||
|
assert.equal(state.formatOperationError({}, '箱码解析失败'), '箱码解析失败')
|
||||||
|
})
|
||||||
|
|
||||||
test('cross customer scan remains read only until confirmation and then claims original task', async () => {
|
test('cross customer scan remains read only until confirmation and then claims original task', async () => {
|
||||||
const { state, calls, pallet, target } = harness()
|
const { state, calls, pallet, target } = harness()
|
||||||
state.workspace = { pallet, members: [] }; state.selectedPalletNo = pallet.palletNo
|
state.workspace = { pallet, members: [] }; state.selectedPalletNo = pallet.palletNo
|
||||||
|
|||||||
Reference in New Issue
Block a user