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 23 // ========== ORDER 模块 1007002000 ==========
24 24 ErrorCode ORDER_NOT_FOUND = new ErrorCode(1007002000, "支付订单不存在");
25 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 27 ErrorCode ORDER_IS_EXPIRED = new ErrorCode(1007002003, "支付订单已经过期");
28 28 ErrorCode ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1007002004, "发起支付报错,错误码:{},错误提示:{}");
29 29 ErrorCode ORDER_REFUND_FAIL_STATUS_ERROR = new ErrorCode(1007002005, "支付订单退款失败,原因:状态不是已支付或已退款");
... ... @@ -31,6 +31,7 @@ public interface ErrorCodeConstants {
31 31 // ========== ORDER 模块(拓展单) 1007003000 ==========
32 32 ErrorCode ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在");
33 33 ErrorCode ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付");
  34 + ErrorCode ORDER_EXTENSION_IS_PAID = new ErrorCode(1007003002, "订单已支付,请等待支付结果");
34 35  
35 36 // ========== 支付模块(退款) 1007006000 ==========
36 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 10 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
11 11 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
12 12 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
13   -import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
14 13 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
15 14 import io.swagger.v3.oas.annotations.Operation;
16 15 import io.swagger.v3.oas.annotations.Parameter;
... ... @@ -44,8 +43,6 @@ public class PayRefundController {
44 43 private PayRefundService refundService;
45 44 @Resource
46 45 private PayAppService appService;
47   - @Resource
48   - private PayOrderService orderService;
49 46  
50 47 @GetMapping("/get")
51 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 5 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
6 6 import org.apache.ibatis.annotations.Mapper;
7 7  
  8 +import java.util.List;
  9 +
8 10 @Mapper
9 11 public interface PayOrderExtensionMapper extends BaseMapperX<PayOrderExtensionDO> {
10 12  
... ... @@ -17,4 +19,8 @@ public interface PayOrderExtensionMapper extends BaseMapperX&lt;PayOrderExtensionDO
17 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 1 package cn.iocoder.yudao.module.pay.service.order;
2 2  
3   -import cn.hutool.core.lang.Pair;
4 3 import cn.hutool.core.util.ObjectUtil;
5 4 import cn.hutool.core.util.StrUtil;
6 5 import cn.hutool.extra.spring.SpringUtil;
... ... @@ -33,6 +32,7 @@ import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
33 32 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
34 33 import cn.iocoder.yudao.module.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
35 34 import cn.iocoder.yudao.module.pay.util.MoneyUtils;
  35 +import com.google.common.annotations.VisibleForTesting;
36 36 import lombok.extern.slf4j.Slf4j;
37 37 import org.springframework.stereotype.Service;
38 38 import org.springframework.transaction.annotation.Transactional;
... ... @@ -129,10 +129,10 @@ public class PayOrderServiceImpl implements PayOrderService {
129 129  
130 130 @Override // 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtensionDO 回滚了
131 131 public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) {
132   - // 1. 获得 PayOrderDO ,并校验其是否存在
  132 + // 1.1 获得 PayOrderDO ,并校验其是否存在
133 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 136 PayClient client = payClientFactory.getPayClient(channel.getId());
137 137  
138 138 // 2. 插入 PayOrderExtensionDO
... ... @@ -173,16 +173,52 @@ public class PayOrderServiceImpl implements PayOrderService {
173 173 if (order == null) { // 是否存在
174 174 throw exception(ORDER_NOT_FOUND);
175 175 }
  176 + if (PayOrderStatusEnum.isSuccess(order.getStatus())) { // 校验状态,发现已支付
  177 + throw exception(ORDER_STATUS_IS_SUCCESS);
  178 + }
176 179 if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
177 180 throw exception(ORDER_STATUS_IS_NOT_WAITING);
178 181 }
179 182 if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期
180 183 throw exception(ORDER_IS_EXPIRED);
181 184 }
  185 +
  186 + // 【重要】校验是否支付拓展单已支付,只是没有回调、或者数据不正常
  187 + validateOrderActuallyPaid(id);
182 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 222 // 校验 App
187 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 267 @Test
268 268 public void testSubmitOrder_notWaiting() {
269 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 271 orderMapper.insert(order);
272 272 // 准备参数
273 273 PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()));
... ... @@ -278,6 +278,19 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
278 278 }
279 279  
280 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 294 public void testSubmitOrder_expired() {
282 295 // mock 数据(order)
283 296 PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
... ... @@ -427,6 +440,57 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
427 440 }
428 441  
429 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 494 public void testNotifyOrder_channelId() {
431 495 PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class);
432 496 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
... ...
yudao-ui-admin/src/views/pay/app/index.vue
... ... @@ -43,7 +43,6 @@
43 43 @change="handleStatusChange(scope.row)"/>
44 44 </template>
45 45 </el-table-column>
46   - <el-table-column label="商户名称" align="center" prop="payMerchant.name"/>
47 46 <el-table-column label="支付宝配置" align="center">
48 47 <el-table-column :label="payChannelEnum.ALIPAY_APP.name" align="center">
49 48 <template v-slot="scope">
... ...
yudao-ui-admin/src/views/pay/cashier/index.vue
... ... @@ -78,7 +78,6 @@
78 78 </template>
79 79 <script>
80 80 import QrcodeVue from 'qrcode.vue'
81   -import { DICT_TYPE, getDictDatas } from "@/utils/dict";
82 81 import { getOrder, submitOrder } from '@/api/pay/order';
83 82 import { PayChannelEnum, PayDisplayModeEnum, PayOrderStatusEnum } from "@/utils/constants";
84 83  
... ...