1. 数据库设计与字段规划
在实现点餐系统的订单提醒功能时,数据库设计是基础。核心对象是“订单”和“提醒记录”,需要以高效的方式对齐业务流程。关键字段包括 order_id、user_id、restaurant_id、status、order_time、estimated_ready_time、reminder_sent_at、reminder_type 等等。通过设计清晰的外键与索引,可以确保提醒触发时查询成本可控。
为了便于扩展与回放历史,建议额外设计一个 提醒日志 表,用于记录每一次提醒的发送时间、渠道和结果状态,避免重复发送并支持审计。可审计性和幂等性是生产环境的关键维度。
1.1 表结构概览
下面给出一个简化的表结构示例,便于理解字段含义和索引布局。请确保在生产中根据实际字段命名与数据类型调整。
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
restaurant_id BIGINT NOT NULL,
status ENUM('received','preparing','ready','completed','cancelled') NOT NULL DEFAULT 'received',
order_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
estimated_ready_time DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
补充一个用于记录提醒的表结构,便于后续统计与去重。
CREATE TABLE order_reminders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
reminder_type ENUM('preparation','ready','pickup') NOT NULL,
scheduled_at DATETIME NOT NULL,
sent_at DATETIME,
status ENUM('pending','sent','failed') NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
还需要一个通知表,用于追踪发送结果及用户可见的消息内容。
CREATE TABLE notifications (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
order_id BIGINT,
type VARCHAR(50),
message TEXT,
delivered_at DATETIME,
status ENUM('pending','delivered','failed') NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
索引设计要点:在 orders 上对 estimated_ready_time、status、id建立组合索引,以便快速定位即将到达或需要提醒的订单;在 order_reminders 上对 order_id、status 的组合建立索引,提升定时任务的查询效率。
2. 触发时机与业务规则
设计清晰的业务规则是保证提醒准确、不过度打扰用户的关键。核心触发点包括:订单从准备阶段进入就绪阶段、以及按预计完成时间进行的提前提醒。节流策略也同样重要:同一订单在一定时间内不重复发送、多渠道并发发送的幂等处理等。通过规则化的状态机,可以稳定地落地提醒逻辑。
建议将提醒分为多类:准备就绪提醒、即将取餐提醒、以及可选的 进度跟踪提醒。不同类型的提醒应有独立的 scheduled_time 计算与单独的记录路径,确保可追溯性与可扩展性。
2.1 业务规则要点
以下是常见的规则要点,便于后续实现与维护:提前时间段(如 10 分钟前、5 分钟前)用于触发提醒;幂等性确保同一提醒不会重复发送;渠道优先级(如推送优先于短信)可以提升用户体验;错误回滚策略用于处理发送失败的情况并支持重发。
3. 使用 MySQL 定时任务实现提醒
MySQL 自带的事件调度器(Event Scheduler)可以在数据库层面定期执行任务,适合简单、稳定的提醒触发场景。需要注意的是,事件调度器需要开启,并且应与应用层的通知逻辑协作,以实现真正的消息投放。
通过事件可以将就绪提醒、取餐提醒等逻辑统一到一个定时任务里,减少应用层轮询压力,并且便于进行历史统计与故障排查。
3.1 使用事件调度实现定时提醒
以下示例展示一个简化的事件调度实现:定时检查状态为 pending 且预计就绪时间在未来 10 分钟内的订单,生成通知并标记提醒为已发送。
-- 启用事件调度
SET GLOBAL event_scheduler = ON;
-- 创建定时任务(请在支持 DELIMITER 的环境中执行)
DELIMITER $$
CREATE EVENT IF NOT EXISTS ev_order_reminders
ON SCHEDULE EVERY 5 MINUTE
DO
BEGIN
-- 1) 生成通知记录
INSERT INTO notifications (user_id, order_id, type, message, created_at, status)
SELECT o.user_id, o.id, 'reminder',
CONCAT('您的订单号 ', o.id, ' 将在 ', DATE_FORMAT(o.estimated_ready_time, '%H:%i'), ' 完成,请留意。'),
NOW(), 'pending'
FROM orders o
JOIN order_reminders r ON r.order_id = o.id
WHERE r.status = 'pending'
AND o.estimated_ready_time BETWEEN NOW() AND NOW() + INTERVAL 10 MINUTE;
-- 2) 标记相关提醒为已发送
UPDATE order_reminders r
JOIN orders o ON o.id = r.order_id
SET r.status = 'sent', r.sent_at = NOW()
WHERE r.status = 'pending'
AND o.estimated_ready_time BETWEEN NOW() AND NOW() + INTERVAL 10 MINUTE;
END$$
DELIMITER ;
注意事项:在生产环境中,应将事件的执行时间间隔与实际业务峰值对齐;同时,事件体内应尽量避免长事务,避免锁表或长时间占用资源。
4. 通过应用层发送通知
尽管数据库层可以生成提醒,但实际的发送落地通常需要应用层的能力:接入短信、推送、邮件等通道、实现重试策略、以及实现跨渠道的幂等保护。应用层应定期查询待发送的通知记录,完成发送后回写状态。可观测性与幂等性是核心设计。
下面给出一个简化的 Python 示例,用于轮询待投递的通知并通过外部推送服务发送,成功则标记为已投递;失败可按配置进行重试或转入人工处理。
import requests
import pymysql
# 数据库连接(请按实际环境替换参数)
conn = pymysql.connect(host='db_host', user='user', password='pwd', db='db_name', charset='utf8mb4')
def fetch_due_notifications(limit=100):
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
cursor.execute("""
SELECT n.id, n.user_id, n.order_id, n.type, n.message
FROM notifications n
WHERE n.status = 'pending' AND n.delivered_at IS NULL
ORDER BY n.created_at ASC
LIMIT %s
""", (limit,))
return cursor.fetchall()
def mark_delivered(notif_id):
with conn.cursor() as cursor:
cursor.execute("""
UPDATE notifications
SET status = 'delivered', delivered_at = NOW()
WHERE id = %s
""", (notif_id,))
conn.commit()
def send_push(user_id, message):
# 这里示例接入外部推送服务
resp = requests.post("https://api.push.example/send", json={"uid": user_id, "msg": message})
return resp.status_code == 200
def main():
notifs = fetch_due_notifications()
for n in notifs:
ok = send_push(n['user_id'], n['message'])
if ok:
mark_delivered(n['id'])
else:
# 可实现重试策略或将失败记录到单独表
pass
if __name__ == "__main__":
main()
幂等性实现要点:确保同一通知在网络波动或重试后不会重复投放,可以通过在通知表中增加唯一性标识、或在发送端对同一订单的同一类型通知进行去重处理。
5. 数据库表设计与完整建表示例
为便于落地部署,下面给出整套建表脚本的综合示例,便于直接在 MySQL 实例中执行。请结合实际业务字段与命名规范调整。
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
restaurant_id BIGINT NOT NULL,
status ENUM('received','preparing','ready','completed','cancelled') NOT NULL DEFAULT 'received',
order_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
estimated_ready_time DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE order_reminders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
reminder_type ENUM('preparation','ready','pickup') NOT NULL,
scheduled_at DATETIME NOT NULL,
sent_at DATETIME,
status ENUM('pending','sent','failed') NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE notifications (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
order_id BIGINT,
type VARCHAR(50),
message TEXT,
delivered_at DATETIME,
status ENUM('pending','delivered','failed') NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE INDEX idx_orders_status_est_time ON orders (status, estimated_ready_time);
CREATE INDEX idx_reminders_order_status ON order_reminders (order_id, status);
6. 监控与调试
监控是确保提醒功能稳定落地的重要环节。核心指标包括:未投递通知的积压数量、定时任务执行耗时、订单状态转换与提醒之间的时延、通道发送成功率等。应将这些指标接入日志与指标系统,便于快速定位瓶颈。
可使用以下 SQL 片段快速自检常见问题:未投递通知的比例、历史提醒的重复率、以及最近 24 小时的提醒分布。
-- 查最近 24 小时的未投递通知
SELECT COUNT(*) AS pending_count
FROM notifications
WHERE delivered_at IS NULL
AND created_at >= NOW() - INTERVAL 1 DAY;
-- 查最近 24 小时的提醒记录
SELECT reminder_type, COUNT(*) AS cnt
FROM order_reminders
WHERE created_at >= NOW() - INTERVAL 1 DAY
GROUP BY reminder_type;
7. 性能与安全考虑
在高并发场景下,性能与安全尤为重要。要点包含:对关键查询建立覆盖索引、避免长事务导致的锁争用、合理的批量处理与分页策略;对提醒内容的敏感字段进行最小化暴露、确保传输过程中使用加密、以及对外部通知服务的访问做限流。对于历史数据,可以定期归档到冷存储,降低主库压力。
此外,数据保留策略应符合业务与合规要求;可设置提醒与通知的清理策略,例如按月清理超过一定时间且状态为已完成的记录,确保数据库维度与存储成本可控。


