SpringBoot Vue 批量上传附件与打包压缩下载附件
前言:
在传统的管理系统项目一般都遇到有附件的上传与下载业务,我最近参与的项目中用到了附件的上传与下载功能,今天正好有空整理一下。
业务逻辑:
附件先上传到临时目录,业务页面点击保存后临时目录附件移动到正式目录下,并将业务数据和附件相关信息保存入库。
废话不多说,直接上代码!!!
- 批量上传附件
前端 vue 代码
前端 使用 Element-UI 的上传控件
<el-card>
<h1>1.上傳附件</h1>
<el-upload
class="upload-demo"
drag
:headers="headers"
name="files"
:on-remove="remove"
:on-success="success"
:auto-upload="true"
action="api/basic/file/uploadFile"
multiple>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">只能上传doc docx文件,且不超10MB</div>
</el-upload>
</el-card>
上图中 :on-success="success" 代表附件上传成功后的钩子
接收后端返回的临时目录
//文件上传成功时的钩子
success(res, file, fileList) {
this.fileList.length = 0
for (let i = 0; i < fileList.length; i++) {
if(fileList[i].response)
this.fileList.push(fileList[i].response.list[0])
}
console.log(`已上传文件列表`, this.fileList)
},
后端 接口
配置文件的上传路径(前缀) 在application-dev.yml 中 ,项目打包发布到对应的服务,附件上传后的位置就是配置的路径
获取application-dev.yml配置路径
/**
*@Description 文件上传处理方法 临时目录
*@Param MultipartFile file
*@Author
*@Date 2020/4/22 18:24
*@return R
*/
@PostMapping("/uploadFile")
public R uploadFile(@RequestParam(required = false, value = "files") MultipartFile[] multipartFiles) throws IOException {
List<AccessoryQueryExt> list = new ArrayList<AccessoryQueryExt>();
if(multipartFiles.length > 0){
for (MultipartFile file : multipartFiles){
R r = fileService.uploadFile(file,filePath);
String path = (String) r.get("path");
String fileName = (String) r.get("fileName");
AccessoryQueryExt accessoryQueryExt = new AccessoryQueryExt();
accessoryQueryExt.setName(fileName);
accessoryQueryExt.setTemporaryPath(path);
list.add(accessoryQueryExt);
}
return R.success().put("list",list);
}
return R.fail();
}
上传的实现类
@Transactional(rollbackFor = Exception.class)
@Override
public R uploadFile(MultipartFile file,String filePath) throws IOException{
// 判断文件是否为空
if(file.isEmpty()){
return R.fail().setMessage("file is empty");
}
String fileName = file.getOriginalFilename();
// 获取文件名后缀
String suffixName = fileName.substring(fileName.lastIndexOf("."));
// 附件重命名 = 生成32位随机码 + 源文件后缀
String uuid = UUID.randomUUID().toString().trim().replaceAll("-", "");
String newFileName = uuid.concat(suffixName);
// 设置文件临时存储路径
String path = "Temporary".concat("/").concat(newFileName);
// 全路径
String fullPath = filePath.concat(path);
File dest = new File(fullPath);
// 判断目录是否存在
if(!dest.getParentFile().exists()){
dest.getParentFile().mkdirs();
}
// 写入文件
file.transferTo(dest);
// 将临时目录和原附件名称返回
return R.success().put("path",path).put("fileName",fileName);
}
2. 移动附件至正式目录
前端 vue
<el-card>
<h1>2.保存附件</h1>
<el-button @click="save()">保存表單</el-button>
</el-card>
save(){
console.log(this.fileList)
for (let i = 0; i < this.fileList.length; i++){
this.fileList[i].isDelete = false // 此处为业务逻辑参数
this.fileList[i].relaId = "1" // 此处为业务逻辑参数
}
http
.post('basic/file/saveFile', this.fileList)
.then(function (res) {
console.log(res)
})
.catch(function (err) {
console.log(err);
});
}
后端接口及实现
/**
*@Description 保存附件至正式目录
*@Param accessoryQueryExt 附件接收参数实体
*@Author
*@Date 2020/5/25 11:50
*@return R
*/
@PostMapping("/saveFile")
public R saveFile(@RequestBody List<AccessoryQueryExt> accessoryQueryExts){
for (AccessoryQueryExt accessoryQueryExt : accessoryQueryExts){
fileService.saveFile(accessoryQueryExt, filePath);
}
return R.success();
}
@Override
public R saveFile(AccessoryQueryExt accessoryQueryExt, String filePath) {
if(accessoryQueryExt.getIsDelete()){
// 删除附件表中对应的数据并删除附件
QueryWrapper<Accessory> wrapper = new QueryWrapper<Accessory>();
wrapper.eq("RELA_ID", accessoryQueryExt.getRelaId());
List<Accessory> accessories = this.baseMapper.selectList(wrapper);
if(accessories != null && accessories.size() > 0){
for (Accessory accessory : accessories){
// 删除附件
new File(filePath.concat(accessory.getPath())).delete();
this.baseMapper.deleteById(accessory.getAccessoryId());
}
}
}
// 获取附件临时目录
String temporaryPath = accessoryQueryExt.getTemporaryPath();
if(StringUtils.isEmpty(temporaryPath)){
throw new RuntimeException("附件临时目录为空");
}
String formatDate = new SimpleDateFormat("yyyyMM").format(new Date());
// 附件正式目录 /data/uploadFile/业务类型码/yyyymm 业务类型码 从字典表中获取
String businessTypeName = "common";
if(StringUtils.isNotBlank(accessoryQueryExt.getBusinessTypeName())){
businessTypeName = accessoryQueryExt.getBusinessTypeName();
}
String savePath = businessTypeName.concat("/").concat(formatDate).concat("/").concat(temporaryPath.substring(10));
String formalPath = filePath.concat(savePath);
File dest = new File(formalPath);
// 判断目录是否存在
if(!dest.getParentFile().exists()){
dest.getParentFile().mkdirs();
}
File fileOld = new File(filePath.concat(temporaryPath));
String sizeUnit = getPrintSize(fileOld.length());
String[] split = sizeUnit.split(":");
if(fileOld.renameTo(dest)){
// 将文件上传信息写入数据库
Accessory accessory = new Accessory();
// 关联ID
accessory.setRelaId(new BigDecimal(accessoryQueryExt.getRelaId()));
String perfix = accessoryQueryExt.getName().substring(accessoryQueryExt.getName().lastIndexOf(".") + 1);
if(FileTypeUtil.fileType(perfix)){
// 文件类型 01 文档 02 图片 03 视频
accessory.setFileType("01");
} else if(FileTypeUtil.imageType(perfix)){
accessory.setFileType("02");
} else if(FileTypeUtil.videoType(perfix)){
accessory.setFileType("03");
}
if(filePath.indexOf(":") != -1){
//文件存储类型 01 本地 02 远程
accessory.setFileStoreType("01");
} else {
accessory.setFileStoreType("02");
}
//文件名
accessory.setName(accessoryQueryExt.getName());
accessory.setPath(savePath);
// 附件大小
accessory.setSize(new BigDecimal(split[0]));
// 附件单位
accessory.setUnit(split[1]);
this.baseMapper.insert(accessory);
return R.success();
}
return R.fail().setMessage("移动附件失败");
}
/**
* 根据文件大小转换为B、KB、MB、GB单位字符串显示
* @param fileSize 文件的大小(long型)
* @return 返回 转换后带有单位的字符串
*/
public static String getPrintSize(long fileSize){
String strFileSize = null;
if(fileSize < 1024){
strFileSize = fileSize + ":" +"B";
return strFileSize;
}
DecimalFormat df = new DecimalFormat("######0.00");
if ((fileSize >= 1024) && (fileSize < 1024*1024)){ //KB
strFileSize = df.format(((double)fileSize)/1024) + ":" +"KB";
}else if((fileSize >= 1024*1024)&&(fileSize < 1024*1024*1024)){ //MB
strFileSize = df.format(((double)fileSize)/(1024*1024)) + ":" +"MB";
}else{ //GB
strFileSize = df.format(((double)fileSize)/(1024*1024*1024)) + ":" +"GB";
}
return strFileSize;
}
- 批量下载并压缩
前端 vue
<el-card>
<h1>3.下載附件</h1>
<el-button @click="download()">下载附件</el-button>
</el-card>
download:function(){
http
({
method: 'post',
url: 'basic/file/downloadFile?relaId=1',
data: {}, //可选参数,后台用 @RequestBody接收
responseType: 'blob'
})
.then(function(res)
{
let data= res.data
var blob = new Blob([data])
// 创建下载的链接
var a = document.createElement('a');
a.style.display = 'none';
var href = window.URL.createObjectURL(blob);
a.href = href;
//显示下载文件名 获取后端返回的文件名
let filename= res.headers['content-disposition'].split('=')[1]
// 文件名中有中文 则对文件名进行转码 decodeURI
a.download= decodeURI(filename)
// 利用a标签做下载
document.body.appendChild(a);
// 点击下载
a.click();
// 下载后移除元素
document.body.removeChild(a);
// 释放掉blob对象
window.URL.revokeObjectURL(href);
})
.catch(function (error)
{
console.info('response.error'+error.message)
});
}
后端 接口
/**
*@Description 文件下载处理方法
*@Param relaId 关联ID
*@Author
*@Date 2020/4/23 13:47
*@return R
*/
@GetMapping("/downloadFile")
public void downloadFile(HttpServletResponse response,@RequestParam("relaId") String relaId){
fileService.downloadFile(relaId,response,filePath);
}
@Override
public void downloadFile(String relaId, HttpServletResponse response, String filePath)
{
// 从数据库获取文件信息
QueryWrapper<Accessory> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("RELA_ID",relaId);
List<Accessory> accessoryList = this.baseMapper.selectList(queryWrapper);
if(accessoryList != null && accessoryList.size() > 1){
// 压缩包名称
String fileName = String.valueOf(System.currentTimeMillis());
batchDownload(accessoryList,fileName, response, filePath);
}
// 若只有一个文件,则不用压缩
if(accessoryList != null && accessoryList.size() == 1){
Accessory accessory = accessoryList.get(0);
download(accessory,filePath,response);
}
}
/**
* 批量下载并压缩
*/
public void batchDownload(List<Accessory> accessoryList,String filename, HttpServletResponse response, String filePath) {
//创建zip文件并返回zip文件路径
String zipPath = createZipAndReturnPath(accessoryList, filename, filePath);
try {
response.reset();
response.setCharacterEncoding("UTF-8");
response.setContentType("application/zip;charset=utf-8");
response.setHeader("content-disposition", "attachment;filename="+filename+".zip");
// 返回文件名,需要设置下面代码
response.setHeader("Access-Control-Expose-Headers", "content-disposition,content-length");
//开始下载
BufferedInputStream is = new BufferedInputStream(new FileInputStream(new File(zipPath)));
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
byte[] buff = new byte[1024];
int len = 0;
while ((len = is.read(buff, 0, buff.length)) != -1) {
out.write(buff, 0, len);
}
out.close();
out.flush();
is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 批量打包
* @param filePath 根目录
* @param filename 压缩包名称
* @return zip文件保存绝对路径
*/
public String createZipAndReturnPath(List<Accessory> accessoryList,String filename, String filePath) {
try {
//生成打包下载后的zip文件:文件名.zip
String papersZipName = filename+".zip";
//zip文件保存路径
String zipPath = filePath + papersZipName;
ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipPath));
for (Accessory accessory : accessoryList){
//获得文件相对路径
String relativePath = accessory.getPath();
//获得文件名
String fileName = accessory.getName();
//获得下载文件完整路径
String downloadPath = filePath + relativePath;
FileInputStream fis = new FileInputStream(downloadPath);
out.putNextEntry(new ZipEntry(fileName));
//写入压缩包
int len;
byte[] buffer = new byte[1024];
while ((len = fis.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
out.closeEntry();
fis.close();
}
out.close();
out.flush();
return zipPath;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 下载
*/
public void download(Accessory accessory, String filePath, HttpServletResponse response){
String fileName = accessory.getName();
String path = accessory.getPath();
File file = new File(filePath.concat(path));
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
String extension = ext(path);
//设置response内容的类型
response = setContextType(extension, response);
// 设置文件名
response.addHeader("content-disposition", "attachment;fileName=" + URLEncoder.encode(fileName,"UTF-8" ));
response.setHeader("content-length", Long.toString(file.length()));
// 返回文件名,需要设置下面代码
response.setHeader("Access-Control-Expose-Headers", "content-disposition,content-length");
byte[] buffer = new byte[1024];
fis = new FileInputStream(file);
bis = new BufferedInputStream(fis);
OutputStream os = response.getOutputStream();
int i = bis.read(buffer);
while (i != -1) {
os.write(buffer, 0, i);
i = bis.read(buffer);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 设置response内容的类型
*/
public static HttpServletResponse setContextType(String extension, HttpServletResponse response){
if("JPG".equalsIgnoreCase(extension)){
response.setContentType("application/x-jpg");
} else if("GIF".equalsIgnoreCase(extension)){
response.setContentType("image/gif");
} else if("mp4".equalsIgnoreCase(extension)){
response.setContentType("video/mpeg4");
} else if("avi".equalsIgnoreCase(extension)){
response.setContentType("video/avi");
} else if("WMV".equalsIgnoreCase(extension)){
response.setContentType("video/x-ms-wmv");
} else if("txt".equalsIgnoreCase(extension)){
response.setContentType("text/plain");
} else if("doc".equalsIgnoreCase(extension)){
response.setContentType("application/msword");
} else if("pdf".equalsIgnoreCase(extension)){
response.setContentType("application/pdf");
} else if("xls".equalsIgnoreCase(extension)){
response.setContentType("application/vnd.ms-excel");
}
return response;
}