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