本文与博客园文章同步
场景
餐厅提供了网络点餐服务,用户通过微信能很方便的进行点餐并支付,享受餐厅提供的各种餐饮服务。其中可靠的支付服务是其中的核心环节之一,如果支付出了问题,对餐厅或用户都是一个损失,甚至会引起纠纷。如何避免发生这样的问题或者是把发生这样问题的概率降到最低,那就需要结合业务特点和使用场景来仔细分析隐藏的问题。
下面以微信支付中的2种支付场景来解析一下对接过程中遇到的问题以及如何解决
条码支付
对于支付宝和微信的条码支付,都是没有支付成功回调的。这点必须注意,那么基于这个特点,服务器对接了条码支付,就需要考虑到如何可靠的获得支付结果,避免支付单边产生。
下面来举例几种做法,一一来说明各种问题
假设wechat.scanPay() 一次就能确定支付结果,不考虑返回用户正在支付中的场景,后面会提供方案
做法1
同步请求
public order scanPay()throws PayException{ try{ beginTransaction order = createOrder result = wechat.scanPay order.makeSuccess commit } catch(Exception e){ rollback throw new PayException("pay fail") } return order }
问题解析
- 到微信的请求在自身数据库的事务过程中,会增加事务操作时间,微信支付并没有想象中的稳定。
- 当微信支付请求成功,执行数据库代码时会发生异常,有可能是代码bug,也有可能是数据库操作和提交事务都会发生,如果遇到数据库问题,整个事务不会提交,直接回滚,因此刚刚创建的订单就无法与微信支付对应上,对于接下来的交易也会被中断。网上好多人给出的解决方案是把代码进行try catch包起来,一旦遇到异常,catch然后记录日志等其他补救操作。如果这样做了,也许你一辈子也不会遇到问题,但是问题仍然存在:如果catch块的代码由于网络等其他问题也出错了,那职能靠人工去找数据了,代码不能保证一定能执行到catch块,例如:断电、机器重启、进程被干掉等。
- 对于数据库事务都会设定最长超时,如果微信请求过程较长,有可能造成事务超时,无法提交。情况与上面描述就类似了
改进后的做法2
同步请求
public order scanPay()throws PayException{ try{ beginTransaction order = createOrder order.makePaying commit } catch(Exception e){ rollback throw new PayException("pay fail") } try{ result = wechat.scanPay if (result == fail) { ..... } ....... } catch(Exception e){ order.makeFail throw new PayException("pay fail") } try{ beginTransaction order.makePaySuccess commit } catch(Exception e){ rollback throw new PayException("pay fail") } return order }
比做法1进步了,至少事务中不会有微信支付请求
- 创建订单成功后请求微信支付失败,请求微信支付成功后更新订单状态失败。以上2个情况都无法让代码继续运行,try catch 微信支付异常,然后做处理,照样解决不了问题。
如果是同步的请求方式,这个问题也能解决,但是解决的方式不太漂亮会影响用户体验。最好的方式就是采用消息队列进行异步的处理
改造支付为异步请求
请求分为多个阶段,其中MQ表示的是消息队列。
我们对消息队列的要求是不能丢失消息,保证至少发送消息一次。只要不删除消息,消息就会被再次发送。
MQ的可靠性不再本文范围之内,请自行Google 可靠消息队列
//创建订单,然后返回给调用者 public order createOrder(){ order = createOrder return order } //调用者发起支付请求,由于异步操作,不会返回任何结果 public void reqeustScanPay(orderId,amount,code){ mqclient.sendMessage(order); } //调用者轮询支付结果 public void checkOrder(orderId,firstQueryTime) throws PayException{ currentTime = System.currentTime if (currentTime > firstQueryTime + 2min) { //轮询2分钟还没有获得结果,终止轮询,订单等待支付服务自动处理 } order = getOrderbyId(orderId) if (order == null) { throw new PayException("order not exist") } if (order.isPaying) { //如果是支付中结束本次查询 } else if (order.isPaySuccess) { //支付成功.... //终止轮询 } else if (order.isPayFail) { //支付失败.... //终止轮询 } }
发送的支付消息开始异步消费
消息会处理以下几个方法:
//当接收到请求扫码支付时调用。扫码返回结果可能有多种,需要一一判断,根据不同条件来决定继续查询或者是撤销支付或支付成功、失败 public void processScanPay(message){ try{ result = wechat.scanPay if (result == userpaying) { send query memsage into MQ 延迟3s接收消息 delete message return } else if (result == error){ send cancel message into MQ delete message return } else if (result == duplicate pay){ send paySuccess message into MQ delete message return } else if (result == paySuccess){ send paySuccess message into MQ delete message return } //其他情况不举例了,根据实际情况来发送查询或者撤销消息 } catch(Exception e){ send query memsage into MQ 延迟3s接收消息 delete message } } //当支付结果不明确或状态为支付中、用户正在输入密码时调用。微信支付有可能不会立刻返回支付结果,例如用户需要输入密码等情况,就需要多次查询才可以获得结果,但是查询也需要有限制,例如2分钟如果超过,就强制撤销支付,这样就不会造成支付单边 public void processQueryPay(message){ orderId = message.orderId firstTime = message.firstTime currentTime = time.currentTime if (currentTime - firstTime > 2min) { send cancel message into MO delete message return } result = wechat.query(orderId) if (result == userpaying) { //用户还在输入密码,或者微信正在跟银行交互 send query memsage into MQ 延迟3s接收消息 delete message return } else if (result == paySuccess) { //支付成功是最终结果 send success message into MQ delete message return } else if (result == payFail) { //支付失败,是最终结果 send fail message into MQ delete message return } else if (result == unknown) { //微信系统异常经常遇到,因此对未知结果进行再次查询处理 send query memsage into MQ 延迟3s接收消息 delete message return } else if (result == orderNotExist) { //为什么订单会不存在,刚才明明发起了支付?原因就是发起支付后,如果timeout,请求有可能没有被微信处理或者根本没有接受到请求 send cancel message into MO delete message return } //其他情况不举例了 } //接收到支付结果时调用 public void processPay(paySuccess){ if (paySuccess) { beginTransaction lock order order.makePaySuccess commit } else{ beginTransaction lock order order.makePayFail commit } delete message } //撤销支付,直到撤销成功才删除消息,否则有可能出现支付单边 public void processCancel(){ orderId=message.orderId wechat.cancel(orderId) delete message }
方案说明
- 在创建订单阶段,如果订单创建失败,前端直接会返回错误给用户,提示支付失败,不影响资金变动
- 在创建订单阶段,如果请求超时,同样可以当做失败来处理。但是这个超时的请求会带来2种 可能性:第一种:请求收到了,但无法在指定时间内响应,订单会创建成功或者创建失败。第二种,请求没有收到,订单也没有创建成功。
- 在请求支付阶段,请求失败,表示发送消息失败。不影响资金变动
- 在请求支付阶段,请求成功,表示发送消息成功。消息发送成功,只要你代码没有bug,消息一定会正常收到,然后进行支付处理
- 在请求支付阶段,如果请求超时,也会有2种可能。第一种:请求收到了,无法在指定时间相应,消息发送成功。第二种:请求收到了,发送消息失败。都没有关系,客户端只要继续轮询支付结果就可以。
- 轮询支付结果阶段,任何的超时,都忽略,只要重复轮询就可以,除非获得明确的响应结果。当然也不可能无限制的轮询,可以在指定之间范围之内,通常我们是在1-2分钟之内。如果超过这个时间还没有获得准确的支付结果,那就可以直接请求撤销本次支付
- 轮询支付结果阶段,还会出现的一种状况就是客户端crash崩溃退出,无法继续刚才的轮询。因此,支付的任何情况都不能把可靠性交给客户端来保证。需要服务端来保证支付的最终结果
- 服务端其实也存在一个轮询,但是这个轮询不是foreach。试想一下,如果是foreach等待结果,那一次请求的时间就不可控了,当支付并发量大了之后,服务器就无法接收新的请求了。因此通过消息队列来实现轮询,这样每次都是非常短时间的数据处理,然后发送一个continue的消息,接收后再次进行后面的处理。这样的好处就是每次请求都是短时间,不会阻塞服务器的其他请求
- 任何网络请求都有可能超时,超时后必须查询才可能获得结果,但是也不一定会立刻获得结果,必须进行多次查询,因此还是通过消息队列来实现foreach的循环调用
- 为什么要延迟3s接收消息,当用户需要输入密码时,间隔3s,基本时间就足够了,如果间隔太小,查询过多也没有意义,此时间可以根据实际情况调整
会出现的问题
微信扫码支付超时,然后发起查询,返回结果告知订单不存在,当时考虑订单不存在就不处理了,但实际上,过了一会订单支付成功。因为在查询的时候,支付的请求还没到达,之后支付的请求也到达了。因此在这个场景下,对于不存在的订单也必须进行一次撤销操作。这时,撤销的功能就非常重要了,它能最彻底的防止支付单边。
微信如何设计的撤销
先看一个错误的设计
public void cancelOrder(orderId)throw CancelException{ beginTransaction order = db.getOrderId(orderId) if (order == null) { throw CancelException("order not exist") } order.makeCancel commit }
当扫码支付的请求没有到达时,如果使用上面的撤销我们得到的结果只能是订单不存在,并没有把订单进行撤销。但是支付请求有可能是在撤销请求之后到达,这时支付就会成功,这与我们期待的结果是不一样,我们希望用户的支付是无效的。
正确的设计
//根据订单号进行支付撤销,不管订单是否存在,只要不是关闭或者失败状态,都可以进行撤销 public void cancelOrder(orderId)throws CancelException{ beginTransaction order = db.getOrderId(orderId) if (order != null) { if (order.isPayFail) { throw new CancelException("order is fail"); } else if (order.isClosed) { throw new CancelException("order is fail"); } ....... order.makeCancel//把钱退回 } makeCancelByOrderId(orderId) commit }
那么 makeCancelByOrderId(orderId) 里面做了什么呢,这里大致的表结构如下 table:tb_order_cancel{ orderId varchar(50), //orderId是唯一索引或主键 createTime bigint } //记录了被撤销的订单号 public void makeCancelByOrderId(orderId){ try{ insert into tb_order_cancel value(orderId,createTime); }catch(DuplicateKeyException ignore){ //ignore exception } } 支付请求到达时,会先判断是否订单号已经被撤销,如果被撤销,就不再进行支付 public order scanPay(orderId,amount) throws PayException{ cancelOrder = select from tb_order_cancel if (cancelOrder != null) { //说明被撤销了,不应该完成支付 throw new PayException("order was canceled") } do pay ...... }
不管是支付还是撤销请求,谁先到达,最终都会被撤销,并把钱退回用户微信
以上的方法基本能覆盖了条码支付的所有问题。这个只是其中的一部分,还有退款、撤销业务其实只要按照以上的思路来做,就没什么问题了。
开发完成后,需要模拟各种网络情况和返回结果进行测试。这个测试条件比较复杂,只能一个个模拟。
针对于支付这里,我开发了一套基于aop的动态模拟功能,提供给测试人员。测试人员通过界面输入各种条件,就能模拟对应的问题进行测试。目前这个代码还在改进,暂时不会开放出来。
总结
- MQ是基础的保障设施,如果没有它,啥也干不了。当然我们可以用定时任务来替代,不过这个方案在数据和并发少的时候还行,一旦大了,那就不好办了。
- 经过网络的请求,timeout必须要考虑。timeout会产生2个结果,一个是对方收到了,一个是对方没有收到
- 如果要做到无单边,就一定要配合查询、撤销操作。你也可以等定时自动对账来找出差错,这个没问,但是如果能及时退钱,就避免了用户和商户的纠纷。以前我们的系统就是没有及时退回,用户经常投诉,无奈只能及时人工解决。自从方案换了之后,基本上一旦遇到问题1分钟之内,都会把钱及时退回,不用人工操作。
- 一定要考虑到任何事情都是有可能发生,但是需要坚信2个前提:消息队列是可靠的,数据库事务提交后持久化是可靠的。如果不考虑全面,我只能告诉你,当你遇到支付宝和微信bug N个小时的时候,你就知道人工搞数据是多么惨了
- 支付中的订单允许再次发起支付
下一篇,我会讲一下如果可靠的对接微信网页支付,网页支付的例子我会增加组合支付的部分,扫码支付这里的组合支付这次先忽略掉。
相关推荐
在Unity游戏开发中,接入第三方服务如微信登录、分享和支付功能,以及支付宝SDK,能够极大地提升用户体验并促进用户互动。下面将详细讲解如何在Unity中实现这些功能。 首先,我们要明白Unity是一个跨平台的游戏开发...
在本文中,我们将深入探讨如何在Unity游戏引擎中接入微信和支付宝这两大主流移动支付平台,以便在Android项目中实现无缝的支付功能。这个压缩包包含的文件是Android Studio工程的相关资源,它们对于理解集成过程至关...
本文将详细介绍如何使用Java语言来接入微信APP支付和支付宝APP支付。这两个支付平台提供了丰富的API和SDK,使得开发者能够方便地集成到自己的应用中,实现便捷的支付功能。 1. **微信支付**: - **接入流程**:...
Delphi开发的微信、支付宝支付源代码,无需域名,只需输入微信公众号、微信商户号、微信API密钥;支付宝APPID 、支付宝验签密钥文件;订单编号、支付金额,就可以完成以下微信、支付宝支付。 1、生成微信支付二维码...
浏览器中(PC或H5)访问链接,产生支付二维码使用微信扫码支付。支付宝也包含两种支付方式:1.手机访问链接,调用支付宝APP进行支付;2.电脑访问链接,产跳转到支付宝官网产生付款码进行支付。具体实现效果可以浏览...
综上所述,"unity接入微信iOS 支付代码工具"是一个综合性的解决方案,涵盖了Unity与iOS平台间的交互、微信支付SDK的集成、支付流程的管理以及错误处理等多个方面,极大地简化了Unity游戏接入微信支付的复杂度。...
在.NET开发环境中,整合微信、支付宝和银联支付是常见的需求,这主要涉及到移动支付和在线支付的集成。本文将详细讲解如何使用C#.NET进行这三方支付的整合。 首先,微信支付API提供了多种接口,如JSAPI、Native、H5...
本文将详细讲解如何使用Delphi作为开发工具来实现个人用户接入支付宝和微信支付的功能。 首先,Delphi是一种强大的面向对象的编程环境,基于Object Pascal语言,广泛用于开发桌面应用程序。在Delphi中实现支付宝和...
在标题“微信支付+支付宝支付”中,我们可以理解这是一个包含两部分的项目:微信支付模块和支付宝支付模块。 1. **微信支付**: 微信支付的接入需要注册成为微信支付商户,获取到APP ID、商户号(MCHID)、API密钥...
本资源为Unity开发者提供了在iOS平台上接入微信与支付宝支付的完整解决方案,包括详细的文档、SDK和源代码。通过本资源,您将学习到如何在Unity项目中集成微信支付和支付宝支付功能。 适用人群: 本资源适合有一定...
在IT行业中,集成支付宝和微信支付到个人应用是常见的需求,尤其对于Delphi开发者来说,了解如何实现这一功能显得尤为重要。本教程旨在帮助Delphi个人用户掌握如何将支付宝和微信支付接口集成到自己的应用程序中,为...
spring+mybatis接入微信支付,支付宝支付(包含微信公众号支付和H5支付),可以作为独立的模块使用也可以作为公共的支付接口
1/netCore 接入微信支付,内含普通支付,微信V3支付,服务商模式 2、【普通支付,支付回写,退款,分账给个人,服务商模式支付,服务商模式支付回写,服务商模式分账给子商户,V3支付退款】 3、源码分享
本文将基于提供的资源“swift-简单封装微信与支付宝支付代码帮助大家快速完成iOS端支付的接入”来详细讲解如何在Swift项目中实现这两个支付方式的接入。 首先,我们需要了解Swift编程语言的基础知识,它是一种由...
2. **条码支付**:与微信支付不同,支付宝的条码支付是指用户展示支付宝App内的条形码或二维码,由商家扫描完成支付。 3. **RSA签名与验签**:支付宝同样使用RSA加密算法进行签名和验签,确保交易的安全性。这里的...
微信/支付宝 H5支付接口(C#版demo)
在IT行业中,聚合支付是一种将多种支付方式整合在一起的支付解决方案,使得商家可以接受不同支付渠道的支付,如微信支付和支付宝。对于Delphi开发者来说,实现这样的功能可以帮助他们为客户提供更加便捷的支付体验。...
c#开发的微信、支付宝支付源代码,无需域名,只需输入微信公众号、微信商户号、微信API密钥;支付宝APPID 、支付宝验签密钥文件;订单编号、支付金额,就可以完成以下微信、支付宝支付。 1、生成微信支付二维码...
"VB6微信支付&支付宝支付到个人账户源代码"这个资源提供了在VB6环境中实现微信支付和支付宝支付功能的源代码,这对于那些希望在自己的应用程序中集成这两种主流支付方式的开发者来说非常有价值。 微信支付是腾讯...
- 对于刷卡支付,微信提供了“刷卡支付”API,通过调用此接口,生成条形码或二维码,用户使用微信App扫描后完成支付。 - 实现订单状态同步和异步回调处理,确保支付状态的准确性和及时性。 3. **技术挑战与注意...