3. 数据库模型设计(上):用户表、通知模板表、通知记录表的核心字段与关系设计

好,咱们今天正式进入数据库模型设计环节。说实话,这部分是整个消息通知模块的基石。你想想看,如果表结构设计得不好,后面写业务逻辑的时候,那叫一个痛苦。我在项目里见过太多因为字段少了一个索引、或者关联关系没理清,导致线上查询慢到崩溃的案例。

今天咱们先搞定三张核心表:用户表通知模板表通知记录表。这三张表,基本能覆盖 80% 的消息通知场景。

3.1 用户表:不只是存个用户名

用户表大家都很熟悉,但做消息通知时,有几个字段是必须额外关注的。

我个人的习惯是,用户表里除了基本的 id、用户名、密码、邮箱、手机号之外,一定要预留一个 notification_config 字段。为什么?因为每个用户对通知的偏好不一样。有的人希望邮件接收,有的人只接受站内信,还有的人想关掉所有通知。

核心字段清单:

  • id:主键,自增或 UUID,看团队习惯
  • username:用户名,唯一索引
  • email:邮箱,用于邮件通知
  • phone:手机号,用于短信通知
  • notification_config:JSON 字段,存储用户的通知偏好设置
  • status:用户状态,比如启用、禁用、已删除
  • created_atupdated_at:时间戳,必备

嗯,这里要注意:notification_config 用 JSON 字段,而不是单独建一张配置表。我在项目中试过拆成多张表,结果每次查询都要 JOIN,性能反而下降。对于这种配置信息,JSON 字段完全够用,而且 Django 和 SQLAlchemy 都原生支持 JSON 操作。

小技巧:用户表的 emailphone 建议加上唯一索引。我曾经遇到过一个用户注册了两个账号,邮箱一样,结果通知发重了,用户投诉说「你们系统是不是有 bug?」。加个唯一索引,从源头避免这种问题。

3.2 通知模板表:让消息内容可配置

通知模板表,说白了就是存消息的「骨架」。你想想看,如果每次发通知都在代码里硬编码消息内容,那改个文案还得重新部署,多麻烦。

我建议模板表至少包含以下字段:

字段名 类型 说明
id int 主键
template_code varchar(64) 模板唯一编码,比如 ORDER_CREATED
title varchar(200) 通知标题,支持占位符
content text 通知正文,支持占位符
channel varchar(20) 通知渠道:email / sms / in_app
variables JSON 占位符变量定义,比如 ["username", "order_id"]
status tinyint 启用/禁用

这里有个关键点:template_code 一定要加唯一索引。为什么?因为业务代码里是通过这个编码来引用模板的。如果重复了,系统就不知道该用哪个模板了。

避坑指南:我曾经在项目里犯过一个错误——模板内容里的占位符和实际传入的变量名不一致。比如模板里写的是 {user_name},但代码里传的是 {username},结果用户收到的通知里显示的是「尊敬的 {user_name},您好」。嗯,从那以后我强制要求模板的 variables 字段必须和占位符一一对应,并且在保存模板时做校验。

3.3 通知记录表:每条通知的「一生」

通知记录表,是消息通知模块里数据量最大的表。每条通知从创建到发送,再到用户是否已读,都要记录在这里。

核心字段设计如下:

CREATE TABLE notification_record (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    template_code VARCHAR(64) NOT NULL,
    title VARCHAR(200),
    content TEXT,
    channel VARCHAR(20) NOT NULL,
    status TINYINT DEFAULT 0 COMMENT '0:待发送 1:已发送 2:发送失败 3:已读',
    send_time DATETIME,
    read_time DATETIME,
    retry_count TINYINT DEFAULT 0,
    extra_data JSON COMMENT '扩展数据,比如错误信息',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user_status (user_id, status),
    INDEX idx_send_time (send_time)
);

你可能会问:为什么要把 titlecontent 冗余存储,而不是直接关联模板表?

原因很简单:模板内容可能会变。假设你修改了某个模板的文案,那之前已经发送的通知记录,如果再去关联模板表,显示的就是新文案,这显然不合理。所以,发送通知时,把最终渲染好的标题和内容直接存到记录表里,这叫「快照」思想。

关键设计思路:

  • 冗余存储:通知记录独立存储标题和内容,不受模板变更影响
  • 状态机:status 字段记录通知的完整生命周期
  • 索引策略:按用户和状态联合索引,方便查询「某个用户的未读通知」
  • 重试机制:retry_count 记录发送失败后的重试次数

3.4 三张表的关系设计

这三张表的关系其实很简单:

  • 用户表通知记录表:一对多。一个用户可以有多条通知记录。
  • 通知模板表通知记录表:通过 template_code 逻辑关联,但不是外键约束。为什么不用外键?因为模板可能会被删除或禁用,但历史通知记录不能受影响。

说白了,通知记录表是核心,它关联了「谁收到了什么通知」,而用户表和模板表都是它的「元数据来源」。

个人经验:我在设计时,不会在通知记录表里加外键约束。外键在写入时会有性能开销,而且对于这种日志型的数据表,我更倾向于在应用层做关联查询。你想想看,每天几百万条通知记录,每条都要检查外键约束,数据库压力会很大。

3.5 一个完整的例子

假设我们要发送一条「订单创建成功」的通知。流程是这样的:

  1. 从用户表查出用户的 notification_config,判断他是否允许接收这类通知
  2. 从模板表根据 template_code = 'ORDER_CREATED' 获取模板内容
  3. 用实际数据(用户名、订单号)替换模板里的占位符
  4. 在通知记录表插入一条记录,status 设为 0(待发送)
  5. 异步任务消费这条记录,调用对应的渠道发送

嗯,这个流程看起来简单,但每一步都有坑。比如第二步,如果模板被禁用了怎么办?我一般会加一个兜底逻辑:如果模板不可用,就用系统默认文案。

3.6 小结

今天咱们把三张核心表的结构和关系理清楚了。用户表关注的是「谁」和「他的偏好」,模板表关注的是「说什么」,通知记录表关注的是「说了没、说了什么、结果如何」。

下一节,咱们会继续聊通知配置表和渠道配置表,以及如何用这些表支撑起一个灵活的消息通知系统。到时候我会分享一个我在项目中踩过的坑——关于渠道降级和熔断的设计,挺有意思的。