Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

oneStarLR/Comment

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

3 Commits

Repository files navigation

零、效果

在网上搜索了很多,发现很多都是用两张表或者使用jpa实现的,本篇文章将讲述使用一张表来实现评论回复楼中楼功能,使用Mybatis作为持久层框架,有图有真相,先来看看最终效果

image

一、数据库设计

首先来看看有哪些字段,既然是评论回复,你觉得应该有哪些字段呢,带着功能去思考这个问题

首先是主键(id),既然是评论,必须要有评论人的姓名(nickname),为了以后能联系到评论人,需要评论人的邮箱(email),然后就是评论内容(content),为了方便显示,还需要评论人的头像(avatar),评论的时间当然少不了(create_time),既然是一张表,要如何确定回复的所属关系呢,那就需要知道是回复谁的评论(parent_comment_id)。bingo!这些字段就OK了,可能你会问,那回复怎么办,问题不大,可以在实体类中创建一个回复评论的集合replyComments,用来存储回复消息。

表结构如下:

建表语句如下:

create database comment;
DROP TABLE IF EXISTS `comment`;
CREATE TABLE `comment` (
 `id` bigint(20) NOT NULL AUTO_INCREMENT,
 `nickname` varchar(255) DEFAULT NULL,
 `email` varchar(255) DEFAULT NULL,
 `content` varchar(255) DEFAULT NULL,
 `avatar` varchar(255) DEFAULT NULL,
 `create_time` datetime DEFAULT NULL,
 `parent_comment_id` bigint(20) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8;

二、搭建SpringBoot框架

  1. 如图,选择相应的组件,创建SpringBoot项目

image

  1. 将application.properties配置文件改成yml后缀,即application.yml,主要对thymeleaf模板、数据库、mybatis、评论头像进行配置,配置如下:
spring:
#配置thymeleaf模板
 thymeleaf:
 mode: HTML
#配置数据库
 datasource:
 driver-class-name: com.mysql.cj.jdbc.Driver
 url: jdbc:mysql://localhost:3306/comment?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
 username: root
 password: 806188
#配置mybatis
mybatis:
 type-aliases-package: com.star.entity
 mapper-locations: classpath:mapper/*.xml
 configuration:
 map-underscore-to-camel-case: true
#配置评论头像
comment.avatar: /images/avatar.png

三、代码编写

1. 实体类

创建实体类,这里除了字段和回复评论列表外父级评论和父级评论姓名的变量,用来设置父级评论的id和显示父级评论姓名的

package com.star.entity;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
 * @Description: 评论实体类
 * @Date: Created in 11:02 2020年4月14日
 * @Author: ONESTAR
 * @QQ: 316392836
 * @URL: https://onestar.newstar.net.cn/
 */
public class Comment {
 private Long id;
 private String nickname;
 private String email;
 private String content;
 private String avatar;
 private Date createTime;
 private Long parentCommentId;
 //回复评论
 private List<Comment> replyComments = new ArrayList<>();
 private Comment parentComment;
 private String parentNickname;
 
 //省去了get和set还有toString方法
}

2. 业务层接口

创建业务层service接口,主要是查询评论列表和存储评论两个接口

package com.star.service;
import com.star.entity.Comment;
import java.util.List;
/**
 * @Description: 评论业务层接口
 * @Date: Created in 11:48 2020年4月14日
 * @Author: ONESTAR
 * @QQ: 316392836
 * @URL: https://onestar.newstar.net.cn/
 */
public interface CommentService {
 //查询评论列表
 List<Comment> listComment();
 //保存评论
 int saveComment(Comment comment);
}

3. 持久层接口

创建持久层dao接口,只要有添加评论、查询父级评论、查询一级回复、查询二级以及所有子集回复

package com.star.dao;
import com.star.entity.Comment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
 * @Description: 评论持久层接口
 * @Date: Created in 12:06 2020年4月14日
 * @Author: ONESTAR
 * @QQ: 316392836
 * @URL: https://onestar.newstar.net.cn/
 */
@Mapper
@Repository
public interface CommentDao {
 //添加一个评论
 int saveComment(Comment comment);
 //查询父级评论
 List<Comment> findByParentIdNull(@Param("ParentId") Long ParentId);
 //查询一级回复
 List<Comment> findParentIdNotNull(@Param("id") Long id);
 //查询二级以及所有子集回复
 List<Comment> findByReplayId(@Param("childId") Long childId);
}

4. Mapper

创建CommentDao.xml文件,主要是添加评论、查询父级评论、查询一级回复、查询二级以及所有子集评论的SQL语句

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.star.dao.CommentDao">
 <!--添加评论-->
 <insert id="saveComment" parameterType="com.star.entity.Comment">
 insert into comment.comment (nickname,email,content,avatar,create_time,parent_comment_id)
 values (#{nickname},#{email},#{content},#{avatar},#{createTime},#{parentCommentId});
 </insert>
 <!--查询父级评论-->
 <select id="findByParentIdNull" resultType="com.star.entity.Comment">
 select *
 from comment.comment c
 where c.parent_comment_id = #{ParentId}
 order by c.create_time desc
 </select>
 <!--查询一级回复-->
 <select id="findByParentIdNotNull" resultType="com.star.entity.Comment">
 select *
 from comment.comment c
 where c.parent_comment_id = #{id}
 order by c.create_time desc
 </select>
 <!--查询二级以及所有子集回复-->
 <select id="findByReplayId" resultType="com.star.entity.Comment">
 select *
 from comment.comment c
 where c.parent_comment_id = #{childId}
 order by c.create_time desc
 </select>
</mapper>

5. 控制器CommentController

package com.star.controller;
import com.star.entity.Comment;
import com.star.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
/**
 * @Description: 评论控制器
 * @Date: Created in 14:30 2020年4月14日
 * @Author: ONESTAR
 * @QQ: 316392836
 * @URL: https://onestar.newstar.net.cn/
 */
@Controller
public class CommentController {
 @Autowired
 private CommentService commentService;
 @Value("${comment.avatar}")
 private String avatar;
 @GetMapping("/")
 public String comment() {
 return "comment";
 }
 @GetMapping("/comment")
 public String comments(Model model) {
 List<Comment> comments = commentService.listComment();
 model.addAttribute("comments", comments);
 return "comment :: commentList";
 }
 @PostMapping("/comment")
 public String post(Comment comment) {
 //设置头像
 comment.setAvatar(avatar);
 if (comment.getParentComment().getId() != null) {
 comment.setParentCommentId(comment.getParentComment().getId());
 }
 commentService.saveComment(comment);
 return "redirect:/comment";
 }
}

6. 接口实现类

逻辑都在接口实现类CommentServiceImpl里面实现,咱们来梳理一下逻辑(逻辑没梳理好,代码永远都是乱的,逻辑是第一步!!!),添加评论直接insert就可以了,这里主要讲一下查询:

  1. 根据id为"-1"查询出所有父评论
  2. 根据父评论的id查询出一级子回复
  3. 根据子回复的id循环迭代查询出所有子集回复
  4. 将查询出来的子回复放到一个集合中

要注意将父评论的姓名给set进去,代码如下:

package com.star.service.impl;
import com.star.dao.CommentDao;
import com.star.entity.Comment;
import com.star.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
 * @Description:
 * @Date: Created in 13:44 2020年4月14日
 * @Author: ONESTAR
 * @QQ: 316392836
 * @URL: https://onestar.newstar.net.cn/
 */
@Service
public class CommentServiceImpl implements CommentService {
 @Autowired
 private CommentDao commentDao;
 //存放迭代找出的所有子代的集合
 private List<Comment> tempReplys = new ArrayList<>();
 /**
 * @Description: 查询评论
 * @Auther: ONESTAR
 * @Date: 17:26 2020年4月14日
 * @Param:
 * @Return: 评论消息
 */
 @Override
 public List<Comment> listComment() {
 //查询出父节点
 List<Comment> comments = commentDao.findByParentIdNull(Long.parseLong("-1"));
 for(Comment comment : comments){
 Long id = comment.getId();
 String parentNickname1 = comment.getNickname();
 List<Comment> childComments = commentDao.findByParentIdNotNull(id);
 //查询出子评论
 combineChildren(childComments, parentNickname1);
 comment.setReplyComments(tempReplys);
 tempReplys = new ArrayList<>();
 }
 return comments;
 }
 /**
 * @Description: 查询出子评论
 * @Auther: ONESTAR
 * @Date: 17:31 2020年4月14日
 * @Param: childComments:所有子评论
 * @Param: parentNickname1:父评论的姓名
 * @Return:
 */
 private void combineChildren(List<Comment> childComments, String parentNickname1) {
 //判断是否有一级子回复
 if(childComments.size() > 0){
 //循环找出子评论的id
 for(Comment childComment : childComments){
 String parentNickname = childComment.getNickname();
 childComment.setParentNickname(parentNickname1);
 tempReplys.add(childComment);
 Long childId = childComment.getId();
 //查询二级以及所有子集回复
 recursively(childId, parentNickname);
 }
 }
 }
 /**
 * @Description: 循环迭代找出子集回复
 * @Auther: ONESTAR
 * @Date: 17:33 2020年4月14日
 * @Param: childId:子评论的id
 * @Param: parentNickname1:子评论的姓名
 * @Return:
 */
 private void recursively(Long childId, String parentNickname1) {
 //根据子一级评论的id找到子二级评论
 List<Comment> replayComments = commentDao.findByReplayId(childId);
 if(replayComments.size() > 0){
 for(Comment replayComment : replayComments){
 String parentNickname = replayComment.getNickname();
 replayComment.setParentNickname(parentNickname1);
 Long replayId = replayComment.getId();
 tempReplys.add(replayComment);
 //循环迭代找出子集回复
 recursively(replayId,parentNickname);
 }
 }
 }
 @Override
 //存储评论信息
 public int saveComment(Comment comment) {
 comment.setCreateTime(new Date());
 return commentDao.saveComment(comment);
 }
}

7. 前端页面comment.html

最后是前端显示页面,这里采用semantic-ui作为UI组件,使用thymeleaf模板解析,记得在static文件夹下创建images文件夹,并放一张名为avatar.png的图片

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>评论楼中楼功能</title>
 <link rel="stylesheet" href="https://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.css">
</head>
<body>
<div id="waypoint" class="m-margin- animated fadeIn">
 <div class="ui container m-opacity box-shadow-max">
 <div class="ui bottom attached segment">
 <!--评论区域列表-->
 <div id="comment-container" class="ui teal segment">
 <div th:fragment="commentList">
 <div class="ui threaded comments" style="max-width: 100%;">
 <h3 class="ui dividing header">评论</h3>
 <div class="comment" th:each="comment : ${comments}">
 <a class="avatar">
 <img src="../static/images/avatar.png" th:src="@{${comment.avatar}}">
 </a>
 <div class="content">
 <a class="author" >
 <span th:text="${comment.nickname}">小红</span>
 </a>
 <div class="metadata">
 <span class="date" th:text="${#dates.format(comment.createTime,'yyyy-MM-dd HH:mm')}">Today at 5:42PM</span>
 </div>
 <div class="text" th:text="${comment.content}">
 愿你走出半生,归来仍是少年!
 </div>
 <div class="actions">
 <a class="reply" data-commentid="1" data-commentnickname="Matt" th:attr="data-commentid=${comment.id},data-commentnickname=${comment.nickname}" onclick="reply(this)">回复</a>
 </div>
 </div>
 <!--子集评论-->
 <div class="comments" th:if="${#arrays.length(comment.replyComments)}>0">
 <div class="comment" th:each="reply : ${comment.replyComments}">
 <a class="avatar">
 <img src="../static/images/avatar.png" th:src="@{${reply.avatar}}">
 </a>
 <div class="content">
 <a class="author" >
 <span th:text="${reply.nickname}">小白</span>
 &nbsp;<span th:text="|@ ${reply.parentNickname}|" class="m-teal">@ 小红</span>
 </a>
 <div class="metadata">
 <span class="date" th:text="${#dates.format(reply.createTime,'yyyy-MM-dd HH:mm')}">Today at 5:42PM</span>
 </div>
 <div class="text" th:text="${reply.content}">
 你也是!
 </div>
 <div class="actions">
 <a class="reply" data-commentid="1" data-commentnickname="Matt" th:attr="data-commentid=${reply.id},data-commentnickname=${reply.nickname}" onclick="reply(this)">回复</a>
 </div>
 </div>
 </div>
 </div>
 </div>
 </div>
 </div>
 </div>
 <div id="comment-form" class="ui form">
 <input type="hidden" name="parentComment.id" value="-1">
 <div class="field">
 <textarea name="content" placeholder="请输入评论信息..."></textarea>
 </div>
 <div class="fields">
 <div class="field m-mobile-wide m-margin-bottom-small">
 <div class="ui left icon input">
 <i class="user icon"></i>
 <input type="text" name="nickname" placeholder="姓名" th:value="${session.user}!=null ? ${session.user.nickname}">
 </div>
 </div>
 <div class="field m-mobile-wide m-margin-bottom-small">
 <div class="ui left icon input">
 <i class="mail icon"></i>
 <input type="text" name="email" placeholder="邮箱" th:value="${session.user}!=null ? ${session.user.email}">
 </div>
 </div>
 <div class="field m-margin-bottom-small m-mobile-wide">
 <button id="commentpost-btn" type="button" class="ui teal button m-mobile-wide"><i class="edit icon"></i>发布</button>
 </div>
 </div>
 </div>
 </div>
 </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.2/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.js"></script>
<script th:inline="javascript">
 //评论表单验证
 $('.ui.form').form({
 fields: {
 title: {
 identifier: 'content',
 rules: [{
 type: 'empty',
 prompt: '请输入评论内容'
 }
 ]
 },
 content: {
 identifier: 'nickname',
 rules: [{
 type: 'empty',
 prompt: '请输入你的大名'
 }]
 },
 type: {
 identifier: 'email',
 rules: [{
 type: 'email',
 prompt: '请填写正确的邮箱地址'
 }]
 }
 }
 });
 $(function () {
 $("#comment-container").load(/*[[@{/comment}]]*/"comment/");
 });
 $('#commentpost-btn').click(function () {
 var boo = $('.ui.form').form('validate form');
 if (boo) {
 console.log('校验成功');
 postData();
 } else {
 console.log('校验失败');
 }
 });
 function postData() {
 $("#comment-container").load(/*[[@{/comment}]]*/"",{
 "parentComment.id" : $("[name='parentComment.id']").val(),
 "nickname": $("[name='nickname']").val(),
 "email" : $("[name='email']").val(),
 "content" : $("[name='content']").val()
 },function (responseTxt, statusTxt, xhr) {
 // $(window).scrollTo($('#goto'),0);
 clearContent();
 });
 }
 function clearContent() {
 $("[name='nickname']").val('');
 $("[name='email']").val('');
 $("[name='content']").val('');
 $("[name='parentComment.id']").val(-1);
 $("[name='content']").attr("placeholder", "请输入评论信息...");
 }
 function reply(obj) {
 var commentId = $(obj).data('commentid');
 var commentNickname = $(obj).data('commentnickname');
 $("[name='content']").attr("placeholder", "@"+commentNickname).focus();
 $("[name='parentComment.id']").val(commentId);
 $(window).scrollTo($('#comment-form'),500);
 }
</script>
</body>
</html>

8. 目录结构

image

源码地址,欢迎star:https://github.com/oneStarLR/SpringBoot-Mybatis-

About

SpringBoot和Mybatis实现评论楼中楼功能(一张表搞定)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

Contributors

AltStyle によって変換されたページ (->オリジナル) /