Compare commits

...

33 Commits

Author SHA1 Message Date
zyj
2f3a2ae28f 远程连接代码逻辑修改,改用websocket(联调任存在问题),新增远程机器对文件树的操作,代码工整格式化 2025-06-24 16:20:09 +08:00
唐潇凯
38ba61ed3a 机器管理规范化 2025-06-16 14:44:34 +08:00
zyj
6b8165028e 机器管理密钥解绑接口 2025-06-16 09:08:12 +08:00
zyj
8189a825d3 机器管理bug修复,代码格式化 2025-06-12 17:19:29 +08:00
唐潇凯
da63f37625 机器管理规范化 2025-06-12 09:54:53 +08:00
唐潇凯
be07dc0e0a 机器管理规范化 2025-06-09 18:05:30 +08:00
唐潇凯
97ffb19c03 机器代理管理规范化 2025-06-06 16:41:13 +08:00
唐潇凯
2d4653b29a 机器环境变量管理规范化 2025-06-06 16:05:29 +08:00
唐潇凯
66bc6a1250 机器代理管理规范化 2025-06-06 16:05:05 +08:00
唐潇凯
926c1af1e1 机器环境变量管理规范化 2025-06-06 11:37:11 +08:00
唐潇凯
f28fe4ecdf 机器环境变量管理规范化 2025-06-06 11:35:57 +08:00
唐潇凯
56150c156b Merge remote-tracking branch 'origin/jenkins-engin' into jenkins-engin 2025-06-05 11:01:56 +08:00
唐潇凯
891a332c59 机器连接修改 2025-06-05 11:00:54 +08:00
zyj
7cd4396001 机器管理代码格式化提交,修改新增环境变量逻辑 2025-06-04 17:11:00 +08:00
唐潇凯
62b0caa3da 阿里云oss 2025-06-04 14:50:33 +08:00
唐潇凯
4815b590c4 Merge remote-tracking branch 'origin/jenkins-engin' into jenkins-engin
# Conflicts:
#	modules/module-ci-machine/src/main/java/cd/casic/module/machine/controller/SecretKeyController.java
#	modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/SecretKeyService.java
#	modules/module-ci-machine/src/main/java/cd/casic/module/machine/service/impl/SecretKeyServiceImpl.java
#	modules/module-ci-machine/src/main/java/cd/casic/module/machine/utils/AliOssUtil.java
2025-06-04 14:45:15 +08:00
唐潇凯
a238538106 密钥模块修改测试完毕 2025-06-04 14:40:30 +08:00
zyj
ea35ed1f0b 机器管理代码格式化提交 2025-06-04 14:19:29 +08:00
zyj
c26a183209 Merge remote-tracking branch 'origin/jenkins-engin' into jenkins-engin 2025-06-04 11:03:55 +08:00
zyj
4b8dfe978a 机器管理调试更改 2025-06-04 11:03:35 +08:00
唐潇凯
30c201a230 sql修改 2025-06-03 14:22:43 +08:00
唐潇凯
818c2e0198 Merge remote-tracking branch 'origin/jenkins-engin' into jenkins-engin 2025-06-03 14:14:54 +08:00
唐潇凯
a7398d265c 代码测试完成 2025-06-03 14:13:39 +08:00
zyj
04afb442cf 机器管理测试更改 2025-06-03 14:03:04 +08:00
唐潇凯
b65bdd95bc get方法修改 2025-06-03 09:35:34 +08:00
唐潇凯
790193d0c9 controller返回值统一,机器相关实体类修改 2025-05-31 14:16:20 +08:00
蒲先生
1028cc0705 调整完毕 2025-05-31 09:58:28 +08:00
蒲先生
a7738d2add 变更包名 2025-05-30 17:23:48 +08:00
蒲先生
1d8a1f6eb9 清除无需要的 2025-05-30 16:57:03 +08:00
蒲先生
db95a112bc 清理 2025-05-30 16:37:31 +08:00
蒲先生
5410bd4374 机器管理迁移到ops-pro 2025-05-30 16:30:16 +08:00
唐潇凯
6642fc493b 机器管理sql 2025-05-30 15:33:09 +08:00
唐潇凯
6e317f9b8c 机器管理 2025-05-30 14:08:16 +08:00
54 changed files with 4049 additions and 5 deletions

3
.idea/compiler.xml generated
View File

@ -7,6 +7,7 @@
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="machine-management-module" />
</profile>
<profile name="Annotation profile for ops-pro" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
@ -53,6 +54,7 @@
<module name="module-ci-dispatch-api" target="17" />
<module name="module-ci-environment" target="17" />
<module name="module-ci-event" target="17" />
<module name="module-ci-execute" target="1.5" />
<module name="module-ci-log" target="17" />
<module name="module-ci-market" target="17" />
<module name="module-ci-project" target="17" />
@ -69,6 +71,7 @@
<module name="app-plugins" options="-parameters" />
<module name="commons" options="-parameters" />
<module name="framework" options="-parameters" />
<module name="machine-management-module" options="-parameters" />
<module name="module-ci-commons" options="-parameters" />
<module name="module-ci-engine" options="-parameters" />
<module name="module-ci-plugin" options="-parameters" />

1
.idea/encodings.xml generated
View File

@ -39,6 +39,7 @@
<file url="file://$PROJECT_DIR$/framework/spring-boot-starter-websocket/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/framework/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/framework/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/modules/ee/machine-management-module/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/modules/module-ci-common-pipeline/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/modules/module-ci-common-pipeline/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/modules/module-ci-common/src/main/java" charset="UTF-8" />

View File

@ -1,16 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="huaweicloud" />
<option name="name" value="huawei" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="huaweicloud" />
<option name="name" value="huawei" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyunmaven" />
<option name="name" value="aliyun" />

1
.idea/misc.xml generated
View File

@ -52,6 +52,7 @@
<option value="$PROJECT_DIR$/modules/module-ci-store-api/pom.xml" />
<option value="$PROJECT_DIR$/modules/module-ci-process-biz/pom.xml" />
<option value="$PROJECT_DIR$/modules/module-ci-process-api/pom.xml" />
<option value="$PROJECT_DIR$/modules/ee/machine-management-module/pom.xml" />
</list>
</option>
<option name="ignoredFiles">

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cd.casic.boot</groupId>
<artifactId>modules</artifactId>
<version>${revision}</version>
</parent>
<artifactId>module-ci-machine</artifactId>
<packaging>jar</packaging>
<version>${revision}</version>
<name>${project.artifactId}</name>
<dependencies>
<dependency>
<groupId>cd.casic.boot</groupId>
<artifactId>commons</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 机器连接-->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- SpringDoc OpenAPI 依赖 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<!--文件上传-->
<dependency>
<groupId>cd.casic.boot</groupId>
<artifactId>module-infra-biz</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

View File

@ -0,0 +1,85 @@
package cd.casic.module.machine.Interceptor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception;
import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*;
@Component
@Slf4j
public class WebSocketHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(
@NotNull ServerHttpRequest request,
@NotNull ServerHttpResponse response,
@NotNull WebSocketHandler wsHandler,
@NotNull Map<String, Object> attributes
) {
log.info("WebSocket握手请求: {}", request.getURI());
// 从URL参数中获取id
String id = extractIdFromUrl(request.getURI());
System.out.println("-----------------------------------------");
if (id != null) {
attributes.put("machineId", id); // 将id存入attributes
log.info("握手验证成功 - ID: {}", id);
return true;
} else {
response.setStatusCode(HttpStatus.BAD_REQUEST);
log.warn("握手验证失败 - 缺少 id URL 参数");
return false;
}
}
@Override
public void afterHandshake(
@NotNull ServerHttpRequest request,
@NotNull ServerHttpResponse response,
@NotNull WebSocketHandler wsHandler,
Exception exception
) {
if (exception == null) {
log.info("WebSocket握手完成 - URI: {}",
request.getURI());
} else {
log.error("WebSocket握手异常 - URI: {}, 异常信息: {}",
request.getURI(),
exception.getMessage(),
exception);
}
}
// 从URI中提取id参数的辅助方法
private String extractIdFromUrl(URI uri) {
try {
String query = uri.getQuery();
if (query != null) {
String[] params = query.split("&");
for (String param : params) {
String[] keyValue = param.split("=");
if (keyValue.length == 2 && "id".equalsIgnoreCase(keyValue[0])) { // 修改为匹配id
return URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8);
}
}
}
} catch (Exception e) {
log.error("解析URL参数失败", e);
throw exception(FAILED_TO_PARSE_URL_PARAMETERS);
}
return null;
}
}

View File

@ -0,0 +1,214 @@
package cd.casic.module.machine.component;
import cd.casic.module.machine.dal.model.FileNode;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception;
import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*;
@Slf4j
@Component
public class FileTreeComponent {
private ChannelSftp sftp;
//todo缓存过期还未设置
private final Map<String, FileNode> directoryCache = new ConcurrentHashMap<>();
private static final long CACHE_EXPIRE_TIME = 60 * 1000; // 缓存过期时间1分钟
private final Map<String, Long> cacheTimeStamp = new ConcurrentHashMap<>();
// 将文件树转换为JSON友好的Map结构仅一级目录
public static Map<String, Object> convertToMap(FileNode node) {
Map<String, Object> map = new HashMap<>();
map.put("name", node.getName());
map.put("isDirectory", node.isDirectory());
map.put("size", node.getSize());
map.put("permissions", node.getPermissions());
map.put("modifiedTime", node.getModifiedTime());
// 仅添加直接子项不递归
if (node.isDirectory() && !node.getChildren().isEmpty()) {
List<Map<String, Object>> children = node.getChildren().stream()
.map(FileTreeComponent::convertToMap)
.collect(Collectors.toList());
map.put("children", children);
}
return map;
}
// 获取指定路径的直接子项不递归
public List<ChannelSftp.LsEntry> listDirectChildren(String path) {
if (sftp == null || !isSftpConnected()) {
log.error("SFTP连接无效无法列出文件");
throw exception(CONNECTION_LOST);
}
List<ChannelSftp.LsEntry> entries = Collections.synchronizedList(new ArrayList<>());
//定义ChannelSftp下LsEntrySelector接口中select方法
ChannelSftp.LsEntrySelector selector = entry -> {
entries.add(entry);
return ChannelSftp.LsEntrySelector.CONTINUE;
};
try {
sftp.ls(path, selector);
} catch (SftpException e) {
log.error("读取远程文件目录结构失败 [错误码: {}, 信息: {}]",
e.id, e.getMessage(), e);
throw exception(READ_REMOTE_DIRECTORY_FAIL);
}
return new ArrayList<>(entries);
}
// 创建文件节点支持使用完整路径或文件名
private static FileNode createFileNode(String name, SftpATTRS attrs, boolean useFullPath) {
String displayName = useFullPath ? name : name.substring(name.lastIndexOf('/') + 1);
return new FileNode(
displayName,
attrs.isDir(),
attrs.getSize(),
attrs.getPermissionsString(),
attrs.getMTime() * 1000L // 转换为毫秒
);
}
// 获取远程文件树的方法仅展示下一级
public Map<String, Object> getRemoteFileTree(Session session, String path) {
path = normalizePath(path);
Channel channel = null;
try {
// 缓存有效性检查
String cacheKey = "ls:" + path;
long currentTime = System.currentTimeMillis();
// 检查缓存是否存在且未过期
if (directoryCache.containsKey(cacheKey) &&
currentTime - cacheTimeStamp.getOrDefault(cacheKey, 0L) < CACHE_EXPIRE_TIME) {
log.debug("从缓存获取目录内容: {}", path);
// 构建缓存中的根节点
FileNode root = directoryCache.get(cacheKey);
return sortFileInfo(convertToMap(root));
}
// 打开SFTP通道
channel = session.openChannel("sftp");
channel.connect();
sftp = (ChannelSftp) channel;
// 先通过stat获取当前路径信息
SftpATTRS rootAttrs = sftp.stat(path);
FileNode root = createFileNode(path, rootAttrs, true);
// 仅获取直接子项不递归
List<ChannelSftp.LsEntry> entries = listDirectChildren(path);
//循环添加子节点
for (ChannelSftp.LsEntry entry : entries) {
String fileName = entry.getFilename();
if (fileName.equals(".") || fileName.equals("..")) continue;
SftpATTRS attrs = entry.getAttrs();
FileNode childNode = createFileNode(fileName, attrs, false);
root.addChild(childNode);
}
// 更新缓
directoryCache.put(cacheKey, root);
cacheTimeStamp.put(cacheKey, currentTime);
return sortFileInfo(convertToMap(root));
} catch (JSchException e) {
log.error("SFTP通道创建或连接失败: {}", e.getMessage(), e);
if (e.getMessage().contains("open")) {
throw exception(CREATE_CHANEL_ERROR);
} else {
throw exception(CHANEL_CONNECT_FAIL);
}
} catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
// 仅捕获路径不存在的错误
log.error("路径不存在: {}", path);
throw exception(PATH_NOT_EXISTS); // 自定义异常
} else if (e.id == ChannelSftp.SSH_FX_PERMISSION_DENIED) {
// 处理权限问题非路径不存在
log.error("无路径访问权限: {}", path);
throw exception(NO_PATH_PERMISSION);
} else {
log.error("获取目录内容失败: {}", e.getMessage(), e);
throw exception(READ_REMOTE_DIRECTORY_FAIL);
}
} finally {
// 确保资源释放
if (sftp != null) {
try {
sftp.disconnect();
} catch (Exception ex) {
log.warn("关闭SFTP连接失败", ex);
}
}
if (channel != null && channel.isConnected()) {
channel.disconnect();
}
}
}
// 检查SFTP连接状态
private boolean isSftpConnected() {
if (sftp == null) return false;
try {
sftp.pwd();
return true;
} catch (Exception e) {
log.warn("SFTP连接状态检查失败", e);
return false;
}
}
// 规范化路径确保末尾有斜杠根路径除外
private String normalizePath(String path) {
// 移除多余的斜杠多个连续的斜杠会被替换为一个
path = path.replaceAll("/+", "/");
// 如果路径不为根路径且末尾没有斜杠则添加斜杠
if (!"/".equals(path) && !path.endsWith("/")) {
path += "/";
}
return path;
}
/**
* 对文件和目录信息进行排序仅处理直接子级
*
* @param fileInfoMap 文件/目录信息映射
* @return 排序后的映射
*/
public Map<String, Object> sortFileInfo(Map<String, Object> fileInfoMap) {
// 检查是否包含子节点
if (fileInfoMap.containsKey("children")) {
List<Map<String, Object>> children = (List<Map<String, Object>>) fileInfoMap.get("children");
// 对子节点列表进行排序
if (children != null && !children.isEmpty()) {
children.sort((a, b) -> {
// 获取isDirectory属性并比较
boolean isADirectory = (boolean) a.get("isDirectory");
boolean isBDirectory = (boolean) b.get("isDirectory");
// 目录排在文件前面
return Boolean.compare(isBDirectory, isADirectory);
});
// 更新排序后的子节点列表
fileInfoMap.put("children", children);
}
}
return fileInfoMap;
}
}

View File

@ -0,0 +1,230 @@
package cd.casic.module.machine.component;
import cd.casic.framework.commons.exception.ServiceException;
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
import cd.casic.module.machine.enums.AuthenticationType;
import cd.casic.module.machine.enums.ConnectionStatus;
import cd.casic.module.machine.enums.SSHChanelType;
import cd.casic.module.machine.service.SecretKeyService;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception;
import static cd.casic.module.machine.contants.MachineErrorCodeConstants.SECRET_KEY_NULL;
import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*;
@Slf4j
@Data
@Component
public class WebSocketConnection {
private SecretKeyService secretKeyService;
private MachineInfoDO machineInfo;
private ConnectionStatus connectionStatus = ConnectionStatus.DISCONNECTED;
private Session sshSession;
public WebSocketConnection(SecretKeyService secretKeyService) {
this.secretKeyService = secretKeyService;
}
private static final int CONNECTION_TIMEOUT = 5000; // 连接超时时间(毫秒)
public void initConnection(MachineInfoDO machineInfo) {
try {
this.machineInfo = machineInfo;
this.sshSession = doConnect(machineInfo);
log.info("已成功建立 SSH 连接至 {} ", machineInfo.getHostIp());
this.connectionStatus = ConnectionStatus.CONNECTING;
} catch (ServiceException e) {
log.warn("SSH 连接失败: {}", e.getMessage());
throw e;
}
}
public void disconnect() {
if (sshSession != null && sshSession.isConnected()) {
try {
sshSession.disconnect();
log.info("SSH连接关闭: {}", machineInfo.getHostIp());
} catch (Exception e) {
log.error("关闭SSH连接失败: {}", e.getMessage());
throw exception(CLOSE_CLOSE_SESSION_ERROR);
}
}
connectionStatus = ConnectionStatus.DISCONNECTED;
}
/**
* 执行远程命令支持超时和中断处理
*/
public void executeCommand(WebSocketSession webSocketSession, String command) {
// 1. 检查连接状态
if (sshSession == null || !sshSession.isConnected()) {
sendErrorMessage(webSocketSession, "SSH连接未建立或已断开");
return;
}
try {
// 2. 创建SSH命令执行通道
Channel channel;
try {
channel = sshSession.openChannel(SSHChanelType.EXEC.getMessage());
} catch (JSchException e) {
throw exception(CREATE_CHANEL_ERROR);
}
((ChannelExec) channel).setCommand(command);
// 3. 设置输入/输出流
channel.setInputStream(null);
((ChannelExec) channel).setErrStream(System.err);
// 4. 获取命令输出流
InputStream inputStream = channel.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader reader = new BufferedReader(inputStreamReader);
// 5. 连接并执行命令
channel.connect();
// 6. 读取命令输出并实时发送给客户端
String line;
while ((line = reader.readLine()) != null) {
// 实时发送输出到客户端
webSocketSession.sendMessage(new TextMessage(line));
}
// 7. 等待命令执行完成
int exitStatus = channel.getExitStatus();
// 8. 发送命令执行完毕的消息
webSocketSession.sendMessage(new TextMessage(
"[系统] 命令执行完毕,退出状态: " + exitStatus
));
// 9. 关闭通道
channel.disconnect();
} catch (JSchException | IOException e) {
throw exception(EXECUTE_COMMAND_FAIL);
}
}
// 发送错误消息的辅助方法
public void sendErrorMessage(WebSocketSession webSocketSession, String message) {
try {
if (webSocketSession.isOpen()) {
webSocketSession.sendMessage(new TextMessage("[错误] " + message));
}
} catch (IOException e) {
log.error("发送错误消息失败", e);
throw exception(WEBSOCKET_SEND_MESSAGE_ERROR);
}
}
/**
* 实际执行连接逻辑
*/
private Session doConnect(MachineInfoDO machineInfo) {
JSch jsch = new JSch();
// 配置认证方式
configureAuthentication(jsch, machineInfo);
Session session;
// 创建SSH会话
try {
session = jsch.getSession(
machineInfo.getUsername(),
machineInfo.getHostIp(),
machineInfo.getSshPort() != null ? machineInfo.getSshPort() : 22
);
} catch (JSchException e) {
throw exception(CREATE_SESSION_ERROR);
}
// 配置连接参数
configureSession(session, machineInfo);
// 建立连接
try {
session.connect(CONNECTION_TIMEOUT);
} catch (JSchException e) {
throw exception(SESSION_CONNECT_ERROR);
}
return session;
}
/**
* 配置认证方式密码或密钥
*/
private void configureAuthentication(JSch jsch, MachineInfoDO machineInfo) {
if (machineInfo.getAuthenticationType() == AuthenticationType.SECRET_KEY.getCode()) {
// 密钥认证
if (machineInfo.getSecretKeyId() == null) {
throw exception(SECRET_KEY_NULL);
}
String pubKeyContent = secretKeyService.getKeyContent(machineInfo.getSecretKeyId());
// 验证秘钥格式
if (!pubKeyContent.startsWith("-----BEGIN")) {
log.error("无效的密钥格式{}", pubKeyContent);
throw exception(INVALID_kEY_FORMAT);
}
try {
// 尝试加载秘钥私钥
jsch.addIdentity(
machineInfo.getName(),
pubKeyContent.getBytes(StandardCharsets.UTF_8),
null,
null
);
log.info("密钥加载成功 {}", machineInfo.getHostIp());
} catch (JSchException e) {
log.error("密钥加载失败: {}", e.getMessage());
throw exception(READ_SECRET_CONTENT_ERROR);
}
} else if (machineInfo.getAuthenticationType() == AuthenticationType.PASSWORD.getCode()) {
// 密码认证
if (!StringUtils.hasText(machineInfo.getPassword())) {
throw exception(PASSWORD_NOT_EXISTS);
}
} else {
log.error("不支持该验证类型:{}", machineInfo.getAuthenticationType());
throw exception(NOT_SUPPORT_AUTHENTICATION_TYPE);
}
}
/**
* 配置SSH会话参数安全增强
*/
private void configureSession(Session session, MachineInfoDO machineInfo) {
Properties config = new Properties();
// 根据认证类型配置不同的认证策略
if (machineInfo.getAuthenticationType() == 1) { // 密码认证
// 设置密码
session.setPassword(machineInfo.getPassword());
config.put("StrictHostKeyChecking", "no");
// 仅使用密码认证禁用其他认证方式
config.put("PreferredAuthentications", "password");
// 禁用公钥相关配置避免干扰
config.put("PubkeyAuthentication", "no");
} else { // 密钥认证
try {
String preKeyPath = secretKeyService.getSecretKey(machineInfo.getSecretKeyId()).getPath();
JSch jsch = new JSch();
jsch.addIdentity(preKeyPath); // 添加私钥
// 保持默认认证顺序公钥优先
config.put("PreferredAuthentications", "publicKey,password,keyboard-interactive");
} catch (JSchException e) {
log.error("SSH密钥配置失败", e);
throw exception(SSH_KEY_CONFIGURATION_FAIL);
}
}
config.put("ServerAliveInterval", "30"); // 每30秒发送一次心跳
config.put("ServerAliveCountMax", "3"); // 允许3次心跳失败
session.setConfig(config);
}
}

View File

@ -0,0 +1,114 @@
package cd.casic.module.machine.component;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
@Component("machineWebSocketSessionManger")
//管理webSocketSession
public class WebSocketSessionManager {
//webSocketSessionId - WebSocketSession 保存 WebSocketSession 对象与会话 ID 的映射
private static final ConcurrentHashMap<String, WebSocketSession> WebSocketSessionMap = new ConcurrentHashMap<>();
//webSocketSessionId - WebSocketConnection 与远程机器的会话管理
private static final ConcurrentHashMap<String, WebSocketConnection> sessionConnectionMap = new ConcurrentHashMap<>();
//机器id - WebSocketConnection
private static final ConcurrentHashMap<Long, WebSocketConnection> webSocketSessionConnectionMap = new ConcurrentHashMap<>();
public static void addWebSocketSession(String sessionId, WebSocketSession session) {
WebSocketSessionMap.put(sessionId, session);
}
/**
* 获取 WebSocketSession
*/
public static WebSocketSession getWebSocketSession(String sessionId) {
return WebSocketSessionMap.get(sessionId);
}
/**
* 移除 WebSocketSession
*/
public static void removeWebSocketSession(String sessionId) {
WebSocketSessionMap.remove(sessionId);
}
/**
* 检查 sessionId 是否存在
*/
public static boolean containsWebSocketSession(String sessionId) {
return WebSocketSessionMap.containsKey(sessionId);
}
/**
* 获取所有 WebSocketSession
*/
public static Collection<WebSocketSession> getAllWebSocketSessions() {
return WebSocketSessionMap.values();
}
public static void addWebSocketConnection(String sessionId, WebSocketConnection connection) {
sessionConnectionMap.put(sessionId, connection);
}
/**
* 获取 WebSocketConnection
*/
public static WebSocketConnection getWebSocketConnection(String sessionId) {
return sessionConnectionMap.get(sessionId);
}
/**
* 移除 WebSocketConnection
*/
public static void removeWebSocketConnection(String sessionId) {
sessionConnectionMap.remove(sessionId);
}
/**
* 检查 sessionId 是否存在
*/
public static boolean containsWebSocketConnection(String sessionId) {
return sessionConnectionMap.containsKey(sessionId);
}
/**
* 获取所有 WebSocketConnection
*/
public static ConcurrentHashMap<Long, WebSocketConnection> getAllWebSocketConnections() {
return webSocketSessionConnectionMap;
}
/**
* 添加 WebSocketConnection
*/
public static void addWebSocketConnectionByMachineId(Long machineId, WebSocketConnection connection) {
webSocketSessionConnectionMap.put(machineId, connection);
}
/**
* 获取 WebSocketConnection
*/
public static WebSocketConnection getWebSocketConnectionByMachineId(Long machineId) {
return webSocketSessionConnectionMap.get(machineId);
}
/**
* 移除 WebSocketConnection
*/
public static void removeWebSocketConnectionByMachineId(Long machineId) {
webSocketSessionConnectionMap.remove(machineId);
}
/**
* 检查 machineId 是否存在
*/
public static boolean containsMachineId(Long machineId) {
return webSocketSessionConnectionMap.containsKey(machineId);
}
}

View File

@ -0,0 +1,33 @@
package cd.casic.module.machine.configuration;
import cd.casic.module.infra.framework.file.core.client.s3.S3FileClientConfig;
import cd.casic.module.machine.utils.AliYunOssClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AliYunOssConfig extends S3FileClientConfig{
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.accessKeyId}")
private String accessKey;
@Value("${aliyun.oss.accessKeySecret}")
private String accessSecret;
@Value("${aliyun.oss.bucketName}")
private String bucket;
// 定义 S3 客户端 Bean
@Bean
public AliYunOssClient aliYunClient() {
// 创建配置对象
S3FileClientConfig config = new AliYunOssConfig();
config.setEndpoint(endpoint);
config.setAccessKey(accessKey);
config.setAccessSecret(accessSecret);
config.setBucket(bucket);
AliYunOssClient aliYunOssClient = new AliYunOssClient(1L, config);
// 创建并返回客户端实例
aliYunOssClient.init();
return aliYunOssClient;
}
}

View File

@ -0,0 +1,40 @@
package cd.casic.module.machine.configuration;
import cd.casic.module.machine.Interceptor.WebSocketHandshakeInterceptor;
import cd.casic.module.machine.handler.MachineWebSocketHandler;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
@Configuration
@EnableWebSocket
//WebSocket端点配置
public class WebSocketConfig implements WebSocketConfigurer {
@Resource
private MachineWebSocketHandler machineWebSocketHandler;
@Resource
private WebSocketHandshakeInterceptor webSocketHandshakeInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(machineWebSocketHandler, "/ssh/terminal")
.addInterceptors(webSocketHandshakeInterceptor)
.setAllowedOrigins("*"); // 允许跨域生产环境需限制
}
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
return new ServletServerContainerFactoryBean();
}
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

View File

@ -0,0 +1,73 @@
package cd.casic.module.machine.contants;
import cd.casic.framework.commons.exception.ErrorCode;
/**
* 机器报错
*/
public interface MachineErrorCodeConstants {
// ========== 机器基础信息模块 1-003-000-000 ==========
ErrorCode MACHINE_INFO_NULL = new ErrorCode(1_003_000_000, "机器信息为空");
ErrorCode MACHINE_INFO_HOST_IP_NULL = new ErrorCode(1_003_000_001, "机器主机IP为空");
ErrorCode MACHINE_INFO_USER_NAME_NULL = new ErrorCode(1_003_000_002, "机器用户名为空");
ErrorCode MACHINE_INFO_TYPE_NULL = new ErrorCode(1_003_000_003, "机器类型为空");
ErrorCode MACHINE_INFO_TYPE_NOT_EXISTS = new ErrorCode(1_003_000_004, "机器类型不存在");
ErrorCode MACHINE_INFO_TAG_NULL = new ErrorCode(1_003_000_005, "机器唯一标识为空");
ErrorCode MACHINE_INFO_TAG_EXISTS = new ErrorCode(1_003_000_006, "机器唯一标识已存在");
ErrorCode MACHINE_INFO_AUTHENTICATION_TYPE_NULL = new ErrorCode(1_003_000_007, "机器认证类型为空");
ErrorCode MACHINE_INFO_AUTHENTICATION_TYPE_NOT_EXISTS = new ErrorCode(1_003_000_008, "机器认证类型不存在");
ErrorCode MACHINE_ENABLE = new ErrorCode(1_003_000_009, "机器启用中");
ErrorCode MACHINE_UN_ENABLE = new ErrorCode(1_003_000_010, "机器不可用");
// ========== 文件操作模块 1-003-001-000 ==========
ErrorCode UPLOADING_FILE_FAIL = new ErrorCode(1_003_001_000, "上传文件失败");
ErrorCode DOWNLOAD_FILE_FAIL = new ErrorCode(1_003_001_001, "下载失败");
ErrorCode FILENAME_NULL = new ErrorCode(1_003_001_002, "文件名为空");
ErrorCode READ_FILE_FAIL = new ErrorCode(1_003_001_003, "读取文件失败");
ErrorCode DELETE_FILE_FAIL = new ErrorCode(1_003_001_004, "删除文件失败");
// ========== 机器环境变量模块 1-003-002-000 ==========
ErrorCode MACHINE_ENV_NULL = new ErrorCode(1_003_002_000, "机器环境变量为空");
ErrorCode MACHINE_ENV_NOT_EXISTS = new ErrorCode(1_003_002_001, "机器不存在");
ErrorCode MACHINE_ENV_KEY_ILLEGAL = new ErrorCode(1_003_002_002, "机器环境变量键不合法");
// ========== 机器代理模块 1-003-003-000 ==========
ErrorCode MACHINE_PROXY_HOST_IP_NULL = new ErrorCode(1_003_003_000, "机器代理主机地址为空");
ErrorCode MACHINE_PROXY_USER_NAME_NULL = new ErrorCode(1_003_003_001, "机器代理用户名为空");
ErrorCode MACHINE_PROXY_NOT_EXISTS = new ErrorCode(1_003_003_002, "机器代理不存在");
ErrorCode MACHINE_PROXY_TYPE_NOT_EXISTS = new ErrorCode(1_003_003_003, "机器代理类型不存在");
ErrorCode MACHINE_PROXY_IS_ONLINE = new ErrorCode(1_003_003_004, "机器代理在线,不能删除");
// ========== 密钥模块 1-003-004-000 ==========
ErrorCode SECRET_KEY_NULL = new ErrorCode(1_003_004_000, "密钥为空");
ErrorCode SECRET_KEY_NOT_EXISTS = new ErrorCode(1_003_004_001, "密钥不存在");
ErrorCode SECRET_KEY_PATH_NULL = new ErrorCode(1_003_004_002, "密钥路径为空");
ErrorCode INVALID_kEY_FORMAT = new ErrorCode(1_003_004_003, "无效的密钥格式");
ErrorCode READ_SECRET_CONTENT_ERROR = new ErrorCode(1_003_004_004, "读取密钥加载失败");
// ========== 其他模块 1-003-005-000 ==========
ErrorCode OSS_PARAM_NULL = new ErrorCode(1_003_005_000, "oss参数无法读取");
ErrorCode FILE_UPLOAD_ERROR = new ErrorCode(1_003_005_001, "文件上传失败");
ErrorCode FILE_DOWNLOAD_ERROR = new ErrorCode(1_003_005_002, "文件下载失败");
//========== 会话连接模块 1-003-006-000 ==========
ErrorCode SESSION_CONNECT_ERROR = new ErrorCode(1_003_006_001, "会话连接失败");
ErrorCode CLOSE_CLOSE_SESSION_ERROR = new ErrorCode(1_003_006_002, "关闭连接失败");
ErrorCode SESSION_NOT_CONNECT = new ErrorCode(1_003_006_003, "会话未连接");
ErrorCode EXECUTE_COMMAND_FAIL = new ErrorCode(1_003_006_004, "命令执行失败");
ErrorCode PASSWORD_NOT_EXISTS = new ErrorCode(1_003_006_005, "密码不存在");
ErrorCode SSH_KEY_CONFIGURATION_FAIL = new ErrorCode(1_003_006_006, "SSH密钥配置失败");
ErrorCode NOT_SUPPORT_AUTHENTICATION_TYPE = new ErrorCode(1_003_006_007, "认证类型不支持");
ErrorCode CREATE_SESSION_ERROR = new ErrorCode(1_003_006_008, "创建会话失败");
ErrorCode WEBSOCKET_SEND_MESSAGE_ERROR = new ErrorCode(1_003_006_009, "websocket发送消息失败");
ErrorCode CREATE_CHANEL_ERROR = new ErrorCode(1_003_006_010, "执行通道创建失败");
ErrorCode CHANEL_CONNECT_FAIL = new ErrorCode(1_003_006_011, "通道连接失败");
ErrorCode FAILED_TO_PARSE_URL_PARAMETERS = new ErrorCode(1_003_006_012, "解析URL参数失败");
//========== 远程文件树模块 1-003-007-000 ==========
ErrorCode NOT_DIRECTORY_NODE = new ErrorCode(1_003_007_001, "非目录节点不能添加子节点");
ErrorCode READ_REMOTE_DIRECTORY_FAIL = new ErrorCode(1_003_007_002, "读取远程文件目录结构失败");
ErrorCode CONNECTION_LOST = new ErrorCode(1_003_007_003, "SFTP连接无效无法列出文件");
ErrorCode PATH_NOT_EXISTS = new ErrorCode(1_003_007_004, "路径不存在");
ErrorCode NO_PATH_PERMISSION = new ErrorCode(1_003_007_005, "无路径访问权限");
}

View File

@ -0,0 +1,84 @@
package cd.casic.module.machine.controller;
import cd.casic.framework.commons.pojo.CommonResult;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.framework.commons.util.object.BeanUtils;
import cd.casic.module.machine.dal.dataobject.MachineEnvDO;
import cd.casic.module.machine.service.MachineEnvService;
import cd.casic.module.machine.controller.vo.MachineEnvVO;
import cn.hutool.core.collection.CollUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import static cd.casic.framework.commons.pojo.CommonResult.success;
/**
* 环境变量控制器
*/
@Tag(name = "环境变量管理")
@RestController
@RequestMapping("/ci/machineEnv")
@Validated
public class MachineEnvController {
@Resource
private MachineEnvService machineEnvService;
@PostMapping("/create")
@Operation(summary = "新增环境变量")
@PreAuthorize("@ss.hasPermission('ci:machineEnv:create')")
public CommonResult<Long> createEnv(@Valid @RequestBody MachineEnvVO machineEnvVO) {
Long id = machineEnvService.createEnv(machineEnvVO);
return success(id);
}
@PutMapping("/update")
@Operation(summary = "修改环境变量")
@PreAuthorize("@ss.hasPermission('ci:machineEnv:update')")
public CommonResult<Boolean> updateEnv(@Valid @RequestBody MachineEnvVO machineEnvVO) {
machineEnvService.updateEnv(machineEnvVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除机器的环境变量")
@PreAuthorize("@ss.hasPermission('ci:machineEnv:delete')")
public CommonResult<Boolean> deleteEnv(@RequestParam("id") Long id) {
machineEnvService.deleteEnv(id);
return success(true);
}
@DeleteMapping("/deleteList")
@Operation(summary = "批量删除机器环境变量")
@PreAuthorize("@ss.hasPermission('ci:machineEnv:delete')")
public CommonResult<Boolean> deleteEnvList(@RequestParam("ids") String ids) {
machineEnvService.deleteEnvList(ids);
return success(true);
}
@GetMapping("/getEnv")
@Operation(summary = "获取机器的环境变量")
public CommonResult<MachineEnvVO> getEnv(@RequestParam("id") Long id) {
MachineEnvVO machineEnvVO = machineEnvService.getEnv(id);
return success(machineEnvVO);
}
@PostMapping("/list")
@Operation(summary = "获取环境变量列表")
public CommonResult<PageResult<MachineEnvVO>> getEnvPage(@Valid @RequestBody MachineEnvVO machineEnvVO) {
PageResult<MachineEnvDO> pageResult = machineEnvService.getEnvPage(machineEnvVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(new PageResult<>(pageResult.getTotal()));
}
return success(BeanUtils.toBean(pageResult, MachineEnvVO.class));
}
}

View File

@ -0,0 +1,131 @@
package cd.casic.module.machine.controller;
import cd.casic.framework.commons.pojo.CommonResult;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.framework.commons.util.object.BeanUtils;
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
import cd.casic.module.machine.enums.ConnectionStatus;
import cd.casic.module.machine.service.MachineInfoService;
import cd.casic.module.machine.controller.vo.MachineInfoVO;
import cn.hutool.core.collection.CollUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import static cd.casic.framework.commons.pojo.CommonResult.success;
@RestController
@Tag(name = "机器信息管理")
@RequestMapping("/ci/machineInfo")
@Validated
public class MachineInfoController {
@Resource
private MachineInfoService machineInfoService;
@PostMapping("/create")
@Operation(summary = "新增机器信息")
// @PreAuthorize("@ss.hasPermission('ci:machineInfo:create')")
public CommonResult<Long> createMachine(@Valid @RequestBody MachineInfoVO machineInfoVO) {
Long id = machineInfoService.createMachine(machineInfoVO);
return success(id);
}
@PutMapping("/update")
@Operation(summary = "编辑机器信息")
// @PreAuthorize("@ss.hasPermission('ci:machineInfo:update')")
public CommonResult<Boolean> updateMachineInfo(@Valid @RequestBody MachineInfoVO machineInfoVO) {
machineInfoService.updateMachineInfo(machineInfoVO);
return success(true);
}
@PutMapping("/updateStatus")
@Operation(summary = "机器启用/停用")
// @PreAuthorize("@ss.hasPermission('ci:machineInfo:status')")
public CommonResult<Integer> updateStatus(@Valid @RequestBody MachineInfoVO machineInfoVO) {
Integer newStatus = machineInfoService.updateStatus(machineInfoVO);
return success(newStatus);
}
@PostMapping("/list")
@Operation(summary = "获取机器信息列表")
public CommonResult<PageResult<MachineInfoVO>> list(@Valid @RequestBody MachineInfoVO machineInfoVO) {
PageResult<MachineInfoDO> pageResult = machineInfoService.listMachineInfo(machineInfoVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(new PageResult<>(pageResult.getTotal()));
}
return success(BeanUtils.toBean(pageResult, MachineInfoVO.class));
}
@DeleteMapping("/delete")
@Operation(summary = "机器信息删除")
// @PreAuthorize("@ss.hasPermission('ci:machineInfo:delete')")
public CommonResult<Boolean> deleteMachineInfo(@RequestParam("machineInfoId") Long machineInfoId) {
machineInfoService.deleteMachineInfo(machineInfoId);
return success(true);
}
@DeleteMapping("/deleteList")
@Operation(summary = "批量删除机器信息")
// @PreAuthorize("@ss.hasPermission('ci:machineInfo:delete')")
public CommonResult<Boolean> deleteMachineInfoList(@RequestParam("machineInfoIds") String ids) {
machineInfoService.deleteMachineInfoList(ids);
return success(true);
}
@GetMapping("/test")
@Operation(summary = "测试机器连接")
public CommonResult<Boolean> testConnection(@RequestParam("id") Long id) {
return success(machineInfoService.testConnection(id));
}
@GetMapping("/status")
@Operation(summary = "获取机器连接状态")
public CommonResult<ConnectionStatus> getConnectionStatus(@RequestParam Long id) {
return success(machineInfoService.getConnectionStatus(id));
}
@GetMapping("/status/all")
@Operation(summary = "获取所有机器连接状态")
public CommonResult<Map<Long, ConnectionStatus>> getAllConnectionStatus() {
return success(machineInfoService.getAllConnectionStatus());
}
@GetMapping("/connect")
@Operation(summary = "建立连接")
public CommonResult<Map<String, Object>> connect(@RequestParam Long id) {
return success(machineInfoService.connect(id));
}
@GetMapping("/fileTreeNode")
@Operation(summary = "获得文件树")
public CommonResult<Map<String, Object>> fileTreeNode(
@RequestParam Long machineId,
@RequestParam(required = false, defaultValue = "/") String path
) {
return CommonResult.success(machineInfoService.fileTreeNode(machineId, path));
}
// @GetMapping("/upload")
// @Operation(summary = "上传文件到远程机器")
// public CommonResult<Boolean> uploadFile(
// @RequestParam String sessionId,
// @RequestParam String localFilePath,
// @RequestParam String remoteFilePath
// ) {
// return success(machineInfoService.uploadFile(sessionId, localFilePath, remoteFilePath));
// }
//
// @GetMapping("/download")
// @Operation(summary = "从远程机器下载文件")
// public CommonResult<Boolean> downloadFile(
// @RequestParam String sessionId,
// @RequestParam String remoteFilePath,
// @RequestParam String localFilePath) {
// return success(machineInfoService.downloadFile(sessionId, remoteFilePath, localFilePath));
// }
}

View File

@ -0,0 +1,81 @@
package cd.casic.module.machine.controller;
import cd.casic.framework.commons.pojo.CommonResult;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.framework.commons.util.object.BeanUtils;
import cd.casic.module.machine.controller.vo.MachineProxyVO;
import cd.casic.module.machine.dal.dataobject.MachineProxyDO;
import cd.casic.module.machine.service.MachineProxyService;
import cn.hutool.core.collection.CollUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import static cd.casic.framework.commons.pojo.CommonResult.success;
/**
* 机器代理控制器
*/
@RestController
@RequestMapping("/ci/machineProxy")
@Tag(name = "机器代理管理")
@Validated
public class MachineProxyController {
@Resource
private MachineProxyService machineProxyService;
@PostMapping("/create")
@Operation(summary = "注册新的机器代理")
// @PreAuthorize("@ss.hasPermission('ci:machineProxy:create')")
public CommonResult<Long> createProxy(@Valid @RequestBody MachineProxyVO machineProxyVO) {
Long id = machineProxyService.createProxy(machineProxyVO);
return success(id);
}
@PutMapping("/update")
@Operation(summary = "修改代理")
// @PreAuthorize("@ss.hasPermission('ci:machineProxy:update')")
public CommonResult<Boolean> updateProxy(@Valid @RequestBody MachineProxyVO machineProxyVO) {
machineProxyService.updateProxy(machineProxyVO);
return success(true);
}
@PostMapping("/list")
@Operation(summary = "获取代理列表")
public CommonResult<PageResult<MachineProxyVO>> getProxyPage(@Valid @RequestBody MachineProxyVO machineProxyVO) {
PageResult<MachineProxyDO> pageResult = machineProxyService.getProxyPage(machineProxyVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(new PageResult<>(pageResult.getTotal()));
}
return success(BeanUtils.toBean(pageResult, MachineProxyVO.class));
}
@GetMapping("/allStatus")
@Operation(summary = "获取所有代理的状态统计")
public CommonResult<Map<Integer, Long>> getStatusStatistics() {
return success(machineProxyService.getAllProxyStatus());
}
@DeleteMapping("delete")
@Operation(summary = "删除代理")
public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
machineProxyService.delete(id);
return success(true);
}
@DeleteMapping("/deleteList")
@Operation(summary = "批量删除代理")
// @PreAuthorize("@ss.hasPermission('ci:machineProxy:delete')")
public CommonResult<Boolean> deleteProxyList(@RequestParam String ids) {
machineProxyService.deleteProxyList(ids);
return success(true);
}
}

View File

@ -0,0 +1,107 @@
package cd.casic.module.machine.controller;
import cd.casic.framework.commons.pojo.CommonResult;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.framework.commons.util.object.BeanUtils;
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
import cd.casic.module.machine.dal.dataobject.SecretKeyDO;
import cd.casic.module.machine.service.SecretKeyService;
import cd.casic.module.machine.controller.vo.SecretKeyVO;
import cn.hutool.core.collection.CollUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cd.casic.framework.commons.pojo.CommonResult.success;
@RestController
@RequestMapping("/ci/secretKey")
@Tag(name = "密钥管理")
@Validated
public class SecretKeyController {
@Resource
private SecretKeyService secretKeyService;
@PostMapping(value = "/create")
@Operation(summary = "新增密钥")
// @PreAuthorize("@ss.hasPermission('ci:secretKey:create')")
public CommonResult<Long> createSecretKey(@Valid @RequestBody SecretKeyVO secretKeyVO) throws Exception {
Long secretKeyId = secretKeyService.createSecretKey(secretKeyVO);
return success(secretKeyId);
}
@PutMapping("/update")
@Operation(summary = "编辑密钥信息")
// @PreAuthorize("@ss.hasPermission('ci:secretKey:update')")
public CommonResult<Boolean> updateSecretKey(@Valid @RequestBody SecretKeyVO secretKeyVO) {
secretKeyService.updateSecretKey(secretKeyVO);
return success(true);
}
@PutMapping("/bindingMachine")
@Operation(summary = "绑定机器")
// @PreAuthorize("@ss.hasPermission('ci:secretKey:binding')")
public CommonResult<Boolean> bindingMachine(@Valid @RequestBody SecretKeyVO secretKeyVO) {
secretKeyService.bindingMachine(secretKeyVO);
return success(true);
}
@PutMapping("/unbindMachine")
@Operation(summary = "解绑机器")
public CommonResult<Boolean> unbindMachine(@Valid @RequestBody SecretKeyVO secretKeyVO) {
secretKeyService.unbindMachine(secretKeyVO);
return success(true);
}
@GetMapping("/getBindMachine")
@Operation(summary = "获取密钥绑定的机器列表")
public CommonResult<List<MachineInfoDO>> getBindMachine(@RequestParam Long secretKeyId) {
return success(secretKeyService.getBindMachine(secretKeyId));
}
@GetMapping("/getSecretKey")
@Operation(summary = "获取机器的密钥")
public CommonResult<SecretKeyVO> getSecretKey(@RequestParam("id") Long id) {
SecretKeyVO secretKeyVO = secretKeyService.getSecretKey(id);
return success(secretKeyVO);
}
@DeleteMapping("/delete")
@Operation(summary = "密钥信息删除")
public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
secretKeyService.deleteSecretKey(id);
return success(true);
}
@DeleteMapping("/deleteList")
@Operation(summary = "批量删除密钥")
// @PreAuthorize("@ss.hasPermission('ci:secretKey:delete')")
public CommonResult<Boolean> deleteSecretKeyList(@RequestParam("ids") List<Long> ids) {
secretKeyService.deleteSecretKeyList(ids);
return success(true);
}
@PostMapping("/list")
@Operation(summary = "获取密钥信息列表")
public CommonResult<PageResult<SecretKeyVO>> getSecretKeyPage(@Valid @RequestBody SecretKeyVO secretKeyVO) {
PageResult<SecretKeyDO> pageResult = secretKeyService.getSecretKeypage(secretKeyVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(new PageResult<>(pageResult.getTotal()));
}
return success(BeanUtils.toBean(pageResult, SecretKeyVO.class));
}
@GetMapping("/download")
@Operation(summary = "下载密钥文件")
public ResponseEntity<InputStreamResource> downloadSecretFile(@RequestParam("id") Long id) {
return secretKeyService.downloadSecretFile(id);
}
}

View File

@ -0,0 +1,38 @@
package cd.casic.module.machine.controller.vo;
import cd.casic.framework.commons.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
@EqualsAndHashCode(callSuper = true)
@Schema(description = "管理后台 - 机器环境变量信息 Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true) // 添加链式调用支持
public class MachineEnvVO extends PageParam {
@Schema(description = "环境变量ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "环境变量键", requiredMode = Schema.RequiredMode.REQUIRED, example = "JAVA_HOME")
private String envKey;
@Schema(description = "环境变量值", requiredMode = Schema.RequiredMode.REQUIRED, example = "/usr/java/jdk1.8.0_271")
private String envValue;
@Schema(description = "环境变量描述", example = "Java运行环境路径")
private String description;
@Schema(description = "关联的机器ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long machineId;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2023-06-15T10:30:00")
private LocalDateTime createTime;
@Schema(description = "更新时间", example = "2023-06-15T10:30:00")
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,64 @@
package cd.casic.module.machine.controller.vo;
import cd.casic.framework.commons.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import lombok.experimental.Accessors;
import java.util.Date;
@EqualsAndHashCode(callSuper = true)
@Schema(description = "管理后台 - 机器信息 Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class MachineInfoVO extends PageParam {
@Schema(description = "机器ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2023-06-15T10:30:00")
private Date createTime;
@Schema(description = "更新时间", example = "2023-06-15T10:30:00")
private Date updateTime;
@Schema(description = "机器名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "server-01")
private String name;
@Schema(description = "机器标签,唯一标识", example = "production,web-server")
private String tag;
@Schema(description = "主机IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.100")
private String hostIp;
@Schema(description = "机器描述", example = "生产环境Web服务器")
private String description;
@Schema(description = "登录用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin")
private String username;
@Schema(description = "机器状态", example = "online,offline,maintenance")
private Integer status = -1;
@Schema(description = "SSH端口", example = "22")
private Integer sshPort;
@Schema(description = "登录密码", example = "******")
private String password;
@Schema(description = "密钥ID", example = "5")
private Long secretKeyId;
@Schema(description = "代理ID", example = "101")
private Long machineProxyId;
@Schema(description = "认证类型", example = "password,key")
private Integer authenticationType ;
@Schema(description = "机器信息类型", example = "Linux,Windows")
private Integer machineInfoType;
}

View File

@ -0,0 +1,52 @@
package cd.casic.module.machine.controller.vo;
import cd.casic.framework.commons.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 机器代理信息 Response VO")
@EqualsAndHashCode(callSuper = true)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class MachineProxyVO extends PageParam {
@Schema(description = "代理ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin")
private String username;
@Schema(description = "代理类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "HTTP")
private Integer proxyType = -1;
@Schema(description = "版本号", example = "1.0.0")
private String version;
@Schema(description = "状态ONLINE:在线OFFLINE:离线)", requiredMode = Schema.RequiredMode.REQUIRED, example = "ONLINE")
private int status = -1;
@Schema(description = "描述信息", example = "用于生产环境的代理服务器")
private String description;
@Schema(description = "主机IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.100")
private String hostIp;
@Schema(description = "SSH端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "22")
private String sshPort;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2023-06-15T10:30:00")
private LocalDateTime createTime;
@Schema(description = "更新时间", example = "2023-06-15T10:30:00")
private LocalDateTime updateTime;
@Schema(description = "密码", example = "******")
private String password;
}

View File

@ -0,0 +1,47 @@
package cd.casic.module.machine.controller.vo;
import cd.casic.framework.commons.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import lombok.experimental.Accessors;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.time.LocalDateTime;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Schema(description = "管理后台 - 密钥信息 Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true) // 添加链式调用支持
public class SecretKeyVO extends PageParam {
@Schema(description = "密钥ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "密钥名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "生产环境密钥")
private String name;
@Schema(description = "密钥描述", example = "用于加密敏感数据的密钥")
private String description;
@Schema(description = "存储路径(本地上传文件路径)", example = "/data/secret_keys/")
private String path;
@Schema(description = "文件名", example = "key.pem")
private String fileName;
@Schema(description = "密钥密码", example = "******")
private String password;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2023-06-15T10:30:00")
private LocalDateTime createTime;
@Schema(description = "更新时间", example = "2023-06-15T10:30:00")
private LocalDateTime updateTime;
@Schema(description = "关联的机器ID列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1024, 2048]")
private List<Long> machineInfoIds;
}

View File

@ -0,0 +1,57 @@
package cd.casic.module.machine.dal.dataobject;
import cd.casic.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.*;
import lombok.*;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 环境变量实体类
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("machine_env")
public class MachineEnvDO extends BaseDO {
/**
* 环境变量id
*/
@TableId
private Long id;
/**
* 机器ID唯一关联
*/
private Long machineId;
/**
* 环境变量键
*/
private String envKey;
/**
* 环境变量值
*/
private String envValue;
/**
* 描述信息
*/
private String description;
}

View File

@ -0,0 +1,68 @@
package cd.casic.module.machine.dal.dataobject;
import cd.casic.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "machine_info")
public class MachineInfoDO extends BaseDO {
/**
* 机器id
*/
@TableId
private Long id;
@TableField(value = "name")
private String name;
/**
* 机器唯一标识
*/
@TableField(value = "tag")
private String tag;
@TableField(value = "host_ip")
private String hostIp;
@TableField(value = "description")
private String description;
@TableField(value = "machine_info_type")
private Integer machineInfoType;
@TableField(value = "status")
private Integer status;
//用户名
@TableField(value = "username")
private String username;
//SSH端口号
@TableField(value = "ssh_port")
private Integer sshPort;
@TableField(value = "password")
private String password;
@TableField(value = "secret_key_id")
private Long secretKeyId;
@TableField(value = "machine_proxy_id")
private Long machineProxyId;
@TableField(value = "authentication_type")
private Integer authenticationType;
}

View File

@ -0,0 +1,58 @@
package cd.casic.module.machine.dal.dataobject;
import cd.casic.framework.mybatis.core.dataobject.BaseDO;
import cd.casic.module.machine.enums.MachineProxyStatus;
import com.baomidou.mybatisplus.annotation.*;
import cd.casic.module.machine.enums.MachineProxyType;
import lombok.*;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 机器代理实体类
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("machine_proxy")
public class MachineProxyDO extends BaseDO {
/**
* 代理id
*/
@TableId
private Long id;
@TableField(value = "host_ip")
private String hostIp;
@TableField(value = "ssh_port")
private String sshPort;
//todo 字典
@TableField(value = "proxy_type")
private int proxyType;
@TableField(value = "version")
private String version;
//todo 字典
@TableField(value = "status")
private int status;
@TableField(value = "username")
private String username;
@TableField(value = "password")
private String password;
@TableField(value = "description")
private String description;
}

View File

@ -0,0 +1,41 @@
package cd.casic.module.machine.dal.dataobject;
import cd.casic.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "machine_secret_key")
public class SecretKeyDO extends BaseDO {
/**
* 密钥id
*/
@TableId
private Long id;
@TableField(value = "name")
private String name;
@TableField(value = "description")
private String description;
//oss存储路径
@TableField(value = "path")
private String path;
@TableField
private String fileName;
//密钥密码
@TableField(value = "password")
private String password;
}

View File

@ -0,0 +1,56 @@
package cd.casic.module.machine.dal.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception;
import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*;
import java.util.ArrayList;
import java.util.List;
//文件节点类
@Data
/*
* 远程文件系统的节点信息用于构建文件树结构
*/
@Schema(description = "远程文件系统节点")
public class FileNode {
@Schema(description = "文件名或目录名")
private String name;
@Schema(description = "是否为目录")
private boolean isDirectory;
@Schema(description = "文件大小(字节)")
private long size;
@Schema(description = "文件权限字符串(如 rwxr-xr-x")
private String permissions;
@Schema(description = "最后修改时间(时间戳,毫秒)")
private long modifiedTime;
@Schema(description = "子节点列表(仅目录有此属性)")
private List<FileNode> children;
public FileNode(String name, boolean isDirectory, long size, String permissions, long modifiedTime) {
this.name = name;
this.isDirectory = isDirectory;
this.size = size;
this.permissions = permissions;
this.modifiedTime = modifiedTime;
if (isDirectory) {
this.children = new ArrayList<>();
}
}
public void addChild(FileNode child) {
if (this.children == null) {
throw exception(NOT_DIRECTORY_NODE);
}
this.children.add(child);
}
}

View File

@ -0,0 +1,27 @@
package cd.casic.module.machine.dal.mysql;
import cd.casic.framework.mybatis.core.mapper.BaseMapperX;
import cd.casic.module.machine.controller.vo.MachineEnvVO;
import cd.casic.module.machine.dal.dataobject.MachineEnvDO;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.framework.mybatis.core.query.LambdaQueryWrapperX;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
/**
* 环境变量Mapper接口
*/
@Mapper
public interface MachineEnvMapper extends BaseMapperX<MachineEnvDO> {
default PageResult<MachineEnvDO> selectPage(MachineEnvVO machineEnvVO) {
LambdaQueryWrapperX<MachineEnvDO> machineEnvDOLambdaQueryWrapperX = new LambdaQueryWrapperX<MachineEnvDO>()
.likeIfPresent(MachineEnvDO::getEnvKey, machineEnvVO.getEnvKey())
.likeIfPresent(MachineEnvDO::getDescription, machineEnvVO.getDescription());
if (machineEnvVO.getMachineId() != null && machineEnvVO.getMachineId() > 0) {
machineEnvDOLambdaQueryWrapperX.eqIfPresent(MachineEnvDO::getMachineId, machineEnvVO.getMachineId());
}
return selectPage(machineEnvVO, machineEnvDOLambdaQueryWrapperX);
}
}

View File

@ -0,0 +1,57 @@
package cd.casic.module.machine.dal.mysql;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.framework.mybatis.core.mapper.BaseMapperX;
import cd.casic.framework.mybatis.core.query.LambdaQueryWrapperX;
import cd.casic.module.machine.controller.vo.MachineEnvVO;
import cd.casic.module.machine.controller.vo.MachineInfoVO;
import cd.casic.module.machine.dal.dataobject.MachineEnvDO;
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import jakarta.annotation.Resource;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MachineInfoMapper extends BaseMapperX<MachineInfoDO> {
default Boolean existsByTag(String tag) {
return selectOne(new QueryWrapper<MachineInfoDO>().eq("tag", tag)) != null;
}
default void updateStatus(Long machineInfoId, Integer status) {
UpdateWrapper<MachineInfoDO> set = new UpdateWrapper<>();
set.eq("id", machineInfoId).set("status", status);
this.update(null, set);
}
default void bindingSecretKey(List<Long> machineInfoIds, Long secretKeyId) {
UpdateWrapper<MachineInfoDO> wrapper = new UpdateWrapper<>();
wrapper.in("id", machineInfoIds) // 匹配 ID 集合
.set("secret_key_id", secretKeyId); // 设置新的 status
this.update(null, wrapper);
}
default PageResult<MachineInfoDO> selectPage(MachineInfoVO machineInfoVO) {
LambdaQueryWrapperX<MachineInfoDO> machineInfoDOLambdaQueryWrapperX = new LambdaQueryWrapperX<MachineInfoDO>()
.likeIfPresent(MachineInfoDO::getName, machineInfoVO.getName())
.likeIfPresent(MachineInfoDO::getTag, machineInfoVO.getTag())
.likeIfPresent(MachineInfoDO::getDescription, machineInfoVO.getDescription())
.likeIfPresent(MachineInfoDO::getUsername, machineInfoVO.getUsername())
.likeIfPresent(MachineInfoDO::getHostIp, machineInfoVO.getHostIp());
if (machineInfoVO.getStatus() != -1) {
machineInfoDOLambdaQueryWrapperX.eqIfPresent(MachineInfoDO::getStatus, machineInfoVO.getStatus());
}
return selectPage(machineInfoVO, machineInfoDOLambdaQueryWrapperX);
}
default List<MachineInfoDO> selectBindMachineBySecretKey(Long secretKeyId) {
LambdaQueryWrapperX<MachineInfoDO> lambdaQueryWrapperX = new LambdaQueryWrapperX<MachineInfoDO>()
.eq(MachineInfoDO::getSecretKeyId, secretKeyId);
return selectList(lambdaQueryWrapperX);
}
}

View File

@ -0,0 +1,29 @@
package cd.casic.module.machine.dal.mysql;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.framework.mybatis.core.mapper.BaseMapperX;
import cd.casic.framework.mybatis.core.query.LambdaQueryWrapperX;
import cd.casic.module.machine.controller.vo.MachineProxyVO;
import cd.casic.module.machine.dal.dataobject.MachineProxyDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 机器代理Mapper接口
*/
@Mapper
public interface MachineProxyMapper extends BaseMapperX<MachineProxyDO> {
default PageResult<MachineProxyDO> selectPage(MachineProxyVO machineProxyVO) {
LambdaQueryWrapperX<MachineProxyDO> machineProxyDOLambdaQueryWrapperX = new LambdaQueryWrapperX<MachineProxyDO>()
.eqIfPresent(MachineProxyDO::getHostIp, machineProxyVO.getHostIp())
.likeIfPresent(MachineProxyDO::getDescription, machineProxyVO.getDescription());
if (machineProxyVO.getStatus() != -1) {
machineProxyDOLambdaQueryWrapperX.eqIfPresent(MachineProxyDO::getStatus, machineProxyVO.getStatus());
}
if (machineProxyVO.getProxyType() != -1) {
machineProxyDOLambdaQueryWrapperX.eqIfPresent(MachineProxyDO::getProxyType, machineProxyVO.getProxyType());
}
return selectPage(machineProxyVO, machineProxyDOLambdaQueryWrapperX);
}
}

View File

@ -0,0 +1,28 @@
package cd.casic.module.machine.dal.mysql;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.framework.mybatis.core.mapper.BaseMapperX;
import cd.casic.framework.mybatis.core.query.LambdaQueryWrapperX;
import cd.casic.module.machine.controller.vo.SecretKeyVO;
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
import cd.casic.module.machine.dal.dataobject.SecretKeyDO;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface SecretKeyMapper extends BaseMapperX<SecretKeyDO> {
//查询列表
default PageResult<SecretKeyDO> selectPage(SecretKeyVO secretKeyVO) {
return selectPage(secretKeyVO, new LambdaQueryWrapperX<SecretKeyDO>()
.likeIfPresent(SecretKeyDO::getName, secretKeyVO.getName())
.likeIfPresent(SecretKeyDO::getDescription, secretKeyVO.getDescription()));
}
default void bindingMachine(Long machineInfoId, List<Long> secretKeyId) {
UpdateWrapper<SecretKeyDO> set = new UpdateWrapper<>();
set.eq("id", secretKeyId).set("machineInfoId", machineInfoId);
this.update(null, set);
}
}

View File

@ -0,0 +1,25 @@
package cd.casic.module.machine.enums;
import cd.casic.framework.commons.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
@Getter
@AllArgsConstructor
public enum AuthenticationType implements IntArrayValuable {
PASSWORD(1,"密码认证"),
SECRET_KEY(2,"密钥认证");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AuthenticationType::getCode).toArray();
private final int code;
private final String message;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,33 @@
package cd.casic.module.machine.enums;
import cd.casic.framework.commons.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 连接状态枚举
*/
@Getter
@AllArgsConstructor
public enum ConnectionStatus implements IntArrayValuable {
DISCONNECTED(1, "断开连接"),
CONNECTING(2, "正在连接"),
CONNECTED(3, "已连接"),
AUTH_FAILED(4, "认证失败"),
CONNECTION_TIMEOUT(5, "连接超时"),
CONNECTION_ERROR(6, "连接错误"),
CLOSED(7, "已关闭");
private final int code;
private final String message;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ConnectionStatus::getCode).toArray();
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,24 @@
package cd.casic.module.machine.enums;
import cd.casic.framework.commons.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
@Getter
@AllArgsConstructor
public enum MachineInfoStatus implements IntArrayValuable {
ENABLE(1, "启用"),
UN_ENABLE(0, "停用");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(MachineInfoStatus::getCode).toArray();
private final int code;
private final String message;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,24 @@
package cd.casic.module.machine.enums;
import cd.casic.framework.commons.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
@Getter
@AllArgsConstructor
public enum MachineInfoType implements IntArrayValuable {
Linux(1,"Linux"),
WINDOWS(2,"Windows");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(MachineInfoType::getCode).toArray();
private final int code;
private final String message;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,33 @@
package cd.casic.module.machine.enums;
import cd.casic.framework.commons.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.List;
@Getter
@AllArgsConstructor
public enum MachineProxyStatus implements IntArrayValuable {
/**
* 代理状态 (online, offline, installing, updating, error)
*/
ONLINE(1, "online"),
OFFLINE(2, "offline"),
INSTALLING(3, "installing"),
UPDATING(4, "updating"),
ERROR(5, "error");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(MachineProxyStatus::getCode).toArray();
private final int code;
private final String message;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,31 @@
package cd.casic.module.machine.enums;
import cd.casic.framework.commons.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
@Getter
@AllArgsConstructor
public enum MachineProxyType implements IntArrayValuable {
HTTP(1, "http"),
SOCKS4(2, "socks4"),
SOCKS5(3, "socks5");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(MachineProxyType::getCode).toArray();
/**
* 状态值
*/
private final Integer code;
/**
* 状态名
*/
private final String message;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,35 @@
package cd.casic.module.machine.enums;
import cd.casic.framework.commons.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
@Getter
@AllArgsConstructor
//ssh远程连接管道类型
public enum SSHChanelType implements IntArrayValuable {
EXEC(1, "单条命令返回结果"),
SHELL(2, "多行命令持续交互"),
SUBSYSTEM(3, "文件传输或其它特点服务"),
DIRECT_TCP_IP(4, "TCP 端口转发"),
X11(5, "X Window 系统转发");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SSHChanelType::getCode).toArray();
/**
* 状态值
*/
private final Integer code;
/**
* 状态名
*/
private final String message;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,67 @@
package cd.casic.module.machine.handler;
import cd.casic.module.machine.component.WebSocketConnection;
import cd.casic.module.machine.component.WebSocketSessionManager;
import cd.casic.module.machine.enums.ConnectionStatus;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception;
import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*;
//WebSocket处理器
@Component("machineWebSocketHandler")
public class MachineWebSocketHandler extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(@NotNull WebSocketSession webSocketSession) {
Long machineId = (Long) webSocketSession.getAttributes().get("machineId");
//保存webSocketSession信息
WebSocketSessionManager.addWebSocketSession(webSocketSession.getId(), webSocketSession);
WebSocketSessionManager.addWebSocketConnection(webSocketSession.getId(), WebSocketSessionManager.getWebSocketConnectionByMachineId(machineId));
try {
webSocketSession.sendMessage(new TextMessage("欢迎连接到 WebSocket 服务器!"));
} catch (IOException e) {
throw exception(WEBSOCKET_SEND_MESSAGE_ERROR);
}
}
// 处理文本消息
@Override
protected void handleTextMessage(WebSocketSession webSocketSession, TextMessage message) {
String payload = message.getPayload();
String sessionId = webSocketSession.getId();
// 从管理器获取连接
WebSocketConnection webSocketConnection = WebSocketSessionManager.getWebSocketConnection(sessionId);
if (webSocketConnection != null && ConnectionStatus.CONNECTING.equals(webSocketConnection.getConnectionStatus())) {
// 转发消息到远程机器
webSocketConnection.executeCommand(webSocketSession, payload);
} else if (webSocketConnection != null) {
webSocketConnection.sendErrorMessage(webSocketSession, "连接已断开,无法发送消息");
} else {
throw exception(WEBSOCKET_SEND_MESSAGE_ERROR);
}
}
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, @NotNull CloseStatus status) {
String sessionId = webSocketSession.getId();
// 获取并关闭相关的 WebSocketConnection
WebSocketConnection webSocketConnection = WebSocketSessionManager.getWebSocketConnection(sessionId);
if (webSocketConnection != null) {
webSocketConnection.disconnect();
}
// 从管理器中移除会话和连接
WebSocketSessionManager.removeWebSocketSession(sessionId);
WebSocketSessionManager.removeWebSocketConnection(sessionId);
Long machineInfoId = (Long) webSocketSession.getAttributes().get("machineId");
WebSocketSessionManager.removeWebSocketConnectionByMachineId(machineInfoId);
}
}

View File

@ -0,0 +1,45 @@
package cd.casic.module.machine.service;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.module.machine.controller.vo.MachineEnvVO;
import cd.casic.module.machine.dal.dataobject.MachineEnvDO;
import jakarta.validation.Valid;
/**
* 环境变量服务接口
*/
public interface MachineEnvService {
/**
* 创建或更新机器的环境变量一对一关系
*/
Long createEnv(@Valid MachineEnvVO machineEnvVO);
/**
* 删除机器的环境变量
*/
void deleteEnv(Long machineEvnId);
/**
* 获取机器的环境变量
*
* @param machineId 机器ID
* @return 环境变量DTO
*/
MachineEnvVO getEnv(Long machineId);
/**
* @return 环境变量列表
*/
PageResult<MachineEnvDO> getEnvPage(@Valid MachineEnvVO machineEnvVO);
/**
* 批量删除
*/
void deleteEnvList(String ids);
/*
* 修改环境变量
*/
void updateEnv(@Valid MachineEnvVO machineEnvVO);
}

View File

@ -0,0 +1,101 @@
package cd.casic.module.machine.service;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.module.machine.controller.vo.MachineInfoVO;
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
import cd.casic.module.machine.enums.ConnectionStatus;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
public interface MachineInfoService {
/**
* 新增机器
* @return 新增机器的id
*/
Long createMachine(MachineInfoVO MachineInfoVO);
/**
* 查看机器列表
* @return 分院
*/
PageResult<MachineInfoDO> listMachineInfo(@Valid MachineInfoVO MachineInfoVO);
void updateMachineInfo(@Valid MachineInfoVO machineInfoVO);
Integer updateStatus(@Valid MachineInfoVO machineInfoVO);
void bindingSecretKey(List<Long> machineInfoIds, Long secretKeyId);
void deleteMachineInfoList(String machineInfoIds);
void deleteMachineInfo(Long machineInfoId);
/**
* 测试机器连接
*
* @param id 机器id
* @return 连接是否成功
*/
boolean testConnection(Long id);
/**
* 连接远程机器
*
* @param id 机器id
* @return 连接后端文件树
*/
Map<String, Object> connect(Long id);
/**
* 获取机器连接状态
*
* @return 连接状态
*/
ConnectionStatus getConnectionStatus(Long id);
/**
* 获取所有连接状态
*
* @return 机器名称到连接状态的映射
*/
Map<Long, ConnectionStatus> getAllConnectionStatus();
// /**
// * 上传文件到远程机器
// *
// * @param sessionId 会话ID
// * @param localFilePath 本地文件路径
// * @param remoteFilePath 远程文件路径
// * @return 操作结果
// */
// boolean uploadFile(String sessionId, String localFilePath, String remoteFilePath);
//
// /**
// * 从远程机器下载文件
// *
// * @param sessionId 会话ID
// * @param remoteFilePath 远程文件路径
// * @param localFilePath 本地文件路径
// * @return 操作结果
// */
// boolean downloadFile(String sessionId, String remoteFilePath, String localFilePath);
/**
* 校验机器是否存在
*
* @param id 机器id
*/
MachineInfoDO validateMachineInfoExists(Long id);
/**
* 根据路径获得远程文件树
*
* @param machineId 机器id
* @param path 文件夹路径
* @return 远程文件树
*/
Map<String, Object> fileTreeNode(Long machineId, String path);
}

View File

@ -0,0 +1,49 @@
package cd.casic.module.machine.service;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.module.machine.controller.vo.MachineProxyVO;
import cd.casic.module.machine.dal.dataobject.MachineProxyDO;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Map;
/**
* 机器代理服务接口
*/
public interface MachineProxyService {
/**
* 注册新的机器代理
*/
Long createProxy(@Valid MachineProxyVO machineProxyVO);
/**
* 更新代理状态
*/
void updateProxy(@Valid MachineProxyVO machineProxyVO);
/**
* 删除代理
*
* @param
*/
void delete(Long id);
/**
* 获取所有代理的状态统计
*
* @return 状态统计Map
*/
Map<Integer, Long> getAllProxyStatus();
/**
* 批量删除代理
*
* @param proxyIds 代理ID列表
*/
void deleteProxyList(String proxyIds);
PageResult<MachineProxyDO> getProxyPage(@Valid MachineProxyVO machineProxyVO);
}

View File

@ -0,0 +1,36 @@
package cd.casic.module.machine.service;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
import cd.casic.module.machine.dal.dataobject.SecretKeyDO;
import cd.casic.module.machine.controller.vo.SecretKeyVO;
import jakarta.validation.Valid;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.ResponseEntity;
import java.util.List;
public interface SecretKeyService {
Long createSecretKey(@Valid SecretKeyVO secretKeyVO) throws Exception;
void bindingMachine(@Valid SecretKeyVO secretKeyVO);
void updateSecretKey(@Valid SecretKeyVO secretKeyVO);
PageResult<SecretKeyDO> getSecretKeypage(@Valid SecretKeyVO secretKeyVO);
void deleteSecretKeyList(List<Long> ids);
SecretKeyVO getSecretKey(Long id);
void deleteSecretKey(Long id);
void unbindMachine(@Valid SecretKeyVO secretKeyVO);
ResponseEntity<InputStreamResource> downloadSecretFile(Long id);
List<MachineInfoDO> getBindMachine(Long secretKeyId);
String getKeyContent(Long secretKeyId);
}

View File

@ -0,0 +1,105 @@
package cd.casic.module.machine.service.impl;
import cd.casic.module.machine.controller.vo.MachineEnvVO;
import cd.casic.module.machine.dal.dataobject.MachineEnvDO;
import cd.casic.module.machine.dal.mysql.MachineEnvMapper;
import cd.casic.module.machine.service.MachineEnvService;
import cd.casic.framework.commons.pojo.PageResult;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import cd.casic.framework.commons.util.object.BeanUtils;
import java.util.Arrays;
import java.util.List;
import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception;
import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*;
/**
* 环境变量服务实现类
*/
@Service("machineEnvService")
public class MachineEnvServiceImpl implements MachineEnvService {
@Resource
private MachineEnvMapper machineEnvMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createEnv(MachineEnvVO machineEnvVO) {
validateMachineEnvAdd(machineEnvVO);
// 检查键是否合法
validateKey(machineEnvVO.getEnvKey());
MachineEnvDO machineEnvDO = BeanUtils.toBean(machineEnvVO, MachineEnvDO.class);
machineEnvMapper.insert(machineEnvDO);
return machineEnvDO.getId();
}
@Override
public void deleteEnv(Long machineEvnId) {
machineEnvMapper.deleteById(machineEvnId);
}
@Override
public MachineEnvVO getEnv(Long machineId) {
MachineEnvDO machineEnvDO = validateMachineEnvExists(machineId);
return BeanUtils.toBean(machineEnvDO, MachineEnvVO.class);
}
@Override
public PageResult<MachineEnvDO> getEnvPage(MachineEnvVO machineEnvVO) {
return machineEnvMapper.selectPage(machineEnvVO);
}
@Override
public void deleteEnvList(String ids) {
//ids转换为List,使用流
List<Long> machineEnvIds = Arrays.stream(ids.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(Long::parseLong)
.toList();
machineEnvMapper.deleteBatchIds(machineEnvIds);
}
@Override
public void updateEnv(MachineEnvVO machineEnvVO) {
MachineEnvDO machineEnvDO = validateMachineEnvExists(machineEnvVO.getId());
BeanUtils.copyProperties(machineEnvVO, machineEnvDO);
machineEnvMapper.updateById(machineEnvDO);
}
@VisibleForTesting
MachineEnvDO validateMachineEnvExists(Long id) {
if (id == null) {
return null;
}
MachineEnvDO machineEnvDO = machineEnvMapper.selectById(id);
if (machineEnvDO == null) {
throw exception(MACHINE_ENV_NOT_EXISTS);
}
return machineEnvDO;
}
@VisibleForTesting
void validateMachineEnvAdd(MachineEnvVO machineEnvVO) {
if (machineEnvVO.getEnvKey() == null || machineEnvVO.getEnvValue() == null) {
throw exception(MACHINE_ENV_NULL);
}
}
// 检查环境变量键是否合法
@VisibleForTesting
private void validateKey(String key) {
if (!key.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
throw exception(MACHINE_ENV_KEY_ILLEGAL);
}
}
}

View File

@ -0,0 +1,274 @@
package cd.casic.module.machine.service.impl;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.module.machine.component.FileTreeComponent;
import cd.casic.module.machine.component.WebSocketSessionManager;
import cd.casic.module.machine.controller.vo.SecretKeyVO;
import cd.casic.module.machine.enums.AuthenticationType;
import cd.casic.module.machine.enums.MachineInfoType;
import cd.casic.module.machine.dal.mysql.MachineInfoMapper;
import cd.casic.module.machine.controller.vo.MachineInfoVO;
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
import cd.casic.module.machine.enums.ConnectionStatus;
import cd.casic.module.machine.enums.MachineInfoStatus;
import cd.casic.module.machine.component.WebSocketConnection;
import cd.casic.module.machine.service.MachineInfoService;
import cd.casic.module.machine.service.SecretKeyService;
import com.google.common.annotations.VisibleForTesting;
import com.jcraft.jsch.Session;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import cd.casic.framework.commons.util.object.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception;
import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*;
/**
* 机器信息服务实现类
*/
@Slf4j
@Service("machineInfoService")
public class MachineInfoServiceImpl implements MachineInfoService {
@Resource
private SecretKeyService secretKeyService;
@Resource
private MachineInfoMapper machineInfoMapper;
@Resource
private FileTreeComponent fileTreeComponent;
@Override
public Long createMachine(MachineInfoVO machineInfoVO) {
validateMachineEnvAdd(machineInfoVO);
validateMachineTagUnique(machineInfoVO.getTag());
MachineInfoDO machineInfoDO = BeanUtils.toBean(machineInfoVO, MachineInfoDO.class);
if (machineInfoVO.getAuthenticationType() == 2) {
Long secretKeyId = machineInfoDO.getSecretKeyId();
SecretKeyVO secretKey = secretKeyService.getSecretKey(secretKeyId);
if (secretKey == null) {
throw exception(SECRET_KEY_NOT_EXISTS);
}
}
machineInfoMapper.insert(machineInfoDO);
return machineInfoDO.getId();
}
@Override
public void updateMachineInfo(MachineInfoVO machineInfoVO) {
validateMachineEnvAdd(machineInfoVO);
String newTag = machineInfoVO.getTag();
MachineInfoDO machineInfoDO = validateMachineInfoExists(machineInfoVO.getId());
String oldTag = machineInfoDO.getTag();
if (!newTag.equals(oldTag)) {
validateMachineTagUnique(newTag);
}
BeanUtils.copyProperties(machineInfoVO, machineInfoDO);
machineInfoMapper.updateById(machineInfoDO);
}
@Override
public Integer updateStatus(MachineInfoVO machineInfoVO) {
machineInfoMapper.updateStatus(machineInfoVO.getId(), machineInfoVO.getStatus());
return machineInfoVO.getStatus();
}
@Override
public PageResult<MachineInfoDO> listMachineInfo(MachineInfoVO machineInfoVO) {
return machineInfoMapper.selectPage(machineInfoVO);
}
@Override
public void bindingSecretKey(List<Long> machineInfoIds, Long secretKeyId) {
machineInfoMapper.bindingSecretKey(machineInfoIds, secretKeyId);
}
@Override
@Transactional//其中一个在线那么就回滚
public void deleteMachineInfoList(String machineInfoIds) {
List<Long> machineInfoIdList = Arrays.stream(machineInfoIds.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(Long::parseLong)
.toList();
machineInfoIdList.forEach(this::deleteMachineInfo);
}
@Override
public void deleteMachineInfo(Long machineInfoId) {
MachineInfoDO machineInfoDO = validateMachineInfoExists(machineInfoId);
validateMachineEnable(machineInfoDO);
machineInfoMapper.deleteById(machineInfoId);
}
@Override
public boolean testConnection(Long id) {
//先查询机器是否存在在判断机器可用性
MachineInfoDO machineInfoDO = validateMachineInfoExists(id);
validateMachineUnEnable(machineInfoDO);
log.info("测试机器连接: {}", machineInfoDO.getHostIp());
WebSocketConnection webSocketConnection = createWebSocketConnection(machineInfoDO);
webSocketConnection.initConnection(machineInfoDO);
return true;
}
@Override
public Map<String, Object> connect(Long id) {
//todo使用代理机器的情况还未完成
WebSocketConnection webSocketConnection = new WebSocketConnection(this.secretKeyService);
MachineInfoDO machineInfoDO = validateMachineInfoExists(id);
//初始化连接
webSocketConnection.initConnection(machineInfoDO);
WebSocketSessionManager.addWebSocketConnectionByMachineId(id, webSocketConnection);
return fileTreeComponent.getRemoteFileTree(webSocketConnection.getSshSession(), "/");
}
@Override
public ConnectionStatus getConnectionStatus(Long id) {
validateMachineInfoExists(id);
WebSocketConnection webSocketConnection = WebSocketSessionManager.getWebSocketConnectionByMachineId(id);
return webSocketConnection == null ? ConnectionStatus.DISCONNECTED : webSocketConnection.getConnectionStatus();
}
@Override
public Map<Long, ConnectionStatus> getAllConnectionStatus() {
return WebSocketSessionManager.getAllWebSocketConnections().entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().getConnectionStatus()
));
}
// @Override
// public boolean uploadFile(String sessionId, String localFilePath, String remoteFilePath) {
// log.info("上传文件: {} -> {}, 会话ID: {}", localFilePath, remoteFilePath, sessionId);
// WebSocketConnection webSocketConnection = sessionConnectionMap.get(sessionId);
// if (webSocketConnection == null||webSocketConnection.getConnectionStatus() != ConnectionStatus.CONNECTED) {
// throw exception(SESSION_NOT_CONNECT);
// }
// try {
// return webSocketConnection.uploadFile(localFilePath, remoteFilePath);
// } catch (Exception e) {
// log.error("文件上传失败: {}", e.getMessage(), e);
// throw exception(FILE_UPLOAD_ERROR);
// }
// }
// @Override
// public boolean downloadFile(String sessionId, String remoteFilePath, String localFilePath) {
// log.info("下载文件: {} -> {}, 会话ID: {}", remoteFilePath, localFilePath, sessionId);
//
// WebSocketConnection webSocketConnection = sessionConnectionMap.get(sessionId);
// if (webSocketConnection == null||webSocketConnection.getConnectionStatus() != ConnectionStatus.CONNECTED) {
// throw new RuntimeException("会话不存在: " + sessionId);
// }
// try {
// return webSocketConnection.downloadFile(remoteFilePath, localFilePath);
// } catch (Exception e) {
// log.error("文件下载失败: {}", e.getMessage(), e);
// throw exception(FILE_DOWNLOAD_ERROR);
// }
// }
@VisibleForTesting
void validateMachineEnvAdd(MachineInfoVO machineInfoVO) {
if (machineInfoVO.getHostIp().isEmpty()) {
throw exception(MACHINE_INFO_HOST_IP_NULL);
}
if (machineInfoVO.getUsername().isEmpty()) {
throw exception(MACHINE_INFO_USER_NAME_NULL);
}
if (machineInfoVO.getTag().isEmpty()) {
throw exception(MACHINE_INFO_TAG_NULL);
}
if (machineInfoVO.getAuthenticationType() != null) {
boolean flag = true;
for (int type : AuthenticationType.ARRAYS) {
if (type == machineInfoVO.getAuthenticationType()) {
flag = false;
break;
}
}
if (flag) {
throw exception(MACHINE_INFO_AUTHENTICATION_TYPE_NOT_EXISTS);
}
} else {
throw exception(MACHINE_INFO_AUTHENTICATION_TYPE_NULL);
}
if (machineInfoVO.getMachineInfoType() != null) {
boolean flag = true;
for (int type : MachineInfoType.ARRAYS) {
if (type == machineInfoVO.getMachineInfoType()) {
flag = false;
break;
}
}
if (flag) {
throw exception(MACHINE_INFO_TYPE_NOT_EXISTS);
}
} else {
throw exception(MACHINE_INFO_TYPE_NULL);
}
}
@VisibleForTesting
void validateMachineTagUnique(String tag) {
if (machineInfoMapper.existsByTag(tag)) {
throw exception(MACHINE_INFO_TAG_EXISTS);
}
}
@Override
public MachineInfoDO validateMachineInfoExists(Long id) {
if (id == null) {
throw exception(MACHINE_INFO_NULL);
}
MachineInfoDO machineInfoDO = machineInfoMapper.selectById(id);
if (machineInfoDO == null) {
throw exception(MACHINE_INFO_NULL);
}
return machineInfoDO;
}
@Override
public Map<String, Object> fileTreeNode(Long machineId, String path) {
validateMachineInfoExists(machineId);
Session sshSession = WebSocketSessionManager.getWebSocketConnectionByMachineId(machineId).getSshSession();
return fileTreeComponent.getRemoteFileTree(sshSession, path);
}
@VisibleForTesting
void validateMachineEnable(MachineInfoDO machineInfoDO) {
if (machineInfoDO.getStatus() == MachineInfoStatus.ENABLE.getCode()) {
throw exception(MACHINE_ENABLE);
}
}
@VisibleForTesting
void validateMachineUnEnable(MachineInfoDO machineInfoDO) {
if (machineInfoDO.getStatus() == MachineInfoStatus.UN_ENABLE.getCode()) {
throw exception(MACHINE_UN_ENABLE);
}
}
@VisibleForTesting
WebSocketConnection createWebSocketConnection(MachineInfoDO machineInfoDO) {
if (WebSocketSessionManager.containsMachineId(machineInfoDO.getId())) {
return WebSocketSessionManager.getWebSocketConnectionByMachineId((machineInfoDO.getId()));
} else {
return new WebSocketConnection(this.secretKeyService);
}
}
}

View File

@ -0,0 +1,131 @@
package cd.casic.module.machine.service.impl;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.module.machine.controller.vo.MachineProxyVO;
import cd.casic.module.machine.dal.dataobject.MachineProxyDO;
import cd.casic.module.machine.enums.MachineProxyStatus;
import cd.casic.module.machine.enums.MachineProxyType;
import cd.casic.module.machine.dal.mysql.MachineProxyMapper;
import cd.casic.module.machine.service.MachineProxyService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Resource;
import cd.casic.framework.commons.util.object.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception;
import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*;
import static com.baomidou.mybatisplus.extension.toolkit.Db.save;
/**
* 机器代理服务实现类
*/
@Service("machineProxyService")
public class MachineProxyServiceImpl implements MachineProxyService {
@Resource
private MachineProxyMapper machineProxyMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createProxy(MachineProxyVO machineProxyVO) {
validateMachineProxyAdd(machineProxyVO);
// 创建代理记录
MachineProxyDO machineProxyDO = BeanUtils.toBean(machineProxyVO, MachineProxyDO.class);
save(machineProxyDO);
return machineProxyDO.getId();
}
@Override
public void updateProxy(MachineProxyVO machineProxyVO) {
// 参数校验
MachineProxyDO machineProxyDO = validateMachineProxyExists(machineProxyVO.getId());
// 更新状态
BeanUtils.copyProperties(machineProxyVO, machineProxyDO);
machineProxyMapper.updateById(machineProxyDO);
}
@Override
public void delete(Long id) {
MachineProxyDO machineProxyDO = validateMachineProxyExists(id);
validateMachineProxyOnline(machineProxyDO);
machineProxyMapper.deleteById(id);
}
@VisibleForTesting
void validateMachineProxyOnline(MachineProxyDO machineProxyDO) {
if (machineProxyDO.getStatus() == MachineProxyStatus.ONLINE.getCode()) {
throw exception(MACHINE_PROXY_IS_ONLINE);
}
}
@Override
public Map<Integer, Long> getAllProxyStatus() {
List<MachineProxyDO> proxyList = machineProxyMapper.selectList(new QueryWrapper<>());
if (CollectionUtils.isEmpty(proxyList)) {
return Collections.emptyMap();
}
return proxyList.stream()
.map(MachineProxyDO::getStatus)
.collect(Collectors.groupingBy(
Function.identity(),
// 统计每个分组的元素数量
Collectors.counting()
));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteProxyList(String ids) {
if (ids == null) {
return;
}
List<Long> machineProxyIds = Arrays.stream(ids.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(Long::parseLong)
.toList();
// 批量逻辑删除
machineProxyIds.forEach(this::delete);
}
@Override
public PageResult<MachineProxyDO> getProxyPage(MachineProxyVO machineProxyVO) {
return machineProxyMapper.selectPage(machineProxyVO);
}
@VisibleForTesting
MachineProxyDO validateMachineProxyExists(Long id) {
if (id == null) {
throw exception(MACHINE_PROXY_NOT_EXISTS);
}
MachineProxyDO machineProxyDO = machineProxyMapper.selectById(id);
if (machineProxyDO == null) {
throw exception(MACHINE_PROXY_NOT_EXISTS);
}
return machineProxyDO;
}
@VisibleForTesting
void validateMachineProxyAdd(MachineProxyVO machineProxyVO) {
if (machineProxyVO.getHostIp() == null) {
throw exception(MACHINE_PROXY_HOST_IP_NULL);
}
if (machineProxyVO.getUsername() == null) {
throw exception(MACHINE_PROXY_USER_NAME_NULL);
}
// 校验代理类型
int[] arrays = MachineProxyType.ARRAYS;
if (Arrays.stream(arrays).filter(i -> i == machineProxyVO.getProxyType()).findAny().isEmpty()) {
throw exception(MACHINE_PROXY_TYPE_NOT_EXISTS);
}
}
}

View File

@ -0,0 +1,226 @@
package cd.casic.module.machine.service.impl;
import cd.casic.framework.commons.pojo.PageResult;
import cd.casic.framework.commons.util.object.BeanUtils;
import cd.casic.module.machine.controller.vo.SecretKeyVO;
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
import cd.casic.module.machine.dal.dataobject.SecretKeyDO;
import cd.casic.module.machine.dal.mysql.MachineInfoMapper;
import cd.casic.module.machine.dal.mysql.SecretKeyMapper;
import cd.casic.module.machine.service.MachineInfoService;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import cd.casic.module.machine.utils.AliYunOssClient;
import cd.casic.module.machine.service.SecretKeyService;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StreamUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception;
import static cd.casic.module.machine.contants.MachineErrorCodeConstants.*;
/**
* 密钥服务实现类
*/
@Slf4j
@Service("secretKeyService")
public class SecretKeyServiceImpl implements SecretKeyService {
@Resource
private MachineInfoService machineInfoService;
@Resource
private AliYunOssClient aliYunOssClient;
@Resource
private SecretKeyMapper secretKeyMapper;
@Resource
private MachineInfoMapper machineInfoMapper;
@Override
public SecretKeyVO getSecretKey(Long id) {
SecretKeyDO secretKeyDO = validateSecretKeyExists(id);
return BeanUtils.toBean(secretKeyDO, SecretKeyVO.class);
}
@Override
public void deleteSecretKey(Long id) {
validateSecretKeyExists(id);
secretKeyMapper.deleteById(id);
}
@Override
public void unbindMachine(SecretKeyVO secretKeyVO) {
machineInfoService.bindingSecretKey(secretKeyVO.getMachineInfoIds(), null);
}
@Override
public ResponseEntity<InputStreamResource> downloadSecretFile(Long id) {
// 获取私钥内容
String keyContent = getKeyContent(id);
if (keyContent == null || keyContent.isEmpty()) {
throw exception(SECRET_KEY_NULL);
}
// 将字符串转换为输入流
ByteArrayInputStream inputStream = new ByteArrayInputStream(keyContent.getBytes(StandardCharsets.UTF_8));
// 设置响应头
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=private_key.pem");
headers.add(HttpHeaders.CONTENT_TYPE, "application/octet-stream");
headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(keyContent.getBytes(StandardCharsets.UTF_8).length));
// 返回带有文件流的响应实体
return ResponseEntity.ok()
.headers(headers)
.body(new InputStreamResource(inputStream));
}
@Override
public List<MachineInfoDO> getBindMachine(Long secretKeyId) {
return machineInfoMapper.selectBindMachineBySecretKey(secretKeyId);
}
@Override
public Long createSecretKey(SecretKeyVO secretKeyVO) {
validateSecretKeyAdd(secretKeyVO);
String localPath = secretKeyVO.getPath();
//将反斜杠替换为双反斜杠Windows路径转义
localPath = localPath.replace("\\", "\\\\");
String ossPath = upLoadSecretKey(localPath);
//检查得到的oss路径是否为空
validateSecretKeyPath(ossPath);
secretKeyVO.setPath(ossPath);
SecretKeyDO secretKeyDO = BeanUtils.toBean(secretKeyVO, SecretKeyDO.class);
secretKeyMapper.insert(secretKeyDO);
return secretKeyDO.getId();
}
@Override
public void updateSecretKey(SecretKeyVO secretKeyVO) {
SecretKeyDO secretKeyDO = validateSecretKeyExists(secretKeyVO.getId());
//路径改变==改变密钥
if (!secretKeyDO.getPath().equals(secretKeyVO.getPath())) {
String ossPath = upLoadSecretKey(secretKeyVO.getPath());
BeanUtils.copyProperties(secretKeyVO, secretKeyDO);
secretKeyDO.setPath(ossPath);
} else {
BeanUtils.copyProperties(secretKeyVO, secretKeyDO);
}
secretKeyMapper.updateById(secretKeyDO);
}
@Override
public void bindingMachine(SecretKeyVO secretKeyVO) {
validateSecretKeyExists(secretKeyVO.getId());
machineInfoService.bindingSecretKey(secretKeyVO.getMachineInfoIds(), secretKeyVO.getId());
}
@Override
@Transactional
public void deleteSecretKeyList(List<Long> ids) {
ids.forEach(
secretKeyId -> {
SecretKeyDO secretKeyDO = validateSecretKeyExists(secretKeyId);
if (secretKeyDO.getPath() != null && !secretKeyDO.getPath().isEmpty()) {
try {
//文件名
//删除子目录文件需要在前面加上根目录文件路径
String fileName = secretKeyDO.getPath().substring(secretKeyDO.getPath().lastIndexOf("/") + 1);
aliYunOssClient.delete(fileName);
} catch (Exception e) {
throw exception(DELETE_FILE_FAIL);
}
}
}
);
//绑定的机器全部设置为空
machineInfoService.bindingSecretKey(ids, null);
secretKeyMapper.deleteBatchIds(ids);
}
@Override
public PageResult<SecretKeyDO> getSecretKeypage(SecretKeyVO secretKeyVO) {
return secretKeyMapper.selectPage(secretKeyVO);
}
public String upLoadSecretKey(String localPath) {
//使用S3FileClient上传文件
aliYunOssClient.init();
//传输到指定文件需要在path前面加上文件路径
String path = IdUtil.fastSimpleUUID() + ".txt";
//上传文件是从本地上传这里传的是本地文件地址
byte[] content = ResourceUtil.readBytes(localPath);
String ossPath;
try {
ossPath = aliYunOssClient.upload(content, path, "txt");
} catch (Exception e) {
throw exception(UPLOADING_FILE_FAIL);
}
return ossPath;
}
@VisibleForTesting
void validateSecretKeyAdd(SecretKeyVO secretKeyVO) {
if (secretKeyVO == null) {
throw exception(SECRET_KEY_NULL);
}
if (secretKeyVO.getPath().isEmpty()) {
throw exception(SECRET_KEY_PATH_NULL);
}
}
@VisibleForTesting
void validateSecretKeyPath(String path) {
if (path.isEmpty()) {
throw exception(SECRET_KEY_PATH_NULL);
}
}
@VisibleForTesting
SecretKeyDO validateSecretKeyExists(Long id) {
if (id == null) {
throw exception(SECRET_KEY_NOT_EXISTS);
}
SecretKeyDO secretKeyDO = secretKeyMapper.selectById(id);
if (secretKeyDO == null) {
throw exception(SECRET_KEY_NOT_EXISTS);
}
return secretKeyDO;
}
@Override
public String getKeyContent(Long secretKeyId) {
if (secretKeyId == null) {
return null;
}
SecretKeyVO secretKey = getSecretKey(secretKeyId);
byte[] content;
try {
content = aliYunOssClient.getContent(secretKey.getPath().substring(secretKey.getPath().lastIndexOf("/") + 1));
} catch (Exception e) {
log.error("读取密钥文件失败", e);
throw exception(READ_SECRET_CONTENT_ERROR);
}
//改为S3FileClient读取
InputStream read = new ByteArrayInputStream(content);
try {
return StreamUtils.copyToString(read, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,11 @@
package cd.casic.module.machine.utils;
import cd.casic.module.infra.framework.file.core.client.s3.S3FileClient;
import cd.casic.module.infra.framework.file.core.client.s3.S3FileClientConfig;
public class AliYunOssClient extends S3FileClient {
public AliYunOssClient(Long id, S3FileClientConfig config) {
super(id, config);
}
}

View File

@ -0,0 +1,78 @@
-- ----------------------------
-- Table structure for machine_env
-- ----------------------------
DROP TABLE IF EXISTS `machine_env`;
CREATE TABLE `machine_env` (
`id` bigint NOT NULL AUTO_INCREMENT,
`create_date` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_date` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0),
`machine_id` bigint NULL DEFAULT NULL,
`env_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`env_value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`sensitive` tinyint(1) NULL DEFAULT NULL COMMENT '是否敏感0=false, 1=true, NULL=未设置)', -- 改为 tinyint 存储布尔值
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_machine_env_key` (`machine_id`, `env_key`) -- 同一机器下 env_key 唯一
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for machine_info
-- ----------------------------
DROP TABLE IF EXISTS `machine_info`;
CREATE TABLE `machine_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`create_date` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_date` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0),
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`tag` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`host_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`status_code` int NULL DEFAULT NULL COMMENT '状态编码(关联字典表)', -- 改为 int 类型
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`ssh_port` int NULL DEFAULT NULL COMMENT 'SSH端口号整数', -- 改为 int 类型
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`secret_key_id` bigint NULL DEFAULT NULL COMMENT '密钥ID逻辑关联无外键',
`machine_proxy_id` bigint NULL DEFAULT NULL COMMENT '代理ID逻辑关联无外键',
`authentication_type_code` int NULL DEFAULT NULL COMMENT '认证类型编码',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_secret_key_id` (`secret_key_id`), -- 保留索引优化查询
KEY `idx_machine_proxy_id` (`machine_proxy_id`)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for machine_proxy
-- ----------------------------
DROP TABLE IF EXISTS `machine_proxy`;
CREATE TABLE `machine_proxy` (
`id` bigint NOT NULL AUTO_INCREMENT,
`create_date` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_date` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0),
`host_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`ssh_port` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`proxy_type_code` tinyint(1) NULL DEFAULT NULL COMMENT '代理类型编码(关联字典表)',
`version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`status_code` tinyint(1) NULL DEFAULT NULL COMMENT '状态编码(关联字典表)', -- 改为 tinyint(1) 类型
`last_heartbeat_time` datetime(0) NULL DEFAULT NULL,
`config` json NULL,
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for machine_secret_key
-- ----------------------------
DROP TABLE IF EXISTS `machine_secret_key`;
CREATE TABLE `machine_secret_key` (
`id` bigint NOT NULL AUTO_INCREMENT,
`create_date` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_date` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0),
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;

View File

@ -0,0 +1,184 @@
//package com.casic.machine.service.impl;
//
//import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
//import com.casic.commons.exception.ServiceException;
//import com.casic.commons.utils.PageResult;
//import com.casic.machine.dto.MachineEnvDTO;
//import com.casic.machine.entity.MachineEnv;
//import com.casic.machine.mapper.MachineEnvMapper;
//import com.casic.machine.service.MachineEnvService;
//import org.junit.jupiter.api.BeforeEach;
//import org.junit.jupiter.api.Test;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.boot.test.context.SpringBootTest;
//import org.springframework.test.annotation.Rollback;
//import org.springframework.test.context.jdbc.Sql;
//import org.springframework.transaction.annotation.Transactional;
//
//import java.util.Date;
//import java.util.List;
//
//import static org.junit.jupiter.api.Assertions.*;
//
//@SpringBootTest
//@Transactional
//@Rollback(value = true) // 测试后回滚数据
//@Sql(scripts = {"classpath:sql/machine_env_test_data.sql"}) // 初始化测试数据可选
//public class MachineEnvServiceImplTest {
//
// @Autowired
// private MachineEnvService machineEnvService;
//
// @Autowired
// private MachineEnvMapper machineEnvMapper;
//
// private MachineEnvDTO validDto;
// private MachineEnvDTO invalidKeyDto;
// private Long existingMachineId;
// private Long nonExistingMachineId;
//
// @BeforeEach
// public void setUp() {
// // 准备测试数据
// existingMachineId = 1L; // 假设数据库中存在ID为1的机器环境变量
// nonExistingMachineId = 999L; // 不存在的机器ID
//
// // 有效测试数据
// validDto = new MachineEnvDTO();
// validDto.setMachineInfoId(existingMachineId);
// validDto.setEnvKey("TEST_ENV_KEY");
// validDto.setEnvValue("test-value");
// validDto.setSensitive(true);
//
// // 无效Key测试数据包含非法字符
// invalidKeyDto = new MachineEnvDTO();
// invalidKeyDto.setMachineInfoId(existingMachineId);
// invalidKeyDto.setEnvKey("test-env-key"); // 包含'-'不符合正则
// invalidKeyDto.setEnvValue("test-value");
// }
//
// // ==================== 更新环境变量测试 ====================
// @Test
// void testUpdateEnv_ValidData_ShouldSucceed() {
// // 执行更新假设数据库中已存在machineId=1的记录
// boolean result = machineEnvService.update(validDto);
//
// // 验证结果
// assertTrue(result);
//
// // 检查数据库数据是否更新
// MachineEnv updatedEnv = machineEnvMapper.selectOne(
// new LambdaQueryWrapper<MachineEnv>().eq(MachineEnv::getMachineId, existingMachineId)
// );
// assertNotNull(updatedEnv);
// assertEquals(validDto.getEnvKey(), updatedEnv.getEnvKey());
// assertEquals(validDto.getEnvValue(), updatedEnv.getEnvValue());
// assertEquals(validDto.getSensitive(), updatedEnv.getSensitive());
// }
//
// @Test
// void testUpdateEnv_NullDto_ShouldThrowException() {
//
// MachineEnvDTO machineEnvDTO = new MachineEnvDTO();
// assertThrows(ServiceException.class, () -> {
// machineEnvService.update(machineEnvDTO);
// }, "环境变量不能为空");
// }
//
// @Test
// void testUpdateEnv_InvalidKey_ShouldThrowException() {
// assertThrows(ServiceException.class, () -> {
// machineEnvService.update(invalidKeyDto);
// }, "环境变量键不合法");
// }
//
// @Test
// void testUpdateEnv_SensitiveKey_ShouldMarkAsSensitive() {
// MachineEnvDTO sensitiveDto = new MachineEnvDTO();
// sensitiveDto.setMachineInfoId(existingMachineId);
// sensitiveDto.setEnvKey("DB_PASSWORD"); // 包含敏感词
// sensitiveDto.setEnvValue("secret");
//
// machineEnvService.update(sensitiveDto);
//
// MachineEnv env = machineEnvMapper.selectOne(
// new LambdaQueryWrapper<MachineEnv>().eq(MachineEnv::getMachineId, existingMachineId)
// );
// assertTrue(env.getSensitive());
// }
//
// // ==================== 删除环境变量测试 ====================
// @Test
// void testDeleteByMachineId_ExistingId_ShouldSucceed() {
// machineEnvService.deleteByMachineId(existingMachineId);
// MachineEnv env = machineEnvMapper.selectById(existingMachineId);
// assertNull(env);
// }
//
// @Test
// void testDeleteByMachineId_NonExistingId_ShouldDoNothing() {
// machineEnvService.deleteByMachineId(nonExistingMachineId);
// // 不抛出异常静默处理
// }
//
// // ==================== 根据机器ID查询测试 ====================
// @Test
// void testGetByMachineId_ExistingId_ShouldReturnDto() {
// MachineEnvDTO dto = machineEnvService.getByMachineId(existingMachineId);
// assertNotNull(dto);
// assertEquals(existingMachineId, dto.getMachineInfoId());
// }
//
// @Test
// void testGetByMachineId_NonExistingId_ShouldReturnNull() {
// MachineEnvDTO dto = machineEnvService.getByMachineId(nonExistingMachineId);
// assertNull(dto);
// }
//
// // ==================== 列表查询测试 ====================
// @Test
// void testListEnv_Pagination_ShouldReturnValidPage() {
// MachineEnvDTO queryDto = new MachineEnvDTO();
// queryDto.setPageIndex(1);
// queryDto.setPageSize(10);
// queryDto.setEnvKey("TEST"); // 假设测试数据中存在包含"TEST"的键
//
// PageResult<MachineEnvDTO> pageResult = machineEnvService.listEnv(queryDto);
//
// assertNotNull(pageResult.getList());
// assertTrue(pageResult.getTotal() >= 0);
// assertEquals(1, pageResult.getPageNum());
// }
//
// @Test
// void testListEnv_SortByCreateTime_ShouldBeOrdered() {
// MachineEnvDTO queryDto = new MachineEnvDTO();
// queryDto.setSortField("createTime");
// queryDto.setSortDirection("desc");
//
// PageResult<MachineEnvDTO> pageResult = machineEnvService.listEnv(queryDto);
//
// List<MachineEnvDTO> list = pageResult.getList();
// if (!list.isEmpty()) {
// Date prevDate = list.get(0).getCreateDate();
// for (int i = 1; i < list.size(); i++) {
// Date currDate = list.get(i).getCreateDate();
// assertTrue(currDate.before(prevDate) || currDate.equals(prevDate), "排序应为降序");
// prevDate = currDate;
// }
// }
// }
//
// // ==================== 批量删除测试 ====================
// @Test
// void testDeleteList_ValidIds_ShouldDeleteBatch() {
// // 假设测试数据中有ID为1和2的记录
// List<Long> ids = List.of(1L, 2L);
// machineEnvService.deleteList(ids);
//
// long count = machineEnvMapper.selectCount(new LambdaQueryWrapper<MachineEnv>().in(MachineEnv::getId, ids));
// assertEquals(0, count);
// }
//
//
//}

View File

@ -0,0 +1,222 @@
//package com.casic.machine.service.impl;
//
//import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
//import com.casic.commons.exception.ServiceException;
//import com.casic.commons.utils.EnumUtils;
//import com.casic.commons.utils.PageResult;
//import com.casic.machine.dto.MachineProxyDTO;
//import com.casic.machine.entity.MachineProxy;
//import com.casic.machine.enums.MachineProxyStatus;
//import com.casic.machine.enums.MachineProxyType;
//import com.casic.machine.mapper.MachineProxyMapper;
//import com.casic.machine.service.MachineProxyService;
//import org.junit.jupiter.api.BeforeEach;
//import org.junit.jupiter.api.Test;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.boot.test.context.SpringBootTest;
//import org.springframework.test.annotation.Rollback;
//import org.springframework.test.context.jdbc.Sql;
//import org.springframework.transaction.annotation.Transactional;
//
//import java.util.*;
//
//import static org.junit.jupiter.api.Assertions.*;
//
//@SpringBootTest
//@Transactional
//@Rollback(true)
//@Sql(scripts = {"classpath:sql/machine_proxy_test_data.sql"})
//public class MachineProxyServiceImplTest {
//
// @Autowired
// private MachineProxyService machineProxyService;
//
// @Autowired
// private MachineProxyMapper machineProxyMapper;
//
// private MachineProxyDTO validProxyDTO;
// private Long existingProxyId;
// private Long nonExistingProxyId;
//
// @BeforeEach
// public void setUp() {
// // 初始化测试数据假设 SQL 脚本已插入一条状态为 OFFLINE 的代理
// existingProxyId = 1L;
// nonExistingProxyId = 999L;
//
// // 有效代理 DTO
// validProxyDTO = new MachineProxyDTO();
// validProxyDTO.setProxyType(MachineProxyType.SOCKS5.getMessage());
// validProxyDTO.setHostIp("192.168.1.100");
// validProxyDTO.setSshPort("22");
// validProxyDTO.setUsername("test_user");
// validProxyDTO.setStatus(MachineProxyStatus.ONLINE.getMessage());
// }
//
// // ============================== 注册代理测试 ==============================
// @Test
// void testRegister_ValidData_ShouldSucceed() {
// // 执行注册
// boolean result = machineProxyService.register(validProxyDTO);
// assertTrue(result, "注册失败");
//
// // 使用 Lambda 表达式查询推荐
// MachineProxy proxy = machineProxyMapper.selectOne(
// new LambdaQueryWrapper<MachineProxy>()
// .eq(MachineProxy::getHostIp, validProxyDTO.getHostIp())
// );
//
// // 断言数据存在
// assertNotNull(proxy, "代理记录未写入数据库");
// assertEquals(MachineProxyType.SOCKS5.getCode(), proxy.getProxyTypeCode());
// assertEquals(MachineProxyStatus.INSTALLING.getCode(), proxy.getStatusCode());
// assertEquals(validProxyDTO.getHostIp(), proxy.getHostIp(), "IP 地址不一致");
// }
//
// // ============================== 更新状态测试 ==============================
// @Test
// void testUpdateStatus_ExistingProxy_ShouldUpdateStatus() {
// // 准备数据查询现有代理状态为 OFFLINE
// MachineProxy proxy = machineProxyMapper.selectById(existingProxyId);
// assertEquals(MachineProxyStatus.OFFLINE.getCode(), proxy.getStatusCode());
//
// // 执行状态更新为 ONLINE
// validProxyDTO.setId(existingProxyId);
// validProxyDTO.setStatus(MachineProxyStatus.ONLINE.getMessage());
// boolean result = machineProxyService.updateStatus(validProxyDTO);
//
// // 验证结果
// assertTrue(result);
// proxy = machineProxyMapper.selectById(existingProxyId);
// assertEquals(MachineProxyStatus.ONLINE.getCode(), proxy.getStatusCode());
// }
//
// @Test
// void testUpdateStatus_NullDto_ShouldThrowException() {
// assertThrows(ServiceException.class, () -> {
// machineProxyService.updateStatus(null);
// }, "MachineProxyDTO对象为空");
// }
//
// // ============================== 心跳测试 ==============================
// @Test
// void testHeartbeat_ValidData_ShouldUpdateVersionAndStatus() {
// // 准备数据现有代理状态为 OFFLINE版本为 1.0.0
// MachineProxy proxy = machineProxyMapper.selectById(existingProxyId);
// assertEquals("1.0.0", proxy.getVersion());
// assertEquals(MachineProxyStatus.OFFLINE.getCode(), proxy.getStatusCode());
//
// // 发送心跳更新版本和状态
// validProxyDTO.setId(existingProxyId);
// validProxyDTO.setVersion("2.0.0");
// validProxyDTO.setStatus(MachineProxyStatus.ONLINE.getMessage());
// machineProxyService.heartbeat(validProxyDTO);
//
// // 验证结果
// proxy = machineProxyMapper.selectById(existingProxyId);
// assertEquals("2.0.0", proxy.getVersion());
// assertEquals(MachineProxyStatus.ONLINE.getCode(), proxy.getStatusCode());
// }
//
// // ============================== 状态统计测试 ==============================
// @Test
// void testGetStatusStatistics_ShouldReturnValidCounts() {
// // 假设测试数据中有 OFFLINE(1)INSTALLING(1)ONLINE(1) 三种状态
// Map<String, Long> stats = machineProxyService.getStatusStatistics();
//
// // 验证统计结果
// assertEquals(3, stats.size());
// assertTrue(stats.containsKey(MachineProxyStatus.OFFLINE.getMessage()));
// assertTrue(stats.containsKey(MachineProxyStatus.INSTALLING.getMessage()));
// assertTrue(stats.containsKey(MachineProxyStatus.ONLINE.getMessage()));
// assertEquals(1L, stats.get(MachineProxyStatus.OFFLINE.getMessage()));
// }
//
// // ============================== 更新配置测试 ==============================
// @Test
// void testUpdateConfig_ValidConfig_ShouldSucceed() {
// // 准备数据现有代理配置为空
// MachineProxy proxy = machineProxyMapper.selectById(existingProxyId);
// assertNull(proxy.getConfig());
//
// // 更新配置
// validProxyDTO.setId(existingProxyId);
// validProxyDTO.setConfig("{\"port\": 8080}");
// boolean result = machineProxyService.updateConfig(validProxyDTO);
//
// // 验证结果
// assertTrue(result);
// proxy = machineProxyMapper.selectById(existingProxyId);
// assertEquals("{\"port\": 8080}", proxy.getConfig());
// }
//
// // ============================== 删除代理测试 ==============================
// @Test
// void testDelete_OfflineProxy_ShouldDeleteSuccessfully() {
// // 准备数据状态为 OFFLINE 的代理 ID
// List<Long> ids = Collections.singletonList(existingProxyId);
// machineProxyService.delete(ids);
//
// // 验证删除
// MachineProxy proxy = machineProxyMapper.selectById(existingProxyId);
// assertNull(proxy);
// }
//
// @Test
// void testDelete_OnlineProxy_ShouldThrowException() {
// // 先将代理状态改为 ONLINE
// MachineProxy onlineProxy = new MachineProxy();
// onlineProxy.setId(existingProxyId);
// onlineProxy.setStatusCode(MachineProxyStatus.ONLINE.getCode());
// machineProxyMapper.updateById(onlineProxy);
//
// // 执行删除
// List<Long> ids = Collections.singletonList(existingProxyId);
// assertThrows(IllegalArgumentException.class, () -> {
// machineProxyService.delete(ids);
// }, "以下代理处于在线状态,无法删除: 1");
// }
//
// // ============================== 列表查询测试 ==============================
// @Test
// void testList_WithStatusFilter_ShouldReturnMatchedRecords() {
// // 查询状态为 OFFLINE 的代理
// MachineProxyDTO queryDto = new MachineProxyDTO();
// queryDto.setStatus(MachineProxyStatus.OFFLINE.getMessage());
//
// PageResult<MachineProxyDTO> pageResult = machineProxyService.list(queryDto);
//
// // 验证结果
// assertFalse(pageResult.getList().isEmpty());
// pageResult.getList().forEach(dto ->
// assertEquals(MachineProxyStatus.OFFLINE.getMessage(), dto.getStatus())
// );
// }
//
// @Test
// void testList_WithHostIpFilter_ShouldReturnMatchedRecords() {
// // 假设测试数据中存在 host_ip "192.168.1.1" 的代理
// MachineProxyDTO queryDto = new MachineProxyDTO();
// queryDto.setHostIp("192.168.1.1");
//
// PageResult<MachineProxyDTO> pageResult = machineProxyService.list(queryDto);
//
// // 验证结果
// assertFalse(pageResult.getList().isEmpty());
// pageResult.getList().forEach(dto ->
// assertEquals("192.168.1.1", dto.getHostIp())
// );
// }
//
// // ============================== 辅助方法测试 ==============================
// @Test
// void testEnumUtils_ConvertCodeToMessage() {
// // 验证代理类型枚举转换
// String typeMessage = EnumUtils.getEnumByCode(MachineProxyType.HTTP.getCode(), MachineProxyType.class).getMessage();
// assertEquals("HTTP", typeMessage);
//
// // 验证状态枚举转换
// String statusMessage = EnumUtils.getEnumByCode(MachineProxyStatus.INSTALLING.getCode(), MachineProxyStatus.class).getMessage();
// assertEquals("安装中", statusMessage);
// }
//}

View File

@ -0,0 +1,176 @@
//package com.casic.machine.service.impl;
//
//import com.casic.commons.exception.ServiceException;
//import com.casic.machine.dto.MachineInfoDto;
//import com.casic.machine.entity.MachineInfo;
//import com.casic.machine.enums.AuthenticationType;
//import com.casic.machine.enums.ConnectionStatus;
//import com.casic.machine.enums.MachineInfoStatus;
//import org.junit.jupiter.api.BeforeEach;
//import org.junit.jupiter.api.Test;
//import org.springframework.beans.BeanUtils;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.boot.test.context.SpringBootTest;
//import org.springframework.test.annotation.Rollback;
//import org.springframework.test.context.jdbc.Sql;
//import org.springframework.transaction.annotation.Transactional;
//
//import java.util.Collections;
//
//import static org.junit.jupiter.api.Assertions.*;
//
//@SpringBootTest
//@Transactional
//@Rollback(true)
//@Sql(scripts = {"classpath:sql/machine_info_test_data.sql"})
//public class MachineinfoServiceImplTest {
//
// @Autowired
// private MachineinfoServiceImpl machineInfoService;
//
// private MachineInfoDto validMachineInfoDto;
// private Long existingMachineId;
// private Long nonExistingMachineId;
//
// @BeforeEach
// public void setUp() {
// // 初始化测试数据假设 SQL 脚本已插入一条状态为 ENABLE 的机器
// existingMachineId = 1L;
// nonExistingMachineId = 999L;
//
// // 有效机器信息 DTO
// validMachineInfoDto = new MachineInfoDto();
// validMachineInfoDto.setName("Test Machine");
// validMachineInfoDto.setHostIp("192.168.1.101");
// validMachineInfoDto.setSshPort("22");
// validMachineInfoDto.setUsername("testuser");
// validMachineInfoDto.setAuthenticationType(AuthenticationType.PASSWORD.getMessage());
// validMachineInfoDto.setStatus(MachineInfoStatus.ENABLE.getMessage());
// }
//
// // ======================= 新增机器测试 =======================
// @Test
// void testAddMachineInfo_ValidData_ShouldSucceed() {
// boolean result = machineInfoService.addMachineInfo(validMachineInfoDto);
// assertTrue(result);
//
// // 验证数据库存在记录
// MachineInfo machineInfo = machineInfoService.getById(validMachineInfoDto.getId());
// assertNotNull(machineInfo);
// assertEquals(validMachineInfoDto.getHostIp(), machineInfo.getHostIp());
// assertEquals(AuthenticationType.PASSWORD.getCode(), machineInfo.getAuthenticationTypeCode());
// }
//
// @Test
// void testAddMachineInfo_NullDto_ShouldThrowException() {
// assertThrows(ServiceException.class, () -> {
// machineInfoService.addMachineInfo(null);
// }, "机器信息为空");
// }
//
// // ======================= 列表查询测试 =======================
// @Test
// void testListMachineInfo_WithStatusFilter_ShouldReturnValidRecords() {
// MachineInfoDto queryDto = new MachineInfoDto();
// queryDto.setStatus(MachineInfoStatus.ENABLE.getMessage());
//
// var pageResult = machineInfoService.listMachineInfo(queryDto);
// assertFalse(pageResult.getList().isEmpty());
// pageResult.getList().forEach(dto ->
// assertEquals(MachineInfoStatus.ENABLE.getMessage(), dto.getStatus())
// );
// }
//
// @Test
// void testListMachineInfo_WithHostIpFilter_ShouldFilterRecords() {
// MachineInfoDto queryDto = new MachineInfoDto();
// queryDto.setHostIp("192.168.1.100"); // 假设测试数据中的 IP
//
// var pageResult = machineInfoService.listMachineInfo(queryDto);
// assertFalse(pageResult.getList().isEmpty());
// pageResult.getList().forEach(dto ->
// assertTrue(dto.getHostIp().contains("192.168.1.100"))
// );
// }
//
// // ======================= 更新机器状态测试 =======================
// @Test
// void testUpdateStatus_ValidId_ShouldUpdateStatus() {
// boolean result = machineInfoService.updateStatus(existingMachineId, MachineInfoStatus.UN_ENABLE.getMessage());
// assertTrue(result);
//
// MachineInfo machineInfo = machineInfoService.getById(existingMachineId);
// assertEquals(MachineInfoStatus.UN_ENABLE.getCode(), machineInfo.getStatus());
// }
//
// @Test
// void testUpdateStatus_NonExistingId_ShouldFail() {
// boolean result = machineInfoService.updateStatus(nonExistingMachineId, MachineInfoStatus.UN_ENABLE.getMessage());
// assertFalse(result);
// }
//
// // ======================= 连接测试 =======================
// @Test
// void testTestConnection_EnabledMachine_ShouldSucceed() {
// MachineInfo machineInfo = machineInfoService.getById(existingMachineId);
// assertTrue(machineInfoService.testConnection(machineInfo));
// }
//
// @Test
// void testTestConnection_DisabledMachine_ShouldThrowException() {
// // 先禁用机器
// machineInfoService.updateStatus(existingMachineId, MachineInfoStatus.UN_ENABLE.getMessage());
// MachineInfo disabledMachine = machineInfoService.getById(existingMachineId);
//
// assertThrows(RuntimeException.class, () -> {
// machineInfoService.testConnection(disabledMachine);
// }, "机器不可用");
// }
//
// // ======================= 会话管理测试 =======================
// @Test
// void testConnect_NewMachine_ShouldCreateSession() {
// MachineInfo machineInfo = machineInfoService.getById(existingMachineId);
// String sessionId = machineInfoService.connect(machineInfo);
//
// assertNotNull(sessionId);
// assertTrue(sessionId.startsWith("session-"));
// assertEquals(ConnectionStatus.CONNECTING, machineInfoService.getConnectionStatus(machineInfo.getName()));
// }
//
//
//
// // ======================= 删除机器测试 =======================
// @Test
// void testDeleteMachineInfo_EnabledMachine_ShouldDelete() {
// machineInfoService.deleteMachineInfo(existingMachineId);
// assertNull(machineInfoService.getById(existingMachineId));
// }
//
// @Test
// void testDeleteList_ValidIds_ShouldDeleteBatch() {
// machineInfoService.deleteList(Collections.singletonList(existingMachineId));
// var list = machineInfoService.list();
// assertFalse(list.contains(existingMachineId));
// }
//
// // ======================= 辅助功能测试 =======================
// @Test
// void testAuthenticationTypeConversion() {
// validMachineInfoDto.setAuthenticationType(AuthenticationType.SECRET_KEY.getMessage());
// MachineInfo machineInfo = new MachineInfo();
// BeanUtils.copyProperties(validMachineInfoDto, machineInfo);
//
// assertEquals(AuthenticationType.SECRET_KEY.getCode(), machineInfo.getAuthenticationTypeCode());
// }
//
// @Test
// void testGetAllConnectionStatus_ShouldReturnStatusMap() {
// MachineInfo machineInfo = machineInfoService.getById(existingMachineId);
// machineInfoService.connect(machineInfo);
//
// var statusMap = machineInfoService.getAllConnectionStatus();
// assertFalse(statusMap.isEmpty());
// assertTrue(statusMap.containsValue(ConnectionStatus.CONNECTING));
// }
//}

View File

@ -0,0 +1,218 @@
//package com.casic.machine.service.impl;
//
//import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
//import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
//import com.casic.commons.exception.ServiceException;
//import com.casic.commons.utils.AliOssUtil;
//import com.casic.machine.entity.MachineInfo;
//import com.casic.machine.entity.SecretKey;
//import com.casic.machine.dto.SecretKeyDto;
//import com.casic.machine.mapper.SecretServiceMapper;
//import com.casic.machine.service.MachineInfoService;
//import com.jayway.jsonpath.internal.Utils;
//import org.junit.jupiter.api.BeforeEach;
//import org.junit.jupiter.api.Test;
//import org.mockito.InjectMocks;
//import org.mockito.Mock;
//import org.mockito.MockitoAnnotations;
//import org.springframework.beans.BeanUtils;
//import org.springframework.core.io.InputStreamResource;
//import org.springframework.http.ResponseEntity;
//import org.springframework.mock.web.MockMultipartFile;
//import org.springframework.test.util.ReflectionTestUtils;
//
//import java.io.ByteArrayInputStream;
//import java.io.IOException;
//import java.util.Collections;
//import java.util.List;
//
//import static org.junit.jupiter.api.Assertions.*;
//import static org.mockito.ArgumentMatchers.any;
//import static org.mockito.Mockito.*;
//
//@SuppressWarnings("unchecked")
//public class SecretKeyServiceImplTest {
//
// @InjectMocks
// private SecretKeyServiceImpl secretKeyService;
//
// @Mock
// private AliOssUtil aliOssUtil;
//
// @Mock
// private SecretServiceMapper secretServiceMapper;
//
// @Mock
// private MachineInfoService machineInfoService;
//
// private SecretKeyDto validSecretKeyDto;
// private MockMultipartFile mockFile;
// private final Long TEST_SECRET_KEY_ID = 1L;
// private final String TEST_FILE_NAME = "test_key.pem";
// private final String TEST_PATH = "https://bucket.endpoint/test_key.pem";
//
// @BeforeEach
// public void setUp() throws IOException {
// MockitoAnnotations.openMocks(this);
// validSecretKeyDto = new SecretKeyDto();
// validSecretKeyDto.setName("Test Key");
// validSecretKeyDto.setDescription("Test secret key");
//
// // 创建模拟文件
// byte[] fileContent = "test content".getBytes();
// mockFile = new MockMultipartFile(
// "file",
// TEST_FILE_NAME,
// "application/octet-stream",
// new ByteArrayInputStream(fileContent)
// );
//
// // 模拟 OSS 工具返回值
// when(aliOssUtil.save(any(MockMultipartFile.class))).thenReturn(TEST_FILE_NAME);
//
// }
//
// // ======================= 新增密钥测试 =======================
// @Test
// void testAddSecretKey_ValidData_ShouldSucceed() throws IOException {
// // 执行新增
// boolean result = secretKeyService.addSecretKey(validSecretKeyDto, mockFile);
//
// // 验证 OSS 保存调用
// verify(aliOssUtil, times(1)).save(mockFile);
//
// // 验证实体属性
// SecretKey savedKey = new SecretKey();
// BeanUtils.copyProperties(validSecretKeyDto, savedKey);
// savedKey.setFileName(TEST_FILE_NAME);
// savedKey.setPath(TEST_PATH);
//
// // 验证 Mapper 调用
// verify(secretServiceMapper, times(1)).insert(savedKey);
// assertTrue(result);
// }
//
// @Test
// void testAddSecretKey_NullFile_ShouldThrowException() {
// assertThrows(ServiceException.class, () -> {
// secretKeyService.addSecretKey(validSecretKeyDto, null);
// }, "文件为空");
// }
//
// // ======================= 绑定机器测试 =======================
// @Test
// void testBindingMachine_ValidIds_ShouldUpdateMachine() {
// // 模拟机器列表
// List<Long> machineIds = Collections.singletonList(1L);
// when(machineInfoService.listByIds(machineIds)).thenReturn(Collections.singletonList(new MachineInfo()));
//
// secretKeyService.bindingMachine(TEST_SECRET_KEY_ID, machineIds);
//
// // 验证机器信息更新
// verify(machineInfoService, times(1)).listByIds(machineIds);
// machineIds.forEach(id -> {
// verify(machineInfoService, times(1)).update(any());
// });
// }
//
// // ======================= 更新密钥测试 =======================
// @Test
// void testUpdateSecretKey_WithNewFile_ShouldUpdatePath() throws IOException {
// MockMultipartFile newFile = new MockMultipartFile(
// "file",
// "new_key.pem",
// "application/octet-stream",
// new ByteArrayInputStream("new content".getBytes())
// );
// when(aliOssUtil.save(newFile)).thenReturn("new_key.pem");
//
// validSecretKeyDto.setId(TEST_SECRET_KEY_ID);
// boolean result = secretKeyService.updateSecretKey(validSecretKeyDto, newFile);
//
// // 验证 OSS 调用和路径更新
// verify(aliOssUtil, times(1)).save(newFile);
// SecretKey updatedKey = new SecretKey();
// BeanUtils.copyProperties(validSecretKeyDto, updatedKey);
// updatedKey.setFileName("new_key.pem");
// updatedKey.setPath("https://bucket.endpoint/new_key.pem");
// verify(secretServiceMapper, times(1)).updateById(updatedKey);
// assertTrue(result);
// }
//
// // ======================= 删除密钥测试 =======================
// @Test
// void testDeleteSecretKey_ValidId_ShouldDeleteFileAndRecord() {
// SecretKey secretKey = new SecretKey();
// secretKey.setId(TEST_SECRET_KEY_ID);
// secretKey.setFileName(TEST_FILE_NAME);
// when(secretServiceMapper.selectById(TEST_SECRET_KEY_ID)).thenReturn(secretKey);
//
// boolean result = secretKeyService.deleteSecretKey(TEST_SECRET_KEY_ID);
//
// // 验证 OSS 删除和 Mapper 调用
// verify(aliOssUtil, times(1)).deleteFile(TEST_FILE_NAME);
// verify(secretServiceMapper, times(1)).deleteById(TEST_SECRET_KEY_ID);
// assertTrue(result);
// }
//
// // ======================= 列表查询测试 =======================
// @Test
// void testListSecretKey_WithNameFilter_ShouldReturnMatchedRecords() {
// SecretKeyDto queryDto = new SecretKeyDto();
// queryDto.setName("Test");
// QueryWrapper<SecretKey> wrapper = new QueryWrapper<>();
// wrapper.like("name", "Test");
//
// when(secretServiceMapper.selectPage(any(), any())).thenReturn(new Page<>());
//
// secretKeyService.listSecretKey(queryDto);
// verify(secretServiceMapper, times(1)).selectPage(any(), wrapper);
// }
//
// // ======================= 文件下载测试 =======================
// @Test
// void testDownloadSecretKeyFile_ValidId_ShouldReturnResponseEntity() throws IOException {
// SecretKey secretKey = new SecretKey();
// secretKey.setFileName(TEST_FILE_NAME);
// when(secretServiceMapper.selectById(TEST_SECRET_KEY_ID)).thenReturn(secretKey);
// InputStreamResource inputStreamResource = new InputStreamResource(
// new ByteArrayInputStream("test content".getBytes())
// );
// when(aliOssUtil.downloadFile(TEST_FILE_NAME)).thenReturn(
// ResponseEntity.ok(inputStreamResource)
// );
//
// ResponseEntity<InputStreamResource> response = secretKeyService.downloadSecretKeyFile(TEST_SECRET_KEY_ID);
// assertNotNull(response);
// assertEquals("test content", Utils.toString(response.getBody().getInputStream()));
// }
//
// // ======================= 批量删除测试 =======================
// @Test
// void testDeleteList_ValidIds_ShouldSubmitAsyncDelete() {
// List<Long> ids = Collections.singletonList(TEST_SECRET_KEY_ID);
// SecretKey secretKey = new SecretKey();
// secretKey.setFileName(TEST_FILE_NAME);
// when(secretServiceMapper.selectBatchIds(ids)).thenReturn(Collections.singletonList(secretKey));
//
// secretKeyService.deleteList(ids);
//
// // 验证异步任务提交
// verify(secretKeyService.FILE_DELETE_EXECUTOR, times(1)).execute(any(Runnable.class));
// verify(secretServiceMapper, times(1)).deleteBatchIds(ids);
// }
//
// // ======================= 异常处理测试 =======================
// @Test
// void testDeleteSecretKey_FileDeleteFailed_ShouldThrowException() {
// aliOssUtil.deleteFile(TEST_FILE_NAME);
// SecretKey secretKey = new SecretKey();
// secretKey.setId(TEST_SECRET_KEY_ID);
// secretKey.setFileName(TEST_FILE_NAME);
// when(secretServiceMapper.selectById(TEST_SECRET_KEY_ID)).thenReturn(secretKey);
//
// assertThrows(ServiceException.class, () -> {
// secretKeyService.deleteSecretKey(TEST_SECRET_KEY_ID);
// }, "删除文件失败");
// }
//}

View File

@ -14,6 +14,7 @@
<module>module-infra-biz</module>
<module>module-system-api</module>
<module>module-system-biz</module>
<module>module-ci-machine</module>
</modules>
<artifactId>modules</artifactId>

View File

@ -25,6 +25,11 @@
<artifactId>module-system-biz</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cd.casic.boot</groupId>
<artifactId>module-ci-machine</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cd.casic.boot</groupId>

View File

@ -156,3 +156,12 @@ logging:
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # 禁用Spring Boot 3.X 存在部分错误的 WARN 提示
debug: false
#阿里云oss
aliyun:
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
accessKeyId: LTAI5tPCKES4ZxdRKybGjJf4
accessKeySecret: mtX94qmxnWR2FDem0z38qjv0rrILSE
bucketName: kkt-web-tlias