SpringBoot+Spring Data JPA 实现留言评论,评论回复

先看效果图

样式是模仿哔哩哔哩的,支持二级评论。

SpringBoot+Spring Data JPA 实现留言评论,评论回复

 

动态效果图如下,点击图片放大

SpringBoot+Spring Data JPA 实现留言评论,评论回复

 

 

 

一、数据库设计

Comment 表

SpringBoot+Spring Data JPA 实现留言评论,评论回复

id:主键,自定生成

content:评论内容,限制500个字符

create_time:创建时间,自动生成

user_id:评论的用户的id

post_id:评论所在文章的id

pid:评论的父id,注意A评论下的所有子评论,我这里都设计为他的儿子,而不是儿子,孙子,曾孙。即,回复的回复,回复的回复的回复的pid都是某个一级评论。这样设计,主要是为了二级显示和避免太多递归(多级显示太麻烦了,/(ㄒoㄒ)/~~)

reply_user_id:被回复人的id,用于@对方时候显示。对一级评论进行评论不需要@他,所以reply_user_id 为空,对回复回复需要@对方,否则不知道回复谁。

 

再补充一下,解释一下上面的。

如图,柯南的评论属于一级评论,有楼层号显示。小兰回复柯南,灰原哀回复小兰,琴酒回复灰原哀。小兰、灰原哀、琴酒的 pid 都是柯南的 id,而柯南的 pid 为 0。

 

SpringBoot+Spring Data JPA 实现留言评论,评论回复

 

 

二、JPA 实体

Post.java  文章实体

  1. package com.liuyanzhao.forum.entity;
  2. import lombok.Data;
  3. import org.hibernate.annotations.Where;
  4. import javax.persistence.*;
  5. import javax.validation.constraints.NotEmpty;
  6. import javax.validation.constraints.Size;
  7. import java.io.Serializable;
  8. import java.util.List;
  9. /**
  10.  * @author 言曌
  11.  * @date 2018/3/19 下午9:54
  12.  */
  13. @Entity
  14. @Data
  15. public class Post implements Serializable {
  16.     private static final long serialVersionUID = -5086173193716866676L;
  17.     @Id
  18.     @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增长策略
  19.     @Column(name = "id", nullable = false)
  20.     private Long id;
  21.     @NotEmpty(message = "标题不能为空")
  22.     @Size(max = 200, message = "标题不能超过200个字符")
  23.     @Column(nullable = false, length = 200)
  24.     private String title;
  25.     @Lob  // 大对象,映射 MySQL 的 Long Text 类型
  26.     @Basic(fetch = FetchType.LAZY) // 懒加载
  27.     @NotEmpty(message = "内容不能为空")
  28.     @Size(max = 100000, message = "内容不能超过100000个字符")
  29.     @Column(nullable = false)
  30.     private String content;//文章全文内容
  31.     @Column(name = "comment_size")
  32.     private Integer commentSize = 0;  // 评论数
  33.     //当文章删除后,与之关联的评论也会被删除
  34.     @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
  35.     @OrderBy("id DESC")
  36.     @Where(clause = "pid=0")
  37.     private List<Comment> commentList;
  38.     public Post() {
  39.     }
  40. }

这里去掉了不必要的代码

 

Comment.java 评论实体

  1. package com.liuyanzhao.forum.entity;
  2. import lombok.Data;
  3. import javax.persistence.*;
  4. import javax.validation.constraints.NotEmpty;
  5. import javax.validation.constraints.Size;
  6. import java.io.Serializable;
  7. import java.sql.Timestamp;
  8. import java.util.List;
  9. /**
  10.  * 评论实体
  11.  * @author 言曌
  12.  * @date 2018/3/19 下午9:54
  13.  */
  14. @Entity
  15. @Data
  16. public class Comment implements Serializable {
  17.     private static final long serialVersionUID = -4502134548520740266L;
  18.     @Id
  19.     @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增长策略
  20.     private Long id;
  21.     @Column(name = "pid", nullable = false)
  22.     private Long pid = 0L;//父评论,如果不设置,默认为0
  23.     @NotEmpty(message = "评论内容不能为空")
  24.     @Size(max = 500, message = "评论内容不能多于500个字符")
  25.     @Column(nullable = false)
  26.     private String content;
  27.     @OneToOne(fetch = FetchType.LAZY)
  28.     @JoinColumn(name = "user_id")
  29.     private User user;
  30.     @Column(nullable = false)
  31.     @org.hibernate.annotations.CreationTimestamp  // 由数据库自动创建时间
  32.     private Timestamp createTime;
  33.     @ManyToOne(fetch = FetchType.LAZY)
  34.     @JoinColumn(name = "post_id")
  35.     private Post post;//所属文章
  36.     @OneToMany(fetch = FetchType.LAZY)//不设置级联,使用懒加载
  37.     @JoinColumn(name = "pid", referencedColumnName = "id")
  38.     @OrderBy("id ASC")
  39.     private List<Comment> commentList;
  40.     @JoinColumn(name = "reply_user_id")
  41.     @OneToOne(fetch = FetchType.LAZY)//不设置级联,使用懒加载
  42.     private User replyUser;
  43.     public Comment() {
  44.     }
  45.     public Comment(User user,Post post,  String content) {
  46.         this.post = post;
  47.         this.content = content;
  48.         this.user = user;
  49.     }
  50.     public Comment(User user, Post post, Long pid, String content) {
  51.         this.user = user;
  52.         this.post = post;
  53.         this.pid = pid;
  54.         this.content = content;
  55.     }
  56.     @Override
  57.     public String toString() {
  58.         return "Comment{" +
  59.                 "id=" + id +
  60.                 ", pid=" + pid +
  61.                 ", content='" + content + '\'' +
  62.                 ", createTime=" + createTime +
  63.                 '}';
  64.     }
  65. }

 

三、DAO 层

本项目采用的是 Spring Data JPA 作为 ORM 框架

  1. package com.liuyanzhao.forum.repository;
  2. import com.liuyanzhao.forum.entity.Comment;
  3. import com.liuyanzhao.forum.entity.Post;
  4. import org.springframework.data.jpa.repository.JpaRepository;
  5. /**
  6.  * @author 言曌
  7.  * @date 2018/4/1 上午9:32
  8.  */
  9. public interface CommentRepository extends JpaRepository<Comment, Long> {
  10.     /**
  11.      * 统计某篇文章有多少条评论
  12.      * @param post
  13.      * @return
  14.      */
  15.     Integer countByPost(Post post);
  16. }

因为我们在 Post 实体中关联查询评论列表,只查询一级评论,其中二级评论由 Comment 实体中的 CommentList 查询。

  1. @Where(clause = "pid=0")
  2. private List<Comment> commentList;

但是,前台需要显示评论数,也就是 Post 表中的字段 commentSize 需要及时更改,所以我们采用每次从数据库查询的方式来解决。

如果我们不使用 @Where(clause = "pid=0")  ,获取评论数可以在 Post 实体中添加这个方法,用于设置评论数

  1. /**
  2.  * 修改评论数
  3.  *
  4.  */
  5. public void updateCommentSize() {
  6.     this.commentSize = this.commentList.size();
  7. }

但是,我们设置了  @Where(clause = "pid=0") ,所以获取的 size 是不准确。所以,这里解释了一下。

 

四、Service 层

CommentServiceImpl.java

  1. package com.liuyanzhao.forum.service.impl;
  2. import com.liuyanzhao.forum.entity.Comment;
  3. import com.liuyanzhao.forum.entity.Post;
  4. import com.liuyanzhao.forum.entity.User;
  5. import com.liuyanzhao.forum.repository.CommentRepository;
  6. import com.liuyanzhao.forum.repository.PostRepository;
  7. import com.liuyanzhao.forum.service.CommentService;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.security.core.context.SecurityContextHolder;
  10. import org.springframework.stereotype.Service;
  11. import org.springframework.transaction.annotation.Transactional;
  12. /**
  13.  * @author 言曌
  14.  * @date 2018/4/18 下午3:33
  15.  */
  16. @Service
  17. public class CommentServiceImpl implements CommentService {
  18.     @Autowired
  19.     private CommentRepository commentRepository;
  20.     @Autowired
  21.     private PostRepository postRepository;
  22.     @Override
  23.     @Transactional
  24.     public void removeComment(Long id) {
  25.         commentRepository.deleteById(id);
  26.     }
  27.     @Override
  28.     public Integer countCommentSizeByPost(Post post) {
  29.         return commentRepository.countByPost(post);
  30.     }
  31.     @Override
  32.     public Comment getCommentById(Long id) {
  33.         return commentRepository.findById(id).get();
  34.     }
  35.     @Override
  36.     public void createComment(Long postId, String commentContent) {
  37.         Post post = postRepository.findById(postId).get();
  38.         User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
  39.         Comment comment = new Comment(user, post, commentContent);
  40.         commentRepository.save(comment);
  41.     }
  42.     @Override
  43.     public void replyComment(Long postId, Long commentId, Long replyId, String commentContent) {
  44.         Post post = postRepository.findById(postId).get();
  45.         User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
  46.         Comment comment = new Comment(user, post, commentId, commentContent);
  47.         //评论回复,需要加上@用户
  48.         if (replyId != null) {
  49.             Comment replyComment = commentRepository.findById(replyId).get();
  50.             comment.setReplyUser(replyComment.getUser());
  51.         }
  52.         //添加评论
  53.        commentRepository.save(comment);
  54.     }
  55. }

 

五、Controller 层

PostController.java

文章详情页方法

  1. /**
  2.      * 文章详情页
  3.      *
  4.      * @param username
  5.      * @param id
  6.      * @param model
  7.      * @return
  8.      */
  9.     @GetMapping("/u/{username}/posts/{id}")
  10.     public String getPostById(@PathVariable("username") String username, @PathVariable("id") Long id, Model model) {
  11.         //1、每次读取,简单的可以认为阅读量增加1次
  12.         try {
  13.             postService.readingIncrease(id);
  14.         } catch (Exception e) {
  15.             //文章不存在
  16.         }
  17.         // 2、判断操作用户是否是博客的所有者
  18.         boolean isPostOwner = false;
  19.         if (SecurityContextHolder.getContext().getAuthentication() != null && SecurityContextHolder.getContext().getAuthentication().isAuthenticated()
  20.                 && !SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString().equals("anonymousUser")) {
  21.             User principal = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
  22.             if (principal != null && username.equals(principal.getUsername())) {
  23.                 isPostOwner = true;
  24.             }
  25.         }
  26.         Post post = postService.getPostById(id);
  27.         model.addAttribute("site_title", post.getTitle() + "-" + SITE_NAME);
  28.         model.addAttribute("isPostOwner", isPostOwner);
  29.         model.addAttribute("post", post);
  30.         return "home/post/detail";
  31.     }

 

CommentController.java

  1. package com.liuyanzhao.forum.controller;
  2. import com.liuyanzhao.forum.entity.Comment;
  3. import com.liuyanzhao.forum.entity.Post;
  4. import com.liuyanzhao.forum.entity.User;
  5. import com.liuyanzhao.forum.repository.PostRepository;
  6. import com.liuyanzhao.forum.service.CommentService;
  7. import com.liuyanzhao.forum.service.PostService;
  8. import com.liuyanzhao.forum.util.ConstraintViolationExceptionHandler;
  9. import com.liuyanzhao.forum.vo.Response;
  10. import org.springframework.beans.factory.annotation.Autowired;
  11. import org.springframework.http.ResponseEntity;
  12. import org.springframework.security.access.prepost.PreAuthorize;
  13. import org.springframework.security.core.context.SecurityContextHolder;
  14. import org.springframework.stereotype.Controller;
  15. import org.springframework.web.bind.annotation.*;
  16. import javax.validation.ConstraintViolationException;
  17. import java.util.List;
  18. /**
  19.  * @author 言曌
  20.  * @date 2018/4/18 下午3:29
  21.  */
  22. @Controller
  23. @RequestMapping("/comments")
  24. public class CommentController {
  25.     @Autowired
  26.     private PostRepository postRepository;
  27.     @Autowired
  28.     private CommentService commentService;
  29.     /**
  30.      * 发表评论
  31.      *
  32.      * @param postId
  33.      * @param commentId
  34.      * @param commentContent
  35.      * @return
  36.      */
  37.     @PostMapping
  38.     @PreAuthorize("hasAnyAuthority('ROLE_ADMIN','ROLE_USER')")  // 指定角色权限才能操作方法
  39.     public ResponseEntity<Response> createComment(Long postId, Long replyId, Long commentId, String commentContent) {
  40.         try {
  41.             postRepository.findById(postId).get();
  42.         } catch (Exception e) {
  43.             return ResponseEntity.ok().body(new Response(false"文章不存在!"));
  44.         }
  45.         try {
  46.             //1、评论
  47.             if (commentId == null) {
  48.                 commentService.createComment(postId, commentContent);
  49.             } else {
  50.                 //回复
  51.                 commentService.replyComment(postId, commentId, replyId, commentContent);
  52.             }
  53.             //2、修改文章的评论数目
  54.             Post originalPost = postRepository.findById(postId).get();
  55.             originalPost.setCommentSize(commentService.countCommentSizeByPost(originalPost));
  56.             postRepository.save(originalPost);
  57.         } catch (ConstraintViolationException e) {
  58.             return ResponseEntity.ok().body(new Response(false, ConstraintViolationExceptionHandler.getMessage(e)));
  59.         } catch (Exception e) {
  60.             return ResponseEntity.ok().body(new Response(false, e.getMessage()));
  61.         }
  62.         return ResponseEntity.ok().body(new Response(true"处理成功!"null));
  63.     }
  64.     /**
  65.      * 删除评论
  66.      *
  67.      * @return
  68.      */
  69.     @DeleteMapping("/{id}")
  70.     @PreAuthorize("hasAnyAuthority('ROLE_ADMIN','ROLE_USER')")  // 指定角色权限才能操作方法
  71.     public ResponseEntity<Response> delete(@PathVariable("id") Long id) {
  72.         boolean isOwner = false;
  73.         User user;
  74.         try {
  75.             user = commentService.getCommentById(id).getUser();
  76.         } catch (Exception e) {
  77.             return ResponseEntity.ok().body(new Response(false"评论不存在!"));
  78.         }
  79.         // 判断操作用户是否是评论的所有者
  80.         if (SecurityContextHolder.getContext().getAuthentication() != null && SecurityContextHolder.getContext().getAuthentication().isAuthenticated()
  81.                 && !SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString().equals("anonymousUser")) {
  82.             User principal = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
  83.             if (principal != null && user.getUsername().equals(principal.getUsername())) {
  84.                 isOwner = true;
  85.             }
  86.         }
  87.         if (!isOwner) {
  88.             return ResponseEntity.ok().body(new Response(false"没有操作权限!"));
  89.         }
  90.         try {
  91.             Comment originalComment = commentService.getCommentById(id);
  92.             //1、先删除子评论
  93.             List<Comment> commentList = originalComment.getCommentList();
  94.             if (commentList != null && commentList.size() != 0) {
  95.                 for (int i = 0; i < commentList.size(); i++) {
  96.                     Comment comment = commentList.get(i);
  97.                     commentService.removeComment(comment.getId());
  98.                 }
  99.             }
  100.             //2、删除该评论
  101.             commentService.removeComment(id);
  102.             //3、修改文章的评论数目
  103.             Post originalPost = originalComment.getPost();
  104.             originalPost.setCommentSize(commentService.countCommentSizeByPost(originalPost));
  105.             postRepository.save(originalPost);
  106.         } catch (ConstraintViolationException e) {
  107.             return ResponseEntity.ok().body(new Response(false, ConstraintViolationExceptionHandler.getMessage(e)));
  108.         } catch (Exception e) {
  109.             return ResponseEntity.ok().body(new Response(false, e.getMessage()));
  110.         }
  111.         return ResponseEntity.ok().body(new Response(true"处理成功"null));
  112.     }
  113. }

 

六、视图层

detail.html

这里只贴出评论列表的代码

  1. <div class="comment-list">
  2.     <div class="list-item reply-wrap"
  3.          th:each="comment,userStat:${post.commentList}"
  4.          data-th-attr="data-commentid=${comment.id}">
  5.         <div class="user-face">
  6.             <a th:href="@{'/u/'+${comment.user.username}}"
  7.                target="_blank">
  8.                 <img th:src="${comment.user.avatar}"
  9.                      th:alt="${comment.user.nickname}">
  10.             </a>
  11.         </div>
  12.         <div class="con">
  13.             <div class="user">
  14.                 <a th:href="@{'/u/'+${comment.user.username}}"
  15.                    target="_blank"
  16.                    class="name"
  17.                    th:text="${comment.user.nickname}"></a>
  18.                 </a>
  19.                 <span class="comment-author-mark"
  20.                       th:if="${comment.user.id==post.user.id}">作者</span>
  21.                 <span th:text="${'Lv'+comment.user.badge.level}"
  22.                       th:class="${comment.user.badge.style}"></span>
  23.             </div>
  24.             <p class="text" th:text="${comment.content}"></p>
  25.             <div class="info">
  26.                 <span class="floor"
  27.                       th:text="${(userStat.size-userStat.count+1)+'楼'}"></span>
  28.                 <span class="time"
  29.                       th:text="${comment.getRelativeCreateTime()}"></span>
  30.                 <span class="reply btn-hover"
  31.                       sec:authorize="isAuthenticated()">回复</span>
  32.                 <div class="operation"
  33.                      sec:authorize="isAuthenticated()">
  34.                     <div class="spot"></div>
  35.                     <div class="opera-list" style="display: none;">
  36.                         <ul>
  37.                             <li class="comment-delete-btn"
  38.                                 th:if="${session.user != null and comment.user.id==session.user.id}">
  39.                                 删除
  40.                             </li>
  41.                             <li class="comment-report-btn"
  42.                                 th:unless="${session.user != null and comment.user.id==session.user.id}">
  43.                                 举报
  44.                             </li>
  45.                         </ul>
  46.                     </div>
  47.                 </div>
  48.             </div>
  49.             <div class="reply-box">
  50.                 <div class="reply-item reply-wrap"
  51.                      th:each="reply:${comment.commentList}"
  52.                      data-th-attr="data-commentid=${reply.id}">
  53.                     <a th:href="@{'/u/'+${reply.user.username}}"
  54.                        target="_blank" class="reply-face">
  55.                         <img th:src="${reply.user.avatar}"
  56.                              th:alt="${reply.user.nickname}">
  57.                     </a>
  58.                     <div class="reply-con">
  59.                         <div class="user">
  60.                             <a th:href="@{'/u/'+${reply.user.username}}"
  61.                                target="_blank"
  62.                                class="name "
  63.                                th:text="${reply.user.nickname}">言曌</a>
  64.                             <a th:href="@{/help/level}"
  65.                                target="_blank">
  66.                             </a>
  67.                             <span th:if="${reply.replyUser != null}">回复
  68.                                  <a th:href="@{'/u/'+${reply.replyUser.username}}"
  69.                                     class="name"
  70.                                     th:text="${' '+reply.replyUser.nickname}"></a>
  71.                              </span>
  72.                             <span class="text-con"
  73.                                   th:text="${reply.content}"></span>
  74.                         </div>
  75.                         <div class="info">
  76.                             <span class="time"
  77.                                   th:text="${reply.getRelativeCreateTime()}"></span>
  78.                             <span class="reply btn-hover"
  79.                                   data-th-attr="data-replyid=${reply.id}">回复</span>
  80.                             <div class="operation"
  81.                                  sec:authorize="isAuthenticated()">
  82.                                 <div class="spot"></div>
  83.                                 <div class="opera-list"
  84.                                      style="display: none;">
  85.                                     <ul>
  86.                                         <li class="comment-delete-btn"
  87.                                             th:if="${session.user != null and reply.user.id==session.user.id}">
  88.                                             删除
  89.                                         </li>
  90.                                         <li class="comment-report-btn"
  91.                                             th:unless="${session.user != null and reply.user.id==session.user.id}">
  92.                                             举报
  93.                                         </li>
  94.                                     </ul>
  95.                                 </div>
  96.                             </div>
  97.                         </div>
  98.                     </div>
  99.                 </div>
  100.             </div>
  101.         </div>
  102.     </div>
  103. </div>

 

main.js

  1. // 提交评论
  2. $(document).on("click"".comment-submit"function () {
  3.     var _ctx = $("meta[name='ctx']").attr("content");
  4.     var token = $("meta[name='_csrf']").attr("content");
  5.     var header = $("meta[name='_csrf_header']").attr("content");
  6.     var postId = $("#comment").attr("data-postid");
  7.     var commentId = $(this).attr("data-commentid");
  8.     var replyId = $(this).attr("data-replyid");
  9.     var commentContent = $(this).prev(".comment-content").val();
  10.     if (commentContent.length < 1) {
  11.         layer.msg("评论不可为空!", {icon: 2, anim: 6});
  12.         return false;
  13.     }
  14.     if (commentContent.length > 500) {
  15.         layer.msg("单条评论文字不能超过500个字符!", {icon: 2, anim: 6});
  16.         return false;
  17.     }
  18.     $.ajax({
  19.         url: _ctx + "/comments",
  20.         type: 'POST',
  21.         data: {
  22.             postId: postId,
  23.             commentId: commentId,
  24.             replyId: replyId,
  25.             commentContent: commentContent
  26.         },
  27.         beforeSend: function (request) {
  28.             request.setRequestHeader(header, token); // 添加  CSRF Token
  29.         },
  30.         success: function (data) {
  31.             if (data.success) {
  32.                 layer.msg("评论成功!", {icon: 1});
  33.                 $("#comment-wrapper").load(window.location.href + " #comment");
  34.             } else {
  35.                 layer.alert(data.message, {icon: 2});
  36.             }
  37.         },
  38.         error: function () {
  39.             layer.msg("出现错误,请尝试刷新页面!", {icon: 2, anim: 6});
  40.         }
  41.     });
  42. });
  43. //删除评论
  44. $(document).on('click', '.comment-delete-btn', function () {
  45.     var _ctx = $("meta[name='ctx']").attr("content");
  46.     var token = $("meta[name='_csrf']").attr("content");
  47.     var header = $("meta[name='_csrf_header']").attr("content");
  48.     //你确定要删除吗?
  49.     var currentNode = $(this);
  50.     var postId = $("#comment").attr("data-postid");
  51.     var commentId = currentNode.parents(".reply-wrap").attr("data-commentid");
  52.     //删除
  53.     $.ajax({
  54.         url: _ctx + "/comments/" + commentId + "?postId=" + postId,
  55.         type: 'DELETE',
  56.         beforeSend: function (request) {
  57.             request.setRequestHeader(header, token); // 添加  CSRF Token
  58.         },
  59.         success: function (data) {
  60.             if (data.success) {
  61.                 layer.msg("删除成功!", {icon: 1});
  62.                 var count = $(".results").html();
  63.                 currentNode.parents(".reply-wrap:first").remove();
  64.                 $(".results").load(window.location.href + " .results");
  65.             } else {
  66.                 layer.alert(data.message, {icon: 2});
  67.             }
  68.         },
  69.         error: function () {
  70.             layer.msg("出现错误,请尝试刷新页面!", {icon: 2, anim: 6});
  71.         }
  72.     });
  73. });

 

css 代码比较多,这里就不贴出来

 

 

 

 

  • 微信
  • 交流学习,有偿服务
  • weinxin
  • 博客/Java交流群
  • 资源分享,问题解决,技术交流。群号:590480292
  • weinxin
言曌

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

目前评论:2   其中:访客  2   博主  0

    • avatar

      自己没事想捅咕捅咕,没想到你这 这么全~
      不过,没CSS,样式不能显示!
      你那还有源码么?求分享一下
      a1016844010@qq.com邮箱

        • avatar 阿萨

          @ 6666啊