压缩下载优化
This commit is contained in:
parent
cb8b5cb708
commit
6b557374bf
|
|
@ -1,22 +1,34 @@
|
|||
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.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 org.apache.commons.collections4.CollectionUtils;
|
||||
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.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.nio.file.Files;
|
||||
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
|
||||
|
|
@ -33,7 +45,16 @@ public class DownloadController {
|
|||
@Autowired
|
||||
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) {
|
||||
downloadService.startDownloadTask(request.getTaskId(), request.getProId(),request.getType(),request.getProName());
|
||||
return ResponseEntity.ok("Download task started");
|
||||
|
|
@ -101,5 +122,183 @@ public class DownloadController {
|
|||
.contentLength(file.length())
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.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
|
|
@ -2,7 +2,160 @@ let proId = decryptCBC(getUrlParam('proId'));
|
|||
let type = decryptCBC(getUrlParam('type'));
|
||||
let title = decryptCBC(decodeURIComponent(getUrlParam('title')));
|
||||
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() {
|
||||
const btn = this;
|
||||
btn.disabled = true;
|
||||
|
|
@ -69,4 +222,4 @@ document.getElementById('downloadBtn').addEventListener('click', function() {
|
|||
btn.disabled = false;
|
||||
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/my/aes.js"></script>
|
||||
<script src="../../js/ajaxRequest.js"></script>
|
||||
<link rel="stylesheet" href="../../css/synthesisQuery/sweetalert2.min.css">
|
||||
</head>
|
||||
<style>
|
||||
<!-- <style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
|
|
@ -54,7 +55,7 @@
|
|||
}
|
||||
.notification {
|
||||
position: fixed;
|
||||
bottom: 66%;
|
||||
bottom: 63%;
|
||||
right: 55%;
|
||||
padding: 15px;
|
||||
background-color: #4CAF50;
|
||||
|
|
@ -64,9 +65,71 @@
|
|||
display: none;
|
||||
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>
|
||||
<body>
|
||||
<h2><span style="color: red;">提示:请勿关闭浏览器、浏览器窗口、请勿睡眠,否则下载会失败!!!</span></h2>
|
||||
<!--<h2><span style="color: red;">提示:请勿关闭浏览器、浏览器窗口、请保证电脑屏幕处于亮屏状态,否则下载会失败!!!</span></h2>
|
||||
<h3 id="title"></h3>
|
||||
|
||||
<p>点击下方按钮下载所有照片</p>
|
||||
|
|
@ -82,7 +145,26 @@
|
|||
|
||||
<div id="downloadNotification" class="notification">
|
||||
您的照片压缩包已准备好!<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>
|
||||
</body>
|
||||
<script src="../../js/synthesisQuery/sweetalert2.js"></script>
|
||||
<script src="../../js/synthesisQuery/fileDownload.js" charset="UTF-8" type="text/javascript"></script>
|
||||
</html>
|
||||
Loading…
Reference in New Issue