应拆分为message和message_user两张表,前者存消息正文,后者存用户与消息关系及状态,通过联合索引和redis缓存优化查询。

站内信数据表怎么设计才支持已读/未读和批量标记
站内信不是简单的一对一聊天,核心是「用户收件箱」视角。必须拆成两张表:message 存消息正文、发送者、创建时间;message_user 存每个用户与每条消息的关系(含 status 字段标识 unread/read/deleted)。不要把状态直接塞进 message 表——否则无法实现 A 发一条,B 读了、C 没读这种独立状态。
常见错误是只建一张表加 to_uid 和 is_read,结果导致同一条消息被多人接收时状态互相覆盖。正确做法是:发消息时只写入 message 表一次,再为每个接收者循环插入一条 message_user 记录,status 初始为 unread。
-
message表字段建议:id, from_uid, title, content, created_at -
message_user表字段建议:id, msg_id, uid, status(tinyint,0=unread, 1=read, 2=deleted), read_at, created_at - 联合索引必须加:
(uid, status)(查收件箱)、(msg_id, uid)(防重复插入)
ThinkPHP6 的模型关联怎么写才能查出「我的未读数」和「最新10条」
不能靠 where 硬连——要利用 ThinkPHP6 的「关联预载入 + 闭包查询」。在 User 模型里定义 messages() 关联,指向 MessageUser 模型,并在关联闭包里限定 status = 0;再单独定义 unreadCount() 关联,用 count() 聚合。
// User.php 模型中
public function messages()
{
return $this->hasMany(MessageUser::class, 'uid', 'id')
->with(['message' => function ($q) {
$q->field('id,title,created_at');
}])
->where('status', 0)
->order('created_at', 'desc')
->limit(10);
}
public function unreadCount()
{
return $this->hasMany(MessageUser::class, 'uid', 'id')
->where('status', 0)
->count();
}
调用时:$user->messages()->select() 拿最新未读列表;$user->unreadCount(注意是属性,非方法)自动触发 count 查询。别手写 Db::table()->join()——既难维护,又绕过模型事件和软删除逻辑。
标记已读或批量删除时,为什么用 update 而不是 delete 物理删记录
物理删除 message_user 记录会丢失操作痕迹,且无法做「回收站」或「撤回」功能。正确做法是统一用 update 改 status 字段:标记已读 → status = 1;用户删除 → status = 2;管理员清理 → 定期用脚本清 status = 2 且超过 30 天的记录。
ThinkPHP6 中推荐用事务包裹批量操作,防止部分成功:
Db::transaction(function () use ($uids, $msgId) {
MessageUser::whereIn('uid', $uids)
->where('msg_id', $msgId)
->update(['status' => 1, 'read_at' => date('Y-m-d H:i:s')]);
});
- 别用
delete()方法——它默认走物理删除,除非你显式重写了deleteTime -
read_at字段必须和status更新同步写入,否则「最后阅读时间」不准 - 前端传来的
$uids必须校验是否全属于当前登录用户,防越权(比如用户 A 传入 [1,2,3] 尝试批量操作他人收件箱)
分页查收件箱时,count 查询慢怎么办
当 message_user 表数据量上百万,SELECT COUNT(*) FROM message_user WHERE uid = ? AND status = 0 会变慢——即使有 (uid, status) 索引。这时候不能硬扛,得降级:把未读数缓存在 Redis,每次新消息插入后 INCR user:unread:{$uid},标记已读后 DECR;分页时只查列表,不实时 COUNT。
缓存需设防击穿:首次查未读数为空时,走数据库查并写回 Redis,同时加分布式锁(可用 SETNX + 过期时间)。
- Redis key 建议格式:
user:unread:{$uid},过期时间设为 24 小时足够 - 消息发送成功后,立刻
INCR对应用户的未读计数,别等用户下次打开才补 - 如果业务要求强一致性(如金融类站内信),那就接受慢一点的 COUNT,但务必确保索引有效——用
EXPLAIN确认是否走了联合索引
真正麻烦的不是代码怎么写,而是「未读状态」在多端(Web / App / 小程序)间如何同步。只要有一个端漏掉 read_at 更新,未读数就永远不准。上线前一定用真实多设备场景压测一遍。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











