PaymentRefundService.java 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. package com.xuekairui.payment.service;
  2. import cn.hutool.core.util.IdUtil;
  3. import com.alipay.api.AlipayClient;
  4. import com.alipay.api.domain.AlipayTradeRefundModel;
  5. import com.alipay.api.request.AlipayTradeRefundRequest;
  6. import com.alipay.api.response.AlipayTradeRefundResponse;
  7. import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
  8. import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
  9. import com.github.binarywang.wxpay.service.WxPayService;
  10. import com.xuekairui.common.BusinessException;
  11. import com.xuekairui.common.ErrorCode;
  12. import com.xuekairui.payment.config.PaymentProperties;
  13. import com.xuekairui.payment.enums.PaymentChannel;
  14. import lombok.RequiredArgsConstructor;
  15. import lombok.extern.slf4j.Slf4j;
  16. import org.springframework.beans.factory.ObjectProvider;
  17. import org.springframework.beans.factory.annotation.Qualifier;
  18. import org.springframework.stereotype.Service;
  19. import java.math.BigDecimal;
  20. /**
  21. * 支付退款服务
  22. *
  23. * <p>封装微信支付和支付宝的原路退款逻辑。
  24. * 用于重复支付场景:同一订单多渠道支付成功后,对第二笔及之后的支付自动发起退款。
  25. */
  26. @Slf4j
  27. @Service
  28. @RequiredArgsConstructor
  29. public class PaymentRefundService {
  30. private final PaymentProperties paymentProperties;
  31. @Qualifier("alipayClient")
  32. private final ObjectProvider<AlipayClient> alipayClientProvider;
  33. private final ObjectProvider<WxPayService> wxPayServiceProvider;
  34. /**
  35. * 发起原路退款
  36. *
  37. * @param channel 支付渠道:WECHAT/ALIPAY
  38. * @param orderNo 商户订单号
  39. * @param tradeNo 第三方交易号(微信 transaction_id / 支付宝 trade_no)
  40. * @param amount 退款金额(元)
  41. * @return 退款交易号(第三方退款单号)
  42. */
  43. public String refund(String channel, String orderNo, String tradeNo, BigDecimal amount) {
  44. String refundNo = "RF" + IdUtil.getSnowflake(1, 1).nextIdStr();
  45. if (paymentProperties.isMock()) {
  46. log.info("【模拟】重复支付退款成功: orderNo={}, tradeNo={}, refundNo={}, amount={}元",
  47. orderNo, tradeNo, refundNo, amount);
  48. return refundNo;
  49. }
  50. PaymentChannel ch = PaymentChannel.fromCode(channel);
  51. if (ch == null) {
  52. throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_SUPPORT, "不支持的退款渠道: " + channel);
  53. }
  54. return switch (ch) {
  55. case WECHAT -> wechatRefund(orderNo, tradeNo, amount, refundNo);
  56. case ALIPAY -> alipayRefund(orderNo, tradeNo, amount, refundNo);
  57. };
  58. }
  59. /**
  60. * 微信支付退款(API v3)
  61. *
  62. * <p>调用微信支付 v3 退款接口,按原交易号原路退回。
  63. * 需要商户已配置退款证书(apiclient_key.pem + apiclient_cert.pem)。
  64. */
  65. private String wechatRefund(String orderNo, String tradeNo, BigDecimal amount, String refundNo) {
  66. WxPayService wxPayService = wxPayServiceProvider.getIfAvailable();
  67. if (wxPayService == null) {
  68. throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED, "WxPayService未初始化,无法退款");
  69. }
  70. try {
  71. WxPayRefundV3Request request = new WxPayRefundV3Request();
  72. request.setOutTradeNo(orderNo);
  73. request.setOutRefundNo(refundNo);
  74. request.setReason("重复支付自动退款");
  75. int totalCents = amount.movePointRight(2).intValue();
  76. WxPayRefundV3Request.Amount amountObj = new WxPayRefundV3Request.Amount();
  77. amountObj.setRefund(totalCents);
  78. amountObj.setTotal(totalCents);
  79. amountObj.setCurrency("CNY");
  80. request.setAmount(amountObj);
  81. WxPayRefundV3Result result = wxPayService.refundV3(request);
  82. log.info("微信退款成功: orderNo={}, tradeNo={}, refundNo={}, refundId={}",
  83. orderNo, tradeNo, refundNo, result.getRefundId());
  84. return result.getRefundId();
  85. } catch (Exception e) {
  86. log.error("微信退款失败: orderNo={}, tradeNo={}, refundNo={}", orderNo, tradeNo, refundNo, e);
  87. throw new BusinessException(ErrorCode.REFUND_FAILED, "微信退款失败: " + e.getMessage());
  88. }
  89. }
  90. /**
  91. * 支付宝退款(alipay.trade.refund)
  92. *
  93. * <p>调用支付宝统一收单交易退款接口,按原交易号原路退回。
  94. */
  95. private String alipayRefund(String orderNo, String tradeNo, BigDecimal amount, String refundNo) {
  96. AlipayClient alipayClient = alipayClientProvider.getIfAvailable();
  97. if (alipayClient == null) {
  98. throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED, "AlipayClient未初始化,无法退款");
  99. }
  100. try {
  101. AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
  102. AlipayTradeRefundModel model = new AlipayTradeRefundModel();
  103. model.setOutTradeNo(orderNo);
  104. model.setTradeNo(tradeNo);
  105. model.setRefundAmount(amount.toPlainString());
  106. model.setOutRequestNo(refundNo);
  107. model.setRefundReason("重复支付自动退款");
  108. request.setBizModel(model);
  109. boolean publicKeyMode = paymentProperties.getAlipay().getAlipayPublicKey() != null
  110. && !paymentProperties.getAlipay().getAlipayPublicKey().isBlank();
  111. AlipayTradeRefundResponse response = publicKeyMode
  112. ? alipayClient.execute(request)
  113. : alipayClient.certificateExecute(request);
  114. if (!response.isSuccess()) {
  115. log.error("支付宝退款失败: orderNo={}, tradeNo={}, refundNo={}, code={}, subCode={}, subMsg={}",
  116. orderNo, tradeNo, refundNo, response.getCode(), response.getSubCode(), response.getSubMsg());
  117. throw new BusinessException(ErrorCode.REFUND_FAILED,
  118. "支付宝退款失败: " + response.getSubMsg());
  119. }
  120. log.info("支付宝退款成功: orderNo={}, tradeNo={}, refundNo={}, tradeNo={}",
  121. orderNo, tradeNo, refundNo, response.getTradeNo());
  122. return response.getTradeNo();
  123. } catch (BusinessException e) {
  124. throw e;
  125. } catch (Exception e) {
  126. log.error("支付宝退款异常: orderNo={}, tradeNo={}, refundNo={}", orderNo, tradeNo, refundNo, e);
  127. throw new BusinessException(ErrorCode.REFUND_FAILED, "支付宝退款异常: " + e.getMessage());
  128. }
  129. }
  130. }