市面上绝大多数的系统都具有充值(支付)功能。具有自有账户体系的系软件统往往还具有提现(代付)功能。支付/代付对接大休上可以分为四个阶段
第一阶段:支付代码嵌入到业务代码
优点:简单,无分布式事务问题
缺点:伸缩性差,可扩展性差,项目管理角度来看不好明确分工
第二阶段:服务化,单独的支付服务
优点:伸缩性,扩展性强,与具体的业务系统解耦,人员分工更合理
缺点:分布式事务问题
第三阶段:系统化,公司内部单独的支付系统
实现支付路由等更多的技术与非技术需求,进一步与具体业务解耦,达到支持公司内部所有产品线的支付
第四阶段:产品化,可对公司外部使用的单独的支付产品
进一步完善安全、商户自定义操作,形成支付产品,对外提供支付产品服务(有相关牌照),这也是第三方支付渠道目前在干的事情。
一些约定##
状态统一的必要性
最核心的状态有三种:成功、失败、处理中。如果需求上需要显示更加精细的状态,可考虑采用子状态。
不同的支付渠道对这些状态的定义往往不同,即使同一支付渠道,对支付和代付两类接口的状态定义也经常不同。
状态统一的好处是可以更加明确的将支付渠道的(很多)状态转换成内部的统一的(三种)状态,对业务系统屏蔽不同支付渠道间的状态区别。
金额单位统一的必要性
一般情况建议使用金额单位为:分
不同支付渠道对使用的金额单位也不统一,金额统一的好处是对业务系统屏蔽不同支付渠道间金额单位的区别。
手续费处理问题
支付/代付通常是按笔支付手续费,在设计之初需要考虑手续费处理问题,按实际业务需求而定。
一个完整的支付服务##
功能:
1、正向调用接口
提供充值和提现接口给业务系统调用
2、轮询渠道查状态
定时轮询渠道查询支付/代付订单状态,需要注意有些渠道对查询间隔有要求
3、接收渠道回调
支付服务提供接口,供支付渠道回调,需要注意加验证,防止伪造请求
4、供业务系统查询用接口
供业务系统查询提现/充值订单状态,实现通常是直接查相关本地表
5、回调业务系统
业务系统提供回调接口,支付服务在拿到最终的订单状态后回调该接口,业务方必须要验签保证请求合法。注意:回调地址尽量在每次充值/提现接口时提供而不是通过配置的方式,否则容易造成反向依赖。
6、与渠道对账
通常渠道都会提供对账单接口,注意笛卡尔乘积的处理方式。
7、调账
调账通常将对账不平的记录展示出来,通过人工进行调账。
允许的调账方向有
成功 -> 失败 (渠道异常订单)
失败 -> 成功(渠道异常订单)
处理中 -> 成功 (渠道正常订单)
处理中 -> 失败(渠道正常订单)
8、与业务系统对账
和渠道与支付服务的对账类似
9、业务系统如何处理长短款
系统长款,这种场景较好处理,直接将相应金额发放到业务系统账户余额。
系统短款,这种场景不好处理,首先检查用户的余额是否还可以补扣,是则执行补扣,否则记入坏账(依具体需求而不同)
支付代付那些坑:##
1、支付状态
不同支付渠道通常会有一系列不同的状态定义,有些定义晦涩而难懂,不同知识背景的开发人员常常会有理解上的偏差,一定要和渠道方反复确认,以便正确将对方的状态转换为统一的成功、失败、处理中状态。另外需要注意,万一出现了协议以外的状态,一般转换为我方的处理中状态。曾经发生过这样的案例:
//协议文档定义 1=支付失败,2=支付成功
if(status==1){
//支付失败处理
}else{
//支付成功处理
}
结果渠道返回协议文档没有定义的status=3(另一种原因的支付失败),结果当支付成功处理了,造成了不少的损失。
2、事务的处理
曾经发生过这样的案例(用户提现):
//1事务开始
//2扣减用户余额
//3调用渠道代付接口
//4其他一些业务操作
//5提交事务
首先,该开发人员是有一定的提现逻辑处理经验,知道先扣减用户余额再调用渠道代付接口(想想如果扣减用户余额和调用代付接口顺序反了会发生什么?),但是在执行到其他一些业务操作时发生了异常,整个事务回滚,结果造成用户可以重复提现,损失了N万。当然,我们可以很快地发现问题点在于事务过大,但是当你面对一个复杂的项目,尤其是到处都使用了Spring AOP实现的事务时,要保证清晰的思路实属不易。
3、重复支付
某支付渠道,具有notify机制,即调用支付接口后,支付渠道会回调我方提供的接口以通知我方支付状态,特别要注意渠道可能通知多次,即我方必须实现幂等处理。开发人员在总结了前人的经验基础上,写出了以下代码:
//收到支付回调
//查询支付订单,如果为处理中则继续
//事务开始
//将支付订单状态修改为成功 update 支付订单 set status = 'SUCCESS' where order_id = 'abc';
//增加用户余额
//提交事务
//响应给渠道SUCCESS
看起来似乎已经无可挑剔了。但是,如果渠道方重复回调(或回放攻击),则可能有多个线程同时进入 //将支付订单状态修改为成功 这一步,结果造成重复增加用户余额。所以还需要加CAS操作:
update 支付订单 set status = 'SUCCESS' where order_id = 'abc';
改为
update 支付订单 set status = 'SUCCESS' where order_id = 'abc' and status = 'DOING'
并增加判断,如果update的结果为空则退出,不执行增加用户余额操作。
4、协议的升级
这一点在对接小渠道时要尤其注意,通过前面的介绍,我们留意到了我们的if...else判断要预防协议外的状态出现。还有一个糟糕的是渠道竟然连定义都全改了,比如升级前:0=成功,1=失败,升级后成了:0=失败,1=成功。如果你的系统有对接小支付渠道,请和他们保持良好及时的沟通,并祈祷他们升级时不会改变协议。
5、重试
通常,渠道会让商户自己定义订单号,不同商户的订单号可以相同,同一个商户订单号是惟一的,后续的一系统操作以该订单号作为幂等字段。但也有一些渠道,如X南支付...,也就是说,你调了他们的接口之后超时了,如果再次以同样的参数尝试,则可能就重复支付/代付了。很多人说我不重试就行了嘛,但是你要知道,很多框架或底层包(如HttpClient)都已经默认帮你实现了重试功能(防止网络抖动影响服务质量),你一不注意,就会出问题。
6、重放
重放,即黑客将你发给渠道的报文监听后再多次发给渠道,与用不用HTTPS无关。一般防重放的方法是服务调用方增加将当前时间戳和其他字段一起加密后加到报文,服务提供方拿到时间戳(重放黑客无法修改时间戳)与系统时间比对,如间隔过久则拒绝执行(双方的服务器时间要同步)。当然这些需要渠道方的支持。
7、时序
支付处理的正确业务时序如下
//调用支付接口
//增加余额操作
代付处理的正确业务时序如下
//扣减(冻结)用户余额
//调用代付接口
时刻想着对系统有利的方向就对了,毕竟多扣了用户的钱或者用户提现没到账但余额被扣,还是可以通过调账处理回去。如果如果用户重复提现或者“充100送900”可能就成了坏账,再也收不回来了。
8、金额单位
不同商户可能会使用不同的金额单位,有用分的,有用元的,一般情况建议系统内统一用分再与渠道定义的单位互相转换,一定要注意转换逻辑的准确。
9、加密及安全
尤其是支付服务提供给外部服务用的回调接口,必须做好安全校验,因为这些接口通常是对互联网开放的(要按收很多渠道的回调嘛)。
10、商户号信息保密
特别是有提现业务的系统,第三方支付通常是使用资金池模式,一般它不记每笔钱是属于谁的,也就是爱转给谁都行。如果你还在用properties文件配置商户号信息,而且所有开发人员都能访问生产的properties配置的话就要小心了。曾发生过一个案例,就是开发人员拿到了代码,又拿到了商户号私钥等信息,然后跑一遍测试用例就把(XX万)提到自己卡里了,当然开发人员最终受到应有的处罚了。但难道就没有办法提前防范了么,答案是有的,这种情况很适合使用配置中心
将生产环境配置中心中的机密配置只开通权限给特定的人,开发人员也无法接触。
11、超时
业务系统在调用支付服务会超时,支付服务调支付渠道会超时,超时了千万不要当成支付/代付成功或失败,而是要当成不明确的处理中状态,然后通过轮询主动查询或对方主动回调的方式取得最终的状态。
12、不要幂等
在服务化设计中,我们经常会对某个接口有幂等性设计,但对支付和代付接口,不要这样设计,幂等很多时候是为了保证最终成功,但支付不同,支付由于渠道的存在,不能保证最终成功,重试反而增加了很多不确定性。
13、支付路由
支付路由是必不可少的,最简单的路由实现是可以通过手工切换,鸡蛋不要放在同个篮子里,如果某个渠道暂时不可用,可以临时切换流量到到可用的渠道。也有按费率高低路由选择渠道的,按业务需求而定。
14、协议转换
有些支付渠道不是HTTP协议,而是自定义的TCP协议,这就需要我们实现一层协议转换层将它转成内部标准统一的协议。
15、前置proxy软件
有些支付渠道会要求你的请求发给前置proxy,再由proxy与对方通讯,通常需要搞清楚proxy软件运行的环境(windows的都有......),是否支持多机部署等。
16、专线
如果需要接专线,提前搞清楚是否双路,一定要提前准备。
17、银行编码标准
支付和代付经常会涉及银行编码,不同渠道的银行编码标准不尽相同,不同渠道支付的银行也不相同。建议系统内提前定义好银行编码,然后将渠道的编码转换为统一的银行编码,对业务系统屏蔽不同的渠道银行编码定义。
18、日志表
与渠道的交互,请求和响应都必须记log,必要时还要记录到单独的日志表。相信我,当出现问题时,它会让你排查问题更加便捷。
19、支付/代付超期限完成
在主动向渠道查询支付订单状态时,因为数据量的关系,通常会加个限制,比如只查半个月内的订单,那如果半个月前的订单完成了,但对方又由于各种原因没有通知到我们呢?通常是通过渠道对账文件发现,然后手工调账完成。
20、再议安全
支付服务通过各种手段保证了自身的安全,但它没办法保证它的调用方的安全,如果调用方被攻破,那如何保证资金的安全呢?
通常是从业务上加以限制,如大额提现审批,通过人工防止虚假的大额提现申请。
大额保住了那小额呢?通常是业务上加同银行卡进出限制,并且用户银行卡信息由支付服务管理(对修改用户银行卡信息做业务限制)。
总结:汽车驾驶有一项技术叫防御性驾驶,它始终为其他驾驶员或环境发生了对自己不利的方向发展做准备,而对接第三方支付也与之类似,始终为第三方支付渠道或用户的(非法)行为发生了对自己不利的方向发展时刻做好准备。