diff --git a/src/main/java/com/gxwebsoft/payment/strategy/WechatNativeStrategy.java b/src/main/java/com/gxwebsoft/payment/strategy/WechatNativeStrategy.java index 6c43eaa..36a7d89 100644 --- a/src/main/java/com/gxwebsoft/payment/strategy/WechatNativeStrategy.java +++ b/src/main/java/com/gxwebsoft/payment/strategy/WechatNativeStrategy.java @@ -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; + } + } } diff --git a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java b/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java index 74718f5..72032fc 100644 --- a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java +++ b/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java @@ -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("修改成功"); } diff --git a/src/main/java/com/gxwebsoft/shop/task/RefundStatusQueryTask.java b/src/main/java/com/gxwebsoft/shop/task/RefundStatusQueryTask.java new file mode 100644 index 0000000..9d5af16 --- /dev/null +++ b/src/main/java/com/gxwebsoft/shop/task/RefundStatusQueryTask.java @@ -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 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(); + } +} \ No newline at end of file