diff --git a/src/main/java/com/xujie/sys/common/utils/StreamCsvExportUtil.java b/src/main/java/com/xujie/sys/common/utils/StreamCsvExportUtil.java
new file mode 100644
index 00000000..18fd8ccc
--- /dev/null
+++ b/src/main/java/com/xujie/sys/common/utils/StreamCsvExportUtil.java
@@ -0,0 +1,495 @@
+package com.xujie.sys.common.utils;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.function.Function;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * 流式CSV导出工具类(真正的流式导出,边查边写)- rqrq
+ *
+ *
功能特点:
+ *
+ * - 支持单CSV和多CSV(ZIP压缩包)两种模式
+ * - 使用GB18030编码,兼容Excel中文显示
+ * - 自动处理CSV字段转义(逗号、引号、换行)
+ * - 支持强制文本格式(保留前导零)
+ * - 内置进度日志记录
+ *
+ *
+ * 使用方式:
+ *
+ * // 1. 创建Writer
+ * StreamCsvExportUtil.CsvStreamWriter writer = StreamCsvExportUtil.createSingleCsvWriter(response.getOutputStream(), "导出文件.csv");
+ * // 或创建ZIP Writer
+ * StreamCsvExportUtil.CsvStreamWriter writer = StreamCsvExportUtil.createZipCsvWriter(response.getOutputStream(), "导出文件.zip", "数据");
+ *
+ * // 2. 写入表头
+ * writer.writeHeader("列1", "列2", "列3");
+ *
+ * // 3. 在ResultHandler中逐行写入
+ * mapper.streamQuery(query, result -> {
+ * writer.writeRow(
+ * writer.asText(result.getField1()), // 强制文本格式
+ * writer.escape(result.getField2()), // 普通转义
+ * writer.formatDate(result.getDate()) // 日期格式化
+ * );
+ * });
+ *
+ * // 4. 关闭Writer
+ * writer.close();
+ *
+ *
+ * @author rqrq
+ * @date 2025/02/04
+ */
+@Slf4j
+public class StreamCsvExportUtil {
+
+ /**
+ * 默认编码(GB18030兼容Excel中文)- rqrq
+ */
+ private static final String DEFAULT_CHARSET = "GB18030";
+
+ /**
+ * 默认缓冲区大小(128KB)- rqrq
+ */
+ private static final int DEFAULT_BUFFER_SIZE = 8192 * 16;
+
+ /**
+ * 每个CSV文件最大行数(默认100万)- rqrq
+ */
+ public static final int DEFAULT_MAX_ROWS_PER_CSV = 1000000;
+
+ /**
+ * @Description 创建单CSV文件写入器 - rqrq
+ * @param outputStream 输出流
+ * @param fileName 文件名(用于日志)
+ * @return CsvStreamWriter
+ * @author rqrq
+ */
+ public static CsvStreamWriter createSingleCsvWriter(OutputStream outputStream, String fileName) throws IOException {
+ return new SingleCsvWriter(outputStream, fileName);
+ }
+
+ /**
+ * @Description 创建ZIP压缩包写入器(多CSV文件)- rqrq
+ * @param outputStream 输出流
+ * @param zipFileName ZIP文件名(用于日志)
+ * @param csvFilePrefix CSV文件名前缀(如"数据",则生成"数据_1.csv"、"数据_2.csv")
+ * @return CsvStreamWriter
+ * @author rqrq
+ */
+ public static CsvStreamWriter createZipCsvWriter(OutputStream outputStream, String zipFileName, String csvFilePrefix) throws IOException {
+ return new ZipCsvWriter(outputStream, zipFileName, csvFilePrefix, DEFAULT_MAX_ROWS_PER_CSV);
+ }
+
+ /**
+ * @Description 创建ZIP压缩包写入器(自定义每个CSV最大行数)- rqrq
+ * @param outputStream 输出流
+ * @param zipFileName ZIP文件名
+ * @param csvFilePrefix CSV文件名前缀
+ * @param maxRowsPerCsv 每个CSV最大行数
+ * @return CsvStreamWriter
+ * @author rqrq
+ */
+ public static CsvStreamWriter createZipCsvWriter(OutputStream outputStream, String zipFileName, String csvFilePrefix, int maxRowsPerCsv) throws IOException {
+ return new ZipCsvWriter(outputStream, zipFileName, csvFilePrefix, maxRowsPerCsv);
+ }
+
+ /**
+ * CSV流式写入器接口 - rqrq
+ *
+ * 异常处理机制:
+ *
+ * - 写入失败时自动设置错误状态
+ * - 后续 writeRow 调用会直接跳过(避免资源浪费)
+ * - 调用方可通过 hasError() 检查是否发生错误
+ * - 调用方可通过 getLastError() 获取错误详情
+ *
+ */
+ public interface CsvStreamWriter extends AutoCloseable {
+
+ /**
+ * 写入表头行 - rqrq
+ */
+ void writeHeader(String... headers) throws IOException;
+
+ /**
+ * 写入数据行(如果已发生错误则跳过)- rqrq
+ */
+ void writeRow(String... values) throws IOException;
+
+ /**
+ * 写入数据行(安全版本,不抛异常,返回是否成功)- rqrq
+ * 用于 ResultHandler 中避免抛出 RuntimeException
+ */
+ boolean writeRowSafe(String... values);
+
+ /**
+ * 获取已写入行数 - rqrq
+ */
+ long getWrittenRows();
+
+ /**
+ * 刷新缓冲区 - rqrq
+ */
+ void flush() throws IOException;
+
+ /**
+ * 关闭写入器 - rqrq
+ */
+ @Override
+ void close() throws IOException;
+
+ /**
+ * 检查是否已发生错误 - rqrq
+ */
+ boolean hasError();
+
+ /**
+ * 获取最后一次错误 - rqrq
+ */
+ Throwable getLastError();
+
+ /**
+ * 手动设置错误状态(用于外部捕获到异常时)- rqrq
+ */
+ void setError(Throwable e);
+
+ /**
+ * CSV字段转义(处理逗号、引号、换行)- rqrq
+ */
+ default String escape(String value) {
+ if (value == null) {
+ return "";
+ }
+ if (value.contains(",") || value.contains("\"") || value.contains("\n") || value.contains("\r")) {
+ return "\"" + value.replace("\"", "\"\"") + "\"";
+ }
+ return value;
+ }
+
+ /**
+ * 强制文本格式(防止前导零丢失)- rqrq
+ * 使用 ="值" 格式,Excel会识别为文本
+ */
+ default String asText(String value) {
+ if (value == null || value.isEmpty()) {
+ return "";
+ }
+ return "=\"" + value.replace("\"", "\"\"") + "\"";
+ }
+
+ /**
+ * 格式化日期 - rqrq
+ */
+ default String formatDate(Date date) {
+ if (date == null) {
+ return "";
+ }
+ return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
+ }
+
+ /**
+ * 格式化日期(自定义格式)- rqrq
+ */
+ default String formatDate(Date date, String pattern) {
+ if (date == null) {
+ return "";
+ }
+ return new SimpleDateFormat(pattern).format(date);
+ }
+ }
+
+ /**
+ * 单CSV文件写入器实现 - rqrq
+ */
+ private static class SingleCsvWriter implements CsvStreamWriter {
+ private final BufferedWriter writer;
+ private final String fileName;
+ private long writtenRows = 0;
+
+ // 错误状态(volatile保证多线程可见性)- rqrq
+ private volatile boolean hasError = false;
+ private volatile Throwable lastError = null;
+
+ SingleCsvWriter(OutputStream outputStream, String fileName) throws IOException {
+ this.fileName = fileName;
+ this.writer = new BufferedWriter(new OutputStreamWriter(outputStream, DEFAULT_CHARSET), DEFAULT_BUFFER_SIZE);
+ log.info("创建单CSV写入器: {} - rqrq", fileName);
+ }
+
+ @Override
+ public void writeHeader(String... headers) throws IOException {
+ if (hasError) return;
+ try {
+ writer.write(String.join(",", headers));
+ writer.newLine();
+ } catch (IOException e) {
+ setError(e);
+ throw e;
+ }
+ }
+
+ @Override
+ public void writeRow(String... values) throws IOException {
+ // 如果已发生错误,直接跳过 - rqrq
+ if (hasError) {
+ return;
+ }
+
+ try {
+ writer.write(String.join(",", values));
+ writer.newLine();
+ writtenRows++;
+
+ if (writtenRows % 100000 == 0) {
+ log.info("CSV已写入 {} 行 - rqrq", writtenRows);
+ flush();
+ }
+ } catch (IOException e) {
+ setError(e);
+ throw e;
+ }
+ }
+
+ @Override
+ public boolean writeRowSafe(String... values) {
+ if (hasError) {
+ return false;
+ }
+ try {
+ writeRow(values);
+ return true;
+ } catch (IOException e) {
+ // 已在writeRow中设置了错误状态
+ log.warn("写入行失败,后续行将跳过: {} - rqrq", e.getMessage());
+ return false;
+ }
+ }
+
+ @Override
+ public long getWrittenRows() {
+ return writtenRows;
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (!hasError) {
+ writer.flush();
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ if (!hasError) {
+ flush();
+ }
+ writer.close();
+ } catch (IOException e) {
+ log.warn("关闭写入器时发生错误: {} - rqrq", e.getMessage());
+ }
+ log.info("单CSV写入完成: {},共 {} 行,hasError: {} - rqrq", fileName, writtenRows, hasError);
+ }
+
+ @Override
+ public boolean hasError() {
+ return hasError;
+ }
+
+ @Override
+ public Throwable getLastError() {
+ return lastError;
+ }
+
+ @Override
+ public void setError(Throwable e) {
+ this.hasError = true;
+ this.lastError = e;
+ log.error("CSV写入器发生错误,后续写入将跳过: {} - rqrq", e.getMessage());
+ }
+ }
+
+ /**
+ * ZIP压缩包写入器实现(多CSV文件)- rqrq
+ */
+ private static class ZipCsvWriter implements CsvStreamWriter {
+ private final ZipOutputStream zipOut;
+ private final String zipFileName;
+ private final String csvFilePrefix;
+ private final int maxRowsPerCsv;
+
+ private BufferedWriter currentWriter;
+ private int currentCsvIndex = 0;
+ private long currentCsvRows = 0;
+ private long totalWrittenRows = 0;
+ private String[] headerRow;
+
+ // 错误状态(volatile保证多线程可见性)- rqrq
+ private volatile boolean hasError = false;
+ private volatile Throwable lastError = null;
+
+ ZipCsvWriter(OutputStream outputStream, String zipFileName, String csvFilePrefix, int maxRowsPerCsv) throws IOException {
+ this.zipOut = new ZipOutputStream(outputStream);
+ this.zipFileName = zipFileName;
+ this.csvFilePrefix = csvFilePrefix;
+ this.maxRowsPerCsv = maxRowsPerCsv;
+ log.info("创建ZIP写入器: {},每个CSV最多 {} 行 - rqrq", zipFileName, maxRowsPerCsv);
+ }
+
+ @Override
+ public void writeHeader(String... headers) throws IOException {
+ if (hasError) return;
+ this.headerRow = headers;
+ try {
+ // 创建第一个CSV文件并写入表头
+ createNewCsvFile();
+ } catch (IOException e) {
+ setError(e);
+ throw e;
+ }
+ }
+
+ @Override
+ public void writeRow(String... values) throws IOException {
+ // 如果已发生错误,直接跳过 - rqrq
+ if (hasError) {
+ return;
+ }
+
+ try {
+ // 检查是否需要创建新CSV文件
+ if (currentCsvRows >= maxRowsPerCsv) {
+ closeCurrentCsv();
+ createNewCsvFile();
+ }
+
+ currentWriter.write(String.join(",", values));
+ currentWriter.newLine();
+ currentCsvRows++;
+ totalWrittenRows++;
+
+ if (totalWrittenRows % 100000 == 0) {
+ log.info("ZIP总计已写入 {} 行,当前CSV {} 有 {} 行 - rqrq",
+ totalWrittenRows, currentCsvIndex, currentCsvRows);
+ flush();
+ }
+ } catch (IOException e) {
+ setError(e);
+ throw e;
+ }
+ }
+
+ @Override
+ public boolean writeRowSafe(String... values) {
+ if (hasError) {
+ return false;
+ }
+ try {
+ writeRow(values);
+ return true;
+ } catch (IOException e) {
+ // 已在writeRow中设置了错误状态
+ log.warn("写入行失败,后续行将跳过: {} - rqrq", e.getMessage());
+ return false;
+ }
+ }
+
+ @Override
+ public long getWrittenRows() {
+ return totalWrittenRows;
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (!hasError && currentWriter != null) {
+ currentWriter.flush();
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ if (!hasError) {
+ closeCurrentCsv();
+ zipOut.finish();
+ }
+ zipOut.close();
+ } catch (IOException e) {
+ log.warn("关闭ZIP写入器时发生错误: {} - rqrq", e.getMessage());
+ }
+ log.info("ZIP写入完成: {},共 {} 个CSV文件,总计 {} 行,hasError: {} - rqrq",
+ zipFileName, currentCsvIndex, totalWrittenRows, hasError);
+ }
+
+ @Override
+ public boolean hasError() {
+ return hasError;
+ }
+
+ @Override
+ public Throwable getLastError() {
+ return lastError;
+ }
+
+ @Override
+ public void setError(Throwable e) {
+ this.hasError = true;
+ this.lastError = e;
+ log.error("ZIP写入器发生错误,后续写入将跳过: {} - rqrq", e.getMessage());
+ }
+
+ private void createNewCsvFile() throws IOException {
+ currentCsvIndex++;
+ // 使用ASCII文件名避免Windows解压乱码 - rqrq
+ String csvFileName = "TID_EPC_Log_" + currentCsvIndex + ".csv";
+ ZipEntry entry = new ZipEntry(csvFileName);
+ zipOut.putNextEntry(entry);
+
+ // 注意:ZIP内的Writer不能关闭底层流
+ currentWriter = new BufferedWriter(new OutputStreamWriter(zipOut, DEFAULT_CHARSET), DEFAULT_BUFFER_SIZE);
+
+ // 写入表头
+ if (headerRow != null) {
+ currentWriter.write(String.join(",", headerRow));
+ currentWriter.newLine();
+ }
+
+ currentCsvRows = 0;
+ log.info("创建CSV文件: {} - rqrq", csvFileName);
+ }
+
+ private void closeCurrentCsv() throws IOException {
+ if (currentWriter != null) {
+ currentWriter.flush();
+ zipOut.closeEntry();
+ log.info("CSV文件 {} 写入完成,共 {} 行 - rqrq", currentCsvIndex, currentCsvRows);
+ }
+ }
+ }
+
+ /**
+ * @Description 构建CSV数据行(通用方法)- rqrq
+ * @param extractors 字段提取器数组
+ * @param data 数据对象
+ * @return String[] CSV行数据
+ * @author rqrq
+ */
+ @SafeVarargs
+ public static String[] buildRow(T data, Function... extractors) {
+ String[] row = new String[extractors.length];
+ for (int i = 0; i < extractors.length; i++) {
+ row[i] = extractors[i].apply(data);
+ }
+ return row;
+ }
+}
diff --git a/src/main/java/com/xujie/sys/modules/pms/mapper/MesTidEpcLogMapper.java b/src/main/java/com/xujie/sys/modules/pms/mapper/MesTidEpcLogMapper.java
index 5ef45f18..3f52f513 100644
--- a/src/main/java/com/xujie/sys/modules/pms/mapper/MesTidEpcLogMapper.java
+++ b/src/main/java/com/xujie/sys/modules/pms/mapper/MesTidEpcLogMapper.java
@@ -7,7 +7,11 @@ import com.xujie.sys.modules.pms.data.MesTidEpcLogData;
import com.xujie.sys.modules.pms.data.MesTidEpcLogExportData;
import com.xujie.sys.modules.pms.entity.MesTidEpcLogEntity;
import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.ResultType;
+import org.apache.ibatis.mapping.ResultSetType;
+import org.apache.ibatis.session.ResultHandler;
import java.util.List;
@@ -71,6 +75,26 @@ public interface MesTidEpcLogMapper extends BaseMapper {
@Param("offset") long offset,
@Param("limit") int limit);
+ /**
+ * @Description 流式查询导出数据(真正的逐行读取,配合ResultHandler使用)- rqrq
+ *
+ * 【流式查询原理】
+ *
+ * - 使用 FORWARD_ONLY 游标,数据逐行从数据库读取
+ * - 配合 responseBuffering=adaptive(在JDBC URL中配置)
+ * - fetchSize=1000 控制每次从数据库预取的行数
+ * - 数据不会全部加载到内存,避免OOM
+ *
+ *
+ * @param query 查询条件
+ * @param handler 结果处理器,逐行处理数据
+ * @author rqrq
+ * @date 2025/02/04
+ */
+ @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000)
+ @ResultType(MesTidEpcLogExportData.class)
+ void streamExportList(@Param("query") MesTidEpcLogData query, ResultHandler handler);
+
/**
* @Description 批量插入日志数据 - rqrq
* @param list 日志数据列表
diff --git a/src/main/java/com/xujie/sys/modules/pms/service/Impl/MesTidEpcLogServiceImpl.java b/src/main/java/com/xujie/sys/modules/pms/service/Impl/MesTidEpcLogServiceImpl.java
index 53658b46..96e04b03 100644
--- a/src/main/java/com/xujie/sys/modules/pms/service/Impl/MesTidEpcLogServiceImpl.java
+++ b/src/main/java/com/xujie/sys/modules/pms/service/Impl/MesTidEpcLogServiceImpl.java
@@ -22,6 +22,8 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
+import com.xujie.sys.common.utils.StreamCsvExportUtil;
+
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
@@ -32,6 +34,7 @@ import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -293,7 +296,15 @@ public class MesTidEpcLogServiceImpl extends ServiceImpl100万返回ZIP包含多个CSV)- rqrq
+ * @Description 导出CSV文件(真正的流式导出:MyBatis ResultHandler + 逐行写入)- rqrq
+ *
+ * 【V5版本:真正的流式导出】
+ *
+ * - 使用 MyBatis ResultHandler 逐行从数据库读取
+ * - 读一行写一行,内存中不存储任何数据批次
+ * - 配合 JDBC responseBuffering=adaptive 和 FORWARD_ONLY 游标
+ * - 真正的 O(1) 内存占用,可导出任意大小数据集
+ *
*
* 【导出策略】
*
@@ -305,15 +316,16 @@ public class MesTidEpcLogServiceImpl extends ServiceImpl100万:输出ZIP压缩包,包含多个CSV - rqrq
+ // >100万:输出ZIP压缩包,包含多个CSV(真正流式)- rqrq
int csvFileCount = (int) Math.ceil((double) dataTotal / MAX_ROWS_PER_CSV);
- log.info("数据量 {} 条 > 100万,输出ZIP压缩包,包含 {} 个CSV文件 - rqrq", dataTotal, csvFileCount);
- writeToZipWithMultipleCsv(data, dataTotal, response);
+ log.info("数据量 {} 条 > 100万,输出ZIP压缩包,包含 {} 个CSV文件(真正流式)- rqrq", dataTotal, csvFileCount);
+ exportToZipRealStreaming(data, dataTotal, response);
}
long endTime = System.currentTimeMillis();
- log.info("导出完成,总耗时: {} 秒 - rqrq", (endTime - startTime) / 1000.0);
+ log.info("导出完成(V5),总耗时: {} 秒 - rqrq", (endTime - startTime) / 1000.0);
} catch (Exception e) {
log.error("导出文件失败 - rqrq: {}", e.getMessage(), e);
@@ -351,9 +363,182 @@ public class MesTidEpcLogServiceImpl extends ServiceImpl【核心原理】
+ *
+ * - MyBatis ResultHandler 逐行回调
+ * - 每收到一行数据立即写入CSV
+ * - 内存中始终只有1行数据
+ *
+ *
+ * @param data 查询条件
+ * @param dataTotal 数据总数
+ * @param response HttpServletResponse
+ * @author rqrq
+ * @date 2025/02/04
+ */
+ private void exportToCsvRealStreaming(MesTidEpcLogData data, long dataTotal, HttpServletResponse response) throws IOException {
+ log.info("【真正流式导出】单CSV,预计 {} 条 - rqrq", dataTotal);
+
+ // 设置响应头 - rqrq
+ response.setContentType("text/csv;charset=GB18030");
+ response.setCharacterEncoding("GB18030");
+ String fileName = URLEncoder.encode("TID_EPC日志导出.csv", "UTF-8");
+ response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
+
+ // 创建流式CSV写入器 - rqrq
+ StreamCsvExportUtil.CsvStreamWriter writer = StreamCsvExportUtil.createSingleCsvWriter(
+ response.getOutputStream(), "TID_EPC日志导出.csv");
+
+ try {
+ // 写入表头 - rqrq
+ writer.writeHeader("序号", "EPC", "TID", "用户区", "LockiBtis", "密匙",
+ "写码成功", "读码成功", "EPC锁定", "强度/读距", "扫描时间", "计数");
+
+ // 使用AtomicLong记录进度(lambda中需要final或effectively final)- rqrq
+ AtomicLong processedCount = new AtomicLong(0);
+
+ // 真正的流式查询:ResultHandler逐行回调 - rqrq
+ // 使用writeRowSafe避免抛出RuntimeException导致MyBatis继续处理后续行 - rqrq
+ mesTidEpcLogMapper.streamExportList(data, resultContext -> {
+ // 如果已发生错误,直接跳过后续行(避免资源浪费)- rqrq
+ if (writer.hasError()) {
+ return;
+ }
+
+ MesTidEpcLogExportData row = resultContext.getResultObject();
+
+ // 使用安全写入方法,失败时自动设置错误状态 - rqrq
+ boolean success = writer.writeRowSafe(
+ writer.asText(row.getSeqNo()), // 序号可能以0开头
+ writer.asText(row.getEpc()), // EPC编码
+ writer.asText(row.getTid()), // TID编码
+ writer.asText(row.getUserArea()), // 用户区
+ writer.asText(row.getLockBits()), // LockBits
+ writer.asText(row.getSecretKey()), // 密匙(重要!防止丢失前导0)
+ writer.escape(row.getWriteSuccess()),
+ writer.escape(row.getReadSuccess()),
+ writer.escape(row.getEpcLocked()),
+ writer.escape(row.getSignalStrength()),
+ writer.formatDate(row.getScanTime()),
+ writer.asText(row.getCountInfo()) // 计数信息
+ );
+
+ if (success) {
+ long count = processedCount.incrementAndGet();
+ if (count % 100000 == 0) {
+ log.info("流式导出进度: {}/{} 条 - rqrq", count, dataTotal);
+ }
+ }
+ });
+
+ // 查询结束后检查是否发生错误 - rqrq
+ if (writer.hasError()) {
+ log.error("流式导出过程中发生错误: {} - rqrq", writer.getLastError().getMessage());
+ // 不抛异常,因为部分数据可能已发送给客户端
+ }
+
+ log.info("流式导出完成,共 {} 条 - rqrq", writer.getWrittenRows());
+
+ } finally {
+ writer.close();
+ }
+ }
+
+ /**
+ * @Description 真正的流式ZIP导出(多CSV文件)- rqrq
+ *
+ * 【核心原理】
+ *
+ * - MyBatis ResultHandler 逐行回调
+ * - 每收到一行数据立即写入当前CSV
+ * - 当前CSV达到100万行自动切换到新CSV
+ * - 内存中始终只有1行数据
+ *
+ *
+ * @param data 查询条件
+ * @param dataTotal 数据总数
+ * @param response HttpServletResponse
+ * @author rqrq
+ * @date 2025/02/04
+ */
+ private void exportToZipRealStreaming(MesTidEpcLogData data, long dataTotal, HttpServletResponse response) throws IOException {
+ log.info("【真正流式导出】ZIP多CSV,预计 {} 条 - rqrq", dataTotal);
+
+ // 设置响应头 - rqrq
+ response.setContentType("application/zip");
+ response.setCharacterEncoding("UTF-8");
+ String fileName = URLEncoder.encode("TID_EPC日志导出.zip", "UTF-8");
+ response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
+
+ // 创建流式ZIP写入器(每个CSV最多100万行)- rqrq
+ StreamCsvExportUtil.CsvStreamWriter writer = StreamCsvExportUtil.createZipCsvWriter(
+ response.getOutputStream(), "TID_EPC日志导出.zip", "TID_EPC日志", MAX_ROWS_PER_CSV);
+
+ try {
+ // 写入表头(自动应用到每个新建的CSV文件)- rqrq
+ writer.writeHeader("序号", "EPC", "TID", "用户区", "LockiBtis", "密匙",
+ "写码成功", "读码成功", "EPC锁定", "强度/读距", "扫描时间", "计数");
+
+ // 使用AtomicLong记录进度 - rqrq
+ AtomicLong processedCount = new AtomicLong(0);
+
+ // 真正的流式查询:ResultHandler逐行回调 - rqrq
+ // 使用writeRowSafe避免抛出RuntimeException导致MyBatis继续处理后续行 - rqrq
+ mesTidEpcLogMapper.streamExportList(data, resultContext -> {
+ // 如果已发生错误,直接跳过后续行(避免资源浪费)- rqrq
+ if (writer.hasError()) {
+ return;
+ }
+
+ MesTidEpcLogExportData row = resultContext.getResultObject();
+
+ // 使用安全写入方法,失败时自动设置错误状态 - rqrq
+ boolean success = writer.writeRowSafe(
+ writer.asText(row.getSeqNo()),
+ writer.asText(row.getEpc()),
+ writer.asText(row.getTid()),
+ writer.asText(row.getUserArea()),
+ writer.asText(row.getLockBits()),
+ writer.asText(row.getSecretKey()),
+ writer.escape(row.getWriteSuccess()),
+ writer.escape(row.getReadSuccess()),
+ writer.escape(row.getEpcLocked()),
+ writer.escape(row.getSignalStrength()),
+ writer.formatDate(row.getScanTime()),
+ writer.asText(row.getCountInfo())
+ );
+
+ if (success) {
+ long count = processedCount.incrementAndGet();
+ if (count % 100000 == 0) {
+ log.info("流式导出进度: {}/{} 条 - rqrq", count, dataTotal);
+ }
+ }
+ });
+
+ // 查询结束后检查是否发生错误 - rqrq
+ if (writer.hasError()) {
+ log.error("流式导出过程中发生错误: {} - rqrq", writer.getLastError().getMessage());
+ }
+
+ log.info("流式导出完成,共 {} 条 - rqrq", writer.getWrittenRows());
+
+ } finally {
+ writer.close();
+ }
+ }
+
+ // ==================================================================================
+ // ========================= V4版本(伪流式:分批查询分批写入,已被V5取代)=============
+ // ==================================================================================
+
+ /**
+ * @Description 【V4备份】写入ZIP压缩包(伪流式:分批查询分批写入)- rqrq
*
- * 【流式写入】边查边写,内存占用低
+ * 【已被V5取代】保留此方法用于对比或回退
+ * 【调用方式】如需使用,在exportCsv方法中替换exportToZipRealStreaming为此方法
*
* @param data 查询条件
* @param dataTotal 数据总数
@@ -361,6 +546,7 @@ public class MesTidEpcLogServiceImpl extends ServiceImpl
+
+
+
+
INSERT INTO mes_tid_epc_log (