SpringBoot聚合项目:达内知道(十)-开发新增评论功能

开发新增评论功能

  我们已经完成了讲师回复和显示讲师回答列表的功能,首先明确问题-->回答-->评论的关系:

  • 一个问题可以有多个回答

  • 一个回答可以有多个评论

  • 评论直接关联回答id,和问题没有关系

1.1 为comment表添加用户昵称列

  我们可以通过sql语句在不删除当前表的前提下为表新增列,comment表中没有用户昵称列,这样显示数据非常不方便,甚至需要连表查询,所以我们新增用户昵称列,sql语句如下:

 -- 为指定表新增列的代码
 ALTER TABLE comment
 ADD COLUMN user_nick_name VARCHAR(255)
 AFTER user_id
 
 -- 编写修改语句,将用户对应的昵称赋值
 UPDATE comment c SET user_nick_name=(
 SELECT nickname FROM user u WHERE
  u.id=c.user_id
 )

  经过上面操作,我们的comment表中就新增了user_nick_name列,并赋上了对应的昵称值。

数据库列变化了,那么对应数据库的实体类也已经要新增一个属性。

Comment实体类新增属性如下:

 /**
  * 用户昵称
  */
 @TableField("user_nick_name")
 private String userNickName;

1.2 编写新增评论的控制层

先在Vo包创建一个新增评论用的Vo类CommentVo,代码如下:

 @Data
 @Accessors(chain = true)
 public class CommentVo implements Serializable {
     @NotNull(message = "回答id不能为空")
     private Integer answerId;
 
     @NotBlank(message = "评论内容不能为空")
     private String content;
 }

编写控制器方法来接收表单提交的信息:

 @RestController
 @RequestMapping("/v1/comments")
 @Slf4j
 public class CommentController {
     //不写("") 也可以,这个方法映射/v1/comments的请求
     @PostMapping
     public Comment postComment(
             @Validated CommentVo commentVo, BindingResult result,
             @AuthenticationPrincipal UserDetails user){
             log.debug("接收到表单信息:{}",commentVo);
             if(result.hasErrors()){
                 String msg=result.getFieldError().getDefaultMessage();
                 throw new ServiceException(msg);
            }
             return null;
    }
 }

1.3 编写页面的绑定

在detail_teacher.html页面修改代码:352行附近

 <p class="text-left text-dark">
   <a class="btn btn-primary mx-2"
      href="#">采纳答案</a>
   <a class="btn btn-outline-primary"
      data-toggle="collapse" href="#collapseExample1"
      role="button" aria-expanded="false"
      aria-controls="collapseExample"
      :href="'#addComment'+answer.id" ><!--修改href-->
     <i class="fa fa-edit"></i>添加评论
   </a>
 </p>
 <div class="collapse" id="collapseExample1"
     :id="'addComment'+answer.id"><!--修改id-->
   <div class="card card-body border-light">
     <form action="#" method="post" class="needs-validation"
           novalidate @submit.prevent="postComment(answer.id)"><!--阻止原有表单提交效果-->
       <div class="form-group">
         <textarea class="form-control" name="content" rows="3" required></textarea>
         <div class="invalid-feedback">
          评论内容不能为空!
         </div>
       </div>
       <button type="submit" class="btn btn-primary my-1 float-right">提交评论</button>
     </form>
   </div>
 </div>

1.4 编写提交评论的js代码

在question_detail.js文件中的answersApp对象中新增一个方法:

 postComment:function(answerId){
     if(!answerId){
         return
    }
     console.log(answerId);
     //获得输入框对象
     let textarea=$("#addComment"+answerId+" textarea");//textarea前面一定要有空格:子孙后代选择器
     let content=textarea.val();
     if(!content){
         alert("必须编写评论内容");
         return;
    }
     let form=new FormData();
     form.append("answerId",answerId);
     form.append("content",content);
     axios({
         url:"/v1/comments",
         method:"post",
         data:form
    }).then(function(response){
         console.log(response.data);
    })
 }

1.5 开发用户添加评论的业务逻辑层

  上次课完成了用户添加评论的控制层、页面和提交信息到java的Controller,现在开发业务逻辑层,以便控制层调用。

在接口ICommentService中添加方法:

 public interface ICommentService extends IService<Comment> {
     //用户新增评论的业务逻辑层方法:因为增加评论后要显示在评论列表,所以要返回Comment进行调用,避免再次查询数据库
     Comment saveComment(CommentVo commentVo,String username);
 }

在CommentServiceImpl类中实现业务逻辑层方法:

 @Service
 public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements ICommentService {
     //添加评论用
     @Autowired
     private CommentMapper commentMapper;
     //查询用户名用
     @Autowired
     private UserMapper userMapper;
     //用户新增评论的业务逻辑层方法实现
     @Override
     //传入表单提交对象和用户名(用来获得用户对象进而获得用户昵称和id)
     public Comment saveComment(CommentVo commentVo, String username) {
         //根据用户名查询用户对象
         User user = userMapper.findUserByUsername(username);
         //实例化评论对象并赋值,赋值内容对应Comment表字段名
         Comment comment = new Comment()
                .setContent(commentVo.getContent())
                .setAnswerId(commentVo.getAnswerId())
                .setUserNickName(user.getNickname())
                .setUserId(user.getId())
                .setCreatetime(LocalDateTime.now());
         //执行插入
         int num = commentMapper.insert(comment);
         //判断是否插入成功
         if(num!=1){
             throw new ServiceException("数据库异常");
        }
         //返回实例化的评论对象
         return comment;
    }
 }

1.6 完善控制器方法

 @RestController
 @RequestMapping("/v1/comments")
 @Slf4j
 public class CommentController {
     @Autowired
     ICommentService commentService;
     @PostMapping //("")可省略,这个方法映射/v1/comments的请求
     //新增评论 传入表单对象,验证表单数据格式,获取当前登录用户身份
     public Comment postComment(@Validated CommentVo commentVo, BindingResult result,
                                @AuthenticationPrincipal UserDetails user){
         log.debug("接收到表单信息:{}",commentVo);
         if(result.hasErrors()){
             String msg = result.getFieldError().getDefaultMessage();
             throw new ServiceException(msg);
        }
         //调用业务逻辑层方法新增评论,因为要显示,所以要有返回对象
         Comment comment = commentService.saveComment(commentVo,user.getUsername());
         return comment;
    }
 }

  重启服务,尝试添加评论,观察数据库是否添加成功!

(1)未修改前,添加评论存在粘连问题,点开一个评论时,所有评论窗口都打开,关闭时都关闭

 

(2)修改后解决了粘连问题,但是不能显示在评论列表上

(3)数据库中新增了评论数据

注意:此时数据只是添加到数据库中,并不能进行显示。

1.7 显示每个回答的评论列表

  现在我们已经成功新增了评论, 但是并不能把评论显示在页面上,每个评论应该显示在对应回答的评论列表下。

利用关联查询获得评论列表

  要想查询出每个回答的评论信息,我们需要从数据库中查询该回答相关的评论信息,那么就有多种思路实现,主流两种思路如下图:

  • 方案一(左侧)

  多次查询,每次查询一个回答对应的所有评论,这样做的好处是逻辑和业务简单,但是效率低下。

  • 方案二(右侧)

  一次查询,连同回答和回答对应的评论全部查询出来,这样做的好处是效率高,但是编写代码比较复杂。

  我们本次查询采用右侧的方式解决,首先明确一次查询要执行的sql语句如下:由于两个表进行关联查询时存在多个重名字段,所以使用别名加以区分(注意检查代码是否正确)。

 SELECT 
  a.id,
  a.content,
  a.like_count,
  a.user_id,
  a.user_nick_name,
  a.quest_id,
  a.createtime,
  a.accept_status,
  c.id comment_id,
  c.user_id comment_user_id,
  c.answer_id comment_answer_id,
  c.user_nick_name   comment_user_nick_name,
  c.content   comment_content,
  c.createtime comment_createtime
 FROM answer a LEFT JOIN comment c
 ON a.id=c.answer_id
 WHERE a.quest_id=157
 ORDER BY a.createtime

查询结果:

补充:数据库连接查询

数据库连接查询分内连接和外连接

  • 内连接关键字: [inner] join inner可以省略

    • 内连接特征:两张关联表必须有对应关系才能出现在查询结果中

  • 外连接又分左连接和右连接

    • 左连接 left [outer] join outer可以省略

    • 右连接 right [outer] join outer可以省略

    • 外连接特征:先确定一张主表(左连接左侧是主表,右连接右侧是主表),主表中的内容即使和连接表没有对应关系,也会至少和null行对应查出一次。

查询结果示意图:

  上面是查询结果的结构示意图,意思是我们查询出的回答会保存在一个List<Answer>中,而这个List中的每一个Answer对象中又可能包含多个Comment,所以我们现在必须在Answer实体类中添加一个List<Comment>类型的属性comments来保存评论,代码如下:

 /**
  * 问答包含的评论集合
  */
 @TableField(exist = false)
 //此处进行了赋初值,为的是防止空指针异常,没有评论时为[],代表长度为0的集合
 private List<Comment> comments=new ArrayList<>();

下面开始利用Mybatis框架提供的解决方案实现我们的关联查询:

  1. 创建resource/mapper文件夹

  1. 将java中mapper/xml中的AnswerMapper.xml复制到resource/mapper文件路径下

  2. Rebuild Module "knows-portal"

修改代码示意图如下:

最终修改后代码如下:注意复制后在Answer、AnswerMapper上Ctrl+鼠标左键点击能跳转到对应的文件才行

 <?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="cn.tedu.knows.portal.mapper.AnswerMapper">
 
     <!-- 通用查询映射结果 -->
     <resultMap id="answerCommentMap" type="cn.tedu.knows.portal.model.Answer">
         <!--
        column对应数据库查询出的列名
        property对应java实体类中的属性名
          -->
         <id column="id" property="id" />
         <result column="content" property="content" />
         <result column="like_count" property="likeCount" />
         <result column="user_id" property="userId" />
         <result column="user_nick_name" property="userNickName" />
         <result column="quest_id" property="questId" />
         <result column="createtime" property="createtime" />
         <result column="accept_status" property="acceptStatus" />
         <!--
        下面标签的含义是将当前Answer实体类中包含了一个集合(默认集合为list),
  集合名称comments,ofType规定泛型的类型:Comment
          -->
         <collection property="comments" ofType="cn.tedu.knows.portal.model.Comment">
         <!--由于已经指定了泛型类型,所以下面就可以编写Comment和查询结果的映射了-->
             <id column="comment_id" property="id" />
             <result column="comment_user_id" property="userId" />
             <result column="comment_answer_id" property="answerId"/>
             <result column="comment_user_nick_name" property="userNickName"/>
             <result column="comment_content" property="content"/>
             <result column="comment_createtime" property="createtime"/>
         </collection>
     </resultMap>
 
     <!--
         id属性和AnswerMapper接口中的方法对应,和方法名一致
         resultMap表示当前方法的返回值类型,和上面定义的<resultMap>的id一致
      -->
     <select id="findAnswersByQuestionId" resultMap="answerCommentMap">
        SELECT
            a.id,
            a.content,
            a.like_count,
            a.user_id,
            a.user_nick_name,
            a.quest_id,
            a.createtime,
            a.accept_status,
            c.id           comment_id,
            c.user_id       comment_user_id,
            c.answer_id         comment_answer_id,
            c.user_nick_name   comment_user_nick_name,
            c.content         comment_content,
            c.createtime     comment_createtime
        FROM answer a LEFT JOIN comment c
        ON a.id=c.answer_id
        WHERE a.quest_id=#{id}
        ORDER BY a.createtime
     </select>
 </mapper>

AnswerMapper中编写findAnswersByQuestionId方法:

 @Repository
 public interface AnswerMapper extends BaseMapper<Answer> {
     //配合AnswerMapper.xml文件进行查询的方法
     //这个方法的方法名必须和xml文件中select标签的id属性一致
     List<Answer> findAnswersByQuestionId(Integer questionId);
 }

由于涉及sql代码及多处修改,一定要测试一下,测试代码如下:

 package cn.tedu.knows.portal;
 
 import cn.tedu.knows.portal.mapper.AnswerMapper;
 import cn.tedu.knows.portal.model.Answer;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 
 import java.util.List;
 
 @SpringBootTest
 public class TestAnswer {
     @Autowired
     AnswerMapper answerMapper;
     @Test
     public void answersTest(){
         List<Answer> answers = answerMapper.findAnswersByQuestionId(157);
         for(Answer answer:answers){
             System.out.println(answer);
        }
    }
 }
 

测试结果如下:

  注意检查输出结果内容是否为空,是否都完整,如果有null时,检查代码是否编写正确。注意问答没有讨论时commets=[ ],有讨论时,后面对应每个评论。如果测试正常,则继续编写下面的代码。

1.8 完成查询评论列表的业务逻辑层

  上面的章节完成了查询评论列表的数据访问层,下面我们完善一下已经编写好的业务逻辑层代码。

将AnswerServiceImpl类中的getAnswersByQuestionId修改如下:

 @Override
 public List<Answer> getAnswersByQuestionId(Integer questionId) {
     // ↓↓↓↓↓↓↓↓↓↓↓↓↓ 修改后的调用:现在得到的回答中包含了对该问答的评论
     List<Answer> answers=answerMapper.findAnswersByQuestionId(questionId);
     //别忘了返回
     return answers;
 }

控制层不用修改,甚至js文件都不修改,只需要修改vue绑定即可。

1.9 Vue绑定显示所有评论

在detail_teacher.html页面的309行附近:

 <div class="card-footer">
   <p class="text-success fa fa-comment"><!--修改评论条数-->
     <span v-text="answer.comments.length">1</span>条评论
   </p>
   <ul class="list-unstyled mt-3">
     <li class="media my-2"
       v-for="comment in answer.comments"><!--遍历评论-->
       <img style="width: 50px;height: 50px;border-radius: 50%;"
            src="../img/user.jpg" class="mr-3"
            alt="...">
       <div class="media-body">
         <h6 class="mt-0 mb-1"><!--修改评论者昵称-->
           <span v-text="comment.userNickName">李四</span>:
         </h6>
         <p class="text-dark">
           <span class="text-monospace"
                 v-text="comment.content"><!--修改评论内容-->
            明白了,谢谢老师!
           </span>
      <!-- 其它代码略 -->
 </div>            

  启动服务进行测试,添加评论,发现添加评论后评论不能立即显示到问答下,需要刷新才能加载出来。

1.10 立即显示评论内容

  上面虽然可以显示回答以及其评论内容了,但是并不能在添加评论时立即将评论内容显示在页面上,在question_detail.js的answersApp定义的Vue中的postComment方法中.then的内容修改如下:

 .then(function(response){
     console.log(response.data);
     //立即将新增的评论显示在页面上
     let comment=response.data;
     let answers=answersApp.answers;
     //遍历当前所有的回答对象
     for(let i=0;i<answers.length;i++){
         //遍历问题id=当前问题id
         if(answers[i].id==answerId){
             //给问题添加评论
             answers[i].comments.push(comment);
             break;
        }
    }
     //清空文本域中的内容
     textarea.val("");
 })

补充:常用js操作数组的API

  push() 向数组的末尾添加一个或更多元素,并返回新的长度
  pop() 删除并返回数组的最后一个元素
  unshift() 向数组的开头添加一个或更多元素,并返回新的长度
  shift() 删除并返回数组的第一个元素
  splice(index, howmany) 从index位置删除howmany个数组元素
  splice(index, howmany, item) 从index位置删除howmany个数据元素并添加item元素
  sort() 对数组的元素进行排序
  reverse() 颠倒数组中元素的顺序
  concat() 连接两个或更多的数组,并返回结果
  join() 把数组的所有元素放入一个字符串,元素通过指定的分隔符进行分隔

  重新启动服务进行测试,发现新增评论后,立即显示在该问答所对应的评论列表中。

 

 

posted @ 2021-09-02 21:18  Coder_Cui  阅读(446)  评论(0编辑  收藏  举报