From a647bc8a43c0212f482bee0630ebd14dca120a8a Mon Sep 17 00:00:00 2001
From: 吴羡 <16976746+Xian-TT3@user.noreply.gitee.com>
Date: 星期一, 28 九月 2026 14:13:28 +0800
Subject: [PATCH] feat: CRM 发票/合同导入、MES 质检批量导入与检测台账、回款与报价校验、权限码细化

---
 yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java |  163 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 163 insertions(+), 0 deletions(-)

diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java
index 5d65ac1..5a9b24d 100644
--- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java
+++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java
@@ -2,7 +2,12 @@
 
 import cn.idev.excel.ExcelWriter;
 import cn.idev.excel.FastExcelFactory;
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.idev.excel.context.AnalysisContext;
 import cn.idev.excel.converters.longconverter.LongStringConverter;
+import cn.idev.excel.enums.CellExtraTypeEnum;
+import cn.idev.excel.event.AnalysisEventListener;
+import cn.idev.excel.metadata.CellExtra;
 import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
 import cn.iocoder.yudao.framework.excel.core.handler.ColumnWidthMatchStyleStrategy;
 import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler;
@@ -11,7 +16,13 @@
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * Excel 宸ュ叿绫�
@@ -64,4 +75,156 @@
         }
     }
 
+    /**
+     * 璇诲彇 Excel锛屽苟鎶娿�岀旱鍚戝悎骞跺崟鍏冩牸銆嶇殑鍊煎~鍏呭埌鍚堝苟鍖哄煙鍐呯殑姣忎竴琛岋紝鍗虫妸鍚堝苟鍗曞厓鏍兼媺骞虫垚鏅�氳〃鏍笺��
+     *
+     * 渚嬪 A2:A5 鍚堝苟銆佸�间负 100 鏃讹紝璇诲彇缁撴灉涓 2~5 琛岀殑璇ュ垪鍧囦负 100銆�
+     * 鍥犱负 Excel 鍙湪鍚堝苟鍖哄煙宸︿笂瑙掍繚瀛樺�硷紝鐩存帴璇诲彇鏃跺叾浣欒涓虹┖锛屼細瀵艰嚧銆屽悓涓�寮犲崟鐨勫琛屾槑缁嗐�嶆棤娉曡瘑鍒��
+     * 娉ㄦ剰锛氬彧澶勭悊鍗曞垪鐨勭旱鍚戝悎骞讹紱琛ㄥご琛岀殑鍚堝苟銆佽法鍒楃殑妯悜鍚堝苟涓嶅鐞嗐��
+     *
+     * @param file 鏂囦欢
+     * @param head Excel head 澶�
+     * @return 鏁版嵁鍒楄〃
+     */
+    public static <T> List<T> readFlat(MultipartFile file, Class<T> head) throws IOException {
+        MergedCellFillReadListener<T> listener = new MergedCellFillReadListener<>(head);
+        // 鍙傝�� https://t.zsxq.com/zM77F 甯栧瓙锛屽鍔� try 澶勭悊锛屽吋瀹� windows 鍦烘櫙
+        try (InputStream inputStream = file.getInputStream()) {
+            FastExcelFactory.read(inputStream, head, listener)
+                    .extraRead(CellExtraTypeEnum.MERGE) // 棰濆璇诲彇鍚堝苟鍗曞厓鏍间俊鎭�
+                    .autoCloseStream(false) // 涓嶈鑷姩鍏抽棴锛屼氦缁� Servlet 鑷繁澶勭悊
+                    .doReadAllSync();
+        }
+        listener.fillMergedCell();
+        return listener.getDataList();
+    }
+
+    /**
+     * 鍚堝苟鍗曞厓鏍煎~鍏呯殑璇诲彇鐩戝惉鍣細璇诲彇杩囩▼涓敹闆嗚〃澶淬�佹暟鎹涓庡悎骞跺尯鍩燂紝璇诲彇瀹屾垚鍚庢妸鍚堝苟鍗曞厓鏍肩殑鍊煎~鍏呭埌鍚堝苟鍖哄煙鍐呯殑姣忎竴琛�
+     */
+    private static class MergedCellFillReadListener<T> extends AnalysisEventListener<T> {
+
+        /**
+         * 琛ㄥご锛歬ey 涓� sheet 缂栧彿锛寁alue 涓恒�屽垪鍙� -> 琛ㄥご鏂囨湰銆�
+         */
+        private final Map<Integer, Map<Integer, String>> sheetHeaderMap = new HashMap<>();
+        /**
+         * 鏁版嵁琛岋細key 涓恒�宻heet 缂栧彿 - 琛屽彿銆嶏紝value 涓烘暟鎹
+         */
+        private final Map<String, T> rowDataMap = new HashMap<>();
+        /**
+         * 鍚堝苟鍖哄煙锛歬ey 涓� sheet 缂栧彿
+         */
+        private final Map<Integer, List<CellExtra>> sheetMergedCellMap = new HashMap<>();
+        /**
+         * 銆岃〃澶存枃鏈� -> 瀛楁銆嶏紝鐢ㄤ簬鎶婂悎骞跺崟鍏冩牸鐨勫�煎~鍏呭洖瀛楁
+         */
+        private final Map<String, Field> headerFieldMap = new HashMap<>();
+        /**
+         * 閲嶅鍑虹幇鐨勮〃澶存枃鏈紝鏃犳硶纭畾濉厖鍒板摢涓瓧娈碉紝蹇界暐
+         */
+        private final Set<String> ambiguousHeaderSet = new HashSet<>();
+        /**
+         * 鏁版嵁鍒楄〃锛屽嵆璇诲彇缁撴灉
+         */
+        private final List<T> dataList = new ArrayList<>();
+
+        private MergedCellFillReadListener(Class<T> head) {
+            for (Field field : head.getDeclaredFields()) {
+                ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
+                if (excelProperty == null || excelProperty.value().length == 0
+                        || excelProperty.value()[0].trim().isEmpty()) {
+                    continue;
+                }
+                String header = excelProperty.value()[0].trim();
+                if (headerFieldMap.containsKey(header)) {
+                    ambiguousHeaderSet.add(header);
+                    continue;
+                }
+                field.setAccessible(true);
+                headerFieldMap.put(header, field);
+            }
+        }
+
+        @Override
+        public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
+            sheetHeaderMap.put(context.readSheetHolder().getSheetNo(), headMap);
+        }
+
+        @Override
+        public void invoke(T data, AnalysisContext context) {
+            dataList.add(data);
+            rowDataMap.put(buildRowKey(context.readSheetHolder().getSheetNo(),
+                    context.readRowHolder().getRowIndex()), data);
+        }
+
+        @Override
+        public void extra(CellExtra extra, AnalysisContext context) {
+            if (extra.getType() == CellExtraTypeEnum.MERGE) {
+                sheetMergedCellMap.computeIfAbsent(context.readSheetHolder().getSheetNo(), k -> new ArrayList<>())
+                        .add(extra);
+            }
+        }
+
+        @Override
+        public void doAfterAllAnalysed(AnalysisContext context) {
+        }
+
+        /**
+         * 鎶婂悎骞跺崟鍏冩牸鐨勫�硷紝濉厖鍒板悎骞跺尯鍩熷唴鐨勬瘡涓�琛�
+         */
+        public void fillMergedCell() {
+            sheetMergedCellMap.forEach((sheetNo, mergedCellList) -> {
+                Map<Integer, String> headerMap = sheetHeaderMap.get(sheetNo);
+                if (headerMap == null) {
+                    return;
+                }
+                mergedCellList.forEach(extra -> fillMergedCell(sheetNo, headerMap, extra));
+            });
+        }
+
+        private void fillMergedCell(Integer sheetNo, Map<Integer, String> headerMap, CellExtra extra) {
+            // 鍙鐞嗗崟鍒楃殑绾靛悜鍚堝苟锛涜法鍒楃殑妯悜鍚堝苟锛堝琛ㄥご鍚堝苟锛変笉澶勭悊
+            if (extra.getFirstColumnIndex() == null || !extra.getFirstColumnIndex().equals(extra.getLastColumnIndex())) {
+                return;
+            }
+            // 琛ㄥご琛岀殑鍚堝苟涓嶅鐞�
+            if (extra.getFirstRowIndex() == null || extra.getFirstRowIndex() < 1) {
+                return;
+            }
+            String header = headerMap.get(extra.getFirstColumnIndex());
+            Field field = header == null ? null : headerFieldMap.get(header);
+            if (field == null || ambiguousHeaderSet.contains(header)) {
+                return;
+            }
+            T sourceData = rowDataMap.get(buildRowKey(sheetNo, extra.getFirstRowIndex()));
+            if (sourceData == null) {
+                return;
+            }
+            try {
+                Object value = field.get(sourceData);
+                if (value == null) {
+                    return;
+                }
+                for (int rowIndex = extra.getFirstRowIndex() + 1; rowIndex <= extra.getLastRowIndex(); rowIndex++) {
+                    T targetData = rowDataMap.get(buildRowKey(sheetNo, rowIndex));
+                    if (targetData != null) {
+                        field.set(targetData, value);
+                    }
+                }
+            } catch (IllegalAccessException e) {
+                // 瀛楁宸� setAccessible锛岀悊璁轰笂涓嶄細鍙戠敓锛涘嵆浣垮彂鐢熶篃蹇界暐璇ュ悎骞跺崟鍏冩牸锛屼笉褰卞搷鍏跺畠鏁版嵁
+            }
+        }
+
+        private String buildRowKey(Integer sheetNo, Integer rowIndex) {
+            return sheetNo + "-" + rowIndex;
+        }
+
+        public List<T> getDataList() {
+            return dataList;
+        }
+
+    }
+
 }

--
Gitblit v1.9.3