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.
11 KiB
11 KiB
数据库设计
本文档介绍 Flash Send 的 SQLite 数据库设计。
概述
Flash Send 使用 SQLite 作为本地数据库,存储:
- 聊天消息记录
- 文件传输记录
- 已知设备信息
- 应用设置
数据库文件位置: {app_data_dir}/flash_send.db
表结构
chat_messages
存储聊天消息。
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
存储文件传输记录。
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 字段格式:
"\"pending\""
"\"transferring\""
"\"completed\""
"\"failed\""
"\"cancelled\""
known_devices
存储已知设备信息。
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
存储应用设置。
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 |
数据操作
消息操作
保存消息
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(())
}
获取聊天历史
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"
)?;
// ...
}
删除聊天历史
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(())
}
传输操作
保存传输记录
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(())
}
更新传输进度
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(())
}
获取传输历史
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"
)?;
// ...
}
设备操作
保存已知设备
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(())
}
设置操作
获取设置
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()
}
保存设置
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(())
}
数据库迁移
迁移机制
使用版本号管理数据库迁移:
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 字段:
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 天未见自动删除
手动清理
// 清理指定设备的所有数据
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 保护:
static DB_CONN: OnceCell<Mutex<Connection>> = OnceCell::new();
pub fn conn() -> MutexGuard<'static, Connection> {
DB_CONN.get().expect("Database not initialized").lock()
}
事务使用
批量操作使用事务提升性能:
conn.execute("BEGIN TRANSACTION", [])?;
for message in messages {
save_message(&message)?;
}
conn.execute("COMMIT", [])?;