From da614c0b85465f92453405928534d5fa2adc3c8c Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期五, 11 九月 2026 16:43:08 +0800
Subject: [PATCH] feat: 网约车订单运行监测报告(docx)导入 + 汇总大表8月数据驱动回填与版式对齐

---
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java |  380 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 372 insertions(+), 8 deletions(-)

diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java
index 435309b..1abdfca 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java
@@ -1,6 +1,8 @@
 package com.trafficaudit.reportexport.calc;
 
 import com.trafficaudit.common.util.RegionUtil;
+import com.trafficaudit.reportexport.calc.FreightCalc.FreightMatrix;
+import com.trafficaudit.reportexport.calc.FreightCalc.FreightMetrics;
 import org.apache.poi.ss.usermodel.Cell;
 import org.apache.poi.ss.usermodel.Row;
 import org.apache.poi.ss.usermodel.Sheet;
@@ -36,12 +38,16 @@
     private MidCalc midCalc;
     @Resource
     private PaxCalc paxCalc;
+    @Resource
+    private FreightCalc freightCalc;
+    @Resource
+    private WycSplitCalc wycSplitCalc;
 
     /** 2026 骞� 1 鏈堝綋鏈堝�煎垪锛�0 鍩猴紝鍗� C 鍒楋級 */
     private static final int FIRST_MONTH_COL = 2;
 
     /** 鍥炲~ 1..keepMonths 鏈堝彴璐﹂〉锛岃繑鍥炲啓鍏ユ牸鏁帮紱缂烘暟鎻愮ず鍐欏叆 problems */
-    public int fillMonthlyLedger(XSSFWorkbook wb, int year, int keepMonths, List<String> problems) {
+    public int fillMonthlyLedger(XSSFWorkbook wb, String period, int year, int keepMonths, List<String> problems) {
         if (wb == null || keepMonths <= 0) return 0;
         if (problems == null) problems = new ArrayList<>();
         String yearPrefix = year + "-";
@@ -50,6 +56,8 @@
         filled += fillCityRows(wb, "鍏氦", paxCalc.busMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false);
         filled += fillCityRows(wb, "鍑虹", paxCalc.taxiMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false);
         filled += fillTrackFerry(wb, paxCalc.railFerryMonthly(yearPrefix, keepMonths), year, keepMonths, problems);
+        filled += fillFreightSheet(wb, period, keepMonths, problems);
+        filled += fillWycSheet(wb, period, keepMonths, problems);
         return filled;
     }
 
@@ -117,6 +125,177 @@
             }
         }
         return filled;
+    }
+
+    /** 璐ц繍椤靛洖濉細鍏ㄧ渷鍧� + 17 甯傚窞鍧楃殑 瑙勪笂/瑙勪笅/鍚堣 脳 璐ц繍閲忋�佸懆杞噺锛堥�愭湀鐜扮畻锛涘綋鏈堝�煎垪=2+2*(m-1)锛� */
+    private int fillFreightSheet(XSSFWorkbook wb, String period, int keepMonths, List<String> problems) {
+        Sheet sh = wb.getSheet(" 璐ц繍");
+        if (sh == null) sh = wb.getSheet("璐ц繍");
+        if (sh == null) {
+            problems.add("璐ц繍椤典笉瀛樺湪锛堟瘝鐗堢己澶辫椤碉級锛岃烦杩囪嚜鍔ㄥ洖濉�");
+            return 0;
+        }
+        int cap = monthColumnCapacity(sh);
+        int mMax = Math.min(keepMonths, cap);
+        if (mMax <= 0) {
+            problems.add("璐ц繍椤垫湭璇嗗埆鍒� 2026 骞寸疮璁″垪锛岃烦杩囪嚜鍔ㄥ洖濉�");
+            return 0;
+        }
+        int year = periodYear(period);
+        // 鍧楋紙鍏ㄧ渷/甯傚窞锛夛細鎸囨爣閿� -> 琛屽彿
+        Map<String, Map<String, Integer>> blockRows = new LinkedHashMap<>();
+        String cur = null;
+        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
+            Row row = sh.getRow(r);
+            if (row == null) continue;
+            String a = text(row.getCell(0));
+            if (a != null && !a.trim().isEmpty()) {
+                String norm = RegionUtil.normalizeCityName(a.trim());
+                if (FreightCalc.PROVINCE.equals(norm) || RegionUtil.cityList().contains(norm)) {
+                    cur = norm;
+                    blockRows.computeIfAbsent(cur, k -> new LinkedHashMap<>());
+                } else {
+                    cur = null; // 鏍囬琛� / 闈炲叏鐪侀潪甯傚窞 -> 鑴辩鍧�
+                }
+            }
+            if (cur == null) continue;
+            String key = freightMetricKey(text(row.getCell(1)));
+            if (key == null) continue;
+            blockRows.get(cur).putIfAbsent(key, r);
+        }
+        if (blockRows.isEmpty()) {
+            problems.add("璐ц繍椤垫湭璇嗗埆鍒板叏鐪�/甯傚窞琛岋紝璺宠繃鑷姩鍥炲~");
+            return 0;
+        }
+        int filled = 0;
+        // 鍙洖濉洰鏍囨湡褰撴湀锛�1..鐩爣鏈�-1 鐨勫�间互姣嶇増锛堝惈鍚屾湡澶囦唤姣嶇増杩樺師锛変负鍑嗭紝閬垮厤鏀瑰啓宸茬‘璁ゅ彛寰�
+        int targetM = Math.min(periodMonth(period), mMax);
+        for (int m = targetM; m <= targetM; m++) {
+            String mp = String.format("%04d-%02d", year, m);
+            FreightMatrix mx;
+            try {
+                mx = freightCalc.calc(mp);
+            } catch (Exception e) {
+                problems.add("璐ц繍椤� " + mp + " 鍙栨暟澶辫触锛�" + e.getMessage());
+                continue;
+            }
+            int col = FIRST_MONTH_COL + 2 * (m - 1);
+            for (Map.Entry<String, Map<String, Integer>> e : blockRows.entrySet()) {
+                FreightMetrics fm = mx.getMonth().get(e.getKey());
+                if (fm == null) continue;
+                Map<String, Integer> rows = e.getValue();
+                // 1 鏈堝垪鏄叕寮忕殑鈥滄淳鐢熻鈥濓紙鍏ㄧ渷鍚堣 璐ц繍閲�/鍛ㄨ浆閲忋�佸競宸� 瑙勪笂+瑙勪笅鍛ㄨ浆閲忥級涓嶅啓鍊硷紝
+                // 鐢� ensureMonthFormulas 鎶婂叕寮忔寜鏈堝彸绉伙紝淇濇寔涓庢瘝鐗堜竴鑷�
+                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("totalFreight")), col, fm.getTotalFreight());
+                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("aboveFreight")), col, fm.getAboveFreight());
+                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("belowFreight")), col, fm.getBelowFreight());
+                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("totalTurnover")), col, fm.getTotalTurnover());
+                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("aboveTurnover")), col, fm.getAboveTurnover());
+                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("belowTurnover")), col, fm.getBelowTurnover());
+            }
+        }
+        return filled;
+    }
+
+    /** 璇ヨ 1 鏈堝垪锛圕锛夋槸鍏紡 -> 娲剧敓琛岋紝杩斿洖 null 琛ㄧず涓嶅洖濉暟鍊� */
+    private Integer skipIfDerived(Sheet sh, Integer r) {
+        if (r == null) return null;
+        Row row = sh.getRow(r);
+        if (row == null) return r;
+        Cell c = row.getCell(FIRST_MONTH_COL);
+        if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) return null;
+        return r;
+    }
+
+    private int setFreightCell(Sheet sh, Integer r, int col, Double v) {
+        if (r == null || v == null) return 0;
+        return setNumeric(sh, r, col, v);
+    }
+
+    /** 璐ц繍椤� B 鍒楁寚鏍囨枃妗� -> 鎸囨爣閿紙瑙勪笂+瑙勪笅 / 瑙勪笂 / 瑙勪笅 脳 璐ц繍閲� / 鍛ㄨ浆閲忥級 */
+    private String freightMetricKey(String b) {
+        if (b == null) return null;
+        String s = b.replaceAll("\\s+", "");
+        if (s.isEmpty()) return null;
+        boolean turnover = s.contains("鍛ㄨ浆閲�");
+        if (s.contains("瑙勪笂+瑙勪笅")) return turnover ? "totalTurnover" : "totalFreight";
+        if (s.contains("瑙勪笂")) return turnover ? "aboveTurnover" : "aboveFreight";
+        if (s.contains("瑙勪笅")) return turnover ? "belowTurnover" : "belowFreight";
+        return turnover ? "totalTurnover" : "totalFreight"; // 鍏ㄧ渷鍧楃殑鈥滆揣杩愰噺/璐х墿鍛ㄨ浆閲忊��= 鍚堣
+    }
+
+    /** 缃戠害杞﹂〉鍥炲~锛�17 甯傚窞 鐨� 瀹㈣繍閲�/鍛ㄨ浆閲�/鍏朵腑鍩庡競鍐呭杩愰噺/鍏朵腑鍩庡競鍐呭懆杞噺锛堥�愭湀鐜扮畻锛�
+     *  鍏ㄧ渷琛岋紙=17 甯傚窞姹傚拰鍏紡锛夌敱 ensureMonthFormulas 鎸夋湀鎵╁垪锛屼笉鍦ㄦ澶勫啓鍊笺�� */
+    private int fillWycSheet(XSSFWorkbook wb, String period, int keepMonths, List<String> problems) {
+        Sheet sh = wb.getSheet("缃戠害杞�");
+        if (sh == null) {
+            problems.add("缃戠害杞﹂〉涓嶅瓨鍦紙姣嶇増缂哄け璇ラ〉锛夛紝璺宠繃鑷姩鍥炲~");
+            return 0;
+        }
+        int cap = monthColumnCapacity(sh);
+        int mMax = Math.min(keepMonths, cap);
+        if (mMax <= 0) {
+            problems.add("缃戠害杞﹂〉鏈瘑鍒埌 2026 骞寸疮璁″垪锛岃烦杩囪嚜鍔ㄥ洖濉�");
+            return 0;
+        }
+        int year = periodYear(period);
+        Map<String, Map<String, Integer>> cityMetricRows = new LinkedHashMap<>();
+        String cur = null;
+        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
+            Row row = sh.getRow(r);
+            if (row == null) continue;
+            String a = text(row.getCell(0));
+            if (a != null && !a.trim().isEmpty()) {
+                String norm = RegionUtil.normalizeCityName(a.trim());
+                cur = RegionUtil.cityList().contains(norm) ? norm : null;
+                if (cur != null) cityMetricRows.computeIfAbsent(cur, k -> new LinkedHashMap<>());
+            }
+            if (cur == null) continue;
+            String key = wycMetricKey(text(row.getCell(1)));
+            if (key == null) continue;
+            cityMetricRows.get(cur).putIfAbsent(key, r);
+        }
+        if (cityMetricRows.isEmpty()) {
+            problems.add("缃戠害杞﹂〉鏈瘑鍒埌甯傚窞琛岋紝璺宠繃鑷姩鍥炲~");
+            return 0;
+        }
+        int filled = 0;
+        // 鍙洖濉洰鏍囨湡褰撴湀锛堝巻鍙叉湀浠ユ瘝鐗�/鍘嗗彶鐢熸垚浠朵负鍑嗭級
+        int targetM = Math.min(periodMonth(period), mMax);
+        for (int m = targetM; m <= targetM; m++) {
+            String mp = String.format("%04d-%02d", year, m);
+            WycSplitCalc.WycResult wr;
+            try {
+                wr = wycSplitCalc.calc(mp);
+            } catch (Exception e) {
+                if (m == mMax) {
+                    problems.add("缃戠害杞﹂〉 " + mp + " 婧愭暟鎹己澶憋紝褰撴湀鏈洖濉細" + e.getMessage());
+                }
+                continue;
+            }
+            int col = FIRST_MONTH_COL + 2 * (m - 1);
+            for (Map.Entry<String, Map<String, Integer>> e : cityMetricRows.entrySet()) {
+                WycSplitCalc.WycMetrics wm = wr.getByCity().get(e.getKey());
+                if (wm == null) continue;
+                Map<String, Integer> rows = e.getValue();
+                filled += setFreightCell(sh, rows.get("pax"), col, wm.getTotalPax());
+                filled += setFreightCell(sh, rows.get("turnover"), col, wm.getTotalTurnover());
+                filled += setFreightCell(sh, rows.get("cityPax"), col, wm.getCityPax());
+                filled += setFreightCell(sh, rows.get("cityTurnover"), col, wm.getCityTurnover());
+            }
+        }
+        return filled;
+    }
+
+    /** 缃戠害杞﹂〉 B 鍒楁寚鏍囨枃妗� -> 鎸囨爣閿� */
+    private String wycMetricKey(String b) {
+        if (b == null) return null;
+        String s = b.replaceAll("\\s+", "");
+        if (s.isEmpty()) return null;
+        boolean city = s.contains("鍩庡競鍐�");
+        boolean turnover = s.contains("鍛ㄨ浆閲�");
+        if (city) return turnover ? "cityTurnover" : "cityPax";
+        return turnover ? "turnover" : "pax";
     }
 
     /** 杞ㄩ亾杞浮椤碉紙杞ㄩ亾=姝︽眽/榛勭煶锛涜疆娓′粎姝︽眽锛変笓鐢ㄥ洖濉� */
@@ -192,15 +371,27 @@
         return -1;
     }
 
-    /** 鍐欐暟鍊硷細鏍间笉瀛樺湪鍒欏缓鏍煎苟澶嶅埗鍚岃 1 鏈堝垪鏍峰紡锛涜繑鍥� 1 */
+    /** 鍐欐暟鍊硷細鏍间笉瀛樺湪鍒欏缓鏍硷紱鏍煎甫鏃ユ湡鏍峰紡锛堝鍏氦/鍑虹 8 鏈堝垪缁ф壙浜嗚〃澶粹�測yyy骞磎鏈堚�濓級鏃舵敼鍥炲悓琛� 1 鏈堝垪鐨勬暟鍊兼牱寮忥紝閬垮厤鏁板�兼樉绀烘垚鏃ユ湡 */
     private int setNumeric(Sheet sh, int r, int col, double val) {
         Row row = sh.getRow(r);
         if (row == null) row = sh.createRow(r);
         Cell c = row.getCell(col);
+        if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) {
+            return 0; // 姣嶇増鏈潵灏遍潬鍏紡绠楃殑琛岋紙鍚堣/鍚岄〉姹傚拰锛変繚鎸佸叕寮忥紝浜� Excel 閲嶇畻
+        }
+        Cell ref = row.getCell(FIRST_MONTH_COL);
+        boolean needStyle = false;
         if (c == null) {
             c = row.createCell(col);
-            Cell ref = row.getCell(FIRST_MONTH_COL);
-            if (ref != null && ref.getCellStyle() != null) c.setCellStyle(ref.getCellStyle());
+            needStyle = true;
+        } else if (c.getCellType() == org.apache.poi.ss.usermodel.CellType.BLANK
+                || (c.getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC
+                    && org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(c))) {
+            // 姣嶇増鎶� 8 鏈堝垪鐨勮〃澶存棩鏈熸牸寮忓甫鍒颁簡鏁版嵁鏍硷紙鎴栨湁鏍煎紡鏃犲�硷級锛屽啓鍊煎墠鏀瑰洖鍚岃 1 鏈堝垪鐨勬暟瀛楁牸寮�
+            needStyle = true;
+        }
+        if (needStyle && ref != null && ref.getCellStyle() != null) {
+            c.setCellStyle(ref.getCellStyle());
         }
         c.setCellValue(val);
         return 1;
@@ -229,20 +420,193 @@
         return row == null ? null : cityOf(row.getCell(0));
     }
 
-    /** 鏈堜唤瀹归噺锛氬湪琛ㄥご锛堝墠 5 琛岋級鎵锯��2026鈥︾疮璁♀�濆垪锛屽閲�=(绱鍒�-1 鏈堝垪)/2 */
+    /**
+     * 涓虹 month 鏈堣ˉ鈥滃綋鏈堝�尖�濆垪鍏紡锛氬彇鍚屼竴琛屾渶杩戜竴涓湀浠嶄负鍏紡鐨勫�煎垪锛屾暣鍒楀彸绉昏繃鏉�
+     * 锛堝鍏氦 7 鏈堝垪 O 鐨� =O9+O13+... -> 8 鏈堝垪 Q 鐨� =Q9+Q13+...锛夈��
+     * 鍙ˉ绌哄崟鍏冩牸锛涘凡鏈夋暟鍊硷紙璐ц繍鍏ㄧ渷鍧�/甯傚窞鍧楃敱鏁版嵁鍥炲~锛夋垨宸叉湁鍏紡鐨勬牸涓嶅姩銆�
+     */
+    public int ensureMonthFormulas(XSSFWorkbook wb, int year, int month) {
+        if (wb == null || month <= 1) return 0;
+        int targetCol = FIRST_MONTH_COL + 2 * (month - 1);
+        int written = 0;
+        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+            Sheet sh = wb.getSheetAt(i);
+            if (sh == null || !isFilledLedgerSheet(sh.getSheetName())) continue;
+            for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
+                Row row = sh.getRow(r);
+                if (row == null) continue;
+                if (hasContent(row.getCell(targetCol))) continue;
+                Cell srcCell = null;
+                int srcMonth = -1;
+                for (int s = month - 1; s >= 1; s--) {
+                    Cell c = row.getCell(FIRST_MONTH_COL + 2 * (s - 1));
+                    if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) {
+                        srcCell = c;
+                        srcMonth = s;
+                        break;
+                    }
+                }
+                if (srcCell == null) continue;
+                String f = srcCell.getCellFormula();
+                if (f == null || f.isEmpty()) continue;
+                Cell target = row.getCell(targetCol);
+                if (target == null) target = row.createCell(targetCol);
+                if (srcCell.getCellStyle() != null) target.setCellStyle(srcCell.getCellStyle());
+                target.setCellFormula(shiftFormulaColumns(f, 2 * (month - srcMonth)));
+                written++;
+            }
+        }
+        return written;
+    }
+
+    /** 璇ユ牸鏄惁宸叉湁鍐呭锛堟暟鍊�/鏂囨湰/鍏紡锛夛紱浠呭甫鏍峰紡鐨勭┖澹筹紙濡傛瘝鐗堥噷缁ф壙琛ㄥご鏃ユ湡鏍煎紡鐨勬牸瀛愶級瑙嗕负绌� */
+    private boolean hasContent(Cell c) {
+        if (c == null) return false;
+        switch (c.getCellType()) {
+            case BLANK: return false;
+            case STRING: return c.getStringCellValue() != null && !c.getStringCellValue().trim().isEmpty();
+            default: return true;
+        }
+    }
+
+    /** 鍙備笌閫愭湀鏁版嵁鍥炲~鐨勫彴璐﹂〉锛堣揣杩�/鐝嚎鍖呰溅/鍏氦/鍑虹/缃戠害杞�/杞ㄩ亾杞浮锛� */
+    private boolean isFilledLedgerSheet(String name) {
+        if (name == null) return false;
+        String n = name.trim();
+        return "璐ц繍".equals(n) || "鐝嚎鍖呰溅".equals(n) || "鍏氦".equals(n)
+                || "鍑虹".equals(n) || "缃戠害杞�".equals(n) || "杞ㄩ亾銆佽疆娓�".equals(n);
+    }
+
+    /** 0 鍩哄垪鍙� + 1 鍩鸿鍙� -> "Q5" */
+    private String cellRef(int col0, int row1) {
+        StringBuilder sb = new StringBuilder();
+        int n = col0 + 1;
+        while (n > 0) {
+            int rem = (n - 1) % 26;
+            sb.insert(0, (char) ('A' + rem));
+            n = (n - 1) / 26;
+        }
+        return sb.toString() + row1;
+    }
+
+    /** 浠� "2026-08" 鍙栨湀浠斤紙瑙f瀽澶辫触杩斿洖 1锛� */
+    private int periodMonth(String period) {
+        if (period != null && period.length() >= 7) {
+            try { return Integer.parseInt(period.substring(5, 7)); } catch (Exception ignore) { }
+        }
+        return 1;
+    }
+
+    /** 浠� "2026-08" 鍙栧勾浠� */
+    private int periodYear(String period) {
+        if (period != null && period.length() >= 4) {
+            try { return Integer.parseInt(period.substring(0, 4)); } catch (Exception ignore) { }
+        }
+        return 0;
+    }
+
+    /**
+     * 涓虹 m 鏈堣ˉ鈥滀笌鍘诲勾鍚屾瘮鈥濆叕寮忥細鎶婄 m-1 鏈堝悓姣斿垪鐨勫叕寮忔暣浣撳彸绉� 2 鍒�
+     * 锛堝鍏氦 7 鏈� P5 鍏紡 =O5/AH5-1 -> 8 鏈� R5 =Q5/AJ5-1锛夛紝
+     * 鍥犳鍚屾瘮鍒嗘瘝鍙栫殑灏辨槸姹囨�昏〃 2025 骞村悓鏈堝垪锛堢敤鎴峰彛寰勶級銆�
+     */
+    public int ensureMonthYoyFormulas(XSSFWorkbook wb, int year, int month) {
+        if (wb == null || month <= 1) return 0;
+        int valueCol = FIRST_MONTH_COL + 2 * (month - 1);
+        int yoyCol = valueCol + 1;
+        int prevYoyCol = yoyCol - 2;
+        int written = 0;
+        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+            Sheet sh = wb.getSheetAt(i);
+            if (sh == null) continue;
+            if (!isMonthLedgerSheet(sh.getSheetName())) continue;
+            for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
+                Row row = sh.getRow(r);
+                if (row == null) continue;
+                Cell prev = row.getCell(prevYoyCol);
+                if (prev == null || prev.getCellType() != org.apache.poi.ss.usermodel.CellType.FORMULA) continue;
+                Cell target = row.getCell(yoyCol);
+                if (target != null && target.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) continue;
+                String f = prev.getCellFormula();
+                if (f == null || f.isEmpty()) continue;
+                String shifted = shiftFormulaColumns(f, 2);
+                if (target == null) {
+                    target = row.createCell(yoyCol);
+                    if (prev.getCellStyle() != null) target.setCellStyle(prev.getCellStyle());
+                }
+                try {
+                    target.setCellFormula(shifted);
+                    written++;
+                } catch (Exception e) {
+                    throw new IllegalStateException(String.format(
+                            "鍚屾瘮鍏紡琛ュ啓澶辫触锛歴heet=%s cell=%s 鍘熷紡=%s 鐩爣寮�=%s",
+                            sh.getSheetName(), cellRef(yoyCol, r + 1), f, shifted), e);
+                }
+            }
+        }
+        return written;
+    }
+
+    /** 闅忔湀鎵╁垪鐨勯暱琛ㄩ〉锛堣〃澶村湪 1..3 琛屻�佹湀浠界粍涓衡�滃綋鏈堝��+鍚屾瘮鈥濅氦鏇匡級 */
+    private boolean isMonthLedgerSheet(String name) {
+        if (name == null) return false;
+        String n = name.trim();
+        return "璐ц繍".equals(n) || "鍏矾鎬诲杩�".equals(n) || "鐝嚎鍖呰溅".equals(n) || "鍩庡競瀹㈣繍".equals(n)
+                || "鍏氦".equals(n) || "鍑虹".equals(n) || "缃戠害杞�".equals(n)
+                || "杞ㄩ亾銆佽疆娓�".equals(n) || "杞ㄩ亾杞浮".equals(n)
+                || "涓彛寰勬槑缁�".equals(n) || "涓彛寰勫杩愰噺".equals(n);
+    }
+
+    /** 鍏紡鍐呮墍鏈� A1 褰㈠紡鐨勫垪寮曠敤鍙崇Щ delta 鍒楋紙SUM 绛夊嚱鏁板悕鍚庢棤鏁板瓧锛屼笉浼氳鍖归厤锛� */
+    private String shiftFormulaColumns(String formula, int delta) {
+        java.util.regex.Matcher m = java.util.regex.Pattern
+                .compile("(?<![A-Za-z0-9_$])([$]?)([A-Z]{1,3})([$]?)([0-9]+)")
+                .matcher(formula);
+        StringBuffer sb = new StringBuffer();
+        while (m.find()) {
+            String col = m.group(2);
+            int idx = 0;
+            for (int i = 0; i < col.length(); i++) idx = idx * 26 + (col.charAt(i) - 'A' + 1);
+            int nidx = idx + delta;
+            StringBuilder nc = new StringBuilder();
+            while (nidx > 0) {
+                int rem = (nidx - 1) % 26;
+                nc.insert(0, (char) ('A' + rem));
+                nidx = (nidx - 1) / 26;
+            }
+            m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(m.group(1) + nc + m.group(3) + m.group(4)));
+        }
+        m.appendTail(sb);
+        return sb.toString();
+    }
+
+    /** 鏈堜唤瀹归噺锛氫紭鍏堝彇琛ㄥご锛堝墠 5 琛岋級鈥�2026鈥︾疮璁♀�濆垪鍙嶆帹锛涜揣杩愰〉鏃犺鏂囨锛�
+     *  鍐嶇敤琛ㄥご閲屾渶鍚庝竴涓��2026骞碝鏈堚��/鏃ユ湡鏍煎紡鐨勬湀鍒楀厹搴曪紙濡傝揣杩� Q3=2026-08-01 -> 8锛� */
     private int monthColumnCapacity(Sheet sh) {
+        int byCumulative = 0;
+        int byMonthHeader = 0;
         for (int r = 0; r <= 4 && r <= sh.getLastRowNum(); r++) {
             Row row = sh.getRow(r);
             if (row == null) continue;
             for (Cell c : row) {
                 if (c == null) continue;
+                int idx = c.getColumnIndex();
+                if (idx <= FIRST_MONTH_COL || (idx - FIRST_MONTH_COL) % 2 != 0) continue; // 鍙湅鈥滃綋鏈堝�尖�濆垪
                 String t = text(c);
-                if (t != null && t.contains("2026") && t.contains("绱") && c.getColumnIndex() > FIRST_MONTH_COL) {
-                    return (c.getColumnIndex() - FIRST_MONTH_COL) / 2;
+                if (byCumulative == 0 && t != null && t.contains("2026") && t.contains("绱")) {
+                    byCumulative = (idx - FIRST_MONTH_COL) / 2;
+                }
+                if (t != null && t.contains("2026") && t.contains("鏈�") && !t.contains("绱")
+                        && !t.contains("鍚屾瘮")) {
+                    byMonthHeader = Math.max(byMonthHeader, (idx - FIRST_MONTH_COL) / 2 + 1);
+                } else if (c.getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC
+                        && org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(c)) {
+                    byMonthHeader = Math.max(byMonthHeader, (idx - FIRST_MONTH_COL) / 2 + 1);
                 }
             }
         }
-        return 7;
+        int cap = Math.max(byCumulative, byMonthHeader);
+        return cap > 0 ? cap : 7;
     }
 
     private String text(Cell c) {

--
Gitblit v1.9.1