Merge remote-tracking branch 'origin/dev' into dev

# Conflicts:
#	src/main/resources/application-dev.yml
This commit is contained in:
2025-08-10 02:52:10 +08:00
6 changed files with 689 additions and 39 deletions

View File

@@ -3,12 +3,16 @@ package com.gxwebsoft.common.core.utils;
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.stereotype.Component;
import com.gxwebsoft.common.core.config.ConfigProperties;
import javax.annotation.Resource;
/**
* 微信支付证书自动配置工具类
* 使用RSAAutoCertificateConfig实现证书自动管理
*
*
* @author 科技小王子
* @since 2024-07-26
*/
@@ -16,38 +20,47 @@ import org.springframework.stereotype.Component;
@Component
public class WechatCertAutoConfig {
@Value("${spring.profiles.active:prod}")
private String activeProfile;
@Resource
private ConfigProperties configProperties;
@Resource
private ConfigProperties configProperties;
/**
* 创建微信支付自动证书配置
*
*
* @param merchantId 商户号
* @param privateKeyPath 私钥文件路径
* @param merchantSerialNumber 商户证书序列号
* @param apiV3Key APIv3密钥
* @return 微信支付配置对象
*/
public Config createAutoConfig(String merchantId, String privateKeyPath,
public Config createAutoConfig(String merchantId, String privateKeyPath,
String merchantSerialNumber, String apiV3Key) {
try {
log.info("创建微信支付自动证书配置...");
log.info("商户号: {}", merchantId);
log.info("私钥路径: {}", privateKeyPath);
log.info("证书序列号: {}", merchantSerialNumber);
Config config = new RSAAutoCertificateConfig.Builder()
.merchantId(merchantId)
.privateKeyFromPath(privateKeyPath)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.build();
log.info("✅ 微信支付自动证书配置创建成功");
log.info("🔄 系统将自动管理平台证书的下载和更新");
return config;
} catch (Exception e) {
log.error("❌ 创建微信支付自动证书配置失败: {}", e.getMessage(), e);
// 提供详细的错误诊断信息
log.error("🔍 错误诊断:");
log.error("1. 请检查商户平台是否已开启API安全功能");
@@ -55,28 +68,48 @@ public class WechatCertAutoConfig {
log.error("3. 请验证APIv3密钥和证书序列号是否正确");
log.error("4. 请检查网络连接是否正常");
log.error("5. 请确认私钥文件路径是否正确: {}", privateKeyPath);
throw new RuntimeException("微信支付自动证书配置失败: " + e.getMessage(), e);
}
}
/**
* 使用默认开发环境配置创建自动证书配置
*
* 根据当前环境自动选择证书路径
* 开发环境拼接规则配置文件upload-path + dev/wechat/ + 租户ID
*
* @return 微信支付配置对象
*/
public Config createDefaultDevConfig() {
String merchantId = "1723321338";
String privateKeyPath = "src/main/resources/certs/dev/wechat/apiclient_key.pem";
String privateKeyPath;
String merchantSerialNumber = "2B933F7C35014A1C363642623E4A62364B34C4EB";
String apiV3Key = "0kF5OlPr482EZwtn9zGufUcqa7ovgxRL";
// 根据环境选择证书路径
if ("dev".equals(activeProfile)) {
// 开发环境使用配置文件upload-path拼接证书路径
String uploadPath = configProperties.getUploadPath(); // 配置文件路径
String tenantId = "10550"; // 租户ID
String certPath = uploadPath + "dev/wechat/" + tenantId + "/";
privateKeyPath = certPath + "apiclient_key.pem";
log.info("开发环境使用配置文件upload-path拼接证书路径");
log.info("配置文件upload-path: {}", uploadPath);
log.info("证书基础路径: {}", certPath);
log.info("私钥文件路径: {}", privateKeyPath);
} else {
// 生产环境:使用相对路径,由系统动态解析
privateKeyPath = "src/main/resources/certs/dev/wechat/apiclient_key.pem";
log.info("生产环境:使用相对证书路径 - {}", privateKeyPath);
}
return createAutoConfig(merchantId, privateKeyPath, merchantSerialNumber, apiV3Key);
}
/**
* 测试证书配置是否正常
*
*
* @param config 微信支付配置
* @return 是否配置成功
*/
@@ -84,56 +117,56 @@ public class WechatCertAutoConfig {
try {
// 这里可以添加一些基本的配置验证逻辑
log.info("🧪 测试微信支付证书配置...");
if (config == null) {
log.error("配置对象为空");
return false;
}
log.info("✅ 证书配置测试通过");
return true;
} catch (Exception e) {
log.error("❌ 证书配置测试失败: {}", e.getMessage(), e);
return false;
}
}
/**
* 获取配置使用说明
*
*
* @return 使用说明
*/
public String getUsageInstructions() {
return """
🚀 微信支付自动证书配置使用说明
================================
✅ 优势:
1. 自动下载微信支付平台证书
2. 证书过期时自动更新
3. 无需手动管理 wechatpay_cert.pem 文件
4. 符合微信支付官方最佳实践
📝 使用方法:
// 方法1: 使用默认开发环境配置
Config config = wechatCertAutoConfig.createDefaultDevConfig();
// 方法2: 自定义配置
Config config = wechatCertAutoConfig.createAutoConfig(
"商户号",
"私钥路径",
"证书序列号",
"商户号",
"私钥路径",
"证书序列号",
"APIv3密钥"
);
🔧 前置条件:
1. 微信商户平台已开启API安全功能
2. 已申请使用微信支付公钥
3. 私钥文件存在且路径正确
4. 网络连接正常
📚 更多信息:
https://pay.weixin.qq.com/doc/v3/merchant/4012153196
""";

View File

@@ -16,6 +16,7 @@ import com.gxwebsoft.common.system.service.SettingService;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAConfig;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@@ -41,6 +42,9 @@ public class SettingServiceImpl extends ServiceImpl<SettingMapper, Setting> impl
@Resource
private StringRedisTemplate stringRedisTemplate;
@Value("${spring.profiles.active:prod}")
private String activeProfile;
@Override
public PageResult<Setting> pageRel(SettingParam param) {
PageParam<Setting, SettingParam> page = new PageParam<>(param);
@@ -120,20 +124,41 @@ public class SettingServiceImpl extends ServiceImpl<SettingMapper, Setting> impl
final String merchantSerialNumber = jsonObject.getString("merchantSerialNumber");
final String apiV3key = jsonObject.getString("wechatApiKey");
if(config == null){
// config = new RSAAutoCertificateConfig.Builder()
// .merchantId(mchId)
// .privateKeyFromPath("/Users/gxwebsoft/Documents/uploads/file/20230622/fb193d3bfff0467b83dc498435a4f433.pem")
// .merchantSerialNumber(merchantSerialNumber)
// .apiV3Key(apiV3key)
// .build();
config =
new RSAConfig.Builder()
// 根据环境选择不同的证书路径配置
if ("dev".equals(activeProfile)) {
// 开发环境使用配置文件的upload-path拼接证书路径 - 租户ID 10550
System.out.println("=== 开发环境使用配置文件upload-path拼接证书路径 ===");
String uploadPath = pathConfig.getUploadPath(); // 获取配置的upload-path
String tenantId = "10550"; // 租户ID
String certBasePath = uploadPath + "dev/wechat/" + tenantId + "/";
String devPrivateKeyPath = certBasePath + "apiclient_key.pem";
String devCertPath = certBasePath + "apiclient_cert.pem";
System.out.println("配置的upload-path: " + uploadPath);
System.out.println("证书基础路径: " + certBasePath);
System.out.println("私钥文件路径: " + devPrivateKeyPath);
System.out.println("证书文件路径: " + devCertPath);
config = new RSAConfig.Builder()
.merchantId("1246610101")
.privateKeyFromPath("/Users/gxwebsoft/cert/1246610101_20221225_cert/01ac632fea184e248d0375e9917063a4.pem")
.privateKeyFromPath(devPrivateKeyPath)
.merchantSerialNumber("2903B872D5CA36E525FAEC37AEDB22E54ECDE7B7")
.wechatPayCertificatesFromPath("/Users/gxwebsoft/cert/1246610101_20221225_cert/bac91dfb3ef143328dde489004c6d002.pem")
.wechatPayCertificatesFromPath(devCertPath)
.build();
System.out.println("开发环境证书路径配置完成");
} else {
// 生产环境:使用数据库存储的路径
System.out.println("=== 生产环境:使用数据库存储的证书路径 ===");
config = new RSAConfig.Builder()
.merchantId(mchId)
.privateKeyFromPath(privateKey)
.merchantSerialNumber(merchantSerialNumber)
.wechatPayCertificatesFromPath(apiclientCert)
.build();
System.out.println("生产环境证书路径: " + privateKey);
}
configMap.put(data.getTenantId().toString(),config);
System.out.println("当前环境: " + activeProfile);
System.out.println("config = " + config);
}
if (service == null) {

View File

@@ -0,0 +1,155 @@
package com.gxwebsoft.test;
import com.gxwebsoft.common.core.config.ConfigProperties;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.beans.factory.annotation.Value;
import javax.annotation.Resource;
import java.io.File;
/**
* 证书路径拼接测试
* 验证开发环境的路径拼接规则配置文件upload-path + dev/wechat/ + 租户ID
*
* @author 科技小王子
* @since 2025-08-09
*/
@SpringBootTest
@ActiveProfiles("dev")
public class CertificatePathConcatenationTest {
@Value("${spring.profiles.active:prod}")
private String activeProfile;
@Resource
private ConfigProperties configProperties;
@Test
public void testCertificatePathConcatenation() {
System.out.println("=== 证书路径拼接测试 ===");
System.out.println("当前环境: " + activeProfile);
if ("dev".equals(activeProfile)) {
testDevEnvironmentPathConcatenation();
} else {
testProdEnvironmentPathConcatenation();
}
System.out.println("=== 证书路径拼接测试完成 ===");
}
private void testDevEnvironmentPathConcatenation() {
System.out.println("--- 开发环境路径拼接测试 ---");
// 获取配置文件中的upload-path
String uploadPath = configProperties.getUploadPath();
System.out.println("配置文件upload-path: " + uploadPath);
// 拼接规则配置文件upload-path + dev/wechat/ + 租户ID
String tenantId = "10550";
String certBasePath = uploadPath + "dev/wechat/" + tenantId + "/";
String privateKeyPath = certBasePath + "apiclient_key.pem";
String certPath = certBasePath + "apiclient_cert.pem";
System.out.println("拼接规则: upload-path + dev/wechat/ + 租户ID");
System.out.println("租户ID: " + tenantId);
System.out.println("证书基础路径: " + certBasePath);
System.out.println("私钥文件路径: " + privateKeyPath);
System.out.println("证书文件路径: " + certPath);
// 验证路径是否正确
File privateKeyFile = new File(privateKeyPath);
File certFile = new File(certPath);
System.out.println("--- 文件存在性验证 ---");
System.out.println("私钥文件存在: " + privateKeyFile.exists());
System.out.println("证书文件存在: " + certFile.exists());
if (privateKeyFile.exists()) {
System.out.println("✅ 私钥文件路径拼接正确");
System.out.println(" 文件大小: " + privateKeyFile.length() + " bytes");
} else {
System.out.println("❌ 私钥文件路径拼接错误或文件不存在");
}
if (certFile.exists()) {
System.out.println("✅ 证书文件路径拼接正确");
System.out.println(" 文件大小: " + certFile.length() + " bytes");
} else {
System.out.println("❌ 证书文件路径拼接错误或文件不存在");
}
// 验证期望的路径
String expectedPath = "/Users/gxwebsoft/JAVA/cms-java-code/src/main/resources/dev/wechat/10550/";
System.out.println("--- 路径验证 ---");
System.out.println("期望的证书路径: " + expectedPath);
System.out.println("实际拼接路径: " + certBasePath);
System.out.println("路径匹配: " + expectedPath.equals(certBasePath));
if (expectedPath.equals(certBasePath)) {
System.out.println("✅ 路径拼接规则正确");
} else {
System.out.println("❌ 路径拼接规则需要调整");
System.out.println(" 请检查配置文件中的upload-path设置");
}
}
private void testProdEnvironmentPathConcatenation() {
System.out.println("--- 生产环境路径配置测试 ---");
System.out.println("生产环境使用数据库配置的证书路径");
System.out.println("路径格式: {uploadPath}/file/{relativePath}");
String uploadPath = configProperties.getUploadPath();
System.out.println("配置的upload-path: " + uploadPath);
// 模拟生产环境路径拼接
String relativePath = "wechat/10550/apiclient_key.pem";
String prodPath = uploadPath + "file/" + relativePath;
System.out.println("生产环境示例路径: " + prodPath);
System.out.println("✅ 生产环境路径配置逻辑正确");
}
@Test
public void testMultipleTenantPaths() {
System.out.println("=== 多租户路径拼接测试 ===");
if (!"dev".equals(activeProfile)) {
System.out.println("跳过:仅在开发环境测试多租户路径");
return;
}
String uploadPath = configProperties.getUploadPath();
String[] tenantIds = {"10324", "10398", "10547", "10549", "10550"};
System.out.println("配置文件upload-path: " + uploadPath);
System.out.println("测试多个租户的证书路径拼接:");
for (String tenantId : tenantIds) {
String certBasePath = uploadPath + "dev/wechat/" + tenantId + "/";
String privateKeyPath = certBasePath + "apiclient_key.pem";
File privateKeyFile = new File(privateKeyPath);
System.out.println("租户 " + tenantId + ": " + (privateKeyFile.exists() ? "" : "") + " " + privateKeyPath);
}
System.out.println("=== 多租户路径拼接测试完成 ===");
}
@Test
public void testConfigurationProperties() {
System.out.println("=== 配置属性测试 ===");
System.out.println("当前环境: " + activeProfile);
System.out.println("ConfigProperties注入: " + (configProperties != null ? "" : ""));
if (configProperties != null) {
System.out.println("upload-path: " + configProperties.getUploadPath());
System.out.println("upload-location: " + configProperties.getUploadLocation());
System.out.println("server-url: " + configProperties.getServerUrl());
}
System.out.println("=== 配置属性测试完成 ===");
}
}

View File

@@ -0,0 +1,99 @@
package com.gxwebsoft.test;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* 证书路径修复验证测试
*
* @author 科技小王子
* @since 2025-08-09
*/
@SpringBootTest
@ActiveProfiles("dev")
public class CertificatePathFixTest {
private static final String CERT_BASE_PATH = "/Users/gxwebsoft/JAVA/cms-java-code/src/main/resources/dev/wechat/10550";
@Test
public void testCertificatePathFix() {
System.out.println("=== 证书路径修复验证测试 ===");
// 验证证书目录存在
File certDir = new File(CERT_BASE_PATH);
assert certDir.exists() && certDir.isDirectory() : "证书目录不存在: " + CERT_BASE_PATH;
System.out.println("✅ 证书目录存在: " + CERT_BASE_PATH);
// 验证私钥文件
String privateKeyPath = CERT_BASE_PATH + "/apiclient_key.pem";
File privateKeyFile = new File(privateKeyPath);
assert privateKeyFile.exists() && privateKeyFile.isFile() : "私钥文件不存在: " + privateKeyPath;
System.out.println("✅ 私钥文件存在: " + privateKeyPath);
System.out.println(" 文件大小: " + privateKeyFile.length() + " bytes");
// 验证证书文件
String certPath = CERT_BASE_PATH + "/apiclient_cert.pem";
File certFile = new File(certPath);
assert certFile.exists() && certFile.isFile() : "证书文件不存在: " + certPath;
System.out.println("✅ 证书文件存在: " + certPath);
System.out.println(" 文件大小: " + certFile.length() + " bytes");
// 验证文件内容格式
try {
String privateKeyContent = Files.readString(Paths.get(privateKeyPath));
assert privateKeyContent.contains("-----BEGIN PRIVATE KEY-----") : "私钥文件格式错误";
System.out.println("✅ 私钥文件格式正确");
String certContent = Files.readString(Paths.get(certPath));
assert certContent.contains("-----BEGIN CERTIFICATE-----") : "证书文件格式错误";
System.out.println("✅ 证书文件格式正确");
} catch (Exception e) {
throw new RuntimeException("读取证书文件失败: " + e.getMessage(), e);
}
System.out.println("=== 证书路径修复验证完成 ===");
System.out.println("🎉 所有证书文件验证通过!");
System.out.println();
System.out.println("📋 修复内容总结:");
System.out.println("1. 修复了 SettingServiceImpl 中的硬编码证书路径");
System.out.println("2. 更新了 WechatCertAutoConfig 中的默认开发环境配置");
System.out.println("3. 证书路径已指向正确位置: " + CERT_BASE_PATH);
System.out.println();
System.out.println("🔧 下一步建议:");
System.out.println("1. 重启应用程序以使配置生效");
System.out.println("2. 测试微信支付功能是否正常工作");
System.out.println("3. 检查应用日志确认证书加载成功");
}
@Test
public void testCertificatePathStructure() {
System.out.println("=== 证书目录结构验证 ===");
File baseDir = new File("/Users/gxwebsoft/JAVA/cms-java-code/src/main/resources/dev/wechat");
if (baseDir.exists()) {
File[] tenantDirs = baseDir.listFiles(File::isDirectory);
if (tenantDirs != null) {
System.out.println("发现租户证书目录:");
for (File tenantDir : tenantDirs) {
System.out.println(" - 租户ID: " + tenantDir.getName());
File privateKey = new File(tenantDir, "apiclient_key.pem");
File cert = new File(tenantDir, "apiclient_cert.pem");
File p12 = new File(tenantDir, "apiclient_cert.p12");
System.out.println(" 私钥文件: " + (privateKey.exists() ? "" : ""));
System.out.println(" 证书文件: " + (cert.exists() ? "" : ""));
System.out.println(" P12文件: " + (p12.exists() ? "" : ""));
}
}
}
System.out.println("=== 目录结构验证完成 ===");
}
}

View File

@@ -0,0 +1,119 @@
package com.gxwebsoft.test;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.beans.factory.annotation.Value;
import javax.annotation.Resource;
import java.io.File;
/**
* 基于环境的证书路径配置测试
*
* @author 科技小王子
* @since 2025-08-09
*/
@SpringBootTest
@ActiveProfiles("dev")
public class EnvironmentBasedCertificateTest {
@Value("${spring.profiles.active:prod}")
private String activeProfile;
@Test
public void testEnvironmentBasedCertificateConfig() {
System.out.println("=== 环境基础证书配置测试 ===");
System.out.println("当前激活的环境: " + activeProfile);
if ("dev".equals(activeProfile)) {
System.out.println("✅ 检测到开发环境");
testDevEnvironmentCertificates();
} else {
System.out.println("✅ 检测到生产环境");
testProdEnvironmentCertificates();
}
System.out.println("=== 环境基础证书配置测试完成 ===");
}
private void testDevEnvironmentCertificates() {
System.out.println("--- 开发环境证书路径测试 ---");
String devCertPath = "/Users/gxwebsoft/JAVA/cms-java-code/src/main/resources/dev/wechat/10550";
String privateKeyPath = devCertPath + "/apiclient_key.pem";
String certPath = devCertPath + "/apiclient_cert.pem";
System.out.println("开发环境证书目录: " + devCertPath);
System.out.println("私钥文件路径: " + privateKeyPath);
System.out.println("证书文件路径: " + certPath);
// 验证文件存在
File privateKeyFile = new File(privateKeyPath);
File certFile = new File(certPath);
assert privateKeyFile.exists() : "开发环境私钥文件不存在: " + privateKeyPath;
assert certFile.exists() : "开发环境证书文件不存在: " + certPath;
System.out.println("✅ 开发环境证书文件验证通过");
System.out.println(" - 私钥文件大小: " + privateKeyFile.length() + " bytes");
System.out.println(" - 证书文件大小: " + certFile.length() + " bytes");
}
private void testProdEnvironmentCertificates() {
System.out.println("--- 生产环境证书路径测试 ---");
System.out.println("生产环境将使用数据库配置的证书路径");
System.out.println("证书路径格式: {uploadPath}/file/{relativePath}");
System.out.println("✅ 生产环境配置逻辑正确");
}
@Test
public void testCertificatePathLogic() {
System.out.println("=== 证书路径逻辑测试 ===");
// 模拟不同环境的路径构建逻辑
String uploadPath = "/www/wwwroot/file.ws/";
String relativePath = "wechat/10550/apiclient_key.pem";
if ("dev".equals(activeProfile)) {
// 开发环境:使用固定的本地路径
String devPath = "/Users/gxwebsoft/JAVA/cms-java-code/src/main/resources/dev/wechat/10550/apiclient_key.pem";
System.out.println("开发环境路径: " + devPath);
File devFile = new File(devPath);
System.out.println("开发环境文件存在: " + devFile.exists());
} else {
// 生产环境:使用数据库配置的路径
String prodPath = uploadPath + "file/" + relativePath;
System.out.println("生产环境路径: " + prodPath);
System.out.println("生产环境路径构建逻辑正确");
}
System.out.println("=== 证书路径逻辑测试完成 ===");
}
@Test
public void testEnvironmentSwitching() {
System.out.println("=== 环境切换测试 ===");
// 测试环境判断逻辑
System.out.println("当前环境: " + activeProfile);
if ("dev".equals(activeProfile)) {
System.out.println("✅ 开发环境配置:");
System.out.println(" - 使用本地固定证书路径");
System.out.println(" - 路径: /Users/gxwebsoft/JAVA/cms-java-code/src/main/resources/dev/wechat/10550/");
System.out.println(" - 优点: 开发便利,无需配置数据库路径");
} else if ("prod".equals(activeProfile)) {
System.out.println("✅ 生产环境配置:");
System.out.println(" - 使用数据库存储的证书路径");
System.out.println(" - 路径格式: {uploadPath}/file/{relativePath}");
System.out.println(" - 优点: 灵活配置,支持多租户");
} else {
System.out.println("⚠️ 未知环境: " + activeProfile);
System.out.println(" - 默认使用生产环境配置");
}
System.out.println("=== 环境切换测试完成 ===");
}
}