docs(repo): 移除过期的 SSE/调试文档
- 清理一次性调试记录与临时总结文档,保留正式文档\n- 降低仓库噪声,便于聚焦代码与正式说明
This commit is contained in:
1 parent
b10940d5d6
commit
806954fc94
8 files changed
-2233
No files matched your search
@@ -1,339 +0,0 @@
|
||||
# SSE 订阅系统更新日志
|
||||
|
||||
## 版本 2.0 - 客户端级别订阅管理 (2025-10-04)
|
||||
|
||||
### 🎯 核心改进
|
||||
|
||||
实现了**客户端级别的 SSE 订阅管理系统**,解决了以下问题:
|
||||
|
||||
1. ✅ **精准推送** - 只向订阅了特定路径的客户端发送事件
|
||||
2. ✅ **自动清理** - 客户端断开时自动清理订阅
|
||||
3. ✅ **独立管理** - 每个客户端独立管理自己的订阅列表
|
||||
4. ✅ **减少噪音** - 避免无关事件干扰客户端
|
||||
|
||||
---
|
||||
|
||||
## 📝 详细更改
|
||||
|
||||
### 后端更改 (Go)
|
||||
|
||||
#### 1. SSE Manager 增强 (`pkg/sse/manager.go`)
|
||||
|
||||
**Client 结构新增字段:**
|
||||
```go
|
||||
type Client struct {
|
||||
// ... 原有字段
|
||||
|
||||
// 新增:订阅管理
|
||||
subscriptions map[string]bool // key: 订阅的路径, value: true
|
||||
subscriptionMutex sync.RWMutex // 保护 subscriptions
|
||||
}
|
||||
```
|
||||
|
||||
**新增方法:**
|
||||
- `Subscribe(clientID, path)` - 客户端订阅指定路径
|
||||
- `Unsubscribe(clientID, path)` - 客户端取消订阅
|
||||
- `GetSubscriptions(clientID)` - 获取客户端所有订阅
|
||||
- `IsSubscribed(clientID, path)` - 检查客户端是否订阅
|
||||
- `BroadcastToSubscribers(path, eventType, message)` - 向订阅者广播事件
|
||||
|
||||
**修改:**
|
||||
- `RegisterClient()` - 初始化 `subscriptions` 映射
|
||||
|
||||
---
|
||||
|
||||
#### 2. SSE Handler 增强 (`internal/handler/sse_handler.go`)
|
||||
|
||||
**新增 HTTP 接口:**
|
||||
|
||||
1. **订阅路径**
|
||||
- 路由: `POST /api/v1/sse/subscribe`
|
||||
- 请求体: `{ "client_id": "...", "path": "..." }`
|
||||
|
||||
2. **取消订阅**
|
||||
- 路由: `POST /api/v1/sse/unsubscribe`
|
||||
- 请求体: `{ "client_id": "...", "path": "..." }`
|
||||
|
||||
3. **获取订阅列表**
|
||||
- 路由: `GET /api/v1/sse/subscriptions/:clientId`
|
||||
|
||||
---
|
||||
|
||||
#### 3. FileSystemEvent 增强 (`internal/utils/filesystem_watcher.go`)
|
||||
|
||||
**新增字段:**
|
||||
```go
|
||||
type FileSystemEvent struct {
|
||||
// ... 原有字段
|
||||
|
||||
WatchedPath string `json:"watched_path"` // 被监听的根路径
|
||||
}
|
||||
```
|
||||
|
||||
**新增方法:**
|
||||
```go
|
||||
// findWatchedPath 查找事件路径对应的被监听根路径
|
||||
func (fsw *FileSystemWatcher) findWatchedPath(eventPath string) string
|
||||
```
|
||||
|
||||
**修改逻辑:**
|
||||
- `handleEvent()` - 自动填充 `WatchedPath` 字段
|
||||
|
||||
---
|
||||
|
||||
#### 4. 文件系统 Handler 修改 (`internal/handler/filesystem_handler.go`)
|
||||
|
||||
**修改:**
|
||||
- 使用 `BroadcastToSubscribers()` 替代 `BroadcastEvent()`
|
||||
- 根据 `event.WatchedPath` 精准推送事件
|
||||
|
||||
**代码对比:**
|
||||
```go
|
||||
// 修改前
|
||||
sseManager.BroadcastEvent("filesystem-change", eventStr)
|
||||
|
||||
// 修改后
|
||||
sseManager.BroadcastToSubscribers(ev.WatchedPath, "filesystem-change", eventStr)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 前端更改 (Vue3/TypeScript)
|
||||
|
||||
#### 1. SSEService 增强 (`services/sseService.ts`)
|
||||
|
||||
**新增字段:**
|
||||
```typescript
|
||||
class SSEService {
|
||||
private clientID: string | null = null // 存储客户端 ID
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**新增方法:**
|
||||
```typescript
|
||||
getClientID(): string | null
|
||||
subscribe(path: string): Promise<boolean>
|
||||
unsubscribe(path: string): Promise<boolean>
|
||||
getSubscriptions(): Promise<string[]>
|
||||
```
|
||||
|
||||
**新增事件监听:**
|
||||
```typescript
|
||||
// 监听 connected 事件获取客户端 ID
|
||||
this.eventSource.addEventListener('connected', (event) => {
|
||||
const data = JSON.parse(event.data)
|
||||
this.clientID = data.client_id
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2. FileSystemChangeEvent 增强
|
||||
|
||||
**新增字段:**
|
||||
```typescript
|
||||
interface FileSystemChangeEvent {
|
||||
// ... 原有字段
|
||||
watched_path?: string // 被监听的根路径
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 使用示例
|
||||
|
||||
### 前端使用
|
||||
|
||||
```typescript
|
||||
import { sseService } from '@/services/sseService'
|
||||
|
||||
// 1. 连接 SSE
|
||||
await sseService.connect()
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 2. 订阅路径
|
||||
await sseService.subscribe('D:\\EdgeDownload')
|
||||
|
||||
// 3. 监听事件
|
||||
const unsubscribe = sseService.onFileSystemChange((event) => {
|
||||
console.log('文件变化:', event)
|
||||
})
|
||||
|
||||
// 4. 清理
|
||||
onUnmounted(async () => {
|
||||
await sseService.unsubscribe('D:\\EdgeDownload')
|
||||
unsubscribe()
|
||||
})
|
||||
```
|
||||
|
||||
### 后端 API 测试
|
||||
|
||||
```http
|
||||
# 1. 订阅路径
|
||||
POST http://127.0.0.1:9484/api/v1/sse/subscribe
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"client_id": "your-client-id",
|
||||
"path": "D:\\EdgeDownload"
|
||||
}
|
||||
|
||||
# 2. 获取订阅列表
|
||||
GET http://127.0.0.1:9484/api/v1/sse/subscriptions/your-client-id
|
||||
|
||||
# 3. 取消订阅
|
||||
POST http://127.0.0.1:9484/api/v1/sse/unsubscribe
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"client_id": "your-client-id",
|
||||
"path": "D:\\EdgeDownload"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 测试步骤
|
||||
|
||||
### 1. 启动后端
|
||||
|
||||
```bash
|
||||
cd D:\go-argus\src\rear
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### 2. 启动前端
|
||||
|
||||
```bash
|
||||
cd D:\go-argus\src\front\argus-front
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. 测试订阅功能
|
||||
|
||||
1. 打开浏览器控制台
|
||||
2. 等待 SSE 连接建立(看到客户端 ID)
|
||||
3. 订阅路径:
|
||||
```javascript
|
||||
await sseService.subscribe('D:\\EdgeDownload')
|
||||
```
|
||||
4. 在 `D:\EdgeDownload` 中创建/修改/删除文件
|
||||
5. 观察控制台输出的文件系统事件
|
||||
|
||||
### 4. 验证精准推送
|
||||
|
||||
1. 打开多个浏览器标签页
|
||||
2. 每个标签页订阅不同的路径
|
||||
3. 修改某个路径下的文件
|
||||
4. 验证只有订阅该路径的标签页收到事件
|
||||
|
||||
---
|
||||
|
||||
## 📊 性能改进
|
||||
|
||||
### 1. 减少网络传输
|
||||
|
||||
**修改前:**
|
||||
- 所有客户端收到所有文件系统事件
|
||||
- 客户端自己过滤无关事件
|
||||
|
||||
**修改后:**
|
||||
- 服务端根据订阅精准推送
|
||||
- 网络传输量大幅减少
|
||||
|
||||
### 2. 减少客户端处理负担
|
||||
|
||||
**修改前:**
|
||||
```
|
||||
事件 100 个 × 客户端 10 个 = 1000 次处理
|
||||
```
|
||||
|
||||
**修改后:**
|
||||
```
|
||||
事件 100 个 × 订阅客户端 2 个 = 200 次处理
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 破坏性更改
|
||||
|
||||
### 1. FileSystemEvent 结构变更
|
||||
|
||||
新增了 `watched_path` 字段,旧版本客户端可能无法正确解析。
|
||||
|
||||
**解决方案:**
|
||||
- 字段为可选 (`json:"-omitempty"`)
|
||||
- 旧客户端可忽略该字段
|
||||
|
||||
### 2. 订阅模式变更
|
||||
|
||||
**修改前:**
|
||||
- 连接即收到所有文件系统事件
|
||||
|
||||
**修改后:**
|
||||
- 需要主动订阅才能收到事件
|
||||
- 未订阅的客户端不会收到 `filesystem-change` 事件
|
||||
|
||||
**迁移指南:**
|
||||
```typescript
|
||||
// 在连接成功后立即订阅
|
||||
await sseService.connect()
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
await sseService.subscribe(defaultPath) // 添加这行
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 已修复的问题
|
||||
|
||||
### 1. 编译错误: `undefined: path`
|
||||
|
||||
**问题:**
|
||||
`filesystem_handler.go:91` 使用了未定义的变量 `path`
|
||||
|
||||
**解决方案:**
|
||||
- 在 `FileSystemEvent` 中添加 `WatchedPath` 字段
|
||||
- 在 `FileSystemWatcher.handleEvent()` 中自动填充该字段
|
||||
- 使用 `event.WatchedPath` 作为订阅路径
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- **使用指南**: `SSE_SUBSCRIPTION_GUIDE.md`
|
||||
- **HTTP 测试**: `rear/tests/sse_subscription.http`
|
||||
- **代码示例**: `front/argus-front/src/examples/sseSubscriptionExample.ts`
|
||||
|
||||
---
|
||||
|
||||
## 🔮 未来改进
|
||||
|
||||
### 计划中的功能
|
||||
|
||||
1. **路径模式匹配**
|
||||
- 支持通配符订阅 (如 `D:\**\*.jpg`)
|
||||
- 支持正则表达式匹配
|
||||
|
||||
2. **订阅限制**
|
||||
- 单个客户端最大订阅数限制
|
||||
- 路径权限验证
|
||||
|
||||
3. **订阅统计**
|
||||
- 每个路径的订阅客户端数量
|
||||
- 事件发送统计
|
||||
|
||||
4. **持久化订阅**
|
||||
- 重连后自动恢复订阅
|
||||
- 订阅信息存储到数据库
|
||||
|
||||
---
|
||||
|
||||
## 👥 贡献者
|
||||
|
||||
- Claude Code
|
||||
|
||||
---
|
||||
|
||||
**版本**: 2.0
|
||||
**发布日期**: 2025-10-04
|
||||
@@ -1,159 +0,0 @@
|
||||
# 优雅关闭问题修复说明
|
||||
|
||||
## 问题描述
|
||||
|
||||
后端服务器在接收到关闭信号(Ctrl+C 或 SIGTERM)后,总是显示 "Shutting down server..." 但无法立即关闭,需要等待 30 秒超时才能退出。
|
||||
|
||||
## 根本原因
|
||||
|
||||
在调用 `srv.Shutdown()` 之前,没有清理以下长连接资源:
|
||||
|
||||
1. **SSE (Server-Sent Events) 连接** - SSE Manager 维护的长连接客户端
|
||||
2. **文件系统监听器** - fsnotify 创建的文件监听 goroutine
|
||||
|
||||
这些未关闭的连接会阻止 HTTP 服务器优雅关闭,导致必须等待超时。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 修改 `internal/router/router.go`
|
||||
|
||||
添加清理函数返回值:
|
||||
|
||||
```go
|
||||
// SetupRoutes 设置路由
|
||||
// 返回一个清理函数,用于在服务器关闭时清理资源
|
||||
func SetupRoutes(r *gin.Engine, contain *container.DbContainer, imgContain *container.TaskContainer) CleanupFunc {
|
||||
// ... 路由设置代码 ...
|
||||
|
||||
// 返回清理函数
|
||||
return func() {
|
||||
// 关闭 SSE Manager
|
||||
if sseHandler != nil && sseHandler.Manager != nil {
|
||||
sseHandler.Manager.Close()
|
||||
}
|
||||
|
||||
// 关闭文件系统监听器
|
||||
if fileSystemHandler != nil && fileSystemHandler.GetFileWatcher() != nil {
|
||||
fileSystemHandler.GetFileWatcher().Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 修改 `internal/handler/filesystem_handler.go`
|
||||
|
||||
添加 `GetFileWatcher()` 方法暴露内部监听器:
|
||||
|
||||
```go
|
||||
// GetFileWatcher 获取文件监听器(用于清理)
|
||||
func (h *FileSystemHandler) GetFileWatcher() *utils.FileSystemWatcher {
|
||||
return h.fileWatcher
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 修改 `internal/utils/filesystem_watcher.go`
|
||||
|
||||
添加 `Close()` 方法作为 `Stop()` 的别名:
|
||||
|
||||
```go
|
||||
// Close 关闭监听器(Stop 的别名,用于资源清理)
|
||||
func (fsw *FileSystemWatcher) Close() {
|
||||
fsw.Stop()
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 修改 `main.go`
|
||||
|
||||
在服务器关闭前调用清理函数:
|
||||
|
||||
```go
|
||||
// 设置路由,并获取清理函数
|
||||
cleanup := router.SetupRoutes(r, con, imgContain)
|
||||
|
||||
// ... 启动服务器 ...
|
||||
|
||||
// 阻塞主goroutine,等待信号
|
||||
<-quit
|
||||
logger.Info("Shutting down server...")
|
||||
|
||||
// 先清理资源(关闭 SSE 连接和文件监听器)
|
||||
logger.Info("Cleaning up resources...")
|
||||
cleanup()
|
||||
logger.Info("Resources cleaned up")
|
||||
|
||||
// 然后关闭 HTTP 服务器(超时时间从 30s 改为 5s)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
logger.Errorf("Server forced to shutdown: %v", err)
|
||||
} else {
|
||||
logger.Info("Server exited gracefully")
|
||||
}
|
||||
```
|
||||
|
||||
## 关闭流程
|
||||
|
||||
修复后的优雅关闭流程:
|
||||
|
||||
1. **接收信号** (SIGINT/SIGTERM)
|
||||
2. **清理长连接资源**
|
||||
- 关闭所有 SSE 客户端连接
|
||||
- 停止文件系统监听器
|
||||
3. **关闭 HTTP 服务器**
|
||||
- 等待现有请求完成
|
||||
- 最多等待 5 秒(之前是 30 秒)
|
||||
4. **退出程序**
|
||||
|
||||
## 测试方法
|
||||
|
||||
1. 启动后端服务器
|
||||
2. 打开前端,建立 SSE 连接
|
||||
3. 在前端打开一个文件夹(触发文件监听)
|
||||
4. 在后端控制台按 `Ctrl+C`
|
||||
5. 观察日志输出
|
||||
|
||||
**预期输出**:
|
||||
```
|
||||
Shutting down server...
|
||||
Cleaning up resources...
|
||||
Resources cleaned up
|
||||
Server exited gracefully
|
||||
```
|
||||
|
||||
**预期时间**: < 1 秒(之前需要 30 秒)
|
||||
|
||||
## 修改的文件
|
||||
|
||||
- `rear/main.go` - 调用清理函数
|
||||
- `rear/internal/router/router.go` - 返回清理函数
|
||||
- `rear/internal/handler/filesystem_handler.go` - 暴露文件监听器
|
||||
- `rear/internal/utils/filesystem_watcher.go` - 添加 Close 方法
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **清理顺序很重要**: 必须先清理长连接资源,再关闭 HTTP 服务器
|
||||
2. **超时时间调整**: 从 30 秒改为 5 秒,因为长连接已提前关闭
|
||||
3. **并发安全**: SSE Manager 和 FileSystemWatcher 的 Close 方法都是并发安全的
|
||||
4. **幂等性**: Close 方法可以多次调用而不会出错
|
||||
|
||||
## 日志说明
|
||||
|
||||
关闭时的日志输出:
|
||||
|
||||
```
|
||||
2025-10-05 15:30:00.000 INFO Shutting down server...
|
||||
2025-10-05 15:30:00.100 INFO Cleaning up resources...
|
||||
2025-10-05 15:30:00.150 INFO Resources cleaned up
|
||||
2025-10-05 15:30:00.200 INFO Server exited gracefully
|
||||
```
|
||||
|
||||
如果仍然超时,日志会显示:
|
||||
```
|
||||
2025-10-05 15:30:00.000 INFO Shutting down server...
|
||||
2025-10-05 15:30:00.100 INFO Cleaning up resources...
|
||||
2025-10-05 15:30:00.150 INFO Resources cleaned up
|
||||
2025-10-05 15:30:05.200 ERROR Server forced to shutdown: context deadline exceeded
|
||||
```
|
||||
|
||||
这种情况下需要检查是否有其他未关闭的连接或阻塞的 goroutine。
|
||||
@@ -1,207 +0,0 @@
|
||||
# 照片预览功能测试指南(v2 - macOS Quick Look 风格)
|
||||
|
||||
## ✅ 已实现的功能
|
||||
|
||||
### 后端
|
||||
- [x] 预览 API: `GET /api/v1/photo/preview?path={path}&size={size}`
|
||||
- [x] 智能格式转换(RAW → PNG → JPG/WebP)
|
||||
- [x] 多级缩略图生成(720p / 2K / 原图)
|
||||
- [x] 自动缓存机制
|
||||
|
||||
### 前端
|
||||
- [x] **macOS Quick Look 风格预览窗口**(悬浮、圆角、半透明背景)
|
||||
- [x] **文件列表中显示图片缩略图**(网格视图和列表视图)
|
||||
- [x] 渐进式加载(缩略图 → 高清 → 原图)
|
||||
- [x] 缩放和平移功能
|
||||
- [x] 空格键快捷预览
|
||||
|
||||
## 🧪 测试步骤
|
||||
|
||||
### 1. 启动服务
|
||||
|
||||
#### 后端
|
||||
```bash
|
||||
cd D:\go-argus\src\rear
|
||||
go run main.go
|
||||
```
|
||||
|
||||
#### 前端
|
||||
```bash
|
||||
cd D:\go-argus\src\front\argus-front
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 2. 测试预览功能
|
||||
|
||||
1. **打开文件管理器**
|
||||
- 访问:http://localhost:3000/field
|
||||
|
||||
2. **浏览到包含照片的文件夹**
|
||||
- 双击进入文件夹
|
||||
- 确保文件夹中有 JPG/PNG/RAW 等图片文件
|
||||
|
||||
3. **测试缩略图显示**(新功能!)
|
||||
- 📸 **网格视图**: 照片文件应显示实际的缩略图,而不是图标
|
||||
- 小尺寸:60x60 像素缩略图
|
||||
- 中等尺寸:80x80 像素缩略图
|
||||
- 大尺寸:120x120 像素缩略图
|
||||
- 📋 **列表视图**: 照片文件图标处应显示 32x32 像素缩略图
|
||||
- 文件夹和其他文件类型仍显示图标
|
||||
|
||||
4. **测试 Quick Look 预览**(新风格!)
|
||||
- 点击选中一张照片(照片项会高亮)
|
||||
- 按下 **空格键**
|
||||
- 应该弹出 **macOS 风格的悬浮预览窗口**:
|
||||
- ✨ 圆角边框(12px)
|
||||
- 🌫️ 半透明背景(带模糊效果)
|
||||
- 📏 适中大小(85%视口,可以看到后面的文件列表)
|
||||
- 🎬 弹出动画(缩放 + 淡入)
|
||||
- 窗口包含三部分:
|
||||
- 顶部标题栏:文件名 + 文件大小 + 关闭按钮
|
||||
- 中间图片区域:可缩放、平移
|
||||
- 底部工具栏:缩放控制 + 质量切换
|
||||
|
||||
5. **测试预览交互**
|
||||
- **初始加载**: 应显示 720p 缩略图(加载快)
|
||||
- **双击放大**: 双击图片,自动放大并切换到高清模式
|
||||
- **滚轮缩放**: 鼠标滚轮缩放图片
|
||||
- **拖拽平移**: 放大后可拖拽图片查看细节
|
||||
- **质量切换**:
|
||||
- 点击底部"缩略图"按钮 → 720p
|
||||
- 点击"高清"按钮 → 2K 高清
|
||||
- 点击"原图"按钮 → 原图(可能很大)
|
||||
- **关闭预览**:
|
||||
- 按 ESC 键
|
||||
- 点击右上角关闭按钮(悬停时变红色)
|
||||
- 点击窗口外的背景区域
|
||||
|
||||
### 3. 测试不同文件格式
|
||||
|
||||
#### 小图片(< 5MB)
|
||||
- 测试 JPG/PNG 小文件
|
||||
- 预期:直接加载原图
|
||||
|
||||
#### 中等图片(5-20MB)
|
||||
- 测试中等 JPG 文件
|
||||
- 预期:初始加载 1080p 缩略图
|
||||
|
||||
#### 大图片(> 20MB)
|
||||
- 测试专业相机拍摄的 RAW 或大 JPG
|
||||
- 预期:
|
||||
1. 初始加载 720p 缩略图(< 200KB,快速)
|
||||
2. 切换到高清时加载 2K 版本(1-2MB)
|
||||
3. 切换到原图时才加载完整文件
|
||||
|
||||
#### RAW 格式
|
||||
- 测试 CR2/NEF/ARW 等 RAW 文件
|
||||
- 预期:后端自动转换为 JPG 并生成缩略图
|
||||
|
||||
### 4. 测试边界情况
|
||||
|
||||
- **路径错误**: 选中文件后手动删除,再预览
|
||||
- **网络错误**: 断开后端服务,再预览
|
||||
- **非照片文件**: 选中文件夹或视频,按空格
|
||||
- 预期:文件夹/其他文件使用旧的 QuickPreview
|
||||
- 仅照片使用 PhotoPreviewModal
|
||||
|
||||
## 🎯 预期表现
|
||||
|
||||
### 性能指标
|
||||
- **初始预览响应**: < 500ms(包括 Hash 计算)
|
||||
- **缩略图加载**: < 200ms(720p < 200KB)
|
||||
- **高清加载**: < 1s(2K 约 1-2MB)
|
||||
- **原图加载**: 取决于文件大小,应显示加载进度
|
||||
|
||||
### 用户体验
|
||||
- **渐进式加载**: 先模糊后清晰,不阻塞操作
|
||||
- **平滑缩放**: 缩放和平移动画流畅
|
||||
- **快捷键**: 空格预览,ESC 关闭,+/- 缩放
|
||||
- **智能提示**: 底部控制栏显示当前质量和文件信息
|
||||
|
||||
## 🐛 已知问题与限制
|
||||
|
||||
1. **首次预览延迟**
|
||||
- 问题:Hash 计算可能需要几百毫秒
|
||||
- 解决:后续可优化为异步计算
|
||||
|
||||
2. **大文件内存占用**
|
||||
- 问题:加载 40MB 原图可能占用大量内存
|
||||
- 解决:用户需明确点击"原图"才加载
|
||||
|
||||
3. **视频不支持**
|
||||
- 当前仅支持照片预览
|
||||
- 视频预览将在未来版本实现
|
||||
|
||||
## 📊 测试检查清单
|
||||
|
||||
### 缩略图显示
|
||||
- [ ] 网格视图中照片显示缩略图(不是图标)
|
||||
- [ ] 列表视图中照片显示小缩略图
|
||||
- [ ] 缩略图加载流畅(无明显延迟)
|
||||
- [ ] 不同尺寸网格的缩略图大小适配
|
||||
- [ ] 文件夹和其他文件仍显示图标
|
||||
|
||||
### Quick Look 预览窗口
|
||||
- [ ] 窗口为悬浮样式(不是全屏)
|
||||
- [ ] 窗口有圆角和阴影
|
||||
- [ ] 背景半透明带模糊效果
|
||||
- [ ] 弹出动画流畅(缩放+淡入)
|
||||
- [ ] 可以看到后面的文件列表
|
||||
- [ ] 窗口大小适中(85%视口)
|
||||
|
||||
### 预览功能
|
||||
- [ ] 后端 API 正常响应
|
||||
- [ ] 空格键触发预览
|
||||
- [ ] 缩略图快速加载
|
||||
- [ ] 高清模式切换正常
|
||||
- [ ] 原图加载正常
|
||||
- [ ] 缩放功能正常
|
||||
- [ ] 平移功能正常
|
||||
- [ ] 双击放大正常
|
||||
- [ ] ESC 关闭正常
|
||||
- [ ] 点击背景关闭正常
|
||||
- [ ] 关闭按钮悬停变红
|
||||
- [ ] 文件信息显示正确
|
||||
- [ ] 不同格式都能预览(JPG/PNG/RAW)
|
||||
- [ ] 错误处理正常(路径错误、网络错误)
|
||||
|
||||
## 🔧 调试技巧
|
||||
|
||||
### 查看网络请求
|
||||
1. 打开浏览器开发者工具(F12)
|
||||
2. 切换到 Network 标签
|
||||
3. 预览照片,观察请求:
|
||||
- `/api/v1/photo/preview?path=...&size=720` → 缩略图
|
||||
- `/api/v1/photo/preview?path=...&size=2048` → 高清
|
||||
- `/api/v1/photo/preview?path=...&size=0` → 原图
|
||||
|
||||
### 查看控制台日志
|
||||
- 前端错误会显示在 Console 标签
|
||||
- 后端日志在终端输出
|
||||
|
||||
### 检查文件生成
|
||||
- 缩略图缓存位置:`D:\go-argus\src\rear\thumbnail\{hash前4位}\{hash}\{size}.jpg`
|
||||
- 检查是否生成了对应尺寸的缩略图
|
||||
|
||||
## 📝 下一步优化
|
||||
|
||||
1. **性能优化**
|
||||
- [ ] Hash 计算异步化
|
||||
- [ ] 预加载相邻照片
|
||||
- [ ] IndexedDB 缓存
|
||||
|
||||
2. **功能增强**
|
||||
- [ ] 左右方向键切换照片
|
||||
- [ ] 缩略图网格预览
|
||||
- [ ] 幻灯片模式
|
||||
|
||||
3. **用户体验**
|
||||
- [ ] 加载进度条
|
||||
- [ ] 更多缩放选项
|
||||
- [ ] 旋转功能
|
||||
|
||||
---
|
||||
|
||||
**测试日期**: 2025-10-06
|
||||
**版本**: v1.0
|
||||
**维护者**: Claude Code
|
||||
@@ -1,156 +0,0 @@
|
||||
# 照片预览功能 v2 - 更新总结
|
||||
|
||||
## 🎨 主要改进
|
||||
|
||||
### 1. macOS Quick Look 风格预览窗口
|
||||
|
||||
**之前**:全屏黑色背景,覆盖整个屏幕
|
||||
**现在**:悬浮窗口,类似 macOS Finder 的 Quick Look
|
||||
|
||||
#### 视觉效果
|
||||
- ✨ **悬浮窗口**:85% 视口大小,居中显示
|
||||
- 🌫️ **半透明背景**:带 20px 模糊效果,可以看到后面的文件列表
|
||||
- 🎨 **圆角设计**:12px 圆角 + 大阴影
|
||||
- 🎬 **优雅动画**:弹出时缩放+淡入效果(0.25s)
|
||||
|
||||
#### 窗口结构
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 标题栏:文件名 + 文件大小 + [X] │ ← 深色半透明
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ 图片显示区域 │
|
||||
│ (可缩放、可拖拽) │
|
||||
│ │
|
||||
├─────────────────────────────────────┤
|
||||
│ 底部工具栏:缩放控制 + 质量切换 │ ← 深色半透明
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 交互优化
|
||||
- **多种关闭方式**:
|
||||
- ESC 键
|
||||
- 点击右上角 X(悬停变红)
|
||||
- 点击窗口外背景区域
|
||||
- **窗口尺寸**:
|
||||
- 宽度:85vw(最大 1400px)
|
||||
- 高度:85vh(最大 900px)
|
||||
- 适中的大小,不会完全遮挡文件列表
|
||||
|
||||
### 2. 文件列表中显示真实缩略图
|
||||
|
||||
**之前**:所有照片都显示 🖼️ 图标
|
||||
**现在**:照片文件显示实际的缩略图预览
|
||||
|
||||
#### 网格视图缩略图
|
||||
- **小尺寸**:60x60 像素
|
||||
- **中等尺寸**:80x80 像素
|
||||
- **大尺寸**:120x120 像素
|
||||
- 圆角边框(8px)+ 阴影效果
|
||||
|
||||
#### 列表视图缩略图
|
||||
- **尺寸**:32x32 像素
|
||||
- 小圆角(4px)+ 轻微阴影
|
||||
- 与文件名对齐显示
|
||||
|
||||
#### 智能加载策略
|
||||
- **网格视图**:加载 300px 缩略图(适中质量)
|
||||
- **列表视图**:加载 200px 缩略图(更快速度)
|
||||
- 使用背景图片方式加载,避免布局抖动
|
||||
- 浏览器自动缓存,重复访问无需再次请求
|
||||
|
||||
## 📁 修改的文件
|
||||
|
||||
### 1. PhotoPreviewModal.vue
|
||||
**位置**:`D:\go-argus\src\front\argus-front\src\components\FileManager\PhotoPreviewModal.vue`
|
||||
|
||||
**主要改动**:
|
||||
- 重构 HTML 结构:`backdrop` + `window` + `header` + `footer`
|
||||
- 完全重写 CSS 样式:macOS 风格
|
||||
- 添加动画效果:`fadeIn` + `scaleIn`
|
||||
- 优化按钮样式:关闭按钮悬停变红
|
||||
|
||||
### 2. FilePane.vue
|
||||
**位置**:`D:\go-argus\src\front\argus-front\src\components\FileManager\FilePane.vue`
|
||||
|
||||
**主要改动**:
|
||||
- 模板修改:
|
||||
- 网格视图图标添加缩略图背景(第 129-139 行)
|
||||
- 列表视图图标添加缩略图背景(第 194-203 行)
|
||||
- JavaScript:
|
||||
- 新增 `getThumbnailStyle()` 函数(第 1843-1856 行)
|
||||
- CSS:
|
||||
- 添加 `.file-icon.has-thumbnail` 样式(第 2318-2335 行)
|
||||
- 添加 `.list-icon.has-thumbnail` 样式(第 2496-2503 行)
|
||||
|
||||
### 3. 测试文档更新
|
||||
**位置**:`D:\go-argus\src\PHOTO_PREVIEW_TEST.md`
|
||||
|
||||
**更新内容**:
|
||||
- 新增缩略图测试步骤
|
||||
- 新增 Quick Look 风格测试要点
|
||||
- 扩展测试检查清单
|
||||
|
||||
## 🎯 用户体验提升
|
||||
|
||||
### 快速浏览
|
||||
- **之前**:看到图标,猜测内容,按空格查看
|
||||
- **现在**:直接看到缩略图,一目了然
|
||||
|
||||
### 预览操作
|
||||
- **之前**:全屏预览,看不到其他文件,需要关闭后才能切换
|
||||
- **现在**:悬浮预览,可以看到文件列表,快速判断是否要切换
|
||||
|
||||
### 视觉美感
|
||||
- **之前**:纯黑背景,过于简陋
|
||||
- **现在**:半透明毛玻璃效果,优雅专业
|
||||
|
||||
## 🚀 性能优化
|
||||
|
||||
### 缩略图加载
|
||||
- 使用小尺寸缩略图(200-300px),加载快速
|
||||
- 浏览器自动缓存,重复访问无延迟
|
||||
- CSS background-image 加载,不影响布局
|
||||
|
||||
### 预览窗口
|
||||
- 初始加载 720p 缩略图(< 200KB)
|
||||
- 按需加载高清/原图(用户主动切换时)
|
||||
- 渐进式体验,不卡顿
|
||||
|
||||
## 📊 对比总结
|
||||
|
||||
| 特性 | v1 (全屏) | v2 (Quick Look) |
|
||||
|------|----------|-----------------|
|
||||
| 窗口样式 | 全屏黑色背景 | 悬浮窗口 |
|
||||
| 背景效果 | 纯黑 | 半透明模糊 |
|
||||
| 可见性 | 完全遮挡 | 可见文件列表 |
|
||||
| 圆角 | 无 | 12px 圆角 |
|
||||
| 阴影 | 无 | 大阴影 |
|
||||
| 动画 | 无 | 缩放+淡入 |
|
||||
| 关闭方式 | ESC / 按钮 | ESC / 按钮 / 点击背景 |
|
||||
| 缩略图 | ❌ 仅图标 | ✅ 真实缩略图 |
|
||||
|
||||
## ✨ 下一步建议
|
||||
|
||||
1. **左右方向键切换照片**
|
||||
- 预览状态下,按左右键切换同文件夹的照片
|
||||
- 自动预加载前后照片
|
||||
|
||||
2. **缩略图加载优化**
|
||||
- 虚拟滚动时按需加载可见区域缩略图
|
||||
- IntersectionObserver 懒加载
|
||||
|
||||
3. **预览增强**
|
||||
- 添加照片信息显示(分辨率、拍摄日期等)
|
||||
- 支持旋转功能
|
||||
- 支持全屏切换
|
||||
|
||||
4. **手势支持**(桌面应用)
|
||||
- 双指捏合缩放
|
||||
- 双指滑动切换照片
|
||||
|
||||
---
|
||||
|
||||
**版本**: v2.0
|
||||
**更新日期**: 2025-10-06
|
||||
**改进者**: Claude Code
|
||||
@@ -1,240 +0,0 @@
|
||||
# SSE 实时更新修复测试指南
|
||||
|
||||
## 📋 修复内容总结
|
||||
|
||||
### 前端修复 (Vue)
|
||||
|
||||
1. **修复响应式更新问题** - `FilePane.vue`
|
||||
- ✅ `visibleItems` computed 直接使用 `fileSystemAPI.fileItems.value`
|
||||
- ✅ 避免了通过 `currentFolder` 的中间转换导致响应式断裂
|
||||
|
||||
2. **移除双重刷新问题** - `useFileSystemAPI.ts`
|
||||
- ✅ CRUD 操作 (create/delete/move/copy) 不再手动调用 `browse()`
|
||||
- ✅ 完全依赖 SSE 推送自动刷新,避免双重请求
|
||||
|
||||
3. **修复路径匹配逻辑** - `useFileSystemAPI.ts`
|
||||
- ✅ 统一路径分隔符处理 (支持 Windows/Linux/macOS)
|
||||
- ✅ 使用 `normalize()` 函数将 `\` 转换为 `/`
|
||||
|
||||
4. **添加详细调试日志**
|
||||
- 🔍 `useFileSystemAPI.browse()` - API 请求和响应
|
||||
- 🔍 `useFileSystemAPI.fileItems` - computed 重新计算
|
||||
- 🔍 `useFileSystemAPI.handleFileSystemChange()` - SSE 事件接收和路径匹配
|
||||
- 🔍 `FilePane.visibleItems` - 最终渲染项目计算
|
||||
- 🔍 `FilePane.currentFolder` - 文件夹数据计算
|
||||
|
||||
### 后端优化 (Go)
|
||||
|
||||
1. **优化日志输出** - `filesystem_handler.go`
|
||||
- ✅ 浏览请求: `🔄 浏览文件系统请求`
|
||||
- ✅ 订阅监听: `👀 订阅文件夹监听请求`
|
||||
- ✅ 文件变化推送: `📤 文件系统变化事件已推送`
|
||||
- ✅ 所有日志会写入 `logs/app.log`
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试步骤
|
||||
|
||||
### 准备工作
|
||||
|
||||
1. **启动后端服务**
|
||||
```bash
|
||||
cd D:\go-argus\src\rear
|
||||
go run main.go
|
||||
```
|
||||
|
||||
2. **启动前端服务**
|
||||
```bash
|
||||
cd D:\go-argus\src\front\argus-front
|
||||
npm run dev
|
||||
```
|
||||
|
||||
3. **打开浏览器控制台** (F12)
|
||||
|
||||
---
|
||||
|
||||
### 测试 1: 基础文件删除自动刷新
|
||||
|
||||
**步骤**:
|
||||
1. 访问 `http://localhost:3000/#/field`
|
||||
2. 进入某个文件夹 (例如 `D:\test`)
|
||||
3. **观察控制台日志**,应该看到:
|
||||
```
|
||||
🔄 [useFileSystemAPI.browse] 开始刷新: D:\test
|
||||
✅ [useFileSystemAPI.browse] API 返回: { items_count: 5, ... }
|
||||
🔄 [useFileSystemAPI.fileItems] computed 重新计算: 5 个项目
|
||||
🔄 [FilePane.visibleItems] computed 重新计算
|
||||
✅ [FilePane.visibleItems] 最终返回: 5 个项目
|
||||
```
|
||||
|
||||
4. **在文件资源管理器中删除该文件夹内的一个文件**
|
||||
5. **观察控制台日志**,应该看到:
|
||||
```
|
||||
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: { type: "delete", name: "test.txt", ... }
|
||||
🔍 [useFileSystemAPI.handleFileSystemChange] 路径匹配检查: { match: true }
|
||||
✅ [useFileSystemAPI.handleFileSystemChange] 当前文件夹内容发生变化 (delete): test.txt
|
||||
🔄 [useFileSystemAPI.browse] 开始刷新: D:\test
|
||||
✅ [useFileSystemAPI.browse] API 返回: { items_count: 4, ... }
|
||||
🔄 [useFileSystemAPI.fileItems] computed 重新计算: 4 个项目
|
||||
🔄 [FilePane.visibleItems] computed 重新计算
|
||||
✅ [FilePane.visibleItems] 最终返回: 4 个项目
|
||||
```
|
||||
|
||||
6. **观察页面**, 文件列表应该自动更新,删除的文件消失
|
||||
|
||||
**预期结果**: ✅ 页面自动刷新,无需手动刷新
|
||||
|
||||
---
|
||||
|
||||
### 测试 2: 创建文件自动刷新
|
||||
|
||||
**步骤**:
|
||||
1. 在当前文件夹中新建一个文件 (例如 `新建文本文档.txt`)
|
||||
2. **观察控制台日志**:
|
||||
```
|
||||
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: { type: "create", name: "新建文本文档.txt", ... }
|
||||
✅ [useFileSystemAPI.handleFileSystemChange] 当前文件夹内容发生变化 (create): 新建文本文档.txt
|
||||
🔄 [useFileSystemAPI.browse] 开始刷新
|
||||
✅ [useFileSystemAPI.browse] API 返回: { items_count: 5, ... }
|
||||
```
|
||||
|
||||
3. **观察页面**, 新文件应该自动出现
|
||||
|
||||
**预期结果**: ✅ 页面自动显示新文件
|
||||
|
||||
---
|
||||
|
||||
### 测试 3: 重命名文件自动刷新
|
||||
|
||||
**步骤**:
|
||||
1. 重命名文件夹中的某个文件
|
||||
2. **观察控制台日志**:
|
||||
```
|
||||
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: { type: "rename", ... }
|
||||
```
|
||||
|
||||
3. **观察页面**, 文件名应该自动更新
|
||||
|
||||
**预期结果**: ✅ 页面自动显示新文件名
|
||||
|
||||
---
|
||||
|
||||
### 测试 4: 路径过滤 (不同文件夹的事件不应触发刷新)
|
||||
|
||||
**步骤**:
|
||||
1. 当前在 `D:\test` 文件夹
|
||||
2. 在 `D:\other` 文件夹中删除文件
|
||||
3. **观察控制台日志**:
|
||||
```
|
||||
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: { type: "delete", path: "D:\other\file.txt", ... }
|
||||
🔍 [useFileSystemAPI.handleFileSystemChange] 路径匹配检查: { match: false }
|
||||
⏭️ [useFileSystemAPI.handleFileSystemChange] 事件不在当前路径,忽略
|
||||
```
|
||||
|
||||
4. **观察页面**, 不应该发生任何变化
|
||||
|
||||
**预期结果**: ✅ 页面不刷新
|
||||
|
||||
---
|
||||
|
||||
### 测试 5: 防抖机制 (快速连续删除多个文件)
|
||||
|
||||
**步骤**:
|
||||
1. 快速连续删除 3 个文件 (在 1 秒内)
|
||||
2. **观察控制台日志**:
|
||||
```
|
||||
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: { type: "delete", name: "file1.txt" }
|
||||
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: { type: "delete", name: "file2.txt" }
|
||||
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: { type: "delete", name: "file3.txt" }
|
||||
// 300ms 防抖后只刷新一次
|
||||
🔄 [useFileSystemAPI.browse] 开始刷新
|
||||
```
|
||||
|
||||
3. **观察页面**, 应该只刷新一次,3 个文件同时消失
|
||||
|
||||
**预期结果**: ✅ 只发送 1 次 API 请求,页面正确更新
|
||||
|
||||
---
|
||||
|
||||
## 📊 后端日志检查
|
||||
|
||||
### 查看日志文件
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
type D:\go-argus\src\rear\logs\app.log | findstr "文件系统"
|
||||
|
||||
# Linux/macOS
|
||||
tail -f D:\go-argus\src\rear\logs\app.log | grep "文件系统"
|
||||
```
|
||||
|
||||
### 预期看到的日志
|
||||
|
||||
**订阅监听时**:
|
||||
```
|
||||
👀 订阅文件夹监听请求 | path=D:\test
|
||||
✅ 已开始监听文件夹 | path=D:\test | total_watched=1
|
||||
```
|
||||
|
||||
**文件变化时**:
|
||||
```
|
||||
📤 文件系统变化事件已推送 | event_type=delete | file_path=D:\test\file.txt | file_name=file.txt | watched_path=D:\test | is_dir=false | timestamp=2025-10-05 14:30:45
|
||||
```
|
||||
|
||||
**浏览请求时**:
|
||||
```
|
||||
🔄 浏览文件系统请求 | path=D:\test
|
||||
✅ 浏览文件系统成功 | current_path=D:\test | items_count=4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 问题排查
|
||||
|
||||
### 如果页面不自动刷新
|
||||
|
||||
**检查清单**:
|
||||
|
||||
1. **检查 SSE 连接是否建立**
|
||||
- 控制台是否有 `✅ SSE 连接已建立`
|
||||
- 网络面板是否有持续的 `GET /api/v1/sse/connect` 请求
|
||||
|
||||
2. **检查是否收到 SSE 事件**
|
||||
- 控制台是否有 `📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件`
|
||||
- 如果没有,检查后端日志是否有 `📤 文件系统变化事件已推送`
|
||||
|
||||
3. **检查路径匹配**
|
||||
- 控制台日志中 `🔍 路径匹配检查` 的 `match` 字段是否为 `true`
|
||||
- 如果为 `false`,检查 `eventDir` 和 `currentDir` 是否一致
|
||||
|
||||
4. **检查 computed 是否重新计算**
|
||||
- 控制台是否有 `🔄 [FilePane.visibleItems] computed 重新计算`
|
||||
- 如果没有,说明响应式链断裂
|
||||
|
||||
5. **检查后端监听是否正常**
|
||||
- 查看 `logs/app.log` 是否有 `✅ 已开始监听文件夹`
|
||||
- 检查 `total_watched` 数量是否正确
|
||||
|
||||
---
|
||||
|
||||
## 📝 已知限制
|
||||
|
||||
1. **防抖延迟**: 300ms 内的多次变化会合并为一次刷新
|
||||
2. **只监听直接子项**: 不会递归监听子文件夹的变化
|
||||
3. **Windows 路径**: 后端日志中路径使用 `\`,前端日志中统一为 `/`
|
||||
|
||||
---
|
||||
|
||||
## ✅ 成功标准
|
||||
|
||||
- ✅ 删除文件后,页面在 300-500ms 内自动更新
|
||||
- ✅ 创建文件后,页面在 300-500ms 内自动显示新文件
|
||||
- ✅ 重命名文件后,页面自动显示新名称
|
||||
- ✅ 其他文件夹的变化不会触发当前页面刷新
|
||||
- ✅ 控制台日志完整,无错误
|
||||
- ✅ 后端日志文件包含所有关键事件
|
||||
|
||||
---
|
||||
|
||||
**测试日期**: 2025-10-05
|
||||
**修复版本**: v1.0-sse-fix
|
||||
@@ -1,371 +0,0 @@
|
||||
# SSE 订阅和文件监听诊断指南
|
||||
|
||||
## 问题描述
|
||||
前端打开某个文件夹后,通过文件管理器删除/改名文件,页面不自动更新。
|
||||
|
||||
## 诊断流程
|
||||
|
||||
### 步骤 1: 检查前端订阅请求是否发送
|
||||
|
||||
**操作**:
|
||||
1. 打开浏览器控制台(F12)
|
||||
2. 切换到 **Network** 标签页
|
||||
3. 在前端进入某个文件夹(例如 `D:\test`)
|
||||
|
||||
**预期看到的请求**:
|
||||
```
|
||||
POST http://localhost:9484/api/v1/filesystem/watch
|
||||
Request Payload: {"path":"D:\\test"}
|
||||
Response:
|
||||
{
|
||||
"code": 200,
|
||||
"message": "已开始监听文件夹",
|
||||
"data": {
|
||||
"path": "D:\\test",
|
||||
"watched_paths": ["D:\\test"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**同时检查 Console 日志**:
|
||||
```
|
||||
🔄 [useFileSystemAPI.browse] 开始刷新: D:\test
|
||||
✅ [useFileSystemAPI.browse] API 返回: { ... }
|
||||
✅ [useFileSystemAPI.browse] items.value 已更新: 5 个项目
|
||||
✅ 已开始监听: D:\test
|
||||
```
|
||||
|
||||
**如果没有看到 `✅ 已开始监听`**:
|
||||
- 检查 `watchCurrentPath()` 是否被调用
|
||||
- 检查是否有错误日志 `⚠️ [useFileSystemAPI.browse] 订阅监听失败`
|
||||
- 检查 Network 是否有 `/filesystem/watch` 请求
|
||||
|
||||
---
|
||||
|
||||
### 步骤 2: 检查后端是否收到订阅请求
|
||||
|
||||
**操作**:
|
||||
查看后端日志 `D:\go-argus\src\rear\logs\app.log`
|
||||
|
||||
**预期日志**:
|
||||
```
|
||||
👀 订阅文件夹监听请求 | path=D:\test | client_ip=127.0.0.1
|
||||
开始监听文件夹: D:\test
|
||||
✅ 已开始监听文件夹 | path=D:\test | total_watched=1
|
||||
```
|
||||
|
||||
**如果没有这些日志**:
|
||||
- 前端请求没有发送
|
||||
- 检查前端代码 `watchCurrentPath()` 逻辑
|
||||
- 检查路径是否为空: `if (!currentPath.value || currentPath.value === '')`
|
||||
|
||||
**如果有订阅日志,但 total_watched=0**:
|
||||
- 后端 `fileWatcher.Watch()` 失败
|
||||
- 检查路径是否有效
|
||||
- 检查文件权限
|
||||
|
||||
---
|
||||
|
||||
### 步骤 3: 检查文件变化是否被监听到
|
||||
|
||||
**操作**:
|
||||
1. 确认步骤 1 和 2 都通过
|
||||
2. 在文件管理器中删除 `D:\test` 下的一个文件(例如 `test.txt`)
|
||||
3. 立即查看后端日志
|
||||
|
||||
**预期日志**:
|
||||
```
|
||||
📤 文件系统变化事件已推送 | event_type=delete | file_path=D:\test\test.txt | file_name=test.txt | watched_path=D:\test | is_dir=false | timestamp=2025-10-05 14:30:45
|
||||
```
|
||||
|
||||
**如果没有这条日志**:
|
||||
- **问题**: fsnotify 没有监听到文件变化
|
||||
- **可能原因**:
|
||||
1. Windows 路径问题:`D:\test` vs `D:/test`
|
||||
2. 文件监听器没有启动
|
||||
3. 路径未正确添加到监听列表
|
||||
|
||||
**调试方法**: 在后端添加调试日志
|
||||
```go
|
||||
// 在 filesystem_handler.go 的 WatchPath 中添加
|
||||
fmt.Printf("DEBUG: 监听路径 = %s\n", req.Path)
|
||||
fmt.Printf("DEBUG: 当前监听列表 = %v\n", h.fileWatcher.GetWatchedPaths())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 步骤 4: 检查 SSE 事件是否推送到前端
|
||||
|
||||
**操作**:
|
||||
1. 删除文件后,检查浏览器控制台 Console 日志
|
||||
|
||||
**预期日志**:
|
||||
```
|
||||
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: {
|
||||
type: "delete",
|
||||
path: "D:\\test\\test.txt",
|
||||
name: "test.txt",
|
||||
is_dir: false,
|
||||
timestamp: "2025-10-05T14:30:45Z"
|
||||
}
|
||||
```
|
||||
|
||||
**如果没有这条日志**:
|
||||
- **问题**: SSE 事件没有推送到前端
|
||||
- **可能原因**:
|
||||
1. SSE 连接断开
|
||||
2. 事件没有通过 SSE Manager 广播
|
||||
3. 前端 `sseService.onFileSystemChange()` 没有注册
|
||||
|
||||
**检查 SSE 连接**:
|
||||
在 Network 标签查看是否有持续的请求:
|
||||
```
|
||||
GET http://localhost:9484/api/v1/sse/connect
|
||||
Status: 200 (持续连接)
|
||||
Type: eventsource
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 步骤 5: 检查路径匹配逻辑
|
||||
|
||||
**操作**:
|
||||
如果步骤 4 有日志,但页面不刷新,检查路径匹配日志
|
||||
|
||||
**预期日志**:
|
||||
```
|
||||
🔍 [useFileSystemAPI.handleFileSystemChange] 路径匹配检查: {
|
||||
eventDir: "D:/test",
|
||||
currentDir: "D:/test",
|
||||
match: true
|
||||
}
|
||||
✅ [useFileSystemAPI.handleFileSystemChange] 当前文件夹内容发生变化 (delete): test.txt
|
||||
🔄 [useFileSystemAPI.browse] 开始刷新: D:\test
|
||||
```
|
||||
|
||||
**如果 match: false**:
|
||||
- **问题**: 路径不匹配
|
||||
- **可能原因**:
|
||||
1. 路径分隔符不一致 (`\` vs `/`)
|
||||
2. 路径大小写不一致
|
||||
3. eventDir 和 currentDir 值不同
|
||||
|
||||
**调试方法**:
|
||||
修改 `handleFileSystemChange` 添加详细日志:
|
||||
```typescript
|
||||
console.log('DEBUG: event.path =', event.path)
|
||||
console.log('DEBUG: currentPath.value =', currentPath.value)
|
||||
console.log('DEBUG: eventDir =', eventDir)
|
||||
console.log('DEBUG: currentDir =', currentDir)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题和解决方案
|
||||
|
||||
### 问题 1: 订阅请求没有发送
|
||||
|
||||
**症状**: Network 中没有 `/filesystem/watch` 请求
|
||||
|
||||
**原因**:
|
||||
- `currentPath.value === ''` (根目录跳过监听)
|
||||
- `currentPath.value === previousWatchedPath.value` (重复路径跳过)
|
||||
|
||||
**解决**:
|
||||
清除 `previousWatchedPath` 缓存
|
||||
```typescript
|
||||
// 在 browse() 开始时强制重置
|
||||
previousWatchedPath.value = ''
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题 2: 后端收到订阅但没有监听到文件变化
|
||||
|
||||
**症状**: 有 `👀 订阅文件夹监听请求` 日志,但删除文件后没有 `📤 文件系统变化事件已推送`
|
||||
|
||||
**原因**:
|
||||
- `fileWatcher.Watch()` 内部失败
|
||||
- Windows 路径规范化问题
|
||||
|
||||
**调试步骤**:
|
||||
1. 检查 `filesystem_watcher.go` 的 `Watch()` 方法
|
||||
2. 打印 `absPath` 和 `fsw.watchedPaths`
|
||||
3. 确认 `fsw.watcher.Add(absPath)` 没有返回错误
|
||||
|
||||
**临时解决**:
|
||||
在 `Watch()` 中添加详细日志:
|
||||
```go
|
||||
func (fsw *FileSystemWatcher) Watch(path string) error {
|
||||
absPath, err := filepath.Abs(path)
|
||||
fmt.Printf("DEBUG: 原始路径=%s, 绝对路径=%s\n", path, absPath)
|
||||
|
||||
if err := fsw.watcher.Add(absPath); err != nil {
|
||||
fmt.Printf("ERROR: fsnotify.Add() 失败: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("SUCCESS: 已添加监听: %s\n", absPath)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题 3: SSE 事件推送了但前端没收到
|
||||
|
||||
**症状**: 后端有 `📤 文件系统变化事件已推送`,但前端 Console 没有对应日志
|
||||
|
||||
**原因**:
|
||||
- SSE 连接已断开
|
||||
- 事件类型不匹配
|
||||
- `sseService.onFileSystemChange()` 回调没有注册
|
||||
|
||||
**检查 SSE 连接**:
|
||||
```typescript
|
||||
// 在 useFileSystemAPI onMounted 中添加
|
||||
console.log('SSE 连接状态:', sseService.isConnected())
|
||||
```
|
||||
|
||||
**检查事件订阅**:
|
||||
```typescript
|
||||
// 在 sseService.ts 中添加
|
||||
onFileSystemChange(callback: (event: FileSystemChangeEvent) => void) {
|
||||
console.log('注册 FileSystemChange 回调')
|
||||
this.eventHandlers['filesystem_change'] = callback
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题 4: 路径匹配失败
|
||||
|
||||
**症状**: 有 SSE 事件,但 `match: false`
|
||||
|
||||
**原因**:
|
||||
- Windows 路径 `D:\test` vs `D:/test`
|
||||
- 事件路径是子文件路径 `D:\test\file.txt`
|
||||
- currentPath 是文件夹路径 `D:\test`
|
||||
|
||||
**当前实现**:
|
||||
```typescript
|
||||
const eventDir = eventPath.substring(0, eventPath.lastIndexOf('/'))
|
||||
if (eventDir === currentDir || eventPath.startsWith(currentDir + '/'))
|
||||
```
|
||||
|
||||
**可能问题**:
|
||||
- `eventDir` 提取错误
|
||||
- `currentDir` 格式不一致
|
||||
|
||||
**改进建议**:
|
||||
```typescript
|
||||
// 更健壮的路径匹配
|
||||
const normalize = (p: string) => {
|
||||
return p.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase()
|
||||
}
|
||||
|
||||
const normalizedEvent = normalize(event.path)
|
||||
const normalizedCurrent = normalize(currentPath.value)
|
||||
|
||||
// 检查事件是否发生在当前目录或其子目录
|
||||
const isInCurrentDir = normalizedEvent.startsWith(normalizedCurrent + '/') ||
|
||||
normalizedEvent === normalizedCurrent
|
||||
|
||||
// 对于删除/重命名,只关心直接子项(不包括子文件夹内的文件)
|
||||
const eventDir = normalizedEvent.substring(0, normalizedEvent.lastIndexOf('/'))
|
||||
const isDirectChild = (eventDir === normalizedCurrent)
|
||||
|
||||
if (isDirectChild) {
|
||||
console.log('✅ 当前文件夹的直接子项发生变化')
|
||||
debouncedRefresh(currentPath.value)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速诊断命令
|
||||
|
||||
### 检查后端监听状态
|
||||
```bash
|
||||
# Windows
|
||||
curl http://localhost:9484/api/v1/filesystem/watched
|
||||
|
||||
# 预期返回
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取监听路径成功",
|
||||
"data": {
|
||||
"count": 1,
|
||||
"paths": ["D:\\test"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 手动订阅测试
|
||||
```bash
|
||||
# 订阅
|
||||
curl -X POST http://localhost:9484/api/v1/filesystem/watch ^
|
||||
-H "Content-Type: application/json" ^
|
||||
-d "{\"path\":\"D:\\test\"}"
|
||||
|
||||
# 取消订阅
|
||||
curl -X POST http://localhost:9484/api/v1/filesystem/unwatch ^
|
||||
-H "Content-Type: application/json" ^
|
||||
-d "{\"path\":\"D:\\test\"}"
|
||||
```
|
||||
|
||||
### 检查 SSE 连接
|
||||
```bash
|
||||
# 使用 curl 监听 SSE
|
||||
curl -N http://localhost:9484/api/v1/sse/connect
|
||||
|
||||
# 预期看到心跳事件
|
||||
event: heartbeat
|
||||
data: {"timestamp":"2025-10-05T14:30:00Z"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 诊断检查清单
|
||||
|
||||
完成以下检查,标记每一步的结果:
|
||||
|
||||
- [ ] **步骤 1**: 前端发送订阅请求 `/filesystem/watch`
|
||||
- [ ] Network 中看到请求
|
||||
- [ ] Response 返回 200
|
||||
- [ ] Console 显示 `✅ 已开始监听`
|
||||
|
||||
- [ ] **步骤 2**: 后端收到订阅请求
|
||||
- [ ] 日志中有 `👀 订阅文件夹监听请求`
|
||||
- [ ] 日志中有 `✅ 已开始监听文件夹`
|
||||
- [ ] `total_watched >= 1`
|
||||
|
||||
- [ ] **步骤 3**: 文件变化被监听到
|
||||
- [ ] 删除文件后日志中有 `📤 文件系统变化事件已推送`
|
||||
- [ ] `event_type` 正确 (delete/create/modify/rename)
|
||||
- [ ] `file_path` 正确
|
||||
|
||||
- [ ] **步骤 4**: SSE 事件推送到前端
|
||||
- [ ] Console 中有 `📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件`
|
||||
- [ ] 事件内容正确
|
||||
- [ ] Network 中 SSE 连接保持活跃
|
||||
|
||||
- [ ] **步骤 5**: 路径匹配和刷新
|
||||
- [ ] Console 中有 `🔍 路径匹配检查`
|
||||
- [ ] `match: true`
|
||||
- [ ] Console 中有 `✅ 当前文件夹内容发生变化`
|
||||
- [ ] Console 中有 `🔄 [useFileSystemAPI.browse] 开始刷新`
|
||||
|
||||
---
|
||||
|
||||
## 下一步操作
|
||||
|
||||
根据上面的检查结果,找到第一个失败的步骤:
|
||||
|
||||
1. **如果步骤 1 失败**: 修复前端订阅逻辑
|
||||
2. **如果步骤 2 失败**: 检查后端路由和 Handler
|
||||
3. **如果步骤 3 失败**: 检查 `filesystem_watcher.go` 实现
|
||||
4. **如果步骤 4 失败**: 检查 SSE Manager 和事件广播
|
||||
5. **如果步骤 5 失败**: 修复路径匹配逻辑
|
||||
|
||||
**请按照上面的诊断流程,逐步检查并告诉我哪一步失败了。**
|
||||
@@ -1,421 +0,0 @@
|
||||
# SSE 订阅管理系统使用指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
本系统实现了**客户端级别的 SSE 订阅管理**,解决了以下问题:
|
||||
|
||||
1. ✅ **精准推送** - 只向订阅了特定路径的客户端发送事件
|
||||
2. ✅ **自动清理** - 客户端断开时自动清理订阅
|
||||
3. ✅ **独立管理** - 每个客户端独立管理自己的订阅列表
|
||||
4. ✅ **减少噪音** - 避免无关事件干扰客户端
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 架构设计
|
||||
|
||||
### 核心组件
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Frontend (Vue3) │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ sseService.ts │ │
|
||||
│ │ - connect() │ │
|
||||
│ │ - subscribe(path) │ │
|
||||
│ │ - unsubscribe(path) │ │
|
||||
│ │ - getSubscriptions() │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
└──────────────────┬──────────────────────────┘
|
||||
│ HTTP + EventSource
|
||||
┌──────────────────▼──────────────────────────┐
|
||||
│ Backend (Go/Gin) │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ SSE Manager │ │
|
||||
│ │ - RegisterClient() │ │
|
||||
│ │ - Subscribe(clientID, path) │ │
|
||||
│ │ - Unsubscribe(clientID, path) │ │
|
||||
│ │ - BroadcastToSubscribers(path, ...) │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ FileSystem Watcher │ │
|
||||
│ │ - Watch(path) │ │
|
||||
│ │ - Unwatch(path) │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 使用流程
|
||||
|
||||
### 1. 前端连接 SSE
|
||||
|
||||
```typescript
|
||||
import { sseService } from '@/services/sseService'
|
||||
|
||||
// 连接 SSE 服务器
|
||||
await sseService.connect()
|
||||
|
||||
// 等待客户端 ID 初始化
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 获取客户端 ID
|
||||
const clientID = sseService.getClientID()
|
||||
console.log('客户端 ID:', clientID)
|
||||
```
|
||||
|
||||
### 2. 订阅文件夹
|
||||
|
||||
```typescript
|
||||
// 订阅指定路径
|
||||
const path = 'D:\\EdgeDownload'
|
||||
const success = await sseService.subscribe(path)
|
||||
|
||||
if (success) {
|
||||
console.log('✅ 订阅成功')
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 监听文件系统变化
|
||||
|
||||
```typescript
|
||||
// 注册回调函数
|
||||
const unsubscribe = sseService.onFileSystemChange((event) => {
|
||||
console.log('文件系统变化:', event)
|
||||
console.log('类型:', event.type) // create, modify, delete, rename
|
||||
console.log('路径:', event.path)
|
||||
console.log('名称:', event.name)
|
||||
})
|
||||
|
||||
// 组件卸载时取消监听
|
||||
onUnmounted(() => {
|
||||
unsubscribe()
|
||||
})
|
||||
```
|
||||
|
||||
### 4. 取消订阅
|
||||
|
||||
```typescript
|
||||
// 取消订阅指定路径
|
||||
await sseService.unsubscribe(path)
|
||||
```
|
||||
|
||||
### 5. 查看当前订阅
|
||||
|
||||
```typescript
|
||||
// 获取当前所有订阅
|
||||
const subscriptions = await sseService.getSubscriptions()
|
||||
console.log('当前订阅:', subscriptions)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 完整 Vue 组件示例
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { sseService } from '@/services/sseService'
|
||||
|
||||
interface FileSystemEvent {
|
||||
type: 'create' | 'modify' | 'delete' | 'rename'
|
||||
path: string
|
||||
name: string
|
||||
timestamp: string
|
||||
is_dir: boolean
|
||||
}
|
||||
|
||||
const currentPath = ref('D:\\EdgeDownload')
|
||||
const events = ref<FileSystemEvent[]>([])
|
||||
const isSubscribed = ref(false)
|
||||
|
||||
// 订阅回调清理函数
|
||||
let unsubscribeCallback: (() => void) | null = null
|
||||
|
||||
onMounted(async () => {
|
||||
// 连接 SSE
|
||||
await sseService.connect()
|
||||
|
||||
// 等待客户端 ID 初始化
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 订阅当前路径
|
||||
const success = await sseService.subscribe(currentPath.value)
|
||||
|
||||
if (success) {
|
||||
isSubscribed.value = true
|
||||
|
||||
// 监听文件系统变化
|
||||
unsubscribeCallback = sseService.onFileSystemChange((event) => {
|
||||
events.value.unshift(event)
|
||||
|
||||
// 保留最近 100 个事件
|
||||
if (events.value.length > 100) {
|
||||
events.value = events.value.slice(0, 100)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(async () => {
|
||||
// 取消订阅
|
||||
if (isSubscribed.value) {
|
||||
await sseService.unsubscribe(currentPath.value)
|
||||
}
|
||||
|
||||
// 取消回调监听
|
||||
if (unsubscribeCallback) {
|
||||
unsubscribeCallback()
|
||||
}
|
||||
})
|
||||
|
||||
// 切换监听的文件夹
|
||||
async function watchFolder(newPath: string) {
|
||||
// 取消旧订阅
|
||||
await sseService.unsubscribe(currentPath.value)
|
||||
|
||||
// 添加新订阅
|
||||
const success = await sseService.subscribe(newPath)
|
||||
|
||||
if (success) {
|
||||
currentPath.value = newPath
|
||||
events.value = []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h3>当前监听: {{ currentPath }}</h3>
|
||||
<p>订阅状态: {{ isSubscribed ? '✅ 已订阅' : '❌ 未订阅' }}</p>
|
||||
|
||||
<div>
|
||||
<h4>文件系统事件 ({{ events.length }})</h4>
|
||||
<ul>
|
||||
<li v-for="(event, index) in events" :key="index">
|
||||
<strong>{{ event.type }}</strong>: {{ event.name }}
|
||||
<span v-if="event.is_dir"> (文件夹)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 后端 API
|
||||
|
||||
### 1. 订阅路径
|
||||
|
||||
```http
|
||||
POST /api/v1/sse/subscribe
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"client_id": "your-client-id",
|
||||
"path": "D:\\EdgeDownload"
|
||||
}
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Subscribed successfully",
|
||||
"data": {
|
||||
"client_id": "your-client-id",
|
||||
"path": "D:\\EdgeDownload"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 取消订阅
|
||||
|
||||
```http
|
||||
POST /api/v1/sse/unsubscribe
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"client_id": "your-client-id",
|
||||
"path": "D:\\EdgeDownload"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 获取订阅列表
|
||||
|
||||
```http
|
||||
GET /api/v1/sse/subscriptions/:clientId
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Subscriptions retrieved successfully",
|
||||
"data": {
|
||||
"client_id": "your-client-id",
|
||||
"subscriptions": ["D:\\EdgeDownload", "D:\\Projects"],
|
||||
"count": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 调试技巧
|
||||
|
||||
### 前端调试
|
||||
|
||||
```typescript
|
||||
// 1. 检查客户端 ID
|
||||
console.log('客户端 ID:', sseService.getClientID())
|
||||
|
||||
// 2. 检查连接状态
|
||||
console.log('连接状态:', sseService.isConnected())
|
||||
|
||||
// 3. 查看当前订阅
|
||||
const subs = await sseService.getSubscriptions()
|
||||
console.log('当前订阅:', subs)
|
||||
```
|
||||
|
||||
### 后端日志
|
||||
|
||||
后端会输出以下日志:
|
||||
|
||||
```bash
|
||||
# 客户端连接
|
||||
Client connected: abc-123 (Total: 1)
|
||||
|
||||
# 订阅路径
|
||||
客户端 abc-123 订阅了路径: D:\EdgeDownload
|
||||
|
||||
# 广播事件
|
||||
向 1 个订阅了路径 D:\EdgeDownload 的客户端广播事件
|
||||
|
||||
# 取消订阅
|
||||
客户端 abc-123 取消订阅路径: D:\EdgeDownload
|
||||
|
||||
# 客户端断开
|
||||
Client disconnected: abc-123 (Total: 0)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 1. 客户端 ID 初始化
|
||||
|
||||
客户端 ID 是在 SSE 连接建立后通过 `connected` 事件传递的,需要等待:
|
||||
|
||||
```typescript
|
||||
await sseService.connect()
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)) // 等待初始化
|
||||
```
|
||||
|
||||
### 2. 组件卸载时清理
|
||||
|
||||
**必须在组件卸载时取消订阅和回调**:
|
||||
|
||||
```typescript
|
||||
onUnmounted(async () => {
|
||||
await sseService.unsubscribe(path)
|
||||
unsubscribeCallback()
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 路径格式
|
||||
|
||||
- ✅ Windows: `D:\\EdgeDownload`(双反斜杠)
|
||||
- ✅ Linux/Mac: `/home/user/Downloads`
|
||||
|
||||
### 4. SSE 重连处理
|
||||
|
||||
SSE 连接会自动重连,但订阅信息保留在服务端,**无需重新订阅**。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 常见问题
|
||||
|
||||
### Q1: 订阅后收不到事件?
|
||||
|
||||
检查清单:
|
||||
- [ ] 客户端 ID 是否正确获取
|
||||
- [ ] 路径是否已正确订阅(调用 `getSubscriptions()` 确认)
|
||||
- [ ] 后端是否正在监听该路径(检查文件系统监听器)
|
||||
- [ ] 监听路径是否与实际文件变化路径一致
|
||||
|
||||
### Q2: SSE 连接频繁断开?
|
||||
|
||||
- 当前后端配置:WriteTimeout = 5 分钟
|
||||
- 前端心跳超时:90 秒
|
||||
- Keepalive 间隔:5 秒
|
||||
- Ping 间隔:10 秒
|
||||
|
||||
如果仍然断开,检查:
|
||||
- 网络代理或防火墙设置
|
||||
- 浏览器是否限制长连接
|
||||
- 服务器资源是否充足
|
||||
|
||||
### Q3: 客户端断开后订阅会自动清理吗?
|
||||
|
||||
✅ **是的**!客户端断开时:
|
||||
1. SSE Manager 自动注销客户端
|
||||
2. 客户端的订阅映射自动清理
|
||||
3. 不会再收到任何事件
|
||||
|
||||
---
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
### 1. 事件节流
|
||||
|
||||
后端使用事件节流通道(容量 10):
|
||||
- 高频事件会被自动丢弃
|
||||
- 避免 SSE 通道阻塞
|
||||
|
||||
### 2. 精准推送
|
||||
|
||||
只向订阅了特定路径的客户端发送事件:
|
||||
- 减少无效网络传输
|
||||
- 降低客户端处理负担
|
||||
|
||||
### 3. 并发发送
|
||||
|
||||
后端使用 goroutine 并发向多个客户端发送事件:
|
||||
- 单个客户端阻塞不影响其他客户端
|
||||
- 100ms 超时自动断开慢速客户端
|
||||
|
||||
---
|
||||
|
||||
## 🔒 安全建议
|
||||
|
||||
1. **客户端 ID 验证** - 服务端验证客户端 ID 是否存在
|
||||
2. **路径验证** - 限制订阅路径范围(防止订阅敏感目录)
|
||||
3. **订阅数量限制** - 限制单个客户端最大订阅数
|
||||
4. **连接数限制** - 当前限制 500 个并发连接
|
||||
|
||||
---
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
### 优势
|
||||
|
||||
- ✅ **精准推送** - 只收到感兴趣的事件
|
||||
- ✅ **自动清理** - 断开连接自动清理订阅
|
||||
- ✅ **易于使用** - 简洁的 API 设计
|
||||
- ✅ **高性能** - 节流 + 并发优化
|
||||
- ✅ **可扩展** - 支持多客户端、多路径订阅
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 实现路径权限验证
|
||||
2. 添加订阅数量限制
|
||||
3. 支持路径模糊匹配(如 `D:\\**\\*.jpg`)
|
||||
4. 添加订阅统计和监控
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0
|
||||
**最后更新**: 2025-10-04
|
||||
**维护者**: Claude Code
|
||||
@@ -1,340 +0,0 @@
|
||||
# SSE 连接断开问题解决方案
|
||||
|
||||
## 🔍 问题分析
|
||||
|
||||
### 错误现象
|
||||
```
|
||||
GET http://127.0.0.1:3001/api/v1/sse/connect net::ERR_INCOMPLETE_CHUNKED_ENCODING 200 (OK)
|
||||
```
|
||||
|
||||
### 根本原因
|
||||
|
||||
**Go 的 `http.Server.WriteTimeout` 与 SSE 长连接冲突**
|
||||
|
||||
1. **WriteTimeout 机制**:
|
||||
- 从第一次写入开始计时
|
||||
- 整个响应必须在超时前完成
|
||||
- **不会因为持续写入而重置**
|
||||
|
||||
2. **SSE 的 c.Stream() 机制**:
|
||||
```go
|
||||
c.Stream(func(w io.Writer) bool {
|
||||
select {
|
||||
case event := <-client.EventChan:
|
||||
writeEvent(w, event) // 写入
|
||||
return true
|
||||
}
|
||||
})
|
||||
```
|
||||
- 每次循环等待新事件
|
||||
- 等待期间**没有写入操作**
|
||||
- 如果等待时间 > WriteTimeout → 连接关闭
|
||||
|
||||
3. **时间线示例** (WriteTimeout = 5分钟):
|
||||
```
|
||||
0:00 - 连接建立,发送 connected 事件
|
||||
0:05 - 发送 keepalive (写入)
|
||||
0:10 - 发送 ping (写入)
|
||||
...
|
||||
5:00 - 从第一次写入起已过 5 分钟
|
||||
5:00 - http.Server 强制关闭连接
|
||||
5:00 - 前端收到 ERR_INCOMPLETE_CHUNKED_ENCODING
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 解决方案
|
||||
|
||||
### 当前实现: WriteTimeout = 0
|
||||
|
||||
**配置文件**: `rear/internal/config/loader.go:125`
|
||||
|
||||
```go
|
||||
App: AppConfig{
|
||||
Port: getEnv("PORT", "8080"),
|
||||
Mode: getEnv("GIN_MODE", "debug"),
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 0, // 🔑 SSE 需要长连接,设置为 0(无限期)
|
||||
IdleTimeout: 10 * time.Minute,
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP 服务器**: `rear/main.go:237-243`
|
||||
|
||||
```go
|
||||
srv := &http.Server{
|
||||
Addr: ":" + config.CONFIG.Port,
|
||||
Handler: r,
|
||||
ReadTimeout: config.CONFIG.ReadTimeout, // 30秒
|
||||
WriteTimeout: config.CONFIG.WriteTimeout, // 0(无限期)
|
||||
IdleTimeout: config.CONFIG.IdleTimeout, // 10分钟
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ WriteTimeout = 0 的影响
|
||||
|
||||
### 对所有路由的影响
|
||||
|
||||
| 路由类型 | 影响 | 风险 |
|
||||
|----------|------|------|
|
||||
| 普通 API | ✅ 无影响 | ⚠️ 慢速客户端可能占用连接 |
|
||||
| 文件上传 | ✅ 支持大文件 | ⚠️ 恶意客户端可慢速上传 |
|
||||
| 文件下载 | ✅ 支持大文件 | ⚠️ 慢速客户端占用资源 |
|
||||
| SSE | ✅✅ 正常工作 | ✅ 无风险 |
|
||||
|
||||
### 风险缓解措施
|
||||
|
||||
#### 1. 应用层超时控制
|
||||
|
||||
普通 API 使用 Context 超时:
|
||||
|
||||
```go
|
||||
func (h *Handler) SomeAPI(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 使用 ctx 进行数据库查询等操作
|
||||
result, err := db.QueryContext(ctx, ...)
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 限流中间件
|
||||
|
||||
```go
|
||||
import "golang.org/x/time/rate"
|
||||
|
||||
var limiter = rate.NewLimiter(100, 200) // 100 req/s, burst 200
|
||||
|
||||
func RateLimitMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !limiter.Allow() {
|
||||
c.JSON(429, gin.H{"error": "Too many requests"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 连接数限制
|
||||
|
||||
当前配置:
|
||||
```go
|
||||
// SSE Manager
|
||||
opts.MaxClients = 500 // 最多 500 个 SSE 连接
|
||||
```
|
||||
|
||||
#### 4. IdleTimeout 保护
|
||||
|
||||
```go
|
||||
IdleTimeout: 10 * time.Minute // 10分钟无活动则关闭
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 生产环境部署建议
|
||||
|
||||
### 使用 Nginx 反向代理
|
||||
|
||||
#### Nginx 配置示例
|
||||
|
||||
```nginx
|
||||
upstream argus_backend {
|
||||
server 127.0.0.1:9484;
|
||||
keepalive 64;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
# 普通 API 路由(有超时限制)
|
||||
location /api/ {
|
||||
proxy_pass http://argus_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
# 普通请求超时设置
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# SSE 路由(特殊配置)
|
||||
location /api/v1/sse/ {
|
||||
proxy_pass http://argus_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
# SSE 专用配置
|
||||
proxy_buffering off; # 🔑 禁用缓冲
|
||||
proxy_cache off; # 禁用缓存
|
||||
proxy_read_timeout 3600s; # 1小时超时
|
||||
proxy_connect_timeout 3600s;
|
||||
chunked_transfer_encoding on; # 启用分块传输
|
||||
proxy_set_header X-Accel-Buffering no; # 禁用 Nginx 缓冲
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 架构图
|
||||
|
||||
```
|
||||
客户端
|
||||
↓
|
||||
Nginx (80端口)
|
||||
├─→ /api/* (30秒超时)
|
||||
└─→ /api/v1/sse/* (1小时超时)
|
||||
↓
|
||||
Go 服务器 (9484端口, WriteTimeout=0)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔬 其他尝试过的方案
|
||||
|
||||
### ❌ 方案 1: 中间件修改 Deadline
|
||||
|
||||
**代码**:
|
||||
```go
|
||||
func NoWriteDeadlineMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if conn, ok := getUnderlyingConn(c.Writer); ok {
|
||||
conn.SetWriteDeadline(time.Time{}) // 移除 deadline
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- 无法在不 Hijack 的情况下访问 `net.Conn`
|
||||
- Hijack 会接管连接,无法继续使用 Gin 的 Stream
|
||||
|
||||
**结论**: ❌ 不可行
|
||||
|
||||
---
|
||||
|
||||
### ❌ 方案 2: 定期写入 comment
|
||||
|
||||
**代码**:
|
||||
```go
|
||||
// 每隔一段时间写入 SSE comment 保持连接
|
||||
go func() {
|
||||
ticker := time.NewTicker(2 * time.Minute)
|
||||
for range ticker.C {
|
||||
fmt.Fprintf(w, ": keepalive\n\n")
|
||||
w.Flush()
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- WriteTimeout 是从**第一次写入**开始计时
|
||||
- 持续写入并不会重置超时
|
||||
- 5分钟后依然会断开
|
||||
|
||||
**结论**: ❌ 无效
|
||||
|
||||
---
|
||||
|
||||
## 📊 测试结果
|
||||
|
||||
### 修改前 (WriteTimeout = 5分钟)
|
||||
|
||||
```
|
||||
0:00 - 连接建立
|
||||
0:05 - keepalive
|
||||
0:10 - ping
|
||||
...
|
||||
5:00 - ❌ ERR_INCOMPLETE_CHUNKED_ENCODING
|
||||
```
|
||||
|
||||
### 修改后 (WriteTimeout = 0)
|
||||
|
||||
```
|
||||
0:00 - 连接建立
|
||||
0:05 - keepalive
|
||||
0:10 - ping
|
||||
0:15 - keepalive
|
||||
...
|
||||
60:00 - ✅ 仍然连接
|
||||
```
|
||||
|
||||
**测试命令**:
|
||||
```bash
|
||||
# 1. 启动后端
|
||||
cd D:\go-argus\src\rear
|
||||
go run main.go
|
||||
|
||||
# 2. 浏览器控制台测试
|
||||
const es = new EventSource('http://127.0.0.1:9484/api/v1/sse/connect')
|
||||
es.addEventListener('keepalive', e => console.log('keepalive:', e.data))
|
||||
es.addEventListener('ping', e => console.log('ping:', e.data))
|
||||
|
||||
# 观察是否在5分钟后仍然收到事件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 参考资料
|
||||
|
||||
### Go 官方文档
|
||||
|
||||
- [http.Server](https://pkg.go.dev/net/http#Server)
|
||||
> WriteTimeout: 写入响应的最大持续时间(从请求头读取完成到响应写入完成)。**零值表示无超时**。
|
||||
|
||||
- [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
|
||||
|
||||
### 相关问题
|
||||
|
||||
- [Gin + SSE timeout issue](https://github.com/gin-gonic/gin/issues/1287)
|
||||
- [Go http.Server WriteTimeout for SSE](https://stackoverflow.com/questions/37294519)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
### 最终方案
|
||||
|
||||
✅ **WriteTimeout = 0** (当前实现)
|
||||
|
||||
### 优点
|
||||
|
||||
1. ✅ 简单可靠,一行配置解决
|
||||
2. ✅ SSE 连接稳定,不会断开
|
||||
3. ✅ 适用于开发和小型部署
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. ⚠️ 生产环境建议使用 Nginx 反向代理
|
||||
2. ⚠️ 应用层需要自己控制超时(Context)
|
||||
3. ⚠️ 配置限流和连接数限制
|
||||
|
||||
### 是否还会断开?
|
||||
|
||||
**仍可能断开的情况**:
|
||||
|
||||
1. **IdleTimeout 触发** (10分钟无活动)
|
||||
- **解决**: 后端每 5 秒发送 keepalive ✅
|
||||
|
||||
2. **浏览器/代理超时**
|
||||
- **解决**: 前端自动重连 ✅
|
||||
|
||||
3. **网络波动**
|
||||
- **解决**: 前端自动重连 ✅
|
||||
|
||||
**正常情况**: 连接应该保持稳定,偶尔断开会自动重连
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0
|
||||
**最后更新**: 2025-10-04
|
||||
**作者**: Claude Code
|
||||
Reference in new issue
Block a user