diff --git a/admin/src/main/java/com/multictrl/modules/business/controller/MediaFileController.java b/admin/src/main/java/com/multictrl/modules/business/controller/MediaFileController.java index b885353..caf3750 100644 --- a/admin/src/main/java/com/multictrl/modules/business/controller/MediaFileController.java +++ b/admin/src/main/java/com/multictrl/modules/business/controller/MediaFileController.java @@ -8,7 +8,6 @@ import com.multictrl.common.constant.Constant; import com.multictrl.common.page.PageData; import com.multictrl.common.utils.Result; import com.multictrl.common.validator.AssertUtils; -import com.multictrl.modules.business.dto.FlightTaskDTO; import com.multictrl.modules.business.dto.MediaFileDTO; import com.multictrl.modules.business.service.MediaFileService; import io.swagger.v3.oas.annotations.Operation; @@ -17,9 +16,10 @@ import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.apache.shiro.authz.annotation.RequiresPermissions; -import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import java.util.List; import java.util.Map; @@ -85,9 +85,9 @@ public class MediaFileController { @Operation(summary = "下载媒体文件") @LogOperation("下载媒体文件") - @GetMapping("/downloadMedia/{id}") + @GetMapping("/downloadMedia") @RequiresPermissions("bus:media:download") - public ResponseEntity downloadMedia(@PathVariable Long id) { + public ResponseEntity downloadMedia(@RequestParam Long id) { return mediaFileService.downloadMedia(id); } @@ -103,7 +103,7 @@ public class MediaFileController { @Parameter(name = "endTime", description = "结束时间") }) @DataFilter(tableAlias = "t") - public ResponseEntity conditionDownloadMedia(@Parameter(hidden = true) @RequestParam Map params) { + public ResponseEntity conditionDownloadMedia(@Parameter(hidden = true) @RequestParam Map params) { return mediaFileService.downloadMediaByZip(params); } diff --git a/admin/src/main/java/com/multictrl/modules/business/service/MediaFileService.java b/admin/src/main/java/com/multictrl/modules/business/service/MediaFileService.java index 062e7d5..b0e2296 100644 --- a/admin/src/main/java/com/multictrl/modules/business/service/MediaFileService.java +++ b/admin/src/main/java/com/multictrl/modules/business/service/MediaFileService.java @@ -6,8 +6,9 @@ import com.multictrl.common.service.CrudService; import com.multictrl.modules.business.dao.MediaFileDao; import com.multictrl.modules.business.dto.MediaFileDTO; import com.multictrl.modules.business.entity.MediaFileEntity; -import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; import org.springframework.http.ResponseEntity; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import java.util.List; import java.util.Map; @@ -30,10 +31,10 @@ public interface MediaFileService extends CrudService downloadMedia(Long id); + ResponseEntity downloadMedia(Long id); //下载媒体zip - ResponseEntity downloadMediaByZip(Map params); + ResponseEntity downloadMediaByZip(Map params); //下载文件地址 Map> oneClickGetDownloadPath(Map params); diff --git a/admin/src/main/java/com/multictrl/modules/business/service/impl/MediaFileServiceImpl.java b/admin/src/main/java/com/multictrl/modules/business/service/impl/MediaFileServiceImpl.java index a45b1d8..5d54ac5 100644 --- a/admin/src/main/java/com/multictrl/modules/business/service/impl/MediaFileServiceImpl.java +++ b/admin/src/main/java/com/multictrl/modules/business/service/impl/MediaFileServiceImpl.java @@ -22,17 +22,20 @@ import com.multictrl.modules.security.user.SecurityUser; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; -import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import java.io.*; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.*; import java.util.stream.Collectors; import java.util.zip.ZipEntry; @@ -122,30 +125,26 @@ public class MediaFileServiceImpl extends CrudServiceImpl downloadMedia(Long id) { + public ResponseEntity downloadMedia(Long id) { try { MediaFileDTO dto = get(id); - String filePath = minioConfig.getOther().getDataPath() + BusinessConstant.DOCK_MEDIA_BUCKET + "/" + dto.getObjectKey(); - String fileName = dto.getName(); - File file = new File(filePath); - - // 检查文件是否存在 + // 构建文件路径(建议用 Paths.get 更安全) + Path filePath = Paths.get(minioConfig.getOther().getDataPath(), BusinessConstant.DOCK_MEDIA_BUCKET, dto.getObjectKey()); + File file = filePath.toFile(); if (!file.exists()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); + return ResponseEntity.notFound().build(); } - // 设置 Content-Disposition 头 - String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20"); - String contentDisposition = "attachment; filename=" + encodedFileName; + String fileName = dto.getName(); + String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8) + .replace("+", "%20"); + String contentDisposition = "attachment; filename*=UTF-8''" + encodedFileName; - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.parseMediaType(MediaType.APPLICATION_OCTET_STREAM_VALUE)); - headers.set(HttpHeaders.CONTENT_DISPOSITION, contentDisposition); - headers.setContentLength((int) Files.size(file.toPath())); - - // 使用 InputStreamResource 支持大文件流式传输 - InputStream inputStream = Files.newInputStream(file.toPath()); - return new ResponseEntity<>(new InputStreamResource(inputStream), headers, HttpStatus.OK); + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition) + .contentLength(Files.size(filePath)) // 直接传 long,安全 + .body(new FileSystemResource(file)); // 自动管理流 } catch (IOException e) { log.error("媒体文件下载失败:{}", ExceptionUtils.getStackTrace(e)); @@ -154,64 +153,54 @@ public class MediaFileServiceImpl extends CrudServiceImpl downloadMediaByZip(Map params) { - ZipOutputStream zipOut = null; - ByteArrayOutputStream baos = null; - try { - baos = new ByteArrayOutputStream(); - zipOut = new ZipOutputStream(baos); - - // 遍历所有ID,将文件添加到ZIP中 - List list = baseDao.list(getWrapperBy(params)); - for (MediaFileEntity entity : list) { - try { - String filePath = minioConfig.getOther().getDataPath() + BusinessConstant.DOCK_MEDIA_BUCKET + "/" + entity.getObjectKey(); - String fileName = entity.getName(); - File file = new File(filePath); - - // 创建ZIP条目 - ZipEntry zipEntry = new ZipEntry(fileName); - zipOut.putNextEntry(zipEntry); - - // 将文件内容写入ZIP - Files.copy(file.toPath(), zipOut); - - zipOut.closeEntry(); - - } catch (Exception e) { - // 记录错误但继续处理其他文件 - log.error("处理文件ID {} 时出错: {}", entity.getId(), e.getMessage()); - } - } - - // 关闭ZIP流 - zipOut.close(); - baos.close(); - - // 设置响应头 - String zipFileName = "media_" + System.currentTimeMillis() + ".zip"; - String encodedFileName = URLEncoder.encode(zipFileName, StandardCharsets.UTF_8).replace("+", "%20"); - String contentDisposition = "attachment; filename=" + encodedFileName; - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.parseMediaType("application/zip")); - headers.set(HttpHeaders.CONTENT_DISPOSITION, contentDisposition); - headers.setContentLength(baos.size()); - - // 创建输入流资源 - ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); - return new ResponseEntity<>(new InputStreamResource(bais), headers, HttpStatus.OK); - - } catch (IOException e) { - // 关闭流 - try { - zipOut.close(); - baos.close(); - } catch (IOException ex) { - log.error("媒体文件(zip)下载失败:{}", ExceptionUtils.getStackTrace(e)); - } - throw new RenException(ErrorCode.INTERNAL_SERVER_ERROR); + public ResponseEntity downloadMediaByZip(Map params) { + List list = baseDao.list(getWrapperBy(params)); + if (list.isEmpty()) { + return ResponseEntity.notFound().build(); } + + String zipFileName = "media_" + System.currentTimeMillis() + ".zip"; + String encodedFileName = URLEncoder.encode(zipFileName, StandardCharsets.UTF_8) + .replace("+", "%20"); + // 使用 RFC 5987 标准编码避免乱码 + String contentDisposition = "attachment; filename*=UTF-8''" + encodedFileName; + + StreamingResponseBody streamingBody = outputStream -> { + // 使用 try-with-resources 确保 zip 流关闭 + try (ZipOutputStream zipOut = new ZipOutputStream(outputStream)) { + for (MediaFileEntity entity : list) { + try { + Path filePath = Paths.get( + minioConfig.getOther().getDataPath(), + BusinessConstant.DOCK_MEDIA_BUCKET, + entity.getObjectKey() + ); + // 避免路径遍历攻击:检查最终路径是否在基础目录内(可选但推荐) + File file = filePath.toFile(); + if (!file.exists()) { + log.warn("文件不存在,跳过: {}", filePath); + continue; + } + String fileName = entity.getName(); + ZipEntry zipEntry = new ZipEntry(fileName); + zipOut.putNextEntry(zipEntry); + Files.copy(filePath, zipOut); + zipOut.closeEntry(); + } catch (Exception e) { + // 记录错误但继续打包其他文件 + log.error("添加文件到ZIP失败,ID: {}", entity.getId(), e); + } + } + } catch (IOException e) { + log.error("ZIP打包过程中发生IO异常", e); + throw new RuntimeException(e); + } + }; + + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType("application/zip")) + .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition) + .body(streamingBody); } @Override