From f92a5899f3bb9e7ee53cc002a10909467b31f5e8 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 21 Jul 2023 21:50:05 +0800 Subject: [PATCH 01/16] =?UTF-8?q?mall=20+=20pay=EF=BC=9A=201.=20=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98=E7=9A=84=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-ui-admin/src/utils/constants.js | 8 - .../pay/app/components/alipayChannelForm.vue | 9 +- .../pay/app/components/wechatChannelForm.vue | 328 ------------------ .../pay/app/components/weixinChannelForm.vue | 257 ++++++++++++++ yudao-ui-admin/src/views/pay/app/index.vue | 167 ++++----- 5 files changed, 318 insertions(+), 451 deletions(-) delete mode 100644 yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue create mode 100644 yudao-ui-admin/src/views/pay/app/components/weixinChannelForm.vue diff --git a/yudao-ui-admin/src/utils/constants.js b/yudao-ui-admin/src/utils/constants.js index 70e0f5a02..a4805096d 100644 --- a/yudao-ui-admin/src/utils/constants.js +++ b/yudao-ui-admin/src/utils/constants.js @@ -180,14 +180,6 @@ export const PayDisplayModeEnum = { } } -/** - * 支付类型枚举 - */ -export const PayType = { - WECHAT: "WECHAT", - ALIPAY: "ALIPAY" -} - /** * 支付订单状态枚举 */ diff --git a/yudao-ui-admin/src/views/pay/app/components/alipayChannelForm.vue b/yudao-ui-admin/src/views/pay/app/components/alipayChannelForm.vue index 1043c414a..cb67b9d76 100644 --- a/yudao-ui-admin/src/views/pay/app/components/alipayChannelForm.vue +++ b/yudao-ui-admin/src/views/pay/app/components/alipayChannelForm.vue @@ -172,7 +172,7 @@ export default { this.formData = response.data; this.formData.config = JSON.parse(response.data.config); } - this.title = this.formData.id ? '创建支付渠道' : '编辑支付渠道' + this.title = !this.formData.id ? '创建支付渠道' : '编辑支付渠道' }).finally(() => { this.formLoading = false; }); @@ -257,12 +257,7 @@ export default { this.formData.config.rootCertContent = e.target.result } readFile.readAsText(event.file); - }, - + } } } - - diff --git a/yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue b/yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue deleted file mode 100644 index c3e691f9f..000000000 --- a/yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue +++ /dev/null @@ -1,328 +0,0 @@ - - - diff --git a/yudao-ui-admin/src/views/pay/app/components/weixinChannelForm.vue b/yudao-ui-admin/src/views/pay/app/components/weixinChannelForm.vue new file mode 100644 index 000000000..00e0773b5 --- /dev/null +++ b/yudao-ui-admin/src/views/pay/app/components/weixinChannelForm.vue @@ -0,0 +1,257 @@ + + diff --git a/yudao-ui-admin/src/views/pay/app/index.vue b/yudao-ui-admin/src/views/pay/app/index.vue index 3a038f8be..be92e0dae 100644 --- a/yudao-ui-admin/src/views/pay/app/index.vue +++ b/yudao-ui-admin/src/views/pay/app/index.vue @@ -9,7 +9,7 @@ - @@ -48,60 +48,55 @@ @@ -110,60 +105,55 @@ @@ -191,7 +181,7 @@ - + {{ dict.label }} @@ -214,24 +204,23 @@ - - + + From e27ec2fd50146abca9c5604e9f1c197e22d22341 Mon Sep 17 00:00:00 2001 From: "zhijiantianya@gmail.com" Date: Fri, 21 Jul 2023 22:02:39 +0800 Subject: [PATCH 02/16] =?UTF-8?q?by=20gateway:=201.=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E9=83=A8=E5=88=86=20refund=20=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/refund/PayRefundServiceImpl.java | 109 ++++---- .../service/order/PayOrderServiceTest.java | 244 +++++++++++++++++- .../service/refund/PayRefundServiceTest.java | 239 ++++++++++++++--- .../src/test/resources/sql/create_tables.sql | 16 +- 4 files changed, 492 insertions(+), 116 deletions(-) diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java index 41f800393..ceaab0a43 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.pay.service.refund; -import cn.hutool.core.date.DateUtil; -import cn.hutool.core.util.RandomUtil; +import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.pay.core.client.PayClient; import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; @@ -16,9 +15,9 @@ 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.channel.PayChannelDO; import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; -import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO; import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper; +import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO; import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants; import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; @@ -35,12 +34,11 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; -import java.time.LocalDateTime; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; -import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.CHANNEL_NOT_FOUND; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; /** * 退款订单 Service 实现类 @@ -60,6 +58,8 @@ public class PayRefundServiceImpl implements PayRefundService { @Resource private PayRefundMapper refundMapper; + @Resource + private PayNoRedisDAO noRedisDAO; @Resource private PayOrderService orderService; @@ -112,8 +112,9 @@ public class PayRefundServiceImpl implements PayRefundService { } // 2.1 插入退款单 + String no = noRedisDAO.generate(payProperties.getRefundNoPrefix()); refund = PayRefundConvert.INSTANCE.convert(reqDTO) - .setNo(generateRefundNo()).setOrderId(order.getId()) + .setNo(no).setOrderId(order.getId()) .setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode()) // 商户相关的字段 .setNotifyUrl(app.getRefundNotifyUrl()) @@ -123,20 +124,27 @@ public class PayRefundServiceImpl implements PayRefundService { .setStatus(PayRefundStatusEnum.WAITING.getStatus()) .setPayPrice(order.getPrice()).setRefundPrice(reqDTO.getPrice()); refundMapper.insert(refund); - // 2.2 向渠道发起退款申请 - PayOrderExtensionDO orderExtension = orderService.getOrderExtension(order.getExtensionId()); - PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO() - .setPayPrice(order.getPrice()) - .setRefundPrice(reqDTO.getPrice()) - .setOutTradeNo(orderExtension.getNo()) - .setOutRefundNo(refund.getNo()) - .setNotifyUrl(genChannelRefundNotifyUrl(channel)) - .setReason(reqDTO.getReason()); - PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); - // 2.3 处理退款返回 - notifyRefund(channel, refundRespDTO); + try { + // 2.2 向渠道发起退款申请 + PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO() + .setPayPrice(order.getPrice()) + .setRefundPrice(reqDTO.getPrice()) + .setOutTradeNo(order.getNo()) + .setOutRefundNo(refund.getNo()) + .setNotifyUrl(genChannelRefundNotifyUrl(channel)) + .setReason(reqDTO.getReason()); + PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); + // 2.3 处理退款返回 + getSelf().notifyRefund(channel, refundRespDTO); + } catch (Throwable e) { + // 注意:这里仅打印异常,不进行抛出。 + // 原因是:虽然调用支付渠道进行退款发生异常(网络请求超时),实际退款成功。这个结果,后续通过退款回调、或者退款轮询补偿可以拿到。 + // 最终,在异常的情况下,支付中心会异步回调业务的退款回调接口,提供退款结果 + log.error("[createPayRefund][退款 id({}) requestDTO({}) 发生异常]", + refund.getId(), reqDTO, e); + } - // 成功在 退款回调中处理 + // 返回退款编号 return refund.getId(); } @@ -151,12 +159,12 @@ public class PayRefundServiceImpl implements PayRefundService { if (order == null) { throw exception(ErrorCodeConstants.ORDER_NOT_FOUND); } - // 校验状态,必须是支付状态 - if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) { - throw exception(ErrorCodeConstants.ORDER_STATUS_IS_NOT_SUCCESS); + // 校验状态,必须是已支付、或者已退款 + if (!PayOrderStatusEnum.isSuccessOrRefund(order.getStatus())) { + throw exception(ORDER_REFUND_FAIL_STATUS_ERROR); } - // 校验金额 退款金额不能大于原定的金额 + // 校验金额,退款金额不能大于原定的金额 if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){ throw exception(ErrorCodeConstants.REFUND_PRICE_EXCEED); } @@ -178,38 +186,22 @@ public class PayRefundServiceImpl implements PayRefundService { return payProperties.getRefundNotifyUrl() + "/" + channel.getId(); } - private String generateRefundNo() { -// wx -// 2014 -// 10 -// 27 -// 20 -// 09 -// 39 -// 5522657 -// a690389285100 - // 目前的算法 - // 时间序列,年月日时分秒 14 位 - // 纯随机,6 位 TODO 芋艿:此处估计是会有问题的,后续在调整 - return DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss") + // 时间序列 - RandomUtil.randomInt(100000, 999999) // 随机。为什么是这个范围,因为偷懒 - ; - } - @Override public void notifyRefund(Long channelId, PayRefundRespDTO notify) { - // 校验支付渠道是否有效 - channelService.validPayChannel(channelId); - // 通知结果 - // 校验支付渠道是否有效 PayChannelDO channel = channelService.validPayChannel(channelId); // 更新退款订单 - TenantUtils.execute(channel.getTenantId(), () -> notifyRefund(channel, notify)); + TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyRefund(channel, notify)); } - // TODO 芋艿:事务问题 - private void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) { + /** + * 通知并更新订单的退款结果 + * + * @param channel 支付渠道 + * @param notify 通知 + */ + @Transactional(rollbackFor = Exception.class) // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效 + public void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) { // 情况一:退款成功 if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) { notifyRefundSuccess(channel, notify); @@ -226,14 +218,14 @@ public class PayRefundServiceImpl implements PayRefundService { PayRefundDO refund = refundMapper.selectByAppIdAndNo( channel.getAppId(), notify.getOutRefundNo()); if (refund == null) { - throw exception(ErrorCodeConstants.REFUND_NOT_FOUND); + throw exception(REFUND_NOT_FOUND); } if (PayRefundStatusEnum.isSuccess(refund.getStatus())) { // 如果已经是成功,直接返回,不用重复更新 log.info("[notifyRefundSuccess][退款订单({}) 已经是退款成功,无需更新]", refund.getId()); return; } if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) { - throw exception(ErrorCodeConstants.REFUND_STATUS_IS_NOT_WAITING); + throw exception(REFUND_STATUS_IS_NOT_WAITING); } // 1.2 更新 PayRefundDO PayRefundDO updateRefundObj = new PayRefundDO() @@ -243,7 +235,7 @@ public class PayRefundServiceImpl implements PayRefundService { .setChannelNotifyData(toJsonString(notify)); int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj); if (updateCounts == 0) { // 校验状态,必须是等待状态 - throw exception(ErrorCodeConstants.REFUND_STATUS_IS_NOT_WAITING); + throw exception(REFUND_STATUS_IS_NOT_WAITING); } log.info("[notifyRefundSuccess][退款订单({}) 更新为退款成功]", refund.getId()); @@ -261,14 +253,14 @@ public class PayRefundServiceImpl implements PayRefundService { PayRefundDO refund = refundMapper.selectByAppIdAndNo( channel.getAppId(), notify.getOutRefundNo()); if (refund == null) { - throw exception(ErrorCodeConstants.REFUND_NOT_FOUND); + throw exception(REFUND_NOT_FOUND); } if (PayRefundStatusEnum.isFailure(refund.getStatus())) { // 如果已经是成功,直接返回,不用重复更新 log.info("[notifyRefundSuccess][退款订单({}) 已经是退款关闭,无需更新]", refund.getId()); return; } if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) { - throw exception(ErrorCodeConstants.REFUND_STATUS_IS_NOT_WAITING); + throw exception(REFUND_STATUS_IS_NOT_WAITING); } // 1.2 更新 PayRefundDO PayRefundDO updateRefundObj = new PayRefundDO() @@ -278,7 +270,7 @@ public class PayRefundServiceImpl implements PayRefundService { .setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg()); int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj); if (updateCounts == 0) { // 校验状态,必须是等待状态 - throw exception(ErrorCodeConstants.REFUND_STATUS_IS_NOT_WAITING); + throw exception(REFUND_STATUS_IS_NOT_WAITING); } log.info("[notifyRefundFailure][退款订单({}) 更新为退款失败]", refund.getId()); @@ -287,4 +279,13 @@ public class PayRefundServiceImpl implements PayRefundService { .type(PayNotifyTypeEnum.REFUND.getType()).dataId(refund.getId()).build()); } + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PayRefundServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java index e81568480..35bc7a835 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java @@ -30,7 +30,6 @@ 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.ArgumentMatcher; import org.mockito.MockedStatic; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; @@ -44,8 +43,7 @@ import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -85,6 +83,51 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest { when(properties.getOrderNotifyUrl()).thenReturn("http://127.0.0.1"); } + @Test + public void testGetOrder_id() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + + // 调用 + PayOrderDO dbOrder = orderService.getOrder(id); + // 断言 + assertPojoEquals(dbOrder, order); + } + + @Test + public void testGetOrder_appIdAndMerchantOrderId() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class); + orderMapper.insert(order); + // 准备参数 + Long appId = order.getAppId(); + String merchantOrderId = order.getMerchantOrderId(); + + // 调用 + PayOrderDO dbOrder = orderService.getOrder(appId, merchantOrderId); + // 断言 + assertPojoEquals(dbOrder, order); + } + + @Test + public void testGetOrderCountByAppId() { + // mock 数据(PayOrderDO) + PayOrderDO order01 = randomPojo(PayOrderDO.class); + orderMapper.insert(order01); + PayOrderDO order02 = randomPojo(PayOrderDO.class); + orderMapper.insert(order02); + // 准备参数 + Long appId = order01.getAppId(); + + // 调用 + Long count = orderService.getOrderCountByAppId(appId); + // 断言 + assertEquals(count, 1L); + } + @Test public void testGetOrderPage() { // mock 数据 @@ -350,7 +393,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest { // mock 方法(client) PayClient client = mock(PayClient.class); when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); - // mock 方法() + // mock 方法(支付渠道的调用) PayOrderRespDTO unifiedOrderResp = randomPojo(PayOrderRespDTO.class, o -> o.setChannelErrorCode(null).setChannelErrorMsg(null) .setDisplayMode(PayOrderDisplayModeEnum.URL.getMode()).setDisplayContent("tudou")); when(client.unifiedOrder(argThat(payOrderUnifiedReqDTO -> { @@ -553,14 +596,193 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest { assertPojoEquals(order, orderMapper.selectOne(null), "updateTime", "updater"); // 断言,调用 - verify(notifyService).createPayNotifyTask(argThat(new ArgumentMatcher() { - @Override - public boolean matches(PayNotifyTaskCreateReqDTO reqDTO) { - assertEquals(reqDTO.getType(), PayNotifyTypeEnum.ORDER.getType()); - assertEquals(reqDTO.getDataId(), orderExtension.getOrderId()); - return true; - } + verify(notifyService).createPayNotifyTask(argThat(reqDTO -> { + assertEquals(reqDTO.getType(), PayNotifyTypeEnum.ORDER.getType()); + assertEquals(reqDTO.getDataId(), orderExtension.getOrderId()); + return true; })); } + @Test + public void testNotifyOrderClosed_orderExtension_notFound() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus())); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + ORDER_EXTENSION_NOT_FOUND); + } + + @Test + public void testNotifyOrderClosed_orderExtension_closed() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 CLOSED + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testNotifyOrderClosed_orderExtension_paid() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testNotifyOrderClosed_orderExtension_refund() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + + @Test + public void testNotifyOrderClosed_orderExtension_waiting() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO + orderExtension.setStatus(PayOrderStatusEnum.CLOSED.getStatus()).setChannelNotifyData(toJsonString(notify)) + .setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg()); + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null), + "updateTime", "updater"); + } + + @Test + public void testUpdateOrderRefundPrice_notFound() { + // 准备参数 + Long id = randomLongId(); + Integer incrRefundPrice = randomInteger(); + + // 调用,并断言异常 + assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice), + ORDER_NOT_FOUND); + } + + @Test + public void testUpdateOrderRefundPrice_waiting() { + testUpdateOrderRefundPrice_waitingOrClosed(PayOrderStatusEnum.WAITING.getStatus()); + } + + @Test + public void testUpdateOrderRefundPrice_closed() { + testUpdateOrderRefundPrice_waitingOrClosed(PayOrderStatusEnum.CLOSED.getStatus()); + } + + private void testUpdateOrderRefundPrice_waitingOrClosed(Integer status) { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(status)); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + Integer incrRefundPrice = randomInteger(); + + // 调用,并断言异常 + assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice), + ORDER_REFUND_FAIL_STATUS_ERROR); + } + + @Test + public void testUpdateOrderRefundPrice_priceExceed() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setRefundPrice(1).setPrice(10)); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + Integer incrRefundPrice = 10; + + // 调用,并断言异常 + assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice), + REFUND_PRICE_EXCEED); + } + + @Test + public void testUpdateOrderRefundPrice_refund() { + testUpdateOrderRefundPrice_refundOrSuccess(PayOrderStatusEnum.REFUND.getStatus()); + } + + @Test + public void testUpdateOrderRefundPrice_success() { + testUpdateOrderRefundPrice_refundOrSuccess(PayOrderStatusEnum.SUCCESS.getStatus()); + } + + private void testUpdateOrderRefundPrice_refundOrSuccess(Integer status) { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(status).setRefundPrice(1).setPrice(10)); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + Integer incrRefundPrice = 8; + + // 调用 + orderService.updateOrderRefundPrice(id, incrRefundPrice); + // 断言 + order.setRefundPrice(9).setStatus(PayOrderStatusEnum.REFUND.getStatus()); + assertPojoEquals(order, orderMapper.selectOne(null), + "updateTime", "updater"); + } + + @Test + public void testGetOrderExtension() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + Long id = orderExtension.getId(); + + // 调用 + PayOrderExtensionDO dbOrderExtension = orderService.getOrderExtension(id); + // 断言 + assertPojoEquals(dbOrderExtension, orderExtension); + } + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java index 53f3cab26..fa01d6698 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java @@ -1,14 +1,19 @@ package cn.iocoder.yudao.module.pay.service.refund; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; -import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; +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.order.PayOrderDO; import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper; +import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO; import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties; @@ -24,12 +29,25 @@ import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.List; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime; import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -@Import(PayRefundServiceImpl.class) +/** + * {@link PayRefundServiceImpl} 的单元测试类 + * + * @author 芋艿 + */ +@Import({PayRefundServiceImpl.class, PayNoRedisDAO.class}) public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { @Resource @@ -56,45 +74,41 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { // mock 数据 PayRefundDO dbRefund = randomPojo(PayRefundDO.class, o -> { // 等会查询到 o.setAppId(1L); - o.setChannelId(1L); o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); - o.setOrderId(1L); - o.setNo("OT0000001"); o.setMerchantOrderId("MOT0000001"); o.setMerchantRefundId("MRF0000001"); - o.setNotifyUrl("https://www.cancanzi.com"); o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); - o.setPayPrice(100); - o.setRefundPrice(500); - o.setReason("就是想退款了,你有意见吗"); - o.setUserIp("127.0.0.1"); o.setChannelOrderNo("CH0000001"); o.setChannelRefundNo("CHR0000001"); - o.setChannelErrorCode(""); - o.setChannelErrorMsg(""); - o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15)); - o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10)); - o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35)); + o.setCreateTime(buildTime(2021, 1, 10)); }); refundMapper.insert(dbRefund); // 测试 appId 不匹配 refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setAppId(2L))); // 测试 channelCode 不匹配 refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); - // 测试 merchantRefundNo 不匹配 - refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId("MRF1111112"))); + // 测试 merchantOrderId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantOrderId(randomString()))); + // 测试 merchantRefundId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId(randomString()))); + // 测试 channelOrderNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelOrderNo(randomString()))); + // 测试 channelRefundNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelRefundNo(randomString()))); // 测试 status 不匹配 refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()))); // 测试 createTime 不匹配 - refundMapper.insert(cloneIgnoreId(dbRefund, o -> - o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10)))); + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setCreateTime(buildTime(2021, 1, 1)))); // 准备参数 PayRefundPageReqVO reqVO = new PayRefundPageReqVO(); reqVO.setAppId(1L); reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqVO.setMerchantOrderId("MOT0000001"); reqVO.setMerchantRefundId("MRF0000001"); - reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()); - reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2021, 1, 1, 10, 10, 10), LocalDateTime.of(2021, 1, 1, 10, 10, 12)})); + reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + reqVO.setChannelOrderNo("CH0000001"); + reqVO.setChannelRefundNo("CHR0000001"); + reqVO.setCreateTime(buildBetweenTime(2021, 1, 9, 2021, 1, 11)); // 调用 PageResult pageResult = refundService.getRefundPage(reqVO); @@ -109,45 +123,41 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { // mock 数据 PayRefundDO dbRefund = randomPojo(PayRefundDO.class, o -> { // 等会查询到 o.setAppId(1L); - o.setChannelId(1L); o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); - o.setOrderId(1L); - o.setNo("OT0000001"); o.setMerchantOrderId("MOT0000001"); o.setMerchantRefundId("MRF0000001"); - o.setNotifyUrl("https://www.cancanzi.com"); o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); - o.setPayPrice(100); - o.setRefundPrice(500); - o.setReason("就是想退款了,你有意见吗"); - o.setUserIp("127.0.0.1"); o.setChannelOrderNo("CH0000001"); o.setChannelRefundNo("CHR0000001"); - o.setChannelErrorCode(""); - o.setChannelErrorMsg(""); - o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15)); - o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10)); - o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35)); + o.setCreateTime(buildTime(2021, 1, 10)); }); refundMapper.insert(dbRefund); // 测试 appId 不匹配 refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setAppId(2L))); // 测试 channelCode 不匹配 refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); - // 测试 merchantRefundNo 不匹配 - refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId("MRF1111112"))); + // 测试 merchantOrderId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantOrderId(randomString()))); + // 测试 merchantRefundId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId(randomString()))); + // 测试 channelOrderNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelOrderNo(randomString()))); + // 测试 channelRefundNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelRefundNo(randomString()))); // 测试 status 不匹配 refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()))); // 测试 createTime 不匹配 - refundMapper.insert(cloneIgnoreId(dbRefund, o -> - o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10)))); - + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setCreateTime(buildTime(2021, 1, 1)))); // 准备参数 PayRefundExportReqVO reqVO = new PayRefundExportReqVO(); reqVO.setAppId(1L); reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); - reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()); - reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2021, 1, 1, 10, 10, 10), LocalDateTime.of(2021, 1, 1, 10, 10, 12)})); + reqVO.setMerchantOrderId("MOT0000001"); + reqVO.setMerchantRefundId("MRF0000001"); + reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + reqVO.setChannelOrderNo("CH0000001"); + reqVO.setChannelRefundNo("CHR0000001"); + reqVO.setCreateTime(buildBetweenTime(2021, 1, 9, 2021, 1, 11)); // 调用 List list = refundService.getRefundList(reqVO); @@ -156,4 +166,151 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { assertPojoEquals(dbRefund, list.get(0)); } + @Test + public void testCreateRefund_orderNotFound() { + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + ORDER_NOT_FOUND); + } + + @Test + public void testCreateRefund_orderWaiting() { + testCreateRefund_orderWaitingOrClosed(PayOrderStatusEnum.WAITING.getStatus()); + } + + @Test + public void testCreateRefund_orderClosed() { + testCreateRefund_orderWaitingOrClosed(PayOrderStatusEnum.CLOSED.getStatus()); + } + + private void testCreateRefund_orderWaitingOrClosed(Integer status) { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(status)); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + ORDER_REFUND_FAIL_STATUS_ERROR); + } + + @Test + public void testCreateRefund_refundPriceExceed() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(10)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1)); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + REFUND_PRICE_EXCEED); + } + + @Test + public void testCreateRefund_orderHasRefunding() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(10)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1)); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 数据(refund 在退款中) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> + o.setOrderId(order.getId()).setStatus(PayOrderStatusEnum.WAITING.getStatus())); + refundMapper.insert(refund); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + REFUND_PRICE_EXCEED); + } + + @Test + public void testCreateRefund_channelNotFound() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1) + .setChannelId(1L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L))).thenReturn(channel); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + CHANNEL_NOT_FOUND); + } + + @Test + public void testCreateRefund_refundExists() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9) + .setReason("测试退款")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1) + .setChannelId(1L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L))).thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 数据(refund 已存在) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> + o.setOrderId(order.getId()).setStatus(PayOrderStatusEnum.WAITING.getStatus())); + refundMapper.insert(refund); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + REFUND_EXISTS); + } + + @Test + public void testCreateRefund_invokeException() { + + } + + @Test + public void testCreateRefund_invokeSuccess() { + + } + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql b/yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql index a748b2340..7d7d2435c 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql @@ -82,29 +82,25 @@ CREATE TABLE IF NOT EXISTS `pay_order_extension` ( CREATE TABLE IF NOT EXISTS `pay_refund` ( "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `no` varchar(64) NOT NULL, `app_id` bigint(20) NOT NULL, `channel_id` bigint(20) NOT NULL, `channel_code` varchar(32) NOT NULL, `order_id` bigint(20) NOT NULL, - `trade_no` varchar(64) NOT NULL, `merchant_order_id` varchar(64) NOT NULL, - `merchant_refund_no` varchar(64) NOT NULL, + `merchant_refund_id` varchar(64) NOT NULL, `notify_url` varchar(1024) NOT NULL, - `notify_status` tinyint(4) NOT NULL, `status` tinyint(4) NOT NULL, - `type` tinyint(4) NOT NULL, - `pay_amount` bigint(20) NOT NULL, - `refund_amount` bigint(20) NOT NULL, + `pay_price` bigint(20) NOT NULL, + `refund_price` bigint(20) NOT NULL, `reason` varchar(256) NOT NULL, `user_ip` varchar(50) NULL DEFAULT NULL, `channel_order_no` varchar(64) NOT NULL, `channel_refund_no` varchar(64) NULL DEFAULT NULL, + `success_time` datetime(0) NULL DEFAULT NULL, `channel_error_code` varchar(128) NULL DEFAULT NULL, `channel_error_msg` varchar(256) NULL DEFAULT NULL, - `channel_extras` varchar(1024) NULL DEFAULT NULL, - `expire_time` datetime(0) NULL DEFAULT NULL, - `success_time` datetime(0) NULL DEFAULT NULL, - `notify_time` datetime(0) NULL DEFAULT NULL, + `channel_notify_data` varchar(1024) NULL, `creator` varchar(64) NULL DEFAULT '', `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, `updater` varchar(64) NULL DEFAULT '', From f46a0371640d4ceb14e361a2284c9b51455c2285 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 22 Jul 2023 11:19:42 +0800 Subject: [PATCH 03/16] =?UTF-8?q?mall=20+=20pay=EF=BC=9A=201.=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=94=AF=E4=BB=98=E5=AE=9D=20Client=20=E7=9A=84?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E8=AE=A2=E5=8D=95=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/pay/core/client/PayClient.java | 10 ++++- .../client/dto/order/PayOrderRespDTO.java | 44 ++++++++++--------- .../core/client/impl/AbstractPayClient.java | 14 ++++++ .../impl/alipay/AbstractAlipayPayClient.java | 42 +++++++++++++++--- .../impl/weixin/AbstractWxPayClient.java | 11 +++-- .../client/impl/weixin/WxBarPayClient.java | 2 +- 6 files changed, 91 insertions(+), 32 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java index 9362466a6..15ce53e95 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java @@ -27,7 +27,7 @@ public interface PayClient { * 调用支付渠道,统一下单 * * @param reqDTO 下单信息 - * @return 各支付渠道的返回结果 + * @return 支付订单信息 */ PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO); @@ -40,6 +40,14 @@ public interface PayClient { */ PayOrderRespDTO parseOrderNotify(Map params, String body); + /** + * 获得支付订单信息 + * + * @param outTradeNo 外部订单号 + * @return 支付订单信息 + */ + PayOrderRespDTO getOrder(String outTradeNo); + // ============ 退款相关 ========== /** diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java index 163ffce65..82050a6fc 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java @@ -94,38 +94,40 @@ public class PayOrderRespDTO { /** * 创建【SUCCESS】状态的订单返回 */ - public PayOrderRespDTO(String channelOrderNo, String channelUserId, LocalDateTime successTime, - String outTradeNo, Object rawData) { - this.status = PayOrderStatusRespEnum.SUCCESS.getStatus(); - this.channelOrderNo = channelOrderNo; - this.channelUserId = channelUserId; - this.successTime = successTime; + public static PayOrderRespDTO successOf(String channelOrderNo, String channelUserId, LocalDateTime successTime, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = PayOrderStatusRespEnum.SUCCESS.getStatus(); + respDTO.channelOrderNo = channelOrderNo; + respDTO.channelUserId = channelUserId; + respDTO.successTime = successTime; // 相对通用的字段 - this.outTradeNo = outTradeNo; - this.rawData = rawData; + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; } /** - * 创建【SUCCESS】或【CLOSED】状态的订单返回,适合支付渠道回调时 + * 创建指定状态的订单返回,适合支付渠道回调时 */ - public PayOrderRespDTO(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime, - String outTradeNo, Object rawData) { - this.status = status; - this.channelOrderNo = channelOrderNo; - this.channelUserId = channelUserId; - this.successTime = successTime; + public static PayOrderRespDTO of(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = status; + respDTO.channelOrderNo = channelOrderNo; + respDTO.channelUserId = channelUserId; + respDTO.successTime = successTime; // 相对通用的字段 - this.outTradeNo = outTradeNo; - this.rawData = rawData; + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; } /** * 创建【CLOSED】状态的订单返回,适合调用支付渠道失败时 - * - * 参数和 {@link #PayOrderRespDTO(String, String, String, Object)} 冲突,所以独立个方法出来 */ - public static PayOrderRespDTO build(String channelErrorCode, String channelErrorMsg, - String outTradeNo, Object rawData) { + public static PayOrderRespDTO closedOf(String channelErrorCode, String channelErrorMsg, + String outTradeNo, Object rawData) { PayOrderRespDTO respDTO = new PayOrderRespDTO(); respDTO.status = PayOrderStatusRespEnum.CLOSED.getStatus(); respDTO.channelErrorCode = channelErrorCode; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java index 194432738..bd4580e2d 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java @@ -107,6 +107,20 @@ public abstract class AbstractPayClient implemen protected abstract PayOrderRespDTO doParseOrderNotify(Map params, String body) throws Throwable; + @Override + public PayOrderRespDTO getOrder(String outTradeNo) { + try { + return doGetOrder(outTradeNo); + } catch (Throwable ex) { + log.error("[getOrder][客户端({}) outTradeNo({}) 查询支付单异常]", + getId(), outTradeNo, ex); + throw buildPayException(ex); + } + } + + protected abstract PayOrderRespDTO doGetOrder(String outTradeNo) + throws Throwable; + // ============ 退款相关 ========== @Override diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java index 8c4a66b11..44dea94ce 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java @@ -17,9 +17,12 @@ import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayConfig; import com.alipay.api.AlipayResponse; import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.domain.AlipayTradeQueryModel; import com.alipay.api.domain.AlipayTradeRefundModel; import com.alipay.api.internal.util.AlipaySignature; +import com.alipay.api.request.AlipayTradeQueryRequest; import com.alipay.api.request.AlipayTradeRefundRequest; +import com.alipay.api.response.AlipayTradeQueryResponse; import com.alipay.api.response.AlipayTradeRefundResponse; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -63,7 +66,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient 0) { status = PayOrderStatusRespEnum.REFUND.getStatus(); @@ -87,10 +87,40 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient) () -> { throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body)); }); - return new PayOrderRespDTO(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")), + return PayOrderRespDTO.of(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")), bodyObj.get("out_trade_no"), body); } + @Override + protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable { + // 1.1 构建 AlipayTradeRefundModel 请求 + AlipayTradeQueryModel model = new AlipayTradeQueryModel(); + model.setOutTradeNo(outTradeNo); + // 1.2 构建 AlipayTradeQueryRequest 请求 + AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); + request.setBizModel(model); + + // 2.1 执行请求 + AlipayTradeQueryResponse response = client.execute(request); + if (!response.isSuccess()) { + return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + outTradeNo, response); + } + // 2.2 解析订单的状态 + Integer status = parseStatus(response.getTradeStatus()); + Assert.notNull(status, (Supplier) () -> { + throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody())); + }); + return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()), + outTradeNo, response); + } + + private Integer parseStatus(String tradeStatus) { + return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus() + : ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusRespEnum.SUCCESS.getStatus() + : Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null; + } + // ============ 退款相关 ========== /** diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java index eeef301c0..e15b06a5f 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java @@ -90,7 +90,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient Date: Sat, 22 Jul 2023 12:07:18 +0800 Subject: [PATCH 04/16] =?UTF-8?q?mall=20+=20pay=EF=BC=9A=201.=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98=20Client=20?= =?UTF-8?q?=E7=9A=84=E6=9F=A5=E8=AF=A2=E8=AE=A2=E5=8D=95=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/alipay/AbstractAlipayPayClient.java | 2 +- .../impl/weixin/AbstractWxPayClient.java | 65 +++++++++++++++++-- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java index 44dea94ce..f5c072a2d 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java @@ -115,7 +115,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient params, String body) throws WxPayException { - // 微信支付 v2 回调结果处理 switch (config.getApiVersion()) { case API_VERSION_V2: return doParseOrderNotifyV2(body); @@ -130,7 +133,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient Date: Sat, 22 Jul 2023 13:19:22 +0800 Subject: [PATCH 05/16] =?UTF-8?q?mall=20+=20pay=EF=BC=9A=201.=20=E5=8F=91?= =?UTF-8?q?=E8=B5=B7=E6=94=AF=E4=BB=98=E6=97=B6=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=AE=9E=E9=99=85=E5=B7=B2=E6=94=AF=E4=BB=98=E7=9A=84=E4=BA=8C?= =?UTF-8?q?=E6=AC=A1=E6=A0=A1=E9=AA=8C=EF=BC=8C=E9=81=BF=E5=85=8D=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E6=94=AF=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/pay/enums/ErrorCodeConstants.java | 3 +- .../admin/refund/PayRefundController.java | 3 - .../mysql/order/PayOrderExtensionMapper.java | 6 ++ .../service/order/PayOrderServiceImpl.java | 46 +++++++++++-- .../service/order/PayOrderServiceTest.java | 66 ++++++++++++++++++- yudao-ui-admin/src/views/pay/app/index.vue | 1 - .../src/views/pay/cashier/index.vue | 1 - 7 files changed, 114 insertions(+), 12 deletions(-) diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java index 7cc1b815c..8e2f935fc 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java @@ -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, "退款金额超过订单可退款金额"); diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/PayRefundController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/PayRefundController.java index 24e2b22e5..df131ddd3 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/PayRefundController.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/PayRefundController.java @@ -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 = "获得退款订单") diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderExtensionMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderExtensionMapper.java index a8918f441..145818417 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderExtensionMapper.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderExtensionMapper.java @@ -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 { @@ -17,4 +19,8 @@ public interface PayOrderExtensionMapper extends BaseMapperX selectListByOrderId(Long orderId) { + return selectList(PayOrderExtensionDO::getOrderId, orderId); + } + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java index 64ecaff94..ee9bcc0aa 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java @@ -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 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); // 校验支付渠道是否有效 diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java index 35bc7a835..06088eb35 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java @@ -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); diff --git a/yudao-ui-admin/src/views/pay/app/index.vue b/yudao-ui-admin/src/views/pay/app/index.vue index be92e0dae..8c83856cb 100644 --- a/yudao-ui-admin/src/views/pay/app/index.vue +++ b/yudao-ui-admin/src/views/pay/app/index.vue @@ -43,7 +43,6 @@ @change="handleStatusChange(scope.row)"/> -