From 488cf7a7127fc137864d4fdeee85942986c05108 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期三, 14 十月 2026 08:27:11 +0800
Subject: [PATCH] docs(交接): 添加2026-10-14重启与合并同事提交的交接快照
---
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java | 757 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 702 insertions(+), 55 deletions(-)
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 bbe8df3..c4476dd 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,13 +34,20 @@
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.calc.SummaryMissingDataCheck;
+import com.trafficaudit.reportexport.layout.SummaryLayoutExpander;
import com.trafficaudit.reportexport.calc.WycSplitCalc;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.FormulaEvaluator;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
@@ -101,6 +108,12 @@
private CityTaxiMonthlyMapper cityTaxiMapper;
@Resource
private SummaryWorkbookFiller summaryWorkbookFiller;
+ @Resource
+ private SummaryLayoutExpander summaryLayoutExpander;
+ @Resource
+ private FreightReadinessCheck freightReadinessCheck;
+ @Resource
+ private SummaryMissingDataCheck missingDataCheck;
@Resource
private WycSplitCalc wycSplitCalc;
@Resource
@@ -1430,10 +1443,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;
@@ -1466,11 +1480,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;
@@ -3622,6 +3637,86 @@
return exportSummaryWorkbook(period, mode, null, null);
}
+ /**
+ * 姹囨�诲ぇ琛ㄣ�屾寜鏈堟墿鍒椼�嶏紙2026-10-09 Step 1锛夛細鎶� period 褰撴湀姣嶇増鎵╁垪鎴愪笅涓�鏈堢増寮忋��
+ * 鍙仛鐗堝紡鈥斺�斿綋鏈堟暟鎹垪鐣欑┖锛涗笉钀界洏锛岀敱璋冪敤鏂瑰喅瀹氭槸鍚︾‘璁ゅ悗鍙﹀瓨涓轰笅鏈熸瘝鐗堛��
+ */
+ public byte[] expandSummaryWorkbook(String period) throws Exception {
+ if (period == null || !period.contains("-")) {
+ throw new RuntimeException("鎵╁垪浠呮敮鎸佹寜鏈堬紝璇锋寚瀹� period=YYYY-MM");
+ }
+ int year = Integer.parseInt(period.split("-")[0]);
+ int month = monthOf(period, "month");
+ if (month >= 12) {
+ throw new RuntimeException("鎵╁垪锛�12 鏈堜箣鍚庨渶鏂板紑涓�鏈紝璺ㄥ勾瑙勫垯鍙﹁澶勭悊");
+ }
+ // 2026-10-12锛氭墿鍒椾互銆屽綋鏈熶骇鐗┿�嶄负搴曪紙浜х墿鍚綋鏈熷垪鐨勫�间笌鍏紡锛涙瘝鐗堢殑褰撴湡鍒楁槸绌虹殑锛�
+ // 鐩存帴鎷挎瘝鐗堟墿鍒椾細鎶婂綋鏈熸暟鎹涪鎴愮┖娲烇紝瀵艰嚧绱婕忔湀锛夈�備骇鐗╁彇鑷鍑鸿惤鐩樼紦瀛橈紱缂轰骇鐗╂椂鍥為��姣嶇増骞跺憡璀︺��
+ File source = resolveLatestProduct(period);
+ boolean fromProduct = source != null;
+ if (source == null) {
+ source = resolveSummaryMother(period, "month");
+ if (source != null) {
+ log.warn("姹囨�诲ぇ琛ㄦ墿鍒楋細鏈壘鍒� {} 鐨勪骇鐗╃紦瀛橈紝鍥為��鐢ㄦ瘝鐗堟墿鍒椻�斺�斿巻鍙插垪閲岀己灏戝綋鏈熸暟鎹紝绱鍙兘婕忔湀", period);
+ }
+ }
+ if (source == null) {
+ throw new RuntimeException("鏈壘鍒版眹鎬昏〃姣嶇増鎴栦骇鐗╋紙docs/鐢熸垚姹囨�诲ぇ琛級锛�" + year + "骞�" + month + "鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx");
+ }
+ try (InputStream in = new FileInputStream(source)) {
+ byte[] out = summaryLayoutExpander.expand(org.apache.poi.util.IOUtils.toByteArray(in), year, month + 1);
+ log.info("姹囨�诲ぇ琛ㄦ墿鍒楀畬鎴愶細{}{} -> {}骞磠}鏈堢増寮忥紙鍙仛鐗堝紡銆佸綋鏈熷垪鐣欑┖锛�",
+ fromProduct ? "浜х墿" : "姣嶇増", source.getName(), year, month + 1);
+ return out;
+ }
+ }
+
+ /** 鍙栬鏈熴�屾渶鏂颁骇鐗┿�嶏細瀵煎嚭钀界洏缂撳瓨 _cache/summaryWorkbook_<period>_month_v*.xlsx锛堟寜淇敼鏃堕棿鍙栨渶鏂帮級 */
+ private File resolveLatestProduct(String period) {
+ try {
+ String rel = summaryTemplateDir;
+ while (rel != null && rel.startsWith("./")) rel = rel.substring(2);
+ String[] roots = {
+ summaryTemplateDir,
+ System.getProperty("user.dir") + "/" + rel,
+ System.getProperty("user.dir") + "/../" + rel
+ };
+ String prefix = "summaryWorkbook_" + period + "_month_";
+ for (String root : roots) {
+ if (root == null || root.trim().isEmpty()) continue;
+ File dir = new File(root, "_cache");
+ if (!dir.isDirectory()) continue;
+ File[] files = dir.listFiles((d, name) -> name.startsWith(prefix) && name.endsWith(".xlsx"));
+ if (files == null || files.length == 0) continue;
+ File latest = files[0];
+ for (File f : files) {
+ if (f.lastModified() > latest.lastModified()) latest = f;
+ }
+ return latest;
+ }
+ return null;
+ } catch (Exception e) {
+ log.warn("姹囨�诲ぇ琛ㄦ墿鍒楋細鏌ユ壘浜х墿缂撳瓨澶辫触锛堝拷鐣ワ級锛歿}", e.getMessage());
+ return null;
+ }
+ }
+
+ /** 鍙慨姝e綋鏈熸瘝鐗堥噷 7 涓爣棰橀〉鐨勬爣棰樻枃瀛楋紙鎸夋瘝鐗堟湡娆¢噸鍐欙紝鐢ㄤ簬淇婊炲悗鏍囬锛夈�� */
+ public byte[] fixSummaryTitles(String period) throws Exception {
+ if (period == null || !period.contains("-")) {
+ throw new RuntimeException("鏍囬淇浠呮敮鎸佹寜鏈堬紝璇锋寚瀹� period=YYYY-MM");
+ }
+ int year = Integer.parseInt(period.split("-")[0]);
+ int month = monthOf(period, "month");
+ File mother = resolveSummaryMother(period, "month");
+ if (mother == null) {
+ throw new RuntimeException("鏈壘鍒版眹鎬昏〃姣嶇増锛坉ocs/鐢熸垚姹囨�诲ぇ琛級锛�" + year + "骞�" + month + "鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx");
+ }
+ try (InputStream in = new FileInputStream(mother)) {
+ return summaryLayoutExpander.fixTitles(org.apache.poi.util.IOUtils.toByteArray(in), year, month);
+ }
+ }
+
/** 鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx锛圥0-P1 楠ㄦ灦锛屾敮鎸� toMonth 瑁佸壀锛夛細
* 浠ャ�奩YYY骞碝鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx銆嬶紙M 鐢� period 鍐冲畾锛変负姣嶇増锛屾寜鍚屾湡澶囦唤姣嶇増杩樺師鏈堝害鍧楋紱
* toMonth 闈炵┖涓斿皬浜� M 鏃讹紝鎶� 8 涓彴璐�/鍚堟垚椤佃鍒� toMonth 鏈堬紙toMonth+1..M 鐨勫綋鏈堝��/鍚屾瘮鍒楁竻绌恒�佷繚鐣欒〃澶达紝
@@ -3664,33 +3759,326 @@
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 鏉¤鍒欙級
+ dbProblems.addAll(missingDataCheck.check(effPeriod)); // 缂烘暟娓呭崟锛堣鏈夌殑鏁版病鏈夛級
reportProgress(progress, 22, "鍥炲~褰撴湀鏁版嵁");
int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, effPeriod, year, effMonth, dbProblems);
int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth);
int monthFormulaFilled = summaryWorkbookFiller.ensureMonthFormulas(wb, year, effMonth);
// 绱鍚屾瘮鍒嗘瘝淇锛氭瘝鐗堟妸銆屽幓骞村悇鏈堛�嶄笌銆屽幓骞�1-M鏈堢疮璁°�嶈緟鍔╁垪鍚屾椂璁″叆锛堥噸澶嶈/婕忓綋鏈堬級
int cumYoyFixed = summaryWorkbookFiller.repairCumulativeYoyFormulas(wb, year, effMonth);
+ // 2026-10-13锛氱疮璁″垪 / 绱鍚屾瘮鍒嗘瘝鎸夎〃澶撮噸寤猴紙鏍规不鎵╁垪鍣ㄦ棭鏈熲�滅疮璁℃紡鏈堚�濈殑鍘嗗彶娆犺处锛�
+ int cumRebuilt = summaryWorkbookFiller.repairCumulativeFormulas(wb, year, effMonth);
reportProgress(progress, 45, "鍒锋柊鎺掑悕椤典笌璺ㄩ〉鍚堟垚椤�");
- int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month");
+ int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month", dbProblems);
int cumulativeRefreshed = refreshSummaryCumulativeSheets(wb, year, effMonth); // 鍥涘紶璺ㄩ〉鍚堟垚椤甸殢鎶ヨ〃鏈熷埛鏂�
- int widthApplied = applySummaryReferenceLayout(wb);
+ // 2026-10-12锛氬仠鐢ㄣ�屽鐢ㄥ閮ㄥ弬鐓т欢銆婄敓鎴恄閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx銆嬪垪瀹姐�嶁�斺�旂増寮忎竴寰嬩互褰撴湡姣嶇増涓哄噯锛�
+ // 閬垮厤姣嶇増涓庡弬鐓т欢涓ゅ缁存姢锛堟瘝鐗堝凡骞跺叆鍙傜収浠剁殑鍒楀/闅愯棌锛夈��
+ int widthApplied = 0;
log.info("姹囨�诲伐浣滅翱鏁版嵁椹卞姩鍥炲~瀹屾垚锛氬啓鍏ュ綋鏈堝�兼牸鏁�={}锛岃ˉ鍐欏綋鏈堝悓姣斿叕寮忔牸鏁�={}锛岃ˉ鍐欏綋鏈堝悎璁″叕寮忔牸鏁�={}锛�"
- + "淇绱鍚屾瘮鍒嗘瘝鏍兼暟={}锛屽埛鏂版帓鍚嶉〉鏁板�兼牸鏁�={}锛屽埛鏂板洓椤碉紙鍩庡競姹囨��/鍩庡競鍒嗗競宸�/涓彛寰勫垎鏋�/閬撹矾杩愯緭鍛ㄨ浆閲忥級鏁板�兼牸鏁�={}锛�"
+ + "淇绱鍚屾瘮鍒嗘瘝鏍兼暟={}锛岄噸寤虹疮璁�/绱鍚屾瘮鏍兼暟={}锛屽埛鏂版帓鍚嶉〉鏁板�兼牸鏁�={}锛�"
+ + "鍒锋柊鍥涢〉锛堝煄甯傛眹鎬�/鍩庡競鍒嗗競宸�/涓彛寰勫垎鏋�/閬撹矾杩愯緭鍛ㄨ浆閲忥級鏁板�兼牸鏁�={}锛�"
+ "鎸夈�婄敓鎴恄閬撹矾杩愯緭閲忔眹鎬昏〃銆嬪榻愬垪瀹藉垪鏁�={}锛岀己鏁版彁绀�={}",
- dbFilled, yoyFilled, monthFormulaFilled, cumYoyFixed, rankRefreshed, cumulativeRefreshed, widthApplied, dbProblems);
+ dbFilled, yoyFilled, monthFormulaFilled, cumYoyFixed, cumRebuilt, rankRefreshed, cumulativeRefreshed, widthApplied, dbProblems);
applyTwoDecimalFormat(wb); // 鏁板�兼樉绀轰袱浣嶅皬鏁帮紙淇濈暀鍏ㄧ簿搴︼級锛�% / 鏃ユ湡绛夋棦鏈夋牱寮忎笉鏀瑰彉
int inlineFixed = normalizeStaleInlineStrings(wb); // 娓呮帀銆岄潪 inlineStr 鍗存畫鐣� <is>銆嶇殑闈炴硶鏍硷紝鍚﹀垯 Excel 鎷掔粷鎵撳紑
if (inlineFixed > 0) log.warn("姹囨�诲伐浣滅翱娓呮帀娈嬬暀 inlineStr 闈炴硶鏍� {} 涓紙姣嶇増鐢� openpyxl 鍐欏嚭鏃剁殑鍏稿瀷闂锛�", inlineFixed);
reportProgress(progress, 70, "閲嶇畻鏁存湰鍏紡锛堢害 4 涓囨牸锛�");
int[] recalc = recalcWorkbookFormulas(wb); // 鍏紡閲嶇畻骞跺啓鍥炵紦瀛樺�硷細鍏ㄧ渷琛�(危甯傚窞)/绱鍒�/鎺掑悕 SUM 鎵嶈兘琚瑙堜笌浜屾澶勭悊璇诲埌
+ setSheetViewsToCurrentMonth(wb, year, effMonth); // 鎵撳紑鏃跺畾浣嶅埌鏈湡鏁版嵁鍒楋紙涓嶅奖鍝嶆暟鎹笌鏍煎紡锛�
log.info("姹囨�诲伐浣滅翱鍏紡閲嶇畻瀹屾垚锛氭垚鍔� {} 鏍硷紝澶辫触 {} 鏍硷紙澶辫触鏍间繚鐣欐瘝鐗堟棫缂撳瓨鍊硷紝鎵撳紑鏃舵寜 fullCalcOnLoad 閲嶇畻锛�", recalc[0], recalc[1]);
if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 杩樺師/鏀瑰啓鍏紡鍚庢墦寮�鍗抽噸绠�
+ appendMissingDataSheet(wb, effPeriod, dbProblems); // 2026-10-12锛氱己鏁�/浣撴缁撴灉鍐欒繘鐢熸垚浠讹紙鐢ㄦ埛瑕佹眰锛�
reportProgress(progress, 95, "鍐欏嚭鏂囦欢");
- return toBytes(wb);
+ return normalizeSheetViews(toBytes(wb), wb, year, effMonth);
} finally {
ZipSecureFile.setMinInflateRatio(savedZipRatio);
}
+ }
+
+ /**
+ * 鎵撳紑宸ヤ綔绨挎椂鐨勫畾浣嶏紙2026-10-13锛夛細鎶婃瘡涓〉绛剧殑婊氬姩/閫変腑浣嶇疆褰掍竴鍒般�屾湰鏈熸暟鎹垪銆嶏紝
+ * 骞舵竻鎺夋瘝鐗堬紙琚� Excel/WPS 瀛樿繃锛夋畫鐣欑殑鏃� topLeftCell/activeCell 鈥斺�� 鍚﹀垯姣忎釜椤电浼氬仠鍦�
+ * 涓婃缂栬緫鏃剁殑濂囨�綅缃紙瀹炴祴姣嶇増閲� 鍩庡競瀹㈣繍 BQ38銆佷腑鍙e緞鏄庣粏 CC103銆佽揣杩愭帓鍚� A40鈥︼級銆�
+ * 鍐荤粨鎷嗗垎锛坸Split/ySplit/state锛夊師鏍蜂繚鐣欙紝鍙妸閿氱偣鎸埌鏈湡鏈堜唤鍒椼��
+ */
+ private byte[] normalizeSheetViews(byte[] xlsx, XSSFWorkbook wb, int year, int month) {
+ try {
+ java.util.Map<String, String> targets = sheetViewTargets(wb, year, month);
+ if (targets.isEmpty()) return xlsx;
+ java.util.Map<String, byte[]> entries = new java.util.LinkedHashMap<>();
+ try (java.util.zip.ZipInputStream zin = new java.util.zip.ZipInputStream(new java.io.ByteArrayInputStream(xlsx))) {
+ java.util.zip.ZipEntry e;
+ byte[] buf = new byte[8192];
+ while ((e = zin.getNextEntry()) != null) {
+ java.io.ByteArrayOutputStream b = new java.io.ByteArrayOutputStream();
+ int n;
+ while ((n = zin.read(buf)) > 0) b.write(buf, 0, n);
+ entries.put(e.getName(), b.toByteArray());
+ }
+ }
+ byte[] wbx = entries.get("xl/workbook.xml");
+ byte[] rlx = entries.get("xl/_rels/workbook.xml.rels");
+ if (wbx == null || rlx == null) return xlsx;
+ String wbs = new String(wbx, java.nio.charset.StandardCharsets.UTF_8);
+ String rls = new String(rlx, java.nio.charset.StandardCharsets.UTF_8);
+ java.util.Map<String, String> rid2t = new java.util.HashMap<>();
+ java.util.regex.Matcher rm = java.util.regex.Pattern.compile("Id=\"([^\"]+)\"[^>]*Target=\"([^\"]+)\"").matcher(rls);
+ while (rm.find()) rid2t.put(rm.group(1), rm.group(2));
+ java.util.Map<String, String> name2part = new java.util.LinkedHashMap<>();
+ java.util.regex.Matcher sm = java.util.regex.Pattern.compile("<sheet[^>]*name=\"([^\"]+)\"[^>]*r:id=\"([^\"]+)\"").matcher(wbs);
+ while (sm.find()) {
+ String t = rid2t.get(sm.group(2));
+ if (t == null) continue;
+ if (t.startsWith("/")) t = t.substring(1);
+ else if (!t.startsWith("xl/")) t = "xl/" + t;
+ name2part.put(sm.group(1), t);
+ }
+ int patched = 0;
+ for (java.util.Map.Entry<String, String> en : name2part.entrySet()) {
+ String target = targets.get(en.getKey());
+ byte[] part = entries.get(en.getValue());
+ if (target == null || part == null) continue;
+ String xml = new String(part, java.nio.charset.StandardCharsets.UTF_8);
+ String np = patchSheetView(xml, target);
+ if (!np.equals(xml)) { entries.put(en.getValue(), np.getBytes(java.nio.charset.StandardCharsets.UTF_8)); patched++; }
+ }
+ try (java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream(xlsx.length);
+ java.util.zip.ZipOutputStream zout = new java.util.zip.ZipOutputStream(bos)) {
+ for (java.util.Map.Entry<String, byte[]> en : entries.entrySet()) {
+ zout.putNextEntry(new java.util.zip.ZipEntry(en.getKey()));
+ zout.write(en.getValue());
+ zout.closeEntry();
+ }
+ zout.finish();
+ log.info("姹囨�诲伐浣滅翱锛氶〉绛炬墦寮�浣嶇疆宸插綊涓�鍒版湰鏈燂紙{}骞磠}鏈堬級锛屽鐞� {} 涓〉绛�", year, month, patched);
+ return bos.toByteArray();
+ }
+ } catch (Exception ex) {
+ log.warn("姹囨�诲伐浣滅翱锛氶〉绛炬墦寮�浣嶇疆褰掍竴澶辫触锛堝拷鐣ワ紝淇濈暀鍘熸牱锛夛細{}", ex.toString());
+ return xlsx;
+ }
+ }
+
+ /** 姣忛〉鐨勫畾浣嶇洰鏍囷細鏈夋湰鏈熸湀浠藉垪 鈫� 璇ユ湀琛ㄥご鏍硷紱娌℃湁锛堟帓鍚�/鍚堟垚椤碉級鈫� A1锛堥『甯︽竻鎺夋畫鐣欎綅缃級 */
+ private java.util.Map<String, String> sheetViewTargets(XSSFWorkbook wb, int year, int month) {
+ java.util.Map<String, String> m = new java.util.LinkedHashMap<>();
+ for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+ XSSFSheet sh = wb.getSheetAt(i);
+ if (sh == null) continue;
+ int headerRow = -1, col = -1;
+ int scan = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + 4);
+ for (int r = sh.getFirstRowNum(); r <= scan && col < 0; r++) {
+ Row row = sh.getRow(r);
+ if (row == null) continue;
+ for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) {
+ Integer ym = smYearMonthOf(row.getCell(c));
+ if (ym != null && ym / 100 == year && ym % 100 == month) { headerRow = r; col = c; break; }
+ }
+ }
+ m.put(sh.getSheetName(), col >= 0 ? a1Ref(headerRow, col) : "A1");
+ }
+ return m;
+ }
+
+ /** 0 鍩鸿鍒� -> "S3" */
+ private String a1Ref(int row0, int col0) {
+ StringBuilder sb = new StringBuilder();
+ int n = col0 + 1;
+ while (n > 0) { int rem = (n - 1) % 26; sb.insert(0, (char) ('A' + rem)); n = (n - 1) / 26; }
+ return sb.toString() + (row0 + 1);
+ }
+
+ /** 鍘绘帀灞炴�� */
+ private String stripAttr(String attrs, String name) {
+ int i = attrs.indexOf(name + "=\"");
+ if (i < 0) return attrs;
+ int e = attrs.indexOf('"', i + name.length() + 2);
+ if (e < 0) return attrs;
+ return attrs.substring(0, i) + attrs.substring(e + 1);
+ }
+
+ /** 鎵炬爣绛捐捣鐐癸細瑕佹眰鏍囩鍚嶅悗闈㈢揣璺� 绌烘牸/>锛岄伩鍏� "<sheetView" 鍛戒腑 "<sheetViews" */
+ private int findTag(String xml, String tag) {
+ int from = 0;
+ while (true) {
+ int i = xml.indexOf(tag, from);
+ if (i < 0) return -1;
+ int nx = i + tag.length();
+ char ch = nx < xml.length() ? xml.charAt(nx) : '>';
+ if (ch == ' ' || ch == '>' || ch == '/') return i;
+ from = i + 1;
+ }
+ }
+
+ /** 鍙栧睘鎬у�硷紙涓嶅瓨鍦ㄨ繑鍥� null锛� */
+ private String attrValue(String attrs, String name) {
+ int i = attrs.indexOf(name + "=\"");
+ if (i < 0) return null;
+ int e = attrs.indexOf('"', i + name.length() + 2);
+ return e < 0 ? null : attrs.substring(i + name.length() + 2, e);
+ }
+
+ /** 杩藉姞灞炴�э紙宸插瓨鍦ㄥ垯涓嶉噸澶嶅姞锛� */
+ private String addAttr(String attrs, String name, String v) {
+ if (attrValue(attrs, name) != null) return attrs;
+ String sep = (attrs.isEmpty() || attrs.endsWith(" ")) ? "" : " ";
+ return attrs + sep + name + "=\"" + v + "\"";
+ }
+
+ /**
+ * 鍗曢〉 XML 褰掍竴锛歴heetView / pane 鐨� topLeftCell銆佹椿鍔� pane 鐨� selection 鍏ㄩ儴鎸囧悜 target銆�
+ * 鍐荤粨鎷嗗垎锛坸Split/ySplit/state锛夊師鏍蜂繚鐣欙紱鐮村潖缁撴瀯鍒欒繑鍥炲師鏂囷紙failed-safe锛夈��
+ */
+ private String patchSheetView(String xml, String target) {
+ String orig = xml;
+ int i = findTag(xml, "<sheetView");
+ if (i >= 0) {
+ int j = xml.indexOf('>', i);
+ if (j > 0) {
+ String attrs = addAttr(stripAttr(xml.substring(i + 10, j), "topLeftCell"), "topLeftCell", target);
+ xml = xml.substring(0, i) + "<sheetView" + attrs + ">" + xml.substring(j + 1);
+ }
+ }
+ String activePane = null;
+ int p = findTag(xml, "<pane");
+ if (p >= 0) {
+ int q = xml.indexOf("/>", p);
+ if (q > 0) {
+ String raw = xml.substring(p + 5, q);
+ activePane = attrValue(raw, "activePane");
+ String attrs = addAttr(stripAttr(raw, "topLeftCell"), "topLeftCell", target);
+ xml = xml.substring(0, p) + "<pane" + attrs + "/>" + xml.substring(q + 2);
+ }
+ }
+ StringBuilder sb = new StringBuilder();
+ int last = 0, pos = 0;
+ boolean done = false, any = false;
+ while (true) {
+ int s0 = xml.indexOf("<selection", pos);
+ if (s0 < 0) break;
+ int s1 = xml.indexOf("/>", s0);
+ if (s1 < 0) break;
+ String raw = xml.substring(s0 + 10, s1);
+ String attrs = stripAttr(stripAttr(raw, "activeCell"), "sqref");
+ String pane = attrValue(raw, "pane");
+ boolean hit = (activePane != null) ? (pane != null && pane.equals(activePane)) : !done;
+ if (hit) { attrs = addAttr(addAttr(attrs, "activeCell", target), "sqref", target); done = true; }
+ sb.append(xml, last, s0).append("<selection").append(attrs).append("/>");
+ last = s1 + 2;
+ pos = s1 + 2;
+ any = true;
+ }
+ if (!any) {
+ int close = xml.indexOf("</sheetView>");
+ if (close > 0) xml = xml.substring(0, close) + "<selection activeCell=\"" + target + "\" sqref=\"" + target + "\"/>" + xml.substring(close);
+ } else {
+ sb.append(xml, last, xml.length());
+ xml = sb.toString();
+ }
+ if (!xml.contains("<sheetViews>") || !xml.contains("</sheetViews>")) return orig;
+ return xml;
+ }
+
+ /**
+ * 鐢熸垚浠躲�婃暟鎹己澶辨竻鍗曘�嬮〉锛�2026-10-12锛岀敤鎴疯姹傦級銆�
+ *
+ * <p>鎶婃湰娆″鍑虹殑銆岀己鏁版竻鍗曘�嶄笌銆岃揣杩愬熀鏁颁綋妫�銆嶇粨鏋滄樉寮忓啓杩涗骇鐗╋細鐢ㄦ埛鎷垮埌鏂囦欢鍗冲彲鐪嬪埌鏈姤琛ㄦ湡
+ * 鏈夊摢浜涙暟娌″埌銆佸摢浜涙暟涓嶅悎鐞嗭紝涓嶅繀缈诲鍑烘棩蹇椼�傚彧璇绘彁绀洪〉锛屼笉鍙備笌鏈〃璁$畻銆�
+ */
+ private void appendMissingDataSheet(XSSFWorkbook wb, String period, List<String> problems) {
+ String sheetName = "鏁版嵁缂哄け娓呭崟";
+ int old = wb.getSheetIndex(sheetName);
+ if (old >= 0) wb.removeSheetAt(old); // 姣嶇増鐢便�屽綋鏈熶骇鐗┿�嶆淳鐢熸椂鍙兘宸插甫鏈〉锛屽厛绉婚櫎閬垮厤閲嶅悕
+ XSSFSheet sh = wb.createSheet(sheetName);
+
+ CellStyle titleStyle = wb.createCellStyle();
+ Font titleFont = wb.createFont();
+ titleFont.setBold(true);
+ titleFont.setFontHeightInPoints((short) 14);
+ titleStyle.setFont(titleFont);
+
+ CellStyle metaStyle = wb.createCellStyle();
+ Font metaFont = wb.createFont();
+ metaFont.setFontHeightInPoints((short) 10);
+ metaFont.setColor(IndexedColors.GREY_50_PERCENT.getIndex());
+ metaStyle.setFont(metaFont);
+
+ CellStyle sectionStyle = wb.createCellStyle();
+ Font sectionFont = wb.createFont();
+ sectionFont.setBold(true);
+ sectionStyle.setFont(sectionFont);
+
+ CellStyle bodyStyle = wb.createCellStyle();
+ bodyStyle.setWrapText(true);
+ bodyStyle.setVerticalAlignment(VerticalAlignment.TOP);
+
+ List<String> missing = new ArrayList<>();
+ List<String> check = new ArrayList<>();
+ List<String> other = new ArrayList<>();
+ if (problems != null) {
+ for (String p : problems) {
+ if (p == null || p.trim().isEmpty()) continue;
+ if (p.startsWith("缂烘暟")) missing.add(p);
+ else if (p.startsWith("璐ц繍浣撴")) check.add(p);
+ else other.add(p);
+ }
+ }
+
+ int r = 0;
+ Row tRow = sh.createRow(r++);
+ Cell tCell = tRow.createCell(0);
+ tCell.setCellValue("鏁版嵁缂哄け娓呭崟");
+ tCell.setCellStyle(titleStyle);
+
+ Row mRow = sh.createRow(r++);
+ Cell mCell = mRow.createCell(0);
+ mCell.setCellValue("鎶ヨ〃鏈� " + period + "銆�鐢熸垚鏃堕棿 "
+ + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm").format(new java.util.Date())
+ + "銆�鍏� " + (missing.size() + check.size() + other.size()) + " 鏉�");
+ mCell.setCellStyle(metaStyle);
+
+ Row nRow = sh.createRow(r++);
+ Cell nCell = nRow.createCell(0);
+ nCell.setCellValue("鏈〉鐢辩▼搴忚嚜鍔ㄧ敓鎴愶紙鍙鎻愮ず锛夛細涓�銆佺己鏁版竻鍗曪紳璇ユ湁鐨勬暟娌℃湁锛涗簩銆佹暟鎹綋妫�锛濇暟鍊间笉鍚堢悊锛堝瑙勪笅涓鸿礋锛夈�備笉鍙備笌鏈〃璁$畻銆�");
+ nCell.setCellStyle(metaStyle);
+
+ r++;
+ r = writeProblemSection(sh, r, "涓�銆佺己鏁版竻鍗曪紙璇ユ湁鐨勬暟娌℃湁锛�", missing, sectionStyle, bodyStyle);
+ r = writeProblemSection(sh, r, "浜屻�佹暟鎹綋妫�锛堟暟鍊间笉鍚堢悊锛�", check, sectionStyle, bodyStyle);
+ if (!other.isEmpty()) {
+ r = writeProblemSection(sh, r, "涓夈�佸叾瀹冩彁绀�", other, sectionStyle, bodyStyle);
+ }
+
+ sh.setColumnWidth(0, 6 * 256);
+ sh.setColumnWidth(1, 120 * 256);
+ sh.createFreezePane(0, 3);
+ }
+
+ /** 鍐欎竴涓竻鍗曞垎鍖猴紙鏍囬 + 閫愭潯锛夛紝杩斿洖涓嬩竴涓彲鍐欒鍙� */
+ private int writeProblemSection(XSSFSheet sh, int rowIdx, String title, List<String> lines,
+ CellStyle sectionStyle, CellStyle bodyStyle) {
+ Row hr = sh.createRow(rowIdx++);
+ Cell hc = hr.createCell(0);
+ hc.setCellValue(title);
+ hc.setCellStyle(sectionStyle);
+ sh.addMergedRegion(new CellRangeAddress(rowIdx - 1, rowIdx - 1, 0, 1));
+ if (lines.isEmpty()) {
+ Row er = sh.createRow(rowIdx++);
+ er.createCell(0).setCellValue("鈥�");
+ er.createCell(1).setCellValue("鏃�");
+ return rowIdx;
+ }
+ int i = 1;
+ for (String line : lines) {
+ Row row = sh.createRow(rowIdx++);
+ row.createCell(0).setCellValue(i++);
+ Cell c = row.createCell(1);
+ c.setCellValue(line);
+ c.setCellStyle(bodyStyle);
+ }
+ return rowIdx;
}
/** 瀵煎嚭杩涘害鍥炶皟锛堝紓姝ュ鍑轰换鍔$敤锛夛細鍥炶皟鑷韩寮傚父涓嶅奖鍝嶅鍑� */
@@ -3862,11 +4250,11 @@
* 鐨勭粨鏋滃埛鏂版暟鍊硷紝閬垮厤姹囨�昏〃閲岀殑鎺掑悕椤靛仠鐣欏湪鏃ф湀浠姐��
* 鏁板�兼牸鎼�笺�佸叕寮忔牸鎸夊亸绉诲钩绉诲叕寮忓悗鍐欏叆锛涚洰鏍囨牸鏈韩宸叉槸鍏紡鐨勶紙SUM/RANK/鍗犳瘮/琛ユ暟锛変繚鐣欎笉鍔紝鎵撳紑鏃舵寜鏂版暟鎹噸绠椼��
*/
- private int refreshSummaryRankSheets(XSSFWorkbook wb, String period, String mode) throws Exception {
+ private int refreshSummaryRankSheets(XSSFWorkbook wb, String period, String mode, List<String> problems) throws Exception {
int n = 0;
- n += copyRankValues(wb, "璐ц繍閲忔帓鍚�", exportFreightRank(period, mode), new int[][]{{0, 0}, {22, 0}});
- n += copyRankValues(wb, "璐ц繍鍛ㄨ浆閲忔帓鍚�", exportTurnoverRank(period, mode), new int[][]{{0, 0}, {22, 0}});
- n += copyRankValues(wb, "涓彛寰勬帓鍚�", exportPassengerMidRank(period, mode, wb), new int[][]{{0, 0}, {0, 10}});
+ n += copyRankValues(wb, "璐ц繍閲忔帓鍚�", exportFreightRank(period, mode), new int[][]{{0, 0}, {22, 0}}, problems);
+ n += copyRankValues(wb, "璐ц繍鍛ㄨ浆閲忔帓鍚�", exportTurnoverRank(period, mode), new int[][]{{0, 0}, {22, 0}}, problems);
+ n += copyRankValues(wb, "涓彛寰勬帓鍚�", exportPassengerMidRank(period, mode, wb), new int[][]{{0, 0}, {0, 10}}, problems);
return n;
}
@@ -3875,13 +4263,15 @@
* srcBlocks[i] = 婧愬潡 i 鐨勮捣濮� {琛�, 鍒梷锛�0 鍩猴級锛涚洰鏍囧潡鎸夋爣棰橀『搴忥紙绱鍧楀湪鍓嶃�佸綋鏈堝潡鍦ㄥ悗锛夊搴斻��
* 鏁板�兼牸鐩存帴鎼�硷紱婧愪晶鏄叕寮忕殑锛堟帓鍚�/鍗犳瘮绛夛級鎸夌洰鏍囧潡鍋忕Щ鏁翠綋骞崇Щ鍚庡啓鍏ュ叕寮忥紝鎵撳紑鏃舵寜姹囨�昏〃鑷韩鏁版嵁閲嶇畻銆�
*/
- private int copyRankValues(XSSFWorkbook dstWb, String dstSheetName, byte[] srcBytes, int[][] srcBlocks) throws Exception {
+ private int copyRankValues(XSSFWorkbook dstWb, String dstSheetName, byte[] srcBytes, int[][] srcBlocks,
+ List<String> problems) throws Exception {
XSSFSheet dst = dstWb.getSheet(dstSheetName);
if (dst == null) return 0;
List<XSSFCell> dstTitles = rankTitleCells(dst);
if (dstTitles.isEmpty()) return 0;
int copied = 0;
FormulaEvaluator dstEv = dstWb.getCreationHelper().createFormulaEvaluator();
+ List<int[]> anchors = new ArrayList<>(); // {婧愬叏鐪佽, 鐩爣鍏ㄧ渷琛�, 鍧楅鍒�, 鍒楀亸绉�, 鍧楀}
try (InputStream sin = new ByteArrayInputStream(srcBytes);
XSSFWorkbook srcWb = new XSSFWorkbook(sin)) {
XSSFSheet src = srcWb.getSheetAt(0);
@@ -3891,19 +4281,59 @@
int sr0 = srcBlocks[b][0];
int sc0 = srcBlocks[b][1];
XSSFCell dstTitle = dstTitles.get(b);
- int rowOff = dstTitle.getRowIndex() - sr0;
int colOff = dstTitle.getColumnIndex() - sc0;
+ int rowOff = dstTitle.getRowIndex() - sr0;
+ // 2026-10-13锛氥�婅揣杩愬懆杞噺鎺掑悕銆嬫瘝鐗堝潡鏍囬鏇惧崰涓よ锛堝悎骞讹級锛�"鏍囬琛�+鍥哄畾鍋忕Щ"浼氭妸婧愯
+ // 鏁翠綋鍐欓敊浣嶄竴琛岋紙鍏ㄧ渷鍊艰惤鍒拌〃澶磋銆佸競宸炲�间笌甯傚窞鍚嶉敊寮�锛夈�傛敼涓虹敤銆屽叏鐪併�嶈鍋氶敋鐐瑰榻愶細
+ // rowOff = 鐩爣鍏ㄧ渷琛� - 婧愬叏鐪佽锛涜瘑鍒笉鍒伴敋鐐瑰啀鍥為��鏍囬琛屽亸绉诲苟鍛婅銆�
+ Integer srcAnchor = rankAnchorRow(src, sr0, sc0);
+ Integer dstAnchor = rankAnchorRow(dst, dstTitle.getRowIndex(), dstTitle.getColumnIndex());
+ if (srcAnchor != null && dstAnchor != null) {
+ int anchored = dstAnchor - srcAnchor;
+ if (anchored != rowOff) {
+ log.info("鎺掑悕椤点�妠}銆嬬 {} 鍧楋細鏍囬琛屽亸绉� {} 涓庛�屽叏鐪併�嶉敋鐐瑰亸绉� {} 涓嶄竴鑷达紝鎸夐敋鐐瑰榻愶紙婧愬叏鐪佽={}锛岀洰鏍囧叏鐪佽={}锛�",
+ dstSheetName, b + 1, rowOff, anchored, srcAnchor + 1, dstAnchor + 1);
+ }
+ rowOff = anchored;
+ } else {
+ log.warn("鎺掑悕椤点�妠}銆嬬 {} 鍧楋細鏈瘑鍒埌銆屽叏鐪併�嶉敋鐐癸紙婧�={}锛岀洰鏍�={}锛夛紝鍥為��鎸夋爣棰樿鍋忕Щ {} 瀵归綈",
+ dstSheetName, b + 1, srcAnchor, dstAnchor, rowOff);
+ }
int h = rankBlockHeight(src, sr0, sc0);
int w = rankBlockWidth(src, sr0, sc0);
+ anchors.add(new int[]{srcAnchor == null ? -1 : srcAnchor, dstAnchor == null ? -1 : dstAnchor, sc0, colOff, w});
+ // 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 娓呮帀鍐嶅啓銆�
@@ -3938,13 +4368,46 @@
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++;
}
}
}
+ // 2026-10-13 鑷锛氱洰鏍囥�屽叏鐪併�嶈搴斾笌婧愪晶涓�鑷达紙鍋忕Щ閿欎綅銆佹眹鎬诲尯闂翠笉瀵规椂鎶ヨ骞跺啓杩涚己鏁版竻鍗曪級
+ for (int[] an : anchors) {
+ if (an[0] < 0 || an[1] < 0) continue;
+ Row srr = src.getRow(an[0]);
+ Row drr = dst.getRow(an[1]);
+ if (srr == null || drr == null) continue;
+ int badCols = 0;
+ StringBuilder ex = new StringBuilder();
+ for (int c = 0; c < an[4]; c++) {
+ Cell sCell = srr.getCell(an[2] + c);
+ Cell dCell = drr.getCell(an[2] + c + an[3]);
+ Double sv = sCell == null ? null : rankNumeric(sCell, ev);
+ Double dv = dCell == null ? null : rankNumeric(dCell, dstEv);
+ if (sv == null && dv == null) continue;
+ if (sv == null || dv == null || Math.abs(sv - dv) > Math.max(1.0, Math.abs(sv) * 1e-4)) {
+ badCols++;
+ if (badCols <= 3) {
+ if (ex.length() > 0) ex.append("锛�");
+ ex.append(colLetter(an[2] + c)).append(" 婧�=").append(sv).append(" 鐩爣=").append(dv);
+ }
+ }
+ }
+ if (badCols >= 2) {
+ String msg = "鎺掑悕椤点��" + dstSheetName + "銆嬨�屽叏鐪併�嶈涓庢簮渚т笉涓�鑷达紙" + badCols + " 鍒楋級锛�" + ex;
+ log.warn(msg);
+ if (problems != null) problems.add("鐢熸垚鏍¢獙锛�" + msg);
+ }
+ }
}
return copied;
}
@@ -3955,6 +4418,34 @@
Cell left = col > 0 ? row.getCell(col - 1) : null;
if (left != null) c.setCellStyle(left.getCellStyle());
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 绛夊嚱鏁板悕鍚庢帴鎷彿涓嶄細琚尮閰嶏級 */
@@ -3983,6 +4474,23 @@
}
/** 鎺掑悕椤甸噷鐨勨�滃潡鏍囬鈥濆崟鍏冩牸锛堝惈鈥滃叏鐪佸垎甯傚窞鈥濈殑璇存槑鏂囧瓧锛夛紝鎸夎銆佸垪椤哄簭杩斿洖 */
+ /** 鍦� [fromRow, fromRow+40) 鑼冨洿鍐呫�佸潡璧峰涓ゅ垪閲屾壘鏁存牸鏂囨湰涓恒�屽叏鐪併�嶇殑琛屽彿锛�0 鍩猴級锛涙壘涓嶅埌杩斿洖 null銆�
+ * 2026-10-13锛氭帓鍚嶉〉鍧楃粨鏋勫彲鑳藉洜姣嶇増鐗堝紡涓嶅悓鑰屾暣浣撲笂/涓嬬Щ涓�琛岋紝鐢ㄣ�屽叏鐪併�嶈鍋氶敋鐐瑰彲鑷姩瀵归綈銆� */
+ private Integer rankAnchorRow(Sheet sh, int fromRow, int col0) {
+ int last = Math.min(sh.getLastRowNum(), fromRow + 40);
+ for (int r = fromRow; r <= last; r++) {
+ Row row = sh.getRow(r);
+ if (row == null) continue;
+ for (int c = Math.max(0, col0); c <= col0 + 1; c++) {
+ Cell cell = row.getCell(c);
+ if (cell == null || cell.getCellType() != CellType.STRING) continue;
+ String v = cell.getStringCellValue();
+ if (v != null && v.replace(" ", "").equals("鍏ㄧ渷")) return r;
+ }
+ }
+ return null;
+ }
+
private List<XSSFCell> rankTitleCells(Sheet sh) {
List<XSSFCell> out = new ArrayList<>();
for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
@@ -4085,9 +4593,8 @@
private static final int SM_WYC_CX_PAX = 16, SM_WYC_CX_TURN = 17;
private static final int SM_FREIGHT_TURN = 18;
private static final int SM_DIM = 19;
- /** 2026 骞� 1 鏈堝綋鏈堝�煎垪锛�0 鍩猴紝C 鍒楋級锛�2025 骞� 1 鏈堝綋鏈堝�煎垪锛�0 鍩猴紝V 鍒楋級 */
- private static final int SM_CUR_COL0 = 2;
- private static final int SM_LAST_COL0 = 21;
+ // 2026-10-09锛氬師鍏堝啓姝荤殑 SM_CUR_COL0=2锛�2026骞�1鏈�=C锛�/ SM_LAST_COL0=21锛�2025骞�1鏈�=V锛夊凡搴熷純锛�
+ // 鏀逛负鎸夎〃澶村姩鎬佸畾浣嶏紙瑙� smMonthCols / smYearMonthOf锛夛紝鍏煎銆屾枃鏈� / 鏃ユ湡搴忓垪鍙� / 鏃ユ湡鏍煎紡銆嶄笁绉嶈〃澶村啓娉曘��
/**
* 鍒锋柊鍥涘紶璺ㄩ〉鍚堟垚椤碉細褰撳勾绱锛�1..month 鏈堬級銆佸綋鏈堬紙month 鏈堬級鍙婂搴斿悓姣旓紱
@@ -4098,10 +4605,10 @@
int[] cumMonths = new int[month];
for (int m = 1; m <= month; m++) cumMonths[m - 1] = m;
int[] oneMonth = new int[]{month};
- Map<String, double[]> curCum = smReadLedgerMetrics(wb, true, cumMonths);
- Map<String, double[]> lastCum = smReadLedgerMetrics(wb, false, cumMonths);
- Map<String, double[]> curMon = smReadLedgerMetrics(wb, true, oneMonth);
- Map<String, double[]> lastMon = smReadLedgerMetrics(wb, false, oneMonth);
+ Map<String, double[]> curCum = smReadLedgerMetrics(wb, year, cumMonths);
+ Map<String, double[]> lastCum = smReadLedgerMetrics(wb, year - 1, cumMonths);
+ Map<String, double[]> curMon = smReadLedgerMetrics(wb, year, oneMonth);
+ Map<String, double[]> lastMon = smReadLedgerMetrics(wb, year - 1, oneMonth);
if (curCum.isEmpty()) {
log.warn("姹囨�诲ぇ琛ㄥ洓椤靛埛鏂帮細鍙拌处椤垫湭璇诲埌甯傚窞鏁版嵁锛岃烦杩囷紙淇濇寔姣嶇増鍘熷�硷級");
return 0;
@@ -4130,22 +4637,23 @@
}
/** 鍙拌处椤电疮璁″彇鏁帮細甯傚窞瑙勮寖鍚� -> 19 缁存寚鏍� */
- private Map<String, double[]> smReadLedgerMetrics(XSSFWorkbook wb, boolean curYear, int[] months) {
+ private Map<String, double[]> smReadLedgerMetrics(XSSFWorkbook wb, int blockYear, int[] months) {
Map<String, double[]> out = new LinkedHashMap<>();
- smReadCityPaxPage(wb, "鍏氦", curYear, months, out, SM_BUS_PAX, SM_BUS_TURN, SM_BUS_CX_PAX, SM_BUS_CX_TURN);
- smReadCityPaxPage(wb, "鍑虹", curYear, months, out, SM_TAXI_PAX, SM_TAXI_TURN, SM_TAXI_CX_PAX, SM_TAXI_CX_TURN);
- smReadCityPaxPage(wb, "缃戠害杞�", curYear, months, out, SM_WYC_PAX, SM_WYC_TURN, SM_WYC_CX_PAX, SM_WYC_CX_TURN);
- smReadRailFerry(wb, curYear, months, out);
- smReadBanxian(wb, curYear, months, out);
- smReadFreightTurnover(wb, curYear, months, out);
+ smReadCityPaxPage(wb, "鍏氦", blockYear, months, out, SM_BUS_PAX, SM_BUS_TURN, SM_BUS_CX_PAX, SM_BUS_CX_TURN);
+ smReadCityPaxPage(wb, "鍑虹", blockYear, months, out, SM_TAXI_PAX, SM_TAXI_TURN, SM_TAXI_CX_PAX, SM_TAXI_CX_TURN);
+ smReadCityPaxPage(wb, "缃戠害杞�", blockYear, months, out, SM_WYC_PAX, SM_WYC_TURN, SM_WYC_CX_PAX, SM_WYC_CX_TURN);
+ smReadRailFerry(wb, blockYear, months, out);
+ smReadBanxian(wb, blockYear, months, out);
+ smReadFreightTurnover(wb, blockYear, months, out);
return out;
}
/** 鍏氦/鍑虹/缃戠害杞﹂〉锛氭瘡甯傚窞 4 琛岋紙瀹㈣繍閲�/鏃呭鍛ㄨ浆閲�/鍏朵腑鍩庡競鍐呭杩愰噺/鍏朵腑鍩庡競鍐呮梾瀹㈠懆杞噺锛� */
- private void smReadCityPaxPage(XSSFWorkbook wb, String sheetName, boolean curYear, int[] months,
+ private void smReadCityPaxPage(XSSFWorkbook wb, String sheetName, int blockYear, int[] months,
Map<String, double[]> out, int paxIdx, int turnIdx, int cxPaxIdx, int cxTurnIdx) {
Sheet sh = wb.getSheet(sheetName);
if (sh == null) return;
+ Map<Integer, Integer> monthCols = smMonthCols(sh, blockYear);
int last = sh.getLastRowNum();
for (int r = sh.getFirstRowNum(); r <= last; r++) {
Row row = sh.getRow(r);
@@ -4168,10 +4676,10 @@
else if (paxRow < 0) paxRow = k;
}
}
- double totalPax = smSumMonths(sh, paxRow, curYear, months);
- double totalTurn = smSumMonths(sh, turnRow, curYear, months);
- double cityPax = smSumMonths(sh, cxPaxRow, curYear, months);
- double cityTurn = smSumMonths(sh, cxTurnRow, curYear, months);
+ double totalPax = smSumMonths(sh, paxRow, monthCols, months);
+ double totalTurn = smSumMonths(sh, turnRow, monthCols, months);
+ double cityPax = smSumMonths(sh, cxPaxRow, monthCols, months);
+ double cityTurn = smSumMonths(sh, cxTurnRow, monthCols, months);
double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]);
arr[paxIdx] += cityPax; // 鍩庡競瀹㈣繍鍙e緞鍙栤�滃煄甯傚唴鈥�
arr[turnIdx] += cityTurn;
@@ -4181,9 +4689,10 @@
}
/** 杞ㄩ亾銆佽疆娓¢〉锛氳建閬撳潡锛堟姹�/榛勭煶锛�+ 杞浮鍧楋紙姝︽眽锛� */
- private void smReadRailFerry(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) {
+ private void smReadRailFerry(XSSFWorkbook wb, int blockYear, int[] months, Map<String, double[]> out) {
Sheet sh = wb.getSheet("杞ㄩ亾銆佽疆娓�");
if (sh == null) return;
+ Map<Integer, Integer> monthCols = smMonthCols(sh, blockYear);
int last = sh.getLastRowNum();
int ferryTitle = -1;
for (int r = sh.getFirstRowNum(); r <= last; r++) {
@@ -4192,12 +4701,12 @@
String a = smText(row.getCell(0));
if (a != null && a.contains("杞浮瀹㈣繍閲�")) { ferryTitle = r; break; }
}
- smReadRailFerryBlock(sh, curYear, months, out, sh.getFirstRowNum(),
+ smReadRailFerryBlock(sh, monthCols, months, out, sh.getFirstRowNum(),
ferryTitle < 0 ? last : ferryTitle - 1, SM_RAIL_PAX);
- if (ferryTitle >= 0) smReadRailFerryBlock(sh, curYear, months, out, ferryTitle, last, SM_FERRY_PAX);
+ if (ferryTitle >= 0) smReadRailFerryBlock(sh, monthCols, months, out, ferryTitle, last, SM_FERRY_PAX);
}
- private void smReadRailFerryBlock(Sheet sh, boolean curYear, int[] months, Map<String, double[]> out,
+ private void smReadRailFerryBlock(Sheet sh, Map<Integer, Integer> monthCols, int[] months, Map<String, double[]> out,
int start, int end, int baseIdx) {
for (int r = start; r <= end; r++) {
Row row = sh.getRow(r);
@@ -4214,15 +4723,16 @@
else if (paxRow < 0) paxRow = k;
}
double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]);
- arr[baseIdx] += smSumMonths(sh, paxRow, curYear, months);
- arr[baseIdx + 1] += smSumMonths(sh, turnRow, curYear, months);
+ arr[baseIdx] += smSumMonths(sh, paxRow, monthCols, months);
+ arr[baseIdx + 1] += smSumMonths(sh, turnRow, monthCols, months);
}
}
/** 鐝嚎鍖呰溅椤碉細姣忓競宸� 瀹㈣繍閲� / 鏃呭鍛ㄨ浆閲忥紙鈥滃叾涓釜浣撯�濊涓嶉噸澶嶈鍏ワ級 */
- private void smReadBanxian(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) {
+ private void smReadBanxian(XSSFWorkbook wb, int blockYear, int[] months, Map<String, double[]> out) {
Sheet sh = wb.getSheet("鐝嚎鍖呰溅");
if (sh == null) return;
+ Map<Integer, Integer> monthCols = smMonthCols(sh, blockYear);
int last = sh.getLastRowNum();
for (int r = sh.getFirstRowNum(); r <= last; r++) {
Row row = sh.getRow(r);
@@ -4239,16 +4749,17 @@
else if (b.contains("瀹㈣繍閲�") && paxRow < 0) paxRow = k;
}
double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]);
- arr[SM_BX_PAX] += smSumMonths(sh, paxRow, curYear, months);
- arr[SM_BX_TURN] += smSumMonths(sh, turnRow, curYear, months);
+ arr[SM_BX_PAX] += smSumMonths(sh, paxRow, monthCols, months);
+ arr[SM_BX_TURN] += smSumMonths(sh, turnRow, monthCols, months);
}
}
/** 璐ц繍椤碉細姣忓競宸� 瑙勪笂璐х墿鍛ㄨ浆閲� + 瑙勪笅璐х墿鍛ㄨ浆閲忥紙鈥滆涓�+瑙勪笅鈥濊鏈韩鏄眰鍜屽叕寮忥紝涓嶇洿鎺ュ彇锛� */
- private void smReadFreightTurnover(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) {
+ private void smReadFreightTurnover(XSSFWorkbook wb, int blockYear, int[] months, Map<String, double[]> out) {
Sheet sh = wb.getSheet(" 璐ц繍");
if (sh == null) sh = wb.getSheet("璐ц繍");
if (sh == null) return;
+ Map<Integer, Integer> monthCols = smMonthCols(sh, blockYear);
int last = sh.getLastRowNum();
for (int r = sh.getFirstRowNum(); r <= last; r++) {
Row row = sh.getRow(r);
@@ -4265,20 +4776,102 @@
else if (b.contains("瑙勪笅璐х墿鍛ㄨ浆閲�")) { if (below < 0) below = k; }
}
double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]);
- arr[SM_FREIGHT_TURN] += smSumMonths(sh, above, curYear, months) + smSumMonths(sh, below, curYear, months);
+ arr[SM_FREIGHT_TURN] += smSumMonths(sh, above, monthCols, months) + smSumMonths(sh, below, monthCols, months);
}
}
- private double smSumMonths(Sheet sh, int rowIdx, boolean curYear, int[] months) {
- if (sh == null || rowIdx < 0) return 0;
+ private double smSumMonths(Sheet sh, int rowIdx, Map<Integer, Integer> monthCols, int[] months) {
+ if (sh == null || rowIdx < 0 || monthCols == null) return 0;
Row row = sh.getRow(rowIdx);
if (row == null) return 0;
double s = 0;
for (int m : months) {
- int col = (curYear ? SM_CUR_COL0 : SM_LAST_COL0) + 2 * (m - 1);
+ Integer col = monthCols.get(m);
+ if (col == null) continue;
s += smNum(row.getCell(col));
}
return s;
+ }
+
+ /** 鍙拌处椤点�屾煇骞� M 鏈堛�嶇殑 0 鍩哄垪鍙凤細鎸夎〃澶村姩鎬佸畾浣嶏紙涓嶅啀鍐欐 C/V锛夛紝鍏煎鏂囨湰 / 鏃ユ湡搴忓垪鍙� / 鏃ユ湡鏍煎紡涓夌鍐欐硶 */
+ private Map<Integer, Integer> smMonthCols(Sheet sh, int wantedYear) {
+ Map<Integer, Integer> cols = new LinkedHashMap<>();
+ if (sh == null) return cols;
+ int lastRow = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + 4);
+ for (int r = sh.getFirstRowNum(); r <= lastRow; r++) {
+ Row row = sh.getRow(r);
+ if (row == null) continue;
+ int lastCol = row.getLastCellNum();
+ for (int c = row.getFirstCellNum(); c >= 0 && c < lastCol; c++) {
+ Integer ym = smYearMonthOf(row.getCell(c));
+ if (ym == null || ym / 100 != wantedYear) continue;
+ int mo = ym % 100;
+ if (!cols.containsKey(mo)) cols.put(mo, c);
+ }
+ }
+ if (cols.isEmpty()) {
+ log.warn("姹囨�诲ぇ琛ㄥ洓椤靛埛鏂帮細{} 椤垫湭璇嗗埆鍒� {} 骞寸殑鏈堜唤琛ㄥご鍒楋紙鏀寔鏂囨湰/搴忓垪鍙�/鏃ユ湡涓夌褰㈠紡锛�", sh.getSheetName(), wantedYear);
+ }
+ return cols;
+ }
+
+ /**
+ * 鎵撳紑宸ヤ綔绨挎椂鎶婃瘡涓〉绛惧畾浣嶅埌銆屾湰鏈熸暟鎹�嶄綅缃紙2026-10-13 鐢ㄦ埛瑕佹眰锛夈��
+ * 鍋氭硶锛氬嚒鑳芥寜琛ㄥご璇嗗埆鍑恒�屽綋骞村綋鏈堝�煎垪銆嶇殑椤碉紝鎶婃椿鍔ㄥ崟鍏冩牸璁惧埌璇ユ湀琛ㄥご鏍硷紱鍏朵綑椤典繚鎸侀粯璁ゃ��
+ * 鍙奖鍝嶆墦寮�鏃剁殑婊氬姩/閫変腑浣嶇疆锛屼笉鏀逛换浣曟暟鎹笌鏍煎紡銆�
+ */
+ private int setSheetViewsToCurrentMonth(XSSFWorkbook wb, int year, int month) {
+ int n = 0;
+ for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+ XSSFSheet sh = wb.getSheetAt(i);
+ if (sh == null) continue;
+ int headerRow = -1, col = -1;
+ int scan = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + 4);
+ for (int r = sh.getFirstRowNum(); r <= scan && col < 0; r++) {
+ Row row = sh.getRow(r);
+ if (row == null) continue;
+ for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) {
+ Integer ym = smYearMonthOf(row.getCell(c));
+ if (ym != null && ym / 100 == year && ym % 100 == month) { headerRow = r; col = c; break; }
+ }
+ }
+ if (col < 0) continue;
+ sh.setActiveCell(new org.apache.poi.ss.util.CellAddress(headerRow, col));
+ n++;
+ }
+ if (n > 0) log.info("姹囨�诲伐浣滅翱锛歿} 涓〉绛惧凡瀹氫綅鍒版湰鏈燂紙{}骞磠}鏈堬級鏁版嵁鍒�", n, year, month);
+ return n;
+ }
+
+ /** 琛ㄥご鍗曞厓鏍� -> 骞�*100+鏈堬紱鍙銆孻YYY骞碝鏈堛�嶆枃鏈垨銆屾煇鏈� 1 鍙枫�嶇殑鏃ユ湡搴忓垪鍙凤紝鎺掗櫎绱/鍚屾瘮/鐜瘮 */
+ private Integer smYearMonthOf(Cell c) {
+ if (c == null) return null;
+ try {
+ CellType t = c.getCellType();
+ if (t == CellType.STRING) {
+ String v = c.getStringCellValue();
+ if (v == null) return null;
+ v = v.trim();
+ if (v.isEmpty() || v.contains("绱") || v.contains("鍚屾瘮") || v.contains("鐜瘮")) return null;
+ java.util.regex.Matcher m = java.util.regex.Pattern
+ .compile("^(\\d{4})\\s*骞碶\s*(\\d{1,2})\\s*鏈�$").matcher(v);
+ if (!m.matches()) return null;
+ int y = Integer.parseInt(m.group(1));
+ int mo = Integer.parseInt(m.group(2));
+ return mo >= 1 && mo <= 12 ? y * 100 + mo : null;
+ }
+ if (t == CellType.NUMERIC) {
+ double v = c.getNumericCellValue();
+ if (v < 36526 || v > 73415) return null;
+ java.util.Calendar cal = java.util.Calendar.getInstance();
+ cal.setTime(org.apache.poi.ss.usermodel.DateUtil.getJavaDate(v));
+ if (cal.get(java.util.Calendar.DAY_OF_MONTH) != 1) return null;
+ return cal.get(java.util.Calendar.YEAR) * 100 + (cal.get(java.util.Calendar.MONTH) + 1);
+ }
+ } catch (Exception ignore) {
+ return null;
+ }
+ return null;
}
/** 鍙拌处椤靛崟鍏冩牸鍙栨暟锛堟暟瀛� / 鏁板瓧鏂囨湰 / 鍏紡缂撳瓨锛� */
@@ -4413,8 +5006,8 @@
if (sh == null) return 0;
smSetTitle(sh, 0, 0, "鍩庡競瀹㈣繍瀹㈣繍閲忓垎甯傚窞1-" + month + "鏈堢疮璁℃儏鍐�");
smSetTitle(sh, 22, 0, "鍩庡競瀹㈣繍瀹㈣繍鍛ㄨ浆閲忓垎甯傚窞1-" + month + "鏈堢疮璁℃儏鍐�");
- smSetTitle(sh, 45, 0, "鍩庡競瀹㈣繍瀹㈣繍閲忓垎甯傚窞" + month + "鏈堟儏鍐�");
- smSetTitle(sh, 67, 0, "鍩庡競瀹㈣繍瀹㈣繍鍛ㄨ浆閲忓垎甯傚窞" + month + "鏈堟儏鍐�");
+ smSetTitle(sh, 44, 0, "鍩庡競瀹㈣繍瀹㈣繍閲忓垎甯傚窞" + month + "鏈堟儏鍐�"); // 0鍩�44 = Excel 绗�45琛岋紙姣嶇増閲屾爣棰樺湪 A45锛孉46 鏄�"鍗曚綅"琛岋級
+ smSetTitle(sh, 66, 0, "鍩庡競瀹㈣繍瀹㈣繍鍛ㄨ浆閲忓垎甯傚窞" + month + "鏈堟儏鍐�"); // 0鍩�66 = Excel 绗�67琛岋紙鍚屼笂锛�
int n = 0;
n += smWriteCityByRegionBlock(sh, 4, true, curCum, lastCum);
n += smWriteCityByRegionBlock(sh, 26, false, curCum, lastCum);
@@ -5781,6 +6374,8 @@
String curCity = null;
Set<Long> used = new HashSet<>();
int filled = 0, kept = 0, dup = 0;
+ List<String> skipped = new ArrayList<>();
+ Map<String, Integer> skippedByCity = new LinkedHashMap<>();
for (int r = 8; r <= sheet.getLastRowNum(); r++) {
Row row = sheet.getRow(r);
if (row == null) continue;
@@ -5792,11 +6387,16 @@
if (!a.matches("\\d+(\\.\\d+)?")) continue;
if (b.isEmpty() || curCity == null) continue;
InvestmentProject p = matchInvest(projects, b, curCity, "瀹㈣繍绔欏満", used);
+ p = preferMonthlyCandidate(projects, p, mByProject, "瀹㈣繍绔欏満", used);
InvestmentMonthly m = p == null ? null : mByProject.get(p.getId());
if (p == null || m == null) {
kept++;
// 鏈夊簱鍐呴」鐩絾褰撴湀鏃犳暟鎹� 鈫� 璁颁负寰呮牳瀵癸紱鏃犲尮閰嶉」鐩� 鈫� 缁撹浆涓婃湀
if (p != null) dup++;
+ // 2026-09-22锛氫笉鍐嶉潤榛樼粨杞笂鏈堬紝鏈鐩栫殑琛屽繀椤荤暀鐥曪紙琛屽彿+甯傚窞+椤圭洰鍚�+鍘熷洜锛�
+ skipped.add("绗�" + (r + 1) + "琛� " + curCity + " " + b
+ + (p == null ? " 鈫� 搴撳唴鏃犲尮閰嶉」鐩�" : " 鈫� 搴撳唴鏃� " + period + " 鏈堝害鏁版嵁"));
+ skippedByCity.merge(curCity, 1, Integer::sum);
continue;
}
used.add(p.getId());
@@ -5826,6 +6426,7 @@
}
log.info("瀹㈣繍绔欐姇璧勬槑缁嗚〃 {}锛氬啓鍏� {} 琛岋紝缁撹浆姣嶇増鍘熷�� {} 琛岋紙鍏朵腑搴撳唴鏈夐」鐩絾褰撴湀鏃犳暟鎹� {} 琛岋級",
period, filled, kept, dup);
+ logSkippedRows("瀹㈣繍绔欐姇璧勬槑缁嗚〃", period, skipped, skippedByCity);
return toBytes(wb);
}
}
@@ -5866,6 +6467,8 @@
Map<String, double[]> cityStats = new LinkedHashMap<>(); // 鍏ㄥ悕 -> {鎬绘暟, 绱, 瑙勫垝鍐呮暟, 瑙勫垝鍐呯疮璁�, 瑙勫垝澶栨暟, 瑙勫垝澶栫疮璁
Set<Long> used = new HashSet<>();
int filled = 0, kept = 0;
+ List<String> skipped = new ArrayList<>();
+ Map<String, Integer> skippedByCity = new LinkedHashMap<>();
for (int i = 0; i < cityRows.size(); i++) {
int cr = cityRows.get(i);
String cityLabel = nvl(cellText(sheet.getRow(cr).getCell(0)));
@@ -5888,8 +6491,16 @@
if (!a.matches("\\d+(\\.\\d+)?")) continue;
if (b.isEmpty()) continue;
InvestmentProject p = matchInvest(projects, b, city, "鐗╂祦鍥尯", used);
+ p = preferMonthlyCandidate(projects, p, mByProject, "鐗╂祦鍥尯", used);
InvestmentMonthly m = p == null ? null : mByProject.get(p.getId());
- if (p == null || m == null) { kept++; continue; }
+ if (p == null || m == null) {
+ kept++;
+ // 2026-09-22锛氫笉鍐嶉潤榛樼粨杞笂鏈堬紝鏈鐩栫殑琛屽繀椤荤暀鐥�
+ skipped.add("绗�" + (r + 1) + "琛� " + city + " " + b
+ + (p == null ? " 鈫� 搴撳唴鏃犲尮閰嶉」鐩�" : " 鈫� 搴撳唴鏃� " + period + " 鏈堝害鏁版嵁"));
+ skippedByCity.merge(city, 1, Integer::sum);
+ continue;
+ }
used.add(p.getId());
// 鍙e緞鍚屽杩愮珯鏄庣粏琛細鍙鐩� H 鑷紑濮嬪缓璁剧疮璁� / J 鑷勾鍒濈疮璁� / K 鏈湀瀹屾垚 / M 褰㈣薄杩涘害
if (m.getStartCum() != null) putNum(row, 7, m.getStartCum());
@@ -6020,6 +6631,7 @@
log.warn("鐗╂祦绔欏満鎶曡祫鏄庣粏鍏紡姹傚�煎け璐�: {}", e.getMessage());
}
log.info("鐗╂祦绔欏満鎶曡祫鏄庣粏 {}锛氬啓鍏� {} 琛岋紝缁撹浆姣嶇増鍘熷�� {} 琛�", period, filled, kept);
+ logSkippedRows("鐗╂祦绔欏満鎶曡祫鏄庣粏", period, skipped, skippedByCity);
return toBytesHssf(wb);
}
@@ -6314,6 +6926,40 @@
return full != null && RegionUtil.cityList().contains(full);
}
+ /**
+ * 鍚屽悕锛堝綊涓�鍖栧悗涓�鑷达級澶氭。鏃讹紝浼樺厛閫夈�屽綋鏈堟湁鏁版嵁銆嶇殑閭d竴妗c��
+ * 鑳屾櫙锛�2026-09-22锛夛細鍗佸牥銆屽揩閫掔墿娴佷骇涓氬洯浜屾湡銆嶅湪搴撳唴鏈変袱妗� id=154锛堢宸� 2026锛屾湀搴﹀彧鍒� 7 鏈堬級涓�
+ * id=181锛堢宸� 2029锛屾湁 8 鏈堬級锛屾ā鏉胯鍚嶅墠缂�涓� 154 涓�鑷� 鈫� 瀵煎嚭鍙栧埌 154 鐨� 7 鏈堝�笺��
+ */
+ private InvestmentProject preferMonthlyCandidate(List<InvestmentProject> pool, InvestmentProject p,
+ Map<Long, InvestmentMonthly> mByProject,
+ String category, Set<Long> excludeIds) {
+ if (p == null || mByProject.containsKey(p.getId())) return p;
+ String norm = normInvestName(p.getProjectName());
+ if (norm.isEmpty()) return p;
+ InvestmentProject better = null;
+ for (InvestmentProject c : pool) {
+ if (c == null || c.getId().equals(p.getId())) continue;
+ if (excludeIds != null && excludeIds.contains(c.getId())) continue;
+ if (category != null && !category.equals(c.getCategory())) continue;
+ if (!mByProject.containsKey(c.getId())) continue;
+ if (!norm.equals(normInvestName(c.getProjectName()))) continue;
+ if (better == null || c.getId() > better.getId()) better = c;
+ }
+ return better != null ? better : p;
+ }
+
+ /** 鏈寜褰撴湀鏁版嵁瑕嗙洊鐨勮蹇呴』鐣欑棔锛氶�愯鍒楀嚭琛屽彿+甯傚窞+椤圭洰鍚嶏紝閬垮厤銆岄潤榛樼粨杞笂鏈熷�笺�嶅啀娆″彂鐢� */
+ private void logSkippedRows(String reportName, String period, List<String> skipped, Map<String, Integer> byCity) {
+ if (skipped == null || skipped.isEmpty()) return;
+ log.warn("{} {}锛歿} 琛屾湭鎸夊綋鏈堟暟鎹鐩栵紙缁撹浆姣嶇増鍘熷�硷級锛岄渶浜哄伐鏍稿婧愭枃浠�/搴撳唴鏁版嵁锛�",
+ reportName, period, skipped.size());
+ int limit = Math.min(skipped.size(), 50);
+ for (int i = 0; i < limit; i++) log.warn(" [鏈鐩朷 {}", skipped.get(i));
+ if (skipped.size() > limit) log.warn(" 鈥﹀彟鏈� {} 琛屾湭閫愯鍒楀嚭", skipped.size() - limit);
+ if (byCity != null && !byCity.isEmpty()) log.warn(" 鏈鐩栬鎸夊競宸炵粺璁★細{}", byCity);
+ }
+
/** 鍦ㄥ�欓�夐」鐩睜涓寜鍚嶇О鍖归厤 DB 椤圭洰锛堝彲閫� 甯傚窞/绫诲埆 杩囨护 + 鍘垮尯鍓嶇紑娑堟锛夛紝鏈懡涓繑鍥� null */
private InvestmentProject matchInvest(List<InvestmentProject> pool, String name, String cityFull, String category) {
return matchInvest(pool, name, cityFull, category, null);
@@ -6362,7 +7008,8 @@
if (name == null) return "";
String s = name.trim();
s = s.replaceAll("[锛�(]", "").replaceAll("[锛�)]", "");
- s = s.replaceAll("[路鈥⑩�旓紞_\\-]", "");
+ // 2026-09-22锛氳ˉ鍏呭崐/鍏ㄨ鍙ョ偣銆侀】鍙枫�佸垎鍙风瓑锛堝簱鍐呫�屼腑鍥戒緵閿�.鍏畨鍟嗚锤鐗╂祦鍥�嶅洜鍗婅鍙ョ偣鍖归厤涓嶄笂妯℃澘琛岋級
+ s = s.replaceAll("[路鈥⑩�旓紞_\\-锛�.銆傘�侊紝,;锛�:锛歖", "");
s = s.replaceAll("[鈪犫叀鈪⑩叄鈪も叆鈪︹収鈪ㄢ叐鈪扳叡鈪测叧鈪粹叺鈪垛叿鈪糕吂]", "I");
s = s.replaceAll("^(婀栧寳鐪亅姝︽眽甯倈榛勭煶甯倈鍗佸牥甯倈瀹滄槍甯倈瑗勯槼甯倈閯傚窞甯倈鑽嗛棬甯倈瀛濇劅甯倈鑽嗗窞甯倈榛勫唸甯倈鍜稿畞甯倈闅忓窞甯倈鎭╂柦宸瀨浠欐甯倈娼滄睙甯倈澶╅棬甯倈绁炲啘鏋舵灄鍖�)", "");
s = s.replaceAll("^(?:[\\u4e00-\\u9fa5]{2,4}(?:鍘縷甯倈鍖�))", "");
--
Gitblit v1.9.1