压缩下载优化
This commit is contained in:
parent
cb8b5cb708
commit
6b557374bf
|
|
@ -1,22 +1,34 @@
|
||||||
package com.bonus.imgTool.backstage.controller;
|
package com.bonus.imgTool.backstage.controller;
|
||||||
|
import com.bonus.imgTool.backstage.dao.SynthesisQueryDao;
|
||||||
|
|
||||||
import com.bonus.imgTool.backstage.entity.DownloadRequest;
|
import com.bonus.imgTool.backstage.entity.DownloadRequest;
|
||||||
import com.bonus.imgTool.backstage.service.DownloadService;
|
import com.bonus.imgTool.backstage.entity.Photo;
|
||||||
|
import com.bonus.imgTool.backstage.entity.SynthesisQueryVo;
|
||||||
|
import com.bonus.imgTool.backstage.service.*;
|
||||||
|
import com.bonus.imgTool.utils.HighQualityWatermark;
|
||||||
|
import com.bonus.imgTool.utils.SystemUtils;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.nio.file.Files;
|
||||||
import java.util.Map;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @className:DownloadController
|
* @className:DownloadController
|
||||||
|
|
@ -33,7 +45,16 @@ public class DownloadController {
|
||||||
@Autowired
|
@Autowired
|
||||||
private DownloadService downloadService;
|
private DownloadService downloadService;
|
||||||
|
|
||||||
@PostMapping("/start")
|
@Autowired
|
||||||
|
private SynthesisQueryDao synthesisQueryDao;
|
||||||
|
|
||||||
|
@Value("${download.output.dir}")
|
||||||
|
private String outputDir;
|
||||||
|
|
||||||
|
@javax.annotation.Resource(name = "testTaskExecutor")
|
||||||
|
private ThreadPoolTaskExecutor testTaskExecutor;
|
||||||
|
|
||||||
|
/*@PostMapping("/start")
|
||||||
public ResponseEntity<String> startDownload(@RequestBody DownloadRequest request) {
|
public ResponseEntity<String> startDownload(@RequestBody DownloadRequest request) {
|
||||||
downloadService.startDownloadTask(request.getTaskId(), request.getProId(),request.getType(),request.getProName());
|
downloadService.startDownloadTask(request.getTaskId(), request.getProId(),request.getType(),request.getProName());
|
||||||
return ResponseEntity.ok("Download task started");
|
return ResponseEntity.ok("Download task started");
|
||||||
|
|
@ -101,5 +122,183 @@ public class DownloadController {
|
||||||
.contentLength(file.length())
|
.contentLength(file.length())
|
||||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
.body(resource);
|
.body(resource);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ParallelZipService zipService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TaskTrackerService taskTracker;
|
||||||
|
|
||||||
|
@PostMapping("/start")
|
||||||
|
public ResponseEntity<String> startCompression(@RequestBody DownloadRequest request) {
|
||||||
|
try {
|
||||||
|
// 下载水印则生成图片水印
|
||||||
|
if(Objects.equals(request.getType(),"2")){
|
||||||
|
generateWatermark(request.getProId());
|
||||||
|
}
|
||||||
|
// 1. 获取照片列表
|
||||||
|
List<Photo> photos = getPhotosForAlbum(request.getProId(),request.getType());
|
||||||
|
// 2. 注册任务
|
||||||
|
taskTracker.registerTask(request.getTaskId(), photos.size());
|
||||||
|
// 3. 开始异步压缩
|
||||||
|
zipService.compressInParallel(request.getTaskId(),request.getProName(), photos,
|
||||||
|
(progress, processed, total) -> {
|
||||||
|
taskTracker.updateProgress(request.getTaskId(), progress, processed, total);
|
||||||
|
})
|
||||||
|
.thenAccept(finalPath -> {
|
||||||
|
String downloadUrl = "/imgTool/api/download/file?taskId=" + request.getTaskId();
|
||||||
|
taskTracker.markComplete(request.getTaskId(), downloadUrl);
|
||||||
|
})
|
||||||
|
.exceptionally(e -> {
|
||||||
|
log.error("压缩任务失败: {}", request.getTaskId(), e);
|
||||||
|
taskTracker.markFailed(request.getTaskId(), e.getMessage());
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return ResponseEntity.ok("压缩任务已启动");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("启动压缩任务失败", e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body("启动压缩任务失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/progress", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
|
public SseEmitter streamProgress(@RequestParam String taskId) {
|
||||||
|
SseEmitter emitter = new SseEmitter(24 * 60 * 60 * 1000L); // 24小时超时
|
||||||
|
|
||||||
|
// 添加监听器
|
||||||
|
taskTracker.addListener(taskId, new TaskTrackerService.TaskListener() {
|
||||||
|
@Override
|
||||||
|
public void onProgress(int progress, int processed, int total) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("type", "progress");
|
||||||
|
data.put("progress", progress);
|
||||||
|
data.put("processed", processed);
|
||||||
|
data.put("total", total);
|
||||||
|
emitter.send(SseEmitter.event().data(data));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("发送进度更新失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete(String downloadUrl) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("type", "complete");
|
||||||
|
data.put("downloadUrl", downloadUrl);
|
||||||
|
emitter.send(SseEmitter.event().data(data));
|
||||||
|
emitter.complete();
|
||||||
|
} catch (IOException e) {
|
||||||
|
emitter.completeWithError(e);
|
||||||
|
} finally {
|
||||||
|
taskTracker.removeListener(taskId, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(String message) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("type", "error");
|
||||||
|
data.put("message", message);
|
||||||
|
emitter.send(SseEmitter.event().data(data));
|
||||||
|
emitter.complete();
|
||||||
|
} catch (IOException e) {
|
||||||
|
emitter.completeWithError(e);
|
||||||
|
} finally {
|
||||||
|
taskTracker.removeListener(taskId, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置超时和错误处理
|
||||||
|
emitter.onTimeout(() -> {
|
||||||
|
taskTracker.removeListener(taskId, (TaskTrackerService.TaskListener) this);
|
||||||
|
log.warn("SSE连接超时: {}", taskId);
|
||||||
|
});
|
||||||
|
emitter.onError(e -> {
|
||||||
|
taskTracker.removeListener(taskId, (TaskTrackerService.TaskListener) this);
|
||||||
|
log.warn("SSE连接错误: {}", taskId, e);
|
||||||
|
});
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/file")
|
||||||
|
public ResponseEntity<Resource> downloadFile(@RequestParam String taskId) {
|
||||||
|
try {
|
||||||
|
Path filePath = Paths.get(outputDir, taskId + ".zip");
|
||||||
|
if (!Files.exists(filePath)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
Resource resource = new FileSystemResource(filePath);
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
|
"attachment; filename=\"" + taskId + ".zip\"")
|
||||||
|
.contentLength(Files.size(filePath))
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.body(resource);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("文件下载失败: {}", taskId, e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Photo> getPhotosForAlbum(String proId,String type) {
|
||||||
|
List<Photo> list = Optional.ofNullable(synthesisQueryDao.findByAlbumId(proId,type)).orElseGet(ArrayList::new);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateWatermark(String proId) {
|
||||||
|
try {
|
||||||
|
// 查询图片未生成水印照片的数据
|
||||||
|
List<SynthesisQueryVo> list = Optional.ofNullable(synthesisQueryDao.generateWatermark(proId)).orElseGet(ArrayList::new);
|
||||||
|
List<Future> futureList = new ArrayList<>();
|
||||||
|
List<SynthesisQueryVo> newList = new ArrayList<>();
|
||||||
|
for (SynthesisQueryVo vo : list) {
|
||||||
|
Future<SynthesisQueryVo> future = testTaskExecutor.submit(new Callable<SynthesisQueryVo>() {
|
||||||
|
@Override
|
||||||
|
public SynthesisQueryVo call() throws Exception {
|
||||||
|
String path = SystemUtils.getUploadPath() + vo.getOriginalFilePath();
|
||||||
|
if (new File(path).exists()) {
|
||||||
|
String syPath = generateWatermarkData(vo);
|
||||||
|
vo.setWatermarkFilePath(syPath);
|
||||||
|
}
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
futureList.add(future);
|
||||||
|
}
|
||||||
|
for (Future<SynthesisQueryVo> future : futureList) {
|
||||||
|
SynthesisQueryVo vo = future.get();
|
||||||
|
newList.add(vo);
|
||||||
|
}
|
||||||
|
if(CollectionUtils.isNotEmpty(newList)){
|
||||||
|
synthesisQueryDao.updateBatchSyData(newList);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.toString(),e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateWatermarkData(SynthesisQueryVo vo){
|
||||||
|
// 准备多行水印文本
|
||||||
|
List<String> watermarkLines = new ArrayList<>();
|
||||||
|
String uploadTime = new SimpleDateFormat("yyyy-MM-dd").format(vo.getUploadTime());
|
||||||
|
watermarkLines.add(uploadTime);
|
||||||
|
watermarkLines.add(vo.getProName().replaceAll("(.{18})", "$1@@"));
|
||||||
|
watermarkLines.add(vo.getUploadTypeName());
|
||||||
|
String sourceTypeName = null;
|
||||||
|
if (Objects.equals(vo.getSourceType(), "9")) {
|
||||||
|
sourceTypeName = vo.getTitle();
|
||||||
|
} else {
|
||||||
|
sourceTypeName = vo.getSourceTypeName().split("-")[1];
|
||||||
|
}
|
||||||
|
watermarkLines.add(sourceTypeName);
|
||||||
|
String localPath = SystemUtils.getUploadPath() +File.separator+ vo.getOriginalFilePath();
|
||||||
|
return HighQualityWatermark.generateWatermark(watermarkLines,localPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
package com.bonus.imgTool.backstage.service;
|
||||||
|
|
||||||
|
import com.bonus.imgTool.backstage.dao.SynthesisQueryDao;
|
||||||
|
import com.bonus.imgTool.backstage.entity.Photo;
|
||||||
|
import com.bonus.imgTool.task.entity.DownloadTaskVo;
|
||||||
|
import com.bonus.imgTool.utils.DateTimeHelper;
|
||||||
|
import com.bonus.imgTool.utils.SystemUtils;
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @className:ParallelZipService
|
||||||
|
* @author:cwchen
|
||||||
|
* @date:2025-04-09-10:23
|
||||||
|
* @version:1.0
|
||||||
|
* @description:
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ParallelZipService {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ParallelZipService.class);
|
||||||
|
|
||||||
|
@Value("${download.temp.dir:/tmp/downloads}")
|
||||||
|
private String tempDir;
|
||||||
|
|
||||||
|
@Value("${download.output.dir}")
|
||||||
|
private String outputDir;
|
||||||
|
|
||||||
|
private ExecutorService executorService;
|
||||||
|
|
||||||
|
private final int threadPoolSize = 8;
|
||||||
|
|
||||||
|
@Resource(name = "SynthesisQueryDao")
|
||||||
|
private SynthesisQueryDao synthesisQueryDao;
|
||||||
|
|
||||||
|
private static final int BUFFER_SIZE = 256 * 1024; // 256KB
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
int threadCount = 8;
|
||||||
|
executorService = Executors.newFixedThreadPool(threadCount, new ThreadFactory() {
|
||||||
|
private final AtomicInteger counter = new AtomicInteger(0);
|
||||||
|
@Override
|
||||||
|
public Thread newThread(Runnable r) {
|
||||||
|
return new Thread(r, "zip-worker-" + counter.incrementAndGet());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void shutdown() {
|
||||||
|
executorService.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Path> compressInParallel(String taskId,String proName, List<Photo> photos,
|
||||||
|
ProgressUpdater progressUpdater) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
Path tempDirPath = Paths.get(tempDir, taskId);
|
||||||
|
Path zipFilePath = tempDirPath.resolve(proName+".zip");
|
||||||
|
// 添加下载任务
|
||||||
|
DownloadTaskVo taskVo = new DownloadTaskVo();
|
||||||
|
taskVo.setTaskId(taskId);
|
||||||
|
taskVo.setTempFilePath(zipFilePath.toString());
|
||||||
|
synthesisQueryDao.addTaskDownload(taskVo);
|
||||||
|
try {
|
||||||
|
Files.createDirectories(tempDirPath);
|
||||||
|
// 分片处理照片列表
|
||||||
|
int totalPhotos = photos.size();
|
||||||
|
int batchSize = (int) Math.ceil((double) totalPhotos / threadPoolSize);
|
||||||
|
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||||
|
List<Path> partFiles = new ArrayList<>();
|
||||||
|
AtomicInteger processedCount = new AtomicInteger(0);
|
||||||
|
// 创建分片压缩任务
|
||||||
|
for (int i = 0; i < threadPoolSize; i++) {
|
||||||
|
int start = i * batchSize;
|
||||||
|
int end = Math.min(start + batchSize, totalPhotos);
|
||||||
|
if (start >= end) break;
|
||||||
|
List<Photo> batch = photos.subList(start, end);
|
||||||
|
Path partFile = tempDirPath.resolve("part_" + i + ".zip");
|
||||||
|
partFiles.add(partFile);
|
||||||
|
futures.add(CompletableFuture.runAsync(() -> {
|
||||||
|
try (ZipOutputStream zos = new ZipOutputStream(
|
||||||
|
new BufferedOutputStream(Files.newOutputStream(partFile)))) {
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
for (Photo photo : batch) {
|
||||||
|
if(StringUtils.isBlank(photo.getFilePath())){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Path photoPath = Paths.get(SystemUtils.getUploadPath() + File.separator + photo.getFilePath());
|
||||||
|
if (!Files.exists(photoPath)) continue;
|
||||||
|
String uniqueEntryName = handlePath(proName,photo,photoPath);
|
||||||
|
ZipEntry entry = new ZipEntry(uniqueEntryName);
|
||||||
|
zos.putNextEntry(entry);
|
||||||
|
try (InputStream is = Files.newInputStream(photoPath)) {
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = is.read(buffer)) > 0) {
|
||||||
|
zos.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zos.closeEntry();
|
||||||
|
// 更新进度
|
||||||
|
int processed = processedCount.incrementAndGet();
|
||||||
|
if (processed % 10 == 0 || processed == totalPhotos) {
|
||||||
|
int progress = (int) ((processed * 100.0) / totalPhotos);
|
||||||
|
progressUpdater.updateProgress(progress, processed, totalPhotos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("分片压缩失败", e);
|
||||||
|
throw new RuntimeException("分片压缩失败", e);
|
||||||
|
}
|
||||||
|
}, executorService));
|
||||||
|
}
|
||||||
|
// 等待所有分片完成
|
||||||
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||||
|
// 合并所有分片
|
||||||
|
try (ZipOutputStream finalZos = new ZipOutputStream(
|
||||||
|
new BufferedOutputStream(Files.newOutputStream(zipFilePath)))) {
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
for (Path partFile : partFiles) {
|
||||||
|
try (ZipInputStream zis = new ZipInputStream(
|
||||||
|
new BufferedInputStream(Files.newInputStream(partFile)))) {
|
||||||
|
ZipEntry entry;
|
||||||
|
while ((entry = zis.getNextEntry()) != null) {
|
||||||
|
finalZos.putNextEntry(entry);
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = zis.read(buffer)) > 0) {
|
||||||
|
finalZos.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
finalZos.closeEntry();
|
||||||
|
zis.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 删除分片文件
|
||||||
|
Files.deleteIfExists(partFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 移动到最终目录
|
||||||
|
Path outputDirPath = Paths.get(outputDir);
|
||||||
|
Files.createDirectories(outputDirPath);
|
||||||
|
Path finalFilePath = outputDirPath.resolve(taskId + ".zip");
|
||||||
|
Files.move(zipFilePath, finalFilePath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
// 更新下载任务
|
||||||
|
String nowTime = DateTimeHelper.currentTwoHours();
|
||||||
|
taskVo.setFilePath(finalFilePath.toString());
|
||||||
|
synthesisQueryDao.updateTaskDownload(taskVo,nowTime);
|
||||||
|
return finalFilePath;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("压缩任务失败: {}", taskId, e);
|
||||||
|
throw new CompletionException(e);
|
||||||
|
} finally {
|
||||||
|
// 7. 清理临时文件
|
||||||
|
cleanupTempFiles(tempDirPath);
|
||||||
|
}
|
||||||
|
}, executorService);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<List<Photo>> partitionList(List<Photo> list, int size) {
|
||||||
|
List<List<Photo>> partitions = new ArrayList<>();
|
||||||
|
for (int i = 0; i < list.size(); i += size) {
|
||||||
|
partitions.add(list.subList(i, Math.min(i + size, list.size())));
|
||||||
|
}
|
||||||
|
return partitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupTempFiles(Path tempDir) {
|
||||||
|
try {
|
||||||
|
if (Files.exists(tempDir)) {
|
||||||
|
FileUtils.deleteDirectory(tempDir.toFile());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("清理临时目录失败: {}", tempDir, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ProgressUpdater {
|
||||||
|
void updateProgress(int progress, int processed, int total);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String handlePath(String proName,Photo photo,Path photoPath){
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(proName).append(File.separator);
|
||||||
|
sb.append(photo.getUploadTypeName()).append(File.separator);
|
||||||
|
if(!Objects.equals(photo.getUploadType(),"5")){
|
||||||
|
String[] sourceTypeNameArr = photo.getSourceTypeName().split("-");
|
||||||
|
sb.append(sourceTypeNameArr[1]).append(File.separator);
|
||||||
|
}else{
|
||||||
|
sb.append(photo.getTitle()).append(File.separator);
|
||||||
|
}
|
||||||
|
sb.append(photo.getPhotoId()).append("_").append(photoPath.getFileName());
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
package com.bonus.imgTool.backstage.service;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
/**
|
||||||
|
* @className:TaskTrackerService
|
||||||
|
* @author:cwchen
|
||||||
|
* @date:2025-04-09-10:23
|
||||||
|
* @version:1.0
|
||||||
|
* @description:
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class TaskTrackerService {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(TaskTrackerService.class);
|
||||||
|
|
||||||
|
private final Map<String, TaskInfo> tasks = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, List<TaskListener>> listeners = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public void registerTask(String taskId, int totalItems) {
|
||||||
|
tasks.put(taskId, new TaskInfo(totalItems));
|
||||||
|
listeners.putIfAbsent(taskId, new CopyOnWriteArrayList<>());
|
||||||
|
logger.info("注册新任务: {}, 总文件数: {}", taskId, totalItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateProgress(String taskId, int progress, int processed, int total) {
|
||||||
|
TaskInfo task = tasks.get(taskId);
|
||||||
|
if (task != null) {
|
||||||
|
task.setProcessed(processed);
|
||||||
|
notifyProgress(taskId, progress, processed, total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markComplete(String taskId, String downloadUrl) {
|
||||||
|
TaskInfo task = tasks.remove(taskId);
|
||||||
|
if (task != null) {
|
||||||
|
task.setDownloadUrl(downloadUrl);
|
||||||
|
notifyComplete(taskId, downloadUrl);
|
||||||
|
logger.info("任务完成: {}", taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markFailed(String taskId, String errorMessage) {
|
||||||
|
TaskInfo task = tasks.remove(taskId);
|
||||||
|
if (task != null) {
|
||||||
|
task.setErrorMessage(errorMessage);
|
||||||
|
notifyError(taskId, errorMessage);
|
||||||
|
logger.error("任务失败: {}, 原因: {}", taskId, errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getFilePath(String taskId) {
|
||||||
|
TaskInfo task = tasks.get(taskId);
|
||||||
|
return task != null ? task.getFilePath() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(String taskId, TaskListener listener) {
|
||||||
|
listeners.computeIfAbsent(taskId, k -> new CopyOnWriteArrayList<>()).add(listener);
|
||||||
|
// 如果任务已完成,立即通知监听器
|
||||||
|
TaskInfo task = tasks.get(taskId);
|
||||||
|
if (task != null && task.isComplete()) {
|
||||||
|
listener.onComplete(task.getDownloadUrl());
|
||||||
|
} else if (task != null && task.isFailed()) {
|
||||||
|
listener.onError(task.getErrorMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(String taskId, TaskListener listener) {
|
||||||
|
List<TaskListener> taskListeners = listeners.get(taskId);
|
||||||
|
if (taskListeners != null) {
|
||||||
|
taskListeners.remove(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyProgress(String taskId, int progress, int processed, int total) {
|
||||||
|
List<TaskListener> taskListeners = listeners.get(taskId);
|
||||||
|
if (taskListeners != null) {
|
||||||
|
taskListeners.forEach(l -> {
|
||||||
|
try {
|
||||||
|
l.onProgress(progress, processed, total);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("通知进度更新失败", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyComplete(String taskId, String downloadUrl) {
|
||||||
|
List<TaskListener> taskListeners = listeners.remove(taskId);
|
||||||
|
if (taskListeners != null) {
|
||||||
|
taskListeners.forEach(l -> {
|
||||||
|
try {
|
||||||
|
l.onComplete(downloadUrl);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("通知任务完成失败", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyError(String taskId, String message) {
|
||||||
|
List<TaskListener> taskListeners = listeners.remove(taskId);
|
||||||
|
if (taskListeners != null) {
|
||||||
|
taskListeners.forEach(l -> {
|
||||||
|
try {
|
||||||
|
l.onError(message);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("通知任务失败失败", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private static class TaskInfo {
|
||||||
|
private final int total;
|
||||||
|
private int processed;
|
||||||
|
private String downloadUrl;
|
||||||
|
private String errorMessage;
|
||||||
|
private Path filePath;
|
||||||
|
|
||||||
|
public TaskInfo(int total) {
|
||||||
|
this.total = total;
|
||||||
|
this.processed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getProgress() {
|
||||||
|
return total == 0 ? 0 : (int) ((processed * 100.0) / total);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isComplete() {
|
||||||
|
return downloadUrl != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFailed() {
|
||||||
|
return errorMessage != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface TaskListener {
|
||||||
|
void onProgress(int progress, int processed, int total);
|
||||||
|
void onComplete(String downloadUrl);
|
||||||
|
void onError(String message);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -3,6 +3,159 @@ let type = decryptCBC(getUrlParam('type'));
|
||||||
let title = decryptCBC(decodeURIComponent(getUrlParam('title')));
|
let title = decryptCBC(decodeURIComponent(getUrlParam('title')));
|
||||||
let proName = decryptCBC(decodeURIComponent(getUrlParam('proName')));
|
let proName = decryptCBC(decodeURIComponent(getUrlParam('proName')));
|
||||||
$('#title').html(proName +" - "+ title);
|
$('#title').html(proName +" - "+ title);
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const downloadBtn = document.getElementById('downloadBtn');
|
||||||
|
const progressContainer = document.getElementById('progressContainer');
|
||||||
|
const progressBar = document.getElementById('progress');
|
||||||
|
const progressPercent = document.getElementById('progressPercent');
|
||||||
|
const processedFiles = document.getElementById('processedFiles');
|
||||||
|
const statusText = document.getElementById('statusText');
|
||||||
|
|
||||||
|
let eventSource = null;
|
||||||
|
let currentTaskId = null;
|
||||||
|
|
||||||
|
downloadBtn.addEventListener('click', async function() {
|
||||||
|
downloadBtn.disabled = true;
|
||||||
|
progressContainer.style.display = 'block';
|
||||||
|
statusText.textContent = '正在准备生成任务...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 生成唯一任务ID
|
||||||
|
currentTaskId = 'task_' + Date.now();
|
||||||
|
// 2. 显示等待提示
|
||||||
|
const swalInstance = Swal.fire({
|
||||||
|
title: '正在生成压缩包',
|
||||||
|
html: '系统正在准备您的照片压缩包,这可能需要一些时间...<br><br><div id="swalProgress" style="margin:10px 0;height:10px;background:#f0f0f0;border-radius:5px;overflow:hidden;"><div id="swalProgressBar" style="height:100%;width:0%;background:#4CAF50;transition:width 0.3s"></div></div><small id="swalProgressText">0% 完成</small>',
|
||||||
|
showConfirmButton: false,
|
||||||
|
allowOutsideClick: false,
|
||||||
|
didOpen: () => {
|
||||||
|
// 连接进度事件
|
||||||
|
setupProgressListener();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 启动下载任务
|
||||||
|
const response = await fetch('/imgTool/api/download/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
taskId: currentTaskId,
|
||||||
|
proId: proId,
|
||||||
|
type: type,
|
||||||
|
proName: proName,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('启动任务失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('错误:', error);
|
||||||
|
Swal.fire({
|
||||||
|
title: '操作失败',
|
||||||
|
text: error.message,
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
resetUI();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupProgressListener() {
|
||||||
|
// 关闭之前的连接
|
||||||
|
if (eventSource) eventSource.close();
|
||||||
|
|
||||||
|
// 创建新的EventSource连接
|
||||||
|
eventSource = new EventSource(`/imgTool/api/download/progress?taskId=${currentTaskId}`);
|
||||||
|
|
||||||
|
eventSource.onmessage = function(event) {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.type === 'progress') {
|
||||||
|
// 更新进度
|
||||||
|
const progress = data.progress;
|
||||||
|
progressBar.style.width = progress + '%';
|
||||||
|
document.getElementById('swalProgressBar').style.width = progress + '%';
|
||||||
|
progressPercent.textContent = progress + '%';
|
||||||
|
document.getElementById('swalProgressText').textContent = progress + '% 完成';
|
||||||
|
processedFiles.textContent = `${data.processed}/${data.total} 文件`;
|
||||||
|
statusText.textContent = getStatusMessage(progress);
|
||||||
|
}
|
||||||
|
else if (data.type === 'complete') {
|
||||||
|
// 关闭进度连接
|
||||||
|
eventSource.close();
|
||||||
|
|
||||||
|
// 显示完成弹窗
|
||||||
|
Swal.fire({
|
||||||
|
title: '压缩包已准备好!',
|
||||||
|
text: '您的照片压缩包已生成完成。',
|
||||||
|
icon: 'success',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: '立即下载',
|
||||||
|
cancelButtonText: '稍后下载',
|
||||||
|
allowOutsideClick: false
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
// 触发下载
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = data.downloadUrl;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resetUI();
|
||||||
|
}
|
||||||
|
else if (data.type === 'error') {
|
||||||
|
// 错误处理
|
||||||
|
eventSource.close();
|
||||||
|
Swal.fire({
|
||||||
|
title: '生成失败',
|
||||||
|
text: data.message,
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
resetUI();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = function() {
|
||||||
|
eventSource.close();
|
||||||
|
Swal.fire({
|
||||||
|
title: '连接中断',
|
||||||
|
text: '与服务器的连接中断,请检查网络后重试',
|
||||||
|
icon: 'warning'
|
||||||
|
});
|
||||||
|
resetUI();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusMessage(progress) {
|
||||||
|
if (progress < 20) return '正在扫描照片文件...';
|
||||||
|
if (progress < 50) return '正在压缩照片...';
|
||||||
|
if (progress < 80) return '正在优化压缩包...';
|
||||||
|
if (progress < 100) return '即将完成...';
|
||||||
|
return '压缩完成!';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetUI() {
|
||||||
|
downloadBtn.disabled = false;
|
||||||
|
progressBar.style.width = '0%';
|
||||||
|
progressPercent.textContent = '0%';
|
||||||
|
processedFiles.textContent = '0/0 文件';
|
||||||
|
statusText.textContent = '准备就绪';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面关闭前提示
|
||||||
|
window.addEventListener('beforeunload', function(e) {
|
||||||
|
if (eventSource && eventSource.readyState === EventSource.OPEN) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = '文件正在后台生成,离开页面不会中断任务';
|
||||||
|
return e.returnValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
/*
|
||||||
document.getElementById('downloadBtn').addEventListener('click', function() {
|
document.getElementById('downloadBtn').addEventListener('click', function() {
|
||||||
const btn = this;
|
const btn = this;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
@ -69,4 +222,4 @@ document.getElementById('downloadBtn').addEventListener('click', function() {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
});
|
});
|
||||||
});
|
});*/
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -9,8 +9,9 @@
|
||||||
<script src="../../js/openIframe.js"></script>
|
<script src="../../js/openIframe.js"></script>
|
||||||
<script src="../../js/my/aes.js"></script>
|
<script src="../../js/my/aes.js"></script>
|
||||||
<script src="../../js/ajaxRequest.js"></script>
|
<script src="../../js/ajaxRequest.js"></script>
|
||||||
|
<link rel="stylesheet" href="../../css/synthesisQuery/sweetalert2.min.css">
|
||||||
</head>
|
</head>
|
||||||
<style>
|
<!-- <style>
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
|
@ -54,7 +55,7 @@
|
||||||
}
|
}
|
||||||
.notification {
|
.notification {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 66%;
|
bottom: 63%;
|
||||||
right: 55%;
|
right: 55%;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
background-color: #4CAF50;
|
background-color: #4CAF50;
|
||||||
|
|
@ -64,9 +65,71 @@
|
||||||
display: none;
|
display: none;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
</style>-->
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
#downloadBtn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
#downloadBtn:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
#downloadBtn:disabled {
|
||||||
|
background-color: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
#progressContainer {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: none;
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
height: 20px;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.progress {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
.progress-text {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.status-text {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #495057;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<h2><span style="color: red;">提示:请勿关闭浏览器、浏览器窗口、请勿睡眠,否则下载会失败!!!</span></h2>
|
<!--<h2><span style="color: red;">提示:请勿关闭浏览器、浏览器窗口、请保证电脑屏幕处于亮屏状态,否则下载会失败!!!</span></h2>
|
||||||
<h3 id="title"></h3>
|
<h3 id="title"></h3>
|
||||||
|
|
||||||
<p>点击下方按钮下载所有照片</p>
|
<p>点击下方按钮下载所有照片</p>
|
||||||
|
|
@ -82,7 +145,26 @@
|
||||||
|
|
||||||
<div id="downloadNotification" class="notification">
|
<div id="downloadNotification" class="notification">
|
||||||
您的照片压缩包已准备好!<a href="#" id="downloadLink" style="color: white; text-decoration: underline;">点击下载</a>
|
您的照片压缩包已准备好!<a href="#" id="downloadLink" style="color: white; text-decoration: underline;">点击下载</a>
|
||||||
|
</div>-->
|
||||||
|
<div class="container">
|
||||||
|
<h2><span style="color: red;">提示:请勿关闭浏览器、浏览器窗口、请保证电脑屏幕处于亮屏状态,否则下载会失败!!!</span></h2>
|
||||||
|
<h3 id="title"></h3>
|
||||||
|
<p>点击下方按钮下载所有照片</p>
|
||||||
|
|
||||||
|
<button id="downloadBtn" class="btn">开始生成压缩包</button>
|
||||||
|
|
||||||
|
<div id="progressContainer">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div id="progress" class="progress"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-text">
|
||||||
|
<span id="progressPercent">0%</span>
|
||||||
|
<span id="processedFiles">0/0 文件</span>
|
||||||
|
</div>
|
||||||
|
<div id="statusText" class="status-text">准备中...</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
<script src="../../js/synthesisQuery/sweetalert2.js"></script>
|
||||||
<script src="../../js/synthesisQuery/fileDownload.js" charset="UTF-8" type="text/javascript"></script>
|
<script src="../../js/synthesisQuery/fileDownload.js" charset="UTF-8" type="text/javascript"></script>
|
||||||
</html>
|
</html>
|
||||||
Loading…
Reference in New Issue