07、SpringBoot+微信支付 -->处理超时订单(定时查询、核实微信支付平台的订单、调用微信支付平台查单接口、更新本地订单状态、记录支付日志)

目录

Native 支付

处理超时订单

处理超时订单(定时查询、核实微信支付平台的订单、调用微信支付平台查单接口、更新本地订单状态、记录支付日志)

定时的讲解

【* * * * * *  】 每一秒执行一次
【0/3 * * * * *  】 从第0秒开始,每隔3秒执行一次
【1-3 * * * * *  】 从第1秒开始执行,到第3秒结束执行
【1,2,3 * * * * *  】 在指定的第1,2,3秒执行


需求分析

每隔30秒执行一次定时查询方法,先在【本地数据库】查询创建超过5分钟,并且未支付的订单。

然后再根据商品订单号调用【微信支付端】的【查单接口】进行查询,核实订单状态

如果订单在微信支付端那边已支付,则更新商户端(就是本地数据库)订单状态为已支付(本地数据库修改商户端的订单状态)

如果订单在微信支付端那边未支付,则调用微信支付平台的关单接口关闭订单,并更新商户端订单状态(本地数据库修改商户端的订单状态)

作用:把那些超时未支付的订单给关了

注意:调用微信支付端的【商户订单号查询订单】的接口,查询出来的数据是明文的,不要老是想着查出来的都是密文。

而且该接口查询出来的数据,和微信支付平台自动发给商户端的支付通知里面携带的通知参数的 resource 属性里面的 ciphertext 这个密文数据解密出来后的数据是一样的。

代码

定时任务:WxPayTask

这里的分钟是1,只是为了方便测试

定时查询的方法:
核实订单状态等操作 :WxPayServiceImpl

根据订单号查询微信支付查单接口,核实订单状态

queryOrder 查单接口方法,查出来的数据是明文的。

查单接口方法:queryOrder

这个是调用微信支付端的【商户订单号查询订单】的接口,查询出来的数据是明文的,不要老是想着查出来的都是密文。

商户订单号查询订单


更新本地订单状态:updateStatusByOrderNo
记录支付日志:createPaymentInfo

因为queryOrder 查单接口方法,查出来的数据是明文的。跟支付通知的 resource 里面的 **ciphertext(密文)**进行解密后的数据是一样的,所以也可以作为参数传给这个方法。

支付通知

关闭订单接口:closeOrder

关闭订单



测试:

创建测试环境:

先创建一个测试环境:

Java课程:点击了【确认支付】,弹出了支付二维码后就直接关掉了,没有进行支付,所以只添加了一条未支付的订单。

大数据课程:扫码支付了,但是我把ngrok内网穿透关掉了,那么隧道就失效了,微信支付平台发送的支付通知,商户端这边也就接收不到了,所以虽然微信支付平台那边,这个订单已经支付了,但商户端本地这边,因没有接收到支付通知,所以这个订单也是未支付的状态。

内网穿透地址注释掉用于演示商户端接收不到微信支付平台发来的支付通知,从而无法修改订单支付状态的情况。


演示环境创建好了,现在启动处理超时订单的方法。

查出创建订单超过1分钟且未支付的订单,然后到微信支付平台调用查询订单的接口,核实这个超时的订单是否真的没支付。

如果在微信支付平台那边已经支付了,那么获取该接口返回的结果里面的支付状态,修改到本地数据库的订单状态里面。

如果没有支付,直接调用微信支付端那边的关闭订单的接口,然后修改本地数据库的那条订单的支付状态为超时未支付。

期望结果:

期望结果应该是:

**Java课程:**是超时1分钟且没有支付的,所以调用定时任务后,本地数据库的该订单的支付状态应该是【超时已关闭】

**大数据库课程:**是超时1分钟,但是已经支付了,只是没收到支付通知,所以调用定时任务后,本地数据库的该订单的支付状态应该【支付成功】,且为该订单生成一条【支付日志记录】

实际结果:成功

实际上:跟预想的一样,成功。

一开始查询未支付且超时的订单:

Java课程的:

大数据课程的:


成功。

完整代码:

WxPayTask
java 复制代码
@Slf4j
@Component //组件,项目启动的时候就会加载这个组件类
public class WxPayTask
{
    @Resource
    private OrderInfoService orderInfoService;
    @Resource
    private WxPayService wxPayService;

    //从0秒开始,每隔30秒执行一次这个方法,查询创建超过5分钟,并且未支付的订单
    @Scheduled(cron = "0/30 * * * * *")
    public void orderConfirm() throws Exception
    {
        //从0秒开始,每隔30秒执行一次这个方法,查询创建超过5分钟,并且未支付的订单
        List<OrderInfo> orderInfoList =  orderInfoService.getNoPayOrderByDuration(1);

        for (OrderInfo orderInfo : orderInfoList)
        {
            String orderNo = orderInfo.getOrderNo();
            log.warn("超时订单 ===> {}" , orderNo);

            //核实订单状态:调用微信支付查单接口
            wxPayService.checkOrderStatus(orderNo);
        }
    }
}
getNoPayOrderByDuration 方法
java 复制代码
    /**
     * 从0秒开始,每隔30秒执行一次这个方法,查询创建超过5分钟,并且未支付的订单
     * @param minutes 5 分钟
     * @return 未支付的订单集合
     */
    @Override
    public List<OrderInfo> getNoPayOrderByDuration(int minutes)
    {
        // 使用Java的 Instant 类和 Duration 类来计算一个时间点。
        // 获取当前时间,并减去指定的分钟数。
        Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));

        //QueryWrapper 是 MyBatis-Plus 提供的一个用于构建查询条件的工具类
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        //构建查询条件
        //条件1:订单的状态为未支付
        queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
        //条件2:该订单创建时间超过5分钟--le 是"less than or equal to"的缩写:小于或等于
        queryWrapper.le("create_time",instant);

        List<OrderInfo> orderInfoList = orderInfoMapper.selectList(queryWrapper);

        return orderInfoList;
    }
}
checkOrderStatus 方法
java 复制代码
    /**
     * 根据订单号查询微信支付查单接口,核实订单状态
     * 如果订单已支付,则更新商户端订单状态
     * 如果订单未支付,则调用微信支付平台的关单接口关闭订单,并更新商户端订单状态
     * @param orderNo 订单id
     */
    @Override
    public void checkOrderStatus(String orderNo) throws Exception
    {
        log.warn("根据订单号核实订单状态 ===> {}", orderNo);

        //调用微信支付的查单接口---这个方法查出来的是明文
        String result = this.queryOrder(orderNo);

        System.err.println("订单号:"+orderNo+",调用微信支付查单接口:" + result);

        Gson gson = new Gson();
        //将结果转成map类型
        Map resultMap = gson.fromJson(result, HashMap.class);

        //获取微信支付端的订单的状态
        Object tradeState = resultMap.get("trade_state");

        //判断订单状态
        if (WxTradeState.SUCCESS.getType().equals(tradeState))
        {
            log.warn("核定该订单已经支付 ===> {}", orderNo);

            //如果确认该订单已支付,则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);

            //记录支付日志
            paymentInfoService.createPaymentInfo(result);
        }

        if (WxTradeState.NOTPAY.getType().equals(tradeState))
        {
            log.warn("核实订单未支付 ===> {}", orderNo);

            //如果订单未支付,则调用关单接口
            this.closeOrder(orderNo);

            //更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);

        }
    }
queryOrder 方法
java 复制代码
    /**
     * 根据商品订单号查询订单
     * @param orderNo 商品订单号
     * @return 订单
     *
     * 注意:这个查单接口查出来的数据是明文不是密文,不要想成是密文
     * 而且查出来的数据 跟支付通知里面的通知参数的密文ciphertext解密出来的数据是一样的
     *
     */
    @Override
    public String queryOrder(String orderNo) throws Exception
    {
        log.info("查单接口调用 ====> {}", orderNo);

        //组装url---主机地址 + 访问接口地址
        String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
        //还要拼接上商户号
        url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());

        //创建远程请求对象
        HttpGet httpGet = new HttpGet(url);

        //get请求只需要设置请求头就可以了--作用:希望接收json类型的响应
        httpGet.setHeader("Accept","application/json");

        // 完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpGet);


        try
        {
            //字符串形式的响应体
            String bodyAsString = EntityUtils.toString(response.getEntity());

            //响应状态码
            int statusCode = response.getStatusLine().getStatusCode();

            if (statusCode == 200)
            { //处理成功
                System.out.println("成功, 返回结果  = " + bodyAsString);
            } else if (statusCode == 204)
            { //处理成功,无返回Body
                System.out.println("成功");
            } else
            {
                System.out.println("下单失败, 响应码 = " + statusCode + ", 返回结果 = " + bodyAsString);
                throw new IOException("请求失败 request failed");
            }

            return bodyAsString;
        } finally
        {
            response.close();
        }
    }
updateStatusByOrderNo 方法
java 复制代码
/**
 * 根据订单号更新订单状态
 * @param orderNo 商品订单号
 * @param orderStatus 订单状态
 */
@Override
public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus)
{
    log.info("修改订单状态为: ===> {}" , orderStatus);

    //QueryWrapper 是 MyBatis-Plus 提供的一个用于构建查询条件的工具类
    QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
    //查询条件---相当于 update t_order_info set xxx = xxx where order_no = orderNo
    queryWrapper.eq("order_no",orderNo);

    //要修改的字段,存到这个 orderInfo 对象里面
    OrderInfo orderInfo = new OrderInfo();
    orderInfo.setOrderStatus(orderStatus.getType());

    //调用sql
    orderInfoMapper.update(orderInfo,queryWrapper);

}
PaymentInfoServiceImpl 方法
java 复制代码
@Resource
private PaymentInfoMapper paymentInfoMapper;

/**
 * 记录支付日志
 * @param plainText 解密后的参数明文
 */
@Override
public void createPaymentInfo(String plainText)
{
    log.info("记录支付日志");

    Gson gson = new Gson();
    // 将字符串的plainText转成 hashMap 对象
    Map plainTextMap = gson.fromJson(plainText, HashMap.class);

    // 创建一个记录支付日志的对象
    PaymentInfo paymentInfo = new PaymentInfo();

    // 从 plainTextMap 对象中获取要存到记录支付日志(PaymentInfo)的 属性字段
    // 商品订单编号
    String orderNo = (String) plainTextMap.get("out_trade_no");
    // 支付系统交易编号
    String transactionId = (String)plainTextMap.get("transaction_id");
    // 交易类型-- NATIVE:扫码支付
    String tradeType = (String)plainTextMap.get("trade_type");
    // 交易状态--SUCCESS:支付成功
    String tradeState = (String)plainTextMap.get("trade_state");
    // 用户支付金额,单位为分    amount.payer_total
    Map<String, Object> amount = (Map)plainTextMap.get("amount");
    // 官网指定返回的金额是int类型,但是直接把object转成int会报错
    // 弄个中转站:隐式类型转换(小转大)将int类型转换成Double,
    // 然后再用intValue 把double类型的值转成Integer整形
    int payerTotal = ((Double) amount.get("payer_total")).intValue();

    paymentInfo.setOrderNo(orderNo); //商品订单编号
    paymentInfo.setTransactionId(transactionId);//支付系统交易编号
    paymentInfo.setTradeType(tradeType);//交易类型
    paymentInfo.setTradeState(tradeState);//交易状态
    paymentInfo.setPayerTotal(payerTotal);//用户支付金额,单位为分
    paymentInfo.setPaymentType(PayType.WXPAY.getType()); //支付类型
    // 通知参数 -- 因为可能会有各种各样的参数,所以直接把整个参数存到一个字段里面,
    // 方便后续遇到问题可以查看
    paymentInfo.setContent(plainText);

    //插入一个订单支付日志记录
    paymentInfoMapper.insert(paymentInfo);
}
closeOrder 方法
java 复制代码
/**
 * 关单接口
 * @param orderNo 订单编号
 */
private void closeOrder(String orderNo) throws IOException
{
    log.info("关单接口的调用,订单编号 ===> {}", orderNo);

    // 构建url地址:
    // 关单接口的url地址--/v3/pay/transactions/out-trade-no/{out_trade_no}/close,
    // {out_trade_no}这个是个占位符,用format方法,把orderNo传进去
    String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);

    //微信端的主机地址:  【主域名】https://api.mch.weixin.qq.com
    url = wxPayConfig.getDomain().concat(url);

    // 创建远程请求对象
    HttpPost httpPost = new HttpPost(url);

    // 组装json请求体
    Gson gson = new Gson();
    HashMap<String, String> paramsMap = new HashMap<>();
    paramsMap.put("mchid", wxPayConfig.getMchId()); // 直连商户号
    String jsonParams = gson.toJson(paramsMap);
    log.info("请求参数 ===> {}", jsonParams);

    // 将请求参数设置到请求对象中
    StringEntity entity = new StringEntity(jsonParams, "utf-8");

    // 要发送的数据类型
    entity.setContentType("application/json");
    httpPost.setEntity(entity);
    // 希望接收的数据类型
    httpPost.setHeader("Accept", "application/json");

    // 完成签名并执行请求
    CloseableHttpResponse response = wxPayClient.execute(httpPost);
    try
    {
        // 响应状态码
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200)
        { // 处理成功
            log.info("成功");
        } else if (statusCode == 204)
        { // 处理成功,无返回Body
            log.info("成功");
        } else
        {
            log.info("关闭订单API失败, 响应码 = " + statusCode + "");
            throw new IOException("请求失败 request failed");
        }
    } finally
    {
        response.close();
    }
}
相关推荐
天天扭码7 分钟前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶7 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺12 分钟前
Spring Boot框架Starter组件整理
java·spring boot·后端
小曲程序19 分钟前
vue3 封装request请求
java·前端·typescript·vue
陈王卜37 分钟前
django+boostrap实现发布博客权限控制
java·前端·django
小码的头发丝、37 分钟前
Spring Boot 注解
java·spring boot
午觉千万别睡过40 分钟前
RuoYI分页不准确问题解决
spring boot
java亮小白199742 分钟前
Spring循环依赖如何解决的?
java·后端·spring
飞滕人生TYF1 小时前
java Queue 详解
java·队列
2301_811274311 小时前
大数据基于Spring Boot的化妆品推荐系统的设计与实现
大数据·spring boot·后端