Initial commit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
liu
2026-02-03 10:10:11 +08:00
commit 14289beb66
45 changed files with 15479 additions and 0 deletions

View 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
- 标准间距16px1rem
- 组件内边距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文件浏览功能