package com.yj.earth.business.controller; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.crypto.digest.DigestUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.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.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @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); } } @Operation(summary = "文件列表") @GetMapping("/list") public ApiResponse getFileList( @Parameter(description = "页码", required = true) Integer pageNum, @Parameter(description = "每页条数", required = true) Integer pageSize, @Parameter(description = "文件名称") String fileName) { // 创建分页对象 Page page = new Page<>(pageNum, pageSize); // 构建查询条件 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); if (fileName != null && !fileName.isEmpty()) { queryWrapper.like(FileInfo::getFileName, fileName); } // 按创建时间倒序排列、最新上传的文件在前 queryWrapper.orderByDesc(FileInfo::getCreatedAt); // 执行分页查询 IPage fileInfoPage = fileInfoService.page(page, queryWrapper); // 转换为VO对象并设置URL List records = fileInfoPage.getRecords().stream().map(fileInfo -> { FileInfoVo vo = new FileInfoVo(); BeanUtils.copyProperties(fileInfo, vo); vo.setPreviewUrl("/fileInfo/preview/" + fileInfo.getId()); vo.setDownloadUrl("/fileInfo/download/" + fileInfo.getId()); return vo; }).collect(Collectors.toList()); // 构建分页结果 Page resultPage = new Page<>(); resultPage.setRecords(records); resultPage.setTotal(fileInfoPage.getTotal()); resultPage.setSize(fileInfoPage.getSize()); resultPage.setCurrent(fileInfoPage.getCurrent()); resultPage.setPages(fileInfoPage.getPages()); return ApiResponse.success(resultPage); } }