SpringBoot聚合项目:达内知道(十一)-删除用户评论、修改用户评论、问题采纳

1 删除评论功能

  下面我们来实现删除评论的功能,注意:删除用户的评论并不是随意删除的:

  • 讲师可以删除任何人的评论(包括自己的、其他讲师的、学生的)

  • 学生只能删除自己发布的评论,不能删除讲师或者其他同学的评论

删除思路:按id删除评论是Mybatis Plus提供的功能,直接编写业务逻辑层即可。

业务逻辑层中:

  • 先判断是不是讲师,如果是讲师,则直接删除;

  • 如果不是讲师,即学生,判断评论的发布者id和当前登录用户id是否一致,如果一致可以删除,否则不能删除(即保证不同同学间评论不能彼此删除)

1.1 编写业务逻辑层

在接口ICommentService添加方法:

 //用户删除评论的方法,使用boolean类型的返回值,判断是否删除成功
 boolean removeComment(Integer commentId,String username);

在CommentServiceImpl类中实现该方法:

     @Override
     //参数为要删除评论的id,username用来获取user对象,从而获得当前登录用户id
     public boolean removeComment(Integer commentId, String username) {
         //获得当前登录用户信息
         User user = userMapper.findUserByUsername(username);
         //判断是讲师可以直接删除
         if(user.getType().equals(1)){//包装类要用equals比较判断,讲师type=1,学生type=0
             //返回受影响的行数
             int num = commentMapper.deleteById(commentId);
             return num==1;//删除成功时受影响的行数为1
        }
         //如果不是讲师删除,那么就要查询当前要删除的Comment对象,以便判断发布者id
         Comment comment = commentMapper.selectById(commentId);
         if(comment.getUserId().equals(user.getId())){
             int num = commentMapper.deleteById(commentId);
             return num==1;
        }
         throw new ServiceException("没有权限删除该评论");
    }

1.2 编写控制层代码

在CommentController中添加方法:

 //按id删除评论 v1/comments/85/delete
 @GetMapping("/{id}/delete") //注意:{id}写在哪里都可以,也可以写成/delete/{id}
 //@PathVariable:获取id、@AuthenticationPrincipal UserDetails:获取当前登录用户身份
 public String removeComment(@PathVariable Integer id, @AuthenticationPrincipal UserDetails user){
     //调用业务逻辑层方法删除评论
     boolean isDelete=commentService.removeComment(id,user.getUsername());
     if(isDelete){
         return "删除成功";
    }else{
         return "删除失败";
    }
 }

  启动服务,进行同步测试,测试路径为:http://localhost:8888/v1/comments/27/delete,进行删除评论,删除效果如下所示:

(1)删除成功

(2)由于该评论已不存在,删除失败

1.3 编写html绑定

  任何用户方面不可逆操作都要有二次确认,尽量选择相对友好的二次确认,避免使用系统弹窗。下面这个效果是点击删除时,出现右侧的删除红色图章,点击该图章时进行删除操作,不点击删除时,不出现该删除图章。

下面就来编写这个弹出效果,在detail_teacher.html 336行附近修改代码:

 <!--老师角色或者属于本用户的评论可以删除该评论-->
 <a class="ml-2 fa fa-close " style="font-size: small"
    data-toggle="collapse"  role="button"
    aria-expanded="false" aria-controls="collapseExample"
    onclick="$(this).next().toggle(300)"><!--修改:toggle添加动画效果-->
  删除
 </a>
 <a class="badge badge-pill badge-danger text-white"
     style="display: none;cursor: pointer"
     @click="removeComment(comment.id)"><!--悬浮变小手,指定删除id-->
     <i class="fa fa-close"></i>
 </a>

1.4 编写js代码

删除评论的js代码也编写在question_detail.js的answersApp对象中,代码如下:

 //删除评论
 removeComment:function(commentId){
     axios({
         url:"/v1/comments/"+commentId+"/delete",
         method:"get"
    }).then(function(response){
         console.log(response.data);
    })
 }

  启动服务,进行删除评论操作,点击删除后,评论会在数据库中进行删除,但是评论列表显示还在。

1.5 同步删除结果

  我们希望在删除评论时,将评论显示的内容也同时删除,这样就要去删除Vue中answers对象中指定answer对象中comments数组中的某一个对象,我们可以通过直接传入必要参数到删除方法中来简化删除过程,在detail_teacher.html的314行附近修改代码:

 <li class="media my-2"
   v-for="(comment,index) in answer.comments"><!--遍历评论,传入当前评论和对应当前索引下标-->

其中,index能够代表当前循环的索引值(从0开始,恰巧匹配数组下标)。

在342行附近进行修改:

 <a class="badge badge-pill badge-danger text-white"
     style="display: none;cursor: pointer"
     @click="removeComment(comment.id,index,answer)"><!--指定删除评论id、索引下标、当前回答对象-->
     <!--                             ↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
     <i class="fa fa-close"></i>
 </a>

  上面代码在执行删除时,传入了index和answer对象,我们可以在删除的js代码中,直接删除answer对象中comments数组的第index位置的元素,从而定位到要删除的评论。

question_detail.js中删除评论的js代码:

 // 174行附近                     ↓↓↓↓↓↓↓↓↓↓↓↓↓
 removeComment:function(commentId,index,answer){//此处要传入删除需要的参数
     axios({
         url:"/v1/comments/"+commentId+"/delete",
         method:"get"
    }).then(function(response){
         console.log(response.data);
         // js代码删除数组中元素的方法splice([删除的索引值],[删除几个])
         //↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
         answer.comments.splice(index,1);//必须要写1,否则全部删除    
    })
 }

  重新服务,进行测试,查看删除评论时,对应的评论是不是立即删除!

  注意:同时也可以测试学生删除评论功能逻辑:先用学生身份进行登录,点击问题详情页后响应404页面,修改浏览器地址栏为http://localhost:8888/question/detail_teacher.html?157(注意一定要指定删除id),先添加评论,再进行删除,删除后进行刷新(shift+F5),发现:学生身份只能删除自己增加的评论,其他人的评论都不能删除,与设计逻辑一致。

 

2 修改用户评论

  因为评论是需要表单提交的,提交方式是post,所以我们先做控制层和页面,方便测试。

  页面方面和添加评论有一样的问题,即当我们点击编辑时,所有编辑全部展开,我们先处理这个问题。

2.1 修改html页面代码

在detail_teacher.html的330行附近:

 <a class="text-primary ml-2"
    style="font-size: small" data-toggle="collapse" href="#editCommemt1"
    role="button" aria-expanded="false"
    aria-controls="collapseExample"
     :href="'#editComment'+comment.id"><!--修改编辑评论-->
     <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
   <i class="fa fa-edit"></i>编辑
 </a>

351行附近:

 <div class="collapse" id="editCommemt1"
   :id="'editComment'+comment.id"><!--修改id-->
   <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
   <div class="card card-body border-light">
     <form action="" method="post" class="needs-validation" novalidate
       @submit.prevent="updateComment(comment.id,answer.id,index,answer)"><!--组织原有表单提交效果-->
       <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
       <div class="form-group">
         <textarea class="form-control"
                   id="textareaComment1" name="content" rows="4"
                   required></textarea>
         <div class="invalid-feedback">
          内容不能为空!
         </div>
       </div>
       <button type="submit" class="btn btn-primary my-1 float-right">提交修改</button>
     </form>
   </div>
 </div>

2.2 编写js代码

在question_detail.js文件中answersApp对象中添加方法:

 //修改评论
 updateComment:function(commentId,answerId,index,answer){//传入评论id、回答id、索引下标、回答
     //获得修改对象
     let textarea=$("#editComment"+commentId+" textarea");
     //获得修改内容
     let content=textarea.val();
     //新建表单,赋值
     let form=new FormData();
     form.append("answerId",answerId);
     form.append("content",content);
     //发请求
     axios({
         url:"/v1/comments/"+commentId+"/update",
         method:"post",
         data:form
    }).then(function(response){
         //将要修改的评论重新赋值对象
         //answer.comments[index]=response.data;
         //上面的修改并不会引发vue对页面的刷新
         //也就是说,数组中的数据已经发生了变化,但是页面中数据不变
         // 是因为Vue不会对当前绑定数组中元素的子元素进行修改时自动刷新
         // 所以,想修改数据还想让页面刷新,需要使用下面的命令:
         Vue.set(answer.comments,index,response.data);
         //Vue.set([修改哪个数组],[修改哪个索引],[修改成什么])
         
         //清空文本域中的内容(修改评论提交后,文本框内内容清空)
         textarea.val("");
 
  //修改后收缩文本框(修改评论提交后,文本框收缩)
  $("#editComment"+commentId).collapse("hide");//前端BootStrap内容  
    })
     // ["red","black","blue","white"]
 }

2.3 编写控制层代码接收信息

先编写一个能够接收js代码请求表单的控制代码:

 // 修改评论的功能
 @PostMapping("/{id}/update")
 //传入修改评论的id、修改评论表单验证、获得当前用户身份
 public Comment update(
         @PathVariable Integer id,
         @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;
 }

  启动服务后,登录讲师首页,进入问题详情页,进行修改评论,提交效果如下:

2.4 编写业务逻辑层

  修改方法MybatisPlus也有提供,我们可以选择直接使用这个方法,也可以自己编写一个针对当前修改业务的方法。在当前业务中,我们选择使用MybatisPlus给我们提供的方法,所以不需要编写数据访问层。

在ICommentService接口添加方法:

 //修改用户评论的方法
 Comment updateComment(Integer commentId,CommentVo commentVo,String username);//需要评论id、表单对象、用户名

在CommentServiceImpl类中实现该方法:

 //用户修改评论的业务逻辑层方法实现
 @Override
 @Transactional
 public Comment updateComment(Integer commentId, CommentVo commentVo, String username{
     // 判断是讲师,或是评论的发布者
     // 查询当前评论对象,修改评论内容后提交到数据库修改---先查看
     Comment comment=commentMapper.selectById(commentId);
     //根据用户名查找用户
     User user=userMapper.findUserByUsername(username);
     //用户身份是讲师 或者 当前用户是评论的发布者
     if(user.getType().equals(1) || user.getId().equals(comment.getUserId())){
         //执行修改操作!!
         comment.setContent(commentVo.getContent());
         //提交到数据库---再修改
         int num=commentMapper.updateById(comment);
         if(num!=1){
             throw new ServiceException("数据库异常");
        }
         //别忘了返回!!!!
         return comment;
    }
     //不满足上述条件
     throw new ServiceException("没有权限修改该评论!");
 }

2.5 控制层调用

在CommentController修改评论的方法添加业务逻辑层调用:

 // 修改评论的功能
 @PostMapping("/{id}/update")
 //传入修改评论的id、修改评论表单验证、获得当前用户身份
 public Comment update(
         @PathVariable Integer id,
         @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.updateComment(id,commentVo,user.getUsername());
     //别忘了返回!!
     return comment;
 }

  重启服务,修改评论进行测试,如果修改成功,并且页面立即变化,表示一切正常!

(1)浏览器

(2)IDEA

(3)数据库

(4)添加下面代码后:

 //清空文本域中的内容(修改评论提交后,文本框内内容清空)
 textarea.val("");
 
 //修改后收缩文本框(修改评论提交后,文本框收缩)
 $("#editComment"+commentId).collapse("hide");//前端BootStrap内容  

  提交评论后,文本域内容清空,同时收起文本域。

2.6 实现学生评论

  现在我们的项目学生登录跳转到学生首页,学生首页点击问题标签还不能跳转到详情页,我们需要给学生也开发一个问题详情页。学生和讲师问题详情页的区别在于:

  • 学生没有删除和回答问题的权限

  • 学生没有富文本编辑器

  先删除原有的detail_student.html,直接复制detail_teacher.html并重命名为detail_student.html,然后将上图需要删除的位置从detail_student.html页面中删除即可。

删除内容如下:

找到学生首页index_student.html,在学生首页的标题连接处修改为:index_student.html的203行附近

 <a class="text-dark" href="question/detail.html"
   v-text="question.title"
   :href="'/question/detail_student.html?'+question.id"><!--页面绑定、修改href-->
  eclipse 如何导入项目?
 </a>

  重启服务,登录学生首页,通过学生首页就可以访问问题详情页了。按F12会出现右侧警告,可以忽略。

 

3 问题的采纳

  最后我们要实现问题的采纳效果,我们项目的设计采纳形式有3种:

  1. 学生采纳 : 学生登录后,将自己提问的问题中的某个回答标记为采纳,问题状态变为已解决(项目中采用这种方式进行讲解);

  1. 讲师采纳 : 学生提问后,长时间不采纳,也不讨论,视为该学员不活跃,在讲师已经回答问题的前提下,可以将回答标记为采纳,问题状态变为已解决;

  1. 超时自动采纳 : 学生提问,讲师回复后,没有任何用户再有新的操作,到一定时间后,我们可以设置系统自动采纳。

  我们的项目允许一个提问多个回答被采纳,已经解决的问题仍然可以继续讨论!

3.1 采纳按钮二次确认

  因为采纳也是用户不可逆的操作,需要和删除一样的二次确认,所以我们设计在点击采纳答案后,在右侧弹出一个绿色的√表示实际的采纳操作。

在detail_student.html的373行附近修改代码:

 <!--白色字体、指针变小手、添加toggle方法进行显示状态切换-->
 <a class="btn btn-primary mx-2 text-white"
    style="cursor: pointer;"
    onclick="$(this).next().toggle(300)"
    >采纳答案</a>
 <!--弹出√:绿色图章背景、白色字体、中间带√、设置样式:鼠标变小手、添加点击事件-->
 <a class="badge badge-pill badge-success text-white"
   style="display: none;cursor: pointer"
    @click="answerSolved(answer.id)">
     <i class="fa fa-check"></i><!--check表示图章√,close表示图章×-->
 </a>

3.2 编写数据访问层

编写采纳回答的数据访问层,需要编写两个修改方法:采纳回答涉及回答和问题表中的状态修改

  1. 修改answer的accept_status的方法

  1. 修改question的status的方法

在answerMapper编写方法修改accept_status:

 //采纳回答:修改指定id的回答的accept_status,注意后面涉及到两个参数的特殊情况,返回的是受影响的行数
 @Update("update answer set accept_status=#{acceptStatus} where id=#{answerId}")
 int updateAcceptStatus(@Param("acceptStatus") Integer acceptStatus,
                        @Param("answerId") Integer answerId);
 /**
  * 默认情况下编译之后局部变量会丢失名称,JVM无法获得方法参数的名称
  * 所以必须使用@Param("")注解来标记对应的名称
  * 但是SpringBoot官方脚手架对编译器和jvm进行了修改,
  * 使得程序编译之后运行时能够获得局部变量的名称,就不必要写@Param注解了,为了安全起见,可以加上
  * 需要注意阿里脚手架并没有这个设置,所以必须写
  */

question问题对象有3个状态:

  • 0:未回复

  • 1:已回复

  • 2:已解决

当涉及3个及以上状态时,需要定义声明常量,我们应该在Question类中定义这3个数组对应的常量:

 public class Question implements Serializable {
     private static final long serialVersionUID = 1L;
 
     // 声明问题状态的3个常量
     public static final Integer POSTED=0;  // 已提交\未回复
     public static final Integer SOLVING=1; // 正在采纳\已回复
     public static final Integer SOLVED=2;  // 已经采纳\已解决
  // 其它代码略
 }

在QuestionMapper添加修改status状态的方法:

 //采纳回答:将当前问题的状态修改为已解决
 @Update("update question set status=#{status} where id=#{questionId}")
 int updateStatus(@Param("status")Integer status,
                  @Param("questionId") Integer questionId);

推荐对上面mapper中编写的两个方法测试一下,测试代码如下:

 @Resource
 QuestionMapper questionMapper;
 @Resource
 AnswerMapper answerMapper;
 
 @Test
 public void update(){
     // 修改回答状态
     int num=answerMapper.updateAcceptStatus(1,47);//
     num=questionMapper.updateStatus(2,124);//
     System.out.println("ok");
 }

输出结果:

(1)IDEA

(2)accept_status由0变为1

(3)status由1变为2

3.3 编写业务逻辑层

  我们要编写的是采纳回答的业务,先在IAnswerService接口中添加方法:

 //采纳回答的方法
 boolean accept(Integer answerId,String username);//目前只考虑了学生自己提出的问题

在AnswerServiceImpl实现代码如下:

     @Resource
     private QuestionMapper questionMapper;
     //采纳回答的业务逻辑层方法实现
     @Override
     @Transactional //开启事务
     public boolean accept(Integer answerId, String username) {
         //根据回答id查找回答
         Answer answer = answerMapper.selectById(answerId);
         //判断当前回答是不是被采纳过了
         if(answer.getAcceptStatus().equals(1)){
             return false;
        }
         //没有被采纳时,进行获取当前登录对象
         User user = userMapper.findUserByUsername(username);
         //查询该回答所对应的问题
         Question question = questionMapper.selectById(answer.getQuestId());
         //这里是学生采纳,那么当前登录用户的id必须要和问题的发布者id一致
         if(user.getId().equals(question.getUserId())){
             //修改accept_status
             int num = answerMapper.updateAcceptStatus(1,answerId);
             if(num!=1){
                 throw new ServiceException("数据库异常");
            }
             //修改status
             num = questionMapper.updateStatus(Question.SOLVED,question.getId());
             if(num!=1){
                 throw new ServiceException("数据库异常");
            }
             //修改成功
             return true;
        }
         //两次获取的id不一致
         throw new ServiceException("权限不足");
    }

3.4 编写控制层方法

在AnswerController中编写采纳回答的方法:

 // 采纳回答的业务逻辑层
 @GetMapping("/{answerId}/solved")
 public String solved(
         @PathVariable Integer answerId,
         @AuthenticationPrincipal UserDetails user){
     //调用业务逻辑层方法进行回答采纳
     boolean isAccept=answerService.accept(answerId,user.getUsername());
     if(isAccept){
         return "采纳完成";
    }else{
         return "该回答已经被采纳";
    }
 }

  重启服务,进行同步测试:

(1)先登小明同学,访问路径:http://localhost:8888/v1/answers/42/solved,提示“权限不足”

(2)再登李四同学,访问路径:http://localhost:8888/v1/answers/42/solved,提示“采纳完成”

(3)再次刷新或者访问该路径,提示“该问题已经被采纳”

3.5 编写js代码

  我们在本章节开始时已经编写了html的绑定,点击采纳回答出现二次确认,再点击√进行回答的采纳。

我们继续在question_detail.js文件中编写,answersApp下添加answerSolved方法,代码如下:

 //回答修改为已解决状态
 answerSolved:function(answerId){
     axios({
         url:"/v1/answers/"+answerId+"/solved",
         method:"get"
    }).then(function(response){
         alert(response.data);
    })
 }

重启服务,进行测试:

(1)先登录小明同学,访问路径:http://localhost:8888/question/detail_student.html?164,进行采纳答案,提示“权限不足”

(2)再登录李四同学,访问路径:http://localhost:8888/question/detail_student.html?164,进行采纳答案,提示“采纳成功”

(3)再次刷新或者访问该路径,提示“该问题已经被采纳”

(4)回到问题首页,问题状态变为已解决

 

posted @ 2021-09-03 22:16  Coder_Cui  阅读(613)  评论(0编辑  收藏  举报