当前位置: 首页 > news >正文

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

Controller

FileConfigController

@Tag(name = "管理后台 - 文件配置")
@RestController
@RequestMapping("/infra/file-config")
@Validated
public class FileConfigController {@Resourceprivate 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")@Datapublic 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)的中间层*/@Getterprivate final LoadingCache<Long, FileClient> clientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),new CacheLoader<Long, FileClient>() {@Overridepublic 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());}});@Resourceprivate FileClientFactory fileClientFactory;@Resourceprivate FileConfigMapper fileConfigMapper;@Resourceprivate Validator validator;@Overridepublic Long createFileConfig(FileConfigSaveReqVO createReqVO) {FileConfigDO fileConfig = FileConfigConvert.INSTANCE.convert(createReqVO).setConfig(parseClientConfig(createReqVO.getStorage(), createReqVO.getConfig())).setMaster(false); // 默认非 masterfileConfigMapper.insert(fileConfig);return fileConfig.getId();}@Overridepublic 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);// 更新其它为非 masterfileConfigMapper.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;}@Overridepublic 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;}@Overridepublic FileConfigDO getFileConfig(Long id) {return fileConfigMapper.selectById(id);}@Overridepublic PageResult<FileConfigDO> getFileConfigPage(FileConfigPageReqVO pageReqVO) {return fileConfigMapper.selectPage(pageReqVO);}@Overridepublic 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");}@Overridepublic FileClient getFileClient(Long id) {return clientCache.getUnchecked(id);}@Overridepublic 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, pathStylebasePath, 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 {@Resourceprivate 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:

@Overridepublic 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)的中间层*/@Getterprivate final LoadingCache<Long, FileClient> clientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),new CacheLoader<Long, FileClient>() {@Overridepublic 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);}@Overrideprotected void doInit() {// 补全 domainif (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,则不进行拼接.主要适配 MinIOif (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());}// 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());}@Overridepublic 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;}@Overridepublic void delete(String path) throws Exception {client.deleteObject(config.getBucket(), path);}@Overridepublic byte[] getContent(String path) throws Exception {S3Object tempS3Object = client.getObject(config.getBucket(), path);return IoUtil.readBytes(tempS3Object.getObjectContent());}@Overridepublic FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception {// 设定过期时间为 10 分钟。取值范围:1 秒 ~ 7 天Date expiration = new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(10));// 生成上传 URLString 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);}@Overrideprotected void doInit() {// 补全 domainif (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,则不进行拼接.主要适配 MinIOif (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 表中的元信息@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);}

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

@Overridepublic 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;@Getterprivate final LoadingCache<Long, FileClient> clientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),new CacheLoader<Long, FileClient>() {@Overridepublic 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());}});@Overridepublic 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。
http://www.dtcms.com/a/316888.html

相关文章:

  • 《手撕设计模式》系列导学目录
  • 仓颉编程语言的基本概念:标识符、变量、注释
  • 信息安全概述--实验总结
  • mcu中的RC振荡器(Resistor-Capacitor Oscillator)
  • 企业高性能web服务器
  • 【docker】UnionFS联合操作系统
  • 2025年渗透测试面试题总结-02(题目+回答)
  • 一种红外遥控RGB灯带控制器-最低价MCU
  • Redis类型之String
  • linux-单用户模式、营救模式
  • 高阶 RAG :技术体系串联与实际落地指南​
  • RHCA03--硬件监控及内核模块调优
  • 【Spring Cloud】-- RestTeplate实现远程调用
  • Java开发时出现的问题---并发与资源管理深层问题
  • 嵌入式开发学习———Linux环境下IO进程线程学习(四)
  • 《嵌入式数据结构笔记(三):数据结构基础与双向链表》
  • 应急响应排查思路
  • MLIR Bufferization
  • JWT 签名验证失败:Java 与 PHP 互操作问题解决方案
  • OpenHarmony 5.0 Settings中wifi删除密码立刻去输入密码连接,连接不上
  • 性能测试终极指南:从指标到实战
  • 自动驾驶中的传感器技术24——Camera(15)
  • Mybatis的应用及部分特性
  • 机器学习——集成学习(Ensemble Learning):随机森林(Random Forest),AdaBoost、Gradient Boosting,Stacking
  • 企业级Redis Cluster部署详解及演练
  • 森赛睿科技成为机器视觉产业联盟会员单位
  • 解决cv::dilate处理后的图像边缘像素出现异常值的问题
  • 结构化设计工具与技术详解
  • Spring 的优势
  • 内部排序算法总结(考研向)