fix(payment): 微信支付改用公钥模式,修「无可用的平台证书」404

商户 1116982020 已在商户平台申请使用微信支付公钥,/v3/certificates 对这类
商户返回 RESOURCE_NOT_EXISTS(404)「无可用的平台证书」;而 WxPayConfigService
无条件使用 RSAAutoCertificateConfig(构造时即下载平台证书),因此创建支付
配置必然失败,Native/JSAPI 下单、查询、退款全部不可用。

- 数据库配置了 pub_key_id 时改用 RSAPublicKeyConfig(不请求平台证书):
  dev 取 classpath dev/wechat/{租户}/pub_key.pem,prod 取证书卷路径 pubKey
- 配了公钥ID但公钥文件缺失时抛出明确错误(含预期路径),
  不再退回平台证书自动下载后抛出难以定位的 404
- application-dev.yml 公钥文件名 wechatpay_cert.pem → pub_key.pem,
  与仓库内实际文件一致(shop 下单流程的 dev 公钥分支同受益)
- clearConfigCache 同时清 Redis Payment:1:{tenantId}:此前库中改了公钥ID
  也要等最长 24 小时缓存过期才生效
- 测试控制器 /api/test/clear-payment-cache 清的键不对(Payment:0:*),
  改为调用 clearConfigCache
- 新增 WxPayConfigServicePublicKeyTest 回归测试(3 个用例)

验证:/api/payment/query 由「创建微信支付配置对象失败: Wrong HttpStatusCode[404]
无可用的平台证书」变为微信业务应答 ORDER_NOT_EXIST;并用真实响应头验证
Wechatpay-Serial=PUB_KEY_ID_… 的公钥签名校验 PASS。
This commit is contained in:
2026-09-12 18:47:59 +08:00
parent da1f9a64d4
commit b2b1a1bdf9
4 changed files with 213 additions and 6 deletions
@@ -5,6 +5,7 @@ import com.gxwebsoft.common.system.entity.Payment;
import com.gxwebsoft.common.system.service.PaymentService;
import com.gxwebsoft.common.system.param.PaymentParam;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.payment.service.WxPayConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -39,6 +40,9 @@ public class TestController extends BaseController {
@Autowired
private PaymentService paymentService;
@Autowired
private WxPayConfigService wxPayConfigService;
@Operation(summary = "测试LocalDateTime序列化")
@GetMapping("/datetime")
public ApiResult<Map<String, Object>> testDateTime() {
@@ -233,6 +237,8 @@ public class TestController extends BaseController {
// 清理可能的缓存键
paymentCacheService.removePaymentConfig("0", tenantId); // 微信支付
paymentCacheService.removePaymentConfig("wechat", tenantId); // 可能的其他格式
// 清理实际生效的缓存键:Payment:1:{tenantId} 与内存中的微信支付配置
wxPayConfigService.clearConfigCache(tenantId);
String result = "✅ 缓存已清理,租户ID: " + tenantId;
System.out.println(result);
@@ -12,12 +12,15 @@ import com.gxwebsoft.payment.exception.PaymentException;
import com.gxwebsoft.payment.enums.PaymentType;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.RSAPublicKeyConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@@ -101,8 +104,11 @@ public class WxPayConfigService {
// 获取证书文件路径
String certificatePath = getCertificatePath(tenantId, payment);
// 获取微信支付公钥文件路径(未配置公钥ID时为null,表示使用平台证书自动下载模式)
String publicKeyPath = resolvePublicKeyPath(tenantId, payment);
// 创建微信支付配置对象
return createWxPayConfig(payment, certificatePath);
return createWxPayConfig(payment, certificatePath, publicKeyPath);
} catch (Exception e) {
if (e instanceof PaymentException) {
@@ -232,17 +238,96 @@ public class WxPayConfigService {
}
}
/**
* 解析微信支付公钥文件路径
*
* <p>商户平台已「申请使用微信支付公钥」的商户,调用 /v3/certificates 会返回
* RESOURCE_NOT_EXISTS(404),必须改用公钥模式(RSAPublicKeyConfig)。
* 是否启用公钥模式以数据库中的公钥ID(pubKeyId)为准:
* 配置了公钥ID却没有可用的公钥文件时直接报错,避免退回平台证书自动下载后
* 再次抛出难以定位的 404。</p>
*
* @param tenantId 租户ID
* @param payment 支付配置
* @return 公钥文件路径;未配置公钥ID时返回 null
* @throws PaymentException 启用了公钥模式但公钥文件不可用时抛出
*/
private String resolvePublicKeyPath(Integer tenantId, Payment payment) throws PaymentException {
if (payment == null || !StringUtils.hasText(payment.getPubKeyId())) {
return null;
}
if ("dev".equals(activeProfile)) {
return resolveDevPublicKeyPath(tenantId);
}
return resolveProdPublicKeyPath(payment);
}
/**
* 获取开发环境微信支付公钥路径
*/
private String resolveDevPublicKeyPath(Integer tenantId) throws PaymentException {
String certDir = certificateProperties.getDevCertPath() + "/"
+ certificateProperties.getWechatPay().getCertDir() + "/" + tenantId + "/";
// 优先使用配置的公钥文件名,并兼容证书目录下实际的 pub_key.pem
String[] candidates = {
certificateProperties.getWechatPay().getDev().getWechatpayCertFile(),
"pub_key.pem"
};
for (String fileName : candidates) {
if (!StringUtils.hasText(fileName)) {
continue;
}
ClassPathResource resource = new ClassPathResource(certDir + fileName);
if (resource.exists()) {
try {
String absolutePath = resource.getFile().getAbsolutePath();
log.debug("开发环境微信支付公钥路径: {}", absolutePath);
return absolutePath;
} catch (IOException e) {
throw PaymentException.systemError("获取开发环境微信支付公钥路径失败: " + e.getMessage(), e);
}
}
}
throw PaymentException.systemError(
"已配置微信支付公钥ID但缺少公钥文件,租户ID: " + tenantId
+ ",预期位置: classpath:" + certDir + "pub_key.pem"
+ "(请从商户平台-API安全下载微信支付公钥后放到该目录)", null);
}
/**
* 获取生产环境微信支付公钥路径
*/
private String resolveProdPublicKeyPath(Payment payment) throws PaymentException {
if (!StringUtils.hasText(payment.getPubKey())) {
throw PaymentException.systemError(
"已配置微信支付公钥ID但公钥文件(pubKey)未配置,商户号: " + payment.getMchId(), null);
}
String publicKeyPath = certificateService.getWechatPayCertPath(payment.getPubKey());
if (publicKeyPath == null || !new File(publicKeyPath).exists()) {
throw PaymentException.systemError(
"微信支付公钥文件不存在: " + publicKeyPath + ",商户号: " + payment.getMchId(), null);
}
log.debug("生产环境微信支付公钥路径: {}", publicKeyPath);
return publicKeyPath;
}
/**
* 创建微信支付配置对象
*/
private Config createWxPayConfig(Payment payment, String certificatePath) throws PaymentException {
private Config createWxPayConfig(Payment payment, String certificatePath, String publicKeyPath) throws PaymentException {
try {
if ("dev".equals(activeProfile) && payment == null) {
// 开发环境测试配置
return createDevTestConfig(certificatePath);
} else if (payment != null) {
// 正常配置
return createNormalConfig(payment, certificatePath);
return createNormalConfig(payment, certificatePath, publicKeyPath);
} else {
throw PaymentException.systemError("无法创建微信支付配置:配置信息不完整", null);
}
@@ -305,21 +390,57 @@ public class WxPayConfigService {
/**
* 创建正常配置
*/
private Config createNormalConfig(Payment payment, String certificatePath) throws PaymentException {
private Config createNormalConfig(Payment payment, String certificatePath, String publicKeyPath) throws PaymentException {
// 验证配置完整性
validatePaymentConfig(payment);
log.info("使用数据库支付配置");
log.debug("商户号: {}", payment.getMchId());
return buildRsaConfig(payment, certificatePath, publicKeyPath);
}
/**
* 构建RSA配置
*
* <p>配置了微信支付公钥ID与公钥文件时使用公钥模式(RSAPublicKeyConfig),
* 否则使用平台证书自动下载模式(RSAAutoCertificateConfig)。
* 公钥模式下不会请求 /v3/certificates,因此不受「无可用的平台证书」404影响。</p>
*
* @param payment 支付配置
* @param privateKeyPath 商户私钥文件路径
* @param publicKeyPath 微信支付公钥文件路径,可为 null
* @return 微信支付配置
*/
static Config buildRsaConfig(Payment payment, String privateKeyPath, String publicKeyPath) {
if (usePublicKeyMode(payment.getPubKeyId(), publicKeyPath)) {
log.info("使用微信支付公钥模式,商户号: {}, 公钥ID: {}", payment.getMchId(), payment.getPubKeyId());
return new RSAPublicKeyConfig.Builder()
.merchantId(payment.getMchId())
.privateKeyFromPath(privateKeyPath)
.merchantSerialNumber(payment.getMerchantSerialNumber())
.publicKeyFromPath(publicKeyPath)
.publicKeyId(payment.getPubKeyId())
.apiV3Key(payment.getApiKey())
.build();
}
log.info("使用微信支付平台证书自动下载模式,商户号: {}", payment.getMchId());
return new RSAAutoCertificateConfig.Builder()
.merchantId(payment.getMchId())
.privateKeyFromPath(certificatePath)
.privateKeyFromPath(privateKeyPath)
.merchantSerialNumber(payment.getMerchantSerialNumber())
.apiV3Key(payment.getApiKey())
.build();
}
/**
* 是否使用微信支付公钥模式:配置了公钥ID且公钥文件可用
*/
static boolean usePublicKeyMode(String publicKeyId, String publicKeyPath) {
return StringUtils.hasText(publicKeyId) && StringUtils.hasText(publicKeyPath);
}
/**
* 验证支付配置完整性
*/
@@ -356,10 +477,14 @@ public class WxPayConfigService {
/**
* 清除指定租户的配置缓存
*
* <p>同时清除内存中的微信支付配置与Redis中的支付配置(Payment:1:{tenantId})
* 否则数据库中的支付配置(如公钥ID)变更后最长24小时才能生效。</p>
*
* @param tenantId 租户ID
*/
public void clearConfigCache(Integer tenantId) {
WxNativeUtil.addConfig(tenantId, null);
redisUtil.delete("Payment:1:" + tenantId);
log.info("清除微信支付配置缓存,租户ID: {}", tenantId);
}
}