博客如何實現評論回覆功能(數據庫設計、java利用鏈表實現查詢、Eledment UI樹形控件實現評論回顯)

背景

在開發博客系統的時候,遇到了一個技術難點,就是如何實現用戶對博客的評論和回覆功能?

嘗試了很多套方案,最後還是採用了Java的鏈表結構來查詢處理評論回覆。

博客如何實現評論回覆功能

數據庫設計

這裏主要展示有關評論和回覆的核心數據庫設計:

Blog(博客表)

在這裏插入圖片描述

User(用戶表)

在這裏插入圖片描述

Observe評論表

在這裏插入圖片描述

數據庫設計概要

  1. 博客表主要用於id和評論表的blog_id關聯,用於表示這條評論是和哪篇博客相關聯的。
  2. 用戶表主要用於id和評論表的observer_id關聯,用於表示這條評論和哪個用戶相關聯。
  3. 博客表和用戶表不是關鍵性內容,只用瞭解其id和評論表有關聯即可。
  4. 關鍵點在於評論表設計的last_id字段
  • 如果last_idnull,說明此條評論爲一級評論(直接評論這篇博客)
  • 如果last_id不爲null,其值代表回覆哪條評論(上一級評論的id)
    依據這樣的last_id設計,可完美實現一條鏈狀的評論(多級評論回覆)。通過last_id我們就能找到這條回覆是回覆的哪條評論。

java實現評論的查詢

存儲

對於存儲結構,使用鏈表。通過鏈表,我們可以一步步找到最後一條評論。因爲一條評論可能存在多人回覆(同級評論或者回復有多條),所以我們採用List存儲下一個對象(下一級評論回覆)。

查詢
  1. 先查詢last_idnull的情況,即所有的一級評論集合。
  2. 然後查詢last_id不爲null的情況,即所有非一級評論集合(爲評論的回覆)。
  3. 通過last_id找到對應的評論、回覆,添加到鏈表中。

具體實現

數據庫查詢主要用的是mybatis的通用mapper,如果不會,可參看我的另一篇博客:SSM項目中如何使用通用mapper(tk.mapper)?如何使用註解簡化mybatis的開發(捨棄大量的xml文件)?註解處理一對一、一對多、多對多的關係?

主要演示查詢的實現,增刪改都比較簡單,暫略。

評論的實體類
@Entity
@Data
@Table(name="observe")
public class Observe implements Serializable {

    /** 評論id */
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @NotNull(groups = UpdateGroup.class,message = "修改評論信息,id不可以空")
    private Long id;

    /** 所屬博客id */
    @Column(name="blog_id")
    @NotNull(message = "所屬博客id不能爲空")
    private Long blogId;

    /** 評論者id */
    @Column(name="observer_id")
    @NotNull(groups = UpdateGroup.class,message = "評論者id不能爲空")
    private Long observerId;

    /** 評論內容 */
    @Column(name="observe_content")
    @NotBlank(message = "評論內容不能爲空")
    private String observeContent;

    /** 評論上一級的id */
    @Column(name="last_id")
    private Long lastId;

    /** 是否刪除 */
    @Column(name="is_delete")
    private Boolean delete;

    /** 創建時間(評論時間) */
    @Column(name="create_date")
    @JSONField(format="yyyy-MM-dd HH:mm:ss")
    private Timestamp createDate;

    /** 修改刪除 */
    @Column(name="update_date")
    @JSONField(format="yyyy-MM-dd HH:mm:ss")
    private Timestamp updateDate;

    public interface UpdateGroup {
    }
}
評論的鏈表結構

繼承評論的實體類

/**
 * 功能描述:封裝博客評論的BO <br>
 * 採用鏈表結構實現
 **/
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
public class ObserveNodeBO extends Observe {

    /**
     * 評論的用戶信息
     */
    private User user;

    /**
     * 下一條回覆
     */
    private List<ObserveNodeBO> nextNodes = new ArrayList<>();

    public ObserveNodeBO ( ObserveNodeBO observeNodeBo ) {
        super();
        setId(observeNodeBo.getId());
        setBlogId(observeNodeBo.getBlogId());
        setObserverId(observeNodeBo.getObserverId());
        setObserveContent(observeNodeBo.getObserveContent());
        setLastId(observeNodeBo.getLastId());
        setDelete(observeNodeBo.getDelete());
        setCreateDate(observeNodeBo.getCreateDate());
        setUpdateDate(observeNodeBo.getUpdateDate());
        this.user = observeNodeBo.getUser();
    }

}
評論的mapper(數據庫操作)

評論與用戶之間是一對一的關係,所以這裏使用mybatis的註解實現一對一關聯。

/**
 * 功能描述: 自定義的mapper
 * ‘@RegisterMapper’ 使自定義的mapper可以被掃描到
 **/
@RegisterMapper
public interface CommentMapper<T> extends Mapper<T>, IdListMapper<T, Long>, InsertListMapper<T> {

}
@Component
public interface ObserveMapper extends CommentMapper<Observe> {

    /**
     * 功能描述:根據博客id和lastId爲空,查詢所有的一級評論信息集合
     * @param blogId 博客id
     * @return 一級評論信息集合
     * @author RenShiWei
     * Date: 2020/4/16 10:37
     */
    @Select("SELECT * FROM observe o LEFT JOIN user u " +
            "ON o.observer_id=u.id " +
            "WHERE o.blog_id=#{blogId} AND o.last_id is null")
    @Results({
            @Result(id = true, column = "id", property = "id"),
            @Result(column = "blog_id", property = "blogId"),
            @Result(column = "observer_id", property = "observerId"),
            @Result(column = "observe_content", property = "observeContent"),
            @Result(column = "observer_id", property = "user",
                    one = @One(select = "com.blog.module.business.mapper.UserMapper.queryUserForObserve",
                            fetchType = FetchType.EAGER)),
            @Result(column = "last_id", property = "lastId"),
            @Result(column = "is_delete", property = "delete"),
            @Result(column = "create_date", property = "createDate"),
            @Result(column = "update_date", property = "updateDate")
    })
    List<ObserveNodeBO> queryFirstObserveList (@Param("blogId") Long blogId );


    /**
     * 功能描述:根據博客id和lastId不爲空,查詢所有的二級評論信息集合
     * @param blogId 博客id
     * @return 二級評論信息集合
     * @author RenShiWei
     * Date: 2020/4/16 10:37
     */
    @Select("SELECT * FROM observe o LEFT JOIN user u " +
            "ON o.observer_id=u.id " +
            "WHERE o.blog_id=#{blogId} AND o.last_id is not null")
    @Results({
            @Result(id = true, column = "id", property = "id"),
            @Result(column = "blog_id", property = "blogId"),
            @Result(column = "observer_id", property = "observerId"),
            @Result(column = "observe_content", property = "observeContent"),
            @Result(column = "observer_id", property = "user",
                    one = @One(select = "com.blog.module.business.mapper.UserMapper.queryUserForObserve",
                            fetchType = FetchType.EAGER)),
            @Result(column = "last_id", property = "lastId"),
            @Result(column = "is_delete", property = "delete"),
            @Result(column = "create_date", property = "createDate"),
            @Result(column = "update_date", property = "updateDate")
    })
    List<ObserveNodeBO> querySecondObserveList (@Param("blogId") Long blogId );


}
@Component
public interface UserMapper extends CommentMapper<User> {

    /**
     * 功能描述:根據主鍵id查詢用戶信息
     * (在observe中一對一關係使用)
     *
     * @param id 用戶id
     * @return 用戶信息
     * @author RenShiWei
     * Date: 2020/4/16 10:43
     */
    @Select("SELECT * FROM user WHERE id=#{id}")
    @Results({
            @Result(id = true, column = "id", property = "id"),
            @Result(column = "create_date", property = "createDate"),
            @Result(column = "last_id", property = "lastId"),
            @Result(column = "is_delete", property = "delete"),
            @Result(column = "update_date", property = "updateDate")
    })
    User queryUserForObserve (@Param("id") Long id );

}

評論的業務邏輯實現
public interface ObserveService {

	/**
     * 功能描述:根據博客id,查詢此博客的所有評論信息
     * @param blogId 博客id
     * @return 博客的評論信息
     */
    List<ObserveNodeBO> queryObserveByBlogId( Long blogId);
    
    /**
     * 功能描述:根據評論id查詢用戶信息
     * @param observeId 評論id
     * @return 評論信息,攜帶用戶信息
     */
    ObserveUserBo queryObserveUserById(Long observeId);
    
}
@Service
public class ObserveServiceImpl implements ObserveService {
	@Autowired
    private ObserveMapper observeMapper;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private BlogMapper blogMapper;

	/**
     * 功能描述:根據博客id,查詢此博客的所有評論信息
     *
     * @param blogId 博客id
     * @return 博客的評論信息
     */
    @Override
    public List<ObserveNodeBO> queryObserveByBlogId ( Long blogId ) {
        //所有未處理的一級評論集合
        List<ObserveNodeBO> firstObserveList = observeMapper.queryFirstObserveList(blogId);
        //所有未處理的二級評論集合
        List<ObserveNodeBO> secondObserveList = observeMapper.querySecondObserveList(blogId);

        //將二級評論用鏈表的方式添加到一級評論
        List<ObserveNodeBO> list = addAllNode(firstObserveList, secondObserveList);

        //控制檯打印評論回覆
        show(list);

        //返回處理後的評論信息
        return list;
    }

    /**
     * 功能描述:根據評論id查詢用戶信息
     *
     * @param observeId 評論id
     * @return 評論信息,攜帶用戶信息
     */
    @Override
    public ObserveUserBo queryObserveUserById ( Long observeId ) {
        Observe observe = observeMapper.selectByPrimaryKey(observeId);
        User user = userMapper.selectByPrimaryKey(observe.getObserverId());
        ObserveUserBo observeUserBo = new ObserveUserBo();
        observeUserBo.setObserve(observe);
        observeUserBo.setUser(user);
        return observeUserBo;
    }


    /**
     * 功能描述:將單個node添加到鏈表中
     *
     * @param firstList   第一層評論集合(鏈表)
     * @param observeNode 非第一層評論的回覆信息
     * @return 是否添加
     */
    private boolean addNode ( List<ObserveNodeBO> firstList, ObserveNodeBO observeNode ) {
        //循環添加
        for (ObserveNodeBO node : firstList) {
            //判斷留言的上一段是否是這條留言(判斷這條回覆,是否是當前評論的回覆)
            if (node.getId().equals(observeNode.getLastId())) {
                //是,添加,返回true
                node.getNextNodes().add(observeNode);
                return true;
            } else {
                //否則遞歸繼續判斷
                if (node.getNextNodes().size() != 0) {
                    if (addNode(node.getNextNodes(), observeNode)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * 功能描述:將查出來的lastId不爲null的回覆都添加到第一層Node集合中
     *
     * @param firstList 第一層評論集合(鏈表)
     * @param thenList  非第一層評論集合(鏈表)
     * @return 所有評論集合(非第一層評論集合對應添加到第一層評論集合,返回)
     */
    private List<ObserveNodeBO> addAllNode ( List<ObserveNodeBO> firstList, List<ObserveNodeBO> thenList ) {
        while (thenList.size() != 0) {
            int size = thenList.size();
            for (int i = 0; i < size; i++) {
                if (addNode(firstList, new ObserveNodeBO(thenList.get(i)))) {
                    thenList.remove(i);
                    i--;
                    size--;
                }
            }
        }
        return firstList;
    }

    /**
     * 功能描述:打印評論的鏈表回覆信息
     *
     * @param list 評論信息(鏈表集合)
     */
    private void show ( List<ObserveNodeBO> list ) {
        for (ObserveNodeBO node : list) {
            System.out.println(node.getObserverId() + " 用戶回覆了" + node.getLastId() + ":" + node.getObserveContent());
            //遞歸打印回覆信息
            if (node.getNextNodes().size() != 0) {
                show(node.getNextNodes());
            }
        }
    }
}
評論的接口
@RestController
@RequestMapping("/api/observe")
public class ObserveController {

    @Autowired
    private ObserveService observeService;

	/**
     * 功能描述:根據博客id,查詢此博客的所有評論信息(鏈表類型的數據)
     * @param blogId 博客id
     * @return 博客的評論信息
     */
    @GetMapping("/{blogId}")
    public ResponseEntity<List<ObserveNodeBO>> queryObserveByBlogId (
            @ApiParam(name = "blogId", value = "博客id", required = true) @PathVariable Long blogId
    ) {
        return ResponseEntity.ok(observeService.queryObserveByBlogId(blogId));
    }

	/**
     * 功能描述:根據評論id查詢用戶信息(評論信息,攜帶用戶信息)
     * @param observeId 評論id
     * @return 評論信息,攜帶用戶信息
     */
    @GetMapping("/user/{observeId}")
    public ResponseEntity<ObserveUserBo> queryObserveUserById (
            @ApiParam(name = "observeId", value = "評論id", required = true)@PathVariable Long observeId
    ) {
        return ResponseEntity.ok(observeService.queryObserveUserById(observeId));
    }
}

查詢評論的實例

數據庫數據

博客id爲29的所有評論信息
在這裏插入圖片描述

查詢後回顯的數據
[
  {
    "id": 27,
    "blogId": 29,
    "observerId": 63,
    "observeContent": "你好",
    "lastId": null,
    "delete": false,
    "createDate": "2020-05-13 12:01:24",
    "updateDate": "2020-05-13 12:01:24",
    "user": {
      "id": 63,
      "nickname": "焦前進",
      "email": "[email protected]",
      "picture": "https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg",
      "identity": 0,
      "delete": false,
      "createDate": "2020-05-13 12:01:24",
      "updateDate": "2020-05-13 12:01:24",
      "account": null,
      "password": null
    },
    "nextNodes": []
  },
  {
    "id": 28,
    "blogId": 29,
    "observerId": 64,
    "observeContent": "你也好啊",
    "lastId": null,
    "delete": false,
    "createDate": "2020-05-13 12:19:16",
    "updateDate": "2020-05-13 12:19:16",
    "user": {
      "id": 64,
      "nickname": "末",
      "email": "[email protected]",
      "picture": "https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg",
      "identity": 0,
      "delete": false,
      "createDate": "2020-05-13 12:19:16",
      "updateDate": "2020-05-13 12:19:16",
      "account": null,
      "password": null
    },
    "nextNodes": [
      {
        "id": 29,
        "blogId": 29,
        "observerId": 65,
        "observeContent": "大家好",
        "lastId": 28,
        "delete": false,
        "createDate": "2020-05-13 12:20:25",
        "updateDate": "2020-05-13 12:20:25",
        "user": {
          "id": 65,
          "nickname": "小黑",
          "email": "[email protected]",
          "picture": "https://fuss10.elemecdn.com/8/27/f01c15bb73e1ef3793e64e6b7bbccjpeg.jpeg",
          "identity": 0,
          "delete": false,
          "createDate": "2020-05-13 12:20:25",
          "updateDate": "2020-05-13 12:20:25",
          "account": null,
          "password": null
        },
        "nextNodes": [
          {
            "id": 30,
            "blogId": 29,
            "observerId": 66,
            "observeContent": "我喜歡小黑",
            "lastId": 29,
            "delete": false,
            "createDate": "2020-05-13 12:21:39",
            "updateDate": "2020-05-13 12:21:39",
            "user": {
              "id": 66,
              "nickname": "小白",
              "email": "[email protected]",
              "picture": "https://fuss10.elemecdn.com/1/8e/aeffeb4de74e2fde4bd74fc7b4486jpeg.jpeg",
              "identity": 0,
              "delete": false,
              "createDate": "2020-05-13 12:21:39",
              "updateDate": "2020-05-13 12:21:39",
              "account": null,
              "password": null
            },
            "nextNodes": [
              {
                "id": 31,
                "blogId": 29,
                "observerId": 67,
                "observeContent": "ddd",
                "lastId": 30,
                "delete": false,
                "createDate": "2020-05-13 12:23:54",
                "updateDate": "2020-05-13 12:23:54",
                "user": {
                  "id": 67,
                  "nickname": "小黃",
                  "email": "[email protected]",
                  "picture": "https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg",
                  "identity": 0,
                  "delete": false,
                  "createDate": "2020-05-13 12:23:54",
                  "updateDate": "2020-05-13 12:23:54",
                  "account": null,
                  "password": null
                },
                "nextNodes": []
              }
            ]
          }
        ]
      }
    ]
  }
]

Element UI樹形控件實現評論信息的回顯

這裏使用的普通H5項目引入Element UI,如果是vue-cli項目更爲方便,原理都是相通的。
引入依賴的目錄結構(當然也可以使用CDN的方式引入)

關於Element UI引入,資源已經上傳,可在文章頂部下載。
在這裏插入圖片描述

HTML(只保留關鍵部分)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>博客詳情</title>
    <link rel="stylesheet" href="../plugins/element/element-ui-index.css"/>
    <link rel="stylesheet" href="../css/blog-detail.css"/>
    <script src="../plugins/jquery/jquery-3.5.0.min.js"></script>
    <script src="../plugins/jquery/jquery.cookie-1.4.1.min.js"></script>
    <script src="../plugins/vue/vue-2.6.11.js"></script>
    <script src="../plugins/element/element-ui-index.js"></script>
    <script src="../js/blog-detail.js"></script>
</head>
<div class="content">
    <!--正文-->
    <section>
        <div id="blog-detail">
        	<!--    展示已有評論    -->
                <div class="look-observe">
                    <el-tree
                            ref="tree"
                            :data="observes"
                            :props="defaultProps"
                    >
                        <div class="custom-tree-node" slot-scope="{ node, data }">
                            <div class="observe-top">
                                <img :src="data.user.picture" alt="頭像" class="observe-header-img">
                                <span class="observe-nickname">{{data.user.nickname}}</span>
                                <span class="observe-user" v-text="data.lastId==null?'回覆了博主':'回覆了你'"></span>
                                <el-tag class="observe-tag" @click="getLastId(data)" round size="mini">回覆</el-tag>
                                <span class="observe-date">{{data.createDate}}</span>
                            </div>
                            <p class="observe-content">{{data.observeContent}}</p>
                        </div>
                    </el-tree>
                </div>
            </div>
        </div>
    </section>
</div>

CSS

/*評論顯示*/
.look-observe {
    padding: 2rem 2rem;
    background-color: #ffffff;
}

#observe-title {
    text-align: center;
    font-weight: bold;
}

#my-observe-user {
    font-size: 1.6rem;
    font-weight: bold;
    margin: 1rem 0;
}

#clear-observe-tag {
    width: 10rem;
    height: 3rem;
    padding: 0.5rem;
    cursor: pointer;
    text-align: center;
    margin-left: 85%;
}

#my-observe-user-content {
    font-size: 1.4rem;
}

.custom-tree-node {
    width: 100%;
}

.el-tree-node__content {
    height: 8rem;
    padding: 1rem 2rem;
    border-radius: 1rem;
    border: 0.1rem solid #eee;
}

.observe-content {
    font-size: 1.4rem;
    margin: 1rem 0;
}

.observe-top {
    line-height: 3rem;
    height: 3rem;
}

.observe-top span {
    display: inline-block;
    vertical-align: middle;
}

.observe-header-img {
    display: inline-block;
    vertical-align: middle;
    height: 3rem;
    width: 3rem;
    border-radius: 3rem;
}

.observe-nickname {
    font-weight: bold;
    font-size: 1.5rem;
    margin: 0 1rem;
}

.observe-user {
    margin: 0 1rem;
    font-size: 1.2rem;
}

.observe-date {
    float: right;
    margin-right: 2rem;
}

.observe-tag {
    float: right;
}

JS

$(document).ready(function () {
    //側邊欄的vue
    let blogDetail = new Vue({
        el: '#blog-detail',
        data: {
            blogId: '', //博客id
            lastId: '',  //評論的lastId
            observes: [], //已經評論的集合
            defaultProps: {
                children: 'nextNodes',
                label: 'observeContent',
                id: 'id',
            },
        },
        methods: {
            //查詢此篇博客的所有評論信息
            getBlogObserve() {
                $.get(baseUrl + "/api/observe/" + this.blogId,
                    function (data, status, xhr) {
                        blogDetail.observes = data;
                    }, "json").fail(function (error) {
                    blogDetail.$message({
                        showClose: true,
                        message: "查詢博客評論失敗" + error.responseJSON.message,
                        type: 'error'
                    });
                });
            },
            //進入博客詳情頁,博客的瀏覽量+1
            increaseViewCount() {
                if ($.cookie("viewId") !== this.blogId) {
                    $.ajax({
                        url: baseUrl + "/api/blog/increase-view-number/" + this.blogId,
                        dataType: "text",
                        type: "put",
                        success: function (data, status, xhr) {
                            //設置過期時間爲1h
                            let date = new Date();
                            date.setTime(date.getTime() + 60 * 60 * 60 * 1000);
                            $.cookie("viewId", blogDetail.blogId, {"path": "/",}, {expires: date});
                        },
                        error: function (error) {
                            blogDetail.$message({
                                showClose: true,
                                message: error.responseJSON.message,
                                type: 'error'
                            });
                        }
                    });
                }
            },
            //點擊點贊,增加博客的點贊量
            increaseLikeCount() {
                if ($.cookie("likeId") !== this.blogId) {
                    $.ajax({
                        url: baseUrl + "/api/blog/increase-like-number/" + this.blogId,
                        dataType: "text",
                        type: "put",
                        success: function (data, status, xhr) {
                            //設置過期時間爲1h
                            let date = new Date();
                            date.setTime(date.getTime() + 60 * 60 * 60 * 1000);
                            $.cookie("likeId", blogDetail.blogId, {"path": "/",}, {expires: date});
                        },
                        error: function (error) {   //請求失敗後的回調方法
                            blogDetail.$message({
                                showClose: true,
                                message: error.responseJSON.message,
                                type: 'error'
                            });
                        }
                    });
                }else{
                    blogDetail.$message({
                        showClose: true,
                        message: "您已經點過贊啦!",
                        type: 'success'
                    });
                }
            },
        },
        mounted() {
            this.getBlogObserve();
        },
        created() {
        	//獲取博客的id,利用的是導航欄拼接,這裏可忽略
            this.blogId = window.location.search.substr(1);
        }
    });
});

博客的訪問量和點贊量是利用cookie存儲對應的博客id來實現的,如果有次cookie則不能增加瀏覽量和點贊量,當然也還存在很多的bug,這個訪問量和點贊量只能在一定程度上解決問題,並不完善(瀏覽和點讚的後端實現暫略,不是重點)。

關鍵

利用Element UI實現評論回顯的關鍵代碼如下:

<el-tree
   ref="tree"
   :data="observes"
   :props="defaultProps"
>
   <div class="custom-tree-node" slot-scope="{ node, data }">
        <div class="observe-top">
             <img :src="data.user.picture" alt="頭像" class="observe-header-img">
             <span class="observe-nickname">{{data.user.nickname}}</span>
             <span class="observe-user" v-text="data.lastId==null?'回覆了博主':'回覆了你'"></span>
             <el-tag class="observe-tag" @click="getLastId(data)" round size="mini">回覆</el-tag>
             <span class="observe-date">{{data.createDate}}</span>
        </div>
        <p class="observe-content">{{data.observeContent}}</p>
   </div>
</el-tree>
  • :data="observes"是評論信息集合,及需要展示在屬性控件的數據
  • :props="defaultProps"主要用於配置每個節點的選項
  • slot-scope是樹形控件的插槽

詳細的樹形控件API可參看官網:https://element.eleme.cn/#/zh-CN/component/tree

回顯數據實例展示

多級評論回覆層級結構非常鮮明
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章