package com.trafficaudit.reportexport.calc; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.trafficaudit.common.util.RegionUtil; import com.trafficaudit.dataimport.entity.FreightTurnoverImport; import com.trafficaudit.dataimport.entity.H2032EnterpriseMonthly; import com.trafficaudit.dataimport.entity.ScaleSplitTransport; import com.trafficaudit.dataimport.mapper.FreightTurnoverImportMapper; import com.trafficaudit.dataimport.mapper.H2032EnterpriseMonthlyMapper; import com.trafficaudit.dataimport.mapper.ScaleSplitTransportMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * 货运口径「导出前基数体检」(B4,2026-10-12 新增 6 条规则)。 * *

背景:规下 = 全口径(②《货运量周转量表》)− 规上(①H203-2 企业月报÷10000)——是**倒减**, * 因此①漏报/缺报会让规下虚高,极端情况还会算出负数。本类把这些异常在导出前挑出来, * 供日志/弹窗/《数据缺失清单》页使用(同一套结果也供生成主链的 dbProblems 复用)。 * *

规则: *

    *
  1. 某市州规下为负(货量或周转量);
  2. *
  3. ②该市州有全口径、①无规上记录 → 规下等于全口径,明显虚高;
  4. *
  5. ②全口径小于①规上(逻辑不可能);
  6. *
  7. Σ17 市州 ≠ ②的"湖北省"行(超容差 0.01);
  8. *
  9. 某市州全口径「本月与上月完全相同」(疑重复导入,两侧 ≥ 50 万吨/万吨公里);
  10. *
  11. 某市州全口径同比异常(|去年同月→本月| > 50% 且两侧 ≥ 50 万吨/万吨公里);
  12. *
  13. 累计勾稽:①周转量 1..M 月求和 ÷10000 与③《拆分表》CUMULATIVE.above_scale_turnover 不一致(超容差 0.01)。
  14. *
*/ @Slf4j @Service public class FreightReadinessCheck { private static final double TOL = 0.01; private static final double YOY_RATIO = 0.50; /** 环比检查的绝对门槛:两侧都要 ≥ 该值才判异常(避免神农架林区这类小基数噪声) */ private static final double CHAIN_FLOOR = 50.0; @Resource private H2032EnterpriseMonthlyMapper h2032Mapper; @Resource private FreightTurnoverImportMapper freightTurnoverMapper; @Resource private ScaleSplitTransportMapper scaleSplitMapper; public List check(String period) { List issues = new ArrayList<>(); if (period == null || period.length() < 7) return issues; int year = Integer.parseInt(period.substring(0, 4)); int month = Integer.parseInt(period.substring(5, 7)); // ① 规上(吨)按市州+月份 Map> aboveFreight = new HashMap<>(); Map> aboveTurnover = new HashMap<>(); for (H2032EnterpriseMonthly rec : h2032Mapper.selectList( new LambdaQueryWrapper() .likeRight(H2032EnterpriseMonthly::getReportPeriod, year))) { String rp = rec.getReportPeriod(); if (rp == null || rp.compareTo(period) > 0) continue; String city = RegionUtil.cityByCode(rec.getRegionCode()); if (city == null) continue; int m = Integer.parseInt(rp.substring(5, 7)); aboveFreight.computeIfAbsent(city, k -> new HashMap<>()) .merge(m, rec.getFreightTotal() == null ? 0.0 : rec.getFreightTotal(), Double::sum); aboveTurnover.computeIfAbsent(city, k -> new HashMap<>()) .merge(m, rec.getTurnoverTotal() == null ? 0.0 : rec.getTurnoverTotal(), Double::sum); } // ② 全口径:市州行 + 湖北省行(当期) Map ft = new HashMap<>(); FreightTurnoverImport province = null; for (FreightTurnoverImport e : freightTurnoverMapper.selectList( new LambdaQueryWrapper() .eq(FreightTurnoverImport::getReportPeriod, period))) { String city = RegionUtil.normalizeCityName(e.getRegionName()); if (city == null) continue; if (FreightCalc.PROVINCE.equals(city)) province = e; else ft.put(city, e); } // 上月全口径(用于环比) String prevPeriod = prevPeriod(period); Map ftPrev = new HashMap<>(); for (FreightTurnoverImport e : freightTurnoverMapper.selectList( new LambdaQueryWrapper() .eq(FreightTurnoverImport::getReportPeriod, prevPeriod))) { String city = RegionUtil.normalizeCityName(e.getRegionName()); if (city != null && !FreightCalc.PROVINCE.equals(city)) ftPrev.put(city, e); } // ③ 拆分表:当月/累计 规上与合计周转量 Map splitCum = new HashMap<>(); for (ScaleSplitTransport rec : scaleSplitMapper.selectList( new LambdaQueryWrapper() .eq(ScaleSplitTransport::getReportPeriod, period))) { String city = RegionUtil.normalizeCityName(rec.getRegionName()); if (city == null || FreightCalc.PROVINCE.equals(city)) continue; if ("CUMULATIVE".equals(rec.getPeriodType())) splitCum.put(city, rec); } double sumFreight = 0.0, sumTurnover = 0.0; for (String city : RegionUtil.cityList()) { FreightTurnoverImport row = ft.get(city); Double totalF = row == null ? null : monthField(row, month, true); Double totalT = row == null ? null : monthField(row, month, false); Double abF = aboveFreight.containsKey(city) ? aboveFreight.get(city).get(month) : null; abF = abF == null ? null : abF / 10000.0; Double abT = aboveTurnover.containsKey(city) && aboveTurnover.get(city).get(month) != null ? aboveTurnover.get(city).get(month) / 10000.0 : null; if (totalF != null) sumFreight += totalF; if (totalT != null) sumTurnover += totalT; if (totalF == null && abF == null) continue; // 两侧都没数 → 交给缺数清单(另算) double belowF = (totalF == null ? 0 : totalF) - (abF == null ? 0 : abF); double belowT = (totalT == null ? 0 : totalT) - (abT == null ? 0 : abT); if (belowF < -TOL) issues.add(String.format("货运体检:%s %s 规下货运量为负(全口径 %.4f − 规上 %.4f = %.4f)", period, city, totalF, abF, belowF)); if (belowT < -TOL) issues.add(String.format("货运体检:%s %s 规下周转量为负(全口径 %.4f − 规上 %.4f = %.4f)", period, city, totalT, abT, belowT)); if (totalF != null && abF == null) issues.add(String.format("货运体检:%s %s 有全口径但无规上记录 → 规下等于全口径(%.4f),疑①漏报", period, city, totalF)); if (totalF != null && abF != null && totalF < abF) issues.add(String.format("货运体检:%s %s 全口径 %.4f 小于规上 %.4f", period, city, totalF, abF)); FreightTurnoverImport prev = ftPrev.get(city); if (prev != null) { int prevMonth = month == 1 ? 12 : month - 1; Double prevF = monthField(prev, prevMonth, true); Double prevT = monthField(prev, prevMonth, false); if (totalF != null && sameValue(prevF, totalF) && Math.abs(totalF) >= CHAIN_FLOOR) { issues.add(String.format("货运体检:%s %s 全口径货运量本月与上月完全相同(%.4f),疑重复导入", period, city, totalF)); } if (totalT != null && sameValue(prevT, totalT) && Math.abs(totalT) >= CHAIN_FLOOR) { issues.add(String.format("货运体检:%s %s 全口径周转量本月与上月完全相同(%.4f),疑重复导入", period, city, totalT)); } } if (row != null) { Double lastF = lastMonthField(row, month, true); Double lastT = lastMonthField(row, month, false); if (lastF != null && totalF != null && Math.abs(lastF) >= CHAIN_FLOOR && Math.abs(totalF) >= CHAIN_FLOOR) { double ratio = totalF / lastF - 1.0; if (Math.abs(ratio) > YOY_RATIO) { issues.add(String.format("货运体检:%s %s 全口径货运量同比异常(去年同月 %.4f -> 本月 %.4f,%.1f%%)", period, city, lastF, totalF, ratio * 100)); } } if (lastT != null && totalT != null && Math.abs(lastT) >= CHAIN_FLOOR && Math.abs(totalT) >= CHAIN_FLOOR) { double ratio = totalT / lastT - 1.0; if (Math.abs(ratio) > YOY_RATIO) { issues.add(String.format("货运体检:%s %s 全口径周转量同比异常(去年同月 %.4f -> 本月 %.4f,%.1f%%)", period, city, lastT, totalT, ratio * 100)); } } } ScaleSplitTransport cum = splitCum.get(city); if (cum != null && cum.getAboveScaleTurnover() != null) { Map buckets = aboveTurnover.get(city); if (buckets != null) { double sum = 0.0; for (int m = 1; m <= month; m++) sum += buckets.getOrDefault(m, 0.0); double fromH2032 = sum / 10000.0; double fromSplit = cum.getAboveScaleTurnover(); if (Math.abs(fromH2032 - fromSplit) > TOL) { issues.add(String.format("货运体检:%s %s 规上周转量累计勾稽不一致(①逐月求和 %.4f vs ③累计 %.4f)", period, city, fromH2032, fromSplit)); } } } } if (province != null) { Double pf = monthField(province, month, true); Double pt = monthField(province, month, false); if (pf != null && Math.abs(pf - sumFreight) > TOL) { issues.add(String.format("货运体检:%s Σ17市州货运量 %.4f ≠ ②表湖北省行 %.4f(差 %.4f)", period, sumFreight, pf, sumFreight - pf)); } if (pt != null && Math.abs(pt - sumTurnover) > TOL) { issues.add(String.format("货运体检:%s Σ17市州周转量 %.4f ≠ ②表湖北省行 %.4f(差 %.4f)", period, sumTurnover, pt, sumTurnover - pt)); } } if (!issues.isEmpty()) { log.warn("货运体检(B4):{} 命中 {} 条问题", period, issues.size()); } return issues; } /** ②表按 月号 取当月货运量/周转量(与 FreightCalc 同一取法) */ private Double monthField(FreightTurnoverImport e, int m, boolean freight) { switch (m) { case 1: return freight ? e.getFreightM01() : e.getTurnoverM01(); case 2: return freight ? e.getFreightM02() : e.getTurnoverM02(); case 3: return freight ? e.getFreightM03() : e.getTurnoverM03(); case 4: return freight ? e.getFreightM04() : e.getTurnoverM04(); case 5: return freight ? e.getFreightM05() : e.getTurnoverM05(); case 6: return freight ? e.getFreightM06() : e.getTurnoverM06(); case 7: return freight ? e.getFreightM07() : e.getTurnoverM07(); case 8: return freight ? e.getFreightM08() : e.getTurnoverM08(); case 9: return freight ? e.getFreightM09() : e.getTurnoverM09(); case 10: return freight ? e.getFreightM10() : e.getTurnoverM10(); case 11: return freight ? e.getFreightM11() : e.getTurnoverM11(); case 12: return freight ? e.getFreightM12() : e.getTurnoverM12(); default: return null; } } /** ②表去年同月值 */ private Double lastMonthField(FreightTurnoverImport e, int m, boolean freight) { switch (m) { case 1: return freight ? e.getLastFreightM01() : e.getLastTurnoverM01(); case 2: return freight ? e.getLastFreightM02() : e.getLastTurnoverM02(); case 3: return freight ? e.getLastFreightM03() : e.getLastTurnoverM03(); case 4: return freight ? e.getLastFreightM04() : e.getLastTurnoverM04(); case 5: return freight ? e.getLastFreightM05() : e.getLastTurnoverM05(); case 6: return freight ? e.getLastFreightM06() : e.getLastTurnoverM06(); case 7: return freight ? e.getLastFreightM07() : e.getLastTurnoverM07(); case 8: return freight ? e.getLastFreightM08() : e.getLastTurnoverM08(); case 9: return freight ? e.getLastFreightM09() : e.getLastTurnoverM09(); case 10: return freight ? e.getLastFreightM10() : e.getLastTurnoverM10(); case 11: return freight ? e.getLastFreightM11() : e.getLastTurnoverM11(); case 12: return freight ? e.getLastFreightM12() : e.getLastTurnoverM12(); default: return null; } } private boolean sameValue(Double a, Double b) { if (a == null || b == null) return false; return Math.abs(a - b) < 1e-9; } private String prevPeriod(String period) { int y = Integer.parseInt(period.substring(0, 4)); int m = Integer.parseInt(period.substring(5, 7)); if (m == 1) return (y - 1) + "-12"; return String.format("%04d-%02d", y, m - 1); } public String describe(String period) { List issues = check(period); return issues.isEmpty() ? "货运体检:未发现问题" : String.join("\n", issues); } }