diff --git a/二期/二期前端联调报告-2026-08-04.md b/二期/二期前端联调报告-2026-08-04.md new file mode 100644 index 0000000..df9294c --- /dev/null +++ b/二期/二期前端联调报告-2026-08-04.md @@ -0,0 +1,566 @@ +# 二期入库测量、计费与入库前端联调报告 + +> 版本日期:2026-08-04 +> 后端仓库:`D:\baiyue-firstleg` +> 代码基线:`test-01` 分支,提交 `25d2a13` +> 使用对象:平台后台前端、商户端前端、第三方测量系统联调人员 + +## 一、联调结论 + +二期后端主流程、平台测量、OCR 重量、客服审核、FBA 计费、费用发布、入库完成、库位和托盘维护、客户只读查询、第三方测量回传接口均已形成代码闭环。 + +最终正常业务流程是: + +```text +订单生成入库单 + → 第三方或平台录入长宽高、箱规和件数 + → 上传称重图片并执行 OCR + → 人工修正并确认 OCR 重量 + → 保存测量草稿 + → 提交测量 + → 系统按托盘容量拆分托盘并立即占用库位 + → 客服审核测量数据 + → 审核通过后自动按 FBA 规则计费 + → 自动发布费用给客户 + → 自动完成入库 + → 客户只读查看费用和入库结果 +``` + +联调时必须遵守以下口径: + +- 客户不确认费用,前端不得设计或调用“客户确认费用”操作。 +- 测量提交后必须由客服审核;第三方回传测量数据不能直接审核、计费或完成入库。 +- 提交测量时已经完成托盘拆分和库位占用。客服驳回只退回测量修改,已占库位不释放。 +- 客服审核通过后,后端自动完成计费、内部确认、费用发布和入库,不需要前端依次点击计费、确认、发送。 +- 已发布费用可以调整;调整会生成新费用版本,但不会回退 `INBOUNDED` 入库状态。 +- 测量审核失败默认不允许异常放行。Nacos 配置 `firstleg.workflow.allow-rejected-measurement=false`,仅开启配置且有专门权限时才能放行。 +- 当前只实现 FBA 目的仓计费,不包含应收应付、对账、核销、发票、利润、PDA 和完整 WMS。 + +## 二、服务依赖、入口与文档 + +### 2.1 前端统一入口 + +本地默认网关: + +```text +http://127.0.0.1:9000 +``` + +聚合 Swagger: + +```text +http://127.0.0.1:9000/webjars/swagger-ui/index.html +``` + +Swagger 分组: + +- `firstleg-platform`:平台入库、测量、审核、计费及客户只读接口。 +- `firstleg-measurement-openapi`:第三方测量回传接口。 +- `warehouse-platform`:库位、托盘等仓储基础资料。 +- `file-platform`:测量图片上传、识别和读取。 +- `integration`:OCR 内部能力,前端一般不直接调用。 + +### 2.2 联调相关服务及默认端口 + +| 顺序 | 服务 | 默认端口 | 用途 | +|---:|---|---:|---| +| 1 | Nacos、MySQL、Redis | 环境配置 | 注册配置、数据和会话 | +| 2 | `iam-service` | 9201 | 用户、角色、按钮权限 | +| 3 | `auth-service` | 9100 | 登录、Token 刷新和退出 | +| 4 | `warehouse-service` | 9205 | 仓库、库位、托盘及占用 | +| 5 | `integration-service` | 9210 | OCR 识别能力 | +| 6 | `file-service` | 9206 | 图片上传、持久化和 OCR 聚合 | +| 7 | `firstleg-service` | 9202 | 入库、测量、审核和计费主流程 | +| 8 | `gateway` | 9000 | 前端统一入口和聚合文档 | + +若登录或订单前置流程依赖其他一期服务,应在 Firstleg 前启动。前端业务请求统一走网关,不要将各服务端口写死到页面。 + +## 三、全局请求与响应约定 + +### 3.1 平台和商户请求头 + +```http +Authorization: Bearer +Content-Type: application/json +``` + +平台代目标企业操作的接口还必须传 `tenantId` 查询参数。后端同时校验登录应用类型、操作者数据范围、目标租户、仓库和资源归属;不能只依靠前端隐藏按钮实现隔离。 + +所有有副作用且要求幂等的接口必须传: + +```http +Idempotency-Key: <每次业务操作唯一且重试保持不变的字符串> +``` + +网络超时重试时复用原键;用户重新发起一笔新操作时生成新键。 + +### 3.2 统一响应 + +```json +{ + "code": "00000", + "message": "成功", + "data": {}, + "requestId": "...", + "traceId": "...", + "timestamp": "2026-08-04T02:38:31Z" +} +``` + +- 前端必须以业务 `code` 判断成功,不能只判断 HTTP 200。 +- 分页数据结构为 `data.records`、`data.page`、`data.size`、`data.total`、`data.pages`。 +- Long 类型 ID 已统一序列化为 JSON 字符串,前端必须按字符串保存和传递,不要转换成 JavaScript `Number`。 +- 401 统一跳转登录或触发刷新 Token;403 显示无权限;409 通常表示重复、状态冲突、乐观锁过期或资源已被占用,页面应刷新详情后再操作。 +- 提交按钮在请求完成前禁用,配合 `Idempotency-Key` 防止重复提交。 + +## 四、状态、页签和按钮规则 + +### 4.1 入库流程状态 `processStatus` + +| 状态 | 中文含义 | 页面主要操作 | +|---|---|---| +| `WAIT_MEASURE` | 待测量 | 进入测量、录入草稿 | +| `MEASURING` | 测量中 | 编辑草稿、OCR、人工修正、提交 | +| `MEASURE_PENDING` | 待客服审核 | 客服审核通过或驳回 | +| `MEASURE_REJECTED` | 测量驳回 | 修改并重新提交;特殊情况下异常放行 | +| `WAIT_PRICING` | 待计费 | 正常流程由后台自动推进 | +| `FEE_CONFIRMING` | 费用处理中 | 正常流程由后台自动推进 | +| `INBOUNDING` | 入库处理中 | 正常流程由后台自动推进 | +| `INBOUNDED` | 已入库 | 查看费用、库位、文件;有权限时调整费用 | +| `VOIDED` | 已作废 | 只读 | + +页面不要根据中文文案判断状态,必须使用状态码。 + +### 4.2 列表页签 `tab` + +| 页签参数 | 后端包含状态 | +|---|---| +| `WAIT_MEASURE` | `WAIT_MEASURE`、`MEASURING`、`MEASURE_REJECTED` | +| `WAIT_REVIEW` | `MEASURE_PENDING` | +| `WAIT_FEE_SEND` | `WAIT_PRICING`、`FEE_CONFIRMING`、`INBOUNDING` | +| `INBOUNDED` | `INBOUNDED` | + +### 4.3 主流程状态变化 + +| 业务动作 | 结果状态 | 库位事实 | +|---|---|---| +| 保存测量草稿 | `MEASURING` | 未占用 | +| 提交测量 | `MEASURE_PENDING` | 已拆托并确认 `OCCUPIED` | +| 客服驳回 | `MEASURE_REJECTED` | 保持占用 | +| 客服通过 | 自动经过计费和入库中间态 | 保持占用 | +| 自动闭环完成 | `INBOUNDED`,费用 `SENT` | 保持占用 | +| 调整已发布费用 | 仍为 `INBOUNDED`;新费用 `SENT`,旧费用 `SUPERSEDED` | 保持占用 | + +## 五、平台入库单列表与详情 + +| 方法 | 路径 | 权限 | +|---|---|---| +| GET | `/api/platform/inbounds` | `platform:firstleg:inbound:list` | +| GET | `/api/platform/inbounds/{inboundId}` | `platform:firstleg:inbound:view` | + +共同参数:`tenantId`。列表支持: + +```text +inboundCode, orderNo, processStatus, orderStatus, measureReviewStatus, +feeStatus, feeReviewStatus, tab, expectedArrivalFrom, expectedArrivalTo, +customerId, customerName, companyId, warehouseId, deliveryWarehouseId, +destinationCode, holdFlag, page, size +``` + +列表已覆盖前端主要展示字段:入仓码、订单号、订单/流程/测量审核/费用状态、客户、公司、仓库、交货仓、FBA 目的仓、扣货、箱数、实重、净重、实测体积、材积重、计费重量、计费体积、托盘数、`locationCodes`、测量和入库完成时间、快递公司和快递单号、配载、集装箱、柜号、船名、航次、开船和到港时间、创建和更新时间。 + +详情在此基础上还返回:`publicRemark`、`palletLocations`、`files`、`internalNotes`、`statusHistory`。 + +## 六、平台测量与 OCR + +接口前缀: + +```text +/api/platform/v1/first-leg/inbounds/{inboundId} +``` + +| 方法 | 子路径 | 权限 | 说明 | +|---|---|---|---| +| GET | `/measurement` | `platform:firstleg:inbound:measure:view` | 获取测量、明细和 OCR 结果 | +| PUT | `/measurement/draft` | `platform:firstleg:inbound:measure:edit` | 保存或更新草稿 | +| POST | `/measurement/submit` | `platform:firstleg:inbound:measure:submit` | 提交并拆托、占库位 | +| POST | `/weight-results` | `platform:firstleg:inbound:weight:edit` | 新增 OCR 结果 | +| PUT | `/weight-results/{resultId}` | `platform:firstleg:inbound:weight:edit` | 人工修正重量 | +| POST | `/weight-results/{resultId}/confirm` | `platform:firstleg:inbound:weight:confirm` | 确认最终重量 | + +以上接口均带 `tenantId`;写接口按 Swagger 要求携带 `Idempotency-Key`。 + +### 6.1 保存测量草稿 + +```json +{ + "measurementId": "已有草稿时传,首次可为空", + "version": 0, + "originalDocumentNo": "原单据号", + "originalGrossWeightKg": 100.000, + "originalVolumeCbm": 2.5000, + "actualGoodsName": "实际品名", + "items": [ + { + "itemNo": "ITEM-001", + "boxSpecification": "60*40*50cm", + "packageCount": 10, + "lengthCm": 60.000, + "widthCm": 40.000, + "heightCm": 50.000, + "unitWeightKg": 12.500, + "intelligentImageFileId": "图片文件ID", + "overLimitWarning": "", + "outerLengthCm": 61.000, + "outerWidthCm": 41.000, + "outerHeightCm": 51.000, + "remark": "" + } + ] +} +``` + +注意:汇总体积、材积重等计算字段由后端计算,前端不要传入或自行覆盖。`items` 至少一条,尺寸和单件重量必须大于 0,`boxSpecification` 必填。 + +提交测量请求: + +```json +{ + "measurementId": "190000000000000001", + "version": 1 +} +``` + +`version` 是乐观锁版本。每次保存、修正或确认成功后都应使用响应中的新版本;409 后重新拉取详情,不能用旧版本强行重试。 + +### 6.2 OCR 重量 + +图片接口: + +| 方法 | 路径 | 权限 | +|---|---|---| +| POST | `/api/platform/v1/files/measurements/{tenantId}/{inboundId}` | `platform:firstleg:inbound:weight:edit` | +| POST | `/api/platform/v1/files/measurements/{tenantId}/{inboundId}/{fileId}/recognize` | `platform:firstleg:inbound:weight:edit` | +| GET | `/api/platform/v1/files/measurements/{tenantId}/{inboundId}/{fileId}` | `platform:firstleg:inbound:measure:view` | + +支持 JPG、PNG、BMP,单文件最大 5 MB。建议交互顺序:上传图片 → 调用识别 → 展示 OCR 原值、单位、置信度 → 用户按需修正 → 确认重量。 + +人工修正请求: + +```json +{ + "version": 0, + "adjustedWeightValue": 12.500, + "adjustedWeightUnit": "KG", + "adjustmentReason": "OCR 小数点识别错误" +} +``` + +确认请求: + +```json +{ + "version": 1 +} +``` + +测量详情会返回 OCR 原文、原重量和单位、置信度、人工修正值和原因、最终 KG、识别状态、修正/确认人员与时间、版本。多张秤图的最终重量由业务服务汇总,前端只负责展示和人工确认,不自行写汇总重量。 + +## 七、客服测量审核 + +| 方法 | 路径 | 权限 | +|---|---|---| +| POST | `/api/platform/v1/inbounds/{inboundId}/measurements/approve` | `firstleg:inbound:measurement-review` | +| POST | `/api/platform/v1/inbounds/{inboundId}/measurements/reject` | `firstleg:inbound:measurement-review` | +| POST | `/api/platform/v1/inbounds/{inboundId}/measurements/exception-release` | `firstleg:inbound:exception-release` | +| GET | `/api/platform/v1/inbounds/{inboundId}/measurement-review-history` | `firstleg:inbound:measurement-review-history` | + +写接口必须传 `tenantId` 和 `Idempotency-Key`。通过/驳回请求: + +```json +{ + "measurementId": "190000000000000001", + "expectedVersion": 2, + "reason": "审核意见,通过时可为空,驳回时按业务填写" +} +``` + +异常放行请求的 `reason` 必填: + +```json +{ + "measurementId": "190000000000000001", + "expectedVersion": 3, + "reason": "负责人审批后的异常放行原因" +} +``` + +审核通过请求成功即表示后端自动闭环已执行完成或可幂等续跑。前端应重新查询入库详情和最新费用,不要再自动串行调用 `/calculate`、`/confirm`、`/send`。 + +## 八、FBA 计费与费用调整 + +接口前缀: + +```text +/api/platform/v1/inbounds/{inboundId}/fees +``` + +| 方法 | 子路径 | 权限 | 前端用途 | +|---|---|---|---| +| GET | `/latest` | `firstleg:fba-pricing:view` | 查询最新费用快照 | +| PUT | `/published` | `firstleg:fba-pricing:adjust` | 调整已发布费用 | +| POST | `/calculate` | `firstleg:fba-pricing:calculate` | 异常恢复/人工补偿,不是正常主流程按钮 | +| POST | `/confirm` | `firstleg:fba-pricing:confirm` | 异常恢复/人工补偿 | +| POST | `/send` | `firstleg:inbound:send-fee` | 异常恢复/人工补偿 | + +调整已发布费用: + +```json +{ + "subtotalAmount": 1000.00, + "additionalAmount": 50.00, + "reason": "经审批调整附加费" +} +``` + +计费规则: + +- 包税、按 KG:`计费数量 = max(实重, 实测 CBM × 换算系数)`。 +- 不包税、按 CBM:`计费数量 = max(实测 CBM, 实重 ÷ 换算系数)`。 +- 换算系数、精度、舍入方式、币种和价格均来自客户规则及船司费率版本,前端不能写死。 + +FBA 基础资料: + +```text +GET /api/platform/v1/fba-pricing/customer-rules +GET /api/platform/v1/fba-pricing/customer-rules/{id} +POST /api/platform/v1/fba-pricing/customer-rules/versions +DELETE /api/platform/v1/fba-pricing/customer-rules/{id} + +GET /api/platform/v1/fba-pricing/carrier-rates +GET /api/platform/v1/fba-pricing/carrier-rates/{id} +POST /api/platform/v1/fba-pricing/carrier-rates/versions +DELETE /api/platform/v1/fba-pricing/carrier-rates/{id} +``` + +规则采用版本化新增,历史订单费用使用当时快照,不应被新版本覆盖。 + +## 九、库位管理 + +| 方法 | 路径 | 权限 | +|---|---|---| +| GET | `/api/platform/v1/locations` | `platform:location:list` | +| GET | `/api/platform/v1/locations/{id}` | `platform:location:list` | +| GET | `/api/platform/v1/locations/available` | `platform:location:list` | +| POST | `/api/platform/v1/locations` | `platform:location:create` | +| PUT | `/api/platform/v1/locations/{id}` | `platform:location:edit` | +| PATCH | `/api/platform/v1/locations/{id}/status` | `platform:location:status` | +| DELETE | `/api/platform/v1/locations/{id}` | `platform:location:delete` | + +分页条件:`tenantId`、`warehouseId`(必填)、`zoneCode`、`locationCode`、`locationType`、`status`、`page`、`size`。 + +库位类型:`SHELF`、`PALLET`、`FLOOR`、`FLOW_RACK`。查询状态:`AVAILABLE`、`RESERVED`、`OCCUPIED`、`DISABLED`。 + +新增/编辑请求: + +```json +{ + "warehouseId": "190000000000000002", + "zoneCode": "A", + "zoneName": "A库区", + "locationCode": "A-001", + "locationName": "A区001库位", + "locationType": "PALLET", + "maxWeightKg": 1000.000, + "status": "AVAILABLE", + "remark": "", + "version": 0 +} +``` + +启停仅允许 `AVAILABLE` 或 `DISABLED`,请求为: + +```json +{ + "status": "DISABLED", + "version": 1 +} +``` + +活动预占或已占用库位不能停用;删除要求库位已停用、无活动预占/占用,并在查询参数传 `warehouseId`、`version`。无普通空闲库位时,系统可使用本租户、本仓库的共享溢出位 `A0`,前端只展示后端结果。 + +## 十、托盘管理 + +| 方法 | 路径 | 权限 | +|---|---|---| +| GET | `/api/platform/v1/pallets` | `platform:pallet:list` | +| GET | `/api/platform/v1/pallets/{id}` | `platform:pallet:list` | +| POST | `/api/platform/v1/pallets` | `platform:pallet:create` | +| PUT | `/api/platform/v1/pallets/{id}` | `platform:pallet:edit` | +| DELETE | `/api/platform/v1/pallets/{id}` | `platform:pallet:delete` | + +分页条件:`tenantId`、`warehouseId`、`palletCode`、`palletType`、`page`、`size`。新增/编辑请求: + +```json +{ + "warehouseId": "190000000000000002", + "palletCode": "TP-001", + "palletType": "木托盘", + "capacityCbm": 2.000000, + "remark": "标准托盘" +} +``` + +当前平台托盘 DTO 只返回主数据,不暴露内部 `AVAILABLE / RESERVED / OCCUPIED` 状态;也没有单独的托盘启停或状态筛选 API。页面本期只实现查询、新增、编辑、删除。已预占或占用托盘删除会被后端拒绝。这是当前明确的接口边界,不要自行猜测状态路径。 + +## 十一、商户端只读接口 + +```http +GET /api/merchant/v1/first-leg/orders/{orderId}/inbound-billing +``` + +权限:`merchant:order:view`。 + +返回 `inbound` 和 `latestFee`。仅允许商户身份查询本租户订单,且只有客服审核通过并已发布的费用可见。商户端只展示:入库结果、测量汇总、托盘/库位结果和最新费用,不提供费用确认、测量审核、费用修改操作。 + +## 十二、第三方测量开放接口 + +| 方法 | 路径 | 说明 | +|---|---|---| +| POST | `/openapi/v1/inbound-measurements` | 一次提交一条箱规明细和一张对应图片 | +| GET | `/openapi/v1/inbound-measurements/events/{eventNo}` | 查询事件处理结果 | + +请求头: + +```http +X-App-Key: <调用方应用键> +X-Timestamp: <时间戳> +X-Nonce: <随机串> +X-Signature: <签名> +Idempotency-Key: <幂等键> +``` + +POST 使用 `multipart/form-data`,包含: + +```text +data:原始 UTF-8 JSON 文本 +image:该条箱规唯一对应的 JPG/PNG/BMP 图片,最大 5 MB +``` + +`data` 示例: + +```json +{ + "eventNo": "M202608010001", + "externalDetailNo": "BOX-20260801-0001", + "warehouseCode": "CN-YW-01", + "inboundCode": "RK202608010001", + "inboundDetailId": "190000000000000001", + "deviceCode": "DEVICE-01", + "measuredAt": "2026-08-01T10:30:00+08:00", + "boxSpecification": "60*40*50cm", + "packageCount": 10, + "lengthCm": 60.000, + "widthCm": 40.000, + "heightCm": 50.000, + "unitWeightKg": 12.500, + "remark": "" +} +``` + +绑定和签名规则: + +- 后端先按 `inboundCode` 匹配入库单,再使用 `inboundDetailId` 通过 Order 内部接口校验该货物明细确实属于入库单关联订单;第三方不能自由创建来源不明的明细。 +- 一次请求只生成一个测量批次、一条测量明细和一个文件中心图片引用。 +- `externalDetailNo` 标识第三方物理测量明细;同一订单明细和相同第三方明细号不能重复绑定。 +- `eventNo` 在同一 `appKey` 下唯一;相同事件号或幂等键重试时,测量 JSON 和图片内容必须完全一致。 +- 签名 body 为:`data原文 + LF + image文件内容SHA-256小写十六进制`;再按既有六行 HMAC 规则计算 `X-Signature`。 +- `measuredAt` 必须带时区;长宽高大于 0;`packageCount` 为 1 至 1,000,000;`boxSpecification` 必填。 +- 图片由 File 服务验证真实格式并持久化;尺寸测量图片不触发重量 OCR。测量落库失败时会调用文件中心补偿清理。 +- 接口只接收测量数据,不推进客服审核、计费、库位或入库状态。 + +成功响应的 `data` 返回:`eventNo`、`inboundCode`、`inboundDetailId`、`measurementId`、`measurementItemId`、`imageFileId`、`accepted`、`status`、`receivedAt`。所有 ID 按字符串处理。 + +## 十三、推荐前端联调顺序 + +1. 验证平台登录、刷新 Token、退出、过期会话、401 和 403。 +2. 给测试角色同步二期权限,确认按钮显示与后端权限码一致。 +3. 初始化启用仓库、至少一个托盘、足量可用库位、客户计费规则和船司 FBA 费率。 +4. 创建或准备一条已进入待测量状态的真实入库单。 +5. 调用入库列表和详情,核对 Long ID 均按字符串处理。 +6. 上传秤图、调用 OCR、人工修正和确认重量。 +7. 保存测量草稿,刷新页面后核对草稿和版本回显。 +8. 提交测量,验证状态变为 `MEASURE_PENDING`,托盘和库位结果已生成。 +9. 先走一次驳回 → 修改 → 重新提交,验证旧页面版本提交被拒绝。 +10. 客服审核通过,随后重新查询详情和 `/fees/latest`,验证自动变为 `INBOUNDED / SENT`。 +11. 商户账号查询 `inbound-billing`,验证只读数据、跨租户 403/404 和未发布费用不可见。 +12. 调整一次已发布费用,验证新版本可见、旧版本被替代且入库状态不回退。 +13. 验证重复点击使用同一幂等键不会重复计费、重复托盘或重复占位。 +14. 验证无权限按钮、直接构造越权请求、空态、加载态、异常提示和移动端展示。 + +## 十四、测试数据准备清单 + +- 平台管理员账号:具备入库查看、测量、OCR、审核、计费查看/调整、库位和托盘权限。 +- 客服账号:具备测量审核权限,但不授予异常放行权限,用于验证按钮隔离。 +- 商户账号:绑定独立租户,只具备本企业订单查看权限。 +- 两个不同租户及各自仓库,用于跨企业越权验证。 +- 一个启用仓库、至少 2 个容量明确的托盘、至少 2 个 `AVAILABLE` 库位。 +- 一套生效中的客户 FBA 计费规则和匹配的船司费率,币种、渠道、目的仓、有效期完整。 +- 一条待测量入库单、一条已发布费用入库单。 +- JPG/PNG/BMP 各一张有效秤图,以及超 5 MB、伪造扩展名和无法识别图片各一张。 +- 第三方测量应用的 AppKey、签名密钥、绑定租户和仓库编码。 + +## 十五、本次回归测试结果 + +2026-08-04 11:42(Asia/Shanghai)在最终代码上重新执行: + +```powershell +D:\apache-maven-3.8.8\bin\mvn.cmd clean test -pl order,firstleg,warehouse,file,integration-service,gateway,iam -am +``` + +结果:13 个 Reactor 模块全部 `SUCCESS`,`BUILD SUCCESS`,耗时约 1 分 5 秒。 + +| 模块 | 测试数 | Failures | Errors | Skipped | +|---|---:|---:|---:|---:| +| common-core | 3 | 0 | 0 | 0 | +| common-web | 10 | 0 | 0 | 0 | +| common-security | 6 | 0 | 0 | 0 | +| common-openapi | 1 | 0 | 0 | 0 | +| common-audit | 0 | 0 | 0 | 0 | +| gateway | 10 | 0 | 0 | 0 | +| iam | 79 | 0 | 0 | 0 | +| order | 30 | 0 | 0 | 0 | +| firstleg | 74 | 0 | 0 | 0 | +| warehouse | 68 | 0 | 0 | 0 | +| file | 23 | 0 | 0 | 0 | +| integration-service | 8 | 0 | 0 | 0 | +| **合计** | **312** | **0** | **0** | **0** | + +重点自动化覆盖: + +- 统一响应、异常映射、Long ID 字符串序列化和权限认证。 +- 测量草稿、OCR 修正/确认、提交及乐观锁。 +- 测量提交时托盘拆分、库位预占/确认、失败回滚、幂等重试。 +- 客服驳回、通过后自动计费/发布/入库、中断续跑。 +- FBA 计费规则选择、已发布费用调整和状态不回退。 +- 客户只见本租户已发布费用,平台/商户身份及跨租户隔离。 +- 第三方签名、防重放、事件幂等、仓库范围及不越权推进状态。 +- 第三方一条箱规对应一条订单货物明细和一张图片、重复绑定拦截、图片补偿清理。 +- 库位/托盘主数据约束、A0 共享溢出及活动占用唯一性契约。 +- 图片上传类型、大小、真实图片校验、下载三字段隔离和 OCR 聚合。 +- Gateway 路由以及 OpenAPI、Application、Nacos YAML 解析。 + +测试日志中的模拟数据库不可用、文件写入失败等异常堆栈,是异常映射测试主动构造的场景;对应测试均通过,不是构建失败。 + +## 十六、尚需在线环境验证与当前遗留 + +本次回归没有启动服务、没有占用 `9000/9100/920x` 默认端口,也没有连接或修改 MySQL。以下内容必须在测试环境由后端与前端共同验证后,才能判定上线验收通过: + +1. 二期 SQL 在真实 MySQL 的建表/迁移执行,以及历史数据与唯一约束冲突检查。 +2. Gateway、Nacos、Redis、MySQL、服务间令牌和各微服务的真实 HTTP 联通。 +3. OCR 引擎对现场秤图的识别准确率、超时和失败重试。 +4. 并发提交、并发抢托盘/库位、A0 多托盘共享在真实数据库事务下的压力验证。 +5. 浏览器端完整登录、刷新 Token、权限按钮、403、跨租户、空态、加载态和重复提交体验。 +6. 当前托盘管理没有独立启停和状态筛选接口;本期页面只做查询、新增、编辑、删除。 + +因此当前结论是:**代码编译及 312 项单元/契约测试全部通过,可以进入前后端测试环境联调;真实数据库迁移和在线 HTTP 业务验收尚未执行。** diff --git a/二期/第三方入仓测量接口联调说明-2026-08-04.md b/二期/第三方入仓测量接口联调说明-2026-08-04.md new file mode 100644 index 0000000..10b744b --- /dev/null +++ b/二期/第三方入仓测量接口联调说明-2026-08-04.md @@ -0,0 +1,193 @@ +# 第三方入仓测量接口联调说明 + +> 版本:2026-08-04 +> 网关基地址:`http://127.0.0.1:9000`(测试环境替换为实际网关域名) + +## 一、接口用途 + +第三方测量设备每次回传一条箱规测量数据和一张对应图片。后端按入仓码匹配入库单,并校验指定货物明细属于该入库单关联订单,然后形成一条测量明细。 + +接口只保存测量结果,不执行客服审核、计费、托盘拆分、库位占用或完成入库。 + +## 二、提交接口 + +```http +POST /openapi/v1/inbound-measurements +Content-Type: multipart/form-data +``` + +请求头: + +```http +X-App-Key: <第三方应用Key> +X-Timestamp: +X-Nonce: <本次请求唯一随机串> +X-Signature: +Idempotency-Key: <本次业务操作幂等键> +``` + +multipart 内容: + +| part 名称 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `data` | UTF-8 JSON 文本 | 是 | 一条箱规测量数据,服务端使用原文验签 | +| `image` | 二进制文件 | 是 | 该条明细唯一对应图片;JPG、PNG、BMP,最大 5 MB | + +`data` 示例: + +```json +{ + "eventNo": "M202608040001", + "externalDetailNo": "BOX-20260804-0001", + "warehouseCode": "CN-YW-01", + "inboundCode": "ZBYHK2604100412", + "inboundDetailId": "190000000000000001", + "deviceCode": "DEVICE-01", + "measuredAt": "2026-08-04T10:30:00+08:00", + "boxSpecification": "40*30*47cm", + "packageCount": 50, + "lengthCm": 40.000, + "widthCm": 30.000, + "heightCm": 47.000, + "unitWeightKg": 20.000, + "remark": "" +} +``` + +字段说明: + +| 字段 | 必填 | 规则 | +|---|---|---| +| `eventNo` | 是 | 同一 AppKey 下唯一;1~128 位字母、数字、点、下划线、冒号或横线 | +| `externalDetailNo` | 是 | 第三方物理测量明细唯一号;重复绑定会被拒绝 | +| `warehouseCode` | 是 | 必须属于该 AppKey 预先绑定的仓库 | +| `inboundCode` | 是 | 柏悦入仓码,后端据此匹配入库单 | +| `inboundDetailId` | 是 | 柏悦提供的订单货物明细 ID;必须属于该入库单关联订单 | +| `deviceCode` | 是 | 测量设备编码,最长 64 位 | +| `measuredAt` | 是 | ISO 8601 时间且必须包含时区 | +| `boxSpecification` | 是 | 箱规显示文本,最长 64 位 | +| `packageCount` | 是 | 本箱规箱数,1~1,000,000 | +| `lengthCm` | 是 | 单箱长度,单位厘米且大于 0 | +| `widthCm` | 是 | 单箱宽度,单位厘米且大于 0 | +| `heightCm` | 是 | 单箱高度,单位厘米且大于 0 | +| `unitWeightKg` | 否 | 单箱重量,单位 KG且大于 0;重量 OCR 流程可另行确认 | +| `remark` | 否 | 最长 500 字符 | + +## 三、签名算法 + +先计算图片内容摘要: + +```text +imageSha256 = SHA256(image原始字节).lowerHex +``` + +构造签名 body,`LF` 是单个换行符 `\n`: + +```text +signatureBody = data原始UTF-8文本 + LF + imageSha256 +bodySha256 = SHA256(signatureBody的UTF-8字节).lowerHex +``` + +构造六行 canonical string,末尾不追加换行: + +```text +POST +/openapi/v1/inbound-measurements + + + + +``` + +最终签名: + +```text +X-Signature = HMAC-SHA256(appSecret, canonicalString).lowerHex +``` + +`data` 中任何空格、换行或字段顺序变化都会改变签名。计算签名后必须发送完全相同的 `data` 原文和图片字节。 + +## 四、成功响应 + +```json +{ + "code": "200", + "message": "success", + "data": { + "eventNo": "M202608040001", + "inboundCode": "ZBYHK2604100412", + "inboundDetailId": "190000000000000001", + "measurementId": "190000000000000010", + "measurementItemId": "190000000000000020", + "imageFileId": "190000000000000030", + "accepted": true, + "status": "RECEIVED", + "receivedAt": "2026-08-04T02:30:00Z" + }, + "requestId": "...", + "traceId": "...", + "timestamp": "2026-08-04T02:30:01Z" +} +``` + +所有 ID 都是字符串,调用方不得转换为 JavaScript Number。 + +## 五、事件查询 + +```http +GET /openapi/v1/inbound-measurements/events/{eventNo} +``` + +GET 同样需要 `X-App-Key`、`X-Timestamp`、`X-Nonce` 和 `X-Signature`,不需要 `Idempotency-Key`。canonical string 中幂等键行为空字符串,body 摘要使用空字节 SHA-256。 + +## 六、幂等与重复规则 + +- `X-Nonce`:短时间防重放,使用后不能再次使用。 +- `Idempotency-Key`:一次 HTTP 写操作标识;网络重试必须复用原值。 +- `appKey + eventNo`:第三方业务事件唯一标识。 +- `tenantId + inboundDetailId + externalDetailNo`:测量明细绑定唯一约束。 +- 相同事件号或幂等键重试时,`data` 原文及图片内容必须与首次完全一致,否则返回冲突。 + +## 七、主要失败场景 + +| 业务码 | 场景 | +|---|---| +| `A0400` | data 不是有效 JSON、图片为空或无法读取 | +| `A0401` | AppKey、时间戳或签名无效 | +| `A0403` | AppKey 未绑定请求仓库,或内部凭证无效 | +| `A0404` | 入仓码不存在,或货物明细不属于入库单关联订单 | +| `A0409` | 入库状态不允许测量、Nonce 重放、事件/幂等内容不一致、明细重复绑定 | +| `A0422` | 字段缺失、格式或数值范围不符合要求 | +| `S0503` | Order 或 File 服务暂不可用 | + +普通业务失败应以响应体 `code` 判断,不能只判断 HTTP 状态。 + +## 八、后台处理过程 + +```text +验签和防重放 +→ 按入仓码及绑定仓库查入库单 +→ 调用 Order 校验货物明细归属 +→ 检查第三方明细号是否已经绑定 +→ 调用 File 保存并校验真实图片 +→ 写入一条测量批次和一条测量明细 +→ 返回测量、明细和图片ID +``` + +如果图片已保存但测量数据落库失败,Firstleg 会调用 File 执行补偿清理。 + +## 九、上线前置 + +部署新版 Firstleg 前,先执行: + +```text +firstleg/src/main/resources/db/phase2-third-party-single-measurement-migration.sql +``` + +或执行已内联该增量的二期统一脚本: + +```text +deploy/sql/2026-08-04-二期入库新增表字段-MySQL8一键执行.sql +``` + +同时确认 Firstleg、Order、File 使用一致的 `INTERNAL_SERVICE_TOKEN`,并配置 `ORDER_SERVICE_URL`、`FILE_SERVICE_URL` 或启用 Nacos 服务发现。