续 框架

157 阅读22分钟

续 开发学生发布问题功能

学生发布问题业务逻辑层代码

@Autowired
private IUserService userService;
@Autowired
private QuestionTagMapper questionTagMapper;
@Autowired
private UserQuestionMapper userQuestionMapper;
@Override
public void saveQuestion(QuestionVO questionVO, String username) {
    // 1.根据用户名获得用户信息
    User user=userMapper.findUserByUsername(username);
    // 2.根据用户选中的标签数组,拼接一个标签字符串
    // {"Java基础","Java SE","面试题"}
    // "Java基础,Java SE,面试题,"
    StringBuilder builder=new StringBuilder();
    for(String tagName : questionVO.getTagNames()){
        builder.append(tagName).append(",");
    }
    //  经过字符串拼接,我们需要删除拼接最后的,
    //  也就是当前字符串长度-1位置的字符
    String tagNames=builder
            .deleteCharAt(builder.length()-1).toString();
    // 3.实例化Question对象,收集需要的信息(共10列)
    Question question=new Question()
            .setTitle(questionVO.getTitle())
            .setContent(questionVO.getContent())
            .setUserNickName(user.getNickname())
            .setUserId(user.getId())
            .setCreatetime(LocalDateTime.now())
            .setStatus(0)
            .setPageViews(0)
            .setPublicStatus(0)
            .setDeleteStatus(0)
            .setTagNames(tagNames);
    // 4.新增Question
    int num=questionMapper.insert(question);
    if (num!=1){
        throw new ServiceException("数据库异常!");
    }
    // 5.新增问题和标签的关系
    // 因为要获得标签的id,所以要先将包含所有标签的Map获取过来
    // 然后根据用户选中标签名称获得对应的标签对象以确定标签的id
    Map<String,Tag> tagMap=tagService.getTagMap();
    // 遍历用户选中的所有标签
    for(String tagName : questionVO.getTagNames()){
        // 根据tagName获得tag对象
        Tag t=tagMap.get(tagName);
        // 实例化QuestionTag对象并赋值然后执行新增操作
        QuestionTag questionTag=new QuestionTag()
                .setQuestionId(question.getId())
                .setTagId(t.getId());
        num=questionTagMapper.insert(questionTag);
        if(num!=1){
            throw new ServiceException("数据库异常");
        }
        log.debug("新增了问题和标签的关系:{}",questionTag);
    }
    // 6.新增问题和讲师的关系
    Map<String,User> teacherMap=userService.getTeacherMap();
    for(String nickname : questionVO.getTeacherNicknames()){
        User teacher=teacherMap.get(nickname);
        UserQuestion userQuestion=new UserQuestion()
                .setQuestionId(question.getId())
                .setUserId(teacher.getId())
                .setCreatetime(LocalDateTime.now());
        num=userQuestionMapper.insert(userQuestion);
        if(num!=1){
            throw new ServiceException("数据库忙");
        }
        log.debug("新增了问题和讲师的关系:{}",userQuestion);
    }
}

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

之前我们只是编写了控制层接收表单信息的代码

现在我们要完成调用业务逻辑层的代码

QuestionController中createQuestion方法修改如下

@PostMapping("")
public String createQuestion(
        @Validated QuestionVO questionVO,
        BindingResult result,
    	//  ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
        @AuthenticationPrincipal UserDetails user){
    log.debug("接收到表单信息:{}",questionVO);
    if(result.hasErrors()){
        String msg=result.getFieldError().getDefaultMessage();
        return msg;
    }
    // 这里调用业务逻辑层
    // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
    try {
        questionService.saveQuestion(questionVO,
                                    user.getUsername());
        // 返回运行结果
        return "ok";
    }catch (ServiceException e){
        log.error("新增失败",e);
        return e.getMessage();
    }
}

完善axios的结果判断

我们要调整一下提供给大家的createQuestion.js代码

.then(function(r){
    console.log(r.data);
    if(r.data=="ok"){
        // 如果控制层返回ok也就是成功的话,跳转到学生首页
        location.href="/index_student.html";
    }else{
        // 如果错误的话将信息输出到浏览器控制台
        console.log(r.data);
    }
})

重启服务之后可以实现新增功能

Spring 声明式事务

什么是事务

一般我们提到的"事务"指数据库事务

所谓数据库事务:是数据库管理系统执行数据库操作的一个逻辑单位,由一个有限的数据库操作序列构成

我们刚刚完成开发的学生发布问题功能就是一个数据库事务

但是如果我们在程序运行过程中发生了异常,可能出现question新增到数据库,但是关系没有完全新增完毕的状况,我们将这种状态称之为"数据库完整性缺失"

数据库完整性缺失会为程序运行出现各种bug埋下隐患,是我们要防止的现象

我们如果要想防止这样的情况发生就需要将这个功能的所有sql操作保存在同一个事务进行提交

这样就能实现这些sql操作要么都执行,要么都不执行的效果

一个数据库事务中的所有sql操作只有都顺利完成才能最终影响数据库数据,如果这个过程中发生了意外而终止,那么就不会提交这个操作,而执行"回滚/roll back",让数据库还原为执行事务之前的状态

常见面试题

数据库事务的四大特性(ACID特性)

  • 原子性(Atomicity):事务是一个整体,是数据库操作的最小单位,不可再分,意思是事务中的sql操作要么都执行,要么都不执行

  • 一致性(Consistency):事务运行前后,数据库状态是一致的

  • 隔离性(Isolation):多个事务并发时,事务之间互不影响\

  • 持久性(Durability):也译作"永久性",意思是事务对数据库的操作是永久的,不会无缘故的自动撤销或更改

SpringBoot下使用事务

SpringBoot框架给我们提供了非常简单的办法实现数据库事务的启动

能够保证一个业务逻辑层方法中的所有数据库操作,要么都执行要么都不执行

在业务逻辑层方法前添加注解即可

@Transactional(事务)

@Override
// 添加事务注解
// 实现效果:当前方法中所有sql操作要么都执行,要么都不执行
// 只要方法运行过程中发生异常,那么已经执行的sql语句都会"回滚"
@Transactional
public void saveQuestion(QuestionVO questionVO, String username) {
		//.......
}

使用事务的原则:

一个业务逻辑层方法中包含两个以及两个以上的增删改操作时

为了保证数据库完整性,应该在方法前添加事务注解

所以,我们之前编写的注册功能的方法也应该添加事务注解

统一异常处理

try-catch的不足

我们的注册和问题的发布功能都在控制层方法中使用了try-catch结构处理的业务逻辑层方法的异常,长此以往,控制层代码会出现大量try-catch结构导致代码臃肿,维护困难

try {
    questionService.saveQuestion(questionVO,user.getUsername());
    return "ok";
}catch (ServiceException e){
    log.error("新增失败",e);
    return e.getMessage();
}

编写统一异常处理类

我们可以利用SpringMvc框架提供的"统一异常处理"功能

简化控制层代码中调用业务逻辑层方法的异常处理,免去编写try-catch结构的冗余

我们针对控制器方法进行优化,所以进行统一异常处理的类编写在controller包

创建ExceptionControllerAdvice代码如下

// 统一异常处理类,类名是没有固定要求的
// @RestControllerAdvice表示当控制器运行出现特定情况时,可以运行这个类中的方法
// 所谓特定情况可以有多种定义,我们这里需要了解的就是异常的情况
@RestControllerAdvice
@Slf4j
public class ExceptionControllerAdvice {
    // 这个注解表示下面的方法是用来处理控制器发生异常时的情况
    // Handler:处理者(处理器)
    @ExceptionHandler
    // 方法的参数类型是ServiceException,表示控制器方法中发生ServiceException时
    // 当前方法就会捕捉异常对象并执行
    // 方法名称没有固定要求
    public String handlerServiceException(ServiceException e){
        log.error("业务异常",e);
        return e.getMessage();
    }

    // 为了保证控制层方法能够处理任何类型的异常
    // 下面编写一个处理异常的方法,参数是异常的父类
    @ExceptionHandler
    public String handlerException(Exception e){
        log.error("其他异常",e);
        return e.getMessage();
    }
}

有了这个类的存在,我们现有的控制器方法中所有的try-catch结构都可以删除

今后在控制器中编写的任何业务逻辑层调用都不需要编写try-catch结构了

image-20211203103925721.png

实现文件上传

创建文件上传页面

提供给大家一个文件上传的页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>文件上载演示</title>
</head>
<body>
    <form id="demoForm" method="post" 
          enctype="multipart/form-data" 
          action="/upload/file" >
        <div>
            <label>上传文件
                <input id="imageFile" type="file" name="imageFile">
            </label>
        </div>
        <button type="submit">上传文件</button>
    </form>
    <img id="image" src=""  alt="">
</body>
</html>

实现同步上传功能

页面中是编写好表单的

我们先使用表单提交的功能实现文件上传

所谓文件上传就是将客户端的文件复制到服务器端

我们在SystemController类中编写文件上传的代码

// 文件上传的方法
@PostMapping("/upload/file")
public String upload(MultipartFile imageFile) throws IOException {

    // 1.确定文件保存的路径
    //  会将不同日期的文件保存在不同的文件夹中,所以使用当前年月日组成文件夹名
    // path:2022/01/04
    String path= DateTimeFormatter.ofPattern("yyyy/MM/dd")
                                .format(LocalDateTime.now());
    // 确定并实例化要上传的文件夹路径对象
    //  F:/upload/2022/01/04
    File folder=new File("F:/upload/"+path);
    // 创建文件夹
    folder.mkdirs();// mkdirssssssssss!!!!

    // 2.确定文件名
    // 获得原始文件名以截取文件后缀名
    String filename=imageFile.getOriginalFilename();// 原始文件名
    // 截取后缀名ab.c.jpg     ->    .jpg
    //         01234567      substring(4)
    String ext=filename.substring(filename.lastIndexOf("."));
    // 创建随机新文件名以尽量减少文件名重复几率
    //   name:aidjvas-32ujd-91nbasj-ahdsfuhauasdf.jpg
    String name= UUID.randomUUID().toString()+ext;

    // 3.执行上传
    // 实例化要保存的文件对象(路径名+文件名的对象)
    File file=new File(folder,name);
    // 输出实际上传路径以方便测试观察
    log.debug("文件上传路径:{}",file.getAbsolutePath());
    // 执行上传
    imageFile.transferTo(file);

    // 4.返回结果
    return "upload complete";

}

因为upload.html相关内容时编写好的

所以直接重启服务在页面上选择文件点击上传即可

去检查上传的问题是否出现在指定的位置

实现文件异步上传

实际开发中,使用异步上传文件的几率更大

我们要学习如何使用js来实现异步文件上传的操作

修改upload.html页面代码如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>文件上载演示</title>
    <script src="bower_components/jquery/dist/jquery.min.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<form id="demoForm" method="post"
      enctype="multipart/form-data"
      action="/upload/file" >
    <div>
        <label>上传文件
            <input id="imageFile" type="file" name="imageFile">
        </label>
    </div>
    <button type="submit">上传文件</button>
</form>
<img id="image" src=""  alt="">
</body>
<script>
    // 在id为demoForm的表单提交时运行的方法
    $("#demoForm").submit(function(){
        // 获得用户选中的所有文件对象
        let files=document.getElementById("imageFile").files;
        // 判断是否选中了文件
        if(files.length>0){
            // 执行上传操作
            uploadFile(files[0]);
        }else{
            alert("请选择要上传的文件");
        }
        // 阻止表单原有的提交效果,实现异步提交
        return false;
    })

    function uploadFile(file){
        // 上传图片(文件)需要表单,先创建表单对象
        let form=new FormData();
        // 向表单对象中保存名称和它对应的数据
        form.append("imageFile",file);
        axios({
            url:"/upload/file",
            method:"post",
            data:form
        }).then(function(response){
            console.log(response.data);
        })
    }

</script>
</html>

重启服务,

依然是在upload.html页面上选择文件点击上传

之后到指定位置检查是否成功

创建静态资源服务器

在我们使用网页上传图片时,一般都会有我们上传图片的预览

这就需要我们搭建一个静态资源服务器,才能更好的实现效果

静态资源服务器是knows父项目的又一个聚合子项目

它专门负责静态资源(如用户上传的图片)的管理和显示和我们的portal项目互补干扰

创建子项目knows-resource

image-20211203142751484.png

不需要勾选任何框架,直接下一步创建项目

父子相认

<module>knows-resource</module>

knows-resource项目的pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>cn.tedu</groupId>
        <artifactId>knows</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.tedu</groupId>
    <artifactId>knows-resource</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>knows-resource</name>
    <description>Demo project for Spring Boot</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

因为删除了测试依赖,所以删除测试的test文件夹

image-20211203143526245.png

为了让静态资源服务器能够读取我们上传的文件,并显示出来

我们要设置当前项目的资源路径

knows-resource项目的application.properties文件配置如下

# portal项目使用了8080端口,当前项目如果直接启动,会和portal项目冲突
# 所以配置一个不同的端口号,以保证运行
server.port=8899

# 配置静态资源服务器的资源路径
# 默认情况下,我们的访问服务器时,端口号之后的路径是在访问当前项目static目录下的内容
# 例如localhost:8899/index.html 是在static目录下寻找index.html来显示
# 但是我们现在要访问的是F:/upload路径下的各种图片资源
# 所以我们要将默认的路径修改为F:/upload
# 这样我们再访问localhost:8899/a.jpg时,访问的就是F:/upload/a.jpg了
spring.resources.static-locations=file:F:/upload

启动knows-resource项目

然后测试访问指定目录中的图片等文件例如

例如:http://localhost:8899/2.jpg

利用静态资源服务器回显上传的图片

图片上传之后再页面上显示预览的效果我们称之为"回显"

想要实习这个效果我们需要先配置一些信息

要将上传文件的路径和静态资源服务器的路径配置在

portal项目的application.properties文件中

# 上传文件的路径以及静态资源服务器的路径
knows.resource.path=file:F:/upload
knows.resource.host=http://localhost:8899

SystemController上传代码进行修改

// 从application.properties文件中获得配置数据的注解
@Value("${knows.resource.path}")
private File resourcePath;
@Value("${knows.resource.host}")
private String resourceHost;


// 文件上传的方法
@PostMapping("/upload/file")
public String upload(MultipartFile imageFile) throws IOException {

    // 1.确定文件保存的路径
    //  会将不同日期的文件保存在不同的文件夹中,所以使用当前年月日组成文件夹名
    // path:2022/01/04
    String path= DateTimeFormatter.ofPattern("yyyy/MM/dd")
                                .format(LocalDateTime.now());
    // 确定并实例化要上传的文件夹路径对象
    //  F:/upload/2022/01/04
    		     //  ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
    File folder=new File(resourcePath,path);
    // 创建文件夹
    folder.mkdirs();// mkdirssssssssss!!!!

    // 2.确定文件名
    // 获得原始文件名以截取文件后缀名
    String filename=imageFile.getOriginalFilename();// 原始文件名
    // 截取后缀名ab.c.jpg     ->    .jpg
    //         01234567      substring(4)
    String ext=filename.substring(filename.lastIndexOf("."));
    // 创建随机新文件名以尽量减少文件名重复几率
    //   name:aidjvas-32ujd-91nbasj-ahdsfuhauasdf.jpg
    String name= UUID.randomUUID().toString()+ext;

    // 3.执行上传
    // 实例化要保存的文件对象(路径名+文件名的对象)
    File file=new File(folder,name);
    // 输出实际上传路径以方便测试观察
    log.debug("文件上传路径:{}",file.getAbsolutePath());
    // 执行上传
    imageFile.transferTo(file);

    // 我们要实现回显,必须得到上传的文件在静态资源服务器的路径
    // 例如  http://localhost:8899/2022/01/04/dac23as....aa0d.jpg
    //         resourceHost    /     path   /      name
    // 我们拼接获得访问这个上传文件的url
    //  ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
    String url=resourceHost+"/"+path+"/"+name;
    log.debug("回显上传图片的url:{}",url);
    // 4.返回结果 返回url以回显图片
    //     ↓↓↓
    return url;

}

最后还需要upload.html页面中axios接收到url之后再页面上显示才行

页面上有个img标签,直接修改它的src属性为url即可

.then(function(response){
    console.log(response.data);
    // 现在response.data是能够显示图片的路径了
    // 将image标签的src属性替换即可
    $("#image").attr("src",response.data);

})

重启portal项目

保证resource项目在启动状态

然后在upload.html页面选中图片后上传,观察回显的图片显示出来即可

富文本编辑器上传文件

我们最终的目标就是在富文本编辑器中上传文件

create.html页面的尾部

在原有的<scrpit>标签及代码中修改结果为:

<script>
  $(document).ready(function() {
    $('#summernote').summernote({
      height: 300,
      tabsize: 2,
      lang: 'zh-CN',
      placeholder: '请输入问题的详细描述...',
      callbacks:{
        //callbacks译为"回调",其实就是我们所说的"事件"
        // onImageUpload就是用户选中图片的事件
        // 在用户选择图片时运行,files就是用户选中图片的数组
        onImageUpload:function(files){
          // 获取数组中的图片以用于上传代码
          let file=files[0];
          // 将要上传的文件保存到表单中
          let form=new FormData();
          form.append("imageFile",file);
          axios({
            url:"/upload/file",
            method:"post",
            data:form
          }).then(function(response) {
            // 我们要将上传的图片显示在富文本编辑器中
            // 创建一个Img标签
            let img=new Image();
            // 设置img的src属性值为上传图片的url
            img.src=response.data;
            // summernote提供的向内容中添加标签的方法
            $("#summernote").summernote("insertNode",img);
          })
        }
      }
    });
    $('select').select2({placeholder:'请选择...'});
  });
</script>

重启portal项目

仍然保持resource启动

在富文本编辑器选中图片后能够显示在富文本编辑器中表示成功

英文

Transactional: 事务

Original:原始

Absolute:绝对的

Attribute:属性\参数 简写attr

显示问题列表的状态

我们现在的学生首页中

问题列表中问题状态全部是已解决,这明显是不对的

我们应该利用question的status属性,来显示它对应的状态

  • 0:未回复
  • 1:已回复
  • 2:已解决

我们使用v-show这个vue属性决定是否显示对应状态的图片

index_student.html页面中修改

192行附近

<div class="col-md-12 col-lg-2">
  <!--
    v-show这个vue绑定直接决定是否显示当前元素
    =号之后的值为true显示,false隐藏
  -->
  <span class="badge badge-pill badge-warning"
        style="display: none"
        v-show="question.status==0">未回复</span>
  <span class="badge badge-pill badge-info"
        style="display: none"
        v-show="question.status==1">已回复</span>
  <span class="badge badge-pill badge-success"
        v-show="question.status==2">已解决</span>
</div>

显示用户信息面板

image-20211203172406207.png

上图显示的内容就是用户信息面板

这个面板会在多个页面出现,需要时使用Vue模板复用即可

我们课上只带大家完成提问数的显示,而收藏数布置为作业

创建UserVO类

我们需要先将一个能够保存用户面板信息的类定义出来

vo包创建UserVO类代码如下

@Data
// 使当前类支出链式set赋值的注解
@Accessors(chain=true)
public class UserVO implements Serializable {

    private Integer id;
    private String username;
    private String nickname;

    // 问题数
    private int questions;
    // 收藏数
    private int collections;

}

确定sql语句

我们需要查询当前登录用户的问题数和收藏数

可以通过sql语句查询

代码如下

-- 查询用户id为11的问题数
SELECT count(*) FROM question
WHERE user_id=11 AND delete_status=0

-- 查询用户id为11的收藏数
SELECT COUNT(*) FROM user_collect
WHERE user_id=11

编写数据访问层

我们编写好了sql语句,就是为了Mapper中来使用的

在QuestionMapper接口中添加查询用户问题数的方法

收藏数作业的查询,也建议编写在QuestionMapper中

@Repository
public interface QuestionMapper extends BaseMapper<Question> {

    // 根据用户id查询问题数
    @Select("select count(*) from question where user_id=#{id}")
    int countQuestionsByUserId(Integer userId);

    // 收藏数作业的方法编写在下面即可
    // ....
}

编写业务逻辑层代码

因为我们编写的是用户信息面板,主体还是用户

所以在IUserService中添加方法

// 根据用户名查询用户信息面板的方法
UserVO getUserVO(String username);

UserServiceImpl实现类添加方法

@Autowired
private QuestionMapper questionMapper;
@Override
public UserVO getUserVO(String username) {
    // 根据用户名获得用户信息
    User user=userMapper.findUserByUsername(username);
    // 根据用户id获得用户的问题数
    int questions=questionMapper.countQuestionsByUserId(user.getId());
    // (作业)根据用户id获得用户的收藏数

    // 实例化UserVo对象 赋值 最后返回
    UserVO userVO=new UserVO()
            .setId(user.getId())
            .setUsername(user.getUsername())
            .setNickname(user.getNickname())
            .setQuestions(questions);
    // 千万别忘了返回userVO
    return userVO;
}

开发控制层代码

UserController中添加返回UserVO的方法

@GetMapping("/me")
public UserVO me(@AuthenticationPrincipal UserDetails user){
    UserVO userVO=userService.getUserVO(user.getUsername());
    return userVO;
}

重启\启动portal项目

浏览器地址栏输入同步请求

localhost:8080/v1/users/me

Vue代码和html绑定

index_student.html页面

277行附近

<!--个人信息-->
<div class="container-fluid font-weight-light"
  id="userApp">
<!-- ↑↑↑↑↑↑↑ -->
  <div class="card">
    <h5 class="card-header"
      v-text="user.nickname">陈某</h5>
      	<!-- ↑↑↑↑↑↑↑ -->
    <div class="card-body">
      <div class="list-inline mb-1 ">
          <a class="list-inline-item mx-3 my-1 text-center">
            <div><strong>10</strong></div>
            <div>回答</div>
          </a>
          <a class="list-inline-item mx-3 my-1 text-center" href="personal/myQuestion.html">
            <div>
              <strong v-text="user.questions">10</strong>
                			<!-- ↑↑↑↑↑↑↑ -->
            </div>
            <div>提问</div>
          </a>
          <a class="list-inline-item mx-3 my-1 text-center" href="personal/collect.html">
            <div>
              <strong v-text="user.collections">10</strong>
                			<!-- ↑↑↑↑↑↑↑ -->
            </div>
            <div>收藏</div>
          </a>
          <a class="list-inline-item mx-3 my-1 text-center" href="personal/task.html">
            <div><strong>10</strong></div>
            <div>任务</div>
          </a>
      </div>
    </div>
  </div>
</div>

下面要开始编写js代码了

这是我们第一次自己编写Vue代码

可以先在页面末尾添加js文件的引用以防止遗忘

</body>
<script src="js/utils.js"></script>
<script src="js/tags_nav.js"></script>
<script src="js/index.js"></script>
<script src="js/user_info.js"></script>
			<!-- ↑↑↑↑↑↑↑ -->
</html>

下面就创建这个user_info.js

代码如下

let userApp=new Vue({
    el:"#userApp",
    data:{
        user:{}
    },
    methods:{
        loadUserVO:function(){
            axios({
                url:"/v1/users/me",
                method:"get"
            }).then(function(response){
                // 将控制器返回的userVO对象,绑定给当前vue的user
                userApp.user=response.data;
            })
        }
    },
    created:function(){
        // 页面加载完毕时运行的方法
        // 执行加载用户信息面板的方法
        this.loadUserVO();
    }
})

显示讲师首页

问答流程回顾

image-20211206100847698.png

我们已经编写完学生提问的功能

下面要开始完成讲师回复的功能

但是在完成回复之前,需要先完成讲师首页,以支持讲师登录

而且同样在登录页面进行登录,我们需要根据身份判断,登录不同首页

根据不同身份,跳转不同页面

我们所有用户的登录页都是login.html

但是我们需要在登录成功之后判断用户的身份(角色),根据用户身份决定跳转哪个页面

具体实现步骤如下

1.首先我们要完成一个查询,根据用户id查询用户角色

2.将查询出来的角色保存在Spring-Security中,之前Spring-Security只保存了登录用户的所有权限,现在要添加保存登录用户的角色信息

3.新建一个专门判断用户角色的控制器类,其中编写方法,这个方法获得当前登录用户详情,判断用户的角色,根据判断结果跳转不同页面

编写查询用户角色的数据访问层

之前我们编写过根据用户id查询用户权限的Mapper(五表关联查询)

现在要添加一个根据用户id查询用户角色的Mapper(三表关联查询)

首先确定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=3

在UserMapper中编写上面sql语句操作的实现,代码如下

// 根据用户id获得用户角色的方法
@Select("SELECT r.id , r.name\n" +
        "FROM user u\n" +
        "LEFT JOIN user_role ur ON u.id=ur.user_id\n" +
        "LEFT JOIN role r       ON r.id=ur.role_id\n" +
        "WHERE u.id=#{id}")
List<Role> findUserRolesById(Integer userId);

将用户角色保存到Spring-Security

我们在UserDetailsServiceImpl类中编写了用户登录的实现

其中有保存所有权限的操作

下面修改该类中的方法,将用户的所有角色也保存在Spring-Security中

以用于在需要时取出,判断用户身份

修改后代码如下

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    //方法的参数是要登录的用户的用户名username
    //1. 根据用户名获得用户对象
    User user=userMapper.findUserByUsername(username);
    //2. 验证用户对象是不是为空,如果为空登录失败
    if(user==null){
        return null;
    }
    //3. 根据用户id查询出用户的所有权限
    List<Permission> permissions=userMapper
            .findUserPermissionsById(user.getId());
    //4. 将权限的List集合转换为String类型数组
    String[] auth=new String[permissions.size()];
    int i=0;
    for(Permission p:permissions){
        auth[i]=p.getName();
        i++;
    }
    //  ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
    // 根据用户id查询用户所有角色
    List<Role> roles=userMapper.findUserRolesById(user.getId());
    // 数组扩容,以保证能够保存新的角色信息
    auth= Arrays.copyOf(auth,auth.length+roles.size());
    // {"/add","/delete","/save","/update",null}
    // 将角色信息保存在auth数组中
    for(Role role:roles){
        auth[i]=role.getName();
        i++;
    }
	// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
    //5. 创建UserDetails对象,并向它的属性中赋值
    UserDetails details= org.springframework.security.core
            .userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .authorities(auth)
            //设置当前账号是否锁定,false表示不锁定
            .accountLocked(user.getLocked()==1)
            //设置当前账号是否可用,false表示可用
            .disabled(user.getEnabled()==0)
            .build();
    //6. 返回UserDetails对象
    // 千万别忘了返回details
    return details;
}

编写控制器跳转对应首页

在controller包中编写一个控制器HomeController

登录成功后让请求访问这个控制器中的方法,

控制器中的方法获得用户详情,解析用户角色

根据用户角色判断结果跳转不同首页

// 本类的目标是根据当前登录用户角色,跳转不同页面
// @RestController标记针对异步请求进行处理的控制器类
// 由它标记的控制器方法的返回值或返回的对象,都会转换成json格式响应给axios
// 我们当前控制器是要根据不同身份跳转不同页面,重点在于要跳转页面,而不是响应json数据
// @Controller标记的控制器类的方法中支持返回特定字符串重定向到指定页面的效果

@Controller
public class HomeController {
    // 我们要判断登录用户是什么角色,所以要先定义两个角色的常量,用于判断
    public static final GrantedAuthority STUDENT=
            new SimpleGrantedAuthority("ROLE_STUDENT");
    public static final GrantedAuthority TEACHER=
            new SimpleGrantedAuthority("ROLE_TEACHER");

    // 当登录成功后一般都是访问首页
    // 我们设计用户访问localhost:8080/或localhost:8080/index.html都是访问这个方法
    @GetMapping(value = {"/","/index.html"})
    public String index(@AuthenticationPrincipal UserDetails user){
        // 判断UserDetails中是否包含讲师身份
        if(user.getAuthorities().contains(TEACHER)){
            // 如果是讲师,使用返回特定格式字符串实现页面重定向效果
            return "redirect:/index_teacher.html";
        }else if(user.getAuthorities().contains(STUDENT)){
            return "redirect:/index_student.html";
        }
        // 既不是讲师也不是学生直接返回null(也可以返回登录页)
        return null;
    }
}

修改一个小bug

因为我们在自定义Spring-Security登录页面时,设置了默认登录成功访问学生首页

所以讲师在访问login.html进行登录后会跳转到学生首页,这是不正确的

我们需要修改之前设置,将登录成功默认页修改为"/index.html"即可

SecurityConfig类

修改如下属性

.defaultSuccessUrl("/index.html")// 登录成功跳转学生首页

重启服务测试

讲师首页复用模板

现在讲师登录可以跳转到讲师首页了

但是讲师首页上标签列表和用户信息面板没有显示

而且他们是学生首页编写过的,可以复用的

复用标签列表

标签列表的模板定义已经完成,在create.html页面中使用过

我们只需要进行调用和引用即可

index_teacher.html

先添加axios的cdn引用

  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>

172行附近,调用模板

<!--引入标签的导航栏-->
<div class="container-fluid">
  <tags-app id="tagsApp" :tags="tags"></tags-app>
</div>

页面末尾添加引用

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

复用用户信息面板

模板复用的步骤

1.定义模板

2.调用模板

3.引用模板

在js文件中定义模板文件user_info_temp.js,代码如下

Vue.component("user-app",{
    props:["user"],
    template:`
    <div class="container-fluid font-weight-light">
        <div class="card">
          <h5 class="card-header"
              v-text="user.nickname">陈某</h5>
          <div class="card-body">
            <div class="list-inline mb-1 ">
                <a class="list-inline-item mx-3 my-1 text-center">
                  <div><strong>10</strong></div>
                  <div>回答</div>
                </a>
                <a class="list-inline-item mx-3 my-1 text-center" href="personal/myQuestion.html">
                  <div>
                    <strong v-text="user.questions">10</strong>
                  </div>
                  <div>提问</div>
                </a>
                <a class="list-inline-item mx-3 my-1 text-center" href="personal/collect.html">
                  <div>
                    <strong v-text="user.collections">10</strong>
                  </div>
                  <div>收藏</div>
                </a>
                <a class="list-inline-item mx-3 my-1 text-center" href="personal/task.html">
                  <div><strong>10</strong></div>
                  <div>任务</div>
                </a>
            </div>
          </div>
        </div>
      </div>
    `
})

调用模板

index_teacher.html

249行附近,删除原有代码替换为:

<!--个人信息-->
<user-app id="userApp" :user="user"></user-app>

引用模板

在页面末尾添加个人信息面板的引用

<script src="js/user_info_temp.js"></script>
<script src="js/user_info.js"></script>

讲师首页任务列表显示分析

学生首页显示学生的问题列表,是当前登录学生提问的所有问题

讲师首页显示的是讲师的任务列表,是要显示当前提问涉及到当前登录讲师的问题

所以我们要先明确讲师任务列表的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

分析完成之后开始开发具体代码

开发数据访问层

QuestionMapper接口中添加查询讲师问题列表的方法

@Select("SELECT q.* FROM question q\n" +
        "LEFT JOIN user_question uq ON q.id=uq.question_id\n" +
        "WHERE uq.user_id=#{id} OR q.user_id=#{id}\n" +
        "ORDER BY q.createtime desc")
List<Question> findTeacherQuestions(Integer id);

这个sql语句比较复杂建议测试一下以保证运行正确

找到测试包,新建测试类,测试代码如下

@Autowired
QuestionMapper questionMapper;
@Test
public void getQuestions(){
    List<Question> questions=questionMapper
            .findTeacherQuestions(3);
    for(Question q:questions){
        System.out.println(q);
    }
}

开发业务逻辑层

我们要开发支持分页的业务逻辑层调用

IQuestionService接口中添加方法

注意分页的返回值和参数

// 分页查询返回讲师任务列表的方法
PageInfo<Question> getTeacherQuestions(String username,
                                   Integer pageNum,Integer pageSize);

QuestionServiceImpl实现代码如下

@Override
public PageInfo<Question> getTeacherQuestions(String username, Integer pageNum, Integer pageSize) {
    User user=userMapper.findUserByUsername(username);
    // 设置分页条件
    PageHelper.startPage(pageNum,pageSize);
    // 执行查询
    List<Question> list=questionMapper
            .findTeacherQuestions(user.getId());
    //list就是查询讲师任务列表的分页结果
    for(Question q:list){
        List<Tag> tags=tagNamesToTags(q.getTagNames());
        q.setTags(tags);
    }
    // 千万别忘了返回任务列表
    return new PageInfo<>(list);
}

开发控制层代码

QuestionController 添加查询讲师任务列表的功能

代码如下

// 查询讲师任务列表
@GetMapping("/teacher")
// 当前登录用户必须是讲师身份才能查询讲师任务列表
// 使用Spring-Security提供的权限\角色验证的功能来实现限制
// @PreAuthorize注解设置要求当前登录用户持有ROLE_TEACHER角色才能访问
@PreAuthorize("hasRole('ROLE_TEACHER')")
public PageInfo<Question> teacher(
        @AuthenticationPrincipal UserDetails user,
        Integer pageNum){
    Integer pageSize=8;
    if(pageNum==null)
        pageNum=1;
    // 调用业务逻辑层方法
    PageInfo<Question> pageInfo=questionService
            .getTeacherQuestions(user.getUsername(),
                    pageNum,pageSize);
    return pageInfo;

}

重启服务进行同步请求测试

localhost:8080/v1/questions/teacher

登录讲师正常显示查询结果(json格式)

登录学生访问显示"不允许访问"的错误提示

表示我们编写的显示注解生效了!

绑定讲师任务列表的Vue和js

我们可以参考学生首页的Vue绑定和js代码

将学生首页中questionsApp的div整体复制到讲师首页的相同位置

学生首页的js文件和讲师首页的js文件也基本一致的

js文件夹中就地复制index.js文件命名为index_teacher.js

修改index_teacher.js文件中的axios请求路径

第16行附近

axios({
    //                  ↓↓↓↓↓↓
    url: '/v1/questions/teacher',
    method: "GET",
    params:{
        pageNum:pageNum
    }
})

最后讲师首页末尾添加上面js的引用

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

重启服务,讲师首页就能显示任务列表了!

显示问题详情

讲师要想进行回答\回复

必须在问题详情页中进行

但是现在任务列表中点击问题的标题连接,报404错误,原因是连接的地址根本不存在

我们必须将它指定为存在的页面

跳转到问题详情页

我们通过修改标题链接的href属性实现将问题id传递到问题详情页

index_teacher.html的209行附近

<a class="text-dark" href="question/detail.html"
   v-text="question.title"
   :href="'/question/detail_teacher.html?'+question.id">
  eclipse 如何导入项目?
</a>

这样我们就实现了页面的正确跳转

而且还传递了当前点击问题的id

这样就可以在问题详情页获得这个id来查询这个问题的内容来显示在页面上了

问题详情页显示问题信息

我们现在显示的是讲师的问题详情页

页面地址栏上有要显示信息问题的id

我们要在页面加载完毕之后,获得?之后的id值

并发送axios请求到控制器查询这个id的问题的数据以显示出来

开发业务逻辑层

IQuestionService接口添加根据问题id查询问题对象的方法

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

QuestionServiceImpl实现代码如下

@Override
public Question getQuestionById(Integer id) {
    // 根据MybatisPlus提供的方法来获取Question对象
    Question question=questionMapper.selectById(id);
    // 获得当前Question对象的所有tags集合
    List<Tag> tags=tagNamesToTags(question.getTagNames());
    question.setTags(tags);
    // 千万别忘了返回question
    return question;
}

续 显示问题详情

开发显示问题详情的功能

开发控制层代码

上次课完成了业务逻辑层的开发

下面继续编写控制层代码

QuestionController添加方法

// 根据问题id查询问题详情
// SpringMvc支持我们编写占位符匹配url
// 下面路径中{id}就是一个占位符
// 当一个请求例如:localhost:8080/v1/questions/149路径时
// 路径中/149就会自动匹配/{id}的占位符,并且149会赋值给id
// 我们可以在控制器方法中获得{id}的值来使用
@GetMapping("/{id}")
public Question question(
        // 获得占位符{id}的赋值的方法
        // 1.必须编写@PathVariable注解
        // 2.参数的名称必须和{}中的名称一致
        @PathVariable Integer id){
    Question question=questionService.getQuestionById(id);
    return  question;
}

重启服务,发送同步请求

localhost:8080/v1/questions/149

如果能够显示出id为149号问题的json格式信息,表示一切正常

vue绑定和js代码

detail_teacher.html页面进vue绑定操作

先添加axios的cdn引用

  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>

设置Vue的id

187行附近

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

215行 附近

开始进行绑定操作

<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>

页面末尾添加js文件的引用

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

在js文件夹中创建question_detail.js文件

编写代码如下

let questionApp=new Vue({
    el:"#questionApp",
    data:{
        question:{}
    },
    methods:{
        loadQuestion:function(){
            // question/detail_teacher.html?149
            // 我们要获得url地址栏中?之后的id值
            let qid=location.search;
            // location.search可以获得地址栏中?之后的内容,具体获取规则如下
            // 如果url中没有? qid=""
            // 如果url中有?但是?之后没有任何内容 qid=""
            // 如果url中有?而且?之后有内容(例如149) qid="?149"
            // 既question/detail_teacher.html?149   ->  qid=?149
            if(!qid){
                // !qid就是判断qid不存在,或理解为qid没有值
                alert("请指定问题的id");
                return;
            }
            //  qid   ?149  -> 149
            qid=qid.substring(1);
            axios({
                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()
    }
})

开发讲师回复功能

问题详情页detail_teacher.html

从上到下分为了3个区域

  • 当前问题的详情
  • 问题的回答列表
  • 讲师回复问题的表单(富文本编辑器)

image-20211207093621081.png

我们要完成的区域是讲师回答\添加回答的表单

网页中间部分的所有回答\回答列表暂时忽略

创建AnswerVO类发送信息到控制器

之前编写注册和问题发布都新增了VO类,这次也不例外

我们经过对数据库表信息的分析,

结果发现新增时需要我们提供的数据是内容(content)和问题id(quest_id)

按照需要的数据声明AnswerVO类代码如下

@Data
@Accessors(chain = true)
public class AnswerVO implements Serializable {

    @NotNull(message = "问题id不能为空")
    private Integer questionId;

    @NotBlank(message = "问题内容不能为空")
    private String content;
}

开发控制器代码

AnswerController类中添加接收讲师回复表单信息的方法

@RestController
@RequestMapping("/v1/answers")
@Slf4j
public class AnswerController {

    @Autowired
    private IAnswerService answerService;

    // 新增讲师回复的控制器方法
    // 路径就是/v1/answers
    @PostMapping("")
    // 只有讲师(持有回答权限)的用户才能回复问题
    // 一种思路是判断是否有回答权限
    @PreAuthorize("hasAuthority('/question/answer')")
    // 另一种思路是判断是否有讲师角色
    // @PreAuthorize("hasRole('ROLE_TEACHER')")
    // hasRole是专门判断登录用户角色的指令
    // hasRole后面的角色名称可以省略ROLE_开头的部分
    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";
    }


}

Vue绑定和js代码

在detail_teacher.html页面中进行vue绑定

380行附近

								<!--   ↓↓↓↓↓↓↓↓↓↓   -->
<div class="container-fluid mt-4" id="postAnswerApp">
  <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">
      <!--   ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑   -->
    <div class="form-group">
      <textarea id="summernote" name="content" required ></textarea>
      <div class="invalid-feedback">
        <h5>回答内容不能为空!</h5>
      </div>
    </div>
    <div class="form-group">
      <p class="text-right">
        <button type="submit" class="btn btn-primary">提交回答</button>
      </p>
    </div>
  </form>
</div>

然后是编写js代码

我们可以继续在question_detail.js文件中编写代码

let postAnswerApp=new Vue({
    el:"#postAnswerApp",
    data:{},
    methods: {
        postAnswer:function(){
            // 要想提交表单,需要问题id和回答内容
            // 问题id就是url地址栏?之后的内容
            let qid=location.search;
            if(!qid){
                alert("qid没有值!!!!");
                return;
            }
            qid=qid.substring(1);
            // 使用jquery代码获得富文本编辑器中的内容
            let content=$("#summernote").val();
            // 创建表单
            let form=new FormData();
            form.append("questionId",qid);
            form.append("content",content);
            axios({
                url:"/v1/answers",
                method:"post",
                data:form
            }).then(function(response){
                console.log(response.data);
            })

        }
    }
})

重启服务

测试讲师和学生登录当前页面进行回复并提交的效果

学生回答被阻止,发生"不允许访问"的异常,讲师的回复可以将回复信息输出到idea控制台

开发业务逻辑层

新增Answer也是不需要开发AnswerMapper直接编写业务逻辑层即可

IAnswerService接口添加方法

public interface IAnswerService extends IService<Answer> {

    // 新增回答的业务逻辑层方法
    // 因为后面需要将我们新增的回答对象显示在回答列表中
    // 所以这个方法返回值为当前新增成功的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())
                .setLikeCount(0)
                .setUserId(user.getId())
                .setUserNickName(user.getNickname())
                .setQuestId(answerVO.getQuestionId())
                .setCreatetime(LocalDateTime.now())
                .setAcceptStatus(0);
        int num=answerMapper.insert(answer);
        if(num!=1){
            throw new ServiceException("数据库异常");
        }
        //千万不要忘记返回answer
        return answer;
    }
}

控制层调用业务逻辑层的代码

AnswerController类中添加调用业务逻辑层方法的代码

// 这里调用业务逻辑层方法
answerService.saveAnswer(answerVO,user.getUsername());
return "ok";

重启服务,讲师登录后编写回答后提交

在数据库中能够看到讲师回复的answer即可

显示问题的回答列表

上面章节我们完成了讲师的回复功能

但是回复的内容不能再页面中显示出来

是因为我们没有开发显示当前问题回答列表的功能

下面我们要开发将当前问题的所有回答显示在页面上的功能

思路:

1.在数据库中查询当前问题对应的所有回答(还是?之后的id值),完成数据访问层开发

2.开发业务逻辑调用数据访问层方法

3.控制层代码以及页面显示

数据访问层的sql

查询一个问题的所有回答的sql语句如下

SELECT * FROM answer
WHERE quest_id=149

上面的sql语句就是实现效果的sql

代码比较简单,我们可以直接使用之前学习的QueryWrapper在业务逻辑层中直接实现

开发业务逻辑层

IAnswerService接口中添加方法

// 根据问题id查询所有回答的方法
List<Answer> getAnswersByQuestionId(Integer questionId);

AnswerServiceImpl实现类

@Override
public List<Answer> getAnswersByQuestionId(Integer questionId) {
    // 实例化QueryWrapper对象,设置查询条件,并执行查询
    QueryWrapper<Answer> query=new QueryWrapper<>();
    query.eq("quest_id",questionId);
    List<Answer> answers=answerMapper.selectList(query);
    // 千万别忘了返回answers
    return answers;
}

开发控制层代码

AnswerController添加方法

// 根据问题id查询回答列表
//  /v1/answers/question/149
@GetMapping("/question/{id}")
public List<Answer> questionAnswers(
        @PathVariable Integer id){
    List<Answer> answers=answerService
            .getAnswersByQuestionId(id);
    return answers;
}

重启服务,同步测试

http://localhost:8080/v1/answers/question/149

如果能显示149号问题所有回答,表示一切正常

Vue绑定和js代码

detail_teacher.html

262行附近

<!--列出所有的答案-->
<div class="row mt-5 ml-2" id="answersApp">
    				   <!-- ↑↑↑↑↑↑↑↑↑↑  -->
  <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文件中编写代码

添加代码如下

let answersApp=new Vue({
    el:"#answersApp",
    data:{
        answers:[]
    },
    methods:{
        loadAnswers:function(){
            // 这个方法也需要问题的id,就在url?之后
            let qid=location.search;
            if(!qid){
                return;
            }
            qid=qid.substring(1);
            axios({
                url:"/v1/answers/question/" + qid,
                method:"get"
            }).then(function(response){
                answersApp.answers=response.data;
            })
        }
    },
    created:function(){
        this.loadAnswers();
    }
})

重启服务之后登录讲师,访问问题详情页

观察是否显示当前问题的所有回答列表

重构计算持续时间的方法

我们的回答列表中也包含显示持续时间的功能

这样我们在项目中已经多次需要计算持续时间

为了减少计算持续时间代码出现的冗余

我们决定在utils.js文件中编写一个计算持续时间的方法

然后所有需要这个方法的位置进行调用

代码如下

// 计算持续时间的方法
function addDuration(item){
    // 判断item参数是否为空,并且是否包含createtime属性
    if(!item || !item.createtime){
        // 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)+"天以前";
    }

}

我们编写了上面的方法之后,在已经编写完毕的js代码中

之前计算持续时间的方法就可以修改为调用addDuration了

question_detail.js文件中questionApp对象中有计算持续时间的方法,修改为

axios({
    url:"/v1/questions/"+qid,
    method:"get"
}).then(function(response){
    questionApp.question=response.data;
    // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
    addDuration(response.data);
})

原本为了实现持续时间编写的updateDurtaion方法可以删除了!

还有index.js\index_teacher.js中计算持续时间的方法也可以优化了

修改为:

updateDuration:function () {
    let questions = this.questions;
    for(let i=0; i<questions.length; i++){
        // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
        addDuration(questions[i]);
    }
}

上面章节中我们实现的回答列表中也需要计算持续时间的功能

我们也可以通过addDuration来计算

question_detail.js文件中loadAnswers方法中添加代码:

loadAnswers:function(){
    let qid=location.search;
    if(!qid){
        return;
    }
    qid=qid.substring(1);
    axios({
        url:"/v1/answers/question/"+qid,
        method:"get"
    }).then(function(response){
        answersApp.answers=response.data;
        // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
        answersApp.updateDuration();
    })
},
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
updateDuration:function(){
    let answers=this.answers;
    for(let i=0;i<answers.length;i++){
        addDuration(answers[i]);
    }
}

将讲师新增的回答立即显示在页面上

上面章节为止

我们可以实现讲师回复的功能,现象新增回答

但是必须通过刷新页面才能实现新增的回答显示在页面上

而刷新这个操作会重新加载这个页面的所有数据, 重新从数据库获得

对于客户端需要消耗流量,对于服务器端需要连接数据库,都是不希望发生的操作

我们希望讲师回答问题提交时,直接通过异步的方式获得新增的回答对象,然后将这个对象添加到回答列表中,这样页面上就能显示他的信息了

我们先要修改AnswerController类中的新增回答的方法,返回新增的Answer对象

//     ↓↓↓↓↓↓
public Answer postAnswer(
        @Validated AnswerVO answerVO,
        BindingResult result,
        @AuthenticationPrincipal UserDetails user){
    log.debug("表单信息:{}",answerVO);
    if(result.hasErrors()){
        String msg=result.getFieldError().getDefaultMessage();
        //  ↓↓↓↓↓↓↓↓↓↓↓ 修改为抛出异常
        throw new ServiceException(msg);
    }
    // 这里调用业务逻辑层方法
    // ↓↓↓↓↓↓↓↓↓↓↓  接收业务逻辑层中新增成功返回的Answer对象
    Answer answer=answerService.saveAnswer(answerVO,user.getUsername());
    return answer;
}

answer新增成功axios会接收由AnswerController返回的answer对象

在js代码中,编写将返回的answer对象添加到回答列表中的代码,就看可以实现新增的回答显示在页面中的效果了

question_detail.js文件中postAnswer方法.then方法进行修改

.then(function(response){
    console.log(response.data);
    let answer=response.data;
    // 设置answer的duration持续时间属性为"刚刚"
    answer.duration="刚刚";
    // 将新增成功的对象新增到回答列表中
    answersApp.answers.push(answer);
    // 利用富文本编辑器提供的方法,重置其中内容
    $("#summernote").summernote("reset");
})

重启服务测试效果

开发新增评论的功能

完善Comment表

comment表就是我们的评论表

这个表故意少编写了一个列,因为在实际开发过程中,经常会遇到需要在开发中修改\添加表中列的情况,现在comment列是缺少了用户昵称列user_nick_name

下面就使用sql语句添加这个列

--comment表新增一个user_nick_name的列
ALTER TABLE comment
ADD COLUMN user_nick_name VARCHAR(255)
AFTER user_id

--为新增的user_nick_name列赋值
UPDATE comment c SET user_nick_name=
(SELECT nickname FROM user u
WHERE c.user_id=u.id)

随笔

@Validated:
SpringValidation框架提供的验证功能,能够触发VO类中各种验证的操作


@AuthenticationPrincipal:
从Spring-Security中获得当前登录用户的UserDetails用户详情信息


@PathVariable:
从url中获得占位符的内容

英文

update,alter,modify,change:修改