From b2b1a1bdf98bfbf574380d15c462e73e1e63f744 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Sat, 12 Sep 2026 18:47:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(payment):=20=E5=BE=AE=E4=BF=A1=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E6=94=B9=E7=94=A8=E5=85=AC=E9=92=A5=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E3=80=8C=E6=97=A0=E5=8F=AF=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E8=AF=81=E4=B9=A6=E3=80=8D404?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 商户 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。 --- .../core/controller/TestController.java | 6 + .../payment/service/WxPayConfigService.java | 135 +++++++++++++++++- src/main/resources/application-dev.yml | 2 +- .../WxPayConfigServicePublicKeyTest.java | 76 ++++++++++ 4 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 src/test/java/com/gxwebsoft/payment/service/WxPayConfigServicePublicKeyTest.java diff --git a/src/main/java/com/gxwebsoft/common/core/controller/TestController.java b/src/main/java/com/gxwebsoft/common/core/controller/TestController.java index 05c4246..3322509 100644 --- a/src/main/java/com/gxwebsoft/common/core/controller/TestController.java +++ b/src/main/java/com/gxwebsoft/common/core/controller/TestController.java @@ -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> 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); diff --git a/src/main/java/com/gxwebsoft/payment/service/WxPayConfigService.java b/src/main/java/com/gxwebsoft/payment/service/WxPayConfigService.java index a681c3d..3b2252d 100644 --- a/src/main/java/com/gxwebsoft/payment/service/WxPayConfigService.java +++ b/src/main/java/com/gxwebsoft/payment/service/WxPayConfigService.java @@ -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 { } } + /** + * 解析微信支付公钥文件路径 + * + *

商户平台已「申请使用微信支付公钥」的商户,调用 /v3/certificates 会返回 + * RESOURCE_NOT_EXISTS(404),必须改用公钥模式(RSAPublicKeyConfig)。 + * 是否启用公钥模式以数据库中的公钥ID(pubKeyId)为准: + * 配置了公钥ID却没有可用的公钥文件时直接报错,避免退回平台证书自动下载后 + * 再次抛出难以定位的 404。

+ * + * @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配置 + * + *

配置了微信支付公钥ID与公钥文件时使用公钥模式(RSAPublicKeyConfig), + * 否则使用平台证书自动下载模式(RSAAutoCertificateConfig)。 + * 公钥模式下不会请求 /v3/certificates,因此不受「无可用的平台证书」404影响。

+ * + * @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 { /** * 清除指定租户的配置缓存 * + *

同时清除内存中的微信支付配置与Redis中的支付配置(Payment:1:{tenantId}), + * 否则数据库中的支付配置(如公钥ID)变更后最长24小时才能生效。

+ * * @param tenantId 租户ID */ public void clearConfigCache(Integer tenantId) { WxNativeUtil.addConfig(tenantId, null); + redisUtil.delete("Payment:1:" + tenantId); log.info("清除微信支付配置缓存,租户ID: {}", tenantId); } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 049ee35..6656e8b 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -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" diff --git a/src/test/java/com/gxwebsoft/payment/service/WxPayConfigServicePublicKeyTest.java b/src/test/java/com/gxwebsoft/payment/service/WxPayConfigServicePublicKeyTest.java new file mode 100644 index 0000000..dc6f1e6 --- /dev/null +++ b/src/test/java/com/gxwebsoft/payment/service/WxPayConfigServicePublicKeyTest.java @@ -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; + +/** + * 微信支付配置构建测试 + * + *

回归场景:商户平台已「申请使用微信支付公钥」的商户,调用 /v3/certificates + * 会返回 RESOURCE_NOT_EXISTS(404)「无可用的平台证书」。数据库里配置了公钥ID时, + * 必须使用公钥模式(RSAPublicKeyConfig),不能再走平台证书自动下载。

+ * + * @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)"); + } +}