WordPress 友链文章适配子比主题[已实装]

前言

之前的友链文章一直使用的 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

也可查看本站效果 友链文章

本文参考:

  1. 子比主题新增一个友圈动态页面
  2. 皮友圈 – 皮皮社
© 版权声明
THE END
喜欢就支持一下吧
点赞1 分享
评论 共8条
头像 - 云晓晨 KaiQi.Wang
欢迎您留下宝贵的见解!
提交
头像 - 云晓晨 KaiQi.Wang

昵称

取消
昵称表情代码图片快捷回复