230 lines
13 KiB
Markdown
230 lines
13 KiB
Markdown
# 海外仓费用统一接入阶段 0 技术契约
|
||
|
||
> 文档状态:开发基线配套契约 V0.1
|
||
> 编制日期:2026-09-21
|
||
> 适用范围:阶段 0A—0E;配套需求基线为 [海外仓费用统一接入财务评审方案](海外仓费用统一接入财务评审方案.md)
|
||
|
||
本文把 V0.5 的阶段 0 规则转成可编码的跨服务边界、状态和唯一性约束。本文不是数据库字段逐项施工单;开发时可以调整表名和类名,但不得改变这里的业务语义、状态分离、幂等边界和跨区域结果规则。
|
||
|
||
## 一、阶段 0 目标与顺序
|
||
|
||
阶段 0 不是新增仓租算法,先修复已有海外费用链路的一致性:
|
||
|
||
| 子阶段 | 目标 | 依赖 |
|
||
|---|---|---|
|
||
| 0A | 统一收费身份、人工/自动去重、换标漏发、拆柜费用身份、派送收付方向 | 先冻结本文模型和唯一键 |
|
||
| 0B | 人工费用草稿/驳回修改、待审工作台、业务费用聚合、直付证据 | 0A 收费身份 |
|
||
| 0C | Finance 费用接受/拒绝结果回传、来源版本变更裁决 | 0A 的版本模型 |
|
||
| 0D | 资金计划 ID/版本、来源精确分配、冻结/结算/释放旧计划隔离 | 0C 的有效版本结果 |
|
||
| 0E | 商业减免、取消收费、待财务处理及自动重算门禁 | 0C、0D |
|
||
|
||
0A—0E 可以在前序数据结构稳定后并行实现,但不能在未冻结本契约前各模块自行新增状态或幂等字段。
|
||
|
||
## 二、当前代码事实与必须修正的断点
|
||
|
||
| 现状入口 | 当前行为 | 阶段 0 修正 |
|
||
|---|---|---|
|
||
| `lastleg-service/financefee/OverseasFeeSourceService` | `adjust` 先把旧版本设为 `SUPERSEDED`,`voidSource` 先设为 `VOIDED` | Finance 接受前旧有效版本继续保留;新候选/作废命令等待 Finance 结果 |
|
||
| `OverseasFeeOutboxService` / `BusinessFeeSourceChangedV1` | 只有海外向 Finance 的完整快照事件,没有业务可消费的接受/拒绝结果 | 增加版本化费用变更命令和 Finance 结果事件,双向 Outbox/Inbox 幂等 |
|
||
| `BusinessFeeSourceEventService` | Finance 直接入池并返回本地方法结果,无结果事件;资金关联按业务类型、业务单号和金额匹配 | 同事务记录命令结果;接受、拒绝、冲突、超时可回传;资金按计划版本和来源分配关联 |
|
||
| `WalletCommandRequestedV1` | 只带业务类型、业务单号、金额和聚合版本 | 资金计划 ID/版本、收费身份、计划金额快照和来源分配进入新目标协议 |
|
||
| `DestuffingPalletCostService` | 手工构造 `APPROVED/PUBLISHED` 来源,绕过通用审核和统一收费身份 | 复用计费身份、来源版本、审核策略和调整门禁;分摊组保持守恒 |
|
||
| 换标完成分支 | 仅部分 `COMPLETED` 路径发布换标费并发起资金结算 | 所有合法完成分支汇合到唯一发布/结算入口,重复完成幂等 |
|
||
| 派送转单 | 转派送时人工费用自动审核,且同一费用项目的应收/应付身份不完整 | 人工费用进入待审核;应收与应付按收费身份独立,实际供应商和直付证据必填 |
|
||
| `listForBusiness` | 主要按单一 `businessType + businessId` 查询 | 业务详情按稳定关系聚合来源,当前有效版本与待确认变更分开返回 |
|
||
|
||
## 三、收费身份模型
|
||
|
||
### 3.1 收费身份与来源版本分离
|
||
|
||
收费身份表示“同一业务应该收哪一笔钱”;来源版本表示“这一笔钱当前提交了哪个金额版本”。二者不得混用。
|
||
|
||
收费身份逻辑字段:
|
||
|
||
```text
|
||
identityKey
|
||
customerId
|
||
sourceSystem
|
||
businessType
|
||
businessId / businessNo
|
||
scenarioCode
|
||
feeItemCode
|
||
direction
|
||
priceRole CUSTOMER_SALE / SUPPLIER_COST
|
||
supplierId 成本侧必填;销售侧为空
|
||
periodScope 离散事实或计费期间范围
|
||
```
|
||
|
||
约束:
|
||
|
||
- 客户应收和供应商应付是两条身份;不能用方向字段在一行中来回切换。
|
||
- 成本侧实际供应商进入身份。多个供应商责任必须拆成多个身份。
|
||
- 自动任务、人工补录、重放、报价恢复和资金计划都引用同一身份。
|
||
- 明确免收也占用身份,不能因为没有 Finance 来源而再次收费。
|
||
- 退货按票身份按 `RETURN_ORDER + RETURN_HANDLING + CUSTOMER_SALE` 去重;退货操作费按实收累计组;仓租按原始分量、角色、供应商和日期范围占用。
|
||
|
||
### 3.2 来源候选版本状态
|
||
|
||
业务审核状态、Finance 同步状态、财务来源状态和结算状态分开保存:
|
||
|
||
| 状态轴 | 首版值 | 含义 |
|
||
|---|---|---|
|
||
| `reviewStatus` | `DRAFT`、`PENDING`、`APPROVED`、`REJECTED` | 业务是否允许向 Finance 提交 |
|
||
| `financeSyncStatus` | `NOT_SENT`、`PENDING_FINANCE`、`ACCEPTED`、`REJECTED` | 该候选命令的跨区结果 |
|
||
| `sourceStatus` | `ACTIVE`、`SUPERSEDED`、`VOIDED` | Finance 接受后的来源有效性;接受前不得覆盖旧有效版本 |
|
||
| `settlementStatus` | `AVAILABLE`、`PENDING_WALLET`、`SETTLED`、`BILLED`、`BLOCKED_FINANCE` | 出账或资金路径状态 |
|
||
|
||
同一收费身份最多一个待 Finance 确认的候选或变更。Finance 拒绝后,旧有效版本继续有效;修订必须以最后 Finance 接受的版本为前序生成新候选。
|
||
|
||
## 四、跨区域费用命令与结果
|
||
|
||
### 4.1 费用变更命令
|
||
|
||
现有 `BusinessFeeSourceChangedV1` 保留为被替换的旧目标,不新增双协议兼容。阶段 0 直接收敛到新版本事件,建议逻辑载荷名为 `BusinessFeeSourceCommandV2`,事件动作:
|
||
|
||
- `PUBLISH`:首次发布一个候选版本;
|
||
- `REPLACE`:申请替代当前有效版本;
|
||
- `VOID`:申请作废当前有效版本。
|
||
|
||
命令必须携带:
|
||
|
||
```text
|
||
commandId
|
||
identityKey
|
||
sourceFeeId
|
||
targetVersion
|
||
expectedActiveVersion 首次发布显式为空
|
||
action
|
||
sourceSnapshot
|
||
sourceHash
|
||
requestedAt
|
||
```
|
||
|
||
`sourceSnapshot` 继续沿用现有完整客户、业务、方向、供应商、付款、币种、数量、期间和审核快照。Finance 不跨区回查 Lastleg。
|
||
|
||
### 4.2 Finance 结果事件
|
||
|
||
Finance 在同一事务完成校验、来源写入/变更裁决和结果 Outbox,向海外发送 `BusinessFeeSourceResultV2`:
|
||
|
||
```text
|
||
commandId
|
||
identityKey
|
||
result ACCEPTED / REJECTED
|
||
acceptedSourceFeeId
|
||
acceptedSourceVersion
|
||
activeSourceVersion
|
||
errorCode
|
||
errorMessage
|
||
processedAt
|
||
```
|
||
|
||
规则:
|
||
|
||
- 同 `commandId + payloadHash` 重放返回原结果;同命令异载荷拒绝。
|
||
- `expectedActiveVersion` 不等于 Finance 当前有效版本时拒绝,不回退当前有效版本。
|
||
- `ACCEPTED` 才能让海外候选变为有效版本;`REJECTED` 只关闭该候选命令,不删除事实。
|
||
- 超时和网络失败保持 `PENDING_FINANCE`,不能标记成功或拒绝。
|
||
- 结果回传失败不改变 Finance 已接受状态;海外重试同一结果事件。
|
||
- Event Bridge 只负责路由和投递,不把“已转发”当作 Finance 已接受。
|
||
|
||
## 五、资金计划版本
|
||
|
||
费用来源与资金冻结不能再通过“业务类型 + 业务单号 + 金额”模糊关联。
|
||
|
||
### 5.1 计划模型
|
||
|
||
资金计划逻辑字段:
|
||
|
||
```text
|
||
planId
|
||
planVersion
|
||
identityKey
|
||
customerId
|
||
amountCny
|
||
status PENDING / FROZEN / SETTLE_PENDING / SETTLED / RELEASE_PENDING / RELEASED / FAILED
|
||
sourceAllocations[] sourceFeeId + sourceVersion + amountCny
|
||
```
|
||
|
||
一笔计划可以分配多笔来源,但所有分配金额之和必须等于计划金额;一份来源版本只能被该计划消费一次。计划金额、身份和版本一旦冻结,不允许先改费用再等待来源到达。
|
||
|
||
### 5.2 指令规则
|
||
|
||
资金命令至少携带 `planId + planVersion + operation + identityKey + amountSnapshot`。`SETTLE` 先到时,Finance 持久化等待;只有目标来源全部接受、分配完整且金额守恒后才扣款。`RELEASE` 结果未知时保持锁定,不允许同时建立新计划。
|
||
|
||
旧计划释放后收到迟到 `SETTLE`,只能访问旧计划并返回拒绝,不能按业务单号找到新计划执行扣款。计划 ID 不复用。
|
||
|
||
## 六、业务链路修复要求
|
||
|
||
### 6.1 换标
|
||
|
||
- 统一所有合法完成分支进入一个费用/资金编排入口。
|
||
- `RELABEL_SERVICE` 保持现有费用编码,不新增同义 `RELABEL_FEE`。
|
||
- 费用发布、资金结算和业务完成各自幂等;纯 SKU、需要出库和退货换标路径都必须覆盖。
|
||
- Finance 接受前允许业务保持原有效候选;资金结算必须绑定计划版本和已接受来源版本。
|
||
|
||
### 6.2 拆柜打托
|
||
|
||
- 保留全柜成本总额和客户分摊守恒;每客户分摊来源拥有稳定收费身份。
|
||
- 不再手工直接插入“已审核已发布”绕过通用服务;自动审核必须来自获批规则,调整和作废复用统一命令。
|
||
- 首版继续 `DESTUFF_PALLET` 与 `OVERSEAS_CONTAINER` 业务身份,但应在通用校验中明确允许该组合,避免项目编码与调整校验不一致。
|
||
- 当前仅自动客户应收;供应商成本属于后续阶段,不能伪造应付。
|
||
|
||
### 6.3 派送
|
||
|
||
- 同一派送业务允许一条客户应收和一条供应商应付,但两者身份、价格、供应商和结算方式独立。
|
||
- 人工金额默认进入 `PENDING`,不能在转派送动作中自动审核。
|
||
- 业务日期使用真实费用触发日期,不使用无关任务执行时刻作为历史补价日期。
|
||
- `DIRECT_PAID` 必须有付款时间、方式、流水号或凭证,并由 Finance 形成已确认成本;`ON_ACCOUNT` 进入应付。
|
||
|
||
### 6.4 业务费用查询
|
||
|
||
业务详情查询按稳定关系聚合订单、入仓、柜、派送、换标和退货来源;返回:
|
||
|
||
- 当前 Finance 接受的有效版本;
|
||
- 待 Finance 确认的候选/变更;
|
||
- 被拒绝、超时、待资金、已结算和已作废结果;
|
||
- 共享费用的分摊金额,不重复计入多个订单小计。
|
||
|
||
Finance 尚未接受的来源必须能从海外异常入口查询,不能以 Finance 费用池没有记录作为“未生成”的唯一判断。
|
||
|
||
## 七、ID、供应商和事件边界
|
||
|
||
- 跨端 DTO 和事件中的仓库、供应商、客户、业务 ID 使用十进制字符串;领域层严格解析,禁止使用名称或 FBA 目的仓代码替代主档 ID。
|
||
- Lastleg 报价仓、实际作业仓、库存仓引用同一国内 Warehouse 主档 ID;`providerName` 只能是快照,不能作为供应商身份。
|
||
- 供应商成本来源必须通过 Warehouse 校验启用状态和类别;Finance 再次失败关闭校验。
|
||
- 事件载荷不携带用户 JWT,不跨区域回查数据库;生产者、Event Bridge、Finance、结果消费者和测试必须同步升级。
|
||
|
||
## 八、开发与验证门禁
|
||
|
||
### 8.1 0A 开工前
|
||
|
||
开发先提交以下技术设计并在代码中一次性收敛:
|
||
|
||
1. 收费身份、候选版本、当前有效版本和唯一约束;
|
||
2. `BusinessFeeSourceCommandV2` / `BusinessFeeSourceResultV2` 事件 DTO、事件类型和双向路由;
|
||
3. 资金计划、分配表、计划版本和 V2 指令事件;
|
||
4. 仓库/供应商 ID 与业务场景枚举校验;
|
||
5. Lastleg、Finance、Event Bridge 的锁顺序、Outbox/Inbox 和拒绝重试分类。
|
||
|
||
### 8.2 阶段验证
|
||
|
||
- 0A:换标四类完成路径、拆柜分摊守恒、派送应收/应付和人工审核。
|
||
- 0B:草稿驳回修改、统一待审查询、业务聚合和直付证据。
|
||
- 0C:Finance 接受、拒绝、超时、乱序、重放、前序版本冲突和来源冻结。
|
||
- 0D:冻结/结算/释放乱序、计划版本隔离、来源分配守恒、旧指令拒绝。
|
||
- 0E:减免接受、减免拒绝、自动重算暂停、取消收费和待财务处理。
|
||
|
||
跨服务修改至少运行公共事件契约、Lastleg、Finance、Event Bridge 直接消费者测试,并执行 `node scripts/mapper-scan-contract.mjs`、`node --test scripts/gateway-route-contract.test.mjs`;未经明确环境授权不启动服务、不执行真实 HTTP、不部署。
|
||
|
||
## 九、代码入口
|
||
|
||
- 海外费用来源:`baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/financefee/`
|
||
- Finance 海外费用消费:`baiyue-firstleg/finance/src/main/java/com/baiyue/finance/service/BusinessFeeSourceEventService.java`
|
||
- 资金事件:`baiyue-firstleg/common-event-contract/src/main/java/com/baiyue/common/event/WalletCommandRequestedV1.java`、`WalletCommandCompletedV1.java`
|
||
- 资金生产/消费:`baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/wallet/`、`baiyue-firstleg/finance/src/main/java/com/baiyue/finance/service/WalletCommandEventService.java`
|
||
- 换标:`baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java`
|
||
- 派送:`baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java`
|
||
- 拆柜打托:`baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/DestuffingPalletCostService.java`
|
||
- 业务费用来源表:`lastleg-service/src/main/resources/db/lastleg.sql` 的 `ll_finance_fee_source`;Finance 表:`finance/src/main/resources/db/finance.sql` 的 `fin_fee_source`、`fin_wallet_hold`、`fin_wallet_command_inbox`
|
||
|