diff --git a/AGENTS.md b/AGENTS.md index ccd0a8b..56bd78b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,7 @@ | AppID、平台图标与打包资源 | `src/manifest.json`、`static/app-icons/` | Android/iOS 尺寸映射由 `tests/app-icons.test.mjs` 锁定;独立 AppID、签名和发布通道不得复用国内 PDA | | 登录、刷新与会话 | `src/pages/login/login.vue`、`src/api/auth.js`、`src/stores/session.js` | 国内 Auth 签发 Token;API 根见 `src/config/runtime.js` | | 海外仓工作台与模块入口 | `src/pages/home/home.vue`、`src/pages/module/module.vue` | `src/data/overseas-workbench.js` 是模块、作业可用性和原型数据入口 | -| 柜号签收与入仓真实作业 | `src/pages/operation/operation.vue`、`src/api/inbound.js`、`src/api/receiving-adapter.js`、`src/api/overseas.js` | Backend `lastleg-service/inboundplan/pda/controller/ReceivingContainerController.java`、`ReceivingContainerService.java`;公网前缀 `/api/pda/v1/last-leg/receiving-containers/**` | +| 柜号签收、照片与入仓真实作业 | `src/pages/operation/operation.vue`、`src/api/inbound.js`、`src/api/receiving-adapter.js`、`src/api/overseas.js`、`src/stores/receiving.js` | 三个固定照片角色各一张,支持拍照/相册、鉴权预览及更换失败保留原图;Backend `lastleg-service/inboundplan/pda/controller/ReceivingContainerController.java`、`ReceivingContainerService.java`,公网前缀 `/api/pda/v1/last-leg/receiving-containers/**` | | 库内、托盘、换标与现场查询待接前端 | `src/data/overseas-workbench.js` 中对应 `available: false` 入口 | Backend 已有 `pda/stocktake/`、`pda/move/`、`pda/warehouse/`、`palletops/`、`relabel/pda/` 与 `overseas-pda-operations-v1.yaml`;本应用新增专用 API/状态恢复和测试前不得启用 | | 扫码设备与诊断 | `src/utils/scan-broadcast.js`、`src/pages/scanner-test/scanner-test.vue` | `src/components/ScanTargetInput.vue`、声音反馈工具;广播 Action/Extra 见本文件服务边界 | | 作业记录与人员 | `src/pages/history/history.vue`、`src/pages/profile/profile.vue` | 会话隔离、退出清理和后续真实作业历史契约 | @@ -47,6 +47,7 @@ - `src/api/inbound.js` 已接入待卸柜候选、柜详情/prepare、current 恢复、三类照片签收、入仓码下 SKU/箱唛解析、实物托盘解析、FBA bulk/OVERSEAS carton 收货及整柜完成;响应与 404/409 判定集中在 `src/api/receiving-adapter.js`,双 Token 刷新与海外 401 单次重放由 `src/api/overseas.js` 统一处理。 - 请求身份和资源范围只取 Token 与 current 会话;前端不得提交 `customerId`、`planNo`、`allocationId`、`warehouseId` 或 `locationId`。409 必须引导账号恢复当前柜,完成前不得切柜。 +- 签收照片固定为三个角色且每个角色只有一个当前文件;上传成功后使用服务端 `downloadPath` 在当前会话内鉴权读取。更换失败时保留原 `fileId` 和可提交状态,无原图的失败槽不得计入 `photosReady`;本地临时路径仅用于本次设备预览,不作为跨会话文件事实。 - 库内、换标与现场查询只有视觉原型依据,当前工作台入口保持禁用;新增真实作业必须建立 `/api/pda/**` 业务 API/DTO、`lastleg:pda:operate` 权限、Token 冻结仓库范围、错误态和契约测试,不得把平台、客户或内部接口直接暴露给 PDA。 - 已有海外入仓 PDA Controller 统一位于 Lastleg `/api/pda/v1/last-leg/inbound-plans/{planNo}/**`;浏览器或设备请求仍必须经 `gateway-overseas` 的显式 PDA 路由,不得直连 Lastleg,也不得改走国内 Gateway。 - 海外业务 Token、PDA 权限和可信身份最终由 Gateway 与服务端校验;前端不得用工作台 `available`、本地客户 ID 或仓库 ID 代替授权和资源归属校验。 diff --git a/src/api/auth.js b/src/api/auth.js index 02cf1c6..aea54d5 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -2,12 +2,17 @@ import { API_ROOT } from '../config/runtime.js' function authRequest(path, data) { return new Promise((resolve, reject) => { + const url = `${API_ROOT}${path}` + console.info('[Overseas PDA auth] request', { method: 'POST', url }) uni.request({ - url: `${API_ROOT}${path}`, + url, method: 'POST', data, success(response) { const body = response.data || {} + console.info('[Overseas PDA auth] response', { url, statusCode: response.statusCode, code: body.code, + 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 (String(body.code ?? response.statusCode) !== '200') { const error = new Error(body.message || '认证服务请求失败') error.code = body.code || response.statusCode @@ -19,6 +24,7 @@ function authRequest(path, data) { resolve(body.data) }, fail(error) { + console.error('[Overseas PDA auth] network failure', { url, errMsg: error.errMsg || 'unknown' }) const next = new Error(error.errMsg || '网络连接失败') next.network = true reject(next) diff --git a/src/api/inbound.js b/src/api/inbound.js index d0014f4..6758163 100644 --- a/src/api/inbound.js +++ b/src/api/inbound.js @@ -1,4 +1,4 @@ -import { overseasRequest, overseasUpload } from '@/api/overseas.js' +import { overseasDownload, overseasRequest, overseasUpload } from '@/api/overseas.js' import { normalizeReceivingCandidate, normalizeReceivingSession } from '@/api/receiving-adapter.js' const RECEIVING_BASE = '/pda/v1/last-leg/receiving-containers' @@ -8,6 +8,7 @@ export const listReceivingCandidates = async () => (await overseasRequest('GET', export const getReceivingContainer = async (containerNo) => normalizeReceivingSession(await overseasRequest('GET', `${RECEIVING_BASE}/${encodeURIComponent(containerNo)}`)) export const prepareReceivingContainer = async (containerNo, request) => normalizeReceivingSession(await overseasRequest('POST', `${RECEIVING_BASE}/${encodeURIComponent(containerNo)}/prepare`, request)) export const uploadCurrentUnloadPhoto = (filePath) => overseasUpload(current('/unload-photos/files'), filePath) +export const downloadCurrentUnloadPhoto = (downloadPath) => overseasDownload(downloadPath) export const signCurrentReceiving = async (request) => normalizeReceivingSession(await overseasRequest('POST', current('/sign'), request)) export const getCurrentReceiving = async () => normalizeReceivingSession(await overseasRequest('GET', current())) export const resolveCurrentCarton = (cartonMark) => overseasRequest('POST', current('/cartons/resolve'), { cartonMark }) diff --git a/src/api/overseas.js b/src/api/overseas.js index 41d4e4c..50c99e8 100644 --- a/src/api/overseas.js +++ b/src/api/overseas.js @@ -78,6 +78,25 @@ function rawUpload(path, filePath, name, token) { }) } +function overseasUrl(path) { + const value = String(path || '') + const normalized = /\/(?:api|overseas-api)$/.test(session.apiBase) + ? value.replace(/^\/api(?=\/)/, '') + : value + return `${session.apiBase}${normalized}` +} + +function rawDownload(path, token) { + return new Promise((resolve, reject) => { + uni.downloadFile({ + url: overseasUrl(path), + header: { Authorization: `Bearer ${token}` }, + success: resolve, + fail: (cause) => reject(Object.assign(new Error(cause.errMsg || '签收照片下载失败'), { network: true })) + }) + }) +} + export async function overseasUpload(path, filePath, name = 'file', retry = true) { const attemptedToken = session.token let response = await rawUpload(path, filePath, name, attemptedToken) @@ -92,4 +111,17 @@ export async function overseasUpload(path, filePath, name = 'file', retry = true return body.data } +export async function overseasDownload(path, retry = true) { + const attemptedToken = session.token + let response = await rawDownload(path, attemptedToken) + if (retry && unauthorized(response)) { + const replacementToken = await tokenAfterUnauthorized(attemptedToken) + response = await rawDownload(path, replacementToken) + } + if (response?.statusCode !== 200 || !response?.tempFilePath) { + throw errorFrom({ ...response, data: {} }, '签收照片下载失败') + } + return response.tempFilePath +} + export function resetOverseasRequestStateForTest() { refreshPromise = null } diff --git a/src/api/receiving-adapter.js b/src/api/receiving-adapter.js index ede9f27..c6874e9 100644 --- a/src/api/receiving-adapter.js +++ b/src/api/receiving-adapter.js @@ -30,7 +30,7 @@ export function normalizeReceivingSession(value) { : [] return { ...value, - photos: RECEIVING_PHOTO_ROLES.map((role) => photos.find((photo) => photo?.role === role)).filter(Boolean), + photos: RECEIVING_PHOTO_ROLES.map((role) => photos.find((photo) => photo?.role === role && photo?.fileId)).filter(Boolean), totalPackages: number(value.totalPackages), receivedPackages: number(value.receivedPackages), pendingPackages: number(value.pendingPackages), diff --git a/src/config/runtime.js b/src/config/runtime.js index cab2108..26cdef6 100644 --- a/src/config/runtime.js +++ b/src/config/runtime.js @@ -5,6 +5,8 @@ const development = Boolean(runtimeEnvironment.DEV) let selectedAuthApiRoot = configuredAuthApiRoot || '/api' let selectedOverseasApiRoot = configuredOverseasApiRoot || '/overseas-api' +const nativeRuntime = typeof plus !== 'undefined' + || (typeof location !== 'undefined' && location.protocol === 'file:') // H5 must stay same-origin and use Vite's CORS-safe proxy; App builds keep the absolute API roots above. // #ifdef H5 @@ -12,6 +14,13 @@ selectedAuthApiRoot = runtimeEnvironment.VITE_H5_AUTH_API_BASE_URL || '/api' selectedOverseasApiRoot = runtimeEnvironment.VITE_H5_OVERSEAS_API_BASE_URL || '/overseas-api' // #endif +// HBuilderX/App 基座 can expose a file:// location; never allow a relative API +// root there, even if a stale H5 bundle or compiler flag was reused. +if (nativeRuntime) { + selectedAuthApiRoot = configuredAuthApiRoot || selectedAuthApiRoot + selectedOverseasApiRoot = configuredOverseasApiRoot || selectedOverseasApiRoot +} + export const API_ROOT = String(selectedAuthApiRoot).replace(/\/$/, '') export const OVERSEAS_API_ROOT = String(selectedOverseasApiRoot).replace(/\/$/, '') export const DEV_LOGIN = Object.freeze({ diff --git a/src/data/overseas-workbench.js b/src/data/overseas-workbench.js index 5163a81..37ebbb7 100644 --- a/src/data/overseas-workbench.js +++ b/src/data/overseas-workbench.js @@ -1,21 +1,21 @@ export const MODULES = [ { key: 'inbound', name: '入仓作业', icon: '入', description: '单账号锁定一柜,完成柜号级入库闭环', operations: [ - { name: '拆柜收货', icon: '卸', iconType: 'download', description: '三类照片签收,FBA 按 SKU 箱数、海外仓逐箱收货', available: true, badge: '真实接口' } + { name: '拆柜收货', icon: '卸', iconType: 'download', description: '三类照片签收,FBA 按 SKU 箱数、海外仓逐箱收货', available: true } ] }, { key: 'inventory', name: '库内作业', icon: '库', description: '盘点、移库与托盘管理', operations: [ - { name: '库存盘点', icon: '盘', iconType: 'list', description: '按任务扫描库位和库存', available: false, badge: '待 PDA 接口' }, - { name: '商品移库', icon: '移', iconType: 'loop', description: '扫描来源与目标库位完成移库', available: false, badge: '待 PDA 接口' }, - { name: '托盘管理', icon: '托', iconType: 'grid', description: '组托、拆托及托盘状态核对', available: false, badge: '待 PDA 接口' } + { name: '库存盘点', icon: '盘', iconType: 'list', description: '按任务扫描库位和库存', available: false }, + { name: '商品移库', icon: '移', iconType: 'loop', description: '扫描来源与目标库位完成移库', available: false }, + { name: '托盘管理', icon: '托', iconType: 'grid', description: '组托、拆托及托盘状态核对', available: false } ] }, { key: 'relabel', name: '换标作业', icon: '换', description: '换标收货、执行与复核', operations: [ - { name: '换标收货', icon: '收', iconType: 'download', description: '扫描换标单并确认待处理库存', available: false, badge: '待 PDA 接口' }, - { name: '换标执行', icon: '换', iconType: 'compose', description: '扫描旧标和新标记录换标结果', available: false, badge: '待 PDA 接口' }, - { name: '换标复核', icon: '核', iconType: 'checkbox', description: '复核标签和商品关联关系', available: false, badge: '待 PDA 接口' } + { name: '换标收货', icon: '收', iconType: 'download', description: '扫描换标单并确认待处理库存', available: false }, + { name: '换标执行', icon: '换', iconType: 'compose', description: '扫描旧标和新标记录换标结果', available: false }, + { name: '换标复核', icon: '核', iconType: 'checkbox', description: '复核标签和商品关联关系', available: false } ] }, { key: 'query', name: '现场查询', icon: '查', description: '库存、库位与托盘查询', operations: [ - { name: '库存查询', icon: '存', iconType: 'search', description: '扫描商品或箱码查询可用库存', available: false, badge: '待 PDA 接口' }, - { name: '库位查询', icon: '位', iconType: 'location', description: '扫描库位查看容量和在库明细', available: false, badge: '待 PDA 接口' }, - { name: '托盘查询', icon: '托', iconType: 'flag', description: '扫描托盘码查看绑定库存', available: false, badge: '待 PDA 接口' } + { name: '库存查询', icon: '存', iconType: 'search', description: '扫描商品或箱码查询可用库存', available: false }, + { name: '库位查询', icon: '位', iconType: 'location', description: '扫描库位查看容量和在库明细', available: false }, + { name: '托盘查询', icon: '托', iconType: 'flag', description: '扫描托盘码查看绑定库存', available: false } ] } ] diff --git a/src/manifest.json b/src/manifest.json index dd1c697..5828a87 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -81,7 +81,11 @@ "vueVersion" : "3", "h5" : { "devServer" : { - "port" : 5176 + "proxy": { + + }, + "port" : 5176, + "https": false } } } diff --git a/src/pages/home/home.vue b/src/pages/home/home.vue index a4bddf1..1f4056d 100644 --- a/src/pages/home/home.vue +++ b/src/pages/home/home.vue @@ -8,8 +8,8 @@ diff --git a/src/pages/login/login.vue b/src/pages/login/login.vue index d42105e..ce38a78 100644 --- a/src/pages/login/login.vue +++ b/src/pages/login/login.vue @@ -34,6 +34,8 @@ signInWithTokens(tokens) uni.reLaunch({ url: '/pages/home/home' }) } catch (error) { + console.error('[Overseas PDA login] failed', { message: error.message, network: error.network === true, + code: error.code, requestId: error.requestId, traceId: error.traceId }) this.error = error.network ? '当前网络不可用,请联网后重试' : (error.message || '登录失败') } finally { this.submitting = false diff --git a/src/pages/module/module.vue b/src/pages/module/module.vue index d7034d7..240cd66 100644 --- a/src/pages/module/module.vue +++ b/src/pages/module/module.vue @@ -4,8 +4,8 @@ {{ currentModule.icon }}{{ currentModule.name }}{{ currentModule.description }} 业务功能 - - 只有拆柜收货已接海外 PDA 真实接口;其余入口在专用接口、权限与仓库范围契约完成前保持禁用。 + + 当前仅开放拆柜收货,其他作业暂未开放。 diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue index a271f09..6018261 100644 --- a/src/pages/operation/operation.vue +++ b/src/pages/operation/operation.vue @@ -3,10 +3,7 @@