Compare commits

..

18 Commits

Author SHA1 Message Date
76b09ed6b2 增加主机信息功能 2026-04-19 22:27:27 +08:00
d05108888a 增加主机信息功能 2026-04-19 21:55:17 +08:00
f6637a255d 增加主机信息功能 2026-04-19 21:44:59 +08:00
bc5ab27986 增加主机信息功能 2026-04-19 21:21:17 +08:00
880bec1745 增加主机信息功能 2026-04-18 12:25:44 +08:00
a8c4c44c16 增加主机信息功能 2026-04-18 12:02:19 +08:00
4604c15f0c 增加主机信息功能 2026-04-18 11:23:28 +08:00
9e1bb5cd70 增加主机信息功能 2026-04-18 11:07:46 +08:00
1821d266af 增加主机信息功能 2026-04-18 11:07:29 +08:00
dbef9c7a06 增加主机信息功能 2026-04-18 10:49:52 +08:00
d78366fed1 增加主机信息功能 2026-04-18 10:47:30 +08:00
1c0432262d 首页接口重构 2026-04-17 09:56:18 +08:00
50db270ffc 首页接口重构 2026-04-17 09:55:42 +08:00
b94009c94c 首页接口重构 2026-04-14 10:45:51 +08:00
d26914b93c 首页接口重构 2026-04-14 09:08:24 +08:00
3567e9ed97 增加主机信息功能 2026-04-13 23:13:10 +08:00
3a79513e95 增加主机信息功能 2026-04-13 22:49:09 +08:00
4103f2d7ac refactor(DockerUtil): 新增SSH连接池,复用Session 2026-04-13 22:35:04 +08:00
48 changed files with 3765 additions and 553 deletions

View File

@@ -94,10 +94,7 @@
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>jeesite-repos</id>
<url>https://maven.jeesite.net/repository/maven-public</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
@@ -110,10 +107,6 @@
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>jeesite-repos</id>
<url>https://maven.jeesite.net/repository/maven-public</url>
</pluginRepository>
</pluginRepositories>
</project>

View File

@@ -8,14 +8,28 @@ import java.io.Serializable;
* Docker 容器信息
*/
@Data
public class ContainerInfo implements Serializable {
public class ContainerInfo implements Serializable {
private String containerId;
private String image;
private String command;
private String status;
private String created;
private String names;
private String status;
private String ports;
private String names;
private String accountId;
public ContainerInfo() {
}
public ContainerInfo(String containerId, String image, String command, String created, String status, String ports, String names, String accountId) {
this.containerId = containerId;
this.image = image;
this.command = command;
this.created = created;
this.status = status;
this.ports = ports;
this.names = names;
this.accountId = accountId;
}
}

View File

@@ -0,0 +1,23 @@
package com.jeesite.modules.apps.Module.Dict;
import lombok.Data;
import java.io.Serializable;
@Data
public class DataColumn implements Serializable {
private String createTime; // 创建时间
private String columnId; // 字段ID
private String tableId; // 所属表ID
private String tableSchema;
private String tableName;
private String columnName; // 字段名
private String columnComment; // 字段注释
private String columnType; // 字段类型
private String dataType; // 数据类型
private Long maxLength; // 长度
private String isNullable; // 0非空 1可为空
private String isPrimaryKey; // 0否 1是主键
private Integer sort; // 排序
}

View File

@@ -0,0 +1,22 @@
package com.jeesite.modules.apps.Module.Dict;
import lombok.Data;
import java.io.Serializable;
@Data
public class DataSource implements Serializable {
private String sourceId; // 主键ID
private String sourceName; // 连接名称
private String dbName; // 数据库名称
public DataSource() {
}
public DataSource(String sourceId, String sourceName, String dbName) {
this.sourceId = sourceId;
this.sourceName = sourceName;
this.dbName = dbName;
}
}

View File

@@ -0,0 +1,18 @@
package com.jeesite.modules.apps.Module.Dict;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
public class DataTable implements Serializable {
private String createTime; // 创建时间
private String sourceId; // 数据源ID
private String tableName; // 数据表名
private String tableComment; // 表注释
private Long tableRows; // 数据行数
private BigDecimal tableSize;
private String dataSource;
}

View File

@@ -0,0 +1,24 @@
package com.jeesite.modules.apps.Module.Dict;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class TableTree implements Serializable {
DataTable dataTable;
List<DataColumn> dataColumns;
public TableTree() {
}
public TableTree(DataTable dataTable, List<DataColumn> dataColumns) {
this.dataTable = dataTable;
this.dataColumns = dataColumns;
}
}

View File

@@ -15,10 +15,11 @@ public class DockerResult implements Serializable {
private String error;
private String message;
public static DockerResult ok(String output) {
public static DockerResult ok(String output,String message) {
DockerResult r = new DockerResult();
r.success = true;
r.output = output;
r.message = message;
return r;
}

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

@@ -0,0 +1,35 @@
package com.jeesite.modules.apps.Module;
import lombok.Data;
import java.io.Serializable;
@Data
public class SftpResult implements Serializable {
private final boolean success;
private final String message;
private final Object data;
private SftpResult(boolean success, String message, Object data) {
this.success = success;
this.message = message;
this.data = data;
}
public static SftpResult ok(String message) {
return new SftpResult(true, message, null);
}
public static SftpResult ok(String message, Object data) {
return new SftpResult(true, message, data);
}
public static SftpResult fail(String message) {
return new SftpResult(false, message, null);
}
public boolean isSuccess() { return success; }
public String getMessage() { return message; }
public Object getData() { return data; }
}

View File

@@ -5,8 +5,8 @@ import com.jeesite.modules.erp.service.ErpAccountService;
import com.jeesite.modules.erp.service.ErpCategoryService;
import com.jeesite.modules.erp.service.ErpDebtsService;
import com.jeesite.modules.erp.service.ErpTransactionFlowService;
import com.jeesite.modules.utils.BigDecimalUtils;
import com.jeesite.modules.utils.DateUtils;
import com.jeesite.modules.apps.utils.BigDecimalUtils;
import com.jeesite.modules.apps.utils.DateUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -70,8 +70,8 @@ public class erpFlowService {
erpIncome.getCategoryId(), // 分类ID
erpIncome.getRemark(), // 备注
erpIncome.getIncomeId(), // 业务ID
com.jeesite.modules.utils.DateUtils.getCurrentYear(), // 年
com.jeesite.modules.utils.DateUtils.getCurrentMonth() // 月
DateUtils.getCurrentYear(), // 年
DateUtils.getCurrentMonth() // 月
);
}

View File

@@ -1,4 +1,4 @@
package com.jeesite.modules.utils;
package com.jeesite.modules.apps.utils;
import java.math.BigDecimal;
import java.math.RoundingMode;

View File

@@ -1,7 +1,8 @@
package com.jeesite.modules.utils;
package com.jeesite.modules.apps.utils;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
@@ -14,6 +15,12 @@ public class DateUtils {
private static final DateTimeFormatter SHORT_YEAR_MONTH_CN_FORMATTER = DateTimeFormatter.ofPattern("yy年MM月");
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static String getCurrentDateTime() {
return LocalDateTime.now().format(DATETIME_FORMATTER);
}
public static String dsValue() {
LocalDate currentDate = LocalDate.now();

View File

@@ -0,0 +1,166 @@
package com.jeesite.modules.apps.utils;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import com.jeesite.modules.apps.Module.ContainerInfo;
import com.jeesite.modules.apps.Module.DockerResult;
import com.jeesite.modules.apps.Module.SystemInfo;
import com.jeesite.modules.biz.entity.MySftpAccounts;
import io.micrometer.common.util.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
public class DockerUtil {
private static final int SSH_TIMEOUT = 3000;
private static String runCommand(MySftpAccounts account, String cmd) {
JSch jsch = new JSch();
Session session = null;
ChannelExec channel = null;
InputStream in = null;
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
int port = Optional.ofNullable(account.getHostPort()).orElse(22);
session = jsch.getSession(account.getUsername(), account.getHostIp(), port);
session.setTimeout(SSH_TIMEOUT);
// 认证
if ("key".equalsIgnoreCase(account.getAuthType()) && StringUtils.isNotBlank(account.getPrivateKey())) {
jsch.addIdentity(
"tempKey",
account.getPrivateKey().getBytes(StandardCharsets.UTF_8),
null, null
);
} else {
session.setPassword(account.getPassword());
}
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
config.put("tcp.nodelay", "yes"); // 加速
session.setConfig(config);
session.connect(SSH_TIMEOUT);
// 执行命令
StringBuilder command = new StringBuilder();
if (StringUtils.isNotBlank(account.getRootPath())) {
command.append("cd ").append(account.getRootPath()).append(" && ");
}
command.append(cmd);
channel = (ChannelExec) session.openChannel("exec");
channel.setCommand(command.toString());
channel.setErrStream(System.err, true);
in = channel.getInputStream();
channel.connect();
byte[] buf = new byte[8192];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
return out.toString(StandardCharsets.UTF_8).trim();
} catch (Exception e) {
return e.getMessage();
} finally {
allClose(in, channel, session);
}
}
public static void allClose(InputStream inputStream, ChannelExec channel, Session session) {
// 关闭流
Optional.ofNullable(inputStream).ifPresent(stream -> {
try {
stream.close();
} catch (IOException e) {
System.err.println(e.getMessage());
}
});
// 断开通道
Optional.ofNullable(channel)
.filter(ChannelExec::isConnected)
.ifPresent(ChannelExec::disconnect);
// 断开会话
Optional.ofNullable(session)
.filter(Session::isConnected)
.ifPresent(Session::disconnect);
}
public static List<ContainerInfo> listContainers(MySftpAccounts accounts, boolean all) {
String fmt = "{{.ID}}|{{.Image}}|{{.Command}}|{{.CreatedAt}}|{{.Status}}|{{.Ports}}|{{.Names}}";
String cmd = (all ? "docker ps -a" : "docker ps") + " --format \"" + fmt + "\"";
String output = runCommand(accounts, cmd);
if (output == null || output.isBlank()) {
return Collections.emptyList();
}
return Arrays.stream(output.split("\\R"))
.filter(StringUtils::isNotBlank)
.map(line -> {
String[] arr = line.split("\\|", 8);
return new ContainerInfo(
arr.length > 0 ? arr[0].trim() : "",
arr.length > 1 ? arr[1].trim() : "",
arr.length > 2 ? arr[2].trim() : "",
arr.length > 3 ? arr[3].trim() : "",
arr.length > 4 ? arr[4].trim() : "",
arr.length > 5 ? arr[5].trim() : "",
arr.length > 6 ? arr[6].trim() : "",
accounts.getAccountId()
);
})
.collect(Collectors.toList());
}
public static DockerResult start(MySftpAccounts accounts, String containerId) {
String res = runCommand(accounts, "docker start " + containerId);
return DockerResult.ok(res, Objects.nonNull(res) ? "启动容器成功" : "启动容器失败");
}
public static DockerResult stop(MySftpAccounts accounts, String containerId) {
String res = runCommand(accounts, "docker stop " + containerId);
return DockerResult.ok(res, Objects.nonNull(res) ? "停止容器成功" : "停止容器失败");
}
public static DockerResult restart(MySftpAccounts accounts, String containerId) {
String res = runCommand(accounts, "docker restart " + containerId);
return DockerResult.ok(res, Objects.nonNull(res) ? "重启容器成功" : "重启容器失败");
}
public static DockerResult getLogs(MySftpAccounts accounts, String containerId, int tail, boolean timestamps) {
String cmd = "docker logs " + (timestamps ? "-t " : "") + "--tail " + tail + " " + containerId;
String res = runCommand(accounts, cmd);
return DockerResult.ok(res, Objects.nonNull(res) ? "获取容器日志成功" : "获取容器日志失败");
}
public static DockerResult listRaw(MySftpAccounts accounts, boolean all) {
String res = runCommand(accounts, all ? "docker ps -a" : "docker ps");
return DockerResult.ok(res, Objects.nonNull(res) ? "获取容器列表成功" : "获取容器列表失败");
}
public static DockerResult inspect(MySftpAccounts accounts, String containerId) {
String res = runCommand(accounts, "docker inspect " + containerId);
return DockerResult.ok(res, Objects.nonNull(res) ? "查询容器详情成功" : "查询容器详情失败");
}
public static SystemInfo systemInfo(MySftpAccounts accounts) {
SystemInfo info = new SystemInfo();
String cmd =
"top -bn1 | grep 'Cpu(s)' | awk '{printf \"%.1f\", 100-$8}'; echo ; " +
"free | grep Mem | awk '{printf \"%.1f\", $3/$2*100}'; echo ; " +
"df -h / | grep / | awk '{gsub(/%/,\"\"); print $5}'";
String output = runCommand(accounts, cmd);
if (output == null) return info;
String[] lines = output.split("\\R");
if (lines.length >= 1) info.setCpu(lines[0].trim());
if (lines.length >= 2) info.setMemory(lines[1].trim());
if (lines.length >= 3) info.setDisk(lines[2].trim());
return info;
}
}

View File

@@ -0,0 +1,42 @@
package com.jeesite.modules.apps.utils;
import java.awt.*;
import java.util.concurrent.ThreadLocalRandom;
public class KeyUtil {
/**
* 生成随机 key
*
* @param length key 长度
* @param type 1=纯数字, 2=大写字母+数字, 3=小写字母+数字, 4=混合大小写+数字
* @return 随机字符串
*/
public static String ObjKey(int length, int type) {
String str;
switch (type) {
case 1: str = "0123456789"; break;
case 2: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; break;
case 3: str = "abcdefghijklmnopqrstuvwxyz0123456789"; break;
default: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; break;
}
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() {
ThreadLocalRandom rnd = ThreadLocalRandom.current();
int r = 100 + rnd.nextInt(140);
int g = 100 + rnd.nextInt(140);
int b = 100 + rnd.nextInt(140);
Color color = new Color(r, g, b);
return "#" + Integer.toHexString(color.getRGB()).substring(2).toUpperCase();
}
}

View File

@@ -1,4 +1,4 @@
package com.jeesite.modules.utils;
package com.jeesite.modules.apps.utils;
import java.io.*;
import java.nio.charset.StandardCharsets;

View File

@@ -0,0 +1,658 @@
//package com.jeesite.modules.utils;
//
//
//
//
//import jakarta.activation.MimetypesFileTypeMap;
//
//import java.io.*;
//import java.nio.ByteBuffer;
//import java.nio.channels.FileChannel;
//import java.nio.charset.Charset;
//import java.nio.charset.StandardCharsets;
//import java.nio.file.Files;
//import java.nio.file.StandardOpenOption;
//import java.security.MessageDigest;
//import java.util.*;
//
//public class MailReceiveUtils {
//
// // 连接重试配置
// private static final int CONNECT_RETRY_TIMES = 3;
// private static final long CONNECT_RETRY_INTERVAL = 1000L;
// private static final LoggerUtils logger = LoggerUtils.getInstance();
//
// // 缓存ByteBuffer
// private static final ThreadLocal<ByteBuffer> BUFFER_LOCAL = ThreadLocal.withInitial(() -> ByteBuffer.allocate(8192));
//
// // MIME类型映射修复附件FileType识别
// private static final MimetypesFileTypeMap MIME_TYPE_MAP = new MimetypesFileTypeMap();
//
// static {
// // 扩展MIME类型映射解决常见文件类型识别错误
// MIME_TYPE_MAP.addMimeTypes("application/pdf pdf");
// MIME_TYPE_MAP.addMimeTypes("application/msword doc");
// MIME_TYPE_MAP.addMimeTypes("application/vnd.openxmlformats-officedocument.wordprocessingml.document docx");
// MIME_TYPE_MAP.addMimeTypes("application/vnd.ms-excel xls");
// MIME_TYPE_MAP.addMimeTypes("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx");
// MIME_TYPE_MAP.addMimeTypes("application/vnd.ms-powerpoint ppt");
// MIME_TYPE_MAP.addMimeTypes("application/vnd.openxmlformats-officedocument.presentationml.presentation pptx");
// MIME_TYPE_MAP.addMimeTypes("image/jpeg jpg jpeg");
// MIME_TYPE_MAP.addMimeTypes("image/png png");
// MIME_TYPE_MAP.addMimeTypes("image/gif gif");
// MIME_TYPE_MAP.addMimeTypes("application/zip zip");
// MIME_TYPE_MAP.addMimeTypes("application/x-rar-compressed rar");
// MIME_TYPE_MAP.addMimeTypes("application/x-7z-compressed 7z");
// }
//
// /**
// * 接收未读邮件优化优先解析HTML格式正文
// */
// public static List<MailReceived> receiveUnreadMails(BizMailAccount mailAccount, String saveBasePath) throws Exception {
// List<MailReceived> receivedMailList = new ArrayList<>();
// Session session = createMailSession(mailAccount);
// Store store = null;
// Folder folder = null;
//
// try {
// // 1. 建立IMAP连接带重试
// store = session.getStore("imap");
// boolean isConnected = false;
// int retryCount = 0;
// while (!isConnected) {
// try {
// store.connect(mailAccount.getHost(), mailAccount.getImapPort(),
// mailAccount.getUsername(), mailAccount.getPassword());
// isConnected = store.isConnected();
// if (isConnected) {
// logger.info("第" + (retryCount + 1) + "次连接IMAP成功" + mailAccount.getHost(), mailAccount.getAccountName());
// }
// } catch (AuthenticationFailedException e) {
// logger.error("账号/密码错误,直接返回", e);
// return receivedMailList;
// } catch (MessagingException e) {
// retryCount++;
// logger.error("第" + retryCount + "次连接失败:" + e.getMessage(), mailAccount.getAccountName());
// if (retryCount >= CONNECT_RETRY_TIMES) {
// throw new IllegalStateException("IMAP连接失败重试3次", e);
// }
// Thread.sleep(CONNECT_RETRY_INTERVAL);
// }
// }
//
// // 2. 打开收件箱
// folder = store.getFolder("INBOX");
// if (folder == null || !folder.exists()) {
// logger.error("收件箱不存在");
// return receivedMailList;
// }
// folder.open(Folder.READ_WRITE);
// logger.info("未读邮件数:" + folder.getUnreadMessageCount());
//
// // 3. 筛选未读邮件
// Message[] unreadMessages = folder.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
// if (unreadMessages == null || unreadMessages.length == 0) {
// logger.warn("无未读邮件", mailAccount.getAccountName());
// return receivedMailList;
// }
//
// // 4. 处理每封邮件
// for (Message message : unreadMessages) {
// try {
// BizMailReceived bizMail = buildBizMailReceived(message, mailAccount);
// // 记录整封邮件附件下载开始时间
// long mailAttachDownloadStart = System.currentTimeMillis();
// logger.info("开始下载邮件[" + getMessageId(message) + "]的附件,开始时间:" + new Date(mailAttachDownloadStart));
//
// List<BizMailAttachments> attachments = downloadExplicitAttachments(message, bizMail.getMessageId(), saveBasePath);
//
// // 记录整封邮件附件下载结束时间
// long mailAttachDownloadEnd = System.currentTimeMillis();
// logger.info("完成下载邮件[" + getMessageId(message) + "]的附件,结束时间:" + new Date(mailAttachDownloadEnd) +
// ",耗时:" + (mailAttachDownloadEnd - mailAttachDownloadStart) + "ms");
//
// receivedMailList.add(new MailReceived(bizMail, attachments));
// message.setFlag(Flags.Flag.SEEN, true); // 标记已读
// } catch (Exception e) {
// logger.error("处理邮件失败Message-ID" + getMessageId(message) + "" + e.getMessage(), e);
// continue;
// }
// }
//
// } finally {
// // 关闭资源
// if (folder != null && folder.isOpen()) {
// try {
// folder.close(true);
// } catch (MessagingException e) {
// logger.error("关闭文件夹失败", e);
// }
// }
// if (store != null && store.isConnected()) {
// try {
// store.close();
// } catch (MessagingException e) {
// logger.error("关闭连接失败", e);
// }
// }
// BUFFER_LOCAL.remove();
// }
//
// return receivedMailList;
// }
//
// /**
// * 创建邮件会话(优化编码+IMAP配置
// */
// private static Session createMailSession(BizMailAccount mailAccount) {
// Properties props = new Properties();
// boolean sslEnable = "true".equals(mailAccount.getSslEnable());
// props.put("mail.imap.host", mailAccount.getHost());
// props.put("mail.imap.port", mailAccount.getImapPort());
// props.put("mail.imap.ssl.enable", sslEnable);
// props.put("mail.imap.ssl.protocols", "TLSv1.2"); // 强制TLS1.2避免SSL漏洞
// props.put("mail.imap.auth", "true");
// props.put("mail.imap.connectiontimeout", "10000");
// props.put("mail.imap.timeout", "30000");
// props.put("mail.imap.partialfetch", "false"); // 禁用部分获取,避免内容截断
// props.put("mail.mime.charset", "UTF-8");
// props.put("mail.mime.base64.ignoreerrors", "true");
// props.put("mail.mime.decodefilenamehandler", "com.sun.mail.imap.protocol.IMAPUTF8DecodeHandler");
// // 关键启用UTF-8文件名解码
// System.setProperty("mail.mime.encodeparameters", "false");
//
// return Session.getInstance(props, new Authenticator() {
// @Override
// protected PasswordAuthentication getPasswordAuthentication() {
// return new PasswordAuthentication(mailAccount.getUsername(), mailAccount.getPassword());
// }
// });
// }
//
// /**
// * 构建邮件实体核心优化优先解析HTML正文
// */
// private static BizMailReceived buildBizMailReceived(Message message, BizMailAccount mailAccount) throws Exception {
// BizMailReceived received = new BizMailReceived();
// received.setCreateTime(new Date());
// received.setMessageId(getMessageId(message));
// received.setAccountId(mailAccount.getId());
// received.setUstatus("1");
// received.setMailbox("INBOX");
//
// // 发件人(修复编码)
// Address[] from = message.getFrom();
// if (from != null && from.length > 0) {
// InternetAddress fromAddr = (InternetAddress) from[0];
// received.setFromAddress(fromAddr.getAddress());
// // 优先解码发件人名称
// String personal = fromAddr.getPersonal();
// if (personal != null) {
// received.setFromName(decodeMimeText(personal));
// } else {
// received.setFromName(fromAddr.getAddress());
// }
// }
//
// // 收件人/抄送/密送(修复编码)
// received.setToAddresses(convertAddresses(message.getRecipients(Message.RecipientType.TO)));
// received.setCcAddresses(convertAddresses(message.getRecipients(Message.RecipientType.CC)));
// received.setBccAddresses(convertAddresses(message.getRecipients(Message.RecipientType.BCC)));
//
// // 主题(修复乱码)
// String subject = message.getSubject();
// received.setSubject(subject == null ? "" : decodeMimeText(subject));
//
// // 核心优化优先解析HTML格式正文
// String htmlContent = parseHtmlMailContent(message);
// // 最终空内容兜底
// received.setMailContent(htmlContent.isEmpty() ? "<p>(无正文内容)</p>" : htmlContent);
//
// // 时间
// received.setReceivedTime(new Date());
// received.setSendTime(message.getSentDate() != null ? message.getSentDate() : new Date());
//
// // 是否有显式附件
// received.setHasAttachment(hasExplicitAttachment(message) ? "1" : "0");
//
// return received;
// }
//
// /**
// * 核心优化专门解析HTML格式正文优先提取HTML无则将纯文本转为HTML
// */
// private static String parseHtmlMailContent(Message message) throws Exception {
// Object content = message.getContent();
// StringBuilder htmlSb = new StringBuilder();
//
// // 递归提取HTML内容
// extractHtmlContentRecursive(content, htmlSb);
//
// // 清理HTML内容去除无效标签和空白
// String rawHtml = htmlSb.toString().replaceAll("\\n+", "").replaceAll("\\s+", " ").trim();
// // 如果没有提取到HTML内容尝试提取纯文本并转为HTML格式
// if (rawHtml.isEmpty()) {
// String plainText = extractPlainTextContent(message);
// if (!plainText.isEmpty()) {
// // 将纯文本转为基本HTML格式换行转<br>,空格转&nbsp;
// rawHtml = "<p>" + plainText.replaceAll("\\n", "<br>").replaceAll(" ", "&nbsp;") + "</p>";
// }
// }
//
// // 标准化HTML结构
// return standardizeHtml(rawHtml);
// }
//
// /**
// * 递归提取HTML内容优先获取text/html类型
// */
// private static void extractHtmlContentRecursive(Object content, StringBuilder sb) throws Exception {
// if (content == null) {
// return;
// }
//
// // 1. 直接是HTML文本极少情况
// if (content instanceof String text) {
// if (text.contains("<html>") || text.contains("<body>") || text.contains("<p>") || text.contains("<br>")) {
// sb.append(text);
// }
// return;
// }
//
// // 2. MimeMultipart优先提取text/html部分
// if (content instanceof MimeMultipart multipart) {
// String multipartType = multipart.getContentType().split(";")[0].trim().toLowerCase();
//
// // 处理multipart/alternative多格式正文优先取text/html
// if ("multipart/alternative".equals(multipartType)) {
// for (int i = 0; i < multipart.getCount(); i++) {
// BodyPart part = multipart.getBodyPart(i);
// String partType = part.getContentType().split(";")[0].trim().toLowerCase();
// if ("text/html".equals(partType)) {
// // 读取HTML内容并追加
// sb.append(readBodyPartContent(part));
// return; // 找到HTML后直接返回不再处理其他部分
// }
// }
// }
//
// // 处理其他multipart类型递归提取所有HTML部分
// for (int i = 0; i < multipart.getCount(); i++) {
// BodyPart bodyPart = multipart.getBodyPart(i);
// String disposition = bodyPart.getDisposition();
//
// // 跳过显式附件
// if (Part.ATTACHMENT.equalsIgnoreCase(disposition)) {
// continue;
// }
//
// // 提取HTML内容
// String partType = bodyPart.getContentType().split(";")[0].trim().toLowerCase();
// if ("text/html".equals(partType)) {
// sb.append(readBodyPartContent(bodyPart));
// } else {
// // 递归解析子内容
// extractHtmlContentRecursive(bodyPart.getContent(), sb);
// }
// }
// return;
// }
//
// // 3. 单个MimeBodyPart如果是HTML类型则读取
// if (content instanceof MimeBodyPart bodyPart) {
// String disposition = bodyPart.getDisposition();
// // 跳过显式附件
// if (Part.ATTACHMENT.equalsIgnoreCase(disposition)) {
// return;
// }
//
// String partType = bodyPart.getContentType().split(";")[0].trim().toLowerCase();
// if ("text/html".equals(partType)) {
// sb.append(readBodyPartContent(bodyPart));
// }
// }
// }
//
// /**
// * 提取纯文本内容作为HTML的降级方案
// */
// private static String extractPlainTextContent(Message message) throws Exception {
// Object content = message.getContent();
// StringBuilder plainSb = new StringBuilder();
//
// extractPlainTextRecursive(content, plainSb);
//
// return plainSb.toString().replaceAll("^\\s+|\\s+$", "").replaceAll("\\n+", "\n");
// }
//
// /**
// * 递归提取纯文本内容
// */
// private static void extractPlainTextRecursive(Object content, StringBuilder sb) throws Exception {
// if (content == null) {
// return;
// }
//
// // 纯文本直接追加
// if (content instanceof String) {
// String text = ((String) content).trim();
// if (!text.isEmpty()) {
// sb.append(text).append("\n");
// }
// return;
// }
//
// // 处理MimeMultipart
// if (content instanceof MimeMultipart multipart) {
// for (int i = 0; i < multipart.getCount(); i++) {
// BodyPart bodyPart = multipart.getBodyPart(i);
// // 跳过附件
// if (Part.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition())) {
// continue;
// }
// // 提取纯文本
// extractPlainTextRecursive(bodyPart.getContent(), sb);
// }
// return;
// }
//
// // 处理单个BodyPart
// if (content instanceof MimeBodyPart bodyPart) {
// if (Part.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition())) {
// return;
// }
// // 读取纯文本内容
// String partContent = readBodyPartContent(bodyPart);
// if (!partContent.isEmpty()) {
// sb.append(partContent).append("\n");
// }
// }
// }
//
// /**
// * 标准化HTML结构补全基本标签确保格式合法
// */
// private static String standardizeHtml(String html) {
// if (html.isEmpty()) {
// return "";
// }
//
// // 补全HTML基本结构
// if (!html.startsWith("<html>")) {
// html = "<html><head><meta charset=\"UTF-8\"></head><body>" + html + "</body></html>";
// }
//
// // 修复常见的HTML格式问题
// html = html.replaceAll("<br>", "<br/>")
// .replaceAll("<hr>", "<hr/>")
// .replaceAll("&", "&amp;")
// .replaceAll("<p>\\s*</p>", "") // 移除空p标签
// .replaceAll(">\\s+<", "><"); // 移除标签间多余空格
//
// return html;
// }
//
// /**
// * 读取BodyPart内容优先原始流自动识别编码
// */
// private static String readBodyPartContent(BodyPart part) throws Exception {
// // 1. 获取ContentType和编码
// String contentType = part.getContentType() == null ? "" : part.getContentType().toLowerCase();
// Charset charset = getCharsetFromContentType(contentType);
//
// // 2. 优先读取原始输入流避免getContent()的自动转换错误)
// try (InputStream is = part.getInputStream();
// BufferedInputStream bis = new BufferedInputStream(is);
// BufferedReader reader = new BufferedReader(new InputStreamReader(bis, charset))) {
//
// StringBuilder sb = new StringBuilder();
// char[] buffer = new char[4096];
// int bytesRead;
// while ((bytesRead = reader.read(buffer)) != -1) {
// sb.append(buffer, 0, bytesRead);
// }
// // 清理无效字符
// return sb.toString();
// } catch (Exception e) {
// logger.warn("读取BodyPart流失败" + e.getMessage());
// Object fallbackContent = part.getContent();
// return fallbackContent instanceof String ? ((String) fallbackContent).trim() : "";
// }
// }
//
// /**
// * 获取邮件内容
// */
// private static Charset getCharsetFromContentType(String contentType) {
// if (contentType == null || contentType.isEmpty()) {
// return StandardCharsets.UTF_8;
// }
//
// // 提取charset参数支持多种格式
// String charsetStr = null;
// String[] parts = contentType.split(";");
// for (String part : parts) {
// part = part.trim().toLowerCase();
// if (part.startsWith("charset=")) {
// charsetStr = part.substring("charset=".length()).trim()
// .replace("\"", "").replace("'", "");
// break;
// }
// }
//
// // 验证编码有效性
// if (charsetStr != null && !charsetStr.isEmpty()) {
// try {
// return Charset.forName(charsetStr);
// } catch (Exception e) {
// logger.warn("不支持的编码:" + charsetStr + "使用默认UTF-8");
// }
// }
//
// // 兜底编码优先GBK兼容中文邮件
// try {
// return Charset.forName("GBK");
// } catch (Exception e) {
// return StandardCharsets.UTF_8;
// }
// }
//
// /**
// * 解码MIME编码的文本主题/发件人名称等)
// */
// private static String decodeMimeText(String text) {
// if (text == null || text.isEmpty()) {
// return "";
// }
// try {
// return MimeUtility.decodeText(text);
// } catch (UnsupportedEncodingException e) {
// // 多级降级处理
// try {
// return new String(text.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
// } catch (Exception e1) {
// try {
// return new String(text.getBytes(StandardCharsets.ISO_8859_1), "GBK");
// } catch (Exception e2) {
// try {
// return new String(text.getBytes(StandardCharsets.ISO_8859_1), "GB2312");
// } catch (Exception e3) {
// return text;
// }
// }
// }
// }
// }
//
// /**
// * 下载显式附件(保持原有逻辑)
// */
// private static List<BizMailAttachments> downloadExplicitAttachments(Message message, String messageId, String saveBasePath) throws Exception {
// List<BizMailAttachments> attachments = new ArrayList<>();
// if (!message.isMimeType("multipart/*")) {
// return attachments;
// }
//
// MimeMultipart multipart = (MimeMultipart) message.getContent();
// File saveDir = new File(saveBasePath);
// if (!saveDir.exists()) {
// Files.createDirectories(saveDir.toPath());
// }
//
// // 遍历所有显式附件
// for (int i = 0; i < multipart.getCount(); i++) {
// BodyPart part = multipart.getBodyPart(i);
// if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
// try {
// BizMailAttachments attach = downloadSingleAttachment(part, messageId, saveDir);
// attachments.add(attach);
// } catch (Exception e) {
// logger.error("单附件下载失败(文件名:" + part.getFileName() + "" + e.getMessage(), e);
// }
// }
// }
//
// return attachments;
// }
//
// /**
// * 下载单个附件(保持原有逻辑)
// */
// private static BizMailAttachments downloadSingleAttachment(BodyPart part, String messageId, File saveDir) throws Exception {
// BizMailAttachments attachment = new BizMailAttachments();
//
// // 1. 记录单个附件下载开始时间
// long attachDownloadStartTime = System.currentTimeMillis();
// String originalFileName = part.getFileName();
// String fileNameForLog = originalFileName == null ? "未知文件名" : decodeMimeText(originalFileName);
// logger.info("开始下载附件[" + fileNameForLog + "],开始时间:" + new Date(attachDownloadStartTime));
// // 2. 修复:文件名解码(解决乱码)
// if (originalFileName != null) {
// originalFileName = decodeMimeText(originalFileName);
// } else {
// originalFileName = "unknown_" + System.currentTimeMillis();
// }
// // 3. 修复附件FileType优先从文件扩展名识别解决ContentType错误
// String fileExt = getFileExtension(originalFileName);
// String fileType = MIME_TYPE_MAP.getContentType(originalFileName);
// // 降级处理如果MIME类型为空使用BodyPart的ContentType
// if (fileType == null || fileType.isEmpty() || fileType.equals("application/octet-stream")) {
// fileType = part.getContentType().split(";")[0].trim(); // 去除charset等参数
// }
//
// // 4. 计算MD5+保存文件
// MessageDigest md = MessageDigest.getInstance("MD5");
// ByteBuffer buffer = BUFFER_LOCAL.get();
// buffer.clear();
//
// // 临时文件
// File tempFile = new File(saveDir, UUID.randomUUID().toString());
// try (InputStream is = part.getInputStream();
// FileChannel channel = FileChannel.open(tempFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
//
// int bytesRead;
// while ((bytesRead = is.read(buffer.array())) != -1) {
// md.update(buffer.array(), 0, bytesRead);
// buffer.limit(bytesRead);
// channel.write(buffer);
// buffer.clear();
// }
// }
//
// // MD5命名避免重名
// String md5 = bytesToHex(md.digest());
// File finalFile = new File(saveDir, md5 + (fileExt.isEmpty() ? "" : "." + fileExt));
//
// // 处理文件已存在
// if (finalFile.exists()) {
// logger.warn("附件已存在MD5" + md5 + "),跳过重复保存");
// } else {
// if (!tempFile.renameTo(finalFile)) {
// logger.warn("附件重命名失败,使用临时文件名:" + tempFile.getName());
// finalFile = tempFile;
// }
// }
//
// // 5. 记录单个附件下载结束时间
// long attachDownloadEndTime = System.currentTimeMillis();
// long costTime = attachDownloadEndTime - attachDownloadStartTime;
// logger.info("完成下载附件[" + originalFileName + "],结束时间:" + new Date(attachDownloadEndTime) +
// ",耗时:" + costTime + "ms文件路径" + finalFile.getAbsolutePath());
// // 6. 封装附件信息修复FileType增加下载时间字段
// attachment.setStoragePath(finalFile.getAbsolutePath());
// attachment.setFileSize(finalFile.length());
// attachment.setCreateTime(new Date());
// attachment.setMailId(System.currentTimeMillis());
// attachment.setMessageId(messageId);
// attachment.setFileName(originalFileName);
// attachment.setFileType(fileType); // 修复后的MIME类型
// attachment.setFileExt(fileExt);
// attachment.setFileMd5(md5);
// attachment.setDownloadCount(0);
// attachment.setIsCompressed(isCompressedFile(originalFileName) ? "1" : "0");
// attachment.setIsEncrypted("0");
// attachment.setDownloadStartTime(new Date(attachDownloadStartTime)); // 附件下载开始时间
// attachment.setDownloadEndTime(new Date(attachDownloadEndTime)); // 附件下载结束时间
// attachment.setDownloadCostTime(costTime); // 附件下载耗时(毫秒)
// attachment.setSvgIcon(MyFileUtils.getIcon(fileExt));
// return attachment;
// }
//
// /**
// * 判断是否有显式附件
// */
// private static boolean hasExplicitAttachment(Message message) throws Exception {
// if (!message.isMimeType("multipart/*")) {
// return false;
// }
// MimeMultipart multipart = (MimeMultipart) message.getContent();
// for (int i = 0; i < multipart.getCount(); i++) {
// if (Part.ATTACHMENT.equalsIgnoreCase(multipart.getBodyPart(i).getDisposition())) {
// return true;
// }
// }
// return false;
// }
//
// // ------------------------ 工具方法 ------------------------
// private static String getMessageId(Message message) throws MessagingException {
// String[] ids = message.getHeader("Message-ID");
// if (ids != null && ids.length > 0) {
// return ids[0].replace("<", "").replace(">", "").trim();
// }
// return UUID.randomUUID().toString() + "@" + message.getSession().getProperty("mail.imap.host");
// }
//
// private static String convertAddresses(Address[] addresses) throws Exception {
// if (addresses == null || addresses.length == 0) {
// return "";
// }
// StringBuilder sb = new StringBuilder();
// for (Address addr : addresses) {
// sb.append(decodeMimeText(addr.toString())).append(",");
// }
// return !sb.isEmpty() ? sb.substring(0, sb.length() - 1) : "";
// }
//
// private static String getFileExtension(String fileName) {
// if (fileName == null || !fileName.contains(".")) {
// return "";
// }
// return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
// }
//
// private static boolean isCompressedFile(String fileName) {
// Set<String> exts = new HashSet<>(Arrays.asList("zip", "rar", "7z", "tar", "gz", "bz2"));
// return exts.contains(getFileExtension(fileName));
// }
//
// private static String bytesToHex(byte[] bytes) {
// char[] hex = new char[bytes.length * 2];
// for (int i = 0; i < bytes.length; i++) {
// int v = bytes[i] & 0xFF;
// hex[i * 2] = Character.forDigit(v >>> 4, 16);
// hex[i * 2 + 1] = Character.forDigit(v & 0x0F, 16);
// }
// return new String(hex).toLowerCase();
// }
//}

View File

@@ -0,0 +1,162 @@
//package com.jeesite.modules.utils;
//
//
//import com.jeesite.modules.file.entity.FileUpload;
//
//
//import java.io.File;
//import java.util.*;
//import java.util.concurrent.ExecutorService;
//import java.util.concurrent.Executors;
//
//public class MailSendUtils {
// private static final ExecutorService MAIL_EXECUTOR = Executors.newFixedThreadPool(5);
//
// private static final String FILE_PATH = "/ogsapp/files";
//
// /**
// * 同步发送HTML格式邮件
// *
// * @param mailAccount 邮件账户配置
// * @param mailSent 邮件发送内容信息
// * @return BizMailSent 填充发送结果后的对象
// */
// public static BizMailSent sendHtmlMail(BizMailAccount mailAccount, BizMailSent mailSent, List<FileUpload> fileUploads) {
// // 初始化发送结果对象
// mailSent.setId(mailSent.getId());
// mailSent.setCreateTime(new Date());
// mailSent.setSendStatus("0"); // 默认失败状态
// // 1. 参数校验
// if (!validateParams(mailAccount, mailSent)) {
// mailSent.setErrorMsg("参数校验失败:邮件账户或发送内容不完整");
// return mailSent;
// }
//
// Properties props = new Properties();
// props.put("mail.smtp.host", mailAccount.getHost());
// props.put("mail.smtp.port", mailAccount.getSmtpPort().toString());
// props.put("mail.smtp.auth", "true"); // 开启认证
// props.put("mail.smtp.socketFactory.port", mailAccount.getSmtpPort().toString());
//
// // SSL配置
// if (mailAccount.getSslEnable().equals("true")) {
// props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
// props.put("mail.smtp.starttls.enable", "true");
// }
//
// // 3. 创建认证器
// Authenticator authenticator = new Authenticator() {
// @Override
// protected PasswordAuthentication getPasswordAuthentication() {
// return new PasswordAuthentication(mailAccount.getUsername(), mailAccount.getPassword());
// }
// };
//
// // 4. 创建邮件会话
// Session session = Session.getInstance(props, authenticator);
// session.setDebug(false); // 生产环境关闭调试
//
// try {
// // 5. 构建MIME邮件消息
// MimeMessage message = new MimeMessage(session);
//
// // 设置发件人
// message.setFrom(new InternetAddress(mailAccount.getFromAddress()));
//
// // 设置收件人
// if (mailSent.getToAddresses() != null && !mailSent.getToAddresses().isEmpty()) {
// String[] toArray = mailSent.getToAddresses().split(",");
// InternetAddress[] toAddresses = new InternetAddress[toArray.length];
// for (int i = 0; i < toArray.length; i++) {
// toAddresses[i] = new InternetAddress(toArray[i].trim());
// }
// message.setRecipients(Message.RecipientType.TO, toAddresses);
// }
//
// // 设置抄送人
// if (mailSent.getCcAddresses() != null && !mailSent.getCcAddresses().isEmpty()) {
// String[] ccArray = mailSent.getCcAddresses().split(",");
// InternetAddress[] ccAddresses = new InternetAddress[ccArray.length];
// for (int i = 0; i < ccArray.length; i++) {
// ccAddresses[i] = new InternetAddress(ccArray[i].trim());
// }
// message.setRecipients(Message.RecipientType.CC, ccAddresses);
// }
// // 设置邮件主题
// message.setSubject(mailSent.getSubject(), "UTF-8");
// // 构建邮件内容支持HTML和附件
// MimeMultipart multipart = new MimeMultipart("mixed");
// // HTML内容部分
// MimeBodyPart contentPart = new MimeBodyPart();
// contentPart.setContent(mailSent.getContent(), "text/html;charset=UTF-8");
// multipart.addBodyPart(contentPart);
// if (fileUploads.size() > 0) {
// mailSent.setHasAttachment("1");
// for (FileUpload upload : fileUploads) {
// MimeBodyPart attachmentPart = new MimeBodyPart();
// File file = new File(FILE_PATH + upload.getFileUrl());
// attachmentPart.attachFile(file);
// attachmentPart.setFileName(MimeUtility.encodeText(upload.getFileName(), "UTF-8", "B"));
// multipart.addBodyPart(attachmentPart);
// }
// }
//
// // 设置邮件内容
// message.setContent(multipart);
// // 设置发送时间
// message.setSentDate(new Date());
// // 6. 发送邮件
// Transport.send(message);
// // 7. 更新发送结果
// mailSent.setSendTime(new Date());
// mailSent.setSendStatus("1");
// mailSent.setErrorMsg("");
// mailSent.setMessageId(message.getMessageID()); // 邮件服务器消息ID
// } catch (Exception e) {
// // 捕获所有异常,记录错误信息
// mailSent.setErrorMsg("邮件发送失败:" + e.getMessage());
// mailSent.setSendTime(new Date());
// // 打印异常栈(生产环境建议日志记录)
// e.printStackTrace();
// }
//
// return mailSent;
// }
//
// /**
// * 异步发送HTML格式邮件
// *
// * @param mailAccount 邮件账户配置
// * @param mailSent 邮件发送内容信息
// */
// public static void sendHtmlMailAsync(BizMailAccount mailAccount, BizMailSent mailSent, List<FileUpload> fileUploads) {
// MAIL_EXECUTOR.submit(() -> sendHtmlMail(mailAccount, mailSent, fileUploads));
// }
//
// /**
// * 参数校验
// *
// * @param mailAccount 邮件账户
// * @param mailSent 邮件内容
// * @return 校验结果
// */
// private static boolean validateParams(BizMailAccount mailAccount, BizMailSent mailSent) {
// // 校验账户必填项
// if (mailAccount == null || mailAccount.getHost() == null || mailAccount.getSmtpPort() == null
// || mailAccount.getUsername() == null || mailAccount.getPassword() == null
// || mailAccount.getFromAddress() == null) {
// return false;
// }
//
// // 校验邮件内容必填项
// return mailSent != null && mailSent.getToAddresses() != null && !mailSent.getToAddresses().isEmpty()
// && mailSent.getSubject() != null && mailSent.getContent() != null;
// }
//
// /**
// * 关闭线程池(应用关闭时调用)
// */
// public static void shutdownExecutor() {
// MAIL_EXECUTOR.shutdown();
// }
//}

View File

@@ -0,0 +1,209 @@
package com.jeesite.modules.apps.utils;
import com.jcraft.jsch.*;
import com.jeesite.modules.apps.Module.MonitorResult;
import com.jeesite.modules.biz.entity.MySftpAccounts;
import io.micrometer.common.util.StringUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* 远程主机系统监控工具类
* 通过 SSH exec 采集 CPU、内存、磁盘、负载、在线时长等指标仅支持 Linux
*
* @author gaoxq
*/
public class MonitorUtil {
private static final int SSH_TIMEOUT = 5000;
// ------------------------------------------------------------------ 主入口
/**
* 采集远程主机系统信息
*
* @param account SSH 账号(复用 MySftpAccounts 的连接信息)
* @return MonitorResult
*/
public static MonitorResult monitor(MySftpAccounts account) {
Session session = null;
ChannelExec exec = null;
try {
session = openSession(account);
exec = (ChannelExec) session.openChannel("exec");
exec.setCommand(buildScript());
InputStream in = exec.getInputStream();
InputStream errIn = exec.getErrStream();
exec.connect(SSH_TIMEOUT);
String output = readStream(in);
String err = readStream(errIn);
if (!err.isEmpty() && output.isEmpty()) {
return MonitorResult.fail("采集失败: " + err);
}
return parseResult(output);
} catch (Exception e) {
return MonitorResult.fail("采集失败: " + e.getMessage());
} finally {
if (exec != null && exec.isConnected()) exec.disconnect();
if (session != null && session.isConnected()) session.disconnect();
}
}
// ------------------------------------------------------------------ 采集脚本
private static String buildScript() {
return "echo '==MARK_HOSTNAME==' && hostname && " +
"echo '==MARK_OS==' && uname -o 2>/dev/null || echo Linux && " +
"echo '==MARK_UPTIME==' && uptime -p 2>/dev/null || uptime && " +
"echo '==MARK_CPU==' && top -bn1 | head -5 && " +
"echo '==MARK_MEM==' && free -m && " +
"echo '==MARK_DISK==' && df -h";
}
// ------------------------------------------------------------------ 解析
private static MonitorResult parseResult(String output) {
String hostname = extract(output, "MARK_HOSTNAME", true);
String os = extract(output, "MARK_OS", true);
String uptime = extract(output, "MARK_UPTIME", true);
Map<String, String> cpu = parseCpu(extract(output, "MARK_CPU", false));
Map<String, String> memory = parseMemory(extract(output, "MARK_MEM", false));
List<Map<String, String>> disk = parseDisk(extract(output, "MARK_DISK", false));
return MonitorResult.ok("采集成功", hostname, os, uptime, cpu, memory, disk);
}
// ---- CPU ----
/**
* 解析 top -bn1 输出
* Cpu(s): 1.2 us, 0.3 sy, 0.0 ni, 98.3 id, 0.1 wa, 0.1 si
*/
private static Map<String, String> parseCpu(String block) {
Map<String, String> cpu = new LinkedHashMap<>();
for (String line : block.split("\n")) {
if (line.startsWith("Cpu") || line.startsWith("%Cpu")) {
String data = line.replaceFirst("^%?Cpu\\(s?\\):\\s*", "");
for (String part : data.split(",")) {
part = part.trim();
String[] kv = part.split("\\s+", 2);
if (kv.length >= 2) {
cpu.put(kv[1].trim(), kv[0].trim());
}
}
break;
}
}
return cpu;
}
// ---- 内存 ----
/**
* 解析 free -m 输出
* Mem: total used free shared buff/cache available
*/
private static Map<String, String> parseMemory(String block) {
Map<String, String> mem = new LinkedHashMap<>();
for (String line : block.split("\n")) {
if (line.startsWith("Mem:")) {
String[] cols = line.trim().split("\\s+");
if (cols.length >= 7) {
mem.put("total", cols[1] + " MB");
mem.put("used", cols[2] + " MB");
mem.put("free", cols[3] + " MB");
mem.put("available", cols[6] + " MB");
try {
double total = Double.parseDouble(cols[1]);
double used = Double.parseDouble(cols[2]);
double pct = total > 0 ? used / total * 100 : 0;
mem.put("usagePercent", String.format("%.1f%%", pct));
} catch (NumberFormatException ignored) {
}
}
break;
}
}
return mem;
}
// ---- 磁盘 ----
/**
* 解析 df -h 输出
* Filesystem Size Used Avail Use% Mounted on
*/
private static List<Map<String, String>> parseDisk(String block) {
List<Map<String, String>> disks = new ArrayList<>();
String[] lines = block.split("\n");
for (int i = 0; i < lines.length; i++) {
String line = lines[i].trim();
if (i == 0 && (line.startsWith("Filesystem") || line.startsWith("文件系统"))) continue;
if (line.isEmpty()) continue;
String[] cols = line.split("\\s+");
if (cols.length >= 6) {
Map<String, String> d = new LinkedHashMap<>();
d.put("filesystem", cols[0]);
d.put("size", cols[1]);
d.put("used", cols[2]);
d.put("avail", cols[3]);
d.put("usePercent", cols[4]);
d.put("mounted", cols[5]);
disks.add(d);
}
}
return disks;
}
// ---- 工具方法 ----
private static String extract(String output, String mark, boolean singleLine) {
String start = "==" + mark + "==";
int s = output.indexOf(start);
if (s < 0) return "";
s += start.length();
if (s >= output.length()) return "";
if (output.charAt(s) == '\n') s++;
if (singleLine) {
int e = output.indexOf('\n', s);
return (e < 0 ? output.substring(s) : output.substring(s, e)).trim();
}
int e = output.indexOf("\n==", s);
return (e < 0 ? output.substring(s) : output.substring(s, e)).trim();
}
private static Session openSession(MySftpAccounts account) throws JSchException {
JSch jsch = new JSch();
int port = Optional.ofNullable(account.getHostPort()).orElse(22);
if ("key".equalsIgnoreCase(account.getAuthType())
&& StringUtils.isNotBlank(account.getPrivateKey())) {
jsch.addIdentity("tempKey",
account.getPrivateKey().getBytes(StandardCharsets.UTF_8),
null, null);
}
Session session = jsch.getSession(account.getUsername(), account.getHostIp(), port);
session.setTimeout(SSH_TIMEOUT);
if (!"key".equalsIgnoreCase(account.getAuthType())) {
session.setPassword(account.getPassword());
}
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
session.connect(SSH_TIMEOUT);
return session;
}
private static String readStream(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
return out.toString(StandardCharsets.UTF_8);
}
}

View File

@@ -0,0 +1,195 @@
package com.jeesite.modules.apps.utils;
import com.jeesite.modules.apps.Module.Dict.DataColumn;
import com.jeesite.modules.apps.Module.Dict.DataTable;
import com.jeesite.modules.apps.Module.Dict.TableTree;
import com.jeesite.modules.biz.entity.MyDataSource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MysqlUtils {
private static final LoggerUtils logger = LoggerUtils.getInstance();
// 需要排除的系统数据库
private static final List<String> SYSTEM_DATABASES = Arrays.asList(
"information_schema", "mysql", "performance_schema", "sys", "test"
);
// 提取字段长度的正则表达式如varchar(50) -> 50
private static final Pattern LENGTH_PATTERN = Pattern.compile("\\((\\d+)\\)");
/**
* 封装获取MySQL数据库连接
*/
public static Connection getConnection(MyDataSource dbConfig) throws Exception {
String driver = "com.mysql.cj.jdbc.Driver";
String jdbcUrl = String.format(
"jdbc:mysql://%s:%d/information_schema?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true",
dbConfig.getDbHost(), dbConfig.getDbPort()
);
Class.forName(driver); // 加载驱动
return DriverManager.getConnection(jdbcUrl, dbConfig.getUsername(), dbConfig.getPassword());
}
/**
* 获取指定MySQL连接的所有非系统库表结构信息
*
* @return 数据库名 -> 表信息列表(包含字段)的映射
* @throws Exception 连接或查询异常
*/
public static Map<String, List<DataTable>> getMysqlSchemaInfo(Connection conn) throws Exception {
Map<String, List<DataTable>> result = new HashMap<>();
// 1. 获取所有非系统数据库
List<String> databases = getNonSystemDatabases(conn);
logger.info("获取到非系统数据库数量:", databases.size());
// 2. 遍历数据库,获取表和字段信息
for (String dbName : databases) {
List<DataTable> tableInfos = getTablesByDatabase(conn, dbName);
result.put(dbName, tableInfos);
}
return result;
}
/**
* 获取所有非系统数据库
*/
public static List<String> getNonSystemDatabases(Connection conn) throws SQLException {
List<String> databases = new ArrayList<>();
String sql = "SELECT SCHEMA_NAME FROM SCHEMATA WHERE SCHEMA_NAME NOT IN ("
+ String.join(",", Collections.nCopies(SYSTEM_DATABASES.size(), "?")) + ")";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
for (int i = 0; i < SYSTEM_DATABASES.size(); i++) {
ps.setString(i + 1, SYSTEM_DATABASES.get(i));
}
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
databases.add(rs.getString("SCHEMA_NAME"));
}
}
}
return databases;
}
/**
* 获取指定数据库下的所有表信息(包含字段)
*/
private static List<DataTable> getTablesByDatabase(Connection conn, String dbName) throws SQLException {
List<DataTable> tableInfos = new ArrayList<>();
String tableSql = "SELECT " +
"TABLE_NAME, TABLE_COMMENT, CREATE_TIME, UPDATE_TIME, " +
"DATA_LENGTH, INDEX_LENGTH, TABLE_ROWS " +
"FROM TABLES WHERE TABLE_SCHEMA = ?";
try (PreparedStatement tablePs = conn.prepareStatement(tableSql)) {
tablePs.setString(1, dbName);
try (ResultSet tableRs = tablePs.executeQuery()) {
while (tableRs.next()) {
DataTable tableInfo = buildDataTableInfo(tableRs, dbName);
tableInfos.add(tableInfo);
}
}
}
return tableInfos;
}
/**
* 构建DataTableInfo实体
*/
private static DataTable buildDataTableInfo(ResultSet tableRs, String dbName) throws SQLException {
DataTable dataTable = new DataTable();
long dataLength = tableRs.getLong("DATA_LENGTH");
long indexLength = tableRs.getLong("INDEX_LENGTH");
BigDecimal tableSize = BigDecimal.valueOf((dataLength + indexLength) / 1024.0 / 1024.0)
.setScale(2, RoundingMode.HALF_UP);
dataTable.setTableRows(tableRs.getLong("TABLE_ROWS"));
dataTable.setTableName(tableRs.getString("TABLE_NAME"));
dataTable.setTableComment(tableRs.getString("TABLE_COMMENT"));
dataTable.setTableSize(tableSize);
dataTable.setDataSource(dbName);
return dataTable;
}
/**
* 获取指定表的字段信息
*/
private static List<DataColumn> getFieldsByTable(Connection conn, String dbName, String tableName) throws SQLException {
List<DataColumn> columns = new ArrayList<>();
String fieldSql = "SELECT " +
"TABLE_SCHEMA,TABLE_NAME,COLUMN_NAME,DATA_TYPE, COLUMN_TYPE, COLUMN_COMMENT, " +
"ORDINAL_POSITION, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE,COLUMN_KEY " +
"FROM COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? " +
"ORDER BY ORDINAL_POSITION";
try (PreparedStatement fieldPs = conn.prepareStatement(fieldSql)) {
fieldPs.setString(1, dbName);
fieldPs.setString(2, tableName);
try (ResultSet fieldRs = fieldPs.executeQuery()) {
while (fieldRs.next()) {
columns.add(buildDataTableField(fieldRs));
}
}
}
return columns;
}
/**
* 构建DataTableField实体
*/
private static DataColumn buildDataTableField(ResultSet fieldRs) throws SQLException {
DataColumn column = new DataColumn();
column.setColumnId(KeyUtil.ObjKey(32, 0));
column.setTableSchema(fieldRs.getString("TABLE_SCHEMA"));
column.setTableName(fieldRs.getString("TABLE_NAME"));
column.setColumnName(fieldRs.getString("COLUMN_NAME"));
column.setDataType(fieldRs.getString("DATA_TYPE"));
column.setSort(fieldRs.getInt("ORDINAL_POSITION"));
column.setIsNullable(fieldRs.getString("IS_NULLABLE"));
column.setIsPrimaryKey(fieldRs.getString("COLUMN_KEY"));
column.setColumnComment(fieldRs.getString("COLUMN_COMMENT"));
Long length = fieldRs.getLong("CHARACTER_MAXIMUM_LENGTH");
if (length == 0 || fieldRs.wasNull()) {
length = extractLengthFromType(fieldRs.getString("COLUMN_TYPE"));
}
column.setMaxLength(length);
return column;
}
/**
* 从字段类型中提取长度如int(11) -> 11
*/
private static Long extractLengthFromType(String fieldType) {
if (fieldType == null) return null;
Matcher matcher = LENGTH_PATTERN.matcher(fieldType);
if (matcher.find()) {
try {
return Long.parseLong(matcher.group(1));
} catch (NumberFormatException e) {
logger.warn("提取字段长度失败,类型", fieldType, e);
}
}
return null;
}
public static List<TableTree> getTableTrees(MyDataSource dbConfig) {
List<TableTree> tableTrees = new ArrayList<>();
try {
Connection conn = getConnection(dbConfig);
Map<String, List<DataTable>> schemaInfo = MysqlUtils.getMysqlSchemaInfo(conn);
for (Map.Entry<String, List<DataTable>> entry : schemaInfo.entrySet()) {
for (DataTable dataTable : entry.getValue()) {
dataTable.setSourceId(dbConfig.getId());
List<DataColumn> dataTableFields = getFieldsByTable(conn, entry.getKey(), dataTable.getTableName());
tableTrees.add(new TableTree(dataTable, dataTableFields));
}
}
} catch (Exception e) {
logger.error(e.getMessage());
}
return tableTrees;
}
}

View File

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

View File

@@ -0,0 +1,496 @@
package com.jeesite.modules.apps.utils;
import com.jcraft.jsch.*;
import com.jeesite.modules.apps.Module.SftpResult;
import com.jeesite.modules.biz.entity.MySftpAccounts;
import io.micrometer.common.util.StringUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* SFTP 工具类
* 支持:上传、下载、删除、创建目录、重命名
*
* @author gaoxq
*/
public class SftpUtil {
private static final int SSH_TIMEOUT = 5000;
// ------------------------------------------------------------------ 连接
/**
* 建立 SFTP Channel调用方负责关闭
*/
private static ChannelSftp openChannel(MySftpAccounts account) throws JSchException {
JSch jsch = new JSch();
int port = Optional.ofNullable(account.getHostPort()).orElse(22);
if ("key".equalsIgnoreCase(account.getAuthType())
&& StringUtils.isNotBlank(account.getPrivateKey())) {
jsch.addIdentity(
"tempKey",
account.getPrivateKey().getBytes(StandardCharsets.UTF_8),
null, null
);
}
Session session = jsch.getSession(account.getUsername(), account.getHostIp(), port);
session.setTimeout(SSH_TIMEOUT);
if (!"key".equalsIgnoreCase(account.getAuthType())) {
session.setPassword(account.getPassword());
}
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
session.connect(SSH_TIMEOUT);
ChannelSftp channel = (ChannelSftp) session.openChannel("sftp");
channel.connect(SSH_TIMEOUT);
return channel;
}
/**
* 安全关闭 channel 及其 session
*/
private static void close(ChannelSftp channel) {
if (channel == null) return;
try {
Session session = channel.getSession();
if (channel.isConnected()) channel.disconnect();
if (session != null && session.isConnected()) session.disconnect();
} catch (JSchException ignored) {
}
}
// ------------------------------------------------------------------ 上传
/**
* 上传本地文件到远程路径
*
* @param account SSH账号
* @param localPath 本地文件绝对路径
* @param remotePath 远程目标路径(含文件名),如 /data/app/test.txt
*/
public static SftpResult upload(MySftpAccounts account, String localPath, String remotePath) {
ChannelSftp channel = null;
try {
channel = openChannel(account);
channel.put(localPath, remotePath, ChannelSftp.OVERWRITE);
return SftpResult.ok("上传成功: " + remotePath);
} catch (Exception e) {
return SftpResult.fail("上传失败: " + e.getMessage());
} finally {
close(channel);
}
}
/**
* 上传字节流到远程路径
*
* @param account SSH账号
* @param input 输入流
* @param remotePath 远程目标路径(含文件名)
*/
public static SftpResult upload(MySftpAccounts account, InputStream input, String remotePath) {
ChannelSftp channel = null;
try {
channel = openChannel(account);
channel.put(input, remotePath, ChannelSftp.OVERWRITE);
return SftpResult.ok("上传成功: " + remotePath);
} catch (Exception e) {
return SftpResult.fail("上传失败: " + e.getMessage());
} finally {
close(channel);
}
}
// ------------------------------------------------------------------ 下载
/**
* 下载远程文件到本地
* <p>
* 示例:
* - download(account, "/data/logs", "*.txt", "D:/tmp/") → 下载目录下所有txt
* - download(account, "/data", "app*.zip", "D:/tmp/") → 匹配 app 开头的 zip
* - download(account, "/data", "test.txt", "D:/tmp/test.txt") → 精确下载单个文件
*
* @param account SSH账号
* @param remoteDir 远程目录,如 /data/logs
* @param fileName 文件名或通配符,如 *.txt、app*.zip、test.txt
* @param localPath 本地保存路径。以 / 或 \ 结尾则视为目录,自动用远程文件名;否则视为完整路径
*/
@SuppressWarnings("unchecked")
public static SftpResult download(MySftpAccounts account, String remoteDir, String fileName, String localPath) {
ChannelSftp channel = null;
try {
channel = openChannel(account);
Vector<ChannelSftp.LsEntry> entries = channel.ls(remoteDir);
boolean toDir = isDirectory(localPath);
String outDir = normalizeDir(localPath);
List<String> downloaded = new ArrayList<>();
int skipped = 0;
for (ChannelSftp.LsEntry entry : entries) {
if (entry.getAttrs().isDir()) continue;
if (!globMatch(fileName, entry.getFilename())) continue;
String remoteFile = normalizeDir(remoteDir) + entry.getFilename();
String target = toDir ? outDir + entry.getFilename() : localPath;
try {
channel.get(remoteFile, target);
downloaded.add(entry.getFilename());
} catch (Exception e) {
skipped++;
}
}
if (downloaded.isEmpty()) {
return SftpResult.fail("无匹配文件: " + remoteDir + "/" + fileName);
}
String msg = "下载完成,成功 " + downloaded.size() + "" + (skipped > 0 ? ",失败 " + skipped + "" : "");
return SftpResult.ok(msg, downloaded);
} catch (Exception e) {
return SftpResult.fail("下载失败: " + e.getMessage());
} finally {
close(channel);
}
}
/**
* 下载远程文件,返回字节数组(单文件,不支持通配符)
*
* @param account SSH账号
* @param remoteDir 远程目录
* @param fileName 文件名(精确)
*/
public static SftpResult download(MySftpAccounts account, String remoteDir, String fileName) {
ChannelSftp channel = null;
try {
channel = openChannel(account);
String remotePath = normalizeDir(remoteDir) + fileName;
ByteArrayOutputStream out = new ByteArrayOutputStream();
channel.get(remotePath, out);
return SftpResult.ok("下载成功", out.toByteArray());
} catch (Exception e) {
return SftpResult.fail("下载失败: " + e.getMessage());
} finally {
close(channel);
}
}
// ------------------------------------------------------------------ 删除
/**
* 删除远程文件
* 支持通配符文件名匹配
* <p>
* 示例:
* - delete(account, "/data/logs", "*.log") → 删除所有 log 文件
* - delete(account, "/data", "test.txt") → 删除单个文件
*
* @param account SSH账号
* @param remoteDir 远程目录
* @param fileName 文件名或通配符,如 *.log、test.txt
*/
@SuppressWarnings("unchecked")
public static SftpResult delete(MySftpAccounts account, String remoteDir, String fileName) {
ChannelSftp channel = null;
try {
channel = openChannel(account);
Vector<ChannelSftp.LsEntry> entries = channel.ls(remoteDir);
List<String> deleted = new ArrayList<>();
for (ChannelSftp.LsEntry entry : entries) {
if (entry.getAttrs().isDir()) continue;
if (!globMatch(fileName, entry.getFilename())) continue;
try {
channel.rm(normalizeDir(remoteDir) + entry.getFilename());
deleted.add(entry.getFilename());
} catch (SftpException ignored) {
}
}
if (deleted.isEmpty()) {
return SftpResult.fail("无匹配文件: " + remoteDir + "/" + fileName);
}
return SftpResult.ok("删除完成,共 " + deleted.size() + " 个文件", deleted);
} catch (Exception e) {
return SftpResult.fail("删除失败: " + e.getMessage());
} finally {
close(channel);
}
}
/**
* 删除远程空目录
*
* @param account SSH账号
* @param remoteDir 远程目录路径
*/
public static SftpResult deleteDir(MySftpAccounts account, String remoteDir) {
ChannelSftp channel = null;
try {
channel = openChannel(account);
channel.rmdir(remoteDir);
return SftpResult.ok("目录删除成功: " + remoteDir);
} catch (Exception e) {
return SftpResult.fail("目录删除失败: " + e.getMessage());
} finally {
close(channel);
}
}
// ------------------------------------------------------------------ 创建目录
/**
* 创建远程目录(单级)
*
* @param account SSH账号
* @param remoteDir 远程目录路径
*/
public static SftpResult mkdir(MySftpAccounts account, String remoteDir) {
ChannelSftp channel = null;
try {
channel = openChannel(account);
channel.mkdir(remoteDir);
return SftpResult.ok("目录创建成功: " + remoteDir);
} catch (Exception e) {
return SftpResult.fail("目录创建失败: " + e.getMessage());
} finally {
close(channel);
}
}
/**
* 递归创建远程目录(多级,类似 mkdir -p
*
* @param account SSH账号
* @param remoteDir 远程目录路径,如 /data/a/b/c
*/
public static SftpResult mkdirs(MySftpAccounts account, String remoteDir) {
ChannelSftp channel = null;
try {
channel = openChannel(account);
mkdirsInternal(channel, remoteDir);
return SftpResult.ok("目录创建成功: " + remoteDir);
} catch (Exception e) {
return SftpResult.fail("目录创建失败: " + e.getMessage());
} finally {
close(channel);
}
}
private static void mkdirsInternal(ChannelSftp channel, String path) throws SftpException {
String[] parts = path.replaceAll("^/", "").split("/");
StringBuilder current = new StringBuilder(path.startsWith("/") ? "/" : "");
for (String part : parts) {
if (part.isEmpty()) continue;
current.append(part);
try {
channel.stat(current.toString());
} catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
channel.mkdir(current.toString());
} else {
throw e;
}
}
current.append("/");
}
}
// ------------------------------------------------------------------ 重命名/移动
/**
* 重命名或移动远程文件/目录
*
* @param account SSH账号
* @param oldPath 原路径
* @param newPath 新路径
*/
public static SftpResult rename(MySftpAccounts account, String oldPath, String newPath) {
ChannelSftp channel = null;
try {
channel = openChannel(account);
channel.rename(oldPath, newPath);
return SftpResult.ok("重命名成功: " + oldPath + "" + newPath);
} catch (Exception e) {
return SftpResult.fail("重命名失败: " + e.getMessage());
} finally {
close(channel);
}
}
// ------------------------------------------------------------------ 列表
/**
* 列出远程目录下的文件列表含MD5
* 对非目录文件通过 SSH exec 批量执行 md5sum 获取摘要
*
* @param account SSH账号
* @param remoteDir 远程目录路径
*/
@SuppressWarnings("unchecked")
public static SftpResult list(MySftpAccounts account, String remoteDir) {
ChannelSftp channel = null;
try {
channel = openChannel(account);
Vector<ChannelSftp.LsEntry> entries = channel.ls(remoteDir);
String dir = normalizeDir(remoteDir);
// 收集非目录文件名
List<String> fileNames = new ArrayList<>();
for (ChannelSftp.LsEntry entry : entries) {
if (".".equals(entry.getFilename()) || "..".equals(entry.getFilename())) continue;
if (!entry.getAttrs().isDir()) {
fileNames.add(entry.getFilename());
}
}
// 批量获取 MD5一个 exec channel一次 md5sum 命令)
Map<String, String> md5Map = batchMd5(account, dir, fileNames);
List<Map<String, Object>> files = new ArrayList<>();
for (ChannelSftp.LsEntry entry : entries) {
if (".".equals(entry.getFilename()) || "..".equals(entry.getFilename())) continue;
Map<String, Object> item = new LinkedHashMap<>();
item.put("name", entry.getFilename());
item.put("size", entry.getAttrs().getSize());
item.put("isDir", entry.getAttrs().isDir());
item.put("permissions", entry.getAttrs().getPermissionsString());
item.put("modifyTime", new Date((long) entry.getAttrs().getMTime() * 1000));
item.put("md5", md5Map.getOrDefault(entry.getFilename(), ""));
files.add(item);
}
return SftpResult.ok("获取列表成功", files);
} catch (Exception e) {
return SftpResult.fail("获取列表失败: " + e.getMessage());
} finally {
close(channel);
}
}
/**
* 批量执行 md5sum返回 {文件名: md5}
*/
private static Map<String, String> batchMd5(MySftpAccounts account, String dir, List<String> fileNames) {
Map<String, String> result = new HashMap<>();
if (fileNames.isEmpty()) return result;
Session session = null;
ChannelExec exec = null;
try {
JSch jsch = new JSch();
int port = Optional.ofNullable(account.getHostPort()).orElse(22);
if ("key".equalsIgnoreCase(account.getAuthType())
&& StringUtils.isNotBlank(account.getPrivateKey())) {
jsch.addIdentity("tempKey",
account.getPrivateKey().getBytes(StandardCharsets.UTF_8),
null, null);
}
session = jsch.getSession(account.getUsername(), account.getHostIp(), port);
session.setTimeout(SSH_TIMEOUT);
if (!"key".equalsIgnoreCase(account.getAuthType())) {
session.setPassword(account.getPassword());
}
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
session.connect(SSH_TIMEOUT);
exec = (ChannelExec) session.openChannel("exec");
// 构建批量命令md5sum dir/file1 dir/file2 ...
StringBuilder cmd = new StringBuilder("md5sum");
for (String name : fileNames) {
cmd.append(" ").append(dir).append(name);
}
exec.setCommand(cmd.toString());
ByteArrayOutputStream errOut = new ByteArrayOutputStream();
exec.setErrStream(errOut);
InputStream in = exec.getInputStream();
exec.connect(SSH_TIMEOUT);
// 读取输出格式hash ./filepath 或 hash filepath
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
// md5sum 输出格式:<32位hex> <filepath>
String[] parts = line.split("\\s+", 2);
if (parts.length < 2) continue;
String md5 = parts[0];
String filepath = parts[1];
// 去掉可能的 ./ 前缀,取纯文件名
String name = filepath;
if (name.startsWith("./")) name = name.substring(2);
int slashIdx = name.lastIndexOf('/');
if (slashIdx >= 0) name = name.substring(slashIdx + 1);
// 处理文件名含 \0 的情况md5sum -b 二进制模式输出)
name = name.replace("\\", "");
result.put(name, md5);
}
} catch (Exception e) {
// MD5 获取失败不影响列表,静默返回空
} finally {
if (exec != null && exec.isConnected()) exec.disconnect();
if (session != null && session.isConnected()) session.disconnect();
}
return result;
}
// ------------------------------------------------------------------ 通配符 & 路径工具
/**
* 简易 glob 匹配,支持 * 和 ?
* 将 glob 转为正则:转义其他正则特殊字符,*→.* ?→.
*/
private static boolean globMatch(String pattern, String name) {
if (pattern == null || name == null) return false;
// 先统一转义,再替换通配符
StringBuilder regex = new StringBuilder("^");
for (char c : pattern.toCharArray()) {
switch (c) {
case '*':
regex.append(".*");
break;
case '?':
regex.append(".");
break;
case '.':
case '(':
case ')':
case '[':
case ']':
case '{':
case '}':
case '+':
case '^':
case '$':
case '|':
case '\\':
regex.append("\\").append(c);
break;
default:
regex.append(c);
}
}
regex.append("$");
return name.matches(regex.toString());
}
/**
* 确保路径以 / 结尾
*/
private static String normalizeDir(String dir) {
if (dir == null) return "/";
return dir.endsWith("/") ? dir : dir + "/";
}
/**
* 判断本地路径是否为目录(简单判断以分隔符结尾)
*/
private static boolean isDirectory(String path) {
return path != null && (path.endsWith("/") || path.endsWith("\\"));
}
}

View File

@@ -0,0 +1,84 @@
package com.jeesite.modules.apps.utils;
import com.jeesite.modules.apps.Module.Dict.DataColumn;
import com.jeesite.modules.apps.Module.Dict.DataTable;
import java.util.List;
public class SqlUtils {
/**
* 生成CREATE TABLE语句
*
* @return CREATE TABLE语句
*/
public static String CreateTableSql(DataTable dataTable, List<DataColumn> dataColumns) {
StringBuilder sb = new StringBuilder();
// 表定义开始
sb.append("CREATE TABLE ").append(dataTable.getTableName()).append(" (\n");
// 拼接字段定义
for (int i = 0; i < dataColumns.size(); i++) {
DataColumn dataColumn = dataColumns.get(i);
sb.append(" ").append(dataColumn.getColumnName()).append(" ");
// 处理字段类型和长度
if (dataColumn.getMaxLength() != null && dataColumn.getMaxLength() > 0) {
sb.append(dataColumn.getMaxLength()).append("(").append(dataColumn.getMaxLength()).append(") ");
} else {
sb.append(dataColumn.getMaxLength()).append(" ");
}
// 字段注释
if (dataColumn.getColumnComment() != null && !dataColumn.getColumnComment().isEmpty()) {
sb.append("COMMENT '").append(dataColumn.getColumnComment()).append("'");
}
// 最后一个字段不加逗号
if (i != dataColumns.size() - 1) {
sb.append(",");
}
sb.append("\n");
}
// 表引擎和字符集
sb.append(") ");
// 表注释
if (dataTable.getTableComment() != null && !dataTable.getTableComment().isEmpty()) {
sb.append("COMMENT\t'").append(dataTable.getTableComment()).append("'");
}
sb.append(";");
return sb.toString();
}
/**
* 生成带注释的SELECT语句
*
* @return 带注释的SELECT语句
*/
public static String SelectSqlComments(DataTable dataTable, List<DataColumn> dataColumns) {
StringBuilder sb = new StringBuilder();
// 表注释
sb.append("-- 表名:").append(dataTable.getTableName()).append("\n");
sb.append("-- 描述:").append(dataTable.getTableComment() != null ? dataTable.getTableComment() : "").append("\n");
sb.append("-- 数据来源:").append(dataTable.getDataSource() != null ? dataTable.getDataSource() : "未知").append("\n");
// SELECT语句开始
sb.append("SELECT\n");
// 拼接字段(带注释)
for (int i = 0; i < dataColumns.size(); i++) {
DataColumn dataColumn = dataColumns.get(i);
// 拼接字段名
sb.append(" ").append(dataColumn.getColumnName());
if (i != dataColumns.size() - 1) {
sb.append(",");
}
if (dataColumn.getColumnComment() != null && !dataColumn.getColumnComment().isEmpty()) {
sb.append(" -- ").append(dataColumn.getColumnComment());
}
sb.append("\n");
}
// 表名
sb.append("FROM ").append(dataTable.getTableName()).append("\n;");
return sb.toString();
}
}

View File

@@ -6,7 +6,7 @@ import cn.hutool.system.oshi.OshiUtil;
import com.jeesite.modules.apps.Module.ChartInfo;
import com.jeesite.modules.biz.entity.MyMunicipalities;
import com.jeesite.modules.biz.service.MyMunicipalitiesService;
import com.jeesite.modules.utils.KeyUtil;
import com.jeesite.modules.apps.utils.KeyUtil;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

View File

@@ -8,7 +8,7 @@ import com.jeesite.modules.sys.entity.DictData;
import com.jeesite.modules.sys.entity.User;
import com.jeesite.modules.sys.utils.DictUtils;
import com.jeesite.modules.sys.utils.UserUtils;
import com.jeesite.modules.utils.DateUtils;
import com.jeesite.modules.apps.utils.DateUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

View File

@@ -5,7 +5,7 @@ import com.jeesite.modules.apps.Module.DockerResult;
import com.jeesite.modules.apps.Module.SystemInfo;
import com.jeesite.modules.biz.dao.MySftpAccountsDao;
import com.jeesite.modules.biz.entity.MySftpAccounts;
import com.jeesite.modules.utils.DockerUtil;
import com.jeesite.modules.apps.utils.DockerUtil;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

View File

@@ -1,10 +1,8 @@
package com.jeesite.modules.apps.web.docker;
import com.jeesite.modules.apps.Module.ServerInfo;
import com.jeesite.modules.apps.Module.SystemInfo;
import com.jeesite.modules.biz.dao.MySftpAccountsDao;
import com.jeesite.modules.biz.entity.MySftpAccounts;
import com.jeesite.modules.utils.DockerUtil;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

View File

@@ -4,8 +4,8 @@ import com.jeesite.modules.apps.Module.ChartDataItem;
import com.jeesite.modules.apps.Module.ScreenTop;
import com.jeesite.modules.erp.entity.ErpTransactionFlow;
import com.jeesite.modules.erp.service.ErpTransactionFlowService;
import com.jeesite.modules.utils.BigDecimalUtils;
import com.jeesite.modules.utils.DateUtils;
import com.jeesite.modules.apps.utils.BigDecimalUtils;
import com.jeesite.modules.apps.utils.DateUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

View File

@@ -0,0 +1,31 @@
package com.jeesite.modules.apps.web.mysql;
import com.jeesite.modules.apps.Module.Dict.DataSource;
import com.jeesite.modules.apps.utils.MysqlUtils;
import com.jeesite.modules.biz.entity.MyDataSource;
import com.jeesite.modules.biz.service.MyDataSourceService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
@RequestMapping(value = "${adminPath}/mysql/myDataSource")
public class DataSourceController {
@Resource
private MyDataSourceService myDataSourceService;
@RequestMapping(value = {"listAll", ""})
public List<DataSource> listAll(MyDataSource myDataSource) throws Exception {
List<DataSource> sourceList = new ArrayList<>();
MyDataSource source = myDataSourceService.get(myDataSource);
Connection conn = MysqlUtils.getConnection(source);
List<String> dataList = MysqlUtils.getNonSystemDatabases(conn);
for (String data : dataList) {
sourceList.add(new DataSource(source.getSourceId(), source.getSourceName(), data));
}
return sourceList;
}
}

View File

@@ -0,0 +1,15 @@
package com.jeesite.modules.biz.dao;
import com.jeesite.common.dao.CrudDao;
import com.jeesite.common.mybatis.annotation.MyBatisDao;
import com.jeesite.modules.biz.entity.MyDataSource;
/**
* 数据库配置 DAO 接口
* @author gaoxq
* @version 2026-04-19
*/
@MyBatisDao(dataSourceName="work")
public interface MyDataSourceDao extends CrudDao<MyDataSource> {
}

View File

@@ -0,0 +1,106 @@
package com.jeesite.modules.biz.entity;
import java.io.Serializable;
import java.util.Date;
import com.jeesite.common.mybatis.annotation.JoinTable;
import com.jeesite.common.mybatis.annotation.JoinTable.Type;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.NotNull;
import com.jeesite.common.entity.DataEntity;
import com.jeesite.common.mybatis.annotation.Column;
import com.jeesite.common.mybatis.annotation.Table;
import com.jeesite.common.mybatis.mapper.query.QueryType;
import com.jeesite.common.utils.excel.annotation.ExcelField;
import com.jeesite.common.utils.excel.annotation.ExcelField.Align;
import com.jeesite.common.utils.excel.annotation.ExcelFields;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
/**
* 数据库配置 Entity
*
* @author gaoxq
* @version 2026-04-19
*/
@EqualsAndHashCode(callSuper = true)
@Table(name = "my_data_source", alias = "a", label = "数据库连接信息", columns = {
@Column(name = "create_time", attrName = "createTime", label = "创建时间", isUpdate = false, isUpdateForce = true),
@Column(name = "source_id", attrName = "sourceId", label = "主键ID", isPK = true),
@Column(name = "source_name", attrName = "sourceName", label = "连接名称", queryType = QueryType.LIKE),
@Column(name = "db_type", attrName = "dbType", label = "数据库类型"),
@Column(name = "db_host", attrName = "dbHost", label = "数据库IP"),
@Column(name = "db_port", attrName = "dbPort", label = "数据库端口", isQuery = false),
@Column(name = "db_name", attrName = "dbName", label = "数据库名称", queryType = QueryType.LIKE),
@Column(name = "username", attrName = "username", label = "账号", isQuery = false),
@Column(name = "password", attrName = "password", label = "密码", isQuery = false),
@Column(name = "params", attrName = "params", label = "连接参数", isQuery = false),
@Column(name = "remark", attrName = "remark", label = "备注说明", isQuery = false),
@Column(name = "ustatus", attrName = "ustatus", label = "状态"),
@Column(name = "update_time", attrName = "updateTime", label = "更新时间", isQuery = false, isUpdateForce = true),
}, orderBy = "a.create_time DESC"
)
@Data
public class MyDataSource extends DataEntity<MyDataSource> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Date createTime; // 创建时间
private String sourceId; // 主键ID
private String sourceName; // 连接名称
private String dbType; // 数据库类型
private String dbHost; // 数据库IP
private Integer dbPort; // 数据库端口
private String dbName; // 数据库名称
private String username; // 账号
private String password; // 密码
private String params; // 连接参数
private String remark; // 备注说明
private String ustatus; // 状态
private Date updateTime; // 更新时间
@ExcelFields({
@ExcelField(title = "创建时间", attrName = "createTime", align = Align.CENTER, sort = 10, dataFormat = "yyyy-MM-dd hh:mm"),
@ExcelField(title = "主键ID", attrName = "sourceId", align = Align.CENTER, sort = 20),
@ExcelField(title = "连接名称", attrName = "sourceName", align = Align.CENTER, sort = 30),
@ExcelField(title = "数据库类型", attrName = "dbType", align = Align.CENTER, sort = 40),
@ExcelField(title = "数据库IP", attrName = "dbHost", align = Align.CENTER, sort = 50),
@ExcelField(title = "数据库端口", attrName = "dbPort", align = Align.CENTER, sort = 60),
@ExcelField(title = "数据库名称", attrName = "dbName", align = Align.CENTER, sort = 70),
@ExcelField(title = "账号", attrName = "username", align = Align.CENTER, sort = 80),
@ExcelField(title = "密码", attrName = "password", align = Align.CENTER, sort = 90),
@ExcelField(title = "连接参数", attrName = "params", align = Align.CENTER, sort = 100),
@ExcelField(title = "备注说明", attrName = "remark", align = Align.CENTER, sort = 110),
@ExcelField(title = "状态", attrName = "ustatus", align = Align.CENTER, sort = 120),
@ExcelField(title = "更新时间", attrName = "updateTime", align = Align.CENTER, sort = 130, dataFormat = "yyyy-MM-dd hh:mm"),
})
public MyDataSource() {
this(null);
}
public MyDataSource(String id) {
super(id);
}
public Date getCreateTime_gte() {
return sqlMap.getWhere().getValue("create_time", QueryType.GTE);
}
public void setCreateTime_gte(Date createTime) {
sqlMap.getWhere().and("create_time", QueryType.GTE, createTime);
}
public Date getCreateTime_lte() {
return sqlMap.getWhere().getValue("create_time", QueryType.LTE);
}
public void setCreateTime_lte(Date createTime) {
sqlMap.getWhere().and("create_time", QueryType.LTE, createTime);
}
}

View File

@@ -0,0 +1,134 @@
package com.jeesite.modules.biz.service;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.jeesite.common.entity.Page;
import com.jeesite.common.service.CrudService;
import com.jeesite.modules.biz.entity.MyDataSource;
import com.jeesite.modules.biz.dao.MyDataSourceDao;
import com.jeesite.common.service.ServiceException;
import com.jeesite.common.config.Global;
import com.jeesite.common.validator.ValidatorUtils;
import com.jeesite.common.utils.excel.ExcelImport;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
/**
* 数据库配置 Service
* @author gaoxq
* @version 2026-04-19
*/
@Service
public class MyDataSourceService extends CrudService<MyDataSourceDao, MyDataSource> {
/**
* 获取单条数据
* @param myDataSource 主键
*/
@Override
public MyDataSource get(MyDataSource myDataSource) {
return super.get(myDataSource);
}
/**
* 查询分页数据
* @param myDataSource 查询条件
* @param myDataSource page 分页对象
*/
@Override
public Page<MyDataSource> findPage(MyDataSource myDataSource) {
return super.findPage(myDataSource);
}
/**
* 查询列表数据
* @param myDataSource 查询条件
*/
@Override
public List<MyDataSource> findList(MyDataSource myDataSource) {
return super.findList(myDataSource);
}
/**
* 保存数据(插入或更新)
* @param myDataSource 数据对象
*/
@Override
@Transactional
public void save(MyDataSource myDataSource) {
super.save(myDataSource);
}
/**
* 导入数据
* @param file 导入的数据文件
*/
@Transactional
public String importData(MultipartFile file) {
if (file == null){
throw new ServiceException(text("请选择导入的数据文件!"));
}
int successNum = 0; int failureNum = 0;
StringBuilder successMsg = new StringBuilder();
StringBuilder failureMsg = new StringBuilder();
try(ExcelImport ei = new ExcelImport(file, 2, 0)){
List<MyDataSource> list = ei.getDataList(MyDataSource.class);
for (MyDataSource myDataSource : list) {
try{
ValidatorUtils.validateWithException(myDataSource);
this.save(myDataSource);
successNum++;
successMsg.append("<br/>" + successNum + "、编号 " + myDataSource.getId() + " 导入成功");
} catch (Exception e) {
failureNum++;
String msg = "<br/>" + failureNum + "、编号 " + myDataSource.getId() + " 导入失败:";
if (e instanceof ConstraintViolationException){
ConstraintViolationException cve = (ConstraintViolationException)e;
for (ConstraintViolation<?> violation : cve.getConstraintViolations()) {
msg += Global.getText(violation.getMessage()) + " ("+violation.getPropertyPath()+")";
}
}else{
msg += e.getMessage();
}
failureMsg.append(msg);
logger.error(msg, e);
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
failureMsg.append(e.getMessage());
return failureMsg.toString();
}
if (failureNum > 0) {
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
throw new ServiceException(failureMsg.toString());
}else{
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
}
return successMsg.toString();
}
/**
* 更新状态
* @param myDataSource 数据对象
*/
@Override
@Transactional
public void updateStatus(MyDataSource myDataSource) {
super.updateStatus(myDataSource);
}
/**
* 删除数据
* @param myDataSource 数据对象
*/
@Override
@Transactional
public void delete(MyDataSource myDataSource) {
super.delete(myDataSource);
}
}

View File

@@ -0,0 +1,151 @@
package com.jeesite.modules.biz.web;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.jeesite.common.config.Global;
import com.jeesite.common.collect.ListUtils;
import com.jeesite.common.entity.Page;
import com.jeesite.common.lang.DateUtils;
import com.jeesite.common.utils.excel.ExcelExport;
import com.jeesite.common.utils.excel.annotation.ExcelField.Type;
import org.springframework.web.multipart.MultipartFile;
import com.jeesite.common.web.BaseController;
import com.jeesite.modules.biz.entity.MyDataSource;
import com.jeesite.modules.biz.service.MyDataSourceService;
/**
* 数据库配置 Controller
* @author gaoxq
* @version 2026-04-19
*/
@Controller
@RequestMapping(value = "${adminPath}/biz/myDataSource")
public class MyDataSourceController extends BaseController {
private final MyDataSourceService myDataSourceService;
public MyDataSourceController(MyDataSourceService myDataSourceService) {
this.myDataSourceService = myDataSourceService;
}
/**
* 获取数据
*/
@ModelAttribute
public MyDataSource get(String sourceId, boolean isNewRecord) {
return myDataSourceService.get(sourceId, isNewRecord);
}
/**
* 查询列表
*/
@RequiresPermissions("biz:myDataSource:view")
@RequestMapping(value = {"list", ""})
public String list(MyDataSource myDataSource, Model model) {
model.addAttribute("myDataSource", myDataSource);
return "modules/biz/myDataSourceList";
}
/**
* 查询列表数据
*/
@RequiresPermissions("biz:myDataSource:view")
@RequestMapping(value = "listData")
@ResponseBody
public Page<MyDataSource> listData(MyDataSource myDataSource, HttpServletRequest request, HttpServletResponse response) {
myDataSource.setPage(new Page<>(request, response));
Page<MyDataSource> page = myDataSourceService.findPage(myDataSource);
return page;
}
/**
* 查看编辑表单
*/
@RequiresPermissions("biz:myDataSource:view")
@RequestMapping(value = "form")
public String form(MyDataSource myDataSource, Model model) {
model.addAttribute("myDataSource", myDataSource);
return "modules/biz/myDataSourceForm";
}
/**
* 保存数据
*/
@RequiresPermissions("biz:myDataSource:edit")
@PostMapping(value = "save")
@ResponseBody
public String save(@Validated MyDataSource myDataSource) {
myDataSourceService.save(myDataSource);
return renderResult(Global.TRUE, text("保存数据库连接成功!"));
}
/**
* 导出数据
*/
@RequiresPermissions("biz:myDataSource:view")
@RequestMapping(value = "exportData")
public void exportData(MyDataSource myDataSource, HttpServletResponse response) {
List<MyDataSource> list = myDataSourceService.findList(myDataSource);
String fileName = "数据库连接" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
try(ExcelExport ee = new ExcelExport("数据库连接", MyDataSource.class)){
ee.setDataList(list).write(response, fileName);
}
}
/**
* 下载模板
*/
@RequiresPermissions("biz:myDataSource:view")
@RequestMapping(value = "importTemplate")
public void importTemplate(HttpServletResponse response) {
MyDataSource myDataSource = new MyDataSource();
List<MyDataSource> list = ListUtils.newArrayList(myDataSource);
String fileName = "数据库连接模板.xlsx";
try(ExcelExport ee = new ExcelExport("数据库连接", MyDataSource.class, Type.IMPORT)){
ee.setDataList(list).write(response, fileName);
}
}
/**
* 导入数据
*/
@ResponseBody
@RequiresPermissions("biz:myDataSource:edit")
@PostMapping(value = "importData")
public String importData(MultipartFile file) {
try {
String message = myDataSourceService.importData(file);
return renderResult(Global.TRUE, "posfull:"+message);
} catch (Exception ex) {
return renderResult(Global.FALSE, "posfull:"+ex.getMessage());
}
}
/**
* 删除数据
*/
@RequiresPermissions("biz:myDataSource:edit")
@RequestMapping(value = "delete")
@ResponseBody
public String delete(MyDataSource myDataSource) {
myDataSourceService.delete(myDataSource);
return renderResult(Global.TRUE, text("删除数据库连接成功!"));
}
@RequestMapping(value = "listAll")
@ResponseBody
public List<MyDataSource> listAll(MyDataSource myDataSource){
return myDataSourceService.findList(myDataSource);
}
}

View File

@@ -5,7 +5,7 @@ import java.util.List;
import com.jeesite.modules.apps.Module.PageResult;
import com.jeesite.modules.apps.Module.Table.MyProjectParams;
import com.jeesite.modules.utils.PageUtil;
import com.jeesite.modules.apps.utils.PageUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

View File

@@ -4,7 +4,7 @@ import java.util.List;
import com.jeesite.modules.apps.Module.Table.ErpFlowParams;
import com.jeesite.modules.apps.Module.PageResult;
import com.jeesite.modules.utils.PageUtil;
import com.jeesite.modules.apps.utils.PageUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

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,151 +0,0 @@
package com.jeesite.modules.utils;
import com.jcraft.jsch.*;
import com.jeesite.modules.apps.Module.ContainerInfo;
import com.jeesite.modules.apps.Module.DockerResult;
import com.jeesite.modules.apps.Module.SystemInfo;
import com.jeesite.modules.biz.entity.MySftpAccounts;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
public class DockerUtil {
private static final int SSH_TIMEOUT = 3000;
private static String runCommand(MySftpAccounts account, String cmd) {
JSch jsch = new JSch();
Session session = null;
ChannelExec channel = null;
try {
int port = account.getHostPort() == null ? 22 : account.getHostPort();
session = jsch.getSession(account.getUsername(), account.getHostIp(), port);
session.setTimeout(SSH_TIMEOUT);
// 认证
if ("key".equalsIgnoreCase(account.getAuthType()) && account.getPrivateKey() != null) {
jsch.addIdentity("temp", account.getPrivateKey().getBytes(StandardCharsets.UTF_8), null, null);
} else {
session.setPassword(account.getPassword());
}
Hashtable<String, String> config = new Hashtable<>();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
session.connect(SSH_TIMEOUT);
// 执行命令
channel = (ChannelExec) session.openChannel("exec");
String command = account.getRootPath() != null && !account.getRootPath().isEmpty()
? "cd " + account.getRootPath() + " && " + cmd
: cmd;
channel.setCommand(command);
InputStream in = channel.getInputStream();
channel.connect();
// 读取输出
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
return out.toString(StandardCharsets.UTF_8).trim();
} catch (Exception e) {
return null;
} finally {
if (channel != null) channel.disconnect();
if (session != null) session.disconnect();
}
}
public static List<ContainerInfo> listContainers(MySftpAccounts accounts, boolean all) {
List<ContainerInfo> list = new ArrayList<>();
String cmd = (all ? "docker ps -a" : "docker ps")
+ " --format \"{{.ID}}|{{.Image}}|{{.Command}}|{{.CreatedAt}}|{{.Status}}|{{.Ports}}|{{.Names}}\"";
String output = runCommand(accounts, cmd);
if (output == null || output.isBlank()) return list;
for (String line : output.split("\\R")) {
if (line.isBlank()) continue;
String[] arr = line.split("\\|");
ContainerInfo info = new ContainerInfo();
info.setContainerId(arr.length > 0 ? arr[0].trim() : "");
info.setImage(arr.length > 1 ? arr[1].trim() : "");
info.setCommand(arr.length > 2 ? arr[2].trim() : "");
info.setCreated(arr.length > 3 ? arr[3].trim() : "");
info.setStatus(arr.length > 4 ? arr[4].trim() : "");
info.setPorts(arr.length > 5 ? arr[5].trim() : "");
info.setNames(arr.length > 6 ? arr[6].trim() : "");
info.setAccountId(accounts.getAccountId());
list.add(info);
}
return list;
}
public static DockerResult start(MySftpAccounts accounts, String containerId) {
String res = runCommand(accounts, "docker start " + containerId);
return res != null ? DockerResult.ok(res) : DockerResult.fail("执行失败");
}
public static DockerResult stop(MySftpAccounts accounts, String containerId) {
String res = runCommand(accounts, "docker stop " + containerId);
return res != null ? DockerResult.ok(res) : DockerResult.fail("执行失败");
}
public static DockerResult restart(MySftpAccounts accounts, String containerId) {
String res = runCommand(accounts, "docker restart " + containerId);
return res != null ? DockerResult.ok(res) : DockerResult.fail("执行失败");
}
public static DockerResult getLogs(MySftpAccounts accounts, String containerId, int tail, boolean timestamps) {
String cmd = "docker logs " + (timestamps ? "-t " : "") + "--tail " + tail + " " + containerId;
String res = runCommand(accounts, cmd);
return res != null ? DockerResult.ok(res) : DockerResult.fail("获取日志失败");
}
public static DockerResult list(MySftpAccounts accounts, boolean all) {
String res = runCommand(accounts, all ? "docker ps -a" : "docker ps");
return res != null ? DockerResult.ok(res) : DockerResult.fail("获取列表失败");
}
public static DockerResult inspect(MySftpAccounts accounts, String containerId) {
String res = runCommand(accounts, "docker inspect " + containerId);
return res != null ? DockerResult.ok(res) : DockerResult.fail("查询详情失败");
}
// 获取 CPU 使用率
public static String getCpuUsage(MySftpAccounts accounts) {
// 1秒采样输出纯数字百分比
return runCommand(accounts,
"top -bn1 | grep 'Cpu(s)' | sed -n '1p' | awk '{printf \"%.1f\", 100 - $8}'");
}
// 获取内存使用率
public static String getMemoryUsage(MySftpAccounts accounts) {
return runCommand(accounts,
"free | grep Mem | awk '{printf \"%.1f\", $3/$2*100}'");
}
// 获取磁盘使用率
public static String getDiskUsage(MySftpAccounts accounts) {
return runCommand(accounts,
"df -h / | grep / | awk '{gsub(/%/,\"\"); print $5}'");
}
public static SystemInfo systemInfo(MySftpAccounts accounts) {
SystemInfo systemInfo = new SystemInfo();
systemInfo.setCpu(getCpuUsage(accounts));
systemInfo.setMemory(getMemoryUsage(accounts));
systemInfo.setDisk(getDiskUsage(accounts));
return systemInfo;
}
}

View File

@@ -1,57 +0,0 @@
package com.jeesite.modules.utils;
import java.awt.*;
import java.util.Random;
public class KeyUtil {
public static String ObjKey(int length, int type) {
Random random = new Random();
StringBuffer key = new StringBuffer();
if (type == 1) {
String str = "0123456789";
for (int i = 0; i < length; ++i) {
//从62个的数字或字母中选择
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();
}
}
public static String getColor() {
int r = 100 + (int) (Math.random() * 140);
int g = 100 + (int) (Math.random() * 140);
int b = 100 + (int) (Math.random() * 140);
Color color = new Color(r, g, b);
return "#" + Integer.toHexString(color.getRGB()).substring(2).toUpperCase();
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jeesite.modules.biz.dao.MyDataSourceDao">
<!-- 查询数据
<select id="findList" resultType="MyDataSource">
SELECT ${sqlMap.column.toSql()}
FROM ${sqlMap.table.toSql()}
<where>
${sqlMap.where.toSql()}
</where>
ORDER BY ${sqlMap.order.toSql()}
</select> -->
</mapper>

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2013-Now https://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author gaoxq
*/
import { defHttp } from '@jeesite/core/utils/http/axios';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
import { BasicModel, Page } from '@jeesite/core/api/model/baseModel';
import { UploadApiResult } from '@jeesite/core/api/sys/upload';
import { UploadFileParams } from '@jeesite/types/axios';
import { AxiosProgressEvent } from 'axios';
const { ctxPath, adminPath } = useGlobSetting();
export interface MyDataSource extends BasicModel<MyDataSource> {
createTime?: string; // 创建时间
sourceId?: string; // 主键ID
sourceName: string; // 连接名称
dbType: string; // 数据库类型
dbHost: string; // 数据库IP
dbPort: number; // 数据库端口
dbName: string; // 数据库名称
username: string; // 账号
password?: string; // 密码
params?: string; // 连接参数
remark?: string; // 备注说明
ustatus: string; // 状态
updateTime?: string; // 更新时间
}
export const myDataSourceList = (params?: MyDataSource | any) =>
defHttp.get<MyDataSource>({ url: adminPath + '/biz/myDataSource/list', params });
export const myDataSourceListData = (params?: MyDataSource | any) =>
defHttp.post<Page<MyDataSource>>({ url: adminPath + '/biz/myDataSource/listData', params });
export const myDataSourceForm = (params?: MyDataSource | any) =>
defHttp.get<MyDataSource>({ url: adminPath + '/biz/myDataSource/form', params });
export const myDataSourceSave = (params?: any, data?: MyDataSource | any) =>
defHttp.postJson<MyDataSource>({ url: adminPath + '/biz/myDataSource/save', params, data });
export const myDataSourceImportData = (
params: UploadFileParams,
onUploadProgress: (progressEvent: AxiosProgressEvent) => void,
) =>
defHttp.uploadFile<UploadApiResult>(
{
url: ctxPath + adminPath + '/biz/myDataSource/importData',
onUploadProgress,
},
params,
);
export const myDataSourceDelete = (params?: MyDataSource | any) =>
defHttp.get<MyDataSource>({ url: adminPath + '/biz/myDataSource/delete', params });

View File

@@ -0,0 +1,197 @@
<template>
<PageWrapper :contentFullHeight="true" :dense="true" title="false" contentClass="analysis-page-wrapper">
<div class="analysis-page">
<div class="mySpring-analysis">
<div class="analysis-layout workbench-layout">
<header class="analysis-panel biz-screen-panel biz-screen-panel--header">
<ChartTop />
</header>
<section class="biz-screen-bottom">
<div class="analysis-left biz-screen-left">
<section class="biz-screen-left-top">
<div class="analysis-panel biz-screen-panel">左上左区域</div>
<div class="analysis-panel biz-screen-panel">左上右区域</div>
</section>
<section class="biz-screen-left-middle">
<div class="analysis-panel biz-screen-panel">左下左区域</div>
<div class="analysis-panel biz-screen-panel">左下右区域</div>
</section>
</div>
<aside class="analysis-right biz-screen-right">
<section class="analysis-panel biz-screen-panel">右上区域</section>
<section class="analysis-panel biz-screen-panel">右中区域</section>
<section class="analysis-panel biz-screen-panel">右下区域</section>
</aside>
</section>
</div>
</div>
</div>
</PageWrapper>
</template>
<script lang="ts" setup name="BizMyBizScreen">
import { PageWrapper } from '@jeesite/core/components/Page';
import ChartTop from './components/ChartTop.vue'
</script>
<style lang="less" scoped>
@dark-bg: rgb(0, 0, 0);
@desktop-page-gap: 12px;
@desktop-page-padding: 0;
@desktop-card-radius: 10px;
@desktop-card-border: 1px solid rgb(226 232 240);
@desktop-card-shadow: 0 1px 3px rgb(15 23 42 / 0.06);
@desktop-dark-border: rgb(51 65 85);
.analysis-page-wrapper {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
padding: 0 !important;
width: calc(100% + 10px);
margin-left: -5px !important;
margin-right: -5px !important;
margin-bottom: 0 !important;
overflow: hidden !important;
background: transparent !important;
}
.analysis-page {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-height: 0;
padding: 0 !important;
padding-bottom: 2px !important;
overflow: hidden;
background: rgb(240, 242, 245);
}
.mySpring-analysis {
flex: 1;
min-height: 0;
padding: 0 !important;
overflow: hidden;
width: 100%;
height: 100%;
margin: 0;
border-radius: 12px;
}
.mySpring-analysis .analysis-layout {
--analysis-gap: @desktop-page-gap;
--analysis-card-radius: @desktop-card-radius;
display: flex;
width: 100%;
height: 100%;
max-height: 100%;
min-height: 0;
gap: var(--analysis-gap);
padding: @desktop-page-padding;
box-sizing: border-box;
overflow: hidden;
background: rgb(240, 242, 245);
border-radius: @desktop-card-radius;
flex-direction: column;
}
.mySpring-analysis .analysis-panel {
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
min-width: 0;
padding: 8px 12px 12px;
border-radius: var(--analysis-card-radius);
border: @desktop-card-border;
background: rgb(255, 255, 255);
box-shadow: @desktop-card-shadow;
overflow: hidden;
color: rgb(71 85 105);
font-size: 14px;
line-height: 20px;
align-items: center;
justify-content: center;
}
.biz-screen-panel--header {
flex: 0 0 10%;
padding: 8px 16px;
}
.biz-screen-bottom {
flex: 1 1 90%;
min-height: 0;
display: grid;
grid-template-columns: 60% calc(40% - 12px);
gap: var(--analysis-gap);
background: rgb(240, 242, 245);
border-radius: var(--analysis-card-radius);
}
.biz-screen-left,
.biz-screen-right {
min-width: 0;
min-height: 0;
display: grid;
gap: var(--analysis-gap);
}
.biz-screen-left {
grid-template-rows: repeat(2, minmax(0, 1fr));
}
.biz-screen-right {
grid-template-rows: repeat(3, minmax(0, 1fr));
}
.biz-screen-left-top,
.biz-screen-left-middle {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--analysis-gap);
min-width: 0;
min-height: 0;
}
html[data-theme='dark'] .mySpring-analysis .analysis-panel {
border-color: @desktop-dark-border;
background: @dark-bg !important;
color: rgb(203 213 225);
box-shadow: none;
}
html[data-theme='dark'] .mySpring-analysis,
html[data-theme='dark'] .analysis-page,
html[data-theme='dark'] .analysis-page-wrapper,
html[data-theme='dark'] .mySpring-analysis .analysis-layout,
html[data-theme='dark'] .biz-screen-bottom,
html[data-theme='dark'] .biz-screen-left,
html[data-theme='dark'] .biz-screen-right,
html[data-theme='dark'] .biz-screen-left-top,
html[data-theme='dark'] .biz-screen-left-middle {
background: @dark-bg !important;
}
@media (max-width: 1280px) {
.biz-screen-bottom {
grid-template-columns: 1fr;
}
.biz-screen-left,
.biz-screen-right {
grid-template-rows: auto;
}
}
@media (max-width: 768px) {
.biz-screen-left-top,
.biz-screen-left-middle {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,194 @@
<template>
<PageWrapper :contentFullHeight="true" :dense="true" title="false" contentClass="analysis-page-wrapper">
<div class="analysis-page">
<div class="mySpring-analysis">
<div class="analysis-layout workbench-layout">
<header class="analysis-panel database-panel database-panel--header"> </header>
<section class="database-bottom">
<div class="analysis-left database-left">
<section class="database-left-top">
<div class="analysis-panel database-panel">左上左区域</div>
<div class="analysis-panel database-panel">左上右区域</div>
</section>
<section class="database-left-middle">
<div class="analysis-panel database-panel">左下左区域</div>
<div class="analysis-panel database-panel">左下右区域</div>
</section>
</div>
<aside class="analysis-right database-right">
<section class="analysis-panel database-panel">右上区域</section>
<section class="analysis-panel database-panel">右中区域</section>
<section class="analysis-panel database-panel">右下区域</section>
</aside>
</section>
</div>
</div>
</div>
</PageWrapper>
</template>
<script lang="ts" setup name="BizMyDataBase">
import { PageWrapper } from '@jeesite/core/components/Page';
</script>
<style lang="less" scoped>
@dark-bg: rgb(0, 0, 0);
@desktop-page-gap: 12px;
@desktop-page-padding: 0;
@desktop-card-radius: 10px;
@desktop-card-border: 1px solid rgb(226 232 240);
@desktop-card-shadow: 0 1px 3px rgb(15 23 42 / 0.06);
@desktop-dark-border: rgb(51 65 85);
.analysis-page-wrapper {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
padding: 0 !important;
width: calc(100% + 10px);
margin-left: -5px !important;
margin-right: -5px !important;
margin-bottom: 0 !important;
overflow: hidden !important;
background: transparent !important;
}
.analysis-page {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-height: 0;
padding: 0 !important;
padding-bottom: 2px !important;
overflow: hidden;
background: rgb(240, 242, 245);
}
.mySpring-analysis {
flex: 1;
min-height: 0;
padding: 0 !important;
overflow: hidden;
width: 100%;
height: 100%;
margin: 0;
border-radius: 12px;
}
.mySpring-analysis .analysis-layout {
--analysis-gap: @desktop-page-gap;
--analysis-card-radius: @desktop-card-radius;
display: flex;
width: 100%;
height: 100%;
max-height: 100%;
min-height: 0;
gap: var(--analysis-gap);
padding: @desktop-page-padding;
box-sizing: border-box;
overflow: hidden;
background: rgb(240, 242, 245);
border-radius: @desktop-card-radius;
flex-direction: column;
}
.mySpring-analysis .analysis-panel {
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
min-width: 0;
padding: 8px 12px 12px;
border-radius: var(--analysis-card-radius);
border: @desktop-card-border;
background: rgb(255, 255, 255);
box-shadow: @desktop-card-shadow;
overflow: hidden;
color: rgb(71 85 105);
font-size: 14px;
line-height: 20px;
align-items: center;
justify-content: center;
}
.database-panel--header {
flex: 0 0 10%;
padding: 8px 16px;
}
.database-bottom {
flex: 1 1 90%;
min-height: 0;
display: grid;
grid-template-columns: 60% calc(40% - 12px);
gap: var(--analysis-gap);
background: rgb(240, 242, 245);
border-radius: var(--analysis-card-radius);
}
.database-left,
.database-right {
min-width: 0;
min-height: 0;
display: grid;
gap: var(--analysis-gap);
}
.database-left {
grid-template-rows: repeat(2, minmax(0, 1fr));
}
.database-right {
grid-template-rows: repeat(3, minmax(0, 1fr));
}
.database-left-top,
.database-left-middle {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--analysis-gap);
min-width: 0;
min-height: 0;
}
html[data-theme='dark'] .mySpring-analysis .analysis-panel {
border-color: @desktop-dark-border;
background: @dark-bg !important;
color: rgb(203 213 225);
box-shadow: none;
}
html[data-theme='dark'] .mySpring-analysis,
html[data-theme='dark'] .analysis-page,
html[data-theme='dark'] .analysis-page-wrapper,
html[data-theme='dark'] .mySpring-analysis .analysis-layout,
html[data-theme='dark'] .database-bottom,
html[data-theme='dark'] .database-left,
html[data-theme='dark'] .database-right,
html[data-theme='dark'] .database-left-top,
html[data-theme='dark'] .database-left-middle {
background: @dark-bg !important;
}
@media (max-width: 1280px) {
.database-bottom {
grid-template-columns: 1fr;
}
.database-left,
.database-right {
grid-template-rows: auto;
}
}
@media (max-width: 768px) {
.database-left-top,
.database-left-middle {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,153 @@
<!--
* Copyright (c) 2013-Now https://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author gaoxq
-->
<template>
<BasicDrawer
v-bind="$attrs"
:showFooter="true"
:okAuth="'biz:myDataSource:edit'"
@register="registerDrawer"
@ok="handleSubmit"
width="70%"
>
<template #title>
<Icon :icon="getTitle.icon" class="m-1 pr-1" />
<span> {{ getTitle.value }} </span>
</template>
<BasicForm @register="registerForm" />
</BasicDrawer>
</template>
<script lang="ts" setup name="ViewsBizMyDataSourceForm">
import { ref, unref, computed } from 'vue';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
import { router } from '@jeesite/core/router';
import { Icon } from '@jeesite/core/components/Icon';
import { BasicForm, FormSchema, useForm } from '@jeesite/core/components/Form';
import { BasicDrawer, useDrawerInner } from '@jeesite/core/components/Drawer';
import { MyDataSource, myDataSourceSave, myDataSourceForm } from '@jeesite/biz/api/biz/myDataSource';
const emit = defineEmits(['success', 'register']);
const { t } = useI18n('biz.myDataSource');
const { showMessage } = useMessage();
const { meta } = unref(router.currentRoute);
const record = ref<MyDataSource>({} as MyDataSource);
const getTitle = computed(() => ({
icon: meta.icon || 'i-ant-design:book-outlined',
value: record.value.isNewRecord ? t('新增数据库连接') : t('编辑数据库连接'),
}));
const inputFormSchemas: FormSchema<MyDataSource>[] = [
{
label: t('基本信息'),
field: 'basicInfo',
component: 'FormGroup',
colProps: { md: 24, lg: 24 },
},
{
label: t('连接名称'),
field: 'sourceName',
component: 'Input',
componentProps: {
maxlength: 100,
},
required: true,
},
{
label: t('数据库类型'),
field: 'dbType',
component: 'Input',
componentProps: {
maxlength: 20,
},
required: true,
},
{
label: t('数据库IP'),
field: 'dbHost',
component: 'Input',
componentProps: {
maxlength: 100,
},
required: true,
},
{
label: t('数据库端口'),
field: 'dbPort',
component: 'InputNumber',
componentProps: {
maxlength: 9,
},
required: true,
},
{
label: t('账号'),
field: 'username',
component: 'Input',
componentProps: {
maxlength: 100,
},
required: true,
},
{
label: t('密码'),
field: 'password',
component: 'Input',
componentProps: {
maxlength: 500,
},
},
{
label: t('备注说明'),
field: 'remark',
component: 'InputTextArea',
componentProps: {
maxlength: 255,
},
colProps: { md: 24, lg: 24 },
},
];
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm<MyDataSource>({
labelWidth: 120,
schemas: inputFormSchemas,
baseColProps: { md: 24, lg: 12 },
});
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
setDrawerProps({ loading: true });
await resetFields();
const res = await myDataSourceForm(data);
record.value = (res.myDataSource || {}) as MyDataSource;
record.value.__t = new Date().getTime();
await setFieldsValue(record.value);
setDrawerProps({ loading: false });
});
async function handleSubmit() {
try {
const data = await validate();
setDrawerProps({ confirmLoading: true });
const params: any = {
isNewRecord: record.value.isNewRecord,
sourceId: record.value.sourceId || data.sourceId,
};
// console.log('submit', params, data, record);
const res = await myDataSourceSave(params, data);
showMessage(res.message);
setTimeout(closeDrawer);
emit('success', data);
} catch (error: any) {
if (error && error.errorFields) {
showMessage(error.message || t('common.validateError'));
}
console.log('error', error);
} finally {
setDrawerProps({ confirmLoading: false });
}
}
</script>

View File

@@ -0,0 +1,275 @@
<!--
* Copyright (c) 2013-Now https://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author gaoxq
-->
<template>
<div>
<BasicTable @register="registerTable">
<template #tableTitle>
<Icon :icon="getTitle.icon" class="m-1 pr-1" />
<span> {{ getTitle.value }} </span>
</template>
<template #toolbar>
<a-button type="default" :loading="loading" @click="handleExport()">
<Icon icon="i-ant-design:download-outlined" /> {{ t('导出') }}
</a-button>
<a-button type="primary" @click="handleForm({})" v-auth="'biz:myDataSource:edit'">
<Icon icon="i-fluent:add-12-filled" /> {{ t('新增') }}
</a-button>
</template>
<template #firstColumn="{ record, text, value }">
<a @click="handleForm({ sourceId: record.sourceId })" :title="value">
{{ text }}
</a>
</template>
</BasicTable>
<InputForm @register="registerDrawer" @success="handleSuccess" />
</div>
</template>
<script lang="ts" setup name="ViewsBizMyDataSourceList">
import { onMounted, ref, unref } from 'vue';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
import { downloadByUrl } from '@jeesite/core/utils/file/download';
import { router } from '@jeesite/core/router';
import { Icon } from '@jeesite/core/components/Icon';
import { BasicTable, BasicColumn, useTable } from '@jeesite/core/components/Table';
import { MyDataSource, myDataSourceList } from '@jeesite/biz/api/biz/myDataSource';
import { myDataSourceDelete, myDataSourceListData } from '@jeesite/biz/api/biz/myDataSource';
import { useDrawer } from '@jeesite/core/components/Drawer';
import { useModal } from '@jeesite/core/components/Modal';
import { FormProps } from '@jeesite/core/components/Form';
import InputForm from './form.vue';
const { t } = useI18n('biz.myDataSource');
const { showMessage } = useMessage();
const { meta } = unref(router.currentRoute);
const record = ref<MyDataSource>({} as MyDataSource);
const getTitle = {
icon: meta.icon || 'i-ant-design:book-outlined',
value: meta.title || t('数据库连接管理'),
};
const loading = ref(false);
const searchForm: FormProps<MyDataSource> = {
baseColProps: { md: 8, lg: 6 },
labelWidth: 90,
schemas: [
{
label: t('记录时间起'),
field: 'createTime_gte',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD HH:mm',
showTime: { format: 'HH:mm' },
},
},
{
label: t('记录时间止'),
field: 'createTime_lte',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD HH:mm',
showTime: { format: 'HH:mm' },
},
},
{
label: t('连接名称'),
field: 'sourceName',
component: 'Input',
},
{
label: t('数据库类型'),
field: 'dbType',
component: 'Input',
},
{
label: t('数据库IP'),
field: 'dbHost',
component: 'Input',
},
{
label: t('数据库名称'),
field: 'dbName',
component: 'Input',
},
{
label: t('状态'),
field: 'ustatus',
component: 'Input',
},
],
};
const tableColumns: BasicColumn<MyDataSource>[] = [
{
title: t('记录时间'),
dataIndex: 'createTime',
key: 'a.create_time',
sorter: true,
width: 150,
align: 'center',
fixed: 'left',
},
{
title: t('连接名称'),
dataIndex: 'sourceName',
key: 'a.source_name',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('数据库类型'),
dataIndex: 'dbType',
key: 'a.db_type',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('数据库IP'),
dataIndex: 'dbHost',
key: 'a.db_host',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('数据库端口'),
dataIndex: 'dbPort',
key: 'a.db_port',
sorter: true,
width: 130,
align: 'center',
},
{
title: t('数据库名称'),
dataIndex: 'dbName',
key: 'a.db_name',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('账号'),
dataIndex: 'username',
key: 'a.username',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('密码'),
dataIndex: 'password',
key: 'a.password',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('连接参数'),
dataIndex: 'params',
key: 'a.params',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('备注说明'),
dataIndex: 'remark',
key: 'a.remark',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('状态'),
dataIndex: 'ustatus',
key: 'a.ustatus',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('更新时间'),
dataIndex: 'updateTime',
key: 'a.update_time',
sorter: true,
width: 180,
align: 'center',
},
];
const actionColumn: BasicColumn<MyDataSource> = {
width: 160,
align: 'center',
actions: (record: MyDataSource) => [
{
icon: 'i-clarity:note-edit-line',
title: t('编辑'),
onClick: handleForm.bind(this, { sourceId: record.sourceId }),
auth: 'biz:myDataSource:edit',
},
{
icon: 'i-ant-design:delete-outlined',
color: 'error',
title: t('删除'),
popConfirm: {
title: t('是否确认删除数据库连接?'),
confirm: handleDelete.bind(this, record),
},
auth: 'biz:myDataSource:edit',
},
],
};
const [registerTable, { reload, getForm }] = useTable<MyDataSource>({
api: myDataSourceListData,
beforeFetch: (params) => {
return params;
},
columns: tableColumns,
actionColumn: actionColumn,
formConfig: searchForm,
showTableSetting: true,
useSearchForm: true,
canResize: true,
});
onMounted(async () => {
const res = await myDataSourceList();
record.value = (res.myDataSource || {}) as MyDataSource;
await getForm().setFieldsValue(record.value);
});
const [registerDrawer, { openDrawer }] = useDrawer();
function handleForm(record: Recordable) {
openDrawer(true, record);
}
async function handleExport() {
loading.value = true;
const { ctxAdminPath } = useGlobSetting();
await downloadByUrl({
url: ctxAdminPath + '/biz/myDataSource/exportData',
params: getForm().getFieldsValue(),
});
loading.value = false;
}
async function handleDelete(record: Recordable) {
const params = { sourceId: record.sourceId };
const res = await myDataSourceDelete(params);
showMessage(res.message);
await handleSuccess(record);
}
async function handleSuccess(record: Recordable) {
await reload({ record });
}
</script>

View File

@@ -0,0 +1,178 @@
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { BasicColumn, BasicTableProps, FormProps } from '@jeesite/core/components/Table';
import { myDataSourceListData } from '@jeesite/biz/api/biz/myDataSource';
const { t } = useI18n('biz.myDataSource');
const modalProps = {
title: t('数据库连接选择'),
};
const searchForm: FormProps<MyDataSource> = {
baseColProps: { md: 8, lg: 6 },
labelWidth: 90,
schemas: [
{
label: t('创建时间起'),
field: 'createTime_gte',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD HH:mm',
showTime: { format: 'HH:mm' },
},
},
{
label: t('创建时间止'),
field: 'createTime_lte',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD HH:mm',
showTime: { format: 'HH:mm' },
},
},
{
label: t('连接名称'),
field: 'sourceName',
component: 'Input',
},
{
label: t('数据库类型'),
field: 'dbType',
component: 'Input',
},
{
label: t('数据库IP'),
field: 'dbHost',
component: 'Input',
},
{
label: t('数据库名称'),
field: 'dbName',
component: 'Input',
},
{
label: t('状态'),
field: 'ustatus',
component: 'Input',
},
],
};
const tableColumns: BasicColumn<MyDataSource>[] = [
{
title: t('创建时间'),
dataIndex: 'createTime',
key: 'a.create_time',
sorter: true,
width: 230,
align: 'left',
slot: 'firstColumn',
},
{
title: t('连接名称'),
dataIndex: 'sourceName',
key: 'a.source_name',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('数据库类型'),
dataIndex: 'dbType',
key: 'a.db_type',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('数据库IP'),
dataIndex: 'dbHost',
key: 'a.db_host',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('数据库端口'),
dataIndex: 'dbPort',
key: 'a.db_port',
sorter: true,
width: 130,
align: 'center',
},
{
title: t('数据库名称'),
dataIndex: 'dbName',
key: 'a.db_name',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('账号'),
dataIndex: 'username',
key: 'a.username',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('密码'),
dataIndex: 'password',
key: 'a.password',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('连接参数'),
dataIndex: 'params',
key: 'a.params',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('备注说明'),
dataIndex: 'remark',
key: 'a.remark',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('状态'),
dataIndex: 'ustatus',
key: 'a.ustatus',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('更新时间'),
dataIndex: 'updateTime',
key: 'a.update_time',
sorter: true,
width: 130,
align: 'center',
},
];
const tableProps: BasicTableProps = {
api: myDataSourceListData,
beforeFetch: (params) => {
params['isAll'] = true;
return params;
},
columns: tableColumns,
formConfig: searchForm,
rowKey: 'sourceId',
};
export default {
modalProps,
tableProps,
itemCode: 'sourceId',
itemName: 'sourceId',
isShowCode: false,
};

View File

@@ -1,196 +0,0 @@
<template>
<PageWrapper :contentFullHeight="true" :dense="true" title="false" contentClass="my-screen-page-wrapper">
<div class="my-screen-page">
<header class="my-screen-panel my-screen-panel--header">
<ChartTop />
</header>
<section class="my-screen-bottom">
<div class="my-screen-left">
<section class="my-screen-panel my-screen-left-top">左上区域</section>
<section class="my-screen-left-middle">
<div class="my-screen-panel">左中左区域</div>
<div class="my-screen-panel">左中右区域</div>
</section>
<section class="my-screen-left-bottom">
<div class="my-screen-panel">左下左区域</div>
<div class="my-screen-panel">左下右区域</div>
</section>
</div>
<aside class="my-screen-right">
<section class="my-screen-panel my-screen-right-top">右上区域</section>
<section class="my-screen-panel my-screen-right-middle">右中区域</section>
<section class="my-screen-panel my-screen-right-bottom">右下区域</section>
</aside>
</section>
</div>
</PageWrapper>
</template>
<script lang="ts" setup name="BizMyScreen">
import { PageWrapper } from '@jeesite/core/components/Page';
import ChartTop from './components/ChartTop.vue';
</script>
<style lang="less" scoped>
@dark-bg: #141414;
@desktop-page-gap: 12px;
@desktop-page-padding: 0;
@desktop-card-radius: 10px;
@desktop-card-border: 1px solid rgb(226 232 240);
@desktop-card-shadow: 0 1px 3px rgb(15 23 42 / 0.06);
@desktop-dark-border: rgb(51 65 85);
.my-screen-page-wrapper {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
padding: 0 !important;
width: calc(100% + 10px);
margin-left: -5px !important;
margin-right: -5px !important;
margin-bottom: 0 !important;
overflow: hidden !important;
background: transparent !important;
}
.my-screen-page {
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
height: 100%;
min-height: 0;
gap: @desktop-page-gap;
padding: @desktop-page-padding;
box-sizing: border-box;
overflow: hidden;
background: transparent;
border-radius: @desktop-card-radius;
}
.my-screen-panel {
min-height: 0;
padding: 8px 12px 12px;
box-sizing: border-box;
border-radius: @desktop-card-radius;
border: @desktop-card-border;
background: rgb(255, 255, 255);
box-shadow: @desktop-card-shadow;
display: flex;
align-items: center;
justify-content: center;
color: rgb(71 85 105);
font-size: 14px;
line-height: 20px;
}
.my-screen-panel--header {
flex: 0 0 10%;
padding: 8px 16px;
font-weight: 500;
color: rgb(51 65 85);
}
.my-screen-bottom {
flex: 1 1 90%;
min-height: 0;
display: flex;
gap: @desktop-page-gap;
background: transparent;
border-radius: @desktop-card-radius;
}
.my-screen-left {
flex: 0 0 70%;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: @desktop-page-gap;
}
.my-screen-right {
flex: 0 0 calc(30% - 12px);
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: @desktop-page-gap;
}
.my-screen-left-top {
flex: 0 0 10%;
}
.my-screen-left-middle,
.my-screen-left-bottom {
flex: 1 1 0;
min-height: 0;
display: flex;
gap: @desktop-page-gap;
}
.my-screen-left-middle .my-screen-panel,
.my-screen-left-bottom .my-screen-panel {
flex: 1 1 0;
}
.my-screen-right .my-screen-panel {
flex: 1 1 0;
}
.my-screen-right-top,
.my-screen-right-middle,
.my-screen-right-bottom {
flex: 1 1 0;
}
html[data-theme='dark'] .my-screen-page,
html[data-theme='dark'] .my-screen-bottom,
html[data-theme='dark'] .my-screen-left,
html[data-theme='dark'] .my-screen-right,
html[data-theme='dark'] .my-screen-left-middle,
html[data-theme='dark'] .my-screen-left-bottom {
background: @dark-bg !important;
}
html[data-theme='dark'] .my-screen-panel {
border-color: @desktop-dark-border;
background: @dark-bg !important;
color: rgb(203 213 225);
box-shadow: none;
}
html[data-theme='dark'] .my-screen-panel--header {
color: rgb(203 213 225);
}
@media (max-width: 768px) {
.my-screen-page {
flex: 1 1 auto;
height: 100%;
min-height: 0;
}
.my-screen-panel--header {
flex-basis: 72px;
}
.my-screen-bottom {
flex-direction: column;
}
.my-screen-left,
.my-screen-right {
flex: 1 1 auto;
width: 100%;
min-height: 360px;
}
.my-screen-left-middle,
.my-screen-left-bottom {
flex-direction: column;
}
}
</style>

View File

@@ -42,7 +42,7 @@
import ProjectInfo from './components/ProjectInfo.vue';
</script>
<style lang="less">
@dark-bg: #141414;
@dark-bg: rgb(0, 0, 0);
@desktop-page-gap: 12px;
@desktop-page-padding: 0;
@desktop-card-radius: 10px;

View File

@@ -41,7 +41,7 @@
</script>
<style lang="less">
@dark-bg: #141414;
@dark-bg: rgb(0, 0, 0);
@desktop-page-gap: 12px;
@desktop-page-padding: 0;
@desktop-card-radius: 10px;