package com.vincent.rsf.server.common.service.impl; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.vincent.rsf.framework.common.Cools; import com.vincent.rsf.framework.exception.CoolException; import com.vincent.rsf.server.common.domain.BaseParam; import com.vincent.rsf.server.common.service.AsyncListExportTaskService; import com.vincent.rsf.server.common.service.ListExportHandler; import com.vincent.rsf.server.common.service.ListExportService; import com.vincent.rsf.server.system.entity.ExportTask; import com.vincent.rsf.server.system.service.ExportTaskService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.usermodel.Workbook; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.function.Function; @Slf4j @Service @RequiredArgsConstructor public class AsyncListExportTaskServiceImpl implements AsyncListExportTaskService { private final ExportTaskService exportTaskService; private final ListExportService listExportService; @Value("${logging.file.path:logs}") private String loggingFilePath; @Value("${rsf.export-task.expire-days:7}") private int exportTaskExpireDays; @Override public ExportTask createTask( String resourceKey, String defaultReportTitle, Map payload, Long tenantId, Long userId ) { ExportTask task = new ExportTask(); task.setTaskCode(generateTaskCode()); task.setResourceKey(resourceKey); task.setReportTitle(resolveReportTitle(payload, defaultReportTitle)); task.setStatus(ExportTask.STATUS_PENDING); task.setPayloadJson(JSON.toJSONString(payload)); task.setDeleted(0); task.setTenantId(tenantId); task.setCreateBy(userId); task.setUpdateBy(userId); task.setCreateTime(new Date()); task.setUpdateTime(new Date()); task.setExpireTime(resolveExpireTime()); exportTaskService.save(task); return task; } @Override public ExportTask getTask(Long taskId, String resourceKey, Long tenantId, Long userId) { return exportTaskService.getOne( buildTaskQuery(taskId, tenantId, userId) .eq(ExportTask::getResourceKey, resourceKey), false ); } @Override public ExportTask getTask(Long taskId, Long tenantId, Long userId) { return exportTaskService.getOne(buildTaskQuery(taskId, tenantId, userId), false); } @Override public File getDownloadFile(Long taskId, String resourceKey, Long tenantId, Long userId) { ExportTask task = getTask(taskId, resourceKey, tenantId, userId); return resolveDownloadFile(task); } @Override public File getDownloadFile(Long taskId, Long tenantId, Long userId) { ExportTask task = getTask(taskId, tenantId, userId); return resolveDownloadFile(task); } private LambdaQueryWrapper buildTaskQuery(Long taskId, Long tenantId, Long userId) { return new LambdaQueryWrapper() .eq(ExportTask::getId, taskId) .eq(ExportTask::getDeleted, 0) .eq(ExportTask::getTenantId, tenantId) .eq(ExportTask::getCreateBy, userId); } private File resolveDownloadFile(ExportTask task) { if (task == null) { throw new CoolException("导出任务不存在"); } if (!Objects.equals(task.getStatus(), ExportTask.STATUS_SUCCESS)) { throw new CoolException("导出任务尚未完成"); } if (Cools.isEmpty(task.getFilePath())) { throw new CoolException("导出文件不存在"); } File file = new File(task.getFilePath()); if (!file.exists()) { throw new CoolException("导出文件不存在"); } return file; } @Override @Async("taskExecutor") public void executeAsync( Long taskId, String resourceKey, Map payload, Function, P> paramBuilder, ListExportHandler exportHandler ) { ExportTask task = exportTaskService.getById(taskId); if (task == null) { return; } task.setStatus(ExportTask.STATUS_PROCESSING); task.setUpdateTime(new Date()); exportTaskService.updateById(task); try { ListExportService.ExportWorkbook exportWorkbook = listExportService.prepareExportWorkbook(payload, paramBuilder, exportHandler); File outputFile = createOutputFile(taskId, resourceKey, task.getReportTitle()); writeWorkbook(exportWorkbook.workbook(), outputFile); task.setStatus(ExportTask.STATUS_SUCCESS); task.setRowCount(exportWorkbook.rowCount()); task.setFileName(outputFile.getName()); task.setFilePath(outputFile.getAbsolutePath()); task.setErrorMsg(null); task.setUpdateTime(new Date()); exportTaskService.updateById(task); } catch (Exception error) { log.error("异步导出失败, resourceKey={}, taskId={}", resourceKey, taskId, error); task.setStatus(ExportTask.STATUS_FAILED); task.setErrorMsg(truncateErrorMessage(error)); task.setUpdateTime(new Date()); exportTaskService.updateById(task); } } private String resolveReportTitle(Map payload, String defaultReportTitle) { Object metaObject = payload.get("meta"); if (metaObject instanceof Map metaMap) { Object reportTitle = metaMap.get("reportTitle"); if (reportTitle != null && !String.valueOf(reportTitle).trim().isEmpty()) { return String.valueOf(reportTitle).trim(); } } return defaultReportTitle; } private String generateTaskCode() { return "EXP-" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + "-" + UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase(); } private Date resolveExpireTime() { return Date.from(Instant.now().plus(Math.max(exportTaskExpireDays, 1), ChronoUnit.DAYS)); } private File createOutputFile(Long taskId, String resourceKey, String reportTitle) { String safeTitle = sanitizeFileName(reportTitle); String safeResourceKey = sanitizePathSegment(resourceKey); String dayFolder = new SimpleDateFormat("yyyyMMdd").format(new Date()); File dir = new File(loggingFilePath, "export-tasks/" + safeResourceKey + "/" + dayFolder); if (!dir.exists() && !dir.mkdirs()) { throw new CoolException("导出目录创建失败"); } String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); return new File(dir, safeTitle + "-" + taskId + "-" + timestamp + ".xlsx"); } private void writeWorkbook(Workbook workbook, File outputFile) throws IOException { try (Workbook targetWorkbook = workbook; FileOutputStream outputStream = new FileOutputStream(outputFile)) { targetWorkbook.write(outputStream); outputStream.flush(); } } private String sanitizeFileName(String fileName) { String normalized = Objects.toString(fileName, "导出报表").trim(); if (normalized.isEmpty()) { normalized = "导出报表"; } return normalized.replaceAll("[\\\\/:*?\"<>|]", "_"); } private String sanitizePathSegment(String segment) { String normalized = Objects.toString(segment, "default").trim(); if (Cools.isEmpty(normalized)) { normalized = "default"; } return normalized.replaceAll("[\\\\/:*?\"<>|]", "_"); } private String truncateErrorMessage(Exception error) { String message = error == null ? "" : Objects.toString(error.getMessage(), error.getClass().getSimpleName()); if (message.length() <= 500) { return message; } return message.substring(0, 500); } }