背景
前面介绍了前端通过集成vue-simple-uploader实现了文件的上传,今天重点说一下后端的设计与实现。
功能需求梳理
从功能角度而言,实际主要就两项,一是上传,二是下载。其中上传在文件体积较大的情况下,为了加快上传速度,提升用户体验,在具体实现上进行了文件分块,以及文件块的合并操作。
从业务场景而言,主要分为两类:
一是表单相关的附件;二是通知公告等场景,使用富文本编辑器时上传的图片。
在这两类场景中,文件实际并不是主体,而是实体的附属品。
平台对文件上传下载的支撑功能,重点还在于表单相关附件,并支持图片的上传与展示。
注:以文件为主体的业务场景也有,主要是文档库、云盘、网盘等,该场景通常会作为独立的业务应用来实现,进行专门的设计与实现,不在平台当前设计考虑范围之内。
系统设计与实现
整体设计
基于功能需求梳理,平台将文件作为实体的附属来处理。平台进行全局的统一封装与处理,避免各实体各自创建和维护自己的附件信息。具体来说,就是增加“附件”实体,将文件的主要信息,如文件名、大小、类型、存放路径等信息存放到附件库表中,并关联实体的唯一性标识。从职责上,将附件实体放到业务支撑(support)模块中进行管理。
整体处理逻辑如下:
上传文件块,如文件体积较小,没有触发分块,则该文件块就是一个完整的文件,将该文件直接存储到磁盘,并生成附件记录,插入到库表(库表中存放文件路径)。
若文件体积较大,触发了分块,则只将分块存到磁盘临时目录下,不生成附件记录;待前端检测到所有文件块均已上传完成,调用合并文件块操作,依据全局唯一的文件标识,去临时目录下找到所有的文件块,进行文件合并操作,生成附件记录。
对于富文本编辑器中上传的图片,同样使用附件功能来进行统一封装,与普通文件不同的是,图片上传不分块,存放到预置的统一目录(image/)下,生成一个虚拟的实体标识,不对应具体的实体,该实体标识来存储图片及读取图片用来展示。
实体
通过平台实体配置功能,实现附件实体的属性配置,如下图所示:
示例数据如下:
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属性中
图片读取操作,内置于富文本编辑器wangeditor的自定义上传操作中
合并文件块的核心操作,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;}
控制器
在标准控制器的基础上,扩展几个方法
- 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);}@Overridepublic 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();}@Overridepublic 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
欢迎收藏、点赞、评论,你的支持是我前行的动力。