This commit is contained in:
2026-08-27 15:02:40 +08:00
parent c06b808354
commit 1a0122b648
5 changed files with 20 additions and 11 deletions
+3 -1
View File
@@ -17,7 +17,7 @@
<button v-for="task in tasks" :key="task.taskId" class="task-card" :disabled="!isTaskReady(task)" :aria-label="`${taskActionLabel(task)},集装箱号 ${task.currentContainerNo}`" @tap="enterTask(task)">
<view class="task-card-head">
<view class="task-container-info"><text class="task-label">集装箱号</text><text class="task-code">{{ task.currentContainerNo }}</text></view>
<text class="task-status" :class="{ progress: task.workflowStatus === 'IN_PROGRESS', evidence: task.workflowStatus === 'AWAITING_REVIEW' }">{{ statusLabel(task.workflowStatus) }}</text>
<text class="task-status" :class="{ progress: task.workflowStatus === 'IN_PROGRESS', evidence: task.workflowStatus === 'AWAITING_REVIEW', processing: task.workflowStatus === 'PROCESSING', failed: task.workflowStatus === 'COMPLETION_FAILED' }">{{ statusLabel(task.workflowStatus) }}</text>
</view>
<view class="task-identifiers"><text class="task-cabinet">柜次号 {{ task.cabinetNo }}</text><text v-if="task.displayNo" class="task-order">{{ task.displayNo }}</text></view>
<view class="task-card-foot">
@@ -108,6 +108,8 @@
.task-status { flex-shrink: 0; padding: 4px 7px; border-radius: 99px; color: #52616f; font-size: 10px; line-height: 16px; background: #eef2f6; }
.task-status.progress { color: #155eef; background: #eff4ff; }
.task-status.evidence { color: #9a5b13; background: #fff4e5; }
.task-status.processing { color: #155eef; background: #eff4ff; }
.task-status.failed { color: #b42318; background: #fff1f0; }
.task-identifiers { display: flex; flex-wrap: wrap; gap: 5px 8px; margin-top: 8px; }
.task-cabinet, .task-order { padding: 3px 7px; border-radius: 5px; color: #475467; font-size: 11px; line-height: 17px; background: #f2f4f7; }
.task-order { overflow: hidden; max-width: 100%; text-overflow: ellipsis; white-space: nowrap; }
+2 -3
View File
@@ -35,13 +35,13 @@ test('completion failure exposes a readable stage and retry entry', () => {
})
})
test('retry reuses the original operation id with the latest server version and command facts', () => {
test('retry reuses the original operation id and request-hashed task version', () => {
const attempt = refreshCompletionAttempt(
{ operationId: 'complete-task-8-v3', expectedTaskVersion: 3, commandId: 'cmd-old' },
{ taskVersion: 7, commandId: 'cmd-latest', commandStatus: 'COMPLETION_FAILED' }
)
assert.equal(attempt.operationId, 'complete-task-8-v3')
assert.equal(attempt.expectedTaskVersion, 7)
assert.equal(attempt.expectedTaskVersion, 3)
assert.equal(attempt.commandId, 'cmd-latest')
assert.throws(() => refreshCompletionAttempt({}, { taskVersion: 7 }), /缺少原完成操作标识/)
assert.throws(() => refreshCompletionAttempt({ operationId: 'stable-id' }), /缺少服务端最新任务版本/)
@@ -59,4 +59,3 @@ test('completed facts win over stale processing facts', () => {
assert.equal(phase, COMPLETION_PHASE.COMPLETED)
assert.equal(completionPresentation(phase).label, '装柜已完成')
})
+4 -3
View File
@@ -59,13 +59,14 @@ test('picking task list uses remote loading orders and never falls back to demo
assert.match(tasks, /进入并开始拣货/)
assert.match(tasks, /AWAITING_REVIEW[\s\S]*?待上传证据\/确认装柜/)
assert.match(tasks, /task\.workflowStatus === 'AWAITING_REVIEW' \? '继续装柜取证'/)
assert.match(tasks, /\['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW'\]\.includes\(task\.workflowStatus\)/)
assert.match(tasks, /task\.workflowStatus === 'AWAITING_REVIEW' \|\| \(task\.assignmentVersion !== null/)
assert.match(tasks, /\['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW', 'COMPLETION_FAILED'\]\.includes\(task\.workflowStatus\)/)
assert.match(tasks, /\['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW', 'PROCESSING', 'COMPLETION_FAILED'\]\.includes\(task\.workflowStatus\)/)
assert.match(tasks, /\['AWAITING_REVIEW', 'COMPLETION_FAILED'\]\.includes\(task\.workflowStatus\) \|\| \(task\.assignmentVersion !== null/)
assert.match(tasks, /暂无待处理的拣货任务/)
assert.match(tasks, /class="task-label">集装箱号<\/text><text class="task-code">\{\{ task\.currentContainerNo \}\}/)
assert.match(tasks, /柜次号 \{\{ task\.cabinetNo \}\}/)
assert.match(tasks, /button v-for="task in tasks"[\s\S]*?class="task-card"[\s\S]*?@tap="enterTask\(task\)"/)
assert.match(tasks, /records\.filter\(\(task\) => this\.isTaskReady\(task\)\)/)
assert.match(tasks, /records\.filter\(\(task\) => this\.isTaskVisible\(task\)\)/)
assert.match(tasks, /isTaskReady\(task\)[\s\S]*?task\.recordStatus === 'ACTIVE'[\s\S]*?!task\.mergedIntoContainerBookingId/)
assert.match(tasks, /records\.filter\(\(task\) => this\.isMergedTask\(task\)\)\.forEach\(\(task\) => this\.clearMergedTaskDraft\(task\)\)/)
assert.match(tasks, /clearMergedTaskDraft\(task\)[\s\S]*?clearPickingDraft\(\{ operatorId, taskId: task\.taskId, loadingPlanId: task\.loadingPlanId, containerBookingId: task\.containerBookingId/)
+8
View File
@@ -29,3 +29,11 @@ test('mutable real container number is not part of draft identity or restore val
assert.doesNotMatch(taskIdentitySource, /containerNo|currentContainerNo|boxNo/)
assert.doesNotMatch(restoreGuard, /containerNo|currentContainerNo|boxNo/)
})
test('completion retry persists the exact accepted operation id and request version', () => {
assert.match(source, /pda-loading-completion/)
assert.match(source, /saveLoadingCompletionAttempt/)
assert.match(source, /restoreLoadingCompletionAttempt/)
assert.match(source, /operationId: String\(attempt\.operationId\)/)
assert.match(source, /expectedTaskVersion: Number\(attempt\.expectedTaskVersion\)/)
})
+3 -4
View File
@@ -47,12 +47,12 @@ export function shouldReconcileCompletion(error = {}) {
}
/**
* A retry keeps the original operationId while taking the latest server
* version/command facts. It must never mint a new idempotency identity.
* A retry keeps the original operationId and the exact accepted request facts.
* expectedTaskVersion participates in requestHash and must not be refreshed.
*/
export function refreshCompletionAttempt(previousAttempt = {}, latestFacts = {}) {
if (!previousAttempt.operationId) throw new Error('缺少原完成操作标识,不能安全重试')
const expectedTaskVersion = latestFacts.expectedTaskVersion ?? latestFacts.taskVersion ?? previousAttempt.expectedTaskVersion
const expectedTaskVersion = previousAttempt.expectedTaskVersion
if (expectedTaskVersion === null || expectedTaskVersion === undefined) throw new Error('缺少服务端最新任务版本,不能安全重试')
return {
...previousAttempt,
@@ -61,4 +61,3 @@ export function refreshCompletionAttempt(previousAttempt = {}, latestFacts = {})
expectedTaskVersion: Number(expectedTaskVersion)
}
}