在企業項目中,文件上傳和管理是非常常見的需求。本文基于 芋道源碼 的實現,介紹如何封裝一個通用的 文件服務 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
的設計,我們實現了:
? 文件上傳、下載、刪除
? 文件路徑唯一性控制
? 文件分頁查詢
? 預簽名直傳(提升性能)
? 存儲器解耦(支持多云對象存儲)
這種實現方式已經是 企業級最佳實踐,可以非常方便地擴展到不同的云存儲平臺。