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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,4 +55,4 @@ certificate:
|
||||
dev:
|
||||
private-key-file: "apiclient_key.pem"
|
||||
apiclient-cert-file: "apiclient_cert.pem"
|
||||
wechatpay-cert-file: "wechatpay_cert.pem"
|
||||
wechatpay-cert-file: "pub_key.pem"
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.gxwebsoft.payment.service;
|
||||
|
||||
import com.gxwebsoft.common.system.entity.Payment;
|
||||
import com.wechat.pay.java.core.Config;
|
||||
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
|
||||
import com.wechat.pay.java.core.RSAPublicKeyConfig;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 微信支付配置构建测试
|
||||
*
|
||||
* <p>回归场景:商户平台已「申请使用微信支付公钥」的商户,调用 /v3/certificates
|
||||
* 会返回 RESOURCE_NOT_EXISTS(404)「无可用的平台证书」。数据库里配置了公钥ID时,
|
||||
* 必须使用公钥模式(RSAPublicKeyConfig),不能再走平台证书自动下载。</p>
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2026-09-12
|
||||
*/
|
||||
@DisplayName("微信支付公钥模式配置构建")
|
||||
class WxPayConfigServicePublicKeyTest {
|
||||
|
||||
private static final String PRIVATE_KEY_PATH = "src/main/resources/dev/wechat/10626/apiclient_key.pem";
|
||||
private static final String PUBLIC_KEY_PATH = "src/main/resources/dev/wechat/10626/pub_key.pem";
|
||||
private static final String PUBLIC_KEY_ID = "PUB_KEY_ID_0111169820202026091100211540003401";
|
||||
|
||||
private Payment payment() {
|
||||
Payment payment = new Payment();
|
||||
payment.setTenantId(10626);
|
||||
payment.setType(1);
|
||||
payment.setMchId("1116982020");
|
||||
payment.setAppId("wx5c60e74d6b6f997f");
|
||||
payment.setMerchantSerialNumber("143F22047204DE942CA46F0EEA8A7AC32B4B1FCA");
|
||||
payment.setApiKey("test-api-v3-key-32-bytes-length!");
|
||||
payment.setPubKeyId(PUBLIC_KEY_ID);
|
||||
payment.setPubKey("/20260912/1661c8232f4c409b831475a12fcd83e0.pem");
|
||||
return payment;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("配置了公钥ID与公钥文件时使用公钥模式")
|
||||
void shouldUsePublicKeyModeWhenPublicKeyConfigured() {
|
||||
assertTrue(WxPayConfigService.usePublicKeyMode(PUBLIC_KEY_ID, PUBLIC_KEY_PATH));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("未配置公钥ID或公钥文件缺失时不使用公钥模式")
|
||||
void shouldNotUsePublicKeyModeWithoutPublicKey() {
|
||||
assertFalse(WxPayConfigService.usePublicKeyMode(null, PUBLIC_KEY_PATH), "公钥ID为空时不应使用公钥模式");
|
||||
assertFalse(WxPayConfigService.usePublicKeyMode("", PUBLIC_KEY_PATH), "公钥ID为空串时不应使用公钥模式");
|
||||
assertFalse(WxPayConfigService.usePublicKeyMode(PUBLIC_KEY_ID, null), "公钥文件缺失时不应使用公钥模式");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("配置了公钥ID时构建的是公钥配置而非平台证书自动下载配置")
|
||||
void shouldBuildPublicKeyConfigInsteadOfAutoCertificateConfig() {
|
||||
assertTrue(new File(PRIVATE_KEY_PATH).exists(), "缺少测试用商户私钥文件: " + PRIVATE_KEY_PATH);
|
||||
assertTrue(new File(PUBLIC_KEY_PATH).exists(), "缺少测试用微信支付公钥文件: " + PUBLIC_KEY_PATH);
|
||||
|
||||
Payment payment = payment();
|
||||
|
||||
// 公钥模式:本地即可完成构建,不会请求 /v3/certificates
|
||||
Config publicKeyConfig = WxPayConfigService.buildRsaConfig(payment, PRIVATE_KEY_PATH, PUBLIC_KEY_PATH);
|
||||
assertNotNull(publicKeyConfig);
|
||||
assertTrue(publicKeyConfig instanceof RSAPublicKeyConfig,
|
||||
"配置了公钥ID时应构建 RSAPublicKeyConfig,实际: " + publicKeyConfig.getClass().getName());
|
||||
assertFalse(publicKeyConfig instanceof RSAAutoCertificateConfig,
|
||||
"配置了公钥ID时不应构建 RSAAutoCertificateConfig(会触发平台证书下载并返回404)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user