SpringBoot聚合项目:达内知道(九)-讲师登录、讲师任务列表显示、显示问题详情页、讲师回复功能

1 开发讲师登录功能

1.1 设计思路

  由于讲师和学生共用同一个登录页,因此需要实现的功能是:

  • 学生登录成功跳转学生的首页:index_student.html

  • 讲师登录成功跳转讲师的首页:index_teacher.html

  按照上面思路我们必须解决一个问题:登录时必须将当前用户的身份(角色)保存到Spring-Security中,然后编写一个专门的控制器方法去判断当前登录用户的身份,再决定跳转哪个页面。

1.2 登录时保存用户身份

  要想在登录时查询并保存用户的角色(身份),那么就需要在数据库中根据用户id查询角色,注意一个用户拥有多个角色(admin),一个角色对应多个用户,为多对多的关系,需要使用中间表进行连接查询,sql语句如下:

 SELECT r.id,r.name 
 FROM user u
 LEFT JOIN user_role ur ON u.id=ur.user_id
 LEFT JOIN role r ON r.id=ur.role_id
 WHERE u.id=11

数据访问层:UserMapper接口中添加方法如下:

 //根据用户id查询用户的所有角色
 @Select("SELECT r.id,r.name" +
         " FROM user u" +
         " LEFT JOIN user_role ur ON u.id=ur.user_id" +
         " LEFT JOIN role r ON r.id=ur.role_id" +
         " WHERE u.id=#{id}")
 List<Role> findUserRolesById(Integer id);//注意:虽然学生和讲师都只有一个身份,但是admin有多个角色,因此要使用List来接收。

业务逻辑层:UserDetailsServiceImpl中方法修改为:

 //4.转换权限List为String[]
 String[] auth=new String[ps.size()];
 int i=0;//数组起始下标
 //遍历权限,存到字符串数组中
 for(Permission p:ps){
     auth[i++]=p.getName();
 }
 //下面是新增的代码....
 // 根据用户id查询用户所有角色
 List<Role> rs=userMapper.findUserRolesById(user.getId());
 // 数组扩容
 auth= Arrays.copyOf(auth,auth.length+rs.size());
 //遍历角色,将角色也存到字符串数组中,即:数组中存了用户权限和角色
 for(Role r:rs){
     auth[i++]=r.getName();//此处的i与上面的i是同一个,因为i是全局变量,可以继续使用
 }
 //以下代码略....

1.3 登录后跳转不同首页

  上面代码中我们已经在登录时将用户的角色保存到Spring-Security中,下面我们编写一个专用的控制器来判断用户的角色,最终决定跳转的页面路径。

控制层:创建HomeController类,代码如下:

 package cn.tedu.knows.portal.controller;
 
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 
 //@RestController注解标记的控制器中的方法返回的字符串会认为是返回的内容
 //@Controller注解标记的控制器中的方法返回的字符串,会认为是要跳转的页面
 @Controller
 public class HomeController {
     //Spring-Security框架判断资格时,需要使用下面的类型常量:
     public static final GrantedAuthority STUDENT = new SimpleGrantedAuthority("ROLE_STUDENT");//学生身份
     public static final GrantedAuthority TEACHER = new SimpleGrantedAuthority("ROLE_TEACHER");//讲师身份
 
     //当路径url是http://localhost:8888或http://localhost:8888/index.html时都运行这个控制器方法
     //这个控制器方法根据当前登录用户角色跳转不同页面
     //@GetMapping("/index.html") 可以通过一个路径访问
     @GetMapping(value = {"/","/index.html"})  //可以通过两个路径访问,在SecurityConfig类中进行默认登录成功页面配置
     public String index(@AuthenticationPrincipal UserDetails user){//获得用户身份
         //判断当前登录用户是否包含讲师身份
         if(user.getAuthorities().contains(TEACHER)){//getAuthorities()所有资格:权限和角色
             //跳转到讲师首页,重定向到讲师首页
             return "redirect:/index_teacher.html";//必须是@Controller才生效
        } else if(user.getAuthorities().contains(STUDENT)){
             //如果是学生,重定向到学生首页
             return "redirect:/index_student.html";
        }
         //既不是学生,也不是讲师,去登录
         return "redirect:/login.html";
    }
 }
 

注意:在设置Spring-Security配置文件时,登录成功默认访问的是学生首页,要把这个配置修改为"index.html"。

在SecurityConfig类中,将登录成功默认路径进行修改:

 .defaultSuccessUrl("/index.html") //设置登录成功的页面,此处设置为为“/”或“/index.html”均可,与HomeController跳转路径对应

  启动服务,访问登录页:http://localhost:8888/login.html,按照学生(用户名:st2、密码:888888)和讲师(用户名:tc2、密码:888888)分别登录,查看是否能成功响应对应首页。

 

2 开发讲师任务列表

  讲师首页显示之后,我们现在需要查询讲师的任务列表,在学生首页位置对应于学生的问答列表。

2.1 讲师任务列表的数据访问层

  我们要查询哪些问题选择了当前登录讲师作答,还有讲师自己提问的问题,都显示在任务列表中,涉及到的表有:user_question、question。

可以通过如下sql语句查询问题(学生提问的、讲师提问的):

 SELECT q.* FROM question q
 LEFT JOIN user_question uq ON q.id=uq.question_id
 WHERE uq.user_id=3 OR q.user_id=3
 ORDER BY q.createtime desc

其中,uq.user_id=3代表学生向3号讲师提问的问题,q.user_id=3代表3号讲师自己提出的问题

数据库查询结果如下:一共25个问题

推荐测试:在test中新建测试类TestTeacher,测试代码如下:

在QuestionMapper添加查询方法(多表查询只能使用sql语句,QueryWrapper只能进行简单查询):

 // 根据讲师id查询讲师任务列表
 @Select("SELECT q.* FROM" +
         " question q" +
         " LEFT JOIN user_question uq" +
         " ON q.id=uq.question_id" +
         " WHERE uq.user_id=#{id} OR q.user_id=#{id}" +
         " ORDER BY q.createtime desc")
 List<Question> findTeacherQuestions(Integer userId);//需要返回多个问题,使用List接收

其中,q.*代表代表查询q(question)表的所有信息

推荐测试:在test中新建测试类TestTeacher,测试代码如下:

 package cn.tedu.knows.portal;
 
 import cn.tedu.knows.portal.mapper.QuestionMapper;
 import cn.tedu.knows.portal.model.Question;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.context.SpringBootTest;
 
 import javax.annotation.Resource;
 import java.util.List;
 
 @SpringBootTest
 public class TestTeacher {
     @Resource //注意:@Resource与@Autowired功能相似
     QuestionMapper questionMapper;
 
     @Test
     public void testList(){
         List<Question> questions = questionMapper.findTeacherQuestions(3);
         for(Question q:questions){
             System.out.println(q);
        }
    }
 
 }
 

  运行测试类,输出结果如下:

2.2 讲师任务列表的业务逻辑层

  讲师任务列表和学生问题列表的功能基本一致,都需要进行分页查询,业务逻辑层接口的声明需要支持分页。

在IQuestionService接口中添加方法:

 //根据用户名(讲师)查询讲师任务列表
 PageInfo<Question> getTeacherQuestions(String username, Integer pageNum,Integer pageSize);//需要利用用户名获得用户对象,pageNum和pageSize提供分页参数

在QuestionServiceImpl类中实现该方法:

 //根据用户名(讲师)查询讲师问题列表的逻辑层实现
 @Override
 public PageInfo<Question> getTeacherQuestions(String username, Integer pageNum, Integer pageSize) {
     //1.查询当前登录用户信息
     User user=userMapper.findUserByUsername(username);
     
     //2.分页查询讲师列表
     PageHelper.startPage(pageNum,pageSize);
     List<Question> questions=questionMapper.findTeacherQuestions(user.getId());
     
     //3.为Question对象的tags属性赋值
     for(Question q : questions){
         List<Tag> tags=tagNames2Tags(q.getTagNames());//将tags字符串转化为tags对象
         q.setTags(tags);
    }
     
     //4.返回PageInfo类型结果,需要指明待分页的数据类型
     return new PageInfo<>(questions);
 }

2.3 开发查询讲师任务列表的控制层

在QuestionController类中添加方法:

 //根据登录用户分页查询讲师任务列表
 @GetMapping("/teacher")
 //学生身份的用户不能按照讲师任务列表的逻辑进行查询
 //下面的注解可以限制只有讲师身份才能访问该控制器方法
 @PreAuthorize("hasRole('ROLE_TEACHER')")
 public PageInfo<Question> teacher(@AuthenticationPrincipal UserDetails user, Integer pageNum){//获得当前登录用户身份,传入分页参数
     if(pageNum==null)//if后面如果只有一条语句要执行,可以不写{},注意代码格式降级
         pageNum = 1;
     Integer pageSize=8;
     PageInfo<Question> pageInfo=questionService
            .getTeacherQuestions(user.getUsername(),pageNum,pageSize);
     return pageInfo;
 }

  启动服务,访问路径:http://localhost:8888/v1/questions/teacher进行同步测试,查看从数据库查询到的json格式的问题列表内容:

  从上面查询结果可以看出,一共查询到了25个问题,与数据库查询到的结果一致,说明查询正确。

2.4 html绑定vue代码

html绑定和学生页面一致,直接将学生页面的绑定复制过来:

  • 在学生首页搜索:"我的问答",搜索结果下面一行复制id为questionsApp的div及其所有内容(先点击-收缩,再复制该条内容)

  • 在讲师首页搜索:"我的任务",搜索结果下面一行删除原有id为questionsApp的div及其所有内容,将从学生页面复制的粘贴到这个位置即可(先点击-收缩,再用复制的内容替换该行内容)。

js代码方面:

  我们复制学生首页的index.js文件重命名为index_teacher.js,将index_teacher.js文件中访问控制器的axios请求路径修改为:/v1/questions/teacher即可,最后别忘了在index_teacher.html末尾添加引用:

 <script src="js/index_teacher.js"></script>

  重启服务,访问讲师首页,查看是否可以成功显示所有任务列表!

 

3 显示问题详情页

3.1 通过将问题id传递到问题详情页进行问题详情显示

  讲师 / 学生首页可以通过点击问题列表中的标题跳转到问题详情页,问题详情页中可以对该问题进行回复或讨论。

在讲师首页index_teacher.html的问题标题html代码部分修改代码:index_teacher.html的204行附近

 <h5 class="mt-0 mb-1 text-truncate">
   <a class="text-dark" href="question/detail.html"
      :href="'/question/detail_teacher.html?'+question.id"<!-- 重新指定跳转路径,后面附带问题id -->
      v-text="question.title">
    eclipse 如何导入项目?
   </a>
 </h5>

  上面代码使用了Vue重新绑定了链接的路径,注意这个链接在?之后,追加了当前问题的id,因为之后将这个问题id发送到问题详情页,才能按问题id查询这个问题的详情并显示在页面上。

3.2 查询问题详情的业务逻辑层

  问题详情页上已经有了问题id,我们要从业务逻辑层开始编写根据问题id查询问题详情的代码。因为根据问题id查询问题对象的数据访问层方法已经由MybatisPlus提供,不需要额外在数据访问层编写代码,所以直接从业务逻辑层开始写代码。

在IQuestionService接口中添加方法:

 // 根据问题id查询问题详情
 Question getQuestionById(Integer id);

在QuestionServiceImpl类中实现该方法:

 @Override
 public Question getQuestionById(Integer id) {
     // 按id查询Question
     Question question=questionMapper.selectById(id);
     // 获得当前问题对应的所有标签对象的集合
     List<Tag> tags=tagNames2Tags(question.getTagNames());
     // 将标签集合赋值到当前question对象的tags属性
     question.setTags(tags);
     //千万别忘了返回!!!!!!question
     return question;
 }

3.3 编写显示问题详情的控制层代码

 //根据问题id查看问题详情
 /**
  * SpringMvc支持路径占位符
  * 如果请求为/v1/questions/150,会先寻找完全匹配的路径
  * 如果没有,会将150这个位置赋值给下面控制器的{id}占位符
  * 150这个值可以在控制器方法中获取并使用
  * 实际上是一种参数传值的方式
  */
 //要想获得占位符的值必须满足以下条件:
 // 1.声明的参数前加@PathVariable注解
 // 2.参数名称必须和{}中占位符一致
 @GetMapping("/{id}")
 public Question question(@PathVariable Integer id){
     //调用业务逻辑层查询问题
     Question question=questionService.getQuestionById(id);
     return question;
 }

  重启服务可以发送同步请求测试,测试路径为:http://localhost:8888/v1/questions/150,如果能够正常显示数据,则表示一切正常。

3.4 Vue绑定和html代码修改

在detail_teacher.html 187行附近添加id:

 <div class="container-fluid bg-light" id="questionApp"><!--添加id-->

在214行附近开始修改html代码:

 <div class="container-fluid ">
   <div class="row px-0 mb-3">
     <div class="col-9 px-0">
       <a class="badge badge-pill badge-info mx-1"
          href="../tag/tag_question.html"
          v-for="tag in question.tags"
          v-text="tag.name">
         <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑   -->
          Java基础</a>
     </div>
     <div class="col-3 px-0">
       <div class="row px-0">
 
         <div class="col border-right text-right">
           <p class="font-weight-light mb-0">收藏</p>
           <p class="font-weight-bold mt-1">1</p>
         </div>
         <div class="col">
           <p class="font-weight-light mb-0">浏览</p>
           <p class="font-weight-bold mt-1"
             v-text="question.pageViews">100</p>
                <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑   -->
         </div>
       </div>
     </div>
   </div>
   <p class=" px-0 text-center font-weight-bold" style="font-size: x-large"
     v-text="question.title">
       <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑   -->
    Java中方法重载和重写的区别
   </p>
   <div class="px-0 container-fluid question-content"
     v-html="question.content">
       <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑   -->
    请问的方法中重写和重载的区别都是什么,如何使用
   </div>
   <p class="text-right px-0 mt-5">
     <span class="font-weight-light badge badge-primary"
       v-text="question.userNickName">张三</span>
       <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑   -->
     <span class="font-weight-light badge badge-info"
       v-text="question.duration">
         <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑   -->3天前</span>
   </p>
 </div>

添加所有引用,无论是axios还是我们自己的js文件:head标签结束前加axios

   <!--引入axios框架-->
   <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js">
   </script>
 </head>

页面末尾:

 </body>
 <script src="../js/utils.js"></script>
 <script src="../js/question_detail.js"></script>
 </html>

在js文件夹下新建编写问题详情页的js文件:question_detail.js

 let questionApp=new Vue({
      el:"#questionApp",
      data:{
          question:{} /*表示对象*/
      },
      methods:{
          loadQuestion:function(){//加载问题
              // 从url?之后获取值
              let qid=location.search;
              // 当url路径中有?并且?之后有值时,这里的qid就是?加?之后的值
              //   /question/detail_teacher.html?150   qid:?150
              //   /question/detail_teacher.html       qid:null
              //   /question/detail_teacher.html?     qid:null
              // 如果qid不存在
              if(!qid){  //qid=null时,!null才存在
                  alert("必须指定问题的id");
                  return;
              }
              //如果qid有值需要去掉?   ?150   ->   150
              //                     0123
              qid=qid.substring(1);
              //通过axios请求获得当前问题详情
              axios({
                  //   /v1/questions/150
                  url:"/v1/questions/"+qid,
                  method:"get"
              }).then(function(response){
                  questionApp.question=response.data;
              })
          }
      },    
      created:function(){
          //页面加载完毕后调用显示问题详情的方法
          this.loadQuestion();
      }
  })

  重启服务,登录讲师首页,查看问题详情页是否能够全部加载!

3.5 显示问题详情的持续时间

  上面编写的问题详情显示中没有持续时间,需要将计算持续时间的代码复制过来,调用最终代码如下:question_detail.js

 let questionApp=new Vue({
     el:"#questionApp",
     data:{
         question:{} /*表示对象*/
    },
     methods:{
         loadQuestion:function(){//加载问题详情
             // 从url?之后获取值
             let qid=location.search;
             // 当url路径中有?并且?之后有值时,这里的qid就是?加?之后的值
             //   /question/detail_teacher.html?150   qid:?150
             //   /question/detail_teacher.html       qid:null
             //   /question/detail_teacher.html?     qid:null
             // 如果qid不存在
             if(!qid){  //qid=null时,!null才存在
                 alert("必须指定问题的id");
                 return;
            }
             //如果qid有值需要去掉?   ?150   ->   150
             //                     0123
             qid=qid.substring(1);
             //通过axios请求获得当前问题详情
             axios({
                 //   /v1/questions/150
                 url:"/v1/questions/"+qid,
                 method:"get"
            }).then(function(response){
                 questionApp.question=response.data;
                 questionApp.updateDuration();
            })
        },
         updateDuration:function(){//更新间隔时间
             //创建问题时候的时间毫秒数
             let createtime = new Date(this.question.createtime).getTime();
             //当前时间毫秒数
             let now = new Date().getTime();
             let duration = now - createtime;
             if (duration < 1000*60){ //一分钟以内
                 this.question.duration = "刚刚";
            }else if(duration < 1000*60*60){ //一小时以内
                 this.question.duration =
                    (duration/1000/60).toFixed(0)+"分钟以前";
            }else if (duration < 1000*60*60*24){
                 this.question.duration =
                    (duration/1000/60/60).toFixed(0)+"小时以前";
            }else {
                 this.question.duration =
                    (duration/1000/60/60/24).toFixed(0)+"天以前";
            }
        }
    },
     created:function(){
         //页面加载完毕后调用显示问题详情的方法
         this.loadQuestion();
    }
 })

   重启服务,访问讲师首页,查看问题详情,加载出来了问题的持续时间。

4 讲师回复功能

  显示出问题的详情后,讲师要可以进行问题的回复了。讲师回复问题也是一个表单提交,也是新增操作,所以和之前注册和发布问题一样,新建一个Vo类AnswerVo,在这个类中包含表单中要提交的信息,代码如下:

 package cn.tedu.knows.portal.vo;
 
 import lombok.Data;
 import lombok.experimental.Accessors;
 
 import javax.validation.constraints.NotBlank;
 import javax.validation.constraints.NotNull;
 import java.io.Serializable;
 
 @Data
 @Accessors(chain=true)
 public class AnswerVo implements Serializable {
     @NotNull(message = "问题id不能为空")
     private Integer questionId;
 
     @NotBlank(message = "回答内容不能为空")
     private String content;
 }
 

4.1 编写控制器接收信息

在AnswerController类中编写代码:

 package cn.tedu.knows.portal.controller;
 
 
 import cn.tedu.knows.portal.vo.AnswerVo;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.validation.BindingResult;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 
 import org.springframework.web.bind.annotation.RestController;
 
 /**
  * <p>
  * 前端控制器
  * </p>
  *
  * @author tedu.cn
  * @since 2021-08-23
  */
 @RestController
 @RequestMapping("/v1/answers")
 @Slf4j
 public class AnswerController {
     //讲师新增问题的方法
     @PostMapping("") // 通过/v1/answers这个路径来访问这个方法
     //方式一:@PreAuthorize("hasRole('ROLE_TEACHER')") 或 下面的方式二:
     @PreAuthorize("hasAuthority('/question/answer')")
     public String postAnswer(@Validated AnswerVo answerVo, BindingResult result,
                              @AuthenticationPrincipal UserDetails user) {
         log.debug("接收到表单信息:{}", answerVo);
         if (result.hasErrors()) {
             String msg = result.getFieldError().getDefaultMessage();
             return msg;
        }
         return "ok";
    }
 }
 

4.2 Vue绑定html

detail_teacher.html的381行附近修改代码:

 <div class="container-fluid mt-4" id="postAnswerApp"><!--修改id名称-->
  <!--   ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
   <h5 class="text-info mb-2"><i class="fa fa-edit"></i>写答案</h5>
   <form action="#" method="post"
         enctype="application/x-www-form-urlencoded"
         class="needs-validation" novalidate
         @submit.prevent="postAnswer"><!--阻止原有表单提交,postAnswer表示方法-->
       <!--   ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
     <div class="form-group">
       <textarea id="summernote" name="content" required ></textarea>
       <div class="invalid-feedback"
         :class="{'d-block':hasError}"><!--绑定class,设置显示-->
           <!--   ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
         <h5 v-text="message">回答内容不能为空!</h5><!--绑定内容-->
          <!--   ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
       </div>
     </div>
     <div class="form-group">
       <p class="text-right">
         <button type="submit" class="btn btn-primary">提交回答</button>
       </p>
     </div>
   </form>
 </div>

4.3 添加提交表单的js代码

继续在question_detail.js文件中添加新的代码:

 /*提交回答问题表单的js代码*/
 let postAnswerApp=new Vue({
     el:"#postAnswerApp",
     data:{
         hasError:false,
         message:""
    },
     methods: {
         postAnswer:function(){
             //从地址栏后面取?id
             let qid=location.search;
             //qid不存在
             if(!qid){
                 this.hasError=true;
                 this.message="必须指定问题id";
                 return;
            }
             // 去掉qid第一位的?   "?150">"150"
             qid=qid.substring(1);
             //获取summernote里面的内容
             let content=$("#summernote").val();
             //content内容为空
             if(!content){
                 this.hasError=true;
                 this.message="必须填写回答内容";
                 return;
            }
             //新建表单,填写提交数据
             let form=new FormData();
             form.append("questionId",qid)
             form.append("content",content);
             //发请求
             axios({
                 url:"/v1/answers",
                 method:"post",
                 data:form
            }).then(function(response){
                 postAnswerApp.hasError=true;
                 postAnswerApp.message=response.data;
            })
        }
    }
 })

  重启服务,登录讲师首页,进行回答问题,观察前后端是否能够正常接收\输出信息!

(1)浏览器

(2)IDEA

 

4.4 开始编写讲师回复的业务逻辑层

在接口IAnswerService添加方法:

 public interface IAnswerService extends IService<Answer> {
     //新增讲师回复问题的方法 要返回回答信息,显示在页面
     Answer saveAnswer(AnswerVo answerVo,String username);
 }

AnswerServiceImpl实现代码:

 @Service
 public class AnswerServiceImpl extends ServiceImpl<AnswerMapper, Answer> implements IAnswerService {
     @Autowired
     private AnswerMapper answerMapper;
     
     @Autowired
     private UserMapper userMapper;
     
     //新增讲师回答问题方法的逻辑层实现
     @Override
     @Transactional //开启事务管理
     public Answer saveAnswer(AnswerVo answerVo, String username) {
         User user=userMapper.findUserByUsername(username);
         Answer answer=new Answer()
                .setContent(answerVo.getContent())
                .setQuestId(answerVo.getQuestionId())
                .setUserNickName(user.getNickname())
                .setUserId(user.getId())
                .setLikeCount(0)
                .setAcceptStatus(0)
                .setCreatetime(LocalDateTime.now());
         int num=answerMapper.insert(answer);
         if(num!=1){
             throw new ServiceException("数据库异常");
        }
         //千万别忘了返回 answer!!!!!!!
         return answer;
    }
 }

4.5 完善控制层调用业务逻辑层代码

 @Resource
 private IAnswerService answerService;
 // 讲师新增回答的方法
 // 通过/v1/answers这个路径来访问这个方法
 @PostMapping("")
 @PreAuthorize("hasRole('ROLE_TEACHER')") //方式一
 //@PreAuthorize("hasAuthority('/question/answer')") //方式二
 public Answer postAnswer(//修改返回值为Answer
         @Validated AnswerVo answerVo, BindingResult result,
         @AuthenticationPrincipal UserDetails user){
     log.debug("接收到表单信息:{}",answerVo);
     if(result.hasErrors()){
         String msg=result.getFieldError().getDefaultMessage();
         throw new ServiceException(msg);//抛给统一异常处理类ExceptionControllerAdvice进行处理
    }
     //调用业务逻辑层方法回答问题
     Answer answer = answerService.saveAnswer(answerVo,user.getUsername());
     return answer;
 }

  重启服务,进行回答问题,观察数据库是否新增成功!

(1)浏览器

(2)数据库

4.6 显示问题回答列表

  我们需要根据当前问题的id查询数据库中所有这个问题的回答,并显示在所有回答的列表中,我们先写一下这个查询的sql语句:

 SELECT * FROM answer WHERE quest_id=157

数据库查询结果:

  

  sql语句很简单,可以使用QueryWrapper实现,所以不需要在数据访问层编写代码。

4.7 编写查询回答列表的业务逻辑层

IAnswerService接口代码:

 // 根据问题id查询对应回答列表
 List<Answer> getAnswersByQuestionId(Integer questionId);

AnswerServiceImpl实现代码如下:

 //根据问题id查询对应回答列表的逻辑层实现
 @Override
 public List<Answer> getAnswersByQuestionId(Integer questionId) {
     QueryWrapper<Answer> query=new QueryWrapper<>();
     query.eq("quest_id",questionId);
     List<Answer> answers=answerMapper.selectList(query);
     //别忘了返回
     return answers;
 }

4.8 编写查询回答列表的控制层

AnswerController编写代码:

 // /v1/answers/question/157
 // 按问题id查询所有回答的方法
 //此处为了与v1/answers/157区分,前面加了question/,按questionid查回答
 @GetMapping("/question/{id}")
 public List<Answer> questionAnswers(@PathVariable Integer id){
     //调用业务逻辑层方法得到回答列表
     List<Answer> answers= answerService.getAnswersByQuestionId(id);
     return answers;
 }

  重启服务,发送同步请求/v1/answers/question/157能观察到同步结果返回的json格式即可!

  通过和上面数据库的查询结果对别,可知查询结果正确!

4.9 vue绑定和js代码

detail_teacher.html的264行附近:

 <div class="row mt-5 ml-2" id="answersApp"><!--添加id-->
   <div class="col-12">
     <div class="well-sm">
       <h3><!--更改回答条数-->
         <span v-text="answers.length">3</span>条回答
       </h3>
     </div>
     <div class="card card-default my-5"
       v-for="answer in answers"><!--遍历回答-->
       <!-- Default panel contents -->
       <div class="card-header">
         <div class="row">
           <div class="col-1">
             <img style="width: 50px;height: 50px;border-radius: 50%;"
                  src="../img/user.jpg">
           </div>
           <div class="col-8 ">
             <div class="row">
               <span class="ml-3" v-text="answer.userNickName">张三</span><!--更改用户名-->
             </div>
             <div class="row">
               <span class="ml-3" v-text="answer.duration">2天前</span><!--绑定时间-->
             </div>
 
           </div>
           <div class="3">
 
           </div>
         </div>
       </div>
       <div class="card-body ">
         <span class="question-content text-monospace" v-html="answer.content"><!--绑定内容-->
          方法的重载是overloading,方法名相同,参数的类型或个数不同,对权限没有要求
          方法的重写是overrding 方法名称和参数列表,参数类型,返回值类型全部相同,但是所实现的内容可以不同,一般发生在继承中
         </span>
      <!-- 中间代码略 -->    
 </div>          

继续在question_detail.js文件中编写新的Vue对象代码如下:

 /*回答问题列表*/
 let answersApp=new Vue({
     el:"#answersApp",
     data:{
         answers:[] //绑定数组
    },
     methods:{
         loadAnswers:function(){
             let qid=location.search;
             if(!qid){
                 alert("必须指定问题id");
                 return;
            }
             qid=qid.substring(1);
             axios({
                 url:"/v1/answers/question/"+qid,
                 method:"get"
            }).then(function(response){
                 answersApp.answers=response.data;
            })
        }
    },
     created:function(){
         this.loadAnswers();
    }
 })

  启动服务,访问讲师问题详情页,查看问答列表是否全部加载出来。

4.10 重构计算持续时间的方法

  当前项目中有多次调用到计算持续时间的方法,如果每次都复制代码会造成代码冗余,所以我们决定将计算持续时间定义为一个方法,需要调用时调用即可。

在utils.js文件中最下方添加方法代码如下:

 //定义一个计算持续时间的方法
 function addDuration(item){  //item可以代表任意对象
     if(!item||!item.createtime){
         return;
    }
     //创建问题时候的时间毫秒数
     let createtime = new Date(item.createtime).getTime();
     //当前时间毫秒数
     let now = new Date().getTime();
     let duration = now - createtime;
     if (duration < 1000*60){ //一分钟以内
         item.duration = "刚刚";
    }else if(duration < 1000*60*60){ //一小时以内
         item.duration =
            (duration/1000/60).toFixed(0)+"分钟以前";
    }else if (duration < 1000*60*60*24){
         item.duration =
            (duration/1000/60/60).toFixed(0)+"小时以前";
    }else {
         item.duration =
            (duration/1000/60/60/24).toFixed(0)+"天以前";
    }
 }

在刚刚编写的question_detail.js代码中answersApp的Vue对象中对计算持续时间的方法进行调用:

 then(function(response){
     answersApp.answers=response.data;
     for(let i=0;i<answersApp.answers.length;i++){
         addDuration(answersApp.answers[i]);
    }
 })

  重启服务就能显示持续时间了!

4.11 新增的回答立即显示

  我们现在虽然能够显示回答列表,但是讲师回答的问题仍然需要刷新(shift+F5)之后,才能显示在页面上。我们希望讲师回答的信息立即显示在页面的回答列表中,需要下面的修改。

在question_detail.js文件修改postAnswerApp对象中的代码:

 then(function(response){
     if(typeof(response.data)=="object"){
         postAnswerApp.hasError=true;
         postAnswerApp.message="提交完成";//发布问题成功后提示“发布成功”
         // 重置summernote
         $("#summernote").summernote("reset");
         //将提交成功的回答追加到回答列表中
         answersApp.answers.push(response.data);
         response.data.duration="刚刚";//设置追加问题的持续时间为“刚刚”
    }else {
         postAnswerApp.hasError = true;
         postAnswerApp.message = response.data;
    }
 })

 

 

 

posted @ 2021-09-01 22:13  Coder_Cui  阅读(207)  评论(0编辑  收藏  举报