前言
之前的友链文章一直使用的 Fciecle 这个项目,现在换成了 WordPress 程序,想着直接调用 WordPress 接口和数据库更方便管理。
注意事项
- 链接需要填写 RSS 地址那一栏,否则将不会自动匹配抓取 RSS 页面。
- 页面需要管理员手动刷新,没有设置定时任务。
- 清除缓存按钮只有管理员登录状态下可见。
- 抓取 RSS 内容后,会将抓取到的内容以 JSON 文件存放在
/wp-resource/fcircle
路径下。 - 抓取结束后,会将抓取成功和失败链接以 LOG 文件存放在
/
路径下。wp-resource
/fcircle - 代码中可以设置每个 RSS 订阅获取的数量,和前台分页时每页文章数量。
部署办法
1. 创建主题模板文件
创建模板文件 fcircle.php(名称自定义) 放在 wp-content/themes/zibll/pages/
路径下
<?php
/*
Template Name: RSS 朋友圈
*/
date_default_timezone_set('Asia/Shanghai');
require_once(ABSPATH . WPINC . '/class-simplepie.php');
// 配置参数
$posts_per_feed = 6; // 每个订阅源获取的文章数量
$storage_path = ABSPATH . 'wp-resource/fcircle/';
$article_file = $storage_path . 'article.json';
$log_file = $storage_path . 'logs.log';
$posts_per_page = 10; // 每页显示多少篇文章(分页用)
// 确保存储目录存在
if (!file_exists($storage_path)) {
mkdir($storage_path, 0755, true);
}
// 处理缓存清除请求(仅管理员)
if (current_user_can('manage_options') && isset($_GET['clear_rss_cache'])) {
if (file_exists($article_file)) {
unlink($article_file);
}
log_message("====== 缓存手动清除请求 ======", $log_file);
$data = fetch_all_rss_items(get_rss_sites(), $posts_per_feed);
save_articles_data($data, $article_file);
wp_redirect(remove_query_arg('clear_rss_cache'));
exit;
}
get_header();
// 获取RSS站点数据用于统计
$rss_sites = get_rss_sites();
$total_links = count($rss_sites); // 总链接数量
// 加载数据
$data = load_articles_data($article_file);
$last_updated = '从未更新';
$success_links = 0; // 成功获取的站点数量
if ($data !== false) {
// 获取最后更新时间
$file_time = filemtime($article_file);
if ($file_time) {
$time_diff = time() - $file_time;
if ($time_diff < 3600) {
$last_updated = floor($time_diff / 60) . ' 分钟前';
} elseif ($time_diff < 86400) {
$last_updated = floor($time_diff / 3600) . ' 小时前';
} else {
$last_updated = floor($time_diff / 86400) . ' 天前';
}
}
// 计算成功获取的站点数量(去重)
$sources = array_unique(array_column((array)$data, 'source_name'));
$success_links = count($sources);
} else {
$data = fetch_all_rss_items($rss_sites, $posts_per_feed);
save_articles_data($data, $article_file);
// 计算成功获取的站点数量(去重)
$sources = array_unique(array_column((array)$data, 'source_name'));
$success_links = count($sources);
}
// 分页处理
$total_posts = count($data);
$paged = isset($_GET['rss_page']) ? max(1, intval($_GET['rss_page'])) : 1;
$total_pages = max(1, ceil($total_posts / $posts_per_page));
$offset = ($paged - 1) * $posts_per_page;
$current_posts = array_slice($data, $offset, $posts_per_page);
// 输出标题和控制区
echo '<div class="zib-title rss-header">';
echo '<div class="header-title">友链文章</div>';
echo '<div class="stats-container">';
echo '<div class="stat-item"><span class="stat-label">总共友链数量 </span> <span class="stat-label-num"> ' . $total_links . ' </span><span class="stat-label"> 条</span></div>';
echo '<div class="stat-item"><span class="stat-label">成功获取数量 </span> <span class="stat-label-num"> ' . $success_links . ' </span><span class="stat-label"> 条</span></div>';
echo '<div class="stat-item"><span class="stat-label">获取文章数量 </span> <span class="stat-label-num"> ' . $total_posts . ' </span><span class="stat-label"> 篇</span></div>';
echo '</div>';
echo '<div class="header-actions">';
echo '<p class="update-info">最后更新: ' . $last_updated . '</p>';
if (current_user_can('manage_options')) {
echo '<a href="' . add_query_arg('clear_rss_cache', '1') . '" class="clear-cache-btn"><i class="fa fa-trash"></i> 清除缓存</a>';
}
echo '</div>';
echo '</div>';
// ======================== RSS 数据函数 ========================
function get_rss_sites() {
global $wpdb;
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}links WHERE link_visible = 'Y' AND TRIM(link_rss) != %s",
''
)
);
}
function fetch_all_rss_items($rss_sites, $posts_per_feed = 5, $timeout = 20) {
global $log_file;
log_message("====== 开始新的RSS抓取任务: " . date('Y-m-d H:i:s') . " ======", $log_file);
log_message("发现 " . count($rss_sites) . " 个有效RSS站点,开始处理...", $log_file);
$mh = curl_multi_init();
$chs = [];
$results = [];
foreach ($rss_sites as $i => $site) {
$site_name = $site->link_name;
if (!isset($site->link_rss) || trim($site->link_rss) === '') {
continue;
}
$rss_url = $site->link_rss;
log_message("正在处理站点: {$site_name},RSS地址: {$rss_url}", $log_file);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $rss_url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_USERAGENT => 'Mozilla/5.0',
]);
curl_multi_add_handle($mh, $ch);
$chs[$i] = [
'ch' => $ch,
'site' => $site
];
}
$running = null;
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
foreach ($chs as $i => $item) {
$ch = $item['ch'];
$site = $item['site'];
$site_name = $site->link_name;
$body = curl_multi_getcontent($ch);
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
if (strlen(trim($body)) < 100) {
$msg = "RSS内容过短(可能无效),跳过: {$site_name}";
log_message("警告: {$msg}", $log_file);
continue;
}
$feed = new SimplePie();
$feed->set_stupidly_fast(true);
$feed->set_raw_data(ltrim(preg_replace('/^\xEF\xBB\xBF/', '', $body)));
$feed->set_useragent('Mozilla/5.0');
$feed->enable_cache(false);
$feed->init();
if (!$feed->error()) {
$items = $feed->get_items(0, $posts_per_feed);
$has_valid_title = false;
foreach ($items as $item) {
if (trim($item->get_title()) !== '') {
$has_valid_title = true;
break;
}
}
if (!$has_valid_title) {
$msg = "所有条目标题为空,跳过: {$site_name}";
echo "<!-- {$msg} -->";
log_message("警告: {$msg}", $log_file);
continue;
}
$item_count = 1;
foreach ($items as $item) {
$results[] = (object)[
'title' => strip_tags(html_entity_decode((string)$item->get_title())),
'link' => $item->get_link(),
'date' => $item->get_date('U'),
'source_name' => $site->link_name,
'source_link' => $site->link_url,
'source_avatar' => $site->link_image,
'item_number' => $item_count++
];
}
log_message("成功抓取 {$site_name} 的 " . count($items) . " 篇文章", $log_file);
} else {
$msg = "RSS解析错误: {$site_name}, 错误: {$feed->error()}";
log_message("错误: {$msg}", $log_file);
}
}
curl_multi_close($mh);
usort($results, fn($a, $b) => $b->date <=> $a->date);
log_message("抓取完成,共获取 " . count($results) . " 篇有效文章", $log_file);
log_message("====== 抓取任务结束 ======" . PHP_EOL, $log_file);
return $results;
}
function save_articles_data($data, $file_path) {
global $log_file;
$json_data = json_encode($data);
if (file_put_contents($file_path, $json_data) !== false) {
log_message("成功保存 " . count($data) . " 条文章数据到 " . $file_path, $log_file);
return true;
} else {
log_message("保存文章数据失败: " . $file_path, $log_file);
return false;
}
}
function load_articles_data($file_path) {
if (file_exists($file_path) && filesize($file_path) > 0) {
$json_data = file_get_contents($file_path);
$data = json_decode($json_data);
if (json_last_error() === JSON_ERROR_NONE) {
return $data;
}
}
return false;
}
function log_message($message, $log_file) {
$time = date('Y-m-d H:i:s');
$log_entry = "[$time] $message" . PHP_EOL;
file_put_contents($log_file, $log_entry, FILE_APPEND);
}
// ======================== END FUNC ========================
?>
<style>
/* 子比主题标题样式 - 确保与列表同宽 */
.zib-title.rss-header {
position: relative;
margin: 0 auto 20px;
padding: 25px;
background-color: var(--main-bg-color);
border-radius: 8px;
border-left: 4px solid var(--theme-color);
border-right: 4px solid var(--theme-color);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.07);
max-width: 964px;
width: calc(100% - 36px);
box-sizing: border-box;
margin-left: auto;
margin-right: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
/* 标题样式 - 水平居中,垂直方向在上方1/4,按比例放大 */
.header-title {
font-size: 2.2em;
color: var(--text-color);
font-weight: 700;
text-align: center;
margin: 0;
line-height: 1.3;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
}
/* 统计数据容器 - 中间1/2内容,增强模块感 */
.stats-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background-color: rgba(0, 0, 0, 0.02);
border-radius: 6px;
gap: 15px;
flex-wrap: wrap;
}
/* 中间统计项样式 */
.stat-item {
flex: 1;
text-align: center;
font-size: 1.2em;
color: var(--text-color);
line-height: 1.6;
min-width: 150px;
}
/* 统计项标签加粗 */
.stat-label {
font-weight: 700;
color: var(--theme-color);
}
.stat-label-num {
font-weight: 700;
}
/* 底部操作区 */
.header-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0 5px;
margin-top: 5px;
flex-wrap: wrap;
gap: 10px;
}
.update-info {
margin: 0;
color: var(--main-color);
font-size: 0.95em;
}
/* 清除缓存按钮 - 确保不透明 */
.clear-cache-btn {
color: var(--main-color);
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 0.95em;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 6px;
opacity: 1;
}
.rss-list {
list-style: none;
padding: 0;
margin: 0 auto;
max-width: 1000px;
}
/* 卡片式列表样式 - 确保与标题宽度一致 */
.rss-item {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
grid-template-areas: "title number" "source date";
padding: 20px;
margin-bottom: 15px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
background-color: var(--main-bg-color);
border: 1px solid var(--border-color);
gap: 12px;
align-items: center;
max-width: 1000px;
width: calc(100% - 36px);
box-sizing: border-box;
margin-left: auto;
margin-right: auto;
}
.rss-title {
grid-area: title;
margin: 0;
font-size: 1.1em;
line-height: 1.4;
}
.rss-title a {
color: var(--link-color);
text-decoration: none;
transition: color 0.3s;
word-break: break-word;
}
.rss-title a:hover {
color: var(--theme-color);
text-decoration: none;
}
.rss-number {
grid-area: number;
font-size: 1.8em;
font-weight: bold;
color: var(--theme-color);
margin: 0;
opacity: 0.8;
justify-self: end;
}
.rss-source {
grid-area: source;
display: flex;
align-items: center;
gap: 10px;
color: var(--text-secondary);
font-size: 0.9em;
}
.rss-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
object-fit: cover;
border: 1px solid var(--border-color);
}
.rss-avatar-placeholder {
width: 28px;
height: 28px;
border-radius: 50%;
background-color: var(--light-bg-color);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
font-size: 14px;
border: 1px solid var(--border-color);
}
.rss-date {
grid-area: date;
margin: 0;
color: var(--text-secondary);
font-size: 0.9em;
justify-self: end;
}
/* 深色模式适配 */
.dark-theme .rss-item,
[data-theme="dark"] .rss-item,
:root.dark .rss-item {
background-color: var(--main-bg-color);
border-color: var(--dark-border-color);
}
.dark-theme .rss-title a,
[data-theme="dark"] .rss-title a,
:root.dark .rss-title a {
color: var(--dark-link-color);
}
.dark-theme .rss-number,
[data-theme="dark"] .rss-number,
:root.dark .rss-number {
color: var(--dark-theme-color);
}
.dark-theme .stats-container,
[data-theme="dark"] .stats-container,
:root.dark .stats-container {
background-color: rgba(255, 255, 255, 0.02);
}
/* 移动端适配 - 优化布局 */
@media (max-width: 768px) {
.zib-title.rss-header {
margin-left: 18px;
margin-right: 18px;
width: calc(100% - 36px);
padding: 18px;
gap: 15px;
}
.header-title {
font-size: 1.8em;
padding-bottom: 8px;
}
.stats-container {
flex-direction: column;
gap: 12px;
text-align: left;
padding: 12px;
}
.stat-item {
width: 100%;
text-align: center;
font-size: 1.1em;
min-width: auto;
}
.header-actions {
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 5px 0 0;
}
.update-info {
font-size: 0.9em;
}
.clear-cache-btn {
padding: 6px 12px;
font-size: 0.9em;
align-self: auto;
margin-top: 0;
}
.rss-item {
margin-left: 18px;
margin-right: 18px;
width: calc(100% - 36px);
grid-template-columns: 1fr auto;
grid-template-rows: auto auto auto;
grid-template-areas: "title number" "date date" "source source";
padding: 15px;
}
.rss-date {
justify-self: start;
padding-top: 5px;
}
}
/* 电脑端样式 */
@media (min-width: 769px) {
/* 保持电脑端放大效果 */
}
/* 分页样式 */
.rss-pagination {
display: flex;
justify-content: center;
margin: 20px auto;
gap: 8px;
flex-wrap: wrap;
max-width: 1000px;
width: calc(100% - 36px);
}
.rss-pagination a,
.rss-pagination span {
padding: 6px 12px;
border-radius: 4px;
border: 1px solid var(--border-color);
background: var(--main-bg-color);
color: var(--text-color);
text-decoration: none;
font-size: 0.9em;
}
.rss-pagination a:hover {
background: var(--theme-color);
color: #fff;
}
.rss-pagination .current {
background: var(--theme-color);
color: #fff;
font-weight: bold;
}
</style>
<?php
if (empty($current_posts)) {
echo '<p style="padding:20px; text-align:center; color:var(--text-secondary); margin:0 auto; max-width:1000px;">暂无RSS数据,请稍后刷新或稍候再试。</p>';
} else {
echo '<ul class="rss-list" id="rss-feed-list">';
$global_counter = $offset + 1;
foreach ($current_posts as $item) {
echo '<li class="rss-item">';
echo '<h3 class="rss-title"><a href="' . esc_url($item->link) . '" target="_blank">' . esc_html($item->title) . '</a></h3>';
echo '<p class="rss-number">' . esc_html($global_counter) . '</p>';
echo '<div class="rss-source">';
if (!empty($item->source_avatar)) {
echo '<img src="' . esc_url($item->source_avatar) . '" alt="' . esc_attr($item->source_name) . '" class="rss-avatar">';
} else {
echo '<div class="rss-avatar-placeholder">' . esc_html(substr($item->source_name, 0, 1)) . '</div>';
}
echo '<a href="' . esc_url($item->source_link) . '" target="_blank">' . esc_html($item->source_name) . '</a>';
echo '</div>';
echo '<p class="rss-date">' . date('Y-m-d H:i', $item->date) . '</p>';
echo '</li>';
$global_counter++;
}
echo '</ul>';
// ======= 分页 =======
if ($total_pages > 1) {
echo '<div class="pagenav ajax-pag">';
// 上一页
if ($paged > 1) {
echo '<a class="prev page-numbers" href="' . esc_url(home_url('/fcircle/page/' . ($paged - 1))) . '">
<i class="fa fa-angle-left em12"></i>
<span class="hide-sm ml6">上一页</span>
</a>';
}
// 首页、2、3
for ($i = 1; $i <= min(3, $total_pages); $i++) {
if ($i == $paged) {
echo '<span aria-current="page" class="page-numbers current">' . $i . '</span>';
} else {
echo '<a class="page-numbers" href="' . esc_url(home_url('/fcircle/page/' . $i)) . '">' . $i . '</a>';
}
}
// 省略号和末页
if ($total_pages > 4) {
echo '<span class="page-numbers dots">…</span>';
// 末页
if ($paged == $total_pages) {
echo '<span aria-current="page" class="page-numbers current">' . $total_pages . '</span>';
} else {
echo '<a class="page-numbers" href="' . esc_url(home_url('/fcircle/page/' . $total_pages)) . '">' . $total_pages . '</a>';
}
}
// 下一页
if ($paged < $total_pages) {
echo '<a class="next page-numbers" href="' . esc_url(home_url('/fcircle/page/' . ($paged + 1))) . '">
<span class="hide-sm mr6">下一页</span>
<i class="fa fa-angle-right em12"></i>
</a>';
}
echo '</div>';
}
}
?>
<?php get_footer(); ?>
2. 修改 Nginx 伪静态文件
修改该站点伪静态规则,加入以下链接重写规则
# 为 /fcircle/page/1 格式的 URL 添加重写规则
rewrite ^/fcircle/page/([0-9]+)/?$ /index.php?pagename=fcircle&rss_page=$1 last;
- 注意:其中的 fciecle 为友链文章页面设置的 Permalink
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
- 最新
- 最热
只看作者