bbs/贴吧/盖楼的技术实现(PHP)

简介: 2015年3月5日 14:36:44 更新: 2015年7月18日 16:33:23 星期六 目标, 实现类似网易盖楼的功能, 但是不重复显示帖子 效果: * 回复 //1楼 ** 回复 //1楼的子回复 *** 回复 //1楼的孙子回复 **** 回复 //1楼的重孙回复 (有点儿别扭.

2015年3月5日 14:36:44

更新: 2015年7月18日 16:33:23 星期六

目标, 实现类似网易盖楼的功能, 但是不重复显示帖子

效果:

* 回复 //1楼
** 回复 //1楼的子回复
*** 回复 //1楼的孙子回复
**** 回复 //1楼的重孙回复 (有点儿别扭...) 
***** 回复 //.....
****** 回复 ******* 回复 ******** 回复 ********* 回复 ********** 回复 *********** 回复 ************ 回复 * 回复 //2楼 ** 回复 //2楼的子回复 * 回复 //3楼 ** 回复 //....
张志斌你真帅 >>> 时间:2015030319
|-47说: @ 就是~怎么那么帅! [2015030319] <回复本帖>
|-|-52说: @47 回复 [2015030319] <回复本帖>
|-|-|-53说: @52 回复 [2015030319] <回复本帖>
|-|-|-|-55说: @53 回复 [2015030511] <回复本帖>
|-|-|-|-|-56说: @55 回复 [2015030511] <回复本帖>
|-|-|-|-|-|-57说: @56 回复 [2015030511] <回复本帖>
|-|-|-|-|-|-|-58说: @57 回复 [2015030511] <回复本帖>
|-|-|-|-|-|-|-|-60说: @58 回复 [2015030511] <回复本帖>
|-|-|-|-|-|-|-|-|-61说: @60 回复 [2015030511] <回复本帖>
|-|-|-|-|-|-|-|-|-|-62说: @61 回复 [2015030511] <回复本帖>
|-|-|-|-|-|-|-|-|-|-|-63说: @62 回复 [2015030511] <回复本帖>
|-|-|-|-|-|-|-|-|-|-|-|-64说: @63 回复 [2015030511] <回复本帖>
|-|-|-|-|-|-|-|-|-66说: @60 回复 [2015-03-06-16] <回复本帖>
|-|-|-|-|-|-|-59说: @57 回复 [2015030511] <回复本帖>
|-|-|-|-|-|-67说: @56 你好呀~ [2015-03-06-16] <回复本帖>
|-|-|-|-|-|-|-68说: @67 你好~ [2015-03-06-16] <回复本帖>
|-|-|-54说: @52 回复 [2015030511] <回复本帖>
|-48说: @ 回复 [2015030319] <回复本帖>
|-|-51说: @48 回复 [2015030319] <回复本帖>
|-49说: @ 回复 [2015030319] <回复本帖>
|-|-50说: @49 回复 [2015030319] <回复本帖>

实现逻辑:

1. 存储, 将数据库(MYSQL)当作一个大的结构体数组, 每一条记录用作为一个结构体, 记录父帖信息, 子帖信息, 兄弟帖信息

2. 显示原理, 因为回复帖在浏览器中显示的时候也是独占一行, 只是比楼主的帖子多了些缩进而已, 因此我将所有的回帖(子回帖, 孙子回帖....脑补网易盖楼)都看做是有着不同缩进的普通帖子

3. 显示数据

将某一贴的所有回帖, 子回帖, 孙子回帖....一次性读到内存中(缺点,可用缓存解决)

方法一:用递归(多叉树遍历)的方法将帖子重新"排序"成一维数组, 然后顺序显示(避免了嵌套循环)

方法二:同样生成一个一维数组的id排序,但是使用插入排序方法(听说递归很低效)

方法三(推荐): 分两步走, 先获取某一分页的20条一级回复给用户, 然后客户端通过ajax异步将子回复补全

4. "排序"的时候会生成两个数组,

一个里边只有帖子的id,用于循环,顺序就是1楼->1楼的所有回帖->2楼->2楼的所有回帖。。。。

另一个是具体的帖子内容等信息

 

实现细节:

1. 数据库:

id rootid fatherid next_brotherid first_childid last_childid level inttime strtime content
本帖id 首帖id  父帖id  下一个兄弟帖id  第一条回帖id  最后一个回复帖的id  本帖深度(第几层回复)  发帖时间戳  发帖字符时间(方便时间轴统计)  帖子内容 

 

2. 数据入库, 将数据库当作链表使用:

 1     //首贴/楼主帖/新闻帖    
 2     public function addRoot($content = '首贴')
 3     {
 4         $a = array(
 5             'rootid' => 0,
 6             'fatherid' => 0,
 7             'next_brotherid' => 0,
 8             'first_childid' => 0,
 9             'level' => 0,
10             'content' => $content
11             );
12 
13         $inttime = time();
14         $strtime = date('YmdH', $inttime);
15 
16         $a['inttime'] = $inttime;
17         $a['strtime'] = $strtime;
18 
19         $insert_id = $this->getlink('tiezi')->insert($a);
20     }
21 
22     //回复帖
23     public function addReplay($fatherid, $content = '回复')
24     {
25         $where = "id={$fatherid}";
26         $r = $this->getlink('tiezi')->selectOne($where);
27 
28         $id = $r['id'];
29         $rootid = $r['rootid'];
30         $first_childid = $r['first_childid'];
31         $last_childid = $r['last_childid'];
32         $level = $r['level'];
33 
34         $a = array(
35             'fatherid' => $fatherid,
36             'next_brotherid' => 0,
37             'first_childid' => 0,
38             'content' => $content
39             );
40 
41         //如果父帖是首帖(level == 0)
42         $a['rootid'] = $level ? $rootid : $id;
43 
44         $inttime = time();
45         $strtime = date('YmdH', $inttime);
46 
47         $a['level'] = ++$level;
48         $a['inttime'] = $inttime;
49         $a['strtime'] = $strtime;
50 
51         $insert_id = $this->getlink('tiezi')->insert($a);
52 
53         //判断是否是沙发帖, 是的话, 在主帖中记录下来
54         if (!$first_childid) {
55             $where = "id = {$id}";
56             $b = array(
57                 'first_childid' => $insert_id
58                 );
59             $this->getlink('tiezi')->update($b, $where);
60         }
61 
62         //将本次回复帖作为兄弟帖, 记录到上一个回复帖的记录中
63         if ($last_childid) {
64             //本次回帖不是沙发, 修改上一个回复帖的next_brotherid
65             $where = "id = {$last_childid}";
66             $c = array(
67                 'next_brotherid' => $insert_id
68                 );
69             $this->getlink('tiezi')->update($c, $where);
70 
71         }
72         //修改父帖的last_childid为本帖
73         $where = "id = {$id}";
74         $c = array(
75             'last_childid' => $insert_id
76             );
77         $this->getlink('tiezi')->update($c, $where);
78     }

有一点需要注意的是, 每次插入, 要执行好几条sql语句

如果并发量比较大的话, 可以考虑: 1.队列;  2.用redis统一生成id,代替msyql的auto_increment; 3. 事务

3. 获取帖子数据并"排序"

3.1 递归排序

 1     //获取帖子详情
 2     public function getTieziDetail($rootid)
 3     {
 4         $this->rootid = $rootid;
 5         //获得首贴信息, 相当于论坛中的文章
 6         $fields = 'first_childid';
 7         $where = 'id = '.$rootid;
 8         $root = $this->getlink('tiezi')->selectOne($where);
 9         $first_childid = $root['first_childid'];
10 
11         //获取所有回复信息
12         $where = 'rootid = '.$rootid;
13         $this->tieziList = $this->getlink('tiezi')->find($where, '', '', '', 'id');//以id为建
14         // $this->tieziList[$rootid] = $root;
15         
16         $this->rv($this->tieziList[$first_childid]);
17         // $this->rv($root);
18 
19         return array(
20             'tiezi' => $this->tieziList,
21             'sort' => $this->sort
22             );
23     }
24 
25     //递归遍历/排序帖子
26     public function rv($node)
27     {
28         $this->sort[$node['id']] = $node['id']; //顺序记录访问id
29         
30         if ($node['first_childid'] && empty($this->sort[$node['first_childid']])) { //本贴有回复, 并且回复没有被访问过
31             $this->rv($this->tieziList[$node['first_childid']]);
32         } elseif ($node['next_brotherid']) {//本帖没有回复, 但是有兄弟帖
33             $this->rv($this->tieziList[$node['next_brotherid']]);
34         } elseif ($this->tieziList[$node['fatherid']]['next_brotherid']) {//叶子节点, 没有回复, 也没有兄弟帖, 就返回上一级, 去遍历父节点的下一个兄弟节点(如果有)
35             // $fatherid = $node['fatherid'];
36             // $next_brotherid_of_father = $this->tieziList[$fatherid]['next_brotherid'];
37             // $this->rv($this->tieziList[$next_brotherid_of_father]); //这三行是对下一行代码的分解
38             $this->rv($this->tieziList[$this->tieziList[$node['fatherid']]['next_brotherid']]);
39         } elseif ($node['fatherid'] != $this->rootid) { //父节点没有兄弟节点, 则继续回溯, 直到其父节点是根节点
40             $this->rv($this->tieziList[$node['fatherid']]);
41         }
42 
43         return;
44     }

3.2 插入排序

 1 //获取帖子详情
 2     public function getTieziDetail($rootid)
 3     {
 4         $this->rootid = $rootid;
 5         //获得首贴信息, 相当于论坛中的文章
 6         // $fields = 'id first_childid content strtime';
 7         $where = 'id = '.$rootid;
 8         $root = $this->getlink('tiezi')->selectOne($where);
 9         $first_childid = $root['first_childid'];
10 
11         //获取所有回复信息
12         $where = 'rootid = '.$rootid;
13         $order = 'id';
14         $this->tieziList = $this->getlink('tiezi')->find($where, '', $order, '', 'id');//以id为建
15         
16         // $this->rv1($this->tieziList[$first_childid]);
17         $this->rv($root);
18         $this->tieziList[$rootid] = $root;
19         unset($this->sort[0]);
20 
21         return array(
22             'tiezi' => $this->tieziList,
23             'root' => $root,
24             'sort' => $this->sort
25             );
26     }
27 
28     //非递归实现 (建议)
29     //每次插入时,将自己以及自己的第一个和最后一个孩子节点,下一个兄弟节点同时插入
30     public function rv($root)
31     {
32         $this->sort[] = $root['id'];
33         $this->sort[] = $root['first_childid'];
34         $this->sort[] = $root['last_childid'];
35 
36         foreach ($this->tieziList as $currentid => $v) {
37             $currentid_key = array_search($currentid, $this->sort); //判断当前节点是否已经插入sort数组            
38             // if ($currentid_key) { //貌似当前节点肯定存在于$this->sort中
39                 $first_childid = $v['first_childid'];
40                 $last_childid = $v['last_childid'];
41                 $next_brotherid = $v['next_brotherid'];
42 
43                 //插入第一个子节点和最后一个子节点
44                 if ($first_childid && ($first_childid != $this->sort[$currentid_key+1])) { //如果其第一个子节点不在sort中,就插入
45                     array_splice($this->sort, $currentid_key + 1, 0, $first_childid);
46                     if ($last_childid && ($last_childid != $first_childid)) { //只有一条回复时,first_childid  == last_childid
47                         array_splice($this->sort, $currentid_key + 2, 0, $last_childid); //插入最后一个子节点
48                     }
49                 }
50 
51                 //插入兄弟节点
52                 if ($next_brotherid) { //存在才插入
53                     $next_brotherid_key = array_search($next_brotherid, $this->sort);
54                     if (!$next_brotherid_key) { // 只有两条回复时,下一个兄弟节点肯定已经插入了
55                         if ($last_childid) {
56                             $last_childid_key = array_search($last_childid, $this->sort);
57                             array_splice($this->sort, $last_childid_key + 1, 0, $next_brotherid); //将下一个兄弟节点插入到最后一个子节点后边
58                         } elseif ($first_childid) {
59                             array_splice($this->sort, $currentid_key + 2, 0, $next_brotherid); //将下一个兄弟节点插入到第一个子节点后边
60                         } else {
61                             array_splice($this->sort, $currentid_key + 1, 0, $next_brotherid); //将下一个兄弟节点插入到本节点后边
62                         }
63                     }
64                 }
65             // }
66         }
67     }

 html展示, 以上两种方法是一次性读取了某篇帖子的所有回复, 会是个缺陷:

 1 <html>
 2 <head>
 3     <meta charset="utf-8">
 4 </head>
 5     <body>
 6         <?php
 7             echo $root['content'], ' >>> 作者 '.$root['id'].' 时间:', $root['strtime'], '<hr>';
 8             $i = 0;
 9             foreach ($sort as $v) {
10                 for($i=0; $i < $tiezi[$v]['level']; ++$i){
11                     echo '|-';
12                 }
13                 $tmp_id = $tiezi[$v]['id'];
14                 $tmp_rootid = $tiezi[$v]['rootid'];
15                 echo $tmp_id.'说: @'. $tiezi[$tiezi[$v]['fatherid']]['id']. ' ' .$tiezi[$v]['content'].' ['.$tiezi[$v]['strtime']."] <a href='{$controllerUrl}/bbs_replay?id={$tmp_id}&rootid={$tmp_rootid}'><回复本帖></a><br>";
16             } 
17         ?>
18     </body>
19 </html>

 

3.3 先根序遍历(将所有回复看作是一颗多叉树,而帖子是这棵树的跟节点, 有循环读取数据库, 介意的话使用3.4方法)

 1 //先根序遍历
 2     // 1. 如果某节点有孩子节点, 将该节点压栈, 并访问其第一个孩子节点
 3     // 2. 如果某节点没有孩子节点, 那么该节点不压栈, 进而判断其是否有兄弟节点
 4     // 3. 如果有兄弟节点, 访问该节点, 并按照1,2步规则进行处理
 5     // 4. 如果没有兄弟节点, 说明该节点是最后一个子节点
 6     // 5. 出栈时, 判断其是否有兄弟节点, 如果有, 则按照1,2,3 进行处理, 如果没有则按照第4步处理, 直到栈为空
 7     public function getAllReplaysByRootFirst($id)
 8     {
 9         $where = "id={$id}";
10         $current = $this->getlink('tiezi')->selectOne($where);
11 
12         $replay = []; //遍历的最终顺序
13         $stack = []; //遍历用的栈
14         $tmp = []; //栈中的单个元素
15 
16         if (!empty($current['first_childid'])) {
17             //因为刚开始 $stack 肯定是空的, 而且也不知道该树是否只有跟节点, 所以用do...while
18             do {
19                 if (empty($current['stack'])) { // 不是保存在栈里的元素
20                     $replay[] = $current;
21                     if (!empty($current['first_childid'])) { //有孩子节点, 就把current替换为孩子节点, 并记录信息
22                         $current['stack'] = 1; 
23                         $stack[] = $current;
24 
25                         $where = "id={$current['first_childid']}";
26                         $current = $this->getlink('tiezi')->selectOne($where);
27                     } elseif (!empty($current['next_brotherid'])) { // 没有孩子节点, 但是有兄弟节点, 就把
28                         $where = "id={$current['next_brotherid']}";
29                         $current = $this->getlink('tiezi')->selectOne($where);
30                     } else {
31                         $current = array_pop($stack);
32                     }
33                 } else { // 是栈里(回溯)的元素, 只用判断其有没有兄弟节点就行了
34                     if (!empty($current['next_brotherid'])) { // 没有孩子节点, 但是有兄弟节点, 就把
35                         $where = "id={$current['next_brotherid']}";
36                         $current = $this->getlink('tiezi')->selectOne($where);
37                     } else {
38                         $current = array_pop($stack);
39                     }
40                 }
41                 
42             } while (!empty($stack));
43         }
44 
45         return $replay;
46     }

 3.4 切合实际, 大多数的帖子回复只有一层, 很少有盖楼的情况发生, 除非像网易刚推出盖楼功能时, 那段时间好像会盖到100多层的深度

分两步走:

第一步, 服务端一次性获取"所有"的"一级"回复, 不获取子回复(盖楼的回复)

第二步, 在客户端, 通过ajax循环异步请求每个帖子的子回复(方法3.3), 然后动态写dom, 完善所有回复

1     //获取一级回复, 这里是获取帖子的所有第一层回复
2     public function getLv1Replays($rootid)
3     {
4         $where = "rootid = {$rootid} and level = 1";
5         return $this->getlink('tiezi')->select($where);
6     }

这样做的优点或者原因是:

1. 并不是获取"所有"的一级回复, 因为现实中肯定会有分页, 每页标准20条, 撑死50条, 超过50条, 可考虑离职, 跟这样的产品混, 要小心智商

2. ajax是异步的, 基于回调的, 如果某一条回复有很多子回复, 也不会说, 完全获取了该回复所有的子回复后才去获取其它的数据

缺点是:

1. 如果网速慢, 会出现卡的现象, NND, 网络不好什么算法都是屎, 可不考虑;

2. 先显示一级回复, 而后才会显示所有子回复, 现在的硬件都很强, 瞬间的事情, 也可不考虑

 

总结:

一个复杂功能的实现, 最好分几步去完成, 不要想着一步就完成掉, 这样会死很多脑细胞才能想出完成功能的方法, 而且效率不会很高

例如:

有些好的字符串匹配算法, 比如说会实现计算好字符串移动的长度, 存放起来, 然后再去用比对字符串

将图片中一个封闭线条内的像素都染上统一颜色, 可以先逐行扫描图片, 将连在一起的像素条记录下来, 然后再去染色

 

目录
相关文章
|
5月前
|
存储 监控 算法
内网监控桌面与 PHP 哈希算法:从数据追踪到行为审计的技术解析
本文探讨了内网监控桌面系统的技术需求与数据结构选型,重点分析了哈希算法在企业内网安全管理中的应用。通过PHP语言实现的SHA-256算法,可有效支持软件准入控制、数据传输审计及操作日志存证等功能。文章还介绍了性能优化策略(如分块哈希计算和并行处理)与安全增强措施(如盐值强化和动态更新),并展望了哈希算法在图像处理、网络流量分析等领域的扩展应用。最终强调了构建完整内网安全闭环的重要性,为企业数字资产保护提供技术支撑。
133 2
|
设计模式 程序员 PHP
PHP程序员的技术成长之路
技术成长是每个PHP程序员不断追求的目标,而这一过程并非只是关于学习新的语言特性或框架,更多的是关乎思维方式和解决问题的能力。本文将探讨PHP程序员在技术成长之路上所面临的挑战,并提出一些建议,帮助他们不断提升自己的技术水平。
103 5
|
11月前
|
XML 前端开发 JavaScript
PHP与Ajax在Web开发中的交互技术。PHP作为服务器端脚本语言,处理数据和业务逻辑
本文深入探讨了PHP与Ajax在Web开发中的交互技术。PHP作为服务器端脚本语言,处理数据和业务逻辑;Ajax则通过异步请求实现页面无刷新更新。文中详细介绍了两者的工作原理、数据传输格式选择、具体实现方法及实际应用案例,如实时数据更新、表单验证与提交、动态加载内容等。同时,针对跨域问题、数据安全与性能优化提出了建议。总结指出,PHP与Ajax的结合能显著提升Web应用的效率和用户体验。
226 3
|
设计模式 安全 关系型数据库
PHP开发涉及一系列步骤和技术
【7月更文挑战第2天】PHP开发涉及一系列步骤和技术
205 57
|
缓存 程序员 PHP
为什么说 Swoole 是 PHP 程序员技术水平的分水岭?
【9月更文挑战第8天】Swoole 被视为 PHP 程序员技术水平的分水岭,因为它要求程序员深入理解底层原理(如网络编程、异步和并发模型),具备性能优化能力(如高效服务器开发、数据库连接池管理),拥有架构设计能力(如微服务架构、项目复杂度管理),并具备持续学习和自我提升意识。熟练掌握 Swoole 的程序员在技术能力和综合素质方面更具优势。
155 9
|
缓存 NoSQL PHP
使用PHP-redis实现键空间通知监听key失效事件的技术与代码示例
通过上述方法,你可以有效地在PHP中使用Redis来监听键空间通知,特别是针对键失效事件。这可以帮助你更好地管理缓存策略,及时响应键的变化。
224 3
|
机器学习/深度学习 人工智能 自然语言处理
PHP编程中的面向对象基础利用AI技术提升文本分类效率
【8月更文挑战第28天】在PHP的编程世界中,面向对象编程(OOP)是一块基石,它不仅塑造了代码的结构,也影响了开发者的思考方式。本文将深入探讨PHP中面向对象的基础概念,通过浅显易懂的语言和生动的比喻,带领初学者步入这个充满魅力的世界。我们将一起探索类与对象的秘密,理解构造函数和析构函数的重要性,以及继承和多态性的魔法。准备好了吗?让我们开始这段激动人心的旅程!
|
前端开发 API PHP
PHP中的异步编程:提升性能和响应速度的关键技术
在Web开发中,性能和响应速度是至关重要的。PHP作为一种流行的服务器端脚本语言,传统上以同步方式处理请求。然而,随着互联网应用复杂性的增加,异步编程成为了优化性能的关键技术之一。本文将探讨PHP中异步编程的实现方式及其在提升性能和响应速度方面的重要作用。
163 27
|
算法 PHP 数据安全/隐私保护
PHP中的数据加密技术及应用
在Web开发中,数据安全始终是一个至关重要的问题。本文将介绍PHP中常用的数据加密技术,包括对称加密算法、非对称加密算法和哈希算法的原理和应用。通过深入了解这些加密技术,开发人员可以更好地保护用户数据和提高系统的安全性。
234 27
|
SQL 安全 PHP
探寻PHP的现代演进之路:从Web开发到框架创新——揭秘PHP语言如何引领技术潮流
【8月更文挑战第2天】探索PHP的现代演进:从Web开发到框架创新
124 1