最近在整理“financial-services”这个项目的归档文档翻出了不少当初上线时熬夜排查的记录。项目代号就叫 financial-services实际是一个面向多家业务方开放的金融服务能力平台涵盖账户、支付、信贷风控和数据服务四个域。前后做了差不多一年从第一个接口上线到扛住业务高峰过程不算惊天动地但踩的坑足够写一篇复盘。这篇文章我不打算讲什么宏大的架构理念就单纯把一个金融服务类项目从需求拆解、方案设计到线上救火的完整过程捋一遍给正在做类似方向的团队做个参考。先说清楚这个项目不是从零开始做一个银行核心系统也不是简单的支付接口封装而是把公司已有的金融服务能力统一抽象以标准接口的方式对外输出。听起来不复杂实际做起来牵涉账户体系、清结算、风控决策、数据合规、客户隐私这些硬骨头。任何一块想糊弄过去后面都会在线上以事故的形式加倍还回来。1. 需求拆解业务方口中的“一个金融服务”到底指什么1.1 先别急着画架构图把业务诉求翻译成技术能懂的边界项目启动会的场景我记得很清楚业务方提了一堆“我们要给客户提供一站式金融服务”“要做账户、要能放款、要能对接多家资金方”之类的大白话。这种需求一听就头大因为每个词都对应一整套独立的技术体系而且业务方自己也没完全想清楚期望边界。我的做法是先不开工拉着产品、运营、合规的人做了一次“能力域拆解”把模糊叙述强制翻译成五个问题客户的钱从哪来、到哪里去、怎么记账、怎么判断能不能服务、服务过程是否合规可追溯。按这五个问题业务诉求被拆成四个能力域账户与记账、支付通道、信贷风控、数据合规。每个能力域再拆出业务子项和对应的技术系统。业务诉求能力域核心系统主要交付物给客户开立虚拟账户、查余额、看流水账户与记账记账引擎、余额服务开户接口、流水查询接口对接资金方放款、还款、代扣支付通道支付路由、清结算系统放款接口、还款计划、对账文件判断客户能不能借钱、借多少信贷风控决策引擎、反欺诈预授信接口、规则配置后台所有操作留痕、数据脱敏、权限可控数据合规统一权限、审计日志日志采集、数据脱敏服务这样拆完业务方也意识到“一站式金融服务”不是一个系统而是四个系统的组合自然就愿意坐下来排优先级。技术侧则明确了第一期的范围先把账户记账和支付通道打通风控做基础规则版数据合规贯穿始终。1.2 为什么“先拆能力再谈架构”在金融服务项目里特别重要金融服务有一个特点每一笔交易都涉及真实的资金变动接口文档里一个字段的语义歧义到线上就是一笔错账。如果前期不把能力边界划清楚开发过程中就会频繁出现“这个接口到底归谁管”“这笔状态应该谁更新”的争执。以“账户”这个词为例业务方眼里是一个余额数字但在技术上至少要区分账面余额、可用余额、冻结余额而且不同的资金业务类型对这三个余额的更新逻辑完全不同。如果不把这些语义在需求阶段掰开写出来的记账代码大概率是各写各的最后对账必然不平。所以我在项目里定了条规矩任何一个能力域的接口设计必须附带一份“业务术语表”把余额、流水、交易状态、冲正、冻结、解冻这些词的定义和流转条件全部写清楚产品和技术共用一份谁改动都要走评审。这套动作看起来慢实际是给后续开发省了大麻烦。2. 账户与记账一套不能算错账的存量逻辑2.1 账户体系的选型内部账薄模型还是外部清算映射账户域是整个 financial-services 里最不性感但最不能出错的部分。我们的做法没有引入复杂的多币种、多账簿系统而是基于内部账薄模型做了一套相对轻量的记账引擎。核心思想是每一笔资金变动都产生一条流水余额不是存储字段而是由流水汇总计算出来的可缓存结果。这里有一个关键决策账户余额更新采用单账户行锁更新还是汇总流水的方式。对账时发现单账户行锁在高并发场景下会成为瓶颈尤其在营销活动日几百笔事务同时打到同一个账户上行锁等待直接拖垮事务线程。所以改成流水汇总后再做余额缓存用户高频查询走缓存对账低频任务走全量流水汇总。表结构上账户流水表是核心字段包括账户ID、变动方向、交易金额、关联订单号、发生时间、渠道编号。金额统一用分为单位的整数存储避免浮点误差。这一点在金融服务上是个老生常谈但永远有人踩的点。2.2 记账引擎的事务边界先记账还是先通知设计记账接口时遇到的最经典问题是业务方调一笔放款资金方那边已经扣款成功本地记账却失败了怎么保证两边最终一致。我们的方案是引入“本地事务 对账兜底”的双轨机制。每一次资金操作先写一张“交易订单表”状态从“处理中”流转到“成功”或“失败”。记账引擎在事务内同时更新订单状态和账户流水。对外部资金方的调用则统一通过异步消息触发不放在本地数据库事务里。这样即使中间断了订单停留在处理中状态对账任务会捞出这笔单子主动去资金方查询最终状态再补记或冲正。注意这笔逻辑在金融服务里属于底线设计任何一个账户变动如果只靠实时链路总会遇到网络抖动、服务重启、消息丢失的情况。没有对账兜底短期看不出问题月底盘账时就等着哭吧。给账户域的部分核心接口建表是一个值得参考的实践。订单表里的每笔记录都带唯一业务单号这个单号由调用方生成服务端对它建唯一索引重复请求直接返回原结果这是做幂等的第一道防线。3. 支付通道与路由把异构上游的差异关在门外3.1 路由设计的本质是做隔离数据在账户系统处理完后下一站就是支付通道。financial-services 要对接多家资金方每家返回码风格不一样有的明文返回有的只有状态码没有原因还有的异步通知会重复推送好几遍。如果业务系统直接接这些上游代码里会到处散落兼容逻辑。我们的做法是做了一层支付路由网关统一封装上游差异对外只暴露一个提交付款接口和异步状态回调。路由网关的核心是三块通道配置、分发规则、回调转换。通道配置里维护每个资金方的接口地址、密钥、超时时间、限额信息分发规则支持按金额段、业务类型、客户归属、渠道优先级组合匹配回调转换则是把上游推送的状态映射成内部统一状态。这里我踩过的一个坑上游 A 资金方的回调会把“处理中”和“成功”混在一个状态码里只在明细字段区分。最初我们的回调解析只映射了主状态码导致部分已成功的交易在系统里还挂着“处理中”。月底对账时差出几百笔全部靠人工补录极其痛苦。后来学乖了对接任何一家新通道第一件事就是把上游全部文档里的返回码枚举整理出来和内部状态码做一张完整的映射表并且写一个模拟上游的测试工具专门测试边界状态。3.2 幂等与对账回调重复和通知丢失都要防支付回调的重复入库是做支付系统的人都会遇到的经典问题。上游通常承诺“至少投递一次”所以同一个付款成功通知可能收到两三次。我们的处理策略不复杂在回调消费处建一张通知接收表以“业务单号 上游流水号 通知类型”做唯一键。第一次收到成功通知时顺便把业务单号的状态推进后续重复通知进来唯一键冲突直接返回消费成功不再触发状态流转。通知丢失也要防。上游说会重试但它重试的前提是它认为没送达。如果我们的回调接口因为自身故障一直返回失败上游会重试但如果接口故障持续很久上游的重试队列可能溢出干脆不推了。所以不能只依赖回调得有一个主动查证的定时任务。我们设计了一个补单服务每五分钟扫描一次那些发送中超过十分钟的付款单主动向上游查证最终状态再根据结果更新本地。场景保障机制核心策略上游重复通知唯一键幂等通知接收表 唯一索引上游丢失通知主动对账查证定时补单任务扫滞留单上游状态语义模糊状态映射 人工巡检维护完整状态枚举映射表金额不一致分单位整数存储金额、手续费、等等同用分存储比对4. 风控决策先追求可解释再谈高精度4.1 规则引擎为什么比复杂模型更合适第一版信贷风控是 financial-services 项目里争议最大的部分。一部分同学主张直接上机器学习评分模型理由是风控嘛用模型更高级、更准。我坚持第一版先用可配置规则引擎。原因很朴素模型需要的历史样本和特征数据当时根本不够而且黑盒模型的决策结果没办法向业务方和审计解释。第一版风控规则引擎包含三类规则黑名单规则、准入规则、额度规则。黑名单规则命中直接拒绝比如手机号命中内部黑名单、身份证命中司法失信名单准入规则配置年龄区间、实名时长、历史逾期次数上限额度规则按照收入区间、已有负债、历史还款表现计算授信系数。规则引擎的配置化要满足三个要求规则可动态上下线、参数可热更新、命中原因可记录。每次请求进来引擎按优先级跑完所有规则把每条规则的命中结果和原因都记录到风控日志中。这个“可解释”能力在后续业务团队争论拒贷原因时特别有用能直接指出是哪条规则拒绝的。4.2 特征数据的时效与成本实时特征不是越多越好风控规则的执行依赖特征数据。最开始我们一股脑接了很多实时接口实时查询芝麻分、实时查征信、实时校验银行卡四要素结果每个接口几十到几百毫秒一笔请求光特征采集就耗了一两秒超时率直线上升。后来我们做了特征分级把实时性要求高的特征比如欺诈相关的设备和IP风险分走实时接口把稳定特征比如客户的年龄、历史还款次数、负债率做成离线快照每天更新一次在请求时直接读本地缓存。这样既有实时风险识别能力又不用为稳定特征付出高额时延。一个可以抄作业的经验特征数据要建一张特征宽表按用户维度每日更新特征字段以列存方式冗余存储。风控请求时从宽表读九成特征只对真正高优的少数特征实时查询。别一上来就给所有特征都开实时链路那是在给上游供应商送钱。5. 数据合规与隐私保护每个服务方都躲不开的隐形门槛5.1 最小化采集不是口号是接口设计的硬约束金融服务的每个环节都会碰到敏感数据身份证号、手机号、银行卡号、联系地址。刚一开始大家的做法很随意能传就传能存就存。后来在内部评审被合规同事怼了才知道敏感数据的采集必须有明确业务场景不能因为接口方便就一股脑全量返回。我们的做法是对每个接口做“最小化采集清单”。开户接口只需要三要素姓名、身份证号、手机号那么接口定义里就只有这三个字段不提供多余的可选参数。内部服务之间传数据也要走数据字典按需要申请字段权限。这条约束一开始让开发觉得麻烦但后期处理隐私请求时省了很多事因为你根本没存那些不该存的数据。5.2 日志脱敏与分级权限数据安全的最后一公里数据合规最容易被忽视的是日志和运维链路。接口层做了字段最小化但日志里可能把完整的身份证号、银行卡号打出来DB 里也可能因为业务需要存了明文。为此我们专门做了三层防护第一层日志脱敏组件。统一的日志框架里内置脱敏函数凡是命中手机号、身份证、银行卡号正则的字段输出时自动打码。第二层存储加密。对数据库中的敏感字段做列级加密存储应用侧使用专门的加解密 SDK密钥由独立的密钥管理服务保存应用代码里拿不到明文密钥。第三层权限管控。运维人员和开发人员默认没有查询敏感表明文数据的权限需要通过申请流程开通临时权限所有查询操作都有审计记录。风险点防护措施验证方式日志打印敏感信息日志脱敏组件巡检日志审计数据库明文存储列级加密存储数据库抽样检查越权查询用户数据分级权限 临时开通审计日志回溯接口过度采集最小化字段清单接口评审卡点这套东西看起来增加了开发成本但它其实是在保护项目自己。金融服务的客户信息安全一旦出事业务信任瞬间清零后续接任何合作伙伴都会先被要求出示数据安全资质。与其临上线补合规不如从开始就把合规约束写进开发规范里。6. 线上踩过的坑对账不平、重复入账、超时风暴6.1 对账不平的根因排查字段语义模糊吃大亏项目上线第三周运营反馈“某合作方账单对不上差了几万块”。第一反应是查对账程序逻辑翻来覆去没找到问题。最后手工拉出差异数据发现上游对账文件里的金额单位是元保留两位小数而我们本地按分存储对账时格式转换把小数丢了精度。例如上游传 1000.10 元我们转整数分时用了强转变成了 100010 分正好少了 10 分钱。一笔两笔看不出几千笔累计就是一大笔缺口。这件事教训深刻对账字段的格式转换必须在程序里显式做并且要有差值容错的告警阈值。现在我们的对账任务里每条差异都会记录上游原值、转换后值、转换规则版本任何一条转换异常都会触发人工复核而不是默默累加。6.2 重复入账幂等没做彻底真金白银会教你做人另一个血泪坑是代扣业务重复入账。当时媒体回调服务更新了业务单状态但没有对“已还本金”“已还利息”这两个账户流水加唯一约束。某个晚上上游重试通知消息消费组件多线程并发处理同一笔还款生成了两条流水账户余额直接虚增了一倍。第二天早上发现时已经影响了几百个用户。这个问题的修复很简单给还款流水表加上“业务单号 资金类型”的唯一索引。但排查过程很曲折因为正常路径不会触发只有在上游重试高峰和消费组件扩容并发拉高时才会暴露。后来我们在所有涉及资金流水的入账逻辑里统一加了“先查后插 唯一索引兜底”的双保险并且把对账任务从每日一次改成每日三次减少异常发现的时滞。6.3 超时风暴下游慢上游重试连环打最惊险的一次故障发生在大促当天。某资金方通道响应变慢平均耗时从 300ms 涨到 3s我们这边的线程池眼看要被占满。上游因为收不到快速响应启动了重试机制重试流量又继续堆进线程池形成恶性循环。那天的处理方案是立即开启下游通道的熔断器停止新流量进入慢通道资金路由切换到备选通道同时把该通道的请求超时时间从原来的 3s 临时改为 1s快速失败丢弃积压请求。事后复盘光有熔断还不够重试限流也很关键。我们给每个下游通道都配置了独立信号量限制同时在途的请求数超过阈值直接返回“通道繁忙”宁可让业务方感知到不可用也不能让故障拖垮整个平台。7. 复盘总结如果重新做 financial-services我会怎么取舍项目收尾后团队做过一次内部复盘讨论最多的不是技术细节而是“如果重新来一遍哪些决策要保留哪些动作其实是浪费”。第一保留“能力域优先拆解”的做法。没有这一步后面所有系统边界都会模糊团队协作成本会高出一个量级。第二保留“对账与幂等作为一等公民优先实现”的策略。金融项目里实时链路再炫酷最终还是要回到账平不平这件事上。第三我觉得可以砍掉的是第一版中过度设计的配置中心。当时我们把几乎所有的业务参数都搬到了配置中心结果配置项几百个光配置评审就花了两周实际有近半的配置上线后没有改过一次。参数配置化要按真实需求驱动不用为了架构好看而提前抽象。如果非要给做类似项目的团队提一条建议我会说先把最小可用闭环跑通账户记账加一条支付通道加一个规则风控比试图第一天就搭建完美平台更重要。金融服务的复杂度不在于某个单一功能而在于多个功能耦合时的边界处理。边界理清楚了扩展就是继续接新通道、加新风控规则的事。另外再分享一个我自己养成的习惯每一次线上问题处理完都会花半小时把“问题表象、排查链路、根因、修复动作、预防措施”写成一篇短复盘挂在项目文档库里。finance 项目不像普通业务系统一个隐患埋久了可能会变成资金损失。文档化的价值不在当下而在半年后新同学接手时能少走一半弯路。