From 91f658319a265c27440b3289e0a4b8bcfd19c993 Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期一, 12 十月 2026 09:54:21 +0800
Subject: [PATCH] feat(汇总大表): 货运基数体检 B4 六条规则(倒减为负/缺报/重复导入/同比异常)待验收

---
 HANDOFF.md                                                                                              |   10 ++
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java       |    4 
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java |    9 +
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/FreightReadinessCheck.java        |  243 ++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 266 insertions(+), 0 deletions(-)

diff --git a/HANDOFF.md b/HANDOFF.md
index 8cdec2f..9936317 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -4,6 +4,16 @@
 > **姣忓ぉ缁撴潫鏃�**锛氭妸褰撳ぉ杩涘睍銆佽俯杩囩殑鍧戙�佹柊闇�姹傘�佹湭瀹屾垚浜嬮」鏇存柊鍒版湰鏂囦欢锛岀劧鍚庡彲浠ユ斁蹇冨紑鏂板璇濄��
 > **鍗忎綔绾﹀畾锛�2026-08-31锛�**锛氳銆屾彁浜ゃ��= 鏈湴 commit + 鎺ㄩ�佸埌杩滅 git锛坥rigin main锛孏itblit锛夛紝涓嶅啀鍙仛鏈湴 commit銆�
 
+### 10-12锛�2锛夛細璐ц繍鍙e緞銆屽鍑哄墠鍩烘暟浣撴銆嶏紙B4锛�6 鏉¤鍒欒惤鍦般�愬緟楠屾敹銆�
+
+- **鑳屾櫙**锛氳涓� = 鈶°�婅揣杩愰噺鍛ㄨ浆閲忚〃銆嬪叏鍙e緞 鈭� 鈶燞203-2 瑙勪笂梅10000锛屾槸**鍊掑噺**鈥斺�斺憼婕忔姤浼氳瑙勪笅铏氶珮銆佹瀬绔繕浼氱畻鍑鸿礋鏁般�傜敤鎴� 2026-10-12 鍚屾剰鎶婅繖缁勮鍒欏姞杩� B4銆�
+- **瀹炵幇**锛氭柊澧� `reportexport/calc/FreightReadinessCheck.java`锛堝彧璇伙級锛涙帴鍏� `exportSummaryWorkbook` 鐨勭己鏁版敹闆� `dbProblems`锛堥殢瀵煎嚭鏃ュ織杈撳嚭锛夛紱鏂板鍙鎺ュ彛 `GET /api/report/summaryWorkbook/freightCheck?period=YYYY-MM`銆�
+- **瑙勫垯**锛氣憼 鏌愬競宸炶涓嬩负璐燂紱鈶� 鈶℃湁鍏ㄥ彛寰勩�佲憼鏃犺涓婅褰曪紙鐤戞紡鎶モ啋瑙勪笅=鍏ㄥ彛寰勶級锛涒憿 鈶″叏鍙e緞 < 鈶犺涓婏紱鈶� 危17 甯傚窞 鈮� 鈶�"婀栧寳鐪�"琛岋紙瀹瑰樊 0.01锛夛紱鈶� **鏈湀涓庝笂鏈堝畬鍏ㄧ浉鍚�**锛堢枒閲嶅瀵煎叆锛屼袱渚� 鈮�50 涓囧惃/涓囧惃鍏噷锛夛紱鈶� **鍚屾瘮寮傚父**锛坾鍘诲勾鍚屾湀鈫掓湰鏈坾 > 50%锛屼袱渚� 鈮�50锛夛紱鈶�' 绱鍕剧ń锛堚憼鍛ㄨ浆閲� 1..M 姹傚拰 梅10000 vs 鈶㈡媶鍒嗚〃 CUMULATIVE锛夈��
+- **闃堝�间慨姝o紙瀹炴祴椹卞姩锛岄噸瑕侊級**锛氶鐗堢敤"瑙勪笅鐜瘮 > 30%" 鈫� 2026-02 鍛戒腑 22 鏉°��2026-03 鍛戒腑 32 鏉★紝澶嶆牳纭**鍏ㄦ槸鏄ヨ妭瀛h妭鎬�**锛堝悇甯傚窞 2 鏈� 鈭�30~鈭�40%銆�3 鏈� +30~+60%锛夛紝闈炴暟鎹敊璇� 鈫� 鏀逛负"閲嶅瀵煎叆鐗瑰緛 + 鍚屾瘮>50%"锛涘苟缁欏垽鎹姞**缁濆闂ㄦ 50 涓囧惃/涓囧惃鍏噷**锛岄伩鍏嶇鍐滄灦鏋楀尯锛�20~46 涓囧惃锛夎繖绫诲皬鍩烘暟鍣0闀挎湡鎶ヨ銆�
+- **瀹炴祴**锛�2026-01~08 **鍏ㄦ湡 0 鍛戒腑**锛堟棤璇姤锛夛紱鈶¤〃 `last_freight_m08`/`last_turnover_m08` **18/18 琛屾湁鍊�** 鈫� 鍚屾瘮瑙勫垯纭疄鍙備笌璁$畻锛堥潪绌鸿窇锛夈��
+- **鎶ュ憡锛堟湰鍦般�佷笉鍏ュ簱锛�**锛歚docs/鍔熻兘娴嬭瘯鎶ュ憡/鍔熻兘娴嬭瘯鎶ュ憡_璐ц繍鍩烘暟浣撴B4鍏潯瑙勫垯_2026-10-12.md`锛涙棩缁撳悓鏃ヤ袱鏉°��
+- **寰呯敤鎴烽獙鏀�/纭**锛氣憼 闃堝�硷紙缁濆闂ㄦ 50銆佸悓姣� 50%锛夋槸鍚﹀悎閫傦紱鈶� 鏄惁鐜板湪鍋� B4 鍓╀綑閮ㄥ垎鈥斺�斿鍑哄墠寮圭獥 + 鐢熸垚浠躲�婃暟鎹己澶辨竻鍗曘�嬮〉锛圥0-3 鍓╀綑锛夈��
+- **鐘舵��**锛�**銆愬緟楠屾敹銆�**銆傚彧璇诲姛鑳斤紝鏈敼鍔ㄤ笟鍔℃暟鎹��
 ## 鏈�鏂帮紙2026-10-12锛寊yj 绾匡級
 
 ### 10-12锛氭墿鍒楀櫒琛ャ�屾爣棰樻寜姣嶇増鏈熸閲嶅啓銆�+ 淇 8 鏈堟瘝鐗堟爣棰樸�愬緟楠屾敹銆�
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..d47dd9a
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/FreightReadinessCheck.java
@@ -0,0 +1,243 @@
+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緞鍚屾瘮寮傚父锛坾鍘诲勾鍚屾湀鈫掓湰鏈坾 &gt; 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;      // 涓や晶閮芥病鏁� 鈫� 浜ょ粰缂烘暟娓呭崟锛堝彟绠楋級
+            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 鏈夊叏鍙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);
+    }
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java
index 776db52..292beb0 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java
@@ -1,6 +1,7 @@
 package com.trafficaudit.reportexport.controller;
 
 import com.trafficaudit.common.Result;
+import com.trafficaudit.reportexport.calc.FreightReadinessCheck;
 import com.trafficaudit.reportexport.service.ReportExportService;
 import com.trafficaudit.reportexport.service.ReportExportTaskService;
 import com.trafficaudit.system.service.OperationLogService;
@@ -26,6 +27,8 @@
 
     @Resource
     private ReportExportService reportService;
+    @Resource
+    private FreightReadinessCheck freightReadinessCheck;
     @Resource
     private ReportExportTaskService exportTaskService;
     @Resource
@@ -89,6 +92,12 @@
     }
 
     /** 鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx锛堟眹鎬诲伐浣滅翱 P0-P1 楠ㄦ灦锛氭暣鏈鍒舵瘝鐗堬紝椤电/鍏紡/2025 缂撳瓨淇濈暀锛� */
+    /** 璐ц繍鍙e緞瀵煎嚭鍓嶄綋妫�锛圔4锛夛細鍙杩斿洖鍛戒腑鐨勯棶棰樻竻鍗曘�� */
+    @GetMapping("/summaryWorkbook/freightCheck")
+    public Result<java.util.List<String>> freightCheck(@RequestParam("period") String period) {
+        return Result.ok(freightReadinessCheck.check(period));
+    }
+
     /** 姹囨�诲ぇ琛ㄦ寜鏈堟墿鍒楋紙Step 1锛屽彧鍋氱増寮忋�佹暟鎹暀绌猴級锛氳繑鍥炰笅涓�鏈堢増寮� xlsx銆� */
     @GetMapping("/export/summaryWorkbook/expand")
     public ResponseEntity<byte[]> expandSummaryWorkbook(@RequestParam("period") String period,
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
index 3465473..a93160b 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -34,6 +34,7 @@
 import com.trafficaudit.reportexport.calc.MidCalc;
 import com.trafficaudit.reportexport.calc.PaxCalc;
 import com.trafficaudit.reportexport.calc.SummaryWorkbookFiller;
+import com.trafficaudit.reportexport.calc.FreightReadinessCheck;
 import com.trafficaudit.reportexport.layout.SummaryLayoutExpander;
 import com.trafficaudit.reportexport.calc.WycSplitCalc;
 import lombok.extern.slf4j.Slf4j;
@@ -104,6 +105,8 @@
     private SummaryWorkbookFiller summaryWorkbookFiller;
     @Resource
     private SummaryLayoutExpander summaryLayoutExpander;
+    @Resource
+    private FreightReadinessCheck freightReadinessCheck;
     @Resource
     private WycSplitCalc wycSplitCalc;
     @Resource
@@ -3707,6 +3710,7 @@
             int effMonth = (toMonth != null && toMonth > 0 && toMonth < month) ? toMonth : month;
             List<String> dbProblems = new ArrayList<>();
             String effPeriod = String.format("%04d-%02d", year, effMonth);
+            dbProblems.addAll(freightReadinessCheck.check(effPeriod));   // B4锛氳揣杩愬彛寰勫鍑哄墠浣撴锛�6 鏉¤鍒欙級
             reportProgress(progress, 22, "鍥炲~褰撴湀鏁版嵁");
             int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, effPeriod, year, effMonth, dbProblems);
             int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth);

--
Gitblit v1.9.1