3.4 离线消息与未读数
课中消息是即时消费的,但课前通知和课后答疑发生时,学生经常不在线。本节处理的问题就一个:学生不在的时候,消息怎么办——既不能丢,也不能把他淹没在红点里。听起来简单,做起来每个环节都是选择。
离线消息策略表
不同消息的处理方式不一样,我们按场景分了五档:
| 消息场景 | 在线时 | 离线保留策略 | 课后怎么补 |
|---|---|---|---|
| 课中群聊(直播群) | 实时收到 | 不保存 | 看录播回放 |
| 班级群通知 | 实时收到 | 保留 7 天 | 打开应用拉取 |
| 课件补充通知 | 实时收到 | 永久保留 | 进课程详情页查看 |
| 一对一答疑 | 实时收到 | 长期漫游 | 未读数提醒补看 |
| 系统通知 | 实时收到 | 不计未读 | 折叠列表里查看 |
关键区分在第一行:课中直播群消息不保存,是刻意的——大群消息对离线的人没有回看价值,硬存只是费存储、费查询。真有课后价值的内容,走班级通知和一对一答疑这两个渠道。这个区分定下来之后,存储成本和对学生的打扰同时降了下来。
补充一点:表里的“7 天”不是拍脑袋定的,而是统计了学生的补看行为——超过九成的课后消息在 3 天内被打开,7 天能覆盖几乎所有正常情况,又给请假的学生留了富余。数据驱动的好处是,定规则时谁都没意见,毕竟数字摆在那里。
做这个决策之前,我们统计过一周的课中消息:百分之九十以上是“收到”“懂了”这类无信息量的应答。为这些消息付出离线存储成本,不划算。
还有一个反向案例:答疑消息最初我们也只留了 7 天,结果第二周有学生想回看两周前老师对一道编程题的详细解答,发现已被清掉。答疑是长尾内容,一道题的答案可能整学期都有人翻,于是答疑消息改成了长期漫游,只有通知类内容维持短期保留。消息留存策略最终是按“内容有没有长尾价值”分层定的。
拉离线消息的技术细节
学生重新打开应用后,拉取离线消息的体验很容易做烂。我们的做法:
- 按会话分页拉取:每个群会话单独拉,先拉最新一屏,历史消息等用户上滑再加载。不要图省事一次性全拉,一周没上线的学生群消息可能有几百条,全拉会卡住首屏。
- 以消息自己的编号去重:消息都有唯一编号,客户端按编号做本地去重,比按时间戳可靠——时间戳在设备时钟不准时会错位重合。
- 拉取完成再清未读:未读清零必须等消息真正渲染成功,不能拉取请求一发出就清。学生网卡了一下,消息没看到但红点没了,这是最招骂的 bug。
这套流程上线后,弱网设备的首屏时间从六秒降到两秒以内。核心思想就一条:离线数据按需取,先给用户看到东西。
再补一个真实数字:优化的动力来自一个具体学生——家在农村、信号不稳的六年级孩子,每次打开应用要等六秒才能看到课堂消息,经常因为等不及而退出。首屏优化做完之后的那个月,他的消息阅读率从三成提到了八成。技术指标背后是具体的人,这件事我记了一整年。
一个常见误区:什么都存
做离线消息最容易犯的错是“先存下来再说”,理由是存起来总能回看。现实是存的成本、查询的复杂度和学生的信息过载会一起找上来。我们自己存的课中直播群消息,半年内被回查的次数是零——学生缺课会直接去看录播视频,没人翻几百页聊天记录。
所以这张策略表的本质不是技术选型,而是问对每个场景的一个问题:学生不在的时候,这条消息还值得他补看吗? 值得的存到天荒地老(课件补充通知),不值得的当场扔掉(直播群闲聊),中间地带给个限期(班级通知 7 天)。把这个问题问遍每类消息,策略表自己就出来了。
未读数怎么设计
未读数是最容易设计过度的地方。我们的做法是分两层:
- 课程未读:课程卡片上一个小红点,提示“这节课有新通知”,学生点进课程详情才清零——保证他不止是划掉红点,而是真的看到了内容。
- 班级群未读:群会话列表显示未读条数,超过 99 显示为 99 加,进入群会话清零。
有一条规则是上线后才定的:系统通知和自定义信令(举手、禁言这些控制指令)一律不计入未读。起因是有天一个学生打开应用,看到满屏红点,全是控制指令,真正重要的课件补充通知被埋在中间。从那天起,未读数只给“需要人读”的消息,控制类消息一生成就是已读。
多端同步:手机划了红点,平板还在
同一个学生经常手机看一半切到平板。最初的两端未读数各算各的,学生抱怨“手机上读完了,平板上还红着”。改造方案是未读状态以服务端为准:客户端每次打开会话先问服务端“我读到哪了”,而不是只信本地记录。
实际实现里我们简化了一步:未读数不再由客户端本地维护,全部由服务端计算下发,客户端只做显示。代价是多几次网络请求,换来的是多端永远一致。这种取舍在教育产品里很划算——家长常用设备检查孩子消息,看到红点会以为有新通知,点进去发现是旧的,体验很糟。
服务端记的位置还有个额外好处:教务后台能统计每个班级的通知阅读率,哪条通知没人看,下次就改措辞或改时段。
顺带一个实现提醒:未读数查询接口要做缓存。每个学生打开应用时都要查几十个会话的未读,裸查数据库在高峰期扛不住。我们加了一层按学生聚合的缓存,失效策略是“有新消息时主动更新”,未读数是典型的读多写少场景,缓存命中率长期在九成以上。
缓存之外还有个合并写入的技巧:一个活跃课堂里每秒几十条新消息,如果每条都去更新一次未读计数,数据库写入就成了热点。我们把同一会话的计数更新在内存里攒一秒再批量落库,写入量降了一个数量级,而未读数最多延迟一秒,学生完全感知不到。这种小优化在教育产品的高峰时段特别值。
消息漫游与审计
教务的需求很直接:学生说“老师上课补充的通知我没看到”,教务得能回查到原话;家长投诉群里有人说不当言论,平台也得拿得出证据。我们的做法是把班级群消息定期同步到对象存储,按课节归档,保留一个学期。回查时按课节号检索,三分钟就能找到原消息。存储桶的目录规划在第四篇统一讲,这里只说消息归档的触发:每天凌晨跑一次增量任务,同步前一天的全部消息。冷数据再往低频存储沉降一层,和《NAS 家庭服务器指南》里冷数据归档的思路一样,存储成本能压下来不少。
归档这件事别忘了跟学生说。我们在隐私协议里写明了课堂消息会保留一个学期用于教务回查,家长知情后反而更放心——比起“消息阅后即焚”,他们更在意出事的时候有据可查。
归档文件还有个格式选择:我们存的是结构化文本而不是导出的聊天记录页面。结构化文本可以按学生编号、时间范围、关键词任意检索,页面只能人眼翻。教务一开始嫌麻烦想要个网页版,真用上检索之后才发现效率差着量级。工具的形态不重要,查找效率才重要。
归档脚本跑在业务服务器上,那台机器的环境初始化直接照着《Lighthouse 建站指南》的清单走的,省了重复折腾。脚本的运行状态接进了我们的值班看板,跑失败会自动告警,归档任务号称是“可以忘记但不会真忘”的基建。
一次审计回查实录
学期中段,一位家长投诉说老师承诺过会补充发布课件通知,但孩子没收到,导致作业写错。教务接到投诉后按课节号在归档存储里检索,两分钟拉出那周的班级群完整消息记录:补充通知确实发了,时间点是当天 18 点 20 分,孩子未读原因是他当晚没打开应用,而那条通知的离线推送因为落在静默时段被延后了。
事实清楚了,但处理才是关键:教务把通知时间从晚上改到了放学后 5 点前,静默时段的延后补推规则也做了提示文案说明。一次投诉推动了两处产品设计改进。如果消息没有归档,这件事只能是各执一词——在教育行业,服务证据的份量不比功能轻。
移动端推送集成注意
推送是离线触达的最后一公里,三个坑记一下:
- 系统通道:走系统级推送通道,应用在后台甚至被杀进程时也能收到,比应用内长连接靠谱得多。
- 推送内容:群通知只显示标题不显示正文,保护学生隐私——消息内容不被推到锁屏上,这个细节家长特别在意。
- 静默时段:晚上 10 点到次日 7 点不推送,延后到早晨补推。这条是被家长投诉之后才加上的,之前老师经常晚上发次日上课提醒。
推送抵达率不是百分之百,重要通知(比如上课时间临时调整)会在应用内再弹一次确认,双保险。应用内补弹的判定也很简单:服务端记录“已推送但学生未读超过两小时”就触发。
值得一提的还有推送文案。最初我们的推送写“您有 3 条新消息”,毫无打开欲望;改写课程上下文后变成“【三年级编程周二班】课件已补充,请查看”,打开率翻了一倍多。推送是产品的一部分,不是系统的附属品,值得花心思打磨。
隐私与告知
离线消息和归档都涉及学生数据留存,我们把边界写进了用户协议:
- 留存范围:仅班级群通知和答疑消息归档一个学期,直播群课中消息不存储。
- 访问权限:仅教务和带班老师可按课节检索,导出操作全留痕。
- 注销删除:学生退课后可申请删除个人相关消息记录,30 天内处理。
把规则写在明面上,家长的反而是支持居多。K12 行业里,坦诚的数据态度比藏着掖着更容易赢得信任。
最后给一个整体观察:离线消息系统的投入产出比常常被低估。家长判断一个教育平台靠不靠谱,很少是因为画质多清晰,往往是因为“老师发的重要通知到底收没收到”这种具体瞬间。我们花在音视频上的功夫是显性的,花在消息可靠性上的功夫是隐性的,但后者被学生每天每天感受着。本篇讲到这里,课堂互动这条线就算闭环了;下一条线是录播回放,见第四篇。
