原作者:Voytek Pitula。本文迁移自《金融科技工程手册》,保留原文结构与章节锚点。
《金融科技工程手册》介绍处理资金的软件系统中常用的工程模式。既可以从头阅读,建立完整理解,也可以在遇到具体问题时查阅相关章节。
适合谁?
刚进入金融科技领域的人。 用来熟悉这个领域,以及让资金系统可信的常见模式。
已经在金融科技领域工作的人。 遇到具体问题时可作为参考,也可以作为团队内部共享的词汇表。
金融科技之外的人。 用来理解资金系统和常规软件系统有什么不同,以及为什么不同。
想了解本书背景,请看附录 C。
原则
下面所有内容,都服务于三条原则:
不凭空创造数据。 资金不能凭空产生,因此不能容忍重复处理或随意改余额。我们通过幂等性、去重和对账来约束这一点。
不丢失数据。 所有和资金有关的事情都必须被跟踪并持久化。我们用完整精度、至少一次投递、事件溯源、审计轨迹和不可变性来保护这一点。
不信任。 不信任外部服务商,不信任内部组件,也不信任外部世界。我们通过验证回调、跨来源核对数据,并在假设被破坏时明确失败来做到这一点。
表示资金
在转移或记录资金之前,必须先表示资金。这一层决定金额如何建模、存储、计算和转换。一旦这里出错,上层每一层都会继承这个错误。
精度处理
资金表示是金融系统里最基础的决策之一。主要有四种方式:
浮点数。 使用内置的
float或double类型。这会产生不可预测的精度损失,几乎从来不是好选择。但它速度最快、内存效率最高,也不需要额外库或数据结构。任意精度。 Java 的
BigDecimal这类类型可以精确控制计算精度。代码行为可预测,也能由我们决定在哪里、如何舍入。它适合外汇或定价数学这类中间计算,因为这里往往会串联很多操作。最小单位精度。 对大多数法币来说,只保留固定精度通常是可以的,也就是接入的中央银行系统使用的精度。位数由 ISO 4217 描述,不能假设永远是 2。实践中,这意味着把金额以最小单位的整数存储,例如 €12.34 变成
1234。加密资产也使用同样的「整数最小单位」思路(BTC 的 satoshi、ETH 的 wei),但有两个差异:精度按资产定义,由代币自身决定(例如 ERC-20 的decimals),通常是 18 位;最终数值大小经常超过 64 位整数,因此需要任意宽度整数来保存。有理数。 用在完全不能接受精度损失的场景。这是能力最强的方式,但也有自己的问题。第一,它比其他方式慢。第二,它无法无损转换为其他格式。第三,它通常需要自定义数据类型或库。
选择哪一种取决于系统类型和职责。这里没有通用经验法则,除了不要用浮点数。这些表示方式也不是互斥的。金额如何存储、如何计算,是两个独立决策,系统经常会组合使用,例如用整数存储,用 BigDecimal 做中间计算。
金额序列化时也要同样小心。在大多数解析器中,裸 JSON 数字是 IEEE-754 double,所以把资金序列化为数字,会在系统边界重新引入浮点数问题,无论内部表示多么谨慎。发送资金时,要么使用字符串("12.34"),要么使用最小单位整数。
涉及的原则:
不丢失数据,错误表示会悄悄丢掉精度,而且无法恢复。
舍入策略
舍入不可避免。 它应该显式发生:任何除法、币种转换、手续费、利息、汇率应用,或不同精度之间的转换,都可能需要舍入。
这是业务决策。 不同舍入策略有不同影响。有时必须保守(例如不能花掉自己没有的钱),所以向下取整;有时关注统计效果,会使用银行家舍入。决定小数部分归谁,可能还有法律或税务影响。
尽量少做舍入。 保留完整精度的时间越久,就越有空间在正确的上下文里做正确决策。舍入通常应该发生在边界,例如数据持久化之前,或展示给用户之前。
舍入会破坏总和。 如果一个数被拆成多个部分并分别舍入,各部分之和可能不再等于原数。根据上下文,这可能需要显式处理,例如使用一个明确的舍入账户。
涉及的原则:
不丢失数据,余数必须被跟踪,不能被丢掉。
不凭空创造数据,舍入绝不能创造原本不存在的钱。
币种处理
资金不能只用数字表示,它总是和币种成对出现。处理币种时有一些细节。
把金额和币种绑定在一起。 一个
Money类型(结构体、类、记录等)可以减少出错机会。禁止跨币种计算。 系统应该禁止把两个不同币种的金额相加。转换必须非常显式,并使用严格受控的汇率。
使用受控的币种集合。 可以是自定义配置项、JDK 数据库、专门的服务。不要接受任意币种代码,要在系统边界验证。
代码只适合识别法币。 币种代码只有在法币场景下才唯一、可作为标识符。对加密货币,需要更复杂的方式,例如
(network, contract address)或类似结构。币种带有元数据。 符号、精度、名称等。展示时通常需要这些细节,但业务逻辑很少需要。
锚定不等于底层资产。 锚定、跨链桥接和封装后的加密货币,不等同于它们背后的底层资产。
涉及的原则:
不信任,在边界用受控集合验证币种。
不凭空创造数据,把不同币种或资产当作可互换,会凭空变出价值。
外汇汇率
外汇汇率允许我们在不同币种之间转换资金。
汇率总是有方向的。 EUR/USD 汇率和反向 USD/EUR 汇率不是一回事。在交易市场上,买入和卖出是两个价格不同的订单(买卖价差),所以两个方向不能简单互为倒数。
汇率的时间很关键。 技术上可以使用任意时间点的汇率,但最常用的是:
当前汇率,用来计算当前持仓价值,或把一笔交易当作刚刚发生来计算价值。
价值日汇率,用来计算价值变化或税额。
转换需要关注两类汇率:
交易汇率,真实转换发生时的汇率。不一定要直接存储,可以从原始金额和结果金额推导出来。
参考汇率(市场中间价或央行汇率),用于估值和等价计算,例如当前持仓值多少钱,或价值日上的计税基础,而不是任何人真正交易的价格。
不存在标准汇率。 汇率来自市场,会因交易场所或计算方法不同而变化。最接近标准的是央行汇率,但它也只能作为参考汇率。即便如此,也可能存在同样有效的替代来源。
涉及的原则:
不丢失数据,保留金额。对于参考汇率,还要保留能追溯到来源的方式。
不信任,不存在标准汇率,因此来源应该是数据的一部分。
记录资金:账本
资金一旦被表示出来,资金流动就必须以一种可平衡、可审计、多年后可重建的方式记录下来。账本、时间戳和历史都在这一层。
复式记账
复式记账是一种广泛使用的金融交易存储方式,把交易保存为一组形如 (credit account, debit account, amount) 的分录(这是压缩形式,经典表示会为每次资金变动分别记录借方行和贷方行)。由于每个分录都把同一笔金额从一个账户移到另一个账户,账本始终平衡,钱只会移动,不会被创造或销毁。
钱总是有来源和去向。 外部服务商也会有专门账户,因此进出系统的资金仍然被跟踪。
余额从不存储。 它从资金变动推导出来。
账户有类型。 资产、负债或权益,因此 会计恒等式(
assets = liabilities + equity)成立,并且每个账户都有定义好的增加方向。实践中还需要收入和费用账户,例如把手续费记作收入,或把坏账核销记作损失(assets = liabilities + equity + revenue - expenses)。一笔交易,多个资金变动。 单笔交易通常会创建多个资金变动,例如一个表示净额,另一个表示手续费。
已过账分录不可变。 惯例上,修正通过新增抵消分录来抵消原记录。
涉及的原则:
不凭空创造数据,钱只在账户之间移动,总量守恒。
价值时间、入账时间与清算时间
交易通常至少有两个时间戳,有时有三个:
价值时间,交易发生的时间。
入账时间,交易被记录进系统的时间。
清算时间,钱实际转移或落地的时间。不是每笔交易都有。通常表示为 T+X,其中 X 是价值时间之后多少天发生清算(例如 T+2 表示价值时间之后 2 天)。
前两个几乎总会不同:
回溯记账(入账时间晚于价值时间)。技术上几乎所有交易都是回溯记账,但当入账时间和价值时间落在不同报告期(例如日、月、年)时,这个概念影响最大。
未来生效(入账时间早于价值时间)。不那么常见,但会发生在预约付款或未来日期付款中,例如今天记录的定期付款指令,下周才生效。
比如:一笔银行卡付款在 T1 发生(价值时间),系统在 T2 记录它(入账时间),支付服务商在 T3 把钱转入结算账户(清算时间)。
业务报告通常关心价值时间或清算时间,而入账时间对可追溯性有用。
涉及的原则:
不丢失数据,记录每个相关时间戳;把它们压成一个
created_at会丢掉以后无法重建的信息。
审计与审计轨迹
金融系统会受到各种形式的审计和监管审查。审计期间可能会验证的事情包括:
公司资金是否没有和用户资金混同,也没有用于公司开支?
所有收入是否都已登记、报告,并且可以解释?比如,能否定位到某个期间内贡献某条收入流的交易?
提供给外部世界的信息(例如提供给用户或税务机关的信息)是否符合现实?比如,公司持有的资产是否和欠用户的金额相匹配?
资金是否受到外部威胁保护?例如,谁可以访问资金,以及如何访问?
为了回答这些问题和许多其他问题,金融系统不仅要跟踪当前状态,还要跟踪这个状态如何形成的完整历史。这段历史就是 审计轨迹:一份足够详细的记录,能解释并重现任何余额、报告或决策。
一个有用的审计轨迹会为每次变更记录:
发生了什么。
什么时候发生。 见价值时间与入账时间。
谁或什么触发了它。 例如用户、操作员、自动任务。
为什么发生。 例如导致它的订单、指令或事故引用。
资金变动显然需要记录,但人工干预、配置变更(手续费表、汇率来源、限额)和权限变更也需要轨迹。
为什么发生 经常是某个决策的输出(例如合规检查或风险评分)。只记录结果(如 blocked)通常无法满足审计,审计人员还会追问结果是如何得出的。如果这段逻辑位于决策表或规则引擎(DMN、Drools、Decisions4s)里,而非埋在命令式代码中,决策就会变成一个结构化、可重放的产物,说明哪些规则在什么输入上触发,并得到什么结果。
涉及的原则:
不丢失数据,只有当前状态无法回答审计问题,完整历史才可以。
事件溯源
事件溯源是一种系统化构建审计轨迹的方式。它只存储事件,再从事件推导状态,不会把当前状态和日志分开维护。复式账本就是这种模式应用到资金上的例子:余额从已保存的分录计算出来。用这种方式,审计轨迹就是主要产物,不会偏离现实。
几个实践注意点:
不需要处处使用。 账本已经覆盖资金;周边领域用常规模型加可靠变更日志可能就够了。
派生状态可以缓存。 余额和投影可以缓存,也可以做快照,以提升性能。
投影很费工。 系统可能需要很多投影,而且无法有效直接查询主要数据集(事件)来回答各种问题,因此需要构建专用或通用投影来查看数据。
提前规划结构演进。 事件会存在多年,因此今天的代码仍然必须能读取很久以前写入的事件。
当系统需要审计轨迹时,事件溯源是很好的方案,但它会显著增加系统复杂度。
涉及的原则:
不丢失数据,当状态从事件推导出来时,轨迹不会和现实不同步,因为它就是事实来源。
不可变性
可编辑的审计轨迹证明不了任何事,因此记录不能更新或删除。日志必须只能追加,所有修正都应该是一条新记录(见下文)。
不可变性是一个不变量,常用工具同样适用:
从构造上保证。 只追加表,在数据库权限层撤销
UPDATE/DELETE。运行时检查。 应用层不暴露对已过账记录的修改操作。
事后检查。 防篡改证据:对记录做校验和或哈希链,并定期验证,使任何事后修改都可检测。
构建真实系统时,bug 不可避免,可能需要修复事件日志或审计轨迹。这种情况下,有时直接原地更新轨迹比严格保持不可变更容易。为了平衡这两者,理解报告节奏和报告义务很重要。通常只有数据被报告之后,才必须像刻在石头上一样不可改,例如月底财务报表已经对外共享。在此之前,如果问题能在数据离开系统前修复,仍然可以考虑原地修改数据。
涉及的原则:
不信任,可编辑历史证明不了任何事;不可变性和防篡改证据让轨迹对外部人员可信,也让调查人员能够信任它。
冲销与修正
错误仍然会发生,例如金额记错,或交易落到错误账户。不可变性意味着只能向前修复:发布一条新的抵消分录,并在两个方向上把它链接到被修正的记录。
冲销。 完全抵消原记录,好像它在经济上从未发生过。但它会和原记录一起留在历史里。
修正(调整)。 记录「已记录值」和「应记录值」之间的差额,或先冲销再用正确值重新过账。
注意报告期。 修正经常落在和原记录不同的报告期(见价值时间与入账时间);链接关系让报告能正确归因,并区分真实业务活动和清理动作。
最后一点尤其重要。发布修正或冲销时,需要决定是否回溯事件时间(指定过去的价值时间)。这里同样高度依赖报告节奏。通常不能把任何内容回溯到已经关闭的期间,因为它已经向外部世界报告过。
涉及的原则:
不凭空创造数据,错误通过发布已链接的抵消分录来修复,用它抵消原记录。
不可变性与 GDPR
GDPR 的删除权看起来和不可变账本矛盾。实践中,这通常可以处理成非问题:
财务记录大多有豁免。 法定留存义务(会计法、AML,通常 5 到 10 年)优先于交易数据的删除请求。在这段时间内,系统不会删除过账记录。
把 PII 与财务数据分离。 豁免只覆盖法定义务要求保留的内容。不可变账本通过不透明的内部标识符引用用户,而 PII(姓名、地址、证件)放在单独的可变存储中,可独立遮盖或删除。
对嵌入式 PII 使用加密粉碎。 如果个人数据必须嵌入不可变记录(例如事件载荷),就用每个用户独立的密钥加密其个人字段,并通过删除密钥来删除数据。删除请求变成密钥删除,而不是改写历史。
涉及的原则:
不丢失数据,分离 PII 和财务数据,既能满足删除请求,又不会丢失依法必须保留的财务历史。
执行资金流程
一次资金操作很少只是一次写入。它会横跨多个步骤、并发和失败场景,并且必须在整个过程中保持正确,既不能创造钱,也不能丢钱。下面这些模式可以让单个流程保持正确,从它必须维持的不变量,到中途崩溃时如何恢复。
不变量
任何系统里都有一些必须始终成立的特殊性质,我们称之为不变量。前面提到的会计恒等式就是一个这样的不变量。业务相关方也可能定义许多类似条件,系统必须强制它们成立。
强制不变量主要有 3 种方式:
从构造上保证。 确保系统只允许创建有效对象,使无效状态无法表示。可以通过多种技术实现:工厂方法(智能构造器)、类型级编程(例如精炼类型)、数据库约束。
运行时检查。 执行逻辑时检查不变量是否成立。这可以是生产代码中的断言,也可以是测试。基于性质的测试在这里尤其适合,例如「任意过账序列都必须让账本平衡」。
事后检查。 分析系统持久化的数据,寻找任何违规,例如对账任务或夜间检查,验证账本余额仍满足会计恒等式。
这些方法相互补充,通常需要并行使用,达到所需的可信度。从构造上保证最强,但无法表达一切,尤其是跨聚合或跨系统不变量;运行时检查能在违规发生点捕获它;事后检查是唯一能捕获已经发布到生产的 bug 的方法,但发现得晚。
涉及的原则:
不信任,不变量要被验证,而不是被假设;即使是内部代码输出,也要检查。
资金预留
大多数情况下,交易需要和外部世界交互。例如,系统可能需要在允许用户提现之前运行合规检查,或在外部系统中登记提现。
这种情况下也要避免竞态条件:同一笔钱被花两次,或在外部世界交互已经发生之后才发现「余额不足」。
为了解决这个问题,系统会实现资金预留(也叫 hold-and-release):先为某个交易预留资金,然后再开始外部交互。外部交互完成后,预留被清算,交易继续;如果出错,预留被释放,资金回到可用余额。
这个模式引入了两种余额的区别:总余额(用户拥有的一切,包括已预留资金)和 可用余额(available = total - reserved)。余额检查和新的预留都基于可用余额进行,这可以防止同一笔资金支撑两笔交易。
几个实践注意点:
最终金额可能不同。 它不总是预先已知,手续费或汇率可能和估算值不同。这时先预留估算金额,再清算实际金额,并释放剩余部分。
预留必须总会结束。 一个既不清算也不释放的预留会锁住用户资金,因此每个创建预留的流程都必须保证它最终被处理。显式过期或超时可以作为安全网,但不是必需条件,也可以依赖内部系统纪律。这个失败模式是保守的:孤儿预留会锁钱,但不会丢钱或创造钱。
它需要强一致性。 检查余额并记录预留必须是线性一致的。如果基于过期读取,两个交易可能都通过检查,并用同一笔资金支撑各自支出。所以这里不适合最终一致性,抱歉。
涉及的原则:
不凭空创造数据,同一笔资金不能支撑两笔交易;预留把这一点显式表达出来,而不是依赖存在竞态的余额检查。
处理透支
当账户余额变成负数时,就发生了透支。透支有两种:
有意透支。 透支是业务明确提供的信贷产品,有额度和利息。这是业务功能,不是异常,基本不在本文范围内。它最可能被建模为单独的透支账户(对用户是负债,对运营方是应收款),并有正余额。
非有意透支。 虽然政策禁止,余额仍然变成负数。
即使在正确系统里,非有意透支也会发生,因为外部世界不会先征求许可:清算金额可能高于预留估算,或冲销在资金已经离开后才到达。资金预留缩小了透支窗口,但无法完全消除它。
禁止不等于无法表示。 很容易想把「余额永不为负」编码进类型或存储,例如使用无符号整数或 CHECK (balance >= 0) 约束。但当系统被迫接受负余额时,一个无法表示它的系统要么在流程中途崩溃,要么静默把余额钳到 0(创造钱),要么做出类似错误行为。
balance >= 0 只是一个不变量,常用工具同样适用:授权交易时在运行时强制它,用监控和对账做事后违规检测,但不要通过构造方式强制它。当检测到透支时,它是一个调查信号,但不一定是 bug。
当透支确实发生时,我们必须记录它并显式恢复,例如用未来入金抵扣、要求还款,或把它核销,作为一条指向费用或损失账户的显式抵消分录。
涉及的原则:
不凭空创造数据,把负余额钳到 0 会凭空造钱。
不信任,无论系统检查得出什么结论,外部世界都可能强迫透支发生。
幂等性
在分布式系统中,无法保证恰好一次投递。任何调用都可能中断,而我们不知道它是否到达另一端。为了确保消息送达,必须重试每个这样的调用。但重试会带来重复投递风险,因此处理过程必须幂等,同一条消息投递两次,也只能触发一次处理。
优先使用显式键。 幂等键相比业务派生幂等(例如基于载荷去重)通常更简单、更好。根据数据推导键很脆弱,例如很难判断两笔金额相同的交易是重复请求,还是两次真实操作。使用幂等键时,要确保它按具体操作和客户端范围划分。
决定错误如何重放。 如果第一次调用失败,重试时应该重新抛出已存储的错误,还是重新触发处理?通常把错误视为幂等结果并重放,会更简单、更容易推理。客户端总可以用新键重试。这里很大程度取决于错误性质。永久错误(例如校验失败)应该原样重放,而临时错误(例如网络故障)可能可以重新处理。
验证重复载荷。 一个好实践是确保重复调用携带和原始调用相同的载荷。实践中这成本很高,只多买到一点信心,却让实现更复杂、灵活性更低(调用方可能有正当理由修改请求)。
规模上很难。 构建可靠幂等性可能很复杂,需要投入足够精力。系统可能需要去重数十亿请求,还要在并发访问下保证行为正确(例如两个重复调用在同一毫秒到达)。幂等屏障必须是原子的。
注意时间窗口。 只在 24 小时内去重这类幂等时间窗口会显著简化实现(否则数据量会永远增长),但也会牺牲正确性。只有在绝对必要时才做这种取舍,因为它会长期影响系统行为。
测试重试。 一个较好的方式,是在集成测试或系统测试中嵌入通用中间件,让它自动重复每个调用。
处理乱序重试。 即使系统已经进入新状态,也必须保持幂等。例如,即使资金已经释放,把资金置为冻结状态的操作也要保持幂等。
幂等性对发起方和接收方都重要。每次消费或暴露一个操作,都要考虑它。
涉及的原则:
不凭空创造数据,重试不可避免,因此处理过程必须把重复投递折叠成单个效果,而不是把钱移动两次。
完整可恢复性
资金流程很少只发生在一个步骤中。提现可能预留资金、运行合规检查、在外部系统中登记操作,最后清算。这样的序列跨越时间,可能死在任意两个步骤之间,所以安全假设是它一定会这样:每两个步骤之间都假设可能失败。因此,一个流程绝不能假设自己会一次性跑完;半完成的流程必须始终落在可恢复状态,而不是不一致状态。
持久化进度,不要把它放在内存里。 把流程建模成显式状态机,其状态要持久保存,并且在开始下一步前提交当前步骤完成。重启后必须能准确知道流程走到哪里。
必须有东西恢复卡住的流程。 一个独立驱动器(调度器、工作进程或轮询器)必须拾取未完成流程并推动它继续。编排器崩溃不能让流程永远搁置。
每个步骤都必须可以安全重跑。 恢复时可能重新执行一个已经部分发生的步骤,因此每个步骤都必须幂等(见幂等性)。
向前推进或补偿。 外部效果不能回滚。一旦调用了外部世界,就不能撤回这次调用,数据库回滚也撤不回外部效果。所以要么一路向前重试直到流程完成;要么在后续步骤永久失败时,发布补偿动作来撤销之前的动作(saga 模式)。
可以使用持久执行引擎(例如 Temporal、Camunda、Workflows4s、AWS Step Functions),也可以自行实现持久状态机。
涉及的原则:
不丢失数据,流程中途崩溃绝不能丢失在途资金的踪迹;持久化进度让流程可以被接手并完成。
不凭空创造数据,恢复会重跑步骤,因此步骤必须可以重复应用而不重复计数,流程最终只完成一次。
外部世界
与外部世界交互不可避免,不管它是第三方服务商(支付、KYC、AML、银行、托管方等),还是内部服务。我们的工作是构建一个系统,使它在这些依赖多么不可靠时都能保持正确。
调用 API
系统迟早需要调用外部 API,例如支付服务商、托管方、区块链节点或 KYC 供应商。外部依赖的代码、质量和可用时间都不受控制,因此安全默认值是:假设它会出问题,并围绕它做防御式设计。
不要信任结构定义。 响应不一定总是符合对方提供的契约:字段可能消失,类型可能变化,本不该出现
null的地方可能出现null。在边界验证重要部分,对任何未预期内容明确失败,避免畸形数据泄漏进系统。同时,无需验证业务不使用的部分,否则第三方违反契约时可能导致不必要的故障。第三方迟早会违反契约。预期不完美的工程实践。 只要时间足够,可疑的工程实践都会出现:令牌放在 URL 里、精度丢失、HTTP 状态码不表达真实含义(
200携带错误体)、分页不一致、自定义日期格式。这些问题属于外部集成工作的一部分。所有调用都会失败。 设计系统时,要让它能处理没有响应的情况。重试和超时是必要保护。
熔断器通常可选。 它们大多是对过载服务端的保护,而成本由客户端侧复杂度承担。合理预期是服务端自己处理负载,并丢弃无法服务的请求。熔断器也能保护客户端延迟和有限资源(线程、连接等),确实需要时再使用。
注意配额。 速率限制和用量配额很容易被忘掉,却可能导致糟糕的周末故障。最好提前做一点粗略计算(预期调用量对比服务商限制),在它造成问题前发现。
存储每个请求和响应。 这听起来过度,但外部 API 开始返回异常内容时,这些记录可能成为调查依据。把发出的请求和收到的响应以结构化、可查询的形式持久化(例如 Redshift 表)。这也会成为审计轨迹,是和服务商争议其行为时的证据,也是 bug 之后重新处理的材料。
争取服务商冗余。 对最关键部分,可以考虑为同一目的使用多个服务商。服务商不能被完全信任,因此在风险最高的地方,可以用多个来源验证数据(例如两个区块链节点),或准备备用银行伙伴、加密资产托管方、KYC 供应商。这个方案成本很高,包括开发、费用和复杂度,但可能是达到所需可靠性等级的必要条件。
不要信任沙盒。 服务商提供测试或沙盒访问是一个好信号。这些环境适合基本场景,但通常和生产设置明显不同。生产测试仍然必要,例如通过金丝雀发布和小影响范围的受控使用完成验证。
涉及的原则:
不信任,服务商的代码、结构定义和可用时间都不受系统控制,因此要用独立来源验证事实,并在边界验证一切。
不丢失数据,持久化每个请求和响应,可以保留一份能用于对账和重新处理的记录。
处理 webhook 回调
Webhook 回调是接收外部系统信号最常见的方式,但安全处理它并不简单。这里聚焦 webhook,即系统暴露的 HTTP 端点,由外部系统携带其定义的载荷调用;许多要点也适用于其他传输方式。
不要假设顺序。 消息可能乱序到达,也可能携带过期数据,所以最后收到的 webhook 不一定代表最新状态。不要盲目用刚到的内容覆盖状态;要与已知内容核对,例如查询 API 获取当前状态。
不要假设有效。 Webhook 可能来自发行系统的次级部分,携带过期或错误转换的数据。一个好实践是忽略 webhook 内容,只把它当作触发器,用来查询 API 的权威状态。注意 API 可能最终一致,并落后于 webhook,因此触发后立即查询仍可能返回旧状态,要准备重试。
不要假设投递。 不管发行方承诺的重新投递策略多强,webhook 迟早会丢。系统必须能够处理缺失的 webhook,通常需要用独立过程修复数据完整性。见对账。
不要假设只投递一次。 同一个 webhook 会被投递多次。处理过程必须幂等。见幂等性。
快速确认,异步处理。 一旦原始事件被持久保存,就立即返回 2xx,然后异步完成后续工作。如果在线处理且耗时较长,发行方可能超时并重试,从而放大系统负载。
持久化原始载荷。 在采取行动前,先逐字节保存收到的内容。这能提高处理可靠性,也会成为服务商实际发送内容的审计轨迹。发生 bug 后可以据此重新处理消息,无需服务商重新发送。
验证调用方。 常见机制是发行方附带载荷签名,用于验证消息来源。最常见的是使用共享密钥计算 HMAC;不太常见的是非对称签名,并公布公钥。签名要基于收到的 原始字节 验证,不能基于重新序列化的载荷,因为重新序列化会改变字节并破坏签名。即便来源可信,仍需验证内容(见第 2 点)。
Webhook 只能作为「发生了某件事」的提示,不能作为「具体发生了什么」的可信记录。
涉及的原则:
不信任,webhook 是可能无序、可能丢失、可能重复的提示;要验证来源,并对照 API 确认真实状态。
不丢失数据,持久化原始事件,并通过对账发现漏收事件,避免丢失的 webhook 变成丢失的事实。
可靠通知:Outbox 与 CDC
系统经常需要可靠地通知外部世界内部状态已经变化,例如发布 Kafka 事件、发送 webhook 调用,或使用其他通道。这些通道不符合常用的事务模型,却必须保证至少一次投递。没有事务性时,会面对两类风险:
先发布,再回滚。 发布成功了,但由于网络问题没拿到响应,于是我们回滚了系统状态。
状态变更了,但没有发布。 发布确实失败了,但我们没有回滚。
教科书答案是两阶段提交或分布式事务,但由于复杂度高,而且缺少标准化和复用方式,实践中很少使用。更实际的选项是:
Outbox 模式。 一个「待发布」事件和状态变更一起以事务方式写入专用存储,再从那里可靠处理(取一行,重试直到成功)。系统先可靠保存「发布意图」,再异步处理。
变更数据捕获(CDC)。 一种自动机制,用来检测已提交到数据库的变更(通常通过追踪预写日志或复制日志),并把它们转成事件流。因为它直接读取日志,每个已提交变更都会被捕获,不会漏掉,而且应用中不需要显式发布代码。Debezium 或 AWS DMS 这类工具可以开箱实现。取舍是耦合和运维重量:原始 CDC 发出的事件形状和表行相同,需要后处理,避免把内部结构泄漏给消费者。
监听自己。 反转顺序,先发布事件(例如发到 Kafka),再从中重建我们自己的状态。
事件溯源。 事件日志已经在数据库中,所以发布只是从中读取的问题(见事件溯源)。
无论选择哪种机制,投递都是至少一次。中继或连接器可能在发布后、记录自己已发布前崩溃,重启后会再次发送。因此消费者必须幂等,并基于稳定事件 ID 去重(见幂等性)。
涉及的原则:
不丢失数据,一个已提交变更必须可靠到达消费者;outbox(或日志)保证通知不会因为单独发布步骤失败而丢失。
不凭空创造数据,我们不会为未提交变更发布通知,重复投递会折叠成单个效果。
对账
任何依赖外部数据的系统都容易发生数据漂移,即一个系统和另一个系统不一致。例如,系统可能漏掉一个 webhook,或一笔交易已经过账到账本,却没有反映在外部服务商系统中。这些情况都需要通过对账核对两个系统。实践中可能涉及账本、支付处理方和银行等多个系统,但方法不变。
频率。 根据具体上下文和约束,对账可能每小时、每天、每月,甚至每年执行。
漂移的性质。 数据可能缺失(这是简单情况),也可能不同(例如同一笔交易的金额不同,这要复杂得多)。时间也很重要:如果清算发生在 T+3,记录会保持未对账 3 天,这个逻辑应该纳入流程,避免对这些情况告警。
匹配算法。 确定两个系统之间比较什么,是最难的部分。通常应在系统内持久化外部服务商 ID,这样匹配直接明了。否则就需要启发式算法,例如按金额和时间匹配。
一对多。 有些情况下,需要把一侧多条记录和另一侧一条记录做对账,例如单笔清算转账可能覆盖多笔交易。
修复差异并不简单。 不能简单覆盖数据来让对账变绿。发现的每个差异都应该先查明原因,再通过正式流程修复,例如修正记录、重新处理 webhook 数据等。
涉及的原则:
不信任,对账让我们在独立来源之间验证,而不是相信任何单一来源正确。
不丢失数据,对账是保障机制,能在缺失 webhook、未清算转账这些事实永久消失前捕获它们。
控制与访问
前面的模式让数据保持正确。但资金系统还必须限制谁可以执行操作,并在事后证明流程已被遵循。这里,不信任 原则开始转向内部:操作员和工程师也是信任边界,就像外部服务商和内部组件一样。审计人员会在审查账本本身的同时审查这些控制措施。
职责分离与双人复核
有些操作太敏感,不应该交给单个人完成,不管这个人多可信。把职责拆开是金融中最古老的控制措施之一,它有两种相关形式:职责分离(没有一个人拥有完整流程)和 双人复核 / maker-checker(某个具体操作在生效前需要第二个人批准,也叫双重控制)。
它适用于资金操作。 大额或人工提现、人工账本修正、资金库和冷钱包转移、修改手续费表或限额,任何能移动资金或误报资金的操作,都可能需要第二个审批人。
它也适用于工程。 合并代码、部署到生产和修改基础设施,在资金系统里也是敏感操作。因此我们通常要求代码评审和审批。
审批是轨迹的一部分。 记录谁请求、谁审批,并且两者是不同的人。否则这个控制措施无法证明(见 审计与审计轨迹)。
紧急通道需要路径。 紧急情况会发生,过于僵硬的控制措施会诱使人绕过它。提供一个显式、重审计的覆盖路径,而不是逼出后门。
涉及的原则:
不信任,单个内部参与者即使可信,也不足以授权敏感或不可逆操作。
访问控制
谁能做什么,本身也是系统状态的一部分,而且会随着人员加入、转组和离开而变化。只知道今天谁能触碰资金还不够;审计人员还会问他们是如何获得这些访问权限的。
最小权限。 给每个参与者(人或服务)授予最低必要权限,并优先使用角色(RBAC),而不是按人单独授权,让访问权限保持可评审。
授权变更需要轨迹。 授予或撤销一项能力是敏感事件,和资金变动一样:记录变了什么、谁改的,以及为什么改。账本的审计轨迹纪律同样适用于这里(见 审计与审计轨迹)。
定期评审访问权限。 权限会变得过期或不准确。定期访问评审(重新认证)就是把事后检查(见 不变量)应用到访问权限上,以便捕获漂移。
涉及的原则:
不信任,长期访问权限会悄悄积累;最小权限和定期评审用来控制它。
变更轨迹(SDLC)
在受监管环境中,我们通常需要审计代码如何进入生产,因此要知道谁评审了变更、谁批准了、什么时候发布等。如果做得好,版本控制和 CI/CD 系统会非常有帮助。
源代码管理是记录。 提交历史把每个变更归因到作者,并通过评审和关联工单说明变更原因(这就是审计轨迹通常要求的 what / who / why)。因此要相应保护它,例如签名提交、受保护分支、禁止强推共享历史。
评审和流水线必须强制执行。 必需的代码评审、状态检查和「禁止直接推送 main」很关键,因为审计不接受只靠自觉的纪律。
部署可追溯。 当前运行哪个版本、谁发布、何时发布,应该可以重建。这让事故能追溯到导致它的变更。
涉及的原则:
不丢失数据,系统自身如何形成的历史,和它持有的资金历史一样,都是轨迹的一部分。
不信任,系统强制交付控制,而不是依赖人记得遵守。
测试
测试在任何地方都重要,但在资金系统里更重要。预期输出通常无法穷举,操作序列的空间太大,复杂失败都藏在组合里。下面这些方法可以帮助团队建立对系统正确性的信心,可按系统风险选择合适的技术。
基于性质的测试。 不断言具体输出,而是断言某个性质对任何生成输入都成立。这天然适合不变量或资金数学。测试框架会生成手工编写时难以覆盖的棘手用例。
步骤之间的不变量检查。 生成操作序列时,不要只在最后断言不变量,要在每一步之后断言。这很难手工大规模完成,因此需要更复杂的测试框架,自动注入断言。
生成式幂等性测试。 每个触碰外部世界的操作都必须幂等(见幂等性),因此可以把它作为系统性质。使用和上面类似的方法,自动重复所有声明的操作,并断言第二次调用不会影响系统。
崩溃与恢复注入。 长流程必须能在任意两个步骤之间死掉后继续(见完整可恢复性),我们可以用常规方法精确测试这一点:在每个步骤注入失败。
往返测试。 编码再解码,序列化再反序列化,转换后再转换回来,并断言回到起点(或落在已知容忍范围内)。这能快速发现金额 / 币种类型在边界上的精度损失和序列化 bug。它和自动数据生成配合很好。
黄金测试。 把计算或投影(手续费拆分、账单、报告)的输出固定到已存储的预期结果,这样任何非预期变化都会显示为差异。它适合棘手、难推理的计算,因为经过评审的结果通常比刚写的新断言更可信。
向后兼容测试。 事件和已存储记录会存在多年,今天的代码必须仍能读取旧代码写下的内容(见事件溯源)。保留一组真实的旧格式载荷,并断言当前代码仍能正确反序列化和投影它们,这可以防止结构变更悄悄破坏历史。
生产环境测试。 有些信心只有面对真实系统才能获得。服务商沙盒和生产差异很大(见调用 API),因此集成是否工作,最终往往必须在线证明,例如通过金丝雀发布、小影响范围的受控上线,或持续用小额真实资金流经系统的合成交易做健康检查。资金系统的特殊注意点是:这些都是真实资金变动。生产测试移动真实资金,因此必须和其他一切一样经过同一套账本、对账和审计轨迹,清楚标记,并通过正常修正 / 冲销机制清理,绝不能用绕过账本的后门。
涉及的原则:
不信任,测试用来验证这些模式确实成立,而不是假设它们成立;不变量才是判定标准,不是某个偶然预期值。
不凭空创造数据,重放操作和注入失败可以证明重试与恢复不会重复计数或凭空造钱。
不丢失数据,往返测试和向后兼容测试可以证明精度和历史能穿过边界,也能经受时间流逝。
附录 A:了解你的领域
进入金融科技领域时,背后的词汇和概念往往比代码更难掌握。这个领域充满了听起来普通、但含义精确的词,也有许多频繁使用却很少展开解释的缩写。
说明:普通人已经知道的术语(入金、提现、转账、币种)会跳过,只有遇到时再学的偏门角落也会跳过。这里尽量聚焦最重要的术语。如果手册正文已经充分讲过某个概念,该条目会链接到对应章节,而不是重复解释。
会计与账本
账本(ledger):资金变动的记录系统;余额从它推导出来,它是事实来源(见 复式记账)。
总账与分账:单一汇总账本,与某个领域的详细账本(例如每个用户或产品一个),后者会汇总到前者。
借方 / 贷方:每个分录的两侧。哪一侧会增加一个账户,取决于账户类型,不取决于「钱进还是钱出」(见 复式记账)。
过账:把分录提交到账本;
posted表示已记录,并按惯例不可变。会计科目表:可以过账的账户目录;一个系统可以有多个科目表(例如按法律实体、账簿或报告标准)。
账户类型:资产、负债、权益(加上收入、费用),让 会计恒等式 成立,并定义每个账户增加的一侧(见 复式记账)。
应收 / 应付:别人欠款 / 欠别人的钱。
IOU:非正式说法,指对某人负有债务的记录。托管平台上的用户余额是平台对用户的 IOU,所以它位于账本的负债一侧。
权责发生制与收付实现制:在钱被赚到或欠下时确认,与在钱实际移动时确认。
试算平衡:检查账本中借方总额是否等于贷方总额。
暂记 / 清算账户:临时持有账户,用于正在转移中或尚未归属的资金。
核销:把预计无法收回的余额记为损失(见 处理透支)。
资金混同:把公司资金和用户资金混在一起,这是监管红旗(见 审计与审计轨迹)。
对账差异:由 对账 暴露出的单个未匹配差异。
资金与外汇
Money(作为类型):金额与币种成对出现(见 币种处理)。
最小单位:币种的最小不可分单位;金额经常以这些单位的整数存储(€12.34 →
1234)(见 精度处理)。基点(bp /
bip):百分之一的百分之一(0.01%);手续费和利率经常用它报价。名义金额:计算所基于的面值,它可能远大于实际发生转移的现金。
法币与加密资产:国家发行币种与区块链原生资产。
稳定币:锚定到参考资产的代币,通常是 USD 这类法币。
锚定 / 封装 / 桥接:和底层资产有连接,但不等同于底层资产的表示形式(见 币种处理)。
买价 / 卖价 / 价差:买价、卖价,以及二者之间的差。
市场中间价:买价与卖价之间的中点;它是参考点,不是实际成交价格(见 外汇汇率)。
参考汇率:用于估值和等价计算的汇率(持仓价值、计税基础),不是实际交易汇率(见 外汇汇率)。
按市值计价:用当前市场价格,而不是买入价格,对持仓重新估值。
交易、时间与清算
价值日 / 入账日 / 清算日:发生时间 / 我们记录时间 / 钱实际移动时间(见 价值时间、入账时间与清算时间)。
T+X:某件事(例如清算)在价值日之后 X 个工作日发生(例如 T+2)。
轧清与清算:确定谁欠谁什么,与实际转移资金。
截单时间:每日截止时间,超过后交易进入下一个清算窗口。
在途资金:钱在转移过程中看起来同时存在于两个系统中(或两个系统中都不存在)。
净额结算:把多项义务抵消成单笔净额转账,而不是逐笔全额清算。
回溯记账:指定早于入账日的价值日(见 价值时间、入账时间与清算时间)。
冲销 / 修正:完全抵消一条过账,好像它在经济上从未发生;与记录「已记录值」和「应记录值」的差额(见 冲销与修正)。
支付、通道与银行卡
支付通道:支付经过的底层网络(SEPA、SWIFT、ACH、卡组织网络、区块链)。
IBAN / SWIFT / SEPA / ACH / FPS / CHAPS / wire:在银行之间移动法币的标识符和网络。
发起方 / 受益方:转账的发送方 / 接收方。
PSP(支付服务商):把商户连接到一个或多个支付通道的供应商。
Nostro / vostro 账户:我们存在对方银行的钱 / 对方存在我们这里的钱,是让跨银行转账运转的账户。
综合账户:一个资金池账户,把多个用户的资金放在一起,并在内部跟踪每个用户的余额。它是合法资金池,不是资金混同。
FBO(for benefit of)账户:公司代表用户持有的账户。
自动划转:按计划在账户之间自动移动余额(例如进入冷存储或计息账户)。
拒付:由持卡人的银行发起的银行卡付款强制冲销。
发卡行 / 收单行:持卡人的银行 / 商户的银行。
交换费:收单行为每笔银行卡交易支付给发卡行的费用;它构成银行卡处理成本的大部分。
授权与请款:对资金放置冻结,与实际扣款;这是银行卡世界里的 资金预留。
催收重试:为恢复失败的周期性付款而执行的重试和通知流程。
交易与市场
订单簿:每个价格档位上未成交买单(bid)和卖单(ask)的实时列表。
市价单与限价单:立即按最佳可用价格执行(吃掉流动性),与只在指定价格或更优价格执行(挂在订单簿上,提供流动性)。
挂单方 / 吃单方:挂单方向订单簿增加静置订单,吃单方跨越价差并移除一个订单;费用通常不同。
滑点:预期价格和实际成交价格之间的差。
流动性 / 深度:在价格发生变化前可以交易多少;薄订单簿更容易移动价格。
现货:买入或卖出资产本身并立即交割。
衍生品:价值来源于底层资产,而不是资产本身的合约。
期货 / 永续合约(perp):未来某日交易的合约 / 没有到期日、通过资金费率贴近现货的合约。
资金费率:多头和空头之间的周期性付款,用来把永续合约价格绑定到指数。
多头 / 空头:价格上涨时获利 / 价格下跌时获利的仓位。
杠杆 / 保证金:控制大于自己资本的仓位 / 为开仓并维持仓位提供的抵押品。
强平:当保证金低于维持要求时强制关闭仓位。
折扣率:对抵押品价值应用的折扣,用来缓冲价格波动。
交易对手方:交易或合约的另一方;交易对手方风险是对方未能履约的风险。
AUM / AUC:管理资产规模(平台主动管理的客户资产,通常带收费授权)与托管资产规模(平台只是保管的客户资产)。同一批持仓可以属于其中之一,也可以两者都属于。
托管与加密资产
托管:谁控制资产:自托管(用户持有密钥)与托管式(平台或托管方持有密钥)。
热钱包 / 冷钱包:在线保存、便于快速访问的密钥,与离线保存、用于安全的密钥。
私钥 / 公钥 / 地址:授权花费的秘密 / 由它派生出的公开标识符 / 接收资金的地址。
助记词:可重建钱包密钥的人类可读备份。
多签 / MPC:要求多个密钥(或密钥分片)才能授权转账,因此单个设备无法单独移动资金。
Gas / 网络费:为了让交易被包含进区块链而支付的费用。
确认 / 终局性:在包含该交易的区块之上继续构建的区块 / 交易无法再被逆转的时点。确认越多,终局性越强。
重组:区块链重组,可能撤销最近已确认的交易;这也是终局性不是即时的原因。
内存池:等待被包含进区块的待处理交易池。
UTXO 与账户模型:Bitcoin 风格的「花掉整枚 coin,收到找零」,与 Ethereum 风格的运行余额;两者需要不同的会计处理。
Token 与 coin:发行在链之上的资产(例如 ERC-20),与链的原生资产(BTC、ETH)。
Dust(粉尘):小到网络费超过其价值的金额。
地址白名单(allow-listing):限制提现只能发往一组预先批准的地址。
合规与监管
KYC:Know Your Customer,验证用户身份。
AML / CFT:反洗钱 / 打击资助恐怖主义,用于发现和防止非法资金的控制措施。
制裁筛查:检查某一方是否出现在受制裁实体名单上。
PEP:政治公众人物,需要额外尽职调查的高风险客户类别。
SoF / SoW:资金来源 / 财富来源,证明客户的钱从何而来。
Travel Rule(旅行规则):对超过阈值的转账分享发起方和受益方信息的要求。
VASP:虚拟资产服务提供商,交易所或托管方这类加密业务的监管标签。
MiCA:欧盟加密资产市场监管法规。
职责分离:把敏感操作拆给多人,使单个人无法独立完成(见 职责分离与双人复核)。
双人复核 / maker-checker / 双重控制:敏感操作生效前需要第二个人批准(见 职责分离与双人复核)。
最小权限 / RBAC:只授予每个参与者必需的最低访问权限 / 通过角色管理,而不是按人单独授权(见 访问控制)。
变更管理:代码和配置进入生产的受控、可追溯流程(评审、审批、部署)(见 变更轨迹(SDLC))。
审计 / 审计轨迹:外部审查账本和控制措施是否反映现实 / 已记录历史,让任何余额或决策可以重现(见 审计与审计轨迹)。
资源
没有一本书能端到端覆盖资金系统,所以下面的列表按层次分组。每个条目都会说明覆盖内容和适合人群,便于按知识缺口选择。
会计与账本
Accounting for Computer Scientists(文章,免费在线阅读):把复式记账映射到图和数据模型,写给工程师,而不是会计师。
The Accounting Game: Basic Accounting Fresh from the Lemonade Stand:通过一个持续展开的柠檬水摊例子,从第一性原理讲会计;不要求金融背景。
Modern Treasury, How to Scale a Ledger(系列文章,免费在线阅读):从软件工程角度构建生产级账本。
支付与银行卡
Payments Systems in the U.S.:以参考手册风格介绍钱如何在银行之间移动(银行卡、ACH、电汇、支票)。偏美国,内容全面但有点干。
The Anatomy of the Swipe:解释从刷卡到钱到账之间发生什么,面向构建者和初学者。
市场与交易
Trading and Exchanges: Market Microstructure for Practitioners:解释订单簿、挂单方 / 吃单方、价差从何而来;一本长而细的实务参考书。
加密资产
Mastering Bitcoin 和 Mastering Ethereum:面向金融科技工程师经常遇到的两种模型(UTXO 和账户模型)的工程级参考。偏技术,面向开发者。
工程部分
Designing Data-Intensive Applications:从系统角度讲幂等性、日志、一致性,以及本手册反复提到的失败模式。
KYC 与 AML
这些书面向合规专业人士。需要了解合规领域本身时适合阅读,不适合作为工程集成指南。
Anti-Money Laundering in a Nutshell:一本短小的认知级入门书,介绍洗钱是什么,以及发现和报告如何工作。
Mastering Anti-Money Laundering and Counter-Terrorist Financing:更重的实务指南,讲如何构建 AML/CTF 框架,包含检查清单和示例文档。
附录 B:端到端示例
正文逐个介绍模式,这有助于聚焦,但不利于建立整体理解和直觉。本附录会走过几个常见流程,它们经过简化但仍有代表性,接近真实系统中的处理方式。生产实现会有更多步骤、更多失败分支和更多记账处理,但简化版本足以传达大体思路。它们覆盖资金移动的三个方向:流出系统(提现)、流入系统(银行卡入金)、以及在系统内部移动(转换)。
流程 1:加密资产提现
用户请求把 0.5 ETH 提现到外部地址。这是三个例子里最丰富的一个,因为资金通过不可逆的外部效果离开系统。一旦链上确认发送,就无法撤回。
请求带着幂等键到达。 客户端可能重试提交(网络吞掉了响应、用户双击),所以第一件事是确保同一个请求投递两次只创建一次提现,而不是两个(见 幂等性)。键以当前用户和当前操作为范围。
任何不可逆事情发生前,先预留资金。 系统针对用户的可用余额预留 0.5 ETH 加估算网络费(见 资金预留)。余额检查和预留是一个线性一致的单步骤,否则两个并发提现可能都通过检查,并用同一批 coin 支撑各自支出(见 不变量)。从这里开始,用户的总余额仍包含已预留金额,但可用余额不包含。
合规关口运行,而且流程可能在这里停几天。 广播前,系统会筛查交易(制裁、AML、目标地址)。这里会组合使用多个模式:
交易被广播到链上。 通过检查后,系统通过节点签名并广播,这是另一个外部调用,受到同样的注意事项影响。这个步骤必须幂等:崩溃后恢复时必须重新检查链上状态,不能盲目第二次广播(见 幂等性,尤其是第 7 点乱序重试)。真实网络费事先未知,因此需要预留估算值,之后再清算实际金额并释放剩余部分。
等待终局性,然后过账到账本。 一个确认还不是结束,重组可能撤销过早标记为「完成」的发送,所以要等待足够多确认。之后才过账复式资金变动:借记用户账户,贷记外部链上账户(外部世界也有账户),把网络费记到费用账户,把服务费记到收入账户(见 复式记账)。
夜间任务对链上做对账。 独立于上面的流程,一个任务会比较账本、链上事实,以及节点记录的交易状态(见 对账)。它可以捕获从未确认的广播,或最终手续费和入账金额的不一致。
如果实际网络费高于预留估算,清算可能把账户推成负数。系统要记录透支并显式恢复(见 处理透支)。如果流程在广播(步骤 4)和确认(步骤 5)之间崩溃,可恢复性加幂等性会让它通过查询链上状态接着走,避免第二次发送资金。
流程 2:银行卡入金
用户通过支付服务商(PSP)用银行卡付款给账户充值。资金正在流入,需要避免重复入账,也不能把外部信号或尚未到账的资金直接记入余额。
用户发起入金。 用户输入金额并提交银行卡信息,系统使用幂等键在 PSP 创建入金交易,因为提交同样可能重试。
授权放置冻结。 PSP 授权银行卡并放置冻结,但还没有请款。这是银行卡世界里的资金预留(授权与请款)。此时不能给用户余额入账,因为资金尚未归平台所有。
Webhook 报告
captured,系统仍要核验。 PSP 调用系统的 webhook 端点(见 处理 webhook 回调)。系统基于原始字节验证签名,持久化原始载荷,快速用 2xx 确认,然后异步处理。Webhook 只能作为「发生了某件事」的提示,系统还要查询 PSP API 获取权威状态,因为 webhook 可能乱序到达、携带过期数据、被重复投递,也可能丢失。处理过程是幂等的(见 幂等性),所以重新投递的captured只会给用户入账一次。入账通过清算账户发生。 资金处于在途状态:PSP 已请款,但还未清算到平台银行账户。因此系统通过暂记 / 清算账户记账,不能假设资金已经到账:贷记用户余额(负债,因为用户余额是平台给用户的 IOU),借记 PSP 应收款。交换费 / 处理费记为费用。入账时间是现在;清算时间在之后,即 T+X(见 价值时间、入账时间与清算时间)。
清算以批次到达,对账是一对多。 几天后,PSP 向平台银行账户清算一笔转账,覆盖多笔入金。系统把这个批次和清算账户做对账(见 对账),把一笔清算匹配到多笔交易。流程内置 T+X 延迟,因此不会对只是还没清算的交易告警。同一个任务也会捕获从未到达的 webhook。
几周后发生拒付。 持卡人争议付款,其银行强制冲销。系统不编辑原过账,而是发布已链接的抵消分录(见 冲销与修正)。它通常会落在比入金更晚的报告期,如果用户已经花掉这笔资金,还可能把用户余额推成负数(回到 处理透支)。
整个流程都不依赖顺利路径信号。Webhook 是触发器,不是事实;清算账户不会在资金实际移动前确认到账;对账用内部账本验证 PSP。每一步都落实了「不信任」原则。
流程 3:带返现的应用内转换
用户把 1,000 EUR 转成 USDC,并在这笔交易上获得一小笔促销返现。资金完全在系统内部移动,所以没有不可靠的外部通道。这个流程会考验表示层(精度、舍入、币种、汇率)和「不凭空创造数据」原则。
有方向的报价和预留。 系统提供 EUR→USDC 报价。这个汇率是独立价格,不是 USDC→EUR 的倒数,买卖位于买卖价差的两侧(见 外汇汇率)。系统预留 1,000 EUR(见 资金预留),请求和其他请求一样携带幂等键。
两侧永不相加。 EUR 和 USDC 是不同币种。事实上,USDC 通过
(network, contract address)识别,而不是裸代码,也不能和它锚定的法币互换(见 币种处理)。系统禁止跨币种计算;两笔金额之间唯一的桥,是按受控汇率显式转换。数学计算保留完整精度,只在边界舍入。 系统以完整精度计算转换,只在边界舍入一次,并明确选择策略(见 精度处理 和 舍入策略)。赚取的价差是收入,必须通过复式记账显式记到收入账户,不能让它消失在舍入余数中(见 复式记账)。交易汇率无需作为单独字段存储,可以从两边金额推导出来。之后给持仓估值时,会使用单独的参考汇率。
返现检验「不凭空创造数据」原则。 很容易把奖励当作免费余额增加,但那会凭空造钱。返现是真钱,必须有资金来源:通过正确的复式过账,从公司促销 / 费用账户移到用户余额。定义它的百分比也需要和其他所有内容一样做显式舍入决策(见 舍入策略)。
清算,然后可靠通知。 系统清算预留,过账所有带时间戳和审计轨迹的资金变动,并发布结果,通知账单、通知和分析等模块。这次发布跨越独立通道,仍然必须可靠,这正是 outbox(或 CDC,或事件日志)的用途(见 可靠通知:Outbox 与 CDC);下游消费者基于稳定事件 ID 去重,因为投递是至少一次。
返现和价差在同一条过账上方向相反。价差是用户支付给平台的钱(收入);返现是平台支付给用户的钱(费用)。两者都是真实的,都要过账本,也都要舍入。因此这笔交易必须同时满足「不凭空创造数据」(账本仍平衡,没有凭空造钱)和「不丢失数据」(每个余数都被跟踪)。
附录 C:关于本手册
作者
我叫 Voytek,是这本手册的作者。内容基于我在行业中的经验,主要围绕法币和加密资产的支付、会计、控制与资金管理。我也对交易和投资产品有一些二手接触。
写作方式
大部分内容是我亲手写的,但我也确实在不少事情上使用了 AI,例如润色、结构整理、研究和评审。
这本书是一份持续更新的文档,我计划在发现值得补充的内容时继续更新。非常欢迎反馈,可以通过任何渠道分享,但 GitHub Issues 可能是最合适的地方。
阅读预期
这本书的大部分内容都可以加一句「视情况而定」,基本上任何非平凡主题都如此。重复这类免责声明没有实际帮助,因此我会给出安全的默认建议,并提供足够上下文,让读者判断某条建议是否适用于实际情况。
本书覆盖的是我有所了解的主题,因此会留下不少空白,例如硬核交易、定价、资本市场等。但标题是泛化且带愿景的。我希望尽可能覆盖更大范围的金融科技领域,也欢迎补充重要且广泛有用的内容。
很多材料(幂等性、顺序、重试、对账)属于通用分布式系统实践,但我把它们放在这里,是因为风险高的时候这些主题至关重要,也是任何金融科技工程师都必须知道的内容。
这本书面向会独立判断的读者,不建议盲目照做。组织内部指南和实践优先于我分享的建议,不过后者也许能提供重新审视现有做法的角度。这里不提供法律、合规或财务建议。