feat(payment): 实现微信支付退款及状态查询功能

- 新增微信支付退款接口实现,支持全额或部分退款
- 实现微信退款状态查询接口,用于轮询退款进度
- 添加退款状态转换逻辑,映射微信状态到系统内部状态
- 创建定时任务定期查询处理中的退款订单并更新状态
- 在订单控制器中集成退款操作,支持手动触发退款
- 完善退款参数校验和异常处理机制
- 添加退款日志记录,便于追踪退款流程和问题排查
This commit is contained in:
2025-12-08 09:42:33 +08:00
parent 57b23e7a33
commit 9eb9282cfe
3 changed files with 501 additions and 4 deletions

View File

@@ -18,6 +18,8 @@ import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;
import com.wechat.pay.java.service.payments.nativepay.model.QueryOrderByOutTradeNoRequest;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@@ -196,14 +198,81 @@ public class WechatNativeStrategy implements PaymentStrategy {
@Override
public PaymentResponse refund(String orderNo, String refundNo, BigDecimal totalAmount,
BigDecimal refundAmount, String reason, Integer tenantId) throws PaymentException {
// TODO: 实现微信支付退款逻辑
throw PaymentException.unsupportedPayment("暂不支持微信支付退款", PaymentType.WECHAT_NATIVE);
log.info("{}, 支付类型: {}, 订单号: {}, 退款单号: {}, 退款金额: {}, 租户ID: {}",
PaymentConstants.LogMessage.REFUND_START, getSupportedPaymentType(),
orderNo, refundNo, refundAmount, tenantId);
try {
// 参数验证
validateRefundRequest(orderNo, refundNo, totalAmount, refundAmount, tenantId);
// 获取支付配置
Payment paymentConfig = wxPayConfigService.getPaymentConfigForStrategy(tenantId);
Config wxPayConfig = wxPayConfigService.getWxPayConfig(tenantId);
// 构建退款请求
CreateRequest refundRequest = buildRefundRequest(
orderNo, refundNo, totalAmount, refundAmount, reason, paymentConfig);
// 调用微信退款API
Refund refundResult = callWechatRefundApi(refundRequest, wxPayConfig);
// 构建响应
PaymentResponse response = buildRefundResponse(refundResult, orderNo, refundNo, tenantId);
log.info("{}, 支付类型: {}, 订单号: {}, 退款单号: {}, 微信退款单号: {}",
PaymentConstants.LogMessage.REFUND_SUCCESS, getSupportedPaymentType(),
orderNo, refundNo, refundResult.getRefundId());
return response;
} catch (PaymentException e) {
log.error("{}, 支付类型: {}, 订单号: {}, 退款单号: {}, 错误: {}",
PaymentConstants.LogMessage.REFUND_FAILED, getSupportedPaymentType(),
orderNo, refundNo, e.getMessage());
throw e;
} catch (Exception e) {
log.error("{}, 支付类型: {}, 订单号: {}, 退款单号: {}, 系统错误: {}",
PaymentConstants.LogMessage.REFUND_FAILED, getSupportedPaymentType(),
orderNo, refundNo, e.getMessage(), e);
throw PaymentException.systemError("微信支付退款失败: " + e.getMessage(), e);
}
}
@Override
public PaymentResponse queryRefund(String refundNo, Integer tenantId) throws PaymentException {
// TODO: 实现微信退款查询逻辑
throw PaymentException.unsupportedPayment("暂不支持微信退款查询", PaymentType.WECHAT_NATIVE);
log.info("开始查询微信退款状态, 退款单号: {}, 租户ID: {}", refundNo, tenantId);
try {
// 参数验证
if (!StringUtils.hasText(refundNo)) {
throw PaymentException.paramError("退款单号不能为空");
}
if (tenantId == null) {
throw PaymentException.paramError("租户ID不能为空");
}
// 获取支付配置
Config wxPayConfig = wxPayConfigService.getWxPayConfig(tenantId);
// 调用微信退款查询API
Refund refundResult = queryWechatRefundStatus(refundNo, wxPayConfig);
// 构建响应
PaymentResponse response = buildRefundQueryResponse(refundResult, tenantId);
log.info("微信退款状态查询成功, 退款单号: {}, 状态: {}, 微信退款单号: {}",
refundNo, refundResult.getStatus(), refundResult.getRefundId());
return response;
} catch (PaymentException e) {
throw e;
} catch (Exception e) {
log.error("查询微信退款状态失败, 退款单号: {}, 租户ID: {}, 错误: {}",
refundNo, tenantId, e.getMessage(), e);
throw PaymentException.systemError("查询微信退款状态失败: " + e.getMessage(), e);
}
}
@Override
@@ -398,4 +467,200 @@ public class WechatNativeStrategy implements PaymentStrategy {
throw PaymentException.networkError("调用微信支付API失败: " + e.getMessage(), PaymentType.WECHAT_NATIVE, e);
}
}
/**
* 验证退款请求参数
*/
private void validateRefundRequest(String orderNo, String refundNo, BigDecimal totalAmount,
BigDecimal refundAmount, Integer tenantId) throws PaymentException {
if (!StringUtils.hasText(orderNo)) {
throw PaymentException.paramError("订单号不能为空");
}
if (!StringUtils.hasText(refundNo)) {
throw PaymentException.paramError("退款单号不能为空");
}
if (tenantId == null) {
throw PaymentException.paramError("租户ID不能为空");
}
if (totalAmount == null || totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw PaymentException.amountError("订单总金额必须大于0");
}
if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw PaymentException.amountError("退款金额必须大于0");
}
if (refundAmount.compareTo(totalAmount) > 0) {
throw PaymentException.amountError("退款金额不能大于订单总金额");
}
log.debug("退款请求参数验证通过, 订单号: {}, 退款单号: {}, 订单总额: {}, 退款金额: {}",
orderNo, refundNo, totalAmount, refundAmount);
}
/**
* 构建微信退款请求
*/
private CreateRequest buildRefundRequest(String orderNo, String refundNo, BigDecimal totalAmount,
BigDecimal refundAmount, String reason, Payment paymentConfig) {
CreateRequest refundRequest = new CreateRequest();
// 设置订单号和退款单号
refundRequest.setOutTradeNo(orderNo);
refundRequest.setOutRefundNo(refundNo);
// 设置金额信息(微信支付金额单位为分)
AmountReq amountReq = new AmountReq();
amountReq.setTotal(totalAmount.multiply(new BigDecimal("100")).longValue());
amountReq.setRefund(refundAmount.multiply(new BigDecimal("100")).longValue());
amountReq.setCurrency(PaymentConstants.Wechat.CURRENCY);
refundRequest.setAmount(amountReq);
// 设置退款原因(可选,但建议填写)
if (StringUtils.hasText(reason)) {
// 退款原因最多80个字符
String effectiveReason = CommonUtil.truncateString(reason, 80);
refundRequest.setReason(effectiveReason);
} else {
refundRequest.setReason("用户申请退款");
}
log.info("构建微信退款请求完成 - 订单号: {}, 退款单号: {}, 订单总额: {}分, 退款金额: {}分",
orderNo, refundNo, amountReq.getTotal(), amountReq.getRefund());
return refundRequest;
}
/**
* 调用微信退款API
*/
private Refund callWechatRefundApi(CreateRequest refundRequest, Config wxPayConfig) throws PaymentException {
try {
// 构建退款服务
RefundService refundService = new RefundService.Builder().config(wxPayConfig).build();
// 调用退款接口
Refund refund = refundService.create(refundRequest);
if (refund == null) {
throw PaymentException.networkError("微信退款API返回数据异常", PaymentType.WECHAT_NATIVE, null);
}
log.debug("微信退款API调用成功, 退款单号: {}, 微信退款单号: {}, 状态: {}",
refundRequest.getOutRefundNo(), refund.getRefundId(), refund.getStatus());
return refund;
} catch (Exception e) {
if (e instanceof PaymentException) {
throw e;
}
log.error("调用微信退款API失败: {}", e.getMessage(), e);
throw PaymentException.networkError("调用微信退款API失败: " + e.getMessage(), PaymentType.WECHAT_NATIVE, e);
}
}
/**
* 构建退款响应
*/
private PaymentResponse buildRefundResponse(Refund refund, String orderNo, String refundNo, Integer tenantId) {
PaymentResponse response = new PaymentResponse();
response.setSuccess(true);
response.setOrderNo(orderNo);
response.setTransactionId(refund.getRefundId()); // 使用微信退款单号
response.setPaymentType(PaymentType.WECHAT_NATIVE);
response.setTenantId(tenantId);
// 转换退款状态
PaymentStatus paymentStatus = convertWechatRefundStatus(refund.getStatus());
response.setPaymentStatus(paymentStatus);
// 设置退款金额(微信返回的金额是分,需要转换为元)
if (refund.getAmount() != null && refund.getAmount().getRefund() != null) {
BigDecimal refundAmount = new BigDecimal(refund.getAmount().getRefund()).divide(new BigDecimal("100"));
response.setAmount(refundAmount);
}
log.debug("构建退款响应完成 - 订单号: {}, 退款单号: {}, 微信退款单号: {}, 状态: {}",
orderNo, refundNo, refund.getRefundId(), paymentStatus);
return response;
}
/**
* 查询微信退款状态
*/
private Refund queryWechatRefundStatus(String refundNo, Config wxPayConfig) throws PaymentException {
try {
// 构建退款服务
RefundService refundService = new RefundService.Builder().config(wxPayConfig).build();
// 构建查询请求
QueryByOutRefundNoRequest queryRequest = new QueryByOutRefundNoRequest();
queryRequest.setOutRefundNo(refundNo);
// 调用查询接口
Refund refund = refundService.queryByOutRefundNo(queryRequest);
if (refund == null) {
throw PaymentException.systemError("微信退款查询返回空结果", null);
}
log.debug("微信退款查询成功, 退款单号: {}, 微信退款单号: {}, 状态: {}",
refundNo, refund.getRefundId(), refund.getStatus());
return refund;
} catch (Exception e) {
if (e instanceof PaymentException) {
throw e;
}
log.error("查询微信退款状态失败, 退款单号: {}, 错误: {}", refundNo, e.getMessage(), e);
throw PaymentException.networkError("查询微信退款状态失败: " + e.getMessage(), PaymentType.WECHAT_NATIVE, e);
}
}
/**
* 构建退款查询响应
*/
private PaymentResponse buildRefundQueryResponse(Refund refund, Integer tenantId) {
PaymentResponse response = new PaymentResponse();
response.setSuccess(true);
response.setOrderNo(refund.getOutTradeNo());
response.setTransactionId(refund.getRefundId());
response.setPaymentType(PaymentType.WECHAT_NATIVE);
response.setTenantId(tenantId);
// 转换退款状态
PaymentStatus paymentStatus = convertWechatRefundStatus(refund.getStatus());
response.setPaymentStatus(paymentStatus);
// 设置退款金额
if (refund.getAmount() != null && refund.getAmount().getRefund() != null) {
BigDecimal refundAmount = new BigDecimal(refund.getAmount().getRefund()).divide(new BigDecimal("100"));
response.setAmount(refundAmount);
}
return response;
}
/**
* 转换微信退款状态
*/
private PaymentStatus convertWechatRefundStatus(Refund.Status refundStatus) {
if (refundStatus == null) {
return PaymentStatus.REFUNDING;
}
switch (refundStatus) {
case SUCCESS:
return PaymentStatus.REFUNDED;
case CLOSED:
return PaymentStatus.REFUND_FAILED;
case PROCESSING:
return PaymentStatus.REFUNDING;
case ABNORMAL:
return PaymentStatus.REFUND_FAILED;
default:
return PaymentStatus.REFUNDING;
}
}
}

View File

@@ -24,6 +24,9 @@ import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.param.ShopOrderParam;
import com.gxwebsoft.shop.dto.OrderCreateRequest;
import com.gxwebsoft.shop.dto.UpdatePaymentStatusRequest;
import com.gxwebsoft.payment.service.PaymentService;
import com.gxwebsoft.payment.dto.PaymentResponse;
import com.gxwebsoft.payment.enums.PaymentType;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.BatchParam;
@@ -95,6 +98,8 @@ public class ShopOrderController extends BaseController {
private ShopUserAddressService shopUserAddressService;
@Resource
private ShopOrderDeliveryService shopOrderDeliveryService;
@Resource
private PaymentService paymentService;
@Operation(summary = "分页查询订单")
@GetMapping("/page")
@@ -192,6 +197,103 @@ public class ShopOrderController extends BaseController {
shopOrderDeliveryService.setExpress(getLoginUser(), shopOrderDelivery, shopOrder);
}
// 退款操作
if(shopOrder.getOrderStatus().equals(6)){
// 当订单状态更改为6已退款执行退款操作
try {
// 1. 验证订单是否可以退款
if (shopOrderNow == null) {
return fail("订单不存在");
}
// 检查订单是否已支付
if (shopOrderNow.getPayStatus() == null || !shopOrderNow.getPayStatus().equals(1)) {
return fail("订单未支付,无法退款");
}
// 检查是否已经退款过了
if (StrUtil.isNotBlank(shopOrderNow.getRefundOrder())) {
logger.warn("订单已经退款过,订单号: {}, 退款单号: {}", shopOrderNow.getOrderNo(), shopOrderNow.getRefundOrder());
return fail("订单已退款,请勿重复操作");
}
// 2. 生成退款单号
String refundNo = "RF" + IdUtil.getSnowflakeNextId();
// 3. 确定退款金额(默认全额退款)
BigDecimal refundAmount = shopOrder.getRefundMoney();
if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
// 如果没有指定退款金额,使用订单实付金额
refundAmount = shopOrderNow.getTotalPrice();
}
// 验证退款金额不能大于订单金额
if (refundAmount.compareTo(shopOrderNow.getTotalPrice()) > 0) {
return fail("退款金额不能大于订单金额");
}
// 4. 确定支付类型默认为微信Native支付
PaymentType paymentType = PaymentType.WECHAT_NATIVE;
if (shopOrderNow.getPayType() != null) {
// 根据订单的支付类型确定
paymentType = PaymentType.fromCode(shopOrderNow.getPayType());
}
// 5. 调用统一支付服务的退款接口
logger.info("开始处理订单退款 - 订单号: {}, 退款单号: {}, 退款金额: {}, 支付方式: {}",
shopOrderNow.getOrderNo(), refundNo, refundAmount, paymentType);
PaymentResponse refundResponse = paymentService.refund(
shopOrderNow.getOrderNo(), // 原订单号
refundNo, // 退款单号
shopOrderNow.getTotalPrice(), // 订单总金额
refundAmount, // 退款金额
shopOrder.getRefundReason() != null ? shopOrder.getRefundReason() : "用户申请退款", // 退款原因
paymentType, // 支付方式
shopOrderNow.getTenantId() // 租户ID
);
// 6. 处理退款结果
if (refundResponse.getSuccess()) {
// 退款成功,更新订单信息
shopOrder.setRefundOrder(refundNo);
shopOrder.setRefundMoney(refundAmount);
shopOrder.setRefundTime(LocalDateTime.now());
// 根据退款状态决定订单状态
// 如果微信返回退款处理中则设置订单状态为5退款处理中
// 如果微信返回退款成功则保持状态为6退款成功
if (refundResponse.getPaymentStatus() != null) {
switch (refundResponse.getPaymentStatus()) {
case REFUNDING:
shopOrder.setOrderStatus(5); // 退款处理中
logger.info("订单退款处理中,订单号: {}, 退款单号: {}", shopOrderNow.getOrderNo(), refundNo);
break;
case REFUNDED:
shopOrder.setOrderStatus(6); // 退款成功
logger.info("订单退款成功,订单号: {}, 退款单号: {}", shopOrderNow.getOrderNo(), refundNo);
break;
case REFUND_FAILED:
logger.error("订单退款失败,订单号: {}, 退款单号: {}", shopOrderNow.getOrderNo(), refundNo);
return fail("退款失败,请联系管理员");
default:
shopOrder.setOrderStatus(5); // 默认为退款处理中
}
}
logger.info("订单退款请求成功 - 订单号: {}, 退款单号: {}, 微信退款单号: {}",
shopOrderNow.getOrderNo(), refundNo, refundResponse.getTransactionId());
} else {
// 退款失败
logger.error("订单退款失败 - 订单号: {}, 错误: {}", shopOrderNow.getOrderNo(), refundResponse.getErrorMessage());
return fail("退款失败: " + refundResponse.getErrorMessage());
}
} catch (Exception e) {
logger.error("处理订单退款异常 - 订单号: {}, 错误: {}", shopOrderNow.getOrderNo(), e.getMessage(), e);
return fail("退款处理异常: " + e.getMessage());
}
}
if (shopOrderService.updateById(shopOrder)) {
return success("修改成功");
}

View File

@@ -0,0 +1,130 @@
package com.gxwebsoft.shop.task;
import com.gxwebsoft.common.core.annotation.IgnoreTenant;
import com.gxwebsoft.payment.dto.PaymentResponse;
import com.gxwebsoft.payment.enums.PaymentType;
import com.gxwebsoft.payment.exception.PaymentException;
import com.gxwebsoft.payment.service.PaymentService;
import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.service.ShopOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 退款状态查询定时任务
* 定期查询处理中的退款订单状态,并更新订单状态
*
* @author WebSoft
* @since 2025-01-26
*/
@Slf4j
@Component
@ConditionalOnProperty(prefix = "shop.order.refund-query", name = "enabled", havingValue = "true", matchIfMissing = true)
public class RefundStatusQueryTask {
@Autowired
private ShopOrderService shopOrderService;
@Autowired
private PaymentService paymentService;
/**
* 查询退款状态并更新订单
* 生产环境每10分钟执行一次
* 开发环境每2分钟执行一次便于测试
*/
@Scheduled(cron = "${shop.order.refund-query.cron:0 0/10 * * * ?}")
@IgnoreTenant("定时任务需要处理所有租户的退款订单")
public void queryRefundStatusAndUpdateOrders() {
log.info("开始执行退款状态查询任务...");
try {
long startTime = System.currentTimeMillis();
int totalProcessedCount = 0;
// 查询所有退款处理中的订单状态为5
List<ShopOrder> refundProcessingOrders = shopOrderService.getRefundProcessingOrders();
if (refundProcessingOrders.isEmpty()) {
log.info("没有找到退款处理中的订单");
return;
}
log.info("找到 {} 个退款处理中的订单,开始查询退款状态...", refundProcessingOrders.size());
int successCount = 0;
int failedCount = 0;
for (ShopOrder order : refundProcessingOrders) {
try {
// 查询退款状态
PaymentResponse response = paymentService.queryRefund(
order.getRefundOrder(), // 退款单号
PaymentType.WECHAT_NATIVE, // 支付类型(这里假设是微信支付)
order.getTenantId() // 租户ID
);
if (response != null) {
// 根据退款状态更新订单
if (response.getSuccess()) {
// 退款成功
order.setOrderStatus(6); // 退款成功
shopOrderService.updateById(order);
log.info("订单 {} 退款成功,更新订单状态为退款成功", order.getOrderNo());
successCount++;
} else if (response.getPaymentStatus() != null) {
// 根据具体的退款状态处理
switch (response.getPaymentStatus()) {
case REFUND_FAILED:
// 退款失败
order.setOrderStatus(5); // 退款被拒绝(保持原状态或根据业务需要调整)
shopOrderService.updateById(order);
log.info("订单 {} 退款失败,更新订单状态", order.getOrderNo());
failedCount++;
break;
case REFUNDING:
// 仍在退款中,无需处理
log.debug("订单 {} 仍在退款中", order.getOrderNo());
break;
default:
log.warn("订单 {} 未知的退款状态: {}", order.getOrderNo(), response.getPaymentStatus());
break;
}
}
} else {
log.warn("订单 {} 退款状态查询返回空结果", order.getOrderNo());
}
} catch (PaymentException e) {
log.error("查询订单 {} 退款状态时发生支付异常: {}", order.getOrderNo(), e.getMessage(), e);
failedCount++;
} catch (Exception e) {
log.error("查询订单 {} 退款状态时发生系统异常: {}", order.getOrderNo(), e.getMessage(), e);
failedCount++;
}
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
log.info("退款状态查询任务完成,总处理数量: {},成功: {},失败: {},耗时: {}ms",
refundProcessingOrders.size(), successCount, failedCount, duration);
} catch (Exception e) {
log.error("退款状态查询任务执行失败", e);
}
}
/**
* 手动触发退款状态查询任务(用于测试)
*/
@IgnoreTenant("手动触发的定时任务需要处理所有租户的退款订单")
public void manualQueryRefundStatus() {
log.info("手动触发退款状态查询任务...");
queryRefundStatusAndUpdateOrders();
}
}