From 71a77e721619e956cab116be691cc3ab58e18093 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期二, 22 九月 2026 15:35:09 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/main'

---
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java | 1009 ++++++++++++++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 866 insertions(+), 143 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 1950983..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,9 +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;
@@ -98,6 +101,10 @@
     private CityTaxiMonthlyMapper cityTaxiMapper;
     @Resource
     private SummaryWorkbookFiller summaryWorkbookFiller;
+    @Resource
+    private WycSplitCalc wycSplitCalc;
+    @Resource
+    private WycOrderMonthlyMapper wycOrderMapper;
     @Resource
     private AuditResultMapper auditResultMapper;
     @Resource
@@ -217,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:
@@ -374,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);
@@ -405,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) {
@@ -661,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) {
@@ -833,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));
         }
 
@@ -887,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);
 
@@ -1179,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();
@@ -1215,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);
         }
     }
 
@@ -1253,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 */
@@ -1355,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);
@@ -1570,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;
     }
@@ -1613,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;
     }
@@ -1635,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);
@@ -1666,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);
@@ -1696,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);
@@ -1725,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);
@@ -1962,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);
@@ -2056,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锛� */
@@ -2181,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) {
@@ -2205,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();
+            }
         }
     }
 
@@ -2256,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锛� */
@@ -2290,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 &lt;= 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 {
@@ -2310,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;
@@ -2332,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;
@@ -2351,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;
@@ -2366,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锛� */
@@ -2431,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));
                 }
@@ -2483,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);
@@ -2505,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++) {
@@ -2521,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);
     }
 
     /** 鍐欐暟鍊兼垨娓呯┖锛堝叕寮忓崟鍏冩牸淇濈暀涓嶅姩锛� */
@@ -2554,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 鏈堟眰鍜岋級 */
@@ -2596,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);
@@ -2612,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++) {
@@ -3032,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锛� */
@@ -3313,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 瑁佸壀锛夛細
@@ -3321,6 +3627,13 @@
      *  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);
@@ -3328,9 +3641,11 @@
             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)) {
@@ -3349,26 +3664,42 @@
             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, rankRefreshed, cumulativeRefreshed, widthApplied, dbProblems);
+                    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) {
+            // 杩涘害鍥炶皟澶辫触涓嶅奖鍝嶅鍑�
         }
     }
 
@@ -3385,6 +3716,14 @@
         } 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<>();
@@ -3527,7 +3866,7 @@
         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), new int[][]{{0, 0}, {0, 10}});
+        n += copyRankValues(wb, "涓彛寰勬帓鍚�", exportPassengerMidRank(period, mode, wb), new int[][]{{0, 0}, {0, 10}});
         return n;
     }
 
@@ -4488,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;
     }
@@ -4734,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);
@@ -4759,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");
@@ -4801,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++;
@@ -5222,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;
             }
@@ -5234,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;
@@ -5262,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;
             }
@@ -5306,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;
@@ -5519,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 + "骞翠氦閫氬缓璁鹃」鐩偍澶囨儏鍐碉紙瑙勬ā浠ヤ笂椤圭洰锛�");
@@ -5751,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;
@@ -5767,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) {
                         // 涓埆鍗曞厓鏍兼牸寮忓紓甯镐笉闃诲瀵煎嚭

--
Gitblit v1.9.1