适配PDA稳定柜次身份契约

This commit is contained in:
2026-08-12 18:32:31 +08:00
parent 0b51ed4347
commit c8509cb452
8 changed files with 142 additions and 56 deletions
+45 -22
View File
@@ -79,15 +79,26 @@ export function isRemoteReady(inboundId) {
return Boolean(session.token && (session.tenantId || inboundId))
}
export function getContainerPickingTask(containerNo, warehouseId) {
if (!containerNo) throw new Error('缺少柜次号,无法请求现场任务')
const query = [`containerNo=${encodeURIComponent(containerNo)}`]
if (warehouseId) query.push(`warehouseId=${encodeURIComponent(warehouseId)}`)
export function getContainerPickingTask(identity, warehouseId) {
const selector = typeof identity === 'object' && identity !== null ? identity : { containerNo: identity }
const taskId = selector.taskId ?? selector.id
if (taskId) {
if (!warehouseId) throw new Error('缺少作业仓库标识,无法请求现场任务')
return request({
url: `/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}?warehouseId=${encodeURIComponent(warehouseId)}`,
method: 'GET'
}).then(normalizePickingTask)
}
const query = []
if (selector.cabinetNo) query.push(`cabinetNo=${encodeURIComponent(selector.cabinetNo)}`)
else if (selector.loadingPlanId) query.push(`loadingPlanId=${encodeURIComponent(selector.loadingPlanId)}`)
else if (selector.containerNo) query.push(`containerNo=${encodeURIComponent(selector.containerNo)}`)
else throw new Error('缺少稳定柜次标识,无法请求现场任务')
if (warehouseId) query.push(`warehouseId=${encodeURIComponent(warehouseId)}`)
if (session.tenantId) query.push(`tenantId=${encodeURIComponent(session.tenantId)}`)
return request({ url: `/first-leg/pda-loading/tasks?${query.join('&')}`, method: 'GET' }).then((data) => {
if (Array.isArray(data)) return data[0] || null
if (data?.records) return data.records[0] || null
return data
const task = Array.isArray(data) ? data[0] : (data?.records ? data.records[0] : data)
return task ? normalizePickingTask(task) : null
})
}
@@ -104,10 +115,10 @@ 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'
})
return request({
url: `/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`,
method: 'GET'
}).then(normalizeInventorySnapshot)
}
function itemAction(taskId, itemId, path, payload, operationId) {
@@ -134,13 +145,13 @@ export function skipContainerItem(taskId, itemId, payload = {}) {
}
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 ?? ''}`
if (!task?.taskId) throw new Error('缺少拣货任务标识,无法提交')
if (!task.loadingPlanId || !task.containerBookingId) throw new Error('缺少稳定柜次标识,无法整批提交')
const submitOperationId = operationId || `submit-${task.taskId}-${task.loadingPlanId}-${task.containerBookingId}-${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,
action: action.type,
@@ -155,7 +166,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, inventorySnapshotToken, items }
data: { warehouseId, loadingPlanId: task.loadingPlanId, containerBookingId: task.containerBookingId, expectedVersion, inventorySnapshotToken, items }
})
}
@@ -175,23 +186,35 @@ export function normalizePdaOrder(row = {}) {
inboundId: row.inboundId ?? row.inbound?.id ?? null,
inboundVersion: row.inboundVersion ?? row.inbound?.version ?? null,
tenantId: row.tenantId ?? row.accountId ?? row.inbound?.tenantId ?? null,
pickingVersion: row.pickingVersion ?? row.loading?.pickingVersion ?? null,
taskId: row.taskId ?? row.pdaTaskId ?? row.loadingTaskId ?? row.loading?.taskId ?? null,
containerNo: row.containerNo ?? row.container?.containerNo ?? row.loadingPlan?.containerNo ?? row.loading?.containerNo ?? '',
pickingVersion: row.pickingVersion ?? row.loading?.pickingVersion ?? null,
taskId: row.taskId ?? row.pdaTaskId ?? row.loadingTaskId ?? row.loading?.taskId ?? null,
loadingPlanId: row.loadingPlanId ?? row.loading?.loadingPlanId ?? row.loadingPlan?.id ?? null,
containerBookingId: row.containerBookingId ?? row.loading?.containerBookingId ?? row.container?.id ?? null,
cabinetNo: row.cabinetNo ?? row.loading?.cabinetNo ?? row.container?.cabinetNo ?? row.containerNo ?? '',
currentContainerNo: row.currentContainerNo ?? row.containerNo ?? row.container?.containerNo ?? row.loadingPlan?.containerNo ?? row.loading?.containerNo ?? null,
warehouseId: row.warehouseId ?? row.warehouse?.id ?? row.loadingPlan?.warehouseId ?? null,
displayNo: row.orderNo ?? row.inboundCode ?? row.order?.orderNo ?? `任务 ${row.taskId ?? row.id}`
}
}
export function normalizePickingTask(task = {}) {
task = task || {}
return {
...task,
pickingVersion: task.pickingVersion ?? task.version ?? null,
task = task || {}
return {
...task,
taskId: task.taskId ?? task.id ?? null,
loadingPlanId: task.loadingPlanId ?? task.loading?.loadingPlanId ?? null,
containerBookingId: task.containerBookingId ?? task.loading?.containerBookingId ?? null,
cabinetNo: task.cabinetNo ?? task.loading?.cabinetNo ?? task.containerNo ?? '',
currentContainerNo: task.currentContainerNo ?? task.containerNo ?? null,
pickingVersion: task.pickingVersion ?? task.version ?? null,
inboundVersion: task.inboundVersion ?? null
}
}
export function normalizeInventorySnapshot(snapshot = {}) {
return normalizePickingTask({ ...snapshot, version: snapshot.taskVersion ?? snapshot.version })
}
export function getPickingTask(inboundId, tenantId) {
return request({
url: `/inbounds/${encodeURIComponent(inboundId)}/loading/picking?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
+16 -8
View File
@@ -14,7 +14,7 @@
<view class="work-content">
<view class="box-select-wrap">
<button class="box-select-btn" @tap="toggleBoxDropdown">
<text>{{ currentBox.boxNo }} · 订单 {{ currentBox.orderNo }}</text>
<text>{{ currentBox.boxNo }}<template v-if="currentBox.currentContainerNo"> · 当前柜号 {{ currentBox.currentContainerNo }}</template> · 订单 {{ currentBox.orderNo }}</text>
<text class="box-chevron" :class="{ open: boxDropdownOpen }">▼</text>
</button>
<view v-if="boxDropdownOpen" class="box-dropdown">
@@ -328,6 +328,7 @@
import { createEvidenceUploadQueue } from '@/utils/evidence-upload-queue.js'
import { isOnline } from '@/utils/network.js'
import { stableOperationId } from '@/utils/idempotency.js'
import { stableTaskOperationIdentity } from '@/utils/task-identity.js'
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import PopupHeader from '@/components/PopupHeader.vue'
@@ -452,9 +453,10 @@
},
onLoad(options = {}) {
if (!requireSession()) return
if (options.containerNo) {
if (options.taskId && (options.cabinetNo || options.containerNo)) {
this.boxes = [{
boxNo: String(options.containerNo), orderNo: String(options.orderNo || options.inboundCode || ''),
boxNo: String(options.cabinetNo || options.containerNo), cabinetNo: String(options.cabinetNo || options.containerNo), currentContainerNo: options.currentContainerNo ? String(options.currentContainerNo) : null, orderNo: String(options.orderNo || options.inboundCode || ''),
taskId: String(options.taskId), loadingPlanId: options.loadingPlanId ? String(options.loadingPlanId) : null, containerBookingId: options.containerBookingId ? String(options.containerBookingId) : null,
inboundId: options.inboundId ? String(options.inboundId) : null,
inboundVersion: options.inboundVersion == null ? null : Number(options.inboundVersion),
warehouseId: options.warehouseId ? String(options.warehouseId) : null,
@@ -489,7 +491,7 @@
},
draftIdentity() {
const operatorId = session.currentUser?.id || session.currentUser?.userId || session.currentUser?.accountId
return { operatorId, taskId: this.remoteTaskId || this.currentBox.taskId || this.currentBox.inboundId, containerNo: this.currentBox.boxNo, warehouseId: this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse }
return { operatorId, taskId: this.remoteTaskId || this.currentBox.taskId, loadingPlanId: this.currentBox.loadingPlanId || this.remotePick?.loadingPlanId, containerBookingId: this.currentBox.containerBookingId || this.remotePick?.containerBookingId, warehouseId: this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse }
},
restoreRemoteDraft() {
const restored = restorePickingSession(this.draftIdentity())
@@ -672,7 +674,7 @@
this.remoteError = ''
try {
const warehouseId = this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse
const task = await getContainerPickingTask(this.currentBox.boxNo, warehouseId)
const task = await getContainerPickingTask({ taskId: this.remoteTaskId || this.currentBox.taskId, cabinetNo: this.currentBox.cabinetNo, loadingPlanId: this.currentBox.loadingPlanId, containerNo: this.currentBox.currentContainerNo }, warehouseId)
if (!task) throw new Error('未找到当前柜次的拣货任务')
const remoteItems = task.items || task.pickItems || task.details || task.lines
if (Array.isArray(remoteItems) && remoteItems.length) {
@@ -703,7 +705,12 @@
this.remoteTaskId = task.taskId ?? task.id
if (!this.remoteTaskId) throw new Error('服务端任务缺少 taskId,无法开始拣货')
this.assignmentVersion = task.assignmentVersion ?? task.version ?? 0
const operationId = `claim-${session.currentUser?.id || 'operator'}-${this.remoteTaskId}-${this.currentBox.boxNo}`
this.currentBox.loadingPlanId = task.loadingPlanId
this.currentBox.containerBookingId = task.containerBookingId
this.currentBox.cabinetNo = task.cabinetNo
this.currentBox.boxNo = task.cabinetNo
this.currentBox.currentContainerNo = task.currentContainerNo
const operationId = `claim-${session.currentUser?.id || 'operator'}-${this.remoteTaskId}-${task.loadingPlanId}-${task.containerBookingId}`
const claimed = await claimContainerPickingTask(this.remoteTaskId, this.assignmentVersion, operationId)
this.remotePick = normalizePickingTask({ ...task, ...(claimed || {}) })
this.pickingVersion = this.remotePick.pickingVersion ?? this.remotePick.version ?? null
@@ -937,7 +944,8 @@
try {
if (!this.remoteTaskId) throw new Error('尚未进入服务端拣货任务,请联网刷新后重试')
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 })
const operationIdentity = stableTaskOperationIdentity(this.draftIdentity())
const submitId = this.submitId || stableOperationId('submit', operationIdentity, { 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)
@@ -950,7 +958,7 @@
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, operationId: submitId, inventorySnapshotToken: inventorySnapshot?.snapshotToken })
await submitContainerPickingBatch({ taskId: this.remoteTaskId, loadingPlanId: this.currentBox.loadingPlanId, containerBookingId: this.currentBox.containerBookingId, 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
+8 -6
View File
@@ -9,11 +9,12 @@
<view v-if="error" class="task-error" role="alert">{{ error }}</view>
<view v-if="loading" class="task-state">正在加载真实任务...</view>
<view v-else-if="!tasks.length && !error" class="task-state">暂无待处理拣货任务</view>
<view v-for="task in tasks" :key="task.id || task.inboundId || task.displayNo" class="task-card">
<view class="task-card-head"><text class="task-code">{{ task.containerNo || '待分配柜次' }}</text><text class="task-status">{{ statusLabel(task.workflowStatus) }}</text></view>
<view v-for="task in tasks" :key="task.taskId" class="task-card">
<view class="task-card-head"><text class="task-code">{{ task.cabinetNo || '待分配柜次' }}</text><text class="task-status">{{ statusLabel(task.workflowStatus) }}</text></view>
<text v-if="task.currentContainerNo" class="task-order">当前真实柜号 {{ task.currentContainerNo }}</text>
<text class="task-order">{{ task.displayNo || '未命名订单' }}</text>
<text class="task-meta">{{ task.warehouseName || (task.warehouseId ? `仓库 ${task.warehouseId}` : '缺少仓库信息') }} · 待处理 {{ task.pendingItemCount ?? task.itemCount ?? 0 }} 项</text>
<button class="task-enter" :disabled="!task.containerNo || !task.warehouseId" @tap="enterTask(task)">{{ task.containerNo && task.warehouseId ? '进入并开始拣货' : '缺少任务信息' }}</button>
<button class="task-enter" :disabled="!isTaskReady(task)" @tap="enterTask(task)">{{ isTaskReady(task) ? '进入并开始拣货' : '任务暂不可进入' }}</button>
</view>
</view>
<view v-if="toast.visible" class="app-toast" :class="toast.type" role="status">{{ toast.message }}</view>
@@ -34,19 +35,20 @@
onUnload() { if (this._stopNetwork) this._stopNetwork(); clearTimeout(this._toastTimer) },
methods: {
statusLabel(status) { return status === 'IN_PROGRESS' ? '拣货中' : '待领取' },
isTaskReady(task) { return Boolean(task.taskId && task.loadingPlanId && task.containerBookingId && task.cabinetNo && task.warehouseId && task.taskStatus === 'LOADING' && ['UNASSIGNED', 'IN_PROGRESS'].includes(task.workflowStatus)) },
async loadTasks() {
if (!isOnline()) { this.error = '当前网络不可用,请联网后刷新'; return }
this.loading = true; this.error = ''
try {
const result = await listLoadingOrders({ page: 1, size: 50 })
this.tasks = (result.records || []).filter((task) => task.containerNo || task.taskId || task.inboundId)
this.tasks = (result.records || []).filter((task) => task.taskId)
} catch (error) {
this.error = error.network ? '当前网络不可用,请联网后刷新' : (error.message || '任务加载失败')
} finally { this.loading = false }
},
enterTask(task) {
if (!task.containerNo || !task.warehouseId) { this.showToast('任务缺少柜号或仓库标识,无法开始', 'error'); return }
uni.navigateTo({ url: `/pages/picking/picking?containerNo=${encodeURIComponent(task.containerNo)}&warehouseId=${encodeURIComponent(task.warehouseId)}&inboundId=${encodeURIComponent(task.inboundId || '')}&inboundVersion=${encodeURIComponent(task.inboundVersion ?? '')}&tenantId=${encodeURIComponent(task.tenantId || '')}&taskId=${encodeURIComponent(task.taskId || '')}&orderNo=${encodeURIComponent(task.displayNo || '')}` })
if (!this.isTaskReady(task)) { this.showToast('任务尚未满足后端进入条件', 'error'); return }
uni.navigateTo({ url: `/pages/picking/picking?cabinetNo=${encodeURIComponent(task.cabinetNo)}&currentContainerNo=${encodeURIComponent(task.currentContainerNo || '')}&loadingPlanId=${encodeURIComponent(task.loadingPlanId)}&containerBookingId=${encodeURIComponent(task.containerBookingId)}&warehouseId=${encodeURIComponent(task.warehouseId)}&taskId=${encodeURIComponent(task.taskId)}&orderNo=${encodeURIComponent(task.displayNo || '')}` })
},
showToast(message, type) { clearTimeout(this._toastTimer); this.toast = { visible: true, message, type }; this._toastTimer = setTimeout(() => { this.toast.visible = false }, 2200) }
}
+10 -14
View File
@@ -1,5 +1,6 @@
import { createBoxes } from '@/data/demo-data.js'
import { createItemStates } from '@/utils/picking-state.js'
import { stableTaskIdentity, stableTaskIdentityKey } from '@/utils/task-identity.js'
function clone(value) {
return JSON.parse(JSON.stringify(value))
@@ -23,25 +24,18 @@ function storageAvailable() {
return typeof uni !== 'undefined' && typeof uni.getStorageSync === 'function' && typeof uni.setStorageSync === 'function'
}
function identityValue(identity = {}) {
const operatorId = identity.operatorId ?? identity.accountId ?? identity.userId
const taskId = identity.taskId ?? identity.inboundId
const containerNo = identity.containerNo ?? identity.boxNo
return { operatorId: operatorId == null ? '' : String(operatorId), taskId: taskId == null ? '' : String(taskId), containerNo: containerNo == null ? '' : String(containerNo) }
}
export function getPickingDraftKey(identity = {}) {
const value = identityValue(identity)
if (!value.operatorId || !value.taskId || !value.containerNo) return ''
return `${STORAGE_PREFIX}:${encodeURIComponent(value.operatorId)}:${encodeURIComponent(value.taskId)}:${encodeURIComponent(value.containerNo)}`
const identityKey = stableTaskIdentityKey(identity)
return identityKey ? `${STORAGE_PREFIX}:${identityKey}` : ''
}
function draftPayload(nextState, identity) {
const value = identityValue(identity)
const value = stableTaskIdentity(identity)
return {
operatorId: value.operatorId,
taskId: value.taskId,
containerNo: value.containerNo,
loadingPlanId: value.loadingPlanId,
containerBookingId: value.containerBookingId,
warehouseId: identity.warehouseId ?? null,
currentBoxIndex: nextState.currentBoxIndex,
itemStates: nextState.itemStates,
@@ -68,11 +62,13 @@ export function restorePickingSession(identity = {}) {
const key = getPickingDraftKey(identity)
if (!key || !storageAvailable()) return null
const saved = uni.getStorageSync(key)
if (!saved || saved.operatorId !== identityValue(identity).operatorId || saved.taskId !== identityValue(identity).taskId || saved.containerNo !== identityValue(identity).containerNo) return null
const expected = stableTaskIdentity(identity)
if (!saved || saved.operatorId !== expected.operatorId || saved.taskId !== expected.taskId || saved.loadingPlanId !== expected.loadingPlanId || saved.containerBookingId !== expected.containerBookingId) return null
const restored = { ...state, ...clone(saved) }
delete restored.operatorId
delete restored.taskId
delete restored.containerNo
delete restored.loadingPlanId
delete restored.containerBookingId
delete restored.warehouseId
delete restored.updatedAt
state = restored
+16
View File
@@ -2,6 +2,7 @@ import test from 'node:test'
import assert from 'node:assert/strict'
const { stableOperationId } = await import(new URL('../utils/idempotency.js', import.meta.url).href)
const { stableTaskIdentityKey, stableTaskOperationIdentity } = await import(new URL('../utils/task-identity.js', import.meta.url).href)
test('PDA operation id is stable ASCII and within the backend 128 character limit', () => {
const payload = { itemStates: Object.fromEntries(Array.from({ length: 500 }, (_, index) => [`明细-${index}`, 'PICKED'])), reason: '中文异常原因'.repeat(100) }
@@ -17,3 +18,18 @@ test('PDA operation id changes when the draft changes', () => {
const second = stableOperationId('submit', 'operator-task-container', { item: 'SKIPPED' })
assert.notEqual(first, second)
})
test('real container number changes do not move the draft or idempotency identity', () => {
const before = { operatorId: 7, taskId: 11, loadingPlanId: 13, containerBookingId: 17, currentContainerNo: 'REAL-001' }
const after = { ...before, currentContainerNo: 'REAL-002' }
const payload = { item: 'PICKED' }
assert.equal(stableTaskIdentityKey(before), stableTaskIdentityKey(after))
assert.equal(stableOperationId('submit', stableTaskOperationIdentity(before), payload), stableOperationId('submit', stableTaskOperationIdentity(after), payload))
})
test('container booking mismatch is rejected by stable task binding', () => {
const original = { operatorId: 7, taskId: 11, loadingPlanId: 13, containerBookingId: 17 }
const differentBooking = { ...original, containerBookingId: 18 }
assert.equal(stableTaskIdentityKey(original), stableTaskIdentityKey(differentBooking))
assert.notEqual(stableTaskOperationIdentity(original), stableTaskOperationIdentity(differentBooking))
})
+9 -1
View File
@@ -68,7 +68,15 @@ 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, inventorySnapshotToken, items \}/)
assert.match(source, /data: \{ warehouseId, loadingPlanId: task\.loadingPlanId, containerBookingId: task\.containerBookingId, expectedVersion, inventorySnapshotToken, items \}/)
assert.match(source, /cabinetNo=/)
assert.match(source, /loadingPlanId=/)
assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\?warehouseId=/)
assert.match(source, /currentContainerNo/)
assert.match(source, /getPickingInventorySnapshot[\s\S]*?\.then\(normalizeInventorySnapshot\)/)
assert.doesNotMatch(source.match(/export async function submitContainerPickingBatch[\s\S]*?\n\}/)?.[0] || '', /containerNo/)
assert.doesNotMatch(page, /claim-[^`]*currentBox\.boxNo/)
assert.doesNotMatch(page.match(/stableOperationId\('submit'[\s\S]*?\n/)?.[0] || '', /boxNo|containerNo|currentContainerNo/)
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 \(/)
+15 -5
View File
@@ -3,19 +3,29 @@ import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const source = await readFile(new URL('../stores/picking.js', import.meta.url), 'utf8')
const taskIdentitySource = await readFile(new URL('../utils/task-identity.js', import.meta.url), 'utf8')
test('picking drafts are persisted under operator, task and cabinet identity', () => {
test('picking drafts are persisted under stable operator, task and loading plan identity', () => {
assert.match(source, /pda-picking-draft/)
assert.match(source, /operatorId/)
assert.match(source, /taskId/)
assert.match(source, /containerNo/)
assert.match(source, /loadingPlanId/)
assert.match(source, /containerBookingId/)
assert.match(source, /uni\.setStorageSync\(key/)
assert.match(source, /uni\.getStorageSync\(key/)
assert.match(source, /uni\.removeStorageSync\(key/)
})
test('draft restore rejects a different operator or task', () => {
assert.match(source, /saved\.operatorId !== identityValue\(identity\)\.operatorId/)
assert.match(source, /saved\.taskId !== identityValue\(identity\)\.taskId/)
assert.match(source, /saved\.containerNo !== identityValue\(identity\)\.containerNo/)
assert.match(source, /saved\.operatorId !== expected\.operatorId/)
assert.match(source, /saved\.taskId !== expected\.taskId/)
assert.match(source, /saved\.loadingPlanId !== expected\.loadingPlanId/)
assert.match(source, /saved\.containerBookingId !== expected\.containerBookingId/)
assert.doesNotMatch(taskIdentitySource, /containerNo|currentContainerNo|boxNo/)
})
test('mutable real container number is not part of draft identity or restore validation', () => {
const restoreGuard = source.match(/const expected = stableTaskIdentity[\s\S]*?return null/)?.[0] || ''
assert.doesNotMatch(taskIdentitySource, /containerNo|currentContainerNo|boxNo/)
assert.doesNotMatch(restoreGuard, /containerNo|currentContainerNo|boxNo/)
})
+23
View File
@@ -0,0 +1,23 @@
function text(value) {
return value == null ? '' : String(value)
}
export function stableTaskIdentity(identity = {}) {
return {
operatorId: text(identity.operatorId ?? identity.accountId ?? identity.userId),
taskId: text(identity.taskId ?? identity.inboundId),
loadingPlanId: text(identity.loadingPlanId),
containerBookingId: text(identity.containerBookingId)
}
}
export function stableTaskIdentityKey(identity = {}) {
const value = stableTaskIdentity(identity)
if (!value.operatorId || !value.taskId || !value.loadingPlanId) return ''
return [value.operatorId, value.taskId, value.loadingPlanId].map(encodeURIComponent).join(':')
}
export function stableTaskOperationIdentity(identity = {}) {
const value = stableTaskIdentity(identity)
return `${value.operatorId}-${value.taskId}-${value.loadingPlanId}-${value.containerBookingId}`
}