|
|
@@ -25,11 +25,20 @@ import com.zsjz.ai.module.plat.mapper.SystemInfoMapper;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.core.io.FileSystemResource;
|
|
|
+import org.springframework.core.io.Resource;
|
|
|
import org.springframework.data.redis.core.RedisTemplate;
|
|
|
+import org.springframework.http.CacheControl;
|
|
|
+import org.springframework.http.ContentDisposition;
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
+import org.springframework.http.MediaType;
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
|
import java.io.File;
|
|
|
+import java.io.IOException;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
import java.nio.file.Files;
|
|
|
import java.nio.file.Path;
|
|
|
import java.nio.file.StandardCopyOption;
|
|
|
@@ -39,6 +48,8 @@ import java.sql.Statement;
|
|
|
import java.time.LocalDate;
|
|
|
import java.time.LocalDateTime;
|
|
|
import java.time.temporal.ChronoUnit;
|
|
|
+import java.util.Locale;
|
|
|
+import java.util.Map;
|
|
|
import java.util.Set;
|
|
|
|
|
|
/**
|
|
|
@@ -244,46 +255,90 @@ public class SystemService extends ServiceImpl<SystemInfoMapper, SystemInfo> {
|
|
|
FileUtil.del(PathConst.ORI_UPGRADE_PATH);
|
|
|
}
|
|
|
|
|
|
+ /** 可下载文件扩展名 → Content-Type */
|
|
|
+ private static final Map<String, MediaType> DOWNLOAD_MEDIA_TYPES = Map.of(
|
|
|
+ "csv", new MediaType("text", "csv", StandardCharsets.UTF_8),
|
|
|
+ "xls", MediaType.parseMediaType("application/vnd.ms-excel"),
|
|
|
+ "xlsx", MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
|
|
|
+
|
|
|
/**
|
|
|
- * 校验并解析待下载的本地文件。
|
|
|
+ * 按文件ID下载文件(Web 端文件下载的唯一入口,原「调用系统程序打开文件」的 Web 化改造)。
|
|
|
+ *
|
|
|
+ * <p>入参只接受 {@code file_info.id}:服务端路径全部由本方法查库得到,
|
|
|
+ * 调用方无法传任意路径,避免退化成任意文件读取接口。
|
|
|
*
|
|
|
- * <p>原实现是客户端形态的「调用系统程序打开文件」,Web 端改为把文件流返回给浏览器下载;
|
|
|
- * 这里沿用原有的 csv/xls/xlsx 后缀白名单,避免该接口退化成任意文件读取。
|
|
|
+ * @param fid 文件或工作表ID
|
|
|
+ * @return 附件形式的文件流响应(刻意不做 Result 包装)
|
|
|
+ */
|
|
|
+ public ResponseEntity<Resource> downloadFile(Long fid) {
|
|
|
+ DownloadFile file = resolveDownloadFile(fid);
|
|
|
+ Path path = file.path();
|
|
|
+ long contentLength;
|
|
|
+ try {
|
|
|
+ contentLength = Files.size(path);
|
|
|
+ } catch (IOException e) {
|
|
|
+ // 校验通过后文件被清理(清洗完成后会递归删除临时目录)
|
|
|
+ throw new ServerException(404, "文件不存在或已被清理");
|
|
|
+ }
|
|
|
+ ContentDisposition disposition = ContentDisposition.attachment()
|
|
|
+ .filename(file.fileName(), StandardCharsets.UTF_8)
|
|
|
+ .build();
|
|
|
+ return ResponseEntity.ok()
|
|
|
+ .contentType(DOWNLOAD_MEDIA_TYPES.getOrDefault(fileSuffix(file.fileName()), MediaType.APPLICATION_OCTET_STREAM))
|
|
|
+ .contentLength(contentLength)
|
|
|
+ // 案件原始数据,禁止浏览器/代理缓存
|
|
|
+ .cacheControl(CacheControl.noStore())
|
|
|
+ .header(HttpHeaders.CONTENT_DISPOSITION, disposition.toString())
|
|
|
+ .body(new FileSystemResource(path));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析待下载文件:查库取路径 → 校验(存在、非目录、csv/xls/xlsx 白名单)。
|
|
|
*
|
|
|
- * @param filePath 文件在服务端的绝对路径
|
|
|
- * @return 绝对路径 + 建议的下载文件名
|
|
|
+ * @param fid 文件或工作表ID
|
|
|
*/
|
|
|
- public DownloadFile resolveDownloadFile(String filePath) {
|
|
|
+ public DownloadFile resolveDownloadFile(Long fid) {
|
|
|
+ if (fid == null) {
|
|
|
+ throw ServerException.spe("文件ID不能为空!");
|
|
|
+ }
|
|
|
+ FileInfo fileInfo = fileInfoMapper.selectById(fid);
|
|
|
+ if (fileInfo == null) {
|
|
|
+ throw ServerException.spe("文件不存在!");
|
|
|
+ }
|
|
|
+ String filePath = resolveFilePath(fileInfo);
|
|
|
if (StrUtil.isBlank(filePath)) {
|
|
|
- throw ServerException.spe("文件路径不能为空!");
|
|
|
+ throw ServerException.spe("该记录没有可下载的源文件!");
|
|
|
}
|
|
|
Path path = Path.of(filePath);
|
|
|
if (Files.notExists(path) || Files.isDirectory(path)) {
|
|
|
- throw ServerException.spe("文件不存在!");
|
|
|
+ throw ServerException.spe("文件不存在或已被清理!");
|
|
|
}
|
|
|
String suffix = FileUtil.getSuffix(path.toFile());
|
|
|
if (StrUtil.isBlank(suffix) || !GlobalCache.suffixList.contains(suffix.toLowerCase())) {
|
|
|
throw ServerException.spe("仅支持下载 csv、xls、xlsx 文件!");
|
|
|
}
|
|
|
- return new DownloadFile(path.toAbsolutePath(), path.getFileName().toString());
|
|
|
+ String fileName = StrUtil.isBlank(fileInfo.getFileName()) ? path.getFileName().toString() : fileInfo.getFileName();
|
|
|
+ return new DownloadFile(path.toAbsolutePath(), fileName);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 按文件ID(file_info.id)解析待下载文件,优先使用记录里的原始文件名。
|
|
|
- *
|
|
|
- * @param fid 文件/工作表ID
|
|
|
+ * 取文件在服务端的路径:工作表行自身没有 filePath(只有 pid 指向文件根),回退到所属文件根。
|
|
|
*/
|
|
|
- public DownloadFile resolveDownloadFileByFid(Long fid) {
|
|
|
- if (fid == null) {
|
|
|
- throw ServerException.spe("文件ID不能为空!");
|
|
|
+ private String resolveFilePath(FileInfo fileInfo) {
|
|
|
+ if (StrUtil.isNotBlank(fileInfo.getFilePath())) {
|
|
|
+ return fileInfo.getFilePath();
|
|
|
}
|
|
|
- FileInfo fileInfo = fileInfoMapper.selectById(fid);
|
|
|
- if (fileInfo == null) {
|
|
|
- throw ServerException.spe("文件不存在!");
|
|
|
+ Long pid = fileInfo.getPid();
|
|
|
+ if (pid == null || pid.equals(fileInfo.getId())) {
|
|
|
+ return null;
|
|
|
}
|
|
|
- DownloadFile resolved = resolveDownloadFile(fileInfo.getFilePath());
|
|
|
- String fileName = StrUtil.isBlank(fileInfo.getFileName()) ? resolved.fileName() : fileInfo.getFileName();
|
|
|
- return new DownloadFile(resolved.path(), fileName);
|
|
|
+ FileInfo parent = fileInfoMapper.selectById(pid);
|
|
|
+ return parent == null ? null : parent.getFilePath();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String fileSuffix(String fileName) {
|
|
|
+ int idx = fileName == null ? -1 : fileName.lastIndexOf('.');
|
|
|
+ return idx < 0 ? "" : fileName.substring(idx + 1).toLowerCase(Locale.ROOT);
|
|
|
}
|
|
|
|
|
|
/**
|