package com.yj.earth.business.controller; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.IdUtil; import com.drew.imaging.ImageMetadataReader; import com.drew.imaging.ImageProcessingException; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; import cn.hutool.crypto.digest.DigestUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.yj.earth.business.domain.FileInfo; import com.yj.earth.business.service.FileInfoService; import com.yj.earth.common.util.ApiResponse; import com.yj.earth.common.util.JsonUtil; 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; import org.springframework.util.DigestUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.InputStream; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @Tag(name = "文件数据管理") @RestController @RequestMapping("/fileInfo") public class FileInfoController { @Resource private FileInfoService fileInfoService; @Value("${file.upload.path}") private String uploadPath; // 获取项目根目录 private String getProjectRootPath() { return System.getProperty("user.dir"); } // 获取完整的上传目录路径 private String getFullUploadPath() { // 拼接项目根目录和配置的上传路径 return getProjectRootPath() + File.separator + uploadPath; } @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("上传文件不能为空"); } // 获取完整的上传目录路径 String fullUploadPath = getFullUploadPath(); List 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 queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(FileInfo::getFileName, originalFilename).eq(FileInfo::getFileMd5, fileMd5); if (fileInfoService.count(queryWrapper) > 0) { // 修复了此处的bug、添加了queryWrapper参数 return ApiResponse.failure("已存在文件名相同且内容完全一致的文件"); } // 保存文件信息到数据库 FileInfo fileInfo = new FileInfo(); fileInfo.setFileName(originalFilename); fileInfo.setFileSuffix(fileSuffix); fileInfo.setContentType(contentType); fileInfo.setFileSize(file.getSize()); fileInfo.setFilePath(uniqueFileName); // 只保存相对文件名、不保存完整路径 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; } // 构建完整文件路径 String fullPath = getFullUploadPath() + File.separator + fileInfo.getFilePath(); 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}") public void previewFile(@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; } // 构建完整文件路径 String fullPath = getFullUploadPath() + File.separator + fileInfo.getFilePath(); 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); } } public String handleLocationImageUpload(MultipartFile file) { try { // 校验文件是否为空 if (file.isEmpty()) { throw new IllegalArgumentException("上传文件不能为空"); } // 获取文件基本信息 String originalFilename = file.getOriginalFilename(); String fileSuffix = FileUtil.extName(originalFilename); String contentType = file.getContentType(); // 验证是否为图片文件 if (contentType == null || !contentType.startsWith("image/")) { throw new IllegalArgumentException("请上传图片文件"); } // 获取完整的上传目录路径 Path fullUploadPath = Paths.get(getFullUploadPath()); // 生成唯一文件名 String uniqueFileName = UUID.randomUUID().toString().replaceAll("-", "") + "." + fileSuffix; // 创建文件存储目录 Files.createDirectories(fullUploadPath); // 构建完整文件路径并保存文件 Path destFilePath = fullUploadPath.resolve(uniqueFileName); // 先将文件保存到目标位置 file.transferTo(destFilePath); // 计算文件MD5(使用已保存的文件) String fileMd5 = calculateFileMd5(destFilePath.toFile()); // 检查文件是否已存在 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(FileInfo::getFileName, originalFilename) .eq(FileInfo::getFileMd5, fileMd5); if (fileInfoService.count(queryWrapper) > 0) { throw new IllegalStateException("已存在相同的图片文件"); } // 提取图片元数据(使用已保存的文件,避免使用临时文件) Map metadata; try (InputStream is = Files.newInputStream(destFilePath)) { metadata = extractImageMetadata(is); } // 保存文件信息到数据库 FileInfo fileInfo = new FileInfo(); fileInfo.setFileName(originalFilename); fileInfo.setFileSuffix(fileSuffix); fileInfo.setContentType(contentType); fileInfo.setFileSize(file.getSize()); fileInfo.setFilePath(uniqueFileName); fileInfo.setFileMd5(fileMd5); fileInfoService.save(fileInfo); // 构建并返回结果 Map result = new HashMap<>(); result.put("previewUrl", "/fileInfo/preview/" + fileInfo.getId()); result.put("downloadUrl", "/fileInfo/download/" + fileInfo.getId()); result.put("metadata", metadata); return JsonUtil.mapToJson(result); } catch (IOException e) { throw new RuntimeException("文件上传失败: " + e.getMessage(), e); } } /** * 计算文件的 MD5 */ private String calculateFileMd5(File file) throws IOException { try (InputStream is = new FileInputStream(file)) { return DigestUtils.md5DigestAsHex(is); } } /** * 提取图片的EXIF元数据(包括定位信息) */ private Map extractImageMetadata(InputStream inputStream) { try { Map result = new HashMap<>(); Metadata metadata = ImageMetadataReader.readMetadata(inputStream); // 遍历所有元数据目录 for (Directory directory : metadata.getDirectories()) { String directoryName = directory.getName(); Map directoryTags = new HashMap<>(); // 提取当前目录下的所有标签 for (com.drew.metadata.Tag tag : directory.getTags()) { directoryTags.put(tag.getTagName(), tag.getDescription()); } // 存储当前目录的所有标签 if (!directoryTags.isEmpty()) { result.put(directoryName, directoryTags); } } return result; } catch (ImageProcessingException | IOException e) { return Collections.emptyMap(); } } }