瀏覽代碼

by gateway:
1. 完善部分 order 单元测试
2. 增加支付订单的 no 生成逻辑

zhijiantianya@gmail.com 1 年之前
父節點
當前提交
b54f7e9256
共有 12 個文件被更改,包括 397 次插入92 次删除
  1. 0 1
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java
  2. 8 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/RedisKeyConstants.java
  3. 31 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/no/PayNoRedisDAO.java
  4. 9 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayConfiguration.java
  5. 16 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java
  6. 4 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/package-info.java
  7. 19 38
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java
  8. 1 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java
  9. 303 47
      yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java
  10. 3 2
      yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java
  11. 1 0
      yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/clean.sql
  12. 2 2
      yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql

+ 0 - 1
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java

@@ -12,7 +12,6 @@ import org.springframework.context.annotation.Bean;
  * @author 芋道源码
  */
 @AutoConfiguration
-@EnableConfigurationProperties(PayProperties.class)
 public class YudaoPayAutoConfiguration {
 
     @Bean

+ 8 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/RedisKeyConstants.java

@@ -14,4 +14,12 @@ public interface RedisKeyConstants {
             "pay_notify:lock:", // 参数来自 DefaultLockKeyBuilder 类
             RedisKeyDefine.KeyTypeEnum.HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson 的 Lock 锁,使用 Hash 数据结构
 
+    /**
+     * 支付序号的缓存
+     *
+     * KEY 格式:pay_no:{prefix}
+     * VALUE 数据格式:编号自增
+     */
+    String PAY_NO = "pay_no";
+
 }

+ 31 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/no/PayNoRedisDAO.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.pay.dal.redis.no;
+
+import cn.hutool.core.date.DatePattern;import cn.hutool.core.date.DateUtil;import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Repository;
+
+import javax.annotation.Resource;import java.time.LocalDateTime;
+
+/**
+ * 支付序号的 Redis DAO
+ *
+ * @author 芋道源码
+ */
+@Repository
+public class PayNoRedisDAO {
+
+    @Resource
+    private StringRedisTemplate stringRedisTemplate;
+
+    /**
+     * 生成序号
+     *
+     * @param prefix 前缀
+     * @return 序号
+     */
+    public String generate(String prefix) {
+        String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN);
+        Long no = stringRedisTemplate.opsForValue().increment(noPrefix);
+        return noPrefix + no;
+    }
+
+}

+ 9 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayConfiguration.java

@@ -0,0 +1,9 @@
+package cn.iocoder.yudao.module.pay.framework.pay.config;
+
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties(PayProperties.class)
+public class PayConfiguration {
+}

+ 16 - 1
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java → yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.framework.pay.config;
+package cn.iocoder.yudao.module.pay.framework.pay.config;
 
 import lombok.Data;
 import org.hibernate.validator.constraints.URL;
@@ -12,6 +12,9 @@ import javax.validation.constraints.NotEmpty;
 @Data
 public class PayProperties {
 
+    private static final String ORDER_NO_PREFIX = "P";
+    private static final String REFUND_NO_PREFIX = "R";
+
     /**
      * 支付回调地址
      *
@@ -34,4 +37,16 @@ public class PayProperties {
     @URL(message = "支付回调地址的格式必须是 URL")
     private String refundNotifyUrl;
 
+    /**
+     * 支付订单 no 的前缀
+     */
+    @NotEmpty(message = "支付订单 no 的前缀不能为空")
+    private String orderNoPrefix = ORDER_NO_PREFIX;
+
+    /**
+     * 退款订单 no 的前缀
+     */
+    @NotEmpty(message = "退款订单 no 的前缀不能为空")
+    private String refundNoPrefix = REFUND_NO_PREFIX;
+
 }

+ 4 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 占位,无实际作用
+ */
+package cn.iocoder.yudao.module.pay.framework.pay.core;

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

@@ -1,14 +1,11 @@
 package cn.iocoder.yudao.module.pay.service.order;
 
-import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.lang.Pair;
 import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.core.util.RandomUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
-import cn.iocoder.yudao.framework.pay.config.PayProperties;
 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
@@ -27,8 +24,10 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderExtensionMapper;
 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
+import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO;
 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
+import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@@ -40,7 +39,6 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
-import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Objects;
 
@@ -68,6 +66,8 @@ public class PayOrderServiceImpl implements PayOrderService {
     private PayOrderMapper orderMapper;
     @Resource
     private PayOrderExtensionMapper orderExtensionMapper;
+    @Resource
+    private PayNoRedisDAO noRedisDAO;
 
     @Resource
     private PayAppService appService;
@@ -136,8 +136,9 @@ public class PayOrderServiceImpl implements PayOrderService {
         PayClient client = payClientFactory.getPayClient(channel.getId());
 
         // 2. 插入 PayOrderExtensionDO
+        String no = noRedisDAO.generate(payProperties.getOrderNoPrefix());
         PayOrderExtensionDO orderExtension = PayOrderConvert.INSTANCE.convert(reqVO, userIp)
-                .setOrderId(order.getId()).setNo(generateOrderExtensionNo())
+                .setOrderId(order.getId()).setNo(no)
                 .setChannelId(channel.getId()).setChannelCode(channel.getCode())
                 .setStatus(PayOrderStatusEnum.WAITING.getStatus());
         orderExtensionMapper.insert(orderExtension);
@@ -155,7 +156,7 @@ public class PayOrderServiceImpl implements PayOrderService {
 
         // 4. 如果调用直接支付成功,则直接更新支付单状态为成功。例如说:付款码支付,免密支付时,就直接验证支付成功
         if (unifiedOrderResp != null) {
-            getSelf().notifyPayOrder(channel, unifiedOrderResp);
+            getSelf().notifyOrder(channel, unifiedOrderResp);
             // 如有渠道错误码,则抛出业务异常,提示用户
             if (StrUtil.isNotEmpty(unifiedOrderResp.getChannelErrorCode())) {
                 throw exception(ORDER_SUBMIT_CHANNEL_ERROR, unifiedOrderResp.getChannelErrorCode(),
@@ -204,31 +205,12 @@ public class PayOrderServiceImpl implements PayOrderService {
         return payProperties.getOrderNotifyUrl() + "/" + channel.getId();
     }
 
-    private String generateOrderExtensionNo() {
-//    wx
-//    2014
-//    10
-//    27
-//    20
-//    09
-//    39
-//    5522657
-//    a690389285100
-        // 目前的算法
-        // 时间序列,年月日时分秒 14 位
-        // 纯随机,6 位 TODO 芋艿:此处估计是会有问题的,后续在调整
-        return DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss") + // 时间序列
-                RandomUtil.randomInt(100000, 999999) // 随机。为什么是这个范围,因为偷懒
-                ;
-    }
-
     @Override
-    @Transactional(rollbackFor = Exception.class)
     public void notifyOrder(Long channelId, PayOrderRespDTO notify) {
         // 校验支付渠道是否有效
         PayChannelDO channel = channelService.validPayChannel(channelId);
         // 更新支付订单为已支付
-        TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyPayOrder(channel, notify));
+        TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyOrder(channel, notify));
     }
 
     /**
@@ -238,7 +220,7 @@ public class PayOrderServiceImpl implements PayOrderService {
      * @param notify 通知
      */
     @Transactional(rollbackFor = Exception.class) // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyPayOrder(channel, notify) 调用,否则事务不生效
-    public void notifyPayOrder(PayChannelDO channel, PayOrderRespDTO notify) {
+    public void notifyOrder(PayChannelDO channel, PayOrderRespDTO notify) {
         // 情况一:支付成功的回调
         if (PayOrderStatusRespEnum.isSuccess(notify.getStatus())) {
             notifyOrderSuccess(channel, notify);
@@ -254,16 +236,16 @@ public class PayOrderServiceImpl implements PayOrderService {
 
     private void notifyOrderSuccess(PayChannelDO channel, PayOrderRespDTO notify) {
         // 1. 更新 PayOrderExtensionDO 支付成功
-        PayOrderExtensionDO orderExtension = updateOrderExtensionSuccess(notify);
+        PayOrderExtensionDO orderExtension = updateOrderSuccess(notify);
         // 2. 更新 PayOrderDO 支付成功
-        Pair<Boolean, PayOrderDO> order = updateOrderExtensionSuccess(channel, orderExtension, notify);
-        if (order.getKey()) { // 如果之前已经成功回调,则直接返回,不用重复记录支付通知记录;例如说:支付平台重复回调
+        Boolean paid = updateOrderSuccess(channel, orderExtension, notify);
+        if (paid) { // 如果之前已经成功回调,则直接返回,不用重复记录支付通知记录;例如说:支付平台重复回调
             return;
         }
 
         // 3. 插入支付通知记录
         notifyService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
-                .type(PayNotifyTypeEnum.ORDER.getType()).dataId(order.getValue().getId()).build());
+                .type(PayNotifyTypeEnum.ORDER.getType()).dataId(orderExtension.getOrderId()).build());
     }
 
     /**
@@ -272,7 +254,7 @@ public class PayOrderServiceImpl implements PayOrderService {
      * @param notify 通知
      * @return PayOrderExtensionDO 对象
      */
-    private PayOrderExtensionDO updateOrderExtensionSuccess(PayOrderRespDTO notify) {
+    private PayOrderExtensionDO updateOrderSuccess(PayOrderRespDTO notify) {
         // 1. 查询 PayOrderExtensionDO
         PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo());
         if (orderExtension == null) {
@@ -302,11 +284,10 @@ public class PayOrderServiceImpl implements PayOrderService {
      * @param channel 支付渠道
      * @param orderExtension 支付拓展单
      * @param notify 通知回调
-     * @return key:是否之前已经成功回调
-     *         value:PayOrderDO 对象
+     * @return 是否之前已经成功回调
      */
-    private Pair<Boolean, PayOrderDO> updateOrderExtensionSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension,
-                                                                  PayOrderRespDTO notify) {
+    private Boolean updateOrderSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension,
+                                                         PayOrderRespDTO notify) {
         // 1. 判断 PayOrderDO 是否处于待支付
         PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId());
         if (order == null) {
@@ -315,7 +296,7 @@ public class PayOrderServiceImpl implements PayOrderService {
         if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功,直接返回,不用重复更新
                 && Objects.equals(order.getExtensionId(), orderExtension.getId())) {
             log.info("[updateOrderExtensionSuccess][支付订单({}) 已经是已支付,无需更新]", order.getId());
-            return Pair.of(true, order);
+            return true;
         }
         if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
             throw exception(ORDER_STATUS_IS_NOT_WAITING);
@@ -333,7 +314,7 @@ public class PayOrderServiceImpl implements PayOrderService {
             throw exception(ORDER_STATUS_IS_NOT_WAITING);
         }
         log.info("[updateOrderExtensionSuccess][支付订单({}) 更新为已支付]", order.getId());
-        return Pair.of(false, order);
+        return false;
     }
 
     private void notifyOrderClosed(PayChannelDO channel, PayOrderRespDTO notify) {

+ 1 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.pay.service.refund;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.util.RandomUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.pay.config.PayProperties;
 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
@@ -24,6 +23,7 @@ import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
+import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;

+ 303 - 47
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java

@@ -1,27 +1,37 @@
 package cn.iocoder.yudao.module.pay.service.order;
 
+import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.pay.config.PayProperties;
 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
-import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
-import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO;
+import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderExtensionMapper;
 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
+import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO;
+import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
+import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
+import cn.iocoder.yudao.module.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatcher;
+import org.mockito.MockedStatic;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
 
@@ -30,6 +40,7 @@ import java.time.Duration;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
@@ -37,19 +48,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
 import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
 
 /**
  * {@link PayOrderServiceImpl} 的单元测试类
  *
  * @author 芋艿
  */
-@Import({PayOrderServiceImpl.class})
-public class PayOrderServiceTest extends BaseDbUnitTest {
+@Import({PayOrderServiceImpl.class, PayNoRedisDAO.class})
+public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
 
     @Resource
     private PayOrderServiceImpl orderService;
@@ -70,6 +80,11 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
     @MockBean
     private PayNotifyService notifyService;
 
+    @BeforeEach
+    public void setUp() {
+        when(properties.getOrderNotifyUrl()).thenReturn("http://127.0.0.1");
+    }
+
     @Test
     public void testGetOrderPage() {
         // mock 数据
@@ -237,7 +252,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
     public void testSubmitOrder_channelNotFound() {
         // mock 数据(order)
         PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
-                .setAppId(1L));
+                .setAppId(1L).setExpireTime(addTime(Duration.ofDays(1))));
         orderMapper.insert(order);
         // 准备参数
         PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId())
@@ -257,54 +272,295 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
 
     @Test // 调用 unifiedOrder 接口,返回存在渠道错误
     public void testSubmitOrder_channelError() {
-        // mock 数据(order)
-        PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
-                .setAppId(1L));
-        orderMapper.insert(order);
+        PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class);
+        try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
+            springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class)))
+                    .thenReturn(payOrderServiceImpl);
+
+            // mock 数据(order)
+            PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                    .setAppId(1L).setExpireTime(addTime(Duration.ofDays(1))));
+            orderMapper.insert(order);
+            // 准备参数
+            PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId())
+                    .setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()));
+            String userIp = randomString();
+            // mock 方法(app)
+            PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
+            when(appService.validPayApp(eq(1L))).thenReturn(app);
+            // mock 方法(channel)
+            PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)
+                    .setCode(PayChannelEnum.ALIPAY_APP.getCode()));
+            when(channelService.validPayChannel(eq(1L), eq(PayChannelEnum.ALIPAY_APP.getCode())))
+                    .thenReturn(channel);
+            // mock 方法(client)
+            PayClient client = mock(PayClient.class);
+            when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
+            // mock 方法()
+            PayOrderRespDTO unifiedOrderResp = randomPojo(PayOrderRespDTO.class, o ->
+                    o.setChannelErrorCode("001").setChannelErrorMsg("模拟异常"));
+            when(client.unifiedOrder(argThat(payOrderUnifiedReqDTO -> {
+                assertNotNull(payOrderUnifiedReqDTO.getOutTradeNo());
+                assertThat(payOrderUnifiedReqDTO)
+                        .extracting("subject", "body", "notifyUrl", "returnUrl", "price", "expireTime")
+                        .containsExactly(order.getSubject(), order.getBody(), "http://127.0.0.1/10",
+                                reqVO.getReturnUrl(), order.getPrice(), order.getExpireTime());
+                return true;
+            }))).thenReturn(unifiedOrderResp);
+
+            // 调用,并断言异常
+            assertServiceException(() -> orderService.submitOrder(reqVO, userIp),
+                    ORDER_SUBMIT_CHANNEL_ERROR, "001", "模拟异常");
+            // 断言,数据记录(PayOrderExtensionDO)
+            PayOrderExtensionDO orderExtension = orderExtensionMapper.selectOne(null);
+            assertNotNull(orderExtension);
+            assertThat(orderExtension).extracting("no", "orderId").isNotNull();
+            assertThat(orderExtension)
+                    .extracting("channelId", "channelCode","userIp" ,"status", "channelExtras",
+                            "channelErrorCode", "channelErrorMsg", "channelNotifyData")
+                    .containsExactly(10L, PayChannelEnum.ALIPAY_APP.getCode(), userIp,
+                            PayOrderStatusEnum.WAITING.getStatus(), reqVO.getChannelExtras(),
+                            null, null, null);
+        }
+    }
+
+    @Test
+    public void testSubmitOrder_success() {
+        PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class);
+        try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
+            springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class)))
+                    .thenReturn(payOrderServiceImpl);
+
+            // mock 数据(order)
+            PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                    .setAppId(1L).setExpireTime(addTime(Duration.ofDays(1))));
+            orderMapper.insert(order);
+            // 准备参数
+            PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId())
+                    .setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()));
+            String userIp = randomString();
+            // mock 方法(app)
+            PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
+            when(appService.validPayApp(eq(1L))).thenReturn(app);
+            // mock 方法(channel)
+            PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)
+                    .setCode(PayChannelEnum.ALIPAY_APP.getCode()));
+            when(channelService.validPayChannel(eq(1L), eq(PayChannelEnum.ALIPAY_APP.getCode())))
+                    .thenReturn(channel);
+            // mock 方法(client)
+            PayClient client = mock(PayClient.class);
+            when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
+            // mock 方法()
+            PayOrderRespDTO unifiedOrderResp = randomPojo(PayOrderRespDTO.class, o -> o.setChannelErrorCode(null).setChannelErrorMsg(null)
+                    .setDisplayMode(PayOrderDisplayModeEnum.URL.getMode()).setDisplayContent("tudou"));
+            when(client.unifiedOrder(argThat(payOrderUnifiedReqDTO -> {
+                assertNotNull(payOrderUnifiedReqDTO.getOutTradeNo());
+                assertThat(payOrderUnifiedReqDTO)
+                        .extracting("subject", "body", "notifyUrl", "returnUrl", "price", "expireTime")
+                        .containsExactly(order.getSubject(), order.getBody(), "http://127.0.0.1/10",
+                                reqVO.getReturnUrl(), order.getPrice(), order.getExpireTime());
+                return true;
+            }))).thenReturn(unifiedOrderResp);
+
+            // 调用
+            PayOrderSubmitRespVO result = orderService.submitOrder(reqVO, userIp);
+            // 断言,数据记录(PayOrderExtensionDO)
+            PayOrderExtensionDO orderExtension = orderExtensionMapper.selectOne(null);
+            assertNotNull(orderExtension);
+            assertThat(orderExtension).extracting("no", "orderId").isNotNull();
+            assertThat(orderExtension)
+                    .extracting("channelId", "channelCode","userIp" ,"status", "channelExtras",
+                            "channelErrorCode", "channelErrorMsg", "channelNotifyData")
+                    .containsExactly(10L, PayChannelEnum.ALIPAY_APP.getCode(), userIp,
+                            PayOrderStatusEnum.WAITING.getStatus(), reqVO.getChannelExtras(),
+                            null, null, null);
+            // 断言,返回(PayOrderSubmitRespVO)
+            assertThat(result)
+                    .extracting("status", "displayMode", "displayContent")
+                    .containsExactly(PayOrderStatusEnum.WAITING.getStatus(), PayOrderDisplayModeEnum.URL.getMode(), "tudou");
+            // 断言,调用
+            verify(payOrderServiceImpl).notifyOrder(same(channel), same(unifiedOrderResp));
+        }
+    }
+
+    @Test
+    public void testNotifyOrder_channelId() {
+        PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class);
+        try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
+            springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class)))
+                    .thenReturn(payOrderServiceImpl);
+            // 准备参数
+            Long channelId = 10L;
+            PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class);
+            // mock 方法(channel)
+            PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+            when(channelService.validPayChannel(eq(10L))).thenReturn(channel);
+
+            // 调用
+            orderService.notifyOrder(channelId, notify);
+            // 断言
+            verify(payOrderServiceImpl).notifyOrder(same(channel), same(notify));
+        }
+    }
+
+    @Test
+    public void testNotifyOrderSuccess_orderExtension_notFound() {
         // 准备参数
-        PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId())
-                .setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()));
-        String userIp = randomString();
-        // mock 方法(app)
-        PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
-        when(appService.validPayApp(eq(1L))).thenReturn(app);
-        // mock 方法(channel)
-        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)
-                .setCode(PayChannelEnum.ALIPAY_APP.getCode()));
-        when(channelService.validPayChannel(eq(1L), eq(PayChannelEnum.ALIPAY_APP.getCode())))
-                .thenReturn(channel);
-        // mock 方法(client)
-        PayClient client = mock(PayClient.class);
-        when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
-        when(client.unifiedOrder(any(PayOrderUnifiedReqDTO.class))).thenThrow(new NullPointerException());
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+        PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus()));
+
+        // 调用,并断言异常
+        assertServiceException(() -> orderService.notifyOrder(channel, notify),
+                ORDER_EXTENSION_NOT_FOUND);
+    }
+
+    @Test
+    public void testNotifyOrderSuccess_orderExtension_closed() {
+        // mock 数据(PayOrderExtensionDO)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())
+                        .setNo("P110"));
+        orderExtensionMapper.insert(orderExtension);
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+        PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
+                        .setOutTradeNo("P110"));
 
         // 调用,并断言异常
-        assertThrows(NullPointerException.class, () -> orderService.submitOrder(reqVO, userIp));
-        // 断言,数据记录(PayOrderExtensionDO)
-        PayOrderExtensionDO orderExtension = orderExtensionMapper.selectOne(null);
-        assertNotNull(orderExtension);
-        assertThat(orderExtension).extracting("no", "orderId").isNotNull();
-        assertThat(orderExtension)
-                .extracting("channelId", "channelCode","userIp" ,"status", "channelExtras",
-                        "channelErrorCode", "channelErrorMsg", "channelNotifyData")
-                .containsExactly(10L, PayChannelEnum.ALIPAY_APP.getCode(), userIp,
-                        PayOrderStatusEnum.WAITING.getStatus(), reqVO.getChannelExtras(),
-                        null, null, null);
+        assertServiceException(() -> orderService.notifyOrder(channel, notify),
+                ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
     }
 
-    @Test // 【成功】支付结果为等待中
-    public void testSubmitOrder_waitingResult() {
+    @Test
+    public void testNotifyOrderSuccess_order_notFound() {
+        // mock 数据(PayOrderExtensionDO)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())
+                        .setNo("P110"));
+        orderExtensionMapper.insert(orderExtension);
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+        PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
+                        .setOutTradeNo("P110"));
 
+        // 调用,并断言异常
+        assertServiceException(() -> orderService.notifyOrder(channel, notify),
+                ORDER_NOT_FOUND);
+        // 断言 PayOrderExtensionDO :数据更新被回滚
+        assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null));
     }
 
-    @Test // 【成功】支付结果为已完成
-    public void testSubmitOrder_successResult() {
+    @Test
+    public void testNotifyOrderSuccess_order_closed() {
+        testNotifyOrderSuccess_order_closedOrRefund(PayOrderStatusEnum.CLOSED.getStatus());
+    }
 
+    @Test
+    public void testNotifyOrderSuccess_order_refund() {
+        testNotifyOrderSuccess_order_closedOrRefund(PayOrderStatusEnum.REFUND.getStatus());
     }
 
-    @Test // 【成功】支付结果为已关闭
-    public void testSubmitOrder_closedResult() {
+    private void testNotifyOrderSuccess_order_closedOrRefund(Integer status) {
+        // mock 数据(PayOrderDO)
+        PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(status));
+        orderMapper.insert(order);
+        // mock 数据(PayOrderExtensionDO)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())
+                        .setNo("P110")
+                        .setOrderId(order.getId()));
+        orderExtensionMapper.insert(orderExtension);
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+        PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
+                        .setOutTradeNo("P110"));
+
+        // 调用,并断言异常
+        assertServiceException(() -> orderService.notifyOrder(channel, notify),
+                ORDER_STATUS_IS_NOT_WAITING);
+        // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS
+        assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null));
+    }
 
+    @Test
+    public void testNotifyOrderSuccess_order_paid() {
+        // mock 数据(PayOrderDO)
+        PayOrderDO order = randomPojo(PayOrderDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()));
+        orderMapper.insert(order);
+        // mock 数据(PayOrderExtensionDO)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())
+                        .setNo("P110")
+                        .setOrderId(order.getId()));
+        orderExtensionMapper.insert(orderExtension);
+        // 重要:需要将 order 的 extensionId 更新下
+        order.setExtensionId(orderExtension.getId());
+        orderMapper.updateById(order);
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+        PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
+                        .setOutTradeNo("P110"));
+
+        // 调用,并断言异常
+        orderService.notifyOrder(channel, notify);
+        // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS
+        assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null));
+        // 断言 PayOrderDO :数据未更新,因为它是 SUCCESS
+        assertPojoEquals(order, orderMapper.selectOne(null));
+        // 断言,调用
+        verify(notifyService, never()).createPayNotifyTask(any(PayNotifyTaskCreateReqDTO.class));
+    }
+
+    @Test
+    public void testNotifyOrderSuccess_order_waiting() {
+        // mock 数据(PayOrderDO)
+        PayOrderDO order = randomPojo(PayOrderDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                        .setPrice(10));
+        orderMapper.insert(order);
+        // mock 数据(PayOrderExtensionDO)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                        .setNo("P110")
+                        .setOrderId(order.getId()));
+        orderExtensionMapper.insert(orderExtension);
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)
+                .setFeeRate(0.1D));
+        PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
+                        .setOutTradeNo("P110"));
+
+        // 调用,并断言异常
+        orderService.notifyOrder(channel, notify);
+        // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS
+        orderExtension.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())
+                .setChannelNotifyData(toJsonString(notify));
+        assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null),
+                "updateTime", "updater");
+        // 断言 PayOrderDO :数据未更新,因为它是 SUCCESS
+        order.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())
+                .setChannelId(10L).setChannelCode(channel.getCode())
+                .setSuccessTime(notify.getSuccessTime()).setExtensionId(orderExtension.getId()).setNo(orderExtension.getNo())
+                .setChannelOrderNo(notify.getChannelOrderNo()).setChannelUserId(notify.getChannelUserId())
+                .setChannelFeeRate(0.1D).setChannelFeePrice(1);
+        assertPojoEquals(order, orderMapper.selectOne(null),
+                "updateTime", "updater");
+        // 断言,调用
+        verify(notifyService).createPayNotifyTask(argThat(new ArgumentMatcher<PayNotifyTaskCreateReqDTO>() {
+            @Override
+            public boolean matches(PayNotifyTaskCreateReqDTO reqDTO) {
+                assertEquals(reqDTO.getType(), PayNotifyTypeEnum.ORDER.getType());
+                assertEquals(reqDTO.getDataId(), orderExtension.getOrderId());
+                return true;
+            }
+        }));
     }
 
 }

+ 3 - 2
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java

@@ -1,9 +1,9 @@
 package cn.iocoder.yudao.module.pay.service.refund;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.pay.config.PayProperties;
 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
@@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
+import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@@ -29,7 +30,7 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 @Import(PayRefundServiceImpl.class)
-public class PayRefundServiceTest extends BaseDbUnitTest {
+public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
 
     @Resource
     private PayRefundServiceImpl refundService;

+ 1 - 0
yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/clean.sql

@@ -1,4 +1,5 @@
 DELETE FROM pay_app;
 DELETE FROM pay_channel;
 DELETE FROM pay_order;
+DELETE FROM pay_order_extension;
 DELETE FROM pay_refund;

+ 2 - 2
yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql

@@ -44,7 +44,7 @@ CREATE TABLE IF NOT EXISTS `pay_order` (
     `channel_fee_price`    bigint(20)             DEFAULT 0,
     `status`               tinyint(4)    NOT NULL,
     `user_ip`              varchar(50)   NOT NULL,
-    `expire_time`          datetime(0)   NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `expire_time`          timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP,
     `success_time`         datetime(0)            DEFAULT CURRENT_TIMESTAMP,
     `notify_time`          datetime(0)            DEFAULT CURRENT_TIMESTAMP,
     `extension_id` bigint(20)             DEFAULT NULL,
@@ -71,7 +71,7 @@ CREATE TABLE IF NOT EXISTS `pay_order_extension` (
     `channel_extras`     varchar(1024) NULL     DEFAULT NULL,
     `channel_error_code`  varchar(64)  NULL,
     `channel_error_msg` varchar(64)    NULL,
-    `channel_notify_data` varchar(64)  NULL,
+    `channel_notify_data` varchar(1024)  NULL,
     `creator`            varchar(64)   NULL     DEFAULT '',
     `create_time`        datetime(0)   NOT NULL DEFAULT CURRENT_TIMESTAMP,
     `updater`            varchar(64)   NULL     DEFAULT '',