Commit 5d67f1fe3b5e72ffab67808ed592f204fff05099

Authored by YunaiV
1 parent 3ed98e15

fix(pay): cancel trade orders close related payment orders

Showing 14 changed files with 466 additions and 4 deletions
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; @@ -13,6 +13,7 @@ import cn.hutool.extra.spring.SpringUtil;
13 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; 13 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
14 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 14 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
15 import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; 15 import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
  16 +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
16 import cn.iocoder.yudao.module.member.api.address.MemberAddressApi; 17 import cn.iocoder.yudao.module.member.api.address.MemberAddressApi;
17 import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO; 18 import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO;
18 import cn.iocoder.yudao.module.pay.api.order.PayOrderApi; 19 import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
@@ -68,6 +69,8 @@ import lombok.extern.slf4j.Slf4j; @@ -68,6 +69,8 @@ import lombok.extern.slf4j.Slf4j;
68 import org.springframework.scheduling.annotation.Async; 69 import org.springframework.scheduling.annotation.Async;
69 import org.springframework.stereotype.Service; 70 import org.springframework.stereotype.Service;
70 import org.springframework.transaction.annotation.Transactional; 71 import org.springframework.transaction.annotation.Transactional;
  72 +import org.springframework.transaction.support.TransactionSynchronization;
  73 +import org.springframework.transaction.support.TransactionSynchronizationManager;
71 74
72 import java.time.LocalDateTime; 75 import java.time.LocalDateTime;
73 import java.util.ArrayList; 76 import java.util.ArrayList;
@@ -639,6 +642,49 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { @@ -639,6 +642,49 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
639 642
640 // 3. 增加订单日志 643 // 3. 增加订单日志
641 TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), TradeOrderStatusEnum.CANCELED.getStatus()); 644 TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), TradeOrderStatusEnum.CANCELED.getStatus());
  645 +
  646 + // 4. 关闭支付单
  647 + closePayOrder(order.getId(), order.getPayOrderId(), cancelType);
  648 + }
  649 +
  650 + /**
  651 + * 注册事务提交后的支付单关闭任务
  652 + *
  653 + * @param orderId 交易订单编号
  654 + * @param payOrderId 支付订单编号
  655 + * @param cancelType 订单取消类型
  656 + */
  657 + private void closePayOrder(Long orderId, Long payOrderId, TradeOrderCancelTypeEnum cancelType) {
  658 + if (!ObjectUtils.equalsAny(cancelType, TradeOrderCancelTypeEnum.MEMBER_CANCEL,
  659 + TradeOrderCancelTypeEnum.PAY_TIMEOUT)
  660 + || payOrderId == null || !TransactionSynchronizationManager.isSynchronizationActive()) {
  661 + return;
  662 + }
  663 + // 必须在事务提交后,再关闭支付单,避免交易事务回滚后支付单已经关闭
  664 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
  665 +
  666 + @Override
  667 + public void afterCommit() {
  668 + // 异步的原因:避免阻塞当前事务,无需等待结果
  669 + getSelf().closePayOrderAsync(orderId, payOrderId);
  670 + }
  671 +
  672 + });
  673 + }
  674 +
  675 + /**
  676 + * 异步关闭订单关联的支付单
  677 + *
  678 + * @param orderId 交易订单编号
  679 + * @param payOrderId 支付订单编号
  680 + */
  681 + @Async
  682 + public void closePayOrderAsync(Long orderId, Long payOrderId) {
  683 + try {
  684 + payOrderApi.closeOrder(payOrderId);
  685 + } catch (Throwable e) {
  686 + log.error("[closePayOrderAsync][order({}) payOrder({}) 异步关闭支付单失败]", orderId, payOrderId, e);
  687 + }
642 } 688 }
643 689
644 /** 690 /**
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApi.java
@@ -30,6 +30,13 @@ public interface PayOrderApi { @@ -30,6 +30,13 @@ public interface PayOrderApi {
30 PayOrderRespDTO getOrder(Long id); 30 PayOrderRespDTO getOrder(Long id);
31 31
32 /** 32 /**
  33 + * 关闭支付单
  34 + *
  35 + * @param id 支付单编号
  36 + */
  37 + void closeOrder(Long id);
  38 +
  39 + /**
33 * 更新支付订单价格 40 * 更新支付订单价格
34 * 41 *
35 * @param id 支付单编号 42 * @param id 支付单编号
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java
@@ -32,6 +32,11 @@ public class PayOrderApiImpl implements PayOrderApi { @@ -32,6 +32,11 @@ public class PayOrderApiImpl implements PayOrderApi {
32 } 32 }
33 33
34 @Override 34 @Override
  35 + public void closeOrder(Long id) {
  36 + payOrderService.closeOrder(id);
  37 + }
  38 +
  39 + @Override
35 public void updatePayOrderPrice(Long id, Integer payPrice) { 40 public void updatePayOrderPrice(Long id, Integer payPrice) {
36 payOrderService.updatePayOrderPrice(id, payPrice); 41 payOrderService.updatePayOrderPrice(id, payPrice);
37 } 42 }
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/PayClient.java
@@ -58,6 +58,14 @@ public interface PayClient<Config> { @@ -58,6 +58,14 @@ public interface PayClient<Config> {
58 */ 58 */
59 PayOrderRespDTO getOrder(String outTradeNo); 59 PayOrderRespDTO getOrder(String outTradeNo);
60 60
  61 + /**
  62 + * 关闭支付订单
  63 + *
  64 + * @param outTradeNo 外部订单号
  65 + * @return 关闭结果
  66 + */
  67 + PayOrderRespDTO closeOrder(String outTradeNo);
  68 +
61 // ============ 退款相关 ========== 69 // ============ 退款相关 ==========
62 70
63 /** 71 /**
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<Config extends PayClientConfig> implemen @@ -134,6 +134,23 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
134 protected abstract PayOrderRespDTO doGetOrder(String outTradeNo) 134 protected abstract PayOrderRespDTO doGetOrder(String outTradeNo)
135 throws Throwable; 135 throws Throwable;
136 136
  137 + @Override
  138 + public final PayOrderRespDTO closeOrder(String outTradeNo) {
  139 + try {
  140 + return doCloseOrder(outTradeNo);
  141 + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
  142 + throw ex;
  143 + } catch (Throwable ex) {
  144 + // 系统异常,则包装成 PayException 异常抛出
  145 + log.error("[closeOrder][客户端({}) outTradeNo({}) 关闭支付单异常]",
  146 + getId(), outTradeNo, ex);
  147 + throw buildPayException(ex);
  148 + }
  149 + }
  150 +
  151 + protected abstract PayOrderRespDTO doCloseOrder(String outTradeNo)
  152 + throws Throwable;
  153 +
137 // ============ 退款相关 ========== 154 // ============ 退款相关 ==========
138 155
139 @Override 156 @Override
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. @@ -51,6 +51,11 @@ import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.
51 @Slf4j 51 @Slf4j
52 public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPayClientConfig> { 52 public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPayClientConfig> {
53 53
  54 + /**
  55 + * 支付宝接口调用成功的响应码
  56 + */
  57 + protected static final String ALIPAY_SUCCESS_CODE = "10000";
  58 +
54 @Getter // 仅用于单测场景 59 @Getter // 仅用于单测场景
55 protected DefaultAlipayClient client; 60 protected DefaultAlipayClient client;
56 61
@@ -117,9 +122,13 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa @@ -117,9 +122,13 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa
117 } else { 122 } else {
118 response = client.execute(request); 123 response = client.execute(request);
119 } 124 }
120 - if (!response.isSuccess()) { // 不成功,例如说订单不存在  
121 - return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),  
122 - outTradeNo, response); 125 + if (!isSuccessResponse(response)) {
  126 + // 只有明确不存在时才关闭,系统异常等情况需要继续等待后续查询
  127 + if (ObjectUtils.equalsAny(response.getSubCode(), "TRADE_NOT_EXIST", "ACQ.TRADE_NOT_EXIST")) {
  128 + return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
  129 + outTradeNo, response);
  130 + }
  131 + return PayOrderRespDTO.waitingOf(null, null, outTradeNo, response);
123 } 132 }
124 // 2.2 解析订单的状态 133 // 2.2 解析订单的状态
125 Integer status = parseStatus(response.getTradeStatus()); 134 Integer status = parseStatus(response.getTradeStatus());
@@ -130,6 +139,30 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa @@ -130,6 +139,30 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa
130 outTradeNo, response); 139 outTradeNo, response);
131 } 140 }
132 141
  142 + @Override
  143 + protected PayOrderRespDTO doCloseOrder(String outTradeNo) throws Throwable {
  144 + // 1.1 构建 AlipayTradeCloseModel 请求
  145 + AlipayTradeCloseModel model = new AlipayTradeCloseModel();
  146 + model.setOutTradeNo(outTradeNo);
  147 + // 1.2 构建 AlipayTradeCloseRequest 请求
  148 + AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
  149 + request.setBizModel(model);
  150 +
  151 + // 2.1 执行请求
  152 + AlipayTradeCloseResponse response;
  153 + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
  154 + response = client.certificateExecute(request);
  155 + } else {
  156 + response = client.execute(request);
  157 + }
  158 + if (!isSuccessResponse(response)) {
  159 + // 关闭失败可能是订单已支付,重新查询保留真实支付事实
  160 + return doGetOrder(outTradeNo);
  161 + }
  162 + // 2.2 创建返回结果
  163 + return PayOrderRespDTO.closedOf(null, null, outTradeNo, response);
  164 + }
  165 +
133 private static Integer parseStatus(String tradeStatus) { 166 private static Integer parseStatus(String tradeStatus) {
134 return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusEnum.WAITING.getStatus() 167 return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusEnum.WAITING.getStatus()
135 : ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusEnum.SUCCESS.getStatus() 168 : ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusEnum.SUCCESS.getStatus()
@@ -372,6 +405,16 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa @@ -372,6 +405,16 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa
372 405
373 // ========== 各种工具方法 ========== 406 // ========== 各种工具方法 ==========
374 407
  408 + /**
  409 + * 判断支付宝接口是否调用成功
  410 + *
  411 + * @param response 响应
  412 + * @return 是否调用成功
  413 + */
  414 + protected static boolean isSuccessResponse(AlipayResponse response) {
  415 + return ALIPAY_SUCCESS_CODE.equals(response.getCode());
  416 + }
  417 +
375 protected String formatAmount(Integer amount) { 418 protected String formatAmount(Integer amount) {
376 return String.valueOf(amount / 100.0); 419 return String.valueOf(amount / 100.0);
377 } 420 }
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java
@@ -72,7 +72,7 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient { @@ -72,7 +72,7 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
72 if (!response.isSuccess()) { 72 if (!response.isSuccess()) {
73 return buildClosedPayOrderRespDTO(reqDTO, response); 73 return buildClosedPayOrderRespDTO(reqDTO, response);
74 } 74 }
75 - if ("10000".equals(response.getCode())) { // 免密支付 75 + if (isSuccessResponse(response)) { // 免密支付
76 LocalDateTime successTime = LocalDateTimeUtil.of(response.getGmtPayment()); 76 LocalDateTime successTime = LocalDateTimeUtil.of(response.getGmtPayment());
77 return PayOrderRespDTO.successOf(response.getTradeNo(), response.getBuyerUserId(), successTime, 77 return PayOrderRespDTO.successOf(response.getTradeNo(), response.getBuyerUserId(), successTime,
78 response.getOutTradeNo(), response) 78 response.getOutTradeNo(), response)
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/mock/MockPayClient.java
@@ -45,6 +45,11 @@ public class MockPayClient extends AbstractPayClient&lt;NonePayClientConfig&gt; { @@ -45,6 +45,11 @@ public class MockPayClient extends AbstractPayClient&lt;NonePayClientConfig&gt; {
45 } 45 }
46 46
47 @Override 47 @Override
  48 + protected PayOrderRespDTO doCloseOrder(String outTradeNo) {
  49 + return PayOrderRespDTO.closedOf(null, null, outTradeNo, MOCK_RESP_SUCCESS_DATA);
  50 + }
  51 +
  52 + @Override
48 protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { 53 protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
49 return PayRefundRespDTO.successOf("MOCK-R-" + reqDTO.getOutRefundNo(), LocalDateTime.now(), 54 return PayRefundRespDTO.successOf("MOCK-R-" + reqDTO.getOutRefundNo(), LocalDateTime.now(),
50 reqDTO.getOutRefundNo(), MOCK_RESP_SUCCESS_DATA); 55 reqDTO.getOutRefundNo(), MOCK_RESP_SUCCESS_DATA);
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&lt;NonePayClientConfig&gt; { @@ -125,6 +125,27 @@ public class WalletPayClient extends AbstractPayClient&lt;NonePayClientConfig&gt; {
125 } 125 }
126 126
127 @Override 127 @Override
  128 + protected PayOrderRespDTO doCloseOrder(String outTradeNo) {
  129 + if (orderService == null) {
  130 + orderService = SpringUtil.getBean(PayOrderService.class);
  131 + }
  132 + // 待支付状态:钱包支付为同步扣款,没有渠道侧的待支付单,所以以钱包流水判断是否已扣款
  133 + // 补充说明:存在扣款成功,但是 notifyOrder 更新支付拓展单失败的情况,此时不能直接关闭
  134 + PayOrderExtensionDO orderExtension = orderService.getOrderExtensionByNo(outTradeNo);
  135 + if (orderExtension != null && PayOrderStatusEnum.isWaiting(orderExtension.getStatus())) {
  136 + PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransaction(
  137 + String.valueOf(orderExtension.getOrderId()), PayWalletBizTypeEnum.PAYMENT);
  138 + if (walletTransaction == null) {
  139 + return PayOrderRespDTO.closedOf(null, null, outTradeNo, "");
  140 + }
  141 + return PayOrderRespDTO.successOf(walletTransaction.getNo(), walletTransaction.getCreator(),
  142 + walletTransaction.getCreateTime(), outTradeNo, walletTransaction);
  143 + }
  144 + // 其它状态:以本地支付状态为准
  145 + return doGetOrder(outTradeNo);
  146 + }
  147 +
  148 + @Override
128 @SuppressWarnings("PatternVariableCanBeUsed") 149 @SuppressWarnings("PatternVariableCanBeUsed")
129 protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { 150 protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
130 try { 151 try {
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&lt;WxPayClientC @@ -216,6 +216,27 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
216 } 216 }
217 } 217 }
218 218
  219 + @Override
  220 + protected PayOrderRespDTO doCloseOrder(String outTradeNo) throws Throwable {
  221 + try {
  222 + switch (config.getApiVersion()) {
  223 + case API_VERSION_V2:
  224 + client.closeOrder(WxPayOrderCloseRequest.newBuilder().outTradeNo(outTradeNo).build());
  225 + break;
  226 + case API_VERSION_V3:
  227 + client.closeOrderV3(new WxPayOrderCloseV3Request()
  228 + .setMchid(config.getMchId()).setOutTradeNo(outTradeNo));
  229 + break;
  230 + default:
  231 + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
  232 + }
  233 + return PayOrderRespDTO.closedOf(null, null, outTradeNo, null);
  234 + } catch (WxPayException e) {
  235 + // 关闭失败时查询真实支付状态,避免遗漏已支付事实
  236 + return doGetOrder(outTradeNo);
  237 + }
  238 + }
  239 +
219 private PayOrderRespDTO doGetOrderV2(String outTradeNo) throws WxPayException { 240 private PayOrderRespDTO doGetOrderV2(String outTradeNo) throws WxPayException {
220 // 构建 WxPayUnifiedOrderRequest 对象 241 // 构建 WxPayUnifiedOrderRequest 对象
221 WxPayOrderQueryRequest request = WxPayOrderQueryRequest.newBuilder() 242 WxPayOrderQueryRequest request = WxPayOrderQueryRequest.newBuilder()
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderService.java
@@ -164,4 +164,11 @@ public interface PayOrderService { @@ -164,4 +164,11 @@ public interface PayOrderService {
164 */ 164 */
165 int expireOrder(); 165 int expireOrder();
166 166
  167 + /**
  168 + * 关闭支付订单
  169 + *
  170 + * @param id 支付单编号
  171 + */
  172 + void closeOrder(Long id);
  173 +
167 } 174 }
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java
@@ -598,6 +598,72 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -598,6 +598,72 @@ public class PayOrderServiceImpl implements PayOrderService {
598 } 598 }
599 } 599 }
600 600
  601 + @Override
  602 + public void closeOrder(Long id) {
  603 + PayOrderDO order = orderMapper.selectById(id);
  604 + if (order == null || PayOrderStatusEnum.isClosed(order.getStatus())
  605 + || PayOrderStatusEnum.isRefund(order.getStatus())) {
  606 + return;
  607 + }
  608 + if (PayOrderStatusEnum.isSuccess(order.getStatus())) {
  609 + log.error("[closeOrder][order({}) 已支付,订单业务可能已取消,需要人工退款]", id);
  610 + return;
  611 + }
  612 + try {
  613 + // 1. 逐个关闭支付拓展单,避免遗漏重复支付尝试
  614 + List<PayOrderExtensionDO> extensions = orderExtensionMapper.selectListByOrderId(id);
  615 + for (PayOrderExtensionDO extension : extensions) {
  616 + if (PayOrderStatusEnum.isClosed(extension.getStatus())) {
  617 + continue;
  618 + }
  619 + if (PayOrderStatusEnum.isSuccess(extension.getStatus())) {
  620 + log.error("[closeOrder][order({}) extension({}) 已支付,订单业务可能已取消,需要人工退款]",
  621 + id, extension.getId());
  622 + return;
  623 + }
  624 + if (!PayOrderStatusEnum.isWaiting(extension.getStatus())) {
  625 + continue;
  626 + }
  627 + PayClient<?> client = channelService.getPayClient(extension.getChannelId());
  628 + if (client == null) {
  629 + log.error("[closeOrder][order({}) extension({}) 渠道({})不存在]",
  630 + id, extension.getId(), extension.getChannelId());
  631 + return;
  632 + }
  633 + // 1.1 调用渠道关单,已支付结果由渠道客户端转换并保留
  634 + PayOrderRespDTO respDTO = client.closeOrder(extension.getNo());
  635 + if (PayOrderStatusEnum.isSuccess(respDTO.getStatus())) {
  636 + notifyOrder(extension.getChannelId(), respDTO);
  637 + log.error("[closeOrder][order({}) extension({}) 查询发现已支付]", id, extension.getId());
  638 + return;
  639 + }
  640 + if (!PayOrderStatusEnum.isClosed(respDTO.getStatus())) {
  641 + log.warn("[closeOrder][order({}) extension({}) 渠道状态({}),暂不关闭本地支付单]",
  642 + id, extension.getId(), respDTO.getStatus());
  643 + return;
  644 + }
  645 + // 1.2 更新 PayOrderExtensionDO 状态
  646 + PayOrderExtensionDO updateObj = new PayOrderExtensionDO()
  647 + .setStatus(PayOrderStatusEnum.CLOSED.getStatus())
  648 + .setChannelErrorCode(respDTO.getChannelErrorCode())
  649 + .setChannelErrorMsg(respDTO.getChannelErrorMsg())
  650 + .setChannelNotifyData(toJsonString(respDTO));
  651 + if (orderExtensionMapper.updateByIdAndStatus(extension.getId(),
  652 + PayOrderStatusEnum.WAITING.getStatus(), updateObj) == 0) {
  653 + log.error("[closeOrder][order({}) extension({}) 更新为支付关闭失败]", id, extension.getId());
  654 + return;
  655 + }
  656 + }
  657 + // 2. 所有支付拓展单都关闭后,再关闭支付主单
  658 + if (orderMapper.updateByIdAndStatus(id, PayOrderStatusEnum.WAITING.getStatus(),
  659 + new PayOrderDO().setStatus(PayOrderStatusEnum.CLOSED.getStatus())) == 0) {
  660 + log.error("[closeOrder][order({}) 更新为支付关闭失败]", id);
  661 + }
  662 + } catch (Throwable e) {
  663 + log.error("[closeOrder][order({}) 关闭支付单异常]", id, e);
  664 + }
  665 + }
  666 +
601 /** 667 /**
602 * 获得自身的代理对象,解决 AOP 生效问题 668 * 获得自身的代理对象,解决 AOP 生效问题
603 * 669 *
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 @@ -5,8 +5,12 @@ import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderR
5 import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 5 import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
6 import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum; 6 import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
7 import com.alipay.api.AlipayApiException; 7 import com.alipay.api.AlipayApiException;
  8 +import com.alipay.api.request.AlipayTradeCloseRequest;
8 import com.alipay.api.request.AlipayTradePagePayRequest; 9 import com.alipay.api.request.AlipayTradePagePayRequest;
  10 +import com.alipay.api.request.AlipayTradeQueryRequest;
  11 +import com.alipay.api.response.AlipayTradeCloseResponse;
9 import com.alipay.api.response.AlipayTradePagePayResponse; 12 import com.alipay.api.response.AlipayTradePagePayResponse;
  13 +import com.alipay.api.response.AlipayTradeQueryResponse;
10 import org.junit.jupiter.api.BeforeEach; 14 import org.junit.jupiter.api.BeforeEach;
11 import org.junit.jupiter.api.DisplayName; 15 import org.junit.jupiter.api.DisplayName;
12 import org.junit.jupiter.api.Test; 16 import org.junit.jupiter.api.Test;
@@ -18,6 +22,7 @@ import static cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum.WAITING @@ -18,6 +22,7 @@ import static cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum.WAITING
18 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; 22 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
19 import static org.junit.jupiter.api.Assertions.*; 23 import static org.junit.jupiter.api.Assertions.*;
20 import static org.junit.jupiter.api.Assertions.assertNull; 24 import static org.junit.jupiter.api.Assertions.assertNull;
  25 +import static org.mockito.ArgumentMatchers.any;
21 import static org.mockito.ArgumentMatchers.argThat; 26 import static org.mockito.ArgumentMatchers.argThat;
22 import static org.mockito.ArgumentMatchers.eq; 27 import static org.mockito.ArgumentMatchers.eq;
23 import static org.mockito.Mockito.when; 28 import static org.mockito.Mockito.when;
@@ -128,4 +133,78 @@ public class AlipayPcPayClientTest extends AbstractAlipayClientTest { @@ -128,4 +133,78 @@ public class AlipayPcPayClientTest extends AbstractAlipayClientTest {
128 assertEquals(subMsg, resp.getChannelErrorMsg()); 133 assertEquals(subMsg, resp.getChannelErrorMsg());
129 } 134 }
130 135
  136 + @Test
  137 + @DisplayName("支付宝 PC 网站支付:关单成功")
  138 + public void testCloseOrder_success() throws AlipayApiException {
  139 + // mock 方法
  140 + AlipayTradeCloseResponse response = randomPojo(AlipayTradeCloseResponse.class, o -> {
  141 + o.setCode(AbstractAlipayPayClient.ALIPAY_SUCCESS_CODE);
  142 + o.setSubCode("");
  143 + });
  144 + when(defaultAlipayClient.execute(any(AlipayTradeCloseRequest.class))).thenReturn(response);
  145 + // 准备请求参数
  146 + String outTradeNo = randomString();
  147 +
  148 + // 调用
  149 + PayOrderRespDTO resp = client.closeOrder(outTradeNo);
  150 + // 断言
  151 + assertEquals(CLOSED.getStatus(), resp.getStatus());
  152 + assertEquals(outTradeNo, resp.getOutTradeNo());
  153 + assertSame(response, resp.getRawData());
  154 + }
  155 +
  156 + @Test
  157 + @DisplayName("支付宝 PC 网站支付:关单时交易不存在,查询后关闭")
  158 + public void testCloseOrder_tradeNotExist() throws AlipayApiException {
  159 + // mock 方法(关单)
  160 + AlipayTradeCloseResponse closeResponse = randomPojo(AlipayTradeCloseResponse.class, o -> {
  161 + o.setCode("40004");
  162 + o.setSubCode("ACQ.TRADE_NOT_EXIST");
  163 + });
  164 + when(defaultAlipayClient.execute(any(AlipayTradeCloseRequest.class))).thenReturn(closeResponse);
  165 + // mock 方法(查询)
  166 + AlipayTradeQueryResponse queryResponse = randomPojo(AlipayTradeQueryResponse.class, o -> {
  167 + o.setCode("40004");
  168 + o.setSubCode("ACQ.TRADE_NOT_EXIST");
  169 + });
  170 + when(defaultAlipayClient.execute(any(AlipayTradeQueryRequest.class))).thenReturn(queryResponse);
  171 + // 准备请求参数
  172 + String outTradeNo = randomString();
  173 +
  174 + // 调用
  175 + PayOrderRespDTO resp = client.closeOrder(outTradeNo);
  176 + // 断言
  177 + assertEquals(CLOSED.getStatus(), resp.getStatus());
  178 + assertEquals(outTradeNo, resp.getOutTradeNo());
  179 + assertSame(queryResponse, resp.getRawData());
  180 + assertEquals(queryResponse.getSubCode(), resp.getChannelErrorCode());
  181 + assertEquals(queryResponse.getSubMsg(), resp.getChannelErrorMsg());
  182 + }
  183 +
  184 + @Test
  185 + @DisplayName("支付宝 PC 网站支付:关单系统异常,保持待支付")
  186 + public void testCloseOrder_systemError() throws AlipayApiException {
  187 + // mock 方法(关单)
  188 + AlipayTradeCloseResponse closeResponse = randomPojo(AlipayTradeCloseResponse.class, o -> {
  189 + o.setCode("40004");
  190 + o.setSubCode("ACQ.SYSTEM_ERROR");
  191 + });
  192 + when(defaultAlipayClient.execute(any(AlipayTradeCloseRequest.class))).thenReturn(closeResponse);
  193 + // mock 方法(查询)
  194 + AlipayTradeQueryResponse queryResponse = randomPojo(AlipayTradeQueryResponse.class, o -> {
  195 + o.setCode("40004");
  196 + o.setSubCode("ACQ.SYSTEM_ERROR");
  197 + });
  198 + when(defaultAlipayClient.execute(any(AlipayTradeQueryRequest.class))).thenReturn(queryResponse);
  199 + // 准备请求参数
  200 + String outTradeNo = randomString();
  201 +
  202 + // 调用
  203 + PayOrderRespDTO resp = client.closeOrder(outTradeNo);
  204 + // 断言:系统异常时,不能误关闭支付单
  205 + assertEquals(WAITING.getStatus(), resp.getStatus());
  206 + assertEquals(outTradeNo, resp.getOutTradeNo());
  207 + assertSame(queryResponse, resp.getRawData());
  208 + }
  209 +
131 } 210 }
yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java
@@ -1105,4 +1105,141 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest { @@ -1105,4 +1105,141 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
1105 "updateTime", "updater"); 1105 "updateTime", "updater");
1106 } 1106 }
1107 1107
  1108 + @Test
  1109 + public void testCloseOrder_orderExtension_isSuccess() {
  1110 + // mock 数据(PayOrderDO)
  1111 + PayOrderDO order = randomPojo(PayOrderDO.class,
  1112 + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()));
  1113 + orderMapper.insert(order);
  1114 + // mock 数据(PayOrderExtensionDO 已支付)
  1115 + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
  1116 + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())
  1117 + .setOrderId(order.getId()));
  1118 + orderExtensionMapper.insert(orderExtension);
  1119 +
  1120 + // 调用
  1121 + orderService.closeOrder(order.getId());
  1122 + // 断言 order 没有变化,因为没更新
  1123 + assertPojoEquals(order, orderMapper.selectOne(null));
  1124 + }
  1125 +
  1126 + @Test
  1127 + public void testCloseOrder_payClient_notFound() {
  1128 + // mock 数据(PayOrderDO)
  1129 + PayOrderDO order = randomPojo(PayOrderDO.class,
  1130 + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()));
  1131 + orderMapper.insert(order);
  1132 + // mock 数据(PayOrderExtensionDO 等待中)
  1133 + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
  1134 + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
  1135 + .setOrderId(order.getId())
  1136 + .setChannelId(10L));
  1137 + orderExtensionMapper.insert(orderExtension);
  1138 +
  1139 + // 调用
  1140 + orderService.closeOrder(order.getId());
  1141 + // 断言 order、extension 没有变化,因为没更新
  1142 + assertPojoEquals(order, orderMapper.selectOne(null));
  1143 + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null));
  1144 + }
  1145 +
  1146 + @Test
  1147 + public void testCloseOrder_closeOrder_isWaiting() {
  1148 + // mock 数据(PayOrderDO)
  1149 + PayOrderDO order = randomPojo(PayOrderDO.class,
  1150 + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()));
  1151 + orderMapper.insert(order);
  1152 + // mock 数据(PayOrderExtensionDO 等待中)
  1153 + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
  1154 + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
  1155 + .setOrderId(order.getId()).setNo("P110")
  1156 + .setChannelId(10L));
  1157 + orderExtensionMapper.insert(orderExtension);
  1158 + // mock 方法(PayClient)
  1159 + PayClient<?> client = mock(PayClient.class);
  1160 + when(channelService.getPayClient(eq(10L))).thenReturn(client);
  1161 + // mock 方法(PayClient 等待返回,例如说渠道系统异常)
  1162 + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class,
  1163 + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()));
  1164 + when(client.closeOrder(eq("P110"))).thenReturn(respDTO);
  1165 +
  1166 + // 调用
  1167 + orderService.closeOrder(order.getId());
  1168 + // 断言 order、extension 没有变化,因为没更新
  1169 + assertPojoEquals(order, orderMapper.selectOne(null));
  1170 + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null));
  1171 + }
  1172 +
  1173 + @Test
  1174 + public void testCloseOrder_closeOrder_isSuccess() {
  1175 + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class);
  1176 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  1177 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class)))
  1178 + .thenReturn(payOrderServiceImpl);
  1179 +
  1180 + // mock 数据(PayOrderDO)
  1181 + PayOrderDO order = randomPojo(PayOrderDO.class,
  1182 + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()));
  1183 + orderMapper.insert(order);
  1184 + // mock 数据(PayOrderExtensionDO 等待中)
  1185 + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
  1186 + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
  1187 + .setOrderId(order.getId()).setNo("P110")
  1188 + .setChannelId(10L));
  1189 + orderExtensionMapper.insert(orderExtension);
  1190 + // mock 方法(PayClient)
  1191 + PayClient<?> client = mock(PayClient.class);
  1192 + when(channelService.getPayClient(eq(10L))).thenReturn(client);
  1193 + // mock 方法(PayClient 成功返回)
  1194 + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class,
  1195 + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()));
  1196 + when(client.closeOrder(eq("P110"))).thenReturn(respDTO);
  1197 + // mock 方法(PayChannelDO)
  1198 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
  1199 + when(channelService.validPayChannel(eq(10L))).thenReturn(channel);
  1200 +
  1201 + // 调用
  1202 + orderService.closeOrder(order.getId());
  1203 + // 断言 order、extension 没有变化,因为没更新
  1204 + assertPojoEquals(order, orderMapper.selectOne(null));
  1205 + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null));
  1206 + verify(payOrderServiceImpl).notifyOrder(same(channel), same(respDTO));
  1207 + }
  1208 + }
  1209 +
  1210 + @Test
  1211 + public void testCloseOrder_success() {
  1212 + // mock 数据(PayOrderDO)
  1213 + PayOrderDO order = randomPojo(PayOrderDO.class,
  1214 + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()));
  1215 + orderMapper.insert(order);
  1216 + // mock 数据(PayOrderExtensionDO 等待中)
  1217 + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
  1218 + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
  1219 + .setOrderId(order.getId()).setNo("P110")
  1220 + .setChannelId(10L));
  1221 + orderExtensionMapper.insert(orderExtension);
  1222 + // mock 方法(PayClient)
  1223 + PayClient<?> client = mock(PayClient.class);
  1224 + when(channelService.getPayClient(eq(10L))).thenReturn(client);
  1225 + // mock 方法(PayClient 关闭返回)
  1226 + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class,
  1227 + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()));
  1228 + when(client.closeOrder(eq("P110"))).thenReturn(respDTO);
  1229 +
  1230 + // 调用
  1231 + orderService.closeOrder(order.getId());
  1232 + // 断言 extension 变化
  1233 + orderExtension.setStatus(PayOrderStatusEnum.CLOSED.getStatus())
  1234 + .setChannelErrorCode(respDTO.getChannelErrorCode())
  1235 + .setChannelErrorMsg(respDTO.getChannelErrorMsg())
  1236 + .setChannelNotifyData(toJsonString(respDTO));
  1237 + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null),
  1238 + "updateTime", "updater");
  1239 + // 断言 order 变化
  1240 + order.setStatus(PayOrderStatusEnum.CLOSED.getStatus());
  1241 + assertPojoEquals(order, orderMapper.selectOne(null),
  1242 + "updateTime", "updater");
  1243 + }
  1244 +
1108 } 1245 }