Commit 348d073718afcd34daa77845335716670796b9be

Authored by YunaiV
1 parent 717caf52

mall + pay:

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 { @@ -23,7 +23,7 @@ public interface ErrorCodeConstants {
23 // ========== ORDER 模块 1007002000 ========== 23 // ========== ORDER 模块 1007002000 ==========
24 ErrorCode ORDER_NOT_FOUND = new ErrorCode(1007002000, "支付订单不存在"); 24 ErrorCode ORDER_NOT_FOUND = new ErrorCode(1007002000, "支付订单不存在");
25 ErrorCode ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付"); 25 ErrorCode ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付");
26 - ErrorCode ORDER_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007002002, "支付订单不处于已支付"); 26 + ErrorCode ORDER_STATUS_IS_SUCCESS = new ErrorCode(1007002002, "订单已支付,请刷新页面");
27 ErrorCode ORDER_IS_EXPIRED = new ErrorCode(1007002003, "支付订单已经过期"); 27 ErrorCode ORDER_IS_EXPIRED = new ErrorCode(1007002003, "支付订单已经过期");
28 ErrorCode ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1007002004, "发起支付报错,错误码:{},错误提示:{}"); 28 ErrorCode ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1007002004, "发起支付报错,错误码:{},错误提示:{}");
29 ErrorCode ORDER_REFUND_FAIL_STATUS_ERROR = new ErrorCode(1007002005, "支付订单退款失败,原因:状态不是已支付或已退款"); 29 ErrorCode ORDER_REFUND_FAIL_STATUS_ERROR = new ErrorCode(1007002005, "支付订单退款失败,原因:状态不是已支付或已退款");
@@ -31,6 +31,7 @@ public interface ErrorCodeConstants { @@ -31,6 +31,7 @@ public interface ErrorCodeConstants {
31 // ========== ORDER 模块(拓展单) 1007003000 ========== 31 // ========== ORDER 模块(拓展单) 1007003000 ==========
32 ErrorCode ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在"); 32 ErrorCode ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在");
33 ErrorCode ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付"); 33 ErrorCode ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付");
  34 + ErrorCode ORDER_EXTENSION_IS_PAID = new ErrorCode(1007003002, "订单已支付,请等待支付结果");
34 35
35 // ========== 支付模块(退款) 1007006000 ========== 36 // ========== 支付模块(退款) 1007006000 ==========
36 ErrorCode REFUND_PRICE_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额"); 37 ErrorCode REFUND_PRICE_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额");
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; @@ -10,7 +10,6 @@ import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
10 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; 10 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
11 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; 11 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
12 import cn.iocoder.yudao.module.pay.service.app.PayAppService; 12 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
13 -import cn.iocoder.yudao.module.pay.service.order.PayOrderService;  
14 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; 13 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
15 import io.swagger.v3.oas.annotations.Operation; 14 import io.swagger.v3.oas.annotations.Operation;
16 import io.swagger.v3.oas.annotations.Parameter; 15 import io.swagger.v3.oas.annotations.Parameter;
@@ -44,8 +43,6 @@ public class PayRefundController { @@ -44,8 +43,6 @@ public class PayRefundController {
44 private PayRefundService refundService; 43 private PayRefundService refundService;
45 @Resource 44 @Resource
46 private PayAppService appService; 45 private PayAppService appService;
47 - @Resource  
48 - private PayOrderService orderService;  
49 46
50 @GetMapping("/get") 47 @GetMapping("/get")
51 @Operation(summary = "获得退款订单") 48 @Operation(summary = "获得退款订单")
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; @@ -5,6 +5,8 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
5 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 5 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
6 import org.apache.ibatis.annotations.Mapper; 6 import org.apache.ibatis.annotations.Mapper;
7 7
  8 +import java.util.List;
  9 +
8 @Mapper 10 @Mapper
9 public interface PayOrderExtensionMapper extends BaseMapperX<PayOrderExtensionDO> { 11 public interface PayOrderExtensionMapper extends BaseMapperX<PayOrderExtensionDO> {
10 12
@@ -17,4 +19,8 @@ public interface PayOrderExtensionMapper extends BaseMapperX&lt;PayOrderExtensionDO @@ -17,4 +19,8 @@ public interface PayOrderExtensionMapper extends BaseMapperX&lt;PayOrderExtensionDO
17 .eq(PayOrderExtensionDO::getId, id).eq(PayOrderExtensionDO::getStatus, status)); 19 .eq(PayOrderExtensionDO::getId, id).eq(PayOrderExtensionDO::getStatus, status));
18 } 20 }
19 21
  22 + default List<PayOrderExtensionDO> selectListByOrderId(Long orderId) {
  23 + return selectList(PayOrderExtensionDO::getOrderId, orderId);
  24 + }
  25 +
20 } 26 }
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java
1 package cn.iocoder.yudao.module.pay.service.order; 1 package cn.iocoder.yudao.module.pay.service.order;
2 2
3 -import cn.hutool.core.lang.Pair;  
4 import cn.hutool.core.util.ObjectUtil; 3 import cn.hutool.core.util.ObjectUtil;
5 import cn.hutool.core.util.StrUtil; 4 import cn.hutool.core.util.StrUtil;
6 import cn.hutool.extra.spring.SpringUtil; 5 import cn.hutool.extra.spring.SpringUtil;
@@ -33,6 +32,7 @@ import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; @@ -33,6 +32,7 @@ import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
33 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; 32 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
34 import cn.iocoder.yudao.module.pay.service.notify.dto.PayNotifyTaskCreateReqDTO; 33 import cn.iocoder.yudao.module.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
35 import cn.iocoder.yudao.module.pay.util.MoneyUtils; 34 import cn.iocoder.yudao.module.pay.util.MoneyUtils;
  35 +import com.google.common.annotations.VisibleForTesting;
36 import lombok.extern.slf4j.Slf4j; 36 import lombok.extern.slf4j.Slf4j;
37 import org.springframework.stereotype.Service; 37 import org.springframework.stereotype.Service;
38 import org.springframework.transaction.annotation.Transactional; 38 import org.springframework.transaction.annotation.Transactional;
@@ -129,10 +129,10 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -129,10 +129,10 @@ public class PayOrderServiceImpl implements PayOrderService {
129 129
130 @Override // 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtensionDO 回滚了 130 @Override // 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtensionDO 回滚了
131 public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) { 131 public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) {
132 - // 1. 获得 PayOrderDO ,并校验其是否存在 132 + // 1.1 获得 PayOrderDO ,并校验其是否存在
133 PayOrderDO order = validateOrderCanSubmit(reqVO.getId()); 133 PayOrderDO order = validateOrderCanSubmit(reqVO.getId());
134 - // 1.2 校验支付渠道是否有效  
135 - PayChannelDO channel = validatePayChannelCanSubmit(order.getAppId(), reqVO.getChannelCode()); 134 + // 1.32 校验支付渠道是否有效
  135 + PayChannelDO channel = validateChannelCanSubmit(order.getAppId(), reqVO.getChannelCode());
136 PayClient client = payClientFactory.getPayClient(channel.getId()); 136 PayClient client = payClientFactory.getPayClient(channel.getId());
137 137
138 // 2. 插入 PayOrderExtensionDO 138 // 2. 插入 PayOrderExtensionDO
@@ -173,16 +173,52 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -173,16 +173,52 @@ public class PayOrderServiceImpl implements PayOrderService {
173 if (order == null) { // 是否存在 173 if (order == null) { // 是否存在
174 throw exception(ORDER_NOT_FOUND); 174 throw exception(ORDER_NOT_FOUND);
175 } 175 }
  176 + if (PayOrderStatusEnum.isSuccess(order.getStatus())) { // 校验状态,发现已支付
  177 + throw exception(ORDER_STATUS_IS_SUCCESS);
  178 + }
176 if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付 179 if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
177 throw exception(ORDER_STATUS_IS_NOT_WAITING); 180 throw exception(ORDER_STATUS_IS_NOT_WAITING);
178 } 181 }
179 if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期 182 if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期
180 throw exception(ORDER_IS_EXPIRED); 183 throw exception(ORDER_IS_EXPIRED);
181 } 184 }
  185 +
  186 + // 【重要】校验是否支付拓展单已支付,只是没有回调、或者数据不正常
  187 + validateOrderActuallyPaid(id);
182 return order; 188 return order;
183 } 189 }
184 190
185 - private PayChannelDO validatePayChannelCanSubmit(Long appId, String channelCode) { 191 + /**
  192 + * 校验支付订单实际已支付
  193 + *
  194 + * @param id 支付编号
  195 + */
  196 + @VisibleForTesting
  197 + void validateOrderActuallyPaid(Long id) {
  198 + List<PayOrderExtensionDO> orderExtensions = orderExtensionMapper.selectListByOrderId(id);
  199 + orderExtensions.forEach(orderExtension -> {
  200 + // 情况一:校验数据库中的 orderExtension 是不是已支付
  201 + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
  202 + log.warn("[validateOrderCanSubmit][order({}) 的 extension({}) 已支付,可能是数据不一致]",
  203 + id, orderExtension.getId());
  204 + throw exception(ORDER_EXTENSION_IS_PAID);
  205 + }
  206 + // 情况二:调用三方接口,查询支付单状态,是不是已支付
  207 + PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId());
  208 + if (payClient == null) {
  209 + log.error("[validateOrderCanSubmit][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
  210 + return;
  211 + }
  212 + PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo());
  213 + if (respDTO != null && PayOrderStatusRespEnum.isSuccess(respDTO.getStatus())) {
  214 + log.warn("[validateOrderCanSubmit][order({}) 的 PayOrderRespDTO({}) 已支付,可能是回调延迟]",
  215 + id, toJsonString(respDTO));
  216 + throw exception(ORDER_EXTENSION_IS_PAID);
  217 + }
  218 + });
  219 + }
  220 +
  221 + private PayChannelDO validateChannelCanSubmit(Long appId, String channelCode) {
186 // 校验 App 222 // 校验 App
187 appService.validPayApp(appId); 223 appService.validPayApp(appId);
188 // 校验支付渠道是否有效 224 // 校验支付渠道是否有效
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 { @@ -267,7 +267,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
267 @Test 267 @Test
268 public void testSubmitOrder_notWaiting() { 268 public void testSubmitOrder_notWaiting() {
269 // mock 数据(order) 269 // mock 数据(order)
270 - PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); 270 + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus()));
271 orderMapper.insert(order); 271 orderMapper.insert(order);
272 // 准备参数 272 // 准备参数
273 PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId())); 273 PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()));
@@ -278,6 +278,19 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest { @@ -278,6 +278,19 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
278 } 278 }
279 279
280 @Test 280 @Test
  281 + public void testSubmitOrder_isSuccess() {
  282 + // mock 数据(order)
  283 + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()));
  284 + orderMapper.insert(order);
  285 + // 准备参数
  286 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()));
  287 + String userIp = randomString();
  288 +
  289 + // 调用, 并断言异常
  290 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), ORDER_STATUS_IS_SUCCESS);
  291 + }
  292 +
  293 + @Test
281 public void testSubmitOrder_expired() { 294 public void testSubmitOrder_expired() {
282 // mock 数据(order) 295 // mock 数据(order)
283 PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) 296 PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
@@ -427,6 +440,57 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest { @@ -427,6 +440,57 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
427 } 440 }
428 441
429 @Test 442 @Test
  443 + public void testValidateOrderActuallyPaid_dbPaid() {
  444 + // 准备参数
  445 + Long id = randomLongId();
  446 + // mock 方法(OrderExtension 已支付)
  447 + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
  448 + o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.SUCCESS.getStatus()));
  449 + orderExtensionMapper.insert(orderExtension);
  450 +
  451 + // 调用,并断言异常
  452 + assertServiceException(() -> orderService.validateOrderActuallyPaid(id),
  453 + ORDER_EXTENSION_IS_PAID);
  454 + }
  455 +
  456 + @Test
  457 + public void testValidateOrderActuallyPaid_remotePaid() {
  458 + // 准备参数
  459 + Long id = randomLongId();
  460 + // mock 方法(OrderExtension 已支付)
  461 + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
  462 + o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.WAITING.getStatus()));
  463 + orderExtensionMapper.insert(orderExtension);
  464 + // mock 方法(PayClient 已支付)
  465 + PayClient client = mock(PayClient.class);
  466 + when(payClientFactory.getPayClient(eq(orderExtension.getChannelId()))).thenReturn(client);
  467 + when(client.getOrder(eq(orderExtension.getNo()))).thenReturn(randomPojo(PayOrderRespDTO.class,
  468 + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())));
  469 +
  470 + // 调用,并断言异常
  471 + assertServiceException(() -> orderService.validateOrderActuallyPaid(id),
  472 + ORDER_EXTENSION_IS_PAID);
  473 + }
  474 +
  475 + @Test
  476 + public void testValidateOrderActuallyPaid_success() {
  477 + // 准备参数
  478 + Long id = randomLongId();
  479 + // mock 方法(OrderExtension 已支付)
  480 + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
  481 + o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.WAITING.getStatus()));
  482 + orderExtensionMapper.insert(orderExtension);
  483 + // mock 方法(PayClient 已支付)
  484 + PayClient client = mock(PayClient.class);
  485 + when(payClientFactory.getPayClient(eq(orderExtension.getChannelId()))).thenReturn(client);
  486 + when(client.getOrder(eq(orderExtension.getNo()))).thenReturn(randomPojo(PayOrderRespDTO.class,
  487 + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())));
  488 +
  489 + // 调用,并断言异常
  490 + orderService.validateOrderActuallyPaid(id);
  491 + }
  492 +
  493 + @Test
430 public void testNotifyOrder_channelId() { 494 public void testNotifyOrder_channelId() {
431 PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); 495 PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class);
432 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { 496 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
yudao-ui-admin/src/views/pay/app/index.vue
@@ -43,7 +43,6 @@ @@ -43,7 +43,6 @@
43 @change="handleStatusChange(scope.row)"/> 43 @change="handleStatusChange(scope.row)"/>
44 </template> 44 </template>
45 </el-table-column> 45 </el-table-column>
46 - <el-table-column label="商户名称" align="center" prop="payMerchant.name"/>  
47 <el-table-column label="支付宝配置" align="center"> 46 <el-table-column label="支付宝配置" align="center">
48 <el-table-column :label="payChannelEnum.ALIPAY_APP.name" align="center"> 47 <el-table-column :label="payChannelEnum.ALIPAY_APP.name" align="center">
49 <template v-slot="scope"> 48 <template v-slot="scope">
yudao-ui-admin/src/views/pay/cashier/index.vue
@@ -78,7 +78,6 @@ @@ -78,7 +78,6 @@
78 </template> 78 </template>
79 <script> 79 <script>
80 import QrcodeVue from 'qrcode.vue' 80 import QrcodeVue from 'qrcode.vue'
81 -import { DICT_TYPE, getDictDatas } from "@/utils/dict";  
82 import { getOrder, submitOrder } from '@/api/pay/order'; 81 import { getOrder, submitOrder } from '@/api/pay/order';
83 import { PayChannelEnum, PayDisplayModeEnum, PayOrderStatusEnum } from "@/utils/constants"; 82 import { PayChannelEnum, PayDisplayModeEnum, PayOrderStatusEnum } from "@/utils/constants";
84 83