From b118189e4ed1a866afadbbfdf5b64eef8b91a7e9 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 29 Sep 2026 18:47:52 +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 | 6 +++--- README.md | 8 ++++---- src/stores/session.js | 9 ++++++++- tests/session-auth.test.mjs | 6 +++--- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7b7fffe..f178d19 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,16 +74,16 @@ - 换标执行完成照片以缩略图展示并支持大图预览、移除和重拍。重新进入任务通过 `relabel-tasks/{relabelNo}/photos/files/{fileId}/download` 鉴权读取,不持久化设备临时图片地址;重拍先上传成功再替换待提交文件 ID,失败保留原照片,移除只解绑待提交引用。 - 退货查询空态集中在 `src/api/return-receipt.js`、`src/api/return-packing.js`:仅 `startReturnReceipt`、`findReturnSkuCandidates`、`findPackingOrders` 将 `A0404/404` 转为无匹配结果;签收确认、合箱写入和标签读取不得套用此处理,权限、网络与服务错误仍须保留诊断并报错。回归见 `tests/return-receipt-api.test.mjs`、`tests/return-packing-api.test.mjs`,不得将查询无记录误报为写入成功。 -- `src/api/inbound.js` 已接入待卸柜候选、柜详情/prepare、current 恢复、三类照片签收、统一箱码定位入仓码、实物托盘解析、普通 FBA bulk/普通 OVERSEAS carton、一件代发 source pallet 整托收货及整柜完成;FBA bulk 不提交 `skuCode`,库存分配由服务端按计划明细完成;一件代发只提交国内冻结的来源托盘号,不允许逐箱或拆托收货。响应与 404/409 判定集中在 `src/api/receiving-adapter.js`,双 Token 刷新与海外 401 单次重放由 `src/api/overseas.js` 统一处理。 +- `src/api/inbound.js` 已接入待卸柜候选、柜详情/prepare、current 恢复、三类照片签收、统一箱码定位入仓码、实物托盘解析、普通 FBA bulk/普通 OVERSEAS carton、一件代发 source pallet 整托收货及整柜完成;FBA bulk 不提交 `skuCode`,库存分配由服务端按计划明细完成;一件代发只提交国内冻结的来源托盘号,不允许逐箱或拆托收货。响应与 404/409 判定集中在 `src/api/receiving-adapter.js`,统一 Token 刷新与业务 401 单次重放由 `src/api/overseas.js` 统一处理。 - 入仓摘要、箱码解析和实收响应的 `assigned/mergeAllowed/urgent/mergeHint` 只消费 Lastleg 返回值,布尔标志只有 `true` 生效,`mergeAllowed` 缺省为 `false`;合托仅作提示,不能改变收货资格或触发分配。首扫实物托盘只确认服务端已有托盘、库位与资源锁预留,不在设备端重新分配托盘、库位或资源锁;只提交业务箱码、托盘码及当前收货数量,最终资格由服务端决定。 -- `src/api/overseas.js` 的 `overseasUrl` 统一拼接普通请求、上传和下载地址:先移除运行时 `apiBase` 末尾斜杠;当基址已以 `/api` 或 `/overseas-api` 结尾时,再移除业务路径重复的 `/api` 前缀。各业务 API 不得自行拼接 `session.apiBase`,也不得依赖双斜杠或重复 `/api` 被网关容错。 +- `src/api/overseas.js` 的 `overseasUrl` 统一拼接普通请求、上传和下载地址:先移除运行时 `apiBase` 末尾斜杠;当基址已以 `/api` 结尾时,再移除业务路径重复的 `/api` 前缀。各业务 API 不得自行拼接 `session.apiBase`,也不得依赖双斜杠或重复 `/api` 被网关容错。 - 待卸柜候选只消费服务端已按 Token 绑定仓过滤的结果,并严格校验每柜正式 `overseasWarehouseCode`、`overseasWarehouseName` 与 `warehouse-profile` 一致;字段缺失或仓不一致一律 fail-closed,不得从 FBA/业务目的仓或内部 ID 推导。候选保留服务端返回的未齐全柜并严格消费 `allPlansReady`、`operable`、`blockReason`、`plannedArrivalAt` 与柜级计划统计;`operable=false` 只能查看只读摘要,不得调用 prepare。候选顺序沿用服务端“可操作优先、同组计划到仓时间升序、时间为空最后”的排序,前端不得用入仓码/计划数组长度自行推导就绪或把预计到港冒充计划到仓。 - 候选详情、prepare 响应和 current 恢复都必须重复校验同一接柜仓编码;prepare 只建立已分配柜的作业会话,不得当作首次认领。服务端仓冲突的 `409/A0409` 中文错误保留 `requestId`/`traceId`;会话建立后仍按既有 current 恢复与完成前禁止切柜逻辑处理。 - `src/api/pallet.js` 只调用 Lastleg 托盘专用 PDA Controller;前端提交服务端扫码返回的 `palletVersion`、`positionVersion` 和随机幂等键,不提交客户、仓库或库位标识。409 后必须重新扫码获取最新版本,禁止用旧库存快照继续作业。 - 请求身份和资源范围只取 Token 与 current 会话;前端不得提交 `customerId`、`planNo`、`allocationId`、`warehouseId` 或 `locationId`。409 必须引导账号恢复当前柜,完成前不得切柜。 - 签收照片固定为三个角色且每个角色只有一个当前文件;上传成功后使用服务端 `downloadPath` 在当前会话内鉴权读取。更换失败时保留原 `fileId` 和可提交状态,无原图的失败槽不得计入 `photosReady`;本地临时路径仅用于本次设备预览,不作为跨会话文件事实。 - 库内、换标与现场查询只调用 `/api/pda/**` 业务 API/DTO,并统一受 `lastleg:pda:operate`、Token 冻结仓库范围和服务端资源归属约束;写操作必须保留稳定幂等键,409 后重新读取服务端状态,不得把平台、客户或内部接口直接暴露给 PDA。 -- 柜号签收入仓调用 Lastleg `/api/pda/v1/last-leg/receiving-containers/**`;退货签收与合箱分别调用 `/api/pda/v1/last-leg/return-receipts/**`、`/api/pda/v1/last-leg/return-inventory/**`,具体入口见上表。浏览器或设备请求必须经 `gateway-overseas` 的显式 PDA 路由,不得直连 Lastleg,也不得改走国内 Gateway。 +- 柜号签收入仓调用 Lastleg `/api/pda/v1/last-leg/receiving-containers/**`;退货签收与合箱分别调用 `/api/pda/v1/last-leg/return-receipts/**`、`/api/pda/v1/last-leg/return-inventory/**`,具体入口见上表。浏览器或设备请求必须经统一 `gateway` 的显式 PDA 路由,不得直连 Lastleg。 - 海外业务 Token、PDA 权限和可信身份最终由 Gateway 与服务端校验;前端不得用工作台 `available`、本地客户 ID 或仓库 ID 代替授权和资源归属校验。 - 排车和出库入口只调用专用 PDA 接口;客户与冻结海外仓范围由 Token、Gateway 和服务端解析,前端不提交 `customerId`、`warehouseId`,也不把内部数据库 ID 展示成业务编号。网络失败保留草稿和幂等键,恢复后以服务端任务状态为准;排车详情返回的 `canDepart`、`departBlockReason` 是发车按钮唯一门禁,平台未转派送生成 `WAIT_OUTBOUND` 出库单时不得发车;完成或发车成功后不得重复提交。派送始终由 Admin/Client 承载。 diff --git a/README.md b/README.md index 676776d..7b23cde 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 独立的 uni-app Vue 3 App 工程,正式产品形态是安装在海外仓手持 PDA 上的 App。工程不依赖国内 PDA 源码或仓库外共享目录,可独立安装依赖、运行、编译和发布;H5 只用于浏览器调试和编译门禁,不是最终交付形态。 -工作台提供柜号级真实入仓、托盘管理、排车拣货、出库任务、一件代发出库、任务完成和发车入口;派送单、派送状态与客户回访由 Admin/Client 承载。登录使用国内 Auth 统一签发的双 Token;海外业务请求统一经 `gateway-overseas`,不得请求国内 Firstleg/Warehouse 业务接口。 +工作台提供柜号级真实入仓、托盘管理、排车拣货、出库任务、一件代发出库、任务完成和发车入口;派送单、派送状态与客户回访由 Admin/Client 承载。登录使用Auth 签发的统一 Access/Refresh Token;业务请求统一经 `gateway`,不得请求国内 Firstleg/Warehouse 业务接口。 ## 本地运行与检查 @@ -32,7 +32,7 @@ npm run build:app 1. 使用海外版所属的 DCloud 账号为本应用获取独立 AppID,写入 `src/manifest.json` 的 `appid`;不得复用国内版 AppID。 2. 确认 Android 包名 `com.baiyuelogistics.pda.overseas`,如公司已有正式命名规范,在首次正式签名前修改。 3. 配置海外版独立 Android 签名证书;证书、密码和账号信息不得提交仓库。 -4. 复制 `.env.example` 为本机 `.env.local`,配置 Auth 与海外网关地址;环境文件不提交仓库。 +4. 复制 `.env.example` 为本机 `.env.local`,配置统一 Gateway 地址;环境文件不提交仓库。 5. 在目标海外 PDA 上验证扫描广播 Action=`android.intent.action.SCANRESULT`、Extra=`value`。 ## 当前实现边界 @@ -46,9 +46,9 @@ npm run build:app - 出库作业已接 `/api/pda/v1/last-leg/outbound-tasks/**`:支持普通出库和一件代发任务列表/详情、托盘码/箱码/SKU/入仓码扫码拣货与核平完成;不会展示或进入派送流程。 - 排车与出库写操作使用可恢复的稳定幂等键。网络失败保留当前任务、扫码草稿和幂等键;成功后释放键并按服务端状态恢复,避免重复完成或发车。 - 入仓请求不手填客户、计划、分配、仓库或库位标识;身份与当前柜范围由 Token、Gateway 和服务端会话确定。页面同时支持 PDA 广播扫码与 H5 人工输入。 -- 登录、恢复和刷新会 fail-closed 校验国内/海外双 Token 的同一平台操作主体和同一冻结海外仓,并从 Auth 登录/刷新响应的顶层权限快照校验 `OVERSEAS_WAREHOUSE_PDA_OPERATOR`、`lastleg:pda:operate`,不从 JWT 解析权限;并发海外 401 共享一次国内 refresh,校验完成后原子替换双 Token和权限快照,并将原请求或照片上传重放一次。 +- 登录、恢复和刷新会 fail-closed 校验统一 Token 的平台操作主体和冻结海外仓,并从 Auth 登录/刷新响应的顶层权限快照校验 `OVERSEAS_WAREHOUSE_PDA_OPERATOR`、`lastleg:pda:operate`,不从 JWT 解析权限;并发 401 共享一次 refresh,主体与绑定仓保持一致后原子替换统一 Token 和权限快照,并将原请求或照片上传重放一次。 - 库存盘点、商品移库、换标收货/执行/复核以及库存/库位/托盘查询均已接入 `/api/pda/**` 专用接口;支持 PDA 广播扫码与 H5 人工输入,并按服务端版本、幂等键和冲突状态恢复,不调用平台、客户或内部接口。 -- 本地契约测试与 H5/App 资源编译不等于真实开发环境 HTTP 或 PDA 真机验收。 +- 本地契约测试与 H5/App 资源编译不等于真实测试环境 HTTP 或 PDA 真机验收。 ## App 在线更新 diff --git a/src/stores/session.js b/src/stores/session.js index e75cbba..73a3584 100644 --- a/src/stores/session.js +++ b/src/stores/session.js @@ -137,7 +137,14 @@ export function signInWithTokens(tokens) { export function replaceTokens(tokens) { if (!tokens?.accessToken) throw new Error('刷新响应缺少访问令牌') const pair = validatePdaToken(tokens.accessToken, tokens) - if (session.currentUser && (String(session.currentUser.id) !== claimId(pair.claims, 'user_id') || String(session.currentUser.warehouseId) !== pair.warehouseId)) throw new Error('作业主体或绑定仓库已变更,请重新登录') + if (session.currentUser) { + const previousClaims = decodeAccessToken(session.token) + const identityChanged = ['user_id', 'membership_id', 'tenant_id', 'app_code', 'overseas_warehouse_id'] + .some((name) => claimId(previousClaims, name) !== claimId(pair.claims, name)) + if (identityChanged || String(session.currentUser.id) !== claimId(pair.claims, 'user_id') || String(session.currentUser.warehouseId) !== pair.warehouseId) { + throw new Error('作业主体或绑定仓库已变更,请重新登录') + } + } const user = userFromClaims(pair, session.currentUser) const nextRefreshToken = tokens.refreshToken || session.refreshToken // Validate the complete replacement before mutating reactive or persisted session state. diff --git a/tests/session-auth.test.mjs b/tests/session-auth.test.mjs index 5013829..2b12bad 100644 --- a/tests/session-auth.test.mjs +++ b/tests/session-auth.test.mjs @@ -108,11 +108,11 @@ test('refresh replacement validates the unified token before atomically changing assert.equal(session.refreshToken, 'refresh-43') }) -test('refresh cannot carry an old operation into another user or warehouse', () => { +test('refresh cannot carry an old operation into another user, membership, tenant or warehouse', () => { storage(); signOut(); signInWithTokens(tokenPair()) const original = session.token - for (const domestic of [{ user_id: 99 }, { overseas_warehouse_id: 901 }]) { - assert.throws(() => replaceTokens(tokenPair({ domestic })), /主体或绑定仓库已变更/) + for (const domestic of [{ user_id: 99 }, { membership_id: 85 }, { tenant_id: 1 }, { overseas_warehouse_id: 901 }]) { + assert.throws(() => replaceTokens(tokenPair({ domestic })), /主体或绑定仓库已变更|不属于平台仓库作业端/) assert.equal(session.token, original) assert.equal(session.currentUser.warehouseId, '900') }