diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileRespVO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileRespVO.java index 2ef0ac68..d4d4223e 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileRespVO.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileRespVO.java @@ -1,33 +1,42 @@ package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file; import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; -import cn.iocoder.yudao.module.infra.service.shortlink.ShortLinkService; +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; +import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClient; +import cn.iocoder.yudao.module.infra.service.file.FileConfigService; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import lombok.experimental.Accessors; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.Base64; -import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; - /** * @author chenbowen */ @Schema(description = "管理后台 - 文件 Response VO,不返回 content 字段,太大") @Data +@Accessors(chain = true) public class FileRespVO { public String getUrl() { - if (this.url == null || this.url.isEmpty()) return null; - Long fileId = this.id; - Long userId = null; - try { - userId = getLoginUserId(); - } catch (Exception ignored) {} - ShortLinkService shortLinkService = SpringUtils.getBean(ShortLinkService.class); - String serverPrefix = SpringUtils.getProperty("yudao.shortlink.server"); - String key = (userId != null ? userId : "anonymous") + "_" + (fileId != null ? fileId : "nofile"); - return serverPrefix + shortLinkService.generateShortLink(key, this.url); + // 加密附件不返回 url + if (Boolean.TRUE.equals(this.isEncrypted)) { + return null; + } + // 如果 url 已经是临时下载地址(如预签名 URL),直接返回 + if (url != null && (url.contains("X-Amz-Signature") || url.contains("?sign="))) { + return url; + } + FileConfigService fileConfigService = SpringUtils.getBean(FileConfigService.class); + FileClient fileClient = fileConfigService.getMasterFileClient(); + if (fileClient instanceof S3FileClient s3FileClient) { + String presignedDownloadUrl = s3FileClient.getPresignedDownloadUrl(this.path, null); + if (presignedDownloadUrl != null && !presignedDownloadUrl.isEmpty()) { + return presignedDownloadUrl; + } + } + return url; } @Schema(description = "文件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @@ -50,6 +59,10 @@ public class FileRespVO { private String previewUrl; public String getPreviewUrl() { + // 加密附件不返回 previewUrl + if (Boolean.TRUE.equals(this.isEncrypted)) { + return null; + } // 仅当 url 不为空时生成 if (this.url == null || this.url.isEmpty()) { return null; @@ -59,15 +72,18 @@ public class FileRespVO { if (onlinePreview == null || onlinePreview.isEmpty()) { return null; } - String shortUrl = this.getUrl(); - if (shortUrl == null || shortUrl.isEmpty()) { + String presignedUrl = this.getUrl(); + if (presignedUrl == null || presignedUrl.isEmpty()) { return null; } - String base64ShortUrl = Base64.getUrlEncoder().encodeToString(shortUrl.getBytes(StandardCharsets.UTF_8)); - return onlinePreview + base64ShortUrl -// + "&fileName=" + URLEncoder.encode(this.getName(), StandardCharsets.UTF_8) + "&watermarkTxt=中国铜业" - ; + String base64PresignedUrl = Base64.getUrlEncoder().encodeToString(presignedUrl.getBytes(StandardCharsets.UTF_8)); + String timestamp = String.valueOf(System.currentTimeMillis()); + String watermark = SpringUtils.getProperty("aj.captcha.water-mark", "中国铜业"); + return onlinePreview + base64PresignedUrl + "&t=" + timestamp + "&watermarkTxt=" + watermark; } + @Schema(description = "是否加密", example = "false") + private Boolean isEncrypted; + @Schema(description = "文件MIME类型", example = "application/octet-stream") private String type; diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/shortlink/ShortLinkController.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/shortlink/ShortLinkController.java deleted file mode 100644 index af115f79..00000000 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/shortlink/ShortLinkController.java +++ /dev/null @@ -1,77 +0,0 @@ -package cn.iocoder.yudao.module.infra.controller.admin.shortlink; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; -import cn.iocoder.yudao.module.infra.service.shortlink.ShortLinkService; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.security.PermitAll; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.*; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.client.RestTemplate; - -/** - * @author chenbowen - */ -@Tag(name = "管理后台 - 文件存储") -@Validated -@Slf4j -@RestController -@RequestMapping("/infra/shortlink") -public class ShortLinkController { - - @Autowired - private ShortLinkService shortLinkService; - - - /** - * 生成短链接 - */ - @PostMapping("/generate") - public CommonResult generateShortLink(@RequestParam("key") String key, @RequestParam("url") String url) { - String shortLink = "/infra/shortlink/" + shortLinkService.generateShortLink(key, url); - return CommonResult.success(shortLink); - } - - /** - * 解析短链接并重定向 - */ - @GetMapping("/{shortKey}") - @PermitAll - @TenantIgnore - public ResponseEntity parseShortLink(@PathVariable("shortKey") String shortKey) { - String realUrl = shortLinkService.parseShortLink(shortKey); - if (realUrl != null) { - RestTemplate restTemplate = new RestTemplate(); - try { - ResponseEntity entity = restTemplate.exchange(realUrl, HttpMethod.GET, HttpEntity.EMPTY, byte[].class); - HttpHeaders headers = new HttpHeaders(); - // 只透传部分常用header,可根据需要扩展 - if (entity.getHeaders().getContentType() != null) { - headers.setContentType(entity.getHeaders().getContentType()); - } - if (entity.getHeaders().getContentLength() > 0) { - headers.setContentLength(entity.getHeaders().getContentLength()); - } - // 透传 disposition/encoding 等 - String disposition = entity.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION); - if (disposition != null) { - headers.set(HttpHeaders.CONTENT_DISPOSITION, disposition); - } - String encoding = entity.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING); - if (encoding != null) { - headers.set(HttpHeaders.CONTENT_ENCODING, encoding); - } - return new ResponseEntity<>(entity.getBody(), headers, entity.getStatusCode()); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(("目标链接内容获取异常: " + e.getMessage()).getBytes()); - } - } else { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body("当前文件请求链接已失效,请从系统重新进行请求操作".getBytes()); - } - } -} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java index 60998a1e..42b501fc 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java @@ -29,8 +29,7 @@ public interface FileMapper extends BaseMapperX { * @return 文件DO,若不存在返回null */ default FileDO selectByHash(String hash) { - return selectOne(new LambdaQueryWrapperX() - .eq(FileDO::getHash, hash)); + return selectFirstOne(FileDO::getHash, hash); } } diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java index 053b3c51..326a6b9e 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java @@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.infra.framework.file.core.client; import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO; +import java.time.Duration; + /** * 文件客户端 * @@ -52,4 +54,8 @@ public interface FileClient { throw new UnsupportedOperationException("不支持的操作"); } + default String getPresignedDownloadUrl(String path, Duration expiration){ + throw new UnsupportedOperationException("不支持的操作"); + } + } diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java index a33f0d73..a9ce9a45 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.framework.file.core.client.s3; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; @@ -15,6 +16,7 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; import java.net.URI; @@ -26,6 +28,36 @@ import java.time.Duration; * @author 芋道源码 */ public class S3FileClient extends AbstractFileClient { + /** + * 生成临时下载地址(预签名下载 URL) + * @param path 文件路径 + * @param expiration 过期时间(可为 null,null 时自动从配置获取) + * @return 下载 URL + */ + @Override + public String getPresignedDownloadUrl(String path, Duration expiration) { + Duration realExpiration = expiration; + if (realExpiration == null) { + long expireSeconds = 30; // 默认 30 秒 + try { + String val = SpringUtils.getProperty("yudao.file.download-expire-seconds"); + if (val != null && !val.isEmpty()) { + expireSeconds = Long.parseLong(val); + } + } catch (Exception ignored) {} + realExpiration = Duration.ofSeconds(expireSeconds); + } + // 使用 S3 官方支持的 responseCacheControl 参数,强制浏览器和代理不缓存 + GetObjectRequest.Builder getObjectRequestBuilder = GetObjectRequest.builder() + .bucket(config.getBucket()) + .key(path) + .responseCacheControl("no-cache, no-store, must-revalidate"); + GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder() + .signatureDuration(realExpiration) + .getObjectRequest(getObjectRequestBuilder.build()) + .build(); + return presigner.presignGetObject(getObjectPresignRequest).url().toString(); + } private S3Client client; private S3Presigner presigner; diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableServiceImpl.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableServiceImpl.java index fb9c1b8b..289cae1e 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableServiceImpl.java @@ -33,9 +33,8 @@ public class DatabaseTableServiceImpl implements DatabaseTableService { @Override public List getTableList(Long dataSourceConfigId, String nameLike, String commentLike) { List tables = getTableList0(dataSourceConfigId, null); - return tables.stream().filter(tableInfo -> - (StrUtil.isEmpty(nameLike) || tableInfo.getName().toLowerCase().contains(nameLike.toLowerCase())) - && (StrUtil.isEmpty(commentLike) || tableInfo.getComment().contains(commentLike))) + return tables.stream().filter(tableInfo -> (StrUtil.isEmpty(nameLike) || tableInfo.getName().contains(nameLike)) + && (StrUtil.isEmpty(commentLike) || tableInfo.getComment().contains(commentLike))) .collect(Collectors.toList()); } diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java index 61eae94f..0b94078f 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java @@ -131,7 +131,8 @@ public class FileServiceImpl implements FileService { .setUrl(entity.getUrl()) .setType(entity.getType()) .setSize(entity.getSize()) - .setConfigId(entity.getConfigId()); + .setConfigId(entity.getConfigId()) + .setIsEncrypted(entity.getAesIv() != null && !entity.getAesIv().isEmpty()); } private FileDO uploadFile(byte[] content, String name, String directory, String type, Boolean encrypt) throws Exception { diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/shortlink/ShortLinkService.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/shortlink/ShortLinkService.java deleted file mode 100644 index 438c1c87..00000000 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/shortlink/ShortLinkService.java +++ /dev/null @@ -1,13 +0,0 @@ -package cn.iocoder.yudao.module.infra.service.shortlink; - -public interface ShortLinkService { - /** - * 生成短链接 - */ - String generateShortLink(String key, String url); - - /** - * 解析短链接 - */ - String parseShortLink(String shortKey); -} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/shortlink/ShortLinkServiceImpl.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/shortlink/ShortLinkServiceImpl.java deleted file mode 100644 index a7bb9a98..00000000 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/shortlink/ShortLinkServiceImpl.java +++ /dev/null @@ -1,58 +0,0 @@ -package cn.iocoder.yudao.module.infra.service.shortlink; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.concurrent.TimeUnit; - -@Service - -public class ShortLinkServiceImpl implements ShortLinkService { - private final StringRedisTemplate redisTemplate; - - @Value("${yudao.shortlink.expire-seconds:30}") - private long expireSeconds; - - public ShortLinkServiceImpl(StringRedisTemplate redisTemplate) { - this.redisTemplate = redisTemplate; - } - - /** - * 生成短链接 - */ - @Override - public String generateShortLink(String key, String url) { - // 使用url生成hash作为短链key - String hashKey = hashUrl(url); - // 存入Redis并设置过期时间 - redisTemplate.opsForValue().set(hashKey, url, expireSeconds, TimeUnit.SECONDS); - return hashKey; - } - - private String hashUrl(String url) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] hash = digest.digest(url.getBytes()); - StringBuilder sb = new StringBuilder(); - // 只取前8字节,够短链用 - for (int i = 0; i < 8 && i < hash.length; i++) { - sb.append(String.format("%02x", hash[i])); - } - return sb.toString(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("Hash算法异常", e); - } - } - - /** - * 解析短链接 - */ - @Override - public String parseShortLink(String shortKey) { - // 直接从Redis获取 - return redisTemplate.opsForValue().get(shortKey); - } -} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/application-dev.yaml b/yudao-module-infra/yudao-module-infra-server/src/main/resources/application-dev.yaml index 938636ad..269cb96d 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/application-dev.yaml +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/application-dev.yaml @@ -114,8 +114,5 @@ spring: context-path: /admin # 配置 Spring --- #################### 芋道相关配置 #################### -yudao: - shortlink: - server: "http://172.16.46.63:48080/admin-api/infra/shortlink/" diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/application-local.yaml b/yudao-module-infra/yudao-module-infra-server/src/main/resources/application-local.yaml index 5cb5e4d1..df378279 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/application-local.yaml +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/application-local.yaml @@ -140,5 +140,5 @@ yudao: enable: true # 固定验证码 verify-code: 666666 - shortlink: - server: "http://localhost:48080/admin-api/infra/shortlink/" \ No newline at end of file + file: + download-expire-seconds: 20 \ No newline at end of file