# 用户数据隔离改造方案

## 背景

当前所有本地数据存储在全局路径下，切换账号后 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** 版本中实施
- 最好在素材库数据量还不大的早期做，迁移成本低
