效果:网页版的朋友圈
1、默认缓存1小时
2、管理员状态可点击 “刷新缓存” 按钮更新。
3、确保usr/cache/目录权限为755,文件权限为644,否则缓存失效,页面加载慢。
4、提取了每个订阅的最新2篇文章
5、增加了重试机制配置。
最大重试次数($maxRetries = 3)
设置了重试延迟时间($retryDelay = 1 秒)
最终失败时会在错误信息中显示尝试次数

  <?php
    /**
    * 朋友圈
    *
    * @package custom
    */
    
    if (!defined('__TYPECHO_ROOT_DIR__')) exit;?>
    <?php $this->need('component/header.php'); ?>
    
    <!-- 引入pyq.css样式文件 -->
    <link rel="stylesheet" href="<?php $this->options->themeUrl('assets/css/pyq.css'); ?>">
    
        <!-- aside -->
        <?php $this->need('component/aside.php'); ?>
        <!-- / aside -->
    
       <a class="off-screen-toggle hide"></a>
       <main class="app-content-body <?php echo Content::returnPageAnimateClass($this); ?>">
        <div class="hbox hbox-auto-xs hbox-auto-sm">
        <!--文章-->
         <div class="col center-part gpu-speed" id="post-panel">
            <?php  echo Content::exportPostPageHeader($this,$this->user->uid); ?>
          <div class="wrapper-md">
           <?php echo Content::BreadcrumbNavigation($this, $this->options->rootUrl); ?>
           <div id="postpage" class="blog-post">
            <article class="single-post panel">
                <?php echo Content::exportHeaderImg($this); ?>
             <div id="post-content" class="wrapper-lg">
            <div class="container">
        <h1><?php $this->title() ?></h1>
        
        <?php
        // 缓存设置
        $cacheDir = __TYPECHO_ROOT_DIR__ . '/usr/cache/'; // Typecho默认缓存目录
        $cacheFile = $cacheDir . 'friends_circle_cache.html';
        $cacheTime = 3600; // 缓存时间:1小时(3600秒)
        
        // 确保缓存目录存在
        if (!is_dir($cacheDir)) {
            mkdir($cacheDir, 0755, true);
        }
        
        // 尝试读取缓存
        $cachedContent = false;
        if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $cacheTime)) {
            $cachedContent = file_get_contents($cacheFile);
        }
        
        // 管理员判断(Typecho 权限控制)
        if ($this->user->hasLogin()) {
            if (isset($_GET['refresh_pyq_cache']) && $_GET['refresh_pyq_cache'] == '1') {
                // 删除缓存文件
                if (file_exists($cacheFile)) unlink($cacheFile);
                // 刷新页面
                $this->response->redirect($this->permalink);
            }
            // 输出刷新按钮
            echo '<a href="' . $this->permalink . '?refresh_pyq_cache=1" class="refresh-cache-btn">刷新内容</a>';
        }
        
        
        if ($cachedContent === false) {
            // 缓存不存在或已过期,重新获取内容
            $feedUrls = [
                  '李的博客' => 'https://lilog.cn/feed',
                '刘郎阁' => 'https://vjo.cc/feed'
               
               
            ];
            
            $allArticles = [];
            $errorMessages = [];
            
            // 创建超时上下文(5秒超时)
            $context = stream_context_create([
                'http' => [
                    'timeout' => 5, // 超时时间设置为5秒
                    'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' // 添加用户代理,避免部分服务器拒绝访问
                ],
                'https' => [
                    'timeout' => 5 // HTTPS链接同样应用5秒超时
                ]
            ]);
            
            foreach ($feedUrls as $blogName => $feedUrl) {
                // 解析RSS(添加超时控制)
                $feedContent = @file_get_contents($feedUrl, false, $context);
              // 修改为带重试机制的代码
$maxRetries = 3; // 最大重试次数
$retryDelay = 1000000; // 重试延迟(微秒),1秒=1000000微秒
$feedContent = false;

// 重试循环
for ($retry = 0; $retry < $maxRetries; $retry++) {
    $feedContent = @file_get_contents($feedUrl, false, $context);
    if ($feedContent !== false) {
        break; // 获取成功,跳出重试循环
    }
    // 重试前等待一段时间
    if ($retry < $maxRetries - 1) {
        usleep($retryDelay);
    }
}

// 所有重试都失败
if (!$feedContent) {
    $errorMessages[] = "无法获取 {$blogName} 的内容(经过 {$maxRetries} 次重试仍失败)";
    continue;
}
                
                // 加载XML
                $xml = simplexml_load_string($feedContent);
                if (!$xml) {
                    $errorMessages[] = "{$blogName} 的RSS格式错误";
                    continue;
                }
                
                // 解析RSS(兼容RSS 2.0和Atom)
                $items = [];
                if (isset($xml->channel->item)) {
                    // RSS 2.0
                    $items = $xml->channel->item;
                } elseif (isset($xml->entry)) {
                    // Atom
                    $items = $xml->entry;
                }
                
                if (empty($items)) {
                    $errorMessages[] = "{$blogName} 暂无内容";
                    continue;
                }
                
                // 提取最新2篇文章
                $count = 0;
                foreach ($items as $item) {
                    if ($count >= 2) break;
                    
                    // 获取标题和链接
                    $title = (string)($item->title ?? '无标题');
                    $link = (string)($item->link ?? ($item->guid ?? '#'));
                    
                    // 处理Atom格式的链接
                    if (isset($item->link['href'])) {
                        $link = (string)$item->link['href'];
                    }
                    
                    // 获取发布时间
                    $pubDate = (string)($item->pubDate ?? ($item->published ?? ''));
                    $pubTime = strtotime($pubDate);
                    $displayDate = $pubDate ? date('Y-m-d', $pubTime) : '';
                    
                    // 将文章添加到总数组
                    $allArticles[] = [
                        'title' => $title,
                        'link' => $link,
                        'date' => $displayDate,
                        'timestamp' => $pubTime,
                        'source' => $blogName
                    ];
                    
                    $count++;
                }
            }
            
            // 按发布时间排序(最新的在前)
            usort($allArticles, function($a, $b) {
                return $b['timestamp'] - $a['timestamp'];
            });
            
            // 生成输出内容
            $output = '';
            
            // 输出错误信息
            if (!empty($errorMessages)) {
                foreach ($errorMessages as $msg) {
                    $output .= "<div class='error-message'>{$msg}</div>";
                }
            }
            
            // 输出文章卡片
            if (!empty($allArticles)) {
                $output .= '<div class="articles-container">';
                
                foreach ($allArticles as $article) {
                    $output .= '<div class="article-card">';
                    $output .= '<div class="card-title"><a href="' . $article['link'] . '" target="_blank" rel="noopener">' . $article['title'] . '</a></div>';
                    $output .= '<div class="card-meta">';
                    if ($article['date']) {
                        $output .= '<span class="card-date">' . $article['date'] . '</span>';
                    }
                    $output .= '<span class="card-source">来源: ' . $article['source'] . '</span>';
                    $output .= '</div></div>';
                }
                
                $output .= '</div>';
            } else {
                $output .= '<div class="error-message">没有获取到任何文章内容</div>';
            }
            
            // 保存到缓存文件
            file_put_contents($cacheFile, $output);
            $cachedContent = $output;
        }
        
        // 输出最终内容
        echo $cachedContent;
        ?>
    </div>
                 <?php Content::pageFooter($this->options,$this) ?>
             </div>
            </article>
           </div>
            <?php $this->need('component/comments.php') ?>
          </div>
             <?php echo WidgetContent::returnRightTriggerHtml() ?>
         </div>
         <!--文章右侧边栏开始-->
        <?php $this->need('component/sidebar.php'); ?>
         <!--文章右侧边栏结束-->
        </div>
       </main>
    <?php echo Content::returnReadModeContent($this,$this->user->uid,$content); ?>
    
        <!-- footer -->
        <?php $this->need('component/footer.php'); ?>
          <!-- / footer -->

/* 文章卡片样式 */
    .article-card {
        background: #fff;
        border-radius: 8px;
        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
        overflow: hidden;
        transition: transform 0.3s ease, box-shadow 0.3s ease;
        border: 1px solid #f0f0f0;
        padding: 12px;
        box-sizing: border-box;
    }
    
    .article-card:hover {
        transform: translateY(-3px);
        box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
    }
    
    /* 文章标题 - 移除h3标签,使用div+class,字号16px */
    .card-title {
        margin: 0 0 10px 0;
        font-size: 16px; /* 标题字号16px */
        line-height: 1.4;
        font-weight: normal;
    }
    
    .card-title a {
        color: #2d3748;
        text-decoration: none;
        transition: color 0.2s;
    }
    
    .card-title a:hover {
        color: #3182ce;
        text-decoration: none;
    }
    
    /* 元数据区域(日期和来源)- 同一行显示 */
    .card-meta {
        display: flex;
        justify-content: space-between;
        align-items: center;
        font-size: 12px;
        gap: 8px; /* 两者之间保留间距 */
        flex-wrap: wrap; /* 防止小屏幕内容重叠 */
    }
    
    /* 发布日期和来源共享样式 */
    .card-date, .card-source {
        color: #718096;
        background: #f7fafc; /* 来源添加与日期相同的背景 */
        padding: 2px 6px;
        border-radius: 3px;
        white-space: nowrap; /* 防止内容换行 */
    }
    
    /* 错误提示样式 */
    .error-message {
        background: #fff5f5;
        border-left: 4px solid #e53e3e;
        padding: 15px;
        margin: 10px 0;
        color: #c53030;
        border-radius: 4px;
        font-size: 14px;
        box-sizing: border-box;
    }