Private
Public Access
1
0
Files
u-desk/docs/04-功能迭代/GO-DESK-10.SFTP直连支持/开发经验.md

4.2 KiB
Raw Blame History

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(所有 profilelc: false
  3. 原因链:
    • Go JSON tag 是 last_connected,前端读 p.lastConnected字段名不匹配
    • SaveProfileRequest 没有 LastConnected 字段 → 从未持久化
    • autoConnect 守卫 p.lastConnected → 即使修了前两层,旧数据仍是 null → 守卫逻辑有误

修复:

// 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-panelserver-contentoverflow: hidden)内部。

修复: DOM 上移到 server-content 外部,改用 position: absolute 相对 sidebar-section

踩坑 3: More-menu 被收藏夹遮挡

现象: 最后一个服务器行的 ··· 菜单被收藏夹区块盖住。

根因: .section-contentoverflow: hidden(用于折叠动画),裁剪了 .more-menu。同时服务器 section 的 z-index 低于收藏夹。

修复:

  • .server-contentoverflow: 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

// ❌ 编译错误
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