xyc
5 天以前 664cae654867b528ea339f187ebe290cb04d6fcf
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 环比基准:上一季度同车牌百公里单耗
        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) + ",上季度=" + 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) + " 超出范围");
    }
    /** 上一季度报表期: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% 需核实(RED) */
    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;
    }
    /** 无班线/无包车却有对应客运量:check_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;
    }
}