上传文件接口设计,SpringBoot + MinIO/S3 文件服务实现:FileService 接口与 FileServiceImpl 详解
在企业项目中,文件上传和管理是非常常见的需求。本文基于 芋道源码 的实现,介绍如何封装一个通用的 文件服务 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;
@Resource
private FileConfigService fileConfigService;
@Resource
private FileMapper fileMapper;
@Override
public PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO) {
if ("all".equals(pageReqVO.getModule())) {
pageReqVO.setModule(null);
}
return fileMapper.selectPage(pageReqVO);
}
@Override
@SneakyThrows
public 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;
}
@VisibleForTesting
String 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
@SneakyThrows
public 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));
}
@Override
public Long createFile(FileCreateReqVO createReqVO) {
FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);
fileMapper.insert(file);
return file.getId();
}
@Override
public 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;
}
@Override
public 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 的设计,我们实现了:
✅ 文件上传、下载、删除
✅ 文件路径唯一性控制
✅ 文件分页查询
✅ 预签名直传(提升性能)
✅ 存储器解耦(支持多云对象存储)
这种实现方式已经是 企业级最佳实践,可以非常方便地扩展到不同的云存储平台。
更多推荐
所有评论(0)