java每日精进 8.04【文件管理细致分析】

发布于:2025-08-06 ⋅ 阅读:(86) ⋅ 点赞:(0)

Controller

FileConfigController

@Tag(name = "管理后台 - 文件配置")
@RestController
@RequestMapping("/infra/file-config")
@Validated
public class FileConfigController {

    @Resource
    private FileConfigService fileConfigService;

    @PostMapping("/create")
    @Operation(summary = "创建文件配置")
    @PreAuthorize("@ss.hasPermission('infra:file-config:create')")
    public CommonResult<Long> createFileConfig(@Valid @RequestBody FileConfigSaveReqVO createReqVO) {
        return success(fileConfigService.createFileConfig(createReqVO));
    }

    @PutMapping("/update")
    @Operation(summary = "更新文件配置")
    @PreAuthorize("@ss.hasPermission('infra:file-config:update')")
    public CommonResult<Boolean> updateFileConfig(@Valid @RequestBody FileConfigSaveReqVO updateReqVO) {
        fileConfigService.updateFileConfig(updateReqVO);
        return success(true);
    }

    @PutMapping("/update-master")
    @Operation(summary = "更新文件配置为 Master")
    @PreAuthorize("@ss.hasPermission('infra:file-config:update')")
    public CommonResult<Boolean> updateFileConfigMaster(@RequestParam("id") Long id) {
        fileConfigService.updateFileConfigMaster(id);
        return success(true);
    }

    @DeleteMapping("/delete")
    @Operation(summary = "删除文件配置")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('infra:file-config:delete')")
    public CommonResult<Boolean> deleteFileConfig(@RequestParam("id") Long id) {
        fileConfigService.deleteFileConfig(id);
        return success(true);
    }

    @GetMapping("/get")
    @Operation(summary = "获得文件配置")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('infra:file-config:query')")
    public CommonResult<FileConfigRespVO> getFileConfig(@RequestParam("id") Long id) {
        FileConfigDO config = fileConfigService.getFileConfig(id);
        return success(BeanUtils.toBean(config, FileConfigRespVO.class));
    }

    @GetMapping("/page")
    @Operation(summary = "获得文件配置分页")
    @PreAuthorize("@ss.hasPermission('infra:file-config:query')")
    public CommonResult<PageResult<FileConfigRespVO>> getFileConfigPage(@Valid FileConfigPageReqVO pageVO) {
        PageResult<FileConfigDO> pageResult = fileConfigService.getFileConfigPage(pageVO);
        return success(BeanUtils.toBean(pageResult, FileConfigRespVO.class));
    }

    @GetMapping("/test")
    @Operation(summary = "测试文件配置是否正确")
    @PreAuthorize("@ss.hasPermission('infra:file-config:query')")
    public CommonResult<String> testFileConfig(@RequestParam("id") Long id) throws Exception {
        String url = fileConfigService.testFileConfig(id);
        return success(url);
    }
}
  • 作用:这是 yudao-module-infra 模块中的文件配置管理控制器,位于管理后台,提供 RESTful API 用于管理文件存储配置(如 S3、磁盘、数据库存储)。
  • 包路径:cn.iocoder.moyun.module.infra.controller.admin.file 表示这是基础设施模块(infra)的管理员相关控制器,专门处理文件配置操作。
  • 依赖
    • 使用 BeanUtils 进行 DO 和 VO 之间的转换。
    • 使用 FileConfigService 处理文件配置的业务逻辑。
    • 使用 Spring Security 的 @PreAuthorize 进行权限控制。
    • 使用 Swagger 注解 (@Tag, @Operation, @Parameter) 生成 API 文档。
    • 使用 Spring MVC 注解 (@RestController, @RequestMapping, @Validated) 处理 HTTP 请求和参数校验。

方法 1: createFileConfig

@PostMapping("/create")

@Operation(summary = "创建文件配置")

@PreAuthorize("@ss.hasPermission('infra:file-config:create')")

public CommonResult<Long> createFileConfig(@Valid @RequestBody FileConfigSaveReqVO createReqVO) {

return success(fileConfigService.createFileConfig(createReqVO));

}
  • 功能:创建新的文件配置(如 S3、磁盘、数据库存储的配置)。
  • 注解
    • @PostMapping("/create"):处理 POST 请求,路径为 /infra/file-config/create。
    • @Operation(summary = "创建文件配置"):Swagger 文档,描述接口用途。
    • @PreAuthorize("@ss.hasPermission('infra:file-config:create')"):权限校验,要求用户具有 infra:file-config:create 权限(@ss 是一个自定义的 Spring Security 表达式)。
    • @Valid:对请求体进行校验,确保 createReqVO 符合定义的约束。
    • @RequestBody:表示参数从请求体中获取,格式为 JSON。
  • 参数
    • FileConfigSaveReqVO createReqVO:请求体对象,包含文件配置信息,结构如下(来自 FileConfigSaveReqVO 类):
      @Schema(description = "管理后台 - 文件配置创建/修改 Request VO")
      
      @Data
      
      public class FileConfigSaveReqVO {
      
      @Schema(description = "编号", example = "1")
      
      private Long id; // 可为空,创建时不需要
      
      @Schema(description = "配置名", requiredMode = Schema.RequiredMode.REQUIRED, example = "S3 - 阿里云")
      
      @NotNull(message = "配置名不能为空")
      
      private String name; // 配置名称,必填
      
      @Schema(description = "存储器,参见 FileStorageEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
      
      @NotNull(message = "存储器不能为空")
      
      private Integer storage; // 存储器类型,必填,参考 FileStorageEnum(如 S3=20)
      
      @Schema(description = "存储配置,配置是动态参数,所以使用 Map 接收", requiredMode = Schema.RequiredMode.REQUIRED)
      
      @NotNull(message = "存储配置不能为空")
      
      private Map<String, Object> config; // 存储配置,动态参数,必填
      
      @Schema(description = "备注", example = "我是备注")
      
      private String remark; // 备注,可选
      
      }
      • 参数说明
        • id:创建时为空,更新时需要提供。
        • name:配置名称,如“S3 - 阿里云”,用于标识配置。
        • storage:存储器类型,参考 FileStorageEnum(如 S3=20, LOCAL=10, DB=1)。
        • config:存储配置,格式为键值对,根据 storage 类型动态变化。例如:
          • S3 存储:需要 endpoint(节点地址,如 s3.cn-south-1.qiniucs.com)、bucket(存储桶,如 ruoyi-vue-pro)、accessKey、accessSecret、domain(自定义域名,如 http://test.yudao.iocoder.cn)、pathStyle(是否启用 Path Style)。
          • 本地磁盘存储:需要 basePath(存储路径)、domain(访问域名)。
          • 数据库存储:通常无需额外配置。
        • remark:可选备注,用于记录配置的额外信息。
  • 返回值:CommonResult<Long>,包装了创建的文件配置 ID,success 方法封装结果为标准响应格式。
  • 实现
    • 调用 fileConfigService.createFileConfig(createReqVO),将请求 VO 转换为 DO 并保存到数据库,返回新配置的 ID。
    • FileConfigServiceImpl 中会校验 config 参数并根据 storage 类型转换为对应的配置类(如 S3FileClientConfig)。
  • 不同配置的影响
    • S3 存储:需要提供完整的 S3 配置(如 endpoint, bucket, accessKey, accessSecret, domain)。文档中提到,S3 存储支持 HTTP 直接访问,返回的 URL 是 S3 的访问路径(如 http://test.yudao.iocoder.cn/xxx.jpg)。
    • 磁盘存储:需要配置存储路径(如 basePath),文件存储在本地或 FTP/SFTP 服务器,返回的 URL 需要通过后端的 /infra/file/{configId}/get/{path} API 访问。
    • 数据库存储:文件内容存储在数据库中,访问也需要通过后端 API。
    • 前端直传 S3:创建的 S3 配置需要设置跨域(CORS)规则以支持前端直传,config 中需要 domain 字段,且 Bucket 需设置为公共读。

方法 2: updateFileConfig

@PutMapping("/update")

@Operation(summary = "更新文件配置")

@PreAuthorize("@ss.hasPermission('infra:file-config:update')")

public CommonResult<Boolean> updateFileConfig(@Valid @RequestBody FileConfigSaveReqVO updateReqVO) {

fileConfigService.updateFileConfig(updateReqVO);

return success(true);

}
  • 功能:更新现有的文件配置。
  • 注解
    • @PutMapping("/update"):处理 PUT 请求,路径为 /infra/file-config/update。
    • @Operation(summary = "更新文件配置"):Swagger 文档描述。
    • @PreAuthorize("@ss.hasPermission('infra:file-config:update')"):权限校验,要求 infra:file-config:update 权限。
    • @Valid @RequestBody:校验请求体,确保 updateReqVO 合法。
  • 参数
    • FileConfigSaveReqVO updateReqVO:与创建时的 VO 相同,但 id 字段必须提供,用于标识要更新的配置。
      • 参数说明:同 createFileConfig,但 id 必填,config 根据 storage 类型动态更新。
  • 返回值:CommonResult<Boolean>,返回 true 表示更新成功。
  • 实现
    • 调用 fileConfigService.updateFileConfig(updateReqVO),校验配置存在后更新数据库中的记录,并清空相关缓存。
    • FileConfigServiceImpl 会验证 id 对应的配置是否存在,并将 config 转换为对应的配置类。
  • 不同配置的影响
    • S3 存储:更新 config 中的 S3 参数(如 accessKey 或 domain),需要确保新配置符合 S3 协议要求(如公共读 Bucket)。
    • 磁盘存储:更新存储路径或域名,可能影响文件访问 URL。
    • 数据库存储:通常只需更新 name 或 remark,config 较少变化。
    • 前端直传 S3:若更新 domain 或 bucket,需确保前端的跨域配置同步更新,否则可能导致直传失败。

方法 3: updateFileConfigMaster

@PutMapping("/update-master")

@Operation(summary = "更新文件配置为 Master")

@PreAuthorize("@ss.hasPermission('infra:file-config:update')")

public CommonResult<Boolean> updateFileConfigMaster(@RequestParam("id") Long id) {

fileConfigService.updateFileConfigMaster(id);

return success(true);

}
  • 功能:将指定 ID 的文件配置设为主配置(Master),用于默认文件上传。
  • 注解
    • @PutMapping("/update-master"):处理 PUT 请求,路径为 /infra/file-config/update-master。
    • @Operation(summary = "更新文件配置为 Master"):Swagger 文档描述。
    • @PreAuthorize("@ss.hasPermission('infra:file-config:update')"):权限校验,要求 infra:file-config:update 权限。
    • @RequestParam("id"):从查询参数获取配置 ID。
  • 参数
    • Long id:要设为主配置的文件配置 ID。
  • 返回值:CommonResult<Boolean>,返回 true 表示设置成功。
  • 实现
    • 调用 fileConfigService.updateFileConfigMaster(id),先校验配置存在,然后将所有配置的 master 字段设为 false,再将指定 ID 的配置设为 true,并清空 Master 缓存。
    • 主配置用于默认的文件上传操作(如前端直传或后端上传)。
  • 不同配置的影响
    • S3 存储:设为 Master 后,前端直传或后端上传默认使用该 S3 配置,需确保跨域和公共读设置正确。
    • 磁盘存储:设为 Master 后,文件存储在指定路径,访问需通过后端 API。
    • 数据库存储:设为 Master 后,文件存储在数据库,适合小文件场景,访问也需通过后端 API。
    • 前端直传 S3:主配置通常用于前端直传,需确保 domain 和跨域配置正确,否则上传会失败。

方法 4: deleteFileConfig

@DeleteMapping("/delete")

@Operation(summary = "删除文件配置")

@Parameter(name = "id", description = "编号", required = true)

@PreAuthorize("@ss.hasPermission('infra:file-config:delete')")

public CommonResult<Boolean> deleteFileConfig(@RequestParam("id") Long id) {

fileConfigService.deleteFileConfig(id);

return success(true);

}
  • 功能:删除指定 ID 的文件配置。
  • 注解
    • @DeleteMapping("/delete"):处理 DELETE 请求,路径为 /infra/file-config/delete。
    • @Operation(summary = "删除文件配置"):Swagger 文档描述。
    • @Parameter(name = "id", description = "编号", required = true):描述查询参数 id。
    • @PreAuthorize("@ss.hasPermission('infra:file-config:delete')"):权限校验,要求 infra:file-config:delete 权限。
    • @RequestParam("id"):从查询参数获取配置 ID。
  • 参数
    • Long id:要删除的文件配置 ID。
  • 返回值:CommonResult<Boolean>,返回 true 表示删除成功。
  • 实现
    • 调用 fileConfigService.deleteFileConfig(id),校验配置存在且不是主配置(Master 不可删除),然后删除记录并清空缓存。
    • 如果尝试删除主配置,会抛出 FILE_CONFIG_DELETE_FAIL_MASTER 异常。
  • 不同配置的影响
    • S3 存储:删除后,相关 S3 配置不可用,已上传的文件可能仍可通过 S3 URL 访问(需手动清理 Bucket)。
    • 磁盘存储:删除后,相关路径的文件仍存在,需手动清理。
    • 数据库存储:删除后,数据库中的文件记录不受影响,需手动清理。
    • 前端直传 S3:若删除的是主配置,会导致前端直传失败,需重新设置主配置。

方法 5: getFileConfig

@GetMapping("/get")

@Operation(summary = "获得文件配置")

@Parameter(name = "id", description = "编号", required = true, example = "1024")

@PreAuthorize("@ss.hasPermission('infra:file-config:query')")

public CommonResult<FileConfigRespVO> getFileConfig(@RequestParam("id") Long id) {

FileConfigDO config = fileConfigService.getFileConfig(id);

return success(BeanUtils.toBean(config, FileConfigRespVO.class));

}
  • 功能:查询指定 ID 的文件配置详情。
  • 注解
    • @GetMapping("/get"):处理 GET 请求,路径为 /infra/file-config/get。
    • @Operation(summary = "获得文件配置"):Swagger 文档描述。
    • @Parameter(name = "id", description = "编号", required = true, example = "1024"):描述查询参数 id。
    • @PreAuthorize("@ss.hasPermission('infra:file-config:query')"):权限校验,要求 infra:file-config:query 权限。
    • @RequestParam("id"):从查询参数获取配置 ID。
  • 参数
    • Long id:要查询的文件配置 ID。
  • 返回值:CommonResult<FileConfigRespVO>,返回文件配置的 VO 对象,包含配置详情。
  • 实现
    • 调用 fileConfigService.getFileConfig(id) 获取 FileConfigDO。
    • 使用 BeanUtils.toBean 将 DO 转换为 FileConfigRespVO(VO 结构未提供,但通常包含 id, name, storage, config, master, remark 等字段)。
  • 不同配置的影响
    • S3 存储:返回的 config 包含 S3 特定的配置(如 endpoint, bucket)。
    • 磁盘存储:返回的 config 包含路径和域名信息。
    • 数据库存储:返回的 config 可能为空或仅包含基本信息。
    • 前端直传 S3:查询主配置时,返回的 config 用于前端直传的初始化(如获取 domain)。

方法 6: getFileConfigPage

@

GetMapping("/page")

@Operation(summary = "获得文件配置分页")

@PreAuthorize("@ss.hasPermission('infra:file-config:query')")

public CommonResult<PageResult<FileConfigRespVO>> getFileConfigPage(@Valid FileConfigPageReqVO pageVO) {

PageResult<FileConfigDO> pageResult = fileConfigService.getFileConfigPage(pageVO);

return success(BeanUtils.toBean(pageResult, FileConfigRespVO.class));

}
  • 功能:分页查询文件配置列表。
  • 注解
    • @GetMapping("/page"):处理 GET 请求,路径为 /infra/file-config/page。
    • @Operation(summary = "获得文件配置分页"):Swagger 文档描述。
    • @PreAuthorize("@ss.hasPermission('infra:file-config:query')"):权限校验,要求 infra:file-config:query 权限。
    • @Valid:校验分页请求参数。
  • 参数
    • FileConfigPageReqVO pageVO:分页查询参数,结构未提供,但通常包含:
      • name:配置名称(模糊查询)。
      • storage:存储器类型(如 S3=20)。
      • createTime:创建时间范围。
      • pageNo:页码。
      • pageSize:每页大小。
  • 返回值:CommonResult<PageResult<FileConfigRespVO>>,返回分页结果,包含配置列表和总数。
  • 实现
    • 调用 fileConfigService.getFileConfigPage(pageVO) 获取分页的 FileConfigDO 列表。
    • 使用 BeanUtils.toBean 将 PageResult<FileConfigDO> 转换为 PageResult<FileConfigRespVO>。
  • 不同配置的影响
    • S3 存储:分页结果包含 S3 配置,config 字段包含 S3 特定的参数。
    • 磁盘存储:分页结果包含磁盘路径配置。
    • 数据库存储:分页结果包含数据库配置(通常较简单)。
    • 前端直传 S3:分页结果可用于前端展示配置列表,主配置用于直传初始化。

方法 7: testFileConfig

@GetMapping("/test")

@Operation(summary = "测试文件配置是否正确")

@PreAuthorize("@ss.hasPermission('infra:file-config:query')")

public CommonResult<String> testFileConfig(@RequestParam("id") Long id) throws Exception {

String url = fileConfigService.testFileConfig(id);

return success(url);

}
  • 功能:测试指定 ID 的文件配置是否有效,通过上传一个测试文件并返回访问 URL。
  • 注解
    • @GetMapping("/test"):处理 GET 请求,路径为 /infra/file-config/test。
    • @Operation(summary = "测试文件配置是否正确"):Swagger 文档描述。
    • @PreAuthorize("@ss.hasPermission('infra:file-config:query')"):权限校验,要求 infra:file-config:query 权限。
    • @RequestParam("id"):从查询参数获取配置 ID。
  • 参数
    • Long id:要测试的文件配置 ID。
  • 返回值:CommonResult<String>,返回测试文件上传后的访问 URL。
  • 实现
    • 调用 fileConfigService.testFileConfig(id),读取内置的测试文件(erweima.jpg),通过 FileClient 上传并返回 URL。
  • 不同配置的影响
    • S3 存储:测试上传到 S3 Bucket,返回 HTTP 访问 URL(如 http://test.yudao.iocoder.cn/xxx.jpg)。需确保 Bucket 公共读和跨域配置正确。
    • 磁盘存储:测试上传到指定路径,返回的 URL 需通过后端 API 访问(如 /infra/file/{configId}/get/{path})。
    • 数据库存储:测试文件存储到数据库,返回的 URL 也需通过后端 API 访问。
    • 前端直传 S3:测试 URL 可用于验证前端直传的配置是否正确(如跨域和域名设置)。

FileConfigServiceImpl

/**
 * 文件配置 Service 实现类
 */
@Service
@Validated
@Slf4j
public class FileConfigServiceImpl implements FileConfigService {

    private static final Long CACHE_MASTER_ID = 0L;

    /**
     * {@link FileClient} 缓存,通过它异步刷新 fileClientFactory
     *  这个缓存是连接文件配置(FileConfigDO)和文件操作客户端(FileClient)的中间层
     */
    @Getter
    private final LoadingCache<Long, FileClient> clientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),
            new CacheLoader<Long, FileClient>() {

                @Override
                public FileClient load(Long id) {
                    FileConfigDO config = Objects.equals(CACHE_MASTER_ID, id) ?
                            fileConfigMapper.selectByMaster() : fileConfigMapper.selectById(id);
                    if (config != null) {
                        fileClientFactory.createOrUpdateFileClient(config.getId(), config.getStorage(), config.getConfig());
                    }
                    return fileClientFactory.getFileClient(null == config ? id : config.getId());
                }

            });

    @Resource
    private FileClientFactory fileClientFactory;

    @Resource
    private FileConfigMapper fileConfigMapper;

    @Resource
    private Validator validator;

    @Override
    public Long createFileConfig(FileConfigSaveReqVO createReqVO) {
        FileConfigDO fileConfig = FileConfigConvert.INSTANCE.convert(createReqVO)
                .setConfig(parseClientConfig(createReqVO.getStorage(), createReqVO.getConfig()))
                .setMaster(false); // 默认非 master
        fileConfigMapper.insert(fileConfig);
        return fileConfig.getId();
    }

    @Override
    public void updateFileConfig(FileConfigSaveReqVO updateReqVO) {
        // 校验存在
        FileConfigDO config = validateFileConfigExists(updateReqVO.getId());
        // 更新
        FileConfigDO updateObj = FileConfigConvert.INSTANCE.convert(updateReqVO)
                .setConfig(parseClientConfig(config.getStorage(), updateReqVO.getConfig()));
        fileConfigMapper.updateById(updateObj);

        // 清空缓存
        clearCache(config.getId(), null);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateFileConfigMaster(Long id) {
        // 校验存在
        validateFileConfigExists(id);
        // 更新其它为非 master
        fileConfigMapper.updateBatch(new FileConfigDO().setMaster(false));
        // 更新
        fileConfigMapper.updateById(new FileConfigDO().setId(id).setMaster(true));

        // 清空缓存
        clearCache(null, true);
    }

    private FileClientConfig parseClientConfig(Integer storage, Map<String, Object> config) {
        // 获取配置类
        Class<? extends FileClientConfig> configClass = FileStorageEnum.getByStorage(storage)
                .getConfigClass();
        FileClientConfig clientConfig = JsonUtils.parseObject2(JsonUtils.toJsonString(config), configClass);
        // 参数校验
        ValidationUtils.validate(validator, clientConfig);
        // 设置参数
        return clientConfig;
    }

    @Override
    public void deleteFileConfig(Long id) {
        // 校验存在
        FileConfigDO config = validateFileConfigExists(id);
        if (Boolean.TRUE.equals(config.getMaster())) {
            throw exception(FILE_CONFIG_DELETE_FAIL_MASTER);
        }
        // 删除
        fileConfigMapper.deleteById(id);

        // 清空缓存
        clearCache(id, null);
    }

    /**
     * 清空指定文件配置
     *
     * @param id 配置编号
     * @param master 是否主配置
     */
    private void clearCache(Long id, Boolean master) {
        if (id != null) {
            clientCache.invalidate(id);
        }
        if (Boolean.TRUE.equals(master)) {
            clientCache.invalidate(CACHE_MASTER_ID);
        }
    }

    private FileConfigDO validateFileConfigExists(Long id) {
        FileConfigDO config = fileConfigMapper.selectById(id);
        if (config == null) {
            throw exception(FILE_CONFIG_NOT_EXISTS);
        }
        return config;
    }

    @Override
    public FileConfigDO getFileConfig(Long id) {
        return fileConfigMapper.selectById(id);
    }

    @Override
    public PageResult<FileConfigDO> getFileConfigPage(FileConfigPageReqVO pageReqVO) {
        return fileConfigMapper.selectPage(pageReqVO);
    }

    @Override
    public String testFileConfig(Long id) throws Exception {
        // 校验存在
        validateFileConfigExists(id);
        // 上传文件
        byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
        return getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg");
    }

    @Override
    public FileClient getFileClient(Long id) {
        return clientCache.getUnchecked(id);
    }

    @Override
    public FileClient getMasterFileClient() {
        return clientCache.getUnchecked(CACHE_MASTER_ID);
    }
}

FileConfigServiceImpl 提供了 FileConfigController 的核心逻辑,以下是一些关键点,与不同配置方式相关:

  • 缓存机制
    • 使用 LoadingCache<Long, FileClient> 缓存 FileClient 实例,CACHE_MASTER_ID=0L 用于主配置。
    • 每次创建、更新、删除或设置主配置时,会清空相关缓存以确保数据一致性。
  • 配置解析
    • parseClientConfig 方法根据 storage 类型将 config Map 转换为特定配置类(如 S3FileClientConfig),并进行参数校验。
    • S3 配置需要更多字段(如 endpoint, bucket),而磁盘存储需要 basePath,数据库存储配置较简单。
  • 文件客户端
    • FileClientFactoryImpl 根据 storage 类型创建对应的 FileClient(如 S3FileClient, LocalFileClient, DBFileClient)。
    • S3 存储支持直接 HTTP 访问,磁盘和数据库存储需通过后端 API。

不同配置方式的比较

根据文档和代码,以下是 S3 对象存储(包括前端直传)、磁盘存储、数据库存储的对比,以及对 FileConfigController 方法的影响:

特性 S3 对象存储 磁盘存储(本地/FTP/SFTP) 数据库存储
存储方式 文件存储在云服务(如 MinIO、七牛云),支持 HTTP 访问 文件存储在本地磁盘或远程 FTP/SFTP 服务器 文件内容存储在数据库(如 MySQL)
配置参数 endpoint, bucket, accessKey, accessSecret, domain, pathStyle basePath, domain 无需复杂配置
访问方式 直接通过 S3 返回的 URL 访问(如 http://test.yudao.iocoder.cn/xxx.jpg) 通过后端 API /infra/file/{configId}/get/{path} 访问 通过后端 API /infra/file/{configId}/get/{path} 访问
前端直传支持 支持,需配置跨域(CORS)和公共读 Bucket,VITE_UPLOAD_TYPE=client 不支持,需经过后端上传 不支持,需经过后端上传
性能 高性能,适合大文件和并发上传,流量不经过后端 性能受磁盘 I/O 或网络限制,流量经过后端 适合小文件,性能受数据库限制
高可用性 高,依赖云服务提供商的 HA 机制 较低,需自行实现高可用(如 RAID 或主从) 高,依赖数据库主从机制
推荐场景 大规模文件存储、前端直传、分布式系统 小规模本地存储或已有 FTP/SFTP 环境 小文件存储、备份方便
Controller 方法影响 - createFileConfig/updateFileConfig:需提供完整 S3 配置,校验严格 - testFileConfig:返回 S3 URL - updateFileConfigMaster:设为主配置后用于直传 - createFileConfig/updateFileConfig:配置简单,仅需路径 - testFileConfig:返回需通过后端 API 的 URL - createFileConfig/updateFileConfig:配置简单 - testFileConfig:返回需通过后端 API 的 URL
前端直传配置 需设置跨域和 VITE_UPLOAD_TYPE=client,主配置用于直传初始化 不支持直传 不支持直传

FileController

@Tag(name = "管理后台 - 文件存储")
@RestController
@RequestMapping("/infra/file")
@Validated
@Slf4j
public class FileController {

    @Resource
    private FileService fileService;

    @PostMapping("/upload")
    @Operation(summary = "上传文件", description = "模式一:后端上传文件")
    public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception {
        MultipartFile file = uploadReqVO.getFile();
        String path = uploadReqVO.getPath();
        return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream())));
    }

    @GetMapping("/presigned-url")
    @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
    public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception {
        return success(fileService.getFilePresignedUrl(path));
    }

    @PostMapping("/create")
    @Operation(summary = "创建文件", description = "模式二:前端上传文件:配合 presigned-url 接口,记录上传的文件")
    public CommonResult<Long> createFile(@Valid @RequestBody FileCreateReqVO createReqVO) {
        return success(fileService.createFile(createReqVO));
    }

    @DeleteMapping("/delete")
    @Operation(summary = "删除文件")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('infra:file:delete')")
    public CommonResult<Boolean> deleteFile(@RequestParam("id") Long id) throws Exception {
        fileService.deleteFile(id);
        return success(true);
    }

    @GetMapping("/{configId}/get/**")
    @PermitAll
    @Operation(summary = "下载文件")
    @Parameter(name = "configId", description = "配置编号", required = true)
    public void getFileContent(HttpServletRequest request,
                               HttpServletResponse response,
                               @PathVariable("configId") Long configId) throws Exception {
        // 获取请求的路径
        String path = StrUtil.subAfter(request.getRequestURI(), "/get/", false);
        if (StrUtil.isEmpty(path)) {
            throw new IllegalArgumentException("结尾的 path 路径必须传递");
        }
        // 解码,解决中文路径的问题 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/807/
        path = URLUtil.decode(path);

        // 读取内容
        byte[] content = fileService.getFileContent(configId, path);
        if (content == null) {
            log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path);
            response.setStatus(HttpStatus.NOT_FOUND.value());
            return;
        }
        FileTypeUtils.writeAttachment(response, path, content);
    }

    @GetMapping("/page")
    @Operation(summary = "获得文件分页")
    @PreAuthorize("@ss.hasPermission('infra:file:query')")
    public CommonResult<PageResult<FileRespVO>> getFilePage(@Valid FilePageReqVO pageVO) {
        PageResult<FileDO> pageResult = fileService.getFilePage(pageVO);
        return success(BeanUtils.toBean(pageResult, FileRespVO.class));
    }

}

createFile(String name, String path, byte[] content)

其中下面一句最为重要:

FileClient client = fileConfigService.getMasterFileClient();

调用 FileConfigServiceImpl.getMasterFileClient:

@Override

public FileClient getMasterFileClient() {

return clientCache.getUnchecked(CACHE_MASTER_ID);

}
  • 动态生成 FileClient
    • clientCache 是 LoadingCache<Long, FileClient>,键为 CACHE_MASTER_ID=0L。
    • 调用 getUnchecked(0L),触发 CacheLoader.load:
/**
     * {@link FileClient} 缓存,通过它异步刷新 fileClientFactory
     *  这个缓存是连接文件配置(FileConfigDO)和文件操作客户端(FileClient)的中间层
     */
    @Getter
    private final LoadingCache<Long, FileClient> clientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),
            new CacheLoader<Long, FileClient>() {

                @Override
                public FileClient load(Long id) {
                    FileConfigDO config = Objects.equals(CACHE_MASTER_ID, id) ?
                            fileConfigMapper.selectByMaster() : fileConfigMapper.selectById(id);
                    if (config != null) {
                        fileClientFactory.createOrUpdateFileClient(config.getId(), config.getStorage(), config.getConfig());
                    }
                    return fileClientFactory.getFileClient(null == config ? id : config.getId());
                }

            });
  • 查询主配置
    • fileConfigMapper.selectByMaster:
      SELECT * FROM file_config WHERE master = 1;
      • 返回 FileConfigDO(id=1, storage=1, master=true)。
  • 创建或更新 FileClient
    • 调用 fileClientFactory.createOrUpdateFileClient:
      public <Config extends FileClientConfig> void createOrUpdateFileClient(Long configId, Integer storage, Config config) {
      
      AbstractFileClient<?> client = clients.get(configId);
      
      if (client == null) {
      
      client = this.createFileClient(configId, storage, config);
      
      client.init();
      
      clients.put(client.getId(), client);
      
      } else {
      
      client.refresh(config);
      
      }
      
      }
      • configId=1, storage=1(FileStorageEnum.DB)。
      • 如果 clients 中无 configId=1,调用 createFileClient:
        • 根据 storage=1,创建 DBFileClient 实例。
        • 调用 client.init() 初始化。
        • 存入 clients(Map<Long, AbstractFileClient<?>>)。
      • 如果存在,调用 client.refresh(config) 更新配置。
    • 存入 clientCache(键为 0L)。
  • 返回 FileClient:DBFileClient(configId=1)。

假如我们创建的是一个S3FileClient:
 

/**
 * 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
 * <p>
 * S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库
 *
 * @author 芋道源码
 */
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {

    private AmazonS3Client client;

    public S3FileClient(Long id, S3FileClientConfig config) {
        super(id, config);
    }

    @Override
    protected void doInit() {
        // 补全 domain
        if (StrUtil.isEmpty(config.getDomain())) {
            config.setDomain(buildDomain());
        }
        // 初始化客户端
        client = (AmazonS3Client)AmazonS3ClientBuilder.standard()
                .withCredentials(buildCredentials())
                .withEndpointConfiguration(buildEndpointConfiguration())
                .build();
    }

    /**
     * 基于 config 秘钥,构建 S3 客户端的认证信息
     * 通过配置中的accessKey(访问密钥 ID)和accessSecret(访问密钥密钥),构建BasicAWSCredentials对象,用于 S3 服务的身份验证(确保上传操作有权限)
     * @return S3 客户端的认证信息
     */
    private AWSStaticCredentialsProvider buildCredentials() {
        return new AWSStaticCredentialsProvider(
                new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret()));
    }

    /**
     * 构建 S3 客户端的 Endpoint 配置,包括 region、endpoint
     * 构建服务端点配置(buildEndpointConfiguration)
     * @return  S3 客户端的 EndpointConfiguration 配置
     */
    private AwsClientBuilder.EndpointConfiguration buildEndpointConfiguration() {
        return new AwsClientBuilder.EndpointConfiguration(config.getEndpoint(),
                null); // 无需设置 region
    }

    /**
     * 基于 bucket + endpoint 构建访问的 Domain 地址
     * 若endpoint是 HTTP/HTTPS 地址(如 MinIO 的http://127.0.0.1:9000),
     * 则域名格式为endpoint/bucket(如http://127.0.0.1:9000/my-bucket)
     * 若endpoint是普通域名(如阿里云 OSS 的oss-cn-beijing.aliyuncs.com),
     * 则域名格式为https://bucket.endpoint(如https://my-bucket.oss-cn-beijing.aliyuncs.com)
     * @return Domain 地址
     */
    private String buildDomain() {
        // 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO
        if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
            return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());
        }
        // 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名
        return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
    }

    @Override
    public String upload(byte[] content, String path, String type) throws Exception {
        // 元数据,主要用于设置文件类型
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentType(type);
        objectMetadata.setContentLength(content.length); // 如果不设置,会有 “ No content length specified for stream data” 警告日志
        // 执行上传
        client.putObject(config.getBucket(),
                path, // 相对路径
                new ByteArrayInputStream(content), // 文件内容
                objectMetadata);

        // 拼接返回路径
        return config.getDomain() + "/" + path;
    }

    @Override
    public void delete(String path) throws Exception {
        client.deleteObject(config.getBucket(), path);
    }

    @Override
    public byte[] getContent(String path) throws Exception {
        S3Object tempS3Object = client.getObject(config.getBucket(), path);
        return IoUtil.readBytes(tempS3Object.getObjectContent());
    }

    @Override
    public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception {
        // 设定过期时间为 10 分钟。取值范围:1 秒 ~ 7 天
        Date expiration = new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(10));
        // 生成上传 URL
        String uploadUrl = String.valueOf(client.generatePresignedUrl(config.getBucket(), path, expiration , HttpMethod.PUT));
        return new FilePresignedUrlRespDTO(uploadUrl, config.getDomain() + "/" + path);
    }

}

开始时会调用

public S3FileClient(Long id, S3FileClientConfig config) {
        super(id, config);
    }

    @Override
    protected void doInit() {
        // 补全 domain
        if (StrUtil.isEmpty(config.getDomain())) {
            config.setDomain(buildDomain());
        }
        // 初始化客户端
        client = (AmazonS3Client)AmazonS3ClientBuilder.standard()
                .withCredentials(buildCredentials())
                .withEndpointConfiguration(buildEndpointConfiguration())
                .build();
    }

调用父类的构造方法,来实现初始化

/**
     * 基于 bucket + endpoint 构建访问的 Domain 地址
     * 若endpoint是 HTTP/HTTPS 地址(如 MinIO 的http://127.0.0.1:9000),
     * 则域名格式为endpoint/bucket(如http://127.0.0.1:9000/my-bucket)
     * 若endpoint是普通域名(如阿里云 OSS 的oss-cn-beijing.aliyuncs.com),
     * 则域名格式为https://bucket.endpoint(如https://my-bucket.oss-cn-beijing.aliyuncs.com)
     * @return Domain 地址
     */
    private String buildDomain() {
        // 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO
        if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
            return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());
        }
        // 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名
        return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
    }
/**
     * 基于 config 秘钥,构建 S3 客户端的认证信息
     * 通过配置中的accessKey(访问密钥 ID)和accessSecret(访问密钥密钥),构建BasicAWSCredentials对象,用于 S3 服务的身份验证(确保上传操作有权限)
     * @return S3 客户端的认证信息
     */
    private AWSStaticCredentialsProvider buildCredentials() {
        return new AWSStaticCredentialsProvider(
                new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret()));
    }
    /**
     * 构建 S3 客户端的 Endpoint 配置,包括 region、endpoint
     * 构建服务端点配置(buildEndpointConfiguration)
     * @return  S3 客户端的 EndpointConfiguration 配置
     */
    private AwsClientBuilder.EndpointConfiguration buildEndpointConfiguration() {
        return new AwsClientBuilder.EndpointConfiguration(config.getEndpoint(),
                null); // 无需设置 region
    }

然后逐步初始化即可;

主配置父类为:FileClientConfig,每个不同的文件上传方式对应不同的配置类

主客户端父类为:AbstractFileClient IM FileClient

createFile(FileCreateReqVO createReqVO)

直接创建文件元信息记录(不上传文件内容),用于前端直传模式(如 S3)后记录元信息

@Override
public Long createFile(FileCreateReqVO createReqVO) {
    FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);
    fileMapper.insert(file);
    return file.getId();
}

参数

  • FileCreateReqVO createReqVO:包含:
    • configId:配置 ID。
    • name:文件名。
    • path:文件路径。
    • url:访问 URL。
    • type:文件类型。
    • size:文件大小。

deleteFile

删除指定 ID 的文件,包括存储器中的内容和 file 表中的元信息

//删除指定 ID 的文件,包括存储器中的内容和 file 表中的元信息
    @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);
    }

getFileContent

获取指定配置和路径的文件内容。

  • 功能:获取指定配置和路径的文件内容。
  • 参数
    • configId:配置 ID(如 1)。
    • path:文件路径(如 /root/home/pic/test.jpg)。
  • 返回值:byte[],文件二进制内容。
  • 实现
    • 获取 FileClient:

      FileClient client = fileConfigService.getFileClient(configId);

      • 调用 FileConfigServiceImpl.getFileClient(1),返回 其子类,可能是任意一种子类(从 clientCache 获取)。
    • 调用 (以DBF举例)DBFileClient.getContent:
      public byte[] getContent(String path) {
      
      FileContentDO fileContent = fileContentMapper.selectOne(
      
      FileContentDO::getConfigId, getId(),
      
      FileContentDO::getPath, path);
      
      return fileContent != null ? fileContent.getContent() : null;
      
      }
      • 查询 file_content 表:

        SELECT content FROM file_content WHERE config_id = 1 AND path = '/root/home/pic/test.jpg';

  • 数据库存储场景
    • 返回 test.jpg 的二进制内容,用于 /infra/file/1/get/root/home/pic/test.jpg API

getFilePresignedUrl

@Override
    public FilePresignedUrlRespVO getFilePresignedUrl(String path) throws Exception {
        FileClient fileClient = fileConfigService.getMasterFileClient();
        FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path);
        return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class,
                object -> object.setConfigId(fileClient.getId()));
    }
  • 功能:获取文件的预签名 URL,用于前端直传(如 S3)。
  • 参数
    • path:文件路径(如 /root/home/pic/test.jpg)。
  • 返回值:FilePresignedUrlRespVO,包含预签名 URL 和配置 ID。
  • 实现
    • 获取主配置的 FileClient(DBFileClient)。
    • 调用 getPresignedObjectUrl:
      • 数据库存储不支持预签名,抛出异常或返回空。
    • 转换 FilePresignedUrlRespDTO 为 FilePresignedUrlRespVO。
  • 数据库存储场景
    • 不支持前端直传,调用无效。
    • 不适用于本次模拟。

动态生成 FileClient 的逻辑

FileClient 的动态生成是文件上传逻辑的核心,依赖 FileConfigServiceImpl 的 clientCache 和 FileClientFactory。以下详细分析:

1. clientCache 和 FileConfigServiceImpl.getMasterFileClient

private static final Long CACHE_MASTER_ID = 0L;

@Getter

private final LoadingCache<Long, FileClient> clientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),

new CacheLoader<Long, FileClient>() {

@Override

public FileClient load(Long id) {

FileConfigDO config = Objects.equals(CACHE_MASTER_ID, id) ?

fileConfigMapper.selectByMaster() : fileConfigMapper.selectById(id);

if (config != null) {

fileClientFactory.createOrUpdateFileClient(config.getId(), config.getStorage(), config.getConfig());

}

return fileClientFactory.getFileClient(null == config ? id : config.getId());

}

});

@Override

public FileClient getMasterFileClient() {

return clientCache.getUnchecked(CACHE_MASTER_ID);

}
  • 作用
    • clientCache 是一个 Guava LoadingCache,缓存 FileClient 实例,键为配置 ID(0L 表示主配置)。
    • getMasterFileClient 获取主配置的 FileClient(DBFileClient)。
  • 动态生成逻辑
    1. 调用 getUnchecked(CACHE_MASTER_ID)
      • 检查 clientCache 是否有键 0L 的缓存。
      • 若缓存命中,直接返回 DBFileClient。
      • 若未命中,触发 CacheLoader.load(0L)。
    2. 查询主配置
      • fileConfigMapper.selectByMaster:

        sql

        SELECT * FROM file_config WHERE master = 1;

        • 返回 FileConfigDO(id=1, storage=1, config={}, master=true)。
    3. 创建或更新 FileClient
      • 调用 fileClientFactory.createOrUpdateFileClient(1, 1, config):
        public <Config extends FileClientConfig> void createOrUpdateFileClient(Long configId, Integer storage, Config config) {
        
        AbstractFileClient<?> client = clients.get(configId);
        
        if (client == null) {
        
        client = this.createFileClient(configId, storage, config);
        
        client.init();
        
        clients.put(client.getId(), client);
        
        } else {
        
        client.refresh(config);
        
        }
        
        }
        • 检查 clients(Map<Long, AbstractFileClient<?>>)是否已有 configId=1。
        • 创建新客户端
          • storage=1(FileStorageEnum.DB)。
          • createFileClient 创建 DBFileClient:
            • 设置 configId=1。
            • config 解析为 DBFileClientConfig(可能为空对象 {})。
          • 调用 client.init() 初始化。
          • 存入 clients(键为 1)。
        • 更新现有客户端
          • 调用 client.refresh(config),更新配置。
        • 存入 clientCache(键为 0L)。
    4. 返回 FileClient
      • 返回 DBFileClient(configId=1)。
  • 存储时机
    • 初次生成:第一次调用 getMasterFileClient 时,CacheLoader.load 创建 DBFileClient 并存入 clientCache。
    • 配置变更
      • 创建配置(/infra/file-config/create):插入 file_config,默认 master=false。
      • 设为主配置(/infra/file-config/update-master):
        @Override
        
        @Transactional(rollbackFor = Exception.class)
        
        public void updateFileConfigMaster(Long id) {
        
        validateFileConfigExists(id);
        
        fileConfigMapper.updateBatch(new FileConfigDO().setMaster(false));
        
        fileConfigMapper.updateById(new FileConfigDO().setId(id).setMaster(true));
        
        clearCache(null, true);
        
        }
        • 更新 file_config 表:

          sql

          UPDATE file_config SET master = 0;

          UPDATE file_config SET master = 1 WHERE id = 1;

        • 清除缓存:
          private void clearCache(Long id, Boolean master) {
          
          if (id != null) {
          
          clientCache.invalidate(id);
          
          }
          
          if (Boolean.TRUE.equals(master)) {
          
          clientCache.invalidate(CACHE_MASTER_ID);
          
          }
          
          }
          • 清除 CACHE_MASTER_ID=0L 缓存。
        • 下次调用 getMasterFileClient 时,重新加载 DBFileClient。
      • 更新或删除配置:清除对应 clientCache 缓存。
    • 缓存刷新:每 10 秒(Duration.ofSeconds(10L))异步刷新 clientCache。

文件上传整体流程(数据库存储)

结合 FileController.uploadFile 和 FileServiceImpl.createFile,以下是上传 test.jpg 的完整流程:

  1. 前端请求

    POST /infra/file/upload

    Content-Type: multipart/form-data

    Authorization: Bearer <token>

    ------WebKitFormBoundary

    Content-Disposition: form-data; name="file"; filename="test.jpg"

    Content-Type: image/jpeg

    <binary_data>

    ------WebKitFormBoundary

    Content-Disposition: form-data; name="path"

    /root/home/pic/test.jpg

    ------WebKitFormBoundary--

  2. 控制器处理(FileController.uploadFile):

    @PostMapping("/upload")

    @Operation(summary = "上传文件", description = "模式一:后端上传文件")

    public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception {

    MultipartFile file = uploadReqVO.getFile();

    String path = uploadReqVO.getPath();

    return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream())));

    }

    • 解析 MultipartFile,提取 name="test.jpg", path=/root/home/pic/test.jpg, content=<binary_data>。
    • 调用 fileService.createFile("test.jpg", "/root/home/pic/test.jpg", <binary_data>).
  3. 服务层处理(FileServiceImpl.createFile):
    • 文件类型:FileTypeUtils.getMineType 返回 image/jpeg。
    • 路径和文件名:使用提供的 path 和 name。
    • 获取 FileClient
      • fileConfigService.getMasterFileClient() 返回 DBFileClient(动态生成,configId=1)。
    • 上传
      • DBFileClient.upload 插入 file_content 表:

        INSERT INTO file_content (config_id, path, content, type, create_time)

        VALUES (1, '/root/home/pic/test.jpg', <binary_data>, 'image/jpeg', '2025-08-04 18:47:00');

      • 返回 URL:/infra/file/1/get/root/home/pic/test.jpg.
    • 保存元信息
      • 插入 file 表:

        INSERT INTO file (config_id, name, path, url, type, size, create_time)

        VALUES (1, 'test.jpg', '/root/home/pic/test.jpg', '/infra/file/1/get/root/home/pic/test.jpg', 'image/jpeg', <size>, '2025-08-04 18:47:00');

    • 返回 URL。
  4. 响应

    {

    "code": 200,

    "data": "/infra/file/1/get/root/home/pic/test.jpg",

    "msg": "success"

    }


关键点和注意事项

  • 动态生成 FileClient
    • clientCache 确保高效获取 FileClient,延迟加载减少开销。
    • CacheLoader.load 根据 storage(如 1=DB)创建具体客户端(DBFileClient)。
    • 配置变更(create, update, delete, update-master)触发 clearCache,保证客户端与最新配置同步。
  • 数据库存储特点
    • 文件内容存储在 file_content.content(LONGBLOB)。
    • 访问需通过 /infra/file/{configId}/get/{path},流量经过后端。
    • 适合小文件(<1MB),大文件影响性能。
  • 错误处理
    • 配置不存在:抛出 FILE_CONFIG_NOT_EXISTS。
    • 客户端为空:抛出 Assert.notNull 异常。
    • 数据库插入失败:需事务回滚。

总结

FileServiceImpl 方法作用

  • getFilePage:分页查询文件元信息。
  • createFile(String, String, byte[]):上传文件到数据库,保存元信息,返回逻辑 URL。
  • createFile(FileCreateReqVO):记录前端直传文件的元信息(不适用于数据库存储)。
  • deleteFile:删除文件内容和元信息。
  • getFileContent:获取文件内容,支持访问。
  • getFilePresignedUrl:获取预签名 URL(数据库存储无效)。

动态生成 FileClient

  • 通过 clientCache.getUnchecked(CACHE_MASTER_ID) 获取 DBFileClient。
  • 初次调用时,CacheLoader.load 查询主配置,创建 DBFileClient,存入缓存。
  • 配置变更(如设为主配置)清除缓存,确保动态加载最新配置。

文件上传逻辑

  • 前端上传 test.jpg 到 /infra/file/upload。
  • FileController.uploadFile 解析请求,调用 FileServiceImpl.createFile。
  • FileServiceImpl 使用 DBFileClient 存储文件内容到 file_content,元信息到 file,返回逻辑 URL。

网站公告

今日签到

点亮在社区的每一天
去签到