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

news2025/1/21 0:57:30

背景

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

功能需求梳理

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

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

系统设计与实现

整体设计

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

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

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

实体

通过平台实体配置功能,实现附件实体的属性配置,如下图所示:
image.png
示例数据如下:
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属性中
image.png
图片读取操作,内置于富文本编辑器wangeditor的自定义上传操作中
image.png
合并文件块的核心操作,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;


}

控制器

在标准控制器的基础上,扩展几个方法
image.png

  • 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
欢迎收藏、点赞、评论,你的支持是我前行的动力。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1347857.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2013年第二届数学建模国际赛小美赛B题寄居蟹进化出人类的就业模式解题全过程文档及程序

2013年第二届数学建模国际赛小美赛 B题 寄居蟹进化出人类的就业模式 原题再现: 寄居蟹是美国最受欢迎的宠物品种,依靠其他动物的壳来保护。剥去寄居蟹的壳,你会看到它柔软、粉红色的腹部卷曲在头状的蕨类叶子后面。大多数寄居蟹喜欢蜗牛壳&…

Java集合/泛型篇----第五篇

系列文章目录 文章目录 系列文章目录前言一、说说LinkHashSet( HashSet+LinkedHashMap)二、HashMap(数组+链表+红黑树)三、说说ConcurrentHashMap前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通…

加强->servlet->tomcat

0什么是servlet jsp也是servlet 细细体会 Servlet 是 JavaEE 的规范之一,通俗的来说就是 Java 接口,将来我们可以定义 Java 类来实现这个接口,并由 Web 服务器运行 Servlet ,所以 TomCat 又被称作 Servlet 容器。 Servlet 提供了…

数据结构: 位图

位图 概念 用一个bit为来标识数据在不在 功能 节省空间快速查找一个数在不在一个集合中排序 去重求两个集合的交集,并集操作系统中的磁盘标记 简单实现 1.设计思想:一个bit位标识一个数据, 使用char(8bit位)集合来模拟 2.预备工作:a.计算这个数在第几个char b.是这个ch…

「实验记录」CS144 Lab1 StreamReassembler

目录 一、Motivation二、SolutionsS1 - StreamReassembler的对外接口S2 - push_substring序列写入ByteStream 三、Result四、My Code五、Reference 一、Motivation 我们都知道 TCP 是基于字节流的传输方式,即 Receiver 收到的数据应该和 Sender 发送的数据是一样的…

C#-CSC编译环境搭建

一.Microsoft .NET Framework 确保系统中安装Microsoft .NET Framework相关版本下载 .NET Framework 4.7 | 免费官方下载 (microsoft.com)https://dotnet.microsoft.com/zh-cn/download/dotnet-framework/net47 二.编译环境搭建 已经集成编译工具csc.exe,归档至gitcode,实现us…

L1-076:降价提醒机器人

题目描述 小 T 想买一个玩具很久了,但价格有些高,他打算等便宜些再买。但天天盯着购物网站很麻烦,请你帮小 T 写一个降价提醒机器人,当玩具的当前价格比他设定的价格便宜时发出提醒。 输入格式: 输入第一行是两个正整数…

数据隐私:技术和法律的双重挑战

当前,数据已成为企业和个人最宝贵的资产之一。然而,随着数据的广泛收集和共享,数据隐私问题也日益突出。保护个人信息的隐私不仅是法律规定的义务,也是维护社会公正、保护个人权益的必要措施。本文将从数据隐私的概念、重要性、面…

Linux学习第48天:Linux USB驱动试验:保持热情,保持节奏,持续学习是作为一个技术人员应有的基本素质和要求

Linux版本号4.1.15 芯片I.MX6ULL 大叔学Linux 品人间百味 思文短情长 最近更新的速度和频率大不如以前,主要原因还是自己有些懈怠了。学习是一个持续努力的过程,一旦中断,再想保持以往的状态可能要…

《MySQL系列-InnoDB引擎01》MySQL体系结构和存储引擎

文章目录 第一章 MySQL体系结构和存储引擎1 数据库和实例2 MySQL配置文件3 MySQL数据库路径4 MySQL体系结构5 MySQL存储引擎5.1 InnoDB存储引擎5.2 MyISAM存储引擎5.3 NDB存储引擎5.4 Memory存储引擎5.5 Archive存储引擎5.6 Federated存储引擎 6 连接MySQL6.1 TCP/IP6.2 命名管…

MySQL数据库学习一

1 什么是数据库的事务&#xff1f; 1.1 事务的典型场景 在项目里面&#xff0c;什么地方会开启事务&#xff0c;或者配置了事务&#xff1f;无论是在方法上加注解&#xff0c;还 是配置切面。 <tx:advice id"txAdvice" transaction-manager"transactionMa…

数据结构式新年贺词

冒泡排序&#xff0c;选择排序&#xff0c;插入排序&#xff0c;快速排序&#xff0c;堆排序&#xff0c;归并排序&#xff0c;希尔排序&#xff0c;桶排序&#xff0c;基数排序新年帮您排忧解难。 有向图&#xff0c;无向图&#xff0c;有环图&#xff0c;无环图&#xff0c;…

回溯法解决n皇后问题(迭代版)

n皇后问题的关键在于judge函数&#xff0c;判断当前的情况是否合法 1.x[i]x[k]说明有两个皇后处于同一列&#xff0c;不符合 2.x[k]-x[i]k-i&#xff1a; 由于k-i是固定的&#xff0c;假设k3,i2,那么k-i1, 如果x[k]-x[i]1, 说明第k个皇后在第i个皇后右…

PostgreSQL16.1(Windows版本)

1、卸载原有的PostgreSQL &#xfeff; &#xfeff; 点击Next即可。 &#xfeff;&#xfeff; 点击OK即可。 卸载完成。 2、安装 &#xff08;1&#xff09; 前两部直接Next&#xff0c;第二部可以换成自己想要安装的路径。 &#xff08;2&#xff09; 直接点击Next。…

WeNet语音识别调用通义千问

WeNet语音识别调用通义千问 WeNet语音识别对通义千问&#xff08;Qwen-72B-Chat Bot&#xff09;调用&#xff0c;首先通过WeNet将用户的语音输入转录为文本&#xff0c;然后将此文本输入通用问答模型以获取答案。 本人原创作品&#xff0c;体验一下 连续对话 WeNet语音识别…

华为云创新中心,引领浙南的数字化腾飞

编辑&#xff1a;阿冒 设计&#xff1a;沐由 县域经济是我国国民经济的重要组成部分&#xff0c;是推动经济社会全面发展的核心力量之一。在推进中国式现代化的征程中&#xff0c;县域经济扮演的角色也越来越重要。 毫无疑问&#xff0c;县域经济的良性发展&#xff0c;需要多方…

软件工程总复习笔记

软件工程课程复习提纲 文章目录 软件工程课程复习提纲一、基本知识点1. 软件工程的概念及目标2. 软件危机的概念及典型表现3. 瀑布模型的概念及特点4. 快速原型模型的特点5. 螺旋模型的基本思想6. 软件生命周期的概念及划分为哪几个阶段7. 软件需求的定义8. 常见的软件需求获取…

基于AT89C51单片机可做实物的温度烟雾火灾报警设计

点击链接获取Keil源码与Project Backups仿真图&#xff1a; https://download.csdn.net/download/qq_64505944/88658141?spm1001.2014.3001.5503 C 源码仿真图毕业设计实物制作步骤02 摘要 随着现代家庭用火、用电量的增加&#xff0c;家庭火灾发生的频率越来越高。火灾报警…

解算人生--写于2023跨年之夜

最近买了一本书&#xff0c;书名叫《计算》 读了部分内容&#xff0c;虽然理解上还需要再下下功夫&#xff0c;但是直观的感觉冲击还是挺大的&#xff0c;最明显的就是表面与本质的把握。大家可能都有这样一种感觉&#xff0c;初步涉足某一领域时&#xff0c;开始我们都会被大量…

2024最全面且有知识深度的web3开发工具、web3学习项目资源平台

在Web3技术迅速发展的时代&#xff0c;寻找一个综合且深入的Web3开发工具和学习项目资源平台变得至关重要。今天&#xff0c;我将向大家介绍一个非常有价值的网站&#xff0c;它就是https://web3x.world 。 Web3X是一个全面而深入的Web3开发者社区&#xff0c;为开发者们提供了…