Files
momentry_studio/docs/USER_DATABASE_ISOLATION_FIX.md
Momentry Studio 08584ecc87 docs: 确认用户数据库隔离问题
问题发现:
- Momentry Studio 硬编码 demo.sqlite
- 违反'一用户一数据库'设计原则
- 所有用户共享同一数据库

检查结果:
- Momentry Studio: 只有 1 个 demo.sqlite ❌
- MarkBase Core: 3 个用户数据库 ✅

修复方案:
1. 方案 1: 函数参数传递 (推荐)
   - db_path(username) 动态生成路径
   - 所有数据库函数添加 username 参数

2. 方案 2: 环境变量 (折中)
   - MOMENTRY_CURRENT_USER 环境变量
   - db_path() 读取环境变量

3. 方案 3: 全局状态 (Tauri State)
   - 线程安全的状态管理
   - 集中管理用户状态

受影响功能:
- 搜索历史
- 书签
- 视频标记
- 用户认证 (例外,保持共享)

工作量: 2-3 天
优先级: 高 (影响数据隔离)

文档:
- docs/USER_DATABASE_ISOLATION_FIX.md - 详细修复方案
- scripts/check_user_isolation.sh - 检查脚本
2026-10-12 14:50:34 +08:00

9.7 KiB
Raw Blame History

用户数据库隔离设计 - 问题与修复方案

发现时间: 2026-10-12
状态: ❌ 当前实现违反"一用户一数据库"原则


🔍 问题分析

设计原则

原始设计: 一个用户一个 SQLite 独立管理,不混淆

data/users/
├── demo.sqlite         # demo 用户的数据
├── admin.sqlite        # admin 用户的数据
└── user1.sqlite        # user1 用户的数据

每个用户的数据完全隔离,互不干扰。


当前实现 (错误)

Momentry Studio (src-tauri/src/db.rs):

fn db_path() -> PathBuf {
    let base = std::env::var("MOMENTRY_DATA_DIR")
        .unwrap_or_else(|_| "../data/users".to_string());
    std::fs::create_dir_all(&base).ok();
    PathBuf::from(base).join("demo.sqlite")  // ❌ 硬编码 demo
}

问题:

  • ❌ 所有用户都使用同一个 demo.sqlite
  • ❌ 没有根据当前登录用户动态选择数据库
  • ❌ 违反了"一用户一数据库"的设计原则

正确实现 (MarkBase Core)

MarkBase Core:

markbase/data/users/
├── accusys.sqlite      # accusys 用户的数据
├── demo.sqlite         # demo 用户的数据
└── credentials.sqlite  # 凭证数据库

✅ 每个用户有独立的数据库文件 ✅ 数据完全隔离


📊 影响范围

受影响的功能

  1. 搜索历史 (search_history)

    • 所有用户共享同一个搜索历史
    • 用户 A 可以看到用户 B 的搜索记录
  2. 书签 (bookmarks)

    • 所有用户共享书签
    • 混乱的书签管理
  3. 视频标记 (video_marks)

    • 所有用户的标记混在一起
    • 无法区分是谁的标记
  4. 用户认证 (app_users)

    • 用户表本身是共享的(这是合理的)
    • 但其他数据应该隔离

✅ 修复方案

方案 1: 函数参数传递 (推荐)

修改 db_path() 函数:

// 旧代码
fn db_path() -> PathBuf {
    let base = std::env::var("MOMENTRY_DATA_DIR")
        .unwrap_or_else(|_| "../data/users".to_string());
    std::fs::create_dir_all(&base).ok();
    PathBuf::from(base).join("demo.sqlite")  // ❌ 硬编码
}

// 新代码
fn db_path(username: &str) -> PathBuf {
    let base = std::env::var("MOMENTRY_DATA_DIR")
        .unwrap_or_else(|_| "../data/users".to_string());
    std::fs::create_dir_all(&base).ok();
    PathBuf::from(base).join(format!("{}.sqlite", username))  // ✅ 动态用户
}

修改所有数据库函数:

// 旧代码
pub fn get_search_history(limit: Option<u32>) -> Result<Vec<HistoryItem>, String> {
    let conn = get_conn()?;
    // ...
}

// 新代码
pub fn get_search_history(username: String, limit: Option<u32>) -> Result<Vec<HistoryItem>, String> {
    let conn = get_conn(&username)?;
    // ...
}

前端调用修改:

// 旧代码
const history = await invoke('get_search_history', { limit: 30 })

// 新代码
const username = localStorage.getItem('username') || 'demo'
const history = await invoke('get_search_history', { 
    username: username, 
    limit: 30 
})

优点:

  • ✅ 明确的用户隔离
  • ✅ 易于理解和维护
  • ✅ 类型安全

缺点:

  • ❌ 需要修改所有函数签名
  • ❌ 需要修改所有前端调用

方案 2: 环境变量 (折中)

启动时设置环境变量:

// 登录成功后设置
pub fn set_current_user(username: &str) {
    std::env::set_var("MOMENTRY_CURRENT_USER", username);
}

// db_path 使用环境变量
fn db_path() -> PathBuf {
    let base = std::env::var("MOMENTRY_DATA_DIR")
        .unwrap_or_else(|_| "../data/users".to_string());
    std::fs::create_dir_all(&base).ok();
    
    let username = std::env::var("MOMENTRY_CURRENT_USER")
        .unwrap_or_else(|_| "demo".to_string());
    
    PathBuf::from(base).join(format!("{}.sqlite", username))
}

优点:

  • ✅ 不需要修改所有函数签名
  • ✅ 前端调用不需要修改

缺点:

  • ❌ 依赖全局状态,不够安全
  • ❌ 多线程环境下可能有竞争条件
  • ❌ 切换用户时需要重启应用

方案 3: 全局状态 (Tauri 状态管理)

使用 Tauri 的 State 管理:

use std::sync::Mutex;
use tauri::State;

struct CurrentUser(Mutex<String>);

fn db_path(state: State<CurrentUser>) -> PathBuf {
    let base = std::env::var("MOMENTRY_DATA_DIR")
        .unwrap_or_else(|_| "../data/users".to_string());
    std::fs::create_dir_all(&base).ok();
    
    let username = state.0.lock().unwrap().clone();
    PathBuf::from(base).join(format!("{}.sqlite", username))
}

优点:

  • ✅ 线程安全
  • ✅ 不需要修改所有函数签名
  • ✅ 集中管理用户状态

缺点:

  • ❌ 需要在每个命令中传递 State
  • ❌ 增加了复杂度

🎯 推荐方案

推荐使用方案 1: 函数参数传递

理由:

  1. ✅ 最清晰明确的设计
  2. ✅ 完全符合"一用户一数据库"原则
  3. ✅ 无全局状态,更安全
  4. ✅ 易于测试和维护

📝 实施步骤

Phase 1: 准备

# 1. 创建备份分支
git checkout -b fix/user-database-isolation

# 2. 检查所有数据库函数
grep -n "pub fn.*Result.*String" src-tauri/src/db.rs

Phase 2: 修改核心函数

修改 db_path():

fn db_path(username: &str) -> PathBuf {
    let base = std::env::var("MOMENTRY_DATA_DIR")
        .unwrap_or_else(|_| {
            let home = dirs::home_dir()
                .expect("Failed to get home directory");
            format!("{}/momentry_studio/data/users", home.display())
        });
    std::fs::create_dir_all(&base).ok();
    PathBuf::from(base).join(format!("{}.sqlite", username))
}

fn get_conn(username: &str) -> Result<Connection, String> {
    let path = db_path(username);
    let conn = Connection::open(&path).map_err(|e| format!("Failed to open db: {}", e))?;
    conn.execute_batch("PRAGMA journal_mode=WAL;").map_err(|e| format!("WAL mode error: {}", e))?;
    Ok(conn)
}

Phase 3: 修改数据库函数

需要修改的函数列表:

  1. ✅ get_search_history(limit) → get_search_history(username, limit)
  2. ✅ save_search_history(item) → save_search_history(username, item)
  3. ✅ rename_search_history(id, title) → rename_search_history(username, id, title)
  4. ✅ pin_search_history(id, pinned) → pin_search_history(username, id, pinned)
  5. ✅ delete_search_history(id) → delete_search_history(username, id)
  6. ✅ get_bookmarks() → get_bookmarks(username)
  7. ✅ save_bookmark(item) → save_bookmark(username, item)
  8. ✅ delete_bookmark(id) → delete_bookmark(username, id)
  9. ✅ get_video_marks(file_uuid) → get_video_marks(username, file_uuid)
  10. ✅ save_video_mark(mark) → save_video_mark(username, mark)

例外:

  • ❌ init_db() - 初始化用户数据库,不传递用户名
  • ❌ login() - 用户认证,使用全局 app_users 表
  • ❌ verify_token() - 验证 token,使用全局 app_users 表

Phase 4: 修改前端调用

创建统一工具函数:

// src/utils/db.ts
export function getCurrentUsername(): string {
  return localStorage.getItem('username') || 'demo'
}

export async function getSearchHistory(limit?: number) {
  const username = getCurrentUsername()
  return await invoke('get_search_history', { username, limit })
}

export async function saveSearchHistory(item: HistoryItem) {
  const username = getCurrentUsername()
  return await invoke('save_search_history', { username, item })
}

// ... 其他函数

Phase 5: 数据迁移

将现有数据迁移到用户数据库:

# 1. 备份现有数据库
cp data/users/demo.sqlite data/users/demo.sqlite.backup

# 2. 重命名为正确的用户数据库
mv data/users/demo.sqlite data/users/{actual_username}.sqlite

# 3. 或保持 demo 用户
# 不需要迁移,demo.sqlite 保持不变

Phase 6: 测试

测试场景:

  1. ✅ demo 用户登录,数据正确
  2. ✅ 切换到 admin 用户,数据隔离
  3. ✅ 新用户注册,自动创建数据库
  4. ✅ 多用户同时使用,数据不混淆

⚠️ 注意事项

1. 用户认证数据库

app_users 表应该保持共享,用于用户认证:

data/users/
├── app_users.sqlite     # 共享的用户认证数据库
├── demo.sqlite          # demo 用户的私有数据
└── admin.sqlite         # admin 用户的私有数据

或者:

data/users/
├── demo.sqlite          # 包含 app_users 表 + 私有表
└── admin.sqlite         # 包含 app_users 表 + 私有表

2. 向后兼容

需要考虑:

  • 现有 demo.sqlite 如何处理
  • 是否需要数据迁移脚本
  • API 变更对前端的影响

3. 数据库初始化

新用户注册时:

pub fn init_user_db(username: &str) -> Result<(), String> {
    let path = db_path(username);
    if !path.exists() {
        let conn = Connection::open(&path)?;
        conn.execute_batch(APP_TABLES)?;
    }
    Ok(())
}

📊 影响评估

影响项 范围 工作量
db.rs 修改 10+ 函数 中等
前端修改 20+ 调用点 中等
测试 功能测试 + 集成测试 中等
文档更新 API 文档 + 用户指南 低
数据迁移 可选 低

预计工作量: 2-3 天


🎯 总结

问题

  • ❌ 当前硬编码 demo.sqlite,违反"一用户一数据库"原则
  • ❌ 所有用户共享数据,不符合设计

解决方案

  • ✅ 方案 1: 函数参数传递 (推荐)
  • ✅ 方案 2: 环境变量 (折中)
  • ✅ 方案 3: 全局状态 (Tauri State)

实施优先级

  • 🔴 高 - 影响数据隔离和安全性
  • 建议在下一次重构时实施

文档位置: docs/USER_DATABASE_ISOLATION_FIX.md
状态: ❌ 需要修复
优先级: 高