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