ca4a0b1303
- 用户数据隔离:所有用户数据按 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
184 lines
6.2 KiB
Markdown
184 lines
6.2 KiB
Markdown
# 用户数据隔离改造方案
|
||
|
||
## 背景
|
||
|
||
当前所有本地数据存储在全局路径下,切换账号后 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** 版本中实施
|
||
- 最好在素材库数据量还不大的早期做,迁移成本低
|