以下将详细介绍当歌平台的技术架构、功能实现以及相关代码逻辑。
一、项目概述
当歌是一个极简的 RSS 订阅分发平台,旨在为用户提供便捷的 RSS 管理和订阅服务,帮助用户轻松获取和分享最新资讯。
二、技术架构
后端语言: PHP
数据库 :MySQL
,通过 PDO
(PHP Data Objects)进行连接和操作,配置信息如下:
php
$host = 'localhost';
$db = 'rss';
$user = 'rss';
$pass = '123456';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
**邮件发送:**使用 PHPMailer 库来实现邮件发送功能,用于发送验证码和订阅推送邮件。
三、功能模块
(一)用户认证与登录
在 index.php
、add_rss.php
等多个页面中,通过 session_start()
启动会话,并检查 $_SESSION['username']
是否存在来判断用户是否登录。例如在 index.php
中:
php
session_start();
$isLoggedIn = isset($_SESSION['username']);
如果用户已登录,导航栏会显示用户名及退出登录选项;未登录时则显示登录和注册链接。
(二)订阅管理
添加订阅
在 add_rss.php
中,首先获取用户 ID
,若用户未登录则提示先登录。然后检查用户是否已有密钥,若无则生成一个新的密钥并存储到 user_keys
表中。
当用户提交 RSS URL 时,会检查是否已订阅该 URL,若未订阅且 URL 可访问,则将订阅信息插入到 subscriptions
表中,并触发 update_rss.php
进行 RSS 内容更新。
部分关键代码如下:
php
// 获取用户ID
$stmt = $pdo->prepare('SELECT id FROM users WHERE username =?');
$stmt->execute([$username]);
$user = $stmt->fetch();
$userId = $user['id'];
// 检查用户是否已有密钥
$stmt = $pdo->prepare('SELECT user_key FROM user_keys WHERE user_id =?');
$stmt->execute([$userId]);
$userKey = $stmt->fetchColumn();
if (!$userKey) {
// 生成新密钥并插入
$userKey = bin2hex(random_bytes(16));
$stmt = $pdo->prepare('INSERT INTO user_keys (user_id, user_key) VALUES (?,?)');
$stmt->execute([$userId, $userKey]);
}
// 处理添加订阅请求
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['rss_url'])) {
$rssUrl = $_POST['rss_url']?? '';
if ($rssUrl && $userId) {
// 检查是否已订阅
$stmt = $pdo->prepare('SELECT COUNT(*) FROM subscriptions WHERE user_id =? AND rss_url =?');
$stmt->execute([$userId, $rssUrl]);
$count = $stmt->fetchColumn();
if ($count > 0) {
echo "<div class='alert alert-warning'>您已订阅此 RSS 源</div>";
} else {
// 验证 RSS URL 是否可访问
$rss = @simplexml_load_file($rssUrl);
if ($rss) {
// 添加订阅关系
$stmt = $pdo->prepare('INSERT INTO subscriptions (user_id, rss_url) VALUES (?,?)');
$stmt->execute([$userId, $rssUrl]);
// 触发更新
$updateUrl = "https://dang.ge/update_rss.php?key={$userKey}";
$response = @file_get_contents($updateUrl);
if ($response === FALSE) {
echo "<div class='alert alert-danger'>无法调用更新服务,请检查配置。</div>";
} else {
echo "<div class='alert alert-success'>订阅添加成功,并已更新。</div>";
}
// 刷新页面
header("Location: add_rss.php");
exit;
} else {
echo "<div class='alert alert-danger'>无法访问该 RSS 源,请检查 URL 是否正确</div>";
}
}
}
}
删除订阅
当用户提交要删除的 RSS URL 时,会从 subscriptions
表和 rss_items
表中删除相关记录,并刷新页面以反映删除操作。
代码示例:
php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_rss_url'])) {
$rssUrlToDelete = $_POST['delete_rss_url']?? '';
if ($rssUrlToDelete && $userId) {
$stmt = $pdo->prepare('DELETE FROM subscriptions WHERE user_id =? AND rss_url =?');
$stmt->execute([$userId, $rssUrlToDelete]);
// 删除相关 RSS 内容
$stmt = $pdo->prepare('DELETE FROM rss_items WHERE user_id =? AND rss_url =?');
$stmt->execute([$userId, $rssUrlToDelete]);
// 刷新页面
header("Location: add_rss.php");
exit;
}
}
订阅列表展示
从 subscriptions
表中获取用户的订阅信息,并在页面上以列表形式展示,每个订阅项包含订阅源标题和删除按钮。点击订阅源标题可查看该订阅的内容。
相关代码如下:
php
// 获取订阅信息
$stmt = $pdo->prepare('SELECT rss_url FROM subscriptions WHERE user_id =?');
$stmt->execute([$userId]);
$subscriptions = $stmt->fetchAll();
// 展示订阅列表
foreach ($subscriptions as $subscription) {
$rssUrl = $subscription['rss_url'];
$rss = simplexml_load_file($rssUrl);
$channelTitle = $rss->channel->title?? $rss->title?? '未知标题';
?>
<li class="list-group-item d-flex justify-content-between align-items-center">
<form method="post" action="" class="d-inline">
<input type="hidden" name="selected_rss" value="<?php echo htmlspecialchars($rssUrl);?>">
<button type="submit" class="btn btn-link p-0"><?php echo htmlspecialchars($channelTitle);?></button>
</form>
<form method="post" action="" class="d-inline">
<input type="hidden" name="delete_rss_url" value="<?php echo htmlspecialchars($rssUrl);?>">
<button type="submit" class="btn btn-danger btn-sm">删除</button>
</form>
</li>
<?php
}
(三)RSS 内容更新与推送
更新机制
在 update_rss.php
中,根据用户密钥获取用户 ID
,然后获取用户的所有订阅 RSS URL。对于每个 URL,先加载 RSS 内容,检测其格式(Atom 或其他)并获取条目。
接着获取已存在的链接,对比新条目链接,若不存在则插入到 rss_items
表中,并构建邮件内容。
关键代码如下:
php
$key = $_GET['key']?? null;
if (!$key) {
echo "无效的请求";
exit;
}
// 验证密钥
$stmt = $pdo->prepare('SELECT user_id FROM user_keys WHERE user_key =?');
$stmt->execute([$key]);
$userKey = $stmt->fetch();
if (!$userKey) {
echo "无效的密钥";
exit;
}
$userId = $userKey['user_id'];
// 获取用户的订阅
$stmt = $pdo->prepare('SELECT DISTINCT rss_url FROM subscriptions WHERE user_id =?');
$stmt->execute([$userId]);
$rssUrls = $stmt->fetchAll();
foreach ($rssUrls as $rssUrl) {
$url = $rssUrl['rss_url'];
$rss = @simplexml_load_file($url);
if ($rss) {
// 检测格式并获取条目
$isAtom = $rss->getNamespaces(true)[''] === 'http://www.w3.org/2005/Atom';
$items = $isAtom? ($rss->entry?? []) : ($rss->channel->item?? []);
// 获取已存在链接
$stmt = $pdo->prepare('SELECT link FROM rss_items WHERE user_id =? AND rss_url =?');
$stmt->execute([$userId, $url]);
$existingLinks = $stmt->fetchAll(PDO::FETCH_COLUMN);
$newCount = 0;
$skipCount = 0;
$emailBody = "<h1>当歌 Rss 订阅推送</h1><ul>";
foreach ($items as $item) {
// 获取链接及其他字段
$link = $isAtom? (string)($item->link['href']?? $item->link?? '#') : (string)($item->link?? '#');
if (in_array($link, $existingLinks)) {
$skipCount++;
continue;
}
$title = (string)($item->title?? '无标题');
$content = $isAtom? (string)($item->content?? $item->summary?? '') : (string)($item->{'content:encoded'}?? $item->description?? '');
$pubDate = $isAtom? (string)($item->published?? $item->updated?? date('Y-m-d H:i:s')) : (string)($item->pubDate?? date('Y-m-d H:i:s'));
try {
$timestamp = strtotime($pubDate);
$formattedDate = $timestamp? date('Y-m-d H:i:s', $timestamp) : date('Y-m-d H:i:s');
// 插入新记录
$stmt = $pdo->prepare('INSERT INTO rss_items (user_id, rss_url, title, link, description, pub_date) VALUES (?,?,?,?,?,?)');
$stmt->execute([
$userId,
$url,
$title,
$link,
$content,
$formattedDate
]);
$newCount++;
$emailBody.= "<li><strong>文章标题:</strong> {$title}<br>".
"<strong>发布时间:</strong> {$formattedDate}<br>".
"<strong>文章地址:</strong> <a href='{$link}'>查看原文</a></li>";
} catch (PDOException $e) {
error_log("插入 RSS 条目失败: ". $e->getMessage());
continue;
}
}
$emailBody.= "</ul>";
// 检查是否首次执行及发送邮件
//...
}
}
邮件推送
若有新内容且不是首次执行,获取用户主邮箱和所有订阅邮箱,合并去重后,使用 PHPMailer
发送邮件通知,邮件内容包含新文章的标题、发布时间和链接。
(四)邮件订阅功能
订阅流程
在 subscribe.php
中,首先根据传入的密钥获取用户 ID
和用户名
,然后展示用户的订阅标题信息。
当用户提交邮箱时,会检查是否在冷却时间内(60 秒),若不在则发送验证码到邮箱,并记录相关信息到会话中。
当用户提交验证码时,会验证验证码是否正确,若正确则将订阅信息插入到 email_subscriptions
表中。
关键代码如下:
php
$key = $_GET['key']?? null;
if (!$key) {
echo "<div class='alert alert-danger'>无效的请求</div>";
exit;
}
// 验证密钥
$stmt = $pdo->prepare('SELECT user_id FROM user_keys WHERE user_key =?');
$stmt->execute([$key]);
$userKey = $stmt->fetch();
if (!$userKey) {
echo "<div class='alert alert-danger'>无效的密钥</div>";
exit;
}
$userId = $userKey['user_id'];
// 获取用户名
$stmt = $pdo->prepare('SELECT username FROM users WHERE id =?');
$stmt->execute([$userId]);
$user = $stmt->fetch();
$keyUsername = $user['username'];
// 获取订阅标题
$stmt = $pdo->prepare('SELECT title, pub_date FROM rss_items WHERE user_id =? ORDER BY pub_date DESC LIMIT 5');
$stmt->execute([$userId]);
$subscriptions = $stmt->fetchAll();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['email'])) {
$currentTime = time();
if ($currentTime - $lastRequestTime < $cooldown) {
$message = "<div class='alert alert-warning'>请稍后 60 秒后再试。</div>";
} else {
$email = $_POST['email'];
$verificationCode = rand(100000, 999999);
// 发送验证码邮件
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host ='smtp.163.com';
$mail->SMTPAuth = true;
$mail->Username = 'xxxxx@163.com';
$mail->Password = 'xxxxx';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 25;
$mail->CharSet = 'UTF-8';
$mail->setFrom('xxxxx@163.com', 'RSS Notifier');
$mail->addAddress($email);
$mail->isHTML(true);
$mail->Subject = '当歌 Rss 订阅平台订阅验证码';
$mail->Body = "您的验证码是:<strong>{$verificationCode}</strong>";
$mail->send();
$message = "<div class='alert alert-success'>验证码已发送到您的邮箱,请查收。</div>";
$showVerification = true;
$_SESSION['last_request_time'] = $currentTime;
} catch (Exception $e) {
$message = "<div class='alert alert-danger'>邮件发送失败: {$mail->ErrorInfo}</div>";
}
$_SESSION['verification_code'] = $verificationCode;
$_SESSION['email'] = $email;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['verification_code'])) {
$inputCode = $_POST['verification_code'];
if ($inputCode == $_SESSION['verification_code']) {
// 验证成功,订阅
$stmt = $pdo->prepare('INSERT INTO email_subscriptions (user_id, email, key_username, subscribed_at) VALUES (?,?,?, NOW())');
$stmt->execute([$userId, $_SESSION['email'], $keyUsername]);
$message = "<div class='alert alert-success'>订阅成功!</div>";
} else {
$message = "<div class='alert alert-danger'>验证码错误,请重试。</div>";
}
}