乐趣区

我对支付平台架构设计的一些思考

微信公众号「后端进阶」,专注后端技术分享:Java、Golang、WEB 框架、分布式中间件、服务治理等等。
老司机倾囊相授,带你一路进阶,来不及解释了快上车!

我在前一家公司的第一个任务是开发统一支付平台,由于公司的业务需求,需要接入多个第三方支付,之前公司的支付都是散落在各个项目中,及其不利于支付的管理,于是聚合三方支付,统一支付平台的任务就落在我手上,可以说是完全从 0 开始设计,经过一翻实战总结,我得出了一些架构设计上的思考,之前就一直很想把自己的架构设计思路写出来,但一直没动手,前几天在技术群里有人问到相关问题,我觉得有必要把它写出来,以帮助到更多需要开发支付平台的开发人员。

组件模式

由于公司业务在很多地区都有,需要提供多种支付途径,以满足业务的发展,所以设计的支付平台需要接入多种第三方支付渠道,如:微信支付、支付宝支付、PayPal、IPayLinks 等等,我们都知道,每个第三方支付,都有自己一套对外 API,官方都有一套 SDK 来实现这些 API,我们应该如何组织这些 API 呢?

由于第三方支付渠道会随着业务的发展变动,所以组织这些 SDK 就需要在不影响支付平台整体架构的前提下可灵活插拔,这里我使用了组件的思想,将支付 API 拆分成各种组件支付组件、退款组件、订单组件、账单组件等等,那么这样就可以当引入一个第三方支付 SDK 时,可灵活在组件上面添加需要的 API,架构设计如下:

通过 Builder 模式根据请求参数构建对应的组件对象,将组件与外部分离,隐藏组件构建的实现。组件模式 + Builder 模式使得支付平台具备了高扩展性。

多账户体系

在接入各种第三方支付平台,我们当时又遇到一个账户的问题,原因是公司当时的小程序与 APP 使用的是不同的微信账号,因此会出现微信支付会对应到多个账户的问题,而我当时设计支付平台时,没有考虑到这个问题,当时第三方支付只对应了一个账户,而且不同的第三方支付的账户之间相互独立且不统一。

于是我引入了多账户体系,多账户体系最重要的一个核心概念是以账户为粒度,接入多个第三方支付,统一账户的参数,构建了统一的支付账户体系,支付平台无需关心不同支付之间的账户差异以及第三方支付是否有多少个账户。

此时我在支付平台架构图加上账户层:

前端只需要传递 accountId,支付平台就可以根据 accountId 查询出对应的支付账户,然后通过 Builder 模式构建支付账户对应的组件对象,完全屏蔽不同支付之间的差异,在多账户体系里面,可以支持无限多个支付账户,完全满足了公司业务的发展需求。

统一回调与异步分发处理

做过支付开发的同学都知道,目前的第三方支付都有一个特点,就是支付 / 退款成功后,会有一个支付 / 退款回调的功能,目的是为了让商户平台自行校验该笔订单是否合法,比如:防止在支付时,客户端恶意篡改金额等参数,那么此时支付成功后,订单会处于支付中状态,需要等待第三方支付的回调,如果此时收到了回调,在校验时发现订单的金额与支付的金额不对,然后将订单改成支付失败,以防止资金损失。回调的思想是只要保证最终的一致性,所以我们调起支付时,并不需要在此时校验参数的正确性,只需要在回调时校验即可。

讲完了回调的目的,那么我们如何来设计支付平台的回调呢?

由于支付平台接入了多个第三方支付,如果此时每个第三方支付设置一个回调地址,那么将会出现多个回调地址,由于回调的 API 必须是暴露出去才能接受第三方的回调请求,所以就会有安全问题,我们必须在 API 外层设置安全过滤,不然很容易出现一些非法暴力访问,所以我们需要统一回调 API,统一做安全校验,之后再进行一层分发。

分发的机制我这里建议用 RocketMQ 来处理,可能有人会问,如果用 RocketMQ 来做分发处理,此时怎么实时返回校验结果到第三方支付呢?这个问题也是我当时一直头疼的问题,以下是我对回调设计的一些思考:

  1. 公司的系统是基于 SpringCloud 微服务架构,微服务之间通过 HTTP 通信,当时有很多个微服务接入了我的支付平台,如果用 HTTP 作分发,可以保证消息返回的实时性,但也会出现一个问题,由于网络不稳定,就会出现请求失败或超时的问题,接口的稳定性得不到保障。
  2. 由于第三方支付如果收到 false 响应,就在接下来一段时间内再次发起回调请求,这么做的目的是为了保证回调的成功率,对于第三方支付来说,这没毛病,但对于商户支付平台来说,也许就是一个比较坑爹的设计,你想一下,假设有一笔订单在支付时恶意篡改了金额,回调校验失败,返回 false 到第三方支付,此时第三方支付会再重复发送回调,无论发送多少次回调,都会校验失败,这就额外增加了不必要的交互,当然这里也可以用幂等作处理,以下是微信支付回调的应用场景说明:

基于以上两点思考,我认为返回 false 到第三方支付是没必要的,为了系统的健壮性,我采用了消息队列来做异步分发,支付平台收到回调请求后直接返回 true,这时你可能会提出一个疑问,如果此时校验失败了,但此时返回 true,会不会出现问题?首先,校验失败情况,订单必定是处于支付失败的状态,此时返回 true 目的是为了减少与第三方支付不必要的远程交互。

因为 RocketMQ 的消息是持久化到磁盘的,所以用消息队列来做异步分发最大的好处,就是可以复查消息队列里面的消息来排查问题,而且消息队列可以在业务的高峰期进行流量削峰。

以下是统一回调与分发处理的架构设计图:

聚合支付

支付平台聚合了多种第三方支付,因此在请求层需要做很多的适配工作,以满足多种支付的需求,可能你会想,直接在适配那里加几行 if else 不就得了吗,这么做也没问题,也可以满足多种支付的需求,但你有没有想过,假设此时再加一个第三方支付,你会怎么做?你只能原有方法上加多个 else 条件,这样就会导致请求层代码不断地随着业务发展改变,使得代码及其不优雅,而且也不好维护,这时我们就得用上策略模式,将这些 if else 代码消除,当我们增加一个第三方支付时,我们只需要新建一个 Strategy 类就可以了,策略模式究竟怎么使用可以看看大话设计模式。

因此我在 Builder 模式前加多了一层支付策略层:

请求处理

由于支付平台涉及到资金,支付的各种请求与返回,以及异常记录在一个支付平台中异常重要,因此我们需要记录每一次的支付请求记录,以便后续排查问题。

基于这点需求,我在开始请求第三方支付之前,设计了一层 Handler 层,所有的请求都必须经过 Handler 层进行处理,Handler 核心方法如下:

public K handle(T t) {
  K k;
  try {before(t);
    k = execute(t);
    after(k);
  } catch (Exception e) {exception(t, e);
  }
  return k;
}
protected abstract void before(T t);
protected abstract void after(K k);
protected abstract void exception(T t, Exception exception);

原则上来说,我设计的 Handler 层,利用了模版模式,不仅仅可以实现日志的记录,还可以实现多种处理方式,比如请求监控,消息推送等等,实现了 Handler 层的高扩展性。

以下是 Handler 层的架构设计图:

写在最后

以上就是我的支付平台架构设计思路,总结来说,支付平台需要具备可扩展性、稳定性、高可用性,因此我在设计支付平台时使用了很多设计模式以及引入消息队列处理回调分发的问题,使得支付平台具备这几点特性,希望能够给你一些启发与帮助,最后我把支付平台整体的架构设计图贴出来:

退出移动版