站内信 - 消息中心设计

为什么需要站内信

入职第三周,产品经理小李找我讨论消息中心的设计。

“用户打开 App,想看最近的通知,但是…”

“但是什么?”

“但是通知已经推送过了,如果用户错过了推送,就没法再看了。”

我恍然大悟。推送是瞬间的,用户可能:

  • 手机静音,没听到
  • 当时没看手机
  • 通知中心清空了
  • App 被杀后台,推送没收到

所以,我们需要一个持久化的站内消息系统,让用户随时可以查看历史通知。

站内信 vs 推送

我画了一张对比表:

特性推送站内信
时机即时永久存储
可见性需要用户当时看随时可查看
存储不存储数据库存储
已读状态不跟踪跟踪已读未读
形式弹窗/通知栏App 内列表

两者是互补的

  • 推送:提醒用户
  • 站内信:记录历史

数据库设计

我设计了一个站内信表:

数据设计要点

  • 核心是在 insite_messages 里保存业务事实,而不是把规则散落在应用逻辑里。
  • 索引服务于高频查询,重点是缩小扫描范围,而不是堆更多字段。

索引设计

idx_user_created:查询用户消息列表

数据设计要点

  • 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。

idx_user_unread:查询未读消息数量

数据设计要点

  • 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。

附加数据设计

data 字段存储 JSON,包含关联的业务数据:

配置要点

  • 配置表达的是环境差异和运行参数,不是业务规则本身。

这样,用户点击消息时,可以直接跳转到对应的评论或动态。

消息中心 API

我设计了几个核心 API:

1. 获取消息列表

2. 标记已读

3. 全部标记已读

4. 删除消息

创建消息的时机

什么时候创建站内信?我总结了几个场景:

场景 1:用户评论动态

场景 2:用户点赞动态

点赞比较特殊,因为一个热门动态可能有上千个赞。如果每个赞都发通知,用户会被淹没。

我采用了”合并通知”的策略:

这样,用户看到的点赞通知是:

张三 等 15 人点赞了你的动态

而不是 15 条独立的点赞通知。

未读数量优化

老王问:“未读数量查询会不会很慢?”

我查了一下数据量:

insite_messages 表:
- 总消息数:500 万条
- 每用户平均:100 条
- 每用户未读:10 条

未读数量查询是高频操作,每次打开 App 都要查。我考虑了几种优化方案:

方案 A:Redis 缓存

方案 B:独立计数表

数据设计要点

  • 核心是在 user_unread_counts 里保存业务事实,而不是把规则散落在应用逻辑里。
  • 索引服务于高频查询,重点是缩小扫描范围,而不是堆更多字段。

每次创建/标记已读时,更新计数:

我选择了方案 B,因为:

  • 计数准确,不依赖缓存过期
  • 可以按类型统计未读数(如评论未读 3 条、点赞未读 5 条)
  • 更新时只需一条 SQL,性能好

消息聚合

产品经理小李提出一个需求:“同一个人连续评论 5 次,能不能合并成一条通知?”

我分析了一下:

消息列表(不合并):
- 张三 评论了你的动态(1 分钟前)
- 张三 评论了你的动态(2 分钟前)
- 张三 评论了你的动态(3 分钟前)
- 张三 评论了你的动态(4 分钟前)
- 张三 评论了你的动态(5 分钟前)

消息列表(合并):
- 张三 评论了你的动态 5 次(刚刚)

我设计了一个聚合策略:

消息清理

站内信会一直累积,需要清理机制。

我设计了一个定时清理脚本:

每天凌晨执行一次,保持表大小可控。

小结

这周我学到:

  1. 站内信的作用:持久化存储,让用户随时查看历史通知
  2. 数据库设计:索引优化,附加数据用 JSON 存储
  3. 未读计数:独立计数表,实时更新
  4. 消息聚合:合并相似通知,减少打扰
  5. 数据清理:定时清理已读旧消息

站内信系统已经基本完成了。接下来,我要处理多端同步的问题了。