系列文章目录
- 系统功能演示——基于SpringBoot和Vue的后台管理系统项目系列博客(一)
- Vue2安装并集成ElementUI——基于SpringBoot和Vue的后台管理系统项目系列博客(二)
- Vue2前端主体框架搭建——基于SpringBoot和Vue的后台管理系统项目系列博客(三)
- SpringBoot后端初始框架搭建——基于SpringBoot和Vue的后台管理系统项目系列博客(四)
- SpringBoot集成Mybatis——基于SpringBoot和Vue的后台管理系统项目系列博客(五)
- SpringBoot实现增删改查——基于SpringBoot和Vue的后台管理系统项目系列博客(六)
- SpringBoot实现分页查询——基于SpringBoot和Vue的后台管理系统项目系列博客(七)
- SpringBoot实现集成Mybatis-Plus和SwaggerUI——基于SpringBoot和Vue的后台管理系统项目系列博客(八)
- Vue实现增删改查——基于SpringBoot和Vue的后台管理系统项目系列博客(九)
- SpringBoot实现代码生成器——基于SpringBoot和Vue的后台管理系统项目系列博客(十)
- Vue使用路由——基于SpringBoot和Vue的后台管理系统项目系列博客(十一)
- SpringBoot和Vue实现导入导出——基于SpringBoot和Vue的后台管理系统项目系列博客(十二)
- SpringBoot和Vue实现用户登录注册与异常处理——基于SpringBoot和Vue的后台管理系统项目系列博客(十三)
- SpringBoot和Vue实现用户个人信息展示与保存与集成JWT——基于SpringBoot和Vue的后台管理系统项目系列博客(十四)
- SpringBoot和Vue实现文件上传与下载——基于SpringBoot和Vue的后台管理系统项目系列博客(十五)
- SpringBoot和Vue整合ECharts——基于SpringBoot和Vue的后台管理系统项目系列博客(十六)
- SpringBoot和Vue实现权限菜单功能——基于SpringBoot和Vue的后台管理系统项目系列博客(十七)
- SpringBoot实现1对1、1对多、多对多关联查询——基于SpringBoot和Vue的后台管理系统项目系列博客(十八)
- 用户前台页面设计与实现——基于SpringBoot和Vue的后台管理系统项目系列博客(十九)
- SpringBoot集成Redis实现缓存——基于SpringBoot和Vue的后台管理系统项目系列博客(二十)
- SpringBoot和Vue集成高德地图——基于SpringBoot和Vue的后台管理系统项目系列博客(二十一)
- SpringBoot和Vue集成视频播放组件——基于SpringBoot和Vue的后台管理系统项目系列博客(二十二)
- SpringBoot和Vue集成Markdown和多级评论——基于SpringBoot和Vue的后台管理系统项目系列博客(二十三)
项目资源下载
文章目录
前言
今天的主要内容包括:实现文件上传与下载功能准备工作、文件上传接口的实现、文件下载接口的实现、使用md5校验唯一文件、文件上传和下载的后端功能实现、文件上传和下载的前端功能实现、用户上传头像功能实现等。今天的内容比较多,也有一定难度,但是只要一步一步跟着我做,就不会有太大问题。下面就开始今天的学习!
一、实现文件上传与下载功能准备工作
- 为了保存文件,按照如下配置新建表。其中id为主键且自动递增。is_delete默认为0表示未删除,如果为1表示已删除。enable默认为1表示可用,如果为0表示不可用
- 表名为sys_file
- 在application.yml中配置一下文件保存路径,代码运行时会自动创建文件夹。读者需要注意改成自己对应的目录
二、文件上传接口的实现
- 设置拦截器将file请求都放行,方便后续测试
- 然后在entity中新建Files.java实体类
- 在Files.java实体类中输入如下内容
- 然后在mapper中新建FileMapper.java接口
- 在FileMapper.java接口中输入如下内容
- 在此目录下新建FileController.java
- 首先在FileController.java中加入如下注解,注明文件上传接口的路由(请求地址)
- 然后在FileController.java中引用如下内容包括刚才的接口和文件存储地址
- 然后在FileController.java加入如下函数接口,功能是实现文件上传
/**
* 文件上传接口
*
* @param file 前端传递过来的文件
* @return 返回文件url
* @throws IOException 异常处理
*/
@PostMapping("/upload")
public String upload(@RequestParam MultipartFile file) throws IOException {
String originalFilename = file.getOriginalFilename();
String type = FileUtil.extName(originalFilename);
long size = file.getSize();
// 存储到磁盘
File uploadParentFile = new File(fileUploadPath);
// 判断配置的文件目录是否存在,若不存在则创建一个新的文件目录
if (!uploadParentFile.exists()) {
uploadParentFile.mkdirs();
}
// 定义一个文件唯一的标识码
String uuid = IdUtil.fastSimpleUUID();
String fileUUID = uuid + StrUtil.DOT + type;
File uploadFile = new File(fileUploadPath + fileUUID);
// 把获取到的文件存储到磁盘目录
file.transferTo(uploadFile);
String url = "http://localhost:9090/file/" + fileUUID;
// 存储到数据库
Files saveFile = new Files();
saveFile.setName(originalFilename);
saveFile.setType(type);
saveFile.setSize(size / 1024);
saveFile.setUrl(url);
fileMapper.insert(saveFile);
return url;
}
- 然后使用Postman进行测试
- 发现文件已经成功上传到后端服务器
- 并且数据库中已经存在此文件数据
- 为了存储到数据库中的文件size为kb表示,在FileController.java的文件上传函数接口中的size除以1024再保存
- 再测试一下发现已经是用kb表示了
三、文件下载接口的实现
- 在 FileController.java中加入如下函数接口,目的是实现文件下载功能
/**
* 文件下载接口
*
* @param fileUUID 文件唯一标识符
* @param response 请求响应
* @throws IOException 异常处理
*/
@GetMapping("/{fileUUID}")
public void download(@PathVariable String fileUUID, HttpServletResponse response) throws IOException {
// 根据文件的唯一标识码获取文件
File uploadFile = new File(fileUploadPath + fileUUID);
// 设置输出流的格式
ServletOutputStream os = response.getOutputStream();
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileUUID, "UTF-8"));
response.setContentType("application/octet-stream");
// 读取文件的字节流
os.write(FileUtil.readBytes(uploadFile));
os.flush();
os.close();
}
- 然后我们将刚刚文件上传返回的文件url复制到浏览器请求
- 然后发现可以实现文件的下载功能
四、使用md5校验唯一文件
- 在数据库中的sys_file表中加入如下字段,按照我的配置就行
- 在Files.java中加入md5属性
- 然后在FileController.java中加入如下代码获取数据库中的md5
- 然后将FileController.java中的文件上传函数修改为如下内容
/**
* 文件上传接口
*
* @param file 前端传递过来的文件
* @return 返回文件url
* @throws IOException 异常处理
*/
@PostMapping("/upload")
public String upload(@RequestParam MultipartFile file) throws IOException {
String originalFilename = file.getOriginalFilename();
String type = FileUtil.extName(originalFilename);
long size = file.getSize();
// 定义一个文件唯一的标识码
String uuid = IdUtil.fastSimpleUUID();
String fileUUID = uuid + StrUtil.DOT + type;
File uploadFile = new File(fileUploadPath + fileUUID);
// 判断配置的文件目录是否存在,若不存在则创建一个新的文件目录
File parentFile = uploadFile.getParentFile();
if (!parentFile.exists()) {
parentFile.mkdirs();
}
String url;
// 获取文件的md5,通过对比md5避免重复上传相同内容的文件
String md5 = SecureUtil.md5(file.getInputStream());
// 从数据库查询是否存在相同的记录
Files dbFiles = getFileByMd5(md5);
if (dbFiles != null) { // 文件已存在
url = dbFiles.getUrl();
} else {
// 上传文件到磁盘
file.transferTo(uploadFile);
// 数据库若不存在重复文件,则不删除刚才上传的文件
url = "http://localhost:9090/file/" + fileUUID;
}
// 存储数据库
Files saveFile = new Files();
saveFile.setName(originalFilename);
saveFile.setType(type);
saveFile.setSize(size / 1024);
saveFile.setUrl(url);
saveFile.setMd5(md5);
fileMapper.insert(saveFile);
return url;
}
- 然后我们使用Postman测试一下,分两次上传,上传同一个文件,但是不同名字
- 然后我们发现对于相同文件只上传了一个,并且数据库中有两个数据,md5值一样,但是文件名字不一样,这样就实现了我们的功能
五、文件上传和下载的后端功能实现
- 在FileController.java中加入分页查询接口
- 在FileController.java中加入更新文件信息接口
- 在FileController.java中加入按照文件id删除接口
- 在FileController.java中加入按照id批量删除文件删除接口
六、文件上传和下载的前端功能实现
- 在views中新建File.vue
- 在File.vue中加入如下代码
<template>
<div>
<div style="margin: 10px 0">
<el-input style="width: 200px" placeholder="请输入名称" suffix-icon="el-icon-search" v-model="name"></el-input>
<el-button class="ml-5" type="primary" @click="load">搜索</el-button>
<el-button type="warning" @click="reset">重置</el-button>
</div>
<div style="margin: 10px 0">
<el-upload action="http://localhost:9090/file/upload" :show-file-list="false" :on-success="handleFileUploadSuccess" style="display: inline-block">
<el-button type="primary" class="ml-5">上传文件 <i class="el-icon-top"></i></el-button>
</el-upload>
<el-popconfirm
class="ml-5"
confirm-button-text='确定'
cancel-button-text='我再想想'
icon="el-icon-info"
icon-color="red"
title="您确定批量删除这些数据吗?"
@confirm="delBatch"
>
<el-button type="danger" slot="reference">批量删除 <i class="el-icon-remove-outline"></i></el-button>
</el-popconfirm>
</div>
<el-table :data="tableData" border stripe :header-cell-class-name="'headerBg'" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="文件名称"></el-table-column>
<el-table-column prop="type" label="文件类型"></el-table-column>
<el-table-column prop="size" label="文件大小(kb)"></el-table-column>
<el-table-column label="下载">
<template slot-scope="scope">
<el-button type="primary" @click="download(scope.row.url)">下载</el-button>
</template>
</el-table-column>
<el-table-column label="启用">
<template slot-scope="scope">
<el-switch v-model="scope.row.enable" active-color="#13ce66" inactive-color="#ccc" @change="changeEnable(scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<el-popconfirm
class="ml-5"
confirm-button-text='确定'
cancel-button-text='我再想想'
icon="el-icon-info"
icon-color="red"
title="您确定删除吗?"
@confirm="del(scope.row.id)">
<el-button type="danger" slot="reference">删除 <i class="el-icon-remove-outline"></i></el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div style="padding: 10px 0">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[2, 5, 10, 20]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</div>
</template>
<script>
export default {
name: "File",
data() {
return {
tableData: [],
name: '',
multipleSelection: [],
pageNum: 1,
pageSize: 10,
total: 0
}
},
created() {
this.load()
},
methods: {
load() {
this.request.get("/file/page", {
params: {
pageNum: this.pageNum,
pageSize: this.pageSize,
name: this.name,
}
}).then(res => {
this.tableData = res.data.records
this.total = res.data.total
})
},
changeEnable(row) {
this.request.post("/file/update", row).then(res => {
if (res.code === '200') {
this.$message.success("操作成功")
}
})
},
del(id) {
this.request.delete("/file/" + id).then(res => {
if (res.code === '200') {
this.$message.success("删除成功")
this.load()
} else {
this.$message.error("删除失败")
}
})
},
handleSelectionChange(val) {
console.log(val)
this.multipleSelection = val
},
delBatch() {
let ids = this.multipleSelection.map(v => v.id) // [{}, {}, {}] => [1,2,3]
this.request.post("/file/del/batch", ids).then(res => {
if (res.code === '200') {
this.$message.success("批量删除成功")
this.load()
} else {
this.$message.error("批量删除失败")
}
})
},
reset() {
this.name = ""
this.load()
},
handleSizeChange(pageSize) {
console.log(pageSize)
this.pageSize = pageSize
this.load()
},
handleCurrentChange(pageNum) {
console.log(pageNum)
this.pageNum = pageNum
this.load()
},
handleFileUploadSuccess(res) {
console.log(res)
this.load()
},
download(url) {
window.open(url)
}
}
}
</script>
<style scoped>
</style>
- 在index.js中加入文件管理界面的路由信息
- 在Aside.vue中加入文件管理界面的侧边栏信息
- 然后测试一下相关功能:搜索、重置、上传文件、批量删除、下载、删除、启用、翻页。发现所有功能都可以正常使用,也就是说我们的文件管理模块已经完成了
七、用户上传头像功能实现
- 首先将Person.vue中的内容全部修改为如下内容
<template>
<el-card style="width: 500px;">
<el-form label-width="80px" size="small">
<el-upload
class="avatar-uploader"
:action="'http://localhost:9090/file/upload'"
:show-file-list="false"
:on-success="handleAvatarSuccess"
>
<img v-if="form.avatar" :src="form.avatar" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
<el-form-item label="用户名">
<el-input v-model="form.username" disabled autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="昵称">
<el-input v-model="form.nickname" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.email" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="电话">
<el-input v-model="form.phone" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="地址">
<el-input v-model="form.address" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="头像链接">
<el-input type="textarea" v-model="form.avatar" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="save">保 存</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
<script>
export default {
name: "Person",
data() {
return {
form: {},
user: localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : {}
}
},
created() {
this.getUser().then(res => {
console.log(res)
this.form = res
})
},
methods: {
async getUser() {
return (await this.request.get("/user/username/" + this.user.username)).data
},
save() {
this.request.post("/user", this.form).then(res => {
if (res) {
this.$message.success("保存成功")
// 触发父级更新User的方法
this.$emit("refreshUser")
// 更新浏览器存储的用户信息
this.getUser().then(res => {
res.token = JSON.parse(localStorage.getItem("user")).token
localStorage.setItem("user", JSON.stringify(res))
})
} else {
this.$message.error("保存失败")
}
})
},
handleAvatarSuccess(res) {
this.form.avatar = res
}
}
}
</script>
<style scoped>
.avatar-uploader {
text-align: center;
padding-bottom: 10px;
}
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 138px;
height: 138px;
line-height: 138px;
text-align: center;
}
.avatar {
width: 138px;
height: 138px;
display: block;
}
</style>
- 然后将Manage.vue中的内容全部修改为如下内容
<template>
<el-container style="min-height: 100vh">
<el-aside :width="sideWidth + 'px'" style="box-shadow: 2px 0 6px rgb(0 21 41 / 35%);">
<Aside :isCollapse="isCollapse" :logoTextShow="logoTextShow"/>
</el-aside>
<el-container>
<el-header style="border-bottom: 1px solid #ccc;">
<Header :collapseBtnClass="collapseBtnClass" @asideCollapse="collapse" :user="user"/>
</el-header>
<el-main>
<!-- 表示当前页面的子路由会在 <router-view /> 里面展示-->
<router-view @refreshUser="getUser"/>
</el-main>
</el-container>
</el-container>
</template>
<script>
import Aside from "@/components/Aside";
import Header from "@/components/Header";
export default {
name: 'Home',
data() {
return {
collapseBtnClass: 'el-icon-s-fold',
isCollapse: false,
sideWidth: 200,
logoTextShow: true,
user: {}
}
},
components: {
Aside,
Header
},
create() {
// 从后台获取最新的User数据
this.getUser()
},
methods: {
collapse() { // 点击收缩按钮触发
this.isCollapse = !this.isCollapse
if (this.isCollapse) { // 收缩
this.sideWidth = 64
this.collapseBtnClass = 'el-icon-s-unfold'
this.logoTextShow = false
} else { // 展开
this.sideWidth = 200
this.collapseBtnClass = 'el-icon-s-fold'
this.logoTextShow = true
}
},
getUser() {
let username = localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")).username : ""
if (username) {
// 从后台获取User数据
this.request.get("/user/username/" + username).then(res => {
// 重新赋值后台的最新User数据
this.user = res.data
})
}
}
}
}
</script>
- 最后将Header.vue的内容全部修改为如下内容
<template>
<div style="line-height: 60px; display: flex">
<div style="flex: 1;">
<span :class="collapseBtnClass" style="cursor: pointer; font-size: 18px" @click="collapse"></span>
<el-breadcrumb separator="/" style="display: inline-block; margin-left: 10px">
<el-breadcrumb-item :to="'/'">首页</el-breadcrumb-item>
<el-breadcrumb-item>{{ currentPathName }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<el-dropdown style="width: 150px; cursor: pointer; text-align: right">
<div style="display: inline-block">
<img :src="user.avatar" referrerPolicy="no-referrer"
style="width: 30px; border-radius: 50%; position: relative; top: 10px; right: 5px">
<span>{{ user.nickname }}</span><i class="el-icon-arrow-down" style="margin-left: 5px"></i>
</div>
<el-dropdown-menu slot="dropdown" style="width: 100px; text-align: center">
<el-dropdown-item style="font-size: 14px; padding: 5px 0">
<router-link to="/password">修改密码</router-link>
</el-dropdown-item>
<el-dropdown-item style="font-size: 14px; padding: 5px 0">
<router-link to="/person">
个人信息
</router-link>
</el-dropdown-item>
<el-dropdown-item style="font-size: 14px; padding: 5px 0">
<span style="text-decoration: none" @click="logout">退出</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script>
export default {
name: "Header",
props: {
collapseBtnClass: String,
user: Object
},
computed: {
currentPathName() {
return this.$store.state.currentPathName; //需要监听的数据
}
},
data() {
return {
}
},
methods: {
collapse() {
this.$emit("asideCollapse")
},
logout() {
this.$router.push("/login")
localStorage.removeItem("user")
this.$message.success("退出成功")
}
}
}
</script>
<style scoped>
</style>
- 然后来到前端测试,发现可以成功实现用户的头像上传功能
总结
以上就是今天学习的全部内容,下篇博文给会给大家带来关于SpringBoot和Vue整合ECharts的内容。明天见!