9 changed files with 1614 additions and 1 deletions
@ -0,0 +1,108 @@ |
|||||
|
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; |
||||
|
} |
||||
|
} |
@ -0,0 +1,200 @@ |
|||||
|
package com.gxwebsoft.shop.constants; |
||||
|
|
||||
|
/** |
||||
|
* 微信支付常量类 |
||||
|
* 管理微信支付相关的常量配置 |
||||
|
* |
||||
|
* @author 科技小王子 |
||||
|
* @since 2025-01-26 |
||||
|
*/ |
||||
|
public class WxPayConstants { |
||||
|
|
||||
|
/** |
||||
|
* 微信支付类型 |
||||
|
*/ |
||||
|
public static class PayType { |
||||
|
/** Native支付 */ |
||||
|
public static final String NATIVE = "NATIVE"; |
||||
|
/** JSAPI支付 */ |
||||
|
public static final String JSAPI = "JSAPI"; |
||||
|
/** H5支付 */ |
||||
|
public static final String MWEB = "MWEB"; |
||||
|
/** APP支付 */ |
||||
|
public static final String APP = "APP"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 支付状态 |
||||
|
*/ |
||||
|
public static class PayStatus { |
||||
|
/** 支付成功 */ |
||||
|
public static final String SUCCESS = "SUCCESS"; |
||||
|
/** 转入退款 */ |
||||
|
public static final String REFUND = "REFUND"; |
||||
|
/** 未支付 */ |
||||
|
public static final String NOTPAY = "NOTPAY"; |
||||
|
/** 已关闭 */ |
||||
|
public static final String CLOSED = "CLOSED"; |
||||
|
/** 已撤销(付款码支付) */ |
||||
|
public static final String REVOKED = "REVOKED"; |
||||
|
/** 用户支付中(付款码支付) */ |
||||
|
public static final String USERPAYING = "USERPAYING"; |
||||
|
/** 支付失败(其他原因,如银行返回失败) */ |
||||
|
public static final String PAYERROR = "PAYERROR"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 回调通知相关 |
||||
|
*/ |
||||
|
public static class Notify { |
||||
|
/** 成功响应 */ |
||||
|
public static final String SUCCESS_RESPONSE = "SUCCESS"; |
||||
|
/** 失败响应 */ |
||||
|
public static final String FAIL_RESPONSE = "FAIL"; |
||||
|
|
||||
|
/** 通知类型 - 支付成功 */ |
||||
|
public static final String EVENT_TYPE_PAYMENT = "TRANSACTION.SUCCESS"; |
||||
|
/** 通知类型 - 退款成功 */ |
||||
|
public static final String EVENT_TYPE_REFUND = "REFUND.SUCCESS"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 缓存键前缀 |
||||
|
*/ |
||||
|
public static class CacheKey { |
||||
|
/** 支付配置缓存键前缀 */ |
||||
|
public static final String PAYMENT_CONFIG_PREFIX = "Payment:wxPay:"; |
||||
|
/** 微信小程序配置缓存键 */ |
||||
|
public static final String MP_WEIXIN_CONFIG = "mp-weixin"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 配置相关 |
||||
|
*/ |
||||
|
public static class Config { |
||||
|
/** 货币类型 - 人民币 */ |
||||
|
public static final String CURRENCY_CNY = "CNY"; |
||||
|
/** 金额转换倍数(元转分) */ |
||||
|
public static final int AMOUNT_MULTIPLIER = 100; |
||||
|
|
||||
|
/** 开发环境标识 */ |
||||
|
public static final String PROFILE_DEV = "dev"; |
||||
|
/** 生产环境标识 */ |
||||
|
public static final String PROFILE_PROD = "prod"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 订单相关 |
||||
|
*/ |
||||
|
public static class Order { |
||||
|
/** 订单超时时间(分钟) */ |
||||
|
public static final int TIMEOUT_MINUTES = 30; |
||||
|
/** 订单描述最大长度 */ |
||||
|
public static final int DESCRIPTION_MAX_LENGTH = 127; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* HTTP头部相关 |
||||
|
*/ |
||||
|
public static class Header { |
||||
|
/** 微信支付签名 */ |
||||
|
public static final String WECHATPAY_SIGNATURE = "Wechatpay-Signature"; |
||||
|
/** 微信支付时间戳 */ |
||||
|
public static final String WECHATPAY_TIMESTAMP = "Wechatpay-Timestamp"; |
||||
|
/** 微信支付随机数 */ |
||||
|
public static final String WECHATPAY_NONCE = "Wechatpay-Nonce"; |
||||
|
/** 微信支付序列号 */ |
||||
|
public static final String WECHATPAY_SERIAL = "Wechatpay-Serial"; |
||||
|
/** 请求ID */ |
||||
|
public static final String REQUEST_ID = "Request-ID"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 错误信息 |
||||
|
*/ |
||||
|
public static class ErrorMessage { |
||||
|
/** 配置未找到 */ |
||||
|
public static final String CONFIG_NOT_FOUND = "微信支付配置未找到"; |
||||
|
/** 证书文件不存在 */ |
||||
|
public static final String CERTIFICATE_NOT_FOUND = "微信支付证书文件不存在"; |
||||
|
/** 参数验证失败 */ |
||||
|
public static final String PARAM_VALIDATION_FAILED = "参数验证失败"; |
||||
|
/** 签名验证失败 */ |
||||
|
public static final String SIGNATURE_VERIFICATION_FAILED = "签名验证失败"; |
||||
|
/** 订单不存在 */ |
||||
|
public static final String ORDER_NOT_FOUND = "订单不存在"; |
||||
|
/** 订单状态异常 */ |
||||
|
public static final String ORDER_STATUS_INVALID = "订单状态异常"; |
||||
|
/** 金额不匹配 */ |
||||
|
public static final String AMOUNT_MISMATCH = "订单金额不匹配"; |
||||
|
/** 网络请求失败 */ |
||||
|
public static final String NETWORK_REQUEST_FAILED = "网络请求失败"; |
||||
|
/** 系统内部错误 */ |
||||
|
public static final String SYSTEM_INTERNAL_ERROR = "系统内部错误"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 日志相关 |
||||
|
*/ |
||||
|
public static class LogMessage { |
||||
|
/** 支付请求开始 */ |
||||
|
public static final String PAY_REQUEST_START = "开始处理微信支付请求"; |
||||
|
/** 支付请求成功 */ |
||||
|
public static final String PAY_REQUEST_SUCCESS = "微信支付请求处理成功"; |
||||
|
/** 支付请求失败 */ |
||||
|
public static final String PAY_REQUEST_FAILED = "微信支付请求处理失败"; |
||||
|
|
||||
|
/** 回调处理开始 */ |
||||
|
public static final String CALLBACK_START = "开始处理微信支付回调"; |
||||
|
/** 回调处理成功 */ |
||||
|
public static final String CALLBACK_SUCCESS = "微信支付回调处理成功"; |
||||
|
/** 回调处理失败 */ |
||||
|
public static final String CALLBACK_FAILED = "微信支付回调处理失败"; |
||||
|
|
||||
|
/** 配置加载成功 */ |
||||
|
public static final String CONFIG_LOADED = "微信支付配置加载成功"; |
||||
|
/** 配置加载失败 */ |
||||
|
public static final String CONFIG_LOAD_FAILED = "微信支付配置加载失败"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 正则表达式 |
||||
|
*/ |
||||
|
public static class Regex { |
||||
|
/** 商户号格式 */ |
||||
|
public static final String MERCHANT_ID = "^\\d{10}$"; |
||||
|
/** 订单号格式 */ |
||||
|
public static final String ORDER_NO = "^[a-zA-Z0-9_-]{1,32}$"; |
||||
|
/** 金额格式(分) */ |
||||
|
public static final String AMOUNT = "^[1-9]\\d*$"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 时间相关 |
||||
|
*/ |
||||
|
public static class Time { |
||||
|
/** 签名有效期(秒) */ |
||||
|
public static final long SIGNATURE_VALID_SECONDS = 300; |
||||
|
/** 配置缓存有效期(秒) */ |
||||
|
public static final long CONFIG_CACHE_SECONDS = 3600; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 文件相关 |
||||
|
*/ |
||||
|
public static class File { |
||||
|
/** 证书文件扩展名 */ |
||||
|
public static final String CERT_EXTENSION = ".pem"; |
||||
|
/** 私钥文件名 */ |
||||
|
public static final String PRIVATE_KEY_FILE = "apiclient_key.pem"; |
||||
|
/** 商户证书文件名 */ |
||||
|
public static final String MERCHANT_CERT_FILE = "apiclient_cert.pem"; |
||||
|
/** 平台证书文件名 */ |
||||
|
public static final String PLATFORM_CERT_FILE = "wechatpay_cert.pem"; |
||||
|
} |
||||
|
|
||||
|
// 私有构造函数,防止实例化
|
||||
|
private WxPayConstants() { |
||||
|
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); |
||||
|
} |
||||
|
} |
@ -0,0 +1,266 @@ |
|||||
|
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); |
||||
|
} |
||||
|
} |
@ -0,0 +1,117 @@ |
|||||
|
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(); |
||||
|
} |
||||
|
} |
@ -0,0 +1,196 @@ |
|||||
|
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); |
||||
|
} |
||||
|
} |
@ -0,0 +1,140 @@ |
|||||
|
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); |
||||
|
} |
||||
|
} |
@ -0,0 +1,275 @@ |
|||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,309 @@ |
|||||
|
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()); |
||||
|
// 推送失败不影响主流程,只记录日志
|
||||
|
} |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue