本文目录导读:

我来给你详细讲解PHP签到功能中连续签到计算的实现方法。
数据库表设计
首先创建签到记录表:
CREATE TABLE `sign_records` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '用户ID', `sign_date` date NOT NULL COMMENT '签到日期', `sign_time` datetime NOT NULL COMMENT '签到时间', `continuous_days` int(11) DEFAULT 0 COMMENT '连续签到天数', PRIMARY KEY (`id`), UNIQUE KEY `user_date_unique` (`user_id`, `sign_date`), KEY `user_id_index` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
PHP核心签到逻辑
<?php
class SignService {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
/**
* 执行签到
* @param int $userId 用户ID
* @return array 签到结果
*/
public function sign($userId) {
$today = date('Y-m-d');
$yesterday = date('Y-m-d', strtotime('-1 day'));
// 检查今天是否已签到
if ($this->isSigned($userId, $today)) {
return ['code' => 0, 'msg' => '今天已经签到过了'];
}
try {
$this->pdo->beginTransaction();
// 获取昨天是否签到
$yesterdaySigned = $this->isSigned($userId, $yesterday);
// 获取昨天的连续签到天数
$continuousDays = 0;
if ($yesterdaySigned) {
$continuousDays = $this->getContinuousDays($userId, $yesterday);
}
// 计算新的连续天数
$newContinuousDays = $yesterdaySigned ? ($continuousDays + 1) : 1;
// 插入签到记录
$sql = "INSERT INTO sign_records
(user_id, sign_date, sign_time, continuous_days)
VALUES (?, ?, NOW(), ?)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$userId, $today, $newContinuousDays]);
$this->pdo->commit();
return [
'code' => 1,
'msg' => '签到成功',
'continuous_days' => $newContinuousDays
];
} catch (Exception $e) {
$this->pdo->rollBack();
return ['code' => 0, 'msg' => '签到失败:' . $e->getMessage()];
}
}
/**
* 检查某天是否已签到
*/
public function isSigned($userId, $date) {
$sql = "SELECT COUNT(*) FROM sign_records
WHERE user_id = ? AND sign_date = ?";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$userId, $date]);
return $stmt->fetchColumn() > 0;
}
/**
* 获取某天的连续签到天数
*/
public function getContinuousDays($userId, $date) {
$sql = "SELECT continuous_days FROM sign_records
WHERE user_id = ? AND sign_date = ?";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$userId, $date]);
return (int)$stmt->fetchColumn();
}
/**
* 获取用户签到日历
* @param int $userId 用户ID
* @param string $month 月份(2024-01)
*/
public function getSignCalendar($userId, $month) {
$startDate = $month . '-01';
$endDate = date('Y-m-t', strtotime($startDate));
$sql = "SELECT sign_date, continuous_days
FROM sign_records
WHERE user_id = ? AND sign_date BETWEEN ? AND ?
ORDER BY sign_date ASC";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$userId, $startDate, $endDate]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 计算连续签到(使用日期反向遍历)
* @param int $userId 用户ID
* @param date $date 目标日期
*/
public function calcContinuousDays($userId, $date = null) {
$date = $date ?: date('Y-m-d');
$continuousDays = 0;
$currentDate = $date;
// 如果今天没签到,从昨天开始计算
if (!$this->isSigned($userId, $date)) {
$currentDate = date('Y-m-d', strtotime($date . ' -1 day'));
}
// 循环向前统计连续签到天数
while ($this->isSigned($userId, $currentDate)) {
$continuousDays++;
$currentDate = date('Y-m-d', strtotime($currentDate . ' -1 day'));
}
return $continuousDays;
}
/**
* 使用SQL直接计算连续天数(效率更高)
*/
public function calcContinuousDaysBySQL($userId, $date = null) {
$date = $date ?: date('Y-m-d');
// 方法:找到最近的无签到日期,然后计算差值
$sql = "SELECT MAX(sign_date) as last_sign_date
FROM sign_records
WHERE user_id = ? AND sign_date <= ?
AND sign_date NOT IN (
SELECT DATE_ADD(sign_date, INTERVAL 1 DAY)
FROM sign_records
WHERE user_id = ? AND sign_date < ?
)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$userId, $date, $userId, $date]);
$result = $stmt->fetch();
if ($result && $result['last_sign_date']) {
// 如果今天已签到,从今天开始计算;否则从昨天开始
$startDate = $this->isSigned($userId, $date) ? $date :
($this->isSigned($userId, date('Y-m-d', strtotime($date . ' -1 day'))) ?
date('Y-m-d', strtotime($date . ' -1 day')) : null);
if ($startDate) {
return (int)((strtotime($startDate) - strtotime($result['last_sign_date'])) / 86400) + 1;
}
}
return 0;
}
/**
* 获取签到奖励(根据连续天数)
*/
public function getSignRewards($continuousDays) {
$rewards = [
1 => ['points' => 1, 'desc' => '第一天'],
2 => ['points' => 2, 'desc' => '第二天'],
3 => ['points' => 3, 'desc' => '第三天'],
4 => ['points' => 4, 'desc' => '第四天'],
5 => ['points' => 5, 'desc' => '第五天'],
6 => ['points' => 6, 'desc' => '第六天'],
];
// 超过7天,每7天为一个周期
if ($continuousDays > 6) {
$cycle = floor(($continuousDays - 1) / 7);
$dayInCycle = ($continuousDays - 1) % 7 + 1;
if ($dayInCycle == 7) {
return ['points' => 10 + $cycle * 2, 'desc' => "连续签到{$continuousDays}天,额外奖励"];
}
return $rewards[$dayInCycle];
}
return $rewards[$continuousDays];
}
}
使用示例
<?php
// 初始化
$pdo = new PDO('mysql:host=localhost;dbname=test', 'username', 'password');
$signService = new SignService($pdo);
// 用户签到
$userId = 1;
$result = $signService->sign($userId);
if ($result['code'] == 1) {
echo "签到成功!连续签到" . $result['continuous_days'] . "天";
// 获取奖励
$rewards = $signService->getSignRewards($result['continuous_days']);
echo "获得奖励:" . $rewards['points'] . "积分";
} else {
echo $result['msg'];
}
// 查看用户当月签到情况
$signCalendar = $signService->getSignCalendar($userId, '2024-01');
echo "本月签到:";
foreach ($signCalendar as $record) {
echo $record['sign_date'] . " ";
}
// 简单的连续天数计算(不使用缓存字段)
$continuous = $signService->calcContinuousDays($userId);
echo "当前连续签到:{$continuous}天";
// 获取签到状态
function getSignStatus() {
$userId = $_SESSION['user_id'];
$signService = new SignService($pdo);
$today = date('Y-m-d');
$isSigned = $signService->isSigned($userId, $today);
$todayContinuousDays = $isSigned ?
$signService->getContinuousDays($userId, $today) :
$signService->calcContinuousDays($userId);
return [
'is_signed' => $isSigned,
'continuous_days' => $todayContinuousDays
];
}
?>
前端展示代码
<!-- 签到按钮 -->
<div class="sign-container">
<button id="signBtn" onclick="doSign()">
<?php if ($isTodaySigned): ?>
今日已签到
<?php else: ?>
签到
<?php endif; ?>
</button>
<div id="signResult"></div>
</div>
<script>
function doSign() {
fetch('/api/sign.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_id: <?php echo $userId; ?>
})
})
.then(response => response.json())
.then(data => {
if (data.code == 1) {
document.getElementById('signResult').innerHTML =
'签到成功!连续签到' + data.continuous_days + '天';
} else {
alert(data.msg);
}
});
}
</script>
注意事项
- 唯一约束:利用数据库的唯一约束防止重复签到
- 事务处理:使用事务确保数据一致性
- 性能优化:为 user_id 和 sign_date 创建联合索引
- 缓存考虑:高频查询可使用 Redis 缓存
- 跨时区:如果用户跨时区,需要注意日期处理逻辑
这种实现方式简单高效,能够准确计算连续签到天数,并且易于维护和扩展。