新增: SFTP直连+网站预览+OSS区域嗅探+热键+BGM播放
This commit is contained in:
114
docs/04-功能迭代/GO-DESK-10.SFTP直连支持/开发经验.md
Normal file
114
docs/04-功能迭代/GO-DESK-10.SFTP直连支持/开发经验.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# SFTP 直连 + autoConnect 开发经验
|
||||
|
||||
> 日期:2026-05-04 | 对应分支:fs-only-v3
|
||||
|
||||
---
|
||||
|
||||
## 架构决策
|
||||
|
||||
### 1. 连接池模式替代单连接
|
||||
|
||||
**背景**: 原方案切换 profile 时断开旧连接再建新连接,切换慢且丢失状态。
|
||||
|
||||
**决策**: `Map<profileId, FsTransport>` 连接池。所有 profile 可同时在线,切换为 O(1)。
|
||||
|
||||
**关键实现**:
|
||||
- `buildAndPool()`: 创建 transport 并入池
|
||||
- `connect()`: 池中已有则直接复用,否则新建
|
||||
- `disconnectProfile(id)`: 断开指定 profile,从池移除
|
||||
- `disconnectAll()`: 清空池,保留 local
|
||||
|
||||
### 2. 文件服务器 URL 集中管理
|
||||
|
||||
**背景**: 前端 8+ 处硬编码 `localhost:2652`,端口冲突时全部失效。
|
||||
|
||||
**决策**: 新建 `file-server.ts`,从后端动态获取 URL。
|
||||
|
||||
**原则**: **单一数据源**。`connectionManager` 不再缓存 URL,所有模块从 `file-server.ts` 读取。
|
||||
|
||||
### 3. 端口自动回退
|
||||
|
||||
**背景**: 端口被占用时应用崩溃,用户必须手动杀进程。
|
||||
|
||||
**决策**: `listenWithFallback(basePort, handler)` 尝试 basePort + 0..9,直接 `srv.Serve(l)` 消除 TOCTOU。
|
||||
|
||||
**关键**: 不用 `Listen → Close → ListenAndServe`(有竞态),改为把 listener 传给 `Serve`。
|
||||
|
||||
---
|
||||
|
||||
## 踩坑记录
|
||||
|
||||
### 踩坑 1: autoConnect 不工作 — 三层嵌套根因
|
||||
|
||||
**现象**: 开启"启动时自动连接",重启后服务器不连接。手动点击则正常。
|
||||
|
||||
**排查过程**:
|
||||
1. 加诊断日志 → 发现 `loadFromDB` 正常执行,DB 返回 4 条记录
|
||||
2. 日志显示 `lastConnected: null`(所有 profile)→ `lc: false`
|
||||
3. 原因链:
|
||||
- Go JSON tag 是 `last_connected`,前端读 `p.lastConnected` → **字段名不匹配**
|
||||
- `SaveProfileRequest` 没有 `LastConnected` 字段 → **从未持久化**
|
||||
- autoConnect 守卫 `p.lastConnected` → 即使修了前两层,旧数据仍是 null → **守卫逻辑有误**
|
||||
|
||||
**修复**:
|
||||
```ts
|
||||
// 1. 字段名 fallback
|
||||
lastConnected: p.lastConnected || p.last_connected ? ... : undefined
|
||||
// 2. persistProfile 传递 lastConnected
|
||||
lastConnected: profile.lastConnected ? Math.floor(profile.lastConnected / 1000) : undefined
|
||||
// 3. 去掉 lastConnected 守卫(核心修复)
|
||||
if (p.type !== 'local') { // 原来是 p.type !== 'local' && p.lastConnected
|
||||
```
|
||||
|
||||
**教训**: 数据链路问题要追踪完整链路:前端写入 → Go 接收 → DB 存储 → DB 读取 → Go 返回 → 前端读取。每一步都可能有字段名/类型/单位不匹配。
|
||||
|
||||
### 踩坑 2: Settings 弹窗被 overflow:hidden 裁剪
|
||||
|
||||
**现象**: 点击表头 `···` 按钮,弹窗不出现。
|
||||
|
||||
**根因**: `settings-panel` 在 `server-content`(`overflow: hidden`)内部。
|
||||
|
||||
**修复**: DOM 上移到 `server-content` 外部,改用 `position: absolute` 相对 `sidebar-section`。
|
||||
|
||||
### 踩坑 3: More-menu 被收藏夹遮挡
|
||||
|
||||
**现象**: 最后一个服务器行的 `···` 菜单被收藏夹区块盖住。
|
||||
|
||||
**根因**: `.section-content` 有 `overflow: hidden`(用于折叠动画),裁剪了 `.more-menu`。同时服务器 section 的 z-index 低于收藏夹。
|
||||
|
||||
**修复**:
|
||||
- `.server-content` 加 `overflow: visible` 覆盖通用规则
|
||||
- 服务器 section 在菜单打开时加 `z-index: 30`(`.section-on-top` 类)
|
||||
|
||||
### 踩坑 4: require() 在 Vite ESM 构建中不可靠
|
||||
|
||||
**现象**: `const { getFileServerBaseURL } = require('./file-server')` 在 dev 模式正常,production build 失败。
|
||||
|
||||
**修复**: 全部改为 ES 静态 `import`。
|
||||
|
||||
### 踩坑 5: Go time.Unix 返回值不能直接赋给 *time.Time
|
||||
|
||||
```go
|
||||
// ❌ 编译错误
|
||||
p.LastConnected = time.Unix(*req.LastConnected, 0)
|
||||
|
||||
// ✅ 中间变量
|
||||
t := time.Unix(*req.LastConnected, 0)
|
||||
p.LastConnected = &t
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### SFTP 二进制文件写入
|
||||
前端剪贴板图片 → `canvas.toDataURL()` 得到 base64 → `SftpWriteBase64File` Go binding → base64 解码 → SFTP Create + Write。
|
||||
|
||||
### CSS overflow 与弹窗
|
||||
`overflow: hidden` 用于折叠动画时,会裁剪子元素的 `position: absolute` 弹窗。解法:弹窗放在 overflow 容器外部,用 `position: absolute` 相对最近的 `position: relative` 祖先定位。
|
||||
|
||||
### z-index 层级管理
|
||||
- 基础层: 1-10
|
||||
- 弹出菜单/面板: 20-30
|
||||
- 模态/对话框: 40-50
|
||||
- 确保弹出元素的父容器也有足够的 z-index
|
||||
Reference in New Issue
Block a user