Files
meijiaka-zy/docs/user-data-isolation-plan.md
T
小鱼开发 ca4a0b1303 feat: 用户数据隔离、动态分辨率、字幕缩放、多项体验优化
- 用户数据隔离:所有用户数据按 users/{user_id}/ 隔离,Rust IPC 命令自治读取 auth.json
- 安全加固:delete_local_product/rename_local_product/export_product 增加前缀校验
- 移除音调(pitch)功能:从 VoiceSynthesis、projectStore、types 等完全移除
- 动态视频分辨率:根据素材最小高度自动选择 720p/1080p,9:16 比例强校验
- ASS 字幕按目标分辨率等比例缩放(720p 和 1080p 视觉一致)
- Canvas 预览支持参数化 playResY,预览与压制效果一致
- 配音合成增加台词字数校验弹窗(语速>1.0时要求更多字)
- BGM 默认音量从 25% 调至 15%
- 素材选择提示文案更新(9:16 比例,5-60秒)
- 视频校验从严格 1080x1920 改为 9:16 比例判断
- 背景图片弹窗宽度从 440px 放大到 560px
2026-06-04 17:30:54 +08:00

6.2 KiB
Raw Blame History

用户数据隔离改造方案

背景

当前所有本地数据存储在全局路径下,切换账号后 A 用户的数据对 B 用户可见。涉及隐私(音色素材)和商业数据(项目文件),需要按 user_id 隔离。

目标

  • 按登录用户的 user_id 隔离本地数据
  • 未登录时使用匿名沙盒或禁止写入
  • 兼容旧数据,首次启动自动迁移

改动范围

1. Rust 层 — 路径改造(src/storage/paths.rs

新增用户隔离根目录,用户相关数据全部迁移:

{app_local_data_dir}/
  users/
    {user_id}/
      voices.json           ← 音色素材库
      cover_avatars.json    ← 封面头像库
      projects/             ← 项目目录
      config.json           ← 用户级配置(可选)
  bgm_cache/                ← 全局共享(缓存无隐私问题)
  config.json               ← 全局配置(窗口状态、偏好设置等)
  temp/                     ← 临时文件(定时清理)

需要修改的函数:

当前路径 改造后
{data}/voices.json {data}/users/{user_id}/voices.json
{data}/cover_avatars.json {data}/users/{user_id}/cover_avatars.json
{data}/cover_avatars/ {data}/users/{user_id}/cover_avatars/
{data}/projects/ {data}/users/{user_id}/projects/
{data}/config.json 保持全局(或拆分为全局+用户级)
{data}/bgm_cache/ 保持全局
{config}/auth.json 保持全局(只存当前登录态)

新增:

  • get_current_user_id() -> Option<String>:从全局状态或调用方获取当前 user_id
  • get_user_data_dir(user_id: &str):返回用户隔离目录

2. Rust 层 — 存储命令改造

受影响的命令文件:

  • src/commands/voice.rs — load/save/delete voice materials
  • src/commands/project.rs — 项目 CRUD
  • src/commands/avatar.rs — 封面头像
  • src/commands/config.rs — 配置(决定全局 or 用户级)

改造方式:

方式 A(推荐):命令函数签名增加 user_id: String 参数

#[tauri::command]
pub async fn load_voice_materials(user_id: String) -> ApiResponse<Vec<VoiceMaterial>> {
    let path = get_user_voices_json_path(&user_id)?;
    // ...
}

方式 B:命令内部从 AppHandle 全局状态读取当前 user_id

let user_id = app.state::<CurrentUser>().id.clone();

推荐方式 A,前端调用时传入 user.id,减少 Rust 层对全局状态的依赖,更明确。

3. 前端 — AuthStore 改造(src/store/authStore.ts

新增:

  • 登录成功后,写入一个全局标记 CURRENT_USER_ID = user.id
  • 登出/被踢时,清除该标记
  • 所有调用 Rust 存储命令的地方,带上 userId 参数

受影响的 Store

  • voiceStore.tsloadVoiceMaterials()addVoiceMaterial() 等需要传 userId
  • projectStore.ts — 项目操作需要传 userId
  • avatarStore.ts — 封面头像操作需要传 userId

4. 前端 — API 调用改造

所有 invoke('xxx', { ... }) 调用用户相关命令时,增加 userId

// 改造前
await invoke('load_voice_materials')

// 改造后
const userId = useAuthStore.getState().user?.id;
await invoke('load_voice_materials', { userId })

5. 数据迁移策略

检测时机: 应用启动时,init_app_data_dir 之后

迁移逻辑(Rust):

pub fn migrate_legacy_data(data_dir: &Path) -> Result<(), StorageError> {
    // 1. 检查是否存在旧数据(全局 voices.json / projects / cover_avatars.json
    let legacy_voices = data_dir.join("voices.json");
    if !legacy_voices.exists() {
        return Ok(()); // 无旧数据,跳过
    }

    // 2. 读取 auth.json 获取当前登录的 user_id
    let auth_path = get_auth_state_path(app)?;
    let auth: Option<AuthState> = read_json(&auth_path)?;
    let user_id = match auth.and_then(|a| a.user).map(|u| u.id) {
        Some(id) => id,
        None => {
            // 未登录但有旧数据:移到 anonymous/ 目录,提示用户登录后迁移
            return move_to_anonymous(data_dir);
        }
    };

    // 3. 创建用户目录并迁移
    let user_dir = data_dir.join("users").join(&user_id);
    ensure_dir(&user_dir)?;

    // 迁移 voices.json
    if legacy_voices.exists() {
        fs::rename(&legacy_voices, user_dir.join("voices.json"))?;
    }

    // 迁移 cover_avatars.json + cover_avatars/
    let legacy_avatars = data_dir.join("cover_avatars.json");
    if legacy_avatars.exists() {
        fs::rename(&legacy_avatars, user_dir.join("cover_avatars.json"))?;
    }
    let legacy_avatars_dir = data_dir.join("cover_avatars");
    if legacy_avatars_dir.exists() {
        fs::rename(&legacy_avatars_dir, user_dir.join("cover_avatars"))?;
    }

    // 迁移 projects/
    let legacy_projects = data_dir.join("projects");
    if legacy_projects.exists() {
        fs::rename(&legacy_projects, user_dir.join("projects"))?;
    }

    // 4. 写迁移标记,避免重复迁移
    let flag = data_dir.join(".migration_v1_done");
    fs::write(&flag, "done")?;

    Ok(())
}

迁移顺序:

  1. 启动应用
  2. 初始化 app_local_data_dir
  3. 检测 .migration_v1_done 标记,不存在则执行迁移
  4. 迁移完成后继续正常启动

工作量评估

模块 文件数 预估工作量
paths.rs 改造 1 2h
Rust 存储命令加 user_id 参数 ~8 个文件 4h
lib.rs 注册新签名 1 0.5h
前端 Store 改造 ~4 个文件 3h
数据迁移逻辑 1 个新文件 3h
测试验证 2h
总计 ~14.5h

风险点

  1. 多设备登录同一账号:数据只在本地隔离,不同设备间数据不互通。如果需要跨设备同步,需要后端支持。
  2. 匿名数据:未登录时产生的数据(理论上目前不存在,因为功能都需要登录),需要决定是禁止未登录操作还是存到 anonymous/ 目录。
  3. 回滚:迁移是单向的(移动文件),回滚需要手动恢复。建议迁移前做备份副本而非直接 move。

建议实施时机

  • v1.8.0v1.9.0 版本中实施
  • 最好在素材库数据量还不大的早期做,迁移成本低