php js怎么实现日历签到

发布时间:2021-07-19 15:54:01 作者:chen
来源:亿速云 阅读:389
# 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;

签到记录表(sign_records)

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;

奖励规则表(reward_rules)

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;

后端PHP实现

数据库连接类

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
        ];
    }
    
    // 其他私有方法...
}

前端JS日历展示

日历组件实现

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">&lt;</button>
                <h2>${this.year}年${this.month}月</h2>
                <button class="next-month">&gt;</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]);
}

性能优化

缓存策略

  1. Redis缓存:缓存用户签到状态和日历数据
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;
    }
}

数据库优化

  1. 索引优化

    ALTER TABLE sign_records ADD INDEX idx_user_sign (user_id, sign_date);
    
  2. 查询优化

    // 使用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]);
    

安全防护

  1. 输入验证

    $userId = filter_input(INPUT_POST, 'user_id', FILTER_VALIDATE_INT);
    if (!$userId || $userId < 1) {
       die(json_encode(['success' => false, 'message' => '无效的用户ID']));
    }
    
  2. 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({/*...*/})
    })
    
  3. 频率限制: “`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结构

”`html <!DOCTYPE html>

推荐阅读:
  1. php 会议预定系统
  2. 使用PHP怎么实现一个连续签到功能

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

php js

上一篇:VB.NET中如何使用OracleTransaction对象

下一篇:python中的EasyOCR库是什么

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》