From 5ad9757dd55bebc78c1de1070ae9d0af87030b7c Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Wed, 26 Aug 2026 17:23:42 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/api/overseas.js | 4 ++++
src/pages/operation/operation.vue | 8 +++----
src/utils/scan-broadcast.js | 35 ++++++++++++++++++++++++-------
tests/project-contract.test.mjs | 4 ++++
tests/scan-broadcast.test.mjs | 12 +++++++++++
5 files changed, 51 insertions(+), 12 deletions(-)
diff --git a/src/api/overseas.js b/src/api/overseas.js
index 50c99e8..8a6eb31 100644
--- a/src/api/overseas.js
+++ b/src/api/overseas.js
@@ -54,6 +54,7 @@ function rawRequest(method, path, data, token) {
}
export async function overseasRequest(method, path, data, retry = true) {
+ console.info('[Overseas PDA API] request', { method, path })
const attemptedToken = session.token
let response = await rawRequest(method, path, data, attemptedToken)
if (retry && unauthorized(response)) {
@@ -61,6 +62,9 @@ export async function overseasRequest(method, path, data, retry = true) {
response = await rawRequest(method, path, data, replacementToken)
}
const body = response?.data || {}
+ console.info('[Overseas PDA API] response', { method, path, statusCode: response?.statusCode, code: body?.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'] })
if (!successful(response, body)) throw errorFrom(response, '海外作业请求失败')
return body.data
}
diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue
index 665aa92..0bfc278 100644
--- a/src/pages/operation/operation.vue
+++ b/src/pages/operation/operation.vue
@@ -3,7 +3,7 @@
- {{ pageTitle }}{{ pageSubtitle }}
+ {{ pageTitle }}{{ pageSubtitle }}
正在恢复当前柜作业…
@@ -133,8 +133,8 @@
statusTone(status) { return status === 'RECEIVING' ? 'status-current' : status === 'COMPLETED' ? 'status-good' : 'status-wait' }, percent(received, total) { return total ? Math.min(100, Math.round(Number(received || 0) / Number(total) * 100)) : 0 },
destinationLabel(item) { return item?.destinationType === 'FBA' ? (item.fbaWarehouseCode || 'FBA') : (item?.destinationWarehouseName || item?.destinationWarehouseCode || '海外仓') },
photoStateLabel(photo) { if (photo?.status === 'uploading') return '正在上传…'; if (photo?.status === 'error') return photo.fileId ? '重拍失败,已保留原图' : '上传失败,请重试'; return photo?.fileId ? '点击左侧照片可查看大图' : '请拍摄现场照片' },
- startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleHardwareScan(value), onError: (error) => { this.error = error } }) }, stopScanner() { if (typeof this._stopScanner === 'function') this._stopScanner(); this._stopScanner = null },
- handleHardwareScan(value) { if (!this.inboundMode) return this.showToast('该功能缺少海外 PDA 专用接口,扫码未提交', 'warning'); if (this.phase === 'candidates') { this.containerInput = value; return this.openContainer(value) } if (this.phase !== 'receive') return; if (this.activeTarget === 'pallet' && this.activeInbound) { this.palletInput = value; return this.resolvePallet(value) } this.cartonInput = value; return this.resolveCarton(value) },
+ startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleHardwareScan(value), onError: (error) => { this.error = error; this.showToast('硬件扫码接收器异常,请检查扫描服务', 'warning') } }) }, stopScanner() { if (typeof this._stopScanner === 'function') this._stopScanner(); this._stopScanner = null },
+ handleHardwareScan(value) { console.info('[Overseas PDA scan] dispatch', { value, phase: this.phase, target: this.activeTarget, matchedInbound: this.activeInbound?.inboundCode }); if (!this.inboundMode) return this.showToast('该功能缺少海外 PDA 专用接口,扫码未提交', 'warning'); if (this.phase === 'candidates') { this.containerInput = value; return this.openContainer(value) } if (this.phase !== 'receive') return; if (this.activeTarget === 'pallet' && this.activeInbound) { this.palletInput = value; return this.resolvePallet(value) } this.cartonInput = value; return this.resolveCarton(value) },
async run(action, work) { this.busy = true; this.error = null; try { const result = await work(); this.showToast(`${action}成功`, 'success'); return result } catch (error) { this.error = error; playErrorSound(); this.showToast(error.message || `${action}失败`, 'error'); throw error } finally { this.busy = false } },
setScanTargetForCurrent(value) { receiving.activeScanTarget = value?.status === 'WAIT_SIGN' ? 'container' : 'carton' },
async restorePhotoPreviews() { await Promise.all(receiving.photoSlots.filter((photo) => photo?.downloadPath && !photo.localPath).map(async (photo) => { photo.status = 'restoring'; try { photo.localPath = await downloadCurrentUnloadPhoto(photo.downloadPath); photo.status = 'success' } catch (error) { photo.status = 'error'; photo.errorMessage = error.message || '照片加载失败'; this.error = error } })) },
@@ -150,7 +150,7 @@
previewPhoto(index) { const path = this.photoSlots[index]?.localPath; if (!path) return; uni.previewImage({ urls: [path], current: path }) },
async signContainer() { if (!this.photosReady) return; const photos = this.photoSlots.map((item, index) => ({ role: this.photoSteps[index].role, fileId: String(item.fileId) })); const key = idempotencyKey('sign', { containerNo: this.current.containerNo, photos }); try { const value = await this.run('签收当前柜', () => signCurrentReceiving({ photos, idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); setCurrentReceiving(value); this.clearReceiptContext() } catch (_) {} },
clearReceiptContext() { receiving.resolvedCarton = null; receiving.resolvedPallet = null; this.cartonInput = ''; this.palletInput = ''; this.packageCount = 1; receiving.activeScanTarget = 'carton' },
- async resolveCarton(raw) { const cartonMark = String(raw?.detail?.value ?? raw ?? this.cartonInput).trim(); if (!cartonMark || this.busy) return; try { const value = await this.run('解析箱码', () => resolveCurrentCarton(cartonMark)); receiving.resolvedCarton = value; receiving.resolvedPallet = null; this.cartonInput = value.cartonMark; this.palletInput = ''; this.packageCount = 1; receiving.activeScanTarget = 'pallet'; playSuccessSound() } catch (_) {} },
+ async resolveCarton(raw) { const cartonMark = String(raw?.detail?.value ?? raw ?? this.cartonInput).trim(); if (!cartonMark) return; if (this.busy) { console.warn('[Overseas PDA carton] ignored while busy', { cartonMark }); return this.showToast('正在处理上一笔扫描,请稍后重试', 'warning') } console.info('[Overseas PDA carton] resolving', { cartonMark, containerNo: this.current?.containerNo }); try { const value = await this.run('解析箱码', () => resolveCurrentCarton(cartonMark)); console.info('[Overseas PDA carton] resolved', { cartonMark: value.cartonMark, inboundCode: value.inboundCode, destinationType: value.destinationType, pendingPackages: value.pendingPackages }); receiving.resolvedCarton = value; receiving.resolvedPallet = null; this.cartonInput = value.cartonMark; this.palletInput = ''; this.packageCount = 1; receiving.activeScanTarget = 'pallet'; playSuccessSound() } catch (error) { console.error('[Overseas PDA carton] failed', { cartonMark, message: error.message, code: error.code, requestId: error.requestId, traceId: error.traceId }) } },
async resolvePallet(raw) { const palletCode = String(raw?.detail?.value ?? raw ?? this.palletInput).trim(); if (!palletCode || !this.activeInbound || this.busy) return; try { const value = await this.run('校验托盘', () => resolveCurrentPallet(palletCode, this.activeInbound.inboundCode)); receiving.resolvedPallet = value; this.palletInput = value.palletCode; if (!value.compatible) throw Object.assign(new Error('托盘不匹配'), { code: 'PALLET_INCOMPATIBLE' }); playSuccessSound() } catch (error) { if (!this.error) this.error = error } },
async submitReceipt() { if (!this.canSubmitReceipt) return; const isFba = this.activeInbound.destinationType === 'FBA'; const payload = isFba ? { inboundCode: this.activeInbound.inboundCode, palletCode: this.resolvedPallet.palletCode, packageCount: Number(this.packageCount) } : { cartonMark: this.activeInbound.cartonMark, palletCode: this.resolvedPallet.palletCode }; const key = idempotencyKey(isFba ? 'bulk-receipt' : 'carton-receipt', payload); try { const result = await this.run('提交实收', () => isFba ? receiveCurrentBulk({ ...payload, idempotencyKey: key.value }) : receiveCurrentCarton({ ...payload, idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); receiving.lastReceipt = result; setCurrentReceiving(await getCurrentReceiving()); this.clearReceiptContext(); playSuccessSound() } catch (_) {} },
async completeContainer() { if (!this.allInboundsReconciled) return; const key = idempotencyKey('complete', { containerNo: this.current.containerNo }); try { const value = await this.run('完成入库', () => completeCurrentReceiving({ idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); setCurrentReceiving(value) } catch (_) {} },
diff --git a/src/utils/scan-broadcast.js b/src/utils/scan-broadcast.js
index 308fbda..2cc7b56 100644
--- a/src/utils/scan-broadcast.js
+++ b/src/utils/scan-broadcast.js
@@ -34,25 +34,44 @@ export function startScanBroadcast({ onScan, onError } = {}) {
const filter = new IntentFilter()
filter.addAction(SCAN_BROADCAST_ACTION)
receiver = plus.android.implements('io.dcloud.feature.internal.reflect.BroadcastReceiver', {
- onReceive(context, intent) {
- try {
- plus.android.importClass(intent)
- const value = normalizeScanValue(intent.getStringExtra(SCAN_BROADCAST_EXTRA))
- if (!value || isDuplicateScan(value)) return
- onScan(value)
- } catch (error) {
- if (typeof onError === 'function') onError(error)
+ onReceive(context, intent) {
+ try {
+ plus.android.importClass(intent)
+ const rawValue = intent.getStringExtra(SCAN_BROADCAST_EXTRA)
+ const value = normalizeScanValue(rawValue)
+ console.info('[Overseas PDA broadcast] received', { action: intent.getAction(), extra: SCAN_BROADCAST_EXTRA, rawValue, value })
+ if (!value) { console.warn('[Overseas PDA broadcast] ignored empty value'); return }
+ if (isDuplicateScan(value)) { console.info('[Overseas PDA broadcast] ignored duplicate', { value }); return }
+ onScan(value)
+ } catch (error) {
+ console.error('[Overseas PDA broadcast] receive failed', { message: error.message })
+ if (typeof onError === 'function') onError(error)
}
}
})
const Build = plus.android.importClass('android.os.Build')
+ const sdkInt = Number(Build.VERSION.SDK_INT)
if (Build.VERSION.SDK_INT >= 33) {
const Context = plus.android.importClass('android.content.Context')
activity.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED)
} else {
activity.registerReceiver(receiver, filter)
}
+ let packageName = ''
+ try { packageName = String(activity.getPackageName()) } catch (_) {}
+ console.info('[Overseas PDA broadcast] receiver registered', {
+ action: SCAN_BROADCAST_ACTION,
+ extra: SCAN_BROADCAST_EXTRA,
+ packageName,
+ sdkInt,
+ exported: sdkInt >= 33
+ })
} catch (error) {
+ console.error('[Overseas PDA broadcast] registration failed', {
+ action: SCAN_BROADCAST_ACTION,
+ extra: SCAN_BROADCAST_EXTRA,
+ message: error?.message || String(error)
+ })
receiver = null
activity = null
if (typeof onError === 'function') onError(error)
diff --git a/tests/project-contract.test.mjs b/tests/project-contract.test.mjs
index a78a7f5..18f0686 100644
--- a/tests/project-contract.test.mjs
+++ b/tests/project-contract.test.mjs
@@ -54,6 +54,9 @@ test('login has no committed demo credentials and inbound operation implements t
assert.match(operation, /const payload = isFba \? \{ inboundCode: this\.activeInbound\.inboundCode, palletCode: this\.resolvedPallet\.palletCode, packageCount: Number\(this\.packageCount\) \}/)
assert.doesNotMatch(operation, /skuCode/)
assert.match(operation, /clearReceiptContext\(\)[\s\S]*activeScanTarget = 'carton'/)
+ assert.match(operation, /\[Overseas PDA scan\] dispatch/)
+ assert.match(operation, /\[Overseas PDA carton\] resolved/)
+ assert.match(operation, /\[Overseas PDA carton\] failed[\s\S]*requestId[\s\S]*traceId/)
for (const role of ['LICENSE_PLATE', 'CONTAINER_SEAL', 'DOOR_OPEN']) assert.match(adapter, new RegExp(role))
for (const forbidden of ['customerId', 'planNo', 'allocationId', 'warehouseId', 'locationId']) assert.doesNotMatch(inbound, new RegExp(forbidden))
assert.match(operation, /isReceivingConflict|recoverCurrentAfterConflict/)
@@ -84,6 +87,7 @@ test('receiving phases use a compiled container summary with cabinet and aggrega
assert.match(summary, /padding:10px 12px/)
assert.match(summary, /destination-tags\{[^}]*flex-wrap:wrap/)
assert.doesNotMatch(summary, /destinationSummary|text-overflow:ellipsis/)
+ assert.match(operation, /v-if="phase !== 'receive'" class="receiving-heading"/)
})
test('Chinese English and Spanish locale scaffolding is installed without exposing unfinished translations', async () => {
diff --git a/tests/scan-broadcast.test.mjs b/tests/scan-broadcast.test.mjs
index c0f3ae6..827dde2 100644
--- a/tests/scan-broadcast.test.mjs
+++ b/tests/scan-broadcast.test.mjs
@@ -1,5 +1,6 @@
import test from 'node:test'
import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
import {
SCAN_BROADCAST_ACTION,
@@ -16,6 +17,17 @@ test('scanner uses the confirmed PDA broadcast contract', () => {
assert.equal(normalizeScanValue(null), '')
})
+test('scanner source keeps receiver diagnostics before page routing', async () => {
+ const source = await readFile(new URL('../src/utils/scan-broadcast.js', import.meta.url), 'utf8')
+ assert.match(source, /\[Overseas PDA broadcast\] received/)
+ assert.match(source, /rawValue[\s\S]*value/)
+ assert.match(source, /ignored duplicate/)
+ assert.match(source, /receive failed/)
+ assert.match(source, /receiver registered/)
+ assert.match(source, /registration failed/)
+ assert.match(source, /activity\.getPackageName\(\)/)
+})
+
test('scanner drops only immediate duplicate broadcasts', () => {
resetScanDeduplication()
assert.equal(isDuplicateScan('EL-38201', 1000), false)