在企业项目中,文件上传和管理是非常常见的需求。本文基于 芋道源码 的实现,介绍如何封装一个通用的 文件服务 FileService,支持:
文件上传(保存数据库记录 + 存储文件到 S3/MinIO 等对象存储)
文件下载与删除
文件分页查询
生成预签名上传地址(适合前端直传场景)
一、FileService 接口设计
首先定义 统一的文件服务接口,抽象业务逻辑,便于后续扩展。
public interface FileService {/*** 获得文件分页*/PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO);/*** 保存文件,并返回访问路径*/String createFile(@NotEmpty byte[] content, String name, String directory, String type, String module);/*** 生成预签名地址信息(前端直传场景)*/FilePresignedUrlRespVO getFilePresignedUrl(@NotEmpty String name, String directory);/*** 数据库中保存文件*/Long createFile(FileCreateReqVO createReqVO);/*** 删除文件*/void deleteFile(Long id) throws Exception;/*** 获得文件内容(下载)*/byte[] getFileContent(Long configId, String path) throws Exception;
}
接口设计要点
上传文件:支持传入字节数组,自动处理文件名、路径、MIME 类型等。
预签名地址:便于前端直传文件,提升性能。
删除文件:既删除存储器里的物理文件,也删除数据库记录。
查询分页:方便后台管理。
二、FileServiceImpl 实现类
@Service
public class FileServiceImpl implements FileService {// 控制路径是否带日期/时间戳static boolean PATH_PREFIX_DATE_ENABLE = true;static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true;@Resourceprivate FileConfigService fileConfigService;@Resourceprivate FileMapper fileMapper;@Overridepublic PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO) {if ("all".equals(pageReqVO.getModule())) {pageReqVO.setModule(null);}return fileMapper.selectPage(pageReqVO);}@Override@SneakyThrowspublic String createFile(byte[] content, String name, String directory, String type, String module) {// 1. 补全文件信息if (StrUtil.isEmpty(type)) {type = FileTypeUtils.getMineType(content, name);}if (StrUtil.isEmpty(name)) {name = DigestUtil.sha256Hex(content);}if (StrUtil.isEmpty(FileUtil.extName(name))) {String extension = FileTypeUtils.getExtension(type);if (StrUtil.isNotEmpty(extension)) {name = name + extension;}}// 2. 生成上传路径String path = generateUploadPath(name, directory);// 3. 上传文件FileClient client = fileConfigService.getMasterFileClient();Assert.notNull(client, "客户端(master) 不能为空");String url = client.upload(content, path, type);// 4. 保存数据库fileMapper.insert(new FileDO().setConfigId(client.getId()).setName(name).setPath(path).setUrl(url).setModule(module).setType(type).setSize(content.length));return url;}@VisibleForTestingString generateUploadPath(String name, String directory) {String prefix = PATH_PREFIX_DATE_ENABLE? LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN): null;String suffix = PATH_SUFFIX_TIMESTAMP_ENABLE ? String.valueOf(System.currentTimeMillis()) : null;if (StrUtil.isNotEmpty(suffix)) {String ext = FileUtil.extName(name);if (StrUtil.isNotEmpty(ext)) {name = FileUtil.mainName(name) + "_" + suffix + "." + ext;} else {name = name + "_" + suffix;}}if (StrUtil.isNotEmpty(prefix)) {name = prefix + "/" + name;}if (StrUtil.isNotEmpty(directory)) {name = directory + "/" + name;}return name;}@Override@SneakyThrowspublic FilePresignedUrlRespVO getFilePresignedUrl(String name, String directory) {String path = generateUploadPath(name, directory);FileClient client = fileConfigService.getMasterFileClient();FilePresignedUrlRespDTO dto = client.getPresignedObjectUrl(path);return BeanUtils.toBean(dto, FilePresignedUrlRespVO.class,object -> object.setConfigId(client.getId()).setPath(path));}@Overridepublic Long createFile(FileCreateReqVO createReqVO) {FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);fileMapper.insert(file);return file.getId();}@Overridepublic void deleteFile(Long id) throws Exception {FileDO file = validateFileExists(id);FileClient client = fileConfigService.getFileClient(file.getConfigId());Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId());client.delete(file.getPath());fileMapper.deleteById(id);}private FileDO validateFileExists(Long id) {FileDO file = fileMapper.selectById(id);if (file == null) {throw exception(FILE_NOT_EXISTS);}return file;}@Overridepublic byte[] getFileContent(Long configId, String path) throws Exception {FileClient client = fileConfigService.getFileClient(configId);Assert.notNull(client, "客户端({}) 不能为空", configId);return client.getContent(path);}
}
三、核心逻辑拆解
1. 文件路径生成策略
是否按日期分目录:
20250908/xxx.png
是否加时间戳后缀:避免文件名冲突
可扩展为 UUID、Hash 等
2. 文件存储
通过
FileClient
统一抽象,可以对接 MinIO、阿里云 OSS、七牛云、AWS S3 等。上传成功后,数据库保存一条记录(包括 url、路径、大小、类型、模块分类等)。
3. 文件删除
先校验数据库记录是否存在
调用存储器客户端删除物理文件
再删除数据库记录
4. 文件预签名 URL
用于前端直传文件到对象存储,避免文件先经过后端。
返回结构中包含:
uploadUrl
、downloadUrl
、path
、configId
等信息。
四、应用场景
后台管理系统:上传/下载合同、报表、Excel
移动端 App:上传头像、图片、视频
大文件上传:通过预签名直传,提升性能
多存储支持:可按业务模块选择不同的存储配置
五、总结
通过 FileService + FileServiceImpl
的设计,我们实现了:
✅ 文件上传、下载、删除
✅ 文件路径唯一性控制
✅ 文件分页查询
✅ 预签名直传(提升性能)
✅ 存储器解耦(支持多云对象存储)
这种实现方式已经是 企业级最佳实践,可以非常方便地扩展到不同的云存储平台。