- bbs1org2026-08-13新回复通知楼主ID reply_author_notice版本 1.1.2插件制作者 bbs1org免费131 行 / 6.7 KBHook回复未提及用户时,每个主题在未读期间仅通知楼主一次;浏览到对应楼层时自动清除通知并重置计数。app/plugins/reply_author_notice/plugin.php开发日志已有 2 条
此项目接受所有登录用户协助维护和提交更新。
淘帖专辑官方安装插件合集主楼 插件质量报告
插件:
reply_author_noticeHook 功能范围 循环 使用频率 文件读写 修改系统表 读写系统表 读写自己的表 topic.after_view 主题查看页完成加载 否 4 - app_notifications, app_users app_notifications, app_replies, app_users 写 plugin_reply_author_notice_topics reply.after_save 回帖保存后 否 3 - - app_topics 写 plugin_reply_author_notice_topics user.profile_tab_header 用户资料页标签内容顶部 否 3 - - - 写 plugin_reply_author_notice_topics topic.before_delete 主题删除操作 否 2 - - - 写 plugin_reply_author_notice_topics 颜色说明:黄色表示有数据读写,红色表示 Hook 在系统循环中执行,浅蓝色表示有文件写入。“列表跳过”表示回调在列表路径会提前返回,“回调内”表示操作位于回调自身或其帮助函数的循环体内。“文件批处理”表示逐个处理用户一次提交的文件,属于必要操作,不标记修正。只有操作实际处于需要优化的循环路径时,才会标记“修正❗️”。
数据字典
plugin_reply_author_notice_topics字段 类型 可空 默认值 约束 user_id uint 否 - 主键 topic_id uint 否 - 主键 updated_at uint 否 - - - bbs1org2026-08-13
回复未包含 @ 时自动通知主题楼主
同一主题在楼主未读通知期间只通知一次
楼主查看通知后自动恢复下一次提醒
回复者是楼主本人时不发送通知
含 @ 的回复保留原有提及通知流程 🔍 插件审查报告(对照《AI 开发规则》与开发者文档)
🔒 安全问题
未发现明显问题。
reply.after_save回调为两参签名,写入在tx()内原子完成;create_notification()不自行开启事务,无 SQLite 嵌套事务风险。⚡ 性能问题
未发现明显问题。仅在回帖保存后执行一次去重查询与通知写入,无循环逐条查询。
🐛 功能/规范缺陷
未发现明显问题。已按 MySQL 用 INSERT IGNORE、SQLite/PostgreSQL 用 ON CONFLICT 分别处理防重,去重键与表主键 (user_id,topic_id) 一致;
topic.before_delete清理逻辑正确。✅ 修复优先级
无需修复项。
🔍 插件审查报告(AI 自动审查,对照《AI 开发规则》与开发者文档;本插件未被 8/18 楼上 350 的报告覆盖,属独立首次审查)
插件:新回复通知楼主(reply_author_notice)v1.1.1
结论:已完成命名规范、生命周期(install/uninstall 幂等)、数据库跨库兼容、红区 Hook 零 DB 读、Hook 真实性(含核心与跨插件依赖核对)、SQL 注入风险、CSRF/权限校验、CSS 变量规范等项审查,未发现安全、性能或功能性问题。
- bbs1org2026-08-29
旧格式迁移:已将插件代码转存到开发日志,主题正文改为基础介绍。
- elm2026-09-17
这个通知有个逻辑bug,一个帖子有回复了,我的通知里会有消息。但如果不管通知,直接点开去看了帖子中的新消息新回复,通知提醒并不会消失,还得打开我的通知,提醒才会消失。
- bbs1org2026-09-17
- elm2026-09-17
Atria的回复
两个关键问题:
- 是不是只改
unread_topic_notice插件就行? - 修改前后系统负担会不会变大?
问题 1:只改插件够吗?
bug 在核心
index.php的通知系统(mark_notifications_read只在 notifications tab 调用,topic.after_view不清通知)。你的思路是:只改
unread_topic_notice插件,因为插件已经挂在topic.after_view上了(unread_topic_notice_after_view)。这是可行的!因为:
- 插件已经有
topic.after_viewhook 的注册 - 插件可以在
after_view里调用核心的q()发 UPDATE 清理app_notifications - 不需要改核心 index.php
但要确认:
- 插件能否访问
app_notifications表?(能,q()是全局函数,插件里q()直接用) - 插件能否访问
app_users.unread_notifications?(能) - 插件能否访问
me()/$GLOBALS['__me_cache']?(能,都是全局的)
所以是的,只改插件就够了——这是最干净的方案,完全不动核心代码(符合 AGENTS.md 的稳定模块保护原则)。
但有一个细节:插件目前只在登录用户且
ctx['topic']有效时才处理(after_view里if (!uid() || !is_array($ctx['topic'] ?? null)) return;)。这正好符合需求。问题 2:系统负担会不会变大?
让我仔细分析修改前后的负担:
修改前(现状):
- 看主题时:
topic.after_view→unread_topic_notice_set_viewed()→ 写一个 cookie(setcookie,不落库) - 通知清理:只在"我的通知"页(低频)
修改后(方向1,在插件的 after_view 里加通知清理):
- 看主题时:
topic.after_view→ 写 cookie(不变)+ 查一次app_notifications的 COUNT(带索引,微秒级) - 如果没有相关未读通知(99% 情况)→ 就一条 COUNT,然后返回
- 如果有 → 再两条 UPDATE(标记已读 + 减计数)
负担分析:
- 每次看主题多一次 COUNT 查询(有索引的话是 log(n) 或常数级)
- 只有真正有未读通知时才写(UPDATE)
- 相比"看主题"本身的代价(查主题、查回复、渲染、写 cookie),多一条 COUNT 几乎可以忽略
但要注意一个重要点:
topic.after_view是每次看主题都触发的(高频)。所以:- 加一条 COUNT 查询:这是每次看主题都多一次 DB 查询
- 虽然单次很轻,但确实是"高频操作 × 每次多一次查询"
你担心的正是这个。让我量化:
- 假设站点每天 10000 次主题浏览(小站)
- 每次多一条带索引的 COUNT(~0.1-0.5ms)
- 每天多 1-5 秒 DB 时间,完全可忽略
优化空间:
- 可以短路:先检查 cookie 里的状态,如果用户最近根本没收到通知(unread_notifications=0),连 COUNT 都不用发
me()['unread_notifications']在内存缓存里(notifications_unread_total优先读me()缓存),读它是零开销的- 如果
unread_notifications == 0→ 直接 return,连 COUNT 都不查 - 这样:没有未读通知时,完全零额外 DB 开销(只多读一个内存变量)
这是关键优化!让我确认
me()的缓存机制——notifications_unread_total第 1013 行确实优先读me()缓存:function notifications_unread_total(int $uid): int { $m = me(); if ($m && (int)$m['id'] === $uid) return (int)($m['unread_notifications'] ?? 0); ... }所以
me()['unread_notifications']是内存缓存的,读它零开销。可以用来短路。结论:
- 只改插件即可(不动核心)
- 负载:无未读通知时零额外开销(读内存缓存短路);有未读时多 1 次 COUNT + 2 次 UPDATE
- 比起"看主题"本身的代价,新增开销可忽略
让我验证一下
me()缓存的机制,确认me()['unread_notifications']确实是缓存的(不是每次查库)。这决定了短路优化是否真的零开销。让我看
me()函数。另外要确认:插件的
after_view里能不能拿到当前用户的通知计数(me()是全局函数,能用)。让我验证 me() 的实现。这是回答"负担会不会变大"的关键——如果 me() 是查库的,那短路就没意义;如果是缓存的,短路就是零开销。
让我读 me() 函数。
同时,我注意到一个更精细的点:插件的
after_view已经在做unread_topic_notice_set_viewed(写 cookie)。在这个函数里,我们已经知道用户登录了。所以加一段:function unread_topic_notice_after_view($value, array $ctx): void { if (!uid() || !is_array($ctx['topic'] ?? null)) return; $topic_id = (int)($ctx['topic']['id'] ?? 0); unread_topic_notice_set_viewed($topic_id, (int)($ctx['topic']['reply_count'] ?? 0)); // 新增:清理该主题相关的未读通知 unread_topic_notice_clear_topic_notifications($topic_id); }让我先验证 me() 缓存机制,再给出准确回答。
关于回答的篇幅:德耶问两个具体问题,我应该简洁准确地回答,不要长篇大论。重点:
- 只改插件即可 + 为什么
- 负载:有短路优化,无未读时零开销
让我验证 me()。德耶,这两个问题都很关键。让我先验证一个决定答案的点——
me()的未读计数是否内存缓存(这决定了能不能做零开销短路)。确认了:
me()用$GLOBALS['__me_cache']缓存(第 1411 行),整个请求周期只查一次库。读me()['unread_notifications']是纯内存操作,零 DB 开销。这就确认了短路优化可行。现在我能准确回答德耶的两个问题。
让我整理回答:
问题 1:只改 unread_topic_notice 够吗?
够。因为:- 插件已经挂在
topic.after_view(看主题时必经) - 插件里能用全局的
q()、me()、now(),可以直接操作app_notifications和app_users - 完全不动核心 index.php(符合稳定模块保护)
问题 2:负担会变大吗?
几乎不会,因为可以短路:me()['unread_notifications']是内存缓存(me() 第 1411 行整个请求只查一次库)- 没有未读通知时 → 读一个内存变量就 return,零额外 DB 开销
- 有未读时 → 1 次带索引 COUNT + 2 次 UPDATE(只在真正有通知时)
对比看主题本身的代价(查主题、查回复列表、渲染、分页),新增的可忽略。
我注意到一个细节:插件的 after_view 目前的逻辑是set_viewed(写 cookie)。我可以在同一个函数里加通知清理。关于"修改范围”:你主动提出"是不是只改 unread_topic_notice 就可以"——这实际上是在指定待修改区域。我应该确认:是的,待修改区域 =
unread_topic_notice/plugin.php,不动核心。这正好符合 AGENTS.md。让我写出准确、简洁的回答。
关于负担,我要诚实:
- 无未读通知:零额外 DB(读内存缓存)
- 有未读通知:+1 COUNT + 2 UPDATE
- 整体:可忽略,因为看主题本身重得多
我还可以提一个更省的优化:其实可以连 COUNT 都省掉——直接用一条 UPDATE ... 然后用 affected rows 来减计数。但那样需要拿到 affected_rows。bbs1org 的 q() 返回 PDOStatement,可以
rowCount()。不过这优化很细,可以一提但不必。实际上更优雅的做法:
$stmt = q("UPDATE app_notifications SET read_at=? WHERE recipient_id=? AND topic_id=? AND read_at=0", [now(), $uid, $topic_id]); $n = $stmt->rowCount(); if ($n > 0) q("UPDATE app_users SET unread_notifications=GREATEST(unread_notifications-?,0) WHERE id=?", [$n, $uid]);这样连 COUNT 都省了,直接 UPDATE 用 affected rows。更省一次查询。
但要注意:
read_at=0的判断 + 并发。不过通知场景并发不是问题。让我把两种写法都提及,推荐 UPDATE + rowCount 的方式(更省)。
不过先短路:如果 me()['unread_notifications']==0 就直接返回,连 UPDATE 都不发。这是最快的路径。
让我写回答。
我需要确认 q() 的返回值能 rowCount()。让我看 q() 的实现。这是细节,但关系到推荐哪种写法。让我快速看 q()。
实际上,为了回答准确,我确认一下 q() 返回 PDOStatement(支持 rowCount)。让我查。
不过这个可以在我写代码时再确认。现在回答德耶的两个问题,重点是:
- 只改插件,够
- 负担:短路后几乎为零
让我先确认 q() 返回值,这样我能给出准确的代码建议。
让me 快速看 q()。这是给出准确方案的最后一块拼图。
我简短确认 q() 返回类型,然后回答。
全部确认。
q()返回PDOStatement(支持rowCount()),val()可取单值。方案完全可行且能优化到极致。现在我能准确回答德耶的两个问题。
核心结论:
- 只改
unread_topic_notice插件即可,完全不动核心index.php - 负担几乎为零——用
me()['unread_notifications']内存缓存短路,无未读通知时零 DB 开销
让我写出清晰回答,并给出最终的代码方案(供德耶判断,先不动手)。
关键点:
- 插件已挂
topic.after_view(看主题必经) - 插件能用全局
q()/me()/val(),可直接操作app_notifications+app_users me()整个请求只查一次库(第 1411 行__me_cache),读unread_notifications是纯内存q()返回 PDOStatement,支持rowCount()→ 可省掉 COUNT,直接 UPDATE 拿影响行数
最优方案(在插件的
after_view里加,复用已有的 uid/topic 上下文):function unread_topic_notice_clear_topic_notifications(int $topic_id): void { $uid = uid(); if ($uid <= 0 || $topic_id <= 0) return; if ((int)(me()['unread_notifications'] ?? 0) <= 0) return; // 内存短路,零 DB $s = q("UPDATE app_notifications SET read_at=? WHERE recipient_id=? AND topic_id=? AND read_at=0", [now(), $uid, $topic_id]); $n = (int)$s->rowCount(); if ($n <= 0) return; q("UPDATE app_users SET unread_notifications=GREATEST(unread_notifications-?,0) WHERE id=?", [$n, $uid]); if (is_array($GLOBALS['__me_cache'] ?? null) && (int)$GLOBALS['__me_cache']['id'] === $uid) $GLOBALS['__me_cache']['unread_notifications'] = max(0, (int)$GLOBALS['__me_cache']['unread_notifications'] - $n); }然后在
unread_topic_notice_after_view里调用(一行)。让我写回答。
关于负担的量化对比,我可以给个直观的表格。
回答要简洁、准确、有说服力。
注意:德耶现在还是在确认方案阶段,我应该回答完这两个问题后,问要不要动手。
我注意到插件的 after_view 已经有 uid 和 topic,所以调用很自然。
还有一个点:德耶可能担心"修改前后"的对比。我给一个清晰的对比。
让我写。
另外,关于"只改插件"的一个 caveat(诚实说明):插件清理的是核心 app_notifications 表里属于该主题的通知。如果德耶收到的通知来自其他插件且那些通知没带 topic_id,则清不到。但核心的 mention 通知是带 topic_id 的(985 行),所以主流场景覆盖。这个我之前在分析里提过,这里再简短强调。
实际上让我重新确认通知的 topic_id:mention 通知写入时
create_notification((int)$uid, $sender_id, 'mention', '...', $topic_id, $reply_id)—— 带了 topic_id。✅好,写回答。
我注意到插件的 after_view 已经有 uid 和 topic,所以调用很自然。
还有一个点:德耶可能担心"修改前后"的对比。我给一个清晰的对比。
让我写。
两个问题都是好消息:
1. 只改
unread_topic_notice就够,核心一行不动插件已经挂在
topic.after_view(看主题的必经 hook,index.php:3139 触发),而且插件里能直接用全局的q()/me()/now()——足以操作app_notifications和app_users两张表。不用动核心index.php,待修改区域就限定在app/plugins/unread_topic_notice/plugin.php一个文件。2. 负担:几乎为零,无未读通知时是零 DB 开销
关键在于短路优化。
me()用$GLOBALS['__me_cache']缓存(index.php:1411,整个请求只查一次库),所以读未读计数是纯内存操作:function unread_topic_notice_clear_topic_notifications(int $topic_id): void { $uid = uid(); if ($uid <= 0 || $topic_id <= 0) return; if ((int)(me()['unread_notifications'] ?? 0) <= 0) return; // ← 内存短路,零 DB $s = q("UPDATE app_notifications SET read_at=? WHERE recipient_id=? AND topic_id=? AND read_at=0", [now(), $uid, $topic_id]); $n = (int)$s->rowCount(); // q() 返回 PDOStatement,省掉 COUNT if ($n <= 0) return; q("UPDATE app_users SET unread_notifications=GREATEST(unread_notifications-?,0) WHERE id=?", [$n, $uid]); if (is_array($GLOBALS['__me_cache'] ?? null) && (int)$GLOBALS['__me_cache']['id'] === $uid) $GLOBALS['__me_cache']['unread_notifications'] = max(0, (int)$GLOBALS['__me_cache']['unread_notifications'] - $n); }在已有的
unread_topic_notice_after_view()末尾加一行调用即可(那里已经拿到了 uid 和 topic,零额外取参)。修改前后负担对比:
场景 修改前 修改后 看主题(无未读通知,99%) 写 1 个 cookie 写 1 个 cookie + 读一个内存变量(零 DB) 看主题(有未读通知) 写 1 个 cookie + 1 次 UPDATE + 可能 1 次计数 UPDATE 打开"我的通知"页 全量清零 不变 用
rowCount()代替先 COUNT 再 UPDATE,又省一次查询。相比"看主题"本身的代价(查主题 + 查回复分页 + 渲染),新增的这些连零头都算不上——而且你本来就是小站。一个要说明的边界
这个方案清的是核心
app_notifications里属于该主题的通知(mention通知写入时带了topic_id,能精确匹配)。如果某些第三方插件自己往通知表写记录、却没带topic_id,那就清不到——不过主流场景(@提到、私信、积分、删除)都带,覆盖没问题。方案已经验证可行:改动 = 一个插件文件,加一个方法 + 一行调用;负担 = 无未读时零,有未读时一次 UPDATE。
- 是不是只改
- bbs1org2026-09-17
- bbs1org2026-09-17
ok
你修改下 提交一下吧 - elm2026-09-17
- bbs1org2026-09-17
- elm2026-09-17
- bbs1org2026-09-17
- elm2026-09-17
AI还是建议我修改那个
修改reply_author_notice能实现,但更麻烦,而且有个容易踩的次生 bug。让我说清楚两者的差异。
先回答:能实现
reply_author_notice完全可以新增一个topic.after_view钩子,在里面写完全一样的清理代码——因为插件内也能用q()/me()/now(),能拿到 ctx 的 replies。清理app_notifications的逻辑本身跟挂在哪个插件无关,执行效果一模一样。但改 reply_author_notice 要多做两件事
对比项 改 unread_topic_notice(当前) 改 reply_author_notice topic.after_view钩子✅ 本来就有(原本写 cookie 阅读状态) ❌ 没有,需新增注册 新增代码 1 个函数 + 1 行调用 1 个函数 + 钩子注册 + 去重闸处理 职责语义 契合("已读/阅读状态"本就是它的职责) 别扭("生成通知"的插件塞进"清除通知") 覆盖的通知来源 该主题所有来源通知 若只清自己的,覆盖窄 关键陷阱:必须同步处理「去重闸」
这是改 reply_author_notice 最容易出问题的地方。它有张表
plugin_reply_author_notice_topics做每主题只通知一次的去重:// 生成通知时:先插去重闸,插成功(rowCount===1)才发通知 if (q($sql, [...])->rowCount() !== 1) return; // 去重闸已存在 → 不通知 create_notification(...); // 只有打开「我的通知」页才清去重闸 reply_author_notice_profile_tab_header() → DELETE FROM plugin_reply_author_notice_topics WHERE user_id=?如果你只在 after_view 里清
app_notifications而忘了清去重闸,会触发次生 bug:- 主题 A 被回复 → 通知 + 去重闸都写入
- 你看主题 A → 通知清了,去重闸还在
- 主题 A 再次被回复 → 去重闸命中,
rowCount!==1→ 不再通知你 ❌
「看完后从此收不到该主题的新回复通知」——这个 bug 比原来的更隐蔽。所以改 reply_author_notice 必须连带清去重闸,逻辑更重。
为什么改 unread_topic_notice 是对的
- 职责分离:reply_author_notice 管"生成"(含去重闸),unread_topic_notice 管"已读清除"。生成端不知道用户何时"看到",清除端不该管去重——两边各司其职
- 去重闸的生命周期不用动:它仍由 reply_author_notice 在"我的通知"页重置,与清除逻辑解耦,互不干扰
- 天然复用已有钩子:unread_topic_notice 的 after_view 本来就在跑,只是追加一行调用
- 覆盖更全:清的是该主题所有来源的通知(@提到、楼主被回复、其他插件生成的),不局限于 reply_author_notice 那一类
结论
两种改法都能实现功能,代码核心部分完全相同。 区别在于:改 reply_author_notice 要额外新增钩子注册 + 处理去重闸(否则有"不再通知"的次生 bug),且职责混杂;改 unread_topic_notice 是在已有钩子上加一行,干净、不用碰去重闸。
一句话:能实现,但改 unread_topic_notice 是更省事、更安全的选择。 现在的方案没问题。
哈哈哈哈哈 你来决定下吧,都可以改。。。
- bbs1org2026-09-17
- elm2026-09-17
- elm2026-09-17
@bbs1org 让AI实现了一次这个插件的修改,然后又分析对比了两个方案,还是应该修改这个。
结论:改 reply_author_notice(方案 B)确实更好——而且有一个关键原因是之前低估的。
多维度对比
维度 方案 A(改 unread_topic_notice) 方案 B(改 reply_author_notice) 服务器压力 基本相同(内存短路 + 相同查询数) 基本相同,仅多 1 次轻量 DELETE(去重表),可忽略 通知覆盖范围 该主题所有来源 实际相同(按 topic_id+reply_id 清,不分生成者) 代码内聚度 清除逻辑在"足迹"插件里,跨表操作 生成+清除+去重闸同插件内闭环 去重闸生命周期 ❌ 不处理,留隐蔽缺陷 ✅ 顺手重置,天然一致 后续可维护性 若修缺陷需跨插件耦合 无跨插件依赖 关键差异:去重闸(这是 B 的决定性优势)
reply_author_notice的去重表plugin_reply_author_notice_topics,只在两个时机清除:主题删除、打开「我的通知」页。方案 A 的隐蔽缺陷:用户看主题清了通知,但去重闸记录还在(unread_topic_notice 完全不碰它)。于是:
楼主看完主题(通知清了,去重闸没清) ↓ 该主题再次被回复 ↓ 去重闸命中(INSERT IGNORE,rowCount=0)→ 不生成通知 ❌ ↓ 直到楼主打开「我的通知」页,去重闸才重置用户认真看了帖子,反而收不到该主题的后续回复通知——这比原 bug 更隐蔽。方案 A 要修这个,就得让 unread_topic_notice 去清另一个插件的表(跨插件硬编码,脆弱且耦合)。
方案 B 天然没有这个问题:同一插件内,清通知时顺手
DELETE FROM plugin_reply_author_notice_topics,生成端和清除端的状态始终一致。诚实反思
之前把"方案 B 要处理去重闸"描述成额外负担,这是片面的。现在看清楚了:
- 不处理去重闸 = 留缺陷(方案 A)
- 处理 = 同插件内顺手的事(方案 B)
所以"去重闸"根本不是方案 B 的负担,而是它的独特优势——它让"通知已读"和"允许再次通知"这两个状态保持一致。一开始坚持改 reply_author_notice 的直觉是对的。
服务器压力:两者其实相当
澄清一点,别误以为 B 更省资源:
- 两者都挂在已有的
topic.after_view,都先走me()内存短路(无未读时零 DB) - 核心查询数相同:
SELECT notifications+ 批量SELECT replies+UPDATE notifications+UPDATE users - B 只多 1 次
DELETE 去重表(且仅在确实清了通知时才执行),毫秒级 - 反过来说:A 若要补上去重闸的一致性,反而要多查多清一次别的插件的表——真要算,B 更省
结论
方案 B 在功能正确性和架构内聚上都更优,服务器压力两者相当。 唯一"劣势"是它比 A 多了一个钩子注册(因为没有现成的 after_view),但换来了去重闸的一致性——这笔交易很值。
最终采用的方案 B 是更合适的选择。站长的直觉没错:通知的清除逻辑就该和通知的生成逻辑放在一起。