为什么需要站内信
在我的上一篇文章 中提到了网站消息通知系统的组成,其中有个很重要的部分就是通知渠道,包括站内信、短信、邮件等其他方式。而在众多渠道中最重要和必不可少的就是站内信了,毕竟短信、邮件这些触达方式要钱不说,还会分分钟钟被用户吐槽和拉黑。 需要注意的是:无论在在 PC 端网站还是 APP 端的推送,本篇文章都统一称为站内信,它们在底层都是同一套,只是展现方式不同而已。
站内信的来源
站内信的通知来源一般包括以下三种:
1.用户事件触发:当某个用户对某个对象执行了评论、@、点赞、留言等动作,都需要对对象拥有者进行通知。这是最常见的需要通知的场景。2.满足系统的规则后自动触发:比如被系统封号、等级提升、获得勋章时,理论上都应该对用户进行通知。3.管理员触发:管理员主动向全网或者某个用户发送通知,比如发送公告等。复制代码
相信读者在使用掘金、知乎等网站或者 APP 收到最多的站内信类型应该就是 1 和 3 了。
站内信的格式
站内信的具体内容我们无法枚举,但是内容的结构却有着固定的模型。根据不同的消息来源参考知乎和掘金我们枚举一些站内信内容,从而更容易的总结出该模型。
站内信案例枚举
1. 用户事件触发
【xxx】【点赞】了你的【文章】【文章的标题】【xxx】【评论】了你的【文章】【文章的标题】【xxx】【点赞】了你在【文章】【文章的标题】下的评论【xxx】【回复】了你的【评论】【被回复的评论的内容】【xxx】【点赞】了你在【文章】【文章标题】下的回复【xxx】在【文章】【文章的标题】中【@】了你【xxx】在【文章】下的【评论】中【@】了你【xxx】在【文章】下的【回复】中【@】了你【xxx】回答了你关注的【问题】【问题标题】【xxx】更新了你关注的【文章】【文章标题】【xxx】邀请你回答【问答】【问答标题】【xxx】关注了你复制代码
2. 系统自动触发
恭喜你,你的【会员】成功升级到了【13级】由于你已经多次违反网站规定,现已经被封号【3个月】复制代码
3. 管理员发送
您的【沸点】被选为编辑精选【管理员】发布了系统公告【文章】【文章标题,请点击查看。【xxx】专栏新增了【N篇】文章复制代码
站内信内容模型
可以看到站内信内容不胜枚举,但稍微总结可以看出它们都需要以下的变量信息:
1. 站内信的主语:某某某用户、系统、管理员,当然也可以不展示,不展示的一般就是系统。为了后文讨论方便,我们称之为主语。该变量是为了告知接受者是谁触发了通知。
2. 站内信触发时所在关联的实体对象:问答、文章、专栏、问答下的回答、回答下的评论、评论下的回复、沸点等。
这里想强调的是:实体对象在通知的时候只取两层,什么意思呢。比如问答下的回答的评论,通知的时候只取【回答下的评论】不用带上问答这个实体信息,否则显得累赘不说还得保存很多实体信息。复制代码
同样为了方便,我们称之为关联对象。是为了将相关联的实体对象信息展示在通知中。
3. 站内信接收人:这个比较简单,肯定是都是当前登录人。
4. 触发站内信的事件:点赞、评论、回复、@、回答、邀请、被关注等等。我们称之为事件,主要是为了具化站内信内容。
5. 站内信触发时所在的主实体的类型:问答、文章、专栏、回答、评论、回复、沸点等,注意与关联实体对象本身的区别。我们称之为主实体类型,主要是为了具化站内信内容,同时减少 event 枚举的个数。
站内信的内容模型可以用伪代码展示如下:
public NoticeMode{ private User subject; //主语 private FatherEntity fatherEntity; //发生对象父实体(可以为空) private SonEntity sonEntity; //发生对象子实体(即主实体)。 private User receiver;//接收人 private NoticeEvent event; //通知事件,枚举中的一个元素 private String sonEntityType; //事件发生时主实体的类型}复制代码
event 和 sonEntityType 组合决定本次站内信的 key。复制代码
站内信内容组织方式
不要将站内信内容的组织放在应用代码中,这样如果要改变通知的内容就得修改代码然后重新发布。在 也讲到了通知的设计可以参考 MVC 模式。模型(M)有了,程序在 C 中(一般是 velocity 中)通过 event 字段组合出想要的站内信内容(V)。同时 C 的代码(假如用 velocity 模板代码实现)可以放到配置中心,这样需要修改站内信内容格式直接像修改配置一样可实时生效。当然这里也可以用规则引擎来实现,但感觉有点大材小用了。
站内信设置
有时候用户对某种事件的站内信并不感兴趣,甚至讨厌。那站内信就要提供设置让用户有选择的接受站内信,当然了,系统的站内信肯定得必须接收。以下是知乎站内信设置的一部分:
本文的站内信内容模型中只需要以 event 和 sonEntityType 为维度设置是否接收消息、接收哪些人的消息,就能比较容易的实现消息通知的设置。
站内信的具体设计
说了那么多,我想把自己的一些设计思路特别是站内信的生成、保存和获取功能展示在这里。给需要者一个参考:
如何生成站内信
很多时候站内信都是在某个用户进行某个动作的时候生成,比如评论。也就是说在评论的时候除了保存评论相关的信息,后台还需要生成站内信。相信很多人都知道保存评论和生成站内信两个操作应该采用异步的方式,防止后者阻塞了前者的返回,影响体验。 应用内的异步使用消息队列有点大材小用了,推荐开源的异步事件框架 Google guava Eventbus.
如何保存站内信
站内信表:tbl_website_message,保存具体的站内信信息,与用户无关,包含的字段大致如下:
字段 | 类型 | 备注 |
---|---|---|
id | Long | 主键 id |
event | String | 事件类型 |
sonEntityType | String | 主实体类型 |
notice_type | String | 通知类型(系统消息、用户消息) |
fatherEntity | json | 关联实体的父对象 |
sonEntity | json | 关联实体的子对象 |
content | String | 站内信的内容 |
站内信与用户关系表:tbl_user_message,包含的字段大致如下:
字段 | 类型 | 备注 |
---|---|---|
id | Long | 主键 id |
website_message_id | Long | 站内信表id |
notice_type | String | 通知类型(系统消息、用户消息) |
max_message_id | Long | 已经遍历过的最大站内信表id |
is_read | Integer | 是否已读 |
其中 notice_type 都是通知类型,比如:掘金的用户消息和系统消息。还可分的更细:点赞、评论、@、其他通知等。具体根据业务需要划分,方便用户区分。
如何获取站内信
用户在登陆网站或者打开 app 的时候触发查询,从用户站内信表(tbl_user_message关联tbl_website_message) 读取站内信 list,比较简单。但有一点需要注意:如果是一对多的场景,即一个事件触发多人接收通知。并不需要将所有人需要接受站内通知放入 tbl_user_message 表,需要被通知的人在登陆获取站内信的时候按需去 tbl_website_message 中查询并出入到 tbl_user_message。具体流程如下:
1. 获取 tbl_user_message 中跟自己有关的最大 max_message_id。2. 去 tbl_website_message 中获取 > max_message_id 的所有记录。3. 循环遍历判断记录是否与自己有关(根据业务逻辑,如果有关将关联关系插入到 tbl_user_message 即可(记得 id 也要保存到 max_message_id 字段中。4. 更新最晚插入 tbl_user_message 那条记录中的 max_message_id 为新遍历的最大的 website_message 表 id。防止下次重复遍历。复制代码
一些问题
站内信关联实体内容的动态获取
前面设计的站内信内容模型保存的是关联实体的对象,在生成站内信内容的时候从模型中将关联的实体信息(例如标题)填充进去。如果在这之后关联实体的信息(如标题)发生改变,在消息中并不能体现出来。如果要动态获取关联内容也很简单,在模型中保存关联实体的 id 就可以了,在用户获取站内信的时候再去根据 id 获取关联实体内容组装成内容返回即可。动态获取关联实体内容可以保证实时性,但我觉得,非动态获取关联实体内容会更好,理由有四:
1.动态获取关联实体的内容导致每次获取站内信都发生表关联查询,对服务器造成一定的压力。2.站内信消息反应的是触发的那一刻的一个状态,并不需要动态性。3.动态改变内容可能会给接收人带来一定的困扰。特别是一些关键性信息,如触发人用户名、标题等。4.如果还有其他通知渠道,比如钉钉、微信、邮件,这些渠道无法做到动态获取。所以还不如统一。复制代码
站内信的聚合
有时候相同类型的站内信过多会导致消息列表很长,比如点赞,这时候可以做一个连续的、相同的 event 和 sonEntityType、相同的操作对象的消息的聚合。虽然本人还没实践过,但我的理解比较简单:在用户登陆网站或者打开 app 获取站内信的时候,后台在原有的站内信消息获取接口上再做一层封装即可。不知道实践过的人的采用的方法是什么样的,欢迎留言讨论。