This commit is contained in:
2026-08-15 11:36:56 +08:00
parent 00aafca2e5
commit a9b5559148
4 changed files with 49 additions and 17 deletions
+4 -9
View File
@@ -31,7 +31,6 @@ function request(options) {
if (session.requestId) headers['X-Request-Id'] = session.requestId
return new Promise((resolve, reject) => {
debugLog('api.request', { method: options.method || 'GET', url: options.url })
uni.request({
...options,
url: `${baseUrl}${options.url}`,
@@ -39,12 +38,7 @@ function request(options) {
success(response) {
const body = response.data || {}
const code = String(body.code ?? response.statusCode)
debugLog('api.response', {
method: options.method || 'GET', url: options.url, statusCode: response.statusCode, code,
message: body.message || '', requestId: body.requestId || response.header?.['X-Request-Id'] || response.header?.['x-request-id'] || '',
traceId: body.traceId || response.header?.['X-Trace-Id'] || response.header?.['x-trace-id'] || '',
dataKeys: body.data && typeof body.data === 'object' ? Object.keys(body.data) : []
})
if (code !== '200') {
const error = new Error(body.message || '服务端请求失败')
error.code = body.code || response.statusCode
@@ -57,7 +51,6 @@ function request(options) {
resolve(body.data)
},
fail(error) {
debugLog('api.network-error', { method: options.method || 'GET', url: options.url, errMsg: error.errMsg || '' })
const next = new Error(error.errMsg || '网络连接失败')
next.network = true
reject(next)
@@ -289,6 +282,7 @@ export function uploadEvidenceFile(inboundId, evidenceType, localPath, tenantId,
const headers = {}
if (session.token) headers.Authorization = `Bearer ${session.token}`
if (session.requestId) headers['X-Request-Id'] = session.requestId
debugLog('evidence.upload.start', { inboundId, evidenceType, tenantId: tenant, hasLocalPath: Boolean(localPath) })
return new Promise((resolve, reject) => {
const task = uni.uploadFile({
url: `${baseUrl}/files/loading-evidence/${encodeURIComponent(tenant)}/${encodeURIComponent(inboundId)}?evidenceType=${encodeURIComponent(evidenceType)}`,
@@ -299,10 +293,11 @@ export function uploadEvidenceFile(inboundId, evidenceType, localPath, tenantId,
let body = response.data
try { body = typeof body === 'string' ? JSON.parse(body) : body } catch (_) { /* handled below */ }
const code = String(body?.code ?? response.statusCode)
debugLog('evidence.upload.response', { inboundId, evidenceType, statusCode: response.statusCode, code, message: body?.message || '', requestId: body?.requestId || response.header?.['X-Request-Id'] || response.header?.['x-request-id'] || '', traceId: body?.traceId || response.header?.['X-Trace-Id'] || response.header?.['x-trace-id'] || '', fileId: body?.data?.fileId || null })
if (code !== '200') { const error = new Error(body?.message || '文件上传失败'); error.code = body?.code || response.statusCode; reject(error); return }
resolve(body?.data || body)
},
fail(error) { const next = new Error(error.errMsg || '网络连接失败'); next.network = true; reject(next) }
fail(error) { debugLog('evidence.upload.error', { inboundId, evidenceType, message: error.errMsg || '' }); const next = new Error(error.errMsg || '网络连接失败'); next.network = true; reject(next) }
})
if (task && typeof task.onProgressUpdate === 'function' && typeof onProgress === 'function') task.onProgressUpdate(({ progress }) => onProgress(progress))
})
+30 -7
View File
@@ -375,7 +375,7 @@
selectedIncompleteReason: '',
completionSummary: { picked: 0, partial: 0, skipped: 0, autoSkipped: 0 },
evidenceSlots: EVIDENCE_SLOTS,
evidenceFiles: {}, evidenceQueueItems: [], evidenceQueue: null, evidenceContextId: 0, evidenceIdentity: null, sealNo: '', loadingVersion: null, pickingVersion: null, remotePick: null,
evidenceFiles: {}, evidenceQueueItems: [], evidenceQueue: null, evidenceContextId: 0, evidenceIdentity: null, cameraPicking: false, sealNo: '', loadingVersion: null, pickingVersion: null, remotePick: null,
remoteTaskId: null, assignmentVersion: null, submitId: '',
taskRequestId: 0,
remoteError: '', remoteLoading: false, submitting: false,
@@ -500,6 +500,10 @@
onShow() {
if (!requireSession()) return
this.startHardwareScanner()
if (this.cameraPicking) {
this.debugEvidence('choose.resume', { evidenceIdentity: this.evidenceIdentity })
return
}
this.syncRemoteTask()
},
onHide() { this.stopHardwareScanner() },
@@ -512,6 +516,9 @@
return false
},
methods: {
debugEvidence(event, payload = {}) {
try { console.info(`[PDA] evidence.${event}`, payload) } catch (_) { /* console may be unavailable on a device */ }
},
debugSubmit(event, payload = {}) {
try {
console.info('[PDA] picking.submit', {
@@ -743,6 +750,10 @@
this.pickScannedBox(result.item, value)
},
evidenceEntries(type) { return Array.isArray(this.evidenceFiles[type]) ? this.evidenceFiles[type].filter((item) => item.status !== 'VOID' && item.status !== 'VOIDED') : (this.evidenceFiles[type] ? [this.evidenceFiles[type]] : []) },
evidenceItems(result) {
if (Array.isArray(result?.evidences)) return result.evidences
return Array.isArray(result?.evidences?.evidences) ? result.evidences.evidences : []
},
validEvidenceEntries(type) { return this.evidenceEntries(type).filter((item) => item.id && item.fileId && item.uploadStatus !== 'PENDING') },
evidenceEntry(type) { const list = this.evidenceEntries(type); return list.length ? list[list.length - 1] : null },
evidenceQueueEntry(type) {
@@ -869,20 +880,28 @@
return this.evidenceEntry(type) ? '继续拍照' : '拍照'
},
chooseEvidence(slot) {
if (!this.requireEvidenceStage()) return
if (!isOnline()) { this.showToast('当前网络不可用,请联网后再拍照上传', 'warning'); return }
if (typeof uni.chooseImage !== 'function') { this.showToast('当前环境不支持选择照片', 'warning'); return }
const evidenceType = slot?.type || null
this.debugEvidence('choose.start', { evidenceType, evidenceIdentity: this.evidenceIdentity, loadingStageReady: this.loadingStageReady, hasChooseImage: typeof uni.chooseImage === 'function' })
if (!this.requireEvidenceStage()) { this.debugEvidence('choose.blocked', { evidenceType, reason: 'stage-not-ready', evidenceIdentity: this.evidenceIdentity, loadingVersion: this.loadingVersion }); return }
if (!isOnline()) { this.debugEvidence('choose.blocked', { evidenceType, reason: 'offline' }); this.showToast('当前网络不可用,请联网后再拍照上传', 'warning'); return }
if (typeof uni.chooseImage !== 'function') { this.debugEvidence('choose.blocked', { evidenceType, reason: 'chooseImage-unavailable' }); this.showToast('当前环境不支持选择照片', 'warning'); return }
const contextId = this.evidenceContextId
const identity = { ...this.evidenceIdentity }
this.cameraPicking = true
this.debugEvidence('choose.open-camera', { evidenceType, contextId, evidenceIdentity: identity })
uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['camera'], success: (result) => {
this.cameraPicking = false
if (!this.isEvidenceContextActive(contextId, identity)) return
const path = result.tempFilePaths && result.tempFilePaths[0]
if (!path) return
if (!path) { this.debugEvidence('camera.empty', { evidenceType: slot.type, evidenceIdentity: identity }); return }
this.debugEvidence('camera.selected', { evidenceType: slot.type, evidenceIdentity: identity })
const next = { evidenceType: slot.type, fileName: path.split('/').pop() || path, localPath: path, uploadStatus: 'PENDING' }
this.evidenceFiles = { ...this.evidenceFiles, [slot.type]: [...(this.evidenceFiles[slot.type] || []), next] }
this.queueEvidenceUpload(slot.type, path)
}, fail: () => {
}, fail: (error) => {
this.cameraPicking = false
if (!this.isEvidenceContextActive(contextId, identity)) return
this.debugEvidence('camera.error', { evidenceType: slot.type, evidenceIdentity: identity, message: error?.errMsg || '' })
this.showToast('未选择照片,可稍后重试', 'warning')
} })
},
@@ -893,9 +912,11 @@
const identity = { ...this.evidenceIdentity }
const queue = this.evidenceQueue
const entry = queue.add({ ...identity, tenantId: this.currentBox.tenantId, evidenceType: type, localPath: path })
this.debugSubmit('evidence.queue.start', { evidenceType: type, queueId: entry.id, evidenceIdentity: identity })
await queue.flush()
if (!this.isEvidenceContextActive(contextId, identity) || queue !== this.evidenceQueue) return
const latest = queue.snapshot().find((item) => item.id === entry.id)
this.debugSubmit('evidence.queue.result', { evidenceType: type, queueId: entry.id, status: latest?.status || 'missing', fileId: latest?.result?.fileId || null, error: latest?.error || '' })
if (latest?.status !== 'SUCCEEDED') { this.remoteError = latest?.error || '文件上传失败'; return }
const current = this.evidenceEntry(type) || {}
this.evidenceFiles = { ...this.evidenceFiles, [type]: [...(this.evidenceFiles[type] || []).slice(0, -1), { ...current, fileId: latest.result?.fileId, fileName: latest.result?.fileName || current.fileName, uploadStatus: 'SUCCEEDED' }] }
@@ -928,10 +949,12 @@
const current = this.evidenceEntry(type) || {}
if (!current.fileId) { this.showToast('照片尚未上传完成', 'warning'); return }
const payload = { expectedVersion: current.version || 0, evidenceType: type, fileId: current.fileId || null, fileName: current.fileName || null, sealNo: type === 'SEAL' ? this.sealNo : null, remark: null, operationId: operationId || `evidence-${identity.taskId || identity.inboundId}-${type}-${current.fileId || 'seal'}` }
this.debugSubmit('evidence.bind.start', { evidenceType: type, fileId: current.fileId, expectedVersion: payload.expectedVersion, evidenceIdentity: identity })
try {
const result = await upsertEvidence(inboundId, payload, tenantId)
if (!this.isEvidenceContextActive(contextId, identity)) return
this.evidenceFiles = (result && result.evidences ? result.evidences : []).reduce((all, item) => { const list = (all[item.evidenceType] || []).filter((entry) => entry.id || entry.fileId !== item.fileId); const index = list.findIndex((entry) => entry.id === item.id); all[item.evidenceType] = index >= 0 ? list.map((entry, i) => i === index ? item : entry) : [...list, item]; return all }, this.evidenceFiles)
this.debugSubmit('evidence.bind.success', { evidenceType: type, fileId: current.fileId, evidenceCount: result?.evidences?.length || 0, evidenceIdentity: identity })
this.evidenceFiles = this.evidenceItems(result).reduce((all, item) => { const list = (all[item.evidenceType] || []).filter((entry) => entry.id || entry.fileId !== item.fileId); const index = list.findIndex((entry) => entry.id === item.id); all[item.evidenceType] = index >= 0 ? list.map((entry, i) => i === index ? item : entry) : [...list, item]; return all }, this.evidenceFiles)
this.showToast(`${type === 'SEAL' ? '封签' : '照片'}已保存`, 'success')
} catch (error) {
if (!this.isEvidenceContextActive(contextId, identity)) return
+5
View File
@@ -61,6 +61,11 @@ test('picking page exposes five evidence slots, retry and remote completion stat
assert.match(page, /evidenceComplete/)
})
test('PDA evidence binding unwraps the nested loading action response before rendering saved files', () => {
assert.match(page, /evidenceItems\(result\)[\s\S]*?Array\.isArray\(result\?\.evidences\?\.evidences\)[\s\S]*?result\.evidences\.evidences/)
assert.match(page, /this\.evidenceFiles = this\.evidenceItems\(result\)\.reduce/)
})
test('PDA cabinet picking adapter claims online and submits one atomic batch', () => {
assert.match(source, /\/first-leg\/pda-loading\/tasks\/pending/)
assert.match(source, /\/first-leg\/pda-loading\/tasks\?/)
+10 -1
View File
@@ -45,7 +45,7 @@ test('evidence mutations defensively reject calls before picking completion', ()
assert.match(page, /requireEvidenceStage\(\)[\s\S]*?if \(!this\.pickingCompleted\)[\s\S]*?请先提交拣货完成[\s\S]*?if \(!this\.loadingStageReady\)/)
for (const method of ['chooseEvidence', 'queueEvidenceUpload', 'retryEvidence', 'saveEvidence', 'saveSealNo', 'voidEvidencePhoto', 'completeLoadingTask']) {
const body = page.match(new RegExp(`(?:async )?${method}\\([^)]*\\) \\{[\\s\\S]*?\\n\\t\\t\\t\\},`))?.[0] || ''
assert.match(body, /if \(!this\.requireEvidenceStage\(\)\) return/, `${method} should guard the evidence stage`)
assert.match(body, /if \(!this\.requireEvidenceStage\(\)\)(?: return| \{)/, `${method} should guard the evidence stage`)
}
})
@@ -69,6 +69,15 @@ test('camera callbacks cannot append evidence after switching cabinets', () => {
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('evidence save, void and loading completion ignore stale cabinet responses', () => {
for (const [method, awaitedCall] of [
['saveEvidence', 'upsertEvidence'],