diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java index ac756f5..24df6f8 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java @@ -131,6 +131,11 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService { order, payOrderId); throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); } + // 1.3 校验支付单号,与订单绑定的支付单号一致。避免其它支付应用中,相同商户订单编号、金额的支付单被用于串单 + if (ObjectUtil.notEqual(order.getPayOrderId(), payOrderId)) { + log.error("[updateDemoOrderPaid][order({}) 支付单不匹配({}),请进行处理!]", order, payOrderId); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + } // 2. 校验支付订单的合法性 PayOrderRespDTO payOrder = validatePayOrderPaid(order, payOrderId); diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImpl.java index dddbcee..eeb5262 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImpl.java @@ -146,6 +146,11 @@ public class PayDemoWithdrawServiceImpl implements PayDemoWithdrawService { log.error("[updateDemoWithdrawStatus][withdraw({}) 转账单不匹配({}),请进行处理!]", withdraw, payTransferId); throw exception(DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_ID_ERROR); } + // 1.3 校验转账单编号,与提现单绑定的转账单编号一致。避免其它支付应用、或重新转账前的转账单被用于更新状态 + if (ObjectUtil.notEqual(withdraw.getPayTransferId(), payTransferId)) { + log.error("[updateDemoWithdrawStatus][withdraw({}) 转账单不匹配({}),请进行处理!]", withdraw, payTransferId); + throw exception(DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_ID_ERROR); + } // 2. 校验提现单的合法性 PayTransferRespDTO payTransfer = validateDemoTransferStatusCanUpdate(withdraw, payTransferId); diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImplTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImplTest.java new file mode 100644 index 0000000..4d3e40d --- /dev/null +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImplTest.java @@ -0,0 +1,132 @@ +package cn.iocoder.yudao.module.pay.service.demo; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.pay.api.order.PayOrderApi; +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO; +import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi; +import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO; +import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoOrderMapper; +import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; +import static org.mockito.Mockito.*; + +/** + * {@link PayDemoOrderServiceImpl} 的单元测试 + * + * @author 芋道源码 + */ +public class PayDemoOrderServiceImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private PayDemoOrderServiceImpl payDemoOrderService; + + @Mock + private PayOrderApi payOrderApi; + @Mock + private PayRefundApi payRefundApi; + @Mock + private PayDemoOrderMapper payDemoOrderMapper; + + @Test + public void testUpdateDemoOrderPaid_success() { + // mock 数据 + when(payDemoOrderMapper.selectById(eq(1L))).thenReturn(buildOrder(false)); + when(payOrderApi.getOrder(eq(100L))).thenReturn(buildPayOrder(100L, PayOrderStatusEnum.SUCCESS.getStatus(), 10, "1")); + when(payDemoOrderMapper.updateByIdAndPayed(eq(1L), eq(false), any())).thenReturn(1); + + // 调用 + payDemoOrderService.updateDemoOrderPaid(1L, 100L); + // 断言 + verify(payDemoOrderMapper).updateByIdAndPayed(eq(1L), eq(false), + argThat(updateObj -> updateObj.getPayStatus() && "mock".equals(updateObj.getPayChannelCode()))); + } + + @Test + public void testUpdateDemoOrderPaid_repeatNotify() { + // mock 数据:已支付,且支付单号相同 + when(payDemoOrderMapper.selectById(eq(1L))).thenReturn(buildOrder(true)); + + // 调用:重复通知,幂等返回 + payDemoOrderService.updateDemoOrderPaid(1L, 100L); + // 断言 + verifyNoInteractions(payOrderApi); + verify(payDemoOrderMapper, never()).updateByIdAndPayed(anyLong(), anyBoolean(), any()); + } + + @Test + public void testUpdateDemoOrderPaid_paidPayOrderIdNotMatch() { + // mock 数据:已支付,但支付单号不同 + when(payDemoOrderMapper.selectById(eq(1L))).thenReturn(buildOrder(true)); + + // 调用,并断言异常 + assertServiceException(() -> payDemoOrderService.updateDemoOrderPaid(1L, 200L), + DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + verifyNoInteractions(payOrderApi); + verify(payDemoOrderMapper, never()).updateByIdAndPayed(anyLong(), anyBoolean(), any()); + } + + @Test + public void testUpdateDemoOrderPaid_unpaidPayOrderIdNotBound() { + // mock 数据:未支付;其它支付应用中,存在相同商户订单编号、金额,且已支付的支付单 + when(payDemoOrderMapper.selectById(eq(1L))).thenReturn(buildOrder(false)); + + // 调用,并断言异常 + assertServiceException(() -> payDemoOrderService.updateDemoOrderPaid(1L, 200L), + DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + // 断言:不查询支付单,也不更新订单 + verifyNoInteractions(payOrderApi); + verify(payDemoOrderMapper, never()).updateByIdAndPayed(anyLong(), anyBoolean(), any()); + } + + @Test + public void testUpdateDemoOrderPaid_payOrderNotSuccess() { + // mock 数据:支付单号一致,但未支付 + when(payDemoOrderMapper.selectById(eq(1L))).thenReturn(buildOrder(false)); + when(payOrderApi.getOrder(eq(100L))).thenReturn(buildPayOrder(100L, PayOrderStatusEnum.WAITING.getStatus(), 10, "1")); + + // 调用,并断言异常 + assertServiceException(() -> payDemoOrderService.updateDemoOrderPaid(1L, 100L), + DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS); + verify(payDemoOrderMapper, never()).updateByIdAndPayed(anyLong(), anyBoolean(), any()); + } + + @Test + public void testUpdateDemoOrderPaid_payPriceNotMatch() { + // mock 数据:支付单号一致,但金额不同 + when(payDemoOrderMapper.selectById(eq(1L))).thenReturn(buildOrder(false)); + when(payOrderApi.getOrder(eq(100L))).thenReturn(buildPayOrder(100L, PayOrderStatusEnum.SUCCESS.getStatus(), 1, "1")); + + // 调用,并断言异常 + assertServiceException(() -> payDemoOrderService.updateDemoOrderPaid(1L, 100L), + DEMO_ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH); + verify(payDemoOrderMapper, never()).updateByIdAndPayed(anyLong(), anyBoolean(), any()); + } + + @Test + public void testUpdateDemoOrderPaid_merchantOrderIdNotMatch() { + // mock 数据:支付单号一致,但商户订单编号不同 + when(payDemoOrderMapper.selectById(eq(1L))).thenReturn(buildOrder(false)); + when(payOrderApi.getOrder(eq(100L))).thenReturn(buildPayOrder(100L, PayOrderStatusEnum.SUCCESS.getStatus(), 10, "2")); + + // 调用,并断言异常 + assertServiceException(() -> payDemoOrderService.updateDemoOrderPaid(1L, 100L), + DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + verify(payDemoOrderMapper, never()).updateByIdAndPayed(anyLong(), anyBoolean(), any()); + } + + private static PayDemoOrderDO buildOrder(boolean payStatus) { + return new PayDemoOrderDO().setId(1L).setUserId(1L).setSpuId(1L).setSpuName("华为手机") + .setPrice(10).setPayStatus(payStatus).setPayOrderId(100L); + } + + private static PayOrderRespDTO buildPayOrder(Long id, Integer status, Integer price, String merchantOrderId) { + return new PayOrderRespDTO().setId(id).setStatus(status).setPrice(price) + .setMerchantOrderId(merchantOrderId).setChannelCode("mock"); + } + +} diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImplTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImplTest.java new file mode 100644 index 0000000..0215463 --- /dev/null +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImplTest.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.pay.service.demo; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.pay.api.transfer.PayTransferApi; +import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO; +import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoWithdrawDO; +import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoWithdrawMapper; +import cn.iocoder.yudao.module.pay.enums.PayChannelEnum; +import cn.iocoder.yudao.module.pay.enums.demo.PayDemoWithdrawStatusEnum; +import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_ID_ERROR; +import static org.mockito.Mockito.*; + +/** + * {@link PayDemoWithdrawServiceImpl} 的单元测试 + * + * @author 芋道源码 + */ +public class PayDemoWithdrawServiceImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private PayDemoWithdrawServiceImpl payDemoWithdrawService; + + @Mock + private PayDemoWithdrawMapper demoTransferMapper; + @Mock + private PayTransferApi payTransferApi; + + @Test + public void testUpdateDemoWithdrawTransferred_success() { + // mock 数据 + when(demoTransferMapper.selectById(eq(1L))).thenReturn(buildWithdraw(PayDemoWithdrawStatusEnum.WAITING.getStatus())); + when(payTransferApi.getTransfer(eq(100L))).thenReturn(buildTransfer(100L)); + + // 调用 + payDemoWithdrawService.updateDemoWithdrawTransferred(1L, 100L); + // 断言 + verify(demoTransferMapper).updateByIdAndStatus(eq(1L), eq(PayDemoWithdrawStatusEnum.WAITING.getStatus()), + argThat(updateObj -> PayDemoWithdrawStatusEnum.SUCCESS.getStatus().equals(updateObj.getStatus()))); + } + + @Test + public void testUpdateDemoWithdrawTransferred_repeatNotify() { + // mock 数据:已结束,且转账单编号相同 + when(demoTransferMapper.selectById(eq(1L))).thenReturn(buildWithdraw(PayDemoWithdrawStatusEnum.SUCCESS.getStatus())); + + // 调用:重复通知,幂等返回 + payDemoWithdrawService.updateDemoWithdrawTransferred(1L, 100L); + // 断言 + verifyNoInteractions(payTransferApi); + verify(demoTransferMapper, never()).updateByIdAndStatus(anyLong(), anyInt(), any()); + } + + @Test + public void testUpdateDemoWithdrawTransferred_waitingPayTransferIdNotBound() { + // mock 数据:未结束;其它支付应用、或重新转账前,存在相同商户转账编号、金额、渠道的转账单 + when(demoTransferMapper.selectById(eq(1L))).thenReturn(buildWithdraw(PayDemoWithdrawStatusEnum.WAITING.getStatus())); + + // 调用,并断言异常 + assertServiceException(() -> payDemoWithdrawService.updateDemoWithdrawTransferred(1L, 200L), + DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_ID_ERROR); + // 断言:不查询转账单,也不更新提现单 + verifyNoInteractions(payTransferApi); + verify(demoTransferMapper, never()).updateByIdAndStatus(anyLong(), anyInt(), any()); + } + + private static PayDemoWithdrawDO buildWithdraw(Integer status) { + return new PayDemoWithdrawDO().setId(1L).setSubject("提现").setPrice(10).setStatus(status) + .setPayTransferId(100L).setTransferChannelCode(PayChannelEnum.WALLET.getCode()); + } + + private static PayTransferRespDTO buildTransfer(Long id) { + return new PayTransferRespDTO().setId(id).setStatus(PayTransferStatusEnum.SUCCESS.getStatus()) + .setPrice(10).setMerchantTransferId("1").setChannelCode(PayChannelEnum.WALLET.getCode()); + } + +}