Compare commits
18 Commits
86ea470208
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 76b09ed6b2 | |||
| d05108888a | |||
| f6637a255d | |||
| bc5ab27986 | |||
| 880bec1745 | |||
| a8c4c44c16 | |||
| 4604c15f0c | |||
| 9e1bb5cd70 | |||
| 1821d266af | |||
| dbef9c7a06 | |||
| d78366fed1 | |||
| 1c0432262d | |||
| 50db270ffc | |||
| b94009c94c | |||
| d26914b93c | |||
| 3567e9ed97 | |||
| 3a79513e95 | |||
| 4103f2d7ac |
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; // 排序
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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() // 月
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.jeesite.modules.utils;
|
||||
package com.jeesite.modules.apps.utils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.jeesite.modules.utils;
|
||||
package com.jeesite.modules.apps.utils;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -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>,空格转 )
|
||||
// rawHtml = "<p>" + plainText.replaceAll("\\n", "<br>").replaceAll(" ", " ") + "</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("&", "&")
|
||||
// .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();
|
||||
// }
|
||||
//}
|
||||
@@ -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();
|
||||
// }
|
||||
//}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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("\\"));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
56
web-vue/packages/biz/api/biz/myDataSource.ts
Normal file
56
web-vue/packages/biz/api/biz/myDataSource.ts
Normal 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 });
|
||||
197
web-vue/packages/biz/views/biz/myBizScreen/index.vue
Normal file
197
web-vue/packages/biz/views/biz/myBizScreen/index.vue
Normal 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>
|
||||
194
web-vue/packages/biz/views/biz/myDataBase/index.vue
Normal file
194
web-vue/packages/biz/views/biz/myDataBase/index.vue
Normal 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>
|
||||
153
web-vue/packages/biz/views/biz/myDataSource/form.vue
Normal file
153
web-vue/packages/biz/views/biz/myDataSource/form.vue
Normal 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>
|
||||
275
web-vue/packages/biz/views/biz/myDataSource/list.vue
Normal file
275
web-vue/packages/biz/views/biz/myDataSource/list.vue
Normal 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>
|
||||
178
web-vue/packages/biz/views/biz/myDataSource/select.ts
Normal file
178
web-vue/packages/biz/views/biz/myDataSource/select.ts
Normal 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,
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user