SpringBoot聚合项目:达内知道(六)-学生发布问题(复用显示标签、富文本编辑器、多选列表、接收表单数据、新增数据库操作)

1.学生发布问题

问答流程介绍:

学生提问功能流程:

1.1 复用显示标签

  我们看到问题发布页也显示所有标签信息,如果再次实现这个功能,会造成代码冗余。我们可以使用Vue模板来复用这个效果,减少冗余。

Vue模板的使用大概分为3个步骤:

  1. 定义模板

  1. 调用模板

  1. 添加引用

定义模板:在js文件夹中创建tags_nav_temp.js文件代码如下:

 /*定义Vue模板,名称为tags-app*/
 Vue.component("tags-app",{
    "props":["tags"],
    "template":`
     <div class="nav font-weight-light"><!--删除id,否则报错-->
         <a href="tag/tag_question.html" class="nav-item nav-link text-info"><small>全部</small></a>
         <a href="tag/tag_question.html"
            class="nav-item nav-link text-info"
            v-for="tag in tags">
           <small v-text="tag.name">Java基础</small></a><!--绑定内容-->
     </div>
    `
 })

调用模板:在需要显示模板中信息的位置编写代码,如在create.html页面186行附近添加代码如下:

 <!--引入标签的导航栏-->
 <div class="container-fluid" >
   <!-- 调用模板 -->
   <!-- tags-app 是模板的名称
       id="tagsApp" 是Vue绑定的id
       :tags对应模板代码中props:["tags"]
       ="tags" 对应的是Vue代码中的data中的tags
       -->
   <tags-app id="tagsApp" :tags="tags"></tags-app>
 </div>

添加引用:首页create.html页面的head标签结束前添加axios的引用

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

页面末尾添加引用:

 </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 富文本编辑器

  我们使用的富文本编辑器是summernote。

  它的使用很简单,但是功能强大,官方给出的必要依赖如下:

 <link rel="stylesheet" href="../bower_components/bootstrap/dist/css/bootstrap.min.css"> 
 <link rel="stylesheet" href="../bower_components/font-awesome/css/font-awesome.min.css">
 <link rel="stylesheet" href="../bower_components/summernote/dist/summernote-bs4.min.css">  
 <script src="../bower_components/jquery/dist/jquery.min.js"></script>
 <script src="../bower_components/popper.js/dist/umd/popper.min.js"></script>
 <script src="../bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
 <script src="../bower_components/polyfill/dist/polyfill.min.js"></script>
 <script src="../bower_components/summernote/dist/summernote-bs4.js"></script>
 <script src="../bower_components/summernote/dist/lang/summernote-zh-CN.min.js"></script>

  它支持我们添加各种常见的样式,甚至支持图片和视频的插入功能,它是怎么启动的呢?一般会定义一个<textarea>,在这个标签中定义id,然后使用summernote规定的固定代码启动。

 $(document).ready(function() {
   $('#summernote').summernote({
     height: 300,
     tabsize: 2,
     lang: 'zh-CN',
     placeholder: '请输入问题的详细描述...'
  });
 });

页面效果:

1.3 多选列表

  相对于单选的下拉框和复选框,多选列表的用户体验更好,方便用户选择和查看选择结果。我们使用Vue官方提供的一个模板插件来实现多选列表的功能:vue-select(v-select)。

使用该插件要添加必要依赖(前端页面已经配置好):

 <link rel="stylesheet"   href="../bower_components/vue-select/dist/vue-select.css"> 
 <script src="../bower_components/vue/dist/vue.js"></script>
 <script src="../bower_components/vue-select/dist/vue-select.js"></script>

绑定多选列表数据:create.html的203行附近有一个div需要添加id,一定要加,否则出不来多选列表框

 <div class="col-8" id="createQuestionApp"><!--添加id-->

create.html的213行附近开始,需要绑定多选列表,代码如下:

 <div class="form-group">
   <label >请至少选择一个标签:</label>
   <!-- multiple支持多选 required必须选
  :options为模板中需要的数据赋值,这里是绑定所有选项
  tags是数据库中所有标签
  selectedTags是用户选择的所有标签
   -->
   <v-select multiple required :options="tags"
     v-model="selectedTags" placeholder="请选择问题相关标签">
   </v-select>
 </div>
 <div class="form-group">
   <label >请选择老师:</label>
   <!--下面同上-->
   <v-select multiple required :options="teachers"
             v-model="selectedTeachers" placeholder="请选择回答老师">
   </v-select>
 </div>

js代码已经准备好,页面尾部添加引用:

 </body>
 <script src="../js/utils.js"></script><!--..表示返回上一级路径-->
 <script src="../js/tags_nav_temp.js"></script><!--必须先添加模板,后加运行的内容-->
 <script src="../js/tags_nav.js"></script>
 <!-- ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ -->
 <script src="../js/createQuestion.js"></script><!--一定要添加,否则没有多选列表-->
 </html>

编写绑定所有讲师的代码:从业务逻辑层开始,先在接口IUserService中添加方法:

 //查询所有讲师的方法
 List<User> getTeachers();

再在实现类UserServiceImpl编写实现代码如下:

 @Override
 public List<User> getTeachers() {
     QueryWrapper<User> query=new QueryWrapper();
     query.eq("type",1);//学生类型为0,老师类型为1
     List<User> list=userMapper.selectList(query);
     //千万别忘了返回!!!!
     return list;
 }

控制层UserController添加代码如下,注意之前的两个测试方法可以删除:

 package cn.tedu.knows.portal.controller;
 
 import cn.tedu.knows.portal.model.User;
 import cn.tedu.knows.portal.service.IUserService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 
 import org.springframework.web.bind.annotation.RestController;
 
 import java.util.List;
 
 /**
  * <p>
  * 前端控制器
  * </p>
  *
  * @author tedu.cn
  * @since 2021-08-23
  */
 @RestController
 //为今后微服务项目方便起见,根据微服务标准,
 //将@RequestMapping("/portal/user")改为@RequestMapping("/v1/users")
 @RequestMapping("/v1/users")
 public class UserController {
     //控制层调用业务逻辑层
     @Autowired
     private IUserService userService;
 
     //查询所有讲师的控制层方法
     @GetMapping("/master")
     public List<User> master(){
         List<User> list = userService.getTeachers();
         return list;
    }
 }
 

最终效果:

讲师业务逻辑层添加list和map的缓存:

在IUserService接口添加方法:

 //查询所有讲师的Map
 Map<String,User> getTeacherMap();

修改UserServiceImpl实现类最终代码如下:

 //创建缓存对象
 private List<User> teachers=new CopyOnWriteArrayList<>();//线程安全,存对象
 private Map<String,User> teacherMap=new ConcurrentHashMap<>();//线程安全,存名称和对象
 
 //查询所有讲师
 @Override
 public List<User> getTeachers() {
     if(teachers.isEmpty()) {//提高效率
         synchronized (teachers) {//加锁,避免线程并发问题
             if(teachers.isEmpty()) {//再次判断
                 QueryWrapper<User> query = new QueryWrapper();
                 query.eq("type", 1);//学生类型为0,老师类型为1
                 List<User> list = userMapper.selectList(query);
                 teachers.addAll(list);//将查询结果存到线程安全的集合中
                 for (User u:list) {//遍历list,存储Map
                     teacherMap.put(u.getNickname(),u);//注意:选老师选昵称
                }
            }
        }
    }
     //千万别忘了返回!!!!
     return teachers;
 }
 
 //查询所有讲师的Map
 @Override
 public Map<String, User> getTeacherMap() {
     if(teacherMap.isEmpty()){//保证效率
  getTeachers();//通过添加list进而给map赋值
  }
     //千万别返回null!!!!
     return teacherMap;
 }

最终效果:

 

1.4 接收表单数据

  上次课我们已经完成了表单提交的准备工作,例如:显示所有标签和显示所有讲师。下面我们为了能够接收表单数据,在vo包,定义个QuestionVo类,这个类中的成员变量对应表单中的每个输入框(createQuestion.js),代码如下:

 package cn.tedu.knows.portal.vo;
 
 import lombok.Data;
 
 import javax.validation.constraints.NotBlank;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.Pattern;
 import java.io.Serializable;
 
 @Data
 public class QuestionVo implements Serializable {//一般接收表单数据/封装数据的类都要实现Serializable接口
     @NotBlank(message = "标题不能为空")
     @Pattern(regexp = "^.{3,50}$",message = "标题是3~50个字符")
     private String title;
 
     @NotEmpty(message = "请至少选择一个标签")
     private String[] tagNames = {};//默认赋值一个空数组,防止空指针异常
 
     @NotEmpty(message = "请至少选择一个老师")
     private String[] teacherNicknames = {};//默认赋值一个空数组,防止空指针异常
 
     @NotBlank(message = "内容不能为空")
     private String content;
 }
 

在控制层QuestionController类中编写代码接收表单数据,代码如下:

 //用户提问的控制层方法
 //路径是localhost:8080/v1/questions
 @PostMapping("")
 //启动Spring Validation验证,验证表单数据是否符合格式要求,注意BindingResult必须写在后面,不能分开
 public String createQuestion(
         @Validated QuestionVo questionVo,BindingResult result){
     log.debug("接收到信息:{}",questionVo);
     //验证结果有错误,不通过
     if(result.hasErrors()){
         //输出错误信息,不在执行下面方法
         String msg=result.getFieldError().getDefaultMessage();
         return msg;
    }
  //验证结果没有错误,返回“ok”
     return "ok";
 }

在create.html页面进行页面绑定:206行附近的form标签

 <form @submit.prevent="createQuestion" ><!--阻止表单原有效果提交-->

重新启动服务,编写表单数据提交,到Idea控制台上观察接收到的数据是否全面/正确。

(1)浏览器

(2)控制台

1.5 新增问题的数据库操作

新增问题后涉及到的表:

新增问题的业务流程如下:

  1.控制器接收用户提交的表单(已完成)

  2.控制器调用业务逻辑层方法完成新增

  3.业务逻辑层方法中先增问题,再增问题和标签关系、问题和讲师的关系

      需要注意的是,一个问题和多个标签、讲师都有关系,所以可能需要用到循环新增

  4.业务逻辑层调用数据访问层完成相关操作,最后返回结果

1.5.1 编写新增问题的业务逻辑层

先编写业务逻辑层接口IQuestionService,添加方法如下:

 // 新增发布问题的业务逻辑层方法
 void saveQuestion(QuestionVo questionVo,String username);

在QuestionServiceImpl实现类中实现方法如下:

     @Autowired
     private QuestionTagMapper questionTagMapper;//问题-标签表用
     @Autowired
     private UserQuestionMapper userQuestionMapper;//用户-问题表用,此处的用户指的是老师
     @Autowired
     IUserService userService;//获得用户信息用
 
     //新增用户发布的问题
     @Override
     public void saveQuestion(QuestionVo questionVo, String username) {
         //1.根据用户名获得用户信息
         User user = userMapper.findUserByUsername(username);
         //2.将接收到的Tag字符串数组拼接成字符串
         // {"java基础","javaSE","面试题"}-->"Java基础,javaSE,面试题"
         StringBuilder builder = new StringBuilder();
         //遍历问题标签名字符串数组,拼接成字符串
         for (String tagName:questionVo.getTagNames()) {
             builder.append(tagName).append(",");//名字后面有,分割
        }
         //去除最后一个,:deleteCharAt
         String tagNames = builder.deleteCharAt(builder.length()-1).toString();
         //3.创建Question对象并赋值,赋值采用链式书写,因为Question类上面已经写了@Accessors(chain=true)[lombok提供,仅限set]
         Question question = new Question()
                .setTitle(questionVo.getTitle())//question 2个
                .setContent(questionVo.getContent())
                .setUserNickName(user.getNickname())//question 2个
                .setUserId(user.getId())
                .setCreatetime(LocalDateTime.now())//question其它参数:6个
                .setStatus(0)
                .setPageViews(0)
                .setPublicStatus(0)
                .setDeleteStatus(0)
                .setTagNames(tagNames);
         //4.执行新增Question
         //根据数据库执行后受影响的行数,判断数据库是否执行成功
         int num = questionMapper.insert(question);
         if(num!=1){
             throw new ServiceException("数据库异常");
        }
 
         //5.新增问题和选中标签的关系 question_tag
         //获得包含所有标签的map对象:方便后面根据标签名取标签对象
         Map<String,Tag> tagMap = tagService.getTagMap();
         //遍历用户选中的所有标签
         for (String tagName:questionVo.getTagNames()) {
             //根据用户选中的标签名称获得标签对象
             Tag t = tagMap.get(tagName);//根据key找value:根据标签名取标签对象
             //实例化QuestionTag表,并赋值
             QuestionTag questionTag = new QuestionTag()
                    .setQuestionId(question.getId())
                    .setTagId(t.getId());//tagid
             //执行新增操作
             num = questionTagMapper.insert(questionTag);
             if (num!=1){
                 throw new ServiceException("数据库异常");
            }
             //输出日志关系
             log.debug("新增了关系:{}",questionTag);//必须在类上加@Slf4j
        }
         
         //6.新增问题和选中老师之间的关系 user_question
         //获得包含所有老师的map对象:方便后面根据标签名取标签对象
         Map<String,User> teacherMap = userService.getTeacherMap();
         //遍历老师名称数组
         for(String nickname:questionVo.getTeacherNicknames()){//注意是老师昵称
             //根据老师昵称获得老师对象
             User teacher = teacherMap.get(nickname);
             //实例化UserQuestion对象,并赋值
             UserQuestion userQuestion = new UserQuestion()
                    .setQuestionId(question.getId()) //问题id
                    .setUserId(teacher.getId()) //注意是老师id,不是userid
                    .setCreatetime(LocalDateTime.now());//记住表还有一个createtime
             //执行新增操作
             num = userQuestionMapper.insert(userQuestion);
             if(num!=1){
                 throw new ServiceException("数据库异常");
            }
             log.debug("新增了关系:{}",userQuestion);
        }
    }

1.5.2 完善控制层调用

控制层编写好的方法只能接收表单信息,但是没调用业务逻辑层,修改代码如下:

 //用户提问的控制层方法
 //路径是localhost:8080/v1/questions
 @PostMapping("")
 //启动Spring Validation验证,验证表单数据是否符合格式要求,注意BindingResult必须写在后面,不能分开; @AuthenticationPrincipal获取当前登录用户
 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) {
         e.printStackTrace();
         return e.getMessage();//返回错误信息
    }
 }

1.5.3 js代码完善

要想提交完毕之后正确显示学生首页或提示错误信息,需要在js文件中进行修改:createQuestion.js的26行开始修改为:

 then(function(r){
     console.log(r.data);
     if(r.data=="ok"){////r.status查看当前状态,r.data表示从controller接收到的数据
         location.href="/index_student.html";//页面跳转到学生首页
    }else{
         alert(r.data);//接收数据失败时,输出接收到的数据
    }
 })

  启动服务,登录学生首页,进行发布问题,查看问题列表是否新增了刚刚发表的问题,同时,刷新数据库查看是否有数据添加上去。

   注意:现在最好不要添加图片,因为在富文本编辑器中图片是以二进制的形式进行存储的,如果此时添加数据要占用很大内存,后续会针对此进行修改,存储图片的路径,相比之前,极大节省了内存。

 

posted @ 2021-08-27 22:03  Coder_Cui  阅读(258)  评论(0编辑  收藏  举报