mirror of
https://gitee.com/huangge1199_admin/vue-pro.git
synced 2025-01-18 19:20:05 +08:00
mall + pay:
1. 发起支付时,增加实际已支付的二次校验,避免重复支付
This commit is contained in:
parent
717caf527a
commit
348d073718
@ -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, "退款金额超过订单可退款金额");
|
||||
|
@ -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 = "获得退款订单")
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
// 校验支付渠道是否有效
|
||||
|
@ -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);
|
||||
|
@ -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">
|
||||
|
@ -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";
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user