# 模块03:连接管理功能 --- ## 🎨 UI设计系统概览 > **完整设计系统文档请参考:** `UI设计系统.md` ### 核心设计原则 - **现代简约**:界面清晰,层次分明 - **专业高效**:减少操作步骤,提升工作效率 - **一致性**:统一的视觉语言和交互模式 - **可访问性**:符合WCAG 2.1 AA标准 ### 关键设计令牌 **颜色系统:** - 主色:`#0d6efd`(操作按钮、选中状态) - 成功:`#198754`(连接成功状态) - 危险:`#dc3545`(删除操作、错误提示) - 深灰:`#212529`(导航栏背景) - 浅灰:`#e9ecef`(工具栏背景) **字体系统:** - 字体族:系统字体栈(-apple-system, Segoe UI, Roboto等) - 正文:14px,行高1.5 - 标题:20-32px,行高1.2-1.4 - 小号文字:12px(文件大小、日期等) **间距系统:** - 基础单位:8px - 标准间距:16px(1rem) - 组件内边距:8px-16px **组件规范:** - 导航栏:高度48px,深色背景 - 工具栏:浅灰背景,按钮间距8px - 文件项:最小高度44px,悬停效果150ms - 按钮:圆角4px,过渡150ms **交互规范:** - 悬停效果:150ms过渡 - 触摸目标:最小44x44px - 键盘导航:Tab、Enter、Delete、F2、F5、Esc - 焦点状态:2px蓝色轮廓 **响应式断点:** - 移动端:< 768px(双面板垂直排列) - 平板:768px - 1024px - 桌面:> 1024px(标准布局) --- ## 3.1 功能概述 实现SFTP连接的建立、断开、保存、加载和删除功能,支持多连接同时管理。 ## 3.2 后端设计 ### 3.2.1 ConnectionRepository接口 ```java package com.sftp.manager.repository; import com.sftp.manager.model.Connection; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; import java.util.Optional; public interface ConnectionRepository extends JpaRepository { List findByOrderByCreatedAtDesc(); // 按创建时间倒序查询 Optional findByName(String name); // 按名称查询 } ``` ### 3.2.2 SessionManager会话管理 ```java package com.sftp.manager.service; import com.jcraft.jsch.ChannelSftp; import org.springframework.stereotype.Component; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @Component public class SessionManager { private final Map activeSessions = new ConcurrentHashMap<>(); private final Map sessionConnections = new ConcurrentHashMap<>(); public String addSession(ChannelSftp channel, Connection connection) { String sessionId = "sftp-" + UUID.randomUUID().toString(); activeSessions.put(sessionId, channel); sessionConnections.put(sessionId, connection); return sessionId; } public ChannelSftp getSession(String sessionId) { return activeSessions.get(sessionId); } public Connection getConnection(String sessionId) { return sessionConnections.get(sessionId); } public void removeSession(String sessionId) { ChannelSftp channel = activeSessions.get(sessionId); if (channel != null) { try { channel.disconnect(); } catch (Exception e) { // 忽略关闭异常 } } activeSessions.remove(sessionId); sessionConnections.remove(sessionId); } public boolean isActive(String sessionId) { return activeSessions.containsKey(sessionId); } public Map getAllActiveConnections() { return new ConcurrentHashMap<>(sessionConnections); } public int getActiveSessionCount() { return activeSessions.size(); } } ``` ### 3.2.3 ConnectionService连接服务 ```java package com.sftp.manager.service; import com.jcraft.jsch.Channel; import com.jcraft.jsch.JSch; import com.jcraft.jsch.Session; import com.sftp.manager.dto.ConnectionRequest; import com.sftp.manager.model.Connection; import com.sftp.manager.repository.ConnectionRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.List; import java.util.UUID; @Service public class ConnectionService { @Autowired private ConnectionRepository connectionRepository; @Autowired private SessionManager sessionManager; @Value("${app.sftp.connection-timeout:10000}") private int connectionTimeout; @Value("${app.sftp.max-retries:3}") private int maxRetries; public String connect(ConnectionRequest request) throws Exception { JSch jsch = new JSch(); Session session = null; Channel channel = null; com.jcraft.jsch.ChannelSftp sftpChannel = null; int retryCount = 0; while (retryCount < maxRetries) { try { // 配置私钥(如果提供) if (request.getPrivateKeyPath() != null && !request.getPrivateKeyPath().isEmpty()) { jsch.addIdentity(request.getPrivateKeyPath(), request.getPassPhrase() != null ? request.getPassPhrase() : ""); } // 创建会话 session = jsch.getSession(request.getUsername(), request.getHost(), request.getPort() != null ? request.getPort() : 22); // 配置密码(如果使用密码认证) if (request.getPassword() != null && !request.getPassword().isEmpty()) { session.setPassword(request.getPassword()); } // 跳过主机密钥验证 java.util.Properties config = new java.util.Properties(); config.put("StrictHostKeyChecking", "no"); session.setConfig(config); // 设置超时 session.setTimeout(connectionTimeout); // 连接 session.connect(); channel = session.openChannel("sftp"); channel.connect(); sftpChannel = (com.jcraft.jsch.ChannelSftp) channel; // 如果指定了默认路径,切换到该路径 if (request.getRootPath() != null && !request.getRootPath().isEmpty()) { try { sftpChannel.cd(request.getRootPath()); } catch (Exception e) { // 路径不存在,使用默认路径 } } // 创建连接对象(用于保存配置) Connection connection = new Connection(); connection.setName(request.getName()); connection.setHost(request.getHost()); connection.setPort(request.getPort() != null ? request.getPort() : 22); connection.setUsername(request.getUsername()); connection.setPassword(request.getPassword()); connection.setPrivateKeyPath(request.getPrivateKeyPath()); connection.setPassPhrase(request.getPassPhrase()); connection.setRootPath(request.getRootPath()); connection.setConnectTimeout(connectionTimeout); // 添加到会话管理器 return sessionManager.addSession(sftpChannel, connection); } catch (Exception e) { retryCount++; if (retryCount >= maxRetries) { throw new Exception("连接失败: " + e.getMessage(), e); } Thread.sleep(1000); // 等待1秒后重试 } } throw new Exception("连接失败"); } public void disconnect(String sessionId) { sessionManager.removeSession(sessionId); } public Connection saveConnection(Connection connection) { return connectionRepository.save(connection); } public List listConnections() { return connectionRepository.findByOrderByCreatedAtDesc(); } public Connection getConnectionById(Long id) { return connectionRepository.findById(id).orElse(null); } public void deleteConnection(Long id) { connectionRepository.deleteById(id); } } ``` ### 3.2.4 ConnectionController连接控制器 ```java package com.sftp.manager.controller; import com.sftp.manager.dto.ApiResponse; import com.sftp.manager.dto.ConnectionRequest; import com.sftp.manager.model.Connection; import com.sftp.manager.service.ConnectionService; import com.sftp.manager.service.SessionManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/connection") @CrossOrigin(origins = "*") public class ConnectionController { @Autowired private ConnectionService connectionService; @Autowired private SessionManager sessionManager; @PostMapping("/connect") public ApiResponse connect(@RequestBody ConnectionRequest request) { try { String sessionId = connectionService.connect(request); return ApiResponse.success("连接成功", sessionId); } catch (Exception e) { return ApiResponse.error("连接失败: " + e.getMessage()); } } @PostMapping("/disconnect") public ApiResponse disconnect(@RequestBody Map request) { try { String sessionId = request.get("sessionId"); connectionService.disconnect(sessionId); return ApiResponse.success("断开成功", null); } catch (Exception e) { return ApiResponse.error("断开失败: " + e.getMessage()); } } @PostMapping("/save") public ApiResponse saveConnection(@RequestBody Connection connection) { try { Connection saved = connectionService.saveConnection(connection); return ApiResponse.success("保存成功", saved); } catch (Exception e) { return ApiResponse.error("保存失败: " + e.getMessage()); } } @GetMapping("/list") public ApiResponse> listConnections() { try { List connections = connectionService.listConnections(); return ApiResponse.success("查询成功", connections); } catch (Exception e) { return ApiResponse.error("查询失败: " + e.getMessage()); } } @GetMapping("/{id}") public ApiResponse getConnection(@PathVariable Long id) { try { Connection connection = connectionService.getConnectionById(id); if (connection != null) { return ApiResponse.success("查询成功", connection); } else { return ApiResponse.error("连接不存在"); } } catch (Exception e) { return ApiResponse.error("查询失败: " + e.getMessage()); } } @DeleteMapping("/{id}") public ApiResponse deleteConnection(@PathVariable Long id) { try { connectionService.deleteConnection(id); return ApiResponse.success("删除成功", null); } catch (Exception e) { return ApiResponse.error("删除失败: " + e.getMessage()); } } @GetMapping("/active") public ApiResponse> getActiveConnections() { try { return ApiResponse.success("查询成功", sessionManager.getAllActiveConnections()); } catch (Exception e) { return ApiResponse.error("查询失败: " + e.getMessage()); } } } ``` ## 3.3 API接口说明 ### 接口列表 | 方法 | 路径 | 说明 | 请求参数 | |------|------|------|----------| | POST | /api/connection/connect | 建立SFTP连接 | ConnectionRequest | | POST | /api/connection/disconnect | 断开连接 | sessionId | | POST | /api/connection/save | 保存连接配置 | Connection | | GET | /api/connection/list | 获取所有保存的连接 | - | | GET | /api/connection/{id} | 获取指定连接 | 路径参数id | | DELETE | /api/connection/{id} | 删除连接配置 | 路径参数id | | GET | /api/connection/active | 获取所有活跃连接 | - | ### 请求/响应示例 #### 1. 建立连接 **请求:** ```json POST /api/connection/connect { "name": "测试服务器", "host": "192.168.1.100", "port": 22, "username": "root", "password": "123456" } ``` **响应:** ```json { "success": true, "message": "连接成功", "data": "sftp-12345678-1234-1234-1234-123456789abc" } ``` #### 2. 保存连接配置 **请求:** ```json POST /api/connection/save { "name": "测试服务器", "host": "192.168.1.100", "port": 22, "username": "root", "password": "encrypted_password" } ``` **响应:** ```json { "success": true, "message": "保存成功", "data": { "id": 1, "name": "测试服务器", "host": "192.168.1.100", "port": 22, "username": "root", "createdAt": "2024-02-02T10:00:00", "updatedAt": "2024-02-02T10:00:00" } } ``` #### 3. 获取连接列表 **请求:** ``` GET /api/connection/list ``` **响应:** ```json { "success": true, "message": "查询成功", "data": [ { "id": 1, "name": "测试服务器", "host": "192.168.1.100", "port": 22, "username": "root", "createdAt": "2024-02-02T10:00:00", "updatedAt": "2024-02-02T10:00:00" } ] } ``` #### 4. 获取活跃连接 **请求:** ``` GET /api/connection/active ``` **响应:** ```json { "success": true, "message": "查询成功", "data": { "sftp-12345678-1234-1234-1234-123456789abc": { "name": "测试服务器", "host": "192.168.1.100", "port": 22, "username": "root" } } } ``` ## 3.4 关键技术点 ### 3.4.1 JSch连接配置 - **StrictHostKeyChecking=no**:跳过主机密钥验证(仅用于开发环境) - **连接超时**:配置合理的超时时间 - **双重认证**:支持密码和私钥两种认证方式 ### 3.4.2 会话管理机制 **使用ConcurrentHashMap存储活跃会话:** - `activeSessions`:存储sessionId到ChannelSftp的映射 - `sessionConnections`:存储sessionId到Connection的映射 - 线程安全:使用ConcurrentHashMap确保多线程安全 ### 3.4.3 会话ID规则 - "local":表示本地文件系统 - "sftp-{uuid}":表示SFTP连接会话 ### 3.4.4 错误处理 - **连接超时**:自动重试(最多3次) - **认证失败**:返回明确错误信息 - **网络异常**:捕获并友好提示 ## 实施步骤 1. **创建Repository接口** ``` touch src/main/java/com/sftp/manager/repository/ConnectionRepository.java ``` 2. **创建SessionManager服务** ``` touch src/main/java/com/sftp/manager/service/SessionManager.java ``` 3. **创建ConnectionService服务** ``` touch src/main/java/com/sftp/manager/service/ConnectionService.java ``` 4. **创建ConnectionController控制器** ``` touch src/main/java/com/sftp/manager/controller/ConnectionController.java ``` 5. **编译测试** ``` mvn clean compile ``` 6. **启动服务并测试** ``` mvn spring-boot:run ``` ## 测试验证 使用Postman或curl测试以下API: 1. **保存连接配置** ``` curl -X POST http://localhost:8080/sftp-manager/api/connection/save \ -H "Content-Type: application/json" \ -d '{"name":"测试","host":"192.168.1.100","port":22,"username":"root","password":"123456"}' ``` 2. **获取连接列表** ``` curl http://localhost:8080/sftp-manager/api/connection/list ``` 3. **建立连接** ``` curl -X POST http://localhost:8080/sftp-manager/api/connection/connect \ -H "Content-Type: application/json" \ -d '{"name":"测试","host":"192.168.1.100","port":22,"username":"root","password":"123456"}' ``` ## 注意事项 1. **密码安全**:生产环境应加密存储密码 2. **连接池**:可考虑使用连接池管理SFTP连接 3. **心跳检测**:定期检测连接状态,自动重连 4. **资源清理**:确保连接断开时正确释放资源 ## 下一步 完成模块03后,继续模块04:文件浏览功能