From a08c70db67e49bf6cd125df04ebaf68c26f0d8dd Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期日, 20 九月 2026 16:19:16 +0800
Subject: [PATCH] feat(投资): 市州月报统一填报模板 + 县(市、区)列解析 + 表头行判定收紧

---
 traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java |  981 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 928 insertions(+), 53 deletions(-)

diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java b/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
index 4af3f35..c694e75 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
@@ -2,7 +2,9 @@
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.trafficaudit.auditengine.entity.AuditResult;
+import com.trafficaudit.auditengine.entity.AuditRun;
 import com.trafficaudit.auditengine.mapper.AuditResultMapper;
+import com.trafficaudit.auditengine.mapper.AuditRunMapper;
 import com.trafficaudit.common.util.RegionUtil;
 import com.trafficaudit.dataimport.entity.EnergyAuthVehicle;
 import com.trafficaudit.dataimport.entity.EnergyVehicleQuarterly;
@@ -54,8 +56,17 @@
 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.xwpf.usermodel.XWPFDocument;
+import org.apache.poi.xwpf.usermodel.XWPFParagraph;
+import org.apache.poi.xwpf.usermodel.XWPFTable;
+import org.apache.poi.xwpf.usermodel.XWPFTableRow;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
 import cn.hutool.core.io.IoUtil;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.Resource;
@@ -78,6 +89,7 @@
 import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
+import java.util.TreeMap;
 import java.util.stream.Collectors;
 
 import java.text.SimpleDateFormat;
@@ -142,6 +154,8 @@
     private FreightTurnoverImportMapper freightTurnoverMapper;
     @Resource
     private AuditResultMapper auditResultMapper;
+    @Resource
+    private AuditRunMapper auditRunMapper;
     @Resource
     private PassengerEnterpriseMonthlyMapper passengerMapper;
     @Resource
@@ -309,6 +323,9 @@
                 entity.setRegionCode(gs(row, colMap, "鎵�灞炲湴鍖�"));
                 entity.setEnterpriseCode(gs(row, colMap, "浼佷笟浠g爜"));
                 entity.setEnterpriseName(name.trim());
+                // 鏂扮増妯℃澘鍦ㄣ�屼紒涓氬悕绉般�嶅悗鏂板浜嗕袱鍒楋紙缁熶竴绀句細淇$敤浠g爜/濉姤鍗曚綅锛夛紝鎸夎〃澶村彇鍊硷紝鑰佹ā鏉挎棤璇ュ垪鏃朵负 null
+                entity.setUnifiedCreditCode(gs(row, colMap, "缁熶竴绀句細淇$敤浠g爜"));
+                entity.setReportUnit(gs(row, colMap, "濉姤鍗曚綅"));
                 entity.setVehicleTotal(gi(row, colMap, "瀹㈣繍杞﹁締_鎬昏"));
                 entity.setSeatTotal(gi(row, colMap, "鏍稿畾杞藉浣峗鎬昏"));
                 entity.setVehicleSchedule(gi(row, colMap, "杞﹁締鏁癬瀹㈣繍鐝溅锛堝惈瀹氱嚎鏃呮父瀹㈣繍瀹㈣溅锛�"));
@@ -558,24 +575,30 @@
         }
     }
 
-    /** 鍏矾鏃呭杩愭斂杞﹁締鏁帮紙鐙珛妯℃澘锛氫紒涓氫唬鐮�/浼佷笟鍚嶇О/杞﹁締鏁�/杞藉浣嶆暟锛� */
+    /** 鍏矾鏃呭杩愭斂杞﹁締鏁帮紙鍏煎涓ょ琛ㄥご锛氣憼 浼佷笟浠g爜/浼佷笟鍚嶇О/杞﹁締鏁�/杞藉浣嶆暟锛涒憽 杩愭斂搴撳鍑� OWNERNAME/鎬昏溅杈嗘暟/瀹綅鏁帮紝鏃犱紒涓氫唬鐮佸垪锛� */
     public ImportResult importPassengerAuth(MultipartFile file, String period) throws Exception {
         List<PassengerAuthVehicle> authList = new ArrayList<>();
         try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
             Sheet sheet = wb.getSheetAt(0);
-            Map<String, Integer> colMap = buildHeaderMap(sheet.getRow(0));
-            requireHeaderCols(sheet, "鏃呭杩愭斂杞﹁締鏁�", "浼佷笟鍚嶇О", "杞﹁締鏁�", "杞藉浣嶆暟");
+            Map<String, Integer> colMap = buildHeaderMapCompat(sheet.getRow(0));
+            Integer nameCol = firstColIdx(colMap, "浼佷笟鍚嶇О", "OWNERNAME");
+            Integer vehicleCol = firstColIdx(colMap, "杞﹁締鏁�", "鎬昏溅杈嗘暟");
+            Integer seatCol = firstColIdx(colMap, "杞藉浣嶆暟", "瀹綅鏁�");
+            Integer codeCol = firstColIdx(colMap, "浼佷笟浠g爜");
+            if (nameCol == null || vehicleCol == null || seatCol == null) {
+                throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨�愭梾瀹㈣繍鏀胯溅杈嗘暟銆戜笌鏂囦欢鍐呭涓嶇锛氳〃澶撮渶鍚�愪紒涓氬悕绉�/杞﹁締鏁�/杞藉浣嶆暟銆戞垨銆怬WNERNAME/鎬昏溅杈嗘暟/瀹綅鏁般�戯紝璇风‘璁ゆ槸鍚﹂�夐敊浜嗘暟鎹被鍨嬫垨浣跨敤浜嗘棫妯℃澘");
+            }
             for (int r = 1; r <= sheet.getLastRowNum(); r++) {
                 Row row = sheet.getRow(r);
                 if (row == null) continue;
-                String name = gs(row, colMap, "浼佷笟鍚嶇О");
+                String name = getString(row, nameCol);
                 if (name == null || name.trim().isEmpty()) continue;
                 PassengerAuthVehicle auth = new PassengerAuthVehicle();
                 auth.setReportPeriod(period);
-                auth.setEnterpriseCode(gs(row, colMap, "浼佷笟浠g爜"));
+                auth.setEnterpriseCode(codeCol == null ? null : getString(row, codeCol));
                 auth.setEnterpriseName(name.trim());
-                auth.setVehicleCount(gi(row, colMap, "杞﹁締鏁�"));
-                auth.setSeatCount(gi(row, colMap, "杞藉浣嶆暟"));
+                auth.setVehicleCount(getInt(row, vehicleCol));
+                auth.setSeatCount(getInt(row, seatCol));
                 authList.add(auth);
             }
         } catch (Exception e) {
@@ -924,10 +947,11 @@
         List<ScaleSplitTransport> list = new ArrayList<>();
         int failRows = 0;
         try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+            int targetMonth = Integer.parseInt(period.split("-")[1]); // 浠呭鍏ョ洰鏍囨湡鎵�鍦ㄦ湀鐨勯〉绛撅紙1-N鏈堟枃浠舵寜鐩爣鏈熷彇褰撴湀/绱锛�
             boolean hasPeriodSheet = false;
             for (int i = 0; i < wb.getNumberOfSheets(); i++) {
                 String sn = wb.getSheetAt(i).getSheetName();
-                if (sn != null && (sn.contains("褰撴湀") || sn.contains("绱"))) { hasPeriodSheet = true; break; }
+                if (sn != null && (sn.contains("褰撴湀") || sn.contains("鏈堜唤") || sn.contains("绱"))) { hasPeriodSheet = true; break; }
             }
             if (!hasPeriodSheet) {
                 throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨�愯涓婅涓嬫媶鍒嗐�戜笌鏂囦欢鍐呭涓嶇锛氭湭鎵惧埌鍚� 褰撴湀/绱 鐨� sheet锛岃纭鏄惁閫夐敊浜嗘暟鎹被鍨�");
@@ -936,8 +960,16 @@
                 Sheet sheet = wb.getSheetAt(i);
                 String sheetName = sheet.getSheetName();
                 String periodType = null;
-                if (sheetName.contains("褰撴湀")) periodType = "MONTH";
-                else if (sheetName.contains("绱")) periodType = "CUMULATIVE";
+                java.util.regex.Matcher mSheetMonth = java.util.regex.Pattern.compile("(\\d{1,2})\\u6708(\\u4efd|\\u7d2f\\u8ba1)").matcher(sheetName);
+                if (mSheetMonth.find()) {
+                    int sheetMonth = Integer.parseInt(mSheetMonth.group(1));
+                    if (sheetMonth == targetMonth) {
+                        periodType = "浠�".equals(mSheetMonth.group(2)) ? "MONTH" : "CUMULATIVE";
+                    }
+                } else {
+                    if (sheetName.contains("褰撴湀")) periodType = "MONTH";
+                    else if (sheetName.contains("绱")) periodType = "CUMULATIVE";
+                }
                 if (periodType == null) continue;
 
                 // 鏍¢獙 sheet 甯冨眬锛氱2琛屽簲鍚�屽競宸�/瑙勪笂浼佷笟杩愯緭閲忋�嶏紝绗�3琛屽瓙琛ㄥご搴斿惈銆屽懆杞噺銆嶆垨銆岃揣杩愰噺銆嶏紙鎵╁睍甯冨眬锛�
@@ -1030,11 +1062,22 @@
     // ========== 璐ц繍閲忓懆杞噺锛堟ā鏉縚璐ц繍閲忓懆杞噺.xlsx锛屾瘡鏈堝鍏ワ級 ==========
 
     public int importFreightTurnover(MultipartFile file, String period) throws Exception {
+        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+            // 澶氬勾搴﹀伐浣滅翱锛堜竴骞翠竴涓� Sheet锛氭暣骞� 1-12 鏈堬紝濡� 21-26 骞村畾绋挎枃浠讹級鈫� 鍏ㄩ儴骞翠唤鍏ュ簱
+            List<String> yearSheets = detectYearSheets(wb);
+            if (!yearSheets.isEmpty()) {
+                return importFreightTurnoverByYear(wb, file, yearSheets);
+            }
+            return importFreightTurnoverByMonth(wb, file, period);
+        }
+    }
+
+    /** 姣忔湀瀵煎叆锛堟ā鏉縚璐ц繍閲忓懆杞噺.xlsx锛夛細宸﹀崐浠婂勾 1-N 鏈堬紝鍙冲崐鍘诲勾 1-12 鏈� */
+    private int importFreightTurnoverByMonth(Workbook wb, MultipartFile file, String period) throws Exception {
         List<FreightTurnoverImport> list = new ArrayList<>();
-        int failRows = 0;
         // 鎶ヨ〃鏈� N 鏈堬細宸﹀崐涓轰粖骞� 1-N 鏈堬紙鍒楁暟涓嶅浐瀹氾級锛屽彸鍗婂浐瀹氫负鍘诲勾 1-12 鏈堬紙绠楀悓姣旂敤锛�
         int monthCount = Integer.parseInt(period.split("-")[1]);
-        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+        {
             Sheet sheet = wb.getSheetAt(0);
             Row header = sheet.getRow(0);
             String h0 = header == null ? null : getString(header, 0);
@@ -1099,27 +1142,149 @@
                 }
                 list.add(entity);
             }
-        } catch (Exception e) {
-            throw e;
         }
+        Map<String, List<FreightTurnoverImport>> byPeriod = new LinkedHashMap<>();
+        byPeriod.put(period, list);
+        return persistFreightTurnover(byPeriod, file.getOriginalFilename());
+    }
 
-        // 骞傜瓑锛氬悓鎶ヨ〃鏈熷厛鍒犻櫎
-        freightTurnoverMapper.delete(new LambdaQueryWrapper<FreightTurnoverImport>()
-            .eq(FreightTurnoverImport::getReportPeriod, period));
+    /** 璇嗗埆銆屽骞村害宸ヤ綔绨裤�嶏細姣忎釜 Sheet 涓�骞达紙Sheet 鍚� 2021骞�/2022骞粹�︼級锛屼笖琛ㄥご涓恒�岃揣杩愰噺/1鏈堚��12鏈堛�嶏紱鍚﹀垯杩斿洖绌哄垪琛� */
+    private List<String> detectYearSheets(Workbook wb) {
+        List<String> names = new ArrayList<>();
+        if (wb.getNumberOfSheets() < 2) return names;
+        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+            Sheet sheet = wb.getSheetAt(i);
+            String nm = sheet.getSheetName() == null ? "" : sheet.getSheetName().trim();
+            if (!nm.matches("\\d{4}\\s*骞�?")) { names.clear(); return names; }
+            Row header = sheet.getRow(0);
+            String h0 = header == null ? null : getString(header, 0);
+            String h1 = header == null ? null : getString(header, 1);
+            if (h0 == null || !h0.contains("璐ц繍閲�") || h1 == null || !h1.contains("鏈�")) {
+                names.clear();
+                return names;
+            }
+            names.add(nm);
+        }
+        return names;
+    }
 
-        int success = 0;
-        for (FreightTurnoverImport entity : list) {
-            try {
-                freightTurnoverMapper.insert(entity);
-                success++;
-            } catch (Exception e) {
-                log.error("freight turnover insert error: {}", entity.getRegionName(), e);
-                failRows++;
+    /** 澶氬勾搴﹀伐浣滅翱瀵煎叆锛氶�愬勾涓�鏉℃姤琛ㄦ湡锛堟姤琛ㄦ湡=璇ュ勾鏈�鍚庢湁鏁版嵁鐨勬湀浠斤級锛屼笂骞村悓鏈熷彇涓婁竴骞村害 Sheet */
+    private int importFreightTurnoverByYear(Workbook wb, MultipartFile file, List<String> yearSheets) {
+        Map<Integer, String> sheetByYear = new TreeMap<>();
+        for (String sheetName : yearSheets) {
+            sheetByYear.put(Integer.parseInt(sheetName.replace("骞�", "").trim()), sheetName);
+        }
+        Map<Integer, Map<String, FreightTurnoverImport>> byYear = new TreeMap<>();
+        Map<Integer, Integer> maxMonthByYear = new HashMap<>();
+        for (Map.Entry<Integer, String> entry : sheetByYear.entrySet()) {
+            Map<String, FreightTurnoverImport> parsed = new LinkedHashMap<>();
+            int maxMonth = parseWholeYearSheet(wb.getSheet(entry.getValue()), parsed);
+            if (parsed.isEmpty()) continue;
+            byYear.put(entry.getKey(), parsed);
+            maxMonthByYear.put(entry.getKey(), Math.max(1, Math.min(12, maxMonth)));
+        }
+        Map<String, List<FreightTurnoverImport>> byPeriod = new LinkedHashMap<>();
+        for (Map.Entry<Integer, Map<String, FreightTurnoverImport>> entry : byYear.entrySet()) {
+            int year = entry.getKey();
+            String yearPeriod = year + "-" + String.format("%02d", maxMonthByYear.get(year));
+            Map<String, FreightTurnoverImport> lastYear = byYear.get(year - 1);
+            List<FreightTurnoverImport> rows = new ArrayList<>();
+            for (Map.Entry<String, FreightTurnoverImport> regionEntry : entry.getValue().entrySet()) {
+                FreightTurnoverImport entity = regionEntry.getValue();
+                entity.setReportPeriod(yearPeriod);
+                // 涓婂勾鍚屾湡 = 涓婁竴骞村害 Sheet锛堝骞村害鏂囦欢娌℃湁鍙冲崐銆屽幓骞�1-12鏈堛�嶅尯鍧楋級
+                FreightTurnoverImport last = lastYear == null ? null : lastYear.get(regionEntry.getKey());
+                if (last != null) {
+                    for (int m = 1; m <= 12; m++) {
+                        setLastFreight(entity, m, getFreight(last, m));
+                        setLastTurnover(entity, m, getTurnover(last, m));
+                    }
+                }
+                rows.add(entity);
+            }
+            byPeriod.put(yearPeriod, rows);
+        }
+        return persistFreightTurnover(byPeriod, file.getOriginalFilename());
+    }
+
+    /** 瑙f瀽鏁村勾 Sheet锛�1-12 鏈堝垪锛夛細璐ц繍閲忓潡 + 鍛ㄨ浆閲忓潡锛涜繑鍥炴渶鍚庝竴涓湁璐ц繍閲忔暟鎹殑鏈堜唤 */
+    private int parseWholeYearSheet(Sheet sheet, Map<String, FreightTurnoverImport> out) {
+        Map<String, FreightTurnoverImport> freightMap = new LinkedHashMap<>();
+        Map<String, FreightTurnoverImport> turnoverMap = new LinkedHashMap<>();
+        Set<String> seen = new HashSet<>();
+        int maxMonth = 0;
+        for (int r = 1; r <= sheet.getLastRowNum(); r++) {
+            Row row = sheet.getRow(r);
+            if (row == null) continue;
+            String name = getString(row, 0);
+            if (name == null || name.trim().isEmpty()) continue;
+            String region = RegionUtil.normalizeCityName(name);
+            boolean isCity = RegionUtil.cityList().contains(region);
+            if (!isCity && !"婀栧寳鐪�".equals(region)) continue; // 琛ㄥご/璇存槑琛�
+            if (seen.contains(region)) {
+                // 璇ュ悕绉板凡鍑虹幇杩� -> 鍛ㄨ浆閲忓尯鍧�
+                FreightTurnoverImport entity = turnoverMap.get(region);
+                if (entity == null) {
+                    entity = new FreightTurnoverImport();
+                    entity.setRegionName(name.trim());
+                    turnoverMap.put(region, entity);
+                }
+                for (int m = 1; m <= 12; m++) {
+                    setTurnover(entity, m, getDecimalOrNull(row, m));
+                }
+            } else {
+                seen.add(region);
+                FreightTurnoverImport entity = freightMap.get(region);
+                if (entity == null) {
+                    entity = new FreightTurnoverImport();
+                    entity.setRegionName(name.trim());
+                    freightMap.put(region, entity);
+                }
+                for (int m = 1; m <= 12; m++) {
+                    Double v = getDecimalOrNull(row, m);
+                    setFreight(entity, m, v);
+                    if (v != null && m > maxMonth) maxMonth = m;
+                }
             }
         }
-        recordBatch(file.getOriginalFilename(), "FREIGHT_TURNOVER", period, list.size(), success, failRows, null);
-        log.info("Freight turnover imported: {} rows for {}", success, period);
-        return success;
+        // 鍚堝苟锛氫竴涓競宸炰竴琛岋紙璐ц繍閲� + 鍛ㄨ浆閲忥級
+        for (Map.Entry<String, FreightTurnoverImport> entry : freightMap.entrySet()) {
+            FreightTurnoverImport entity = entry.getValue();
+            FreightTurnoverImport turnover = turnoverMap.get(entry.getKey());
+            if (turnover != null) {
+                for (int m = 1; m <= 12; m++) {
+                    setTurnover(entity, m, getTurnover(turnover, m));
+                }
+            }
+            out.put(entry.getKey(), entity);
+        }
+        return maxMonth;
+    }
+
+    /** 钀藉簱锛氭寜鎶ヨ〃鏈熷厛鍒犲悗鎻掞紙骞傜瓑锛夛紝姣忎釜鎶ヨ〃鏈熶竴鏉″鍏ユ壒娆¤褰曪紝杩斿洖鎴愬姛鏉℃暟 */
+    private int persistFreightTurnover(Map<String, List<FreightTurnoverImport>> byPeriod, String fileName) {
+        int total = 0;
+        for (Map.Entry<String, List<FreightTurnoverImport>> entry : byPeriod.entrySet()) {
+            String p = entry.getKey();
+            List<FreightTurnoverImport> rows = entry.getValue();
+            freightTurnoverMapper.delete(new LambdaQueryWrapper<FreightTurnoverImport>()
+                .eq(FreightTurnoverImport::getReportPeriod, p));
+            int failRows = 0;
+            int success = 0;
+            for (FreightTurnoverImport entity : rows) {
+                try {
+                    freightTurnoverMapper.insert(entity);
+                    success++;
+                } catch (Exception e) {
+                    log.error("freight turnover insert error: {}", entity.getRegionName(), e);
+                    failRows++;
+                }
+            }
+            recordBatch(fileName, "FREIGHT_TURNOVER", p, rows.size(), success, failRows, null);
+            log.info("Freight turnover imported: {} rows for {}", success, p);
+            total += success;
+        }
+        return total;
     }
 
     /** 瑙f瀽璐ц繍閲忓尯鍧楄锛氬乏鍗� B 璧� N 鍒椾负浠婂勾 1-N 鏈堬紝鍙冲崐涓哄幓骞� 1-12 鏈� */
@@ -1297,6 +1462,275 @@
         }
     }
 
+    /** 鏀寔鈥滄寜绫诲瀷娓呯┖鈥濈殑鏁版嵁绫诲瀷 -> 鏁版嵁琛ㄦ搷浣滈敭锛堝悓琛ㄥ悓绫诲埆褰掑苟锛岄伩鍏嶉噸澶嶅垹闄わ級 */
+    private static final Map<String, String> CLEAR_TYPE_TO_OP = new LinkedHashMap<>();
+    /** 鏁版嵁绫诲瀷 -> 鍏宠仈瀹℃牳瑙勫垯绫诲瀷锛堝悓姝ユ竻闄ゅ鏍哥粨鏋滀笌宸插鏍告爣璁帮級 */
+    private static final Map<String, String[]> CLEAR_TYPE_TO_AUDIT = new HashMap<>();
+    /** 鏁版嵁绫诲瀷 -> 鍏宠仈瀵煎叆鎵规 importType锛堝悓姝ユ竻闄ゅ鍏ユ壒娆¤褰曪級 */
+    private static final Map<String, String[]> CLEAR_TYPE_TO_BATCH = new HashMap<>();
+
+    static {
+        CLEAR_TYPE_TO_OP.put("h2032", "H2032");
+        CLEAR_TYPE_TO_OP.put("transportAuth", "TRANSPORT_AUTH");
+        CLEAR_TYPE_TO_OP.put("trackMileage", "TRACK_MILEAGE");
+        CLEAR_TYPE_TO_OP.put("scaleSplit", "SCALE_SPLIT");
+        CLEAR_TYPE_TO_OP.put("freightTurnover", "FREIGHT_TURNOVER");
+        CLEAR_TYPE_TO_OP.put("passengerMonthly", "H2031");
+        CLEAR_TYPE_TO_OP.put("passengerAuth", "PASSENGER_AUTH");
+        CLEAR_TYPE_TO_OP.put("passengerIndividual", "PASSENGER_INDIVIDUAL");
+        CLEAR_TYPE_TO_OP.put("energyMonthly", "H204");
+        CLEAR_TYPE_TO_OP.put("energyAuth", "ENERGY_AUTH");
+        CLEAR_TYPE_TO_OP.put("investment", "INVEST_MONTHLY_PASSENGER");
+        CLEAR_TYPE_TO_OP.put("investCityPassenger", "INVEST_MONTHLY_PASSENGER");
+        CLEAR_TYPE_TO_OP.put("investmentLogistics", "INVEST_MONTHLY_LOGISTICS");
+        CLEAR_TYPE_TO_OP.put("investCityLogistics", "INVEST_MONTHLY_LOGISTICS");
+        CLEAR_TYPE_TO_OP.put("investmentSystem", "INVEST_SYSTEM");
+        CLEAR_TYPE_TO_OP.put("cityBus", "CITY_BUS");
+        CLEAR_TYPE_TO_OP.put("cityTaxi", "CITY_TAXI");
+        CLEAR_TYPE_TO_OP.put("cityTaxiAuth", "CITY_TAXI_AUTH");
+        CLEAR_TYPE_TO_OP.put("wycTotal", "WYC_TOTAL");
+        CLEAR_TYPE_TO_OP.put("wycOrder", "WYC_ORDER");
+        CLEAR_TYPE_TO_OP.put("wycOrderReport", "WYC_ORDER");
+
+        CLEAR_TYPE_TO_AUDIT.put("h2032", new String[]{"H2032"});
+        CLEAR_TYPE_TO_AUDIT.put("transportAuth", new String[]{"H2032"});
+        CLEAR_TYPE_TO_AUDIT.put("trackMileage", new String[]{"H2032"});
+        CLEAR_TYPE_TO_AUDIT.put("scaleSplit", new String[]{"H2032"});
+        CLEAR_TYPE_TO_AUDIT.put("freightTurnover", new String[]{"H2032"});
+        CLEAR_TYPE_TO_AUDIT.put("passengerMonthly", new String[]{"H2031"});
+        CLEAR_TYPE_TO_AUDIT.put("passengerAuth", new String[]{"H2031"});
+        CLEAR_TYPE_TO_AUDIT.put("passengerIndividual", new String[]{"H2031"});
+        CLEAR_TYPE_TO_AUDIT.put("energyMonthly", new String[]{"H204"});
+        CLEAR_TYPE_TO_AUDIT.put("energyAuth", new String[]{"H204"});
+        CLEAR_TYPE_TO_AUDIT.put("investment", new String[]{"INVEST"});
+        CLEAR_TYPE_TO_AUDIT.put("investmentLogistics", new String[]{"INVEST"});
+        CLEAR_TYPE_TO_AUDIT.put("investCityPassenger", new String[]{"INVEST"});
+        CLEAR_TYPE_TO_AUDIT.put("investCityLogistics", new String[]{"INVEST"});
+        CLEAR_TYPE_TO_AUDIT.put("investmentSystem", new String[]{"INVEST"});
+        CLEAR_TYPE_TO_AUDIT.put("cityBus", new String[]{"CITY_BUS"});
+        CLEAR_TYPE_TO_AUDIT.put("cityTaxi", new String[]{"CITY_TAXI"});
+        CLEAR_TYPE_TO_AUDIT.put("cityTaxiAuth", new String[]{"CITY_TAXI"});
+
+        String[] investPassengerBatch = {"INVEST_瀹㈣繍绔欏満", "INVEST_瀹㈣繍绔欏満_CITY"};
+        String[] investLogisticsBatch = {"INVEST_鐗╂祦鍥尯", "INVEST_鐗╂祦鍥尯_CITY"};
+        CLEAR_TYPE_TO_BATCH.put("h2032", new String[]{"H2032"});
+        CLEAR_TYPE_TO_BATCH.put("transportAuth", new String[]{"TRANSPORT_AUTH"});
+        CLEAR_TYPE_TO_BATCH.put("trackMileage", new String[]{"TRACK_MILEAGE"});
+        CLEAR_TYPE_TO_BATCH.put("scaleSplit", new String[]{"SCALE_SPLIT"});
+        CLEAR_TYPE_TO_BATCH.put("freightTurnover", new String[]{"FREIGHT_TURNOVER"});
+        CLEAR_TYPE_TO_BATCH.put("passengerMonthly", new String[]{"H2031"});
+        CLEAR_TYPE_TO_BATCH.put("passengerAuth", new String[]{"PASSENGER_AUTH"});
+        CLEAR_TYPE_TO_BATCH.put("passengerIndividual", new String[]{"PASSENGER_INDIVIDUAL"});
+        CLEAR_TYPE_TO_BATCH.put("energyMonthly", new String[]{"H204"});
+        CLEAR_TYPE_TO_BATCH.put("energyAuth", new String[]{"ENERGY_AUTH"});
+        CLEAR_TYPE_TO_BATCH.put("investment", investPassengerBatch);
+        CLEAR_TYPE_TO_BATCH.put("investCityPassenger", investPassengerBatch);
+        CLEAR_TYPE_TO_BATCH.put("investmentLogistics", investLogisticsBatch);
+        CLEAR_TYPE_TO_BATCH.put("investCityLogistics", investLogisticsBatch);
+        CLEAR_TYPE_TO_BATCH.put("investmentSystem", new String[]{"INVEST_SYSTEM"});
+        CLEAR_TYPE_TO_BATCH.put("cityBus", new String[]{"CITY_BUS"});
+        CLEAR_TYPE_TO_BATCH.put("cityTaxi", new String[]{"CITY_TAXI"});
+        CLEAR_TYPE_TO_BATCH.put("cityTaxiAuth", new String[]{"CITY_TAXI_AUTH"});
+        CLEAR_TYPE_TO_BATCH.put("wycTotal", new String[]{"WYC_TOTAL"});
+        CLEAR_TYPE_TO_BATCH.put("wycOrder", new String[]{"WYC_ORDER", "WYC_ORDER_REPORT"});
+        CLEAR_TYPE_TO_BATCH.put("wycOrderReport", new String[]{"WYC_ORDER_REPORT", "WYC_ORDER"});
+    }
+
+    /**
+     * 娓呯┖鎸囧畾鎶ヨ〃鏈熷凡瀵煎叆鏁版嵁涓庡鏍哥粨鏋滐紝鐢ㄤ簬鏁存湀閲嶅锛涙姇璧勯」鐩富妗d繚鐣欍��
+     *
+     * @param types 闇�娓呯┖鐨勬暟鎹被鍨嬶紙鍓嶇绫诲瀷閿紝濡� h2032/cityBus锛夛紱涓虹┖鍒欐竻绌鸿鎶ヨ〃鏈熷叏閮ㄧ被鍨�
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> clearPeriod(String period, List<String> types) {
+        if (period == null || !period.matches("\\d{4}-\\d{2}")) {
+            throw new IllegalArgumentException("鎶ヨ〃鏈熸牸寮忎笉姝g‘锛屽簲涓� yyyy-MM");
+        }
+        Set<String> selected = new LinkedHashSet<>();
+        if (types != null) {
+            for (String t : types) {
+                if (t != null && !t.trim().isEmpty()) selected.add(t.trim());
+            }
+        }
+        boolean clearAll = selected.isEmpty();
+        int dataRows = 0;
+        int auditRows = 0;
+        int auditRunRows = 0;
+        int batchRows = 0;
+        if (clearAll) {
+        dataRows += h2032Mapper.delete(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
+            .eq(H2032EnterpriseMonthly::getReportPeriod, period));
+        dataRows += transportAuthMapper.delete(new LambdaQueryWrapper<TransportAuthVehicle>()
+            .eq(TransportAuthVehicle::getReportPeriod, period));
+        dataRows += trackMileageMapper.delete(new LambdaQueryWrapper<VehicleTrackMileage>()
+            .eq(VehicleTrackMileage::getReportPeriod, period));
+        dataRows += scaleSplitMapper.delete(new LambdaQueryWrapper<ScaleSplitTransport>()
+            .eq(ScaleSplitTransport::getReportPeriod, period));
+        dataRows += freightTurnoverMapper.delete(new LambdaQueryWrapper<FreightTurnoverImport>()
+            .eq(FreightTurnoverImport::getReportPeriod, period));
+        dataRows += passengerMapper.delete(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
+            .eq(PassengerEnterpriseMonthly::getReportPeriod, period));
+        dataRows += passengerIndividualMapper.delete(new LambdaQueryWrapper<PassengerIndividualMonthly>()
+            .eq(PassengerIndividualMonthly::getReportPeriod, period));
+        dataRows += passengerAuthMapper.delete(new LambdaQueryWrapper<PassengerAuthVehicle>()
+            .eq(PassengerAuthVehicle::getReportPeriod, period));
+        dataRows += energyMapper.delete(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
+            .eq(EnergyVehicleQuarterly::getReportPeriod, period));
+        dataRows += energyAuthMapper.delete(new LambdaQueryWrapper<EnergyAuthVehicle>()
+            .eq(EnergyAuthVehicle::getReportPeriod, period));
+        dataRows += investMonthlyMapper.delete(new LambdaQueryWrapper<InvestmentMonthly>()
+            .eq(InvestmentMonthly::getReportPeriod, period));
+        dataRows += investSystemMapper.delete(new LambdaQueryWrapper<InvestmentSystem>()
+            .eq(InvestmentSystem::getReportPeriod, period));
+        dataRows += cityBusMapper.delete(new LambdaQueryWrapper<CityBusMonthly>()
+            .eq(CityBusMonthly::getReportPeriod, period));
+        dataRows += cityTaxiMapper.delete(new LambdaQueryWrapper<CityTaxiMonthly>()
+            .eq(CityTaxiMonthly::getReportPeriod, period));
+        dataRows += cityTaxiAuthMapper.delete(new LambdaQueryWrapper<CityTaxiAuth>()
+            .eq(CityTaxiAuth::getReportPeriod, period));
+        dataRows += wycTotalMapper.delete(new LambdaQueryWrapper<WycTotalMonthly>()
+            .eq(WycTotalMonthly::getReportPeriod, period));
+        dataRows += wycOrderMapper.delete(new LambdaQueryWrapper<WycOrderMonthly>()
+            .eq(WycOrderMonthly::getReportPeriod, period));
+        auditRows = auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
+            .eq(AuditResult::getReportPeriod, period));
+        auditRunRows = auditRunMapper.delete(new LambdaQueryWrapper<AuditRun>()
+            .eq(AuditRun::getReportPeriod, period));
+        batchRows = importBatchMapper.delete(new LambdaQueryWrapper<ImportBatch>()
+            .eq(ImportBatch::getReportPeriod, period));
+        } else {
+            Set<String> ops = new LinkedHashSet<>();
+            Set<String> auditTypes = new LinkedHashSet<>();
+            Set<String> batchTypes = new LinkedHashSet<>();
+            for (String t : selected) {
+                String op = CLEAR_TYPE_TO_OP.get(t);
+                if (op == null) {
+                    log.warn("娓呯┖鏁版嵁锛氭湭鐭ユ暟鎹被鍨� {}锛屽凡蹇界暐", t);
+                    continue;
+                }
+                ops.add(op);
+                String[] at = CLEAR_TYPE_TO_AUDIT.get(t);
+                if (at != null) java.util.Collections.addAll(auditTypes, at);
+                String[] bt = CLEAR_TYPE_TO_BATCH.get(t);
+                if (bt != null) java.util.Collections.addAll(batchTypes, bt);
+            }
+            if (ops.isEmpty()) {
+                throw new IllegalArgumentException("鏈寚瀹氭湁鏁堢殑鏁版嵁绫诲瀷");
+            }
+            for (String op : ops) {
+                dataRows += clearTypeData(op, period);
+            }
+            if (!auditTypes.isEmpty()) {
+                List<Long> ruleIds = new ArrayList<>();
+                for (com.trafficaudit.rulemanage.entity.AuditRule rule : ruleMapper.selectList(null)) {
+                    if (rule.getReportType() != null && auditTypes.contains(rule.getReportType())) {
+                        ruleIds.add(rule.getId());
+                    }
+                }
+                if (!ruleIds.isEmpty()) {
+                    auditRows = auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
+                        .eq(AuditResult::getReportPeriod, period)
+                        .in(AuditResult::getRuleId, ruleIds));
+                }
+                for (String rt : auditTypes) {
+                    auditRunRows += auditRunMapper.delete(new LambdaQueryWrapper<AuditRun>()
+                        .eq(AuditRun::getReportPeriod, period)
+                        .eq(AuditRun::getReportType, rt));
+                }
+            }
+            if (!batchTypes.isEmpty()) {
+                batchRows = importBatchMapper.delete(new LambdaQueryWrapper<ImportBatch>()
+                    .eq(ImportBatch::getReportPeriod, period)
+                    .in(ImportBatch::getImportType, batchTypes));
+            }
+        }
+        Map<String, Object> stats = new LinkedHashMap<>();
+        stats.put("period", period);
+        stats.put("scope", clearAll ? "all" : "types");
+        if (!clearAll) stats.put("types", new ArrayList<>(selected));
+        stats.put("dataRows", dataRows);
+        stats.put("auditRows", auditRows);
+        stats.put("auditRunRows", auditRunRows);
+        stats.put("batchRows", batchRows);
+        log.info("娓呯┖鎶ヨ〃鏈� {}锛坽}锛夊畬鎴愶細涓氬姟鏁版嵁 {} 琛屻�佸鏍哥粨鏋� {} 琛屻�佸鏍告爣璁� {} 琛屻�佸鍏ユ壒娆� {} 琛�",
+            period, clearAll ? "鍏ㄩ儴绫诲瀷" : String.join(",", selected), dataRows, auditRows, auditRunRows, batchRows);
+        return stats;
+    }
+
+    /** 鎸夆�滄暟鎹〃鎿嶄綔閿�濇竻绌哄搴斾笟鍔¤〃锛堟姇璧勬湀搴︽寜椤圭洰绫诲埆杩囨护锛夛紝杩斿洖鍒犻櫎琛屾暟 */
+    private int clearTypeData(String op, String period) {
+        switch (op) {
+            case "H2032":
+                return h2032Mapper.delete(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
+                    .eq(H2032EnterpriseMonthly::getReportPeriod, period));
+            case "TRANSPORT_AUTH":
+                return transportAuthMapper.delete(new LambdaQueryWrapper<TransportAuthVehicle>()
+                    .eq(TransportAuthVehicle::getReportPeriod, period));
+            case "TRACK_MILEAGE":
+                return trackMileageMapper.delete(new LambdaQueryWrapper<VehicleTrackMileage>()
+                    .eq(VehicleTrackMileage::getReportPeriod, period));
+            case "SCALE_SPLIT":
+                return scaleSplitMapper.delete(new LambdaQueryWrapper<ScaleSplitTransport>()
+                    .eq(ScaleSplitTransport::getReportPeriod, period));
+            case "FREIGHT_TURNOVER":
+                return freightTurnoverMapper.delete(new LambdaQueryWrapper<FreightTurnoverImport>()
+                    .eq(FreightTurnoverImport::getReportPeriod, period));
+            case "H2031":
+                return passengerMapper.delete(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
+                    .eq(PassengerEnterpriseMonthly::getReportPeriod, period));
+            case "PASSENGER_AUTH":
+                return passengerAuthMapper.delete(new LambdaQueryWrapper<PassengerAuthVehicle>()
+                    .eq(PassengerAuthVehicle::getReportPeriod, period));
+            case "PASSENGER_INDIVIDUAL":
+                return passengerIndividualMapper.delete(new LambdaQueryWrapper<PassengerIndividualMonthly>()
+                    .eq(PassengerIndividualMonthly::getReportPeriod, period));
+            case "H204":
+                return energyMapper.delete(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
+                    .eq(EnergyVehicleQuarterly::getReportPeriod, period));
+            case "ENERGY_AUTH":
+                return energyAuthMapper.delete(new LambdaQueryWrapper<EnergyAuthVehicle>()
+                    .eq(EnergyAuthVehicle::getReportPeriod, period));
+            case "INVEST_MONTHLY_PASSENGER":
+                return deleteInvestmentMonthly(period, "瀹㈣繍绔欏満");
+            case "INVEST_MONTHLY_LOGISTICS":
+                return deleteInvestmentMonthly(period, "鐗╂祦鍥尯");
+            case "INVEST_SYSTEM":
+                return investSystemMapper.delete(new LambdaQueryWrapper<InvestmentSystem>()
+                    .eq(InvestmentSystem::getReportPeriod, period));
+            case "CITY_BUS":
+                return cityBusMapper.delete(new LambdaQueryWrapper<CityBusMonthly>()
+                    .eq(CityBusMonthly::getReportPeriod, period));
+            case "CITY_TAXI":
+                return cityTaxiMapper.delete(new LambdaQueryWrapper<CityTaxiMonthly>()
+                    .eq(CityTaxiMonthly::getReportPeriod, period));
+            case "CITY_TAXI_AUTH":
+                return cityTaxiAuthMapper.delete(new LambdaQueryWrapper<CityTaxiAuth>()
+                    .eq(CityTaxiAuth::getReportPeriod, period));
+            case "WYC_TOTAL":
+                return wycTotalMapper.delete(new LambdaQueryWrapper<WycTotalMonthly>()
+                    .eq(WycTotalMonthly::getReportPeriod, period));
+            case "WYC_ORDER":
+                return wycOrderMapper.delete(new LambdaQueryWrapper<WycOrderMonthly>()
+                    .eq(WycOrderMonthly::getReportPeriod, period));
+            default:
+                log.warn("娓呯┖鏁版嵁锛氭湭鐭ユ暟鎹〃鎿嶄綔 {}", op);
+                return 0;
+        }
+    }
+
+    /** 鎶曡祫鏈堝害鎸夐」鐩被鍒竻闄わ紙鎶曡祫椤圭洰涓绘。淇濈暀锛� */
+    private int deleteInvestmentMonthly(String period, String category) {
+        List<Long> projectIds = new ArrayList<>();
+        for (InvestmentProject p : investProjectMapper.selectList(new LambdaQueryWrapper<InvestmentProject>()
+                .eq(InvestmentProject::getCategory, category))) {
+            projectIds.add(p.getId());
+        }
+        if (projectIds.isEmpty()) return 0;
+        return investMonthlyMapper.delete(new LambdaQueryWrapper<InvestmentMonthly>()
+            .eq(InvestmentMonthly::getReportPeriod, period)
+            .in(InvestmentMonthly::getProjectId, projectIds));
+    }
+
     /** 鏁版嵁鍙樺寲鍚庡垹闄ゆ寚瀹氭姤琛ㄧ被鍨嬫寚瀹氭姤琛ㄦ湡鐨勫鏍哥粨鏋滐紙闇�閲嶆柊鎵ц瀹℃牳锛� */
     private void deleteRuleResults(String reportType, String period) {
         List<Long> ruleIds = new ArrayList<>();
@@ -1385,6 +1819,33 @@
             }
         }
         return colMap;
+    }
+
+    /** 鍏煎琛ㄥご鏄犲皠锛氬幓寮曞彿/鍘荤┖鐧斤紱绾嫳鏂囪〃澶寸粺涓�澶у啓锛堝 'OWNERNAME' 鈫� OWNERNAME锛夛紝渚夸簬鏂版棫妯℃澘娣风敤 */
+    private Map<String, Integer> buildHeaderMapCompat(Row header) {
+        Map<String, Integer> colMap = new HashMap<>();
+        if (header == null) return colMap;
+        for (Cell cell : header) {
+            if (cell == null) continue;
+            String h = FORMATTER.formatCellValue(cell).replace("'", "").replace("\"", "").trim();
+            if (h.isEmpty()) continue;
+            boolean ascii = true;
+            for (int i = 0; i < h.length(); i++) {
+                if (h.charAt(i) > 127) { ascii = false; break; }
+            }
+            if (ascii) h = h.toUpperCase();
+            if (!colMap.containsKey(h)) colMap.put(h, cell.getColumnIndex());
+        }
+        return colMap;
+    }
+
+    /** 鎸夊埆鍚嶉『搴忓彇绗竴涓瓨鍦ㄧ殑鍒楀彿锛屽叏閮ㄧ己澶辫繑鍥� null */
+    private Integer firstColIdx(Map<String, Integer> colMap, String... aliases) {
+        for (String a : aliases) {
+            Integer idx = colMap.get(a);
+            if (idx != null) return idx;
+        }
+        return null;
     }
 
     private int colIdx(Map<String, Integer> colMap, String header) {
@@ -2004,10 +2465,10 @@
             }
             if (name == null || name.trim().isEmpty()) continue;
             String nt = name.trim().replaceAll("[\r\n]+", "");
-            if (nt.contains("鍚堣") || nt.contains("鎬昏")) continue;
+            String ntFlat = nt.replaceAll("\\s+", "");
+            if (ntFlat.contains("鍚堣") || ntFlat.contains("鎬昏") || ntFlat.contains("灏忚")) continue;
             if (nt.matches("^[涓�浜屼笁鍥涗簲鍏竷鍏節鍗乚+銆�.*")) continue;
             if (nt.matches("^锛圼涓�浜屼笁鍥涗簲鍏竷鍏節鍗乚+锛�.*")) continue;
-            if (nt.replace(" ", "").matches(".*灏廫\s*璁�.*")) continue;
             if (nt.startsWith("濉姤") || nt.startsWith("鍗曚綅璐熻矗浜�") || nt.matches("^\\d+銆�.*")) continue;
             if (hasSeq) {
                 if (seq == null || seq.trim().isEmpty()) {
@@ -2028,6 +2489,8 @@
             project.setProjectName(nt);
             project.setCategory(category);
             project.setCity(currentCity);
+            String countyCell = cellStr(row, col.get("county"));
+            if (countyCell != null && !countyCell.trim().isEmpty()) project.setCounty(countyCell.trim());
             project.setBuilderName(cellStr(row, col.get("builder")));
             project.setConstructNature(cellStr(row, col.get("nature")));
             project.setStartTime(investTime(cellStr(row, col.get("start"))));
@@ -2063,13 +2526,22 @@
             Row row = sh.getRow(r);
             if (row == null) continue;
             boolean hasName = false;
+            int headerCells = 0;
             for (int c = 0; c <= row.getLastCellNum(); c++) {
                 String v = cellStr(row, c);
-                if (v != null && v.contains("椤圭洰鍚嶇О")) { hasName = true; break; }
+                if (v == null) continue;
+                String t = v.trim();
+                if (t.isEmpty() || t.length() > 20) continue;   // 璇存槑鎬ч暱鍙ヤ笉绠楄〃澶村瓧娈�
+                String k = investHeaderKey(t);
+                if (k == null) continue;
+                headerCells++;
+                if ("name".equals(k)) hasName = true;
             }
-            if (!hasName) continue;
+            // 鐪熻〃澶磋鑷冲皯鏈� 2 涓煭琛ㄥご瀛楁锛涘彧鏈変竴鏍奸暱鍙ュ惈銆岄」鐩悕绉般�嶇殑璇存槑椤典細琚帓闄�
+            if (!hasName || headerCells < 2) continue;
             Map<Integer, String> colTexts = new HashMap<>();
-            for (int rr = r; rr <= Math.min(r + 3, sh.getLastRowNum()); rr++) {
+            int scanEnd = investHeaderScanEnd(sh, r, row);
+            for (int rr = r; rr <= scanEnd; rr++) {
                 Row rr2 = sh.getRow(rr);
                 if (rr2 == null) continue;
                 for (int c = 0; c <= rr2.getLastCellNum(); c++) {
@@ -2080,23 +2552,8 @@
             }
             Map<String, Integer> col = new HashMap<>();
             for (Map.Entry<Integer, String> e : colTexts.entrySet()) {
-                int c = e.getKey();
-                String v = e.getValue();
-                if (v.contains("椤圭洰鍚嶇О")) col.putIfAbsent("name", c);
-                else if (v.contains("鎶曡祫寤鸿") || v.contains("寤鸿鍗曚綅") || v.contains("椤圭洰涓氫富")) col.putIfAbsent("builder", c);
-                else if (v.contains("寤鸿鎬ц川")) col.putIfAbsent("nature", c);
-                else if (v.contains("寮�宸�")) col.putIfAbsent("start", c);
-                else if (v.contains("绔e伐") || v.contains("寤烘垚")) col.putIfAbsent("end", c);
-                else if ((v.contains("璁″垝鎬绘姇璧�") || v.contains("鎬绘姇璧�")) && !v.contains("绱")) col.putIfAbsent("total", c);
-                else if (v.contains("鑷紑濮嬪缓璁�")) col.putIfAbsent("startcum", c);
-                else if (v.contains("鏈勾璁″垝")) col.putIfAbsent("yearplan", c);
-                else if (v.contains("鑷勾鍒濈疮璁�")) col.putIfAbsent("yearcum", c);
-                else if (v.contains("鏈湀瀹屾垚")) col.putIfAbsent("monthdone", c);
-                else if (v.contains("寤鸿闃舵")) col.putIfAbsent("stage", c);
-                else if (v.contains("褰㈣薄杩涘害")) col.putIfAbsent("desc", c);
-                else if (v.contains("宸ュ彲") && !v.contains("椤圭洰鍚嶇О")) col.putIfAbsent("gk", c);
-                else if (v.contains("鍒濊") && !v.contains("椤圭洰鍚嶇О")) col.putIfAbsent("cs", c);
-                else if (v.contains("寤虹瓚闈㈢Н") && !v.contains("鏂板")) col.putIfAbsent("area", c);
+                String k = investHeaderKey(e.getValue());
+                if (k != null) col.putIfAbsent(k, e.getKey());
             }
             if (!col.containsKey("name")) continue;
             for (int c = 0; c <= row.getLastCellNum(); c++) {
@@ -2113,14 +2570,85 @@
                 int md = col.get("yearcum") + 1;
                 if (md <= row.getLastCellNum()) col.put("monthdone", md);
             }
+            // desc 缂哄け鏃舵寜 stage+1 鍏滃簳锛氱渷鏍囥�岄」鐩缓璁鹃樁娈点�佽繘搴︽儏鍐点�嶆槸 L/M 鍚堝苟琛ㄥご锛�
+            // 瀛濇劅/鎭╂柦/鏋楀尯/闅忓窞绛夊競宸炲彧鍦� L 鍐欍�屽缓璁鹃樁娈点�嶃�丮 鐩存帴鍐欒繘搴︽枃瀛楄�屼笉鍐欐爣棰�
+            if (!col.containsKey("desc") && col.containsKey("stage") && col.get("stage") >= 0) {
+                int dc = col.get("stage") + 1;
+                if (dc <= row.getLastCellNum()) col.put("desc", dc);
+            }
             for (String k : new String[]{"builder", "nature", "start", "end", "total", "startcum",
-                "yearplan", "yearcum", "monthdone", "stage", "desc", "gk", "cs", "area"}) {
+                "yearplan", "yearcum", "monthdone", "stage", "desc", "gk", "cs", "area", "county"}) {
                 col.putIfAbsent(k, -1);
             }
             col.put("headerRow", r);
             return col;
         }
         return null;
+    }
+
+    /** 鍗曚釜琛ㄥご鍗曞厓鏍� -> 瀛楁閿紙null 琛ㄧず涓嶆槸琛ㄥご瀛楁锛夛紱渚涜〃澶磋鍒ゅ畾涓庡垪鏄犲皠鍏辩敤 */
+    private String investHeaderKey(String v) {
+        if (v == null) return null;
+        String flat = v.replace(" ", "");
+        if (v.contains("椤圭洰鍚嶇О")) return "name";
+        // 銆屽幙锛堝競銆佸尯锛夈�嶇殑鍚勭鍐欐硶锛氬崐/鍏ㄨ鎷彿銆侀】鍙�/閫楀彿銆佹湁鏃犳嫭鍙凤紝缁熶竴褰掍竴鍖栧悗鍐嶅垽瀹�
+        String flatCounty = flat.replace("锛�", "(").replace("锛�", ")").replace("銆�", "").replace("锛�", "").replace(",", "");
+        if (flatCounty.contains("鍘�(甯傚尯)") || flatCounty.contains("鍘垮競鍖�") || flatCounty.contains("鍖哄幙")) return "county";
+        if (v.contains("鎶曡祫寤鸿") || v.contains("寤鸿鍗曚綅") || v.contains("椤圭洰涓氫富")) return "builder";
+        if (v.contains("寤鸿鎬ц川")) return "nature";
+        if (v.contains("寮�宸�")) return "start";
+        if (v.contains("绔e伐") || v.contains("寤烘垚")) return "end";
+        if ((v.contains("璁″垝鎬绘姇璧�") || v.contains("鎬绘姇璧�")) && !v.contains("绱")) return "total";
+        if (v.contains("鑷紑濮嬪缓璁�")) return "startcum";
+        if (v.contains("鏈勾璁″垝")) return "yearplan";
+        if (v.contains("鑷勾鍒濈疮璁�")) return "yearcum";
+        if (v.contains("鏈湀瀹屾垚")) return "monthdone";
+        if (v.contains("寤鸿闃舵")) return "stage";
+        if (v.contains("褰㈣薄杩涘害") || v.contains("寤鸿杩涘睍")) return "desc";
+        if (v.contains("宸ュ彲") && !v.contains("椤圭洰鍚嶇О")) return "gk";
+        if (v.contains("鍒濊") && !v.contains("椤圭洰鍚嶇О")) return "cs";
+        if (v.contains("寤虹瓚闈㈢Н") && !v.contains("鏂板")) return "area";
+        return null;
+    }
+
+    /** 琛ㄥご鏂囨湰鎵弿涓嬬晫锛氶粯璁よ〃澶磋 +3锛涜嫢銆岄」鐩悕绉般�嶅垪棣栦釜鏁版嵁琛屾洿闈犱笅锛屽垯鎵╁睍鍒版暟鎹鍓嶄竴琛岋紙涓婇檺 +12锛夈��
+     *  鑳屾櫙锛氬捀瀹�/鍗佸牥绛夊競宸炴妸銆屽缓璁鹃樁娈�/褰㈣薄杩涘害銆嶆爣绛惧啓鍦ㄨ〃澶翠笅绗� 4~8 琛岋紝鍥哄畾 +3 浼氭紡鍒楋紙褰㈣薄杩涘害璇讳笉鍒帮級銆� */
+    private int investHeaderScanEnd(Sheet sh, int headerRow, Row headerRowObj) {
+        int hardEnd = Math.min(headerRow + 12, sh.getLastRowNum());
+        int nameCol = -1;
+        if (headerRowObj != null) {
+            for (int c = 0; c <= headerRowObj.getLastCellNum(); c++) {
+                String v = cellStr(headerRowObj, c);
+                if (v != null && v.contains("椤圭洰鍚嶇О")) { nameCol = c; break; }
+            }
+        }
+        int firstData = findFirstInvestDataRow(sh, headerRow, nameCol);
+        int scanEnd = (firstData > headerRow)
+            ? Math.min(hardEnd, firstData - 1)
+            : Math.min(headerRow + 3, sh.getLastRowNum());
+        if (scanEnd < headerRow) scanEnd = headerRow;
+        return scanEnd;
+    }
+
+    /** 銆岄」鐩悕绉般�嶅垪棣栦釜鏁版嵁琛屼笅鏍囷紙璺宠繃琛ㄥご缁涓庣┖琛岋級锛涙壘涓嶅埌杩斿洖 -1 */
+    private int findFirstInvestDataRow(Sheet sh, int headerRow, int nameCol) {
+        if (nameCol < 0) return -1;
+        for (int r = headerRow + 1; r <= sh.getLastRowNum(); r++) {
+            Row row = sh.getRow(r);
+            if (row == null) continue;
+            String name = cellStr(row, nameCol);
+            if (name == null || name.trim().isEmpty()) continue;
+            String nt = name.trim().replaceAll("[\r\n]+", "");
+            if (nt.contains("椤圭洰鍚嶇О")) continue;
+            String flat = nt.replaceAll("\\s+", "");
+            if (flat.contains("鍚堣") || flat.contains("鎬昏") || flat.contains("灏忚")) continue;
+            if (nt.matches("^[涓�浜屼笁鍥涗簲鍏竷鍏節鍗乚+銆�.*")) continue;
+            if (nt.matches("^锛圼涓�浜屼笁鍥涗簲鍏竷鍏節鍗乚+锛�.*")) continue;
+            if (nt.startsWith("濉姤") || nt.startsWith("鍗曚綅璐熻矗浜�") || nt.matches("^\\d+銆�.*")) continue;
+            if (investmentCity(nt) != null) continue;
+            return r;
+        }
+        return -1;
     }
 
     /** 缁熻 sheet 鍐呴」鐩暟鎹鏁帮紙鐢ㄤ簬澶� sheet 鍘嗗彶鏂囦欢閫夊綋鏈� sheet锛� */
@@ -2135,9 +2663,9 @@
             String name = cellStr(row, nameCol);
             if (name == null || name.trim().isEmpty()) continue;
             String nt = name.trim().replaceAll("[\r\n]+", "");
-            if (nt.contains("鍚堣") || nt.contains("鎬昏")) continue;
+            String ntFlat = nt.replaceAll("\\s+", "");
+            if (ntFlat.contains("鍚堣") || ntFlat.contains("鎬昏") || ntFlat.contains("灏忚")) continue;
             if (nt.matches("^[涓�浜屼笁鍥涗簲鍏竷鍏節鍗乚+銆�.*")) continue;
-            if (nt.replace(" ", "").matches(".*灏廫\s*璁�.*")) continue;
             if (nt.startsWith("濉姤") || nt.startsWith("鍗曚綅璐熻矗浜�") || nt.matches("^\\d+銆�.*")) continue;
             if (!hasSeq) {
                 if (nt.length() > 4) count++;
@@ -2830,6 +3358,352 @@
         return ok;
     }
 
+    // ========== 缃戠害杞﹁鍗曡繍琛岀洃娴嬪垎鏋愭姤鍛婏紙docx锛夊鍏� ==========
+
+    /** 缃戠害杞﹁鍗曟姤鍛婂鍏ョ粨鏋滐紙鎶ヨ〃鏈�/璁㈠崟鍚堣/瑕嗙洊甯傚窞鏁�/缂哄け甯傚窞锛� */
+    public static class WycOrderReportResult {
+        private final String period;
+        private final double orderSum;
+        private final int cityCount;
+        private final List<String> missingCities;
+
+        public WycOrderReportResult(String period, double orderSum, int cityCount, List<String> missingCities) {
+            this.period = period;
+            this.orderSum = orderSum;
+            this.cityCount = cityCount;
+            this.missingCities = missingCities;
+        }
+
+        public String getPeriod() { return period; }
+        public double getOrderSum() { return orderSum; }
+        public int getCityCount() { return cityCount; }
+        public List<String> getMissingCities() { return missingCities; }
+    }
+
+    /**
+     * 銆婄綉绾﹁溅琛屼笟YYYY骞碝鏈堣繍琛岀洃娴嬫儏鍐靛垎鏋愭姤鍛娿�嬶紙docx锛夊鍏ワ細
+     * 瑙f瀽鎶ュ憡闄勪欢涓�屽湴甯傚悕绉� | 鈥� | 璁㈠崟鏁版�绘暟銆嶈〃鐨� 17 甯傚窞璁㈠崟鏁帮紝鍐欏叆 wyc_order_monthly 鐨�
+     * F 璁㈠崟鍚堣涓� G-W 17 甯傚窞璁㈠崟鍒楋紱鍏ㄧ渷 pin锛圔-E锛夋部鐢ㄨ鏈熷凡鏈夎锛岃嫢鏃犺鏈熻鍒欎粠
+     * wyc_total_monthly 鍚屾姤琛ㄦ湡澶嶅埗銆傛姤琛ㄦ湡浼樺厛鍙栨姤鍛婃爣棰樺勾鏈堬紝鍙栦笉鍒版椂鐢ㄩ〉闈㈡墍閫夋姤琛ㄦ湡銆�
+     */
+    public synchronized WycOrderReportResult importWycOrderReport(MultipartFile file, String period) throws Exception {
+        Map<String, Double> orders = new LinkedHashMap<>();
+        double[] sumHolder = new double[]{0.0};
+        String docPeriod;
+        byte[] bytes;
+        try (java.io.InputStream in = file.getInputStream()) {
+            bytes = IoUtil.readBytes(in);
+        }
+        boolean isZip = bytes.length > 3 && (bytes[0] & 0xFF) == 0x50 && (bytes[1] & 0xFF) == 0x4B;
+        boolean isWordMl = !isZip && looksLikeWordMl(bytes);
+        try {
+            if (isZip && !isWordPackage(bytes)) {
+                throw new RuntimeException("杩欎笉鏄� Word 鏂囨。锛堝帇缂╁寘閲屾病鏈� word/document.xml锛夛紝"
+                        + "澶氬崐鏄� Excel 鎴栧叾瀹冩牸寮忋�傝涓婁紶缃戠害杞﹁鍗曞垎鏋愭姤鍛婄殑 .docx锛堟垨 Word 2003 XML锛�");
+            }
+            if (isZip) {
+                try (XWPFDocument doc = new XWPFDocument(new java.io.ByteArrayInputStream(bytes))) {
+                    docPeriod = periodOfParagraphs(paragraphsOfDocx(doc));
+                    parseWycOrderReportTables(doc, orders, sumHolder);
+                }
+            } else if (isWordMl) {
+                // 缃戠害杞︾洃绠″钩鍙颁笅鍙戠殑鎶ュ憡甯歌 WPS/Word 瀛樻垚銆學ord 2003 XML锛堝钩閾� XML锛夈�嶅悗鍐嶆敼鍚� .docx锛�
+                // 杩欑鏂囦欢涓嶆槸 zip 鍖咃紝XWPFDocument 鎵撲笉寮�锛堟姤 "File is not a zip file"锛夛紝闇�鎸� XML 鐩存帴瑙f瀽銆�
+                String xml = new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
+                Document dom = parseXmlSafely(xml);
+                docPeriod = periodOfParagraphs(paragraphsOfDocxXml(dom));
+                parseWycOrderReportTablesXml(dom, orders, sumHolder);
+            } else if (isOle2(bytes)) {
+                throw new RuntimeException("杩欐槸 Word 97-2003 鑰佹牸寮忥紙.doc锛夛紝璇风敤 WPS/Word 鎵撳紑鍚庛�屽彟瀛樹负銆峝ocx 鏍煎紡鍐嶅鍏�");
+            } else {
+                throw new RuntimeException("鏃犳硶璇嗗埆鐨勬枃浠舵牸寮忥細鏃笉鏄� docx锛坺ip 鍖咃級锛屼篃涓嶆槸 Word 2003 XML 鎴� .doc");
+            }
+        } catch (RuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new RuntimeException("鏃犳硶璇诲彇 Word 鏂囨。锛岃涓婁紶 .docx 鏍煎紡鐨勩�婄綉绾﹁溅琛屼笟YYYY骞碝鏈堣繍琛岀洃娴嬫儏鍐靛垎鏋愭姤鍛娿�嬶紱"
+                    + "鑻ヤ笂浼犵殑鍏跺疄鏄� Excel/PDF锛岃鏀圭敤瀵瑰簲妯℃澘瀵煎叆銆傦紙鍘熷閿欒锛�" + e.getMessage() + "锛�");
+        }
+        if (orders.isEmpty()) {
+            throw new RuntimeException("鏈В鏋愬埌浠讳綍甯傚窞璁㈠崟鏁帮紝璇风‘璁や笂浼犵殑鏄�婄綉绾﹁溅琛屼笟YYYY骞碝鏈堣繍琛岀洃娴嬫儏鍐靛垎鏋愭姤鍛娿��");
+        }
+        String rp = null;
+        if (docPeriod != null && docPeriod.matches("\\d{4}-\\d{2}")) rp = docPeriod;
+        else if (period != null && period.matches("\\d{4}-\\d{2}")) rp = period;
+        if (rp == null) {
+            throw new RuntimeException("鏃犳硶纭畾鎶ヨ〃鏈燂細鎶ュ憡鏍囬鏈惈銆孻YYY骞碝鏈堛�嶏紝涓旈〉闈㈡湭閫夋嫨鎶ヨ〃鏈�");
+        }
+
+        List<String> missing = new ArrayList<>();
+        double citySum = 0.0;
+        for (String city : RegionUtil.cityList()) {
+            Double v = orders.get(city);
+            if (v == null) missing.add(city);
+            else citySum += v;
+        }
+        double sum = sumHolder[0] > 0 ? sumHolder[0] : citySum;
+
+        WycOrderMonthly it = wycOrderMapper.selectOne(new LambdaQueryWrapper<WycOrderMonthly>()
+                .eq(WycOrderMonthly::getReportPeriod, rp).last("LIMIT 1"));
+        if (it == null) it = new WycOrderMonthly();
+        it.setReportPeriod(rp);
+        it.setOrderSum(sum > 0 ? sum : null);
+        it.setOrderWuhan(orderOf(orders, "姝︽眽甯�"));
+        it.setOrderHuangshi(orderOf(orders, "榛勭煶甯�"));
+        it.setOrderShiyan(orderOf(orders, "鍗佸牥甯�"));
+        it.setOrderYichang(orderOf(orders, "瀹滄槍甯�"));
+        it.setOrderXiangyang(orderOf(orders, "瑗勯槼甯�"));
+        it.setOrderEzhou(orderOf(orders, "閯傚窞甯�"));
+        it.setOrderJingmen(orderOf(orders, "鑽嗛棬甯�"));
+        it.setOrderXiaogan(orderOf(orders, "瀛濇劅甯�"));
+        it.setOrderJingzhou(orderOf(orders, "鑽嗗窞甯�"));
+        it.setOrderHuanggang(orderOf(orders, "榛勫唸甯�"));
+        it.setOrderXianning(orderOf(orders, "鍜稿畞甯�"));
+        it.setOrderSuizhou(orderOf(orders, "闅忓窞甯�"));
+        it.setOrderEnshi(orderOf(orders, "鎭╂柦宸�"));
+        it.setOrderXiantao(orderOf(orders, "浠欐甯�"));
+        it.setOrderQianjiang(orderOf(orders, "娼滄睙甯�"));
+        it.setOrderTianmen(orderOf(orders, "澶╅棬甯�"));
+        it.setOrderShennong(orderOf(orders, "绁炲啘鏋舵灄鍖�"));
+
+        if (it.getPinK() == null || it.getPinCK() == null || it.getPinZ() == null || it.getPinCZ() == null) {
+            WycTotalMonthly tot = wycTotalMapper.selectOne(new LambdaQueryWrapper<WycTotalMonthly>()
+                    .eq(WycTotalMonthly::getReportPeriod, rp).last("LIMIT 1"));
+            if (tot != null) {
+                if (it.getPinK() == null) it.setPinK(tot.getPinK());
+                if (it.getPinCK() == null) it.setPinCK(tot.getPinCK());
+                if (it.getPinZ() == null) it.setPinZ(tot.getPinZ());
+                if (it.getPinCZ() == null) it.setPinCZ(tot.getPinCZ());
+            }
+        }
+
+        wycOrderMapper.delete(new LambdaQueryWrapper<WycOrderMonthly>().eq(WycOrderMonthly::getReportPeriod, rp));
+        wycOrderMapper.insert(it);
+        recordBatch(file.getOriginalFilename(), "WYC_ORDER_REPORT", rp, 1, 1, 0, null);
+        log.info("WycOrderReport imported: period={} sum={} cities={} missing={}", rp, sum, orders.size(), missing);
+        return new WycOrderReportResult(rp, sum, orders.size(), missing);
+    }
+
+    /** 鎶ュ憡鏍囬涓殑鎶ヨ〃鏈燂細缃戠害杞﹁涓�2026骞�7鏈� -> 2026-07锛涘彇涓嶅埌杩斿洖 null */
+    private String periodOfParagraphs(List<String> paragraphs) {
+        java.util.regex.Pattern titlePat = java.util.regex.Pattern.compile("缃戠害杞﹁涓歕\s*(\\d{4})\\s*骞碶\s*(\\d{1,2})\\s*鏈�");
+        java.util.regex.Pattern anyPat = java.util.regex.Pattern.compile("(\\d{4})\\s*骞碶\s*(\\d{1,2})\\s*鏈�");
+        String fallback = null;
+        int scanned = 0;
+        for (String text : paragraphs) {
+            if (text == null) continue;
+            String flat = text.replaceAll("\\s+", "");
+            if (flat.isEmpty()) continue;
+            java.util.regex.Matcher m = titlePat.matcher(flat);
+            if (m.find()) return ymd(m.group(1), m.group(2));
+            if (fallback == null && scanned < 10) {
+                java.util.regex.Matcher m2 = anyPat.matcher(flat);
+                if (m2.find()) fallback = ymd(m2.group(1), m2.group(2));
+            }
+            scanned++;
+        }
+        return fallback;
+    }
+
+    /** docx 娈佃惤鏂囨湰锛堜繚鎸佸師濮嬮『搴忥級 */
+    private List<String> paragraphsOfDocx(XWPFDocument doc) {
+        List<String> out = new ArrayList<>();
+        for (XWPFParagraph para : doc.getParagraphs()) out.add(para.getText());
+        return out;
+    }
+
+    /** Word 2003 XML锛圵ordML锛夋钀芥枃鏈細<w:p> 涓嬫墍鏈� <w:t> 鎷兼帴鍚庡帇鎺夌┖鐧� */
+    private List<String> paragraphsOfDocxXml(Document dom) {
+        List<String> out = new ArrayList<>();
+        for (Element p : elementsByLocalName(dom, "p")) out.add(textOfXmlElement(p));
+        return out;
+    }
+
+    /** 鏄惁涓� Word 2003 XML锛圵ordML 骞抽摵 XML锛夆�斺�旇繖绉嶆枃浠跺父琚敼鍚嶆垚 .docx */
+    private boolean looksLikeWordMl(byte[] bytes) {
+        int n = Math.min(bytes.length, 4096);
+        String head = new String(bytes, 0, n, java.nio.charset.StandardCharsets.UTF_8);
+        return head.contains("wordml") && head.contains("<w:wordDocument");
+    }
+
+    /** zip 鍖呭唴鏄惁鍚� word/document.xml锛堢敤浜庡尯鍒� docx 涓� xlsx/鍏跺畠 OPC 鍖咃級 */
+    private boolean isWordPackage(byte[] bytes) {
+        try (java.util.zip.ZipInputStream zin =
+                     new java.util.zip.ZipInputStream(new java.io.ByteArrayInputStream(bytes))) {
+            java.util.zip.ZipEntry e;
+            while ((e = zin.getNextEntry()) != null) {
+                String n = e.getName();
+                if ("word/document.xml".equals(n)) return true;
+            }
+        } catch (Exception ignore) {
+            return false;
+        }
+        return false;
+    }
+
+    /** 鏄惁涓� OLE2 澶嶅悎鏂囨。锛圵ord 97-2003 .doc锛� */
+    private boolean isOle2(byte[] b) {
+        return b.length > 8 && (b[0] & 0xFF) == 0xD0 && (b[1] & 0xFF) == 0xCF
+                && (b[2] & 0xFF) == 0x11 && (b[3] & 0xFF) == 0xE0;
+    }
+
+    /** 瀹夊叏瑙f瀽 XML锛氱鐢ㄥ閮ㄥ疄浣�/DTD锛堟姤鍛婃潵鑷閮ㄥ钩鍙帮紝闃� XXE锛� */
+    private Document parseXmlSafely(String xml) throws Exception {
+        javax.xml.parsers.DocumentBuilderFactory f = javax.xml.parsers.DocumentBuilderFactory.newInstance();
+        f.setNamespaceAware(true);
+        try { f.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); } catch (Exception ignore) { }
+        try { f.setFeature("http://xml.org/sax/features/external-general-entities", false); } catch (Exception ignore) { }
+        try { f.setFeature("http://xml.org/sax/features/external-parameter-entities", false); } catch (Exception ignore) { }
+        javax.xml.parsers.DocumentBuilder b = f.newDocumentBuilder();
+        b.setEntityResolver(new org.xml.sax.EntityResolver() {
+            @Override
+            public org.xml.sax.InputSource resolveEntity(String publicId, String systemId) {
+                return new org.xml.sax.InputSource(new java.io.StringReader(""));
+            }
+        });
+        return b.parse(new org.xml.sax.InputSource(new java.io.StringReader(xml)));
+    }
+
+    /** 鎸� localName 鍙栧叏閮ㄥ悗浠e厓绱狅紙鍛藉悕绌洪棿鏃犲叧锛屽吋瀹� WordML 涓� docx 鐨� w: 鍓嶇紑锛� */
+    private List<Element> elementsByLocalName(Document dom, String localName) {
+        List<Element> out = new ArrayList<>();
+        NodeList list = dom.getElementsByTagNameNS("*", localName);
+        for (int i = 0; i < list.getLength(); i++) out.add((Element) list.item(i));
+        return out;
+    }
+
+    /** 鍙彇鐩存帴瀛愬厓绱狅紙閬垮厤宓屽琛ㄦ牸/宓屽灞炴�у厓绱犺绠楄繘鏉ワ級 */
+    private List<Element> childElementsByLocalName(Element parent, String localName) {
+        List<Element> out = new ArrayList<>();
+        NodeList kids = parent.getChildNodes();
+        for (int i = 0; i < kids.getLength(); i++) {
+            Node n = kids.item(i);
+            if (n.getNodeType() == Node.ELEMENT_NODE && localName.equals(n.getLocalName())) out.add((Element) n);
+        }
+        return out;
+    }
+
+    /** 鍏冪礌鍐呮墍鏈� <w:t> 鏂囨湰鎷兼帴骞跺帇鎺夌┖鐧� */
+    private String textOfXmlElement(Element el) {
+        StringBuilder sb = new StringBuilder();
+        NodeList ts = el.getElementsByTagNameNS("*", "t");
+        for (int i = 0; i < ts.getLength(); i++) sb.append(ts.item(i).getTextContent());
+        return flat(sb.toString());
+    }
+
+    private String ymd(String y, String mo) {
+        try {
+            int month = Integer.parseInt(mo);
+            if (month < 1 || month > 12) return null;
+            return String.format("%04d-%02d", Integer.parseInt(y), month);
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    /** 瑙f瀽鎶ュ憡涓殑銆屽湴甯傚悕绉� | 鈥� | 璁㈠崟鏁版�绘暟銆嶈〃锛宱rders=甯傚窞璁㈠崟锛宻umHolder[0]=鍚堣琛岃鍗曟暟 */
+    private void parseWycOrderReportTables(XWPFDocument doc, Map<String, Double> orders, double[] sumHolder) {
+        List<List<List<String>>> tables = new ArrayList<>();
+        for (XWPFTable table : doc.getTables()) {
+            List<List<String>> rows = new ArrayList<>();
+            for (XWPFTableRow row : table.getRows()) {
+                List<String> cells = new ArrayList<>();
+                for (org.apache.poi.xwpf.usermodel.XWPFTableCell c : row.getTableCells()) cells.add(flat(c.getText()));
+                rows.add(cells);
+            }
+            tables.add(rows);
+        }
+        matchWycOrderTable(tables, orders, sumHolder);
+    }
+
+    /** Word 2003 XML锛圵ordML锛夊垎鏀細<w:tbl>/<w:tr>/<w:tc> 鎶芥垚鏂囨湰琛ㄥ悗璧板悓涓�濂楀尮閰嶉�昏緫 */
+    private void parseWycOrderReportTablesXml(Document dom, Map<String, Double> orders, double[] sumHolder) {
+        List<List<List<String>>> tables = new ArrayList<>();
+        for (Element tbl : elementsByLocalName(dom, "tbl")) {
+            List<List<String>> rows = new ArrayList<>();
+            for (Element tr : childElementsByLocalName(tbl, "tr")) {
+                List<String> cells = new ArrayList<>();
+                for (Element tc : childElementsByLocalName(tr, "tc")) cells.add(textOfXmlElement(tc));
+                rows.add(cells);
+            }
+            tables.add(rows);
+        }
+        matchWycOrderTable(tables, orders, sumHolder);
+    }
+
+    /**
+     * 鍦ㄣ�屾枃鏈寲琛ㄦ牸銆嶉噷鎵鹃檮浠�1锛堣〃澶村惈銆岃鍗曟暟鎬绘暟銆�/銆岃鍗曟暟銆嶏級骞跺彇 17 甯傚窞璁㈠崟鏁颁笌鍚堣銆�
+     * 鍏ュ弬 tables = 琛� -> 琛� -> 鍗曞厓鏍兼枃鏈紙宸插帇绌虹櫧锛夛紝涓庢潵婧愭牸寮忔棤鍏炽��
+     */
+    private void matchWycOrderTable(List<List<List<String>>> tables, Map<String, Double> orders, double[] sumHolder) {
+        for (List<List<String>> rows : tables) {
+            if (rows.size() < 3) continue;
+            int headerRow = -1;
+            int orderCol = -1;
+            for (int r = 0; r < Math.min(rows.size(), 3); r++) {
+                List<String> cells = rows.get(r);
+                for (int c = 0; c < cells.size(); c++) {
+                    String norm = cells.get(c);
+                    if (norm.contains("璁㈠崟鏁版�绘暟") || "璁㈠崟鏁�".equals(norm)) {
+                        headerRow = r;
+                        orderCol = c;
+                        break;
+                    }
+                }
+                if (headerRow >= 0) break;
+            }
+            if (headerRow < 0 || orderCol < 0) continue;
+
+            Map<String, Double> found = new LinkedHashMap<>();
+            double citySum = 0.0;
+            boolean hasTotal = false;
+            for (int r = headerRow + 1; r < rows.size(); r++) {
+                List<String> cells = rows.get(r);
+                if (cells.isEmpty() || orderCol >= cells.size()) continue;
+                String name = cells.get(0);
+                if (name.isEmpty()) continue;
+                Double val = parsePlainNumber(cells.get(orderCol));
+                if (val == null) continue;
+                String city = RegionUtil.normalizeCityName(name);
+                if (city != null && RegionUtil.cityList().contains(city)) {
+                    found.put(city, val);
+                    citySum += val;
+                } else if (name.contains("鍚堣") || name.contains("鎬昏") || name.contains("鍏ㄧ渷") || name.contains("婀栧寳")) {
+                    hasTotal = true;
+                    sumHolder[0] = val;
+                }
+            }
+            if (!found.isEmpty()) {
+                orders.clear();
+                orders.putAll(found);
+                if (!hasTotal) sumHolder[0] = citySum;
+                return;
+            }
+        }
+    }
+
+    private String flat(String s) {
+        return s == null ? "" : s.replaceAll("\\s+", "").trim();
+    }
+
+    /** 銆�11,183,267 鍗曘�嶈繖绫绘枃鏈彇鏁板�� */
+    private Double parsePlainNumber(String s) {
+        if (s == null) return null;
+        String t = s.replaceAll("[,\\s]", "").trim();
+        if (t.isEmpty()) return null;
+        java.util.regex.Matcher m = java.util.regex.Pattern.compile("-?\\d+(?:\\.\\d+)?").matcher(t);
+        if (!m.find()) return null;
+        try {
+            return Double.parseDouble(m.group());
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    private Double orderOf(Map<String, Double> orders, String city) {
+        Double v = orders.get(city);
+        return (v == null || v <= 0) ? null : v;
+    }
     private String periodOfCell(Cell c) {
         if (c == null) return null;
         if (c.getCellType() == CellType.NUMERIC) {
@@ -3199,6 +4073,7 @@
             case "scaleSplit": return new ImportResult(importScaleSplit(file, period), 0, new ArrayList<>());
             case "wycTotal": return new ImportResult(importWycTotal(file, period), 0, new ArrayList<>());
             case "wycOrder": return new ImportResult(importWycOrder(file, period), 0, new ArrayList<>());
+            case "wycOrderReport": return new ImportResult(importWycOrderReport(file, period).getCityCount(), 0, new ArrayList<>());
             default: throw new RuntimeException("涓嶆敮鎸佺殑瀵煎叆绫诲瀷锛�" + type);
         }
     }

--
Gitblit v1.9.1