From ae47753f77690615762a77bde38a34e9ff0fa4ae Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期一, 12 十月 2026 17:12:24 +0800
Subject: [PATCH] feat(汇总大表): 缺数清单可见且缺数期不沿用上一期旧值(排名页按源侧留空)(待验收)
---
HANDOFF.md | 14 +
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java | 72 ++++++
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryMissingDataCheck.java | 453 +++++++++++++++++++++++++++++++++++++++++++++
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java | 9
4 files changed, 541 insertions(+), 7 deletions(-)
diff --git a/HANDOFF.md b/HANDOFF.md
index 4149825..5b161de 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -4,6 +4,20 @@
> **姣忓ぉ缁撴潫鏃�**锛氭妸褰撳ぉ杩涘睍銆佽俯杩囩殑鍧戙�佹柊闇�姹傘�佹湭瀹屾垚浜嬮」鏇存柊鍒版湰鏂囦欢锛岀劧鍚庡彲浠ユ斁蹇冨紑鏂板璇濄��
> **鍗忎綔绾﹀畾锛�2026-08-31锛�**锛氳銆屾彁浜ゃ��= 鏈湴 commit + 鎺ㄩ�佸埌杩滅 git锛坥rigin main锛孏itblit锛夛紝涓嶅啀鍙仛鏈湴 commit銆�
+### 10-12锛�12锛夛細姹囨�诲ぇ琛ㄣ�岀己鏁颁笉鐣欎笂涓�鏈熸棫鍊笺�嶏紙鎺掑悕椤垫寜婧愪晶鐣欑┖锛�+ 缂烘暟娓呭崟浠g爜鍏ュ簱銆愬緟楠屾敹銆�
+
+- **鑳屾櫙**锛氱敤鎴烽棶"鐢熸垚琛ㄩ噷寰堝鏁版嵁閮芥槸 8 鏈堢殑鍚�"锛涘悓鏃跺簱閲岀殑缂烘暟娓呭崟浠g爜涓�鐩存湭鎻愪氦锛堟湰鏈烘矙绠辨晠闅滄湡闂村啓鐨勶紝娌欑鎭㈠鍚庤ˉ鎻愪氦锛夈��
+- **鍙e緞鏀瑰姩锛�3 澶勶紝鍚屼竴浠朵簨锛氱己鏁颁笉鐣欎笂涓�鏈熸棫鍊硷級**锛�
+ 1. `loadCumulativeMap` / `getProvinceCumulative`锛氣憿瑙勪笂瑙勪笅鎷嗗垎琛�**鍙鐩爣鏈�**锛堝師瀹炵幇鍏佽"鐩爣鏈熷強涔嬪墠鏈�杩戜竴鏈�"鈫� 浼氶潤榛樻嬁涓婁竴鏈熺殑鏁板綋鏈湡锛夛紱
+ 2. `copyRankValues`锛氭簮渚э紙鐙珛鎺掑悕琛級缂烘暟鏃�**鐩爣鏍肩暀绌�**锛屼笉鍐嶆部鐢ㄦ瘝鐗堜笂涓�鎶ヨ〃鏈熺殑鏃у�硷紙婧愪晶鏂囧瓧鏍囩涓嶆竻锛夛紱
+ 3. 鎺掑悕鍧�**鏁村潡缂烘暟**锛堟簮渚т竴涓暟閮芥病鏈夛紝濡� 9 鏈堢己鈶♀憿鏃剁殑銆婅揣杩愬懆杞噺鎺掑悕銆嬶級鈫� 鏁村潡鍙暀鏍囬涓庡競宸炲悕锛岄伩鍏嶆瘝鐗堣嚜甯� RANK/鍗犳瘮鍏紡鍦ㄧ┖鍧椾笂绠楀嚭"鎺掑悕鍏� 1 + 鍗犳瘮 #DIV/0!"銆�
+- **鏁堟灉锛�2026-09 瀹炴祴锛屼骇鐗� `docs\鐢熸垚姹囨�诲ぇ琛╘杈撳嚭\鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃_2026-09.xlsx`锛�**锛�
+ - 銆婅揣杩愬懆杞噺鎺掑悕銆嬫暣鍧楃暀绌� 鈥斺�� 涓庣敤鎴锋棩甯告牳瀵圭殑銆婄敓鎴恄鍛ㄨ浆閲忔帓鍚嶃�嬬┖鎬佷竴鑷� 鉁擄紱
+ - 銆婅揣杩愰噺鎺掑悕銆�9 鏈堝潡锛氳涓�=鈶犲疄鏁帮紙鍏ㄧ渷 **3575.1211**锛夈�佽涓�/鍚堣鐣欑┖锛堢己鈶★級锛涘崰姣斿垪娌跨敤婧愪晶鍏紡锛堝叾涓� 72 涓� #DIV/0! 鏄嫭绔嬫帓鍚嶈〃鏈韩灏辨湁鐨勶級锛�
+ - 鏃犲洖褰掞細鍏氦 9 鏈堝杩愰噺 **13476.35**銆佸煄甯傚唴 12021.00銆佸嚭绉熷杩愰噺 5626.81 涓庡簱鍐� 危17 甯傚窞閫愪綅涓�鑷� 鉁擄紙鍏朵綑椤典笌涓婁竴鐗堜骇鐗╁樊寮備粎鏉ヨ嚜姣嶇増鑴忔牸娓呯悊涓庣紦瀛樺埛鏂帮級銆�
+- **鍚屾椂琛ユ彁浜�**锛堟矙绠辨晠闅滄湡闂撮仐鐣欙紝宸叉寜 10-12锛�4锛夋姤鍛婇獙璇佽繃锛夛細`reportexport/calc/SummaryMissingDataCheck.java`锛堝彧璇荤己鏁版竻鍗曪級銆乣GET /api/report/summaryWorkbook/missing`銆佸鍑烘棩蹇� `dbProblems` 鎺ュ叆锛堟湰娆″鍑烘棩蹇楀凡鍙"缂烘暟鎻愮ず=[鈥"锛夈��
+- **閬楃暀/寰呯‘璁�**锛氣憼 鍩庡競鍒嗗競宸炪�侀亾璺繍杈撳懆杞噺鐨� 34 涓� #DIV/0! 鎸� 10-12锛�7锛夌敤鎴峰喅瀹氫笉鍔紱鈶� **鎺掑悕椤靛湪缂烘暟鏈熸暣浣撶暀绌烘槸鍚︾鍚堥鏈燂紝璇烽獙鏀舵椂纭**锛堣嫢瑕�"淇濈暀涓婁竴鏈熷��"鐨勫彛寰勶紝鍙洖閫�绗� 2/3 鏉★級銆�
+- **鎶ュ憡锛堟湰鍦般�佷笉鍏ュ簱锛�**锛歚docs/鍔熻兘娴嬭瘯鎶ュ憡/鍔熻兘娴嬭瘯鎶ュ憡_姹囨�诲ぇ琛ㄧ己鏁颁笉鐣欎笂涓�鏈熸棫鍊糭2026-10-12.md`銆傜姸鎬侊細**銆愬緟楠屾敹銆�**銆�
### 10-12锛�11锛夛細鎵╁垪琛ㄥご琛屼簩娆′慨姝� + 9 鏈堟瘝鐗堣鎵╁垪鍐欐垚鏃ユ湡鐨� 13 鏍兼竻鐞嗐�愬緟楠屾敹銆�
- **鐜拌薄锛堢敤鎴锋姤鍛婏級**锛氱敓鎴愯〃閲�"寰堝鏁版嵁鏄� 8 鏈堢殑"銆傛帓鏌ュ彂鐜� **9 鏈堟瘝鐗堥噷鏈� 13 涓暟鎹牸琚啓鎴愪簡 9 鏈堟湀浠芥棩鏈熷簭鍒� `46266`锛�= 2026-09-01锛�**锛歚 璐ц繍!S11/T11`銆乣鐝嚎鍖呰溅!S9/T9`銆乣鍏氦!S8/T8`銆乣鍑虹!S6/T6`銆乣涓彛寰勬槑缁�!S5/T5`锛涗笅娓哥紦瀛樿甯﹀亸锛坄鐝嚎鍖呰溅!S5`銆乣涓彛寰勬槑缁�!S99/S100/S104/S105` 绛変篃鏄� 46266锛夈��
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryMissingDataCheck.java b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryMissingDataCheck.java
new file mode 100644
index 0000000..2d5e89e
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryMissingDataCheck.java
@@ -0,0 +1,453 @@
+package com.trafficaudit.reportexport.calc;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.trafficaudit.common.util.RegionUtil;
+import com.trafficaudit.dataimport.entity.CityBusMonthly;
+import com.trafficaudit.dataimport.entity.CityTaxiMonthly;
+import com.trafficaudit.dataimport.entity.FreightTurnoverImport;
+import com.trafficaudit.dataimport.entity.H2032EnterpriseMonthly;
+import com.trafficaudit.dataimport.entity.PassengerEnterpriseMonthly;
+import com.trafficaudit.dataimport.entity.ScaleSplitTransport;
+import com.trafficaudit.dataimport.entity.WycOrderMonthly;
+import com.trafficaudit.dataimport.entity.WycTotalMonthly;
+import com.trafficaudit.dataimport.mapper.CityBusMonthlyMapper;
+import com.trafficaudit.dataimport.mapper.CityTaxiMonthlyMapper;
+import com.trafficaudit.dataimport.mapper.FreightTurnoverImportMapper;
+import com.trafficaudit.dataimport.mapper.H2032EnterpriseMonthlyMapper;
+import com.trafficaudit.dataimport.mapper.PassengerEnterpriseMonthlyMapper;
+import com.trafficaudit.dataimport.mapper.ScaleSplitTransportMapper;
+import com.trafficaudit.dataimport.mapper.WycOrderMonthlyMapper;
+import com.trafficaudit.dataimport.mapper.WycTotalMonthlyMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.openxml4j.util.ZipSecureFile;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.usermodel.WorkbookFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 姹囨�诲ぇ琛ㄣ�岀己鏁版竻鍗曘�嶏紙鍙锛�2026-10-12 鏂板锛岀敤鎴疯姹傦級銆�
+ *
+ * <p>涓庛�婅揣杩愬熀鏁颁綋妫�銆媨@link FreightReadinessCheck} 浜掕ˉ銆佸繀椤诲垎寮�锛�
+ * 閭d釜鏌ャ�屼笉鍙兘鐨勬暟銆嶏紙鍊掑噺涓鸿礋銆佄e競宸炩墵鐪佽鈥︼級锛屾湰绫绘煡銆岃鏈夌殑鏁版病鏈夈�嶃��
+ * 2026-08 浣撴 0 鍛戒腑銆佷絾 2026-09 鍑鸿〃鏃� 8 鏈堝垪浠嶄负绌� 鈥斺�� 灏辨槸鍙湁鍓嶈�呫�佹病鏈夊悗鑰呯殑鍚庢灉銆�
+ *
+ * <p>妫�鏌ラ」锛�
+ * <ol>
+ * <li>绋嬪簭閫愭湀浠庡簱鍥炲~鐨勯〉锛堢彮绾垮寘杞︺�佸叕浜ゃ�佸嚭绉熴�佽建閬撹疆娓★級锛�1..鐩爣鏈� 閫愭湀鏌ユ潵婧愭槸鍚︽湁鏁般��17 甯傚窞鏄惁榻愬叏锛�</li>
+ * <li>绋嬪簭鍙洖濉洰鏍囨湀鐨勯〉锛堣揣杩愩�佺綉绾﹁溅锛夛細鐩爣鏈堜笁椤规潵婧愭槸鍚﹂綈鍏紱</li>
+ * <li>涓婅堪涓ら〉鐨� 1..鐩爣鏈�-1 鐢辨瘝鐗堟壙鎷咃細姣嶇増璇ユ湀鍊煎垪鍏ㄧ┖鍒欏嚭琛ㄥ嵆缂猴紙搴撻噷鏈夋暟銆佺▼搴忎笉鍥炲~锛夈��</li>
+ * </ol>
+ *
+ * <p>鏈被鍙銆佷笉鎶涘紓甯革紙寮傚父杞垚娓呭崟鏂囧瓧锛夛紝渚涘鍑烘棩蹇� / 寮圭獥 / 鐢熸垚浠躲�婃暟鎹己澶辨竻鍗曘�嬮〉浣跨敤銆�
+ */
+@Slf4j
+@Service
+public class SummaryMissingDataCheck {
+
+ @Resource
+ private PassengerEnterpriseMonthlyMapper h2031Mapper; // 鐝嚎鍖呰溅锛堝叕璺梾瀹紒涓氭湀鎶ワ級
+ @Resource
+ private CityBusMonthlyMapper cityBusMapper; // 鍏氦 + 杞ㄩ亾/杞浮
+ @Resource
+ private CityTaxiMonthlyMapper cityTaxiMapper; // 鍑虹锛堢綉绾﹁溅鎷嗗垎涔熺敤瀹冪殑骞冲潎杩愯窛锛�
+ @Resource
+ private H2032EnterpriseMonthlyMapper h2032Mapper; // 璐ц繍瑙勪笂锛堚憼锛�
+ @Resource
+ private FreightTurnoverImportMapper freightTurnoverMapper; // 璐ц繍鍏ㄥ彛寰勶紙鈶★級
+ @Resource
+ private ScaleSplitTransportMapper scaleSplitMapper; // 瑙勪笂瑙勪笅鎷嗗垎锛堚憿锛�
+ @Resource
+ private WycOrderMonthlyMapper wycOrderMapper; // 缃戠害杞﹁鍗� + 鍏ㄧ渷 pin
+ @Resource
+ private WycTotalMonthlyMapper wycTotalMapper; // 缃戠害杞� pin 涓撹〃锛堝厹搴曪級
+
+ @Value("${summary.template-dir:docs/鐢熸垚姹囨�诲ぇ琛▆")
+ private String summaryTemplateDir;
+
+ /** 绋嬪簭鍙洖濉�岀洰鏍囨湀銆嶇殑椤� 鈥斺�� 鏇存棭鏈堜唤鐢辨瘝鐗堟壙鎷� */
+ private static final String[] TARGET_ONLY_PAGES = {" 璐ц繍", "缃戠害杞�"};
+
+ public List<String> check(String period) {
+ List<String> issues = new ArrayList<>();
+ if (period == null || period.length() < 7) return issues;
+ int year;
+ int month;
+ try {
+ year = Integer.parseInt(period.substring(0, 4));
+ month = Integer.parseInt(period.substring(5, 7));
+ } catch (Exception e) {
+ return issues;
+ }
+ // 1) 閫愭湀鍥炲~椤碉細1..鐩爣鏈�
+ for (int m = 1; m <= month; m++) {
+ String p = String.format("%04d-%02d", year, m);
+ checkH2031Month(p, issues);
+ checkCityBusMonth(p, issues);
+ checkRailFerryMonth(p, issues);
+ checkCityTaxiMonth(p, issues);
+ }
+ // 2) 鍙洖濉洰鏍囨湀鐨勪袱椤碉細鐩爣鏈堟潵婧�
+ String target = String.format("%04d-%02d", year, month);
+ checkFreightTarget(target, issues);
+ checkWycTarget(target, issues);
+ // 3) 杩欎袱椤电殑鍘嗗彶鏈堬細姣嶇増鏄惁涓虹┖
+ checkMotherHistory(target, year, month, issues);
+ if (!issues.isEmpty()) log.warn("缂烘暟娓呭崟锛坽}锛夛細鍛戒腑 {} 鏉�", period, issues.size());
+ return issues;
+ }
+
+ // ---------- 鍚勯〉妫�鏌� ----------
+
+ private void checkH2031Month(String p, List<String> issues) {
+ try {
+ List<PassengerEnterpriseMonthly> rows = h2031Mapper.selectList(
+ new LambdaQueryWrapper<PassengerEnterpriseMonthly>().eq(PassengerEnterpriseMonthly::getReportPeriod, p));
+ if (rows.isEmpty()) {
+ issues.add(String.format("缂烘暟锛氱彮绾垮寘杞� %s 鏃犮�婂叕璺梾瀹紒涓氭湀鎶ャ�嬫潵婧� 鈫� 璇ユ湀瀹㈣繍閲�/鍛ㄨ浆閲忋�佷腑鍙e緞銆屽叕璺彮绾裤�嶅皢涓虹┖", p));
+ return;
+ }
+ Set<String> got = new LinkedHashSet<>();
+ for (PassengerEnterpriseMonthly r : rows) {
+ String c = RegionUtil.cityByCode(r.getRegionCode());
+ if (c != null) got.add(c);
+ }
+ addMissingCities("鐝嚎鍖呰溅", p, got, issues);
+ } catch (Exception e) {
+ issues.add("缂烘暟妫�鏌ュ紓甯革細鐝嚎鍖呰溅 " + p + " :: " + e.getMessage());
+ }
+ }
+
+ private void checkCityBusMonth(String p, List<String> issues) {
+ try {
+ List<CityBusMonthly> rows = cityBusMapper.selectList(
+ new LambdaQueryWrapper<CityBusMonthly>().eq(CityBusMonthly::getReportPeriod, p));
+ if (rows.isEmpty()) {
+ issues.add(String.format("缂烘暟锛氬叕浜� %s 鏃犮�婂煄甯傚叕浜ゆ湀鎶ャ�嬫潵婧� 鈫� 鍏氦椤典负绌恒�佸煄甯傚杩愰〉灏嗙己鍏氦閮ㄥ垎", p));
+ return;
+ }
+ addMissingCities("鍏氦", p, citySetOf(rows), issues);
+ } catch (Exception e) {
+ issues.add("缂烘暟妫�鏌ュ紓甯革細鍏氦 " + p + " :: " + e.getMessage());
+ }
+ }
+
+ private void checkRailFerryMonth(String p, List<String> issues) {
+ try {
+ List<CityBusMonthly> rows = cityBusMapper.selectList(
+ new LambdaQueryWrapper<CityBusMonthly>().eq(CityBusMonthly::getReportPeriod, p));
+ if (rows.isEmpty()) {
+ issues.add(String.format("缂烘暟锛氳建閬撱�佽疆娓� %s 鏃犮�婂煄甯傚叕浜ゆ湀鎶ャ�嬫潵婧愶紙杞ㄩ亾/杞浮鍚岃〃锛夆啋 璇ラ〉涓虹┖", p));
+ return;
+ }
+ boolean rail = false;
+ boolean ferry = false;
+ for (CityBusMonthly r : rows) {
+ if (r.getRailPassengerVolume() != null) rail = true;
+ if (r.getFerryPassengerVolume() != null) ferry = true;
+ }
+ List<String> miss = new ArrayList<>();
+ if (!rail) miss.add("杞ㄩ亾");
+ if (!ferry) miss.add("杞浮");
+ if (!miss.isEmpty()) {
+ issues.add(String.format("缂烘暟锛氳建閬撱�佽疆娓� %s 鐨� %s 瀛楁鍏ㄧ┖ 鈫� 璇ュ潡灏嗕负绌�",
+ p, String.join("銆�", miss)));
+ }
+ } catch (Exception e) {
+ issues.add("缂烘暟妫�鏌ュ紓甯革細杞ㄩ亾銆佽疆娓� " + p + " :: " + e.getMessage());
+ }
+ }
+
+ private void checkCityTaxiMonth(String p, List<String> issues) {
+ try {
+ List<CityTaxiMonthly> rows = cityTaxiMapper.selectList(
+ new LambdaQueryWrapper<CityTaxiMonthly>().eq(CityTaxiMonthly::getReportPeriod, p));
+ if (rows.isEmpty()) {
+ issues.add(String.format("缂烘暟锛氬嚭绉� %s 鏃犮�婂贰娓稿嚭绉熸苯杞︽湀鎶ャ�嬫潵婧� 鈫� 鍑虹椤典负绌猴紝涓旂綉绾﹁溅鎷嗗垎鍙栦笉鍒板钩鍧囪繍璺�", p));
+ return;
+ }
+ Set<String> got = new LinkedHashSet<>();
+ for (CityTaxiMonthly r : rows) {
+ String c = RegionUtil.normalizeCityName(r.getCity());
+ if (c != null && !c.isEmpty()) got.add(c);
+ }
+ addMissingCities("鍑虹", p, got, issues);
+ } catch (Exception e) {
+ issues.add("缂烘暟妫�鏌ュ紓甯革細鍑虹 " + p + " :: " + e.getMessage());
+ }
+ }
+
+ private void checkFreightTarget(String p, List<String> issues) {
+ try {
+ // 鈶� H203-2 浼佷笟鏈堟姤锛堣涓婅揣杩愰噺/鍛ㄨ浆閲忥級
+ List<H2032EnterpriseMonthly> h = h2032Mapper.selectList(
+ new LambdaQueryWrapper<H2032EnterpriseMonthly>().eq(H2032EnterpriseMonthly::getReportPeriod, p));
+ if (h.isEmpty()) {
+ issues.add(String.format("缂烘暟锛氳揣杩� %s 鏃犫憼銆奌203-2 閬撹矾璐х墿杩愯緭鏈堝害鐢熶骇鎯呭喌銆嬧啋 瑙勪笂璐ц繍閲�/鍛ㄨ浆閲忓彇涓嶅埌", p));
+ } else {
+ Set<String> got = new LinkedHashSet<>();
+ for (H2032EnterpriseMonthly r : h) {
+ String c = RegionUtil.cityByCode(r.getRegionCode());
+ if (c != null) got.add(c);
+ }
+ addMissingCities("璐ц繍鈶犺涓�", p, got, issues);
+ }
+ // 鈶� 璐ц繍閲忓懆杞噺琛紙鍚堣 = 瑙勪笂+瑙勪笅锛�
+ List<FreightTurnoverImport> f = freightTurnoverMapper.selectList(
+ new LambdaQueryWrapper<FreightTurnoverImport>().eq(FreightTurnoverImport::getReportPeriod, p));
+ if (f.isEmpty()) {
+ issues.add(String.format("缂烘暟锛氳揣杩� %s 鏃犫憽銆婅揣杩愰噺鍛ㄨ浆閲忚〃銆嬧啋 鍚堣璐ц繍閲�/鍛ㄨ浆閲忋�佽涓嬶紙鍊掑噺锛夊叏閮ㄥ彇涓嶅埌", p));
+ } else {
+ Set<String> got = new LinkedHashSet<>();
+ for (FreightTurnoverImport r : f) {
+ String c = RegionUtil.normalizeCityName(r.getRegionName());
+ if (c != null && !RegionUtil.cityList().contains(c) && !"婀栧寳鐪�".equals(c)) continue;
+ if (c != null && RegionUtil.cityList().contains(c)) got.add(c);
+ }
+ addMissingCities("璐ц繍鈶″叏鍙e緞", p, got, issues);
+ }
+ // 鈶� 鎷嗗垎琛紙瑙勪笂/鍚堣鍛ㄨ浆閲忥級锛屽綋鏈� + 绱
+ List<ScaleSplitTransport> s = scaleSplitMapper.selectList(
+ new LambdaQueryWrapper<ScaleSplitTransport>().eq(ScaleSplitTransport::getReportPeriod, p));
+ if (s.isEmpty()) {
+ issues.add(String.format("缂烘暟锛氳揣杩� %s 鏃犫憿銆婅涓婅涓嬫媶鍒嗚〃銆嬧啋 瑙勪笂/鍚堣鍛ㄨ浆閲忓彇涓嶅埌锛堣鏈堝懆杞噺灏嗕负绌�/0锛�", p));
+ } else {
+ Set<String> month = new LinkedHashSet<>();
+ Set<String> cum = new LinkedHashSet<>();
+ for (ScaleSplitTransport r : s) {
+ String c = RegionUtil.normalizeCityName(r.getRegionName());
+ if (c == null || !RegionUtil.cityList().contains(c)) continue;
+ if ("MONTH".equals(r.getPeriodType())) month.add(c);
+ else if ("CUMULATIVE".equals(r.getPeriodType())) cum.add(c);
+ }
+ addMissingCities("璐ц繍鈶㈡媶鍒�(褰撴湀)", p, month, issues);
+ addMissingCities("璐ц繍鈶㈡媶鍒�(绱)", p, cum, issues);
+ }
+ } catch (Exception e) {
+ issues.add("缂烘暟妫�鏌ュ紓甯革細璐ц繍 " + p + " :: " + e.getMessage());
+ }
+ }
+
+ private void checkWycTarget(String p, List<String> issues) {
+ try {
+ List<WycOrderMonthly> orders = wycOrderMapper.selectList(
+ new LambdaQueryWrapper<WycOrderMonthly>().eq(WycOrderMonthly::getReportPeriod, p));
+ boolean okOrders = false;
+ boolean okPin = false;
+ if (!orders.isEmpty()) {
+ WycOrderMonthly o = orders.get(0);
+ double sum = nz(o.getOrderWuhan()) + nz(o.getOrderHuangshi()) + nz(o.getOrderShiyan()) + nz(o.getOrderYichang())
+ + nz(o.getOrderXiangyang()) + nz(o.getOrderEzhou()) + nz(o.getOrderJingmen()) + nz(o.getOrderXiaogan())
+ + nz(o.getOrderJingzhou()) + nz(o.getOrderHuanggang()) + nz(o.getOrderXianning()) + nz(o.getOrderSuizhou())
+ + nz(o.getOrderEnshi()) + nz(o.getOrderXiantao()) + nz(o.getOrderQianjiang()) + nz(o.getOrderTianmen())
+ + nz(o.getOrderShennong());
+ okOrders = sum > 0;
+ okPin = nz(o.getPinK()) > 0 && nz(o.getPinCK()) > 0 && nz(o.getPinZ()) > 0 && nz(o.getPinCZ()) > 0;
+ }
+ if (!okOrders) {
+ issues.add(String.format("缂烘暟锛氱綉绾﹁溅 %s 鏃犺鍗曟暟鎹紙wyc_order_monthly 17 甯傚窞璁㈠崟锛夆啋 鎷嗗垎鏃犳硶璁$畻锛岃椤靛皢涓虹┖", p));
+ }
+ if (!okPin) {
+ List<WycTotalMonthly> tot = wycTotalMapper.selectList(
+ new LambdaQueryWrapper<WycTotalMonthly>().eq(WycTotalMonthly::getReportPeriod, p));
+ boolean okTot = !tot.isEmpty()
+ && nz(tot.get(0).getPinK()) > 0 && nz(tot.get(0).getPinCK()) > 0
+ && nz(tot.get(0).getPinZ()) > 0 && nz(tot.get(0).getPinCZ()) > 0;
+ if (!okTot) {
+ issues.add(String.format("缂烘暟锛氱綉绾﹁溅 %s 缂哄叏鐪� pin锛坥rder 琛ㄤ笌 wyc_total_monthly 閮芥病鏈夛級鈫� 鎷嗗垎鏃犳硶璁$畻", p));
+ }
+ }
+ List<CityTaxiMonthly> taxi = cityTaxiMapper.selectList(
+ new LambdaQueryWrapper<CityTaxiMonthly>().eq(CityTaxiMonthly::getReportPeriod, p));
+ Set<String> got = new LinkedHashSet<>();
+ for (CityTaxiMonthly r : taxi) {
+ String c = RegionUtil.normalizeCityName(r.getCity());
+ if (c != null && !c.isEmpty()) got.add(c);
+ }
+ addMissingCities("缃戠害杞�(渚濊禆鍑虹骞冲潎杩愯窛)", p, got, issues);
+ } catch (Exception e) {
+ issues.add("缂烘暟妫�鏌ュ紓甯革細缃戠害杞� " + p + " :: " + e.getMessage());
+ }
+ }
+
+ /** 璐ц繍/缃戠害杞︾殑鍘嗗彶鏈堬紙1..M-1锛夌敱姣嶇増鎵挎媴锛堟瘝鐗堟病鏈夋椂鎸夈�屽悓鏈熷浠芥瘝鐗堛�嶈繕鍘燂級锛�
+ * 鍙湁姣嶇増涓庡浠芥瘝鐗堥兘娌℃湁鏁帮紝鎵嶇畻鐪熸鐨勫嚭琛ㄧ己鍙o紙鍚﹀垯浼氳鎶� 7 鏈堥偅绉嶃�岄鏋舵瘝鐗� + donor 杩樺師銆嶇殑鎯呭喌锛夈�� */
+ private void checkMotherHistory(String period, int year, int month, List<String> issues) {
+ File mother = resolveMother(year, month);
+ if (mother == null) {
+ issues.add(String.format("缂烘暟锛氭湭鎵惧埌姣嶇増銆�%d骞�%d鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx銆嬶紙%s锛�", year, month, summaryTemplateDir));
+ return;
+ }
+ File donor = resolveDonor(year, month);
+ double saved = ZipSecureFile.getMinInflateRatio();
+ ZipSecureFile.setMinInflateRatio(0.0001);
+ try (InputStream in = new FileInputStream(mother);
+ Workbook wb = WorkbookFactory.create(in);
+ InputStream in2 = (donor != null) ? new FileInputStream(donor) : null;
+ Workbook dwb = (in2 != null) ? WorkbookFactory.create(in2) : null) {
+ for (String pageName : TARGET_ONLY_PAGES) {
+ List<String> blankMonths = new ArrayList<>();
+ for (int m = 1; m < month; m++) {
+ if (monthHasData(wb, pageName, year, m)) continue;
+ if (dwb != null && monthHasData(dwb, pageName, year, m)) continue;
+ blankMonths.add(String.format("%d骞�%d鏈�", year, m));
+ }
+ if (!blankMonths.isEmpty()) {
+ issues.add(String.format("缂烘暟锛�%s 姣嶇増锛�%s 鏈燂級鐨� %s 鍊煎垪鍏ㄧ┖%s锛岃�岀▼搴忓彧鍥炲~鐩爣鏈� 鈫� 鍑鸿〃璇ユ湀灏嗕负绌猴紙搴撻噷鍙兘鏈夋暟鎹級",
+ pageName.trim(), period, String.join("銆�", blankMonths),
+ donor == null ? "" : "锛堝悓鏈熷浠芥瘝鐗堜篃娌℃湁锛�"));
+ }
+ }
+ } catch (Exception e) {
+ issues.add("缂烘暟妫�鏌ュ紓甯革細璇绘瘝鐗� " + mother.getName() + " :: " + e.getMessage());
+ } finally {
+ ZipSecureFile.setMinInflateRatio(saved);
+ }
+ }
+
+ /** 璇ュ伐浣滅翱璇ラ〉銆佽骞磋鏈堢殑銆屽�煎垪銆嶆槸鍚﹀凡鏈夋暟鍊硷紙琛ㄥご琛屼箣澶栵級 */
+ private boolean monthHasData(Workbook wb, String pageName, int year, int m) {
+ Sheet sheet = findSheet(wb, pageName);
+ if (sheet == null) return false;
+ int[] ref = detectYearMonthCols(sheet, year).get(m);
+ if (ref == null) return false;
+ return countNumericInColumn(sheet, ref[1], ref[0]) > 0;
+ }
+
+ private Sheet findSheet(Workbook wb, String pageName) {
+ Sheet s = wb.getSheet(pageName);
+ if (s == null && " 璐ц繍".equals(pageName)) s = wb.getSheet("璐ц繍");
+ return s;
+ }
+
+ /** 鍚屾湡澶囦唤姣嶇増锛堟竻鐞嗗墠鐨勫畬鏁存湀姣嶇増锛夛紝鍛藉悕 _澶囦唤_{骞磢骞磠鏈坿鏈堚�﹂亾璺繍杈撻噺姹囨�昏〃 */
+ private File resolveDonor(int year, int month) {
+ String rel = summaryTemplateDir;
+ while (rel.startsWith("./")) rel = rel.substring(2);
+ String[] roots = {summaryTemplateDir, System.getProperty("user.dir") + "/" + rel,
+ System.getProperty("user.dir") + "/../" + rel};
+ for (String root : roots) {
+ if (root == null || root.trim().isEmpty()) continue;
+ File dir = new File(root);
+ File[] files = dir.listFiles((d, name) -> name.startsWith("_澶囦唤_")
+ && name.contains(year + "骞�" + month + "鏈�") && name.contains("閬撹矾杩愯緭閲忔眹鎬昏〃"));
+ if (files != null && files.length > 0) return files[0];
+ }
+ return null;
+ }
+
+ // ---------- 宸ュ叿 ----------
+
+ private Set<String> citySetOf(List<CityBusMonthly> rows) {
+ Set<String> got = new LinkedHashSet<>();
+ for (CityBusMonthly r : rows) {
+ String c = RegionUtil.normalizeCityName(r.getCity());
+ if (c != null && !c.isEmpty()) got.add(c);
+ }
+ return got;
+ }
+
+ private void addMissingCities(String page, String period, Set<String> got, List<String> issues) {
+ List<String> miss = new ArrayList<>();
+ for (String c : RegionUtil.cityList()) {
+ if (!got.contains(c)) miss.add(c);
+ }
+ if (!miss.isEmpty()) {
+ issues.add(String.format("缂烘暟锛�%s %s 缂哄競宸烇細%s", page, period, String.join("銆�", miss)));
+ }
+ }
+
+ /** 姣嶇増閲屽畾浣嶇洰鏍囧勾鍚勬湀銆屽�煎垪銆嶏細杩斿洖 鏈� -> [琛ㄥご琛�(0鍩�), 鍒�(0鍩�)]锛涘吋瀹规枃鏈��2026骞碝鏈堛�嶄笌鏃ユ湡搴忓垪鍙� */
+ private Map<Integer, int[]> detectYearMonthCols(Sheet sheet, int year) {
+ Map<Integer, int[]> out = new LinkedHashMap<>();
+ int lastRow = Math.min(5, sheet.getLastRowNum());
+ for (int r = 0; r <= lastRow; r++) {
+ Row row = sheet.getRow(r);
+ if (row == null) continue;
+ for (Cell c : row) {
+ Integer m = monthOfCell(c, year);
+ if (m != null && !out.containsKey(m)) {
+ out.put(m, new int[]{r, c.getColumnIndex()});
+ }
+ }
+ }
+ return out;
+ }
+
+ private Integer monthOfCell(Cell c, int year) {
+ try {
+ if (c == null) return null;
+ if (c.getCellType() == CellType.STRING) {
+ String s = c.getStringCellValue();
+ if (s == null) return null;
+ java.util.regex.Matcher mt = java.util.regex.Pattern
+ .compile("(20\\d\\d)\\s*骞碶\s*(\\d{1,2})\\s*鏈�").matcher(s);
+ if (mt.find()) {
+ if (Integer.parseInt(mt.group(1)) != year) return null;
+ int m = Integer.parseInt(mt.group(2));
+ return (m >= 1 && m <= 12) ? m : null;
+ }
+ return null;
+ }
+ if (c.getCellType() == CellType.NUMERIC) {
+ double v = c.getNumericCellValue();
+ if (v < 20000 || v > 60000 || v != Math.floor(v)) return null;
+ java.util.Date d = org.apache.poi.ss.usermodel.DateUtil.getJavaDate(v);
+ java.util.Calendar cal = java.util.Calendar.getInstance();
+ cal.setTime(d);
+ if (cal.get(java.util.Calendar.YEAR) != year) return null;
+ return cal.get(java.util.Calendar.MONTH) + 1;
+ }
+ } catch (Exception ignore) {
+ // 闈炴湀浠借〃澶�
+ }
+ return null;
+ }
+
+ /** 缁熻鏌愬垪锛堣烦杩囪〃澶磋锛夐潪绌烘暟鍊兼牸鏁� */
+ private int countNumericInColumn(Sheet sheet, int col, int headerRow) {
+ int n = 0;
+ for (int r = 0; r <= sheet.getLastRowNum(); r++) {
+ if (r == headerRow) continue;
+ Row row = sheet.getRow(r);
+ if (row == null) continue;
+ Cell c = row.getCell(col);
+ if (c == null) continue;
+ if (c.getCellType() == CellType.NUMERIC || c.getCellType() == CellType.FORMULA) n++;
+ }
+ return n;
+ }
+
+ private File resolveMother(int year, int month) {
+ String rel = summaryTemplateDir;
+ while (rel.startsWith("./")) rel = rel.substring(2);
+ String name = year + "骞�" + month + "鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx";
+ String[] roots = {summaryTemplateDir, System.getProperty("user.dir") + "/" + rel,
+ System.getProperty("user.dir") + "/../" + rel};
+ for (String root : roots) {
+ if (root == null || root.trim().isEmpty()) continue;
+ File f = new File(root, name);
+ if (f.exists() && f.isFile()) return f;
+ }
+ return null;
+ }
+
+ private double nz(Double v) {
+ return v == null ? 0.0 : v;
+ }
+}
\ No newline at end of file
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 292beb0..db32034 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
@@ -2,6 +2,7 @@
import com.trafficaudit.common.Result;
import com.trafficaudit.reportexport.calc.FreightReadinessCheck;
+import com.trafficaudit.reportexport.calc.SummaryMissingDataCheck;
import com.trafficaudit.reportexport.service.ReportExportService;
import com.trafficaudit.reportexport.service.ReportExportTaskService;
import com.trafficaudit.system.service.OperationLogService;
@@ -29,6 +30,8 @@
private ReportExportService reportService;
@Resource
private FreightReadinessCheck freightReadinessCheck;
+ @Resource
+ private SummaryMissingDataCheck missingDataCheck;
@Resource
private ReportExportTaskService exportTaskService;
@Resource
@@ -98,6 +101,12 @@
return Result.ok(freightReadinessCheck.check(period));
}
+ /** 姹囨�诲ぇ琛ㄧ己鏁版竻鍗曪紙鍙锛夛細鍒楀嚭鐩爣鏈熷嚭琛ㄤ細缂虹殑鏈堜唤/甯傚窞/鏉ユ簮锛堜笌 freightCheck 浜掕ˉ锛夈�� */
+ @GetMapping("/summaryWorkbook/missing")
+ public Result<java.util.List<String>> missingCheck(@RequestParam("period") String period) {
+ return Result.ok(missingDataCheck.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 ee4203f..59ceab0 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
@@ -1439,10 +1439,11 @@
Map<String, ScaleSplitTransport> result = new HashMap<>();
List<ScaleSplitTransport> list = scaleSplitMapper.selectList(
new LambdaQueryWrapper<ScaleSplitTransport>()
- .eq(ScaleSplitTransport::getPeriodType, "CUMULATIVE"));
+ .eq(ScaleSplitTransport::getPeriodType, "CUMULATIVE")
+ .eq(ScaleSplitTransport::getReportPeriod, period)); // 2026-10-12锛氬彧璁ょ洰鏍囨湡锛岀己鏁扮暀绌恒�佷笉闈欓粯鍥為��涓婁竴鏈�
for (ScaleSplitTransport record : list) {
if (record.getReportPeriod() == null) continue;
- if (record.getReportPeriod().compareTo(period) > 0) continue;
+ if (!period.equals(record.getReportPeriod())) continue;
String city = RegionUtil.normalizeCityName(record.getRegionName());
if (city == null) continue;
if ("婀栧寳鐪�".equals(city)) continue;
@@ -1475,11 +1476,12 @@
private ScaleSplitTransport getProvinceCumulative(String period) {
List<ScaleSplitTransport> list = scaleSplitMapper.selectList(
new LambdaQueryWrapper<ScaleSplitTransport>()
- .eq(ScaleSplitTransport::getPeriodType, "CUMULATIVE"));
+ .eq(ScaleSplitTransport::getPeriodType, "CUMULATIVE")
+ .eq(ScaleSplitTransport::getReportPeriod, period)); // 2026-10-12锛氬悓涓婏紙鍙鐩爣鏈燂級
ScaleSplitTransport best = null;
for (ScaleSplitTransport record : list) {
if (record.getReportPeriod() == null) continue;
- if (record.getReportPeriod().compareTo(period) > 0) continue;
+ if (!period.equals(record.getReportPeriod())) continue;
String name = RegionUtil.normalizeCityName(record.getRegionName());
if (!"婀栧寳鐪�".equals(name)) continue;
if (best == null || record.getReportPeriod().compareTo(best.getReportPeriod()) > 0) best = record;
@@ -3988,15 +3990,38 @@
int colOff = dstTitle.getColumnIndex() - sc0;
int h = rankBlockHeight(src, sr0, sc0);
int w = rankBlockWidth(src, sr0, sc0);
+ // 2026-10-12锛氭暣鍧楃己鏁帮紙婧愪晶涓�涓暟鍊奸兘娌℃湁锛屽 9 鏈堢己鈶♀憿鏃剁殑鍛ㄨ浆閲忔帓鍚嶏級鈫� 鐩爣鍧楀彧鐣欐爣棰樹笌鏍囩锛�
+ // 鏁版嵁/鎺掑悕/鍗犳瘮涓�寰嬬暀绌猴紱鍚﹀垯姣嶇増鐨� RANK/鍗犳瘮鍏紡浼氬湪绌哄潡涓婄畻鍑恒�屽叏 1 + #DIV/0!銆嶃��
+ boolean blockHasData = false;
+ for (int r = 1; r < h && !blockHasData; r++) {
+ Row sr = src.getRow(sr0 + r);
+ if (sr == null) continue;
+ for (int c = 0; c < w; c++) {
+ Cell sc0c = sr.getCell(sc0 + c);
+ if (sc0c != null && rankNumeric(sc0c, ev) != null) {
+ blockHasData = true;
+ break;
+ }
+ }
+ }
for (int r = 0; r < h; r++) {
Row srcRow = src.getRow(sr0 + r);
if (srcRow == null) continue;
Row dstRow = dst.getRow(sr0 + r + rowOff);
if (dstRow == null) dstRow = dst.createRow(sr0 + r + rowOff);
for (int c = 0; c < w; c++) {
- Cell sc = srcRow.getCell(sc0 + c);
- if (sc == null) continue;
int dc = sc0 + c + colOff;
+ Cell sc = srcRow.getCell(sc0 + c);
+ if (!blockHasData && r > 0) {
+ // 鏁村潡缂烘暟锛氬彧淇濈暀鏍囩锛堝競宸炲悕/鎸囨爣鍚嶏級锛屾暟鎹笌鎺掑悕/鍗犳瘮閲嶇畻缁撴灉涓�寰嬬暀绌�
+ if (sc == null || !isRankLabel(sc)) clearRankCell(dstRow, dc);
+ continue;
+ }
+ if (sc == null) {
+ // 2026-10-12锛氭簮渚ф暣鏍肩己澶� = 缂烘暟 鈫� 鐩爣鏍兼寜銆岀己鏁扮暀绌恒�嶆竻绌猴紙涓嶅啀娌跨敤姣嶇増涓婁竴鎶ヨ〃鏈熺殑鏃у�硷級
+ if (r > 0) clearRankCell(dstRow, dc);
+ continue;
+ }
if (r == 0) {
// 鏍囬琛岋細鍚屾鍧楁爣棰樻枃妗堛�傛瘝鐗堟爣棰樻槸 inlineStr锛孭OI 鐨� setCellValue 鍙啓 <v>銆�
// 涓嶆洿鏂� <is>锛孍xcel 浼氱户缁樉绀烘棫鏈堜唤锛屽繀椤诲厛 setBlank 娓呮帀鍐嶅啓銆�
@@ -4031,7 +4056,12 @@
continue;
}
Double v = rankNumeric(sc, ev);
- if (v == null) continue;
+ if (v == null) {
+ // 2026-10-12锛氭簮渚ф暟鍊间綅涓虹┖锛堝缂衡憽鍏ㄥ彛寰勬椂鎺掑悕琛ㄧ殑銆岃涓嬨�嶅垪锛�= 缂烘暟 鈫� 鐣欑┖锛�
+ // 婧愪晶鏄爣绛炬枃鏈紙甯傚窞鍚�/鎸囨爣鍚嶏級鏃朵笉鏄己鏁帮紝淇濈暀姣嶇増鑷甫鍐呭銆�
+ if (!isRankLabel(sc)) clearRankCell(dstRow, dc);
+ continue;
+ }
if (dcCell == null) dcCell = rankCell(dstRow, dc);
dcCell.setCellValue(v);
copied++;
@@ -4050,6 +4080,34 @@
return c;
}
+ /**
+ * 鐩爣鏍兼寜銆岀己鏁扮暀绌恒�嶆竻绌恒��
+ *
+ * <p>2026-10-12锛氳繖閲岃繛姣嶇増鑷甫鐨勬帓鍚�/鍗犳瘮鍏紡鏍间竴璧锋竻锛堢己鏁版湡涓嶆樉绀�"涓婁竴鏈熸畫鐣欏�� + 鍏� 1 鎺掑悕 + #DIV/0!"锛夛紝
+ * 涓庛�婄敓鎴恄璐ц繍閲忔帓鍚�/鍛ㄨ浆閲忔帓鍚�/涓彛寰勬帓鍚嶃�嬭繖浜涚敤鎴锋棩甯告牳瀵逛欢鐨勭┖鎬佷竴鑷达紱涓嬫湡婧愪晶鏈夋暟鏃舵寜婧愪晶閲嶅啓鍥炴潵銆�
+ * 鏂囧瓧鏍囩鏍间笉鍔ㄣ��
+ */
+ private void clearRankCell(Row dstRow, int col) {
+ Cell c = dstRow.getCell(col);
+ if (c == null) return;
+ if (c.getCellType() != CellType.STRING) c.setBlank();
+ }
+
+ /** 婧愪晶鏄惁涓烘枃瀛楁爣绛撅紙甯傚窞鍚�/鎸囨爣鍚嶏級鈥斺�旀爣绛句笉鏄己鏁帮紝涓嶆竻绌虹洰鏍囨牸 */
+ private boolean isRankLabel(Cell c) {
+ if (c.getCellType() != CellType.STRING) return false;
+ String v = c.getStringCellValue();
+ if (v == null) return false;
+ String x = v.replace(",", "").trim();
+ if (x.isEmpty()) return false;
+ try {
+ Double.parseDouble(x);
+ return false;
+ } catch (NumberFormatException ignore) {
+ return true;
+ }
+ }
+
/** 鍏紡鍐呮墍鏈� A1 寮曠敤鏁翠綋骞崇Щ锛氬垪 +dCol銆佽 +dRow锛圫UM/RANK 绛夊嚱鏁板悕鍚庢帴鎷彿涓嶄細琚尮閰嶏級 */
private String shiftFormula(String formula, int dCol, int dRow) {
if (formula == null || (dCol == 0 && dRow == 0)) return formula;
--
Gitblit v1.9.1