如何用PHP项目实现签到系统:从零搭建高并发企业级签到功能
目录导读
- 签到系统的核心业务逻辑与数据库设计
- PHP实现连续签到与积分奖励算法
- 防止恶意刷签:时间校验与IP/设备限制
- 高并发场景下的Redis缓存优化方案
- 前端签到交互设计(Ajax+倒计时)
- 常见问题解答(QA)
签到系统的核心业务逻辑与数据库设计
业务需求拆解
签到系统需要满足以下核心功能:

- 每日签到:用户每天只能签到一次,记录签到日期。
- 连续签到奖励:连续签到天数增加,奖励积分、优惠券或虚拟道具。
- 补签功能(可选):允许用户消耗积分或付费补签遗漏天数。
- 数据统计:展示用户累计签到天数、当前连续天数、排行榜等。
数据库表结构设计
使用MySQL,建议建表如下:
-- 用户签到记录表 CREATE TABLE `user_sign` ( `id` int(11) NOT NULL AUTO_INCREMENT, `uid` int(11) NOT NULL COMMENT '用户ID', `sign_date` date NOT NULL COMMENT '签到日期(仅日期)', `sign_time` datetime NOT NULL COMMENT '签到时间(精确到时分秒)', `continue_days` int(11) DEFAULT 0 COMMENT '截止本次的连续天数', `reward_points` int(11) DEFAULT 0 COMMENT '本次签到获得积分', `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_uid_date` (`uid`,`sign_date`), KEY `idx_uid` (`uid`), KEY `idx_date` (`sign_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 积分总览表 CREATE TABLE `user_points` ( `uid` int(11) PRIMARY KEY, `total_points` int(11) DEFAULT 0, `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB;
设计要点:
UNIQUE KEY (uid, sign_date)确保每日只能签一次。sign_date用date类型而不是datetime,方便比较和索引。continue_days字段记录连续天数,避免每次计算消耗性能。
PHP实现连续签到与积分奖励算法
核心逻辑步骤
// 签到控制器 - signIn()
public function signIn(Request $request, $uid)
{
$today = date('Y-m-d');
$yesterday = date('Y-m-d', strtotime('-1 day'));
// 1. 检查今日是否已签到
$todaySign = DB::table('user_sign')
->where('uid', $uid)
->where('sign_date', $today)
->first();
if ($todaySign) {
return ['code' => 0, 'msg' => '今日已签到,不可重复'];
}
// 2. 查询最后一次签到记录
$lastSign = DB::table('user_sign')
->where('uid', $uid)
->orderBy('sign_date', 'desc')
->first();
// 3. 计算连续天数
$newContinue = 1; // 默认首次签到
if ($lastSign && $lastSign->sign_date == $yesterday) {
$newContinue = $lastSign->continue_days + 1;
}
// 4. 根据连续天数计算奖励积分(阶梯奖励)
$reward = $this->calculateReward($newContinue);
// 5. 写入签到记录
DB::table('user_sign')->insert([
'uid' => $uid,
'sign_date' => $today,
'sign_time' => date('Y-m-d H:i:s'),
'continue_days' => $newContinue,
'reward_points' => $reward,
]);
// 6. 累加积分到用户总表
DB::table('user_points')
->where('uid', $uid)
->increment('total_points', $reward);
return ['code' => 1, 'msg' => '签到成功', 'data' => [
'continue_days' => $newContinue,
'reward' => $reward
]];
}
// 积分奖励算法(可根据运营需求调整)
private function calculateReward($days)
{
if ($days == 1) return 5;
if ($days >= 2 && $days <= 7) return 10;
if ($days >= 8 && $days <= 30) return 15;
if ($days > 30) return 20;
return 0;
}
算法优化建议
- 阶梯奖励:典型模式是“1-7天10分,8-30天15分,30天以上20分”,可在数据库或配置文件中定义规则,方便运营动态调整。
- 特殊奖励:连续签到第7天、第30天可额外发放勋章或大额积分。
防止恶意刷签:时间校验与IP/设备限制
服务端时间校验
问题:如果客户端篡改时间戳,可能导致每天多次签到。
解法:永远使用服务器时间而非客户端传递的时间。
// 拒绝客户端传time参数,只取PHP的date()或time()
$serverDate = date('Y-m-d');
IP频率限制
使用Redis的INCR统计同一IP单位时间内请求次数:
$ip = $request->getClientIp();
$key = 'sign:ip_limit:' . $ip . ':' . date('YmdH'); // 按小时统计
$count = Redis::incr($key);
if ($count > 10) { // 每小时最多10次签到请求
return ['code' => -1, 'msg' => '请求过于频繁'];
}
Redis::expire($key, 3600);
设备指纹限制
对同一uid加锁,防止并发请求导致多次入库:
$lockKey = 'sign:lock:' . $uid;
if (Redis::setnx($lockKey, 1) == 0) {
return ['code' => -2, 'msg' => '正在处理中'];
}
Redis::expire($lockKey, 5); // 锁5秒后自动释放
// ... 执行签到逻辑 ...
Redis::del($lockKey);
高并发场景下的Redis缓存优化方案
缓存策略
场景:用户打开签到页面时需要显示“今日是否已签到”及“连续天数”。
SQL问题:每个用户打开页面都查数据库,高并发下数据库压力大。
缓存方案:
- Redis Hash 存储用户签到状态:
// 签到后写入缓存
Redis::hSet('user:sign:' . $uid, 'today', '1');
Redis::hSet('user:sign:' . $uid, 'continue', $newContinue);
Redis::expire('user:sign:' . $uid, 86400); // 自然日过期
// 查询时优先读缓存
$signData = Redis::hgetall('user:sign:' . $uid);
if (!$signData) {
// 缓存不存在再查库,并回写缓存
}
- 预加载:用户登录时加载最近7天签到状态存入Redis,避免每日反复查询。
防击穿思路
- 使用布隆过滤器快速判断用户是否从未签过到,减少无效数据库查询。
- 热点用户签到数据失效时,使用互斥锁只让一个请求查库重建缓存。
前端签到交互设计(Ajax+倒计时)
用户体验要点
- 签到按钮状态:已签到显示灰色“已签到”,未签到显示可点击状态。
- 连续天数动画:展示日历或进度条,高亮已签到的日期。
- 防误触:签到成功后按钮立即禁用,并显示“签到成功”。
- 倒计时:若今日已签到,显示“距离下次签到还有:23:59:59”。
核心Ajax代码示例(jQuery)
$.ajax({
url: '/api/sign-in',
method: 'POST',
data: { _token: '{{ csrf_token() }}' },
success: function(res) {
if (res.code === 1) {
$('#signBtn').text('已签到').prop('disabled', true);
$('#continueDays').text(res.data.continue_days + '天');
$('#rewardPoints').text('+' + res.data.reward);
startCountdown();
} else {
alert(res.msg);
}
}
});
倒计时实现(JavaScript)
function startCountdown() {
var now = new Date();
var tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
var diff = Math.floor((tomorrow - now) / 1000);
// 每1秒更新显示
setInterval(function() {
var h = Math.floor(diff / 3600);
var m = Math.floor((diff % 3600) / 60);
var s = diff % 60;
$('#countdown').text(h + ':' + pad(m) + ':' + pad(s));
diff--;
}, 1000);
}
常见问题解答(QA)
Q1:PHP签到系统如何实现补签功能?
A:补签本质是“允许用户给过去的某一天添加签到记录”。
- 用户消耗积分(如50分/次);
- 后端校验补签日期不能是今天,且不能重复补签;
- 补签成功后,需重新计算补签日期至今天之间的连续天数,并更新
continue_days字段。
示例代码(校验补签日期逻辑):
if ($signDate >= date('Y-m-d')) {
return ['code' => 0, 'msg' => '只能补签过去日期'];
}
// 检查该日期是否已有记录
$existed = DB::table('user_sign')->where(['uid' => $uid, 'sign_date' => $signDate])->exists();
if ($existed) {
return ['code' => 0, 'msg' => '该日期已签到,不可补签'];
}
Q2:连续签到算法中,如果用户昨天没签到但前天签到了,如何正确计算?
A:我们的算法只检查“最后一次签到日期是否是昨天”,如果昨天没签到,则连续天数重置为1。
如果需要更复杂的“间隔补签”逻辑,可以在计算时往前扫描最近30天,找到最长的连续段,但一般业务场景中,用户期望的是“连续性”,断签重置是常见设计。
Q3:PHP签到系统如何应对高并发秒杀场景?
A:
- 使用Redis原子操作(
INCR)替代数据库锁; - 采用消息队列(如RabbitMQ、Redis队列)异步处理积分发放,延迟但保证不丢;
- 签到记录表可进行水平分表,按用户ID模16分到不同子表;
- 积分变动操作使用MySQL事务,配合
FOR UPDATE锁定用户积分行。
Q4:签到系统中的“连续天数”定义是什么?跨午夜如何处理?
A:
- 连续天数 = 用户连续签到的天数,不包含今天的前一天必须签到,否则中断。
- 跨午夜:以服务器时间
00:00:00为基准,到达0点则开启新一天的签到,倒计时显示“距离下次签到剩余时间”就是基于明天0点 - 当前时间计算。
Q5:运营后台如何查看签到统计数据?
A:使用SQL聚合查询:
-- 今日签到总人数 SELECT COUNT(DISTINCT uid) FROM user_sign WHERE sign_date = CURDATE(); -- 用户连续签到排行(当前连续) SELECT uid, continue_days FROM user_sign WHERE sign_date = CURDATE() ORDER BY continue_days DESC LIMIT 100;
建议这些统计查询使用定时任务+汇总表,避免实时扫描大表。
一套可落地的PHP签到系统方案
| 模块 | 技术选型 | 核心优化点 |
|---|---|---|
| 数据库 | MySQL InnoDB | 唯一索引防重复,date类型节省空间 |
| 后端 | PHP + Laravel/ThinkPHP | 服务器时间校验,分布式锁防并发 |
| 缓存 | Redis | 签到状态预缓存,IP限流,消息队列 |
| 前端 | Ajax + 倒计时 | 即时反馈,防抖处理 |
| 安全 | CSRF、Token | 接口签名校验,防参数篡改 |
通过以上完整的数据库设计、业务逻辑、高并发优化以及前端交互,你可以快速搭建一个生产级的PHP签到系统,建议在实际部署时配合压力测试(如使用JMeter模拟1000并发请求),根据数据库慢查询日志进一步优化索引和缓存策略。
最后提醒:本文所有代码示例基于PHP7+,并使用了Eloquent ORM语法,若使用原生PHP或其它框架,只需将数据库查询方法替换为对应实现即可。