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:
2026-10-12 14:50:34 +08:00
parent 88151b2666
commit 08584ecc87
2 changed files with 534 additions and 0 deletions

View 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`
**状态**: ❌ 需要修复
**优先级**: 高

126
scripts/check_user_isolation.sh Executable file
View File

@@ -0,0 +1,126 @@
#!/bin/bash
# 用户数据库隔离检查脚本
echo "================================================================================
用户数据库隔离检查
================================================================================
"
USER_HOME=$(eval echo ~${SUDO_USER:-$USER})
echo "1. 检查数据库文件"
echo "--------------------------------------------------------------------------------"
# 检查 Momentry Studio 数据库
STUDIO_DB_DIR="/Users/accusys/momentry_studio/data/users"
if [ -d "$STUDIO_DB_DIR" ]; then
echo "Momentry Studio 数据库目录:"
ls -lh "$STUDIO_DB_DIR"/*.sqlite 2>/dev/null || echo " ❌ 无数据库文件"
echo ""
echo "数据库数量: $(ls "$STUDIO_DB_DIR"/*.sqlite 2>/dev/null | wc -l | tr -d ' ')"
# 检查是否只有一个 demo.sqlite
if [ -f "$STUDIO_DB_DIR/demo.sqlite" ] && [ $(ls "$STUDIO_DB_DIR"/*.sqlite 2>/dev/null | wc -l) -eq 1 ]; then
echo " ⚠️ 只有 demo.sqlite,违反'一用户一数据库'原则"
fi
else
echo "❌ Momentry Studio 数据库目录不存在"
fi
echo ""
# 检查 MarkBase 数据库
MARKBASE_DB_DIR="${USER_HOME}/markbase/data/users"
if [ -d "$MARKBASE_DB_DIR" ]; then
echo "MarkBase 数据库目录:"
ls -lh "$MARKBASE_DB_DIR"/*.sqlite 2>/dev/null
echo ""
echo "数据库数量: $(ls "$MARKBASE_DB_DIR"/*.sqlite 2>/dev/null | wc -l | tr -d ' ')"
echo " ✅ 多个用户数据库,符合'一用户一数据库'原则"
else
echo "❌ MarkBase 数据库目录不存在"
fi
echo ""
echo "2. 检查代码实现"
echo "--------------------------------------------------------------------------------"
# 检查 db.rs 中的 db_path 函数
if grep -q 'join("demo.sqlite")' /Users/accusys/momentry_studio/src-tauri/src/db.rs 2>/dev/null; then
echo "❌ db.rs 硬编码 demo.sqlite:"
grep -n 'join("demo.sqlite")' /Users/accusys/momentry_studio/src-tauri/src/db.rs
echo ""
echo " 违反了'一用户一数据库'原则"
else
echo "✅ db.rs 实现动态用户数据库路径"
fi
echo ""
echo "3. 检查当前登录用户"
echo "--------------------------------------------------------------------------------"
# 从 localStorage 获取当前用户(需要前端运行)
if pgrep -f "momentry-studio\|momentry-proxy" >/dev/null 2>&1; then
echo "应用正在运行"
echo " ⚠️ 无法直接检查 localStorage,需要手动验证"
echo " 建议: 检查前端 localStorage.getItem('username')"
else
echo "应用未运行"
fi
echo ""
echo "================================================================================
问题总结
================================================================================
"
echo "当前状态:"
echo ""
echo "Momentry Studio:"
if [ -f "$STUDIO_DB_DIR/demo.sqlite" ] && [ $(ls "$STUDIO_DB_DIR"/*.sqlite 2>/dev/null | wc -l) -eq 1 ]; then
echo " ❌ 只有 demo.sqlite (硬编码)"
echo " ❌ 所有用户共享同一数据库"
echo " ❌ 违反'一用户一数据库'原则"
else
echo " ✅ 多个用户数据库"
echo " ✅ 符合设计原则"
fi
echo ""
echo "MarkBase Core:"
if [ -d "$MARKBASE_DB_DIR" ] && [ $(ls "$MARKBASE_DB_DIR"/*.sqlite 2>/dev/null | wc -l) -gt 1 ]; then
echo " ✅ 多个用户数据库"
echo " ✅ 符合'一用户一数据库'原则"
else
echo " ⚠️ 数据库数量不足"
fi
echo ""
echo "================================================================================
修复建议
================================================================================
"
if [ -f "$STUDIO_DB_DIR/demo.sqlite" ] && [ $(ls "$STUDIO_DB_DIR"/*.sqlite 2>/dev/null | wc -l) -eq 1 ]; then
echo "需要修复:"
echo "1. 修改 db.rs 的 db_path() 函数"
echo " - 支持动态用户名"
echo " - 从环境变量或函数参数获取"
echo ""
echo "2. 修改所有数据库函数"
echo " - 添加 username 参数"
echo " - 或使用环境变量"
echo ""
echo "3. 修改前端调用"
echo " - 传递当前用户名"
echo ""
echo "详见: docs/USER_DATABASE_ISOLATION_FIX.md"
else
echo "✅ 当前实现符合设计原则"
fi
echo ""
echo "================================================================================
"