[开发]楼中楼(floor_reply)插件说明

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

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

楼中楼(floor_reply)插件说明

版本:1.3.1 | 作者:FlintHub | 状态:✅ 已启用

功能:在帖子楼层(posts)下方支持对楼层的再回复(楼中楼),支持 @ 提及用户(高亮 + 跳转个人主页);

并在有人回复楼层 / @ 到某人时发送通知,通知可深链直达那一条楼中楼。


一、功能简介

  • 楼中楼:对某条回复(楼层)进行再回复,数据存独立库,不占用核心楼层数
  • @ 提及:回复时 @ 指定用户,前端高亮显示,点击跳转其个人主页
  • mention_user_id 只记录第一个被 @ 的用户,仅用于列表高亮渲染
  • 通知则覆盖全部被 @ 的用户(见第四节)
  • 回复计数联动:回复成功后同步更新帖子的回复数与最后回复时间(Thread::incrementReplyCount)
  • 积分奖励:回复成功触发核心积分奖励(Points::markReplied / Points::award),并计入站点统计
  • 通知 + 深链(V1.1.0):楼中楼回复会通知楼层作者与被 @ 的人(楼主可选),

点通知直达该条楼中楼所在页并高亮

  • 站内表情(V1.2.0):回复框右侧「表情」按钮弹出表情面板(60 个站内表情,复用核心编辑器同款面板样式)。

插入的是 :code: 文本(如 huaji),入库仍是纯文本,渲染时才由核心 Emoji::parse() 转成

<img class="emoji-emotion">;存量纯文本回复与表情可混存,无需数据迁移

  • ⚠️ V1.3.1 起面板改为「数据懒取 + JS 建 DOM」:页面源码里不再有任何表情标记

(旧版把 60 个格子内联成页面级 <template class="fr-emoji-tpl"> ≈ 12.6 KB,占帖子页 11.4%)。

现由 script.js 首次点表情按钮时 fetch 一次 GET /floor-reply/emojis,再在 JS 里建 DOM 挂 body 常驻

二、目录结构

plugins/floor_reply/
├── plugin.json              # 插件清单(6 个钩子 + admin_url + permissions)
├── Plugin.php               # 独立库连接 + 建表 + 楼中楼统计/计数 + 配置读写 + pageOfReply()
├── FrontController.php      # 楼中楼回复 / 列表接口(?focus= 深链)/ 表情数据接口(V1.3.1)
├── AdminController.php      # 后台通知设置页(V1.1.0 新增)
├── hook/
│   ├── init_after.php               # 幂等建表兜底
│   ├── route_register.php           # 前台路由(回复/列表/删除/表情数据)
│   ├── admin_route_register.php     # 后台路由(V1.1.0 新增,需 route:admin 权限)
│   ├── layout_head_end.php          # 注入楼中楼 CSS/JS(仅 thread/show);把表情数据地址挂 

三、数据表(插件独立库 plugins/floor_reply/data/floor_reply.sqlite)

表用途
fr_replies楼中楼回复(post_id / thread_id / user_id / mention_user_id / content / created_at,索引 idx_fr_post、idx_fr_thread)

  • 建表:activate() + init_after 双保险(幂等 DDL)
  • 卸载:uninstall() 删表 并清理核心 settings 表里本插件的配置键(会清空全部楼中楼数据,请谨慎)

四、路由

前台

方法路径说明
GET/floor-reply/list/{id}某楼层({id} = 楼层 post_id)的楼中楼列表片段(htmx 局部渲染,每页 5 条)<br>可选 ?page=N 翻页;可选 ?focus=<楼中楼ID> 强制渲染该条所在页并标记高亮(通知深链用)
POST/floor-reply/reply提交楼中楼回复(CSRF + 频控)
POST/floor-reply/delete删除楼中楼回复(作者本人 / 管理员 / 楼主)
GET/floor-reply/emojisV1.3.1 表情数据(JSON,{v,list:[[code,中文名,文件名],…]},60 条)<br>供 script.js 首次点表情按钮时懒取;与用户无关 → 允许匿名 + 长缓存(public, max-age=604800, immutable,URL 带 ?v=<Emoji.php mtime> 便于改表情即失效)

后台

方法路径说明
GET/admin/floor-reply通知设置页
POST/admin/floor-reply/save保存设置(CSRF)

⚠️ 后台路由需在 plugin.json 声明 route:admin;写全局设置需 system:settings。二者均已声明。

五、通知机制(V1.1.0)

收件人规则

一条楼中楼回复可能同时命中三类收件人,按优先级去重,保证「每个收件人最多收到 1 条」:

优先级收件人通知类型默认
1被 @ 到的人mention✅ 开
2楼层作者floor_reply✅ 开
3楼主(帖子作者)floor_reply❌ 关

  • 回复人自己永不收到通知(与核心 notifyReply / notifyQuote / notifyVote 一致)
  • 楼主默认不通知:楼中楼是「楼层内部的对话」,核心 notifyReplyAggregated() 已负责「帖子有新回复」的聚合通知
  • 通知写入核心 business.sqlite 的 notifications 表:related_id = 楼层 post_id、related_type = 'floor'

深链格式

/thread/{主题ID}?pid={楼层ID}&fr={楼中楼ID}#fr-{楼中楼ID}

三段分工:

  • ?pid= → 核心 ThreadController::show() 经 Post::pageOf() 算出楼层所在页码并跳转
  • ?fr= → 本插件 ?focus= 参数算出该条楼中楼所在页并标记高亮
  • #fr-{id} → 原生锚点兜底,script.js 负责滚动到视野中央

通知文案键(核心语言包 lang/{zh,en,zh_tw}.php)

键用途
plugin.notifications.floor_reply「{user} 回复了你在「{title}」中的回复」
plugin.notifications.floor_reply_owner「{user} 在「{title}」的楼中楼中回复了 {target}」(楼主专用,避免误称「你的回复」)
plugin.notifications.mentioned「{user} 在回复中提到了你,主题「{title}」」

六、后台设置项

配置存核心 settings 表,键前缀 floor_reply_。

键默认说明
floor_reply_notify_floor_author1通知楼层作者
floor_reply_notify_mention1通知被 @ 的人
floor_reply_notify_thread_owner0通知楼主(默认关)
floor_reply_auto_expand1打开帖子时自动展开已有楼中楼(通知深链不受此开关影响)

七、安全与规范

  • 提交受 RateLimiter 频控(3 次 / 30 秒)保护,防刷分漏洞
  • 回复内容入库前 strip_tags(),输出经 $this->e() / renderMentions() 转义,防 XSS
  • 全部 PDO 预处理绑定;ID 一律 (int) 强转
  • 后台设置页表单带 csrf 隐藏域,服务端 Csrf::verifyOrDie() 校验
  • 楼中楼仅占独立库,不写核心帖子表结构,卸载/禁用对核心数据零污染
  • 样式全部走 --mn-* 主题变量(含 fallback),双主题(浅色 / 暗夜星辰)可读
  • 表情:入库为 :code: 纯文本(与核心编辑器同格式),渲染时才转 <img>,天然防注入

(renderMentions() 的顺序固定为「转义 → @ 替换 → Emoji::parse()」,表情标签不会被二次转义)

  • ⚠️ 渲染 src 必须查 Emoji::EMOJI_MAP 取文件名:code 与文件名并非恒等(ok → OK.png,

Windows 大小写不敏感能跑、Linux 会 404)。表情数据接口(V1.3.1)吐的就是 EMOJI_MAP 后的文件名,

前端不再自己拼 code + '.png'

  • 表情面板 DOM(V1.3.1 起):由 script.js 首次点表情按钮时 fetch GET /floor-reply/emojis,

再在 JS 里建 DOM 挂到 body 常驻(htmx 整块换片段也清不掉)。

数据地址 / 面板 aria-label 由 hook/layout_head_end.php 挂在本插件 <script> 自身的

data-fr-emoji-url / data-fr-emoji-label 上,script.js 用 document.currentScript 读(取不到时回落默认值)

  • ⚠️ 单一数据源 = app/Helpers/Emoji.php(EMOJI_LIST + EMOJI_MAP):新增表情只改那一处,

接口与前端自动跟上,不需要重新生成任何产物

八、注意事项

  • 依赖 controller_view_before 一次性注入全帖楼中楼数据,避免每楼层一次查询
  • 删除楼层(post)时不自动清理其楼中楼(历史遗留记录保留)
  • 删除楼中楼不撤回已发出的通知(通知按「历史事件」处理)
  • 积分奖励 / 回复计数联动为核心约定,流程调整时需同步 Plugin.php 内相关方法
  • 改动 plugin.json 的 hooks 后必须重建 plugins/plugins_cache.json(删文件或调 Plugin::refresh()),否则新钩子不生效
  • 通知深链的页码换算口径与核心 Post::getByThread() 严格同桶同表同排序;若核心改了楼层排序规则,Post::pageOf() 需同步调整
  • 表情面板回归套件:.workbuddy-ai/tmp/fr_emoji_probe/test_picker.cjs(jsdom 真实 DOM + 真实脚本,

40 断言;--old 跑 1.3.0 旧脚本做 A/B 反证,须 21 条翻红)。改动表情面板前先跑它:

```bash

NODE_PATH=C:/Users/Administrator/.workbuddy-ai/binaries/node/workspace/node_modules \

node .workbuddy-ai/tmp/fr_emoji_probe/test_picker.cjs

```

依赖输入(可重抓):fr79b.html(帖子页)、fr_frag_1422.html(带登录态抓的楼中楼片段)、

fr_emoji.json(接口响应)、fr_ab_old_script.js(A/B 基线,从 1.3.0 发布包里抽出)

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

全部回复 (1)

🌳Lv.4 中级 🌛 见习会员
2026-10-26 00:56:35
1.3.1 这一版把「通知深链直达那一条楼中楼」补上了——没有深链的通知等于半成品,用户点进去还要自己翻楼层,这个改动价值很高。

三点(承接前面对 534 的意见):

1. mention_user_id 只记第一个、通知却覆盖全部被 @ 的人,会造成「B 收到通知但列表里 B 的名字没高亮」。建议要么全记(存 JSON),要么通知只发第一个,两边口径统一。
2. 「不占用核心楼层数」但调 Thread::incrementReplyCount,意味着核心 reply_count 会把楼中楼算进去。楼层页显示「第 N 楼」又是另一套数字,两者出现在同一页时用户会疑惑。建议在楼层列表底部注明「另有 N 条楼中楼回复」。
3. 数据存独立库,要确认清理逻辑:主帖被删除时,楼中楼里那些回复谁来删?没有级联清理会积累孤儿数据。

「@ 高亮 + 点击跳个人主页」这个交互已是标配,做对了。
#1 楼

请 登录