欢迎访问笔者个人技术博客: http://rukihuang.xyz/
后端
- 9月初,接到单位领导的任务,要求实现一个在线教育平台,主要展示岗位教学视频,以及相关的作业指导书,并建议要配套一个后台系统方便文件的上传和管理。
- 作为后端开发,决定使用前后端分离,前端使用vue-admin-element,后端使用Springboot+MyBatis,数据库使用Mysql,容器使用tomcat,以及部署vue项目需要的nginx,jdk使用1.8
- 由于项目开发只有我一个人,开发过程中遇到了不少问题,但所幸都一一解决了,趁着摸鱼的空档,决定将遇到的问题和解决方法记录下来,方便之后查阅。
1. SpringBoot实现文件上传
- 参考的是这篇掘金的文章,很详细。SpringBoot实现文件上传
1.1 配置类 UploadConfig
- 限制上传文件大小,以及总的请求文件大小。
/**
* @author :RukiHuang
* @description:文件上传配置类
* 配置MaxFileSize等属性
* @date :2022/9/2 9:45
*/
@Configuration
public class UploadConfig {
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
//单个数据大小
factory.setMaxFileSize(DataSize.ofMegabytes(200));
//总上传文件的大小
factory.setMaxRequestSize(DataSize.ofGigabytes(10));
return factory.createMultipartConfig();
}
}
- 也可以通过在
application.properties
配置文件中指定
#文件上传
# 文件大小设置已在UploadConfig中配置,也可在配置文件中配置
#单个文件大小
spring.servlet.multipart.max-file-size=200MB
#总文件大小(允许存储文件的文件夹大小)
spring.servlet.multipart.max-request-size=10240MB
1.2 配置文件 application.properties
- 指定文件上传目标路径以及允许的文件类型
#文件上传的目标路径
file.upload.path=G:\\temp\\
#文件上传允许的类型
file.upload.allowType[0]=application/pdf
file.upload.allowType[1]=video/mp4
1.3 配置文件读取 UploadProperties
/**
* @author :RukiHuang
* @description:文件上传
* 上传路径
* 文件格式
* @date :2022/9/2 10:05
*/
@Component
@ConfigurationProperties(prefix = "file.upload")
public class UploadProperties {
private String path;
private List<String> allowTypeList;
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public List<String> getAllowType() {
return allowTypeList;
}
public void setAllowType(List<String> allowTypeList) {
this.allowTypeList = allowTypeList;
}
}
1.4 工具类
1.4.1 唯一ID生成器 IDUtils
- 生成唯一id
/**
* @author :RukiHuang
* @description:唯一ID生成器
* @date :2022/9/2 10:07
*/
public class IDUtils {
public static String generateUniqueId() {
return UUID.randomUUID().toString() + System.currentTimeMillis();
}
}
1.4.2 文件名替换工具 UploadUtils
- 替换原始文件名,避免文件名重复
/**
* @author :RukiHuang
* @description:文件名替换工具 避免文件名重复
* @date :2022/9/2 10:09
*/
public class UploadUtils {
public static String generateFileName(String oldName) {
String suffix = oldName.substring(oldName.lastIndexOf("."));
return IDUtils.generateUniqueId() + suffix;
}
}
1.5 web层 CoursewareController
/**
* @author :RukiHuang
* @description:课件的Controller
* @date :2022/9/1 13:37
*/
@CrossOrigin(origins = "*",maxAge = 3600)
@RequestMapping("/forum")
@RestController
public class CoursewareController {
private static Logger logger = LoggerFactory.getLogger(DocController.class);
@Autowired
CoursewareService coursewareService;
@Autowired
UploadService uploadService;
@RequestMapping("/coursewareUpload/uploadCourseware")
public ResponseResult uploadVideo(
@RequestParam("file") MultipartFile file
) {
String filename = null;
try {
filename = uploadService.uploadCourseware(file);
return ResponseResult.ok(filename, "课件上传成功");
} catch (IOException e) {
logger.error(e.getMessage());
return ResponseResult.failed(e.getMessage(),"课件上传失败");
}
}
@RequestMapping(value = "/coursewareUpload/submitCoursewareInfo", method = RequestMethod.POST)
public ResponseResult submitCoursewareInfo(
@RequestParam(name = "serverFileName")String serverFileName) {
try {
coursewareService.addCoursewareInfo(serverFileName);
return ResponseResult.ok("提交成功");
} catch (Exception e) {
logger.error(e.getMessage());
return ResponseResult.failed(e.getMessage(), "提交失败");
}
}
}
1.6 service层 UploadService CoursewareService
UploadService
/**
* @author :RukiHuang
* @description:上传service
* @date :2022/9/2 10:16
*/
@Service
public class UploadServiceImpl implements UploadService {
@Autowired
UploadProperties uploadProperties;
@Override
public String uploadCourseware(MultipartFile file) throws IOException {
System.out.println(file.getContentType());
if(!uploadProperties.getAllowType().get(0).equals(file.getContentType())) {
throw new IOException("课件上传类型错误");
}
String fileName = UploadUtils.generateFileName(file.getOriginalFilename());
File newFile = new File(uploadProperties.getPath()+"\\courseware\\" + fileName);//当前是在windows目录,部署到linux路径要修改为/courseware
file.transferTo(newFile);
System.out.println(newFile.getPath());
return fileName;
}
}
CoursewareService
/**
* @author :RukiHuang
* @description:课件service
* @date :2022/9/1 13:42
*/
@Service
public class CoursewareServiceImpl implements CoursewareService {
@Autowired
CoursewareDao coursewareDao;
@Override
public void addCoursewareInfo(String serverFileName) throws Exception {
String[] nameArray = serverFileName.split(" / ");
String coursewareName = nameArray[0];
String serverStorageName = nameArray[1];
String storagePath = "courseware/" + serverStorageName;
CoursewareInfoDto coursewareInfoDto = new CoursewareInfoDto(coursewareName, storagePath);
coursewareDao.addCoursewareInfo(coursewareInfoDto);
}
}
1.7 PostMan测试文件上传
- 第一步,头文件置空
- 第二步,请求体中选择上传文件
前端
2. Vue实现文件上传
2.1 单文件上传
2.1.1 业务逻辑
- 先对文件进行校验,判断文件大小以及类型是否符合要求。
- 将文件上传至服务器。
- 服务器返回新文件名和存储路径
- 将文件名称和服务器名称提交至服务器,并将存储路径传入至数据库。
- 组件使用的是vue-admin-template自带的iView,iView官方文档
2.1.2 代码实现
<template>
<div class="home-container">
<div class="upload-content" style="padding: 20px">
<Upload
:before-upload="beforeUpload"
type="drag"
action="http://localhost:8080/eduonline/forum/coursewareUpload/submitCoursewareInfo">
<div style="padding: 20px 0">
<Icon type="ios-cloud-upload" size="52" style="color: #3399ff"></Icon>
<p>点击或拖拽文件至此<br>文档限制pdf格式,视频限制mp4格式(大小不超过200MB)</p>
</div>
</Upload>
<div style="height: 10px">
<div v-if="file !== null">
上传文件: {{ file.name }}
<span style="padding-left: 10px">
<Button type="primary" @click="upload" :loading="loadingStatus" ghost size="small">
{{ loadingStatus ? '上传中' : '点击上传' }}
</Button>
</span>
</div>
</div>
</div>
<div class="searchParam" style="padding: 20px">
<Input v-model="serverFileName" placeholder="文件名称 / 服务器文件名称" style="width: 300px;" disabled/>
<span style="padding-left: 20px">
<Button type="primary" @click="submit">提交</Button>
</span>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'coursewareUpload',
data() {
return {
file: null,
filename: '',
loadingStatus: false,
serverFileName: '',
}
},
methods: {
beforeUpload(file) {
let name = file.name
let suffix = name.substring(name.lastIndexOf('.'))
let isNameLegal = true
let isLs2M = true
let isRight = true
isNameLegal = suffix === '.pdf'
isLs2M = file.size / 1024 / 1024 < 200
if (!isLs2M || !isNameLegal) {
this.loadingStatus = false
this.$Message.error('上传的文件大小不能超过200MB 且 格式为pdf!')
isRight = false
}
this.filename = file.name
this.file = file
// 一定要return false 不然会直接上传
return false
},
upload() {
let formData = new FormData()
formData.append('file', this.file)
this.loadingStatus = true
axios({
method: 'post',
url: 'forum/coursewareUpload/uploadCourseware',
headers: {
'content-type': 'multipart/form-data',
},
data: formData,
}).then(res => {
this.loadingStatus = false
this.serverFileName = this.filename + ' / ' + res.data.data
}, err => {
this.$Message.error('后台服务出问题,请联系技术人员')
})
},
submit() {
let formData = new FormData()
formData.append('position', this.position)
formData.append('type', this.type)
formData.append('serverFileName', this.serverFileName)
axios({
method: 'post',
url: 'forum/coursewareUpload/submitCoursewareInfo',
headers: {
'content-type': 'application/json',
},
data: formData,
}).then(res => {
this.$Message.success('提交成功')
this.position = ''
this.type = ''
this.serverFileName = ''
this.file = null
}, err => {
this.$Message.error('提交失败,请联系技术人员')
})
},
},
}
</script>
<style scoped>
</style>
2.2 一个页面多处地方需要文件上传
2.2.1 业务逻辑
- iview无法实现单个页面多处地方文件上传,
file
会被替换 - 使用原生的上传组件进行上传。
2.2.2 代码实现
<template>
<div class="btn-box">
<h3>作业指导书:</h3>
<input class="file-input" type="file" @change="getDocFile($event)" />
<Button type="primary" @click="uploadDoc" :loading="loadingStatus1">{{ loadingStatus1 ? '上传中' : '上传文件' }}</Button>
<Input v-model="docServerFileName" placeholder="文件名称 / 服务器文件名称" style="width: 300px; padding-left: 30px" disabled/>
<h3>SOC文档:</h3>
<input class="file-input" type="file" @change="getSOCFile($event)" />
<Button type="primary" @click="uploadSOC" :loading="loadingStatus4">{{ loadingStatus4 ? '上传中' : '上传文件' }}</Button>
<Input v-model="socServerFileName" placeholder="文件名称 / 服务器文件名称" style="width: 300px; padding-left: 30px" disabled/>
<h3>教学视频:</h3>
<input class="file-input" type="file" @change="getVidFile($event)" />
<Button type="primary" @click="uploadVideo" :loading="loadingStatus5">{{ loadingStatus5 ? '上传中' : '上传文件' }}</Button>
<Input v-model="vidServerFileName" placeholder="文件名称 / 服务器文件名称" style="width: 300px; padding-left: 30px" disabled/>
<h3>工段岗位:</h3>
<Select v-model="position" style="width:200px; margin: 20px" placeholder="请选择岗位" clearable>
<OptionGroup label="1 片叶">
<Option v-for="item in unit_pianye_posList" :value="item.value" :key="item.value">{{ item.label }}</Option>
</OptionGroup>
<OptionGroup label="2 烘丝">
<Option v-for="item in unit_hongsi_posList" :value="item.value" :key="item.value">{{ item.label }}</Option>
</OptionGroup>
<OptionGroup label="3 掺配加香">
<Option v-for="item in unit_canpeijiaxiang_posList" :value="item.value" :key="item.value">{{ item.label }}</Option>
</OptionGroup>
<OptionGroup label="4 膨胀">
<Option v-for="item in unit_pengzhang_posList" :value="item.value" :key="item.value">{{ item.label }}</Option>
</OptionGroup>
<OptionGroup label="5 梗丝">
<Option v-for="item in unit_gengsi_posList" :value="item.value" :key="item.value">{{ item.label }}</Option>
</OptionGroup>
<OptionGroup label="6 残烟间">
<Option v-for="item in unit_canyanjian_posList" :value="item.value" :key="item.value">{{ item.label }}</Option>
</OptionGroup>
</Select>
<h3>岗位职责:</h3>
<Input v-model="posResponsibility" type="textarea" :rows="4" placeholder="请输入岗位职责" style="padding: 20px"/>
<!-- <divider />-->
<Button type="primary" @click="submit" class="btn-submit">提交</Button>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'attendPoint',
data() {
return {
unit_pianye_posList: [],
unit_hongsi_posList: [],
unit_canpeijiaxiang_posList: [],
unit_pengzhang_posList: [],
unit_gengsi_posList: [],
unit_canyanjian_posList: [],
loadingStatus1: false,
loadingStatus2: false,
loadingStatus3: false,
loadingStatus4: false,
loadingStatus5: false,
position: '',
type: '',
file1: null,
file2: null,
file3: null,
file4: null,
file5: null,
filename1: '',
filename2: '',
filename3: '',
filename4: '',
filename5: '',
file: null,
filename: '',
docServerFileName: '',
docProcessServerFileName: '',
docIndexServerFileName: '',
socServerFileName: '',
vidServerFileName: '',
posResponsibility: '',
}
},
methods: {
getPositionList() {
let that = this
axios({
method: 'get',
url: 'position/getPositionList',
headers: {
'content-type': 'application/json',
},
}).then(res => {
that.unit_pianye_posList = res.data.data.unit_pianye_posList
that.unit_hongsi_posList = res.data.data.unit_hongsi_posList
that.unit_canpeijiaxiang_posList = res.data.data.unit_canpeijiaxiang_posList
that.unit_pengzhang_posList = res.data.data.unit_pengzhang_posList
that.unit_gengsi_posList = res.data.data.unit_gengsi_posList
that.unit_canyanjian_posList = res.data.data.unit_canyanjian_posList
}, err => {
this.$Message.error('后台服务出问题,请联系技术人员')
})
},
// 选取文件
getDocFile(event) {
this.file1 = event.target.files[0]
this.filename1 = this.file1.name
},
// 选取文件
getProcessFile(event) {
this.file2 = event.target.files[0]
this.filename2 = this.file2.name
},
// 选取文件
getIndexFile(event) {
this.file3 = event.target.files[0]
this.filename3 = this.file3.name
},
// 选取文件
getSOCFile(event) {
this.file4 = event.target.files[0]
this.filename4 = this.file4.name
},
// 选取文件
getVidFile(event) {
this.file5 = event.target.files[0]
this.filename5 = this.file5.name
},
// 上传文件 doc 作业指导书
uploadDoc() {
let uncheckedFile = this.file1
if (uncheckedFile == null) {
this.$Message.error('未选择文件!请先选择文件!')
return
}
let isRight = this.beforeUploadDoc(uncheckedFile)
if (!isRight) {
this.$Message.error('请重新上传文件')
return
}
let checkedFile = uncheckedFile
let formData = new FormData()
formData.append('file', checkedFile)
this.loadingStatus1 = true
axios({
method: 'post',
url: 'positionLearning/docUpload/uploadDoc',
headers: {
'content-type': 'multipart/form-data',
},
data: formData,
}).then(res => {
this.loadingStatus1 = false
this.docServerFileName = this.filename + ' / ' + res.data.data
}, err => {
this.$Message.error('后台服务出问题,请联系技术人员')
})
},
uploadSOC() {
let uncheckedFile = this.file4
if (uncheckedFile == null) {
this.$Message.error('未选择文件!请先选择文件!')
return
}
let isRight = this.beforeUploadDoc(uncheckedFile)
if (!isRight) {
this.$Message.error('请重新上传文件')
return
}
let checkedFile = uncheckedFile
let formData = new FormData()
formData.append('file', checkedFile)
this.loadingStatus4 = true
axios({
method: 'post',
url: 'positionLearning/docUpload/uploadDoc',
headers: {
'content-type': 'multipart/form-data',
},
data: formData,
}).then(res => {
this.loadingStatus4 = false
this.socServerFileName = this.filename + ' / ' + res.data.data
}, err => {
this.$Message.error('后台服务出问题,请联系技术人员')
})
},
uploadVideo() {
let uncheckedFile = this.file5
if (uncheckedFile == null) {
this.$Message.error('未选择文件!请先选择文件!')
return
}
let isRight = this.beforeUploadVid(uncheckedFile)
if (!isRight) {
this.$Message.error('请重新上传文件')
return
}
let formData = new FormData()
formData.append('file', this.file)
this.loadingStatus5 = true
axios({
method: 'post',
url: 'positionLearning/videoUpload/uploadVideo',
headers: {
'content-type': 'multipart/form-data',
},
data: formData,
}).then(res => {
this.loadingStatus5 = false
this.vidServerFileName = this.filename + ' / ' + res.data.data
}, err => {
this.$Message.error('后台服务出问题,请联系技术人员')
})
},
beforeUploadDoc(file) {
let name = file.name
let suffix = name.substring(name.lastIndexOf('.'))
let isNameLegal = true
let isLs2M = true
let isRight = true
isNameLegal = suffix === '.pdf'
isLs2M = file.size / 1024 / 1024 < 200
if (!isLs2M || !isNameLegal) {
this.loadingStatus = false
this.$Message.error('上传的文件大小不能超过200MB 且 格式为pdf!')
isRight = false
}
this.filename = file.name
this.file = file
// 一定要return false 不然会直接上传
return isRight
},
beforeUploadVid(file) {
let name = file.name
let suffix = name.substring(name.lastIndexOf('.'))
let isNameLegal = true
let isLs2M = true
let isRight = true
isNameLegal = suffix === '.mp4'
isLs2M = file.size / 1024 / 1024 < 200
if (!isLs2M || !isNameLegal) {
this.loadingStatus = false
this.$Message.error('上传的文件大小不能超过200MB 且 格式为mp4!')
isRight = false
}
this.filename = file.name
this.file = file
// 一定要return false 不然会直接上传
return isRight
},
submit() {
let formData = new FormData()
formData.append('position', this.position)
formData.append('docServerFileName', this.docServerFileName)
formData.append('socServerFileName', this.socServerFileName)
formData.append('vidServerFileName', this.vidServerFileName)
formData.append('posResponsibility', this.posResponsibility)
axios({
method: 'post',
url: 'page/submitPageInfo',
headers: {
'content-type': 'application/json',
},
data: formData,
}).then(res => {
// console.log('成功了')
this.$Message.success('提交成功')
this.position = ''
this.docServerFileName = ''
this.socServerFileName = ''
this.vidServerFileName = ''
this.posResponsibility = ''
this.file1 = null
this.file2 = null
this.file3 = null
this.filename1 = ''
this.filename2 = ''
this.filename3 = ''
}, err => {
console.log(err)
this.$Message.error('提交失败,请联系技术人员')
})
},
},
created() {
this.getPositionList()
},
}
</script>
<style scoped>
.btn-box {
padding: 20px;
}
input {
/*max-width: fit-content;*/
/*display: block;*/
margin: 20px;
}
.btn-submit {
vertical-align: middle;
display: flex;
align-items: center;
/*justify-content:center;*/
margin: 50px auto;
}
</style>