Commit 5d67f1fe3b5e72ffab67808ed592f204fff05099
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<AlipayPa | @@ -117,9 +122,13 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<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<AlipayPa | @@ -130,6 +139,30 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<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<AlipayPa | @@ -372,6 +405,16 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<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<NonePayClientConfig> { | @@ -45,6 +45,11 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> { | ||
| 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<NonePayClientConfig> { | @@ -125,6 +125,27 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> { | ||
| 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<WxPayClientC | @@ -216,6 +216,27 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<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
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 | } |