右栏排行榜面板:积分 / 主题 / 回复 / 签到 四榜 TOP6,纯 CSS 切换 tab。
一、挂载位置(方案 B)
复用核心通用侧栏钩子 sidebar_above_friend_links,零核心改动。
| 端 | 容器 | 渲染顺序 |
|---|---|---|
| PC | aside.mn-right(right_sidebar.php) |
用户面板 → 站点统计 → 热门标签 → 排行榜 → 友情链接 |
| 移动 | .mn-mobile-sidebar(mobile_sidebar.php) |
热门标签 → 排行榜 → 友情链接 |
- 该钩子一次页面渲染触发两次(PC 右栏 + 移动侧栏各一次),两份 DOM 靠核心媒体查询切换显隐, 因此「同一页面出现两份面板」是正确行为。
- ⚠️ 移动端那份被核心包在
if (!$mobileIsTagPage)内 →/tags与/tag/*页面只有 1 份(PC 右栏)。 - ⚠️ 移动端侧栏没有站点统计面板,故移动端排行榜位于热门标签下方。
二、四个榜的口径
| tab | 数据源 | 口径 |
|---|---|---|
| 积分 | data/meta/business.sqlite → users |
status='active' AND points>0,按 points DESC, id ASC |
| 主题 | data/meta/main_index.sqlite → topic_index |
status=0 AND deleted_at IS NULL,按 uid 计数 |
| 回复 | data/meta/main_index.sqlite → reply_index |
status=0,按 uid 计数 |
| 签到 | plugins/daily_checkin/data/daily_checkin.sqlite |
按 uid 计数(累计签到天数) |
- 站长不排除(榜单为真实数据)。
- 用户名 / 头像:主题 / 回复 / 签到三榜拿到 uid 后统一走
User::batchGetUsers(); 用户名缺失(用户已注销)的行直接丢弃,避免死链。 - 并列名次用
uid ASC稳定排序。
⚠️ 签到榜为什么直读 daily_checkin 表
实测(2026-09-27):points_log 里 related_type='daily_checkin' 只有 4 条,
而 daily_checkin 表有 63 条 —— 差 15 倍,历史签到并未全部写入积分日志。
走积分日志统计会严重失真,故必须直读签到表。
跨插件边界处理(规范 §11.6 灰区):不调用对方 Plugin 类、不写对方表,仅只读聚合;
并以三重守卫降级 —— Plugin::isActivated('daily_checkin') + 库文件存在 + 表存在,
任一不满足则该 tab 显示「暂无数据」(不影响其余三榜)。
三、缓存机制(性能)
右栏每页都渲染,故不能在渲染路径做全表聚合。
渲染钩子(每页)
└─ 读 lb_cache(1 次查询,全表仅 4 行)
├─ 数据新鲜 → 直接渲染
└─ 已过期 → 仍渲染旧数据(不阻塞),置 $GLOBALS['lb_need_refresh']
route_after_dispatch(index.php:321,响应产出之后)
└─ 若标志为真 → 重算 4 榜 + 回写(4 次聚合 + 1 次 upsert)
- 正常请求的 hook 查询数 = 1(缓存表)+ 1(batchGetUsers)= 2 次(规范红线 ≤5 次)。
- 重算发生在响应之后,不占用户等待时间;未过期的请求走 1 次
if判断即返回,零查询。 - 冷启动(首次启用 / 缓存被清):渲染钩子内同步算一次,保证首屏不留空面板。
- TTL = 300 秒(与生产云函数 5 分钟
cron_trigger.php节奏一致)。 - 请求内静态缓存:双触发时第二次调用复用同一份数据,不重复查库。
- ⚠️ 游客的列表页走核心
PageCache(整页 HTML 永久缓存),榜单数据会随之变旧; 登录用户不走 PageCache,看到的是 TTL 内的数据。这是核心缓存机制决定的,非本插件引入。
四、tab 实现:纯 CSS,零 JS
- 4 个
<input type="radio">+ 4 个<label for>+ 4 个 pane。 - 显隐由
:checked ~ .lb-panes .lb-pane:nth-child(N)控制;激活态由:checked ~ .lb-tabs .lb-tab:nth-child(N)控制。 - 对应关系靠 **
:nth-of-type(N)(radio,按 input 标签计数)/:nth-child(N)(label、pane)**建立,不依赖 id 语义。 - ⚠️ 双份面板的 radio 若同名会变成同一个 radio group → 点移动端 tab 会同时改掉 PC 端选中态。
故 radio 的
name/id带渲染序号(插件自实现 static 计数器Plugin::nextSeq()),两份面板互不干扰。 - 服务端一次渲染全部 4 个 pane,切换零请求。
五、文件结构
plugins/leaderboard/
├── plugin.json # hooks: init_after / sidebar_above_friend_links
│ # / route_after_dispatch / layout_head_end
├── Plugin.php # 独立库 / 建表 / 生命周期 / 4 榜聚合 / 缓存读写
├── hook/
│ ├── init_after.php # 惰性建表兜底(戳文件门控)
│ ├── sidebar_above_friend_links.php # 面板渲染(双触发钩子)
│ ├── route_after_dispatch.php # TTL 懒刷新(有触发源收敛)
│ └── layout_head_end.php # 注入 style.css(带 ?v=filemtime)
├── assets/style.css # 仅补 tab 条 + 排名序号/头像;颜色全走 --mn-*
├── lang/{zh,zh_tw,en}.php # 6 个键,三语键集合与键序一致
└── data/
├── leaderboard.sqlite # 独立库(lb_cache 表)
├── schema.version # 数据层版本号戳文件
└── .htaccess / web.config # 自动生成,防 Web 直读
可调常量(Plugin.php 顶部):TOP_N = 6、TTL = 300。
改动 ddl() 时必须同步递增 SCHEMA_VERSION,否则老站点戳文件命中、新表建不出来(规范 §11.15)。
六、设计约束(改动前必读)
- 零内联:样式全在
assets/style.css,无内联<style>/on*/style=""(规范 §7.3 P0)。 - 颜色只能用既有
--mn-*:当前用到--mn-text/--mn-text-muted/--mn-primary/--mn-primary-subtle/--mn-border-light,五者在 9 套主题 + modern.css 中均有定义。 ⚠️--mn-bg-hover不存在、--mn-danger/--mn-spectrum-*不可用(已避开)。 - 选择器一律带
.lb-panel前缀提权:核心同特异性声明因源序靠后会静默压掉插件样式。 - 渲染钩子里
$this不可用(闭包作用域,规范 §11.24):用htmlspecialchars()、Upload::url()、I18n::get(),图标用核心字号类<i class="fa mn-fs-12">(不用$view->icon(), 其返回值带内联style)。 - 钩子文件禁止声明顶层函数/类(规范 §9.0):
sidebar_above_friend_links双触发, 一旦声明会被降级为include_once,第二处入口静默消失。 - 不做的部分:无独立榜单页、无「更多」链接、无后台配置页(改常量即可)。
- 改
hooks清单后必须\app\Helpers\Plugin::refresh()重建plugins/plugins_cache.json; 部署后须走后台「维护 → 清理缓存」。
七、验证记录(2026-09-27)
| 套件 | 结果 |
|---|---|
| 数据层(4 榜 vs 手写 SQL 逐行对照 / 缓存落盘 / TTL 判定 / 数据形状) | 17 PASS / 0 FAIL |
| 结构(顶层符号 / 双 include / 请求内缓存 / i18n 三语 / CSS 卫生 / 零内联 / 生命周期) | 35 PASS / 0 FAIL |
| 渲染产物(真实 HTTP,首页 + 论坛页) | 58 PASS / 0 FAIL |
| tab 选择器对应关系(jsdom 真实 DOM) | 50 PASS / 0 FAIL |
| 卸载与冷启动 | 16 PASS / 0 FAIL |
| TTL 懒刷新端到端(HTTP) | 未过期不重算 ✓ / 过期后重算 ✓ |
protected/error.log |
无新增、零 leaderboard 记录 |
未验证项(交给站长自测):双主题(默认浅色 + 暗夜星辰)视觉观感、面板实际观感与右栏整体协调度。