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

数据库设计

本文档介绍 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", [])?;