[开发]论坛热榜插件开发方案(`hot_topics` · 右栏面板 + 独立页 `/hot`)

👑Lv.11 元老 🌞 骨干会员
2026-10-26 01:54:24

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/docs/热榜插件开发方案_2026-09-27.md。

论坛热榜插件开发方案(hot_topics · 右栏面板 + 独立页 /hot)

日期:2026-09-27 | 依据:docs/插件开发规范.md(§9 钩子全表 / §11 红线 / §十三 查询预算)

状态:待确认(本文只出方案,未创建任何 plugins/hot_topics/ 文件)

参照实现:plugins/leaderboard(同类:右栏面板 + 纯 CSS tab + TTL 懒刷新,已上线)

⚠️ 插件目录名 hot_topics 含下划线 → 触发规范 §4.1 硬坑(见 §六·风险 1),方案已内置处置


一、结论摘要

在右栏「站点统计」面板正上方新增一个 .mn-panel 面板(默认展示「近 24h · 回复榜」TOP 5 + 「查看完整热榜 →」),

并提供一个独立页 /hot(?range=24h|3d|7d × ?metric=replies|views,共 6 组)。

关键结论(全部经实测,非推断):

  1. 「放在站点统计上面」无需改核心 —— 核心已有现成钩子 right_sidebar_after(right_sidebar.php:211),

它的位置就在「站点统计」(:214–241)之前。→ 零核心改动即可精准落位。

  1. 规格里的数据源字段名几乎全错,必须纠正(见 §2.2):

created_at→create_time、last_reply_at→last_reply_time、forum_id→category_id,

且两个时间字段是 Unix 整数秒,不是 DATETIME 字符串。

  1. 权限过滤与全局缓存是天然冲突的 —— 榜单排序依赖「当前用户可见版块」,若把过滤后的榜存进全局缓存,

游客会看到私密版块标题。→ 采用「全局候选池(不按版块过滤)+ 每请求内存过滤」(见 §3.2,本方案最核心的设计点)。

  1. view_count / reply_count 上没有索引 → 纯按这两列排序会全表扫 + TEMP B-TREE。

本机 86 行量级实测 < 1ms,可接受;但方案按「候选池 + 缓存」把该扫描收敛到 TTL 刷新路径(5 分钟一次),不进每页路径。

  1. 不建表、不上 cron —— 文件缓存(data/hot_cache.json)+ route_after_dispatch TTL 懒刷新(照抄 leaderboard 范式)。

需先拍板的点:6 个(规格给的 4 个 + 我新增 2 个),见 §四。


二、现状核查(实测,非推断)

2.1 右栏结构 —— 「站点统计上面」的确切落位

app/Views/_components/right_sidebar.php(275 行)实测渲染顺序:

序区块行号显示条件
1发帖人信息栏 + 主题卡片 或 用户面板41–207详情页 / 其他页
2🔌 钩子 right_sidebar_after211无条件
3站点统计面板214–241$showSiteStats = 首页 / /forum / /forum/category/ / /blog
4热门标签面板244–259有 tags 且非 /tags 页
5🔌 钩子 sidebar_above_friend_links263无条件
6友情链接面板265–274上述页面 + friend_links 已启用

→ 「站点统计正上方」= 钩子 right_sidebar_after(规范 §9.1 #14),现成、零核心改动。

  • ✅ 权威依据:规范 §9.1 #14「右栏用户面板之后。所有渲染右栏的页面都触发(含帖子/博客详情页)」;

且 §11.26 明确分工:「想往右栏加整块面板(带 .mn-panel 标题栏)用 #14」。

  • ⚠️ 触发范围比站点统计更宽:站点统计只在 $showSiteStats 的 4 类页面显示,而本钩子在所有带右栏的页面触发

(含帖子/博客详情页)→ 需拍板「全站显示」还是「复刻站点统计的可见域」(§四·决策 5)。

  • ⚠️ right_sidebar_after 只在 PC 右栏触发一次(移动端侧栏 mobile_sidebar.php 里没有这个钩子,只有

sidebar_above_friend_links + home_sidebar_bottom)→ 无需「双触发」防护;但也意味着移动端看不到本面板

(移动端侧栏本来就没有站点统计,口径一致)→ 真正的入口靠 nav_plugin_links(§3.4)。

  • ⚠️ 右栏在 @media (max-width:1024px) 整块 display:none。

2.2 数据源实测 —— topic_index 真实结构(纠正规格的错误假设)

app/SplitDB/Schema.php:780 的真实 DDL:

CREATE TABLE IF NOT EXISTS topic_index (
    id INTEGER PRIMARY KEY,
    uid INTEGER,
    title TEXT,
    create_time INTEGER,        -- ⚠️ 不是 created_at;Unix 整数秒
    last_reply_time INTEGER,    -- ⚠️ 不是 last_reply_at;Unix 整数秒
    view_count INTEGER DEFAULT 0,
    reply_count INTEGER DEFAULT 0,
    status INTEGER DEFAULT 0,
    category_id INTEGER DEFAULT 0,   -- ⚠️ 不是 forum_id
    is_pinned INTEGER DEFAULT 0,
    is_highlighted INTEGER DEFAULT 0,
    color TEXT DEFAULT '',
    reply_to_view INTEGER DEFAULT 0,
    deleted_at INTEGER,
    bucket_path TEXT,
    is_archived INTEGER DEFAULT 0,
    excerpt TEXT DEFAULT '',
    excerpt_images TEXT DEFAULT ''
);

规格假设 → 实际字段对照表:

规格里写的实际字段说明
created_atcreate_timeUnix 整数秒
last_reply_atlast_reply_timeUnix 整数秒
forum_idcategory_id版块 ID
(未提)status0 = 正常;1 = 在回收站(Thread::STATUS_NORMAL = 0)
(未提)deleted_atIS NULL = 未软删

现有索引(Schema.php:812–822):

idx_topic_index_archive(is_archived, create_time)
idx_topic_index_list(is_pinned DESC, last_reply_time DESC, create_time DESC)
idx_topic_index_create_time(create_time)
idx_topic_index_category(category_id)
idx_topic_index_uid(uid)

⚠️ view_count / reply_count 上没有任何索引 → ORDER BY reply_count DESC 会走全表扫 + TEMP B-TREE。

本机 topic_index 86 行(83 行有效),实测该排序约 < 1ms,可接受。

方案把该扫描收敛到「每 5 分钟一次的 TTL 刷新路径」,不进每页渲染路径(§3.2)。

读取入口(必须走这条,不能裸 PDO 直读文件):

  • \app\SplitDB\Schema::mainIndexDb() → 返回 \PDO(有 prepare()),库文件 data/meta/main_index.sqlite(WAL)。
  • ⚠️ app\Core\Database 没有 fetchColumn() / prepare() → 误用会 fatal 且伪装成全 PASS(MEMORY §1)。
  • ⚠️ main_index.sqlite 是 WAL → 单拷不带 -wal 读不到表且不报错。

权限过滤接口(实测签名):

\app\Helpers\Permission::getAuthorizedCategoryIds(?int $userId = null): array
// 返回 int[] 版块 ID;管理员组(group_id=4)返回全部;
// 游客(userId=null → group_id=0)自动回退用 group=1 的权限;
// 内部走请求级缓存 permTable(),一请求最多 1 次查询。

2.3 可复用范式:plugins/leaderboard(已上线的同类插件)

维度leaderboard 做法本插件是否照抄
右栏挂载sidebar_above_friend_links(站点统计下方)❌ 改用 right_sidebar_after(上方)
缓存插件独立库 lb_cache 表 + TTL 300s⚠️ 改「文件缓存」(规格要求不建表)
懒刷新route_after_dispatch + $GLOBALS['lb_need_refresh'] 收敛✅ 完全照抄
冷启动渲染路径同步算一次,避免首屏空面板✅ 照抄
CSS 注入layout_head_end(不做模板白名单)✅ 照抄(+ 独立页另用 section('css'))
tab纯 CSS radio + :checked ~(零 JS)✅ 照抄
图标字面量 <i class="fa mn-fs-12">&#xf091;</i>✅ 照抄(不用 $view->icon(),其带内联 style)
管理员判定不涉及本插件也不涉及(不做排除)

2.4 设置存储机制(system:settings 权限对应什么)

\app\Helpers\Settings::get(string $key, string $default = ''): string
\app\Helpers\Settings::update(string $key, string $value, string $pluginName = ''): void
// 第三个参数传插件目录名做命名空间隔离;update() 需要 system:settings 权限

→ 与规格要求的 permissions = ["route:admin", "system:settings"] 完全对应(参照 plugins/suiyu)。

2.5 路由注册机制(独立页 /hot 怎么加)

// plugins/hot_topics/hook/route_register.php
if (!isset($router)) { return; }
// ⚠️ route_register 在 $router->group('/admin') 之外触发 → 路径必须写全(/hot)
$router->get('/hot', ['\\Plugin\\HotTopics\\FrontController', 'index']);

  • 触发点:index.php:295,$router 由核心注入(照抄 plugins/suiyu/hook/route_register.php)。
  • ⚠️ 前缀口径两相反:route_register(前台)路径写全;admin_route_register(后台,在 group 内)路径不写 /admin。
  • 导航入口:nav_plugin_links(双触发:PC 左栏「应用」+ 顶栏「应用」下拉)→ 钩子文件禁声明顶层符号。

三、技术设计

3.1 数据口径

候选池 SQL(缓存构建时执行,不按版块过滤 → 全局池):

-- metric = replies → 排序列 reply_count;metric = views → view_count
-- cutoff = time() - {24h:86400 | 3d:259200 | 7d:604800}
SELECT id, uid, title, category_id, view_count, reply_count, last_reply_time, create_time
FROM topic_index
WHERE status = 0
  AND deleted_at IS NULL
  AND last_reply_time >= :cutoff
ORDER BY {col} DESC, last_reply_time DESC, id DESC
LIMIT :pool

  • :pool = 候选池容量(默认 60,见 §四·决策 6)。
  • 6 组组合(3 range × 2 metric)→ 刷新时执行 6 次该查询。
  • ⚠️ range 的窗口字段语义(last_reply_time vs create_time)是必须拍板的点 → §四·决策 4。
  • ⚠️ 不排除 is_pinned(置顶帖回复数低,天然排不上去);不额外过滤 is_archived

(核心 Thread::getLatestAllowed() 也没过滤,且时间窗口已天然排除旧帖)→ 与核心口径一致。

每请求过滤(内存,0 额外查询):

$allowed = \app\Helpers\Permission::getAuthorizedCategoryIds($uid ?: null); // int[]
$visible = array_filter($pool, fn($r) => in_array((int)$r['category_id'], $allowed, true));
$list    = array_slice($visible, 0, $n);

作者名水合(仅 /hot 页,1 次查询):\app\Models\User::batchGetUsers($uids)(规范 §11.8.3:禁止插件直查 users 表)。

右栏面板不需要作者名/头像(极简风)→ 面板 0 次用户查询。

3.2 缓存设计(★ 本方案最核心:全局池 + 请求内过滤)

为什么不能直接缓存「过滤后的榜单」:

榜单排序依赖当前用户可见版块。若把过滤后的结果写进全局缓存文件,则游客会看到私密版块的帖子标题(越权泄露)。

反之若按「用户权限签名」分桶缓存,则缓存份数爆炸、且游客/多用户组命中率低。

正解 = 两层:

① 全局候选池(缓存文件,不含版块过滤,权限无关)
     └─ 每 5 分钟刷新一次,6 组组合 × 60 条
② 每请求:读池 → 按 getAuthorizedCategoryIds() 内存过滤 → 取前 N
     └─ 0 次数据库查询(除权限表本身的请求级缓存)

缓存文件结构 plugins/hot_topics/data/hot_cache.json:

{
  "v": 1,
  "updated_at": 1758960000,
  "pools": {
    "24h": {
      "replies": [
        {"id":123,"uid":1,"title":"…","category_id":2,"view_count":88,"reply_count":12,"last_reply_time":1758959000}
      ],
      "views": [ … ]
    },
    "3d":  { "replies": [ … ], "views": [ … ] },
    "7d":  { "replies": [ … ], "views": [ … ] }
  }
}

  • ⚠️ 缓存文件含未过滤的标题 → 必须由 data/.htaccess + data/web.config 禁止 HTTP 直接访问

(照抄 plugins/site_stats/data/ 的两个保护文件,实测内容已核对)。

  • ⚠️ 缓存只存 category_id,不存 category_name;分类名在渲染时由 (new Category())->allOrdered() 映射

(app/Models/Category.php:50,1 次查询,且该方法是列表页常规调用)。→ 分类改名即时生效。

请求路径(照抄 leaderboard 的收敛机制):

右栏钩子 right_sidebar_after(每页,PC 右栏一次)
  └─ file_get_contents(data/hot_cache.json)            ← 1 次文件读,0 次 DB
       ├─ 文件缺失 / 版本不符 → 同步算一次(冷启动,避免首屏空面板)
       ├─ 未过期 → 直接用
       └─ 已过期 → 仍用旧数据(不阻塞),置 $GLOBALS['hot_need_refresh'] = true

route_after_dispatch(响应已产出,index.php:321)
  └─ if (!empty($GLOBALS['hot_need_refresh'])) → refreshCache()   ← 6 聚合 + 1 次文件写

  • 正常请求 DB 查询 = 0(文件缓存)+ 0~1(权限表请求级缓存) ✅ 远低于规范 §十三「≤ 5 次/请求」红线。
  • 过期请求的 6 次查询发生在响应产出之后,不占用户等待。
  • TTL 默认 300s(与生产云函数 5 分钟 cron_trigger.php 节奏一致)。
  • 不上 cron:完全靠「有人访问 → 懒刷新」,无命令行依赖(生产无命令行,MEMORY §1)。

3.3 UI 结构

右栏面板(固定 24h + replies,零 tab、零 JS):

论坛热榜

… ×5
查看完整热榜 →

/hot 页(6 组切换用普通链接,全页刷新,URL 可分享/可缓存):

… 论坛热榜

  1. 1 标题 版块 · 作者 · 12 回复 · 88 浏览 · 3 小时前
  2. …

视觉一致性要点:

  • 外层 / 标题 / 列表 / 条目 / 数值全部复用核心类(.mn-panel / .mn-panel-title / .mn-stat-list / .mn-stat-item /

.mn-stat-label / .mn-stat-value)→ 与站点统计面板像素级一致。

  • 插件自建类一律带 .hot-topics- 前缀并提权(如 .hot-topics-panel .hot-topics-rank),

只写核心没定义的属性(规范 §5 / MEMORY §4:插件 CSS 在 main.php:40 注入、早于核心 editor.css,同权重核心后胜)。

  • 颜色**只用既有 --mn-***(写前逐条 grep 核对;--mn-danger / --mn-spectrum-* 已知不存在)。
  • 图标字面量 <i class="fa mn-fs-12">&#xf06d;</i>(fa-fire)——不用 $view->icon()(其返回值带内联 style,违反零内联)。
  • 空榜 → 渲染一行 muted 文案(i18n 键),不整块隐藏(面板已存在时隐藏会显得坏掉);但「一条数据都没有」时

按规范 §8「无数据不渲染」整块 return。

3.4 路由、钩子与文件清单

钩子清单(6 个):

#钩子触发点作用关键红线
1right_sidebar_afterright_sidebar.php:211右栏面板(站点统计上方)$this 不可用;$GLOBALS['__view'] 取模板
2route_registerindex.php:295注册 /hot路径写全;if (!isset($router)) return;
3layout_head_endmain.php:40注入 style.css带 ?v=filemtime;不做模板白名单(面板全站可见)
4nav_plugin_linksleft_sidebar.php:100 + main.php:218「应用」入口双触发 → 禁声明顶层符号;图标 16px;守卫 __nav_uri
5route_after_dispatchindex.php:321TTL 懒刷新必须有触发源收敛($GLOBALS 标志)
6admin_route_registerindex.php:276后台设置路由在 /admin group 内 → 路径不写 /admin;需 route:admin

目录结构:

plugins/hot_topics/
├── plugin.json                       # hooks(6) + permissions["route:admin","system:settings"]
├── Plugin.php                        # 常量 / 缓存读写 / 6 组聚合 / refreshCache() / getList()
│                                     #   ⚠️ 文件末尾 class_alias(目录名含下划线,见 §六·风险 1)
├── FrontController.php               # GET /hot → 整页视图
├── AdminController.php               # 后台设置页(GET/POST,PRG)
├── hook/
│   ├── right_sidebar_after.php       # 右栏面板
│   ├── route_register.php            # /hot
│   ├── layout_head_end.php           # CSS
│   ├── nav_plugin_links.php          # 「应用」入口(双触发)
│   ├── route_after_dispatch.php      # TTL 懒刷新
│   └── admin_route_register.php      # 后台路由
├── views/
│   ├── _panel.php                    # 右栏面板片段(viewRaw / 无 link+script)
│   ├── index.php                     # /hot 整页
│   └── admin/settings.php            # 后台设置
├── assets/style.css                  # 全部样式(零内联)
├── lang/{zh,en,zh_tw}.php            # 键前缀 plugin.hot_topics.*(三语键集合零差异)
└── data/
    ├── .htaccess                     # 禁 HTTP 访问(照抄 site_stats)
    ├── web.config                    # IIS 禁 data 段(照抄 site_stats)
    └── hot_cache.json                # 运行时生成(不进发行包)

路由:

// 前台(route_register,路径写全)
$router->get('/hot', ['\\Plugin\\HotTopics\\FrontController', 'index']);
// 后台(admin_route_register,在 group 内,不写 /admin)
$router->get('/hot-topics/settings',      ['\\Plugin\\HotTopics\\AdminController', 'settings']);
$router->post('/hot-topics/settings/save',['\\Plugin\\HotTopics\\AdminController', 'settingsSave']);

3.5 i18n 键(初版,三语同步)

plugin.hot_topics.nav                 热榜
plugin.hot_topics.title               论坛热榜
plugin.hot_topics.more                查看完整热榜
plugin.hot_topics.range_24h           近 24 小时
plugin.hot_topics.range_3d            近 3 天
plugin.hot_topics.range_7d            近 7 天
plugin.hot_topics.metric_replies      回复榜
plugin.hot_topics.metric_views        浏览榜
plugin.hot_topics.col_replies         回复
plugin.hot_topics.col_views           浏览
plugin.hot_topics.empty               暂无数据
plugin.hot_topics.admin_title         热榜设置
plugin.hot_topics.admin_top_n         面板显示条数
plugin.hot_topics.admin_pool          候选池容量
plugin.hot_topics.admin_ttl           缓存有效期(秒)
plugin.hot_topics.admin_default_range 默认时间范围
plugin.hot_topics.admin_default_metric默认榜单
plugin.hot_topics.admin_panel_on      显示右栏面板
plugin.hot_topics.admin_saved         已保存

⚠️ 验收标准 = 三语键集合零差异(不是「值非空」),含 plugins/*/lang/ 全量比对(MEMORY §6)。

⚠️ I18n::get() 缺键 return $key;(永不白屏)→ 改视图后必跑全量比对。

3.6 后台设置页

设置项键默认说明
面板显示条数top_n5右栏 TOP N
候选池容量pool60过滤前候选数(须 ≥ top_n 数倍,避免过滤后不足)
缓存有效期ttl300秒
默认时间范围default_range24h/hot 无参时的默认
默认榜单default_metricreplies/hot 无参时的默认
显示右栏面板panel_on1关掉后右栏不渲染(/hot 仍在)

  • 存储走 Settings::update($key, $val, 'hot_topics')(需 system:settings)。
  • ⚠️⚠️ 保存时必须用 array_key_exists($key, $_POST) 判缺失即跳过,

禁止 (int)($_POST[$k] ?? 0) —— 后者在字段名对不上时会静默写最小值,

且「302 + 页面提示成功、DB 与 settings_cache.php 都不变、error.log 无记录」(MEMORY §7,症状欺骗性极强)。

勾选框(panel_on)另算:缺失 = 显式关闭。

  • ⚠️ 上线新设置项后必须清一次缓存(settings_cache.php 不会自动补新键,MEMORY §1)。

四、需拍板的 6 个点

决策 1:右栏面板固定「24h + 回复榜」,不做 tab

方案 A(**推荐**)方案 B
面板固定 24h + replies TOP 5 + 「查看完整热榜 →」面板内做 range/metric tab
交互零 tab、零 JS6 组 tab(纯 CSS radio 需 6 个 pane,面板高度翻倍)
复杂度极低中(窄栏塞 6 个 chip 会挤)

推荐 A。理由:右栏宽度有限(面板约 210–300px),6 组 tab 会把面板撑很高;「完整榜单」的浏览场景天然属于独立页 /hot;

且符合你「没有实际用途的控件就不要出现」的取向。

决策 2:/hot 页切换用普通链接(全页刷新),不做 AJAX

方案 A(**推荐**)方案 B
切换<a href="/hot?range=3d&metric=views"> 全页刷新XHR 局部替换(hx-swap 或 fetch)
降级不存在降级问题必须写降级(无 JS 时回退全页)
依赖零 JS需 JSON 端点 + JS(本项目有「点按钮没反应 = JS 运行时 TypeError」的历史坑,且 node --check/静态扫一律漏)
URL可分享、可被页面缓存状态在 JS 里,URL 与内容可能不一致
SEO每种组合都可被抓取单一 URL

推荐 A。理由:6 组组合用链接切换没有任何功能损失,却把整块 JS 风险与降级负担归零;

仓库内已有 leaderboard 的「零 JS 优先」先例。AJAX 可作为后期可选增强(不在本方案范围)。

决策 3:缓存放插件自己的 data/

推荐:是 → plugins/hot_topics/data/hot_cache.json,并用 .htaccess + web.config 禁 HTTP 访问。

理由:与 site_stats / suiyu / leaderboard 的既有惯例一致;不污染核心 data/runtime/(那里是核心自管目录);

插件 data/ 天然不进发行包。

决策 4:range 的时间窗口按哪个字段?

方案 A(**推荐**)方案 B
条件last_reply_time >= cutoffcreate_time >= cutoff
语义「近 N 天有人讨论过的帖子」按总回复数排「近 N 天新发布的帖子」按总回复数排
现象老帖今天被顶一下就可能进榜今天新发但无人回复的帖会占位(0 回复也上榜)

推荐 A(活跃度窗口)。理由:热榜的语义是「最近有人在讨论」;用 create_time 会让「刚发但没人理」的帖子霸榜,观感差。

⚠️ 但这带来一个必须你知情的语义限制:因为规格要求「纯指标、无时间衰减」,reply_count 是该帖的累计回复数(不是窗口内回复数)。

→ 即「近 24h 有动静的帖子里,累计回复最多的那个」。若要「窗口内回复数」,必须扫 reply_index 按时间聚合(查询更重,且与「零核心改动 + 只读 topic_index」的规格不符)。

请确认接受这个语义。

决策 5:右栏面板的可见页面范围(我新增)

方案 A(**推荐**)方案 B
做法right_sidebar_after 天然触发 → 所有带右栏页面都显示在钩子内复刻 $showSiteStats 条件,仅首页/论坛/分类/博客显示
差异帖子/博客详情页也会出现本面板与站点统计同域,两面板始终成对出现
复杂度0 判断需在钩子里读 $GLOBALS['__view']->currentPath() 复刻 4 个条件

推荐 A。理由:零判断、与 suiyu/leaderboard 的既有行为一致;且详情页放「热榜」本身合理(引导继续浏览)。

若你希望「跟站点统计严格成对」,选 B。

决策 6:候选池容量 / TOP N / TTL(我新增)

  • top_n(面板条数)默认 5;/hot 页展示 20 条(不分页)。
  • pool(候选池)默认 60 —— 理由:池需不按版块过滤,而每请求过滤后可能只剩少数,

池太小会导致有权限的用户也看到不足 N 条。60 条 ≈ 6 组 × 60 = 360 行的 JSON(约 60–80 KB),文件读写开销可忽略。

  • ttl 默认 300s。

五、实施阶段(每阶段末暂停确认)

阶段内容验收方式
P1骨架 + 后台:plugin.json / Plugin.php(常量 + class_alias)/ AdminController / hook/admin_route_register / views/admin/settings / 三语 lang/ / data/ 两个保护文件插件可激活;后台设置页 200;保存后同时回读 DB 与 settings_cache.php 确认落库
P2数据层 + 缓存:6 组聚合 SQL + refreshCache() + getList() + 权限内存过滤 + 文件缓存读写CLI 探针:6 组结果与手写 SQL 一致;池内条目全部落在 getAuthorizedCategoryIds() 之外时游客取到空;TTL 内 0 次重算、过期后恰好 1 次
P3独立页 /hot:route_register + FrontController + views/index.php + 6 组链接切换 + CSSHTTP 冒烟:6 组 URL 全 200;断言每组的排序键单调不增;/hot?range=bogus 回落默认
P4右栏面板:hook/right_sidebar_after.php + views/_panel.php + nav_plugin_links + layout_head_endHTTP 冒烟:断言面板 DOM 出现在「站点统计」面板之前(这是「放在上面」的硬验收);窄屏面板随右栏隐藏;面板在游客/管理员下均正确过滤
P5验证与收尾:门禁全跑 + 说明.md + 清页面缓存见 §七


六、风险与坑(逐条对应规范 / MEMORY)

#风险依据处置
1⚠️⚠️ 目录名含下划线 → 核心按目录名硬拼类名落空 → 生命周期静默跳过 → 卸载留孤儿数据规范 §4.1 / MEMORY §7Plugin.php 末尾加 \class_alias(Plugin::class, 'Plugin\hot_topics\Plugin', false)(下划线 ≠ 大小写差异,class_exists 会恒 false)
2⚠️⚠️ 渲染钩子里 $this 不可用 → $this->e() 被吞成「HTML 渲染一半断掉、HTTP 200、无告警」规范 §11.24 / MEMORY §7钩子内一律 htmlspecialchars($s, ENT_QUOTES, 'UTF-8');表单用 Csrf::token();静态扫不出,只能发真实 HTTP + 查 protected/error.log
3⚠️ 全局缓存泄露私密版块标题本方案 §3.2缓存只存未过滤候选池,过滤在每请求内存做;缓存文件由 .htaccess+web.config 禁访问
4每请求查询超红线规范 §十三(≤5 次)文件缓存 → 正常请求 0 次 DB(+权限表请求级缓存)
5核心同特异性声明后胜 → 插件样式静默失效MEMORY §4插件类一律提权,且只写核心没定义的属性
6夜间主题浅底浅字规范 §11.7只用既有 --mn-*,写前 grep 核对;禁硬编码色
7内联 <style>/<script>/on*规范 §7.3(P0 一票否决)全部收敛到 assets/;发布前跑运行期零内联扫描(只认「标签自身带插件标记」)
8钩子文件声明顶层函数/类 → 双触发钩子(nav_plugin_links)第二处静默消失规范 §9.0/§9.3nav_plugin_links.php 禁声明任何顶层符号
9后台保存「字段名对不上 → 静默写最小值」MEMORY §7用 array_key_exists 判缺失即跳过
10上线新设置项后 settings_cache.php 不自动补键MEMORY §1上线后清一次缓存
11改 nav_* / layout_head_end 后前台仍看旧页规范 §9.5 / MEMORY §1走后台「维护→清理缓存」(data/runtime/pages/ 永久缓存,仅游客列表页)
12视图模板 i18n 缺键 → 显示原始键名MEMORY §6改视图后跑「引用键 ↔ 三语字典」全量比对
13main_index.sqlite 是 WAL,裸 PDO 直读拿到旧快照MEMORY §1/§10一律走 Schema::mainIndexDb()
14若控制器内做 404 分支 → 只渲染不设码 = 软 404MEMORY §7\http_response_code(404) 必须在 view() 之前
15⚠️ 全角标点紧跟 $var → PHP 变量名含 ≥0x80 字节 → 静默空值MEMORY §9交付前跑 .workbuddy-ai/tools/php_var_cjk_check.php
16同文件多处改动并行 Edit 互相覆盖 → 「报成功但未落盘」MEMORY §9同文件多处改动必须串行,改后回读校验


七、验证方案(P5)

项手法通过标准
语法php -l(本机 8.2 + 底线 8.0 双版本)0 error
插件体检plugin_hygiene.pyFAIL = 0
变量/全角php_var_cjk_check.php通过
i18n引用键 ↔ 三语字典全量比对键集合零差异
零内联运行期扫描(只认插件标记)0 命中
「在站点统计上面」HTTP 抓首页 → 断言 hot-topics-panel 的 DOM 位置 早于「站点统计」面板✅(硬验收)
权限过滤造 1 条私密版块帖子(游客不可见)→ 游客抓 /hot 与首页标题不出现;管理员抓则出现
6 组 URL抓 /hot?range={24h,3d,7d}&metric={replies,views}全 200;排序键单调不增
缓存CLI 探针(带 __construct(){} 空构造 + 增量守卫)TTL 内 0 次重算、过期后恰好 1 次
错误日志protected/error.log零新增
收尾清理页面缓存data/runtime/pages/ 清空

⚠️ 探针红线(MEMORY §8):CLI 探针首尾各取一次「行数 + MAX(id)」做增量守卫;禁止按硬编码 id / user_id IN (...) 删数据;

new class extends X {} 是实例化,继承 Admin\BaseController 会跑构造函数 → 必须显式 public function __construct(){}。

⚠️ 用户正开着页面自测时:不清页面缓存、不按「数据眼熟」臆断删数据(先查 users 确认 uid)。


八、待你确认

  1. 决策 1:右栏面板固定「24h + 回复榜」TOP 5、不做 tab?(推荐:是)
  2. 决策 2:/hot 页用普通链接切换、不做 AJAX?(推荐:是)
  3. 决策 3:缓存放 plugins/hot_topics/data/hot_cache.json?(推荐:是)
  4. 决策 4:range 窗口用 last_reply_time(活跃度)还是 create_time(新帖)?

—— 且请确认接受「回复数 = 帖子累计回复数,不是窗口内回复数」这个语义。

  1. 决策 5:面板显示范围 —— 所有带右栏页面(含详情页)还是仅站点统计可见的 4 类页面?(推荐:前者)
  2. 决策 6:top_n=5 / pool=60 / ttl=300s,可否?
  3. 目录名:保持 hot_topics(需 class_alias 兜底)还是改成无下划线名(如 hottopics / hot_list,天然绕开该坑)?

确认后我按 P1 → P5 分阶段做,每阶段末停下来给你看。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 2 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录