必须用数据库记录点赞关系,因redis无外键、时间字段和关联查询能力,无法支持统计、取消、防刷等业务;mysql需建(user_id,post_id)联合唯一索引、post_id普通索引及created_at字段,并用事务实现原子化点/取消操作。

ThinkPHP 的点赞功能不能只靠 session 或 cookie 做去重,必须用数据库记录用户与点赞关系,否则无法统计、无法取消、无法防刷。
为什么不能只用 Redis 缓存点赞状态
Redis 确实快,但单靠它做点赞状态管理会丢失关键业务信息:谁在什么时候点了哪条内容?取消点赞时怎么反查?后台要导出“本周被点赞最多的 10 篇文章”怎么办?Redis 没有外键、没有时间字段、不支持关联查询。
- 缓存只能作临时校验层,
user_id+post_id的唯一索引必须落在 MySQL 表里 - 如果把点赞数也存在 Redis,需用 Lua 脚本保证原子性,但一旦 Redis 故障或重启,
like_count和实际记录数就会不一致 - ThinkPHP 的
Cache::get()默认不带过期时间,容易误留脏数据;而Db::table('like')->where(...)->find()虽慢一点,但语义清晰、可审计
数据库表设计要点(含唯一约束和索引)
建表不是加个 id、user_id、post_id 就完事。缺少约束会导致重复插入、查重变慢、统计不准。
- 主键用自增
id,方便分页和日志追踪 - 联合唯一索引必须是
(user_id, post_id),不是(post_id, user_id)——因为查询“某用户点过哪些”比“某文章被谁点过”更频繁 - 加普通索引
post_id,用于SELECT COUNT(*) FROM `like` WHERE post_id = ?统计点赞数 - 不要加
created_at字段?错。哪怕当前不用,留着以后做“24 小时内点赞热榜”或风控限频就省得加字段、锁表、改迁移
ThinkPHP 中实现“点/取消”双态操作的核心逻辑
一个接口处理两种状态,关键不在前端传参,而在后端如何用一次查询+一次写入完成原子切换。
- 先查
Db::name('like')->where(['user_id' => $uid, 'post_id' => $pid])->find(),存在则执行删除,不存在则插入 - 别用
insertGetId()后再判断是否重复——并发下可能两个请求同时查到“不存在”,然后都插入成功 - 推荐用
Db::name('like')->replace()->data([...]),前提是表有PRIMARY KEY或UNIQUE KEY;但注意:replace 是 delete+insert,会改变自增 ID,不适合需要稳定日志 ID 的场景 - 更稳妥的是用事务:
Db::startTrans()→ 查 → 插或删 →Db::commit(),并捕获PDOException处理唯一键冲突
前端传参和响应格式的常见坑
很多人卡在“点了没反应”,其实问题常出在前后端约定不一致。
- 前端传
post_id必须是整型,不要传字符串"123";ThinkPHP 的where()对字符串数字会自动转,但遇到"123abc"就静默失效 - 后端返回不要只写
['code'=>0,'msg'=>'ok'],至少带is_liked(布尔)和like_count(当前总数),避免前端再发一次请求查状态 - 别在控制器里直接 echo JSON;用
json(['is_liked'=>true, 'like_count'=>127]),它会自动设 Content-Type 并结束响应,防止后续代码意外输出干扰 JSON 格式 - CSRF 验证默认开启,AJAX 请求记得带上
X-CSRF-TOKENheader,值从模板里的{:token()}获取
真正难的不是写通点赞,而是当并发量上来、运营开始查“昨天新增点赞中 63% 来自新用户”、风控要求“同一 IP 10 分钟内最多点 5 次”时,你当初建的那张表、写的那个事务、留的那几个字段,还撑不撑得住。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











