资讯动态

待办提醒系统源码实战:数据库设计、调度与避坑指南

发布时间:2026/10/9 17:31:10 来源:尧图企业网站定制
简介这是一套面向计算机、软件工程等专业学生的待办事项提醒系统完整项目源码适合用作课程设计、期末大作业或毕业设计的参考实现也可供需要练习前后端分离开发与数据库设计的开发者借鉴。压缩包共602个文件约1.49MB以Java后端源码252个和Vue前端组件95个为核心辅以JavaScript脚本、SVG图标、XML配置、SQL建表脚本及bat启动脚本等覆盖从界面到数据持久化的完整链路。项目说明文档与数据库脚本齐备下载后可直接运行调试便于理解任务管理、提醒调度等模块的代码组织方式与目录结构。目前已有306人学习下载适合希望快速获得可运行项目骨架、并在此基础上扩展功能的学习者参考。1. 从一份待办提醒系统源码说起为什么“能跑”和“敢上线”是两回事很多同学拿到“待办事项提醒系统实现源码项目说明数据库.zip”这类资源时第一反应是解压、导入、跑起来看到页面能增删改查就以为大功告成。但真正做过提醒类系统的人都知道待办提醒的核心难点从来不是列表渲染而是时间语义和触发可靠性。一个待办在“明天上午九点”该不该提醒取决于时区、用户是否在线、服务是否重启过、提醒是否重复发送。我见过太多 Demo 在本地跑得飞起一放到真实环境就出现漏提醒、重复提醒、跨天任务错乱。这篇笔记就围绕这套源码结构把待办提醒系统从数据库设计、提醒调度、接口实现到避坑排查完整拆一遍适合想把这套代码真正用起来的后端新手和需要落地提醒功能的开发者。2. 待办提醒系统的数据库设计与字段取舍2.1 三张核心表任务、提醒、用户偏好拿到源码先看数据库这是判断一个待办系统是否靠谱的最快方式。常见做法是把结构拆成三张表task存任务本体reminder存提醒计划user_preference存用户的时区和提醒渠道偏好。很多简易 Demo 会把提醒时间直接塞进 task 表字段叫remind_time看起来省事但一旦一个任务需要“提前 30 分钟提醒一次、到点再提醒一次”这种设计就崩了。下面是我一般会用的建表结构字段名和源码里基本一致只做了必要注释。-- 任务表只存任务本身的状态和内容 CREATE TABLE task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 归属用户, title VARCHAR(200) NOT NULL, description TEXT, due_time DATETIME NOT NULL COMMENT 任务截止时间UTC存储, status TINYINT DEFAULT 0 COMMENT 0待办 1完成 2取消, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_user_status (user_id, status), INDEX idx_due (due_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 提醒表一个任务可以有多条提醒计划 CREATE TABLE reminder ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id BIGINT NOT NULL, remind_at DATETIME NOT NULL COMMENT 实际触发时间UTC存储, channel VARCHAR(20) DEFAULT inapp COMMENT inapp/email/sms, sent TINYINT DEFAULT 0 COMMENT 0未发 1已发 2发送失败, retry_count INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_task_time (task_id, remind_at), INDEX idx_pending (sent, remind_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户偏好时区和渠道决定提醒怎么发 CREATE TABLE user_preference ( user_id BIGINT PRIMARY KEY, timezone VARCHAR(50) DEFAULT Asia/Shanghai, email VARCHAR(120), quiet_start TIME DEFAULT 22:00:00 COMMENT 免打扰开始, quiet_end TIME DEFAULT 08:00:00 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个参数值得说清楚。due_time和remind_at我都用 UTC 存储这是血泪经验如果直接存本地时间用户改时区或者服务器迁移机房历史数据全部错位。reminder表上的uk_task_time唯一索引是为了防重复插入idx_pending是给调度器扫描待发提醒用的少了这个索引提醒一多扫描就会拖慢整库。sent字段用 0/1/2 三态而不是布尔是因为发送失败需要重试布尔表达不了失败态。2.2 提醒时间到底存 UTC 还是本地时间这是被问得最多的问题。结论是存储用 UTC展示和计算用用户时区。源码里如果直接拿new Date()往数据库塞那它在跨时区场景一定翻车。正确做法是在应用层统一转换。下面这段 Python 演示了从用户输入的本地时间转成 UTC 入库的过程。from datetime import datetime from zoneinfo import ZoneInfo def local_to_utc(local_str: str, tz_name: str) - datetime: 把用户输入的本地时间字符串转成 UTC datetime tz ZoneInfo(tz_name) # 用户输入形如 2025-03-10 09:00:00 local_dt datetime.strptime(local_str, %Y-%m-%d %H:%M:%S) local_dt local_dt.replace(tzinfotz) return local_dt.astimezone(ZoneInfo(UTC)) def utc_to_local(utc_dt: datetime, tz_name: str) - str: 展示时把 UTC 转回用户本地时间 return utc_dt.replace(tzinfoZoneInfo(UTC)) \ .astimezone(ZoneInfo(tz_name)) \ .strftime(%Y-%m-%d %H:%M:%S)ZoneInfo是 Python 3.9 之后的标准库不需要额外装 pytz。参数tz_name从user_preference.timezone取默认给Asia/Shanghai。注意replace(tzinfo...)和astimezone的区别前者是“给这个裸时间贴上时区标签”后者是“做真正的时区换算”顺序不能反反了就会差好几个小时。2.3 免打扰时段与提醒渠道的关联设计quiet_start和quiet_end这两个字段容易被忽略但真实用户很在意。如果用户在免打扰时段收到提醒体验直接崩。处理逻辑是调度器取出待发提醒后先查用户偏好如果remind_at落在免打扰区间内就把提醒顺延到quiet_end。这里有个边界坑免打扰跨天时比如 22:00 到次日 08:00简单的start t end判断会失效必须分两种情况写。源码里如果没处理这个夜间提醒就会漏发。3. 提醒调度器的实现轮询、延迟队列还是定时任务3.1 三种调度方案的选型对比提醒系统最核心的组件就是调度器它决定了“到点提醒”这件事靠不靠谱。常见做法有三类数据库轮询、延迟队列、系统定时任务。我整理了一张对比表方便按规模选。方案实现方式优点缺点适用规模数据库轮询定时扫 reminder 表简单、无额外依赖精度受轮询间隔限制、量大时压力大日提醒量万级以内延迟队列Redis ZSet / RabbitMQ 延迟插件精度高、解耦需维护中间件、重启要恢复十万级以上系统定时任务cron 每分钟触发无需常驻进程粒度粗、难做秒级小规模或兜底源码如果用的是数据库轮询不要急着否定它。对大多数中小项目轮询加好索引完全够用而且没有中间件依赖部署简单。我一般会先用轮询把功能跑通等提醒量真的上来了再换延迟队列。3.2 用数据库轮询实现一个可靠的提醒扫描器下面这段是轮询调度器的核心逻辑用 Python 写思路通用。关键是先标记再发送避免并发重复发送。import time from datetime import datetime, timedelta def scan_and_send(db, sender, batch100): 扫描到点的提醒并发送每 30 秒跑一次 now datetime.utcnow() # 只取未来 1 分钟内到点、且未发送的提醒 window_end now timedelta(minutes1) rows db.query( SELECT id, task_id, remind_at, channel FROM reminder WHERE sent 0 AND remind_at %s ORDER BY remind_at LIMIT %s FOR UPDATE SKIP LOCKED, (window_end, batch) ) for r in rows: # 先占位防止另一个调度实例重复处理 affected db.execute( UPDATE reminder SET sent 1 WHERE id %s AND sent 0, (r[id],) ) if affected 0: continue # 已被其他实例抢走 try: sender.send(r[task_id], r[channel]) except Exception as e: db.execute( UPDATE reminder SET sent 2, retry_count retry_count 1 WHERE id %s, (r[id],) ) log.error(send fail id%s err%s, r[id], e)FOR UPDATE SKIP LOCKED是 MySQL 8.0 和 PostgreSQL 都支持的行锁语法多个调度实例同时跑时各自锁各自的行不会互相阻塞也不会重复发。batch控制单次处理量别设太大否则一次事务锁太多行。sent 1是占位标记发送失败再改成 2 并累加retry_count。这里有个细节占位和发送不在同一个事务里是为了避免发送耗时长导致长事务代价是极端情况下进程崩溃会丢一条所以还需要一个兜底扫描把sent 1但超过 5 分钟未确认的记录捞回来重发。3.3 失败重试与幂等别让用户收到两条一样的提醒重试是必须的但重试必须幂等。做法是在发送端带一个唯一键比如reminder_id接收端邮件服务、推送服务按这个键去重。如果源码里没有幂等设计重试就会变成骚扰。我一般会在reminder表加一个dedup_key字段值就是task_id remind_at的哈希发送时带上接收方记录已处理的 key。重试策略用指数退避第一次失败等 1 分钟第二次 5 分钟第三次 30 分钟超过三次标记为最终失败并告警。这样既不会疯狂重试打爆下游也不会轻易丢提醒。4. 接口层与提醒触发链路的落地细节4.1 创建任务时如何生成提醒计划用户创建一个任务接口层要做的不只是插一条 task还要根据提醒规则生成 reminder 记录。常见规则有“到点提醒”“提前 N 分钟提醒”“每天重复”。下面是一个创建接口的伪代码重点看提醒计划的生成。def create_task(user_id, title, due_local, tz, advance_minutes0, repeatNone): due_utc local_to_utc(due_local, tz) task_id db.insert(task, { user_id: user_id, title: title, due_time: due_utc, status: 0 }) # 生成第一条提醒截止时间减去提前量 remind_at due_utc - timedelta(minutesadvance_minutes) db.insert(reminder, { task_id: task_id, remind_at: remind_at, channel: inapp, sent: 0 }) # 如果是重复任务按规则批量生成后续提醒 if repeat daily: for i in range(1, 7): # 预生成未来 7 天 db.insert(reminder, { task_id: task_id, remind_at: remind_at timedelta(daysi), channel: inapp, sent: 0 }) return task_idadvance_minutes是提前量0 表示到点提醒。重复任务不要一次性生成太多预生成 7 天足够剩下的靠一个每日任务滚动补充否则数据量会失控。uk_task_time唯一索引在这里起作用如果用户重复提交第二次插入会被数据库挡掉不会产生重复提醒。4.2 提醒渠道的抽象与降级提醒渠道不要写死。源码里如果直接调邮件 SDK后面想加站内信就得改一堆地方。正确做法是定义一个 sender 接口每个渠道一个实现调度器只认接口。下面是一个简单的渠道抽象。class Sender: def send(self, task_id, channel): raise NotImplementedError class InAppSender(Sender): def send(self, task_id, channel): # 写站内信表前端轮询或长连接推送 db.insert(notification, {task_id: task_id, read: 0}) class EmailSender(Sender): def send(self, task_id, channel): task db.get(task, task_id) mailer.send(totask[email], subjecttask[title]) def dispatch(task_id, channel): sender {inapp: InAppSender(), email: EmailSender()}.get(channel) if not sender: raise ValueError(funknown channel {channel}) sender.send(task_id, channel)降级逻辑是如果 email 发送失败自动降级到 inapp保证用户至少能在应用内看到。这个降级不要写在 sender 内部而是写在调度器的异常处理里保持 sender 职责单一。4.3 前端轮询与提醒已读状态同步站内提醒需要前端配合。最简单的是轮询每 30 秒拉一次未读通知。接口设计成GET /notifications?unread1返回后前端标记已读调POST /notifications/read。这里有个坑如果用户开着多个标签页一个标签页标记已读另一个还在显示红点。解决办法是已读状态以服务端为准前端每次轮询都重新拉全量未读数不要本地累加。轮询间隔别低于 15 秒否则服务端压力大也别高于 60 秒否则提醒不及时。30 秒是个平衡点。5. 待办提醒系统避坑与排查五条真实踩坑记录5.1 提醒时间差 8 小时现象用户设置上午 9 点提醒实际下午 5 点才收到。原因数据库存了本地时间但服务器时区是 UTC读取时又按 UTC 解释来回差了 8 小时。解决统一 UTC 存储所有转换在应用层用zoneinfo完成数据库连接串显式指定time_zone00:00。5.2 服务重启后提醒全丢现象调度器用内存延迟队列重启后队列清空待发提醒消失。原因提醒计划只存在内存里没有持久化。解决提醒计划必须落库调度器启动时从reminder表重新加载未发送记录内存队列只作为加速缓存。5.3 同一条提醒发了三次现象用户收到三条相同提醒。原因部署了两个调度实例各自扫描到同一条记录并发送。解决用FOR UPDATE SKIP LOCKED加sent占位标记保证一条记录只被一个实例处理发送端再加dedup_key幂等。5.4 免打扰时段把提醒吞了现象夜间设置的提醒第二天没出现。原因免打扰逻辑直接continue跳过了没有顺延。解决落在免打扰区间的提醒把remind_at改到quiet_end并重新入队而不是丢弃。5.5 重复任务把数据库撑爆现象一个每日重复任务生成了上万条 reminder。原因创建时一次性生成了未来一年的提醒。解决只预生成 7 天用一个每日凌晨的滚动任务补充下一天同时给reminder表加定期归档已发送超过 30 天的记录转历史表。6. 让提醒更准的一个技巧用“时间轮”思路做秒级精度前面讲的轮询方案精度受限于扫描间隔一般 30 秒到 1 分钟。如果业务要求秒级提醒比如会议开始前 10 秒弹窗轮询就力不从心了。我一般会加一层轻量的时间轮做内存加速把未来 60 秒的提醒按秒槽放进一个环形数组调度线程每秒推进一格只处理当前槽里的提醒。数据库仍然是真相来源时间轮只是缓存重启后从库里重建。class TimeWheel: def __init__(self, size60): self.size size self.slots [[] for _ in range(size)] self.cursor 0 def add(self, remind_at, reminder_id): # 只接受未来 60 秒内的提醒 delta int((remind_at - datetime.utcnow()).total_seconds()) if 0 delta self.size: self.slots[(self.cursor delta) % self.size].append(reminder_id) def tick(self): 每秒调用一次返回当前槽里到点的提醒 due self.slots[self.cursor] self.slots[self.cursor] [] self.cursor (self.cursor 1) % self.size return duesize60表示覆盖 60 秒cursor每秒走一格。add时算好偏移量放进对应槽tick取出当前槽并清空。这个结构的好处是插入和取出都是 O(1)比每次扫全表快得多。注意它只处理 60 秒内的提醒更远的还是靠数据库轮询提前 1 分钟加载进来。两者配合既保证精度又保证不丢。验证方法很简单写一个脚本连续创建 100 条未来 5 到 55 秒的提醒观察实际触发时间和设定时间的偏差。正常情况下偏差应该在 1 秒以内。如果偏差大先检查tick是不是被其他任务阻塞了再检查系统时钟有没有漂移。我自己的习惯是任何提醒系统上线前都跑一遍这个 100 条压测确认没有漏发和重复才敢放到真实用户面前。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑