一二三应用开发平台文件处理设计与实现系列之3——后端统一封装设计与实现

背景

前面介绍了前端通过集成vue-simple-uploader实现了文件的上传,今天重点说一下后端的设计与实现。

功能需求梳理

从功能角度而言,实际主要就两项,一是上传,二是下载。其中上传在文件体积较大的情况下,为了加快上传速度,提升用户体验,在具体实现上进行了文件分块,以及文件块的合并操作。
从业务场景而言,主要分为两类:
一是表单相关的附件;二是通知公告等场景,使用富文本编辑器时上传的图片。
在这两类场景中,文件实际并不是主体,而是实体的附属品。
平台对文件上传下载的支撑功能,重点还在于表单相关附件,并支持图片的上传与展示。

注:以文件为主体的业务场景也有,主要是文档库、云盘、网盘等,该场景通常会作为独立的业务应用来实现,进行专门的设计与实现,不在平台当前设计考虑范围之内。

系统设计与实现

整体设计

基于功能需求梳理,平台将文件作为实体的附属来处理。平台进行全局的统一封装与处理,避免各实体各自创建和维护自己的附件信息。具体来说,就是增加“附件”实体,将文件的主要信息,如文件名、大小、类型、存放路径等信息存放到附件库表中,并关联实体的唯一性标识。从职责上,将附件实体放到业务支撑(support)模块中进行管理。

整体处理逻辑如下:
上传文件块,如文件体积较小,没有触发分块,则该文件块就是一个完整的文件,将该文件直接存储到磁盘,并生成附件记录,插入到库表(库表中存放文件路径)。
若文件体积较大,触发了分块,则只将分块存到磁盘临时目录下,不生成附件记录;待前端检测到所有文件块均已上传完成,调用合并文件块操作,依据全局唯一的文件标识,去临时目录下找到所有的文件块,进行文件合并操作,生成附件记录。

对于富文本编辑器中上传的图片,同样使用附件功能来进行统一封装,与普通文件不同的是,图片上传不分块,存放到预置的统一目录(image/)下,生成一个虚拟的实体标识,不对应具体的实体,该实体标识来存储图片及读取图片用来展示。

实体

通过平台实体配置功能,实现附件实体的属性配置,如下图所示:
一二三应用开发平台文件处理设计与实现系列之3——后端统一封装设计与实现_第1张图片
示例数据如下:
image.png
name存放原始文件名;realName存放的是最终落盘文件名,为防止同名文件覆盖,落盘时会附加文件唯一性标识前缀。
length存放的是文件原始长度,长整型,单位是B;size则是将文件长度进行友好化转换,根据体积显示G或M或**K。
path是存储文件的相对路径,包含文件自身,是读取文件的重要关联关系。
entity存放附件对应的实体的标识。
type存放文件类型。

前端

前端api定义如下:

// 附件
export const attachment = Object.assign({}, COMMON_METHOD, {
  serveUrl: '/' + moduleName + '/' + 'attachment' + '/',
  // 上传操作内置于vue-simple-uploader中
  // 下载
  download(id) {
    return request.download({ url: this.serveUrl + id + '/download' })
  },
  // 合并文件块
  mergeChunks(param) {
    return request.post({ url: this.serveUrl + 'mergeChunks', data: param })
  },
  // 上传图片
  uploadImage(param) {
    return request.upload({ url: this.serveUrl + 'uploadImage', data: param })
  }
})

涉及到组件集成,部分后端服务地址没有体现在统一的api定义中,涉及到以下两处:
上传文件块操作,内置于vue-simple-uploader组件的配置选项options的targt属性中
一二三应用开发平台文件处理设计与实现系列之3——后端统一封装设计与实现_第2张图片
图片读取操作,内置于富文本编辑器wangeditor的自定义上传操作中
一二三应用开发平台文件处理设计与实现系列之3——后端统一封装设计与实现_第3张图片
合并文件块的核心操作,vue-simple-uploader的文件上传成功事件中,将文件关键信息整合后传到后端来,如下所示:

fileSuccess(rootFile, file) {
      if (file.chunks.length > 1) {
        //分块上传
        const param = {
          identifier: file.uniqueIdentifier,
          filename: file.name,
          moduleCode: this.moduleCode,
          entityType: this.entityType,
          entityId: this.entityId,
          type: file.fileType,
          totalSize: file.size
        }
        // 合并文件块
        this.$api.support.attachment.mergeChunks(param).then(() => {
          // 移除已上传成功的文件
          this.$refs.uploader.uploader.removeFile(file)
        })
      } else {
        // 不分块,移除已上传成功的文件
        this.$refs.uploader.uploader.removeFile(file)
      }
    }

对象视图

有两个辅助的对象视图,一个是文件块的定义,用于分块上传;另外一个是文件信息,用于合并文件块。

/**
 * 文件块对象模型,匹配前端vue-simple-uploader控件
 *
 * @author wqliu
 * @date 2023-03-08
 */
@Data
public class FileChunkVO extends BaseVO {


    /**
     * 当前文件块编号,从1开始
     */
    private Integer chunkNumber;
    /**
     * 分块大小
     */
    private Long chunkSize;
    /**
     * 当前分块大小
     */
    private Long currentChunkSize;
    /**
     * 总大小
     */
    private Long totalSize;
    /**
     * 文件标识
     */
    private String identifier;
    /**
     * 文件名
     */
    private String filename;
    /**
     * 相对路径
     */
    private String relativePath;
    /**
     * 总块数
     */
    private Integer totalChunks;
    /**
     * 文件类型
     */
    private String type;

    /**
     * 文件块内容
     */
    private MultipartFile file;

    /**
     * 业务分类
     */
    private String entityType;

    /**
     * 业务实体标识
     */
    private String entityId;

    /**
     * 模块编码
     */
    private String moduleCode;

}

/**
 * 文件 实体
 * 匹配前端simple-uploader控件
 *
 * @author wqliu
 * @date 2023-11-27
 */
@Data
public class FileInfo  {

    /**
     * 文件标识
     */
    private String identifier;
    /**
     * 文件名
     */
    private String filename;

    /**
     * 模块编码
     */
    private String moduleCode;

    /**
     * 实体类型
     */
    private String entityType;

    /**
     * 实体标识
     */
    private String entityId;

    /**
     * 文件类型
     */
    private String type;


    /**
     * 总大小
     */
    private Long totalSize;


}

控制器

在标准控制器的基础上,扩展几个方法
一二三应用开发平台文件处理设计与实现系列之3——后端统一封装设计与实现_第4张图片

  • uploadChunk:上传文件块
  • mergeChunks:合并文件块
  • downloadFile:通过附件的唯一性标识来找到文件并返回文件流
  • list:根据实体标识查找其附件数据,返回列表
  • uploadImage:为图片设置的专门上传方法,接收参数是MultipartFile,而不是uploadChunk方法中的FileChunkVO
  • getImage:为图片设置的专门读取方法,与downloadFile实际调用的是同一个服务层方法getFile,差别在于downloadFile方法需要为response响应设置header,即response.setHeader(“Content-disposition”, “attachment;filename=” + encodeFileName(fileName));以便触发下载;对于图片,直接返回流即可。

服务

服务接口只有四个,分别是上传文件块、合并文件块、上传图片和获取文件流(包括图片流)。


    /**
     * 上传文件块
     *
     * @param fileChunk
     * @return 如是最后一块, 返回附件实体实体标识, 否则返回null
     */
    String uploadChunk(FileChunk fileChunk);

    /**
     * 合并文件块
     * @param fileInfo 文件信息
     * @return {@link String} 文件标识
     */
    String mergeChunks(FileInfo fileInfo);

    /**
     * 上传图片
     *
     * @param image
     * @return 附件实体实体标识
     */
    String uploadImage(MultipartFile image);


    /**
     * 获取文件流
     *
     * @param id
     * @return 文件流
     */
    InputStream getFile(String id);

对应的服务实现代码如下:

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String uploadChunk(FileChunk fileChunk) {
        // 附件上传比较特殊,传输的数据是文件块,先根据文件块处理文件,然后生成附件实体数据

        // 上传文件块
        objectStoreService.uploadChunk(fileChunk);
        // 如只有一块,直接生成附件
        if (fileChunk.getTotalChunks() == 1) {
            // 生成附件信息
            return create(fileChunk);
        }
        return null;

    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String mergeChunks(FileInfo fileInfo) {

        // 合并文件
        objectStoreService.mergeChunks(fileInfo);
        // 生成附件信息
        return create(fileInfo);

    }

    @Override
    public String uploadImage(MultipartFile image) {
        //生成唯一性标识
        String entityId = IdWorker.getIdStr();
        // 存储文件
        objectStoreService.uploadImage(image, entityId);
        String realName = entityId + image.getOriginalFilename();
        // 生成附件信息
        Attachment entity = new Attachment();
        entity.setName(image.getOriginalFilename());
        // 设置友好显示大小
        entity.setSize(FileUtil.getFileSize(image.getSize()));
        entity.setLength(image.getSize());
        // 设置存储相对路径
        entity.setPath(FileConstant.IMAGE_PATH+realName);
        entity.setType(image.getContentType());
        entity.setRealName(realName);
        entity.setEntity(entityId);
        add(entity);
        return entity.getId();
    }


    @Override
    public InputStream getFile(String id) {
        Attachment entity = query(id);
        return objectStoreService.getFile(entity.getPath());

    }

    /**
     * 创建附件——依据文件信息
     * @param fileInfo 文件
     * @return {@link String} 附件标识
     */
    private String create(FileInfo fileInfo) {
        //实际存储文件名
        String realName = fileInfo.getIdentifier() + fileInfo.getFilename();
        // 存储相对路径
        String relativePath = objectStoreService.generateRelativePath(fileInfo.getModuleCode(),fileInfo.getEntityType());

        Attachment entity = new Attachment();
        entity.setName(fileInfo.getFilename());
        // 设置友好显示大小
        if (fileInfo.getTotalSize() != null) {
            entity.setSize(FileUtil.getFileSize(fileInfo.getTotalSize()));
            entity.setLength(fileInfo.getTotalSize());
        }
        // 设置存储相对路径
        entity.setPath(FilenameUtils.concat(relativePath, realName));
        entity.setType(fileInfo.getType());
        entity.setRealName(realName);
        entity.setEntity(fileInfo.getEntityId());
        add(entity);
        return entity.getId();
    }


    /**
     * 创建附件——依据文件块信息
     * @param fileChunk 文件块
     * @return {@link String} 附件标识
     */
    private String create(FileChunk fileChunk) {
        String realName = fileChunk.getIdentifier() + fileChunk.getFilename();
        // 存储相对路径
        String relativePath = objectStoreService.generateRelativePath(fileChunk.getModuleCode(),fileChunk.getEntityType());


        Attachment entity = new Attachment();
        entity.setName(fileChunk.getFilename());
        // 设置友好显示大小
        if (fileChunk.getTotalSize() != null) {
            entity.setSize(FileUtil.getFileSize(fileChunk.getTotalSize()));
            entity.setLength(fileChunk.getTotalSize());
        }
        // 设置存储相对路径
        entity.setPath(FilenameUtils.concat(relativePath, realName));
        entity.setType(fileChunk.getFile().getContentType());
        entity.setRealName(realName);
        entity.setEntity(fileChunk.getEntityId());
        add(entity);

        return entity.getId();
    }

开源平台资料

平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT
欢迎收藏、点赞、评论,你的支持是我前行的动力。

你可能感兴趣的:(#,能力扩展,文件处理,minio,对象存储,附件上传下载,文件存储方案)