`
ak478288
  • 浏览: 73525 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

接入微信、支付宝条码支付的正确姿势

阅读更多
本文与博客园文章同步
 
场景
餐厅提供了网络点餐服务,用户通过微信能很方便的进行点餐并支付,享受餐厅提供的各种餐饮服务。其中可靠的支付服务是其中的核心环节之一,如果支付出了问题,对餐厅或用户都是一个损失,甚至会引起纠纷。如何避免发生这样的问题或者是把发生这样问题的概率降到最低,那就需要结合业务特点和使用场景来仔细分析隐藏的问题。
 
下面以微信支付中的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
}

 

方案说明
  1. 在创建订单阶段,如果订单创建失败,前端直接会返回错误给用户,提示支付失败,不影响资金变动
  2. 在创建订单阶段,如果请求超时,同样可以当做失败来处理。但是这个超时的请求会带来2种 可能性:第一种:请求收到了,但无法在指定时间内响应,订单会创建成功或者创建失败。第二种,请求没有收到,订单也没有创建成功。
  3. 在请求支付阶段,请求失败,表示发送消息失败。不影响资金变动
  4. 在请求支付阶段,请求成功,表示发送消息成功。消息发送成功,只要你代码没有bug,消息一定会正常收到,然后进行支付处理
  5. 在请求支付阶段,如果请求超时,也会有2种可能。第一种:请求收到了,无法在指定时间相应,消息发送成功。第二种:请求收到了,发送消息失败。都没有关系,客户端只要继续轮询支付结果就可以。
  6. 轮询支付结果阶段,任何的超时,都忽略,只要重复轮询就可以,除非获得明确的响应结果。当然也不可能无限制的轮询,可以在指定之间范围之内,通常我们是在1-2分钟之内。如果超过这个时间还没有获得准确的支付结果,那就可以直接请求撤销本次支付
  7. 轮询支付结果阶段,还会出现的一种状况就是客户端crash崩溃退出,无法继续刚才的轮询。因此,支付的任何情况都不能把可靠性交给客户端来保证。需要服务端来保证支付的最终结果
  8. 服务端其实也存在一个轮询,但是这个轮询不是foreach。试想一下,如果是foreach等待结果,那一次请求的时间就不可控了,当支付并发量大了之后,服务器就无法接收新的请求了。因此通过消息队列来实现轮询,这样每次都是非常短时间的数据处理,然后发送一个continue的消息,接收后再次进行后面的处理。这样的好处就是每次请求都是短时间,不会阻塞服务器的其他请求
  9. 任何网络请求都有可能超时,超时后必须查询才可能获得结果,但是也不一定会立刻获得结果,必须进行多次查询,因此还是通过消息队列来实现foreach的循环调用
  10. 为什么要延迟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的动态模拟功能,提供给测试人员。测试人员通过界面输入各种条件,就能模拟对应的问题进行测试。目前这个代码还在改进,暂时不会开放出来。
 
总结
  1. MQ是基础的保障设施,如果没有它,啥也干不了。当然我们可以用定时任务来替代,不过这个方案在数据和并发少的时候还行,一旦大了,那就不好办了。
  2. 经过网络的请求,timeout必须要考虑。timeout会产生2个结果,一个是对方收到了,一个是对方没有收到
  3. 如果要做到无单边,就一定要配合查询、撤销操作。你也可以等定时自动对账来找出差错,这个没问,但是如果能及时退钱,就避免了用户和商户的纠纷。以前我们的系统就是没有及时退回,用户经常投诉,无奈只能及时人工解决。自从方案换了之后,基本上一旦遇到问题1分钟之内,都会把钱及时退回,不用人工操作。
  4. 一定要考虑到任何事情都是有可能发生,但是需要坚信2个前提:消息队列是可靠的,数据库事务提交后持久化是可靠的。如果不考虑全面,我只能告诉你,当你遇到支付宝和微信bug N个小时的时候,你就知道人工搞数据是多么惨了
  5. 支付中的订单允许再次发起支付
 
下一篇,我会讲一下如果可靠的对接微信网页支付,网页支付的例子我会增加组合支付的部分,扫码支付这里的组合支付这次先忽略掉。
 
 
分享到:
评论

相关推荐

    白色简洁风格的软件UI界面后台管理系统模板.zip

    白色简洁风格的软件UI界面后台管理系统模板.zip

    自动软包电芯极耳短路测试精切一体机sw17可编辑全套技术资料100%好用.zip

    自动软包电芯极耳短路测试精切一体机sw17可编辑全套技术资料100%好用.zip

    RuntimeException如何解决.md

    RuntimeException如何解决.md

    云链客服需要注意的事项

    定期分析系统的投资回报率(ROI)是确保企业在实施云链客服系统后获得实际效益的关键步骤。以下是一个系统的框架和方法,帮助您有效地进行投资回报率分析。 投资回报率(ROI)分析框架 一、定义投资回报率 投资回报率(ROI)是衡量投资效率的指标,通常通过以下公式计算: ROI= 成本 收益−成本 ​ ×100% 收益:通过实施系统所带来的直接经济利益,例如收入增加、成本节省等。 成本:系统的实施和运营成本,包括初始投资和持续运营费用。 二、确定收益来源 直接收益 销售增长:由于客服系统提升了客户满意度和响应速度,导致客户购买量增加。 客户保留率提高:系统帮助降低客户流失率,保持长期客户关系。 跨卖和追加销售:通过更好的客户互动和数据分析,提升交叉销售和追加销售的机会。 间接收益 运营效率提升:客服人员的工作效率提高,能够处理更多客户请求,减少人力成本。 品牌形象增强:客户体验的改善有助于提升品牌形象,吸引新客户。 客户忠诚度提升:满意的客户更可能成为回头客,提升长期收益。

    白色简洁风格的室内设计案例源码下载.rar

    白色简洁风格的室内设计案例源码下载.rar

    (177373454)html+css+js学习代码.zip

    html+css+js学习代码html+css+js学习代码html+css+js学习代码 html+css+js学习代码html+css+js学习代码html+css+js学习代码 html+css+js学习代码html+css+js学习代码html+css+js学习代码 html+css+js学习代码html+css+js学习代码html+css+js学习代码 html+css+js学习代码html+css+js学习代码html+css+js学习代码 html+css+js学习代码html+css+js学习代码html+css+js学习代码 html+css+js学习代码html+css+js学习代码html+css+js学习代码 html+css+js学习代码html+css+js学习代码html+css+js学习代码 html+css+js学习代码html+css+js学习代码html+css+js学习代码 html+css+js学习代码html+css+js学习代码html+css+js学习代码 html+css+js学习代码html+css+js学习代码html+css+j

    三相逆变 单相 三相逆变器 SPWM -stm32主控(输入、输出具体可根据需要设定),本逆变器可以二次开发 本内容只包括 逆变程序,实现变频(0~100Hz)、变压调节,均有外接按键控制(使用

    三相逆变 单相 三相逆变器 SPWM ---stm32主控(输入、输出具体可根据需要设定),本逆变器可以二次开发。 本内容只包括 逆变程序,实现变频(0~100Hz)、变压调节,均有外接按键控制(使用C语言实现)。

    基于STM32单片机的激光雕刻机控制系统设计-含详细步骤和代码

    内容概要:本文详细介绍了基于STM32单片机的激光雕刻机控制系统的设计。系统包括硬件设计、软件设计和机械结构设计,主要功能有可调节激光功率大小、改变雕刻速率、手动定位、精确雕刻及切割。硬件部分包括STM32最小系统、步进电机驱动模块、激光发生器控制电路、人机交互电路和串口通信电路。软件部分涉及STM32CubeMX配置、G代码解析、步进电机控制、激光功率调节和手动定位功能的实现。 适合人群:对嵌入式系统和激光雕刻机感兴趣的工程师和技术人员。 使用场景及目标:① 适用于需要高精度激光雕刻的应用场合;② 为开发类似的激光雕刻控制系统提供设计参考。 阅读建议:本文提供了详细的硬件和软件设计方案,读者应结合实际应用场景进行理解,重点关注电路设计和代码实现。

    北航软件体系架构.7z

    北航软件体系架构.7z

    白色简洁风格的高端汽车预订企业网站源码下载.zip

    白色简洁风格的高端汽车预订企业网站源码下载.zip

    白色宽屏风格的时尚摄影图片网站模板下载.zip

    白色宽屏风格的时尚摄影图片网站模板下载.zip

    (31028834)大数据技术之Hadoop(入门).docx

    ### 大数据技术之Hadoop(入门)知识点详解 #### 第1章 大数据概论 ##### 1.1 大数据概念 大数据是指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合。这些数据具有体量巨大、来源多样化、格式复杂等特点。 ##### 1.2 大数据特点(4V) **Volume(体量大)**:指的是数据量非常庞大。 **Velocity(速度快)**:指数据产生的速度极快。 **Variety(多样性)**:指数据类型多样,不仅限于结构化数据,还包括大量半结构化和非结构化数据。 **Value(价值密度低)**:尽管数据总量很大,但真正有价值的信息可能只占一小部分。 ##### 1.3 大数据应用场景 - **金融行业**:风险控制、精准营销、反欺诈等。 - **零售行业**:客户行为分析、库存管理优化等。 - **医疗健康**:疾病预测、个性化治疗方案制定等。 - **交通物流**:智能交通系统、物流路径优化等。 ##### 1.4 大数据发展前景 随着物联网、云计算等技术的发展,大数据的应用场景将会更加广泛。预计未来几年内,大数据技术将更加成熟,处理能力更强,为

    UnknownHostException(解决方案).md

    UnknownHostException(解决方案).md

    LP3_PLC程序培训_01.zip

    LP3_PLC程序培训_01.zip

    白色简洁风格的重型汽车销售企业网站源码下载.zip

    白色简洁风格的重型汽车销售企业网站源码下载.zip

    白色简洁风格的摄影图片模板下载.zip

    白色简洁风格的摄影图片模板下载.zip

    白色宽屏风格的农家乐有机蔬菜企业网站模板.rar

    白色宽屏风格的农家乐有机蔬菜企业网站模板.rar

    北航智能自主系统.7z

    北航智能自主系统.7z

    白色简洁风格的网络实验室CSS模板.zip

    白色简洁风格的网络实验室CSS模板.zip

    (175218226)利用仿真实现定时器设计的门铃

    门铃是日常生活中常见的一种设备,它通过发出声音来通知人们有访客或者有其他重要事件发生。在信息技术领域,特别是在嵌入式系统中,利用单片机设计定时器门铃是一项基础且实用的技术实践。单片机,即单片微型计算机,因其集成度高、成本低、应用广泛,常被用于各种控制系统的开发。本文将详细探讨如何使用单片机实现定时器门铃的设计。 我们需要了解单片机的基本结构。单片机通常包括CPU、存储器(ROM和RAM)、定时/计数器、输入/输出接口等组成部分。其中,定时/计数器是实现定时器功能的关键。它可以通过对内部时钟脉冲的计数来达到定时的效果,或者对外部事件的计数来实现计数功能。 在设计定时器门铃时,我们会用到单片机的定时器功能。定时器工作模式通常有多种,如自由运行模式、捕获模式、比较模式等。对于门铃应用,我们可能选择自由运行模式,设置一个预设的时间间隔,当定时器溢出时,触发中断,从而启动门铃音效。 实现门铃的代码主要包括以下几个部分: 1. 初始化定时器:这一步通常包括设置定时器的工作模式、初值、分频系数等。例如,我们可以选择定时器工作在自动重装载模式,并设定合适的初值,使得定时器在一定时间后溢出。 2. 中断

Global site tag (gtag.js) - Google Analytics