From a08c70db67e49bf6cd125df04ebaf68c26f0d8dd Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期日, 20 九月 2026 16:19:16 +0800
Subject: [PATCH] feat(投资): 市州月报统一填报模板 + 县(市、区)列解析 + 表头行判定收紧

---
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java |  247 +++++++++++++++++++++++++++++++++++--------------
 1 files changed, 175 insertions(+), 72 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 6673e20..d4673d6 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
@@ -226,6 +226,36 @@
                     }
                     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:
                     rows = 0;
                     notes.add("鏈煡鎶ヨ〃绫诲瀷锛�" + t);
@@ -381,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);
@@ -412,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) {
@@ -668,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) {
@@ -840,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));
         }
 
@@ -894,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);
 
@@ -1244,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 */
@@ -1346,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);
@@ -1561,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;
     }
@@ -1604,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;
     }
@@ -1626,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);
@@ -1657,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);
@@ -1687,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);
@@ -1716,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);
@@ -1953,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);
@@ -2047,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锛� */
@@ -2286,7 +2333,7 @@
         if (v == 0.0) return; // 搴撳唴鏃犳簮锛氫繚鐣欐瘝鐗�/瀹氱鍘熷�硷紝缂烘暟鎹笉瑕嗙洊
         // POI setCellValue(double) 瀵瑰叕寮忓崟鍏冩牸鍙洿鏂扮紦瀛樹笉绉婚櫎鍏紡锛屽繀椤诲厛 setBlank 鍐嶅啓鍊�
         c.setBlank();
-        c.setCellValue(round(v, 4));
+        c.setCellValue(v);
     }
 
     /** 鏄惧紡鍐欏叆 0锛圥OI setCellValue(0.0) 浼氳浆 blank锛岄渶鎿嶄綔搴曞眰 XML锛� */
@@ -2582,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));
                 }
@@ -2670,18 +2717,18 @@
                 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, 1, pass == 0 ? null : pass);
                 setValOrBlank(sheet, r0, 3, midCumYoy(mid, currentYear, city, maxMonth, 0, true));
-                setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4));
+                setValOrBlank(sheet, r0, 5, turn == 0 ? null : turn);
                 setValOrBlank(sheet, r0, 7, midCumYoy(mid, currentYear, city, maxMonth, 0, false));
                 // 鍙冲潡褰撴湀锛歀/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, 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 : round(mTurn, 4));
+                setValOrBlank(sheet, r0, 15, mTurn == 0 ? null : mTurn);
                 setValOrBlank(sheet, r0, 17, mTurn == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, city, 0, false));
             }
             recalc(wb);
@@ -2701,7 +2748,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 鏈堟眰鍜岋級 */
@@ -2743,9 +2790,9 @@
                 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, 1, pass == 0 ? null : pass);
                 setValOrBlank(sheet, r0, 2, midCumYoy(mid, currentYear, "婀栧寳鐪�", maxMonth, dims[i], true));
-                setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4));
+                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锛�
@@ -2759,9 +2806,9 @@
                 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, 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 : round(turn, 4));
+                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
@@ -3179,7 +3226,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锛� */
@@ -3460,7 +3507,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 瑁佸壀锛夛細
@@ -3468,6 +3515,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);
@@ -3475,9 +3529,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)) {
@@ -3496,26 +3552,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) {
+            // 杩涘害鍥炶皟澶辫触涓嶅奖鍝嶅鍑�
         }
     }
 
@@ -4643,8 +4715,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;
     }
@@ -4960,19 +5032,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++;
@@ -5381,6 +5453,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;
             }
@@ -5393,6 +5466,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;
@@ -5421,6 +5495,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;
             }
@@ -5465,7 +5540,25 @@
         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();
+        }
     }
 
 
@@ -5797,7 +5890,7 @@
                 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 ? round(s[3] / b * 100, 4) : null);
+                putNum(row, 4, b > 0 ? s[3] / b * 100 : null);
                 cT += s[3];
                 dT += s[4];
             }
@@ -5806,7 +5899,7 @@
                 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 ? round(cT / b * 100, 4) : null);
+                putNum(tot, 4, b > 0 ? cT / b * 100 : null);
             }
         }
         try {
@@ -6027,7 +6120,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 + "骞翠氦閫氬缓璁鹃」鐩偍澶囨儏鍐碉紙瑙勬ā浠ヤ笂椤圭洰锛�");
@@ -6259,6 +6352,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;
@@ -6275,24 +6370,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