You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
450 lines
11 KiB
Markdown
450 lines
11 KiB
Markdown
# 数据库设计
|
|
|
|
本文档介绍 Flash Send 的 SQLite 数据库设计。
|
|
|
|
## 概述
|
|
|
|
Flash Send 使用 SQLite 作为本地数据库,存储:
|
|
- 聊天消息记录
|
|
- 文件传输记录
|
|
- 已知设备信息
|
|
- 应用设置
|
|
|
|
**数据库文件位置**: `{app_data_dir}/flash_send.db`
|
|
|
|
## 表结构
|
|
|
|
### chat_messages
|
|
|
|
存储聊天消息。
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
id TEXT PRIMARY KEY, -- 消息唯一 ID (UUID)
|
|
from_device TEXT NOT NULL, -- 发送方设备 ID
|
|
to_device TEXT NOT NULL, -- 接收方设备 ID
|
|
content TEXT NOT NULL, -- 消息内容
|
|
message_type TEXT NOT NULL, -- 消息类型 (text/image/file/system)
|
|
timestamp INTEGER NOT NULL, -- 时间戳 (毫秒)
|
|
is_read INTEGER DEFAULT 0 -- 是否已读 (0/1)
|
|
);
|
|
|
|
-- 索引:按设备和时间查询
|
|
CREATE INDEX IF NOT EXISTS idx_messages_devices
|
|
ON chat_messages(from_device, to_device);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_messages_timestamp
|
|
ON chat_messages(timestamp);
|
|
```
|
|
|
|
**字段说明**:
|
|
|
|
| 字段 | 类型 | 说明 |
|
|
|------|------|------|
|
|
| id | TEXT | 消息 UUID |
|
|
| from_device | TEXT | 发送方设备 ID |
|
|
| to_device | TEXT | 接收方设备 ID |
|
|
| content | TEXT | 消息内容 |
|
|
| message_type | TEXT | text/image/file/system |
|
|
| timestamp | INTEGER | Unix 时间戳(毫秒) |
|
|
| is_read | INTEGER | 0=未读, 1=已读 |
|
|
|
|
---
|
|
|
|
### file_transfers
|
|
|
|
存储文件传输记录。
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS file_transfers (
|
|
file_id TEXT PRIMARY KEY, -- 传输唯一 ID (UUID)
|
|
name TEXT NOT NULL, -- 文件名
|
|
size INTEGER NOT NULL, -- 文件大小 (字节)
|
|
progress REAL DEFAULT 0, -- 传输进度 (0.0-1.0)
|
|
status TEXT NOT NULL, -- 传输状态 (JSON)
|
|
mime_type TEXT, -- MIME 类型
|
|
from_device TEXT NOT NULL, -- 发送方设备 ID
|
|
to_device TEXT NOT NULL, -- 接收方设备 ID
|
|
local_path TEXT, -- 本地文件路径
|
|
created_at INTEGER NOT NULL, -- 创建时间
|
|
completed_at INTEGER, -- 完成时间
|
|
transferred_bytes INTEGER DEFAULT 0 -- 已传输字节数
|
|
);
|
|
|
|
-- 索引:按设备查询
|
|
CREATE INDEX IF NOT EXISTS idx_transfers_devices
|
|
ON file_transfers(from_device, to_device);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_transfers_created
|
|
ON file_transfers(created_at);
|
|
```
|
|
|
|
**字段说明**:
|
|
|
|
| 字段 | 类型 | 说明 |
|
|
|------|------|------|
|
|
| file_id | TEXT | 传输 UUID |
|
|
| name | TEXT | 文件名 |
|
|
| size | INTEGER | 文件大小(字节) |
|
|
| progress | REAL | 进度 0.0-1.0 |
|
|
| status | TEXT | JSON: "pending"/"transferring"/"completed"/"failed"/"cancelled" |
|
|
| mime_type | TEXT | MIME 类型 |
|
|
| from_device | TEXT | 发送方设备 ID |
|
|
| to_device | TEXT | 接收方设备 ID |
|
|
| local_path | TEXT | 本地存储路径 |
|
|
| created_at | INTEGER | 创建时间戳 |
|
|
| completed_at | INTEGER | 完成时间戳 |
|
|
| transferred_bytes | INTEGER | 已传输字节数 |
|
|
|
|
**status 字段格式**:
|
|
```json
|
|
"\"pending\""
|
|
"\"transferring\""
|
|
"\"completed\""
|
|
"\"failed\""
|
|
"\"cancelled\""
|
|
```
|
|
|
|
---
|
|
|
|
### known_devices
|
|
|
|
存储已知设备信息。
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS known_devices (
|
|
device_id TEXT PRIMARY KEY, -- 设备唯一 ID
|
|
device_name TEXT NOT NULL, -- 设备名称
|
|
ip TEXT, -- 最后已知 IP
|
|
ws_port INTEGER, -- WebSocket 端口
|
|
http_port INTEGER, -- HTTP 端口
|
|
last_seen INTEGER -- 最后在线时间
|
|
);
|
|
```
|
|
|
|
**字段说明**:
|
|
|
|
| 字段 | 类型 | 说明 |
|
|
|------|------|------|
|
|
| device_id | TEXT | 设备 UUID |
|
|
| device_name | TEXT | 显示名称 |
|
|
| ip | TEXT | IP 地址 |
|
|
| ws_port | INTEGER | WebSocket 端口 |
|
|
| http_port | INTEGER | HTTP 端口 |
|
|
| last_seen | INTEGER | 最后在线时间戳 |
|
|
|
|
---
|
|
|
|
### app_settings
|
|
|
|
存储应用设置。
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS app_settings (
|
|
key TEXT PRIMARY KEY, -- 设置键名
|
|
value TEXT -- 设置值
|
|
);
|
|
```
|
|
|
|
**常用设置键**:
|
|
|
|
| 键名 | 说明 | 示例值 |
|
|
|------|------|--------|
|
|
| theme | 主题模式 | system/light/dark |
|
|
| device_name | 设备名称 | My Computer |
|
|
| download_dir | 下载目录 | C:\Downloads |
|
|
| language | 语言 | zh-CN |
|
|
|
|
---
|
|
|
|
## 数据操作
|
|
|
|
### 消息操作
|
|
|
|
#### 保存消息
|
|
|
|
```rust
|
|
pub fn save_message(message: &ChatMessage) -> Result<()> {
|
|
conn.execute(
|
|
"INSERT INTO chat_messages
|
|
(id, from_device, to_device, content, message_type, timestamp, is_read)
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
|
params![
|
|
message.id,
|
|
message.from_device,
|
|
message.to_device,
|
|
message.content,
|
|
message.message_type.to_string(),
|
|
message.timestamp,
|
|
message.is_read as i32,
|
|
],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
#### 获取聊天历史
|
|
|
|
```rust
|
|
pub fn get_chat_history(
|
|
device_id: &str,
|
|
local_device_id: &str,
|
|
limit: usize,
|
|
offset: usize,
|
|
) -> Result<Vec<ChatMessage>> {
|
|
let mut stmt = conn.prepare(
|
|
"SELECT id, from_device, to_device, content, message_type, timestamp, is_read
|
|
FROM chat_messages
|
|
WHERE (from_device = ?1 AND to_device = ?2)
|
|
OR (from_device = ?2 AND to_device = ?1)
|
|
ORDER BY timestamp DESC
|
|
LIMIT ?3 OFFSET ?4"
|
|
)?;
|
|
// ...
|
|
}
|
|
```
|
|
|
|
#### 删除聊天历史
|
|
|
|
```rust
|
|
pub fn delete_chat_history(device_id: &str, local_device_id: &str) -> Result<()> {
|
|
conn.execute(
|
|
"DELETE FROM chat_messages
|
|
WHERE (from_device = ?1 AND to_device = ?2)
|
|
OR (from_device = ?2 AND to_device = ?1)",
|
|
params![device_id, local_device_id],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 传输操作
|
|
|
|
#### 保存传输记录
|
|
|
|
```rust
|
|
pub fn save_file_transfer(transfer: &FileTransfer) -> Result<()> {
|
|
conn.execute(
|
|
"INSERT INTO file_transfers
|
|
(file_id, name, size, progress, status, mime_type,
|
|
from_device, to_device, local_path, created_at, completed_at, transferred_bytes)
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)
|
|
ON CONFLICT(file_id) DO UPDATE SET
|
|
progress = ?4, status = ?5, local_path = ?9,
|
|
completed_at = ?11, transferred_bytes = ?12",
|
|
params![...],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
#### 更新传输进度
|
|
|
|
```rust
|
|
pub fn update_transfer_progress(
|
|
file_id: &str,
|
|
progress: f64,
|
|
transferred_bytes: u64,
|
|
status: &str,
|
|
) -> Result<()> {
|
|
let completed_at = if status == "completed" || status == "failed" {
|
|
Some(chrono::Utc::now().timestamp())
|
|
} else {
|
|
None
|
|
};
|
|
|
|
conn.execute(
|
|
"UPDATE file_transfers
|
|
SET progress = ?1, transferred_bytes = ?2, status = ?3, completed_at = ?4
|
|
WHERE file_id = ?5",
|
|
params![progress, transferred_bytes, status, completed_at, file_id],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
#### 获取传输历史
|
|
|
|
```rust
|
|
pub fn get_transfer_history(
|
|
device_id: &str,
|
|
local_device_id: &str,
|
|
limit: usize,
|
|
) -> Result<Vec<FileTransfer>> {
|
|
let mut stmt = conn.prepare(
|
|
"SELECT file_id, name, size, progress, status, mime_type,
|
|
from_device, to_device, local_path, created_at, completed_at, transferred_bytes
|
|
FROM file_transfers
|
|
WHERE (from_device = ?1 AND to_device = ?2)
|
|
OR (from_device = ?2 AND to_device = ?1)
|
|
ORDER BY created_at DESC
|
|
LIMIT ?3"
|
|
)?;
|
|
// ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 设备操作
|
|
|
|
#### 保存已知设备
|
|
|
|
```rust
|
|
pub fn save_known_device(device: &DeviceInfo) -> Result<()> {
|
|
conn.execute(
|
|
"INSERT INTO known_devices
|
|
(device_id, device_name, ip, ws_port, http_port, last_seen)
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
|
ON CONFLICT(device_id) DO UPDATE SET
|
|
device_name = ?2, ip = ?3, ws_port = ?4, http_port = ?5, last_seen = ?6",
|
|
params![...],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 设置操作
|
|
|
|
#### 获取设置
|
|
|
|
```rust
|
|
pub fn get_setting(key: &str) -> Result<Option<String>> {
|
|
let mut stmt = conn.prepare(
|
|
"SELECT value FROM app_settings WHERE key = ?1"
|
|
)?;
|
|
stmt.query_row(params![key], |row| row.get(0)).optional()
|
|
}
|
|
```
|
|
|
|
#### 保存设置
|
|
|
|
```rust
|
|
pub fn set_setting(key: &str, value: &str) -> Result<()> {
|
|
conn.execute(
|
|
"INSERT INTO app_settings (key, value) VALUES (?1, ?2)
|
|
ON CONFLICT(key) DO UPDATE SET value = ?2",
|
|
params![key, value],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 数据库迁移
|
|
|
|
### 迁移机制
|
|
|
|
使用版本号管理数据库迁移:
|
|
|
|
```rust
|
|
pub fn init_database(conn: &Connection) -> Result<()> {
|
|
// 创建版本表
|
|
conn.execute(
|
|
"CREATE TABLE IF NOT EXISTS schema_version (version INTEGER)",
|
|
[],
|
|
)?;
|
|
|
|
let version: i32 = conn
|
|
.query_row("SELECT version FROM schema_version", [], |row| row.get(0))
|
|
.unwrap_or(0);
|
|
|
|
// 根据版本执行迁移
|
|
if version < 1 {
|
|
// 创建初始表结构
|
|
create_initial_tables(conn)?;
|
|
update_version(conn, 1)?;
|
|
}
|
|
|
|
if version < 2 {
|
|
// 添加 transferred_bytes 字段
|
|
add_transferred_bytes_column(conn)?;
|
|
update_version(conn, 2)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### 迁移示例
|
|
|
|
添加 `transferred_bytes` 字段:
|
|
|
|
```rust
|
|
fn add_transferred_bytes_column(conn: &Connection) -> Result<()> {
|
|
// 检查字段是否存在
|
|
let columns: Vec<String> = conn
|
|
.prepare("PRAGMA table_info(file_transfers)")?
|
|
.query_map([], |row| row.get::<_, String>(1))?
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
if !columns.contains(&"transferred_bytes".to_string()) {
|
|
conn.execute(
|
|
"ALTER TABLE file_transfers ADD COLUMN transferred_bytes INTEGER DEFAULT 0",
|
|
[],
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 数据清理
|
|
|
|
### 自动清理策略
|
|
|
|
- **消息**: 保留最近 1000 条/设备
|
|
- **传输记录**: 保留最近 100 条/设备
|
|
- **离线设备**: 30 天未见自动删除
|
|
|
|
### 手动清理
|
|
|
|
```rust
|
|
// 清理指定设备的所有数据
|
|
pub fn cleanup_device_data(device_id: &str) -> Result<()> {
|
|
conn.execute("DELETE FROM chat_messages WHERE from_device = ?1 OR to_device = ?1", [device_id])?;
|
|
conn.execute("DELETE FROM file_transfers WHERE from_device = ?1 OR to_device = ?1", [device_id])?;
|
|
conn.execute("DELETE FROM known_devices WHERE device_id = ?1", [device_id])?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 性能优化
|
|
|
|
### 索引策略
|
|
|
|
- 按设备 ID 查询:`idx_messages_devices`, `idx_transfers_devices`
|
|
- 按时间排序:`idx_messages_timestamp`, `idx_transfers_created`
|
|
|
|
### 连接池
|
|
|
|
使用全局单例连接,通过 `parking_lot::Mutex` 保护:
|
|
|
|
```rust
|
|
static DB_CONN: OnceCell<Mutex<Connection>> = OnceCell::new();
|
|
|
|
pub fn conn() -> MutexGuard<'static, Connection> {
|
|
DB_CONN.get().expect("Database not initialized").lock()
|
|
}
|
|
```
|
|
|
|
### 事务使用
|
|
|
|
批量操作使用事务提升性能:
|
|
|
|
```rust
|
|
conn.execute("BEGIN TRANSACTION", [])?;
|
|
for message in messages {
|
|
save_message(&message)?;
|
|
}
|
|
conn.execute("COMMIT", [])?;
|
|
```
|