diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index 9cbeccf..233c3f9 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -13,6 +13,7 @@ import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.member.api.address.MemberAddressApi; import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO; import cn.iocoder.yudao.module.pay.api.order.PayOrderApi; @@ -68,6 +69,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import java.time.LocalDateTime; import java.util.ArrayList; @@ -639,6 +642,49 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { // 3. 增加订单日志 TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), TradeOrderStatusEnum.CANCELED.getStatus()); + + // 4. 关闭支付单 + closePayOrder(order.getId(), order.getPayOrderId(), cancelType); + } + + /** + * 注册事务提交后的支付单关闭任务 + * + * @param orderId 交易订单编号 + * @param payOrderId 支付订单编号 + * @param cancelType 订单取消类型 + */ + private void closePayOrder(Long orderId, Long payOrderId, TradeOrderCancelTypeEnum cancelType) { + if (!ObjectUtils.equalsAny(cancelType, TradeOrderCancelTypeEnum.MEMBER_CANCEL, + TradeOrderCancelTypeEnum.PAY_TIMEOUT) + || payOrderId == null || !TransactionSynchronizationManager.isSynchronizationActive()) { + return; + } + // 必须在事务提交后,再关闭支付单,避免交易事务回滚后支付单已经关闭 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + // 异步的原因:避免阻塞当前事务,无需等待结果 + getSelf().closePayOrderAsync(orderId, payOrderId); + } + + }); + } + + /** + * 异步关闭订单关联的支付单 + * + * @param orderId 交易订单编号 + * @param payOrderId 支付订单编号 + */ + @Async + public void closePayOrderAsync(Long orderId, Long payOrderId) { + try { + payOrderApi.closeOrder(payOrderId); + } catch (Throwable e) { + log.error("[closePayOrderAsync][order({}) payOrder({}) 异步关闭支付单失败]", orderId, payOrderId, e); + } } /** diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApi.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApi.java index 965d79a..502463e 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApi.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApi.java @@ -30,6 +30,13 @@ public interface PayOrderApi { PayOrderRespDTO getOrder(Long id); /** + * 关闭支付单 + * + * @param id 支付单编号 + */ + void closeOrder(Long id); + + /** * 更新支付订单价格 * * @param id 支付单编号 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java index 3584f47..9d6a26b 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java @@ -32,6 +32,11 @@ public class PayOrderApiImpl implements PayOrderApi { } @Override + public void closeOrder(Long id) { + payOrderService.closeOrder(id); + } + + @Override public void updatePayOrderPrice(Long id, Integer payPrice) { payOrderService.updatePayOrderPrice(id, payPrice); } diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/PayClient.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/PayClient.java index 4748c40..59d6631 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/PayClient.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/PayClient.java @@ -58,6 +58,14 @@ public interface PayClient { */ PayOrderRespDTO getOrder(String outTradeNo); + /** + * 关闭支付订单 + * + * @param outTradeNo 外部订单号 + * @return 关闭结果 + */ + PayOrderRespDTO closeOrder(String outTradeNo); + // ============ 退款相关 ========== /** diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/AbstractPayClient.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/AbstractPayClient.java index fce355e..fc321fa 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/AbstractPayClient.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/AbstractPayClient.java @@ -134,6 +134,23 @@ public abstract class AbstractPayClient implemen protected abstract PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable; + @Override + public final PayOrderRespDTO closeOrder(String outTradeNo) { + try { + return doCloseOrder(outTradeNo); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + // 系统异常,则包装成 PayException 异常抛出 + log.error("[closeOrder][客户端({}) outTradeNo({}) 关闭支付单异常]", + getId(), outTradeNo, ex); + throw buildPayException(ex); + } + } + + protected abstract PayOrderRespDTO doCloseOrder(String outTradeNo) + throws Throwable; + // ============ 退款相关 ========== @Override diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java index 21a2198..b5d3b4b 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java @@ -51,6 +51,11 @@ import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay. @Slf4j public abstract class AbstractAlipayPayClient extends AbstractPayClient { + /** + * 支付宝接口调用成功的响应码 + */ + protected static final String ALIPAY_SUCCESS_CODE = "10000"; + @Getter // 仅用于单测场景 protected DefaultAlipayClient client; @@ -117,9 +122,13 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient { } @Override + protected PayOrderRespDTO doCloseOrder(String outTradeNo) { + return PayOrderRespDTO.closedOf(null, null, outTradeNo, MOCK_RESP_SUCCESS_DATA); + } + + @Override protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { return PayRefundRespDTO.successOf("MOCK-R-" + reqDTO.getOutRefundNo(), LocalDateTime.now(), reqDTO.getOutRefundNo(), MOCK_RESP_SUCCESS_DATA); diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/wallet/WalletPayClient.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/wallet/WalletPayClient.java index 8021dc7..ae1e5be 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/wallet/WalletPayClient.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/wallet/WalletPayClient.java @@ -125,6 +125,27 @@ public class WalletPayClient extends AbstractPayClient { } @Override + protected PayOrderRespDTO doCloseOrder(String outTradeNo) { + if (orderService == null) { + orderService = SpringUtil.getBean(PayOrderService.class); + } + // 待支付状态:钱包支付为同步扣款,没有渠道侧的待支付单,所以以钱包流水判断是否已扣款 + // 补充说明:存在扣款成功,但是 notifyOrder 更新支付拓展单失败的情况,此时不能直接关闭 + PayOrderExtensionDO orderExtension = orderService.getOrderExtensionByNo(outTradeNo); + if (orderExtension != null && PayOrderStatusEnum.isWaiting(orderExtension.getStatus())) { + PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransaction( + String.valueOf(orderExtension.getOrderId()), PayWalletBizTypeEnum.PAYMENT); + if (walletTransaction == null) { + return PayOrderRespDTO.closedOf(null, null, outTradeNo, ""); + } + return PayOrderRespDTO.successOf(walletTransaction.getNo(), walletTransaction.getCreator(), + walletTransaction.getCreateTime(), outTradeNo, walletTransaction); + } + // 其它状态:以本地支付状态为准 + return doGetOrder(outTradeNo); + } + + @Override @SuppressWarnings("PatternVariableCanBeUsed") protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { try { diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java index 2c8bb82..48c2d10 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java @@ -216,6 +216,27 @@ public abstract class AbstractWxPayClient extends AbstractPayClient extensions = orderExtensionMapper.selectListByOrderId(id); + for (PayOrderExtensionDO extension : extensions) { + if (PayOrderStatusEnum.isClosed(extension.getStatus())) { + continue; + } + if (PayOrderStatusEnum.isSuccess(extension.getStatus())) { + log.error("[closeOrder][order({}) extension({}) 已支付,订单业务可能已取消,需要人工退款]", + id, extension.getId()); + return; + } + if (!PayOrderStatusEnum.isWaiting(extension.getStatus())) { + continue; + } + PayClient client = channelService.getPayClient(extension.getChannelId()); + if (client == null) { + log.error("[closeOrder][order({}) extension({}) 渠道({})不存在]", + id, extension.getId(), extension.getChannelId()); + return; + } + // 1.1 调用渠道关单,已支付结果由渠道客户端转换并保留 + PayOrderRespDTO respDTO = client.closeOrder(extension.getNo()); + if (PayOrderStatusEnum.isSuccess(respDTO.getStatus())) { + notifyOrder(extension.getChannelId(), respDTO); + log.error("[closeOrder][order({}) extension({}) 查询发现已支付]", id, extension.getId()); + return; + } + if (!PayOrderStatusEnum.isClosed(respDTO.getStatus())) { + log.warn("[closeOrder][order({}) extension({}) 渠道状态({}),暂不关闭本地支付单]", + id, extension.getId(), respDTO.getStatus()); + return; + } + // 1.2 更新 PayOrderExtensionDO 状态 + PayOrderExtensionDO updateObj = new PayOrderExtensionDO() + .setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setChannelErrorCode(respDTO.getChannelErrorCode()) + .setChannelErrorMsg(respDTO.getChannelErrorMsg()) + .setChannelNotifyData(toJsonString(respDTO)); + if (orderExtensionMapper.updateByIdAndStatus(extension.getId(), + PayOrderStatusEnum.WAITING.getStatus(), updateObj) == 0) { + log.error("[closeOrder][order({}) extension({}) 更新为支付关闭失败]", id, extension.getId()); + return; + } + } + // 2. 所有支付拓展单都关闭后,再关闭支付主单 + if (orderMapper.updateByIdAndStatus(id, PayOrderStatusEnum.WAITING.getStatus(), + new PayOrderDO().setStatus(PayOrderStatusEnum.CLOSED.getStatus())) == 0) { + log.error("[closeOrder][order({}) 更新为支付关闭失败]", id); + } + } catch (Throwable e) { + log.error("[closeOrder][order({}) 关闭支付单异常]", id, e); + } + } + /** * 获得自身的代理对象,解决 AOP 生效问题 * diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java index 62041f9..763924d 100644 --- a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java @@ -5,8 +5,12 @@ import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderR import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; import com.alipay.api.AlipayApiException; +import com.alipay.api.request.AlipayTradeCloseRequest; import com.alipay.api.request.AlipayTradePagePayRequest; +import com.alipay.api.request.AlipayTradeQueryRequest; +import com.alipay.api.response.AlipayTradeCloseResponse; import com.alipay.api.response.AlipayTradePagePayResponse; +import com.alipay.api.response.AlipayTradeQueryResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -18,6 +22,7 @@ import static cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum.WAITING import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @@ -128,4 +133,78 @@ public class AlipayPcPayClientTest extends AbstractAlipayClientTest { assertEquals(subMsg, resp.getChannelErrorMsg()); } + @Test + @DisplayName("支付宝 PC 网站支付:关单成功") + public void testCloseOrder_success() throws AlipayApiException { + // mock 方法 + AlipayTradeCloseResponse response = randomPojo(AlipayTradeCloseResponse.class, o -> { + o.setCode(AbstractAlipayPayClient.ALIPAY_SUCCESS_CODE); + o.setSubCode(""); + }); + when(defaultAlipayClient.execute(any(AlipayTradeCloseRequest.class))).thenReturn(response); + // 准备请求参数 + String outTradeNo = randomString(); + + // 调用 + PayOrderRespDTO resp = client.closeOrder(outTradeNo); + // 断言 + assertEquals(CLOSED.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertSame(response, resp.getRawData()); + } + + @Test + @DisplayName("支付宝 PC 网站支付:关单时交易不存在,查询后关闭") + public void testCloseOrder_tradeNotExist() throws AlipayApiException { + // mock 方法(关单) + AlipayTradeCloseResponse closeResponse = randomPojo(AlipayTradeCloseResponse.class, o -> { + o.setCode("40004"); + o.setSubCode("ACQ.TRADE_NOT_EXIST"); + }); + when(defaultAlipayClient.execute(any(AlipayTradeCloseRequest.class))).thenReturn(closeResponse); + // mock 方法(查询) + AlipayTradeQueryResponse queryResponse = randomPojo(AlipayTradeQueryResponse.class, o -> { + o.setCode("40004"); + o.setSubCode("ACQ.TRADE_NOT_EXIST"); + }); + when(defaultAlipayClient.execute(any(AlipayTradeQueryRequest.class))).thenReturn(queryResponse); + // 准备请求参数 + String outTradeNo = randomString(); + + // 调用 + PayOrderRespDTO resp = client.closeOrder(outTradeNo); + // 断言 + assertEquals(CLOSED.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertSame(queryResponse, resp.getRawData()); + assertEquals(queryResponse.getSubCode(), resp.getChannelErrorCode()); + assertEquals(queryResponse.getSubMsg(), resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝 PC 网站支付:关单系统异常,保持待支付") + public void testCloseOrder_systemError() throws AlipayApiException { + // mock 方法(关单) + AlipayTradeCloseResponse closeResponse = randomPojo(AlipayTradeCloseResponse.class, o -> { + o.setCode("40004"); + o.setSubCode("ACQ.SYSTEM_ERROR"); + }); + when(defaultAlipayClient.execute(any(AlipayTradeCloseRequest.class))).thenReturn(closeResponse); + // mock 方法(查询) + AlipayTradeQueryResponse queryResponse = randomPojo(AlipayTradeQueryResponse.class, o -> { + o.setCode("40004"); + o.setSubCode("ACQ.SYSTEM_ERROR"); + }); + when(defaultAlipayClient.execute(any(AlipayTradeQueryRequest.class))).thenReturn(queryResponse); + // 准备请求参数 + String outTradeNo = randomString(); + + // 调用 + PayOrderRespDTO resp = client.closeOrder(outTradeNo); + // 断言:系统异常时,不能误关闭支付单 + assertEquals(WAITING.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertSame(queryResponse, resp.getRawData()); + } + } diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java index 3172c5e..6481707 100755 --- a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java @@ -1105,4 +1105,141 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest { "updateTime", "updater"); } + @Test + public void testCloseOrder_orderExtension_isSuccess() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 已支付) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setOrderId(order.getId())); + orderExtensionMapper.insert(orderExtension); + + // 调用 + orderService.closeOrder(order.getId()); + // 断言 order 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + } + + @Test + public void testCloseOrder_payClient_notFound() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()) + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + + // 调用 + orderService.closeOrder(order.getId()); + // 断言 order、extension 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testCloseOrder_closeOrder_isWaiting() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()).setNo("P110") + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 等待返回,例如说渠道系统异常) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())); + when(client.closeOrder(eq("P110"))).thenReturn(respDTO); + + // 调用 + orderService.closeOrder(order.getId()); + // 断言 order、extension 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testCloseOrder_closeOrder_isSuccess() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()).setNo("P110") + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 成功返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + when(client.closeOrder(eq("P110"))).thenReturn(respDTO); + // mock 方法(PayChannelDO) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + orderService.closeOrder(order.getId()); + // 断言 order、extension 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + verify(payOrderServiceImpl).notifyOrder(same(channel), same(respDTO)); + } + } + + @Test + public void testCloseOrder_success() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()).setNo("P110") + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(channelService.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 关闭返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())); + when(client.closeOrder(eq("P110"))).thenReturn(respDTO); + + // 调用 + orderService.closeOrder(order.getId()); + // 断言 extension 变化 + orderExtension.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setChannelErrorCode(respDTO.getChannelErrorCode()) + .setChannelErrorMsg(respDTO.getChannelErrorMsg()) + .setChannelNotifyData(toJsonString(respDTO)); + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null), + "updateTime", "updater"); + // 断言 order 变化 + order.setStatus(PayOrderStatusEnum.CLOSED.getStatus()); + assertPojoEquals(order, orderMapper.selectOne(null), + "updateTime", "updater"); + } + }