必须建独立的likes表,含likable_id、likable_type、user_id等字段,加联合唯一索引,用firstOrCreate和delete实现原子操作,API返回is_liked字段需服务端实时查询。

点赞功能该用布尔字段还是计数器?
直接存 is_liked 布尔值看似简单,但查“谁点过”“点没点过当前用户”“总点赞数”全得绕弯子;用 likes_count 计数器又容易脏数据、不支持取消点赞。真实项目里,**必须建独立的 likes 表**,靠外键 + 多态关联支撑行为溯源和原子操作。
- 表结构至少含:
likable_id、likable_type(如"App\Models\Post")、user_id、created_at - 联合唯一索引必须加:
(user_id, likable_id, likable_type),防止重复点赞 - 别在主表加
likes_count字段——初期可读,后期更新冲突频发,尤其并发点赞时
多态关联怎么写才不翻车?
Laravel 的 MorphTo 和 MorphMany 不是套模板就能跑通,关键在命名和迁移字段对齐。
-
likable_id必须是 unsignedBigInteger(MySQL)或 bigIncrements(Laravel 11+ 默认),不能用integer,否则关联大 ID 会截断 -
likable_type字段类型用string,长度至少 255,因为 FQCN(如"App\Models\Comment")可能很长 - 模型里定义关系时,
likes()方法返回$this->morphMany(Like::class, 'likable'),注意第二个参数名必须和迁移里likable_*前缀一致 - 被点赞模型(如
Post)不用手动加likeable_type字段——那是likes表的字段,不是posts表的
点赞/取消点赞的原子操作怎么写?
不能先查再 insert/delete,竞态下会重复插入或漏删。Laravel 没有内置“upsert for morph”,得手写逻辑。
- 用
firstOrCreate()插入新记录,返回Like实例;如果已存在,说明是重复请求,直接返回失败 - 取消点赞用
where()+delete(),别用find()->delete(),避免 N+1 和空对象报错 - 务必加事务:包裹
DB::transaction(),尤其涉及更新likes_count缓存字段时(虽不推荐,但若真要用就得保一致) - 示例关键行:
Like::firstOrCreate(['user_id' => auth()->id(), 'likable_id' => $post->id, 'likable_type' => Post::class])
前端怎么判断“我点没点过”?
每次请求都查一遍 likes 表?太重。常见错误是把判断逻辑全丢给前端,结果用户刷新就变回未点赞状态。
- API 返回数据里,给每个资源加个
is_liked字段,值来自服务端实时查询(用withCount(['likes as is_liked' => function ($q) { $q->where('user_id', auth()->id()); }])) - 别用
exists()查,它不走缓存,且无法复用关联预加载;withCount虽然多一次子查询,但语义清晰、能合并到主查询 - 如果列表很长(如首页 50 条动态),改用 Redis 缓存用户点赞集合:
SMEMBERS user:123:likes,查 ID 是否在其中——但要注意缓存失效时机










