refactor(payment): 重构支付服务实现类
- 为 PaymentServiceImpl 类添加统一支付服务标识 - 在 PaymentController 中明确引用统一支付服务
This commit is contained in:
@@ -1,108 +0,0 @@
|
|||||||
package com.gxwebsoft.shop.config;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信支付配置属性
|
|
||||||
* 管理微信支付相关的配置项
|
|
||||||
*
|
|
||||||
* @author 科技小王子
|
|
||||||
* @since 2025-01-26
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Component
|
|
||||||
@ConfigurationProperties(prefix = "wx.pay")
|
|
||||||
public class WxPayProperties {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 服务器URL(用于构建回调地址)
|
|
||||||
*/
|
|
||||||
private String serverUrl = "https://server.gxwebsoft.com";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 回调路径模板
|
|
||||||
*/
|
|
||||||
private String notifyUrlTemplate = "/api/system/wx-native-pay/notify/{tenantId}";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 订单超时时间(分钟)
|
|
||||||
*/
|
|
||||||
private int orderTimeoutMinutes = 30;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否启用签名验证
|
|
||||||
*/
|
|
||||||
private boolean enableSignatureVerification = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否启用金额验证
|
|
||||||
*/
|
|
||||||
private boolean enableAmountVerification = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 最大重试次数
|
|
||||||
*/
|
|
||||||
private int maxRetryCount = 3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重试间隔(毫秒)
|
|
||||||
*/
|
|
||||||
private long retryIntervalMs = 1000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否启用详细日志
|
|
||||||
*/
|
|
||||||
private boolean enableDetailedLogging = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试环境配置
|
|
||||||
*/
|
|
||||||
private TestConfig test = new TestConfig();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试环境配置
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public static class TestConfig {
|
|
||||||
/**
|
|
||||||
* 测试商户号
|
|
||||||
*/
|
|
||||||
private String merchantId = "1246610101";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试商户序列号
|
|
||||||
*/
|
|
||||||
private String merchantSerialNumber = "2903B872D5CA36E525FAEC37AEDB22E54ECDE7B7";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否启用测试模式
|
|
||||||
*/
|
|
||||||
private boolean enabled = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试金额(分)
|
|
||||||
*/
|
|
||||||
private int testAmount = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建完整的回调URL
|
|
||||||
*
|
|
||||||
* @param tenantId 租户ID
|
|
||||||
* @return 完整的回调URL
|
|
||||||
*/
|
|
||||||
public String buildNotifyUrl(Integer tenantId) {
|
|
||||||
return serverUrl + notifyUrlTemplate.replace("{tenantId}", tenantId.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取订单超时时间(秒)
|
|
||||||
*
|
|
||||||
* @return 超时时间(秒)
|
|
||||||
*/
|
|
||||||
public long getOrderTimeoutSeconds() {
|
|
||||||
return orderTimeoutMinutes * 60L;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
package com.gxwebsoft.shop.controller;
|
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
|
||||||
import com.gxwebsoft.common.core.utils.CommonUtil;
|
|
||||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
|
||||||
import com.gxwebsoft.common.core.web.ApiResult;
|
|
||||||
import com.gxwebsoft.common.core.web.BaseController;
|
|
||||||
import com.gxwebsoft.common.system.entity.Payment;
|
|
||||||
import com.gxwebsoft.common.system.service.SettingService;
|
|
||||||
import com.gxwebsoft.shop.constants.WxPayConstants;
|
|
||||||
import com.gxwebsoft.shop.entity.ShopOrder;
|
|
||||||
import com.gxwebsoft.shop.exception.WxPayException;
|
|
||||||
import com.gxwebsoft.shop.service.ShopOrderService;
|
|
||||||
import com.gxwebsoft.shop.config.WxPayProperties;
|
|
||||||
import com.gxwebsoft.shop.dto.WxPayRequest;
|
|
||||||
import com.gxwebsoft.shop.service.WxPayConfigService;
|
|
||||||
import com.gxwebsoft.shop.service.WxPayNotifyService;
|
|
||||||
import com.wechat.pay.java.core.Config;
|
|
||||||
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
|
|
||||||
import com.wechat.pay.java.service.payments.nativepay.model.Amount;
|
|
||||||
import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest;
|
|
||||||
import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.validation.annotation.Validated;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import javax.validation.Valid;
|
|
||||||
import javax.validation.constraints.NotNull;
|
|
||||||
import javax.validation.constraints.Positive;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Validated
|
|
||||||
@Tag(name = "微信Native支付接口")
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/system/wx-native-pay")
|
|
||||||
public class WxNativePayController extends BaseController {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private RedisUtil redisUtil;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private SettingService settingService;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private ShopOrderService shopOrderService;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private WxPayConfigService wxPayConfigService;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private WxPayNotifyService wxPayNotifyService;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private WxPayProperties wxPayProperties;
|
|
||||||
|
|
||||||
|
|
||||||
@Operation(summary = "生成付款码")
|
|
||||||
@PostMapping("/codeUrl")
|
|
||||||
public ApiResult<?> getCodeUrl(@Valid @RequestBody WxPayRequest payRequest) {
|
|
||||||
log.info("{}, 租户ID: {}, 金额: {}",
|
|
||||||
WxPayConstants.LogMessage.PAY_REQUEST_START, payRequest.getTenantId(), payRequest.getFormattedAmount());
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 设置当前租户ID(从请求参数中获取)
|
|
||||||
setCurrentTenantId(payRequest.getTenantId());
|
|
||||||
|
|
||||||
// 获取支付配置
|
|
||||||
Payment payment = getPaymentConfig();
|
|
||||||
|
|
||||||
// 获取微信小程序配置
|
|
||||||
String appId = getWxAppId(payRequest.getTenantId());
|
|
||||||
|
|
||||||
// 准备订单数据
|
|
||||||
ShopOrder order = buildOrderFromRequest(payRequest);
|
|
||||||
|
|
||||||
// 创建支付请求
|
|
||||||
PrepayRequest request = buildPrepayRequest(order, payment, appId);
|
|
||||||
|
|
||||||
// 调用微信支付API
|
|
||||||
PrepayResponse response = callWxPayApi(request);
|
|
||||||
|
|
||||||
log.info("{}, 租户ID: {}, 订单号: {}, 金额: {}",
|
|
||||||
WxPayConstants.LogMessage.PAY_REQUEST_SUCCESS,
|
|
||||||
payRequest.getTenantId(), order.getOrderNo(), payRequest.getFormattedAmount());
|
|
||||||
|
|
||||||
return success("生成付款码", response.getCodeUrl());
|
|
||||||
|
|
||||||
} catch (WxPayException e) {
|
|
||||||
log.error("{}, 租户ID: {}, 错误: {}",
|
|
||||||
WxPayConstants.LogMessage.PAY_REQUEST_FAILED, payRequest.getTenantId(), e.getMessage());
|
|
||||||
if (wxPayProperties.isEnableDetailedLogging()) {
|
|
||||||
log.error("详细错误信息", e);
|
|
||||||
}
|
|
||||||
return fail(e.getMessage());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("{}, 租户ID: {}, 系统错误: {}",
|
|
||||||
WxPayConstants.LogMessage.PAY_REQUEST_FAILED, payRequest.getTenantId(), e.getMessage(), e);
|
|
||||||
return fail(WxPayConstants.ErrorMessage.SYSTEM_INTERNAL_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置当前租户ID(用于多租户上下文)
|
|
||||||
*/
|
|
||||||
private void setCurrentTenantId(Integer tenantId) {
|
|
||||||
// 这里可以设置到ThreadLocal或其他上下文中
|
|
||||||
// 具体实现取决于你的多租户架构
|
|
||||||
log.debug("设置当前租户ID: {}", tenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从支付请求构建订单对象
|
|
||||||
*/
|
|
||||||
private ShopOrder buildOrderFromRequest(WxPayRequest payRequest) {
|
|
||||||
ShopOrder order = new ShopOrder();
|
|
||||||
|
|
||||||
// 基本信息
|
|
||||||
order.setTenantId(payRequest.getTenantId());
|
|
||||||
order.setPayPrice(payRequest.getPayPrice());
|
|
||||||
order.setMoney(payRequest.getPayPrice());
|
|
||||||
order.setTotalPrice(payRequest.getPayPrice());
|
|
||||||
order.setComments(payRequest.getEffectiveDescription());
|
|
||||||
|
|
||||||
// 生成订单号
|
|
||||||
if (StringUtils.hasText(payRequest.getOrderNo())) {
|
|
||||||
order.setOrderNo(payRequest.getOrderNo());
|
|
||||||
} else {
|
|
||||||
order.setOrderNo(CommonUtil.createOrderNo());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 其他字段
|
|
||||||
order.setUserId(payRequest.getUserId());
|
|
||||||
order.setFormId(payRequest.getGoodsId());
|
|
||||||
order.setTotalNum(payRequest.getQuantity());
|
|
||||||
order.setType(payRequest.getOrderType());
|
|
||||||
order.setDeliveryType(payRequest.getDeliveryType());
|
|
||||||
order.setChannel(payRequest.getChannel());
|
|
||||||
order.setBuyerRemarks(payRequest.getBuyerRemarks());
|
|
||||||
order.setAddressId(payRequest.getAddressId());
|
|
||||||
order.setSelfTakeMerchantId(payRequest.getSelfTakeMerchantId());
|
|
||||||
|
|
||||||
// 设置默认值
|
|
||||||
order.setPayStatus(false);
|
|
||||||
order.setOrderStatus(0);
|
|
||||||
order.setPayType(102); // 微信Native支付
|
|
||||||
|
|
||||||
log.debug("从支付请求构建订单完成, 订单号: {}, 金额: {}",
|
|
||||||
order.getOrderNo(), order.getPayPrice());
|
|
||||||
|
|
||||||
return order;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取支付配置
|
|
||||||
*/
|
|
||||||
private Payment getPaymentConfig() throws WxPayException {
|
|
||||||
String cacheKey = WxPayConstants.CacheKey.PAYMENT_CONFIG_PREFIX + getTenantId();
|
|
||||||
Payment payment = redisUtil.get(cacheKey, Payment.class);
|
|
||||||
|
|
||||||
if (payment == null) {
|
|
||||||
throw WxPayException.configError(WxPayConstants.ErrorMessage.CONFIG_NOT_FOUND, getTenantId());
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("获取支付配置成功, 租户ID: {}", getTenantId());
|
|
||||||
return payment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取微信小程序AppId
|
|
||||||
*/
|
|
||||||
private String getWxAppId(Integer tenantId) throws WxPayException {
|
|
||||||
JSONObject setting = settingService.getBySettingKey(WxPayConstants.CacheKey.MP_WEIXIN_CONFIG, tenantId);
|
|
||||||
if (setting == null) {
|
|
||||||
throw WxPayException.configError("微信小程序配置未找到", tenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
String appId = setting.getString("appId");
|
|
||||||
if (!StringUtils.hasText(appId)) {
|
|
||||||
throw WxPayException.configError("微信小程序AppId未配置", tenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("获取微信AppId成功, 租户ID: {}", tenantId);
|
|
||||||
return appId;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建预支付请求
|
|
||||||
*/
|
|
||||||
private PrepayRequest buildPrepayRequest(ShopOrder order, Payment payment, String appId) throws WxPayException {
|
|
||||||
PrepayRequest request = new PrepayRequest();
|
|
||||||
|
|
||||||
// 设置金额(转换为分)
|
|
||||||
BigDecimal amountYuan = order.getMoney();
|
|
||||||
int amountFen = amountYuan.multiply(new BigDecimal(WxPayConstants.Config.AMOUNT_MULTIPLIER)).intValue();
|
|
||||||
|
|
||||||
Amount amount = new Amount();
|
|
||||||
amount.setTotal(amountFen);
|
|
||||||
amount.setCurrency(WxPayConstants.Config.CURRENCY_CNY);
|
|
||||||
|
|
||||||
request.setAmount(amount);
|
|
||||||
request.setAppid(appId);
|
|
||||||
request.setMchid(payment.getMchId());
|
|
||||||
request.setDescription(order.getComments());
|
|
||||||
request.setOutTradeNo(order.getOrderNo());
|
|
||||||
|
|
||||||
// 构建回调URL
|
|
||||||
String notifyUrl = wxPayProperties.buildNotifyUrl(getTenantId());
|
|
||||||
request.setNotifyUrl(notifyUrl);
|
|
||||||
|
|
||||||
log.debug("预支付请求构建完成, 订单号: {}, 金额: {}分, 回调URL: {}",
|
|
||||||
order.getOrderNo(), amountFen, notifyUrl);
|
|
||||||
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 调用微信支付API
|
|
||||||
*/
|
|
||||||
private PrepayResponse callWxPayApi(PrepayRequest request) throws WxPayException {
|
|
||||||
try {
|
|
||||||
// 获取微信支付配置
|
|
||||||
Config wxPayConfig = wxPayConfigService.getWxPayConfig(getTenantId());
|
|
||||||
|
|
||||||
// 构建服务
|
|
||||||
NativePayService service = new NativePayService.Builder().config(wxPayConfig).build();
|
|
||||||
|
|
||||||
// 调用预支付接口
|
|
||||||
PrepayResponse response = service.prepay(request);
|
|
||||||
|
|
||||||
if (response == null || !StringUtils.hasText(response.getCodeUrl())) {
|
|
||||||
throw WxPayException.networkError("微信支付API返回数据异常", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("微信支付API调用成功, 订单号: {}", request.getOutTradeNo());
|
|
||||||
return response;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (e instanceof WxPayException) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
throw WxPayException.networkError("调用微信支付API失败: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "异步通知")
|
|
||||||
@PostMapping("/notify/{tenantId}")
|
|
||||||
public String wxNotify(@RequestHeader Map<String, String> headers,
|
|
||||||
@RequestBody String body,
|
|
||||||
@PathVariable("tenantId") Integer tenantId) {
|
|
||||||
|
|
||||||
log.info("收到微信支付回调通知, 租户ID: {}", tenantId);
|
|
||||||
|
|
||||||
// 委托给专门的回调处理服务
|
|
||||||
return wxPayNotifyService.handlePaymentNotify(headers, body, tenantId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package com.gxwebsoft.shop.dto;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import javax.validation.constraints.*;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信支付请求DTO
|
|
||||||
* 用于接收和验证微信支付请求参数
|
|
||||||
*
|
|
||||||
* @author 科技小王子
|
|
||||||
* @since 2025-01-26
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Schema(name = "微信支付请求", description = "微信支付请求参数")
|
|
||||||
public class WxPayRequest {
|
|
||||||
|
|
||||||
@Schema(description = "租户ID", required = true)
|
|
||||||
@NotNull(message = "租户ID不能为空")
|
|
||||||
@Positive(message = "租户ID必须为正数")
|
|
||||||
private Integer tenantId;
|
|
||||||
|
|
||||||
@Schema(description = "支付金额", required = true, example = "0.01")
|
|
||||||
@NotNull(message = "支付金额不能为空")
|
|
||||||
@DecimalMin(value = "0.01", message = "支付金额必须大于0.01元")
|
|
||||||
@DecimalMax(value = "999999.99", message = "支付金额不能超过999999.99元")
|
|
||||||
@Digits(integer = 6, fraction = 2, message = "支付金额格式不正确,最多6位整数2位小数")
|
|
||||||
private BigDecimal payPrice;
|
|
||||||
|
|
||||||
@Schema(description = "订单描述", example = "商品订单")
|
|
||||||
@Size(max = 127, message = "订单描述不能超过127个字符")
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
@Schema(description = "订单号(可选,不提供则自动生成)")
|
|
||||||
@Size(max = 32, message = "订单号不能超过32个字符")
|
|
||||||
@Pattern(regexp = "^[a-zA-Z0-9_-]*$", message = "订单号只能包含字母、数字、下划线和横线")
|
|
||||||
private String orderNo;
|
|
||||||
|
|
||||||
@Schema(description = "用户ID")
|
|
||||||
@Positive(message = "用户ID必须为正数")
|
|
||||||
private Integer userId;
|
|
||||||
|
|
||||||
@Schema(description = "商品ID")
|
|
||||||
@Positive(message = "商品ID必须为正数")
|
|
||||||
private Integer goodsId;
|
|
||||||
|
|
||||||
@Schema(description = "购买数量", example = "1")
|
|
||||||
@Min(value = 1, message = "购买数量必须大于0")
|
|
||||||
@Max(value = 9999, message = "购买数量不能超过9999")
|
|
||||||
private Integer quantity = 1;
|
|
||||||
|
|
||||||
@Schema(description = "订单类型", example = "0")
|
|
||||||
@Min(value = 0, message = "订单类型不能为负数")
|
|
||||||
@Max(value = 10, message = "订单类型值超出范围")
|
|
||||||
private Integer orderType = 0;
|
|
||||||
|
|
||||||
@Schema(description = "配送方式", example = "1")
|
|
||||||
@Min(value = 0, message = "配送方式不能为负数")
|
|
||||||
private Integer deliveryType;
|
|
||||||
|
|
||||||
@Schema(description = "下单渠道", example = "0")
|
|
||||||
@Min(value = 0, message = "下单渠道不能为负数")
|
|
||||||
private Integer channel = 0;
|
|
||||||
|
|
||||||
@Schema(description = "买家备注")
|
|
||||||
@Size(max = 500, message = "买家备注不能超过500个字符")
|
|
||||||
private String buyerRemarks;
|
|
||||||
|
|
||||||
@Schema(description = "收货地址ID")
|
|
||||||
@Positive(message = "收货地址ID必须为正数")
|
|
||||||
private Integer addressId;
|
|
||||||
|
|
||||||
@Schema(description = "自提店铺ID")
|
|
||||||
@Positive(message = "自提店铺ID必须为正数")
|
|
||||||
private Integer selfTakeMerchantId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取有效的订单描述
|
|
||||||
* 如果没有提供描述,返回默认值
|
|
||||||
*/
|
|
||||||
public String getEffectiveDescription() {
|
|
||||||
if (description == null || description.trim().isEmpty()) {
|
|
||||||
return "商品订单";
|
|
||||||
}
|
|
||||||
return description.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证必要参数是否完整
|
|
||||||
*/
|
|
||||||
public boolean isValid() {
|
|
||||||
return tenantId != null && tenantId > 0
|
|
||||||
&& payPrice != null && payPrice.compareTo(BigDecimal.ZERO) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取格式化的金额字符串
|
|
||||||
*/
|
|
||||||
public String getFormattedAmount() {
|
|
||||||
if (payPrice == null) {
|
|
||||||
return "0.00";
|
|
||||||
}
|
|
||||||
return String.format("%.2f", payPrice);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 转换为分(微信支付API需要)
|
|
||||||
*/
|
|
||||||
public Integer getAmountInCents() {
|
|
||||||
if (payPrice == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return payPrice.multiply(new BigDecimal(100)).intValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
package com.gxwebsoft.shop.exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信支付异常类
|
|
||||||
* 用于处理微信支付相关的业务异常
|
|
||||||
*
|
|
||||||
* @author 科技小王子
|
|
||||||
* @since 2025-01-26
|
|
||||||
*/
|
|
||||||
public class WxPayException extends Exception {
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 错误代码
|
|
||||||
*/
|
|
||||||
private String errorCode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 租户ID(可选)
|
|
||||||
*/
|
|
||||||
private Integer tenantId;
|
|
||||||
|
|
||||||
public WxPayException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public WxPayException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
public WxPayException(String errorCode, String message) {
|
|
||||||
super(message);
|
|
||||||
this.errorCode = errorCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public WxPayException(String errorCode, String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
this.errorCode = errorCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public WxPayException(String errorCode, String message, Integer tenantId) {
|
|
||||||
super(message);
|
|
||||||
this.errorCode = errorCode;
|
|
||||||
this.tenantId = tenantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public WxPayException(String errorCode, String message, Integer tenantId, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
this.errorCode = errorCode;
|
|
||||||
this.tenantId = tenantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getErrorCode() {
|
|
||||||
return errorCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setErrorCode(String errorCode) {
|
|
||||||
this.errorCode = errorCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getTenantId() {
|
|
||||||
return tenantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTenantId(Integer tenantId) {
|
|
||||||
this.tenantId = tenantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append("WxPayException{");
|
|
||||||
if (errorCode != null) {
|
|
||||||
sb.append("errorCode='").append(errorCode).append("', ");
|
|
||||||
}
|
|
||||||
if (tenantId != null) {
|
|
||||||
sb.append("tenantId=").append(tenantId).append(", ");
|
|
||||||
}
|
|
||||||
sb.append("message='").append(getMessage()).append("'");
|
|
||||||
sb.append("}");
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信支付错误代码常量
|
|
||||||
*/
|
|
||||||
public static class ErrorCode {
|
|
||||||
/** 配置错误 */
|
|
||||||
public static final String CONFIG_ERROR = "CONFIG_ERROR";
|
|
||||||
|
|
||||||
/** 证书错误 */
|
|
||||||
public static final String CERTIFICATE_ERROR = "CERTIFICATE_ERROR";
|
|
||||||
|
|
||||||
/** 参数错误 */
|
|
||||||
public static final String PARAM_ERROR = "PARAM_ERROR";
|
|
||||||
|
|
||||||
/** 网络错误 */
|
|
||||||
public static final String NETWORK_ERROR = "NETWORK_ERROR";
|
|
||||||
|
|
||||||
/** 签名验证失败 */
|
|
||||||
public static final String SIGNATURE_ERROR = "SIGNATURE_ERROR";
|
|
||||||
|
|
||||||
/** 订单状态错误 */
|
|
||||||
public static final String ORDER_STATUS_ERROR = "ORDER_STATUS_ERROR";
|
|
||||||
|
|
||||||
/** 金额错误 */
|
|
||||||
public static final String AMOUNT_ERROR = "AMOUNT_ERROR";
|
|
||||||
|
|
||||||
/** 租户配置错误 */
|
|
||||||
public static final String TENANT_CONFIG_ERROR = "TENANT_CONFIG_ERROR";
|
|
||||||
|
|
||||||
/** 回调处理错误 */
|
|
||||||
public static final String CALLBACK_ERROR = "CALLBACK_ERROR";
|
|
||||||
|
|
||||||
/** 系统内部错误 */
|
|
||||||
public static final String SYSTEM_ERROR = "SYSTEM_ERROR";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建配置错误异常
|
|
||||||
*/
|
|
||||||
public static WxPayException configError(String message) {
|
|
||||||
return new WxPayException(ErrorCode.CONFIG_ERROR, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建配置错误异常(带租户ID)
|
|
||||||
*/
|
|
||||||
public static WxPayException configError(String message, Integer tenantId) {
|
|
||||||
return new WxPayException(ErrorCode.CONFIG_ERROR, message, tenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建证书错误异常
|
|
||||||
*/
|
|
||||||
public static WxPayException certificateError(String message) {
|
|
||||||
return new WxPayException(ErrorCode.CERTIFICATE_ERROR, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建参数错误异常
|
|
||||||
*/
|
|
||||||
public static WxPayException paramError(String message) {
|
|
||||||
return new WxPayException(ErrorCode.PARAM_ERROR, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建网络错误异常
|
|
||||||
*/
|
|
||||||
public static WxPayException networkError(String message, Throwable cause) {
|
|
||||||
return new WxPayException(ErrorCode.NETWORK_ERROR, message, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建签名验证失败异常
|
|
||||||
*/
|
|
||||||
public static WxPayException signatureError(String message) {
|
|
||||||
return new WxPayException(ErrorCode.SIGNATURE_ERROR, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建订单状态错误异常
|
|
||||||
*/
|
|
||||||
public static WxPayException orderStatusError(String message) {
|
|
||||||
return new WxPayException(ErrorCode.ORDER_STATUS_ERROR, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建金额错误异常
|
|
||||||
*/
|
|
||||||
public static WxPayException amountError(String message) {
|
|
||||||
return new WxPayException(ErrorCode.AMOUNT_ERROR, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建租户配置错误异常
|
|
||||||
*/
|
|
||||||
public static WxPayException tenantConfigError(String message, Integer tenantId) {
|
|
||||||
return new WxPayException(ErrorCode.TENANT_CONFIG_ERROR, message, tenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建回调处理错误异常
|
|
||||||
*/
|
|
||||||
public static WxPayException callbackError(String message, Throwable cause) {
|
|
||||||
return new WxPayException(ErrorCode.CALLBACK_ERROR, message, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建系统内部错误异常
|
|
||||||
*/
|
|
||||||
public static WxPayException systemError(String message, Throwable cause) {
|
|
||||||
return new WxPayException(ErrorCode.SYSTEM_ERROR, message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
package com.gxwebsoft.shop.exception;
|
|
||||||
|
|
||||||
import com.gxwebsoft.common.core.web.ApiResult;
|
|
||||||
import com.gxwebsoft.shop.constants.WxPayConstants;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.validation.BindException;
|
|
||||||
import org.springframework.validation.FieldError;
|
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|
||||||
|
|
||||||
import javax.validation.ConstraintViolation;
|
|
||||||
import javax.validation.ConstraintViolationException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信支付异常处理器
|
|
||||||
* 处理微信支付相关的异常和参数验证异常
|
|
||||||
*
|
|
||||||
* @author 科技小王子
|
|
||||||
* @since 2025-01-26
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@RestControllerAdvice(basePackages = "com.gxwebsoft.shop.controller")
|
|
||||||
public class WxPayExceptionHandler {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理微信支付业务异常
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(WxPayException.class)
|
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
|
||||||
public ApiResult<?> handleWxPayException(WxPayException e) {
|
|
||||||
log.warn("微信支付业务异常: {}", e.getMessage());
|
|
||||||
|
|
||||||
if (e.getTenantId() != null) {
|
|
||||||
log.warn("异常租户ID: {}", e.getTenantId());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.getErrorCode() != null) {
|
|
||||||
log.warn("错误代码: {}", e.getErrorCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
return ApiResult.fail(e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理参数验证异常(@Valid注解)
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
|
||||||
public ApiResult<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
|
|
||||||
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
|
|
||||||
|
|
||||||
String errorMessage = fieldErrors.stream()
|
|
||||||
.map(error -> error.getField() + ": " + error.getDefaultMessage())
|
|
||||||
.collect(Collectors.joining("; "));
|
|
||||||
|
|
||||||
log.warn("参数验证失败: {}", errorMessage);
|
|
||||||
|
|
||||||
return ApiResult.fail(WxPayConstants.ErrorMessage.PARAM_VALIDATION_FAILED + ": " + errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理绑定异常
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(BindException.class)
|
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
|
||||||
public ApiResult<?> handleBindException(BindException e) {
|
|
||||||
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
|
|
||||||
|
|
||||||
String errorMessage = fieldErrors.stream()
|
|
||||||
.map(error -> error.getField() + ": " + error.getDefaultMessage())
|
|
||||||
.collect(Collectors.joining("; "));
|
|
||||||
|
|
||||||
log.warn("数据绑定失败: {}", errorMessage);
|
|
||||||
|
|
||||||
return ApiResult.fail(WxPayConstants.ErrorMessage.PARAM_VALIDATION_FAILED + ": " + errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理约束违反异常(@Validated注解)
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(ConstraintViolationException.class)
|
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
|
||||||
public ApiResult<?> handleConstraintViolationException(ConstraintViolationException e) {
|
|
||||||
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
|
|
||||||
|
|
||||||
String errorMessage = violations.stream()
|
|
||||||
.map(violation -> violation.getPropertyPath() + ": " + violation.getMessage())
|
|
||||||
.collect(Collectors.joining("; "));
|
|
||||||
|
|
||||||
log.warn("约束验证失败: {}", errorMessage);
|
|
||||||
|
|
||||||
return ApiResult.fail(WxPayConstants.ErrorMessage.PARAM_VALIDATION_FAILED + ": " + errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理非法参数异常
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(IllegalArgumentException.class)
|
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
|
||||||
public ApiResult<?> handleIllegalArgumentException(IllegalArgumentException e) {
|
|
||||||
log.warn("非法参数异常: {}", e.getMessage());
|
|
||||||
return ApiResult.fail(WxPayConstants.ErrorMessage.PARAM_VALIDATION_FAILED + ": " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理空指针异常
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(NullPointerException.class)
|
|
||||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
public ApiResult<?> handleNullPointerException(NullPointerException e) {
|
|
||||||
log.error("空指针异常", e);
|
|
||||||
return ApiResult.fail(WxPayConstants.ErrorMessage.SYSTEM_INTERNAL_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理其他运行时异常
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(RuntimeException.class)
|
|
||||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
public ApiResult<?> handleRuntimeException(RuntimeException e) {
|
|
||||||
log.error("运行时异常: {}", e.getMessage(), e);
|
|
||||||
return ApiResult.fail(WxPayConstants.ErrorMessage.SYSTEM_INTERNAL_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理其他异常
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(Exception.class)
|
|
||||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
public ApiResult<?> handleException(Exception e) {
|
|
||||||
log.error("未知异常: {}", e.getMessage(), e);
|
|
||||||
return ApiResult.fail(WxPayConstants.ErrorMessage.SYSTEM_INTERNAL_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
package com.gxwebsoft.shop.service;
|
|
||||||
|
|
||||||
import com.gxwebsoft.common.core.config.CertificateProperties;
|
|
||||||
import com.gxwebsoft.common.core.config.ConfigProperties;
|
|
||||||
import com.gxwebsoft.common.core.service.CertificateService;
|
|
||||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
|
||||||
import com.gxwebsoft.common.core.utils.WxNativeUtil;
|
|
||||||
import com.gxwebsoft.common.system.entity.Payment;
|
|
||||||
import com.gxwebsoft.shop.exception.WxPayException;
|
|
||||||
import com.wechat.pay.java.core.Config;
|
|
||||||
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.core.io.ClassPathResource;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信支付配置服务
|
|
||||||
* 负责处理微信支付的配置获取和管理
|
|
||||||
*
|
|
||||||
* @author 科技小王子
|
|
||||||
* @since 2025-01-26
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
public class WxPayConfigService {
|
|
||||||
|
|
||||||
@Value("${spring.profiles.active}")
|
|
||||||
private String activeProfile;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private RedisUtil redisUtil;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private ConfigProperties configProperties;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private CertificateService certificateService;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private CertificateProperties certificateProperties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取微信支付配置
|
|
||||||
*
|
|
||||||
* @param tenantId 租户ID
|
|
||||||
* @return 微信支付配置
|
|
||||||
* @throws WxPayException 配置获取失败时抛出
|
|
||||||
*/
|
|
||||||
public Config getWxPayConfig(Integer tenantId) throws WxPayException {
|
|
||||||
if (tenantId == null) {
|
|
||||||
throw new WxPayException("租户ID不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 先从缓存获取已构建的配置
|
|
||||||
Config cachedConfig = WxNativeUtil.getConfig(tenantId);
|
|
||||||
if (cachedConfig != null) {
|
|
||||||
log.debug("从缓存获取微信支付配置成功,租户ID: {}", tenantId);
|
|
||||||
return cachedConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建新的配置
|
|
||||||
Config newConfig = buildWxPayConfig(tenantId);
|
|
||||||
|
|
||||||
// 缓存配置
|
|
||||||
WxNativeUtil.addConfig(tenantId, newConfig);
|
|
||||||
log.info("微信支付配置创建并缓存成功,租户ID: {}", tenantId);
|
|
||||||
|
|
||||||
return newConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建微信支付配置
|
|
||||||
*/
|
|
||||||
private Config buildWxPayConfig(Integer tenantId) throws WxPayException {
|
|
||||||
try {
|
|
||||||
// 获取支付配置信息
|
|
||||||
Payment payment = getPaymentConfig(tenantId);
|
|
||||||
|
|
||||||
// 获取证书路径
|
|
||||||
String certificatePath = getCertificatePath(tenantId, payment);
|
|
||||||
|
|
||||||
// 构建配置
|
|
||||||
return createWxPayConfig(payment, certificatePath);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("构建微信支付配置失败,租户ID: {}, 错误: {}", tenantId, e.getMessage(), e);
|
|
||||||
throw new WxPayException("微信支付配置构建失败: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取支付配置信息
|
|
||||||
*/
|
|
||||||
private Payment getPaymentConfig(Integer tenantId) throws WxPayException {
|
|
||||||
String cacheKey = "Payment:wxPay:" + tenantId;
|
|
||||||
Payment payment = redisUtil.get(cacheKey, Payment.class);
|
|
||||||
|
|
||||||
if (payment == null && !"dev".equals(activeProfile)) {
|
|
||||||
throw new WxPayException("微信支付配置未找到,租户ID: " + tenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payment != null) {
|
|
||||||
log.debug("从缓存获取支付配置成功,租户ID: {}", tenantId);
|
|
||||||
} else {
|
|
||||||
log.debug("开发环境模式,将使用测试配置,租户ID: {}", tenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return payment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取证书文件路径
|
|
||||||
*/
|
|
||||||
private String getCertificatePath(Integer tenantId, Payment payment) throws WxPayException {
|
|
||||||
if ("dev".equals(activeProfile)) {
|
|
||||||
return getDevCertificatePath(tenantId);
|
|
||||||
} else {
|
|
||||||
return getProdCertificatePath(payment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取开发环境证书路径
|
|
||||||
*/
|
|
||||||
private String getDevCertificatePath(Integer tenantId) throws WxPayException {
|
|
||||||
try {
|
|
||||||
String relativePath = certificateService.getWechatPayCertPath(
|
|
||||||
certificateProperties.getWechatPay().getDev().getPrivateKeyFile()
|
|
||||||
);
|
|
||||||
|
|
||||||
String certificatePath;
|
|
||||||
if (certificateProperties.isClasspathMode()) {
|
|
||||||
// classpath模式
|
|
||||||
try {
|
|
||||||
ClassPathResource resource = new ClassPathResource(relativePath);
|
|
||||||
if (resource.exists()) {
|
|
||||||
certificatePath = resource.getFile().getAbsolutePath();
|
|
||||||
} else {
|
|
||||||
certificatePath = "classpath:" + relativePath;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
certificatePath = "classpath:" + relativePath;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 文件系统模式
|
|
||||||
certificatePath = new File(relativePath).getAbsolutePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证证书文件是否存在
|
|
||||||
if (!certificateService.certificateExists("wechat",
|
|
||||||
certificateProperties.getWechatPay().getDev().getPrivateKeyFile())) {
|
|
||||||
throw new WxPayException("微信支付私钥证书文件不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("开发环境证书路径: {}", certificatePath);
|
|
||||||
return certificatePath;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WxPayException("获取开发环境证书路径失败: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取生产环境证书路径
|
|
||||||
*/
|
|
||||||
private String getProdCertificatePath(Payment payment) throws WxPayException {
|
|
||||||
if (payment == null || payment.getApiclientKey() == null) {
|
|
||||||
throw new WxPayException("生产环境支付配置或证书路径为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
String relativePath = payment.getApiclientKey();
|
|
||||||
String certificatePath = configProperties.getUploadPath() + "file" + relativePath;
|
|
||||||
|
|
||||||
// 验证证书文件是否存在
|
|
||||||
File certFile = new File(certificatePath);
|
|
||||||
if (!certFile.exists()) {
|
|
||||||
throw new WxPayException("生产环境证书文件不存在: " + certificatePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("生产环境证书路径: {}", certificatePath);
|
|
||||||
return certificatePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建微信支付配置对象
|
|
||||||
*/
|
|
||||||
private Config createWxPayConfig(Payment payment, String certificatePath) throws WxPayException {
|
|
||||||
try {
|
|
||||||
if ("dev".equals(activeProfile) && payment == null) {
|
|
||||||
// 开发环境测试配置
|
|
||||||
return createDevTestConfig(certificatePath);
|
|
||||||
} else if (payment != null) {
|
|
||||||
// 正常配置
|
|
||||||
return createNormalConfig(payment, certificatePath);
|
|
||||||
} else {
|
|
||||||
throw new WxPayException("无法创建微信支付配置:配置信息不完整");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WxPayException("创建微信支付配置对象失败: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建开发环境测试配置
|
|
||||||
*/
|
|
||||||
private Config createDevTestConfig(String certificatePath) throws WxPayException {
|
|
||||||
String testMerchantId = "1246610101";
|
|
||||||
String testMerchantSerialNumber = "2903B872D5CA36E525FAEC37AEDB22E54ECDE7B7";
|
|
||||||
String testApiV3Key = certificateProperties.getWechatPay().getDev().getApiV3Key();
|
|
||||||
|
|
||||||
if (testApiV3Key == null || testApiV3Key.trim().isEmpty()) {
|
|
||||||
throw new WxPayException("开发环境APIv3密钥未配置");
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("使用开发环境测试配置");
|
|
||||||
log.debug("测试商户号: {}", testMerchantId);
|
|
||||||
log.debug("测试序列号: {}", testMerchantSerialNumber);
|
|
||||||
|
|
||||||
return new RSAAutoCertificateConfig.Builder()
|
|
||||||
.merchantId(testMerchantId)
|
|
||||||
.privateKeyFromPath(certificatePath)
|
|
||||||
.merchantSerialNumber(testMerchantSerialNumber)
|
|
||||||
.apiV3Key(testApiV3Key)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建正常配置
|
|
||||||
*/
|
|
||||||
private Config createNormalConfig(Payment payment, String certificatePath) throws WxPayException {
|
|
||||||
if (payment.getMchId() == null || payment.getMerchantSerialNumber() == null || payment.getApiKey() == null) {
|
|
||||||
throw new WxPayException("支付配置信息不完整:商户号、序列号或APIv3密钥为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("使用数据库支付配置");
|
|
||||||
log.debug("商户号: {}", payment.getMchId());
|
|
||||||
|
|
||||||
return new RSAAutoCertificateConfig.Builder()
|
|
||||||
.merchantId(payment.getMchId())
|
|
||||||
.privateKeyFromPath(certificatePath)
|
|
||||||
.merchantSerialNumber(payment.getMerchantSerialNumber())
|
|
||||||
.apiV3Key(payment.getApiKey())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除指定租户的配置缓存
|
|
||||||
*
|
|
||||||
* @param tenantId 租户ID
|
|
||||||
*/
|
|
||||||
public void clearConfigCache(Integer tenantId) {
|
|
||||||
WxNativeUtil.addConfig(tenantId, null);
|
|
||||||
log.info("清除微信支付配置缓存,租户ID: {}", tenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证支付配置是否有效
|
|
||||||
*
|
|
||||||
* @param tenantId 租户ID
|
|
||||||
* @return 配置是否有效
|
|
||||||
*/
|
|
||||||
public boolean isConfigValid(Integer tenantId) {
|
|
||||||
try {
|
|
||||||
getWxPayConfig(tenantId);
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("微信支付配置验证失败,租户ID: {}, 错误: {}", tenantId, e.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
package com.gxwebsoft.shop.service;
|
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
|
||||||
import com.gxwebsoft.common.core.constants.OrderConstants;
|
|
||||||
import com.gxwebsoft.common.core.utils.RequestUtil;
|
|
||||||
import com.gxwebsoft.shop.constants.WxPayConstants;
|
|
||||||
import com.gxwebsoft.shop.entity.ShopOrder;
|
|
||||||
import com.gxwebsoft.shop.exception.WxPayException;
|
|
||||||
import com.wechat.pay.java.core.Config;
|
|
||||||
import com.wechat.pay.java.core.notification.NotificationParser;
|
|
||||||
import com.wechat.pay.java.core.notification.RequestParam;
|
|
||||||
import com.wechat.pay.java.service.payments.model.Transaction;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信支付回调通知处理服务
|
|
||||||
* 负责处理微信支付的异步通知回调
|
|
||||||
*
|
|
||||||
* @author 科技小王子
|
|
||||||
* @since 2025-01-26
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
public class WxPayNotifyService {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private WxPayConfigService wxPayConfigService;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private ShopOrderService shopOrderService;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private RequestUtil requestUtil;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理微信支付回调通知
|
|
||||||
*
|
|
||||||
* @param headers 请求头
|
|
||||||
* @param body 请求体
|
|
||||||
* @param tenantId 租户ID
|
|
||||||
* @return 处理结果响应
|
|
||||||
*/
|
|
||||||
public String handlePaymentNotify(Map<String, String> headers, String body, Integer tenantId) {
|
|
||||||
log.info("{}, 租户ID: {}", WxPayConstants.LogMessage.CALLBACK_START, tenantId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 参数验证
|
|
||||||
validateNotifyParams(headers, body, tenantId);
|
|
||||||
|
|
||||||
// 获取微信支付配置
|
|
||||||
Config wxPayConfig = wxPayConfigService.getWxPayConfig(tenantId);
|
|
||||||
|
|
||||||
// 解析并验证回调数据
|
|
||||||
Transaction transaction = parseAndVerifyNotification(headers, body, wxPayConfig);
|
|
||||||
|
|
||||||
// 处理支付结果
|
|
||||||
processPaymentResult(transaction, tenantId);
|
|
||||||
|
|
||||||
log.info("{}, 租户ID: {}, 订单号: {}",
|
|
||||||
WxPayConstants.LogMessage.CALLBACK_SUCCESS, tenantId, transaction.getOutTradeNo());
|
|
||||||
|
|
||||||
return WxPayConstants.Notify.SUCCESS_RESPONSE;
|
|
||||||
|
|
||||||
} catch (WxPayException e) {
|
|
||||||
log.error("{}, 租户ID: {}, 错误: {}",
|
|
||||||
WxPayConstants.LogMessage.CALLBACK_FAILED, tenantId, e.getMessage(), e);
|
|
||||||
return WxPayConstants.Notify.FAIL_RESPONSE;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("{}, 租户ID: {}, 系统错误: {}",
|
|
||||||
WxPayConstants.LogMessage.CALLBACK_FAILED, tenantId, e.getMessage(), e);
|
|
||||||
return WxPayConstants.Notify.FAIL_RESPONSE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证回调通知参数
|
|
||||||
*/
|
|
||||||
private void validateNotifyParams(Map<String, String> headers, String body, Integer tenantId)
|
|
||||||
throws WxPayException {
|
|
||||||
|
|
||||||
if (tenantId == null) {
|
|
||||||
throw WxPayException.paramError("租户ID不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (headers == null || headers.isEmpty()) {
|
|
||||||
throw WxPayException.paramError("请求头不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!StringUtils.hasText(body)) {
|
|
||||||
throw WxPayException.paramError("请求体不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证必要的微信支付头部
|
|
||||||
String signature = headers.get(WxPayConstants.Header.WECHATPAY_SIGNATURE);
|
|
||||||
String timestamp = headers.get(WxPayConstants.Header.WECHATPAY_TIMESTAMP);
|
|
||||||
String nonce = headers.get(WxPayConstants.Header.WECHATPAY_NONCE);
|
|
||||||
String serial = headers.get(WxPayConstants.Header.WECHATPAY_SERIAL);
|
|
||||||
|
|
||||||
if (!StringUtils.hasText(signature)) {
|
|
||||||
throw WxPayException.paramError("微信支付签名不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!StringUtils.hasText(timestamp)) {
|
|
||||||
throw WxPayException.paramError("微信支付时间戳不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!StringUtils.hasText(nonce)) {
|
|
||||||
throw WxPayException.paramError("微信支付随机数不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!StringUtils.hasText(serial)) {
|
|
||||||
throw WxPayException.paramError("微信支付序列号不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("回调通知参数验证通过, 租户ID: {}", tenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析并验证回调通知
|
|
||||||
*/
|
|
||||||
private Transaction parseAndVerifyNotification(Map<String, String> headers, String body, Config wxPayConfig)
|
|
||||||
throws WxPayException {
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 构建请求参数
|
|
||||||
RequestParam requestParam = new RequestParam.Builder()
|
|
||||||
.serialNumber(headers.get(WxPayConstants.Header.WECHATPAY_SERIAL))
|
|
||||||
.nonce(headers.get(WxPayConstants.Header.WECHATPAY_NONCE))
|
|
||||||
.signature(headers.get(WxPayConstants.Header.WECHATPAY_SIGNATURE))
|
|
||||||
.timestamp(headers.get(WxPayConstants.Header.WECHATPAY_TIMESTAMP))
|
|
||||||
.body(body)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 创建通知解析器
|
|
||||||
NotificationParser parser = new NotificationParser(wxPayConfig);
|
|
||||||
|
|
||||||
// 解析并验证通知
|
|
||||||
Transaction transaction = parser.parse(requestParam, Transaction.class);
|
|
||||||
|
|
||||||
if (transaction == null) {
|
|
||||||
throw WxPayException.callbackError("解析回调通知失败:transaction为空", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("回调通知解析成功, 订单号: {}, 交易状态: {}",
|
|
||||||
transaction.getOutTradeNo(), transaction.getTradeState());
|
|
||||||
|
|
||||||
return transaction;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (e instanceof WxPayException) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
throw WxPayException.signatureError("签名验证失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理支付结果
|
|
||||||
*/
|
|
||||||
private void processPaymentResult(Transaction transaction, Integer tenantId) throws WxPayException {
|
|
||||||
String outTradeNo = transaction.getOutTradeNo();
|
|
||||||
String tradeState = transaction.getTradeState();
|
|
||||||
|
|
||||||
if (!StringUtils.hasText(outTradeNo)) {
|
|
||||||
throw WxPayException.paramError("商户订单号不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询订单
|
|
||||||
ShopOrder order = shopOrderService.getByOutTradeNo(outTradeNo);
|
|
||||||
if (order == null) {
|
|
||||||
throw WxPayException.orderStatusError("订单不存在: " + outTradeNo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证租户ID
|
|
||||||
if (!tenantId.equals(order.getTenantId())) {
|
|
||||||
throw WxPayException.tenantConfigError("订单租户ID不匹配", tenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证订单状态 - 使用Boolean类型的payStatus字段
|
|
||||||
if (Boolean.TRUE.equals(order.getPayStatus())) {
|
|
||||||
log.info("订单已支付,跳过处理, 订单号: {}", outTradeNo);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据交易状态处理
|
|
||||||
switch (tradeState) {
|
|
||||||
case WxPayConstants.PayStatus.SUCCESS:
|
|
||||||
handlePaymentSuccess(order, transaction);
|
|
||||||
break;
|
|
||||||
case WxPayConstants.PayStatus.REFUND:
|
|
||||||
handlePaymentRefund(order, transaction);
|
|
||||||
break;
|
|
||||||
case WxPayConstants.PayStatus.CLOSED:
|
|
||||||
case WxPayConstants.PayStatus.REVOKED:
|
|
||||||
case WxPayConstants.PayStatus.PAYERROR:
|
|
||||||
handlePaymentFailed(order, transaction);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
log.warn("未处理的交易状态: {}, 订单号: {}", tradeState, outTradeNo);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理支付成功
|
|
||||||
*/
|
|
||||||
private void handlePaymentSuccess(ShopOrder order, Transaction transaction) throws WxPayException {
|
|
||||||
try {
|
|
||||||
// 验证金额
|
|
||||||
validateAmount(order, transaction);
|
|
||||||
|
|
||||||
// 更新订单状态
|
|
||||||
order.setPayStatus(true); // 使用Boolean类型
|
|
||||||
order.setTransactionId(transaction.getTransactionId());
|
|
||||||
order.setPayTime(transaction.getSuccessTime());
|
|
||||||
|
|
||||||
// 使用专门的更新方法,会触发支付成功后的业务逻辑
|
|
||||||
shopOrderService.updateByOutTradeNo(order);
|
|
||||||
|
|
||||||
// 推送支付结果通知
|
|
||||||
pushPaymentNotification(order, transaction);
|
|
||||||
|
|
||||||
log.info("支付成功处理完成, 订单号: {}, 微信交易号: {}",
|
|
||||||
order.getOrderNo(), transaction.getTransactionId());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw WxPayException.callbackError("处理支付成功回调失败: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理支付退款
|
|
||||||
*/
|
|
||||||
private void handlePaymentRefund(ShopOrder order, Transaction transaction) throws WxPayException {
|
|
||||||
try {
|
|
||||||
log.info("处理支付退款, 订单号: {}, 微信交易号: {}",
|
|
||||||
order.getOrderNo(), transaction.getTransactionId());
|
|
||||||
|
|
||||||
// 这里可以添加退款相关的业务逻辑
|
|
||||||
// 例如:更新订单状态、处理库存、发送通知等
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw WxPayException.callbackError("处理支付退款回调失败: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理支付失败
|
|
||||||
*/
|
|
||||||
private void handlePaymentFailed(ShopOrder order, Transaction transaction) throws WxPayException {
|
|
||||||
try {
|
|
||||||
log.info("处理支付失败, 订单号: {}, 交易状态: {}",
|
|
||||||
order.getOrderNo(), transaction.getTradeState());
|
|
||||||
|
|
||||||
// 这里可以添加支付失败相关的业务逻辑
|
|
||||||
// 例如:释放库存、发送通知等
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw WxPayException.callbackError("处理支付失败回调失败: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证支付金额
|
|
||||||
*/
|
|
||||||
private void validateAmount(ShopOrder order, Transaction transaction) throws WxPayException {
|
|
||||||
if (transaction.getAmount() == null || transaction.getAmount().getTotal() == null) {
|
|
||||||
throw WxPayException.amountError("回调通知中金额信息为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将订单金额转换为分
|
|
||||||
BigDecimal orderAmount = order.getMoney();
|
|
||||||
if (orderAmount == null) {
|
|
||||||
throw WxPayException.amountError("订单金额为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
int orderAmountFen = orderAmount.multiply(new BigDecimal(WxPayConstants.Config.AMOUNT_MULTIPLIER)).intValue();
|
|
||||||
int callbackAmountFen = transaction.getAmount().getTotal();
|
|
||||||
|
|
||||||
if (orderAmountFen != callbackAmountFen) {
|
|
||||||
throw WxPayException.amountError(
|
|
||||||
String.format("订单金额不匹配,订单金额: %d分, 回调金额: %d分",
|
|
||||||
orderAmountFen, callbackAmountFen));
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("金额验证通过, 订单号: {}, 金额: {}分", order.getOrderNo(), orderAmountFen);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 推送支付结果通知
|
|
||||||
*/
|
|
||||||
private void pushPaymentNotification(ShopOrder order, Transaction transaction) {
|
|
||||||
try {
|
|
||||||
// 使用现有的推送工具
|
|
||||||
requestUtil.pushWxPayNotify(transaction, null);
|
|
||||||
log.debug("支付结果通知推送成功, 订单号: {}", order.getOrderNo());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("支付结果通知推送失败, 订单号: {}, 错误: {}", order.getOrderNo(), e.getMessage());
|
|
||||||
// 推送失败不影响主流程,只记录日志
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user