8.8 KiB
Firstleg-Finance 跨服务契约(技术草案)
版本:V0.1(开发前冻结草案)
日期:2026-08-10
状态:技术边界已确认;业务口径以《财务管理产品确认表》为准,未将产品待确认项擅自固化。
本文是 firstleg-service 与规划中的 finance-service 之间的技术契约草案。它只规定服务边界、传输可靠性和来源快照,不替代财务账单、收款、核销、汇率或收入确认规则。
1. 事实依据与当前实现差异
当前 Firstleg 已有 fl_inbound_outbox(与入库事务同库写入)和发布状态/重试字段;FbaPricingService 已发布 FIRSTLEG_FEE_SENT,调整已发布费用时发布 FIRSTLEG_FEE_ADJUSTED。现有 payload 主要只有 inboundId、feeId、feeVersion,Finance 不得把它当作完整财务凭证,必须再调用受保护的来源快照接口。
当前代码没有独立 Finance 消费者、FIRSTLEG_FEE_VOIDED 事件或 Finance 数据库;下列事件版本、信封、消费表和接口均为实施时新增/补齐的契约,不代表已上线。
2. 事件目录
| 事件名 | 生产时机 | Finance 处理 | 备注 |
|---|---|---|---|
FIRSTLEG_FEE_PUBLISHED |
费用确认并发送、来源明细发布成功 | 建立/更新来源池候选,待规则校验 | 首版可由现有 FIRSTLEG_FEE_SENT 兼容映射;新契约统一使用 PUBLISHED |
FIRSTLEG_FEE_ADJUSTED |
已发布费用被调整,新版本替代旧版本 | 原来源保留,新增调整来源;不得覆盖已出账明细 | 只有来源版本递增且哈希改变才接受 |
FIRSTLEG_FEE_VOIDED |
费用来源作废 | 标记来源作废;若已入账,生成冲销/补差任务,不自动删除应收 | 需要 Firstleg 补充明确作废动作及原因 |
事件不是“已生成应收”通知;Finance 仍按产品确认的入池条件(例如 settlementIncluded、审核状态和作废状态)决定是否进入可出账池。
3. 统一事件信封
{
"eventId": "fl-01J...", "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": 3, "idempotencyKey": "FIRSTLEG_FEE:456:789:2",
"payloadHash": "sha256...", "traceId": "...", "payload": { "inboundId": "456", "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
请求头:X-Service-Name: finance-service、X-Service-Token、X-Request-Id。令牌由环境密钥管理,按服务名、路径和方法授予最小权限;拒绝客户端 JWT 伪造内部调用。
响应 DTO 至少包含:
tenantId, inboundId, inboundCode, customerId, customerName,
feeId, feeNo, feeVersion, sourceType, direction, businessDate,
currency, subtotalAmount, additionalAmount, totalAmount,
feeStatus, publicationStatus, settlementIncluded, publishedAt,
chargeUnit, chargeableQuantity, actualGrossWeightKg, actualVolumeCbm,
originWarehouseId/name, channelCode/name, destinationCode/name,
items[{feeItemId, feeCode, name, quantity, unit, unitPrice, amount}],
sourceHash, snapshotVersion
响应必须是当前 Firstleg 已发布版本的不可变业务快照;Finance 将完整 DTO 和 sourceHash 存入自有来源池。若上游只返回 SUPERSEDED/VOID 版本,Finance 不得将其作为新应收来源。
建议附加只读校验接口: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. 发布顺序与契约测试
- 先冻结本文和产品确认中 F0-08/F1-09 的业务口径,补齐 Firstleg 事件版本与快照 OpenAPI。
- 先上线兼容生产者(保留
FIRSTLEG_FEE_SENT映射),再部署 Finance 消费者并回放历史 Outbox;确认无冲突后切换新事件名。 - 最后启用调整/作废新事件及 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、服务令牌、错误隔离和审计要求。
仍依赖产品确认:何时 settlementIncluded、何种来源进入应收;客户/供应商账单聚合和确认节点;调整/作废对已出账单的补差或冲销策略;汇率、财务期间和收入/成本确认口径;客户账户是否抵扣应收。上述未确认项不得由事件消费者自行推断。
主要风险:现有 FIRSTLEG_FEE_SENT payload 过小且 Outbox 未带 eventVersion/payloadHash/sequence;当前没有明确的作废 API;fl_consumed_event 属 Firstleg 内部消费表,不能直接复用为 Finance Inbox。实施时需新增兼容字段/映射和回放工具,并在切换前验证历史费用版本的唯一性与哈希完整性。