数据识别导出下载

This commit is contained in:
cwchen 2025-12-30 18:36:14 +08:00
parent a579dd6418
commit 94436bd6d9
1 changed files with 101 additions and 73 deletions

View File

@ -1,6 +1,9 @@
package com.bonus.web.service.data; package com.bonus.web.service.data;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.handler.SheetWriteHandler; import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import com.bonus.common.domain.data.dto.ParamsDto; import com.bonus.common.domain.data.dto.ParamsDto;
@ -11,19 +14,17 @@ import org.springframework.stereotype.Service;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.DateUnit; import cn.hutool.core.date.DateUnit;
import cn.hutool.core.util.ZipUtil; import cn.hutool.core.util.ZipUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.File; import java.io.File;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/** /**
* 识别数据导出-业务逻辑层 (已修复 IO 关闭异常) * 识别数据导出-业务逻辑层 (多线程+防OOM版本)
*/ */
@Service(value = "ExportExcelService") @Service(value = "ExportExcelService")
@Slf4j @Slf4j
@ -32,6 +33,13 @@ public class ExportExcelService {
@Resource @Resource
private RedisTemplate<String, Object> redisTemplate; private RedisTemplate<String, Object> redisTemplate;
// 自定义线程池限制核心并发数为2最大为3防止大量图片IO导致内存溢出
private static final ExecutorService exportThreadPool = new ThreadPoolExecutor(
2, 3, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时由主线程执行防止任务堆积
);
private static final String REDIS_KEY_PREFIX = "export:task:"; private static final String REDIS_KEY_PREFIX = "export:task:";
private static final String TEMP_PATH = System.getProperty("java.io.tmpdir") + File.separator + "export_task" + File.separator; private static final String TEMP_PATH = System.getProperty("java.io.tmpdir") + File.separator + "export_task" + File.separator;
@ -46,21 +54,13 @@ public class ExportExcelService {
return taskId; return taskId;
} }
public ExportTask getTask(String taskId) {
return (ExportTask) redisTemplate.opsForValue().get(REDIS_KEY_PREFIX + taskId);
}
private void updateTaskInRedis(ExportTask task) {
redisTemplate.opsForValue().set(REDIS_KEY_PREFIX + task.getTaskId(), task, 24, TimeUnit.HOURS);
}
@Async("taskExecutor") @Async("taskExecutor")
public void runAsyncExport(String taskId, ParamsDto dto) { public void runAsyncExport(String taskId, ParamsDto dto) {
try { try {
Date start = dto.getStartTime(); Date start = dto.getStartTime();
Date end = dto.getEndTime(); Date end = dto.getEndTime();
long dayDiff = DateUtil.between(start, end, DateUnit.DAY); long dayDiff = DateUtil.between(start, end, DateUnit.DAY);
log.info("临时文件地址:{}", TEMP_PATH);
File tempDir = new File(TEMP_PATH); File tempDir = new File(TEMP_PATH);
if (!tempDir.exists()) tempDir.mkdirs(); if (!tempDir.exists()) tempDir.mkdirs();
@ -68,51 +68,58 @@ public class ExportExcelService {
String downloadFileName; String downloadFileName;
if (dayDiff <= 0) { if (dayDiff <= 0) {
// 单天任务直接处理
downloadFileName = "数据识别_" + DateUtil.formatDate(start) + ".xlsx"; downloadFileName = "数据识别_" + DateUtil.formatDate(start) + ".xlsx";
finalResultFile = new File(TEMP_PATH + taskId + "_" + downloadFileName); finalResultFile = new File(TEMP_PATH + taskId + "_" + downloadFileName);
generateExcel(finalResultFile, start, taskId, 0, 100); generateExcel(finalResultFile, start, taskId, 0, 100, true);
} else { } else {
List<File> subFiles = new ArrayList<>(); // 多天任务多线程处理
List<Date> dateRange = getDatesBetween(start, end); List<Date> dateRange = getDatesBetween(start, end);
int totalDays = dateRange.size(); int totalDays = dateRange.size();
List<File> subFiles = Collections.synchronizedList(new ArrayList<>());
CountDownLatch latch = new CountDownLatch(totalDays);
AtomicInteger finishedCount = new AtomicInteger(0);
for (int i = 0; i < totalDays; i++) { for (Date currentDate : dateRange) {
Date currentDate = dateRange.get(i); exportThreadPool.execute(() -> {
File subFile = new File(TEMP_PATH + taskId + "_" + i + ".xlsx"); try {
int startPct = (int) ((double) i / totalDays * 95); String subFileName = taskId + "_数据识别_" + DateUtil.formatDate(currentDate) + ".xlsx";
int endPct = (int) ((double) (i + 1) / totalDays * 95); File subFile = new File(TEMP_PATH + subFileName);
generateExcel(subFile, currentDate, taskId, startPct, endPct); // 传入 false表示子线程不直接更新 Redis 整体进度由主线程负责更新
subFiles.add(subFile); generateExcel(subFile, currentDate, taskId, -1, -1, false);
subFiles.add(subFile);
} catch (Exception e) {
log.error("子任务执行失败: {}", currentDate, e);
} finally {
// 更新整体进度0-95%
int finished = finishedCount.incrementAndGet();
int currentProgress = (int) ((double) finished / totalDays * 95);
updateStatusProgress(taskId, currentProgress, "running");
latch.countDown();
}
});
} }
// 最多等待子任务处理 30 分钟
latch.await(30, TimeUnit.MINUTES);
updateStatusProgress(taskId, 98, "running"); updateStatusProgress(taskId, 98, "running");
downloadFileName = "批量数据识别_" + DateUtil.formatDate(new Date()) + ".zip"; String dateRangeStr = DateUtil.format(start, "yyyyMMdd") + "-" + DateUtil.format(end, "yyyyMMdd");
downloadFileName = "批量数据识别_" + dateRangeStr + ".zip";
finalResultFile = new File(TEMP_PATH + taskId + ".zip"); finalResultFile = new File(TEMP_PATH + taskId + ".zip");
ZipUtil.zip(finalResultFile, false, subFiles.toArray(new File[0])); ZipUtil.zip(finalResultFile, false, subFiles.toArray(new File[0]));
subFiles.forEach(File::delete); subFiles.forEach(File::delete);
} }
ExportTask task = getTask(taskId); // 完成任务更新
if (task != null) { completeTask(taskId, finalResultFile, downloadFileName);
task.setFilePath(finalResultFile.getAbsolutePath());
task.setFileName(downloadFileName);
task.setProgress(100);
task.setStatus("completed");
updateTaskInRedis(task);
}
} catch (Exception e) { } catch (Exception e) {
log.error("导出异步任务失败", e); handleError(taskId, e);
ExportTask task = getTask(taskId);
if (task != null) {
task.setStatus("failed");
task.setMessage("导出异常: " + e.getMessage());
updateTaskInRedis(task);
}
} }
} }
private void generateExcel(File file, Date date, String taskId, int baseProgress, int maxProgress) { private void generateExcel(File file, Date date, String taskId, int basePct, int maxPct, boolean updateRedis) {
// 定义冻结表头的拦截器
SheetWriteHandler freezePaneHandler = new SheetWriteHandler() { SheetWriteHandler freezePaneHandler = new SheetWriteHandler() {
@Override @Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
@ -121,14 +128,16 @@ public class ExportExcelService {
writeSheetHolder.getSheet().createFreezePane(0, 1, 0, 1); writeSheetHolder.getSheet().createFreezePane(0, 1, 0, 1);
} }
}; };
ExcelWriter excelWriter = null; ExcelWriter excelWriter = null;
try { try {
// 修复点1: 显式构建并管理 ExcelWriter不完全依赖外部 try-with-resources excelWriter = EasyExcel.write(file, DataRecognitionExportVO.class)
excelWriter = EasyExcel.write(file, DataRecognitionExportVO.class).registerWriteHandler(freezePaneHandler).build(); .registerWriteHandler(freezePaneHandler)
.build();
WriteSheet writeSheet = EasyExcel.writerSheet("识别数据").build(); WriteSheet writeSheet = EasyExcel.writerSheet("识别数据").build();
int pageNo = 1; int pageNo = 1;
int pageSize = 200; // 修复点2: 有图片时减小 pageSize (建议200)降低 IO 负载 int pageSize = 150; // 多线程下进一步减小 pageSize 降低内存压力
long totalCount = mockDbCount(date); long totalCount = mockDbCount(date);
int totalPages = (int) Math.ceil((double) totalCount / pageSize); int totalPages = (int) Math.ceil((double) totalCount / pageSize);
@ -144,56 +153,71 @@ public class ExportExcelService {
for (int i = 0; i < dbDataList.size(); i++) { for (int i = 0; i < dbDataList.size(); i++) {
DataRecognitionExportVO entity = dbDataList.get(i); DataRecognitionExportVO entity = dbDataList.get(i);
DataRecognitionExportVO vo = new DataRecognitionExportVO(); DataRecognitionExportVO vo = new DataRecognitionExportVO();
// 属性映射...
// 属性映射
vo.setIndex((pageNo - 1) * pageSize + i + 1); vo.setIndex((pageNo - 1) * pageSize + i + 1);
vo.setLocation(entity.getLocation()); vo.setLocation(entity.getLocation());
vo.setRecognitionTime(entity.getRecognitionTime()); vo.setRecognitionTime(entity.getRecognitionTime());
vo.setPlateColor(entity.getPlateColor()); vo.setPlateColor(entity.getPlateColor());
vo.setVehicleType(entity.getVehicleType()); vo.setVehicleType(entity.getVehicleType());
// 修复点3: 图片路径防御性处理防止读取流失败中断整个写入
try { try {
String absolutePath = entity.getPhotoPath(); if (entity.getPhotoPath() != null) {
if (absolutePath != null && !absolutePath.isEmpty()) { File img = new File(entity.getPhotoPath());
File imageFile = new File(absolutePath); if (img.exists() && img.canRead()) vo.setPhotoFile(img);
if (imageFile.exists() && imageFile.canRead() && imageFile.isFile()) {
vo.setPhotoFile(imageFile);
}
} }
} catch (Exception imgEx) { } catch (Exception ignored) {}
log.warn("行 {} 图片读取失败,跳过图片插入: {}", vo.getIndex(), imgEx.getMessage());
}
voList.add(vo); voList.add(vo);
} }
excelWriter.write(voList, writeSheet); excelWriter.write(voList, writeSheet);
// 更新进度 // 单天模式下更新详细进度多天模式由主任务更新此处置空
double fileRatio = (double) pageNo / totalPages; if (updateRedis) {
int currentProgress = baseProgress + (int) (fileRatio * (maxProgress - baseProgress)); double fileRatio = (double) pageNo / totalPages;
if (currentProgress >= 100) currentProgress = 99; int progress = basePct + (int) (fileRatio * (maxPct - basePct));
updateStatusProgress(taskId, currentProgress, "running"); updateStatusProgress(taskId, Math.min(progress, 99), "running");
}
voList.clear(); voList.clear(); // 显式清理辅助 GC
pageNo++; pageNo++;
} }
} catch (Exception e) {
log.error("Excel 生成核心逻辑失败: ", e);
throw e;
} finally { } finally {
// 修复点4: 确保在 finally 中安全关闭并捕获关闭时的异常
if (excelWriter != null) { if (excelWriter != null) {
try { try { excelWriter.finish(); } catch (Exception e) { log.error("IO Close Error", e); }
excelWriter.finish(); // finish 内部会调用 close() 并清理 IO
} catch (Exception closeEx) {
log.error("关闭 ExcelWriter IO 失败: ", closeEx);
}
} }
} }
} }
private void completeTask(String taskId, File file, String name) {
ExportTask task = getTask(taskId);
if (task != null) {
task.setFilePath(file.getAbsolutePath());
task.setFileName(name);
task.setProgress(100);
task.setStatus("completed");
updateTaskInRedis(task);
}
}
private void handleError(String taskId, Exception e) {
log.error("Export Error", e);
ExportTask task = getTask(taskId);
if (task != null) {
task.setStatus("failed");
task.setMessage(e.getMessage());
updateTaskInRedis(task);
}
}
// ... getTask, updateTaskInRedis, getDatesBetween, updateStatusProgress 保持不变 ...
public ExportTask getTask(String taskId) {
return (ExportTask) redisTemplate.opsForValue().get(REDIS_KEY_PREFIX + taskId);
}
private void updateTaskInRedis(ExportTask task) {
redisTemplate.opsForValue().set(REDIS_KEY_PREFIX + task.getTaskId(), task, 24, TimeUnit.HOURS);
}
private void updateStatusProgress(String taskId, int progress, String status) { private void updateStatusProgress(String taskId, int progress, String status) {
ExportTask task = getTask(taskId); ExportTask task = getTask(taskId);
if (task != null) { if (task != null) {
@ -208,12 +232,16 @@ public class ExportExcelService {
Calendar cal = Calendar.getInstance(); Calendar cal = Calendar.getInstance();
cal.setTime(start); cal.setTime(start);
while (cal.getTime().getTime() <= end.getTime()) { while (cal.getTime().getTime() <= end.getTime()) {
list.add(cal.getTime()); list.add(new Date(cal.getTime().getTime()));
cal.add(Calendar.DATE, 1); cal.add(Calendar.DATE, 1);
} }
return list; return list;
} }
/*private long mockDbCount(Date date) { return 500; }
private List<DataRecognitionExportVO> mockDbQuery(Date date, int p, int s) {
return new ArrayList<>(); // 替换为实际查询
}*/
private long mockDbCount(Date date) { private long mockDbCount(Date date) {
return 1000; // 模拟少量数据测试 return 1000; // 模拟少量数据测试
} }