design.md 6.8 KB

Context

TTMusic 的歌单数据存储在两个本地 RDB 数据库中:

  • PlaylistStore.db:包含 playlistTable(歌单信息)和 playlistSongTable(歌单-歌曲关联)
  • mediaDB.db:包含 mediaTable(歌曲元数据),歌单通过 songFilePath 关联到歌曲

当前没有任何备份机制。应用已有完善的远程网盘集成(RemoteDriveManager 支持 WebDAV、SMB、百度网盘等 13 种协议),以及基于服务器 API(pay.ss5.xyz)的 VIP 会员体系。VIP 状态通过 PreferencesUtil.getBooleanSync('hasActiveSubscription') 判断。

Goals / Non-Goals

Goals:

  • 用户可以将全部歌单数据导出为单个 JSON 文件到本地存储
  • 用户可以从 JSON 备份文件导入/恢复歌单数据
  • 用户可以将备份文件上传到已配置的 WebDAV 账户
  • 用户可以从 WebDAV 账户下载并恢复备份
  • VIP 用户可以启用自动备份到服务器
  • 在设置页面提供统一的备份管理入口

Non-Goals:

  • 不备份歌曲音频文件本身,仅备份歌单结构和歌曲元数据
  • 不支持增量备份,每次备份为全量快照
  • 不支持实时多设备同步(仅手动或定时备份+恢复)
  • 不支持非 WebDAV 类型的远程网盘备份(SMB/FTP/百度等留待后续扩展)
  • 不修改现有数据库表结构

Decisions

D1: 备份文件格式 — JSON

选择: 使用单个 JSON 文件作为备份格式

备选方案:

  • SQLite DB 文件直接拷贝:简单但不可读、版本兼容性差
  • Protocol Buffers:性能好但增加依赖、不可人工编辑
  • JSON:可读、无额外依赖、易于调试和版本迁移

理由: JSON 格式与项目现有的数据交换模式一致(ConfigManager、API 响应均为 JSON),ArkTS 原生支持 JSON.stringify/JSON.parse,无需引入新依赖。

格式结构:

{
  "version": 1,
  "exportTime": "2026-02-08T12:00:00.000Z",
  "appVersion": "1.0.0",
  "playlists": [
    {
      "id": "...",
      "name": "...",
      "coverPath": "...",
      "description": "...",
      "createTime": "...",
      "updateTime": "...",
      "songCount": 10,
      "sortOrder": 0,
      "songs": [
        {
          "songFilePath": "...",
          "addTime": "...",
          "sortOrder": 0
        }
      ]
    }
  ]
}

D2: 导入合并策略 — 按名称匹配 + 用户选择

选择: 导入时按歌单名称检测冲突,让用户选择「覆盖」「跳过」或「重命名」

备选方案:

  • 按 ID 匹配:ID 是 Date.now() + random,不同设备几乎不会冲突,但无法识别逻辑重复
  • 全量覆盖:简单但会丢失用户修改
  • 按名称匹配 + 用户选择:最灵活

理由: 歌单名称对用户有实际意义,同名歌单大概率是同一歌单的不同版本。提供选择权避免意外数据丢失。

D3: WebDAV 备份路径 — 固定子目录

选择: 备份文件存放在 WebDAV 账户的 /TTMusic/backups/ 固定目录下

理由: 使用固定路径避免用户需要手动选择目录,同时与用户的音乐文件隔离。文件名格式为 playlist_backup_YYYYMMDD_HHmmss.json,便于识别和管理。

D4: 服务器自动备份 — 复用已有 API 体系

选择: 通过 pay.ss5.xyz 服务器 API 上传/下载备份数据,需要 userToken 认证

API 设计:

  • POST /backup/playlist/upload — 上传备份 JSON(需 token + VIP)
  • GET /backup/playlist/download — 下载最新备份(需 token + VIP)
  • GET /backup/playlist/list — 获取备份历史列表(需 token + VIP)

理由: 复用已有的用户认证体系(userToken)和服务器架构,与现有 VIP 功能(会员信息、支付等)保持一致。

D5: 自动备份触发时机 — 歌单变更后延迟上传

选择: 在歌单增删改操作后设置 5 分钟延迟窗口,窗口内多次变更合并为一次上传

备选方案:

  • 即时上传:频繁操作时产生大量请求
  • 固定间隔轮询:浪费资源且实时性差
  • 延迟合并:兼顾实时性和效率

理由: 用户整理歌单时通常连续操作(添加多首歌、创建多个歌单),延迟合并避免频繁网络请求。使用 setTimeout + 标志位实现防抖。

D6: 核心模块设计 — PlaylistBackupManager

选择: 新建 PlaylistBackupManager 类统一管理所有备份相关逻辑

模块职责:

  • exportToJson(): 从 PlaylistTable 读取全部歌单数据,序列化为 JSON
  • importFromJson(): 解析 JSON,校验格式版本,写入 PlaylistTable
  • saveToLocal(): 使用 DocumentViewPicker 让用户选择保存位置
  • loadFromLocal(): 使用 DocumentViewPicker 让用户选择备份文件
  • uploadToWebDav(): 通过 RemoteDriveManagerrcpSocket 上传到 WebDAV
  • downloadFromWebDav(): 从 WebDAV 下载备份文件
  • uploadToServer(): VIP 用户上传到服务器 API
  • downloadFromServer(): VIP 用户从服务器下载
  • scheduleAutoBackup(): 管理自动备份的防抖定时器

理由: 集中管理避免备份逻辑散落在多个文件中,便于测试和维护。

Risks / Trade-offs

[风险] 大量歌单导出的内存占用 → 分批序列化歌单数据,避免一次性将所有歌单加载到内存中。对于超过 100 个歌单的场景进行分页查询。

[风险] WebDAV 服务器兼容性 → 不同 WebDAV 服务器对大文件上传行为不一致。备份文件通常较小(几十 KB 到几 MB),风险可控。使用现有 rcpSocket.uploadFile 方法,已验证兼容性。

[风险] 备份文件版本不兼容 → JSON 格式包含 version 字段。导入时检查版本号,不兼容的版本给出明确提示。当前版本为 1,预留升级空间。

[风险] 自动备份消耗流量 → 仅 VIP 用户启用,且使用防抖合并。备份数据为纯文本 JSON,体积较小。设置页面提供开关让用户控制。

[风险] 服务器 API 需要后端配合 → 服务器端 API 需要新增三个端点。可以先实现本地导出/导入 + WebDAV 备份,服务器自动备份作为第二阶段上线。

[取舍] 不备份歌曲音频文件 → 降低了备份完整性,但大幅减少备份体积和时间。歌曲文件可以从原始来源重新获取(本地扫描或 WebDAV)。

[取舍] 不支持增量备份 → 实现简单,但每次备份为全量。鉴于歌单数据量通常较小(几十个歌单,几千首歌曲关联),全量备份的开销完全可接受。

Open Questions

  • Q1: 服务器自动备份的 API 端点是否需要额外的签名验证机制(类似 deleteAccount 中的签名方式)?
  • Q2: 备份文件中是否需要包含部分歌曲元数据(如歌名、艺术家)以便在目标设备没有对应歌曲文件时仍能显示歌单内容?
  • Q3: WebDAV 备份是否应该支持自动定时备份(不仅限于服务器备份),还是仅支持手动触发?