Files
docs/财务管理/Firstleg-Finance跨服务契约.md
T

12 KiB
Raw Blame History

Firstleg-Finance 跨服务契约(技术草案)

版本:V0.4(费用作废命令与事件回流对齐)
日期:2026-08-20
状态:技术边界和订单与客户产品口径已确认;Firstleg 事件/Relay/快照、Finance Inbox/来源写入及 Finance 发起的业务作废闭环均已实现。

本文是 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. 统一事件信封

{
  "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 至少包含:

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,不得创建新来源。

4.1 Finance 发起业务费用作废

Finance 的客户应收应付页面允许发起 Firstleg 来源作废,但 Finance 不是业务费用事实源,不得先更新 fin_fee_source,也不得自行生产 FIRSTLEG_FEE_VOIDED。Finance 使用服务间令牌调用:

POST /internal/firstleg/v1/fee-sources/commands/void

请求必须携带 Idempotency-Key、规范化请求的 X-Request-Hash,以及 sourceType/sourceFeeId/sourceVersion/customerId/inboundId|containerId/operatorId/operatorName/reason。Firstleg 必须重新校验客户、业务归属、稳定来源 ID 和精确版本,在同一事务中作废业务费用、记录 fl_finance_fee_source_command 幂等审计并写入 FIRSTLEG_FEE_VOIDED Outbox;相同幂等键同参重放原结果,异参冲突。Finance 只有消费该事件后才将本地来源置为 VOIDED。

Firstleg 来源的金额编辑和调整仍必须回到入库费用或柜次费用业务明细完成,由 Firstleg 生成新版本并发布 FIRSTLEG_FEE_ADJUSTED。Finance 的本地 edit/adjust 接口对 sourceSystem=FIRSTLEG 必须拒绝,不能只修改 fin_fee_source.amount。

建议附加只读校验接口: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-EVT-016 来源版本已进入非作废对账单或结算单,禁止调整、替代或作废 否,保留已冻结单据并修正上游费用
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 不从费用名称猜测类别。PAYABLE 还必须携带 settlementMode=ON_ACCOUNT|DIRECT_PAID:ON_ACCOUNT 可进入 AP,且已付字段必须为空;DIRECT_PAID 必填 paidAt、paymentMethod,paymentReferenceNo 与付款凭证至少一项,只计成本、不得进入 AP 或待付金额。付款方式只允许 ONLINE/BANK_TRANSFER/CASH/OTHER。这些字段必须进入不可变来源快照和 sourceHash,RECEIVABLE 必须为空。出账后冻结,调整/作废重开保留版本和原因;不做汇率、月结、钱包抵扣、费用分析和利润表。

成本与资金动作分离:同一来源版本按 sourceSystem + sourceType + sourceFeeId + sourceVersion 只识别一次成本。DIRECT_PAID 来源唯一物化为 fin_cost 已付成本事实;ON_ACCOUNT 成本仍由来源事实识别,正式 AP 仅承载负债、到期日和付款,不重复确认成本。总成本等于有效 ON_ACCOUNT 来源成本加有效 DIRECT_PAID 成本事实;待付金额只来自非作废正式 AP 的 outstanding_amount;已付金额等于 AP paid_amount 加 DIRECT_PAID 金额。旧来源版本被替代或作废时,对应 DIRECT_PAID 成本事实同步失效。

主要剩余风险:测试环境尚未完成 Firstleg Relay → Finance Inbox → fin_fee_source 的真实 HTTP 冒烟。产品已确认无历史财务数据,不需要历史事件回放方案。