当歌 - RSS 订阅分发平台开发

以下将详细介绍当歌平台的技术架构、功能实现以及相关代码逻辑。

一、项目概述

当歌是一个极简的 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.phpadd_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>";
    }
}

平台地址

Dang.Ge

相关推荐
孑么7 小时前
GDPU Android移动应用 重点习题集
android·xml·java·okhttp·kotlin·android studio·webview
@OuYang9 小时前
Audio音频输出通道
android
ta叫我小白10 小时前
Kotlin 中 forEach 的 return@forEach 的使用误区
android·开发语言·kotlin
archko10 小时前
试用kotlin multiplatform
android·开发语言·kotlin
大雄野比13 小时前
UOS系统mysql服务安装
android·mysql·adb
yozyyyqls13 小时前
自定义Compose Pager实现电影卡片列表
android
张二三13 小时前
flutter 开发笔记(九):原生桥接
android·flutter·ios
梅名智14 小时前
Android studio gradle与gradle插件
android·ide·android studio
Hacker_Fuchen14 小时前
攻防世界 ics-07
android