钱付了,订单还是未支付,用户炸了!

发表于 2年以前  | 总阅读数:502 次

大家好, 之前在 [如何防止订单重复支付] 里和大家聊过掉单导致的重复支付,这篇文章,我们来聊聊,如何防止掉单。

好好的支付,怎么就掉单了?

我听说过下单、买单、脱单……掉单是什么东西?

所谓的掉单,就是用户下单支付,在钱包里完成了支付,结果回到电商APP一看,订单还是未支付……

毫无疑问,用户肯定会炸,结果不是客诉,就是差评。

用户感觉受到了欺诈

那么掉单是怎么来的呢?

我们先来看看订单支付的完整流程:

钱包支付的完整流程

  1. 用户从电商应用点击支付,客户端向服务端发起支付请求
  2. 支付服务会向第三方的支付渠道发起支付,支付渠道会响应对应的url
  3. 以APP为例,客户端通常是会拉起对应的钱包,用户跳到对应的钱包
  4. 用户在钱包里完成支付
  5. 用户完成支付后,跳转回对应的电商APP
  6. 客户端轮询订单服务,获取订单状态
  7. 支付渠道回调支付服务,通知支付结果
  8. 支付服务通知订单服务,更新订单状态

对于支付订单而言,大概可以分为这么几个状态:

支付状态

  • 未支付:用户在点击支付之后,支付服务请求支付渠道之前,处于未支付状态
  • 支付中:用户发起支付后,到跳转到支付钱包,再到完成支付,支付服务获取到最终支付结果之间,属于支付中状态,这个状态下,可以说是一个迷雾状态,电商系统对于用户的支付是不确定
  • 支付成功/失败/取消/关闭:电商系统最终确定了用户在第三方钱包的支付最终结果

看起来没什么问题啊,怎么就掉单了?简单说,就是支付的状态没有同步到,或者没有及时同步到。

掉单发生

1 . 支付渠道的支付回调

发生了一些异常,导致支付服务没有收到支付渠道的回调通知

2 . 支付服务通知订单服务

服务内部出现异常,导致支付状态没有同步到订单服务

3 . 客户端获取订单状态

客户端通常是轮询获取状态,可能会在轮询时间内没有获取到订单状态,结果用户看到未支付

其中1可以称之为外部掉单,2和3可以称之为内部掉单。

接下来我们看看,怎么预防掉单问题。

怎么防止内部掉单

我们先从系统内部的掉单说起,当然在系统内部,稳定性更容易保证,发生掉单的概率还是比较小的。

服务端防止掉单

支付服务和订单服务之间防止掉单,关键就在于尽可能保证支付通知订单支付结果成功,我们一般通过这两种方式。

服务端防止掉单

1 . 同步调用重试机制

支付服务调用订单服务的时候,要进行失败重试,防止网络抖动情况下的调用失败。

2 . 异步消息可靠性投递

同步不稳妥,那就再加一个异步。支付服务投递一个支付成功消息,订单服务消费支付成功消息,整个过程要尽可能保证可靠性,例如订单服务要在完成订单状态更新后再确认完成消息消费。

同步+异步两手策略,基本上可以防范服务端的内部掉单。

至于引入分布式事务(事务消息、Seata)来保证状态一致,我觉得也没有必要。

客户端如何防止掉单

用户支付完成后,跳回电商系统,客户端会轮询一下订单的状态,通常两三秒内,就会得到订单完成支付的结果,这个过程出现问题的概率相比是非常低的。

但是也不排除,很小概率下,客户端轮询一段时间,还没得到结果,那么只能结束轮询,给用户展示未支付。

这种情况,通常问题也是出在服务端,没有及时更新订单的状态,最主要的还是要处理服务端的掉单,保证服务端能及时同步支付订单的状态。

但是一旦服务端的订单状态变更了,也要尽可能同步到客户端,不能让用户一直看到未支付。

客户端和服务端之间,同步状态,无非就是推和拉:

1 . 客户端轮询

客户端判断用户未支付之后,通常会进行订单倒计时。

倒计时

这里再提一下?大家觉得这种倒计时是怎么实现的呢?纯客户端组组件倒计时吗?

——肯定不行,通常是客户端组件倒计时,定期向服务端请求,检查倒计时时间。同样的,这种情况下,客户端也可以检查支付状态。

2 . 服务端推送

说真的,服务端推送,看上去是一种很美好的方案,Web端可以使用Websocket,APP端可以用自定义Push,大家可以看看[我有 7种 实现web实时消息推送的方案,7种!] 。但实际上,推送的成功率经常不那么理想。

怎么防止外部掉单

相比较内部掉单,外部掉单发生的概率就大很多,毕竟和外部渠道的对接,不可控的因素更多。

要防止外部掉单,核心就是四个字:“主动查询”,如果只是等待第三方的回调通知,风险还是比较大的,支付服务要主动向第三方查询支付状态,即使有什么异常,也能及时感知到。

主动查询,主要就是两种形式:

定时任务查询

毫无疑问,最简单的肯定就是定时任务了,支付服务,定时查询一段时间内支付中的支付订单,向第三方渠道查询支付结果,查询到终态之后,就去更新支付订单状态、通知订单服务:

定时查询支付状态

实现也很简单,用xxl-job之类的定时任务框架,定时扫表,向第三方查询就行了,大概代码如下:

    @XxlJob("syncPaymentResult")
    public ReturnT<String> syncPaymentResult(int hour) {
        //……
        //查询一段之间支付中的流水
        List<PayDO> pendingList = payMapper.getPending(now.minusHours(hour));
        for (PayDO payDO : pendingList) {
            //……
            // 主动去第三方查
            PaymentStatusResult paymentStatusResult = paymentService.getPaymentStatus(paymentId);
            // 第三方支付中
            if (PaymentStatusEnum.PENDING.equals(paymentStatusResult.getPayStatus())) {
                continue;
            }
            //支付完成,获取到终态
            //……
            // 1.更新流水
            payMapper.updatePayDO(payDO);
            // 2.通知订单服务
            orderService.notifyOrder(notifyLocalRequestVO);
        }
        return ReturnT.SUCCESS;
    }

定时任务的最大好处肯定是简单了,但是它也有一些问题:

1 . 查询的结果不实时

定时任务频率的设置永远是个不好确定的事情,间隔短对数据库压力大,间隔长了不实时,很容易出现,上面提到的用户回到APP,结果轮询不到支付成功状态的情况。

实际上,用户跳转钱包之后,通常会很快完成支付,如果短时间内没有完成支付,那么一般也不会再付了。所以其实,发起支付开始,从第三方查询支付结果的频率应该是递减的。

2 . 对数据库有压力

定时任务扫表,对数据库肯定是会有压力的,扫表的时候,经常会看到数据库的监控出现一个小突刺,如果数据量大的话,可能影响更大。

可以单独创建一个支付中流水表,定时任务扫描这张表,获取到支付最终态之后,就删除掉对应的记录。

延时消息查询

定时任务存在一些问题,那么有没有什么其它办法呢?答案是延时消息。

延时消息查询支付状态

  • 在发起支付之后,发送一个延时消息,前面讲到,用户跳转到钱包,通常很快会支付,所以我们希望查询支付状态这个步骤,符合这个规律,所以希望在10s、30s、1min、1min30s、2min、5min、7min……这种频率去查询支付订单的状态,这里我们可以用一个队列结构实现,队列里存放下一次查询的时间间隔。

    大概代码如下:

           //……
          //控制查询频率的队列,时间单位为s
          Deque<Integer> queue = new LinkedList<>();
          queue.offer(10);
          queue.offer(30);
          queue.offer(60);
          //……
          //支付订单号
          PaymentConsultDTO paymentConsultDTO = new PaymentConsultDTO();
          paymentConsultDTO.setPaymentId(paymentId);
          paymentConsultDTO.setIntervalQueue(queue);
          //发送延时消息
          Message message = new Message();
          message.setTopic("PAYMENT");
          message.setKey(paymentId);
          message.setTag("CONSULT");
          message.setBody(toJSONString(paymentConsultDTO).getBytes(StandardCharsets.UTF_8));
          try {
              //第一个延时消息,延时10s
              long delayTime = System.currentTimeMillis() + 10 * 1000;
              // 设置消息需要被投递的时间。
              message.setStartDeliverTime(delayTime);
              SendResult sendResult = producer.send(message);
              //……
          } catch (Throwable th) {
              log.error("[sendMessage] error:", th);
          }

    PS:这里用的是RocketMQ云服务器版,支持任意级别的延时消息,开源版的RocketMQ只支持固定级别的延时消息,不得不感慨充钱才能变强。有实力的开发团队,可以在开源基础上,进行二次开发。

  • 在消费到延时消息之后,向第三方查询支付订单的状态,如果还在支付中,就继续发送下一个延时消息,延时间隔从队列结构中取。如果获取到最终态,就去更新支付订单状态、通知订单服务。

    @Component
    @Slf4j
    public class ConsultListener implements MessageListener {
      //消费者注册,监听器注册
      //……
    
      @Override
      public Action consume(Message message, ConsumeContext context) {
          // UTF-8解析
          String body = new String(message.getBody(), StandardCharsets.UTF_8);
          PaymentConsultDTO paymentConsultDTO= JsonUtil.parseObject(body, new TypeReference<PaymentConsultDTO>() {
          });
          if (paymentConsultDTO == null) {
              return Action.ReconsumeLater;
          }
          //获取支付流水
          PayDO payDO=payMapper.selectById(paymentConsultDTO.getPaymentId());
          //……
          //查询支付状态
          PaymentStatusResult paymentStatusResult=payService.getPaymentStatus(paymentStatusContext);
          //还在支付中,继续投递一个延时消息
          if (PaymentStatusEnum.PENDING.equals(paymentStatusResult.getPayStatus())){
              //发送延时消息
              Message msg = new Message();
              message.setTopic("PAYMENT");
              message.setKey(paymentConsultDTO.getPaymentId());
              message.setTag("CONSULT");
             //下一个延时消息的频率
              Long delaySeconds=paymentConsultDTO.getIntervalQueue().poll();        message.setBody(toJSONString(paymentConsultDTO).getBytes(StandardCharsets.UTF_8));
              try {
                  Long delayTime = System.currentTimeMillis() + delaySeconds * 1000;
                  // 设置消息需要被投递的时间。
                  message.setStartDeliverTime(delayTime);
                  SendResult sendResult = producer.send(message);
                  //……
              } catch (Throwable th) {
                  log.error("[sendMessage] error:", th);
              }
              return Action.CommitMessage;
          }
          //获取到最终态
          //更新支付订单状态
          //…… 
          //通知订单服务
          //……
          return Action.CommitMessage;
      }
    }

    延时消息的方案相对于定时轮询方案来讲:

    不过大家也看到,我这里的实现是利用的是充钱版的RocketMQ,所以看起来不太复杂,但是如果用开源方案,那就没那么简单。

    充钱就能解决

  • 时效性更好

  • 无需扫表,对数据库压力较小

结语

这篇文章介绍了一个让用户炸毛,让客服恼火,让开发挠头的问题——掉单,包括为什么会掉单,怎么防止掉单。

其中内部掉单,发生的概率相对较少,掉单最主要的原因还是所谓的外部掉单。

外部掉单解决的关键点是主动查询,有两种常用的方案:定时任务查询延时消息查询,前者简单一些,后者功能上更加出色。


参考:

  • [1]. 支付掉单异常最全解决方案
  • [2]. 解决支付掉单问题

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/090VqpAxmxn3jHaXYKeztA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237236次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8071次阅读
 目录