| | |
| | | |
| | | 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 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; |
| | |
| | | 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)); |
| | |
| | | } |
| | | } |
| | | 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) { |
| | |
| | | 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()); |
| | |
| | | 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) { |
| | | // 上报数据被重新导入后旧审核结果已失效,给出提示而非空白 |
| | |
| | | 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, |
| | |
| | | case "NULL_CHECK": |
| | | return checkNull(rule, report); |
| | | case "OUT_RANGE": |
| | | return checkOutRange(rule, report, authMap); |
| | | return checkOutRange(rule, report); |
| | | default: |
| | | return null; |
| | | } |
| | |
| | | return null; |
| | | } |
| | | |
| | | /** 上报周转量与轨迹测算周转量对比,高出30%核实 */ |
| | | /** 上报周转量与轨迹测算周转量对比,高出阈值比例核实 */ |
| | | private AuditResult checkRatio(AuditRule rule, H2032EnterpriseMonthly report, |
| | | Map<String, TransportAuthVehicle> authMap, |
| | | Map<String, VehicleTrackMileage> trackMap) { |
| | |
| | | |
| | | 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) { |
| | |
| | | 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( |
| | |
| | | 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); |
| | | } |
| | |
| | | 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); |
| | |
| | | } |
| | | } |
| | | |
| | | 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; |
| | | } |
| | | } |