续 学生问题发布功能
学生问题发布功能的收尾
控制层调用业务逻辑层
在上次课完成user(讲师)和问题关系的业务逻辑层代码之后
附:QuestionServiceImpl中saveQuestion方法的完整代码
@Autowired
QuestionTagMapper questionTagMapper;
@Autowired
UserQuestionMapper userQuestionMapper;
@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.getNickname())
.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);
}
下面我们就要开始进行控制层的调用了
我们已经写好的控制层的基本代码
重构这个方法调用业务逻辑层方法即可
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);
try {
questionService.saveQuestion(questionVo);
return R.ok("发布成功!");
}catch (ServiceException e){
log.error("发布失败",e);
return R.failed(e);
}
}
进行测试…
声明式事务
如果上面章节中新增过程中发生了异常
那么已经新增到数据库的数据不会删除,还没新增到数据库的数据就不会进入数据库了
就会造成数据的不完整
为了保证事务的完整性我们需要学习Spring的声明式事务
什么是事务
事务是数据库管理系统执行过程的一个最小逻辑单位
转账操作对数据库的影响分两步:
- 转出账户金额减少
- 转入账户金额增加
如果转出账户成功,转入账户失败,那么转出账户金额的减少操作应该撤销
即这两个操作要么都执行要么多不执行
事务的出现就是为了保证数据完整性的
面试常见题:
数据库事务拥有的四个特性
简称ACID
-
原子性(Atomicity):事务是执行数据库操作的最小逻辑,不可再分
-
一致性(Consistency):事务中对数据库操作的命令状态应该是一致的,
即要么都执行要么都不执行
-
隔离性(Isolation):一个事务的执行,不影响其他事务
-
持久性(Durability):事务操作如果提交,多数据库的影响是持久的
Spring的声明式事务
SpringBoot提供了对事务的支持
相较于我们自己控制JDBC或Mybatis来实现事务,明显SpringBoot实现的方式更简单
只需要在Service层方法上加@Transactional即可
加上这个注解的效果就是:
这个方法对数据库的操作要么都成功,要么都失败
只要发生异常,在发生异常之前的数据库操作会自动撤销!
@Transactional
public void saveXXX(){
//...
}
使用Spring异常增强来统一异常处理
我们现在编写的代码
在控制器方法中多数都需要使用try-catch结构来处理异常
而这个异常的处理又不能省略,每个catch代码又都是相似的,造成了代码冗余
如果想解决上面的问题,就可以使用Spring提供的异常增强处理功能
来统一处理控制层方法的异常
处理原理
我们可以定义一个异常增强类
这个异常增强类可以声明为自动处理控制层发生的异常,这样我们就不必每个方法都处理了
在controller包中新建类ExceptionControllerAdvice
代码如下
//@RestControllerAdvice表示对控制器方法的异常增强处理
@RestControllerAdvice
@Slf4j
public class ExceptionControllerAdvice {
//@ExceptionHandler表示这个方法时用来出处理异常的
@ExceptionHandler
public R handlerServiceException(ServiceException e){
log.error("业务异常",e);
return R.failed(e);
}
@ExceptionHandler
public R handlerException(Exception e) {
log.error("其它异常", e);
return R.failed(e);
}
}
说明
- @RestControllerAdvice表示对控制器方法的异常增强处理
- @ExceptionHandler表示这个方法是用来出处理异常的
控制器发生异常时,会自动匹配合适的异常类型,运行方法
文件上传(上载)
什么是文件上传
在Http协议的标准上
实现将客户端本地文件复制到服务器硬盘中的过程
http协议规定了一些上传文件时的标准
-
表单提交的方式必须是post
-
表单提交的编码方式必须修改为 multipart/form-data(二进制)
-
要上传的文件使用<input type=“file” name=“xxx”> 来表示
-
HTTP请求头中必须包含 Content-type: multipart/form-data, boundary=AaB03x;
4中的描述了解即可
-
允许上传多个文件
文件上传流程
编写页面代码
只是为了测试,所以我们编写在static文件夹中即可
upload.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="post" enctype="multipart/form-data"
action="/upload/file">
<input type="file" name="imageFile"><br>
<input type="submit">
</form>
</body>
</html>
提交到SystemController控制器中的方法代码如下
需要保证f:盘中有upload文件夹
//接收表单上传的文件
@PostMapping("/upload/file")
public R<String> upload(MultipartFile imageFile) throws IOException {
//获得文件名
String name=imageFile.getOriginalFilename();
File f=new File("F:/upload/"+name);
imageFile.transferTo(f);
return R.ok("上载完成!");
}
其中MultipartFile imageFile参数imageFile的名字必须和表单中file控件的name属性一致
getOriginalFilename获得原始文件名
transferTo将这个文件写入到指定的file对象中
Ajax上传文件
我们在实际的开发中,也是使用ajax提交的情况较多
所以我们需要学习ajax如何上传文件
重构upload.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="bower_components/jquery/dist/jquery.min.js"></script>
</head>
<body>
<form id="demoForm">
<input type="file" id="imageFile" name="imageFile"><br>
<input type="submit">
</form>
</body>
<script src="js/utils.js"></script>
<script>
//在页面加载完毕之后运行!
$(function () {
//先绑定一个方法,当用户点击提交时,验证是否选中了图片
$("#demoForm").submit(function(){
//获得用户选择的文件(js是获得用户选择的文件数组)
let files=document.getElementById("imageFile").files;
//判断文件数组是不是长度>0
if(files.length>0){
//有文件,做上传
let file=files[0];//将文件从数组中取出
console.log(file);
//调用专门上传文件的方法
uploadImage(file);
}else{
//没文件,直接结束
alert("请选择文件")
}
return false;//阻止表单提交
})
//完成文件上传的方法
function uploadImage(file){
//构建表单
let form=new FormData();
form.append("imageFile",file);
$.ajax({
url:"/upload/file",
method:"post",
data:form,//发送的是我们构建的表单中的数据
//下面有两个特殊参数,需要在文件上传时设置
contentType:false,
processData:false,
success:function(r){
if(r.code==OK){
console.log(r);
alert(r.message);
}else{
alert(r.message);
}
}
});
}
})
</script>
</html>
无需修改控制器代码
直接提交文件测试,成功即可
完成稻草项目中富文本编辑器中文件的上传
搭建一个静态资源服务器
使用SpringBoot聚合项目的子项目完成搭建任务即可
创建straw-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>straw</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>straw-resource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>straw-resource</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
创建一个文件夹作为图片服务器的资源路径
F:/resource
在这个文件夹中适当保存若干测试图片
在straw-resource项目的application.properties文件中配置如下
server.port=8899
spring.web.resources.static-locations=file:F:/resource
启动服务,输入url
http://localhost:8899/1.jpg
能够看到保存的图片即可,其中1.jpg是我们创建的resource文件夹中真是存在的文件名
将图片上传到静态资源服务器
我们需要将静态资源服务器的ip地址和端口号以及存放文件的路径名保存到当前配置文件中
straw-portal项目的application.properties配置添加代码如下
straw.resource.path=file:F:/resource
straw.resource.host=http://localhost:8899
重构SystemController中上传文件的代码
//下面两个属性值来自application.properties配置文件
@Value("${straw.resource.path}")
private File resourcePath;
@Value("${straw.resource.host}")
private String resourceHost;
//接收表单上传的文件
@PostMapping("/upload/file")
public R<String> upload(MultipartFile imageFile) throws IOException {
/*
我们需要保证任何用户上传的文件的文件名都不能重复
我们为了尽量避免文件名的重复,采用以下策略
1.将原有文件名修改为使用UUID生成的字符串
2.不同的日期创建不同的文件夹
3.保留文件的扩展名,还能方便文件识别
*/
//按照当前日期创建文件夹
String path= DateTimeFormatter.ofPattern("yyyy/MM/dd")
.format(LocalDate.now());
//path="2020/12/16"
File folder=new File(resourcePath,path);
//folder->F:/resource/2020/12/16
folder.mkdirs();//创建一串文件夹带s的!!!!
log.debug("上传的文件夹为:{}",folder.getAbsolutePath());
//按照上传文件的原始文件名,保留扩展名xx.xx.jpg
// 012345678
String fileName=imageFile.getOriginalFilename();
String ext=fileName.substring(fileName.lastIndexOf("."));
//使用UUID生成文件名
String name= UUID.randomUUID().toString()+ext;
log.debug("生成的文件名:{}",name);
//F:/resource/2020/12/16/uuid.jpg
File file=new File(folder,name);
//向硬盘写入文件
imageFile.transferTo(file);
//直接返回路径方便调用测试
String url=resourceHost+"/"+path+"/"+name;
log.debug("访问这个文件的路径为:{}",url);
return R.ok(url);
}
如果希望我们上传文件之后能立即显示在页面上
需要在页面上定一个img标签
并在ajax接收到上传图片的url后,将这个ur赋值给img标签的src属性
upload.html代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="bower_components/jquery/dist/jquery.min.js"></script>
</head>
<body>
<form id="demoForm">
<input type="file" id="imageFile" name="imageFile"><br>
<input type="submit">
</form>
<img src="" id="image">
</body>
<script src="js/utils.js"></script>
<script>
//在页面加载完毕之后运行!
$(function () {
//先绑定一个方法,当用户点击提交时,验证是否选中了图片
$("#demoForm").submit(function(){
//获得用户选择的文件(js是获得用户选择的文件数组)
let files=document.getElementById("imageFile").files;
//判断文件数组是不是长度>0
if(files.length>0){
//有文件,做上传
let file=files[0];//将文件从数组中取出
console.log(file);
//调用专门上传文件的方法
uploadImage(file);
}else{
//没文件,直接结束
alert("请选择文件")
}
return false;//阻止表单提交
})
//完成文件上传的方法
function uploadImage(file){
//构建表单
let form=new FormData();
form.append("imageFile",file);
$.ajax({
url:"/upload/file",
method:"post",
data:form,//发送的是我们构建的表单中的数据
//下面有两个特殊参数,需要在文件上传时设置
contentType:false,
processData:false,
success:function(r){
if(r.code==OK){
console.log(r);
//alert(r.message);
$("#image").attr("src",r.message);
}else{
alert(r.message);
}
}
});
}
})
</script>
</html>
将富文本编辑器中用户选择的文件上传
上传流程
重构create.html中的代码
代码如下
<!--底部-->
<footer class="container-fluid bg-light mt-2 py-3">
<p class="text-center font-weight-light">达内教育-Java教研部 版权所有<br><a href="http://tedu.cn" rel="nofollow" target="_blank">京ICP备16053980号-3</a>
</p>
</footer>
<script src="../plugins/summernote/summernote.min.js"></script>
<script src="../plugins/summernote/summernote-zh-CN.min.js"></script>
<script>
$(document).ready(function() {
$('#summernote').summernote({
height: 300,
lang: 'zh-CN',
placeholder: '请输入问题的详细描述...',
callbacks:{
//在执行指定操作后自动调用下面的方法
//onImageUpload方法就会在用户选中图片之后立即运行
onImageUpload:function(files) {
//参数是一个file数组取出第一个,因为我们只会选中一个
let file =files[0];
//构建表单
let form=new FormData();
form.append("imageFile",file);
$.ajax({
url:"/upload/file",
method:"post",
data:form,//发送的是我们构建的表单中的数据
//下面有两个特殊参数,需要在文件上传时设置
cache:false,
contentType:false,
processData:false,
success:function(r){
if(r.code==OK){
console.log(r);
//将刚刚上传成功的图片显示在summernote富文本编辑器中
var img=new Image();//实例化了一个img标签
img.src=r.message;//将img标签的src属性赋值为刚上传的图片
//summernote方法中提供了插入标签的功能
//支持使用"insertNode"表示要向富文本编辑器中添加标签内容
$("#summernote").summernote(
"insertNode",img
)
}else{
alert(r.message);
}
}
});
}
}
});
$('select').select2({placeholder:'请选择...'});
});
</script>
</body>
<script src="../js/utils.js"></script>
<script src="../js/tags_nav.js"></script>
<script src="../js/createQuestion.js"></script>
</html>
显示问题状态
index.html中通过v-show来控制span的显示或隐藏即可
<div class="col-md-12 col-lg-2">
<!-- v-show="[boolean类型表达式]" -->
<!-- 只要boolean类型表达式值为真,这个元素就会显示
反之就不会显示-->
<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>
随笔
如果电脑系统是linux
资源路径推荐
file:/home/soft01/resource