部分团队在做支付系统时,会把所有支付渠道塞进一个微服务里。微信、支付宝、钱包,全部用if-else或策略模式在同一个进程内切换。这在业务初期没什么问题,但当你的业务跨了国家、跨了币种、对接了七八个支付品牌时,这种做法的代价会越来越大。一个渠道的SDK升级,可能影响到所有渠道的稳定性。一个渠道的流量激增,会拖慢整个支付服务的响应。
我之前参与过一个实际的支付中心建设,最终落地的方案是:每个支付渠道独立一个微服务,物理隔离,独立部署。整个支付中心由9个微服务组成,覆盖国内的微信、支付宝,海外多个地区的聚合支付平台,以及内部的钱包账户体系和自由卡支付。
下面把这个支付中心的架构设计展开,包括模块划分、支付编排服务设计、组合支付机制、钱包记账模型、安全体系。
整体架构概览
先给一个全局视角。整个支付中心分为四层,从上到下依次是:接入层、编排层、渠道层、存储层。
支付网关负责接收前端的支付请求,做参数校验、接入路由和用户信息补全。支付编排服务是整个支付中心的大脑,负责支付路由、费用分摊、组合支付编排、回调合并。渠道层是与第三方支付平台对接的执行单元,每个支付品牌一个独立服务。存储层负责支付流水的持久化,支撑对账和查询。
9个微服务的职责划分如下:
| 服务 | 定位 | 核心职责 |
|---|---|---|
| 支付网关服务 | 接入层 | 接收前端请求,做参数校验和接入路由,获取用户渠道信息,构建下单参数 |
| 支付编排服务 | 编排层 | 支付路由、费用分摊、组合支付编排、回调处理 |
| 支付流水服务 | 存储层 | 支付/退款流水的CRUD,分表存储,状态管理 |
| 微信渠道服务 | 渠道层 | 对接微信支付API(小程序、APP、H5) |
| 支付宝渠道服务 | 渠道层 | 对接支付宝API(小程序、APP),蚂蚁森林联动 |
| 钱包服务 | 渠道层 | 用户账户管理、余额记账、充值/扣款/冻结 |
| 国外支付渠道 | 渠道层 | 对接海外各地区聚合支付平台(日本、澳门等),支撑跨国业务 |
| 支付管理后台 | 管理层 | 商户配置、应用配置、支付查询 |
为什么每个支付品牌要独立一个微服务
这个决策在很多团队看起来过于激进。毕竟所有渠道的接口模式大同小异:下单、查询、退款、回调,用一个服务加策略模式就能搞定。
问题在于,当你的业务走向多国家、多币种时,不同支付品牌之间的差异远不止接口参数的不同:
第一,SDK依赖冲突。 微信支付用的是weixin-java-pay,支付宝用的是alipay-sdk-java,海外的聚合支付平台通常是REST API加HMAC签名,而且不同地区的签名体系各不相同。这些SDK的依赖树不同,版本升级节奏不同。把它们塞在一个服务里,依赖冲突的概率随渠道数量快速增长。
第二,发布节奏不同。 微信支付的接口可能一年改两次,支付宝可能每个季度有新特性,海外支付渠道的变动频率更不可预测。独立服务意味着独立发布,一个渠道的升级不需要重新发布整个支付系统。
第三,故障隔离。 某个渠道的第三方API出了问题,只影响该渠道的服务实例,不会拖慢其他渠道的支付成功率。这在支付这种对可用性要求极高的场景下,价值非常大。
第四,流量特征差异。 国内的微信小程序支付在晚间高峰可能是平时的10倍流量,而海外渠道的流量可能一直很平稳。独立服务可以按需扩缩容,不浪费资源。
当然,这个方案也有代价:服务数量多了,运维复杂度上升,联调成本增加。如果你的业务只有国内两三个渠道,一个服务加策略模式就够了。渠道数量超过5个,或者涉及跨国业务时,物理隔离的收益才会超过成本。
支付网关
支付网关和基础网关是两回事
很多人一看到「网关」两个字,第一反应是 Spring Cloud Gateway 这类基础设施网关。这两者是完全不同的东西。
基础网关是基础设施层,处理的是所有服务共用的横切问题:身份鉴权、限流熔断、服务路由。它对业务是无感的,不知道也不关心请求是支付请求还是查询请求。
支付网关是支付域内部的一个独立服务,职责完全不同:
- 作为支付系统对外的统一入口,接收来自业务系统的支付请求
- 做支付域内的参数合法性校验和签名验证
- 判断请求来自哪条业务线或哪个接入场景,转发到支付域内对应的处理路径
- 屏蔽上游接入来源的差异,向下游暴露统一的内部调用接口
- 处理各渠道的异步回调,统一格式后再通知业务系统
这里有一个容易混淆的地方需要说清楚。支付网关做的「路由」,和支付编排服务做的「路由」是两回事,不在同一个维度。
支付网关做的是接入路由:这个支付请求从哪里来,属于哪条业务线,应该分发到支付域内部的哪个处理路径。它解决的是接入层的差异问题,比如 APP 支付和小程序支付的入参格式不同、不同业务线的请求需要不同的前置校验逻辑。
支付编排服务做的是渠道路由:一笔支付应该走微信还是支付宝,组合支付时每个渠道分摊多少金额,走哪个商户号的配置。它解决的是支付业务本身的编排问题,逻辑更重,和接入无关。
两者不重叠,分工明确:支付网关处理「谁来请求、从哪里来」,支付编排服务处理「用哪个渠道、怎么执行」。
完整的请求链路是:前端 → 基础网关(鉴权/限流) → 支付网关(参数校验/接入路由) → 支付编排服务(渠道路由/费用分摊) → 具体渠道服务 → 第三方支付平台。
美团、京东这类有规模的支付系统,这两层都是独立存在的。美团技术博客中描述他们的支付中心架构时,明确把「支付网关」和上游的「基础接入层」作为两个独立角色处理。
支付网关和支付编排服务的职责区分
这是两个在命名上容易混淆、但分工截然不同的角色。
支付网关解决的是接入问题:请求从哪里来、来的格式是什么、请求方身份是否合法。它不知道也不关心这笔钱最终走微信还是支付宝,只负责把外部的多样化接入收敛成统一的内部格式,然后交给后面的服务处理。具体职责包括:参数合法性校验、签名验证、接入路由(根据来源业务线或接入场景分发请求)、异步回调的统一接收和格式转换。
支付编排服务解决的是编排问题:这笔支付用哪个渠道、组合支付时怎么分摊金额、多个渠道的异步回调怎么合并成一个支付完成事件。它面向的是支付渠道,不感知请求从哪里来,只处理支付业务本身的编排和协调。
两者的分工可以用一句话概括:支付网关处理的是入口的复杂性,支付编排服务处理的是渠道的复杂性。
在实际系统里,这两个角色对应的是两个不同的扩展维度。业务接入方越来越多(不同APP、扫码收款、外部系统API接入),支付网关的接入路由规则就越来越重要;支付渠道越来越多(国内微信支付宝、海外各地区聚合支付),支付编排服务的渠道路由和费用分摊逻辑就越来越复杂。两者独立演进,互不干扰。
service-pay-gateway 承担的是支付网关的职责:作为前端的统一接入点,聚合用户渠道信息(openId、unionId),做接入适配,然后调用支付编排服务执行编排。支付编排服务负责渠道路由、费用分摊、回调合并。
支付编排服务的设计
支付编排服务是整个支付中心最核心的服务,承担支付请求的路由分发、组合支付的编排协调、支付回调的合并处理三大职责。
路由机制
支付编排服务用策略模式实现渠道路由。每个支付渠道对应一个处理器实现类,通过Spring容器自动注入到一个Map中。请求进来时,根据paymentChannel字段从Map中取出对应的处理器执行。
// 渠道路由器,根据渠道标识获取处理器
public PayChannelHandler getHandler(String channel) {
ChannelEnum channelEnum = ChannelEnum.valueOf(channel);
return handlerMap.get(channelEnum.getServiceName());
}
这个设计的好处是新增渠道只需要实现接口、注册为Bean即可,不需要改动路由代码。
费用分摊计算
组合支付场景下,一笔订单的金额需要分摊到多个支付渠道。分摊规则是固定优先级:自由卡优先全额使用,不足部分用钱包补,钱包仍不够的部分由微信或支付宝承担。
这个优先级的设定有商业考量:自由卡是一种预付卡,用户提前购买充值后可直接用于支付,属于平台的沉淀资金,优先消耗对平台的资金利用率最好;钱包余额次之;第三方支付渠道有手续费成本,排在最后。
回调合并
组合支付最复杂的地方在回调处理。一笔订单可能同时用了钱包和微信,两个渠道的回调是异步到达的。支付编排服务需要判断:所有参与支付的渠道是否都已经回调成功,只有全部成功才能把主订单标记为支付完成,然后通过消息队列通知下游业务系统。
这个回调合并的状态通过Redis来维护。每个渠道回调时更新自己在Redis中的状态,同时检查其他渠道的状态。全部完成后发送支付成功的MQ消息。
组合支付的数据模型
支付流水服务的数据模型是理解整个支付中心的关键。采用主流水+明细的双层结构:
一笔支付对应一条主流水记录,记录订单级别的信息(总金额、整体支付状态、用户、门店)。同时对应N条明细记录,每条明细对应一个参与支付的渠道(该渠道的分摊金额、渠道支付状态、外部交易号)。
// 主流水:一笔订单一条
TradeOrder: tradeNo, outTradeNo, amount, payStatus, userId
// 明细:每个渠道一条
TradeDetail: tradeNo, payChannel, amount, payStatus, outTransNo
支付状态机的流转路径:新建(0) → 处理中(1) → 成功(2) 或 失败(3),成功后还可能冲正(4)。
这个双层模型的价值在于:主流水给业务系统一个统一的支付视图,不管底层用了几个渠道,业务系统只看主流水的状态就知道这笔订单付没付。明细则给对账和运营提供了每个渠道维度的精确数据。
钱包与记账系统
钱包服务在这个支付中心里不仅仅是一个支付渠道,它是一套完整的账户体系。采用复式记账原理:每一笔交易都产生借方分录和贷方分录,确保账务平衡。
账户模型支持:个人钱包账户、子账户(自由卡)、平台收益账户。每个账户有余额、冻结金额、币种、是否可透支等属性。
并发控制用乐观锁实现:账户表有version字段,更新余额时带上当前version作为条件。如果并发更新导致version不匹配,操作失败,由上层重试。
// 乐观锁更新余额
UPDATE customer_account
SET balance = ?, version = version + 1
WHERE id = ? AND version = ?
记账器的设计用了模板方法模式:基础记账器定义了校验分录平衡、计算余额、记录变动明细的骨架流程,具体的记账策略(普通消费、充值、退款、冲正)由子类实现。
一个值得注意的设计:记账模式分为实时和延迟两种。实时记账立即更新余额,用于支付扣款等对实时性要求高的场景;延迟记账只记录变动明细,后续批量入账,用于结算等可以异步处理的场景。
安全体系
支付系统的安全设计覆盖了从网关到渠道的完整链路。
网关层:VIP网关对所有支付请求做签名验证。请求体中包含clientId、payload、sign、timestamp四个字段,网关用客户端公钥验签,同时校验timestamp防重放攻击。通过后提取JWT中的用户身份信息注入请求上下文。
渠道层:每个支付品牌有自己的签名验签体系。微信用MD5签名,支付宝用RSA签名,海外聚合支付用HMAC-SHA256。回调到达时先验签再处理业务,验签失败直接丢弃。
支付确认:涉及钱包扣款的支付需要密码验证。支付网关服务在确认支付阶段调用用户中心校验支付密码。
配置安全:支付渠道的密钥、证书等敏感配置存储在配置中心(Apollo),运行时动态获取,不写在代码或本地配置文件中。每个门店可以有自己的商户号和密钥配置,支持多商户模式。
支付配置管理
支付配置是整个系统中容易被忽视但非常重要的模块。实际业务中,不同门店可能使用不同的商户号,不同的应用(小程序、APP、H5)对应不同的AppId,同一个支付品牌在不同国家有不同的接入参数。
配置分为三层:
| 层级 | 说明 | 示例 |
|---|---|---|
| 应用层 | 区分不同客户端 | 小程序AppId、APP的AppId |
| 商户层 | 区分不同商户主体 | 商户号、密钥、证书 |
| 门店层 | 区分不同门店的支付能力 | 门店A用商户X,门店B用商户Y |
支付编排服务在处理支付请求时,根据请求中的门店号和来源渠道,查找对应的支付配置,然后把配置传给具体的渠道服务。渠道服务拿到配置后初始化对应的SDK客户端实例。
这个方案的好处是支持多商户并行,新开一个商户主体只需要在管理后台配置即可,不需要改代码。
小结
支付中心的架构设计,核心矛盾在于「统一管控」和「渠道隔离」之间的平衡。统一管控要求有一个中心化的编排层来协调所有支付流程,保证数据一致性和业务规则的统一执行;渠道隔离要求每个支付品牌独立部署、独立发布、独立扩容,互不影响。
这套系统用「编排+渠道」的分层方式解决了这个矛盾:编排层做路由和规则,渠道层做执行和隔离。从落地效果来看,这个架构经受住了多国家多渠道的考验。
如果让我重新设计,我会在三个地方做不同的选择:预下单数据必须持久化而不是只放Redis;回调合并用计数器模式取代硬编码判断;独立出对账模块作为支付系统的标配组件。这三个问题在业务初期不会暴露,但随着交易量增长和渠道增多,会变成越来越大的风险敞口。
支付系统的设计原则可以归结为一句话:对外宽容,对内严格。 对外要兼容各种第三方支付品牌的差异和不确定性,对内要保证每一分钱的流向都有迹可循、可对账、可追溯。
最近在知乎出了
- 「应付6000万会员的秒杀系统专栏」
- 「几亿用户,百万并发的C端商品系统实战」
- 「技术团队DDD领域驱动设计三年落地实战」
专栏,感兴趣的可以订阅一下。至于知识星球的,可以搜:
- 老码头的技术浮生录
它是一个能实际帮你解决难题的星球。有问题的,找知心的Sam哥,支持无限次语音一对一解决你遇到的难题。「另外后续我新写的所有对外的付费专栏,在星球内都是免费的,且可以拿到所有源代码。」
当前星球里免费看的专栏是:
- 「几亿用户,百万并发的C端商品系统实战」
- 「技术团队DDD领域驱动设计三年落地实战」
知识星球内后续将推出20+个付费专栏,覆盖电商全链路:
| 选购线 | 用户会员营销线 | 中后台 |
|---|---|---|
| 购物车服务 | 营销系统 | 订单系统 |
| 商品服务 | 用户系统 | 支付系统 |
| 菜单服务 | 结算服务 |
从前台选购到中后台结算,星球成员全部免费,后续新增也不额外收费。
我的知乎账号:
- SamDeepThinking