小探网页支付网关的架构设计与体验优化

开题

现在网络支付方式越来越发达,已经不可避免地开始与各类 IT 系统集成。在财务制度比较规范的地方,这类入账自然需要统一管理,资金不能随意流动。如果由各个业务系统自己去申请对接网络支付接口,自然是很难实现统一管理的,而且也麻烦。

因此,就需要一个统一的企业级支付平台作为中间层,来负责各业务系统与网络支付的对接。这样,不仅方便了财务数据的统一提取、账户对账,也使企业支付平台可以代表所有的业务系统与网络支付接口进行交互,业务系统不会接触到企业的网络支付总密钥,较为安全可控。

这样的例子其实我们天天见。例如 12306 的车票支付教育部考试中心的考试费支付,都采用了中间层,来接入多个网络支付平台。

此时,企业级支付平台的角色可以认为是一个代理,协助业务系统完成支付流,并保存相关数据。目前这类“代理”的架构并没有标准,所以实现起来会各有小差别。如何去定义企业级支付平台(下称企业层)和业务系统各自的角色,就会决定架构的实现方式,相应就会影响到支付业务的实现情况。

WePay 的实现思路

下面介绍一款已经在运行中的企业支付平台 WePay,其目前向上对接了微信企业号的微信支付接口,远期规划可加入其他网络支付的支持。

WePay 会给每个业务系统分配一个“子商户”,对应会有 ID 和密钥,也就意味着业务系统通过 WePay 实现的子商户进行支付请求,WePay 自己处理网络支付接口的密钥,应用系统只能借助 WePay 获取网络支付的结果信息。

WePay 当前的架构和支付流程

上图的流程目前其实工作得非常好。应用系统需要完成的工作很简单,生成应用端的订单信息并保存订单号,然后把订单信息一股脑前台 POST 给 WePay,WePay 的网页会帮应用系统实现支付过程,应用系统只管收支付结果就可以了。

在这里,WePay 的角色并不只是“代理”那么简单了,因为要帮应用系统实现支付流程,所以中间还得控制支付过程。我就暂且叫它做“富代理”吧。

为了更好地说明纯粹的“代理”是个什么情况,我假设了新的流程。这里业务系统会把 WePay 当作是网络支付的服务器,所以业务系统传给 WePay 的 ID 可以是自编号的子商户,WePay 把请求递交给网络支付的时候再用上网络支付统一的密钥。

WePay 如果变成一个纯粹的代理,会变成这个流程

代理与否,利弊明显。实际上现在基本上都是“富代理”的解决方案。

  • 纯代理
    • 业务系统需要自己处理支付过程(图中,浏览器端中间的蓝栈页面均由应用服务器实现);
    • 不同系统的技术良莠不齐,支付过程的体验和安全性会有差别,不利于企业对支付过程进行标准化;
    • 但如果业务系统已经做了现有的网络支付对接(目前网络支付有大量的 SDK),技术上,纯代理模式下业务系统会更容易接入企业层系统。
  • 富代理
    • 支付过程由 WePay 实现(图中,浏览器端中间的蓝栈页面均由 WePay 实现);
    • 如果应用是从头开发的,接入的工作量就会比直接接入网络支付少(即便人家有 SDK 了还得自己实现前端呢);
    • 支付过程均由企业层实现,用户体验和安全性可以由企业层的统一实现来保证。

“富代理”角色可以多做这点判断

企业层需要做的事情多了,这样的角色定位下,每单支付业务系统该给企业层传多少参数呢?

根据内部公开资料得知,目前业务系统在创建支付订单时,需要传以下这些参数给这款 WePay 系统。我做了下分类:

  • 请求身份和请求校验
    • spbillCreateIp 发起订单的 IP
    • tenant 租户代码(由 WePay 分配)
    • timestamp 订单生成的时间戳
    • sign 所有参数加上密钥后的签名值
  • 订单内容(必须)
    • tradeType 交易类型
    • totalFee 支付金额(单位为分)
    • tenantTradeNumber 租户订单号(业务系统内部唯一)
    • redirectUrl 前端跳转确认地址
  • 订单内容(增强)
    • productBodyproductDetail 交易内容和详情
    • tenantUserCodetenantUserName 企业应用中用户的用户名、姓名

实际上这一整套接口的参数内容,多是直接借鉴微信支付的参数来着的,反正就是转发参数,用现成的参数名也没啥问题。

但注意到 tradeType 这个参数了吗?实际上这个参数是微信支付用的。微信那边目前支持的值有 JSAPI(微信内浏览器)、NATIVE(用户扫码付款)、APP(手机应用)、MWEB(微信外手机浏览器)、MICROPAY*(商户条码付款),而 WePay 实现了 JSAPI 和 NATIVE,根据业务系统发的参数来返回给浏览器不同的界面。

那么,这个参数真的要靠业务系统来给定吗?

  • 大多数情况下,业务系统需要根据客户端来提供不同的界面来适配,比如根据 UA 有没有 micromessenger,判断提供 PC 版还是微信版。业务系统很多时候似乎知道客户端属性了,让业务系统来指定这个类型,下不同的单,似乎顺理成章。
  • 然而,当今的网页技术早已可以实现一套前端模板,通吃 PC 移动(Bootstrap 框架推了很多不会写响应式设计的人一把)。因此,有的业务系统根本没必要去判断客户端是不是微信(而提供不同的界面)。这时候让业务系统来下单,业务系统肯定下一个 PC 版的单了,除非非常考究,才会特意适配一下微信。
  • 此外前文提到,企业层在支付流程中,会协助应用系统实现面向用户的支付交互流程(前端)。而显然,JSAPINATIVEMWEB 这三种模式,只是触发支付的方式不同,并不会影响到支付业务的核心内容和最终结果。这种情况下,显然没有必要让应用去选择下单模式,而应该让企业层来统一判断,从而继续降低业务系统的集成难度。
    • JSAPI创建订单的时候需要 OPENID,可能有的业务会靠这个参数来限制只能某人支付这一订单。实际上这个需求并非必要,很多时候反而会造成业务规则不统一(支付页面如果可以分享,会导致 OPENID 不同而支付失败;不限制的话,虽然可能会有退款退不到账户本人的问题,但很多业务直接上 NATIVE 模式,谁扫码都行,完全没考虑这一点),确实有业务需要的话也可以保留,就是 WePay 多判断一下而已。
  • 更重要的是,这三种模式是高度与特定的网络支付端口耦合的。如果日后需要接入其它的网络支付,较好的办法是业务系统不用管网络支付端口的具体的支付模式参数,而靠 WePay 去给用户选择,否则业务系统全得再改一遍,多没意思。

结论就是,这个 tradeType 应当做一下整合。鉴于JSAPINATIVEMWEB 这三种模式本质上都是网页支付,可以在 WePay 端整合为一个名字(如 HTML),再由 WePay 进行选择,而不应该让业务系统来选。如果业务系统还发了这三个串进来,就统一定向到新的 HTML 模式就好。至于 APP 和 MICROPAY,现在这个系统似乎没有用到,这些方式就预留着空位就好。

支付中间层如何判断支付模式?

既然提出了这个解决方案,自然也得考虑,没有业务系统的任何数据,支付层能不能高效率地自行判断支付模式呢?当然是可以的。

支付中间层最麻烦的事情,莫过于在开始让用户支付前,就得先跟网络支付开好订单。而开订单的时候,就必须决定具体的支付模式。

有种稳妥的方法,就是通过前端来检查微信的 JSAPI 能不能用、有没有手机(微信),再来按需异步开订单。不过这样当然太麻烦了,所以靠粗暴的后端 User Agent 检查,就可以实现绝大多数情况下的正确及时判断了。

所以是:

  • UA 判断是否包含了 micromessenger,是的话,默认在微信环境下,开 JSAPI 订单
    • 调 JSAPI 需要获取 OPENID,企业号的接口无论微信用户是否有关注企业号,都会返回一个 OPENID,而且鉴于支付是低频操作,这个时候让
      WePay 跳一次 OAuth 其实也并不太影响响应时间(OAuth 有两种方案,一种是 WePay 自己跟微信拿数据然后 Cookie 缓存,另一种是跟企业号应用主域名拿数据,由于企业号域名通常用户都有登陆,都带 Cookie,可能可以节省 OAuth 的开销)
  • 其他情况全部走 NATIVE 订单;或者,如果有闲心兼容下 MWEB(手机浏览器在这个模式下可以直接拉起微信,体验会更好),就再 UA 判断是否包含 Android 或 iPhone / iPad 串,有的话就进 MWEB,没有就进 NATIVE 扫码去
  • 要做到 100% 靠谱,就得考虑在 WePay 端提供切换模式的功能了,不过一切换模式,就得关闭订单,还得重新生成订单号给网络支付用,是挺烦的,所以没必要做

EOF

说明:本文资料均来自完全公开和内部公开资料,图是自己用这个画的,并没有透露什么机密。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

To respond on your own website, enter the URL of your response which should contain a link to this post's permalink URL. Your response will then appear (possibly after moderation) on this page. Want to update or remove your response? Update or delete your post and re-enter your post's URL again. (Find out more about Webmentions.)