1.在controller文件下创建一个FileController类( 01:25 )
2.创建sys_file数据表,id列勾选自增( 03:30 )
3.接着回到FileController写代码 ( 06:19 )
打开application.yml,增加files以及下面的代码( 10:18 )
注意:files文件由代码自动创建
继续编辑FileController类文件( 11:07 )
package com.SpringBoot.demo.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
/**
* 文件上传相关接口
*/
@RestController
@RequestMapping("/file")
public class FileController {
@Value("${files.upload.path}") //将application.yml文件中的文件位置赋予到fileUploadPath //注意导包:.beans.factory.annotation.Value;
private String fileUploadPath;
/**
* 文件上传接口
* @param file 前端传过来的文件
* @return
* @throws IOException
*/
@PostMapping("/upload")
public String upload(@RequestParam MultipartFile file) throws IOException {
String OriginalFilename = file.getOriginalFilename(); //获取( 原始名称 )
String type = FileUtil.extName(OriginalFilename); //获取( 文件类型 ) //注意FileUtil.extName是String
long size = file.getSize(); //获取( 文件大小 )
// 先存储到磁盘
File uploadParentFile = new File(fileUploadPath); //注意导报: File(java.io)
// 判断配置的文件目录是否存在,若不存在则创建一个新的文件目录
if(!uploadParentFile.exists()) {
uploadParentFile.mkdirs();
}
// 定义一个文件唯一的标识位
String uuid = IdUtil.fastSimpleUUID();
File uploadFile = new File(fileUploadPath + uuid);
// 把前端获取到的文件存储到磁盘目录
file.transferTo(uploadFile); //把文件传到磁盘上去 //注意(1)file.transferTo()是File dest (2)transferTo报红Add一个异常数据就好了
// 再存储到数据库
return "";
}
}
然后打开postman测试,由于SpringBoot拦截器没有放行files所以出现无token
在SpringBoot拦截器InterceptorConfig类文件中放行files,重新运行后台
我们可以看见电脑C盘有files文件生成,里面有我们的照片
通过拼接的方式获取图片的后缀,然后重新运行后台,再使用Postman测试
package com.SpringBoot.demo.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
/**
* 文件上传相关接口
*/
@RestController
@RequestMapping("/file")
public class FileController {
@Value("${files.upload.path}") //将application.yml文件中的文件位置赋予到fileUploadPath //注意导包:.beans.factory.annotation.Value;
private String fileUploadPath;
/**
* 文件上传接口
* @param file 前端传过来的文件
* @return
* @throws IOException
*/
@PostMapping("/upload")
public String upload(@RequestParam MultipartFile file) throws IOException {
String originalFilename = file.getOriginalFilename(); //获取( 原始名称 )
String type = FileUtil.extName(originalFilename); //获取( 文件类型 ) //注意FileUtil.extName是String
long size = file.getSize(); //获取( 文件大小 )
// 先存储到磁盘
File uploadParentFile = new File(fileUploadPath); //注意导报: File(java.io)
// 判断配置的文件目录是否存在,若不存在则创建一个新的文件目录
if(!uploadParentFile.exists()) {
uploadParentFile.mkdirs();
}
// 定义一个文件唯一的标识位
String uuid = IdUtil.fastSimpleUUID();
File uploadFile = new File(fileUploadPath + uuid + StrUtil.DOT + type); //StrUtil.DOT( 文件名) + type( png )
// 把前端获取到的文件存储到磁盘目录
file.transferTo(uploadFile); //把文件传到磁盘上去 //注意(1)file.transferTo()是File dest (2)transferTo报红Add一个异常数据就好了
// 再存储到数据库
return "";
}
}
4.在entity包下创建File实体类(24:50)
注意:数据库中tinyint在实体类中就是Boolean类型
package com.SpringBoot.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* 数据库中 int 在实体类中就是 Integer
* 数据库中 varchar 在实体类中就是 String
* 数据库中 bigint 在实体类中就是 Long
* 数据库中 tinyint 在实体类中就是 Boolean 类型
*/
@Data
@TableName("sys_file")
public class File {
@TableId(type = IdType.AUTO)
private Integer id;
private String name;
private String type;
private Long size;
private String url;
private Boolean isDelete;
private Boolean enable;
}
5.在mapper文件中创建接口FileMapper.java(26:50)
在FileController中引用它
在FileController类的上传接口新增几行代码
再修改几行代码
5.在FileMapper.java中写一个下载download接口(),重新启动后台
打开postman测试一下
将地址复制到浏览器打开即可
6.重复的图片排除掉(44:02)
在FileController类文件中加上获取md5的代码
在Navicat 15数据库图形化界面中新增md5字段,然后给is_delete、enable字段默认为0
注:暂时不给md5加索引
在Files实体类中加上私有变量md5
在FileController中写一个存一个md5的代码(如果文件中有相同的图片文件不上传图片)
在FileController文件接口下面写一个方法查询MD5文件getFileByMd5
声明:通过md5查询出多条记录
代码优化一下 //将存储到磁盘 和 //判断配置的文件下方的代码进行优化,优化后的代码
package com.SpringBoot.demo.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.SpringBoot.demo.entity.Files;
import com.SpringBoot.demo.mapper.FileMapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
/**
* 文件上传相关接口
*/
@RestController
@RequestMapping("/file")
public class FileController {
@Value("${files.upload.path}") //将application.yml文件中的文件位置赋予到fileUploadPath //注意导包:.beans.factory.annotation.Value;
private String fileUploadPath;
@Resource
private FileMapper fileMapper;
/**
* 【1】文件上传接口
* @param file 前端传过来的文件
* @return
* @throws IOException
*/
@PostMapping("/upload")
public String upload(@RequestParam MultipartFile file) throws IOException {
String originalFilename = file.getOriginalFilename(); //获取( 原始名称 )
String type = FileUtil.extName(originalFilename); //获取( 文件类型 ) //注意FileUtil.extName是String
long size = file.getSize(); //获取( 文件大小 )
// 1.定义一个文件唯一的标识位
String uuid = IdUtil.fastSimpleUUID();
String fileUUID = uuid + StrUtil.DOT + type;
File uploadFile = new File(fileUploadPath + fileUUID); //StrUtil.DOT( 文件名) + type( png )
// 2.判断配置的文件目录是否存在,若不存在则创建一个新的文件目录
if(!uploadFile.getParentFile().exists()) {
uploadFile.getParentFile().mkdirs();
}
// 3.获取文件的url
String url;
// 上传文件到磁盘
file.transferTo(uploadFile);
// 获取文件的md5
String md5 = SecureUtil.md5(uploadFile);
// 从数据库查询是否存在相同的记录
Files dbFiles = getFileByMd5(md5);
if (dbFiles != null) {
url = dbFiles.getUrl();
// 由于文件已存在,所以删除刚才上传的重复文件
uploadFile.delete();
} else {
// 数据库若不存在重复文件,则不删除刚才上传的文件
url = "http://localhost:8085/file/" + fileUUID;
}
// 4.再存储到数据库
Files saveFile = new Files();
saveFile.setName(originalFilename);
saveFile.setType(type);
saveFile.setSize(size/1024);//转换在数据库显示的图片大小为KB
saveFile.setUrl(url);
saveFile.setMd5(md5);
fileMapper.insert(saveFile);
return url; //上传成功后返回url
}
/**
* 【2】文件下载接口 http://localhost:8085/file/{fileUUID}
* @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();
}
/**
* 【3】通过文件的MD5查询文件
*/
private Files getFileByMd5(String md5) {
// 查询文件的md5是否存在
QueryWrapper<Files> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("md5",md5);
List<Files> filesList = fileMapper.selectList(queryWrapper);
return filesList.size() == 0 ? null : filesList.get(0);
}
}
重新启动后台,然后打开postman测试一下,上传三次相同文件(不同重命名),我们可以看见files文件里面只有一张图片,数据库有三条记录
说明:三个不同命名,文件一致的图片在上传时,只保留一个
7.前端页面展示上传、下载功能(1:20:40)
新建File.vue组件
<template>
<div>
<!-- 1.2.2.(2)新增、删除、导入、导出按钮 -->
<div style="margin:10px 0">
<el-upload action="http://localhost:8085/file/upload" :show-file-list="false" :on-success="handleFileUploadSuccess" style="display:inline-block">
<el-button type="primary">
上传文件
<i class="el-icon-top"></i>
</el-button>
</el-upload>
<el-button type="danger" @click="delBatch" class="ml-5">
批量删除
<i class="el-icon-remove-outline"></i>
</el-button>
</div>
<!-- 1.2.2.(3)表格 -->
<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="50"></el-table-column>
<el-table-column prop="name" label="文件名称"></el-table-column>
<el-table-column prop="type" label="文件类型" width="120"></el-table-column>
<el-table-column prop="size" label="文件大小KB" width="190"></el-table-column>
<el-table-column label="下载" width="120">
<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 prop="address" label="操作">
<template slot-scope="scope">
<el-popconfirm
class="ml-10"
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>
<!-- 1.2.2(4)分页查询 -->
<div class="block ml-5">
<!--
current-page(当前页面)=pageNum(前端下面传过来的)
page-size(每页条数)=pagesize(前端下面传过来的)
:total=total(后台传过来的)
@size-change="handleSizeChange"
-->
<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,
headerBg: "headerBg"
}
},
created() {
//执行create生命周期函数之前,我们已经拿到了data数据,和methods方法了
this.load();
},
methods: {
load() {
this.$http.get("/file/page", {
params: {
pageNum: this.pageNum,
pageSize: this.pageSize,
name: this.name
}
}).then(res => {
// console.log(res.data);
this.tableData = res.data.records;
this.total = res.data.total;
})
},
changeEnable(row) {
this.$http.post("/file/update",row).then(res =>{
if(res.code === '200'){
this.$message.success("操作成功")
}
})
},
// 【重置】
reset() {
this.username = "";
this.email = "";
this.address = "";
this.load();
},
// 【删除】
del(id) {
this.$http.delete("/file/" + id).then(res =>{
if (res.code === '200') {
console.log(res.code)
this.$message.success("删除成功!");
// if (id) {
// //如果当前页面没有数据,跳转到上一页 (暂时写不到)
// }
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] 对象数组 变为 纯id数组
this.$http.post("/file/del/batch", ids).then(res => {
if (ids.length !== 0) {
console.log(ids);
this.$confirm("是否确认删除这" + ids.length + "条数据?", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
return batchDelete(rows);
})
.then(() => {
this.created();
this.msgSuccess("删除成功");
})
}
if (res.code === '200') {
this.$message.success("批量删除成功!");
this.load(); //重新加载页面
} else {
this.$message.error("未选择数据!");
}
});
},
// 【分页】 - val当前页码
handleSizeChange(val) {
console.log(val);
this.pageSize = val;
this.load();
},
// 【分页】 - 当前为val条数据每页
handleCurrentChange(val) {
console.log(val);
this.pageNum = val;
this.load();
},
// 【上传成功】 回调函数
handleFileUploadSuccess(res) {
console.log(res)
this.load()
},
// 【下载】
download(url) {
window.open(url)
}
}
}
</script>
<style>
</style>
8.SpringBoot写接口(1:30:20)
在FileController中,写一个删除接口
/**
* 【7】分页查询接口
*/
//(5.2)第二种方式( mybatis-plus 调用userService中接口的方法)
@GetMapping("/page") //接口路径,/file/page (defaultValue = "")//这里是默认为空查询不会报错
//三个queryWrapper.like,表示三个条件同时查询(而且默认为空查询不会失败)
public Result findPage(@RequestParam Integer pageNum,
@RequestParam Integer pageSize,
@RequestParam(defaultValue = "") String name) {
QueryWrapper<Files> queryWrapper = new QueryWrapper<>();
// 查询未删除的记录
queryWrapper.eq("is_delete",false);//0代表:false 1代表:true
queryWrapper.orderByDesc("id");
if(!"".equals(name)) {
queryWrapper.like("name",name);
}
return Result.success(fileMapper.selectPage(new Page<>(pageNum,pageSize),queryWrapper));
}
再写一个更新接口
/**
* 【4】新增或更新接口
*/
@PostMapping("/update")
public Result update(@RequestBody Files files) {
return Result.success(fileMapper.updateById(files));
}
再写一个删除接口
/**
* 【5】删除接口
*/
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {
Files files = fileMapper.selectById(id);
files.setIsDelete(true);
fileMapper.updateById(files);
return Result.success();
}
写一个批量删除接口
/**
* 【6】批量删除接口
*/
@PostMapping("/del/batch")
public Result deleteBatch(@RequestBody List<Integer> ids) {
// select * from sys_file where id in(id,id,id...)
QueryWrapper<Files> queryWrapper = new QueryWrapper<>();
queryWrapper.in("id",ids);
List<Files> files = fileMapper.selectList(queryWrapper);
for (Files file : files) {
file.setIsDelete(true);
fileMapper.updateById(file);
}
return Result.success();
}
9.在Aside.vue中,新增一个文件管理
10.在vue界面测试上传文件的接口
11.个人用户头像上传
在Person.vue组件中写头像的标签
<template>
<el-card style="width: 400px; padding: 20px; ">
<el-form label-width="80px" size="big">
<el-upload
class="avatar-uploader"
action="http://localhost:8085/file/upload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
>
<img v-if="form.avatarUrl" :src="form.avatarUrl" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
<el-form-item label="用户名" :label-width="formLabelWidth">
<el-input v-model="form.username" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="昵称" :label-width="formLabelWidth">
<el-input v-model="form.nickname" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="邮箱" :label-width="formLabelWidth">
<el-input v-model="form.email" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="电话" :label-width="formLabelWidth">
<el-input v-model="form.phone" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="地址" :label-width="formLabelWidth">
<el-input v-model="form.address" 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 {
formLabelWidth: "",
form: {},
user: localStorage.getItem("user")
? JSON.parse(localStorage.getItem("user"))
: {}
};
},
created() {
this.$http.get("/user/username/" + this.user.username).then(res => {
if (res.code === "200") {
this.form = res.data;
}
});
},
methods: {
save() {
this.$http.post("/user", this.form).then(res => {
if (res.data) {
this.$message.success("保存成功!");
this.dialogFormVisible = false; //添加成功后,关闭对话框
this.load; //重新加载页面,始终抱持前端页面能看见( 新增 和 修改后 )
} else {
this.$message.error("保存失败!");
}
});
},
handleAvatarSuccess(res) {
this.form.avatarUrl = res
}
}
};
</script>
<style>
.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: 178px;
text-align: center;
}
.avatar {
width: 138px;
height: 138px;
display: block;
}
</style>