1. 新增附件下载预览的短链功能

This commit is contained in:
chenbowen
2025-07-25 19:18:39 +08:00
parent 9f1fad0096
commit 296cb6d59d
10 changed files with 199 additions and 351 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,6 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
@@ -29,9 +28,6 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@@ -49,25 +45,8 @@ public class FileController {
@Resource
private FileService fileService;
@Value("${yudao.kkfile:}")
private String onlinePreview;
@GetMapping("/download-url")
@Operation(summary = "获取文件下载地址", description = "根据 fileId 返回文件下载 url")
public CommonResult<FileRespVO> getDownloadUrl(@RequestParam("fileId") Long fileId) {
FileDO fileDO = fileService.getActiveFileById(fileId);
if (fileDO == null) {
return CommonResult.error(HttpStatus.NOT_FOUND.value(), "文件不存在");
}
// FileDO 转换为 FileRespVO
FileRespVO fileRespVO = BeanUtils.toBean(fileDO, FileRespVO.class);
if (StrUtil.isEmpty(onlinePreview) || StrUtil.isEmpty(fileRespVO.getUrl())) {
return CommonResult.error(HttpStatus.BAD_REQUEST.value(), "文件 URL 为空");
}
return success(fileRespVO);
}
@GetMapping("/preview-url")
@GetMapping("/get")
@Operation(summary = "获取文件预览地址", description = "根据 fileId 返回文件预览 urlkkfile")
public CommonResult<FileRespVO> getPreviewUrl(@RequestParam("fileId") Long fileId) {
FileDO fileDO = fileService.getActiveFileById(fileId);
@@ -76,11 +55,6 @@ public class FileController {
}
// FileDO 转换为 FileRespVO
FileRespVO fileRespVO = BeanUtils.toBean(fileDO, FileRespVO.class);
if (StrUtil.isEmpty(onlinePreview) || StrUtil.isEmpty(fileRespVO.getUrl())) {
return CommonResult.error(HttpStatus.BAD_REQUEST.value(), "在线预览地址未配置或文件 URL 为空");
}
String previewUrl = onlinePreview + URLEncoder.encode(Base64.getEncoder().encodeToString(fileRespVO.getUrl().getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
fileRespVO.setPreviewUrl(previewUrl);
return success(fileRespVO);
}
@@ -99,11 +73,6 @@ public class FileController {
MultipartFile file = uploadReqVO.getFile();
byte[] content = IoUtil.readBytes(file.getInputStream());
FileRespVO fileWhitReturn = fileService.createFileWhitReturn(content, file.getOriginalFilename(), uploadReqVO.getDirectory(), file.getContentType(), uploadReqVO.getEncrypt());
if (StrUtil.isEmpty(onlinePreview) || StrUtil.isEmpty(fileWhitReturn.getUrl())) {
fileWhitReturn.setPreviewUrl(fileWhitReturn.getUrl());
return success(fileWhitReturn);
}
fileWhitReturn.setPreviewUrl(onlinePreview + URLEncoder.encode(fileWhitReturn.getUrl(), StandardCharsets.UTF_8));
return success(fileWhitReturn);
}

View File

@@ -1,13 +1,34 @@
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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
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
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);
}
@Schema(description = "文件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@@ -28,6 +49,26 @@ public class FileRespVO {
@Schema(description = "附件预览地址", example = "https://www.iocoder.cn/yudao.jpg")
private String previewUrl;
public String getPreviewUrl() {
// 仅当 url 不为空时生成
if (this.url == null || this.url.isEmpty()) {
return null;
}
// 这里的 onlinePreview 通过 SpringUtils 获取
String onlinePreview = SpringUtils.getProperty("yudao.kkfile");
if (onlinePreview == null || onlinePreview.isEmpty()) {
return null;
}
String shortUrl = this.getUrl();
if (shortUrl == null || shortUrl.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=中国铜业"
;
}
@Schema(description = "文件MIME类型", example = "application/octet-stream")
private String type;

View File

@@ -0,0 +1,77 @@
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<String> 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<byte[]> parseShortLink(@PathVariable("shortKey") String shortKey) {
String realUrl = shortLinkService.parseShortLink(shortKey);
if (realUrl != null) {
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<byte[]> 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());
}
}
}

View File

@@ -33,8 +33,9 @@ public class DatabaseTableServiceImpl implements DatabaseTableService {
@Override
public List<TableInfo> getTableList(Long dataSourceConfigId, String nameLike, String commentLike) {
List<TableInfo> tables = getTableList0(dataSourceConfigId, null);
return tables.stream().filter(tableInfo -> (StrUtil.isEmpty(nameLike) || tableInfo.getName().contains(nameLike))
&& (StrUtil.isEmpty(commentLike) || tableInfo.getComment().contains(commentLike)))
return tables.stream().filter(tableInfo ->
(StrUtil.isEmpty(nameLike) || tableInfo.getName().toLowerCase().contains(nameLike.toLowerCase()))
&& (StrUtil.isEmpty(commentLike) || tableInfo.getComment().contains(commentLike)))
.collect(Collectors.toList());
}

View File

@@ -0,0 +1,13 @@
package cn.iocoder.yudao.module.infra.service.shortlink;
public interface ShortLinkService {
/**
* 生成短链接
*/
String generateShortLink(String key, String url);
/**
* 解析短链接
*/
String parseShortLink(String shortKey);
}

View File

@@ -0,0 +1,58 @@
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);
}
}

View File

@@ -114,5 +114,8 @@ spring:
context-path: /admin # 配置 Spring
--- #################### 芋道相关配置 ####################
yudao:
shortlink:
server: "http://172.16.46.63:48080/admin-api/infra/shortlink/"

View File

@@ -140,3 +140,5 @@ yudao:
enable: true
# 固定验证码
verify-code: 666666
shortlink:
server: "http://localhost:48080/admin-api/infra/shortlink/"