您的当前位置:首页正文

基于Spring Boot+Vue的博客系统 13——评论功能的实现

2024-11-08 来源:个人技术集锦
需求
  • 评论分为匿名评论和实名评论,匿名评论的评论者(也就是commentator字段)的值为0,头像为默认头像,用户登录之后可以进行实名评论
  • 在文章评论下面,还可以对一级评论进行评论,一级评论和二级评论的区别在于type字段的不同,一级评论为1,二级评论为2
后端设计
  • 新建com.qianyucc.blog.model.dto.CommentDTO类,用于封装前端向后端传输的评论信息
package com.qianyucc.blog.model.dto;

import lombok.*;

/**
 * @author lijing
 * @date 2019-10-13 10:53
 * @description 前端向后端传输的评论信息
 */
@Data
public class CommentDTO {
    private Long id;
    private Long parentId;
    private Long commentator;
    private String content;
    private Integer type;
}
  • 新建com.qianyucc.blog.model.vo.CommentVO类,用于封装后端返回到前端的评论信息
package com.qianyucc.blog.model.vo;

import lombok.*;

import java.util.*;

/**
 * @author lijing
 * @date 2019-10-13 11:18
 * @description 封装返回到前端的评论信息
 */
@Data
public class CommentVO {
    private Long id;
    private Long parentId;
    private Integer type;
    private Long commentator;
    private String content;
    private String gmtCreate;
    private String gmtUpdate;
    private Long likes;
    private Long comments;

    private String commentatorAvatarUrl;
    private String commentatorName;
    private List<CommentVO> secondLevelComments;
}
  • 新建CommentService编写业务层代码,需要注意的是在插入评论的时候要将父级(这里的父级评论数是文章的评论数或者一级评论的评论数)的评论数加一
package com.qianyucc.blog.service;

import cn.hutool.core.bean.*;
import com.qianyucc.blog.model.dto.*;
import com.qianyucc.blog.model.entity.*;
import com.qianyucc.blog.repository.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.stereotype.*;

import java.util.*;

/**
 * @author lijing
 * @date 2019-10-13 10:55
 * @description 与评论相关的业务
 */
@Service
public class CommentService {
    @Autowired
    private ArticleRepository articleRepository;
    @Autowired
    private CommentRepository commentRepository;
    @Autowired
    private UserRepository userRepository;
    /**
     * 插入评论
     *
     * @param commentDTO
     * @return
     */
    public void insComment(CommentDTO commentDTO) {
        // 先将父级评论数或者文章评论数加一
        if (commentDTO.getType().equals(1)) {
            Optional<ArticleDO> byId = articleRepository.findById(commentDTO.getParentId());
            byId.ifPresent(articleDO -> {
                articleDO.setComments(articleDO.getComments()+1);
                articleRepository.save(articleDO);
            });
        } else if (commentDTO.getType().equals(2)) {
            Optional<CommentDO> byId = commentRepository.findById(commentDTO.getParentId());
            byId.ifPresent(commentDO -> {
                commentDO.setComments(commentDO.getComments() + 1);
                commentRepository.save(commentDO);
            });
        }

        CommentDO commentDO = new CommentDO();
        BeanUtil.copyProperties(commentDTO, commentDO);
        commentDO.setComments(0L);
        commentDO.setLikes(0L);
        commentDO.setGmtCreate(System.currentTimeMillis());
        commentDO.setGmtUpdate(commentDO.getGmtCreate());
        commentRepository.save(commentDO);
    }
    
    /**
     * 根据文章的id查询该文章的所有评论
     *
     * @param articleId
     * @return
     */
    public List<CommentVO> findCommentByArticleId(Long articleId) {
        List<CommentDO> commentDOS = commentRepository.findByParentIdAndType(articleId, 1);
        List<CommentVO> commentVOS = CommentUtil.jpaDosToVos(commentDOS, userRepository);
        // 查找所有一级评论对应的二级评论
        commentVOS.forEach(commentVO -> {
            List<CommentDO> secondLevelCommentDOS = commentRepository.findByParentIdAndType(commentVO.getId(), 2);
            List<CommentVO> secondLevelCommentAOS = CommentUtil.jpaDosToVos(secondLevelCommentDOS, userRepository);
            commentVO.setSecondLevelComments(secondLevelCommentAOS);
        });
        return commentVOS;
    }
}

上面的业务层代码用到了自定义工具类CommentUtil,并且在CommentRepository里面定义根据parentIdtype字段查询的方法

package com.qianyucc.blog.repository;

import com.qianyucc.blog.model.entity.*;
import org.springframework.data.jpa.repository.*;

import java.util.*;

/**
 * @author lijing
 * @date 2019-10-11 10:40
 * @description 访问数据库中评论
 */
public interface CommentRepository extends JpaRepository<CommentDO, Long>, JpaSpecificationExecutor<CommentDO> {
    List<CommentDO> findByParentIdAndType(Long articleId, Integer type);
}
package com.qianyucc.blog.utils;

import cn.hutool.core.bean.*;
import com.qianyucc.blog.model.entity.*;
import com.qianyucc.blog.model.vo.*;
import com.qianyucc.blog.repository.*;

import java.util.*;

/**
 * @author lijing
 * @date 2019-10-13 11:19
 * @description 与评论相关的工具方法
 */
public class CommentUtil {
    private static final String DATE_PATTERN = "yyyy-MM-dd HH:mm";
    /**
     * 将model转换为ao,并格式化日期
     *
     * @param commentDOS
     * @return
     */
    public static List<CommentVO> jpaDosToVos(List<CommentDO> commentDOS, UserRepository userRepository) {
        ArrayList<CommentVO> commentVOS = new ArrayList<>();
        commentDOS.forEach(commentDO -> {
            CommentVO commentVO = new CommentVO();
            BeanUtil.copyProperties(commentDO, commentVO);
            Optional<UserDO> byId = userRepository.findById(commentDO.getCommentator());
            byId.ifPresent(userDO -> {
                commentVO.setCommentatorAvatarUrl(userDO.getAvatarUrl());
                commentVO.setCommentatorName(userDO.getName());
            });
            commentVO.setGmtCreate(BlogUtil.formatDate(commentDO.getGmtCreate(), DATE_PATTERN));
            commentVO.setGmtUpdate(BlogUtil.formatDate(commentDO.getGmtUpdate(), DATE_PATTERN));
            commentVOS.add(commentVO);
        });
        return commentVOS;
    }
}
  • 编写Controller
package com.qianyucc.blog.controller.comm;

import com.qianyucc.blog.model.dto.*;
import com.qianyucc.blog.model.vo.*;
import com.qianyucc.blog.service.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.web.bind.annotation.*;

import java.util.*;

/**
 * @author lijing
 * @date 2019-10-13 12:19
 * @description 与评论相关的api
 */
@RestController
@RequestMapping("/api/comm/comment")
public class CommentController {

    @Autowired
    private CommentService commentService;

    @PostMapping("/insComment")
    public RespDataVO comment(@RequestBody CommentDTO commentDTO) {
        commentService.insComment(commentDTO);
        return RespDataVO.ok("评论成功!");
    }

    @GetMapping("/getComments")
    public List<CommentVO> getAllComments(Long articleId) {
        List<CommentVO> commentVOS = commentService.findCommentByArticleId(articleId);
        return commentVOS;
    }
}
前端数据渲染
  • /src/request/api/url.js中添加与评论相关的url
  • 新建/src/request/api/comment.js封装请求api
import url from '@/request/api/url'
// 导入axios实例
import axios from '@/request/http'

export default {
  submitComment(commentInfo, callback) {
    axios
      .post(url.doCommentUrl, commentInfo)
      .then(callback)
      .catch(err => {
        console.log('submitComment Error');
      })
  },
  getAllComments(articleId, callback) {
    axios
      .get(url.getCommentsUrl, {
        params: {
          articleId: articleId
        }
      })
      .then(callback)
      .catch(err => {
        console.log("getAllComments Error");
      })
  }
}
  • 别忘了在/src/request/api/index.js中导出

  • articleDetails.vue中定义数据存储评论信息,并导入store中的isLoginuserInfo

data() {
  return {
  	// 文章信息
    article: {},
    // 所有评论
    comments: [],
    // 文章内容
    content: null,
    // 与二级评论内容绑定
    slcContent: null,
    // 与一级评论内容绑定
    commentContent: null
  };
},
computed: {
  ...mapState({
    isLogin: state => state.app.isLogin,
    userInfo: state => state.user.userInfo
  })
},
  • 定义获取所有评论的方法
getAllComments() {
  this.$api.comment.getAllComments(this.article.id, resp => {
    this.comments = resp.data;
  });
}
  • 定义提交一级评论和提交二级评论的方法,注意每一次提交评论之后都要重新刷新一次评论列表
submitSlComment(id) {
    if (this.slcContent == null || this.slcContent == "") {
      return;
    }
    this.$api.comment.submitComment(
      {
        content: this.slcContent,
        type: 2,
        parentId: id,
        commentator: this.isLogin ? this.userInfo.id : 0
      },
      resp => {
        this.slcContent = "";
        // 重新获取所有评论
        this.getAllComments();
      }
    );
  },
  submitComment() {
    if (!this.commentContent || this.commentContent == "") {
      return;
    }
    this.$api.comment.submitComment(
      {
        content: this.commentContent,
        type: 1,
        parentId: this.article.id,
        commentator: this.isLogin ? this.userInfo.id : 0
      },
      resp => {
        this.commentContent = "";
        // 重新获取所有评论
        this.getAllComments();
      }
    );
  }
}
  • 在页面的created()方法中获取文章信息之后再获取该文章的所有评论
created() {
  this.$api.article.getArticleById(this.$route.params.articleId, resp => {
    this.article = resp.data;
    let converter = new showdown.Converter({
      // 使代码高亮显示
      extensions: [showdownHighlight],
      // 启用后可以为图片设置尺寸
      parseImgDimensions: true
    });
    // markdown 转 html
    this.content = converter.makeHtml(this.article.content);
    // 获取所有评论,因为axios为异步操作,下面的操作不能写在该函数的外面
    this.getAllComments();
  });
}
  • 将数据渲染到页面上:
<template>
  <b-container class="main">
    <!-- 文章标题 -->
    <h2 class="title">{{article.title}}</h2>
    <!-- 文章描述 -->
    <h6 class="description">
      <b-badge variant="info">{{article.type==1 ? '原创' : '转载'}}</b-badge>&nbsp;&nbsp;
      <i class="icon iconfont icon-riqi"></i>
      &nbsp;{{article.gmtUpdate}}
      &nbsp;&nbsp;
      <i
        class="icon iconfont icon-gaojian-zuozhe"
      ></i>
      &nbsp;{{article.author}}
      &nbsp;&nbsp;
      <i class="icon iconfont icon-yuedu"></i>
      &nbsp;{{article.views}}
      &nbsp;&nbsp;
      <i class="icon iconfont icon-fenlei"></i>
      &nbsp;{{article.category}}
    </h6>
    <!-- 文章内容 -->
    <div v-html="content"></div>
    <!-- 标签 -->
    <div class="tag-box">
      <b-link class="tag" variant="info" v-for="(tag,index) in article.tags" :key="index">
        <i class="icon iconfont icon-tag"></i>
        {{tag}}
      </b-link>
    </div>
    <!-- 评论回复 -->
    <hr />
    <h5>总共有{{comments.length}}条评论</h5>
    <hr />
    <!-- 一级评论列表 start -->
    <b-media v-for="comment in comments" :key="comment.id">
      <template v-slot:aside>
        <b-img
          width="60"
          height="60"
          :src="comment.commentator==0 ? '/static/images/no-name.png' : comment.commentatorAvatarUrl"
        ></b-img>
      </template>
      <h6
        thumbnail
        class="commentator-name"
        v-text="comment.commentator==0 ? '匿名用户' : comment.commentatorName"
      ></h6>
      <p>{{comment.content}}</p>
      <p>
        <i class="icon iconfont icon-dianzan1 link-icon"></i>
        <i
          class="icon iconfont icon-pinglun link-icon"
          @click="showOrHideSecondLevelComments(comment.id)"
        ></i>
        {{comment.secondLevelComments.length}}
      </p>
      <div :id="'second-level-comment-'+comment.id" style="display:none;">
        <!-- 二级评论列表 start -->
        <b-media v-for="cll in comment.secondLevelComments" :key="cll.id">
          <template v-slot:aside>
            <b-img
              width="60"
              height="60"
              :src="cll.commentator==0 ? '/static/images/no-name.png' : cll.commentatorAvatarUrl"
            ></b-img>
          </template>
          <h6
            thumbnail
            class="commentator-name"
            v-text="cll.commentator==0 ? '匿名用户' : cll.commentatorName"
          ></h6>
          <p>{{cll.content}}</p>
        </b-media>
        <!-- 二级评论 end -->
        <hr />
        <b-textarea placeholder="请输入评论内容......" v-model="slcContent"></b-textarea>
        <b-button class="pull-right" variant="success" @click="submitSlComment(comment.id)">提交</b-button>
      </div>
    </b-media>
    <!-- 一级评论列表 end -->
    <hr />
    <b-media>
      <template v-slot:aside>
        <b-img
          width="60"
          height="60"
          :src="isLogin ? userInfo.avatarUrl : '/static/images/no-name.png'"
        ></b-img>
      </template>
      <h6 thumbnail class="commentator-name" v-text="isLogin ? userInfo.name : '匿名用户'"></h6>
    </b-media>
    <b-textarea placeholder="请输入评论内容......" v-model="commentContent"></b-textarea>
    <b-button variant="success" class="pull-right" @click="submitComment()">提交</b-button>
    <!-- 回顶部按钮 -->
    <i class="icon iconfont icon-huidingbu" @click="backTop()"></i>
  </b-container>
</template>
  • 效果如下:
Top