thinkphp消息通知系统核心卡点是未读数不准、标记已读丢数据、实时推送连不上;需规范建表(含to_user_id/from_user_id/type/status/created_at及联合索引)、redis+db双写保障未读数一致、sse严格按data格式推送并绑定身份、标记已读须前后端防抖+事务兜底。

ThinkPHP消息通知系统不是靠“加个弹窗”就能跑起来的,核心卡点就三个:未读数不准、标记已读丢数据、实时推送连不上。绕开这几个坑,剩下的都是配置和收口工作。
消息表结构怎么建才不翻车
只存 content 和 user_id 的表,上线三天必出问题。必须包含以下字段且带约束:
-
to_user_id(NOT NULL,接收方 ID) -
from_user_id(可为 NULL 或 0,系统消息用 0 标识) -
type(TINYINT,1=系统通知,2=私信,3=评论回复;别用字符串,索引效率低) -
status(TINYINT NOT NULL DEFAULT 0,0=未读,1=已读,2=已删除;布尔值没扩展性) -
created_at(DATETIME DEFAULT CURRENT_TIMESTAMP,否则新消息排在最底下)
联合索引必须加:(to_user_id, status, created_at)。漏掉这个,用户一进消息页就卡顿,DB 查未读列表会扫全表。
未读数为什么总不准
前端每 5 秒调一次 /message/unread,后端如果每次执行 SELECT COUNT(*) FROM message WHERE to_user_id = ? AND status = 0,1000 用户在线时 DB 就开始抖。根本解法是「DB 主表 + Redis 缓存」双写:
- 新消息插入时,在事务里先写 DB,成功后再执行
INCR redis_key:unread:{user_id} - 用户打开消息列表页,先执行
UPDATE message SET status = 1 WHERE to_user_id = ? AND status = 0清未读,再查当前缓存值 - 轮询接口优先读 Redis,空值或超时则 fallback 到 DB 查询,并回填缓存
别用定时任务去“修复”未读数——那只是掩盖状态不同步,不是解决问题。
ThinkPHP6 怎么接 SSE 实现实时推送
SSE 比 WebSocket 轻,适合服务端单向推通知,而且 ThinkPHP6 原生支持流响应,不用装 Workerman。但浏览器断连、连接保活、格式校验这些细节一漏,前端就收不到 data。
- 后端用
response()->sendFile()或直接输出text/event-stream头,每条消息必须严格按data: {...}\n\n格式,结尾双换行 - 连接要绑定用户身份:SSE URL 带
?token=xxx,服务端校验 session 或 JWT,防止未授权监听 - 前端
EventSource必须实现自动重连:es.onopen和es.onerror里手动 new 一个新实例 - 别在控制器里直接 echo 循环发消息——SSE 是长连接流,得保持 response 不关闭,用
ob_flush()+flush()推送
Chrome 和 Edge 支持良好,Safari 对跨域 SSE 限制较严,withCredentials: true 和后端 CORS 配置必须同步开启。
标记已读为什么不能只改 status
只执行 UPDATE message SET status = 1 WHERE to_user_id = ? AND status = 0 看似正确,但有两个隐藏雷:
- 前端防抖没做,用户狂点“标为已读”,可能触发多次请求,虽然后端 WHERE 条件能防重复更新,但 Redis 里的
DECR会多减,导致未读数变负 - 批量操作(如“全部标为已读”)后,首页小红点不刷新,因为缓存没清——必须同步执行
DEL redis_key:unread:{user_id}或设为 0 - 如果用了数据库队列(如 think-queue),标记已读逻辑不能丢进队列异步执行,否则用户刚点完,刷新页面又看到红点,体验断裂
真正安全的做法:前端按钮加 loading 状态 + 防抖 300ms,后端用事务包裹 DB 更新和 Redis 操作,失败则整个回滚。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











