浏览代码

mall + pay:
1. 发起支付时,增加实际已支付的二次校验,避免重复支付

YunaiV 1 年之前
父节点
当前提交
348d073718

+ 2 - 1
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java

@@ -23,7 +23,7 @@ public interface ErrorCodeConstants {
     // ========== ORDER 模块 1007002000 ==========
     ErrorCode ORDER_NOT_FOUND = new ErrorCode(1007002000, "支付订单不存在");
     ErrorCode ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付");
-    ErrorCode ORDER_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007002002, "支付订单不处于已支付");
+    ErrorCode ORDER_STATUS_IS_SUCCESS = new ErrorCode(1007002002, "订单已支付,请刷新页面");
     ErrorCode ORDER_IS_EXPIRED = new ErrorCode(1007002003, "支付订单已经过期");
     ErrorCode ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1007002004, "发起支付报错,错误码:{},错误提示:{}");
     ErrorCode ORDER_REFUND_FAIL_STATUS_ERROR = new ErrorCode(1007002005, "支付订单退款失败,原因:状态不是已支付或已退款");
@@ -31,6 +31,7 @@ public interface ErrorCodeConstants {
     // ========== ORDER 模块(拓展单) 1007003000 ==========
     ErrorCode ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在");
     ErrorCode ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付");
+    ErrorCode ORDER_EXTENSION_IS_PAID = new ErrorCode(1007003002, "订单已支付,请等待支付结果");
 
     // ========== 支付模块(退款) 1007006000 ==========
     ErrorCode REFUND_PRICE_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额");

+ 0 - 3
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/PayRefundController.java

@@ -10,7 +10,6 @@ import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
-import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
@@ -44,8 +43,6 @@ public class PayRefundController {
     private PayRefundService refundService;
     @Resource
     private PayAppService appService;
-    @Resource
-    private PayOrderService orderService;
 
     @GetMapping("/get")
     @Operation(summary = "获得退款订单")

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

@@ -5,6 +5,8 @@ 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.util.List;
+
 @Mapper
 public interface PayOrderExtensionMapper extends BaseMapperX<PayOrderExtensionDO> {
 
@@ -17,4 +19,8 @@ public interface PayOrderExtensionMapper extends BaseMapperX<PayOrderExtensionDO
                 .eq(PayOrderExtensionDO::getId, id).eq(PayOrderExtensionDO::getStatus, status));
     }
 
+    default List<PayOrderExtensionDO> selectListByOrderId(Long orderId) {
+        return selectList(PayOrderExtensionDO::getOrderId, orderId);
+    }
+
 }

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

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.pay.service.order;
 
-import cn.hutool.core.lang.Pair;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.spring.SpringUtil;
@@ -33,6 +32,7 @@ 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 cn.iocoder.yudao.module.pay.util.MoneyUtils;
+import com.google.common.annotations.VisibleForTesting;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -129,10 +129,10 @@ public class PayOrderServiceImpl implements PayOrderService {
 
     @Override // 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtensionDO 回滚了
     public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) {
-        // 1. 获得 PayOrderDO ,并校验其是否存在
+        // 1.1 获得 PayOrderDO ,并校验其是否存在
         PayOrderDO order = validateOrderCanSubmit(reqVO.getId());
-        // 1.2 校验支付渠道是否有效
-        PayChannelDO channel = validatePayChannelCanSubmit(order.getAppId(), reqVO.getChannelCode());
+        // 1.32 校验支付渠道是否有效
+        PayChannelDO channel = validateChannelCanSubmit(order.getAppId(), reqVO.getChannelCode());
         PayClient client = payClientFactory.getPayClient(channel.getId());
 
         // 2. 插入 PayOrderExtensionDO
@@ -173,16 +173,52 @@ public class PayOrderServiceImpl implements PayOrderService {
         if (order == null) { // 是否存在
             throw exception(ORDER_NOT_FOUND);
         }
+        if (PayOrderStatusEnum.isSuccess(order.getStatus())) { // 校验状态,发现已支付
+            throw exception(ORDER_STATUS_IS_SUCCESS);
+        }
         if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
             throw exception(ORDER_STATUS_IS_NOT_WAITING);
         }
         if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期
             throw exception(ORDER_IS_EXPIRED);
         }
+
+        // 【重要】校验是否支付拓展单已支付,只是没有回调、或者数据不正常
+        validateOrderActuallyPaid(id);
         return order;
     }
 
-    private PayChannelDO validatePayChannelCanSubmit(Long appId, String channelCode) {
+    /**
+     * 校验支付订单实际已支付
+     *
+     * @param id 支付编号
+     */
+    @VisibleForTesting
+    void validateOrderActuallyPaid(Long id) {
+        List<PayOrderExtensionDO> orderExtensions = orderExtensionMapper.selectListByOrderId(id);
+        orderExtensions.forEach(orderExtension -> {
+            // 情况一:校验数据库中的 orderExtension 是不是已支付
+            if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
+                log.warn("[validateOrderCanSubmit][order({}) 的 extension({}) 已支付,可能是数据不一致]",
+                        id, orderExtension.getId());
+                throw exception(ORDER_EXTENSION_IS_PAID);
+            }
+            // 情况二:调用三方接口,查询支付单状态,是不是已支付
+            PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId());
+            if (payClient == null) {
+                log.error("[validateOrderCanSubmit][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
+                return;
+            }
+            PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo());
+            if (respDTO != null && PayOrderStatusRespEnum.isSuccess(respDTO.getStatus())) {
+                log.warn("[validateOrderCanSubmit][order({}) 的 PayOrderRespDTO({}) 已支付,可能是回调延迟]",
+                        id, toJsonString(respDTO));
+                throw exception(ORDER_EXTENSION_IS_PAID);
+            }
+        });
+    }
+
+    private PayChannelDO validateChannelCanSubmit(Long appId, String channelCode) {
         // 校验 App
         appService.validPayApp(appId);
         // 校验支付渠道是否有效

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

@@ -267,7 +267,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
     @Test
     public void testSubmitOrder_notWaiting() {
         // mock 数据(order)
-        PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()));
+        PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus()));
         orderMapper.insert(order);
         // 准备参数
         PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()));
@@ -277,6 +277,19 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
         assertServiceException(() -> orderService.submitOrder(reqVO, userIp), ORDER_STATUS_IS_NOT_WAITING);
     }
 
+    @Test
+    public void testSubmitOrder_isSuccess() {
+        // mock 数据(order)
+        PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()));
+        orderMapper.insert(order);
+        // 准备参数
+        PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()));
+        String userIp = randomString();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> orderService.submitOrder(reqVO, userIp), ORDER_STATUS_IS_SUCCESS);
+    }
+
     @Test
     public void testSubmitOrder_expired() {
         // mock 数据(order)
@@ -426,6 +439,57 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
         }
     }
 
+    @Test
+    public void testValidateOrderActuallyPaid_dbPaid() {
+        // 准备参数
+        Long id = randomLongId();
+        // mock 方法(OrderExtension 已支付)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.SUCCESS.getStatus()));
+        orderExtensionMapper.insert(orderExtension);
+
+        // 调用,并断言异常
+        assertServiceException(() -> orderService.validateOrderActuallyPaid(id),
+                ORDER_EXTENSION_IS_PAID);
+    }
+
+    @Test
+    public void testValidateOrderActuallyPaid_remotePaid() {
+        // 准备参数
+        Long id = randomLongId();
+        // mock 方法(OrderExtension 已支付)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.WAITING.getStatus()));
+        orderExtensionMapper.insert(orderExtension);
+        // mock 方法(PayClient 已支付)
+        PayClient client = mock(PayClient.class);
+        when(payClientFactory.getPayClient(eq(orderExtension.getChannelId()))).thenReturn(client);
+        when(client.getOrder(eq(orderExtension.getNo()))).thenReturn(randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())));
+
+        // 调用,并断言异常
+        assertServiceException(() -> orderService.validateOrderActuallyPaid(id),
+                ORDER_EXTENSION_IS_PAID);
+    }
+
+    @Test
+    public void testValidateOrderActuallyPaid_success() {
+        // 准备参数
+        Long id = randomLongId();
+        // mock 方法(OrderExtension 已支付)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.WAITING.getStatus()));
+        orderExtensionMapper.insert(orderExtension);
+        // mock 方法(PayClient 已支付)
+        PayClient client = mock(PayClient.class);
+        when(payClientFactory.getPayClient(eq(orderExtension.getChannelId()))).thenReturn(client);
+        when(client.getOrder(eq(orderExtension.getNo()))).thenReturn(randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())));
+
+        // 调用,并断言异常
+        orderService.validateOrderActuallyPaid(id);
+    }
+
     @Test
     public void testNotifyOrder_channelId() {
         PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class);

+ 0 - 1
yudao-ui-admin/src/views/pay/app/index.vue

@@ -43,7 +43,6 @@
                      @change="handleStatusChange(scope.row)"/>
         </template>
       </el-table-column>
-      <el-table-column label="商户名称" align="center" prop="payMerchant.name"/>
       <el-table-column label="支付宝配置" align="center">
         <el-table-column :label="payChannelEnum.ALIPAY_APP.name" align="center">
           <template v-slot="scope">

+ 0 - 1
yudao-ui-admin/src/views/pay/cashier/index.vue

@@ -78,7 +78,6 @@
 </template>
 <script>
 import QrcodeVue from 'qrcode.vue'
-import { DICT_TYPE, getDictDatas } from "@/utils/dict";
 import { getOrder, submitOrder } from '@/api/pay/order';
 import { PayChannelEnum, PayDisplayModeEnum, PayOrderStatusEnum } from "@/utils/constants";