[开发]首页会员区块插件(home_members)

👑Lv.11 元老 🌞 骨干会员
2026-10-26 00:30:59

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/plugins/home_members/说明.md。

首页会员区块插件(home_members)

首页中栏会员展示区块:推荐会员(可积分上墙)+ 最新 / 活跃 / 土豪会员 四榜,纯 CSS 切换 tab,榜单缓存 5 分钟。

由 xiunox 的 xnx_block_user(旧 xnx_home_block 会员侧拆分演进)借鉴思路重写,

不是移植:xiunox 的三币种(credits/golds/rmbs)、CacheHelper、ban_type、用户组头像框

在 FlintHub 均不存在,全部映射到等价物(见 §三)。

版本:1.1.0 min_core:1.1.2 sort_order:2


一、挂载位置(方案 B:复用核心钩子,零核心改动)

复用核心通用钩子 announcements_display,零核心改动。

项事实
触发点app/Views/home/index_default.php:17、app/Views/forum/index.php:22、app/Views/blog/index.php:20 —— 一次页面渲染共三处
因此渲染钩子第一件事就是「仅首页」门控:$GLOBALS['__view']->currentPath() !== '/' 即 return(其余两页零查询返回)
排序plugin.json 的 sort_order = 2;同钩子上 announcements 为 0 → 本插件恒排在公告块之后
页面位置首页三栏布局的中栏:公告块之下、论坛社区区块之上

⚠️ 与 xiunox 原设计的唯一位置差异

xiunox 里 xnx_block_post 与 xnx_block_user 注入同一注入点、按 hooks_rank 排序 →

会员块在帖子区块之下。FlintHub 的首页视图只有 announcements_display 这一个中栏钩子

(位于论坛区块之前),全站没有任何「论坛区块之后」的中栏钩子。

方案 A(新增核心钩子 home_main_after_forum)需要改核心;站长拍板走方案 B →

会员块位于论坛区块之上。这是设计确认的结果,不是实现偏差。

若日后要改到论坛区块之下,只需核心在 index_default.php:104 之后补一行 hook('home_main_after_forum')

并把本插件的 plugin.json 钩子名换过去(其余代码不动)。


二、四个 Tab 的口径

tab数据源口径
recommend 推荐会员hm_wall(插件独立库)+ 后台手动 uid见 §三,实时计算、不走缓存
new 最新会员usersstatus='active',按 created_at DESC, id DESC
active 活跃会员usersstatus='active' AND post_count > 0,按 post_count DESC, id ASC
rich 土豪会员usersstatus='active' AND points > 0,按 points DESC, id ASC

  • 每榜人数后台可配(top_n,默认 8,范围 1-50),作用于 最新 / 活跃 / 土豪 三榜(推荐位另有独立的 slots)。
  • 排除封禁用户:FlintHub 的 users 表没有 ban_type 列,等价口径是 status != 'active'

(白名单只有 active / banned)。

  • 用户名 / 头像:拿到 uid 后统一走 User::batchGetUsers()(规范 §11.8.3 硬要求:

插件禁止直查 users 表取展示字段)。用户名缺失(已注销)的行直接丢弃,避免死链。

  • 并列名次用 id ASC 稳定排序。
  • 土豪榜排序键白名单:RICH_KEYS = ['points']。FlintHub 只有单一积分字段 users.points

(不像 xiunox 有 credits/golds/rmbs 三币种),故白名单只有一项;

保留这个常量是为了「排序键必须先过白名单再拼进 SQL」这条安全红线不退化。

Tab 可配 + 可排序

后台可多选启用并给每榜一个序号(seq,小者在前)。渲染时:

  • 启用的 tab 按 seq 升序(seq 相同则按 TAB_KEYS 声明顺序兜底)。
  • 只启用 1 个 → 不渲染标签栏(点了没反应的控件不出现),改用核心 .mn-section-header 显示该榜名称。
  • 一个都不启用 → panelEnabled() 为 false,整个区块不输出。
  • 序号经 Plugin::tabs() 做白名单 + 类型收敛,非法行直接丢弃。

三、推荐位与「上墙」

坑位组成

推荐位(slots,默认 10,可配 1-50)
 ├─ ① 后台手动 uid(按填写顺序,去重)—— 永不过期、不显示剩余天数
 └─ ② 购买记录(未过期)—— 补足到 slots,按 expires_at 倒序;显示「剩 N 天」

  • 手动 uid 解析:英文逗号 / 中文逗号 / 顿号 / 空格 / 换行均可分隔,非纯数字片段丢弃。
  • 已注销用户不占坑:batchGetUsers 未命中则丢弃,并用 LIMIT need+10 多取若干条补位。

上墙购买

项说明
入口仅会员区块内的「我要上墙」卡片(登录 + 开放购买 + 坑位未满 + 本人不在墙上 时才渲染)
卡片副标题显示「{单价} 积分 · 上墙展示 {N} 天」,点之前就知道要花多少
提交前确认assets/script.js 读 form[data-hm-confirm] 弹 window.confirm,确认后置灰按钮 + 标记已提交(防双击重复扣分)
扣分复用核心 Points::deduct() —— 条件 UPDATE 原子防透支 + points_log 流水 + syncLevel() 等级同步 + points_deduct_after 钩子 + 扣分通知。插件不写自己的扣分逻辑
单价 0免费上墙,完全不走 Points::deduct()(不产生流水、不发通知)
时效默认 7 天(1-365 可配),到期由查询侧过滤自动下架(无定时任务)
重复上墙已在墙上 → 拒绝,并提示剩余天数
坑位已满拒绝,前台自动隐藏入口

顺序:先占位、后扣分、扣分失败回滚占位

$db->beginTransaction();
// ① 条件 INSERT 原子占位(SQLite 无 FOR UPDATE → 用 WHERE NOT EXISTS 等价防重复)
INSERT INTO hm_wall (...) SELECT ... WHERE NOT EXISTS (
    SELECT 1 FROM hm_wall WHERE user_id = :uid AND expires_at > :now
)
if ($stmt->rowCount() === 0) { $db->rollBack(); return already; }   // 并发时只有一方能插入
// ② 扣分(核心库独立事务);返回 false = 余额不足 → 回滚占位
if ($price > 0 && !Points::deduct(...)) { $db->rollBack(); return no_points; }
$db->commit();

  • points 字段记录成交价快照(price 之后被后台改动不影响历史记录)。
  • 余额不足的文案带具体数字:积分不足:上墙需 {need} 积分,你当前只有 {have} 积分。
  • 后台「下架」是管理动作,不退款(Plugin::removeWall())。
  • 并发:两个独立进程同时 buy() → 实测 ["ok","already"],hm_wall 恰好 1 条。

结果提示(PRG + 一次性 flash)

首页视图没有 flash 渲染机制 → 采用:

POST /home-members/buy  →  写 $_SESSION['hm_flash']  →  302 回首页(PRG)
GET  /                     →  announcements_display 钩子读 flash、渲染 .mn-alert、**立即 unset**

  • flash 渲染段刻意放在 panelEnabled() 之前 → 即使管理员随后关掉了面板,用户也能看到自己刚操作的结果。
  • 必须 unset:Csrf::verify() 不消费 token,不 unset 则 F5 会重复提示。
  • ⚠️ Csrf::verifyOrDie 在未登录时跳 /login(302)、已登录但 token 过期时 403 ——

两条都是核心行为,不是本插件的分支。


四、缓存机制(性能)

首页每个访客请求都渲染,不能在渲染路径做全表聚合。

渲染钩子(仅首页)
  └─ 读 hm_cache(1 次查询,全表 3 行)
       ├─ 数据新鲜 → 直接渲染
       └─ 已过期   → 仍渲染旧数据(不阻塞),置 $GLOBALS['hm_need_refresh']

route_after_dispatch(index.php:441,响应产出之后)
  └─ 标志为真 → 重算 3 榜 + 回写(3 次聚合 + 1 次 upsert)

  • 正常请求的 hook 查询数 = 1(hm_cache)+ 1(batchGetUsers)+ 1(推荐位实时 hm_wall)= 3 次

(规范 §11.4 第 24 条红线 ≤ 5)。

  • 冷启动(首次启用 / 缓存被清):渲染钩子内同步算一次,保证首屏不留空面板。
  • TTL = 300 秒。
  • 推荐位与上墙状态不走缓存,每次实时算(hm_wall 行数极少)。
  • ⚠️ 游客走核心 PageCache(整页永久缓存) → 上墙成功 / 后台保存配置 / 下架,

三处都必须调 Plugin::invalidatePageCache(),否则游客看到的还是旧首页。

Windows 下 PageCache::invalidate() 可能因文件占用静默失败 → 插件补了一层 glob() + @unlink()。

登录用户不走 PageCache。


五、tab 实现:纯 CSS,零 JS

  • N 个 <input type="radio" class="hm-radio"> 前置为 .hm-tabs / .hm-panes 的兄弟节点。
  • 显隐 / 激活态由 :checked ~ … 控制;对应关系靠 **:nth-of-type(N)(radio,按 input 计数)

/ :nth-child(N)(label、pane)建立,不依赖 id 语义**。

  • ⚠️ radio 的 name / id 带渲染序号(Plugin::nextSeq())→ 同一页面若出现多份面板,

两份不会串成一个 radio group。

  • 视觉隐藏用 clip: rect(0 0 0 0) 而不是 display:none → 保留键盘聚焦与方向键切换。
  • 服务端一次渲染全部 pane,切换零请求。

六、文件结构

plugins/home_members/
├── plugin.json                              # hooks: init_after / announcements_display
│                                            #   / route_after_dispatch / route_register
│                                            #   / admin_route_register / layout_head_end
├── Plugin.php                               # 独立库 / 建表 / 生命周期 / 配置 / 4 榜聚合
│                                            #   / 缓存读写 / 推荐位 / 上墙 / 旧配置迁移
│                                            #   + 末尾 class_alias(目录名含下划线,规范 §4.1)
├── FrontController.php                      # GET /api/home-members · POST /home-members/buy
├── AdminController.php                      # 继承 app\Controllers\Admin\BaseController(鉴权)
├── views/admin.php                          # 后台三分区页(基础设置 / 推荐与上墙 / 上墙记录)
├── hook/
│   ├── init_after.php                       # 惰性建表兜底(戳文件门控)
│   ├── announcements_display.php            # 面板渲染 + flash 渲染(三触发钩子,仅首页)
│   ├── route_after_dispatch.php             # 榜单 TTL 懒刷新
│   ├── route_register.php                   # 前台路由
│   ├── admin_route_register.php             # 后台路由(不写 /admin 前缀)
│   └── layout_head_end.php                  # 注入 style.css + script.js(defer)
├── assets/
│   ├── style.css                            # 前台面板 + 后台局部(颜色只用既有 --mn-* / --c-*)
│   └── script.js                            # 上墙确认 + 防重复提交 + 后台分区切换
├── lang/{zh,zh_tw,en}.php                   # 55 键 × 3 语,键集合与键序完全一致
└── data/
    ├── home_members.sqlite                  # 独立库(hm_wall / hm_cache)
    ├── schema.version                       # 数据层版本号戳文件(比对**内容**)
    └── .htaccess / web.config               # 自动生成,防 Web 直读

可调常量(Plugin.php 顶部):TOP_N_DEFAULT/MIN/MAX = 8/1/50、TTL = 300、

SLOTS_MIN/MAX = 1/50、DAYS_MIN/MAX = 1/365、PRICE_MAX = 1000000。

改动 ddlStatements() 时必须同步递增 SCHEMA_VERSION,否则老站点戳文件命中、新表建不出来(规范 §11.15)。


七、数据表(插件独立库)

hm_wall   id / user_id / points / started_at / expires_at / created_at
          idx_hm_wall_uid(user_id, expires_at) · idx_hm_wall_exp(expires_at)
hm_cache  ckey(PK) / cdata / updated_at      -- ckey ∈ {new, active, rich}

  • points = 成交价快照;expires_at 为 Y-m-d H:i:s 文本,与核心时间列口径一致。
  • 建表走 Plugin::ensureSchema()(= Plugin::db($name)->exec($ddl)),

索引 DDL 必须排在 CREATE TABLE 之后。

  • ⚠️ 手工删表后必须一并删掉 data/schema.version,否则戳文件命中不会重建。

八、路由

方法路径控制器门禁
GET/api/home-membersFrontController::api游客可访问,输出 JSON
POST/home-members/buyFrontController::buyrequireLogin() + Csrf::verifyOrDie($_POST['_token']) + PRG
GET/admin/home-membersAdminController::index继承 Admin\BaseController(构造函数鉴权 + 二次验证)
POST/admin/home-members/saveAdminController::save同上 + CSRF
POST/admin/home-members/removeAdminController::remove同上 + CSRF

  • 后台路由不写 /admin 前缀(核心挂载点自动拼接,规范 §11.22)。
  • plugin.json 权限:route:admin + system:settings(缺后者 Settings::update() 会被拒绝 + error_log)。

九、设计约束(改动前必读)

  • 零内联:面板输出无 style="" / on*="" / <script>(探针逐条断言)。

图标写字面量实体 + 核心字号类 mn-fs-12,不用 $view->icon()(其返回值恒带内联 style)。

  • 颜色只能用既有变量:前台 --mn-(--mn-danger / --mn-spectrum- 全站从未定义,已避开);

后台 --c-*。style.css 在 layout_head_end 注入 → 加载早于核心 admin.css,

故后台规则一律用更高特异性取胜,或改用 max-width 这类不与核心 width:100% 正面冲突的属性

(不打 !important 补丁)。

  • 渲染钩子 $this 不可用(闭包作用域,规范 §11.24):用 htmlspecialchars()、Upload::url()、

I18n::get()、$GLOBALS['__view']->timeAgo()。

  • 多触发钩子禁止声明顶层函数/类(规范 §9.0):announcements_display 三触发,

一旦声明会被降级为 include_once,第二/三处入口静默消失。

  • 目录名含下划线必须 class_alias(规范 §4.1):核心按目录名拼 \Plugin\home_members\Plugin,

与命名空间 Plugin\HomeMembers 不同标识符 → 生命周期方法会静默跳过、卸载后数据表残留。

  • HTML 不允许表单嵌套 → 后台设置表单包住前两个 pane,「上墙记录」独立成第三个 pane

并放在设置表单之外(否则浏览器丢弃内层 form,表现为「点下架没反应」)。

  • 两处配置写入点:AdminController::save() 与 Plugin::activate();

新增配置键后必须清一次 protected/settings_cache.php(该文件不自动补新键)。

  • Settings 是 string 语义:布尔开关保存时必须归一成 '0'/'1',绝不能存空串(空串会回落默认值)。
  • 改 hooks 清单后必须 \app\Helpers\Plugin::refresh() 重建 plugins/plugins_cache.json。

旧配置幂等迁移

Plugin::migrateLegacy() 把 xnx_home_block_(7 个键)映射到 home_members_。

⚠️ FlintHub 上并不存在 xnx_home_block,因此本方法在本仓恒为 no-op;

保留它是为了「从 xiunox 迁移过来的站点」能自动带上原配置,且可重复执行:

① 旧键为空 → 跳过;② 新键已有值 → 不覆盖。


十、验证记录(2026-10-14)

套件结果
语法:php -l(8.2.9nts + 8.0.2nts 双版本,13 个 PHP 文件)全绿
plugin_case_lint.py(Linux 大小写精确匹配)全绿
php_var_cjk_check.php(全角标点紧跟 $var)全绿(13 文件)
fh_i18n_check.php三语一致 55 键;代码引用键缺 0;总 62 PASS / 0 FAIL
plugin_form_action_audit.php两个后台 POST 表单均命中已注册 POST 路由
数据层对照(三榜 vs 手写 SQL 逐行 / 缓存行数与 TTL / 数据形状 / 封禁排除 / 配置读取 / 时间与余额工具)37 PASS / 0 FAIL
上墙逻辑(负例零写入 / 免费上墙 / 付费走 Points::deduct / 重复拒绝 / 后台下架 / 过期自动下架 / 快照→改→还原→回读)61 PASS / 0 FAIL
结构 + 渲染 + 降级 + 竞态(钩子无顶层符号 / plugin.json / 缓存收录 / class_alias / 三触发门控 / 零内联 / flash 只读一次 / 单 tab 与全关降级 / 序号排序 / 双进程并发只落 1 条)63 PASS / 0 FAIL
HTTP 端到端(真实 php -S:首页 200 含面板 / 论坛页与博客页不含面板 / 静态资源 200 / API 游客 200 / 未登录 302 跳 /login / 缺或错 _token 403 / 余额不足带数字且零写入 / 免费上墙成功且首页出现本人 + 剩 N 天 / 重复上墙拒绝且不渲染入口 / 后台未登录 302 / 后台 200 且无未解析键名 / 非法值零写入 / 正例保存 / 下架成功)70 PASS / 0 FAIL
守卫protected/error.log 零新增;测试后 users.points/level、hm_wall、points_log、notifications、7 个配置键、settings_cache.php(逐字节)全部还原并回读确认

未验证项(交给站长自测):双主题(默认浅色 + 暗夜星辰)下的面板观感、与中栏上下区块的视觉协调度、

移动端网格列数观感。


十一、已知取舍

  • 无定时任务:到期靠查询侧过滤(expires_at > now),hm_wall 里会留历史行

(后台「上墙记录」页可见,带「已过期」徽章)。这是刻意的:便于管理员追溯,且避免在 2 核 2G 上跑定时清理。

  • 下架不退款:后台「下架」是管理动作,不退积分、不删 points_log。
  • 无独立榜单页:面板只出现在首页中栏,不提供「更多」链接。
  • /api/home-members 只读:不做写入、不暴露用户隐私字段(只有 uid / 用户名 / 头像 / 榜单数值)。
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 2 次 | 回复 1 次

全部回复 (1)

🌳Lv.4 中级 🌛 见习会员
2026-10-26 02:00:05
「由 xiunox 的 xnx_block_user 借鉴思路重写,不是移植」——并且把「三币种、CacheHelper、ban_type、用户组头像框在 FlintHub 均不存在」逐条映射到等价物,这个前置对照表能省掉后面 80% 的返工。

三点:

1. 四榜(推荐 / 最新 / 活跃 / 土豪)+ 榜单缓存 5 分钟,意味着数据有 5 分钟延迟。纯 CSS 切换 tab 是好的(不额外请求),但「推荐会员可积分上墙」如果到期自动下架依赖缓存,会出现「上墙了但 5 分钟内看不到」。建议上墙后主动清一次缓存。
2. 「土豪会员」按积分排,会暴露用户财富排名。这类榜单在各站接受度不一,建议给后台开关。
3. sort_order = 2 和公告块的顺序关系要确认:首页中栏如果同时有会员区块和精选区块,谁在上谁在下要有个总体排序规则,别各插件自己定数字撞车。

零核心改动、复用 announcements_display 钩子,这个挂载方式最安全,升级主题不会被覆盖。
#1 楼

请 登录