# 用户数据隔离改造方案 ## 背景 当前所有本地数据存储在全局路径下,切换账号后 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`:从全局状态或调用方获取当前 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` 参数 ```rust #[tauri::command] pub async fn load_voice_materials(user_id: String) -> ApiResponse> { let path = get_user_voices_json_path(&user_id)?; // ... } ``` 方式 B:命令内部从 AppHandle 全局状态读取当前 user_id ```rust let user_id = app.state::().id.clone(); ``` **推荐方式 A**,前端调用时传入 `user.id`,减少 Rust 层对全局状态的依赖,更明确。 ### 3. 前端 — AuthStore 改造(`src/store/authStore.ts`) **新增:** - 登录成功后,写入一个全局标记 `CURRENT_USER_ID = user.id` - 登出/被踢时,清除该标记 - 所有调用 Rust 存储命令的地方,带上 `userId` 参数 **受影响的 Store:** - `voiceStore.ts` — `loadVoiceMaterials()`、`addVoiceMaterial()` 等需要传 `userId` - `projectStore.ts` — 项目操作需要传 `userId` - `avatarStore.ts` — 封面头像操作需要传 `userId` ### 4. 前端 — API 调用改造 所有 `invoke('xxx', { ... })` 调用用户相关命令时,增加 `userId`: ```typescript // 改造前 await invoke('load_voice_materials') // 改造后 const userId = useAuthStore.getState().user?.id; await invoke('load_voice_materials', { userId }) ``` ### 5. 数据迁移策略 **检测时机:** 应用启动时,`init_app_data_dir` 之后 **迁移逻辑(Rust):** ```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 = 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.0** 或 **v1.9.0** 版本中实施 - 最好在素材库数据量还不大的早期做,迁移成本低