搞懂邮件数据是怎么在数据库里存放和查询的。这是搜索功能和看板数据的基础。

第5课:数据库与邮件存储 —— SQLite 怎么存邮件

本课目标

搞懂邮件数据是怎么在数据库里存放和查询的。这是搜索功能和看板数据的基础。


为什么需要数据库?

直接从 Gmail 搜邮件不行吗?也行,但有几个问题:

直接查 Gmail 先存到本地数据库
速度 每次都要网络请求,慢 本地查询,毫秒级
离线 断网就完蛋 没网也能看历史邮件
灵活性 只支持 Gmail 的搜索语法 想怎么查就怎么查
额外数据 不能存自定义字段 可以存标签、分类等

所以 demo 的策略是:先把邮件同步到本地 SQLite,后续操作都在本地做


SQLite 是什么?

SQLite 是一个超轻量的数据库——不需要安装任何服务器,整个数据库就是一个文件(emails.db)。

打个比方:MySQL/PostgreSQL 是大型仓库,需要专人管理;SQLite 是你桌上的文件夹,打开就用。


数据库里有哪些表?

位置:database/email-db.ts

表1:emails(邮件主表)

code
CREATE TABLE emails (
    id              INTEGER PRIMARY KEY,
    messageId       TEXT UNIQUE,        -- Gmail 唯一标识
    threadId        TEXT,               -- 对话线程
    "from"          TEXT,               -- 发件人
    "to"            TEXT,               -- 收件人
    date_sent       TEXT,               -- 发送时间
    date_received   TEXT,               -- 接收时间
    subject         TEXT,               -- 主题
    body_text       TEXT,               -- 纯文本正文
    body_html       TEXT,               -- HTML 正文
    snippet         TEXT,               -- 摘要
    is_read         INTEGER DEFAULT 0,  -- 已读?
    is_starred      INTEGER DEFAULT 0,  -- 星标?
    is_important    INTEGER DEFAULT 0,  -- 重要?
    has_attachments INTEGER DEFAULT 0,  -- 有附件?
    folder          TEXT,               -- 文件夹
    labels          TEXT                -- 标签(JSON数组)
);

表2:recipients(收件人明细)

code
CREATE TABLE recipients (
    id       INTEGER PRIMARY KEY,
    email_id INTEGER,          -- 关联到 emails 表
    type     TEXT,             -- 'to', 'cc', 'bcc'
    address  TEXT,             -- 邮箱地址
    name     TEXT              -- 显示名
);

表3:attachments(附件)

code
CREATE TABLE attachments (
    id           INTEGER PRIMARY KEY,
    email_id     INTEGER,
    filename     TEXT,
    content_type TEXT,         -- 如 'application/pdf'
    size_bytes   INTEGER
);

表4:ui_states(看板数据)

code
CREATE TABLE ui_states (
    stateId    TEXT PRIMARY KEY,     -- 如 'financial-dashboard'
    data       TEXT,                 -- JSON 格式的数据
    createdAt  TEXT,
    updatedAt  TEXT
);

这个表很特别——它不存邮件,而是存看板的持久数据(财务记录、任务列表等)。后面第10课详讲。


常见的数据库操作

插入新邮件

code
// database-manager.ts(简化版)
async function insertEmail(email: ParsedEmail) {
    db.run(`
        INSERT INTO emails (messageId, threadId, "from", "to", subject, body_text, ...)
        VALUES (?, ?, ?, ?, ?, ?, ...)
    `, [email.messageId, email.threadId, email.from, ...]);
}

搜索邮件

code
// email-search.ts(简化版)
async function searchEmails(query: string) {
    return db.all(`
        SELECT * FROM emails
        WHERE subject LIKE ? OR body_text LIKE ?
        ORDER BY date_sent DESC
        LIMIT 20
    `, [`%${query}%`, `%${query}%`]);
}

读取/更新 UI State

code
// ui-state-manager.ts
async function getState(stateId: string) {
    const row = db.get('SELECT data FROM ui_states WHERE stateId = ?', stateId);
    return JSON.parse(row.data);
}

async function setState(stateId: string, data: any) {
    db.run(`
        INSERT OR REPLACE INTO ui_states (stateId, data, updatedAt)
        VALUES (?, ?, ?)
    `, [stateId, JSON.stringify(data), new Date().toISOString()]);

    // 关键:更新后通过 WebSocket 广播给前端
    broadcastStateUpdate(stateId, data);
}

数据怎么流到前端?

graph TD A["数据库"] A -->|"HTTP 接口"| B["后端服务器 (server/endpoints/)"] B -->|"GET /api/emails/inbox -> 返回邮件列表<br>POST /api/emails/search -> 搜索邮件<br>GET /api/ui-state/:id -> 返回看板数据"| C["前端浏览器"] B -->|"WebSocket 推送"| C C -->|"React 组件渲染"| D["你看到的界面"]

HTTP 接口 用于前端主动请求数据(比如打开收件箱时加载邮件列表)。

WebSocket 推送 用于实时更新(比如新邮件到达时自动刷新列表)。


WAL 模式:为什么数据库不卡?

demo 开启了 SQLite 的 WAL(Write-Ahead Logging) 模式:

code
db.pragma('journal_mode = WAL');

简单说:普通模式下,写数据库时会锁住,其他人不能读。WAL 模式下,读写可以同时进行,互不干扰。

这对邮件助手很重要——你在前端搜邮件(读)的同时,后台可能在同步新邮件(写),WAL 保证两者不冲突。


本课小结

  1. SQLite 是本地轻量数据库,整个数据库就是一个文件
  2. 四张表:emails(邮件)、recipients(收件人)、attachments(附件)、ui_states(看板数据)
  3. HTTP 接口 让前端查询数据库
  4. WebSocket 让数据变化实时推送到前端
  5. WAL 模式 保证读写不冲突

沿着当前专题继续,或返回课程目录重新整理阅读顺序。

返回课程目录