From bc986f1eef251b2ab5834fe36961143d0d55974f Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 24 Aug 2026 19:29:41 +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 --- AGENTS.md | 5 ++-- README.md | 5 ++-- src/api/inbound.js | 13 ++++----- src/api/receiving-adapter.js | 45 +++++++++++++++++++++++++++++++ src/data/overseas-workbench.js | 4 +-- src/pages.json | 2 +- src/pages/operation/operation.vue | 18 ++++++++----- src/stores/receiving.js | 12 ++++----- tests/project-contract.test.mjs | 13 +++++---- tests/receiving-adapter.test.mjs | 22 +++++++++++++++ 10 files changed, 107 insertions(+), 32 deletions(-) create mode 100644 src/api/receiving-adapter.js create mode 100644 tests/receiving-adapter.test.mjs diff --git a/AGENTS.md b/AGENTS.md index 16c82e5..398772c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ - 工作台定义:`src/data/overseas-workbench.js`。 - 登录与会话:`src/api/auth.js`、`src/stores/session.js`。 - 扫码设备层:`src/utils/scan-broadcast.js`;广播 Action=`android.intent.action.SCANRESULT`,Extra=`value`。 -- 当前通用作业页中的入仓作业已接真实海外 API;其他库内、换标与查询模块仍是静态原型,不得将原型提示改成模拟远程成功。 +- 当前通用作业页中的入仓作业已接柜号级真实海外 API:账号锁柜、三类照片签收、FBA 按 SKU 与箱数收货、OVERSEAS 逐箱收货并逐入仓码核平;其他库内、换标与查询模块仍是静态原型,不得将原型提示改成模拟远程成功。 ## 页面与能力快速索引 @@ -41,7 +41,8 @@ ## 海外业务接口边界 -- `src/api/inbound.js` 已接入入仓任务、推送、托盘分配、清点、拣货、箱码扫描/移除、卸柜照片和完成入库;双 Token 刷新与海外 401 单次重放由 `src/api/overseas.js` 统一处理。 +- `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 必须引导账号恢复当前柜,完成前不得切柜。 - 库内、换标与现场查询仍是静态原型;新增真实作业必须建立明确的业务 API/DTO、错误态和契约测试,不得把原型任务改成模拟成功。 - 已有海外入仓 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/README.md b/README.md index 9a59baf..84c13be 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 独立的 uni-app Vue 3 工程,面向海外仓现场人员。工程不依赖国内 PDA 源码或仓库外共享目录,可独立安装依赖、运行、编译和发布。 -首版工作台按四期边界提供真实入仓作业,以及库内作业、换标作业和现场查询原型。登录使用国内 Auth 统一签发的双 Token;海外入仓请求统一经 `gateway-overseas`,不得请求国内 Firstleg/Warehouse 业务接口。 +首版工作台按四期边界提供柜号级真实入仓作业,以及库内作业、换标作业和现场查询原型。登录使用国内 Auth 统一签发的双 Token;海外入仓请求统一经 `gateway-overseas`,不得请求国内 Firstleg/Warehouse 业务接口。 ## 本地运行与检查 @@ -37,6 +37,7 @@ npm run build:app - 已复用并内置:登录壳、PDA 样式、扫码广播、声音反馈、导航、网络检测、图标及 `uni_modules`。 - 已隔离:国内集装箱拣货、装柜证据、国内 Firstleg API、国内任务状态和国内本地草稿。 -- 入仓页面已接任务、推送、托盘分配、清点、拣货、箱码扫描/移除、卸柜照片和完成入库的真实 API;错误会保留业务码、requestId 与 traceId。 +- 入仓页面已接待卸柜候选、柜详情与账号锁柜、三类角色照片签收、current 恢复、SKU/箱唛与既有物理托盘解析、FBA 按 SKU 和箱数收货、OVERSEAS 逐箱收货、逐入仓码核平及整柜完成的真实 API;409 会引导返回当前柜,错误会保留业务码、requestId 与 traceId。 +- 入仓请求不手填客户、计划、分配、仓库或库位标识;身份与当前柜范围由 Token、Gateway 和服务端会话确定。页面同时支持 PDA 广播扫码与 H5 人工输入。 - 库内、换标和现场查询仍是可运行的静态原型,不会模拟海外服务提交成功。 - 本地契约测试与 H5/App 资源编译不等于真实开发环境 HTTP 或 PDA 真机验收。 diff --git a/src/api/inbound.js b/src/api/inbound.js index e82bbc4..d0014f4 100644 --- a/src/api/inbound.js +++ b/src/api/inbound.js @@ -1,17 +1,18 @@ import { overseasRequest, overseasUpload } from '@/api/overseas.js' +import { normalizeReceivingCandidate, normalizeReceivingSession } from '@/api/receiving-adapter.js' const RECEIVING_BASE = '/pda/v1/last-leg/receiving-containers' const current = (suffix = '') => `${RECEIVING_BASE}/current${suffix}` -export const listReceivingCandidates = () => overseasRequest('GET', `${RECEIVING_BASE}/candidates`) -export const getReceivingContainer = (containerNo) => overseasRequest('GET', `${RECEIVING_BASE}/${encodeURIComponent(containerNo)}`) -export const prepareReceivingContainer = (containerNo, request) => overseasRequest('POST', `${RECEIVING_BASE}/${encodeURIComponent(containerNo)}/prepare`, request) +export const listReceivingCandidates = async () => (await overseasRequest('GET', `${RECEIVING_BASE}/candidates`)).map(normalizeReceivingCandidate) +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 signCurrentReceiving = (request) => overseasRequest('POST', current('/sign'), request) -export const getCurrentReceiving = () => overseasRequest('GET', current()) +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 }) export const resolveCurrentSku = (skuCode) => overseasRequest('POST', current('/skus/resolve'), { skuCode }) export const resolveCurrentPallet = (palletCode, inboundCode) => overseasRequest('POST', current('/pallets/resolve'), { palletCode, inboundCode }) export const receiveCurrentBulk = (request) => overseasRequest('POST', current('/receipts/bulk'), request) export const receiveCurrentCarton = (request) => overseasRequest('POST', current('/receipts/carton'), request) -export const completeCurrentReceiving = (request) => overseasRequest('POST', current('/complete'), request) +export const completeCurrentReceiving = async (request) => normalizeReceivingSession(await overseasRequest('POST', current('/complete'), request)) diff --git a/src/api/receiving-adapter.js b/src/api/receiving-adapter.js new file mode 100644 index 0000000..91720bb --- /dev/null +++ b/src/api/receiving-adapter.js @@ -0,0 +1,45 @@ +export const RECEIVING_PHOTO_ROLES = ['LICENSE_PLATE', 'CONTAINER_SEAL', 'DOOR_OPEN'] + +const number = (value) => Math.max(0, Number(value || 0)) + +export function normalizeReceivingInbound(value = {}) { + return { + ...value, + plannedPalletCount: number(value.plannedPalletCount), + totalPackages: number(value.totalPackages), + receivedPackages: number(value.receivedPackages), + pendingPackages: number(value.pendingPackages) + } +} + +export function normalizeReceivingCandidate(value = {}) { + return { + ...value, + destinationCodes: Array.isArray(value.destinationCodes) ? value.destinationCodes.filter(Boolean) : [], + plannedPalletCount: number(value.plannedPalletCount), + totalPackages: number(value.totalPackages), + receivedPackages: number(value.receivedPackages), + currentAccount: value.currentAccount === true + } +} + +export function normalizeReceivingSession(value) { + if (!value) return null + const photos = Array.isArray(value.photos) ? value.photos : [] + return { + ...value, + photos: RECEIVING_PHOTO_ROLES.map((role) => photos.find((photo) => photo?.role === role)).filter(Boolean), + totalPackages: number(value.totalPackages), + receivedPackages: number(value.receivedPackages), + pendingPackages: number(value.pendingPackages), + inbounds: Array.isArray(value.inbounds) ? value.inbounds.map(normalizeReceivingInbound) : [] + } +} + +export function isNoCurrentReceiving(error) { + return ['404', 'A0404', 'B0404'].includes(String(error?.code || '')) +} + +export function isReceivingConflict(error) { + return ['409', 'A0409', 'B0409'].includes(String(error?.code || '')) +} diff --git a/src/data/overseas-workbench.js b/src/data/overseas-workbench.js index 675cc23..d488797 100644 --- a/src/data/overseas-workbench.js +++ b/src/data/overseas-workbench.js @@ -1,6 +1,6 @@ export const MODULES = [ - { key: 'inbound', name: '入仓作业', icon: '入', description: '柜号签收、卸柜收货与入库', operations: [ - { name: '拆柜收货', icon: '卸', iconType: 'download', description: '待卸柜拍照签收,按目的类型扫码收货', available: true, badge: '真实接口' } + { key: 'inbound', name: '入仓作业', icon: '入', description: '单账号锁定一柜,完成柜号级入库闭环', operations: [ + { name: '拆柜收货', icon: '卸', iconType: 'download', description: '三类照片签收,FBA 按 SKU 箱数、海外仓逐箱收货', available: true, badge: '真实接口' } ] }, { key: 'inventory', name: '库内作业', icon: '库', description: '盘点、移库与托盘管理', operations: [ { name: '库存盘点', icon: '盘', iconType: 'list', description: '按任务扫描库位和库存', available: true, badge: '原型' }, diff --git a/src/pages.json b/src/pages.json index f9e33e7..5ce934d 100644 --- a/src/pages.json +++ b/src/pages.json @@ -3,7 +3,7 @@ { "path": "pages/login/login", "style": { "navigationStyle": "custom", "navigationBarTitleText": "柏悦物流海外版", "backgroundColor": "#F5F7FA" } }, { "path": "pages/home/home", "style": { "navigationStyle": "custom", "navigationBarTitleText": "海外仓工作台", "backgroundColor": "#F5F7FA" } }, { "path": "pages/module/module", "style": { "navigationStyle": "custom", "navigationBarTitleText": "海外仓业务", "backgroundColor": "#F5F7FA" } }, - { "path": "pages/operation/operation", "style": { "navigationStyle": "custom", "navigationBarTitleText": "现场作业", "backgroundColor": "#F5F7FA" } }, + { "path": "pages/operation/operation", "style": { "navigationStyle": "custom", "navigationBarTitleText": "柜号入库与现场作业", "backgroundColor": "#F5F7FA" } }, { "path": "pages/history/history", "style": { "navigationStyle": "custom", "navigationBarTitleText": "作业记录", "backgroundColor": "#F5F7FA" } }, { "path": "pages/profile/profile", "style": { "navigationStyle": "custom", "navigationBarTitleText": "个人中心", "backgroundColor": "#F5F7FA" } }, { "path": "pages/scanner-test/scanner-test", "style": { "navigationStyle": "custom", "navigationBarTitleText": "广播扫码测试", "backgroundColor": "#F5F7FA" } } diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue index cd6d90f..856fd82 100644 --- a/src/pages/operation/operation.vue +++ b/src/pages/operation/operation.vue @@ -88,7 +88,7 @@ 本次已收 {{ lastReceipt.acceptedPackages }} 箱,{{ lastReceipt.inboundCode }} 剩余 {{ lastReceipt.pendingPackages }} 箱 - +