From 37496dcec47bc98dc545d94287f39b33eb181bca Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期五, 28 八月 2026 17:36:48 +0800
Subject: [PATCH] docs: 关机交接——菜单布局修复(2bd663b)与实测结论、/import 主线程卡死新发现、sessionStorage 强制登录待办

---
 traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java | 1823 +++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 1,730 insertions(+), 93 deletions(-)

diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java b/traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java
index 47d5fe2..29bffc1 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java
@@ -2,11 +2,33 @@
 
 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.dataimport.entity.EnergyAuthVehicle;
+import com.trafficaudit.dataimport.entity.EnergyVehicleQuarterly;
 import com.trafficaudit.dataimport.entity.H2032EnterpriseMonthly;
+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.CityTaxiAuth;
+import com.trafficaudit.dataimport.entity.CityTaxiMonthly;
+import com.trafficaudit.dataimport.entity.PassengerAuthVehicle;
+import com.trafficaudit.dataimport.entity.PassengerEnterpriseMonthly;
 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.H2032EnterpriseMonthlyMapper;
+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.CityTaxiAuthMapper;
+import com.trafficaudit.dataimport.mapper.CityTaxiMonthlyMapper;
+import com.trafficaudit.dataimport.mapper.PassengerAuthVehicleMapper;
+import com.trafficaudit.dataimport.mapper.PassengerEnterpriseMonthlyMapper;
 import com.trafficaudit.dataimport.mapper.TransportAuthVehicleMapper;
 import com.trafficaudit.dataimport.mapper.VehicleTrackMileageMapper;
 import com.trafficaudit.rulemanage.entity.AuditRule;
@@ -14,7 +36,13 @@
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+
 import javax.annotation.Resource;
+import java.io.ByteArrayOutputStream;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -45,19 +73,114 @@
         FREIGHT_TYPE_NAMES.put("freightGrain", "绮");
     }
 
+    /** H204 杞﹁締绫诲瀷鐮� -> 鍏佽鐨勮繍鏀胯溅杈嗙被鍨嬶紙宸插幓鎺夐噸/涓�/杞诲瀷鍓嶇紑锛� */
+    private static final Map<String, java.util.Set<String>> ENERGY_VEHICLE_TYPE_ALLOWED = new HashMap<>();
+
+    /** H204 鐕冩枡绫诲瀷鐮� -> 鍏佽鐨勮繍鏀跨噧鏂欑被鍨� */
+    private static final Map<String, java.util.Set<String>> ENERGY_FUEL_TYPE_ALLOWED = new HashMap<>();
+
+    static {
+        ENERGY_VEHICLE_TYPE_ALLOWED.put("01", new java.util.HashSet<>(java.util.Arrays.asList("鏅�氳揣杞�", "鏍忔澘璐ц溅", "鑷嵏璐ц溅")));
+        ENERGY_VEHICLE_TYPE_ALLOWED.put("02", new java.util.HashSet<>(java.util.Arrays.asList("骞虫澘璐ц溅", "鏍忔澘璐ц溅")));
+        ENERGY_VEHICLE_TYPE_ALLOWED.put("03", new java.util.HashSet<>(java.util.Arrays.asList("浠撴爡寮忚揣杞�")));
+        ENERGY_VEHICLE_TYPE_ALLOWED.put("04", new java.util.HashSet<>(java.util.Arrays.asList("鍘㈠紡璐ц溅")));
+        ENERGY_VEHICLE_TYPE_ALLOWED.put("06", new java.util.HashSet<>(java.util.Arrays.asList("缃愬紡璐ц溅")));
+        ENERGY_VEHICLE_TYPE_ALLOWED.put("07", new java.util.HashSet<>(java.util.Arrays.asList("鐗规畩缁撴瀯璐ц溅", "杞﹁締杩愯緭杞�")));
+        ENERGY_VEHICLE_TYPE_ALLOWED.put("08", new java.util.HashSet<>(java.util.Arrays.asList("鑷嵏璐ц溅", "鏍忔澘璐ц溅")));
+        ENERGY_VEHICLE_TYPE_ALLOWED.put("09", new java.util.HashSet<>(java.util.Arrays.asList("杞﹁締杩愯緭杞�", "涓疆杞磋溅杈嗚繍杈撹溅")));
+        ENERGY_VEHICLE_TYPE_ALLOWED.put("11", new java.util.HashSet<>(java.util.Arrays.asList("鍗婃寕鐗靛紩杞�")));
+
+        ENERGY_FUEL_TYPE_ALLOWED.put("01", new java.util.HashSet<>(java.util.Arrays.asList("姹芥补")));
+        ENERGY_FUEL_TYPE_ALLOWED.put("02", new java.util.HashSet<>(java.util.Arrays.asList("鏌存补")));
+        ENERGY_FUEL_TYPE_ALLOWED.put("03", new java.util.HashSet<>(java.util.Arrays.asList("鍘嬬缉澶╃劧姘�")));
+        ENERGY_FUEL_TYPE_ALLOWED.put("04", new java.util.HashSet<>(java.util.Arrays.asList("娑叉�佸ぉ鐒舵皵", "娑插寲澶╃劧姘�", "鍙岀噧鏂�")));
+        ENERGY_FUEL_TYPE_ALLOWED.put("07", new java.util.HashSet<>(java.util.Arrays.asList("鐢靛姩")));
+        ENERGY_FUEL_TYPE_ALLOWED.put("08", new java.util.HashSet<>(java.util.Arrays.asList("姘㈡皵")));
+    }
+
     @Resource
     private AuditRuleMapper ruleMapper;
     @Resource
     private AuditResultMapper resultMapper;
+    @Resource
+    private AuditRunMapper auditRunMapper;
     @Resource
     private H2032EnterpriseMonthlyMapper h2032Mapper;
     @Resource
     private TransportAuthVehicleMapper transportAuthMapper;
     @Resource
     private VehicleTrackMileageMapper trackMileageMapper;
+    @Resource
+    private PassengerEnterpriseMonthlyMapper passengerMapper;
+    @Resource
+    private PassengerAuthVehicleMapper passengerAuthMapper;
+    @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;
+
+    /** 璁板綍銆岃鏈熻绫诲瀷宸插畬鎴愬鏍搞�嶆爣璁帮紙鍏堝鏍稿啀鍑鸿〃渚濇嵁锛涙棤瑙勫垯鎴栨棤鏁版嵁涓嶆爣璁帮紝閲嶅鎵ц骞傜瓑锛� */
+    public void markAudited(String reportType, String reportPeriod, List<AuditRule> rules, boolean hasData) {
+        if (rules == null || rules.isEmpty() || !hasData) return;
+        Long cnt = auditRunMapper.selectCount(new LambdaQueryWrapper<AuditRun>()
+            .eq(AuditRun::getReportType, reportType)
+            .eq(AuditRun::getReportPeriod, reportPeriod));
+        if (cnt != null && cnt > 0) return;
+        AuditRun run = new AuditRun();
+        run.setReportType(reportType);
+        run.setReportPeriod(reportPeriod);
+        auditRunMapper.insert(run);
+    }
+
+    /** 2 鍙傛暟閲嶈浇锛氭寜鎶ヨ〃绫诲瀷鑷姩鍙栧惎鐢ㄨ鍒� + 鏍¢獙璇ユ湡鏈夋暟鎹悗鍐嶅啓鏍囪锛堢嚎涓嬪鏍搁�氳繃鏁版嵁鎵嬪姩鏍囪 / Controller 璋冪敤锛� */
+    public boolean markAudited(String reportType, String reportPeriod) {
+        List<AuditRule> rules = rulesOf(reportType);
+        if (rules.isEmpty()) {
+            log.info("markAudited skip: no enabled rules for {}", reportType);
+            return false;
+        }
+        long dataCount;
+        if ("H2031".equals(reportType)) {
+            dataCount = passengerMapper.selectCount(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
+                .eq(PassengerEnterpriseMonthly::getReportPeriod, reportPeriod));
+        } else if ("H204".equals(reportType)) {
+            dataCount = energyMapper.selectCount(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
+                .eq(EnergyVehicleQuarterly::getReportPeriod, reportPeriod));
+        } else if ("INVEST".equals(reportType)) {
+            dataCount = investMonthlyMapper.selectCount(new LambdaQueryWrapper<InvestmentMonthly>()
+                .eq(InvestmentMonthly::getReportPeriod, reportPeriod));
+        } else if ("CITY_BUS".equals(reportType)) {
+            dataCount = cityBusMapper.selectCount(new LambdaQueryWrapper<CityBusMonthly>()
+                .eq(CityBusMonthly::getReportPeriod, reportPeriod));
+        } else if ("CITY_TAXI".equals(reportType)) {
+            dataCount = cityTaxiMapper.selectCount(new LambdaQueryWrapper<CityTaxiMonthly>()
+                .eq(CityTaxiMonthly::getReportPeriod, reportPeriod));
+        } else {
+            dataCount = h2032Mapper.selectCount(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
+                .eq(H2032EnterpriseMonthly::getReportPeriod, reportPeriod));
+        }
+        if (dataCount <= 0) {
+            log.info("markAudited skip: no data for {} / {}", reportType, reportPeriod);
+            return false;
+        }
+        markAudited(reportType, reportPeriod, rules, true);
+        return true;
+    }
 
     public List<AuditResult> executeAudit(String reportPeriod) {
-        List<AuditRule> rules = ruleMapper.selectEnabledRules();
+        List<AuditRule> rules = rulesOf("H2032");
         List<H2032EnterpriseMonthly> reports = h2032Mapper.selectList(
             new LambdaQueryWrapper<H2032EnterpriseMonthly>()
                 .eq(H2032EnterpriseMonthly::getReportPeriod, reportPeriod));
@@ -90,15 +213,1062 @@
             }
         }
         log.info("Audit complete: {} reports, {} issues for {}", reports.size(), results.size(), reportPeriod);
+        if (!rules.isEmpty() && !reports.isEmpty()) markAudited("H2032", reportPeriod);
         return results;
     }
 
-    public List<AuditResult> queryResults(String reportPeriod) {
+    public List<AuditResult> executePassengerAudit(String reportPeriod) {
+        List<AuditRule> rules = rulesOf("H2031");
+        List<PassengerEnterpriseMonthly> reports = passengerMapper.selectList(
+            new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
+                .eq(PassengerEnterpriseMonthly::getReportPeriod, reportPeriod));
+
+        Map<String, PassengerAuthVehicle> authMap = loadPassengerAuthMap(reportPeriod);
+        Map<String, PassengerEnterpriseMonthly> lastMonthMap = loadPassengerLastMonthMap(reportPeriod);
+
+        // 骞傜瓑锛氬彧娓呴櫎 H2031 瑙勫垯鐨勬棫瀹℃牳缁撴灉
+        List<Long> ruleIds = new ArrayList<>();
+        for (AuditRule rule : rules) ruleIds.add(rule.getId());
+        if (!ruleIds.isEmpty()) {
+            resultMapper.delete(new LambdaQueryWrapper<AuditResult>()
+                .eq(AuditResult::getReportPeriod, reportPeriod)
+                .in(AuditResult::getRuleId, ruleIds));
+        }
+
+        Map<Long, AuditRule> ruleMap = new HashMap<>();
+        for (AuditRule rule : rules) ruleMap.put(rule.getId(), rule);
+
+        List<AuditResult> results = new ArrayList<>();
+        for (PassengerEnterpriseMonthly report : reports) {
+            for (AuditRule rule : rules) {
+                AuditResult result = checkPassengerRule(rule, report, authMap, lastMonthMap);
+                if (result != null) {
+                    result.setRuleName(rule.getRuleName());
+                    result.setRuleCode(rule.getRuleCode());
+                    result.setAlertLevel(rule.getAlertLevel());
+                    result.setEnterpriseName(report.getEnterpriseName());
+                    result.setVerifyExplanation(report.getVerifyExplanation());
+                    resultMapper.insert(result);
+                    results.add(result);
+                }
+            }
+        }
+        log.info("Passenger audit complete: {} reports, {} issues for {}", reports.size(), results.size(), reportPeriod);
+        if (!rules.isEmpty() && !reports.isEmpty()) markAudited("H2031", reportPeriod);
+        return results;
+    }
+
+
+    // ========== H204 鑳借�楀鏍革紙杞﹁締绮掑害锛屼笌杩愭斂杞﹁締淇℃伅璺ㄨ〃瀵规瘮锛� ==========
+
+    public List<AuditResult> executeEnergyAudit(String reportPeriod) {
+        List<AuditRule> rules = rulesOf("H204");
+        List<EnergyVehicleQuarterly> vehicles = energyMapper.selectList(
+            new LambdaQueryWrapper<EnergyVehicleQuarterly>()
+                .eq(EnergyVehicleQuarterly::getReportPeriod, reportPeriod));
+
+        Map<String, EnergyAuthVehicle> authMap = new HashMap<>();
+        List<EnergyAuthVehicle> authList = energyAuthMapper.selectList(
+            new LambdaQueryWrapper<EnergyAuthVehicle>()
+                .eq(EnergyAuthVehicle::getReportPeriod, reportPeriod));
+        for (EnergyAuthVehicle v : authList) {
+            authMap.putIfAbsent(key(v.getPlateNo()), v);
+        }
+
+        // E007 鐜瘮鍩哄噯锛氫笂涓�瀛e害鍚岃溅鐗岀櫨鍏噷鍗曡��
+        String lastPeriod = prevQuarter(reportPeriod);
+        Map<String, EnergyVehicleQuarterly> lastQuarterMap = new HashMap<>();
+        if (lastPeriod != null) {
+            for (EnergyVehicleQuarterly v : energyMapper.selectList(
+                    new LambdaQueryWrapper<EnergyVehicleQuarterly>()
+                        .eq(EnergyVehicleQuarterly::getReportPeriod, lastPeriod))) {
+                lastQuarterMap.putIfAbsent(key(v.getPlateNo()), v);
+            }
+        }
+
+        // E006 瓒呰寖鍥村熀鍑嗭細鏈湡鍚勭噧鏂欑被鍨嬬櫨鍏噷鍗曡�椾腑浣嶆暟
+        Map<String, Double> fuelMedianMap = fuelPer100kmMedian(vehicles);
+
+        // E008/E009 瀵规瘮鍩哄噯锛氬悓鏈� H2032 浼佷笟鏈堟姤锛堟寜浼佷笟鍚嶇储寮曪級
+        Map<String, H2032EnterpriseMonthly> h2032Map = new HashMap<>();
+        for (H2032EnterpriseMonthly m : h2032Mapper.selectList(
+                new LambdaQueryWrapper<H2032EnterpriseMonthly>()
+                    .eq(H2032EnterpriseMonthly::getReportPeriod, reportPeriod))) {
+            String ent = m.getEnterpriseName();
+            if (ent != null && !ent.trim().isEmpty()) h2032Map.putIfAbsent(ent.trim(), m);
+        }
+
+        // 骞傜瓑锛氬彧娓呴櫎 H204 瑙勫垯鐨勬棫瀹℃牳缁撴灉
+        List<Long> ruleIds = new ArrayList<>();
+        for (AuditRule rule : rules) ruleIds.add(rule.getId());
+        if (!ruleIds.isEmpty()) {
+            resultMapper.delete(new LambdaQueryWrapper<AuditResult>()
+                .eq(AuditResult::getReportPeriod, reportPeriod)
+                .in(AuditResult::getRuleId, ruleIds));
+        }
+
+        List<AuditResult> results = new ArrayList<>();
+        for (EnergyVehicleQuarterly vehicle : vehicles) {
+            for (AuditRule rule : rules) {
+                AuditResult result = checkEnergyRule(rule, vehicle, authMap, fuelMedianMap, lastQuarterMap, h2032Map);
+                if (result != null) {
+                    result.setRuleName(rule.getRuleName());
+                    result.setRuleCode(rule.getRuleCode());
+                    result.setAlertLevel(rule.getAlertLevel());
+                    result.setEnterpriseName(vehicle.getEnterpriseName());
+                    result.setVerifyExplanation(null);
+                    resultMapper.insert(result);
+                    results.add(result);
+                }
+            }
+        }
+        log.info("Energy audit complete: {} vehicles, {} issues for {}", vehicles.size(), results.size(), reportPeriod);
+        if (!rules.isEmpty() && !vehicles.isEmpty()) markAudited("H204", reportPeriod);
+        return results;
+    }
+
+    private AuditResult checkEnergyRule(AuditRule rule, EnergyVehicleQuarterly vehicle,
+                                        Map<String, EnergyAuthVehicle> authMap,
+                                        Map<String, Double> fuelMedianMap,
+                                        Map<String, EnergyVehicleQuarterly> lastQuarterMap,
+                                        Map<String, H2032EnterpriseMonthly> h2032Map) {
+        try {
+            switch (rule.getCompareType()) {
+                case "CROSS_DIFF":
+                    return checkEnergyCrossDiff(rule, vehicle, authMap);
+                case "EXISTENCE_AUTH":
+                    return checkEnergyExistence(rule, vehicle, authMap);
+                case "LOADED_RATIO_DIFF":
+                    return checkEnergyLoadedRatio(rule, vehicle);
+                case "FUEL_RANGE":
+                    return checkEnergyFuelRange(rule, vehicle, fuelMedianMap);
+                case "FUEL_MOM":
+                    return checkEnergyFuelMom(rule, vehicle, lastQuarterMap);
+                case "FREIGHT_VS_H2032":
+                    return checkEnergyVsFreightReport(rule, vehicle, h2032Map);
+                case "AVG_DIST_VS_H2032":
+                    return checkEnergyAvgDistVsFreight(rule, vehicle, h2032Map);
+                case "LOADED_TON_RATIO":
+                    return checkEnergyLoadedTonnageRatio(rule, vehicle);
+                default:
+                    return null;
+            }
+        } catch (Exception e) {
+            log.error("Energy rule check error: {} - {}", rule.getRuleCode(), e.getMessage());
+            return null;
+        }
+    }
+
+    /** 璺ㄨ〃瀛楁瀵规瘮锛氳溅杈嗙被鍨�/鐕冩枡绫诲瀷鎸夌爜琛ㄦ瘮瀵癸紝鏍囪鍚ㄤ綅锛堢壍寮曡溅鐢ㄥ噯鐗靛紩璐ㄩ噺锛夋寜闃堝�兼瘮瀵� */
+    private AuditResult checkEnergyCrossDiff(AuditRule rule, EnergyVehicleQuarterly vehicle,
+                                             Map<String, EnergyAuthVehicle> authMap) {
+        EnergyAuthVehicle auth = authMap.get(key(vehicle.getPlateNo()));
+        if (auth == null) return null; // 杩愭斂缂哄け鐢� EXISTENCE_AUTH 瑙勫垯鎻愮ず
+
+        String field = rule.getCheckField();
+        if ("vehicleType".equals(field)) {
+            java.util.Set<String> allowed = ENERGY_VEHICLE_TYPE_ALLOWED.get(vehicle.getVehicleTypeCode());
+            if (allowed == null || !allowed.contains(normalizeVehicleType(auth.getVehicleType()))) {
+                return buildEnergyResult(rule, vehicle,
+                    "鑳借��=" + valueOf(vehicle.getVehicleType()) + "(鐮�" + valueOf(vehicle.getVehicleTypeCode())
+                        + "), 杩愭斂=" + valueOf(auth.getVehicleType()),
+                    "杞﹁締绫诲瀷涓�鑷�", "杞﹁締绫诲瀷涓嶄竴鑷�");
+            }
+            return null;
+        }
+        if ("fuelType".equals(field)) {
+            java.util.Set<String> allowed = ENERGY_FUEL_TYPE_ALLOWED.get(vehicle.getFuelTypeCode());
+            if (allowed == null || !allowed.contains(key(auth.getFuelType()))) {
+                return buildEnergyResult(rule, vehicle,
+                    "鑳借��=" + valueOf(vehicle.getFuelType()) + "(鐮�" + valueOf(vehicle.getFuelTypeCode())
+                        + "), 杩愭斂=" + valueOf(auth.getFuelType()),
+                    "鐕冩枡绫诲瀷涓�鑷�", "鐕冩枡绫诲瀷涓嶄竴鑷�");
+            }
+            return null;
+        }
+        if ("markedTonnage".equals(field)) {
+            double actual = nvl(vehicle.getMarkedTonnage());
+            double reference = "11".equals(vehicle.getVehicleTypeCode())
+                ? nvl(auth.getTractionQuality())
+                : nvl(auth.getMarkedTonnage());
+            double threshold = Double.parseDouble(rule.getThreshold());
+            double diff = Math.abs(actual - reference);
+            if (diff > threshold) {
+                String refName = "11".equals(vehicle.getVehicleTypeCode()) ? "鍑嗙壍寮曡川閲�" : "鏍囪鍚ㄤ綅";
+                return buildEnergyResult(rule, vehicle,
+                    "鑳借�楁爣璁板惃浣�=" + fmt(actual) + ", 杩愭斂" + refName + "=" + fmt(reference),
+                    "宸窛鈮�" + fmt(threshold), "宸窛=" + fmt(diff));
+            }
+            return null;
+        }
+        return null;
+    }
+
+    /** 鑳借�楁槑缁嗚溅鐗屽湪杩愭斂杞﹁締淇℃伅涓笉瀛樺湪鏃舵彁绀� */
+    private AuditResult checkEnergyExistence(AuditRule rule, EnergyVehicleQuarterly vehicle,
+                                             Map<String, EnergyAuthVehicle> authMap) {
+        EnergyAuthVehicle auth = authMap.get(key(vehicle.getPlateNo()));
+        if (auth == null) {
+            return buildEnergyResult(rule, vehicle,
+                "杞︾墝鍙�=" + vehicle.getPlateNo(), "杩愭斂杞﹁締淇℃伅瀛樺湪", "杩愭斂杞﹁締淇℃伅缂哄け");
+        }
+        return null;
+    }
+
+    /** E005 杞借揣閲岀▼鍗犳瘮 vs 骞冲潎杩愯窛/鍗曡稛骞冲潎閲岀▼ 鐩稿樊卤10% */
+    private AuditResult checkEnergyLoadedRatio(AuditRule rule, EnergyVehicleQuarterly vehicle) {
+        double threshold = parseDoubleSafe(rule.getThreshold(), 0.1);
+        double total = nvl(vehicle.getTotalMileage());
+        double loaded = nvl(vehicle.getLoadedMileage());
+        double avgDist = nvl(vehicle.getAvgDistance());
+        double avgTrip = nvl(vehicle.getAvgTripMileage());
+        if (total <= 0 || avgTrip <= 0 || avgDist <= 0) return null;
+        double r1 = loaded / total;
+        double r2 = avgDist / avgTrip;
+        if (r2 <= 0) return null;
+        double diff = Math.abs(r1 - r2) / r2;
+        if (diff <= threshold) return null;
+        return buildEnergyResult(rule, vehicle,
+            "杞借揣閲岀▼/鎬昏绋�=" + fmt4(r1) + "锛屽钩鍧囪繍璺�/鍗曡稛骞冲潎閲岀▼=" + fmt4(r2),
+            "涓よ�呯浉宸墹" + fmt(threshold * 100) + "%",
+            "鐩稿樊=" + fmt(diff * 100) + "%");
+    }
+
+    /** E006 鍚岀噧鏂欑被鍨嬬櫨鍏噷鍗曡�楀亸绂讳腑浣嶆暟瓒呴槇鍊硷紙榛樿 50%锛� */
+    private AuditResult checkEnergyFuelRange(AuditRule rule, EnergyVehicleQuarterly vehicle,
+                                             Map<String, Double> fuelMedianMap) {
+        double threshold = parseDoubleSafe(rule.getThreshold(), 0.5);
+        Double v = vehicle.getFuelPer100km();
+        if (v == null || v <= 0 || vehicle.getFuelTypeCode() == null) return null;
+        Double median = fuelMedianMap.get(vehicle.getFuelTypeCode().trim());
+        if (median == null || median <= 0) return null;
+        double diff = Math.abs(v - median) / median;
+        if (diff <= threshold) return null;
+        return buildEnergyResult(rule, vehicle,
+            "鐧惧叕閲屽崟鑰�=" + fmt(v) + "锛�" + valueOf(vehicle.getFuelType()) + "锛夛紝鍚岀噧鏂欎腑浣嶆暟=" + fmt(median),
+            "鍋忕涓綅鏁扳墹" + fmt(threshold * 100) + "%",
+            "鍋忕=" + fmt(diff * 100) + "%");
+    }
+
+    /** E007 鍚屼竴杈嗚溅鐧惧叕閲屽崟鑰楃幆姣斿彉鍖栬秴闃堝�硷紙榛樿 卤50%锛� */
+    private AuditResult checkEnergyFuelMom(AuditRule rule, EnergyVehicleQuarterly vehicle,
+                                           Map<String, EnergyVehicleQuarterly> lastQuarterMap) {
+        double threshold = parseDoubleSafe(rule.getThreshold(), 0.5);
+        Double cur = vehicle.getFuelPer100km();
+        if (cur == null || cur <= 0) return null;
+        EnergyVehicleQuarterly last = lastQuarterMap.get(key(vehicle.getPlateNo()));
+        if (last == null || last.getFuelPer100km() == null || last.getFuelPer100km() <= 0) return null;
+        double diff = Math.abs(cur - last.getFuelPer100km()) / last.getFuelPer100km();
+        if (diff <= threshold) return null;
+        return buildEnergyResult(rule, vehicle,
+            "鏈湡鐧惧叕閲屽崟鑰�=" + fmt(cur) + "锛屼笂瀛e害=" + fmt(last.getFuelPer100km()),
+            "鐜瘮鍙樺寲鈮�" + fmt(threshold * 100) + "%",
+            "鐜瘮鍙樺寲=" + fmt(diff * 100) + "%");
+    }
+
+    /** E008 鑳借�楄揣杩愰噺/鍛ㄨ浆閲忎笌 H2032 鏈堟姤瀵规瘮瓒呴槇鍊硷紙榛樿 50%锛� */
+    private AuditResult checkEnergyVsFreightReport(AuditRule rule, EnergyVehicleQuarterly vehicle,
+                                                   Map<String, H2032EnterpriseMonthly> h2032Map) {
+        double threshold = parseDoubleSafe(rule.getThreshold(), 0.5);
+        String ent = vehicle.getEnterpriseName();
+        if (ent == null || ent.trim().isEmpty()) return null;
+        H2032EnterpriseMonthly m = h2032Map.get(ent.trim());
+        if (m == null) return null;
+        List<String> msgs = new ArrayList<>();
+        if (m.getFreightTotal() != null && m.getFreightTotal() > 0 && vehicle.getFreight() != null) {
+            double r = vehicle.getFreight() / m.getFreightTotal();
+            if (r > threshold) {
+                msgs.add("鑳借�楄揣杩愰噺=" + fmt(vehicle.getFreight()) + "锛屾湀鎶�=" + fmt(m.getFreightTotal())
+                    + "锛屽崰姣�=" + fmt4(r));
+            }
+        }
+        if (m.getTurnoverTotal() != null && m.getTurnoverTotal() > 0 && vehicle.getTurnover() != null) {
+            double r = vehicle.getTurnover() / m.getTurnoverTotal();
+            if (r > threshold) {
+                msgs.add("鑳借�楀懆杞噺=" + fmt(vehicle.getTurnover()) + "锛屾湀鎶�=" + fmt(m.getTurnoverTotal())
+                    + "锛屽崰姣�=" + fmt4(r));
+            }
+        }
+        if (msgs.isEmpty()) return null;
+        return buildEnergyResult(rule, vehicle, String.join("锛�", msgs),
+            "鑳借�楅噺鈮ゆ湀鎶�" + fmt(threshold * 100) + "%", "瓒呰繃鏈堟姤" + fmt(threshold * 100) + "%");
+    }
+
+    /** E009 杞﹁締骞冲潎杩愯窛涓� H2032 鏈堟姤骞冲潎杩愯窛鐩稿樊瓒呴槇鍊硷紙榛樿 卤60%锛� */
+    private AuditResult checkEnergyAvgDistVsFreight(AuditRule rule, EnergyVehicleQuarterly vehicle,
+                                                    Map<String, H2032EnterpriseMonthly> h2032Map) {
+        double threshold = parseDoubleSafe(rule.getThreshold(), 0.6);
+        String ent = vehicle.getEnterpriseName();
+        if (ent == null || ent.trim().isEmpty()) return null;
+        H2032EnterpriseMonthly m = h2032Map.get(ent.trim());
+        if (m == null || m.getAvgDistance() == null || m.getAvgDistance() <= 0) return null;
+        double cur = nvl(vehicle.getAvgDistance());
+        if (cur <= 0) return null;
+        double diff = Math.abs(cur - m.getAvgDistance()) / m.getAvgDistance();
+        if (diff <= threshold) return null;
+        return buildEnergyResult(rule, vehicle,
+            "鑳借�楀钩鍧囪繍璺�=" + fmt(cur) + "锛屾湀鎶�=" + fmt(m.getAvgDistance()),
+            "鐩稿樊鈮�" + fmt(threshold * 100) + "%",
+            "鐩稿樊=" + fmt(diff * 100) + "%");
+    }
+
+    /** E010 骞冲潎杞借揣鍚ㄤ綅/鏍囪鍚ㄤ綅 姣斿�艰秴鍑� [0.5, 1] */
+    private AuditResult checkEnergyLoadedTonnageRatio(AuditRule rule, EnergyVehicleQuarterly vehicle) {
+        double lo = 0.5, hi = 1.0;
+        String t = rule.getThreshold();
+        if (t != null && t.contains(",")) {
+            String[] parts = t.split(",");
+            try {
+                lo = Double.parseDouble(parts[0].trim());
+                hi = Double.parseDouble(parts[1].trim());
+            } catch (Exception ignore) {
+            }
+        }
+        double marked = nvl(vehicle.getMarkedTonnage());
+        double loaded = nvl(vehicle.getAvgLoadedTonnage());
+        if (marked <= 0 || loaded <= 0) return null;
+        double r = loaded / marked;
+        if (r >= lo && r <= hi) return null;
+        return buildEnergyResult(rule, vehicle,
+            "骞冲潎杞借揣鍚ㄤ綅=" + fmt(loaded) + "锛屾爣璁板惃浣�=" + fmt(marked) + "锛屾瘮鍊�=" + fmt4(r),
+            "姣斿�煎湪[" + fmt(lo) + "," + fmt(hi) + "]",
+            "姣斿��=" + fmt4(r) + " 瓒呭嚭鑼冨洿");
+    }
+
+    /** 涓婁竴瀛e害鎶ヨ〃鏈燂細2026-06 鈫� 2026-03 */
+    private String prevQuarter(String period) {
+        if (period == null || period.length() < 7) return null;
+        try {
+            int year = Integer.parseInt(period.substring(0, 4));
+            int month = Integer.parseInt(period.substring(5, 7));
+            int pm = month - 3;
+            int py = year;
+            if (pm <= 0) {
+                pm += 12;
+                py -= 1;
+            }
+            return String.format("%04d-%02d", py, pm);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /** 鏈湡鍚勭噧鏂欑被鍨嬬櫨鍏噷鍗曡�椾腑浣嶆暟 */
+    private Map<String, Double> fuelPer100kmMedian(List<EnergyVehicleQuarterly> vehicles) {
+        Map<String, List<Double>> byFuel = new HashMap<>();
+        for (EnergyVehicleQuarterly v : vehicles) {
+            if (v.getFuelTypeCode() == null || v.getFuelPer100km() == null || v.getFuelPer100km() <= 0) continue;
+            byFuel.computeIfAbsent(v.getFuelTypeCode().trim(), k -> new ArrayList<>()).add(v.getFuelPer100km());
+        }
+        Map<String, Double> med = new HashMap<>();
+        for (Map.Entry<String, List<Double>> e : byFuel.entrySet()) {
+            List<Double> list = e.getValue();
+            list.sort(null);
+            double m = list.size() % 2 == 1 ? list.get(list.size() / 2)
+                : (list.get(list.size() / 2 - 1) + list.get(list.size() / 2)) / 2.0;
+            med.put(e.getKey(), m);
+        }
+        return med;
+    }
+
+    private Map<String, EnergyAuthVehicle> loadEnergyAuthMap(String reportPeriod) {
+        Map<String, EnergyAuthVehicle> map = new HashMap<>();
+        List<EnergyAuthVehicle> list = energyAuthMapper.selectList(
+            new LambdaQueryWrapper<EnergyAuthVehicle>()
+                .eq(EnergyAuthVehicle::getReportPeriod, reportPeriod));
+        for (EnergyAuthVehicle v : list) {
+            map.putIfAbsent(key(v.getPlateNo()), v);
+        }
+        return map;
+    }
+
+    /** 杩愭斂杞﹁締绫诲瀷鍘绘帀 閲嶅瀷/涓瀷/杞诲瀷 鍓嶇紑鍚庢瘮瀵� */
+    private String normalizeVehicleType(String type) {
+        if (type == null) return "";
+        String t = type.trim();
+        for (String prefix : new String[]{"閲嶅瀷", "涓瀷", "杞诲瀷"}) {
+            if (t.startsWith(prefix)) {
+                t = t.substring(prefix.length());
+                break;
+            }
+        }
+        return t;
+    }
+
+    private String valueOf(String v) {
+        return v == null ? "绌�" : v.trim();
+    }
+
+    private AuditResult buildEnergyResult(AuditRule rule, EnergyVehicleQuarterly vehicle,
+                                          String actual, String threshold, String deviation) {
+        AuditResult result = new AuditResult();
+        result.setRuleId(rule.getId());
+        result.setReportId(vehicle.getId());
+        result.setEnterpriseCode(vehicle.getEnterpriseCode());
+        result.setReportPeriod(vehicle.getReportPeriod());
+        result.setActualValue(actual);
+        result.setThresholdValue(threshold);
+        result.setDeviation(deviation);
+        result.setStatus("PENDING");
+        return result;
+    }
+
+
+    // ========== INVEST 鎶曡祫瀹℃牳锛堟眹鎬诲ぇ琛� 涓� 鎶曡祫绯荤粺瀵煎嚭 璺ㄨ〃瀵规瘮锛� ==========
+
+    public List<AuditResult> executeInvestmentAudit(String reportPeriod) {
+        List<AuditRule> rules = rulesOf("INVEST");
+        List<InvestmentMonthly> monthlies = investMonthlyMapper.selectList(
+            new LambdaQueryWrapper<InvestmentMonthly>()
+                .eq(InvestmentMonthly::getReportPeriod, reportPeriod));
+        Map<Long, InvestmentProject> projectMap = new HashMap<>();
+        for (InvestmentProject p : investProjectMapper.selectList(null)) {
+            projectMap.put(p.getId(), p);
+        }
+        Map<String, List<InvestmentSystem>> sysMap = new HashMap<>();
+        for (InvestmentSystem s : investSystemMapper.selectList(
+                new LambdaQueryWrapper<InvestmentSystem>()
+                    .eq(InvestmentSystem::getReportPeriod, reportPeriod))) {
+            sysMap.computeIfAbsent(normInvestName(s.getProjectName()), k -> new ArrayList<>()).add(s);
+        }
+
+        // 骞傜瓑锛氬彧娓呴櫎 INVEST 瑙勫垯鐨勬棫瀹℃牳缁撴灉
+        List<Long> ruleIds = new ArrayList<>();
+        for (AuditRule rule : rules) ruleIds.add(rule.getId());
+        if (!ruleIds.isEmpty()) {
+            resultMapper.delete(new LambdaQueryWrapper<AuditResult>()
+                .eq(AuditResult::getReportPeriod, reportPeriod)
+                .in(AuditResult::getRuleId, ruleIds));
+        }
+
+        List<AuditResult> results = new ArrayList<>();
+        for (InvestmentMonthly monthly : monthlies) {
+            InvestmentProject project = projectMap.get(monthly.getProjectId());
+            String projectName = project == null ? "鏈煡椤圭洰" : project.getProjectName();
+            InvestmentSystem sys = findInvestSystem(sysMap, projectName);
+            for (AuditRule rule : rules) {
+                AuditResult result = checkInvestRule(rule, monthly, projectName, sys);
+                if (result != null) {
+                    result.setRuleName(rule.getRuleName());
+                    result.setRuleCode(rule.getRuleCode());
+                    result.setAlertLevel(rule.getAlertLevel());
+                    result.setEnterpriseName(projectName);
+                    result.setVerifyExplanation(null);
+                    resultMapper.insert(result);
+                    results.add(result);
+                }
+            }
+        }
+        log.info("Investment audit complete: {} projects, {} issues for {}", monthlies.size(), results.size(), reportPeriod);
+        if (!rules.isEmpty() && !monthlies.isEmpty()) markAudited("INVEST", reportPeriod);
+        return results;
+    }
+
+    private AuditResult checkInvestRule(AuditRule rule, InvestmentMonthly monthly,
+                                        String projectName, InvestmentSystem sys) {
+        try {
+            switch (rule.getCompareType()) {
+                case "CROSS_DIFF":
+                    return checkInvestCrossDiff(rule, monthly, projectName, sys);
+                case "EXISTENCE_SYS":
+                    return checkInvestExistence(rule, monthly, projectName, sys);
+                default:
+                    return null;
+            }
+        } catch (Exception e) {
+            log.error("Investment rule check error: {} - {}", rule.getRuleCode(), e.getMessage());
+            return null;
+        }
+    }
+
+    /** 涓庢姇璧勭郴缁熷鍑烘暟鍊煎姣旓細闃堝�兼牸寮� "缁濆宸畖鐩稿宸�%" */
+    private AuditResult checkInvestCrossDiff(AuditRule rule, InvestmentMonthly monthly,
+                                             String projectName, InvestmentSystem sys) {
+        if (sys == null) return null; // 缂哄け鐢� EXISTENCE_SYS 瑙勫垯鎻愮ず
+        String field = rule.getCheckField();
+        double actual;
+        double reference;
+        String fieldName;
+        if ("startCum".equals(field)) {
+            actual = nvl(monthly.getStartCum());
+            reference = nvl(sys.getStartCum());
+            fieldName = "鑷紑濮嬪缓璁剧疮璁�";
+        } else if ("yearCum".equals(field)) {
+            actual = nvl(monthly.getYearCum());
+            reference = nvl(sys.getYearCum());
+            fieldName = "鑷勾鍒濈疮璁�";
+        } else if ("monthDone".equals(field)) {
+            actual = nvl(monthly.getMonthDone());
+            reference = nvl(sys.getMonthDone());
+            fieldName = "鏈湀瀹屾垚";
+        } else {
+            return null;
+        }
+        String[] parts = rule.getThreshold().split("\\|");
+        double absThreshold = parts.length > 0 ? parseDoubleSafe(parts[0], 50) : 50;
+        double pctThreshold = parts.length > 1 ? parseDoubleSafe(parts[1], 5) : 5;
+        double diff = Math.abs(actual - reference);
+        double base = Math.max(Math.abs(reference), 1.0);
+        double pct = diff / base * 100;
+        boolean hit = diff > absThreshold && pct > pctThreshold;
+        if (!hit && reference == 0 && actual > absThreshold) {
+            hit = true;
+        }
+        if (hit) {
+            return buildInvestResult(rule, monthly,
+                "姹囨�诲ぇ琛�" + fieldName + "=" + fmt(actual) + ", 鎶曡祫绯荤粺=" + fmt(reference),
+                "缁濆宸墹" + fmt(absThreshold) + "涓� 涓� 鐩稿宸墹" + fmt(pctThreshold) + "%",
+                "缁濆宸�=" + fmt(diff) + "涓�, 鐩稿宸�=" + fmt(Math.round(pct * 10) / 10.0) + "%");
+        }
+        return null;
+    }
+
+    /** 姹囨�诲ぇ琛ㄩ」鐩湪鎶曡祫绯荤粺瀵煎嚭涓笉瀛樺湪 */
+    private AuditResult checkInvestExistence(AuditRule rule, InvestmentMonthly monthly,
+                                             String projectName, InvestmentSystem sys) {
+        if (sys == null) {
+            return buildInvestResult(rule, monthly,
+                "椤圭洰鍚嶇О=" + projectName, "鎶曡祫绯荤粺瀵煎嚭涓瓨鍦�", "鎶曡祫绯荤粺瀵煎嚭涓笉瀛樺湪");
+        }
+        return null;
+    }
+
+    private AuditResult buildInvestResult(AuditRule rule, InvestmentMonthly monthly,
+                                          String actual, String threshold, String deviation) {
+        AuditResult result = new AuditResult();
+        result.setRuleId(rule.getId());
+        result.setReportId(monthly.getId());
+        result.setReportPeriod(monthly.getReportPeriod());
+        result.setActualValue(actual);
+        result.setThresholdValue(threshold);
+        result.setDeviation(deviation);
+        result.setStatus("PENDING");
+        return result;
+    }
+
+    // ========== CITY_BUS 鍩庡競鍏氦瀹℃牳锛�4 缁勮鍒欙紝浼佷笟绾э級 ==========
+
+    public List<AuditResult> executeCityBusAudit(String reportPeriod) {
+        List<AuditRule> rules = rulesOf("CITY_BUS");
+        List<CityBusMonthly> rows = cityBusMapper.selectList(
+            new LambdaQueryWrapper<CityBusMonthly>()
+                .eq(CityBusMonthly::getReportPeriod, reportPeriod));
+
+        // 骞傜瓑锛氬彧娓呴櫎 CITY_BUS 瑙勫垯鐨勬棫瀹℃牳缁撴灉
+        List<Long> ruleIds = new ArrayList<>();
+        for (AuditRule rule : rules) ruleIds.add(rule.getId());
+        if (!ruleIds.isEmpty()) {
+            resultMapper.delete(new LambdaQueryWrapper<AuditResult>()
+                .eq(AuditResult::getReportPeriod, reportPeriod)
+                .in(AuditResult::getRuleId, ruleIds));
+        }
+
+        List<AuditResult> results = new ArrayList<>();
+        for (CityBusMonthly row : rows) {
+            // 鏃犲叕浜ょ粡钀ユ椿鍔ㄤ紒涓氾紙杩愯惀杞︽暟/瀹㈣繍閲�/鍛ㄨ浆閲忓潎 0锛夎烦杩�
+            if (nvl(row.getOpVehicles()) <= 0 && nvl(row.getPassengerVolume()) <= 0
+                    && nvl(row.getTurnover()) <= 0) {
+                continue;
+            }
+            for (AuditRule rule : rules) {
+                AuditResult result = checkCityBusRule(rule, row);
+                if (result != null) {
+                    result.setRuleName(rule.getRuleName());
+                    result.setRuleCode(rule.getRuleCode());
+                    result.setAlertLevel(rule.getAlertLevel());
+                    result.setEnterpriseName(row.getEnterpriseName());
+                    result.setVerifyExplanation(row.getVerifyExplanation());
+                    resultMapper.insert(result);
+                    results.add(result);
+                }
+            }
+        }
+        log.info("CityBus audit complete: {} enterprises, {} issues for {}", rows.size(), results.size(), reportPeriod);
+        if (!rules.isEmpty() && !rows.isEmpty()) markAudited("CITY_BUS", reportPeriod);
+        return results;
+    }
+
+    private AuditResult checkCityBusRule(AuditRule rule, CityBusMonthly row) {
+        try {
+            switch (rule.getCompareType()) {
+                case "TREND_CONSISTENCY":
+                    return checkCityBusTrend(rule, row);
+                case "RATIO_CHANGE":
+                    return checkCityBusRatioChange(rule, row);
+                case "AVG_DIST_CHANGE":
+                    return checkCityBusAvgDistChange(rule, row);
+                default:
+                    return null;
+            }
+        } catch (Exception e) {
+            log.error("CityBus rule check error: {} - {}", rule.getRuleCode(), e.getMessage());
+            return null;
+        }
+    }
+
+    /** 瓒嬪娍涓�鑷达細check_field 涓垚瀵瑰瓧娈碉紙鐜瘮/鍚屾瘮锛夎秼鍔夸笉涓�鑷翠笖澧為�熻秴闃堝�奸渶鏍稿疄 */
+    private AuditResult checkCityBusTrend(AuditRule rule, CityBusMonthly row) {
+        double threshold = parseDoubleSafe(rule.getThreshold(), 0.02);
+        String field = rule.getCheckField() == null ? "" : rule.getCheckField();
+        List<String> msgs = new ArrayList<>();
+        if (field.contains("passengerCityMom")) {
+            checkTrendPair(msgs, "鍩庡競鍐呭杩愰噺鐜瘮", row.getRatePassengerCityMom(),
+                "鍩庨檯鍩庝埂瀹㈣繍閲忕幆姣�", row.getRateChengxiangMom(), threshold);
+        }
+        if (field.contains("passengerCityYoy")) {
+            checkTrendPair(msgs, "鍩庡競鍐呭杩愰噺鍚屾瘮", row.getRatePassengerCityYoy(),
+                "鍩庨檯鍩庝埂瀹㈣繍閲忓悓姣�", row.getRateChengxiangYoy(), threshold);
+        }
+        if (field.contains("passengerMom")) {
+            checkTrendPair(msgs, "瀹㈣繍閲忕幆姣�", row.getRatePassengerMom(),
+                "鏃呭鍛ㄨ浆閲忕幆姣�", row.getRateTurnoverMom(), threshold);
+        }
+        if (field.contains("passengerYoy")) {
+            checkTrendPair(msgs, "瀹㈣繍閲忓悓姣�", row.getRatePassengerYoy(),
+                "鏃呭鍛ㄨ浆閲忓悓姣�", row.getRateTurnoverYoy(), threshold);
+        }
+        if (msgs.isEmpty()) return null;
+        return buildCityBusResult(rule, row, String.join("锛�", msgs),
+            "涓ゆ寚鏍囩幆姣�/鍚屾瘮瓒嬪娍涓�鑷�", "瓒嬪娍涓嶄竴鑷翠笖澧為�熻秴闃堝��");
+    }
+
+    private void checkTrendPair(List<String> msgs, String aName, Double aVal,
+                                String bName, Double bVal, double threshold) {
+        double a = nvl(aVal);
+        double b = nvl(bVal);
+        boolean aUp = a > 0, bUp = b > 0;
+        boolean aDown = a < 0, bDown = b < 0;
+        if ((aUp && bDown) || (aDown && bUp)) {
+            double maxAbs = Math.max(Math.abs(a), Math.abs(b));
+            if (maxAbs > threshold) {
+                msgs.add(aName + "=" + fmt(a) + ", " + bName + "=" + fmt(b) + "锛堣秼鍔夸笉涓�鑷达級");
+            }
+        }
+    }
+
+    /** 鍗犳瘮鍙樺寲锛氭湰鏈堝崰姣� vs 鐢辩幆姣斿閫熷弽鎺ㄧ殑涓婃湀鍗犳瘮锛屽彉鍖�>1% 闇�鏍稿疄 */
+    private AuditResult checkCityBusRatioChange(AuditRule rule, CityBusMonthly row) {
+        double threshold = parseDoubleSafe(rule.getThreshold(), 0.01);
+        String field = rule.getCheckField() == null ? "" : rule.getCheckField();
+        List<String> msgs = new ArrayList<>();
+        if (field.contains("passengerCityRatio")) {
+            Double change = cityBusRatioChange(row.getPassengerCityRatio(),
+                row.getPassengerCity(), row.getRatePassengerCityMom(),
+                row.getPassengerVolume(), row.getRatePassengerMom());
+            if (change != null && change > threshold) {
+                msgs.add("鍩庡競鍐呭杩愰噺鍗犳瘮鍙樺寲=" + fmt4(change) + "锛�>1%锛�");
+            }
+        }
+        if (field.contains("chengxiangRatio")) {
+            Double change = cityBusRatioChange(row.getChengxiangRatio(),
+                row.getPassengerChengxiang(), row.getRateChengxiangMom(),
+                row.getPassengerVolume(), row.getRatePassengerMom());
+            if (change != null && change > threshold) {
+                msgs.add("鍩庨檯鍩庝埂瀹㈣繍閲忓崰姣斿彉鍖�=" + fmt4(change) + "锛�>1%锛�");
+            }
+        }
+        if (msgs.isEmpty()) return null;
+        return buildCityBusResult(rule, row, String.join("锛�", msgs),
+            "鍗犳瘮鍙樺寲鈮�1%", "鍗犳瘮鍙樺寲>1%");
+    }
+
+    /** 鍗犳瘮鐜瘮鍙樺寲锛氭湰鏈堝崰姣� vs 涓婃湀鍗犳瘮锛堢敱鐜瘮澧為�熷弽鎺級锛涙棤娉曟帹绠楄繑鍥� null */
+    private Double cityBusRatioChange(Double curRatio, Double curValue, Double curRate,
+                                      Double curTotal, Double totalRate) {
+        if (curRatio == null) return null;
+        double r = nvl(curRate);
+        double tr = nvl(totalRate);
+        if (r <= -1.0 || tr <= -1.0) return null;
+        double lastRatio = (nvl(curValue) / (1 + r)) / (nvl(curTotal) / (1 + tr));
+        if (Double.isNaN(lastRatio) || Double.isInfinite(lastRatio)) return null;
+        return Math.abs(curRatio - lastRatio);
+    }
+
+    /** 骞冲潎杩愯窛鍙樺寲锛氱幆姣斿鍑忥紙鍏噷锛夋垨鍩庨檯鍩庝埂鐢� 鍛ㄨ浆閲�/瀹㈣繍閲� 鐜瘮鍙嶆帹锛岃秴杩� 3 鍏噷闇�鏍稿疄 */
+    private AuditResult checkCityBusAvgDistChange(AuditRule rule, CityBusMonthly row) {
+        double threshold = parseDoubleSafe(rule.getThreshold(), 3.0);
+        String field = rule.getCheckField() == null ? "" : rule.getCheckField();
+        List<String> msgs = new ArrayList<>();
+        if (field.contains("avgDistanceMomChange")) {
+            double chg = Math.abs(nvl(row.getAvgDistanceMomChange()));
+            if (chg > threshold) {
+                msgs.add("骞冲潎杩愯窛鐜瘮澧炲噺=" + fmt(nvl(row.getAvgDistanceMomChange())) + "鍏噷");
+            }
+        }
+        if (field.contains("avgDistanceCityMomChange")) {
+            double chg = Math.abs(nvl(row.getAvgDistanceCityMomChange()));
+            if (chg > threshold) {
+                msgs.add("鍩庡競鍐呭钩鍧囪繍璺濈幆姣斿鍑�=" + fmt(nvl(row.getAvgDistanceCityMomChange())) + "鍏噷");
+            }
+        }
+        if (field.contains("avgDistanceChengxiang")) {
+            double cur = nvl(row.getAvgDistanceChengxiang());
+            double km = nvl(row.getRateChengxiangMom());
+            double kt = nvl(row.getRateChengxiangTurnoverMom());
+            if (cur > 0 && km > -1.0 && kt > -1.0) {
+                double last = cur * (1 + km) / (1 + kt);
+                double chg = Math.abs(cur - last);
+                if (chg > threshold) {
+                    msgs.add("鍩庨檯鍩庝埂骞冲潎杩愯窛鍙樺寲=" + fmt(chg) + "鍏噷锛堟湰鏈�" + fmt(cur)
+                        + " vs 涓婃湀" + fmt(last) + "锛�");
+                }
+            }
+        }
+        if (msgs.isEmpty()) return null;
+        return buildCityBusResult(rule, row, String.join("锛�", msgs),
+            "骞冲潎杩愯窛鍙樺寲鈮�" + fmt(threshold) + "鍏噷", "骞冲潎杩愯窛鍙樺寲>" + fmt(threshold) + "鍏噷");
+    }
+
+    private AuditResult buildCityBusResult(AuditRule rule, CityBusMonthly row,
+                                           String actual, String threshold, String deviation) {
+        AuditResult result = new AuditResult();
+        result.setRuleId(rule.getId());
+        result.setReportId(row.getId());
+        result.setReportPeriod(row.getReportPeriod());
+        result.setActualValue(actual);
+        result.setThresholdValue(threshold);
+        result.setDeviation(deviation);
+        result.setStatus("PENDING");
+        return result;
+    }
+
+    // ========== CITY_TAXI 宸℃父鍑虹瀹℃牳锛�7 缁勮鍒欙紝甯傚窞绾э級 ==========
+
+    public List<AuditResult> executeCityTaxiAudit(String reportPeriod) {
+        List<AuditRule> rules = rulesOf("CITY_TAXI");
+        List<CityTaxiMonthly> rows = cityTaxiMapper.selectList(
+            new LambdaQueryWrapper<CityTaxiMonthly>()
+                .eq(CityTaxiMonthly::getReportPeriod, reportPeriod));
+
+        // 骞傜瓑锛氬彧娓呴櫎 CITY_TAXI 瑙勫垯鐨勬棫瀹℃牳缁撴灉
+        List<Long> ruleIds = new ArrayList<>();
+        for (AuditRule rule : rules) ruleIds.add(rule.getId());
+        if (!ruleIds.isEmpty()) {
+            resultMapper.delete(new LambdaQueryWrapper<AuditResult>()
+                .eq(AuditResult::getReportPeriod, reportPeriod)
+                .in(AuditResult::getRuleId, ruleIds));
+        }
+
+        // 杩愭斂杞﹁締鎸夊競宸炶鏁帮紙T001 瀵规瘮鍩哄噯锛�
+        Map<String, Long> authCount = loadTaxiAuthCount(reportPeriod);
+
+        List<AuditResult> results = new ArrayList<>();
+        for (CityTaxiMonthly row : rows) {
+            if (row.getCity() == null || row.getCity().trim().isEmpty()) continue;
+            // 鏃犵粡钀ユ椿鍔ㄥ競宸烇紙杩愯惀杞︽暟/瀹㈣繍閲�/鍛ㄨ浆閲忓潎 0锛夎烦杩�
+            if (nvl(row.getOpVehicles()) <= 0 && nvl(row.getPassengerVolume()) <= 0
+                    && nvl(row.getTurnover()) <= 0) {
+                continue;
+            }
+            for (AuditRule rule : rules) {
+                AuditResult result = checkCityTaxiRule(rule, row, authCount);
+                if (result != null) {
+                    result.setRuleName(rule.getRuleName());
+                    result.setRuleCode(rule.getRuleCode());
+                    result.setAlertLevel(rule.getAlertLevel());
+                    result.setEnterpriseName(row.getCity());
+                    result.setVerifyExplanation(row.getVerifyExplanation());
+                    resultMapper.insert(result);
+                    results.add(result);
+                }
+            }
+        }
+        log.info("CityTaxi audit complete: {} cities, {} issues for {}", rows.size(), results.size(), reportPeriod);
+        if (!rules.isEmpty() && !rows.isEmpty()) markAudited("CITY_TAXI", reportPeriod);
+        return results;
+    }
+
+    private Map<String, Long> loadTaxiAuthCount(String reportPeriod) {
+        Map<String, Long> map = new HashMap<>();
+        List<CityTaxiAuth> auths = cityTaxiAuthMapper.selectList(
+            new LambdaQueryWrapper<CityTaxiAuth>()
+                .eq(CityTaxiAuth::getReportPeriod, reportPeriod));
+        for (CityTaxiAuth a : auths) {
+            if (a.getCity() == null || a.getCity().trim().isEmpty()) continue;
+            map.merge(a.getCity(), 1L, Long::sum);
+        }
+        return map;
+    }
+
+    private AuditResult checkCityTaxiRule(AuditRule rule, CityTaxiMonthly row, Map<String, Long> authCount) {
+        try {
+            switch (rule.getCompareType()) {
+                case "CROSS_DIFF_PCT":
+                    return checkTaxiAuthDiff(rule, row, authCount);
+                case "TREND_CONSISTENCY":
+                    return checkCityTaxiTrend(rule, row);
+                case "RATIO_CHANGE":
+                    return checkCityTaxiRatioChange(rule, row);
+                case "AVG_DIST_CHANGE":
+                    return checkCityTaxiAvgDistChange(rule, row);
+                case "PAX_PER_TRIP_CHANGE":
+                    return checkTaxiPaxPerTripChange(rule, row);
+                default:
+                    return null;
+            }
+        } catch (Exception e) {
+            log.error("CityTaxi rule check error: {} - {}", rule.getRuleCode(), e.getMessage());
+            return null;
+        }
+    }
+
+    /** T001 杩愯惀杞︽暟 vs 杩愭斂鍦ㄨ惀杞﹁締鏁帮細宸紓鐜�>3% 闇�鏍稿疄锛圧ED锛� */
+    private AuditResult checkTaxiAuthDiff(AuditRule rule, CityTaxiMonthly row, Map<String, Long> authCount) {
+        // 鏁存湡鏈鍏ヨ繍鏀挎暟鎹椂璺宠繃瀵规瘮锛堥伩鍏嶆妸鈥滆繍鏀�=0鈥濊褰撳熀鍑嗘姤宸紓锛�
+        if (authCount == null || authCount.isEmpty()) return null;
+        double threshold = parseDoubleSafe(rule.getThreshold(), 3.0);
+        double monthly = nvl(row.getOpVehicles());
+        Long auth = authCount.get(row.getCity());
+        long authV = auth == null ? 0 : auth;
+        if (monthly <= 0 && authV <= 0) return null;
+        double diffPct = authV <= 0 ? 100.0 : Math.abs(monthly - authV) / authV * 100.0;
+        if (diffPct <= threshold) return null;
+        return buildTaxiResult(rule, row,
+            "鏈堟姤杩愯惀杞﹁締鏁�=" + fmt(monthly) + "锛岃繍鏀垮湪钀ヨ溅杈嗘暟=" + authV + "锛屽樊寮傜巼=" + fmt(diffPct) + "%",
+            "宸紓鐜団墹" + fmt(threshold) + "%",
+            "宸紓鐜�=" + fmt(diffPct) + "%>" + fmt(threshold) + "%");
+    }
+
+    /** T002/T003/T007 瓒嬪娍涓�鑷达細check_field 涓垚瀵瑰瓧娈碉紙鐜瘮/鍚屾瘮锛夎秼鍔夸笉涓�鑷翠笖澧為�熻秴闃堝�奸渶鏍稿疄 */
+    private AuditResult checkCityTaxiTrend(AuditRule rule, CityTaxiMonthly row) {
+        double threshold = parseDoubleSafe(rule.getThreshold(), 0.02);
+        String field = rule.getCheckField() == null ? "" : rule.getCheckField();
+        List<String> msgs = new ArrayList<>();
+        if (field.contains("passengerCityMom")) {
+            checkTrendPair(msgs, "鍩庡競鍐呭杩愰噺鐜瘮", row.getRatePassengerCityMom(),
+                "鍩庨檯鍩庝埂瀹㈣繍閲忕幆姣�", row.getRatePassengerChengxiangMom(), threshold);
+        }
+        if (field.contains("turnoverMomDerived")) {
+            // 鏈堟姤鏃犵洿鎺ュ懆杞噺鐜瘮鍒楋細鍛ㄨ浆閲�=瀹㈣繍閲徝楀钩鍧囪繍璺濓紝鐢变袱鑰呯幆姣旀帹瀵�
+            Double rateTurnover = taxiTurnoverMomDerived(row);
+            if (rateTurnover != null) {
+                checkTrendPair(msgs, "瀹㈣繍閲忕幆姣�", row.getRatePassengerMom(),
+                    "鏃呭鍛ㄨ浆閲忕幆姣�(鎺ㄥ)", rateTurnover, threshold);
+            }
+        }
+        if (field.contains("tripMom")) {
+            checkTrendPair(msgs, "杞藉杞︽鎬绘暟鐜瘮", row.getRateTripMom(),
+                "瀹㈣繍閲忕幆姣�", row.getRatePassengerMom(), threshold);
+        }
+        if (field.contains("tripYoy")) {
+            checkTrendPair(msgs, "杞藉杞︽鍚屾瘮", row.getRateTripYoy(),
+                "瀹㈣繍閲忓悓姣�", row.getRatePassengerYoy(), threshold);
+        }
+        if (msgs.isEmpty()) return null;
+        return buildTaxiResult(rule, row, String.join("锛�", msgs),
+            "涓ゆ寚鏍囩幆姣�/鍚屾瘮瓒嬪娍涓�鑷�", "瓒嬪娍涓嶄竴鑷翠笖澧為�熻秴闃堝��");
+    }
+
+    /** 鎺ㄥ鍛ㄨ浆閲忕幆姣旓細rate鈮�(1+瀹㈣繍閲忕幆姣�)脳(1+骞冲潎杩愯窛鐜瘮)-1锛屽钩鍧囪繍璺濈幆姣旂敱涓婃湡杩愯窛=鏈湡鈭掑鍑忔帹绠� */
+    private Double taxiTurnoverMomDerived(CityTaxiMonthly row) {
+        double dist = nvl(row.getAvgDistance());
+        double chg = nvl(row.getAvgDistanceMomChange());
+        double rp = nvl(row.getRatePassengerMom());
+        if (dist <= 0 || dist - chg <= 0 || rp <= -1.0) return null;
+        double rd = chg / (dist - chg);
+        return (1 + rp) * (1 + rd) - 1;
+    }
+
+    /** T004 鍗犳瘮鍙樺寲锛氬煄甯傚唴瀹㈣繍閲忓崰姣斿彉鍖�>1% 闇�鏍稿疄 */
+    private AuditResult checkCityTaxiRatioChange(AuditRule rule, CityTaxiMonthly row) {
+        double threshold = parseDoubleSafe(rule.getThreshold(), 0.01);
+        String field = rule.getCheckField() == null ? "" : rule.getCheckField();
+        List<String> msgs = new ArrayList<>();
+        if (field.contains("passengerCityRatio")) {
+            Double change = cityBusRatioChange(row.getPassengerCityRatio(),
+                row.getPassengerCity(), row.getRatePassengerCityMom(),
+                row.getPassengerVolume(), row.getRatePassengerMom());
+            if (change != null && change > threshold) {
+                msgs.add("鍩庡競鍐呭杩愰噺鍗犳瘮鍙樺寲=" + fmt4(change) + "锛�>1%锛�");
+            }
+        }
+        if (msgs.isEmpty()) return null;
+        return buildTaxiResult(rule, row, String.join("锛�", msgs),
+            "鍗犳瘮鍙樺寲鈮�1%", "鍗犳瘮鍙樺寲>1%");
+    }
+
+    /** T005 骞冲潎杩愯窛鍙樺寲锛氱幆姣斿鍑忥紙鍏噷锛夋垨鍩庨檯鍩庝埂鐢卞钩鍧囪繍璺濆閫熷弽鎺紝瓒呰繃 3 鍏噷闇�鏍稿疄 */
+    private AuditResult checkCityTaxiAvgDistChange(AuditRule rule, CityTaxiMonthly row) {
+        double threshold = parseDoubleSafe(rule.getThreshold(), 3.0);
+        String field = rule.getCheckField() == null ? "" : rule.getCheckField();
+        List<String> msgs = new ArrayList<>();
+        if (field.contains("avgDistanceMomChange")) {
+            double chg = Math.abs(nvl(row.getAvgDistanceMomChange()));
+            if (chg > threshold) {
+                msgs.add("骞冲潎杩愯窛鐜瘮澧炲噺=" + fmt(nvl(row.getAvgDistanceMomChange())) + "鍏噷");
+            }
+        }
+        if (field.contains("avgDistanceCityMomChange")) {
+            double chg = Math.abs(nvl(row.getAvgDistanceCityMomChange()));
+            if (chg > threshold) {
+                msgs.add("鍩庡競鍐呭钩鍧囪繍璺濈幆姣斿鍑�=" + fmt(nvl(row.getAvgDistanceCityMomChange())) + "鍏噷");
+            }
+        }
+        if (field.contains("avgDistanceChengxiang")) {
+            double cur = nvl(row.getAvgDistanceChengxiang());
+            double dist = nvl(row.getAvgDistance());
+            double chg = nvl(row.getAvgDistanceMomChange());
+            if (cur > 0 && dist > 0 && dist > chg) {
+                double rd = chg / (dist - chg);
+                double last = cur / (1 + rd);
+                double diff = Math.abs(cur - last);
+                if (diff > threshold) {
+                    msgs.add("鍩庨檯鍩庝埂骞冲潎杩愯窛鍙樺寲=" + fmt(diff) + "鍏噷锛堟湰鏈�" + fmt(cur)
+                        + " vs 涓婃湀" + fmt(last) + "锛�");
+                }
+            }
+        }
+        if (msgs.isEmpty()) return null;
+        return buildTaxiResult(rule, row, String.join("锛�", msgs),
+            "骞冲潎杩愯窛鍙樺寲鈮�" + fmt(threshold) + "鍏噷", "骞冲潎杩愯窛鍙樺寲>" + fmt(threshold) + "鍏噷");
+    }
+
+    /** T006 鍗曡溅娆¤浇瀹汉鏁板彉鍖栵細鐢变笂鏈�(杞藉杞︽/瀹㈣繍閲忕幆姣�)鍙嶆帹锛屽彉鍖�>0.2浜� 闇�鏍稿疄 */
+    private AuditResult checkTaxiPaxPerTripChange(AuditRule rule, CityTaxiMonthly row) {
+        double threshold = parseDoubleSafe(rule.getThreshold(), 0.2);
+        String field = rule.getCheckField() == null ? "" : rule.getCheckField();
+        List<String> msgs = new ArrayList<>();
+        if (field.contains("passengersPerTrip")) {
+            double cur = nvl(row.getPassengersPerTrip());
+            double rp = nvl(row.getRatePassengerMom());
+            double rt = nvl(row.getRateTripMom());
+            if (cur > 0 && rp > -1.0 && rt > -1.0) {
+                double last = cur * (1 + rt) / (1 + rp);
+                double diff = Math.abs(cur - last);
+                if (diff > threshold) {
+                    msgs.add("鍗曡溅娆¤浇瀹汉鏁板彉鍖�=" + fmt(diff) + "浜猴紙鏈湀" + fmt(cur)
+                        + " vs 涓婃湀" + fmt(last) + "锛�");
+                }
+            }
+        }
+        if (msgs.isEmpty()) return null;
+        return buildTaxiResult(rule, row, String.join("锛�", msgs),
+            "鍗曡溅娆¤浇瀹汉鏁板彉鍖栤墹" + fmt(threshold) + "浜�", "鍗曡溅娆¤浇瀹汉鏁板彉鍖�>" + fmt(threshold) + "浜�");
+    }
+
+    private AuditResult buildTaxiResult(AuditRule rule, CityTaxiMonthly row,
+                                        String actual, String threshold, String deviation) {
+        AuditResult result = new AuditResult();
+        result.setRuleId(rule.getId());
+        result.setReportId(row.getId());
+        result.setReportPeriod(row.getReportPeriod());
+        result.setActualValue(actual);
+        result.setThresholdValue(threshold);
+        result.setDeviation(deviation);
+        result.setStatus("PENDING");
+        return result;
+    }
+
+    /** 椤圭洰鍚嶅綊涓�鍖栵細鍘绘嫭鍙峰瓧绗︺�佽繛鎺ョ锛岀綏椹暟瀛楃粺涓�锛屽幓鐪�/甯�/鍘�/鍖哄墠缂�涓庣┖鐧� */
+    private String normInvestName(String name) {
+        if (name == null) return "";
+        String s = name.trim();
+        s = s.replaceAll("[锛�(]", "").replaceAll("[锛�)]", "");
+        s = s.replaceAll("[路鈥⑩�旓紞_\\-]", "");
+        s = s.replaceAll("[鈪犫叀鈪⑩叄鈪も叆鈪︹収鈪ㄢ叐鈪扳叡鈪测叧鈪粹叺鈪垛叿鈪糕吂]", "I");
+        s = s.replaceAll("^(婀栧寳鐪亅姝︽眽甯倈榛勭煶甯倈鍗佸牥甯倈瀹滄槍甯倈瑗勯槼甯倈閯傚窞甯倈鑽嗛棬甯倈瀛濇劅甯倈鑽嗗窞甯倈榛勫唸甯倈鍜稿畞甯倈闅忓窞甯倈鎭╂柦宸瀨浠欐甯倈娼滄睙甯倈澶╅棬甯倈绁炲啘鏋舵灄鍖�)", "");
+        s = s.replaceAll("^(?:[\\u4e00-\\u9fa5]{2,4}(?:鍘縷甯倈鍖�))", "");
+        s = s.replaceAll("\\s+", "");
+        return s;
+    }
+
+    /** 鍚嶇О褰掍竴鍖栧悗绮剧‘鍖归厤锛堝悓鍚嶅鍊欓�夋寜鍘垮尯娑堟锛夛紝澶辫触鍐嶆寜璇勫垎鍋氬墠缂�/鍖呭惈/瀛愬簭鍒楁ā绯婂尮閰� */
+    private InvestmentSystem findInvestSystem(Map<String, List<InvestmentSystem>> sysMap, String projectName) {
+        String norm = normInvestName(projectName);
+        if (norm.isEmpty()) return null;
+        List<InvestmentSystem> exact = sysMap.get(norm);
+        if (exact != null && !exact.isEmpty()) {
+            return disambiguateInvest(exact, projectName);
+        }
+        InvestmentSystem best = null;
+        int bestScore = Integer.MIN_VALUE;
+        for (Map.Entry<String, List<InvestmentSystem>> e : sysMap.entrySet()) {
+            String key = e.getKey();
+            if (key.length() < 5 || norm.length() < 5) continue;
+            String shortSide = key.length() <= norm.length() ? key : norm;
+            String longSide = key.length() > norm.length() ? key : norm;
+            int idx = longSide.indexOf(shortSide);
+            int lenDiff = Math.abs(key.length() - norm.length());
+            int score;
+            if (idx == 0) {
+                // 鍓嶇紑寮忓寘鍚紙鐭悕=闀垮悕寮�澶达級鏈�鍙俊
+                score = 1000 - lenDiff * 2;
+            } else if (idx > 0 && shortSide.length() >= 6) {
+                // 涓悗閮ㄥ寘鍚細鐭悕瓒冲闀挎墠鍙俊
+                score = 500 - idx * 5 - lenDiff * 2 + shortSide.length();
+            } else if (idx > 0 && shortSide.length() == 5 && lenDiff <= 5) {
+                // 5瀛楃煭鍚嶄腑鍚庨儴锛氬樊寮傞儴鍒嗕笉鍚� 鍘�/甯�/宸� 鎵嶅尮閰嶏紙鎺掗櫎 缃楃敯鍘跨患鍚堢墿娴佸洯 杩欑被锛�
+                String diff = longSide.substring(0, idx) + longSide.substring(idx + shortSide.length());
+                if (diff.matches(".*(鍘縷甯倈宸�).*")) continue;
+                score = 400 - idx * 5 - lenDiff;
+            } else if (isInvestSubsequence(shortSide, longSide) && lenDiff <= 8) {
+                // 瀛愬簭鍒楀厹搴曪細鐭悕涓洪暱鍚嶅幓鎺夋彃鍏ヨ瘝鍚庣殑瀛愬簭鍒楋紙濡� 缁煎悎鏋㈢航 vs 缁煎悎瀹㈣繍鏋㈢航銆佺墿娴佷腑蹇� vs 鐗╂祦鍥腑蹇冿級
+                score = 300 - lenDiff * 3;
+            } else {
+                continue;
+            }
+            if (score > bestScore) {
+                bestScore = score;
+                best = disambiguateInvest(e.getValue(), projectName);
+            }
+        }
+        return best;
+    }
+
+    /** 澶氫釜绯荤粺椤圭洰褰掍竴鍖栧悗鍚屽悕锛堝 鍜镐赴鍘跨患鍚堢墿娴佸洯/缃楃敯鍘跨患鍚堢墿娴佸洯 鈫� 缁煎悎鐗╂祦鍥級锛屾寜鍘垮尯鍓嶇紑鍖哄垎 */
+    private InvestmentSystem disambiguateInvest(List<InvestmentSystem> candidates, String projectName) {
+        if (candidates == null || candidates.isEmpty()) return null;
+        if (candidates.size() == 1) return candidates.get(0);
+        String county = extractCounty(projectName);
+        if (!county.isEmpty()) {
+            for (InvestmentSystem c : candidates) {
+                String raw = c.getProjectName() == null ? "" : c.getProjectName();
+                if (raw.contains(county)) return c;
+            }
+        }
+        return candidates.get(0);
+    }
+
+    /** 鎻愬彇鍚嶇О涓殑 鍘�/甯�/鍖� 鍓嶇紑锛堝 鍜镐赴鍘匡級 */
+    private String extractCounty(String name) {
+        if (name == null) return "";
+        java.util.regex.Matcher m = java.util.regex.Pattern.compile("([\\u4e00-\\u9fa5]{2,4}(?:鍘縷甯倈鍖�))").matcher(name);
+        if (m.find()) return m.group(1);
+        return "";
+    }
+
+    /** 鍒ゆ柇鐭悕鏄惁涓洪暱鍚嶇殑瀛楃瀛愬簭鍒楋紙鎸夐『搴忓嚭鐜板嵆鍙紝鍏佽涓棿鎻掑叆璇嶏級 */
+    private boolean isInvestSubsequence(String shortSide, String longSide) {
+        int i = 0;
+        for (int j = 0; i < shortSide.length() && j < longSide.length(); j++) {
+            if (shortSide.charAt(i) == longSide.charAt(j)) i++;
+        }
+        return i == shortSide.length();
+    }
+
+    private double parseDoubleSafe(String v, double def) {
+        try {
+            return Double.parseDouble(v.trim());
+        } catch (Exception e) {
+            return def;
+        }
+    }
+
+    public List<AuditResult> queryResults(String reportPeriod, String reportType) {
+        String type = reportType == null || reportType.trim().isEmpty() ? "H2032" : reportType.trim();
         List<AuditResult> results = resultMapper.selectList(new LambdaQueryWrapper<AuditResult>()
             .eq(AuditResult::getReportPeriod, reportPeriod)
             .orderByAsc(AuditResult::getId));
-        attachInfo(results);
-        return results;
+        List<Long> ruleIds = new ArrayList<>();
+        for (AuditRule rule : ruleMapper.selectList(null)) {
+            if (type.equals(rule.getReportType())) ruleIds.add(rule.getId());
+        }
+        List<AuditResult> filtered = new ArrayList<>();
+        for (AuditResult result : results) {
+            if (ruleIds.contains(result.getRuleId())) filtered.add(result);
+        }
+        attachInfo(filtered);
+        return filtered;
     }
 
     public void reviewResult(Long id, String status, String comment) {
@@ -114,11 +1284,43 @@
         if (results.isEmpty()) return;
         Map<Long, AuditRule> ruleMap = new HashMap<>();
         for (AuditRule rule : ruleMapper.selectList(null)) ruleMap.put(rule.getId(), rule);
-        Map<Long, String> nameMap = new HashMap<>();
-        Map<Long, String> explainMap = new HashMap<>();
+        Map<Long, String> h2032Name = new HashMap<>();
+        Map<Long, String> h2032Explain = new HashMap<>();
+        Map<Long, String> passengerName = new HashMap<>();
+        Map<Long, String> passengerExplain = new HashMap<>();
+        Map<Long, String> energyName = new HashMap<>();
+        Map<Long, String> investName = new HashMap<>();
+        Map<Long, String> busName = new HashMap<>();
+        Map<Long, String> busExplain = new HashMap<>();
+        Map<Long, String> taxiName = new HashMap<>();
+        Map<Long, String> taxiExplain = new HashMap<>();
         for (H2032EnterpriseMonthly r : h2032Mapper.selectList(null)) {
-            nameMap.putIfAbsent(r.getId(), r.getEnterpriseName());
-            explainMap.putIfAbsent(r.getId(), r.getVerifyExplanation());
+            h2032Name.putIfAbsent(r.getId(), r.getEnterpriseName());
+            h2032Explain.putIfAbsent(r.getId(), r.getVerifyExplanation());
+        }
+        for (PassengerEnterpriseMonthly r : passengerMapper.selectList(null)) {
+            passengerName.putIfAbsent(r.getId(), r.getEnterpriseName());
+            passengerExplain.putIfAbsent(r.getId(), r.getVerifyExplanation());
+        }
+        for (EnergyVehicleQuarterly r : energyMapper.selectList(null)) {
+            energyName.putIfAbsent(r.getId(), r.getEnterpriseName());
+        }
+        for (CityBusMonthly r : cityBusMapper.selectList(null)) {
+            busName.putIfAbsent(r.getId(), r.getEnterpriseName());
+            busExplain.putIfAbsent(r.getId(), r.getVerifyExplanation());
+        }
+        for (CityTaxiMonthly r : cityTaxiMapper.selectList(null)) {
+            taxiName.putIfAbsent(r.getId(), r.getCity());
+            taxiExplain.putIfAbsent(r.getId(), r.getVerifyExplanation());
+        }
+        Map<Long, Long> investProjectId = new HashMap<>();
+        for (InvestmentMonthly r : investMonthlyMapper.selectList(null)) {
+            investProjectId.putIfAbsent(r.getId(), r.getProjectId());
+        }
+        for (InvestmentProject p : investProjectMapper.selectList(null)) {
+            for (Map.Entry<Long, Long> e : investProjectId.entrySet()) {
+                if (e.getValue().equals(p.getId())) investName.putIfAbsent(e.getKey(), p.getProjectName());
+            }
         }
         for (AuditResult result : results) {
             AuditRule rule = ruleMap.get(result.getRuleId());
@@ -127,6 +1329,16 @@
                 result.setRuleCode(rule.getRuleCode());
                 result.setAlertLevel(rule.getAlertLevel());
             }
+            // 鍚勬姤琛ㄨ〃 id 鍚勮嚜鐙珛鑷锛岄渶鎸夎鍒欐墍灞炴姤琛ㄧ被鍨嬪垎琛ㄥ洖濉紒涓氬悕绉�
+            String type = rule == null ? null : rule.getReportType();
+            Map<Long, String> nameMap = "INVEST".equals(type) ? investName
+                : "H204".equals(type) ? energyName
+                : "H2031".equals(type) ? passengerName
+                : "CITY_BUS".equals(type) ? busName
+                : "CITY_TAXI".equals(type) ? taxiName : h2032Name;
+            Map<Long, String> explainMap = "H2031".equals(type) ? passengerExplain
+                : "CITY_BUS".equals(type) ? busExplain
+                : "CITY_TAXI".equals(type) ? taxiExplain : h2032Explain;
             String enterpriseName = nameMap.get(result.getReportId());
             if (enterpriseName == null) {
                 // 涓婃姤鏁版嵁琚噸鏂板鍏ュ悗鏃у鏍哥粨鏋滃凡澶辨晥锛岀粰鍑烘彁绀鸿�岄潪绌虹櫧
@@ -135,6 +1347,214 @@
             result.setEnterpriseName(enterpriseName);
             result.setVerifyExplanation(explainMap.get(result.getReportId()));
         }
+    }
+
+    /** 鏈�杩戞湁鏁版嵁鐨勬姤琛ㄦ湡锛堢敤浜庡墠绔粯璁ら�変腑锛� */
+    public String latestPeriod(String reportType) {
+        String type = reportType == null || reportType.trim().isEmpty() ? "H2032" : reportType.trim();
+        String max = null;
+        switch (type) {
+            case "H2031":
+                for (PassengerEnterpriseMonthly m : passengerMapper.selectList(
+                        new LambdaQueryWrapper<PassengerEnterpriseMonthly>().select(PassengerEnterpriseMonthly::getReportPeriod))) {
+                    max = maxPeriod(max, m.getReportPeriod());
+                }
+                break;
+            case "H204":
+                for (EnergyVehicleQuarterly m : energyMapper.selectList(
+                        new LambdaQueryWrapper<EnergyVehicleQuarterly>().select(EnergyVehicleQuarterly::getReportPeriod))) {
+                    max = maxPeriod(max, m.getReportPeriod());
+                }
+                break;
+            case "INVEST":
+                for (InvestmentMonthly m : investMonthlyMapper.selectList(
+                        new LambdaQueryWrapper<InvestmentMonthly>().select(InvestmentMonthly::getReportPeriod))) {
+                    max = maxPeriod(max, m.getReportPeriod());
+                }
+                break;
+            case "CITY_BUS":
+                for (CityBusMonthly m : cityBusMapper.selectList(
+                        new LambdaQueryWrapper<CityBusMonthly>().select(CityBusMonthly::getReportPeriod))) {
+                    max = maxPeriod(max, m.getReportPeriod());
+                }
+                break;
+            case "CITY_TAXI":
+                for (CityTaxiMonthly m : cityTaxiMapper.selectList(
+                        new LambdaQueryWrapper<CityTaxiMonthly>().select(CityTaxiMonthly::getReportPeriod))) {
+                    max = maxPeriod(max, m.getReportPeriod());
+                }
+                break;
+            default:
+                for (H2032EnterpriseMonthly m : h2032Mapper.selectList(
+                        new LambdaQueryWrapper<H2032EnterpriseMonthly>().select(H2032EnterpriseMonthly::getReportPeriod))) {
+                    max = maxPeriod(max, m.getReportPeriod());
+                }
+                break;
+        }
+        return max;
+    }
+
+    private String maxPeriod(String a, String b) {
+        if (b == null || b.trim().isEmpty()) return a;
+        if (a == null || a.compareTo(b) < 0) return b;
+        return a;
+    }
+
+    /** 瀹℃牳缁撴灉瀵煎嚭 Excel锛堝惈浼佷笟鍚�/瑙勫垯鍚�/鐘舵��/瀹℃牳鎰忚锛� */
+    public byte[] exportResults(String reportPeriod, String reportType) throws Exception {
+        List<AuditResult> results = queryResults(reportPeriod, reportType);
+        try (XSSFWorkbook wb = new XSSFWorkbook()) {
+            Sheet sheet = wb.createSheet("瀹℃牳缁撴灉");
+            String[] headers = {"鎶ヨ〃鏈�", "浼佷笟/甯傚窞", "瑙勫垯缂栫爜", "瀹℃牳瑙勫垯", "绛夌骇", "瀹為檯鍊�", "闃堝��/鍩哄噯", "鍋忓樊璇存槑", "鐘舵��", "瀹℃牳鎰忚"};
+            Row head = sheet.createRow(0);
+            for (int i = 0; i < headers.length; i++) {
+                Cell cell = head.createCell(i);
+                cell.setCellValue(headers[i]);
+                org.apache.poi.ss.usermodel.CellStyle style = wb.createCellStyle();
+                org.apache.poi.ss.usermodel.Font font = wb.createFont();
+                font.setBold(true);
+                style.setFont(font);
+                cell.setCellStyle(style);
+            }
+            int r = 1;
+            for (AuditResult res : results) {
+                Row row = sheet.createRow(r++);
+                row.createCell(0).setCellValue(valueOf(res.getReportPeriod()));
+                row.createCell(1).setCellValue(valueOf(res.getEnterpriseName()));
+                row.createCell(2).setCellValue(valueOf(res.getRuleCode()));
+                row.createCell(3).setCellValue(valueOf(res.getRuleName()));
+                row.createCell(4).setCellValue(valueOf(res.getAlertLevel()));
+                row.createCell(5).setCellValue(valueOf(res.getActualValue()));
+                row.createCell(6).setCellValue(valueOf(res.getThresholdValue()));
+                row.createCell(7).setCellValue(valueOf(res.getDeviation()));
+                row.createCell(8).setCellValue(auditStatusCn(res.getStatus()));
+                row.createCell(9).setCellValue(valueOf(res.getReviewComment()));
+            }
+            int[] widths = {12, 28, 12, 28, 8, 36, 22, 36, 10, 28};
+            for (int i = 0; i < widths.length; i++) {
+                sheet.setColumnWidth(i, widths[i] * 256);
+            }
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            wb.write(out);
+            return out.toByteArray();
+        }
+    }
+
+    private String auditStatusCn(String status) {
+        if ("CONFIRMED".equals(status)) return "宸茬‘璁�";
+        if ("IGNORED".equals(status)) return "宸插拷鐣�";
+        return "寰呭鐞�";
+    }
+
+    /** 瑙勫垯鎵ц缁熻锛氭瘡鏉″惎鐢ㄨ鍒欑殑妫�鏌ュ璞℃暟涓庡懡涓暟 */
+    public List<Map<String, Object>> ruleStats(String reportPeriod, String reportType) {
+        String type = reportType == null || reportType.trim().isEmpty() ? "H2032" : reportType.trim();
+        List<AuditRule> rules = rulesOf(type);
+        long checked;
+        switch (type) {
+            case "H2031":
+                checked = passengerMapper.selectCount(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
+                    .eq(PassengerEnterpriseMonthly::getReportPeriod, reportPeriod));
+                break;
+            case "H204":
+                checked = energyMapper.selectCount(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
+                    .eq(EnergyVehicleQuarterly::getReportPeriod, reportPeriod));
+                break;
+            case "INVEST":
+                checked = investMonthlyMapper.selectCount(new LambdaQueryWrapper<InvestmentMonthly>()
+                    .eq(InvestmentMonthly::getReportPeriod, reportPeriod));
+                break;
+            case "CITY_BUS":
+                checked = cityBusMapper.selectCount(new LambdaQueryWrapper<CityBusMonthly>()
+                    .eq(CityBusMonthly::getReportPeriod, reportPeriod));
+                break;
+            case "CITY_TAXI":
+                checked = cityTaxiMapper.selectCount(new LambdaQueryWrapper<CityTaxiMonthly>()
+                    .eq(CityTaxiMonthly::getReportPeriod, reportPeriod));
+                break;
+            default:
+                checked = h2032Mapper.selectCount(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
+                    .eq(H2032EnterpriseMonthly::getReportPeriod, reportPeriod));
+                break;
+        }
+        List<AuditResult> results = resultMapper.selectList(new LambdaQueryWrapper<AuditResult>()
+            .eq(AuditResult::getReportPeriod, reportPeriod));
+        Map<Long, Long> hitMap = new HashMap<>();
+        for (AuditResult r : results) {
+            hitMap.merge(r.getRuleId(), 1L, Long::sum);
+        }
+        List<Map<String, Object>> out = new ArrayList<>();
+        for (AuditRule rule : rules) {
+            Map<String, Object> m = new HashMap<>();
+            m.put("ruleId", rule.getId());
+            m.put("ruleCode", rule.getRuleCode());
+            m.put("ruleName", rule.getRuleName());
+            m.put("alertLevel", rule.getAlertLevel());
+            m.put("checked", checked);
+            m.put("hit", hitMap.getOrDefault(rule.getId(), 0L));
+            m.put("skipped", ruleSkipped(rule, reportPeriod));
+            out.add(m);
+        }
+        return out;
+    }
+
+    /** 璺ㄨ〃/鐜瘮鍩哄噯鏁版嵁鏁存湡缂哄け鏃讹紝瑙勫垯鏍囪涓鸿烦杩囷紙閬垮厤璇姤锛� */
+    private boolean ruleSkipped(AuditRule rule, String period) {
+        String code = rule.getRuleCode();
+        if (code == null) return false;
+        switch (code) {
+            case "RULE_001":
+            case "RULE_002":
+                return transportAuthMapper.selectCount(new LambdaQueryWrapper<TransportAuthVehicle>()
+                    .eq(TransportAuthVehicle::getReportPeriod, period)) == 0;
+            case "RULE_003":
+                return trackMileageMapper.selectCount(new LambdaQueryWrapper<VehicleTrackMileage>()
+                    .eq(VehicleTrackMileage::getReportPeriod, period)) == 0;
+            case "RULE_004":
+                return h2032Mapper.selectCount(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
+                    .eq(H2032EnterpriseMonthly::getReportPeriod, getLastPeriod(period))) == 0;
+            case "RULE_P001":
+            case "RULE_P002":
+                return passengerAuthMapper.selectCount(new LambdaQueryWrapper<PassengerAuthVehicle>()
+                    .eq(PassengerAuthVehicle::getReportPeriod, period)) == 0;
+            case "RULE_P003":
+            case "RULE_P004":
+            case "RULE_P005":
+            case "RULE_P006":
+                return passengerMapper.selectCount(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
+                    .eq(PassengerEnterpriseMonthly::getReportPeriod, getLastPeriod(period))) == 0;
+            case "RULE_E001":
+            case "RULE_E002":
+            case "RULE_E003":
+                return energyAuthMapper.selectCount(new LambdaQueryWrapper<EnergyAuthVehicle>()
+                    .eq(EnergyAuthVehicle::getReportPeriod, period)) == 0;
+            case "RULE_E008":
+            case "RULE_E009":
+                return h2032Mapper.selectCount(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
+                    .eq(H2032EnterpriseMonthly::getReportPeriod, period)) == 0;
+            case "RULE_T001":
+                return cityTaxiAuthMapper.selectCount(new LambdaQueryWrapper<CityTaxiAuth>()
+                    .eq(CityTaxiAuth::getReportPeriod, period)) == 0;
+            case "RULE_I001":
+            case "RULE_I002":
+            case "RULE_I003":
+                return investSystemMapper.selectCount(new LambdaQueryWrapper<InvestmentSystem>()
+                    .eq(InvestmentSystem::getReportPeriod, period)) == 0;
+            default:
+                return false;
+        }
+    }
+
+    private List<AuditRule> rulesOf(String reportType) {
+        List<AuditRule> all = ruleMapper.selectEnabledRules();
+        List<AuditRule> matched = new ArrayList<>();
+        for (AuditRule rule : all) {
+            String type = rule.getReportType();
+            if (reportType.equals(type) || (type == null && "H2032".equals(reportType))) {
+                matched.add(rule);
+            }
+        }
+        return matched;
     }
 
     private AuditResult checkRule(AuditRule rule, H2032EnterpriseMonthly report,
@@ -152,7 +1572,7 @@
                 case "NULL_CHECK":
                     return checkNull(rule, report);
                 case "OUT_RANGE":
-                    return checkOutRange(rule, report, authMap);
+                    return checkOutRange(rule, report);
                 default:
                     return null;
             }
@@ -187,7 +1607,7 @@
         return null;
     }
 
-    /** 涓婃姤鍛ㄨ浆閲忎笌杞ㄨ抗娴嬬畻鍛ㄨ浆閲忓姣旓紝楂樺嚭30%鏍稿疄 */
+    /** 涓婃姤鍛ㄨ浆閲忎笌杞ㄨ抗娴嬬畻鍛ㄨ浆閲忓姣旓紝楂樺嚭闃堝�兼瘮渚嬫牳瀹� */
     private AuditResult checkRatio(AuditRule rule, H2032EnterpriseMonthly report,
                                    Map<String, TransportAuthVehicle> authMap,
                                    Map<String, VehicleTrackMileage> trackMap) {
@@ -215,67 +1635,59 @@
 
         if (ratio > threshold) {
             return buildResult(rule, report,
-                "涓婃姤鍛ㄨ浆閲�=" + fmt(actual) + ", 杞ㄨ抗娴嬬畻=" + Math.round(estimatedTurnover),
-                "姣斿�尖墹" + fmt(threshold), "姣斿��=" + fmt(ratio));
+                "涓婃姤鍛ㄨ浆閲�=" + fmt(actual) + ", 杞ㄨ抗娴嬬畻=" + fmt(estimatedTurnover),
+                "鈮よ建杩规祴绠椕�" + fmt(threshold), "=杞ㄨ抗娴嬬畻脳" + fmt(ratio));
         }
         return null;
     }
 
-    /** 璐ц繍绫诲瀷鐜瘮锛氭湰鏈堟柊澧炶揣杩愮被鍨嬫牳瀹烇紙澶氱殑鏍稿疄锛屽皯鐨勪笉绠★級 */
+    /** 璐ц繍绫诲瀷鐜瘮锛氭湰鏈堟柊澧烇紙涓婃湀涓�0銆佹湰鏈�>0锛夐渶鏍稿疄锛屽皯鐨勪笉绠� */
     private AuditResult checkMom(AuditRule rule, H2032EnterpriseMonthly report,
                                  Map<String, H2032EnterpriseMonthly> lastMonthMap) {
         H2032EnterpriseMonthly lastMonth = lastMonthMap.get(key(report.getEnterpriseCode()));
         if (lastMonth == null) return null;
-
-        String[] fields = {"freightContainer", "freightCoal", "freightOilGas", "freightCrudeOil",
-            "freightMetalOre", "freightIronOre", "freightBuilding", "freightGrain"};
-        List<String> newTypes = new ArrayList<>();
-        for (String field : fields) {
-            double current = getFieldValue(report, field);
-            double last = getFieldValue(lastMonth, field);
+        List<String> hits = new ArrayList<>();
+        for (String field : rule.getCheckField().split(",")) {
+            double current = getH2032FieldValue(report, field);
+            double last = getH2032FieldValue(lastMonth, field);
             if (current > 0 && last == 0) {
-                newTypes.add(FREIGHT_TYPE_NAMES.get(field) + "=" + fmt(current) + "鍚�");
+                hits.add(h2032FieldLabel(field) + "=" + fmt(current));
             }
         }
-        if (!newTypes.isEmpty()) {
-            return buildResult(rule, report, "鏈湀鏂板璐х被: " + String.join("銆�", newTypes),
-                "涓婃湀涓�0", "鐜瘮鏂板");
+        if (!hits.isEmpty()) {
+            return buildResult(rule, report, "鏈湀鏂板: " + String.join("銆�", hits),
+                "涓婃湀璇ョ被鍨嬩负0", "鏈湀鏂板璐ц繍绫诲瀷锛堜笂鏈堟棤锛�");
         }
         return null;
     }
 
-    /** 鏃犻泦瑁呯杞︿絾鏈夐泦瑁呯璐ц繍閲� */
+    /** 鏈夎揣杩愰噺浣嗗搴旇溅杈嗕负0 闇�鏍稿疄锛堝 鏈夐泦瑁呯璐ц繍閲忔棤闆嗚绠辫溅杈嗭級 */
     private AuditResult checkNull(AuditRule rule, H2032EnterpriseMonthly report) {
-        double containerFreight = nvl(report.getFreightContainer());
-        double containerVehicle = nvl(report.getVehicleContainer());
-        if (containerFreight > 0 && containerVehicle == 0) {
-            return buildResult(rule, report, "闆嗚绠辫揣杩愰噺=" + fmt(containerFreight),
-                "鏈夐泦瑁呯杞�", "鏃犻泦瑁呯杞︿絾鏈夐泦瑁呯璐ц繍閲�");
+        String[] fields = rule.getCheckField().split(",");
+        if (fields.length < 2) return null;
+        double volume = getH2032FieldValue(report, fields[0]);
+        double vehicle = getH2032FieldValue(report, fields[1]);
+        if (volume > 0 && vehicle == 0) {
+            return buildResult(rule, report,
+                h2032FieldLabel(fields[0]) + "=" + fmt(volume) + ", " + h2032FieldLabel(fields[1]) + "=0",
+                "鏈�" + h2032FieldLabel(fields[0]) + "搴旀湁瀵瑰簲杞﹁締", "鏈�" + h2032FieldLabel(fields[0]) + "浣嗘棤" + h2032FieldLabel(fields[1]));
         }
         return null;
     }
 
-    /** 鏁磋溅鍚ㄤ綅涓嶅湪(4,40]鑼冨洿鍐呮牳瀹� */
-    private AuditResult checkOutRange(AuditRule rule, H2032EnterpriseMonthly report,
-                                      Map<String, TransportAuthVehicle> authMap) {
-        TransportAuthVehicle auth = findAuth(authMap, report.getEnterpriseName());
-        if (auth == null) return null;
-        if (num(auth.getOtherCount()) == 0) return null;
-
-        double wholeTonnage = nvl(auth.getOtherTons()) / num(auth.getOtherCount());
-
-        String[] range = rule.getThreshold().split(",");
-        double min = Double.parseDouble(range[0]);
-        double max = Double.parseDouble(range[1]);
-
-        if (wholeTonnage < min || wholeTonnage > max) {
-            return buildResult(rule, report, "鏁磋溅鍚ㄤ綅=" + fmt(wholeTonnage),
-                fmt(min) + " 鈮� 鏁磋溅鍚ㄤ綅 鈮� " + fmt(max), "瓒呭嚭鑼冨洿");
+    /** 鏁板�艰寖鍥存鏌ワ紙濡� 鏁磋溅鍚ㄤ綅 4鈮や笖鈮�40锛� */
+    private AuditResult checkOutRange(AuditRule rule, H2032EnterpriseMonthly report) {
+        String[] bounds = rule.getThreshold().split(",");
+        if (bounds.length < 2) return null;
+        double value = getH2032FieldValue(report, rule.getCheckField());
+        double low = Double.parseDouble(bounds[0].trim());
+        double high = Double.parseDouble(bounds[1].trim());
+        if (value > 0 && (value < low || value > high)) {
+            return buildResult(rule, report, h2032FieldLabel(rule.getCheckField()) + "=" + fmt(value),
+                "鑼冨洿 " + fmt(low) + "~" + fmt(high), "瓒呭嚭鑼冨洿");
         }
         return null;
     }
-
-    // ========== 杈呭姪鏂规硶 ==========
 
     private AuditResult buildResult(AuditRule rule, H2032EnterpriseMonthly report,
                                     String actual, String threshold, String deviation) {
@@ -291,6 +1703,10 @@
         return result;
     }
 
+    private TransportAuthVehicle findAuth(Map<String, TransportAuthVehicle> authMap, String name) {
+        return authMap.get(key(name));
+    }
+
     private Map<String, TransportAuthVehicle> loadAuthMap(String reportPeriod) {
         Map<String, TransportAuthVehicle> map = new HashMap<>();
         List<TransportAuthVehicle> list = transportAuthMapper.selectList(
@@ -302,11 +1718,11 @@
         return map;
     }
 
-    private Map<String, VehicleTrackMileage> loadTrackMap(String period) {
+    private Map<String, VehicleTrackMileage> loadTrackMap(String reportPeriod) {
         Map<String, VehicleTrackMileage> map = new HashMap<>();
         List<VehicleTrackMileage> list = trackMileageMapper.selectList(
             new LambdaQueryWrapper<VehicleTrackMileage>()
-                .eq(VehicleTrackMileage::getReportPeriod, period));
+                .eq(VehicleTrackMileage::getReportPeriod, reportPeriod));
         for (VehicleTrackMileage v : list) {
             map.putIfAbsent(key(v.getEnterpriseName()), v);
         }
@@ -325,40 +1741,20 @@
         return map;
     }
 
-    private TransportAuthVehicle findAuth(Map<String, TransportAuthVehicle> authMap, String name) {
-        return authMap.get(key(name));
-    }
-
-    private String key(String s) {
-        return s == null ? "" : s.trim();
-    }
-
-    /** double 鏄剧ず鏍煎紡鍖栵細閬垮厤绉戝璁℃暟娉曪紝鏁存暟鍊间笉甯﹀皬鏁� */
-    private String fmt(double v) {
-        if (Double.isNaN(v) || Double.isInfinite(v)) return String.valueOf(v);
-        if (v == Math.rint(v) && Math.abs(v) < 1e15) {
-            return String.valueOf((long) v);
+    private String getLastPeriod(String period) {
+        String[] parts = period.split("-");
+        int year = Integer.parseInt(parts[0]);
+        int month = Integer.parseInt(parts[1]);
+        if (month == 1) {
+            year--;
+            month = 12;
+        } else {
+            month--;
         }
-        String s = String.format("%.6f", v);
-        if (s.indexOf('.') >= 0) {
-            s = s.replaceAll("0+$", "").replaceAll("\\.$", "");
-        }
-        return s;
+        return year + "-" + (month < 10 ? "0" + month : String.valueOf(month));
     }
 
-    private double nvl(Double v) {
-        return v == null ? 0.0 : v;
-    }
-
-    private double nvl(Integer v) {
-        return v == null ? 0.0 : v;
-    }
-
-    private int num(Integer v) {
-        return v == null ? 0 : v;
-    }
-
-    private double getFieldValue(H2032EnterpriseMonthly report, String fieldName) {
+    private double getH2032FieldValue(H2032EnterpriseMonthly report, String fieldName) {
         try {
             java.lang.reflect.Field field = H2032EnterpriseMonthly.class.getDeclaredField(fieldName);
             field.setAccessible(true);
@@ -372,16 +1768,257 @@
         }
     }
 
-    private String getLastPeriod(String period) {
-        String[] parts = period.split("-");
-        int year = Integer.parseInt(parts[0]);
-        int month = Integer.parseInt(parts[1]);
-        if (month == 1) {
-            year--;
-            month = 12;
-        } else {
-            month--;
-        }
-        return String.format("%d-%02d", year, month);
+    private String h2032FieldLabel(String field) {
+        String label = FREIGHT_TYPE_NAMES.get(field);
+        if (label != null) return label;
+        java.util.Map<String, String> labels = new HashMap<>();
+        labels.put("vehicleTotal", "杞﹁締鏁�");
+        labels.put("tonsTotal", "鎬诲惃浣�");
+        labels.put("vehicleContainer", "闆嗚绠辫溅杈嗘暟");
+        labels.put("tonsWhole", "鏁磋溅鍚ㄤ綅");
+        labels.put("turnoverTotal", "鍛ㄨ浆閲�");
+        labels.put("vehicleWhole", "鏁磋溅杞﹁締鏁�");
+        String v = labels.get(field);
+        return v == null ? field : v;
     }
-}
+
+    private boolean nearlyEqual(double a, double b) {
+        if (a == b) return true;
+        return Math.abs(a - b) <= 1e-6 * Math.max(1.0, Math.max(Math.abs(a), Math.abs(b)));
+    }
+
+    private String key(String s) {
+        if (s == null) return "";
+        return s.trim().replaceAll("\\s+", "");
+    }
+
+    private double nvl(Double v) {
+        return v == null ? 0.0 : v;
+    }
+
+    private double num(Integer v) {
+        return v == null ? 0.0 : v.doubleValue();
+    }
+
+    private String fmt(double v) {
+        if (v == Math.floor(v) && !Double.isInfinite(v)) {
+            return String.valueOf((long) v);
+        }
+        return String.valueOf(Math.round(v * 100.0) / 100.0);
+    }
+
+    /** 鍗犳瘮/灏忔暟鍊硷細淇濈暀 4 浣嶅皬鏁� */
+    private String fmt4(double v) {
+        if (v == Math.floor(v) && !Double.isInfinite(v)) {
+            return String.valueOf((long) v);
+        }
+        return String.valueOf(Math.round(v * 10000.0) / 10000.0);
+    }
+
+    // ========== H2031 鏃呭瀹℃牳 ==========
+
+    private AuditResult checkPassengerRule(AuditRule rule, PassengerEnterpriseMonthly report,
+                                           Map<String, PassengerAuthVehicle> authMap,
+                                           Map<String, PassengerEnterpriseMonthly> lastMonthMap) {
+        try {
+            switch (rule.getCompareType()) {
+                case "DIFF":
+                    return checkPassengerDiff(rule, report, authMap);
+                case "MOM_EQ_ZERO":
+                    return checkPassengerMomEqZero(rule, report, lastMonthMap);
+                case "MOM_PCT":
+                    return checkPassengerMomPct(rule, report, lastMonthMap);
+                case "MOM_EQ":
+                    return checkPassengerMomEq(rule, report, lastMonthMap);
+                case "EXISTENCE":
+                    return checkPassengerExistence(rule, report);
+                default:
+                    return null;
+            }
+        } catch (Exception e) {
+            log.error("Passenger rule check error: {} - {}", rule.getRuleCode(), e.getMessage());
+            return null;
+        }
+    }
+
+    /** 杞﹁締鏁�/杞藉浣嶆暟 涓庤繍鏀垮姣旓紝宸窛瓒呴槇鍊兼牳瀹� */
+    private AuditResult checkPassengerDiff(AuditRule rule, PassengerEnterpriseMonthly report,
+                                           Map<String, PassengerAuthVehicle> authMap) {
+        PassengerAuthVehicle auth = authMap.get(key(report.getEnterpriseName()));
+        if (auth == null) return null;
+        double actual;
+        double reference;
+        if (rule.getCheckField().contains("seat")) {
+            actual = num(report.getSeatTotal());
+            reference = num(auth.getSeatCount());
+        } else {
+            actual = num(report.getVehicleTotal());
+            reference = num(auth.getVehicleCount());
+        }
+        double threshold = Double.parseDouble(rule.getThreshold());
+        double diff = Math.abs(actual - reference);
+        if (diff > threshold) {
+            return buildPassengerResult(rule, report, "涓婃姤=" + fmt(actual) + ", 杩愭斂=" + fmt(reference),
+                "宸窛鈮�" + fmt(threshold), "宸窛=" + fmt(diff));
+        }
+        return null;
+    }
+
+    /** 鐜瘮涓婁釜鏈堢瓑浜�0锛氬�间笌涓婃湀鐩稿悓闇�鏍稿疄锛堜笂鏈堜负0涓嶆牳瀹烇級 */
+    private AuditResult checkPassengerMomEqZero(AuditRule rule, PassengerEnterpriseMonthly report,
+                                                Map<String, PassengerEnterpriseMonthly> lastMonthMap) {
+        PassengerEnterpriseMonthly lastMonth = lastMonthMap.get(key(report.getEnterpriseCode()));
+        if (lastMonth == null) return null;
+        List<String> hits = new ArrayList<>();
+        for (String field : rule.getCheckField().split(",")) {
+            double current = getPassengerFieldValue(report, field);
+            double last = getPassengerFieldValue(lastMonth, field);
+            if (last > 0 && nearlyEqual(current, last)) {
+                hits.add(fieldLabel(field) + "=" + fmt(current));
+            }
+        }
+        if (!hits.isEmpty()) {
+            return buildPassengerResult(rule, report, "鏈湀涓庝笂鏈堢浉鍚�: " + String.join("銆�", hits),
+                "鐜瘮鈮�0", "鐜瘮=0");
+        }
+        return null;
+    }
+
+    /** 骞冲潎杩愯窛鐜瘮瓒� 卤threshold% 鏍稿疄 */
+    private AuditResult checkPassengerMomPct(AuditRule rule, PassengerEnterpriseMonthly report,
+                                             Map<String, PassengerEnterpriseMonthly> lastMonthMap) {
+        PassengerEnterpriseMonthly lastMonth = lastMonthMap.get(key(report.getEnterpriseCode()));
+        if (lastMonth == null) return null;
+        double threshold = Double.parseDouble(rule.getThreshold());
+        List<String> hits = new ArrayList<>();
+        for (String field : rule.getCheckField().split(",")) {
+            double current = getPassengerFieldValue(report, field);
+            double last = getPassengerFieldValue(lastMonth, field);
+            if (last > 0) {
+                double pct = Math.abs((current - last) / last) * 100.0;
+                if (pct > threshold) {
+                    hits.add(fieldLabel(field) + "=" + fmt(current) + "锛屼笂鏈�=" + fmt(last) + "锛岀幆姣�" + fmt(pct) + "%");
+                }
+            }
+        }
+        if (!hits.isEmpty()) {
+            return buildPassengerResult(rule, report, String.join("锛�", hits),
+                "鐜瘮鈮ぢ�" + fmt(threshold) + "%", "鐜瘮瓒呴檺");
+        }
+        return null;
+    }
+
+    /** 鍖呰溅骞冲潎杩愯窛绛変簬涓婃湀鏍稿疄 */
+    private AuditResult checkPassengerMomEq(AuditRule rule, PassengerEnterpriseMonthly report,
+                                            Map<String, PassengerEnterpriseMonthly> lastMonthMap) {
+        PassengerEnterpriseMonthly lastMonth = lastMonthMap.get(key(report.getEnterpriseCode()));
+        if (lastMonth == null) return null;
+        List<String> hits = new ArrayList<>();
+        for (String field : rule.getCheckField().split(",")) {
+            double current = getPassengerFieldValue(report, field);
+            double last = getPassengerFieldValue(lastMonth, field);
+            if (last > 0 && nearlyEqual(current, last)) {
+                hits.add(fieldLabel(field) + "=" + fmt(current));
+            }
+        }
+        if (!hits.isEmpty()) {
+            return buildPassengerResult(rule, report, "绛変簬涓婃湀: " + String.join("銆�", hits),
+                "涓庝笂鏈堜笉鍚�", "涓庝笂鏈堢浉鍚�");
+        }
+        return null;
+    }
+
+    /** 鏃犵彮绾�/鏃犲寘杞﹀嵈鏈夊搴斿杩愰噺锛歝heck_field 棣栦釜涓鸿溅杈嗗瓧娈碉紝鍏朵綑涓哄杩愰噺瀛楁 */
+    private AuditResult checkPassengerExistence(AuditRule rule, PassengerEnterpriseMonthly report) {
+        String[] fields = rule.getCheckField().split(",");
+        if (fields.length < 2) return null;
+        String vehicleField = fields[0];
+        double vehicleCount = getPassengerFieldValue(report, vehicleField);
+        if (vehicleCount > 0) return null;
+        List<String> hits = new ArrayList<>();
+        for (int i = 1; i < fields.length; i++) {
+            double value = getPassengerFieldValue(report, fields[i]);
+            if (value > 0) {
+                hits.add(fieldLabel(fields[i]) + "=" + fmt(value));
+            }
+        }
+        if (!hits.isEmpty()) {
+            String label = "schedule".equals(vehicleField) || "vehicleSchedule".equals(vehicleField) ? "鐝嚎" : "鍖呰溅";
+            return buildPassengerResult(rule, report, String.join("銆�", hits),
+                "鏈�" + label + "杞﹁締", "鏃�" + label + "杞﹁締浣嗘湁瀹㈣繍閲�");
+        }
+        return null;
+    }
+
+    private Map<String, PassengerAuthVehicle> loadPassengerAuthMap(String reportPeriod) {
+        Map<String, PassengerAuthVehicle> map = new HashMap<>();
+        List<PassengerAuthVehicle> list = passengerAuthMapper.selectList(
+            new LambdaQueryWrapper<PassengerAuthVehicle>()
+                .eq(PassengerAuthVehicle::getReportPeriod, reportPeriod));
+        for (PassengerAuthVehicle v : list) {
+            map.putIfAbsent(key(v.getEnterpriseName()), v);
+        }
+        return map;
+    }
+
+    private Map<String, PassengerEnterpriseMonthly> loadPassengerLastMonthMap(String period) {
+        Map<String, PassengerEnterpriseMonthly> map = new HashMap<>();
+        String lastPeriod = getLastPeriod(period);
+        List<PassengerEnterpriseMonthly> list = passengerMapper.selectList(
+            new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
+                .eq(PassengerEnterpriseMonthly::getReportPeriod, lastPeriod));
+        for (PassengerEnterpriseMonthly v : list) {
+            map.putIfAbsent(key(v.getEnterpriseCode()), v);
+        }
+        return map;
+    }
+
+    private double getPassengerFieldValue(PassengerEnterpriseMonthly report, String fieldName) {
+        try {
+            java.lang.reflect.Field field = PassengerEnterpriseMonthly.class.getDeclaredField(fieldName);
+            field.setAccessible(true);
+            Object value = field.get(report);
+            if (value == null) return 0.0;
+            if (value instanceof Double) return (Double) value;
+            if (value instanceof Integer) return ((Integer) value).doubleValue();
+            return Double.parseDouble(value.toString());
+        } catch (Exception e) {
+            return 0.0;
+        }
+    }
+
+    private String fieldLabel(String field) {
+        java.util.Map<String, String> labels = new HashMap<>();
+        labels.put("passengerClass1", "涓�绫荤彮绾垮杩愰噺");
+        labels.put("passengerClass2", "浜岀被鐝嚎瀹㈣繍閲�");
+        labels.put("passengerClass3", "涓夌被鐝嚎瀹㈣繍閲�");
+        labels.put("passengerClass4", "鍥涚被鐝嚎瀹㈣繍閲�");
+        labels.put("passengerCharter", "鍖呰溅瀹㈣繍閲�");
+        labels.put("turnoverClass1", "涓�绫荤彮绾垮懆杞噺");
+        labels.put("turnoverClass2", "浜岀被鐝嚎鍛ㄨ浆閲�");
+        labels.put("turnoverClass3", "涓夌被鐝嚎鍛ㄨ浆閲�");
+        labels.put("turnoverClass4", "鍥涚被鐝嚎鍛ㄨ浆閲�");
+        labels.put("turnoverCharter", "鍖呰溅鍛ㄨ浆閲�");
+        labels.put("avgDistanceClass1", "涓�绫荤彮绾垮钩鍧囪繍璺�");
+        labels.put("avgDistanceClass2", "浜岀被鐝嚎骞冲潎杩愯窛");
+        labels.put("avgDistanceClass3", "涓夌被鐝嚎骞冲潎杩愯窛");
+        labels.put("avgDistanceClass4", "鍥涚被鐝嚎骞冲潎杩愯窛");
+        labels.put("avgDistanceCharter", "鍖呰溅骞冲潎杩愯窛");
+        String label = labels.get(field);
+        return label == null ? field : label;
+    }
+
+    private AuditResult buildPassengerResult(AuditRule rule, PassengerEnterpriseMonthly report,
+                                             String actual, String threshold, String deviation) {
+        AuditResult result = new AuditResult();
+        result.setRuleId(rule.getId());
+        result.setReportId(report.getId());
+        result.setEnterpriseCode(report.getEnterpriseCode());
+        result.setReportPeriod(report.getReportPeriod());
+        result.setActualValue(actual);
+        result.setThresholdValue(threshold);
+        result.setDeviation(deviation);
+        result.setStatus("PENDING");
+        return result;
+    }
+}
\ No newline at end of file

--
Gitblit v1.9.1