From b14fec1599277629e10b2bc686f02fd0c2e0bec1 Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期五, 18 九月 2026 16:46:30 +0800
Subject: [PATCH] fix(货运月报): 订正持续在规企业累计运距同比基期口径(v2)

---
 traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java | 3411 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 3,379 insertions(+), 32 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 fc3edb9..19191bd 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,21 +2,53 @@
 
 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;
 import com.trafficaudit.dataimport.entity.FreightTurnoverImport;
 import com.trafficaudit.dataimport.entity.H2032EnterpriseMonthly;
+import com.trafficaudit.dataimport.entity.PassengerAuthVehicle;
+import com.trafficaudit.dataimport.entity.PassengerEnterpriseMonthly;
+import com.trafficaudit.dataimport.entity.PassengerIndividualMonthly;
 import com.trafficaudit.dataimport.entity.ImportBatch;
+import com.trafficaudit.dataimport.entity.InvestmentMonthly;
+import com.trafficaudit.dataimport.entity.InvestmentProject;
+import com.trafficaudit.dataimport.entity.InvestmentSystem;
+import com.trafficaudit.dataimport.entity.CityBusMonthly;
+import com.trafficaudit.dataimport.entity.CityTaxiMonthly;
+import com.trafficaudit.dataimport.entity.CityTaxiAuth;
+import com.trafficaudit.dataimport.entity.WycOrderMonthly;
+import com.trafficaudit.dataimport.entity.WycTotalMonthly;
 import com.trafficaudit.dataimport.entity.ScaleSplitTransport;
 import com.trafficaudit.dataimport.entity.TransportAuthVehicle;
 import com.trafficaudit.dataimport.entity.VehicleTrackMileage;
+import com.trafficaudit.dataimport.mapper.EnergyAuthVehicleMapper;
+import com.trafficaudit.dataimport.mapper.EnergyVehicleQuarterlyMapper;
 import com.trafficaudit.dataimport.mapper.FreightTurnoverImportMapper;
 import com.trafficaudit.dataimport.mapper.H2032EnterpriseMonthlyMapper;
+import com.trafficaudit.dataimport.mapper.PassengerAuthVehicleMapper;
+import com.trafficaudit.dataimport.mapper.PassengerEnterpriseMonthlyMapper;
+import com.trafficaudit.dataimport.mapper.PassengerIndividualMonthlyMapper;
+import com.trafficaudit.rulemanage.mapper.AuditRuleMapper;
 import com.trafficaudit.dataimport.mapper.ImportBatchMapper;
+import com.trafficaudit.dataimport.mapper.InvestmentMonthlyMapper;
+import com.trafficaudit.dataimport.mapper.InvestmentProjectMapper;
+import com.trafficaudit.dataimport.mapper.InvestmentSystemMapper;
+import com.trafficaudit.dataimport.mapper.CityBusMonthlyMapper;
+import com.trafficaudit.dataimport.mapper.CityTaxiMonthlyMapper;
+import com.trafficaudit.dataimport.mapper.CityTaxiAuthMapper;
+import com.trafficaudit.dataimport.mapper.WycOrderMonthlyMapper;
+import com.trafficaudit.dataimport.mapper.WycTotalMonthlyMapper;
 import com.trafficaudit.dataimport.mapper.ScaleSplitTransportMapper;
 import com.trafficaudit.dataimport.mapper.TransportAuthVehicleMapper;
 import com.trafficaudit.dataimport.mapper.VehicleTrackMileageMapper;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.session.ExecutorType;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
 import org.apache.poi.ss.usermodel.Cell;
 import org.apache.poi.ss.usermodel.CellType;
 import org.apache.poi.ss.usermodel.DataFormatter;
@@ -24,18 +56,41 @@
 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;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import com.trafficaudit.dataimport.dto.ImportResult;
 import org.apache.poi.ss.usermodel.DateUtil;
+import java.util.Comparator;
+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;
 
@@ -47,6 +102,43 @@
 public class DataImportService {
 
     private static final DataFormatter FORMATTER = new DataFormatter();
+
+    /** H204 鑳借�楁槑缁嗚溅杈嗙被鍨嬬爜 -> 涓枃 */
+    private static final Map<String, String> ENERGY_VEHICLE_TYPE_NAMES = new HashMap<>();
+
+    /** H204 鑳借�楁槑缁嗙噧鏂欑被鍨嬬爜 -> 涓枃 */
+    private static final Map<String, String> ENERGY_FUEL_TYPE_NAMES = new HashMap<>();
+
+    /** H204 鐕冩枡1璁¢噺鍗曚綅鐮� -> 涓枃鍗曚綅 */
+    private static final Map<String, String> ENERGY_FUEL_UNIT_NAMES = new HashMap<>();
+
+    /** 鑳借�楀鍏ユ寜鎶ヨ〃鏈熶簰鏂ワ紝闃叉鍚屾姤琛ㄦ湡骞跺彂閲嶅浜掔浉瑕嗙洊 */
+    private final ConcurrentHashMap<String, Object> energyImportLocks = new ConcurrentHashMap<>();
+
+    static {
+        ENERGY_VEHICLE_TYPE_NAMES.put("01", "鏅�氳揣杞�");
+        ENERGY_VEHICLE_TYPE_NAMES.put("02", "骞虫澘璐ц溅");
+        ENERGY_VEHICLE_TYPE_NAMES.put("03", "浠撴爡寮忚揣杞�");
+        ENERGY_VEHICLE_TYPE_NAMES.put("04", "鍘㈠紡璐ц溅");
+        ENERGY_VEHICLE_TYPE_NAMES.put("06", "缃愬紡璐ц溅");
+        ENERGY_VEHICLE_TYPE_NAMES.put("07", "鐗规畩缁撴瀯璐ц溅");
+        ENERGY_VEHICLE_TYPE_NAMES.put("08", "鑷嵏璐ц溅");
+        ENERGY_VEHICLE_TYPE_NAMES.put("09", "杞﹁締杩愯緭杞�");
+        ENERGY_VEHICLE_TYPE_NAMES.put("11", "閲嶅瀷鍗婃寕鐗靛紩杞�");
+
+        ENERGY_FUEL_TYPE_NAMES.put("01", "姹芥补");
+        ENERGY_FUEL_TYPE_NAMES.put("02", "鏌存补");
+        ENERGY_FUEL_TYPE_NAMES.put("03", "鍘嬬缉澶╃劧姘�");
+        ENERGY_FUEL_TYPE_NAMES.put("04", "娑插寲澶╃劧姘�");
+        ENERGY_FUEL_TYPE_NAMES.put("07", "鐢靛姩");
+        ENERGY_FUEL_TYPE_NAMES.put("08", "姘㈡皵");
+
+        ENERGY_FUEL_UNIT_NAMES.put("1", "鍗�");
+        ENERGY_FUEL_UNIT_NAMES.put("2", "鍗�");
+        ENERGY_FUEL_UNIT_NAMES.put("4", "鍗冨厠");
+        ENERGY_FUEL_UNIT_NAMES.put("5", "鍗冨厠");
+        ENERGY_FUEL_UNIT_NAMES.put("7", "鍗冪摝鏃�");
+    }
 
     @Resource
     private H2032EnterpriseMonthlyMapper h2032Mapper;
@@ -62,6 +154,38 @@
     private FreightTurnoverImportMapper freightTurnoverMapper;
     @Resource
     private AuditResultMapper auditResultMapper;
+    @Resource
+    private AuditRunMapper auditRunMapper;
+    @Resource
+    private PassengerEnterpriseMonthlyMapper passengerMapper;
+    @Resource
+    private PassengerIndividualMonthlyMapper passengerIndividualMapper;
+    @Resource
+    private PassengerAuthVehicleMapper passengerAuthMapper;
+    @Resource
+    private AuditRuleMapper ruleMapper;
+    @Resource
+    private EnergyVehicleQuarterlyMapper energyMapper;
+    @Resource
+    private EnergyAuthVehicleMapper energyAuthMapper;
+    @Resource
+    private InvestmentProjectMapper investProjectMapper;
+    @Resource
+    private InvestmentMonthlyMapper investMonthlyMapper;
+    @Resource
+    private InvestmentSystemMapper investSystemMapper;
+    @Resource
+    private CityBusMonthlyMapper cityBusMapper;
+    @Resource
+    private CityTaxiMonthlyMapper cityTaxiMapper;
+    @Resource
+    private CityTaxiAuthMapper cityTaxiAuthMapper;
+    @Resource
+    private WycTotalMonthlyMapper wycTotalMapper;
+    @Resource
+    private WycOrderMonthlyMapper wycOrderMapper;
+    @Resource
+    private SqlSessionFactory sqlSessionFactory;
 
     // ========== H2032 鏈堟姤 ==========
 
@@ -70,6 +194,7 @@
         Map<String, Integer> colMap;
         try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
             Sheet sheet = wb.getSheetAt(0);
+            requireHeaderCols(sheet, "閬撹矾璐х墿杩愯緭鏈堝害鐢熶骇鎯呭喌", "浼佷笟鍚嶇О", "璐ц繍閲廮鎬昏");
             colMap = buildHeaderMap(sheet.getRow(0));
             for (int r = 1; r <= sheet.getLastRowNum(); r++) {
                 Row row = sheet.getRow(r);
@@ -136,11 +261,18 @@
             throw e;
         }
 
-        // 骞傜瓑锛氬悓鎶ヨ〃鏈熼噸鏂板鍏ワ紝鍏堝垹闄ゆ棫鏁版嵁
+        // 骞傜瓑锛氬悓鎶ヨ〃鏈熼噸鏂板鍏ワ紝鍏堝垹闄ゆ棫鏁版嵁锛堝彧娓� H2032 瑙勫垯鐨勫鏍哥粨鏋滐級
         h2032Mapper.delete(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
             .eq(H2032EnterpriseMonthly::getReportPeriod, period));
-        auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
-            .eq(AuditResult::getReportPeriod, period));
+        List<Long> h2032RuleIds = new ArrayList<>();
+        for (com.trafficaudit.rulemanage.entity.AuditRule rule : ruleMapper.selectList(null)) {
+            if ("H2032".equals(rule.getReportType())) h2032RuleIds.add(rule.getId());
+        }
+        if (!h2032RuleIds.isEmpty()) {
+            auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
+                .eq(AuditResult::getReportPeriod, period)
+                .in(AuditResult::getRuleId, h2032RuleIds));
+        }
 
         int success = 0;
         List<String> failDetails = new ArrayList<>();
@@ -171,26 +303,570 @@
     }
 
 
+    // ========== H203-1 鍏矾鏃呭鏈堟姤锛堝惈杩愭斂杞﹁締鏁皊heet锛� ==========
+
+    public ImportResult importPassengerMonthly(MultipartFile file, String period) throws Exception {
+        List<PassengerRow> rows = new ArrayList<>();
+        List<PassengerAuthVehicle> authList = new ArrayList<>();
+        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+            // sheet0: 鍏矾鏃呭6鏈堟姤
+            Sheet sheet = wb.getSheetAt(0);
+            requireHeaderCols(sheet, "鍏矾鏃呭杩愯緭鏈堝害鐢熶骇鎯呭喌", "浼佷笟鍚嶇О", "瀹㈣繍閲廮鎬昏");
+            Map<String, Integer> colMap = buildHeaderMap(sheet.getRow(0));
+            for (int r = 1; r <= sheet.getLastRowNum(); r++) {
+                Row row = sheet.getRow(r);
+                if (row == null) continue;
+                String name = gs(row, colMap, "浼佷笟鍚嶇О");
+                if (name == null || name.trim().isEmpty()) continue;
+                PassengerEnterpriseMonthly entity = new PassengerEnterpriseMonthly();
+                entity.setReportPeriod(period);
+                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, "杞﹁締鏁癬瀹㈣繍鐝溅锛堝惈瀹氱嚎鏃呮父瀹㈣繍瀹㈣溅锛�"));
+                entity.setSeatSchedule(gi(row, colMap, "鏍稿畾杞藉浣峗瀹㈣繍鐝溅锛堝惈瀹氱嚎鏃呮父瀹㈣繍瀹㈣溅锛�"));
+                entity.setVehicleCharter(gi(row, colMap, "杞﹁締鏁癬瀹㈣繍鍖呰溅锛堝惈闈炲畾绾挎梾娓稿杩愬杞︼級"));
+                entity.setSeatCharter(gi(row, colMap, "鏍稿畾杞藉浣峗瀹㈣繍鍖呰溅锛堝惈闈炲畾绾挎梾娓稿杩愬杞︼級"));
+                entity.setPassengerTotal(gd(row, colMap, "瀹㈣繍閲廮鎬昏"));
+                entity.setTurnoverTotal(gd(row, colMap, "鏃呭鍛ㄨ浆閲廮鎬昏"));
+                entity.setPassengerClass1(gd(row, colMap, "瀹㈣繍閲廮涓�绫诲杩愮彮绾匡紙鍚畾绾挎梾娓革級"));
+                entity.setTurnoverClass1(gd(row, colMap, "鏃呭鍛ㄨ浆閲廮涓�绫诲杩愮彮绾匡紙鍚畾绾挎梾娓革級"));
+                entity.setPassengerClass2(gd(row, colMap, "瀹㈣繍閲廮浜岀被瀹㈣繍鐝嚎锛堝惈瀹氱嚎鏃呮父锛�"));
+                entity.setTurnoverClass2(gd(row, colMap, "鏃呭鍛ㄨ浆閲廮浜岀被瀹㈣繍鐝嚎锛堝惈瀹氱嚎鏃呮父锛�"));
+                entity.setPassengerClass3(gd(row, colMap, "瀹㈣繍閲廮涓夌被瀹㈣繍鐝嚎锛堝惈瀹氱嚎鏃呮父锛�"));
+                entity.setTurnoverClass3(gd(row, colMap, "鏃呭鍛ㄨ浆閲廮涓夌被瀹㈣繍鐝嚎锛堝惈瀹氱嚎鏃呮父锛�"));
+                entity.setPassengerClass4(gd(row, colMap, "瀹㈣繍閲廮鍥涚被瀹㈣繍鐝嚎锛堝惈瀹氱嚎鏃呮父锛�"));
+                entity.setTurnoverClass4(gd(row, colMap, "鏃呭鍛ㄨ浆閲廮鍥涚被瀹㈣繍鐝嚎锛堝惈瀹氱嚎鏃呮父锛�"));
+                entity.setPassengerCharter(gd(row, colMap, "瀹㈣繍閲廮瀹㈣繍鍖呰溅锛堝惈闈炲畾绾挎梾娓革級"));
+                entity.setTurnoverCharter(gd(row, colMap, "鏃呭鍛ㄨ浆閲廮瀹㈣繍鍖呰溅锛堝惈闈炲畾绾挎梾娓革級"));
+                entity.setAvgDistanceTotal(gd(row, colMap, "瀹㈣繍骞冲潎杩愯窛"));
+                entity.setAvgDistanceClass1(gd(row, colMap, "涓�绫诲杩愮彮绾垮钩鍧囪繍璺�"));
+                entity.setAvgDistanceClass2(gd(row, colMap, "浜岀被瀹㈣繍鐝嚎骞冲潎杩愯窛"));
+                entity.setAvgDistanceClass3(gd(row, colMap, "涓夌被瀹㈣繍鐝嚎骞冲潎杩愯窛"));
+                entity.setAvgDistanceClass4(gd(row, colMap, "鍥涚被瀹㈣繍鐝嚎骞冲潎杩愯窛"));
+                entity.setAvgDistanceCharter(gd(row, colMap, "鏃呮父銆佸寘杞﹀钩鍧囪繍璺�"));
+                entity.setVerifyExplanation(gs(row, colMap, "鏍稿疄鎬у師鍥犺В閲�"));
+                rows.add(new PassengerRow(r + 1, entity));
+            }
+            // sheet1: 杩愭斂杞﹁締鏁帮紙鍥哄畾4鍒楋細浼佷笟浠g爜/浼佷笟鍚嶇О/杞﹁締鏁�/杞藉浣嶆暟锛�
+            if (wb.getNumberOfSheets() > 1) {
+                Sheet authSheet = wb.getSheetAt(1);
+                for (int r = 1; r <= authSheet.getLastRowNum(); r++) {
+                    Row row = authSheet.getRow(r);
+                    if (row == null) continue;
+                    String name = getString(row, 1);
+                    if (name == null || name.trim().isEmpty()) continue;
+                    PassengerAuthVehicle auth = new PassengerAuthVehicle();
+                    auth.setReportPeriod(period);
+                    auth.setEnterpriseCode(getString(row, 0));
+                    auth.setEnterpriseName(name.trim());
+                    auth.setVehicleCount(getInt(row, 2));
+                    auth.setSeatCount(getInt(row, 3));
+                    authList.add(auth);
+                }
+            }
+        } catch (Exception e) {
+            log.error("Passenger monthly parse error", e);
+            throw e;
+        }
+
+        // 骞傜瓑锛氬悓鎶ヨ〃鏈熼噸鏂板鍏ワ紝鍏堝垹闄ゆ棫鏁版嵁
+        passengerMapper.delete(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
+            .eq(PassengerEnterpriseMonthly::getReportPeriod, period));
+        passengerAuthMapper.delete(new LambdaQueryWrapper<PassengerAuthVehicle>()
+            .eq(PassengerAuthVehicle::getReportPeriod, period));
+        // 鍙竻 H2031 瑙勫垯浜х敓鐨勫鏍哥粨鏋滐紝閬垮厤璇垹鍚屾湡鐨勮揣杩愬鏍哥粨鏋�
+        List<Long> h2031RuleIds = new ArrayList<>();
+        for (com.trafficaudit.rulemanage.entity.AuditRule rule : ruleMapper.selectList(null)) {
+            if ("H2031".equals(rule.getReportType())) h2031RuleIds.add(rule.getId());
+        }
+        if (!h2031RuleIds.isEmpty()) {
+            auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
+                .eq(AuditResult::getReportPeriod, period)
+                .in(AuditResult::getRuleId, h2031RuleIds));
+        }
+
+        int success = 0;
+        List<String> failDetails = new ArrayList<>();
+        for (PassengerRow item : rows) {
+            try {
+                passengerMapper.insert(item.entity);
+                success++;
+            } catch (Exception e) {
+                Throwable cause = e;
+                while (cause.getCause() != null) {
+                    cause = cause.getCause();
+                }
+                String reason = cause.getMessage();
+                if (reason == null || reason.trim().isEmpty()) {
+                    reason = e.getMessage();
+                }
+                failDetails.add("绗� " + item.excelRow + " 琛岋紙" + item.entity.getEnterpriseName() + "锛夛細" + reason);
+                log.error("Passenger insert error: row {}, {}", item.excelRow, item.entity.getEnterpriseName(), e);
+            }
+        }
+        String errorDetail = String.join("\n", failDetails);
+        if (errorDetail.length() > 4000) {
+            errorDetail = errorDetail.substring(0, 4000) + "\n鈥︹��";
+        }
+        recordBatch(file.getOriginalFilename(), "H2031", period, rows.size(), success, failDetails.size(), errorDetail);
+
+        int authSuccess = 0;
+        for (PassengerAuthVehicle auth : authList) {
+            try {
+                passengerAuthMapper.insert(auth);
+                authSuccess++;
+            } catch (Exception e) {
+                log.error("passenger auth insert error: {}", auth.getEnterpriseName(), e);
+            }
+        }
+        recordBatch(file.getOriginalFilename(), "PASSENGER_AUTH", period, authList.size(), authSuccess, 0, null);
+        log.info("Passenger monthly imported: {} rows, auth {} rows for {}", success, authSuccess, period);
+        return new ImportResult(success, failDetails.size(), failDetails);
+    }
+
+
+
+    /** 鍏矾鏃呭涓綋瀹㈣繍閲�/鍛ㄨ浆閲忥紙瀵煎叆鍚庢寜甯傚窞姹囨�诲~鍏呭垎甯傚窞琛ㄤ釜浣撹锛夛紝鏀寔涓ょ妯℃澘锛�
+     *  鈶� 浼佷笟绾夸笅姹囨姤鏄庣粏锛堣〃澶村惈銆屼紒涓氬悕绉般�嶏級锛氶�愪紒涓氬叆搴擄紱
+     *  鈶� 鐪佸巺甯傚窞姹囨�昏〃銆婃箹鍖楃渷鍏矾鏃呭杩愯緭涓綋鎴锋湀搴︾敓浜ф儏鍐碉紙YYYY.M锛夈�嬶細
+     *     sheet 鍐呬负銆屽叏鐪丯鏈堜釜浣撳杩愰噺姹囨�昏〃銆嶆爣棰樺潡 + 銆屽競宸�/涓綋杞﹁締鏁�/鈥�/涓綋瀹㈣繍閲�/涓綋鏃呭鍛ㄨ浆閲忋�嶈〃澶达紝
+     *     鎸夊競宸為�愯鍏ュ簱锛堝悓涓�甯傚窞澶氳濡� 鍘垮唴鐝嚎+璺ㄥ幙鐝嚎 鍧囧叆搴擄紝鎶ヨ〃渚ф寜甯傚窞鑷姩姹囨�伙級銆� */
+    public ImportResult importPassengerIndividual(MultipartFile file, String period) throws Exception {
+        List<PassengerIndividualMonthly> rows = new ArrayList<>();
+        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+            Sheet sheet = wb.getSheetAt(0);
+            Map<String, Integer> colMap = buildHeaderMap(sheet.getRow(0));
+            if (colMap.containsKey("浼佷笟鍚嶇О")) {
+                parsePassengerIndividualEnterprise(sheet, colMap, period, rows);
+            } else {
+                parsePassengerIndividualCitySheet(sheet, period, rows);
+            }
+        } catch (Exception e) {
+            log.error("Passenger individual parse error", e);
+            throw e;
+        }
+        passengerIndividualMapper.delete(new LambdaQueryWrapper<PassengerIndividualMonthly>()
+            .eq(PassengerIndividualMonthly::getReportPeriod, period));
+        int success = 0;
+        List<String> failDetails = new ArrayList<>();
+        for (PassengerIndividualMonthly item : rows) {
+            try {
+                passengerIndividualMapper.insert(item);
+                success++;
+            } catch (Exception e) {
+                Throwable cause = e;
+                while (cause.getCause() != null) {
+                    cause = cause.getCause();
+                }
+                String reason = cause.getMessage();
+                if (reason == null || reason.trim().isEmpty()) {
+                    reason = e.getMessage();
+                }
+                failDetails.add("绗� " + (rows.indexOf(item) + 2) + " 琛岋紙" + item.getEnterpriseName() + "锛夛細" + reason);
+            }
+        }
+        String errorDetail = String.join("\n", failDetails);
+        if (errorDetail.length() > 4000) {
+            errorDetail = errorDetail.substring(0, 4000) + "\n鈥︹��";
+        }
+        recordBatch(file.getOriginalFilename(), "PASSENGER_INDIVIDUAL", period, rows.size(), success, failDetails.size(), errorDetail);
+        log.info("Passenger individual imported: {} rows for {}", success, period);
+        return new ImportResult(success, failDetails.size(), failDetails);
+    }
+
+    /** 鏃х増涓綋瀹㈣繍閲忔ā鏉匡紙浼佷笟绾夸笅姹囨姤鏄庣粏锛氭墍灞炲湴鍖�/浼佷笟浠g爜/浼佷笟鍚嶇О/鈥︿釜浣撳杩愰噺/鈥︿釜浣撴梾瀹㈠懆杞噺锛� */
+    private void parsePassengerIndividualEnterprise(Sheet sheet, Map<String, Integer> colMap, String period,
+                                                    List<PassengerIndividualMonthly> rows) {
+        Integer codeCol = colMap.get("鎵�灞炲湴鍖�");
+        Integer entCol = colMap.get("浼佷笟浠g爜");
+        Integer nameCol = colMap.get("浼佷笟鍚嶇О");
+        Integer passCol = null;
+        Integer turnCol = null;
+        for (Map.Entry<String, Integer> e : colMap.entrySet()) {
+            String k = e.getKey();
+            if (k != null && k.contains("涓綋") && k.contains("瀹㈣繍閲�")) passCol = e.getValue();
+            if (k != null && k.contains("涓綋") && (k.contains("鍛ㄨ浆閲�") || k.contains("鍛ㄨ浆"))) turnCol = e.getValue();
+        }
+        if (passCol == null && turnCol == null) {
+            throw new RuntimeException("鏈壘鍒般�屼釜浣撳杩愰噺/涓綋鏃呭鍛ㄨ浆閲忋�嶅垪锛岃妫�鏌ユā鏉匡紙鍒楀悕闇�鍖呭惈鈥滀釜浣撳杩愰噺鈥濃�滀釜浣撳懆杞噺鈥濓級");
+        }
+        for (int r = 1; r <= sheet.getLastRowNum(); r++) {
+            Row row = sheet.getRow(r);
+            if (row == null) continue;
+            String name = nameCol != null ? getString(row, nameCol) : null;
+            if (name == null || name.trim().isEmpty()) continue;
+            Double pass = passCol != null ? getDecimalOrNull(row, passCol) : null;
+            Double turn = turnCol != null ? getDecimalOrNull(row, turnCol) : null;
+            if ((pass == null || pass == 0.0) && (turn == null || turn == 0.0)) continue;
+            PassengerIndividualMonthly entity = new PassengerIndividualMonthly();
+            entity.setReportPeriod(period);
+            entity.setRegionCode(codeCol != null ? getString(row, codeCol) : null);
+            entity.setEnterpriseCode(entCol != null ? getString(row, entCol) : null);
+            entity.setEnterpriseName(name.trim());
+            entity.setPassengerCount(pass);
+            entity.setTurnover(turn);
+            rows.add(entity);
+        }
+    }
+
+    /** 鏂扮増甯傚窞姹囨�绘ā鏉匡細銆婂叏鐪丯鏈堜釜浣撳杩愰噺姹囨�昏〃銆嬫爣棰樿 鈫� 琛ㄥご琛岋紙甯傚窞/鈥�/涓綋瀹㈣繍閲�/涓綋鏃呭鍛ㄨ浆閲忥級鈫� 甯傚窞鏁版嵁琛� */
+    private void parsePassengerIndividualCitySheet(Sheet sheet, String period, List<PassengerIndividualMonthly> rows) {
+        int targetMonth;
+        try {
+            targetMonth = Integer.parseInt(period.split("-")[1]);
+        } catch (Exception e) {
+            throw new RuntimeException("鎶ヨ〃鏈熸牸寮忓簲涓� yyyy-MM锛�" + period);
+        }
+        int titleRow = -1;
+        for (int r = 0; r <= sheet.getLastRowNum(); r++) {
+            Row row = sheet.getRow(r);
+            if (row == null) continue;
+            String t = getString(row, 0);
+            if (t == null || !t.contains("鍏ㄧ渷") || !t.contains("鏈�")) continue;
+            java.util.regex.Matcher m = java.util.regex.Pattern.compile("鍏ㄧ渷(\\d+)鏈�").matcher(t);
+            if (!m.find()) continue;
+            if (Integer.parseInt(m.group(1)) == targetMonth) { titleRow = r; break; }
+            if (titleRow < 0) titleRow = r; // 鏍囬涓嶅惈鐩爣鏈堜唤鏃舵殏璁扮涓�涓爣棰樺潡鍏滃簳
+        }
+        if (titleRow < 0) {
+            throw new RuntimeException("鏈壘鍒般�屽叏鐪丯鏈堜釜浣撳杩愰噺姹囨�昏〃銆嶆爣棰樺潡锛岃浣跨敤銆婃ā鏉縚婀栧寳鐪佸叕璺梾瀹㈣繍杈撲釜浣撴埛鏈堝害鐢熶骇鎯呭喌锛圷YYY.M锛夈�嬫暣鐞嗗悗瀵煎叆");
+        }
+        Row header = sheet.getRow(titleRow + 1);
+        if (header == null) {
+            throw new RuntimeException("銆屽叏鐪�" + targetMonth + "鏈堚�︽眹鎬昏〃銆嶆爣棰樺悗缂哄皯琛ㄥご琛岋紙甯傚窞/涓綋瀹㈣繍閲�/涓綋鏃呭鍛ㄨ浆閲忥級");
+        }
+        int cityCol = -1, passCol = -1, turnCol = -1;
+        for (Cell c : header) {
+            String h = FORMATTER.formatCellValue(c).trim();
+            if (h.isEmpty()) continue;
+            if (h.contains("甯傚窞")) cityCol = c.getColumnIndex();
+            else if (h.contains("涓綋") && h.contains("瀹㈣繍閲�")) passCol = c.getColumnIndex();
+            else if (h.contains("涓綋") && (h.contains("鍛ㄨ浆閲�") || h.contains("鍛ㄨ浆"))) turnCol = c.getColumnIndex();
+        }
+        if (cityCol < 0 || (passCol < 0 && turnCol < 0)) {
+            throw new RuntimeException("鏈壘鍒般�屽競宸� / 涓綋瀹㈣繍閲� / 涓綋鏃呭鍛ㄨ浆閲忋�嶅垪锛岃浣跨敤绯荤粺妯℃澘鏁寸悊鍚庡鍏�");
+        }
+        int lastRow = Math.min(sheet.getLastRowNum(), titleRow + 40);
+        for (int r = titleRow + 2; r <= lastRow; r++) {
+            Row row = sheet.getRow(r);
+            if (row == null) continue;
+            String first = getString(row, cityCol);
+            if (first == null) break;
+            String t = first.trim();
+            if (t.isEmpty() || t.contains("鍚堣") || t.contains("鐜瘮") || t.contains("鍏ㄧ渷")) break;
+            String city = RegionUtil.normalizeCityName(t);
+            if (!RegionUtil.cityList().contains(city)) continue;
+            Double pass = passCol >= 0 ? getDecimalOrNull(row, passCol) : null;
+            Double turn = turnCol >= 0 ? getDecimalOrNull(row, turnCol) : null;
+            if ((pass == null || pass == 0.0) && (turn == null || turn == 0.0)) continue;
+            PassengerIndividualMonthly entity = new PassengerIndividualMonthly();
+            entity.setReportPeriod(period);
+            entity.setRegionCode(RegionUtil.codeOfCity(city));
+            entity.setEnterpriseName(city);
+            entity.setPassengerCount(pass);
+            entity.setTurnover(turn);
+            rows.add(entity);
+        }
+    }
+
+    /** 鍏矾鏃呭杩愭斂杞﹁締鏁帮紙鍏煎涓ょ琛ㄥご锛氣憼 浼佷笟浠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 = 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 = getString(row, nameCol);
+                if (name == null || name.trim().isEmpty()) continue;
+                PassengerAuthVehicle auth = new PassengerAuthVehicle();
+                auth.setReportPeriod(period);
+                auth.setEnterpriseCode(codeCol == null ? null : getString(row, codeCol));
+                auth.setEnterpriseName(name.trim());
+                auth.setVehicleCount(getInt(row, vehicleCol));
+                auth.setSeatCount(getInt(row, seatCol));
+                authList.add(auth);
+            }
+        } catch (Exception e) {
+            log.error("Passenger auth parse error", e);
+            throw e;
+        }
+        // 骞傜瓑锛氬悓鎶ヨ〃鏈熼噸鏂板鍏ワ紝鍏堝垹闄ゆ棫鏁版嵁
+        passengerAuthMapper.delete(new LambdaQueryWrapper<PassengerAuthVehicle>()
+            .eq(PassengerAuthVehicle::getReportPeriod, period));
+        deleteRuleResults("H2031", period);
+        int success = 0;
+        List<String> failDetails = new ArrayList<>();
+        for (PassengerAuthVehicle auth : authList) {
+            try {
+                passengerAuthMapper.insert(auth);
+                success++;
+            } catch (Exception e) {
+                Throwable cause = e;
+                while (cause.getCause() != null) cause = cause.getCause();
+                String reason = cause.getMessage();
+                if (reason == null || reason.trim().isEmpty()) reason = e.getMessage();
+                failDetails.add("绗� " + (authList.indexOf(auth) + 2) + " 琛岋紙" + auth.getEnterpriseName() + "锛夛細" + reason);
+                log.error("Passenger auth insert error: {}", auth.getEnterpriseName(), e);
+            }
+        }
+        String errorDetail = String.join("\n", failDetails);
+        if (errorDetail.length() > 4000) errorDetail = errorDetail.substring(0, 4000) + "\n鈥︹��";
+        recordBatch(file.getOriginalFilename(), "PASSENGER_AUTH", period, authList.size(), success, failDetails.size(), errorDetail);
+        log.info("Passenger auth imported: {} rows for {}", success, period);
+        return new ImportResult(success, failDetails.size(), failDetails);
+    }
+
+    // ========== H204 閬撹矾璐ц繍杞﹁締鑳芥簮娑堣�楁儏鍐碉紙瀛f姤锛屽惈杩愭斂杞﹁締淇℃伅sheet锛� ==========
+
+    /** 鑳借�楁槑缁嗘瘡缁勫浐瀹�18鍒楋紝杞︾墝鍙疯捣濮嬪垪锛�0-based锛夛細3,21,39,57,75 */
+    private static final int[] ENERGY_GROUP_COLS = {3, 21, 39, 57, 75};
+
+    /** 鑳借�楁槑缁嗘ā鏉胯〃澶村叧閿垪锛�0-based锛夛細绋嬪簭鎸夊浐瀹氬垪浣嶈В鏋愶紝瑙f瀽鍓嶆牎楠岃〃澶达紝閿欎綅鐩存帴鎷掔粷 */
+    private static final int[] ENERGY_HEADER_COLS = {3, 4, 5, 21, 39, 57, 75};
+    private static final String[] ENERGY_HEADER_KEYWORDS = {"杞︾墝鍙�", "杞﹁締绫诲瀷", "鐕冩枡绫诲瀷", "杞︾墝鍙�", "杞︾墝鍙�", "杞︾墝鍙�", "杞︾墝鍙�"};
+
+    /** 鑳借�楀鍏ユ寜鎶ヨ〃鏈熶簰鏂ラ攣锛圚204 鏄庣粏涓庤繍鏀夸俊鎭叡鐢紝閬垮厤鍚屾姤琛ㄦ湡骞跺彂閲嶅浜掔浉瑕嗙洊锛� */
+    private Object energyLock(String period) {
+        return energyImportLocks.computeIfAbsent(period == null ? "" : period, k -> new Object());
+    }
+
+    public ImportResult importEnergyMonthly(MultipartFile file, String period) throws Exception {
+        List<EnergyRow> rows = new ArrayList<>();
+        List<EnergyAuthVehicle> authList = new ArrayList<>();
+        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+            // sheet0: 绗簩瀛e害璐ц溅鑳借�楁槑缁嗭紙姣忚宸﹀彸鍚�5缁勮溅杈嗭紝姣忕粍鍥哄畾18鍒楋級
+            Sheet sheet = wb.getSheetAt(0);
+            requireHeaderKeyword(sheet, "閬撹矾璐ц繍杞﹁締鑳芥簮娑堣�楁儏鍐�", "浼佷笟鍚嶇О", "杞︾墝鍙�");
+            requireEnergyHeaderLayout(sheet);
+            for (int r = 1; r <= sheet.getLastRowNum(); r++) {
+                Row row = sheet.getRow(r);
+                if (row == null) continue;
+                String enterpriseName = getString(row, 2);
+                if (enterpriseName == null || enterpriseName.trim().isEmpty()) continue;
+                for (int group : ENERGY_GROUP_COLS) {
+                    String plate = getString(row, group);
+                    if (plate == null || plate.trim().isEmpty() || "0".equals(plate.trim())) continue;
+                    EnergyVehicleQuarterly e = new EnergyVehicleQuarterly();
+                    e.setReportPeriod(period);
+                    e.setRegionCode(getString(row, 0));
+                    e.setEnterpriseCode(getString(row, 1));
+                    e.setEnterpriseName(enterpriseName.trim());
+                    e.setPlateNo(plate.trim());
+                    String vtc = getString(row, group + 1);
+                    e.setVehicleTypeCode(vtc);
+                    e.setVehicleType(vtc == null ? null : ENERGY_VEHICLE_TYPE_NAMES.getOrDefault(vtc, vtc));
+                    String ftc = getString(row, group + 2);
+                    e.setFuelTypeCode(ftc);
+                    e.setFuelType(ftc == null ? null : ENERGY_FUEL_TYPE_NAMES.getOrDefault(ftc, ftc));
+                    e.setManufactureYear(getIntOrNull(row, group + 3));
+                    e.setMarkedTonnage(getDecimalOrNull(row, group + 4));
+                    e.setTotalMileage(getDecimalOrNull(row, group + 5));
+                    e.setLoadedMileage(getDecimalOrNull(row, group + 6));
+                    e.setEmptyMileage(getDecimalOrNull(row, group + 7));
+                    e.setFreight(getDecimalOrNull(row, group + 8));
+                    e.setTurnover(getDecimalOrNull(row, group + 9));
+                    e.setTripCount(getIntOrNull(row, group + 10));
+                    String unit = getString(row, group + 11);
+                    e.setFuelUnit(unit == null ? null : ENERGY_FUEL_UNIT_NAMES.getOrDefault(unit, unit));
+                    e.setFuelConsumption(getDecimalOrNull(row, group + 12));
+                    e.setAvgDistance(getDecimalOrNull(row, group + 13));
+                    e.setAvgLoadedTonnage(getDecimalOrNull(row, group + 14));
+                    e.setAvgTripMileage(getDecimalOrNull(row, group + 15));
+                    e.setFuelPer100km(getDecimalOrNull(row, group + 16));
+                    e.setTurnoverFuel(getDecimalOrNull(row, group + 17));
+                    rows.add(new EnergyRow(r + 1, e));
+                }
+            }
+            // sheet1: 杩愭斂杞﹁締淇℃伅锛堝浐瀹�5鍒楋細杞︾墝鍙�/杞﹁締绫诲瀷/鐕冩枡绫诲瀷/鏍囪鍚ㄤ綅/鍑嗙壍寮曡川閲忥級
+            if (wb.getNumberOfSheets() > 1) {
+                Sheet authSheet = wb.getSheetAt(1);
+                for (int r = 1; r <= authSheet.getLastRowNum(); r++) {
+                    Row row = authSheet.getRow(r);
+                    if (row == null) continue;
+                    String plate = getString(row, 0);
+                    if (plate == null || plate.trim().isEmpty() || "0".equals(plate.trim())) continue;
+                    EnergyAuthVehicle auth = new EnergyAuthVehicle();
+                    auth.setReportPeriod(period);
+                    auth.setPlateNo(plate.trim());
+                    auth.setVehicleType(getString(row, 1));
+                    auth.setFuelType(getString(row, 2));
+                    auth.setMarkedTonnage(getDecimal(row, 3));
+                    auth.setTractionQuality(getDecimal(row, 4));
+                    authList.add(auth);
+                }
+            }
+        } catch (Exception e) {
+            log.error("Energy monthly parse error", e);
+            throw e;
+        }
+
+        // 骞傜瓑锛氬悓鎶ヨ〃鏈熼噸鏂板鍏ワ紝鍏堝垹闄ゆ棫鏁版嵁锛堜袱琛� + H204 瑙勫垯瀹℃牳缁撴灉锛�
+        // 鍔犻攣锛氬悓鎶ヨ〃鏈熷苟鍙戦噸瀵间簰鏂ワ紝闃叉 delete/insert 浜ら敊瀵艰嚧鏁版嵁娈嬬暀鎴栫炕鍊�
+        synchronized (energyLock(period)) {
+            energyMapper.delete(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
+                .eq(EnergyVehicleQuarterly::getReportPeriod, period));
+            energyAuthMapper.delete(new LambdaQueryWrapper<EnergyAuthVehicle>()
+                .eq(EnergyAuthVehicle::getReportPeriod, period));
+            List<Long> h204RuleIds = new ArrayList<>();
+            for (com.trafficaudit.rulemanage.entity.AuditRule rule : ruleMapper.selectList(null)) {
+                if ("H204".equals(rule.getReportType())) h204RuleIds.add(rule.getId());
+            }
+            if (!h204RuleIds.isEmpty()) {
+                auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
+                    .eq(AuditResult::getReportPeriod, period)
+                    .in(AuditResult::getRuleId, h204RuleIds));
+            }
+
+            int success = 0;
+            List<String> failDetails = new ArrayList<>();
+            for (EnergyRow item : rows) {
+                try {
+                    energyMapper.insert(item.entity);
+                    success++;
+                } catch (Exception e) {
+                    Throwable cause = e;
+                    while (cause.getCause() != null) cause = cause.getCause();
+                    String reason = cause.getMessage();
+                    if (reason == null || reason.trim().isEmpty()) reason = e.getMessage();
+                    failDetails.add("绗� " + item.excelRow + " 琛岋紙" + item.entity.getEnterpriseName()
+                        + " " + item.entity.getPlateNo() + "锛夛細" + reason);
+                    log.error("Energy insert error: row {}, {}", item.excelRow, item.entity.getPlateNo(), e);
+                }
+            }
+            String errorDetail = String.join("\n", failDetails);
+            if (errorDetail.length() > 4000) {
+                errorDetail = errorDetail.substring(0, 4000) + "\n鈥︹��";
+            }
+            recordBatch(file.getOriginalFilename(), "H204", period, rows.size(), success, failDetails.size(), errorDetail);
+
+            int authSuccess = 0;
+            for (EnergyAuthVehicle auth : authList) {
+                try {
+                    energyAuthMapper.insert(auth);
+                    authSuccess++;
+                } catch (Exception e) {
+                    log.error("energy auth insert error: {}", auth.getPlateNo(), e);
+                }
+            }
+            recordBatch(file.getOriginalFilename(), "ENERGY_AUTH", period, authList.size(), authSuccess, 0, null);
+            log.info("Energy monthly imported: {} vehicles, auth {} rows for {}", success, authSuccess, period);
+            return new ImportResult(success, failDetails.size(), failDetails);
+        }
+    }
+
+    /** 鑳借�楄溅杈嗚繍鏀夸俊鎭紙鐙珛妯℃澘锛氳溅鐗屽彿/杞﹁締绫诲瀷/鐕冩枡绫诲瀷/鏍囪鍚ㄤ綅/鍑嗙壍寮曡川閲忥級 */
+    public ImportResult importEnergyAuth(MultipartFile file, String period) throws Exception {
+        List<EnergyAuthVehicle> authList = new ArrayList<>();
+        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+            Sheet sheet = wb.getSheetAt(0);
+            requireHeaderKeyword(sheet, "鑳借�楄溅杈嗚繍鏀夸俊鎭�", "杞︾墝鍙�");
+            Row authHeader = sheet.getRow(0);
+            String authH0 = authHeader == null ? null : FORMATTER.formatCellValue(authHeader.getCell(0)).trim();
+            if (authH0 == null || authH0.isEmpty() || !authH0.contains("杞︾墝鍙�")) {
+                throw new RuntimeException("鑳借�楄溅杈嗚繍鏀夸俊鎭〃澶翠笌绯荤粺妯℃澘涓嶄竴鑷达細绗� 1 鍒楀簲涓恒�岃溅鐗屽彿銆嶏紝瀹為檯涓恒��"
+                    + (authH0 == null || authH0.isEmpty() ? "绌�" : authH0) + "銆嶃�傝浣跨敤绯荤粺妯℃澘銆婃ā鏉縚鑳借�楄溅杈嗚繍鏀夸俊鎭�.xlsx銆嬫暣鐞嗗悗鍐嶅鍏�");
+            }
+            for (int r = 1; r <= sheet.getLastRowNum(); r++) {
+                Row row = sheet.getRow(r);
+                if (row == null) continue;
+                String plate = getString(row, 0);
+                if (plate == null || plate.trim().isEmpty() || "0".equals(plate.trim())) continue;
+                EnergyAuthVehicle auth = new EnergyAuthVehicle();
+                auth.setReportPeriod(period);
+                auth.setPlateNo(plate.trim());
+                auth.setVehicleType(getString(row, 1));
+                auth.setFuelType(getString(row, 2));
+                auth.setMarkedTonnage(getDecimal(row, 3));
+                auth.setTractionQuality(getDecimal(row, 4));
+                authList.add(auth);
+            }
+        } catch (Exception e) {
+            log.error("Energy auth parse error", e);
+            throw e;
+        }
+        // 骞傜瓑锛氬悓鎶ヨ〃鏈熼噸鏂板鍏ワ紝鍏堝垹闄ゆ棫鏁版嵁锛涗笌 H204 鏄庣粏鍏辩敤鎶ヨ〃鏈熼攣锛岄槻骞跺彂浜掔浉瑕嗙洊
+        synchronized (energyLock(period)) {
+            energyAuthMapper.delete(new LambdaQueryWrapper<EnergyAuthVehicle>()
+                .eq(EnergyAuthVehicle::getReportPeriod, period));
+            deleteRuleResults("H204", period);
+            int success = 0;
+            List<String> failDetails = new ArrayList<>();
+            for (EnergyAuthVehicle auth : authList) {
+                try {
+                    energyAuthMapper.insert(auth);
+                    success++;
+                } catch (Exception e) {
+                    Throwable cause = e;
+                    while (cause.getCause() != null) cause = cause.getCause();
+                    String reason = cause.getMessage();
+                    if (reason == null || reason.trim().isEmpty()) reason = e.getMessage();
+                    failDetails.add("绗� " + (authList.indexOf(auth) + 2) + " 琛岋紙" + auth.getPlateNo() + "锛夛細" + reason);
+                    log.error("Energy auth insert error: {}", auth.getPlateNo(), e);
+                }
+            }
+            String errorDetail = String.join("\n", failDetails);
+            if (errorDetail.length() > 4000) errorDetail = errorDetail.substring(0, 4000) + "\n鈥︹��";
+            recordBatch(file.getOriginalFilename(), "ENERGY_AUTH", period, authList.size(), success, failDetails.size(), errorDetail);
+            log.info("Energy auth imported: {} rows for {}", success, period);
+            return new ImportResult(success, failDetails.size(), failDetails);
+        }
+    }
+
+    /** H204 琛岃褰曪細淇濈暀 Excel 琛屽彿鐢ㄤ簬澶辫触瀹氫綅 */
+    private static class EnergyRow {
+        final int excelRow;
+        final EnergyVehicleQuarterly entity;
+
+        EnergyRow(int excelRow, EnergyVehicleQuarterly entity) {
+            this.excelRow = excelRow;
+            this.entity = entity;
+        }
+    }
+
+    /** 绌哄崟鍏冩牸杩斿洖 null锛堢敤浜庤兘鑰楁槑缁嗘湭濉啓椤逛笌 0 鍖哄垎锛� */
+    private Integer getIntOrNull(Row row, int idx) {
+        Double val = getDecimalOrNull(row, idx);
+        return val == null ? null : val.intValue();
+    }
+
     // ========== 杩愭斂杞﹁締 ==========
 
+    /** 杩愭斂杞﹁締瀵煎叆锛氫笌 H203-2 鏈堟姤涓�鑷达紝鎸夎〃澶村悕绉板畾浣嶅垪锛屽垪椤哄簭鍙樺寲涔熻兘姝g‘瀵煎叆 */
     public int importTransportAuth(MultipartFile file, String period) throws Exception {
         List<TransportAuthVehicle> list = new ArrayList<>();
         int failRows = 0;
         try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
             Sheet sheet = wb.getSheetAt(0);
+            requireHeaderCols(sheet, "杩愭斂杞﹁締", "浼佷笟鍚嶇О", "鎬荤壍寮曡溅鏁�", "鎬绘寕杞︽暟", "鍏跺畠杞﹁締鏁�", "鎸傝溅鎬诲惃浣�(鍚�)", "鍏跺畠杞﹁締鎬诲惃浣�(鍚�)");
+            Map<String, Integer> colMap = buildHeaderMap(sheet.getRow(0));
             for (int r = 1; r <= sheet.getLastRowNum(); r++) {
                 Row row = sheet.getRow(r);
                 if (row == null) continue;
-                String name = getString(row, 0);
+                String name = gs(row, colMap, "浼佷笟鍚嶇О");
                 if (name == null || name.trim().isEmpty()) continue;
                 TransportAuthVehicle entity = new TransportAuthVehicle();
                 entity.setReportPeriod(period);
                 entity.setEnterpriseName(name);
-                entity.setTractorCount(getInt(row, 1));
-                entity.setTrailerCount(getInt(row, 2));
-                entity.setOtherCount(getInt(row, 3));
-                entity.setTrailerTons(getDecimal(row, 4));
-                entity.setOtherTons(getDecimal(row, 5));
+                entity.setTractorCount(gi(row, colMap, "鎬荤壍寮曡溅鏁�"));
+                entity.setTrailerCount(gi(row, colMap, "鎬绘寕杞︽暟"));
+                entity.setOtherCount(gi(row, colMap, "鍏跺畠杞﹁締鏁�"));
+                entity.setTrailerTons(gd(row, colMap, "鎸傝溅鎬诲惃浣�(鍚�)"));
+                entity.setOtherTons(gd(row, colMap, "鍏跺畠杞﹁締鎬诲惃浣�(鍚�)"));
                 list.add(entity);
             }
         } catch (Exception e) {
@@ -220,20 +896,23 @@
 
     // ========== 杞ㄨ抗閲岀▼ ==========
 
+    /** 杞ㄨ抗閲岀▼瀵煎叆锛氫笌 H203-2 鏈堟姤涓�鑷达紝鎸夎〃澶村悕绉板畾浣嶅垪锛屽垪椤哄簭鍙樺寲涔熻兘姝g‘瀵煎叆 */
     public int importTrackMileage(MultipartFile file, String period) throws Exception {
         List<VehicleTrackMileage> list = new ArrayList<>();
         int failRows = 0;
         try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
             Sheet sheet = wb.getSheetAt(0);
+            requireHeaderCols(sheet, "杞ㄨ抗閲岀▼", "浼佷笟鍚嶇О", "鏈堝害鎬婚噷绋�(鍏噷)", "鏈夐噷绋嬬殑杞﹁締鏁�");
+            Map<String, Integer> colMap = buildHeaderMap(sheet.getRow(0));
             for (int r = 1; r <= sheet.getLastRowNum(); r++) {
                 Row row = sheet.getRow(r);
                 if (row == null) continue;
-                String name = getString(row, 1);
+                String name = gs(row, colMap, "浼佷笟鍚嶇О");
                 if (name == null || name.trim().isEmpty()) continue;
                 VehicleTrackMileage entity = new VehicleTrackMileage();
                 entity.setEnterpriseName(name);
-                entity.setMonthlyMileage(getDecimal(row, 2));
-                entity.setTrackedVehicles(getInt(row, 3));
+                entity.setMonthlyMileage(gd(row, colMap, "鏈堝害鎬婚噷绋�(鍏噷)"));
+                entity.setTrackedVehicles(gi(row, colMap, "鏈夐噷绋嬬殑杞﹁締鏁�"));
                 entity.setReportPeriod(period);
                 list.add(entity);
             }
@@ -268,13 +947,44 @@
         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("鏈堜唤") || sn.contains("绱"))) { hasPeriodSheet = true; break; }
+            }
+            if (!hasPeriodSheet) {
+                throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨�愯涓婅涓嬫媶鍒嗐�戜笌鏂囦欢鍐呭涓嶇锛氭湭鎵惧埌鍚� 褰撴湀/绱 鐨� sheet锛岃纭鏄惁閫夐敊浜嗘暟鎹被鍨�");
+            }
             for (int i = 0; i < wb.getNumberOfSheets(); i++) {
                 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琛屽瓙琛ㄥご搴斿惈銆屽懆杞噺銆嶆垨銆岃揣杩愰噺銆嶏紙鎵╁睍甯冨眬锛�
+                Row headRow = sheet.getRow(1);
+                Row subHeadRow = sheet.getRow(2);
+                boolean layoutOk = headRow != null
+                    && getString(headRow, 0) != null && getString(headRow, 0).contains("甯傚窞")
+                    && getString(headRow, 1) != null && getString(headRow, 1).contains("瑙勪笂")
+                    && subHeadRow != null
+                    && ("璐ц繍閲�".equals(getString(subHeadRow, 0))
+                        || (getString(subHeadRow, 1) != null && getString(subHeadRow, 1).contains("鍛ㄨ浆閲�")));
+                if (!layoutOk) {
+                    throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨�愯涓婅涓嬫媶鍒嗐�戜笌鏂囦欢鍐呭涓嶇锛歴heet銆�" + sheetName
+                        + "銆嶆湭鎵惧埌銆屽競宸�/瑙勪笂浼佷笟杩愯緭閲�/鍛ㄨ浆閲忋�嶈〃澶达紝璇风‘璁ゆ槸鍚﹂�夐敊浜嗘暟鎹被鍨�");
+                }
 
                 // 妫�娴嬪瓙琛ㄥご锛氭槸鍚﹀寘鍚揣杩愰噺鍒楋紙鎵╁睍甯冨眬锛�
                 Row subHeader = sheet.getRow(2);
@@ -352,12 +1062,41 @@
     // ========== 璐ц繍閲忓懆杞噺锛堟ā鏉縚璐ц繍閲忓懆杞噺.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);
+            String h1 = header == null ? null : getString(header, 1);
+            if (header == null || h0 == null || !h0.contains("璐ц繍閲�") || h1 == null || !h1.contains("鏈�")) {
+                throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨�愯揣杩愰噺鍛ㄨ浆閲忋�戜笌鏂囦欢鍐呭涓嶇锛氳〃澶村簲涓恒�岃揣杩愰噺(涓囧惃)/1鏈堚��12鏈堛�嶅竷灞�锛岃纭鏄惁閫夐敊浜嗘暟鎹被鍨�");
+            }
+            boolean cityRowFound = false;
+            for (int r = 1; r <= Math.min(sheet.getLastRowNum(), 30); r++) {
+                Row rr = sheet.getRow(r);
+                if (rr == null) continue;
+                String nm = getString(rr, 0);
+                if (nm == null || nm.trim().isEmpty()) continue;
+                String region = RegionUtil.normalizeCityName(nm);
+                if (RegionUtil.cityList().contains(region) || "婀栧寳鐪�".equals(region)) { cityRowFound = true; break; }
+            }
+            if (!cityRowFound) {
+                throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨�愯揣杩愰噺鍛ㄨ浆閲忋�戜笌鏂囦欢鍐呭涓嶇锛氭湭璇嗗埆鍒板競宸炶锛堥鍒楀簲涓哄競宸炲悕绉帮級锛岃纭鏄惁閫夐敊浜嗘暟鎹被鍨�");
+            }
             Map<String, FreightTurnoverImport> freightMap = new HashMap<>();
             Map<String, FreightTurnoverImport> turnoverMap = new HashMap<>();
             Set<String> seen = new HashSet<>();
@@ -403,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 鏈� */
@@ -601,6 +1462,352 @@
         }
     }
 
+    /** 鏀寔鈥滄寜绫诲瀷娓呯┖鈥濈殑鏁版嵁绫诲瀷 -> 鏁版嵁琛ㄦ搷浣滈敭锛堝悓琛ㄥ悓绫诲埆褰掑苟锛岄伩鍏嶉噸澶嶅垹闄わ級 */
+    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<>();
+        for (com.trafficaudit.rulemanage.entity.AuditRule rule : ruleMapper.selectList(null)) {
+            if (reportType.equals(rule.getReportType())) ruleIds.add(rule.getId());
+        }
+        if (!ruleIds.isEmpty()) {
+            auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
+                .eq(AuditResult::getReportPeriod, period)
+                .in(AuditResult::getRuleId, ruleIds));
+        }
+    }
+
+    /** 绫诲瀷-琛ㄥご鏍¢獙锛氭寜蹇呴渶鍒楀悕锛堣В鏋愬櫒瀹為檯渚濊禆鐨勫垪锛夛紝缂哄け鍗虫姏"绫诲瀷涓庢枃浠朵笉绗�" */
+    private void requireHeaderCols(Sheet sheet, String typeLabel, String... cols) {
+        Map<String, Integer> colMap = buildHeaderMap(sheet.getRow(0));
+        List<String> missing = new ArrayList<>();
+        for (String c : cols) {
+            if (!colMap.containsKey(c)) missing.add(c);
+        }
+        if (!missing.isEmpty()) {
+            throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨��" + typeLabel + "銆戜笌鏂囦欢鍐呭涓嶇锛氳〃澶寸己灏戝繀闇�鍒椼��" + String.join("銆戙��", missing) + "銆戯紝璇风‘璁ゆ槸鍚﹂�夐敊浜嗘暟鎹被鍨�");
+        }
+    }
+
+    /** 绫诲瀷-琛ㄥご鏍¢獙锛氭壂鎻忚〃澶磋锛屽懡涓换涓�鍏抽敭瀛楀嵆閫氳繃 */
+    private void requireHeaderKeyword(Sheet sheet, String typeLabel, String... keywords) {
+        Row header = sheet == null ? null : sheet.getRow(0);
+        if (header != null) {
+            for (Cell cell : header) {
+                String v = FORMATTER.formatCellValue(cell);
+                if (v == null || v.isEmpty()) continue;
+                for (String k : keywords) {
+                    if (v.contains(k)) return;
+                }
+            }
+        }
+        throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨��" + typeLabel + "銆戜笌鏂囦欢鍐呭涓嶇锛氳〃澶存湭鍖呭惈銆�" + String.join("銆戙��", keywords) + "銆戠瓑鍒楋紝璇风‘璁ゆ槸鍚﹂�夐敊浜嗘暟鎹被鍨�");
+    }
+
+    /** 鑳借�楁槑缁嗚〃澶村垪浣嶆牎楠岋細绯荤粺妯℃澘涓衡�滃墠3鍒� + 5缁劽�18鍒椻�濓紝绋嬪簭鎸夊浐瀹氬垪浣嶈В鏋愶紝琛ㄥご閿欎綅浼氬鑷存暣琛ㄩ敊涔憋紝閿欎綅鏃剁洿鎺ユ嫆缁� */
+    private void requireEnergyHeaderLayout(Sheet sheet) {
+        Row header = sheet == null ? null : sheet.getRow(0);
+        if (header == null) {
+            throw new RuntimeException("鑳借�楁槑缁嗚〃澶寸己澶憋紝璇蜂娇鐢ㄧ郴缁熸ā鏉裤�婃ā鏉縚閬撹矾璐ц繍杞﹁締鑳芥簮娑堣�楁儏鍐碉紙2026锛�.xlsx銆嬫暣鐞嗗悗鍐嶅鍏�");
+        }
+        for (int i = 0; i < ENERGY_HEADER_COLS.length; i++) {
+            int col = ENERGY_HEADER_COLS[i];
+            String keyword = ENERGY_HEADER_KEYWORDS[i];
+            Cell cell = header.getCell(col);
+            String hv = cell == null ? null : FORMATTER.formatCellValue(cell).trim();
+            if (hv == null || hv.isEmpty() || !hv.contains(keyword)) {
+                throw new RuntimeException("琛ㄥご涓庣郴缁熸ā鏉夸笉涓�鑷达細绗� " + (col + 1) + " 鍒楀簲涓恒��" + keyword + "銆嶏紝瀹為檯涓恒��"
+                    + (hv == null || hv.isEmpty() ? "绌�" : hv) + "銆嶃�傝浣跨敤绯荤粺妯℃澘銆婃ā鏉縚閬撹矾璐ц繍杞﹁締鑳芥簮娑堣�楁儏鍐碉紙2026锛�.xlsx銆嬫暣鐞嗗悗鍐嶅鍏�");
+            }
+        }
+    }
+
+    /** 閫氱敤琛ㄥご鍒椾綅鏍¢獙锛氱▼搴忔寜鍥哄畾鍒椾綅瑙f瀽锛岃〃澶撮敊浣嶄細瀵艰嚧鏁磋〃閿欎贡锛岄敊浣嶆椂鐩存帴鎷掔粷瀵煎叆 */
+    private void requireHeaderLayout(Sheet sheet, String typeLabel, String templateHint,
+                                     int[] cols, String[] keywords) {
+        Row header = sheet == null ? null : sheet.getRow(0);
+        if (header == null) {
+            throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨��" + typeLabel + "銆戞枃浠惰〃澶寸己澶憋紝璇蜂娇鐢ㄧ郴缁熸ā鏉裤��" + templateHint + "銆嬫暣鐞嗗悗鍐嶅鍏�");
+        }
+        for (int i = 0; i < cols.length; i++) {
+            int col = cols[i];
+            String keyword = keywords[i];
+            Cell cell = header.getCell(col);
+            String hv = cell == null ? null : FORMATTER.formatCellValue(cell).trim();
+            if (hv == null || hv.isEmpty() || !hv.contains(keyword)) {
+                throw new RuntimeException("琛ㄥご涓庣郴缁熸ā鏉夸笉涓�鑷达細绗� " + (col + 1) + " 鍒楀簲涓恒��" + keyword + "銆嶏紝瀹為檯涓恒��"
+                    + (hv == null || hv.isEmpty() ? "绌�" : hv) + "銆嶃�傛墍閫夋暟鎹被鍨嬨��" + typeLabel + "銆戣浣跨敤绯荤粺妯℃澘銆�" + templateHint + "銆嬫暣鐞嗗悗鍐嶅鍏�");
+            }
+        }
+    }
+
     /** 鎸夎〃澶村悕绉板缓绔嬪垪鍙锋槧灏勶紝鍏煎鏂版棫妯℃澘鍒椾綅宸紓 */
     private Map<String, Integer> buildHeaderMap(Row header) {
         Map<String, Integer> colMap = new HashMap<>();
@@ -612,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) {
@@ -662,10 +1896,26 @@
         }
     }
 
+    private static class PassengerRow {
+        int excelRow;
+        PassengerEnterpriseMonthly entity;
+
+        PassengerRow(int excelRow, PassengerEnterpriseMonthly entity) {
+            this.excelRow = excelRow;
+            this.entity = entity;
+        }
+    }
+
 
     private String getString(Row row, int idx) {
         Cell cell = row.getCell(idx);
         if (cell == null) return null;
+        // 鍏紡鍗曞厓鏍硷細璇荤紦瀛樺瓧绗︿覆锛�.et/WPS 鏂囦欢甯歌锛孌ataFormatter 鏃� evaluator 鏃跺彧杩斿洖鍏紡鏂囨湰锛�
+        if (cell.getCellType() == CellType.FORMULA && cell.getCachedFormulaResultType() == CellType.STRING) {
+            String cached = cell.getStringCellValue();
+            cached = cached == null ? null : cached.trim();
+            return cached == null || cached.isEmpty() ? null : cached;
+        }
         String val = FORMATTER.formatCellValue(cell).trim();
         return val.isEmpty() ? null : val;
     }
@@ -675,6 +1925,10 @@
         if (cell == null) return 0.0;
         // 鏁板�煎崟鍏冩牸鐩存帴鍙栧師濮嬪�硷紝閬垮厤琚� Excel 鏄剧ず鏍煎紡锛堢櫨鍒嗘瘮/0_ 绛夛級褰卞搷瑙f瀽
         if (cell.getCellType() == CellType.NUMERIC) {
+            return cell.getNumericCellValue();
+        }
+        // 鍏紡鍗曞厓鏍硷細璇荤紦瀛樻暟鍊硷紙.et/WPS 璺ㄨ〃鍏紡甯歌锛孌ataFormatter 鏃� evaluator 鏃跺彧杩斿洖鍏紡鏂囨湰锛�
+        if (cell.getCellType() == CellType.FORMULA && cell.getCachedFormulaResultType() == CellType.NUMERIC) {
             return cell.getNumericCellValue();
         }
         String val = FORMATTER.formatCellValue(cell).trim();
@@ -699,6 +1953,9 @@
         if (cell.getCellType() == CellType.NUMERIC) {
             return cell.getNumericCellValue();
         }
+        if (cell.getCellType() == CellType.FORMULA && cell.getCachedFormulaResultType() == CellType.NUMERIC) {
+            return cell.getNumericCellValue();
+        }
         String val = FORMATTER.formatCellValue(cell).trim();
         if (val.isEmpty()) return null;
         try {
@@ -707,4 +1964,2094 @@
             return null;
         }
     }
+    // ========== 鎶曡祫妯″潡锛堝杩愮珯鍦� / 鐗╂祦鍥尯锛屽叏鐪佹眹鎬诲ぇ琛級 ==========
+
+    /** 浜垮厓鎶曡祫椤圭洰鍥哄畾鍚嶅崟锛�2026骞磋涓婇」鐩〃锛� */
+    private static final java.util.Set<String> INVEST_BILLION_NAMES = new LinkedHashSet<>(Arrays.asList(
+        "姹夊彛瀹㈣繍涓績", "閽熺ゥ甯傜患鍚堜氦閫氬杩愭灑绾戒竴鏈熼」鐩�", "鑸彂鏅烘収鐗╂祦鍥�", "鍥借儨鍏矾娓�",
+        "鍗佸牥鐢熶骇鏈嶅姟鍨嬪浗瀹剁墿娴佹灑绾藉伐绋嬮」鐩�", "绉綊鍘胯剱姗欎骇涓氱患鍚堢墿娴佷腑蹇�", "瀹滈兘渚涢攢鍟嗚锤鐗╂祦鍥�",
+        "闀块槼鍘垮煄涔″喎閾剧墿娴佸洯", "瀹滄槍娓叴灞辨腐鍖虹鍏按澶氬紡鑱旇繍寤鸿椤圭洰", "鐜夋箹鍐烽摼锛堣闃筹級浜ゆ槗涓績椤圭洰",
+        "姝︽眽涓囧惃鍗庝腑鍐烽摼娓簩鏈熼」鐩�", "婀栧寳闀挎睙鐜颁唬鐗╂祦浜т笟闆嗚仛绀鸿寖鍖郝风幇浠g墿娴佸洯",
+        "閯傚窞鑺辨箹鍥介檯鏈哄満绌烘腐鍨嬪浗瀹剁墿娴佹灑绾芥櫤鎱у叕鍏卞浗闄呰揣绔欓」鐩�", "鑽嗛棬鏅烘収鍐烽摼鐗╂祦鍥�",
+        "鑽嗛棬鍖楃珯椤烘磱鐗╂祦鍩哄湴锛堣崋闂ㄥ寳瀛愰櫟閾佽矾鐗╂祦鍥級", "娌欐磱澶氬紡鑱旇繍鐜颁唬鐗╂祦鍥竴鏈燂紙娌欐磱鐓ょ偔鍌ㄥ鈥㈤泦瑁呯鏁f敼闆嗗熀鍦帮級",
+        "棣栬 鍩庢櫤鎱у喎閾剧墿娴佷簩鏈熼」鐩瓽鍖�", "瀛濇槍骞挎禂鏅烘収鐗╂祦鐢靛晢浜т笟鍥�", "鐢抽�氾紙瀛濇劅锛夋櫤鎱х墿娴佺數鍟嗕骇涓氬洯锛堜笁鏈燂級",
+        "鍏畨鍘垮寮忚仈杩愮患鍚堢墿娴佸洯", "鑽嗗窞甯傞搧姘磋仈杩愮墿娴佷腑蹇�", "鑽嗗窞甯傜憺娴峰浗闄呯墿娴佸洯涓�鏈�", "鑽嗗窞缁忓紑鍖烘櫤鑳戒粨鍌ㄧ墿娴佸洯椤圭洰",
+        "閯備笢锛堥粍鍐堬級鏁版櫤鐗╂祦缁煎悎鏈嶅姟涓績",
+        "閯傛箻璧e晢璐哥墿娴佷腑蹇冧竴鏈燂紙閫氬煄鍘跨墿娴侀厤閫佷腑蹇冦�侀剛鍗楁櫤鎱х墿娴佷骇涓氬洯寤鸿銆侀剛鍗楁櫤鎱х墿娴佷骇涓氬洯寤鸿椤圭洰锛堜簩鏈燂級銆佸喎閾鹃厤閫佷腑蹇冿級椤圭洰",
+        "鍜稿畞鍥介檯闄嗘腐鐗╂祦鍥�-鍜稿畞甯傚浗闄呴檰娓患鍚堢墿娴佸洯锛堜竴鏈燂級", "鍜稿畞鍥介檯闄嗘腐鐗╂祦鍥�-鏂板缓鍜稿畞閾佽矾鐗╂祦鍩哄湴椤圭洰",
+        "宕囬槼鍘跨患鍚堢墿娴佸洯鍙婇厤濂楄鏂藉缓璁鹃」鐩�", "鎭╂柦涓冮噷鍧墿娴佸洯", "鑱屼笟宸ヨ浜т笟闆嗚仛鍖轰粨鍌ㄧ墿娴侀」鐩�",
+        "鏋佸厰閫熼�掑崕涓墿娴佹灑绾藉熀鍦伴」鐩�", "婀栧寳杩堢澘杈句緵搴旈摼鎬婚儴鐮斿彂鍙婄敓浜у熀鍦伴」鐩�", "涓滄鑸┖鍐烽摼鐗╂祦涓績",
+        "涓夊场鏅烘収鑸┖鐗╂祦浜т笟鍥�", "姹夋睙瑗勯槼锛堝皬娌筹級娓寮忚仈杩愮墿娴佸洯", "瀹忚寕鍏ㄧ悆閫氳繍椤圭洰", "姝︽眽澶╂渤缁煎悎瀹㈣繍鏋㈢航",
+        "骞挎按甯傛潹瀹跺鍏搧鐗╂祦鍥」鐩�---骞挎按甯傛潹瀵ㄩ搧璺揣鍦猴紙骞挎按甯傚崕閼喍閲戠墿娴佽揣鍦猴級", "闃虫柊缁煎悎瀹㈣繍鏋㈢航绔�",
+        "褰撻槼甯傞珮閾佺患鍚堝杩愮珯", "瀹滄槍鍖楀杩愮珯", "鍗楁汲鍘垮叕閾佹崲涔樹腑蹇�(閮戜竾楂橀搧鍗楁汲缁煎悎瀹㈣繍鏋㈢航)", "浜北鍗楀叕閾佺患鍚堝杩愭灑绾�",
+        "娌挎睙楂橀搧姹夊窛鍖楃珯绔欏墠骞垮満(缁煎悎瀹㈣繍鏋㈢航)", "姝︽澀楂橀搧榛勫唸瑗跨患鍚堝杩愭灑绾�", "榛勯粍楂橀搧娴犳按缁煎悎瀹㈣繍鏋㈢航",
+        "閮戜竾楂橀搧宸翠笢缁煎悎瀹㈣繍鏋㈢航", "涓�氫緵搴旈摼婀栧寳绠$悊涓績椤圭洰", "闃抽�绘腐閾佹按鑱旇繍浜屾湡涓鐝垪闆嗙粨涓績",
+        "瑗块┈鍥介檯锛堥粍鐭筹級鏅烘収鐗╂祦鍥�", "鍗佸牥甯傚煄鍙戜粨鍌ㄧ墿娴佷腑蹇�", "鏋濇睙甯傞【瀹跺簵浠撳偍鐗╂祦涓績锛堜竴鏈燂級", "涓夊场寤烘潗鐗╂祦鍥�",
+        "瀹滈兘甯傞檰鍩庣患鍚堢墿娴佽繍杈撳粖閬�", "瀹滄槍浼犲寲鍏矾娓簩鏈�", "姝︽眽娓埅瑗勯槼鏅烘収鐗╂祦鍥�", "璋峰煄鍘垮煄涔℃櫤鎱т粨鍌ㄧ墿娴佷骇涓氬洯",
+        "涔濆窞閫氭矁鐢板浗闄呬緵搴旈摼涓績椤圭洰", "婀栧寳鎭掑嘲鍥介檯鐗╂祦鍥�", "婀栧寳涓潚渚涘簲閾炬湁闄愬叕鍙稿寘瑁呴厤濂椼�佷粨鍌ㄨ繍杈撻」鐩�",
+        "澶ф偀鍘跨數鍟嗙墿娴佷骇涓氬洯", "瀹夐檰甯傚煄涔″喎閾剧墿娴侀厤閫佷腑蹇�", "澶у埆灞辨櫤鎱х墿娴佷骇涓氬洯", "姝︾┐甯備緵閿�绀鹃泦閲囬泦閰嶇患鍚堟湇鍔′腑蹇冨伐绋�",
+        "缃楃敯鍘跨患鍚堢墿娴佸洯", "榛戣惫鍥介檯鐗╂祦鍥紙榛勫唸缁煎悎鍥尯锛�", "楹诲煄甯傚啘浜у搧缁煎悎鐗╂祦鍥�", "宸翠笢缁忔祹寮�鍙戝尯闈掗緳妗ユ櫤鎱х墿娴佷骇涓氬洯",
+        "楣ゅ嘲姝﹂櫟灞变粨鍌ㄧ墿娴佸洯", "鍜镐赴鍘跨患鍚堢墿娴佸洯", "瀹f仼鍘垮瘎閫掔墿娴佸洯", "鏉ュ嚖鍘块緳鍑ょ墿娴佸洯浜ら�氱墿娴佷腑蹇�",
+        "浠欐鍥藉楂樻柊鍖虹墿娴佹灑绾戒腑蹇冨缓璁鹃」鐩�", "澶╅棬甯傜墿娴佷腑蹇冨缓璁鹃」鐩�"));
+
+    /** 缁忔祹寮哄幙椤圭洰鍥哄畾鍚嶅崟 */
+    private static final java.util.Set<String> INVEST_COUNTY_NAMES = new LinkedHashSet<>(Arrays.asList(
+        "瑗块┈鍥介檯锛堥粍鐭筹級鏅烘収鐗╂祦鍥�", "榛勭煶澶у喍婀栭珮鏂板尯鏅烘収鍐烽摼鐗╂祦鍥�", "澶у喍婀栭珮鏂版櫤鎱х墿娴佹腐椤圭洰锛堝煄瑗垮寳渚涘簲閾炬湇鍔′腑蹇冿級",
+        "绉綊鍘胯剱姗欎骇涓氱患鍚堢墿娴佷腑蹇�", "瀹滈兘渚涢攢鍟嗚锤鐗╂祦鍥�", "闀块槼鍘垮煄涔″喎閾剧墿娴佸洯鍙婂叾閰嶅璁炬柦寤鸿椤圭洰",
+        "瀹滄槍娓叴灞辨腐鍖虹鍏按澶氬紡鑱旇繍寤鸿椤圭洰", "鏋濇睙甯傞【瀹跺簵浠撳偍鐗╂祦涓績锛堜竴鏈燂級", "瀹滈兘甯傞檰鍩庣患鍚堢墿娴佽繍杈撳粖閬�",
+        "鑰佹渤鍙e競鍐滀骇鍝佷粨鍌ㄧ墿娴佷氦鏄撲腑蹇冨缓璁鹃」鐩�", "璋峰煄鍘垮煄涔℃櫤鎱т粨鍌ㄧ墿娴佷骇涓氬洯", "娌欐磱澶氬紡鑱旇繍鐜颁唬鐗╂祦鍥�",
+        "澶ф偀鍘跨數鍟嗙墿娴佷骇涓氬洯", "搴斿煄甯傛櫤鎱у喎閾句粨鍌ㄧ墿娴佸洯", "瀛濇槍骞挎禂鏅烘収鐗╂祦鐢靛晢浜т笟鍥缓璁鹃」鐩�", "瀹夐檰甯傚煄涔″喎閾剧墿娴侀厤閫佷腑蹇�",
+        "鏉炬粙甯傛槦缁滅墿娴佸洯", "鍏畨鍘垮寮忚仈杩愮患鍚堢墿娴佸洯", "涓浗渚涢攢鍏畨鍟嗚锤鐗╂祦鍥紙鍏畨鍘垮煙闆嗛噰闆嗛厤涓績锛�", "鍚屾床鏅烘収鐗╂祦鍥�",
+        "澶у埆灞辨櫤鎱х墿娴佷骇涓氬洯", "姝︾┐甯備緵閿�绀鹃泦閲囬泦閰嶇患鍚堟湇鍔′腑蹇冨伐绋�", "缃楃敯鍘跨患鍚堢墿娴佸洯", "閯備笢锛堥粍鍐堬級鏁版櫤鐗╂祦缁煎悎鏈嶅姟涓績",
+        "楹诲煄甯傚啘浜у搧缁煎悎鐗╂祦鍥�", "閯傛箻璧e晢璐哥墿娴佷腑蹇冿紙涓�鏈燂級", "鍢夐奔鍘挎櫤鎱т氦閫氱墿娴佹灑绾戒腑蹇�",
+        "宕囬槼鍘垮浗瀹剁骇缁煎悎鐗╂祦鏋㈢航鍙婇厤濂楄鏂藉缓璁鹃」鐩�", "搴峰崕鏅烘収鐗╂祦鍥�", "缁煎悎鐗╂祦鍥紙婀栧寳浜ゆ姇闅忓窞鏅烘収渚涘簲閾句骇涓氬洯锛�",
+        "宸翠笢缁忔祹寮�鍙戝尯闈掗緳妗ユ櫤鎱х墿娴佷骇涓氬洯", "鎭╂柦甯備竷閲屽潽鐗╂祦鍥�", "楣ゅ嘲姝﹂櫟灞变粨鍌ㄧ墿娴佸洯", "鍜镐赴鍘跨患鍚堢墿娴佸洯",
+        "鏉ュ嚖鍘块緳鍑ょ墿娴佸洯浜ら�氱墿娴佷腑蹇�", "瀹f仼鍘垮瘎閫掔墿娴佸洯", "浠欐鍥藉楂樻柊鍖虹墿娴佹灑绾戒腑蹇冨缓璁鹃」鐩�",
+        "鑱屼笟宸ヨ浜т笟闆嗚仛鍖轰粨鍌ㄧ墿娴侀」鐩�", "涓浗铏捐胺鍐烽摼浠撳偍鐗╂祦鍥�", "澶╅棬甯傜墿娴佷腑蹇冨缓璁鹃」鐩�", "绁炲啘鏋剁墿娴佸洯"));
+
+    /** "鍗佷簲浜�"閲嶇偣鐗╂祦椤圭洰鍥哄畾鍚嶅崟 */
+    private static final java.util.Set<String> INVEST_FIVE_YEAR_NAMES = new LinkedHashSet<>(Arrays.asList(
+        "鑸彂鏅烘収鐗╂祦鍥�", "涓�氫緵搴旈摼婀栧寳绠$悊涓績椤圭洰", "闃抽�绘腐閾佹按鑱旇繍浜屾湡涓鐝垪闆嗙粨涓績", "鏋佸厰閫熼�掑崕涓墿娴佹灑绾藉熀鍦伴」鐩�",
+        "涓鐝垪閰嶅澶氬紡鑱旇繍涓浆涓績椤圭洰", "鏂板紑鍙戦摱琛岃捶娆鹃暱姹熶腑娓革紙姝︽眽锛夋櫤鎱х墿娴佹灑绾介」鐩搧璺伐涓氱珯瀛愰」",
+        "榛勭煶鏂版腐鍥借儨鏅烘収鍏矾娓」鐩�", "鍗佸牥鐢熶骇鏈嶅姟鍨嬪浗瀹剁墿娴佹灑绾藉伐绋嬮」鐩�", "鍗佸牥甯傚煄鍙戜粨鍌ㄧ墿娴佷腑蹇�",
+        "瀵垮悍姘镐箰鏅鸿兘鍖栫墿娴佽澶囧崌绾ф敼閫�", "鍗佸牥姹介厤锛堝箍鍦猴級鐗╂祦涓績鎵╄兘鍗囩骇椤圭洰", "鍗佸牥姝﹀綋灞辨満鍦鸿埅绌虹墿娴佸洯椤圭洰",
+        "涓规睙鍙e競闄堝娓搧姘村叕绌鸿仈杩愬熀鍦�", "闀块槼鍘垮煄涔″喎閾剧墿娴佸洯鍙婂叾閰嶅璁炬柦寤鸿椤圭洰", "涓夊场寤烘潗鐗╂祦鍥�",
+        "瀹滈兘甯傞檰鍩庣患鍚堢墿娴佽繍杈撳粖閬�", "涓夊场鏅烘収鑸┖鐗╂祦浜т笟鍥�", "褰撻槼甯傚潩闄靛寲宸ュ洯鍖虹患鍚堢墿娴佸洯", "杩滃畨鍘跨墿娴佷腑蹇冩彁鑳藉崌绾ч」鐩�",
+        "瀹滄槍甯傛櫤鎱х墿娴侀泦鏁d腑蹇�", "瀹滈兘甯傛灊鍩庡寮忚仈杩愪复娓墿娴佸洯", "鐜夋箹鍐烽摼锛堣闃筹級浜ゆ槗涓績椤圭洰", "浼犲寲閯傝タ鍖楁櫤鎱х墿娴佷腑蹇冮」鐩�",
+        "姝︽眽娓埅瑗勯槼鏅烘収鐗╂祦鍥�", "鍞愮櫧娌虫腐鏁h揣杞繍涓績椤圭洰", "姹夋睙瑗勯槼锛堝皬娌筹級娓寮忚仈杩愮墿娴佸洯", "瑗勫窞鍖哄ぇ瀹楀晢鍝佸寮忚仈杩愪氦鏄撲腑蹇�",
+        "缁煎悎鎬т緵搴旈摼浜т笟鍥尯锛堜腑鍥芥湁鏈鸿胺渚涘簲閾炬櫤鎱т骇涓氬洯銆佽胺鍩庢櫤鎱у喎閾剧墿娴佷緵搴旈摼浜т笟鍥級", "鍗楁汲鍘跨幇浠f櫤鑳界墿娴佸洯鍖�",
+        "婀栧寳闀挎睙鐜颁唬鐗╂祦浜т笟闆嗚仛绀鸿寖鍖郝风幇浠g墿娴佸洯", "寮樹笟鐜颁唬鐗╂祦浜т笟鍥�", "瀹忚寕鍏ㄧ悆閫氳繍椤圭洰", "娌欐磱澶氬紡鑱旇繍鐜颁唬鐗╂祦鍥�",
+        "浜北甯傚叕閾佺墿娴佷腑蹇�", "鏂版潗鏂欏伐涓氬搧鏅烘収鐗╂祦鍥�", "闀胯崋閾佽矾锛堥挓绁ワ級鍏搧鑱旇繍闆嗚绠辩墿娴佸洯", "灞堝宀櫤鎱х墿娴佸洯",
+        "涓滄柟鍐烽摼鏅烘収鐗╂祦閰嶉�佷腑蹇�", "搴斿煄甯傛櫤鎱у喎閾句粨鍌ㄧ墿娴佸洯", "瀛濇槍骞挎禂鏅烘収鐗╂祦鐢靛晢浜т笟鍥缓璁鹃」鐩�",
+        "浜眽绮ゅぇ閫氶亾瀛濇劅鍥藉缁煎悎璐ц繍鏋㈢航", "鍏畨鍘垮寮忚仈杩愮患鍚堢墿娴佸洯", "鑽嗗窞缁忓紑鍖烘櫤鑳戒粨鍌ㄧ墿娴佸洯椤圭洰",
+        "涓浗渚涢攢鍏畨鍟嗚锤鐗╂祦鍥紙鍏畨鍘垮煙闆嗛噰闆嗛厤涓績锛�", "鐩戝埄甯傛腐鍙g墿娴佸洯锛堝煄鍖哄洯锛�", "閯備笢锛堥粍鍐堬級鏁版櫤鐗╂祦缁煎悎鏈嶅姟涓績",
+        "宕囬槼鍘垮浗瀹剁骇缁煎悎鐗╂祦鏋㈢航鍙婇厤濂楄鏂藉缓璁鹃」鐩�", "鍜稿畞楂樻柊鍖哄喕搴撳喎閾鹃」鐩紙涓�鏈燂級", "鍜稿畨鍖烘暟瀛楃墿娴佷骇涓氬洯鍙婂熀纭�璁炬柦寤鸿椤圭洰",
+        "閫氬北鍘跨壒鑹插啘浜у搧鐜颁唬鐗╂祦鍩哄湴寤鸿椤圭洰", "璧ゅ娌挎睙鍏搧鑱旇繍缁胯壊鐗╂祦涓績椤圭洰", "鍜稿畞鍩庝埂鏅烘収鐗╂祦鍥�",
+        "缁煎悎鐗╂祦鍥紙婀栧寳浜ゆ姇闅忓窞鏅烘収渚涘簲閾句骇涓氬洯锛�", "闅忓窞缁煎悎浠撳偍閰嶉�佷腑蹇�",
+        "骞挎按甯傛潹瀹跺鍏搧鐗╂祦鍥」鐩�---绗竴鏈熷箍姘村競鏉ㄥ閾佽矾璐у満锛堝箍姘村競鍗庨懌鍐堕噾鐗╂祦璐у満锛�",
+        "宸翠笢缁忔祹寮�鍙戝尯闈掗緳妗ユ櫤鎱х墿娴佷骇涓氬洯", "楣ゅ嘲姝﹂櫟灞变粨鍌ㄧ墿娴佸洯", "鍜镐赴鍘跨患鍚堢墿娴佸洯",
+        "浠欐鍥藉楂樻柊鍖虹墿娴佹灑绾戒腑蹇冨缓璁鹃」鐩�", "澶╅棬甯傚煙鍏叡缁煎悎鐗╂祦鍥�", "娼滄睙甯傚啘浜у搧鐜颁唬鐗╂祦鍩虹璁炬柦寤鸿椤圭洰",
+        "娼滄睙鐜颁唬鍐滀笟绉戞妧绀鸿寖鍥喎閾剧墿娴佷腑蹇冮」鐩�", "婀栧寳娼滅數鐗╂祦鏈夐檺鍏徃鐜颁唬鐗╂祦涓績锛堜竴銆佷簩銆佷笁鏈燂級"));
+
+    /** 姹囨�诲ぇ琛ㄥ競宸炵煭鍚� -> 瑙勮寖鍚� */
+    private static final Map<String, String> INVEST_CITY_SHORT_TO_FULL = new LinkedHashMap<>();
+
+    static {
+        INVEST_CITY_SHORT_TO_FULL.put("姝︽眽", "姝︽眽甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("榛勭煶", "榛勭煶甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("鍗佸牥", "鍗佸牥甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("瀹滄槍", "瀹滄槍甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("瑗勯槼", "瑗勯槼甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("閯傚窞", "閯傚窞甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("鑽嗛棬", "鑽嗛棬甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("瀛濇劅", "瀛濇劅甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("鑽嗗窞", "鑽嗗窞甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("榛勫唸", "榛勫唸甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("鍜稿畞", "鍜稿畞甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("闅忓窞", "闅忓窞甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("鎭╂柦", "鎭╂柦宸�");
+        INVEST_CITY_SHORT_TO_FULL.put("浠欐", "浠欐甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("娼滄睙", "娼滄睙甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("澶╅棬", "澶╅棬甯�");
+        INVEST_CITY_SHORT_TO_FULL.put("鏋楀尯", "绁炲啘鏋舵灄鍖�");
+    }
+
+    /**
+     * 鎶曡祫鏈堟姤瀵煎叆锛堝叏鐪佹眹鎬诲ぇ琛級銆�
+     * category: 瀹㈣繍绔欏満锛坸lsx 鏄庣粏 sheet锛�/ 鐗╂祦鍥尯锛坸ls 鍒嗛」鐩姇璧勫畬鎴愭儏鍐� sheet锛�
+     * 骞傜瓑锛氬悓鎶ヨ〃鏈熸棫鏈堝害鏁版嵁涓� INVEST 瀹℃牳缁撴灉鍏堝垹锛岄」鐩富妗f寜 椤圭洰鍚�+绫诲埆 upsert銆�
+     */
+    public ImportResult importInvestment(MultipartFile file, String period, String category) throws Exception {
+        boolean passenger = "瀹㈣繍绔欏満".equals(category);
+        List<InvestRow> rows = new ArrayList<>();
+        List<String> cityOrder = new ArrayList<>();
+        String currentCity = null;
+        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+            Sheet sheet = pickInvestmentSheet(wb, passenger);
+            // 瀹氫綅琛ㄥご琛岋細鍖呭惈"椤圭洰鍚嶇О"涓斿叾鍚庢湁鏁版嵁
+            int headerRow = -1;
+            for (int r = 0; r <= Math.min(sheet.getLastRowNum(), 8); r++) {
+                Row row = sheet.getRow(r);
+                if (row == null) continue;
+                String a = getString(row, 0);
+                String b = getString(row, 1);
+                if (b != null && b.contains("椤圭洰鍚嶇О") && (a == null || a.contains("搴忓彿") || a.contains("搴�"))) {
+                    headerRow = r;
+                    break;
+                }
+            }
+            if (headerRow < 0) {
+                throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨��" + (passenger ? "鎶曡祫鏈堟姤锛堝杩愮珯鍦猴級" : "鎶曡祫鏈堟姤锛堢墿娴佸洯鍖猴級") + "銆戜笌鏂囦欢鍐呭涓嶇锛氭湭鎵惧埌琛ㄥご琛岋紙闇�鍖呭惈 搴忓彿/椤圭洰鍚嶇О 鍒楋級锛岃纭鏄惁閫夐敊浜嗘暟鎹被鍨�");
+            }
+            for (int r = headerRow + 1; r <= sheet.getLastRowNum(); r++) {
+                Row row = sheet.getRow(r);
+                if (row == null) continue;
+                String a = getString(row, 0);
+                String b = getString(row, 1);
+                if (a == null || a.trim().isEmpty()) {
+                    if (b == null || b.trim().isEmpty()) continue;
+                }
+                String aTrim = a == null ? "" : a.trim();
+                String bTrim = b == null ? "" : b.trim();
+                // 鍚堣/鏍囬琛�
+                if (aTrim.contains("鍚堣") || aTrim.contains("鎬昏")) continue;
+                // 璧勯噾绫诲埆/鍒嗙粍鏍囬琛岋紙涓�銆佷簩銆佷笁銆佸洓銆佲�︼級
+                if (aTrim.matches("^[涓�浜屼笁鍥涗簲鍏竷鍏節鍗乚+銆�.*")) continue;
+                // 甯傚窞灏忚妭琛�
+                String city = investmentCity(aTrim);
+                if (city != null) {
+                    currentCity = city;
+                    if (!cityOrder.contains(city)) cityOrder.add(city);
+                    continue;
+                }
+                // 椤圭洰琛岋細搴忓彿涓烘暟瀛椾笖椤圭洰鍚嶉潪绌�
+                boolean seqNum = aTrim.matches("\\d+(\\.\\d+)?");
+                if (seqNum && !bTrim.isEmpty()) {
+                    InvestmentProject project = new InvestmentProject();
+                    project.setProjectName(bTrim);
+                    project.setCategory(category);
+                    project.setCity(currentCity);
+                    project.setBuilderName(getString(row, 2));
+                    project.setConstructNature(getString(row, 3));
+                    project.setStartTime(investTime(getString(row, 4)));
+                    project.setEndTime(investTime(getString(row, 5)));
+                    project.setTotalInvestment(getDecimal(row, 6));
+                    project.setApprovalGk(getString(row, 18));
+                    project.setApprovalCs(getString(row, 19));
+                    project.setSource("鍏ㄧ渷姹囨�诲ぇ琛�");
+                    project.setIsBillion(INVEST_BILLION_NAMES.contains(bTrim) ? 1 : 0);
+                    project.setIsCounty(INVEST_COUNTY_NAMES.contains(bTrim) ? 1 : 0);
+                    project.setIsFiveYear(INVEST_FIVE_YEAR_NAMES.contains(bTrim) ? 1 : 0);
+
+                    InvestmentMonthly monthly = new InvestmentMonthly();
+                    monthly.setReportPeriod(period);
+                    monthly.setYearPlan(getDecimal(row, 8));
+                    monthly.setYearCum(getDecimal(row, 9));
+                    monthly.setMonthDone(getDecimal(row, 10));
+                    monthly.setStartCum(getDecimal(row, 7));
+                    monthly.setProgressStage(getString(row, 11));
+                    monthly.setProgressDesc(getString(row, 12));
+                    monthly.setBuildingArea(getDecimalOrNull(row, 15));
+                    rows.add(new InvestRow(r + 1, project, monthly));
+                }
+            }
+        } catch (Exception e) {
+            log.error("Investment import parse error: {}", category, e);
+            throw e;
+        }
+
+        // 骞傜瓑娓呯悊锛氬悓鎶ヨ〃鏈熴�佸悓绫诲埆鏈堝害鏁版嵁 + INVEST 瀹℃牳缁撴灉锛堥」鐩富妗h法鏈堜繚鐣欙紝鎸夊悕 upsert锛�
+        List<InvestmentProject> sameCat = investProjectMapper.selectList(
+            new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, category));
+        java.util.Set<Long> catProjectIds = new HashSet<>();
+        for (InvestmentProject pc : sameCat) catProjectIds.add(pc.getId());
+        if (!catProjectIds.isEmpty()) {
+            investMonthlyMapper.delete(new LambdaQueryWrapper<InvestmentMonthly>()
+                .eq(InvestmentMonthly::getReportPeriod, period)
+                .in(InvestmentMonthly::getProjectId, catProjectIds));
+        }
+        List<Long> investRuleIds = new ArrayList<>();
+        for (com.trafficaudit.rulemanage.entity.AuditRule rule : ruleMapper.selectList(null)) {
+            if ("INVEST".equals(rule.getReportType())) investRuleIds.add(rule.getId());
+        }
+        if (!investRuleIds.isEmpty()) {
+            auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
+                .eq(AuditResult::getReportPeriod, period)
+                .in(AuditResult::getRuleId, investRuleIds));
+        }
+
+        int success = 0;
+        List<String> failDetails = new ArrayList<>();
+        for (InvestRow item : rows) {
+            try {
+                // 涓绘。 upsert锛堟寜 椤圭洰鍚�+绫诲埆锛�
+                InvestmentProject exist = investProjectMapper.selectOne(new LambdaQueryWrapper<InvestmentProject>()
+                    .eq(InvestmentProject::getProjectName, item.project.getProjectName())
+                    .eq(InvestmentProject::getCategory, category)
+                    .last("LIMIT 1"));
+                if (exist == null) {
+                    investProjectMapper.insert(item.project);
+                    item.monthly.setProjectId(item.project.getId());
+                } else {
+                    item.project.setId(exist.getId());
+                    item.project.setCreatedAt(exist.getCreatedAt());
+                    investProjectMapper.updateById(item.project);
+                    item.monthly.setProjectId(exist.getId());
+                }
+                investMonthlyMapper.insert(item.monthly);
+                success++;
+            } catch (Exception e) {
+                Throwable cause = e;
+                while (cause.getCause() != null) cause = cause.getCause();
+                String reason = cause.getMessage();
+                if (reason == null || reason.trim().isEmpty()) reason = e.getMessage();
+                failDetails.add("绗� " + item.excelRow + " 琛岋紙" + item.project.getProjectName() + "锛夛細" + reason);
+                log.error("Investment insert error: row {}", item.excelRow, e);
+            }
+        }
+        String errorDetail = String.join("\n", failDetails);
+        if (errorDetail.length() > 4000) errorDetail = errorDetail.substring(0, 4000) + "\n鈥︹��";
+        recordBatch(file.getOriginalFilename(), "INVEST_" + category, period, rows.size(), success, failDetails.size(), errorDetail);
+        log.info("Investment {} imported: {} rows for {}", category, success, period);
+        return new ImportResult(success, failDetails.size(), failDetails);
+    }
+
+    // ========== 鎶曡祫妯″潡 M2锛氬競宸炲崟琛ㄦ壒閲忓鍏ワ紙瀹归敊瑙f瀽锛� ==========
+
+    /**
+     * 甯傚窞鍗曡〃鎵归噺瀵煎叆锛氭壂鎻� docs/鎶曡祫/杈撳叆/{绫诲埆鐩綍}/{X鏈坿 涓嬪叏閮� xls/xlsx/et锛堣烦杩� pdf锛夛紝
+     * 瀹归敊瑙f瀽姣忎釜鏂囦欢锛堝畾浣嶈〃澶磋 + 鍒楀悕鏄犲皠锛夛紝upsert 椤圭洰涓绘。 + 鍐欏叆鏈堝害锛�
+     * 骞朵笌銆屽崄浜斾簲銆嶆ā鏉挎竻鍗曡鑼冨寲鍖归厤鍚庢爣璁� is_five_year銆�
+     */
+    public Map<String, Object> importInvestCityDir(String period, String category) throws Exception {
+        boolean passenger = "瀹㈣繍绔欏満".equals(category);
+        String catDir = passenger ? "瀹㈣繍锛堝杩愮珯锛�" : "鐗╂祦锛堢墿娴佸洯鍖猴級";
+        String monthDir = investMonthDir(period);
+        if (monthDir.isEmpty()) throw new RuntimeException("鎶ヨ〃鏈熸牸寮忓簲涓� yyyy-MM锛屼緥濡� 2026-07");
+        File dir = resolveInvestInputDir(catDir, monthDir);
+        if (dir == null || !dir.isDirectory()) {
+            throw new RuntimeException("鏈壘鍒板競宸炲崟琛ㄧ洰褰曪細docs/鎶曡祫/杈撳叆/" + catDir + "/" + monthDir);
+        }
+        Set<String> fiveYearNames = loadFiveYearProjectNames();
+        List<InvestCityRow> rows = new ArrayList<>();
+        List<Map<String, Object>> fileResults = new ArrayList<>();
+        File[] files = dir.listFiles((d, n) -> {
+            String lower = n.toLowerCase();
+            return lower.endsWith(".xls") || lower.endsWith(".xlsx") || lower.endsWith(".et");
+        });
+        if (files == null || files.length == 0) {
+            throw new RuntimeException("鐩綍涓病鏈夊彲瑙f瀽鐨勬姤琛ㄦ枃浠讹紙xls/xlsx/et锛夛細" + dir.getAbsolutePath());
+        }
+        Arrays.sort(files, Comparator.comparing(File::getName));
+        for (File f : files) {
+            Map<String, Object> fr = new LinkedHashMap<>();
+            fr.put("fileName", f.getName());
+            try (java.io.InputStream is = new java.io.FileInputStream(f);
+                 Workbook wb = WorkbookFactory.create(is)) {
+                parseInvestCityWorkbook(wb, f.getName(), category, period, fiveYearNames, rows, fr);
+            } catch (Exception e) {
+                fr.put("success", 0);
+                fr.put("error", e.getMessage() == null ? e.toString() : e.getMessage());
+                log.error("invest city file parse error: {}", f.getName(), e);
+            }
+            fileResults.add(fr);
+        }
+        // 骞傜瓑娓呯悊锛氭寜鏈鏂囦欢娑夊強甯傚窞娓呯悊鏈堝害鏁版嵁 + INVEST 瀹℃牳缁撴灉锛堥」鐩富妗h法鏈堜繚鐣欙紝鎸夊悕 upsert锛�
+        cleanInvestMonthlyByCity(category, period, rows);
+        Map<String, Set<String>> knownCities = loadInvestKnownCities(category);
+        Map<String, String> fileFirstMismatch = new LinkedHashMap<>();
+        int success = 0;
+        List<String> failDetails = new ArrayList<>();
+        for (InvestCityRow item : rows) {
+            String mismatch = investCityMismatchReason(item, knownCities);
+            if (mismatch != null) {
+                failDetails.add(mismatch);
+                fileFirstMismatch.putIfAbsent(item.fileName, mismatch);
+                continue;
+            }
+            try {
+                saveInvestProjectRow(item.project, item.monthly);
+                success++;
+            } catch (Exception e) {
+                Throwable cause = e;
+                while (cause.getCause() != null) cause = cause.getCause();
+                String reason = cause.getMessage();
+                if (reason == null || reason.trim().isEmpty()) reason = e.getMessage();
+                failDetails.add(item.fileName + " 绗� " + item.excelRow + " 琛岋紙" + item.project.getProjectName() + "锛夛細" + reason);
+                log.error("Invest city insert error: file {}, row {}", item.fileName, item.excelRow, e);
+            }
+        }
+        // 鎶婇涓競宸為敊浣嶆彁绀烘寕鍒板搴旀枃浠剁粨鏋滀笂锛屼究浜庨〉闈㈠脊绐楃洿鎺ョ湅鍒�
+        for (Map<String, Object> fr2 : fileResults) {
+            String msg = fileFirstMismatch.get(fr2.get("fileName"));
+            if (msg != null && fr2.get("error") == null) fr2.put("error", msg);
+        }
+        String errorDetail = String.join("\n", failDetails);
+        if (errorDetail.length() > 4000) errorDetail = errorDetail.substring(0, 4000) + "\n鈥︹��";
+        recordBatch("甯傚窞鍗曡〃鎵归噺瀵煎叆锛�" + catDir + "/" + monthDir + "锛�", "INVEST_" + category + "_CITY",
+            period, rows.size(), success, failDetails.size(), errorDetail);
+        int fiveYearMatched = 0;
+        for (InvestCityRow item : rows) {
+            if (Integer.valueOf(1).equals(item.project.getIsFiveYear())) fiveYearMatched++;
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("total", rows.size());
+        result.put("success", success);
+        result.put("fail", failDetails.size());
+        result.put("failDetails", failDetails);
+        result.put("files", fileResults);
+        result.put("fiveYearTemplateProjects", fiveYearNames.size());
+        result.put("fiveYearMatched", fiveYearMatched);
+        log.info("Invest city {} {} imported: {} rows (fiveYear matched {}/{})",
+            category, period, success, fiveYearMatched, fiveYearNames.size());
+        return result;
+    }
+
+    /**
+     * 澶氭枃浠舵壒閲忓鍏ワ細鍓嶇涓�娆¢�夋嫨澶氫釜 Excel 涓婁紶銆�
+     * 鎶曡祫绫诲瀷锛坕nvestment/investmentLogistics锛夋寜甯傚窞鍗曡〃瑙f瀽锛堟枃浠跺悕/濉姤鍗曚綅璇嗗埆甯傚窞锛屽崄浜斾簲鑷姩鏍囪锛夛紱
+     * 鍏朵粬绫诲瀷閫愪釜璧扮幇鏈夎В鏋愰�昏緫锛坕mportDirFile锛夈��
+     */
+    public Map<String, Object> importBatchFiles(String type, String period, List<MultipartFile> files) throws Exception {
+        if (files == null || files.isEmpty()) {
+            throw new RuntimeException("鏈帴鏀跺埌浠讳綍鏂囦欢");
+        }
+        if ("investment".equals(type) || "investmentLogistics".equals(type)) {
+            String category = "investment".equals(type) ? "瀹㈣繍绔欏満" : "鐗╂祦鍥尯";
+            return importInvestCityFiles(period, category, files);
+        }
+        List<Map<String, Object>> fileResults = new ArrayList<>();
+        int totalSuccess = 0, totalFail = 0;
+        for (MultipartFile f : files) {
+            Map<String, Object> fr = new LinkedHashMap<>();
+            fr.put("fileName", f.getOriginalFilename());
+            try {
+                ImportResult r = importDirFile(type, f, period);
+                fr.put("success", r.getSuccess());
+                fr.put("fail", r.getFail());
+                if (r.getFail() > 0 && r.getFailDetails() != null && !r.getFailDetails().isEmpty()) {
+                    fr.put("error", r.getFailDetails().get(0));
+                }
+                totalSuccess += r.getSuccess();
+                totalFail += r.getFail();
+            } catch (Exception e) {
+                fr.put("success", 0);
+                fr.put("fail", 0);
+                fr.put("error", e.getMessage() == null ? e.toString() : e.getMessage());
+                totalFail++;
+            }
+            fileResults.add(fr);
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("type", type);
+        result.put("total", files.size());
+        result.put("success", totalSuccess);
+        result.put("fail", totalFail);
+        result.put("files", fileResults);
+        return result;
+    }
+
+    /** 鎶曡祫澶氭枃浠舵壒閲忥細閫愭枃浠舵寜甯傚窞鍗曡〃瑙f瀽锛岀粺涓�骞傜瓑娓呯悊钀藉簱锛屾壒娆℃眹鎬� */
+    public Map<String, Object> importInvestCityFiles(String period, String category, List<MultipartFile> files) throws Exception {
+        boolean passenger = "瀹㈣繍绔欏満".equals(category);
+        Set<String> fiveYearNames = loadFiveYearProjectNames();
+        List<InvestCityRow> rows = new ArrayList<>();
+        List<Map<String, Object>> fileResults = new ArrayList<>();
+        for (MultipartFile f : files) {
+            Map<String, Object> fr = new LinkedHashMap<>();
+            fr.put("fileName", f.getOriginalFilename());
+            try (Workbook wb = WorkbookFactory.create(f.getInputStream())) {
+                parseInvestCityWorkbook(wb, f.getOriginalFilename(), category, period, fiveYearNames, rows, fr);
+            } catch (Exception e) {
+                fr.put("success", 0);
+                fr.put("error", e.getMessage() == null ? e.toString() : e.getMessage());
+                log.error("invest city file parse error: {}", f.getOriginalFilename(), e);
+            }
+            fileResults.add(fr);
+        }
+        String catDir = passenger ? "瀹㈣繍锛堝杩愮珯锛�" : "鐗╂祦锛堢墿娴佸洯鍖猴級";
+        cleanInvestMonthlyByCity(category, period, rows);
+        Map<String, Set<String>> knownCities = loadInvestKnownCities(category);
+        Map<String, String> fileFirstMismatch = new LinkedHashMap<>();
+        int success = 0;
+        List<String> failDetails = new ArrayList<>();
+        for (InvestCityRow item : rows) {
+            String mismatch = investCityMismatchReason(item, knownCities);
+            if (mismatch != null) {
+                failDetails.add(mismatch);
+                fileFirstMismatch.putIfAbsent(item.fileName, mismatch);
+                continue;
+            }
+            try {
+                saveInvestProjectRow(item.project, item.monthly);
+                success++;
+            } catch (Exception e) {
+                Throwable cause = e;
+                while (cause.getCause() != null) cause = cause.getCause();
+                String reason = cause.getMessage();
+                if (reason == null || reason.trim().isEmpty()) reason = e.getMessage();
+                failDetails.add(item.fileName + " 绗� " + item.excelRow + " 琛岋紙" + item.project.getProjectName() + "锛夛細" + reason);
+                log.error("Invest city insert error: file {}, row {}", item.fileName, item.excelRow, e);
+            }
+        }
+        // 鎶婇涓競宸為敊浣嶆彁绀烘寕鍒板搴旀枃浠剁粨鏋滀笂锛屼究浜庨〉闈㈠脊绐楃洿鎺ョ湅鍒�
+        for (Map<String, Object> fr2 : fileResults) {
+            String msg = fileFirstMismatch.get(fr2.get("fileName"));
+            if (msg != null && fr2.get("error") == null) fr2.put("error", msg);
+        }
+        String errorDetail = String.join("\n", failDetails);
+        if (errorDetail.length() > 4000) errorDetail = errorDetail.substring(0, 4000) + "\n鈥︹��";
+        recordBatch("澶氭枃浠舵壒閲忓鍏ワ紙" + catDir + "/" + period + "锛�", "INVEST_" + category + "_CITY",
+            period, rows.size(), success, failDetails.size(), errorDetail);
+        int fiveYearMatched = 0;
+        for (InvestCityRow item : rows) {
+            if (Integer.valueOf(1).equals(item.project.getIsFiveYear())) fiveYearMatched++;
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("total", rows.size());
+        result.put("success", success);
+        result.put("fail", failDetails.size());
+        result.put("failDetails", failDetails);
+        result.put("files", fileResults);
+        result.put("fiveYearTemplateProjects", fiveYearNames.size());
+        result.put("fiveYearMatched", fiveYearMatched);
+        log.info("Invest city multi-file {} {} imported: {} rows (fiveYear matched {}/{})",
+            category, period, success, fiveYearMatched, fiveYearNames.size());
+        return result;
+    }
+
+    /** 澶� sheet 鍘嗗彶鏂囦欢锛堣闃� .et 绛夛級锛歴heet 鍚嶅惈鎶ヨ〃鏈熶紭鍏堬紝鍏舵閫夋暟鎹鏈�澶� */
+    private boolean investSheetMatchesPeriod(Sheet sh, String period) {
+        if (period == null || !period.matches("\\d{4}-\\d{2}")) return false;
+        String name = sh.getSheetName();
+        if (name == null || name.isEmpty()) return false;
+        String norm = name.replace(" ", "");
+        int month = Integer.parseInt(period.substring(5, 7));
+        String normDash = norm.replace('.', '-'); // "2026.8"/"2026.08" -> "2026-8"/"2026-08"
+        return norm.contains(month + "鏈�") || norm.contains(String.format("%02d", month) + "鏈�")
+            || name.contains(period) || name.contains(period.replace("-0", "-"))
+            || normDash.contains(period) || normDash.contains(period.replace("-0", "-"));
+    }
+
+    /** 瑙f瀽鍗曚釜甯傚窞鎶ヨ〃锛氶�� sheet锛堟姤琛ㄦ湡鍚嶄紭鍏� + 鏁版嵁琛屾暟锛夛紝鎸夊垪鍚嶆槧灏勬彁鍙栭」鐩 */
+    private void parseInvestCityWorkbook(Workbook wb, String fileName, String category, String period,
+                                         Set<String> fiveYearNames, List<InvestCityRow> rows, Map<String, Object> fr) {
+        Sheet best = null;
+        Map<String, Integer> bestCol = null;
+        int bestScore = -1;
+        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+            Sheet sh = wb.getSheetAt(i);
+            Map<String, Integer> col = findInvestHeader(sh);
+            if (col == null) continue;
+            int cnt = countInvestRows(sh, col);
+            int score = cnt + (cnt > 0 && investSheetMatchesPeriod(sh, period) ? 1000000 : 0);
+            if (cnt > 0 && investSheetMatchesPeriod(sh, period)) {
+                // 褰撴湡鐪熷疄鎶ヨ〃 sheet 鍚嶉�氬父甯﹀勾浠斤紙濡� 2026.8/2026骞�8鏈堬級锛屽巻鍙瞫heet澶氫负绾湀浠藉悕锛�8鏈堬級锛氬甫骞翠唤鐨勪紭鍏�
+                String norm = sh.getSheetName() == null ? "" : sh.getSheetName().replace(" ", "");
+                if (norm.contains(period.substring(0, 4))) score += 100000;
+            }
+            if (score > bestScore) {
+                best = sh;
+                bestCol = col;
+                bestScore = score;
+            }
+        }
+        if (best == null) {
+            fr.put("success", 0);
+            fr.put("error", "鏈壘鍒板惈銆岄」鐩悕绉般�嶇殑琛ㄥご");
+            return;
+        }
+        String city = cityFromFileName(fileName);
+        if (city == null) city = cityFromSheetHeader(best);
+        fr.put("city", city == null ? "" : city);
+        String currentCity = city;
+        Map<String, Integer> col = bestCol;
+        int nameCol = col.get("name");
+        boolean hasSeq = col.containsKey("seq");
+        int seqCol = hasSeq ? col.get("seq") : 0;
+        int parsed = 0;
+        for (int r = col.get("headerRow") + 1; r <= best.getLastRowNum(); r++) {
+            Row row = best.getRow(r);
+            if (row == null) continue;
+            String name = cellStr(row, nameCol);
+            String seq = hasSeq ? cellStr(row, seqCol) : null;
+            // 甯傚窞灏忚妭琛岋細閮ㄥ垎姹囨�昏〃鎶婂競宸炲悕鍐欏湪搴忓彿鍒楋紙濡傚崄鍫版眹鎬昏〃 A 鍒楀啓"姝︽眽"锛夛紝闇�鍚屾牱璇嗗埆
+            if (seq != null && !seq.trim().isEmpty()) {
+                String seqCity = investmentCity(seq.trim());
+                if (seqCity != null) {
+                    currentCity = seqCity;
+                    continue;
+                }
+            }
+            if (name == null || name.trim().isEmpty()) continue;
+            String nt = name.trim().replaceAll("[\r\n]+", "");
+            if (nt.contains("鍚堣") || nt.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()) {
+                    // 甯傚窞灏忚妭琛岋紙濡� "姝︽眽" "澶� 闂�"锛夛紝鏇存柊褰撳墠甯傚窞
+                    String c = investmentCity(nt);
+                    if (c != null) { currentCity = c; continue; }
+                    // 搴忓彿涓虹┖鐨勭湡瀹為」鐩锛堥儴鍒嗗競宸炴湀鎶ヤ笉濉簭鍙凤紝濡傜鍐滄灦鐗╂祦鍥級涔熸寜椤圭洰澶勭悊
+                    if (!isLikelyInvestProjectName(nt)) continue;
+                } else if (!seq.trim().matches("\\d+(\\.\\d+)?")) {
+                    continue;
+                }
+            } else {
+                // 鏃犲簭鍙峰垪锛堥殢宸炵瓑锛夛細椤圭洰鍚嶉潪绌哄嵆椤圭洰琛岋紝甯傚窞灏忚妭琛岀敤 investmentCity 璇嗗埆
+                String c = investmentCity(nt);
+                if (c != null) { currentCity = c; continue; }
+            }
+            InvestmentProject project = new InvestmentProject();
+            project.setProjectName(nt);
+            project.setCategory(category);
+            project.setCity(currentCity);
+            project.setBuilderName(cellStr(row, col.get("builder")));
+            project.setConstructNature(cellStr(row, col.get("nature")));
+            project.setStartTime(investTime(cellStr(row, col.get("start"))));
+            project.setEndTime(investTime(cellStr(row, col.get("end"))));
+            project.setTotalInvestment(cellNum(row, col.get("total")));
+            project.setApprovalGk(cellStr(row, col.get("gk")));
+            project.setApprovalCs(cellStr(row, col.get("cs")));
+            project.setBuildingArea(cellNumOrNull(row, col.get("area")));
+            project.setSource("甯傚窞鍗曡〃");
+            project.setIsBillion(INVEST_BILLION_NAMES.contains(nt) ? 1 : 0);
+            project.setIsCounty(INVEST_COUNTY_NAMES.contains(nt) ? 1 : 0);
+            project.setIsFiveYear(fiveYearNames.contains(normalizeProjectName(nt)) ? 1 : 0);
+            InvestmentMonthly monthly = new InvestmentMonthly();
+            monthly.setReportPeriod(period);
+            monthly.setStartCum(cellNum(row, col.get("startcum")));
+            monthly.setYearPlan(cellNum(row, col.get("yearplan")));
+            monthly.setYearCum(cellNum(row, col.get("yearcum")));
+            monthly.setMonthDone(cellNum(row, col.get("monthdone")));
+            monthly.setProgressStage(cellStr(row, col.get("stage")));
+            monthly.setProgressDesc(cellStr(row, col.get("desc")));
+            monthly.setBuildingArea(cellNumOrNull(row, col.get("area")));
+            rows.add(new InvestCityRow(r + 1, fileName, project, monthly));
+            parsed++;
+        }
+        fr.put("city", currentCity);
+        fr.put("success", parsed);
+    }
+
+    /** 瀹氫綅鍚�岄」鐩悕绉般�嶇殑琛ㄥご琛屽苟鎸夊垪鍚嶅叧閿瘝寤虹珛鍒楁槧灏勶紙鏀寔鍙屽眰琛ㄥご鍚堝苟锛� */
+    private Map<String, Integer> findInvestHeader(Sheet sh) {
+        int maxHeaderRow = Math.min(22, sh.getLastRowNum());
+        for (int r = 0; r <= maxHeaderRow; r++) {
+            Row row = sh.getRow(r);
+            if (row == null) continue;
+            boolean hasName = false;
+            for (int c = 0; c <= row.getLastCellNum(); c++) {
+                String v = cellStr(row, c);
+                if (v != null && v.contains("椤圭洰鍚嶇О")) { hasName = true; break; }
+            }
+            if (!hasName) continue;
+            Map<Integer, String> colTexts = new HashMap<>();
+            for (int rr = r; rr <= Math.min(r + 3, sh.getLastRowNum()); rr++) {
+                Row rr2 = sh.getRow(rr);
+                if (rr2 == null) continue;
+                for (int c = 0; c <= rr2.getLastCellNum(); c++) {
+                    String v = cellStr(rr2, c);
+                    if (v == null) continue;
+                    colTexts.merge(c, v, (a, b) -> a + "|" + b);
+                }
+            }
+            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);
+            }
+            if (!col.containsKey("name")) continue;
+            for (int c = 0; c <= row.getLastCellNum(); c++) {
+                String v = cellStr(row, c);
+                if (v != null && v.trim().matches("搴廫\s*鍙�")) { col.put("seq", c); break; }
+            }
+            // fallback锛歽earcum 缂哄け鏃舵寜 startcum+2锛堟�绘姇璧�/鑷紑濮嬬疮璁�/鏈勾璁″垝/鑷勾鍒濈疮璁� 鐨勬爣鍑嗗垪搴忥級
+            if (!col.containsKey("yearcum") && col.containsKey("startcum")) {
+                int yc = col.get("startcum") + 2;
+                if (yc <= row.getLastCellNum()) col.put("yearcum", yc);
+            }
+            // monthdone 缂哄け鏃舵寜 yearcum+1
+            if (!col.containsKey("monthdone") && col.containsKey("yearcum")) {
+                int md = col.get("yearcum") + 1;
+                if (md <= row.getLastCellNum()) col.put("monthdone", md);
+            }
+            for (String k : new String[]{"builder", "nature", "start", "end", "total", "startcum",
+                "yearplan", "yearcum", "monthdone", "stage", "desc", "gk", "cs", "area"}) {
+                col.putIfAbsent(k, -1);
+            }
+            col.put("headerRow", r);
+            return col;
+        }
+        return null;
+    }
+
+    /** 缁熻 sheet 鍐呴」鐩暟鎹鏁帮紙鐢ㄤ簬澶� sheet 鍘嗗彶鏂囦欢閫夊綋鏈� sheet锛� */
+    private int countInvestRows(Sheet sh, Map<String, Integer> col) {
+        int nameCol = col.get("name");
+        boolean hasSeq = col.containsKey("seq");
+        int seqCol = hasSeq ? col.get("seq") : 0;
+        int count = 0;
+        for (int r = col.get("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("鍚堣") || nt.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++;
+            } else {
+                String seq = cellStr(row, seqCol);
+                if (seq != null && seq.trim().matches("\\d+(\\.\\d+)?")) {
+                    count++;
+                } else if ((seq == null || seq.trim().isEmpty()) && isLikelyInvestProjectName(nt)) {
+                    count++;
+                }
+            }
+        }
+        return count;
+    }
+
+    /** 浠庢枃浠跺悕鎻愬彇甯傚窞锛堝 "锛堝瓭鎰熷競閬撹繍涓績锛夆��" -> 瀛濇劅甯傦級 */
+    private String cityFromFileName(String fileName) {
+        for (Map.Entry<String, String> e : INVEST_CITY_SHORT_TO_FULL.entrySet()) {
+            if (fileName.contains(e.getKey())) return e.getValue();
+        }
+        return null;
+    }
+
+    /** 浠� sheet 澶撮儴銆屽~鎶ュ崟浣嶏細XX銆嶈鎻愬彇甯傚窞 */
+    private String cityFromSheetHeader(Sheet sh) {
+        for (int r = 0; r < Math.min(6, sh.getLastRowNum() + 1); r++) {
+            Row row = sh.getRow(r);
+            if (row == null) continue;
+            for (int c = 0; c <= row.getLastCellNum(); c++) {
+                String v = cellStr(row, c);
+                if (v == null || !v.contains("濉姤鍗曚綅")) continue;
+                java.util.regex.Matcher m = java.util.regex.Pattern
+                    .compile("濉姤鍗曚綅[锛�:锛�(]?(鐩栫珷|绛剧珷)?[锛�)]?[锛�:]*([^\\s锛�,锛�;]{2,14})")
+                    .matcher(v);
+                if (m.find()) {
+                    String raw = m.group(2);
+                    for (Map.Entry<String, String> e : INVEST_CITY_SHORT_TO_FULL.entrySet()) {
+                        if (raw.contains(e.getKey())) return e.getValue();
+                    }
+                    String city = investmentCity(raw);
+                    if (city != null) return city;
+                }
+            }
+        }
+        return null;
+    }
+
+    /** 瑙f瀽銆屽崄浜斾簲銆嶈鍒掔墿娴侀」鐩ā鏉挎竻鍗曪紝杩斿洖瑙勮寖鍖栭」鐩悕闆嗗悎 */
+    private Set<String> loadFiveYearProjectNames() {
+        Set<String> names = new LinkedHashSet<>();
+        File template = resolveInvestTemplate("妯℃澘_鍗佷簲浜旇鍒掔墿娴侀」鐩繘灞曟儏鍐�.xls");
+        if (template == null || !template.exists()) {
+            log.warn("鍗佷簲浜旀ā鏉夸笉瀛樺湪锛宨s_five_year 鏍囪璺宠繃: {}", template);
+            return names;
+        }
+        try (java.io.InputStream is = new java.io.FileInputStream(template);
+             Workbook wb = WorkbookFactory.create(is)) {
+            Sheet sh = null;
+            for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+                if (wb.getSheetName(i).contains("鍒嗛」鐩姇璧�")) { sh = wb.getSheetAt(i); break; }
+            }
+            if (sh == null) sh = wb.getSheetAt(0);
+            Map<String, Integer> col = findInvestHeader(sh);
+            if (col == null) return names;
+            int nameCol = col.get("name");
+            int seqCol = col.containsKey("seq") ? col.get("seq") : 0;
+            for (int r = col.get("headerRow") + 1; r <= sh.getLastRowNum(); r++) {
+                Row row = sh.getRow(r);
+                if (row == null) continue;
+                String seq = cellStr(row, seqCol);
+                String name = cellStr(row, nameCol);
+                if (seq == null || name == null) continue;
+                if (!seq.trim().matches("\\d+")) continue;
+                names.add(normalizeProjectName(name.trim()));
+            }
+        } catch (Exception e) {
+            log.error("load five year project names error", e);
+        }
+        return names;
+    }
+
+    /** 搴忓彿涓虹┖鐨勯潪甯傚窞琛屾槸鍚﹀儚椤圭洰琛岋細闀垮害>4 涓旈潪澶囨敞/璇存槑绛夋潅椤� */
+    private boolean isLikelyInvestProjectName(String nt) {
+        if (nt == null || nt.length() <= 4) return false;
+        if (investmentCity(nt) != null) return false;
+        return !nt.startsWith("澶囨敞") && !nt.startsWith("璇存槑") && !nt.startsWith("娉細")
+            && !nt.startsWith("濉〃") && !nt.startsWith("瀹℃牳") && !nt.startsWith("缁熻")
+            && !nt.startsWith("璐熻矗");
+    }
+
+    /** 椤圭洰鍚嶈鑼冨寲锛氬幓绌虹櫧/鎹㈣/鎷彿鍐呭/鏍囩偣/銆岄」鐩�嶅悗缂�锛岀敤浜庡崄浜斾簲娓呭崟鍖归厤 */
+    private String normalizeProjectName(String name) {
+        String s = name.replaceAll("\\s+", "").replace("\n", "");
+        s = s.replaceAll("[锛�(銆怽\[][^锛�)銆慭\]]*[锛�)銆慭\]]", "");
+        s = s.replaceAll("[锛�,銆�.銆侊紱;锛�:鈥擻\-锛峗/\\\\|]", "");
+        s = s.replace("鐗╂祦椤圭洰", "鐗╂祦").replace("椤圭洰", "");
+        return s;
+    }
+
+    /** 鎶ヨ〃鏈� "2026-07" -> "7鏈�" */
+    private String investMonthDir(String period) {
+        if (period == null || !period.matches("\\d{4}-\\d{2}")) return "";
+        int month = Integer.parseInt(period.substring(5, 7));
+        return month + "鏈�";
+    }
+
+    /** 鎶曡祫杈撳叆鐩綍瀹氫綅锛歶ser.dir -> user.dir/.. 閫愮骇鍥為��锛坰pring-boot:run 鐨� user.dir 鏄� traffic-audit-server锛� */
+    private File resolveInvestInputDir(String catDir, String monthDir) {
+        String rel = "docs/鎶曡祫/杈撳叆/" + catDir + "/" + monthDir;
+        for (String baseDir : new String[]{System.getProperty("user.dir"), System.getProperty("user.dir") + "/.."}) {
+            File f = new File(baseDir, rel);
+            if (f.isDirectory()) return f;
+        }
+        return null;
+    }
+
+    private File resolveInvestTemplate(String fileName) {
+        String rel = "docs/鎶曡祫/妯℃澘/" + fileName;
+        for (String baseDir : new String[]{System.getProperty("user.dir"), System.getProperty("user.dir") + "/.."}) {
+            File f = new File(baseDir, rel);
+            if (f.isFile()) return f;
+        }
+        return null;
+    }
+
+    /** 鍚岀被鍒悓鎶ヨ〃鏈熸湀搴︽暟鎹� + INVEST 瀹℃牳缁撴灉娓呯悊锛堝箓绛夛紝鍏ㄧ渷姹囨�诲ぇ琛ㄥ叏閲忔竻鐞嗭級 */
+    private void cleanInvestMonthly(String category, String period) {
+        List<InvestmentProject> sameCat = investProjectMapper.selectList(
+            new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, category));
+        java.util.Set<Long> catProjectIds = new HashSet<>();
+        for (InvestmentProject pc : sameCat) catProjectIds.add(pc.getId());
+        if (!catProjectIds.isEmpty()) {
+            investMonthlyMapper.delete(new LambdaQueryWrapper<InvestmentMonthly>()
+                .eq(InvestmentMonthly::getReportPeriod, period)
+                .in(InvestmentMonthly::getProjectId, catProjectIds));
+        }
+        deleteInvestAuditResults(period, null);
+    }
+
+    /** 鎸夊競宸炴竻鐞嗭細鍙垹闄ゆ湰娆℃枃浠舵秹鍙婂競宸烇紙鍚湭璇嗗埆甯傚窞鐨� null 椤圭洰锛夌殑鏈堝害鏁版嵁 + INVEST 瀹℃牳缁撴灉 */
+    private void cleanInvestMonthlyByCity(String category, String period, List<InvestCityRow> rows) {
+        Set<String> citySet = new HashSet<>();
+        boolean hasNullCity = false;
+        for (InvestCityRow item : rows) {
+            String c = item.project.getCity();
+            if (c == null || c.trim().isEmpty()) hasNullCity = true;
+            else citySet.add(c.trim());
+        }
+        if (citySet.isEmpty() && !hasNullCity) return;
+        boolean cleanNullCity = hasNullCity;
+        LambdaQueryWrapper<InvestmentProject> pw = new LambdaQueryWrapper<InvestmentProject>()
+            .eq(InvestmentProject::getCategory, category);
+        pw.and(w -> {
+            if (!citySet.isEmpty()) w.in(InvestmentProject::getCity, citySet);
+            if (cleanNullCity) {
+                if (!citySet.isEmpty()) w.or();
+                w.isNull(InvestmentProject::getCity);
+            }
+        });
+        List<InvestmentProject> cityProjects = investProjectMapper.selectList(pw);
+        java.util.Set<Long> catProjectIds = new HashSet<>();
+        for (InvestmentProject pc : cityProjects) catProjectIds.add(pc.getId());
+        if (catProjectIds.isEmpty()) return;
+        List<InvestmentMonthly> oldMonthlies = investMonthlyMapper.selectList(
+            new LambdaQueryWrapper<InvestmentMonthly>()
+                .eq(InvestmentMonthly::getReportPeriod, period)
+                .in(InvestmentMonthly::getProjectId, catProjectIds));
+        java.util.Set<Long> oldMonthlyIds = new HashSet<>();
+        for (InvestmentMonthly m : oldMonthlies) oldMonthlyIds.add(m.getId());
+        investMonthlyMapper.delete(new LambdaQueryWrapper<InvestmentMonthly>()
+            .eq(InvestmentMonthly::getReportPeriod, period)
+            .in(InvestmentMonthly::getProjectId, catProjectIds));
+        deleteInvestAuditResults(period, oldMonthlyIds);
+    }
+
+    /** 娓呯悊 INVEST 瀹℃牳缁撴灉锛坢onthlyIds 涓� null 琛ㄧず璇ユ湡闂村叏閲忔竻鐞嗭級 */
+    private void deleteInvestAuditResults(String period, java.util.Set<Long> monthlyIds) {
+        List<Long> investRuleIds = new ArrayList<>();
+        for (com.trafficaudit.rulemanage.entity.AuditRule rule : ruleMapper.selectList(null)) {
+            if ("INVEST".equals(rule.getReportType())) investRuleIds.add(rule.getId());
+        }
+        if (investRuleIds.isEmpty()) return;
+        LambdaQueryWrapper<AuditResult> w = new LambdaQueryWrapper<AuditResult>()
+            .eq(AuditResult::getReportPeriod, period)
+            .in(AuditResult::getRuleId, investRuleIds);
+        if (monthlyIds != null) {
+            if (monthlyIds.isEmpty()) return;
+            w.in(AuditResult::getReportId, monthlyIds);
+        }
+        auditResultMapper.delete(w);
+    }
+
+    /** 椤圭洰涓绘。 upsert锛堟寜 椤圭洰鍚�+绫诲埆锛�+ 鏈堝害鍐欏叆 */
+    private void saveInvestProjectRow(InvestmentProject project, InvestmentMonthly monthly) {
+        InvestmentProject exist = investProjectMapper.selectOne(new LambdaQueryWrapper<InvestmentProject>()
+            .eq(InvestmentProject::getProjectName, project.getProjectName())
+            .eq(InvestmentProject::getCategory, project.getCategory())
+            .last("LIMIT 1"));
+        if (exist == null) {
+            investProjectMapper.insert(project);
+            monthly.setProjectId(project.getId());
+        } else {
+            project.setId(exist.getId());
+            project.setCreatedAt(exist.getCreatedAt());
+            investProjectMapper.updateById(project);
+            monthly.setProjectId(exist.getId());
+        }
+        investMonthlyMapper.insert(monthly);
+    }
+
+    /** 杞藉叆鏌愮被鍒�岃鑼冨寲椤圭洰鍚� -> 宸插叆搴撳競宸為泦鍚堛�嶏紝鐢ㄤ簬甯傚窞涓�鑷存�ф牎楠岋紙鍚嶇О鍦ㄥ涓競宸為噸澶嶆椂鏀惧純鏍¢獙锛� */
+    private Map<String, Set<String>> loadInvestKnownCities(String category) {
+        Map<String, Set<String>> map = new HashMap<>();
+        List<InvestmentProject> list = investProjectMapper.selectList(
+            new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, category));
+        for (InvestmentProject p : list) {
+            if (p.getCity() == null || p.getCity().trim().isEmpty()) continue;
+            map.computeIfAbsent(normalizeProjectName(p.getProjectName()), k -> new HashSet<>())
+               .add(p.getCity().trim());
+        }
+        return map;
+    }
+
+    /** 甯傚窞涓�鑷存�ф牎楠岋細搴撲腑鍞竴褰掑睘甯傚窞涓庢湰娆¤В鏋愪笉涓�鑷存椂杩斿洖閿欒璇存槑锛屽惁鍒欒繑鍥� null */
+    private String investCityMismatchReason(InvestCityRow item, Map<String, Set<String>> knownCities) {
+        String parsed = item.project.getCity();
+        if (parsed == null || parsed.trim().isEmpty()) return null;
+        Set<String> known = knownCities.get(normalizeProjectName(item.project.getProjectName()));
+        if (known == null || known.size() != 1) return null;
+        String knownCity = known.iterator().next();
+        if (knownCity.equals(parsed.trim())) return null;
+        return item.fileName + " 绗� " + item.excelRow + " 琛岋紙" + item.project.getProjectName()
+            + "锛夛細甯傚窞涓�鑷存�ф牎楠屾湭閫氳繃鈥斺�斿簱涓椤圭洰褰掑睘銆�" + knownCity + "銆嶏紝鏈鏂囦欢瑙f瀽涓恒��" + parsed.trim()
+            + "銆嶏紝鐤戜技甯傚窞閿欎綅锛屽凡璺宠繃瀵煎叆锛岃鏍稿婧愭枃浠�";
+    }
+
+    private String cellStr(Row row, int idx) {
+        if (idx < 0) return null;
+        return getString(row, idx);
+    }
+
+    private Double cellNum(Row row, int idx) {
+        if (idx < 0) return 0.0;
+        return getDecimal(row, idx);
+    }
+
+    private Double cellNumOrNull(Row row, int idx) {
+        if (idx < 0) return null;
+        return getDecimalOrNull(row, idx);
+    }
+
+    /** 甯傚窞鍗曡〃琛岃褰曪細淇濈暀鏂囦欢 + Excel 琛屽彿鐢ㄤ簬澶辫触瀹氫綅 */
+    private static class InvestCityRow {
+        final int excelRow;
+        final String fileName;
+        final InvestmentProject project;
+        final InvestmentMonthly monthly;
+
+        InvestCityRow(int excelRow, String fileName, InvestmentProject project, InvestmentMonthly monthly) {
+            this.excelRow = excelRow;
+            this.fileName = fileName;
+            this.project = project;
+            this.monthly = monthly;
+        }
+    }
+    /** 鎶曡祫绯荤粺瀵煎嚭瀵煎叆锛堝鏍告瘮瀵瑰熀鍑嗭級锛屽浐瀹�8鍒楋細琛屽彿/鍗曚綅/鏃舵湡/璁″垝鎬绘姇璧�/鑷紑濮嬬疮璁�/鏈勾璁″垝/鑷勾鍒濈疮璁�/褰撴湀瀹屾垚 */
+    private static final int[] INVEST_SYSTEM_HEADER_COLS = {0, 1, 2, 3, 4, 5, 6, 7};
+    private static final String[] INVEST_SYSTEM_HEADER_KEYWORDS =
+        {"琛屽彿", "鍗曚綅", "鏃舵湡", "璁″垝鎬绘姇璧�", "鑷紑濮嬪缓璁�", "鏈勾璁″垝鎶曡祫", "鑷勾鍒�", "褰撴湀瀹屾垚鎶曡祫"};
+
+    public int importInvestmentSystem(MultipartFile file, String period) throws Exception {
+        List<InvestmentSystem> list = new ArrayList<>();
+        int failRows = 0;
+        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+            Sheet sheet = wb.getSheetAt(0);
+            requireHeaderKeyword(sheet, "鎶曡祫绯荤粺瀵煎嚭", "椤圭洰鍚嶇О", "璁″垝鎬绘姇璧�", "鍗曚綅");
+            requireHeaderLayout(sheet, "鎶曡祫绯荤粺瀵煎嚭", "妯℃澘_鏌ヨ缁撴灉(鎶曡祫绯荤粺-瀹㈣繍+鐗╂祦 2026.7).xls",
+                INVEST_SYSTEM_HEADER_COLS, INVEST_SYSTEM_HEADER_KEYWORDS);
+            for (int r = 1; r <= sheet.getLastRowNum(); r++) {
+                Row row = sheet.getRow(r);
+                if (row == null) continue;
+                String name = getString(row, 1);
+                if (name == null || name.trim().isEmpty()) continue;
+                InvestmentSystem sys = new InvestmentSystem();
+                sys.setReportPeriod(period);
+                sys.setProjectName(name.trim());
+                sys.setTotalInvestment(getDecimal(row, 3));
+                sys.setStartCum(getDecimal(row, 4));
+                sys.setYearPlan(getDecimal(row, 5));
+                sys.setYearCum(getDecimal(row, 6));
+                sys.setMonthDone(getDecimal(row, 7));
+                list.add(sys);
+            }
+        } catch (Exception e) {
+            log.error("Investment system parse error", e);
+            throw e;
+        }
+        investSystemMapper.delete(new LambdaQueryWrapper<InvestmentSystem>()
+            .eq(InvestmentSystem::getReportPeriod, period));
+        int success = 0;
+        for (InvestmentSystem sys : list) {
+            try {
+                investSystemMapper.insert(sys);
+                success++;
+            } catch (Exception e) {
+                failRows++;
+                log.error("Investment system insert error: {}", sys.getProjectName(), e);
+            }
+        }
+        recordBatch(file.getOriginalFilename(), "INVEST_SYSTEM", period, list.size(), success, failRows, null);
+        log.info("Investment system imported: {} rows for {}", success, period);
+        return success;
+    }
+
+    /** 鍩庡競鍏氦鏈堟姤瀵煎叆锛堝煄甯傚叕鍏变氦閫氭湀搴﹁繍钀ユ儏鍐碉紝浼佷笟绾ф槑缁嗭紝鍙栧叕鍏辨苯鐢佃溅瀛楁锛� */
+    /** 鍩庡競鍏氦锛圲203锛夋ā鏉垮叧閿垪浣嶏紙0-based锛夛細鎵�灞炲湴鍖�/浼佷笟浠g爜/浼佷笟鍚嶇О/杩愯惀杞︽暟/瀹㈣繍閲�/杞ㄩ亾瀹㈣繍閲�/杞浮瀹㈣繍閲�/骞冲潎杩愯窛/濉〃浜�/鑱旂郴鐢佃瘽/鎶ュ嚭鏃ユ湡 */
+    private static final int[] CITY_BUS_HEADER_COLS = {0, 1, 2, 5, 8, 13, 17, 19, 24, 25, 26};
+    private static final String[] CITY_BUS_HEADER_KEYWORDS =
+        {"鎵�灞炲湴鍖�", "浼佷笟浠g爜", "浼佷笟鍚嶇О", "鍏叡姹界數杞杩愯惀杞︽暟", "鍏叡姹界數杞瀹㈣繍閲�", "杞ㄩ亾浜ら�歘瀹㈣繍閲�",
+         "鍩庡競瀹㈣繍杞浮_瀹㈣繍閲�", "鍏叡姹界數杞﹀钩鍧囪繍璺�", "濉〃浜�", "鑱旂郴鐢佃瘽", "鎶ュ嚭鏃ユ湡"};
+
+    public ImportResult importCityBus(MultipartFile file, String period) throws Exception {
+        List<CityBusRow> rows = new ArrayList<>();
+        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+            Sheet sheet = wb.getSheetAt(0);
+            requireHeaderLayout(sheet, "鍩庡競鍏氦", "妯℃澘_浜や紒缁烾203琛╛鍩庡競鍏叡浜ら�氭湀搴﹁繍钀ユ儏鍐碉紙2026骞�7鏈堬級.xls",
+                CITY_BUS_HEADER_COLS, CITY_BUS_HEADER_KEYWORDS);
+            for (int r = 1; r <= sheet.getLastRowNum(); r++) {
+                Row row = sheet.getRow(r);
+                if (row == null) continue;
+                String name = getString(row, 2);
+                if (name == null || name.trim().isEmpty()) continue;
+                CityBusMonthly item = new CityBusMonthly();
+                item.setReportPeriod(period);
+                item.setRegionCode(getString(row, 0));
+                item.setCity(RegionUtil.cityByCode(getString(row, 0)));
+                item.setEnterpriseCode(getString(row, 1));
+                item.setEnterpriseName(name.trim());
+                item.setOpVehicles(getDecimal(row, 5));
+                item.setStopTotal(getDecimal(row, 6));
+                item.setStopCity(getDecimal(row, 7));
+                item.setPassengerVolume(getDecimal(row, 8));
+                item.setPassengerCity(getDecimal(row, 9));
+                item.setTurnover(getDecimal(row, 10));
+                item.setTurnoverCity(getDecimal(row, 11));
+                item.setRailPassengerVolume(getDecimal(row, 13));
+                item.setRailTurnover(getDecimal(row, 14));
+                item.setFerryPassengerVolume(getDecimal(row, 17));
+                item.setFerryTurnover(getDecimal(row, 18));
+                item.setAvgDistance(getDecimal(row, 19));
+                item.setAvgDistanceCity(getDecimal(row, 20));
+                item.setContactPerson(getString(row, 24));
+                item.setContactPhone(getString(row, 25));
+                item.setReportDate(getDateString(row, 26));
+                item.setRateOpVehiclesMom(getDecimal(row, 29));
+                item.setRatePassengerMom(getDecimal(row, 30));
+                item.setRatePassengerCityMom(getDecimal(row, 31));
+                item.setRateChengxiangMom(getDecimal(row, 32));
+                item.setRateChengxiangTurnoverMom(getDecimal(row, 33));
+                item.setAvgDistanceMomChange(getDecimal(row, 34));
+                item.setAvgDistanceCityMomChange(getDecimal(row, 35));
+                item.setDailyPassengerPerVehicle(getDecimal(row, 36));
+                item.setRateOpVehiclesYoy(getDecimal(row, 39));
+                item.setRatePassengerYoy(getDecimal(row, 40));
+                item.setRatePassengerCityYoy(getDecimal(row, 41));
+                item.setRateChengxiangYoy(getDecimal(row, 42));
+                item.setRateChengxiangTurnoverYoy(getDecimal(row, 43));
+                item.setStopCityRatio(getDecimal(row, 44));
+                item.setPassengerCityRatio(getDecimal(row, 45));
+                item.setChengxiangRatio(getDecimal(row, 46));
+                item.setRateTurnoverYoy(getDecimal(row, 47));
+                item.setRateTurnoverMom(getDecimal(row, 48));
+                item.setPassengerChengxiang(getDecimal(row, 49));
+                item.setTurnoverChengxiang(getDecimal(row, 50));
+                item.setAvgDistanceChengxiang(getDecimal(row, 51));
+                item.setVerifyExplanation(getString(row, 65));
+                item.setReportNote(getString(row, 66));
+                item.setModifyRecord(getString(row, 67));
+                rows.add(new CityBusRow(r + 1, item));
+            }
+        } catch (Exception e) {
+            log.error("CityBus import parse error: {}", e.getMessage());
+            throw e;
+        }
+        // 骞傜瓑锛氬悓鎶ヨ〃鏈熷厛娓呮棫鏁版嵁涓� CITY_BUS 瀹℃牳缁撴灉
+        cityBusMapper.delete(new LambdaQueryWrapper<CityBusMonthly>()
+            .eq(CityBusMonthly::getReportPeriod, period));
+        List<Long> busRuleIds = new ArrayList<>();
+        for (com.trafficaudit.rulemanage.entity.AuditRule rule : ruleMapper.selectList(null)) {
+            if ("CITY_BUS".equals(rule.getReportType())) busRuleIds.add(rule.getId());
+        }
+        if (!busRuleIds.isEmpty()) {
+            auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
+                .eq(AuditResult::getReportPeriod, period)
+                .in(AuditResult::getRuleId, busRuleIds));
+        }
+        int success = 0;
+        List<String> failDetails = new ArrayList<>();
+        for (CityBusRow item : rows) {
+            try {
+                cityBusMapper.insert(item.entity);
+                success++;
+            } catch (Exception e) {
+                Throwable cause = e;
+                while (cause.getCause() != null) cause = cause.getCause();
+                String reason = cause.getMessage();
+                if (reason == null || reason.trim().isEmpty()) reason = e.getMessage();
+                failDetails.add("绗� " + item.excelRow + " 琛岋紙" + item.entity.getEnterpriseName() + "锛夛細" + reason);
+                log.error("CityBus insert error: row {}", item.excelRow, e);
+            }
+        }
+        String errorDetail = String.join("\n", failDetails);
+        if (errorDetail.length() > 4000) errorDetail = errorDetail.substring(0, 4000) + "\n鈥︹��";
+        recordBatch(file.getOriginalFilename(), "CITY_BUS", period, rows.size(), success, failDetails.size(), errorDetail);
+        log.info("CityBus imported: {} rows for {}", success, period);
+        return new ImportResult(success, failDetails.size(), failDetails);
+    }
+
+    /** 鍩庡競鍏氦琛岃褰曪細淇濈暀 Excel 琛屽彿鐢ㄤ簬澶辫触瀹氫綅 */
+    private static class CityBusRow {
+        final int excelRow;
+        final CityBusMonthly entity;
+
+        CityBusRow(int excelRow, CityBusMonthly entity) {
+            this.excelRow = excelRow;
+            this.entity = entity;
+        }
+    }
+
+    /** 宸℃父鍑虹鏈堟姤瀵煎叆锛堝競宸炵骇锛�17 琛岋紱鍚牳瀹炶鏄�/涓婃姤璇存槑/淇敼璁板綍锛� */
+    /** 宸℃父鍑虹锛圲402锛夋ā鏉垮叧閿垪浣嶏紙0-based锛夛細鎵�灞炲湴鍖�/浼佷笟浠g爜/浼佷笟鍚嶇О/杞藉杞︽鎬绘暟/瀹㈣繍閲�/鏃呭鍛ㄨ浆閲�/杩愯惀杞﹁締鏁�/骞冲潎杩愯窛/濉〃浜�/鑱旂郴鐢佃瘽/鎶ュ嚭鏃ユ湡 */
+    private static final int[] CITY_TAXI_HEADER_COLS = {0, 1, 2, 4, 6, 8, 10, 11, 15, 16, 17};
+    private static final String[] CITY_TAXI_HEADER_KEYWORDS =
+        {"鎵�灞炲湴鍖�", "浼佷笟浠g爜", "浼佷笟鍚嶇О", "杞藉杞︽鎬绘暟", "瀹㈣繍閲�", "鏃呭鍛ㄨ浆閲�", "杩愯惀杞﹁締鏁�",
+         "宸℃父鍑虹姹借溅骞冲潎杩愯窛", "濉〃浜�", "鑱旂郴鐢佃瘽", "鎶ュ嚭鏃ユ湡"};
+
+    public ImportResult importCityTaxi(MultipartFile file, String period) throws Exception {
+        List<TaxiRow> rows = new ArrayList<>();
+        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+            Sheet sheet = wb.getSheetAt(0);
+            requireHeaderLayout(sheet, "宸℃父鍑虹", "妯℃澘_浜よ缁烾402琛�-宸℃父鍑虹姹借溅杩愯惀鏈嶅姟鎯呭喌鏈堟姤(2026).xls",
+                CITY_TAXI_HEADER_COLS, CITY_TAXI_HEADER_KEYWORDS);
+            for (int r = 1; r <= sheet.getLastRowNum(); r++) {
+                Row row = sheet.getRow(r);
+                if (row == null) continue;
+                String name = getString(row, 2);
+                if (name == null || name.trim().isEmpty()) continue;
+                CityTaxiMonthly item = new CityTaxiMonthly();
+                item.setReportPeriod(period);
+                item.setRegionCode(getString(row, 0));
+                item.setCity(cityOf(getString(row, 0), name));
+                item.setTripTotal(getDecimal(row, 4));
+                item.setTripCity(getDecimal(row, 5));
+                item.setPassengerVolume(getDecimal(row, 6));
+                item.setPassengerCity(getDecimal(row, 7));
+                item.setTurnover(getDecimal(row, 8));
+                item.setTurnoverCity(getDecimal(row, 9));
+                item.setOpVehicles(getDecimal(row, 10));
+                item.setAvgDistance(getDecimal(row, 11));
+                item.setAvgDistanceCity(getDecimal(row, 12));
+                item.setContactPerson(getString(row, 15));
+                item.setContactPhone(getString(row, 16));
+                item.setReportDate(getDateString(row, 17));
+                item.setDailyTripsPerVehicle(getDecimal(row, 18));
+                item.setDailyPassengerPerVehicle(getDecimal(row, 19));
+                item.setPassengersPerTrip(getDecimal(row, 20));
+                item.setRateOpVehiclesMom(getDecimal(row, 21));
+                item.setRateOpVehiclesYoy(getDecimal(row, 22));
+                item.setRateTripMom(getDecimal(row, 23));
+                item.setRateTripYoy(getDecimal(row, 24));
+                item.setRatePassengerMom(getDecimal(row, 25));
+                item.setRatePassengerYoy(getDecimal(row, 26));
+                item.setAvgDistanceMomChange(getDecimal(row, 27));
+                item.setPassengersPerTripCity(getDecimal(row, 28));
+                item.setRateTripCityMom(getDecimal(row, 29));
+                item.setRatePassengerCityMom(getDecimal(row, 30));
+                item.setAvgDistanceCityMomChange(getDecimal(row, 31));
+                item.setTripCityRatio(getDecimal(row, 32));
+                item.setPassengerCityRatio(getDecimal(row, 33));
+                item.setPassengersPerTripChengxiang(getDecimal(row, 34));
+                item.setRateTripChengxiangMom(getDecimal(row, 35));
+                item.setRatePassengerChengxiangMom(getDecimal(row, 36));
+                item.setAvgDistanceChengxiang(getDecimal(row, 37));
+                item.setVerifyExplanation(getString(row, 38));
+                item.setReportNote(getString(row, 39));
+                item.setModifyRecord(getString(row, 40));
+                rows.add(new TaxiRow(r + 1, item));
+            }
+        } catch (Exception e) {
+            log.error("CityTaxi import parse error: {}", e.getMessage());
+            throw e;
+        }
+        // 骞傜瓑锛氬悓鎶ヨ〃鏈熷厛娓呮棫鏁版嵁涓� CITY_TAXI 瀹℃牳缁撴灉
+        cityTaxiMapper.delete(new LambdaQueryWrapper<CityTaxiMonthly>()
+            .eq(CityTaxiMonthly::getReportPeriod, period));
+        deleteAuditResults(period, "CITY_TAXI");
+        int success = 0;
+        List<String> failDetails = new ArrayList<>();
+        for (TaxiRow item : rows) {
+            try {
+                cityTaxiMapper.insert(item.entity);
+                success++;
+            } catch (Exception e) {
+                Throwable cause = e;
+                while (cause.getCause() != null) cause = cause.getCause();
+                String reason = cause.getMessage();
+                if (reason == null || reason.trim().isEmpty()) reason = e.getMessage();
+                failDetails.add("绗� " + item.excelRow + " 琛岋紙" + item.entity.getCity() + "锛夛細" + reason);
+                log.error("CityTaxi insert error: row {}", item.excelRow, e);
+            }
+        }
+        String errorDetail = String.join("\n", failDetails);
+        if (errorDetail.length() > 4000) errorDetail = errorDetail.substring(0, 4000) + "\n鈥︹��";
+        recordBatch(file.getOriginalFilename(), "CITY_TAXI", period, rows.size(), success, failDetails.size(), errorDetail);
+        log.info("CityTaxi imported: {} rows for {}", success, period);
+        return new ImportResult(success, failDetails.size(), failDetails);
+    }
+
+    /** 鍑虹杞﹁繍鏀胯溅杈嗕俊鎭鍏ワ紙杞﹁締绾ф槑缁嗭紝鎸夊競宸炲綊灞炵粺璁$敤锛� */
+    public synchronized int importCityTaxiAuth(MultipartFile file, String period) throws Exception {
+        List<CityTaxiAuth> list = new ArrayList<>();
+        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+            Sheet sheet = wb.getSheetAt(0);
+            requireHeaderKeyword(sheet, "鍑虹杞﹁繍鏀胯溅杈�", "杞︾墝");
+            for (int r = 1; r <= sheet.getLastRowNum(); r++) {
+                Row row = sheet.getRow(r);
+                if (row == null) continue;
+                String plate = getString(row, 0);
+                if (plate == null || plate.trim().isEmpty()) continue;
+                CityTaxiAuth item = new CityTaxiAuth();
+                item.setReportPeriod(period);
+                item.setCity(taxiCity(getString(row, 9), getString(row, 8), getString(row, 4), getString(row, 18)));
+                item.setPlateNo(plate.trim());
+                item.setVehicleType(getString(row, 1));
+                item.setBizType(getString(row, 2));
+                item.setOwnerName(getString(row, 3));
+                item.setLicenseNo(getString(row, 4));
+                item.setFuelType(getString(row, 7));
+                item.setArchiveNo(getString(row, 8));
+                item.setRoadCertNo(getString(row, 9));
+                item.setOperatorName(getString(row, 17));
+                item.setOperatorAddr(getString(row, 18));
+                item.setOpStatus(getString(row, 19));
+                item.setCertStatus(getString(row, 20));
+                list.add(item);
+            }
+        } catch (Exception e) {
+            log.error("CityTaxiAuth import parse error", e);
+            throw e;
+        }
+        // 骞傜瓑锛氬悓鎶ヨ〃鏈熷厛娓呮棫鏁版嵁锛堣繍鏀胯溅杈嗛噺澶э紝鎵归噺娓呯悊锛�
+        cityTaxiAuthMapper.delete(new LambdaQueryWrapper<CityTaxiAuth>()
+            .eq(CityTaxiAuth::getReportPeriod, period));
+        deleteAuditResults(period, "CITY_TAXI");
+        // 鎵归噺鎻掑叆锛圔ATCH 妯″紡锛夛細42636 琛岀害 20~40 绉掞紝閫愭潯鎻掑叆闇�绾� 6 鍒嗛挓浼氳Е鍙戝墠绔秴鏃�
+        int success = 0;
+        int fail = 0;
+        try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
+            CityTaxiAuthMapper batchMapper = session.getMapper(CityTaxiAuthMapper.class);
+            for (CityTaxiAuth item : list) {
+                batchMapper.insert(item);
+            }
+            session.flushStatements();
+            session.commit();
+            success = list.size();
+        } catch (Exception e) {
+            fail = list.size();
+            log.error("CityTaxiAuth batch insert error", e);
+        }
+        recordBatch(file.getOriginalFilename(), "CITY_TAXI_AUTH", period, list.size(), success, fail, null);
+        log.info("CityTaxiAuth imported: {} rows for {}", success, period);
+        return success;
+    }
+
+
+    // ========== 缃戠害杞︽湀搴﹁緭鍏ュ鍏ワ紙鍏ㄧ渷 pin / 璁㈠崟锛� ==========
+
+    /** 缃戠害杞︽�绘暟鏈堝害杈撳叆锛氭ā鏉縚缃戠害杞︽�绘暟.xlsx锛坰heet1锛汚 鍒�=鏈堜唤鏃ユ湡锛汢=鍏ㄧ渷瀹㈣繍閲� C=鍏朵腑鍩庡競鍐� D=鍛ㄨ浆閲� E=鍏朵腑鍩庡競鍐呭懆杞噺锛夈�傛暣琛ㄦ寜鎶ヨ〃鏈熻鐩栥�� */
+    public synchronized int importWycTotal(MultipartFile file, String period) throws Exception {
+        Map<String, WycTotalMonthly> rows = new LinkedHashMap<>();
+        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+            Sheet sheet = wb.getSheetAt(0);
+            for (int r = 2; r <= sheet.getLastRowNum(); r++) {
+                Row row = sheet.getRow(r);
+                if (row == null) continue;
+                String rp = periodOfCell(row.getCell(0));
+                if (rp == null) continue;
+                double pk = dbl(row, 1), pck = dbl(row, 2), pz = dbl(row, 3), pcz = dbl(row, 4);
+                if (pk <= 0 && pck <= 0 && pz <= 0 && pcz <= 0) continue;
+                WycTotalMonthly item = new WycTotalMonthly();
+                item.setReportPeriod(rp);
+                item.setPinK(pk > 0 ? pk : null);
+                item.setPinCK(pck > 0 ? pck : null);
+                item.setPinZ(pz > 0 ? pz : null);
+                item.setPinCZ(pcz > 0 ? pcz : null);
+                rows.put(rp, item);
+            }
+        }
+        if (rows.isEmpty()) throw new RuntimeException("鏈瘑鍒埌鏈夋晥鏈堜唤琛岋細A 鍒楀簲涓烘棩鏈燂紙濡� 2026-07锛夛紝B-E 涓哄叏鐪佸杩愰噺/鍩庡競鍐�/鍛ㄨ浆閲�/鍩庡競鍐呭懆杞噺");
+        int ok = upsertWycTotal(rows);
+        log.info("WycTotal imported: {} periods ok={}", rows.size(), ok);
+        return ok;
+    }
+
+    /** 缃戠害杞﹁鍗曞強鍏ㄧ渷鎬婚噺锛氭ā鏉縚缃戠害杞﹁鍗曞強鍏ㄧ渷鎬婚噺.xlsx 棣栦釜鏁版嵁 sheet锛圓=鏈堜唤锛汢-E=鍏ㄧ渷 pin锛汧=璁㈠崟鍚堣锛汫-W=17 甯傚窞璁㈠崟锛� */
+    public synchronized int importWycOrder(MultipartFile file, String period) throws Exception {
+        Map<String, WycOrderMonthly> rows = new LinkedHashMap<>();
+        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+            Sheet sheet = wb.getSheetAt(0);
+            int head = -1;
+            for (int r = 0; r <= Math.min(sheet.getLastRowNum(), 4); r++) {
+                Row row = sheet.getRow(r);
+                if (row == null) continue;
+                String a = getString(row, 0);
+                if (a != null && a.contains("鏈堜唤")) { head = r; break; }
+            }
+            if (head < 0) throw new RuntimeException("鏈壘鍒拌〃澶达細A 鍒楅渶鍚�滄湀浠解��");
+            for (int r = head + 1; r <= sheet.getLastRowNum(); r++) {
+                Row row = sheet.getRow(r);
+                if (row == null) continue;
+                String rp = periodOfCell(row.getCell(0));
+                if (rp == null) continue;
+                double pk = dbl(row, 1), pck = dbl(row, 2), pz = dbl(row, 3), pcz = dbl(row, 4), sum = dbl(row, 5);
+                double citySum = 0;
+                for (int k = 0; k < 17; k++) citySum += dbl(row, 6 + k);
+                if (pk <= 0 && pck <= 0 && pz <= 0 && pcz <= 0 && sum <= 0 && citySum <= 0) continue;
+                WycOrderMonthly it = new WycOrderMonthly();
+                it.setReportPeriod(rp);
+                it.setPinK(pk > 0 ? pk : null);
+                it.setPinCK(pck > 0 ? pck : null);
+                it.setPinZ(pz > 0 ? pz : null);
+                it.setPinCZ(pcz > 0 ? pcz : null);
+                if (sum <= 0 && citySum > 0) sum = citySum;
+                it.setOrderSum(sum > 0 ? sum : null);
+                it.setOrderWuhan(dblN(row, 6));
+                it.setOrderHuangshi(dblN(row, 7));
+                it.setOrderShiyan(dblN(row, 8));
+                it.setOrderYichang(dblN(row, 9));
+                it.setOrderXiangyang(dblN(row, 10));
+                it.setOrderEzhou(dblN(row, 11));
+                it.setOrderJingmen(dblN(row, 12));
+                it.setOrderXiaogan(dblN(row, 13));
+                it.setOrderJingzhou(dblN(row, 14));
+                it.setOrderHuanggang(dblN(row, 15));
+                it.setOrderXianning(dblN(row, 16));
+                it.setOrderSuizhou(dblN(row, 17));
+                it.setOrderEnshi(dblN(row, 18));
+                it.setOrderXiantao(dblN(row, 19));
+                it.setOrderQianjiang(dblN(row, 20));
+                it.setOrderTianmen(dblN(row, 21));
+                it.setOrderShennong(dblN(row, 22));
+                rows.put(rp, it);
+            }
+        }
+        if (rows.isEmpty()) throw new RuntimeException("鏈瘑鍒埌鏈夋晥鏈堜唤琛岋細A 鍒楀簲涓� yyyy-MM锛孊-E 鍏ㄧ渷 pin / F 鍚堣 / G-W 17 甯傚窞璁㈠崟");
+        int ok = upsertWyc(rows);
+        log.info("WycOrder imported: {} periods ok={}", rows.size(), ok);
+        return ok;
+    }
+
+    private int upsertWyc(Map<String, WycOrderMonthly> rows) {
+        int ok = 0, fail = 0;
+        for (WycOrderMonthly item : rows.values()) {
+            try {
+                wycOrderMapper.delete(new LambdaQueryWrapper<WycOrderMonthly>().eq(WycOrderMonthly::getReportPeriod, item.getReportPeriod()));
+                wycOrderMapper.insert(item);
+                recordBatch("缃戠害杞﹁鍗曞強鍏ㄧ渷鎬婚噺", "WYC_ORDER", item.getReportPeriod(), 1, 1, 0, null);
+                ok++;
+            } catch (Exception e) {
+                fail++;
+                log.error("WycOrder insert error period={}", item.getReportPeriod(), e);
+            }
+        }
+        return ok;
+    }
+
+    private int upsertWycTotal(Map<String, WycTotalMonthly> rows) {
+        int ok = 0, fail = 0;
+        for (WycTotalMonthly item : rows.values()) {
+            try {
+                wycTotalMapper.delete(new LambdaQueryWrapper<WycTotalMonthly>().eq(WycTotalMonthly::getReportPeriod, item.getReportPeriod()));
+                wycTotalMapper.insert(item);
+                recordBatch("缃戠害杞︽�绘暟", "WYC_TOTAL", item.getReportPeriod(), 1, 1, 0, null);
+                ok++;
+            } catch (Exception e) {
+                fail++;
+                log.error("WycTotal insert error period={}", item.getReportPeriod(), e);
+            }
+        }
+        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) {
+            try {
+                double v = c.getNumericCellValue();
+                if (v >= 40000 && v <= 60000) {
+                    java.util.Date d = org.apache.poi.ss.usermodel.DateUtil.getJavaDate(v);
+                    java.text.SimpleDateFormat f = new java.text.SimpleDateFormat("yyyy-MM");
+                    return f.format(d);
+                }
+            } catch (Exception ignore) { }
+        }
+        String s = null;
+        try { s = c.getStringCellValue(); } catch (Exception ignore) { }
+        if (s != null) {
+            java.util.regex.Matcher m = java.util.regex.Pattern.compile("(\\d{4})[-/.骞碷(\\d{1,2})").matcher(s.trim());
+            if (m.find()) return String.format("%04d-%02d", Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)));
+        }
+        return null;
+    }
+
+    private double dbl(Row row, int idx) {
+        Double v = getDecimal(row, idx);
+        return v == null ? 0.0 : v;
+    }
+
+    private Double dblN(Row row, int idx) {
+        Double v = getDecimal(row, idx);
+        return (v == null || v <= 0) ? null : v;
+    }
+
+    /** 杩愭斂鍑虹杞﹀競宸炴彁鍙栵細閬撹矾杩愯緭璇佸瓧鍙封啋妗f鍙封啋缁忚惀鏉冨彿鈫掍笟鎴峰湴鍧�锛屽彇 6 浣嶅尯鍒掍唬鐮� */
+    private static final java.util.regex.Pattern TAXI_REGION_PAT =
+        java.util.regex.Pattern.compile("(4290\\d{2}|42(?:0[1-9]|1[0-3]|28)\\d{2})");
+
+    private String taxiCity(String... fields) {
+        for (String f : fields) {
+            if (f == null) continue;
+            java.util.regex.Matcher m = TAXI_REGION_PAT.matcher(f);
+            if (m.find()) {
+                String city = RegionUtil.cityByCode(m.group(1));
+                if (city != null) return city;
+            }
+        }
+        return null;
+    }
+
+    /** 甯傚窞褰掑睘锛氫紭鍏堟寜鍖哄垝浠g爜璇嗗埆锛屽叾娆℃寜鍚嶇О瑙勮寖鍖� */
+    private String cityOf(String regionCode, String name) {
+        String city = RegionUtil.cityByCode(regionCode);
+        if (city != null) return city;
+        return RegionUtil.normalizeCityName(name);
+    }
+
+    /** 娓呴櫎鏌愭姤琛ㄧ被鍨嬪湪鎸囧畾鎶ヨ〃鏈熺殑瀹℃牳缁撴灉锛堝鍏ュ箓绛夛級 */
+    private void deleteAuditResults(String period, String reportType) {
+        List<Long> ruleIds = new ArrayList<>();
+        for (com.trafficaudit.rulemanage.entity.AuditRule rule : ruleMapper.selectList(null)) {
+            if (reportType.equals(rule.getReportType())) ruleIds.add(rule.getId());
+        }
+        if (!ruleIds.isEmpty()) {
+            auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
+                .eq(AuditResult::getReportPeriod, period)
+                .in(AuditResult::getRuleId, ruleIds));
+        }
+    }
+
+    /** 宸℃父鍑虹琛岃褰曪細淇濈暀 Excel 琛屽彿鐢ㄤ簬澶辫触瀹氫綅 */
+    private static class TaxiRow {
+        final int excelRow;
+        final CityTaxiMonthly entity;
+
+        TaxiRow(int excelRow, CityTaxiMonthly entity) {
+            this.excelRow = excelRow;
+            this.entity = entity;
+        }
+    }
+
+    private Sheet pickInvestmentSheet(Workbook wb, boolean passenger) {
+        if (passenger) {
+            for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+                String name = wb.getSheetName(i);
+                if (name != null && name.contains("鏄庣粏")) return wb.getSheetAt(i);
+            }
+            return wb.getSheetAt(0);
+        }
+        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+            String name = wb.getSheetName(i);
+            if (name != null && name.contains("鍒嗛」鐩姇璧�")) return wb.getSheetAt(i);
+        }
+        return wb.getSheetAt(0);
+    }
+
+    /** 甯傚窞灏忚妭琛岃瘑鍒細鐭悕/瑙勮寖鍚� -> 瑙勮寖鍚嶏紝闈炲競宸炶杩斿洖 null */
+    private String investmentCity(String a) {
+        if (a == null) return null;
+        String full = INVEST_CITY_SHORT_TO_FULL.get(a);
+        if (full != null) return full;
+        String norm = RegionUtil.normalizeCityName(a);
+        if (RegionUtil.CITY_LIST.contains(norm)) return norm;
+        return null;
+    }
+
+    /** 寮�宸�/寤烘垚鏃堕棿锛氭暟瀛楋紙2025 鎴� 202501锛夋垨鏂囨湰缁熶竴涓� yyyy 鎴� yyyyMM */
+    private String investTime(String v) {
+        if (v == null || v.trim().isEmpty()) return null;
+        String t = v.trim();
+        if (t.matches("\\d+\\.0")) t = t.substring(0, t.length() - 2);
+        if (t.matches("\\d{4}")) return t;
+        if (t.matches("\\d{6}")) return t;
+        if (t.matches("\\d{4}-\\d{2}-\\d{2}")) return t;
+        return t;
+    }
+
+    /** 鎶曡祫琛岃褰曪細淇濈暀 Excel 琛屽彿鐢ㄤ簬澶辫触瀹氫綅 */
+    private static class InvestRow {
+        final int excelRow;
+        final InvestmentProject project;
+        final InvestmentMonthly monthly;
+
+        InvestRow(int excelRow, InvestmentProject project, InvestmentMonthly monthly) {
+            this.excelRow = excelRow;
+            this.project = project;
+            this.monthly = monthly;
+        }
+    }
+
+    // ========== 閫氱敤鏂囦欢澶规壒閲忓鍏ワ紙鏈嶅姟绔洰褰曟祻瑙堝櫒锛� ==========
+
+    /**
+     * 鐩綍娴忚鍣細path 涓虹┖鏃惰繑鍥為」鐩� docs 鏁版嵁鐩綍涓嬬殑瀛愮洰褰曪紱
+     * 杩斿洖褰撳墠鐩綍缁濆璺緞銆佷笂绾х洰褰曘�佸瓙鐩綍鍒楄〃鍙婂叾涓洿鎺ュ寘鍚殑 Excel 鏂囦欢鍚�/鏁伴噺銆�
+     */
+    public Map<String, Object> listDirs(String path) throws Exception {
+        File cur;
+        if (path == null || path.trim().isEmpty()) {
+            File docs = resolveDocsDir();
+            if (!docs.isDirectory()) {
+                throw new RuntimeException("鏈壘鍒版暟鎹洰褰� docs锛堝綋鍓嶈繍琛岀洰褰曪細" + System.getProperty("user.dir") + "锛�");
+            }
+            cur = docs;
+        } else {
+            cur = new File(path);
+            if (!cur.isDirectory()) {
+                throw new RuntimeException("鐩綍涓嶅瓨鍦ㄦ垨涓嶆槸鏂囦欢澶癸細" + path);
+            }
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("current", cur.getAbsolutePath());
+        result.put("parent", cur.getParent() == null ? "" : cur.getParent());
+        List<Map<String, Object>> dirs = new ArrayList<>();
+        File[] subs = cur.listFiles(File::isDirectory);
+        int dirTotal = 0;
+        if (subs != null) {
+            Arrays.sort(subs, Comparator.comparing(File::getName));
+            for (File d : subs) {
+                if (d.isHidden()) continue;
+                dirTotal++;
+                if (dirs.size() >= 300) continue;
+                Map<String, Object> m = new LinkedHashMap<>();
+                m.put("name", d.getName());
+                m.put("path", d.getAbsolutePath());
+                dirs.add(m);
+            }
+        }
+        result.put("dirs", dirs);
+        result.put("dirsTruncated", dirTotal > dirs.size());
+        List<String> excelFiles = new ArrayList<>();
+        File[] files = cur.listFiles((d, n) -> {
+            String lower = n.toLowerCase();
+            return lower.endsWith(".xlsx") || lower.endsWith(".xls");
+        });
+        int excelTotal = files == null ? 0 : files.length;
+        if (files != null) {
+            Arrays.sort(files, Comparator.comparing(File::getName));
+            int take = Math.min(files.length, 300);
+            for (int i = 0; i < take; i++) excelFiles.add(files[i].getName());
+        }
+        result.put("excelCount", excelTotal);
+        result.put("files", excelFiles);
+        result.put("filesTruncated", excelTotal > excelFiles.size());
+        return result;
+    }
+
+    /** 鍚戜笂鏌ユ壘椤圭洰 docs 鏁版嵁鐩綍锛堝吋瀹逛粠 IDEA 浠� server 妯″潡鐩綍鍚姩鐨勬儏鍐碉級 */
+    private File resolveDocsDir() {
+        File base = new File(System.getProperty("user.dir"));
+        File cur = base;
+        for (int i = 0; i < 4 && cur != null; i++) {
+            File candidate = new File(cur, "docs");
+            if (candidate.isDirectory()) return candidate;
+            cur = cur.getParentFile();
+        }
+        return new File(base, "docs");
+    }
+
+    /**
+     * 浠庢湇鍔$鏂囦欢澶规壒閲忓鍏ワ細鍙壂鎻� dir 鐩存帴鍖呭惈鐨� .xlsx/.xls锛�
+     * 鎸� type 閫愪釜璋冪敤瀵瑰簲瀵煎叆鏂规硶锛堜笌涓婁紶瀵煎叆鍚屼竴濂楄В鏋愰�昏緫锛夛紝杩斿洖閫愭枃浠剁粨鏋溿��
+     */
+    public Map<String, Object> importFromDir(String type, String period, boolean skipImported, String dir) throws Exception {
+        File folder = new File(dir);
+        if (!folder.isDirectory()) {
+            throw new RuntimeException("鐩綍涓嶅瓨鍦ㄦ垨涓嶆槸鏂囦欢澶癸細" + dir);
+        }
+        File[] files = folder.listFiles((d, n) -> {
+            String lower = n.toLowerCase();
+            return lower.endsWith(".xlsx") || lower.endsWith(".xls");
+        });
+        if (files == null || files.length == 0) {
+            throw new RuntimeException("鐩綍涓病鏈� Excel 鏂囦欢锛�.xlsx/.xls锛夛細" + folder.getAbsolutePath());
+        }
+        Arrays.sort(files, Comparator.comparing(File::getName));
+        List<Map<String, Object>> fileResults = new ArrayList<>();
+        int totalSuccess = 0, totalFail = 0;
+        for (File f : files) {
+            Map<String, Object> fr = new LinkedHashMap<>();
+            fr.put("fileName", f.getName());
+            try {
+                String filePeriod = detectPeriod(f.getName(), period);
+                if (filePeriod == null || filePeriod.trim().isEmpty()) {
+                    throw new RuntimeException("鏃犳硶浠庢枃浠跺悕璇嗗埆鎶ヨ〃鏈燂紙鏀寔 2026骞�7鏈� / 2026.7 / 2026-07 / 7鏈堬級锛屼篃鏈�夋嫨鎶ヨ〃鏈燂細" + f.getName());
+                }
+                fr.put("period", filePeriod);
+                if (skipImported && alreadyImported(filePeriod, f.getName())) {
+                    fr.put("skipped", true);
+                    fr.put("success", 0);
+                    fr.put("fail", 0);
+                    totalFail++;
+                    fileResults.add(fr);
+                    continue;
+                }
+                byte[] bytes;
+                try (InputStream is = new FileInputStream(f)) {
+                    bytes = IoUtil.readBytes(is);
+                }
+                MultipartFile mf = new ByteArrayMultipartFile(f.getName(), bytes);
+                ImportResult r = importDirFile(type, mf, filePeriod);
+                fr.put("success", r.getSuccess());
+                fr.put("fail", r.getFail());
+                if (r.getFail() > 0 && r.getFailDetails() != null && !r.getFailDetails().isEmpty()) {
+                    String d = r.getFailDetails().get(0);
+                    if (d.length() > 300) d = d.substring(0, 300);
+                    fr.put("error", d);
+                }
+                totalSuccess += r.getSuccess();
+                totalFail += r.getFail();
+            } catch (Exception e) {
+                String msg = e.getMessage() == null ? e.toString() : e.getMessage();
+                if (msg.length() > 300) msg = msg.substring(0, 300);
+                fr.put("success", 0);
+                fr.put("fail", 0);
+                fr.put("error", msg);
+                totalFail++;
+            }
+            fileResults.add(fr);
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("type", type);
+        result.put("total", fileResults.size());
+        result.put("success", totalSuccess);
+        result.put("fail", totalFail);
+        result.put("files", fileResults);
+        return result;
+    }
+
+    /**
+     * 鎸夋枃浠惰矾寰勫鍏ュ崟涓� Excel锛堜緵鍓嶇閫愭枃浠跺鍏ユ樉绀鸿繘搴︼級銆�
+     * 鎶ヨ〃鏈熶紭鍏堜粠鏂囦欢鍚嶈瘑鍒紙濡� 2026骞�7鏈� / 2026.7 / 2026-07 / 7鏈堬級锛岃瘑鍒笉鍒版墠鐢� period銆�
+     */
+    public Map<String, Object> importDirFileByPath(String type, String period, boolean skipImported, String filePath) throws Exception {
+        File f = new File(filePath);
+        if (!f.isFile()) {
+            throw new RuntimeException("鏂囦欢涓嶅瓨鍦細" + filePath);
+        }
+        String filePeriod = detectPeriod(f.getName(), period);
+        if (filePeriod == null || filePeriod.trim().isEmpty()) {
+            throw new RuntimeException("鏃犳硶浠庢枃浠跺悕璇嗗埆鎶ヨ〃鏈燂紙鏀寔 2026骞�7鏈� / 2026.7 / 2026-07 / 7鏈堬級锛屼篃鏈�夋嫨鎶ヨ〃鏈燂細" + f.getName());
+        }
+        Map<String, Object> fr = new LinkedHashMap<>();
+        fr.put("fileName", f.getName());
+        fr.put("period", filePeriod);
+        if (skipImported && alreadyImported(filePeriod, f.getName())) {
+            fr.put("skipped", true);
+            fr.put("success", 0);
+            fr.put("fail", 0);
+            fr.put("error", "璇ユ姤琛ㄦ湡宸插鍏ヨ繃姝ゆ枃浠讹紝宸茶烦杩囷紙濡傞渶瑕嗙洊璇峰彇娑堝嬀閫夆�滆烦杩囧凡瀵煎叆鏂囦欢鈥濓級");
+            return fr;
+        }
+        byte[] bytes;
+        try (InputStream is = new FileInputStream(f)) {
+            bytes = IoUtil.readBytes(is);
+        }
+        ImportResult r = importDirFile(type, new ByteArrayMultipartFile(f.getName(), bytes), filePeriod);
+        fr.put("success", r.getSuccess());
+        fr.put("fail", r.getFail());
+        if (r.getFail() > 0 && r.getFailDetails() != null && !r.getFailDetails().isEmpty()) {
+            String d = r.getFailDetails().get(0);
+            if (d.length() > 300) d = d.substring(0, 300);
+            fr.put("error", d);
+        }
+        return fr;
+    }
+
+    /** 璇ユ姤琛ㄦ湡鏄惁宸叉垚鍔熷鍏ヨ繃鍚屽悕鏂囦欢锛堢敤浜庢枃浠跺す閲嶅瀵煎叆鏃惰烦杩囷級 */
+    private boolean alreadyImported(String period, String fileName) {
+        if (period == null || fileName == null) return false;
+        Long cnt = importBatchMapper.selectCount(new LambdaQueryWrapper<ImportBatch>()
+            .eq(ImportBatch::getReportPeriod, period)
+            .eq(ImportBatch::getFileName, fileName)
+            .eq(ImportBatch::getFailRows, 0));
+        return cnt != null && cnt > 0;
+    }
+
+    /** 浠庢枃浠跺悕璇嗗埆鎶ヨ〃鏈燂細2026骞�7鏈� / 2026骞�07鏈� / 2026.7 / 2026-07 / 202607 / 7鏈堬紙鏃犲勾浠界敤 period 骞翠唤鎴栧綋鍓嶅勾浠斤級 */
+    private String detectPeriod(String fileName, String defaultPeriod) {
+        if (fileName == null) return defaultPeriod;
+        String name = fileName.replace("锛�", "(").replace("锛�", ")").replace(" ", "");
+        java.util.regex.Matcher m;
+        m = java.util.regex.Pattern.compile("(\\d{4})\\s*骞碶\s*(\\d{1,2})\\s*鏈�").matcher(name);
+        if (m.find()) return String.format("%04d-%02d", Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)));
+        m = java.util.regex.Pattern.compile("(\\d{4})[.\\-/\\s](\\d{1,2})").matcher(name);
+        if (m.find()) return String.format("%04d-%02d", Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)));
+        m = java.util.regex.Pattern.compile("(?:^|[^\\d])(\\d{4})(\\d{2})(?=[^\\d]|$)").matcher(name);
+        if (m.find()) {
+            int y = Integer.parseInt(m.group(1));
+            int mo = Integer.parseInt(m.group(2));
+            if (y >= 2000 && y <= 2100 && mo >= 1 && mo <= 12) {
+                return String.format("%04d-%02d", y, mo);
+            }
+        }
+        m = java.util.regex.Pattern.compile("(\\d{1,2})\\s*鏈�").matcher(name);
+        if (m.find()) {
+            int mo = Integer.parseInt(m.group(1));
+            if (mo >= 1 && mo <= 12) {
+                return String.format("%04d-%02d", defaultYear(defaultPeriod), mo);
+            }
+        }
+        return defaultPeriod;
+    }
+
+    private int defaultYear(String defaultPeriod) {
+        if (defaultPeriod != null && defaultPeriod.matches("\\d{4}-\\d{2}")) {
+            return Integer.parseInt(defaultPeriod.substring(0, 4));
+        }
+        return java.time.Year.now().getValue();
+    }
+
+    /** 绫诲瀷鍒嗗彂锛氫笌涓婁紶瀵煎叆鍚屼竴濂楄В鏋愭柟娉� */
+    private ImportResult importDirFile(String type, MultipartFile file, String period) throws Exception {
+        switch (type) {
+            case "h2032": return importH2032(file, period);
+            case "passengerMonthly": return importPassengerMonthly(file, period);
+            case "passengerIndividual": return importPassengerIndividual(file, period);
+            case "passengerAuth": return importPassengerAuth(file, period);
+            case "energyMonthly": return importEnergyMonthly(file, period);
+            case "energyAuth": return importEnergyAuth(file, period);
+            case "cityBus": return importCityBus(file, period);
+            case "cityTaxi": return importCityTaxi(file, period);
+            case "cityTaxiAuth": return new ImportResult(importCityTaxiAuth(file, period), 0, new ArrayList<>());
+            case "investment": return importInvestment(file, period, "瀹㈣繍绔欏満");
+            case "investmentLogistics": return importInvestment(file, period, "鐗╂祦鍥尯");
+            case "investmentSystem": return new ImportResult(importInvestmentSystem(file, period), 0, new ArrayList<>());
+            case "transportAuth": return new ImportResult(importTransportAuth(file, period), 0, new ArrayList<>());
+            case "trackMileage": return new ImportResult(importTrackMileage(file, period), 0, new ArrayList<>());
+            case "freightTurnover": return new ImportResult(importFreightTurnover(file, period), 0, new ArrayList<>());
+            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);
+        }
+    }
+
+    /** 鎶婃湇鍔$鏂囦欢鍖呰涓� MultipartFile锛屽鐢ㄥ悇瀵煎叆鏂规硶 */
+    public static class ByteArrayMultipartFile implements MultipartFile {
+        private final String name;
+        private final byte[] content;
+
+        public ByteArrayMultipartFile(String name, byte[] content) {
+            this.name = name;
+            this.content = content;
+        }
+
+        @Override
+        public String getName() { return name; }
+
+        @Override
+        public String getOriginalFilename() { return name; }
+
+        @Override
+        public String getContentType() {
+            String lower = name.toLowerCase();
+            return lower.endsWith(".xls")
+                ? "application/vnd.ms-excel"
+                : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+        }
+
+        @Override
+        public boolean isEmpty() { return content == null || content.length == 0; }
+
+        @Override
+        public long getSize() { return content == null ? 0 : content.length; }
+
+        @Override
+        public byte[] getBytes() { return content; }
+
+        @Override
+        public InputStream getInputStream() { return new ByteArrayInputStream(content); }
+
+        @Override
+        public void transferTo(File dest) throws IOException {
+            try (FileOutputStream out = new FileOutputStream(dest)) {
+                out.write(content);
+            }
+        }
+    }
 }

--
Gitblit v1.9.1