问题域
金融软件有一个大多数软件不具备的特点:它的错误以金钱计量,并且会被客户、财务团队和监管机构注意到。一个超时后被重试的请求,可能变成一笔重复付款。以单个可变数值保存的余额,可能与本应解释它的交易逐渐偏离。服务商的结算文件与内部记录之间,可能存在一笔谁也追查不出来源的差额。这些都是工程层面的故障,都有成熟的解决办法,而这些办法必须打在地基里。
这个地基就是账本。复式记账账本把每一笔资金流动记录为跨账户、借贷平衡的分录,只追加不修改,余额由分录推导得出。支付、钱包、信贷和手续费,都是建立在账本之上的产品。围绕账本的,是让它在分布式系统中值得信赖的一系列机制:每一次写入的幂等、每笔支付的显式状态机、可靠的事件发布,以及与每一个外部参与方的对账。
Starone 提供的是工程服务。我们不持有金融牌照,也不声称获得任何监管批准。系统的设计用于支持运营该系统的持牌机构履行其义务,包括记录保存、审计、客户尽职调查和报告。哪些义务适用,由客户及其合规与法律顾问判定。
难在哪里
工程挑战
重复交易与丢失交易
网络可能在请求与响应之间发生故障,调用方因此无法知道付款是否已经发生。不加保护地重试会让客户被扣款两次;不重试则让付款悬而未决。每一个涉及资金划转的操作,都需要一个幂等键,使重复请求返回最初的结果。
无法解释的余额
直接更新余额字段会丢失历史,并且在两笔交易同时触及同一账户时引发竞态条件。数字出错时,没有任何东西可供审计。余额必须由不可变的账本分录推导得出,任何缓存的余额都应视为一个可验证的投影。
结果不确定的外部服务商
卡处理机构、银行和支付网络以异步方式响应,会超时,会乱序发送 webhook,偶尔还会在几天后冲正某个结果。把服务商调用简单地当作成功或失败,就会产生卡住的和对不上的付款。每笔支付都需要一个状态机,显式建模“处理中”“未知”和“已冲正”这些状态。
拖到月末才做的对账
内部记录、服务商报告和银行对账单,以不同的格式、在不同的时间描述同一笔钱。如果只是定期手工比对,差异就会不断累积,成因也无从追查。对账需要自动化并持续进行,每一个未匹配项都作为被跟踪的异常提出来。
热点账户上的并发
商户结算账户或手续费账户,可能被全部交易中的很大一部分触及。简单的行锁会让它成为瓶颈,不够谨慎的乐观并发方案又会允许透支。账本设计必须有意识地处理争用,同时仍然防止余额跌破其限额。
关键路径上的合规检查
名单筛查、身份验证和交易监控依赖第三方服务,而这些服务有各自的延迟和故障模式。如果它们是事后拼接上去的,一次服务中断要么阻塞所有客户,要么让交易在未经检查的情况下悄悄放行。流程必须定义当某项检查缓慢、不可用或返回疑似命中时该怎么办。
我们如何构建
架构
平台以只追加的复式记账账本为核心,支付编排、服务商集成和合规服务围绕它构建,并通过可靠发布的事件相互连接。
- 01
复式记账账本服务
每一个财务事件都记录为一笔交易,其借方与贷方分录合计为零,在 PostgreSQL 这类关系型数据库中,以可串行化隔离级别或经过审慎设计的加锁事务原子写入。分录从不更新或删除;更正通过新增冲销分录完成。金额以最小货币单位的整数存储,并显式标明币种,绝不使用浮点数。
- 02
幂等 API 层
所有变更类接口都要求调用方提供幂等键。幂等键、请求指纹和最终响应,与业务效果在同一个事务中存储,因此重试会返回已保存的结果,而用同一个键提交不同的请求内容则会被拒绝。同样的原则也适用于入站 webhook 和消息消费者。
- 03
支付编排
每笔支付都是一个持久化的状态机,具有已创建、已授权、已请款、已结算、已失败和已冲正等明确定义的状态转换。编排器驱动状态转换,通过适配器调用服务商,并在未收到响应时安排状态查询。在发起外部调用之前,先在账本中冻结资金,再依据结果释放或确认。
- 04
服务商与支付通道适配器
银行、卡处理机构、支付网络和钱包体系各自由一个适配器包裹,对它们的 API、错误码和 webhook 格式做归一化处理。适配器负责超时、带退避的有限次重试和熔断。在商务安排允许的前提下,路由规则可以在某个服务商劣化时把流量导向备选服务商。
- 05
客户准入与合规集成
客户准入是一个工作流:收集数据,调用身份验证和制裁名单筛查服务商,并把不确定的结果转入案件队列交由人工审核。交易监控消费事件流,依据可配置的规则发出告警。工作流为服务商超时定义了明确的行为,因此“暂挂还是继续”是一项有记录的策略。
- 06
对账引擎
服务商结算文件、银行对账单和内部账本分录被摄取、归一化,再依据可配置的规则按参考号、金额和日期进行匹配。匹配成功的条目自动关闭。未匹配和部分匹配的条目成为异常,各有负责人和账龄计时;数据源能支持多高的频率,引擎就以多高的频率运行。
- 07
审计、报表与事件流
状态变更通过事务性发件箱发布到 Kafka 这类持久化日志,由它向报表存储、监控和下游系统供数,而不给账本数据库增加负载。不可变的审计记录保存了谁在何时做了什么,包括运营人员的操作。监管报表和管理报表由这些存储生成,并可追溯到源分录。
渠道
- Web
- 移动端
- 合作方 API
- 批量文件
API 网关
- 身份认证
- 限流
- 幂等键
- 请求签名
交易服务
Java 与 Spring Boot,无状态,水平扩展
- 支付发起
- 授权
- 账本记账
- 限额与风控
Kafka 事件主干
- 交易事件
- Outbox 中继
- 分区主题
- 死信主题
账本数据库与 Redis
账本是权威数据,缓存不是
- 复式记账账本
- 只读副本
- 余额缓存
- 分布式锁
对账与审计
- 结算匹配
- 差异检测
- 不可变审计日志
- 报表导出
监控
- 延迟与错误率 SLO
- 消费者滞后
- 链路追踪
- 业务告警
一次合作交付什么
- 账本设计,含会计科目表与过账规则
- 带公开契约的幂等支付与钱包 API
- 支付状态机与服务商适配器
- 与 KYC 及筛查服务商集成的客户准入工作流
- 带异常管理的自动化对账
- 审计轨迹与报表数据存储
- 覆盖超时、重复与冲正的故障模式测试套件
开始之前
需要尽早做出的决策
- 自建账本,还是使用现有账本
- 账本在概念上很小,在细节上却不容出错。当成熟的开源或商用账本的账户模型契合您的产品时,采用它可以节省时间。当您需要现有方案无法提供的过账规则、多币种行为或性能特征时,再自建,并为正确性所要求的测试留出预算。
- 强一致性,还是最终一致性
- 账本写入以及为其把关的余额检查必须是强一致的;这里没有最终一致性的位置。由账本派生的一切,例如对账单、通知、分析和搜索,可以是最终一致的,并由事件驱动。把这条界线划清楚,关键路径才能保持短小而快速。
- 一家支付服务商,还是多家
- 单一服务商更容易集成、对账和推理,是合理的起点。当可用性目标、地域覆盖或成本有需要时,再增加第二家。多家服务商意味着多份对账数据源、各不相同的争议处理流程,以及本身也需要测试的路由逻辑。
- 如何扩展交易吞吐量
- 首先消除争用:对热点账户批量过账,使用子账户,并保持事务简短。然后通过只读副本和投影扩展读取能力。只有当一个经过充分调优的单库被证明确实是瓶颈时,才按账户或租户对账本分区,因为跨分区转账会带来实实在在的复杂度。
常见问题
常见问题
Starone 是持牌金融机构吗?
不是。Starone 是一家工程公司。我们为已持有或正在申请相关牌照的机构设计并构建技术系统。我们不声称获得任何监管批准,牌照事宜和对监管规定的解读由客户及其顾问负责。
如何防止重复扣款?
在每一层都做到幂等。客户端请求携带幂等键,并与账本过账原子地记录在一起;调用服务商时,凡服务商自身提供幂等机制的都加以使用;消息消费者记录已处理的消息标识。重复的请求会返回最初的结果,而不是产生一笔新的资金流动。
平台能否支持银行卡数据与相关安全标准?
架构的设计目标是把暴露面降到最低,通常做法是使用服务商的托管字段和令牌化,使原始卡数据不进入您的系统。网络分段、加密、密钥管理和访问日志在设计上与 PCI DSS 等框架对齐。正式的评估与认证由运营主体会同具备资质的评估机构完成。
上线前如何测试故障场景?
通过注入故障。测试套件模拟服务商超时、重复和乱序的 webhook、过账过程中的部分失败,以及事务中途的进程崩溃,然后断言账本依然平衡,没有支付丢失或重复。对账则用故意制造差异的文件来演练。
