【开源项目--稻草】Day06
- 1. 学生提问与解答功能
- 2. 显示create.html
- 2.1 HomeController中代码
- 2.2 复用网页的标签导航条
- 3. 创建问题发布界面
- 3.1 富文本编辑器
- 4.多选下列框
- 5.动态加载所有标签和老师
- 6. 发布问题的业务处理
1. 学生提问与解答功能
学生提问:
提问时指定标签和回答问题的老师
讲师回复:
指定讲师登录系统后可以对学员的提问进行回复
评论:
学员收到讲师回复后可以对回复进行评论(追问)
讲课也可以进行评论(追答或补充)
问题状态:
学生刚提问时为:未回复
讲师回复后为:已回复
问题解决后为:已解决
- 问题怎么能称为解决?
- 学员标记为解决状态
- 讲师可以将问题标记为解决
- 问题超过一定时间,自动解决
我们先开发的模块是学员的问题发布功能
2. 显示create.html
将static/question/create.html
复制到
templates/question/create.html
并编写控制器代码显示这个页面
2.1 HomeController中代码
// 显示学生问题发布页面
@GetMapping("/question/create.html")
public ModelAndView createQuestion(){
//templates/question/create.html
return new ModelAndView("question/create");
}
2.2 复用网页的标签导航条
我们在index.html页面中已经开发过显示数据库中所有标签到页面的导航条中的代码
现在create.html又需要这样的功能,难道我们要再开发一次吗? 显然不是的
我们可以使用Thymeleaf提供的fragment模板来替换当前页面的内容
使用步骤
步骤1:
定义模板
在index.html页面中,将要复用的html区域用特定标签标记
th:fragment=“xxx”
<div class="container-fluid" th:fragment="tags_nav" >
<!-- 代码略 -->
</div>
步骤2:
套用模板
现在是create.html需要复用代码,所以是这个页面套用模板
th:replace="xxx"来套用
保证页面支持th:的写法不报错
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
套用模板
<div class="container-fluid" th:replace="index::tags_nav" >
<div class="nav font-weight-light">
<!-- 代码略 -->
</div>
</div>
其中th:replace="index::tags_nav"的意思是
用index.html页面中名为tags_nav的模板中的代码替换掉当前编写套用标记的html标签
最后在代码临结束之前,引入ajax和Vue代码
</body>
<script src="../js/utils.js"></script>
<script src="../js/tags_nav.js"></script>
</html>
3. 创建问题发布界面
3.1 富文本编辑器
富文本编辑器适用于那些需要格式甚至是图片的用户输入需求
这样的编辑器都是基于标签的
只是在这个标签的基础上添加了很多js代码或相关插件的实现,我们无需手动开发
市面上有很多功能全面的免费的富文本编辑器工具,其中比较流行的就是我们使用的这个
summernote官方网站是:www.summernote.org
下载它的支持后再页面上编写如下代码引入样式和js文件
<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 name="content" id="summernote"></textarea>
最终通过编写js代码来开启这个编辑器的效果
<script>
$(document).ready(function() {
$('#summernote').summernote({
height: 300, //高度
tabsize: 2, //tab大小
lang: 'zh-CN',//中文支持
placeholder: '请输入问题的详细描述...'
});
});
</script>
一般这个代码在页面最后位置
4.多选下列框
网页中的下拉列表框是一个能够多选,并且有选中后样式的功能的控件
这个控件是由Vue提供的插件Vue-Select实现的
官方网站是 https://vue-select.org/
依赖JQuery同时也依赖Vue核心的js
除此之外还需要引入一些依赖
<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的form表单中找到选择标签和老师的下拉框
将他们的代码修改为:
<!-- 这个id是自己添加的!!!注意!!!! -->
<div class="col-8" id="createQuestionApp">
<h4 class="border-bottom m-2 p-2 font-weight-light"><i class="fa fa-question-circle-o" aria-hidden="true"></i>
填写问题</h4>
<form >
<div class="form-group">
<label for="title">标题:</label>
<input type="text" class="form-control" id="title" name="title" placeholder="请填写标题3~50字符"
pattern="^.{3,50}$" required v-model="title">
</div>
<div class="form-group">
<label >请至少选择一个标签:</label>
<v-select multiple required v-bind:options="tags"
v-model="selectedTags" placeholder="请选择问题的标签"
>
</v-select>
</div>
<div class="form-group">
<label >请选择老师:</label>
<v-select multiple required
v-bind:options="teachers"
v-model="selectedTeachers"
placeholder="请选择回答的老师"
>
</v-select>
</div>
<div class="form-group">
<!--富文本编辑器 start-->
<label for="summernote">问题正文</label>
<textarea name="content" id="summernote"></textarea>
<!--富文本编辑器 end-->
</div>
<button type="submit" class="btn btn-primary mt-3">提交问题</button>
</form>
</div>
上面表单修改完成后,js文件中指定createQuestion.js引用添加依赖
</body>
<script src="../js/utils.js"></script>
<script src="../js/tags_nav.js"></script>
<script src="../js/createQuestion.js"></script>
</html>
createQuestion.js文件中的内容
不连接数据库可以写为:
Vue.component('v-select', VueSelect.VueSelect);
let createQuestionApp = new Vue({
el:'#createQuestionApp',
data:{
title:'',
selectedTags:[],
tags:['Java基础','Java OOP', 'Java SE'],
selectedTeachers:[],
teachers:['范传奇', '王克晶','刘国斌']
}
});
测试可以发现,我们成功的可以选择做个标签和老师了
下面的工作就是从数据库加载所有标签和老师
5.动态加载所有标签和老师
动态加载所有标签的实现非常简单,因为我们直接可以调用现成的控制器方法
返回所有Tag的集合
createQuestion.js中编写代码如下
//启动v-select标签
Vue.component("v-select", VueSelect.VueSelect);
let createQuestionApp = new Vue({
el: "#createQuestionApp",
data: {
title:"",
selectedTags:[],
tags:[],
selectedTeachers:[],
teachers:["苍老师","范老师","克晶老师"]
},
methods: {
loadTags:function(){
$.ajax({
url:"/v1/tags",
method:"get",
success:function(r){
console.log(r);
if(r.code==OK){
let list=r.data;//获得所有标签数组
//list=[{id:1,name:"java基础"},{...},{...}]
let tags=[];
for(let i=0;i<list.length;i++){
//push方法表示向这个数组的最后位置添加元素
//效果和java中list的add方法一致
tags.push(list[i].name);
}
console.log(tags);
createQuestionApp.tags=tags;
}
}
});
}
},
created:function(){
this.loadTags();
}
});
动态加载所有老师
实现步骤
步骤1:
添加业务逻辑层接口方法IUserService
// 查询所有老师用户的方法
List<User> getMasters();
步骤2:
实现这个业务逻辑层接口的方法UserServiceImpl
@Override
public List<User> getMasters() {
QueryWrapper<User> query=new QueryWrapper<>();
query.eq("type",1);
List<User> list=userMapper.selectList(query);
return list;
}
步骤3:
编写控制层:UserController中设计路径v1/users/master,返回R<List>即可
@RestController
@RequestMapping("/v1/users")
public class UserController {
@Autowired
IUserService userService;
@GetMapping("/master")
public R<List<User>> master(){
List<User> masters=userService.getMasters();
return R.ok(masters);
}
}
步骤4:
参照绑定所有标签的Vue代码绑定所有老师即可
//启动v-select标签
Vue.component("v-select", VueSelect.VueSelect);
let createQuestionApp = new Vue({
el: "#createQuestionApp",
data: {
title:"",
selectedTags:[],
tags:[],
selectedTeachers:[],
teachers:["苍老师","范老师","克晶老师"]
},
methods: {
loadTags:function(){
$.ajax({
url:"/v1/tags",
method:"get",
success:function(r){
console.log(r);
if(r.code==OK){
let list=r.data;//获得所有标签数组
//list=[{id:1,name:"java基础"},{...},{...}]
let tags=[];
for(let i=0;i<list.length;i++){
//push方法表示向这个数组的最后位置添加元素
//效果和java中list的add方法一致
tags.push(list[i].name);
}
console.log(tags);
createQuestionApp.tags=tags;
}
}
});
},
loadTeachers:function(){
$.ajax({
url:"/v1/users/master",
method:"get",
success:function(r){
console.log(r);
if(r.code==OK){
let list=r.data;//获得所有讲师数组
let teachers=[];
for(let i=0;i<list.length;i++){
//push方法表示向这个数组的最后位置添加元素
//效果和java中list的add方法一致
teachers.push(list[i].nickname);
}
console.log(teachers);
createQuestionApp.teachers=teachers;
}
}
});
}
},
created:function(){
this.loadTags();
this.loadTeachers();
}
});
6. 发布问题的业务处理
我们先来完成数据提交到控制器的内容
步骤1:
为了这次提交新建一个Vo类 QuestionVo
@Data
public class QuestionVo implements Serializable {
@NotBlank(message = "标题不能为空")
@Pattern(regexp = "^.{3,50}$",message = "标题长度在3~50个字符之间")
private String title;
private String[] tagNames={};
private String[] teacherNickNames={};
@NotBlank(message = "问题内容不能为空")
private String content;
}
步骤2:
开发发布问题的控制器代码
在QuestionController中代码如下
// 学生发布问题的控制器方法
@PostMapping
public R createQuestion(
@Validated QuestionVo questionVo,
BindingResult result){
if(result.hasErrors()){
String message=result.getFieldError()
.getDefaultMessage();
log.warn(message);
return R.unproecsableEntity(message);
}
if(questionVo.getTagNames().length==0){
log.warn("必须选择至少一个标签");
return R.unproecsableEntity("必须选择至少一个标签");
}
if(questionVo.getTeacherNickNames().length==0){
log.warn("必须选择至少一个老师");
return R.unproecsableEntity("必须选择至少一个老师");
}
//这里应该将vo对象交由service层去新增
log.debug("接收到表单数据{}",questionVo);
return R.ok("发布成功!");
}
步骤3:
找到create.html的form标签
使用v-on:submit.prevent绑定提交事件 .prevent是阻止表单提交用的
<form v-on:submit.prevent="createQuestion">
步骤4:
在createQuestion.js
文件中新增createQuestion方法
并在方法中收集要提交的信息,最后使用ajax提交到控制器
createQuestion:function(){
let content=$("#summernote").val();
console.log(content);
//定义一个data对象,用于ajax提交信息到控制器
let data={
title:this.title,
tagNames:this.selectedTags,
teacherNickNames:this.selectedTeachers,
content:content
}
console.log(data);
$.ajax({
url:"/v1/questions",
traditional:true,//使用传统数组的编码方式,SpringMvc才能接收
method:"post",
data:data,
success:function(r){
console.log(r)
if(r.code== OK){
console.log(r.message);
}else{
console.log(r.message);
}
}
});
}
下面我们需要完成新增问题的业务逻辑的开发
首先来了解一下我们需要什么操作才能完成这个业务
举例
步骤1:
讲师的信息也是可以保存在换存中来避免多次访问数据库来提交运行效率的
所以我们参照对标签的处理方法,对所有讲师也进行缓存
IUserService中
// 查询所有老师用户的方法
List<User> getMasters();
//查询所有老师用户的Map方法
Map<String,User> getMasterMap();
步骤2
参照TagServiceImpl中对标签的缓存,处理讲师缓存
UserServiceImpl代码如下
private final List<User> masters=
new CopyOnWriteArrayList<>();
private final Map<String,User> masterMap=
new ConcurrentHashMap<>();
private final Timer timer=new Timer();
//初始化块:在构造方法运行前开始运行
{
timer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (masters){
masters.clear();
masterMap.clear();
}
}
},1000*60*30,1000*60*30);
}
@Override
public List<User> getMasters() {
if(masters.isEmpty()){
synchronized (masters){
if(masters.isEmpty()){
QueryWrapper<User> query=new QueryWrapper<>();
query.eq("type",1);
//将所有老师缓存masters集合中
masters.addAll(userMapper.selectList(query));
for(User u: masters){
masterMap.put(u.getNickname(),u);
}
//脱敏:将敏感信息从数组(集合\map)中移除
for(User u: masters){
u.setPassword("");
}
}
}
}
return masters;
}
@Override
public Map<String, User> getMasterMap() {
if(masterMap.isEmpty()){
getMasters();
}
return masterMap;
}
步骤3:
编写IQuestionService接口中发布问题的方法
步骤4:
在QuestionServiceImpl类中实现接口中定义的方法
业务的步骤大概为
// 获取当前登录用户信息(可以验证登录情况)
// 将该问题包含的标签拼接成字符串以","分割 以便添加tag_names列
// 构造Question对象
// 新增Question对象
// 处理新增的Question和对应Tag的关系
// 处理新增的Question和对应User(老师)的关系
代码如下
@Autowired
QuestionTagMapper questionTagMapper;
@Override
public void saveQuestion(QuestionVo questionVo) {
log.debug("收到问题数据{}",questionVo);
// 获取当前登录用户信息(可以验证登录情况)
String username=userService.currentUsername();
User user=userMapper.findUserByUsername(username);
// 将该问题包含的标签拼接成字符串以","分割 以便添加tag_names列
StringBuilder bud=new StringBuilder();
for(String tag : questionVo.getTagNames()){
bud.append(tag).append(",");
}
//删除最后一个","
bud.deleteCharAt(bud.length()-1);
String tagNames=bud.toString();
// 构造Question对象
Question question=new Question()
.setTitle(questionVo.getTitle())
.setContent(questionVo.getContent())
.setUserId(user.getId())
.setUserNickName(user.getUsername())
.setTagNames(tagNames)
.setCreatetime(LocalDateTime.now())
.setStatus(0)
.setPageViews(0)
.setPublicStatus(0)
.setDeleteStatus(0);
// 新增Question对象
int num=questionMapper.insert(question);
if(num!=1){
throw new ServiceException("服务器忙!");
}
log.debug("保存了对象:{}",question);
// 处理新增的Question和对应Tag的关系
Map<String,Tag> name2TagMap=tagService.getName2TagMap();
for(String tagName : questionVo.getTagNames()){
//根据本次循环的标签名称获得对应的标签对象
Tag tag=name2TagMap.get(tagName);
//构建QuestionTag实体类对象
QuestionTag questionTag=new QuestionTag()
.setQuestionId(question.getId())
.setTagId(tag.getId());
//执行新增
num=questionTagMapper.insert(questionTag);
if(num!=1){
throw new ServiceException("数据库忙!");
}
log.debug("新增了问题和标签的关系:{}",questionTag);
}
// 处理新增的Question和对应User(老师)的关系
}