侧栏挂件「文章聚合 · 热点」只显示 1 条(时间窗设成"最近30天发文",候选池只有 1 篇) #19

Closed
opened 2026-10-26 14:07:13 +00:00 by laoyang · 2 comments
Owner

现象

侧栏「文章聚合」挂件的热点标签页总是只显示 1 条。最新、随机两个标签页正常(各 6 条)。

线上实测(2026-10-26 抓 https://yangyq.net/ 首页):

<div class="tab-pane fade" id="nav-hot" role="tabpanel">
  <a class="bookmark-item" title="同步日历-我的第一款鸿蒙应用(鸿蒙CalDav客户端)"
     href="https://yangyq.net/2026/10/synccalendar.html" rel="bookmark">
     <i class="kicon i-book"></i>同步日历-我的第一款鸿蒙应用(鸿蒙CalDav客户端)</a>
</div>

最新页 6 条、随机页 6 条,只有热点页 1 条。

根因(已定位,不是排序问题)

inc/theme-widgets.php 的 most_comm_posts($days, $nums):

$today   = date("Y-m-d H:i:s");
$daysago = date("Y-m-d H:i:s", strtotime($today) - ($days * 24 * 60 * 60));
$result  = $wpdb->get_results(
    "SELECT comment_count, ID, post_title, post_date FROM $wpdb->posts
     WHERE post_date BETWEEN '$daysago' AND '$today'
       AND post_type='post' AND post_status='publish'
     ORDER BY comment_count DESC LIMIT 0, $nums");

挂件传入的默认 $days 是 30($instance['days'] 默认 '30')。
所以热门文章的候选池 = 最近 30 天内发布的文章,再在其中按评论数排序。

而这个站的真实发文节奏(服务器上查库,2026-10-26):

时间窗口 已发布文章数
7 天 0
30 天 1
90 天 1
180 天 6
365 天 15
3650 天 261
全站 967

30 天内只有 1 篇文章 ⇒ 候选池只有 1 条 ⇒ LIMIT 6 也只能返回 1 条。
排序、LIMIT、SQL 全都没错,错在「用发文时间当热门的时间窗」。

这个设计对日更站点成立,对低频更新的站点(本站在 2026 年只发了 15 篇)完全失效。

附带问题

most_comm_posts() 里有一句:

date_default_timezone_set("PRC");

这是直接改 PHP 全局时区,会影响同一次请求里之后所有时间函数。上面那段 $today / $daysago 本可以直接用 current_time() 拿 WP 时区,不需要动全局。

期望

  1. 热点要真的热:候选池不能只看「最近 N 天发的文章」,否则低频站永远只有一两条。
    本站明明有 727 篇带评论的文章、views 元数据累计 822 万次浏览,这些才是热的依据。
  2. 数量要够:默认展示 6 条时,热点就应给出 6 条(只有在全站相关文章本身就不足 6 篇时才更少)。
  3. 不要动全局时区。
  4. 后台「统计天数」这个设置项要重新定义清楚——如果保留,它应该是「多老的也算历史」之类有意义的语义,不能继续是「只有这个时间段内发的文章才有资格上榜」。

验收标准

  • 线上侧栏热点页在默认设置下显示 6 条(不是 1 条)。
  • 热点排序依据是真实热度,不是发文时间。
  • 同一请求内 date_default_timezone_set 不再被调用。
  • 反向验证:把候选池改回「最近 30 天发布」后,判据必须报红。

为什么单独建单

侧栏 widget_posts 热点标签页的候选池 bug。与 #18(归档页/评论区样式)、#17([Archives] 标签大小写)、#16(评论区语言)、#15(归档短代码)都不是同一件事:那些是归档与评论模块,这是侧栏挂件里 most_comm_posts() 的时间窗逻辑,可独立验收。

## 现象 侧栏「文章聚合」挂件的**热点**标签页总是只显示 1 条。最新、随机两个标签页正常(各 6 条)。 线上实测(2026-10-26 抓 `https://yangyq.net/` 首页): ```html <div class="tab-pane fade" id="nav-hot" role="tabpanel"> <a class="bookmark-item" title="同步日历-我的第一款鸿蒙应用(鸿蒙CalDav客户端)" href="https://yangyq.net/2026/10/synccalendar.html" rel="bookmark"> <i class="kicon i-book"></i>同步日历-我的第一款鸿蒙应用(鸿蒙CalDav客户端)</a> </div> ``` 最新页 6 条、随机页 6 条,只有热点页 1 条。 ## 根因(已定位,不是排序问题) `inc/theme-widgets.php` 的 `most_comm_posts($days, $nums)`: ```php $today = date("Y-m-d H:i:s"); $daysago = date("Y-m-d H:i:s", strtotime($today) - ($days * 24 * 60 * 60)); $result = $wpdb->get_results( "SELECT comment_count, ID, post_title, post_date FROM $wpdb->posts WHERE post_date BETWEEN '$daysago' AND '$today' AND post_type='post' AND post_status='publish' ORDER BY comment_count DESC LIMIT 0, $nums"); ``` 挂件传入的默认 `$days` 是 **30**(`$instance['days']` 默认 `'30'`)。 所以热门文章的**候选池 = 最近 30 天内发布的文章**,再在其中按评论数排序。 而这个站的真实发文节奏(服务器上查库,2026-10-26): | 时间窗口 | 已发布文章数 | |---|---| | 7 天 | 0 | | **30 天** | **1** | | 90 天 | 1 | | 180 天 | 6 | | 365 天 | 15 | | 3650 天 | 261 | | 全站 | 967 | **30 天内只有 1 篇文章** ⇒ 候选池只有 1 条 ⇒ `LIMIT 6` 也只能返回 1 条。 排序、LIMIT、SQL 全都没错,错在「用发文时间当热门的时间窗」。 这个设计对日更站点成立,对低频更新的站点(本站在 2026 年只发了 15 篇)完全失效。 ## 附带问题 `most_comm_posts()` 里有一句: ```php date_default_timezone_set("PRC"); ``` 这是**直接改 PHP 全局时区**,会影响同一次请求里之后所有时间函数。上面那段 `$today` / `$daysago` 本可以直接用 `current_time()` 拿 WP 时区,不需要动全局。 ## 期望 1. **热点要真的热**:候选池不能只看「最近 N 天发的文章」,否则低频站永远只有一两条。 本站明明有 727 篇带评论的文章、`views` 元数据累计 822 万次浏览,这些才是热的依据。 2. **数量要够**:默认展示 6 条时,热点就应给出 6 条(只有在全站相关文章本身就不足 6 篇时才更少)。 3. **不要动全局时区**。 4. 后台「统计天数」这个设置项要重新定义清楚——如果保留,它应该是「多老的也算历史」之类有意义的语义,不能继续是「只有这个时间段内发的文章才有资格上榜」。 ## 验收标准 - 线上侧栏热点页在默认设置下显示 **6 条**(不是 1 条)。 - 热点排序依据是**真实热度**,不是发文时间。 - 同一请求内 `date_default_timezone_set` 不再被调用。 - 反向验证:把候选池改回「最近 30 天发布」后,判据必须报红。 --- ### 为什么单独建单 侧栏 widget_posts 热点标签页的候选池 bug。与 #18(归档页/评论区样式)、#17([Archives] 标签大小写)、#16(评论区语言)、#15(归档短代码)都不是同一件事:那些是归档与评论模块,这是侧栏挂件里 most_comm_posts() 的时间窗逻辑,可独立验收。
Author
Owner

已完成并发布 3.2.49

commit d3070be(inc/theme-widgets.php + style.css,192+/32-)
线上 update.json → 3.2.49,用户可在 WP 后台点「升级」。
服务器主题目录仍是 3.2.47,未被覆盖(按约定只走更新通道)。

根因:不是排序问题,是候选池

旧 SQL 先圈「最近 N 天发布」当候选池,再按评论数排序取 6。查真实库(2026-10-26):

窗口 已发布 post 数
最近 7 天 0
最近 30 天 1 ← 默认 $days=30,池子里就 1 条
最近 90 天 1
最近 180 天 6
最近 365 天 15
全站 967

LIMIT 6 在有 1 条候选时也只能返回 1 条。日更站这个写法成立,低频站必然失灵。

新算法(按你选的口径:全站历史最热,浏览/点赞/评论综合算分)

热度 = 3 × log10(浏览 + 1)
     + 5 × log10(点赞 + 1)
     + 4 × log10(评论 + 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,退化成「评论数排行榜」。提到 3 之后三个指标才势均力敌。

最终前 6 名(真实库):

# 标题 浏览 赞 评论 年
1 为多用户安装Anaconda及其使用方法 12959 40 5 2023
2 WordPress主题发布:kratos_Yang 10434 9 37 2022
3 LaTex中上划线\overline和\bar的使用说明 32460 19 3 2022
4 那一年,2009 56075 0 88 2009
5 LaTex中设置字体的正确方法 16539 12 8 2022
6 使用Windows7自带…播放RMVB影片 49411 0 93 2009

有意不加时间衰减:查到的 Hacker News (P-1)/(T+2)^1.8 和 Reddit log10(净赞)+时间/45000 都是「新内容榜」,带强衰减,会把老文章压到看不见——那正好又回到你说的这个问题。要时效榜属于另一个功能,没塞进来。

关于你提的「只看 post,不考虑 page」

担心的两个异常值核查过:ID 217「留言本」浏览 214 万、ID 522「博客时间」8.3 万,post_type 都是 page,SQL 里本来就有 post_type='post',天然挡掉,不需要额外处理。

顺手修掉的三处旧问题

  1. date_default_timezone_set("PRC") —— 会改掉整个 PHP 请求的全局时区,影响之后所有时间函数。已删。
  2. 标题裸拼进 title="..." —— 标题有双引号就截断属性。已改 esc_attr(),正文另用 esc_html()。
  3. get_posts('numberposts=6 & offset=0') 字符串里多了一个空格,WP 解析出的参数名是 ' offset'(前面带空格),offset=0 实际从没生效。已改数组传参。

后台

「统计天数」输入框下线(它控制的就是那个把候选池缩成一条的时间窗),换一行说明;update() 不再保存 days,老实例残留值下次保存时自动丢掉。

验证

  • 主测试 33/0:分数用独立实现重算后逐条比对(不调产品函数算期望值,避免自己测自己);含 SQL 里不再有时间窗、2009 年最热文能进结果、三条单调性、log10 压缩性质、时区未被改动等。
  • 反向验证 20/0(基线 2 + 框架自检 2 + 注入 8 组 × 2 条):8 组注入分别是时间窗加回 / 去 post_type / 去 log10 / 权重改回 1-3-5 / 标题退裸拼 / 全局时区加回 / 排序改升序 / 后台加回统计天数,每组都确认"改回 bug 后测试确实变红、且红的正是该红那条"。
  • 真浏览器几何 22/0(1440 + 1180 两视口各 11 条):6 条真画出来、每条宽度等于面板宽、图标存在、相邻不重叠、长标题走省略号不溢出。
  • 发布脚本反证 8/8、线上包下载后逐条校验 10/10。

过程中修掉的 4 个测试自身缺陷(产品没问题,是判据坏了)

  1. ④ 那条「双引号被转义」判据是假绿:标题同时出现在 title 属性和链接正文,把 esc_attr 摘掉后正文照样产出 &quot;,判据仍然通过。改成把属性值抠出来逐字比对。
  2. ⑤ 注入时把 $s = file_get_contents($p) 删掉了,explode(null) 直接炸。
  3. 反向验证的命中判定在整段输出里找关键词,而测试标签本身就含关键词 ⇒「红在别处」也会被判成「红在正确位置」。改成只在 FAIL 行里找。
  4. 发布脚本 R3 用 '${1} * ...' 做替换串(PHP preg_replace 语法),写进 bash 双引号后被 bash 当位置参数展开 ⇒ set -u 下 unbound variable 中断。另 R8 用「先跑 php 再看 $?」,中间夹了 cp ⇒ $? 永远是 cp 的状态,等于永远判成功。

工单先不关,等你升级 3.2.49 后确认侧栏「热点」真的显示 6 条再关。

## 已完成并发布 3.2.49 **commit** `d3070be`(`inc/theme-widgets.php` + `style.css`,192+/32-) **线上** `update.json` → 3.2.49,用户可在 WP 后台点「升级」。 服务器主题目录仍是 3.2.47,**未被覆盖**(按约定只走更新通道)。 ### 根因:不是排序问题,是候选池 旧 SQL 先圈「最近 N 天发布」当候选池,再按评论数排序取 6。查真实库(2026-10-26): | 窗口 | 已发布 post 数 | |---|---| | 最近 7 天 | 0 | | **最近 30 天** | **1** ← 默认 `$days=30`,池子里就 1 条 | | 最近 90 天 | 1 | | 最近 180 天 | 6 | | 最近 365 天 | 15 | | 全站 | 967 | `LIMIT 6` 在有 1 条候选时也只能返回 1 条。日更站这个写法成立,低频站必然失灵。 ### 新算法(按你选的口径:全站历史最热,浏览/点赞/评论综合算分) ``` 热度 = 3 × log10(浏览 + 1) + 5 × log10(点赞 + 1) + 4 × log10(评论 + 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`,退化成「评论数排行榜」。提到 3 之后三个指标才势均力敌。 最终前 6 名(真实库): | # | 标题 | 浏览 | 赞 | 评论 | 年 | |---|---|---|---|---|---| | 1 | 为多用户安装Anaconda及其使用方法 | 12959 | 40 | 5 | 2023 | | 2 | WordPress主题发布:kratos_Yang | 10434 | 9 | 37 | 2022 | | 3 | LaTex中上划线\overline和\bar的使用说明 | 32460 | 19 | 3 | 2022 | | 4 | 那一年,2009 | 56075 | 0 | 88 | 2009 | | 5 | LaTex中设置字体的正确方法 | 16539 | 12 | 8 | 2022 | | 6 | 使用Windows7自带…播放RMVB影片 | 49411 | 0 | 93 | 2009 | **有意不加时间衰减**:查到的 Hacker News `(P-1)/(T+2)^1.8` 和 Reddit `log10(净赞)+时间/45000` 都是「新内容榜」,带强衰减,会把老文章压到看不见——那正好又回到你说的这个问题。要时效榜属于另一个功能,没塞进来。 ### 关于你提的「只看 post,不考虑 page」 担心的两个异常值核查过:ID 217「留言本」浏览 214 万、ID 522「博客时间」8.3 万,**`post_type` 都是 `page`**,SQL 里本来就有 `post_type='post'`,天然挡掉,不需要额外处理。 ### 顺手修掉的三处旧问题 1. `date_default_timezone_set("PRC")` —— 会改掉**整个 PHP 请求的全局时区**,影响之后所有时间函数。已删。 2. 标题裸拼进 `title="..."` —— 标题有双引号就截断属性。已改 `esc_attr()`,正文另用 `esc_html()`。 3. `get_posts('numberposts=6 & offset=0')` 字符串里**多了一个空格**,WP 解析出的参数名是 `' offset'`(前面带空格),`offset=0` 实际从没生效。已改数组传参。 ### 后台 「统计天数」输入框下线(它控制的就是那个把候选池缩成一条的时间窗),换一行说明;`update()` 不再保存 `days`,老实例残留值下次保存时自动丢掉。 ### 验证 - **主测试 33/0**:分数用**独立实现**重算后逐条比对(不调产品函数算期望值,避免自己测自己);含 SQL 里不再有时间窗、2009 年最热文能进结果、三条单调性、`log10` 压缩性质、时区未被改动等。 - **反向验证 20/0**(基线 2 + 框架自检 2 + 注入 8 组 × 2 条):8 组注入分别是时间窗加回 / 去 `post_type` / 去 `log10` / 权重改回 1-3-5 / 标题退裸拼 / 全局时区加回 / 排序改升序 / 后台加回统计天数,**每组都确认"改回 bug 后测试确实变红、且红的正是该红那条"**。 - **真浏览器几何 22/0**(1440 + 1180 两视口各 11 条):6 条真画出来、每条宽度等于面板宽、图标存在、相邻不重叠、长标题走省略号不溢出。 - **发布脚本反证 8/8**、线上包下载后逐条校验 10/10。 ### 过程中修掉的 4 个测试自身缺陷(产品没问题,是判据坏了) 1. ④ 那条「双引号被转义」判据**是假绿**:标题同时出现在 `title` 属性和链接正文,把 `esc_attr` 摘掉后正文照样产出 `&quot;`,判据仍然通过。改成把属性值抠出来**逐字比对**。 2. ⑤ 注入时把 `$s = file_get_contents($p)` 删掉了,`explode(null)` 直接炸。 3. 反向验证的命中判定在**整段输出**里找关键词,而测试标签本身就含关键词 ⇒「红在别处」也会被判成「红在正确位置」。改成只在 `FAIL` 行里找。 4. 发布脚本 R3 用 `'${1} * ...'` 做替换串(PHP `preg_replace` 语法),写进 bash 双引号后被 bash 当位置参数展开 ⇒ `set -u` 下 unbound variable 中断。另 R8 用「先跑 php 再看 `$?`」,中间夹了 `cp` ⇒ `$?` 永远是 `cp` 的状态,等于永远判成功。 工单先不关,等你升级 3.2.49 后确认侧栏「热点」真的显示 6 条再关。
Author
Owner

按用户确认收尾,交付见上条发布评论。

交付版本:3.2.49(热点换成全站累计热度算法,不再受时间窗候选池限制)

用户已验收,关闭本单。

按用户确认收尾,交付见上条发布评论。 交付版本:3.2.49(热点换成全站累计热度算法,不再受时间窗候选池限制) 用户已验收,关闭本单。
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: laoyang/wordpress-kratos-yang#19