文件实时上传方案

文件实时上传方案

一、页面要实现的功能

  1. 上传多个文件(没有要求同时上传)
  2. 编辑附件(在一次操作中,可能删除了一些附件,同时又新增了一些附件)
  3. 预览附件。

文件实时上传方案_第1张图片

二、技术方案

2.1 文件的存储方案

2.1.1 常见的文件存储方案:

A:数据库存储:文件以二进制流的形式保存到数据库,如MySQL、PostgreSQL和MongoDB

B:本地存储:文件保存在本地服务器上,数据库保存文件路径。(最常见)

C: 云存储:文件保存在专门的文件云服务器上。云存储服务提供商。网络存储提供了可扩展性、高可用性和数据备份等优势,可以方便地访问和共享文件。(费用爆炸)

2.1.2选择文件存储方案

排除A:基于性能考虑。业务会上传50M的大文件。当文件较大时,数据库的读写操作可能变得缓慢,对数据库性能产生负面影响。
排除C:基于业务量和费用考虑。业务量少,需要存储的文件非常少,没必要额外引入云存储。费用贵
选择 方案B 本地存储

2.1.3 本地存储实现方案

实时上传1个文件的后端接口流程
1.生成文件的唯一ID,把文件保存到指定目录下(保存时用id重命名文件)。
2.文件表新增记录(包含唯一ID,文件名,文件路径,文件类型)
3.返回唯一ID给前端

文件表结构文件实时上传方案_第2张图片

文件记录示例
文件实时上传方案_第3张图片

三、功能实现

字段attachments

前后端约定 附件列表使用字段attachments,字符串类型。
结构如下:
文件的唯一id:文件名称,文件的唯一id:文件名称

"1704851852142:Default.jpg,1704851870184:规范.docx,1704851886511:附件2023-03023修改.docx"

3.1. 上传多个文件(没有要求同时上传)

实现的效果:点击+,打开文件夹,只能选择1个文件(不能上传当前用户的同名文件),实时请求后端接口,返回 文件的唯一ID。前端在attachments追加“文件的唯一id:文件名称”。
每次只能实时上传1个文件,要上传几个文件,就要点几次+

3.2.编辑附件

编辑附件是指对 attachments的修改。(不是编辑附件的内容)
1.删除附件,前端只需要在attachments中删除对应的文件,
2.新增附件,前端请求接口实时上传文件,拿到文件的唯一ID,在attachments中追加“文件的唯一id:文件名称”。
3.点击提交按钮,请求后端的接口,后端解析attachments,找到需要删除的文件id,去文件表删除。然后把attachments保存到用户表。完成编辑功能。

3.3.预览附件

在attachments中有文件id,id传入预览接口
后端接口逻辑:
(文件表)文件id–>保存路径
根据保存路径读取文件,返回文件流到前端

四、代码实现

以下代码包括
功能接口的核心代码(需要自己补充用户表和文件表的dao层)、Feign调用的相关代码

1.核心代码
controller
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 "";
    }

}

自定义MyFile类
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调用的代码

controller
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("文件上传失败");
        }
    }

}

Feign远程调用基础模块的接口
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);

}

你可能感兴趣的:(java)