2015年3月5日 14:36:44

更新: 2019年12月23日 最后一个, 不再更新了 : https://talk.hearu.top/

更新: 2019年4月17日 15:40:34 星期三 存储和组装数据更简单:  这里 https://www.cnblogs.com/iLoveMyD/p/10320015.html

更新: 2018年4月15日 效率更高, 前端排序, 代码更简单的实现 这里 http://www.cnblogs.com/iLoveMyD/p/8847056.html

更新: 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. 显示数据

方法一:

需要先将某一贴的所有回帖, 子回帖, 孙子回帖....一次性读到内存中, 然后组装

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

方法二:

分两步走, 先获取一级回复给用户, 然后当用户点开某个回复查看子回复时, 通过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. 数据入库, 将数据库当作链表使用:

     //首贴/楼主帖/新闻帖
public function addRoot($content = '首贴')
{
$a = array(
'rootid' => 0,
'fatherid' => 0,
'next_brotherid' => 0,
'first_childid' => 0,
'level' => 0,
'content' => $content
); $inttime = time();
$strtime = date('YmdH', $inttime); $a['inttime'] = $inttime;
$a['strtime'] = $strtime; $insert_id = $this->getlink('tiezi')->insert($a);
} //回复帖
public function addReplay($fatherid, $content = '回复')
{
$where = "id={$fatherid}";
$r = $this->getlink('tiezi')->selectOne($where); $id = $r['id'];
$rootid = $r['rootid'];
$first_childid = $r['first_childid'];
$last_childid = $r['last_childid'];
$level = $r['level']; $a = array(
'fatherid' => $fatherid,
'next_brotherid' => 0,
'first_childid' => 0,
'content' => $content
); //如果父帖是首帖(level == 0)
$a['rootid'] = $level ? $rootid : $id; $inttime = time();
$strtime = date('YmdH', $inttime); $a['level'] = ++$level;
$a['inttime'] = $inttime;
$a['strtime'] = $strtime; $insert_id = $this->getlink('tiezi')->insert($a); //判断是否是沙发帖, 是的话, 在主帖中记录下来
if (!$first_childid) {
$where = "id = {$id}";
$b = array(
'first_childid' => $insert_id
);
$this->getlink('tiezi')->update($b, $where);
} //将本次回复帖作为兄弟帖, 记录到上一个回复帖的记录中
if ($last_childid) {
//本次回帖不是沙发, 修改上一个回复帖的next_brotherid
$where = "id = {$last_childid}";
$c = array(
'next_brotherid' => $insert_id
);
$this->getlink('tiezi')->update($c, $where); }
//修改父帖的last_childid为本帖
$where = "id = {$id}";
$c = array(
'last_childid' => $insert_id
);
$this->getlink('tiezi')->update($c, $where);
}

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

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

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

3.1 递归排序

     //获取帖子详情
public function getTieziDetail($rootid)
{
$this->rootid = $rootid;
//获得首贴信息, 相当于论坛中的文章
$fields = 'first_childid';
$where = 'id = '.$rootid;
$root = $this->getlink('tiezi')->selectOne($where);
$first_childid = $root['first_childid']; //获取所有回复信息
$where = 'rootid = '.$rootid;
$this->tieziList = $this->getlink('tiezi')->find($where, '', '', '', 'id');//以id为建
// $this->tieziList[$rootid] = $root; $this->rv($this->tieziList[$first_childid]);
// $this->rv($root); return array(
'tiezi' => $this->tieziList,
'sort' => $this->sort
);
} //递归遍历/排序帖子
public function rv($node)
{
$this->sort[$node['id']] = $node['id']; //顺序记录访问id if ($node['first_childid'] && empty($this->sort[$node['first_childid']])) { //本贴有回复, 并且回复没有被访问过
$this->rv($this->tieziList[$node['first_childid']]);
} elseif ($node['next_brotherid']) {//本帖没有回复, 但是有兄弟帖
$this->rv($this->tieziList[$node['next_brotherid']]);
} elseif ($this->tieziList[$node['fatherid']]['next_brotherid']) {//叶子节点, 没有回复, 也没有兄弟帖, 就返回上一级, 去遍历父节点的下一个兄弟节点(如果有)
// $fatherid = $node['fatherid'];
// $next_brotherid_of_father = $this->tieziList[$fatherid]['next_brotherid'];
// $this->rv($this->tieziList[$next_brotherid_of_father]); //这三行是对下一行代码的分解
$this->rv($this->tieziList[$this->tieziList[$node['fatherid']]['next_brotherid']]);
} elseif ($node['fatherid'] != $this->rootid) { //父节点没有兄弟节点, 则继续回溯, 直到其父节点是根节点
$this->rv($this->tieziList[$node['fatherid']]);
} return;
}

3.2 插入排序

 //获取帖子详情
public function getTieziDetail($rootid)
{
$this->rootid = $rootid;
//获得首贴信息, 相当于论坛中的文章
// $fields = 'id first_childid content strtime';
$where = 'id = '.$rootid;
$root = $this->getlink('tiezi')->selectOne($where);
$first_childid = $root['first_childid']; //获取所有回复信息
$where = 'rootid = '.$rootid;
$order = 'id';
$this->tieziList = $this->getlink('tiezi')->find($where, '', $order, '', 'id');//以id为建 // $this->rv1($this->tieziList[$first_childid]);
$this->rv($root);
$this->tieziList[$rootid] = $root;
unset($this->sort[0]); return array(
'tiezi' => $this->tieziList,
'root' => $root,
'sort' => $this->sort
);
} //非递归实现 (建议)
//每次插入时,将自己以及自己的第一个和最后一个孩子节点,下一个兄弟节点同时插入
public function rv($root)
{
$this->sort[] = $root['id'];
$this->sort[] = $root['first_childid'];
$this->sort[] = $root['last_childid']; foreach ($this->tieziList as $currentid => $v) {
$currentid_key = array_search($currentid, $this->sort); //判断当前节点是否已经插入sort数组
// if ($currentid_key) { //貌似当前节点肯定存在于$this->sort中
$first_childid = $v['first_childid'];
$last_childid = $v['last_childid'];
$next_brotherid = $v['next_brotherid']; //插入第一个子节点和最后一个子节点
if ($first_childid && ($first_childid != $this->sort[$currentid_key+1])) { //如果其第一个子节点不在sort中,就插入
array_splice($this->sort, $currentid_key + 1, 0, $first_childid);
if ($last_childid && ($last_childid != $first_childid)) { //只有一条回复时,first_childid == last_childid
array_splice($this->sort, $currentid_key + 2, 0, $last_childid); //插入最后一个子节点
}
} //插入兄弟节点
if ($next_brotherid) { //存在才插入
$next_brotherid_key = array_search($next_brotherid, $this->sort);
if (!$next_brotherid_key) { // 只有两条回复时,下一个兄弟节点肯定已经插入了
if ($last_childid) {
$last_childid_key = array_search($last_childid, $this->sort);
array_splice($this->sort, $last_childid_key + 1, 0, $next_brotherid); //将下一个兄弟节点插入到最后一个子节点后边
} elseif ($first_childid) {
array_splice($this->sort, $currentid_key + 2, 0, $next_brotherid); //将下一个兄弟节点插入到第一个子节点后边
} else {
array_splice($this->sort, $currentid_key + 1, 0, $next_brotherid); //将下一个兄弟节点插入到本节点后边
}
}
}
// }
}
}

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

 <html>
<head>
<meta charset="utf-8">
</head>
<body>
<?php
echo $root['content'], ' >>> 作者 '.$root['id'].' 时间:', $root['strtime'], '<hr>';
$i = 0;
foreach ($sort as $v) {
for($i=0; $i < $tiezi[$v]['level']; ++$i){
echo '|-';
}
$tmp_id = $tiezi[$v]['id'];
$tmp_rootid = $tiezi[$v]['rootid'];
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>";
}
?>
</body>
</html>

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

 //先根序遍历
// 1. 如果某节点有孩子节点, 将该节点压栈, 并访问其第一个孩子节点
// 2. 如果某节点没有孩子节点, 那么该节点不压栈, 进而判断其是否有兄弟节点
// 3. 如果有兄弟节点, 访问该节点, 并按照1,2步规则进行处理
// 4. 如果没有兄弟节点, 说明该节点是最后一个子节点
// 5. 出栈时, 判断其是否有兄弟节点, 如果有, 则按照1,2,3 进行处理, 如果没有则按照第4步处理, 直到栈为空
public function getAllReplaysByRootFirst($id)
{
$where = "id={$id}";
$current = $this->getlink('tiezi')->selectOne($where); $replay = []; //遍历的最终顺序
$stack = []; //遍历用的栈
$tmp = []; //栈中的单个元素 if (!empty($current['first_childid'])) {
//因为刚开始 $stack 肯定是空的, 而且也不知道该树是否只有跟节点, 所以用do...while
do {
if (empty($current['stack'])) { // 不是保存在栈里的元素
$replay[] = $current;
if (!empty($current['first_childid'])) { //有孩子节点, 就把current替换为孩子节点, 并记录信息
$current['stack'] = 1;
$stack[] = $current; $where = "id={$current['first_childid']}";
$current = $this->getlink('tiezi')->selectOne($where);
} elseif (!empty($current['next_brotherid'])) { // 没有孩子节点, 但是有兄弟节点, 就把
$where = "id={$current['next_brotherid']}";
$current = $this->getlink('tiezi')->selectOne($where);
} else {
$current = array_pop($stack);
}
} else { // 是栈里(回溯)的元素, 只用判断其有没有兄弟节点就行了
if (!empty($current['next_brotherid'])) { // 没有孩子节点, 但是有兄弟节点, 就把
$where = "id={$current['next_brotherid']}";
$current = $this->getlink('tiezi')->selectOne($where);
} else {
$current = array_pop($stack);
}
} } while (!empty($stack));
} return $replay;
}

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

分两步走:

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

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

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

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

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

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

缺点是:

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

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

总结:

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

例如:

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

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

bbs/贴吧/盖楼的技术实现(PHP)的更多相关文章

  1. SQL递归查询实现跟帖盖楼效果

    网易新闻的盖楼乐趣多,某一天也想实现诸如网易新闻跟帖盖楼的功能,无奈技术不佳(基础不牢),网上搜索了资料才发现SQL查询方法有一种叫递归查询,整理如下: 一.查询出 id = 1 的所有子结点 wit ...

  2. 💒 es6 + canvas 开源 盖楼小游戏 完整代码注释 从零教你做游戏(一)

    盖楼游戏 一个基于 Canvas 的盖楼游戏 Demo 预览 在线预览地址 (Demo Link) 手机设备可以扫描下方二维码 github https://github.com/bmqb/tower ...

  3. 如何用ABP框架快速完成项目(7) - 用ABP一个人快速完成项目(3) - 通过微服务模式而不是盖楼式来避免难度升级和奥卡姆剃刀原理

    这节文章十分重要!十分重要!十分重要!   很多同学在使用ABP的过程中遇到很多问题, 花费了很多时间和精力都还无法解决, 就是卡在这节文章这里.   Talk is cheap, just show ...

  4. PHP 仿网易云的评论盖楼

    一.简要 第一次做这种设计,当然有许多不足,希望多多指出. 评论盖楼,就是每条评论一个楼层,而楼层里面可以嵌套很多引用的评论,直接上图 A:牛什么牛(见图 Top4) B回复A:好牛啊.(所以这里就嵌 ...

  5. 使用fiddler盖楼评论

    使用fiddler盖楼评论:使用replay重复请求某接口

  6. 双十一还在盖楼?少年你应该掌握Docker 部署 Consul了

    ▶ Spring Boot 依赖与配置 Maven 依赖 <dependencyManagement> <dependencies> <dependency> &l ...

  7. 蚂蚁金服CTO程立:金融级分布式交易的技术路径

    总结: 强一致的微服务 oceanbase里面的投票选举以及多中心多地部署 单元化市异地多活的基础.支付宝是异地多活和容灾结合,而容灾的基础也是单元化.基于单元化进行单元的调度.部署.容灾. 混合云架 ...

  8. SQL注入技术专题—由浅入深【精华聚合】

    作者:坏蛋链接:https://zhuanlan.zhihu.com/p/23569276来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 不管用什么语言编写的Web应用 ...

  9. 技术之美[程序人生]我在IBM实习的日子

    写这篇文章的时候,我已经在IBM正式工作了,看看上一篇博文的发布日期,才发现,我已经将近三个月没有更新博客了,多么惊人!为什么这么久?期间发生了很多事情.最重要的一件就是我大学毕业了!毕业的那么平淡, ...

随机推荐

  1. 如何配置pl/sql (本地客户端)连接远程oracle服务器

    配置方法: 1.找到oracle的安装目录.如:C:\oracle\product\10.2.0\db_1\network\ADMIN 2.找到tnsnames.ora文件. 3.用文本方式打开,添加 ...

  2. 360chrome,google chrome浏览器使用jquery.ajax加载本地html文件

    使用360chrome和google chrome浏览器加载本地html文件时,会报错,提示: XMLHttpRequest cannot load file:///Y:/jswg/code/html ...

  3. 新浪微博客户端(40)-使用AFN发送带图片的微博

    DJComposeViewController.m /** 发微博 */ - (void)sendStatusRequest { AFHTTPSessionManager *RequestManage ...

  4. 【转】 使用maven创建web项目

    生成kafka java客户端时,参考的资料!!!文章来源:http://blog.csdn.net/zhshulin/article/details/37921705 目前做的项目使用的是MAVEN ...

  5. mysql 分页性能优化

    最简单的分页方法是这样的 , 该表中存在5w左右数据 执行时间平均在10s左右,因此该种方式在数据量大的情况下查询效率极低. 优化方式有以下几种 1.此种方式平均在7-8s之间(CreateDate ...

  6. asp.net mvc 实现博客的时间分类管理

    先看效果 这个其实用c#实现起来比较简单: Sides = bllSession.IArticleBLL.GetList("") .Select(a => a.Time) . ...

  7. 2015年12月02日 GitHub入门学习(四)Git操作

    序,学习使用Git是一项新技能,你将了解到Git与SubVersion的区别. 一.基本操作 git init 初始化仓库,请实际建立一个目录并初始化仓库,.git目录里存储着管理当前目录内容所需的仓 ...

  8. 未能从程序集“System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089”中加载类型“System.ServiceModel.Activation.HttpModule”。

    ********************************* 思路壹(也是网络上铺天盖地的通俗解决方案) 原因: 这是因为先安装了 .NET Framework , 随后启用了 .NET Fra ...

  9. AlwaysOn可用性组测试环境安装与配置(二)--AlwaysOn配置(界面与T-SQL)

    四.AlwaysOn配置 1.开启AlwaysOn高可用性功能. 1.1.开启Server01的可用性组 1.2.需要重启服务:属于SQL server群集节点的服务,需要通过故障转移界面重启 1.3 ...

  10. OC第六节—— 继承与类别

    1.继承:    父类和子类的关系.    1.1 生活中的继承        父类           子类           父类              子类                 ...