钱留在您的系统里。算数留在我们这里。
平台是一次计算和一份记录。它接收您系统里发生的事,算出欠了多少,再把指令送回去。它从不坐在资金路径上。
一份已发布的规范,以及照着它写的两个客户端。
仓库里的一份 OpenAPI 文档、一个 TypeScript 客户端和一个 Dart 客户端。在任何人约会议之前,您的工程师就能把接口面读完。
OpenAPI
api/openapi.yaml
TypeScript 客户端
sdk/typescript
Dart 客户端
sdk/dart
可以放心它会送到的事件。
十一种事件类型,与产生它们的状态变更在同一个事务里入队。
- 01
事务性发件箱,而不是发完就忘的 POST
事件在导致它的那个事务里写入,之后从一个带租约的队列里派发。不存在这样的窗口:您的系统被告知了某件事,而总账随后回滚了它;也不存在总账已提交却没人被告知的窗口。internal/outbox
- 02
带签名,密钥 id 可轮换
每次投递都带一个 HMAC-SHA256 签名,附密钥 id 和时间戳。轮换是重叠的:旧密钥在您切换期间仍然有效,所以轮换一个密钥不是一次您得排期的停机。internal/outbox/sign.go
- 03
结算事件走在前面
提取指令不会排在一堆通知后面。投递类别是分开的车道,而结算就是那条载着资金指令的车道。internal/events
- 04
确定性 id,所以重跑是空操作
重跑一个处理器会把同一个事件 id 重新入队,发件箱会去重。这是您真的可以重试的“至少一次”投递,而不是您必须小心翼翼的“至少一次”投递。internal/events · internal/outbox
- 05
对您的 URL 做出站守卫
目的地由操作方配置,这在构造上就使它成为一个 SSRF 面。分发器在发出请求之前检查 URL,并封掉您预期它会封掉的那些地址。internal/outbox/ssrf.go
- 06
退避、停放和一个对账器
失败会退避并最终停放,而不是猛敲一个已经宕掉的目的地。一个跨系统对账器负责抓出真正丢掉的事件,而不是指望队列永远完美。internal/outbox · internal/reconciler
以您自己的记录 id 为键、恰好执行一次的命令。
您告诉平台:发生了一笔购买、发生了一次退款、一位会员通过了核验。重试是免费的,因为键是您的。
每条入站命令按工作区和参考号幂等,响应也会被缓存。把同一笔购买发两次,第二次调用返回的是第一次的答案——它不会创建第二笔购买,也不会为第一笔再发一轮佣金。
错误契约是显式的,因为“要不要重试”才是一个客户端集成真正必须回答的问题。返回错误会释放回执,您可以重试。带 4xx 码的拒绝是终态且被缓存的:这条命令是按其内容被拒的,再发一次还是会被拒。
internal/commands · internal/command
您原来怎么结算,就还怎么结算。
平台锁定确切的总账分录并下达指令。执行付款的是您。
如果您有结算系统,它会以结算类事件收到提取指令,并回调告知结清或失败。如果没有,付款控制台会列出欠了什么,您从自己的钱包付款,并记录成交价格和您自己的交易参考号。没有参考号的付款记录会被拒绝——它永远无法和您的钱包对上账,接受它只意味着对账会在更晚、更远的地方失败。
internal/claims · internal/httpapi/payouts.go
您自己的域名,先验证再开通。
认领主机名,发布平台生成的 TXT 记录,解析通过后即开通。
顺序很重要,而且这就是全部要点。在证明控制权之前就绑定主机名——或者把一个域名加进身份提供方的授权列表——会让真正拥有那个名字的人收到本该给您的登录。所以在记录解析通过之前,什么都不开通,也什么都不加进登录白名单。一个后台开通器会在一分钟内接手。
internal/domains