feat: complete overseas customer and PDA workflows
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
| 脚本管理、全量验证、测试造数与诊断 | 仓库根 `scripts/`;服务专属脚本留在 `<service>/scripts/` | 先读 `scripts/AGENTS.md` 和 `scripts/README.md`;Admin HTTP 内部模块继续读 `scripts/admin-test-env-modules/README.md`。新增入口必须登记用途、目标环境、数据副作用和报告位置,并通过 `scripts/script-documentation-contract.test.mjs` |
|
||||
| 分柜、柜次、智能排柜、预装/装柜清单、真实柜号后补、PDA | `firstleg/` | `firstleg/AGENTS.md`,再按 `split/`、`loading/`、`outbound/` 定位;创建柜次时原子生成唯一预装计划,加入入仓码时同步建立关联,不设推送步骤;预装/正式详情由 `/loading-plans/{planId}/lifecycle` 解析。真实集装箱号不阻塞订舱关联、计划确认或 PDA task 创建,可在 `EDITING/LOADING` 首次补录,但必须在 PDA 最终完成前存在。上传以幂等键绑定包含文件内容的请求哈希,完全重放不得再次调用 File |
|
||||
| 合柜报关 / 同柜不可拆组、跨客户合柜、整组分配/移除 | `firstleg/` | `loading/coload/controller/PlatformColoadGroupController.java`、`service/ColoadGroupService.java`、`persistence/ColoadGroupMapper.java`、`src/main/resources/openapi/coload-groups-v1.yaml`;按订单建组并冻结全部可用入仓片段,仅要求同国内仓、同渠道 |
|
||||
| 全局智能装柜候选、柜次、占位与移除 | `firstleg/`、`order/` | `controller/PlatformSmartLoadingController.java` 的 `/inbound-candidates`、`/containers`、`/plans/{planId}/inbound-assignments`、`/pending-review-assignments`、`/group-assignments`、`/unpicked-items`,业务编排 `service/SmartLoadingService.java`;待审核事实由 `client/PendingReviewOrderClient.java` 查询 Order `PendingReviewOrderQueryService.java`。候选四态只用于筛选展示,资格以 `canAssign` 为准;人工加入允许跨渠道,智能推荐仍按柜次渠道;同柜组整组分页、分配和移除 |
|
||||
| 全局智能装柜候选、柜次、占位与移除 | `firstleg/`、`order/` | Firstleg `controller/PlatformSmartLoadingController.java` 的 `/inbound-candidates`、`/containers`、`/plans/{planId}/inbound-assignments`、`/pending-review-assignments`、`/group-assignments`、`/unpicked-items`,业务编排 `service/SmartLoadingService.java`;待审核事实由 `client/PendingReviewOrderClient.java` 经内部令牌查询 Order `/internal/order/v1/pending-review-orders` 与 `PendingReviewOrderQueryService.java` 的权威 `SUBMITTED/PENDING_REVIEW` 行。候选四态只用于筛选展示,资格以 `canAssign` 为准;待审核或待测量可先占位,确认预装和启动 PDA 前仍须进入 `WAIT_SPLIT`;人工加入允许跨渠道,智能推荐仍按柜次渠道;同柜组整组分页、分配和移除 |
|
||||
| 客户入库结果与头程费用确认 | `firstleg/` | `controller/MerchantInboundBillingController.java`、`service/MerchantInboundBillingService.java`、`FbaPricingService.java`,接口 `/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing` 与 `/confirm`;客户按登录态范围读取并以费用版本和幂等键确认当前 `SENT` 费用,确认后锁定 |
|
||||
| 订舱资料/打印、船期、订舱级拖车、报关 | `firstleg/` | `oceanbooking/`、`portschedule/`、`TruckAppointmentService.java`、`outbound/dispatch/`、`customs/`;拖车只绑定 `RELEASED` 订舱,不携带客户、柜次、装柜计划或真实柜号,每订舱最多一张非终态预约,订舱文件联查 `file/AGENTS.md` |
|
||||
| 客户账期、费用池、账单、入仓码联查、收款、成本、核销 | `finance/` | `finance/AGENTS.md`;客户账单行 `inboundCode` 由 `FinanceQueryService` 从费用来源快照提供,Client 复用头程入仓关系详情;费用来源和柜次成本快照再联查 `firstleg/AGENTS.md`,凭证再联查 `file/AGENTS.md` |
|
||||
@@ -51,9 +51,9 @@
|
||||
| 柜级入仓计划进度与整柜转换 | `lastleg-service/` | `orders/container-summaries` 按 `sourceContainerBookingId` 跨客户聚合,分页前排除全部入仓码均已生成有效独立计划的柜,只分页返回仍有入仓码待生成有效独立计划的柜;详情返回柜内全部订单/入仓码/计划且不受订单分页截断;`plannedArrivalAt` 只取装柜完成事件 `estimatedWarehouseArrivalAt` 冻结事实并升序、空值最后;`inbound-plans/convert-container` 锁定同柜订单,每票生成独立计划,整批原子且幂等 |
|
||||
| 海外入仓分页、FBA 预约、PDA 柜号签收与入仓 | `lastleg-service/` | 平台入口 `inboundplan/controller/InboundPlanController.java`、`InboundPlanQueryMapper.java`、`InboundFbaAppointmentService.java`;PDA 公网入口 `inboundplan/pda/controller/ReceivingContainerController.java`、`ReceivingContainerService.java`,旧 planNo/allocationId Controller 已移除 |
|
||||
| 海外仓排车查询、候选与智能预排 | `lastleg-service/` | `dispatch/controller/DispatchQueryController.java`、`SmartDispatchController.java`;草稿柜、托盘锁和预排归 `dispatch/`,只读排车和预排创建独立开关,不自动开放编辑、取消、发车、PDA 出库或派送 |
|
||||
| 五期海外排车执行、PDA 出库、派送与一件代发 | `lastleg-service/` | `dispatch/controller/DispatchController.java`、`dispatch/pda/controller/DispatchPdaController.java`、`delivery/controller/DeliveryTransferController.java`、`OutboundPdaController.java`、`MerchantDeliveryController.java`,一件代发库存归 `dropship/`;统一经 `gateway-overseas/`,设备端是否已有正式入口继续查 `../apps/pda-overseas-uniapp/AGENTS.md` |
|
||||
| 五期海外异常退回与客户回访 | `lastleg-service/` | `exceptionorder/`、`customervisit/`;平台异常建档和关系解析、派送退回入仓、派送完成/状态、回访资料与附件均以 Lastleg 自身客户范围和状态为准 |
|
||||
| 五期一件代发 SKU/箱唛与暂扣跨区协同 | `order/`、`firstleg/`、`common-event-contract/`、`lastleg-service/` | Order `InternalOrderCargoController` 提供强制 SKU;Firstleg `InboundCartonLabelService` 冻结 SKU、`LoadingWorkPalletService` 维护装柜工作托盘并消费 `OverseasInboundOrderDetainedChangedV1`;Lastleg `dropship/` 建立库存并发布暂扣变更 |
|
||||
| 海外排车执行、PDA 出库、派送与一件代发 | `lastleg-service/` | `dispatch/controller/DispatchController.java`、`dispatch/pda/controller/DispatchPdaController.java`、`delivery/controller/DeliveryTransferController.java`、`OutboundPdaController.java`、`MerchantDeliveryController.java`,一件代发库存归 `dropship/`;统一经 `gateway-overseas/`,设备端正式入口继续查 `../apps/pda-overseas-uniapp/AGENTS.md` |
|
||||
| 海外异常退回与客户回访 | `lastleg-service/` | `exceptionorder/`、`customervisit/`;平台异常建档和关系解析、派送退回入仓、派送完成/状态、回访资料与附件均以 Lastleg 自身客户范围和状态为准 |
|
||||
| 一件代发 SKU/箱唛与暂扣跨区协同 | `order/`、`firstleg/`、`common-event-contract/`、`lastleg-service/` | Order `InternalOrderCargoController` 提供强制 SKU;Firstleg `InboundCartonLabelService` 冻结 SKU、`LoadingWorkPalletService` 维护装柜工作托盘并消费 `OverseasInboundOrderDetainedChangedV1`;Lastleg `dropship/` 建立库存并发布暂扣变更 |
|
||||
| 客户海外仓贴标、转运、废弃与退货换标 | `lastleg-service/` | `relabel/`、`transfer/`、`discard/` 的 Merchant Controller/Service;Client 消费端 `../apps/client-web/src/modules/overseas-warehouse/`,统一经 `gateway-overseas/` |
|
||||
| 海外仓 PDA 盘点、移库、托盘、换标与查询 | `lastleg-service/`、`warehouse-overseas-service/` | Lastleg `pda/stocktake/`、`pda/move/`、`pda/warehouse/`、`palletops/`、`relabel/pda/`;契约 `lastleg-service/src/main/resources/openapi/overseas-pda-operations-v1.yaml`;物理托盘流转与补偿进入 Warehouse `InternalPdaPalletTransitionController`,设备端入口是否开放以 `../apps/pda-overseas-uniapp/AGENTS.md` 为准 |
|
||||
| 国内船节点向海外接柜门禁事件 | `firstleg/`、`common-event-contract/`、`lastleg-service/` | Firstleg `portschedule/service/CarrierScheduleNodeOutboxService.java` 发布 `FirstLegContainerShippingNodeRecordedV1`;Lastleg `shipping/service/ContainerShippingNodeConsumer.java` 单调消费,只有实际 `PICKUP_READY` 及以后节点允许海外 PDA 候选进入可收货 |
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
## 全量验收路由
|
||||
|
||||
- 当前一期、二期、三期及财务统一进入全量验收;按本次链路读取对应阶段/专题文档,不以历史阶段终点阻断跨服务联调或回归。
|
||||
- 当前订单与客户、入库测量与计费、国内货柜履约、海外仓业务及财务统一进入全量验收;按本次链路读取对应业务域/专题文档,不以历史规划终点阻断跨服务联调或回归。
|
||||
- `auth`、`iam`、`warehouse`、`order`、`firstleg`、`file`、`finance`、`integration-service` 及出仓相关能力按各自模块契约协作;状态、权限、数据库和 OpenAPI 以当前实现及已确认契约为准。
|
||||
|
||||
## 国内头程与海外仓协同
|
||||
@@ -97,14 +97,14 @@
|
||||
- 后端同样遵守根指引中的“开发数据和上传文件可丢弃”边界:不得为现有开发库孤儿文件、旧文件归属或历史部分失败新增删除补偿接口、兼容复用、回填或清理链路;只修复当前正式上传与持久化路径,既有开发文件由人工重新上传。
|
||||
- 通用配置在 `config/nacos/{local,cn}/`;服务日志、上传文件、`target/`、`reports/` 不属于源码。
|
||||
- Java 签名变更的最低编译门禁是 `mvn -pl <module> -am test-compile`,它必须在部署前完成并覆盖测试源码;有行为变化时继续运行 `mvn -pl <module> -am test`。`-DskipTests` 可跳过测试执行但仍须保留测试编译,`-Dmaven.test.skip=true` 会跳过测试编译,不得作为签名变更的交付验证。全量验证仍使用 `mvn -DskipTests validate`、`mvn test`;不要在本地启动 Java/Nacos/数据库,除非用户明确要求。
|
||||
- `AGENTS.md` 索引维护完成后运行 `./scripts/agents-index-contract.ps1`;脚本只读源码目录和指定指引,验证根快速表唯一性、部署单元、Client active 路由、四期范围漂移、仓库主档唯一权威、订舱关联候选与柜量语义、整柜出仓与船节点单调性、海外 Gateway 覆盖、PDA 运行配置/独立更新/语言预留、人工分段验收链、Lastleg 海外安全投影、同步 Trace、模块覆盖、关键入口与排除目录,不访问 HTTP、数据库或部署环境。
|
||||
- `AGENTS.md` 索引维护完成后运行 `./scripts/agents-index-contract.ps1`;脚本只读源码目录和指定指引,验证根快速表唯一性、部署单元、Client active 路由、海外仓业务范围漂移、仓库主档唯一权威、订舱关联候选与柜量语义、整柜出仓与船节点单调性、海外 Gateway 覆盖、PDA 运行配置/独立更新/语言预留、人工分段验收链、Lastleg 海外安全投影、同步 Trace、模块覆盖、关键入口与排除目录,不访问 HTTP、数据库或部署环境。
|
||||
- Mapper 注册静态合同为 `scripts/mapper-scan-contract.mjs`、`mapper-scan-contract.test.mjs`:自动发现聚合模块、生产 `*Mapper` 接口、服务启动类和分散扫描配置,不启动 Spring、不连接数据库;遗漏注解、扫描根漂移或重新引入子域扫描时直接失败并输出类与文件路径。
|
||||
- 单服务镜像构建/部署从 `Jenkinsfile.service`、`deploy/docker/Dockerfile`、`deploy/scripts/deploy-built-service.sh` 进入;新镜像启动失败或健康检查失败时只允许回滚到同服务先前镜像,并保持任务失败,离线合同见 `scripts/deploy-built-service-rollback.test.mjs`。实际部署仍须当前对话明确授权。
|
||||
|
||||
## 全量 HTTP 验收脚本路由
|
||||
|
||||
- 脚本管理的唯一总索引是 `scripts/README.md`,最近执行规则是 `scripts/AGENTS.md`。仓库级脚本、Admin 内部模块、服务局部合同和部署脚本的目录归属以这两个文件为准,不再通过无范围搜索猜测入口。
|
||||
- 全量验收唯一指向测试环境 HTTP:平台端执行 `scripts/admin-test-env-acceptance.mjs`,客户端执行 `scripts/client-test-env-acceptance.mjs`;二期专项场景才单独执行 `scripts/client-phase2-test-env-acceptance.mjs`,不替代客户端全量入口。
|
||||
- 全量验收唯一指向测试环境 HTTP:平台端执行 `scripts/admin-test-env-acceptance.mjs`,客户端执行 `scripts/client-test-env-acceptance.mjs`;入库测量与计费专项场景才单独执行 `scripts/client-phase2-test-env-acceptance.mjs`,不替代客户端全量入口。
|
||||
- 固定客户待审核订单使用 `scripts/create-pending-review-orders.mjs`;固定“IT测试”与“义乌仓”、随机真实线路并推进至 `WAIT_SPLIT` 使用 `scripts/it-test-wait-split-bulk.mjs`。二者都是造数入口,不属于全量验证;会写目标环境且不自动清理成功数据。
|
||||
- Admin 总入口默认按模块列表覆盖平台全链路;需要定位单个业务时使用 `--modules <name>`,模块名称和静态合同先查 `scripts/admin-test-env-modules/README.md`。
|
||||
- 这些脚本会创建、更新或最终停用测试数据,必须在当前对话明确授权测试环境验收后执行,并保存 JSON 报告;未授权不得请求 Gateway。
|
||||
@@ -113,13 +113,13 @@
|
||||
- HTTP 全量通过后,才进入 Admin/Client 真实页面调用、浏览器样式和 PDA 页面/设备验收;自动化 HTTP 通过不代表视觉验收完成。
|
||||
- 修改脚本目录、入口或说明后,至少运行 `node --test scripts/script-documentation-contract.test.mjs`;修改 `AGENTS.md` 后继续运行 `./scripts/agents-index-contract.ps1`。两者均为离线只读合同,不能代替真实 HTTP 验收。
|
||||
|
||||
## 阶段文档入口
|
||||
## 业务域文档入口
|
||||
|
||||
- 一期:`../docs/一期/README.md`、`../docs/一期/一期头程订单闭环设计.md`
|
||||
- 二期:`../docs/二期/README.md`、`../docs/二期/二期需求优化版-v2-测量计费入库主流程.md`、需求确认清单及联调报告
|
||||
- 三期:`../docs/三期/README.md`、`../docs/三期/三期开工门禁.md`、`../docs/三期/三期开发规划.md`、`../docs/三期/三期需求确认表.md`、页面与原型清单
|
||||
- 四期:`../docs/四期/README.md`;海外入口 `gateway-overseas/`,尾程与入仓库存 `lastleg-service/`,海外仓储资源 `warehouse-overseas-service/`
|
||||
- 五期:当前以本轮确认的正式 Controller、前端路由、权限、SQL 与跨区事件契约为范围依据;海外排车执行、出库、派送、一件代发、异常退回和客户回访进入 `lastleg-service/`,国内 SKU/箱唛及暂扣协同进入 `order/`、`firstleg/`、`common-event-contract/`
|
||||
- 订单与客户:`../docs/订单与客户/README.md`、`../docs/订单与客户/订单与客户头程订单闭环设计.md`
|
||||
- 入库测量与计费:`../docs/入库测量与计费/README.md`、`../docs/入库测量与计费/入库测量与计费需求优化版-v2-测量计费入库主流程.md`、需求确认清单及联调报告
|
||||
- 国内货柜履约:`../docs/国内货柜履约/README.md`、`../docs/国内货柜履约/国内货柜履约开工门禁.md`、`../docs/国内货柜履约/国内货柜履约开发规划.md`、`../docs/国内货柜履约/国内货柜履约需求确认表.md`、页面与原型清单
|
||||
- 海外仓业务:`../docs/海外仓业务/README.md`;海外入口 `gateway-overseas/`,尾程与入仓库存 `lastleg-service/`,海外仓储资源 `warehouse-overseas-service/`
|
||||
- 海外仓全流程:以正式 Controller、前端路由、权限、SQL 与跨区事件契约为范围依据;海外接柜、入仓、库存、库内服务、排车、出库、派送、一件代发、异常退回和客户回访进入 `lastleg-service/`,国内 SKU/箱唛及暂扣协同进入 `order/`、`firstleg/`、`common-event-contract/`
|
||||
- 财务:`../docs/财务管理/README.md` 中已确认的范围、对象、状态和接口约束
|
||||
|
||||
全量验收任务再读取对应阶段进度表、验收报告和证据矩阵;普通代码定位只读取当前链路涉及的范围、契约、模块指引和代码。
|
||||
|
||||
@@ -27,7 +27,7 @@ mvn test
|
||||
|
||||
GitLab 在独立 Node 镜像中自动执行拓扑合同,随后才允许服务制品打包。Maven `verify` 会为执行过测试且产生覆盖率数据的模块生成 `target/site/jacoco/index.html` 与 `jacoco.xml`;当前只生成报告,不设置覆盖率阈值,也不改变测试选择。Jenkins 的 `java-builder` 尚未在仓库中声明 Node 运行时,因此暂不重复执行该 Node 合同,以 GitLab 门禁和上述本地命令为准。
|
||||
|
||||
2026-07-30 一期前端联调缺口已完成真实回归:19 个 Reactor 项目构建成功,75 项单元及组件测试、68 项认证与 IAM 真实 API、37 项订单与仓库真实 API 全部通过。详细证据见 `docs/phase1-business-maturity-regression-report.md`。
|
||||
2026-07-30 订单与客户前端联调缺口已完成真实回归:19 个 Reactor 项目构建成功,75 项单元及组件测试、68 项认证与 IAM 真实 API、37 项订单与仓库真实 API 全部通过。详细证据见 `docs/phase1-business-maturity-regression-report.md`。
|
||||
|
||||
国内本地联调启动顺序为:IAM、Auth、Firstleg、Order、Warehouse、File、Print,最后启动 Gateway。往来单位不是独立服务,由 `warehouse-service` 承载并保存到 `baiyue_warehouse.wh_partner`。海外服务通过独立 Compose 与 Nacos 配置启动,不与国内服务共享数据库或配置命名空间。
|
||||
|
||||
|
||||
+1
-1
@@ -35,4 +35,4 @@
|
||||
|
||||
## 全量验收导航
|
||||
|
||||
一期、二期、三期及财务全量验收均复用本模块认证能力;按本次链路读取对应阶段/专题文档,重点核对 Token、应用域、客户隔离、失效和跨 Auth/IAM 补偿,不因历史阶段划分排除其他消费者。
|
||||
订单与客户、入库测量与计费、国内货柜履约、海外仓业务及财务全量验收均复用本模块认证能力;按本次链路读取对应业务域/专题文档,重点核对 Token、应用域、客户隔离、失效和跨 Auth/IAM 补偿,不因历史规划边界排除其他消费者。
|
||||
|
||||
+17
-17
@@ -6,7 +6,7 @@
|
||||
- 海外区域固定增量入口:`海外区域统一增量-MySQL8.sql`;空库先分别执行 `lastleg-service/src/main/resources/db/lastleg.sql`与 `warehouse-overseas-service/src/main/resources/db/warehouse.sql` 基线,再执行该入口。固定入口会创建并校验独立的 `baiyue_event_bridge_overseas`,再升级 `baiyue_lastleg`与 `baiyue_lastlag_warehouse`;IAM 安全事件 Inbox 和可恢复投影已并入 `baiyue_lastleg.ll_security_*`,Redis 只作可重建镜像。
|
||||
- Lastleg 基线和海外固定增量仅确定性初始化国内固定 `platform_admin`(`authUserId=1000001`、`tenantId=0`)的 USER/AUTHORIZATION version=0 安全投影;其他既有开发账号不建立兼容表或长期补投入口。
|
||||
- 国内、海外 Compose 部署默认设置 `SPRING_SQL_INIT_MODE=never`,执行任一 SQL 均需用户单独授权。
|
||||
- 该脚本汇总一期、二期及已实现三期出仓能力的数据库基线、存量升级、IAM 权限资源和最终结构校验,适用于 MySQL 8.0。
|
||||
- 该脚本汇总订单与客户、入库测量与计费及已实现国内货柜履约出仓能力的数据库基线、存量升级、IAM 权限资源和最终结构校验,适用于 MySQL 8.0。
|
||||
- 正式上线前的数据库变更直接维护该文件中的当前基线、存量兼容处理和最终校验,不再创建按日期拆分的发布 SQL;正式上线后的迁移策略在生产发布规范中另行冻结。
|
||||
- 当前测试环境尚未上线,船期价格表相关表、字段和索引只维护在现有初始化建表定义及本固定入口中,不向 `phase1-order-extension.sql`、`phase2-fba-pricing-migration.sql` 或新的增量脚本追加同一结构。
|
||||
- 模块 `src/main/resources/db/` 下的 SQL 用于开发溯源;`deploy/sql/` 下已有日期文件全部冻结为历史记录,不再单独部署,也不得与固定入口重复执行。
|
||||
@@ -32,9 +32,9 @@
|
||||
- 不包含钱包、发票、供应商在线应付、银行自动对账、复杂核销、汇率和月结。
|
||||
- 统一脚本末尾会强校验 13 张 Finance 表、客户隔离/CNY/人工凭证关键字段,以及费用来源、Inbox、账单版本快照、收款、成本和写操作幂等唯一约束;任一缺失不会返回 `PASS`。
|
||||
|
||||
## 历史一期回归脚本
|
||||
## 历史订单与客户回归脚本
|
||||
|
||||
- 历史文件:`2026-07-31一期回归测试环境增量迁移.sql`,仅供追溯,不再作为统一入口。
|
||||
- 历史文件:`2026-07-31订单与客户回归测试环境增量迁移.sql`,仅供追溯,不再作为统一入口。
|
||||
- 适用数据库:MySQL 8.0。
|
||||
- 执行账号需要对 `baiyue_iam`、`baiyue_warehouse`、`baiyue_order`、`baiyue_firstleg` 具备建表、改表、建索引、创建临时存储过程和数据写入权限。
|
||||
- 脚本不会创建数据库,不会清理或删除业务数据,也不会写入演示租户、演示用户、演示仓库、演示往来单位或快递公司样本。
|
||||
@@ -63,13 +63,13 @@
|
||||
|
||||
执行结束后必须看到 `migration_status = PASS`。如果中途出现 `SQLSTATE 45000`,必须按错误中给出的缺失表、字段或权限处理后重新完整执行。
|
||||
|
||||
## 三期分柜、预装、拣货与装柜当前基线
|
||||
## 国内货柜履约分柜、预装、拣货与装柜当前基线
|
||||
|
||||
- 模块表结构来源:`firstleg/src/main/resources/db/phase3-split-preload-schema.sql`、`firstleg/src/main/resources/db/phase3-loading-delivery-schema.sql`。
|
||||
- 三期平台权限直接维护在统一初始化与发布入口中,避免额外增加独立权限脚本。
|
||||
- 国内货柜履约平台权限直接维护在统一初始化与发布入口中,避免额外增加独立权限脚本。
|
||||
- 测试和生产环境仍只执行固定入口 `头程数据库统一增量-MySQL8.sql`,不得再串行执行上述模块脚本。
|
||||
- 必须先执行统一增量并取得 `PASS`,再启动或重启 Firstleg Service;否则统一订单摘要查询会因缺少三期操作表而失败。
|
||||
- 统一脚本会创建智能装柜任务 `fl_loading_pick_task`,并把历史入库汇总任务按字段签名无损迁移到 `fl_inbound_loading_pick_task`;未知混合结构会直接终止。`WAIT_SPLIT` 是二期有效终点,脚本不会改写主表或状态历史。
|
||||
- 必须先执行统一增量并取得 `PASS`,再启动或重启 Firstleg Service;否则统一订单摘要查询会因缺少国内货柜履约操作表而失败。
|
||||
- 统一脚本会创建智能装柜任务 `fl_loading_pick_task`,并把历史入库汇总任务按字段签名无损迁移到 `fl_inbound_loading_pick_task`;未知混合结构会直接终止。`WAIT_SPLIT` 是入库测量与计费有效终点,脚本不会改写主表或状态历史。
|
||||
- 下载中心固定使用 `5103040-5103041`,库位权限迁移到 `5103060-5103064`;入库装柜权限迁移到 `5102121-5102129`,迁移会按 `permission_code` 保留角色授权,目标 ID 被其他语义占用时直接终止。
|
||||
|
||||
部署前可只读盘点:
|
||||
@@ -123,11 +123,11 @@ SELECT COUNT(*) = 8 AS scheduler_columns_ready
|
||||
|
||||
任一结果不是 1,禁止启动 File Service;只能继续追加兼容性增量修复,不得删除存量导出任务。
|
||||
|
||||
## 二期第三方测量增量
|
||||
## 入库测量与计费第三方测量增量
|
||||
|
||||
- 模块脚本:`firstleg/src/main/resources/db/phase2-third-party-single-measurement-migration.sql`。
|
||||
- 历史二期脚本:`2026-08-04-二期入库新增表字段-MySQL8一键执行.sql` 已冻结,仅供追溯。
|
||||
- 当前固定入口 `头程数据库统一增量-MySQL8.sql` 已内联该增量以及订单船司字段、二期 28 项 IAM 权限和最新仓储资源租户字段。
|
||||
- 历史入库测量与计费脚本:`2026-08-04-入库新增表字段-MySQL8一键执行.sql` 已冻结,仅供追溯。
|
||||
- 当前固定入口 `头程数据库统一增量-MySQL8.sql` 已内联该增量以及订单船司字段、入库测量与计费 28 项 IAM 权限和最新仓储资源租户字段。
|
||||
- 新增 `external_request_hash`、`inbound_detail_id`、`external_detail_no` 和第三方明细唯一索引,用于一条回传绑定一条订单货物明细及一张文件中心图片。
|
||||
- 部署 Firstleg 新版本前必须完整执行固定入口,并确认末尾总体验证为 `PASS`。
|
||||
|
||||
@@ -221,23 +221,23 @@ SELECT
|
||||
- IAM 新增 `platform:overseas-storage-fee:list/create/edit/delete` 四项权限,默认授予平台系统管理员。
|
||||
- Warehouse Service 启动前必须先执行固定入口;末尾只有两张表、五个索引和四项权限均存在才返回 `PASS`。
|
||||
|
||||
## 四期海外权限目录
|
||||
## 海外仓权限目录
|
||||
|
||||
- 固定入口已同步 `iam/src/main/resources/db/lastleg-permissions.sql` 的 `platform:overseas-*` 权限,并默认授予平台系统管理员。
|
||||
- 旧 `platform:lastleg:*` 权限会撤销角色关联并停用;不保留旧码兼容映射。
|
||||
- 四期目录保留海外出库、派送列表入口;五期写操作和客户派送权限由后续五期区块直接收敛,不使用旧码兼容映射。
|
||||
- 海外仓业务目录保留海外出库、派送列表入口;海外仓出库与派送写操作和客户派送权限由后续海外仓出库与派送区块直接收敛,不使用旧码兼容映射。
|
||||
|
||||
## 五期海外排车、出库与派送权限
|
||||
## 海外排车、出库与派送权限
|
||||
|
||||
- 国内固定入口已在四期 `lastleg-permissions.sql` 权限段之后完整内联 `iam/src/main/resources/db/phase5-delivery-permissions.sql`;测试和部署只执行固定入口,不再单独执行模块脚本。
|
||||
- 国内固定入口已在海外仓业务 `lastleg-permissions.sql` 权限段之后完整内联 `iam/src/main/resources/db/phase5-delivery-permissions.sql`;测试和部署只执行固定入口,不再单独执行模块脚本。
|
||||
- 平台端固定使用排车列表/编辑/取消/转派送、出库列表/创建/取消、派送列表/退回/状态、异常订单列表/创建和客户回访列表/编辑十四项权限;列表权限包含详情及只读辅助接口,写操作保持独立授权。
|
||||
- 客户端固定使用 `merchant:warehouse:delivery:list` 和 `merchant:warehouse:delivery:create`;客户默认管理员获得两项权限,其他客户角色不自动扩权。
|
||||
- `WAREHOUSE_OPERATOR` 获得十四项后台作业权限,`WAREHOUSE_TALLY` 只获得排车、出库、派送、异常订单和客户回访五项列表只读;国内、海外 PDA 岗位不得获得上述后台权限。
|
||||
|
||||
## 五期海外排车与出库结构升级
|
||||
## 海外排车与出库结构升级
|
||||
|
||||
- 五期数据库部署仍只执行 `海外区域统一增量-MySQL8.sql`;空库先执行 Lastleg 与 Warehouse Overseas 当前基线,再完整执行固定入口并确认最末 `migration_status = PASS`。
|
||||
- 该海外固定入口会在任何五期 `ALTER` 与数据门禁前幂等创建排车 PDA、出库、派送、一件代发、异常订单和客户回访的完整当前结构,因此既有四期库不依赖模块基线或历史 `scripts/db/incremental.sql`。国内固定入口只保留 IAM 五期权限目录,不创建或修改 `baiyue_lastleg` 业务表。
|
||||
- 海外仓出库与派送数据库部署仍只执行 `海外区域统一增量-MySQL8.sql`;空库先执行 Lastleg 与 Warehouse Overseas 当前基线,再完整执行固定入口并确认最末 `migration_status = PASS`。
|
||||
- 该海外固定入口会在任何海外仓出库与派送 `ALTER` 与数据门禁前幂等创建排车 PDA、出库、派送、一件代发、异常订单和客户回访的完整当前结构,因此既有海外仓业务库不依赖模块基线或历史 `scripts/db/incremental.sql`。国内固定入口只保留 IAM 海外仓出库与派送权限目录,不创建或修改 `baiyue_lastleg` 业务表。
|
||||
- 固定入口只补目标字段并做 fail-fast 校验,不回填、改号、清理或从其他表推断开发旧行。`ll_dispatch_pda_task.warehouse_id` 存在 `NULL`、空值或 `0`,或任一待收敛全局业务号/任务号、完成幂等键发生冲突时,脚本立即 `SIGNAL`;应重新造数,重置或清理仍需用户另行授权。
|
||||
- 数据门禁通过后,脚本原位把 `dispatch_no`、`outbound_no`、排车 PDA `task_no`、出库 PDA `task_no` 收敛为全局唯一,并建立 `(customer_id, completion_idempotency_key)` 完成命令唯一约束;旧客户复合索引不保留。
|
||||
- 必须在启动或重启 Lastleg Service 前完成固定入口;不得单独执行模块基线补丁、日期脚本或手工兼容 SQL。
|
||||
|
||||
+101
-49
@@ -16628,15 +16628,17 @@ ALTER TABLE fl_inbound_outbox
|
||||
MODIFY COLUMN payload_hash VARCHAR(71) NULL COMMENT '事件规范化负载SHA-256;标准跨区事件含sha256:前缀';
|
||||
ALTER TABLE fl_inbound_outbox
|
||||
MODIFY COLUMN payload_json LONGTEXT NOT NULL COMMENT '事件负载快照;文本类型保留跨区事件摘要所依赖的数值词法';
|
||||
-- 四期海外权限统一目录(来源:iam/db/lastleg-permissions.sql)
|
||||
-- 海外权限统一目录(来源:iam/db/lastleg-permissions.sql)
|
||||
USE baiyue_iam;
|
||||
|
||||
-- 四期 Admin 海外权限只使用 platform:overseas-*;列表权限包含详情只读。
|
||||
DELETE rp FROM baiyue_iam.iam_role_permission rp
|
||||
JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id
|
||||
DELETE rp FROM iam_role_permission rp
|
||||
JOIN iam_permission p ON p.id=rp.permission_id
|
||||
WHERE p.permission_code LIKE 'platform:lastleg:%';
|
||||
UPDATE baiyue_iam.iam_permission SET status='INACTIVE', deleted=1
|
||||
UPDATE iam_permission SET status='INACTIVE', deleted=1
|
||||
WHERE permission_code LIKE 'platform:lastleg:%';
|
||||
|
||||
INSERT INTO baiyue_iam.iam_permission
|
||||
INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code,
|
||||
permission_name, route_path, http_method, sort_no, status, built_in, deleted)
|
||||
VALUES
|
||||
@@ -16696,57 +16698,60 @@ VALUES
|
||||
(5107221,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:create','创建车辆主数据',NULL,'POST',921,'ACTIVE',1,0),
|
||||
(5107222,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:edit','编辑车辆主数据',NULL,'PUT',922,'ACTIVE',1,0),
|
||||
(5107223,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:status','变更车辆状态',NULL,'PATCH',923,'ACTIVE',1,0),
|
||||
(5107224,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:export','导出车辆主数据',NULL,'GET',924,'ACTIVE',1,0)
|
||||
(5107224,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:export','导出车辆主数据',NULL,'GET',924,'ACTIVE',1,0),
|
||||
(5306100,0,'MERCHANT','MENU',0,'merchant:cross-border:procurement','海外采购','/cross-border/procurement',NULL,70,'ACTIVE',1,0),
|
||||
(5306110,0,'MERCHANT','MENU',0,'merchant:cross-border:domestic-trucking','美国本土卡','/cross-border/domestic-trucking',NULL,80,'ACTIVE',1,0),
|
||||
(5306120,0,'MERCHANT','MENU',0,'merchant:cross-border:pallet-shipping','美本土散板','/cross-border/pallet-shipping',NULL,90,'ACTIVE',1,0),
|
||||
(5306130,0,'MERCHANT','MENU',0,'merchant:warehouse:inbound:list','海外仓入库订单','/orders/inbound',NULL,50,'ACTIVE',1,0)
|
||||
ON DUPLICATE KEY UPDATE permission_name=VALUES(permission_name), route_path=VALUES(route_path),
|
||||
http_method=VALUES(http_method), sort_no=VALUES(sort_no), status='ACTIVE', built_in=1, deleted=0;
|
||||
|
||||
INSERT IGNORE INTO baiyue_iam.iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, role.id, permission.id, NOW(3)
|
||||
FROM baiyue_iam.iam_role role
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
FROM iam_role role
|
||||
JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
AND permission.id BETWEEN 5107000 AND 5107299 AND permission.permission_code<>'lastleg:pda:operate'
|
||||
WHERE role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='PLATFORM_ADMIN'
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||
|
||||
DELETE role_permission FROM baiyue_iam.iam_role_permission role_permission
|
||||
JOIN baiyue_iam.iam_role role ON role.id=role_permission.role_id AND role.tenant_id=role_permission.tenant_id
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.id=role_permission.permission_id AND permission.tenant_id=role_permission.tenant_id
|
||||
DELETE role_permission FROM iam_role_permission role_permission
|
||||
JOIN iam_role role ON role.id=role_permission.role_id AND role.tenant_id=role_permission.tenant_id
|
||||
JOIN iam_permission permission ON permission.id=role_permission.permission_id AND permission.tenant_id=role_permission.tenant_id
|
||||
WHERE permission.permission_code='lastleg:pda:operate'
|
||||
AND NOT (role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR');
|
||||
|
||||
INSERT IGNORE INTO baiyue_iam.iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, role.id, permission.id, NOW(3)
|
||||
FROM baiyue_iam.iam_role role
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
FROM iam_role role
|
||||
JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
AND permission.permission_code='lastleg:pda:operate'
|
||||
WHERE role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR'
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||
|
||||
DROP PROCEDURE IF EXISTS baiyue_iam.assert_overseas_pda_picker_permission;
|
||||
DROP PROCEDURE IF EXISTS assert_overseas_pda_picker_permission;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE baiyue_iam.assert_overseas_pda_picker_permission()
|
||||
CREATE PROCEDURE assert_overseas_pda_picker_permission()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission grant_row
|
||||
JOIN baiyue_iam.iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.id=grant_row.permission_id AND permission.tenant_id=grant_row.tenant_id
|
||||
IF (SELECT COUNT(*) FROM iam_role_permission grant_row
|
||||
JOIN iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
|
||||
JOIN iam_permission permission ON permission.id=grant_row.permission_id AND permission.tenant_id=grant_row.tenant_id
|
||||
WHERE permission.permission_code='lastleg:pda:operate'
|
||||
AND role.id=4000005 AND role.app_code='PLATFORM' AND role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR') <> 1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外PDA权限初始化失败:海外仓 PDA 作业员未精确授权';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM baiyue_iam.iam_role_permission grant_row
|
||||
JOIN baiyue_iam.iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.id=grant_row.permission_id AND permission.tenant_id=grant_row.tenant_id
|
||||
IF EXISTS (SELECT 1 FROM iam_role_permission grant_row
|
||||
JOIN iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
|
||||
JOIN iam_permission permission ON permission.id=grant_row.permission_id AND permission.tenant_id=grant_row.tenant_id
|
||||
WHERE permission.permission_code='lastleg:pda:operate'
|
||||
AND NOT (role.id=4000005 AND role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR')) THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外PDA权限初始化失败:存在非海外仓 PDA 作业员授权';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL baiyue_iam.assert_overseas_pda_picker_permission();
|
||||
DROP PROCEDURE baiyue_iam.assert_overseas_pda_picker_permission;
|
||||
CALL assert_overseas_pda_picker_permission();
|
||||
DROP PROCEDURE assert_overseas_pda_picker_permission;
|
||||
|
||||
-- 四期仓库后台角色严格最小授权:list 同时覆盖列表与详情,只读角色不得获得任何写权限。
|
||||
USE baiyue_iam;
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_phase4_warehouse_role_permission;
|
||||
CREATE TEMPORARY TABLE tmp_phase4_warehouse_role_permission (
|
||||
role_code VARCHAR(64) NOT NULL,
|
||||
@@ -16775,9 +16780,9 @@ INSERT INTO tmp_phase4_warehouse_role_permission (role_code, permission_code) VA
|
||||
('WAREHOUSE_TALLY','platform:overseas-business:truck:list'),
|
||||
('WAREHOUSE_TALLY','platform:overseas-business:looseboard:list');
|
||||
|
||||
DELETE grant_row FROM baiyue_iam.iam_role_permission grant_row
|
||||
JOIN baiyue_iam.iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.id=grant_row.permission_id AND permission.tenant_id=grant_row.tenant_id
|
||||
DELETE grant_row FROM iam_role_permission grant_row
|
||||
JOIN iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
|
||||
JOIN iam_permission permission ON permission.id=grant_row.permission_id AND permission.tenant_id=grant_row.tenant_id
|
||||
LEFT JOIN tmp_phase4_warehouse_role_permission expected
|
||||
ON expected.role_code=role.role_code AND expected.permission_code=permission.permission_code
|
||||
WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
|
||||
@@ -16785,18 +16790,18 @@ WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
|
||||
AND permission.permission_code LIKE 'platform:overseas-%'
|
||||
AND expected.permission_code IS NULL;
|
||||
|
||||
INSERT IGNORE INTO baiyue_iam.iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, role.id, permission.id, NOW(3)
|
||||
FROM tmp_phase4_warehouse_role_permission expected
|
||||
JOIN baiyue_iam.iam_role role ON role.tenant_id=0 AND role.app_code='PLATFORM'
|
||||
JOIN iam_role role ON role.tenant_id=0 AND role.app_code='PLATFORM'
|
||||
AND role.role_code=expected.role_code AND role.status='ACTIVE' AND role.deleted=0
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
AND permission.permission_code=expected.permission_code
|
||||
AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||
|
||||
DROP PROCEDURE IF EXISTS baiyue_iam.assert_phase4_warehouse_role_minimum;
|
||||
DROP PROCEDURE IF EXISTS assert_phase4_warehouse_role_minimum;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE baiyue_iam.assert_phase4_warehouse_role_minimum()
|
||||
CREATE PROCEDURE assert_phase4_warehouse_role_minimum()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM tmp_phase4_warehouse_role_permission) <> 20 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='四期仓库角色最小权限矩阵数量异常';
|
||||
@@ -16806,25 +16811,25 @@ BEGIN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='四期仓库只读角色错误获得写权限';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM tmp_phase4_warehouse_role_permission expected
|
||||
LEFT JOIN baiyue_iam.iam_role role ON role.tenant_id=0 AND role.app_code='PLATFORM'
|
||||
LEFT JOIN iam_role role ON role.tenant_id=0 AND role.app_code='PLATFORM'
|
||||
AND role.role_code=expected.role_code AND role.status='ACTIVE' AND role.deleted=0
|
||||
LEFT JOIN baiyue_iam.iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
LEFT JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
AND permission.permission_code=expected.permission_code AND permission.status='ACTIVE' AND permission.deleted=0
|
||||
LEFT JOIN baiyue_iam.iam_role_permission grant_row ON grant_row.tenant_id=0
|
||||
LEFT JOIN iam_role_permission grant_row ON grant_row.tenant_id=0
|
||||
AND grant_row.role_id=role.id AND grant_row.permission_id=permission.id
|
||||
WHERE grant_row.role_id IS NULL) THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='四期仓库角色最小权限授权不完整';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL baiyue_iam.assert_phase4_warehouse_role_minimum();
|
||||
DROP PROCEDURE baiyue_iam.assert_phase4_warehouse_role_minimum;
|
||||
CALL assert_phase4_warehouse_role_minimum();
|
||||
DROP PROCEDURE assert_phase4_warehouse_role_minimum;
|
||||
DROP TEMPORARY TABLE tmp_phase4_warehouse_role_permission;
|
||||
|
||||
INSERT IGNORE INTO baiyue_iam.iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, role.id, permission.id, NOW(3)
|
||||
FROM baiyue_iam.iam_role role
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
FROM iam_role role
|
||||
JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
AND permission.permission_code IN (
|
||||
'platform:overseas-warehouse:inbound-order:fba-appointment',
|
||||
'platform:overseas-warehouse:inbound-order:smart-dispatch'
|
||||
@@ -16833,10 +16838,57 @@ WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
|
||||
AND UPPER(role.role_code)='PLATFORM_CUSTOM_ADMIN'
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||
|
||||
INSERT IGNORE INTO baiyue_iam.iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
-- 客户默认管理员获得当前客户海外业务入口;其他客户角色按企业实际分工授权。
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, role.id, permission.id, NOW(3)
|
||||
FROM baiyue_iam.iam_role role
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
FROM iam_role role
|
||||
JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code='MERCHANT'
|
||||
AND permission.permission_code IN (
|
||||
'merchant:cross-border:procurement',
|
||||
'merchant:cross-border:domestic-trucking',
|
||||
'merchant:cross-border:pallet-shipping',
|
||||
'merchant:warehouse:inbound:list'
|
||||
)
|
||||
WHERE role.tenant_id=0 AND role.app_code='MERCHANT' AND role.role_code='MERCHANT_OWNER'
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||
|
||||
DROP PROCEDURE IF EXISTS assert_overseas_merchant_permissions;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE assert_overseas_merchant_permissions()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM iam_permission
|
||||
WHERE tenant_id=0 AND app_code='MERCHANT' AND status='ACTIVE' AND deleted=0
|
||||
AND permission_code IN (
|
||||
'merchant:cross-border:procurement',
|
||||
'merchant:cross-border:domestic-trucking',
|
||||
'merchant:cross-border:pallet-shipping',
|
||||
'merchant:warehouse:inbound:list')) <> 4 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外客户权限初始化失败:权限资源不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM iam_role_permission grant_row
|
||||
JOIN iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
|
||||
JOIN iam_permission permission ON permission.id=grant_row.permission_id
|
||||
WHERE role.tenant_id=0 AND role.app_code='MERCHANT' AND role.role_code='MERCHANT_OWNER'
|
||||
AND role.status='ACTIVE' AND role.deleted=0
|
||||
AND permission.tenant_id=0 AND permission.app_code='MERCHANT'
|
||||
AND permission.status='ACTIVE' AND permission.deleted=0
|
||||
AND permission.permission_code IN (
|
||||
'merchant:cross-border:procurement',
|
||||
'merchant:cross-border:domestic-trucking',
|
||||
'merchant:cross-border:pallet-shipping',
|
||||
'merchant:warehouse:inbound:list')) <> 4 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外客户权限初始化失败:客户默认管理员授权不完整';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL assert_overseas_merchant_permissions();
|
||||
DROP PROCEDURE assert_overseas_merchant_permissions;
|
||||
|
||||
-- Tail-leg order actions are available to the current platform administrator role.
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, role.id, permission.id, NOW(3)
|
||||
FROM iam_role role
|
||||
JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
AND permission.permission_code IN (
|
||||
'platform:overseas-warehouse:last-mile-order:edit',
|
||||
'platform:overseas-warehouse:inbound-order:create'
|
||||
@@ -16846,10 +16898,10 @@ WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||
|
||||
-- PLATFORM_CUSTOM_ADMIN needs access to the overseas outbound and delivery lists shown in the admin navigation.
|
||||
INSERT IGNORE INTO baiyue_iam.iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, role.id, permission.id, NOW(3)
|
||||
FROM baiyue_iam.iam_role role
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
FROM iam_role role
|
||||
JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
AND permission.permission_code IN (
|
||||
'platform:overseas-warehouse:outbound:list',
|
||||
'platform:overseas-warehouse:delivery:list'
|
||||
@@ -16858,7 +16910,7 @@ WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
|
||||
AND UPPER(role.role_code)='PLATFORM_CUSTOM_ADMIN'
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||
|
||||
-- 五期海外排车、出库与派送权限(来源:iam/db/phase5-delivery-permissions.sql)
|
||||
-- 海外排车、出库与派送权限(来源:iam/db/phase5-delivery-permissions.sql)
|
||||
USE baiyue_iam;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
5. 平台身份只检查 `app_code=PLATFORM`,部分 Gateway/模块守卫没有再次断言登录主体 `tenant_id=0`;客户身份也应断言 `tenant_id>0`。
|
||||
6. 平台代客业务审计当前可能记录平台主体 `tenant_id=0`,导致按目标客户查询审计日志漏记;应同时保留操作者作用域和目标客户归属。
|
||||
7. Integration 内部令牌存在可预测默认值,必须改为环境显式注入并保持启动失败保护。
|
||||
8. 模块级 `AGENTS.md`、README、分期文档和 OpenAPI 仍把兼容 `tenant_id` 描述成真实上级租户,三期存在重新按三级租户开发的返工风险。
|
||||
8. 模块级 `AGENTS.md`、README、分期文档和 OpenAPI 仍把兼容 `tenant_id` 描述成真实上级租户,国内货柜履约存在重新按三级租户开发的返工风险。
|
||||
|
||||
## P2 最小加固
|
||||
|
||||
@@ -63,6 +63,6 @@
|
||||
- 订单履约:`order/`、`firstleg/` 的生产代码与测试,不修改 SQL、OpenAPI、README、AGENTS。
|
||||
- 公共审计事件:`common-audit/`、`audit-service/`、`integration-service/`、`event-bridge-service/`、`common-openapi/` 的生产代码与测试。
|
||||
- Warehouse/File/迁移:`warehouse/`、`file/` 生产代码与测试,以及各模块数据库脚本、`deploy/sql/`、`config/nacos/`;平台公共资源(含往来单位、快递公司)作用域迁移为 `0`,客户私有资源保持客户 ID。
|
||||
- 文档契约:模块 `AGENTS.md`、README、OpenAPI 和一期/二期/三期指导性文档;不修改 Java 或 SQL。
|
||||
- 文档契约:模块 `AGENTS.md`、README、OpenAPI 和订单与客户/入库测量与计费/国内货柜履约指导性文档;不修改 Java 或 SQL。
|
||||
|
||||
跨模块新接口、公共 DTO、事件结构和公共仓库作用域变更由主对话统一裁决,单个修改对话不得自行扩展范围。
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
重点检查商户下单、平台代客、列表详情、状态流转、货物/参与方/历史/备注、幂等、Outbox、IAM 资格查询和 Warehouse 主数据调用。必须区分当前隔离键 `tenant_id` 与往来单位语义的 `customer_id`。
|
||||
|
||||
### 对话 C:Firstleg 履约与二期入库
|
||||
### 对话 C:Firstleg 履约与入库
|
||||
|
||||
负责目录:`firstleg/`。
|
||||
|
||||
@@ -78,11 +78,11 @@
|
||||
|
||||
重点检查表、索引、唯一键、增量迁移、种子数据和环境配置中的客户隔离维度;盘点 `tenant_id = 0`、`10001` 等保留值的真实用途;提出上线前只读数据核验 SQL,不执行写操作。
|
||||
|
||||
### 对话 G:测试、OpenAPI、文档与三期防返工
|
||||
### 对话 G:测试、OpenAPI、文档与国内货柜履约防返工
|
||||
|
||||
只负责 `src/test/`、各模块 `src/main/resources/openapi/`、`docs/` 和模块 `README` / `AGENTS.md`,不修改生产代码。
|
||||
|
||||
检查测试是否覆盖跨客户拒绝、平台跨客户、内部调用归属和端域隔离;检查契约及文档是否仍把当前 `tenant_id` 描述成真实上级租户;定向核对三期文档,列出可能诱导后续按三级租户开发的内容。
|
||||
检查测试是否覆盖跨客户拒绝、平台跨客户、内部调用归属和端域隔离;检查契约及文档是否仍把当前 `tenant_id` 描述成真实上级租户;定向核对国内货柜履约文档,列出可能诱导后续按三级租户开发的内容。
|
||||
|
||||
## 五、每个审计对话的统一输出格式
|
||||
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# 二期、三期业务功能回归报告
|
||||
# 入库测量与计费、国内货柜履约业务功能回归报告
|
||||
|
||||
测试时间:2026-08-07
|
||||
|
||||
## 测试范围
|
||||
|
||||
- 二期:企业资质、平台审核、公共主数据、客户计费规则、头程下单、入库测量、称重、计费与入库。
|
||||
- 三期:虚拟柜、智能装柜、预装、PDA 拣货和最终装柜清单。
|
||||
- 入库测量与计费:企业资质、平台审核、公共主数据、客户计费规则、头程下单、入库测量、称重、计费与入库。
|
||||
- 国内货柜履约:虚拟柜、智能装柜、预装、PDA 拣货和最终装柜清单。
|
||||
- 测试环境:本机 MySQL、Redis、Nacos;隔离服务端口 `19300` 到 `19306`。未停止、未占用用户的 `192xx` 服务端口。
|
||||
- 测试结束后已停止本次启动的 Gateway 和全部 `193xx` 隔离服务;未停止 MySQL、Redis、Nacos 或用户服务。
|
||||
|
||||
## 项目索引结论
|
||||
|
||||
- 二期主状态和测量、计费、入库编排在 `firstleg`;订单货物在 `order`;仓库、托盘、库位、往来单位在 `warehouse`;文件在 `file`。
|
||||
- 三期智能装柜、预装、PDA 拣货、装柜清单均在 `firstleg`。
|
||||
- 入库测量与计费主状态和测量、计费、入库编排在 `firstleg`;订单货物在 `order`;仓库、托盘、库位、往来单位在 `warehouse`;文件在 `file`。
|
||||
- 国内货柜履约智能装柜、预装、PDA 拣货、装柜清单均在 `firstleg`。
|
||||
- 当前 `tenant_id` 在业务上代表客户归属;公共主数据应使用 `tenant_id=0`,订单服务仅可读取公共仓库和公共往来单位。
|
||||
|
||||
## 二期结果
|
||||
## 入库测量与计费结果
|
||||
|
||||
结果:未通过,主链路在创建头程订单草稿前被阻断。
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
2. 当前平台往来单位 API 对 `tenantId=0` 返回 `A0422`,校验信息为“最小不能小于 1”;公共主数据无法经 API 创建或查询。
|
||||
3. 旧订单请求仍强制 `carrierId`、`carrierName`。本次不传时返回 `A0422`。根据最新业务确认,头程业务不存在“承运商”概念,这一历史字段、校验、分配接口和费率依赖应移除或改为船司领域模型,不能继续作为下单前置条件。
|
||||
|
||||
结论:在修复公共主数据 `tenantId=0` API 契约,并移除订单承运商强制校验前,二期无法从下单继续验证测量、称重、计费和入库。
|
||||
结论:在修复公共主数据 `tenantId=0` API 契约,并移除订单承运商强制校验前,入库测量与计费无法从下单继续验证测量、称重、计费和入库。
|
||||
|
||||
## 三期结果
|
||||
## 国内货柜履约结果
|
||||
|
||||
结果:通过。
|
||||
|
||||
@@ -59,13 +59,13 @@
|
||||
## 构建与脚本观察
|
||||
|
||||
- 离线全量模块测试执行到 `firstleg`:211 项中 1 项失败。失败项是 `InboundParentChildSqlContractTest`,原因是统一 SQL 已使用最终索引命名,但测试仍断言旧索引名 `uk_inbound_parent_sequence`;属于 SQL 契约断言滞后,不是 Java 业务断言失败。
|
||||
- 二期验收脚本需要 PowerShell 7 的 `SkipHttpErrorCheck`,而本机只有 Windows PowerShell 5;测试时使用了临时 UTF-8 BOM 兼容副本。
|
||||
- 入库测量与计费验收脚本需要 PowerShell 7 的 `SkipHttpErrorCheck`,而本机只有 Windows PowerShell 5;测试时使用了临时 UTF-8 BOM 兼容副本。
|
||||
- 原脚本引用的 `c7.png`、`c8.png` 示例图片未随项目提供;测试目录使用最小 PNG 仅验证上传协议。
|
||||
- 初始 File 隔离 JAR 未包含当前资质包上传端点,使用当前编译类启动后接口通过。发布前应重新打包 File 服务,避免运行包与源码不一致。
|
||||
|
||||
## 建议处理顺序
|
||||
|
||||
1. 移除头程订单及二期计费中的承运商强制字段、校验和分配逻辑;如需要船司,改为独立船司主数据,不复用往来单位承运商。
|
||||
1. 移除头程订单及入库测量与计费计费中的承运商强制字段、校验和分配逻辑;如需要船司,改为独立船司主数据,不复用往来单位承运商。
|
||||
2. 修复平台公共主数据 API:允许且仅允许平台身份使用 `tenantId=0` 创建、查询公共仓库和往来单位。
|
||||
3. 为订单下单提供可选的公共发货单位选择,或明确订单客户快照不再依赖 `SHIPPER` 往来单位。
|
||||
4. 完成以上两项后,重新执行二期全链路:下单、审核、入仓单、测量、OCR 称重、费用试算、费用确认、入库完成。
|
||||
4. 完成以上两项后,重新执行入库测量与计费全链路:下单、审核、入仓单、测量、OCR 称重、费用试算、费用确认、入库完成。
|
||||
@@ -7,7 +7,7 @@
|
||||
- `file-service` 统一承载任务、可靠调度、通用 XLSX、存储、过期清理、下载鉴权和指标。
|
||||
- File 不跨库读取 IAM、Order、Warehouse 等业务数据;业务服务只实现标准只读 Provider。
|
||||
- MQ 只传任务标识,不传筛选条件、业务行、Token 或文件。
|
||||
- 一期存储是 `FILE_STORAGE_ROOT` 目录且按单节点部署;多实例前必须使用跨主机共享卷或对象存储。
|
||||
- 订单与客户存储是 `FILE_STORAGE_ROOT` 目录且按单节点部署;多实例前必须使用跨主机共享卷或对象存储。
|
||||
- 当前首个类型为 `CUSTOMER_LIST`,由 IAM 提供数据。
|
||||
|
||||
## 2. 执行链路
|
||||
|
||||
+15
-15
@@ -54,7 +54,7 @@
|
||||
- 下载中心继续使用统一任务表,不依赖 MQ 查询状态。
|
||||
- 新业务服务只实现公共 Provider 契约并注册导出类型,不写任务、Excel、存储、下载代码。
|
||||
- File Service 统一负责 XLSX、存储、下载鉴权、过期清理、可观测性。
|
||||
- 代码层面支持 File Service 多实例(租约、条件抢占、`lease_owner` 齐备);**部署层面一期按单节点运行**,因为存储是宿主机目录(§11.1)。滚动发布期间的短暂双实例由租约保证安全。
|
||||
- 代码层面支持 File Service 多实例(租约、条件抢占、`lease_owner` 齐备);**部署层面订单与客户按单节点运行**,因为存储是宿主机目录(§11.1)。滚动发布期间的短暂双实例由租约保证安全。
|
||||
|
||||
### 2.2 非目标
|
||||
|
||||
@@ -65,8 +65,8 @@
|
||||
- 不以 Redis 作为任务、重试或幂等的事实来源。
|
||||
- 不实现用户自定义列编排器。
|
||||
- 不实现导出断点续传:每次执行从零重写整个文件(见 §9.5)。
|
||||
- 一期不引入 MinIO/OSS(见 §21 决策 6),但存储访问必须走接口抽象。
|
||||
- 一期不引入 Testcontainers(见 §21 决策 8)。
|
||||
- 订单与客户不引入 MinIO/OSS(见 §21 决策 6),但存储访问必须走接口抽象。
|
||||
- 订单与客户不引入 Testcontainers(见 §21 决策 8)。
|
||||
|
||||
## 3. 核心原则
|
||||
|
||||
@@ -821,7 +821,7 @@ Provider 可将 Token 实现为签名后的最大 ID、截止时间或数据库
|
||||
| 截止时间戳 + 按 `updated_at` 过滤 | ✅ | ⚠️ 取决于是否有历史版本 | ❌ |
|
||||
| 数据库快照(如 MySQL 一致性读快照、或导出前落临时表) | ✅ | ✅ | ✅ |
|
||||
|
||||
**一期 `CUSTOMER_LIST` 采用"签名后的最大 ID",即只保证不混入新增数据,不保证修改与删除的一致性。** 这对客户列表导出是可接受的——导出期间恰好有客户资料被改动的概率极低,且影响仅是个别字段取值偏新。
|
||||
**订单与客户 `CUSTOMER_LIST` 采用"签名后的最大 ID",即只保证不混入新增数据,不保证修改与删除的一致性。** 这对客户列表导出是可接受的——导出期间恰好有客户资料被改动的概率极低,且影响仅是个别字段取值偏新。
|
||||
|
||||
因此对外表述应为“**不会混入导出开始后新增的数据**”,而非笼统的“数据快照一致”。若某个导出类型确实需要完整快照(如财务对账),该 Provider 需自行实现临时表或一致性读方案,本契约不阻止,但也不代为实现。
|
||||
|
||||
@@ -916,15 +916,15 @@ baiyue:
|
||||
|
||||
将现有 `LocalExportStorage` 提取为接口 `ExportStorage`(`create` / `size` / `load` / `delete`),实现类 `SharedDirectoryExportStorage` 保持当前行为与对象键格式 `exports/{taskId}/{uuid}.xlsx`。Consumer、API、Reconciler 只依赖接口。
|
||||
|
||||
一期不引入对象存储(§21 决策 6),但接口就位后未来替换为 MinIO/OSS 不需要改动调用方。**对象键始终由 File Service 生成与校验,业务服务不得提供。**
|
||||
订单与客户不引入对象存储(§21 决策 6),但接口就位后未来替换为 MinIO/OSS 不需要改动调用方。**对象键始终由 File Service 生成与校验,业务服务不得提供。**
|
||||
|
||||
**普通宿主机目录挂载不构成"共享存储",它只在同一宿主机的容器之间共享。** 这是第一版的一处不实表述:部署已有的 `FILE_UPLOADS_HOST_DIR:/data/files` 是宿主机 bind mount,若 File Service 的两个实例被调度到**不同物理机**,各自看到的是各自宿主机上的独立目录——A 实例生成的文件,B 实例既读不到也删不掉,表现为:下载随机 404(请求打到没有该文件的实例)、过期清理留下永久孤儿文件。
|
||||
|
||||
因此一期存储的明确边界,**二选一,必须在部署前确认**:
|
||||
因此订单与客户存储的明确边界,**二选一,必须在部署前确认**:
|
||||
|
||||
| 方案 | 约束 | 适用 |
|
||||
|---|---|---|
|
||||
| **A. 单节点部署**(一期默认) | File Service 只允许 1 个实例;若用编排系统需固定节点亲和,禁止自动扩容 | 1000 用户量级,导出低频,单实例足够 |
|
||||
| **A. 单节点部署**(订单与客户默认) | File Service 只允许 1 个实例;若用编排系统需固定节点亲和,禁止自动扩容 | 1000 用户量级,导出低频,单实例足够 |
|
||||
| **B. 跨主机共享卷** | 挂载 NFS / CephFS / 云盘共享卷到同一路径 | 需要多实例时 |
|
||||
|
||||
选 A 时必须注意:**代码不得因此引入任何单实例假设**——租约、`lease_owner`、条件更新全部保留,因为滚动发布期间会短暂存在两个实例,且方案 B 随时可能启用。单实例是部署约束,不是设计约束。
|
||||
@@ -1020,7 +1020,7 @@ spring:
|
||||
- 文件删除成功或确认不存在后,任务才能标记 `EXPIRED`;删除失败保持原状并重试 + 告警(沿用现有实现)。
|
||||
- `file_export_outbox` 的 `SENT` 行保留 7 天后**物理删除**(每日一次,每次 ≤5000 行分批删除)。清理必须限定 `status = 'SENT'`,绝不能删除 `PENDING` / `CLAIMED` 行——它们是尚未送达的命令。
|
||||
- `file_export_task` 终态行长期保留(下载中心需要历史),当前量级无需归档。
|
||||
- 存储:一期按 §11.1 方案 A(单节点)或方案 B(跨主机共享卷)二选一,部署前必须确认。宿主机 bind mount 只在同一主机内共享,**不足以支撑跨主机多实例**。
|
||||
- 存储:订单与客户按 §11.1 方案 A(单节点)或方案 B(跨主机共享卷)二选一,部署前必须确认。宿主机 bind mount 只在同一主机内共享,**不足以支撑跨主机多实例**。
|
||||
- 对象键由 File Service 生成和校验,业务服务不得提供。
|
||||
|
||||
## 16. 基础设施落地
|
||||
@@ -1333,7 +1333,7 @@ RocketMQ NameServer/Broker 就绪
|
||||
- 存储的多实例边界已明确(§11.1),不把宿主机 bind mount 当作跨主机共享存储。
|
||||
- MQ 不承担延迟与业务重排;Reconciler 是唯一重派通道;DLQ 只承载损坏消息。
|
||||
- Outbox 与任务均有租约,无“领取后崩溃即永久卡死”的路径。
|
||||
- 至少一次投递、幂等、租约、Outbox、Reconciler、DLQ、指标与告警全部纳入一期实现。
|
||||
- 至少一次投递、幂等、租约、Outbox、Reconciler、DLQ、指标与告警全部纳入订单与客户实现。
|
||||
- 新业务类型能通过 Provider 契约 + 受控注册接入,不复制客户导出代码。
|
||||
|
||||
## 21. 已定型决策
|
||||
@@ -1342,12 +1342,12 @@ RocketMQ NameServer/Broker 就绪
|
||||
|
||||
| # | 决策 | 取值 | 理由 |
|
||||
|---|---|---|---|
|
||||
| 1 | 一期是否含 MQ | 含 | 业务方已定。可靠性由数据库承担,MQ 引入不降低稳定性 |
|
||||
| 1 | 订单与客户是否含 MQ | 含 | 业务方已定。可靠性由数据库承担,MQ 引入不降低稳定性 |
|
||||
| 2 | MQ 客户端 | `rocketmq-spring-boot-starter` 2.3.3,Remoting 协议 | 需要显式控制消费返回语义;不引入 Proxy 少一个可失效组件;同时删除 `firstleg` 的 Stream 死依赖。Boot 3.2.9 兼容性与 `rocketmq-client 5.3.1` 传递版本已实证核验,见 §6.1 |
|
||||
| 3 | Outbox 是否抽公共模块 | 一期不抽,`file_export_outbox` 独立建表并作为全仓基线 | 一期只有一个真实使用者,过早抽象会把接口定错。`order` 的 Outbox 目前**根本没有发布器**,是独立待修问题;后续收敛时以本表结构为准(含 `lease_owner`/`lease_until`/`next_retry_at`),命名统一为 `<模块>_outbox` |
|
||||
| 3 | Outbox 是否抽公共模块 | 订单与客户不抽,`file_export_outbox` 独立建表并作为全仓基线 | 订单与客户只有一个真实使用者,过早抽象会把接口定错。`order` 的 Outbox 目前**根本没有发布器**,是独立待修问题;后续收敛时以本表结构为准(含 `lease_owner`/`lease_until`/`next_retry_at`),命名统一为 `<模块>_outbox` |
|
||||
| 4 | 契约放哪 | 放入 `common-core` 的 `com.baiyue.common.core.export` 包,**不新建模块**,模块总数仍为 18 | 契约是 file 与 iam 双方共用的 HTTP 结构。放 `file` 内会迫使 iam 依赖 `file-service`(拖入 POI/MyBatis/任务表实体,且业务服务反向依赖 file);新建模块则多一个模块。放 `common-core` 两全:双方已通过 `common-web` 传递依赖到它,pom 都不用改。与 `common-event-contract` 不合并——后者是跨区域事件信封(含 `sourceRegion`/`payloadHash` 等无关字段),强行复用会互相牵制 |
|
||||
| 5 | Prometheus | 一期补 `micrometer-registry-prometheus`;服务端由运维落地 | 无 registry 则 §13 全部指标无处可采,等于没有可观测性 |
|
||||
| 6 | 文件存储 | 一期共享目录挂载 + `ExportStorage` 接口抽象 | 1000 用户、7 天保留、单文件 ≤50MB,总量在 GB 级,共享目录足够。引入 MinIO 多一个有状态组件与备份负担。接口就位后替换不改调用方 |
|
||||
| 5 | Prometheus | 订单与客户补 `micrometer-registry-prometheus`;服务端由运维落地 | 无 registry 则 §13 全部指标无处可采,等于没有可观测性 |
|
||||
| 6 | 文件存储 | 订单与客户共享目录挂载 + `ExportStorage` 接口抽象 | 1000 用户、7 天保留、单文件 ≤50MB,总量在 GB 级,共享目录足够。引入 MinIO 多一个有状态组件与备份负担。接口就位后替换不改调用方 |
|
||||
| 7 | 导出类型注册 | Nacos 受控配置 + 代码内置服务白名单 + 启动强校验 | 纯 Nacos 有 SSRF 面;纯代码注册每加类型都要发版。两者结合:配置只能从白名单里选 |
|
||||
| 8 | Testcontainers | 不引入,集成测试连测试环境 Broker | 全仓无 Testcontainers 先例,RocketMQ 容器需同时起 NameServer+Broker 并处理 `brokerIP1`,投入产出比低于直接用测试环境 |
|
||||
| 9 | 新旧双轨 | 不双轨,阶段 3 直接停用旧 Worker | 旧 `claim()` 不识别 `next_retry_at` 与 `dispatch_version`,双轨会提前执行退避中的任务。回滚靠配置开关,不靠双轨 |
|
||||
@@ -1359,8 +1359,8 @@ RocketMQ NameServer/Broker 就绪
|
||||
| 15 | Outbox 失败终态 | 取消 `DEAD`,发送无限退避重试 + 告警 | 设终态会让 Broker 长期故障时任务静默停滞,违背 §2.1 的“提交成功的任务最终一定被调度”。靠告警驱动人工介入,Broker 恢复后自动追平 |
|
||||
| 16 | 续租方式 | 独立 watchdog 线程按 60 秒周期,覆盖全执行期 | 挂在批次循环里会漏掉最终 `write()`/压缩/落盘阶段,慢磁盘下大文件会在临成功时被误判失联并反复重跑 |
|
||||
| 17 | 时间基准 | 全部走数据库 `CURRENT_TIMESTAMP(3)` | 实例时钟偏差会导致提前抢占(两实例同写一任务)或延迟恢复 |
|
||||
| 18 | 多实例部署 | 代码支持,**一期部署按单节点**;需多实例时先上跨主机共享卷 | 宿主机 bind mount 只在同主机内共享,跨主机会导致下载随机 404 与永久孤儿文件(§11.1) |
|
||||
| 19 | 快照强度 | 一期只保证“不混入导出开始后新增的数据”,不冻结修改与删除 | 最大 ID 游标的固有限制。对客户列表可接受;需要完整快照的类型由该 Provider 自行实现临时表或一致性读(§10.2) |
|
||||
| 18 | 多实例部署 | 代码支持,**订单与客户部署按单节点**;需多实例时先上跨主机共享卷 | 宿主机 bind mount 只在同主机内共享,跨主机会导致下载随机 404 与永久孤儿文件(§11.1) |
|
||||
| 19 | 快照强度 | 订单与客户只保证“不混入导出开始后新增的数据”,不冻结修改与删除 | 最大 ID 游标的固有限制。对客户列表可接受;需要完整快照的类型由该 Provider 自行实现临时表或一致性读(§10.2) |
|
||||
| 20 | 权限取值时点 | 创建与人工重试时校验,执行阶段不校验;不存权限快照 | 执行在后台无用户会话,无法重新鉴权。已受理即视为已授权,代价与已知缺口写明于 §10.4 |
|
||||
| 21 | 毒消息处置 | 独立 DLQ Handler 自动将匹配的 Outbox 置 `QUARANTINED`、任务置 `FAILED` | 人工隔离无法保证在 Reconciler 10 分钟补投窗口内完成;定位字段放 User Properties,避免依赖损坏 payload |
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 目的与口径
|
||||
|
||||
本基线覆盖当前官方 22 个 Maven 模块、15 个可执行服务之间的 OpenFeign、`RestClient`、`WebClient` 与 JDK `HttpClient` 调用。海外安全 Inbox/投影属于 Lastleg 内部子域,不再作为独立服务节点。基线用于识别同步依赖方向、在线请求深度、后台中继、超时、重试、熔断、幂等、错误映射和失败传播,不改变 Finance 或四期业务状态机。
|
||||
本基线覆盖当前官方 22 个 Maven 模块、15 个可执行服务之间的 OpenFeign、`RestClient`、`WebClient` 与 JDK `HttpClient` 调用。海外安全 Inbox/投影属于 Lastleg 内部子域,不再作为独立服务节点。基线用于识别同步依赖方向、在线请求深度、后台中继、超时、重试、熔断、幂等、错误映射和失败传播,不改变 Finance 或海外仓业务业务状态机。
|
||||
|
||||
“存在双向服务边”不等于“一次在线请求会递归调用”。每条边必须继续区分:
|
||||
|
||||
@@ -106,7 +106,7 @@ File 的五个导出 Provider、Order→Firstleg 入库 Outbox、该入库消费
|
||||
|
||||
### 已完成的第一个 REQUEST 边退出
|
||||
|
||||
`IAM → Firstleg` 的 `CustomerDimensionRuleClient` 默认尺寸初始化已经从 REQUEST WRITE 改为可靠后台编排。IAM 与客户主档同事务持久化稳定 `initializationKey` 的任务,显式维护 `PENDING/READY/FAILED`,由租约调度重试并提供诊断查询和人工重试;审批通过与下单资格在未 READY 时失败关闭。此边现在仅属于 BACKGROUND,在线五服务强连通分量实际拆为 Auth↔IAM 与 Order↔Firstleg 两个子环,未触碰财务、Event Bridge 或四期业务状态机。
|
||||
`IAM → Firstleg` 的 `CustomerDimensionRuleClient` 默认尺寸初始化已经从 REQUEST WRITE 改为可靠后台编排。IAM 与客户主档同事务持久化稳定 `initializationKey` 的任务,显式维护 `PENDING/READY/FAILED`,由租约调度重试并提供诊断查询和人工重试;审批通过与下单资格在未 READY 时失败关闭。此边现在仅属于 BACKGROUND,在线五服务强连通分量实际拆为 Auth↔IAM 与 Order↔Firstleg 两个子环,未触碰财务、Event Bridge 或海外仓业务业务状态机。
|
||||
|
||||
### Order↔Firstleg 第二阶段结论
|
||||
|
||||
@@ -143,7 +143,7 @@ File 的五个导出 Provider、Order→Firstleg 入库 Outbox、该入库消费
|
||||
| IAM/Firstleg | `mvn -pl iam,firstleg -am test` |
|
||||
| Firstleg/Finance | `mvn -pl firstleg,finance -am test`;业务口径先由财务总控确认 |
|
||||
| Firstleg/File 或 File/Finance | `mvn -pl firstleg,file,finance -am test`,按实际消费者缩小模块集 |
|
||||
| Lastleg 原始 HTTP | `mvn -pl lastleg-service,warehouse-overseas-service,file -am test`;不改变四期状态机 |
|
||||
| Lastleg 原始 HTTP | `mvn -pl lastleg-service,warehouse-overseas-service,file -am test`;不改变海外仓业务状态机 |
|
||||
| 全部治理阶段收口 | `mvn -DskipTests validate` 后执行 `mvn test` |
|
||||
|
||||
以上均为本地离线验证;未经明确授权不启动 Java/Nacos/数据库,不执行 SQL、部署或测试环境 HTTP 验收。
|
||||
|
||||
@@ -43,7 +43,7 @@ baiyue-firstleg/
|
||||
├─ auth/ # 统一登录与账号凭据
|
||||
├─ iam/ # 租户、成员、角色、权限、企业审核
|
||||
├─ order/ # 头程订单中心
|
||||
├─ firstleg/ # 履约任务、二期入库、测量与计费
|
||||
├─ firstleg/ # 履约任务、入库、测量与计费
|
||||
├─ warehouse/ # 仓库主数据、托盘与库位资源
|
||||
├─ finance/ # 财务账期、账单、收付款与核销
|
||||
├─ lastleg-service/ # 海外尾程、入仓、库存与本土业务
|
||||
@@ -93,9 +93,9 @@ baiyue-firstleg/
|
||||
| `gateway-overseas` | `gateway-overseas` | 9300 | 无 | 海外区域 API 入口、海外 Audience/JWKS 校验及海外服务路由 |
|
||||
| `auth` | `auth-service` | 9100 | `baiyue_auth` | 登录、刷新 Token、退出、商户注册、账号密码和账号内部管理 |
|
||||
| `iam` | `iam-service` | 9201 | `baiyue_iam` | 租户、企业资料、企业审核、成员、角色、权限、按钮权限、个人资料和授权快照 |
|
||||
| `firstleg` | `firstleg-service` | 9202 | `baiyue_firstleg` | 履约任务与分配;二期入库单、测量审核、FBA 计费、费用发布和完成入库编排;不是订单主数据服务 |
|
||||
| `firstleg` | `firstleg-service` | 9202 | `baiyue_firstleg` | 履约任务与分配;入库单、测量审核、FBA 计费、费用发布和完成入库编排;不是订单主数据服务 |
|
||||
| `order` | `order-service` | 9204 | `baiyue_order` | 订单草稿、编辑、提交、详情、状态历史、货物、往来单位快照、内部备注和 Outbox |
|
||||
| `warehouse` | `warehouse-service` | 9205 | `baiyue_warehouse` | 仓库、地址、服务能力、往来单位、快递公司,以及二期托盘、库位、预占和占用;往来单位不单独拆服务 |
|
||||
| `warehouse` | `warehouse-service` | 9205 | `baiyue_warehouse` | 仓库、地址、服务能力、往来单位、快递公司,以及入库测量与计费托盘、库位、预占和占用;往来单位不单独拆服务 |
|
||||
| `file` | `file-service` | 9206 | `baiyue_file` | 企业资质文件、公共异步导出下载、入库测量图片及重量 OCR 调用协作 |
|
||||
| `print` | `print-service` | 9207 | 无 | 无状态 PDF 渲染服务;调用方传入完整标签数据,服务在内存中生成一次性多页 PDF |
|
||||
| `integration-service` | `integration-service` | 9210 | 无 | 百度 OCR、Amazon SP-API 等第三方适配器和启停状态;默认关闭外部调用 |
|
||||
@@ -128,13 +128,13 @@ Order 负责订单草稿、提交、平台处理状态、货物、参与方和
|
||||
|
||||
### 5.5 `firstleg/`:头程履约中心
|
||||
|
||||
Firstleg 承担订单提交后的履约任务、操作分配与历史,以及二期入库单查询、测量回传、人工测量、审核、异常放行、FBA 计费、费用发布和完成入库状态编排。
|
||||
Firstleg 承担订单提交后的履约任务、操作分配与历史,以及入库单查询、测量回传、人工测量、审核、异常放行、FBA 计费、费用发布和完成入库状态编排。
|
||||
|
||||
订单主单和货物主数据仍属于 Order;托盘和库位资源属于 Warehouse;图片对象属于 File;OCR 供应商协议属于 Integration。Firstleg 只保存自身业务状态、快照和逻辑引用,通过内部 DTO 调用其他服务,不复制对方领域模型。
|
||||
|
||||
### 5.6 `warehouse/`:仓库与仓储资源
|
||||
|
||||
Warehouse 负责仓库、地址、服务能力、往来单位、快递公司,以及二期托盘、库位、预占和占用资源。往来单位只是 Warehouse 下的主数据配置,保存在 `baiyue_warehouse.wh_partner`,不创建独立服务和数据库。
|
||||
Warehouse 负责仓库、地址、服务能力、往来单位、快递公司,以及入库测量与计费托盘、库位、预占和占用资源。往来单位只是 Warehouse 下的主数据配置,保存在 `baiyue_warehouse.wh_partner`,不创建独立服务和数据库。
|
||||
|
||||
Firstleg 通过内部接口发起托盘库位预占、确认和释放,只保存逻辑引用与入库快照。容量、共享溢出、预占和占用一致性由 Warehouse 的数据库事务和条件更新最终保证。
|
||||
|
||||
@@ -156,7 +156,7 @@ Print 是无数据库的无状态 PDF 渲染服务。业务归属服务完成权
|
||||
|
||||
### 5.10 `integration-service/`:第三方集成中心
|
||||
|
||||
Integration 负责百度 OCR、重量识别和 Amazon Logistics/SP-API 等第三方协议适配及能力状态。二期重量 OCR 由 File 调用,业务图片和入库状态不归 Integration;服务不使用业务数据库,真实外部调用默认关闭,密钥必须通过 Jenkins Credentials、环境变量或 Nacos 密文注入。
|
||||
Integration 负责百度 OCR、重量识别和 Amazon Logistics/SP-API 等第三方协议适配及能力状态。入库测量与计费重量 OCR 由 File 调用,业务图片和入库状态不归 Integration;服务不使用业务数据库,真实外部调用默认关闭,密钥必须通过 Jenkins Credentials、环境变量或 Nacos 密文注入。
|
||||
|
||||
### 5.11 `event-bridge-service/`:跨区域事件桥
|
||||
|
||||
@@ -194,7 +194,7 @@ deploy/
|
||||
|
||||
- `local-env.ps1`:本地服务环境变量
|
||||
- `publish-local-nacos-config.ps1`:发布本地 Nacos 配置
|
||||
- `phase1-acceptance.ps1`:一期验收入口
|
||||
- `phase1-acceptance.ps1`:订单与客户验收入口
|
||||
- `phase1-business-maturity-regression.ps1`:业务成熟度回归
|
||||
- `phase1-onboarding-order-e2e.ps1`:企业入驻到订单端到端验证
|
||||
- `order-warehouse-real-api-regression.ps1`:订单、仓库、往来单位真实 API 回归
|
||||
|
||||
+2
-2
@@ -76,7 +76,7 @@
|
||||
| 库位占用记录 | `OCCUPIED` | 正在占用。 |
|
||||
| 库位占用记录 | `RELEASED` | 已释放。 |
|
||||
|
||||
## 二期入库
|
||||
## 入库
|
||||
|
||||
### 入库单主流程状态
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
| 入库与计费幂等记录 | `PROCESSING` | 请求正在执行。 |
|
||||
| 入库与计费幂等记录 | `COMPLETED` | 请求已成功完成,可返回幂等结果。 |
|
||||
|
||||
## 三期智能装柜和 PDA
|
||||
## 国内货柜履约智能装柜和 PDA
|
||||
|
||||
| 字段 | 状态值 | 中文含义 |
|
||||
| --- | --- | --- |
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
## 职责与边界
|
||||
|
||||
- Mapper 只由 `FileServiceApplication.java` 对 `com.baiyue.file` 根包按 `@Mapper` 过滤扫描;新增或移动 Mapper 后从后端根运行 `node scripts/mapper-scan-contract.mjs`,不得增加子域扫描配置。
|
||||
- 负责企业资质文件、公共导出、二期入库测量图片的上传、存储、读取和鉴权。
|
||||
- 负责企业资质文件、公共导出、入库测量图片的上传、存储、读取和鉴权。
|
||||
- 资质审核元数据归 `iam/`,入库状态归 `firstleg/`,OCR 适配归 `integration-service/`;不跨库读取业务表。
|
||||
|
||||
## 模块入口
|
||||
@@ -37,4 +37,4 @@
|
||||
|
||||
## 全量验收导航
|
||||
|
||||
一期、二期、三期及财务全量验收均复用本模块文件能力;按本次链路读取对应阶段/专题文档,统一核对资质、测量图片、装柜证据、报关回执的归属、鉴权、审计和生命周期。照片允许追加、错误记录仅作废不硬删除,客户不得读取内部照片或锁码。
|
||||
订单与客户、入库测量与计费、国内货柜履约及财务全量验收均复用本模块文件能力;按本次链路读取对应阶段/专题文档,统一核对资质、测量图片、装柜证据、报关回执的归属、鉴权、审计和生命周期。照片允许追加、错误记录仅作废不硬删除,客户不得读取内部照片或锁码。
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
## 模块定位
|
||||
|
||||
- 本模块是独立财务服务,拥有客户结算条款、财务费用来源、CNY 账单与明细、应收款月度统计、客户对账确认、收款及分配、成本及分摊、账单修订、单据编号、幂等记录和财务审计状态。
|
||||
- 产品范围先读 `../../docs/财务管理/财务管理产品确认表.md` 与 `财务一期五页产品与接口验收契约.md`,跨服务契约再读 `Firstleg-Finance跨服务契约.md`;专题 README、证据矩阵和测试环境报告只用于状态或验收问题,不作为普通开发首读。
|
||||
- 产品范围先读 `../../docs/财务管理/财务管理产品确认表.md` 与 `财务核心五页产品与接口验收契约.md`,跨服务契约再读 `Firstleg-Finance跨服务契约.md`;专题 README、证据矩阵和测试环境报告只用于状态或验收问题,不作为普通开发首读。
|
||||
- Firstleg 拥有入库/柜次业务费用和履约事实,File 拥有凭证文件;Finance 通过内部接口读取或消费快照,不跨库查询,也不复制上游业务状态机。
|
||||
|
||||
## 快速查询
|
||||
@@ -13,7 +13,7 @@
|
||||
| 平台账户、费用池、账单、收款与成本 API | `controller/PlatformFinanceController.java`,统一前缀 `/api/platform/finance/v1` |
|
||||
| 平台财务凭证上传 | `PlatformFinanceController.java` 的 `POST /vouchers`、`client/FinanceFileClient.java`;文件类型固定 `FINANCE_VOUCHER`,业务 ID/客户范围使用当前客户 ID |
|
||||
| 客户只读账单、收款与应收 API | `controller/ClientFinanceController.java`,统一前缀 `/api/client/finance/v1` |
|
||||
| 一期账户、费用来源审核与结算纳入 | `controller/FinancePhase1OperationController.java`、`service/FinancePhase1OperationService.java` |
|
||||
| 订单与客户账户、费用来源审核与结算纳入 | `controller/FinancePhase1OperationController.java`、`service/FinancePhase1OperationService.java` |
|
||||
| 客户财务账户汇总 | `service/FinanceCustomerAccountService.java`、`client/IamCustomerClient.java`;客户主档事实仍归 IAM |
|
||||
| 客户对账单 | `controller/ReconciliationStatementController.java`、`service/ReconciliationStatementService.java` |
|
||||
| 应收款月度统计 | `controller/ReceivableStatisticsController.java`、`service/ReceivableStatisticsService.java`、`dto/ReceivableStatisticsDtos.java`;按结算单生成月份和客户汇总 CNY 正式应收结算单 |
|
||||
@@ -21,7 +21,7 @@
|
||||
| 对账单客户确认 | `controller/ClientFinanceController.java`、`service/ReconciliationStatementService.java`;客户接口 `/api/client/finance/v1/statements/{statementId}/confirm` |
|
||||
| 财务单据编号 | `service/FinanceDocumentNumberGenerator.java`;对账单、应收结算单、应付结算单分别使用 `REC`、`AR`、`AP` 前缀 |
|
||||
| Finance 导出数据 Provider | `controller/FinanceExportProviderController.java`、`service/FinanceExportProviderService.java`;由 File `ExportProviderClient` 按已登记 `exportType` 分批拉取,不从 Admin 直接生成文件 |
|
||||
| 五页产品 OpenAPI | `../../docs/财务管理/openapi/finance-phase1-five-pages-v1.yaml`;人工财务补充契约 `finance-f1-manual-v1.yaml` |
|
||||
| 五页产品 OpenAPI | `../../docs/财务管理/openapi/finance-core-five-pages-v1.yaml`;人工财务补充契约 `finance-f1-manual-v1.yaml` |
|
||||
| 业务费用来源事件入口 | Firstleg:`controller/InternalFinanceFeeSourceController.java`、`service/FinanceFeeSourceEventService.java`;海外完整快照:`controller/InternalBusinessFeeSourceController.java` 的 `/internal/finance/v1/business-fee-source-events`、`service/BusinessFeeSourceEventService.java`;事件模型 `../common-event-contract/src/main/java/com/baiyue/common/event/BusinessFeeSourceChangedV1.java` |
|
||||
| 查询模型、客户可见性与账单行入仓码 | `service/FinanceQueryService.java`、`dto/FinanceReadModels.java`;`StatementLine.inboundCode` 从对应费用来源不可变快照读取,供 Client 跳转客户隔离的入仓关系详情 |
|
||||
| 账期、账单、收款、成本及幂等写模型 | `service/FinanceService.java` |
|
||||
|
||||
@@ -43,7 +43,7 @@ class FinanceExportContractTest {
|
||||
Assumptions.assumeTrue(outerRoot != null, "outer product contract repository is not part of this checkout");
|
||||
|
||||
String openApi = Files.readString(
|
||||
outerRoot.resolve("docs/财务管理/openapi/finance-phase1-five-pages-v1.yaml"),
|
||||
outerRoot.resolve("docs/财务管理/openapi/finance-core-five-pages-v1.yaml"),
|
||||
StandardCharsets.UTF_8);
|
||||
assertThat((Object) new Yaml().load(openApi)).isNotNull();
|
||||
assertThat(openApi).contains("/internal/exports/v1/{exportType}/batches:")
|
||||
|
||||
+7
-7
@@ -3,7 +3,7 @@
|
||||
## 职责与边界
|
||||
|
||||
- Mapper 统一显式使用 `@Mapper`,仅由 `BaiyueFirstlegApplication.java` 对 `com.baiyue.firstleg` 根包按 `annotationClass = Mapper.class` 扫描;禁止恢复业务子域 Mapper 包白名单。静态门禁见根 `scripts/mapper-scan-contract.mjs`,模块内编译门禁见 `MapperRegistrationArchitectureTest.java`、`BaiyueFirstlegApplicationContractTest.java`。
|
||||
- 负责履约任务/分配、入库单、测量/OCR、审核后初始计价、FBA 计费、客户在途柜量统计、国内出仓履约链路,以及四期国内出仓后的船节点查询任务和节点事实。
|
||||
- 负责履约任务/分配、入库单、测量/OCR、审核后初始计价、FBA 计费、客户在途柜量统计、国内出仓履约链路,以及海外仓业务国内出仓后的船节点查询任务和节点事实。
|
||||
- 订单主数据归 `order/`,托盘/库位归 `warehouse/`,文件对象归 `file/`,OCR 供应商归 `integration-service/`;通过客户端 DTO 和内部令牌协作。
|
||||
- 入库、分柜、装柜、PDA 和出仓能力均在本模块按对应服务端契约协作;`WAIT_SPLIT + APPROVED + CONFIRMED` 是入库链路状态节点,不是全量验收的范围终点。财务状态仍归 `finance`,本模块只生产不可变费用来源和柜次成本快照。
|
||||
|
||||
@@ -34,14 +34,14 @@
|
||||
| 财务整柜成本分摊快照 | `loading/controller/InternalFinanceCostSnapshotController.java`、`loading/service/FinanceCostSnapshotService.java` |
|
||||
| 分柜、预装、真实柜号后补与正式装柜清单生命周期 | `split/controller/PlatformSplitPreloadController.java`、`controller/PlatformLoadingListController.java`、`service/LoadingListService.java`;契约 `src/main/resources/openapi/loading-lists-v1.yaml`,详情阶段先查 `/loading-plans/{planId}/lifecycle`。创建柜次时原子生成唯一预装计划,加入入仓码明细时同步创建入库预装聚合与关联,不设推送步骤;同一 `planId` 在 `EDITING/LOADING` 返回 `PRELOAD`,`COMPLETED` 返回 `LOADING_LIST`。订舱关联、预装确认和 PDA task 创建不依赖真实集装箱号;可在 `EDITING/LOADING` 首次补录,最终 PDA 完成前必须存在;已有值纠正仅限 `EDITING` 且任务未领取、无物理作业事实 |
|
||||
| 同柜不可拆组 | `loading/coload/controller/PlatformColoadGroupController.java`、`service/ColoadGroupService.java`、`persistence/ColoadGroupMapper.java`;契约 `src/main/resources/openapi/coload-groups-v1.yaml`。请求按订单 ID 建组,服务端冻结每票全部 `WAIT_SPLIT` 可用入仓片段与满箱数;仅要求同国内仓、同渠道,允许跨客户及不同目的仓港;分配、移除、取消均保持整组原子 |
|
||||
| 货柜与全局智能排柜 | `controller/PlatformContainerController.java`、`PlatformSmartLoadingController.java`、`service/SmartLoadingService.java`、`repository/SmartLoadingRepository.java`、`client/PendingReviewOrderClient.java`、`loading/`;全局候选与工作台柜次从 `/inbound-candidates`、`/containers` 进入,普通箱数、待审核占位、同柜组和全部未拣移除分别走 `/inbound-assignments`、`/pending-review-assignments`、`/group-assignments`、`/unpicked-items`,明细规格拆分留在服务端 |
|
||||
| 货柜与全局智能排柜 | `controller/PlatformContainerController.java`、`PlatformSmartLoadingController.java`、`service/SmartLoadingService.java`、`repository/SmartLoadingRepository.java`、`client/PendingReviewOrderClient.java`、`loading/`;全局候选合并本服务履约投影与 Order `/internal/order/v1/pending-review-orders` 权威待审核页,工作台柜次从 `/inbound-candidates`、`/containers` 进入;普通箱数、待审核占位、同柜组和全部未拣移除分别走 `/inbound-assignments`、`/pending-review-assignments`、`/group-assignments`、`/unpicked-items`,明细规格拆分留在服务端 |
|
||||
| 货柜资料、PDA 证据与业务总览 | `controller/PlatformContainerController.java` 的 `/{id}/overview`、`/{id}/documents/{documentType}`,聚合 `mapper/ContainerOverviewMapper.java`;契约 `src/main/resources/openapi/phase3-container-overview-v1.yaml`,四类资料固定为目的港清关、国内出口报关、DO、PU 的当前唯一版本 |
|
||||
| PDA 拣货与装柜、证据幂等上传 | `controller/PlatformLoadingPdaController.java`、`loading/controller/PlatformLoadingDeliveryController.java`、`loading/service/CabinetLoadingEvidenceService.java`;证据写入和 multipart 上传均先以任务内幂等键匹配请求哈希,上传哈希覆盖仓库、版本、证据类型、锁码、备注、文件名、媒体类型、大小与文件内容;完全重放返回当前视图且不得再次上传 File,相同键不同内容返回冲突 |
|
||||
| 装柜完成跨区事件 | `service/LoadingCompletedOutboxService.java`、`LoadingCompletedOutboxRelay.java`、`repository/LoadingCompletedOutboxRepository.java`、`mapper/LoadingCartonManifestMapper.java`、`client/EventBridgeClient.java`;从 `fl_inbound_order.destination_type` 冻结 V2 `orders[].destinationType`(`FBA` 或 `OVERSEAS`),OVERSEAS 按最终实装与稳定箱码冻结,FBA 可仅冻结箱数;真实生产验收 `src/test/java/com/baiyue/firstleg/service/TaillegRealProducerAcceptanceTest.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
||||
| 海外卸柜完成事件回写 | 内部入口 `controller/InternalOverseasContainerEventController.java`、消费者 `service/OverseasContainerEventConsumer.java`、Inbox `entity/ContainerEventInboxEntity.java` 与 `mapper/ContainerEventInboxMapper.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
||||
| 海外暂扣状态回写 | 同一内部事件入口、消费者 `service/OverseasInboundOrderDetainedConsumer.java`;按来源入仓单 ID 更新 `fl_inbound_order.hold_flag/current_hold_reason`,复用跨区 Inbox 并写 `fl_inbound_hold_record` |
|
||||
| 三期海运订舱、资料/打印、关联候选与出仓前船期资料 | `oceanbooking/controller/PlatformOceanBookingController.java`、`oceanbooking/service/OceanBookingService.java`、`OceanBookingDocumentService.java`、`OceanBookingPrintService.java`、`oceanbooking/mapper/OceanBookingMapper.java`、`portschedule/controller/PlatformPortScheduleController.java`;文件经 `client/OceanBookingFileClient.java` 进入 File,关联候选 `/api/platform/v1/ocean-bookings/link-candidates`,契约 `src/main/resources/openapi/ocean-bookings-v1.yaml`,表结构见 `src/main/resources/db/phase3-ocean-booking-schema.sql` |
|
||||
| 四期国内出仓后船节点定时查询 | `outbound/dispatch/service/OutboundDispatchService.java` 在确认出仓事务内幂等创建现有 `CarrierScheduleQuery`;编排与租约 `portschedule/service/CarrierScheduleQueryScheduler.java`,任务/十节点事实/异常事实/不可变 attempt `CarrierScheduleQueryService.java`,平台查询与人工补录入口 `PlatformCarrierScheduleQueryController.java` 的 `/{queryId}/manual-nodes`;停止只由实际 `EMPTY_RETURNED` 节点触发,不提供无节点停止入口;外部船司/轨迹调用经 `integration-service/`;标准见 `../../docs/四期/船节点标准化契约.md` |
|
||||
| 国内货柜履约海运订舱、资料/打印、关联候选与出仓前船期资料 | `oceanbooking/controller/PlatformOceanBookingController.java`、`oceanbooking/service/OceanBookingService.java`、`OceanBookingDocumentService.java`、`OceanBookingPrintService.java`、`oceanbooking/mapper/OceanBookingMapper.java`、`portschedule/controller/PlatformPortScheduleController.java`;文件经 `client/OceanBookingFileClient.java` 进入 File,关联候选 `/api/platform/v1/ocean-bookings/link-candidates`,契约 `src/main/resources/openapi/ocean-bookings-v1.yaml`,表结构见 `src/main/resources/db/phase3-ocean-booking-schema.sql` |
|
||||
| 海外仓业务国内出仓后船节点定时查询 | `outbound/dispatch/service/OutboundDispatchService.java` 在确认出仓事务内幂等创建现有 `CarrierScheduleQuery`;编排与租约 `portschedule/service/CarrierScheduleQueryScheduler.java`,任务/十节点事实/异常事实/不可变 attempt `CarrierScheduleQueryService.java`,平台查询与人工补录入口 `PlatformCarrierScheduleQueryController.java` 的 `/{queryId}/manual-nodes`;停止只由实际 `EMPTY_RETURNED` 节点触发,不提供无节点停止入口;外部船司/轨迹调用经 `integration-service/`;标准见 `../../docs/海外仓业务/船节点标准化契约.md` |
|
||||
| 船节点跨区交接事件 | `portschedule/service/CarrierScheduleNodeOutboxService.java`;实际节点落库后发布 `FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED` V1,复用 `LoadingCompletedOutboxRepository`/Relay 投 Event Bridge;海外消费见 `../lastleg-service/src/main/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumer.java` |
|
||||
| Firstleg 业务单据编号 | `service/FirstlegDocumentNumberGenerator.java`;当前支持 `OB`、`TA`、`CB`、`CAB`、`LP` 前缀,按北京时间每日独立编号;具体业务归属以各调用服务为准 |
|
||||
| 订舱级拖车与整柜出仓调度 | `controller/PlatformTruckAppointmentController.java` 的 `/candidates`、`service/TruckAppointmentService.java`、`repository/TruckAppointmentRepository.java`、`outbound/dispatch/controller/PlatformOutboundDispatchController.java`;拖车契约 `src/main/resources/openapi/truck-appointments-v1.yaml`。预约只绑定 `RELEASED` 订舱,候选排除缺提单/有效订舱文件或已有非终态预约的订舱;API 不含客户、柜次、装柜计划或真实柜号,每订舱最多一张非终态预约,PDA 完成不推进拖车状态 |
|
||||
@@ -70,7 +70,7 @@
|
||||
- 订舱号统一使用 `OB-yyMMdd-####`,按北京时间创建日每日独立递增且单日最多 9999;不得以实体 ID 拼接生成。放舱不要求真实柜号,也不得为满足放舱校验伪造柜号。
|
||||
- 订舱列表 `containerCount` 固定为该订舱全部柜型需求 `quantity` 之和,与真实柜关联/解除无关;关联候选只返回 `RELEASED`、剩余数量大于 0 且 `warehouseId + channelCode + containerType` 精确匹配的需求,由服务端集合查询 ACTIVE 分配量。前端不得扇出详情计算或用已关联柜数覆盖该字段,最终关联命令仍须重验目标货柜可修改性。
|
||||
- 拖车预约是平台级订舱聚合,不是客户或柜次资源:请求、实体、查询、响应和附件不得恢复 `customerId/customerName`,公开 DTO 也不得出现 `containerBookingId/loadingPlanId/cabinetNo/containerNo`。创建候选只返回有提单号、ACTIVE `BOOKING_CONFIRMATION` 或 `BILL_OF_LADING` 且没有非终态拖车单的 `RELEASED` 订舱;创建或编辑只冻结订舱仓库/提单与 TRUCKING 供应商快照。每订舱通过查询锁与唯一约束最多一张非终态预约,重复创建返回现有派车单号;PDA 装柜完成不得改变拖车状态。附件使用 `tenantId=0 + TRUCK_APPOINTMENT_ATTACHMENT + appointmentId`,提单文件继续归订舱。
|
||||
- 四期船节点任务从确认柜子离开国内仓库后启用,必须绑定真实订舱和柜次;十个标准节点、预计/实际时间、异常事实、供应商缺失节点和还柜停止规则统一以 `../../docs/四期/船节点标准化契约.md` 为准。`CarrierScheduleQuery` 的正常业务查询槽位固定为 `Asia/Shanghai` 每日 09:00、12:30、16:00;每分钟任务只扫描已到期 `next_allowed_at`,不是每分钟请求船司。成功、无变化、需人工或永久失败进入下一固定槽位;仅技术性 `RETRYABLE_FAILURE` 在当前槽内按15/30分钟退避,含首次最多3次且不得跨下一槽,实际 `EMPTY_RETURNED` 后停止。错过的正常槽位直接顺延,不在服务恢复时补跑;单轮按受控批次持续领取并受最大任务数保护。租约、调度尝试和内部停止入口不得暴露为平台页面 API,外部失败不得推进业务节点,不得在三期另建查询任务。
|
||||
- 海外仓业务船节点任务从确认柜子离开国内仓库后启用,必须绑定真实订舱和柜次;十个标准节点、预计/实际时间、异常事实、供应商缺失节点和还柜停止规则统一以 `../../docs/海外仓业务/船节点标准化契约.md` 为准。`CarrierScheduleQuery` 的正常业务查询槽位固定为 `Asia/Shanghai` 每日 09:00、12:30、16:00;每分钟任务只扫描已到期 `next_allowed_at`,不是每分钟请求船司。成功、无变化、需人工或永久失败进入下一固定槽位;仅技术性 `RETRYABLE_FAILURE` 在当前槽内按15/30分钟退避,含首次最多3次且不得跨下一槽,实际 `EMPTY_RETURNED` 后停止。错过的正常槽位直接顺延,不在服务恢复时补跑;单轮按受控批次持续领取并受最大任务数保护。租约、调度尝试和内部停止入口不得暴露为平台页面 API,外部失败不得推进业务节点,不得在国内货柜履约另建查询任务。
|
||||
- 国内出口报关在装柜完成后启用,不再阻塞 `LOADED`。整柜 `fl_customs_container.status=COMPLETED` 才通过离仓报关门禁;完成后以系统身份、确定性幂等键立即尝试确认国内离仓,其他门禁未齐时由受控定时补偿重试,禁止部分入仓成功或重复创建离仓/船司查询事实。
|
||||
- 国内出仓确认以真实 `containerBookingId` 聚合全部入仓绑定并锁定校验;装柜证据按物理货柜只计算一次,PDA `taskId` 仅用于来源审计和任务授权,File 所有权仍固定 `tenantId=0 + containerBookingId`;拖车状态和独立船期只作参考。全部门禁在同一事务通过后推进所有技术行、记录一条可见历史并幂等创建 `CarrierScheduleQuery/DEPARTED_WAREHOUSE`,不允许单入仓局部成功。
|
||||
- 手工船节点的预计时间只追加事实,不推进 `latestStandardNode`;携带 `actualAt` 时标准节点序号必须严格大于最新实际节点,重复、相同或回退节点返回冲突。已审核实际时间修正只能更新同一事实的时间/证据,不得制造第二条同节点实际事实或绕过单调推进。
|
||||
@@ -78,7 +78,7 @@
|
||||
- Firstleg 只拥有业务费用及履约事实;Finance 拥有费用入池、账期、账单、收款、成本分摊结果和财务状态。修改费用来源版本、发布条件、柜次归属或体积快照时同步检查 `../finance/src/main/java/com/baiyue/finance/client/FirstlegFeeSourceClient.java`、同目录 `FirstlegCostSnapshotClient.java` 及双方契约测试,不在 Firstleg 复制财务状态机。
|
||||
- `DIRECT_PAID` 仅适用于 `PAYABLE`,必须同时保存 `paidAt`、合法 `paymentMethod`,并至少提供 `paymentReferenceNo` 或一个当前客户范围内的 `FINANCE_VOUCHER`;`RECEIVABLE` 与 `ON_ACCOUNT` 不得残留付款字段。创建、调整和审核前均通过 `FeePaymentSnapshotValidator` 失败关闭复核。
|
||||
|
||||
## 三期装柜与 PDA 资源结算
|
||||
## 国内货柜履约装柜与 PDA 资源结算
|
||||
|
||||
- 智能装柜全局工作台由服务端把业务渠道分组映射为实际渠道,并分别分页查询候选与 `WAIT_LOADING/EDITING/LOADING` 柜次;候选状态筛选使用最多四项的 `orderStatuses`,未传表示 `PENDING_REVIEW/WAIT_MEASURE/WAIT_SPLIT/LOADING` 全部未完成状态。状态只用于搜索和展示,能否加入以响应 `canAssign` 为准,由有效柜次占用、剩余箱数、国内中转仓、容量、组锁和版本等事实决定;人工加入允许订单渠道与柜次渠道不同,智能推荐仍只选择柜次渠道。容积和重量均允许达到柜型额定值的 150%,超过即拒绝。待审核事实必须经 `PendingReviewOrderClient` 重新核验 Order 权威 `SUBMITTED` 订单,待审核或待测量可先占位,但确认预装和启动 PDA 前仍必须进入 `WAIT_SPLIT`。同柜不可拆组只有全部成员命中状态集合时才返回,且必须成员完整、满箱并作为单个分页与分配单元,不能跨页或拆分。一次性移除未拣明细时,普通未拣项直接移除;同柜组仅全组未拣时整组移除,任一成员已拣则整组保留,单事务只递增一次计划版本。推荐顺序按入库时间先进先出。装柜计划状态为 `EDITING -> LOADING -> COMPLETED`,PDA 全部明细处理完成后自动完成。
|
||||
- 客户入库费用只在登录客户范围、测量审核通过、`feeStatus=SENT` 且存在已发布明细时可见;确认必须具备 `merchant:order:edit`,携带当前 `expectedFeeVersion` 与幂等键。成功后记录确认成员和时间并将该费用版本永久锁定,平台不得再调整;旧版本、未发送或重复异参确认均返回冲突,不做兼容覆盖。
|
||||
@@ -92,4 +92,4 @@
|
||||
|
||||
## 全量验收导航
|
||||
|
||||
本模块承载一期履约、二期入库测量计费、三期订单编排/柜次/排柜/订舱/拖车/PDA/`LOADED` 门禁等全量链路;按本次验收范围读取对应阶段文档、Controller、DTO、OpenAPI、当前 SQL 定义和调用方,不以历史阶段终点阻断联调。PDA 按 uni-app 在线更新安排开发与验收,不另设设备型号门禁。
|
||||
本模块承载订单履约、入库测量计费、订单编排、柜次、排柜、订舱、拖车、PDA 和 `LOADED` 门禁等全量链路;按本次验收范围读取对应业务域文档、Controller、DTO、OpenAPI、当前 SQL 定义和调用方,不以历史规划终点阻断联调。PDA 按 uni-app 在线更新安排开发与验收,不另设设备型号门禁。
|
||||
|
||||
+3
-3
@@ -60,7 +60,7 @@ public interface InboundPalletAllocationMapper extends BaseMapper<InboundPalletA
|
||||
UPDATE fl_inbound_pallet_allocation
|
||||
SET picked_package_count = picked_package_count + #{count},
|
||||
/* Keep the snapshot confirmed until the atomic warehouse release
|
||||
succeeds. markReleased is the single owner of this transition;
|
||||
succeeds. markBlockReleased is the single owner of this transition;
|
||||
changing it here makes the subsequent idempotent release update
|
||||
affect zero rows and surfaces as S0500 from PDA pick. */
|
||||
allocation_status = 'CONFIRMED',
|
||||
@@ -93,9 +93,9 @@ public interface InboundPalletAllocationMapper extends BaseMapper<InboundPalletA
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_pallet_allocation
|
||||
SET allocation_status='RELEASED', slot_status='RELEASED', updated_at=CURRENT_TIMESTAMP(3)
|
||||
SET allocation_status = 'RELEASED', slot_status = 'RELEASED', updated_at = CURRENT_TIMESTAMP(3)
|
||||
WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND block_id=#{blockId}
|
||||
AND allocation_status='CONFIRMED'
|
||||
AND allocation_status = 'CONFIRMED'
|
||||
""")
|
||||
int markBlockReleased(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("blockId") long blockId);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: 柏悦二期 FBA 计费 API
|
||||
title: 柏悦入库测量与计费 FBA 计费 API
|
||||
version: 2.0.0
|
||||
description: 业务成功和失败均使用 HTTP 200;仅响应体 code 为字符串 "200" 时表示成功。
|
||||
security:
|
||||
|
||||
+3
-3
@@ -161,10 +161,10 @@ class PdaSplitSettlementSqlContractTest {
|
||||
long.class, long.class, long.class)
|
||||
.getAnnotation(Update.class).value()[0];
|
||||
assertThat(release)
|
||||
.contains("allocation_status='RELEASED'")
|
||||
.contains("slot_status='RELEASED'")
|
||||
.contains("allocation_status = 'RELEASED'")
|
||||
.contains("slot_status = 'RELEASED'")
|
||||
.contains("block_id=#{blockId}")
|
||||
.contains("allocation_status='CONFIRMED'");
|
||||
.contains("allocation_status = 'CONFIRMED'");
|
||||
|
||||
String transfer = InboundPalletAllocationMapper.class.getMethod("markBlockTransferred",
|
||||
long.class, long.class, long.class)
|
||||
|
||||
+1
-1
@@ -183,7 +183,7 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
.contains("存在缺失中文注释的业务表")
|
||||
.contains("存在缺失中文注释的业务字段")
|
||||
.doesNotContain("CREATE PROCEDURE upgrade_column_comment");
|
||||
assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(145);
|
||||
assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(147);
|
||||
assertThat(count(sql, "CALL queue_column_comment(")).isGreaterThanOrEqualTo(2374);
|
||||
assertThat(sql)
|
||||
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'supplier_id'")
|
||||
|
||||
@@ -201,8 +201,6 @@ class PdaPickSettlementServiceTest {
|
||||
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 4)));
|
||||
when(allocations.lockConfirmed(10001L, 31L))
|
||||
.thenReturn(List.of(allocation(1L, 1, 501L, 4)));
|
||||
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new WarehousePalletLocationClient.SettlementResult(List.of(released(91L, 501L)), List.of())));
|
||||
writes(0, 4);
|
||||
|
||||
service.settle(1L, 9L, "operator");
|
||||
|
||||
+2
-2
@@ -16,5 +16,5 @@
|
||||
## 修改约束与分期
|
||||
|
||||
- 新增路由同步检查本地/Nacos、App Scope、CORS、公开路径和 Swagger;不得暴露内部令牌接口。
|
||||
- 一期承载登录后 Admin/Client 入口;二期接入到仓/测量/计费公开路由但保持内部测量接口隔离;三期仅在 Controller、权限和公开访问域契约稳定后新增必要路由,内部 PDA、仓储和服务令牌接口继续不得经 Gateway 暴露。
|
||||
- 三期先读 `../../docs/三期/三期开工门禁.md`;网关变更只服务已确认模块,不以路由提前暴露未完成能力。
|
||||
- 订单与客户承载登录后 Admin/Client 入口;入库测量与计费接入到仓/测量/计费公开路由但保持内部测量接口隔离;国内货柜履约仅在 Controller、权限和公开访问域契约稳定后新增必要路由,内部 PDA、仓储和服务令牌接口继续不得经 Gateway 暴露。
|
||||
- 国内货柜履约先读 `../../docs/国内货柜履约/国内货柜履约开工门禁.md`;网关变更只服务已确认模块,不以路由提前暴露未完成能力。
|
||||
|
||||
+1
-1
@@ -37,4 +37,4 @@
|
||||
|
||||
## 全量验收导航
|
||||
|
||||
本模块承载客户建档审核、平台用户/RBAC、企业资料、消息中心及三期作业角色权限;按本次验收链路读取对应阶段/专题文档和权限契约,核对授权快照、客户隔离、权限码和消息收件箱,不因历史阶段划分排除协作场景。
|
||||
本模块承载客户建档审核、平台用户/RBAC、企业资料、消息中心及全流程作业角色权限;按本次验收链路读取对应业务域/专题文档和权限契约,核对授权快照、客户隔离、权限码和消息收件箱,不因历史规划边界排除协作场景。
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# IAM 一期交付说明 / IAM Phase 1 Delivery
|
||||
# IAM 订单与客户交付说明 / IAM Phase 1 Delivery
|
||||
|
||||
## 数据库归属 / Database ownership
|
||||
|
||||
@@ -37,7 +37,7 @@ Merge the packages under `src/main/java` into `D:/baiyue/iam/src/main/java`; exe
|
||||
|
||||
初始化器及其补偿必须幂等。任一初始化器失败时,协调器按完成顺序的逆序执行补偿;IAM 本地事务最终回滚时也会触发同一补偿链。初始化器名称必须非空且全局唯一,初始密码等敏感结果只能通过审核响应一次性返回,不得持久化到 IAM 或写入日志。
|
||||
|
||||
管理员初始化会验证既有 OWNER 成员、IAM 用户及 Auth 账号均处于可用状态且租户、成员和应用范围一致。历史数据只有 OWNER/IAM 身份而缺少 Auth 账号时,复用原身份补建认证账号;停用或范围冲突的数据会阻止审核通过,避免激活后没有可登录管理员。部署前必须执行 `phase1-onboarding.sql`;测试环境统一执行 `deploy/sql/2026-07-31一期回归测试环境增量迁移.sql`,确保全局 `MERCHANT_OWNER` 模板及权限完整。
|
||||
管理员初始化会验证既有 OWNER 成员、IAM 用户及 Auth 账号均处于可用状态且租户、成员和应用范围一致。历史数据只有 OWNER/IAM 身份而缺少 Auth 账号时,复用原身份补建认证账号;停用或范围冲突的数据会阻止审核通过,避免激活后没有可登录管理员。部署前必须执行 `phase1-onboarding.sql`;测试环境统一执行 `deploy/sql/2026-07-31订单与客户回归测试环境增量迁移.sql`,确保全局 `MERCHANT_OWNER` 模板及权限完整。
|
||||
|
||||
OpenAPI is synchronized in `openapi/iam-v1.yaml`.
|
||||
|
||||
|
||||
@@ -67,7 +67,11 @@ VALUES
|
||||
(5107221,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:create','创建车辆主数据',NULL,'POST',921,'ACTIVE',1,0),
|
||||
(5107222,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:edit','编辑车辆主数据',NULL,'PUT',922,'ACTIVE',1,0),
|
||||
(5107223,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:status','变更车辆状态',NULL,'PATCH',923,'ACTIVE',1,0),
|
||||
(5107224,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:export','导出车辆主数据',NULL,'GET',924,'ACTIVE',1,0)
|
||||
(5107224,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:export','导出车辆主数据',NULL,'GET',924,'ACTIVE',1,0),
|
||||
(5306100,0,'MERCHANT','MENU',0,'merchant:cross-border:procurement','海外采购','/cross-border/procurement',NULL,70,'ACTIVE',1,0),
|
||||
(5306110,0,'MERCHANT','MENU',0,'merchant:cross-border:domestic-trucking','美国本土卡','/cross-border/domestic-trucking',NULL,80,'ACTIVE',1,0),
|
||||
(5306120,0,'MERCHANT','MENU',0,'merchant:cross-border:pallet-shipping','美本土散板','/cross-border/pallet-shipping',NULL,90,'ACTIVE',1,0),
|
||||
(5306130,0,'MERCHANT','MENU',0,'merchant:warehouse:inbound:list','海外仓入库订单','/orders/inbound',NULL,50,'ACTIVE',1,0)
|
||||
ON DUPLICATE KEY UPDATE permission_name=VALUES(permission_name), route_path=VALUES(route_path),
|
||||
http_method=VALUES(http_method), sort_no=VALUES(sort_no), status='ACTIVE', built_in=1, deleted=0;
|
||||
|
||||
@@ -203,6 +207,52 @@ WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
|
||||
AND UPPER(role.role_code)='PLATFORM_CUSTOM_ADMIN'
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||
|
||||
-- 客户默认管理员获得当前客户海外业务入口;其他客户角色按企业实际分工授权。
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, role.id, permission.id, NOW(3)
|
||||
FROM iam_role role
|
||||
JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code='MERCHANT'
|
||||
AND permission.permission_code IN (
|
||||
'merchant:cross-border:procurement',
|
||||
'merchant:cross-border:domestic-trucking',
|
||||
'merchant:cross-border:pallet-shipping',
|
||||
'merchant:warehouse:inbound:list'
|
||||
)
|
||||
WHERE role.tenant_id=0 AND role.app_code='MERCHANT' AND role.role_code='MERCHANT_OWNER'
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||
|
||||
DROP PROCEDURE IF EXISTS assert_overseas_merchant_permissions;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE assert_overseas_merchant_permissions()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM iam_permission
|
||||
WHERE tenant_id=0 AND app_code='MERCHANT' AND status='ACTIVE' AND deleted=0
|
||||
AND permission_code IN (
|
||||
'merchant:cross-border:procurement',
|
||||
'merchant:cross-border:domestic-trucking',
|
||||
'merchant:cross-border:pallet-shipping',
|
||||
'merchant:warehouse:inbound:list')) <> 4 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外客户权限初始化失败:权限资源不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM iam_role_permission grant_row
|
||||
JOIN iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
|
||||
JOIN iam_permission permission ON permission.id=grant_row.permission_id
|
||||
WHERE role.tenant_id=0 AND role.app_code='MERCHANT' AND role.role_code='MERCHANT_OWNER'
|
||||
AND role.status='ACTIVE' AND role.deleted=0
|
||||
AND permission.tenant_id=0 AND permission.app_code='MERCHANT'
|
||||
AND permission.status='ACTIVE' AND permission.deleted=0
|
||||
AND permission.permission_code IN (
|
||||
'merchant:cross-border:procurement',
|
||||
'merchant:cross-border:domestic-trucking',
|
||||
'merchant:cross-border:pallet-shipping',
|
||||
'merchant:warehouse:inbound:list')) <> 4 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外客户权限初始化失败:客户默认管理员授权不完整';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL assert_overseas_merchant_permissions();
|
||||
DROP PROCEDURE assert_overseas_merchant_permissions;
|
||||
|
||||
-- Tail-leg order actions are available to the current platform administrator role.
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, role.id, permission.id, NOW(3)
|
||||
|
||||
@@ -124,7 +124,7 @@ paths:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 查询当前用户有效权限
|
||||
description: 返回当前有效平台成员权限编码;只要求有效 PLATFORM JWT,不依赖 `platform:role:list`。五期海外仓返回 `platform:overseas-warehouse:dispatch:list`、`platform:overseas-warehouse:dispatch:edit`、`platform:overseas-warehouse:dispatch:cancel`、`platform:overseas-warehouse:dispatch:transfer-delivery`、`platform:overseas-warehouse:outbound:list`、`platform:overseas-warehouse:outbound:create`、`platform:overseas-warehouse:outbound:cancel`、`platform:overseas-warehouse:delivery:list`、`platform:overseas-warehouse:delivery:return`、`platform:overseas-warehouse:delivery:status`、`platform:overseas-warehouse:exception-order:list`、`platform:overseas-warehouse:exception-order:create`、`platform:overseas-warehouse:customer-visit:list`、`platform:overseas-warehouse:customer-visit:edit`;列表权限包含详情和只读辅助接口,写操作独立授权。
|
||||
description: 返回当前有效平台成员权限编码;只要求有效 PLATFORM JWT,不依赖 `platform:role:list`。海外仓返回 `platform:overseas-warehouse:dispatch:list`、`platform:overseas-warehouse:dispatch:edit`、`platform:overseas-warehouse:dispatch:cancel`、`platform:overseas-warehouse:dispatch:transfer-delivery`、`platform:overseas-warehouse:outbound:list`、`platform:overseas-warehouse:outbound:create`、`platform:overseas-warehouse:outbound:cancel`、`platform:overseas-warehouse:delivery:list`、`platform:overseas-warehouse:delivery:return`、`platform:overseas-warehouse:delivery:status`、`platform:overseas-warehouse:exception-order:list`、`platform:overseas-warehouse:exception-order:create`、`platform:overseas-warehouse:customer-visit:list`、`platform:overseas-warehouse:customer-visit:edit`;海外 PDA 盘点、移库、换标和现场查询统一返回 `lastleg:pda:operate`,且仅限 `OVERSEAS_WAREHOUSE_PDA_OPERATOR`。列表权限包含详情和只读辅助接口,写操作独立授权。
|
||||
responses:
|
||||
'200':
|
||||
description: 当前平台成员权限查询成功
|
||||
@@ -326,7 +326,7 @@ paths:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 查询当前用户有效权限
|
||||
description: 返回当前有效客户成员权限编码。五期客户派送返回 `merchant:warehouse:delivery:list` 和 `merchant:warehouse:delivery:create`;列表权限包含详情只读,创建权限独立授权。
|
||||
description: 返回当前有效客户成员权限编码。海外采购、本土卡、本土散板和海外入库订单分别返回 `merchant:cross-border:procurement`、`merchant:cross-border:domestic-trucking`、`merchant:cross-border:pallet-shipping`、`merchant:warehouse:inbound:list`;客户派送返回 `merchant:warehouse:delivery:list` 和 `merchant:warehouse:delivery:create`。列表权限包含详情只读,创建权限独立授权。
|
||||
responses:
|
||||
'200':
|
||||
description: 当前商户成员权限查询成功
|
||||
|
||||
@@ -36,6 +36,12 @@ class IamOpenApiContractTest {
|
||||
.contains("platform:overseas-warehouse:outbound:cancel")
|
||||
.contains("merchant:warehouse:delivery:list")
|
||||
.contains("merchant:warehouse:delivery:create")
|
||||
.contains("merchant:cross-border:procurement")
|
||||
.contains("merchant:cross-border:domestic-trucking")
|
||||
.contains("merchant:cross-border:pallet-shipping")
|
||||
.contains("merchant:warehouse:inbound:list")
|
||||
.contains("lastleg:pda:operate")
|
||||
.contains("OVERSEAS_WAREHOUSE_PDA_OPERATOR")
|
||||
.contains("列表权限包含详情只读")
|
||||
.contains("PermissionCodesApiResponse:")
|
||||
.contains("TenantPageApiResponse:")
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class OverseasCustomerPermissionSqlContractTest {
|
||||
private static final Pattern MERCHANT_CODE = Pattern.compile("merchant:[a-z0-9:-]+");
|
||||
private static final Set<String> CUSTOMER_OVERSEAS_PERMISSIONS = Set.of(
|
||||
"merchant:cross-border:procurement",
|
||||
"merchant:cross-border:domestic-trucking",
|
||||
"merchant:cross-border:pallet-shipping",
|
||||
"merchant:warehouse:inbound:list");
|
||||
|
||||
@Test
|
||||
void declaresTheFrozenCustomerOverseasPermissionCatalog() throws Exception {
|
||||
String sql = sql();
|
||||
|
||||
assertThat(merchantCodes(sql)).containsExactlyInAnyOrderElementsOf(CUSTOMER_OVERSEAS_PERMISSIONS);
|
||||
for (String permission : CUSTOMER_OVERSEAS_PERMISSIONS) {
|
||||
assertThat(sql).contains("0,'MERCHANT','MENU'", "'" + permission + "'");
|
||||
}
|
||||
assertThat(sql).contains(
|
||||
"'merchant:cross-border:procurement','海外采购','/cross-border/procurement'",
|
||||
"'merchant:cross-border:domestic-trucking','美国本土卡','/cross-border/domestic-trucking'",
|
||||
"'merchant:cross-border:pallet-shipping','美本土散板','/cross-border/pallet-shipping'",
|
||||
"'merchant:warehouse:inbound:list','海外仓入库订单','/orders/inbound'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void grantsAllFourPermissionsOnlyToTheDefaultCustomerAdministrator() throws Exception {
|
||||
String sql = sql();
|
||||
int comment = sql.indexOf("-- 客户默认管理员获得当前客户海外业务入口");
|
||||
int grantStart = sql.indexOf("INSERT IGNORE INTO iam_role_permission", comment);
|
||||
int grantEnd = sql.indexOf(';', grantStart);
|
||||
|
||||
assertThat(comment).isGreaterThanOrEqualTo(0);
|
||||
assertThat(grantStart).isGreaterThan(comment);
|
||||
assertThat(grantEnd).isGreaterThan(grantStart);
|
||||
String grant = sql.substring(grantStart, grantEnd);
|
||||
assertThat(merchantCodes(grant)).containsExactlyInAnyOrderElementsOf(CUSTOMER_OVERSEAS_PERMISSIONS);
|
||||
assertThat(grant).contains(
|
||||
"role.app_code='MERCHANT'",
|
||||
"role.role_code='MERCHANT_OWNER'",
|
||||
"permission.app_code='MERCHANT'");
|
||||
assertThat(grant).doesNotContain("PLATFORM_ADMIN", "PLATFORM_CUSTOM_ADMIN");
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertsCatalogAndOwnerGrantCompleteness() throws Exception {
|
||||
String sql = sql();
|
||||
|
||||
assertThat(sql).contains(
|
||||
"assert_overseas_merchant_permissions",
|
||||
"海外客户权限初始化失败:权限资源不完整",
|
||||
"海外客户权限初始化失败:客户默认管理员授权不完整",
|
||||
"permission.app_code='MERCHANT'",
|
||||
"role.role_code='MERCHANT_OWNER'",
|
||||
")) <> 4 THEN");
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsAllOverseasPdaOperationsBehindTheSingleExistingPermission() throws Exception {
|
||||
String sql = sql();
|
||||
Set<String> pdaCodes = new LinkedHashSet<>();
|
||||
Pattern.compile("lastleg:pda:[a-z0-9:-]+")
|
||||
.matcher(sql).results().map(result -> result.group()).forEach(pdaCodes::add);
|
||||
|
||||
assertThat(pdaCodes).containsExactly("lastleg:pda:operate");
|
||||
assertThat(sql).contains(
|
||||
"role.id=4000005",
|
||||
"role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR'",
|
||||
"存在非海外仓 PDA 作业员授权");
|
||||
assertThat(sql).doesNotContain(
|
||||
"lastleg:pda:stocktake",
|
||||
"lastleg:pda:transfer",
|
||||
"lastleg:pda:relabel",
|
||||
"lastleg:pda:query");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesTheFrozenLastlegPermissionConstants() throws Exception {
|
||||
Path constants = Path.of(
|
||||
"lastleg-service/src/main/java/com/baiyue/lastleg/security/OverseasPermissions.java");
|
||||
if (!Files.exists(constants)) constants = Path.of("..").resolve(constants);
|
||||
|
||||
assertThat(merchantCodes(Files.readString(constants, StandardCharsets.UTF_8)))
|
||||
.containsAll(CUSTOMER_OVERSEAS_PERMISSIONS);
|
||||
}
|
||||
|
||||
private static Set<String> merchantCodes(String source) {
|
||||
Set<String> values = new LinkedHashSet<>();
|
||||
MERCHANT_CODE.matcher(source).results().map(result -> result.group()).forEach(values::add);
|
||||
return values;
|
||||
}
|
||||
|
||||
private static String sql() throws Exception {
|
||||
return new ClassPathResource("db/lastleg-permissions.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -138,17 +138,17 @@ class Phase5DeliveryPermissionSqlContractTest {
|
||||
void domesticUnifiedEntryInlinesTheCompleteModuleSqlAfterPhase4Permissions() throws Exception {
|
||||
String moduleSql = sql().trim();
|
||||
String unifiedSql = repositoryFile("deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
int phase4 = unifiedSql.indexOf("-- 四期海外权限统一目录");
|
||||
int phase5 = unifiedSql.indexOf("-- 五期海外排车、出库与派送权限(来源:iam/db/phase5-delivery-permissions.sql)");
|
||||
int bodyStart = unifiedSql.indexOf("USE baiyue_iam;", phase5);
|
||||
int overseasCatalog = unifiedSql.indexOf("-- 海外权限统一目录");
|
||||
int outboundDelivery = unifiedSql.indexOf("-- 海外排车、出库与派送权限(来源:iam/db/phase5-delivery-permissions.sql)");
|
||||
int bodyStart = unifiedSql.indexOf("USE baiyue_iam;", outboundDelivery);
|
||||
int nextSection = unifiedSql.indexOf("-- IAM 客户提交即成功", bodyStart);
|
||||
|
||||
assertThat(phase4).isGreaterThanOrEqualTo(0);
|
||||
assertThat(phase5).isGreaterThan(phase4);
|
||||
assertThat(bodyStart).isGreaterThan(phase5);
|
||||
assertThat(overseasCatalog).isGreaterThanOrEqualTo(0);
|
||||
assertThat(outboundDelivery).isGreaterThan(overseasCatalog);
|
||||
assertThat(bodyStart).isGreaterThan(outboundDelivery);
|
||||
assertThat(nextSection).isGreaterThan(bodyStart);
|
||||
assertThat(unifiedSql.substring(bodyStart, nextSection).trim()).isEqualTo(moduleSql);
|
||||
assertThat(codes(unifiedSql.substring(phase5, nextSection)))
|
||||
assertThat(codes(unifiedSql.substring(outboundDelivery, nextSection)))
|
||||
.containsExactlyInAnyOrderElementsOf(REQUIRED);
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ class Phase5DeliveryPermissionSqlContractTest {
|
||||
String readme = repositoryFile("deploy/sql/README.md");
|
||||
|
||||
assertThat(readme).contains(
|
||||
"五期海外排车、出库与派送权限",
|
||||
"海外排车、出库与派送权限",
|
||||
"测试和部署只执行固定入口,不再单独执行模块脚本",
|
||||
"排车列表/编辑/取消/转派送",
|
||||
"异常订单列表/创建和客户回访列表/编辑十四项权限",
|
||||
|
||||
@@ -90,6 +90,26 @@ class AuthorizationSnapshotServiceTest {
|
||||
"merchant:warehouse:delivery:list");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesCustomerOverseasBusinessPermissionsFromCurrentRoleAssignments() {
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of("MERCHANT_OWNER"));
|
||||
when(mapper.findPermissions(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of(
|
||||
"merchant:cross-border:procurement",
|
||||
"merchant:cross-border:domestic-trucking",
|
||||
"merchant:cross-border:pallet-shipping",
|
||||
"merchant:warehouse:inbound:list"));
|
||||
|
||||
var result = service.currentPermissions(new AuthenticatedUser(42L, 7L, 7L, 9L,
|
||||
"owner", "MERCHANT", "MERCHANT", java.util.Set.of()), "MERCHANT");
|
||||
|
||||
assertThat(result).containsExactlyInAnyOrder(
|
||||
"merchant:cross-border:procurement",
|
||||
"merchant:cross-border:domestic-trucking",
|
||||
"merchant:cross-border:pallet-shipping",
|
||||
"merchant:warehouse:inbound:list");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsIncompleteCurrentIdentityAsBusinessError() {
|
||||
assertThatThrownBy(() -> service
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
## 职责与边界
|
||||
|
||||
- 负责百度 OCR、Amazon Logistics/SP-API、四期船司/船舶轨迹查询等第三方适配和能力开关;不保存业务主数据。
|
||||
- 二期重量 OCR 由 `file/` 调用;本服务只返回识别结果,不保存 `file_object`、入库单或重量确认记录。
|
||||
- 负责百度 OCR、Amazon Logistics/SP-API、海外仓业务船司/船舶轨迹查询等第三方适配和能力开关;不保存业务主数据。
|
||||
- 入库测量与计费重量 OCR 由 `file/` 调用;本服务只返回识别结果,不保存 `file_object`、入库单或重量确认记录。
|
||||
|
||||
## 模块入口与验证
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
- 内部接口:`controller/IntegrationProviderController.java`、`WeightOcrController.java`
|
||||
- OCR 编排:`service/ocr/WeightOcrBatchService.java`、`ImageCropper.java`、`WeightCandidateSelector.java`
|
||||
- 供应商适配:`provider/IntegrationProviderAdapter.java`、`BaiduOcrAdapter.java`、`AmazonLogisticsAdapter.java`
|
||||
- 四期船节点外部适配:内部契约 `controller/IntegrationProviderController.java` 的 `/internal/integrations/carrier-schedules/query`,统一十节点与四类异常 DTO `dto/CarrierScheduleDtos.java`,provider SPI `provider/CarrierScheduleProviderAdapter.java`,标准枚举 `domain/CarrierScheduleStandardNode.java`、`CarrierScheduleExceptionCode.java`;美森网页会话、动态 `ttr` 与公开查询代理适配见 `provider/MatsonTrackingClient.java`、`MatsonCarrierScheduleProviderAdapter.java`,由 `MATSON_TRACKING_ENABLED` 显式启用;SML 的 122→125→124→126 JSON 调用链与 A/E 时间解析见 `provider/SmlTrackingClient.java`、`SmlCarrierScheduleProviderAdapter.java`,由 `SML_TRACKING_ENABLED` 显式启用;ZIM 正式 Tracing API 的 OAuth2 token 缓存、401 单次刷新及柜号活动解析见 `provider/ZimTrackingClient.java`、`ZimCarrierScheduleProviderAdapter.java`,凭据只从外部 secret 注入并由 `ZIM_TRACKING_ENABLED` 显式启用;长荣柜号→关联提单两段 UTF-8 HTML 查询、表头定位与日粒度事实解析见 `provider/EvergreenTrackingClient.java`、`EvergreenCarrierScheduleProviderAdapter.java`,由 `EMC_TRACKING_ENABLED` 显式启用且不输出完整提单号;合德 UTF-8 HTML 主表解析见 `provider/HedeTrackingClient.java`、`HedeCarrierScheduleProviderAdapter.java`,由 `HEDE_TRACKING_ENABLED` 显式启用且当前只自动解析柜号;轮询任务、租约、订舱/柜次关联、最新实际节点、节点/异常事实和历史归 `firstleg/portschedule/`,标准见 `../../docs/四期/船节点标准化契约.md`
|
||||
- 海外仓业务船节点外部适配:内部契约 `controller/IntegrationProviderController.java` 的 `/internal/integrations/carrier-schedules/query`,统一十节点与四类异常 DTO `dto/CarrierScheduleDtos.java`,provider SPI `provider/CarrierScheduleProviderAdapter.java`,标准枚举 `domain/CarrierScheduleStandardNode.java`、`CarrierScheduleExceptionCode.java`;美森网页会话、动态 `ttr` 与公开查询代理适配见 `provider/MatsonTrackingClient.java`、`MatsonCarrierScheduleProviderAdapter.java`,由 `MATSON_TRACKING_ENABLED` 显式启用;SML 的 122→125→124→126 JSON 调用链与 A/E 时间解析见 `provider/SmlTrackingClient.java`、`SmlCarrierScheduleProviderAdapter.java`,由 `SML_TRACKING_ENABLED` 显式启用;ZIM 正式 Tracing API 的 OAuth2 token 缓存、401 单次刷新及柜号活动解析见 `provider/ZimTrackingClient.java`、`ZimCarrierScheduleProviderAdapter.java`,凭据只从外部 secret 注入并由 `ZIM_TRACKING_ENABLED` 显式启用;长荣柜号→关联提单两段 UTF-8 HTML 查询、表头定位与日粒度事实解析见 `provider/EvergreenTrackingClient.java`、`EvergreenCarrierScheduleProviderAdapter.java`,由 `EMC_TRACKING_ENABLED` 显式启用且不输出完整提单号;合德 UTF-8 HTML 主表解析见 `provider/HedeTrackingClient.java`、`HedeCarrierScheduleProviderAdapter.java`,由 `HEDE_TRACKING_ENABLED` 显式启用且当前只自动解析柜号;轮询任务、租约、订舱/柜次关联、最新实际节点、节点/异常事实和历史归 `firstleg/portschedule/`,标准见 `../../docs/海外仓业务/船节点标准化契约.md`
|
||||
- 安全:`security/InternalServiceTokenGuard.java`;配置 `application.yml`、`../config/nacos/{local,cn}/integration-service.yml`
|
||||
- 测试:`src/test/java/com/baiyue/integration/`;`mvn -pl integration-service -am test`
|
||||
|
||||
## 修改约束与分期
|
||||
|
||||
- 密钥、原始响应和客户敏感数据不得写入日志;真实调用默认关闭,适配器负责超时、重试、限流、错误归一化。
|
||||
- 船节点适配器不得自行维护业务轮询任务或到港状态;必须返回可审计的来源、观测时间、标准化节点和错误结果,由 Firstleg 负责幂等落库与停止条件。查询周期和供应商额度通过配置表达,不硬编码复用三期人工查询限制。
|
||||
- 船节点适配器不得自行维护业务轮询任务或到港状态;必须返回可审计的来源、观测时间、标准化节点和错误结果,由 Firstleg 负责幂等落库与停止条件。查询周期和供应商额度通过配置表达,不硬编码复用国内货柜履约人工查询限制。
|
||||
- OCR 只能访问受控输入/输出目录,接口仅接受内部服务令牌;契约变更同步检查 File 的 `WeightOcrClient` 和测试。
|
||||
- 全量验收按业务链路核对通用适配、File 重量 OCR 及已确认的报关协作契约;柜子确认国内出仓后的船司/船节点外部查询整体归四期,不再列作三期能力。外部调用必须保留来源、频率、限流、超时、最近有效结果和过期标识,凭证缺失按既定人工修正降级,不把未确认的自动化能力当作已实现契约。相关依据:`../../docs/一期/`、`../../docs/二期/`、`../../docs/三期/`、`../../docs/四期/`。
|
||||
- 全量验收按业务链路核对通用适配、File 重量 OCR 及已确认的报关协作契约;柜子确认国内出仓后的船司/船节点外部查询整体归海外仓业务,不再列作国内货柜履约能力。外部调用必须保留来源、频率、限流、超时、最近有效结果和过期标识,凭证缺失按既定人工修正降级,不把未确认的自动化能力当作已实现契约。相关依据:`../../docs/订单与客户/`、`../../docs/入库测量与计费/`、`../../docs/国内货柜履约/`、`../../docs/海外仓业务/`。
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
| 独立尾程入仓单快照 | `entity/LastlegInboundOrder.java`、`mapper/LastlegInboundOrderMapper.java`;按客户与来源入仓标识冻结 inboundCode、来源/根订单及目的仓,不以尾程订单行代替入仓单聚合 |
|
||||
| 内部令牌保护 | `security/InternalServiceTokenGuard.java` |
|
||||
| IAM 海外安全投影与实时校验 | 内部入口 `security/controller/LastlegSecurityController.java`,固定 `/internal/lastleg/security/v1/events`、`/validate` 且复用 `InternalServiceTokenGuard`;投影/校验 `security/service/`,DTO `security/dto/`,表固定 `ll_security_event_inbox`、`ll_security_projection`;MySQL 投影缺失、落后或不可用一律 fail-closed,Redis 仅为提交后可重建镜像;业务切片不得直接依赖投影内部类,边界由 `SecurityProjectionArchitectureTest` 与 Node 合同锁定 |
|
||||
| 入仓计划分页、转换与详情 | `inboundplan/controller/InboundPlanController.java`、`inboundplan/dto/InboundPlanRequests.java`、`InboundPlanListResponse.java`、`inboundplan/mapper/InboundPlanQueryMapper.java`、`inboundplan/service/InboundPlanService.java`;整柜转换固定为 `/convert-container`,锁定同一 `sourceContainerBookingId` 后每票独立生成计划,整批原子且幂等 |
|
||||
| 入仓计划分页、转换与详情 | 平台 `inboundplan/controller/InboundPlanController.java`,客户只读 `inboundplan/controller/MerchantInboundPlanController.java`;共用 `inboundplan/dto/InboundPlanRequests.java`、`InboundPlanListResponse.java`、`inboundplan/mapper/InboundPlanQueryMapper.java`、`inboundplan/service/InboundPlanService.java`;客户入口固定 `/api/merchant/v1/last-leg/inbound-plans`,范围只取 Token;整柜转换固定为 `/convert-container`,锁定同一 `sourceContainerBookingId` 后每票独立生成计划,整批原子且幂等 |
|
||||
| 托盘分配算法 | `inboundplan/service/PalletAllocator.java` |
|
||||
| FBA 预约创建、详情与编辑 | `InboundPlanController.java` 的 `/fba-appointments/**`、`inboundplan/service/InboundFbaAppointmentService.java`、`BatchFbaAppointmentRequest.java`、`UpdateFbaAppointmentRequest.java` |
|
||||
| PDA 入仓、实收、上架与完成 | `inboundplan/pda/controller/ReceivingContainerController.java`、`inboundplan/pda/service/ReceivingContainerService.java`、内部编排 `InboundPdaWorkflowService.java`;公网入口固定为 `/api/pda/v1/last-leg/receiving-containers/**`,`/warehouse-profile` 只按 Token 绑定仓解析共享仓库中文名称与编码,旧 planNo/allocationId Controller 已移除 |
|
||||
@@ -30,10 +30,10 @@
|
||||
| 卸柜照片与海外卸柜完成 Outbox | `ReceivingContainerController.java`、`ReceivingContainerService.java`、`inboundplan/pda/service/InboundUnloadCompletedOutboxService.java`、`entity/LastlegEventOutbox.java`、`mapper/LastlegEventOutboxMapper.java`;多计划同柜只能在全柜完成后调用 `publishContainerCompleted`,按来源柜预订 ID 幂等发布一次,完成时刻与拍照时刻分离;事件模型见 `../common-event-contract/AGENTS.md` |
|
||||
| 尾程暂扣状态回传 | `service/InboundOrderDetainedOutboxService.java`;暂扣与解除暂扣在订单更新事务内按 `sourceInboundId` 写入海外到国内 Outbox,国内按来源入仓单绑定更新 |
|
||||
| 入仓后 SKU 库存余额、库位和流水 | `inventory/`;平台只读查询与非出库库存命令分别从 `inventory/query/controller/InventoryQueryController.java`、`inventory/command/controller/InventoryCommandController.java` 进入 |
|
||||
| 海外PDA盘点、同仓整托移库、托盘组拆换绑与换标现场作业 | `pda/stocktake/`、`pda/move/`、`palletops/`、`relabel/pda/`;版本化契约 `src/main/resources/openapi/overseas-pda-operations-v1.yaml`;全部范围取自PDA Token,不含出库和派送 |
|
||||
| 海外PDA盘点、同仓整托移库、托盘组拆换绑与换标现场作业 | `pda/stocktake/`、`pda/move/`、`palletops/`、`relabel/pda/`;版本化契约 `src/main/resources/openapi/overseas-pda-operations-v1.yaml`;全部范围取自PDA Token,排车和出库使用独立 PDA Controller |
|
||||
| PDA 仓内通用扫码与操作历史 | `pda/warehouse/controller/PdaWarehouseController.java`、`pda/warehouse/service/PdaWarehouseService.java`;解析库存、库位和托盘码并查询操作历史,固定要求 `lastleg:pda:operate` 与 `ROLE_WAREHOUSE_PICKER`,仓库范围只能取自 PDA Token |
|
||||
| 船节点接收与海外 PDA 可收货门禁 | `shipping/service/ContainerShippingNodeConsumer.java`、`shipping/entity/ContainerShippingGate.java`、`shipping/mapper/ContainerShippingGateMapper.java`;只单调接受实际标准节点,`PICKUP_READY` 及以后才允许对应柜进入 PDA 收货候选 |
|
||||
| 平台排车列表/详情与智能预排 | `dispatch/controller/DispatchQueryController.java`、`SmartDispatchController.java`、`dispatch/mapper/DispatchCandidateMapper.java`、`dispatch/service/DispatchService.java`;草稿柜 `entity/DispatchDraftContainer.java`、`mapper/DispatchDraftContainerMapper.java`,托盘锁 `DispatchPalletLock.java`;入口 `/api/platform/v1/last-leg/dispatches`,范围开关与禁用写操作见“四期开发范围路由” |
|
||||
| 平台排车列表/详情与智能预排 | `dispatch/controller/DispatchQueryController.java`、`SmartDispatchController.java`、`dispatch/mapper/DispatchCandidateMapper.java`、`dispatch/service/DispatchService.java`;草稿柜 `entity/DispatchDraftContainer.java`、`mapper/DispatchDraftContainerMapper.java`,托盘锁 `DispatchPalletLock.java`;入口 `/api/platform/v1/last-leg/dispatches`,查询、预排与执行边界见“海外仓全流程开发路由” |
|
||||
| 排车编辑、PDA 出库、派送与交接 | `dispatch/controller/DispatchController.java`、`dispatch/pda/controller/DispatchPdaController.java`、`delivery/controller/DeliveryTransferController.java`、`MerchantDeliveryController.java`、`OutboundPdaController.java` |
|
||||
| 仓储资源调用 | `inboundplan/pda/gateway/WarehouseResourceGateway.java`、`HttpWarehouseResourceGateway.java` |
|
||||
| 仓储转运、历史订单/入仓绑定、可选换标联动 | `transfer/controller/WarehouseTransferController.java`、`transfer/service/WarehouseTransferService.java` |
|
||||
@@ -41,20 +41,20 @@
|
||||
| 货物废弃、待审锁箱、审批扣减 | `discard/controller/CargoDiscardController.java`、`discard/service/CargoDiscardService.java` |
|
||||
| 客户拜访、派送状态与签收附件 | `customervisit/controller/CustomerVisitController.java`、`customervisit/service/CustomerVisitService.java`;平台维护拜访、附件上传、派送完成/状态 |
|
||||
| 异常订单与派送退回入仓 | `exceptionorder/controller/ExceptionOrderController.java`、`exceptionorder/service/ExceptionOrderService.java`;异常订单建档、关联解析、派送退回候选与标记 |
|
||||
| 海外采购、客户下单、采购审核、客户/采购仓候选与操作流水 | `procurement/controller/`、`procurement/service/OverseasProcurementService.java`;OpenAPI `src/main/resources/openapi/overseas-business-v1.yaml` |
|
||||
| 海外采购、客户下单、采购审核、客户/采购仓候选与操作流水 | `procurement/controller/`、`procurement/service/OverseasProcurementService.java`;客户入口要求 `merchant:cross-border:procurement` 且只取 Token 客户范围;OpenAPI `src/main/resources/openapi/overseas-business-v1.yaml` |
|
||||
| 海外业务费用来源 | `financefee/controller/PlatformOverseasFeeSourceController.java`、`financefee/service/OverseasFeeSourceService.java`、`OverseasFeeBusinessGuard.java`、`OverseasFeeOutboxService.java`、`OverseasFeeOutboxRelay.java`;海外入仓、采购、美国卡车、本土散板和海外仓服务费必须匹配当前客户的真实业务,入仓记录和入仓码按事实选填;审核通过、调整或作废后以版本化完整快照经 Outbox/Event Bridge 进入独立 Finance,OpenAPI `src/main/resources/openapi/overseas-finance-fees-v1.yaml` |
|
||||
| 美国/本土卡车订单 | `trucking/controller/PlatformUsTruckOrderController.java`、`MerchantUsTruckOrderController.java` 及同子域 Service/DTO |
|
||||
| 本土散板订单 | `looseboard/controller/PlatformLocalLooseboardController.java` 及同子域 Service/DTO/Entity/Mapper;OpenAPI `src/main/resources/openapi/overseas-business-v1.yaml` |
|
||||
| 美国/本土卡车订单 | `trucking/controller/PlatformUsTruckOrderController.java`、`MerchantUsTruckOrderController.java` 及同子域 Service/DTO;客户入口要求 `merchant:cross-border:domestic-trucking` |
|
||||
| 本土散板订单 | `looseboard/controller/PlatformLocalLooseboardController.java`、`MerchantLocalLooseboardController.java` 及同子域 Service/DTO/Entity/Mapper;客户入口要求 `merchant:cross-border:pallet-shipping`;OpenAPI `src/main/resources/openapi/overseas-business-v1.yaml` |
|
||||
| 海外仓报价、月度汇率、车辆主数据 | 报价 `masterdata/quote/`、汇率 `masterdata/exchange/`;车辆入口 `masterdata/vehicle/adapter/in/web/PlatformVehicleController.java`,应用服务 `application/VehicleApplicationService.java`,持久化端口与适配器 `application/port/out/VehicleRepository.java`、`adapter/out/persistence/`;OpenAPI `src/main/resources/openapi/overseas-master-data-v1.yaml`,车辆档案不含派送履约历史 |
|
||||
| 跨服务 HTTP 客户端装配 | `config/LastlegRestClientFactory.java`、`config/LastlegRestClientProperties.java`;目标地址与内部服务令牌只来自配置,不在业务子域手工创建客户端 |
|
||||
| 数据库基线、表索引与中文注释契约 | `src/main/resources/db/lastleg.sql`、`src/test/java/com/baiyue/lastleg/schema/SchemaChineseCommentContractTest.java` |
|
||||
| 测试 | `src/test/java/com/baiyue/lastleg/` |
|
||||
| 真实事件消费验收 | `src/test/java/com/baiyue/lastleg/service/TaillegRealConsumerAcceptanceTest.java`;覆盖真实 V2 建单、独立入仓单快照及下游转运/换标可消费性 |
|
||||
|
||||
## 四期/五期开发范围路由
|
||||
## 海外仓全流程开发路由
|
||||
|
||||
- 四期范围止于尾程订单、入仓计划/PDA 入仓、换标、产品与箱唛快照、库存、海外采购、本土卡车、本土散板以及排车查询/智能预排;不把出库、派送、异常退回或回访倒灌为四期验收项。
|
||||
- 五期范围承接排车编辑/取消/发车、PDA 排车拣货与出库任务、一件代发出库、排车转派送、客户派送单、异常退回和客户回访。服务端正式入口为 `dispatch/controller/DispatchController.java`、`dispatch/pda/controller/DispatchPdaController.java`、`delivery/controller/DeliveryTransferController.java`、`delivery/controller/OutboundPdaController.java`、`delivery/controller/MerchantDeliveryController.java`、`exceptionorder/` 与 `customervisit/`;海外 PDA 前端工作台尚未接入时只视为后端能力,不得声称设备页面可用。
|
||||
- 海外仓全流程连续覆盖尾程订单、入仓计划/PDA 入仓、换标、产品与箱唛快照、库存、转运、废弃、采购、本土卡车、本土散板、排车查询/智能预排、排车编辑/取消/发车、PDA 拣货与出库、一件代发、转派送、客户派送单、异常退回和客户回访。
|
||||
- 服务端正式入口包括 `dispatch/controller/DispatchController.java`、`dispatch/pda/controller/DispatchPdaController.java`、`delivery/controller/DeliveryTransferController.java`、`delivery/controller/OutboundPdaController.java`、`delivery/controller/MerchantDeliveryController.java`、`exceptionorder/` 与 `customervisit/`;设备页面是否可用以海外 PDA 正式路由和能力开关为准,未由 PDA 承载的操作由 Admin/Client 完成。
|
||||
- 排车转派送允许在 `PENDING_DEPARTURE`、`PICKING`、`READY_TO_DEPART` 阶段幂等生成 `WAIT_OUTBOUND` 出库单与派送单,但必须早于发车;PDA 排车详情由 `DispatchPdaWorkflowService` 返回 `canDepart`、`departBlockReason`,只有车辆信息完整、拣货任务完成且箱数核平、排车为 `READY_TO_DEPART`、关联出库单仍为 `WAIT_OUTBOUND` 时才允许发车,PDA 不得自行推导该门禁。
|
||||
- 排车执行与 `delivery/` Controller 和其他尾程业务接口一同注册,不受功能开关控制。平台排车列表与详情仍由 `lastleg.dispatch-query-api.enabled=true` 独立启用;入库单智能排车候选、草稿锁定/解锁/释放和预排创建仍由 `lastleg.smart-dispatch-api.enabled=true` 独立启用。这些排车平台入口均不接收 `customerId`:排车列表跨客户分页,详情按全局排车单号定位后使用记录自身客户范围加载明细;智能排车候选未传计划 ID 时跨客户查询,传入时根据计划 ID 解析范围;托盘移入草稿时立即以操作人和随机草稿标识锁定,移除或关闭时释放,草稿锁超时失效;预排创建根据所选托盘 ID 在服务端解析并校验唯一客户范围,并将当前操作人的草稿锁原子转换为正式排车锁。
|
||||
- 仓库 ID、类型、状态、地址和能力唯一权威归国内 `warehouse-service`;海外库位、实物托盘和资源锁归 `warehouse-overseas-service/` 且只引用同一国内 `warehouseId`。Lastleg PDA 通过 Warehouse Overseas 内部资源入口校验该国内共享收货仓,不得要求海外数据库重复建仓;浏览器 API 统一经 `gateway-overseas/`。
|
||||
|
||||
@@ -56,7 +56,7 @@ com.baiyue.lastleg.transfer.service
|
||||
| 排车/派车 `dispatch-delivery` | `dispatch/`、`delivery/`、未来 `outbound/` | 46 | 20/0 | 主源码无入向 Java 类型边,但涉及明确排除的出库/派送范围,不作为首切片 |
|
||||
| 换标/转运/废弃 `relabel-transfer-discard` | `relabel/`、`transfer/`、`discard/`,其 PDA 子包保持本域归属 | 61 | 31/0 | 直接使用尾程、库存和一件代发持久化模型;业务事务边界复杂 |
|
||||
| 一件代发 `dropship` | `dropship/` | 12 | 4/11 | 与尾程消费及废弃库存双向耦合,位于主 SCC |
|
||||
| 采购/卡车/散板 `procurement-trucking-looseboard` | `procurement/`、`trucking/`、`looseboard/` | 35 | 11/0 | 依赖尾程核心,采购另读取报价目录;仍由四期业务专项维护 |
|
||||
| 采购/卡车/散板 `procurement-trucking-looseboard` | `procurement/`、`trucking/`、`looseboard/` | 35 | 11/0 | 依赖尾程核心,采购另读取报价目录;仍由海外仓业务业务专项维护 |
|
||||
| 海外主数据 `overseas-master-data` | `masterdata/quote`、`exchange`、`vehicle` | 28 | 3/1 | 不在任何 SCC;仅出向共享权限常量,入向来自采购对报价目录的使用 |
|
||||
| 海外安全投影 `security-projection` | `security/controller`、`security/dto`、`security/service` | 6 | 1/0 | 独立于业务切片与主 SCC;唯一出向为内部 Controller 复用根 `InternalServiceTokenGuard`,无业务切片入向依赖 |
|
||||
|
||||
@@ -122,7 +122,7 @@ com.baiyue.lastleg.transfer.service
|
||||
3. 海外主数据不在 main 或 main+test 的逻辑 SCC 内,车辆子域也没有 Entity/DTO/Mapper 跨切片泄漏。
|
||||
4. 不包含跨服务 Client、RestClient 或 HTTP Gateway,不受同步依赖超时专项影响。
|
||||
5. 车辆档案明确不保存派送履约历史;首切片不触碰 `dispatch/`、`delivery/`、出库或派送代码。
|
||||
6. 当前业务面固定为平台车辆档案列表、详情、创建、编辑、状态和 CSV 导出,不需要补齐四期业务缺口。
|
||||
6. 当前业务面固定为平台车辆档案列表、详情、创建、编辑、状态和 CSV 导出,不需要补齐海外仓业务业务缺口。
|
||||
|
||||
报价子域虽然也不在 SCC,但被采购服务通过 `PartnerWarehouseDirectory` 入向依赖,并包含 `DomesticPartnerWarehouseClient`,因此避让。月度汇率有 8 个生产类,也比车辆档案更大。
|
||||
|
||||
@@ -211,8 +211,8 @@ node lastleg-service/scripts/analyze-lastleg-dependencies.mjs --source-set all -
|
||||
- `loading`、`shipping`、`inboundplan` 及其他业务切片不得直接依赖 `security/controller|dto|service`;当前 Java 类型图零入向,Node 与 ArchUnit 双重锁定。
|
||||
- 安全投影不进入主逻辑 SCC,唯一出向为内部 Controller 复用共享令牌守卫,不注册为 loading/shipping/inbound 的模型、Mapper 或应用服务。
|
||||
|
||||
## 12. 五期出库派送稳定边界
|
||||
## 12. 海外仓出库与派送出库派送稳定边界
|
||||
|
||||
- `dispatch-delivery -> dropship` 是一件代发出库的稳定业务依赖:出库应用服务按客户范围读取一件代发托盘、箱唛和 SKU 库存,并在 PDA 出库事务中完成扣减。
|
||||
- `tailleg-handoff -> dispatch-delivery` 是异常退回与客户回访的稳定业务依赖:异常单和回访应用服务按派送业务号读取派送单及操作记录,不引入反向兼容入口或跨服务共享模型。
|
||||
- 两条边均属于同一 `lastleg-service` 部署单元内的五期正式链路;本次只冻结已存在的生产依赖,不扩大海外主数据或安全投影的允许方向。
|
||||
- 两条边均属于同一 `lastleg-service` 部署单元内的海外仓出库与派送正式链路;本次只冻结已存在的生产依赖,不扩大海外主数据或安全投影的允许方向。
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.baiyue.lastleg.inboundplan.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/merchant/v1/last-leg/inbound-plans")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_INBOUND_LIST + "')")
|
||||
public class MerchantInboundPlanController {
|
||||
@Resource private InboundPlanService service;
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<PageResult<InboundPlanListResponse>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid InboundPlanRequests.Query query) {
|
||||
return ApiResponse.success(service.page(customer(user), query));
|
||||
}
|
||||
|
||||
@GetMapping("/{planNo}")
|
||||
public ApiResponse<InboundPlanResponse> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo) {
|
||||
return ApiResponse.success(service.detailByPlanNo(customer(user), planNo));
|
||||
}
|
||||
|
||||
private String customer(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.baiyue.lastleg.looseboard.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardRequests;
|
||||
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardResponses;
|
||||
import com.baiyue.lastleg.looseboard.service.LocalLooseboardService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/merchant/v1/last-leg/local-looseboard-orders")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_LOOSEBOARD + "')")
|
||||
public class MerchantLocalLooseboardController {
|
||||
@Resource private LocalLooseboardService service;
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<LocalLooseboardResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody LocalLooseboardRequests.Create request) {
|
||||
return ApiResponse.success(service.create(customer(user), operator(user), request));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<PageResult<LocalLooseboardResponses.Summary>> page(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @Valid LocalLooseboardRequests.MerchantQuery query) {
|
||||
return ApiResponse.success(service.page(customer(user), query));
|
||||
}
|
||||
|
||||
@GetMapping("/{orderNo}")
|
||||
public ApiResponse<LocalLooseboardResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String orderNo) {
|
||||
return ApiResponse.success(service.detail(customer(user), orderNo));
|
||||
}
|
||||
|
||||
@PutMapping("/{orderNo}")
|
||||
public ApiResponse<LocalLooseboardResponses.Detail> update(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String orderNo, @Valid @RequestBody LocalLooseboardRequests.Update request) {
|
||||
return ApiResponse.success(service.update(customer(user), orderNo, operator(user), request));
|
||||
}
|
||||
|
||||
private String customer(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
private String operator(AuthenticatedUser user) {
|
||||
return user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
}
|
||||
}
|
||||
+10
@@ -65,6 +65,16 @@ public final class LocalLooseboardRequests {
|
||||
LocalDate requestedFrom,
|
||||
LocalDate requestedTo) { }
|
||||
|
||||
public record MerchantQuery(
|
||||
@Min(1) long page,
|
||||
@Min(1) @Max(200) long size,
|
||||
String orderNo,
|
||||
String routeKeyword,
|
||||
String status,
|
||||
String transportMode,
|
||||
LocalDate requestedFrom,
|
||||
LocalDate requestedTo) { }
|
||||
|
||||
public record ChangeStatus(
|
||||
@NotNull @Min(0) Long version,
|
||||
@NotBlank @Size(max = 32) String status,
|
||||
|
||||
+8
@@ -118,6 +118,14 @@ public class LocalLooseboardService {
|
||||
result.getSize(), result.getTotal(), result.getPages());
|
||||
}
|
||||
|
||||
/** Customer-scoped query that ignores any client-supplied customer filter. */
|
||||
public PageResult<LocalLooseboardResponses.Summary> page(String customerId,
|
||||
LocalLooseboardRequests.MerchantQuery query) {
|
||||
return page(new LocalLooseboardRequests.Query(query.page(), query.size(), customerId,
|
||||
query.orderNo(), query.routeKeyword(), query.status(), query.transportMode(),
|
||||
query.requestedFrom(), query.requestedTo()));
|
||||
}
|
||||
|
||||
public LocalLooseboardResponses.Detail detail(String customerId, String orderNo) {
|
||||
LocalLooseboardOrder order = orders.findNo(customerId, orderNo);
|
||||
if (order == null) throw notFound();
|
||||
|
||||
+5
-2
@@ -6,9 +6,11 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.procurement.dto.OverseasProcurementRequests;
|
||||
import com.baiyue.lastleg.procurement.dto.OverseasProcurementResponses;
|
||||
import com.baiyue.lastleg.procurement.service.OverseasProcurementService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -20,6 +22,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/merchant/v1/last-leg/overseas-procurements")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_PROCUREMENT + "')")
|
||||
public class MerchantOverseasProcurementController {
|
||||
@Resource private OverseasProcurementService service;
|
||||
|
||||
@@ -32,8 +35,8 @@ public class MerchantOverseasProcurementController {
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<PageResult<OverseasProcurementResponses.Summary>> page(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @Valid OverseasProcurementRequests.Query query) {
|
||||
return ApiResponse.success(service.page(customer(user), query));
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @Valid OverseasProcurementRequests.MerchantQuery query) {
|
||||
return ApiResponse.success(service.page(customer(user), query.scoped()));
|
||||
}
|
||||
|
||||
@GetMapping("/{orderNo}")
|
||||
|
||||
+11
@@ -36,6 +36,17 @@ public final class OverseasProcurementRequests {
|
||||
String productName, String status, String purchaseWarehouseId,
|
||||
LocalDateTime createdFrom, LocalDateTime createdTo) {}
|
||||
|
||||
public record MerchantQuery(
|
||||
@Min(1) long page, @Min(1) @Max(200) long size,
|
||||
String orderNo, String purchaseCategory, String productName,
|
||||
String status, String purchaseWarehouseId,
|
||||
LocalDateTime createdFrom, LocalDateTime createdTo) {
|
||||
public Query scoped() {
|
||||
return new Query(page, size, orderNo, null, purchaseCategory, productName,
|
||||
status, purchaseWarehouseId, createdFrom, createdTo);
|
||||
}
|
||||
}
|
||||
|
||||
public record Review(
|
||||
@NotNull @Min(0) Long version,
|
||||
@NotBlank String decision,
|
||||
|
||||
@@ -36,6 +36,10 @@ public final class OverseasPermissions {
|
||||
public static final String CUSTOMER_VISIT_EDIT = "platform:overseas-warehouse:customer-visit:edit";
|
||||
public static final String MERCHANT_DELIVERY_LIST = "merchant:warehouse:delivery:list";
|
||||
public static final String MERCHANT_DELIVERY_CREATE = "merchant:warehouse:delivery:create";
|
||||
public static final String MERCHANT_INBOUND_LIST = "merchant:warehouse:inbound:list";
|
||||
public static final String MERCHANT_PROCUREMENT = "merchant:cross-border:procurement";
|
||||
public static final String MERCHANT_TRUCKING = "merchant:cross-border:domestic-trucking";
|
||||
public static final String MERCHANT_LOOSEBOARD = "merchant:cross-border:pallet-shipping";
|
||||
public static final String PURCHASE_LIST = "platform:overseas-business:purchase:list";
|
||||
public static final String PURCHASE_EDIT = "platform:overseas-business:purchase:edit";
|
||||
public static final String PURCHASE_REVIEW = "platform:overseas-business:purchase:review";
|
||||
|
||||
+5
-2
@@ -6,9 +6,11 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.trucking.dto.UsTruckOrderRequests;
|
||||
import com.baiyue.lastleg.trucking.dto.UsTruckOrderResponses;
|
||||
import com.baiyue.lastleg.trucking.service.UsTruckOrderService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
@@ -21,6 +23,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/merchant/v1/last-leg/us-truck-orders")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_TRUCKING + "')")
|
||||
public class MerchantUsTruckOrderController {
|
||||
@Resource private UsTruckOrderService service;
|
||||
|
||||
@@ -32,8 +35,8 @@ public class MerchantUsTruckOrderController {
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<PageResult<UsTruckOrderResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid UsTruckOrderRequests.Query query) {
|
||||
return ApiResponse.success(service.page(customer(user), query));
|
||||
@Valid UsTruckOrderRequests.MerchantQuery query) {
|
||||
return ApiResponse.success(service.page(customer(user), query.scoped()));
|
||||
}
|
||||
|
||||
@GetMapping("/{orderNo}")
|
||||
|
||||
+10
@@ -53,6 +53,16 @@ public final class UsTruckOrderRequests {
|
||||
String orderNo, String routeKeyword, String status, String transportType,
|
||||
String customerId, LocalDate requestedFrom, LocalDate requestedTo) {}
|
||||
|
||||
public record MerchantQuery(
|
||||
@Min(1) long page, @Min(1) @Max(200) long size,
|
||||
String orderNo, String routeKeyword, String status, String transportType,
|
||||
LocalDate requestedFrom, LocalDate requestedTo) {
|
||||
public Query scoped() {
|
||||
return new Query(page, size, orderNo, routeKeyword, status, transportType,
|
||||
null, requestedFrom, requestedTo);
|
||||
}
|
||||
}
|
||||
|
||||
public record Update(
|
||||
@NotNull @Min(0) Long version,
|
||||
@Size(max = 32) String transportType,
|
||||
|
||||
@@ -86,7 +86,7 @@ paths:
|
||||
description: 权限 lastleg:pda:operate;FBA 从完整 cartonMark 的第一个 # 前提取固定入仓码,并精确匹配当前锁定柜唯一 FBA line;不要求逐箱 manifest,不校验 SKU 或箱序号。OVERSEAS 必须命中真实 manifest。两者均不返回 SKU,也不登记收货。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { type: object, required: [cartonMark], properties: { cartonMark: { type: string } } } } } }
|
||||
responses: { '200': { description: 箱唛与入仓码信息, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingCartonResolvedApiResponse' } } } }, '404': { description: 入仓码未命中当前柜,或 OVERSEAS 箱码未命中真实 manifest }, '409': { description: 缺少 #、空入仓码前缀、跨柜或当前柜多重命中 } }
|
||||
responses: { '200': { description: 箱唛与入仓码信息, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingCartonResolvedApiResponse' } } } }, '404': { description: 入仓码未命中当前柜,或 OVERSEAS 箱码未命中真实 manifest }, '409': { description: '缺少 #、空入仓码前缀、跨柜或当前柜多重命中' } }
|
||||
/api/pda/v1/last-leg/receiving-containers/current/pallets/resolve:
|
||||
post:
|
||||
summary: 解析当前柜实物托盘码
|
||||
@@ -714,20 +714,125 @@ paths:
|
||||
/api/merchant/v1/last-leg/overseas-procurements:
|
||||
post:
|
||||
summary: 客户创建海外采购订单
|
||||
description: 客户 ID 只取认证身份;新单固定进入 PENDING_REVIEW。
|
||||
description: 权限 merchant:cross-border:procurement;客户 ID 只取认证身份且不接收 customerId;新单固定进入 PENDING_REVIEW。同幂等键同请求返回既有订单,异请求返回 A0409。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProcurementCreate' } } } }
|
||||
responses: { '200': { description: 统一 ApiResponse 包装的采购订单详情 } }
|
||||
responses: { '200': { description: 采购订单详情, content: { application/json: { schema: { $ref: '#/components/schemas/ProcurementDetailResponse' } } } }, '409': { description: 幂等冲突 } }
|
||||
get:
|
||||
summary: 客户分页查询自己的海外采购订单
|
||||
description: 权限 merchant:cross-border:procurement;客户范围只取认证 Token,不接收 customerId。
|
||||
security: [{ bearerAuth: [] }]
|
||||
responses: { '200': { description: 统一 ApiResponse 包装的采购订单分页 } }
|
||||
parameters:
|
||||
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
|
||||
- { name: size, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 20 } }
|
||||
- { name: orderNo, in: query, schema: { type: string } }
|
||||
- { name: purchaseCategory, in: query, schema: { type: string } }
|
||||
- { name: productName, in: query, schema: { type: string } }
|
||||
- { name: status, in: query, schema: { type: string } }
|
||||
- { name: purchaseWarehouseId, in: query, schema: { type: string } }
|
||||
- { name: createdFrom, in: query, schema: { type: string, format: date-time } }
|
||||
- { name: createdTo, in: query, schema: { type: string, format: date-time } }
|
||||
responses: { '200': { description: 当前客户采购订单分页, content: { application/json: { schema: { $ref: '#/components/schemas/ProcurementPageResponse' } } } } }
|
||||
/api/merchant/v1/last-leg/overseas-procurements/{orderNo}:
|
||||
get:
|
||||
summary: 客户查询自己的海外采购订单详情及流水
|
||||
description: 权限 merchant:cross-border:procurement;列表权限包含详情只读,客户范围只取认证 Token。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters: [{ name: orderNo, in: path, required: true, schema: { type: string } }]
|
||||
responses: { '200': { description: 统一 ApiResponse 包装的采购订单详情 } }
|
||||
responses: { '200': { description: 当前客户采购订单详情, content: { application/json: { schema: { $ref: '#/components/schemas/ProcurementDetailResponse' } } } }, '404': { description: 当前客户下订单不存在 } }
|
||||
/api/merchant/v1/last-leg/us-truck-orders:
|
||||
get:
|
||||
summary: 客户分页查询自己的美国本土卡订单
|
||||
description: 权限 merchant:cross-border:domestic-trucking;客户范围只取认证 Token,不接收 customerId。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
|
||||
- { name: size, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 20 } }
|
||||
- { name: orderNo, in: query, schema: { type: string } }
|
||||
- { name: routeKeyword, in: query, schema: { type: string } }
|
||||
- { name: status, in: query, schema: { $ref: '#/components/schemas/TruckStatus' } }
|
||||
- { name: transportType, in: query, schema: { type: string } }
|
||||
- { name: requestedFrom, in: query, schema: { type: string, format: date } }
|
||||
- { name: requestedTo, in: query, schema: { type: string, format: date } }
|
||||
responses: { '200': { description: 当前客户本土卡订单分页, content: { application/json: { schema: { $ref: '#/components/schemas/TruckPageResponse' } } } } }
|
||||
post:
|
||||
summary: 客户创建美国本土卡订单
|
||||
description: 权限 merchant:cross-border:domestic-trucking;新单固定进入 PENDING_REVIEW,客户不能调用平台审核或状态推进端点。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/TruckCreate' } } } }
|
||||
responses: { '200': { description: 本土卡订单详情, content: { application/json: { schema: { $ref: '#/components/schemas/TruckDetailResponse' } } } }, '409': { description: 幂等冲突 } }
|
||||
/api/merchant/v1/last-leg/us-truck-orders/{orderNo}:
|
||||
parameters: [{ name: orderNo, in: path, required: true, schema: { type: string } }]
|
||||
get:
|
||||
summary: 客户查询自己的美国本土卡订单详情
|
||||
description: 权限 merchant:cross-border:domestic-trucking;不泄露其他客户订单。
|
||||
security: [{ bearerAuth: [] }]
|
||||
responses: { '200': { description: 当前客户本土卡订单详情, content: { application/json: { schema: { $ref: '#/components/schemas/TruckDetailResponse' } } } }, '404': { description: 当前客户下订单不存在 } }
|
||||
patch:
|
||||
summary: 客户编辑待审核的美国本土卡订单
|
||||
description: 权限 merchant:cross-border:domestic-trucking;仅 PENDING_REVIEW 可编辑,version 必填;不能修改状态。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/TruckUpdate' } } } }
|
||||
responses: { '200': { description: 更新后的订单详情, content: { application/json: { schema: { $ref: '#/components/schemas/TruckDetailResponse' } } } }, '409': { description: 版本或状态冲突 } }
|
||||
/api/merchant/v1/last-leg/local-looseboard-orders:
|
||||
get:
|
||||
summary: 客户分页查询自己的本土散板订单
|
||||
description: 权限 merchant:cross-border:pallet-shipping;客户范围只取认证 Token,不接收 customerId。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
|
||||
- { name: size, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 20 } }
|
||||
- { name: orderNo, in: query, schema: { type: string } }
|
||||
- { name: routeKeyword, in: query, schema: { type: string } }
|
||||
- { name: status, in: query, schema: { $ref: '#/components/schemas/LooseboardStatus' } }
|
||||
- { name: transportMode, in: query, schema: { type: string, enum: [SHARED_TRUCK, PRIORITY_CONSOLIDATION] } }
|
||||
- { name: requestedFrom, in: query, schema: { type: string, format: date } }
|
||||
- { name: requestedTo, in: query, schema: { type: string, format: date } }
|
||||
responses: { '200': { description: 当前客户本土散板订单分页, content: { application/json: { schema: { $ref: '#/components/schemas/LooseboardPageResponse' } } } } }
|
||||
post:
|
||||
summary: 客户创建本土散板订单
|
||||
description: 权限 merchant:cross-border:pallet-shipping;新单固定进入 PENDING_ACCEPTANCE,客户不能调用平台状态推进端点。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/LooseboardCreate' } } } }
|
||||
responses: { '200': { description: 本土散板订单详情, content: { application/json: { schema: { $ref: '#/components/schemas/LooseboardDetailResponse' } } } }, '409': { description: 幂等冲突 } }
|
||||
/api/merchant/v1/last-leg/local-looseboard-orders/{orderNo}:
|
||||
parameters: [{ name: orderNo, in: path, required: true, schema: { type: string } }]
|
||||
get:
|
||||
summary: 客户查询自己的本土散板订单详情
|
||||
description: 权限 merchant:cross-border:pallet-shipping;不泄露其他客户订单。
|
||||
security: [{ bearerAuth: [] }]
|
||||
responses: { '200': { description: 当前客户本土散板订单详情, content: { application/json: { schema: { $ref: '#/components/schemas/LooseboardDetailResponse' } } } }, '404': { description: 当前客户下订单不存在 } }
|
||||
put:
|
||||
summary: 客户编辑待接单的本土散板订单
|
||||
description: 权限 merchant:cross-border:pallet-shipping;仅 PENDING_ACCEPTANCE 可编辑,version 必填;不能修改状态。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/LooseboardUpdate' } } } }
|
||||
responses: { '200': { description: 更新后的订单详情, content: { application/json: { schema: { $ref: '#/components/schemas/LooseboardDetailResponse' } } } }, '409': { description: 版本或状态冲突 } }
|
||||
/api/merchant/v1/last-leg/inbound-plans:
|
||||
get:
|
||||
summary: 客户分页查询自己的海外入库订单
|
||||
description: 权限 merchant:warehouse:inbound:list;客户范围只取认证 Token,不接收 customerId。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
|
||||
- { name: size, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 20 } }
|
||||
- { name: planNo, in: query, schema: { type: string } }
|
||||
- { name: inboundCode, in: query, schema: { type: string } }
|
||||
- { name: status, in: query, schema: { type: string } }
|
||||
- { name: overseasWarehouseId, in: query, schema: { type: string } }
|
||||
- { name: destinationWarehouseCode, in: query, schema: { type: string } }
|
||||
- { name: appointmentStatus, in: query, schema: { type: string, enum: [UNAPPOINTED, PENDING, SUCCESS, FAILED, CANCELLED] } }
|
||||
- { name: dispatchStatus, in: query, schema: { type: string, enum: [UNSCHEDULED, SCHEDULED] } }
|
||||
- { name: createdFrom, in: query, schema: { type: string, format: date-time } }
|
||||
- { name: createdTo, in: query, schema: { type: string, format: date-time } }
|
||||
responses: { '200': { description: 当前客户入库订单分页, content: { application/json: { schema: { $ref: '#/components/schemas/InboundPlanPageResponse' } } } } }
|
||||
/api/merchant/v1/last-leg/inbound-plans/{planNo}:
|
||||
get:
|
||||
summary: 客户查询自己的海外入库订单详情
|
||||
description: 权限 merchant:warehouse:inbound:list;列表权限包含详情只读,不接收 customerId。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters: [{ name: planNo, in: path, required: true, schema: { type: string } }]
|
||||
responses: { '200': { description: 当前客户入库订单详情, content: { application/json: { schema: { $ref: '#/components/schemas/InboundPlanDetailResponse' } } } }, '404': { description: 当前客户下入库订单不存在 } }
|
||||
/api/platform/v1/last-leg/us-truck-orders:
|
||||
get:
|
||||
summary: 分页查询本土卡车订单
|
||||
@@ -1570,6 +1675,325 @@ components:
|
||||
properties:
|
||||
status: { $ref: '#/components/schemas/RelabelStatus' }
|
||||
remark: { type: string, nullable: true, maxLength: 1000 }
|
||||
MerchantApiResponse:
|
||||
type: object
|
||||
required: [code, data]
|
||||
properties:
|
||||
code: { type: string, example: '200' }
|
||||
message: { type: string, nullable: true }
|
||||
data: { nullable: true }
|
||||
requestId: { type: string, nullable: true }
|
||||
traceId: { type: string, nullable: true }
|
||||
timestamp: { type: string, format: date-time, nullable: true }
|
||||
ProcurementPageResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/MerchantApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/ProcurementPage' } }
|
||||
ProcurementDetailResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/MerchantApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/ProcurementDetail' } }
|
||||
TruckPageResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/MerchantApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/TruckPage' } }
|
||||
TruckDetailResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/MerchantApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/TruckDetail' } }
|
||||
LooseboardPageResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/MerchantApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/LooseboardPage' } }
|
||||
LooseboardDetailResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/MerchantApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/LooseboardDetail' } }
|
||||
InboundPlanPageResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/MerchantApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/InboundPlanPage' } }
|
||||
InboundPlanDetailResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/MerchantApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/InboundPlanDetail' } }
|
||||
PageMetadata:
|
||||
type: object
|
||||
required: [page, size, total, pages]
|
||||
properties:
|
||||
page: { type: integer, format: int64 }
|
||||
size: { type: integer, format: int64 }
|
||||
total: { type: integer, format: int64 }
|
||||
pages: { type: integer, format: int64 }
|
||||
ProcurementPage:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/PageMetadata' }
|
||||
- type: object
|
||||
required: [records]
|
||||
properties: { records: { type: array, items: { $ref: '#/components/schemas/ProcurementSummary' } } }
|
||||
ProcurementSummary:
|
||||
type: object
|
||||
required: [id, orderNo, customerId, purchaseCategory, productName, purchaseQuantity, inboundQuantity, outboundQuantity, inventoryQuantity, status, version, createdAt, updatedAt]
|
||||
properties:
|
||||
id: { type: string, pattern: '^[0-9]+$' }
|
||||
orderNo: { type: string }
|
||||
customerId: { type: string }
|
||||
customerName: { type: string, nullable: true }
|
||||
purchaseCategory: { type: string }
|
||||
productName: { type: string }
|
||||
productSpecification: { type: string, nullable: true }
|
||||
purchaseQuantity: { type: integer, format: int64 }
|
||||
budgetUnitPrice: { type: number, nullable: true }
|
||||
currency: { type: string, nullable: true }
|
||||
purchaseWarehouseId: { type: string, nullable: true }
|
||||
purchaseWarehouseName: { type: string, nullable: true }
|
||||
inboundQuantity: { type: integer, format: int64 }
|
||||
outboundQuantity: { type: integer, format: int64 }
|
||||
inventoryQuantity: { type: integer, format: int64 }
|
||||
status: { type: string, description: PENDING_REVIEW/APPROVED/REJECTED 或按审核后出库数量派生的 PENDING_OUTBOUND/PARTIALLY_OUTBOUND/OUTBOUNDED }
|
||||
version: { type: integer, format: int64 }
|
||||
createdAt: { type: string, format: date-time }
|
||||
updatedAt: { type: string, format: date-time }
|
||||
ProcurementDetail:
|
||||
type: object
|
||||
required: [summary, histories, outboundDetails]
|
||||
properties:
|
||||
summary: { $ref: '#/components/schemas/ProcurementSummary' }
|
||||
productUrl: { type: string, nullable: true }
|
||||
receivingAddress: { type: string, nullable: true }
|
||||
contactName: { type: string, nullable: true }
|
||||
contactPhone: { type: string, nullable: true }
|
||||
domesticCarrier: { type: string, nullable: true }
|
||||
domesticTrackingNo: { type: string, nullable: true }
|
||||
overseasCarrier: { type: string, nullable: true }
|
||||
overseasTrackingNo: { type: string, nullable: true }
|
||||
estimatedArrivalAt: { type: string, format: date-time, nullable: true }
|
||||
remark: { type: string, nullable: true }
|
||||
createdBy: { type: string, nullable: true }
|
||||
reviewedBy: { type: string, nullable: true }
|
||||
reviewRemark: { type: string, nullable: true }
|
||||
reviewedAt: { type: string, format: date-time, nullable: true }
|
||||
histories: { type: array, items: { $ref: '#/components/schemas/BusinessHistory' } }
|
||||
outboundDetails: { type: array, items: { $ref: '#/components/schemas/ProcurementOutboundDetail' } }
|
||||
ProcurementOutboundDetail:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
quantity: { type: integer, format: int64 }
|
||||
carrier: { type: string, nullable: true }
|
||||
trackingNo: { type: string, nullable: true }
|
||||
shippedAt: { type: string, format: date-time, nullable: true }
|
||||
remark: { type: string, nullable: true }
|
||||
TruckPage:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/PageMetadata' }
|
||||
- type: object
|
||||
required: [records]
|
||||
properties: { records: { type: array, items: { $ref: '#/components/schemas/TruckSummary' } } }
|
||||
TruckSummary:
|
||||
type: object
|
||||
required: [id, customerId, orderNo, origin, destination, palletCount, packageCount, receivedPalletCount, allocatedPalletCount, totalWeightKg, totalVolumeCbm, status, version, createdAt, updatedAt]
|
||||
properties:
|
||||
id: { type: string, pattern: '^[0-9]+$' }
|
||||
customerId: { type: string }
|
||||
orderNo: { type: string }
|
||||
transportType: { type: string, nullable: true }
|
||||
origin: { type: string }
|
||||
destination: { type: string }
|
||||
containerType: { type: string, nullable: true }
|
||||
palletCount: { type: integer }
|
||||
packageCount: { type: integer }
|
||||
receivedPalletCount: { type: integer }
|
||||
allocatedPalletCount: { type: integer }
|
||||
totalWeightKg: { type: number }
|
||||
totalVolumeCbm: { type: number }
|
||||
budgetAmount: { type: number, nullable: true }
|
||||
currency: { type: string, nullable: true }
|
||||
contactName: { type: string, nullable: true }
|
||||
contactInfo: { type: string, nullable: true }
|
||||
requestedDate: { type: string, format: date, nullable: true }
|
||||
status: { $ref: '#/components/schemas/TruckStatus' }
|
||||
version: { type: integer, format: int64 }
|
||||
createdAt: { type: string, format: date-time }
|
||||
updatedAt: { type: string, format: date-time }
|
||||
TruckDetail:
|
||||
type: object
|
||||
required: [summary, pallets, histories]
|
||||
properties:
|
||||
summary: { $ref: '#/components/schemas/TruckSummary' }
|
||||
originAddress: { type: string, nullable: true }
|
||||
destinationAddress: { type: string, nullable: true }
|
||||
remark: { type: string, nullable: true }
|
||||
createdBy: { type: string, nullable: true }
|
||||
pallets: { type: array, items: { $ref: '#/components/schemas/TruckPallet' } }
|
||||
histories: { type: array, items: { $ref: '#/components/schemas/BusinessHistory' } }
|
||||
TruckPallet:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string, pattern: '^[0-9]+$' }
|
||||
palletNo: { type: string }
|
||||
sequenceNo: { type: integer }
|
||||
source: { type: string }
|
||||
status: { type: string }
|
||||
warehousePalletId: { type: string, pattern: '^[0-9]+$', nullable: true }
|
||||
packageCount: { type: integer, nullable: true }
|
||||
receivedAt: { type: string, format: date-time, nullable: true }
|
||||
LooseboardPage:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/PageMetadata' }
|
||||
- type: object
|
||||
required: [records]
|
||||
properties: { records: { type: array, items: { $ref: '#/components/schemas/LooseboardSummary' } } }
|
||||
LooseboardSummary:
|
||||
type: object
|
||||
required: [id, customerId, orderNo, transportMode, palletRequired, transportType, origin, destination, packageCount, totalWeightKg, totalVolumeCbm, status, version, createdAt, updatedAt]
|
||||
properties:
|
||||
id: { type: string, pattern: '^[0-9]+$' }
|
||||
customerId: { type: string }
|
||||
orderNo: { type: string }
|
||||
transportMode: { type: string, enum: [SHARED_TRUCK, PRIORITY_CONSOLIDATION] }
|
||||
palletRequired: { type: string, enum: [YES, NO] }
|
||||
transportType: { type: string }
|
||||
origin: { type: string }
|
||||
destination: { type: string }
|
||||
containerType: { type: string, nullable: true }
|
||||
packageCount: { type: integer }
|
||||
totalWeightKg: { type: number }
|
||||
totalVolumeCbm: { type: number }
|
||||
budgetAmount: { type: number, nullable: true }
|
||||
currency: { type: string, nullable: true }
|
||||
contactName: { type: string, nullable: true }
|
||||
contactInfo: { type: string, nullable: true }
|
||||
requestedDate: { type: string, format: date, nullable: true }
|
||||
status: { $ref: '#/components/schemas/LooseboardStatus' }
|
||||
version: { type: integer, format: int64 }
|
||||
createdAt: { type: string, format: date-time }
|
||||
updatedAt: { type: string, format: date-time }
|
||||
LooseboardDetail:
|
||||
type: object
|
||||
required: [summary, histories]
|
||||
properties:
|
||||
summary: { $ref: '#/components/schemas/LooseboardSummary' }
|
||||
originAddress: { type: string, nullable: true }
|
||||
destinationAddress: { type: string, nullable: true }
|
||||
remark: { type: string, nullable: true }
|
||||
createdBy: { type: string, nullable: true }
|
||||
histories: { type: array, items: { $ref: '#/components/schemas/BusinessHistory' } }
|
||||
BusinessHistory:
|
||||
type: object
|
||||
properties:
|
||||
operationType: { type: string }
|
||||
fromStatus: { type: string, nullable: true }
|
||||
toStatus: { type: string, nullable: true }
|
||||
operatorId: { type: string, nullable: true }
|
||||
idempotencyKey: { type: string, nullable: true }
|
||||
changeDetail: { type: string, nullable: true }
|
||||
remark: { type: string, nullable: true }
|
||||
createdAt: { type: string, format: date-time }
|
||||
InboundPlanPage:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/PageMetadata' }
|
||||
- type: object
|
||||
required: [records]
|
||||
properties: { records: { type: array, items: { $ref: '#/components/schemas/InboundPlanSummary' } } }
|
||||
InboundPlanSummary:
|
||||
type: object
|
||||
required: [lineId, planId, planNo, customerId, orderId, inboundCode, packageCount, pickedPackageCount, palletCount, status, createdAt, version]
|
||||
properties:
|
||||
lineId: { type: string, pattern: '^[0-9]+$' }
|
||||
planId: { type: string, pattern: '^[0-9]+$' }
|
||||
planNo: { type: string }
|
||||
customerId: { type: string }
|
||||
orderId: { type: string, pattern: '^[0-9]+$' }
|
||||
inboundCode: { type: string }
|
||||
orderType: { type: string, nullable: true }
|
||||
warehouseId: { type: string, nullable: true }
|
||||
warehouseCode: { type: string, nullable: true }
|
||||
warehouseName: { type: string, nullable: true }
|
||||
fbaWarehouseCode: { type: string, nullable: true }
|
||||
customerName: { type: string, nullable: true }
|
||||
packageCount: { type: integer }
|
||||
pickedPackageCount: { type: integer }
|
||||
volumeCbm: { type: number, nullable: true }
|
||||
palletCount: { type: integer }
|
||||
status: { type: string }
|
||||
appointmentNo: { type: string, nullable: true }
|
||||
appointmentStatus: { type: string, nullable: true }
|
||||
appointmentAt: { type: string, format: date-time, nullable: true }
|
||||
expectedLoadingAt: { type: string, format: date-time, nullable: true }
|
||||
dispatchStatus: { type: string, nullable: true }
|
||||
createdAt: { type: string, format: date-time }
|
||||
version: { type: integer }
|
||||
InboundPlanDetail:
|
||||
type: object
|
||||
required: [id, planNo, customerId, status, lineCount, palletCount, totalVolumeCbm, expectedPackageCount, pickedPackageCount, lines, pallets, allocations, version]
|
||||
properties:
|
||||
id: { type: string, pattern: '^[0-9]+$' }
|
||||
planNo: { type: string }
|
||||
customerId: { type: string }
|
||||
sourceOrderId: { type: string, pattern: '^[0-9]+$', nullable: true }
|
||||
containerNo: { type: string, nullable: true }
|
||||
status: { type: string }
|
||||
lineCount: { type: integer }
|
||||
palletCount: { type: integer }
|
||||
totalVolumeCbm: { type: number }
|
||||
expectedPackageCount: { type: integer }
|
||||
pickedPackageCount: { type: integer }
|
||||
lines: { type: array, items: { $ref: '#/components/schemas/InboundPlanLine' } }
|
||||
pallets: { type: array, items: { $ref: '#/components/schemas/InboundPlanPallet' } }
|
||||
allocations: { type: array, items: { $ref: '#/components/schemas/InboundPlanAllocation' } }
|
||||
version: { type: integer }
|
||||
InboundPlanLine:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string, pattern: '^[0-9]+$' }
|
||||
orderId: { type: string, pattern: '^[0-9]+$' }
|
||||
inboundCode: { type: string }
|
||||
fbaWarehouseCode: { type: string, nullable: true }
|
||||
destinationWarehouseId: { type: string, nullable: true }
|
||||
destinationWarehouseCode: { type: string, nullable: true }
|
||||
destinationWarehouseName: { type: string, nullable: true }
|
||||
volumeCbm: { type: number }
|
||||
requiredPackageCount: { type: integer }
|
||||
plannedPalletCount: { type: integer }
|
||||
InboundPlanPallet:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string, pattern: '^[0-9]+$' }
|
||||
palletNo: { type: string }
|
||||
destinationWarehouseId: { type: string, nullable: true }
|
||||
destinationWarehouseCode: { type: string, nullable: true }
|
||||
destinationWarehouseName: { type: string, nullable: true }
|
||||
capacityCbm: { type: number }
|
||||
allocatedVolumeCbm: { type: number }
|
||||
sequenceNo: { type: integer }
|
||||
warehousePalletId: { type: string, pattern: '^[0-9]+$', nullable: true }
|
||||
warehouseLocationId: { type: string, pattern: '^[0-9]+$', nullable: true }
|
||||
warehouseResourceLockId: { type: string, pattern: '^[0-9]+$', nullable: true }
|
||||
assignmentStatus: { type: string, nullable: true }
|
||||
lockedAt: { type: string, format: date-time, nullable: true }
|
||||
actualVolumeCbm: { type: number, nullable: true }
|
||||
actualPackageCount: { type: integer }
|
||||
InboundPlanAllocation:
|
||||
type: object
|
||||
properties:
|
||||
lineId: { type: string, pattern: '^[0-9]+$' }
|
||||
palletId: { type: string, pattern: '^[0-9]+$' }
|
||||
allocatedVolumeCbm: { type: number }
|
||||
plannedPackageCount: { type: integer }
|
||||
countedPackageCount: { type: integer }
|
||||
pickedPackageCount: { type: integer }
|
||||
actualVolumeCbm: { type: number, nullable: true }
|
||||
status: { type: string }
|
||||
version: { type: integer }
|
||||
ProcurementStatus: { type: string, enum: [PENDING_REVIEW, APPROVED, REJECTED] }
|
||||
ProcurementReview:
|
||||
type: object
|
||||
|
||||
@@ -75,33 +75,70 @@ paths:
|
||||
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
|
||||
- { name: size, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
|
||||
- { name: status, in: query, schema: { type: string, enum: [IN_PROGRESS, SUBMITTED] } }
|
||||
responses: { '200': { description: 盘点任务分页 } }
|
||||
responses:
|
||||
'200': { description: 盘点任务分页, content: { application/json: { schema: { $ref: '#/components/schemas/StocktakeSummaryPageResponse' } } } }
|
||||
post:
|
||||
summary: 按库位或托盘创建盘点任务
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/StocktakeCreate' } } } }
|
||||
responses: { '200': { description: 创建后的完整盘点任务 } }
|
||||
responses:
|
||||
'200': { description: 创建后的完整盘点任务, content: { application/json: { schema: { $ref: '#/components/schemas/StocktakeDetailResponse' } } } }
|
||||
/api/pda/v1/last-leg/stocktakes/{taskNo}:
|
||||
get:
|
||||
summary: 查询盘点任务详情
|
||||
parameters: [{ name: taskNo, in: path, required: true, schema: { type: string } }]
|
||||
responses: { '200': { description: 盘点任务及冻结库存位置 } }
|
||||
responses:
|
||||
'200': { description: 盘点任务及冻结库存位置, content: { application/json: { schema: { $ref: '#/components/schemas/StocktakeDetailResponse' } } } }
|
||||
/api/pda/v1/last-leg/stocktakes/{taskNo}/counts:
|
||||
post:
|
||||
summary: 提交一个库存位置的实盘箱数
|
||||
parameters: [{ name: taskNo, in: path, required: true, schema: { type: string } }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/StocktakeCount' } } } }
|
||||
responses: { '200': { description: 已计数明细 }, '409': { description: 版本或幂等冲突 } }
|
||||
responses:
|
||||
'200': { description: 已计数明细, content: { application/json: { schema: { $ref: '#/components/schemas/StocktakeItemResponse' } } } }
|
||||
'409': { description: 版本或幂等冲突 }
|
||||
/api/pda/v1/last-leg/stocktakes/{taskNo}/submit:
|
||||
post:
|
||||
summary: 提交盘点并按差异调整库存
|
||||
parameters: [{ name: taskNo, in: path, required: true, schema: { type: string } }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/StocktakeSubmit' } } } }
|
||||
responses: { '200': { description: 已提交盘点任务 }, '409': { description: 未全盘、库存版本变化或负库存风险 } }
|
||||
responses:
|
||||
'200': { description: 已提交盘点任务, content: { application/json: { schema: { $ref: '#/components/schemas/StocktakeDetailResponse' } } } }
|
||||
'409': { description: 未全盘、库存版本变化或负库存风险 }
|
||||
/api/pda/v1/last-leg/inventory-moves:
|
||||
post:
|
||||
summary: 同仓整托移库
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/InventoryMove' } } } }
|
||||
responses: { '200': { description: 移库结果 }, '409': { description: 跨仓、资源或库存版本冲突 } }
|
||||
responses:
|
||||
'200': { description: 移库结果, content: { application/json: { schema: { $ref: '#/components/schemas/InventoryMoveResponse' } } } }
|
||||
'409': { description: 跨仓、资源或库存版本冲突 }
|
||||
/api/pda/v1/last-leg/inventory/resolve:
|
||||
post:
|
||||
summary: 扫码查询Token绑定仓库存
|
||||
description: 支持箱唛、SKU、入仓码、托盘码或库位码;不接收customerId或warehouseId。
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ScanCode' } } } }
|
||||
responses:
|
||||
'200': { description: 匹配的库存位置列表, content: { application/json: { schema: { $ref: '#/components/schemas/InventoryScanResponse' } } } }
|
||||
/api/pda/v1/last-leg/locations/resolve:
|
||||
post:
|
||||
summary: 扫码查询Token绑定仓库位
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ScanCode' } } } }
|
||||
responses:
|
||||
'200': { description: 库位及占用摘要, content: { application/json: { schema: { $ref: '#/components/schemas/LocationResponse' } } } }
|
||||
/api/pda/v1/last-leg/pallets/resolve:
|
||||
post:
|
||||
summary: 扫码查询Token绑定仓托盘
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ScanCode' } } } }
|
||||
responses:
|
||||
'200': { description: 托盘及其库存内容, content: { application/json: { schema: { $ref: '#/components/schemas/PalletResponse' } } } }
|
||||
/api/pda/v1/last-leg/operations:
|
||||
get:
|
||||
summary: 查询当前操作人的仓内操作历史
|
||||
parameters:
|
||||
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
|
||||
- { name: size, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 20 } }
|
||||
- { name: operationType, in: query, required: false, schema: { type: string } }
|
||||
responses:
|
||||
'200': { description: 当前Token绑定仓及操作人的历史分页, content: { application/json: { schema: { $ref: '#/components/schemas/OperationPageResponse' } } } }
|
||||
/api/pda/v1/last-leg/pallet-operations/pallets/resolve:
|
||||
post:
|
||||
summary: 扫码查询托盘及库存内容
|
||||
@@ -130,58 +167,355 @@ paths:
|
||||
/api/pda/v1/last-leg/relabel-tasks/candidates:
|
||||
get:
|
||||
summary: 查询Token绑定仓可执行换标任务
|
||||
responses: { '200': { description: 待认领、处理中或待复核换标任务 } }
|
||||
responses:
|
||||
'200': { description: 待认领、处理中或待复核换标任务, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelCandidatesResponse' } } } }
|
||||
/api/pda/v1/last-leg/relabel-tasks/{relabelNo}:
|
||||
get:
|
||||
summary: 查询换标现场任务详情
|
||||
parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }]
|
||||
responses: { '200': { description: 申请明细、逐件事实、照片与历史 } }
|
||||
responses:
|
||||
'200': { description: 申请明细、逐件事实、照片与历史, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelDetailResponse' } } } }
|
||||
/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/claim:
|
||||
post:
|
||||
summary: 认领换标任务并推进申请至处理中
|
||||
parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/IdempotentAction' } } } }
|
||||
responses: { '200': { description: 已认领任务 }, '409': { description: 仓库、状态或幂等冲突 } }
|
||||
responses: { '200': { description: 已认领任务, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelDetailResponse' } } } }, '409': { description: 仓库、状态或幂等冲突 } }
|
||||
/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/units:
|
||||
post:
|
||||
summary: 登记一件正常换标事实
|
||||
parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelUnit' } } } }
|
||||
responses: { '200': { description: 更新后的任务 }, '409': { description: 旧标重复、新标不符或版本冲突 } }
|
||||
responses: { '200': { description: 更新后的任务, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelDetailResponse' } } } }, '409': { description: 旧标重复、新标不符或版本冲突 } }
|
||||
/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/units/skip:
|
||||
post:
|
||||
summary: 登记一件有原因的跳过事实
|
||||
parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelSkip' } } } }
|
||||
responses: { '200': { description: 更新后的任务 } }
|
||||
responses: { '200': { description: 更新后的任务, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelDetailResponse' } } } } }
|
||||
/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/exceptions:
|
||||
post:
|
||||
summary: 登记一件换标异常事实
|
||||
parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelException' } } } }
|
||||
responses: { '200': { description: 更新后的任务 } }
|
||||
responses: { '200': { description: 更新后的任务, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelDetailResponse' } } } } }
|
||||
/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/photos/files:
|
||||
post:
|
||||
summary: 上传换标完成现场照片
|
||||
parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }]
|
||||
requestBody: { required: true, content: { multipart/form-data: { schema: { type: object, required: [file], properties: { file: { type: string, format: binary } } } } } }
|
||||
responses: { '200': { description: OVERSEAS_RELABEL_PHOTO文件元数据 } }
|
||||
responses: { '200': { description: OVERSEAS_RELABEL_PHOTO文件元数据, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelPhotoResponse' } } } } }
|
||||
/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/complete:
|
||||
post:
|
||||
summary: 提交现场任务等待复核,不提前完成申请
|
||||
parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelComplete' } } } }
|
||||
responses: { '200': { description: WAIT_REVIEW任务 }, '409': { description: 箱数未核平、照片或版本冲突 } }
|
||||
responses: { '200': { description: WAIT_REVIEW任务, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelDetailResponse' } } } }, '409': { description: 箱数未核平、照片或版本冲突 } }
|
||||
/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/review:
|
||||
post:
|
||||
summary: 现场复核;APPROVE才完成既有换标申请,REWORK退回处理
|
||||
parameters: [{ name: relabelNo, in: path, required: true, schema: { type: string } }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelReview' } } } }
|
||||
responses: { '200': { description: COMPLETED或PROCESSING任务 }, '409': { description: 状态或版本冲突 } }
|
||||
responses: { '200': { description: COMPLETED或PROCESSING任务, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelDetailResponse' } } } }, '409': { description: 状态或版本冲突 } }
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
|
||||
schemas:
|
||||
ApiResponse:
|
||||
type: object
|
||||
required: [code, data]
|
||||
properties:
|
||||
code: { type: string, example: '200' }
|
||||
message: { type: string, nullable: true }
|
||||
data: { nullable: true }
|
||||
requestId: { type: string, nullable: true }
|
||||
traceId: { type: string, nullable: true }
|
||||
timestamp: { type: string, format: date-time, nullable: true }
|
||||
StocktakeSummaryPageResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/StocktakeSummaryPage' } }
|
||||
StocktakeDetailResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/StocktakeDetail' } }
|
||||
StocktakeItemResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/StocktakeItem' } }
|
||||
InventoryMoveResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/InventoryMoveResult' } }
|
||||
InventoryScanResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/InventoryScan' } }
|
||||
LocationResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/WarehouseLocation' } }
|
||||
PalletResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/WarehousePallet' } }
|
||||
OperationPageResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/OperationPage' } }
|
||||
RelabelCandidatesResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ApiResponse' }
|
||||
- type: object
|
||||
properties:
|
||||
data: { type: array, items: { $ref: '#/components/schemas/RelabelCandidate' } }
|
||||
RelabelDetailResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/RelabelDetail' } }
|
||||
RelabelPhotoResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/RelabelPhoto' } }
|
||||
StocktakeSummaryPage:
|
||||
type: object
|
||||
required: [records, page, size, total, pages]
|
||||
properties:
|
||||
records: { type: array, items: { $ref: '#/components/schemas/StocktakeSummary' } }
|
||||
page: { type: integer, format: int64 }
|
||||
size: { type: integer, format: int64 }
|
||||
total: { type: integer, format: int64 }
|
||||
pages: { type: integer, format: int64 }
|
||||
StocktakeSummary:
|
||||
type: object
|
||||
required: [taskNo, scopeType, scopeCode, status, positionCount, countedCount, differenceCount, version, createdAt]
|
||||
properties:
|
||||
taskNo: { type: string }
|
||||
scopeType: { type: string, enum: [LOCATION, PALLET] }
|
||||
scopeCode: { type: string }
|
||||
status: { type: string, enum: [IN_PROGRESS, SUBMITTED] }
|
||||
positionCount: { type: integer }
|
||||
countedCount: { type: integer }
|
||||
differenceCount: { type: integer }
|
||||
version: { type: integer, format: int64 }
|
||||
createdAt: { type: string, format: date-time }
|
||||
submittedAt: { type: string, format: date-time, nullable: true }
|
||||
StocktakeItem:
|
||||
type: object
|
||||
required: [itemId, customerId, balanceId, positionId, skuCode, inboundCode, palletId, palletCode, locationId, locationCode, bookPackages, status, positionVersion, version]
|
||||
properties:
|
||||
itemId: { type: string, pattern: '^[0-9]+$' }
|
||||
customerId: { type: string }
|
||||
balanceId: { type: string, pattern: '^[0-9]+$' }
|
||||
positionId: { type: string, pattern: '^[0-9]+$' }
|
||||
skuCode: { type: string }
|
||||
productName: { type: string, nullable: true }
|
||||
inboundCode: { type: string }
|
||||
palletId: { type: string, pattern: '^[0-9]+$' }
|
||||
palletCode: { type: string }
|
||||
locationId: { type: string, pattern: '^[0-9]+$' }
|
||||
locationCode: { type: string }
|
||||
bookPackages: { type: integer, format: int64 }
|
||||
actualPackages: { type: integer, format: int64, nullable: true }
|
||||
differencePackages: { type: integer, format: int64, nullable: true }
|
||||
status: { type: string }
|
||||
positionVersion: { type: integer, format: int64 }
|
||||
version: { type: integer, format: int64 }
|
||||
StocktakeDetail:
|
||||
type: object
|
||||
required: [task, items]
|
||||
properties:
|
||||
task: { $ref: '#/components/schemas/StocktakeSummary' }
|
||||
items: { type: array, items: { $ref: '#/components/schemas/StocktakeItem' } }
|
||||
InventoryMoveResult:
|
||||
type: object
|
||||
required: [moveNo, palletId, palletCode, sourceLocationId, sourceLocationCode, targetLocationId, targetLocationCode, customerCount, positionCount, packageCount, palletVersion, status, completedAt, replayed]
|
||||
properties:
|
||||
moveNo: { type: string }
|
||||
palletId: { type: string, pattern: '^[0-9]+$' }
|
||||
palletCode: { type: string }
|
||||
sourceLocationId: { type: string, pattern: '^[0-9]+$' }
|
||||
sourceLocationCode: { type: string }
|
||||
targetLocationId: { type: string, pattern: '^[0-9]+$' }
|
||||
targetLocationCode: { type: string }
|
||||
customerCount: { type: integer }
|
||||
positionCount: { type: integer }
|
||||
packageCount: { type: integer, format: int64 }
|
||||
palletVersion: { type: integer }
|
||||
status: { type: string }
|
||||
completedAt: { type: string, format: date-time }
|
||||
replayed: { type: boolean }
|
||||
InventoryScan:
|
||||
type: object
|
||||
required: [queryCode, items]
|
||||
properties:
|
||||
queryCode: { type: string }
|
||||
items: { type: array, items: { $ref: '#/components/schemas/InventoryItem' } }
|
||||
InventoryItem:
|
||||
type: object
|
||||
required: [customerId, balanceId, positionId, skuCode, inboundCode, palletId, palletCode, availablePackages, holdPackages, receivingPackages, status, version]
|
||||
properties:
|
||||
customerId: { type: string }
|
||||
customerName: { type: string, nullable: true }
|
||||
balanceId: { type: string, pattern: '^[0-9]+$' }
|
||||
positionId: { type: string, pattern: '^[0-9]+$' }
|
||||
skuCode: { type: string }
|
||||
productName: { type: string, nullable: true }
|
||||
inboundCode: { type: string }
|
||||
cartonMark: { type: string, nullable: true }
|
||||
palletId: { type: string, pattern: '^[0-9]+$' }
|
||||
palletCode: { type: string }
|
||||
locationId: { type: string, pattern: '^[0-9]+$', nullable: true }
|
||||
locationCode: { type: string, nullable: true }
|
||||
availablePackages: { type: integer, format: int64 }
|
||||
holdPackages: { type: integer, format: int64 }
|
||||
receivingPackages: { type: integer, format: int64 }
|
||||
status: { type: string }
|
||||
version: { type: integer, format: int64 }
|
||||
WarehouseLocation:
|
||||
type: object
|
||||
required: [locationId, locationCode, locationType, status, occupiedPackages, inventoryPositionCount]
|
||||
properties:
|
||||
locationId: { type: string, pattern: '^[0-9]+$' }
|
||||
locationCode: { type: string }
|
||||
locationType: { type: string }
|
||||
maxLoadKg: { type: number, nullable: true }
|
||||
status: { type: string }
|
||||
occupiedPackages: { type: integer, format: int64 }
|
||||
inventoryPositionCount: { type: integer }
|
||||
WarehousePallet:
|
||||
type: object
|
||||
required: [palletId, palletCode, status, version, locationId, locationCode, occupiedPackages, inventoryPositionCount, inventory]
|
||||
properties:
|
||||
palletId: { type: string, pattern: '^[0-9]+$' }
|
||||
palletCode: { type: string }
|
||||
status: { type: string }
|
||||
version: { type: integer }
|
||||
locationId: { type: string, pattern: '^[0-9]+$' }
|
||||
locationCode: { type: string }
|
||||
occupiedPackages: { type: integer, format: int64 }
|
||||
inventoryPositionCount: { type: integer }
|
||||
inventory: { type: array, items: { $ref: '#/components/schemas/InventoryItem' } }
|
||||
OperationPage:
|
||||
type: object
|
||||
required: [records, page, size, total, pages]
|
||||
properties:
|
||||
records: { type: array, items: { $ref: '#/components/schemas/WarehouseOperation' } }
|
||||
page: { type: integer, format: int64 }
|
||||
size: { type: integer, format: int64 }
|
||||
total: { type: integer, format: int64 }
|
||||
pages: { type: integer, format: int64 }
|
||||
WarehouseOperation:
|
||||
type: object
|
||||
required: [operationId, operationType, businessKey, status, occurredAt]
|
||||
properties:
|
||||
operationId: { type: string, pattern: '^[0-9]+$' }
|
||||
operationType: { type: string }
|
||||
businessKey: { type: string }
|
||||
status: { type: string }
|
||||
summary: { type: string, nullable: true }
|
||||
customerId: { type: string, nullable: true }
|
||||
occurredAt: { type: string, format: date-time }
|
||||
RelabelCandidate:
|
||||
type: object
|
||||
required: [relabelNo, applicationStatus, taskStatus, itemCount, expectedPackages, relabelledPackages, skippedPackages, exceptionPackages, version, claimedByCurrentOperator]
|
||||
properties:
|
||||
relabelNo: { type: string }
|
||||
customerOrderNo: { type: string, nullable: true }
|
||||
labelDeadline: { type: string, format: date-time, nullable: true }
|
||||
applicationStatus: { type: string }
|
||||
taskStatus: { type: string }
|
||||
itemCount: { type: integer }
|
||||
expectedPackages: { type: integer }
|
||||
relabelledPackages: { type: integer }
|
||||
skippedPackages: { type: integer }
|
||||
exceptionPackages: { type: integer }
|
||||
version: { type: integer }
|
||||
claimedByCurrentOperator: { type: boolean }
|
||||
RelabelDetail:
|
||||
type: object
|
||||
required: [relabelNo, applicationStatus, taskStatus, expectedPackages, relabelledPackages, skippedPackages, exceptionPackages, version, completionPhotoFileIds, items, unitFacts, history]
|
||||
properties:
|
||||
relabelNo: { type: string }
|
||||
customerOrderNo: { type: string, nullable: true }
|
||||
labelDeadline: { type: string, format: date-time, nullable: true }
|
||||
contactName: { type: string, nullable: true }
|
||||
contactPhone: { type: string, nullable: true }
|
||||
applicationStatus: { type: string }
|
||||
taskStatus: { type: string }
|
||||
expectedPackages: { type: integer }
|
||||
relabelledPackages: { type: integer }
|
||||
skippedPackages: { type: integer }
|
||||
exceptionPackages: { type: integer }
|
||||
version: { type: integer }
|
||||
operatorName: { type: string, nullable: true }
|
||||
reviewerName: { type: string, nullable: true }
|
||||
completionPhotoFileIds: { type: array, items: { type: string, pattern: '^[0-9]+$' } }
|
||||
completionRemark: { type: string, nullable: true }
|
||||
reviewDecision: { type: string, nullable: true }
|
||||
reviewRemark: { type: string, nullable: true }
|
||||
claimedAt: { type: string, format: date-time, nullable: true }
|
||||
submittedAt: { type: string, format: date-time, nullable: true }
|
||||
reviewedAt: { type: string, format: date-time, nullable: true }
|
||||
items: { type: array, items: { $ref: '#/components/schemas/RelabelItem' } }
|
||||
unitFacts: { type: array, items: { $ref: '#/components/schemas/RelabelUnitFact' } }
|
||||
history: { type: array, items: { $ref: '#/components/schemas/RelabelHistory' } }
|
||||
RelabelItem:
|
||||
type: object
|
||||
properties:
|
||||
itemId: { type: string, pattern: '^[0-9]+$' }
|
||||
skuCode: { type: string, nullable: true }
|
||||
productNameZh: { type: string, nullable: true }
|
||||
shipmentNo: { type: string, nullable: true }
|
||||
trackingCode: { type: string, nullable: true }
|
||||
newSkuCode: { type: string, nullable: true }
|
||||
labelRequirement: { type: string, nullable: true }
|
||||
relabelFileId: { type: string, nullable: true }
|
||||
relabelFileName: { type: string, nullable: true }
|
||||
relabelFileUrl: { type: string, nullable: true }
|
||||
expectedPackages: { type: integer }
|
||||
relabelledPackages: { type: integer }
|
||||
skippedPackages: { type: integer }
|
||||
exceptionPackages: { type: integer }
|
||||
RelabelUnitFact:
|
||||
type: object
|
||||
properties:
|
||||
factId: { type: string, pattern: '^[0-9]+$' }
|
||||
itemId: { type: string, pattern: '^[0-9]+$' }
|
||||
oldLabel: { type: string, nullable: true }
|
||||
newLabel: { type: string, nullable: true }
|
||||
result: { type: string }
|
||||
exceptionCode: { type: string, nullable: true }
|
||||
reason: { type: string, nullable: true }
|
||||
operatorName: { type: string, nullable: true }
|
||||
occurredAt: { type: string, format: date-time }
|
||||
RelabelHistory:
|
||||
type: object
|
||||
properties:
|
||||
action: { type: string }
|
||||
fromStatus: { type: string, nullable: true }
|
||||
toStatus: { type: string, nullable: true }
|
||||
operatorName: { type: string, nullable: true }
|
||||
detail: { type: string, nullable: true }
|
||||
createdAt: { type: string, format: date-time }
|
||||
RelabelPhoto:
|
||||
type: object
|
||||
required: [fileId, fileName, contentType, sizeBytes]
|
||||
properties:
|
||||
fileId: { type: string, pattern: '^[0-9]+$' }
|
||||
fileName: { type: string }
|
||||
contentType: { type: string }
|
||||
sizeBytes: { type: integer, format: int64 }
|
||||
ScanCode:
|
||||
type: object
|
||||
required: [code]
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package com.baiyue.lastleg.merchant;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.inboundplan.controller.MerchantInboundPlanController;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
|
||||
import com.baiyue.lastleg.looseboard.controller.MerchantLocalLooseboardController;
|
||||
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardRequests;
|
||||
import com.baiyue.lastleg.looseboard.service.LocalLooseboardService;
|
||||
import com.baiyue.lastleg.procurement.controller.MerchantOverseasProcurementController;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import com.baiyue.lastleg.trucking.controller.MerchantUsTruckOrderController;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MerchantOverseasCustomerApiContractTest {
|
||||
@Mock LocalLooseboardService looseboardService;
|
||||
@Mock InboundPlanService inboundPlanService;
|
||||
private MerchantLocalLooseboardController looseboard;
|
||||
private MerchantInboundPlanController inbound;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
looseboard = new MerchantLocalLooseboardController();
|
||||
inbound = new MerchantInboundPlanController();
|
||||
ReflectionTestUtils.setField(looseboard, "service", looseboardService);
|
||||
ReflectionTestUtils.setField(inbound, "service", inboundPlanService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantControllersUseFrozenPathsAndExistingClientPermissionCodes() {
|
||||
assertController(MerchantOverseasProcurementController.class,
|
||||
"/api/merchant/v1/last-leg/overseas-procurements", OverseasPermissions.MERCHANT_PROCUREMENT);
|
||||
assertController(MerchantUsTruckOrderController.class,
|
||||
"/api/merchant/v1/last-leg/us-truck-orders", OverseasPermissions.MERCHANT_TRUCKING);
|
||||
assertController(MerchantLocalLooseboardController.class,
|
||||
"/api/merchant/v1/last-leg/local-looseboard-orders", OverseasPermissions.MERCHANT_LOOSEBOARD);
|
||||
assertController(MerchantInboundPlanController.class,
|
||||
"/api/merchant/v1/last-leg/inbound-plans", OverseasPermissions.MERCHANT_INBOUND_LIST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void looseboardAndInboundQueriesUseAuthenticatedCustomerScope() {
|
||||
AuthenticatedUser merchant = user(101L, "MERCHANT");
|
||||
LocalLooseboardRequests.MerchantQuery looseboardQuery = new LocalLooseboardRequests.MerchantQuery(1, 20,
|
||||
null, null, null, null, null, null);
|
||||
InboundPlanRequests.Query inboundQuery = new InboundPlanRequests.Query(1, 20, null, null, null,
|
||||
null, null, null, null, null, null, null, null);
|
||||
|
||||
looseboard.page(merchant, looseboardQuery);
|
||||
inbound.page(merchant, inboundQuery);
|
||||
|
||||
verify(looseboardService).page("101", looseboardQuery);
|
||||
verify(inboundPlanService).page("101", inboundQuery);
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantQueryDtosDoNotAcceptCustomerScope() {
|
||||
for (Class<?> type : Set.of(LocalLooseboardRequests.MerchantQuery.class,
|
||||
com.baiyue.lastleg.procurement.dto.OverseasProcurementRequests.MerchantQuery.class,
|
||||
com.baiyue.lastleg.trucking.dto.UsTruckOrderRequests.MerchantQuery.class,
|
||||
InboundPlanRequests.Query.class)) {
|
||||
assertThat(Arrays.stream(type.getRecordComponents()).map(component -> component.getName()))
|
||||
.doesNotContain("customerId", "warehouseId", "operatorId");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantEndpointsRejectPlatformIdentityBeforeCallingServices() {
|
||||
AuthenticatedUser platform = user(0L, "PLATFORM");
|
||||
assertThatThrownBy(() -> looseboard.detail(platform, "LB-1"))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("403");
|
||||
assertThatThrownBy(() -> inbound.detail(platform, "IP-1"))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("403");
|
||||
}
|
||||
|
||||
private static void assertController(Class<?> type, String path, String permission) {
|
||||
assertThat(type.getAnnotation(RequestMapping.class).value()).containsExactly(path);
|
||||
assertThat(type.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('" + permission + "')");
|
||||
}
|
||||
|
||||
private static AuthenticatedUser user(long customerId, String appCode) {
|
||||
return new AuthenticatedUser(7L, customerId, customerId, 8L, "operator", appCode, "WEB", Set.of());
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.baiyue.lastleg.merchant;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MerchantOverseasOpenApiContractTest {
|
||||
@Test
|
||||
void documentsFrozenMerchantPathsPermissionsResponsesAndConflictSemantics() throws Exception {
|
||||
String yaml;
|
||||
try (var input = getClass().getResourceAsStream("/openapi/overseas-business-v1.yaml")) {
|
||||
assertThat(input).isNotNull();
|
||||
yaml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
for (String path : new String[] {
|
||||
"/api/merchant/v1/last-leg/overseas-procurements:",
|
||||
"/api/merchant/v1/last-leg/overseas-procurements/{orderNo}:",
|
||||
"/api/merchant/v1/last-leg/us-truck-orders:",
|
||||
"/api/merchant/v1/last-leg/us-truck-orders/{orderNo}:",
|
||||
"/api/merchant/v1/last-leg/local-looseboard-orders:",
|
||||
"/api/merchant/v1/last-leg/local-looseboard-orders/{orderNo}:",
|
||||
"/api/merchant/v1/last-leg/inbound-plans:",
|
||||
"/api/merchant/v1/last-leg/inbound-plans/{planNo}:"}) {
|
||||
assertThat(yaml).contains(path);
|
||||
assertThat(pathBlock(yaml, path)).doesNotContain("name: customerId");
|
||||
}
|
||||
assertThat(yaml).contains("merchant:cross-border:procurement")
|
||||
.contains("merchant:cross-border:domestic-trucking")
|
||||
.contains("merchant:cross-border:pallet-shipping")
|
||||
.contains("merchant:warehouse:inbound:list")
|
||||
.contains("#/components/schemas/ProcurementDetailResponse")
|
||||
.contains("#/components/schemas/TruckDetailResponse")
|
||||
.contains("#/components/schemas/LooseboardDetailResponse")
|
||||
.contains("#/components/schemas/InboundPlanDetailResponse")
|
||||
.contains("PENDING_REVIEW").contains("PENDING_ACCEPTANCE")
|
||||
.contains("version 必填").contains("幂等冲突");
|
||||
}
|
||||
|
||||
private static String pathBlock(String yaml, String path) {
|
||||
int start = yaml.indexOf(" " + path);
|
||||
int end = yaml.indexOf("\n /api/", start + path.length());
|
||||
return yaml.substring(start, end < 0 ? yaml.length() : end);
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -50,6 +50,10 @@ class PdaOperationsOpenApiContractTest {
|
||||
"/api/pda/v1/last-leg/stocktakes/{taskNo}/counts:",
|
||||
"/api/pda/v1/last-leg/stocktakes/{taskNo}/submit:",
|
||||
"/api/pda/v1/last-leg/inventory-moves:",
|
||||
"/api/pda/v1/last-leg/inventory/resolve:",
|
||||
"/api/pda/v1/last-leg/locations/resolve:",
|
||||
"/api/pda/v1/last-leg/pallets/resolve:",
|
||||
"/api/pda/v1/last-leg/operations:",
|
||||
"/api/pda/v1/last-leg/pallet-operations/pallets/resolve:",
|
||||
"/api/pda/v1/last-leg/pallet-operations/assemble:",
|
||||
"/api/pda/v1/last-leg/pallet-operations/disassemble:",
|
||||
@@ -74,12 +78,15 @@ class PdaOperationsOpenApiContractTest {
|
||||
"/api/pda/v1/last-leg/outbound-tasks/{taskNo}/pick:",
|
||||
"/api/pda/v1/last-leg/outbound-tasks/{taskNo}/complete:");
|
||||
for (String path : paths) assertTrue(yaml.contains(path), path);
|
||||
assertEquals(28, paths.size());
|
||||
assertEquals(32, paths.size());
|
||||
assertTrue(yaml.contains("enum: [LOCATION, PALLET]"));
|
||||
assertTrue(yaml.contains("enum: [APPROVE, REWORK]"));
|
||||
assertTrue(yaml.contains("enum: [DAMAGED, MISSING_LABEL, LABEL_MISMATCH, UNREADABLE, OTHER]"));
|
||||
for (String field : SERVER_OWNED) assertFalse(yaml.contains(field + ":"), field);
|
||||
assertFalse(yaml.contains("/api/pda/v1/last-leg/delivery"));
|
||||
for (String response : List.of("StocktakeSummaryPageResponse", "StocktakeDetailResponse",
|
||||
"StocktakeItemResponse", "InventoryMoveResponse", "InventoryScanResponse", "LocationResponse",
|
||||
"PalletResponse", "OperationPageResponse", "RelabelCandidatesResponse", "RelabelDetailResponse",
|
||||
"RelabelPhotoResponse")) assertTrue(yaml.contains("#/components/schemas/" + response), response);
|
||||
}
|
||||
|
||||
private static void assertNoServerOwnedFields(Class<?> type) {
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ class PdaWarehouseContractTest {
|
||||
|
||||
@Test
|
||||
void openApiAndGatewayPublishOnlyStablePdaPaths() throws Exception {
|
||||
String openapi = Files.readString(Path.of("src/main/resources/openapi/overseas-business-v1.yaml"));
|
||||
String openapi = Files.readString(Path.of("src/main/resources/openapi/overseas-pda-operations-v1.yaml"));
|
||||
for (String path : Set.of("/api/pda/v1/last-leg/inventory/resolve:",
|
||||
"/api/pda/v1/last-leg/locations/resolve:", "/api/pda/v1/last-leg/pallets/resolve:",
|
||||
"/api/pda/v1/last-leg/operations:")) assertThat(openapi).contains(path);
|
||||
|
||||
+3
-3
@@ -33,9 +33,9 @@ class OverseasProcurementControllerTest {
|
||||
|
||||
@Test
|
||||
void merchantAlwaysUsesTenantFromTrustedIdentity() {
|
||||
var query = new OverseasProcurementRequests.Query(1, 20, null, null, null, null, null, null, null, null);
|
||||
var query = new OverseasProcurementRequests.MerchantQuery(1, 20, null, null, null, null, null, null, null);
|
||||
merchant.page(user(100L, "MERCHANT", Set.of()), query);
|
||||
verify(service).page("100", query);
|
||||
verify(service).page("100", query.scoped());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -48,7 +48,7 @@ class OverseasProcurementControllerTest {
|
||||
|
||||
@Test
|
||||
void merchantEndpointRejectsPlatformIdentity() {
|
||||
var query = new OverseasProcurementRequests.Query(1, 20, null, null, null, null, null, null, null, null);
|
||||
var query = new OverseasProcurementRequests.MerchantQuery(1, 20, null, null, null, null, null, null, null);
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> merchant.page(user(0L, "PLATFORM", Set.of()), query));
|
||||
assertEquals(403, error.getStatusCode().value());
|
||||
|
||||
+5
-3
@@ -31,8 +31,8 @@ class UsTruckOrderControllerTest {
|
||||
|
||||
@Test
|
||||
void merchantListAlwaysUsesTrustedTenant() {
|
||||
var query = query(); merchant.page(user(100L, "MERCHANT", Set.of()), query);
|
||||
verify(service).page("100", query);
|
||||
var query = merchantQuery(); merchant.page(user(100L, "MERCHANT", Set.of()), query);
|
||||
verify(service).page("100", query.scoped());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -92,7 +92,9 @@ class UsTruckOrderControllerTest {
|
||||
verify(service).update("100", "TK1", "7", request);
|
||||
}
|
||||
|
||||
private UsTruckOrderRequests.Query query() { return new UsTruckOrderRequests.Query(1, 20, null, null, null, null, null, null, null); }
|
||||
private UsTruckOrderRequests.MerchantQuery merchantQuery() {
|
||||
return new UsTruckOrderRequests.MerchantQuery(1, 20, null, null, null, null, null, null);
|
||||
}
|
||||
private AuthenticatedUser user(long tenant, String app, Set<String> permissions) {
|
||||
return new AuthenticatedUser(7L, tenant, tenant, 8L, "operator", app, "WEB", permissions);
|
||||
}
|
||||
|
||||
+3
-2
@@ -13,6 +13,7 @@
|
||||
|---|---|
|
||||
| 平台订单 API | `controller/PlatformFirstLegOrderController.java` |
|
||||
| 统一订单查询、审核优先状态与稳定渠道编码 | `service/UnifiedOrderQueryService.java`、`dto/UnifiedOrderResponse.java`、`src/main/resources/openapi/order-v1.yaml`;`channelCode` 直接来自 Order/Firstleg 权威快照,只用于业务校验,不得由 `channelName` 推导。Order 为 `SUBMITTED` 时即使已有 Firstleg 入库投影,统一主状态/下一动作仍固定为 `PENDING_REVIEW/APPROVE_ORDER` |
|
||||
| Firstleg 智能装柜待审核候选 | `controller/InternalPendingReviewOrderController.java`、`service/PendingReviewOrderQueryService.java`、`dto/PendingReviewOrderDtos.java`、`repository/OrderRepository.java`;内部入口 `/internal/order/v1/pending-review-orders` 只接受服务令牌,只分页返回未删除的 `SUBMITTED` 头程订单并固定投影为 `PENDING_REVIEW`,按 `Asia/Shanghai` 自然日过滤、`created_at DESC, id DESC` 稳定排序,不读取或修复 Firstleg 投影 |
|
||||
| 平台版本化修改客户原始申报 | `controller/PlatformUnifiedOrderController.java` 的 `PUT /api/platform/v1/orders/{orderId}/declaration`、`service/OrderDeclarationUpdateService.java`、`dto/OrderDeclarationDtos.java`;权限 `platform:order:declaration:edit`,货物以订单内 `itemNo` 定位并保持行集合完整,契约 `src/main/resources/openapi/order-v1.yaml` |
|
||||
| 申报同步、渠道履约/计价门禁 | `service/OrderFulfillmentChannelChangeGuard.java`、`OrderPricingChannelGuard.java`、`OrderInboundOutboxPublisher.java`,内部客户端 `FirstlegInboundClient.java`、`FirstlegPricingClient.java`;审核后通过版本化 `ORDER_DECLARATION_UPDATED_V1` 同步 Firstleg |
|
||||
| 入仓码业务关系摘要 | `service/InboundCodeAssociationService.java`、`dto/InboundCodeAssociationSummary.java`、平台/客户 `FirstLegOrderController` 的 `/inbound-code-associations/{inboundCode}`;客户入口必须按认证客户隔离 |
|
||||
@@ -21,7 +22,7 @@
|
||||
| 入库号生成 | `config/InboundCodeModeProvider.java`、`mapper/InboundCodeSequenceMapper.java`、`repository/OrderRepository.java`、`service/OrderApplicationService.java` |
|
||||
| 下单计价渠道校验 | `service/OrderPricingChannelGuard.java`、`client/FirstlegPricingClient.java` |
|
||||
| 下单仓库主数据守卫 | `service/OrderMasterDataGuard.java`、`client/WarehouseClient.java`;校验公共收货仓、目的仓类型、状态、地址与收货能力,并保存服务端快照 |
|
||||
| 统一订单七状态与操作映射 | `../../docs/统一订单主状态与操作契约.md`;`LOADED` 硬门禁和 `DELIVERED` 可信回传同时核对三期需求确认表与开工门禁 |
|
||||
| 统一订单七状态与操作映射 | `../../docs/统一订单主状态与操作契约.md`;`LOADED` 硬门禁和 `DELIVERED` 可信回传同时核对国内货柜履约需求确认表与开工门禁 |
|
||||
| 货物内部查询与箱唛绑定来源 | `controller/InternalOrderCargoController.java` 向 Firstleg 提供箱唛生成的 SKU 快照来源;`CargoLabelBinding.skuNo` 对 `ONE_CLICK_DROPSHIP` 为强制字段。Firstleg 消费端为 `../firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java`,变更两端 DTO 必须同步。 |
|
||||
| 智能装柜待审核候选内部查询 | `controller/InternalPendingReviewOrderController.java`、`service/PendingReviewOrderQueryService.java`、`dto/PendingReviewOrderDtos.java`;Firstleg 消费端为 `../firstleg/src/main/java/com/baiyue/firstleg/client/PendingReviewOrderClient.java`,契约 `src/main/resources/openapi/order-v1.yaml` |
|
||||
| 快照/Outbox | `service/`、`repository/`、`event/` |
|
||||
@@ -43,4 +44,4 @@
|
||||
|
||||
## 全量验收导航
|
||||
|
||||
本模块承载订单下单、交易状态、货物查询、父子订单、数量守恒、主状态、幂等和时间线等全量订单契约;按本次验收链路读取对应阶段文档和 `firstleg` 编排契约,不创建独立出仓单,也不绕过 `LOADED` 服务端门禁。
|
||||
本模块承载订单下单、交易状态、货物查询、父子订单、数量守恒、主状态、幂等和时间线等全量订单契约;按本次验收链路读取对应业务域文档和 `firstleg` 编排契约,不创建独立出仓单,也不绕过 `LOADED` 服务端门禁。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 头程订单一期 / First-leg Order Phase 1
|
||||
# 头程订单订单与客户 / First-leg Order Phase 1
|
||||
|
||||
## 数据库归属 / Database ownership
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
| 类别 | 正式入口 | 管理边界 |
|
||||
|---|---|---|
|
||||
| Admin 全量 HTTP 验证 | `admin-test-env-acceptance.mjs` | 默认模块覆盖平台全链路;定向验证使用 `--modules`,模块注册和依赖以总入口及 `admin-test-env-modules/README.md` 为准 |
|
||||
| Client 全量 HTTP 验证 | `client-test-env-acceptance.mjs` | 客户工作台完整链路;`client-phase2-test-env-acceptance.mjs` 只是二期专项,不替代全量入口 |
|
||||
| Client 全量 HTTP 验证 | `client-test-env-acceptance.mjs` | 客户工作台完整链路;`client-phase2-test-env-acceptance.mjs` 只是入库测量与计费专项,不替代全量入口 |
|
||||
| 待审核订单造数 | `create-pending-review-orders.mjs` | 使用已配置客户创建并提交订单,停在待审核;不审核、不推进后续履约 |
|
||||
| 批量待分柜造数 | `it-test-wait-split-bulk.mjs` | 固定“IT测试”和“义乌仓”,随机客户可选真实线路并尽力推进到 `WAIT_SPLIT`;失败记录后继续,不自动删除成功数据;只复用 `TP-001`/`YW-TP-002`,不新增托盘或库位,普通位不足落 `A0` |
|
||||
| Admin HTTP 内部模块 | `admin-test-env-modules/*.mjs` | 由 Admin 总入口动态加载,除文档明确的独立编排器外不得当作独立全量入口;`*contract.test.mjs` 只做离线合同 |
|
||||
|
||||
+12
-12
@@ -37,7 +37,7 @@ node --env-file="$env:USERPROFILE\.codex\chongchong-global-test-acceptance.env"
|
||||
node --env-file="$env:USERPROFILE\.codex\chongchong-global-test-acceptance.env" scripts/it-test-wait-split-bulk.mjs
|
||||
```
|
||||
|
||||
Client 全量、Client 二期、待审核订单和批量待分柜均复用这套变量,不再维护各自的 Gateway、管理员或客户账号变量。外部文件必须保留在仓库之外,不得复制到项目目录、报告或提交记录。
|
||||
Client 全量、Client 入库测量与计费、待审核订单和批量待分柜均复用这套变量,不再维护各自的 Gateway、管理员或客户账号变量。外部文件必须保留在仓库之外,不得复制到项目目录、报告或提交记录。
|
||||
|
||||
## 脚本目录归属
|
||||
|
||||
@@ -65,18 +65,18 @@ Client 全量、Client 二期、待审核订单和批量待分柜均复用这套
|
||||
| 测试环境 HTTP | `admin-test-env-acceptance.mjs` | Admin 全模块真实 HTTP 验收;按模块创建、更新、停用测试数据 |
|
||||
| 测试环境 HTTP | `client-test-env-acceptance.mjs` | Client 完整真实 HTTP 链路;写入客户、订单、入库、权限和消息数据 |
|
||||
| 测试环境 HTTP | `client-quick-order-test-env-acceptance.mjs` | Client 快速下单定向真实 HTTP 验收;使用既有测试客户创建 FBA、海外仓和私人地址草稿,验证逐行失败与幂等重放,不提交订单 |
|
||||
| 测试环境 HTTP | `client-phase2-test-env-acceptance.mjs` | Client 二期入库、测量、费用和权限专项;写测试数据 |
|
||||
| 测试环境 HTTP | `client-phase2-test-env-acceptance.mjs` | Client 入库、测量、费用和权限专项;写测试数据 |
|
||||
| 测试环境 HTTP | `create-pending-review-orders.mjs` | 固定客户批量创建并提交待审核订单,不做平台审核和后续履约 |
|
||||
| 测试环境 HTTP | `it-test-wait-split-bulk.mjs` | 固定“IT测试”与“义乌仓”,渠道等随机,逐票推进到 `WAIT_SPLIT`;失败继续并落报告。只复用 `TP-001`/`YW-TP-002`,不新增托盘或库位,库位不足使用公共位 `A0` |
|
||||
| 测试环境 HTTP | `dimension-ocr-pricing-test-env.ps1` | 尺寸、OCR 和渠道计价专项,上传本地图片并写测试业务数据;包含历史默认参数,只能显式传入当前目标参数后使用 |
|
||||
| 测试环境 HTTP | `export-center-test-env-acceptance.ps1` | 导出中心测试环境验收,会创建导出任务和相关测试数据 |
|
||||
| 测试环境 HTTP | `phase2-test-env-acceptance.ps1` | 二期测试环境 API 验收,会写业务数据 |
|
||||
| 测试环境 HTTP | `phase2-test-env-acceptance.ps1` | 入库测量与计费测试环境 API 验收,会写业务数据 |
|
||||
| 场景组件 | `mojia-measurement-scenario.mjs` | 被 Admin 验收入口复用的墨家测量场景;不是独立 CLI,调用正式测量接口并上传图片 |
|
||||
| 定向诊断 | `pda-final-complete-diagnostic.mjs` | 对指定 PDA task 回查最终完成门禁;传 `--seed-evidence` 时会补五类证据并产生写操作 |
|
||||
| 本地真实 API | `order-warehouse-real-api-regression.ps1` | 本地 Order/Warehouse 往来单位真实 API 回归,并直接读写本机 MySQL;不得指向共享或测试数据库 |
|
||||
| 一期本地验收 | `phase1-acceptance.ps1` | 一期本地服务/API 综合验收,会创建本地业务数据 |
|
||||
| 一期本地验收 | `phase1-business-maturity-regression.ps1` | 一期业务成熟度回归,调用本地服务并生成报告 |
|
||||
| 一期本地验收 | `phase1-onboarding-order-e2e.ps1` | 注册、资质、审核到下单的本地 E2E,会创建客户、仓库、往来单位和订单 |
|
||||
| 订单与客户本地验收 | `phase1-acceptance.ps1` | 订单与客户本地服务/API 综合验收,会创建本地业务数据 |
|
||||
| 订单与客户本地验收 | `phase1-business-maturity-regression.ps1` | 订单与客户业务成熟度回归,调用本地服务并生成报告 |
|
||||
| 订单与客户本地验收 | `phase1-onboarding-order-e2e.ps1` | 注册、资质、审核到下单的本地 E2E,会创建客户、仓库、往来单位和订单 |
|
||||
| 本地质量 | `backend-full-regression.ps1` | Mapper 门禁后执行受影响后端 Maven 测试;`-DryRun` 只打印与校验 |
|
||||
| 本地质量 | `frontend-full-regression.ps1` | 编排 Admin/Client 本地测试与类型检查,不访问 Gateway |
|
||||
| 本地质量 | `database-comment-audit.ps1` | 离线扫描数据库中文注释,不连接数据库 |
|
||||
@@ -106,7 +106,7 @@ Client 全量、Client 二期、待审核订单和批量待分柜均复用这套
|
||||
| 客户工作台完整 HTTP 链路验收 | `scripts/client-test-env-acceptance.mjs` | 会创建测试企业、订单、入库、权限和站内消息数据 | 注册/审核、客户隔离、订单、入库计费、文件、财务、客户侧权限,以及我的消息未读/已读/全部已读链路 |
|
||||
| Client 快速下单定向 HTTP 验收 | `scripts/client-quick-order-test-env-acceptance.mjs` | 使用 `MERCHANT_TEST_USERNAME/PASSWORD` 对应客户创建快速下单草稿 | 真实调用 `/api/merchant/v1/orders/first-leg/batch`,覆盖 FBA、多产品、海外仓、多个私人地址、地址归属失败、渠道失败、重复行、逐行隔离、失败重试、详情归属、幂等重放与冲突;只创建草稿,不提交、不审核 |
|
||||
| 固定客户批量创建待审核订单 | `scripts/create-pending-review-orders.mjs` | 只使用 `MERCHANT_TEST_USERNAME/PASSWORD` 配置的客户账号创建指定数量订单 | 使用 `ADMIN_TEST_GATEWAY`,选择真实可计价仓库/渠道,创建并提交订单,停在待审核,不创建客户、不审核订单 |
|
||||
| Client 二期专项 HTTP 验收 | `scripts/client-phase2-test-env-acceptance.mjs` | 会写入测试环境业务数据 | 二期客户入库、测量、费用和权限专项场景;不是替代 Client 全量入口 |
|
||||
| Client 入库测量与计费专项 HTTP 验收 | `scripts/client-phase2-test-env-acceptance.mjs` | 会写入测试环境业务数据 | 入库测量与计费客户入库、测量、费用和权限专项场景;不是替代 Client 全量入口 |
|
||||
| 人工分柜至 LOADED 分段验收 | `scripts/admin-test-env-acceptance.mjs` 的 `manual-*-fixture/verify` 可选模块;步骤和参数见 `scripts/admin-test-env-modules/README.md` | fixture 通过正式 HTTP 准备本轮数据,verify 只读核验;不进入默认全量 | `WAIT_SPLIT → 人工分柜 → 推送预装 → 订舱关联/计划确认 → PDA 领取与原子 batch → 五类证据/完成 → LOADED` |
|
||||
|
||||
执行前必须确认目标环境、账号和数据清理/停用策略,并获得当前对话的测试环境验收授权。`admin-test-env-modules/*contract.test.mjs` 只验证模块脚本结构,不能代替 HTTP 验收。
|
||||
@@ -143,7 +143,7 @@ node scripts/create-pending-review-orders.mjs
|
||||
| Admin/Client 本地测试与类型检查 | `scripts/frontend-full-regression.ps1` |
|
||||
| PDA 本地页面和状态测试 | `../apps/pda-uniapp/tests/*.test.mjs` |
|
||||
| 本地数据库 SQL 静态/受控验收 | `scripts/local-db-full-acceptance.ps1` |
|
||||
| AGENTS 快速索引一致性 | `scripts/agents-index-contract.ps1`;只读核对根快速表唯一性、部署单元、业务模块、Client active 路由、四期范围漂移、仓库主档唯一权威、订舱候选与柜量语义、整柜出仓与船节点单调性、海外 Gateway 覆盖、PDA 页面/运行配置/独立更新/语言预留/能力开关、人工分段验收链、Maven 模块、Lastleg 海外安全投影、同步 Trace、关键入口与排除目录 |
|
||||
| AGENTS 快速索引一致性 | `scripts/agents-index-contract.ps1`;只读核对根快速表唯一性、部署单元、业务模块、Client active 路由、海外仓业务范围漂移、仓库主档唯一权威、订舱候选与柜量语义、整柜出仓与船节点单调性、海外 Gateway 覆盖、PDA 页面/运行配置/独立更新/语言预留/能力开关、人工分段验收链、Maven 模块、Lastleg 海外安全投影、同步 Trace、关键入口与排除目录 |
|
||||
| Mapper 注册静态合同 | `node scripts/mapper-scan-contract.mjs`、`node --test scripts/mapper-scan-contract.test.mjs`;自动核对全部持久化模块的 `@Mapper`、服务根包注解过滤扫描和唯一扫描入口,不启动 Spring、不连接数据库;`backend-full-regression.ps1` 在 Maven 前强制执行 |
|
||||
| Java 质量聚合摘要 | `node --test scripts/java-quality-summary.test.mjs`、`node scripts/java-quality-summary.mjs --output target/java-quality-summary.json`;聚合 Maven/服务拓扑、OpenAPI 覆盖、JaCoCo 和 SpotBugs 报告,默认只报告存量缺口,CI 用显式 `--require-jacoco --require-spotbugs` 检查报告完整性且当前允许失败 |
|
||||
| 单服务部署失败回滚合同 | `scripts/deploy-built-service-rollback.test.mjs`;模拟成功、健康失败和启动失败,确认仅恢复同服务先前健康镜像且部署任务仍失败 |
|
||||
@@ -153,7 +153,7 @@ Admin/Client HTTP 脚本的离线合同只验证脚本结构、参数和模块
|
||||
|
||||
| 合同范围 | 入口 |
|
||||
|---|---|
|
||||
| Admin 订单、箱码、三期装柜/PDA、订舱、拖车、通知 | `scripts/admin-test-env-modules/*contract.test.mjs`;模块映射见 `scripts/admin-test-env-modules/README.md` |
|
||||
| Admin 订单、箱码、国内货柜履约装柜/PDA、订舱、拖车、通知 | `scripts/admin-test-env-modules/*contract.test.mjs`;模块映射见 `scripts/admin-test-env-modules/README.md` |
|
||||
| Client 消息中心调用 | `scripts/client-notifications-contract.test.mjs` |
|
||||
| 验收入口与模块注册 | `scripts/full-regression-entry-contract.test.mjs`、`scripts/controller-pipeline-contract.test.mjs` |
|
||||
|
||||
@@ -188,9 +188,9 @@ mvn -f <仓库根>/pom.xml -pl order,firstleg,iam,gateway,file,warehouse -am tes
|
||||
|
||||
这会先构建公共依赖,再运行 `order`、`firstleg`、`iam`、`gateway`、`file` 与 `warehouse` 的全部 Maven 测试。覆盖包括:
|
||||
|
||||
- 三期 Ocean Booking Controller/Service/Mapper/SQL 契约;
|
||||
- 三期 Truck Appointment Controller/Service/Mapper 契约、状态机、乐观锁和幂等操作;
|
||||
- 二期 `WAIT_SPLIT` 的 firstleg 查询、order 统一查询及筛选语义;
|
||||
- 国内货柜履约 Ocean Booking Controller/Service/Mapper/SQL 契约;
|
||||
- 国内货柜履约 Truck Appointment Controller/Service/Mapper 契约、状态机、乐观锁和幂等操作;
|
||||
- 入库测量与计费 `WAIT_SPLIT` 的 firstleg 查询、order 统一查询及筛选语义;
|
||||
- Warehouse 海外仓阶梯费用 SQL 与 Controller 权限合同;
|
||||
- 拣货/预装/装柜模型映射与既有 Smart Loading/Split 契约。
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
| 对话 ID | 主题 | 负责文件 / 场景 |
|
||||
|---|---|---|
|
||||
| 当前总控对话 | Admin 全量脚本 | 总入口、`system.mjs`、`master-data.mjs`、`downloads.mjs`、最终集成与全量报告 |
|
||||
| `019ff0da-7699-7532-ac64-703325c995fd` | Finance 一期验收总控 | `finance.mjs`、Finance 正式全量接入与报告 |
|
||||
| `019ff0da-7699-7532-ac64-703325c995fd` | Finance 订单与客户验收总控 | `finance.mjs`、Finance 正式全量接入与报告 |
|
||||
| `019ff905-a477-7a03-b806-953ba598d305` | Finance 下载中心共享契约 | `finance-downloads.mjs`、直接契约测试与 Finance 完整链接入;不覆盖通用 `downloads.mjs` |
|
||||
| `019fdbd3-f24f-7903-9124-de79c3ba37d0` | 客户尺寸规则 | 总入口内 `customer-dimension-rules` 子场景;不得改其他 customers 流程 |
|
||||
| `019fdb7d-8e29-7c40-bda6-5a29dcb37971` | OCR 扫描 | `inbound-weight-ocr.mjs` |
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function run(context) {
|
||||
| 订单创建、可计价主数据、幂等零漂移与审核异步收敛 | `orders.mjs` | `orders-pricing-fixture-contract.test.mjs`、`orders-lifecycle-idempotency-contract.test.mjs`、`orders-approval-convergence-contract.test.mjs` |
|
||||
| 入库测量、首次打印冻结箱码与墨家扫码 | `inbound-measurement.mjs` | `inbound-measurement-box-code-contract.test.mjs` |
|
||||
| 入仓自动头程费用、派生详情、人工发布与 MANUAL 其他费用版本 | `inbound-fees.mjs`(前置:`merchant-qualification-fixture,orders,inbound-measurement`) | `inbound-fees-contract.test.mjs` |
|
||||
| 三期模拟柜关联、柜次概览、渠道隔离、装柜模型桥接、PDA、动态增量、四类货柜资料、报关与证据/出仓门禁 | `phase3-container-loading.mjs`、`ocean-booking-fixture.mjs`、`phase3-container-overview.mjs`、`phase3-loading.mjs`、`phase3-loading-model-bridge.mjs`、`phase3-pda-convergence.mjs`、`phase3-evidence-gate.mjs`、`phase3-loading-finalize.mjs`、`phase3-container-documents.mjs`、`phase3-customs.mjs`、`phase3-outbound-dispatch.mjs` | `container-simulation-association-contract.test.mjs`、`container-specification-fixture-contract.test.mjs`、`ocean-booking-channel-contract.test.mjs`、`phase3-container-regression-contract.test.mjs`、`phase3-loading-model-bridge-contract.test.mjs`、`phase3-loading-dynamic-final-contract.test.mjs`、`phase3-container-documents-contract.test.mjs`、`phase3-phase4-departure-pickup-chain-contract.test.mjs`、`phase3-producer-chain-contract.test.mjs`、`manual-state-loading-pda-contract.test.mjs` |
|
||||
| 国内货柜履约模拟柜关联、柜次概览、渠道隔离、装柜模型桥接、PDA、动态增量、四类货柜资料、报关与证据/出仓门禁 | `phase3-container-loading.mjs`、`ocean-booking-fixture.mjs`、`phase3-container-overview.mjs`、`phase3-loading.mjs`、`phase3-loading-model-bridge.mjs`、`phase3-pda-convergence.mjs`、`phase3-evidence-gate.mjs`、`phase3-loading-finalize.mjs`、`phase3-container-documents.mjs`、`phase3-customs.mjs`、`phase3-outbound-dispatch.mjs` | `container-simulation-association-contract.test.mjs`、`container-specification-fixture-contract.test.mjs`、`ocean-booking-channel-contract.test.mjs`、`phase3-container-regression-contract.test.mjs`、`phase3-loading-model-bridge-contract.test.mjs`、`phase3-loading-dynamic-final-contract.test.mjs`、`phase3-container-documents-contract.test.mjs`、`phase3-phase4-departure-pickup-chain-contract.test.mjs`、`phase3-producer-chain-contract.test.mjs`、`manual-state-loading-pda-contract.test.mjs` |
|
||||
| 头程整柜报关 | `phase3-container-customs-fixture.mjs`、`phase3-container-customs.mjs`;定向入口通过正式订单、测量、同柜装载和最终完成 API 生成独立 `BUY_DECLARATION + TAX_REFUND` 混合柜,客户资料只覆盖退税票,买单行由下载中心 XLSX 验证;不复用历史柜、不执行 SQL | `container-customs-acceptance-contract.test.mjs`、`container-customs-acceptance-matrix.mjs` |
|
||||
| 分柜与父子入库单数量守恒 | `phase3-order-splitting.mjs` | `phase3-order-splitting-contract.test.mjs` |
|
||||
| 同柜且不可拆组 | `coload-groups.mjs`(可选;前置正式生成至少7票同线路 `WAIT_SPLIT`,另造不同线路反向票;覆盖创建/取消幂等、原子分配/移除、目标组隔离超容拒绝与零漂移、推送防绕过、PDA短拣完成拒绝且无余量子单、足量整组消费、普通单拆分不回归及已有父子单建组拒绝) | `coload-groups-contract.test.mjs` |
|
||||
@@ -78,19 +78,19 @@ export async function run(context) {
|
||||
| 消息中心管理 | `notifications.mjs` | `notifications-contract.test.mjs` |
|
||||
| Finance 真实前置、对账、结算与下载中心 | `finance-full-fixtures.mjs`、`finance-reconciliation-full.mjs`、`finance-settlement-full.mjs`、`finance-downloads.mjs` | `finance-full-fixtures-contract.test.mjs`、`finance-reconciliation-full-contract.test.mjs`、`finance-settlement-full-contract.test.mjs`、`finance-downloads-contract.test.mjs` |
|
||||
| Finance 扩展隔离与跨区费用 | `firstleg-container-fee-allocation.mjs`(柜次跨客户分摊)、`finance-direct-paid-isolation.mjs`(海外仓服务直接已付)、`phase4-overseas-finance-cross-region.mjs`(五类海外费用跨区进入 Finance) | 对应同名 `*-contract.test.mjs`;跨区事件持久化链在本轮获授权时另以只读数据库证据核对 |
|
||||
| 四期海外仓 HTTP 生命周期 | `phase4-overseas-warehouse.mjs` | `phase4-overseas-warehouse-contract.test.mjs` |
|
||||
| 四期海外 PDA 柜号收货与库内操作 HTTP 生命周期 | `phase4-overseas-pda.mjs`(按柜完成签收和实收,成功后仅以内存上下文交接操作员会话和已完成库存事实)、`phase4-overseas-pda-operations.mjs`(紧随收货执行 P2 盘点、P3 整托移库、P4 托盘拆装换绑、P5 换标复核;finally 停用双操作员并清空临时 Token;任何真实 fixture 缺失均 fail-closed) | `phase4-overseas-pda-contract.test.mjs`、`phase4-overseas-pda-operations-contract.test.mjs` |
|
||||
| 三四期跨区事件与安全门禁 | `phase4-cross-region-events.mjs`(双 Token Audience 互斥、canonical 权限 403、国内装柜完成到 Lastleg 建单、海外柜完成到 Firstleg 回写、轮询重试/幂等/诊断证据;缺本轮 marker 前置时阻断) | `phase4-cross-region-events-contract.test.mjs` |
|
||||
| 海外仓 HTTP 生命周期 | `phase4-overseas-warehouse.mjs` | `phase4-overseas-warehouse-contract.test.mjs` |
|
||||
| 海外 PDA 柜号收货与库内操作 HTTP 生命周期 | `phase4-overseas-pda.mjs`(按柜完成签收和实收,成功后仅以内存上下文交接操作员会话和已完成库存事实)、`phase4-overseas-pda-operations.mjs`(紧随收货执行盘点、整托移库、托盘拆装换绑和换标复核;finally 停用双操作员并清空临时 Token;任何真实 fixture 缺失均 fail-closed) | `phase4-overseas-pda-contract.test.mjs`、`phase4-overseas-pda-operations-contract.test.mjs` |
|
||||
| 三海外仓业务跨区事件与安全门禁 | `phase4-cross-region-events.mjs`(双 Token Audience 互斥、canonical 权限 403、国内装柜完成到 Lastleg 建单、海外柜完成到 Firstleg 回写、轮询重试/幂等/诊断证据;缺本轮 marker 前置时阻断) | `phase4-cross-region-events-contract.test.mjs` |
|
||||
| 国内离仓至海外 PDA 可见人工验收节点 | `phase3-customs.mjs`(四类柜资料与整柜照片)、`phase3-outbound-dispatch.mjs`(柜级离仓)、`phase3-carrier-schedule-queries.mjs`、`phase4-carrier-pickup-ready.mjs`、`phase4-overseas-pda.mjs`(同柜候选) | `phase3-phase4-departure-pickup-chain-contract.test.mjs` |
|
||||
| 四期海外业务 HTTP 生命周期 | `phase4-overseas-business.mjs` | `phase4-overseas-business-contract.test.mjs` |
|
||||
| 四期海外主数据 HTTP 生命周期 | `phase4-overseas-master-data.mjs` | `phase4-overseas-master-data-contract.test.mjs` |
|
||||
| 四期船节点安全冒烟 | `phase4-carrier-nodes.mjs`(standalone 对历史任务只读;仅复用当前 marker 的 `phase3CarrierScheduleQuery` 时补录预计事实) | `phase4-carrier-nodes-contract.test.mjs` |
|
||||
| 五期海外排车、出库、派送、异常与回访 HTTP 生命周期 | `phase5-overseas-fulfillment.mjs`(复用本轮四期已收货托盘和预排单,正式创建临时 PDA 操作员并在 `finally` 停用;覆盖排车编辑、转出库/派送、PDA 全量拣货/发车、派送完成、客户回访、人工异常及派送退回) | `phase5-overseas-fulfillment-contract.test.mjs` |
|
||||
| 海外业务 HTTP 生命周期 | `phase4-overseas-business.mjs` | `phase4-overseas-business-contract.test.mjs` |
|
||||
| 海外主数据 HTTP 生命周期 | `phase4-overseas-master-data.mjs` | `phase4-overseas-master-data-contract.test.mjs` |
|
||||
| 海外仓业务船节点安全冒烟 | `phase4-carrier-nodes.mjs`(standalone 对历史任务只读;仅复用当前 marker 的 `phase3CarrierScheduleQuery` 时补录预计事实) | `phase4-carrier-nodes-contract.test.mjs` |
|
||||
| 海外排车、出库、派送、异常与回访 HTTP 生命周期 | `phase5-overseas-fulfillment.mjs`(复用本轮海外仓已收货托盘和预排单,正式创建临时 PDA 操作员并在 `finally` 停用;覆盖排车编辑、转出库/派送、PDA 全量拣货/发车、派送完成、客户回访、人工异常及派送退回) | `phase5-overseas-fulfillment-contract.test.mjs` |
|
||||
| 初始化基线(仅离线) | — | `initialization-baseline-contract.test.mjs` |
|
||||
|
||||
表中的 `*-fixture.mjs` 是被业务模块复用的测试数据前置,不是新增的独立全量入口。执行 HTTP 时仍从 `../admin-test-env-acceptance.mjs` 进入;`*contract.test.mjs` 只做离线脚本结构和数据归属回归。
|
||||
|
||||
四期 HTTP 验收使用国内登录响应同时签发的短时海外 Token。总入口通过 `--gateway` 访问国内 Gateway,通过 `--overseas-gateway` 访问海外 Gateway。海外仓、海外业务和海外主数据模块只对带本轮 marker 的资源执行已确认的新增、编辑、状态与导出,并在结束前按契约停用;PDA 收货模块使用本轮临时 OVERSEAS_WAREHOUSE_PDA_OPERATOR 海外 Token,只调用 `/api/pda/v1/last-leg/receiving-containers/**` 柜号收货入口,不接受 `customerId`、`planNo` 或分配 ID。收货成功后由相邻的 `phase4-overseas-pda-operations` 消费仅存在于进程内存、不会进入 JSON 报告的 Token 与库存事实,执行盘点、整托移库、托盘操作和换标;新模块无论成功失败都在 finally 停用两个 picker 并清空临时凭据。两模块均不调用出库、退货出库、派送或内部接口;任何真实柜、箱唛、托盘、库存或换标前置缺失都必须形成 deployment blocker,不能按空场景通过;船节点禁止手工创建任务。
|
||||
海外仓业务 HTTP 验收使用国内登录响应同时签发的短时海外 Token。总入口通过 `--gateway` 访问国内 Gateway,通过 `--overseas-gateway` 访问海外 Gateway。海外仓、海外业务和海外主数据模块只对带本轮 marker 的资源执行已确认的新增、编辑、状态与导出,并在结束前按契约停用;PDA 收货模块使用本轮临时 OVERSEAS_WAREHOUSE_PDA_OPERATOR 海外 Token,只调用 `/api/pda/v1/last-leg/receiving-containers/**` 柜号收货入口,不接受 `customerId`、`planNo` 或分配 ID。收货成功后由相邻的 `phase4-overseas-pda-operations` 消费仅存在于进程内存、不会进入 JSON 报告的 Token 与库存事实,执行盘点、整托移库、托盘操作和换标;新模块无论成功失败都在 finally 停用两个 picker 并清空临时凭据。两模块均不调用出库、退货出库、派送或内部接口;任何真实柜、箱唛、托盘、库存或换标前置缺失都必须形成 deployment blocker,不能按空场景通过;船节点禁止手工创建任务。
|
||||
|
||||
节点全链定向入口为 `--modules phase4-overseas-pda`。依赖展开固定经过本轮 `LOADED` 柜次的 `DESTINATION_CLEARANCE`、`CUSTOMS_DECLARATION`、`DELIVERY_ORDER`、`PICKUP_ORDER` 四类资料生命周期,再确认国内出仓、核对自动创建的 `OUTBOUND_CONFIRM` 船节点任务、人工补录至实际 `PICKUP_READY`,最后验证海外 PDA 候选可见。四类资料模块只写本轮 marker 柜次,覆盖上传/完成重放、同键异载荷 `A0409`、冲突后零漂移和聚合回读;任一 fixture 或 Trace 缺失均为 deployment blocker。
|
||||
|
||||
@@ -104,7 +104,7 @@ node scripts/admin-test-env-acceptance.mjs --gateway <development-gateway> --ove
|
||||
|
||||
只有已登记在 `../admin-test-env-acceptance.mjs` 的 `CONFIGURED_MODULES` 中的名称才能用于 `--modules`;目录中存在同名 `.mjs` 不代表总入口已注册。新增或改名 HTTP 模块时必须同步总入口、依赖展开、离线入口合同和本表;尚未注册的实验脚本不得写成可执行验收入口。
|
||||
|
||||
五期履约链定向入口为 `--modules phase5-overseas-fulfillment`。依赖展开固定从本轮三期装柜完成、四期跨区接柜和海外 PDA 实收入库生成真实库存,再由四期智能预排创建 marker 专属 `PENDING_DEPARTURE` 排车。五期模块只消费该本轮排车,先补齐车辆与司机,原子转为 `WAIT_OUTBOUND + WAIT_DELIVERY`,随后使用绑定同一海外仓的临时 PDA 身份执行全量拣货和发车,使出库单进入 `OUTBOUNDED`;最后覆盖派送完成、回访、人工异常与退回海外仓。所有幂等重放都核对业务单号或数量不漂移,真实前置不足必须形成 deployment blocker,临时 PDA 身份无论成功失败都由正式 IAM 状态接口停用。
|
||||
海外仓出库与派送履约链定向入口为 `--modules phase5-overseas-fulfillment`。依赖展开固定从本轮国内货柜履约装柜完成、海外仓业务跨区接柜和海外 PDA 实收入库生成真实库存,再由海外仓业务智能预排创建 marker 专属 `PENDING_DEPARTURE` 排车。海外仓出库与派送模块只消费该本轮排车,先补齐车辆与司机,原子转为 `WAIT_OUTBOUND + WAIT_DELIVERY`,随后使用绑定同一海外仓的临时 PDA 身份执行全量拣货和发车,使出库单进入 `OUTBOUNDED`;最后覆盖派送完成、回访、人工异常与退回海外仓。所有幂等重放都核对业务单号或数量不漂移,真实前置不足必须形成 deployment blocker,临时 PDA 身份无论成功失败都由正式 IAM 状态接口停用。
|
||||
|
||||
```powershell
|
||||
node --env-file="$env:USERPROFILE\.codex\chongchong-global-dev-acceptance.env" scripts/admin-test-env-acceptance.mjs --modules phase5-overseas-fulfillment
|
||||
@@ -118,7 +118,7 @@ node --env-file="$env:USERPROFILE\.codex\chongchong-global-dev-acceptance.env" s
|
||||
|
||||
其中,无号 push 只冻结装柜计划、建立 bridge 并使预装可见,不得创建 PDA task;订舱放舱也不创建真实货柜或 allocation。港口取得真实柜号后,四字段 link 直接补全原模拟柜并创建 allocation,原 `containerBookingId/cabinetNo/loadingPlan/items` 不变;未关联、订舱未 `RELEASED` 或整份计划未确认时,PDA 查询必须返回业务码 `A0404`。唯一确认入口为 `POST /api/platform/v1/first-leg/loading-plans/{loadingPlanId}/confirm`,请求体只含当前计划 `expectedVersion` 并携带 `Idempotency-Key`;成功必须返回 `confirmationStatus=CONFIRMED`、`readiness.readyForPda=true` 和唯一 `readiness.pdaTaskId`。验收脚本禁止调用旧入库单级 `/preload/confirming`、`/preload/confirm`。
|
||||
|
||||
计划在 `EDITING/LOADING` 可通过正式 assignments/PATCH/DELETE 端点增量;计划、任务和明细版本必须递增,PDA 四元身份不变,旧 `inventorySnapshotToken` 必须返回 `A0409 库存已变化,请刷新后重试`。完整三期装柜 fixture 通过平台托盘 API 创建本轮专用称重托盘,再交给通用测量模块;当普通位不足时由 Warehouse 自然分配到 A0,随后仅使用公开 A0 换绑接口制造库存版本变化,不直连数据库或调用内部 Warehouse 接口。主三期链强制完成该专项,四期生产矩阵的附加柜只负责跨区生产,不重复要求 A0;附加柜若自然落入 A0 仍执行同一验证。批量待分柜入口仍只复用 `TP-001`/`YW-TP-002`,不创建托盘。已拣项不可删或减量,未拣项可删;增量会把全拣的 `AWAITING_REVIEW` 撤回为 `IN_PROGRESS`,刷新 task detail 与 inventory 后续拣才可再次等待最终确认。最终唯一完成入口仍为 `POST /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/complete`:保留同柜 `RELEASED` 订舱、bridge 与五类 ACTIVE 证据门禁;拖车状态和数量不是前置门禁。主事务提交后,零条有效拖车直接跳过,恰有一条时在独立事务尝试推进 `COMPLETED`,多条或并发冲突只记录拖车同步失败,不回滚已完成的 task/plan/inbound。PDA 结算可能先递增来源入库单版本或创建实装分片,完成动作必须在结算后重新锁定实际完成行并使用当前版本;HTTP finalizer 会回查 full/partial 履约分片,失败时保存 `requestId/traceId` 并断言 task/plan/inbound/operation 零漂移。后续模块必须复用 `phase3Loading` 发布的本轮 `taskId + inboundId + loadingPlanId + containerBookingId`,不得回退历史资源。
|
||||
计划在 `EDITING/LOADING` 可通过正式 assignments/PATCH/DELETE 端点增量;计划、任务和明细版本必须递增,PDA 四元身份不变,旧 `inventorySnapshotToken` 必须返回 `A0409 库存已变化,请刷新后重试`。完整国内货柜履约装柜 fixture 通过平台托盘 API 创建本轮专用称重托盘,再交给通用测量模块;当普通位不足时由 Warehouse 自然分配到 A0,随后仅使用公开 A0 换绑接口制造库存版本变化,不直连数据库或调用内部 Warehouse 接口。主国内货柜履约链强制完成该专项,海外仓业务生产矩阵的附加柜只负责跨区生产,不重复要求 A0;附加柜若自然落入 A0 仍执行同一验证。批量待分柜入口仍只复用 `TP-001`/`YW-TP-002`,不创建托盘。已拣项不可删或减量,未拣项可删;增量会把全拣的 `AWAITING_REVIEW` 撤回为 `IN_PROGRESS`,刷新 task detail 与 inventory 后续拣才可再次等待最终确认。最终唯一完成入口仍为 `POST /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/complete`:保留同柜 `RELEASED` 订舱、bridge 与五类 ACTIVE 证据门禁;拖车状态和数量不是前置门禁。主事务提交后,零条有效拖车直接跳过,恰有一条时在独立事务尝试推进 `COMPLETED`,多条或并发冲突只记录拖车同步失败,不回滚已完成的 task/plan/inbound。PDA 结算可能先递增来源入库单版本或创建实装分片,完成动作必须在结算后重新锁定实际完成行并使用当前版本;HTTP finalizer 会回查 full/partial 履约分片,失败时保存 `requestId/traceId` 并断言 task/plan/inbound/operation 零漂移。后续模块必须复用 `phase3Loading` 发布的本轮 `taskId + inboundId + loadingPlanId + containerBookingId`,不得回退历史资源。
|
||||
|
||||
`phase3-pda-convergence` 同时回查 `/tasks/pending` 与稳定 task detail:两者的 `workflowStatus`、`itemCount`、`pendingItemCount` 必须来自同一任务明细事实。全拣任务必须在两处均为 `AWAITING_REVIEW`,可执行证据 GET,但派生 `actions=[]`,不得重复发送 batch;操作历史必须恰有一次有效 `BATCH`。若 pending 仍按 assignment 返回 `IN_PROGRESS`,该轮以真实 HTTP 失败阻断,不由脚本或前端改写状态。
|
||||
|
||||
@@ -188,7 +188,7 @@ node --test scripts/admin-test-env-modules/domestic-container-contract.test.mjs
|
||||
|
||||
## 货柜履约列表字段合同
|
||||
|
||||
`fulfillment-list-field-contract.mjs` 统一校验货柜、预装/装柜清单、订舱、船期、拖车、报关协同和国内出仓正式响应中的稳定 ID、非空展示字段、非负数量/重量/体积及 ISO 时间格式。各生产模块在已取得的本轮 marker 响应上执行断言,不额外借用历史数据;未冻结的原型字段只登记在 `docs/三期/货柜履约原型字段验收矩阵-2026-08-17.md` 的 `UNKNOWN`,不得由 fixture 伪造。
|
||||
`fulfillment-list-field-contract.mjs` 统一校验货柜、预装/装柜清单、订舱、船期、拖车、报关协同和国内出仓正式响应中的稳定 ID、非空展示字段、非负数量/重量/体积及 ISO 时间格式。各生产模块在已取得的本轮 marker 响应上执行断言,不额外借用历史数据;未冻结的原型字段只登记在 `docs/国内货柜履约/货柜履约原型字段验收矩阵-2026-08-17.md` 的 `UNKNOWN`,不得由 fixture 伪造。
|
||||
|
||||
离线合同:
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const containerList = read(`${adminRoot}container-loading/pages/ContainersView.v
|
||||
const containerOverviewPanel = read(`${adminRoot}container-loading/components/ContainerOverviewPanel.vue`)
|
||||
const containerLoadingAcceptance = read('phase3-container-loading.mjs')
|
||||
const truckAcceptance = read('truck-appointments.mjs')
|
||||
const matrix = read('../../../docs/三期/货柜管理全链验收矩阵-2026-08-13.md')
|
||||
const matrix = read('../../../docs/国内货柜履约/货柜管理全链验收矩阵-2026-08-13.md')
|
||||
|
||||
test('DRAFT stores all five transport fields and release validates the persisted snapshot', () => {
|
||||
assert.match(oceanOpenApi, /ReleaseRequest: \{type: object, required: \[expectedVersion\]/)
|
||||
|
||||
@@ -130,7 +130,7 @@ Assert-NotContains $testTaskRegistry '01a05b10-a5fc-7d51-8203-c671f3a823ad' '测
|
||||
Assert-NotContains $testTaskRegistry '6f874ec3-d3b2-49bd-8625-327f4d685f98' '测试登记开发 projectId'
|
||||
Assert-Contains $devTaskRegistry 'projectId `6f874ec3-d3b2-49bd-8625-327f4d685f98`' '开发登记保存项目'
|
||||
Assert-Contains $devTaskRegistry 'C:\Users\BY-OY\Desktop\code\chongchong-global-dev`' '开发登记工作目录'
|
||||
Assert-Contains $devTaskRegistry '`01a05b10-a5fc-7d51-8203-c671f3a823ad`' '开发环境五期总控'
|
||||
Assert-Contains $devTaskRegistry '`01a05b10-a5fc-7d51-8203-c671f3a823ad`' '开发环境全流程总控'
|
||||
Assert-NotContains $devTaskRegistry '01a0194a-259c-7171-8967-074731a27618' '开发登记测试总控'
|
||||
Assert-NotContains $devTaskRegistry 'c64729ff-6710-43c6-8ef2-d84e9666cc1e' '开发登记测试 projectId'
|
||||
$testTaskIds = @([regex]::Matches($testTaskRegistry, '(?m)^\|\s*`([0-9a-f-]{36})`\s*\|') | ForEach-Object { $_.Groups[1].Value })
|
||||
@@ -363,18 +363,23 @@ foreach ($pdaApplication in $pdaApplications) {
|
||||
}
|
||||
}
|
||||
|
||||
# 海外 PDA 当前开放真实拆柜收货与托盘管理;其余后端虽已存在,前端接入前必须保持禁用。
|
||||
# 海外 PDA 已开放拆柜收货、库内、换标和现场查询真实作业。
|
||||
$overseasWorkbenchPath = Join-Path $workspaceRoot 'apps/pda-overseas-uniapp/src/data/overseas-workbench.js'
|
||||
$overseasWorkbench = Read-RequiredText $overseasWorkbenchPath
|
||||
Assert-Matches $overseasWorkbench "(?s)name:\s*'拆柜收货'.{0,300}?available:\s*true" 'Overseas PDA 拆柜收货开关'
|
||||
Assert-Matches $overseasWorkbench "(?s)name:\s*'托盘管理'.{0,300}?available:\s*true.{0,200}?route:\s*'/pages/pallet/pallet'" 'Overseas PDA 托盘管理开关'
|
||||
foreach ($disabledOperation in @(
|
||||
'库存盘点', '商品移库',
|
||||
'换标收货', '换标执行', '换标复核',
|
||||
'库存查询', '库位查询', '托盘查询'
|
||||
foreach ($enabledOperation in @(
|
||||
@{ Name = '库存盘点'; Route = '/pages/stocktake/stocktake' },
|
||||
@{ Name = '商品移库'; Route = '/pages/move/move' },
|
||||
@{ Name = '换标收货'; Route = '/pages/relabel/relabel\?op=relabelReceive' },
|
||||
@{ Name = '换标执行'; Route = '/pages/relabel/relabel\?op=relabelExecute' },
|
||||
@{ Name = '换标复核'; Route = '/pages/relabel/relabel\?op=relabelReview' },
|
||||
@{ Name = '库存查询'; Route = '/pages/query/query\?op=inventoryQuery' },
|
||||
@{ Name = '库位查询'; Route = '/pages/query/query\?op=locationQuery' },
|
||||
@{ Name = '托盘查询'; Route = '/pages/query/query\?op=palletQuery' }
|
||||
)) {
|
||||
$disabledPattern = "(?s)name:\s*'$([regex]::Escape($disabledOperation))'.{0,300}?available:\s*false"
|
||||
Assert-Matches $overseasWorkbench $disabledPattern "Overseas PDA 禁用入口 $disabledOperation"
|
||||
$enabledPattern = "(?s)name:\s*'$([regex]::Escape($enabledOperation.Name))'.{0,300}?available:\s*true.{0,200}?route:\s*'$($enabledOperation.Route)'"
|
||||
Assert-Matches $overseasWorkbench $enabledPattern "Overseas PDA 开放入口 $($enabledOperation.Name)"
|
||||
}
|
||||
|
||||
# 文档声明为正式开放的 Client 海外仓页面必须仍是 active 路由。
|
||||
@@ -415,7 +420,7 @@ Assert-Contains $overseasCompose 'LASTLEG_SECURITY_SERVICE_URL' 'Lastleg 安全
|
||||
Assert-NotContains $serviceJenkinsfile "'overseas-security-service'" '独立海外安全服务构建入口'
|
||||
|
||||
# 高变化语义必须在最近指引与服务契约中保持一致。
|
||||
Assert-Contains $adminAgents '海外模块内部快速索引(四期/五期)' 'Admin 四五期范围边界'
|
||||
Assert-Contains $adminAgents '海外模块内部快速索引(海外仓全流程)' 'Admin 海外仓全流程范围'
|
||||
Assert-Contains $pdaAgents 'VITE_H5_API_BASE_URL' '国内 PDA H5 运行配置'
|
||||
Assert-Contains $overseasPdaAgents 'tests/app-icons.test.mjs' '海外 PDA 图标合同'
|
||||
Assert-Contains $backendAgents 'RestClientTraceInterceptor.java' '后端同步 Trace 入口'
|
||||
@@ -431,29 +436,29 @@ Assert-Contains $adminOceanBookingApi '/link-candidates' 'Admin 订舱关联候
|
||||
Assert-Contains $adminOceanBookingView "label: '订舱柜量'" 'Admin 订舱柜量文案'
|
||||
Assert-Contains $adminOceanBookingViewTest 'without detail N+1' 'Admin 订舱列表无详情扇出合同'
|
||||
|
||||
# 五期海外出库、派送、异常和回访必须从根路由贯通到 Admin 与 Lastleg;PDA 后端存在不等于设备端已挂载。
|
||||
# 海外出库、派送、异常和回访必须从根路由贯通到 Admin 与 Lastleg;PDA 后端存在不等于设备端已挂载。
|
||||
$adminOverseasRoutes = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/overseas-warehouse/routes.ts')
|
||||
$lastlegAgents = Read-RequiredText (Join-Path $backendRoot 'lastleg-service/AGENTS.md')
|
||||
foreach ($phase5OverseasRoute in @(
|
||||
foreach ($overseasFulfillmentRoute in @(
|
||||
'overseas-warehouse/outbounds',
|
||||
'overseas-warehouse/delivery-orders',
|
||||
'overseas-warehouse/exception-orders',
|
||||
'overseas-warehouse/customer-visits'
|
||||
)) {
|
||||
Assert-Contains $adminOverseasRoutes "path: '$phase5OverseasRoute'" "Admin 五期正式路由 $phase5OverseasRoute"
|
||||
Assert-Contains $adminOverseasRoutes "path: '$overseasFulfillmentRoute'" "Admin 海外履约正式路由 $overseasFulfillmentRoute"
|
||||
}
|
||||
foreach ($phase5RootEntry in @(
|
||||
foreach ($overseasFulfillmentRootEntry in @(
|
||||
'OutboundsView.vue', 'DeliveryOrdersView.vue', 'ExceptionOrdersView.vue', 'CustomerVisitsView.vue'
|
||||
)) {
|
||||
Assert-Contains $rootAgents $phase5RootEntry '根正式五期业务路由'
|
||||
Assert-Contains $rootAgents $overseasFulfillmentRootEntry '根海外履约正式业务路由'
|
||||
}
|
||||
Assert-Contains $adminAgents '五期海外排车、出库与派送' 'Admin 五期出库派送入口'
|
||||
Assert-Contains $adminAgents '五期海外异常退回与客户回访' 'Admin 五期异常回访入口'
|
||||
Assert-Contains $backendAgents '五期海外排车执行、PDA 出库、派送与一件代发' '后端五期出库派送入口'
|
||||
Assert-Contains $backendAgents '五期一件代发 SKU/箱唛与暂扣跨区协同' '后端五期跨区协同入口'
|
||||
Assert-Contains $lastlegAgents '五期范围承接排车编辑/取消/发车' 'Lastleg 五期范围边界'
|
||||
Assert-Contains $overseasPdaAgents '五期排车拣货与发车' '海外 PDA 五期排车入口'
|
||||
Assert-Contains $overseasPdaAgents '五期普通/一件代发出库' '海外 PDA 五期出库入口'
|
||||
Assert-Contains $adminAgents '海外排车、出库与派送' 'Admin 海外出库派送入口'
|
||||
Assert-Contains $adminAgents '海外异常退回与客户回访' 'Admin 海外异常回访入口'
|
||||
Assert-Contains $backendAgents '海外排车执行、PDA 出库、派送与一件代发' '后端海外出库派送入口'
|
||||
Assert-Contains $backendAgents '一件代发 SKU/箱唛与暂扣跨区协同' '后端一件代发跨区协同入口'
|
||||
Assert-Contains $lastlegAgents '海外仓全流程连续覆盖' 'Lastleg 海外仓全流程范围'
|
||||
Assert-Contains $overseasPdaAgents '排车拣货与发车' '海外 PDA 排车入口'
|
||||
Assert-Contains $overseasPdaAgents '普通/一件代发出库' '海外 PDA 出库入口'
|
||||
|
||||
# Admin 仅对静态资源过期做一次受控恢复,不能吞掉普通路由错误或覆盖未保存编辑。
|
||||
$navigationRecovery = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/core/routing/navigationRecovery.ts')
|
||||
@@ -573,7 +578,7 @@ Assert-Contains $lastlegBusinessOpenApi '/api/platform/v1/last-leg/orders/contai
|
||||
Assert-Contains $lastlegBusinessOpenApi '/api/platform/v1/last-leg/inbound-plans/convert-container:' 'Lastleg 整柜计划转换契约'
|
||||
Assert-Contains $lastlegBusinessOpenApi 'sourceContainerBookingId' 'Lastleg 稳定来源柜身份'
|
||||
Assert-Contains $adminAgents '/orders/container-summaries' 'Admin 柜级摘要目标入口'
|
||||
Assert-Contains $rootAgents '只分页返回仍有入仓码待生成有效独立计划的柜' '根柜级计划待处理筛选'
|
||||
Assert-Contains $rootAgents '柜级入仓计划进度与转换' '根柜级计划入口'
|
||||
Assert-Contains $backendAgents '分页前排除全部入仓码均已生成有效独立计划的柜' '后端柜级计划待处理筛选'
|
||||
Assert-Contains $adminAgents '返回仍有待生成计划入仓码的柜' 'Admin 柜级计划待处理筛选'
|
||||
Assert-Contains $lastlegAgents '分页前排除全部入仓码均已生成有效独立计划的柜' 'Lastleg 柜级计划待处理筛选'
|
||||
@@ -888,11 +893,11 @@ $criticalPaths = @(
|
||||
'baiyue-firstleg/scripts/admin-test-env-modules/manual-pda-ready-contract.test.mjs',
|
||||
'baiyue-firstleg/scripts/admin-test-env-modules/manual-pda-batch-contract.test.mjs',
|
||||
'baiyue-firstleg/scripts/admin-test-env-modules/manual-loading-complete-contract.test.mjs',
|
||||
'docs/四期/船节点标准化契约.md',
|
||||
'docs/一期/一期头程订单闭环设计.md',
|
||||
'docs/二期/二期需求优化版-v2-测量计费入库主流程.md',
|
||||
'docs/海外仓业务/船节点标准化契约.md',
|
||||
'docs/订单与客户/订单与客户头程订单闭环设计.md',
|
||||
'docs/入库测量与计费/入库测量与计费需求优化版-v2-测量计费入库主流程.md',
|
||||
'docs/墨家设备测量上传接口说明-V3.md',
|
||||
'docs/三期/三期需求确认表.md',
|
||||
'docs/国内货柜履约/国内货柜履约需求确认表.md',
|
||||
'docs/统一订单主状态与操作契约.md',
|
||||
'docs/开发环境独立任务协作登记.md',
|
||||
'docs/独立任务协作登记.md',
|
||||
@@ -967,5 +972,5 @@ if ($failures.Count -gt 0) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "AGENTS 索引合同通过:$checked 项检查,覆盖快速表唯一性、部署单元、开发/测试任务登记隔离、前端模块、Client active 路由、客户中文批量下单、箱规算法与测量计费、全局智能装柜组分页、客户账单入仓码联查、导出退避任务重试、脚本目录治理、Finance 扩展 HTTP 模块、柜费跨客户原子分摊、海外五类费用跨区入池与直接已付隔离、企业联系信息与登录标识解耦、选填法人身份证独立上传、待审核订单状态优先、订单申报版本同步与渠道门禁、装柜证据内容哈希重放、业务编号与内部 ID 隔离、装柜清单生命周期与真实柜号后补、订舱级拖车与柜级证据、四/五期范围边界、四期柜级待处理计划筛选与范围漂移、仓库主档唯一权威、订舱候选与柜量语义、整柜出仓与船节点单调性、Gateway 覆盖、PDA 页面/运行配置/独立更新/语言预留/能力开关、人工分段验收链、Maven 模块、Lastleg 海外安全投影、同步 Trace、关键入口和排除目录。" -ForegroundColor Green
|
||||
Write-Host "AGENTS 索引合同通过:$checked 项检查,覆盖快速表唯一性、部署单元、开发/测试任务登记隔离、前端模块、Client active 路由、客户中文批量下单、箱规算法与测量计费、全局智能装柜组分页、客户账单入仓码联查、导出退避任务重试、脚本目录治理、Finance 扩展 HTTP 模块、柜费跨客户原子分摊、海外费用跨区入池与直接已付隔离、企业联系信息与登录标识解耦、选填法人身份证独立上传、待审核订单状态优先、订单申报版本同步与渠道门禁、装柜证据内容哈希重放、业务编号与内部 ID 隔离、装柜清单生命周期与真实柜号后补、订舱级拖车与柜级证据、海外仓全流程范围、柜级待处理计划筛选与范围漂移、仓库主档唯一权威、订舱候选与柜量语义、整柜出仓与船节点单调性、Gateway 覆盖、PDA 页面/运行配置/独立更新/语言预留/能力开关、人工分段验收链、Maven 模块、Lastleg 海外安全投影、同步 Trace、关键入口和排除目录。" -ForegroundColor Green
|
||||
exit 0
|
||||
|
||||
+5
-5
@@ -3,9 +3,9 @@
|
||||
## 职责与边界
|
||||
|
||||
- Mapper 只由 `BaiyueWarehouseApplication.java` 对 `com.baiyue.warehouse` 根包按 `@Mapper` 过滤扫描;新增或移动 Mapper 后从后端根运行 `node scripts/mapper-scan-contract.mjs`,不得增加子域扫描配置。
|
||||
- 负责国内收货仓及现有平台仓库主数据、地址/能力、往来单位、快递公司、平台公共供应商、国内整柜库存/出入库,以及托盘主数据、库位预占和占用。四期只在本模块扩展“海外仓合作仓”仓库类型及其公共主数据契约。
|
||||
- 负责国内收货仓及现有平台仓库主数据、地址/能力、往来单位、快递公司、平台公共供应商、国内整柜库存/出入库,以及托盘主数据、库位预占和占用。海外仓业务只在本模块扩展“海外仓合作仓”仓库类型及其公共主数据契约。
|
||||
- 入库单、测量和入库状态归 `firstleg/`;订单只通过内部接口校验并保存快照。
|
||||
- 四期海外接柜、卸货、海外入库、换标、产品、箱号、库存和库内作业属于海外区域服务,不得落入本国内 Warehouse Service;范围先读 `../../docs/四期/README.md`。
|
||||
- 海外接柜、卸货、入仓、换标、产品、箱号、库存、库内作业、出库和派送属于海外区域服务,不得落入本国内 Warehouse Service;全流程范围先读 `../../docs/海外仓业务/README.md`。
|
||||
- 公共资源统一 `tenant_id=0`,不得绑定客户或要求客户筛选。
|
||||
|
||||
## 模块入口
|
||||
@@ -16,7 +16,7 @@
|
||||
| FBA 目的仓公共主数据与 Admin 候选 | 权威表 `wh_warehouse` 的 `tenant_id=0 + warehouse_type=FBA` 记录;Admin 直接调用 `GET /api/platform/v1/warehouse-admin?warehouseType=FBA&status=ACTIVE&page=1&size=200`,权限 `platform:warehouse:list`,DTO `WarehouseSummaryResponse` 的 `id/warehouseCode/warehouseName/status/countryCode/primaryAddress.stateProvince` 分别作为 ID/编码/名称/状态/国家/区域 |
|
||||
| FBA 目的仓内部校验 | `InternalWarehouseController.java` 的 `GET /internal/warehouses/0/{warehouseId}/active`,使用内部服务令牌返回 `WarehouseDetailResponse`;调用方仍须校验 `warehouseType=FBA` 及 ID/编码/名称一致,不得把 `OVERSEAS` 实际收货仓或 `DOMESTIC` 中转仓当作 FBA 目的仓 |
|
||||
| 公共海外收货仓内部目录 | `controller/InternalWarehouseController.java` 的 `/internal/warehouses/shared-overseas`、`/shared-overseas/{warehouseId}`,响应 `dto/SharedOverseasWarehouseResponse.java`;只返回平台公共 `OVERSEAS + ACTIVE + RECEIVING` 仓,供海外资源服务按 warehouseId 引用 |
|
||||
| 四期海外仓合作仓类型 | 沿用仓库/地址/能力入口;同步核对仓库 DTO、OpenAPI、`src/main/resources/db/`、统一 SQL 及 Admin `modules/warehouses/`,并与普通海外仓语义明确区分 |
|
||||
| 海外仓合作仓类型 | 沿用仓库/地址/能力入口;同步核对仓库 DTO、OpenAPI、`src/main/resources/db/`、统一 SQL 及 Admin `modules/warehouses/`,并与普通海外仓语义明确区分 |
|
||||
| 往来单位/快递 | `controller/PlatformPartnerController.java`、`PlatformExpressCompanyController.java`、`service/PlatformPartnerQueryService.java`、`PartnerImportService.java` |
|
||||
| 托盘 | `controller/PlatformPalletController.java`、`service/PalletApplicationService.java` |
|
||||
| 库位与分配 | `controller/PlatformLocationController.java`、`InternalPalletLocationController.java`、`service/PalletLocationAllocationService.java` |
|
||||
@@ -33,7 +33,7 @@
|
||||
- 仓库 ID、类型、状态、地址和服务能力在本模块唯一维护;海外服务不得复制仓库主档。共享海外收货仓目录只接受内部服务令牌,列表和单项必须使用相同的 `tenant_id=0 + OVERSEAS + ACTIVE + RECEIVING` 判定。
|
||||
- 导入先全量校验再原子写入;调整 DTO、类型或容量规则时同步 Order/Firstleg 客户端和契约测试。
|
||||
|
||||
## 三期托盘、库位与 A0 规则
|
||||
## 国内货柜履约托盘、库位与 A0 规则
|
||||
|
||||
- 托盘是可复用标识和称重主数据,不参与 `AVAILABLE → RESERVED → OCCUPIED → AVAILABLE`;同一托盘主数据可被多条分配复用。
|
||||
- 普通库位保持单活动预占/占用;A0 是系统创建的共享临时溢出位,可承载多条占用,但每条库位占用仍必须通过 `reservation_id`、`occupancy_id` 独立识别。
|
||||
@@ -45,4 +45,4 @@
|
||||
|
||||
## 全量验收导航
|
||||
|
||||
本模块承载公共仓与主数据、托盘/库位预占占用、海外仓费用、PDA 拣货和资源结算协作;按本次验收链路读取对应阶段文档,核对实时库存、当前位置、可用量、原子结算和客户隔离。不扩展未确认的完整 WMS,设备真机事项不改变服务端资源契约。
|
||||
本模块承载公共仓与主数据、托盘/库位预占占用、海外仓费用、PDA 拣货和资源结算协作;按本次验收链路读取对应业务域文档,核对实时库存、当前位置、可用量、原子结算和客户隔离。不扩展未确认的完整 WMS,设备真机事项不改变服务端资源契约。
|
||||
|
||||
Reference in New Issue
Block a user