diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java index 7cc1b815c..8e2f935fc 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java +++ b/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, "退款金额超过订单可退款金额"); diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/PayRefundController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/PayRefundController.java index 24e2b22e5..df131ddd3 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/PayRefundController.java +++ b/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 = "获得退款订单") diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderExtensionMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderExtensionMapper.java index a8918f441..145818417 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderExtensionMapper.java +++ b/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 { @@ -17,4 +19,8 @@ public interface PayOrderExtensionMapper extends BaseMapperX selectListByOrderId(Long orderId) { + return selectList(PayOrderExtensionDO::getOrderId, orderId); + } + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java index 64ecaff94..ee9bcc0aa 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java +++ b/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 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); // 校验支付渠道是否有效 diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java index 35bc7a835..06088eb35 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java +++ b/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); diff --git a/yudao-ui-admin/src/views/pay/app/index.vue b/yudao-ui-admin/src/views/pay/app/index.vue index be92e0dae..8c83856cb 100644 --- a/yudao-ui-admin/src/views/pay/app/index.vue +++ b/yudao-ui-admin/src/views/pay/app/index.vue @@ -43,7 +43,6 @@ @change="handleStatusChange(scope.row)"/> -