news 2026/10/14 5:35:54

Discuz论坛关注功能二次开发实战:从数据表设计到通知系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Discuz论坛关注功能二次开发实战:从数据表设计到通知系统

1. 为什么Discuz站点需要"关注"功能

Discuz是国内覆盖面很广的PHP论坛系统,X3.x系列在中小型社区里依然是主力。站长做到一定阶段都会遇到同一个问题:用户与用户之间除了"发帖、回帖、私信",缺少一种轻量的关系沉淀方式。很多用户在论坛里只是"潜水",看到感兴趣的人就手动去翻TA的历史帖子,刷新页面等更新——这种体验很原始,用户留存自然上不去。

"关注"功能解决的就是这个问题:把"人对内容"的兴趣,转化为"人对人"的持续关注,让用户进入站点后能第一时间看到自己关注的用户发了什么。用户粘性和回访率都会因此提升。这套机制在很多内容社区已经被验证过了,Discuz原生后台没有一键开启的开关,需要自己做二次开发。

我这次要讲的,不是装个现成插件就完事的方案,而是从数据表设计、接口逻辑、前端交互到通知推送的完整开发过程。适合有一定PHP基础和Discuz模板修改经验的站长,也适合想学习Discuz二次开发思路的开发者参考。开发过程中遇到的坑我也会一并列出来,能帮你省下不少调试时间。

2. 功能边界与数据表设计:先想清楚再动手

2.1 先划清需求边界

动手写代码之前,一定要把需求边界划清楚。我见过不少人在这一步偷懒,结果开发到一半发现逻辑越写越乱。关注功能最基础的能力是三块:

  • 关注与取消关注(双向操作,支持重复点击不报错)
  • 被关注列表与粉丝列表(在个人主页展示)
  • 被关注用户发新帖时的动态提醒(可选,但强烈建议做)

不要一上来就做"关注后自动站内信通知对方""关注者可以查看被关注人隐私帖"这类需求,先把主线跑通,再做扩展。因为在Discuz里,"用户资料可见性"本身有一套权限体系,过早耦合会让后面的调试成本翻倍。我见过有人把关注关系直接绑到好友表上,结果用户的Discuz好友列表全被污染了,后台怎么查都查不出问题,最后只能回滚数据库。

2.2 数据表结构:一张表解决核心关系

Discuz的数据库是模块化的,我们不需要动原有的pre_common_member这类核心表,只要新建一张独立的关系表就行。我建议建表SQL如下:

CREATE TABLE `pre_common_user_follow` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', `uid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '关注者UID', `followuid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '被关注者UID', `dateline` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '关注时间戳', PRIMARY KEY (`id`), KEY `uid` (`uid`), KEY `followuid` (`followuid`), UNIQUE KEY `uid_followuid` (`uid`, `followuid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户关注关系表';

这里有两个关键设计点,我说一下为什么:

  • 复合唯一键uid_followuid:保证同一个人不能重复关注另一个人,这是幂等性的第一道防线。就算前端按钮被连点、接口被重复请求,数据库层面也不会插入重复记录。
  • followuid单独建索引:查粉丝列表(谁关注了我)和查关注列表(我关注了谁)会走不同索引,避免全表扫描。关注关系表的数据量增长是很快的,一个万人在线的站点,关注关系突破几十万条很常见,索引不提前设计好,后期必卡。

关于是否要加status字段(用于拉黑/互关状态),我的建议是第一阶段不加。Discuz本身已经有好友和黑名单体系,关注关系越纯粹越好,后续需要可以通过关联表去扩展,不要在核心关系表里堆业务状态。

2.3 关联查询:一次性拿到用户头像和昵称

Discuz的用户基础信息分散在pre_common_member和pre_common_member_profile等表里,查询关注列表时如果逐条查会很慢。我习惯的做法是先查出关注UID列表,再用C::t()批量拼接用户信息:

$followuids = array_column($followList, 'followuid'); $users = C::t('common_member')->fetch_all($followuids); foreach ($users as $uid => $member) { $avatar = avatar($uid, 'small'); // 组装展示数据 }

avatar()是Discuz自带的头像函数,直接调用即可,不用自己拼头像路径。用户名也是通过$member['username']直接拿到,不需要额外关联profile表,除非你要在关注列表里展示签名、地区等扩展信息。

3. 后端接口实现:关注/取消关注与统计字段更新

3.1 接口设计:一个入口,两种动作

我推荐用一个接口处理关注/取消关注两个动作,前端通过参数action=follow|unfollow区分。PHP端代码放在插件的class/目录下,通过Discuz的模块入口调用。

public function onFollowSubmit() { global $_G; $uid = intval($_G['uid']); $targetUid = intval($_GET['uid']); if ($uid <= 0) { showmessage('请先登录后再操作', '', array(), array('showmsg' => true)); } if ($uid == $targetUid) { showmessage('不能关注自己'); } // 判断目标用户是否存在 $targetUser = C::t('common_member')->fetch($targetUid); if (empty($targetUser)) { showmessage('目标用户不存在'); } $table = DB::table('common_user_follow'); $exists = DB::result_first("SELECT COUNT(*) FROM $table WHERE uid=%d AND followuid=%d", $uid, $targetUid); $action = $_GET['action'] == 'unfollow' ? 'unfollow' : 'follow'; if ($action == 'follow') { if ($exists) { showmessage('你已经关注过该用户了'); } DB::insert('common_user_follow', array( 'uid' => $uid, 'followuid' => $targetUid, 'dateline' => TIMESTAMP, )); } else { if (!$exists) { showmessage('你还没有关注该用户'); } DB::delete('common_user_follow', array('uid' => $uid, 'followuid' => $targetUid), 1); } // 同步更新统计字段(见3.2) $this->updateCount($uid, $targetUid, $action); showmessage('操作成功', '', array(), array('showmsg' => true, 'location' => '')); }

代码里几处容易被忽略的细节:

  • 判断目标用户是否存在:接口里只校验了不能关注自己,但还要用C::t('common_member')->fetch($targetUid)判断用户是否存在,否则会写入脏数据。我第一版就漏了这个,结果关注列表里出现一堆不存在的UID。
  • showmessage的最后一个参数:加array('showmsg' => true)会让它走Ajax返回JSON而不是页面跳转,同时传'location' => ''避免返回后自动跳转。前者配合前端Ajax很好用,后者能防止用户被莫名弹回上一页。

3.2 粉丝数统计:冗余字段替代实时count

很多教程会在关注后直接UPDATE pre_common_member SET fans = fans + 1,这样也行,但我个人不推荐在核心会员表上加字段。更稳妥的思路是把统计数字做成冗余字段,放到Discuz现成的用户统计表里:

ALTER TABLE `pre_common_member_count` ADD `follow_count` int(10) unsigned NOT NULL DEFAULT '0'; ALTER TABLE `pre_common_member_count` ADD `fans_count` int(10) unsigned NOT NULL DEFAULT '0';

pre_common_member_count本来就存帖子数、好友数等,把关注数加在这里非常自然。每次关注/取消关注时同步加减:

private function updateCount($uid, $targetUid, $action) { $countTable = DB::table('common_member_count'); if ($action == 'follow') { DB::query("UPDATE $countTable SET follow_count = follow_count + 1 WHERE uid=%d", $uid); DB::query("UPDATE $countTable SET fans_count = fans_count + 1 WHERE uid=%d", $targetUid); } else { DB::query("UPDATE $countTable SET follow_count = GREATEST(follow_count - 1, 0) WHERE uid=%d", $uid); DB::query("UPDATE $countTable SET fans_count = GREATEST(fans_count - 1, 0) WHERE uid=%d", $targetUid); } }

这里用GREATEST(x - 1, 0)防止并发请求下统计字段变成负数。个人主页展示关注数、粉丝数时直接读字段就行,不用临时count整张关系表。站内用户量大以后,这种提前设计能省不少查询开销。

3.3 一个容易踩的坑:Discuz的请求校验

Discuz在showmessage时会自动校验Form Hash,如果你的接口是直接处理$_GET请求,可能会遇到"请求来源不合法"的报错。解决方法是:

  • 前端提交时带上FORMHASH:var formhash = '{FORMHASH}';
  • 后端接口在Discuz插件的module入口里,对插件模块放行参数校验。

很多Discuz二次开发的新手卡在这一步,明明接口代码没问题,请求却被拦在门外,然后各种加exit、die绕过,反而破坏了Discuz的安全机制。正确做法是让前端带上formhash,后端走Discuz标准流程。

4. 前端交互改造:关注按钮、个人主页与Ajax状态切换

4.1 按钮位置与状态判断

关注按钮最常见的两个位置是:帖子楼层的作者信息区、用户个人主页。这两个位置要展示的状态不同:

场景显示状态
未关注"关注"按钮(高亮)
已关注"已关注"按钮(灰色/可点击取消)
自己看自己不显示按钮
目标用户不存在不显示按钮

模板里的实现思路是:在按钮位置输出两个区块,一个默认显示、一个通过JS控制显隐。不要在页面加载时用PHP判断后只输出某一个,那样切换状态时要整页刷新,体验很差。

<span class="follow-btn">function doFollow(uid) { var url = 'plugin.php?id=user_follow:follow&action=follow&uid=' + uid + '&formhash=' + FORMHASH; $.get(url, function(res) { if (typeof res === 'string') { res = JSON.parse(res); } if (res.status === 1) { $('#follow_status_' + uid + ' .follow-btn-normal').hide(); $('#follow_status_' + uid + ' .follow-btn-followed').show(); } else { showDialog(res.message, 'notice'); } }); } function cancelFollow(uid) { var url = 'plugin.php?id=user_follow:follow&action=unfollow&uid=' + uid + '&formhash=' + FORMHASH; $.get(url, function(res) { if (typeof res === 'string') { res = JSON.parse(res); } if (res.status === 1) { $('#follow_status_' + uid + ' .follow-btn-normal').show(); $('#follow_status_' + uid + ' .follow-btn-followed').hide(); } else { showDialog(res.message, 'notice'); } }); }

接口返回的JSON格式需要和Discuz的showmessage约定对齐,它的返回结构一般是:

{"status": 1, "message": "操作成功", "location": ""}

所以前端直接判断res.status就行。注意如果是老版本Discuz,showmessage可能返回HTML而不是JSON,这取决于$_SERVER['HTTP_X_REQUESTED_WITH']。推荐在调用时给$.get加上'json'类型参数,让jQuery自动处理。

4.3 个人主页的关注页签

个人主页改造相对简单:在home.php?mod=space&uid=xxx的模板里增加"关注"和"粉丝"两个Tab,通过do=follow和do=fans区分。列表页就是前面提到的关联查询,注意分页用Discuz的multi函数:

$count = DB::result_first("SELECT COUNT(*) FROM %t WHERE uid=%d", array('common_user_follow', $space['uid'])); $page = max(1, intval($_GET['page'])); $pagesize = 20; $list = DB::fetch_all("SELECT * FROM %t WHERE uid=%d ORDER BY dateline DESC LIMIT %d, %d", array('common_user_follow', $space['uid'], ($page - 1) * $pagesize, $pagesize)); $multipage = multi($count, $pagesize, $page, "home.php?mod=space&uid={$space['uid']}&do=follow");

分页参数必须过滤非数字值,直接用intval($_GET['page'])会踩到字符串拼接的坑。multi返回的分页HTML直接输出到模板就行,样式是Discuz内置的,无需额外适配。

5. 通知与动态聚合:让关注关系产生实际价值

5.1 新帖通知的触达方式

如果做了关注但没通知,用户根本感知不到关注的价值。Discuz原生支持站内通知(notification表),我们可以在发帖的钩子里追加一条"你关注的用户发布了新主题"的通知。

具体做法是挂source/plugin/user_follow/下的钩子,在主题发布成功后触发,携带帖子ID和作者UID:

if ($tid && $authorid) { $followers = DB::fetch_all("SELECT uid FROM %t WHERE followuid=%d", array('common_user_follow', $authorid)); foreach ($followers as $f) { notification_add($f['uid'], 'system', 'new_follow_thread', '你关注的用户发布了新主题', array('tid' => $tid, 'from_id' => $authorid)); } }

notification_add的参数格式在不同Discuz版本里有差异,X3.2和X3.4的签名不一样,我建议直接查当前版本的source/function/function_core.php里的notification_add定义,照着参数传就行,别照搬网上的老代码。

5.2 消息推送的量级控制

这里有一个非常现实的性能问题:如果一个用户有几万粉丝,发一个帖子就要给几万人插通知记录,数据库瞬间压力很大。我的处理方式有两个:

  • 分页插入:粉丝数量大时,每次最多插200条,多批次循环执行,避免单次SQL过长。
  • 去重通知:同一用户短时间内发布多个主题,只发送一次通知。可以在通知里加dateline字段,24小时内已通知过就不再重复通知。

实际运营中,我选择了一个折中方案:不在发帖时通知所有粉丝,而是在粉丝的"关注动态"页面实时聚合关注对象的发帖记录。这样虽然实时查询压力略大,但避免了通知风暴,而且对用户来说,一个聚合的动态页比零散的站内信体验更好。

5.3 动态聚合页的查询写法

$followuids = array_column( DB::fetch_all("SELECT followuid FROM %t WHERE uid=%d", array('common_user_follow', $_G['uid'])), 'followuid' ); if (empty($followuids)) { // 没有关注任何人时,页面输出引导内容:去发现页看看活跃用户 } $threads = DB::fetch_all( "SELECT tid, subject, authorid, author, dateline FROM %t WHERE authorid IN (%n) AND displayorder>=0 ORDER BY dateline DESC LIMIT 50", array('forum_thread', $followuids) );

注意displayorder>=0是为了过滤被删除或审核中的帖子。Discuz的forum_thread表通常很大,这个查询一定要走authorid索引。如果站点帖子量在百万级以上,建议给这个查询做5分钟缓存,减少重复扫描。

6. 缓存、并发与数据一致性踩坑记录

6.1 并发取消关注导致的负粉丝数

我第一版上线之后遇到过一个bug:用户快速连续点击"关注/取消关注"两个按钮,导致粉丝数变成负数。原因是两次Ajax请求并发到达,后端没做好幂等,UPDATE fans_count = fans_count - 1在一个已经取消关注的事务里又执行了一次。

修复方案就是3.2里的GREATEST(fans_count - 1, 0),同时在删除关系记录时限制只删除一条,并在删除前确认影响行数:

$result = DB::delete('common_user_follow', array('uid' => $uid, 'followuid' => $targetUid), 1); if ($result->affected_rows > 0) { // 只有真正删除了记录才去减粉丝数 DB::query("UPDATE %t SET fans_count = GREATEST(fans_count - 1, 0) WHERE uid=%d", array('common_member_count', $targetUid)); }

这个问题的本质是"更新统计前没有确认关系记录存在"。以后凡是涉及"关系 + 统计"的场景,都要记住:删除成功才更新统计,更新统计用GREATEST兜底。

6.2 关注关系表数量级增大后的索引策略

关注关系表是典型的"写多读多"场景。初期数据量小,复合唯一索引就够用了。但站点到了几十万关注关系后,ORDER BY dateline DESC分页会变慢。建议在uid + dateline上加联合索引,专门服务"我关注了谁,按时间倒序"的查询:

ALTER TABLE `pre_common_user_follow` ADD KEY `uid_dateline` (`uid`, `dateline`);

同理,粉丝列表的查询是followuid + dateline方向,也可以加一个。但索引不是越多越好,每个索引都会影响写入性能,只加真正被查询覆盖的就行。

6.3 模板缓存的坑

Discuz的模板是有缓存的,修改template目录下的.htm文件后,后台需要"清空缓存",前台才能看到变化。很多人开发时改了模板没清缓存,盯着屏幕以为自己代码写错了,排查半天。

我习惯的做法是本地开发环境直接设置config_global.php里的模板自动刷新:

$_config['template']['tplrefresh'] = 1;

这样模板文件修改后会自动重新编译,无需手动清缓存。正式环境记得改回0,避免每次访问都重新编译模板,拖慢响应。

7. 上线实测与后续迭代方向

7.1 一次真实的上线问题:头像显示空白

功能开发完成后,我在本地测试一切正常,部署上线后却发现粉丝列表页的头像大面积空白。排查后发现是环境差异:本地PHP版本较新,avatar()函数返回的是高清原图路径,而服务器上Discuz版本较旧,头像存储路径规则不同。

解决方案是在调用avatar()前统一require_once对应版本的function_profile.php,或者直接用全局函数指定头像大小。这类环境差异问题在Discuz开发里很常见,建议上线前把本地环境的PHP版本、MySQL版本尽量对齐服务器,能少踩很多这种基础坑。

7.2 可以继续深化的方向

关注功能跑通之后,我建议按下面的优先级迭代:

  1. 关注分组:用户可以把关注对象分到"技术大牛""同城朋友"等分组,动态页按组筛选。这是提升关注功能可用性的关键一步,很多用户关注了几百人之后,信息流会被灌爆,分组能有效缓解。
  2. 推荐关注:根据用户已有关注对象,推荐"他们也关注了谁",可以显著提升关注关系的增长速度。实现上就是取交集、计共现次数,不用引入太重的推荐算法。
  3. 隐私设置:在Discuz个人设置页增加"是否允许他人关注我"的开关,保护不想被打扰的用户。实现时在关注接口里判断目标用户的这个设置即可。
  4. 管理后台统计:在Discuz后台插件设置页展示关注关系总量、日增长量等数据,方便判断功能使用情况。这个对运营很重要,没有数据你根本不知道功能有没有被使用。

7.3 一点个人心得

开发"关注"功能,本质上是在Discuz的"帖子—版块"结构之外,补上了一层"用户—用户"的关系维度。Discuz的二次开发不像现代框架从零搭项目那么规整,很多数据表、函数、模板机制有历史包袱,但只要顺着它的扩展机制走,能做出原生体验很好的功能。

项目做完之后,我最大的感受是:不要把关注功能当成一个"按钮"来做,而要当成一套"关系系统"来做。数据表设计、接口幂等、通知策略、查询性能,每一环都得提前想清楚。也希望这篇分享能让你少踩几个我踩过的坑,如果你在实现过程中遇到卡壳的地方,欢迎在评论区把具体现象发出来一起讨论。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 5:35:52

多Agent越加越慢?四大效率陷阱与重构实战指南

1. 从单打独斗到团队作战&#xff1a;我为什么执意要上多Agent架构上个月我接手了一个内部数据整理项目&#xff0c;初期用单个Agent跑业务流程&#xff0c;效果说实话还不错。但业务方很快提出新需求&#xff1a;每天要处理的不再是几十条零散请求&#xff0c;而是几百份格式各…

作者头像 李华
网站建设 2026/10/14 5:35:49

Agent架构越复杂效果越差?A/B测试揭示删掉一半后反而更好

1. 先说结论&#xff1a;那次 A/B 测试的赢家&#xff0c;是被我删掉一半的“半成品”去年年中&#xff0c;我手头有一个已经跑了大半年的 Agent 项目&#xff0c;内部代号就叫“群聊管家”。它做的事情不复杂&#xff1a;把用户群里零散的问题收集起来&#xff0c;按主题分组、…

作者头像 李华
网站建设 2026/10/14 5:34:46

模型剪枝与知识蒸馏:把大模型变小

&#x1f4dd; 本章学习目标&#xff1a;通过本章学习&#xff0c;你将全面掌握"模型剪枝与知识蒸馏&#xff1a;把大模型变小"这一核心主题&#xff0c;建立系统性认知。一、引言&#xff1a;为什么这个话题如此重要 在人工智能快速发展的今天&#xff0c;模型剪枝与…

作者头像 李华
网站建设 2026/10/14 5:34:41

外网无法访问:排查路由器、NAT与运营商线路问题

&#x1f4dd; 本章学习目标&#xff1a;本章介绍网络服务&#xff0c;帮助读者掌握常见网络服务的配置与管理。通过本章学习&#xff0c;你将全面掌握"外网无法访问&#xff1a;排查路由器、NAT与运营商线路问题"这一核心主题。一、引言&#xff1a;为什么这个话题如…

作者头像 李华
网站建设 2026/10/14 5:34:36

AI-CAD副驾时代:17K stars项目与2分钟出图实战解析

最近 AI-CAD 圈子出了个挺有代表性的信号&#xff1a;一个开源项目冲上 17K stars&#xff0c;团队拿到 740 万美元融资&#xff0c;演示视频里 2 分钟就能出一版图。三个数字放在一起&#xff0c;很多人下意识觉得"AI 要替代设计师了"&#xff0c;但我的判断恰好相反…

作者头像 李华
网站建设 2026/10/14 5:34:03

LeetCode 333:最大BST子树问题与树形DP递归状态设计详解

刷LeetCode 333的人&#xff0c;十有八九会卡在递归返回值的设计上。题面很短&#xff1a;给一棵普通二叉树&#xff0c;在里面找出最大的二叉搜索子树&#xff0c;返回节点数。听起来不就是判断BST吗&#xff1f;真写起来才发现&#xff0c;判断一棵树是不是BST容易&#xff0…

作者头像 李华