PHP 用户协议更新通知

wen PHP项目 1

本文目录导读:

PHP 用户协议更新通知

  1. 数据库设计
  2. 核心PHP类实现
  3. 前端页面示例 agreement.php
  4. 后台任务脚本 cron/process_notifications.php
  5. 使用示例
  6. 推荐增强功能

我来为你提供PHP用户协议更新通知的完整实现方案,包含数据库设计、核心代码和发送逻辑。

数据库设计

-- 用户协议表
CREATE TABLE `user_agreements` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `version` varchar(20) NOT NULL COMMENT '协议版本号', varchar(200) NOT NULL COMMENT '协议标题',
  `content` text NOT NULL COMMENT '协议内容',
  `effective_date` datetime NOT NULL COMMENT '生效日期',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_version` (`version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 用户协议接受记录表
CREATE TABLE `user_agreement_acceptances` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL COMMENT '用户ID',
  `agreement_id` int(11) NOT NULL COMMENT '协议ID',
  `accept_ip` varchar(45) DEFAULT NULL COMMENT '接受IP',
  `accept_count` int(4) DEFAULT '1' COMMENT '查看次数',
  `last_accept_at` datetime DEFAULT NULL COMMENT '最后接受时间',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_agreement` (`user_id`,`agreement_id`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 用户协议通知记录表
CREATE TABLE `agreement_notifications` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL COMMENT '用户ID',
  `agreement_id` int(11) NOT NULL COMMENT '协议ID',
  `notify_type` enum('email','sms','push') NOT NULL DEFAULT 'email' COMMENT '通知方式',
  `status` tinyint(1) DEFAULT '0' COMMENT '0待发送 1成功 2失败',
  `retry_count` int(4) DEFAULT '0' COMMENT '重试次数',
  `error_msg` varchar(255) DEFAULT NULL COMMENT '错误信息',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  `sent_at` datetime DEFAULT NULL COMMENT '发送时间',
  PRIMARY KEY (`id`),
  KEY `idx_user_status` (`user_id`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

核心PHP类实现

1 协议管理类 AgreementManager.php

<?php
class AgreementManager {
    private $db;
    public function __construct($db) {
        $this->db = $db;
    }
    /**
     * 发布新版本协议
     */
    public function publishNewAgreement($title, $content, $effectiveDate = null) {
        $effectiveDate = $effectiveDate ?: date('Y-m-d H:i:s');
        $version = 'v' . date('YmdHis');
        try {
            $stmt = $this->db->prepare(
                "INSERT INTO user_agreements (version, title, content, effective_date) 
                 VALUES (?, ?, ?, ?)"
            );
            $stmt->execute([$version, $title, $content, $effectiveDate]);
            return $this->db->lastInsertId();
        } catch (PDOException $e) {
            error_log("发布协议失败: " . $e->getMessage());
            return false;
        }
    }
    /**
     * 获取当前有效协议
     */
    public function getLatestAgreement() {
        $stmt = $this->db->prepare(
            "SELECT * FROM user_agreements 
             WHERE effective_date <= NOW() 
             ORDER BY effective_date DESC 
             LIMIT 1"
        );
        $stmt->execute();
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    /**
     * 获取用户未接受的协议
     */
    public function getUnacceptedAgreements($userId) {
        $sql = "
            SELECT a.* FROM user_agreements a
            WHERE a.effective_date <= NOW()
            AND a.id NOT IN (
                SELECT agreement_id FROM user_agreement_acceptances 
                WHERE user_id = ?
            )
            ORDER BY a.effective_date DESC
        ";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$userId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    /**
     * 记录用户接受协议
     */
    public function acceptAgreement($userId, $agreementId, $ip = null) {
        try {
            // 检查是否已存在记录
            $stmt = $this->db->prepare(
                "SELECT * FROM user_agreement_acceptances 
                 WHERE user_id = ? AND agreement_id = ?"
            );
            $stmt->execute([$userId, $agreementId]);
            if ($stmt->rowCount() > 0) {
                // 更新现有记录
                $updateStmt = $this->db->prepare(
                    "UPDATE user_agreement_acceptances 
                     SET accept_count = accept_count + 1,
                         last_accept_at = NOW(),
                         accept_ip = ?
                     WHERE user_id = ? AND agreement_id = ?"
                );
                $updateStmt->execute([$ip, $userId, $agreementId]);
            } else {
                // 插入新记录
                $insertStmt = $this->db->prepare(
                    "INSERT INTO user_agreement_acceptances 
                     (user_id, agreement_id, accept_ip) 
                     VALUES (?, ?, ?)"
                );
                $insertStmt->execute([$userId, $agreementId, $ip]);
            }
            // 删除通知记录
            $deleteStmt = $this->db->prepare(
                "DELETE FROM agreement_notifications 
                 WHERE user_id = ? AND agreement_id = ?"
            );
            $deleteStmt->execute([$userId, $agreementId]);
            return true;
        } catch (PDOException $e) {
            error_log("接受协议失败: " . $e->getMessage());
            return false;
        }
    }
    /**
     * 检查用户是否已接受最新协议
     */
    public function hasAcceptedLatestAgreement($userId) {
        $latest = $this->getLatestAgreement();
        if (!$latest) return true;
        $stmt = $this->db->prepare(
            "SELECT COUNT(*) FROM user_agreement_acceptances 
             WHERE user_id = ? AND agreement_id = ?"
        );
        $stmt->execute([$userId, $latest['id']]);
        return $stmt->fetchColumn() > 0;
    }
}

2 通知发送类 AgreementNotifier.php

<?php
class AgreementNotifier {
    private $db;
    private $mailer;
    private $smsClient;
    public function __construct($db, $mailer = null, $smsClient = null) {
        $this->db = $db;
        $this->mailer = $mailer;
        $this->smsClient = $smsClient;
    }
    /**
     * 发送协议更新通知给所有用户
     */
    public function notifyAllUsers($agreementId, $userList = null) {
        // 获取所有用户
        if (!$userList) {
            $userList = $this->getAllUsers();
        }
        foreach ($userList as $user) {
            $this->createNotification($user['id'], $agreementId, 'email');
        }
        return $this->processPendingNotifications();
    }
    /**
     * 创建通知记录
     */
    public function createNotification($userId, $agreementId, $type = 'email') {
        $stmt = $this->db->prepare(
            "INSERT INTO agreement_notifications 
             (user_id, agreement_id, notify_type) 
             VALUES (?, ?, ?)"
        );
        $stmt->execute([$userId, $agreementId, $type]);
        return $this->db->lastInsertId();
    }
    /**
     * 处理待发送的通知队列
     */
    public function processPendingNotifications($limit = 100) {
        $stmt = $this->db->prepare(
            "SELECT n.*, u.email, u.username 
             FROM agreement_notifications n
             JOIN users u ON n.user_id = u.id
             WHERE n.status = 0
             LIMIT ?"
        );
        $stmt->execute([$limit]);
        $notifications = $stmt->fetchAll(PDO::FETCH_ASSOC);
        $successCount = 0;
        foreach ($notifications as $noti) {
            $result = $this->sendNotification($noti);
            if ($result) {
                $successCount++;
            }
        }
        return $successCount;
    }
    /**
     * 发送具体通知
     */
    private function sendNotification($notification) {
        try {
            switch ($notification['notify_type']) {
                case 'email':
                    $sent = $this->sendEmail($notification);
                    break;
                case 'sms':
                    $sent = $this->sendSMS($notification);
                    break;
                default:
                    $sent = false;
            }
            // 更新通知状态
            $status = $sent ? 1 : 2;
            $errorMsg = $sent ? null : '发送失败';
            $updateStmt = $this->db->prepare(
                "UPDATE agreement_notifications 
                 SET status = ?, sent_at = NOW(), error_msg = ?,
                     retry_count = retry_count + 1
                 WHERE id = ?"
            );
            $updateStmt->execute([$status, $errorMsg, $notification['id']]);
            return $sent;
        } catch (Exception $e) {
            $this->updateErrorStatus($notification['id'], $e->getMessage());
            return false;
        }
    }
    /**
     * 发送邮件通知
     */
    private function sendEmail($notification) {
        // 获取协议信息
        $stmt = $this->db->prepare(
            "SELECT * FROM user_agreements WHERE id = ?"
        );
        $stmt->execute([$notification['agreement_id']]);
        $agreement = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$agreement) return false;
        $subject = '【重要通知】用户协议更新 - ' . $agreement['version'];
        $emailContent = "
            <html>
            <body>
                <h2>尊敬的用户,您好!</h2>
                <p>我们的服务协议已更新至<strong>{$agreement['version']}</strong>版本。</p>
                <p>更新内容摘要:</p>
                <p>{$agreement['title']}</p>
                <p>生效日期:{$agreement['effective_date']}</p>
                <p>请点击以下链接查看完整的协议内容:</p>
                <p><a href='https://yourdomain.com/agreement/{$agreement['id']}'>查看协议详情</a></p>
                <p>如您未在7天内确认接受新协议,您的账户可能会受到限制。</p>
                <p>如有疑问,请联系客服。</p>
            </body>
            </html>
        ";
        // 使用你的邮件库发送,这里以PHPMailer为例
        if ($this->mailer) {
            $this->mailer->addAddress($notification['email']);
            $this->mailer->Subject = $subject;
            $this->mailer->isHTML(true);
            $this->mailer->Body = $emailContent;
            return $this->mailer->send();
        }
        return false;
    }
    /**
     * 发送短信通知(可选)
     */
    private function sendSMS($notification) {
        // 如果需要短信通知,在这里实现
        // 使用阿里云、腾讯云等短信服务
        return true;
    }
    /**
     * 更新错误状态
     */
    private function updateErrorStatus($notiId, $errorMsg) {
        $stmt = $this->db->prepare(
            "UPDATE agreement_notifications 
             SET status = 2, error_msg = ?,
                 retry_count = retry_count + 1
             WHERE id = ?"
        );
        $stmt->execute([$errorMsg, $notiId]);
    }
    /**
     * 获取所有用户
     */
    private function getAllUsers() {
        $stmt = $this->db->query("SELECT id, email, username FROM users");
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

3 用户协议管理控制器 AgreementController.php

<?php
class AgreementController {
    private $agreementManager;
    private $notifier;
    public function __construct($db, $mailer = null) {
        $this->agreementManager = new AgreementManager($db);
        $this->notifier = new AgreementNotifier($db, $mailer);
    }
    /**
     * 处理用户接受协议的请求
     */
    public function handleAcceptance($userId, $agreementId) {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            return json_encode(['error' => 'Invalid request method']);
        }
        $ip = $_SERVER['REMOTE_ADDR'];
        $result = $this->agreementManager->acceptAgreement($userId, $agreementId, $ip);
        return json_encode([
            'success' => $result,
            'message' => $result ? '已接受协议' : '接受协议失败'
        ]);
    }
    /**
     * 获取用户需要接受的协议列表
     */
    public function getPendingAgreements($userId) {
        $agreements = $this->agreementManager->getUnacceptedAgreements($userId);
        return json_encode($agreements);
    }
    /**
     * 发布新协议并通知用户
     */
    public function publishAndNotify($title, $content, $effectiveDate = null) {
        $agreementId = $this->agreementManager->publishNewAgreement(
            $title, $content, $effectiveDate
        );
        if ($agreementId) {
            // 后台任务异步处理(可以使用redis队列等)
            $this->notifier->notifyAllUsers($agreementId);
            return "协议发布成功,通知已发送";
        }
        return "协议发布失败";
    }
}

前端页面示例 agreement.php

<?php
// 检查用户登录
session_start();
require_once 'config/database.php';
require_once 'classes/AgreementManager.php';
$db = getDB();
$agreementManager = new AgreementManager($db);
$userId = $_SESSION['user_id'] ?? null;
if (!$userId) {
    header('Location: login.php');
    exit;
}
$pendingAgreements = $agreementManager->getUnacceptedAgreements($userId);
// 如果用户已接受所有协议,重定向到主页
if (empty($pendingAgreements)) {
    header('Location: dashboard.php');
    exit;
}
$currentAgreement = $pendingAgreements[0]; // 取最新的一个
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">用户协议更新</title>
    <meta name="csrf-token" content="<?php echo $_SESSION['csrf_token']; ?>">
    <style>
        body { font-family: 'Microsoft YaHei', sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
        .container { max-width: 800px; margin: 0 auto; background: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        .header { text-align: center; margin-bottom: 30px; }
        .header h1 { color: #333; }
        .agreement-content { 
            background: #f9f9f9; 
            padding: 20px; 
            border-radius: 5px;
            max-height: 400px;
            overflow-y: auto;
            margin-bottom: 20px;
            line-height: 1.6;
        }
        .actions { display: flex; justify-content: center; gap: 20px; }
        .btn { 
            padding: 10px 30px; 
            border: none; 
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
        }
        .btn-accept { background: #4CAF50; color: #fff; }
        .btn-decline { background: #f44336; color: #fff; }
        .checkbox-container { margin: 20px 0; }
        .checkbox-container label { font-size: 14px; }
        .error-msg { color: #f44336; text-align: center; margin-top: 10px; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>用户协议更新通知</h1>
            <p>版本:<?php echo htmlspecialchars($currentAgreement['version']); ?> | 
               生效日期:<?php echo htmlspecialchars($currentAgreement['effective_date']); ?>
            </p>
        </div>
        <div class="agreement-content">
            <?php echo nl2br(htmlspecialchars($currentAgreement['content'])); ?>
        </div>
        <div class="checkbox-container">
            <label>
                <input type="checkbox" id="acceptCheckbox_<?php echo $currentAgreement['id']; ?>">
                我已认真阅读并同意以上协议内容
            </label>
        </div>
        <div class="actions">
            <button class="btn btn-accept" onclick="acceptAgreement(<?php echo $currentAgreement['id']; ?>)">
                接受协议
            </button>
            <button class="btn btn-decline" onclick="declineAgreement()">
                不同意
            </button>
        </div>
        <div id="errorMsg" class="error-msg"></div>
    </div>
    <script>
        function acceptAgreement(agreementId) {
            const checkbox = document.getElementById('acceptCheckbox_' + agreementId);
            if (!checkbox.checked) {
                alert('请先勾选同意选项');
                return;
            }
            const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
            fetch('accept_agreement.php', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'X-CSRF-Token': csrfToken
                },
                body: 'agreement_id=' + agreementId
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    alert('感谢您的确认!');
                    // 重新加载以检查是否有其他待接受的协议
                    window.location.reload();
                } else {
                    document.getElementById('errorMsg').textContent = data.message;
                }
            })
            .catch(error => {
                console.error('Error:', error);
                document.getElementById('errorMsg').textContent = '网络错误,请稍后重试';
            });
        }
        function declineAgreement() {
            if (confirm('您确定不同意此协议吗?不同意将无法使用我们的服务。')) {
                // 处理拒绝逻辑
                fetch('decline_agreement.php', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ action: 'decline' })
                })
                .then(response => response.json())
                .then(data => {
                    window.location.href = 'logout.php';
                });
            }
        }
    </script>
</body>
</html>

后台任务脚本 cron/process_notifications.php

<?php
/**
 * 定时处理待发送通知(可配置crontab)
 * 建议每5分钟运行一次
 */
require_once '../config/database.php';
require_once '../classes/AgreementNotifier.php';
$db = getDB();
$notifier = new AgreementNotifier($db, $mailer);
// 处理待发送的通知
$processed = $notifier->processPendingNotifications();
// 记录日志
$logFile = '../logs/notifications_' . date('Y-m-d') . '.log';
file_put_contents($logFile, 
    date('Y-m-d H:i:s') . " - 处理了 {$processed} 条通知\n", 
    FILE_APPEND
);
echo "处理完成:{$processed} 条通知";

使用示例

// 发布新协议并通知所有用户
$controller = new AgreementController($db, $mailer);
$result = $controller->publishAndNotify(
    '服务协议更新',
    file_get_contents('agreement_content_2024.html'),
    new DateTime('2024-01-01') // 可选
);
// 用户接受协议
$userId = 123;
$agreementId = 456;
$acceptance = $controller->handleAcceptance($userId, $agreementId);

推荐增强功能

  1. 邮件模板:配置美观的邮件HTML模板
  2. 多语言支持:根据用户语言设置发送不同语言的协议通知
  3. 重试机制:对未成功发送的通知进行多次重试
  4. 用户分组:针对不同用户群体发送不同的协议版本
  5. Webhook:通知第三方系统协议更新事件
  6. 审计日志:记录所有协议相关操作的完整日志
  7. 强制过期:可设置协议接受的超时时间,未接受则限制账户

这个方案提供了完整的协议更新通知流程,包含通知的生成、发送和跟踪,以及用户接受协议的前后端交互,你可以根据实际需求进行调整和扩展。

抱歉,评论功能暂时关闭!