本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/docs/楼中楼通知升级方案_2026-09-21.md。
楼中楼(floor_reply)通知能力全面升级方案
文档性质:方案(未动一行代码,等指令执行)
日期:2026-09-21
适用版本:站点当前
FLINTHUB_VERSION = V1.0.26方案依据:全部结论均来自本次实测(读源码 + 查生产库),关键处标注
文件:行号
零、一页速览(决策摘要)
| # | 决策点 | 推荐 | 影响面 |
|---|---|---|---|
| 1 | 通知楼层作者 | ✅ 做 | 插件 1 处 + 核心 1 方法 |
| 2 | 通知被 @ 的人 | ✅ 做(支持多人) | 同上,零额外查询 |
| 3 | 通知楼主 | ❌ 默认不通知,留后台开关 | 插件 1 处判断 |
| 4 | 去重规则 | 每个收件人最多 1 条,优先级 @ > 楼层作者 > 楼主 | 纯逻辑,无额外 IO |
| 5 | 通知类型 | 启用已有 mention + 新增 floor_reply | 核心 2 处 + 三语文案 |
| 6 | 图标/配色 | 改核心 index.php 的 $iconMap/$colorMap(各补 3 行) | 顺带修好 reply_quote 缺失 |
| 7 | 深链定位 | 插件侧 ?fr= + 核心侧 ?pid=(同时修好既有 4 类通知) | 核心 2 文件小改(可选) |
| 8 | 插件后台开关 | ✅ 做(复用 SEO 插件范式) | 新增 2 文件 + 改 plugin.json |
| 9 | 发布形态 | update/27/ 增量包(含核心文件) | 沿用 update/26 体例 |
工作量:核心 5 文件(约 60 行)+ 插件 9 文件(约 350 行)+ 三语文案 6 键。
风险等级:低—中。唯一中风险项是第 7 项的核心改动(ThreadController::show()),但它可独立开关、可单独回滚。
一、现状核查(事实基线)
1.1 楼中楼通知:完全空白(本次升级的主因)
plugins/floor_reply/FrontController.php 的 reply()(:61-127)整条链路零通知调用。它做了 7 件事,唯独没有通知:
:67 Csrf::verifyOrDie()
:70 RateLimiter::hitConfig('reply', 3, 30)
:90 Plugin::parseMentions($content) ← 已拿到「全部」被 @ 用户
:98 $mentionUserId = (int)reset($mentions['exists']) ← 只取第一个
:105 Plugin::addReply(...)
:109 Thread::incrementReplyCount()
:111 Settings::runtimeIncr('total_posts')
:113 Points::markReplied()
:115 Points::award()
:118 PageCache::invalidate()生产库实测(plugins/floor_reply/data/floor_reply.sqlite):
| 指标 | 值 |
|---|---|
fr_replies 总条数 | 28 |
含 @(mention_user_id > 0) | 4 |
| 自己 @ 自己 | 0 |
| 单楼层最多回复数 | 5(post 1344) |
| 多人对话楼层 | 4 个(post 786 / 844 / 1045 / 1344) |
后果:三个场景全部静默——① 楼层作者不知道有人回了自己;② 被 @ 的人不知道被提到;③ 计数变了(incrementReplyCount)却没有任何通知,用户感知不一致。
1.2 引用回复通知:已实现,但有三个缺陷
链路完整且生产已验证:
thread/show.php:338 [data-action="quote"](带 data-quote-uid)
→ assets/js/thread.js:8-24 事件委托
→ assets/js/editor.js:2307 window.quotePost() 动态注入 #quoted_uids
→ ThreadController.php:185 读 $_POST['quoted_uids']
→ Notification.php:388 notifyQuote()生产库实证:notifications 表已有 6 条 reply_quote(id = 1711 / 1713 / 1715 / 1727 / 1903 / 2041,2026-09-14 ~ 09-19,收件人 269 / 1 / 267 / 275);protected/error.log 零 notifyQuote error。
| 缺陷 | 证据 | 性质 |
|---|---|---|
| a. 只有点「引用」按钮才发 | quoted_uids 纯 JS 注入,手打 @ 或 > 引用语法不发 | 设计边界,本方案不扩大 |
| b. 通知中心没有专属图标 | index.php:13-27 的 $iconMap 无 reply_quote → 落 ?? 'info' 通用感叹号 | 真缺陷,顺带修 |
| c. 注释过时 | Notification.php:22 写「预留:无引用检测,暂不触发」,实际早已触发 | 真缺陷,顺带修 |
1.3 通知基础设施:零钩子
| 文件 | 是否有 Hook |
|---|---|
app/Helpers/Notification.php | ❌ 无 |
app/Controllers/NotificationController.php | ❌ 无 |
app/Views/notifications/index.php | ❌ 无 |
app/Views/_components/notification_bell.php | ❌ 无 |
推论:新增通知类型的图标/配色无法纯靠插件实现,必须改核心视图(见 §五)。
(docs/插件开发规范.md §9.1 的 40 个钩子里,没有任何通知相关钩子。)
1.4 ⚠️ 深链缺陷(连带发现,影响既有全部 4 类通知)
现有通知的 link 一律形如 /thread/{tid}?pid={pid}#post-{pid}(Notification.php:219 / 262 / 397 / 441)。实测两个环节都是断的:
| 环节 | 现状 | 证据 |
|---|---|---|
?pid= | 被完全忽略 | ThreadController.php:220 只读 $_GET['page'];全项目无任何 pid → 页码换算逻辑 |
#post-{pid} | 从未被消费 | thread/show.php 楼层容器(:248-251)没有 id="post-{pid}";全页仅 id="replies"(:235)、id="post-body-{threadId}"(:150) |
结论:今天所有通知点进去都只到帖子第 1 页顶部,不会跳到目标楼层。 这是既有缺陷,不是本次引入的——但楼中楼要精准跳转,就必须先把它解决。
1.5 插件形态:无后台页
plugins/floor_reply/plugin.json 现状:"activated": true、"permissions": []、无 admin_url、无 admin_route_register 钩子、无 AdminController.php。即:楼中楼目前没有任何可配置项。
对照范式(plugins/seo,同项目先例):admin_url: "/admin/seo" + hook/admin_route_register.php + AdminController.php + permissions: ["route:admin", "system:settings"]。
1.6 数据模型与可用素材
notifications 表(app/SplitDB/Schema.php:563-576):
| 列 | 类型 | 本方案用法 |
|---|---|---|
user_id | INTEGER | 收件人 |
type | TEXT(20) | mention / floor_reply |
title | TEXT(500) | 通知主文案 |
link | TEXT(500) | 深链 |
actor_name | TEXT(100) | 触发者用户名 |
summary | TEXT(300) | 楼中楼内容摘要(比主题名更有信息量) |
related_id | INTEGER | post_id(楼层 ID) |
related_type | TEXT(20) | 'floor'(新语义,为将来按楼层聚合预留) |
agg_count | INTEGER | 首版固定 1,阶段二可用于聚合 |
已存在但未启用的类型常量(Notification.php:19-47):
| 常量 | 值 | 现状 |
|---|---|---|
TYPE_MENTION | mention | 预留,从未触发 ← 本方案启用 |
TYPE_REPLY_QUOTE | reply_quote | 已触发(注释错) |
核心图标表(TemplateCompiler.php:611+)已确认可用:chat / comment(均 f075)、user(f007)、quote(f10d)、forum(f086)。
⚠️ comments 不在表中 → plugin.json 现有 "icon": "comments" 是失效值(静默回落),顺带修。
主题色变量:--mn-primary / --mn-success / --mn-warning / --mn-error 在 9 个主题文件(assets/css/themes/modern-*.css)中均有定义,可安全使用。
二、设计目标与边界
目标
- 楼中楼回复 → 让该知道的人知道,且不重复打扰。
- 通知点进去 → 精准落到那一条楼中楼(而不是帖子第 1 页顶部)。
- 保留站长控制权 → 后台可开关、可静音。
- 顺带把引用回复通知的两个既有缺陷一起修掉。
非目标(明确不做,避免范围失控)
- ❌ 不做「楼内订阅」(同楼层的其他参与者不通知)
- ❌ 不做「手打
@即通知」的引用回复扩展(引用回复维持现状:仅按钮触发) - ❌ 不做邮件 / 推送 / WebSocket
- ❌ 不新增「通知设置」用户级偏好页(只做站长级开关)
- ❌ 不改
fr_replies表结构(零迁移)
三、通知对象与去重规则 ★核心决策
3.1 三个候选角色
设:用户 U 在某楼层 F(作者 A)中发了一条楼中楼,内容 @ 了 M,所在帖子楼主为 O。
| 角色 | 是否通知 | 理由 |
|---|---|---|
| M(被 @ 的人) | ✅ 总是 | 显式定向,最高优先级 |
| A(楼层作者) | ✅ 总是(A ≠ U) | "有人回了我的回复",对话的基本预期 |
| O(楼主) | ⚠️ 默认否,开关控制 | 见下 |
3.2 楼主为什么默认不通知
- 语义:楼中楼是「楼层内部的对话」,不是「对帖子的新回复」。核心已有
notifyReplyAggregated()负责「帖子有新回复」的聚合通知。 - 行业惯例:Discourse / Flarum / Discuz 均只通知「评论作者」,不通知「主题作者」——除非被 @ 或是评论作者。
- 噪音:实测 post 1344 单个楼层已有 5 条楼中楼;热门楼层会对楼主形成刷屏。
- 可回退:做成后台开关(默认关),站长想要就开。
⚠️ 已知代价:楼中楼会
incrementReplyCount,楼主看到回复数 +1 却无通知,存在轻微感知不一致。这是刻意的取舍,由开关交给站长决定。
3.3 去重规则(硬约束)
recipients = {}
if M > 0 且 M ≠ U: recipients[M] = 'mention' # 优先级 1
if A > 0 且 A ≠ U 且 A ∉ recipients: recipients[A] = 'floor_reply' # 优先级 2
if 开关开启 且 O > 0 且 O ≠ U 且 O ∉ recipients: recipients[O] = 'floor_reply_owner' # 优先级 3不变式:一次楼中楼回复,任一用户最多收到 1 条通知。
(U 永远不通知自己——与核心 notifyReply / notifyQuote / notifyVote 的既有约定一致。)
示例:
| 场景 | U | A | M | O | 收件人 |
|---|---|---|---|---|---|
| 普通回复 | 5 | 9 | – | 9 | 9(floor_reply) |
| 回复 + @楼主 | 5 | 9 | 3 | 3 | 9(floor_reply)、3(mention) |
| @了楼层作者本人 | 5 | 9 | 9 | 9 | 9(mention,只 1 条) |
| 楼主自己回自己楼层 | 9 | 9 | – | 9 | 无人 |
| 开楼主通知,回复他人楼层 | 5 | 9 | – | 3 | 9(floor_reply)、3(floor_reply_owner) |
3.4 多 @ 支持
现状:reply() 的 :98-101 只把第一个被 @ 用户存进 mention_user_id(该列仅用于列表高亮渲染)。
升级做法(推荐):通知时用全部,渲染仍用第一个。
// FrontController::reply() 已算出 $mentions['exists'] = [用户名 => uid](全部)
$mentionUids = array_values($mentions['exists']); // ← 零额外查询
Notification::notifyFloorReply($postId, $threadId, $uid, $safeContent, $mentionUids);✅ 不改表结构、不加查询、不影响渲染。
(阶段二可选:新增 mention_uids 列,让列表也显示全部 @ 对象。)
3.5 删除楼中楼时是否撤回通知
推荐:不撤回。 理由:通知是「历史事件」记录;撤回需要按 related_id + actor + 时间窗 反查,易误删且增加耦合。用户点进去看到内容已删,属正常。
(列为 §十三 待决项,若要撤回另议。)
四、通知类型与文案
4.1 类型方案
| 用途 | 类型 | 说明 |
|---|---|---|
| 楼中楼里被 @ | mention(启用已有常量) | Notification.php:21 常量已存在,注释「预留」→ 改为已启用 |
| 楼中楼回复了我的楼层 | floor_reply(新增常量) | 语义独立,避免污染 notifyReplyAggregated 的 type='reply' 聚合查询 |
| 楼主兜底(开关开启时) | floor_reply | 复用类型,文案不同 |
为什么必须新类型而不是复用
reply:notifyReplyAggregated()(:446-451)按type = 'reply' AND related_id = threadId AND related_type = 'thread'查未读并累加agg_count。若楼中楼也用reply,会被聚合进「N 条新回复」,且related_type语义冲突(帖子 vs 楼层)。
4.2 新增文案键(三语,lang/{zh,en,zh_tw}.php)
⚠️ i18n 红线:三语键集合零差异 + 键序一致(当前各 1340 键)。以下 3 键须在三个文件的同一位置插入。
| 键 | zh | en | zh_tw |
|---|---|---|---|
plugin.notifications.floor_reply | {user} 回复了你在「{title}」中的回复 | {user} replied to your reply in "{title}" | {user} 回覆了你在「{title}」中的回覆 |
plugin.notifications.floor_reply_owner | {user} 在「{title}」的楼中楼中回复了 {target} | {user} replied in "{title}" | {user} 在「{title}」的樓中樓中回覆了 {target} |
plugin.notifications.mentioned | {user} 在回复中提到了你,主题「{title}」 | {user} mentioned you in a reply, topic "{title}" | {user} 在回覆中提到了你,主題「{title}」 |
插入位置建议:紧邻现有 plugin.notifications.quote(lang/zh.php:49)之后,三语同步。
({title} = 主题标题前 40 字;{user} = 触发者用户名,空则回落 plugin.notifications.someone。)
4.3 字段取值规则
| 字段 | 取值 |
|---|---|
title | 上表文案(mb_substr 到 500,Notification::add() 内部已截断) |
summary | 楼中楼内容摘要(strip_tags 后前 40 字)← 与 reply/vote 用主题名不同,楼中楼更有信息量 |
link | /thread/{tid}?pid={pid}&fr={rid}#fr-{rid}(见 §六) |
actor_name | 触发者用户名 |
related_id | 楼层 post_id |
related_type | 'floor'(新语义,为阶段二按楼层聚合预留) |
agg_count | 1(首版不聚合) |
五、图标与配色
5.1 落点:改核心视图(唯一可行方案)
app/Views/notifications/index.php:13-40 的 $iconMap / $colorMap 各补 3 行:
// $iconMap 追加
'floor_reply' => 'chat', // 楼中楼(对话气泡,f075)
'mention' => 'user', // @ 提及(f007)
'reply_quote' => 'quote', // ★ 顺带修:既有缺失,原落 'info' 通用图标(f10d)
// $colorMap 追加
'floor_reply' => 'var(--mn-primary)',
'mention' => 'var(--mn-primary)',
'reply_quote' => 'var(--mn-primary)', // ★ 顺带修✅ 所有图标名已在 TemplateCompiler.php 图标表中确认存在;--mn-primary 在 9 个主题文件中均有定义(夜间主题自动适配)。
5.2 为什么不做成钩子
可以新增 notifications_icon_map 钩子(app/Views/notifications/index.php 是视图,符合 controller_view_before 的改造模式),但:
- 只为 3 个映射项引入 1 个新钩子 + 1 个插件钩子文件,性价比低;
- 新钩子一旦被注册就不能改名(
插件开发规范.md§9.1 注),等于永久 API 负担。
推荐直接改核心(3 行 × 2 处)。若你更希望「插件零侵入核心」,可改用钩子方案,代价是多 2 个文件。
六、深链定位策略 ★核心决策
6.1 目标链接形态
/thread/{tid}?pid={pid}&fr={rid}#fr-{rid}
↑ ↑ ↑ ↑
帖子 楼层ID 楼中楼ID 楼中楼条目锚点三段各解决一个问题:?pid= → 跳到正确页码;?fr= → 展开正确楼层的楼中楼并定位到具体那一条;#fr-{rid} → 原生滚动兜底。
6.2 三个方案对比
| 方案 | 做法 | 优 | 劣 | 结论 |
|---|---|---|---|---|
| A. 只跳帖子 | 维持现状,link 只带 tid | 零改动、零风险 | 点进去还是看不到内容 → 通知等于废掉 | ❌ 不可接受 |
| B. 纯插件 JS | 插件 JS 读 ?fr=,自己 fetch 算页码后 location.replace() 重载 | 不碰核心 | 需要 2 次页面加载(闪烁);#post-{pid} 锚点仍缺;既有 4 类通知照样跳不准 | ⚠️ 次选 |
| C. 核心+插件分工(推荐) | 核心支持 ?pid= 算页码 + 楼层加 id="post-{pid}";插件支持 ?fr= 展开定位 | 一次加载即到位;同时修好既有 4 类通知;每处改动都很小 | 需改 2 个核心文件 | ✅ 推荐 |
6.3 推荐方案(C)的实现要点
核心侧(2 文件,约 25 行)
① app/Controllers/ThreadController.php — show() 在 :220 之前插入页码推导:
// ?pid= 深链:把「回复 ID」换算为「所在页码」,优先于 ?page=
// 口径必须与 Post::getByThread() 完全一致(同桶同表同排序),否则页码会漂
$page = max(1, min($totalPages, (int)($_GET['page'] ?? 1)));
$pidHint = (int)($_GET['pid'] ?? 0);
if ($pidHint > 0) {
$p = $postModel->pageOf($threadId, $pidHint, $perPage); // 见 ②
if ($p > 0) $page = min($totalPages, $p);
}② app/Models/Post.php — 新增 pageOf():
/**
* 回复 ID → 所在页码(口径与 getByThread 严格一致:同桶同表同排序)
* @return int 0 = 该回复不存在或已删(调用方保持原页码)
*/
public function pageOf(int $threadId, int $postId, int $perPage): int
{
$idx = $this->fetchThreadIndex($threadId);
if (!$idx) return 0;
$bdb = Schema::bucketFromPath($idx['bucket_path']);
$st = $bdb->prepare('SELECT create_time FROM reply WHERE id = ? AND pid = ? AND status = ?');
$st->execute([$postId, $threadId, self::STATUS_NORMAL]);
$ct = $st->fetchColumn();
if ($ct === false) return 0; // 不存在/已删 → 不跳
$st2 = $bdb->prepare(
'SELECT COUNT(*) FROM reply WHERE pid = ? AND status = ? AND (create_time < ? OR (create_time = ? AND id < ?))'
);
$st2->execute([$threadId, self::STATUS_NORMAL, $ct, $ct, $postId]);
return (int)floor(((int)$st2->fetchColumn()) / max(1, $perPage)) + 1;
}✅ 为什么不用
reply_index算:getByThread()读的是桶内reply表(Post.php:252-257),而reply_index是主库全局索引。两者理论上可能漂移(项目已知有计数器漂移史)。同桶同表同排序才能保证页码与实际渲染逐字节一致——这也顺带规避了idx_reply_index_*的索引选择风险。
③ app/Views/thread/show.php — 楼层容器(:248)补 id:
⚠️ 注意 #replies(:235)带 hx-boost + hx-target="#replies"。锚点加在循环内的楼层容器上,htmx 局部替换 #replies 时会随之重建——这是期望行为。
插件侧(3 文件)
① FrontController::list() — 支持 ?focus={rid}:
$focus = (int)($_GET['focus'] ?? 0);
// 若 focus 命中本楼层,则强制渲染该条所在页(而非第 1 页),并标记高亮
② Plugin.php — 新增 pageOfReply():
/** 楼中楼 ID → 所在页(口径与 getByPost 一致:post_id + id ASC) */
public static function pageOfReply(int $postId, int $replyId, int $perPage = 5): int
③ assets/script.js — 深链自动展开:
页面加载 →
读 location.search 的 pid / fr
若 pid 命中某楼层锚点 #fr-anchor-{pid}:
自动触发该楼层的 htmx 展开(带 focus=fr)
等 htmx:afterSwap 后:
高亮 [data-fr-id="{fr}"](加 .fr-focus 类,2 秒后淡出)
scrollIntoView({block:'center'})
④ assets/style.css — 高亮样式(**只能用既有 --mn-* 变量**):
.fr-item.fr-focus { background: var(--mn-bg-row-hover); outline: 2px solid var(--mn-primary); }
6.4 风险与回滚
风险 缓解 ?pid= 与 ?page= 冲突pid 优先,page 作回退;pid 非法/已删则静默用 page(pageOf 返回 0)老链接(无 pid) 完全不受影响($pidHint = 0 直接跳过) 分页链接被污染 pagination() 生成的分页链接不含 pid → 翻页后回到正常分页,符合预期回滚 核心 3 处改动互相独立,任一可单独还原;pageOf() 是新增方法,删除即无副作用
⚠️ 附加收益:方案 C 落地后,既有 4 类通知(reply / vote / reply_quote / reply_aggregated)的深链同时被修好(它们本就在拼 ?pid=…#post-,只是没人消费)。这是本方案最划算的一笔。
七、插件后台开关(可选但推荐)
7.1 范式(照搬 plugins/seo)
文件 动作 plugin.json加 "admin_url": "/admin/floor-reply";permissions 改 ["route:admin", "system:settings"] hook/admin_route_register.php新建:注册 GET /floor-reply + POST /floor-reply/save AdminController.php新建:读/写开关 views/admin.php新建:后台表单 lang/{zh,en,zh_tw}.php加后台文案键
7.2 开关项建议
键(存核心 `settings`) 默认 说明 floor_reply_notify_floor_author1通知楼层作者 floor_reply_notify_mention1通知被 @ 的人 floor_reply_notify_thread_owner0通知楼主(默认关) floor_reply_auto_expand1深链自动展开楼中楼
配置写入:Settings::update($key, $value, 'floor_reply') —— 显式传插件名,走 system:settings 权限校验(Settings.php:255-271,SEO 插件同款先例)。
7.3 ⚠️ 部署必带 plugins_cache.json
plugin.json 改动(新增 admin_url / permissions / 新钩子)不会自动生效 —— 必须一并更新 plugins/plugins_cache.json(update/25 正是这么做的)。漏了会导致后台入口不出现。
八、顺带修正项(低成本、高确定性)
# 问题 位置 修法 1 说明文档带 BOM(ef bb bf) plugins/floor_reply/说明.md 首 3 字节去 BOM(项目约定 UTF-8 无 BOM + LF) 2 说明文档路由写错 同上「三、目录结构」章节 实际为 /floor-reply/reply、/floor-reply/list/{id}、/floor-reply/delete;文中写的 /floor-reply 与 /floor-reply/list 不存在 3 TYPE_REPLY_QUOTE 注释过时Notification.php:22「预留:无引用检测,暂不触发」→「引用回复已启用(V1.0.26 起生产已验证)」 4 TYPE_MENTION 注释过时Notification.php:21「预留:无 @ 解析,暂不触发」→「楼中楼 @ 提及(本版启用)」 5 plugin.json 图标失效plugins/floor_reply/plugin.json"icon": "comments" → "chat"(comments 不在核心图标表,静默回落)6 reply_quote 通知无图标index.php:13-40见 §5.1(已含)
九、改动清单(文件级)
核心(5 文件)
文件 改动 行数 app/Helpers/Notification.php新增 TYPE_FLOOR_REPLY 常量;新增 notifyFloorReply();改 2 处过时注释 ~55 app/Views/notifications/index.php$iconMap +3、$colorMap +36 app/Controllers/ThreadController.phpshow() 支持 ?pid=~6 app/Models/Post.php新增 pageOf() ~18 app/Views/thread/show.php楼层容器加 id="post-{pid}" 1 lang/{zh,en,zh_tw}.php各 +3 键(同位置同序) 9
插件(9 文件)
文件 改动 状态 plugins/floor_reply/FrontController.phpreply() 加通知调用;list() 支持 ?focus=改 plugins/floor_reply/Plugin.php新增 pageOfReply() 改 plugins/floor_reply/assets/script.js深链自动展开 + 高亮 改 plugins/floor_reply/assets/style.css.fr-focus 样式改 plugins/floor_reply/views/list.php高亮标记 改 plugins/floor_reply/plugin.jsonadmin_url / permissions / icon 修正 / 新钩子改 plugins/floor_reply/AdminController.php后台开关读写 新建 plugins/floor_reply/hook/admin_route_register.php注册后台路由 新建 plugins/floor_reply/views/admin.php后台表单 新建 plugins/floor_reply/lang/{zh,en,zh_tw}.php后台文案键 改 plugins/floor_reply/说明.md去 BOM + 修路由 + 补通知章节 改 plugins/plugins_cache.json重新生成 改
十、验证方案
层 手段 判据 语法 php -l(8.0.2 + 8.2.9)、node --check全 PASS 编码 0 BOM / 0 CRLF / UTF-8 全 PASS i18n 三语键集合差异 0 + 键序一致 + 模板引用键全量比对 1340 → 1343,三语一致 逻辑(桩) Node DOM 桩:pageOf 边界(首条 / 末条 / 不存在 / 已删) 断言全过 去重 构造 §3.3 全部 5 种场景,断言收件人集合与类型 与表逐条一致 E2E php -S + 手写 sessions 行登录态,实发楼中楼 → 查 notifications 表行数/类型/link 正确 深链 点通知 → 断言落在正确页码 + 楼层展开 + 条目高亮 视觉 + ?pid 生效 回归 verify_b7_csrf.php / verify_d6_cache.php / test_p5_content_img.php / test_p8d_img_group.php / test_trash_preview.sh零回归 A/B 反证 核心 ?pid= 改动前后各跑一次深链,比对落点 反证成立后 sha256 还原一致 清理 访客会话 0、测试通知行删除、页面缓存清空、php -S 停止 无残留
⚠️ 测试通知会写入生产 business.sqlite.notifications —— 必须记录插入的 id 并在测后删除,避免污染真实用户的通知中心。
十一、发布形态
推荐:update/27/ 增量包(因核心文件也改了)。
沿用 update/26 体例,包内镜像站点根结构:
update/27/
├── app/
│ ├── Controllers/ThreadController.php
│ ├── Helpers/Notification.php
│ ├── Models/Post.php
│ └── Views/{notifications/index.php, thread/show.php}
├── lang/{zh,en,zh_tw}.php
├── plugins/
│ ├── plugins_cache.json
│ └── floor_reply/**
├── version.php ← V1.0.27
└── 改动说明.md
部署注意(沿用既有红线):
- 只传「代码 + 新增的
md_*」;data/ 与 assets/uploads/ 绝不覆盖; - 不要传
sessions.sqlite(会把全站踢下线); - 部署后必须走后台「维护 → 清理缓存」(
data/runtime/pages/*.html 是永久缓存); - 插件新增
AdminController.php + 新钩子 → 必须同时更新 plugins_cache.json; sw.js 的 CACHE_VERSION 由 SW_VERSION 派生 → 若前端资源有改动,version.php 升版即可自动破缓存。
若你希望「只发插件、不升核心版本」:可改走 update/25 的纯插件包先例(update/25 就只有 plugins/ + 改动说明.md,无 version.php)。但那样 §六 的核心深链修复必须放弃或延后——通知点进去仍跳不准。不推荐。
十二、分批建议
批次 内容 价值 风险 第 1 批(必做) §三 通知逻辑 + §四 文案 + §五 图标 + §八 顺带修正 楼中楼从「完全静默」到「有人知道」 低 第 2 批(推荐) §六 深链(核心 ?pid= + 楼层锚点 + 插件 ?fr=) 通知从「知道」到「一键到位」;顺带修好既有 4 类通知 中(核心改动,可独立回滚) 第 3 批(可选) §七 后台开关 站长可控 低
建议 1 + 2 一起做:第 1 批产出的通知如果不带第 2 批的深链,用户体验是「收到通知 → 点进去 → 看不到内容」,等于半个废功能。
十三、待你决策的 5 个问题
- 楼主通知:同意「默认不通知 + 后台开关」吗?(§3.2)
- 深链:第 2 批(核心
?pid=)做不做?它同时修好既有 4 类通知,但需改 2 个核心文件。(§六) - 后台开关:第 3 批做不做?(§七)
- 多 @ 支持:确认「通知全部被 @ 的人、渲染仍只显示第一个」?(§3.4)
- 发布形态:
update/27/ 增量包(含核心)还是纯插件包?(§十一)
附:本次核查使用的证据来源
结论 证据 楼中楼零通知 FrontController.php:61-127 全文生产 28 条 / 4 条带 @ / 0 条自 @ 直查 plugins/floor_reply/data/floor_reply.sqlite 引用回复已发通知 生产 notifications 表 6 条 reply_quote 通知基础设施零钩子 4 个文件全文 + 插件开发规范.md §9.1(40 钩子表) ?pid= 被忽略ThreadController.php:220 只读 $_GET['page']#post-{pid} 无消费方grep id="post- 于 thread/show.php → 仅 post-body-{threadId}reply_quote 无图标index.php:13-27 $iconMap 无该键 → 落 ?? 'info'comments 图标不存在TemplateCompiler.php:611-760 图标表--mn-primary 等 9 主题均定义assets/css/themes/modern-*.css:12,43-45notifications 表结构app/SplitDB/Schema.php:563-576插件后台页范式 plugins/seo/{plugin.json,hook/admin_route_register.php,AdminController.php}纯插件包先例 update/25/ 仅含 plugins/ + 改动说明.mdPost::getByThread() 口径app/Models/Post.php:243-260(桶内 reply 表,create_time ASC, id ASC)