diff --git a/api/pda-loading.js b/api/pda-loading.js index 3a0ab9d..a26cf55 100644 --- a/api/pda-loading.js +++ b/api/pda-loading.js @@ -101,6 +101,15 @@ export function claimContainerPickingTask(taskId, expectedAssignmentVersion, ope }) } +export function getPickingInventorySnapshot(taskId, warehouseId) { + if (!taskId) throw new Error('缺少拣货任务标识,无法查询实时库存') + if (!warehouseId) throw new Error('缺少作业仓库标识,无法查询实时库存') + return request({ + url: `/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`, + method: 'GET' + }) +} + function itemAction(taskId, itemId, path, payload, operationId) { // Concrete routes include /items/${encodeURIComponent(itemId)}/pick, /items/${encodeURIComponent(itemId)}/partial and /items/${encodeURIComponent(itemId)}/skip. if (!taskId || itemId == null) throw new Error('缺少拣货任务或明细标识,无法提交') @@ -124,12 +133,13 @@ export function skipContainerItem(taskId, itemId, payload = {}) { return itemAction(taskId, itemId, 'skip', payload, payload.operationId) } -export async function submitContainerPickingBatch(task, actions, { warehouseId, operationId } = {}) { +export async function submitContainerPickingBatch(task, actions, { warehouseId, operationId, inventorySnapshotToken } = {}) { if (!task?.taskId) throw new Error('缺少拣货任务标识,无法提交') const submitOperationId = operationId || `submit-${task.taskId}-${task.containerNo || ''}-${task.version ?? task.pickingVersion ?? ''}` const expectedVersion = task.version ?? task.pickingVersion if (expectedVersion == null) throw new Error('缺少拣货任务版本,无法整批提交') if (!warehouseId) throw new Error('缺少作业仓库标识,无法整批提交') + if (!inventorySnapshotToken) throw new Error('缺少实时库存快照,请刷新后重试') if (!task.containerNo) throw new Error('缺少柜号,无法整批提交') const items = (actions || []).map((action) => ({ itemId: action.itemId, @@ -145,7 +155,7 @@ export async function submitContainerPickingBatch(task, actions, { warehouseId, url: `/first-leg/pda-loading/tasks/${encodeURIComponent(task.taskId)}/batch`, method: 'POST', header: { 'Idempotency-Key': stableIdempotencyKey(submitOperationId) }, - data: { warehouseId, containerNo: task.containerNo, expectedVersion, items } + data: { warehouseId, containerNo: task.containerNo, expectedVersion, inventorySnapshotToken, items } }) } diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue index 4104dae..dda7d44 100644 --- a/pages/picking/picking.vue +++ b/pages/picking/picking.vue @@ -324,7 +324,7 @@ import { navigateBackOr } from '@/utils/navigation.js' import { startScanBroadcast } from '@/utils/scan-broadcast.js' import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js' - import { claimContainerPickingTask, completeLoading, completePicking, EVIDENCE_SLOTS, getContainerPickingTask, getEvidence, getPickingTask, isRemoteReady, normalizePickingTask, submitContainerPickingBatch, uploadEvidenceFile, upsertEvidence, voidEvidence } from '@/api/pda-loading.js' + import { claimContainerPickingTask, completeLoading, completePicking, EVIDENCE_SLOTS, getContainerPickingTask, getEvidence, getPickingInventorySnapshot, getPickingTask, isRemoteReady, normalizePickingTask, submitContainerPickingBatch, uploadEvidenceFile, upsertEvidence, voidEvidence } from '@/api/pda-loading.js' import { createEvidenceUploadQueue } from '@/utils/evidence-upload-queue.js' import { isOnline } from '@/utils/network.js' import { stableOperationId } from '@/utils/idempotency.js' @@ -909,6 +909,8 @@ if (this.pickingVersion === null) throw new Error('缺少拣货版本号 pickingVersion,无法提交') const submitId = this.submitId || stableOperationId('submit', `${session.currentUser?.id || 'operator'}-${this.remoteTaskId}-${this.currentBox.boxNo}`, { itemStates: this.itemStates, partialQuantities: this.partialQuantities, incompleteReason }) this.submitId = submitId + const warehouseId = this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse + const inventorySnapshot = await getPickingInventorySnapshot(this.remoteTaskId, warehouseId) const actions = this.currentBox.items.map((item) => { const status = this.itemState(item) const itemId = item.taskItemId ?? item.remoteItemId ?? item.id @@ -918,12 +920,14 @@ if (incompleteReason) return { type: 'SKIP', itemId, reason: `${incompleteReason}(提交时确认未拣)`, version: Number(item.version || 0) } return null }).filter(Boolean) - await submitContainerPickingBatch({ taskId: this.remoteTaskId, containerNo: this.currentBox.boxNo, version: Number(this.pickingVersion) }, actions, { warehouseId: this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse, operationId: submitId }) + await submitContainerPickingBatch({ taskId: this.remoteTaskId, containerNo: this.currentBox.boxNo, version: Number(this.pickingVersion) }, actions, { warehouseId, operationId: submitId, inventorySnapshotToken: inventorySnapshot?.snapshotToken }) // Legacy inbound completePicking(expectedVersion: Number(this.pickingVersion)) remains adapter-only; cabinet tasks use item actions. } catch (error) { const versionConflict = error.code === 'A0409' || error.statusCode === 409 - this.remoteError = versionConflict ? '任务版本已变化,请刷新后重试' : (error.network ? '网络失败,拣货完成未提交,请重试' : (error.message || '拣货完成失败')) - this.showToast(versionConflict ? '任务版本已变化,请刷新后重试' : '拣货完成未提交', 'error') + const inventoryConflict = versionConflict && /库存/.test(error.message || '') + const conflictMessage = inventoryConflict ? '库存已变化,请刷新后重试' : '任务版本已变化,请刷新后重试' + this.remoteError = versionConflict ? conflictMessage : (error.network ? '网络失败,拣货完成未提交,请重试' : (error.message || '拣货完成失败')) + this.showToast(versionConflict ? conflictMessage : '拣货完成未提交', 'error') return } finally { this.submitting = false } } diff --git a/tests/pda-loading-api.test.mjs b/tests/pda-loading-api.test.mjs index 214bd05..d669203 100644 --- a/tests/pda-loading-api.test.mjs +++ b/tests/pda-loading-api.test.mjs @@ -59,6 +59,7 @@ test('PDA cabinet picking adapter claims online and submits one atomic batch', ( assert.match(source, /\/first-leg\/pda-loading\/tasks\?/) assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/claim/) assert.match(source, /tasks\/\$\{encodeURIComponent\(task\.taskId\)\}\/batch/) + assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/inventory-snapshot\?warehouseId=/) assert.match(source, /submitContainerPickingBatch/) assert.match(source, /stableIdempotencyKey/) assert.match(source, /Math\.imul\(hash, 16777619\)/) @@ -66,7 +67,9 @@ test('PDA cabinet picking adapter claims online and submits one atomic batch', ( assert.match(page, /claimContainerPickingTask\(/) assert.match(page, /submitContainerPickingBatch\(/) assert.match(page, /任务版本已变化,请刷新后重试/) - assert.match(source, /data: \{ warehouseId, containerNo: task\.containerNo, expectedVersion, items \}/) + assert.match(source, /data: \{ warehouseId, containerNo: task\.containerNo, expectedVersion, inventorySnapshotToken, items \}/) + assert.match(page, /await getPickingInventorySnapshot\([\s\S]*await submitContainerPickingBatch/) + assert.match(page, /库存已变化,请刷新后重试/) assert.doesNotMatch(source.match(/export async function submitContainerPickingBatch[\s\S]*?\n\}/)?.[0] || '', /for \(/) assert.match(page, /version: Number\(item\.version \|\| 0\)/) assert.match(page, /refreshTask/)