前言
之前的友链文章一直使用的 Fciecle 这个项目,现在换成了 WordPress 程序,想着直接调用 WordPress 接口和数据库更方便管理。
修订历史
v1.1 底部翻页无法更新按钮列表
v1.2 优化UI、将定时任务分离放入 functions.php
v1.3 更换为 Ajax 请求、调用主题自带通知
v1.4 规范日志格式
注意事项
- 链接需要填写 RSS 地址那一栏,否则将不会自动匹配抓取 RSS 页面。
- 页面需要管理员手动刷新,也可定时刷新。
- 清除缓存按钮只有管理员登录状态下可用。
- 抓取 RSS 内容后,会将抓取到的内容以 JSON 文件存放在
/wp-resource/fcircle路径下。 - 抓取结束后,会将抓取成功和失败链接以 LOG 文件存放在
/wp-resource/fcircle路径下。 - 代码中可以设置每个 RSS 订阅获取的数量,和前台分页时每页文章数量。
部署办法
1. 创建主题模板文件
- 创建模板文件 fcircle.php(名称自定义) 放在
wp-content/themes/zibll/pages/路径下
<?php
/*
* Template Name: CatchWang-友链文章
* Description: 友链文章
*/
date_default_timezone_set('Asia/Shanghai');
require_once(ABSPATH . WPINC . '/class-simplepie.php');
// ========== 配置参数 ==========
$posts_per_feed = 8; // 每个订阅源获取的文章数量
$storage_path = ABSPATH . 'wp-resource/fcircle/';
$article_file = $storage_path . 'article.json';
$log_file = $storage_path . 'logs.log';
$posts_per_page = 15; // 每页显示多少篇文章
$refresh_interval = 480; // 自动刷新间隔(分钟)
// ========== 确保存储目录存在 ==========
if (!file_exists($storage_path)) {
mkdir($storage_path, 0755, true);
}
// ========== 日志函数 ==========
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);
}
// ========== 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("==== 开始任务 --- 手动刷新 ====", $log_file);
$mh = curl_multi_init();
$chs = [];
$results = [];
$error_list = [];
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;
$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;
$rss_url = $site->link_rss;
$body = curl_multi_getcontent($ch);
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
if (strlen(trim($body)) < 100) {
$error_reason = 'RSS内容过短(可能无效)';
log_message("[Error] [" . date('Y-m-d H:i:s') . "] [Manual] [{$site_name}] [无] [{$rss_url}]", $log_file);
$error_list[] = [
'name' => $site_name,
'link' => $rss_url,
'reason' => $error_reason
];
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) {
$error_reason = '所有条目标题为空';
log_message("[Error] [" . date('Y-m-d H:i:s') . "] [Manual] [{$site_name}] [无] [{$rss_url}]", $log_file);
$error_list[] = [
'name' => $site_name,
'link' => $rss_url,
'reason' => $error_reason
];
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("[Success] [" . date('Y-m-d H:i:s') . "] [Manual] [{$site_name}] [" . count($items) . "篇] [{$rss_url}]", $log_file);
} else {
$error_reason = 'RSS解析错误: ' . $feed->error();
log_message("[Error] [" . date('Y-m-d H:i:s') . "] [Manual] [{$site_name}] [无] [{$rss_url}]", $log_file);
$error_list[] = [
'name' => $site_name,
'link' => $rss_url,
'reason' => $error_reason
];
}
}
curl_multi_close($mh);
usort($results, fn($a, $b) => $b->date <=> $a->date);
log_message("抓取完成,共获取 " . count($results) . " 篇有效文章", $log_file);
if (!empty($error_list)) {
log_message("[Info] [" . date('Y-m-d H:i:s') . "] [ErrorList]", $log_file);
foreach ($error_list as $index => $err) {
$num = $index + 1;
log_message("[{$num}] [{$err['name']}] [{$err['link']}] [{$err['reason']}]", $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;
}
// ========== 处理 AJAX 刷新请求 ==========
if (isset($_GET['ajax_refresh']) && current_user_can('manage_options')) {
header('Content-Type: application/json');
$rss_sites = get_rss_sites();
$data = fetch_all_rss_items($rss_sites, $posts_per_feed);
save_articles_data($data, $article_file);
echo json_encode(['success' => true, 'message' => '缓存刷新成功']);
exit;
}
// ========== 处理传统刷新请求(保留,以防直接访问) ==========
if (current_user_can('manage_options') && isset($_GET['clear_rss_cache'])) {
if (file_exists($article_file)) {
unlink($article_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_sites = get_rss_sites();
$total_links = count($rss_sites);
$is_admin = current_user_can('manage_options');
$data = load_articles_data($article_file);
$last_updated = '从未更新';
$last_updated_time = 0;
$success_links = 0;
if ($data !== false) {
$file_time = filemtime($article_file);
$last_updated_time = $file_time ? $file_time : 0;
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);
$last_updated_time = time();
$sources = array_unique(array_column((array)$data, 'source_name'));
$success_links = count($sources);
}
$failed_links = $total_links - $success_links;
$next_refresh_time = $last_updated_time + ($refresh_interval * 60);
$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);
// ========== 输出页面 ==========
?>
<div class="baige-feed-stats">
<h2 class="baige-feed-stats-title">友链文章</h2>
<p class="baige-feed-stats-desc">友链文章定时自动更新。如若不想在此页面展示贵站,请留言取消。如若有意加入,请留言站点信息和贵站RSS,即可在此页面展示。</p>
<div class="baige-feed-stats-meta">
<span class="baige-feed-stats-tag">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
更新时间:<?php echo $last_updated_time ? date('Y-m-d H:i:s', $last_updated_time) : '从未更新'; ?>
</span>
<span class="baige-feed-stats-tag">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
自动刷新:<?php echo $refresh_interval / 60; ?> 小时
</span>
<span class="baige-feed-stats-tag baige-feed-countdown" data-next-refresh="<?php echo $next_refresh_time; ?>">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
剩余:计算中...
</span>
</div>
<div class="baige-feed-stats-grid">
<div class="baige-feed-stats-card">
<div class="baige-feed-stats-num"><?php echo $total_links; ?></div>
<div class="baige-feed-stats-label">订阅总数</div>
</div>
<div class="baige-feed-stats-card">
<div class="baige-feed-stats-num"><?php echo $success_links; ?></div>
<div class="baige-feed-stats-label">抓取成功</div>
</div>
<div class="baige-feed-stats-card">
<div class="baige-feed-stats-num"><?php echo $failed_links; ?></div>
<div class="baige-feed-stats-label">抓取失败</div>
</div>
<div class="baige-feed-stats-card">
<div class="baige-feed-stats-num"><?php echo $total_posts; ?></div>
<div class="baige-feed-stats-label">文章数量</div>
</div>
</div>
<!-- 刷新按钮(AJAX) -->
<a class="baige-feed-refresh" href="#" data-ajax-refresh="1" data-is-admin="<?php echo $is_admin ? '1' : '0'; ?>">
<svg class="baige-feed-refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.3"></path></svg>
<span class="baige-feed-refresh-text">刷新缓存</span>
</a>
</div>
<?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) {
$prev_page_url = home_url('/fcircle/page/' . ($paged - 1));
echo '<a class="prev page-numbers" href="' . esc_url($prev_page_url) . '">
<i class="fa fa-angle-left em12"></i>
<span class="hide-sm ml6">上一页</span>
</a>';
}
$home_page_url = home_url('/fcircle/page/1');
if ($paged == 1) {
echo '<span aria-current="page" class="page-numbers current">1</span>';
} else {
echo '<a class="page-numbers" href="' . esc_url($home_page_url) . '">1</a>';
}
if ($paged > 3) {
echo '<span class="page-numbers dots">…</span>';
}
for ($i = max(2, $paged - 2); $i <= min($total_pages - 1, $paged + 2); $i++) {
$page_url = home_url('/fcircle/page/' . $i);
if ($i == $paged) {
echo '<span aria-current="page" class="page-numbers current">' . $i . '</span>';
} else {
echo '<a class="page-numbers" href="' . esc_url($page_url) . '">' . $i . '</a>';
}
}
if ($paged < $total_pages - 2) {
echo '<span class="page-numbers dots">…</span>';
}
$last_page_url = home_url('/fcircle/page/' . $total_pages);
if ($paged != $total_pages) {
echo '<a class="page-numbers" href="' . esc_url($last_page_url) . '">' . $total_pages . '</a>';
} else {
echo '<span aria-current="page" class="page-numbers current">' . $total_pages . '</span>';
}
if ($paged < $total_pages) {
$next_page_url = home_url('/fcircle/page/' . ($paged + 1));
echo '<a class="next page-numbers" href="' . esc_url($next_page_url) . '">
<span class="hide-sm mr6">下一页</span>
<i class="fa fa-angle-right em12"></i>
</a>';
}
echo '</div>';
}
}
?>
<style>
/* ========== 皮友圈头部样式 ========== */
.baige-feed-stats {
--font-size-base: 14px;
--font-size-sm: 12px;
--font-size-lg: 16px;
--font-size-xl: 18px;
--font-size-2xl: 24px;
color: var(--text-color);
word-wrap: break-word;
word-break: break-all;
text-align: left;
line-height: 1.85;
font-size: var(--font-size-base);
margin: 0 auto 20px;
outline: none !important;
scrollbar-color: #75747400 #cbd5e000;
scrollbar-width: thin;
box-sizing: border-box;
font-family: 'PY Font', sans-serif;
background: var(--main-bg-color);
border-radius: 14px;
padding: 20px;
margin-top: 15px;
margin-bottom: 20px;
max-width: calc(1000px - 36px);
width: calc(100% - 36px);
position: relative;
}
.baige-feed-stats-title {
margin: 0 0 8px 0;
font-size: var(--font-size-2xl);
font-weight: 700;
color: var(--text-color);
}
.baige-feed-stats-desc {
margin: 0 0 16px 0;
font-size: var(--font-size-base);
color: var(--text-secondary);
line-height: 1.6;
}
.baige-feed-stats-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 18px;
}
.baige-feed-stats-tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
background: color-mix(in srgb, var(--theme-color) 10%, transparent);
border-radius: 20px;
font-size: var(--font-size-sm);
color: var(--theme-color);
}
.baige-feed-stats-tag svg {
width: 14px;
height: 14px;
flex-shrink: 0;
}
.baige-feed-stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 16px;
}
.baige-feed-stats-card {
background: rgba(0, 0, 0, 0.03);
border-radius: 10px;
padding: 14px;
text-align: center;
border: 1px solid #999;
}
.baige-feed-stats-num {
font-size: 28px;
font-weight: 700;
color: var(--theme-color);
line-height: 1.2;
margin-bottom: 4px;
}
.baige-feed-stats-label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.baige-feed-refresh,
.baige-feed-refresh:visited,
.baige-feed-refresh:hover,
.baige-feed-refresh:active,
.baige-feed-refresh:focus {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 20px;
background: var(--theme-color);
color: #fff !important; /* 强制所有状态保持白色 */
border-radius: 8px;
text-decoration: none;
font-size: var(--font-size-base);
font-weight: 500;
border: none;
cursor: pointer;
width: 100%;
}
.baige-feed-refresh-icon {
width: 16px;
height: 16px;
}
.baige-feed-refresh.refreshing {
pointer-events: none;
}
/* ========== 原有列表样式 ========== */
.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);
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;
}
.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;
}
.rss-date {
grid-area: date;
margin: 0;
color: var(--text-secondary);
font-size: 0.9em;
justify-self: end;
}
@media (max-width: 768px) {
.baige-feed-stats {
margin-left: 18px;
margin-right: 18px;
width: calc(100% - 36px);
padding: 18px;
max-width: none;
}
.baige-feed-stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.baige-feed-stats-num {
font-size: 24px;
}
.baige-feed-stats-meta {
gap: 8px;
}
.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;
}
}
.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;
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;
}
.baige-feed-stats-card {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.baige-feed-stats-tag {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 倒计时
var countdownEl = document.querySelector('.baige-feed-countdown');
if (countdownEl) {
var nextRefresh = parseInt(countdownEl.getAttribute('data-next-refresh'));
function updateCountdown() {
var now = Math.floor(Date.now() / 1000);
var remaining = nextRefresh - now;
if (remaining <= 0) {
countdownEl.innerHTML = countdownEl.querySelector('svg').outerHTML + ' 即将刷新...';
return;
}
var hours = Math.floor(remaining / 3600);
var minutes = Math.floor((remaining % 3600) / 60);
var seconds = remaining % 60;
var timeStr = hours + '时' +
String(minutes).padStart(2, '0') + '分' +
String(seconds).padStart(2, '0') + '秒';
countdownEl.innerHTML = countdownEl.querySelector('svg').outerHTML + ' 剩余:' + timeStr;
}
updateCountdown();
setInterval(updateCountdown, 1000);
}
// 刷新按钮(AJAX)
var refreshBtn = document.querySelector('.baige-feed-refresh');
if (refreshBtn) {
var isAdmin = refreshBtn.getAttribute('data-is-admin') === '1';
refreshBtn.addEventListener('click', function(e) {
e.preventDefault();
if (!isAdmin) {
notyf('无操作权限!', 'error');
return;
}
if (refreshBtn.classList.contains('refreshing')) return;
refreshBtn.classList.add('refreshing');
// 显示加载提示(id 用于后续更新)
notyf('正在获取最新文章...', 'load', 60000, 'refresh-noty');
var url = window.location.href.split('?')[0] + '?ajax_refresh=1';
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.timeout = 120000; // 2分钟超时
xhr.onload = function() {
if (xhr.status === 200) {
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
notyf('已获取最新文章', 'success', 2000, 'refresh-noty');
setTimeout(function() {
location.reload();
}, 2000);
} else {
notyf('文章获取失败:' + (response.message || '未知错误'), 'error', 5000, 'refresh-noty');
refreshBtn.classList.remove('refreshing');
}
} catch (e) {
notyf('解析响应失败', 'error', 5000, 'refresh-noty');
refreshBtn.classList.remove('refreshing');
}
} else {
notyf('请求失败,状态码:' + xhr.status, 'error', 5000, 'refresh-noty');
refreshBtn.classList.remove('refreshing');
}
};
xhr.onerror = function() {
notyf('网络请求失败', 'error', 5000, 'refresh-noty');
refreshBtn.classList.remove('refreshing');
};
xhr.ontimeout = function() {
notyf('请求超时,请重试', 'error', 5000, 'refresh-noty');
refreshBtn.classList.remove('refreshing');
};
xhr.send();
});
}
});
</script>
<?php get_footer(); ?>
- 将以下内容放入
functions.php文件中
// ======================== WP Cron 定时任务 ========================
// 1. 注册自定义 8 小时间隔
add_filter('cron_schedules', 'fcircle_add_cron_schedule');
function fcircle_add_cron_schedule($schedules) {
$schedules['every_8_hours'] = [
'interval' => 480 * 60,
'display' => '每8小时'
];
return $schedules;
}
// 2. 定时任务回调
add_action('fcircle_auto_refresh_cron', 'fcircle_cron_refresh_callback');
function fcircle_cron_refresh_callback() {
$article_file = ABSPATH . 'wp-resource/fcircle/article.json';
$log_file = ABSPATH . 'wp-resource/fcircle/logs.log';
$posts_per_feed = 8;
$timeout = 20;
// 确保目录存在
$storage_path = dirname($article_file);
if (!file_exists($storage_path)) {
mkdir($storage_path, 0755, true);
}
// 写开始日志(新格式)
file_put_contents($log_file, "==== 开始任务 --- 定时刷新 ====" . PHP_EOL, FILE_APPEND);
// 获取 RSS 站点列表
global $wpdb;
$rss_sites = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}links WHERE link_visible = 'Y' AND TRIM(link_rss) != %s",
''
)
);
// 可选信息日志(保留,但以 [Info] 风格)
$time = date('Y-m-d H:i:s');
file_put_contents($log_file, "[Info] [$time] [Cron] 发现 " . count($rss_sites) . " 个站点" . PHP_EOL, FILE_APPEND);
if (empty($rss_sites)) {
file_put_contents($log_file, "[Info] [$time] [Cron] 无有效站点,结束" . PHP_EOL . "====== 抓取任务结束 ======" . PHP_EOL . PHP_EOL, FILE_APPEND);
return;
}
// 加载 SimplePie
if (!class_exists('SimplePie')) {
require_once ABSPATH . WPINC . '/class-simplepie.php';
}
// curl_multi 并发抓取
$mh = curl_multi_init();
$chs = [];
$results = [];
$error_list = []; // 收集失败站点
foreach ($rss_sites as $i => $site) {
if (empty($site->link_rss)) continue;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $site->link_rss,
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 $ch_item) {
$ch = $ch_item['ch'];
$site = $ch_item['site'];
$rss_url = $site->link_rss;
$body = curl_multi_getcontent($ch);
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
$now = date('Y-m-d H:i:s');
if (strlen(trim($body)) < 100) {
$reason = 'RSS内容过短(可能无效)';
file_put_contents($log_file, "[Error] [$now] [Cron] [{$site->link_name}] [无] [{$rss_url}]" . PHP_EOL, FILE_APPEND);
$error_list[] = [
'name' => $site->link_name,
'link' => $rss_url,
'reason' => $reason
];
continue;
}
$feed = new SimplePie();
$feed->set_stupidly_fast(true);
$feed->set_raw_data(ltrim(preg_replace('/^\xEF\xBB\xBF/', '', $body)));
$feed->enable_cache(false);
$feed->init();
if ($feed->error()) {
$reason = 'RSS解析错误: ' . $feed->error();
file_put_contents($log_file, "[Error] [$now] [Cron] [{$site->link_name}] [无] [{$rss_url}]" . PHP_EOL, FILE_APPEND);
$error_list[] = [
'name' => $site->link_name,
'link' => $rss_url,
'reason' => $reason
];
continue;
}
$count = 0;
$has_valid_title = false;
foreach ($feed->get_items(0, $posts_per_feed) as $feed_item) {
$title = trim($feed_item->get_title());
if ($title === '') continue;
$has_valid_title = true;
$results[] = (object)[
'title' => strip_tags(html_entity_decode((string)$title)),
'link' => $feed_item->get_link(),
'date' => $feed_item->get_date('U'),
'source_name' => $site->link_name,
'source_link' => $site->link_url,
'source_avatar' => $site->link_image,
'item_number' => ++$count
];
}
if (!$has_valid_title) {
$reason = '所有条目标题为空';
file_put_contents($log_file, "[Error] [$now] [Cron] [{$site->link_name}] [无] [{$rss_url}]" . PHP_EOL, FILE_APPEND);
$error_list[] = [
'name' => $site->link_name,
'link' => $rss_url,
'reason' => $reason
];
continue;
}
// 成功日志(使用新格式)
file_put_contents($log_file, "[Success] [$now] [Cron] [{$site->link_name}] [{$count}篇] [{$rss_url}]" . PHP_EOL, FILE_APPEND);
}
curl_multi_close($mh);
// 按时间倒序
usort($results, fn($a, $b) => $b->date <=> $a->date);
// 保存数据
$saved = file_put_contents($article_file, json_encode($results));
if ($saved === false) {
file_put_contents($log_file, "[Error] [" . date('Y-m-d H:i:s') . "] [Cron] 写入 article.json 失败,请检查目录权限" . PHP_EOL, FILE_APPEND);
} else {
// 写入成功信息(可选,非必需格式,但保留)
file_put_contents($log_file, "[Info] [" . date('Y-m-d H:i:s') . "] [Cron] 写入成功,共 " . count($results) . " 篇文章" . PHP_EOL, FILE_APPEND);
}
// 输出错误列表总结
if (!empty($error_list)) {
file_put_contents($log_file, "[Info] [" . date('Y-m-d H:i:s') . "] [ErrorList]" . PHP_EOL, FILE_APPEND);
foreach ($error_list as $index => $err) {
$num = $index + 1;
file_put_contents($log_file, "[{$num}] [{$err['name']}] [{$err['link']}] [{$err['reason']}]" . PHP_EOL, FILE_APPEND);
}
}
// 结束日志
file_put_contents($log_file, "====== 抓取任务结束 ======" . PHP_EOL . PHP_EOL, FILE_APPEND);
}
// 3. 注册定时事件
add_action('init', 'fcircle_register_cron_event');
function fcircle_register_cron_event() {
if (!wp_next_scheduled('fcircle_auto_refresh_cron')) {
wp_schedule_event(time(), 'every_8_hours', 'fcircle_auto_refresh_cron');
}
}
// ======================== END Cron ========================
2. 修改 Nginx 伪静态文件
修改该站点伪静态规则,加入以下链接重写规则
# 为 /fcircle/page/1 格式的 URL 添加重写规则
rewrite ^/fcircle/page/([0-9]+)/?$ /index.php?pagename=fcircle&rss_page=$1 last;
- 注意:其中的 fciecle 为友链文章页面设置的 Permalink
效果展示
![图片[1] - WordPress 友链文章适配子比主题[已实装] - 云晓晨 KaiQi.Wang](https://cdn.kaiqi.wang/wp-content/uploads/2025/08/1b2702193520260716121842.webp)
也可查看本站效果 友链文章
本文参考:
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
![WordPress 友链文章适配子比主题[已实装] - 云晓晨 KaiQi.Wang](https://cdn.kaiqi.wang/wp-content/uploads/2025/08/50bcec3f6520250819102232.webp)












- 最新
- 最热
查看全部