初始提交

This commit is contained in:
2026-02-05 19:52:29 +08:00
commit 7f725c6e65
49 changed files with 2910 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
package top.gtb520.java.pve_back_api;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import top.gtb520.java.pve_back_api.config.AppConfig;
import top.gtb520.java.pve_back_api.config.ConfigManager;
import java.util.Collection;
import java.util.Collections;
@SpringBootApplication
public class Main {
public static void main(String[] args) {
System.out.println("=== PVE后端API服务启动 ===");
// 初始化配置系统
initializeConfiguration();
// 执行初始化
Main main = new Main();
main.postInitialization();
// 启动Spring Boot应用
SpringApplication app = new SpringApplication(Main.class);
app.setDefaultProperties(Collections.singletonMap("server.port", AppConfig.HTTP_PORT));
app.run(args);
System.out.println("Spring框架加载完成!");
}
/**
* 初始化配置系统
*/
private static void initializeConfiguration() {
try {
String configPath = ConfigManager.initializeConfig();
if (configPath != null) {
System.out.println("配置系统初始化成功: " + configPath);
// 验证配置完整性
if (!AppConfig.validateRequiredConfig()) {
System.err.println("警告: 必要配置项不完整,请检查配置文件");
}
} else {
System.err.println("配置系统初始化失败!");
// 使用默认配置继续运行
AppConfig.markAsLoaded();
}
} catch (Exception e) {
System.err.println("配置初始化异常: " + e.getMessage());
e.printStackTrace();
// 即使配置失败也继续启动应用
AppConfig.markAsLoaded();
}
}
/**
* 应用启动后的初始化工作
*/
public void postInitialization() {
System.out.println("=== 应用初始化 ===");
if (AppConfig.isConfigLoaded()) {
System.out.println("配置状态: 已加载");
// 显示关键配置信息
System.out.println("服务地址: " + AppConfig.HTTP_IP + ":" + AppConfig.HTTP_PORT);
System.out.println("PVE地址: " + AppConfig.PVE_URL);
// 可以在这里添加其他初始化逻辑
performAdditionalInitialization();
} else {
System.out.println("配置状态: 未加载,使用默认配置");
}
System.out.println("应用初始化完成!");
}
/**
* 执行额外的初始化任务
*/
private void performAdditionalInitialization() {
// 这里可以添加数据库连接测试、PVE连接验证等
try {
// 示例:测试数据库连接配置
if (AppConfig.MYSQL_JDBC != null && !AppConfig.MYSQL_JDBC.isEmpty()) {
System.out.println("数据库配置已设置");
}
// 示例测试PVE配置
if (AppConfig.PVE_URL != null && !AppConfig.PVE_URL.isEmpty()) {
System.out.println("PVE配置已设置");
}
} catch (Exception e) {
System.err.println("初始化检查时发生错误: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,113 @@
package top.gtb520.java.pve_back_api.config;
/**
* 全局配置管理类
* 将配置文件中的配置项存储到全局静态变量中
*/
public class AppConfig {
// 全局配置
public static volatile String HTTP_IP = "0.0.0.0";
public static volatile String HTTP_PORT = "8080";
// 数据库配置
public static volatile String MYSQL_JDBC = "";
public static volatile String MYSQL_USERNAME = "";
public static volatile String MYSQL_PASSWORD = "";
// PVE配置 - 保留原有URL并新增拆解配置
public static volatile String PVE_URL = "";
public static volatile String PVE_HOSTNAME_IP = "";
public static volatile String PVE_PORT = "8006";
public static volatile String PVE_PROTOCOL = "https";
public static volatile String PVE_API_TOKEN_NAME = "";
public static volatile String PVE_API_TOKEN_ID = "";
public static volatile String PVE_API_TOKEN_SECRET = "";
// 配置状态
private static volatile boolean isLoaded = false;
private AppConfig() {
// 私有构造函数,防止实例化
}
/**
* 标记配置已加载
*/
public static void markAsLoaded() {
isLoaded = true;
}
/**
* 检查配置是否已加载
*/
public static boolean isConfigLoaded() {
return isLoaded;
}
/**
* 获取完整的PVE URL优先使用拆解后的配置构建
*/
public static String getPveFullUrl() {
// 如果有拆解后的配置优先使用它们构建URL
if (PVE_HOSTNAME_IP != null && !PVE_HOSTNAME_IP.isEmpty()) {
return PVE_PROTOCOL + "://" + PVE_HOSTNAME_IP + ":" + PVE_PORT + "/";
}
// 否则返回原始URL
return PVE_URL != null ? PVE_URL : "";
}
/**
* 获取完整的数据库连接URL包含用户名和密码
*/
public static String getFullJdbcUrl() {
if (MYSQL_JDBC == null || MYSQL_JDBC.isEmpty()) {
return "";
}
return MYSQL_JDBC + "?user=" + MYSQL_USERNAME + "&password=" + MYSQL_PASSWORD;
}
/**
* 验证必要配置是否完整
*/
public static boolean validateRequiredConfig() {
return HTTP_IP != null && !HTTP_IP.isEmpty() &&
HTTP_PORT != null && !HTTP_PORT.isEmpty() &&
(PVE_URL != null && !PVE_URL.isEmpty()) ||
(PVE_HOSTNAME_IP != null && !PVE_HOSTNAME_IP.isEmpty());
}
/**
* 打印当前配置信息
*/
public static void printCurrentConfig() {
System.out.println("=== 应用配置信息 ===");
System.out.println("HTTP 监听地址: " + HTTP_IP + ":" + HTTP_PORT);
System.out.println("MySQL 连接: " + (MYSQL_JDBC != null ? "已配置" : "未配置"));
System.out.println("PVE 原始URL: " + (PVE_URL != null ? PVE_URL : "未配置"));
System.out.println("PVE 主机: " + (PVE_HOSTNAME_IP != null ? PVE_HOSTNAME_IP : "未配置"));
System.out.println("PVE 端口: " + PVE_PORT);
System.out.println("PVE 协议: " + PVE_PROTOCOL);
System.out.println("配置加载状态: " + (isLoaded ? "已完成" : "未完成"));
System.out.println("==================");
}
/**
* 重置配置到默认值
*/
public static void resetToDefaults() {
HTTP_IP = "0.0.0.0";
HTTP_PORT = "8080";
MYSQL_JDBC = "";
MYSQL_USERNAME = "";
MYSQL_PASSWORD = "";
PVE_URL = "";
PVE_HOSTNAME_IP = "";
PVE_PORT = "8006";
PVE_PROTOCOL = "https";
PVE_API_TOKEN_NAME = "";
PVE_API_TOKEN_ID = "";
PVE_API_TOKEN_SECRET = "";
isLoaded = false;
}
}

View File

@@ -0,0 +1,405 @@
package top.gtb520.java.pve_back_api.config;
import org.yaml.snakeyaml.Yaml;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 配置文件管理器
* 负责配置文件的检测、创建、加载和解析
*/
public class ConfigManager {
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static final String CONFIG_TEMPLATE_PATH = "config.yaml";
private static final String CONFIG_DIR_NAME = "conf";
private static final String CONFIG_FILE_NAME = "config.yaml";
/**
* 初始化配置系统
* @return 配置文件路径
*/
public static String initializeConfig() {
lock.writeLock().lock();
try {
String jarDir = getApplicationDirectory();
Path configDir = Paths.get(jarDir, CONFIG_DIR_NAME);
Path configFile = configDir.resolve(CONFIG_FILE_NAME);
System.out.println("应用运行目录: " + jarDir);
// 确保配置目录存在
createConfigDirectory(configDir);
// 检查并创建配置文件
if (!Files.exists(configFile)) {
createDefaultConfigFile(configFile);
} else {
System.out.println("配置文件已存在: " + configFile.toString());
}
// 加载配置到全局变量
loadConfiguration(configFile.toString());
return configFile.toString();
} catch (Exception e) {
System.err.println("配置初始化失败: " + e.getMessage());
e.printStackTrace();
return null;
} finally {
lock.writeLock().unlock();
}
}
/**
* 获取应用运行目录
*/
private static String getApplicationDirectory() {
try {
String classPath = ConfigManager.class.getProtectionDomain()
.getCodeSource().getLocation().getPath();
classPath = java.net.URLDecoder.decode(classPath, "UTF-8");
File classFile = new File(classPath);
if (classFile.isFile() && classPath.endsWith(".jar")) {
return classFile.getParent();
} else if (classFile.isDirectory()) {
String projectDir = classFile.getAbsolutePath();
if (projectDir.contains("target" + File.separator + "classes")) {
return projectDir.substring(0, projectDir.indexOf("target"));
}
return projectDir;
}
} catch (Exception e) {
System.err.println("获取应用目录失败: " + e.getMessage());
}
return System.getProperty("user.dir");
}
/**
* 创建配置目录
*/
private static void createConfigDirectory(Path configDir) throws IOException {
if (!Files.exists(configDir)) {
Files.createDirectories(configDir);
System.out.println("创建配置目录: " + configDir.toString());
}
}
/**
* 创建默认配置文件
*/
private static void createDefaultConfigFile(Path configFile) throws IOException {
System.out.println("创建默认配置文件: " + configFile.toString());
try (InputStream templateStream = getResourceAsStream(CONFIG_TEMPLATE_PATH);
OutputStream outputStream = Files.newOutputStream(configFile)) {
if (templateStream == null) {
createMinimalConfigFile(configFile);
} else {
byte[] buffer = new byte[1024];
int length;
while ((length = templateStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
System.out.println("从模板创建配置文件成功");
}
}
}
/**
* 创建最小化配置文件(当模板不可用时)
*/
private static void createMinimalConfigFile(Path configFile) throws IOException {
String minimalConfig = "# PVE后端API配置文件\n" +
"global:\n" +
" http_ip: \"0.0.0.0\"\n" +
" http_port: \"8080\"\n\n" +
"database:\n" +
" mysql_jdbc: \"\"\n" +
" mysql_username: \"\"\n" +
" mysql_password: \"\"\n\n" +
"pve:\n" +
" # 推荐使用拆分配置(优先级更高)\n" +
" hostname_ip: \"10.168.2.18\"\n" +
" port: \"8006\"\n" +
" protocol: \"https\"\n" +
" # 或者使用完整URL向后兼容\n" +
" url: \"https://10.168.2.18:8006/\"\n" +
" # API Token配置\n" +
" api_tocken_name: \"dev\"\n" +
" api_token_id: \"root@pam!dev\"\n" +
" api_token_secret: \"68e9dac5-3110-4f1b-b33a-feeaa4014f63\"\n";
Files.write(configFile, minimalConfig.getBytes("UTF-8"));
System.out.println("创建最小化配置文件成功");
}
/**
* 从资源获取输入流
*/
private static InputStream getResourceAsStream(String resourcePath) {
// 首先尝试从类路径获取
InputStream stream = ConfigManager.class.getClassLoader().getResourceAsStream(resourcePath);
if (stream != null) {
return stream;
}
// 如果类路径中没有,尝试从文件系统获取
try {
Path resourceFile = Paths.get(resourcePath);
if (Files.exists(resourceFile)) {
return Files.newInputStream(resourceFile);
}
} catch (IOException e) {
// 忽略文件系统访问异常
}
return null;
}
/**
* 加载配置文件到全局变量
* @param configFilePath 配置文件路径
*/
private static void loadConfiguration(String configFilePath) {
try {
Map<String, Object> configData = loadYamlConfig(configFilePath);
if (configData == null) {
handleEmptyConfig();
return;
}
// 解析并设置配置值
parseAndSetConfig(configData);
AppConfig.markAsLoaded();
System.out.println("✅ 配置加载成功!");
AppConfig.printCurrentConfig();
} catch (Exception e) {
handleConfigLoadError(e);
}
}
/**
* 加载YAML配置文件
*/
private static Map<String, Object> loadYamlConfig(String configFilePath) throws IOException {
Yaml yaml = new Yaml();
try (InputStream inputStream = Files.newInputStream(Paths.get(configFilePath))) {
return yaml.load(inputStream);
}
}
/**
* 处理空配置情况
*/
private static void handleEmptyConfig() {
System.err.println("❌ 配置文件为空或格式错误");
}
/**
* 处理配置加载错误
*/
private static void handleConfigLoadError(Exception e) {
System.err.println("❌ 加载配置文件失败: " + e.getMessage());
e.printStackTrace();
}
/**
* 解析配置数据并设置到全局变量
* @param data 配置数据映射
*/
private static void parseAndSetConfig(Map<String, Object> data) {
try {
parseGlobalSection(data);
parseDatabaseSection(data);
parsePveSection(data);
} catch (Exception e) {
System.err.println("❌ 解析配置数据时发生错误: " + e.getMessage());
throw e;
}
}
/**
* 解析全局配置部分
*/
private static void parseGlobalSection(Map<String, Object> data) {
if (!data.containsKey("global")) {
return;
}
@SuppressWarnings("unchecked")
Map<String, Object> global = (Map<String, Object>) data.get("global");
AppConfig.HTTP_IP = getStringValue(global, "http_ip", AppConfig.HTTP_IP);
AppConfig.HTTP_PORT = getStringValue(global, "http_port", AppConfig.HTTP_PORT);
}
/**
* 解析数据库配置部分
*/
private static void parseDatabaseSection(Map<String, Object> data) {
if (!data.containsKey("database")) {
return;
}
@SuppressWarnings("unchecked")
Map<String, Object> database = (Map<String, Object>) data.get("database");
AppConfig.MYSQL_JDBC = getStringValue(database, "mysql_jdbc", AppConfig.MYSQL_JDBC);
AppConfig.MYSQL_USERNAME = getStringValue(database, "mysql_username", AppConfig.MYSQL_USERNAME);
AppConfig.MYSQL_PASSWORD = getStringValue(database, "mysql_password", AppConfig.MYSQL_PASSWORD);
}
/**
* 解析PVE配置部分
*/
private static void parsePveSection(Map<String, Object> data) {
if (!data.containsKey("pve")) {
return;
}
@SuppressWarnings("unchecked")
Map<String, Object> pve = (Map<String, Object>) data.get("pve");
processPveUrlConfig(pve);
processPveApiTokens(pve);
}
/**
* 处理PVE URL相关配置
*/
private static void processPveUrlConfig(Map<String, Object> pve) {
// 保留原始URL配置
AppConfig.PVE_URL = getStringValue(pve, "url", AppConfig.PVE_URL);
// 解析拆分后的PVE配置优先级更高
AppConfig.PVE_HOSTNAME_IP = getStringValue(pve, "hostname_ip", AppConfig.PVE_HOSTNAME_IP);
AppConfig.PVE_PORT = getStringValue(pve, "port", AppConfig.PVE_PORT);
AppConfig.PVE_PROTOCOL = getStringValue(pve, "protocol", AppConfig.PVE_PROTOCOL);
// 如果提供了拆分配置同时更新URL
if (hasValidHostname()) {
AppConfig.PVE_URL = buildPveUrl();
}
// 如果只有URL而没有拆分配置则解析URL
else if (hasValidUrl()) {
parsePveUrl(AppConfig.PVE_URL);
}
}
/**
* 处理PVE API令牌配置
*/
private static void processPveApiTokens(Map<String, Object> pve) {
AppConfig.PVE_API_TOKEN_NAME = getStringValue(pve, "api_tocken_name", AppConfig.PVE_API_TOKEN_NAME);
AppConfig.PVE_API_TOKEN_ID = getStringValue(pve, "api_token_id", AppConfig.PVE_API_TOKEN_ID);
AppConfig.PVE_API_TOKEN_SECRET = getStringValue(pve, "api_token_secret", AppConfig.PVE_API_TOKEN_SECRET);
}
/**
* 检查是否有有效的主机名配置
*/
private static boolean hasValidHostname() {
return AppConfig.PVE_HOSTNAME_IP != null && !AppConfig.PVE_HOSTNAME_IP.isEmpty();
}
/**
* 检查是否有有效的URL配置
*/
private static boolean hasValidUrl() {
return AppConfig.PVE_URL != null && !AppConfig.PVE_URL.isEmpty();
}
/**
* 构建PVE完整URL
*/
private static String buildPveUrl() {
return AppConfig.PVE_PROTOCOL + "://" + AppConfig.PVE_HOSTNAME_IP + ":" + AppConfig.PVE_PORT + "/";
}
/**
* 从完整URL解析PVE主机和端口信息
*/
private static void parsePveUrl(String url) {
try {
if (url == null || url.isEmpty()) {
return;
}
// 移除末尾的斜杠
if (url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
// 解析协议
if (url.startsWith("https://")) {
AppConfig.PVE_PROTOCOL = "https";
url = url.substring(8);
} else if (url.startsWith("http://")) {
AppConfig.PVE_PROTOCOL = "http";
url = url.substring(7);
}
// 解析主机和端口
int portIndex = url.lastIndexOf(":");
if (portIndex > 0) {
AppConfig.PVE_HOSTNAME_IP = url.substring(0, portIndex);
String portStr = url.substring(portIndex + 1);
if (!portStr.isEmpty()) {
AppConfig.PVE_PORT = portStr;
}
} else {
AppConfig.PVE_HOSTNAME_IP = url;
}
} catch (Exception e) {
System.err.println("解析PVE URL时发生错误: " + e.getMessage());
}
}
/**
* 安全获取字符串值
*/
private static String getStringValue(Map<String, Object> map, String key, String defaultValue) {
if (map.containsKey(key)) {
Object value = map.get(key);
return value != null ? value.toString() : defaultValue;
}
return defaultValue;
}
/**
* 重新加载配置
*/
public static boolean reloadConfiguration() {
lock.writeLock().lock();
try {
String jarDir = getApplicationDirectory();
Path configFile = Paths.get(jarDir, CONFIG_DIR_NAME, CONFIG_FILE_NAME);
if (Files.exists(configFile)) {
AppConfig.resetToDefaults();
loadConfiguration(configFile.toString());
return true;
} else {
System.err.println("配置文件不存在,无法重新加载");
return false;
}
} catch (Exception e) {
System.err.println("重新加载配置失败: " + e.getMessage());
return false;
} finally {
lock.writeLock().unlock();
}
}
}

View File

@@ -0,0 +1,46 @@
package top.gtb520.java.pve_back_api.config;
import org.yaml.snakeyaml.Yaml;
import java.io.InputStream;
import java.util.Map;
/**
* 全局配置类
* 存储所有配置项的全局变量
*/
public class GlobalConfig {
// 全局配置
public static String HTTP_IP = "0.0.0.0";
public static String HTTP_PORT = "8080";
// 数据库配置
public static String MYSQL_JDBC = "";
public static String MYSQL_USERNAME = "";
public static String MYSQL_PASSWORD = "";
// PVE配置
public static String PVE_URL = "";
public static String PVE_API_TOKEN_NAME = "";
public static String PVE_API_TOKEN_ID = "";
public static String PVE_API_TOKEN_SECRET = "";
// 私有构造函数,防止实例化
private GlobalConfig() {}
/**
* 打印所有配置信息(用于调试)
*/
public static void printConfig() {
System.out.println("=== 当前配置信息 ===");
System.out.println("HTTP IP: " + HTTP_IP);
System.out.println("HTTP PORT: " + HTTP_PORT);
System.out.println("MYSQL JDBC: " + MYSQL_JDBC);
System.out.println("MYSQL USERNAME: " + MYSQL_USERNAME);
System.out.println("PVE URL: " + PVE_URL);
System.out.println("PVE API TOKEN NAME: " + PVE_API_TOKEN_NAME);
System.out.println("PVE API TOKEN ID: " + PVE_API_TOKEN_ID);
System.out.println("===================");
}
}

View File

@@ -0,0 +1,148 @@
package top.gtb520.java.pve_back_api.config;
import org.yaml.snakeyaml.Yaml;
import java.io.InputStream;
import java.util.Map;
/**
* YAML配置文件解析器
* 负责从不同来源加载和解析YAML配置文件
*/
public class YamlConfigLoader {
private static final String GLOBAL_SECTION = "global";
private static final String DATABASE_SECTION = "database";
private static final String PVE_SECTION = "pve";
/**
* 从资源文件加载配置
* @param resourcePath 资源文件路径
*/
public static void loadConfigFromResource(String resourcePath) {
try {
Yaml yaml = new Yaml();
InputStream inputStream = YamlConfigLoader.class.getClassLoader().getResourceAsStream(resourcePath);
if (inputStream == null) {
handleResourceNotFound(resourcePath);
return;
}
Map<String, Object> data = yaml.load(inputStream);
parseConfig(data);
} catch (Exception e) {
handleLoadException("资源", resourcePath, e);
}
}
/**
* 从文件路径加载配置
* @param filePath 文件路径
*/
public static void loadConfigFromFile(String filePath) {
try {
Yaml yaml = new Yaml();
InputStream inputStream = java.nio.file.Files.newInputStream(java.nio.file.Paths.get(filePath));
Map<String, Object> data = yaml.load(inputStream);
parseConfig(data);
} catch (Exception e) {
handleLoadException("文件", filePath, e);
}
}
/**
* 解析配置数据并设置到全局变量
* @param data 配置数据映射
*/
@SuppressWarnings("unchecked")
private static void parseConfig(Map<String, Object> data) {
try {
parseGlobalConfig(data);
parseDatabaseConfig(data);
parsePveConfig(data);
System.out.println("配置加载成功!");
GlobalConfig.printConfig();
} catch (Exception e) {
System.err.println("解析配置数据时发生错误: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 解析全局配置部分
*/
private static void parseGlobalConfig(Map<String, Object> data) {
if (!data.containsKey(GLOBAL_SECTION)) {
return;
}
@SuppressWarnings("unchecked")
Map<String, Object> global = (Map<String, Object>) data.get(GLOBAL_SECTION);
GlobalConfig.HTTP_IP = getStringValue(global, "http_ip", GlobalConfig.HTTP_IP);
GlobalConfig.HTTP_PORT = getStringValue(global, "http_port", GlobalConfig.HTTP_PORT);
}
/**
* 解析数据库配置部分
*/
private static void parseDatabaseConfig(Map<String, Object> data) {
if (!data.containsKey(DATABASE_SECTION)) {
return;
}
@SuppressWarnings("unchecked")
Map<String, Object> database = (Map<String, Object>) data.get(DATABASE_SECTION);
GlobalConfig.MYSQL_JDBC = getStringValue(database, "mysql_jdbc", GlobalConfig.MYSQL_JDBC);
GlobalConfig.MYSQL_USERNAME = getStringValue(database, "mysql_username", GlobalConfig.MYSQL_USERNAME);
GlobalConfig.MYSQL_PASSWORD = getStringValue(database, "mysql_password", GlobalConfig.MYSQL_PASSWORD);
}
/**
* 解析PVE配置部分
*/
private static void parsePveConfig(Map<String, Object> data) {
if (!data.containsKey(PVE_SECTION)) {
return;
}
@SuppressWarnings("unchecked")
Map<String, Object> pve = (Map<String, Object>) data.get(PVE_SECTION);
GlobalConfig.PVE_URL = getStringValue(pve, "url", GlobalConfig.PVE_URL);
GlobalConfig.PVE_API_TOKEN_NAME = getStringValue(pve, "api_tocken_name", GlobalConfig.PVE_API_TOKEN_NAME);
GlobalConfig.PVE_API_TOKEN_ID = getStringValue(pve, "api_token_id", GlobalConfig.PVE_API_TOKEN_ID);
GlobalConfig.PVE_API_TOKEN_SECRET = getStringValue(pve, "api_token_secret", GlobalConfig.PVE_API_TOKEN_SECRET);
}
/**
* 安全获取字符串值
*/
private static String getStringValue(Map<String, Object> map, String key, String defaultValue) {
if (map.containsKey(key)) {
Object value = map.get(key);
return value != null ? value.toString() : defaultValue;
}
return defaultValue;
}
/**
* 处理资源未找到的情况
*/
private static void handleResourceNotFound(String resourcePath) {
System.err.println("❌ 无法找到资源配置文件: " + resourcePath);
}
/**
* 处理加载异常
*/
private static void handleLoadException(String type, String path, Exception e) {
System.err.println("❌ 解析" + type + "配置文件失败: " + path);
System.err.println("错误详情: " + e.getMessage());
e.printStackTrace();
}
}

View File

@@ -0,0 +1,168 @@
package top.gtb520.java.pve_back_api.route.pve;
import it.corsinvest.proxmoxve.api.*;
import org.springframework.web.bind.annotation.GetMapping;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.web.bind.annotation.RestController;
// 导入配置变量
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static top.gtb520.java.pve_back_api.config.AppConfig.*;
/**
* PVE状态相关路由处理器
* 提供与Proxmox VE服务器通信的客户端创建功能和状态查询
*/
@RestController
public class status {
private static final int DEFAULT_TIMEOUT = 120000; // 2分钟超时时间
/**
* API/api/pve/status
* 类型GET
* 获取PVE集群状态信息包括节点、虚拟机、容器等资源状态
*
* @return 包含集群状态信息的列表
* @throws Exception 当获取状态失败时抛出
*/
@GetMapping("/api/pve/status")
public List<Map<String, Object>> GetPveStatus() throws Exception {
System.out.println("开始获取PVE状态信息...");
List<Map<String, Object>> pveStatus = new ArrayList<>();
try {
// 创建PVE客户端
PveClient client = createClient();
// 获取集群资源信息
var resourcesResult = client.getCluster().getResources().resources();
if (resourcesResult.isSuccessStatusCode()) {
JsonNode resources = resourcesResult.getResponse().get("data");
// 统计各类资源
int nodeCount = 0, vmCount = 0, ctCount = 0;
int runningNodes = 0, runningVms = 0, runningCts = 0;
// 处理每个资源
for (JsonNode resource : resources) {
String type = resource.get("type").asText();
String status = resource.get("status").asText();
Map<String, Object> resourceInfo = new HashMap<>();
resourceInfo.put("type", type);
resourceInfo.put("status", status);
switch (type) {
case "node":
nodeCount++;
if ("online".equals(status)) runningNodes++;
resourceInfo.put("name", resource.get("node").asText());
resourceInfo.put("cpu_usage", String.format("%.2f%%",
resource.get("cpu").asDouble() * 100));
resourceInfo.put("memory_usage", String.format("%.2f%%",
(resource.get("mem").asDouble() / resource.get("maxmem").asDouble()) * 100));
resourceInfo.put("uptime", formatUptime(resource.get("uptime").asInt()));
break;
case "qemu":
vmCount++;
if ("running".equals(status)) runningVms++;
resourceInfo.put("vmid", resource.get("vmid").asInt());
resourceInfo.put("name", resource.get("name").asText());
resourceInfo.put("node", resource.get("node").asText());
break;
case "lxc":
ctCount++;
if ("running".equals(status)) runningCts++;
resourceInfo.put("vmid", resource.get("vmid").asInt());
resourceInfo.put("name", resource.get("name").asText());
resourceInfo.put("node", resource.get("node").asText());
break;
}
pveStatus.add(resourceInfo);
}
// 添加统计信息
Map<String, Object> summary = new HashMap<>();
summary.put("summary", true);
summary.put("total_nodes", nodeCount);
summary.put("running_nodes", runningNodes);
summary.put("total_vms", vmCount);
summary.put("running_vms", runningVms);
summary.put("total_containers", ctCount);
summary.put("running_containers", runningCts);
summary.put("timestamp", System.currentTimeMillis());
pveStatus.add(summary);
System.out.println("✅ 成功获取PVE状态信息");
System.out.printf("📊 统计: 节点%d(%d运行), VM%d(%d运行), 容器%d(%d运行)%n",
nodeCount, runningNodes, vmCount, runningVms, ctCount, runningCts);
} else {
// 更安全的错误处理
String errorMsg = "未知错误";
try {
if (resourcesResult != null) {
errorMsg = resourcesResult.getError();
} else {
errorMsg = "API调用返回null结果";
}
} catch (Exception e) {
errorMsg = "错误处理异常: " + e.getClass().getSimpleName() + " - " + e.getMessage();
}
throw new Exception("获取集群资源失败: " + errorMsg);
}
} catch (Exception e) {
System.err.println("❌ 获取PVE状态失败: " + e.getMessage());
throw new Exception("获取PVE状态失败: " + e.getMessage(), e);
}
return pveStatus;
}
/**
* 格式化运行时间
*/
private static String formatUptime(int seconds) {
int days = seconds / 86400;
int hours = (seconds % 86400) / 3600;
int minutes = (seconds % 3600) / 60;
StringBuilder sb = new StringBuilder();
if (days > 0) sb.append(days).append("");
if (hours > 0) sb.append(hours).append("小时 ");
if (minutes > 0) sb.append(minutes).append("分钟");
return sb.toString().trim();
}
/**
* 创建PVE客户端实例
* 支持API令牌认证和传统登录认证两种方式
*
* @return 配置好的PveClient实例
* @throws RuntimeException 当客户端创建失败时抛出
*/
public static PveClient createClient() throws RuntimeException {
// 构建API_TOKEN字符串
String[] api_token_list = PVE_API_TOKEN_ID.split("!");
String api_token = api_token_list[0] + ":" + api_token_list[1] + "@" + "token=" + PVE_API_TOKEN_SECRET;
System.out.println("API_TOKEN: " + api_token);
// 创建PVE客户端实例
var client = new PveClient(PVE_HOSTNAME_IP, Integer.parseInt(PVE_PORT));
client.setValidateCertificate(false); // SSL证书验证
client.setTimeout(DEFAULT_TIMEOUT); // 设置请求超时时间
client.setApiToken(api_token);
return client;
}
}

View File

@@ -0,0 +1,12 @@
package top.gtb520.java.pve_back_api.route;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class test {
@GetMapping("/test")
public String test() {
return "test";
}
}

View File

@@ -0,0 +1 @@
spring.application.name=pve-back-api

View File

@@ -0,0 +1,18 @@
# 全局配置
global:
http_ip: "0.0.0.0"
http_port: "8080"
# 数据库配置
database:
mysql_jdbc: "jdbc:mysql://10.168.2.2:3306/pve_monitor?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&allowMultiQueries=true&useSSL=false&allowPublicKeyRetrieval=true&tinyInt1isBit=false&allowLoadLocalInfile=true&allowLocalInfile=true&allowUrlInLocalInfile=true&allowPublicKeyRetrieval=true&allowMultiQueries=true&allowPublicKeyRetrieval=true&allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowPublicKeyRetrieval=true&allowMultiQueries=true&allowPublicKeyRetrieval=true&allowLoadLocalInfile=true&allowUrlInLocal"
mysql_username: "root"
mysql_password: "Password"
# PVE配置
pve:
# PVE后端地址示例https://192.168.1.1:8006/ 协议HTTPS带结尾的“/”,不保留结尾其他参数)
url: "https://10.168.2.18:8006/"
api_tocken_name: "dev"
api_token_id: "root@pam!dev"
api_token_secret: "68e9dac5-3110-4f1b-b33a-feeaa4014f63"

View File

@@ -0,0 +1,56 @@
package top.gtb520.java.pve_back_api;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import top.gtb520.java.pve_back_api.config.AppConfig;
import top.gtb520.java.pve_back_api.config.ConfigManager;
import top.gtb520.java.pve_back_api.config.YamlConfigLoader;
import top.gtb520.java.pve_back_api.route.pve.status;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class MainTests {
@Test
void contextLoads() {
}
@Test
void testAppConfigInitialization() {
// 测试配置类的基本功能
assertNotNull(AppConfig.HTTP_IP);
assertNotNull(AppConfig.HTTP_PORT);
assertFalse(AppConfig.isConfigLoaded());
}
@Test
void testConfigManagerMethodsExist() {
// 测试ConfigManager类的方法是否存在
assertTrue(ConfigManager.class.getDeclaredMethods().length > 0);
}
@Test
void testYamlConfigLoaderMethodsExist() {
// 测试YamlConfigLoader类的方法是否存在
assertTrue(YamlConfigLoader.class.getDeclaredMethods().length > 0);
}
@Test
void testStatusClassExists() {
// 测试status类是否存在createClient方法
assertNotNull(status.class.getDeclaredMethods());
}
@Test
void testPveUrlBuilding() {
// 测试PVE URL构建逻辑
AppConfig.PVE_PROTOCOL = "https";
AppConfig.PVE_HOSTNAME_IP = "192.168.1.100";
AppConfig.PVE_PORT = "8006";
String expectedUrl = "https://192.168.1.100:8006/";
assertEquals(expectedUrl, AppConfig.getPveFullUrl());
}
}

View File

@@ -0,0 +1,79 @@
package top.gtb520.java.pve_back_api;
import org.junit.jupiter.api.Test;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import reactor.netty.http.client.HttpClient;
import reactor.netty.tcp.TcpClient;
public class ResourcesResultTest {
/**
* 创建忽略SSL证书验证的WebClient实例
* 用于测试环境或自签名证书场景
*/
private static WebClient createInsecureWebClient() {
try {
// 使用Netty内置的不安全信任管理器工厂
TcpClient tcpClient = TcpClient.create()
.secure(sslContextSpec -> {
try {
sslContextSpec.sslContext(
io.netty.handler.ssl.SslContextBuilder.forClient()
.trustManager(io.netty.handler.ssl.util.InsecureTrustManagerFactory.INSTANCE)
.build()
);
} catch (Exception e) {
throw new RuntimeException("SSL配置失败", e);
}
});
// 创建HttpClient和连接器
HttpClient httpClient = HttpClient.from(tcpClient);
ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
// 构建并返回WebClient
return WebClient.builder()
.clientConnector(connector)
.build();
} catch (Exception e) {
throw new RuntimeException("创建WebClient失败", e);
}
}
@Test
void testHttpsRequest() {
System.out.println("开始发送HTTPS请求...");
try {
// 创建忽略证书验证的WebClient
WebClient client = createInsecureWebClient();
// 发送HTTPS GET请求
Mono<String> result = client.get()
.uri("https://10.168.2.2:8013/")
.retrieve()
.bodyToMono(String.class);
// 阻塞获取响应结果
String response = result.block();
System.out.println("✅ 请求成功!");
System.out.println("响应内容长度: " + (response != null ? response.length() : 0) + " 字符");
// 如果需要查看完整响应内容,取消下面的注释
// System.out.println("响应内容: " + response);
} catch (Exception e) {
System.err.println("❌ HTTPS请求失败: " + e.getMessage());
System.err.println("可能的原因:");
System.err.println("1. 目标服务器不可达");
System.err.println("2. 端口未开放");
System.err.println("3. 网络连接问题");
System.err.println("4. SSL配置问题");
e.printStackTrace();
}
}
}