What should I do if the payment is successful again after paying but it shows that the payment has not been made? What is the backend code logic? 【Hangzhou multi-tester_Wang sir】【Hangzhou multi-tester】...

What is a dropped order?
The so-called lost order means that the user places an order and pays, completes the payment in the wallet, and then returns to the e-commerce app to see that the order is still unpaid... There is no doubt that the user will explode, and the result is either a complaint or a negative review
.
The user feels that he has been cheated,
so how did the order drop come from? Let's take a look at the complete process of order payment:

The complete process of wallet payment
The user clicks the payment from the e-commerce application, and the client initiates a payment request to the server. The payment
service will initiate payment to the third-party payment channel, and the payment channel will respond to the corresponding url. Taking
APP as an example, the client usually will Pull up the corresponding wallet, and the user jumps to the corresponding wallet. The
user completes the payment in the wallet.
After the user completes the payment, the user jumps back to the corresponding e-commerce APP
client to poll the order service to obtain the order status.
The payment channel calls back the payment service and notifies the payment result
The payment service notifies the order service to update the order status.
For payment orders, it can be roughly divided into the following states:

Payment status
Unpaid: After the user clicks to pay, but before the payment service requests the payment channel, the user is in an unpaid status.
Payment: After the user initiates the payment, jumps to the payment wallet, and then completes the payment. In this state, it can be said to be a foggy state. The e-commerce system is not sure about the user's payment
success/failure/cancel/close: the e-commerce system finally determines the user's payment in the third-party wallet The final result
seems to be no problem. Why did you drop the order? Simply put, the status of the payment is not synchronized, or not synchronized in time.

Order drop occurred
1.
Some exceptions occurred in the payment callback of the payment channel, which caused the payment service to not receive the callback notification from the payment
channel . Obtaining the order status The client usually polls to obtain the status. It may not obtain the order status within the polling time. As a result, the user sees that the order is not paid. 1 can be called an external order, and 2 and 3 can be called an internal order. one. Next, let's see how to prevent the problem of dropping orders. How to prevent internal order loss Let’s start with the order loss within the system. Of course, within the system, stability is easier to ensure, and the probability of order loss is relatively small. The server side prevents dropped orders. The key to prevent dropped orders between the payment service and the order service is to ensure that the payment result of the payment notification order is successful as much as possible. We generally use these two methods.






Prevent order drop on the server side 1. Synchronous call retry mechanism
When the payment service calls the order service, it must retry on failure to prevent the call from failing due to network jitter. 2. Reliability of asynchronous message delivery
Synchronization is not secure, then add another asynchronous. The payment service delivers a payment success message, and the order service consumes the payment success message. The whole process must ensure reliability as much as possible. For example, the order service must complete the message consumption after completing the order status update.
The synchronous + asynchronous two-handed strategy can basically prevent internal orders from the server.
As for the introduction of distributed transactions (transaction messages, Seata) to ensure consistent state, I don't think it is necessary.

客户端如何防止掉单
用户支付完成后,跳回电商系统,客户端会轮询一下订单的状态,通常两三秒内,就会得到订单完成支付的结果,这个过程出现问题的概率相比是非常低的。
但是也不排除,很小概率下,客户端轮询一段时间,还没得到结果,那么只能结束轮询,给用户展示未支付。
这种情况,通常问题也是出在服务端,没有及时更新订单的状态,最主要的还是要处理服务端的掉单,保证服务端能及时同步支付订单的状态。
但是一旦服务端的订单状态变更了,也要尽可能同步到客户端,不能让用户一直看到未支付。
客户端和服务端之间,同步状态,无非就是推和拉:1.  客户端轮询
客户端判断用户未支付之后,通常会进行订单倒计时。

倒计时
这里再提一下?大家觉得这种倒计时是怎么实现的呢?纯客户端组组件倒计时吗?
——肯定不行,通常是客户端组件倒计时,定期向服务端请求,检查倒计时时间。同样的,这种情况下,客户端也可以检查支付状态。2.  服务端推送
说真的,服务端推送,看上去是一种很美好的方案,Web端可以使用Websocket,APP端可以用自定义Push,大家可以看看7种实现web实时消息推送的方案。但实际上,推送的成功率经常不那么理想。怎么防止外部掉单
相比较内部掉单,外部掉单发生的概率就大很多,毕竟和外部渠道的对接,不可控的因素更多。
要防止外部掉单,核心就是四个字:“主动查询”,如果只是等待第三方的回调通知,风险还是比较大的,支付服务要主动向第三方查询支付状态,即使有什么异常,也能及时感知到。
主动查询,主要就是两种形式:定时任务查询
毫无疑问,最简单的肯定就是定时任务了,支付服务,定时查询一段时间内支付中的支付订单,向第三方渠道查询支付结果,查询到终态之后,就去更新支付订单状态、通知订单服务:

定时查询支付状态
实现也很简单,用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,所以看起来不太复杂,但是如果用开源方案,那就没那么简单。
充钱就能解决
时效性更好
无需扫表,对数据库压力较小结语
这篇文章介绍了一个让用户炸毛,让客服恼火,让开发挠头的问题——掉单,包括为什么会掉单,怎么防止掉单。
其中内部掉单,发生的概率相对较少,掉单最主要的原因还是所谓的外部掉单。
外部掉单解决的关键点是主动查询,有两种常用的方案:定时任务查询和延时消息查询,前者简单一些,后者功能上更加出色。


原文链接转载于:https://blog.csdn.net/m0_73311735/article/details/126661708 

参考文章:https://blog.csdn.net/July_whj/article/details/126819380

Guess you like

Origin blog.csdn.net/weixin_39362573/article/details/128946243