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

408 lines
9.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 用户数据库隔离设计 - 问题与修复方案
**发现时间**: 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<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)?;
// ...
}
```
**前端调用修改**:
```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<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: 准备
```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<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: 修改前端调用
**创建统一工具函数**:
```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`
**状态**: ❌ 需要修复
**优先级**: 高