Commit 30a0175edcf29facce2f350f1bb8d80b71c43c24

Authored by YunaiV
1 parent c842ebde

fix(pay): 修正支付通知重试次数与间隔偏移

yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java
... ... @@ -283,13 +283,15 @@ public class PayNotifyServiceImpl implements PayNotifyService {
283 283  
284 284 // 情况二:调用失败、调用异常
285 285 // 2.1 超过最大回调次数
286   - if (updateTask.getNotifyTimes() >= PayNotifyTaskDO.NOTIFY_FREQUENCY.length) {
  286 + // 注意:算上首次的通知,一共 1 + NOTIFY_FREQUENCY.length = 9 次(与 maxNotifyTimes 一致),所以第 9 次通知失败后,才标记为通知失败
  287 + if (updateTask.getNotifyTimes() >= PayNotifyTaskDO.NOTIFY_FREQUENCY.length + 1) {
287 288 updateTask.setStatus(PayNotifyStatusEnum.FAILURE.getStatus());
288 289 notifyTaskMapper.updateById(updateTask);
289 290 return updateTask.getStatus();
290 291 }
291 292 // 2.2 未超过最大回调次数
292   - updateTask.setNextNotifyTime(addTime(Duration.ofSeconds(PayNotifyTaskDO.NOTIFY_FREQUENCY[updateTask.getNotifyTimes()])));
  293 + // 注意:第 N 次通知失败后,使用第 N 个通知间隔(下标为 N - 1),保证 NOTIFY_FREQUENCY 的 8 个间隔从第 1 个开始依次使用
  294 + updateTask.setNextNotifyTime(addTime(Duration.ofSeconds(PayNotifyTaskDO.NOTIFY_FREQUENCY[updateTask.getNotifyTimes() - 1])));
293 295 updateTask.setStatus(invokeException != null ? PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()
294 296 : PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus());
295 297 notifyTaskMapper.updateById(updateTask);
... ...
yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImplUnitTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.pay.service.notify;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  5 +import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
  6 +import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskMapper;
  7 +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
  8 +import org.junit.jupiter.params.ParameterizedTest;
  9 +import org.junit.jupiter.params.provider.Arguments;
  10 +import org.junit.jupiter.params.provider.MethodSource;
  11 +import org.junit.jupiter.params.provider.ValueSource;
  12 +import org.mockito.ArgumentCaptor;
  13 +import org.mockito.InjectMocks;
  14 +import org.mockito.Mock;
  15 +
  16 +import java.time.LocalDateTime;
  17 +import java.util.stream.Stream;
  18 +
  19 +import static org.junit.jupiter.api.Assertions.*;
  20 +import static org.mockito.Mockito.*;
  21 +
  22 +/**
  23 + * {@link PayNotifyServiceImpl} 的单元测试类
  24 + *
  25 + * 重点覆盖 {@link PayNotifyServiceImpl#processNotifyResult(PayNotifyTaskDO, CommonResult, Throwable)} 的通知次数与间隔:
  26 + * 算上首次的通知,一共 1 + 8 = 9 次(与 maxNotifyTimes 一致);第 1 ~ 8 次通知失败后,依次使用 8 个通知间隔;第 9 次通知失败后,标记为通知失败
  27 + *
  28 + * @author 芋道源码
  29 + */
  30 +public class PayNotifyServiceImplUnitTest extends BaseMockitoUnitTest {
  31 +
  32 + private static final Long TASK_ID = 1024L;
  33 + /**
  34 + * 最大通知次数,即 {@link PayNotifyServiceImpl#createPayNotifyTask(Integer, Long)} 写入的 maxNotifyTimes
  35 + */
  36 + private static final int MAX_NOTIFY_TIMES = 9;
  37 +
  38 + @InjectMocks
  39 + private PayNotifyServiceImpl notifyService;
  40 +
  41 + @Mock
  42 + private PayNotifyTaskMapper notifyTaskMapper;
  43 +
  44 + @ParameterizedTest(name = "第 {0} 次通知失败(异常:{2}),{1} 秒后重试")
  45 + @MethodSource("retryIntervals")
  46 + public void testProcessNotifyResult_retry(int notifyTimes, long expectedIntervalSeconds, boolean invokeException) {
  47 + // 准备参数:本次是第 notifyTimes 次通知
  48 + PayNotifyTaskDO task = buildTask(notifyTimes - 1);
  49 +
  50 + // 调用
  51 + LocalDateTime before = LocalDateTime.now();
  52 + Integer status = invokeFailure(task, invokeException);
  53 + LocalDateTime after = LocalDateTime.now();
  54 + // 断言:未超过最大通知次数,按照间隔安排下一次通知
  55 + Integer expectedStatus = invokeException ? PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()
  56 + : PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus();
  57 + assertEquals(expectedStatus, status);
  58 + PayNotifyTaskDO updateTask = captureUpdateTask();
  59 + assertEquals(TASK_ID, updateTask.getId());
  60 + assertEquals(notifyTimes, updateTask.getNotifyTimes());
  61 + assertEquals(expectedStatus, updateTask.getStatus());
  62 + assertBetween(before, after, updateTask.getLastExecuteTime());
  63 + assertBetween(before.plusSeconds(expectedIntervalSeconds), after.plusSeconds(expectedIntervalSeconds),
  64 + updateTask.getNextNotifyTime());
  65 + }
  66 +
  67 + /**
  68 + * 第 1 ~ 8 次通知失败后的通知间隔(秒),与业务约定的 15s、15s、30s、3m、30m、30m、30m、1h 一致
  69 + */
  70 + private static Stream<Arguments> retryIntervals() {
  71 + Stream<Arguments> intervals = Stream.of(
  72 + Arguments.of(1, 15L), Arguments.of(2, 15L), Arguments.of(3, 30L), Arguments.of(4, 180L),
  73 + Arguments.of(5, 1800L), Arguments.of(6, 1800L), Arguments.of(7, 1800L), Arguments.of(8, 3600L));
  74 + // 分别覆盖:业务返回失败、调用异常
  75 + return intervals.flatMap(arguments -> Stream.of(false, true)
  76 + .map(invokeException -> Arguments.of(arguments.get()[0], arguments.get()[1], invokeException)));
  77 + }
  78 +
  79 + @ParameterizedTest(name = "第 9 次通知失败(异常:{0}),标记为通知失败")
  80 + @ValueSource(booleans = {false, true})
  81 + public void testProcessNotifyResult_lastFailure(boolean invokeException) {
  82 + // 准备参数:本次是第 9 次(最后一次)通知
  83 + PayNotifyTaskDO task = buildTask(MAX_NOTIFY_TIMES - 1);
  84 +
  85 + // 调用
  86 + LocalDateTime before = LocalDateTime.now();
  87 + Integer status = invokeFailure(task, invokeException);
  88 + LocalDateTime after = LocalDateTime.now();
  89 + // 断言:标记为通知失败,不再安排下一次通知
  90 + assertEquals(PayNotifyStatusEnum.FAILURE.getStatus(), status);
  91 + PayNotifyTaskDO updateTask = captureUpdateTask();
  92 + assertEquals(TASK_ID, updateTask.getId());
  93 + assertEquals(MAX_NOTIFY_TIMES, updateTask.getNotifyTimes());
  94 + assertEquals(PayNotifyStatusEnum.FAILURE.getStatus(), updateTask.getStatus());
  95 + assertBetween(before, after, updateTask.getLastExecuteTime());
  96 + assertNull(updateTask.getNextNotifyTime());
  97 + }
  98 +
  99 + @ParameterizedTest(name = "第 {0} 次通知成功")
  100 + @ValueSource(ints = {1, 8, MAX_NOTIFY_TIMES})
  101 + public void testProcessNotifyResult_success(int notifyTimes) {
  102 + // 准备参数:本次是第 notifyTimes 次通知
  103 + PayNotifyTaskDO task = buildTask(notifyTimes - 1);
  104 +
  105 + // 调用
  106 + LocalDateTime before = LocalDateTime.now();
  107 + Integer status = notifyService.processNotifyResult(task, CommonResult.success(true), null);
  108 + LocalDateTime after = LocalDateTime.now();
  109 + // 断言:标记为通知成功,不再安排下一次通知
  110 + assertEquals(PayNotifyStatusEnum.SUCCESS.getStatus(), status);
  111 + PayNotifyTaskDO updateTask = captureUpdateTask();
  112 + assertEquals(TASK_ID, updateTask.getId());
  113 + assertEquals(notifyTimes, updateTask.getNotifyTimes());
  114 + assertEquals(PayNotifyStatusEnum.SUCCESS.getStatus(), updateTask.getStatus());
  115 + assertBetween(before, after, updateTask.getLastExecuteTime());
  116 + assertNull(updateTask.getNextNotifyTime());
  117 + }
  118 +
  119 + // ========== 辅助方法 ==========
  120 +
  121 + /**
  122 + * 构建通知任务
  123 + *
  124 + * @param notifyTimes 已经通知的次数
  125 + * @return 通知任务
  126 + */
  127 + private static PayNotifyTaskDO buildTask(int notifyTimes) {
  128 + return new PayNotifyTaskDO().setId(TASK_ID).setNotifyTimes(notifyTimes).setMaxNotifyTimes(MAX_NOTIFY_TIMES)
  129 + .setStatus(notifyTimes == 0 ? PayNotifyStatusEnum.WAITING.getStatus()
  130 + : PayNotifyStatusEnum.REQUEST_FAILURE.getStatus());
  131 + }
  132 +
  133 + /**
  134 + * 处理一次失败的通知结果
  135 + *
  136 + * @param task 通知任务
  137 + * @param invokeException true:调用异常;false:业务返回失败
  138 + * @return 最终任务的状态
  139 + */
  140 + private Integer invokeFailure(PayNotifyTaskDO task, boolean invokeException) {
  141 + if (invokeException) {
  142 + return notifyService.processNotifyResult(task, null, new RuntimeException("模拟请求超时"));
  143 + }
  144 + return notifyService.processNotifyResult(task, CommonResult.error(500, "模拟业务处理失败"), null);
  145 + }
  146 +
  147 + /**
  148 + * 断言:只更新了一次通知任务,并返回更新的内容
  149 + */
  150 + private PayNotifyTaskDO captureUpdateTask() {
  151 + ArgumentCaptor<PayNotifyTaskDO> updateTaskCaptor = ArgumentCaptor.forClass(PayNotifyTaskDO.class);
  152 + verify(notifyTaskMapper).updateById(updateTaskCaptor.capture());
  153 + verifyNoMoreInteractions(notifyTaskMapper);
  154 + return updateTaskCaptor.getValue();
  155 + }
  156 +
  157 + private static void assertBetween(LocalDateTime expectedMin, LocalDateTime expectedMax, LocalDateTime actual) {
  158 + assertNotNull(actual);
  159 + assertFalse(actual.isBefore(expectedMin), () -> actual + " 早于 " + expectedMin);
  160 + assertFalse(actual.isAfter(expectedMax), () -> actual + " 晚于 " + expectedMax);
  161 + }
  162 +
  163 +}
... ...