package com.xuekairui.payment.service; import cn.hutool.core.util.IdUtil; import com.alipay.api.AlipayClient; import com.alipay.api.domain.AlipayTradeRefundModel; import com.alipay.api.request.AlipayTradeRefundRequest; import com.alipay.api.response.AlipayTradeRefundResponse; import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; import com.github.binarywang.wxpay.service.WxPayService; import com.xuekairui.common.BusinessException; import com.xuekairui.common.ErrorCode; import com.xuekairui.payment.config.PaymentProperties; import com.xuekairui.payment.enums.PaymentChannel; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import java.math.BigDecimal; /** * 支付退款服务 * *

封装微信支付和支付宝的原路退款逻辑。 * 用于重复支付场景:同一订单多渠道支付成功后,对第二笔及之后的支付自动发起退款。 */ @Slf4j @Service @RequiredArgsConstructor public class PaymentRefundService { private final PaymentProperties paymentProperties; @Qualifier("alipayClient") private final ObjectProvider alipayClientProvider; private final ObjectProvider wxPayServiceProvider; /** * 发起原路退款 * * @param channel 支付渠道:WECHAT/ALIPAY * @param orderNo 商户订单号 * @param tradeNo 第三方交易号(微信 transaction_id / 支付宝 trade_no) * @param amount 退款金额(元) * @return 退款交易号(第三方退款单号) */ public String refund(String channel, String orderNo, String tradeNo, BigDecimal amount) { String refundNo = "RF" + IdUtil.getSnowflake(1, 1).nextIdStr(); if (paymentProperties.isMock()) { log.info("【模拟】重复支付退款成功: orderNo={}, tradeNo={}, refundNo={}, amount={}元", orderNo, tradeNo, refundNo, amount); return refundNo; } PaymentChannel ch = PaymentChannel.fromCode(channel); if (ch == null) { throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_SUPPORT, "不支持的退款渠道: " + channel); } return switch (ch) { case WECHAT -> wechatRefund(orderNo, tradeNo, amount, refundNo); case ALIPAY -> alipayRefund(orderNo, tradeNo, amount, refundNo); }; } /** * 微信支付退款(API v3) * *

调用微信支付 v3 退款接口,按原交易号原路退回。 * 需要商户已配置退款证书(apiclient_key.pem + apiclient_cert.pem)。 */ private String wechatRefund(String orderNo, String tradeNo, BigDecimal amount, String refundNo) { WxPayService wxPayService = wxPayServiceProvider.getIfAvailable(); if (wxPayService == null) { throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED, "WxPayService未初始化,无法退款"); } try { WxPayRefundV3Request request = new WxPayRefundV3Request(); request.setOutTradeNo(orderNo); request.setOutRefundNo(refundNo); request.setReason("重复支付自动退款"); int totalCents = amount.movePointRight(2).intValue(); WxPayRefundV3Request.Amount amountObj = new WxPayRefundV3Request.Amount(); amountObj.setRefund(totalCents); amountObj.setTotal(totalCents); amountObj.setCurrency("CNY"); request.setAmount(amountObj); WxPayRefundV3Result result = wxPayService.refundV3(request); log.info("微信退款成功: orderNo={}, tradeNo={}, refundNo={}, refundId={}", orderNo, tradeNo, refundNo, result.getRefundId()); return result.getRefundId(); } catch (Exception e) { log.error("微信退款失败: orderNo={}, tradeNo={}, refundNo={}", orderNo, tradeNo, refundNo, e); throw new BusinessException(ErrorCode.REFUND_FAILED, "微信退款失败: " + e.getMessage()); } } /** * 支付宝退款(alipay.trade.refund) * *

调用支付宝统一收单交易退款接口,按原交易号原路退回。 */ private String alipayRefund(String orderNo, String tradeNo, BigDecimal amount, String refundNo) { AlipayClient alipayClient = alipayClientProvider.getIfAvailable(); if (alipayClient == null) { throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED, "AlipayClient未初始化,无法退款"); } try { AlipayTradeRefundRequest request = new AlipayTradeRefundRequest(); AlipayTradeRefundModel model = new AlipayTradeRefundModel(); model.setOutTradeNo(orderNo); model.setTradeNo(tradeNo); model.setRefundAmount(amount.toPlainString()); model.setOutRequestNo(refundNo); model.setRefundReason("重复支付自动退款"); request.setBizModel(model); boolean publicKeyMode = paymentProperties.getAlipay().getAlipayPublicKey() != null && !paymentProperties.getAlipay().getAlipayPublicKey().isBlank(); AlipayTradeRefundResponse response = publicKeyMode ? alipayClient.execute(request) : alipayClient.certificateExecute(request); if (!response.isSuccess()) { log.error("支付宝退款失败: orderNo={}, tradeNo={}, refundNo={}, code={}, subCode={}, subMsg={}", orderNo, tradeNo, refundNo, response.getCode(), response.getSubCode(), response.getSubMsg()); throw new BusinessException(ErrorCode.REFUND_FAILED, "支付宝退款失败: " + response.getSubMsg()); } log.info("支付宝退款成功: orderNo={}, tradeNo={}, refundNo={}, tradeNo={}", orderNo, tradeNo, refundNo, response.getTradeNo()); return response.getTradeNo(); } catch (BusinessException e) { throw e; } catch (Exception e) { log.error("支付宝退款异常: orderNo={}, tradeNo={}, refundNo={}", orderNo, tradeNo, refundNo, e); throw new BusinessException(ErrorCode.REFUND_FAILED, "支付宝退款异常: " + e.getMessage()); } } }