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 - 检查脚本
This commit is contained in:
408
docs/USER_DATABASE_ISOLATION_FIX.md
Normal file
408
docs/USER_DATABASE_ISOLATION_FIX.md
Normal file
@@ -0,0 +1,408 @@
|
||||
# 用户数据库隔离设计 - 问题与修复方案
|
||||
|
||||
**发现时间**: 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`
|
||||
**状态**: ❌ 需要修复
|
||||
**优先级**: 高
|
||||
Reference in New Issue
Block a user