From 259db33b22095891f436b9ed5a81a21496b11c4c Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期一, 12 十月 2026 23:41:55 +0800
Subject: [PATCH] feat(汇总大表): 缺数清单加导出前弹窗并在生成件附《数据缺失清单》页(待验收)
---
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/FreightReadinessCheck.java | 257 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 257 insertions(+), 0 deletions(-)
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/FreightReadinessCheck.java b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/FreightReadinessCheck.java
new file mode 100644
index 0000000..11d4e2c
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/FreightReadinessCheck.java
@@ -0,0 +1,257 @@
+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;
+
+/**
+ * 璐ц繍鍙e緞銆屽鍑哄墠鍩烘暟浣撴銆嶏紙B4锛�2026-10-12 鏂板 6 鏉¤鍒欙級銆�
+ *
+ * <p>鑳屾櫙锛氳涓� = 鍏ㄥ彛寰勶紙鈶°�婅揣杩愰噺鍛ㄨ浆閲忚〃銆嬶級鈭� 瑙勪笂锛堚憼H203-2 浼佷笟鏈堟姤梅10000锛夆�斺�旀槸**鍊掑噺**锛�
+ * 鍥犳鈶犳紡鎶�/缂烘姤浼氳瑙勪笅铏氶珮锛屾瀬绔儏鍐佃繕浼氱畻鍑鸿礋鏁般�傛湰绫绘妸杩欎簺寮傚父鍦ㄥ鍑哄墠鎸戝嚭鏉ワ紝
+ * 渚涙棩蹇�/寮圭獥/銆婃暟鎹己澶辨竻鍗曘�嬮〉浣跨敤锛堝悓涓�濂楃粨鏋滀篃渚涚敓鎴愪富閾剧殑 dbProblems 澶嶇敤锛夈��
+ *
+ * <p>瑙勫垯锛�
+ * <ol>
+ * <li>鏌愬競宸炶涓嬩负璐燂紙璐ч噺鎴栧懆杞噺锛夛紱</li>
+ * <li>鈶¤甯傚窞鏈夊叏鍙e緞銆佲憼鏃犺涓婅褰� 鈫� 瑙勪笅绛変簬鍏ㄥ彛寰勶紝鏄庢樉铏氶珮锛�</li>
+ * <li>鈶″叏鍙e緞灏忎簬鈶犺涓婏紙閫昏緫涓嶅彲鑳斤級锛�</li>
+ * <li>危17 甯傚窞 鈮� 鈶$殑"婀栧寳鐪�"琛岋紙瓒呭宸� 0.01锛夛紱</li>
+ * <li>鏌愬競宸炲叏鍙e緞銆屾湰鏈堜笌涓婃湀瀹屽叏鐩稿悓銆嶏紙鐤戦噸澶嶅鍏ワ紝涓や晶 鈮� 50 涓囧惃/涓囧惃鍏噷锛夛紱</li>
+ * <li>鏌愬競宸炲叏鍙e緞鍚屾瘮寮傚父锛坾鍘诲勾鍚屾湀鈫掓湰鏈坾 > 50% 涓斾袱渚� 鈮� 50 涓囧惃/涓囧惃鍏噷锛夛紱</li>
+ * <li>绱鍕剧ń锛氣憼鍛ㄨ浆閲� 1..M 鏈堟眰鍜� 梅10000 涓庘憿銆婃媶鍒嗚〃銆婥UMULATIVE.above_scale_turnover 涓嶄竴鑷达紙瓒呭宸� 0.01锛夈��</li>
+ * </ol>
+ */
+@Slf4j
+@Service
+public class FreightReadinessCheck {
+
+ private static final double TOL = 0.01;
+ private static final double YOY_RATIO = 0.50;
+ /** 鐜瘮妫�鏌ョ殑缁濆闂ㄦ锛氫袱渚ч兘瑕� 鈮� 璇ュ�兼墠鍒ゅ紓甯革紙閬垮厤绁炲啘鏋舵灄鍖鸿繖绫诲皬鍩烘暟鍣0锛� */
+ private static final double CHAIN_FLOOR = 50.0;
+
+ @Resource
+ private H2032EnterpriseMonthlyMapper h2032Mapper;
+ @Resource
+ private FreightTurnoverImportMapper freightTurnoverMapper;
+ @Resource
+ private ScaleSplitTransportMapper scaleSplitMapper;
+
+ public List<String> check(String period) {
+ List<String> 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<String, Map<Integer, Double>> aboveFreight = new HashMap<>();
+ Map<String, Map<Integer, Double>> aboveTurnover = new HashMap<>();
+ for (H2032EnterpriseMonthly rec : h2032Mapper.selectList(
+ new LambdaQueryWrapper<H2032EnterpriseMonthly>()
+ .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<String, FreightTurnoverImport> ft = new HashMap<>();
+ FreightTurnoverImport province = null;
+ for (FreightTurnoverImport e : freightTurnoverMapper.selectList(
+ new LambdaQueryWrapper<FreightTurnoverImport>()
+ .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<String, FreightTurnoverImport> ftPrev = new HashMap<>();
+ for (FreightTurnoverImport e : freightTurnoverMapper.selectList(
+ new LambdaQueryWrapper<FreightTurnoverImport>()
+ .eq(FreightTurnoverImport::getReportPeriod, prevPeriod))) {
+ String city = RegionUtil.normalizeCityName(e.getRegionName());
+ if (city != null && !FreightCalc.PROVINCE.equals(city)) ftPrev.put(city, e);
+ }
+
+ // 鈶� 鎷嗗垎琛細褰撴湀/绱 瑙勪笂涓庡悎璁″懆杞噺
+ Map<String, ScaleSplitTransport> splitCum = new HashMap<>();
+ for (ScaleSplitTransport rec : scaleSplitMapper.selectList(
+ new LambdaQueryWrapper<ScaleSplitTransport>()
+ .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; // 涓や晶閮芥病鏁� 鈫� 浜ょ粰缂烘暟娓呭崟锛堝彟绠楋級
+ if (totalF == null && abF != null) {
+ issues.add(String.format("璐ц繍浣撴锛�%s %s 缂衡憽鍏ㄥ彛寰勶紙瑙勪笂 %.4f 鍦ㄣ�佸叏鍙e緞缂猴級鈫� 瑙勪笅鏃犳硶璁$畻"
+ + "锛堥渶鍏堝銆婅揣杩愰噺鍛ㄨ浆閲忚〃銆嬪綋鏈堟。锛�", period, city, abF));
+ }
+ if (totalT == null && abT != null) {
+ issues.add(String.format("璐ц繍浣撴锛�%s %s 缂衡憽鍏ㄥ彛寰勫懆杞噺锛堣涓� %.4f 鍦ㄣ�佸叏鍙e緞缂猴級鈫� 瑙勪笅鏃犳硶璁$畻",
+ period, city, abT));
+ }
+ // 2026-10-12锛氣憽鍏ㄥ彛寰勭己锛堟湭瀵硷級鏃朵笉鍐嶆姤銆岃涓嬩负璐熴�嶁�斺�斾笂闈㈠凡鍗曠嫭鎶ャ�岀己鈶″叏鍙e緞銆嶏紝
+ // 鑻ヨ繖閲屼粛鎸� 0 鈭� 瑙勪笂 绠楋紝浼氬姣忎釜甯傚窞澶氬埛涓�鏉″亣闃虫�э紙鍊间笉鏄礋銆佹槸缂猴級锛屾妸鐪熼棶棰樻饭娌°��
+ if (totalF != null) {
+ double belowF = totalF - (abF == null ? 0 : abF);
+ if (belowF < -TOL) issues.add(String.format("璐ц繍浣撴锛�%s %s 瑙勪笅璐ц繍閲忎负璐燂紙鍏ㄥ彛寰� %.4f 鈭� 瑙勪笂 %.4f = %.4f锛�", period, city, totalF, abF, belowF));
+ }
+ if (totalT != null) {
+ double belowT = totalT - (abT == null ? 0 : abT);
+ 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 鏈夊叏鍙e緞浣嗘棤瑙勪笂璁板綍 鈫� 瑙勪笅绛変簬鍏ㄥ彛寰勶紙%.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<Integer, Double> 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("璐ц繍浣撴锛圔4锛夛細{} 鍛戒腑 {} 鏉¢棶棰�", 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<String> issues = check(period);
+ return issues.isEmpty() ? "璐ц繍浣撴锛氭湭鍙戠幇闂" : String.join("\n", issues);
+ }
+}
--
Gitblit v1.9.1