本文目录导读:

我来详细说明如何用PHP实现用户续费提醒功能,包括多种实现方案和代码示例。
核心设计方案
数据库设计
-- 用户表
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`email` varchar(100) NOT NULL,
`phone` varchar(20) DEFAULT NULL,
`status` tinyint(1) DEFAULT 1 COMMENT '1:正常 0:暂停',
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
-- 会员/订阅表
CREATE TABLE `subscriptions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`plan_name` varchar(50) NOT NULL COMMENT '套餐名称',
`start_date` date NOT NULL,
`expire_date` date NOT NULL,
`auto_renew` tinyint(1) DEFAULT 0 COMMENT '是否自动续费',
`reminded_7` tinyint(1) DEFAULT 0 COMMENT '7天前已提醒',
`reminded_3` tinyint(1) DEFAULT 0 COMMENT '3天前已提醒',
`reminded_1` tinyint(1) DEFAULT 0 COMMENT '1天前已提醒',
`status` tinyint(1) DEFAULT 1 COMMENT '1:有效 0:已过期',
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
);
-- 提醒日志表
CREATE TABLE `reminder_logs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`subscription_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`remind_type` varchar(20) COMMENT 'email/sms/wechat',
`remind_day` int(11) COMMENT '提前天数',
`sent_at` timestamp DEFAULT CURRENT_TIMESTAMP,
`status` tinyint(1) DEFAULT 1 COMMENT '1:成功 0:失败',
`error_msg` text,
PRIMARY KEY (`id`)
);
核心功能实现
提醒服务类
<?php
// ReminderService.php
class ReminderService {
private $db;
private $mailer;
private $smsService;
public function __construct($db, $mailer = null, $smsService = null) {
$this->db = $db;
$this->mailer = $mailer;
$this->smsService = $smsService;
}
/**
* 检查并发送提醒
*/
public function checkAndSendReminders() {
$today = date('Y-m-d');
$remindDates = [
7 => 'reminded_7',
3 => 'reminded_3',
1 => 'reminded_1'
];
foreach ($remindDates as $days => $field) {
$targetDate = date('Y-m-d', strtotime("+{$days} days"));
// 查询需要提醒的用户
$sql = "SELECT s.*, u.username, u.email, u.phone
FROM subscriptions s
JOIN users u ON s.user_id = u.id
WHERE s.expire_date = :target_date
AND s.status = 1
AND s.{$field} = 0";
$stmt = $this->db->prepare($sql);
$stmt->execute(['target_date' => $targetDate]);
$subscriptions = $stmt->fetchAll();
foreach ($subscriptions as $sub) {
$this->sendReminder($sub, $days);
$this->updateReminderFlag($sub['id'], $field);
}
}
}
/**
* 发送提醒
*/
private function sendReminder($subscription, $days) {
$template = $this->getReminderTemplate($subscription, $days);
$success = true;
$errorMsg = '';
// 发送邮件
if (!empty($subscription['email'])) {
try {
$this->mailer->send(
$subscription['email'],
$template['subject'],
$template['body']
);
} catch (Exception $e) {
$success = false;
$errorMsg = $e->getMessage();
}
}
// 发送短信(可选)
if (!empty($subscription['phone']) && $days <= 3) {
try {
$this->smsService->send(
$subscription['phone'],
$template['sms']
);
} catch (Exception $e) {
$success = false;
$errorMsg = $e->getMessage();
}
}
// 记录日志
$this->logReminder($subscription['id'], $subscription['user_id'], $days, $success, $errorMsg);
}
/**
* 获取提醒模板
*/
private function getReminderTemplate($subscription, $days) {
$username = $subscription['username'];
$planName = $subscription['plan_name'];
$expireDate = $subscription['expire_date'];
$templates = [
7 => [
'subject' => "尊敬的{$username},您的{$planName}即将到期",
'body' => "您好 {$username}:<br><br>
您的 {$planName} 将在 7 天后({$expireDate})到期。<br>
请及时续费以继续享受服务。<br><br>
<a href='https://yourdomain.com/renew'>立即续费</a>",
'sms' => "【您的应用名】{$username},您的{$planName}将在7天后到期,请及时续费。"
],
3 => [
'subject' => "重要提醒:{$planName} 还有3天到期",
'body' => "您好 {$username}:<br><br>
紧急提醒!您的 {$planName} 将在 3 天后({$expireDate})到期。<br>
为避免服务中断,请尽快续费。<br><br>
<a href='https://yourdomain.com/renew'>立即续费</a>",
'sms' => "【您的应用名】紧急提醒!{$username},您的{$planName}将在3天后到期,请尽快续费。"
],
1 => [
'subject' => "最后提醒:{$planName} 明天到期",
'body' => "您好 {$username}:<br><br>
您的 {$planName} 将于明天({$expireDate})到期。<br>
今天是续费的最后机会,服务将于明天停止。<br><br>
<a href='https://yourdomain.com/renew'>立即续费</a>",
'sms' => "【您的应用名】最后提醒!{$username},您的{$planName}明天到期,今天续费避免服务中断。"
]
];
return $templates[$days] ?? $templates[7];
}
/**
* 更新提醒标记
*/
private function updateReminderFlag($subscriptionId, $field) {
$sql = "UPDATE subscriptions SET {$field} = 1 WHERE id = :id";
$stmt = $this->db->prepare($sql);
$stmt->execute(['id' => $subscriptionId]);
}
/**
* 记录提醒日志
*/
private function logReminder($subscriptionId, $userId, $days, $success, $errorMsg = '') {
$sql = "INSERT INTO reminder_logs (subscription_id, user_id, remind_type, remind_day, status, error_msg)
VALUES (:sub_id, :user_id, :type, :day, :status, :error)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
'sub_id' => $subscriptionId,
'user_id' => $userId,
'type' => 'email',
'day' => $days,
'status' => $success ? 1 : 0,
'error' => $errorMsg
]);
}
}
用户端续费提醒页面
<?php
// user_reminder.php
class UserReminderController {
private $db;
public function __construct($db) {
$this->db = $db;
}
/**
* 获取用户续费信息
*/
public function getUserRenewalInfo($userId) {
$sql = "SELECT s.*, u.username, u.email
FROM subscriptions s
JOIN users u ON s.user_id = u.id
WHERE s.user_id = :user_id
AND s.status = 1
ORDER BY s.expire_date DESC
LIMIT 3";
$stmt = $this->db->prepare($sql);
$stmt->execute(['user_id' => $userId]);
return $stmt->fetchAll();
}
/**
* 渲染续费提醒页面
*/
public function renderReminderPage($userId) {
$subscriptions = $this->getUserRenewalInfo($userId);
?>
<!DOCTYPE html>
<html>
<head>
<title>续费管理</title>
<style>
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
.expiring-soon { border-left: 4px solid #ff4444; }
.expiring { border-left: 4px solid #ffaa00; }
.btn-renew { background: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
.countdown { font-size: 24px; font-weight: bold; color: #ff4444; }
</style>
</head>
<body>
<div class="container">
<h1>我的订阅</h1>
<?php foreach ($subscriptions as $sub): ?>
<?php
$daysLeft = (strtotime($sub['expire_date']) - time()) / 86400;
$cardClass = $daysLeft <= 3 ? 'expiring-soon' : ($daysLeft <= 7 ? 'expiring' : '');
?>
<div class="card <?php echo $cardClass; ?>">
<h3><?php echo htmlspecialchars($sub['plan_name']); ?></h3>
<p>到期时间:<?php echo $sub['expire_date']; ?></p>
<p>剩余天数:<span class="countdown"><?php echo ceil($daysLeft); ?></span> 天</p>
<?php if ($daysLeft <= 7): ?>
<div class="alert alert-warning">
⚠️ 您的订阅即将到期,请及时续费!
</div>
<button class="btn-renew" onclick="renewSubscription(<?php echo $sub['id']; ?>)">
立即续费
</button>
<?php else: ?>
<button class="btn-renew" onclick="renewSubscription(<?php echo $sub['id']; ?>)">
续费
</button>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<script>
function renewSubscription(subscriptionId) {
// 这里实现续费逻辑,可以是弹窗或跳转到支付页面
if (confirm('确定要续费吗?')) {
window.location.href = '/renew.php?id=' + subscriptionId;
}
}
// 实时刷新剩余天数
setInterval(function() {
location.reload();
}, 3600000); // 每小时刷新一次
</script>
</body>
</html>
<?php
}
}
定时任务实现
Cron任务配置
# 每天凌晨2点检查并发送提醒 0 2 * * * /usr/bin/php /path/to/your/project/cron/reminder.php # 或每6小时检查一次 0 */6 * * * /usr/bin/php /path/to/your/project/cron/reminder.php
定时任务脚本
<?php
// cron/reminder.php
require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../classes/ReminderService.php';
// 初始化数据库连接
$db = new PDO("mysql:host=$db_host;dbname=$db_name", $db_user, $db_pass);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 初始化邮件服务
$mailer = new Mailer($smtp_config);
// 初始化短信服务(可选)
$smsService = new SmsService($sms_config);
// 执行提醒检查
$reminder = new ReminderService($db, $mailer, $smsService);
try {
$reminder->checkAndSendReminders();
echo "[" . date('Y-m-d H:i:s') . "] Reminders sent successfully.\n";
} catch (Exception $e) {
echo "[" . date('Y-m-d H:i:s') . "] Error: " . $e->getMessage() . "\n";
error_log("Reminder cron error: " . $e->getMessage());
}
Web触发接口
<?php
// api/renewal_reminder.php
class ReminderAPI {
private $reminderService;
public function __construct($reminderService) {
$this->reminderService = $reminderService;
}
/**
* 管理员手动触发提醒
*/
public function manualTrigger() {
// 验证管理员权限
if (!$this->checkAdminAuth()) {
http_response_code(401);
return json_encode(['error' => 'Unauthorized']);
}
try {
$this->reminderService->checkAndSendReminders();
return json_encode([
'success' => true,
'message' => '提醒已发送'
]);
} catch (Exception $e) {
http_response_code(500);
return json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
}
/**
* 检查特定用户的续费状态
*/
public function checkUserStatus($userId) {
$sql = "SELECT s.*, DATEDIFF(s.expire_date, CURDATE()) as days_left
FROM subscriptions s
WHERE s.user_id = :user_id
AND s.status = 1
ORDER BY s.expire_date ASC LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute(['user_id' => $userId]);
$result = $stmt->fetch();
if ($result) {
$daysLeft = $result['days_left'];
$alertLevel = $daysLeft <= 3 ? 'critical' : ($daysLeft <= 7 ? 'warning' : 'normal');
return json_encode([
'status' => 'active',
'days_left' => $daysLeft,
'alert_level' => $alertLevel,
'expire_date' => $result['expire_date'],
'should_renew' => $daysLeft <= 7
]);
}
return json_encode(['status' => 'no_subscription']);
}
}
邮件发送类
<?php
// Mailer.php
class Mailer {
private $config;
public function __construct($config) {
$this->config = $config;
}
/**
* 发送邮件
*/
public function send($to, $subject, $body) {
// 使用PHP内置mail函数
$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-type: text/html; charset=utf-8\r\n";
$headers .= "From: " . $this->config['from'] . "\r\n";
$headers .= "Reply-To: " . $this->config['reply_to'] . "\r\n";
return mail($to, $subject, $body, $headers);
}
/**
* 使用SMTP发送(推荐)
*/
public function sendSMTP($to, $subject, $body) {
require_once 'PHPMailer/PHPMailer.php';
require_once 'PHPMailer/SMTP.php';
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = $this->config['smtp_host'];
$mail->SMTPAuth = true;
$mail->Username = $this->config['smtp_user'];
$mail->Password = $this->config['smtp_pass'];
$mail->SMTPSecure = 'tls';
$mail->Port = $this->config['smtp_port'];
$mail->setFrom($this->config['from'], $this->config['from_name']);
$mail->addAddress($to);
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $body;
return $mail->send();
} catch (Exception $e) {
throw new Exception("邮件发送失败: " . $mail->ErrorInfo);
}
}
}
配置示例
<?php
// config.php
// 数据库配置
$db_host = 'localhost';
$db_name = 'your_database';
$db_user = 'your_user';
$db_pass = 'your_password';
// 邮件配置
$smtp_config = [
'from' => 'noreply@yourdomain.com',
'from_name' => '您的应用名',
'reply_to' => 'support@yourdomain.com',
'smtp_host' => 'smtp.yourdomain.com',
'smtp_user' => 'your_smtp_user',
'smtp_pass' => 'your_smtp_pass',
'smtp_port' => 587
];
// 短信配置(可选)
$sms_config = [
'api_key' => 'your_sms_api_key',
'api_secret' => 'your_sms_api_secret',
'sender' => 'YourApp'
];
优化建议
- 批量处理:大量用户时使用批量处理,减少数据库查询
- 队列系统:使用Redis/RabbitMQ处理发送任务
- 重试机制:发送失败时自动重试3次
- 个性化设置:允许用户设置提醒时间和方式
- A/B测试:测试不同提醒文案的效果
这个方案可以覆盖大部分续费提醒需求,根据实际业务规模可以适当调整实现方式。