news 2026/10/2 12:11:22

标签化推送与已读统计怎么做?一次通知链路的工程复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
标签化推送与已读统计怎么做?一次通知链路的工程复盘

一、一条通知,两万条记录

去年汛期的一次强降雨预警,镇里要求在半小时内把撤离提示发到全镇八个村。我们照着老办法做了一次全量推送,触达常住人口两万三千人,其中党员六百四十人、帮扶对象三百一十二人。事后统计发现,光是这一条通知产生的已读记录就有两万多行,因为系统给每个接收人都预先写了一条未读记录,人有没有点开先记上再说。

问题在第二天暴露。那个星期镇上连着发了十一条通知,已读明细表一下子涨到一百四十万行,后台看阅读率的报表直接跑到十二秒还没出结果,超时被网关掐断了。更要命的是,村干部问某个组到底有多少人看了,我们只能给出一个全量数字,拆不出村组维度,因为当时的推送范围是整镇一把梭,标签这个概念压根没进系统。

二、范围是集合运算,已读是去重问题

把需求拆开看,第一件事是范围计算。镇干部描述接收人时用的是一串条件,比如三组的党员、全体村民代表、加上帮扶对象,这些条件有的是并集,有的是交集,还有偶尔要用到的差集。把它抽象出来就是标签的集合运算,每个标签对应一批成员,通知的目标是一棵表达式树,叶子是标签,节点是交并差三种运算。

第二件事是统计口径。一条通知的阅读率等于已读人数除以触达人数,分子要按人去重,同一个人用手机看了一次、用网页又看了一次,只能算一个人。这里最容易踩的是分母,触达人数不能在出报表时现算,因为人员会流动,半年后有人迁出,现算出来的分母会比发布当天小,阅读率被人为抬高。所以触达人数必须在发布的那一刻做一次快照,之后不再变。

三、正向记账还是反向记账

第一条路是正向记录,也就是我们原来那套,发布时给每个接收人写一条未读记录,点开后把状态改成已读。这条路的好处是查询简单,一条 SQL 就能数出未读人数,代价是写入量等于触达人数乘以通知数,两万三千人一天发三条就是七万行,而且绝大部分记录到过期都不会被读,纯属负担。

第二条路是反向记录,只写已读,未读用触达快照减去已读集合算出来。代价是每次统计要做一次差集,还有人退订、注销之后已读集合会比快照多出几个,需要额外处理。我们最后选了第二条,理由是基层推送的打开率并不高,实测在四成左右,也就是说超过一半的预写记录从生到死都不会被改动,为了这一半的垃圾数据付出全量写入成本,不划算。

四、把范围表达式收进通知模块

推送体系建在万村乐数字乡村的通知模块上,标签体系和通知发布共用同一套范围表达式。发布页面上,镇干部勾选条件,前端拼出一棵表达式树,提交到后端后由范围服务编译成一条成员标识集合,再把集合的基数写进通知表作为快照。整个过程对外只暴露一个动作,给我这批人。

标签本身分成三类。第一类是村组标签,由行政区划码前缀自动生成,不用人工维护;第二类是身份标签,党员、村民代表、帮扶对象这些由业务系统在成员信息变更时反向刷新;第三类是自定义标签,镇里自己建的临时分组,比如参加某次环境整治的志愿者。三类标签放在同一张表里,靠一个来源字段区分,查询时行为一致,维护方式各管各的。

五、表结构与回执去重

核心是四张表。标签表 t_tag 存标签名、来源类型和刷新方式;关系表 t_tag_member 存标签与成员的对应,主键是标签加成员;通知表 t_notice 存标题、正文、范围表达式、触达快照和状态;回执表 t_notice_read 存通知标识、成员标识、首次阅读时间、阅读渠道和设备序号,主键是通知加成员,这一条主键就是去重的全部依据。

回执上报走批量接口,客户端在弱网下会把同一批回执重发三次。为了挡住重放,我们让客户端带上一个单调递增的设备序号,服务端写入时用插入忽略的语义,冲突了不报错也不覆盖,首次阅读时间保持最早的那一次。阅读率的查询直接数回执表里有几条,不去关联成员表,这样即使某个成员后来注销了,历史阅读率也不会跟着变。

六、三个坑都在数据量上

第一个坑是写入放大,也是促使我们重构的直接原因。老办法一天写两万条未读记录,一周一百四十万行,一个月就能把明细表顶到六百万行,备份和查询都开始变慢。改成反向记录之后,同样两万三千人的通知,按四成打开率算,一天只写九千多条,行数降了六成,报表从十二秒压到一点四秒。

第二个坑是多端重复计数。有一批通知的阅读率算出来是一点一八,超过了整一百,村干部拿着截图来问是谁看了两次。根因是客户端在手机端和网页端各上报了一次,而当时的回执表没有主键约束,两条都写进去了。改法是给回执表加上通知加成员的复合主键,上报接口改成插入忽略语义,客户端再补一个设备序号用于幂等,同一个人的多条回执只会留下最早的那条。

第三个坑是撤回之后的统计。有一次通知写错了内容,发布三分钟后被撤回,按说阅读率应该定格,可我们发现它还在慢慢往上涨,因为有人从历史消息里点开了旧链接,回执照样上报。改法是把通知状态和统计状态分开,撤回时同时冻结统计并记下冻结时间,回执接口遇到已撤回的通知仍然入库用于审计,但不再参与阅读率计算。这样既保住了原始数据,也保住了对外口径的稳定。

七、这套标签算不出来的事

标签只能描述已经知道的属性,推不出哪些人可能会关心。比如一条关于养殖补贴的通知,理论上应该发给养殖户,可如果入户登记时没把这项职业信息录进去,系统就永远选不中这批人。我们在发布页上加了一个手动补人的入口,允许村干部在系统算出的集合上临时加减几个成员,这个动作会记进操作日志,方便事后复盘为什么这条通知发给了名单外的人。

短信兜底那一路也只能证明到达,证明不了阅读。对于没有智能终端的老人,我们只能走短信或大喇叭,短信能拿到运营商的到达回执,但老人有没有真的看,谁也说不清。所以对外口径上我们把这类通知单列,不计入阅读率的分母,避免把到达率混成阅读率报上去。

还有一条是阅读率本身不等于看懂了。通知发出去百分之多少的人点开过,这个数字好统计,内容有没有被理解、有没有按提示撤离,得靠村委会逐户回访。报表只能告诉镇上哪些组还没看,不能替他们做动员,这一层我们从来没打算用系统替代人的工作。

八、小结

三个汛期下来,万村乐数字乡村的标签从最初的五个长到四十六个,推送范围从单个村扩到四个乡镇,中间没有再改过表结构,只加过一个用于统计冻结的状态位。回头看,省下来的其实不是存储,而是每一次发布时的写入压力,以及由此带来的报表响应时间。

如果你的系统也在做类似的范围推送,建议先想清楚两件事,一是接收范围能不能抽象成标签运算,二是触达人数到底在哪一刻固化。这两件事想明白了,剩下的写入路径、回执去重、撤回处理都是它们的自然推论,想不明白就先别急着建表。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 12:11:04

树莓派5车间部署实战:供电散热与YOLOv5稳定运行指南

这台树莓派5,我在工位上跑得好好的。跑YOLOv5推理、刷Ubuntu、连屏调参,手感丝滑。结果真把它装进车间设备柜,连续踩了一整天的坑。重启、掉帧、断连、掉系统,每一步都卡得人头疼。这篇文章就把我在车间里被卡住的六件事完整写一遍…

作者头像 李华
网站建设 2026/10/2 12:09:09

STM32低成本实战:AI辅助带你跑通ADC采集全流程

先聊点实在的:很多人学嵌入式,第一步就卡在“不知道从哪下手”。开发板动辄几百上千,资料又多又散,学完定时器学中断,学完中断学串口,最后发现连一个能拿得出手的“成品”都没做出来。这套教程想解决的&…

作者头像 李华
网站建设 2026/10/2 12:08:50

MCP Server 开发入门:手把手写一个能跑的 Server,三种协议怎么选

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 12:07:07

Spring AI 实现 MCP-Server:从零搭建可复用的工具服务

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华