diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java index 660cc23..160dad3 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java @@ -71,6 +71,7 @@ public interface ErrorCodeConstants { ErrorCode PAY_TRANSFER_CREATE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_007_009_004, "转账发起失败,原因:已经存在相同的转账单,且状态不是已关闭"); ErrorCode PAY_TRANSFER_NOTIFY_FAIL_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_009_006, "通知转账结果失败,原因:转账单不处于待转账"); ErrorCode PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING = new ErrorCode(1_007_009_007, "通知转账结果失败,原因:转账单不处于待转账或转账中"); + ErrorCode PAY_TRANSFER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1_007_009_008, "发起转账报错,错误码:{},错误提示:{}"); // ========== 示例订单 1-007-900-000 ========== ErrorCode DEMO_ORDER_NOT_FOUND = new ErrorCode(1_007_900_000, "示例订单不存在"); diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java index e2edb15..545d6f7 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -35,8 +36,6 @@ import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; -// TODO @jason:等彻底实现完,单测写写; - /** * 转账 Service 实现类 * @@ -96,19 +95,61 @@ public class PayTransferServiceImpl implements PayTransferService { .setOutTransferNo(transfer.getNo()) .setNotifyUrl(genChannelTransferNotifyUrl(channel)); unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq); - // 4. 通知转账结果 - getSelf().notifyTransfer(channel, unifiedTransferResp); } catch (Throwable e) { // 注意这里仅打印异常,不进行抛出。 // 原因是:虽然调用支付渠道进行转账发生异常(网络请求超时),实际转账成功。这个结果,后续转账轮询可以拿到。 // 或者,使用相同 no 再次发起转账请求 log.error("[createTransfer][转账编号({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e); } + + // 4. 通知转账结果 + if (unifiedTransferResp != null) { + PayTransferDO latestTransfer = null; + try { + getSelf().notifyTransfer(channel, unifiedTransferResp); + } catch (ServiceException ex) { + // 由于转账回调、转账轮询可能同时更新转账单,导致存在并发更新问题,此时以最新的转账状态为准 + latestTransfer = validateTransferNotifyConcurrent(transfer.getId(), ex); + log.warn("[createTransfer][transfer({}) channel({}) 转账结果({}) 通知时发生并发更新,最新状态为({})]", + transfer.getId(), channel.getId(), unifiedTransferResp, latestTransfer.getStatus(), ex); + } + // 如有渠道错误码,则抛出业务异常,提示用户 + // 特殊:如果已被并发更新为成功,则以最新状态为准,避免旧的关闭结果误报 + if (StrUtil.isNotEmpty(unifiedTransferResp.getChannelErrorCode()) + && (latestTransfer == null || PayTransferStatusEnum.isClosed(latestTransfer.getStatus()))) { + throw exception(PAY_TRANSFER_SUBMIT_CHANNEL_ERROR, unifiedTransferResp.getChannelErrorCode(), + unifiedTransferResp.getChannelErrorMsg()); + } + } return new PayTransferCreateRespDTO().setId(transfer.getId()) .setChannelPackageInfo(unifiedTransferResp != null ? unifiedTransferResp.getChannelPackageInfo() : null); } /** + * 校验转账结果通知时的并发更新 + * + * 只有状态竞争导致的通知失败,并且转账单已被并发更新为成功或关闭时,才返回最新的转账单; + * 否则,直接抛出原异常,避免掩盖真实的通知失败 + * + * @param id 转账单编号 + * @param ex 通知转账结果时的业务异常 + * @return 最新的转账单 + */ + private PayTransferDO validateTransferNotifyConcurrent(Long id, ServiceException ex) { + // 1. 校验是否为状态竞争 + if (ObjectUtil.notEqual(ex.getCode(), PAY_TRANSFER_NOTIFY_FAIL_STATUS_IS_NOT_WAITING.getCode()) + && ObjectUtil.notEqual(ex.getCode(), PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING.getCode())) { + throw ex; + } + // 2. 校验最新状态为成功或关闭 + PayTransferDO latestTransfer = transferMapper.selectById(id); + if (latestTransfer == null || !PayTransferStatusEnum.isSuccessOrClosed(latestTransfer.getStatus())) { + throw ex; + } + return latestTransfer; + } + + /** * 根据支付渠道的编码,生成支付渠道的回调地址 * * @param channel 支付渠道 diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceTest.java index 26bd189..787b5b8 100644 --- a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceTest.java +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceTest.java @@ -1,23 +1,40 @@ package cn.iocoder.yudao.module.pay.service.transfer; +import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest; +import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO; +import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateRespDTO; +import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO; import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO; import cn.iocoder.yudao.module.pay.dal.mysql.transfer.PayTransferMapper; import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO; +import cn.iocoder.yudao.module.pay.enums.PayChannelEnum; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum; import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties; +import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient; import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; 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 jakarta.annotation.Resource; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; import org.springframework.context.annotation.Import; import org.springframework.test.context.bean.override.mockito.MockitoBean; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; /** * {@link PayTransferServiceImpl} 的单元测试类 @@ -88,4 +105,286 @@ public class PayTransferServiceTest extends BaseDbAndRedisUnitTest { assertEquals("package-info-110", dbTransfer.getChannelPackageInfo()); } + @Test // 调用 unifiedTransfer 接口,发生异常(例如说:网络超时) + public void testCreateTransfer_unifiedTransferException() { + // mock 方法(client) + PayClient client = mockPayClient(); + when(client.unifiedTransfer(any())).thenThrow(new RuntimeException("模拟网络超时")); + // 准备参数 + PayTransferCreateReqDTO reqDTO = buildTransferCreateReqDTO(); + + // 调用:未知结果不抛出异常,保留 WAITING 等待轮询 + PayTransferCreateRespDTO respDTO = transferService.createTransfer(reqDTO); + // 断言 + PayTransferDO dbTransfer = transferMapper.selectById(respDTO.getId()); + assertNotNull(dbTransfer); + assertEquals(PayTransferStatusEnum.WAITING.getStatus(), dbTransfer.getStatus()); + assertNull(respDTO.getChannelPackageInfo()); + verify(notifyService, never()).createPayNotifyTask(any(), any()); + } + + @Test // 调用 unifiedTransfer 接口,返回 null + public void testCreateTransfer_nullResponse() { + // mock 方法(client) + PayClient client = mockPayClient(); + when(client.unifiedTransfer(any())).thenReturn(null); + // 准备参数 + PayTransferCreateReqDTO reqDTO = buildTransferCreateReqDTO(); + + // 调用 + PayTransferCreateRespDTO respDTO = transferService.createTransfer(reqDTO); + // 断言 + PayTransferDO dbTransfer = transferMapper.selectById(respDTO.getId()); + assertEquals(PayTransferStatusEnum.WAITING.getStatus(), dbTransfer.getStatus()); + assertNull(respDTO.getChannelPackageInfo()); + } + + @Test // 调用 unifiedTransfer 接口,返回存在渠道错误 + public void testCreateTransfer_channelError() { + // mock 方法(client) + PayClient client = mockPayClient(); + when(client.unifiedTransfer(any())).thenAnswer(invocation -> { + PayTransferUnifiedReqDTO unifiedReqDTO = invocation.getArgument(0); + return PayTransferRespDTO.closedOf("PAYEE_NOT_EXIST", "收款账号不存在", + unifiedReqDTO.getOutTransferNo(), "closed"); + }); + // 准备参数 + PayTransferCreateReqDTO reqDTO = buildTransferCreateReqDTO(); + + // 调用,并断言异常 + assertServiceException(() -> transferService.createTransfer(reqDTO), + PAY_TRANSFER_SUBMIT_CHANNEL_ERROR, "PAYEE_NOT_EXIST", "收款账号不存在"); + // 断言:关闭状态已落库 + PayTransferDO dbTransfer = transferMapper.selectByAppIdAndMerchantOrderId(1L, reqDTO.getMerchantTransferId()); + assertNotNull(dbTransfer); + assertEquals(PayTransferStatusEnum.CLOSED.getStatus(), dbTransfer.getStatus()); + assertEquals("PAYEE_NOT_EXIST", dbTransfer.getChannelErrorCode()); + assertEquals("收款账号不存在", dbTransfer.getChannelErrorMsg()); + verify(notifyService).createPayNotifyTask(eq(PayNotifyTypeEnum.TRANSFER.getType()), eq(dbTransfer.getId())); + } + + @Test // 调用 unifiedTransfer 接口,返回关闭,但不存在渠道错误 + public void testCreateTransfer_closedWithoutChannelError() { + // mock 方法(client) + PayClient client = mockPayClient(); + when(client.unifiedTransfer(any())).thenAnswer(invocation -> { + PayTransferUnifiedReqDTO unifiedReqDTO = invocation.getArgument(0); + return PayTransferRespDTO.closedOf(null, null, unifiedReqDTO.getOutTransferNo(), "closed"); + }); + // 准备参数 + PayTransferCreateReqDTO reqDTO = buildTransferCreateReqDTO(); + + // 调用 + PayTransferCreateRespDTO respDTO = transferService.createTransfer(reqDTO); + // 断言 + PayTransferDO dbTransfer = transferMapper.selectById(respDTO.getId()); + assertEquals(PayTransferStatusEnum.CLOSED.getStatus(), dbTransfer.getStatus()); + } + + @Test // 调用 unifiedTransfer 接口,返回成功 + public void testCreateTransfer_success() { + // mock 方法(client) + PayClient client = mockPayClient(); + when(client.unifiedTransfer(any())).thenAnswer(invocation -> { + PayTransferUnifiedReqDTO unifiedReqDTO = invocation.getArgument(0); + return PayTransferRespDTO.successOf("CHANNEL_T001", LocalDateTime.now(), + unifiedReqDTO.getOutTransferNo(), "success"); + }); + // 准备参数 + PayTransferCreateReqDTO reqDTO = buildTransferCreateReqDTO(); + + // 调用 + PayTransferCreateRespDTO respDTO = transferService.createTransfer(reqDTO); + // 断言 + PayTransferDO dbTransfer = transferMapper.selectById(respDTO.getId()); + assertEquals(PayTransferStatusEnum.SUCCESS.getStatus(), dbTransfer.getStatus()); + assertEquals("CHANNEL_T001", dbTransfer.getChannelTransferNo()); + verify(notifyService).createPayNotifyTask(eq(PayNotifyTypeEnum.TRANSFER.getType()), eq(dbTransfer.getId())); + } + + @Test // 调用 unifiedTransfer 接口,返回转账中 + public void testCreateTransfer_processing() { + // mock 方法(client) + PayClient client = mockPayClient(); + when(client.unifiedTransfer(any())).thenAnswer(invocation -> { + PayTransferUnifiedReqDTO unifiedReqDTO = invocation.getArgument(0); + PayTransferRespDTO resp = PayTransferRespDTO.processingOf("CHANNEL_T001", + unifiedReqDTO.getOutTransferNo(), "processing"); + resp.setChannelPackageInfo("package-info-001"); + return resp; + }); + // 准备参数 + PayTransferCreateReqDTO reqDTO = buildTransferCreateReqDTO(); + + // 调用 + PayTransferCreateRespDTO respDTO = transferService.createTransfer(reqDTO); + // 断言 + assertEquals("package-info-001", respDTO.getChannelPackageInfo()); + PayTransferDO dbTransfer = transferMapper.selectById(respDTO.getId()); + assertEquals(PayTransferStatusEnum.PROCESSING.getStatus(), dbTransfer.getStatus()); + assertEquals("package-info-001", dbTransfer.getChannelPackageInfo()); + } + + @Test // 调用 unifiedTransfer 接口返回成功,但通知转账结果时找不到转账单 + public void testCreateTransfer_notifyTransferNotFound() { + // mock 方法(client):返回不存在的转账单号,使 notifyTransfer 抛出异常 + PayClient client = mockPayClient(); + when(client.unifiedTransfer(any())).thenReturn(PayTransferRespDTO.successOf("CHANNEL_T001", + LocalDateTime.now(), "NOT_EXISTS", "success")); + // 准备参数 + PayTransferCreateReqDTO reqDTO = buildTransferCreateReqDTO(); + + // 调用,并断言异常:非并发更新的通知异常,不能被吞掉 + assertServiceException(() -> transferService.createTransfer(reqDTO), PAY_TRANSFER_NOT_FOUND); + // 断言 + PayTransferDO dbTransfer = transferMapper.selectByAppIdAndMerchantOrderId(1L, reqDTO.getMerchantTransferId()); + assertEquals(PayTransferStatusEnum.WAITING.getStatus(), dbTransfer.getStatus()); + } + + @Test // 调用 unifiedTransfer 接口返回存在渠道错误,但创建转账通知任务时发生异常 + public void testCreateTransfer_channelErrorAndNotifyTaskException() { + // mock 方法(client) + PayClient client = mockPayClient(); + when(client.unifiedTransfer(any())).thenAnswer(invocation -> { + PayTransferUnifiedReqDTO unifiedReqDTO = invocation.getArgument(0); + return PayTransferRespDTO.closedOf("PAYEE_NOT_EXIST", "收款账号不存在", + unifiedReqDTO.getOutTransferNo(), "closed"); + }); + // mock 方法(notify) + RuntimeException notifyTaskException = new RuntimeException("模拟创建通知任务异常"); + doThrow(notifyTaskException).when(notifyService).createPayNotifyTask(any(), any()); + // 准备参数 + PayTransferCreateReqDTO reqDTO = buildTransferCreateReqDTO(); + + // 调用,并断言异常:抛出真实的通知异常,而不是渠道错误 + RuntimeException ex = assertThrows(RuntimeException.class, () -> transferService.createTransfer(reqDTO)); + assertSame(notifyTaskException, ex); + // 断言:关闭状态随通知事务回滚 + PayTransferDO dbTransfer = transferMapper.selectByAppIdAndMerchantOrderId(1L, reqDTO.getMerchantTransferId()); + assertEquals(PayTransferStatusEnum.WAITING.getStatus(), dbTransfer.getStatus()); + assertNull(dbTransfer.getChannelErrorCode()); + } + + @Test // 调用 unifiedTransfer 接口返回存在渠道错误,但转账单已被并发更新为成功 + public void testCreateTransfer_channelErrorWhenConcurrentSuccess() { + // mock 方法(client):返回前,转账回调已经将转账单更新为成功 + PayClient client = mockPayClient(); + when(client.unifiedTransfer(any())).thenAnswer(invocation -> { + PayTransferUnifiedReqDTO unifiedReqDTO = invocation.getArgument(0); + updateTransferStatus(unifiedReqDTO.getOutTransferNo(), PayTransferStatusEnum.SUCCESS.getStatus()); + return PayTransferRespDTO.closedOf("PAYEE_NOT_EXIST", "收款账号不存在", + unifiedReqDTO.getOutTransferNo(), "closed"); + }); + // 准备参数 + PayTransferCreateReqDTO reqDTO = buildTransferCreateReqDTO(); + + // 调用:以最新的成功状态为准,不误报渠道错误 + PayTransferCreateRespDTO respDTO = transferService.createTransfer(reqDTO); + // 断言 + PayTransferDO dbTransfer = transferMapper.selectById(respDTO.getId()); + assertEquals(PayTransferStatusEnum.SUCCESS.getStatus(), dbTransfer.getStatus()); + assertNull(dbTransfer.getChannelErrorCode()); + verify(notifyService, never()).createPayNotifyTask(any(), any()); + } + + @Test // 调用 unifiedTransfer 接口返回成功,但转账单已被并发更新为关闭 + public void testCreateTransfer_successWhenConcurrentClosed() { + // mock 方法(client):返回前,转账轮询已经将转账单更新为关闭 + PayClient client = mockPayClient(); + when(client.unifiedTransfer(any())).thenAnswer(invocation -> { + PayTransferUnifiedReqDTO unifiedReqDTO = invocation.getArgument(0); + updateTransferStatus(unifiedReqDTO.getOutTransferNo(), PayTransferStatusEnum.CLOSED.getStatus()); + return PayTransferRespDTO.successOf("CHANNEL_T001", LocalDateTime.now(), + unifiedReqDTO.getOutTransferNo(), "success"); + }); + // 准备参数 + PayTransferCreateReqDTO reqDTO = buildTransferCreateReqDTO(); + + // 调用:以最新的关闭状态为准 + PayTransferCreateRespDTO respDTO = transferService.createTransfer(reqDTO); + // 断言 + PayTransferDO dbTransfer = transferMapper.selectById(respDTO.getId()); + assertEquals(PayTransferStatusEnum.CLOSED.getStatus(), dbTransfer.getStatus()); + verify(notifyService, never()).createPayNotifyTask(any(), any()); + } + + @Test // 调用 unifiedTransfer 接口返回存在渠道错误,通知时发生状态竞争,且转账单已被并发更新为关闭 + public void testCreateTransfer_channelErrorWhenConcurrentClosed() { + PayTransferServiceImpl transferServiceImpl = mock(PayTransferServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayTransferServiceImpl.class))) + .thenReturn(transferServiceImpl); + doThrow(exception(PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING)) + .when(transferServiceImpl).notifyTransfer(any(PayChannelDO.class), any()); + // mock 方法(client):返回前,转账轮询已经将转账单更新为关闭 + PayClient client = mockPayClient(); + when(client.unifiedTransfer(any())).thenAnswer(invocation -> { + PayTransferUnifiedReqDTO unifiedReqDTO = invocation.getArgument(0); + updateTransferStatus(unifiedReqDTO.getOutTransferNo(), PayTransferStatusEnum.CLOSED.getStatus()); + return PayTransferRespDTO.closedOf("PAYEE_NOT_EXIST", "收款账号不存在", + unifiedReqDTO.getOutTransferNo(), "closed"); + }); + // 准备参数 + PayTransferCreateReqDTO reqDTO = buildTransferCreateReqDTO(); + + // 调用,并断言异常:已确认关闭,提示渠道错误 + assertServiceException(() -> transferService.createTransfer(reqDTO), + PAY_TRANSFER_SUBMIT_CHANNEL_ERROR, "PAYEE_NOT_EXIST", "收款账号不存在"); + } + } + + @Test // 调用 unifiedTransfer 接口返回存在渠道错误,通知时发生状态竞争,但转账单仍是待转账 + public void testCreateTransfer_channelErrorWhenConcurrentWaiting() { + PayTransferServiceImpl transferServiceImpl = mock(PayTransferServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayTransferServiceImpl.class))) + .thenReturn(transferServiceImpl); + doThrow(exception(PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING)) + .when(transferServiceImpl).notifyTransfer(any(PayChannelDO.class), any()); + // mock 方法(client) + PayClient client = mockPayClient(); + when(client.unifiedTransfer(any())).thenAnswer(invocation -> { + PayTransferUnifiedReqDTO unifiedReqDTO = invocation.getArgument(0); + return PayTransferRespDTO.closedOf("PAYEE_NOT_EXIST", "收款账号不存在", + unifiedReqDTO.getOutTransferNo(), "closed"); + }); + // 准备参数 + PayTransferCreateReqDTO reqDTO = buildTransferCreateReqDTO(); + + // 调用,并断言异常:无法确认最新状态,抛出原通知异常 + assertServiceException(() -> transferService.createTransfer(reqDTO), + PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING); + // 断言 + PayTransferDO dbTransfer = transferMapper.selectByAppIdAndMerchantOrderId(1L, reqDTO.getMerchantTransferId()); + assertEquals(PayTransferStatusEnum.WAITING.getStatus(), dbTransfer.getStatus()); + } + } + + private PayClient mockPayClient() { + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L).setTransferNotifyUrl("http://127.0.0.1/transfer")); + when(appService.validPayApp(eq("demo"))).thenReturn(app); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L) + .setCode(PayChannelEnum.ALIPAY_PC.getCode())); + when(channelService.validPayChannel(eq(1L), eq(PayChannelEnum.ALIPAY_PC.getCode()))).thenReturn(channel); + when(payProperties.getTransferNotifyUrl()).thenReturn("http://127.0.0.1"); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + return client; + } + + private void updateTransferStatus(String no, Integer status) { + PayTransferDO transfer = transferMapper.selectByAppIdAndNo(1L, no); + transferMapper.updateById(new PayTransferDO().setId(transfer.getId()).setStatus(status)); + } + + private static PayTransferCreateReqDTO buildTransferCreateReqDTO() { + return new PayTransferCreateReqDTO().setAppKey("demo").setChannelCode(PayChannelEnum.ALIPAY_PC.getCode()) + .setUserIp("127.0.0.1").setUserId(1L).setUserType(1) + .setMerchantTransferId("1001").setSubject("佣金提现").setPrice(100) + .setUserAccount("test@example.com").setUserName("芋艿"); + } + }