Procházet zdrojové kódy

mall + pay:
1. 支付订单的状态同步 Job
2. 支付订单的过期关闭 Job

YunaiV před 1 rokem
rodič
revize
15cca8f3de

+ 10 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java

@@ -33,6 +33,16 @@ public enum PayOrderStatusRespEnum {
         return Objects.equals(status, SUCCESS.getStatus());
     }
 
+    /**
+     * 判断是否已退款
+     *
+     * @param status 状态
+     * @return 是否支付成功
+     */
+    public static boolean isRefund(Integer status) {
+        return Objects.equals(status, REFUND.getStatus());
+    }
+
     /**
      * 判断是否支付关闭
      *

+ 7 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderExtensionMapper.java

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Mapper
@@ -23,4 +24,10 @@ public interface PayOrderExtensionMapper extends BaseMapperX<PayOrderExtensionDO
         return selectList(PayOrderExtensionDO::getOrderId, orderId);
     }
 
+    default List<PayOrderExtensionDO> selectListByStatusAndCreateTimeGe(Integer status, LocalDateTime minCreateTime) {
+        return selectList(new LambdaQueryWrapper<PayOrderExtensionDO>()
+                .eq(PayOrderExtensionDO::getStatus, status)
+                .ge(PayOrderExtensionDO::getCreateTime, minCreateTime));
+    }
+
 }

+ 7 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderMapper.java

@@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Mapper
@@ -52,4 +53,10 @@ public interface PayOrderMapper extends BaseMapperX<PayOrderDO> {
                 .eq(PayOrderDO::getId, id).eq(PayOrderDO::getStatus, status));
     }
 
+    default List<PayOrderDO> selectListByStatusAndExpireTimeLt(Integer status, LocalDateTime expireTime) {
+        return selectList(new LambdaQueryWrapper<PayOrderDO>()
+                .eq(PayOrderDO::getStatus, status)
+                .lt(PayOrderDO::getExpireTime, expireTime));
+    }
+
 }

+ 31 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/order/PayOrderExpireJob.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.pay.job.order;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
+import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 支付订单的过期 Job
+ *
+ * 支付超过过期时间时,支付渠道是不会通知进行过期,所以需要定时进行过期关闭。
+ *
+ * @author 芋道源码
+ */
+@Component
+@TenantJob
+public class PayOrderExpireJob implements JobHandler {
+
+    @Resource
+    private PayOrderService orderService;
+
+    @Override
+    public String execute(String param) {
+        int count = orderService.expireOrder();
+        return StrUtil.format("支付过期 {} 个", count);
+    }
+
+}

+ 43 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/order/PayOrderSyncJob.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.pay.job.order;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
+import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+/**
+ * 支付订单的同步 Job
+ *
+ * 由于支付订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。
+ *
+ * @author 芋道源码
+ */
+@Component
+@TenantJob
+public class PayOrderSyncJob implements JobHandler {
+
+    /**
+     * 同步创建时间在 N 分钟之前的订单
+     *
+     * 为什么同步 10 分钟之前的订单?
+     *  因为一个订单发起支付,到支付成功,大多数在 10 分钟内,需要保证轮询到。
+     *  如果设置为 30、60 或者更大时间范围,会导致轮询的订单太多,影响性能。当然,你也可以根据自己的业务情况来处理。
+     */
+    private static final Duration CREATE_TIME_DURATION_BEFORE = Duration.ofMinutes(10);
+
+    @Resource
+    private PayOrderService orderService;
+
+    @Override
+    public String execute(String param) {
+        LocalDateTime minCreateTime = LocalDateTime.now().minus(CREATE_TIME_DURATION_BEFORE);
+        int count = orderService.syncOrder(minCreateTime);
+        return StrUtil.format("同步订单 {} 个", count);
+    }
+
+}

+ 16 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderService.java

@@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
 
 import javax.validation.Valid;
 import javax.validation.constraints.NotEmpty;
+import java.time.LocalDateTime;
 import java.util.List;
 
 /**
@@ -105,4 +106,19 @@ public interface PayOrderService {
      */
     PayOrderExtensionDO getOrderExtension(Long id);
 
+    /**
+     * 同步订单的支付状态
+     *
+     * @param minCreateTime 最小创建时间
+     * @return 同步到已支付的订单数量
+     */
+    int syncOrder(LocalDateTime minCreateTime);
+
+    /**
+     * 将已过期的订单,状态修改为已关闭
+     *
+     * @return 过期的订单数量
+     */
+    int expireOrder();
+
 }

+ 133 - 7
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.pay.service.order;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.spring.SpringUtil;
@@ -39,6 +40,8 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
+import java.time.Duration;
+import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Objects;
 
@@ -152,6 +155,7 @@ public class PayOrderServiceImpl implements PayOrderService {
                 .setReturnUrl(reqVO.getReturnUrl())
                 // 订单相关字段
                 .setPrice(order.getPrice()).setExpireTime(order.getExpireTime());
+        unifiedOrderReqDTO.setExpireTime(LocalDateTimeUtils.addTime(Duration.ofSeconds(70))); // TODO 芋艿:稍后删除掉
         PayOrderRespDTO unifiedOrderResp = client.unifiedOrder(unifiedOrderReqDTO);
 
         // 4. 如果调用直接支付成功,则直接更新支付单状态为成功。例如说:付款码支付,免密支付时,就直接验证支付成功
@@ -297,7 +301,7 @@ public class PayOrderServiceImpl implements PayOrderService {
             throw exception(ORDER_EXTENSION_NOT_FOUND);
         }
         if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功,直接返回,不用重复更新
-            log.info("[updateOrderExtensionSuccess][支付拓展单({}) 已经是已支付,无需更新]", orderExtension.getId());
+            log.info("[updateOrderExtensionSuccess][orderExtension({}) 已经是已支付,无需更新]", orderExtension.getId());
             return orderExtension;
         }
         if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付
@@ -310,7 +314,7 @@ public class PayOrderServiceImpl implements PayOrderService {
         if (updateCounts == 0) { // 校验状态,必须是待支付
             throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
         }
-        log.info("[updateOrderExtensionSuccess][支付拓展单({}) 更新为已支付]", orderExtension.getId());
+        log.info("[updateOrderExtensionSuccess][orderExtension({}) 更新为已支付]", orderExtension.getId());
         return orderExtension;
     }
 
@@ -331,7 +335,7 @@ public class PayOrderServiceImpl implements PayOrderService {
         }
         if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功,直接返回,不用重复更新
                 && Objects.equals(order.getExtensionId(), orderExtension.getId())) {
-            log.info("[updateOrderExtensionSuccess][支付订单({}) 已经是已支付,无需更新]", order.getId());
+            log.info("[updateOrderExtensionSuccess][order({}) 已经是已支付,无需更新]", order.getId());
             return true;
         }
         if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
@@ -349,7 +353,7 @@ public class PayOrderServiceImpl implements PayOrderService {
         if (updateCounts == 0) { // 校验状态,必须是待支付
             throw exception(ORDER_STATUS_IS_NOT_WAITING);
         }
-        log.info("[updateOrderExtensionSuccess][支付订单({}) 更新为已支付]", order.getId());
+        log.info("[updateOrderExtensionSuccess][order({}) 更新为已支付]", order.getId());
         return false;
     }
 
@@ -364,12 +368,12 @@ public class PayOrderServiceImpl implements PayOrderService {
             throw exception(ORDER_EXTENSION_NOT_FOUND);
         }
         if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { // 如果已经是关闭,直接返回,不用重复更新
-            log.info("[updateOrderExtensionClosed][支付拓展单({}) 已经是支付关闭,无需更新]", orderExtension.getId());
+            log.info("[updateOrderExtensionClosed][orderExtension({}) 已经是支付关闭,无需更新]", orderExtension.getId());
             return;
         }
         // 一般出现先是支付成功,然后支付关闭,都是全部退款导致关闭的场景。这个情况,我们不更新支付拓展单,只通过退款流程,更新支付单
         if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
-            log.info("[updateOrderExtensionClosed][支付拓展单({}) 是已支付,无需更新为支付关闭]", orderExtension.getId());
+            log.info("[updateOrderExtensionClosed][orderExtension({}) 是已支付,无需更新为支付关闭]", orderExtension.getId());
             return;
         }
         if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付
@@ -383,7 +387,7 @@ public class PayOrderServiceImpl implements PayOrderService {
         if (updateCounts == 0) { // 校验状态,必须是待支付
             throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
         }
-        log.info("[updateOrderExtensionClosed][支付拓展单({}) 更新为支付关闭]", orderExtension.getId());
+        log.info("[updateOrderExtensionClosed][orderExtension({}) 更新为支付关闭]", orderExtension.getId());
     }
 
     @Override
@@ -414,6 +418,128 @@ public class PayOrderServiceImpl implements PayOrderService {
         return orderExtensionMapper.selectById(id);
     }
 
+    @Override
+    public int syncOrder(LocalDateTime minCreateTime) {
+        // 1. 查询指定创建时间内的待支付订单
+        List<PayOrderExtensionDO> orderExtensions = orderExtensionMapper.selectListByStatusAndCreateTimeGe(
+                PayOrderStatusEnum.WAITING.getStatus(), minCreateTime);
+        if (CollUtil.isEmpty(orderExtensions)) {
+            return 0;
+        }
+        // 2. 遍历执行
+        int count = 0;
+        for (PayOrderExtensionDO orderExtension : orderExtensions) {
+            count += syncOrder(orderExtension) ? 1 : 0;
+        }
+        return count;
+    }
+
+    /**
+     * 同步单个支付拓展单
+     *
+     * @param orderExtension 支付拓展单
+     * @return 是否已支付
+     */
+    private boolean syncOrder(PayOrderExtensionDO orderExtension) {
+        try {
+            // 1.1 查询支付订单信息
+            PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId());
+            if (payClient == null) {
+                log.error("[syncOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
+                return false;
+            }
+            PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo());
+            // 1.2 回调支付结果
+            notifyOrder(orderExtension.getChannelId(), respDTO);
+
+            // 2. 如果是已支付,则返回 1
+            return PayOrderStatusRespEnum.isSuccess(respDTO.getStatus());
+        } catch (Throwable e) {
+            log.error("[syncOrder][orderExtension({}) 同步支付状态异常]", orderExtension.getId(), e);
+            return false;
+        }
+    }
+
+    @Override
+    public int expireOrder() {
+        // 1. 查询过期的待支付订单
+        List<PayOrderDO> orders = orderMapper.selectListByStatusAndExpireTimeLt(
+                PayOrderStatusEnum.WAITING.getStatus(), LocalDateTime.now());
+        if (CollUtil.isEmpty(orders)) {
+            return 0;
+        }
+
+        // 2. 遍历执行
+        int count = 0;
+        for (PayOrderDO order : orders) {
+            count += expireOrder(order) ? 1 : 0;
+        }
+        return count;
+    }
+
+    /**
+     * 同步单个支付单
+     *
+     * @param order 支付单
+     * @return 是否已过期
+     */
+    private boolean expireOrder(PayOrderDO order) {
+        try {
+            // 1. 需要先处理关联的支付拓展单,避免错误的过期已支付 or 已退款的订单
+            List<PayOrderExtensionDO> orderExtensions = orderExtensionMapper.selectListByOrderId(order.getId());
+            for (PayOrderExtensionDO orderExtension : orderExtensions) {
+                if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) {
+                    continue;
+                }
+                // 情况一:校验数据库中的 orderExtension 是不是已支付
+                if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
+                    log.error("[expireOrder][order({}) 的 extension({}) 已支付,可能是数据不一致]",
+                            order.getId(), orderExtension.getId());
+                    return false;
+                }
+                // 情况二:调用三方接口,查询支付单状态,是不是已支付/已退款
+                PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId());
+                if (payClient == null) {
+                    log.error("[expireOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
+                    return false;
+                }
+                PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo());
+                if (PayOrderStatusRespEnum.isRefund(respDTO.getStatus())) {
+                    // 补充说明:按道理,应该是 WAITING => SUCCESS => REFUND 状态,如果直接 WAITING => REFUND 状态,说明中间丢了过程
+                    // 此时,需要人工介入,手工补齐数据,保持 WAITING => SUCCESS => REFUND 的过程
+                    log.error("[expireOrder][extension({}) 的 PayOrderRespDTO({}) 已退款,可能是回调延迟]",
+                            orderExtension.getId(), toJsonString(respDTO));
+                    return false;
+                }
+                if (PayOrderStatusRespEnum.isSuccess(respDTO.getStatus())) {
+                    notifyOrder(orderExtension.getChannelId(), respDTO);
+                    return false;
+                }
+                // 兜底逻辑:将支付拓展单更新为已关闭
+                PayOrderExtensionDO updateObj = new PayOrderExtensionDO().setStatus(PayOrderStatusEnum.CLOSED.getStatus())
+                        .setChannelNotifyData(toJsonString(respDTO));
+                if (orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), PayOrderStatusEnum.WAITING.getStatus(),
+                        updateObj) == 0) {
+                    log.error("[expireOrder][extension({}) 更新为支付关闭失败]", orderExtension.getId());
+                    return false;
+                }
+                log.info("[expireOrder][extension({}) 更新为支付关闭成功]", orderExtension.getId());
+            }
+
+            // 2. 都没有上述情况,可以安心更新为已关闭
+            PayOrderDO updateObj = new PayOrderDO().setStatus(PayOrderStatusEnum.CLOSED.getStatus());
+            if (orderMapper.updateByIdAndStatus(order.getId(), order.getStatus(), updateObj) == 0) {
+                log.error("[expireOrder][order({}) 更新为支付关闭失败]", order.getId());
+                return false;
+            }
+            log.info("[expireOrder][order({}) 更新为支付关闭失败]", order.getId());
+            return true;
+        } catch (Throwable e) {
+            log.error("[expireOrder][order({}) 过期订单异常]", order.getId(), e);
+            return false;
+        }
+    }
+
     /**
      * 获得自身的代理对象,解决 AOP 生效问题
      *