diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index de0e642..0000000 --- a/.dockerignore +++ /dev/null @@ -1,12 +0,0 @@ -# Only compiled H5 assets and image configuration enter the build context. -** -!deploy/ -!deploy/Dockerfile.h5 -!deploy/nginx.conf -!dist/ -!dist/build/ -!dist/build/h5/ -!dist/build/h5/** - -!deploy/Dockerfile.h5.production -!deploy/nginx.production.conf.template diff --git a/AGENTS.md b/AGENTS.md index e6be3ad..68cc29b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,7 @@ ## 服务与发布边界 +- H5只上传Nginx站点,禁止容器发布及重跑旧灰度脚本;步骤见`README.md`。 - Auth 唯一签发账号/Token;认证/业务共用 Gateway,App 根地址只读 VITE_API_BASE_URL,H5 用 VITE_H5_API_BASE_URL。 - 登录/刷新/会话校验Token平台主体、成员、有效期、冻结仓;`OVERSEAS_WAREHOUSE_PDA_OPERATOR`、`lastleg:pda:operate` 仅读存Auth登录/刷新顶层快照,禁解析JWT权限。快照缺失/无效/过期/跨主体或撤权均fail-closed清会话,不能只藏入口。 - 并发401仅一次refresh,完整校验Token/快照再原子替换;主体/仓变化清会话重登录,原请求/照片上传最多重放一次。刷新失败清会话;业务/网络错留code、requestId、traceId或network诊断。 @@ -35,7 +36,7 @@ | AppID、平台图标与打包资源 | `src/manifest.json`、`static/app-icons/` | | Android APK/WGT 在线更新 | `src/App.vue`、`src/utils/app-update.js`、`.env.example` | | 登录、刷新与会话 | `src/pages/login/login.vue`、`src/api/auth.js`、`src/stores/session.js` | -| 海外仓工作台与模块入口 | `src/pages/home/home.vue`、`src/pages/module/module.vue` | +| 海外仓工作台与作业入口 | `src/pages/home/home.vue`;首页直接进入各作业,页面刷新后返回首页 | | 中英西语言设置 | `src/i18n/index.js`、`src/i18n/locales/`、`src/pages/profile/profile.vue` | | 柜号签收、照片与入仓真实作业 | `src/pages/operation/operation.vue`、`src/components/ReceivingSessionSummary.vue`、`src/api/inbound.js` | | 退货入库签收与合箱 | `src/pages/return-receipt/return-receipt.vue`、`src/pages/return-packing/return-packing.vue`、`src/api/return-receipt.js` | @@ -55,8 +56,7 @@ ## 海外业务接口边界 - 库内/换标/收货/排车/出库/查询仅用 `/api/pda/**` API/DTO,经 `gateway` 显式PDA路由,不直连Lastleg/借平台、客户、内部接口。Gateway/服务端校验Token、`lastleg:pda:operate`、冻结仓、客户/资源归属;工作台 `available`、本地客户/仓ID不代替授权。 -- 仅展示服务端业务标识/资格;写操作留稳定幂等键,失败/409重读权威状态,超时不判未执行/成功。 -- 收货/退货/移库/托盘/换标/发车/照片/恢复变更须读 `docs/business-index.md` 对应页面行及“海外业务接口边界”,执行专项门禁。 +- 仅展示服务端业务标识/资格;写请求携当前版本及本次幂等键,仅请求中防连点,结束即释放操作态。保留表单,不持久化或重放失败命令,下次点击按当前输入新建请求;409/结果不明先刷新权威状态,超时不判未执行/成功。Token刷新单次重放仍沿认证规则。 - 仓储转运逐托存箱数、长宽高(cm)、毛重(kg),本托尺寸算体积;逐SKU核对件数差异,不以预报箱数强制核平。沿用客户预报箱唛,不造箱号。测量完成前客户/平台可增整单预报箱数、不选SKU,箱内SKU以PDA实点为准;刷新合并新箱、留本机草稿,清点传forecastPackageCount拦旧快照。生成托盘号锁该卡测量,服务端绑定确认且至少一张现场照片才可完成打托;完成传稳定幂等键、照片文件ID,SKU实收差异在完成弹窗必填原因,不重传测量。重进从详情恢复托盘/鉴权照片;`labelDownloadPath` 下载PDF,打开不等于真机打印。 - 换标目标确认即提交拆合托,无底部二次提交。下述组件按本轮最新 `PALLET_BIND` 逐箱改绑,扫新目标弹窗确认,经 `relabel-disassemble` 精确来源路径(不传 `actualSource`);同目标不写。服务端约束库存/托盘版本及幂等,已排车/出库禁改绑。回归 `tests/relabel-pallet-work.test.mjs`,离线非真机验收。 @@ -81,8 +81,8 @@ - 请求身份/资源范围仅取Token/current,不传 `customerId/planNo/allocationId/warehouseId/locationId`;409引导账号恢复当前柜,完成前禁切柜。 - 签收照片三角色各一当前文件;上传后以服务端 `downloadPath` 会话内鉴权读。换图失败留原 `fileId`/可提交状态,无原图失败槽不计 `photosReady`;临时路径仅本次预览。 - 柜号签收入仓用Lastleg `/api/pda/v1/last-leg/receiving-containers/**`,退货签收/合箱分别用 `/api/pda/v1/last-leg/return-receipts/**`、`/api/pda/v1/last-leg/return-inventory/**`,入口见上表。 -- 理货审核沿既有排车详情;规则/回归见业务索引排车条目。 -- 排车/出库网络失败留原命令和键;资格、阶段、恢复及装车资料API见业务索引排车条目。`src/components/DispatchLoadingDocuments.vue` 支持装车中同步上传,装车与资料均完成后自动派送,无独立发车入口。 +- 排车资格、阶段、理货审核及装车资料 API/回归查业务索引排车条目。 +- `src/components/DispatchLoadingDocuments.vue` 支持装车中上传,装车与资料均完成后自动派送,无独立发车入口。 ## 质量门禁 diff --git a/README.md b/README.md index 5d9b525..0829686 100644 --- a/README.md +++ b/README.md @@ -75,32 +75,38 @@ Android App 启动后会读取 `VITE_PDA_UPDATE_URL` 并检查海外 PDA 的独 3. 更新清单与下载地址使用 HTTPS,先在测试设备验证可选更新、强制更新、断网重试、APK 覆盖安装、wgt 重启及扫码作业回归,再切换正式清单。 4. 海外和国内更新记录、文件目录及发布权限保持隔离,服务端必须按请求中的 `appid` 拒绝跨应用更新包。 -## TEST H5 静态托管 +## H5 静态托管(TEST 与灰度) -发布构建使用 `/` 资源 base 与 hash 路由,部署到本应用独立域名根目录,浏览器入口为 `/#/`。开发服务仍使用根路径;不能将 Vite 开发代理当作静态服务器配置。 +国内、海外 PDA H5 统一由宿主机已有 Nginx/宝塔站点直接提供静态文件,不再构建或发布 PDA H5 Docker 镜像、Compose 服务或容器。正式部署约束见 [测试环境单机部署与配置契约](../../docs/测试环境单机部署与配置契约.md) 的“PDA H5 发布方式”;历史灰度报告不再作为发布入口。 -执行 `npm run build:h5`。无需启动 Vite 开发服务器;`npm run build:app` 不属于 H5 发布。 +发布构建使用 `/` 资源 base 与 hash 路由,部署到本应用独立域名根目录,浏览器入口为 `/#/`。不能放到子目录后继续共用根路径资源,也不能将 Vite 开发代理当作线上配置。 -`deploy/Dockerfile.h5` 只复制已构建 `dist/build/h5/` 及静态 Nginx 配置,不在镜像中复制源码、依赖或重新编译。`.dockerignore` 只放行这两类输入。容器静态目录为 `/srv/pda-h5/`。 +执行 `npm run build:h5` 导出 Web 静态文件;`npm run build:app` 不属于 H5 发布。 -从本仓库执行 `docker compose -f deploy/docker-compose.h5.test.yml build --pull=false`,再由获授权的部署流程执行 `docker compose -f deploy/docker-compose.h5.test.yml up -d --wait --wait-timeout 120`。必须设置 `IMAGE_TAG`,`NGINX_BASE_IMAGE` 可指定已缓存的兼容 Alpine Nginx 镜像。容器名及网络别名为 `pda-overseas-h5-test`,仅在外部 `firstleg-test-network` 暴露容器 80,不发布宿主端口。健康检查同时请求 `/healthz` 与 `/` 的实际首页。 +发布步骤: -独立域名入口将 `/` 代理至本应用静态容器根目录,或将构建目录内容直接发布到站点根目录;不再挂载 PDA 子路径。缺失资源返回 404,不回退首页,HTML 使用 `Cache-Control: no-store`。域名入口还须配置同源 API 代理:两端均将 `/gateway-api/` 转统一 Gateway `/api/`。入口须保留 Authorization,支持文件上传下载及至少 11m 请求体。域名与上游只在部署侧配置,不编入 PDA 源码。 +1. 构建后执行 `node scripts/verify-h5.mjs`,核对 HTML/JS/CSS 资源路径及同源 API。构建、测试仍按根规则经 `local-check.ps1` 执行。 +2. 将 `dist/build/h5/` **目录内的文件**上传到该 PDA 站点现有 `root`。先上传 `assets/`、`static/` 等资源,最后替换 `index.html`,保留站点证书验证目录和站点配置;不要把外层构建目录一起嵌套上传。 +3. 页面 `location /` 使用 `try_files $uri $uri/ =404` 直接读取静态文件,设置 `Cache-Control: no-store`;不得再转发到旧 PDA 容器。接口 `/gateway-api/` 独立转发到当前环境统一 Gateway `/api/`,保留 Authorization 和上传能力。 +4. 首次切换前备份实际生效站点配置,合并下述片段并执行 `nginx -t`,成功后才重载。已有独立 API 规则先核对并复用,不能直接追加重复 `location`。后续仅更新静态文件时无需新建容器或重载 Nginx。 +5. 检查域名首页及实际引用的 JS/CSS 均返回 200,并将响应内容的 SHA-256 与本次产物逐项比较;确认响应不是旧容器内容。再检查登录和受影响页面。首页 200 不等于接口或业务验收通过。 -部署配置检查:`node --test tests/h5-deployment.test.mjs`。没有 Docker 运行时的本地合同检查不能替代目标主机 `docker compose config`、Nginx 启动与真实 HTTP 验收。 +### 站点配置片段 -构建后执行 `node scripts/verify-h5.mjs`,核对 HTML/JS/CSS 资源路径、同源 API 和无本机/旧海外配置泄漏。该检查与本地构建仅证明产物;TEST 页面登录、仓权限、业务操作和 Android 真机另行验收。 +`deploy/nginx.conf.template` 是合并到已有 `server` 的参考片段,不是完整站点配置。保留现有域名、HTTPS、证书与宝塔 include;替换原有 root/index 及同名 location。环境地址、端口及网站目录只在部署侧配置,不提交到源码。 + +在部署机器上将 `PDA_SITE_ROOT` 设置为该站点真实根目录,`PDA_GATEWAY_ORIGIN` 设置为宿主机可达的当前环境 Gateway 源地址(协议、主机、端口,不含末尾 `/` 或 `/api`)。不要使用仅 Docker 网络可解析的服务名。显式只渲染这两个变量,保留 `$uri`、`$host` 和代理请求头变量: + +```bash +envsubst '${PDA_SITE_ROOT} ${PDA_GATEWAY_ORIGIN}' < deploy/nginx.conf.template +``` + +输出仅供核对和合并,不直接覆盖完整站点。若机器没有 `envsubst`,手工替换这两个占位符;未替换的模板不能直接加载。 ### 两端统一 API 配置 -两端的 `VITE_API_BASE_URL` 均为统一 Gateway 绝对地址(含 `/api`);`VITE_H5_API_BASE_URL=/gateway-api`;`API_PROXY_TARGET` 为同一 Gateway 源地址(不含 `/api`)。实际地址只放在未提交的 `.env.local`;修改后重启开发服务,发布前重新打包。 +App 的 `VITE_API_BASE_URL` 为统一 Gateway 绝对地址(含 `/api`);H5 使用 `VITE_H5_API_BASE_URL=/gateway-api`。`API_PROXY_TARGET` 仅供本地 Vite 开发代理使用,不随静态包发布。实际地址放在未提交环境文件;变更编译变量后须重新构建。 -两端独立域名入口均配置以下规则;`gateway:9000` 仅适用于同一 Docker 网络,其他部署须替换成实际统一 Gateway 上游。静态容器只提供页面文件,Vite 代理不会随静态包发布。 +站点 API 规则须使用 `location ^~ /gateway-api/`,防止驾驶证等包含 `LICENSE` 的业务路径被站点敏感文件正则截获。仅更新静态包不能修复代理配置;域名入口和每层代理均须核对。 -```nginx -location ^~ /gateway-api/ { - proxy_pass http://gateway:9000/api/; - proxy_set_header Authorization $http_authorization; - client_max_body_size 11m; -} -``` +部署定向检查:`node --test tests/h5-deployment.test.mjs`,经 `local-check.ps1` 执行。该检查覆盖静态发布约束、资源 base 和开发代理重写,不替代目标主机 Nginx 校验、真实 HTTP、人工页面及 Android 真机验收。 diff --git a/deploy/Dockerfile.h5 b/deploy/Dockerfile.h5 deleted file mode 100644 index d20020b..0000000 --- a/deploy/Dockerfile.h5 +++ /dev/null @@ -1,5 +0,0 @@ -ARG NGINX_BASE_IMAGE=nginx:1.27-alpine -FROM ${NGINX_BASE_IMAGE} -COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf -COPY dist/build/h5/ /srv/pda-h5/ -EXPOSE 80 diff --git a/deploy/Dockerfile.h5.production b/deploy/Dockerfile.h5.production deleted file mode 100644 index 5ecc940..0000000 --- a/deploy/Dockerfile.h5.production +++ /dev/null @@ -1,5 +0,0 @@ -ARG NGINX_BASE_IMAGE=nginx:1.27-alpine -FROM ${NGINX_BASE_IMAGE} -COPY deploy/nginx.production.conf.template /etc/nginx/templates/default.conf.template -COPY dist/build/h5/ /srv/pda-h5/ -EXPOSE 80 diff --git a/deploy/docker-compose.h5.production.yml b/deploy/docker-compose.h5.production.yml deleted file mode 100644 index 3dc79ee..0000000 --- a/deploy/docker-compose.h5.production.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: firstleg-production-pda-overseas-h5 -services: - pda-overseas-h5: - build: - context: .. - dockerfile: deploy/Dockerfile.h5.production - args: - NGINX_BASE_IMAGE: "${NGINX_BASE_IMAGE:-nginx:1.27-alpine}" - image: pda-overseas-h5-prod:${IMAGE_TAG:?IMAGE_TAG is required} - container_name: pda-overseas-h5-prod - restart: unless-stopped - environment: - API_UPSTREAM: "http://gateway:9000" - ports: - - "127.0.0.1:${HOST_PORT:-29006}:80" - healthcheck: - test: ["CMD-SHELL", "wget -qO /dev/null http://127.0.0.1/healthz && wget -qO /dev/null http://127.0.0.1/"] - interval: 5s - timeout: 3s - retries: 12 - start_period: 5s - networks: - backend: - aliases: - - pda-overseas-h5-prod -networks: - backend: - external: true - name: firstleg-production-network diff --git a/deploy/docker-compose.h5.test.yml b/deploy/docker-compose.h5.test.yml deleted file mode 100644 index 1022898..0000000 --- a/deploy/docker-compose.h5.test.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: firstleg-test-pda-overseas-h5 -services: - pda-overseas-h5: - build: - context: .. - dockerfile: deploy/Dockerfile.h5 - args: - NGINX_BASE_IMAGE: "${NGINX_BASE_IMAGE:-nginx:1.27-alpine}" - image: pda-overseas-h5-test:${IMAGE_TAG:?IMAGE_TAG is required} - container_name: pda-overseas-h5-test - restart: unless-stopped - expose: - - "80" - healthcheck: - test: ["CMD-SHELL", "wget -qO /dev/null http://127.0.0.1/healthz && wget -qO /dev/null http://127.0.0.1/"] - interval: 5s - timeout: 3s - retries: 12 - start_period: 5s - networks: - test-backend: - aliases: - - pda-overseas-h5-test -networks: - test-backend: - external: true - name: firstleg-test-network diff --git a/deploy/nginx.conf b/deploy/nginx.conf deleted file mode 100644 index bdef818..0000000 --- a/deploy/nginx.conf +++ /dev/null @@ -1,16 +0,0 @@ -server { - listen 80; - server_name _; - root /srv/pda-h5; - - location = /healthz { - default_type text/plain; - add_header Cache-Control "no-store" always; - return 200 "ok\n"; - } - location / { - index index.html; - try_files $uri $uri/ =404; - add_header Cache-Control "no-store" always; - } -} diff --git a/deploy/nginx.conf.template b/deploy/nginx.conf.template new file mode 100644 index 0000000..b7a742d --- /dev/null +++ b/deploy/nginx.conf.template @@ -0,0 +1,21 @@ +# Merge into the existing site's server block; preserve TLS and panel includes. +# Render only PDA_SITE_ROOT and PDA_GATEWAY_ORIGIN (see README). +# Replace existing root/index and matching locations; do not append duplicates. +root ${PDA_SITE_ROOT}; +index index.html; + +location / { + try_files $uri $uri/ =404; + add_header Cache-Control "no-store" always; +} + +location ^~ /gateway-api/ { + proxy_pass ${PDA_GATEWAY_ORIGIN}/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Authorization $http_authorization; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + client_max_body_size 55m; +} diff --git a/deploy/nginx.production.conf.template b/deploy/nginx.production.conf.template deleted file mode 100644 index 4ce203e..0000000 --- a/deploy/nginx.production.conf.template +++ /dev/null @@ -1,25 +0,0 @@ -server { - listen 80; - server_name _; - root /srv/pda-h5; - - location = /healthz { - default_type text/plain; - add_header Cache-Control "no-store" always; - return 200 "ok\n"; - } - location /gateway-api/ { - client_max_body_size 55m; - proxy_pass ${API_UPSTREAM}/api/; - proxy_http_version 1.1; - proxy_set_header Host $proxy_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - location / { - index index.html; - try_files $uri $uri/ =404; - add_header Cache-Control "no-store" always; - } -} diff --git a/docs/business-index.md b/docs/business-index.md index 2d96f1b..87f9a8a 100644 --- a/docs/business-index.md +++ b/docs/business-index.md @@ -14,10 +14,10 @@ | AppID、平台图标与打包资源 | `src/manifest.json`、`static/app-icons/` | Android/iOS 尺寸映射由 `tests/app-icons.test.mjs` 锁定;独立 AppID、签名和发布通道不得复用国内 PDA | | Android APK/WGT 在线更新 | `src/App.vue`、`src/utils/app-update.js`、`.env.example`、`src/manifest.json` | `VITE_PDA_UPDATE_URL` 使用海外独立 HTTPS 通道;合同 `tests/app-update.test.mjs`,H5 不执行更新逻辑 | | 登录、刷新与会话 | `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` 是模块与作业可用性入口;首页通过 `/api/pda/v1/last-leg/receiving-containers/warehouse-profile` 读取 Token 绑定仓的中文主档名称 | +| 海外仓工作台与作业入口 | `src/pages/home/home.vue` | `src/data/overseas-workbench.js` 是分组与作业可用性入口;首页直接进入各作业,页面刷新后返回首页;首页通过 `/api/pda/v1/last-leg/receiving-containers/warehouse-profile` 读取 Token 绑定仓的中文主档名称 | | 中英西语言设置 | `src/i18n/index.js`、`src/i18n/locales/`、`src/pages/profile/profile.vue` | 个人中心可选择并持久化 `zh-CN`、`en`、`es`;当前业务翻译资源仍为空,页面业务文案继续使用中文,不得把语言选择入口误报为全量翻译完成 | | 柜号签收、照片与入仓真实作业 | `src/pages/operation/operation.vue`、`src/components/ReceivingSessionSummary.vue`、`src/api/inbound.js`、`src/api/receiving-adapter.js`、`src/api/overseas.js`、`src/stores/receiving.js` | 签收前展开显示国内空仓、半仓、满仓、锁仓和带锁码五类整柜只读照片,收货中折叠查看,单图加载失败只允许重试且不阻断拆柜;海外三个固定照片角色各一张,支持现场拍照、鉴权预览及更换失败保留原图;多计划同柜直接展示服务端 `assigned/mergeAllowed/urgent/mergeHint`,合托明确为现场可选而非完成门禁;扫码实收确认计划生成阶段已预留的真实托盘和库位,校验卡展示预留库位编码及 `A0` 共享溢出位提示,不在设备端重新分配。Backend `lastleg-service/inboundplan/pda/controller/ReceivingContainerController.java`、`ReceivingContainerService.java`,公网前缀 `/api/pda/v1/last-leg/receiving-containers/**` | -| 退货入库签收与合箱 | `src/pages/return-receipt/return-receipt.vue`、`src/pages/return-packing/return-packing.vue`、`src/api/return-receipt.js`、`src/api/return-packing.js`、`src/stores/warehouse-work.js` | 签收优先扫描退货单号,也可扫描 SKU 后选择候选退货单,按本包实际 SKU 数量生成退货箱与计费批次;合箱只选择同客户已完成退货单的可用库存并录入新箱箱规;公网前缀 `/api/pda/v1/last-leg/return-receipts/**`、`/api/pda/v1/last-leg/return-inventory/**` | +| 退货入库签收与合箱 | `src/pages/return-receipt/return-receipt.vue`、`src/pages/return-packing/return-packing.vue`、`src/api/return-receipt.js`、`src/api/return-packing.js`、`src/stores/warehouse-work.js` | 签收优先扫描退货单号,也可扫描 SKU 后选择候选退货单,按本包实际 SKU 数量生成退货箱与计费批次;刷新时服务端实收已变化须清空旧实物输入,未变化保留可编辑草稿;合箱只选择同客户已完成退货单的可用库存并录入新箱箱规;公网前缀 `/api/pda/v1/last-leg/return-receipts/**`、`/api/pda/v1/last-leg/return-inventory/**` | | 退货箱内清点 | `src/pages/return-counting/return-counting.vue`、`src/api/return-counting.js` | 扫描箱号或托盘号定位清点任务;各箱 SKU 汇总与签收数量核平,支持恢复当前任务;页面注册见 `src/pages.json` | | 仓储转运测量、打托与标签 | `src/pages/warehouse-transfer/warehouse-transfer.vue`、`src/api/warehouse-transfers.js` | 工作台键 `warehouseTransfer`;候选→详情→逐托测量草稿→生成新托盘号→扫码绑定→照片与完成打托。API 模块路径 `/pda/v1/last-leg/warehouse-transfers` 经统一海外请求层处理;不是 Admin 转运审核或排车转派送。回归 `tests/warehouse-transfer-api.test.mjs` | | 托盘管理真实作业 | `src/pages/pallet/pallet.vue`、`src/api/pallet.js` | 扫码查询托盘内容,按服务端返回的托盘/库存版本执行组托、拆托、换托和解绑;公网前缀 `/api/pda/v1/last-leg/pallet-operations/**` | @@ -26,7 +26,7 @@ | 换标真实作业 | `src/pages/relabel/relabel.vue`、`src/api/relabel.js` | 同一页面按操作参数承载换标收货、执行与复核;执行页按当前单明细切换 SKU/箱唛 Tab,组合明细同时显示两类需求。SKU 逐条扫码旧新SKU并携带审批件数,箱唛扫码仓库唛头/新FBA箱唛;组合单按同箱保留两个Tab的核对输入,全部核对后只提交一次箱事实。认领同步处理中;支持拍照、异常/返工和幂等恢复 | | 现场查询真实作业 | `src/pages/query/query.vue`、`src/api/warehouse-operations.js` | 同一页面按操作参数承载库存、库位与托盘查询,只展示稳定业务标识 | | 排车拣货与发车/理货审核 | `src/pages/dispatch/dispatch.vue`、`src/api/fulfillment.js`、`src/stores/fulfillment.js` | 普通进入展示任务列表和“上次作业”,不因本地草稿自动打开详情;明确跳转 `?resume=1` 才恢复排车。默认任务列表含待理货排车,详情展示服务端车辆/司机快照、计划发车及预计送达时间、当前有效FBA收货确认、调度审核状态/意见和货物;时间明确标注北京时间。预约或审核依据失效时 `APPROVAL_BLOCKED` 禁止批准但保留驳回。仅 `actions.LOADING_REVIEW.allowed=true` 时可调用 `POST /api/pda/v1/last-leg/dispatches/{dispatchNo}/loading-review`,提交原 `recordVersion/idempotencyKey/decision/reason`;驳回原因必填,任一级拒绝统一回客服处理(车辆信息),重新提交后再次逐级审核。PDA 不执行调度审核;理货通过并重读资格后才允许按实物托盘码或来源箱唛真实拣货,服务端 `actions.PICK/LOADING_COMPLETE` 分别决定真实拣货和装车完成资格;按已扫托数、箱数整车一次确认装车,不逐托填重量、体积或托位,资料未齐进入 `AFTER_LOADING`、已齐直接进入 `DELIVERING`;详情采用“拣货作业/装车拍照”两个 Tab,打开任务默认拣货;切换保留拣货草稿与照片状态,拍照页不响应拣货扫码,同单照片保存刷新保留当前 Tab。装车中即可在拍照 Tab 通过 `src/components/DispatchLoadingDocuments.vue` 上传五张整车照片,照片齐全且确认装车完成后由服务端在最后一项完成的事务自动生成派送并发车;不额外安排派送或确认发车。公网前缀 `/api/pda/v1/last-leg/dispatches/**`。回归 `tests/dispatch-workflow.test.mjs tests/fulfillment-workflows.test.mjs` | -| 整车发车与证照提醒 | `src/pages/dispatch/dispatch.vue`、`src/api/fulfillment.js` 的 `completeDispatchLoading` | 一张排车单代表一趟车,使用 `dispatchNo` 与公开 `recordVersion`(0 合法),保留完整原命令和 `idempotencyKey`。审核和现场作业均恢复校验原单号、动作、请求键、结果及版本;理货审核通过结果须 `stage=LOADING`,驳回须 `stage=BEFORE_LOADING`;已确认结果重读失败不再 POST,首个明确拒绝可手动重读、网络未知保留完整原命令且不以新版本覆盖。`LOADING_COMPLETE` 结果须为 `stage=AFTER_LOADING|DELIVERING`(result 仍为 Loaded,输入为原版本、manifestHash和幂等键,不再填报逐托测量),重读任务且版本不早于已确认结果后再清除原命令。司机证照、资源及各客户派送资格由服务端校验。正式流程见工作区 `docs/海外仓业务/排车列表统一作业改造契约.md`,不再使用独立车次发车请求或以车次状态代替排车公开状态 | +| 装车确认、自动派送与证照提醒 | `src/pages/dispatch/dispatch.vue`、`src/api/fulfillment.js` 的 `completeDispatchLoading` | 一单一趟,使用 `dispatchNo`、公开 `recordVersion`(0 合法)及本次 `idempotencyKey`。只在请求中锁定,结束即释放;保留表单,不持久化或重放失败命令,下次点击按当前输入和版本生成新键。结果不明或 409 先刷新权威任务,不以超时推断未执行或成功。结果核对单号、动作、请求键和版本;理货审核通过须 `stage=LOADING`,驳回须 `stage=BEFORE_LOADING`;`LOADING_COMPLETE` 返回 `stage=AFTER_LOADING|DELIVERING`、Loaded 结果,提交版本、manifestHash 和幂等键,不逐托填测量。成功后重读任务,版本不得早于已确认结果;重读失败只刷新,不自动重复 POST。司机证照、资源及各客户派送资格由服务端校验;正式流程见工作区 `docs/海外仓业务/排车列表统一作业改造契约.md`,不使用独立车次发车请求 | | 普通/一件代发出库 | `src/pages/outbound/outbound.vue`、`src/api/fulfillment.js`、`src/stores/fulfillment.js` | 首次进入清空扫描框并展示任务列表和“上次作业”标记,不自动恢复旧详情;详情提供独立返回列表入口。逐箱扫码、DP 托盘整托拣取及幂等完成保持不变;扫描排车业务号显式跳转排车 `?resume=1`。公网前缀 `/api/pda/v1/last-leg/outbound-tasks/**`;派送由 Admin/Client 承载 | | 扫码设备与诊断 | `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` | 会话隔离、退出清理和后续真实作业历史契约 | @@ -58,8 +58,8 @@ - `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。 +- 库内、换标与现场查询只调用 `/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` 的显式 PDA 路由,不得直连 Lastleg。 - 海外业务 Token、PDA 权限和可信身份最终由 Gateway 与服务端校验;前端不得用工作台 `available`、本地客户 ID 或仓库 ID 代替授权和资源归属校验。 -- 排车和出库入口只调用专用 PDA 接口;客户与绑定海外仓由 Token、Gateway 和服务端解析,前端不提交 `customerId`、`warehouseId`,也不展示内部数据库 ID 作为业务编号。网络失败保留完整原命令、草稿和幂等键,恢复后以服务端任务状态为准;动作资格与恢复规则见本文件排车条目,装车中可同步上传五张照片,装车确认与资料齐全两项均完成后服务端自动生成派送并发车,装完但资料未齐时留在装车后;照片API为 `/pda/v1/last-leg/dispatches/{dispatchNo}/loading-documents`,前端不补派送费用。完成成功后不得重复提交;PDA 不直接创建派送单,平台与客户的读取范围遵循现行契约。 -- 排车拣货、普通出库、整托出库和废弃确认在实际写库存前,先用原扫码 `idempotencyKey` 调用 `POST /api/pda/v1/last-leg/storage-rent/preparations`。四种模式分别为 `DISPATCH/OUTBOUND/OUTBOUND_PALLET/DISCARD`,货源、客户、计租范围和可用资金由服务端校验。仅明确 `ready=true` 且状态为 `NOT_REQUIRED/READY/CONSUMED` 才继续原扫码命令;待冻结、失败、过期、响应异常或网络未知均不发库存写请求。保留完整原命令与键,再次提交同一预校验接口恢复,不能换新键绕过费用确认;已消费的预校验只用于恢复原命令结果,不重复收费。定向回归为 `tests/storage-rent-preflight.test.mjs`,离线通过不代表实际冻结、扣款和出库联调已验收。 +- 排车和出库入口只调用专用 PDA 接口;客户与绑定海外仓由 Token、Gateway 和服务端解析,前端不提交 `customerId`、`warehouseId`,也不展示内部数据库 ID 作为业务编号。动作资格、版本和失败处理见本文件排车条目,装车中可同步上传五张照片,装车确认与资料齐全两项均完成后服务端自动生成派送并发车,装完但资料未齐时留在装车后;照片API为 `/pda/v1/last-leg/dispatches/{dispatchNo}/loading-documents`,前端不补派送费用。完成成功后不得重复提交;PDA 不直接创建派送单,平台与客户的读取范围遵循现行契约。 +- 排车拣货、普通出库、整托出库和废弃确认在实际写库存前,先用原扫码 `idempotencyKey` 调用 `POST /api/pda/v1/last-leg/storage-rent/preparations`。四种模式分别为 `DISPATCH/OUTBOUND/OUTBOUND_PALLET/DISCARD`,货源、客户、计租范围和可用资金由服务端校验。仅明确 `ready=true` 且状态为 `NOT_REQUIRED/READY/CONSUMED` 才继续原扫码命令;待冻结、失败、过期、响应异常或网络未知均不发库存写请求。预校验与紧随其后的库存写请求使用同一个键和同一份货源参数;请求结束释放操作态,保留表单但不自动重放失败命令。再次点击按当前输入和版本生成新键,仍必须重新通过服务端费用预校验;结果不明时先刷新权威任务,不以换键跳过费用确认。定向回归为 `tests/storage-rent-preflight.test.mjs`,离线通过不代表实际冻结、扣款和出库联调已验收。 diff --git a/src/api/dispatch-loading-documents.js b/src/api/dispatch-loading-documents.js index f355e9d..07b188d 100644 --- a/src/api/dispatch-loading-documents.js +++ b/src/api/dispatch-loading-documents.js @@ -21,6 +21,5 @@ export function normalizeLoadingDocuments(value, no) { } export async function getLoadingDocuments(no) { return normalizeLoadingDocuments(await overseasRequest('GET', path(no)), no) } export function uploadLoadingPhoto(no, slot, filePath, key) { return overseasUpload(`${slotPath(no, slot)}/uploads?idempotencyKey=${encodeURIComponent(key)}`, filePath) } -export function recoverLoadingPhoto(no, key) { return overseasRequest('GET', `${path(no)}/uploads/${encodeURIComponent(key)}`) } export async function bindLoadingPhoto(no, slot, body) { return normalizeLoadingDocuments(await overseasRequest('PUT', slotPath(no, slot), body), no) } export function readLoadingPhoto(no, photo) { return overseasDownload(`${slotPath(no, photo.slot)}/files/${encodeURIComponent(photo.fileId)}/content`, { expectedContentTypePrefix: 'image/' }) } diff --git a/src/api/overseas.js b/src/api/overseas.js index 3966a6f..1f618eb 100644 --- a/src/api/overseas.js +++ b/src/api/overseas.js @@ -135,12 +135,22 @@ function rawUpload(path, filePath, name, token) { const request = new XMLHttpRequest() request.open('POST', url) request.setRequestHeader('Authorization', `Bearer ${token}`) + request.timeout = 60000 request.onload = () => resolve({ statusCode: request.status, data: request.responseText, header: {} }) request.onerror = () => reject(new Error('海外作业文件上传失败')) + request.ontimeout = () => reject(new Error('照片上传超时,请重试')) + request.onabort = () => reject(new Error('照片上传已中断,请重试')) request.send(form) }) - const response = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: form }) - return { statusCode: response.status, data: await response.text(), header: Object.fromEntries(response.headers.entries()) } + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 60000) + try { + const response = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: form, signal: controller.signal }) + return { statusCode: response.status, data: await response.text(), header: Object.fromEntries(response.headers.entries()) } + } catch (cause) { + if (controller.signal.aborted) throw new Error('照片上传超时,请重试') + throw cause + } finally { clearTimeout(timeout) } }) .catch((cause) => { throw Object.assign(new Error(cause.message || '海外作业文件上传失败'), { network: true, uploadUrl: url }) }) } diff --git a/src/components/DispatchLoadingDocuments.vue b/src/components/DispatchLoadingDocuments.vue index b77428f..3db4448 100644 --- a/src/components/DispatchLoadingDocuments.vue +++ b/src/components/DispatchLoadingDocuments.vue @@ -8,21 +8,20 @@ 可边装车边上传;确认装车完成且照片齐全后自动进入派送中。 上传并保存 5 张装车照片后,本车自动进入派送中。 {{ error.message }}requestId:{{ error.requestId }}traceId:{{ error.traceId }} - 有装车照片尚未确认保存。 {{ snapshot?.photos.some(item => item.slot === slot) ? '✓' : index + 1 }} - 装车照片 {{ index + 1 }}{{ snapshot?.photos.some(item => item.slot === slot) ? '已保存 1 张' : '待上传' }} + {{ slotLabels[slot] }}{{ snapshot?.photos.some(item => item.slot === slot) ? '已保存 1 张' : '待上传' }} - + - - - 查看大图 + + + 查看大图 @@ -32,28 +31,18 @@ diff --git a/src/pages/outbound/outbound.vue b/src/pages/outbound/outbound.vue index 93e09d9..a138331 100644 --- a/src/pages/outbound/outbound.vue +++ b/src/pages/outbound/outbound.vue @@ -3,12 +3,11 @@ - 出库任务整托扫描托盘码,散箱扫描箱唛;扫描排车业务号会转入“排车拣货” + 出库任务 当前任务出库作业详情 - 任务号直接恢复;退货整托出库扫托盘码,散箱出库扫箱唛。 正在加载出库任务… @@ -17,14 +16,14 @@