输入关键词开始搜索

数据库设计

04 — 云融(YunRong)· 数据库设计文档

前置文档02-系统架构设计文档03-通信协议详细设计文档 设计思路00-设计思路文档

版本:v0.1 状态:已发布 最后更新:2025-01


1. 设计概述

1.1 双数据库架构

┌─────────────────────────────────────────────────────────┐
│                      客户端                              │
│                                                         │
│  ┌─────────────────────┐    ┌─────────────────────────┐ │
│  │     SQLite (本地)    │    │   PostgreSQL (远程)      │ │
│  │                     │    │                         │ │
│  │ • 消息全文           │◄──►│ • 全量消息归档           │ │
│  │ • 会话摘要           │ 同步 │ • 用户/组织架构          │ │
│  │ • 联系人缓存          │    │ • 任务/审批数据          │ │
│  │ • 文件传输记录        │    │ • 文件元数据             │ │
│  │ • 用户配置/草稿       │    │ • 多端同步状态           │ │
│  │ • 离线操作队列        │    │                         │ │
│  └─────────────────────┘    └─────────────────────────┘ │
│                                                         │
│  读: 优先 SQLite (快速)     写: 本地+远端 或 本地优先     │
└─────────────────────────────────────────────────────────┘

1.2 角色分工

维度SQLitePostgreSQL
定位本地一级缓存 + 离线数据库服务端权威数据源
数据范围当前用户相关的热/温数据全量数据(多用户)
访问延迟< 1ms (本地文件 IO)5–50ms (网络)
写入模式串行化单线程写入并发写入(多客户端)
生命周期可随时删除重建(从远端同步恢复)持久化,定期备份
表结构简化版(多余字段从远端按需拉取)完整版(含所有字段和约束)

2. SQLite 本地数据库设计

2.1 配置参数

-- 打开数据库时执行的 pragma
PRAGMA journal_mode = WAL;           -- Write-Ahead Logging: 支持并发读
PRAGMA synchronous  = NORMAL;        -- 平衡安全与性能(WAL 模式下安全)
PRAGMA foreign_keys = ON;            -- 启用外键约束
PRAGMA cache_size   = -64000;        -- 64MB 缓存
PRAGMA temp_store   = MEMORY;        -- 临时表放内存
PRAGMA mmap_size    = 268435456;     -- 256MB 内存映射
PRAGMA page_size    = 4096;          -- 4KB 页(匹配 OS 页大小)

2.2 表结构

2.2.1 会话表 conversations

CREATE TABLE conversations (
    id              INTEGER PRIMARY KEY,           -- 会话 ID (服务端生成)
    type            INTEGER NOT NULL DEFAULT 0,     -- 0=私聊, 1=群聊
    title           TEXT    NOT NULL,               -- 会话名称 (私聊=对方昵称, 群聊=群名)
    avatar_url      TEXT,                           -- 头像 URL
    last_msg_preview TEXT,                          -- 最后一条消息摘要
    last_msg_time   INTEGER NOT NULL DEFAULT 0,     -- 最后消息时间 (Unix 毫秒)
    last_msg_seq    INTEGER NOT NULL DEFAULT 0,     -- 最后消息的 seq
    unread_count    INTEGER NOT NULL DEFAULT 0,     -- 未读计数
    is_pinned       INTEGER NOT NULL DEFAULT 0,     -- 是否置顶
    is_muted        INTEGER NOT NULL DEFAULT 0,     -- 是否免打扰
    sort_order      INTEGER NOT NULL DEFAULT 0,     -- 自定义排序
    local_updated   INTEGER NOT NULL DEFAULT 0,     -- 本地更新时间
    synced          INTEGER NOT NULL DEFAULT 1      -- 0=等待同步, 1=已同步
);

CREATE INDEX idx_conv_last_msg  ON conversations(last_msg_time DESC);
CREATE INDEX idx_conv_pinned    ON conversations(is_pinned, sort_order);

2.2.2 消息表 messages

CREATE TABLE messages (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    msg_id          TEXT    NOT NULL,               -- 全局唯一消息 ID (UUID 或 Snowflake)
    conv_id         INTEGER NOT NULL,               -- 所属会话 ID
    sender_id       INTEGER NOT NULL,               -- 发送者 ID
    sender_name     TEXT    NOT NULL,               -- 发送者名称 (冗余,避免 JOIN)
    content_type    INTEGER NOT NULL DEFAULT 0,     -- 0=文本, 1=图片, 2=文件, 3=系统
    content_body    TEXT    NOT NULL,               -- 消息体 JSON
    quote_msg_id    TEXT,                           -- 引用消息 ID
    status          INTEGER NOT NULL DEFAULT 0,     -- 0=发送中, 1=已送达, 2=已读, 3=发送失败
    seq             INTEGER NOT NULL DEFAULT 0,     -- 协议序列号
    timestamp       INTEGER NOT NULL,               -- 消息时间 (Unix 毫秒)
    is_mine         INTEGER NOT NULL DEFAULT 0,     -- 1=我发送的
    synced          INTEGER NOT NULL DEFAULT 0,     -- 0=本地未同步, 1=已同步到远端
    created_at      INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
);

CREATE INDEX idx_msg_conv_ts    ON messages(conv_id, timestamp DESC);
CREATE INDEX idx_msg_seq        ON messages(seq);
CREATE INDEX idx_msg_synced     ON messages(synced, timestamp);
CREATE UNIQUE INDEX idx_msg_msgid ON messages(msg_id);

-- content_body JSON 结构示例:
-- 文本:  {"type":"text","text":"你好"}
-- 图片:  {"type":"image","url":"...","thumbnail_id":"...","width":1920,"height":1080,"size":245760}
-- 文件:  {"type":"file","name":"报告.pdf","url":"...","size":1048576,"hash":"sha256:..."}
-- 系统:  {"type":"system","subtype":"group_create","actor_id":1001,"extra":{...}}

2.2.3 联系人缓存表 contacts

CREATE TABLE contacts (
    id              INTEGER PRIMARY KEY,           -- 用户 ID
    name            TEXT    NOT NULL,               -- 显示名称
    avatar_url      TEXT,                           -- 头像 URL
    department      TEXT,                           -- 部门名称
    title           TEXT,                           -- 职位
    email           TEXT,                           -- 邮箱
    phone           TEXT,                           -- 电话
    status          INTEGER NOT NULL DEFAULT 0,     -- 0=离线, 1=在线, 2=忙碌, 3=离开
    is_favorite     INTEGER NOT NULL DEFAULT 0,     -- 是否星标联系人
    pinyin          TEXT,                           -- 姓名拼音 (搜索用)
    local_updated   INTEGER NOT NULL DEFAULT 0,
    synced          INTEGER NOT NULL DEFAULT 1
);

CREATE INDEX idx_contact_name   ON contacts(name);
CREATE INDEX idx_contact_pinyin ON contacts(pinyin);
CREATE INDEX idx_contact_dept   ON contacts(department);

2.2.4 组织架构缓存表 departments

CREATE TABLE departments (
    id              INTEGER PRIMARY KEY,
    parent_id       INTEGER NOT NULL DEFAULT 0,     -- 父部门 ID (0=根)
    name            TEXT    NOT NULL,
    sort_order      INTEGER NOT NULL DEFAULT 0,
    member_count    INTEGER NOT NULL DEFAULT 0,
    local_updated   INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX idx_dept_parent ON departments(parent_id);

2.2.5 文件传输任务表 file_transfers

CREATE TABLE file_transfers (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    transfer_id     TEXT    NOT NULL UNIQUE,        -- 传输任务 UUID
    file_name       TEXT    NOT NULL,
    file_path       TEXT    NOT NULL,               -- 本地文件路径
    file_size       INTEGER NOT NULL,               -- 字节
    file_hash       TEXT,                           -- SHA-256
    direction       INTEGER NOT NULL,               -- 0=上传, 1=下载
    conv_id         INTEGER,                        -- 关联会话 (可为空)
    chunk_size      INTEGER NOT NULL DEFAULT 1048576, -- 1MB
    total_chunks    INTEGER NOT NULL,
    completed_chunks INTEGER NOT NULL DEFAULT 0,    -- 已完成块数
    chunk_bitmap    BLOB,                           -- 块完成位图 (二进制)
    status          INTEGER NOT NULL DEFAULT 0,     -- 0=等待, 1=传输中, 2=暂停, 3=完成, 4=失败
    error_msg       TEXT,                           -- 失败原因
    remote_url      TEXT,                           -- 上传完成后的远端 URL
    speed_bps       INTEGER NOT NULL DEFAULT 0,     -- 当前速度 (字节/秒)
    started_at      INTEGER,                       -- 开始时间
    completed_at    INTEGER,                       -- 完成时间
    created_at      INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
);

CREATE INDEX idx_ft_status ON file_transfers(status);
CREATE INDEX idx_ft_conv   ON file_transfers(conv_id);

2.2.6 任务/通知表 tasks

CREATE TABLE tasks (
    id              INTEGER PRIMARY KEY,            -- 任务 ID (服务端生成)
    notify_id       TEXT    NOT NULL UNIQUE,        -- 通知唯一 ID
    task_type       TEXT    NOT NULL,               -- "approval", "assignment", "announcement"
    title           TEXT    NOT NULL,
    body            TEXT,
    priority        INTEGER NOT NULL DEFAULT 0,     -- 0=低, 1=普通, 2=高
    status          INTEGER NOT NULL DEFAULT 0,     -- 0=未读, 1=已读, 2=已处理, 3=已忽略
    action_url      TEXT,                           -- 操作跳转路径
    from_user_id    INTEGER,
    from_user_name  TEXT,
    created_at      INTEGER NOT NULL,
    handled_at      INTEGER,
    synced          INTEGER NOT NULL DEFAULT 1
);

CREATE INDEX idx_task_status ON tasks(status, priority DESC);
CREATE INDEX idx_task_time   ON tasks(created_at DESC);

2.2.7 草稿表 drafts

CREATE TABLE drafts (
    conv_id         INTEGER PRIMARY KEY,
    content_text    TEXT    NOT NULL DEFAULT '',
    content_json    TEXT,                           -- 富文本/引用等结构化草稿
    updated_at      INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
);

2.2.8 配置表 config

CREATE TABLE config (
    key             TEXT    PRIMARY KEY,
    value           TEXT    NOT NULL,
    updated_at      INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
);

-- 示例配置项:
-- "ui.theme"              → "light" | "dark" | "system"
-- "ui.font_size"          → "14"
-- "ui.sidebar_width"      → "280"
-- "notify.sound"          → "true" | "false"
-- "notify.desktop"        → "true" | "false"
-- "network.proxy"         → "" | "socks5://127.0.0.1:1080"
-- "file.download_dir"     → "/home/user/Downloads"
-- "file.upload_limit_kbps"→ "0" (0=不限速)
-- "sync.last_seq"         → "2147483700"
-- "auth.refresh_token"    → "encrypted_token_here"

2.2.9 离线操作队列表 offline_queue

CREATE TABLE offline_queue (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    operation       TEXT    NOT NULL,               -- "send_msg", "mark_read", "set_status"
    payload_json    TEXT    NOT NULL,               -- 操作参数
    created_at      INTEGER NOT NULL,
    retry_count     INTEGER NOT NULL DEFAULT 0,
    last_retry_at   INTEGER,
    status          INTEGER NOT NULL DEFAULT 0      -- 0=等待, 1=处理中, 2=完成, 3=失败
);

CREATE INDEX idx_oq_status ON offline_queue(status, created_at);

2.2.10 全文搜索表 messages_fts

-- SQLite FTS5 全文搜索引擎
CREATE VIRTUAL TABLE messages_fts USING fts5(
    content_text,                                 -- 提取的纯文本 (去除 JSON 标记)
    sender_name,
    content='messages',                           -- 外部内容表 (无副本模式)
    content_rowid='id'
);

-- 触发器:消息插入/更新/删除时自动同步 FTS 索引
CREATE TRIGGER msg_fts_insert AFTER INSERT ON messages BEGIN
    INSERT INTO messages_fts(rowid, content_text, sender_name)
    VALUES (new.id,
        json_extract(new.content_body, '$.text'),
        new.sender_name);
END;

CREATE TRIGGER msg_fts_delete AFTER DELETE ON messages BEGIN
    INSERT INTO messages_fts(messages_fts, rowid, content_text, sender_name)
    VALUES ('delete', old.id, '', '');
END;

2.3 数据生命周期管理

数据类型本地保留策略清理触发条件
消息保留最近 30 天超过 30 天 + SQLite 文件 > 500MB → 清理最旧的
文件传输记录保留最近 100 条超过 100 条 → 删除已完成且超过 7 天的
联系人缓存保留全部远端全量同步时覆盖
离线队列保留最近 7 天已完成/失败的 7 天后删除
草稿保留全部用户手动清除或消息发送成功后自动删除
FTS 索引与消息同步消息删除时同步清理
-- 定期清理脚本 (客户端启动时执行)
DELETE FROM messages
WHERE timestamp < (strftime('%s','now') * 1000 - 30*24*3600*1000)
  AND synced = 1;

DELETE FROM file_transfers
WHERE status IN (3, 4)
  AND completed_at < (strftime('%s','now') * 1000 - 7*24*3600*1000);

DELETE FROM offline_queue
WHERE status IN (2, 3)
  AND created_at < (strftime('%s','now') * 1000 - 7*24*3600*1000);

3. PostgreSQL 远程数据库设计

3.1 设计原则

  • 服务端表是客户端的超集:客户端表的所有字段远程都有,外加审计、索引优化字段
  • 多租户隔离:所有表通过 user_idconv_id 分区
  • 时序数据优化messages 表按时间分区(月)
  • 不允许客户端直接 SQL 访问 PG:通过 REST API 间接访问,保证安全和接口稳定

3.2 核心表结构

3.2.1 用户表 users

CREATE TABLE users (
    id              BIGSERIAL PRIMARY KEY,
    username        VARCHAR(64)  NOT NULL UNIQUE,
    password_hash   VARCHAR(256) NOT NULL,
    display_name    VARCHAR(128) NOT NULL,
    avatar_url      VARCHAR(512),
    email           VARCHAR(256),
    phone           VARCHAR(32),
    department_id   BIGINT       REFERENCES departments(id),
    title           VARCHAR(128),
    status          SMALLINT     NOT NULL DEFAULT 0,   -- 0=离线, 1=在线
    last_active_at  TIMESTAMPTZ,
    created_at      TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_users_dept     ON users(department_id);
CREATE INDEX idx_users_status   ON users(status, last_active_at DESC);

3.2.2 会话表 conversations

CREATE TABLE conversations (
    id              BIGSERIAL PRIMARY KEY,
    type            SMALLINT     NOT NULL,              -- 0=私聊, 1=群聊
    title           VARCHAR(256),
    creator_id      BIGINT       REFERENCES users(id),
    avatar_url      VARCHAR(512),
    settings_json   JSONB        DEFAULT '{}',          -- 群设置 (仅群聊)
    last_msg_at     TIMESTAMPTZ,
    created_at      TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

-- 会话成员关联表
CREATE TABLE conversation_members (
    conv_id         BIGINT  NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
    user_id         BIGINT  NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    role            SMALLINT NOT NULL DEFAULT 0,        -- 0=成员, 1=管理员, 2=群主
    last_read_seq   BIGINT  NOT NULL DEFAULT 0,         -- 该用户在此会话中已读的最大 server_seq(标记已读)
    is_muted        BOOLEAN NOT NULL DEFAULT FALSE,
    is_pinned       BOOLEAN NOT NULL DEFAULT FALSE,
    joined_at       TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (conv_id, user_id)
);

3.2.3 消息表 messages

-- 全局消息序列号(跨所有会话)
CREATE SEQUENCE IF NOT EXISTS global_server_seq;

CREATE TABLE messages (
    id              BIGSERIAL PRIMARY KEY,
    msg_id          VARCHAR(64)  NOT NULL UNIQUE,       -- 全局唯一 ID
    conv_id         BIGINT       NOT NULL,
    sender_id       BIGINT       NOT NULL,
    content_type    SMALLINT     NOT NULL,
    content_body    JSONB        NOT NULL,
    quote_msg_id    VARCHAR(64),
    client_seq      BIGINT,                             -- 发送客户端的 seq(客户端分配)
    server_seq      BIGINT NOT NULL DEFAULT nextval('global_server_seq'), -- 全局递增序列号
    timestamp       TIMESTAMPTZ  NOT NULL,
    created_at      TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

-- 核心索引(5000 人规模单表即可满足,v2 可评估按时间分区)
CREATE INDEX idx_msg_conv_ts    ON messages(conv_id, timestamp DESC);
CREATE INDEX idx_msg_server_seq ON messages(server_seq);

-- 全文搜索 (PostgreSQL 内置)
ALTER TABLE messages ADD COLUMN search_vector tsvector
    GENERATED ALWAYS AS (to_tsvector('simple',
        coalesce(content_body->>'text',''))) STORED;
CREATE INDEX idx_msg_search ON messages USING GIN(search_vector);

3.2.4 部门表 departments

CREATE TABLE departments (
    id              BIGSERIAL PRIMARY KEY,
    parent_id       BIGINT  REFERENCES departments(id),
    name            VARCHAR(128) NOT NULL,
    sort_order      INTEGER NOT NULL DEFAULT 0,
    path            LTREE,                              -- 物化路径 (快速子树查询)
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_dept_path ON departments USING GIST(path);

3.2.5 任务表 tasks

CREATE TABLE tasks (
    id              BIGSERIAL PRIMARY KEY,
    notify_id       VARCHAR(64) NOT NULL UNIQUE,
    task_type       VARCHAR(32) NOT NULL,
    title           VARCHAR(256) NOT NULL,
    body            TEXT,
    priority        SMALLINT NOT NULL DEFAULT 1,
    status          SMALLINT NOT NULL DEFAULT 0,
    from_user_id    BIGINT  REFERENCES users(id),
    to_user_id      BIGINT  NOT NULL REFERENCES users(id),
    action_url      VARCHAR(512),
    related_conv_id BIGINT  REFERENCES conversations(id),
    handled_at      TIMESTAMPTZ,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_task_user   ON tasks(to_user_id, status, priority DESC);
CREATE INDEX idx_task_time   ON tasks(created_at DESC);

3.2.6 文件元数据表 files

CREATE TABLE files (
    id              BIGSERIAL PRIMARY KEY,
    file_hash       VARCHAR(128) NOT NULL,              -- SHA-256 hex
    file_name       VARCHAR(256) NOT NULL,
    file_size       BIGINT  NOT NULL,
    mime_type       VARCHAR(64),
    storage_path    VARCHAR(512) NOT NULL,              -- 存储后端路径
    uploader_id     BIGINT  REFERENCES users(id),
    chunk_size      INTEGER NOT NULL DEFAULT 1048576,
    total_chunks    INTEGER NOT NULL,
    is_complete     BOOLEAN NOT NULL DEFAULT FALSE,     -- 全部分块是否上传完
    upload_started  TIMESTAMPTZ,
    upload_finished TIMESTAMPTZ,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE UNIQUE INDEX idx_files_hash ON files(file_hash);
CREATE INDEX idx_files_uploader ON files(uploader_id, created_at DESC);

3.2.7 同步水位表 sync_watermarks

-- 记录每个用户在每个会话中的同步进度
CREATE TABLE sync_watermarks (
    user_id         BIGINT  NOT NULL REFERENCES users(id),
    conv_id         BIGINT  NOT NULL REFERENCES conversations(id),
    last_server_seq BIGINT  NOT NULL DEFAULT 0,         -- 该用户在此会话中收到的最后 seq
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (user_id, conv_id)
);

4. 数据同步机制

4.1 同步策略矩阵

操作在线 (网络正常)离线 (网络断开)
发送消息写本地 + 发 WebSocket → 收到 ACK → 标记 synced=1写本地 + 入 offline_queuesynced=0
标记已读写本地 + 发 msg_read写本地 + 入 offline_queue
接收消息WebSocket 推送 → 写本地 → synced=1不在线,不存在此操作
联系人更新写本地 + 不立即同步网络恢复后增量拉取
配置修改写本地网络恢复后上传

4.2 增量同步算法(上线时触发)

Client (上线)                        Server
    │                                   │
    │ 1. 读取本地 sync.watermark        │
    │    = { last_server_seq, last_ts } │
    │                                   │
    │ 2. WS auth 成功后                  │
    │── sync(last_seq, last_ts, limit) ─►│
    │                                   │
    │                                    │ 3. 查询 server_seq > last_seq
    │                                    │    AND ts > last_ts
    │                                    │    按 server_seq ASC, LIMIT limit
    │                                    │
    │◄── sync_data(messages, notify,    │
    │              contacts_delta,       │
    │              end_seq, has_more) ──│
    │                                   │
    │ 4. 合并到本地 SQLite               │
    │    - messages: INSERT OR IGNORE   │
    │      (ON CONFLICT msg_id 去重)    │
    │    - contacts: UPSERT              │
    │    - 更新 sync.watermark          │
    │                                   │
    │ 5. if has_more → goto 2           │
    │    (使用新的 last_seq 继续拉取)     │
    │                                   │
    │ 6. 重放 offline_queue 中未同步操作  │
    │    (按 created_at ASC 顺序执行)    │
    │                                   │
    │ 7. 同步完成                         │

4.3 冲突处理

冲突类型策略说明
消息 ID 碰撞INSERT OR IGNORE (SQLite) + ON CONFLICT DO NOTHING (PG)msg_id 全局唯一,相同 ID 一定是同一条消息
联系人信息不一致服务端覆盖 (Last-Write-Wins by server timestamp)客户端缓存的联系人信息以服务端为准
配置冲突服务端覆盖,本地仅在离线时修改配置的权威源是服务端
草稿冲突不解决——只有本地有草稿,不上传草稿是纯本地概念

4.4 离线队列重放

// 伪代码
void SyncManager::replayOfflineQueue() {
    auto ops = db_->getPendingOfflineOps();  // ORDER BY created_at ASC
    for (auto& op : ops) {
        if (op.operation == "send_msg") {
            auto msg = parseJson(op.payload_json);
            imService_->sendMessage(msg, [&](bool ok) {
                if (ok) {
                    db_->markOfflineOpDone(op.id);
                    db_->markMessageSynced(msg.msg_id);
                } else {
                    op.retry_count++;
                    if (op.retry_count >= 3) {
                        db_->markOfflineOpFailed(op.id);
                        db_->markMessageFailed(msg.msg_id);
                    }
                }
            });
        }
        // ... 其他操作同理
    }
}

5. 查询模式与优化

5.1 高频查询

#查询SQL (SQLite)优化策略
1会话列表 (按最后消息排序)SELECT * FROM conversations ORDER BY is_pinned DESC, last_msg_time DESC LIMIT 50idx_conv_last_msg 覆盖
2消息历史 (分页)SELECT * FROM messages WHERE conv_id=? ORDER BY timestamp DESC LIMIT 50 OFFSET ?idx_msg_conv_ts 覆盖
3消息搜索SELECT m.* FROM messages m JOIN messages_fts f ON m.id = f.rowid WHERE messages_fts MATCH ? ORDER BY m.timestamp DESC LIMIT 20FTS5 全文索引
4未读计数SELECT SUM(unread_count) FROM conversations冗余字段,无需 COUNT JOIN
5联系人搜索SELECT * FROM contacts WHERE name LIKE ? OR pinyin LIKE ? LIMIT 20idx_contact_pinyin
6待处理任务SELECT * FROM tasks WHERE status=0 ORDER BY priority DESC, created_at DESCidx_task_status

5.2 消息分页策略

不使用传统 OFFSET(大偏移时性能差),而是用游标分页(Keyset Pagination):

-- 第一页 (最新 50 条)
SELECT * FROM messages
WHERE conv_id = ? AND timestamp < ?
ORDER BY timestamp DESC LIMIT 50;

-- 上一页 (使用上一页最后一条的 timestamp 作为游标)
SELECT * FROM messages
WHERE conv_id = ? AND timestamp < :cursor_ts
ORDER BY timestamp DESC LIMIT 50;

5.3 批量插入优化

消息同步时可能一次收到 200 条消息:

-- 使用事务包裹批量插入
BEGIN TRANSACTION;
INSERT OR IGNORE INTO messages (...) VALUES (...), (...), ...;
UPDATE conversations SET last_msg_time=?, last_msg_preview=? WHERE id=?;
COMMIT;

配合 PRAGMA synchronous = NORMAL,批量插入 200 条通常 < 50ms。


6. 数据库迁移方案

6.1 版本管理

-- 在 SQLite 中维护 schema 版本
CREATE TABLE IF NOT EXISTS schema_version (
    version     INTEGER PRIMARY KEY,
    applied_at  INTEGER NOT NULL
);

6.2 迁移流程

// 应用启动时
void DatabaseManager::migrateIfNeeded() {
    int currentVersion = getSchemaVersion();  // 查询 schema_version
    int targetVersion  = APP_DB_VERSION;      // 编译期常量

    while (currentVersion < targetVersion) {
        currentVersion++;
        applyMigration(currentVersion);       // 执行 v{N}.sql
        setSchemaVersion(currentVersion);
    }
}

6.3 迁移脚本规范

migrations/
├── v1_initial.sql        -- 初始表结构
├── v2_add_pinyin.sql     -- contacts 增加 pinyin 字段
├── v3_add_fts.sql        -- 创建 FTS5 索引
└── v4_add_read_status.sql -- messages 增加 status 字段

每份迁移脚本必须是幂等的(可重复执行不报错):

-- v2_add_pinyin.sql
ALTER TABLE contacts ADD COLUMN pinyin TEXT DEFAULT '';
UPDATE contacts SET pinyin = '' WHERE pinyin IS NULL;

6.4 PostgreSQL 迁移

使用 Flyway 或手写迁移管理,CI/CD 自动执行:

-- V1__initial_schema.sql
-- V2__add_search_vector.sql
-- V3__add_messages_partition.sql

7. 数据安全

7.1 SQLite 加密

方案说明
SQLCipherSQLite 的加密扩展,AES-256-CBC,透明加解密
应用层加密敏感字段(Token、私钥)在写入前手动 AES 加密,读取后解密

建议:全库使用 SQLCipher(简单),或至少加密 config 表中的 auth.refresh_token

// SQLCipher 密钥设置
sqlite3_key(db, key.data(), key.size());
// 每次打开数据库后、执行任何 SQL 之前调用

7.2 PostgreSQL 访问控制

级别措施
网络层仅允许应用服务器 IP 访问 PG 端口,客户端不直连 PG
应用层所有客户端请求经 REST API → 应用服务器 → PG 连接池
行级安全ALTER TABLE messages ENABLE ROW LEVEL SECURITY + Policy: 用户只能读自己参与会话的消息
敏感字段password_hash 使用 bcrypt ($2b$ 前缀)

8. ER 图

┌──────────┐      ┌─────────────────┐      ┌──────────┐
│  users   │      │conversation_     │      │conversa- │
│          │◄─────│    members       │─────►│tions     │
│  id (PK) │  N:M │ user_id  (FK)    │      │ id (PK)  │
│  name    │      │ conv_id  (FK)    │      │ type     │
│  dept_id │      │ role             │      │ title    │
└────┬─────┘      │ unread_seq       │      └────┬─────┘
     │            └─────────────────┘           │
     │                                          │
     │ 1:N                                      │ 1:N
     ▼                                          ▼
┌──────────┐      ┌──────────┐          ┌──────────┐
│ messages │      │  tasks   │          │  files   │
│ msg_id   │      │ id (PK)  │          │ id (PK)  │
│ conv_id  │      │ from_uid │          │ hash     │
│ sender_id│      │ to_uid   │          │ name     │
│ content  │      │ type     │          │ size     │
│ seq      │      │ status   │          │ uploader │
└──────────┘      └──────────┘          └──────────┘

┌──────────────┐      ┌──────────────┐
│ departments  │      │  config      │
│ id (PK)      │      │ key (PK)     │
│ parent_id    │      │ value        │
│ name         │      └──────────────┘
│ path         │
└──────────────┘      仅客户端本地:
                       ┌──────────────┐
                       │ drafts       │
                       │ conv_id (PK) │
                       │ content      │
                       └──────────────┘
                       ┌──────────────┐
                       │ offline_queue│
                       │ id (PK)      │
                       │ operation    │
                       │ payload_json │
                       └──────────────┘

附录 A — 索引策略总结

索引类型用途
conversationsidx_conv_last_msgB-Tree DESC会话列表排序
messagesidx_msg_conv_ts复合 B-Tree消息历史分页
messagesidx_msg_msgidUNIQUE去重 / 幂等插入
messagesidx_msg_seqB-Tree按 seq 查找
messages_fts(FTS5 内部)倒排索引全文搜索
contactsidx_contact_pinyinB-Tree拼音搜索
tasksidx_task_status复合 B-Tree待处理任务列表
offline_queueidx_oq_status复合 B-Tree离线队列重放

附录 B — 文档修订记录

版本日期作者变更说明
v0.12025-01初稿:SQLite 10 表 + PostgreSQL 7 表 + 同步机制 + 迁移方案