必须用多态关联+独立中间表user_favorites,含user_id、favoriteable_id、favoriteable_type、created_at字段,favoriteable_type存完整类名,加联合唯一索引,收藏用replaceInto去重,取消用where-delete,查收藏需分批预加载关联模型。

直接建 user_favorites 中间表,别往 User 模型里加 JSON 字段或硬编码字段——查谁收藏了某篇文章、按时间倒序、统计总数、分页展示,全都会卡死或不准。
ThinkPHP 6+ 必须用多态关联 + 独立中间表
收藏对象类型不固定(文章、视频、商品),就不能用 user_id + article_id 这种二元结构。必须支持 favoriteable_type 字段存模型类名(如 'app\model\Article'),配合 favoriteable_id 实现多态。
-
user_favorites表至少含:user_id、favoriteable_id、favoriteable_type、created_at -
favoriteable_type必须用完整类名字符串,不用数字编码(否则换模型名后历史数据查不出来) - 在
User模型中定义关联:public function favorites() { return $this->morphToMany(Favoriteable::class, 'favoriteable', 'user_favorites'); }(注意:ThinkPHP 6.1+ 才原生支持morphToMany;低于此版本需手动实现或升级) - 联合唯一索引必须加:
UNIQUE KEY `uniq_user_type_id` (`user_id`, `favoriteable_type`, `favoriteable_id`)
收藏/取消收藏接口不能先查再删或插
高并发下「SELECT 判断 → INSERT/DELETE」必然导致重复插入或误删。ThinkPHP 的 replaceInto() 或事务兜底才是正解。
- 收藏逻辑应直接用
replaceInto(),靠唯一索引自动去重:$data = [ 'user_id' => $userId, 'favoriteable_id' => $id, 'favoriteable_type' => $type, ]; Db::name('user_favorites')->replaceInto($data); - 取消收藏直接
where()->delete(),无需前置查询:Db::name('user_favorites') ->where('user_id', $userId) ->where('favoriteable_id', $id) ->where('favoriteable_type', $type) ->delete(); - 前端按钮状态必须以接口返回为准,别依赖本地缓存判断“是否已收藏”——登录态切换、多端同步时极易错乱
查用户所有收藏要带关联预加载
只查中间表 ID 不够,得连出真实模型数据(比如文章标题、封面图)。用 with() 预加载避免 N+1 查询。
立即学习“PHP免费学习笔记(深入)”;
- 推荐写法:
$favorites = Db::name('user_favorites') ->where('user_id', $userId) ->order('created_at', 'desc') ->paginate(20); // 手动组装关联数据(因 morphToMany 在 paginate 场景下预加载不稳定) $idsByType = []; foreach ($favorites as $fav) { $idsByType[$fav['favoriteable_type']][] = $fav['favoriteable_id']; } // 分批查 Article、Video 等模型 $articles = model('Article')->whereIn('id', $idsByType['app\model\Article'] ?? [])->select(); // ……其他类型同理 - 不要用
$user->favorites()->with('favoriteable')直接链式调用——ThinkPHP 的多态预加载在分页场景下常失效或报错 - 如果只收藏单一类型(如纯文章),可简化为
belongsToMany+article_id字段,但表结构仍建议预留favoriteable_type字段,避免后期改库
最易被忽略的是:favoriteable_type 字段一旦写成 'article' 这类简写,后续模型类名一改(比如从 Article 改成 Post),所有历史收藏就查不出来了。宁可多打几个字符,也要存完整命名空间。



















