在开发带有日程安排的客户端应用时,把用户的会议、生日、待办事项以及对应的提醒时间可靠地保存下来是基本需求。SQLite凭借零配置、单文件、跨平台的特性,成为很多桌面工具和手机App管理本地日历事件的首选。它不需要独立的数据库服务进程,直接以文件形式落在用户目录中,既方便备份,也降低了运维复杂度。本章我们先看整体表结构如何设计才能支撑复杂的提醒逻辑。

一、日历事件表结构与时区处理
设计日历事件存储时,最核心的矛盾是用户分布在各个时区,而提醒必须按用户本地时间准确触发。如果直接存手机或电脑上的本地时间字符串,一旦用户跨时区旅行或者系统更改时区,旧数据就会错乱。因此业界通用做法是所有触发时间都以UTC存入数据库,仅在读取展示时转换为当前时区。我们可以在表中增加timezone字段记录事件创建时的原始时区,方便后续做历史回看。
下面给出一个最小可用的建表语句,涵盖了事件标题、说明、开始时间、结束时间、是否全天、提前提醒分钟数以及重复规则。这里使用TEXT类型配合ISO8601格式存UTC时间,SQLite对时间函数支持良好,后续可以用datetime()做范围查询。
CREATE TABLE calendar_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
start_utc TEXT NOT NULL,
end_utc TEXT,
all_day INTEGER NOT NULL DEFAULT 0,
reminder_minutes INTEGER NOT NULL DEFAULT 0,
repeat_rule TEXT,
origin_timezone TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX idx_start_utc ON calendar_events(start_utc);
对于重复事件,比如每周一上午九点开会,简单方案是把repeat_rule存为RRULE风格字符串,由应用层计算出下一次触发时间后写入一张reminder_queue临时表;更轻量的做法是查询时动态展开,但那会加大SQL复杂度。一般本地日历数据量不大,用应用层展开并插入队列更直观,也方便标记已提醒状态。
二、提醒任务的检索与去重机制
有了数据表,下一步是让提醒服务知道“现在该响哪些事件”。假设我们有一个后台线程每三十秒轮询一次,就需要写出高效且不会重复提醒的SQL。关键点在于记录每条提醒的最后触发时间,或者单独建一张sent_reminders表关联事件ID与触发批次。否则进程重启后,同一个到期事件会被反复弹出,用户体验极差。
以下示例展示如何找出“当前UTC时间减去提前量后已经越过开始时间,且今天还没提醒过”的事件。我们用一个辅助表reminder_log来存发送记录,通过LEFT JOIN剔除已发项。这种写法在十万级数据下依然能靠索引保持毫秒响应。
SELECT e.id, e.title, e.start_utc, e.reminder_minutes
FROM calendar_events e
LEFT JOIN reminder_log r
ON r.event_id = e.id
AND r.trigger_date = date('now')
WHERE datetime(e.start_utc, '-' || e.reminder_minutes || ' minutes') <= datetime('now')
AND r.event_id IS NULL;
除了轮询,SQLite从3.22起支持UPDATE上的触发器,也可以在插入新事件时直接写一条未来时间的提醒入队。但触发器难以跨进程通知,仍需要服务去扫表。实际工程中更推荐“轮询+幂等日志”组合,逻辑清晰,出故障也容易修复。若担心频繁读库耗电,可把轮询间隔调到一分钟,并在应用在前台时缩短为十秒。
三、数据可靠性与离线同步考量
日历数据属于典型不能丢的用户资产,SQLite本身通过回滚日志和WAL模式保证写入原子性。在移动端,建议开启PRAGMA journal_mode=WAL,这样读写可并发,提醒查询不会被大批量插入卡住。同时定期执行VACUUM回收空间,避免单文件无限膨胀影响启动速度。
当用户有多台设备时,纯本地SQLite无法自动互通。此时可在表尾加last_modified与deleted_flag字段,由同步引擎按字段拉取增量,再合并冲突。因为提醒类事件大多由用户手动建,冲突概率低,采用“最后写入胜出”即可。若接入云端,注意把UTC时间和时区原样上传,云端只做存储转发,不做时区换算,换算责任始终留在客户端。
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
ALTER TABLE calendar_events ADD COLUMN last_modified TEXT DEFAULT (datetime('now'));
ALTER TABLE calendar_events ADD COLUMN deleted_flag INTEGER NOT NULL DEFAULT 0;
备份方面,由于SQLite是单文件,直接复制.db即可全量保存。但复制时务必先执行PRAGMA wal_checkpoint把WAL内容落盘,否则副本可能缺最新几秒的提醒。对于开发调试,可以用sqlite3命令行快速导出CSV核对触发逻辑,确认无误后再打包发布。这样一套基于SQLite的日历事件提醒存储,既轻量又经得起真实场景考验。