package com.trafficaudit.auditengine.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.trafficaudit.auditengine.entity.AuditResult; import com.trafficaudit.auditengine.mapper.AuditResultMapper; import com.trafficaudit.dataimport.entity.H2032EnterpriseMonthly; import com.trafficaudit.dataimport.entity.TransportAuthVehicle; import com.trafficaudit.dataimport.entity.VehicleTrackMileage; import com.trafficaudit.dataimport.mapper.H2032EnterpriseMonthlyMapper; import com.trafficaudit.dataimport.mapper.TransportAuthVehicleMapper; import com.trafficaudit.dataimport.mapper.VehicleTrackMileageMapper; import com.trafficaudit.rulemanage.entity.AuditRule; import com.trafficaudit.rulemanage.mapper.AuditRuleMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 审核规则引擎(Demo 6条规则) */ @Slf4j @Service public class AuditEngineService { /** 牵引车标准吨位 */ private static final double TRACTOR_STD_TONS = 31.0; /** 货运类型字段 → 中文名称 */ private static final Map FREIGHT_TYPE_NAMES = new HashMap<>(); static { FREIGHT_TYPE_NAMES.put("freightContainer", "集装箱"); FREIGHT_TYPE_NAMES.put("freightCoal", "煤炭及制品"); FREIGHT_TYPE_NAMES.put("freightOilGas", "石油天然气及制品"); FREIGHT_TYPE_NAMES.put("freightCrudeOil", "原油"); FREIGHT_TYPE_NAMES.put("freightMetalOre", "金属矿石"); FREIGHT_TYPE_NAMES.put("freightIronOre", "铁矿石"); FREIGHT_TYPE_NAMES.put("freightBuilding", "矿物性建筑材料"); FREIGHT_TYPE_NAMES.put("freightGrain", "粮食"); } @Resource private AuditRuleMapper ruleMapper; @Resource private AuditResultMapper resultMapper; @Resource private H2032EnterpriseMonthlyMapper h2032Mapper; @Resource private TransportAuthVehicleMapper transportAuthMapper; @Resource private VehicleTrackMileageMapper trackMileageMapper; public List executeAudit(String reportPeriod) { List rules = ruleMapper.selectEnabledRules(); List reports = h2032Mapper.selectList( new LambdaQueryWrapper() .eq(H2032EnterpriseMonthly::getReportPeriod, reportPeriod)); // 加载运政/轨迹数据,按企业名索引 Map authMap = loadAuthMap(reportPeriod); Map trackMap = loadTrackMap(reportPeriod); Map lastMonthMap = loadLastMonthMap(reportPeriod); // 幂等:同报表期先清除旧审核结果 resultMapper.delete(new LambdaQueryWrapper() .eq(AuditResult::getReportPeriod, reportPeriod)); Map ruleMap = new HashMap<>(); for (AuditRule rule : rules) ruleMap.put(rule.getId(), rule); List results = new ArrayList<>(); for (H2032EnterpriseMonthly report : reports) { for (AuditRule rule : rules) { AuditResult result = checkRule(rule, report, authMap, trackMap, lastMonthMap); if (result != null) { result.setRuleName(rule.getRuleName()); result.setRuleCode(rule.getRuleCode()); result.setAlertLevel(rule.getAlertLevel()); result.setEnterpriseName(report.getEnterpriseName()); result.setVerifyExplanation(report.getVerifyExplanation()); resultMapper.insert(result); results.add(result); } } } log.info("Audit complete: {} reports, {} issues for {}", reports.size(), results.size(), reportPeriod); return results; } public List queryResults(String reportPeriod) { List results = resultMapper.selectList(new LambdaQueryWrapper() .eq(AuditResult::getReportPeriod, reportPeriod) .orderByAsc(AuditResult::getId)); attachInfo(results); return results; } public void reviewResult(Long id, String status, String comment) { AuditResult result = resultMapper.selectById(id); if (result == null) return; result.setStatus(status); result.setReviewComment(comment); result.setReviewedAt(LocalDateTime.now()); resultMapper.updateById(result); } private void attachInfo(List results) { if (results.isEmpty()) return; Map ruleMap = new HashMap<>(); for (AuditRule rule : ruleMapper.selectList(null)) ruleMap.put(rule.getId(), rule); Map nameMap = new HashMap<>(); Map explainMap = new HashMap<>(); for (H2032EnterpriseMonthly r : h2032Mapper.selectList(null)) { nameMap.putIfAbsent(r.getId(), r.getEnterpriseName()); explainMap.putIfAbsent(r.getId(), r.getVerifyExplanation()); } for (AuditResult result : results) { AuditRule rule = ruleMap.get(result.getRuleId()); if (rule != null) { result.setRuleName(rule.getRuleName()); result.setRuleCode(rule.getRuleCode()); result.setAlertLevel(rule.getAlertLevel()); } String enterpriseName = nameMap.get(result.getReportId()); if (enterpriseName == null) { // 上报数据被重新导入后旧审核结果已失效,给出提示而非空白 enterpriseName = "\uFF08\u539F\u6570\u636E\u5DF2\u91CD\u65B0\u5BFC\u5165\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C\u5BA1\u6838\uFF09"; } result.setEnterpriseName(enterpriseName); result.setVerifyExplanation(explainMap.get(result.getReportId())); } } private AuditResult checkRule(AuditRule rule, H2032EnterpriseMonthly report, Map authMap, Map trackMap, Map lastMonthMap) { try { switch (rule.getCompareType()) { case "DIFF": return checkDiff(rule, report, authMap); case "RATIO": return checkRatio(rule, report, authMap, trackMap); case "MOM": return checkMom(rule, report, lastMonthMap); case "NULL_CHECK": return checkNull(rule, report); case "OUT_RANGE": return checkOutRange(rule, report, authMap); default: return null; } } catch (Exception e) { log.error("Rule check error: {} - {}", rule.getRuleCode(), e.getMessage()); return null; } } /** 车辆数/吨位 与运政数据对比,差距超阈值核实 */ private AuditResult checkDiff(AuditRule rule, H2032EnterpriseMonthly report, Map authMap) { TransportAuthVehicle auth = findAuth(authMap, report.getEnterpriseName()); if (auth == null) return null; double actual; double reference; if (rule.getCheckField().contains("tons")) { actual = nvl(report.getTonsTotal()); reference = nvl(auth.getTrailerTons()) + nvl(auth.getOtherTons()); } else { actual = num(report.getVehicleTotal()); reference = num(auth.getTractorCount()) + num(auth.getTrailerCount()) + num(auth.getOtherCount()); } double threshold = Double.parseDouble(rule.getThreshold()); double diff = Math.abs(actual - reference); if (diff > threshold) { return buildResult(rule, report, "上报=" + fmt(actual) + ", 运政=" + fmt(reference), "差距≤" + fmt(threshold), "差距=" + fmt(diff)); } return null; } /** 上报周转量与轨迹测算周转量对比,高出30%核实 */ private AuditResult checkRatio(AuditRule rule, H2032EnterpriseMonthly report, Map authMap, Map trackMap) { TransportAuthVehicle auth = findAuth(authMap, report.getEnterpriseName()); VehicleTrackMileage track = trackMap.get(key(report.getEnterpriseName())); if (auth == null || track == null) return null; double trackVehicles = num(track.getTrackedVehicles()); if (trackVehicles == 0.0) return null; if (num(auth.getOtherCount()) == 0) return null; // 整车吨位 = 其它车辆总吨位 / 其它车辆数 double wholeTonnage = nvl(auth.getOtherTons()) / num(auth.getOtherCount()); // 轨迹测算周转量 = 轨迹里程/轨迹车辆 × (牵引车数×31 + 整车吨位) double avgMileagePerVehicle = nvl(track.getMonthlyMileage()) / trackVehicles; double totalTonnage = num(auth.getTractorCount()) * TRACTOR_STD_TONS + wholeTonnage; double estimatedTurnover = avgMileagePerVehicle * totalTonnage; if (estimatedTurnover == 0.0) return null; double actual = nvl(report.getTurnoverTotal()); double ratio = actual / estimatedTurnover; double threshold = Double.parseDouble(rule.getThreshold()); if (ratio > threshold) { return buildResult(rule, report, "上报周转量=" + fmt(actual) + ", 轨迹测算=" + Math.round(estimatedTurnover), "比值≤" + fmt(threshold), "比值=" + fmt(ratio)); } return null; } /** 货运类型环比:本月新增货运类型核实(多的核实,少的不管) */ private AuditResult checkMom(AuditRule rule, H2032EnterpriseMonthly report, Map lastMonthMap) { H2032EnterpriseMonthly lastMonth = lastMonthMap.get(key(report.getEnterpriseCode())); if (lastMonth == null) return null; String[] fields = {"freightContainer", "freightCoal", "freightOilGas", "freightCrudeOil", "freightMetalOre", "freightIronOre", "freightBuilding", "freightGrain"}; List newTypes = new ArrayList<>(); for (String field : fields) { double current = getFieldValue(report, field); double last = getFieldValue(lastMonth, field); if (current > 0 && last == 0) { newTypes.add(FREIGHT_TYPE_NAMES.get(field) + "=" + fmt(current) + "吨"); } } if (!newTypes.isEmpty()) { return buildResult(rule, report, "本月新增货类: " + String.join("、", newTypes), "上月为0", "环比新增"); } return null; } /** 无集装箱车但有集装箱货运量 */ 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), "有集装箱车", "无集装箱车但有集装箱货运量"); } return null; } /** 整车吨位不在(4,40]范围内核实 */ private AuditResult checkOutRange(AuditRule rule, H2032EnterpriseMonthly report, Map 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), "超出范围"); } return null; } // ========== 辅助方法 ========== private AuditResult buildResult(AuditRule rule, H2032EnterpriseMonthly report, String actual, String threshold, String deviation) { AuditResult result = new AuditResult(); result.setRuleId(rule.getId()); result.setReportId(report.getId()); result.setEnterpriseCode(report.getEnterpriseCode()); result.setReportPeriod(report.getReportPeriod()); result.setActualValue(actual); result.setThresholdValue(threshold); result.setDeviation(deviation); result.setStatus("PENDING"); return result; } private Map loadAuthMap(String reportPeriod) { Map map = new HashMap<>(); List list = transportAuthMapper.selectList( new LambdaQueryWrapper() .eq(TransportAuthVehicle::getReportPeriod, reportPeriod)); for (TransportAuthVehicle v : list) { map.putIfAbsent(key(v.getEnterpriseName()), v); } return map; } private Map loadTrackMap(String period) { Map map = new HashMap<>(); List list = trackMileageMapper.selectList( new LambdaQueryWrapper() .eq(VehicleTrackMileage::getReportPeriod, period)); for (VehicleTrackMileage v : list) { map.putIfAbsent(key(v.getEnterpriseName()), v); } return map; } private Map loadLastMonthMap(String period) { Map map = new HashMap<>(); String lastPeriod = getLastPeriod(period); List list = h2032Mapper.selectList( new LambdaQueryWrapper() .eq(H2032EnterpriseMonthly::getReportPeriod, lastPeriod)); for (H2032EnterpriseMonthly v : list) { map.putIfAbsent(key(v.getEnterpriseCode()), v); } return map; } private TransportAuthVehicle findAuth(Map 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); } String s = String.format("%.6f", v); if (s.indexOf('.') >= 0) { s = s.replaceAll("0+$", "").replaceAll("\\.$", ""); } return s; } 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) { try { java.lang.reflect.Field field = H2032EnterpriseMonthly.class.getDeclaredField(fieldName); field.setAccessible(true); Object value = field.get(report); if (value == null) return 0.0; if (value instanceof Double) return (Double) value; if (value instanceof Integer) return ((Integer) value).doubleValue(); return Double.parseDouble(value.toString()); } catch (Exception e) { return 0.0; } } private String 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); } }