* @license MIT License * @version 2020.08.03 */ // 添加小工具 function widgets_init() { register_sidebar(array( 'name' => __('侧边栏工具', 'kratos'), 'id' => 'sidebar_tool', 'before_widget' => '
', 'after_widget' => '
', 'before_title' => '
', 'after_title' => '
', )); // 去掉默认小工具 $wp_widget = array( 'WP_Widget_Pages', 'WP_Widget_Meta', 'WP_Widget_Recent_Posts', 'WP_Widget_Recent_Comments', 'WP_Widget_RSS', 'WP_Widget_Search', 'WP_Widget_Tag_Cloud', 'WP_Nav_Menu_Widget', ); foreach ($wp_widget as $wp_widget) { unregister_widget($wp_widget); } } add_action('widgets_init', 'widgets_init'); /* ========================================================================== * 热度算法(3.2.49) * * ── 为什么废弃旧的 most_comm_posts() ────────────────────────────────── * 旧实现的候选池是「最近 N 天(默认 30)**发布**的文章」: * * WHERE post_date BETWEEN '$daysago' AND '$today' * ORDER BY comment_count DESC LIMIT 6 * * 对日更站点没问题,对低频更新的站点直接失效。本站实测(2026-10-26): * 7 天内发布 0 篇 / 30 天内 1 篇 / 90 天内 1 篇 / 180 天内 6 篇 * 30 天候选池只有 1 篇 ⇒ LIMIT 6 也只能返回 1 条。 * 这就是侧栏「热点」永远只显示一条的真正原因 —— **不是排序错,是池子空**。 * * 而且「最近 30 天发布」本身就不是「热」的定义:热是累计互动,不是新鲜度。 * * ── 现在的算法 ──────────────────────────────────────────────────────── * 候选池:全站所有已发布 post(不含 page —— 「留言本」这种页面 views 214 万, * 它的热度对"最热文章"没有意义;`post_type='post'` 天然挡掉)。 * 打分:三个指标各取 log10 后加权求和 * * score = 3*log10(views+1) + 5*log10(love+1) + 4*log10(comments+1) * * 为什么取 log10:本站 views 中位数 4981、最大 56075(差 11 倍), * 但 love 最大只有 40、评论最大 93。不取对数的话 views 一个指标就吃掉 * 全部权重,点赞和评论等于没参与。取对数后三者量级被拉平到可比的区间。 * * 为什么权重是 3 / 5 / 4(不是常见的 1 / 3 / 5): * 用通用值实测,前 8 名有 6 篇是 2009 年的老随笔 —— 因为那批文章评论 * 80~93 条,仅评论一项(1.97×5=9.86)就压过 views 最大值的贡献(4.75×1=4.75), * 结果变成"评论数排行榜"。把 views 权重提到 3 之后三个指标影响力均衡: * 点赞最高(5)—— 点👍 是比评论更强的认可信号,且本站只有 197 篇有点赞, * 稀缺性让它更有区分度;评论次之(4)—— 要付出打字成本;浏览最轻(3)。 * * 实测这组权重的排名(前 8):Anaconda多用户安装(love=40) · kratos主题发布(love=9,评论37) * · LaTex上划线(love=19) · 那一年2009 · LaTex字体(love=12) · Windows7自带WMP(评论93) * → 技术文章与高互动随笔混合出现,2009 年那批也没有被完全埋掉。 * * ── 有意不加时间衰减 ────────────────────────────────────────────────── * 「全站历史最热」是累计榜,不是时效榜。Hacker News / Reddit 那套 * gravity 衰减是给「新内容榜」用的,加上去会把老文章压到看不见, * 那就又回到"只有最近几条"的老问题。要时效榜是另一个功能,不塞在这里。 * * ── 顺手修掉的两个坑 ────────────────────────────────────────────────── * ① 旧代码里 `date_default_timezone_set("PRC")` 直接改 **PHP 全局时区**, * 影响同一请求里之后所有时间函数。已删。 * ② 旧代码把 `$title` 原样插进 `title="..."` 属性,标题里有双引号就会 * 截断属性(甚至注入)。已改用 esc_attr()。 * ====================================================================== */ if (!function_exists('yang_hot_score')): /** * 单篇文章的热度分。 * * @param int $views 浏览数 * @param int $love 点赞数 * @param int $comments 评论数 * @return float */ function yang_hot_score($views, $love, $comments) { return 3 * log10((int) $views + 1) + 5 * log10((int) $love + 1) + 4 * log10((int) $comments + 1); } endif; if (!function_exists('yang_hot_posts')): /** * 取热度最高的文章。 * * 为什么在 PHP 里算分、不在 SQL 里 ORDER BY: * 分值是三项 log 加权和,SQL 里要写三段 LOG10 + COALESCE + 两个 * LEFT JOIN 到 postmeta,可读性极差,而且没法复用 yang_hot_score() * (测试和产品必须是同一份实现,不能各写一遍)。 * 代价是要把候选行取回来 —— 本站 967 篇 post,一次取 4 个字段 * 内存占用可忽略;如果将来文章上万,再加 LIMIT / 缓存。 * 为防止极端情况,SQL 侧先用 views 粗排取前 N*10(对数排序里 * 真正的高分不会来自 views 极低的文章,这个粗排是安全的)。 * * @param int $nums 返回条数 * @return array 文章对象数组,每项含 ID/post_title/views/love/comments/score */ function yang_hot_posts($nums = 6) { global $wpdb; $nums = max(1, (int) $nums); // 粗排窗口:给足余量。views 前 200 名以外不可能靠 love/评论翻盘 // (log10 域下,love 满分 1.61×5=8.05,补不回 views 差 1 个数量级的 3 分)。 $pool = max(200, $nums * 20); $sql = $wpdb->prepare( "SELECT p.ID, p.post_title, p.post_date, p.comment_count, COALESCE(CAST(mv.meta_value AS UNSIGNED), 0) AS views, COALESCE(CAST(ml.meta_value AS UNSIGNED), 0) AS love FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} mv ON mv.post_id = p.ID AND mv.meta_key = 'views' LEFT JOIN {$wpdb->postmeta} ml ON ml.post_id = p.ID AND ml.meta_key = 'love' WHERE p.post_status = 'publish' AND p.post_type = 'post' ORDER BY views DESC LIMIT %d", $pool ); $rows = $wpdb->get_results($sql); if (empty($rows)) { return array(); } $scored = array(); foreach ($rows as $r) { $r->views = (int) $r->views; $r->love = (int) $r->love; $r->comments = (int) $r->comment_count; $r->score = yang_hot_score($r->views, $r->love, $r->comments); $scored[] = $r; } usort($scored, function ($a, $b) { if ($a->score == $b->score) { // 同分按浏览数兜底,保证顺序稳定(否则每次刷新顺序会跳) return $b->views <=> $a->views; } return $b->score <=> $a->score; }); return array_slice($scored, 0, $nums); } endif; /** * 小工具文章聚合 —— 热点文章 * * ⚠️ 这个函数名和签名保留着,因为 `widget_posts::widget()` 里有一句 * `if (function_exists('most_comm_posts'))` 调用它。留着旧名不破兼容, * 但内部已经完全换成 yang_hot_posts()。 * * @param int $days 已废弃。旧实现用它当"最近 N 天发布"的时间窗, * 那正是只显示一条的根因。保留参数只为不破坏调用签名。 * @param int $nums 返回条数 */ function most_comm_posts($days = 30, $nums = 6) { $posts = yang_hot_posts($nums); $output = ''; foreach ($posts as $p) { $output .= '' . '' . esc_html(wp_strip_all_tags($p->post_title)) . ''; } echo $output; } class widget_search extends WP_Widget { public function __construct() { $widget_ops = array( 'classname' => 'widget_search', 'description' => __( 'A search form for your site.' ), 'customize_selective_refresh' => true, ); parent::__construct( 'search', _x( 'Search', 'Search widget' ), $widget_ops ); } public function widget( $args, $instance ) { $title = ! empty( $instance['title'] ) ? $instance['title'] : ''; $title = apply_filters( 'widget_title', $title, $instance, $this->id_base ); echo ''; } public function form( $instance ) { $instance = wp_parse_args( (array) $instance, array( 'title' => '' ) ); $title = $instance['title']; ?>

'' ) ); $instance['title'] = sanitize_text_field( $new_instance['title'] ); return $instance; } } class widget_ad extends WP_Widget { public function __construct() { add_action('admin_enqueue_scripts', array($this, 'scripts')); $widget_ops = array( 'name' => __('图片广告', 'kratos'), 'description' => __('显示自定义图片广告的工具', 'kratos'), ); parent::__construct(false, false, $widget_ops); } public function scripts() { wp_enqueue_script('media-upload'); wp_enqueue_media(); wp_enqueue_script('widget_scripts', ASSET_PATH . '/assets/js/widget.min.js', array('jquery')); wp_enqueue_style('widget_css', ASSET_PATH . '/assets/css/widget.min.css', array()); } public function widget($args, $instance) { $subtitle = !empty($instance['subtitle']) ? $instance['subtitle'] : __('广告', 'kratos'); $image = !empty($instance['image']) ? $instance['image'] : ''; $url = !empty($instance['url']) ? $instance['url'] : ''; echo '
'; echo '
' . $subtitle . '
'; echo '
'; } public function update($new_instance, $old_instance) { $instance = array(); $instance['subtitle'] = (!empty($new_instance['subtitle'])) ? $new_instance['subtitle'] : ''; $instance['image'] = (!empty($new_instance['image'])) ? $new_instance['image'] : ''; $instance['url'] = (!empty($new_instance['url'])) ? $new_instance['url'] : ''; return $instance; } public function form($instance) { $subtitle = !empty($instance['subtitle']) ? $instance['subtitle'] : __('广告', 'kratos'); $image = !empty($instance['image']) ? $instance['image'] : ''; $url = !empty($instance['url']) ? $instance['url'] : ''; ?>

__('个人简介', 'kratos'), 'description' => __('可跳转后台的个人简介展示工具', 'kratos'), ); parent::__construct(false, false, $widget_ops); } public function scripts() { wp_enqueue_script('media-upload'); wp_enqueue_media(); wp_enqueue_script('widget_scripts', ASSET_PATH . '/assets/js/widget.min.js', array('jquery')); wp_enqueue_style('widget_css', ASSET_PATH . '/assets/css/widget.min.css', array()); } public function widget($args, $instance) { $introduce = kratos_option('a_about', __('保持饥渴的专注,追求最佳的品质', 'kratos')); $username = kratos_option('a_nickname', __('Kratos', 'kratos')); $avatar = yang_option_image('a_gravatar'); $background = !empty($instance['background']) ? $instance['background'] : ASSET_PATH . '/assets/img/about-background.png'; echo '
'; echo '
'; if (current_user_can('manage_options')) { echo ''; } else { echo ''; } echo ''; echo '

'. $username .'

' . $introduce . '

'; echo '
'; } public function update($new_instance, $old_instance) { $instance = array(); $instance['background'] = (!empty($new_instance['background'])) ? $new_instance['background'] : ''; return $instance; } public function form($instance) { $background = !empty($instance['background']) ? $instance['background'] : ASSET_PATH . '/assets/img/about-background.png'; ?>

__('标签聚合', 'kratos'), 'description' => __('文章标签的展示工具', 'kratos'), ); parent::__construct(false, false, $widget_ops); } public function widget($args, $instance) { $number = !empty($instance['number']) ? $instance['number'] : '8'; $order = !empty($instance['order']) ? $instance['order'] : 'RAND'; $tags = wp_tag_cloud(array( 'unit' => 'px', 'smallest' => 14, 'largest' => 14, 'number' => $number, 'format' => 'flat', 'orderby' => 'count', 'order' => $order, 'echo' => false, ) ); echo '
'; echo '
' . __('标签聚合', 'kratos') . '
'; echo '
' . $tags . '
'; echo '
'; } public function update($new_instance, $old_instance) { $instance = array(); $instance['number'] = (!empty($new_instance['number'])) ? $new_instance['number'] : ''; $instance['order'] = (!empty($new_instance['order'])) ? $new_instance['order'] : ''; return $instance; } public function form($instance) { global $wpdb; $number = !empty($instance['number']) ? $instance['number'] : '8'; $order = !empty($instance['order']) ? $instance['order'] : 'RAND'; ?>

__('文章聚合', 'kratos'), 'description' => __('展示最热、随机、最新文章的工具', 'kratos'), ); parent::__construct(false, false, $widget_ops); } public function widget($args, $instance) { $number = !empty($instance['number']) ? (int) $instance['number'] : 6; if ($number < 1) { $number = 6; } echo '