From 799ec6799ad9e994f7d369f059ca7682962f648f Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期三, 02 九月 2026 00:08:30 +0800
Subject: [PATCH] fix: 能耗导入同报表期并发重导互斥锁 + 空车牌(0)过滤 + 前端导入按钮防连点

---
 traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java |  434 ++++++++++++++++++++++++++++++++++++++++++++---------
 1 files changed, 355 insertions(+), 79 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 58b5305..41b922c 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
@@ -67,6 +67,7 @@
 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;
@@ -94,6 +95,9 @@
 
     /** 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", "鏅�氳揣杞�");
@@ -168,6 +172,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);
@@ -284,6 +289,7 @@
         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);
@@ -476,6 +482,7 @@
         try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
             Sheet sheet = wb.getSheetAt(0);
             Map<String, Integer> colMap = buildHeaderMap(sheet.getRow(0));
+            requireHeaderCols(sheet, "鏃呭杩愭斂杞﹁締鏁�", "浼佷笟鍚嶇О", "杞﹁締鏁�", "杞藉浣嶆暟");
             for (int r = 1; r <= sheet.getLastRowNum(); r++) {
                 Row row = sheet.getRow(r);
                 if (row == null) continue;
@@ -524,12 +531,18 @@
     /** 鑳借�楁槑缁嗘瘡缁勫浐瀹�18鍒楋紝杞︾墝鍙疯捣濮嬪垪锛�0-based锛夛細3,21,39,57,75 */
     private static final int[] ENERGY_GROUP_COLS = {3, 21, 39, 57, 75};
 
+    /** 鑳借�楀鍏ユ寜鎶ヨ〃鏈熶簰鏂ラ攣锛圚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, "閬撹矾璐ц繍杞﹁締鑳芥簮娑堣�楁儏鍐�", "浼佷笟鍚嶇О", "杞︾墝鍙�");
             for (int r = 1; r <= sheet.getLastRowNum(); r++) {
                 Row row = sheet.getRow(r);
                 if (row == null) continue;
@@ -537,7 +550,7 @@
                 if (enterpriseName == null || enterpriseName.trim().isEmpty()) continue;
                 for (int group : ENERGY_GROUP_COLS) {
                     String plate = getString(row, group);
-                    if (plate == null || plate.trim().isEmpty()) continue;
+                    if (plate == null || plate.trim().isEmpty() || "0".equals(plate.trim())) continue;
                     EnergyVehicleQuarterly e = new EnergyVehicleQuarterly();
                     e.setReportPeriod(period);
                     e.setRegionCode(getString(row, 0));
@@ -576,7 +589,7 @@
                     Row row = authSheet.getRow(r);
                     if (row == null) continue;
                     String plate = getString(row, 0);
-                    if (plate == null || plate.trim().isEmpty()) continue;
+                    if (plate == null || plate.trim().isEmpty() || "0".equals(plate.trim())) continue;
                     EnergyAuthVehicle auth = new EnergyAuthVehicle();
                     auth.setReportPeriod(period);
                     auth.setPlateNo(plate.trim());
@@ -593,54 +606,57 @@
         }
 
         // 骞傜瓑锛氬悓鎶ヨ〃鏈熼噸鏂板鍏ワ紝鍏堝垹闄ゆ棫鏁版嵁锛堜袱琛� + H204 瑙勫垯瀹℃牳缁撴灉锛�
-        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);
+        // 鍔犻攣锛氬悓鎶ヨ〃鏈熷苟鍙戦噸瀵间簰鏂ワ紝闃叉 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());
             }
-        }
-        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);
+            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);
         }
-        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);
     }
 
     /** 鑳借�楄溅杈嗚繍鏀夸俊鎭紙鐙珛妯℃澘锛氳溅鐗屽彿/杞﹁締绫诲瀷/鐕冩枡绫诲瀷/鏍囪鍚ㄤ綅/鍑嗙壍寮曡川閲忥級 */
@@ -648,11 +664,12 @@
         List<EnergyAuthVehicle> authList = 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;
+                if (plate == null || plate.trim().isEmpty() || "0".equals(plate.trim())) continue;
                 EnergyAuthVehicle auth = new EnergyAuthVehicle();
                 auth.setReportPeriod(period);
                 auth.setPlateNo(plate.trim());
@@ -666,30 +683,32 @@
             log.error("Energy auth parse error", e);
             throw e;
         }
-        // 骞傜瓑锛氬悓鎶ヨ〃鏈熼噸鏂板鍏ワ紝鍏堝垹闄ゆ棫鏁版嵁
-        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);
+        // 骞傜瓑锛氬悓鎶ヨ〃鏈熼噸鏂板鍏ワ紝鍏堝垹闄ゆ棫鏁版嵁锛涗笌 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);
         }
-        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 琛屽彿鐢ㄤ簬澶辫触瀹氫綅 */
@@ -716,6 +735,7 @@
         int failRows = 0;
         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;
@@ -763,6 +783,7 @@
         int failRows = 0;
         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;
@@ -806,6 +827,14 @@
         List<ScaleSplitTransport> list = new ArrayList<>();
         int failRows = 0;
         try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
+            boolean hasPeriodSheet = false;
+            for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+                String sn = wb.getSheetAt(i).getSheetName();
+                if (sn != null && (sn.contains("褰撴湀") || sn.contains("绱"))) { hasPeriodSheet = true; break; }
+            }
+            if (!hasPeriodSheet) {
+                throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨�愯涓婅涓嬫媶鍒嗐�戜笌鏂囦欢鍐呭涓嶇锛氭湭鎵惧埌鍚� 褰撴湀/绱 鐨� sheet锛岃纭鏄惁閫夐敊浜嗘暟鎹被鍨�");
+            }
             for (int i = 0; i < wb.getNumberOfSheets(); i++) {
                 Sheet sheet = wb.getSheetAt(i);
                 String sheetName = sheet.getSheetName();
@@ -896,6 +925,18 @@
         int monthCount = Integer.parseInt(period.split("-")[1]);
         try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
             Sheet sheet = wb.getSheetAt(0);
+            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<>();
@@ -1152,6 +1193,33 @@
         }
     }
 
+    /** 绫诲瀷-琛ㄥご鏍¢獙锛氭寜蹇呴渶鍒楀悕锛堣В鏋愬櫒瀹為檯渚濊禆鐨勫垪锛夛紝缂哄け鍗虫姏"绫诲瀷涓庢枃浠朵笉绗�" */
+    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) + "銆戠瓑鍒楋紝璇风‘璁ゆ槸鍚﹂�夐敊浜嗘暟鎹被鍨�");
+    }
+
     /** 鎸夎〃澶村悕绉板缓绔嬪垪鍙锋槧灏勶紝鍏煎鏂版棫妯℃澘鍒椾綅宸紓 */
     private Map<String, Integer> buildHeaderMap(Row header) {
         Map<String, Integer> colMap = new HashMap<>();
@@ -1398,7 +1466,7 @@
                 }
             }
             if (headerRow < 0) {
-                throw new RuntimeException("鏈壘鍒拌〃澶磋锛堥渶鍖呭惈 搴忓彿/椤圭洰鍚嶇О 鍒楋級");
+                throw new RuntimeException("鎵�閫夋暟鎹被鍨嬨��" + (passenger ? "鎶曡祫鏈堟姤锛堝杩愮珯鍦猴級" : "鎶曡祫鏈堟姤锛堢墿娴佸洯鍖猴級") + "銆戜笌鏂囦欢鍐呭涓嶇锛氭湭鎵惧埌琛ㄥご琛岋紙闇�鍖呭惈 搴忓彿/椤圭洰鍚嶇О 鍒楋級锛岃纭鏄惁閫夐敊浜嗘暟鎹被鍨�");
             }
             for (int r = headerRow + 1; r <= sheet.getLastRowNum(); r++) {
                 Row row = sheet.getRow(r);
@@ -1553,11 +1621,19 @@
             }
             fileResults.add(fr);
         }
-        // 骞傜瓑娓呯悊锛氬悓绫诲埆鍚屾姤琛ㄦ湡鏈堝害鏁版嵁 + INVEST 瀹℃牳缁撴灉锛堥」鐩富妗h法鏈堜繚鐣欙紝鎸夊悕 upsert锛�
-        cleanInvestMonthly(category, period);
+        // 骞傜瓑娓呯悊锛氭寜鏈鏂囦欢娑夊強甯傚窞娓呯悊鏈堝害鏁版嵁 + 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++;
@@ -1569,6 +1645,11 @@
                 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鈥︹��";
@@ -1587,6 +1668,119 @@
         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;
     }
@@ -1627,6 +1821,7 @@
         }
         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");
@@ -1637,6 +1832,15 @@
             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;
@@ -1645,7 +1849,6 @@
             if (nt.replace(" ", "").matches(".*灏廫\s*璁�.*")) continue;
             if (nt.startsWith("濉姤") || nt.startsWith("鍗曚綅璐熻矗浜�") || nt.matches("^\\d+銆�.*")) continue;
             if (hasSeq) {
-                String seq = cellStr(row, seqCol);
                 if (seq == null || seq.trim().isEmpty()) {
                     // 甯傚窞灏忚妭琛岋紙濡� "姝︽眽" "澶� 闂�"锛夛紝鏇存柊褰撳墠甯傚窞
                     String c = investmentCity(nt);
@@ -1884,7 +2087,7 @@
         return null;
     }
 
-    /** 鍚岀被鍒悓鎶ヨ〃鏈熸湀搴︽暟鎹� + INVEST 瀹℃牳缁撴灉娓呯悊锛堝箓绛夛級 */
+    /** 鍚岀被鍒悓鎶ヨ〃鏈熸湀搴︽暟鎹� + INVEST 瀹℃牳缁撴灉娓呯悊锛堝箓绛夛紝鍏ㄧ渷姹囨�诲ぇ琛ㄥ叏閲忔竻鐞嗭級 */
     private void cleanInvestMonthly(String category, String period) {
         List<InvestmentProject> sameCat = investProjectMapper.selectList(
             new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, category));
@@ -1895,15 +2098,60 @@
                 .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()) {
-            auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
-                .eq(AuditResult::getReportPeriod, period)
-                .in(AuditResult::getRuleId, investRuleIds));
+        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锛堟寜 椤圭洰鍚�+绫诲埆锛�+ 鏈堝害鍐欏叆 */
@@ -1922,6 +2170,32 @@
             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) {
@@ -1959,6 +2233,7 @@
         int failRows = 0;
         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;
@@ -2198,6 +2473,7 @@
         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;

--
Gitblit v1.9.1