Files
docs/财务管理/Firstleg-Finance跨服务契约.md
T
2026-08-19 09:42:09 +08:00

123 lines
9.2 KiB
Markdown
Raw 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.
# Firstleg-Finance 跨服务契约(技术草案)
> 版本:V0.3(一期五页来源链路实现对齐)
> 日期:2026-08-11
> 状态:技术边界和一期产品口径已确认;Firstleg 事件/Relay/快照与 Finance Inbox/来源写入已实现并通过定向测试。
本文是 `firstleg-service` 与已落地 `finance-service` 之间的费用来源技术契约。产品范围以《财务一期五页产品与接口验收契约》为准。
## 1. 事实依据与当前实现差异
当前 Firstleg 使用 `fl_inbound_outbox` 与业务事务同库记录统一事件信封,Outbox Relay 至少一次投递到 `POST /internal/finance/v1/fee-source-events`;事件只携带入库/柜子 ID、费用 ID 和版本,Finance 必须再调用受保护的来源快照接口,不能把短 payload 当作财务凭证。
Finance 已实现内部事件 Controller、令牌鉴权、Inbox 同事件哈希幂等、Firstleg 快照回查,以及 `fin_fee_source` 新版本写入、旧版本替代和作废保留。非作废事件显式要求 `publicationStatus=PUBLISHED`,否则返回 `FIN-EVT-015`;金额必须 `>0`,零/负值返回 `FIN-EVT-010`。Finance 来源专项 10/10、全量 35/35 BUILD SUCCESS,Firstleg 事件/Relay/快照/柜子费用相关测试 17/17 通过。
## 2. 事件目录
| 事件名 | 生产时机 | Finance 处理 | 备注 |
|---|---|---|---|
| `FIRSTLEG_FEE_PUBLISHED` | 入库或柜子费用审核通过后自动发布;由 `aggregateType/payload` 区分来源 | 写入不可变来源版本 | 已实现并验证 |
| `FIRSTLEG_FEE_ADJUSTED` | 入库或柜子费用调整产生新版本 | 追加新版本并将旧 `AVAILABLE` 标为 `SUPERSEDED` | 已实现并验证 |
| `FIRSTLEG_FEE_VOIDED` | 入库或柜子费用作废 | 将同版本来源标为 `VOIDED`,不物理删除 | 已实现并验证 |
事件不是“已生成应收”通知;Finance 按审核、发布、来源状态、币种和正金额条件决定是否进入可出账池,不保存人工“是否结算”开关。
## 3. 统一事件信封
```json
{
"eventId": "FIRSTLEG_FEE:INBOUND:456:789:2:FIRSTLEG_FEE_PUBLISHED", "eventType": "FIRSTLEG_FEE_PUBLISHED",
"eventVersion": 1, "producer": "firstleg-service", "occurredAt": "2026-08-10T10:20:30.123+08:00",
"tenantId": "123", "aggregateType": "INBOUND", "aggregateId": "456",
"sequence": 2, "idempotencyKey": "FIRSTLEG_FEE:INBOUND:456:789:2:FIRSTLEG_FEE_PUBLISHED",
"payloadHash": "sha256...", "payload": { "inboundId": "456", "containerId": null, "feeId": "789", "feeVersion": 2 }
}
```
约束:ID 以字符串传输;`eventId` 全局唯一且不可复用;`eventVersion` 为整数并只向后兼容增加字段;金额/数量不放在短 payload 中,统一从快照查询获取;`payloadHash` 对规范化 JSON 计算 SHA-256;`tenantId` 必须与来源快照客户一致。`sequence` 按同一 `aggregateId` 单调递增,用于识别乱序,但不能替代版本校验。
## 4. 来源快照内部接口
Finance 使用服务间令牌调用 Firstleg(不经过客户端权限,不共享 Entity):
目标:`GET /internal/firstleg/v1/inbounds/{inboundId}/fees/{feeId}/source-snapshot`
入库和柜子来源快照接口均已实现并通过 Firstleg 定向测试;Finance Feign Client 使用相同路径和内部令牌。既有 `/internal/firstleg/v1/finance-cost-snapshots/customers/{customerId}/containers/{containerId}` 仍只是体积分摊接口,不能作为费用来源快照使用。
请求头:`X-Internal-Service-Token`。令牌由环境密钥管理;Firstleg Controller 使用恒定时间比较校验,拒绝客户端 JWT 伪造内部调用。
响应 DTO 至少包含:
```text
tenantId, sourceType, feeId, feeVersion,
inboundId, inboundCode, orderId, containerId, containerBookingId, containerNo,
customerId, customerName, feeItemId, feeItemCode, feeItemName,
direction, businessDate, currency, totalAmount,
reviewStatus, publicationStatus, publishedAt,
chargeableQuantity, chargeUnit, actualGrossWeightKg, actualVolumeCbm,
supplierId, supplierName, supplierCategory, sourceHash, snapshotVersion
```
Finance 将完整 DTO 和 `sourceHash` 存入自有来源池。非 `FIRSTLEG_FEE_VOIDED` 事件只接受 `publicationStatus=PUBLISHED`;未发布返回 `FIN-EVT-015`。作废事件允许读取对应作废版本,但只能把既有来源标为 `VOIDED`,不得创建新来源。
建议附加只读校验接口:`GET /internal/firstleg/v1/inbounds/{inboundId}/fees/{feeId}/publication`,用于消费重试时快速判断发布状态;该接口不能返回跨客户数据。
## 5. 消费幂等、乱序与冲突
- Finance 在本地 `fin_event_inbox` 以 `eventId` 唯一;以 `sourceType + sourceFeeId + sourceFeeVersion` 建唯一业务键。
- 首次消费在同一事务内写入 Inbox、来源快照和处理结果;重复事件返回原结果,不重复生成来源/账单。
- 同一业务键的 `payloadHash` 不一致,标记 `CONFLICT`,告警并人工处理,禁止覆盖原快照。
- `feeVersion` 小于已处理版本:标记 `STALE`,保留审计,不回退财务来源。
- 高版本先到:先保存为 `PENDING_SOURCE`,查询快照成功且版本链完整后转 `ACCEPTED`;缺少前序版本不阻塞新版本,但需记录 gap,产品规则决定是否暂停出账。
- `VOIDED` 只能作用于同一来源版本;已出账时生成待处理冲销事件/任务,绝不物理删除。
## 6. 重试、超时与降级
Firstleg Outbox 发布采用至少一次投递:指数退避(建议 10s、30s、2m、10m、30m,最多 12 次),每次带 `traceId`;超过次数进入 `FAILED/DLQ` 并告警。Finance 消费失败分为:
| 类型 | 处理 |
|---|---|
| 连接/HTTP 408、429、5xx | 可重试;不确认消息 |
| 401/403、签名/租户不匹配 | 不重试,标记 `REJECTED` 并告警 |
| 来源 404/尚未提交 | 重试查询,超过保留期转 `SOURCE_UNAVAILABLE` |
| DTO 校验、哈希或版本冲突 | 不自动重试,进入人工队列 |
快照接口超时建议 3 秒连接、5 秒读取;消费端总处理超时 10 秒。Finance 不因 Firstleg 暂时不可用而伪造金额、跳过来源校验或直接生成应收;仅允许已保存且校验通过的来源继续后续财务流程。
## 7. 统一错误码(内部契约)
| 错误码 | 含义 | 是否重试 |
|---|---|---|
| `FIN-SRC-001` | 来源不存在/不可见 | 查询型可重试,最终人工 |
| `FIN-SRC-002` | 来源状态不满足入池 | 否 |
| `FIN-SRC-003` | 客户/主体范围不一致 | 否,安全告警 |
| `FIN-SRC-004` | 来源哈希或版本冲突 | 否,人工 |
| `FIN-SRC-005` | 快照字段不完整 | 否,修复上游契约后重放 |
| `FIN-EVT-001` | 事件重复且内容一致 | 否,返回原结果 |
| `FIN-EVT-002` | 事件重复但内容不同 | 否,隔离冲突 |
| `FIN-EVT-003` | 事件版本不支持 | 否,进入 DLQ |
| `FIN-AUTH-001` | 服务令牌无效/过期 | 否 |
| `FIN-TIME-001` | 财务期间已关闭 | 否,走调整/冲销 |
业务 API 继续遵循后端统一约定:HTTP 200 + body `code`(成功为字符串 `"200"`);内部认证/基础设施拒绝可使用 401/403/503。
## 8. 可观测性与审计
所有事件和查询日志必须带 `eventId`、`traceId`、`tenantId`、`aggregateId`、`sourceFeeId/version`、`attempt`、`consumerStatus`。Finance 至少提供 Inbox/来源池指标:待处理数、重试数、DLQ 数、冲突数、快照延迟、事件到财务入池延迟。拒绝、冲突、作废、重放和人工补偿写入财务审计;日志不得记录服务令牌和完整客户敏感凭证。
## 9. 发布顺序与契约测试
1. 先冻结本文和产品确认中 F0-08/F1-09 的业务口径,补齐 Firstleg 事件版本与快照 OpenAPI。
2. 先上线兼容生产者(保留 `FIRSTLEG_FEE_SENT` 映射),再部署 Finance 消费者并回放历史 Outbox;确认无冲突后切换新事件名。
3. 最后启用调整/作废新事件及 Finance 出账入口;任何一方不支持的 `eventVersion` 必须安全拒绝。
契约测试至少覆盖:信封字段和 JSON Schema、签名/服务令牌、租户隔离、重复事件同结果、同键不同哈希冲突、乱序版本、Outbox 重试/DLQ、快照 404/超时、作废已出账保护、金额精度和三位币种、跨币种核销拒绝。测试可由 Firstleg producer contract test 与 Finance consumer contract test 共享固定 JSON fixtures,但不得共享 Java Entity。
## 10. 确认边界与风险
技术已确认:独立服务/数据库、DTO/事件通信、Outbox 至少一次、Finance Inbox 幂等、不可变来源快照、无跨库 Join、服务令牌、错误隔离和审计要求。
产品口径已冻结:只有 `APPROVED + PUBLISHED + AVAILABLE + CNY + amount>0` 来源可入池;审核通过后自动成为结算候选。PAYABLE 的 supplierId 必须是 Warehouse `ACTIVE` 供应商,supplierCategory 为空时允许任意启用供应商,非空时严格匹配且 Finance 不从费用名称猜测类别。出账后冻结,调整/作废重开保留版本和原因;不做汇率、月结、钱包抵扣、费用分析和利润表。
主要剩余风险:测试环境尚未完成 Firstleg Relay → Finance Inbox → `fin_fee_source` 的真实 HTTP 冒烟。产品已确认无历史财务数据,不需要历史事件回放方案。