适配PDA稳定柜次身份契约
This commit is contained in:
+45
-22
@@ -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))}`,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}¤tContainerNo=${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
@@ -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
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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 \(/)
|
||||
|
||||
@@ -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/)
|
||||
})
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
Reference in New Issue
Block a user