SpringBoot聚合项目:达内知道(九)-讲师登录、讲师任务列表显示、显示问题详情页、讲师回复功能
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查询用户的所有角色
业务逻辑层: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注解标记的控制器中的方法返回的字符串,会认为是要跳转的页面
注意:
在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查询讲师任务列表
其中,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;
运行测试类,输出结果如下:
2.2 讲师任务列表的业务逻辑层
讲师任务列表和学生问题列表的功能基本一致,都需要进行分页查询,业务逻辑层接口的声明需要支持分页。
在IQuestionService接口中添加方法:
//根据用户名(讲师)查询讲师任务列表
PageInfo<Question> getTeacherQuestions(String username, Integer pageNum,Integer pageSize);//需要利用用户名获得用户对象,pageNum和pageSize提供分页参数
在QuestionServiceImpl类中实现该方法:
//根据用户名(讲师)查询讲师问题列表的逻辑层实现
2.3 开发查询讲师任务列表的控制层
在QuestionController类中添加方法:
//根据登录用户分页查询讲师任务列表
启动服务,访问路径: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类中实现该方法:
3.3 编写显示问题详情的控制层代码
//根据问题id查看问题详情
/**
* SpringMvc支持路径占位符
* 如果请求为/v1/questions/150,会先寻找完全匹配的路径
* 如果没有,会将150这个位置赋值给下面控制器的{id}占位符
* 150这个值可以在控制器方法中获取并使用
* 实际上是一种参数传值的方式
*/
//要想获得占位符的值必须满足以下条件:
// 1.声明的参数前加@PathVariable注解
// 2.参数名称必须和{}中占位符一致
重启服务可以发送同步请求测试,测试路径为: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;
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
*/
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实现代码:
4.5 完善控制层调用业务逻辑层代码
重启服务,进行回答问题,观察数据库是否新增成功!
(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查询对应回答列表的逻辑层实现
4.8 编写查询回答列表的控制层
AnswerController编写代码:
// /v1/answers/question/157
// 按问题id查询所有回答的方法
//此处为了与v1/answers/157区分,前面加了question/,按questionid查回答
重启服务,发送同步请求/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;
}
})