A:数据库存储:文件以二进制流的形式保存到数据库,如MySQL、PostgreSQL和MongoDB
B:本地存储:文件保存在本地服务器上,数据库保存文件路径。(最常见)
C: 云存储:文件保存在专门的文件云服务器上。云存储服务提供商。网络存储提供了可扩展性、高可用性和数据备份等优势,可以方便地访问和共享文件。(费用爆炸)
排除A:基于性能考虑。业务会上传50M的大文件。当文件较大时,数据库的读写操作可能变得缓慢,对数据库性能产生负面影响。
排除C:基于业务量和费用考虑。业务量少,需要存储的文件非常少,没必要额外引入云存储。费用贵
选择 方案B 本地存储
实时上传1个文件的后端接口流程
1.生成文件的唯一ID,把文件保存到指定目录下(保存时用id重命名文件)。
2.文件表新增记录(包含唯一ID,文件名,文件路径,文件类型)
3.返回唯一ID给前端
前后端约定 附件列表使用字段attachments,字符串类型。
结构如下:
文件的唯一id:文件名称,文件的唯一id:文件名称
"1704851852142:Default.jpg,1704851870184:规范.docx,1704851886511:附件2023-03023修改.docx"
实现的效果:点击+,打开文件夹,只能选择1个文件(不能上传当前用户的同名文件),实时请求后端接口,返回 文件的唯一ID。前端在attachments追加“文件的唯一id:文件名称”。
每次只能实时上传1个文件,要上传几个文件,就要点几次+
编辑附件是指对 attachments的修改。(不是编辑附件的内容)
1.删除附件,前端只需要在attachments中删除对应的文件,
2.新增附件,前端请求接口实时上传文件,拿到文件的唯一ID,在attachments中追加“文件的唯一id:文件名称”。
3.点击提交按钮,请求后端的接口,后端解析attachments,找到需要删除的文件id,去文件表删除。然后把attachments保存到用户表。完成编辑功能。
在attachments中有文件id,id传入预览接口
后端接口逻辑:
(文件表)文件id–>保存路径
根据保存路径读取文件,返回文件流到前端
以下代码包括
功能接口的核心代码(需要自己补充用户表和文件表的dao层)、Feign调用的相关代码
package com.example.demo.other.base;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.*;
@RestController
@Slf4j
public class Controller {
@Value("${file.upload.path}")
String UPLOAD_DIR;
/**
* 新增 or 编辑 用户。
*/
@PostMapping("/addOrEdit")
@ResponseBody
public AjaxResult addOrEditWithAttachments(@RequestParam(name = "expertId", required = false) Long expertId,
@RequestParam("x1") String x1,
@RequestParam("x2") String x2,
@RequestParam("x3") String x3,
@RequestParam("x4") String x4,
@RequestParam("x5") String x5,
@RequestParam(name = "attachments", required = false) String attachments) {
boolean insert = ObjectUtils.isEmpty(expertId);
if (insert) {
expertId = SnowFlakeGenerator.getInstance().nextId();
} else {
//编辑时,找到要删除的文件并删除
User user = userDao.selectById(expertId);
List<Long> oldList = new LinkedList<>();
String oldAttach = user.getAttachments();
if (!StringUtils.isEmpty(oldAttach)) {
String[] arrayOld = oldAttach.split(",");
for (String attachment : arrayOld) {
String attachmentId = attachment.split(":")[0];
oldList.add(Long.valueOf(attachmentId));
}
}
Set<Long> newIdSet = new HashSet<>();
if (!StringUtils.isEmpty(attachments)) {
String[] arrayNew = attachments.split(",");
for (String attachment : arrayNew) {
String attachmentId = attachment.split(":")[0];
newIdSet.add(Long.valueOf(attachmentId));
}
}
//获取要删除的文件id
List<Long> deleteIds = new LinkedList<>();
oldList.forEach(id -> {
if (!newIdSet.contains(id)) {
deleteIds.add(id);
}
});
//删除文件
if (!CollectionUtils.isEmpty(deleteIds)) {
List<MyFile> files = fileDao.listFileByIds(deleteIds);
if (!CollectionUtils.isEmpty(files)) {
File directory = new File(UPLOAD_DIR);
files.forEach(f -> {
if (directory.exists() && directory.isDirectory()) {
File file = new File(directory, f.getId()+"."+f.getType());
if (file.exists() && file.isFile()) {
boolean deleted = file.delete();
if (deleted) {
log.info("文件删除成功!{}",f.getName());
} else {
log.error("文件删除失败!{}",f.getName());
}
} else {
log.error("文件不存在!{}",f.getName());
}
} else {
log.error("目录不存在!{}",UPLOAD_DIR);
}
});
}
//删除 文件表
fileDao.deleteByIds(deleteIds);
}
}
userDao.insertOrUpdateUser(new User(expertId, x1, x2, x3, x4, x5, attachments));
return AjaxResult.success();
}
/**
* 预览单个文件
* @param id
* @param response
* @return
*/
@GetMapping("/preview")
public ResponseEntity<byte[]> previewAttachment(@RequestParam("id") String id, HttpServletResponse response) {
List<MyFile> files = fileDao.listFileByIds(Collections.singletonList(Long.valueOf(id)));
if(CollectionUtils.isEmpty(files)){
throw new RuntimeException("文件不存在:文件id="+id);
}
String attachmentPath =files.get(0).getPath();
File attachment = new File(attachmentPath);
try (FileInputStream fis = new FileInputStream(attachment)) {
// 设置响应头
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setHeader("Content-Disposition", "inline; filename=\"" + attachment.getName() + "\"");
// 将附件内容写入响应输出流
byte[] buffer = new byte[1024];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
byte[] attachmentBytes = bos.toByteArray();
// 创建 ResponseEntity 对象并返回
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header("Content-Disposition", "inline; filename=\"" + attachment.getName() + "\"")
.body(attachmentBytes);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
/**
* 上传单个文件
* @param file
* @return
*/
@PostMapping("/file/upload")
public AjaxResult uploadFile(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return AjaxResult.error("请选择要上传的文件");
}
try {
String fileName = file.getOriginalFilename();//文件名
Long id= System.currentTimeMillis();//文件id
String extension = getFileExtension(fileName);//文件类型
String path=UPLOAD_DIR + File.separator +id+ "."+extension;//文件路径
File destFile = new File(path);
// 检查UPLOAD_DIR目录是否存在,如果不存在则创建它
File uploadDir = new File(UPLOAD_DIR);
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}
// 保存文件
file.transferTo(destFile);
fileDao.insert(new MyFile(id,fileName,path,extension,1,new Timestamp(id)));
JSONObject r=new JSONObject();
r.put("fileId",id);
return AjaxResult.success(r);
} catch (IOException e) {
e.printStackTrace();
return AjaxResult.error("文件上传失败");
}
}
private String getFileExtension(String fileName) {
int dotIndex = fileName.lastIndexOf(".");
if (dotIndex > 0 && dotIndex < fileName.length() - 1) {
return fileName.substring(dotIndex + 1);
}
return "";
}
}
package com.example.demo.other.base;
import lombok.Data;
import java.sql.Timestamp;
@Data
public class MyFile {
/**
* 文件id
*/
Long id;
String name;
String path;
String type;
Integer store;
Timestamp createTime;
public MyFile() {
}
public MyFile(Long id, String name, String path, String type, Integer store, Timestamp createTime) {
this.id = id;
this.name = name;
this.path = path;
this.type = type;
this.store = store;
this.createTime = createTime;
}
}
2.通过openFeign调用的代码
package com.example.demo.other;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@Slf4j
public class Controller1 {
@Autowired
MyService myService;
/*
预览1个文件
*/
@GetMapping("/preview")
public ResponseEntity<byte[]> preview(@RequestParam("id") String id) {
ResponseEntity<byte[]> response;
try {
response= myService.preview(id);
}catch (Exception e){
log.error("文件预览失败");
response=new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
return response;
}
/*
编辑 or 新增用户
*/
@PostMapping("/addOrEdit")
public AjaxResult addOrEdit(@RequestParam(name = "expertId", required = false) Long expertId,
@RequestParam("x1") String x1,
@RequestParam("x2") String x2,
@RequestParam("x3") String x3,
@RequestParam("x4") String x4,
@RequestParam("x5") String x5,
@RequestParam(name = "attachments", required = false) String attachments) {
return myService.addOrEditWithAttachments(expertId, x1, x2, x3, x4, x5, attachments);
}
@PostMapping("/file/upload")
public AjaxResult upload(@RequestParam("file") MultipartFile file) {
try {
return myService.uploadFile(file);
}catch (Exception e){
return AjaxResult.error("文件上传失败");
}
}
}
package com.example.demo.other;
import feign.Headers;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@FeignClient(contextId = "file", value = "xxxx", path = "xxxx")
public interface MyService {
/**
* 预览
*
* 文件id
*/
@GetMapping("/preview")
@Headers("Accept: application/octet-stream")
ResponseEntity<byte[]> preview(@RequestParam(name = "id") String id);
/**
* 创建or编辑 用户
*/
@PostMapping(value = "/addOrEdit", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
AjaxResult addOrEditWithAttachments(@RequestParam(name = "expertId", required = false) Long expertId,
@RequestParam("x1") String x1,
@RequestParam("x2") String x2
@RequestParam("x3") String x3,
@RequestParam("x4") String x4,
@RequestParam("x5") String x5,
@RequestPart(name = "attachments", required = false) String attachments);
/*
文件上传
*/
@PostMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
AjaxResult uploadFile(@RequestPart("file") MultipartFile file);
}