fix: 优化综合导出,增加了异步导出功能
This commit is contained in:
parent
c84a405453
commit
766899bb51
@ -16,6 +16,8 @@ public class EngVmsstbprptVo implements Serializable {
|
||||
private String id;
|
||||
private String sttp;
|
||||
private String sttpCode;
|
||||
private String sttpMap;
|
||||
private String logo;
|
||||
private String sttpName;
|
||||
private String sttpFullPath;
|
||||
private String sttpTreeLevel;
|
||||
|
||||
@ -1079,6 +1079,7 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl<SdEngInfoBHMapper, SdEng
|
||||
return "SELECT " +
|
||||
"eng.STCD AS id, " +
|
||||
"'ENG' AS sttpCode, " +
|
||||
"'ENG' AS sttpMap, " +
|
||||
"sttp.FULL_PATH AS sttpFullPath, " +
|
||||
"sttp.TREE_LEVEL AS sttpTreeLevel, " +
|
||||
"sttp.ID AS sttp, " +
|
||||
@ -1095,6 +1096,7 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl<SdEngInfoBHMapper, SdEng
|
||||
"eng.HBRVCD AS hbrvcd, " +
|
||||
"hbrv.HBRVNM AS hbrvcdName, " +
|
||||
"eng.RVCD AS rvcd, " +
|
||||
"eng.LOGO AS logo, " +
|
||||
"rv.RVNM AS rvcdName, " +
|
||||
"rv.PATH AS rvcdFullPath, " +
|
||||
"addv.ADDVNM AS addvcdName, " +
|
||||
|
||||
@ -1,34 +1,26 @@
|
||||
package com.yfd.platform.qgc_export.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.yfd.platform.annotation.Log;
|
||||
import com.yfd.platform.qgc_export.domain.ExportTask;
|
||||
import com.yfd.platform.qgc_export.service.IQgcExportService;
|
||||
import com.yfd.platform.qgc_export.service.ExportTaskService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 综合导出控制器
|
||||
* <p>
|
||||
* 前端传参示例:
|
||||
* <pre>{@code
|
||||
* POST /qgcExport/exportData
|
||||
* {
|
||||
* "tmDimension": "month",
|
||||
* "dataField": "v,q,z",
|
||||
* "months": "2026-08,2026-07",
|
||||
* "stcd": "00001,00002",
|
||||
* "sysId": "qgc",
|
||||
* "isDailyDimension": true,
|
||||
* "isCalcQec": true
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/qgcExport")
|
||||
@ -38,8 +30,14 @@ public class QgcExportController {
|
||||
@Resource
|
||||
private IQgcExportService qgcExportService;
|
||||
|
||||
@Resource
|
||||
private ExportTaskService exportTaskService;
|
||||
|
||||
/**
|
||||
* 原有同步导出接口(保留兼容)
|
||||
*/
|
||||
@PostMapping("/exportData")
|
||||
@Operation(summary = "综合数据导出(ZIP 含多 Sheet Excel)")
|
||||
@Operation(summary = "综合数据导出(ZIP 含多 Sheet Excel)— 同步返回文件流")
|
||||
public void exportData(@RequestParam(required = false) String tmDimension,
|
||||
@RequestParam(required = false) String dataField,
|
||||
@RequestParam(required = false) String months,
|
||||
@ -52,7 +50,6 @@ public class QgcExportController {
|
||||
@RequestParam(required = false) String baseId,
|
||||
HttpServletResponse response) {
|
||||
|
||||
// 解析逗号分隔参数
|
||||
List<String> dataFields = parseCsv(dataField);
|
||||
List<String> monthList = parseCsv(months);
|
||||
List<String> stationList = parseCsv(stcd);
|
||||
@ -62,7 +59,6 @@ public class QgcExportController {
|
||||
tmDimension = "month";
|
||||
}
|
||||
|
||||
// 年度导出分支
|
||||
if ("year".equals(tmDimension)) {
|
||||
List<String> yearStatTypeList = parseCsv(yearStatType);
|
||||
List<String> yearList = parseCsv(year);
|
||||
@ -74,6 +70,122 @@ public class QgcExportController {
|
||||
isDailyDimension, tmDimension, response);
|
||||
}
|
||||
|
||||
// ======================== 异步导出三接口 ========================
|
||||
|
||||
/**
|
||||
* 接口一:创建导出任务,返回唯一任务 ID
|
||||
*/
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建异步导出任务,返回 taskId")
|
||||
public Map<String, Object> createTask(@RequestParam(required = false) String tmDimension,
|
||||
@RequestParam(required = false) String dataField,
|
||||
@RequestParam(required = false) String months,
|
||||
@RequestParam(required = false) String stcd,
|
||||
@RequestParam(defaultValue = "true") boolean isDailyDimension,
|
||||
@RequestParam(required = false) String yearStatType,
|
||||
@RequestParam(required = false) String year,
|
||||
@RequestParam(required = false) String baseId) {
|
||||
|
||||
List<String> dataFields = parseCsv(dataField);
|
||||
List<String> monthList = parseCsv(months);
|
||||
List<String> stationList = parseCsv(stcd);
|
||||
List<String> baseIdList = parseCsv(baseId);
|
||||
|
||||
if (tmDimension == null || tmDimension.isEmpty()) {
|
||||
tmDimension = "month";
|
||||
}
|
||||
|
||||
String taskId;
|
||||
if ("year".equals(tmDimension)) {
|
||||
List<String> yearStatTypeList = parseCsv(yearStatType);
|
||||
List<String> yearList = parseCsv(year);
|
||||
taskId = exportTaskService.submitYearExport(yearStatTypeList, yearList, stationList, baseIdList);
|
||||
} else {
|
||||
taskId = exportTaskService.submitMonthExport(dataFields, monthList, stationList,
|
||||
isDailyDimension, tmDimension);
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("taskId", taskId);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口二:查询任务状态
|
||||
*/
|
||||
@GetMapping("/status")
|
||||
@Operation(summary = "查询异步导出任务状态")
|
||||
public Map<String, Object> getStatus(@RequestParam String taskId) {
|
||||
ExportTask task = exportTaskService.getTask(taskId);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
if (task == null) {
|
||||
result.put("exists", false);
|
||||
return result;
|
||||
}
|
||||
result.put("exists", true);
|
||||
result.put("taskId", task.getTaskId());
|
||||
result.put("status", task.getStatus().name());
|
||||
result.put("statusDesc", task.getStatus().getDesc());
|
||||
if (task.getErrorMessage() != null) {
|
||||
result.put("errorMessage", task.getErrorMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口三:下载导出文件(参数与 /exportData 一致 + taskId,文件名根据 tmDimension 生成)
|
||||
*/
|
||||
@PostMapping("/download")
|
||||
@Operation(summary = "下载异步导出的 ZIP 文件")
|
||||
public void download(@RequestParam String taskId,
|
||||
@RequestParam(required = false) String tmDimension,
|
||||
@RequestParam(required = false) String dataField,
|
||||
@RequestParam(required = false) String months,
|
||||
@RequestParam(required = false) String stcd,
|
||||
@RequestParam(defaultValue = "true") boolean isDailyDimension,
|
||||
@RequestParam(required = false) String yearStatType,
|
||||
@RequestParam(required = false) String year,
|
||||
@RequestParam(required = false) String baseId,
|
||||
HttpServletResponse response) {
|
||||
ExportTask task = exportTaskService.getTask(taskId);
|
||||
if (task == null || task.getStatus() != ExportTask.Status.DONE) {
|
||||
response.setStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tmDimension == null || tmDimension.isEmpty()) {
|
||||
tmDimension = "month";
|
||||
}
|
||||
|
||||
String zipFileName = getFileNamePrefix(tmDimension) + ".zip";
|
||||
response.setContentType("application/zip");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
try {
|
||||
response.setHeader("Content-Disposition",
|
||||
"attachment; filename=" + URLEncoder.encode(zipFileName, "UTF-8"));
|
||||
} catch (IOException e) {
|
||||
response.setHeader("Content-Disposition", "attachment; filename=" + zipFileName);
|
||||
}
|
||||
|
||||
try (OutputStream os = response.getOutputStream()) {
|
||||
exportTaskService.downloadTask(taskId, os);
|
||||
} catch (IOException e) {
|
||||
response.setStatus(500);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 工具方法 ========================
|
||||
|
||||
private static String getFileNamePrefix(String tmDimension) {
|
||||
return switch (tmDimension) {
|
||||
case "year" -> "年度报表";
|
||||
case "month" -> "月度报表";
|
||||
case "day" -> "日度报表";
|
||||
case "hour" -> "小时报表";
|
||||
default -> "未知报表";
|
||||
};
|
||||
}
|
||||
|
||||
private List<String> parseCsv(String value) {
|
||||
if (StrUtil.isBlank(value)) {
|
||||
return Collections.emptyList();
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
package com.yfd.platform.qgc_export.domain;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 导出任务 — 用于异步导出
|
||||
*/
|
||||
public class ExportTask {
|
||||
|
||||
public enum Status {
|
||||
CREATED("已创建"),
|
||||
PROCESSING("处理中"),
|
||||
DONE("已完成"),
|
||||
ERROR("失败");
|
||||
|
||||
private final String desc;
|
||||
|
||||
Status(String desc) { this.desc = desc; }
|
||||
|
||||
public String getDesc() { return desc; }
|
||||
}
|
||||
|
||||
/** 任务唯一 ID */
|
||||
private String taskId;
|
||||
/** 任务状态 */
|
||||
private Status status;
|
||||
/** 导出完成的临时文件 */
|
||||
private File file;
|
||||
/** ZIP 文件名 */
|
||||
private String fileName;
|
||||
/** 错误信息(status=ERROR 时) */
|
||||
private String errorMessage;
|
||||
/** 创建时间戳 */
|
||||
private long createTime;
|
||||
|
||||
public ExportTask(String taskId) {
|
||||
this.taskId = taskId;
|
||||
this.status = Status.CREATED;
|
||||
this.createTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public String getTaskId() { return taskId; }
|
||||
public void setTaskId(String taskId) { this.taskId = taskId; }
|
||||
|
||||
public Status getStatus() { return status; }
|
||||
public void setStatus(Status status) { this.status = status; }
|
||||
|
||||
public File getFile() { return file; }
|
||||
public void setFile(File file) { this.file = file; }
|
||||
|
||||
public String getFileName() { return fileName; }
|
||||
public void setFileName(String fileName) { this.fileName = fileName; }
|
||||
|
||||
public String getErrorMessage() { return errorMessage; }
|
||||
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
|
||||
|
||||
public long getCreateTime() { return createTime; }
|
||||
}
|
||||
@ -0,0 +1,227 @@
|
||||
package com.yfd.platform.qgc_export.service;
|
||||
|
||||
import com.yfd.platform.qgc_export.domain.ExportTask;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.ServletOutputStream;
|
||||
import jakarta.servlet.WriteListener;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpServletResponseWrapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 导出任务管理服务 — 异步导出 + 状态查询 + 文件下载
|
||||
*/
|
||||
@Service
|
||||
public class ExportTaskService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ExportTaskService.class);
|
||||
|
||||
@Resource
|
||||
private IQgcExportService qgcExportService;
|
||||
|
||||
/** 任务缓存(taskId → task) */
|
||||
private final Map<String, ExportTask> taskMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 创建月度/小时/日导出任务,返回任务 ID
|
||||
*/
|
||||
public String submitMonthExport(List<String> dataFields, List<String> months,
|
||||
List<String> stationCodes, boolean isDailyDimension,
|
||||
String tmDimension) {
|
||||
String taskId = UUID.randomUUID().toString().replace("-", "");
|
||||
ExportTask task = new ExportTask(taskId);
|
||||
task.setStatus(ExportTask.Status.PROCESSING);
|
||||
taskMap.put(taskId, task);
|
||||
|
||||
CompletableFuture.runAsync(() -> {
|
||||
File tempFile = null;
|
||||
try {
|
||||
tempFile = File.createTempFile("qgc_export_", ".zip");
|
||||
FileOutputStream fos = new FileOutputStream(tempFile);
|
||||
CapturingResponse capturing = new CapturingResponse(fos);
|
||||
qgcExportService.exportData(dataFields, months, stationCodes,
|
||||
isDailyDimension, tmDimension, capturing);
|
||||
capturing.flushAndClose();
|
||||
fos.close();
|
||||
task.setFile(tempFile);
|
||||
task.setStatus(ExportTask.Status.DONE);
|
||||
log.info("导出任务 {} 完成,文件: {}", taskId, tempFile.getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
log.error("导出任务 {} 失败", taskId, e);
|
||||
task.setErrorMessage(e.getMessage());
|
||||
task.setStatus(ExportTask.Status.ERROR);
|
||||
if (tempFile != null) {
|
||||
try { Files.deleteIfExists(tempFile.toPath()); } catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建年度导出任务,返回任务 ID
|
||||
*/
|
||||
public String submitYearExport(List<String> yearStatType, List<String> years,
|
||||
List<String> stationCodes, List<String> baseIds) {
|
||||
String taskId = UUID.randomUUID().toString().replace("-", "");
|
||||
ExportTask task = new ExportTask(taskId);
|
||||
task.setStatus(ExportTask.Status.PROCESSING);
|
||||
taskMap.put(taskId, task);
|
||||
|
||||
CompletableFuture.runAsync(() -> {
|
||||
File tempFile = null;
|
||||
try {
|
||||
tempFile = File.createTempFile("qgc_export_", ".zip");
|
||||
FileOutputStream fos = new FileOutputStream(tempFile);
|
||||
CapturingResponse capturing = new CapturingResponse(fos);
|
||||
qgcExportService.exportYearData(yearStatType, years, stationCodes,
|
||||
baseIds, capturing);
|
||||
capturing.flushAndClose();
|
||||
fos.close();
|
||||
task.setFile(tempFile);
|
||||
task.setStatus(ExportTask.Status.DONE);
|
||||
log.info("年度导出任务 {} 完成,文件: {}", taskId, tempFile.getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
log.error("年度导出任务 {} 失败", taskId, e);
|
||||
task.setErrorMessage(e.getMessage());
|
||||
task.setStatus(ExportTask.Status.ERROR);
|
||||
if (tempFile != null) {
|
||||
try { Files.deleteIfExists(tempFile.toPath()); } catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询任务状态
|
||||
*/
|
||||
public ExportTask getTask(String taskId) {
|
||||
return taskMap.get(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将任务文件写入 OutputStream(下载),写完后删除临时文件和任务缓存
|
||||
*/
|
||||
public void downloadTask(String taskId, OutputStream os) throws IOException {
|
||||
ExportTask task = taskMap.get(taskId);
|
||||
if (task == null || task.getStatus() != ExportTask.Status.DONE || task.getFile() == null) {
|
||||
throw new IllegalStateException("任务未完成或文件不存在");
|
||||
}
|
||||
try (FileInputStream fis = new FileInputStream(task.getFile())) {
|
||||
byte[] buf = new byte[8192];
|
||||
int len;
|
||||
while ((len = fis.read(buf)) != -1) {
|
||||
os.write(buf, 0, len);
|
||||
}
|
||||
} finally {
|
||||
try { Files.deleteIfExists(task.getFile().toPath()); } catch (IOException e) {
|
||||
log.warn("删除临时文件失败: {}", task.getFile(), e);
|
||||
}
|
||||
taskMap.remove(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理超时任务(可由定时任务调用)
|
||||
*/
|
||||
public void cleanExpiredTasks(long timeoutMs) {
|
||||
long now = System.currentTimeMillis();
|
||||
taskMap.entrySet().removeIf(entry -> {
|
||||
ExportTask task = entry.getValue();
|
||||
if (now - task.getCreateTime() > timeoutMs) {
|
||||
if (task.getFile() != null) {
|
||||
try { Files.deleteIfExists(task.getFile().toPath()); } catch (IOException ignored) {}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 内部类 ====================
|
||||
|
||||
/**
|
||||
* 最小化 HttpServletResponse — 基于 HttpServletResponseWrapper,仅重写导出用到的 5 个方法
|
||||
*/
|
||||
private static class CapturingResponse extends HttpServletResponseWrapper {
|
||||
private final FileOutputStream fos;
|
||||
private final ServletOutputStream sos;
|
||||
|
||||
/** 兜底 response,仅用于满足 super() 非 null 约束,实际所有调用都被本类重写 */
|
||||
private static final HttpServletResponse NOOP = new HttpServletResponse() {
|
||||
@Override public ServletOutputStream getOutputStream() { throw new UnsupportedOperationException(); }
|
||||
@Override public java.io.PrintWriter getWriter() { throw new UnsupportedOperationException(); }
|
||||
@Override public String getCharacterEncoding() { return null; }
|
||||
@Override public String getContentType() { return null; }
|
||||
@Override public void setCharacterEncoding(String charset) {}
|
||||
@Override public void setContentLength(int len) {}
|
||||
@Override public void setContentLengthLong(long len) {}
|
||||
@Override public void setContentType(String type) {}
|
||||
@Override public void setBufferSize(int size) {}
|
||||
@Override public int getBufferSize() { return 0; }
|
||||
@Override public void flushBuffer() {}
|
||||
@Override public void resetBuffer() {}
|
||||
@Override public boolean isCommitted() { return false; }
|
||||
@Override public void reset() {}
|
||||
@Override public void setLocale(java.util.Locale loc) {}
|
||||
@Override public java.util.Locale getLocale() { return java.util.Locale.getDefault(); }
|
||||
@Override public void addCookie(jakarta.servlet.http.Cookie cookie) {}
|
||||
@Override public boolean containsHeader(String name) { return false; }
|
||||
@Override public String encodeURL(String url) { return url; }
|
||||
@Override public String encodeRedirectURL(String url) { return url; }
|
||||
@Override public void sendError(int sc, String msg) {}
|
||||
@Override public void sendError(int sc) {}
|
||||
@Override public void sendRedirect(String location) {}
|
||||
@Override public void sendRedirect(String location, int sc, boolean clearBuffer) {}
|
||||
@Override public void setDateHeader(String name, long date) {}
|
||||
@Override public void addDateHeader(String name, long date) {}
|
||||
@Override public void setHeader(String name, String value) {}
|
||||
@Override public void addHeader(String name, String value) {}
|
||||
@Override public void setIntHeader(String name, int value) {}
|
||||
@Override public void addIntHeader(String name, int value) {}
|
||||
@Override public void setStatus(int sc) {}
|
||||
@Override public int getStatus() { return 200; }
|
||||
@Override public String getHeader(String name) { return null; }
|
||||
@Override public java.util.Collection<String> getHeaders(String name) { return java.util.Collections.emptyList(); }
|
||||
@Override public java.util.Collection<String> getHeaderNames() { return java.util.Collections.emptyList(); }
|
||||
};
|
||||
|
||||
CapturingResponse(FileOutputStream fos) {
|
||||
super(NOOP);
|
||||
this.fos = fos;
|
||||
this.sos = new ServletOutputStream() {
|
||||
@Override public void write(int b) throws IOException { fos.write(b); }
|
||||
@Override public boolean isReady() { return true; }
|
||||
@Override public void setWriteListener(WriteListener listener) {}
|
||||
};
|
||||
}
|
||||
|
||||
void flushAndClose() throws IOException {
|
||||
sos.flush();
|
||||
fos.flush();
|
||||
}
|
||||
|
||||
@Override public ServletOutputStream getOutputStream() { return sos; }
|
||||
@Override public void setContentType(String type) {}
|
||||
@Override public void setCharacterEncoding(String charset) {}
|
||||
@Override public void setHeader(String name, String value) {}
|
||||
@Override public String getCharacterEncoding() { return "UTF-8"; }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user