fix(pda): enforce sealed pallet contract and deduplicate activation refresh
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
- 扫码是主路径:输入焦点、连续扫描、防抖、重复码、无效码、错箱和扫描反馈必须作为一等场景处理。
|
- 扫码是主路径:输入焦点、连续扫描、防抖、重复码、无效码、错箱和扫描反馈必须作为一等场景处理。
|
||||||
- 拣货扫描身份是后端下发的稳定箱唛箱码 `boxCode`;一次有效且未重复的箱码扫描表示拣出一箱。SKU、商品件数、入库单号和页面展示文案只用于辅助识别,不得作为扫码匹配键或拣货计数单位。
|
- 拣货扫描身份是后端下发的稳定箱唛箱码 `boxCode`;一次有效且未重复的箱码扫描表示拣出一箱。SKU、商品件数、入库单号和页面展示文案只用于辅助识别,不得作为扫码匹配键或拣货计数单位。
|
||||||
- 一件代发封托明细由 `Task.items` 的 `unitType/palletId/palletNo/indivisible` 标识,`PALLET` 只扫描 DP 码并一次将同托全部明细写入本地草稿,不接受托内逐箱扫码、部分拣货或跳拣;可整托撤销未提交草稿。点击提交时通过同一 `/tasks/{taskId}/batch` 按每个明细提交 `PICK_PALLET + palletNo + version`,由服务端验证封托和完整集合后原子处理,禁止扫码立即写后端。DP 托盘标识不得与库存资源托盘标识混用。
|
- 一件代发封托明细由 `Task.items` 的 `unitType/palletId/palletNo/indivisible` 标识,`SEALED_PALLET` 只扫描 DP 码并一次将同托全部明细写入本地草稿,不接受托内逐箱扫码、部分拣货或跳拣;可整托撤销未提交草稿。点击提交时通过同一 `/tasks/{taskId}/batch` 按每个明细提交 `PICK_PALLET + palletNo + version`,由服务端验证封托和完整集合后原子处理,禁止扫码立即写后端。DP 托盘标识不得与库存资源托盘标识混用。
|
||||||
- 进入任务时由服务端标记“拣货中”;扫描结果先写入绑定作业员、任务和柜次的设备持久化草稿,仅在点击提交时批量写后端,禁止逐码提交。
|
- 进入任务时由服务端标记“拣货中”;扫描结果先写入绑定作业员、任务和柜次的设备持久化草稿,仅在点击提交时批量写后端,禁止逐码提交。
|
||||||
- 允许部分拣货和跳拣,但每个未完成项必须填写异常原因;存在未解决异常时任务挂起,不能完成或推进 `LOADED`。
|
- 允许部分拣货和跳拣,但每个未完成项必须填写异常原因;存在未解决异常时任务挂起,不能完成或推进 `LOADED`。
|
||||||
- 作业提交使用对员工不可见的轻量幂等标识和服务端版本;重复提交返回原结果,版本冲突提示刷新后重试,不静默覆盖。
|
- 作业提交使用对员工不可见的轻量幂等标识和服务端版本;重复提交返回原结果,版本冲突提示刷新后重试,不静默覆盖。
|
||||||
|
|||||||
@@ -532,6 +532,8 @@
|
|||||||
},
|
},
|
||||||
async onShow() {
|
async onShow() {
|
||||||
if (!requireSession()) return
|
if (!requireSession()) return
|
||||||
|
if (this._pageVisible) return this._showRefreshPromise
|
||||||
|
this._pageVisible = true
|
||||||
this.currentUser = session.currentUser
|
this.currentUser = session.currentUser
|
||||||
this.startHardwareScanner()
|
this.startHardwareScanner()
|
||||||
if (this.cameraPicking) {
|
if (this.cameraPicking) {
|
||||||
@@ -543,10 +545,16 @@
|
|||||||
this.debugEvidence('preview.resume', { evidenceIdentity: this.evidenceIdentity })
|
this.debugEvidence('preview.resume', { evidenceIdentity: this.evidenceIdentity })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await this.refreshAvailableBoxes()
|
if (this._showRefreshPromise) return this._showRefreshPromise
|
||||||
await this.syncRemoteTask()
|
this._showRefreshPromise = (async () => {
|
||||||
|
await this.refreshAvailableBoxes()
|
||||||
|
await this.syncRemoteTask()
|
||||||
|
})()
|
||||||
|
try { await this._showRefreshPromise }
|
||||||
|
finally { this._showRefreshPromise = null }
|
||||||
},
|
},
|
||||||
onHide() {
|
onHide() {
|
||||||
|
this._pageVisible = false
|
||||||
if (this.evidencePreviewOpening) {
|
if (this.evidencePreviewOpening) {
|
||||||
this.evidencePreviewOpening = false
|
this.evidencePreviewOpening = false
|
||||||
this.evidencePreviewReturning = true
|
this.evidencePreviewReturning = true
|
||||||
|
|||||||
@@ -4,6 +4,42 @@ import { readFile } from 'node:fs/promises'
|
|||||||
|
|
||||||
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8')
|
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8')
|
||||||
|
|
||||||
|
test('duplicate onShow shares one load chain while background return refreshes again', async () => {
|
||||||
|
const page = await read('../pages/picking/picking.vue')
|
||||||
|
const show = page.match(/async onShow\(\) \{[\s\S]*?\r?\n\t\t\},/)?.[0]
|
||||||
|
const hide = page.match(/onHide\(\) \{[\s\S]*?\r?\n\t\t\},/)?.[0]
|
||||||
|
assert.ok(show && hide)
|
||||||
|
const hooks = new Function('requireSession', 'session', `return ({${show}${hide}})`)(() => true, { currentUser: { id: '7' } })
|
||||||
|
const calls = []
|
||||||
|
let release
|
||||||
|
const pending = new Promise((resolve) => { release = resolve })
|
||||||
|
const context = { startHardwareScanner() {}, stopHardwareScanner() {}, async refreshAvailableBoxes() { calls.push('pending'); await pending }, async syncRemoteTask() { calls.push('detail+evidence') } }
|
||||||
|
const first = hooks.onShow.call(context)
|
||||||
|
const duplicate = hooks.onShow.call(context)
|
||||||
|
assert.deepEqual(calls, ['pending'])
|
||||||
|
release()
|
||||||
|
await Promise.all([first, duplicate])
|
||||||
|
assert.deepEqual(calls, ['pending', 'detail+evidence'])
|
||||||
|
await hooks.onShow.call(context)
|
||||||
|
assert.equal(calls.length, 2)
|
||||||
|
hooks.onHide.call(context)
|
||||||
|
await hooks.onShow.call(context)
|
||||||
|
assert.deepEqual(calls, ['pending', 'detail+evidence', 'pending', 'detail+evidence'])
|
||||||
|
const onLoad = page.match(/onLoad\(options = \{\}\) \{[\s\S]*?\r?\n\t\t\},/)?.[0]
|
||||||
|
assert.doesNotMatch(onLoad, /refreshAvailableBoxes|syncRemoteTask/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('camera and evidence preview resume still avoid unnecessary refreshes', async () => {
|
||||||
|
const page = await read('../pages/picking/picking.vue')
|
||||||
|
const show = page.match(/async onShow\(\) \{[\s\S]*?\r?\n\t\t\},/)?.[0]
|
||||||
|
const hooks = new Function('requireSession', 'session', `return ({${show}})`)(() => true, { currentUser: {} })
|
||||||
|
for (const flag of ['cameraPicking', 'evidencePreviewReturning']) {
|
||||||
|
const context = { [flag]: true, startHardwareScanner() {}, debugEvidence() {}, refreshAvailableBoxes() { throw new Error('unexpected refresh') } }
|
||||||
|
await hooks.onShow.call(context)
|
||||||
|
assert.equal(context._pageVisible, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('direct H5 refresh restores the page user before rendering picking content', async () => {
|
test('direct H5 refresh restores the page user before rendering picking content', async () => {
|
||||||
const page = await read('../pages/picking/picking.vue')
|
const page = await read('../pages/picking/picking.vue')
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,20 @@ const state = await import(`data:text/javascript;base64,${Buffer.from(source).to
|
|||||||
const items = [{ id: 1 }, { id: 2 }, { id: 3 }]
|
const items = [{ id: 1 }, { id: 2 }, { id: 3 }]
|
||||||
|
|
||||||
test('sealed pallet matches its DP code and never accepts an individual carton', () => {
|
test('sealed pallet matches its DP code and never accepts an individual carton', () => {
|
||||||
const pallet = { id: '1', unitType: 'PALLET', indivisible: true, palletNo: 'DP-260909-0143', boxCodes: ['IN#2-1', 'IN#2-2'], qty: 2 }
|
const pallet = { id: '1', unitType: 'SEALED_PALLET', indivisible: true, palletNo: 'DP-260909-0143', boxCodes: ['IN#2-1', 'IN#2-2'], qty: 2 }
|
||||||
assert.equal(state.classifyScan([pallet], {}, 'IN#2-1').type, 'unknown')
|
assert.equal(state.classifyScan([pallet], {}, 'IN#2-1').type, 'unknown')
|
||||||
assert.equal(state.classifyScan([pallet], {}, pallet.palletNo).type, 'pending')
|
assert.equal(state.classifyScan([pallet], {}, pallet.palletNo).type, 'pending')
|
||||||
assert.equal(state.classifyScan([pallet], { 1: state.ITEM_STATUS.PICKED }, pallet.palletNo).type, 'processed')
|
assert.equal(state.classifyScan([pallet], { 1: state.ITEM_STATUS.PICKED }, pallet.palletNo).type, 'processed')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('only the authoritative sealed-pallet unit type selects pallet behavior', () => {
|
||||||
|
assert.equal(state.isPalletItem({ unitType: 'SEALED_PALLET', indivisible: true }), true)
|
||||||
|
assert.equal(state.isPalletItem({ unitType: 'PALLET', indivisible: true }), false)
|
||||||
|
assert.equal(state.isPalletItem({ unitType: 'PACKAGE', indivisible: true }), false)
|
||||||
|
})
|
||||||
|
|
||||||
test('mixed task keeps package scanning separate from sealed pallet scanning', () => {
|
test('mixed task keeps package scanning separate from sealed pallet scanning', () => {
|
||||||
const mixed = [{ id: '1', unitType: 'PALLET', palletNo: 'DP-260909-0001', boxCodes: ['BOX-1'] }, { id: '2', unitType: 'PACKAGE', boxCodes: ['BOX-2'] }]
|
const mixed = [{ id: '1', unitType: 'SEALED_PALLET', palletNo: 'DP-260909-0001', boxCodes: ['BOX-1'] }, { id: '2', unitType: 'PACKAGE', boxCodes: ['BOX-2'] }]
|
||||||
assert.equal(state.classifyScan(mixed, {}, 'BOX-2').item.id, '2')
|
assert.equal(state.classifyScan(mixed, {}, 'BOX-2').item.id, '2')
|
||||||
assert.equal(state.classifyScan(mixed, {}, 'DP-260909-0001').item.id, '1')
|
assert.equal(state.classifyScan(mixed, {}, 'DP-260909-0001').item.id, '1')
|
||||||
assert.equal(state.classifyScan(mixed, {}, 'BOX-1').type, 'unknown')
|
assert.equal(state.classifyScan(mixed, {}, 'BOX-1').type, 'unknown')
|
||||||
|
|||||||
@@ -72,5 +72,5 @@ export function classifyScan(items, states, scanCode, scannedBoxCodes = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isPalletItem(item) {
|
export function isPalletItem(item) {
|
||||||
return item?.unitType === 'PALLET' || item?.indivisible === true
|
return item?.unitType === 'SEALED_PALLET'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user