by gateway:

1. notify 部分单元测试
This commit is contained in:
zhijiantianya@gmail.com 2023-07-24 20:11:21 +08:00
parent 00e1c30f57
commit 73e1158836
14 changed files with 238 additions and 66 deletions

View File

@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -40,6 +41,7 @@ public class PayNotifyTaskDO extends TenantBaseDO {
/**
* 编号自增
*/
@TableId
private Long id;
/**
* 应用编号

View File

@ -11,7 +11,7 @@ import org.redisson.api.RLock;
public interface RedisKeyConstants {
RedisKeyDefine PAY_NOTIFY_LOCK = new RedisKeyDefine("通知任务的分布式锁",
"pay_notify:lock:", // 参数来自 DefaultLockKeyBuilder
"pay_notify:lock:%d", // 参数来自 DefaultLockKeyBuilder
RedisKeyDefine.KeyTypeEnum.HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson Lock 使用 Hash 数据结构
/**

View File

@ -4,9 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
import cn.iocoder.yudao.module.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
import javax.validation.Valid;
import java.util.List;
/**
@ -19,9 +17,10 @@ public interface PayNotifyService {
/**
* 创建回调通知任务
*
* @param reqDTO 任务信息
* @param type 类型
* @param dataId 数据编号
*/
void createPayNotifyTask(@Valid PayNotifyTaskCreateReqDTO reqDTO);
void createPayNotifyTask(Integer type, Long dataId);
/**
* 执行回调通知

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.pay.service.notify;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
@ -22,7 +23,6 @@ import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskMapper;
import cn.iocoder.yudao.module.pay.dal.redis.notify.PayNotifyLockRedisDAO;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.module.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import lombok.extern.slf4j.Slf4j;
@ -84,15 +84,10 @@ public class PayNotifyServiceImpl implements PayNotifyService {
@Resource
private PayNotifyLockRedisDAO notifyLockCoreRedisDAO;
@Resource
@Lazy // 循环依赖自己依赖自己避免报错
private PayNotifyServiceImpl self;
@Override
@Transactional(rollbackFor = Exception.class)
public void createPayNotifyTask(PayNotifyTaskCreateReqDTO reqDTO) {
PayNotifyTaskDO task = new PayNotifyTaskDO();
task.setType(reqDTO.getType()).setDataId(reqDTO.getDataId());
public void createPayNotifyTask(Integer type, Long dataId) {
PayNotifyTaskDO task = new PayNotifyTaskDO().setType(type).setDataId(dataId);
task.setStatus(PayNotifyStatusEnum.WAITING.getStatus()).setNextNotifyTime(LocalDateTime.now())
.setNotifyTimes(0).setMaxNotifyTimes(PayNotifyTaskDO.NOTIFY_FREQUENCY.length + 1);
// 补充 appId + notifyUrl 字段
@ -178,7 +173,7 @@ public class PayNotifyServiceImpl implements PayNotifyService {
}
// 执行通知
self.executeNotify0(dbTask);
getSelf().executeNotify0(dbTask);
});
}
@ -285,4 +280,13 @@ public class PayNotifyServiceImpl implements PayNotifyService {
return notifyLogMapper.selectListByTaskId(taskId);
}
/**
* 获得自身的代理对象解决 AOP 生效问题
*
* @return 自己
*/
private PayNotifyServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
}

View File

@ -1,32 +0,0 @@
package cn.iocoder.yudao.module.pay.service.notify.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
/**
* 支付通知创建 DTO
*
* @author 芋道源码
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayNotifyTaskCreateReqDTO {
/**
* 类型
*/
@NotNull(message = "类型不能为空")
private Integer type;
/**
* 数据编号
*/
@NotNull(message = "数据编号不能为空")
private Long dataId;
}

View File

@ -31,7 +31,6 @@ import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
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;
@ -282,8 +281,8 @@ public class PayOrderServiceImpl implements PayOrderService {
}
// 3. 插入支付通知记录
notifyService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
.type(PayNotifyTypeEnum.ORDER.getType()).dataId(orderExtension.getOrderId()).build());
notifyService.createPayNotifyTask(PayNotifyTypeEnum.ORDER.getType(),
orderExtension.getOrderId());
}
/**

View File

@ -26,7 +26,6 @@ import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
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.service.order.PayOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -242,8 +241,8 @@ public class PayRefundServiceImpl implements PayRefundService {
orderService.updateOrderRefundPrice(refund.getOrderId(), refund.getRefundPrice());
// 3. 插入退款通知记录
notifyService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
.type(PayNotifyTypeEnum.REFUND.getType()).dataId(refund.getId()).build());
notifyService.createPayNotifyTask(PayNotifyTypeEnum.REFUND.getType(),
refund.getId());
}
private void notifyRefundFailure(PayChannelDO channel, PayRefundRespDTO notify) {
@ -273,8 +272,8 @@ public class PayRefundServiceImpl implements PayRefundService {
log.info("[notifyRefundFailure][退款订单({}) 更新为退款失败]", refund.getId());
// 2. 插入退款通知记录
notifyService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
.type(PayNotifyTypeEnum.REFUND.getType()).dataId(refund.getId()).build());
notifyService.createPayNotifyTask(PayNotifyTypeEnum.REFUND.getType(),
refund.getId());
}
@Override

View File

@ -0,0 +1,184 @@
package cn.iocoder.yudao.module.pay.service.notify;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyLogMapper;
import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskMapper;
import cn.iocoder.yudao.module.pay.dal.redis.notify.PayNotifyLockRedisDAO;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.module.pay.framework.job.config.PayJobConfiguration;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundServiceImpl;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import javax.annotation.Resource;
import java.time.Duration;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
/**
* {@link PayRefundServiceImpl} 的单元测试类
*
* @author 芋艿
*/
@Import({PayJobConfiguration.class, PayNotifyServiceImpl.class, PayNotifyLockRedisDAO.class})
public class PayNotifyServiceTest extends BaseDbUnitTest {
@Resource
private PayNotifyServiceImpl notifyService;
@MockBean
private PayOrderService orderService;
@MockBean
private PayRefundService refundService;
@Resource
private PayNotifyTaskMapper notifyTaskMapper;
@Resource
private PayNotifyLogMapper notifyLogMapper;
@MockBean
private RedissonClient redissonClient;
@Test
public void testCreatePayNotifyTask_order() {
PayNotifyServiceImpl payNotifyService = mock(PayNotifyServiceImpl.class);
try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayNotifyServiceImpl.class)))
.thenReturn(payNotifyService);
// 准备参数
Integer type = PayNotifyTypeEnum.ORDER.getType();
Long dataId = 1L;
// mock 方法(order)
PayOrderDO order = randomPojo(PayOrderDO.class);
when(orderService.getOrder(eq(1L))).thenReturn(order);
// mock 方法lock
mockLock(null); // null 的原因是咱没办法拿到 taskId 新增
// 调用
notifyService.createPayNotifyTask(type, dataId);
// 断言task
PayNotifyTaskDO dbTask = notifyTaskMapper.selectOne(null);
assertNotNull(dbTask.getNextNotifyTime());
assertThat(dbTask)
.extracting("type", "dataId", "status", "notifyTimes", "maxNotifyTimes",
"appId", "merchantOrderId", "notifyUrl")
.containsExactly(type, dataId, PayNotifyStatusEnum.WAITING.getStatus(), 0, 9,
order.getAppId(), order.getMerchantOrderId(), order.getNotifyUrl());
// 断言调用
verify(payNotifyService).executeNotify0(eq(dbTask));
}
}
@Test
public void testCreatePayNotifyTask_refund() {
PayNotifyServiceImpl payNotifyService = mock(PayNotifyServiceImpl.class);
try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayNotifyServiceImpl.class)))
.thenReturn(payNotifyService);
// 准备参数
Integer type = PayNotifyTypeEnum.REFUND.getType();
Long dataId = 1L;
// mock 方法(refund)
PayRefundDO refund = randomPojo(PayRefundDO.class);
when(refundService.getRefund(eq(1L))).thenReturn(refund);
// mock 方法lock
mockLock(null); // null 的原因是咱没办法拿到 taskId 新增
// 调用
notifyService.createPayNotifyTask(type, dataId);
// 断言task
PayNotifyTaskDO dbTask = notifyTaskMapper.selectOne(null);
assertNotNull(dbTask.getNextNotifyTime());
assertThat(dbTask)
.extracting("type", "dataId", "status", "notifyTimes", "maxNotifyTimes",
"appId", "merchantOrderId", "notifyUrl")
.containsExactly(type, dataId, PayNotifyStatusEnum.WAITING.getStatus(), 0, 9,
refund.getAppId(), refund.getMerchantOrderId(), refund.getNotifyUrl());
// 断言调用
verify(payNotifyService).executeNotify0(eq(dbTask));
}
}
@Test
public void testExecuteNotify() throws InterruptedException {
// mock 数据notify
PayNotifyTaskDO dbTask01 = randomPojo(PayNotifyTaskDO.class,
o -> o.setStatus(PayNotifyStatusEnum.WAITING.getStatus())
.setNextNotifyTime(addTime(Duration.ofMinutes(-1))));
notifyTaskMapper.insert(dbTask01);
PayNotifyTaskDO dbTask02 = randomPojo(PayNotifyTaskDO.class,
o -> o.setStatus(PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus())
.setNextNotifyTime(addTime(Duration.ofMinutes(-1))));
notifyTaskMapper.insert(dbTask02);
PayNotifyTaskDO dbTask03 = randomPojo(PayNotifyTaskDO.class,
o -> o.setStatus(PayNotifyStatusEnum.REQUEST_FAILURE.getStatus())
.setNextNotifyTime(addTime(Duration.ofMinutes(-1))));
notifyTaskMapper.insert(dbTask03);
PayNotifyTaskDO dbTask04 = randomPojo(PayNotifyTaskDO.class, // 不满足状态
o -> o.setStatus(PayNotifyStatusEnum.FAILURE.getStatus())
.setNextNotifyTime(addTime(Duration.ofMinutes(-1))));
notifyTaskMapper.insert(dbTask04);
PayNotifyTaskDO dbTask05 = randomPojo(PayNotifyTaskDO.class, // 不满足状态
o -> o.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus())
.setNextNotifyTime(addTime(Duration.ofMinutes(-1))));
notifyTaskMapper.insert(dbTask05);
PayNotifyTaskDO dbTask06 = randomPojo(PayNotifyTaskDO.class, // 不满足时间
o -> o.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus())
.setNextNotifyTime(addTime(Duration.ofMinutes(1))));
notifyTaskMapper.insert(dbTask06);
// mock 方法lock
mockLock(dbTask01.getId());
mockLock(dbTask02.getId());
mockLock(dbTask03.getId());
// 调用
int count = notifyService.executeNotify();
// 断言数量
assertEquals(count, 3);
}
@Test // 由于 HttpUtil 不好 mock所以只测试异常的情况
public void testExecuteNotify0_exception() {
// mock 数据task
PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, o -> o.setType(-1)
.setNotifyTimes(0).setMaxNotifyTimes(9));
notifyTaskMapper.insert(task);
// 调用
notifyService.executeNotify0(task);
}
private void mockLock(Long id) {
RLock lock = mock(RLock.class);
if (id == null) {
when(redissonClient.getLock(anyString()))
.thenReturn(lock);
} else {
when(redissonClient.getLock(eq("pay_notify:lock:" + id)))
.thenReturn(lock);
}
}
}

View File

@ -27,7 +27,6 @@ import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
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 org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
@ -622,7 +621,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
// 断言 PayOrderDO 数据未更新因为它是 SUCCESS
assertPojoEquals(order, orderMapper.selectOne(null));
// 断言调用
verify(notifyService, never()).createPayNotifyTask(any(PayNotifyTaskCreateReqDTO.class));
verify(notifyService, never()).createPayNotifyTask(anyInt(), anyLong());
}
@Test
@ -661,11 +660,8 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
assertPojoEquals(order, orderMapper.selectOne(null),
"updateTime", "updater");
// 断言调用
verify(notifyService).createPayNotifyTask(argThat(reqDTO -> {
assertEquals(reqDTO.getType(), PayNotifyTypeEnum.ORDER.getType());
assertEquals(reqDTO.getDataId(), orderExtension.getOrderId());
return true;
}));
verify(notifyService).createPayNotifyTask(eq(PayNotifyTypeEnum.ORDER.getType()),
eq(orderExtension.getOrderId()));
}
@Test

View File

@ -25,7 +25,6 @@ import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
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.service.order.PayOrderService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -552,8 +551,8 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
"updateTime", "updater");
// 断言调用
verify(orderService).updateOrderRefundPrice(eq(100L), eq(23));
verify(notifyService).createPayNotifyTask(eq(PayNotifyTaskCreateReqDTO.builder()
.type(PayNotifyTypeEnum.REFUND.getType()).dataId(refund.getId()).build()));
verify(notifyService).createPayNotifyTask(eq(PayNotifyTypeEnum.REFUND.getType()),
eq(refund.getId()));
}
@Test
@ -624,8 +623,8 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
assertPojoEquals(refund, refundMapper.selectById(refund.getId()),
"updateTime", "updater");
// 断言调用
verify(notifyService).createPayNotifyTask(eq(PayNotifyTaskCreateReqDTO.builder()
.type(PayNotifyTypeEnum.REFUND.getType()).dataId(refund.getId()).build()));
verify(notifyService).createPayNotifyTask(eq(PayNotifyTypeEnum.REFUND.getType()),
eq(refund.getId()));
}
@Test

View File

@ -3,3 +3,4 @@ DELETE FROM pay_channel;
DELETE FROM pay_order;
DELETE FROM pay_order_extension;
DELETE FROM pay_refund;
DELETE FROM pay_notify_task;

View File

@ -109,3 +109,24 @@ CREATE TABLE IF NOT EXISTS `pay_refund` (
`deleted` bit(1) NOT NULL DEFAULT FALSE,
PRIMARY KEY ("id")
) COMMENT = '退款订单';
CREATE TABLE IF NOT EXISTS `pay_notify_task` (
"id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
`app_id` bigint(20) NOT NULL,
`type` tinyint(4) NOT NULL,
`data_id` bigint(20) NOT NULL,
`merchant_order_id` varchar(64) NOT NULL,
`status` tinyint(4) NOT NULL,
`next_notify_time` datetime(0) NULL DEFAULT NULL,
`last_execute_time` datetime(0) NULL DEFAULT NULL,
`notify_times` int NOT NULL,
`max_notify_times` int NOT NULL,
`notify_url` varchar(1024) NOT NULL,
`creator` varchar(64) NULL DEFAULT '',
`create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updater` varchar(64) NULL DEFAULT '',
`update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` bit(1) NOT NULL DEFAULT FALSE,
`tenant_id` bigint(20) NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
) COMMENT = '支付通知';