最稳妥方案是用多态多对多中间表likes+唯一索引。需定义likeable_id/type、user_id、created_at,加UNIQUE(user_id,likeable_id,likeable_type),关系中加withoutTrashed()处理软删除,用withCount预加载is_liked_count字段。

点赞逻辑该用 Eloquent 模型关系还是单独数据表
直接用 likes 表 + 多对多中间表结构最稳妥,别图省事用 JSON 字段或布尔字段硬塞。Eloquent 的 belongsToMany 关系天然适配“用户对某篇文章/评论点赞”这种场景,也方便后续加时间戳、取消赞、统计总数等扩展。
常见错误是把点赞状态存在 posts.is_liked_by_current_user 这类字段里——每次用户操作都要全表更新,且无法查“谁点了赞”“点过几次”。
-
likes表至少包含:user_id、likeable_id、likeable_type(实现多态)、created_at - 对应模型用
HasLikestrait 或直接在Post、Comment中定义likers()关系 - 避免用
increment('likes_count')做计数器——并发点赞时会丢数据;改用likes()->count()或带锁的原子更新
Laravel 10+ 中如何安全处理重复点赞请求
前端连点两次、刷新后重发、接口被脚本调用……这些都会导致重复插入 likes 记录。靠数据库唯一索引比 PHP 层判断更可靠。
必须给 likes 表加联合唯一约束:UNIQUE(user_id, likeable_id, likeable_type)。Laravel 迁移里写成:
Schema::create('likes', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->unsignedBigInteger('likeable_id');
$table->string('likeable_type');
$table->timestamps();
$table->unique(['user_id', 'likeable_id', 'likeable_type']);
});
插入失败时捕获 Illuminate\Database\QueryException,检查错误码是否为 23000(唯一约束冲突),再返回 409 或静默忽略。
- 别在控制器里先
where()查一遍再插入——竞态条件依然存在 - 不要用
firstOrCreate()替代唯一索引——它仍可能因并发漏判 - 前端按钮点击后立即置灰,配合防抖,但后端校验不可省
API 接口怎么返回当前用户是否已点赞
不能每次查列表都 N+1 加载 likers 关系。正确做法是在查询主资源时预加载一个“当前用户是否点赞”的标记字段,用子查询或关联聚合。
推荐用 Laravel 的 withCount() 配合条件约束:
$posts = Post::withCount(['likers as is_liked' => function ($q) {
$q->where('user_id', auth()->id());
}])->get();
这样每个 $post 会带一个 is_liked_count 字段(0 或 1),前端直接判断即可。注意字段名是 is_liked_count,不是 is_liked。
- 别在循环里对每个
$post调用$post->likers()->where('user_id', ...)->exists() - 如果用 API Resource,记得在
toArray()里把is_liked_count > 0转成布尔值返回 - 未登录用户要提前过滤掉
auth()->id(),否则报错
软删除模型点赞时要注意什么
当被点赞的模型(比如 Post)启用了软删除,likes 表里仍会存着指向已删除记录的 likeable_id。这会导致 likers() 关系查不到数据,withCount() 统计变少,甚至关联查询报错。
解决方案不是删 likes 记录,而是让关系自动忽略软删除模型:
public function likers()
{
return $this->morphToMany(User::class, 'likeable')
->withoutTrashed(); // 关键
}
同时确保迁移中 likeable_id 字段允许为 NULL(万一目标模型被强制删除),并在业务逻辑里考虑“点赞对象不存在了”该如何展示(比如显示“内容已不可见”)。
- 别依赖数据库外键级联删除——软删除不触发外键动作
-
withoutTrashed()必须显式加,Eloquent 默认不跳过软删除模型 - 如果需要保留历史点赞数据(比如审计),就别删
likes,只清理无效关联的展示逻辑
多态关联字段类型、唯一索引覆盖范围、软删除穿透控制——这三个地方一旦漏掉,上线后就容易出现“点不动”“数不对”“查崩溃”。细节都在迁移和关系定义里,不在控制器里补。


















