Files
meijiaka-zy/docs/user-data-isolation-plan.md
小鱼开发 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

184 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 用户数据隔离改造方案
## 背景
当前所有本地数据存储在全局路径下,切换账号后 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` 参数
```rust
#[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
```rust
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.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<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.0** 或 **v1.9.0** 版本中实施
- 最好在素材库数据量还不大的早期做,迁移成本低