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. DataTable转实体

    public class ModelConvertHelper<T> where T : new() { public static IList<T> ConvertToMod ...

  2. [CentOS]添加删除用户

    摘要 在安装CentOS的时候,我们只设置了root,类似windows的超级管理员.当然我们在工作的时候,为了安全考虑,不可能对外开发root,一方面是从安全的角度,另一方面也是方便管理. 添加删除 ...

  3. 清北学堂模拟day6 圆桌游戏

    [问题描述] 有一种圆桌游戏是这样进行的:n个人围着圆桌坐成一圈,按顺时针顺序依次标号为1号至n号.对1<i<n的i来说,i号的左边是i+1号,右边是i-1号.1号的右边是n号,n号的左边 ...

  4. Hadoop 之Impala

    impala 是基于hive的大数据实时分析查询引擎,直接使用Hive的元数据库metadata意味着impala元数据都存储在hive的metadstore中并且impala兼容hive的 sql解 ...

  5. 【PHP面向对象(OOP)编程入门教程】3.什么是面向对象编程呢?

    就不说他的概念,如果你想建立一个电脑教室,首先要有一个房间, 房间里面要有N台电脑,有N个桌子, N个椅子, 白板, 投影机等等,这些是什么,刚才咱们说了, 这就是对象,能看到的一个个的实体,可以说这 ...

  6. Mac Pro 安装 Adobe Photoshop CC for mac V2014 破解版

    一.下载 Photoshop CC for mac V2014 原版(.dmg 文件): 百度网盘下载1 百度网盘下载2 百度网盘下载3 百度网盘下载4 百度网盘下载5 百度网盘下载6 百度网盘下载7 ...

  7. linux下svn命令使用大全

    最近经常使用svn进行代码管理,这些命令老是记不住,得经常上网查,终于找了一个linux下svn命令使用大全:1.将文件checkout到本地目录 svn checkout path(path是服务器 ...

  8. Mac os装软件时提示显示需要安装旧Java SE 6运行环境解决办法

    这个时Java版本的问题,换用合适的低版本即可,下面是官方的 https://support.apple.com/kb/DL1572?viewlocale=zh_CN&locale=en_US ...

  9. .Net的要知道的一些事

    1.什么是.NET?什么是CLI?什么是CLR?IL是什么?JIT是什么,它是如何工作的?GC是什么,简述一下GC的工作方式? .Net是微软推出的框架 CLI是公共语言接口(规范) CLR是公共语言 ...

  10. 字符集与编码01--charset vs encoding

    声明:此文章转载自 http://my.oschina.net/goldenshaw/blog/304493 许多时候,字符集与编码这两个概念常被混为一谈,但两者是有差别的,作为深入理解的第一步,首先 ...