578
docs/03-连接管理功能.md
Normal file
578
docs/03-连接管理功能.md
Normal file
@@ -0,0 +1,578 @@
|
||||
# 模块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<Connection, Long> {
|
||||
List<Connection> findByOrderByCreatedAtDesc(); // 按创建时间倒序查询
|
||||
Optional<Connection> 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<String, ChannelSftp> activeSessions = new ConcurrentHashMap<>();
|
||||
private final Map<String, Connection> 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<String, Connection> 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<Connection> 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<String> 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<Void> disconnect(@RequestBody Map<String, String> 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<Connection> 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<List<Connection>> listConnections() {
|
||||
try {
|
||||
List<Connection> connections = connectionService.listConnections();
|
||||
return ApiResponse.success("查询成功", connections);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("查询失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<Connection> 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<Void> deleteConnection(@PathVariable Long id) {
|
||||
try {
|
||||
connectionService.deleteConnection(id);
|
||||
return ApiResponse.success("删除成功", null);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("删除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/active")
|
||||
public ApiResponse<Map<String, Connection>> 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:文件浏览功能
|
||||
Reference in New Issue
Block a user