资讯动态

Discuz关注功能开发实战:从数据表到前端交互

发布时间:2026/10/14 5:39:43 来源:尧图企业网站定制
做Discuz开发的朋友多多少少都经历过这样的需求论坛里用户想要“关注某人”关注之后能在个人中心看到自己关注了谁、谁又关注了自己甚至希望能收到被关注对象的动态提醒。这个功能在主流社交平台里是标配但在Discuz这种传统论坛系统里官方并没有提供现成的方案。我早年接过好几个类似的需求一开始也觉得无从下手后来把整个链路梳理清楚之后发现这事其实没有想象中那么复杂。这篇文章我想完整拆一遍“Discuz如何开发关注功能”的实操过程从需求分析、数据表设计到后台接口封装、前台模板改造再到通知联动和性能优化最后把踩过的坑一并拿出来说。无论你是打算用插件方式实现还是想直接改模板这篇都能给你一个可以落地参考的完整路径。1. 需求拆解与整体设计思路1.1 关注功能到底要解决什么问题很多人在动手之前容易把“关注功能”想得太简单以为就是一个按钮、一张表。但真正落地的时候你会发现它牵连着好几个模块用户之间要建立“关注”与“被关注”的双向关系并且要能查询列表个人主页要展示粉丝数和关注数最好还能展示关注列表、粉丝列表帖子楼层里要有“关注”按钮方便在阅读时直接操作被关注后对方最好能收到系统通知如果还想做得完整一点可以在“动态”或“广播”里推送给关注者。也就是说关注功能的本质不是“存两条记录”而是一套围绕用户关系链的小型社交系统。一套合理的结构设计比堆代码重要得多。1.2 两种实现方案的取舍我见过不少开发者用“用户表加字段”的方式做关注功能在用户表里加一个字段专门存关注者的uid列表用逗号分隔。这种方案在数据量小的时候看着很直接但隐患非常大。我以某个论坛的实测数据为例当用户量到几千、关注关系到了几万条时这种逗号分隔字符串的查询基本就废了。你想查“谁关注了某用户”得先把字符串取出来再在PHP里循环处理想查“某用户关注了谁”还得再处理一次。如果用户量继续涨到十万级这种写法的性能会变得很难看。另一种方案是建独立的关系表一条记录表示“uid 关注了 targetuid”。这是社交系统里最标准的做法。表结构简单查询直接走索引扩展也方便——以后想做拉黑、好友、特别关注在这张表上加类型字段就能搞定。我的建议是直接采用独立关系表方案。前期多写几十行建表代码后面能省下一大堆重构的时间。1.3 数据流向与核心模块划分拿我后来在某个模拟项目X里的实际设计来说整个关注功能分成四个模块数据层关注关系表的增删改查以及关注数、粉丝数的统计查询业务层关注/取消关注的逻辑处理包括防重复、事务与计数更新展示层个人中心页、帖子楼层的按钮与列表模板通知层关注后向被关注者发送通知。处理请求的流程也不复杂用户在前台点击关注按钮通过AJAX提交到处理脚本脚本校验登录态和formhash通过后写数据库再返回最新的关注状态和数量。前端根据返回值做按钮文案切换个人主页的列表和计数则直接通过查询数据库展示。2. 数据表设计与数据库操作封装2.1 关注关系表结构设计数据表是整个功能的地基。我一般会按下面的结构来建CREATE TABLE pre_user_follow ( id int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 自增ID, uid int(10) unsigned NOT NULL DEFAULT 0 COMMENT 关注者UID, targetuid int(10) unsigned NOT NULL DEFAULT 0 COMMENT 被关注者UID, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 状态1关注 0取消, dateline int(10) unsigned NOT NULL DEFAULT 0 COMMENT 关注时间戳, PRIMARY KEY (id), UNIQUE KEY uk_follow (uid, targetuid), KEY idx_targetuid (targetuid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户关注关系表;字段没必要设太多核心就三个关系字段uid谁关注、targetuid关注谁、status是否有效。dateline用于排序和后续的时间线功能。这里有一个关键点唯一键要建在 (uid, targetuid) 上这样同一个用户对同一个目标只能存在一条记录。重复关注在数据库层面就杜绝了业务代码只需要处理“更新状态”而不是“判断后插入”省掉很多麻烦。2.2 索引与唯一键的设计原因很多新人建表时容易忽略索引等数据量上来之后才发现查询慢得离谱。我解释一下索引设计的逻辑uk_follow (uid, targetuid)这个联合唯一索引解决两件事防止重复关注同时加速“我关注了谁”的查询。比如要查 uid5 关注的列表直接WHERE uid5 ORDER BY dateline DESC这个索引天然能覆盖。idx_targetuid单独建索引是为了加速“谁关注了我”的查询。MySQL在联合索引里把uid放最前面单独用targetuid查时走不了联合索引所以必须给targetuid单独建索引。这个设计不是拍脑袋想出来的是根据实际查询需求倒推的。你在设计阶段能预期到哪些查询最频繁就为那些查询建索引这才是合理的索引思路。2.3 基于Discuz数据库类封装读写方法Discuz目前主流版本X3.4、X3.5都内置了数据库操作类我们不需要自己封装PDO直接用系统提供的功能兼容性也更好。关注关系表建好之后我把常用的方法单独做成了一个类放在source/class/下或者直接在插件机制里建一个类文件。核心方法大概有这些?php class follow_service { /** * 添加关注 */ public static function follow($uid, $targetuid) { if ($uid 1 || $targetuid 1 || $uid $targetuid) { return false; } $data array( uid $uid, targetuid $targetuid, status 1, dateline TIMESTAMP, ); $exists DB::result_first(SELECT id FROM . DB::table(user_follow) . WHERE uid$uid AND targetuid$targetuid); if ($exists) { DB::update(user_follow, array(status 1, dateline TIMESTAMP), array(id $exists)); } else { DB::insert(user_follow, $data); } return true; } /** * 取消关注逻辑删除 */ public static function unfollow($uid, $targetuid) { return DB::update(user_follow, array(status 0), array(uid $uid, targetuid $targetuid)); } /** * 是否已关注 */ public static function is_followed($uid, $targetuid) { if ($uid 1 || $targetuid 1) { return false; } $status DB::result_first(SELECT status FROM . DB::table(user_follow) . WHERE uid$uid AND targetuid$targetuid); return $status 1; } /** * 获取某个用户的关注列表 */ public static function following_list($uid, $limit 20, $offset 0) { $list DB::fetch_all(SELECT targetuid FROM . DB::table(user_follow) . WHERE uid$uid AND status1 ORDER BY dateline DESC LIMIT $offset,$limit); $uids array_map(function($item) { return $item[targetuid]; }, $list); return $uids; } /** * 获取某个用户的粉丝列表 */ public static function follower_list($targetuid, $limit 20, $offset 0) { $list DB::fetch_all(SELECT uid FROM . DB::table(user_follow) . WHERE targetuid$targetuid AND status1 ORDER BY dateline DESC LIMIT $offset,$limit); $uids array_map(function($item) { return $item[uid]; }, $list); return $uids; } /** * 关注数 粉丝数 */ public static function count_follow($uid) { $follow DB::result_first(SELECT COUNT(*) FROM . DB::table(user_follow) . WHERE uid$uid AND status1); $fans DB::result_first(SELECT COUNT(*) FROM . DB::table(user_follow) . WHERE targetuid$uid AND status1); return array(follow $follow, fans $fans); } }代码逻辑本身不复杂但有几个细节值得说明使用DB::table(user_follow)而不是直接写pre_user_follow是为了兼容不同站点的表前缀“取消关注”做的是逻辑删除status0而不是物理删除这样用户反复关注、取关时记录不会无限增长也能保留历史关系关注和取关时后面还要同步更新用户profile里的缓存计数这个放到性能优化部分讲。3. 前端展示与交互实现3.1 个人主页的关注列表与粉丝列表Discuz的个人空间地址一般是home.php?modspaceuidxxx默认展示用户资料。我们要在个人主页上增加“关注”“粉丝”两个Tab页通常需要改模板。我见过两种做法一种是直接修改模板文件比如template/default/home/space_profile.htm和新增space_follow.htm另一种是写一个独立插件页面通过自定义导航跳过去。第一种做法直观、改动小适合站点本身需求明确的情况第二种更规范升级论坛版本时不容易被覆盖。我个人的建议是如果只是为自己站点开发直接改模板问题不大但改动前一定要备份如果想做成插件分发建议走插件的页面机制避免影响别人的模板。展示层最核心的是要处理“当前访客与页面主人之间的关系”如果访客还没登录按钮显示“登录后才能关注”点击后跳转到登录页如果访客访问的是自己的主页不显示关注按钮如果访客已经关注了该用户按钮显示“已关注”否则显示“ 关注”。3.2 帖子楼层里的关注按钮帖子楼层的关注按钮是出现频率最高的入口。Discuz帖子查看页的楼层模板在template/default/forum/viewthread_node.htm作者信息区通常包含头像、用户名、等级等信息。我一般会在用户名的下方加一个关注按钮核心模板代码大致像这样!-- 判断不是本人且已登录 -- !--{if $_G[uid] $_G[uid] ! $post[authorid]}-- a hrefjavascript:; idfollow_btn_{$post[authorid]} classfollow-btn>// 在获取到帖子列表 $postlist 之后 $authorids array_unique(array_column($postlist, authorid)); $followed_uids array(); if ($_G[uid]) { $query DB::query(SELECT targetuid FROM . DB::table(user_follow) . WHERE uid$_G[uid] AND targetuid IN ( . implode(,, $authorids) . ) AND status1); while ($row DB::fetch($query)) { $followed_uids[$row[targetuid]] 1; } }这样每条楼层数据都带了$isfollowed状态页面渲染时不会因为关注判断产生额外的SQL请求。3.3 异步请求与状态切换关注按钮的交互动作应该走AJAX避免整个页面刷新。Discuz本身自带jquery我们直接写一个简单的异步请求处理脚本。前端JavaScript放在模板底部或专门JS文件里$(.follow-btn).on(click, function() { var btn $(this); var targetuid btn.data(uid); var status btn.data(status); if (typeof targetuid undefined) return; $.ajax({ url: plugin.php?idfollow:ajax, type: POST, data: { targetuid: targetuid, op: status 1 ? unfollow : follow, formhash: !--{$_G[formhash]}-- }, dataType: json, success: function(res) { if (res.code 0) { if (res.is_followed 1) { btn.text(已关注).data(status, 1); } else { btn.text( 关注).data(status, 0); } // 更新用户卡片里的关注数 $(#follow_count).text(res.follow_count); } else { alert(res.msg); } } }); });服务端接收逻辑// ajax.php 或插件入口 $targetuid intval($_POST[targetuid]); $op $_POST[op] follow ? follow : unfollow; // 登录校验和formhash校验安全关键 if (!$_G[uid]) { exit(json_encode(array(code 1, msg 未登录))); } if (!$_G[uid] || !submitcheck(follow, POST)) { exit(json_encode(array(code 1, msg 校验失败))); } if ($op follow) { follow_service::follow($_G[uid], $targetuid); } else { follow_service::unfollow($_G[uid], $targetuid); } // 同时更新计数缓存 $counts follow_service::count_follow($targetuid); update_profile_follow_count($targetuid, $counts); exit(json_encode(array( code 0, is_followed $op follow ? 1 : 0, follow_count $counts[fans], )));3.4 模板定制的避坑点这里有一个容易踩的坑Discuz的模板语法是基于PHP的但不建议在模板里写太复杂的逻辑。模板只负责输出变量复杂的判断和查数据尽量在PHP端完成把结果组装成数组传给模板。另外Discuz模板有缓存机制。你改了模板之后可能发现页面没变化需要在后台“工具 → 更新缓存”里刷新模板缓存甚至可以删掉data/template目录下的编译文件来强制更新。4. 通知联动与动态扩展4.1 利用Discuz通知机制实现“被关注提醒”一个完整的关注功能少了通知体验会差一大截。用户A关注用户BB如果能收到一条系统通知会明显感觉到有人在“关注”自己有助于拉回站内活跃度。Discuz的通知系统自带notification机制我通常是在关注成功后追加一段通知逻辑// 关注成功后发送通知 if ($op follow $targetuid ! $_G[uid]) { C::t(common_notification)-insert(array( uid $targetuid, type follow, new 1, authorid $_G[uid], author $_G[member][username], note user_follow, dateline TIMESTAMP, )); }这里我把通知类型定义成了follow并在语言包里加了一条对应的通知文案比如“{author} 关注了你”。通知类型可以自行扩展Discuz的type字段是用来区分的不同站点自定义类型完全没问题。还有一点要注意频繁被同一人反复关注、取关会产生大量通知。我建议做频率限制——比如同一个用户对同一个目标在10分钟内只发送一条通知避免刷屏。4.2 关注对象的动态推送思路如果想让关注功能更有社交属性可以把“动态”串进来。Discuz本身有“广播”或者“动态”机制但并不是所有版本都默认开放很多站长干脆自己写轻量动态。我给出一个适合大多数场景的思路当用户发了新主题时往一张“动态表”里写一条记录记录uid和action比如“发布了新主题”以及关联的tid链接。关注了他的用户在“我的关注动态”页面里只需要查这张动态表即可SELECT * FROM pre_user_feed WHERE uid IN (SELECT targetuid FROM pre_user_follow WHERE uid$_G[uid] AND status1) ORDER BY dateline DESC LIMIT 20;这个方案的好处是查询条件简单直接动态表可以定期清理旧数据不至于无限膨胀。如果你想做一个类似时间线的页面这个结构已经够用了。动态表的建表参考CREATE TABLE pre_user_feed ( feedid int(10) unsigned NOT NULL AUTO_INCREMENT, uid int(10) unsigned NOT NULL DEFAULT 0 COMMENT 动态作者UID, action varchar(20) NOT NULL DEFAULT COMMENT 动作类型, tid int(10) unsigned NOT NULL DEFAULT 0 COMMENT 关联主题ID, dateline int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (feedid), KEY idx_uid (uid), KEY idx_dateline (dateline) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;发帖的时候往这张表插入一条数据关注动态页查询时就按上面那条语句逻辑非常清晰。5. 性能与容错细节5.1 关注数缓存别每次都COUNT很多人开发到这一步就收工了但实际线上跑一段就会发现个人空间被频繁访问时每次打开都要COUNT(*)统计粉丝数和关注数数据稍微大一点数据库压力明显上来了。优化方式有两种。第一种是把关注数、粉丝数冗余到用户资料表common_member_profile或common_member_status里关注/取关操作时同步更新这两个数值。展示个人主页时根本不需要count直接读字段。第二种是使用Discuz自带的内存缓存。把关注数缓存在$_G[setting]或者用memory驱动存储设置一定过期时间。我推荐第一种方式为主、第二种为辅。计数不必做到绝对实时短暂延迟完全可以接受。只要在关注/取关时能同步更新数据就能保持相对准确。如果真的出现少量计数不一致后台可以写一个定时任务定期从user_follow表重新统计把正确的值回写进去。5.2 大V场景下的软删除与重复处理某个用户粉丝特别多比如上万人同时关注他这时会出现几个问题。第一个是并发写入几千人同时插入同一targetuid的user_follow记录idx_targetuid这个索引会面临写入压力但这通常不至于出现重大问题真正需要留神的是防重复插入。我在2.2里建的uk_follow (uid, targetuid)唯一键在这个场景下就发挥作用了。即使两个请求同时到达数据库也只会允许一条成功第二条走UPDATE操作不会出现重复记录。我建议你在业务层不要过度依赖“先查再插”让数据库的唯一键做最终防线。还有一个细节如果要做“互相关注”状态可以在查询时多返回两个字段一是“我是否关注了他”二是“他是否关注了我”。这个可以通过两次IN查询批量拿到不需要每条单独算。在个人主页里展示“回关”按钮体验会更好。5.3 关注接口的安全与频率控制接口安全容易被忽略。Discuz本身有防CSRF的机制一定要在AJAX里带上formhash并校验否则容易被恶意刷接口、批量关注或取关。更稳妥的做法是服务端对同一个IP、同一个uid加一个每分钟请求频率限制比如每分钟最多操作30次。实现也不复杂用$_G[uid]time()在内存缓存里做一个计数器即可。也可以直接用Discuz的floodctrl机制利用系统自带的防灌水逻辑做拦截改动最小。6. 常见问题与排查实录6.1 建表语句执行报错的几个原因很多人在执行建表SQL时碰到报错最常见的是没选对数据库。Discuz的配置文件在config/config_global.php里面dbname就是数据库名。执行SQL前先USE 数据库名再执行建表语句。另外utf8mb4和旧版本的MySQL兼容性也要注意。如果你的MySQL版本在5.5以下utf8mb4索引长度可能超限会提示“Specified key was too long”。我实际处理过这种情况——最终方案有两种一是升级MySQL版本到5.7或以上二是把索引字段的长度改小或者在my.cnf里调整大索引前缀限制。对关注表这种结构前者更根本。6.2 关注按钮状态不同步按钮显示“已关注”但点进去发现根本没有关注记录这类问题大多是页面缓存造成的。我排查过好几次最后发现模板层面做了整页缓存。Discuz的模板只缓存HTML结构但帖子页有时候会被一些SEO类插件做静态化缓存导致按钮状态完全停留在页面生成的那一刻。解决办法很简单关注操作完成后通过AJAX刷新的是当前按钮的状态但如果是页面级缓存需要即时更新帖子页的按钮状态就比较麻烦。我建议对关注按钮区域做动态加载也就是在页面加载完成后通过一个接口批量获取当前页面所有作者的关注状态再填充到按钮上可以绕开静态缓存的干扰。6.3 SQL查询越来越慢当关注关系表数据量超过百万条之后简单的COUNT和分页可能变慢。我在某次线上排查时发现慢的根源在于ORDER BY dateline DESC配合LIMIT的分页查询。因为复合索引是(uid, targetuid)排序字段dateline并不在索引覆盖范围内。优化策略是如果在业务上不要求严格按关注时间排序可以直接按主键id倒序排列因为自增id的顺序和时间顺序基本一致SELECT targetuid FROM pre_user_follow WHERE uid$uid AND status1 ORDER BY id DESC LIMIT 20;这样查询可以完全走主键速度会快很多。如果一定要按dateline排序就需要把索引调整成(uid, status, dateline)的复合索引让排序字段也进入索引这样才能避免文件排序。6.4 模板修改后页面无变化这个问题我在第3节提过一次但实际遇到的人特别多这里再单独列一下。修改Discuz模板后如果页面没变化先不要怀疑自己的代码大多数情况下是模板缓存没刷新。后台“工具 → 更新缓存”里可以选择“模板缓存”一键清除也可以直接在服务器上删除data/template下面生成的.php缓存文件。另外很多第三方模板套件本身也带自己的编译缓存需要同时清理。如果模板是别人开发的最好先确认模板作者的更新方式避免清不掉还找不着原因。6.5 关注数与实际列表不一致关注数量显示和列表数量对不上基本是计数缓存和数据表写入不同步引起的。常见场景是关注成功但更新计数的代码在异常情况下没有执行或者用户在短时间内多次关注取关逻辑删除的记录没有被正确计数。我的排查思路是先直接查user_follow表验证真实关系数据是否正确。如果真实数据正确那问题就出在计数同步如果真实数据也不对那问题就出在写入逻辑。定位到具体环节之后再跑一次全量重计-- 重计某用户的关注数与粉丝数并回写示意语句 UPDATE pre_user_follow uf SET uf.status CASE WHEN EXISTS ( SELECT 1 FROM pre_common_member m WHERE m.uid uf.targetuid ) THEN uf.status ELSE 0 END;实际上我更推荐写一个后台脚本用PHP遍历所有用户重新查一次count_follow回写到资料表里这样更可控也更好调试。写在最后的一些个人经验我大概前前后后给不同类型的站点做过三四版关注功能有纯模板改造的也有插件化实现的。最大的体会是Discuz虽然老但底层扩展性并不差关键是找对它的套路。建关系表、写业务类、改模板、挂通知这套流程熟悉以后实际开发一个完整的关注功能用不了多少代码。另一个值得说的是论坛类站点的“关注”和社交平台还不完全一样。很多论坛用户习惯了主题式浏览关注入口一定要放在显眼的位置——比如帖子楼层作者名片区域以及个人空间页的显眼位置。入口藏得太深用户很难形成使用习惯。做完之后最好自己走一遍完整流程甚至找人帮忙测一下看看会不会出现按钮点了没反应、页面刷新后状态丢失这些问题。最后再分享一个小技巧上线前可以把关注表的数据量人为造到几十万条然后跑一下列表页和计数接口提前看看在哪一步会出现性能瓶颈。这些小成本预演往往比上线之后再救火管用得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑