步骤1:创建插件文件
- 登录网站的文件管理器或使用FTP
- 进入
/wp-content/plugins/目录 - 创建新文件夹
friendlinks-manager - 在文件夹中创建文件
friendlinks-manager.php,并粘贴下面的完整代码
代码1
<?php
/**
* Plugin Name: 友链管理与RSS订阅
* Plugin URI: https://yourwebsite.com/
* Description: 为WordPress添加友链管理功能并自动抓取友链最新文章
* Version: 2.1.0
* Author: Your Name
* License: GPL v2 or later
* Text Domain: friendlinks
*/
// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* 1. 启用WordPress原生链接管理功能
*/
add_filter( 'pre_option_link_manager_enabled', '__return_true' );
/**
* 2. 创建links_meta表
*/
register_activation_hook( __FILE__, 'friendlinks_create_tables' );
function friendlinks_create_tables() {
global $wpdb;
$table_name = $wpdb->prefix . 'links_meta';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
link_id bigint(20) unsigned NOT NULL DEFAULT '0',
meta_key varchar(255) DEFAULT NULL,
meta_value longtext,
PRIMARY KEY (meta_id),
KEY link_id (link_id),
KEY meta_key (meta_key)
) $charset_collate;";
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
dbDelta( $sql );
// 创建抓取记录选项
add_option( 'friendlinks_last_fetch', 0 );
add_option( 'friendlinks_fetch_count', 0 );
add_option( 'friendlinks_last_error', '' );
}
/**
* 3. 为链接添加RSS字段
*/
// 在链接编辑页面添加RSS字段
add_action( 'add_link_fields', 'friendlinks_add_rss_field' );
add_action( 'edit_link_fields', 'friendlinks_add_rss_field' );
function friendlinks_add_rss_field( $link ) {
$rss_url = isset( $link ) ? friendlinks_get_rss_url( $link->link_id ) : '';
?>
<div class="form-field">
<label for="link_rss"><?php _e( 'RSS地址', 'friendlinks' ); ?></label>
<input name="link_rss" id="link_rss" type="url" value="<?php echo esc_url( $rss_url ); ?>" style="width: 95%;" />
<p class="description"><?php _e( '输入此友链的RSS订阅地址(如:https://example.com/feed/)', 'friendlinks' ); ?></p>
</div>
<?php
}
// 保存RSS字段
add_action( 'add_link', 'friendlinks_save_rss_field' );
add_action( 'edit_link', 'friendlinks_save_rss_field' );
function friendlinks_save_rss_field( $link_id ) {
if ( isset( $_POST['link_rss'] ) ) {
$rss_url = esc_url_raw( $_POST['link_rss'] );
friendlinks_update_meta( $link_id, 'rss_url', $rss_url );
}
}
/**
* 4. 链接元数据操作函数
*/
function friendlinks_update_meta( $link_id, $meta_key, $meta_value ) {
global $wpdb;
$link_id = absint( $link_id );
if ( ! $link_id || ! $meta_key ) {
return false;
}
$table_name = $wpdb->prefix . 'links_meta';
// 检查记录是否存在
$exists = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM $table_name WHERE link_id = %d AND meta_key = %s",
$link_id,
$meta_key
) );
$meta_value = maybe_serialize( $meta_value );
if ( $exists ) {
// 更新
return $wpdb->update(
$table_name,
array( 'meta_value' => $meta_value ),
array( 'link_id' => $link_id, 'meta_key' => $meta_key ),
array( '%s' ),
array( '%d', '%s' )
);
} else {
// 插入
return $wpdb->insert(
$table_name,
array(
'link_id' => $link_id,
'meta_key' => $meta_key,
'meta_value' => $meta_value
),
array( '%d', '%s', '%s' )
);
}
}
function friendlinks_get_meta( $link_id, $meta_key = '', $single = true ) {
global $wpdb;
$link_id = absint( $link_id );
if ( ! $link_id ) {
return $single ? '' : array();
}
$table_name = $wpdb->prefix . 'links_meta';
if ( $meta_key ) {
$meta_value = $wpdb->get_var( $wpdb->prepare(
"SELECT meta_value FROM $table_name WHERE link_id = %d AND meta_key = %s",
$link_id,
$meta_key
) );
if ( $meta_value ) {
$meta_value = maybe_unserialize( $meta_value );
return $single ? $meta_value : array( $meta_value );
}
return $single ? '' : array();
}
// 获取所有元数据
$results = $wpdb->get_results( $wpdb->prepare(
"SELECT meta_key, meta_value FROM $table_name WHERE link_id = %d",
$link_id
) );
$meta = array();
foreach ( $results as $result ) {
$meta[$result->meta_key] = maybe_unserialize( $result->meta_value );
}
return $meta;
}
function friendlinks_get_rss_url( $link_id ) {
return friendlinks_get_meta( $link_id, 'rss_url' );
}
/**
* 5. RSS抓取与缓存系统(增强版)
*/
class FriendLinks_RSS_Aggregator {
private static $instance = null;
private $cache_expiration = 3600; // 1小时缓存
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action( 'wp_feed_options', array( $this, 'set_feed_options' ) );
add_action( 'friendlinks_fetch_rss', array( $this, 'fetch_all_feeds' ) );
// 计划任务
if ( ! wp_next_scheduled( 'friendlinks_fetch_rss' ) ) {
wp_schedule_event( time(), 'hourly', 'friendlinks_fetch_rss' );
}
// 初始化错误日志
$this->log( 'FriendLinks RSS聚合系统初始化完成' );
}
// 设置Feed抓取选项
public function set_feed_options( $feed ) {
$feed->set_timeout( 15 ); // 15秒超时
$feed->enable_cache( true );
$feed->set_cache_duration( $this->cache_expiration );
$feed->force_feed( true ); // 强制解析为feed
}
// 抓取所有友链的RSS
public function fetch_all_feeds( $posts_per_source = 0 ) {
global $wpdb;
$this->log( '开始抓取所有友链RSS...' );
if ( $posts_per_source > 0 ) {
$this->log( '每源抓取文章数: ' . $posts_per_source );
}
$links = $wpdb->get_results(
"SELECT l.link_id, l.link_name, l.link_url, lm.meta_value as rss_url
FROM {$wpdb->links} l
LEFT JOIN {$wpdb->prefix}links_meta lm ON l.link_id = lm.link_id
WHERE lm.meta_key = 'rss_url' AND lm.meta_value != '' AND lm.meta_value IS NOT NULL"
);
$this->log( '找到 ' . count( $links ) . ' 个需要抓取的友链RSS' );
$all_posts = array();
$success_count = 0;
$error_count = 0;
if ( ! empty( $links ) ) {
foreach ( $links as $link ) {
$this->log( '正在处理友链: ' . $link->link_name . ' (' . $link->rss_url . ')' );
$feed_posts = $this->fetch_feed( $link->rss_url, $link->link_id, $link->link_name, $posts_per_source );
if ( ! empty( $feed_posts ) ) {
$all_posts = array_merge( $all_posts, $feed_posts );
$success_count++;
$this->log( '✓ 成功获取 ' . count( $feed_posts ) . ' 篇文章' );
} else {
$error_count++;
$this->log( '✗ 获取失败或没有文章' );
}
}
// 按日期排序
usort( $all_posts, function( $a, $b ) {
return strtotime( $b['date'] ) - strtotime( $a['date'] );
} );
$this->log( '总共获取到 ' . count( $all_posts ) . ' 篇文章,成功: ' . $success_count . ',失败: ' . $error_count );
}
// 存储到缓存
set_transient( 'friendlinks_rss_cache', $all_posts, $this->cache_expiration );
update_option( 'friendlinks_last_fetch', current_time( 'timestamp' ) );
update_option( 'friendlinks_fetch_count', $success_count );
if ( $error_count > 0 ) {
update_option( 'friendlinks_last_error', date( 'Y-m-d H:i:s' ) . ' - ' . $error_count . '个RSS源获取失败' );
}
return $all_posts;
}
// 抓取单个RSS源(支持每源文章数限制)
private function fetch_feed( $feed_url, $link_id, $link_name, $posts_per_source = 0 ) {
$this->log( '[详细] 开始抓取: ' . $link_name );
$this->log( '[详细] RSS地址: ' . $feed_url );
if ( ! function_exists( 'fetch_feed' ) ) {
include_once( ABSPATH . WPINC . '/feed.php' );
$this->log( '[详细] 已加载feed.php' );
}
// 验证URL格式
if ( ! filter_var( $feed_url, FILTER_VALIDATE_URL ) ) {
$this->log( '[详细] ✗ RSS地址格式无效' );
return array();
}
$this->log( '[详细] 调用fetch_feed()...' );
$feed = fetch_feed( $feed_url );
if ( is_wp_error( $feed ) ) {
$error_msg = $feed->get_error_message();
$this->log( '[详细] ✗ fetch_feed错误: ' . $error_msg );
// 记录具体错误类型
if ( strpos( $error_msg, 'cURL error 28' ) !== false ) {
$this->log( '[详细] 分析: 连接超时' );
} elseif ( strpos( strtolower( $error_msg ), 'ssl' ) !== false ) {
$this->log( '[详细] 分析: SSL证书问题' );
} elseif ( strpos( $error_msg, 'Could not find' ) !== false ) {
$this->log( '[详细] 分析: 无法找到Feed,URL可能错误' );
}
return array();
}
$this->log( '[详细] ✓ Feed对象获取成功' );
$this->log( '[详细] Feed标题: ' . $feed->get_title() );
// 检查Feed是否有效
if ( ! $feed || ! method_exists( $feed, 'get_item_quantity' ) ) {
$this->log( '[详细] ✗ Feed对象无效' );
return array();
}
// 使用传入的 posts_per_source,如果没有则使用默认值
$max_items = ( $posts_per_source > 0 ) ? $posts_per_source : 3;
$items = $feed->get_items( 0, $max_items );
$this->log( '[详细] 获取到 ' . count( $items ) . ' 篇文章 (每源限制: ' . $max_items . ')' );
$posts = array();
foreach ( $items as $index => $item ) {
$post_title = esc_html( $item->get_title() );
$post_link = esc_url( $item->get_permalink() );
$post_date = $item->get_date( 'Y-m-d H:i:s' );
$this->log( "[详细] 文章{$index}: {$post_title}" );
$posts[] = array(
'title' => $post_title,
'link' => $post_link,
'date' => $post_date,
'timestamp' => strtotime( $item->get_date( 'c' ) ),
'source_id' => $link_id,
'source' => $link_name ?: $feed->get_title()
);
}
$this->log( '[详细] ✓ 完成处理,返回 ' . count( $posts ) . ' 篇文章' );
return $posts;
}
// 获取缓存的文章(支持源数量限制和每源文章数限制)
public function get_cached_posts( $limit = 10, $source_limit = 0, $posts_per_source = 0 ) {
$cached = get_transient( 'friendlinks_rss_cache' );
// 如果缓存过期或为空,尝试抓取
if ( false === $cached || empty( $cached ) ) {
$this->log( '缓存为空或过期,尝试重新抓取' );
$cached = $this->fetch_all_feeds( $posts_per_source ); // 传递每源文章数
}
if ( empty( $cached ) ) {
return array();
}
// 如果有源数量限制,进行筛选
if ( $source_limit > 0 ) {
$cached = $this->filter_by_source_limit( $cached, $source_limit, $posts_per_source );
}
// 应用总数量限制
$result = array_slice( $cached, 0, $limit );
$this->log( '返回文章: 总数限制 ' . $limit . ',源限制 ' . $source_limit . ',每源限制 ' . $posts_per_source . ',实际返回 ' . count( $result ) . ' 篇' );
return $result;
}
// 根据源数量限制筛选文章
private function filter_by_source_limit( $posts, $source_limit, $posts_per_source ) {
if ( $source_limit <= 0 ) {
return $posts;
}
$sources = array();
$filtered_posts = array();
$per_source_limit = ( $posts_per_source > 0 ) ? $posts_per_source : 2;
foreach ( $posts as $post ) {
$source_id = $post['source_id'];
// 初始化该源的计数器
if ( ! isset( $sources[$source_id] ) ) {
$sources[$source_id] = 0;
}
// 如果该源的文章数量还没达到限制,并且我们还没有超过源数量限制
if ( $sources[$source_id] < $per_source_limit && count( $sources ) <= $source_limit ) {
$filtered_posts[] = $post;
$sources[$source_id]++;
}
// 如果已经达到源数量限制且每个源都达到了文章限制,可以提前结束
if ( count( $sources ) >= $source_limit ) {
$all_sources_full = true;
foreach ( $sources as $count ) {
if ( $count < $per_source_limit ) {
$all_sources_full = false;
break;
}
}
if ( $all_sources_full ) {
break;
}
}
}
$this->log( '按源限制筛选: 限制 ' . $source_limit . ' 个源,每源 ' . $per_source_limit . ' 篇,实际 ' . count( $sources ) . ' 个源,' . count( $filtered_posts ) . ' 篇文章' );
return $filtered_posts;
}
// 获取最后抓取时间(友好格式)
public function get_last_fetch_time() {
$timestamp = get_option( 'friendlinks_last_fetch', 0 );
if ( ! $timestamp ) {
return __( '从未抓取', 'friendlinks' );
}
$current_time = current_time( 'timestamp' );
$diff = $current_time - $timestamp;
if ( $diff < 60 ) {
return __( '刚刚', 'friendlinks' );
} elseif ( $diff < 3600 ) {
$minutes = floor( $diff / 60 );
return sprintf( __( '%d分钟前', 'friendlinks' ), $minutes );
} elseif ( $diff < 86400 ) {
$hours = floor( $diff / 3600 );
return sprintf( __( '%d小时前', 'friendlinks' ), $hours );
} else {
$days = floor( $diff / 86400 );
return sprintf( __( '%d天前', 'friendlinks' ), $days );
}
}
// 日志记录函数
private function log( $message ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
$timestamp = date( 'Y-m-d H:i:s' );
error_log( "[FriendLinks RSS] {$timestamp} - {$message}" );
}
}
}
// 初始化RSS聚合器
add_action( 'init', function() {
FriendLinks_RSS_Aggregator::get_instance();
} );
/**
* 6. 友情链接小工具
*/
class FriendLinks_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'friendlinks_widget',
__( '友情链接', 'friendlinks' ),
array(
'description' => __( '显示友情链接列表', 'friendlinks' ),
'classname' => 'widget_friendlinks'
)
);
}
public function widget( $args, $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( '友情链接', 'friendlinks' );
$category = ! empty( $instance['category'] ) ? $instance['category'] : '';
$orderby = ! empty( $instance['orderby'] ) ? $instance['orderby'] : 'name';
$limit = ! empty( $instance['limit'] ) ? absint( $instance['limit'] ) : 0;
$show_description = ! empty( $instance['show_description'] ) ? true : false;
echo $args['before_widget'];
if ( $title ) {
echo $args['before_title'] . apply_filters( 'widget_title', $title ) . $args['after_title'];
}
$query_args = array(
'orderby' => $orderby,
'order' => 'ASC',
'title_li' => '',
'echo' => 0
);
if ( ! empty( $category ) ) {
if ( is_numeric( $category ) ) {
$query_args['category'] = $category;
} else {
$query_args['category_name'] = $category;
}
}
if ( $limit > 0 ) {
$query_args['limit'] = $limit;
}
if ( $show_description ) {
$query_args['show_description'] = true;
}
$links = wp_list_bookmarks( apply_filters( 'widget_links_args', $query_args ) );
if ( $links ) {
echo '<ul class="friendlinks-list">' . $links . '</ul>';
} else {
echo '<p class="friendlinks-empty">' . __( '暂无友情链接', 'friendlinks' ) . '</p>';
}
echo $args['after_widget'];
}
public function form( $instance ) {
$defaults = array(
'title' => __( '友情链接', 'friendlinks' ),
'category' => '',
'orderby' => 'name',
'limit' => 0,
'show_description' => false
);
$instance = wp_parse_args( (array) $instance, $defaults );
?>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php _e( '标题:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text"
value="<?php echo esc_attr( $instance['title'] ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'category' ) ); ?>"><?php _e( '链接分类:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'category' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'category' ) ); ?>" type="text"
value="<?php echo esc_attr( $instance['category'] ); ?>">
<small><?php _e( '输入分类ID、名称或别名,留空显示所有', 'friendlinks' ); ?></small>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'orderby' ) ); ?>"><?php _e( '排序方式:', 'friendlinks' ); ?></label>
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'orderby' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'orderby' ) ); ?>">
<option value="name" <?php selected( $instance['orderby'], 'name' ); ?>><?php _e( '名称', 'friendlinks' ); ?></option>
<option value="rating" <?php selected( $instance['orderby'], 'rating' ); ?>><?php _e( '评分', 'friendlinks' ); ?></option>
<option value="id" <?php selected( $instance['orderby'], 'id' ); ?>><?php _e( 'ID', 'friendlinks' ); ?></option>
<option value="url" <?php selected( $instance['orderby'], 'url' ); ?>><?php _e( '网址', 'friendlinks' ); ?></option>
<option value="updated" <?php selected( $instance['orderby'], 'updated' ); ?>><?php _e( '更新时间', 'friendlinks' ); ?></option>
<option value="rand" <?php selected( $instance['orderby'], 'rand' ); ?>><?php _e( '随机', 'friendlinks' ); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>"><?php _e( '显示数量:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'limit' ) ); ?>" type="number"
value="<?php echo esc_attr( $instance['limit'] ); ?>" min="0" step="1">
<small><?php _e( '0表示不限制', 'friendlinks' ); ?></small>
</p>
<p>
<input class="checkbox" type="checkbox" <?php checked( $instance['show_description'] ); ?>
id="<?php echo esc_attr( $this->get_field_id( 'show_description' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_description' ) ); ?>" />
<label for="<?php echo esc_attr( $this->get_field_id( 'show_description' ) ); ?>"><?php _e( '显示描述', 'friendlinks' ); ?></label>
</p>
<?php
}
public function update( $new_instance, $old_instance ) {
$instance = $old_instance;
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
$instance['category'] = ! empty( $new_instance['category'] ) ? sanitize_text_field( $new_instance['category'] ) : '';
$instance['orderby'] = ! empty( $new_instance['orderby'] ) ? sanitize_text_field( $new_instance['orderby'] ) : 'name';
$instance['limit'] = ! empty( $new_instance['limit'] ) ? absint( $new_instance['limit'] ) : 0;
$instance['show_description'] = ! empty( $new_instance['show_description'] ) ? 1 : 0;
return $instance;
}
}
/**
* 7. 友链最新文章小工具(增强配置版)
*/
class FriendLinks_RSS_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'friendlinks_rss_widget',
__( '友链最新文章', 'friendlinks' ),
array(
'description' => __( '显示友情链接的最新文章,可配置显示源数量和每源文章数', 'friendlinks' ),
'classname' => 'widget_friendlinks_rss'
)
);
}
public function widget( $args, $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( '友链最新文章', 'friendlinks' );
$limit = ! empty( $instance['limit'] ) ? absint( $instance['limit'] ) : 5;
$source_limit = ! empty( $instance['source_limit'] ) ? absint( $instance['source_limit'] ) : 0;
$posts_per_source = ! empty( $instance['posts_per_source'] ) ? absint( $instance['posts_per_source'] ) : 0;
$show_date = ! empty( $instance['show_date'] ) ? true : false;
$show_source = ! empty( $instance['show_source'] ) ? true : false;
$show_fetch_time = ! empty( $instance['show_fetch_time'] ) ? true : false;
$aggregator = FriendLinks_RSS_Aggregator::get_instance();
$posts = $aggregator->get_cached_posts( $limit, $source_limit, $posts_per_source );
echo $args['before_widget'];
if ( $title ) {
echo $args['before_title'] . apply_filters( 'widget_title', $title ) . $args['after_title'];
}
if ( empty( $posts ) ) {
echo '<div class="friendlinks-no-content">';
echo '<p>' . __( '暂无友链动态数据', 'friendlinks' ) . '</p>';
if ( $show_fetch_time ) {
$last_fetch = $aggregator->get_last_fetch_time();
echo '<p class="friendlinks-fetch-time">' . sprintf( __( '最后抓取尝试:%s', 'friendlinks' ), $last_fetch ) . '</p>';
}
echo '</div>';
} else {
echo '<ul class="friendlinks-rss-list">';
foreach ( $posts as $post ) {
echo '<li class="friendlinks-rss-item">';
echo '<a href="' . esc_url( $post['link'] ) . '" target="_blank" rel="noopener noreferrer" class="friendlinks-rss-title">';
echo esc_html( $post['title'] );
echo '</a>';
if ( $show_date && ! empty( $post['date'] ) ) {
$date_display = date_i18n( get_option( 'date_format' ), strtotime( $post['date'] ) );
echo '<span class="friendlinks-rss-date">' . $date_display . '</span>';
}
if ( $show_source && ! empty( $post['source'] ) ) {
// 修复:不再显示[测试]字样,改为更友好的格式
echo '<span class="friendlinks-rss-source">来自 ' . esc_html( $post['source'] ) . '</span>';
}
echo '</li>';
}
echo '</ul>';
if ( $show_fetch_time ) {
$last_fetch = $aggregator->get_last_fetch_time();
echo '<div class="friendlinks-fetch-time">' . sprintf( __( '最后更新:%s', 'friendlinks' ), $last_fetch ) . '</div>';
}
// 显示配置摘要(调试用,可选)
if ( current_user_can( 'manage_options' ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
echo '<div class="friendlinks-debug-info" style="display:none; font-size:10px; color:#999; margin-top:5px;">';
echo '配置: 总限' . $limit . '篇';
if ( $source_limit > 0 ) echo ',源限' . $source_limit . '个';
if ( $posts_per_source > 0 ) echo ',每源' . $posts_per_source . '篇';
echo ',实际' . count( $posts ) . '篇';
echo '</div>';
}
}
echo $args['after_widget'];
}
public function form( $instance ) {
$defaults = array(
'title' => __( '友链最新文章', 'friendlinks' ),
'limit' => 5,
'source_limit' => 0,
'posts_per_source' => 0,
'show_date' => true,
'show_source' => false,
'show_fetch_time' => true
);
$instance = wp_parse_args( (array) $instance, $defaults );
?>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php _e( '标题:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text"
value="<?php echo esc_attr( $instance['title'] ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>"><?php _e( '总显示数量:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'limit' ) ); ?>" type="number"
value="<?php echo esc_attr( $instance['limit'] ); ?>" min="1" max="20" step="1">
<small><?php _e( '最多显示多少篇文章', 'friendlinks' ); ?></small>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'source_limit' ) ); ?>"><?php _e( '显示友链数量:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'source_limit' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'source_limit' ) ); ?>" type="number"
value="<?php echo esc_attr( $instance['source_limit'] ); ?>" min="0" max="10" step="1">
<small><?php _e( '0表示不限制,最多显示多少个友链的数据', 'friendlinks' ); ?></small>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'posts_per_source' ) ); ?>"><?php _e( '每个友链显示文章数:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'posts_per_source' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'posts_per_source' ) ); ?>" type="number"
value="<?php echo esc_attr( $instance['posts_per_source'] ); ?>" min="0" max="10" step="1">
<small><?php _e( '0表示不限制,每个友链最多显示多少篇文章', 'friendlinks' ); ?></small>
</p>
<p>
<input class="checkbox" type="checkbox" <?php checked( $instance['show_date'] ); ?>
id="<?php echo esc_attr( $this->get_field_id( 'show_date' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_date' ) ); ?>" />
<label for="<?php echo esc_attr( $this->get_field_id( 'show_date' ) ); ?>"><?php _e( '显示发布日期', 'friendlinks' ); ?></label>
</p>
<p>
<input class="checkbox" type="checkbox" <?php checked( $instance['show_source'] ); ?>
id="<?php echo esc_attr( $this->get_field_id( 'show_source' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_source' ) ); ?>" />
<label for="<?php echo esc_attr( $this->get_field_id( 'show_source' ) ); ?>"><?php _e( '显示来源网站', 'friendlinks' ); ?></label>
</p>
<p>
<input class="checkbox" type="checkbox" <?php checked( $instance['show_fetch_time'] ); ?>
id="<?php echo esc_attr( $this->get_field_id( 'show_fetch_time' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_fetch_time' ) ); ?>" />
<label for="<?php echo esc_attr( $this->get_field_id( 'show_fetch_time' ) ); ?>"><?php _e( '显示最后抓取时间', 'friendlinks' ); ?></label>
</p>
<?php
}
public function update( $new_instance, $old_instance ) {
$instance = $old_instance;
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
$instance['limit'] = ! empty( $new_instance['limit'] ) ? absint( $new_instance['limit'] ) : 5;
$instance['source_limit'] = ! empty( $new_instance['source_limit'] ) ? absint( $new_instance['source_limit'] ) : 0;
$instance['posts_per_source'] = ! empty( $new_instance['posts_per_source'] ) ? absint( $new_instance['posts_per_source'] ) : 0;
$instance['show_date'] = ! empty( $new_instance['show_date'] ) ? 1 : 0;
$instance['show_source'] = ! empty( $new_instance['show_source'] ) ? 1 : 0;
$instance['show_fetch_time'] = ! empty( $new_instance['show_fetch_time'] ) ? 1 : 0;
return $instance;
}
}
/**
* 8. 注册小工具
*/
function friendlinks_register_widgets() {
register_widget( 'FriendLinks_Widget' );
register_widget( 'FriendLinks_RSS_Widget' );
}
add_action( 'widgets_init', 'friendlinks_register_widgets' );
/**
* 9. 添加管理页面
*/
add_action( 'admin_menu', 'friendlinks_add_admin_page' );
function friendlinks_add_admin_page() {
add_links_page(
__( '友链RSS设置', 'friendlinks' ),
__( 'RSS设置', 'friendlinks' ),
'manage_options',
'friendlinks-settings',
'friendlinks_admin_page_content'
);
}
function friendlinks_admin_page_content() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// 处理手动抓取请求
if ( isset( $_POST['friendlinks_manual_fetch'] ) && check_admin_referer( 'friendlinks_manual_fetch' ) ) {
$aggregator = FriendLinks_RSS_Aggregator::get_instance();
$result = $aggregator->fetch_all_feeds();
$message = __( 'RSS抓取已完成!', 'friendlinks' );
}
$aggregator = FriendLinks_RSS_Aggregator::get_instance();
$last_fetch = $aggregator->get_last_fetch_time();
$fetch_count = get_option( 'friendlinks_fetch_count', 0 );
$last_error = get_option( 'friendlinks_last_error', '' );
// 获取当前配置摘要
global $wpdb;
$total_links = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->links}" );
$rss_links = $wpdb->get_var(
"SELECT COUNT(DISTINCT l.link_id)
FROM {$wpdb->links} l
LEFT JOIN {$wpdb->prefix}links_meta lm ON l.link_id = lm.link_id
WHERE lm.meta_key = 'rss_url' AND lm.meta_value != ''"
);
?>
<div class="wrap">
<h1><?php _e( '友链RSS设置', 'friendlinks' ); ?></h1>
<?php if ( isset( $message ) ) : ?>
<div class="notice notice-success is-dismissible">
<p><?php echo $message; ?></p>
</div>
<?php endif; ?>
<div class="card">
<h2 class="title"><?php _e( '系统概览', 'friendlinks' ); ?></h2>
<table class="form-table">
<tr>
<th scope="row"><?php _e( '友链总数', 'friendlinks' ); ?></th>
<td><?php echo $total_links; ?></td>
</tr>
<tr>
<th scope="row"><?php _e( '已配置RSS的友链', 'friendlinks' ); ?></th>
<td><?php echo $rss_links; ?></td>
</tr>
<tr>
<th scope="row"><?php _e( '最后抓取时间', 'friendlinks' ); ?></th>
<td><?php echo $last_fetch; ?></td>
</tr>
<tr>
<th scope="row"><?php _e( '成功抓取的RSS源', 'friendlinks' ); ?></th>
<td><?php echo $fetch_count; ?></td>
</tr>
<tr>
<th scope="row"><?php _e( '下次计划抓取', 'friendlinks' ); ?></th>
<td>
<?php
$next_scheduled = wp_next_scheduled( 'friendlinks_fetch_rss' );
if ( $next_scheduled ) {
echo date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $next_scheduled );
} else {
_e( '未计划', 'friendlinks' );
}
?>
</td>
</tr>
<?php if ( ! empty( $last_error ) ) : ?>
<tr>
<th scope="row"><?php _e( '最后错误', 'friendlinks' ); ?></th>
<td style="color: #dc3232;"><?php echo esc_html( $last_error ); ?></td>
</tr>
<?php endif; ?>
</table>
</div>
<div class="card">
<h2 class="title"><?php _e( '手动操作', 'friendlinks' ); ?></h2>
<form method="post">
<?php wp_nonce_field( 'friendlinks_manual_fetch' ); ?>
<p><?php _e( '点击按钮立即抓取所有友链的RSS更新:', 'friendlinks' ); ?></p>
<p>
<input type="submit" name="friendlinks_manual_fetch" class="button button-primary"
value="<?php _e( '立即抓取RSS', 'friendlinks' ); ?>">
</p>
</form>
</div>
<div class="card">
<h2 class="title"><?php _e( '新功能介绍', 'friendlinks' ); ?></h2>
<p><strong>版本 2.1.0 新增功能:</strong></p>
<ul>
<li>✓ 修复RSS来源显示格式(不再显示<code>[测试]</code>字样)</li>
<li>✓ 新增"显示友链数量"配置(控制显示几个友链的数据)</li>
<li>✓ 新增"每个友链显示文章数"配置(控制每个源显示多少条文章)</li>
<li>✓ 增强的日志系统,记录详细的配置执行信息</li>
</ul>
<p><?php _e( '要使用新功能,请前往"外观 → 小工具",编辑"友链最新文章"小工具进行配置。', 'friendlinks' ); ?></p>
</div>
<div class="card">
<h2 class="title"><?php _e( '调试信息', 'friendlinks' ); ?></h2>
<p><?php _e( '调试日志位置:', 'friendlinks' ); ?> <code>/wp-content/debug.log</code></p>
<p><?php _e( '日志中包含以 [FriendLinks RSS] 开头的详细抓取和配置信息。', 'friendlinks' ); ?></p>
<p>
<a href="<?php echo admin_url( 'site-health.php?tab=debug' ); ?>" class="button">
<?php _e( '查看站点健康状态', 'friendlinks' ); ?>
</a>
<button type="button" class="button button-secondary" onclick="clearFriendlinksCache()">
<?php _e( '清除RSS缓存', 'friendlinks' ); ?>
</button>
</p>
</div>
<script>
function clearFriendlinksCache() {
if (confirm('确定要清除友链RSS缓存吗?下次访问时将重新抓取数据。')) {
jQuery.post(ajaxurl, {
action: 'friendlinks_clear_cache',
_ajax_nonce: '<?php echo wp_create_nonce( "friendlinks_clear_cache" ); ?>'
}, function(response) {
if (response.success) {
alert('缓存已清除!');
location.reload();
} else {
alert('清除失败:' + response.data);
}
});
}
}
</script>
</div>
<?php
}
/**
* 10. 添加AJAX清理缓存功能
*/
add_action( 'wp_ajax_friendlinks_clear_cache', 'friendlinks_ajax_clear_cache' );
function friendlinks_ajax_clear_cache() {
if ( ! current_user_can( 'manage_options' ) || ! check_ajax_referer( 'friendlinks_clear_cache', false, false ) ) {
wp_die( '权限不足' );
}
delete_transient( 'friendlinks_rss_cache' );
update_option( 'friendlinks_last_fetch', 0 );
wp_send_json_success( '缓存已清除' );
}
/**
* 11. 添加CSS样式
*/
function friendlinks_enqueue_styles() {
wp_register_style(
'friendlinks-styles',
plugin_dir_url( __FILE__ ) . 'css/friendlinks-styles.css',
array(),
'2.1.0'
);
// 只要小工具可能显示就加载样式
if ( is_active_widget( false, false, 'friendlinks_widget', true ) ||
is_active_widget( false, false, 'friendlinks_rss_widget', true ) ||
is_admin() ) {
wp_enqueue_style( 'friendlinks-styles' );
}
// 在管理页面加载jQuery
if ( is_admin() && isset( $_GET['page'] ) && $_GET['page'] === 'friendlinks-settings' ) {
wp_enqueue_script( 'jquery' );
}
}
add_action( 'wp_enqueue_scripts', 'friendlinks_enqueue_styles' );
add_action( 'admin_enqueue_scripts', 'friendlinks_enqueue_styles' );
/**
* 12. 创建CSS文件
*/
function friendlinks_create_css_file() {
$css_dir = plugin_dir_path( __FILE__ ) . 'css';
if ( ! file_exists( $css_dir ) ) {
wp_mkdir_p( $css_dir );
}
$css_file = $css_dir . '/friendlinks-styles.css';
$css_content = '/* 友链管理插件样式 - 版本 2.1.0 */
/* 友情链接小工具样式 */
.widget_friendlinks ul.friendlinks-list,
.widget_friendlinks_rss ul.friendlinks-rss-list {
list-style: none;
padding-left: 0;
margin: 0 0 15px 0;
}
.friendlinks-list li,
.friendlinks-rss-item {
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
line-height: 1.5;
}
.friendlinks-list li:last-child,
.friendlinks-rss-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.friendlinks-list li a {
text-decoration: none;
color: #2c3e50;
font-weight: 500;
display: block;
padding: 2px 0;
transition: color 0.2s ease;
}
.friendlinks-list li a:hover {
color: #3498db;
}
.friendlinks-list li .link-description {
font-size: 12px;
color: #7f8c8d;
display: block;
margin-top: 2px;
}
/* 友链最新文章样式 */
.friendlinks-rss-title {
text-decoration: none;
color: #2980b9;
font-weight: 500;
display: block;
margin-bottom: 4px;
transition: color 0.2s ease;
}
.friendlinks-rss-title:hover {
color: #e74c3c;
text-decoration: underline;
}
.friendlinks-rss-date {
font-size: 12px;
color: #7f8c8d;
display: block;
margin-top: 2px;
}
.friendlinks-rss-source {
font-size: 11px;
color: #95a5a6;
background: #f8f9fa;
padding: 2px 6px;
border-radius: 3px;
margin-left: 8px;
display: inline-block;
font-style: italic;
}
/* 无内容提示 */
.friendlinks-no-content {
text-align: center;
padding: 20px 10px;
color: #95a5a6;
font-style: italic;
border: 1px dashed #ecf0f1;
border-radius: 4px;
background: #fdfdfd;
}
.friendlinks-empty {
color: #95a5a6;
font-style: italic;
margin: 0;
padding: 10px;
text-align: center;
}
/* 抓取时间 */
.friendlinks-fetch-time {
font-size: 11px;
color: #bdc3c7;
text-align: right;
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #ecf0f1;
}
/* 小工具表单中的说明文字 */
.widget_friendlinks_rss small {
font-size: 11px;
color: #666;
display: block;
margin-top: 2px;
}
/* 管理页面样式 */
.wrap .card {
max-width: 800px;
margin-bottom: 20px;
padding: 20px;
background: #fff;
border: 1px solid #ccd0d4;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
/* 响应式调整 */
@media screen and (max-width: 768px) {
.friendlinks-rss-source {
display: block;
margin-left: 0;
margin-top: 4px;
}
.friendlinks-rss-date {
font-size: 11px;
}
}
/* 调试信息(仅管理员可见) */
.friendlinks-debug-info {
display: none;
}';
if ( ! file_exists( $css_file ) || filesize( $css_file ) < 100 ) {
file_put_contents( $css_file, $css_content );
}
}
// 激活时创建CSS文件
register_activation_hook( __FILE__, 'friendlinks_create_css_file' );
/**
* 13. 清理计划任务(停用时)
*/
register_deactivation_hook( __FILE__, 'friendlinks_deactivate' );
function friendlinks_deactivate() {
$timestamp = wp_next_scheduled( 'friendlinks_fetch_rss' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'friendlinks_fetch_rss' );
}
// 删除缓存和选项
delete_transient( 'friendlinks_rss_cache' );
delete_option( 'friendlinks_last_fetch' );
delete_option( 'friendlinks_fetch_count' );
delete_option( 'friendlinks_last_error' );
}
/**
* 14. 加载文本域(国际化)
*/
function friendlinks_load_textdomain() {
load_plugin_textdomain( 'friendlinks', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
}
add_action( 'plugins_loaded', 'friendlinks_load_textdomain' );
代码2,增加头像管理
<?php
/**
* Plugin Name: 友链管理与RSS订阅
* Plugin URI: https://yourwebsite.com/
* Description: 为WordPress添加友链管理功能并自动抓取友链最新文章,支持友链头像显示
* Version: 3.0.0
* Author: Your Name
* License: GPL v2 or later
* Text Domain: friendlinks
*/
// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* 1. 启用WordPress原生链接管理功能
*/
add_filter( 'pre_option_link_manager_enabled', '__return_true' );
/**
* 2. 创建links_meta表
*/
register_activation_hook( __FILE__, 'friendlinks_create_tables' );
function friendlinks_create_tables() {
global $wpdb;
$table_name = $wpdb->prefix . 'links_meta';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
link_id bigint(20) unsigned NOT NULL DEFAULT '0',
meta_key varchar(255) DEFAULT NULL,
meta_value longtext,
PRIMARY KEY (meta_id),
KEY link_id (link_id),
KEY meta_key (meta_key)
) $charset_collate;";
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
dbDelta( $sql );
// 创建抓取记录选项
add_option( 'friendlinks_last_fetch', 0 );
add_option( 'friendlinks_fetch_count', 0 );
add_option( 'friendlinks_last_error', '' );
}
/**
* 3. 为链接添加RSS和头像字段
*/
// 在链接编辑页面添加字段
add_action( 'add_link_fields', 'friendlinks_add_custom_fields' );
add_action( 'edit_link_fields', 'friendlinks_add_custom_fields' );
function friendlinks_add_custom_fields( $link ) {
$rss_url = isset( $link ) ? friendlinks_get_meta( $link->link_id, 'rss_url' ) : '';
$avatar_url = isset( $link ) ? friendlinks_get_meta( $link->link_id, 'avatar_url' ) : '';
?>
<div class="form-field">
<label for="link_rss"><?php _e( 'RSS地址', 'friendlinks' ); ?></label>
<input name="link_rss" id="link_rss" type="url" value="<?php echo esc_url( $rss_url ); ?>" style="width: 95%;" />
<p class="description"><?php _e( '输入此友链的RSS订阅地址(如:https://example.com/feed/)', 'friendlinks' ); ?></p>
</div>
<div class="form-field">
<label for="link_avatar"><?php _e( '头像地址', 'friendlinks' ); ?></label>
<input name="link_avatar" id="link_avatar" type="url" value="<?php echo esc_url( $avatar_url ); ?>" style="width: 95%;" />
<p class="description"><?php _e( '输入此友链的头像图片地址(建议尺寸:50x50像素)', 'friendlinks' ); ?></p>
<?php if ( ! empty( $avatar_url ) ) : ?>
<div style="margin-top: 5px;">
<img src="<?php echo esc_url( $avatar_url ); ?>" alt="头像预览" style="max-width: 50px; max-height: 50px; border: 1px solid #ddd; padding: 2px; border-radius: 3px;" />
</div>
<?php endif; ?>
</div>
<?php
}
// 保存字段
add_action( 'add_link', 'friendlinks_save_custom_fields' );
add_action( 'edit_link', 'friendlinks_save_custom_fields' );
function friendlinks_save_custom_fields( $link_id ) {
if ( isset( $_POST['link_rss'] ) ) {
$rss_url = esc_url_raw( $_POST['link_rss'] );
friendlinks_update_meta( $link_id, 'rss_url', $rss_url );
}
if ( isset( $_POST['link_avatar'] ) ) {
$avatar_url = esc_url_raw( $_POST['link_avatar'] );
friendlinks_update_meta( $link_id, 'avatar_url', $avatar_url );
}
}
/**
* 4. 链接元数据操作函数
*/
function friendlinks_update_meta( $link_id, $meta_key, $meta_value ) {
global $wpdb;
$link_id = absint( $link_id );
if ( ! $link_id || ! $meta_key ) {
return false;
}
$table_name = $wpdb->prefix . 'links_meta';
// 检查记录是否存在
$exists = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM $table_name WHERE link_id = %d AND meta_key = %s",
$link_id,
$meta_key
) );
$meta_value = maybe_serialize( $meta_value );
if ( $exists ) {
// 更新
return $wpdb->update(
$table_name,
array( 'meta_value' => $meta_value ),
array( 'link_id' => $link_id, 'meta_key' => $meta_key ),
array( '%s' ),
array( '%d', '%s' )
);
} else {
// 插入
return $wpdb->insert(
$table_name,
array(
'link_id' => $link_id,
'meta_key' => $meta_key,
'meta_value' => $meta_value
),
array( '%d', '%s', '%s' )
);
}
}
function friendlinks_get_meta( $link_id, $meta_key = '', $single = true ) {
global $wpdb;
$link_id = absint( $link_id );
if ( ! $link_id ) {
return $single ? '' : array();
}
$table_name = $wpdb->prefix . 'links_meta';
if ( $meta_key ) {
$meta_value = $wpdb->get_var( $wpdb->prepare(
"SELECT meta_value FROM $table_name WHERE link_id = %d AND meta_key = %s",
$link_id,
$meta_key
) );
if ( $meta_value ) {
$meta_value = maybe_unserialize( $meta_value );
return $single ? $meta_value : array( $meta_value );
}
return $single ? '' : array();
}
// 获取所有元数据
$results = $wpdb->get_results( $wpdb->prepare(
"SELECT meta_key, meta_value FROM $table_name WHERE link_id = %d",
$link_id
) );
$meta = array();
foreach ( $results as $result ) {
$meta[$result->meta_key] = maybe_unserialize( $result->meta_value );
}
return $meta;
}
function friendlinks_get_rss_url( $link_id ) {
return friendlinks_get_meta( $link_id, 'rss_url' );
}
function friendlinks_get_avatar_url( $link_id ) {
return friendlinks_get_meta( $link_id, 'avatar_url' );
}
/**
* 5. RSS抓取与缓存系统(增强版)
*/
class FriendLinks_RSS_Aggregator {
private static $instance = null;
private $cache_expiration = 3600; // 1小时缓存
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action( 'wp_feed_options', array( $this, 'set_feed_options' ) );
add_action( 'friendlinks_fetch_rss', array( $this, 'fetch_all_feeds' ) );
// 计划任务
if ( ! wp_next_scheduled( 'friendlinks_fetch_rss' ) ) {
wp_schedule_event( time(), 'hourly', 'friendlinks_fetch_rss' );
}
// 初始化错误日志
$this->log( 'FriendLinks RSS聚合系统初始化完成' );
}
// 设置Feed抓取选项
public function set_feed_options( $feed ) {
$feed->set_timeout( 15 ); // 15秒超时
$feed->enable_cache( true );
$feed->set_cache_duration( $this->cache_expiration );
$feed->force_feed( true ); // 强制解析为feed
}
// 抓取所有友链的RSS
public function fetch_all_feeds( $posts_per_source = 0 ) {
global $wpdb;
$this->log( '开始抓取所有友链RSS...' );
if ( $posts_per_source > 0 ) {
$this->log( '每源抓取文章数: ' . $posts_per_source );
}
$links = $wpdb->get_results(
"SELECT l.link_id, l.link_name, l.link_url, l.link_image,
lm_rss.meta_value as rss_url,
lm_avatar.meta_value as avatar_url
FROM {$wpdb->links} l
LEFT JOIN {$wpdb->prefix}links_meta lm_rss ON l.link_id = lm_rss.link_id AND lm_rss.meta_key = 'rss_url'
LEFT JOIN {$wpdb->prefix}links_meta lm_avatar ON l.link_id = lm_avatar.link_id AND lm_avatar.meta_key = 'avatar_url'
WHERE lm_rss.meta_value != '' AND lm_rss.meta_value IS NOT NULL"
);
$this->log( '找到 ' . count( $links ) . ' 个需要抓取的友链RSS' );
$all_posts = array();
$success_count = 0;
$error_count = 0;
if ( ! empty( $links ) ) {
foreach ( $links as $link ) {
$this->log( '正在处理友链: ' . $link->link_name . ' (' . $link->rss_url . ')' );
$feed_posts = $this->fetch_feed( $link->rss_url, $link->link_id, $link->link_name, $posts_per_source );
if ( ! empty( $feed_posts ) ) {
// 为每篇文章添加头像信息
foreach ( $feed_posts as &$post ) {
$post['avatar'] = $this->get_avatar_for_link( $link );
}
$all_posts = array_merge( $all_posts, $feed_posts );
$success_count++;
$this->log( '✓ 成功获取 ' . count( $feed_posts ) . ' 篇文章' );
} else {
$error_count++;
$this->log( '✗ 获取失败或没有文章' );
}
}
// 按日期排序
usort( $all_posts, function( $a, $b ) {
return strtotime( $b['date'] ) - strtotime( $a['date'] );
} );
$this->log( '总共获取到 ' . count( $all_posts ) . ' 篇文章,成功: ' . $success_count . ',失败: ' . $error_count );
}
// 存储到缓存
set_transient( 'friendlinks_rss_cache', $all_posts, $this->cache_expiration );
update_option( 'friendlinks_last_fetch', current_time( 'timestamp' ) );
update_option( 'friendlinks_fetch_count', $success_count );
if ( $error_count > 0 ) {
update_option( 'friendlinks_last_error', date( 'Y-m-d H:i:s' ) . ' - ' . $error_count . '个RSS源获取失败' );
}
return $all_posts;
}
// 获取友链头像(优先级:自定义头像 > 链接图片 > 默认头像)
private function get_avatar_for_link( $link ) {
// 1. 自定义头像字段
if ( ! empty( $link->avatar_url ) ) {
return esc_url( $link->avatar_url );
}
// 2. WordPress链接自带的图片字段
if ( ! empty( $link->link_image ) ) {
return esc_url( $link->link_image );
}
// 3. 尝试从网站获取favicon
$favicon = $this->get_favicon_url( $link->link_url );
if ( $favicon ) {
return $favicon;
}
// 4. 返回默认头像
return plugins_url( 'images/default-avatar.png', __FILE__ );
}
// 获取网站favicon
private function get_favicon_url( $site_url ) {
$domain = parse_url( $site_url, PHP_URL_HOST );
if ( ! $domain ) {
return false;
}
// 常见favicon地址
$favicon_urls = array(
'https://' . $domain . '/favicon.ico',
'https://' . $domain . '/favicon.png',
'https://www.' . $domain . '/favicon.ico',
'https://www.' . $domain . '/favicon.png',
);
// 简单的URL验证,实际使用时可能需要更复杂的检查
foreach ( $favicon_urls as $url ) {
if ( filter_var( $url, FILTER_VALIDATE_URL ) ) {
return $url;
}
}
return false;
}
// 抓取单个RSS源(支持每源文章数限制)
private function fetch_feed( $feed_url, $link_id, $link_name, $posts_per_source = 0 ) {
$this->log( '[详细] 开始抓取: ' . $link_name );
$this->log( '[详细] RSS地址: ' . $feed_url );
if ( ! function_exists( 'fetch_feed' ) ) {
include_once( ABSPATH . WPINC . '/feed.php' );
$this->log( '[详细] 已加载feed.php' );
}
// 验证URL格式
if ( ! filter_var( $feed_url, FILTER_VALIDATE_URL ) ) {
$this->log( '[详细] ✗ RSS地址格式无效' );
return array();
}
$this->log( '[详细] 调用fetch_feed()...' );
$feed = fetch_feed( $feed_url );
if ( is_wp_error( $feed ) ) {
$error_msg = $feed->get_error_message();
$this->log( '[详细] ✗ fetch_feed错误: ' . $error_msg );
// 记录具体错误类型
if ( strpos( $error_msg, 'cURL error 28' ) !== false ) {
$this->log( '[详细] 分析: 连接超时' );
} elseif ( strpos( strtolower( $error_msg ), 'ssl' ) !== false ) {
$this->log( '[详细] 分析: SSL证书问题' );
} elseif ( strpos( $error_msg, 'Could not find' ) !== false ) {
$this->log( '[详细] 分析: 无法找到Feed,URL可能错误' );
}
return array();
}
$this->log( '[详细] ✓ Feed对象获取成功' );
$this->log( '[详细] Feed标题: ' . $feed->get_title() );
// 检查Feed是否有效
if ( ! $feed || ! method_exists( $feed, 'get_item_quantity' ) ) {
$this->log( '[详细] ✗ Feed对象无效' );
return array();
}
// 使用传入的 posts_per_source,如果没有则使用默认值
$max_items = ( $posts_per_source > 0 ) ? $posts_per_source : 3;
$items = $feed->get_items( 0, $max_items );
$this->log( '[详细] 获取到 ' . count( $items ) . ' 篇文章 (每源限制: ' . $max_items . ')' );
$posts = array();
foreach ( $items as $index => $item ) {
$post_title = esc_html( $item->get_title() );
$post_link = esc_url( $item->get_permalink() );
$post_date = $item->get_date( 'Y-m-d H:i:s' );
$this->log( "[详细] 文章{$index}: {$post_title}" );
$posts[] = array(
'title' => $post_title,
'link' => $post_link,
'date' => $post_date,
'timestamp' => strtotime( $item->get_date( 'c' ) ),
'source_id' => $link_id,
'source' => $link_name ?: $feed->get_title(),
'avatar' => '' // 将在外部添加
);
}
$this->log( '[详细] ✓ 完成处理,返回 ' . count( $posts ) . ' 篇文章' );
return $posts;
}
// 获取缓存的文章(支持源数量限制和每源文章数限制)
public function get_cached_posts( $limit = 10, $source_limit = 0, $posts_per_source = 0 ) {
$cached = get_transient( 'friendlinks_rss_cache' );
// 如果缓存过期或为空,尝试抓取
if ( false === $cached || empty( $cached ) ) {
$this->log( '缓存为空或过期,尝试重新抓取' );
$cached = $this->fetch_all_feeds( $posts_per_source ); // 传递每源文章数
}
if ( empty( $cached ) ) {
return array();
}
// 如果有源数量限制,进行筛选
if ( $source_limit > 0 ) {
$cached = $this->filter_by_source_limit( $cached, $source_limit, $posts_per_source );
}
// 应用总数量限制
$result = array_slice( $cached, 0, $limit );
$this->log( '返回文章: 总数限制 ' . $limit . ',源限制 ' . $source_limit . ',每源限制 ' . $posts_per_source . ',实际返回 ' . count( $result ) . ' 篇' );
return $result;
}
// 根据源数量限制筛选文章
private function filter_by_source_limit( $posts, $source_limit, $posts_per_source ) {
if ( $source_limit <= 0 ) {
return $posts;
}
$sources = array();
$filtered_posts = array();
$per_source_limit = ( $posts_per_source > 0 ) ? $posts_per_source : 2;
foreach ( $posts as $post ) {
$source_id = $post['source_id'];
// 初始化该源的计数器
if ( ! isset( $sources[$source_id] ) ) {
$sources[$source_id] = 0;
}
// 如果该源的文章数量还没达到限制,并且我们还没有超过源数量限制
if ( $sources[$source_id] < $per_source_limit && count( $sources ) <= $source_limit ) {
$filtered_posts[] = $post;
$sources[$source_id]++;
}
// 如果已经达到源数量限制且每个源都达到了文章限制,可以提前结束
if ( count( $sources ) >= $source_limit ) {
$all_sources_full = true;
foreach ( $sources as $count ) {
if ( $count < $per_source_limit ) {
$all_sources_full = false;
break;
}
}
if ( $all_sources_full ) {
break;
}
}
}
$this->log( '按源限制筛选: 限制 ' . $source_limit . ' 个源,每源 ' . $per_source_limit . ' 篇,实际 ' . count( $sources ) . ' 个源,' . count( $filtered_posts ) . ' 篇文章' );
return $filtered_posts;
}
// 获取最后抓取时间(友好格式)
public function get_last_fetch_time() {
$timestamp = get_option( 'friendlinks_last_fetch', 0 );
if ( ! $timestamp ) {
return __( '从未抓取', 'friendlinks' );
}
$current_time = current_time( 'timestamp' );
$diff = $current_time - $timestamp;
if ( $diff < 60 ) {
return __( '刚刚', 'friendlinks' );
} elseif ( $diff < 3600 ) {
$minutes = floor( $diff / 60 );
return sprintf( __( '%d分钟前', 'friendlinks' ), $minutes );
} elseif ( $diff < 86400 ) {
$hours = floor( $diff / 3600 );
return sprintf( __( '%d小时前', 'friendlinks' ), $hours );
} else {
$days = floor( $diff / 86400 );
return sprintf( __( '%d天前', 'friendlinks' ), $days );
}
}
// 日志记录函数
private function log( $message ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
$timestamp = date( 'Y-m-d H:i:s' );
error_log( "[FriendLinks RSS] {$timestamp} - {$message}" );
}
}
}
// 初始化RSS聚合器
add_action( 'init', function() {
FriendLinks_RSS_Aggregator::get_instance();
} );
/**
* 6. 友情链接小工具
*/
class FriendLinks_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'friendlinks_widget',
__( '友情链接', 'friendlinks' ),
array(
'description' => __( '显示友情链接列表', 'friendlinks' ),
'classname' => 'widget_friendlinks'
)
);
}
public function widget( $args, $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( '友情链接', 'friendlinks' );
$category = ! empty( $instance['category'] ) ? $instance['category'] : '';
$orderby = ! empty( $instance['orderby'] ) ? $instance['orderby'] : 'name';
$limit = ! empty( $instance['limit'] ) ? absint( $instance['limit'] ) : 0;
$show_description = ! empty( $instance['show_description'] ) ? true : false;
$show_avatars = ! empty( $instance['show_avatars'] ) ? true : false;
$avatar_size = ! empty( $instance['avatar_size'] ) ? absint( $instance['avatar_size'] ) : 32;
echo $args['before_widget'];
if ( $title ) {
echo $args['before_title'] . apply_filters( 'widget_title', $title ) . $args['after_title'];
}
$query_args = array(
'orderby' => $orderby,
'order' => 'ASC',
'title_li' => '',
'echo' => 0
);
if ( ! empty( $category ) ) {
if ( is_numeric( $category ) ) {
$query_args['category'] = $category;
} else {
$query_args['category_name'] = $category;
}
}
if ( $limit > 0 ) {
$query_args['limit'] = $limit;
}
if ( $show_description ) {
$query_args['show_description'] = true;
}
$links = wp_list_bookmarks( apply_filters( 'widget_links_args', $query_args ) );
if ( $links ) {
// 如果需要显示头像,我们需要自定义输出
if ( $show_avatars ) {
echo '<ul class="friendlinks-list with-avatars">';
$links_array = wp_list_bookmarks( array_merge( $query_args, array( 'echo' => false ) ) );
// 这里需要解析链接并添加头像,但由于wp_list_bookmarks返回的是HTML字符串,我们重写输出逻辑
global $wpdb;
$bookmarks = get_bookmarks( $query_args );
foreach ( $bookmarks as $bookmark ) {
$avatar_url = friendlinks_get_avatar_url( $bookmark->link_id );
if ( empty( $avatar_url ) && ! empty( $bookmark->link_image ) ) {
$avatar_url = $bookmark->link_image;
}
echo '<li class="friendlinks-item-with-avatar">';
if ( $avatar_url ) {
echo '<img src="' . esc_url( $avatar_url ) . '" alt="' . esc_attr( $bookmark->link_name ) . '" class="friendlink-avatar" style="width:' . $avatar_size . 'px;height:' . $avatar_size . 'px;" /> ';
}
echo '<a href="' . esc_url( $bookmark->link_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $bookmark->link_name ) . '</a>';
if ( $show_description && ! empty( $bookmark->link_description ) ) {
echo '<span class="link-description">' . esc_html( $bookmark->link_description ) . '</span>';
}
echo '</li>';
}
echo '</ul>';
} else {
echo '<ul class="friendlinks-list">' . $links . '</ul>';
}
} else {
echo '<p class="friendlinks-empty">' . __( '暂无友情链接', 'friendlinks' ) . '</p>';
}
echo $args['after_widget'];
}
public function form( $instance ) {
$defaults = array(
'title' => __( '友情链接', 'friendlinks' ),
'category' => '',
'orderby' => 'name',
'limit' => 0,
'show_description' => false,
'show_avatars' => false,
'avatar_size' => 32
);
$instance = wp_parse_args( (array) $instance, $defaults );
?>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php _e( '标题:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text"
value="<?php echo esc_attr( $instance['title'] ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'category' ) ); ?>"><?php _e( '链接分类:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'category' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'category' ) ); ?>" type="text"
value="<?php echo esc_attr( $instance['category'] ); ?>">
<small><?php _e( '输入分类ID、名称或别名,留空显示所有', 'friendlinks' ); ?></small>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'orderby' ) ); ?>"><?php _e( '排序方式:', 'friendlinks' ); ?></label>
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'orderby' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'orderby' ) ); ?>">
<option value="name" <?php selected( $instance['orderby'], 'name' ); ?>><?php _e( '名称', 'friendlinks' ); ?></option>
<option value="rating" <?php selected( $instance['orderby'], 'rating' ); ?>><?php _e( '评分', 'friendlinks' ); ?></option>
<option value="id" <?php selected( $instance['orderby'], 'id' ); ?>><?php _e( 'ID', 'friendlinks' ); ?></option>
<option value="url" <?php selected( $instance['orderby'], 'url' ); ?>><?php _e( '网址', 'friendlinks' ); ?></option>
<option value="updated" <?php selected( $instance['orderby'], 'updated' ); ?>><?php _e( '更新时间', 'friendlinks' ); ?></option>
<option value="rand" <?php selected( $instance['orderby'], 'rand' ); ?>><?php _e( '随机', 'friendlinks' ); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>"><?php _e( '显示数量:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'limit' ) ); ?>" type="number"
value="<?php echo esc_attr( $instance['limit'] ); ?>" min="0" step="1">
<small><?php _e( '0表示不限制', 'friendlinks' ); ?></small>
</p>
<p>
<input class="checkbox" type="checkbox" <?php checked( $instance['show_description'] ); ?>
id="<?php echo esc_attr( $this->get_field_id( 'show_description' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_description' ) ); ?>" />
<label for="<?php echo esc_attr( $this->get_field_id( 'show_description' ) ); ?>"><?php _e( '显示描述', 'friendlinks' ); ?></label>
</p>
<p>
<input class="checkbox" type="checkbox" <?php checked( $instance['show_avatars'] ); ?>
id="<?php echo esc_attr( $this->get_field_id( 'show_avatars' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_avatars' ) ); ?>" />
<label for="<?php echo esc_attr( $this->get_field_id( 'show_avatars' ) ); ?>"><?php _e( '显示头像', 'friendlinks' ); ?></label>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'avatar_size' ) ); ?>"><?php _e( '头像尺寸:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'avatar_size' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'avatar_size' ) ); ?>" type="number"
value="<?php echo esc_attr( $instance['avatar_size'] ); ?>" min="16" max="100" step="1">
<small><?php _e( '头像的宽度和高度(像素)', 'friendlinks' ); ?></small>
</p>
<?php
}
public function update( $new_instance, $old_instance ) {
$instance = $old_instance;
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
$instance['category'] = ! empty( $new_instance['category'] ) ? sanitize_text_field( $new_instance['category'] ) : '';
$instance['orderby'] = ! empty( $new_instance['orderby'] ) ? sanitize_text_field( $new_instance['orderby'] ) : 'name';
$instance['limit'] = ! empty( $new_instance['limit'] ) ? absint( $new_instance['limit'] ) : 0;
$instance['show_description'] = ! empty( $new_instance['show_description'] ) ? 1 : 0;
$instance['show_avatars'] = ! empty( $new_instance['show_avatars'] ) ? 1 : 0;
$instance['avatar_size'] = ! empty( $new_instance['avatar_size'] ) ? absint( $new_instance['avatar_size'] ) : 32;
return $instance;
}
}
/**
* 7. 友链最新文章小工具(增强配置版,支持头像显示)
*/
class FriendLinks_RSS_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'friendlinks_rss_widget',
__( '友链最新文章', 'friendlinks' ),
array(
'description' => __( '显示友情链接的最新文章,可配置显示源数量和每源文章数,支持头像显示', 'friendlinks' ),
'classname' => 'widget_friendlinks_rss'
)
);
}
public function widget( $args, $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( '友链最新文章', 'friendlinks' );
$limit = ! empty( $instance['limit'] ) ? absint( $instance['limit'] ) : 5;
$source_limit = ! empty( $instance['source_limit'] ) ? absint( $instance['source_limit'] ) : 0;
$posts_per_source = ! empty( $instance['posts_per_source'] ) ? absint( $instance['posts_per_source'] ) : 0;
$show_date = ! empty( $instance['show_date'] ) ? true : false;
$show_source = ! empty( $instance['show_source'] ) ? true : false;
$show_fetch_time = ! empty( $instance['show_fetch_time'] ) ? true : false;
$show_avatar = ! empty( $instance['show_avatar'] ) ? true : false;
$avatar_position = ! empty( $instance['avatar_position'] ) ? $instance['avatar_position'] : 'before';
$avatar_size = ! empty( $instance['avatar_size'] ) ? absint( $instance['avatar_size'] ) : 32;
$aggregator = FriendLinks_RSS_Aggregator::get_instance();
$posts = $aggregator->get_cached_posts( $limit, $source_limit, $posts_per_source );
echo $args['before_widget'];
if ( $title ) {
echo $args['before_title'] . apply_filters( 'widget_title', $title ) . $args['after_title'];
}
if ( empty( $posts ) ) {
echo '<div class="friendlinks-no-content">';
echo '<p>' . __( '暂无友链动态数据', 'friendlinks' ) . '</p>';
if ( $show_fetch_time ) {
$last_fetch = $aggregator->get_last_fetch_time();
echo '<p class="friendlinks-fetch-time">' . sprintf( __( '最后抓取尝试:%s', 'friendlinks' ), $last_fetch ) . '</p>';
}
echo '</div>';
} else {
echo '<ul class="friendlinks-rss-list' . ( $show_avatar ? ' with-avatars' : '' ) . '">';
foreach ( $posts as $post ) {
echo '<li class="friendlinks-rss-item">';
// 显示头像(如果启用)
if ( $show_avatar && ! empty( $post['avatar'] ) && $avatar_position === 'before' ) {
echo '<img src="' . esc_url( $post['avatar'] ) . '" alt="' . esc_attr( $post['source'] ) . '" class="friendlinks-rss-avatar avatar-' . $avatar_position . '" style="width:' . $avatar_size . 'px;height:' . $avatar_size . 'px;" /> ';
}
// 文章标题链接
echo '<a href="' . esc_url( $post['link'] ) . '" target="_blank" rel="noopener noreferrer" class="friendlinks-rss-title">';
echo esc_html( $post['title'] );
echo '</a>';
// 显示头像(如果启用且在标题后)
if ( $show_avatar && ! empty( $post['avatar'] ) && $avatar_position === 'after' ) {
echo ' <img src="' . esc_url( $post['avatar'] ) . '" alt="' . esc_attr( $post['source'] ) . '" class="friendlinks-rss-avatar avatar-' . $avatar_position . '" style="width:' . $avatar_size . 'px;height:' . $avatar_size . 'px;" />';
}
// 显示日期
if ( $show_date && ! empty( $post['date'] ) ) {
$date_display = date_i18n( get_option( 'date_format' ), strtotime( $post['date'] ) );
echo '<span class="friendlinks-rss-date">' . $date_display . '</span>';
}
// 显示来源
if ( $show_source && ! empty( $post['source'] ) ) {
echo '<span class="friendlinks-rss-source">来自 ' . esc_html( $post['source'] ) . '</span>';
}
echo '</li>';
}
echo '</ul>';
if ( $show_fetch_time ) {
$last_fetch = $aggregator->get_last_fetch_time();
echo '<div class="friendlinks-fetch-time">' . sprintf( __( '最后更新:%s', 'friendlinks' ), $last_fetch ) . '</div>';
}
// 显示配置摘要(调试用,可选)
if ( current_user_can( 'manage_options' ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
echo '<div class="friendlinks-debug-info" style="display:none; font-size:10px; color:#999; margin-top:5px;">';
echo '配置: 总限' . $limit . '篇';
if ( $source_limit > 0 ) echo ',源限' . $source_limit . '个';
if ( $posts_per_source > 0 ) echo ',每源' . $posts_per_source . '篇';
echo ',实际' . count( $posts ) . '篇';
echo '</div>';
}
}
echo $args['after_widget'];
}
public function form( $instance ) {
$defaults = array(
'title' => __( '友链最新文章', 'friendlinks' ),
'limit' => 5,
'source_limit' => 0,
'posts_per_source' => 0,
'show_date' => true,
'show_source' => false,
'show_fetch_time' => true,
'show_avatar' => false,
'avatar_position' => 'before',
'avatar_size' => 32
);
$instance = wp_parse_args( (array) $instance, $defaults );
?>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php _e( '标题:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text"
value="<?php echo esc_attr( $instance['title'] ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>"><?php _e( '总显示数量:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'limit' ) ); ?>" type="number"
value="<?php echo esc_attr( $instance['limit'] ); ?>" min="1" max="20" step="1">
<small><?php _e( '最多显示多少篇文章', 'friendlinks' ); ?></small>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'source_limit' ) ); ?>"><?php _e( '显示友链数量:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'source_limit' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'source_limit' ) ); ?>" type="number"
value="<?php echo esc_attr( $instance['source_limit'] ); ?>" min="0" max="10" step="1">
<small><?php _e( '0表示不限制,最多显示多少个友链的数据', 'friendlinks' ); ?></small>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'posts_per_source' ) ); ?>"><?php _e( '每个友链显示文章数:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'posts_per_source' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'posts_per_source' ) ); ?>" type="number"
value="<?php echo esc_attr( $instance['posts_per_source'] ); ?>" min="0" max="10" step="1">
<small><?php _e( '0表示不限制,每个友链最多显示多少篇文章', 'friendlinks' ); ?></small>
</p>
<p>
<input class="checkbox" type="checkbox" <?php checked( $instance['show_date'] ); ?>
id="<?php echo esc_attr( $this->get_field_id( 'show_date' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_date' ) ); ?>" />
<label for="<?php echo esc_attr( $this->get_field_id( 'show_date' ) ); ?>"><?php _e( '显示发布日期', 'friendlinks' ); ?></label>
</p>
<p>
<input class="checkbox" type="checkbox" <?php checked( $instance['show_source'] ); ?>
id="<?php echo esc_attr( $this->get_field_id( 'show_source' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_source' ) ); ?>" />
<label for="<?php echo esc_attr( $this->get_field_id( 'show_source' ) ); ?>"><?php _e( '显示来源网站', 'friendlinks' ); ?></label>
</p>
<p>
<input class="checkbox" type="checkbox" <?php checked( $instance['show_fetch_time'] ); ?>
id="<?php echo esc_attr( $this->get_field_id( 'show_fetch_time' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_fetch_time' ) ); ?>" />
<label for="<?php echo esc_attr( $this->get_field_id( 'show_fetch_time' ) ); ?>"><?php _e( '显示最后抓取时间', 'friendlinks' ); ?></label>
</p>
<hr style="border: none; border-top: 1px solid #ddd; margin: 15px 0;">
<p><strong><?php _e( '头像设置', 'friendlinks' ); ?></strong></p>
<p>
<input class="checkbox" type="checkbox" <?php checked( $instance['show_avatar'] ); ?>
id="<?php echo esc_attr( $this->get_field_id( 'show_avatar' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_avatar' ) ); ?>" />
<label for="<?php echo esc_attr( $this->get_field_id( 'show_avatar' ) ); ?>"><?php _e( '显示友链头像', 'friendlinks' ); ?></label>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'avatar_position' ) ); ?>"><?php _e( '头像位置:', 'friendlinks' ); ?></label>
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'avatar_position' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'avatar_position' ) ); ?>">
<option value="before" <?php selected( $instance['avatar_position'], 'before' ); ?>><?php _e( '标题前', 'friendlinks' ); ?></option>
<option value="after" <?php selected( $instance['avatar_position'], 'after' ); ?>><?php _e( '标题后', 'friendlinks' ); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'avatar_size' ) ); ?>"><?php _e( '头像尺寸:', 'friendlinks' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'avatar_size' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'avatar_size' ) ); ?>" type="number"
value="<?php echo esc_attr( $instance['avatar_size'] ); ?>" min="16" max="100" step="1">
<small><?php _e( '头像的宽度和高度(像素)', 'friendlinks' ); ?></small>
</p>
<?php
}
public function update( $new_instance, $old_instance ) {
$instance = $old_instance;
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
$instance['limit'] = ! empty( $new_instance['limit'] ) ? absint( $new_instance['limit'] ) : 5;
$instance['source_limit'] = ! empty( $new_instance['source_limit'] ) ? absint( $new_instance['source_limit'] ) : 0;
$instance['posts_per_source'] = ! empty( $new_instance['posts_per_source'] ) ? absint( $new_instance['posts_per_source'] ) : 0;
$instance['show_date'] = ! empty( $new_instance['show_date'] ) ? 1 : 0;
$instance['show_source'] = ! empty( $new_instance['show_source'] ) ? 1 : 0;
$instance['show_fetch_time'] = ! empty( $new_instance['show_fetch_time'] ) ? 1 : 0;
$instance['show_avatar'] = ! empty( $new_instance['show_avatar'] ) ? 1 : 0;
$instance['avatar_position'] = ! empty( $new_instance['avatar_position'] ) ? sanitize_text_field( $new_instance['avatar_position'] ) : 'before';
$instance['avatar_size'] = ! empty( $new_instance['avatar_size'] ) ? absint( $new_instance['avatar_size'] ) : 32;
return $instance;
}
}
/**
* 8. 注册小工具
*/
function friendlinks_register_widgets() {
register_widget( 'FriendLinks_Widget' );
register_widget( 'FriendLinks_RSS_Widget' );
}
add_action( 'widgets_init', 'friendlinks_register_widgets' );
/**
* 9. 添加管理页面
*/
add_action( 'admin_menu', 'friendlinks_add_admin_page' );
function friendlinks_add_admin_page() {
add_links_page(
__( '友链RSS设置', 'friendlinks' ),
__( 'RSS设置', 'friendlinks' ),
'manage_options',
'friendlinks-settings',
'friendlinks_admin_page_content'
);
}
function friendlinks_admin_page_content() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// 处理手动抓取请求
if ( isset( $_POST['friendlinks_manual_fetch'] ) && check_admin_referer( 'friendlinks_manual_fetch' ) ) {
$aggregator = FriendLinks_RSS_Aggregator::get_instance();
$result = $aggregator->fetch_all_feeds();
$message = __( 'RSS抓取已完成!', 'friendlinks' );
}
$aggregator = FriendLinks_RSS_Aggregator::get_instance();
$last_fetch = $aggregator->get_last_fetch_time();
$fetch_count = get_option( 'friendlinks_fetch_count', 0 );
$last_error = get_option( 'friendlinks_last_error', '' );
// 获取当前配置摘要
global $wpdb;
$total_links = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->links}" );
$rss_links = $wpdb->get_var(
"SELECT COUNT(DISTINCT l.link_id)
FROM {$wpdb->links} l
LEFT JOIN {$wpdb->prefix}links_meta lm ON l.link_id = lm.link_id AND lm.meta_key = 'rss_url'
WHERE lm.meta_value != ''"
);
$avatar_links = $wpdb->get_var(
"SELECT COUNT(DISTINCT l.link_id)
FROM {$wpdb->links} l
LEFT JOIN {$wpdb->prefix}links_meta lm ON l.link_id = lm.link_id AND lm.meta_key = 'avatar_url'
WHERE lm.meta_value != ''"
);
?>
<div class="wrap">
<h1><?php _e( '友链RSS设置', 'friendlinks' ); ?></h1>
<?php if ( isset( $message ) ) : ?>
<div class="notice notice-success is-dismissible">
<p><?php echo $message; ?></p>
</div>
<?php endif; ?>
<div class="card">
<h2 class="title"><?php _e( '系统概览', 'friendlinks' ); ?></h2>
<table class="form-table">
<tr>
<th scope="row"><?php _e( '友链总数', 'friendlinks' ); ?></th>
<td><?php echo $total_links; ?></td>
</tr>
<tr>
<th scope="row"><?php _e( '已配置RSS的友链', 'friendlinks' ); ?></th>
<td><?php echo $rss_links; ?></td>
</tr>
<tr>
<th scope="row"><?php _e( '已配置头像的友链', 'friendlinks' ); ?></th>
<td><?php echo $avatar_links; ?></td>
</tr>
<tr>
<th scope="row"><?php _e( '最后抓取时间', 'friendlinks' ); ?></th>
<td><?php echo $last_fetch; ?></td>
</tr>
<tr>
<th scope="row"><?php _e( '成功抓取的RSS源', 'friendlinks' ); ?></th>
<td><?php echo $fetch_count; ?></td>
</tr>
<tr>
<th scope="row"><?php _e( '下次计划抓取', 'friendlinks' ); ?></th>
<td>
<?php
$next_scheduled = wp_next_scheduled( 'friendlinks_fetch_rss' );
if ( $next_scheduled ) {
echo date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $next_scheduled );
} else {
_e( '未计划', 'friendlinks' );
}
?>
</td>
</tr>
<?php if ( ! empty( $last_error ) ) : ?>
<tr>
<th scope="row"><?php _e( '最后错误', 'friendlinks' ); ?></th>
<td style="color: #dc3232;"><?php echo esc_html( $last_error ); ?></td>
</tr>
<?php endif; ?>
</table>
</div>
<div class="card">
<h2 class="title"><?php _e( '手动操作', 'friendlinks' ); ?></h2>
<form method="post">
<?php wp_nonce_field( 'friendlinks_manual_fetch' ); ?>
<p><?php _e( '点击按钮立即抓取所有友链的RSS更新:', 'friendlinks' ); ?></p>
<p>
<input type="submit" name="friendlinks_manual_fetch" class="button button-primary"
value="<?php _e( '立即抓取RSS', 'friendlinks' ); ?>">
</p>
</form>
</div>
<div class="card">
<h2 class="title"><?php _e( '新功能介绍', 'friendlinks' ); ?></h2>
<p><strong>版本 3.0.0 新增功能:</strong></p>
<ul>
<li>✓ 新增友链头像管理功能</li>
<li>✓ RSS文章列表支持显示友链头像</li>
<li>✓ 可设置头像显示位置(标题前/标题后)</li>
<li>✓ 可自定义头像尺寸</li>
<li>✓ 友情链接小工具也支持头像显示</li>
<li>✓ 头像获取优先级:自定义头像 > 链接图片 > 网站favicon > 默认头像</li>
</ul>
<p><?php _e( '要使用新功能,请在编辑友链时添加头像地址,并在小工具设置中启用头像显示。', 'friendlinks' ); ?></p>
</div>
<div class="card">
<h2 class="title"><?php _e( '调试信息', 'friendlinks' ); ?></h2>
<p><?php _e( '调试日志位置:', 'friendlinks' ); ?> <code>/wp-content/debug.log</code></p>
<p><?php _e( '日志中包含以 [FriendLinks RSS] 开头的详细抓取和配置信息。', 'friendlinks' ); ?></p>
<p>
<a href="<?php echo admin_url( 'site-health.php?tab=debug' ); ?>" class="button">
<?php _e( '查看站点健康状态', 'friendlinks' ); ?>
</a>
<button type="button" class="button button-secondary" onclick="clearFriendlinksCache()">
<?php _e( '清除RSS缓存', 'friendlinks' ); ?>
</button>
</p>
</div>
<script>
function clearFriendlinksCache() {
if (confirm('确定要清除友链RSS缓存吗?下次访问时将重新抓取数据。')) {
jQuery.post(ajaxurl, {
action: 'friendlinks_clear_cache',
_ajax_nonce: '<?php echo wp_create_nonce( "friendlinks_clear_cache" ); ?>'
}, function(response) {
if (response.success) {
alert('缓存已清除!');
location.reload();
} else {
alert('清除失败:' + response.data);
}
});
}
}
</script>
</div>
<?php
}
/**
* 10. 添加AJAX清理缓存功能
*/
add_action( 'wp_ajax_friendlinks_clear_cache', 'friendlinks_ajax_clear_cache' );
function friendlinks_ajax_clear_cache() {
if ( ! current_user_can( 'manage_options' ) || ! check_ajax_referer( 'friendlinks_clear_cache', false, false ) ) {
wp_die( '权限不足' );
}
delete_transient( 'friendlinks_rss_cache' );
update_option( 'friendlinks_last_fetch', 0 );
wp_send_json_success( '缓存已清除' );
}
/**
* 11. 创建默认头像和CSS文件
*/
function friendlinks_create_assets() {
// 创建CSS目录
$css_dir = plugin_dir_path( __FILE__ ) . 'css';
if ( ! file_exists( $css_dir ) ) {
wp_mkdir_p( $css_dir );
}
// 创建图片目录
$images_dir = plugin_dir_path( __FILE__ ) . 'images';
if ( ! file_exists( $images_dir ) ) {
wp_mkdir_p( $images_dir );
}
// 创建默认头像(如果不存在)
$default_avatar = $images_dir . '/default-avatar.png';
if ( ! file_exists( $default_avatar ) ) {
// 这里可以创建一个简单的默认头像
// 由于创建图像需要GD库,我们提供一个简单的方法:使用一个在线默认头像
// 实际使用时,你可以准备一个本地的默认头像图片
}
// 创建CSS文件
$css_file = $css_dir . '/friendlinks-styles.css';
$css_content = '/* 友链管理插件样式 - 版本 3.0.0 */
/* 友情链接小工具样式 */
.widget_friendlinks ul.friendlinks-list,
.widget_friendlinks_rss ul.friendlinks-rss-list {
list-style: none;
padding-left: 0;
margin: 0 0 15px 0;
}
.friendlinks-list li,
.friendlinks-rss-item {
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
line-height: 1.5;
display: flex;
align-items: center;
}
.friendlinks-list li:last-child,
.friendlinks-rss-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.friendlinks-list li a {
text-decoration: none;
color: #2c3e50;
font-weight: 500;
display: block;
padding: 2px 0;
transition: color 0.2s ease;
flex-grow: 1;
}
.friendlinks-list li a:hover {
color: #3498db;
}
.friendlinks-list li .link-description {
font-size: 12px;
color: #7f8c8d;
display: block;
margin-top: 2px;
}
/* 友链头像样式 */
.friendlinks-rss-avatar,
.friendlink-avatar {
border-radius: 3px;
border: 1px solid #eee;
object-fit: cover;
vertical-align: middle;
}
.friendlinks-rss-avatar.avatar-before {
margin-right: 8px;
float: left;
}
.friendlinks-rss-avatar.avatar-after {
margin-left: 8px;
float: right;
}
/* 带头像的列表项 */
.friendlinks-rss-list.with-avatars .friendlinks-rss-item {
min-height: 40px;
align-items: flex-start;
}
/* 友链最新文章样式 */
.friendlinks-rss-title {
text-decoration: none;
color: #2980b9;
font-weight: 500;
display: block;
margin-bottom: 4px;
transition: color 0.2s ease;
flex-grow: 1;
}
.friendlinks-rss-title:hover {
color: #e74c3c;
text-decoration: underline;
}
.friendlinks-rss-date {
font-size: 12px;
color: #7f8c8d;
display: block;
margin-top: 2px;
clear: both;
}
.friendlinks-rss-source {
font-size: 11px;
color: #95a5a6;
background: #f8f9fa;
padding: 2px 6px;
border-radius: 3px;
margin-left: 8px;
display: inline-block;
font-style: italic;
}
/* 无内容提示 */
.friendlinks-no-content {
text-align: center;
padding: 20px 10px;
color: #95a5a6;
font-style: italic;
border: 1px dashed #ecf0f1;
border-radius: 4px;
background: #fdfdfd;
}
.friendlinks-empty {
color: #95a5a6;
font-style: italic;
margin: 0;
padding: 10px;
text-align: center;
}
/* 抓取时间 */
.friendlinks-fetch-time {
font-size: 11px;
color: #bdc3c7;
text-align: right;
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #ecf0f1;
clear: both;
}
/* 小工具表单中的说明文字 */
.widget_friendlinks_rss small {
font-size: 11px;
color: #666;
display: block;
margin-top: 2px;
}
/* 管理页面样式 */
.wrap .card {
max-width: 800px;
margin-bottom: 20px;
padding: 20px;
background: #fff;
border: 1px solid #ccd0d4;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
/* 响应式调整 */
@media screen and (max-width: 768px) {
.friendlinks-rss-source {
display: block;
margin-left: 0;
margin-top: 4px;
}
.friendlinks-rss-date {
font-size: 11px;
}
.friendlinks-rss-avatar {
max-width: 24px;
max-height: 24px;
}
}
/* 调试信息(仅管理员可见) */
.friendlinks-debug-info {
display: none;
}
/* 友情链接小工具中的头像 */
.friendlinks-list.with-avatars li {
padding-left: 0;
}
.friendlinks-item-with-avatar {
display: flex;
align-items: center;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
}
.friendlinks-item-with-avatar:last-child {
border-bottom: none;
}
.friendlinks-item-with-avatar .friendlink-avatar {
margin-right: 10px;
flex-shrink: 0;
}
.friendlinks-item-with-avatar a {
flex-grow: 1;
}
.friendlinks-item-with-avatar .link-description {
display: block;
font-size: 12px;
color: #666;
margin-top: 2px;
}';
if ( ! file_exists( $css_file ) || filesize( $css_file ) < 100 ) {
file_put_contents( $css_file, $css_content );
}
}
// 激活时创建资源文件
register_activation_hook( __FILE__, 'friendlinks_create_assets' );
/**
* 12. 添加CSS样式
*/
function friendlinks_enqueue_styles() {
wp_register_style(
'friendlinks-styles',
plugin_dir_url( __FILE__ ) . 'css/friendlinks-styles.css',
array(),
'3.0.0'
);
// 只要小工具可能显示就加载样式
if ( is_active_widget( false, false, 'friendlinks_widget', true ) ||
is_active_widget( false, false, 'friendlinks_rss_widget', true ) ||
is_admin() ) {
wp_enqueue_style( 'friendlinks-styles' );
}
// 在管理页面加载jQuery
if ( is_admin() && isset( $_GET['page'] ) && $_GET['page'] === 'friendlinks-settings' ) {
wp_enqueue_script( 'jquery' );
}
}
add_action( 'wp_enqueue_scripts', 'friendlinks_enqueue_styles' );
add_action( 'admin_enqueue_scripts', 'friendlinks_enqueue_styles' );
/**
* 13. 清理计划任务(停用时)
*/
register_deactivation_hook( __FILE__, 'friendlinks_deactivate' );
function friendlinks_deactivate() {
$timestamp = wp_next_scheduled( 'friendlinks_fetch_rss' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'friendlinks_fetch_rss' );
}
// 删除缓存和选项
delete_transient( 'friendlinks_rss_cache' );
delete_option( 'friendlinks_last_fetch' );
delete_option( 'friendlinks_fetch_count' );
delete_option( 'friendlinks_last_error' );
}
/**
* 14. 加载文本域(国际化)
*/
function friendlinks_load_textdomain() {
load_plugin_textdomain( 'friendlinks', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
}
add_action( 'plugins_loaded', 'friendlinks_load_textdomain' );
步骤2:创建CSS文件夹和文件
- 在
friendlinks-manager文件夹中创建css子文件夹 - 在
css文件夹中创建friendlinks-styles.css文件 - 代码激活时会自动创建CSS内容,你也可以手动创建
- 代码2需要在插件目录下创建
css和images文件夹
步骤3:激活插件
- 登录WordPress后台
- 进入 插件 → 已安装的插件
- 找到 “友链管理与RSS订阅” 插件
- 点击 “启用”
步骤4:数据库表创建
插件激活时会自动创建所需的数据库表:
wp_links_meta(存储友链的RSS地址)- 在
wp_options表中添加抓取记录
步骤5:添加友情链接
- 进入 链接 → 添加
- 填写友链信息:
- 名称:友链网站名称
- Web地址:友链网站URL
- 描述:(可选)
- RSS地址:输入友链网站的RSS地址(例如:
https://example.com/feed/或https://example.com/rss/)
- 选择分类(可选)
- 点击 “添加链接”
如何获取友链RSS地址:
- 大多数WordPress网站:
https://域名.com/feed/ - 其他博客平台:通常在网站底部有RSS图标
- 可以尝试:
域名.com/rss或域名.com/atom.xml
步骤6:配置小工具
- 进入 外观 → 小工具
- 在可用小工具中找到:
- 友情链接:显示友链列表
- 友链最新文章:显示友链的最新文章
- 配置”友情链接”小工具: