问题发现: - 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 - 检查脚本
9.7 KiB
9.7 KiB
用户数据库隔离设计 - 问题与修复方案
发现时间: 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 # 凭证数据库
✅ 每个用户有独立的数据库文件 ✅ 数据完全隔离
📊 影响范围
受影响的功能
-
搜索历史 (
search_history)- 所有用户共享同一个搜索历史
- 用户 A 可以看到用户 B 的搜索记录
-
书签 (
bookmarks)- 所有用户共享书签
- 混乱的书签管理
-
视频标记 (
video_marks)- 所有用户的标记混在一起
- 无法区分是谁的标记
-
用户认证 (
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: 函数参数传递
理由:
- ✅ 最清晰明确的设计
- ✅ 完全符合"一用户一数据库"原则
- ✅ 无全局状态,更安全
- ✅ 易于测试和维护
📝 实施步骤
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: 修改数据库函数
需要修改的函数列表:
- ✅
get_search_history(limit) → get_search_history(username, limit) - ✅
save_search_history(item) → save_search_history(username, item) - ✅
rename_search_history(id, title) → rename_search_history(username, id, title) - ✅
pin_search_history(id, pinned) → pin_search_history(username, id, pinned) - ✅
delete_search_history(id) → delete_search_history(username, id) - ✅
get_bookmarks() → get_bookmarks(username) - ✅
save_bookmark(item) → save_bookmark(username, item) - ✅
delete_bookmark(id) → delete_bookmark(username, id) - ✅
get_video_marks(file_uuid) → get_video_marks(username, file_uuid) - ✅
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: 测试
测试场景:
- ✅ demo 用户登录,数据正确
- ✅ 切换到 admin 用户,数据隔离
- ✅ 新用户注册,自动创建数据库
- ✅ 多用户同时使用,数据不混淆
⚠️ 注意事项
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
状态: ❌ 需要修复
优先级: 高