2026-08-10 17:48:45 +08:00
|
|
|
|
# Firstleg-Finance 跨服务契约(技术草案)
|
|
|
|
|
|
|
2026-08-11 18:06:30 +08:00
|
|
|
|
> 版本:V0.3(一期五页来源链路实现对齐)
|
|
|
|
|
|
> 日期:2026-08-11
|
|
|
|
|
|
> 状态:技术边界和一期产品口径已确认;Firstleg 事件/Relay/快照与 Finance Inbox/来源写入已实现并通过定向测试。
|
2026-08-10 17:48:45 +08:00
|
|
|
|
|
2026-08-11 18:06:30 +08:00
|
|
|
|
本文是 `firstleg-service` 与已落地 `finance-service` 之间的费用来源技术契约。产品范围以《财务一期五页产品与接口验收契约》为准。
|
2026-08-10 17:48:45 +08:00
|
|
|
|
|
|
|
|
|
|
## 1. 事实依据与当前实现差异
|
|
|
|
|
|
|
2026-08-11 18:06:30 +08:00
|
|
|
|
当前 Firstleg 使用 `fl_inbound_outbox` 与业务事务同库记录统一事件信封,Outbox Relay 至少一次投递到 `POST /internal/finance/v1/fee-source-events`;事件只携带入库/柜子 ID、费用 ID 和版本,Finance 必须再调用受保护的来源快照接口,不能把短 payload 当作财务凭证。
|
2026-08-10 17:48:45 +08:00
|
|
|
|
|
2026-08-11 18:06:30 +08:00
|
|
|
|
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 通过。
|
2026-08-10 17:48:45 +08:00
|
|
|
|
|
|
|
|
|
|
## 2. 事件目录
|
|
|
|
|
|
|
|
|
|
|
|
| 事件名 | 生产时机 | Finance 处理 | 备注 |
|
|
|
|
|
|
|---|---|---|---|
|
2026-08-11 18:06:30 +08:00
|
|
|
|
| `FIRSTLEG_FEE_PUBLISHED` | 入库或柜子费用审核通过、参与结算并发布;由 `aggregateType/payload` 区分来源 | 写入不可变来源版本 | 已实现并验证 |
|
|
|
|
|
|
| `FIRSTLEG_FEE_ADJUSTED` | 入库或柜子费用调整产生新版本 | 追加新版本并将旧 `AVAILABLE` 标为 `SUPERSEDED` | 已实现并验证 |
|
|
|
|
|
|
| `FIRSTLEG_FEE_VOIDED` | 入库或柜子费用作废 | 将同版本来源标为 `VOIDED`,不物理删除 | 已实现并验证 |
|
2026-08-10 17:48:45 +08:00
|
|
|
|
|
|
|
|
|
|
事件不是“已生成应收”通知;Finance 仍按产品确认的入池条件(例如 `settlementIncluded`、审核状态和作废状态)决定是否进入可出账池。
|
|
|
|
|
|
|
|
|
|
|
|
## 3. 统一事件信封
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
2026-08-11 18:06:30 +08:00
|
|
|
|
"eventId": "FIRSTLEG_FEE:INBOUND:456:789:2:FIRSTLEG_FEE_PUBLISHED", "eventType": "FIRSTLEG_FEE_PUBLISHED",
|
2026-08-10 17:48:45 +08:00
|
|
|
|
"eventVersion": 1, "producer": "firstleg-service", "occurredAt": "2026-08-10T10:20:30.123+08:00",
|
|
|
|
|
|
"tenantId": "123", "aggregateType": "INBOUND", "aggregateId": "456",
|
2026-08-11 18:06:30 +08:00
|
|
|
|
"sequence": 2, "idempotencyKey": "FIRSTLEG_FEE:INBOUND:456:789:2:FIRSTLEG_FEE_PUBLISHED",
|
|
|
|
|
|
"payloadHash": "sha256...", "payload": { "inboundId": "456", "containerId": null, "feeId": "789", "feeVersion": 2 }
|
2026-08-10 17:48:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
约束:ID 以字符串传输;`eventId` 全局唯一且不可复用;`eventVersion` 为整数并只向后兼容增加字段;金额/数量不放在短 payload 中,统一从快照查询获取;`payloadHash` 对规范化 JSON 计算 SHA-256;`tenantId` 必须与来源快照客户一致。`sequence` 按同一 `aggregateId` 单调递增,用于识别乱序,但不能替代版本校验。
|
|
|
|
|
|
|
|
|
|
|
|
## 4. 来源快照内部接口
|
|
|
|
|
|
|
|
|
|
|
|
Finance 使用服务间令牌调用 Firstleg(不经过客户端权限,不共享 Entity):
|
|
|
|
|
|
|
2026-08-11 18:06:30 +08:00
|
|
|
|
目标:`GET /internal/firstleg/v1/inbounds/{inboundId}/fees/{feeId}/source-snapshot`
|
2026-08-10 17:48:45 +08:00
|
|
|
|
|
2026-08-11 18:06:30 +08:00
|
|
|
|
入库和柜子来源快照接口均已实现并通过 Firstleg 定向测试;Finance Feign Client 使用相同路径和内部令牌。既有 `/internal/firstleg/v1/finance-cost-snapshots/customers/{customerId}/containers/{containerId}` 仍只是体积分摊接口,不能作为费用来源快照使用。
|
|
|
|
|
|
|
|
|
|
|
|
请求头:`X-Internal-Service-Token`。令牌由环境密钥管理;Firstleg Controller 使用恒定时间比较校验,拒绝客户端 JWT 伪造内部调用。
|
2026-08-10 17:48:45 +08:00
|
|
|
|
|
|
|
|
|
|
响应 DTO 至少包含:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
2026-08-11 18:06:30 +08:00
|
|
|
|
tenantId, sourceType, feeId, feeVersion,
|
|
|
|
|
|
inboundId, inboundCode, orderId, containerId, containerBookingId, containerNo,
|
|
|
|
|
|
customerId, customerName, feeItemId, feeItemCode, feeItemName,
|
|
|
|
|
|
direction, businessDate, currency, totalAmount,
|
|
|
|
|
|
reviewStatus, publicationStatus, settlementIncluded, publishedAt,
|
|
|
|
|
|
chargeableQuantity, chargeUnit, actualGrossWeightKg, actualVolumeCbm,
|
2026-08-10 17:48:45 +08:00
|
|
|
|
sourceHash, snapshotVersion
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-11 18:06:30 +08:00
|
|
|
|
Finance 将完整 DTO 和 `sourceHash` 存入自有来源池。非 `FIRSTLEG_FEE_VOIDED` 事件只接受 `publicationStatus=PUBLISHED`;未发布返回 `FIN-EVT-015`。作废事件允许读取对应作废版本,但只能把既有来源标为 `VOIDED`,不得创建新来源。
|
2026-08-10 17:48:45 +08:00
|
|
|
|
|
|
|
|
|
|
建议附加只读校验接口:`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、服务令牌、错误隔离和审计要求。
|
|
|
|
|
|
|
2026-08-11 18:06:30 +08:00
|
|
|
|
产品口径已冻结:只有 `APPROVED + PUBLISHED + settlementIncluded=true + ACTIVE + CNY` 来源可入池;一期按客户/供应商形成五页单据,出账后冻结,调整/作废重开保留版本和原因;不做汇率、月结、钱包抵扣、费用分析和利润表。事件消费者不得从历史原型推断这些已排除能力。剩余问题是来源模型、接口、事件和消费者实现,不是产品冲突。
|
2026-08-10 17:48:45 +08:00
|
|
|
|
|
2026-08-11 18:06:30 +08:00
|
|
|
|
主要剩余风险:测试环境尚未完成 Firstleg Relay → Finance Inbox → `fin_fee_source` 的真实 HTTP 冒烟。产品已确认无历史财务数据,不需要历史事件回放方案。
|