增加主机信息功能

This commit is contained in:
2026-04-18 11:23:28 +08:00
parent 9e1bb5cd70
commit 4604c15f0c
5 changed files with 129 additions and 178 deletions

View File

@@ -0,0 +1,66 @@
package com.jeesite.modules.apps.Module;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* 主机系统监控结果
*
* @author gaoxq
*/
@Data
public class MonitorResult implements Serializable {
private boolean success;
private String message;
/** 主机名 */
private String hostname;
/** 操作系统 */
private String os;
/** 在线时长描述 */
private String uptime;
/** CPU 占用率明细key: us/sy/ni/id/wa/st */
private Map<String, String> cpu;
/** 内存明细total/used/free/available/usagePercent */
private Map<String, String> memory;
/** 各挂载点磁盘信息 */
private List<Map<String, String>> disk;
// ---------------------- 构造工厂 ----------------------
public static MonitorResult ok(String message) {
MonitorResult r = new MonitorResult();
r.success = true;
r.message = message;
return r;
}
public static MonitorResult ok(String message, String hostname, String os, String uptime,
Map<String, String> cpu, Map<String, String> memory,
List<Map<String, String>> disk) {
MonitorResult r = ok(message);
r.hostname = hostname;
r.os = os;
r.uptime = uptime;
r.cpu = cpu;
r.memory = memory;
r.disk = disk;
return r;
}
public static MonitorResult fail(String message) {
MonitorResult r = new MonitorResult();
r.success = false;
r.message = message;
return r;
}
}

View File

@@ -1,109 +0,0 @@
package com.jeesite.modules.utils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
/**
* AES加密解密工具类CBC/PKCS5Padding
* 优化说明修复密钥BUG+解决换行问题+性能优化+安全加固+零依赖
*/
public class AesUtil {
private static final LoggerUtils logger = LoggerUtils.getInstance();
private static final String AES_KEY_HEX = "AD42F6697B035B7580E4FEF93BE20BAD"; // 你的32位16进制密钥
private static final String CHARSET = StandardCharsets.UTF_8.name();
private static final int IV_LENGTH = 16; // AES CBC IV固定16字节
private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
private static final String ALGORITHM = "AES";
private static final ThreadLocal<Cipher> ENCRYPT_CIPHER = ThreadLocal.withInitial(() -> {
try {
return Cipher.getInstance(TRANSFORMATION);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
private static final ThreadLocal<Cipher> DECRYPT_CIPHER = ThreadLocal.withInitial(() -> {
try {
return Cipher.getInstance(TRANSFORMATION);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
/**
* 加密
*/
public static String encrypt(String content) {
return encrypt(content, hex2Bytes(AES_KEY_HEX));
}
/**
* 解密
*/
public static String decrypt(String content) {
return decrypt(content, hex2Bytes(AES_KEY_HEX));
}
public static String encrypt(String content, byte[] key) {
if (content == null || content.isEmpty()) {
return "";
}
try {
SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
IvParameterSpec iv = new IvParameterSpec(Arrays.copyOf(key, IV_LENGTH));
Cipher cipher = ENCRYPT_CIPHER.get();
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
byte[] encryptBytes = cipher.doFinal(content.getBytes(CHARSET));
return Base64.getEncoder().encodeToString(encryptBytes);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String decrypt(String content, byte[] key) {
if (content == null || content.isEmpty()) {
return "";
}
try {
SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
IvParameterSpec iv = new IvParameterSpec(Arrays.copyOf(key, IV_LENGTH));
Cipher cipher = DECRYPT_CIPHER.get();
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
byte[] decryptBytes = cipher.doFinal(Base64.getDecoder().decode(content));
String result = new String(decryptBytes, CHARSET);
return result.trim();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 32位16进制字符串转16字节数组解决你的密钥长度BUG的核心方法
*/
private static byte[] hex2Bytes(String hexStr) {
if (hexStr == null || hexStr.length() % 2 != 0) {
throw new IllegalArgumentException("密钥必须是偶数长度的16进制字符串");
}
byte[] bytes = new byte[hexStr.length() / 2];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) Integer.parseInt(hexStr.substring(i * 2, i * 2 + 2), 16);
}
return bytes;
}
/**
* 生成随机16字节IV向量方案2安全随机IV新业务推荐使用
*/
private static byte[] generateRandomIV() {
byte[] iv = new byte[IV_LENGTH];
ThreadLocalRandom.current().nextBytes(iv);
return iv;
}
}

View File

@@ -1,56 +1,41 @@
package com.jeesite.modules.utils; package com.jeesite.modules.utils;
import java.awt.*; import java.awt.*;
import java.util.Random; import java.util.concurrent.ThreadLocalRandom;
public class KeyUtil { public class KeyUtil {
/**
* 生成随机 key
*
* @param length key 长度
* @param type 1=纯数字, 2=大写字母+数字, 3=小写字母+数字, 4=混合大小写+数字
* @return 随机字符串
*/
public static String ObjKey(int length, int type) { public static String ObjKey(int length, int type) {
Random random = new Random(); String str;
StringBuffer key = new StringBuffer(); switch (type) {
if (type == 1) { case 1: str = "0123456789"; break;
String str = "0123456789"; case 2: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; break;
for (int i = 0; i < length; ++i) { case 3: str = "abcdefghijklmnopqrstuvwxyz0123456789"; break;
//从62个的数字或字母中选择 default: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; break;
int number = random.nextInt(10);
//将产生的数字通过length次承载到key中
key.append(str.charAt(number));
}
return key.toString();
} else if (type == 2) {
String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
for (int i = 0; i < length; ++i) {
//从62个的数字或字母中选择
int number = random.nextInt(36);
//将产生的数字通过length次承载到key中
key.append(str.charAt(number));
}
return key.toString();
} else if (type == 3) {
String str = "abcdefghijklmnopqrstuvwxyz0123456789";
for (int i = 0; i < length; ++i) {
//从62个的数字或字母中选择
int number = random.nextInt(36);
//将产生的数字通过length次承载到key中
key.append(str.charAt(number));
}
return key.toString();
} else {
String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (int i = 0; i < length; ++i) {
//从62个的数字或字母中选择
int number = random.nextInt(62);
//将产生的数字通过length次承载到key中
key.append(str.charAt(number));
}
return key.toString();
} }
ThreadLocalRandom rnd = ThreadLocalRandom.current();
StringBuilder key = new StringBuilder(length);
for (int i = 0; i < length; i++) {
key.append(str.charAt(rnd.nextInt(str.length())));
}
return key.toString();
} }
/**
* 生成随机柔和颜色RGB 100-239
*/
public static String getColor() { public static String getColor() {
int r = 100 + (int) (Math.random() * 140); ThreadLocalRandom rnd = ThreadLocalRandom.current();
int g = 100 + (int) (Math.random() * 140); int r = 100 + rnd.nextInt(140);
int b = 100 + (int) (Math.random() * 140); int g = 100 + rnd.nextInt(140);
int b = 100 + rnd.nextInt(140);
Color color = new Color(r, g, b); Color color = new Color(r, g, b);
return "#" + Integer.toHexString(color.getRGB()).substring(2).toUpperCase(); return "#" + Integer.toHexString(color.getRGB()).substring(2).toUpperCase();
} }

View File

@@ -1,7 +1,7 @@
package com.jeesite.modules.utils; package com.jeesite.modules.utils;
import com.jcraft.jsch.*; import com.jcraft.jsch.*;
import com.jeesite.modules.apps.Module.SftpResult; import com.jeesite.modules.apps.Module.MonitorResult;
import com.jeesite.modules.biz.entity.MySftpAccounts; import com.jeesite.modules.biz.entity.MySftpAccounts;
import io.micrometer.common.util.StringUtils; import io.micrometer.common.util.StringUtils;
@@ -11,7 +11,7 @@ import java.util.*;
/** /**
* 远程主机系统监控工具类 * 远程主机系统监控工具类
* 通过 SSH exec 采集 CPU、内存、磁盘、负载、在线时长等指标 * 通过 SSH exec 采集 CPU、内存、磁盘、负载、在线时长等指标(仅支持 Linux
* *
* @author gaoxq * @author gaoxq
*/ */
@@ -24,10 +24,10 @@ public class MonitorUtil {
/** /**
* 采集远程主机系统信息 * 采集远程主机系统信息
* *
* @param account SSH账号复用 MySftpAccounts 的连接信息) * @param account SSH 账号(复用 MySftpAccounts 的连接信息)
* @return MonitorResult * @return MonitorResult
*/ */
public static SftpResult monitor(MySftpAccounts account) { public static MonitorResult monitor(MySftpAccounts account) {
Session session = null; Session session = null;
ChannelExec exec = null; ChannelExec exec = null;
try { try {
@@ -41,12 +41,12 @@ public class MonitorUtil {
String output = readStream(in); String output = readStream(in);
String err = readStream(errIn); String err = readStream(errIn);
if (!err.isEmpty() && output.isEmpty()) { if (!err.isEmpty() && output.isEmpty()) {
return SftpResult.fail("采集失败: " + err); return MonitorResult.fail("采集失败: " + err);
} }
return SftpResult.ok("采集成功", parse(output)); return parseResult(output);
} catch (Exception e) { } catch (Exception e) {
return SftpResult.fail("采集失败: " + e.getMessage()); return MonitorResult.fail("采集失败: " + e.getMessage());
} finally { } finally {
if (exec != null && exec.isConnected()) exec.disconnect(); if (exec != null && exec.isConnected()) exec.disconnect();
if (session != null && session.isConnected()) session.disconnect(); if (session != null && session.isConnected()) session.disconnect();
@@ -64,17 +64,17 @@ public class MonitorUtil {
"echo '==MARK_DISK==' && df -h"; "echo '==MARK_DISK==' && df -h";
} }
private static Map<String, Object> parse(String output) { // ------------------------------------------------------------------ 解析
Map<String, Object> info = new LinkedHashMap<>();
info.put("hostname", extract(output, "MARK_HOSTNAME", true)); private static MonitorResult parseResult(String output) {
info.put("os", extract(output, "MARK_OS", true)); String hostname = extract(output, "MARK_HOSTNAME", true);
info.put("uptime", extract(output, "MARK_UPTIME", true)); String os = extract(output, "MARK_OS", true);
info.put("cpu", parseCpu(extract(output, "MARK_CPU", false))); String uptime = extract(output, "MARK_UPTIME", true);
info.put("memory", parseMemory(extract(output, "MARK_MEM", false))); Map<String, String> cpu = parseCpu(extract(output, "MARK_CPU", false));
info.put("disk", parseDisk(extract(output, "MARK_DISK", false))); Map<String, String> memory = parseMemory(extract(output, "MARK_MEM", false));
List<Map<String, String>> disk = parseDisk(extract(output, "MARK_DISK", false));
return info; return MonitorResult.ok("采集成功", hostname, os, uptime, cpu, memory, disk);
} }
// ---- CPU ---- // ---- CPU ----
@@ -87,7 +87,7 @@ public class MonitorUtil {
Map<String, String> cpu = new LinkedHashMap<>(); Map<String, String> cpu = new LinkedHashMap<>();
for (String line : block.split("\n")) { for (String line : block.split("\n")) {
if (line.startsWith("Cpu") || line.startsWith("%Cpu")) { if (line.startsWith("Cpu") || line.startsWith("%Cpu")) {
String data = line.replaceFirst(".*?:\\s*", "").replaceFirst("^%Cpu\\(s?\\):\\s*", ""); String data = line.replaceFirst("^%?Cpu\\(s?\\):\\s*", "");
for (String part : data.split(",")) { for (String part : data.split(",")) {
part = part.trim(); part = part.trim();
String[] kv = part.split("\\s+", 2); String[] kv = part.split("\\s+", 2);
@@ -101,6 +101,8 @@ public class MonitorUtil {
return cpu; return cpu;
} }
// ---- 内存 ----
/** /**
* 解析 free -m 输出 * 解析 free -m 输出
* Mem: total used free shared buff/cache available * Mem: total used free shared buff/cache available
@@ -157,7 +159,7 @@ public class MonitorUtil {
return disks; return disks;
} }
// ---- 段落提取 ---- // ---- 工具方法 ----
private static String extract(String output, String mark, boolean singleLine) { private static String extract(String output, String mark, boolean singleLine) {
String start = "==" + mark + "=="; String start = "==" + mark + "==";

View File

@@ -10,18 +10,17 @@ import java.util.List;
@Data @Data
public class PageUtil<T> implements Serializable { public class PageUtil<T> implements Serializable {
private List<T> data; private List<T> data;
private Integer curPage;// 当前页 private Integer curPage;// 当前页
private int totalCount;// 总条数 private int totalCount;// 总条数
private int pageSize; // 每页显示的条数 private int pageSize; // 每页显示的条数
//构造数据 // 构造数据
public PageUtil(int curPage, int pageSize, List<T> data) { public PageUtil(int curPage, int pageSize, List<T> data) {
this.data = data; this.data = data == null ? new ArrayList<>() : data;
this.curPage = curPage; this.curPage = curPage;
this.pageSize = pageSize; this.pageSize = pageSize;
this.totalCount = data.size(); this.totalCount = this.data.size();
} }
public int beginIndex(int pageSize, int curPage) { public int beginIndex(int pageSize, int curPage) {
@@ -32,12 +31,20 @@ public class PageUtil<T> implements Serializable {
return (pageSize * curPage) - 1; return (pageSize * curPage) - 1;
} }
/**
* 返回当前页数据(含边界保护)
*/
public List<T> OkData() { public List<T> OkData() {
List<T> list = new ArrayList<T>(); // 页码保护:小于 1 → 第 1 页,超出范围 → 空列表
for (int i = beginIndex(pageSize, curPage); i <= endIndex(pageSize, curPage); i++) { int safePage = (curPage == null || curPage < 1) ? 1 : curPage;
if (i < totalCount) { int begin = beginIndex(pageSize, safePage);
list.add(data.get(i)); if (begin >= totalCount || pageSize <= 0) {
} return new ArrayList<>();
}
List<T> list = new ArrayList<>();
int end = Math.min(endIndex(pageSize, safePage), totalCount - 1);
for (int i = begin; i <= end; i++) {
list.add(data.get(i));
} }
return list; return list;
} }