当歌 - 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

相关推荐
参宿四南河三1 小时前
Android Compose SideEffect(副作用)实例加倍详解
android·app
火柴就是我2 小时前
mmkv的 mmap 的理解
android
没有了遇见2 小时前
Android之直播宽高比和相机宽高比不支持后动态获取所支持的宽高比
android
shenshizhong2 小时前
揭开 kotlin 中协程的神秘面纱
android·kotlin
vivo高启强3 小时前
如何简单 hack agp 执行过程中的某个类
android
沐怡旸3 小时前
【底层机制】 Android ION内存分配器深度解析
android·面试
你听得到113 小时前
肝了半个月,我用 Flutter 写了个功能强大的图片编辑器,告别image_cropper
android·前端·flutter
KevinWang_3 小时前
Android 原生 app 和 WebView 如何交互?
android
用户69371750013844 小时前
Android Studio中Gradle、AGP、Java 版本关系:不再被构建折磨!
android·android studio
杨筱毅4 小时前
【底层机制】Android低内存管理机制深度解析
android·底层机制