Gogs hai 2 meses
pai
achega
5bafa452ee

+ 139 - 118
dtm-system/src/main/java/com/dtm/storage/service/StorageDataLoader.java

@@ -1,4 +1,4 @@
-package com.dtm.storage.service;
+package com.dtm.storage.service;
 
 import com.dtm.storage.model.AssemblyRecord;
 import com.dtm.storage.model.ProductInfo;
@@ -140,113 +140,140 @@ public class StorageDataLoader {
     }
 
     private List<PurchaseRecord> loadPurchaseRecords() {
-        ExcelSheet sheet = loadPurchaseSheet();
-        if (sheet.getRows().isEmpty()) {
+        Path file = findPurchaseFile();
+        if (file == null) {
             return Collections.emptyList();
         }
 
-        List<String> headers = sheet.getHeaders();
-        int dateIdx = findHeaderIndex(headers, new String[]{"业务日期", "日期"}, 0);
-        int codeIdx = findHeaderIndex(headers, new String[]{"产品代码", "产品编码", "编码", "代码"}, 1);
-        int qtyIdx = findHeaderIndex(headers, new String[]{"数量", "入库数量"}, 2);
-        int amountIdx = findHeaderIndex(headers, new String[]{"实际金额", "金额"}, 3);
-
         List<PurchaseRecord> records = new ArrayList<>();
-        for (List<Object> row : sheet.getRows()) {
-            String code = toText(getValue(row, codeIdx)).trim();
-            if (code.isEmpty()) {
-                continue;
+        final int[] indexes = {-1, -1, -1, -1};
+        ExcelUtils.processSheet(file, 0, resolveRowLimit(), new ExcelUtils.SheetHandler() {
+            @Override
+            public void onHeader(List<String> headers) {
+                indexes[0] = findHeaderIndex(headers, new String[]{"业务日期", "日期"}, 0);
+                indexes[1] = findHeaderIndex(headers, new String[]{"产品代码", "产品编码", "编码", "代码"}, 1);
+                indexes[2] = findHeaderIndex(headers, new String[]{"数量", "入库数量"}, 2);
+                indexes[3] = findHeaderIndex(headers, new String[]{"实际金额", "金额"}, 3);
             }
-            LocalDate date = toLocalDate(getValue(row, dateIdx));
-            double qty = toDouble(getValue(row, qtyIdx));
-            double amount = toDouble(getValue(row, amountIdx));
-            records.add(new PurchaseRecord(code, date, qty, amount));
-        }
+
+            @Override
+            public void onRow(List<Object> row) {
+                String code = toText(getValue(row, indexes[1])).trim();
+                if (code.isEmpty()) {
+                    return;
+                }
+                LocalDate date = toLocalDate(getValue(row, indexes[0]));
+                double qty = toDouble(getValue(row, indexes[2]));
+                double amount = toDouble(getValue(row, indexes[3]));
+                records.add(new PurchaseRecord(code, date, qty, amount));
+            }
+        });
         return records;
     }
 
     private List<SalesRecord> loadSalesRecords() {
-        ExcelSheet sheet = loadSalesSheet();
-        if (sheet.getRows().isEmpty()) {
+        Path file = findSalesFile();
+        if (file == null) {
             return Collections.emptyList();
         }
-        List<String> headers = sheet.getHeaders();
-        int codeIdx = findHeaderIndex(headers, new String[]{"商家编码", "产品编码", "产品代码", "编码"}, 0);
-        int qtyIdx = findHeaderIndex(headers, new String[]{"购买数量", "数量"}, 1);
-        int dateIdx = findHeaderIndex(headers, new String[]{"订单创建时间", "创建时间", "日期"}, 2);
 
         List<SalesRecord> records = new ArrayList<>();
-        for (List<Object> row : sheet.getRows()) {
-            String code = toText(getValue(row, codeIdx)).trim();
-            if (code.isEmpty()) {
-                continue;
+        final int[] indexes = {-1, -1, -1};
+        ExcelUtils.processSheet(file, 0, resolveRowLimit(), new ExcelUtils.SheetHandler() {
+            @Override
+            public void onHeader(List<String> headers) {
+                indexes[0] = findHeaderIndex(headers, new String[]{"商家编码", "产品编码", "产品代码", "编码"}, 0);
+                indexes[1] = findHeaderIndex(headers, new String[]{"购买数量", "数量"}, 1);
+                indexes[2] = findHeaderIndex(headers, new String[]{"订单创建时间", "创建时间", "日期"}, 2);
             }
-            LocalDate date = toLocalDate(getValue(row, dateIdx));
-            double qty = toDouble(getValue(row, qtyIdx));
-            records.add(new SalesRecord(code, date, qty));
-        }
+
+            @Override
+            public void onRow(List<Object> row) {
+                String code = toText(getValue(row, indexes[0])).trim();
+                if (code.isEmpty()) {
+                    return;
+                }
+                LocalDate date = toLocalDate(getValue(row, indexes[2]));
+                double qty = toDouble(getValue(row, indexes[1]));
+                records.add(new SalesRecord(code, date, qty));
+            }
+        });
         return records;
     }
 
     private List<AssemblyRecord> loadAssemblyRecords() {
-        ExcelSheet sheet = loadAssemblySheet();
-        if (sheet.getRows().isEmpty()) {
+        Path file = findAssemblyFile();
+        if (file == null) {
             return Collections.emptyList();
         }
-        List<String> headers = sheet.getHeaders();
-        int codeIdx = findHeaderIndex(headers, new String[]{"产品编码", "产品代码", "半成品"}, 0);
-        int qtyIdx = findHeaderIndex(headers, new String[]{"数量", "入库数量"}, 1);
-        int dateIdx = findHeaderIndex(headers, new String[]{"日期", "时间"}, 2);
 
         List<AssemblyRecord> records = new ArrayList<>();
-        for (List<Object> row : sheet.getRows()) {
-            String code = toText(getValue(row, codeIdx)).trim();
-            if (code.isEmpty()) {
-                continue;
+        final int[] indexes = {-1, -1, -1};
+        ExcelUtils.processSheet(file, 1, resolveRowLimit(), new ExcelUtils.SheetHandler() {
+            @Override
+            public void onHeader(List<String> headers) {
+                indexes[0] = findHeaderIndex(headers, new String[]{"产品编码", "产品代码", "半成品"}, 0);
+                indexes[1] = findHeaderIndex(headers, new String[]{"数量", "入库数量"}, 1);
+                indexes[2] = findHeaderIndex(headers, new String[]{"日期", "时间"}, 2);
             }
-            LocalDate date = toLocalDate(getValue(row, dateIdx));
-            double qty = toDouble(getValue(row, qtyIdx));
-            if (qty <= 0) {
-                continue;
+
+            @Override
+            public void onRow(List<Object> row) {
+                String code = toText(getValue(row, indexes[0])).trim();
+                if (code.isEmpty()) {
+                    return;
+                }
+                LocalDate date = toLocalDate(getValue(row, indexes[2]));
+                double qty = toDouble(getValue(row, indexes[1]));
+                if (qty <= 0) {
+                    return;
+                }
+                records.add(new AssemblyRecord(code, date, qty));
             }
-            records.add(new AssemblyRecord(code, date, qty));
-        }
+        });
         return records;
     }
 
     private List<ProductInfo> loadProductInfo() {
-        ExcelSheet sheet = loadProductInfoSheet();
-        if (sheet.getRows().isEmpty()) {
+        Path file = findProductInfoFile();
+        if (file == null) {
             return Collections.emptyList();
         }
-        List<String> headers = sheet.getHeaders();
-        int codeIdx = findHeaderIndex(headers, new String[]{"产品代码", "产品编码", "编码", "代码"}, 0);
-        int nameIdx = findHeaderIndex(headers, new String[]{"产品名称", "名称"}, 1);
-        int categoryIdx = findHeaderIndex(headers, new String[]{"分类", "成品", "半成品", "辅料"}, 4);
-        int attributeIdx = findHeaderIndex(headers, new String[]{"属性", "等级", "ABC"}, 10);
-        int spuIdx = findHeaderIndex(headers, new String[]{"SPU"}, 5);
-        int priceIdx = findHeaderIndex(headers, new String[]{"价格", "单价"}, 6);
 
         List<ProductInfo> infos = new ArrayList<>();
-        for (List<Object> row : sheet.getRows()) {
-            String code = toText(getValue(row, codeIdx)).trim();
-            if (code.isEmpty()) {
-                continue;
+        final int[] indexes = {-1, -1, -1, -1, -1, -1};
+        ExcelUtils.processSheet(file, 0, resolveRowLimit(), new ExcelUtils.SheetHandler() {
+            @Override
+            public void onHeader(List<String> headers) {
+                indexes[0] = findHeaderIndex(headers, new String[]{"产品代码", "产品编码", "编码", "代码"}, 0);
+                indexes[1] = findHeaderIndex(headers, new String[]{"产品名称", "名称"}, 1);
+                indexes[2] = findHeaderIndex(headers, new String[]{"分类", "成品", "半成品", "辅料"}, 4);
+                indexes[3] = findHeaderIndex(headers, new String[]{"属性", "等级", "ABC"}, 10);
+                indexes[4] = findHeaderIndex(headers, new String[]{"SPU"}, 5);
+                indexes[5] = findHeaderIndex(headers, new String[]{"价格", "单价"}, 6);
             }
-            String name = toText(getValue(row, nameIdx)).trim();
-            String category = toText(getValue(row, categoryIdx)).trim();
-            String attribute = toText(getValue(row, attributeIdx)).trim();
-            String spu = toText(getValue(row, spuIdx)).trim();
-            Double price = null;
-            Object priceObj = getValue(row, priceIdx);
-            if (priceObj != null) {
-                double parsed = toDouble(priceObj);
-                if (parsed > 0) {
-                    price = parsed;
+
+            @Override
+            public void onRow(List<Object> row) {
+                String code = toText(getValue(row, indexes[0])).trim();
+                if (code.isEmpty()) {
+                    return;
+                }
+                String name = toText(getValue(row, indexes[1])).trim();
+                String category = toText(getValue(row, indexes[2])).trim();
+                String attribute = toText(getValue(row, indexes[3])).trim();
+                String spu = toText(getValue(row, indexes[4])).trim();
+                Double price = null;
+                Object priceValue = getValue(row, indexes[5]);
+                if (priceValue != null) {
+                    double parsed = toDouble(priceValue);
+                    if (parsed > 0) {
+                        price = parsed;
+                    }
                 }
+                infos.add(new ProductInfo(code, name, category, attribute, spu, price));
             }
-            infos.add(new ProductInfo(code, name, category, attribute, spu, price));
-        }
+        });
         return infos;
     }
 
@@ -266,38 +293,6 @@ public class StorageDataLoader {
         return readSheet(file, 0);
     }
 
-    private ExcelSheet loadPurchaseSheet() {
-        Path file = findPurchaseFile();
-        if (file == null) {
-            return new ExcelSheet(Collections.emptyList(), Collections.emptyList());
-        }
-        return readSheet(file, 0);
-    }
-
-    private ExcelSheet loadSalesSheet() {
-        Path file = findSalesFile();
-        if (file == null) {
-            return new ExcelSheet(Collections.emptyList(), Collections.emptyList());
-        }
-        return readSheet(file, 0);
-    }
-
-    private ExcelSheet loadAssemblySheet() {
-        Path file = findAssemblyFile();
-        if (file == null) {
-            return new ExcelSheet(Collections.emptyList(), Collections.emptyList());
-        }
-        return readSheet(file, 1);
-    }
-
-    private ExcelSheet loadProductInfoSheet() {
-        Path file = findProductInfoFile();
-        if (file == null) {
-            return new ExcelSheet(Collections.emptyList(), Collections.emptyList());
-        }
-        return readSheet(file, 0);
-    }
-
     private ExcelSheet readSheet(Path file, int headerRowIndex) {
         if (tempMode) {
             return ExcelUtils.readSheet(file, headerRowIndex, TEMP_MAX_ROWS);
@@ -305,6 +300,10 @@ public class StorageDataLoader {
         return ExcelUtils.readSheet(file, headerRowIndex);
     }
 
+    private int resolveRowLimit() {
+        return tempMode ? TEMP_MAX_ROWS : -1;
+    }
+
     private Path resolveBasePath() {
         String envPath = Optional.ofNullable(System.getenv("DTM_DATA_PATH"))
                 .filter(v -> !v.trim().isEmpty())
@@ -347,38 +346,45 @@ public class StorageDataLoader {
     }
 
     private Path findPurchaseFile() {
-        Path dir = resolveDir("purchase");
+        Path dir = resolveDir("purchase", "入库数据");
         return findFile(dir, "purchase", "入库", "采购");
     }
 
     private Path findSalesFile() {
-        Path dir = resolveDir("sales");
+        Path dir = resolveDir("sales", "销售数据");
         return findFile(dir, "sales", "订单", "销售");
     }
 
     private Path findAssemblyFile() {
-        Path dir = resolveDir("assembly");
+        Path dir = resolveDir("assembly", "半成品组装");
         return findFile(dir, "assembly", "组装");
     }
 
     private Path findSemiMappingFile() {
-        Path dir = resolveDir("semi-mapping");
+        Path dir = resolveDir("semi-mapping", "半成品组装");
         return findFile(dir, "semi-mapping", "匹配");
     }
 
     private Path findProductInfoFile() {
-        Path dir = resolveDir("product");
+        Path dir = resolveDir("product", "产品资料", "入库数据");
         return findFile(dir, "product", "产品", "资料");
     }
 
-    private Path resolveDir(String dirName) {
+    private Path resolveDir(String... dirNames) {
         ensureTempValid();
         if (basePath == null) {
             return null;
         }
-        Path direct = basePath.resolve(dirName);
-        if (Files.exists(direct)) {
-            return direct;
+        if (dirNames != null) {
+            for (String dirName : dirNames) {
+                if (dirName == null || dirName.trim().isEmpty()) {
+                    continue;
+                }
+                Path candidate = basePath.resolve(dirName);
+                if (Files.exists(candidate)) {
+                    return candidate;
+                }
+            }
         }
         return basePath;
     }
@@ -418,7 +424,7 @@ public class StorageDataLoader {
         try (Stream<Path> stream = Files.list(dir)) {
             List<Path> candidates = stream
                     .filter(Files::isRegularFile)
-                    .filter(p -> p.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".xlsx"))
+                    .filter(this::isExcelFile)
                     .sorted(Comparator.comparing(Path::getFileName))
                     .collect(Collectors.toList());
 
@@ -443,6 +449,14 @@ public class StorageDataLoader {
         }
     }
 
+    private boolean isExcelFile(Path path) {
+        if (path == null || path.getFileName() == null) {
+            return false;
+        }
+        String name = path.getFileName().toString().toLowerCase(Locale.ROOT);
+        return name.endsWith(".xlsx") || name.endsWith(".xls");
+    }
+
     private int findHeaderIndex(List<String> headers, String[] keywords, int fallback) {
         if (headers == null || headers.isEmpty()) {
             return fallback;
@@ -522,6 +536,16 @@ public class StorageDataLoader {
             return null;
         }
         text = text.replace('/', '-').replace('.', '-');
+        if (text.matches("\\d+(\\.0+)?")) {
+            try {
+                double numeric = Double.parseDouble(text);
+                if (DateUtil.isValidExcelDate(numeric)) {
+                    Date date = DateUtil.getJavaDate(numeric);
+                    return Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault()).toLocalDate();
+                }
+            } catch (Exception ignored) {
+            }
+        }
         List<String> patterns = new ArrayList<>();
         patterns.add("yyyy-M-d");
         patterns.add("yyyy-MM-dd");
@@ -573,6 +597,3 @@ public class StorageDataLoader {
         }
     }
 }
-
-
-

+ 202 - 17
dtm-system/src/main/java/com/dtm/storage/util/ExcelUtils.java

@@ -1,5 +1,7 @@
 package com.dtm.storage.util;
 
+import org.apache.poi.ooxml.util.SAXHelper;
+import org.apache.poi.openxml4j.opc.OPCPackage;
 import org.apache.poi.ss.usermodel.Cell;
 import org.apache.poi.ss.usermodel.CellType;
 import org.apache.poi.ss.usermodel.DataFormatter;
@@ -9,57 +11,114 @@ import org.apache.poi.ss.usermodel.Row;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.usermodel.Workbook;
 import org.apache.poi.ss.usermodel.WorkbookFactory;
+import org.apache.poi.ss.util.CellReference;
+import org.apache.poi.xssf.eventusermodel.ReadOnlySharedStringsTable;
+import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;
+import org.apache.poi.xssf.eventusermodel.XSSFReader;
+import org.apache.poi.xssf.model.StylesTable;
+import org.apache.poi.xssf.usermodel.XSSFComment;
+import org.xml.sax.InputSource;
+import org.xml.sax.XMLReader;
 
 import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
+import java.util.Locale;
 
 public final class ExcelUtils {
     private ExcelUtils() {
     }
 
+    public interface SheetHandler {
+        void onHeader(List<String> headers);
+
+        void onRow(List<Object> row);
+    }
+
     public static ExcelSheet readSheet(Path filePath, int headerRowIndex) {
         return readSheet(filePath, headerRowIndex, -1);
     }
 
     public static ExcelSheet readSheet(Path filePath, int headerRowIndex, int maxRows) {
         if (filePath == null || !Files.exists(filePath)) {
-            return new ExcelSheet(new ArrayList<>(), new ArrayList<>());
+            return emptySheet();
         }
 
         List<String> headers = new ArrayList<>();
         List<List<Object>> rows = new ArrayList<>();
-        DataFormatter formatter = new DataFormatter();
+        processSheet(filePath, headerRowIndex, maxRows, new SheetHandler() {
+            @Override
+            public void onHeader(List<String> headerRow) {
+                headers.clear();
+                headers.addAll(headerRow);
+            }
+
+            @Override
+            public void onRow(List<Object> row) {
+                rows.add(row);
+            }
+        });
+        return new ExcelSheet(headers, rows);
+    }
+
+    public static void processSheet(Path filePath, int headerRowIndex, int maxRows, SheetHandler handler) {
+        if (filePath == null || !Files.exists(filePath) || handler == null) {
+            return;
+        }
+
+        String fileName = filePath.getFileName() == null ? "" : filePath.getFileName().toString().toLowerCase(Locale.ROOT);
+        if (fileName.endsWith(".xlsx")) {
+            try {
+                processXlsx(filePath, headerRowIndex, maxRows, handler);
+                return;
+            } catch (StopProcessingException ignored) {
+                return;
+            } catch (Exception ignored) {
+            }
+        }
+
+        try {
+            processWorkbook(filePath, headerRowIndex, maxRows, handler);
+        } catch (StopProcessingException ignored) {
+        } catch (Exception ignored) {
+        }
+    }
 
+    private static void processWorkbook(Path filePath, int headerRowIndex, int maxRows, SheetHandler handler) throws Exception {
+        DataFormatter formatter = new DataFormatter();
         try (InputStream inputStream = Files.newInputStream(filePath);
              Workbook workbook = WorkbookFactory.create(inputStream)) {
             Sheet sheet = workbook.getNumberOfSheets() > 0 ? workbook.getSheetAt(0) : null;
             if (sheet == null) {
-                return new ExcelSheet(headers, rows);
+                return;
             }
 
             FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
             Row headerRow = sheet.getRow(headerRowIndex);
             if (headerRow == null) {
-                return new ExcelSheet(headers, rows);
+                return;
             }
 
-            int maxCol = headerRow.getLastCellNum();
-            if (maxCol < 0) {
-                return new ExcelSheet(headers, rows);
+            int maxCol = Math.max(0, headerRow.getLastCellNum());
+            if (maxCol == 0) {
+                return;
             }
 
+            List<String> headers = new ArrayList<>(maxCol);
             for (int c = 0; c < maxCol; c++) {
                 Cell cell = headerRow.getCell(c);
                 String raw = formatter.formatCellValue(cell, evaluator);
                 headers.add(raw == null ? "" : raw.trim());
             }
+            handler.onHeader(headers);
 
+            int emitted = 0;
             for (int r = headerRowIndex + 1; r <= sheet.getLastRowNum(); r++) {
-                if (maxRows > 0 && rows.size() >= maxRows) {
+                if (maxRows > 0 && emitted >= maxRows) {
                     break;
                 }
                 Row row = sheet.getRow(r);
@@ -69,22 +128,39 @@ public final class ExcelUtils {
                 List<Object> values = new ArrayList<>(maxCol);
                 boolean hasValue = false;
                 for (int c = 0; c < maxCol; c++) {
-                    Cell cell = row.getCell(c);
-                    Object value = readCellValue(cell, formatter, evaluator);
-                    if (value != null && !(value instanceof String && ((String) value).trim().isEmpty())) {
+                    Object value = readCellValue(row.getCell(c), formatter, evaluator);
+                    if (!isBlankValue(value)) {
                         hasValue = true;
                     }
                     values.add(value);
                 }
-                if (hasValue) {
-                    rows.add(values);
+                if (!hasValue) {
+                    continue;
                 }
+                handler.onRow(values);
+                emitted++;
             }
-        } catch (Exception ignored) {
-            return new ExcelSheet(new ArrayList<>(), new ArrayList<>());
         }
+    }
 
-        return new ExcelSheet(headers, rows);
+    private static void processXlsx(Path filePath, int headerRowIndex, int maxRows, SheetHandler handler) throws Exception {
+        try (OPCPackage pkg = OPCPackage.open(filePath.toFile())) {
+            ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(pkg);
+            XSSFReader reader = new XSSFReader(pkg);
+            StylesTable styles = reader.getStylesTable();
+            XSSFReader.SheetIterator iterator = (XSSFReader.SheetIterator) reader.getSheetsData();
+            if (!iterator.hasNext()) {
+                return;
+            }
+            try (InputStream sheetStream = iterator.next()) {
+                XMLReader parser = SAXHelper.newXMLReader();
+                DataFormatter formatter = new DataFormatter();
+                XSSFSheetXMLHandler.SheetContentsHandler sheetHandler = new StreamingSheetHandler(headerRowIndex, maxRows, handler);
+                XSSFSheetXMLHandler xssfHandler = new XSSFSheetXMLHandler(styles, null, strings, sheetHandler, formatter, false);
+                parser.setContentHandler(xssfHandler);
+                parser.parse(new InputSource(sheetStream));
+            }
+        }
     }
 
     private static Object readCellValue(Cell cell, DataFormatter formatter, FormulaEvaluator evaluator) {
@@ -115,6 +191,115 @@ public final class ExcelUtils {
         String value = formatter.formatCellValue(cell, evaluator);
         return value == null ? null : value.trim();
     }
-}
 
+    private static boolean isBlankValue(Object value) {
+        return value == null || (value instanceof String && ((String) value).trim().isEmpty());
+    }
+
+    private static ExcelSheet emptySheet() {
+        return new ExcelSheet(Collections.emptyList(), Collections.emptyList());
+    }
+
+    private static class StreamingSheetHandler implements XSSFSheetXMLHandler.SheetContentsHandler {
+        private final int headerRowIndex;
+        private final int maxRows;
+        private final SheetHandler delegate;
+        private List<String> headers = Collections.emptyList();
+        private List<Object> currentRow = new ArrayList<>();
+        private int emittedRows;
+
+        private StreamingSheetHandler(int headerRowIndex, int maxRows, SheetHandler delegate) {
+            this.headerRowIndex = headerRowIndex;
+            this.maxRows = maxRows;
+            this.delegate = delegate;
+        }
 
+        @Override
+        public void startRow(int rowNum) {
+            currentRow = new ArrayList<>();
+        }
+
+        @Override
+        public void endRow(int rowNum) {
+            trimTrailingNulls(currentRow);
+            if (rowNum < headerRowIndex) {
+                return;
+            }
+            if (rowNum == headerRowIndex) {
+                headers = toHeaders(currentRow);
+                delegate.onHeader(headers);
+                return;
+            }
+            if (headers.isEmpty()) {
+                return;
+            }
+            if (maxRows > 0 && emittedRows >= maxRows) {
+                throw new StopProcessingException();
+            }
+            List<Object> row = new ArrayList<>(Math.max(headers.size(), currentRow.size()));
+            row.addAll(currentRow);
+            while (row.size() < headers.size()) {
+                row.add(null);
+            }
+            if (!hasValue(row)) {
+                return;
+            }
+            delegate.onRow(row);
+            emittedRows++;
+            if (maxRows > 0 && emittedRows >= maxRows) {
+                throw new StopProcessingException();
+            }
+        }
+
+        @Override
+        public void cell(String cellReference, String formattedValue, XSSFComment comment) {
+            int columnIndex = resolveColumnIndex(cellReference, currentRow.size());
+            while (currentRow.size() < columnIndex) {
+                currentRow.add(null);
+            }
+            String value = formattedValue == null ? null : formattedValue.trim();
+            currentRow.add(value == null || value.isEmpty() ? null : value);
+        }
+
+        @Override
+        public void headerFooter(String text, boolean isHeader, String tagName) {
+        }
+
+        private int resolveColumnIndex(String cellReference, int fallback) {
+            if (cellReference == null || cellReference.trim().isEmpty()) {
+                return fallback;
+            }
+            return new CellReference(cellReference).getCol();
+        }
+
+        private List<String> toHeaders(List<Object> row) {
+            List<String> result = new ArrayList<>(row.size());
+            for (Object value : row) {
+                result.add(value == null ? "" : String.valueOf(value).trim());
+            }
+            return result;
+        }
+
+        private boolean hasValue(List<Object> row) {
+            for (Object value : row) {
+                if (!isBlankValue(value)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private void trimTrailingNulls(List<Object> row) {
+            for (int i = row.size() - 1; i >= 0; i--) {
+                Object value = row.get(i);
+                if (!isBlankValue(value)) {
+                    break;
+                }
+                row.remove(i);
+            }
+        }
+    }
+
+    private static class StopProcessingException extends RuntimeException {
+    }
+}