diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/FileController.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/FileController.java index 702db48..16fb56f 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/FileController.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/FileController.java @@ -51,7 +51,8 @@ public class FileController { byte[] content = IoUtil.readBytes(file.getInputStream()); FileDO fileDO = fileService.createFile(content, file.getOriginalFilename(), uploadReqVO.getDirectory(), file.getContentType()); - return success(new FileUploadRespVO(String.valueOf(fileDO.getId()), fileDO.getUrl())); + return success(new FileUploadRespVO(String.valueOf(fileDO.getId()), + fileDO.getConfigId(), fileDO.getPath(), fileDO.getUrl())); } @GetMapping("/presigned-url") @@ -115,8 +116,7 @@ public class FileController { @GetMapping("/{configId}/get/**") @PermitAll - - @Operation(summary = "下载文件") + @Operation(summary = "获取文件内容(匿名)", description = "富文本 等匿名场景使用:图片走 inline 内联渲染,其它类型走 attachment 下载") @Parameter(name = "configId", description = "配置编号", required = true) public void getFileContent(HttpServletRequest request, HttpServletResponse response, diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/vo/file/FileUploadRespVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/vo/file/FileUploadRespVO.java index e09a244..1a8ab45 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/vo/file/FileUploadRespVO.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/vo/file/FileUploadRespVO.java @@ -14,6 +14,12 @@ public class FileUploadRespVO { @Schema(description = "文件编号,Long 以字符串返回,避免前端精度丢失", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private String id; + @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "22") + private Long configId; + + @Schema(description = "文件相对路径,含目录与文件名", requiredMode = Schema.RequiredMode.REQUIRED, example = "20260514/avatar_1747273800000.png") + private String path; + @Schema(description = "文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/rdms.jpg") private String url; diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/file/FileMapper.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/file/FileMapper.java index 8a19781..e12c5f4 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/file/FileMapper.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/file/FileMapper.java @@ -41,4 +41,18 @@ public interface FileMapper extends BaseMapperX { return file; } + /** + * 按 (configId, path) 查询文件记录 + * + * 用于匿名图片代理接口 {@code GET /system/file/{configId}/get/**} 的存在性校验: + * 当 infra_file 已被逻辑删除时,MyBatis-Plus 全局过滤会自动加上 deleted=0, + * 这里查不到即视为已删除,由调用方返 404 给浏览器(裂图),避免外部探测到对象存储仍可读 + */ + default FileDO selectByConfigIdAndPath(Long configId, String path) { + return selectOne(new LambdaQueryWrapperX() + .eq(FileDO::getConfigId, configId) + .eq(FileDO::getPath, path) + .last("LIMIT 1")); + } + } diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/framework/file/core/utils/FileTypeUtils.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/framework/file/core/utils/FileTypeUtils.java index fb604de..3f90ce0 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/framework/file/core/utils/FileTypeUtils.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/framework/file/core/utils/FileTypeUtils.java @@ -2,6 +2,7 @@ package com.njcn.rdms.module.system.framework.file.core.utils; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; import com.njcn.rdms.framework.common.util.http.HttpUtils; import jakarta.servlet.http.HttpServletResponse; import lombok.SneakyThrows; @@ -86,6 +87,10 @@ public class FileTypeUtils { if (isImage(mineType)) { // 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论 response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename)); + // 图片匿名代理的主要消费方是富文本 ,反复访问同一张图,下发缓存头让浏览器 max-age 内不再回源 + // ETag 用字节 sha256,让 CDN / 网关层有条件做 304 命中 + response.setHeader("Cache-Control", "public, max-age=86400"); + response.setHeader("ETag", "\"" + DigestUtil.sha256Hex(content) + "\""); } else { response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); } diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/file/FileServiceImpl.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/file/FileServiceImpl.java index b827f7c..02d17d3 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/file/FileServiceImpl.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/file/FileServiceImpl.java @@ -207,6 +207,12 @@ public class FileServiceImpl implements FileService { @Override public byte[] getFileContent(Long configId, String path) throws Exception { + // 软删 / 不存在的记录直接拒绝 + // 该方法被匿名图片代理 (FileController#getFileContent) 复用,必须先按 DB 校验存在性, + // 否则历史路径在 infra_file 被逻辑删除后,外部仍能匿名拉到对象存储字节 + if (fileMapper.selectByConfigIdAndPath(configId, path) == null) { + return null; + } FileClient client = fileConfigService.getFileClient(configId); Assert.notNull(client, "客户端({}) 不能为空", configId); return client.getContent(path);