Rails API:如何用单个HTTP请求及控制器实现点赞/取消点赞功能
嘿,这个问题我之前也遇到过——前端维护点赞状态不仅麻烦,还容易出现前后端状态不一致的情况,尤其是网络延迟或者用户快速操作的时候。给你分享一个更优雅的方案,让后端来自动处理点赞/取消点赞的逻辑,前端只需要发一个请求就搞定!
核心思路:用单一端点实现状态切换
不用让前端判断用户是否已点赞再发不同请求,而是后端提供一个状态切换的端点,前端不管当前状态,只发送一个请求,后端自动判断是创建点赞记录还是删除它。
步骤1:完善模型关联与数据保障
首先确保你的模型关联和数据唯一性约束到位:
模型定义
假设你有User、Post和Like三个模型:
# app/models/user.rb class User < ApplicationRecord has_many :likes, dependent: :destroy has_many :liked_posts, through: :likes, source: :post end # app/models/post.rb class Post < ApplicationRecord has_many :likes, dependent: :destroy has_many :likers, through: :likes, source: :user end # app/models/like.rb class Like < ApplicationRecord belongs_to :user belongs_to :post # 模型层验证:同一用户不能重复点赞同一帖子 validates :user_id, uniqueness: { scope: :post_id, message: "已经点赞过这个帖子啦" } end
数据库索引
为了在数据库层面防止重复点赞,给likes表加唯一索引:
# 生成迁移文件 rails generate migration AddUniqueIndexToLikes user_id:integer post_id:integer # 编辑迁移文件 class AddUniqueIndexToLikes < ActiveRecord::Migration[7.0] def change add_index :likes, [:user_id, :post_id], unique: true end end # 执行迁移 rails db:migrate
步骤2:添加状态切换的控制器Action
在PostsController里新增一个toggle_like方法,负责处理点赞/取消点赞的逻辑:
# app/controllers/posts_controller.rb def toggle_like # 找到目标帖子 post = Post.find(params[:id]) # 检查当前用户是否已经点赞 existing_like = post.likes.find_by(user_id: current_user.id) if existing_like # 已点赞:删除记录 existing_like.destroy render json: { status: :unliked, message: "取消点赞成功" }, status: :ok else # 未点赞:创建记录 begin post.likes.create!(user_id: current_user.id) render json: { status: :liked, message: "点赞成功" }, status: :created rescue ActiveRecord::RecordNotUnique # 处理竞态问题:如果同时发送多个点赞请求,数据库索引会拦截,返回已点赞状态 render json: { status: :liked, message: "已经点赞过这个帖子啦" }, status: :ok end end end
步骤3:配置路由
在routes.rb里给帖子资源添加这个成员路由:
# config/routes.rb Rails.application.routes.draw do resources :posts do # 给单个帖子添加toggle_like端点 put :toggle_like, on: :member end end
步骤4:前端调用方式
现在前端只需要给/posts/:id/toggle_like发送一个PUT请求,不用管当前是否点赞。请求完成后,根据后端返回的status字段(liked或unliked)更新UI即可。
比如用JavaScript的示例:
async function togglePostLike(postId) { try { const response = await fetch(`/posts/${postId}/toggle_like`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${yourAuthToken}` // 如果用JWT或类似认证 } }); const data = await response.json(); // 根据返回的status更新UI if (data.status === 'liked') { // 显示已点赞状态 document.getElementById(`like-btn-${postId}`).textContent = '取消点赞'; } else { // 显示未点赞状态 document.getElementById(`like-btn-${postId}`).textContent = '点赞'; } } catch (error) { console.error('点赞/取消点赞失败:', error); } }
为什么这个方案更好?
- 减少前端逻辑:不用维护点赞状态,避免前后端状态不同步的问题
- 数据一致性保障:模型验证+数据库索引双重兜底,绝对不会出现重复点赞记录
- 处理竞态问题:捕获
RecordNotUnique异常,避免用户快速点击导致的错误 - 语义清晰:单一请求对应状态切换,逻辑更直观
内容的提问来源于stack exchange,提问作者Krilo Max
相关产品推荐
相关产品推荐

