# 用户数据库隔离设计 - 问题与修复方案 **发现时间**: 2026-10-12 **状态**: ❌ 当前实现违反"一用户一数据库"原则 --- ## 🔍 问题分析 ### 设计原则 **原始设计**: 一个用户一个 SQLite 独立管理,不混淆 ``` data/users/ ├── demo.sqlite # demo 用户的数据 ├── admin.sqlite # admin 用户的数据 └── user1.sqlite # user1 用户的数据 ``` 每个用户的数据完全隔离,互不干扰。 --- ### 当前实现 (错误) **Momentry Studio** (`src-tauri/src/db.rs`): ```rust 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() 函数**: ```rust // 旧代码 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)) // ✅ 动态用户 } ``` **修改所有数据库函数**: ```rust // 旧代码 pub fn get_search_history(limit: Option) -> Result, String> { let conn = get_conn()?; // ... } // 新代码 pub fn get_search_history(username: String, limit: Option) -> Result, String> { let conn = get_conn(&username)?; // ... } ``` **前端调用修改**: ```typescript // 旧代码 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: 环境变量 (折中) **启动时设置环境变量**: ```rust // 登录成功后设置 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 管理**: ```rust use std::sync::Mutex; use tauri::State; struct CurrentUser(Mutex); fn db_path(state: State) -> 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: 准备 ```bash # 1. 创建备份分支 git checkout -b fix/user-database-isolation # 2. 检查所有数据库函数 grep -n "pub fn.*Result.*String" src-tauri/src/db.rs ``` --- ### Phase 2: 修改核心函数 **修改 db_path()**: ```rust 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 { 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: 修改前端调用 **创建统一工具函数**: ```typescript // 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: 数据迁移 **将现有数据迁移到用户数据库**: ```bash # 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. 数据库初始化 **新用户注册时**: ```rust 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` **状态**: ❌ 需要修复 **优先级**: 高