From abf468c9946c21512ddf2093895b30910e09e536 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期一, 21 九月 2026 11:33:54 +0800
Subject: [PATCH] fix(汇总表): 中口径排名累计同比基数改用母版2025静态列
---
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java | 2094 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 1,903 insertions(+), 191 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 9fdb6f2..bbe8df3 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
@@ -13,6 +13,7 @@
import com.trafficaudit.dataimport.entity.PassengerEnterpriseMonthly;
import com.trafficaudit.dataimport.entity.PassengerIndividualMonthly;
import com.trafficaudit.dataimport.entity.ScaleSplitTransport;
+import com.trafficaudit.dataimport.entity.WycOrderMonthly;
import com.trafficaudit.auditengine.entity.AuditResult;
import com.trafficaudit.auditengine.entity.AuditRun;
import com.trafficaudit.auditengine.mapper.AuditResultMapper;
@@ -29,6 +30,11 @@
import com.trafficaudit.dataimport.mapper.PassengerEnterpriseMonthlyMapper;
import com.trafficaudit.dataimport.mapper.PassengerIndividualMonthlyMapper;
import com.trafficaudit.dataimport.mapper.ScaleSplitTransportMapper;
+import com.trafficaudit.dataimport.mapper.WycOrderMonthlyMapper;
+import com.trafficaudit.reportexport.calc.MidCalc;
+import com.trafficaudit.reportexport.calc.PaxCalc;
+import com.trafficaudit.reportexport.calc.SummaryWorkbookFiller;
+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;
@@ -44,11 +50,13 @@
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.openxml4j.util.ZipSecureFile;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.STCellType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
@@ -91,6 +99,12 @@
private CityBusMonthlyMapper cityBusMapper;
@Resource
private CityTaxiMonthlyMapper cityTaxiMapper;
+ @Resource
+ private SummaryWorkbookFiller summaryWorkbookFiller;
+ @Resource
+ private WycSplitCalc wycSplitCalc;
+ @Resource
+ private WycOrderMonthlyMapper wycOrderMapper;
@Resource
private AuditResultMapper auditResultMapper;
@Resource
@@ -210,6 +224,36 @@
rows = 0;
notes.add("姣嶇増瀹氫綅澶辫触锛�" + ex.getMessage());
}
+ break;
+ }
+ case "investPassengerDetail":
+ case "investLogisticsDetail": {
+ boolean paxDetail = "investPassengerDetail".equals(t);
+ String catName = paxDetail ? "瀹㈣繍绔欏満" : "鐗╂祦鍥尯";
+ String catLabel = paxDetail ? "瀹㈣繍绔欏満鎶曡祫鏈堟姤" : "鐗╂祦鍥尯鎶曡祫鏈堟姤";
+ List<InvestmentProject> catProjects = investProjectMapper.selectList(
+ new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, catName));
+ final List<Long> catIds = new ArrayList<>();
+ for (InvestmentProject cp : catProjects) catIds.add(cp.getId());
+ if (catIds.isEmpty()) {
+ rows = 0;
+ notes.add("缂烘暟鎹細" + catLabel);
+ } else {
+ rows = addReadyCount(() -> investMonthlyMapper.selectCount(
+ periodQw(period, mode, InvestmentMonthly::getReportPeriod)
+ .in(InvestmentMonthly::getProjectId, catIds)), catLabel, notes);
+ }
+ String detTpl = paxDetail ? "妯℃澘_X鏈堝杩愮珯鎶曡祫鏄庣粏琛�.xlsx" : "妯℃澘_X鏈堢墿娴佺珯鍦烘姇璧勬槑缁�.xls";
+ try {
+ if (resolveAnyTemplate(investTemplateDir, detTpl) == null) {
+ rows = 0;
+ notes.add("docs/鎶曡祫/妯℃澘 涓嬬己灏戙��" + detTpl + "銆�");
+ }
+ } catch (Exception ex) {
+ rows = 0;
+ notes.add("姣嶇増瀹氫綅澶辫触锛�" + ex.getMessage());
+ }
+ auditTypes.add("INVEST");
break;
}
default:
@@ -333,12 +377,7 @@
if (resolveSummaryMother(period, mode) == null) {
problems.add("docs/鐢熸垚姹囨�诲ぇ琛� 涓嬬己灏戝彲澶嶅埗鐨勬瘝鐗堟眹鎬昏〃");
} else {
- Map<Integer, Map<String, double[]>> busM = loadCityBusByMonth(yearPrefix, month);
- Map<Integer, Map<String, double[]>> taxiM = loadCityTaxiByMonth(yearPrefix, month);
- Map<String, double[]> bc = busM.get(month);
- Map<String, double[]> tc = taxiM.get(month);
- if (bc != null && bc.get("鍏ㄧ渷") != null) addSum(summary, "鍩庡競鍏氦瀹㈣繍閲忥紙鍏ㄧ渷褰撴湀锛�", bc.get("鍏ㄧ渷")[0], "涓囦汉");
- if (tc != null && tc.get("鍏ㄧ渷") != null) addSum(summary, "宸℃父鍑虹瀹㈣繍閲忥紙鍏ㄧ渷褰撴湀锛�", tc.get("鍏ㄧ渷")[0], "涓囦汉");
+ summaryWorkbookPreview(period, yearNum, month, summary, problems);
}
} catch (Exception ex) {
problems.add(ex.getMessage());
@@ -372,7 +411,9 @@
coverage.addAll(investCover);
// 鎶曡祫涓哄綋鏈堟姤琛紙鏃� 1..N 绱鍙e緞锛夛紝鍙彁绀哄綋鏈堢己澶�
if (!investCover.contains(month)) missing.add(month);
- investPreview(period, mode, summary, problems);
+ if ("investPassengerDetail".equals(t)) investPreview(period, mode, "瀹㈣繍绔欏満", summary, problems);
+ else if ("investLogisticsDetail".equals(t)) investPreview(period, mode, "鐗╂祦鍥尯", summary, problems);
+ else investPreview(period, mode, summary, problems);
} else if ("energySummary".equals(t)) {
Long ec = energyMapper.selectCount(periodQw(period, mode, EnergyVehicleQuarterly::getReportPeriod));
if (ec != null && ec > 0) coverage.add(month);
@@ -403,7 +444,8 @@
}
private boolean isInvestType(String t) {
- return "investPlan".equals(t) || "investFiveYearLogistics".equals(t) || "investBillion".equals(t) || "investCounty".equals(t);
+ return "investPlan".equals(t) || "investFiveYearLogistics".equals(t) || "investBillion".equals(t) || "investCounty".equals(t)
+ || "investPassengerDetail".equals(t) || "investLogisticsDetail".equals(t);
}
private boolean isEnergyType(String t) {
@@ -440,6 +482,118 @@
sb.append(m).append("鏈�");
}
return sb.toString();
+ }
+
+ /** 姹囨�绘暣鏈叧閿寚鏍囬瑙堬細璐ц繍閲�/鍛ㄨ浆閲忋�佸煄甯傚杩愩�佷腑鍙e緞鐨� 褰撴湀銆�1..M 绱銆佸綋鏈堝悓姣斻�佺疮璁″悓姣旓紙鍚屾瘮鍙栧簱鍐呭幓骞村悓鏈燂紝缂哄垯鏄剧ず鈥旓級 */
+ private void summaryWorkbookPreview(String period, int year, int month,
+ List<Map<String, Object>> summary, List<String> problems) {
+ String yearPrefix = year + "-";
+ // ---- 璐ц繍锛堥噺=妯℃澘_璐ц繍閲忓懆杞噺鍏ㄧ渷琛岋紱鍛ㄨ浆閲�=瑙勪笂瑙勪笅鎷嗗垎琛ㄥ叏鐪佽锛� ----
+ FreightTurnoverImport ft = loadFreightTurnover(period).get("婀栧寳鐪�");
+ Double fm = ft == null ? null : freightMonth(ft, month);
+ Double fcum = ft == null ? null : freightCum(ft, month);
+ Double fy = ft == null ? null : freightYoy(ft, month);
+ Double fcy = ft == null ? null : freightCumYoy(ft, month);
+ addKpi(summary, "鍏ㄧ渷璐ц繍閲�", fm, fcum, fy, fcy, "涓囧惃");
+ if (fm != null && fy == null) {
+ problems.add((year - 1) + "-" + String.format("%02d", month) + " 鍚屾湡璐ц繍閲忔暟鎹湭瀵煎叆搴擄紝璐ц繍閲忓綋鏈�/绱鍚屾瘮鏆備笉鏄剧ず锛屽緟 2025 骞村畾绋挎暟鎹鍏ュ悗鑷姩鍑虹幇");
+ }
+ ScaleSplitTransport tm = loadProvinceMonthMap(period).get(month);
+ ScaleSplitTransport tc = getProvinceCumulative(period);
+ Double tvM = tm == null ? null : tm.getTotalTurnover();
+ Double tvC = tc == null ? null : tc.getTotalTurnover();
+ Double tvMY = tm == null ? null : getYoyMetric(tm, "turnover", "total");
+ Double tvCY = tc == null ? null : getYoyMetric(tc, "turnover", "total");
+ addKpi(summary, "鍏ㄧ渷璐х墿鍛ㄨ浆閲�", tvM, tvC, tvMY, tvCY, "涓囧惃鍏噷");
+ if (fm == null) problems.add("妯℃澘_璐ц繍閲忓懆杞噺缂� " + period + " 褰撴湀鏁版嵁锛岃揣杩愰噺浠呰兘鎻愪緵鎴嚦鏈�杩戝鍏ユ湀鐨勭疮璁�");
+ if (tvM == null) problems.add("瑙勪笂瑙勪笅鎷嗗垎缂� " + period + " 褰撴湀鏁版嵁锛屽懆杞噺/瑙勪笂瑙勪笅褰撴湀缂猴紝绱涓烘埅鑷虫渶杩戝鍏ユ湀");
+
+ // ---- 鍩庡競瀹㈣繍锛堝叕浜�+鍑虹+杞ㄩ亾+杞浮 鍏ㄧ渷灏忚锛涙寚鏍囦綅 0/2/4/6=瀹㈣繍锛�1/3/5/7=鍛ㄨ浆锛� ----
+ double[] cumM = cityCumProv(yearPrefix, month);
+ double[] cumPrev = cityCumProv(yearPrefix, month - 1);
+ double[] monthVec = diff8(cumM, cumPrev);
+ double cityPaxM = sumIdx(monthVec, new int[]{0, 2, 4, 6});
+ double cityTurnM = sumIdx(monthVec, new int[]{1, 3, 5, 7});
+ double cityPaxC = sumIdx(cumM, new int[]{0, 2, 4, 6});
+ double cityTurnC = sumIdx(cumM, new int[]{1, 3, 5, 7});
+ double[] lastCumM = cityCumProv((year - 1) + "-", month);
+ double[] lastCumPrev = cityCumProv((year - 1) + "-", month - 1);
+ double[] lastVec = diff8(lastCumM, lastCumPrev);
+ double lastPaxM = sumIdx(lastVec, new int[]{0, 2, 4, 6});
+ double lastPaxC = sumIdx(lastCumM, new int[]{0, 2, 4, 6});
+ double lastTurnM = sumIdx(lastVec, new int[]{1, 3, 5, 7});
+ double lastTurnC = sumIdx(lastCumM, new int[]{1, 3, 5, 7});
+ addKpiRaw(summary, "鍩庡競瀹㈣繍閲�", cityPaxM, cityPaxC, lastPaxM, lastPaxC, "涓囦汉娆�");
+ addKpiRaw(summary, "鍩庡競瀹㈣繍鍛ㄨ浆閲�", cityTurnM, cityTurnC, lastTurnM, lastTurnC, "涓囦汉鍏噷");
+
+ // ---- 涓彛寰勶紙鍏矾鏃呭浼佷笟 H203-1 鍏ㄧ渷锛屽崟浣嶆崲绠� /10000锛� ----
+ Map<Integer, Map<String, PassengerAgg>> data = loadPassengerAggMap();
+ double paxM = 0.0, paxC = 0.0, turnM = 0.0, turnC = 0.0;
+ double lastPaxM2 = 0.0, lastPaxC2 = 0.0, lastTurnM2 = 0.0, lastTurnC2 = 0.0;
+ for (int m = 1; m <= month; m++) {
+ PassengerAgg agg = aggOf(data, year, m, "婀栧寳鐪�");
+ if (agg != null) {
+ double pax = agg.passengerTotal / 10000.0;
+ double turn = agg.turnoverTotal / 10000.0;
+ if (m == month) { paxM = pax; turnM = turn; }
+ paxC += pax;
+ turnC += turn;
+ }
+ PassengerAgg last = aggOf(data, year - 1, m, "婀栧寳鐪�");
+ if (last != null) {
+ double lp = last.passengerTotal / 10000.0;
+ double lt = last.turnoverTotal / 10000.0;
+ if (m == month) { lastPaxM2 = lp; lastTurnM2 = lt; }
+ lastPaxC2 += lp;
+ lastTurnC2 += lt;
+ }
+ }
+ addKpiRaw(summary, "涓彛寰勫杩愰噺", paxM, paxC, lastPaxM2, lastPaxC2, "涓囦汉娆�");
+ addKpiRaw(summary, "涓彛寰勫懆杞噺", turnM, turnC, lastTurnM2, lastTurnC2, "涓囦汉鍏噷");
+
+ if (paxM <= 0 && paxC <= 0 && cityPaxC <= 0 && fm == null) {
+ problems.add("璐ц繍閲忓懆杞噺/瑙勪笂瑙勪笅/鍏矾鏃呭/鍩庡競瀹㈣繍褰撴湀鏁版嵁鍧囩己澶�");
+ }
+ if (cityPaxC > 0 && lastPaxC <= 0) {
+ problems.add((year - 1) + "-" + String.format("%02d", month) + " 鍚屾湡鍩庡競瀹㈣繍鏁版嵁鏈鍏ュ簱锛屽煄甯傚杩愬悓姣旀殏涓嶆樉绀猴紝寰� 2025 骞村畾绋挎暟鎹鍏ュ悗鑷姩鍑虹幇");
+ }
+ if (paxC > 0 && lastPaxC2 <= 0) {
+ problems.add((year - 1) + "-" + String.format("%02d", month) + " 鍚屾湡鍏矾鏃呭鏁版嵁鏈鍏ュ簱锛屼腑鍙e緞鍚屾瘮鏆備笉鏄剧ず锛屽緟 2025 骞村畾绋挎暟鎹鍏ュ悗鑷姩鍑虹幇");
+ }
+ }
+
+ /** 鍩庡競瀹㈣繍鍏ㄧ渷绱鍚戦噺锛�8 鎸囨爣锛氬叕浜ゅ杩�/鍛ㄨ浆銆佸嚭绉熷杩�/鍛ㄨ浆銆佽建閬撳杩�/鍛ㄨ浆銆佽疆娓″杩�/鍛ㄨ浆锛� */
+ private double[] cityCumProv(String yearPrefix, int month) {
+ if (month <= 0) return new double[8];
+ return loadCityPassengerCumulative(yearPrefix, month).getOrDefault("鍏ㄧ渷", new double[8]);
+ }
+
+ private double[] diff8(double[] a, double[] b) {
+ double[] out = new double[8];
+ for (int i = 0; i < 8; i++) out[i] = a[i] - b[i];
+ return out;
+ }
+
+ private double sumIdx(double[] arr, int[] idx) {
+ double sum = 0.0;
+ for (int i : idx) sum += (i < arr.length ? arr[i] : 0.0);
+ return sum;
+ }
+
+ /** 鐧惧垎姣斿悓姣旓紙鍊煎凡涓烘瘮鐜� 0.xx锛夛細杈撳嚭鍥涜 褰撴湀/绱/褰撴湀鍚屾瘮/绱鍚屾瘮 */
+ private void addKpi(List<Map<String, Object>> summary, String name, Double m, Double cum, Double yoyM, Double yoyC, String unit) {
+ if (m != null) addSum(summary, name + "路褰撴湀", round(m, 2), unit);
+ if (cum != null) addSum(summary, name + "路绱", round(cum, 2), unit);
+ if (yoyM != null) addSum(summary, name + "路褰撴湀鍚屾瘮", round(yoyM * 100.0, 1), "%");
+ if (yoyC != null) addSum(summary, name + "路绱鍚屾瘮", round(yoyC * 100.0, 1), "%");
+ }
+
+ /** 鍘熷鏁板�肩増锛堟湀/绱涓� 0 瑙嗕负鏃犳暟鎹笉鍑鸿锛� */
+ private void addKpiRaw(List<Map<String, Object>> summary, String name, double m, double cum, double lastM, double lastC, String unit) {
+ if (m > 0) addSum(summary, name + "路褰撴湀", round(m, 2), unit);
+ if (cum > 0) addSum(summary, name + "路绱", round(cum, 2), unit);
+ if (m > 0 && lastM > 0) addSum(summary, name + "路褰撴湀鍚屾瘮", round((m - lastM) / lastM * 100.0, 1), "%");
+ if (cum > 0 && lastC > 0) addSum(summary, name + "路绱鍚屾瘮", round((cum - lastC) / lastC * 100.0, 1), "%");
}
private void addSum(List<Map<String, Object>> summary, String label, Double value, String unit) {
@@ -547,9 +701,23 @@
if (allZero) problems.add("鍏ㄧ渷绱鍊煎叏涓� 0锛堝叕浜�/鍑虹鏈堟姤鍙兘鏈鍏ワ級");
}
- /** 鎶曡祫鎽樿锛氶」鐩暟 + 鏈湀瀹屾垚/鑷勾鍒�/鑷紑濮嬬疮璁� */
+ /** 鎶曡祫鎽樿锛氶」鐩暟 + 鏈湀瀹屾垚/鑷勾鍒�/鑷紑濮嬬疮璁★紙鍏ㄦ姇璧勬湀鎶ュ悎璁★紝涓嶅垎绫诲埆锛� */
private void investPreview(String period, String mode, List<Map<String, Object>> summary, List<String> problems) {
- List<InvestmentMonthly> list = investMonthlyMapper.selectList(periodQw(period, mode, InvestmentMonthly::getReportPeriod));
+ investPreview(period, mode, null, summary, problems);
+ }
+
+ /** 鎶曡祫鎽樿锛坈ategory 闈炵┖鏃跺彧缁熻璇ョ被鍒紝濡傚杩愮珯鍦�/鐗╂祦鍥尯鎶曡祫鏄庣粏琛級 */
+ private void investPreview(String period, String mode, String category, List<Map<String, Object>> summary, List<String> problems) {
+ LambdaQueryWrapper<InvestmentMonthly> qw = periodQw(period, mode, InvestmentMonthly::getReportPeriod);
+ if (category != null) {
+ List<InvestmentProject> ps = investProjectMapper.selectList(
+ new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, category));
+ List<Long> ids = new ArrayList<>();
+ for (InvestmentProject p : ps) ids.add(p.getId());
+ if (ids.isEmpty()) { problems.add("缂烘暟鎹細搴撳唴娌℃湁銆�" + category + "銆嶉」鐩富妗�"); return; }
+ qw.in(InvestmentMonthly::getProjectId, ids);
+ }
+ List<InvestmentMonthly> list = investMonthlyMapper.selectList(qw);
Set<Long> proj = new HashSet<>();
double monthDone = 0.0, yearCum = 0.0, startCum = 0.0;
for (InvestmentMonthly m : list) {
@@ -719,36 +887,36 @@
for (Double v : h2032FreightCum.values()) {
if (v != null) provAbove += v;
}
- Double provAboveWan = round(provAbove / 10000.0, 4);
+ Double provAboveWan = provAbove / 10000.0;
Double provTotal = freightCum(ftMap.get("婀栧寳鐪�"), month);
- Double provBelow = provTotal == null ? null : round(provTotal - provAboveWan, 4);
+ Double provBelow = provTotal == null ? null : provTotal - provAboveWan;
Double provTotalYoy = freightCumYoy(ftMap.get("婀栧寳鐪�"), month);
double provAboveLast = 0.0;
for (Double v : h2032FreightCumLast.values()) {
if (v != null) provAboveLast += v;
}
- Double provAboveYoy = provAboveLast == 0.0 ? null : round((provAbove - provAboveLast) / provAboveLast, 4);
+ Double provAboveYoy = provAboveLast == 0.0 ? null : (provAbove - provAboveLast) / provAboveLast;
Double provTotalLast = lastFreightCum(ftMap.get("婀栧寳鐪�"), month);
- Double provBelowLast = provTotalLast == null ? null : round(provTotalLast - provAboveLast / 10000.0, 4);
+ Double provBelowLast = provTotalLast == null ? null : provTotalLast - provAboveLast / 10000.0;
Double provBelowYoy = provBelowLast == null || provBelowLast == 0.0 ? null
- : round((provBelow - provBelowLast) / provBelowLast, 4);
+ : (provBelow - provBelowLast) / provBelowLast;
for (String city : RegionUtil.cityList()) {
Double a = h2032FreightCum.get(city);
- double aWan = a == null ? 0.0 : round(a / 10000.0, 4);
+ double aWan = a == null ? 0.0 : a / 10000.0;
Double t = freightCum(ftMap.get(city), month);
above.put(city, aWan);
total.put(city, t);
- below.put(city, t == null ? null : round(t - aWan, 4));
+ below.put(city, t == null ? null : t - aWan);
aboveYoy.put(city, h2032FreightYoy.get(city));
// 瑙勪笅鍚屾瘮 = (瑙勪笅浠婂勾 - 瑙勪笅鍘诲勾) / 瑙勪笅鍘诲勾锛涜涓嬪幓骞� = 妯℃澘鍘诲勾鍚堣 - H2032鍘诲勾瑙勪笂
Double tLast = lastFreightCum(ftMap.get(city), month);
Double aLast = h2032FreightCumLast.get(city);
- double aLastWan = aLast == null ? 0.0 : round(aLast / 10000.0, 4);
- Double belowLast = tLast == null ? null : round(tLast - aLastWan, 4);
+ double aLastWan = aLast == null ? 0.0 : aLast / 10000.0;
+ Double belowLast = tLast == null ? null : tLast - aLastWan;
Double b = below.get(city);
belowYoy.put(city, belowLast == null || belowLast == 0.0 || b == null
- ? null : round((b - belowLast) / belowLast, 4));
+ ? null : (b - belowLast) / belowLast);
totalYoy.put(city, freightCumYoy(ftMap.get(city), month));
}
@@ -773,20 +941,20 @@
if (maLast != null) provMonthAboveLast += maLast;
monthAbove.put(city, ma);
monthTotal.put(city, mt);
- monthBelow.put(city, mt == null ? null : round(mt - (ma == null ? 0.0 : ma), 4));
+ monthBelow.put(city, mt == null ? null : mt - (ma == null ? 0.0 : ma));
monthAboveYoy.put(city, growthYoy(ma, maLast));
Double mb = monthBelow.get(city);
- Double mbLast = mtLast == null ? null : round(mtLast - (maLast == null ? 0.0 : maLast), 4);
+ Double mbLast = mtLast == null ? null : mtLast - (maLast == null ? 0.0 : maLast);
monthBelowYoy.put(city, growthYoy(mb, mbLast));
monthTotalYoy.put(city, freightYoy(ftMap.get(city), month));
}
- Double provMonthAboveWan = provMonthAbove == 0.0 ? null : round(provMonthAbove, 4);
+ Double provMonthAboveWan = provMonthAbove == 0.0 ? null : provMonthAbove;
Double provMonthTotal = freightMonth(ftMap.get("婀栧寳鐪�"), month);
- Double provMonthBelow = provMonthTotal == null ? null : round(provMonthTotal - provMonthAbove, 4);
+ Double provMonthBelow = provMonthTotal == null ? null : provMonthTotal - provMonthAbove;
Double provMonthAboveYoy = growthYoy(provMonthAbove == 0.0 ? null : provMonthAbove, provMonthAboveLast);
Double provMonthTotalLast = lastFreightMonth(ftMap.get("婀栧寳鐪�"), month);
Double provMonthBelowLast = provMonthTotalLast == null ? null
- : round(provMonthTotalLast - provMonthAboveLast, 4);
+ : provMonthTotalLast - provMonthAboveLast;
Double provMonthBelowYoy = growthYoy(provMonthBelow, provMonthBelowLast);
Double provMonthTotalYoy = freightYoy(ftMap.get("婀栧寳鐪�"), month);
@@ -1065,25 +1233,15 @@
return lv == 0.0 ? null : (v - lv) / lv;
}
- /** 涓彛寰勬湀鍚屾瘮锛歬=-1 琛ㄧず鎬昏锛堢彮绾�+鍏氦涔嬪拰锛屽嚭绉�/缃戠害杞︽棤婧愭暟鎹寜 0锛夛紱鏃犲幓骞存暟鎹暀绌� */
+ /** 涓彛寰勬湀鍚屾瘮锛歬=-1 琛ㄧず鎬昏锛堢彮绾�+鍏氦+鍑虹鍩庝埂+缃戠害杞﹀煄涔� 鍥涢」涔嬪拰锛夛紱鏃犲幓骞存暟鎹暀绌� */
private void fillMidYoyCell(Cell c, Map<Integer, Map<String, double[][]>> mid, int year, int month,
String area, int k, boolean volume) {
- double v = 0.0, lv = 0.0;
- if (k == -1) {
- for (int kk = 1; kk <= 4; kk++) {
- if (kk >= 3) continue;
- v += midClassVal(mid, year, month, area, kk, volume);
- lv += midClassVal(mid, year - 1, month, area, kk, volume);
- }
- } else if (k <= 2) {
- v = midClassVal(mid, year, month, area, k, volume);
- lv = midClassVal(mid, year - 1, month, area, k, volume);
- }
+ Double y = midMonthYoy(mid, year, month, area, k, volume);
c.setBlank();
- if (v != 0.0 && lv != 0.0) c.setCellValue(round((v - lv) / lv, 4));
+ if (y != null) c.setCellValue(y);
}
- /** 涓彛寰勭疮璁″悓姣斿垪锛�1..months 鏈堢疮璁★紱k=-1 鎬昏 = 鐝嚎+鍏氦锛� */
+ /** 涓彛寰勭疮璁″悓姣斿垪锛�1..months 鏈堢疮璁★紱鍥涚淮榻愬叏锛� */
private void fillMidCumYoyColumn(Sheet sheet, Map<Integer, Map<String, double[][]>> mid, int year,
int months, int colIdx) {
List<String> areas = midTemplateAreas();
@@ -1101,19 +1259,13 @@
if (blockOffset < 0 || blockOffset % 5 == 0) continue;
int k = blockOffset % 5;
String area = areas.get(blockOffset / 5);
- double v = 0.0, lv = 0.0;
- for (int m = 1; m <= months; m++) {
- if (k <= 2) {
- v += midClassVal(mid, year, m, area, k, volume);
- lv += midClassVal(mid, year - 1, m, area, k, volume);
- }
- }
+ Double y = midCumYoy(mid, year, area, months, k, volume);
Row row = sheet.getRow(r0);
if (row == null) continue;
Cell c = row.getCell(colIdx);
if (c == null) c = row.createCell(colIdx);
c.setBlank();
- if (v != 0.0 && lv != 0.0) c.setCellValue(round((v - lv) / lv, 4));
+ if (y != null) c.setCellValue(y);
}
}
@@ -1139,7 +1291,7 @@
private Double monthFreightWan(Map<String, Map<Integer, Double>> byCityMonth, String city, int month) {
Map<Integer, Double> byMonth = byCityMonth == null ? null : byCityMonth.get(city);
Double v = byMonth == null ? null : byMonth.get(month);
- return v == null ? null : round(v / 10000.0, 4);
+ return v == null ? null : v / 10000.0;
}
/** 鍚屾瘮 = (鏈湡-鍩烘湡)/鍩烘湡锛涗换涓�鏈熺己澶辨垨鍩烘湡涓� 0 杩斿洖 null */
@@ -1241,12 +1393,12 @@
}
}
- /** 鍛ㄨ浆閲忔帓鍚� Q/R 鍗犳瘮锛氭ā鏉夸负缂撳瓨鍊硷紝鎸夋柊鏁版嵁閲嶇畻锛堣涓婂崰姣� 1 浣嶅皬鏁帮級 */
+ /** 鍛ㄨ浆閲忔帓鍚� Q/R 鍗犳瘮锛氭ā鏉夸负缂撳瓨鍊硷紝鎸夋柊鏁版嵁閲嶇畻锛堝叏绮惧害鍐欏叆锛屼袱浣嶅皬鏁板睍绀轰氦缁欏崟鍏冩牸鏍煎紡锛� */
private void setFreightShare(Row row, Double above, Double total) {
if (total != null && total > 0 && above != null) {
- double share = round(above * 10.0 / total, 1);
+ double share = above * 10.0 / total;
setFreightCell(row, 16, share);
- setFreightCell(row, 17, round(10.0 - share, 1));
+ setFreightCell(row, 17, 10.0 - share);
} else {
setFreightCell(row, 16, (Double) null);
setFreightCell(row, 17, (Double) null);
@@ -1456,9 +1608,9 @@
setNumeric(row, 15, isProvince ? null : yoyRankOfMap(totalYoy, city));
if (t != null && t > 0 && a != null) {
- double share = round(a * 10.0 / t, 1);
+ double share = a * 10.0 / t;
setNumeric(row, 16, share);
- setNumeric(row, 17, round(10.0 - share, 1));
+ setNumeric(row, 17, 10.0 - share);
}
return rowIdx + 1;
}
@@ -1499,9 +1651,9 @@
setNumeric(row, 15, isProvince || cum == null ? null : yoyRankOfTurnover(cumMap, city, 2));
if (total != null && total > 0 && above != null) {
- double share = round(above * 10.0 / total, 1);
+ double share = above * 10.0 / total;
setNumeric(row, 16, share);
- setNumeric(row, 17, round(10.0 - share, 1));
+ setNumeric(row, 17, 10.0 - share);
}
return rowIdx + 1;
}
@@ -1521,7 +1673,7 @@
Double v = byMonth == null ? null : byMonth.get(month);
if (v != null) sum += v;
}
- return round(sum / 10000.0, 4);
+ return sum / 10000.0;
}
Double total = freightMonth(ft, month);
Double above = getProvinceMonthValue(provinceMonthMap, ft, month, "freight", "above", h2032Freight);
@@ -1552,7 +1704,7 @@
for (Double v : h2032FreightCum.values()) {
if (v != null) sum += v;
}
- return round(sum / 10000.0, 4);
+ return sum / 10000.0;
}
Double total = freightCum(ft, monthCount);
Double above = getProvinceCumValue(provinceCum, ft, "freight", "above", h2032FreightCum, monthCount);
@@ -1582,7 +1734,7 @@
if ("above".equals(scale)) {
Map<Integer, Double> byMonth = h2032Freight.get(city);
Double v = byMonth == null ? null : byMonth.get(month);
- return v == null ? 0.0 : round(v / 10000.0, 4);
+ return v == null ? 0.0 : v / 10000.0;
}
Double total = freightMonth(ft, month);
Double above = getCityMonthValue(monthData, ft, city, month, "freight", "above", h2032Freight);
@@ -1611,7 +1763,7 @@
if ("total".equals(scale)) return freightCum(ft, monthCount);
if ("above".equals(scale)) {
Double v = h2032FreightCum.get(city);
- return v == null ? 0.0 : round(v / 10000.0, 4);
+ return v == null ? 0.0 : v / 10000.0;
}
Double total = freightCum(ft, monthCount);
Double above = getCityCumValue(cum, ft, "freight", "above", h2032FreightCum, city, monthCount);
@@ -1848,20 +2000,20 @@
double v = passengerMonthVal(data, indi, currentYear, m, region, metricIdx);
if (!keepFormula) {
if (c == null) c = row.createCell(col);
- if (v == 0.0) c.setBlank(); else c.setCellValue(round(v, 4));
+ if (v == 0.0) c.setBlank(); else c.setCellValue(v);
}
// 鍚屾瘮鍒楋細搴撳唴鍘诲勾鍚屾湀鍚屾瘮锛堟ā鏉� #REF! 鍏紡鏇挎崲涓烘暟鍊硷紝鍏紡琛屼篃瑕嗗啓锛�
double lv = passengerMonthVal(data, indi, currentYear - 1, m, region, metricIdx);
Cell yc = row.getCell(col + 1);
if (yc == null) yc = row.createCell(col + 1);
- if (v == 0.0 || lv == 0.0) yc.setBlank(); else yc.setCellValue(round((v - lv) / lv, 4));
+ if (v == 0.0 || lv == 0.0) yc.setBlank(); else yc.setCellValue((v - lv) / lv);
}
// 绱鍚屾瘮鍒楋紙妯℃澘 #REF! 鈫� 搴撳唴鍘诲勾 1..N 鏈堢疮璁″悓姣旀暟鍊硷級
int cumCol = extended ? 26 : 14;
Double cumYoy = passengerCumYoy(data, indi, currentYear, fillMonths, region, metricIdx);
Cell yc2 = row.getCell(cumCol + 1);
if (yc2 == null) yc2 = row.createCell(cumCol + 1);
- if (cumYoy == null) yc2.setBlank(); else yc2.setCellValue(round(cumYoy, 4));
+ if (cumYoy == null) yc2.setBlank(); else yc2.setCellValue(cumYoy);
}
recalc(wb);
return toBytes(wb);
@@ -1942,7 +2094,7 @@
private Double yoy(Double cur, Double base) {
if (cur == null || base == null || base == 0.0) return null;
- return round((cur - base) / base, 4);
+ return (cur - base) / base;
}
/** 鎸囨爣鍙栧�硷紙宸叉崲绠椾负涓囦汉/涓囦汉鍏噷/鍏噷锛涙棤鏁版嵁杩斿洖 null锛� */
@@ -2067,20 +2219,15 @@
boolean extended = fillMonths > 6;
if (extended) extendMonthlyColumns(sheet, 2, currentYear, 4, sheet.getLastRowNum());
List<String> areas = midTemplateAreas();
+ // 2026-09-18锛氭敼涓恒�屾寜鍗曞厓鏍肩被鍨嬫棤鍏炽�嶉亶鍘嗏�斺�旀墿鍒楋紙7~12 鏈堬級鍑烘潵鐨勫�煎垪鍦ㄦā鏉块噷鏄┖鏍硷紝
+ // 鏃у疄鐜板彧澶勭悊 CellType.FORMULA 鐨勬牸锛屽鑷� 7 鏈堝強浠ュ悗鏈堜唤姘歌繙濉笉涓婏紙8 鏈堟暣鍒� 0銆佺疮璁″彧鍒� 6 鏈堬級銆�
for (Row row : sheet) {
if (row == null) continue;
- for (Cell c : row) {
- if (c.getCellType() != CellType.FORMULA) continue;
- String f = c.getCellFormula();
- if (f == null) continue;
- boolean crossBook = f.contains("[");
- if (!crossBook && !isYoyMonthCol(c.getColumnIndex() + 1)) continue; // 鍐呴儴鍏紡锛堝悎璁�/绱锛変繚鐣欓噸绠楋紱鍚屾瘮鍒� #REF! 瑕嗗啓涓烘暟鍊�
- if (crossBook && !is2026MonthCol(c.getColumnIndex() + 1)) {
- keepCached(c); // 2025 骞村垪淇濈暀妯℃澘缂撳瓨鍊�
- continue;
- }
+ for (int c0 = 0; c0 < Math.max(row.getLastCellNum(), 0); c0++) {
+ Cell c = row.getCell(c0);
+ if (c == null) continue;
int r = c.getRowIndex() + 1;
- int col = c.getColumnIndex() + 1;
+ int col = c0 + 1;
int blockOffset = -1;
boolean volume = false;
if (r >= 5 && r <= 94) {
@@ -2091,33 +2238,65 @@
volume = false;
}
if (blockOffset < 0) {
- keepCached(c); // r1/r2 澶囨敞绛�
+ if (c.getCellType() == CellType.FORMULA) keepCached(c); // r1/r2 澶囨敞銆侀〉澶栬法绨垮紩鐢ㄧ瓑
continue;
}
- if (blockOffset % 5 == 0) {
- if (isYoyMonthCol(col)) {
- fillMidYoyCell(c, mid, currentYear, monthOfCol(col), areas.get(blockOffset / 5), -1, volume);
- } else {
- keepCached(c); // 鎬昏鍐呴儴鍏紡锛堥槻寰★級
+ boolean isVal = is2026MonthCol(col);
+ boolean isYoy = isYoyMonthCol(col);
+ if (!isVal && !isYoy) {
+ // 2025 骞村垪绛夛細璺ㄧ翱鍏紡鍙栫紦瀛樺�兼柇閾撅紱鍐呴儴鍏紡锛堝悎璁�/绱锛変繚鐣欙紝绋嶅悗 recalc 閲嶇畻
+ if (c.getCellType() == CellType.FORMULA) {
+ String f = c.getCellFormula();
+ if (f != null && f.contains("[")) keepCached(c);
}
continue;
}
- int k = blockOffset % 5; // 1=鐝嚎 2=鍏氦 3=鍑虹 4=缃戠害杞�
String area = areas.get(blockOffset / 5);
- if (is2026MonthCol(col)) {
+ if (blockOffset % 5 == 0) {
+ if (isYoy) {
+ fillMidYoyCell(c, mid, currentYear, monthOfCol(col), area, -1, volume);
+ }
+ // 鎬昏鍊煎垪锛氫竴寰嬩繚鐣欐ā鏉垮叕寮忥紙1~6 鏈堝師鍏紡 + 鎵╁垪鍏嬮殕鍑烘潵鐨勫叕寮忥級锛�
+ // 鐢辩敓鎴愭敹灏剧殑 recalcWorkbookFormulas 姹傚�煎啓鍥炵紦瀛樸�傛棭鏈熺増鏈繖閲� keepCached
+ // 浼氭妸鍒氬厠闅嗗嚭鏉ョ殑鍏紡娓呮垚绌烘牸锛屽鑷� 7 鏈堝強浠ュ悗鎬昏姘歌繙绌虹櫧銆�
+ continue;
+ }
+ int k = blockOffset % 5; // 1=鐝嚎 2=鍏氦 3=鍑虹鍩庝埂 4=缃戠害杞﹀煄涔�
+ if (isVal) {
fillMidCell(c, mid, currentYear, monthOfCol(col), area, k, volume);
- } else if (isYoyMonthCol(col)) {
- fillMidYoyCell(c, mid, currentYear, monthOfCol(col), area, k, volume);
} else {
- keepCached(c); // 2025 骞村垪淇濈暀妯℃澘缂撳瓨鍊�
+ fillMidYoyCell(c, mid, currentYear, monthOfCol(col), area, k, volume);
}
}
}
// 绱鍚屾瘮鍒楋紙妯℃澘 #REF! 鈫� 搴撳唴鍘诲勾 1..N 鏈堢疮璁″悓姣旀暟鍊硷級
fillMidCumYoyColumn(sheet, mid, currentYear, fillMonths, extended ? 27 : 15);
- recalc(wb);
+ // evaluateAll 浼氬湪璺ㄧ翱鍏紡锛圫heet2 鐨� [1]xx!A1锛夊鏁存湰涓柇锛岀疮璁″垪 AA 灏辨案杩滄嬁涓嶅埌鏂扮紦瀛樺�硷紝
+ // 鏀圭敤閫愭牸瀹归敊鐨� recalcWorkbookFormulas锛氬潖鏍艰烦杩囥�佸ソ鏍肩収绠椼��
+ int[] recalc = recalcWorkbookFormulas(wb);
+ log.info("涓彛寰勬槑缁嗗叕寮忛噸绠楀畬鎴愶細鎴愬姛 {} 鏍硷紝澶辫触 {} 鏍�", recalc[0], recalc[1]);
clearFormulaErrorsAll(wb); // 鍘诲勾鍚屾湡鏄庣粏椤垫畫鐣� #REF! 鍚屾瘮鍏紡 鈫� 娓呯┖
+ blankFutureMonthColumns(sheet, fillMonths); // 鎶ヨ〃鏈熶箣鍚庣殑鏈堜唤鍒椾笉淇濈暀鍏嬮殕鍏紡/0 鍊�
return toBytes(wb);
+ }
+ }
+
+ /**
+ * 娓呯┖銆屾姤琛ㄦ湡涔嬪悗銆嶇殑鏈堜唤鍒楋紙鎵╁垪鍏嬮殕鍑烘潵鐨� 7~12 鏈堥噷銆佸皻鏈埌鏈熺殑鏈堜唤锛夈��
+ * 涓嶆竻鐨勮瘽锛屽厠闅嗗嚭鏉ョ殑鎬昏鍏紡浼氱畻鍑� 0锛屾姤琛ㄤ笂浼氬嚭鐜�"9~12 鏈堝叏鏄� 0"鐨勫亣鏁版嵁锛�
+ * 绱鍒楋紙AA锛夌殑鍏紡/缂撳瓨鍊煎凡鍦ㄩ噸绠楅樁娈电畻濂斤紝娓呯┖鏍煎瓙涓嶅奖鍝嶅畠锛堢┖ = 0锛夈��
+ */
+ private void blankFutureMonthColumns(Sheet sheet, int filledMonths) {
+ for (int m = filledMonths + 1; m <= 12; m++) {
+ int col = 2 * m; // 0 鍩猴細1 鏈�=2(C)銆�7 鏈�=14(O)銆�12 鏈�=24(Y)
+ for (int r = 4; r <= sheet.getLastRowNum(); r++) { // 0 鍩虹 4 琛� = 绗� 5 琛岋紙棣栦釜鏁版嵁琛岋級锛岃〃澶翠笉鍔�
+ Row row = sheet.getRow(r);
+ if (row == null) continue;
+ Cell val = row.getCell(col);
+ if (val != null) val.setBlank();
+ Cell yoy = row.getCell(col + 1);
+ if (yoy != null) yoy.setBlank();
+ }
}
}
@@ -2142,16 +2321,19 @@
return (col - 1) / 2;
}
- /** 濉腑鍙e緞鏁版嵁鍗曞厓鏍硷細鍑虹/缃戠害杞�(3/4)鏄惧紡 0锛岀彮绾�/鍏氦濉簱鍐呭�硷紙鏃犲�兼竻绌猴級 */
+ /**
+ * 濉腑鍙e緞鏁版嵁鍗曞厓鏍硷細鐝嚎/鍏氦/鍑虹鍩庝埂/缃戠害杞﹀煄涔� 鍥涚淮缁熶竴鍙栧簱鍐呭�笺��
+ * 搴撳唴璇ユ湀璇ョ淮鏃犳簮锛堝彇鍊间负 0锛夋椂**淇濈暀姣嶇増/妯℃澘鍘熷��**鈥斺�旂粷涓嶅啓绌恒�佷篃涓嶅啓 0銆�
+ * 渚濇嵁锛氭ā鏉跨殑鍩庡競绾у煄涔¤鏄�屽彴璐﹂〉 鎬婚噺 鈭� 鍩庡競鍐呫�嶇殑寮曠敤銆佸苟宸蹭笟鍔″畾绋匡紱
+ * 鐢ㄥ簱鍐呯己澶卞�艰鐩栦細鎶婂畾绋挎暟娓呮帀锛堝 2026-01~06 缃戠害杞﹁鍗曠己澶辨椂锛�
+ * 鍏ㄧ渷缃戠害杞﹀煄涔� 333.39 涓庡悇甯傚窞鍊间細琚竻绌猴級銆�
+ */
private void fillMidCell(Cell c, Map<Integer, Map<String, double[][]>> mid, int year, int month, String area, int k, boolean volume) {
- if (k == 3 || k == 4) {
- writeExplicitZero(c); // 鍑虹/缃戠害杞︽殏鏃犳暟鎹簮锛屾樉寮� 0
- return;
- }
double v = midClassVal(mid, year, month, area, k, volume);
+ if (v == 0.0) return; // 搴撳唴鏃犳簮锛氫繚鐣欐瘝鐗�/瀹氱鍘熷�硷紝缂烘暟鎹笉瑕嗙洊
// POI setCellValue(double) 瀵瑰叕寮忓崟鍏冩牸鍙洿鏂扮紦瀛樹笉绉婚櫎鍏紡锛屽繀椤诲厛 setBlank 鍐嶅啓鍊�
c.setBlank();
- if (v != 0.0) c.setCellValue(round(v, 4));
+ c.setCellValue(v);
}
/** 鏄惧紡鍐欏叆 0锛圥OI setCellValue(0.0) 浼氳浆 blank锛岄渶鎿嶄綔搴曞眰 XML锛� */
@@ -2176,6 +2358,56 @@
return volume ? arr[k][0] : arr[k][1];
}
+ /** B3锛氱櫥璁般�岃鏈熻甯傚窞璇ョ淮搴︽湁婧愯銆嶃�傚垽鎹槸婧愯〃鏈夋病鏈夎繖涓�琛岋紝鑰屼笉鏄畻鍑烘潵鐨勫�兼槸鍚﹂潪 0
+ * 鈥斺�斿洜涓恒�屽�间负 0銆嶅彲鑳芥槸鐪熷疄涓氬姟锛堝姝︽眽甯傚贰娓稿嚭绉熷叏閮ㄤ负鍩庡競鍐咃紝鍩庨檯鍩庝埂鎭掍负 0锛夈�� */
+ private void markMid(Map<Integer, Map<String, boolean[]>> pres, int key, String city, int k) {
+ pres.computeIfAbsent(key, kk -> new HashMap<>())
+ .computeIfAbsent(city, kk -> new boolean[5])[k] = true;
+ }
+
+ /**
+ * B3锛氳鏈堣缁村害鏄惁銆屾湁婧愩�嶏紙鍚屾瘮瀹屽鎬ф牎楠岀敤锛夈��
+ * k <= 0 琛ㄧず銆屾�婚噺琛屻�嶏紝瑕佹眰鍥涚淮锛堢彮绾�/鍏氦/鍑虹/缃戠害杞︼級閮芥湁婧愭墠绠楅綈鍏ㄣ��
+ */
+ private boolean midMonthHas(Map<Integer, Map<String, double[][]>> mid, int year, int month, String area, int k, boolean volume) {
+ Map<String, boolean[]> pm = midPresence.get().get(year * 100 + month);
+ if (pm == null) return false;
+ boolean[] b = pm.get(area);
+ if (b == null) return false;
+ if (k <= 0) {
+ for (int kk = 1; kk <= 4; kk++) {
+ if (!b[kk]) return false;
+ }
+ return true;
+ }
+ return k < b.length && b[k];
+ }
+
+ /** B3锛氬幓骞� 1..monthCount 鏈堢殑鍛戒腑鏈堜唤鏁帮紙鐢ㄤ簬鍒ゅ畾鍚屾瘮鍩烘暟鏄惁榻愬叏锛� */
+ private int midCumHitMonths(Map<Integer, Map<String, double[][]>> mid, int year, String area, int monthCount, int k, boolean volume) {
+ int n = 0;
+ for (int m = 1; m <= monthCount; m++) {
+ if (midMonthHas(mid, year, m, area, k, volume)) n++;
+ }
+ return n;
+ }
+
+ /** B3锛氬綋鏈堝悓姣斺�斺�斿幓骞村悓鏈堝熀鏁扮己澶卞垯鐣欑┖锛堢己鏁版嵁涓嶅啀褰� 0 璁$畻锛� */
+ private Double midMonthYoy(Map<Integer, Map<String, double[][]>> mid, int year, int month, String area, int k, boolean volume) {
+ if (!midMonthHas(mid, year - 1, month, area, k, volume)) return null;
+ double v = midClassVal(mid, year, month, area, k, volume);
+ if (v == 0.0) return null;
+ return yoyOf(v, midClassVal(mid, year - 1, month, area, k, volume));
+ }
+
+ /** B3锛氱疮璁″悓姣斺�斺�斿幓骞� 1..monthCount 鍚勬湀榻愬叏鎵嶈绠楋紝缂轰换浣曚竴鏈堜竴寰嬬暀绌� */
+ private Double midCumYoy(Map<Integer, Map<String, double[][]>> mid, int year, String area, int monthCount, int k, boolean volume) {
+ if (midCumHitMonths(mid, year - 1, area, monthCount, k, volume) < monthCount) return null;
+ double v = midCumClass(mid, year, area, monthCount, k, volume);
+ if (v == 0.0) return null;
+ return yoyOf(v, midCumClass(mid, year - 1, area, monthCount, k, volume));
+ }
+
/** 鍏紡鍗曞厓鏍兼浛鎹负缂撳瓨鏁板�硷紙閬垮厤璺ㄧ翱寮曠敤鏂摼锛涢潪鏁板�肩紦瀛樼疆绌猴級 */
private void keepCached(Cell c) {
try {
@@ -2196,7 +2428,20 @@
* 涓彛寰勫垎绫绘湀搴︽暟鎹細骞�*100+鏈� -> (甯傚窞/鍏ㄧ渷 -> double[5][2])
* 缁村害 0=鎬婚噺 1=鍏矾鐝嚎(h2031+涓綋) 2=鍩庨檯鍩庝埂鍏氦(cityBus) 3=宸℃父鍑虹 4=缃戠害杞︼紱[瀹㈣繍閲�(涓囦汉), 鍛ㄨ浆閲�(涓囦汉鍏噷)]
*/
+ /** B3锛氭渶杩戜竴娆� loadMidClassMap 鐨勩�岀淮搴︽槸鍚︽湁婧愯銆嶇櫥璁拌〃锛坘ey -> city -> boolean[5]锛夛紝鐢ㄤ簬鍚屾瘮瀹屽鎬ф牎楠� */
+ private final ThreadLocal<Map<Integer, Map<String, boolean[]>>> midPresence =
+ ThreadLocal.withInitial(HashMap::new);
+
+ /** B4/B5锛氭渶杩戜竴娆� loadMidClassMap 鐨勬暟鎹棶棰樻竻鍗曪紙绾跨▼鍐咃紝闅忓鍑轰綋妫�杈撳嚭/鍐欑敓鎴愪欢锛� */
+ private final ThreadLocal<java.util.List<String>> midIssues = ThreadLocal.withInitial(java.util.ArrayList::new);
+
+ /** B4/B5锛氬彇鏈�杩戜竴娆′腑鍙e緞鏁版嵁鍔犺浇鐨勯棶棰樻竻鍗曪紙鍙鍓湰锛� */
+ public java.util.List<String> getLastMidIssues() { return new java.util.ArrayList<>(midIssues.get()); }
+
private Map<Integer, Map<String, double[][]>> loadMidClassMap() {
+ midIssues.get().clear(); // B5锛氭湰娆″姞杞界殑闂娓呭崟
+ Map<Integer, Map<String, boolean[]>> pres = new HashMap<>();
+ midPresence.set(pres); // B3锛氭湰娆″姞杞界殑缁村害鏈夋簮鐧昏
Map<Integer, Map<String, double[][]>> mid = new HashMap<>();
for (PassengerEnterpriseMonthly e : passengerMapper.selectList(null)) {
if (e.getReportPeriod() == null) continue;
@@ -2218,6 +2463,8 @@
+ nz(e.getTurnoverClass4()) + nz(e.getTurnoverCharter())) / 10000.0;
addMidClass(mid, key, city, 1, pass, turn);
addMidClass(mid, key, "婀栧寳鐪�", 1, pass, turn);
+ markMid(pres, key, city, 1);
+ markMid(pres, key, "婀栧寳鐪�", 1);
}
for (PassengerIndividualMonthly e : passengerIndividualMapper.selectList(null)) { // 涓綋骞跺叆鍏矾鐝嚎/鎬婚噺锛�09-03 鍙e緞锛�
if (e.getReportPeriod() == null) continue;
@@ -2237,6 +2484,8 @@
double turn = nz(e.getTurnover()) / 10000.0;
addMidClass(mid, key, city, 1, pass, turn);
addMidClass(mid, key, "婀栧寳鐪�", 1, pass, turn);
+ markMid(pres, key, city, 1);
+ markMid(pres, key, "婀栧寳鐪�", 1);
}
for (CityBusMonthly b : cityBusMapper.selectList(null)) {
if (b.getReportPeriod() == null || b.getCity() == null) continue;
@@ -2252,10 +2501,73 @@
String city = RegionUtil.normalizeCityName(b.getCity());
if (city == null) continue;
int key = y * 100 + m;
- addMidClass(mid, key, city, 2, nz(b.getPassengerChengxiang()), nz(b.getTurnoverChengxiang()));
- addMidClass(mid, key, "婀栧寳鐪�", 2, nz(b.getPassengerChengxiang()), nz(b.getTurnoverChengxiang()));
+ // 鍙e緞缁熶竴锛�2026-09-19锛岀敤鎴锋牳瀵广�屼腑鍙e緞鎺掑悕銆嶅懆杞噺 vs 浜哄伐鍒楋級锛氬煄闄呭煄涔′竴寰嬬敤銆屾�婚噺 鈭� 鍩庡競鍐呫�嶇幇绠椼��
+ // 鍘熼�昏緫銆屽瓨鍌ㄧ殑鍩庨檯鍩庝埂 > 0 灏变紭鍏堝彇瀛楁鍊硷紝<=0 鎵嶅洖閫�鍒板樊棰濇硶銆嶄細鍦ㄤ釜鍒紒涓氳涓婁笌宸娉曞樊 卤0.01
+ // 锛堟簮鎶ヨ〃閲屾�婚噺/鍩庡競鍐�/鍩庨檯鍩庝埂鍚勮嚜涓や綅灏忔暟鑸嶅叆锛�2026 骞� 1-8 鏈堝叏鐪佸叡 72 琛岋級锛岀疮鍔犲悗
+ // 銆屼腑鍙e緞鎺掑悕銆嶉〉姣斻�屼腑鍙e緞鏄庣粏銆�/浜哄伐鍙e緞澶� 0.32 涓囦汉鍏噷銆傛敼涓哄樊棰濇硶鍚庝笌鍙拌处椤点�佹槑缁嗛〉銆佸洓椤靛悎鎴愬彛寰勪竴鑷淬��
+ double cxPass = nz(b.getPassengerVolume()) - nz(b.getPassengerCity());
+ double cxTurn = nz(b.getTurnover()) - nz(b.getTurnoverCity());
+ if (cxPass < 0) cxPass = 0.0;
+ if (cxTurn < 0) cxTurn = 0.0;
+ addMidClass(mid, key, city, 2, cxPass, cxTurn);
+ addMidClass(mid, key, "婀栧寳鐪�", 2, cxPass, cxTurn);
+ markMid(pres, key, city, 2);
+ markMid(pres, key, "婀栧寳鐪�", 2);
+ }
+ // 缁村害3锛氬煄闄呭煄涔″贰娓稿嚭绉� = 鍙拌处椤点�屽嚭绉�!鎬婚噺 鈭� 鍑虹!鍩庡競鍐呫�嶏紙婧� city_taxi_monthly锛�
+ for (CityTaxiMonthly t : cityTaxiMapper.selectList(null)) {
+ if (t.getReportPeriod() == null || t.getCity() == null) continue;
+ int key = midPeriodKey(t.getReportPeriod());
+ if (key <= 0) continue;
+ String city = RegionUtil.normalizeCityName(t.getCity());
+ if (city == null) continue;
+ double pass = nz(t.getPassengerVolume()) - nz(t.getPassengerCity());
+ double turn = nz(t.getTurnover()) - nz(t.getTurnoverCity());
+ addMidClass(mid, key, city, 3, pass, turn);
+ addMidClass(mid, key, "婀栧寳鐪�", 3, pass, turn);
+ markMid(pres, key, city, 3);
+ markMid(pres, key, "婀栧寳鐪�", 3);
+ }
+ // 缁村害4锛氬煄闄呭煄涔$綉绾﹁溅 = 缃戠害杞︽媶鍒嗙粨鏋滐紙涓庡彴璐︺�岀綉绾﹁溅銆嶉〉鍚屾簮鍚屾硶锛�
+ for (WycOrderMonthly o : wycOrderMapper.selectList(null)) {
+ String per = o.getReportPeriod();
+ if (per == null) continue;
+ int key = midPeriodKey(per);
+ if (key <= 0) continue;
+ WycSplitCalc.WycResult wr;
+ try {
+ wr = wycSplitCalc.calc(per);
+ } catch (Exception e) {
+ // B5锛氫笉鍐嶉潤榛樿烦杩団�斺�旇褰曘�屾湡 + 鍘熷洜銆嶅苟鍐欐棩蹇楋紝闅忓鍑轰綋妫�涓�骞惰緭鍑�
+ String msg = per + " 缃戠害杞﹀煄闄呭煄涔$淮搴︾己澶憋細" + e.getMessage();
+ midIssues.get().add(msg);
+ log.warn("loadMidClassMap {}", msg);
+ continue;
+ }
+ for (Map.Entry<String, WycSplitCalc.WycMetrics> en : wr.getByCity().entrySet()) {
+ WycSplitCalc.WycMetrics wm = en.getValue();
+ addMidClass(mid, key, en.getKey(), 4, wm.getSuburbanPax(), wm.getSuburbanTurnover());
+ addMidClass(mid, key, "婀栧寳鐪�", 4, wm.getSuburbanPax(), wm.getSuburbanTurnover());
+ markMid(pres, key, en.getKey(), 4);
+ markMid(pres, key, "婀栧寳鐪�", 4);
+ }
}
return mid;
+ }
+
+ /** "yyyy-MM" -> yyyy*100+MM锛涢潪娉曡繑鍥� 0锛堜笌 loadMidClassMap 鐨� key 鍙e緞涓�鑷达級 */
+ private int midPeriodKey(String period) {
+ if (period == null) return 0;
+ String[] parts = period.trim().split("-");
+ if (parts.length != 2) return 0;
+ try {
+ int y = Integer.parseInt(parts[0]);
+ int m = Integer.parseInt(parts[1]);
+ if (y <= 0 || m < 1 || m > 12) return 0;
+ return y * 100 + m;
+ } catch (NumberFormatException e) {
+ return 0;
+ }
}
/** 鍐欏叆鍒嗙被鍊煎苟绱姞鎬婚噺锛堢淮搴�0锛� */
@@ -2317,12 +2629,12 @@
int monthCount = (y == currentYear) ? maxMonth : 12;
for (int m = 1; m <= monthCount; m++) {
Double cur = midMetricValue(aggOf(data, y, m, region), i, volume);
- setNumeric(row, col++, cur == null ? null : round(cur, 4));
+ setNumeric(row, col++, cur);
Double base = midMetricValue(aggOf(data, y - 1, m, region), i, volume);
setNumeric(row, col++, yoy(cur, base));
}
Double cum = midCumOf(data, y, region, monthCount, i, volume);
- setNumeric(row, col++, cum == null ? null : round(cum, 4));
+ setNumeric(row, col++, cum);
Double cumBase = midCumOf(data, y - 1, region, monthCount, i, volume);
setNumeric(row, col++, yoy(cum, cumBase));
}
@@ -2369,9 +2681,19 @@
* 鐢熸垚_涓彛寰勬帓鍚�.xlsx锛氬崟鍧楃疮璁★紙閫� 1 涓湀=褰撴湀锛岄�� 1-6 鏈�=绱锛夛紝RANK/SUM 鍏紡淇濈暀銆�
*/
public byte[] exportPassengerMidRank(String period, String mode) throws Exception {
+ return exportPassengerMidRank(period, mode, null);
+ }
+
+ /**
+ * 鏂规 A锛�2026-09-21锛夛細涓彛寰勬帓鍚嶇殑绱鍚屾瘮鍩烘暟涓庘�滀腑鍙e緞鏄庣粏鈥濅繚鎸佸悓婧愩��
+ * 鏄庣粏椤电殑 2025 骞村垪鏄瘝鐗堥潤鎬佸畾绋垮�硷紝鎺掑悕椤靛師鍏堟煡搴撶幇绠楋紝涓よ�呭湪 2025-03/04 缃戠害杞︽媶鍒嗕笂涓嶅悓銆�
+ * summaryWb 闈炵┖鏃剁洿鎺ュ鐢ㄥ綋鍓嶆鍦ㄧ敓鎴愮殑姹囨�诲伐浣滅翱锛涚嫭绔嬪鍑烘椂鍐嶆墦寮�瀵瑰簲姣嶇増銆�
+ */
+ private byte[] exportPassengerMidRank(String period, String mode, XSSFWorkbook summaryWb) throws Exception {
int maxMonth = monthOf(period, mode);
int currentYear = Integer.parseInt(period.substring(0, 4));
File template = resolvePassengerTemplate("鐢熸垚_涓彛寰勬帓鍚�.xlsx");
+ Map<String, double[]> lastYearCumBase = loadMidRankStaticBase(period, mode, summaryWb, currentYear, maxMonth);
try (InputStream in = new FileInputStream(template);
XSSFWorkbook wb = new XSSFWorkbook(in)) {
Sheet sheet = wb.getSheetAt(0);
@@ -2391,15 +2713,11 @@
title.getCell(10).setCellValue(currentYear + "骞�" + maxMonth + "鏈堝叏鐪佸垎甯傚窞瀹屾垚閬撹矾瀹㈣繍鐢熶骇鎯呭喌");
}
// 鍏ㄧ渷琛� r3锛欱3/F3 涓� SUM 鍏紡淇濈暀锛汥3/H3 鍚屾瘮濉�硷紙0-based 鍒� 3/7锛�
- setValOrBlank(sheet, 2, 3, yoyOf(midCumClass(mid, currentYear, "婀栧寳鐪�", maxMonth, 0, true),
- midCumClass(mid, currentYear - 1, "婀栧寳鐪�", maxMonth, 0, true)));
- setValOrBlank(sheet, 2, 7, yoyOf(midCumClass(mid, currentYear, "婀栧寳鐪�", maxMonth, 0, false),
- midCumClass(mid, currentYear - 1, "婀栧寳鐪�", maxMonth, 0, false)));
+ setValOrBlank(sheet, 2, 3, midRankCumYoy(mid, currentYear, "婀栧寳鐪�", maxMonth, true, lastYearCumBase));
+ setValOrBlank(sheet, 2, 7, midRankCumYoy(mid, currentYear, "婀栧寳鐪�", maxMonth, false, lastYearCumBase));
// 鍙冲潡鍏ㄧ渷琛� N3/R3锛氬綋鏈堝悓姣旓紙L3/P3 涓� SUM 鍏紡淇濈暀锛�
- setValOrBlank(sheet, 2, 13, yoyOf(midClassVal(mid, currentYear, maxMonth, "婀栧寳鐪�", 0, true),
- midClassVal(mid, currentYear - 1, maxMonth, "婀栧寳鐪�", 0, true)));
- setValOrBlank(sheet, 2, 17, yoyOf(midClassVal(mid, currentYear, maxMonth, "婀栧寳鐪�", 0, false),
- midClassVal(mid, currentYear - 1, maxMonth, "婀栧寳鐪�", 0, false)));
+ setValOrBlank(sheet, 2, 13, midMonthYoy(mid, currentYear, maxMonth, "婀栧寳鐪�", 0, true));
+ setValOrBlank(sheet, 2, 17, midMonthYoy(mid, currentYear, maxMonth, "婀栧寳鐪�", 0, false));
// 甯傚窞琛� r4-20锛欱/F 绱鍊笺�丏/H 鍚屾瘮锛汣/E/G/I 鎺掑悕鍏紡淇濈暀
List<String> cities = RegionUtil.CITY_LIST;
for (int i = 0; i < cities.size(); i++) {
@@ -2407,25 +2725,127 @@
int r0 = 3 + i;
double pass = midCumClass(mid, currentYear, city, maxMonth, 0, true);
double turn = midCumClass(mid, currentYear, city, maxMonth, 0, false);
- double lastPass = midCumClass(mid, currentYear - 1, city, maxMonth, 0, true);
- double lastTurn = midCumClass(mid, currentYear - 1, city, maxMonth, 0, false);
- setValOrBlank(sheet, r0, 1, pass == 0 ? null : round(pass, 4));
- setValOrBlank(sheet, r0, 3, yoyOf(pass, lastPass));
- setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4));
- setValOrBlank(sheet, r0, 7, yoyOf(turn, lastTurn));
+ setValOrBlank(sheet, r0, 1, pass == 0 ? null : pass);
+ setValOrBlank(sheet, r0, 3, midRankCumYoy(mid, currentYear, city, maxMonth, true, lastYearCumBase));
+ setValOrBlank(sheet, r0, 5, turn == 0 ? null : turn);
+ setValOrBlank(sheet, r0, 7, midRankCumYoy(mid, currentYear, city, maxMonth, false, lastYearCumBase));
// 鍙冲潡褰撴湀锛歀/P 鍊笺�丯/R 褰撴湀鍚屾瘮锛圡/O/Q/S 鎺掑悕鍏紡淇濈暀锛�
double mPass = midClassVal(mid, currentYear, maxMonth, city, 0, true);
double mTurn = midClassVal(mid, currentYear, maxMonth, city, 0, false);
- double mLastPass = midClassVal(mid, currentYear - 1, maxMonth, city, 0, true);
- double mLastTurn = midClassVal(mid, currentYear - 1, maxMonth, city, 0, false);
- setValOrBlank(sheet, r0, 11, mPass == 0 ? null : round(mPass, 4));
- setValOrBlank(sheet, r0, 13, mPass == 0 ? null : yoyOf(mPass, mLastPass));
- setValOrBlank(sheet, r0, 15, mTurn == 0 ? null : round(mTurn, 4));
- setValOrBlank(sheet, r0, 17, mTurn == 0 ? null : yoyOf(mTurn, mLastTurn));
+ setValOrBlank(sheet, r0, 11, mPass == 0 ? null : mPass);
+ setValOrBlank(sheet, r0, 13, mPass == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, city, 0, true));
+ setValOrBlank(sheet, r0, 15, mTurn == 0 ? null : mTurn);
+ setValOrBlank(sheet, r0, 17, mTurn == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, city, 0, false));
}
recalc(wb);
return toBytes(wb);
}
+ }
+
+ /** 璇诲彇姣嶇増銆婁腑鍙e緞鏄庣粏銆嬬殑 2025 骞撮�愭湀闈欐�佸�硷紝姹囨�绘垚鎺掑悕椤垫墍闇�鐨勫幓骞村悓鏈熺疮璁″熀鏁般�� */
+ private Map<String, double[]> readMidDetailLastYearCumulative(XSSFWorkbook sourceWb, int currentYear, int maxMonth) {
+ Map<String, double[]> out = new LinkedHashMap<>();
+ if (sourceWb == null || maxMonth <= 0) return out;
+ Sheet sheet = sourceWb.getSheet("涓彛寰勬槑缁�");
+ if (sheet == null) return out;
+ Map<Integer, Integer> monthCols = new LinkedHashMap<>();
+ java.util.regex.Pattern monthHeader = java.util.regex.Pattern.compile("^(\\d{4})骞�(\\d{1,2})鏈�$");
+ int headerEnd = Math.min(sheet.getLastRowNum(), sheet.getFirstRowNum() + 5);
+ for (int r = sheet.getFirstRowNum(); r <= headerEnd; r++) {
+ Row row = sheet.getRow(r);
+ if (row == null) continue;
+ for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) {
+ String text = cellText(row.getCell(c));
+ if (text == null) continue;
+ java.util.regex.Matcher matcher = monthHeader.matcher(text.trim());
+ if (!matcher.matches()) continue;
+ int y = Integer.parseInt(matcher.group(1));
+ int m = Integer.parseInt(matcher.group(2));
+ if (y == currentYear - 1 && m >= 1 && m <= maxMonth) monthCols.putIfAbsent(m, c);
+ }
+ }
+ if (monthCols.size() < maxMonth) return out;
+ for (int r = sheet.getFirstRowNum(); r <= sheet.getLastRowNum(); r++) {
+ Row row = sheet.getRow(r);
+ if (row == null) continue;
+ String city = RegionUtil.normalizeCityName(cellText(row.getCell(0)));
+ if (city == null || !RegionUtil.CITY_LIST.contains(city)) continue;
+ String metric = cellText(row.getCell(1));
+ if (metric == null) continue;
+ int idx = metric.contains("鎬绘梾瀹㈠懆杞噺") ? 1 : (metric.contains("鎬诲杩愰噺") ? 0 : -1);
+ if (idx < 0) continue;
+ double sum = 0.0;
+ boolean complete = true;
+ for (int m = 1; m <= maxMonth; m++) {
+ Cell c = row.getCell(monthCols.get(m));
+ if (c == null) { complete = false; break; }
+ if (c.getCellType() == CellType.NUMERIC) {
+ sum += c.getNumericCellValue();
+ } else if (c.getCellType() == CellType.FORMULA && c.getCachedFormulaResultType() == CellType.NUMERIC) {
+ sum += c.getNumericCellValue();
+ } else {
+ complete = false;
+ break;
+ }
+ }
+ double[] values = out.computeIfAbsent(city, k -> new double[]{Double.NaN, Double.NaN});
+ values[idx] = complete ? sum : Double.NaN;
+ }
+ double[] province = new double[2];
+ for (String city : RegionUtil.CITY_LIST) {
+ double[] values = out.get(city);
+ if (values == null || Double.isNaN(values[0]) || Double.isNaN(values[1])) return new LinkedHashMap<>();
+ province[0] += values[0];
+ province[1] += values[1];
+ }
+ out.put("婀栧寳鐪�", province);
+ return out;
+ }
+
+ /** 鐙珛瀵煎嚭鏃朵粠瀵瑰簲姹囨�绘瘝鐗堣鍙栭潤鎬佸熀鏁帮紱姹囨�诲鍑烘椂鐩存帴澶嶇敤鍐呭瓨涓殑宸ヤ綔绨匡紝閬垮厤閲嶅 I/O銆� */
+ private Map<String, double[]> loadMidRankStaticBase(String period, String mode, XSSFWorkbook summaryWb,
+ int currentYear, int maxMonth) {
+ if (summaryWb != null) {
+ Map<String, double[]> base = readMidDetailLastYearCumulative(summaryWb, currentYear, maxMonth);
+ if (!base.isEmpty()) return base;
+ log.warn("涓彛寰勬帓鍚嶏細褰撳墠姹囨�诲伐浣滅翱鐨勪腑鍙e緞鏄庣粏鏈鍒板畬鏁� 2025 闈欐�佺疮璁″熀鏁帮紝鍥為��搴撳唴鐜扮畻");
+ return null;
+ }
+ double savedZipRatio = ZipSecureFile.getMinInflateRatio();
+ ZipSecureFile.setMinInflateRatio(0.0001);
+ try {
+ File mother = resolveSummaryMother(period, mode);
+ if (mother == null || !mother.isFile()) {
+ log.warn("涓彛寰勬帓鍚嶏細鏈壘鍒� {} 鐨勬眹鎬绘瘝鐗堬紝2025 绱鍩烘暟鍥為��搴撳唴鐜扮畻", period);
+ return null;
+ }
+ try (InputStream in = new FileInputStream(mother); XSSFWorkbook wb = new XSSFWorkbook(in)) {
+ Map<String, double[]> base = readMidDetailLastYearCumulative(wb, currentYear, maxMonth);
+ if (base.isEmpty()) {
+ log.warn("涓彛寰勬帓鍚嶏細姣嶇増 {} 鐨勪腑鍙e緞鏄庣粏鏈鍒板畬鏁� 2025 闈欐�佺疮璁″熀鏁帮紝鍥為��搴撳唴鐜扮畻", mother.getName());
+ return null;
+ }
+ log.info("涓彛寰勬帓鍚嶏細2025 绱鍩烘暟鏀圭敤姣嶇増闈欐�佸垪锛坽}锛�", mother.getName());
+ return base;
+ }
+ } catch (Exception e) {
+ log.warn("涓彛寰勬帓鍚嶏細璇诲彇姣嶇増 2025 闈欐�佺疮璁″熀鏁板け璐ワ紝鍥為��搴撳唴鐜扮畻锛歿}", e.getMessage());
+ return null;
+ } finally {
+ ZipSecureFile.setMinInflateRatio(savedZipRatio);
+ }
+ }
+
+ /** 涓彛寰勬帓鍚嶇疮璁″悓姣旓細鏂规 A 鏈夐潤鎬佸熀鏁版椂涓庡叾瀵归綈锛屽惁鍒欎繚鐣欏師搴撳唴鐜扮畻閫昏緫銆� */
+ private Double midRankCumYoy(Map<Integer, Map<String, double[][]>> mid, int year, String area,
+ int monthCount, boolean volume, Map<String, double[]> staticBase) {
+ if (staticBase == null) return midCumYoy(mid, year, area, monthCount, 0, volume);
+ double[] values = staticBase.get(area);
+ if (values == null) return null;
+ double current = midCumClass(mid, year, area, monthCount, 0, volume);
+ if (current == 0.0) return null;
+ double base = volume ? values[0] : values[1];
+ return base == 0.0 ? null : yoyOf(current, base);
}
/** 鍐欐暟鍊兼垨娓呯┖锛堝叕寮忓崟鍏冩牸淇濈暀涓嶅姩锛� */
@@ -2440,7 +2860,7 @@
private Double yoyOf(double cur, double base) {
if (base == 0.0) return null;
- return round((cur - base) / base, 4);
+ return (cur - base) / base;
}
/** 涓彛寰勫垎绫荤疮璁★紙1..monthCount 鏈堟眰鍜岋級 */
@@ -2482,10 +2902,10 @@
double turn = midCumClass(mid, currentYear, "婀栧寳鐪�", maxMonth, dims[i], false);
double lastPass = midCumClass(mid, currentYear - 1, "婀栧寳鐪�", maxMonth, dims[i], true);
double lastTurn = midCumClass(mid, currentYear - 1, "婀栧寳鐪�", maxMonth, dims[i], false);
- setValOrBlank(sheet, r0, 1, pass == 0 ? null : round(pass, 4));
- setValOrBlank(sheet, r0, 2, yoyOf(pass, lastPass));
- setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4));
- setValOrBlank(sheet, r0, 6, yoyOf(turn, lastTurn));
+ setValOrBlank(sheet, r0, 1, pass == 0 ? null : pass);
+ setValOrBlank(sheet, r0, 2, midCumYoy(mid, currentYear, "婀栧寳鐪�", maxMonth, dims[i], true));
+ setValOrBlank(sheet, r0, 5, turn == 0 ? null : turn);
+ setValOrBlank(sheet, r0, 6, midCumYoy(mid, currentYear, "婀栧寳鐪�", maxMonth, dims[i], false));
}
// 涓嬪崐鍧楋紙褰撴湀锛夛細鏍囬琛�9銆佹暟鎹11-15锛�0-based 8銆�10-14锛�
Row bottomTitle = sheet.getRow(8);
@@ -2498,10 +2918,10 @@
double turn = midClassVal(mid, currentYear, maxMonth, "婀栧寳鐪�", dims[i], false);
double lastPass = midClassVal(mid, currentYear - 1, maxMonth, "婀栧寳鐪�", dims[i], true);
double lastTurn = midClassVal(mid, currentYear - 1, maxMonth, "婀栧寳鐪�", dims[i], false);
- setValOrBlank(sheet, r0, 1, pass == 0 ? null : round(pass, 4));
- setValOrBlank(sheet, r0, 2, pass == 0 ? null : yoyOf(pass, lastPass));
- setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4));
- setValOrBlank(sheet, r0, 6, turn == 0 ? null : yoyOf(turn, lastTurn));
+ setValOrBlank(sheet, r0, 1, pass == 0 ? null : pass);
+ setValOrBlank(sheet, r0, 2, pass == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, "婀栧寳鐪�", dims[i], true));
+ setValOrBlank(sheet, r0, 5, turn == 0 ? null : turn);
+ setValOrBlank(sheet, r0, 6, turn == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, "婀栧寳鐪�", dims[i], false));
}
// 涓嬪崐鍧楀崰姣斿叕寮忕敱鏍蜂緥鎸囧悜涓婂崐鍧楋紙B$3/F$3锛夛紝鏀瑰啓涓烘湰鍧楁�昏锛圔$11/F$11锛夛紝琛�12-15
for (int i = 1; i < names.length; i++) {
@@ -2918,7 +3338,7 @@
Cell c = row.getCell(colIdx);
if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) return;
if (c == null) c = row.createCell(colIdx);
- if (v == 0) c.setBlank(); else c.setCellValue(round(v, 2));
+ if (v == 0) c.setBlank(); else c.setCellValue(v);
}
/** 鏄惧紡鍐欏叆 0锛坰etDataCell 瀵� 0 缃┖锛涙眹鎬昏〃缃戠害杞﹁闇�瑕佹樉绀� 0锛� */
@@ -3199,7 +3619,7 @@
* 淇濈暀鍏ㄩ儴椤电/鐗堝紡/鍏紡/2025 骞寸紦瀛樺�硷紝浠呮妸琛ㄥご骞翠唤鍔ㄦ�佸寲涓虹洰鏍囧勾锛涙暟鎹~鍏呮寜 P2 璧烽�愭鎺ュ叆銆�
*/
public byte[] exportSummaryWorkbook(String period, String mode) throws Exception {
- return exportSummaryWorkbook(period, mode, null);
+ return exportSummaryWorkbook(period, mode, null, null);
}
/** 鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx锛圥0-P1 楠ㄦ灦锛屾敮鎸� toMonth 瑁佸壀锛夛細
@@ -3207,14 +3627,25 @@
* toMonth 闈炵┖涓斿皬浜� M 鏃讹紝鎶� 8 涓彴璐�/鍚堟垚椤佃鍒� toMonth 鏈堬紙toMonth+1..M 鐨勫綋鏈堝��/鍚屾瘮鍒楁竻绌恒�佷繚鐣欒〃澶达紝
* 绱涓庣疮璁″悓姣斿叕寮忔敼涓烘寜 toMonth 鏈堥噸绠楋級锛岀敤浜庘�滃厛鐢熸垚 1-6 鏈堛��1-7 鏈堟墿鍒椻�濋獙璇併�� */
public byte[] exportSummaryWorkbook(String period, String mode, Integer toMonth) throws Exception {
+ return exportSummaryWorkbook(period, mode, toMonth, null);
+ }
+
+ /** 鍚屼笂锛岄澶栭�氳繃 progress 鍥炶皟鎶ュ憡杩涘害锛堝紓姝ュ鍑轰换鍔$敤锛氭楠ょ櫨鍒嗘瘮 + 姝ラ鏂囨锛� */
+ public byte[] exportSummaryWorkbook(String period, String mode, Integer toMonth,
+ java.util.function.BiConsumer<Integer, String> progress) throws Exception {
+ reportProgress(progress, 2, "鏍¢獙鎶ヨ〃鏈熶笌姣嶇増");
int year = Integer.parseInt(period.contains("-") ? period.split("-")[0] : period);
int month = period.contains("-") ? monthOf(period, mode) : 12;
File template = resolveSummaryMother(period, mode);
if (template == null) {
throw new RuntimeException("鏈壘鍒版眹鎬昏〃姣嶇増銆�" + year + "骞�" + month + "鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx銆嬶紙docs/鐢熸垚姹囨�诲ぇ琛級");
}
+ double savedZipRatio = ZipSecureFile.getMinInflateRatio();
+ reportProgress(progress, 6, "璇诲彇姣嶇増宸ヤ綔绨�");
+ ZipSecureFile.setMinInflateRatio(0.0001); // 鏂扮増姣嶇増 styles.xml 鐢� openpyxl 楂樺帇缂╋紙0.0099<0.01锛夎Е鍙� POI Zip-bomb 璇姤锛岃鍙栧悗杩樺師
try (InputStream in = new FileInputStream(template);
XSSFWorkbook wb = new XSSFWorkbook(in)) {
+ reportProgress(progress, 12, "杩樺師鍚屾湡鏈堝害鍧�");
File donor = resolveSummaryDonor(year, month);
if (donor != null) {
try (InputStream din = new FileInputStream(donor); XSSFWorkbook dwb = new XSSFWorkbook(din)) {
@@ -3229,11 +3660,915 @@
log.info("姹囨�诲伐浣滅翱瑁佸壀鑷� {} 鏈堝畬鎴愶紝娓呯┖/鏀瑰啓鍗曞厓鏍兼暟={}", toMonth, trimmed);
}
dynamicSummaryYear(wb, year);
- applyTwoDecimalFormat(wb); // 姹囨�诲ぇ琛ㄤ繚鐣欎汉宸ユ瘝鐗堝垪瀹�/鐗堝紡锛氫笉鍋� autoFit锛堥暱鍏紡浼氭妸鍒楀椤剁垎锛�
+ cleanSummarySheetPresentation(wb); // 鍙栨秷鍚勯〉绛涢�夈�佽繕鍘熼殣钘忚锛堝惁鍒欌�滅彮绾垮寘杞︹�濈瓑椤电湅璧锋潵鍍忕己鏁版嵁锛�
+ int effMonth = (toMonth != null && toMonth > 0 && toMonth < month) ? toMonth : month;
+ List<String> dbProblems = new ArrayList<>();
+ String effPeriod = String.format("%04d-%02d", year, effMonth);
+ 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);
+ reportProgress(progress, 45, "鍒锋柊鎺掑悕椤典笌璺ㄩ〉鍚堟垚椤�");
+ int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month");
+ int cumulativeRefreshed = refreshSummaryCumulativeSheets(wb, year, effMonth); // 鍥涘紶璺ㄩ〉鍚堟垚椤甸殢鎶ヨ〃鏈熷埛鏂�
+ int widthApplied = applySummaryReferenceLayout(wb);
+ log.info("姹囨�诲伐浣滅翱鏁版嵁椹卞姩鍥炲~瀹屾垚锛氬啓鍏ュ綋鏈堝�兼牸鏁�={}锛岃ˉ鍐欏綋鏈堝悓姣斿叕寮忔牸鏁�={}锛岃ˉ鍐欏綋鏈堝悎璁″叕寮忔牸鏁�={}锛�"
+ + "淇绱鍚屾瘮鍒嗘瘝鏍兼暟={}锛屽埛鏂版帓鍚嶉〉鏁板�兼牸鏁�={}锛屽埛鏂板洓椤碉紙鍩庡競姹囨��/鍩庡競鍒嗗競宸�/涓彛寰勫垎鏋�/閬撹矾杩愯緭鍛ㄨ浆閲忥級鏁板�兼牸鏁�={}锛�"
+ + "鎸夈�婄敓鎴恄閬撹矾杩愯緭閲忔眹鎬昏〃銆嬪榻愬垪瀹藉垪鏁�={}锛岀己鏁版彁绀�={}",
+ dbFilled, yoyFilled, monthFormulaFilled, cumYoyFixed, 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 鎵嶈兘琚瑙堜笌浜屾澶勭悊璇诲埌
+ log.info("姹囨�诲伐浣滅翱鍏紡閲嶇畻瀹屾垚锛氭垚鍔� {} 鏍硷紝澶辫触 {} 鏍硷紙澶辫触鏍间繚鐣欐瘝鐗堟棫缂撳瓨鍊硷紝鎵撳紑鏃舵寜 fullCalcOnLoad 閲嶇畻锛�", recalc[0], recalc[1]);
if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 杩樺師/鏀瑰啓鍏紡鍚庢墦寮�鍗抽噸绠�
+ reportProgress(progress, 95, "鍐欏嚭鏂囦欢");
return toBytes(wb);
+ } finally {
+ ZipSecureFile.setMinInflateRatio(savedZipRatio);
}
+ }
+
+ /** 瀵煎嚭杩涘害鍥炶皟锛堝紓姝ュ鍑轰换鍔$敤锛夛細鍥炶皟鑷韩寮傚父涓嶅奖鍝嶅鍑� */
+ private static void reportProgress(java.util.function.BiConsumer<Integer, String> progress, int percent, String step) {
+ if (progress == null) return;
+ try {
+ progress.accept(percent, step);
+ } catch (Exception ignore) {
+ // 杩涘害鍥炶皟澶辫触涓嶅奖鍝嶅鍑�
+ }
+ }
+
+ /**
+ * 姹囨�诲伐浣滅翱鍏紡閲嶇畻锛歅OI 鍐欐枃浠朵笉甯︾紦瀛樺�硷紝瀵艰嚧銆屽叏鐪佽锛埼�17甯傚窞锛夈�嶃�岀疮璁″垪銆嶃�屾帓鍚� SUM銆�
+ * 鍦ㄩ瑙�/浜屾澶勭悊閲屽彧鐪嬪埌姣嶇増涓婁竴鏈熺殑鏃у�硷紙2026-09-18 鐢ㄦ埛鎶モ��8鏈堟病鏁版嵁 / 鍏ㄧ渷琛屽仠鍦ㄤ笂涓�鏈熲�濈殑鏍瑰洜涔嬩竴锛夈��
+ * 閫愭牸 evaluate 骞舵妸缁撴灉鍐欏洖缂撳瓨鍊硷紱涓埆涓嶆敮鎸佸嚱鏁�/澶栭儴寮曠敤鏍煎け璐ュ垯璺宠繃骞惰鏁帮紝涓嶉樆濉炲鍑恒��
+ */
+ private int[] recalcWorkbookFormulas(XSSFWorkbook wb) {
+ if (wb == null) return new int[]{0, 0};
+ org.apache.poi.ss.usermodel.FormulaEvaluator ev;
+ try {
+ ev = wb.getCreationHelper().createFormulaEvaluator();
+ } catch (Exception e) {
+ log.warn("姹囨�诲伐浣滅翱鍏紡閲嶇畻锛氬垱寤烘眰鍊煎櫒澶辫触锛岃烦杩囷紙{}锛�", e.getMessage());
+ return new int[]{0, 0};
+ }
+ try {
+ // 璺ㄧ翱鍏紡锛圼1]鍑虹!C13 涔嬬被锛夊湪澶栭儴宸ヤ綔绨跨己澶辨椂榛樿鎶涘紓甯革紝浼氳寮曠敤瀹冪殑
+ // 鎬昏/绱/娲剧敓鍏紡杩為攣澶辫触锛堣〃鐜颁负銆屽叕寮忚繕鍦ㄤ絾娌℃湁缂撳瓨鍊笺�嶏級銆�
+ // 缃� true 鍚庢敼鐢ㄥ叕寮忔牸鑷韩鐨勭紦瀛樺�硷紙鍗冲畾绋垮�硷級鍙備笌璁$畻銆�
+ ev.setIgnoreMissingWorkbooks(true);
+ } catch (Exception ignore) {
+ // 涓埆瀹炵幇涓嶆敮鎸佽寮�鍏筹紝蹇界暐鍗冲彲
+ }
+ int ok = 0, fail = 0;
+ java.util.List<String> samples = new java.util.ArrayList<>();
+ for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+ Sheet sh = wb.getSheetAt(i);
+ if (sh == null) continue;
+ for (Row row : sh) {
+ if (row == null) continue;
+ for (Cell c : row) {
+ if (c == null || c.getCellType() != CellType.FORMULA) continue;
+ try {
+ ev.evaluateFormulaCell(c); // 娉ㄦ剰锛歟valuate() 鏃犲壇浣滅敤锛屽繀椤荤敤 evaluateFormulaCell 鎵嶄細鎶婄粨鏋滃啓鍥炵紦瀛樺��
+ ok++;
+ } catch (Exception ex) {
+ fail++;
+ if (samples.size() < 10) {
+ samples.add(sh.getSheetName() + "!" + c.getAddress() + " :: " + ex.getClass().getSimpleName());
+ }
+ }
+ }
+ }
+ }
+ if (!samples.isEmpty()) log.warn("姹囨�诲伐浣滅翱鍏紡閲嶇畻澶辫触鏍蜂緥锛歿}", samples);
+ return new int[]{ok, fail};
+ }
+
+ /**
+ * 娓呮帀銆岄潪 inlineStr 鍗存畫鐣� <is>銆嶇殑闈炴硶鍗曞厓鏍笺��
+ *
+ * 姣嶇増鑻ョ敱 openpyxl 鍐欏嚭锛屾枃鏈崟鍏冩牸鐢� <is>锛坕nlineStr锛夊瓨鍌紱POI 鐨� setCellValue 鍙敼鍐� t 涓� <v>锛�
+ * 涓嶄細娓呮帀鏃� <is>锛屼簬鏄骇鍑� <c t="n"><v>67.0</v><is><t>67.00</t></is></c> 杩欑闈炴硶鏍硷紝
+ * Excel 鍒ゅ畾涓恒�屾棤娉曡鍙栫殑鍐呭銆嶈�岀洿鎺ユ嫆缁濇墦寮�锛圕OM 琛ㄧ幇涓恒�屼笉鑳藉彇寰楃被 Workbooks 鐨� Open 灞炴�с�嶏級銆�
+ * 2026-07 瀵煎嚭浠跺嵆鍥犳鎵撲笉寮�锛堛�婂叕浜ゃ��17 鏍� +銆婅建閬撱�佽疆娓°��2 鏍硷級銆傝惤鐩樺墠缁熶竴娓呯悊锛屽箓绛夈��
+ */
+ private int normalizeStaleInlineStrings(XSSFWorkbook wb) {
+ int fixed = 0;
+ for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+ org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorksheet ctw = wb.getSheetAt(i).getCTWorksheet();
+ if (ctw == null || ctw.getSheetData() == null) continue;
+ for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRow row : ctw.getSheetData().getRowArray()) {
+ for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCell c : row.getCArray()) {
+ if (!c.isSetIs()) continue;
+ STCellType.Enum t = c.getT();
+ if (t == null || !STCellType.INLINE_STR.equals(t)) {
+ c.unsetIs();
+ fixed++;
+ }
+ }
+ }
+ }
+ return fixed;
+ }
+
+ /**
+ * 鐗堝紡瀵归綈锛氭寜 docs/鐢熸垚姹囨�诲ぇ琛�/銆婄敓鎴恄閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx銆嬶紙鐢ㄦ埛鎻愪緵鐨勭増寮忓弬鐓э級閫愬垪濂楃敤鍒楀涓庨殣钘忔爣璁帮紝
+ * 浣跨敓鎴愪欢涓庡弬鐓т欢鈥滄牸寮忓畬鍏ㄤ竴鑷粹�濓紝骞惰姣嶇増娌″畾涔夊搴︾殑鈥滃悗缁墿鍒椻�濓紙濡� 8 鏈堝垪锛変篃鏈変笌 8 鏈堜竴鑷寸殑鍒楀銆�
+ * 鍙傜収浠剁己澶辨椂闈欓粯璺宠繃锛堣繑鍥� 0锛夛紝涓嶅奖鍝嶅師鏈夊鍑恒��
+ */
+ private int applySummaryReferenceLayout(XSSFWorkbook wb) {
+ File ref;
+ try {
+ ref = resolveSummaryLayoutRef();
+ } catch (Exception e) {
+ return 0;
+ }
+ if (ref == null) return 0;
+ double savedZipRatio = ZipSecureFile.getMinInflateRatio();
+ ZipSecureFile.setMinInflateRatio(0.0001);
+ int applied = 0;
+ try (InputStream in = new FileInputStream(ref); XSSFWorkbook rwb = new XSSFWorkbook(in)) {
+ for (int i = 0; i < rwb.getNumberOfSheets(); i++) {
+ org.apache.poi.xssf.usermodel.XSSFSheet rs = rwb.getSheetAt(i);
+ if (rs == null) continue;
+ org.apache.poi.xssf.usermodel.XSSFSheet ts = wb.getSheet(rs.getSheetName());
+ if (ts == null) continue;
+ int maxCol = Math.min(usedColumnCount(rs), usedColumnCount(ts) + 4);
+ if (maxCol <= 0) continue;
+ double[] width = new double[maxCol];
+ boolean[] hasWidth = new boolean[maxCol];
+ boolean[] hidden = new boolean[maxCol];
+ for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCols cols : rs.getCTWorksheet().getColsArray()) {
+ for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCol col : cols.getColArray()) {
+ long min = col.getMin();
+ long max = col.getMax();
+ for (long c = min; c <= max; c++) {
+ int idx = (int) (c - 1);
+ if (idx < 0 || idx >= maxCol) {
+ if (idx >= maxCol) break;
+ continue;
+ }
+ if (col.isSetWidth()) {
+ width[idx] = col.getWidth();
+ hasWidth[idx] = true;
+ }
+ hidden[idx] = col.isSetHidden() && col.getHidden();
+ }
+ }
+ }
+ for (int c = 0; c < maxCol; c++) {
+ if (hasWidth[c]) {
+ int w = (int) Math.round(width[c] * 256d);
+ if (ts.getColumnWidth(c) != w) {
+ ts.setColumnWidth(c, w);
+ applied++;
+ }
+ }
+ if (hidden[c] != ts.isColumnHidden(c)) ts.setColumnHidden(c, hidden[c]);
+ }
+ }
+ log.info("姹囨�诲伐浣滅翱鐗堝紡瀵归綈銆婄敓鎴恄閬撹矾杩愯緭閲忔眹鎬昏〃銆嬪畬鎴愶細濂楃敤鍒楀 {} 鍒楋紙鍙傜収浠讹細{}锛�", applied, ref.getName());
+ } catch (Exception e) {
+ log.warn("姹囨�诲伐浣滅翱鐗堝紡瀵归綈澶辫触锛堝拷鐣ワ紝缁х画鎸夋瘝鐗堢増寮忚緭鍑猴級锛歿}", e.getMessage());
+ return 0;
+ } finally {
+ ZipSecureFile.setMinInflateRatio(savedZipRatio);
+ }
+ return applied;
+ }
+
+ /** 涓�椤靛凡鐢ㄥ埌鐨勬渶澶у垪鏁帮紙0 鍩鸿鏁帮級 */
+ private int usedColumnCount(org.apache.poi.ss.usermodel.Sheet sh) {
+ int max = 0;
+ for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
+ org.apache.poi.ss.usermodel.Row row = sh.getRow(r);
+ if (row == null) continue;
+ if (row.getLastCellNum() > max) max = row.getLastCellNum();
+ }
+ return max;
+ }
+
+ // ==================== 姹囨�诲伐浣滅翱鍐呯殑鎺掑悕椤靛埛鏂� ====================
+
+ /**
+ * 姹囨�诲伐浣滅翱閲岀殑銆岃揣杩愰噺鎺掑悕 / 璐ц繍鍛ㄨ浆閲忔帓鍚� / 涓彛寰勬帓鍚嶃�嶅湪姣嶇増閲屾槸涓婁竴鎶ヨ〃鏈熺殑闈欐�佸�硷紝
+ * 瀵煎嚭鏃剁敤鐙珛鎺掑悕琛紙銆婄敓鎴恄璐ц繍閲忔帓鍚嶃�嬨�婄敓鎴恄鍛ㄨ浆閲忔帓鍚嶃�嬨�婄敓鎴恄涓彛寰勬帓鍚嶃�嬶紝涓庣敤鎴锋棩甯告牳瀵圭殑鍙e緞涓�鑷达級
+ * 鐨勭粨鏋滃埛鏂版暟鍊硷紝閬垮厤姹囨�昏〃閲岀殑鎺掑悕椤靛仠鐣欏湪鏃ф湀浠姐��
+ * 鏁板�兼牸鎼�笺�佸叕寮忔牸鎸夊亸绉诲钩绉诲叕寮忓悗鍐欏叆锛涚洰鏍囨牸鏈韩宸叉槸鍏紡鐨勶紙SUM/RANK/鍗犳瘮/琛ユ暟锛変繚鐣欎笉鍔紝鎵撳紑鏃舵寜鏂版暟鎹噸绠椼��
+ */
+ private int refreshSummaryRankSheets(XSSFWorkbook wb, String period, String mode) 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}});
+ return n;
+ }
+
+ /**
+ * 鎶婄嫭绔嬫帓鍚嶈〃鐨勫悇涓潡鎸夋爣棰樺榻愭惉杩涙眹鎬诲ぇ琛ㄥ搴旈〉銆�
+ * srcBlocks[i] = 婧愬潡 i 鐨勮捣濮� {琛�, 鍒梷锛�0 鍩猴級锛涚洰鏍囧潡鎸夋爣棰橀『搴忥紙绱鍧楀湪鍓嶃�佸綋鏈堝潡鍦ㄥ悗锛夊搴斻��
+ * 鏁板�兼牸鐩存帴鎼�硷紱婧愪晶鏄叕寮忕殑锛堟帓鍚�/鍗犳瘮绛夛級鎸夌洰鏍囧潡鍋忕Щ鏁翠綋骞崇Щ鍚庡啓鍏ュ叕寮忥紝鎵撳紑鏃舵寜姹囨�昏〃鑷韩鏁版嵁閲嶇畻銆�
+ */
+ private int copyRankValues(XSSFWorkbook dstWb, String dstSheetName, byte[] srcBytes, int[][] srcBlocks) 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();
+ try (InputStream sin = new ByteArrayInputStream(srcBytes);
+ XSSFWorkbook srcWb = new XSSFWorkbook(sin)) {
+ XSSFSheet src = srcWb.getSheetAt(0);
+ FormulaEvaluator ev = srcWb.getCreationHelper().createFormulaEvaluator();
+ int blocks = Math.min(srcBlocks.length, dstTitles.size());
+ for (int b = 0; b < blocks; b++) {
+ 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 h = rankBlockHeight(src, sr0, sc0);
+ int w = rankBlockWidth(src, sr0, sc0);
+ 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;
+ if (r == 0) {
+ // 鏍囬琛岋細鍚屾鍧楁爣棰樻枃妗堛�傛瘝鐗堟爣棰樻槸 inlineStr锛孭OI 鐨� setCellValue 鍙啓 <v>銆�
+ // 涓嶆洿鏂� <is>锛孍xcel 浼氱户缁樉绀烘棫鏈堜唤锛屽繀椤诲厛 setBlank 娓呮帀鍐嶅啓銆�
+ Cell dt = dstRow.getCell(dc);
+ if (dt == null) dt = rankCell(dstRow, dc);
+ if (sc.getCellType() == CellType.STRING && dt.getCellType() != CellType.FORMULA) {
+ String text = sc.getStringCellValue();
+ String old = dt.getCellType() == CellType.STRING ? dt.getStringCellValue() : null;
+ if (text != null && !text.equals(old)) {
+ dt.setBlank();
+ dt.setCellValue(text);
+ }
+ }
+ continue;
+ }
+ Cell dcCell = dstRow.getCell(dc);
+ if (dcCell != null && dcCell.getCellType() == CellType.FORMULA) continue; // 淇濈暀姣嶇増鑷繁鐨勫叕寮�
+ if (sc.getCellType() == CellType.FORMULA) {
+ // 婧愪晶鍏紡锛堟帓鍚�/鍗犳瘮锛夛細鎸夌洰鏍囧潡鍋忕Щ骞崇Щ鍚庡師鏍峰啓鍏ワ紝鍐嶆寜姹囨�昏〃鑷韩鏁版嵁姹傚�肩紦瀛樸��
+ // 涓嶇洿鎺ユ惉 POI 瀵规簮琛ㄧ殑姹傚�肩粨鏋滐紙濡� RANK 瀵规墜宸ョ暀绌虹殑鍚屾瘮涓�寰嬭繑鍥� 1锛夈��
+ String f = sc.getCellFormula();
+ if (f == null || f.isEmpty()) continue;
+ String shifted = shiftFormula(f, colOff, rowOff);
+ if (dcCell == null) dcCell = rankCell(dstRow, dc);
+ dcCell.setCellFormula(shifted);
+ try {
+ dstEv.evaluateFormulaCell(dcCell);
+ } catch (Exception ignore) {
+ // 姹傚�煎け璐ヤ笉闃诲瀵煎嚭锛氭墦寮�鏃剁敱 Excel/WPS 閲嶇畻
+ }
+ copied++;
+ continue;
+ }
+ Double v = rankNumeric(sc, ev);
+ if (v == null) continue;
+ if (dcCell == null) dcCell = rankCell(dstRow, dc);
+ dcCell.setCellValue(v);
+ copied++;
+ }
+ }
+ }
+ }
+ return copied;
+ }
+
+ /** 鐩爣鍧楃己鏍兼椂鏂板缓锛屽苟娌跨敤鍚岃宸﹂偦鏍兼牱寮忥紙闃叉柊鏍间涪杈规/鐧惧垎鏁版牸寮忥級 */
+ private Cell rankCell(Row row, int col) {
+ Cell c = row.createCell(col);
+ Cell left = col > 0 ? row.getCell(col - 1) : null;
+ if (left != null) c.setCellStyle(left.getCellStyle());
+ return c;
+ }
+
+ /** 鍏紡鍐呮墍鏈� A1 寮曠敤鏁翠綋骞崇Щ锛氬垪 +dCol銆佽 +dRow锛圫UM/RANK 绛夊嚱鏁板悕鍚庢帴鎷彿涓嶄細琚尮閰嶏級 */
+ private String shiftFormula(String formula, int dCol, int dRow) {
+ if (formula == null || (dCol == 0 && dRow == 0)) return formula;
+ java.util.regex.Matcher m = java.util.regex.Pattern
+ .compile("(?<![A-Za-z0-9_$])([$]?)([A-Z]{1,3})([$]?)([0-9]+)")
+ .matcher(formula);
+ StringBuffer sb = new StringBuffer();
+ while (m.find()) {
+ String col = m.group(2);
+ int idx = 0;
+ for (int i = 0; i < col.length(); i++) idx = idx * 26 + (col.charAt(i) - 'A' + 1);
+ int nidx = Math.max(1, idx + dCol);
+ StringBuilder nc = new StringBuilder();
+ while (nidx > 0) {
+ int rem = (nidx - 1) % 26;
+ nc.insert(0, (char) ('A' + rem));
+ nidx = (nidx - 1) / 26;
+ }
+ int nrow = Math.max(1, Integer.parseInt(m.group(4)) + dRow);
+ m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(m.group(1) + nc + m.group(3) + nrow));
+ }
+ m.appendTail(sb);
+ return sb.toString();
+ }
+
+ /** 鎺掑悕椤甸噷鐨勨�滃潡鏍囬鈥濆崟鍏冩牸锛堝惈鈥滃叏鐪佸垎甯傚窞鈥濈殑璇存槑鏂囧瓧锛夛紝鎸夎銆佸垪椤哄簭杩斿洖 */
+ private List<XSSFCell> rankTitleCells(Sheet sh) {
+ List<XSSFCell> out = new ArrayList<>();
+ for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
+ Row row = sh.getRow(r);
+ if (row == null) continue;
+ for (int c = row.getFirstCellNum(); c < row.getLastCellNum(); c++) {
+ Cell cell = row.getCell(c);
+ if (cell == null || cell.getCellType() != CellType.STRING) continue;
+ String t = cell.getStringCellValue();
+ if (t != null && t.contains("鍏ㄧ渷鍒嗗競宸�")) out.add((XSSFCell) cell);
+ }
+ }
+ return out;
+ }
+
+ /** 鍧楅珮搴︼細浠庤捣濮嬭鍚戜笅鐩村埌鏁磋涓虹┖锛堝湪鍧楀垪鑼冨洿鍐咃級 */
+ private int rankBlockHeight(Sheet sh, int r0, int c0) {
+ int h = 0;
+ for (int r = r0; r <= sh.getLastRowNum(); r++) {
+ Row row = sh.getRow(r);
+ boolean any = false;
+ if (row != null) {
+ int last = row.getLastCellNum();
+ for (int c = c0; c < last; c++) {
+ if (rankHasContent(row.getCell(c))) { any = true; break; }
+ }
+ }
+ if (!any) break;
+ h++;
+ }
+ return h;
+ }
+
+ /** 鍧楀搴︼細浠庤捣濮嬪垪鍚戝彸鐩村埌鏁村垪涓虹┖锛堝湪鍧楄鑼冨洿鍐咃紝鎵弿涓婇檺 40 鍒楋級 */
+ private int rankBlockWidth(Sheet sh, int r0, int c0) {
+ int w = 0;
+ for (int c = c0; c < c0 + 40; c++) {
+ boolean any = false;
+ for (int r = r0; r < r0 + 30; r++) {
+ Row row = sh.getRow(r);
+ if (row != null && rankHasContent(row.getCell(c))) { any = true; break; }
+ }
+ if (!any) break;
+ w++;
+ }
+ return w;
+ }
+
+ private boolean rankHasContent(Cell c) {
+ if (c == null) return false;
+ CellType t = c.getCellType();
+ if (t == CellType.BLANK) return false;
+ if (t == CellType.STRING) {
+ String v = c.getStringCellValue();
+ return v != null && !v.trim().isEmpty();
+ }
+ return true;
+ }
+
+ /** 鍙栧崟鍏冩牸鏁板�硷紙鍏紡鍙栨眰鍊肩粨鏋滐紝绾暟瀛楁枃鏈寜鏁板�艰В鏋愶級锛屽彇涓嶅埌杩斿洖 null */
+ private Double rankNumeric(Cell c, FormulaEvaluator ev) {
+ try {
+ CellType t = c.getCellType();
+ if (t == CellType.NUMERIC) return c.getNumericCellValue();
+ if (t == CellType.FORMULA) {
+ org.apache.poi.ss.usermodel.CellValue cv = ev.evaluate(c);
+ return cv != null && cv.getCellType() == CellType.NUMERIC ? cv.getNumberValue() : null;
+ }
+ if (t == CellType.STRING) {
+ String v = c.getStringCellValue();
+ if (v == null) return null;
+ String x = v.replace(",", "").trim();
+ if (x.isEmpty()) return null;
+ try { return Double.parseDouble(x); } catch (NumberFormatException ignore) { return null; }
+ }
+ } catch (Exception ignore) {
+ return null;
+ }
+ return null;
+ }
+
+ /** 瀹氫綅鐗堝紡鍙傜収浠� docs/鐢熸垚姹囨�诲ぇ琛�/鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx锛堜紭鍏堢骇鍚屾瘝鐗堬細閰嶇疆鐩綍 鈫� user.dir 鐩稿 鈫� 涓婄骇锛� */
+ // ==================== 姹囨�诲ぇ琛細鍥涘紶鈥滆法椤靛悎鎴愨�濋〉闅忔姤琛ㄦ湡鍒锋柊 ====================
+ // 鍩庡競姹囨�� / 鍩庡競鍒嗗競宸� / 涓彛寰勫垎鏋� / 閬撹矾杩愯緭鍛ㄨ浆閲� 姝ゅ墠鏁撮〉娌跨敤姣嶇増闈欐�佸�硷紙鏍囬鍋滃湪 1-7 鏈堬級銆�
+ // 鏈鎸� 2026-09-13 宸查獙璇佸彛寰勶紙涓� 8 鏈堟瘝鐗堥�愭牸 0 宸紓锛変粠姣嶇増鍙拌处椤电幇绠楋細
+ // 鍩庡競瀹㈣繍閲� = 鍩庡競鍐呭叕浜� + 鍩庡競鍐呭贰娓稿嚭绉� + 鍩庡競鍐呯綉绾﹁溅 + 杞ㄩ亾 + 杞浮
+ // 閬撹矾瀹㈣繍鍛ㄨ浆閲� = 鍏矾鐝嚎 + 鍩庨檯鍩庝埂鍏氦 + 鍩庨檯鍩庝埂宸℃父鍑虹 + 鍩庨檯鍩庝埂缃戠害杞�
+ // 閬撹矾璐ц繍鍛ㄨ浆閲� = 璐ц繍椤� 甯傚窞 瑙勪笂璐х墿鍛ㄨ浆閲� + 瑙勪笅璐х墿鍛ㄨ浆閲�
+ // 2026 骞� 1..N 鏈堝彇鍙拌处椤� 2026 鍒楋紙C/E/G鈥︼級锛屽幓骞村悓鏈熷彇姣嶇増缂撳瓨 2025 鍒楋紙V/X/Z鈥︼級鈥斺�斿簱閲屾病鏈� 2025 骞村煄甯傚杩愭暟鎹��
+
+ /** 鍩庡競瀹㈣繍鍙e緞鎸囨爣浣嶏紙0..17 鍩庡競瀹㈣繍锛�18 璐ц繍鍛ㄨ浆閲忥級 */
+ private static final int SM_BUS_PAX = 0, SM_BUS_TURN = 1;
+ private static final int SM_TAXI_PAX = 2, SM_TAXI_TURN = 3;
+ private static final int SM_WYC_PAX = 4, SM_WYC_TURN = 5;
+ private static final int SM_RAIL_PAX = 6, SM_RAIL_TURN = 7;
+ private static final int SM_FERRY_PAX = 8, SM_FERRY_TURN = 9;
+ private static final int SM_BX_PAX = 10, SM_BX_TURN = 11;
+ private static final int SM_BUS_CX_PAX = 12, SM_BUS_CX_TURN = 13;
+ private static final int SM_TAXI_CX_PAX = 14, SM_TAXI_CX_TURN = 15;
+ 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;
+
+ /**
+ * 鍒锋柊鍥涘紶璺ㄩ〉鍚堟垚椤碉細褰撳勾绱锛�1..month 鏈堬級銆佸綋鏈堬紙month 鏈堬級鍙婂搴斿悓姣旓紱
+ * 2026 涓� 2025 鍚屾湡閮藉彇鑷瘝鐗堝彴璐﹂〉缂撳瓨锛岃繑鍥炲啓鍏ュ崟鍏冩牸鏁般��
+ */
+ private int refreshSummaryCumulativeSheets(XSSFWorkbook wb, int year, int month) {
+ if (wb == null || month <= 0) return 0;
+ 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);
+ if (curCum.isEmpty()) {
+ log.warn("姹囨�诲ぇ琛ㄥ洓椤靛埛鏂帮細鍙拌处椤垫湭璇诲埌甯傚窞鏁版嵁锛岃烦杩囷紙淇濇寔姣嶇増鍘熷�硷級");
+ return 0;
+ }
+ double[] pCurCum = smProvinceSum(curCum);
+ double[] pLastCum = smProvinceSum(lastCum);
+ double[] pCurMon = smProvinceSum(curMon);
+ double[] pLastMon = smProvinceSum(lastMon);
+ int n = 0;
+ n += smWriteCitySummary(wb, year, month, pCurCum, pLastCum, pCurMon, pLastMon);
+ n += smWriteCityByRegion(wb, month, curCum, lastCum, curMon, lastMon);
+ n += smWriteMidAnalysis(wb, month, pCurCum, pLastCum, pCurMon, pLastMon);
+ n += smWriteTurnoverSheets(wb, year, month, curCum, lastCum, curMon, lastMon);
+ return n;
+ }
+
+ /** 17 甯傚窞鍚堣锛堜笉鍚彴璐﹂〉鈥滃叏鐪佲�濆叕寮忚锛� */
+ private double[] smProvinceSum(Map<String, double[]> m) {
+ double[] p = new double[SM_DIM];
+ for (String c : RegionUtil.cityList()) {
+ double[] a = m.get(c);
+ if (a == null) continue;
+ for (int i = 0; i < SM_DIM; i++) p[i] += a[i];
+ }
+ return p;
+ }
+
+ /** 鍙拌处椤电疮璁″彇鏁帮細甯傚窞瑙勮寖鍚� -> 19 缁存寚鏍� */
+ private Map<String, double[]> smReadLedgerMetrics(XSSFWorkbook wb, boolean curYear, 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);
+ return out;
+ }
+
+ /** 鍏氦/鍑虹/缃戠害杞﹂〉锛氭瘡甯傚窞 4 琛岋紙瀹㈣繍閲�/鏃呭鍛ㄨ浆閲�/鍏朵腑鍩庡競鍐呭杩愰噺/鍏朵腑鍩庡競鍐呮梾瀹㈠懆杞噺锛� */
+ private void smReadCityPaxPage(XSSFWorkbook wb, String sheetName, boolean curYear, int[] months,
+ Map<String, double[]> out, int paxIdx, int turnIdx, int cxPaxIdx, int cxTurnIdx) {
+ Sheet sh = wb.getSheet(sheetName);
+ if (sh == null) return;
+ int last = sh.getLastRowNum();
+ for (int r = sh.getFirstRowNum(); r <= last; r++) {
+ Row row = sh.getRow(r);
+ if (row == null) continue;
+ String city = smCityOf(row.getCell(0));
+ if (city == null) continue;
+ int paxRow = -1, turnRow = -1, cxPaxRow = -1, cxTurnRow = -1;
+ for (int k = r; k <= Math.min(last, r + 4); k++) {
+ Row kr = sh.getRow(k);
+ if (kr == null) continue;
+ String b = smText(kr.getCell(1));
+ if (b == null || b.contains("涓綋")) continue;
+ boolean inner = b.contains("鍩庡競鍐�");
+ boolean turn = b.contains("鍛ㄨ浆閲�");
+ if (inner) {
+ if (turn) { if (cxTurnRow < 0) cxTurnRow = k; }
+ else if (cxPaxRow < 0) cxPaxRow = k;
+ } else {
+ if (turn) { if (turnRow < 0) turnRow = k; }
+ 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[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]);
+ arr[paxIdx] += cityPax; // 鍩庡競瀹㈣繍鍙e緞鍙栤�滃煄甯傚唴鈥�
+ arr[turnIdx] += cityTurn;
+ arr[cxPaxIdx] += totalPax - cityPax; // 鍩庨檯鍩庝埂 = 鎬婚噺 - 鍩庡競鍐�
+ arr[cxTurnIdx] += totalTurn - cityTurn;
+ }
+ }
+
+ /** 杞ㄩ亾銆佽疆娓¢〉锛氳建閬撳潡锛堟姹�/榛勭煶锛�+ 杞浮鍧楋紙姝︽眽锛� */
+ private void smReadRailFerry(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) {
+ Sheet sh = wb.getSheet("杞ㄩ亾銆佽疆娓�");
+ if (sh == null) return;
+ int last = sh.getLastRowNum();
+ int ferryTitle = -1;
+ for (int r = sh.getFirstRowNum(); r <= last; r++) {
+ Row row = sh.getRow(r);
+ if (row == null) continue;
+ String a = smText(row.getCell(0));
+ if (a != null && a.contains("杞浮瀹㈣繍閲�")) { ferryTitle = r; break; }
+ }
+ smReadRailFerryBlock(sh, curYear, 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);
+ }
+
+ private void smReadRailFerryBlock(Sheet sh, boolean curYear, int[] months, Map<String, double[]> out,
+ int start, int end, int baseIdx) {
+ for (int r = start; r <= end; r++) {
+ Row row = sh.getRow(r);
+ if (row == null) continue;
+ String city = smCityOf(row.getCell(0));
+ if (city == null) continue;
+ int paxRow = -1, turnRow = -1;
+ for (int k = r; k <= Math.min(end, r + 3); k++) {
+ Row kr = sh.getRow(k);
+ if (kr == null) continue;
+ String b = smText(kr.getCell(1));
+ if (b == null) continue;
+ if (b.contains("鍛ㄨ浆閲�")) { if (turnRow < 0) turnRow = k; }
+ 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);
+ }
+ }
+
+ /** 鐝嚎鍖呰溅椤碉細姣忓競宸� 瀹㈣繍閲� / 鏃呭鍛ㄨ浆閲忥紙鈥滃叾涓釜浣撯�濊涓嶉噸澶嶈鍏ワ級 */
+ private void smReadBanxian(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) {
+ Sheet sh = wb.getSheet("鐝嚎鍖呰溅");
+ if (sh == null) return;
+ int last = sh.getLastRowNum();
+ for (int r = sh.getFirstRowNum(); r <= last; r++) {
+ Row row = sh.getRow(r);
+ if (row == null) continue;
+ String city = smCityOf(row.getCell(0));
+ if (city == null) continue;
+ int paxRow = -1, turnRow = -1;
+ for (int k = r; k <= Math.min(last, r + 4); k++) {
+ Row kr = sh.getRow(k);
+ if (kr == null) continue;
+ String b = smText(kr.getCell(1));
+ if (b == null || b.contains("涓綋")) continue;
+ if (b.contains("鍛ㄨ浆閲�")) { if (turnRow < 0) turnRow = k; }
+ 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);
+ }
+ }
+
+ /** 璐ц繍椤碉細姣忓競宸� 瑙勪笂璐х墿鍛ㄨ浆閲� + 瑙勪笅璐х墿鍛ㄨ浆閲忥紙鈥滆涓�+瑙勪笅鈥濊鏈韩鏄眰鍜屽叕寮忥紝涓嶇洿鎺ュ彇锛� */
+ private void smReadFreightTurnover(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) {
+ Sheet sh = wb.getSheet(" 璐ц繍");
+ if (sh == null) sh = wb.getSheet("璐ц繍");
+ if (sh == null) return;
+ int last = sh.getLastRowNum();
+ for (int r = sh.getFirstRowNum(); r <= last; r++) {
+ Row row = sh.getRow(r);
+ if (row == null) continue;
+ String city = smCityOf(row.getCell(0));
+ if (city == null) continue;
+ int above = -1, below = -1;
+ for (int k = r; k <= Math.min(last, r + 6); k++) {
+ Row kr = sh.getRow(k);
+ if (kr == null) continue;
+ String b = smText(kr.getCell(1));
+ if (b == null || b.contains("鍏朵腑")) continue;
+ if (b.contains("瑙勪笂璐х墿鍛ㄨ浆閲�")) { if (above < 0) above = k; }
+ 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);
+ }
+ }
+
+ private double smSumMonths(Sheet sh, int rowIdx, boolean curYear, int[] months) {
+ if (sh == null || rowIdx < 0) 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);
+ s += smNum(row.getCell(col));
+ }
+ return s;
+ }
+
+ /** 鍙拌处椤靛崟鍏冩牸鍙栨暟锛堟暟瀛� / 鏁板瓧鏂囨湰 / 鍏紡缂撳瓨锛� */
+ private double smNum(Cell c) {
+ if (c == null) return 0;
+ try {
+ CellType t = c.getCellType();
+ if (t == CellType.NUMERIC) return c.getNumericCellValue();
+ if (t == CellType.FORMULA) {
+ return c.getCachedFormulaResultType() == CellType.NUMERIC ? c.getNumericCellValue() : 0;
+ }
+ if (t == CellType.STRING) {
+ String s = c.getStringCellValue();
+ if (s == null) return 0;
+ s = s.replace(",", "").replace("%", "").trim();
+ if (s.isEmpty()) return 0;
+ return Double.parseDouble(s);
+ }
+ } catch (Exception ignore) {
+ return 0;
+ }
+ return 0;
+ }
+
+ private String smText(Cell c) {
+ if (c == null) return null;
+ try {
+ return c.getCellType() == CellType.STRING ? c.getStringCellValue() : null;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /** 鍒� A 鏂囨 -> 瑙勮寖甯傚窞鍚嶏紙鈥滃叏鐪�/婀栧寳鐪佲�濅笌闈炲競宸炶杩斿洖 null锛� */
+ private String smCityOf(Cell a) {
+ String t = smText(a);
+ if (t == null || t.trim().isEmpty()) return null;
+ String norm = RegionUtil.normalizeCityName(t.trim());
+ if (norm == null) return null;
+ return RegionUtil.cityList().contains(norm) ? norm : null;
+ }
+
+ private double smYoy(double cur, double last) {
+ return last == 0.0 ? 0.0 : (cur - last) / last;
+ }
+
+ /** 鍐欏�硷細鍏紡鏍间笉鍔紙淇濈暀姣嶇増鍏紡锛屾墦寮�鍗抽噸绠楋級锛�0 涓斿師涓虹┖鏍煎垯涓嶆柊澧� */
+ private int smSetVal(Sheet sh, int r0, int c0, double v) {
+ Row row = sh.getRow(r0);
+ if (row == null) return 0;
+ Cell c = row.getCell(c0);
+ if (c != null && c.getCellType() == CellType.FORMULA) return 0;
+ if (c == null) {
+ if (v == 0.0) return 0;
+ c = row.createCell(c0);
+ Cell left = row.getCell(c0 - 1);
+ if (left != null) c.setCellStyle(left.getCellStyle());
+ } else if (v == 0.0 && c.getCellType() == CellType.BLANK) {
+ return 0;
+ }
+ c.setBlank();
+ c.setCellValue(v);
+ return 1;
+ }
+
+ private void smSetTitle(Sheet sh, int r0, int c0, String text) {
+ if (sh == null) return;
+ Row row = sh.getRow(r0);
+ if (row == null) return;
+ Cell c = row.getCell(c0);
+ if (c == null || c.getCellType() == CellType.FORMULA) return;
+ c.setBlank();
+ c.setCellValue(text);
+ }
+
+ /** 鍩庡競姹囨�婚〉锛氬潡1锛�1..N 鏈堢疮璁★級琛� 3-9锛屽潡2锛堝綋鏈堬級琛� 14-20锛涘垪 B/C/D/E = 瀹㈣繍閲�/鍚屾瘮/鍛ㄨ浆閲�/鍚屾瘮 */
+ private int smWriteCitySummary(XSSFWorkbook wb, int year, int month,
+ double[] pCurCum, double[] pLastCum, double[] pCurMon, double[] pLastMon) {
+ Sheet sh = wb.getSheet("鍩庡競姹囨��");
+ if (sh == null) return 0;
+ smSetTitle(sh, 0, 0, year + "骞�1-" + month + "鏈堝叏鐪佺疮璁″畬鎴愬煄甯傚杩愰噺鎯呭喌");
+ smSetTitle(sh, 11, 0, year + "骞�" + month + "鏈堝叏鐪佸畬鎴愬煄甯傚杩愰噺鎯呭喌");
+ int n = 0;
+ int[] rows = {2, 3, 4, 5, 6, 7, 8};
+ for (int i = 0; i < rows.length; i++) {
+ double[] v = smCitySummaryRow(pCurCum, pLastCum, i);
+ n += smSetVal(sh, rows[i], 1, v[0]);
+ n += smSetVal(sh, rows[i], 2, v[2]);
+ n += smSetVal(sh, rows[i], 3, v[1]);
+ n += smSetVal(sh, rows[i], 4, v[3]);
+ }
+ for (int i = 0; i < rows.length; i++) {
+ double[] v = smCitySummaryRow(pCurMon, pLastMon, i);
+ n += smSetVal(sh, rows[i] + 11, 1, v[0]);
+ n += smSetVal(sh, rows[i] + 11, 2, v[2]);
+ n += smSetVal(sh, rows[i] + 11, 3, v[1]);
+ n += smSetVal(sh, rows[i] + 11, 4, v[3]);
+ }
+ return n;
+ }
+
+ /** i=0 鎬诲杩愰噺 1 鍏氦 2 鍑虹杞� 3 宸℃父鍑虹 4 缃戠害杞� 5 杞ㄩ亾 6 杞浮锛涜繑鍥� [瀹㈣繍閲�, 鍛ㄨ浆閲�, 瀹㈣繍閲忓悓姣�, 鍛ㄨ浆閲忓悓姣擼 */
+ private double[] smCitySummaryRow(double[] p, double[] pl, int i) {
+ double pax, turn, lPax, lTurn;
+ if (i == 0) {
+ pax = p[SM_BUS_PAX] + p[SM_TAXI_PAX] + p[SM_WYC_PAX] + p[SM_RAIL_PAX] + p[SM_FERRY_PAX];
+ turn = p[SM_BUS_TURN] + p[SM_TAXI_TURN] + p[SM_WYC_TURN] + p[SM_RAIL_TURN] + p[SM_FERRY_TURN];
+ lPax = pl[SM_BUS_PAX] + pl[SM_TAXI_PAX] + pl[SM_WYC_PAX] + pl[SM_RAIL_PAX] + pl[SM_FERRY_PAX];
+ lTurn = pl[SM_BUS_TURN] + pl[SM_TAXI_TURN] + pl[SM_WYC_TURN] + pl[SM_RAIL_TURN] + pl[SM_FERRY_TURN];
+ } else if (i == 1) {
+ pax = p[SM_BUS_PAX]; turn = p[SM_BUS_TURN]; lPax = pl[SM_BUS_PAX]; lTurn = pl[SM_BUS_TURN];
+ } else if (i == 2) {
+ pax = p[SM_TAXI_PAX] + p[SM_WYC_PAX]; turn = p[SM_TAXI_TURN] + p[SM_WYC_TURN];
+ lPax = pl[SM_TAXI_PAX] + pl[SM_WYC_PAX]; lTurn = pl[SM_TAXI_TURN] + pl[SM_WYC_TURN];
+ } else if (i == 3) {
+ pax = p[SM_TAXI_PAX]; turn = p[SM_TAXI_TURN]; lPax = pl[SM_TAXI_PAX]; lTurn = pl[SM_TAXI_TURN];
+ } else if (i == 4) {
+ pax = p[SM_WYC_PAX]; turn = p[SM_WYC_TURN]; lPax = pl[SM_WYC_PAX]; lTurn = pl[SM_WYC_TURN];
+ } else if (i == 5) {
+ pax = p[SM_RAIL_PAX]; turn = p[SM_RAIL_TURN]; lPax = pl[SM_RAIL_PAX]; lTurn = pl[SM_RAIL_TURN];
+ } else {
+ pax = p[SM_FERRY_PAX]; turn = p[SM_FERRY_TURN]; lPax = pl[SM_FERRY_PAX]; lTurn = pl[SM_FERRY_TURN];
+ }
+ return new double[]{pax, turn, smYoy(pax, lPax), smYoy(turn, lTurn)};
+ }
+
+ /** 鍩庡競鍒嗗競宸為〉锛�4 鍧楋紙绱瀹㈣繍閲�/绱鍛ㄨ浆閲�/褰撴湀瀹㈣繍閲�/褰撴湀鍛ㄨ浆閲忥級锛屾瘡鍧� 鍏ㄧ渷 1 琛� + 17 甯傚窞 */
+ private int smWriteCityByRegion(XSSFWorkbook wb, int month,
+ Map<String, double[]> curCum, Map<String, double[]> lastCum,
+ Map<String, double[]> curMon, Map<String, double[]> lastMon) {
+ Sheet sh = wb.getSheet("鍩庡競鍒嗗競宸�");
+ 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 + "鏈堟儏鍐�");
+ int n = 0;
+ n += smWriteCityByRegionBlock(sh, 4, true, curCum, lastCum);
+ n += smWriteCityByRegionBlock(sh, 26, false, curCum, lastCum);
+ n += smWriteCityByRegionBlock(sh, 49, true, curMon, lastMon);
+ n += smWriteCityByRegionBlock(sh, 71, false, curMon, lastMon);
+ return n;
+ }
+
+ private int smWriteCityByRegionBlock(Sheet sh, int provRow, boolean pax,
+ Map<String, double[]> cur, Map<String, double[]> last) {
+ int n = 0;
+ double[] provCur = smProvinceSum(cur);
+ double[] provLast = smProvinceSum(last);
+ List<String> cities = RegionUtil.cityList();
+ for (int i = -1; i < cities.size(); i++) {
+ int r = provRow + i + 1;
+ double[] c = i < 0 ? provCur : cur.get(cities.get(i));
+ double[] l = i < 0 ? provLast : last.get(cities.get(i));
+ if (c == null) c = new double[SM_DIM];
+ if (l == null) l = new double[SM_DIM];
+ double[] g = smCityGroupValues(c, l, pax);
+ n += smSetVal(sh, r, 1, g[0]);
+ n += smSetVal(sh, r, 4, g[6]);
+ n += smSetVal(sh, r, 6, g[1]);
+ n += smSetVal(sh, r, 9, g[7]);
+ n += smSetVal(sh, r, 11, g[2]);
+ n += smSetVal(sh, r, 14, g[8]);
+ n += smSetVal(sh, r, 16, g[3]);
+ n += smSetVal(sh, r, 19, g[9]);
+ n += smSetVal(sh, r, 21, g[4]);
+ n += smSetVal(sh, r, 22, g[10]);
+ n += smSetVal(sh, r, 23, g[5]);
+ n += smSetVal(sh, r, 24, g[11]);
+ }
+ return n;
+ }
+
+ /** 6 缁勫�� + 6 缁勫閫燂細[鎬婚噺, 鍏氦, 鍑虹, 缃戠害杞�, 杞ㄩ亾, 杞浮, 鎬婚噺澧為��, 鍏氦鈥�, 杞浮] */
+ private double[] smCityGroupValues(double[] c, double[] l, boolean pax) {
+ int busI = pax ? SM_BUS_PAX : SM_BUS_TURN;
+ int taxiI = pax ? SM_TAXI_PAX : SM_TAXI_TURN;
+ int wycI = pax ? SM_WYC_PAX : SM_WYC_TURN;
+ int railI = pax ? SM_RAIL_PAX : SM_RAIL_TURN;
+ int ferryI = pax ? SM_FERRY_PAX : SM_FERRY_TURN;
+ double bus = c[busI], taxi = c[taxiI], wyc = c[wycI], rail = c[railI], ferry = c[ferryI];
+ double lBus = l[busI], lTaxi = l[taxiI], lWyc = l[wycI], lRail = l[railI], lFerry = l[ferryI];
+ double total = bus + taxi + wyc + rail + ferry;
+ double lTotal = lBus + lTaxi + lWyc + lRail + lFerry;
+ return new double[]{total, bus, taxi, wyc, rail, ferry,
+ smYoy(total, lTotal), smYoy(bus, lBus), smYoy(taxi, lTaxi), smYoy(wyc, lWyc),
+ smYoy(rail, lRail), smYoy(ferry, lFerry)};
+ }
+
+ /** 涓彛寰勫垎鏋愰〉锛氬潡1锛堢疮璁★級琛� 3-7锛屽潡2锛堝綋鏈堬級琛� 12-16锛涘垪 B/C/F/G = 瀹㈣繍閲�/鍚屾瘮/鍛ㄨ浆閲�/鍚屾瘮 */
+ private int smWriteMidAnalysis(XSSFWorkbook wb, int month,
+ double[] pCurCum, double[] pLastCum, double[] pCurMon, double[] pLastMon) {
+ Sheet sh = wb.getSheet("涓彛寰勫垎鏋�");
+ if (sh == null) return 0;
+ smSetTitle(sh, 0, 0, "1-" + month + "鏈堜腑鍙e緞瀹㈣繍閲忓強鍛ㄨ浆閲�");
+ smSetTitle(sh, 9, 0, month + "鏈堜腑鍙e緞瀹㈣繍閲忓強鍛ㄨ浆閲�");
+ int n = 0;
+ int[] rows = {2, 3, 4, 5, 6};
+ for (int i = 0; i < rows.length; i++) {
+ double[] v = smMidRow(pCurCum, pLastCum, i);
+ n += smSetVal(sh, rows[i], 1, v[0]);
+ n += smSetVal(sh, rows[i], 2, v[2]);
+ n += smSetVal(sh, rows[i], 5, v[1]);
+ n += smSetVal(sh, rows[i], 6, v[3]);
+ }
+ for (int i = 0; i < rows.length; i++) {
+ double[] v = smMidRow(pCurMon, pLastMon, i);
+ n += smSetVal(sh, rows[i] + 9, 1, v[0]);
+ n += smSetVal(sh, rows[i] + 9, 2, v[2]);
+ n += smSetVal(sh, rows[i] + 9, 5, v[1]);
+ n += smSetVal(sh, rows[i] + 9, 6, v[3]);
+ }
+ return n;
+ }
+
+ /** i=0 鎬� 1 鍏矾鐝嚎 2 鍩庨檯鍩庝埂鍏氦 3 鍩庨檯鍩庝埂宸℃父鍑虹 4 鍩庨檯鍩庝埂缃戠害杞︼紱杩斿洖 [瀹㈣繍閲�, 鍛ㄨ浆閲�, 瀹㈣繍閲忓悓姣�, 鍛ㄨ浆閲忓悓姣擼 */
+ private double[] smMidRow(double[] p, double[] pl, int i) {
+ int[] paxIdx = {SM_BX_PAX, SM_BX_PAX, SM_BUS_CX_PAX, SM_TAXI_CX_PAX, SM_WYC_CX_PAX};
+ int[] turnIdx = {SM_BX_TURN, SM_BX_TURN, SM_BUS_CX_TURN, SM_TAXI_CX_TURN, SM_WYC_CX_TURN};
+ double pax, turn, lPax, lTurn;
+ if (i == 0) {
+ pax = p[SM_BX_PAX] + p[SM_BUS_CX_PAX] + p[SM_TAXI_CX_PAX] + p[SM_WYC_CX_PAX];
+ turn = p[SM_BX_TURN] + p[SM_BUS_CX_TURN] + p[SM_TAXI_CX_TURN] + p[SM_WYC_CX_TURN];
+ lPax = pl[SM_BX_PAX] + pl[SM_BUS_CX_PAX] + pl[SM_TAXI_CX_PAX] + pl[SM_WYC_CX_PAX];
+ lTurn = pl[SM_BX_TURN] + pl[SM_BUS_CX_TURN] + pl[SM_TAXI_CX_TURN] + pl[SM_WYC_CX_TURN];
+ } else {
+ pax = p[paxIdx[i]]; turn = p[turnIdx[i]];
+ lPax = pl[paxIdx[i]]; lTurn = pl[turnIdx[i]];
+ }
+ return new double[]{pax, turn, smYoy(pax, lPax), smYoy(turn, lTurn)};
+ }
+
+ /** 閬撹矾杩愯緭鍛ㄨ浆閲忛〉锛氬潡1锛堢疮璁★級琛� 4-21锛屽潡2锛堝綋鏈堬級琛� 26-43锛涘彧鍐欏�煎垪锛圔/E/V 绛夊叕寮忎繚鐣欙級 */
+ private int smWriteTurnoverSheets(XSSFWorkbook wb, int year, int month,
+ Map<String, double[]> curCum, Map<String, double[]> lastCum,
+ Map<String, double[]> curMon, Map<String, double[]> lastMon) {
+ Sheet sh = wb.getSheet("閬撹矾杩愯緭鍛ㄨ浆閲�");
+ if (sh == null) return 0;
+ smSetTitle(sh, 0, 0, year + "骞�1-" + month + "鏈堝叏鐪佺疮璁″畬鎴愪氦閫氬懆杞噺鎯呭喌");
+ smSetTitle(sh, 22, 0, year + "骞�" + month + "鏈堝叏鐪佸畬鎴愪氦閫氬懆杞噺鎯呭喌");
+ int n = 0;
+ n += smWriteTurnoverBlock(sh, 3, curCum, lastCum);
+ n += smWriteTurnoverBlock(sh, 25, curMon, lastMon);
+ return n;
+ }
+
+ private int smWriteTurnoverBlock(Sheet sh, int provRow, Map<String, double[]> cur, Map<String, double[]> last) {
+ int n = 0;
+ double[] provCur = smProvinceSum(cur);
+ double[] provLast = smProvinceSum(last);
+ List<String> cities = RegionUtil.cityList();
+ for (int i = -1; i < cities.size(); i++) {
+ int r = provRow + i + 1;
+ double[] c = i < 0 ? provCur : cur.get(cities.get(i));
+ double[] l = i < 0 ? provLast : last.get(cities.get(i));
+ if (c == null) c = new double[SM_DIM];
+ if (l == null) l = new double[SM_DIM];
+ double freight = c[SM_FREIGHT_TURN], lFreight = l[SM_FREIGHT_TURN];
+ double pax = c[SM_BUS_PAX] + c[SM_TAXI_PAX] + c[SM_WYC_PAX] + c[SM_RAIL_PAX] + c[SM_FERRY_PAX];
+ double lPax = l[SM_BUS_PAX] + l[SM_TAXI_PAX] + l[SM_WYC_PAX] + l[SM_RAIL_PAX] + l[SM_FERRY_PAX];
+ double road = c[SM_BX_TURN] + c[SM_BUS_CX_TURN] + c[SM_TAXI_CX_TURN] + c[SM_WYC_CX_TURN];
+ double lRoad = l[SM_BX_TURN] + l[SM_BUS_CX_TURN] + l[SM_TAXI_CX_TURN] + l[SM_WYC_CX_TURN];
+ n += smSetVal(sh, r, 6, freight);
+ n += smSetVal(sh, r, 9, smYoy(freight, lFreight));
+ n += smSetVal(sh, r, 11, pax);
+ n += smSetVal(sh, r, 14, smYoy(pax, lPax));
+ n += smSetVal(sh, r, 16, road);
+ n += smSetVal(sh, r, 19, smYoy(road, lRoad));
+ n += smSetVal(sh, r, 22, lFreight);
+ n += smSetVal(sh, r, 23, lPax);
+ n += smSetVal(sh, r, 24, lRoad);
+ }
+ return n;
+ }
+
+
+ private File resolveSummaryLayoutRef() {
+ 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
+ };
+ for (String root : roots) {
+ if (root == null || root.trim().isEmpty()) continue;
+ File f = new File(root, "鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx");
+ if (f.isFile()) return f;
+ }
+ return null;
}
/** 姹囨�昏〃姣嶇増瀹氫綅锛氫粎绮剧‘鍖归厤 {year}骞磠month}鏈堚�︼紙P0-P1 楠ㄦ灦鏈熶笉鍏佽璺ㄦ湀/璺ㄥ勾闈欓粯鍥為��锛�
@@ -3492,8 +4827,8 @@
if (r.getAvgDistanceCity() != null && r.getAvgDistanceCity() > 0) a[5] = r.getAvgDistanceCity();
}
for (double[] a : map.values()) {
- if (a[4] <= 0 && a[0] > 0) a[4] = round(a[1] / a[0], 2);
- if (a[5] <= 0 && a[2] > 0) a[5] = round(a[3] / a[2], 2);
+ if (a[4] <= 0 && a[0] > 0) a[4] = a[1] / a[0];
+ if (a[5] <= 0 && a[2] > 0) a[5] = a[3] / a[2];
}
return map;
}
@@ -3526,12 +4861,19 @@
if (ds == null) continue;
Sheet bs = wb.getSheet(ds.getSheetName());
if (bs == null) continue;
+ // 涓彛寰勬帓鍚嶉〉 A..H 闄堟棫宸﹀潡宸蹭簬 2026-09-09 鎷嶆澘鏁村潡鍒犻櫎锛氳鍖哄煙姘镐笉浠庡浠芥瘝鐗堝洖濉紝閬垮厤鈥滅敓鎴愬張澶嶆椿鈥�
+ boolean skipMidRankLeftBlock = "涓彛寰勬帓鍚�".equals(ds.getSheetName());
+ // 銆� 璐ц繍銆嬮〉 2025..2021 骞村害鍧楋紙T 鍒楄捣锛屽惈鍚勫勾绱/绱鍚屾瘮鍒楋級鍦ㄦ瘝鐗堥噷鏄�屽競宸炴淳鐢熷叕寮忋�嶏紝
+ // 澶囦唤姣嶇増鍚屽尯浠嶆槸鏃х殑鍐欐甯搁噺锛堝叏鐪佽涓嬬瓑锛夛紝鍥炲~浼氳 2021/2022/2023 绱鍙堝涓嶄笂锛堚�滅敓鎴愬張澶嶆椿鈥濓級锛屾晠璇ュ尯涓嶅洖濉��
+ boolean skipFreightHistoryBlocks = " 璐ц繍".equals(ds.getSheetName());
for (int r = ds.getFirstRowNum(); r <= ds.getLastRowNum(); r++) {
Row dr = ds.getRow(r);
if (dr == null) continue;
Row br = bs.getRow(r);
for (Cell dc : dr) {
if (dc == null) continue;
+ if (skipMidRankLeftBlock && dc.getColumnIndex() < 8) continue;
+ if (skipFreightHistoryBlocks && dc.getColumnIndex() >= 19) continue; // T 鍒楀強浠ュ悗 = 2025..2021 骞村害鍧�
CellType t = dc.getCellType();
if (t == CellType.BLANK) continue;
String dtx = summaryCellText(dc);
@@ -3543,6 +4885,7 @@
Cell tc = (bc != null) ? bc : tr.getCell(dc.getColumnIndex());
if (tc == null) tc = tr.createCell(dc.getColumnIndex());
tc.setBlank();
+ applyDonorNumberFormat(wb, tc, dc);
if (t == CellType.FORMULA) {
String f = dc.getCellFormula();
if (f != null) tc.setCellFormula(f);
@@ -3560,6 +4903,24 @@
return restored;
}
+ /** 杩樺師鍥炲~鏃跺悓姝� donor锛堝凡鍋氱増寮忕粺涓�锛夌殑鏁板瓧鏍煎紡锛屼繚璇佸悓姣�/鏁板��/鎺掑悕鏍兼樉绀烘牸寮忎笌姣嶇増涓�鑷� */
+ private void applyDonorNumberFormat(XSSFWorkbook wb, Cell dst, Cell src) {
+ if (wb == null || dst == null || src == null) return;
+ try {
+ String fmt = src.getCellStyle().getDataFormatString();
+ if (fmt == null || fmt.isEmpty() || "General".equalsIgnoreCase(fmt)) return;
+ org.apache.poi.ss.usermodel.DataFormat df = wb.createDataFormat();
+ short idx = df.getFormat(fmt);
+ CellStyle cur = dst.getCellStyle();
+ if (cur != null && idx == cur.getDataFormat()) return;
+ CellStyle ns = wb.createCellStyle();
+ if (cur != null) ns.cloneStyleFrom(cur);
+ ns.setDataFormat(idx);
+ dst.setCellStyle(ns);
+ } catch (Exception ignore) {
+ // 涓埆鏍煎紡寮傚父涓嶉樆濉炶繕鍘�
+ }
+ }
/** 鍗曞厓鏍煎唴瀹规枃鏈寲锛堝叕寮忓甫 = 鍓嶇紑锛夛紝鐢ㄤ簬璺ㄧ翱姣斿 */
private String summaryCellText(Cell c) {
if (c == null) return null;
@@ -3712,6 +5073,8 @@
case "investFiveYearLogistics": return investFiveYearLogistics(period, mode);
case "investBillion": return investBillion(period, mode);
case "investCounty": return investCounty(period, mode);
+ case "investPassengerDetail": return exportInvestPassengerDetail(period, mode);
+ case "investLogisticsDetail": return exportInvestLogisticsDetail(period, mode);
case "cityBusDetail": return exportCityBusDetail(period, mode);
case "cityTaxiDetail": return exportCityTaxiDetail(period, mode);
case "cityRailFerryDetail": return exportCityRailFerryDetail(period, mode);
@@ -3737,6 +5100,8 @@
REPORT_FILE_NAMES.put("investFiveYearLogistics", "鐢熸垚_鍗佷簲浜旇鍒掔墿娴侀」鐩繘灞曟儏鍐�.xls");
REPORT_FILE_NAMES.put("investBillion", "鐢熸垚_婀栧寳鐪侊紙瀹㈣揣绔欏満锛変嚎鍏冩姇璧勯」鐩�.xls");
REPORT_FILE_NAMES.put("investCounty", "鐢熸垚_缁忔祹寮哄幙浜ら�氱墿娴佸熀纭�璁炬柦鎶曡祫缁熻鎶ヨ〃.xls");
+ REPORT_FILE_NAMES.put("investPassengerDetail", "鐢熸垚_瀹㈣繍绔欐姇璧勬槑缁嗚〃.xlsx");
+ REPORT_FILE_NAMES.put("investLogisticsDetail", "鐢熸垚_鐗╂祦绔欏満鎶曡祫鏄庣粏.xls");
REPORT_FILE_NAMES.put("cityBusDetail", "鐢熸垚_鍩庡競鍏氦瀹㈣繍閲忓垎甯傚窞鏄庣粏.xlsx");
REPORT_FILE_NAMES.put("cityTaxiDetail", "鐢熸垚_宸℃父鍑虹瀹㈣繍閲忓垎甯傚窞鏄庣粏.xlsx");
REPORT_FILE_NAMES.put("cityRailFerryDetail", "鐢熸垚_杞ㄩ亾杞浮瀹㈣繍閲忓垎甯傚窞鏄庣粏.xlsx");
@@ -3779,19 +5144,19 @@
Row row = sheet.getRow(rowIdx);
if (row == null) row = sheet.createRow(rowIdx);
if (a > 0) {
- // 2026骞� 1..N 鏈堬細搴撳唴鏈夊�煎垯濉紙2浣嶅皬鏁帮級锛屾棤鍊兼竻绌烘ā鏉挎牱渚�
+ // 2026骞� 1..N 鏈堬細搴撳唴鏈夊�煎垯濉紙鍏ㄧ簿搴﹀啓鍏ワ紝涓や綅灏忔暟鐢卞崟鍏冩牸鏍煎紡灞曠ず锛夛紝鏃犲�兼竻绌烘ā鏉挎牱渚�
for (int m = 1; m <= month; m++) {
double v = cityVal(monthCity, area, m, k);
Cell c = row.getCell(2 + (m - 1) * 2);
if (c == null) c = row.createCell(2 + (m - 1) * 2);
- if (v == 0) c.setBlank(); else c.setCellValue(round(v, 2));
+ if (v == 0) c.setBlank(); else c.setCellValue(v);
}
// 2025骞� 1..12 鏈堬細搴撳唴鏈夊幓骞存暟鎹垯濉紝鍚﹀垯娓呯┖妯℃澘鏍蜂緥
for (int m = 1; m <= 12; m++) {
double v = cityVal(lastYearMonthCity, area, m, k);
Cell c = row.getCell(19 + (m - 1) * 2);
if (c == null) c = row.createCell(19 + (m - 1) * 2);
- if (v == 0) c.setBlank(); else c.setCellValue(round(v, 2));
+ if (v == 0) c.setBlank(); else c.setCellValue(v);
}
}
rowIdx++;
@@ -4200,6 +5565,7 @@
hssfText(row, 0, "鍚堣");
InvestAgg prov = new InvestAgg();
for (InvestmentMonthly m : monthlies) prov.add(m, pmap.get(m.getProjectId()));
+ addProjectsWithoutMonthly(prov, projects, mByProject, null, null);
writeInvestAggHssf(row, prov);
continue;
}
@@ -4212,6 +5578,7 @@
InvestmentProject p = pmap.get(m.getProjectId());
if (p != null && blockCategory.equals(p.getCategory())) agg.add(m, p);
}
+ addProjectsWithoutMonthly(agg, projects, mByProject, blockCategory, null);
writeInvestAggHssf(row, agg);
seq = 0;
continue;
@@ -4240,6 +5607,7 @@
if (p != null && city.equals(p.getCity())
&& (blockCategory == null || blockCategory.equals(p.getCategory()))) agg.add(m, p);
}
+ addProjectsWithoutMonthly(agg, projects, mByProject, blockCategory, city);
writeInvestAggHssf(row, agg);
continue;
}
@@ -4284,9 +5652,376 @@
hssfNum(row, 8, agg.startCum == 0 ? null : agg.startCum);
hssfNum(row, 9, agg.yearPlan == 0 ? null : agg.yearPlan);
hssfNum(row, 22, agg.yearCum == 0 ? null : agg.yearCum);
- if (agg.yearPlan > 0) hssfNum(row, 23, round(agg.yearCum / agg.yearPlan, 4));
+ if (agg.yearPlan > 0) hssfNum(row, 23, agg.yearCum / agg.yearPlan);
}
+ /**
+ * 琛ヨ銆屾湰鏈熸病鏈夋湀搴﹁褰曘�佷絾椤圭洰涓绘。鏈夋�绘姇璧勩�嶇殑椤圭洰锛堝彧璁℃�绘姇璧勮繖涓�鍒楋級銆�
+ * 鍦烘櫙锛氭煇甯傚窞鏈湀鏈姤閫佸崟琛紙濡傛姹� 8 鏈堢墿娴侊級锛屼富妗i噷椤圭洰浠嶅湪锛岃嫢涓嶈ˉ璁★紝璇ュ競宸炲皬璁�/鍚堣浼氭暣鍧椾负绌恒��
+ *
+ * @param category 鍧楃被鍒紙瀹㈣繍绔欏満/鐗╂祦鍥尯锛夛紱null 琛ㄧず涓嶉檺
+ * @param city 甯傚窞鍏ㄧО锛堝 姝︽眽甯傦級锛沶ull 琛ㄧず涓嶉檺
+ */
+ private void addProjectsWithoutMonthly(InvestAgg agg, List<InvestmentProject> projects,
+ Map<Long, InvestmentMonthly> mByProject,
+ String category, String city) {
+ for (InvestmentProject p : projects) {
+ if (p == null || mByProject.containsKey(p.getId())) continue;
+ if (category != null && !category.equals(p.getCategory())) continue;
+ if (city != null && !city.equals(p.getCity())) continue;
+ if (p.getTotalInvestment() != null) agg.total += p.getTotalInvestment();
+ }
+ }
+
+
+
+ // ==================== 鎶曡祫妯″潡锛氬杩愮珯鎶曡祫鏄庣粏琛� / 鐗╂祦绔欏満鎶曡祫鏄庣粏锛堟ā鏉垮簳绋垮師浣嶆浛鎹級 ====================
+
+ /** 鐗╂祦绔欏満鏄庣粏琛ㄦā鏉夸腑鐨勫競宸炲皬鑺傜煭鍚� -> 搴撳唴瑙勮寖鍏ㄥ悕 */
+ private static final Map<String, String> LOGI_CITY_LABELS = new LinkedHashMap<>();
+ static {
+ LOGI_CITY_LABELS.put("姝︽眽", "姝︽眽甯�");
+ LOGI_CITY_LABELS.put("榛勭煶", "榛勭煶甯�");
+ LOGI_CITY_LABELS.put("鍗佸牥", "鍗佸牥甯�");
+ LOGI_CITY_LABELS.put("瀹滄槍", "瀹滄槍甯�");
+ LOGI_CITY_LABELS.put("瑗勯槼", "瑗勯槼甯�");
+ LOGI_CITY_LABELS.put("閯傚窞", "閯傚窞甯�");
+ LOGI_CITY_LABELS.put("鑽嗛棬", "鑽嗛棬甯�");
+ LOGI_CITY_LABELS.put("瀛濇劅", "瀛濇劅甯�");
+ LOGI_CITY_LABELS.put("鑽嗗窞", "鑽嗗窞甯�");
+ LOGI_CITY_LABELS.put("榛勫唸", "榛勫唸甯�");
+ LOGI_CITY_LABELS.put("鍜稿畞", "鍜稿畞甯�");
+ LOGI_CITY_LABELS.put("闅忓窞", "闅忓窞甯�");
+ LOGI_CITY_LABELS.put("鎭╂柦", "鎭╂柦宸�");
+ LOGI_CITY_LABELS.put("浠欐", "浠欐甯�");
+ LOGI_CITY_LABELS.put("娼滄睙", "娼滄睙甯�");
+ LOGI_CITY_LABELS.put("澶╅棬", "澶╅棬甯�");
+ LOGI_CITY_LABELS.put("鏋楀尯", "绁炲啘鏋舵灄鍖�");
+ }
+
+ /** 璇ユ湡鍚勯」鐩湀搴︽暟鎹細projectId -> InvestmentMonthly */
+ private Map<Long, InvestmentMonthly> investMonthlyOf(String period) {
+ Map<Long, InvestmentMonthly> map = new HashMap<>();
+ for (InvestmentMonthly m : investMonthlyMapper.selectList(
+ new LambdaQueryWrapper<InvestmentMonthly>().eq(InvestmentMonthly::getReportPeriod, period))) {
+ map.put(m.getProjectId(), m);
+ }
+ return map;
+ }
+
+ /** 鍐欏崟鍏冩牸鏁板�硷紙淇濈暀鏍峰紡/鍚堝苟锛夛細null 娓呯┖ */
+ private void putNum(Row row, int idx, Double v) {
+ if (row == null) return;
+ Cell c = row.getCell(idx);
+ if (c == null) c = row.createCell(idx);
+ c.setBlank();
+ if (v != null) c.setCellValue(v);
+ }
+
+ /** 鍐欏崟鍏冩牸鏂囨湰锛堜繚鐣欐牱寮�/鍚堝苟锛夛細绌轰覆娓呯┖ */
+ private void putText(Row row, int idx, String v) {
+ if (row == null) return;
+ Cell c = row.getCell(idx);
+ if (c == null) c = row.createCell(idx);
+ c.setBlank();
+ if (v != null && !v.trim().isEmpty()) c.setCellValue(v.trim());
+ }
+
+ /** 璇诲崟鍏冩牸鏁板�硷紙绌�/闈炴暟鍊�/鍏紡鏃犵紦瀛樹竴寰� 0锛� */
+ private double numOf(Cell c) {
+ if (c == null) return 0.0;
+ try {
+ if (c.getCellType() == CellType.NUMERIC || c.getCellType() == CellType.FORMULA) {
+ return c.getNumericCellValue();
+ }
+ String s = cellText(c);
+ if (s == null) return 0.0;
+ s = s.replace(",", "").trim();
+ return s.isEmpty() ? 0.0 : Double.parseDouble(s);
+ } catch (Exception e) {
+ return 0.0;
+ }
+ }
+
+ /** 妯℃澘鏍囬閲岀殑銆岋紙N鏈堟湀鎶ワ級銆嶃�岋紙N鏈堬級銆嶆敼涓烘姤琛ㄦ湡鏈堜唤 */
+ private void fixInvestMonthTitle(Sheet sheet, int rowIdx, int colIdx, int month) {
+ Row row = sheet.getRow(rowIdx);
+ if (row == null) return;
+ String t = cellText(row.getCell(colIdx));
+ if (t == null) return;
+ String nv = t.replaceAll("锛圽\d+鏈堟湀鎶ワ級", "锛�" + month + "鏈堟湀鎶ワ級")
+ .replaceAll("锛圽\d+鏈堬級", "锛�" + month + "鏈堬級");
+ if (!nv.equals(t)) putText(row, colIdx, nv);
+ }
+
+ /** 妯℃澘琛� A 鍒楁槸鍚︿负銆屼竴浜屼笁鍥涖�併�嶆笭閬撳垎缁勬爣棰� */
+ private boolean isInvestChannelLabel(String a) {
+ return a != null && a.matches("^[涓�浜屼笁鍥涗簲鍏竷鍏節鍗乚+銆�.*");
+ }
+
+ /**
+ * 鐢熸垚_X鏈堝杩愮珯鎶曡祫鏄庣粏琛�.xlsx锛堟ā鏉匡細docs/鎶曡祫/妯℃澘/妯℃澘_X鏈堝杩愮珯鎶曡祫鏄庣粏琛�.xlsx锛宻heet锛氭槑缁� + 姹囨�伙級銆�
+ * 鍙e緞锛氭ā鏉垮嵆涓婃湀鍙拌处搴曠锛涙湰鏈堝彧瑕嗙洊銆屽簱鍐呮湁璇ユ湡鏁版嵁銆嶇殑椤圭洰琛�
+ * 锛圚鑷紑濮嬪缓璁剧疮璁� / I鏈勾璁″垝 / J鑷勾鍒濈疮璁� / K鏈湀瀹屾垚 / L寤鸿闃舵 / M褰㈣薄杩涘害锛夛紝
+ * 搴撳唴鏃犺鏈熺殑椤圭洰琛屼繚鐣欐瘝鐗堝師鍊硷紙绛夊悓浜哄伐銆屼笂鏈堢粨杞� + 鏈湀鏇存柊銆嶏級锛�
+ * 鍩庡競/娓犻亾/鍚堣琛屾部鐢ㄦā鏉� SUM 鍏紡锛岄殢琛ㄥ唴鏁板�艰嚜鍔ㄩ噸绠椼��
+ */
+ public byte[] exportInvestPassengerDetail(String period, String mode) throws Exception {
+ int year = Integer.parseInt(period.split("-")[0]);
+ int month = monthOf(period, mode);
+ List<InvestmentProject> projects = investProjectMapper.selectList(
+ new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, "瀹㈣繍绔欏満"));
+ Map<Long, InvestmentMonthly> mByProject = investMonthlyOf(period);
+ File tpl = resolveAnyTemplate(investTemplateDir, "妯℃澘_X鏈堝杩愮珯鎶曡祫鏄庣粏琛�.xlsx");
+ try (InputStream in = new FileInputStream(tpl);
+ XSSFWorkbook wb = new XSSFWorkbook(in)) {
+ Sheet sheet = wb.getSheet("鏄庣粏");
+ if (sheet == null) throw new RuntimeException("妯℃澘銆婃ā鏉縚X鏈堝杩愮珯鎶曡祫鏄庣粏琛�.xlsx銆嬬己灏戙�屾槑缁嗐�嶅伐浣滆〃");
+ fixInvestMonthTitle(sheet, 0, 1, month);
+ String curCity = null;
+ Set<Long> used = new HashSet<>();
+ int filled = 0, kept = 0, dup = 0;
+ for (int r = 8; r <= sheet.getLastRowNum(); r++) {
+ Row row = sheet.getRow(r);
+ if (row == null) continue;
+ String a = nvl(cellText(row.getCell(0)));
+ String b = nvl(cellText(row.getCell(1)));
+ if (isCityLabel(a)) { curCity = templateCityFull(a); continue; }
+ if (isInvestChannelLabel(a)) continue;
+ if (a.contains("鍚堣") || a.contains("鎬昏")) continue;
+ if (!a.matches("\\d+(\\.\\d+)?")) continue;
+ if (b.isEmpty() || curCity == null) continue;
+ InvestmentProject p = matchInvest(projects, b, curCity, "瀹㈣繍绔欏満", used);
+ InvestmentMonthly m = p == null ? null : mByProject.get(p.getId());
+ if (p == null || m == null) {
+ kept++;
+ // 鏈夊簱鍐呴」鐩絾褰撴湀鏃犳暟鎹� 鈫� 璁颁负寰呮牳瀵癸紱鏃犲尮閰嶉」鐩� 鈫� 缁撹浆涓婃湀
+ if (p != null) dup++;
+ continue;
+ }
+ used.add(p.getId());
+ // 鍙e緞锛堢敤鎴� 2026-09-18 纭锛夛細鍙鐩栦汉宸ヤ細闅忔湀鎶ユ洿鏂扮殑鍥涘垪鈥斺��
+ // H 鑷紑濮嬪缓璁剧疮璁� / J 鑷勾鍒濈疮璁� / K 鏈湀瀹屾垚 / M 褰㈣薄杩涘害锛�
+ // I 鏈勾璁″垝鎶曡祫銆丩 寤鸿闃舵銆丟 璁″垝鎬绘姇璧勪负鐪佸畾/姣嶇増鍙e緞锛屼竴寰嬩笉鍔紱搴撳唴涓虹┖鍒欎繚鐣欐瘝鐗堝師鍊�
+ if (m.getStartCum() != null) putNum(row, 7, m.getStartCum());
+ if (m.getYearCum() != null) putNum(row, 9, m.getYearCum());
+ if (m.getMonthDone() != null) putNum(row, 10, m.getMonthDone());
+ if (m.getProgressDesc() != null && !m.getProgressDesc().trim().isEmpty()) putText(row, 12, m.getProgressDesc());
+ filled++;
+ }
+ Sheet sumSheet = wb.getSheet("姹囨��");
+ if (sumSheet != null) {
+ Row r2 = sumSheet.getRow(1);
+ if (r2 != null) {
+ Cell c = r2.getCell(0);
+ if (c == null) c = r2.createCell(0);
+ c.setBlank();
+ c.setCellValue(java.time.LocalDate.of(year, month, 1).atStartOfDay());
+ }
+ }
+ try {
+ wb.getCreationHelper().createFormulaEvaluator().evaluateAll();
+ } catch (Exception e) {
+ log.warn("瀹㈣繍绔欐姇璧勬槑缁嗚〃鍏紡姹傚�煎け璐�: {}", e.getMessage());
+ }
+ log.info("瀹㈣繍绔欐姇璧勬槑缁嗚〃 {}锛氬啓鍏� {} 琛岋紝缁撹浆姣嶇増鍘熷�� {} 琛岋紙鍏朵腑搴撳唴鏈夐」鐩絾褰撴湀鏃犳暟鎹� {} 琛岋級",
+ period, filled, kept, dup);
+ return toBytes(wb);
+ }
+ }
+
+ /**
+ * 鐢熸垚_X鏈堢墿娴佺珯鍦烘姇璧勬槑缁�.xls锛堟ā鏉匡細docs/鎶曡祫/妯℃澘/妯℃澘_X鏈堢墿娴佺珯鍦烘姇璧勬槑缁�.xls锛夈��
+ * 瑕嗙洊銆� 鍒嗛」鐩姇璧勫畬鎴愭儏鍐点�嶉」鐩锛堝彛寰勫悓瀹㈣繍绔欐槑缁嗚〃锛夛紝骞舵寜琛ㄥ唴鏁板�奸噸绠�
+ * 娓犻亾琛� / 甯傚窞琛� / 鍚堣琛岋紝鍚屾鍒锋柊銆屽垎甯傚窞瑙勫垝鍐呭椤圭洰瀹屾垚鎯呭喌銆嶃�屽競宸炴眹鎬汇�嶄袱寮犳眹鎬婚〉銆�
+ */
+ public byte[] exportInvestLogisticsDetail(String period, String mode) throws Exception {
+ int year = Integer.parseInt(period.split("-")[0]);
+ int month = monthOf(period, mode);
+ List<InvestmentProject> projects = investProjectMapper.selectList(
+ new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, "鐗╂祦鍥尯"));
+ Map<Long, InvestmentMonthly> mByProject = investMonthlyOf(period);
+ HSSFWorkbook wb = loadInvestTemplate("妯℃澘_X鏈堢墿娴佺珯鍦烘姇璧勬槑缁�.xls");
+ HSSFSheet sheet = wb.getSheet(" 鍒嗛」鐩姇璧勫畬鎴愭儏鍐�");
+ if (sheet == null) throw new RuntimeException("妯℃澘銆婃ā鏉縚X鏈堢墿娴佺珯鍦烘姇璧勬槑缁�.xls銆嬬己灏戙�� 鍒嗛」鐩姇璧勫畬鎴愭儏鍐点�嶅伐浣滆〃");
+ fixInvestMonthTitle(sheet, 0, 0, month);
+ Row head = sheet.getRow(3);
+ if (head != null) {
+ String t = cellText(head.getCell(0));
+ if (t != null) {
+ String nv = t.replaceAll("\\d{4}骞碶\d+鏈�", year + "骞�" + month + "鏈�");
+ if (!nv.equals(t)) putText(head, 0, nv);
+ }
+ }
+ // 1) 缁撴瀯鎵弿锛氬競宸炲皬鑺傝
+ List<Integer> cityRows = new ArrayList<>();
+ for (int r = 7; r <= sheet.getLastRowNum(); r++) {
+ HSSFRow row = sheet.getRow(r);
+ if (row == null) continue;
+ String a = nvl(cellText(row.getCell(0)));
+ String b = nvl(cellText(row.getCell(1)));
+ if (LOGI_CITY_LABELS.containsKey(a) && b.isEmpty()) cityRows.add(r);
+ }
+ Map<String, double[]> citySums = new LinkedHashMap<>(); // 鍏ㄥ悕 -> G,H,I,J,K
+ Map<String, double[]> cityStats = new LinkedHashMap<>(); // 鍏ㄥ悕 -> {鎬绘暟, 绱, 瑙勫垝鍐呮暟, 瑙勫垝鍐呯疮璁�, 瑙勫垝澶栨暟, 瑙勫垝澶栫疮璁
+ Set<Long> used = new HashSet<>();
+ int filled = 0, kept = 0;
+ for (int i = 0; i < cityRows.size(); i++) {
+ int cr = cityRows.get(i);
+ String cityLabel = nvl(cellText(sheet.getRow(cr).getCell(0)));
+ String city = LOGI_CITY_LABELS.getOrDefault(cityLabel, cityLabel);
+ int start = cr + 1;
+ int end = (i + 1 < cityRows.size() ? cityRows.get(i + 1) : sheet.getLastRowNum() + 1) - 1;
+ List<Integer> chRows = new ArrayList<>();
+ for (int r = start; r <= end; r++) {
+ HSSFRow row = sheet.getRow(r);
+ if (row == null) continue;
+ if (isInvestChannelLabel(nvl(cellText(row.getCell(0))))) chRows.add(r);
+ }
+ // (1) 鍏堟寜椤圭洰鍚�+甯傚窞 瑕嗙洊鏈夊簱鍐呭綋鏈熸暟鎹殑椤圭洰琛�
+ for (int r = start; r <= end; r++) {
+ HSSFRow row = sheet.getRow(r);
+ if (row == null) continue;
+ String a = nvl(cellText(row.getCell(0)));
+ String b = nvl(cellText(row.getCell(1)));
+ if (isInvestChannelLabel(a)) continue;
+ if (!a.matches("\\d+(\\.\\d+)?")) continue;
+ if (b.isEmpty()) continue;
+ InvestmentProject p = matchInvest(projects, b, city, "鐗╂祦鍥尯", used);
+ InvestmentMonthly m = p == null ? null : mByProject.get(p.getId());
+ if (p == null || m == null) { kept++; continue; }
+ used.add(p.getId());
+ // 鍙e緞鍚屽杩愮珯鏄庣粏琛細鍙鐩� H 鑷紑濮嬪缓璁剧疮璁� / J 鑷勾鍒濈疮璁� / K 鏈湀瀹屾垚 / M 褰㈣薄杩涘害
+ if (m.getStartCum() != null) putNum(row, 7, m.getStartCum());
+ if (m.getYearCum() != null) putNum(row, 9, m.getYearCum());
+ if (m.getMonthDone() != null) putNum(row, 10, m.getMonthDone());
+ if (m.getProgressDesc() != null && !m.getProgressDesc().trim().isEmpty()) putText(row, 12, m.getProgressDesc());
+ filled++;
+ }
+ // (2) 鍐嶆寜娓犻亾鍧楁眹鎬伙紝骞跺啓鍥炴笭閬撹 / 甯傚窞琛�
+ double[] citySum = new double[5];
+ double[] stat = new double[6];
+ for (int j = 0; j < chRows.size(); j++) {
+ int r0 = chRows.get(j);
+ int r1 = (j + 1 < chRows.size() ? chRows.get(j + 1) : end + 1) - 1;
+ boolean planned = !nvl(cellText(sheet.getRow(r0).getCell(0))).startsWith("鍥涖��");
+ double[] s = new double[5];
+ for (int r = r0 + 1; r <= r1; r++) {
+ HSSFRow row = sheet.getRow(r);
+ if (row == null) continue;
+ String a = nvl(cellText(row.getCell(0)));
+ String b = nvl(cellText(row.getCell(1)));
+ if (isInvestChannelLabel(a) || !a.matches("\\d+(\\.\\d+)?")) continue;
+ if (b.isEmpty()) continue;
+ for (int k = 0; k < 5; k++) s[k] += numOf(row.getCell(6 + k));
+ stat[0] += 1;
+ stat[1] += numOf(row.getCell(9));
+ if (planned) { stat[2] += 1; stat[3] += numOf(row.getCell(9)); }
+ else { stat[4] += 1; stat[5] += numOf(row.getCell(9)); }
+ }
+ HSSFRow crow = sheet.getRow(r0);
+ for (int k = 0; k < 5; k++) putNum(crow, 6 + k, s[k] == 0 ? null : s[k]);
+ for (int k = 0; k < 5; k++) citySum[k] += s[k];
+ }
+ // 娓犻亾鍧椾箣澶栫殑娓哥椤圭洰琛岋紙妯℃澘鏈垎缁勬椂锛変篃璁″叆甯傚窞鍚堣
+ for (int r = start; r <= end; r++) {
+ HSSFRow row = sheet.getRow(r);
+ if (row == null) continue;
+ String a = nvl(cellText(row.getCell(0)));
+ String b = nvl(cellText(row.getCell(1)));
+ if (isInvestChannelLabel(a) || !a.matches("\\d+(\\.\\d+)?")) continue;
+ if (b.isEmpty()) continue;
+ boolean inChannel = false;
+ for (int j = 0; j < chRows.size(); j++) {
+ int r0 = chRows.get(j);
+ int r1 = (j + 1 < chRows.size() ? chRows.get(j + 1) : end + 1) - 1;
+ if (r > r0 && r <= r1) { inChannel = true; break; }
+ }
+ if (inChannel) continue;
+ for (int k = 0; k < 5; k++) citySum[k] += numOf(row.getCell(6 + k));
+ stat[0] += 1;
+ stat[1] += numOf(row.getCell(9));
+ }
+ HSSFRow cityRow = sheet.getRow(cr);
+ for (int k = 0; k < 5; k++) putNum(cityRow, 6 + k, citySum[k] == 0 ? null : citySum[k]);
+ citySums.put(city, citySum);
+ cityStats.put(city, stat);
+ }
+ // 2) 鍚堣琛岋紙绗� 7 琛岋級
+ double[] total = new double[5];
+ for (double[] s : citySums.values()) {
+ for (int k = 0; k < 5; k++) total[k] += s[k];
+ }
+ for (int k = 0; k < 5; k++) putNum(sheet.getRow(6), 6 + k, total[k] == 0 ? null : total[k]);
+
+ // 3) 鍒嗗競宸炶鍒掑唴澶栭」鐩畬鎴愭儏鍐�
+ HSSFSheet citySheet = wb.getSheet("鍒嗗競宸炶鍒掑唴澶栭」鐩畬鎴愭儏鍐�");
+ if (citySheet != null) {
+ Row t1 = citySheet.getRow(0);
+ if (t1 != null) {
+ String t = cellText(t1.getCell(0));
+ if (t != null) {
+ String nv = t.replaceAll("\\d{4}骞�", year + "骞�");
+ if (!nv.equals(t)) putText(t1, 0, nv);
+ }
+ }
+ double[] st = new double[6];
+ for (int r = 3; r <= citySheet.getLastRowNum(); r++) {
+ HSSFRow row = citySheet.getRow(r);
+ if (row == null) continue;
+ String a = nvl(cellText(row.getCell(0)));
+ String b = nvl(cellText(row.getCell(1)));
+ if (a.contains("鍚堣") || b.contains("鍚堣")) {
+ for (int k = 0; k < 6; k++) putNum(row, 2 + k, st[k] == 0 ? null : st[k]);
+ continue;
+ }
+ double[] s = cityStats.get(LOGI_CITY_LABELS.getOrDefault(b, b));
+ if (s == null) continue;
+ for (int k = 0; k < 6; k++) {
+ putNum(row, 2 + k, s[k] == 0 ? null : s[k]);
+ st[k] += s[k];
+ }
+ }
+ }
+
+ // 4) 甯傚窞姹囨�伙紙B 骞村害璁″垝鎶曡祫涓哄畼鏂瑰彛寰勶紝淇濇寔妯℃澘鍘熷�硷紱C/D/E 鎸夊垎椤圭洰椤甸噸绠楋級
+ HSSFSheet sumSheet = wb.getSheet("甯傚窞姹囨��");
+ if (sumSheet != null) {
+ HSSFRow r2 = sumSheet.getRow(1);
+ if (r2 != null) {
+ putNum(r2, 0, (double) org.apache.poi.ss.usermodel.DateUtil.getExcelDate(
+ java.time.LocalDate.of(year, month, 1).atStartOfDay()));
+ }
+ double cT = 0.0, dT = 0.0;
+ for (int r = 7; r <= 23; r++) {
+ HSSFRow row = sumSheet.getRow(r);
+ if (row == null) continue;
+ String a = nvl(cellText(row.getCell(0)));
+ double[] s = citySums.get(LOGI_CITY_LABELS.getOrDefault(a, a));
+ if (s == null) continue;
+ putNum(row, 2, s[3] == 0 ? null : s[3]);
+ putNum(row, 3, s[4] == 0 ? null : s[4]);
+ double b = numOf(row.getCell(1));
+ putNum(row, 4, b > 0 ? s[3] / b * 100 : null);
+ cT += s[3];
+ dT += s[4];
+ }
+ HSSFRow tot = sumSheet.getRow(6);
+ if (tot != null) {
+ putNum(tot, 2, cT == 0 ? null : cT);
+ putNum(tot, 3, dT == 0 ? null : dT);
+ double b = numOf(tot.getCell(1));
+ putNum(tot, 4, b > 0 ? cT / b * 100 : null);
+ }
+ }
+ try {
+ wb.getCreationHelper().createFormulaEvaluator().evaluateAll();
+ } catch (Exception e) {
+ log.warn("鐗╂祦绔欏満鎶曡祫鏄庣粏鍏紡姹傚�煎け璐�: {}", e.getMessage());
+ }
+ log.info("鐗╂祦绔欏満鎶曡祫鏄庣粏 {}锛氬啓鍏� {} 琛岋紝缁撹浆姣嶇増鍘熷�� {} 琛�", period, filled, kept);
+ return toBytesHssf(wb);
+ }
private static class InvestAgg {
double total, startCum, yearPlan, yearCum;
@@ -4497,7 +6232,7 @@
for (InvestmentMonthly m : monthlies) provinceYearPlan += nz(m.getYearPlan());
HSSFRow rowOther = s1.getRow(9);
for (int c = 1; c <= 5; c++) hssfNum(rowOther, c, null);
- hssfNum(rowOther, 5, provinceYearPlan == 0 ? null : round(provinceYearPlan / 10000, 2));
+ hssfNum(rowOther, 5, provinceYearPlan == 0 ? null : provinceYearPlan / 10000);
// ---- sheet2 瑙勪笂椤圭洰琛細琛ㄥご骞翠唤 + 鍚堣 + 椤圭洰琛� ----
HSSFSheet s2 = wb.getSheetAt(1);
hssfText(s2.getRow(0), 0, year + "骞翠氦閫氬缓璁鹃」鐩偍澶囨儏鍐碉紙瑙勬ā浠ヤ笂椤圭洰锛�");
@@ -4729,6 +6464,8 @@
private double nz(Double v) {
return v == null ? 0.0 : v;
}
+ /** 鏁板�兼牸寮忛噷 3 浣嶅強浠ヤ笂鐨勫皬鏁伴儴鍒嗭紙濡� 0.0000 / 0.000_ 锛� */
+ private static final java.util.regex.Pattern DEC_MORE_THAN_TWO = java.util.regex.Pattern.compile("\\.0{3,}");
/** 鐢熸垚琛ㄦ暟鍊煎垪缁熶竴鏍峰紡锛氫繚鐣欏叏绮惧害鏁板�硷紝鏄剧ず涓や綅灏忔暟锛涙暣鏁板�间笌 %/鏃ユ湡/绉戝璁℃暟绛夋棦鏈夋牱寮忎笉鏀瑰彉 */
private void applyTwoDecimalFormat(org.apache.poi.ss.usermodel.Workbook wb) {
if (wb == null) return;
@@ -4745,24 +6482,32 @@
CellType ct = cell.getCellType();
if (ct != CellType.NUMERIC && ct != CellType.FORMULA) continue;
if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) continue;
- boolean integral = false;
- if (ct == CellType.NUMERIC) {
- integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue();
- } else if (cell.getCachedFormulaResultType() == CellType.NUMERIC) {
- integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue();
- }
- if (integral) continue; // 鏁存暟涓嶆樉绀� .00锛堝惈鍏紡缁撴灉涓烘暣鏁扮殑鏍硷級
org.apache.poi.ss.usermodel.CellStyle cs = cell.getCellStyle();
if (cs == null) continue;
String f = cs.getDataFormatString();
if (f == null) f = "";
- if (f.contains("%") || f.contains("E") || f.contains("0.00")) continue;
- boolean plain = f.isEmpty() || "General".equals(f) || "general".equals(f)
- || "0".equals(f) || "#,##0".equals(f) || "0.0".equals(f) || "#,##0.0".equals(f);
- if (!plain) continue;
+ if (f.contains("%") || f.contains("E") || f.contains("@")) continue;
+ String normalized = null;
+ if (DEC_MORE_THAN_TWO.matcher(f).find()) {
+ // 姣嶇増涓埆鏍煎甫 3~4 浣嶅皬鏁版牸寮忥紙濡� 0.000_ 銆�0.0000_);[Red]\(0.0000\)锛夛細鏁板�间粛鍏ㄧ簿搴︼紝
+ // 灞曠ず缁熶竴鏀舵暃鍒颁袱浣嶃�傝繖绫绘牸鏈氨鎸夋暟鍊兼牸寮忓睍绀猴紝鏁呬笉鍙椼�屾暣鏁颁笉鏄剧ず .00銆嶄繚鎶ゅ奖鍝嶃��
+ normalized = DEC_MORE_THAN_TWO.matcher(f).replaceAll(".00");
+ } else {
+ boolean integral = false;
+ if (ct == CellType.NUMERIC) {
+ integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue();
+ } else if (cell.getCachedFormulaResultType() == CellType.NUMERIC) {
+ integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue();
+ }
+ if (integral) continue; // 鏁存暟涓嶆樉绀� .00锛堝惈鍏紡缁撴灉涓烘暣鏁扮殑鏍硷級
+ if (f.contains("0.00")) continue;
+ boolean plain = f.isEmpty() || "General".equals(f) || "general".equals(f)
+ || "0".equals(f) || "#,##0".equals(f) || "0.0".equals(f) || "#,##0.0".equals(f);
+ if (!plain) continue;
+ }
org.apache.poi.ss.usermodel.CellStyle ns = wb.createCellStyle();
ns.cloneStyleFrom(cs);
- ns.setDataFormat(fmtTwo);
+ ns.setDataFormat(normalized == null ? fmtTwo : df.getFormat(normalized));
cell.setCellStyle(ns);
} catch (Exception ignore) {
// 涓埆鍗曞厓鏍兼牸寮忓紓甯镐笉闃诲瀵煎嚭
@@ -4773,59 +6518,26 @@
}
/** 姹囨�昏〃瀵煎嚭鍚庢寜鍐呭鑷姩鍔犲鍒楀锛堝彧鍔犲涓嶇缉绐勶紱璺宠繃鍚堝苟鍗曞厓鏍兼爣棰橈紝闃查暱鏍囬鎶婂崟鍒楁拺鐖嗭級 */
- private void autoFitContentColumns(org.apache.poi.ss.usermodel.Workbook wb) {
+ /** 姹囨�诲ぇ琛ㄨ緭鍑哄墠娓呯悊姣嶇増娈嬬暀涓嬫潵鐨勭瓫閫変笌琛岄殣钘忥細绛涢�夌澶�/闅愯棌琛屼細璁╂暣椤电湅璧锋潵鈥滅己鏁版嵁鈥� */
+ private void cleanSummarySheetPresentation(org.apache.poi.ss.usermodel.Workbook wb) {
if (wb == null) return;
- org.apache.poi.ss.usermodel.DataFormatter dfmt = new org.apache.poi.ss.usermodel.DataFormatter();
for (int s = 0; s < wb.getNumberOfSheets(); s++) {
org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(s);
if (sh == null) continue;
- java.util.Set<String> merged = new java.util.HashSet<>();
- for (org.apache.poi.ss.util.CellRangeAddress ra : sh.getMergedRegions()) {
- for (int r = ra.getFirstRow(); r <= ra.getLastRow(); r++) {
- for (int c = ra.getFirstColumn(); c <= ra.getLastColumn(); c++) {
- merged.add(r + ":" + c);
- }
+ if (sh instanceof org.apache.poi.xssf.usermodel.XSSFSheet) {
+ org.apache.poi.xssf.usermodel.XSSFSheet xs = (org.apache.poi.xssf.usermodel.XSSFSheet) sh;
+ if (xs.getCTWorksheet().isSetAutoFilter()) xs.getCTWorksheet().unsetAutoFilter();
+ for (org.apache.poi.ss.usermodel.Row row : xs) {
+ if (row == null) continue;
+ org.apache.poi.xssf.usermodel.XSSFRow xr = (org.apache.poi.xssf.usermodel.XSSFRow) row;
+ if (xr.getCTRow().isSetHidden()) xr.getCTRow().unsetHidden();
}
- }
- int maxCol = -1;
- for (org.apache.poi.ss.usermodel.Row row : sh) {
- if (row == null) continue;
- maxCol = Math.max(maxCol, (int) row.getLastCellNum() - 1);
- }
- if (maxCol < 0) continue;
- int[] need = new int[maxCol + 1];
- for (org.apache.poi.ss.usermodel.Row row : sh) {
- if (row == null) continue;
- for (org.apache.poi.ss.usermodel.Cell cell : row) {
- int c = cell.getColumnIndex();
- if (c > maxCol || merged.contains(row.getRowNum() + ":" + c)) continue;
- String txt;
- try {
- txt = dfmt.formatCellValue(cell);
- } catch (Exception ignore) {
- continue;
- }
- if (txt == null || txt.isEmpty()) continue;
- int units = 0;
- for (int i = 0; i < txt.length(); i++) {
- units += isWideChar(txt.charAt(i)) ? 2 : 1;
- }
- if (units > need[c]) need[c] = Math.min(units, 34); // 闀挎枃鏈笉鎶婂崟鍒楁拺鐖�
- }
- }
- for (int c = 0; c <= maxCol; c++) {
- if (need[c] <= 0) continue;
- int target = Math.min(need[c] + 2, 46) * 256;
- if (target > sh.getColumnWidth(c)) sh.setColumnWidth(c, target);
+ } else {
+ sh.setAutoFilter(null);
}
}
}
- /** CJK / 鍏ㄨ瀛楃鎸変袱鍊嶅搴﹁锛堣繎浼� Excel 鏄剧ず鍒楀锛� */
- private boolean isWideChar(char ch) {
- return (ch >= 0x2E80 && ch <= 0x9FFF) || (ch >= 0xF900 && ch <= 0xFAFF)
- || (ch >= 0xFF00 && ch <= 0xFFEF);
- }
private byte[] toBytes(XSSFWorkbook wb) throws Exception {
applyTwoDecimalFormat(wb);
--
Gitblit v1.9.1