Files
pda-uniapp/tests/picking-evidence-stage.test.mjs
T
2026-09-15 15:01:03 +08:00

279 lines
20 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
test('picking and photo workspaces are independently selectable', () => {
assert.match(page, /v-if="activeStage === 'evidence'" class="loading-evidence-section"/)
assert.match(page, /selectStage\(stage\) \{[\s\S]*?this\.activeStage = stage/)
assert.doesNotMatch(page, /:disabled="!loadingStageReady"/)
assert.match(page, /isPickingCompleteTask\(task\)[\s\S]*?AWAITING_REVIEW[\s\S]*?COMPLETED/)
assert.match(page, /pickingCompleted\(\)[\s\S]*?this\.remoteTaskId \|\| this\.currentBox\.taskId[\s\S]*?if \(!this\.remotePick\) return false[\s\S]*?hasServerPending[\s\S]*?isPickingCompleteTask\(this\.remotePick\) && !hasServerPending/)
assert.doesNotMatch(page, /任务增量更新后|evidence-stage-hint/)
})
test('a server task never inherits a completed marker from the previously opened local cabinet', () => {
assert.match(page, /pickingCompleted\(\) \{[\s\S]*?this\.remoteTaskId \|\| this\.currentBox\.taskId[\s\S]*?if \(!this\.remotePick\) return false[\s\S]*?hasServerPending/)
assert.match(page, /if \(options\.taskId\) \{[\s\S]*?this\.completedBoxes = \{\}[\s\S]*?this\.boxes = \[/)
assert.match(page, /:disabled="submitting \|\| \(!pickingCompleted && !summary\.canSubmit\)"/)
assert.match(page, /submitButtonText\(\)[\s\S]*?return this\.pickingCompleted \? '继续装柜取证'/)
assert.match(page, /submitStatusHint\(\)[\s\S]*?if \(this\.pickingCompleted\) return '拣货已完成,请继续装柜取证'/)
const submit = page.match(/async submitPicking\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.doesNotMatch(submit, /completedBoxes/)
assert.match(page, /v-else-if="pickingOperationReady" class="pick-item-actions processed-actions"/)
assert.doesNotMatch(page, /v-else-if="!(?:pickingCompleted|completedBoxes\[currentBoxIndex\])"/)
})
test('picking completion leads to current cabinet evidence instead of an all-complete summary', () => {
assert.match(page, /装柜照片已拍 \{\{ evidenceCapturedTypeCount \}\}\/\{\{ evidenceSlots\.length \}\} 类/)
assert.match(page, /evidenceNextButtonText\(\)[\s\S]*?'查看装柜取证'[\s\S]*?'去拍照'/)
assert.match(page, /continueToEvidence\(\)[\s\S]*?this\.activeStage = 'evidence'[\s\S]*?this\.scrollToTop\(\)/)
assert.doesNotMatch(page, /查看全部完成|allCompletePopup|nextBox\(\)/)
})
test('loading evidence is requested by task id as soon as the task is claimed', () => {
assert.match(page, /this\.evidenceIdentity = this\.captureEvidenceIdentity\(\)[\s\S]*?await this\.loadLoadingEvidence\(\)/)
const loader = page.match(/async loadLoadingEvidence\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(loader, /if \(!this\.evidenceIdentity\?\.taskId/)
assert.doesNotMatch(loader, /if \(!this\.loadingStageReady/)
assert.match(loader, /await getEvidence\(/)
})
test('operators receive actionable refresh guidance without internal version wording', () => {
assert.match(page, /状态尚未同步,请刷新重试/)
assert.doesNotMatch(page, /缺少拣货版本号|提供装柜版本号/)
const complete = page.match(/async completeLoadingTask\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(complete, /if \(!this\.requireEvidenceStage\(\)\) return/)
assert.match(complete, /completeContainerPickingTask\(this\.remoteTaskId/)
assert.match(complete, /expectedTaskVersion/)
assert.doesNotMatch(complete, /completeLoading\(inboundId/)
})
test('AWAITING_REVIEW uses the canonical task version for final completion', () => {
assert.match(page, /loadingStageReady\(\)[\s\S]*?this\.pickingCompleted[\s\S]*?this\.evidenceIdentity\.taskId[\s\S]*?this\.evidenceIdentity\.warehouseId[\s\S]*?this\.pickingVersion !== null[\s\S]*?Number\.isFinite/)
assert.match(page, /拣货已完成,装柜状态尚未同步,请刷新重试/)
assert.match(page, /async completeLoadingTask\([^)]*\) \{[\s\S]*?requireEvidenceStage/)
assert.match(page, /requireEvidenceMutation\(\)[\s\S]*?requireEvidenceCapture/)
})
test('dynamic pending items do not block switching between picking and evidence', () => {
assert.match(page, /class="stage-switch" role="tablist"/)
assert.match(page, /@tap="selectStage\('picking'\)"/)
assert.match(page, /@tap="selectStage\('evidence'\)"/)
assert.doesNotMatch(page, /stage === 'evidence' && !this\.loadingStageReady/)
assert.doesNotMatch(page, /请先完成新增待拣明细/)
assert.match(page, /activeStage === 'picking'/)
assert.match(page, /:disabled="submitting \|\| !loadingStageReady \|\| pickingVersion === null/)
})
test('incremental refresh merges new pending items without overwriting submitted server results', () => {
const restore = page.match(/restoreRemoteDraft\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(restore, /const serverStates = \{ \.\.\.this\.itemStates \}/)
assert.match(restore, /serverStates\[id\] !== ITEM_STATUS\.PENDING/)
assert.match(restore, /status === ITEM_STATUS\.PARTIAL[\s\S]*?partial >= Number\(item\?\.qty/)
assert.match(restore, /this\.remotePick && !isPickingCompleteTask\(this\.remotePick\)[\s\S]*?delete completedBoxes\[this\.currentBoxIndex\]/)
assert.match(page, /const previousTaskVersion = this\.pickingVersion/)
assert.match(page, /const previousItemIds = new Set/)
assert.match(page, /previousEvidenceState[\s\S]*?isSameEvidenceIdentity\(previousEvidenceState\.identity, this\.evidenceIdentity\)[\s\S]*?this\.evidenceFiles = \{ \.\.\.this\.evidenceFiles, \.\.\.previousEvidenceState\.files \}/)
assert.match(page, /if \(!this\.pickingCompleted\)[\s\S]*?delete completedBoxes\[this\.currentBoxIndex\]/)
assert.doesNotMatch(page, /delete completedBoxes\[this\.currentBoxIndex\][\s\S]{0,160}?this\.activeStage = 'picking'/)
assert.match(page, /refresh\.incremental[\s\S]*?addedItemIds/)
})
test('incremental batch submits only current server-pending items and refreshes stale inventory conflicts', () => {
const submit = page.match(/async performSubmit\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(submit, /item\.itemStatus && !\['PENDING', 'PICKING'\]\.includes\(item\.itemStatus\)\) return null/)
assert.match(submit, /const versionConflict = this\.isTaskConflict\(error\)/)
assert.match(submit, /await this\.refreshAfterTaskConflict\(conflictMessage\)/)
assert.match(submit, /const inventorySnapshot = await this\.refreshInventorySnapshot\(\)/)
assert.match(page, /refreshAfterTaskConflict\(message\)[\s\S]*?this\.submitId = ''[\s\S]*?await this\.syncRemoteTask\(\)[\s\S]*?await this\.refreshInventorySnapshot\(\)/)
assert.match(page, /refreshInventorySnapshot\(\)[\s\S]*?getPickingInventorySnapshot\(this\.remoteTaskId, warehouseId\)[\s\S]*?String\(snapshot\.version\) !== String\(this\.pickingVersion\)[\s\S]*?error\.code = 'A0409'/)
assert.match(page, /this\.pickingVersion = this\.remotePick\.pickingVersion \?\? this\.remotePick\.version/)
})
test('completed detail overrides stale local state and an empty batch refreshes into evidence instead of submitting', () => {
assert.match(page, /if \(item\.itemStatus === 'PICKED'\) serverStates\[item\.id\] = ITEM_STATUS\.PICKED/)
assert.match(page, /if \(!this\.pickingCompleted\) this\.restoreRemoteDraft\(\)/)
assert.match(page, /await this\.loadLoadingEvidence\(\)[\s\S]*?if \(this\.loadingStageReady\) this\.activeStage = 'evidence'/)
const submit = page.match(/async submitPicking\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(submit, /if \(this\.pickingCompleted\)[\s\S]*?await this\.syncRemoteTask\(\)[\s\S]*?this\.activeStage = 'evidence'[\s\S]*?return/)
const perform = page.match(/async performSubmit\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(perform, /if \(!actions\.length\) \{[\s\S]*?submit\.empty-refresh[\s\S]*?await this\.syncRemoteTask\(\)[\s\S]*?return/)
assert.ok(perform.indexOf('if (!actions.length)') < perform.indexOf('submitContainerPickingBatch('))
})
test('successful batch submission automatically refreshes canonical item status before completion gates', () => {
const perform = page.match(/async performSubmit\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
const submitIndex = perform.indexOf('await submitContainerPickingBatch(')
const refreshIndex = perform.indexOf('await this.syncRemoteTask()', submitIndex)
const gateIndex = perform.indexOf('if (!this.pickingCompleted)', refreshIndex)
assert.ok(submitIndex >= 0 && refreshIndex > submitIndex, 'canonical detail should refresh after the accepted batch')
assert.ok(gateIndex > refreshIndex, 'completion should be evaluated only after the automatic refresh')
assert.match(perform, /拣货已提交,正在刷新最新状态/)
assert.match(perform, /拣货已提交,但最新任务状态尚未同步,请点击刷新任务/)
assert.match(perform, /if \(!this\.pickingCompleted\)[\s\S]*?return/)
})
test('final completion preserves backend gate errors and refreshes after an atomic A0409', () => {
const complete = page.match(/async completeLoadingTask\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(complete, /this\.isTaskConflict\(error\)[\s\S]*?await this\.refreshAfterTaskConflict\(sanitizeUserMessage\(error\.message, '订舱、装柜关联或证据状态已变化,请刷新后重试'\)\)/)
assert.doesNotMatch(complete, /拖车|trucking/i)
})
test('seal photo upload waits for an explicit seal-number save without waiting for picking completion', () => {
const chooser = page.match(/chooseEvidence\(slot\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(chooser, /slot\.type === 'SEAL'[\s\S]*?照片已拍摄,请输入封签号并保存上传[\s\S]*?return/)
assert.match(page, /async saveSealNo\(\)[\s\S]*?await this\.queueEvidenceUpload\('SEAL', current\.localPath, String\(this\.sealNo\)\.trim\(\)\)/)
const saver = page.match(/async saveSealNo\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.doesNotMatch(saver, /if \(!this\.loadingStageReady\)/)
assert.match(page, /sealReadyToSave\(\)[\s\S]*?hasPendingLocalEvidence\('SEAL'\)/)
assert.match(page, /slot\.type === 'SEAL' \? '请填写封签号,保存后上传' : '照片待自动上传'/)
assert.match(page, /带锁码照上传成功,封签号已保存/)
})
test('cabinet evidence identity is task-scoped and never selects an inbound owner', () => {
const capture = page.match(/captureEvidenceIdentity\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
const same = page.match(/isSameEvidenceIdentity\(left, right\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(capture, /taskId: this\.remoteTaskId \|\| this\.currentBox\.taskId/)
assert.match(capture, /warehouseId: this\.currentBox\.warehouseId/)
assert.doesNotMatch(capture, /inboundId|loadingPlanId|containerBookingId/)
assert.match(same, /left\?\.taskId[\s\S]*?right\?\.taskId/)
assert.doesNotMatch(same, /inboundId|loadingPlanId|containerBookingId/)
assert.match(page, /String\(detailTaskId \|\| ''\) !== String\(requestedTaskId\)/)
assert.match(page, /!task\.loadingPlanId \|\| !task\.containerBookingId/)
assert.doesNotMatch(page, /const evidenceItem|String\(task\.inboundId\)/)
assert.match(page, /this\.evidenceIdentity = this\.captureEvidenceIdentity\(\)/)
})
test('merged or non-active cabinet details are rejected before claim and clear stale local state', () => {
const sync = page.match(/async syncRemoteTask\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(sync, /if \(this\.isMergedContainerRecord\(task\)\)[\s\S]*?this\.clearMergedTask\(task, previousIdentity\)[\s\S]*?return[\s\S]*?task\.recordStatus !== 'ACTIVE'[\s\S]*?claimContainerPickingTask/)
const clear = page.match(/clearMergedTask\(task, previousIdentity\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(clear, /taskId: previousIdentity\?\.taskId \?\? task\?\.taskId/)
assert.match(clear, /loadingPlanId: previousIdentity\?\.loadingPlanId \?\? task\?\.loadingPlanId/)
assert.match(clear, /containerBookingId: previousIdentity\?\.containerBookingId \?\? task\?\.containerBookingId/)
assert.match(clear, /clearPickingDraft\(staleIdentity\)/)
assert.match(clear, /this\.evidenceDrafts = Object\.fromEntries/)
assert.match(clear, /this\.evidencePreviewCache = Object\.fromEntries/)
for (const reset of ['remoteLoading = false', 'submitting = false', 'remoteTaskId = null', 'scanHistory = []', 'currentBox.containerBookingId = null', 'currentBox.currentContainerNo = null', 'resetEvidenceState()']) assert.match(clear, new RegExp(reset.replace(/[.()[\]]/g, '\\$&')))
})
test('photos upload, bind, reload and void before picking completion', () => {
assert.match(page, /requireEvidenceStage\(\)[\s\S]*?if \(!this\.pickingCompleted\)[\s\S]*?请先提交拣货完成[\s\S]*?if \(!this\.loadingStageReady\)/)
const queue = page.match(/async queueEvidenceUpload\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(queue, /await queue\.flush\(\)[\s\S]*?this\.applyEvidenceResponse\(latest\.result, type, path, identity\)/)
assert.doesNotMatch(queue, /if \(!this\.loadingStageReady\)/)
assert.match(page, /chooseEvidence\(slot\)[\s\S]*?this\.queueEvidenceUpload\(slot\.type, path\)/)
const retry = page.match(/async retryEvidence\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(retry, /if \(!this\.requireEvidenceCapture\(\)\) return/)
assert.match(retry, /await queue\.flush\(\)[\s\S]*?this\.applyEvidenceResponse\(latest\.result, type, item\.localPath, identity\)/)
assert.doesNotMatch(retry, /if \(!this\.loadingStageReady\)/)
assert.match(page, /async loadLoadingEvidence\(\) \{[\s\S]*?!this\.evidenceIdentity\?\.taskId[\s\S]*?!this\.evidenceIdentity\?\.warehouseId[\s\S]*?getEvidence\(identity\.taskId, identity\.warehouseId\)/)
assert.match(page, /await this\.loadLoadingEvidence\(\)[\s\S]*?if \(this\.loadingStageReady\) this\.activeStage = 'evidence'/)
assert.match(page, /async deleteEvidencePhoto\([^)]*\) \{[\s\S]*?if \(!this\.requireEvidenceMutation\(\)\) return/)
assert.match(page, /async completeLoadingTask\([^)]*\) \{[\s\S]*?if \(!this\.requireEvidenceStage\(\)\) return/)
assert.doesNotMatch(page, /照片已上传,拣货完成后自动保存/)
})
test('refresh preserves local photos and flushes them as soon as evidence identity is ready', () => {
assert.match(page, /this\.rememberEvidenceDraft\(\)/)
assert.match(page, /this\.restoreEvidenceDraft\(\)/)
assert.match(page, /async flushPendingEvidence\(\)[\s\S]*?queueEvidenceUpload/)
assert.match(page, /const localFiles = Object\.fromEntries/)
})
test('evidence photos render thumbnails and support full-screen preview', () => {
assert.match(page, /class="evidence-thumbnail" :src="evidencePreviewSource\(photo\)"/)
assert.match(page, /class="work-content" :class="\{ 'evidence-active': activeStage === 'evidence' \}"/)
assert.match(page, /class="evidence-photo-grid"/)
assert.match(page, /class="evidence-count"/)
assert.match(page, /@tap="previewEvidencePhoto\(photo, slot\.type\)"/)
assert.match(page, /evidencePreviewSource\(photo\)[\s\S]*?localPath[\s\S]*?fileUrl/)
assert.match(page, /previewEvidencePhoto\(photo, type\)[\s\S]*?uni\.previewImage\([\s\S]*?current[\s\S]*?urls/)
assert.match(page, /cacheEvidencePreview\(fileId, localPath/)
assert.match(page, /withCachedEvidencePreview\(photo[\s\S]*?evidencePreviewCache/)
assert.match(page, /item\.previewUrl \|\| getEvidencePreviewUrl\(identity\.taskId, item\.id, identity\.warehouseId\)/)
assert.match(page, /['"]list\.loaded['"]/)
assert.match(page, /loadEvidencePreviews\(contextId, identity\)[\s\S]*?downloadEvidencePreview\(item\.previewUrl\)/)
assert.match(page, /['"]preview\.downloaded['"]/)
assert.match(page, /已上传并保存/)
})
test('switching cabinets invalidates old sync and evidence upload state', () => {
const selector = page.match(/selectBox\(index\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(selector, /this\.taskRequestId \+= 1/)
assert.match(selector, /this\.pickingVersion = null/)
assert.match(selector, /this\.submitting = false/)
assert.match(selector, /this\.resetEvidenceState\(\)/)
assert.match(page, /!this\.isEvidenceContextActive\(contextId, identity\) \|\| queue !== this\.evidenceQueue/)
const loader = page.match(/async loadLoadingEvidence\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.ok((loader.match(/isEvidenceContextActive\(contextId, identity\)/g) || []).length >= 2)
})
test('camera callbacks cannot append evidence after switching cabinets', () => {
const chooser = page.match(/chooseEvidence\(slot\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(chooser, /const contextId = this\.evidenceContextId/)
assert.match(chooser, /const identity = \{ \.\.\.this\.evidenceIdentity \}/)
assert.equal((chooser.match(/isEvidenceContextActive\(contextId, identity\)/g) || []).length, 2)
assert.match(chooser, /isEvidenceContextActive\(contextId, identity\)[\s\S]*?this\.evidenceFiles =/)
})
test('returning from the camera does not refresh and invalidate the active evidence context', () => {
assert.match(page, /cameraPicking: false/)
assert.match(page, /onShow\(\) \{[\s\S]*?if \(this\.cameraPicking\)[\s\S]*?choose\.resume[\s\S]*?return[\s\S]*?this\.syncRemoteTask\(\)/)
const chooser = page.match(/chooseEvidence\(slot\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(chooser, /this\.cameraPicking = true/)
assert.match(chooser, /success: \(result\) => \{[\s\S]*?this\.cameraPicking = false/)
assert.match(chooser, /fail: \(error\) => \{[\s\S]*?this\.cameraPicking = false/)
})
test('returning from an image preview does not refresh the picking task', () => {
assert.match(page, /evidencePreviewOpening: false, evidencePreviewReturning: false/)
assert.match(page, /onShow\(\) \{[\s\S]*?if \(this\.evidencePreviewReturning\)[\s\S]*?preview\.resume[\s\S]*?return[\s\S]*?this\.syncRemoteTask\(\)/)
assert.match(page, /onHide\(\) \{[\s\S]*?if \(this\.evidencePreviewOpening\)[\s\S]*?this\.evidencePreviewReturning = true/)
const preview = page.match(/previewEvidencePhoto\(photo, type\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(preview, /this\.evidencePreviewOpening = true/)
assert.match(preview, /uni\.previewImage\(\{ current, urls, fail:/)
})
test('evidence save, void and loading completion ignore stale cabinet responses', () => {
for (const [method, awaitedCall] of [
['deleteEvidencePhoto', 'voidEvidence'],
['completeLoadingTask', 'completeContainerPickingTask']
]) {
const body = page.match(new RegExp(`async ${method}\\([^)]*\\) \\{[\\s\\S]*?\\n\\t\\t\\t\\},`))?.[0] || ''
assert.match(body, /const contextId = this\.evidenceContextId/, `${method} should capture the evidence context`)
assert.match(body, /const identity = \{ \.\.\.this\.evidenceIdentity \}/, `${method} should capture stable evidence identity`)
assert.match(body, new RegExp(`await ${awaitedCall}\\([\\s\\S]*?if \\(!this\\.isEvidenceContextActive\\(contextId, identity\\)\\) return`), `${method} should guard the success writeback`)
assert.ok((body.match(/isEvidenceContextActive\(contextId, identity\)/g) || []).length >= 2, `${method} should guard success and error writebacks`)
}
const complete = page.match(/async completeLoadingTask\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(complete, /finally \{[\s\S]*?isEvidenceContextActive\(contextId, identity\)[\s\S]*?this\.submitting = false/)
})
test('completed server tasks restore evidence stage without being claimed again', () => {
assert.match(page, /const remoteTask = normalizePickingTask\(task\)/)
assert.match(page, /remoteTask\.workflowStatus !== 'AWAITING_REVIEW' && this\.assignmentVersion === null/)
assert.match(page, /if \(remoteTask\.workflowStatus === 'UNASSIGNED'\) \{[\s\S]*?claimContainerPickingTask/)
assert.match(page, /else if \(!isPickingCompleteTask\(remoteTask\)\)/)
assert.match(page, /await this\.loadLoadingEvidence\(\)[\s\S]*?if \(this\.loadingStageReady\) this\.activeStage = 'evidence'/)
})
test('picking mutations stay closed until the canonical task has been claimed', () => {
assert.match(page, /pickingOperationReady\(\)[\s\S]*?workflowStatus === 'IN_PROGRESS'[\s\S]*?remoteTaskId[\s\S]*?loadingPlanId[\s\S]*?containerBookingId/)
assert.match(page, /requirePickingOperation\(\)[\s\S]*?任务尚未领取完成/)
for (const method of ['processScannedValue', 'pickItem', 'openSkip', 'openPartial', 'submitPicking']) {
const body = page.match(new RegExp(`${method}\\([^)]*\\) \\{[\\s\\S]*?\\n\\t\\t\\t\\},`))?.[0] || ''
assert.match(body, /if \(!this\.requirePickingOperation\(\)\) return/, `${method} should wait for claim`)
}
})
test('completion shows a Chinese server failure with a request correlation id', () => {
assert.match(page, /remoteFailureMessage\(error, fallback = '操作失败'\)/)
assert.match(page, /formatApiError\(error, fallback\)/)
assert.match(page, /import \{ containerReference, formatApiError, sanitizeUserMessage \}/)
assert.match(page, /requestIdFromServer: error\.requestId/)
assert.match(page, /this\.remoteFailureMessage\(error, '装柜完成失败'\)/)
})