本文目录导读:

我来为你提供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);
推荐增强功能
- 邮件模板:配置美观的邮件HTML模板
- 多语言支持:根据用户语言设置发送不同语言的协议通知
- 重试机制:对未成功发送的通知进行多次重试
- 用户分组:针对不同用户群体发送不同的协议版本
- Webhook:通知第三方系统协议更新事件
- 审计日志:记录所有协议相关操作的完整日志
- 强制过期:可设置协议接受的超时时间,未接受则限制账户
这个方案提供了完整的协议更新通知流程,包含通知的生成、发送和跟踪,以及用户接受协议的前后端交互,你可以根据实际需求进行调整和扩展。