您好,登录后才能下订单哦!
密码登录
登录注册
点击 登录注册 即表示同意《亿速云用户服务条款》
# PHP与JS实现日历签到系统的完整指南
日历签到系统是现代Web应用中常见的功能模块,广泛应用于电商、社交、教育等领域。本文将深入探讨如何使用PHP和JavaScript从零开始构建一个功能完善的日历签到系统。
## 目录
1. [系统需求分析](#系统需求分析)
2. [数据库设计](#数据库设计)
3. [后端PHP实现](#后端php实现)
4. [前端JS日历展示](#前端js日历展示)
5. [签到功能实现](#签到功能实现)
6. [连续签到统计](#连续签到统计)
7. [奖励系统集成](#奖励系统集成)
8. [性能优化](#性能优化)
9. [安全防护](#安全防护)
10. [完整代码示例](#完整代码示例)
## 系统需求分析
### 功能需求
1. **日历展示**:按月展示日历视图
2. **签到状态标记**:已签到日期特殊标识
3. **签到操作**:用户点击当日日期完成签到
4. **连续签到统计**:显示连续签到天数
5. **签到奖励**:根据连续签到天数发放奖励
### 技术选型
- **后端**:PHP 7.4+ (处理业务逻辑和数据存储)
- **前端**:
- JavaScript (交互逻辑)
- CSS3 (样式设计)
- AJAX (前后端通信)
- **数据库**:MySQL 5.7+ (数据持久化)
## 数据库设计
### 用户表(users)
```sql
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`last_sign_date` date DEFAULT NULL,
`consecutive_days` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `sign_records` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`sign_date` date NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `user_date` (`user_id`,`sign_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `reward_rules` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`days_required` int(11) NOT NULL,
`reward_type` varchar(50) NOT NULL,
`reward_value` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
class DB {
private static $instance = null;
private $conn;
private function __construct() {
$this->conn = new PDO(
"mysql:host=localhost;dbname=sign_calendar",
"username",
"password",
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
);
}
public static function getInstance() {
if (!self::$instance) {
self::$instance = new DB();
}
return self::$instance;
}
public function getConnection() {
return $this->conn;
}
}
class SignService {
private $db;
public function __construct() {
$this->db = DB::getInstance()->getConnection();
}
/**
* 用户签到
*/
public function sign($userId) {
try {
$this->db->beginTransaction();
$today = date('Y-m-d');
$lastSignDate = $this->getLastSignDate($userId);
// 检查今天是否已签到
if ($lastSignDate === $today) {
throw new Exception('今天已经签到过了');
}
// 计算连续签到天数
$consecutiveDays = $this->calculateConsecutiveDays($userId, $today);
// 更新用户签到信息
$stmt = $this->db->prepare(
"UPDATE users SET last_sign_date = ?, consecutive_days = ? WHERE id = ?"
);
$stmt->execute([$today, $consecutiveDays, $userId]);
// 插入签到记录
$stmt = $this->db->prepare(
"INSERT INTO sign_records (user_id, sign_date) VALUES (?, ?)"
);
$stmt->execute([$userId, $today]);
// 检查是否达到奖励条件
$rewards = $this->checkRewards($userId, $consecutiveDays);
$this->db->commit();
return [
'success' => true,
'consecutive_days' => $consecutiveDays,
'rewards' => $rewards
];
} catch (Exception $e) {
$this->db->rollBack();
return ['success' => false, 'message' => $e->getMessage()];
}
}
/**
* 获取用户签到日历
*/
public function getSignCalendar($userId, $year, $month) {
$startDate = "$year-$month-01";
$endDate = date('Y-m-t', strtotime($startDate));
$stmt = $this->db->prepare(
"SELECT sign_date FROM sign_records
WHERE user_id = ? AND sign_date BETWEEN ? AND ?"
);
$stmt->execute([$userId, $startDate, $endDate]);
$signedDates = array_column($stmt->fetchAll(), 'sign_date');
return [
'year' => $year,
'month' => $month,
'signed_dates' => $signedDates
];
}
// 其他私有方法...
}
class Calendar {
constructor(options) {
this.container = document.querySelector(options.container);
this.year = options.year || new Date().getFullYear();
this.month = options.month || new Date().getMonth() + 1;
this.signedDates = options.signedDates || [];
this.onSign = options.onSign || function() {};
this.onMonthChange = options.onMonthChange || function() {};
this.render();
this.bindEvents();
}
render() {
const daysInMonth = new Date(this.year, this.month, 0).getDate();
const firstDay = new Date(this.year, this.month - 1, 1).getDay();
let html = `
<div class="calendar-header">
<button class="prev-month"><</button>
<h2>${this.year}年${this.month}月</h2>
<button class="next-month">></button>
</div>
<div class="calendar-weekdays">
<div>日</div><div>一</div><div>二</div>
<div>三</div><div>四</div><div>五</div><div>六</div>
</div>
<div class="calendar-days">
`;
// 填充空白格
for (let i = 0; i < firstDay; i++) {
html += `<div class="empty"></div>`;
}
// 填充日期
const today = new Date();
const isCurrentMonth = today.getFullYear() === this.year &&
today.getMonth() + 1 === this.month;
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${this.year}-${this.month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
const isSigned = this.signedDates.includes(dateStr);
const isToday = isCurrentMonth && day === today.getDate();
const canSign = isToday && !isSigned;
let className = 'day';
if (isSigned) className += ' signed';
if (isToday) className += ' today';
if (canSign) className += ' can-sign';
html += `<div class="${className}" data-date="${dateStr}">${day}</div>`;
}
html += `</div>`;
this.container.innerHTML = html;
}
bindEvents() {
// 月份切换
this.container.querySelector('.prev-month').addEventListener('click', () => {
this.changeMonth(-1);
});
this.container.querySelector('.next-month').addEventListener('click', () => {
this.changeMonth(1);
});
// 签到点击
this.container.addEventListener('click', (e) => {
const dayElement = e.target.closest('.day.can-sign');
if (dayElement) {
const date = dayElement.dataset.date;
this.onSign(date, dayElement);
}
});
}
changeMonth(offset) {
let newMonth = this.month + offset;
let newYear = this.year;
if (newMonth > 12) {
newMonth = 1;
newYear++;
} else if (newMonth < 1) {
newMonth = 12;
newYear--;
}
this.month = newMonth;
this.year = newYear;
this.onMonthChange(newYear, newMonth);
}
updateSignedDates(signedDates) {
this.signedDates = signedDates;
this.render();
}
}
document.addEventListener('DOMContentLoaded', function() {
const userId = 1; // 实际应用中从session获取
let currentYear = new Date().getFullYear();
let currentMonth = new Date().getMonth() + 1;
// 获取初始日历数据
fetchSignCalendar(userId, currentYear, currentMonth);
// 初始化日历实例
const calendar = new Calendar({
container: '#calendar-container',
year: currentYear,
month: currentMonth,
signedDates: [],
onSign: function(date, element) {
handleSign(userId, date, element);
},
onMonthChange: function(year, month) {
currentYear = year;
currentMonth = month;
fetchSignCalendar(userId, year, month);
}
});
// 获取用户连续签到天数
fetchConsecutiveDays(userId);
});
function fetchSignCalendar(userId, year, month) {
fetch(`api/get_sign_calendar.php?user_id=${userId}&year=${year}&month=${month}`)
.then(response => response.json())
.then(data => {
if (data.success) {
calendar.updateSignedDates(data.signed_dates);
} else {
console.error('获取日历数据失败:', data.message);
}
});
}
function handleSign(userId, date, element) {
fetch('api/sign.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id: userId,
date: date
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
element.classList.remove('can-sign');
element.classList.add('signed');
updateConsecutiveDays(data.consecutive_days);
if (data.rewards && data.rewards.length > 0) {
showRewards(data.rewards);
}
} else {
alert('签到失败: ' + data.message);
}
});
}
private function calculateConsecutiveDays($userId, $today) {
$yesterday = date('Y-m-d', strtotime('-1 day', strtotime($today)));
$stmt = $this->db->prepare(
"SELECT consecutive_days, last_sign_date FROM users WHERE id = ?"
);
$stmt->execute([$userId]);
$user = $stmt->fetch();
// 新用户首次签到
if (!$user['last_sign_date']) {
return 1;
}
// 昨天签到了,连续天数+1
if ($user['last_sign_date'] === $yesterday) {
return $user['consecutive_days'] + 1;
}
// 今天已经签到过了(理论上不会进入这里)
if ($user['last_sign_date'] === $today) {
return $user['consecutive_days'];
}
// 签到中断,重新开始计数
return 1;
}
private function checkRewards($userId, $consecutiveDays) {
$stmt = $this->db->prepare(
"SELECT * FROM reward_rules WHERE days_required <= ?
AND NOT EXISTS (
SELECT 1 FROM user_rewards
WHERE user_id = ? AND reward_rule_id = reward_rules.id
)
ORDER BY days_required DESC"
);
$stmt->execute([$consecutiveDays, $userId]);
$eligibleRewards = $stmt->fetchAll();
$grantedRewards = [];
foreach ($eligibleRewards as $reward) {
// 发放奖励
$this->grantReward($userId, $reward['id']);
$grantedRewards[] = $reward;
}
return $grantedRewards;
}
private function grantReward($userId, $rewardRuleId) {
$stmt = $this->db->prepare(
"INSERT INTO user_rewards (user_id, reward_rule_id) VALUES (?, ?)"
);
$stmt->execute([$userId, $rewardRuleId]);
}
class SignServiceWithCache extends SignService {
private $redis;
public function __construct() {
parent::__construct();
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
}
public function getSignCalendar($userId, $year, $month) {
$cacheKey = "sign_calendar:{$userId}:{$year}-{$month}";
$cached = $this->redis->get($cacheKey);
if ($cached !== false) {
return json_decode($cached, true);
}
$result = parent::getSignCalendar($userId, $year, $month);
$this->redis->setex($cacheKey, 3600, json_encode($result));
return $result;
}
}
索引优化:
ALTER TABLE sign_records ADD INDEX idx_user_sign (user_id, sign_date);
查询优化:
// 使用JOIN替代多次查询
$stmt = $this->db->prepare(
"SELECT u.consecutive_days,
COUNT(sr.id) as signed_today,
MAX(sr.sign_date) as last_sign_date
FROM users u
LEFT JOIN sign_records sr ON sr.user_id = u.id AND sr.sign_date = ?
WHERE u.id = ?"
);
$stmt->execute([$today, $userId]);
输入验证:
$userId = filter_input(INPUT_POST, 'user_id', FILTER_VALIDATE_INT);
if (!$userId || $userId < 1) {
die(json_encode(['success' => false, 'message' => '无效的用户ID']));
}
CSRF防护:
// 前端添加CSRF Token
fetch('api/sign.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({/*...*/})
})
频率限制: “`php \(redis = new Redis(); \)redis->connect(‘127.0.0.1’);
\(key = "sign_limit:{\)userId}”; \(signAttempts = \)redis->incr(\(key); \)redis->expire($key, 60);
if ($signAttempts > 3) { die(json_encode([‘success’ => false, ‘message’ => ‘操作过于频繁’])); }
## 完整代码示例
### 后端API (sign.php)
```php
require_once 'DB.php';
require_once 'SignService.php';
header('Content-Type: application/json');
try {
$input = json_decode(file_get_contents('php://input'), true);
$userId = filter_var($input['user_id'] ?? 0, FILTER_VALIDATE_INT);
$date = filter_var($input['date'] ?? '', FILTER_SANITIZE_STRING);
if (!$userId) {
throw new Exception('无效的用户ID');
}
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
throw new Exception('无效的日期格式');
}
$signService = new SignService();
$result = $signService->sign($userId);
echo json_encode($result);
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
”`html <!DOCTYPE html>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。