压缩下载

This commit is contained in:
cwchen 2025-04-07 16:15:20 +08:00
parent 280b9ef75c
commit d2b6200af8
14 changed files with 557 additions and 3 deletions

View File

@ -0,0 +1,105 @@
package com.bonus.imgTool.backstage.controller;
import com.bonus.imgTool.backstage.entity.DownloadRequest;
import com.bonus.imgTool.backstage.service.DownloadService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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;
/**
* @className:DownloadController
* @author:cwchen
* @date:2025-04-07-13:23
* @version:1.0
* @description:文件下载
*/
@RestController
@RequestMapping("/api/download")
@Slf4j
public class DownloadController {
@Autowired
private DownloadService downloadService;
@PostMapping("/start")
public ResponseEntity<String> startDownload(@RequestBody DownloadRequest request) {
downloadService.startDownloadTask(request.getTaskId(), request.getProId(),request.getType());
return ResponseEntity.ok("Download task started");
}
@GetMapping(value = "/progress", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamProgress(@RequestParam String taskId) {
SseEmitter emitter = new SseEmitter(3600000L); // 1小时超时
downloadService.addProgressListener(taskId, new DownloadService.DownloadProgressListener() {
@Override
public void onProgress(String taskId, 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.error(e.toString(),e);
}
}
@Override
public void onComplete(String taskId, 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);
}
}
@Override
public void onError(String taskId, 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);
}
}
});
return emitter;
}
@GetMapping("/file")
public ResponseEntity<Resource> downloadFile(@RequestParam String taskId) throws IOException {
File file = downloadService.getDownloadFile(taskId);
if (file == null || !file.exists()) {
return ResponseEntity.notFound().build();
}
org.springframework.core.io.Resource resource = new FileSystemResource(file);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + file.getName() + "\"")
.contentLength(file.length())
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}
}

View File

@ -3,6 +3,7 @@ package com.bonus.imgTool.backstage.controller;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.hutool.core.io.resource.InputStreamResource;
import com.bonus.imgTool.annotation.DecryptAndVerify;
import com.bonus.imgTool.annotation.LogAnnotation;
import com.bonus.imgTool.backstage.entity.ProClassifyStatisticsVo;
@ -16,9 +17,12 @@ import com.bonus.imgTool.system.vo.SysWhiteVo;
import com.bonus.imgTool.utils.ServerResponse;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.google.common.net.HttpHeaders;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@ -29,6 +33,9 @@ import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

View File

@ -18,6 +18,7 @@ public interface SynthesisQueryDao {
/**
* 综合查询-照片综合查询-照片数量
*
* @param dto
* @return SynthesisNumVo
* @author cwchen
@ -27,6 +28,7 @@ public interface SynthesisQueryDao {
/**
* 照片综合查询
*
* @param dto
* @return List<SynthesisQueryVo>
* @author cwchen
@ -36,6 +38,7 @@ public interface SynthesisQueryDao {
/**
* 收藏/取消收藏图片
*
* @param dto
* @return void
* @author cwchen
@ -49,6 +52,7 @@ public interface SynthesisQueryDao {
/**
* 生成水印照片
*
* @param vo
* @return void
* @author cwchen
@ -58,6 +62,7 @@ public interface SynthesisQueryDao {
/**
* 获取水印照片地址
*
* @param vo
* @return String
* @author cwchen
@ -67,6 +72,7 @@ public interface SynthesisQueryDao {
/**
* 项目分类统计
*
* @param dto
* @return List<ProClassifyStatisticsVo>
* @author cwchen
@ -78,6 +84,7 @@ public interface SynthesisQueryDao {
/**
* 项目分类统计-查看图片
*
* @param dto
* @return List<SynthesisQueryVo>
* @author cwchen
@ -87,6 +94,7 @@ public interface SynthesisQueryDao {
/**
* 项目分类统计-查看列表
*
* @param dto
* @return List<ProClassifyStatisticDetailVo>
* @author cwchen
@ -96,10 +104,21 @@ public interface SynthesisQueryDao {
/**
* 获取图片
*
* @param detailVo
* @return List<SynthesisQueryVo>
* @author cwchen
* @date 2025/4/6 18:35
*/
List<SynthesisQueryVo> getImgs(@Param("params") ProClassifyStatisticDetailVo detailVo, @Param("type") int type);
/**
* 查询原图/水印照片
* @param proId
* @param type
* @return List<String>
* @author cwchen
* @date 2025/4/7 10:59
*/
List<Photo> findByAlbumId(@Param("proId") String proId, @Param("type") String type);
}

View File

@ -0,0 +1,20 @@
package com.bonus.imgTool.backstage.entity;
import lombok.Data;
/**
* @className:DownloadFileVo
* @author:cwchen
* @date:2025-04-07-11:03
* @version:1.0
* @description:
*/
@Data
public class DownloadFileVo {
private String path;
private String uploadTypeName;
private String uploadType;
}

View File

@ -0,0 +1,18 @@
package com.bonus.imgTool.backstage.entity;
import lombok.Data;
/**
* @className:DownloadRequest
* @author:cwchen
* @date:2025-04-07-13:30
* @version:1.0
* @description:
*/
@Data
public class DownloadRequest {
private String taskId;
private String proId;
private String type;
}

View File

@ -0,0 +1,18 @@
package com.bonus.imgTool.backstage.entity;
import lombok.Data;
/**
* @className:Photo
* @author:cwchen
* @date:2025-04-07-13:31
* @version:1.0
* @description:
*/
@Data
public class Photo {
private String photoId;
private String albumId;
private String filePath;
private String uploadTypeName;
}

View File

@ -0,0 +1,161 @@
package com.bonus.imgTool.backstage.service;
import com.bonus.imgTool.backstage.dao.SynthesisQueryDao;
import com.bonus.imgTool.backstage.entity.Photo;
import com.bonus.imgTool.backstage.entity.ProClassifyStatisticDetailVo;
import com.bonus.imgTool.utils.SystemUtils;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* @className:DownloadService
* @author:cwchen
* @date:2025-04-07-10:46
* @version:1.0
* @description: 原图/水印下载
*/
@Service(value = "DownloadService")
public class DownloadService {
private static final Logger logger = LoggerFactory.getLogger(DownloadService.class);
@Value("${download.temp.dir:/tmp/downloads}")
private String tempDir;
@Value("${download.output.dir}")
private String outputDir;
@Resource(name = "SynthesisQueryDao")
private SynthesisQueryDao synthesisQueryDao;
private final Map<String, List<DownloadProgressListener>> progressListeners = new ConcurrentHashMap<>();
private final Map<String, String> taskFileMap = new ConcurrentHashMap<>();
@Async
public void startDownloadTask(String taskId, String proId,String type) {
Path tempDirPath = null;
try {
// 准备临时目录
tempDirPath = Paths.get(tempDir, taskId);
Files.createDirectories(tempDirPath);
Path zipFilePath = tempDirPath.resolve("photos.zip");
// 获取照片列表
List<Photo> photos = getPhotosForAlbum(proId,type);
int total = photos.size();
// 创建ZIP文件
try (ZipOutputStream zos = new ZipOutputStream(
new BufferedOutputStream(Files.newOutputStream(zipFilePath)))) {
byte[] buffer = new byte[8192];
int processed = 0;
for (Photo photo : photos) {
String path = SystemUtils.getUploadPath() + File.separator + photo.getFilePath();
Path photoPath = Paths.get(path);
String uniqueEntryName = "photos/" + photo.getPhotoId() + "_" + photoPath.getFileName();
ZipEntry entry = new ZipEntry(uniqueEntryName);
zos.putNextEntry(entry);
try (InputStream is = Files.newInputStream(photoPath)) {
int len;
while ((len = is.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
}
zos.closeEntry();
processed++;
// 更新进度 (每处理5%或至少每10张照片更新一次)
if (processed % Math.max(10, total/20) == 0 || processed == total) {
int progress = (int) ((processed * 100.0) / total);
notifyProgress(taskId, progress, processed, total);
}
}
}
// 移动到输出目录
Path outputDirPath = Paths.get(outputDir);
Files.createDirectories(outputDirPath);
Path finalFilePath = outputDirPath.resolve(taskId + ".zip");
Files.move(zipFilePath, finalFilePath, StandardCopyOption.REPLACE_EXISTING);
// 记录文件位置
taskFileMap.put(taskId, finalFilePath.toString());
// 通知完成
notifyComplete(taskId, "/imgTool/api/download/file?taskId=" + taskId);
} catch (Exception e) {
logger.error("下载任务失败: " + taskId, e);
notifyError(taskId, "文件生成失败: " + e.getMessage());
} finally {
// 清理临时目录
if (tempDirPath != null) {
try {
FileUtils.deleteDirectory(tempDirPath.toFile());
} catch (IOException e) {
logger.warn("清理临时目录失败: " + tempDirPath, e);
}
}
}
}
public void addProgressListener(String taskId, DownloadProgressListener listener) {
progressListeners.computeIfAbsent(taskId, k -> new CopyOnWriteArrayList<>()).add(listener);
}
public File getDownloadFile(String taskId) {
String filePath = taskFileMap.get(taskId);
return filePath != null ? new File(filePath) : null;
}
private List<Photo> getPhotosForAlbum(String proId,String type) {
// 实现获取照片列表的逻辑
// 返回包含所有照片路径的列表
List<Photo> list = Optional.ofNullable(synthesisQueryDao.findByAlbumId(proId,type)).orElseGet(ArrayList::new);
return list;
}
private void notifyProgress(String taskId, int progress, int processed, int total) {
List<DownloadProgressListener> listeners = progressListeners.get(taskId);
if (listeners != null) {
listeners.forEach(l -> l.onProgress(taskId, progress, processed, total));
}
}
private void notifyComplete(String taskId, String downloadUrl) {
List<DownloadProgressListener> listeners = progressListeners.remove(taskId);
if (listeners != null) {
listeners.forEach(l -> l.onComplete(taskId, downloadUrl));
}
}
private void notifyError(String taskId, String message) {
List<DownloadProgressListener> listeners = progressListeners.remove(taskId);
if (listeners != null) {
listeners.forEach(l -> l.onError(taskId, message));
}
}
public interface DownloadProgressListener {
void onProgress(String taskId, int progress, int processed, int total);
void onComplete(String taskId, String downloadUrl);
void onError(String taskId, String message);
}
}

View File

@ -0,0 +1,31 @@
package com.bonus.imgTool.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
/**
* @className:AsyncConfig
* @author:cwchen
* @date:2025-04-07-13:29
* @version:1.0
* @description:
*/
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("DownloadTask-");
executor.initialize();
return executor;
}
}

View File

@ -57,6 +57,8 @@ public class SpringThreadPoolConfig {
executor.setThreadNamePrefix(threadNamePrefix);
// 线程池对拒绝任务的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 允许核心线程在空闲时被回收
executor.setAllowCoreThreadTimeOut(true);
// 初始化
executor.initialize();
return executor;

View File

@ -62,7 +62,6 @@ public class SystemUtils {
*/
public static String getUploadPath() {
String os = getSystem();
System.err.println("当前系统是=" + os);
if ("windows".equals(os)) {
return windowsPath;
} else if ("linux".equals(os)) {

View File

@ -357,6 +357,17 @@
AND sfr.source_id = #{params.id} AND sfr.upload_type = #{params.uploadType} AND sfr.source_type = #{type} AND sfr.is_active = '1'
</where>
</select>
<!--查询原图/水印照片-->
<select id="findByAlbumId" resultType="com.bonus.imgTool.backstage.entity.Photo">
SELECT sfr.id AS photoId,
IF(#{type} = '1',sfr.original_file_path,sfr.watermark_file_path) AS filePath,
CASE sfr.upload_type WHEN '1' THEN '安全违章' WHEN '2' THEN '质量检查' WHEN '3' THEN '安全措施落实'
WHEN '4' THEN '协调照片' WHEN '5' THEN '重要事项及宣传类' ELSE '' END AS uploadTypeName
FROM tb_comprehensive_query tcq
LEFT JOIN sys_file_resource sfr ON tcq.id = sfr.source_id AND tcq.upload_type = sfr.upload_type AND sfr.is_active = '1'
WHERE tcq.pro_id = #{proId} AND tcq.is_active = '1'
ORDER BY sfr.create_time DESC
</select>
<!--收藏/取消收藏图片-->
<update id="collectData">
<if test="collectType == 1">

View File

@ -0,0 +1,71 @@
let proId = decryptCBC(getUrlParam('proId'));
let type = decryptCBC(getUrlParam('type'));
let title = decryptCBC(getUrlParam('title'));
let proName = decryptCBC(getUrlParam('proName'));
$('#title').html(proName +"-"+ title);
document.getElementById('downloadBtn').addEventListener('click', function() {
const btn = this;
btn.disabled = true;
document.getElementById('progressContainer').style.display = 'block';
// 创建任务ID
const taskId = 'task_' + Date.now();
// 使用EventSource接收服务器推送的进度更新
const eventSource = new EventSource(`/imgTool/api/download/progress?taskId=${taskId}`);
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'progress') {
// 更新进度条
document.getElementById('progress').style.width = data.progress + '%';
document.getElementById('statusText').textContent =
`正在压缩: ${data.progress}% (已处理 ${data.processed} / ${data.total} 文件)`;
}
else if (data.type === 'complete') {
// 完成处理
document.getElementById('progress').style.width = '100%';
document.getElementById('statusText').textContent = '压缩完成!';
// 显示下载通知
const notification = document.getElementById('downloadNotification');
const downloadLink = document.getElementById('downloadLink');
downloadLink.onclick = function(e) {
e.preventDefault();
window.location.href = data.downloadUrl;
notification.style.display = 'none';
window.close();
};
notification.style.display = 'block';
// 2小时后自动隐藏通知
setTimeout(() => {
notification.style.display = 'none';
}, 1000 * 60 * 60 * 2);
// 关闭EventSource连接
eventSource.close();
}
else if (data.type === 'error') {
// 错误处理
document.getElementById('statusText').textContent = '错误: ' + data.message;
btn.disabled = false;
eventSource.close();
}
};
eventSource.onerror = function() {
document.getElementById('statusText').textContent = '连接出错,请重试';
btn.disabled = false;
eventSource.close();
};
// 启动下载任务
fetch('/imgTool/api/download/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
taskId: taskId,
proId: proId,
type: type,
})
}).catch(error => {
document.getElementById('statusText').textContent = '启动任务失败';
btn.disabled = false;
eventSource.close();
});
});

View File

@ -112,8 +112,8 @@ function initTable(dataList, limit, page) {
templet: function (d) {
let html = '';
let view = "<a class='layui-icon layui-icon-file' style='cursor:pointer;' title='详情' onclick='viewData("+JSON.stringify(d)+")'></a>"
let originalDownload = "<a class='layui-icon layui-icon-download-circle' style='cursor:pointer;' title='原图下载' onclick='delData('" + d.id + "')'></a>"
let waterDownload = "<a class='layui-icon layui-icon-download-circle' style='cursor:pointer;' title='水印下载' onclick='addProcesses('" + d.id + "')'></a>";
let originalDownload = "<a class='layui-icon layui-icon-download-circle' style='cursor:pointer;' title='原图下载' onclick='downloadFile("+JSON.stringify(d)+",1)'></a>"
let waterDownload = "<a class='layui-icon layui-icon-download-circle' style='cursor:pointer;' title='水印下载' onclick='downloadFile("+JSON.stringify(d)+",2)'></a>";
html = view + originalDownload + waterDownload;
return html;
}
@ -203,4 +203,10 @@ function downloadExcel(){
};
// xhr.send(params);
xhr.send();
}
/**下载原图/水印*/
function downloadFile(obj,type){
let title = type === 1 ? "原图下载" : "水印下载";
window.open("./fileDownload.html?type="+encryptCBC(type)+"&proId="+encryptCBC(obj.proId) + "&title=" + encryptCBC(title) + "&proName=" + encryptCBC(obj.proName));
}

View File

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>图片下载</title>
<script src="../../js/libs/jquery-3.7.0.min.js" charset="UTF-8" type="text/javascript"></script>
<script src="../../js/publicJs.js"></script>
<script src="../../js/commonUtils.js"></script>
<script src="../../js/openIframe.js"></script>
<script src="../../js/my/aes.js"></script>
<script src="../../js/ajaxRequest.js"></script>
</head>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
box-sizing: border-box;
}
#downloadBtn {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
#downloadBtn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
#progressContainer {
margin-top: 20px;
display: none;
}
#progressBar {
width: 100%;
background-color: #f0f0f0;
border-radius: 4px;
height: 20px;
}
#progress {
height: 100%;
width: 0%;
background-color: #4CAF50;
border-radius: 4px;
transition: width 0.3s;
}
#statusText {
margin-top: 5px;
font-size: 14px;
}
.notification {
position: fixed;
bottom: 70%;
right: 55%;
padding: 15px;
background-color: #4CAF50;
color: white;
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
display: none;
z-index: 1000;
}
</style>
<body>
<h3 id="title">照片下载中心</h3>
<p>点击下方按钮下载所有照片</p>
<button id="downloadBtn">下载照片压缩包</button>
<div id="progressContainer">
<div id="progressBar">
<div id="progress"></div>
</div>
<div id="statusText">准备中...</div>
</div>
<div id="downloadNotification" class="notification">
您的照片压缩包已准备好!<a href="#" id="downloadLink" style="color: white; text-decoration: underline;">点击下载</a>
</div>
</body>
<script src="../../js/synthesisQuery/fileDownload.js" charset="UTF-8" type="text/javascript"></script>
</html>