ThinkPHP签到必须用continuous_days字段实时维护而非查询推导,因高并发下全表扫描、时区不一致、竞态条件会导致数据错误;需事务+唯一索引+NOW()更新主表,并辅以Redis幂等token和异步写日志。

ThinkPHP签到功能不是“查历史记录再统计连续天数”,而是靠 continuous_days 字段实时维护——这个字段必须存在、必须更新、不能靠 COUNT 或子查询推导,否则高并发下必出错。
为什么不能用「签到日志表 + GROUP BY + DATEDIFF」算连续天数
这种做法看似直观,实际在生产环境里是隐患源头:
- 每次签到都要全表扫描或范围扫描
sign_log表,COUNT(*)+WHERE user_id = ? AND sign_in_date >= ?在百万级数据时延迟飙升 - 跨日判断依赖 PHP 的
date('Y-m-d')和数据库时间不一致,比如服务器时区设错、NTP 未同步,会导致“明明昨天签了,今天却重置” - 并发请求下,两个请求同时读到同一条旧记录(如
last_sign_at = '2026-05-13'),都判断为“间隔 1 天”,结果都执行+1,本该是 2 的连续天数变成 2 次写入后仍为 2 - 没有唯一约束时,可能重复插入同一天记录,后续逻辑全乱
必须用事务 + 唯一索引 + NOW() 更新主表
核心逻辑只作用于一张主表(如 user_sign),结构至少含:user_id(加唯一索引)、last_sign_at(datetime)、continuous_days(tinyint)、total_sign_days(可选)。
关键实操点:
立即学习“PHP免费学习笔记(深入)”;
-
last_sign_at必须用数据库函数NOW()赋值,别用 PHP 的date('Y-m-d H:i:s') - 判断是否今日已签到,只比日期部分:
DATE(last_sign_at) = CURDATE()或 PHP 端用date('Y-m-d', strtotime($record['last_sign_at'])) === date('Y-m-d') - 更新必须用
Db::name('user_sign')->update(...),不用save(),避免模型自动过滤字段或触发冗余事件 - 整个流程必须包裹在
Db::transaction()中,哪怕只有一条 update —— 并发安全不是靠运气
示例片段(TP6+):
Db::transaction(function () use ($userId) {
$record = Db::name('user_sign')->where('user_id', $userId)->find();
$today = date('Y-m-d');
$lastDate = $record ? date('Y-m-d', strtotime($record['last_sign_at'])) : '';
if ($lastDate === $today) {
throw new Exception('今日已签到');
}
$continuous = ($lastDate === date('Y-m-d', strtotime('-1 day')))
? $record['continuous_days'] + 1
: 1;
Db::name('user_sign')->update([
'user_id' => $userId,
'last_sign_at' => date('Y-m-d H:i:s'), // ← 这里必须是 NOW() 级精度,且用 PHP 生成也得严格对齐时区
'continuous_days' => $continuous,
'total_sign_days' => ($record['total_sign_days'] ?? 0) + 1,
]);
});
Redis 缓存和幂等 token 是防并发的第二道防线
光靠数据库事务还不够。当秒杀级签到流量来临时(比如运营活动),MySQL 行锁会排队,响应变慢甚至超时。这时要加一层缓存校验:
- 用户点击签到前,前端生成一个 token(如 UUID v4),传给后端
- 后端先用
SETNX redis_key token EX 30尝试占位,失败则直接返回“重复提交” - 事务成功后,删掉该 key;失败则保留,防止重试污染
- 注意:token 必须绑定
user_id,key 形如sign:token:{$userId},避免不同用户 token 冲突
这个 token 不是用来替代数据库校验的,而是把大量无效重试挡在 DB 之前。
如果业务要求保留明细,log 表必须与主表解耦
很多团队误以为“明细表 + 主表聚合”更清晰,结果主逻辑被拖垮。正确做法是:
- 所有连续天数、奖励发放、状态判断,只读写主表(
user_sign) - 签到成功后,异步写入
sign_log表(可用队列或简单 insert ignore) -
sign_log表只要user_id、sign_date、created_at即可,加联合唯一索引(user_id, sign_date) - 不要在签到主流程里做
INSERT INTO sign_log ... SELECT ... FROM user_sign这类操作
主表负责状态,log 表只负责归档——职责一分开,性能和稳定性就立竿见影。



















