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<String, String> 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<AuditResult> executeAudit(String reportPeriod) {
|
List<AuditRule> rules = ruleMapper.selectEnabledRules();
|
List<H2032EnterpriseMonthly> reports = h2032Mapper.selectList(
|
new LambdaQueryWrapper<H2032EnterpriseMonthly>()
|
.eq(H2032EnterpriseMonthly::getReportPeriod, reportPeriod));
|
|
// 加载运政/轨迹数据,按企业名索引
|
Map<String, TransportAuthVehicle> authMap = loadAuthMap(reportPeriod);
|
Map<String, VehicleTrackMileage> trackMap = loadTrackMap(reportPeriod);
|
Map<String, H2032EnterpriseMonthly> lastMonthMap = loadLastMonthMap(reportPeriod);
|
|
// 幂等:同报表期先清除旧审核结果
|
resultMapper.delete(new LambdaQueryWrapper<AuditResult>()
|
.eq(AuditResult::getReportPeriod, reportPeriod));
|
|
Map<Long, AuditRule> ruleMap = new HashMap<>();
|
for (AuditRule rule : rules) ruleMap.put(rule.getId(), rule);
|
|
List<AuditResult> 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<AuditResult> queryResults(String reportPeriod) {
|
List<AuditResult> results = resultMapper.selectList(new LambdaQueryWrapper<AuditResult>()
|
.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<AuditResult> results) {
|
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<>();
|
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<String, TransportAuthVehicle> authMap,
|
Map<String, VehicleTrackMileage> trackMap,
|
Map<String, H2032EnterpriseMonthly> 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<String, TransportAuthVehicle> 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<String, TransportAuthVehicle> authMap,
|
Map<String, VehicleTrackMileage> 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<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);
|
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<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), "超出范围");
|
}
|
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<String, TransportAuthVehicle> loadAuthMap(String reportPeriod) {
|
Map<String, TransportAuthVehicle> map = new HashMap<>();
|
List<TransportAuthVehicle> list = transportAuthMapper.selectList(
|
new LambdaQueryWrapper<TransportAuthVehicle>()
|
.eq(TransportAuthVehicle::getReportPeriod, reportPeriod));
|
for (TransportAuthVehicle v : list) {
|
map.putIfAbsent(key(v.getEnterpriseName()), v);
|
}
|
return map;
|
}
|
|
private Map<String, VehicleTrackMileage> loadTrackMap(String period) {
|
Map<String, VehicleTrackMileage> map = new HashMap<>();
|
List<VehicleTrackMileage> list = trackMileageMapper.selectList(
|
new LambdaQueryWrapper<VehicleTrackMileage>()
|
.eq(VehicleTrackMileage::getReportPeriod, period));
|
for (VehicleTrackMileage v : list) {
|
map.putIfAbsent(key(v.getEnterpriseName()), v);
|
}
|
return map;
|
}
|
|
private Map<String, H2032EnterpriseMonthly> loadLastMonthMap(String period) {
|
Map<String, H2032EnterpriseMonthly> map = new HashMap<>();
|
String lastPeriod = getLastPeriod(period);
|
List<H2032EnterpriseMonthly> list = h2032Mapper.selectList(
|
new LambdaQueryWrapper<H2032EnterpriseMonthly>()
|
.eq(H2032EnterpriseMonthly::getReportPeriod, lastPeriod));
|
for (H2032EnterpriseMonthly v : list) {
|
map.putIfAbsent(key(v.getEnterpriseCode()), 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);
|
}
|
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);
|
}
|
}
|