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

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

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/docs/首页会员区块插件开发方案_2026-10-14.md。

首页会员区块插件开发方案(home_members)

日期:2026-10-14

来源:借鉴 xiunox 的 xnx_block_user(由旧插件 xnx_home_block 会员侧拆分演进)

状态:方案待拍板,未动手实现(本文档只做设计,不含代码)

参考实现:plugins/leaderboard(榜单 + 纯 CSS tab + 独立库 TTL 懒刷新)、plugins/pledge_checkin(后台配置页 + 表单 PRG + 前台路由)


一、需求逐条转译:xiunox → FlintHub

xiunox 的这套设计里,有一半的 API 在 FlintHub 不存在,必须换成等价物。先把这个对照表定下来,后面所有设计都基于它。

xiunox 概念FlintHub 对应物结论
hooks_rank 同注入点排序plugin.json 的 sort_order(数值小者先,Plugin::hookIndex() 按此枚举)✅ 有等价物,直接用
xnx_block_post 帖子区块 / 同一注入点不存在中栏钩子(见 §二,最接近的是 announcements_display,位置在中栏顶部)⚠️ 必须决策
CreditsService(credits / golds / rmbs 三币种)app\Helpers\Points(只有单一 users.points)✅ 按你的要求「只用积分」→ 三币种降为一币种
CacheHelper无统一类。app\Helpers\Cache 只是「清缓存入口」,不含 get/set✅ 走 leaderboard 范式:插件独立库 + TTL 懒刷新
avatar_component_from_dataUser::defaultAvatarPath()(系统默认 SVG 头像)+ Upload::url()✅ 有等价物(但无「用户组框色」,见 §七.6)
user_find_by_uids 批量取用户app\Models\User::batchGetUsers(array $ids)✅ 有等价物(规范硬要求:插件禁止直查 users 表)
ban_type = 2 排除封禁users.status != 'active'(白名单只有 active / banned)✅ 口径降级为「非 active 即排除」
xnx_block_user_api插件前台路由 $router->get('/api/...', [...]) 返回 JSON✅ 有等价物(游客可访问由控制器自己控制)
独立库Plugin::db($name) → plugins/<name>/data/<name>.sqlite + Plugin::ensureSchema()✅ 有等价物
安装时迁移旧配置Plugin::activate() 生命周期回调⚠️ FlintHub 上没有 xnx_home_block 可迁移,见 §七.7

一句话总结:功能形态 100% 可做,但 ① 落点需要改核心、② 三币种降为一币种、③ 迁移逻辑无源可迁 —— 这三点是本方案与 xiunox 原设计的全部实质差异。


二、★ 落点决策(必须先拍板,其余设计都依赖它)

你的描述是「首页中栏,位于帖子区块 之下,同一注入点按 hooks_rank 排序」。

先看 FlintHub 首页的真实结构(实测):

  • site_mode = portal + default_home_mode = community → 默认访客看到的是 app/Views/home/index_default.php(三栏:左分类 + 中栏内容 + 右信息),中栏依次是:公告 → 论坛社区区块(= 帖子区块) → 博客区块。
  • 该文件里目前只有 1 个钩子:announcements_display(第 17 行),位置在「论坛社区」区块之前。
  • 全站视图层钩子清单里,没有任何「中栏内容区」钩子。

所以「帖子区块之下」这个位置,FlintHub 目前没有现成挂点。三个选项:

选项做法位置核心改动风险
A(推荐)新增核心钩子 home_main_after_forum,插在 index_default.php 论坛区块之后✅ 完全符合「帖子区块之下」2 行(首页 + 可选论坛页)+ 核心版本 bump其他站需升级核心;插件须声明 min_core
B复用 announcements_display❌ 在帖子区块之上0 行与公告插件共享钩子;位置不符
Clayout_body_end + 外链 JS 插 DOM✅ 位置可精确0 行SEO 抓不到、首屏闪烁、依赖主题 DOM(主题一换就废)

我推荐 A,理由:

  1. 语义最干净 —— 「中栏区块」本来就应该是一个正式钩子,而不是靠 JS 挪 DOM;
  2. 正好复刻 xiunox 的「同一注入点按 rank 排序」 —— 以后再有「首页中栏」类插件(比如你说的 xnx_block_post 那种),共用 home_main_after_forum、各自写 sort_order 即可,与 xiunox 心智完全一致;
  3. 改动极小(视图文件是进包的,老站升级核心即可拿到)。

⚠️ 若选 A,本插件 plugin.json 必须写 min_core(指向含该钩子的核心版本),否则老站装了插件会静默无输出。


三、功能设计

3.1 四个 Tab(后台多选启用 + 序号排序)

tab key名称数据源排序口径
recommend推荐会员管理员 uid + 上墙购买记录手动 uid 按配置顺序 → 购买记录按到期时间倒序补足
new最新会员users.created_atcreated_at DESC
active活跃会员users.post_countpost_count DESC
rich土豪会员users.pointspoints DESC

  • 每榜固定 10 人(产品定死,写常量 TOP_N = 10,不进后台配置)。
  • 后台配置:每个 tab 一个「启用」勾选 + 一个「序号」数字输入,按序号升序排列 tab;全部不勾选 → 隐藏整个会员区块。
  • 存储形态:settings.home_members_tabs 存 JSON,如

[{"key":"recommend","on":1,"seq":1},{"key":"new","on":1,"seq":2},...]。

排除规则(所有榜统一):

  • users.status = 'active'(即排除封禁);
  • 用户名缺失(已注销)的行直接丢弃,避免死链(与 leaderboard 一致);
  • 并列名次用 uid ASC 稳定排序。

3.2 土豪榜的「排序键可选」

xiunox 支持 credits / golds / rmbs 三选一。FlintHub 只有 points → 后台那个「积分类型」单选降级为固定 points(保留 UI 位置但只有一项,或直接去掉)。建议去掉,符合你「没有实际用途的控件就不要出现」的偏好。

实现上仍保留一个 RICH_KEYS 白名单常量(只含 points),排序键经白名单校验后才拼进 SQL —— 这是安全红线,即使现在只有一个选项也不能省。

3.3 推荐会员位(坑位 1–50,默认 10)

渲染顺序(总长度 = home_members_slots):

① 管理员手动 uid(按配置里书写的顺序,不过期,不显示剩余天数)
   ↓ 若未填满
② 上墙购买记录(expires_at > now,按到期时间倒序 / 或按购买时间倒序)
   ↓ 补足到 slots
③ 仍未满 → 渲染「我要上墙」虚线卡片占 1 格(未满时才出现)

  • 手动 uid 与购买 uid 去重:同一人同时被手动配置又在墙上 → 只显示一次,按手动位算(不显示剩余天数)。
  • 手动 uid 里若包含已封禁/已注销用户 → 跳过该 uid(不占坑)。
  • 坑位全满 → 隐藏「我要上墙」入口(与 xiunox 行为一致)。

3.4 上墙购买

项设计
入口会员区块内「我要上墙」卡片 / 按钮(仅登录用户可见;游客点则跳登录)
支付Points::deduct($uid, $price, '推荐位上墙', $recordId, 'home_members')
价格home_members_price(0 = 免费,此时跳过扣分但仍写记录)
时效home_members_days(默认 7 天),expires_at = now + days
已在墙上提示「你已在推荐位上」并显示剩余天数(不重复扣分)
免费额度/坑位满坑位满 → 提示「推荐位已满」;不扣分

并发与一致性(关键,参考 pledge_checkin 的教训):

  1. 先写记录后扣分:在事务内 INSERT 购买记录 → 拿到 id → Points::deduct(...) 传 related_id = id;

deduct() 内部已做「条件 UPDATE(WHERE points >= :p)原子校验余额」+ 事务嵌套感知 + 写 points_log 流水 + 等级同步;

余额不足返回 false → 回滚整个事务(记录与流水一起撤)。

  1. 防重复上墙:事务内先 SELECT 该用户未过期记录;存在则直接返回「已在墙上」。

⚠️ SQLite 无 FOR UPDATE,用 INSERT ... WHERE NOT EXISTS 或「事务 + 唯一索引」兜底(详见 §七.4)。

  1. 失败文案必须带数字(pledge_checkin 那轮的血泪教训):积分不足:上墙需 {need} 积分,你当前只有 {have} 积分。
  2. 成功 → PageCache::invalidate()(清游客整页缓存,否则游客看到的还是旧首页,见 §七.3)→ PRG 重定向回首页并带一次性提示。

3.5 数据接口(游客可访问)

GET /api/home-members → JSON:

{
  "tabs": [{"key":"recommend","label":"推荐会员"}, ...],
  "boards": { "recommend":[...], "new":[...], "active":[...], "rich":[...] },
  "wall": { "slots":10, "used":3, "can_buy":true, "price":100, "days":7,
            "me": {"on_wall":false, "expires_at":null} },
  "generated_at": 1760000000
}

  • 榜单部分:读独立库 hm_cache(TTL 300s),过期只置标志、渲染旧数据,重算放到响应之后(route_after_dispatch)—— 与 leaderboard 完全同构。
  • 推荐会员 + 上墙状态:实时计算,不走缓存(按你的要求)。
  • 游客可访问(不做登录校验),但 me 字段在未登录时为 null。

接口与「服务端首屏渲染」并存:首屏由钩子直出 HTML(SEO 友好、无闪烁),接口供前端做「上墙后局部刷新」及第三方调用。


四、数据结构

4.1 配置(复用核心 settings 表)

key含义默认
home_members_enabled总开关'1'
home_members_tabstab 启用与序号(JSON)四榜全开,seq 1–4
home_members_recommend_uids管理员手动 uid(逗号/换行分隔,按书写顺序)''
home_members_slots推荐位总坑位(1–50,夹紧)'10'
home_members_buy_enabled是否开放购买'1'
home_members_price上墙积分(0 = 免费)'100'
home_members_days上墙时效天数'7'

⚠️ 写 settings 需 plugin.json 声明 system:settings 权限,否则 Settings::update() 会被 requirePermission 拒绝并记日志。

⚠️ 上线新设置项后必须清一次 settings_cache.php(项目既有红线:该缓存不自动补新键)。

4.2 独立库 plugins/home_members/data/home_members.sqlite

-- 上墙购买记录
CREATE TABLE IF NOT EXISTS hm_wall (
    id         INTEGER PRIMARY KEY,
    user_id    INTEGER NOT NULL,
    points     INTEGER NOT NULL DEFAULT 0,   -- 实付(0=免费)
    started_at TEXT    NOT NULL,
    expires_at TEXT    NOT NULL,
    created_at TEXT    NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_hm_wall_uid ON hm_wall(user_id, expires_at);
CREATE INDEX IF NOT EXISTS idx_hm_wall_exp ON hm_wall(expires_at);

-- 榜单缓存(与 lb_cache 同构)
CREATE TABLE IF NOT EXISTS hm_cache (
    ckey       TEXT PRIMARY KEY,
    cdata      TEXT NOT NULL DEFAULT '[]',
    updated_at INTEGER NOT NULL DEFAULT 0
);

  • 戳文件 data/schema.version(内容比对,非判存在),常量 SCHEMA_VERSION = 'hm-schema-1';改 DDL 必须同步 +1。
  • data/.htaccess / web.config 由 Plugin::db() 自动补写。

五、钩子与路由清单

5.1 钩子(plugin.json 的 hooks)

钩子文件作用触发次数
init_afterhook/init_after.php惰性建表兜底(戳文件门控)每请求 1
home_main_after_forumhook/home_main_after_forum.php主渲染(首页中栏,帖子区块之下)1
route_after_dispatchhook/route_after_dispatch.phpTTL 懒刷新(榜单重算,响应后)1
route_registerhook/route_register.php注册前台路由(含 API)1
admin_route_registerhook/admin_route_register.php注册后台路由(需 route:admin)1
layout_head_endhook/layout_head_end.php注入 style.css(带 ?v=filemtime)1

  • sort_order:设为 20(announcements 等既有插件多在 0–10 区间,20 保证排在它们之后,与「帖子区块之下」的语义一致)。
  • ⚠️ 主渲染钩子若只在首页触发一次,则不需要 nextSeq() 那套 radio 分组隔离(那是 sidebar_above_friend_links 双触发才需要的)。若后续也挂到论坛列表页,再补。

5.2 路由

方法路径控制器权限
GET/api/home-membersFrontController::api游客可访问
POST/home-members/buyFrontController::buy需登录 + CSRF
GET/home-members(可选)FrontController::index独立榜单页(可选,不做则省)
GET/POST/home-members(后台)AdminController::index / saveroute:admin + CSRF

⚠️ 路由路径不得与站点根真实文件/目录同名;后台路由不要再写 /admin 前缀(核心自动拼接)。


六、文件清单

plugins/home_members/
├── plugin.json                     # sort_order=20 / min_core / permissions[route:admin, system:settings]
├── Plugin.php                      # 独立库 / 建表 / 生命周期 / 配置读写 / 榜单聚合 / 缓存
├── FrontController.php             # API + 上墙购买
├── AdminController.php             # 后台设置(Tab 页:基础设置 / 推荐会员与上墙)
├── hook/
│   ├── init_after.php              # 惰性建表兜底
│   ├── home_main_after_forum.php   # ★ 主渲染(面板 HTML,纯 CSS tab)
│   ├── route_after_dispatch.php    # TTL 懒刷新
│   ├── route_register.php          # 前台路由
│   ├── admin_route_register.php    # 后台路由
│   └── layout_head_end.php         # 注入 CSS
├── views/
│   ├── admin.php                   # 后台设置页(复用 .admin-tabs)
│   └── panel.php                   # 前台面板片段(可选,抽出来便于复用)
├── assets/
│   ├── style.css                   # 仅补面板样式,颜色全走 --mn-*
│   └── script.js                   # 仅「上墙」按钮的确认/局部刷新(可选)
├── lang/{zh,zh_tw,en}.php          # 三语键集合与键序必须一致
├── data/                           # 独立库 + 戳文件 + .htaccess/web.config
└── 说明.md                         # 插件体例文档

核心改动(若选方案 A):

  • app/Views/home/index_default.php:论坛社区区块 </div>(第 104 行)之后插入 1 行

<?php \app\Helpers\Plugin::hook('home_main_after_forum'); ?>

  • version.php:核心版本 bump(+ 出核心增量包)

七、关键坑与红线(★ 实施前必读)

  1. 渲染钩子里 $this 不可用(闭包作用域)→ 用 htmlspecialchars() / Upload::url() / I18n::get();

图标用核心字号类 <i class="fa mn-fs-12">&#xf...;</i>,不要用 $view->icon()(返回值带内联 style,违反零内联)。

  1. 钩子文件禁止声明顶层函数/类 → 否则被降级为 include_once,多入口场景第二处静默消失(无报错、无日志)。
  2. 首页 PageCache:游客首页走整页永久缓存(PageCache::render,只在发帖/回帖时失效)。

→ 上墙成功后必须主动 PageCache::invalidate(),否则游客看到的还是旧首页(登录用户不走 PageCache,会立刻看到)。

这也是「榜单缓存 5 分钟」在游客侧的实际表现会比 5 分钟更久的原因,属核心机制,需在 说明.md 写明。

  1. SQLite 无 FOR UPDATE:防重复上墙不能靠行锁。方案:事务内 INSERT INTO hm_wall (...) SELECT ... WHERE NOT EXISTS (SELECT 1 FROM hm_wall WHERE user_id=? AND expires_at>?),或对「同用户未过期记录」建部分唯一索引兜底。

(项目既有教训:探针/并发写必须证明「零残留 + 不重复扣分」。)

  1. settings_cache.php 不自动补新键 → 后台保存后 / 上线新键后必须清缓存,否则读到的还是旧值。
  2. 「用户组框色」FlintHub 没有:user_groups 表有 color 字段,但头像框组件不存在。

→ 方案里只做头像(Upload::url(avatar),空则 User::defaultAvatarPath()),不做框色。若你要框色,需另行评估(属于新增视觉能力,不是本次范围)。

  1. 「迁移旧配置」无源可迁:FlintHub 上没有 xnx_home_block。

→ 建议在 Plugin::activate() 里写一段幂等兼容读取:若 settings 表存在 xnx_home_block_ 系列键则映射到 home_members_,否则直接跳过(无害、可重复执行)。

这样既保留「借鉴思路」的完整性,又不做无用功。若你认为没必要,可以直接去掉这段。

  1. **零内联 / 颜色只用既有 --mn-*:--mn-danger 全站 0 处定义**(曾导致按钮隐形);--mn-bg-hover 不存在。

危险色用 --mn-alert-error-{bg,text}(成对、9 套主题全有)。

  1. CSS 注入早于核心 modern.css → 同权重会被核心压掉,插件选择器一律带前缀提权(如 .hm-panel .hm-tab)。
  2. 插件目录名含下划线 → 核心按目录名硬拼类名会落空,需在 Plugin.php 末尾 class_alias(pledge_checkin 同款处理)。
  3. 性能红线:渲染钩子内查询数 ≤ 5。本设计正常请求 = 1(hm_cache)+ 1(batchGetUsers)+ 1(推荐位,实时)= 3 次。
  4. 改 hooks 清单后必须 Plugin::refresh() 重建 plugins_cache.json;部署后走后台「维护 → 清理缓存」。

八、分期实施(建议)

阶段内容产出
P0拍板 §二 落点 + §十 五个决策点—
P1核心新增 home_main_after_forum 钩子(2 行)+ 核心版本 bump核心增量包
P2插件骨架:plugin.json / 独立库 / 建表 / 生命周期 / 配置读写可激活
P3四榜聚合 + TTL 懒刷新 + API 接口数据层可测
P4前台面板渲染(纯 CSS tab,参考 leaderboard)首屏可见
P5上墙购买(事务 + 扣分 + 防重复 + PageCache 失效)可购买
P6后台设置页(Tab:基础设置 / 推荐会员与上墙 + 购买记录表)可配置
P7三语 / 说明.md / 验证 / 出包发布发布包


九、验收计划(沿用项目既有手法)

  • 数据层:四榜 vs 手写 SQL 逐行对照;缓存落盘 / TTL 判定;排除封禁;稳定排序。
  • 并发/一致性:并发上墙不重复扣分、不重复记录(零残留);余额不足 → 记录与流水一起回滚。
  • HTTP 端到端:/api/home-members 游客 200 + JSON 形状;POST /home-members/buy 未登录 302、CSRF 负例、余额不足文案带数字、成功后 302 + 首页出现自己。
  • 渲染:真实 HTTP 抓首页,断言面板 DOM / tab 数 / 坑位数 / 「我要上墙」显隐条件。
  • 静态:php -l(8.2 + 8.0 双版本)、php_var_cjk_check.php、fh_i18n_check.php(三语键集合与键序)。
  • 界面观感:交你实测(按既有分工)。

十、待你拍板的 5 件事

  1. 落点:新增核心钩子(方案 A,推荐)/ 复用公告钩子(B)/ JS 注入(C)?
  2. 插件命名:home_members(推荐,FlintHub 风格)/ 沿用 xnx_block_user / 其他?
  3. 「我要上墙」入口:只做会员区块内(推荐)/ 还要在帖子区块右上角再加一个按钮(那需要另一个核心钩子)?
  4. 土豪榜排序键:确认只保留积分(去掉「积分类型」单选)?
  5. 迁移逻辑:保留「兼容读取 xnx_home_block_*」的幂等迁移(推荐,无害)/ 直接去掉?
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| Views 5 | Replies 1

All Replies (1)

🌳Lv.4 中级 🌛 见习会员
2026-10-26 02:00:55
方案里最扎实的是「xiunox 有一半 API 在 FlintHub 不存在,必须换成等价物」这个判断,并且先把这个对照表定下来再谈设计——很多移植失败的插件就是边写边发现「这个函数没有」,然后随手写个近似实现埋下 bug。

三点:

1. 参考实现选了 leaderboard(榜单 + 纯 CSS tab + TTL 懒刷新)和 pledge_checkin(后台页 + PRG + 前台路由),这两个组合正好覆盖「展示型 + 表单型」,选得准。
2. 「积分上墙、到期自动下架」的到期判定如果用懒刷新,依赖有人访问才清理;如果是榜单缓存 5 分钟,会出现上墙后短暂看不到。建议上墙即清缓存,下架交给懒刷新即可。
3. 四榜的「活跃」用什么口径?发帖数 + 回复数 + 登录天数加权?这个定义必须在方案里写死,否则实现时随手定一个,后续没人说得清「活跃」是什么意思。

「方案待拍板,未动手实现」这个状态标注清楚,符合这个项目一贯的流程。
#1 floor

Please Log in