2025-09-08 17:01:50 +08:00
|
|
|
|
package com.yj.earth.business.controller;
|
|
|
|
|
|
|
|
|
|
|
|
import cn.hutool.core.io.FileUtil;
|
|
|
|
|
|
import cn.hutool.core.util.IdUtil;
|
2025-09-16 11:41:45 +08:00
|
|
|
|
import com.drew.imaging.ImageMetadataReader;
|
|
|
|
|
|
import com.drew.imaging.ImageProcessingException;
|
2025-09-22 17:13:22 +08:00
|
|
|
|
import com.drew.lang.Rational;
|
2025-09-16 11:41:45 +08:00
|
|
|
|
import com.drew.metadata.Directory;
|
|
|
|
|
|
import com.drew.metadata.Metadata;
|
|
|
|
|
|
|
|
|
|
|
|
import java.io.*;
|
|
|
|
|
|
import java.nio.file.Files;
|
2025-09-22 17:13:22 +08:00
|
|
|
|
import java.nio.file.NoSuchFileException;
|
2025-09-16 11:41:45 +08:00
|
|
|
|
import java.nio.file.Path;
|
|
|
|
|
|
import java.nio.file.Paths;
|
2025-09-22 17:13:22 +08:00
|
|
|
|
import java.nio.file.attribute.BasicFileAttributes;
|
2025-09-16 11:41:45 +08:00
|
|
|
|
import java.util.*;
|
|
|
|
|
|
|
2025-09-08 17:01:50 +08:00
|
|
|
|
import cn.hutool.crypto.digest.DigestUtil;
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
2025-09-22 17:13:22 +08:00
|
|
|
|
import com.drew.metadata.MetadataException;
|
|
|
|
|
|
import com.drew.metadata.exif.GpsDirectory;
|
2025-09-23 16:45:42 +08:00
|
|
|
|
import com.yj.earth.annotation.CheckAuth;
|
2025-09-08 17:01:50 +08:00
|
|
|
|
import com.yj.earth.business.domain.FileInfo;
|
|
|
|
|
|
import com.yj.earth.business.service.FileInfoService;
|
|
|
|
|
|
import com.yj.earth.common.util.ApiResponse;
|
2025-09-16 17:41:23 +08:00
|
|
|
|
import com.yj.earth.common.util.JsonUtil;
|
2025-09-08 17:01:50 +08:00
|
|
|
|
import com.yj.earth.vo.FileInfoVo;
|
|
|
|
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
|
|
|
|
import io.swagger.v3.oas.annotations.Parameter;
|
|
|
|
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
|
|
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
|
|
|
|
import org.springframework.beans.BeanUtils;
|
|
|
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
|
import org.springframework.http.HttpHeaders;
|
2025-09-16 11:41:45 +08:00
|
|
|
|
import org.springframework.util.DigestUtils;
|
2025-09-08 17:01:50 +08:00
|
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
2025-09-16 11:41:45 +08:00
|
|
|
|
import java.io.InputStream;
|
2025-09-08 17:01:50 +08:00
|
|
|
|
import java.net.URLEncoder;
|
|
|
|
|
|
import java.nio.charset.StandardCharsets;
|
2025-09-16 11:41:45 +08:00
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
|
import java.util.Map;
|
2025-09-08 17:01:50 +08:00
|
|
|
|
|
|
|
|
|
|
@Tag(name = "文件数据管理")
|
2025-09-23 16:45:42 +08:00
|
|
|
|
@CheckAuth
|
2025-09-08 17:01:50 +08:00
|
|
|
|
@RestController
|
|
|
|
|
|
@RequestMapping("/fileInfo")
|
|
|
|
|
|
public class FileInfoController {
|
|
|
|
|
|
|
|
|
|
|
|
@Resource
|
|
|
|
|
|
private FileInfoService fileInfoService;
|
|
|
|
|
|
|
|
|
|
|
|
@Operation(summary = "文件上传")
|
|
|
|
|
|
@PostMapping("/upload")
|
|
|
|
|
|
public ApiResponse uploadFiles(@Parameter(description = "上传的文件数组", required = true) @RequestParam("files") MultipartFile[] files) throws IOException {
|
|
|
|
|
|
// 校验文件数组是否为空
|
|
|
|
|
|
if (files == null || files.length == 0) {
|
|
|
|
|
|
return ApiResponse.failure("上传文件不能为空");
|
|
|
|
|
|
}
|
|
|
|
|
|
// 获取完整的上传目录路径
|
2025-09-22 17:13:22 +08:00
|
|
|
|
String fullUploadPath = fileInfoService.getFullUploadPath();
|
2025-09-08 17:01:50 +08:00
|
|
|
|
List<FileInfoVo> fileInfoVoList = new ArrayList<>();
|
|
|
|
|
|
// 遍历处理每个文件
|
|
|
|
|
|
for (MultipartFile file : files) {
|
|
|
|
|
|
// 跳过空文件
|
|
|
|
|
|
if (file.isEmpty()) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 获取原始文件名和后缀
|
|
|
|
|
|
String originalFilename = file.getOriginalFilename();
|
|
|
|
|
|
String fileSuffix = FileUtil.extName(originalFilename);
|
|
|
|
|
|
String contentType = file.getContentType();
|
|
|
|
|
|
|
|
|
|
|
|
// 生成唯一文件名、避免重复
|
|
|
|
|
|
String uniqueFileName = IdUtil.simpleUUID() + "." + fileSuffix;
|
|
|
|
|
|
|
|
|
|
|
|
// 创建文件存储目录(如果不存在则自动创建)
|
|
|
|
|
|
File uploadDir = new File(fullUploadPath);
|
|
|
|
|
|
FileUtil.mkdir(uploadDir);
|
|
|
|
|
|
|
|
|
|
|
|
// 构建完整文件路径
|
|
|
|
|
|
String filePath = fullUploadPath + File.separator + uniqueFileName;
|
|
|
|
|
|
File destFile = new File(filePath);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用Hutool工具类保存文件
|
|
|
|
|
|
FileUtil.writeBytes(file.getBytes(), destFile);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算文件MD5(用于校验文件完整性)
|
|
|
|
|
|
String fileMd5 = DigestUtil.md5Hex(destFile);
|
|
|
|
|
|
|
|
|
|
|
|
// 查询有没有文件名一样并且 MD5 也一样的数据
|
|
|
|
|
|
LambdaQueryWrapper<FileInfo> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
|
queryWrapper.eq(FileInfo::getFileName, originalFilename).eq(FileInfo::getFileMd5, fileMd5);
|
2025-09-22 17:13:22 +08:00
|
|
|
|
if (fileInfoService.count(queryWrapper) > 0) {
|
2025-09-08 17:01:50 +08:00
|
|
|
|
return ApiResponse.failure("已存在文件名相同且内容完全一致的文件");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存文件信息到数据库
|
|
|
|
|
|
FileInfo fileInfo = new FileInfo();
|
|
|
|
|
|
fileInfo.setFileName(originalFilename);
|
|
|
|
|
|
fileInfo.setFileSuffix(fileSuffix);
|
|
|
|
|
|
fileInfo.setContentType(contentType);
|
|
|
|
|
|
fileInfo.setFileSize(file.getSize());
|
2025-09-22 17:13:22 +08:00
|
|
|
|
fileInfo.setFilePath(uniqueFileName);
|
2025-09-08 17:01:50 +08:00
|
|
|
|
fileInfo.setFileMd5(fileMd5);
|
|
|
|
|
|
|
|
|
|
|
|
// 保存文件信息并获取ID
|
|
|
|
|
|
fileInfoService.save(fileInfo);
|
|
|
|
|
|
|
|
|
|
|
|
// 构建并设置预览URL和下载URL
|
|
|
|
|
|
String previewUrl = "/fileInfo/preview/" + fileInfo.getId();
|
|
|
|
|
|
String downloadUrl = "/fileInfo/download/" + fileInfo.getId();
|
|
|
|
|
|
|
|
|
|
|
|
FileInfoVo fileInfoVo = new FileInfoVo();
|
|
|
|
|
|
BeanUtils.copyProperties(fileInfo, fileInfoVo);
|
|
|
|
|
|
fileInfoVo.setPreviewUrl(previewUrl);
|
|
|
|
|
|
fileInfoVo.setDownloadUrl(downloadUrl);
|
|
|
|
|
|
|
|
|
|
|
|
fileInfoVoList.add(fileInfoVo);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (fileInfoVoList.isEmpty()) {
|
|
|
|
|
|
return ApiResponse.failure("未成功上传任何文件");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return ApiResponse.success(fileInfoVoList);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Operation(summary = "文件下载")
|
|
|
|
|
|
@GetMapping("/download/{id}")
|
|
|
|
|
|
public void downloadFile(@Parameter(description = "文件ID", required = true) @PathVariable String id, HttpServletResponse response) throws IOException {
|
|
|
|
|
|
// 根据ID查询文件信息
|
|
|
|
|
|
FileInfo fileInfo = fileInfoService.getById(id);
|
|
|
|
|
|
if (fileInfo == null) {
|
|
|
|
|
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建完整文件路径
|
2025-09-22 17:13:22 +08:00
|
|
|
|
String fullPath = fileInfoService.getFullUploadPath() + File.separator + fileInfo.getFilePath();
|
2025-09-08 17:01:50 +08:00
|
|
|
|
File file = new File(fullPath);
|
|
|
|
|
|
|
|
|
|
|
|
// 校验文件是否存在
|
|
|
|
|
|
if (!file.exists()) {
|
|
|
|
|
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置响应头
|
|
|
|
|
|
response.setContentType(fileInfo.getContentType());
|
|
|
|
|
|
response.setContentLengthLong(fileInfo.getFileSize());
|
|
|
|
|
|
String encodedFileName = URLEncoder.encode(fileInfo.getFileName(), StandardCharsets.UTF_8.name());
|
|
|
|
|
|
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
|
|
|
|
|
|
"attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 Hutool 工具类复制文件流到响应输出流
|
|
|
|
|
|
try (OutputStream os = response.getOutputStream()) {
|
|
|
|
|
|
FileUtil.writeToStream(file, os);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Operation(summary = "文件预览")
|
|
|
|
|
|
@GetMapping("/preview/{id}")
|
2025-09-16 11:41:45 +08:00
|
|
|
|
public void previewFile(@Parameter(description = "文件ID", required = true) @PathVariable String id, HttpServletResponse response) throws IOException {
|
2025-09-08 17:01:50 +08:00
|
|
|
|
// 根据ID查询文件信息
|
|
|
|
|
|
FileInfo fileInfo = fileInfoService.getById(id);
|
|
|
|
|
|
if (fileInfo == null) {
|
|
|
|
|
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建完整文件路径
|
2025-09-22 17:13:22 +08:00
|
|
|
|
String fullPath = fileInfoService.getFullUploadPath() + File.separator + fileInfo.getFilePath();
|
2025-09-08 17:01:50 +08:00
|
|
|
|
File file = new File(fullPath);
|
|
|
|
|
|
|
|
|
|
|
|
// 校验文件是否存在
|
|
|
|
|
|
if (!file.exists()) {
|
|
|
|
|
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置响应头(不设置 attachment、让浏览器直接显示)
|
|
|
|
|
|
response.setContentType(fileInfo.getContentType());
|
|
|
|
|
|
response.setContentLengthLong(fileInfo.getFileSize());
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 Hutool 工具类复制文件流到响应输出流
|
|
|
|
|
|
try (OutputStream os = response.getOutputStream()) {
|
|
|
|
|
|
FileUtil.writeToStream(file, os);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-22 17:13:22 +08:00
|
|
|
|
@Operation(summary = "本地文件预览")
|
|
|
|
|
|
@GetMapping("/previewLocal")
|
|
|
|
|
|
public void previewLocalFile(@Parameter(description = "本地文件绝对路径") @RequestParam String fileAbsolutePath, HttpServletResponse response) {
|
|
|
|
|
|
Path targetFilePath = null;
|
2025-09-16 11:41:45 +08:00
|
|
|
|
try {
|
2025-09-22 17:13:22 +08:00
|
|
|
|
// 标准化路径
|
|
|
|
|
|
targetFilePath = Paths.get(fileAbsolutePath).toRealPath();
|
|
|
|
|
|
|
|
|
|
|
|
// 校验文件合法性:是否存在、是否为普通文件
|
|
|
|
|
|
BasicFileAttributes fileAttr = Files.readAttributes(targetFilePath, BasicFileAttributes.class);
|
|
|
|
|
|
if (!fileAttr.isRegularFile()) {
|
|
|
|
|
|
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
|
|
|
|
|
fileInfoService.writeResponseMessage(response, "目标路径不是有效的文件");
|
|
|
|
|
|
return;
|
2025-09-16 11:41:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-22 17:13:22 +08:00
|
|
|
|
// 设置预览响应头
|
|
|
|
|
|
String fileName = targetFilePath.getFileName().toString();
|
|
|
|
|
|
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
|
|
|
|
|
|
String contentType = Files.probeContentType(targetFilePath);
|
2025-09-16 11:41:45 +08:00
|
|
|
|
|
2025-09-22 17:13:22 +08:00
|
|
|
|
// 对于文本类型文件、指定字符编码
|
|
|
|
|
|
if (contentType != null && contentType.startsWith("text/")) {
|
|
|
|
|
|
response.setContentType(contentType + "; charset=UTF-8");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
response.setContentType(contentType != null ? contentType : "application/octet-stream");
|
|
|
|
|
|
}
|
2025-09-16 11:41:45 +08:00
|
|
|
|
|
2025-09-22 17:13:22 +08:00
|
|
|
|
response.setContentLengthLong(fileAttr.size());
|
|
|
|
|
|
// 关键修改:将attachment改为inline实现预览
|
|
|
|
|
|
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
|
|
|
|
|
|
"inline; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
|
|
|
|
|
|
|
|
|
|
|
|
// 写入文件流
|
|
|
|
|
|
try (InputStream inputStream = Files.newInputStream(targetFilePath);
|
|
|
|
|
|
OutputStream outputStream = response.getOutputStream()) {
|
|
|
|
|
|
byte[] buffer = new byte[1024 * 8];
|
|
|
|
|
|
int len;
|
|
|
|
|
|
while ((len = inputStream.read(buffer)) != -1) {
|
|
|
|
|
|
outputStream.write(buffer, 0, len);
|
2025-09-16 11:41:45 +08:00
|
|
|
|
}
|
2025-09-22 17:13:22 +08:00
|
|
|
|
outputStream.flush();
|
2025-09-16 11:41:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-22 17:13:22 +08:00
|
|
|
|
} catch (NoSuchFileException e) {
|
|
|
|
|
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
|
|
|
|
|
fileInfoService.writeResponseMessage(response, "文件不存在:" + fileAbsolutePath);
|
|
|
|
|
|
} catch (SecurityException e) {
|
|
|
|
|
|
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
|
|
|
|
|
fileInfoService.writeResponseMessage(response, "访问拒绝:无权限读取该文件");
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
|
|
|
|
|
fileInfoService.writeResponseMessage(response, "预览失败:" + e.getMessage());
|
2025-09-08 17:01:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|