feat: 扫描整理并行化、WebSocket 实时进度与统计看板

- 移动阶段并行处理文件,按原子计数上报累计进度
- 扫描遍历阶段实时上报已发现文件数,界面不再卡在 0/0
- 新增 /api/scan/ws WebSocket 实时推送扫描状态;前端断线指数退避自动重连,并以低频轮询兜底
- 新增统计看板与媒体库筛选/排序,目录选择器路径不存在时回退到最近可用父目录
- 存放目录支持自定义并持久化,默认输出到 KMP已整理
This commit is contained in:
2025-05-08 21:16:20 +08:00
parent 36d7ecced8
commit f915a9b1ac
22 changed files with 1342 additions and 281 deletions
+52
View File
@@ -0,0 +1,52 @@
// 状态订阅发布:将 ScanService 的状态变更实时推送给订阅者(WebSocket 等)。
package service
import "sync"
// StatusListener 状态订阅通道(缓冲 1,保底容纳一条未消费状态)
type StatusListener chan ScanStatus
// StatusHub 管理状态订阅者;发布时若消费者来不及消费则丢弃旧消息,
// 保证订阅者拿到的始终是最新状态。
type StatusHub struct {
mu sync.RWMutex
subs map[StatusListener]struct{}
}
// NewStatusHub 创建状态订阅中心
func NewStatusHub() *StatusHub {
return &StatusHub{subs: make(map[StatusListener]struct{})}
}
// Subscribe 注册订阅,返回频道与取消函数。取消后频道被关闭,写方需停止发送。
func (h *StatusHub) Subscribe() (StatusListener, func()) {
h.mu.Lock()
defer h.mu.Unlock()
ch := make(StatusListener, 1)
h.subs[ch] = struct{}{}
return ch, func() {
h.mu.Lock()
if _, ok := h.subs[ch]; ok {
delete(h.subs, ch)
close(ch)
}
h.mu.Unlock()
}
}
// Publish 向所有订阅者推送最新状态(非阻塞;缓冲满时覆盖为最新状态)
func (h *StatusHub) Publish(st ScanStatus) {
h.mu.RLock()
defer h.mu.RUnlock()
for ch := range h.subs {
select {
case ch <- st:
default:
select {
case <-ch:
default:
}
ch <- st
}
}
}