diff --git a/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/controller/ZipDownloadController.java b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/controller/ZipDownloadController.java new file mode 100644 index 0000000..bd670cd --- /dev/null +++ b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/controller/ZipDownloadController.java @@ -0,0 +1,208 @@ +package com.bonus.bmw.controller; + +import com.bonus.bmw.domain.po.DownloadTask; +import com.bonus.bmw.domain.po.ZipFileMapping; +import com.bonus.bmw.domain.vo.BmWorkerAtt; +import com.bonus.bmw.service.ZipDownloadService; +import io.minio.GetObjectArgs; +import io.minio.MinioClient; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.archivers.zip.Zip64Mode; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +@RestController +@RequestMapping("/zipDownload") +@Slf4j +public class ZipDownloadController { + + @Resource + private ZipDownloadService service; + + @Resource + private RedisTemplate redisTemplate; + + private static final String TASK_PREFIX = "zip_task:"; // key 前缀 + private static final long TASK_EXPIRE_SECONDS = 2 * 3600; // 2小时过期 + + private final ExecutorService executor = Executors.newFixedThreadPool(4); + + private static final MinioClient minioClient = MinioClient.builder() + .endpoint("http://192.168.0.14:9090") + .credentials("minio", "bonus@admin123") + .build(); + private static final String bucket = "realname"; + + @PostMapping("/createZipTask") + public ResponseEntity createZipTask(@RequestBody BmWorkerAtt o) { + String taskId = UUID.randomUUID().toString(); + String redisKey = TASK_PREFIX + taskId; + + DownloadTask task = new DownloadTask(taskId); + // 保存到 Redis,设置过期时间 + redisTemplate.opsForValue().set(redisKey, task, TASK_EXPIRE_SECONDS, TimeUnit.SECONDS); + //查询文件地址 + List objectNames = service.getFileList(o); +// List objectNames = new ArrayList<>(); +// objectNames.add(new ZipFileMapping("relname/2025/11/11/1f18f8b3c0b14143be1f30db6b078e2d.jpg","工程1/分包1/班组1/张三/人脸/user123_face.jpg")); +// objectNames.add(new ZipFileMapping("relname/2025/11/11/ae3044c218714985ba8977479f712278.jpg","工程1/分包1/班组1/张三/合同/con.jpg")); + // 异步执行打包 + executor.submit(() -> { + try { + String zipName = taskId + ".zip"; + String zipPath = "/tmp/downloads/" + zipName; +// generateZip(request.getFilePaths(), zipPath); // 你的打包逻辑 + generateZipFromMinIO(minioClient, bucket, objectNames, zipPath); + // 更新任务状态 + task.setStatus("completed"); +// task.setDownloadUrl("/downloadFile/" + zipName); + task.setDownloadUrl("/" + zipName); + redisTemplate.opsForValue().set(redisKey, task, TASK_EXPIRE_SECONDS, TimeUnit.SECONDS); + } catch (Exception e) { + task.setStatus("failed"); + task.setErrorMessage(e.getMessage()); + redisTemplate.opsForValue().set(redisKey, task, TASK_EXPIRE_SECONDS, TimeUnit.SECONDS); + } + }); + + return ResponseEntity.accepted().body(task); + } + + /** + * 查询任务状态 + */ + @GetMapping("/taskStatus/{taskId}") + public ResponseEntity getTaskStatus(@PathVariable String taskId) { + String redisKey = TASK_PREFIX + taskId; + DownloadTask task = redisTemplate.opsForValue().get(redisKey); + if (task == null) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(task); + } + + /** + * 下载文件 + */ + @GetMapping("/downloadFile/{filename:.+}") + public void serveZipFile(@PathVariable String filename, HttpServletResponse response) throws IOException { + // 1. 基础安全:禁止路径遍历 + if (filename.contains("..") || filename.startsWith("/") || filename.startsWith("\\")) { + response.sendError(403, "Path traversal not allowed"); + return; + } + // 2. 必须以 .zip 结尾 + if (!filename.endsWith(".zip")) { + response.sendError(400, "Only .zip files allowed"); + return; + } + // 3. 文件名只能包含:字母、数字、中文、连字符(-)、下划线(_)、点(.) + // 且不能有连续多个点或非法字符 + if (!filename.matches("^[\\w\\u4e00-\\u9fff.-]+$")) { + response.sendError(400, "Invalid characters in filename"); + return; + } + // 4. 防止超长文件名(可选) + if (filename.length() > 255) { + response.sendError(400, "Filename too long"); + return; + } + Path zipPath = Paths.get("/tmp/downloads/", filename); + if (!Files.exists(zipPath)) { + response.sendError(404); + return; + } + response.setContentType("application/zip"); + response.setHeader("Content-Disposition", "attachment; filename=" + filename); + response.setContentLengthLong(Files.size(zipPath)); + + try (InputStream is = Files.newInputStream(zipPath); + OutputStream os = response.getOutputStream()) { + byte[] buffer = new byte[8192]; + int n; + while ((n = is.read(buffer)) > 0) { + os.write(buffer, 0, n); + } + } + } + + /** + * 使用 Apache Commons Compress 从 MinIO 打包 ZIP(支持 UTF-8 中文) + */ + public static void generateZipFromMinIO(MinioClient minioClient, String bucket, List fileMappings, String zipPath) throws Exception { + Path outputPath = Paths.get(zipPath); + Files.createDirectories(outputPath.getParent()); + try (FileOutputStream fos = new FileOutputStream(outputPath.toFile()); + BufferedOutputStream bos = new BufferedOutputStream(fos, 64 * 1024); + ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(bos)) { + zipOut.setEncoding("UTF-8"); + zipOut.setUseZip64(Zip64Mode.AsNeeded); + for (ZipFileMapping mapping : fileMappings) { + String objectName = mapping.getObjectName(); + String targetPath = mapping.getTargetPath(); + // 安全校验 + if (objectName == null || targetPath == null || + objectName.contains("..") || targetPath.contains("..") || + objectName.startsWith("/") || targetPath.startsWith("/")) { + continue; // 跳过非法项 + } + // 统一使用正斜杠(兼容 Windows) + String entryName = targetPath.replace('\\', '/'); + try (InputStream objectStream = minioClient.getObject( + GetObjectArgs.builder() + .bucket(bucket) + .object(objectName) + .build())) { + ZipArchiveEntry entry = new ZipArchiveEntry(entryName); + zipOut.putArchiveEntry(entry); + byte[] buffer = new byte[8192]; + int len; + while ((len = objectStream.read(buffer)) > 0) { + zipOut.write(buffer, 0, len); + } + zipOut.closeArchiveEntry(); + } catch (Exception e) { + System.err.println("Failed to process object: " + objectName + " -> " + targetPath); + // 可选择继续或中断 + } + } + zipOut.finish(); + } + } + + /** + * 定时清理过期文件 + */ + @Scheduled(fixedRate = 3600000) // 每小时清理一次 + public void cleanOldZips() throws IOException { + try (Stream files = Files.list(Paths.get("/tmp/downloads"))) { + files.filter(p -> p.toString().endsWith(".zip")) + .filter(p -> { + try { + return System.currentTimeMillis() - Files.getLastModifiedTime(p).toMillis() > 2 * 3600_000; // 2 小时过期 + } catch (IOException e) { return false; } + }) + .forEach(p -> { + try { Files.delete(p); } catch (IOException ignored) {} + }); + } + } + +} diff --git a/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/domain/po/DownloadTask.java b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/domain/po/DownloadTask.java new file mode 100644 index 0000000..f76f9a0 --- /dev/null +++ b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/domain/po/DownloadTask.java @@ -0,0 +1,30 @@ +package com.bonus.bmw.domain.po; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DownloadTask implements Serializable { + /** + * 任务ID + */ + private String taskId; + /** + * 状态:processing / completed / failed + */ + private String status; + private String downloadUrl; + private String errorMessage; + + public DownloadTask(String taskId) { + this.taskId = taskId; + this.status = "processing"; + } + +} diff --git a/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/domain/po/ZipFileMapping.java b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/domain/po/ZipFileMapping.java new file mode 100644 index 0000000..e1283f1 --- /dev/null +++ b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/domain/po/ZipFileMapping.java @@ -0,0 +1,14 @@ +package com.bonus.bmw.domain.po; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ZipFileMapping { + private String objectName; // MinIO 中的对象名,如 "user123_face.jpg" + private String targetPath; // 在 ZIP 中的目标路径,如 "工程1/分包1/班组1/张三/人脸/user123_face.jpg" +} diff --git a/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/mapper/ZipDownloadMapper.java b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/mapper/ZipDownloadMapper.java new file mode 100644 index 0000000..29d0a19 --- /dev/null +++ b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/mapper/ZipDownloadMapper.java @@ -0,0 +1,11 @@ +package com.bonus.bmw.mapper; + +import com.bonus.bmw.domain.po.ZipFileMapping; +import com.bonus.bmw.domain.vo.BmWorkerAtt; + +import java.util.List; + +public interface ZipDownloadMapper { + + List getFileList(BmWorkerAtt o); +} diff --git a/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/service/ZipDownloadService.java b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/service/ZipDownloadService.java new file mode 100644 index 0000000..58b1e3f --- /dev/null +++ b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/service/ZipDownloadService.java @@ -0,0 +1,11 @@ +package com.bonus.bmw.service; + +import com.bonus.bmw.domain.po.ZipFileMapping; +import com.bonus.bmw.domain.vo.BmWorkerAtt; + +import java.util.List; + +public interface ZipDownloadService { + + List getFileList(BmWorkerAtt o); +} diff --git a/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/service/impl/ZipDownloadServiceImpl.java b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/service/impl/ZipDownloadServiceImpl.java new file mode 100644 index 0000000..19e97b3 --- /dev/null +++ b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/service/impl/ZipDownloadServiceImpl.java @@ -0,0 +1,29 @@ +package com.bonus.bmw.service.impl; + +import com.bonus.bmw.domain.po.ZipFileMapping; +import com.bonus.bmw.domain.vo.BmWorkerAtt; +import com.bonus.bmw.mapper.ZipDownloadMapper; +import com.bonus.bmw.service.ZipDownloadService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @author 马三炮 + * @date 2025/8/14 + */ +@Service +@Slf4j +public class ZipDownloadServiceImpl implements ZipDownloadService { + + @Resource + private ZipDownloadMapper mapper; + + + @Override + public List getFileList(BmWorkerAtt o) { + return mapper.getFileList(o); + } +} diff --git a/bonus-modules/bonus-bmw/src/main/resources/mapper/bmw/ZipDownloadMapper.xml b/bonus-modules/bonus-bmw/src/main/resources/mapper/bmw/ZipDownloadMapper.xml new file mode 100644 index 0000000..c6a78ba --- /dev/null +++ b/bonus-modules/bonus-bmw/src/main/resources/mapper/bmw/ZipDownloadMapper.xml @@ -0,0 +1,89 @@ + + + + + + + +