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. 數據入庫, 將數據庫當作鏈表使用:
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. 先顯示一級回復, 而后才會顯示所有子回復, 現在的硬件都很強, 瞬間的事情, 也可不考慮
總結:
一個復雜功能的實現, 最好分幾步去完成, 不要想着一步就完成掉, 這樣會死很多腦細胞才能想出完成功能的方法, 而且效率不會很高
例如:
有些好的字符串匹配算法, 比如說會實現計算好字符串移動的長度, 存放起來, 然后再去用比對字符串
將圖片中一個封閉線條內的像素都染上統一顏色, 可以先逐行掃描圖片, 將連在一起的像素條記錄下來, 然后再去染色