Files
docs/财务管理/海外仓费用统一接入阶段0技术契约.md
2026-09-23 17:47:52 +08:00

230 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 海外仓费用统一接入阶段 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`