如何防止重复支付
1、业务订单和支付订单不是一个概念,我想讨论的是已经生成的支付单如何防止重复支付。一个业务订单是允许对应多个支付单的,也就是分多次支付,但是单个支付单不允许被支付两次。从系统层面来说,同一个业务订单ID可以对应多个支付流水号,但是同一笔支付流水只能向通道方发送一次。
2、重复支付有以下可能的原因:
1.客户端多次提交。可能由于弱网络环境,或者客户端按钮没有做限制用户手速过快等。
2.由于系统代码或设计问题导致的状态不一致,例如通道方超时的订单被系统判定为失败,或者高并发环境下没有做好锁的控制等等。分布式系统之间常常是弱一致性的设计,因此如果系统上下游没有做好足够的规约,很容易出现重复支付或重复出款。
3、针对上述原因,可以思考相对应的解决方案。客户端多次提交,可以选择在前端做控制,也可以不控制,任由用户进行多次支付,后续对金额和状态做检查,给用户发起退款也可以。对公司不会有什么资金损失,就是会损失一点用户体验。。。。系统上下游的规约或者代码问题是最应当关注的问题,整个支付系统要做好规范。
对于发起支付的订单,必须先从库中检查是否已经存在。
请求通道方超时或者上下游通信网络超时,结果码不能配置终态的结果码。
用户主动发起的多次支付,要对关联的业务订单做校验。
未知的状态要有补偿的策略来查询,必须在查询之后再判断是否关单或者冲正。
常见的解决方案是宁可让订单一直不置终态,等财务结算对账后再确认,也不要随意的允许用户或系统重新发送。
重复支付再思考
我在思考这样一个场景:一笔支付请求,愉快的从渠道拿到了处理结果,就在此时,啪唧,停电了。那么在系统全部重启之后,我方db中,该订单由于还没来得及更新任何状态,还是一副人畜无害的待处理模样。
此时系统重新处理该订单,势必会导致重复请求通道方。运气好的话,通道方帮你做了幂等,运气不好的话,该如何处理?
我的想法:如果订单在交易状态之外还存在发送状态,那么在发送之前就应该修改发送状态为已发送。那么在系统重启之后,已发送但是没结果的订单由系统自动发起补偿查询,如果结果不存在或者已经成功,就按正常流程处理。
如果订单不存在发送状态,交易状态是待处理,那应当如何处理?我认为此问题无解。因此渠道应用层必须对订单建立发送状态。
补充思考
之前说的不指定发送状态则问题无解可能有点绝对。我司的做法,是在发送通道方前一步才将订单落地,那么如果在发送时断电,db中存在该订单,则重发查询,根据通道方响应来判定是否需要重发。(一般来说哪怕交易不存在也不会直接重发该订单,而是由交易订单重新生成支付订单)
关于结果码处理的一点讨论
今天在搭建新系统渠道部分的时候,和组长关于结果码的映射有一点讨论,记录在此。在处理银行端的结果时,结果码映射是一个看似不起眼,但是最容易出现资损的步骤。例如出款订单的结果码能否映射成失败,扣款订单的结果码能否映射成成功,查询订单的结果码和其对应的交易订单的结果码的映射,等等。我们新系统在渠道层分了两层应用,第一层可以称作渠道服务,主要职责是对上游暴露接口,落地支付订单,落地银行端结果等等,第二层可以称作渠道前置,主要职责是拼接报文,报文签名验签,敏感字段加密,对银行结果做标准化等,这一层是一个单纯的通道,不会有db的操作。
对银行的结果做标准化就涉及到结果码的映射。我的意见是将网联的结果码和我们系统的标准结果码的映射保存在数据库+redis里,可以放在渠道服务里处理,也可以放在渠道前置处理。
放在渠道前置做映射,如果渠道前置不允许操作db,那么可以从缓存里读,然后将映射完的标准结果码返回给渠道服务。这种方式缓存的过期处理会麻烦一点,可以配成永不过期,需要另外做一个后台模块来提供手动刷新功能,要么就通过定时job来更新。这样做看起来实时性会很差,但是仔细想想其实并不存在这种实时性的需求,因为有新的结果码需要配置映射关系时,必然是要人工配置+人工审核的,人工审核完了顺手刷一下缓存也不是什么大不了的事。
也可以放在渠道服务里处理,缓存过期可以配成固定的时间。服务在读缓存时如果已过期,可以直接读db并顺便刷新一把缓存。这种设计也有一个双写的问题比较棘手。并且缓存的过期时间很难把握,实时性其实同样会存在问题。
不过最后讨论下来选定的方案是全部写成枚举,在代码里映射。理由是新的结果码不会经常出现,万一出现则修改代码重新发布即可。。。害。。