应使用独立中间表(如user_favorites)实现收藏功能,包含user_id、favoriteable_id、favoriteable_type和created_at字段,并建立联合唯一索引以防止重复收藏。

收藏功能该用什么数据结构
ThinkPHP 里实现收藏,核心是「用户-资源」多对多关系。别直接在 User 表加字段,也别用 JSON 字段存 ID 列表——后期查谁收藏了某篇文章、按收藏时间排序、去重统计都会变麻烦。
必须建独立中间表,比如 user_favorites,至少含:user_id、favoriteable_id、favoriteable_type(支持收藏文章/视频/商品)、created_at。这样才方便用 belongsToMany 关联,也兼容 Laravel 风格的多态关联(ThinkPHP 6.1+ 支持)。
常见错误:用 user_id + article_id 二元组合但没加唯一索引 → 同一用户可重复收藏同一内容。
- 加联合唯一索引:
UNIQUE KEY `user_type_id` (`user_id`,`favoriteable_type`,`favoriteable_id`) -
favoriteable_type建议用字符串如'app\model\Article',别用数字编码,否则换模型名就查不出历史数据 - 如果只收藏一种类型(比如仅文章),可简化成
user_id+article_id,但留好扩展余地
怎么写收藏/取消收藏的接口逻辑
别把「是否已收藏」判断和「插入/删除」拆成两个查询再执行,高并发下容易重复插入或误删。ThinkPHP 的 insertGetId() 和 delete() 本身不带条件冲突处理,得靠数据库语义兜底。
立即学习“PHP免费学习笔记(深入)”;
推荐用 replaceInto() 或事务 + 唯一索引捕获异常:
// 收藏(假设已登录,$userId = $this->auth->id)
$data = [
'user_id' => $userId,
'favoriteable_id' => $articleId,
'favoriteable_type' => 'app\model\Article'
];
try {
Db::name('user_favorites')->replaceInto($data); // 自动忽略重复
return ['code' => 0, 'msg' => '已收藏'];
} catch ( hinkdbexceptionDataNotFoundException| hinkdbexceptionModelNotFoundException| hinkdbexceptionDbException $e) {
if (strpos($e->getMessage(), 'Duplicate entry') !== false) {
return ['code' => 0, 'msg' => '已收藏']; // 忽略重复错误
}
throw $e;
}
- 取消收藏直接
where()->delete()即可,不用先查是否存在 - 别在控制器里手动查一遍再决定 insert 还是 delete —— 多一次查询 + 竞态窗口,纯属给自己挖坑
- 前端按钮状态应以接口返回为准,别依赖本地缓存判断“是否已收藏”
怎么快速查出某用户所有收藏
用 with() 预加载能避免 N+1,但要注意关联定义方式。如果中间表是多态的,ThinkPHP 6.1+ 可用 morphedByMany(),否则得手写 hasManyThrough 或原生 join。
示例(单类型收藏,中间表为 user_favorites,目标模型为 Article):
// 在 User 模型中定义
public function favoriteArticles()
{
return $this->belongsToMany(Article::class, 'user_favorites', 'user_id', 'favoriteable_id')
->where('favoriteable_type', 'app\model\Article');
}
// 使用
$user->with('favoriteArticles')->find($userId);
- 如果不加
where('favoriteable_type', ...),查出来可能是混着视频和商品的 ID,关联会错乱 - 分页时别用
with()直接链式调用,先查出收藏 ID 列表,再用whereIn('id', $ids)查主表,更可控 - 收藏时间排序要显式指定:
->order('user_favorites.created_at DESC'),不能只写->order('created_at'),否则走的是 Article 表的时间
Redis 缓存收藏状态的取舍
单次请求查「当前用户是否收藏了这篇」,走数据库小题大做?可以缓存,但别缓存整个收藏列表——用户收藏上百条时,缓存体积和更新成本都失控。
只缓存布尔状态,键设计为 fav:{user_id}:{type}:{id},TTL 设 24 小时足够:
$key = 'fav:'.$userId.':article:'.$articleId;
if (Cache::has($key)) {
return Cache::get($key);
}
$exists = Db::name('user_favorites')
->where(['user_id' => $userId, 'favoriteable_id' => $articleId, 'favoriteable_type' => 'app\model\Article'])
->count();
Cache::set($key, $exists, 86400);
return $exists;
- 缓存穿透风险:ID 不存在时也写入
false,避免恶意刷不存在 ID - 取消收藏后必须删缓存:
Cache::delete($key),别等过期,否则用户点完“取消”刷新页面还显示已收藏 - 如果业务要求强一致性(比如收藏后立刻影响推荐排序),干脆别缓存,数据库查一次也就几毫秒
最易被忽略的点:中间表没加索引时,count() 查询在收藏量大了以后会越来越慢,而缓存又掩盖了这个问题,直到某天 DB 负载飙升才暴露。上线前务必在测试环境用万级数据压测收藏状态查询。


















