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

相关推荐
子非衣2 小时前
MySQL修改JSON格式数据示例
android·mysql·json
openinstall全渠道统计5 小时前
免填邀请码工具:赋能六大核心场景,重构App增长新模型
android·ios·harmonyos
双鱼大猫6 小时前
一句话说透Android里面的ServiceManager的注册服务
android
双鱼大猫6 小时前
一句话说透Android里面的查找服务
android
双鱼大猫6 小时前
一句话说透Android里面的SystemServer进程的作用
android
双鱼大猫6 小时前
一句话说透Android里面的View的绘制流程和实现原理
android
双鱼大猫6 小时前
一句话说透Android里面的Window的内部机制
android
双鱼大猫7 小时前
一句话说透Android里面的为什么要设计Window?
android
双鱼大猫7 小时前
一句话说透Android里面的主线程创建时机,frameworks层面分析
android
苏金标7 小时前
android 快速定位当前页面
android