前面介绍了前端通过集成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;
}
在标准控制器的基础上,扩展几个方法
服务接口只有四个,分别是上传文件块、合并文件块、上传图片和获取文件流(包括图片流)。
/**
* 上传文件块
*
* @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
欢迎收藏、点赞、评论,你的支持是我前行的动力。