在企业项目中,文件上传和管理是非常常见的需求。本文基于 芋道源码 的实现,介绍如何封装一个通用的 文件服务 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;
}

接口设计要点

  1. 上传文件:支持传入字节数组,自动处理文件名、路径、MIME 类型等。

  2. 预签名地址:便于前端直传文件,提升性能。

  3. 删除文件:既删除存储器里的物理文件,也删除数据库记录。

  4. 查询分页:方便后台管理。


二、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

  • 用于前端直传文件到对象存储,避免文件先经过后端。

  • 返回结构中包含:uploadUrldownloadUrlpathconfigId 等信息。


四、应用场景

  • 后台管理系统:上传/下载合同、报表、Excel

  • 移动端 App:上传头像、图片、视频

  • 大文件上传:通过预签名直传,提升性能

  • 多存储支持:可按业务模块选择不同的存储配置


五、总结

通过 FileService + FileServiceImpl 的设计,我们实现了:

✅ 文件上传、下载、删除
✅ 文件路径唯一性控制
✅ 文件分页查询
✅ 预签名直传(提升性能)
✅ 存储器解耦(支持多云对象存储)

这种实现方式已经是 企业级最佳实践,可以非常方便地扩展到不同的云存储平台。

更多推荐