package com.trafficaudit.auditengine.service; 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; import com.trafficaudit.rulemanage.mapper.AuditRuleMapper; 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; import java.util.List; import java.util.Map; /** * 审核规则引擎(Demo 6条规则) */ @Slf4j @Service public class AuditEngineService { /** 牵引车标准吨位 */ private static final double TRACTOR_STD_TONS = 31.0; /** 货运类型字段 → 中文名称 */ private static final Map FREIGHT_TYPE_NAMES = new HashMap<>(); static { FREIGHT_TYPE_NAMES.put("freightContainer", "集装箱"); FREIGHT_TYPE_NAMES.put("freightCoal", "煤炭及制品"); FREIGHT_TYPE_NAMES.put("freightOilGas", "石油天然气及制品"); FREIGHT_TYPE_NAMES.put("freightCrudeOil", "原油"); FREIGHT_TYPE_NAMES.put("freightMetalOre", "金属矿石"); FREIGHT_TYPE_NAMES.put("freightIronOre", "铁矿石"); FREIGHT_TYPE_NAMES.put("freightBuilding", "矿物性建筑材料"); FREIGHT_TYPE_NAMES.put("freightGrain", "粮食"); } /** H204 车辆类型码 -> 允许的运政车辆类型(已去掉重/中/轻型前缀) */ private static final Map> ENERGY_VEHICLE_TYPE_ALLOWED = new HashMap<>(); /** H204 燃料类型码 -> 允许的运政燃料类型 */ private static final Map> 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 rules, boolean hasData) { if (rules == null || rules.isEmpty() || !hasData) return; Long cnt = auditRunMapper.selectCount(new LambdaQueryWrapper() .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 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() .eq(PassengerEnterpriseMonthly::getReportPeriod, reportPeriod)); } else if ("H204".equals(reportType)) { dataCount = energyMapper.selectCount(new LambdaQueryWrapper() .eq(EnergyVehicleQuarterly::getReportPeriod, reportPeriod)); } else if ("INVEST".equals(reportType)) { dataCount = investMonthlyMapper.selectCount(new LambdaQueryWrapper() .eq(InvestmentMonthly::getReportPeriod, reportPeriod)); } else if ("CITY_BUS".equals(reportType)) { dataCount = cityBusMapper.selectCount(new LambdaQueryWrapper() .eq(CityBusMonthly::getReportPeriod, reportPeriod)); } else if ("CITY_TAXI".equals(reportType)) { dataCount = cityTaxiMapper.selectCount(new LambdaQueryWrapper() .eq(CityTaxiMonthly::getReportPeriod, reportPeriod)); } else { dataCount = h2032Mapper.selectCount(new LambdaQueryWrapper() .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 executeAudit(String reportPeriod) { List rules = rulesOf("H2032"); List reports = h2032Mapper.selectList( new LambdaQueryWrapper() .eq(H2032EnterpriseMonthly::getReportPeriod, reportPeriod)); // 加载运政/轨迹数据,按企业名索引 Map authMap = loadAuthMap(reportPeriod); Map trackMap = loadTrackMap(reportPeriod); Map lastMonthMap = loadLastMonthMap(reportPeriod); // 幂等:同报表期先清除旧审核结果 resultMapper.delete(new LambdaQueryWrapper() .eq(AuditResult::getReportPeriod, reportPeriod)); Map ruleMap = new HashMap<>(); for (AuditRule rule : rules) ruleMap.put(rule.getId(), rule); List results = new ArrayList<>(); for (H2032EnterpriseMonthly report : reports) { for (AuditRule rule : rules) { AuditResult result = checkRule(rule, report, authMap, trackMap, 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("Audit complete: {} reports, {} issues for {}", reports.size(), results.size(), reportPeriod); if (!rules.isEmpty() && !reports.isEmpty()) markAudited("H2032", reportPeriod); return results; } public List executePassengerAudit(String reportPeriod) { List rules = rulesOf("H2031"); List reports = passengerMapper.selectList( new LambdaQueryWrapper() .eq(PassengerEnterpriseMonthly::getReportPeriod, reportPeriod)); Map authMap = loadPassengerAuthMap(reportPeriod); Map lastMonthMap = loadPassengerLastMonthMap(reportPeriod); // 幂等:只清除 H2031 规则的旧审核结果 List ruleIds = new ArrayList<>(); for (AuditRule rule : rules) ruleIds.add(rule.getId()); if (!ruleIds.isEmpty()) { resultMapper.delete(new LambdaQueryWrapper() .eq(AuditResult::getReportPeriod, reportPeriod) .in(AuditResult::getRuleId, ruleIds)); } Map ruleMap = new HashMap<>(); for (AuditRule rule : rules) ruleMap.put(rule.getId(), rule); List 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 executeEnergyAudit(String reportPeriod) { List rules = rulesOf("H204"); List vehicles = energyMapper.selectList( new LambdaQueryWrapper() .eq(EnergyVehicleQuarterly::getReportPeriod, reportPeriod)); Map authMap = new HashMap<>(); List authList = energyAuthMapper.selectList( new LambdaQueryWrapper() .eq(EnergyAuthVehicle::getReportPeriod, reportPeriod)); for (EnergyAuthVehicle v : authList) { authMap.putIfAbsent(key(v.getPlateNo()), v); } // E007 环比基准:上一季度同车牌百公里单耗 String lastPeriod = prevQuarter(reportPeriod); Map lastQuarterMap = new HashMap<>(); if (lastPeriod != null) { for (EnergyVehicleQuarterly v : energyMapper.selectList( new LambdaQueryWrapper() .eq(EnergyVehicleQuarterly::getReportPeriod, lastPeriod))) { lastQuarterMap.putIfAbsent(key(v.getPlateNo()), v); } } // E006 超范围基准:本期各燃料类型百公里单耗中位数 Map fuelMedianMap = fuelPer100kmMedian(vehicles); // E008/E009 对比基准:同期 H2032 企业月报(按企业名索引) Map h2032Map = new HashMap<>(); for (H2032EnterpriseMonthly m : h2032Mapper.selectList( new LambdaQueryWrapper() .eq(H2032EnterpriseMonthly::getReportPeriod, reportPeriod))) { String ent = m.getEnterpriseName(); if (ent != null && !ent.trim().isEmpty()) h2032Map.putIfAbsent(ent.trim(), m); } // 幂等:只清除 H204 规则的旧审核结果 List ruleIds = new ArrayList<>(); for (AuditRule rule : rules) ruleIds.add(rule.getId()); if (!ruleIds.isEmpty()) { resultMapper.delete(new LambdaQueryWrapper() .eq(AuditResult::getReportPeriod, reportPeriod) .in(AuditResult::getRuleId, ruleIds)); } List 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 authMap, Map fuelMedianMap, Map lastQuarterMap, Map 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 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 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 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 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 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 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 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 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 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 fuelPer100kmMedian(List vehicles) { Map> 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 med = new HashMap<>(); for (Map.Entry> e : byFuel.entrySet()) { List 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 loadEnergyAuthMap(String reportPeriod) { Map map = new HashMap<>(); List list = energyAuthMapper.selectList( new LambdaQueryWrapper() .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 executeInvestmentAudit(String reportPeriod) { List rules = rulesOf("INVEST"); List monthlies = investMonthlyMapper.selectList( new LambdaQueryWrapper() .eq(InvestmentMonthly::getReportPeriod, reportPeriod)); Map projectMap = new HashMap<>(); for (InvestmentProject p : investProjectMapper.selectList(null)) { projectMap.put(p.getId(), p); } Map> sysMap = new HashMap<>(); for (InvestmentSystem s : investSystemMapper.selectList( new LambdaQueryWrapper() .eq(InvestmentSystem::getReportPeriod, reportPeriod))) { sysMap.computeIfAbsent(normInvestName(s.getProjectName()), k -> new ArrayList<>()).add(s); } // 幂等:只清除 INVEST 规则的旧审核结果 List ruleIds = new ArrayList<>(); for (AuditRule rule : rules) ruleIds.add(rule.getId()); if (!ruleIds.isEmpty()) { resultMapper.delete(new LambdaQueryWrapper() .eq(AuditResult::getReportPeriod, reportPeriod) .in(AuditResult::getRuleId, ruleIds)); } List 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 executeCityBusAudit(String reportPeriod) { List rules = rulesOf("CITY_BUS"); List rows = cityBusMapper.selectList( new LambdaQueryWrapper() .eq(CityBusMonthly::getReportPeriod, reportPeriod)); // 幂等:只清除 CITY_BUS 规则的旧审核结果 List ruleIds = new ArrayList<>(); for (AuditRule rule : rules) ruleIds.add(rule.getId()); if (!ruleIds.isEmpty()) { resultMapper.delete(new LambdaQueryWrapper() .eq(AuditResult::getReportPeriod, reportPeriod) .in(AuditResult::getRuleId, ruleIds)); } List 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 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 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 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 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 executeCityTaxiAudit(String reportPeriod) { List rules = rulesOf("CITY_TAXI"); List rows = cityTaxiMapper.selectList( new LambdaQueryWrapper() .eq(CityTaxiMonthly::getReportPeriod, reportPeriod)); // 幂等:只清除 CITY_TAXI 规则的旧审核结果 List ruleIds = new ArrayList<>(); for (AuditRule rule : rules) ruleIds.add(rule.getId()); if (!ruleIds.isEmpty()) { resultMapper.delete(new LambdaQueryWrapper() .eq(AuditResult::getReportPeriod, reportPeriod) .in(AuditResult::getRuleId, ruleIds)); } // 运政车辆按市州计数(T001 对比基准) Map authCount = loadTaxiAuthCount(reportPeriod); List 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 loadTaxiAuthCount(String reportPeriod) { Map map = new HashMap<>(); List auths = cityTaxiAuthMapper.selectList( new LambdaQueryWrapper() .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 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 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 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 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 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 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> sysMap, String projectName) { String norm = normInvestName(projectName); if (norm.isEmpty()) return null; List exact = sysMap.get(norm); if (exact != null && !exact.isEmpty()) { return disambiguateInvest(exact, projectName); } InvestmentSystem best = null; int bestScore = Integer.MIN_VALUE; for (Map.Entry> 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 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 queryResults(String reportPeriod, String reportType) { String type = reportType == null || reportType.trim().isEmpty() ? "H2032" : reportType.trim(); List results = resultMapper.selectList(new LambdaQueryWrapper() .eq(AuditResult::getReportPeriod, reportPeriod) .orderByAsc(AuditResult::getId)); List ruleIds = new ArrayList<>(); for (AuditRule rule : ruleMapper.selectList(null)) { if (type.equals(rule.getReportType())) ruleIds.add(rule.getId()); } List 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) { AuditResult result = resultMapper.selectById(id); if (result == null) return; result.setStatus(status); result.setReviewComment(comment); result.setReviewedAt(LocalDateTime.now()); resultMapper.updateById(result); } private void attachInfo(List results) { if (results.isEmpty()) return; Map ruleMap = new HashMap<>(); for (AuditRule rule : ruleMapper.selectList(null)) ruleMap.put(rule.getId(), rule); Map h2032Name = new HashMap<>(); Map h2032Explain = new HashMap<>(); Map passengerName = new HashMap<>(); Map passengerExplain = new HashMap<>(); Map energyName = new HashMap<>(); Map investName = new HashMap<>(); Map busName = new HashMap<>(); Map busExplain = new HashMap<>(); Map taxiName = new HashMap<>(); Map taxiExplain = new HashMap<>(); for (H2032EnterpriseMonthly r : h2032Mapper.selectList(null)) { 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 investProjectId = new HashMap<>(); for (InvestmentMonthly r : investMonthlyMapper.selectList(null)) { investProjectId.putIfAbsent(r.getId(), r.getProjectId()); } for (InvestmentProject p : investProjectMapper.selectList(null)) { for (Map.Entry 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()); if (rule != null) { result.setRuleName(rule.getRuleName()); result.setRuleCode(rule.getRuleCode()); result.setAlertLevel(rule.getAlertLevel()); } // 各报表表 id 各自独立自增,需按规则所属报表类型分表回填企业名称 String type = rule == null ? null : rule.getReportType(); Map 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 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) { // 上报数据被重新导入后旧审核结果已失效,给出提示而非空白 enterpriseName = "\uFF08\u539F\u6570\u636E\u5DF2\u91CD\u65B0\u5BFC\u5165\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C\u5BA1\u6838\uFF09"; } 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().select(PassengerEnterpriseMonthly::getReportPeriod))) { max = maxPeriod(max, m.getReportPeriod()); } break; case "H204": for (EnergyVehicleQuarterly m : energyMapper.selectList( new LambdaQueryWrapper().select(EnergyVehicleQuarterly::getReportPeriod))) { max = maxPeriod(max, m.getReportPeriod()); } break; case "INVEST": for (InvestmentMonthly m : investMonthlyMapper.selectList( new LambdaQueryWrapper().select(InvestmentMonthly::getReportPeriod))) { max = maxPeriod(max, m.getReportPeriod()); } break; case "CITY_BUS": for (CityBusMonthly m : cityBusMapper.selectList( new LambdaQueryWrapper().select(CityBusMonthly::getReportPeriod))) { max = maxPeriod(max, m.getReportPeriod()); } break; case "CITY_TAXI": for (CityTaxiMonthly m : cityTaxiMapper.selectList( new LambdaQueryWrapper().select(CityTaxiMonthly::getReportPeriod))) { max = maxPeriod(max, m.getReportPeriod()); } break; default: for (H2032EnterpriseMonthly m : h2032Mapper.selectList( new LambdaQueryWrapper().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 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> ruleStats(String reportPeriod, String reportType) { String type = reportType == null || reportType.trim().isEmpty() ? "H2032" : reportType.trim(); List rules = rulesOf(type); long checked; switch (type) { case "H2031": checked = passengerMapper.selectCount(new LambdaQueryWrapper() .eq(PassengerEnterpriseMonthly::getReportPeriod, reportPeriod)); break; case "H204": checked = energyMapper.selectCount(new LambdaQueryWrapper() .eq(EnergyVehicleQuarterly::getReportPeriod, reportPeriod)); break; case "INVEST": checked = investMonthlyMapper.selectCount(new LambdaQueryWrapper() .eq(InvestmentMonthly::getReportPeriod, reportPeriod)); break; case "CITY_BUS": checked = cityBusMapper.selectCount(new LambdaQueryWrapper() .eq(CityBusMonthly::getReportPeriod, reportPeriod)); break; case "CITY_TAXI": checked = cityTaxiMapper.selectCount(new LambdaQueryWrapper() .eq(CityTaxiMonthly::getReportPeriod, reportPeriod)); break; default: checked = h2032Mapper.selectCount(new LambdaQueryWrapper() .eq(H2032EnterpriseMonthly::getReportPeriod, reportPeriod)); break; } List results = resultMapper.selectList(new LambdaQueryWrapper() .eq(AuditResult::getReportPeriod, reportPeriod)); Map hitMap = new HashMap<>(); for (AuditResult r : results) { hitMap.merge(r.getRuleId(), 1L, Long::sum); } List> out = new ArrayList<>(); for (AuditRule rule : rules) { Map 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() .eq(TransportAuthVehicle::getReportPeriod, period)) == 0; case "RULE_003": return trackMileageMapper.selectCount(new LambdaQueryWrapper() .eq(VehicleTrackMileage::getReportPeriod, period)) == 0; case "RULE_004": return h2032Mapper.selectCount(new LambdaQueryWrapper() .eq(H2032EnterpriseMonthly::getReportPeriod, getLastPeriod(period))) == 0; case "RULE_P001": case "RULE_P002": return passengerAuthMapper.selectCount(new LambdaQueryWrapper() .eq(PassengerAuthVehicle::getReportPeriod, period)) == 0; case "RULE_P003": case "RULE_P004": case "RULE_P005": case "RULE_P006": return passengerMapper.selectCount(new LambdaQueryWrapper() .eq(PassengerEnterpriseMonthly::getReportPeriod, getLastPeriod(period))) == 0; case "RULE_E001": case "RULE_E002": case "RULE_E003": return energyAuthMapper.selectCount(new LambdaQueryWrapper() .eq(EnergyAuthVehicle::getReportPeriod, period)) == 0; case "RULE_E008": case "RULE_E009": return h2032Mapper.selectCount(new LambdaQueryWrapper() .eq(H2032EnterpriseMonthly::getReportPeriod, period)) == 0; case "RULE_T001": return cityTaxiAuthMapper.selectCount(new LambdaQueryWrapper() .eq(CityTaxiAuth::getReportPeriod, period)) == 0; case "RULE_I001": case "RULE_I002": case "RULE_I003": return investSystemMapper.selectCount(new LambdaQueryWrapper() .eq(InvestmentSystem::getReportPeriod, period)) == 0; default: return false; } } private List rulesOf(String reportType) { List all = ruleMapper.selectEnabledRules(); List 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, Map authMap, Map trackMap, Map lastMonthMap) { try { switch (rule.getCompareType()) { case "DIFF": return checkDiff(rule, report, authMap); case "RATIO": return checkRatio(rule, report, authMap, trackMap); case "MOM": return checkMom(rule, report, lastMonthMap); case "NULL_CHECK": return checkNull(rule, report); case "OUT_RANGE": return checkOutRange(rule, report); default: return null; } } catch (Exception e) { log.error("Rule check error: {} - {}", rule.getRuleCode(), e.getMessage()); return null; } } /** 车辆数/吨位 与运政数据对比,差距超阈值核实 */ private AuditResult checkDiff(AuditRule rule, H2032EnterpriseMonthly report, Map authMap) { TransportAuthVehicle auth = findAuth(authMap, report.getEnterpriseName()); if (auth == null) return null; double actual; double reference; if (rule.getCheckField().contains("tons")) { actual = nvl(report.getTonsTotal()); reference = nvl(auth.getTrailerTons()) + nvl(auth.getOtherTons()); } else { actual = num(report.getVehicleTotal()); reference = num(auth.getTractorCount()) + num(auth.getTrailerCount()) + num(auth.getOtherCount()); } double threshold = Double.parseDouble(rule.getThreshold()); double diff = Math.abs(actual - reference); if (diff > threshold) { return buildResult(rule, report, "上报=" + fmt(actual) + ", 运政=" + fmt(reference), "差距≤" + fmt(threshold), "差距=" + fmt(diff)); } return null; } /** 上报周转量与轨迹测算周转量对比,高出阈值比例核实 */ private AuditResult checkRatio(AuditRule rule, H2032EnterpriseMonthly report, Map authMap, Map trackMap) { TransportAuthVehicle auth = findAuth(authMap, report.getEnterpriseName()); VehicleTrackMileage track = trackMap.get(key(report.getEnterpriseName())); if (auth == null || track == null) return null; double trackVehicles = num(track.getTrackedVehicles()); if (trackVehicles == 0.0) return null; if (num(auth.getOtherCount()) == 0) return null; // 整车吨位 = 其它车辆总吨位 / 其它车辆数 double wholeTonnage = nvl(auth.getOtherTons()) / num(auth.getOtherCount()); // 轨迹测算周转量 = 轨迹里程/轨迹车辆 × (牵引车数×31 + 整车吨位) double avgMileagePerVehicle = nvl(track.getMonthlyMileage()) / trackVehicles; double totalTonnage = num(auth.getTractorCount()) * TRACTOR_STD_TONS + wholeTonnage; double estimatedTurnover = avgMileagePerVehicle * totalTonnage; if (estimatedTurnover == 0.0) return null; double actual = nvl(report.getTurnoverTotal()); double ratio = actual / estimatedTurnover; double threshold = Double.parseDouble(rule.getThreshold()); if (ratio > threshold) { return buildResult(rule, report, "上报周转量=" + fmt(actual) + ", 轨迹测算=" + fmt(estimatedTurnover), "≤轨迹测算×" + fmt(threshold), "=轨迹测算×" + fmt(ratio)); } return null; } /** 货运类型环比:本月新增(上月为0、本月>0)需核实,少的不管 */ private AuditResult checkMom(AuditRule rule, H2032EnterpriseMonthly report, Map lastMonthMap) { H2032EnterpriseMonthly lastMonth = lastMonthMap.get(key(report.getEnterpriseCode())); if (lastMonth == null) return null; List hits = new ArrayList<>(); for (String field : rule.getCheckField().split(",")) { double current = getH2032FieldValue(report, field); double last = getH2032FieldValue(lastMonth, field); if (current > 0 && last == 0) { hits.add(h2032FieldLabel(field) + "=" + fmt(current)); } } if (!hits.isEmpty()) { return buildResult(rule, report, "本月新增: " + String.join("、", hits), "上月该类型为0", "本月新增货运类型(上月无)"); } return null; } /** 有货运量但对应车辆为0 需核实(如 有集装箱货运量无集装箱车辆) */ private AuditResult checkNull(AuditRule rule, H2032EnterpriseMonthly report) { 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) { 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) { 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; } private TransportAuthVehicle findAuth(Map authMap, String name) { return authMap.get(key(name)); } private Map loadAuthMap(String reportPeriod) { Map map = new HashMap<>(); List list = transportAuthMapper.selectList( new LambdaQueryWrapper() .eq(TransportAuthVehicle::getReportPeriod, reportPeriod)); for (TransportAuthVehicle v : list) { map.putIfAbsent(key(v.getEnterpriseName()), v); } return map; } private Map loadTrackMap(String reportPeriod) { Map map = new HashMap<>(); List list = trackMileageMapper.selectList( new LambdaQueryWrapper() .eq(VehicleTrackMileage::getReportPeriod, reportPeriod)); for (VehicleTrackMileage v : list) { map.putIfAbsent(key(v.getEnterpriseName()), v); } return map; } private Map loadLastMonthMap(String period) { Map map = new HashMap<>(); String lastPeriod = getLastPeriod(period); List list = h2032Mapper.selectList( new LambdaQueryWrapper() .eq(H2032EnterpriseMonthly::getReportPeriod, lastPeriod)); for (H2032EnterpriseMonthly v : list) { map.putIfAbsent(key(v.getEnterpriseCode()), v); } return map; } 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 year + "-" + (month < 10 ? "0" + month : String.valueOf(month)); } private double getH2032FieldValue(H2032EnterpriseMonthly report, String fieldName) { try { java.lang.reflect.Field field = H2032EnterpriseMonthly.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 h2032FieldLabel(String field) { String label = FREIGHT_TYPE_NAMES.get(field); if (label != null) return label; java.util.Map 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 authMap, Map 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 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 lastMonthMap) { PassengerEnterpriseMonthly lastMonth = lastMonthMap.get(key(report.getEnterpriseCode())); if (lastMonth == null) return null; List 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 lastMonthMap) { PassengerEnterpriseMonthly lastMonth = lastMonthMap.get(key(report.getEnterpriseCode())); if (lastMonth == null) return null; double threshold = Double.parseDouble(rule.getThreshold()); List 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 lastMonthMap) { PassengerEnterpriseMonthly lastMonth = lastMonthMap.get(key(report.getEnterpriseCode())); if (lastMonth == null) return null; List 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 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 loadPassengerAuthMap(String reportPeriod) { Map map = new HashMap<>(); List list = passengerAuthMapper.selectList( new LambdaQueryWrapper() .eq(PassengerAuthVehicle::getReportPeriod, reportPeriod)); for (PassengerAuthVehicle v : list) { map.putIfAbsent(key(v.getEnterpriseName()), v); } return map; } private Map loadPassengerLastMonthMap(String period) { Map map = new HashMap<>(); String lastPeriod = getLastPeriod(period); List list = passengerMapper.selectList( new LambdaQueryWrapper() .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 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; } }