阅读 2058

coder,你会设计交易系统吗(概念篇)?

文中我们从严谨的角度一步步聊到支付如何演变成独立的系统。内容包括:系统演进过程、接口设计、数据库设计以及代码如何组织的示例。若有不足之处,欢迎讨论共同学习。

从模块到服务

我记得最开始工作的时候,所有的功能:加购物车/下单/支付 等逻辑都是放在一个项目里。如果一个新的项目需要某个功能,就把这个部分的功能包拷贝到新的项目。数据库也原封不动的拷贝过来,稍微根据需求改改。

这就是所谓的 单体应用 时代,随着公司产品线开始多元,每条产品线都需要用到支付服务。如果支付模块调整了代码,那么就会处处改动、处处测试。另一方面公司的交易数据割裂在不同的系统中,无法有效汇总统一分析、管理。

这时就到了系统演进的时候,我们把每个产品线的支付模块抽离成统一的服务。对自己公司内部提供统一的API使用,可以对这些API进一步包装成对应的SDK,供内部业务线快速接入。这里服务使用HTTP或者是RPC协议都可以根据公司实际情况决定。不过如果考虑到未来给第三方使用,建议使用HTTP协议,

系统的演变过程:

image-20190309104541749

总结下,将支付单独抽离成服务后,带来好处如下:

  1. 避免重复开发,数据隔离的现象出现;
  2. 支付系统周边功能演进更容易,整个系统更完善丰满。如:对账系统、实时交易数据展示;
  3. 随时可对外开发,对外输出Paas能力,成为有收入的项目;
  4. 专门的团队进行维护,系统更有机会演进成顶级系统;
  5. 公司重要账号信息保存一处,风险更小。

系统能力

如果我们接手该需求,需要为公司从零搭建支付系统。我们该从哪些方面入手?这样的系统到底需要具备什么样的能力呢?

首先支付系统我们可以理解成是一个适配器。他需要把很多第三方的接口进行统一的整合封装后,对内部提供统一的接口,减少内部接入的成本。做为一个最基本的支付系统。需要对内提供如下接口出来:

  1. 发起支付,我们取名:/gopay
  2. 发起退款,我们取名:/refund
  3. 接口异步通知,我们取名:/notify/支付渠道/商户交易号
  4. 接口同步通知,我们取名:/return/支付渠道/商户交易号
  5. 交易查询,我们取名:/query/trade
  6. 退款查询,我们取名:/query/refund
  7. 账单获取,我们取名:/query/bill
  8. 结算明细,我们取名:/query/settle

一个基础的支付系统,上面8个接口是肯定需要提供的(这里忽略某些支付中的转账、绑卡等接口)。现在我们来基于这些接口看看都有哪些系统会用到。

image-20190309111001880

下面按照系统维度,介绍下这些接口如何使用,以及内部的一些逻辑。

应用系统

一般支付网关会提供两种方式让应用系统接入:

  1. 网关模式,也就是应用系统自己需要开发一个收银台;(适合提供给第三方)
  2. 收银台模式,应用系统直接打开支付网关的统一收银台。(内部业务)

下面为了讲清楚设计思路,我们按照 网关模式 进行讲解。

对于应用系统它需要能够请求支付,也就是调用 gopay 接口。这个接口会处理商户的数据,完成后会调用第三方网关接口,并将返回结果统一处理后返回给应用方。

这里需要注意,第三方针对支付接口根据我的经验大致有以下情况:

  1. 支付时,不需要调用第三方,按照规则生成数据即可;
  2. 支付时,需要调用第三方多个接口完成逻辑(这可能比较慢,大型活动时需要考虑限流/降配);
  3. 返回的数据是一个url,可直接跳转到第三方完成支付(wap/pc站);
  4. 返回的数据是xml/json结构,需要拼装或作为参数传给她的sdk(app)。

这里由于第三方返回结构的不统一,我们需要统一处理成统一格式,返回给商户端。我推荐使用json格式。

{
    "errno":0,
    "msg":"ok",
    "data":{

    }
}
复制代码

我们把所有的变化封装在 data 结构中。举个例子,如果返回的一个url。只需要应用程序发起 GET 请求。我们可以这样返回:

{
    "errno":0,
    "msg":"ok",
    "data":{
        "url":"xxxxx",
        "method":"GET"
    }
}
复制代码

如果是返回的结构,需要应用程序直接发起 POST 请求。我们可以这样返回:

{
    "errno":1,
    "msg":"ok",
    "data":{
        "from":"<form action="xxx" method="POST">xxxxx</form>",
        "method":"POST"
    }
}
复制代码

这里的 form 字段,生成了一个form表单,应用程序拿到后可直接显示然后自动提交。当然封装成 from表单这一步也可以放在商户端进行。

上面的数据格式仅仅是一个参考。大家可根据自己的需求进行调整。

一般应用系统除了会调用发起支付的接口外,可能还需要调用 支付结果查询接口。当然大多数情况下不需要调用,应用系统对交易的状态只应该依赖自己的系统状态。

对账系统

对于对账,一般分为两个类型:交易对账结算对账

交易对账

交易对账的核心点是:检查每一笔交易是否正确。它主要目的是看我们系统中的每一笔交易与第三方的每一笔交易是否一致。

这个检查逻辑很简单,对两份账单数据进行比较。它主要是使用 /query/bill 接口,拿到在第三方那边完成的交易数据。然后跟我方的交易成功数据进行比较。检查是否存在误差。

这个逻辑非常简单,但是有几点需要大家注意:

  1. 我方的数据需要正常支付数据+重复支付数据的总和;
  2. 对账检查不成功主要包括:金额不对第三方没有找到对应的交易数据我方不存在对应的交易数据

针对这些情况都需要有对应的处理手段进行处理。在我的经验中上面的情况都有过遇到。

金额不对:主要是由于第三方的问题,可能是系统升级故障、可能是账单接口金额错误;

第三方无交易数据: 可能是拉去的账单时间维度问题(比如存在时差),这种时区问题需要自己跟第三方确认找到对应的时间差。也可能是被攻击,有人冒充第三方异步通知(说明系统校验机制又问题或者密钥泄漏了)。

自己系统无交易数据: 这种原因可能是第三方通知未发出或者未正确处理导致的。

上面这些问题的处理绝大部份都可以依赖 query/trade query/refund 来完成自动化处理。

结算对账

那么有了上面的 交易对账 为什么还需要 结算对账 呢?这个系统又是干嘛的?先来看下结算的含义。

结算,就是第三方网关在固定时间点,将T+x或其它约定时间的金额,汇款到公司账号。

下面我们假设结算周期是: T+1。结算对账主要使用到的接口是 /query/settle,这个接口获取的主要内容是:每一笔结算的款项都是由哪些笔交易组成(交易成功与退款数据)。以及本次结算扣除多少手续费用。

它的逻辑其实也很简单。我们先从自己的系统按照 T+1 的结算周期,计算出对方应该汇款给我们多少金额。然后与刚刚接口获取到的数据金额比较:

银行收款金额 + 手续费 = 我方系统计算的金额

这一步检查通过后,说明金额没有问题。接下来需要检查本次结算下的每一笔订单是否一致。

结算系统是 强依赖 对账系统的。如果对账发现异常,那么结算金额肯定会出现异常。另外结算需要注意的一些问题是:

  • 银行可能会自行退款给用户,因为用户可直接向自己发卡行申请退款;
  • 结算也存在时区差问题;
  • 结算接口中的明细交易状态与我方并不完全一致。比如:银行结算时发现某笔退款完成,但我方系统在进行比较时按照未退款完成的逻辑在处理。

针对上面的问题,大家根据自己的业务需求需要做一些方案来进行自动化处理。

财务系统

财务系统有很多内部业务,我这里只聊与支付系统相关的。(当然上面的对账系统也可以算是财务范畴)。

财务系统与支付主要的一个关系点在于校验交易、以及退款。这里校验交易可以使用 query/trade query/refund这两个接口来完成。这个逻辑过程就不需要说了。下面重点说下退款。

我看到很多的系统退款是直接放在了应用里边,用户申请退款直接就调用退款接口进行退款。这样的风险非常高。支付系统的关于资金流向的接口一定要慎重,不能过多的直接暴露给外部,带来风险。

退款的功能应该是放到财务系统来做。这样可以走内部的审批流程(是否需要根据业务来),并且在财务系统中可以进行更多检查来觉得是否立即进行退款,或者进入等待、拒绝等流程。

第三方网关

针对第三方主要使用到的其实就是异步通知与同步通知两个接口。这一部分的逻辑其实非常简单。就是根据第三方的通知完成交易状态的变更。以及通知到自己对应的应用系统。

这部分比较复杂的是,第三方的通知数据结构不统一、通知的类型不统一。比如:有的退款是同步返回结果、有的是异步返回结果。这里如何设计会在后面的 系统设计 中给出答案。

第一部份的内容就到此结束了。如果有什么疑问欢迎到我们GitHub主页留言。

GitHub: https://github.com/skr-shop

关注下面的标签,发现更多相似文章
评论