From d35a0a281f75dadb81029672442815fc8bc71d30 Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期二, 08 九月 2026 10:08:02 +0800
Subject: [PATCH] feat: 汇总大表数据驱动 M1-城市客运——新增 PaxCalc 计算服务(公交/出租/轨道轮渡)与 7 月回归测试报告

---
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java |  236 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 234 insertions(+), 2 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 b52289d..9fdb6f2 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
@@ -456,7 +456,7 @@
         Map<String, Double> h2032Cum = loadH2032FreightCumulative(period);
         ScaleSplitTransport provCum = getProvinceCumulative(period);
         Double total = freightCum(ft.get("婀栧寳鐪�"), month);
-        Double totalWan = total == null ? null : round(total / 10000.0, 4);
+        Double totalWan = total == null ? null : round(total, 4); // 妯℃澘鍒楀凡鏄竾鍚紝涓嶅啀 梅10000
         double above = 0.0;
         for (Double v : h2032Cum.values()) if (v != null) above += v;
         double aboveWan = round(above / 10000.0, 4);
@@ -3199,6 +3199,14 @@
      * 淇濈暀鍏ㄩ儴椤电/鐗堝紡/鍏紡/2025 骞寸紦瀛樺�硷紝浠呮妸琛ㄥご骞翠唤鍔ㄦ�佸寲涓虹洰鏍囧勾锛涙暟鎹~鍏呮寜 P2 璧烽�愭鎺ュ叆銆�
      */
     public byte[] exportSummaryWorkbook(String period, String mode) throws Exception {
+        return exportSummaryWorkbook(period, mode, null);
+    }
+
+    /** 鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx锛圥0-P1 楠ㄦ灦锛屾敮鎸� toMonth 瑁佸壀锛夛細
+     *  浠ャ�奩YYY骞碝鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx銆嬶紙M 鐢� period 鍐冲畾锛変负姣嶇増锛屾寜鍚屾湡澶囦唤姣嶇増杩樺師鏈堝害鍧楋紱
+     *  toMonth 闈炵┖涓斿皬浜� M 鏃讹紝鎶� 8 涓彴璐�/鍚堟垚椤佃鍒� toMonth 鏈堬紙toMonth+1..M 鐨勫綋鏈堝��/鍚屾瘮鍒楁竻绌恒�佷繚鐣欒〃澶达紝
+     *  绱涓庣疮璁″悓姣斿叕寮忔敼涓烘寜 toMonth 鏈堥噸绠楋級锛岀敤浜庘�滃厛鐢熸垚 1-6 鏈堛��1-7 鏈堟墿鍒椻�濋獙璇併�� */
+    public byte[] exportSummaryWorkbook(String period, String mode, Integer toMonth) throws Exception {
         int year = Integer.parseInt(period.contains("-") ? period.split("-")[0] : period);
         int month = period.contains("-") ? monthOf(period, mode) : 12;
         File template = resolveSummaryMother(period, mode);
@@ -3207,7 +3215,23 @@
         }
         try (InputStream in = new FileInputStream(template);
              XSSFWorkbook wb = new XSSFWorkbook(in)) {
+            File donor = resolveSummaryDonor(year, month);
+            if (donor != null) {
+                try (InputStream din = new FileInputStream(donor); XSSFWorkbook dwb = new XSSFWorkbook(din)) {
+                    int restored = restoreSummaryMonthlyBlocks(wb, dwb);
+                    log.info("姹囨�诲伐浣滅翱鏈堝害鍧楄繕鍘熷畬鎴愶紝琛ラ綈/瑕嗙洊鍗曞厓鏍兼暟={}锛堟瘝鐗堬細{}锛屽浠芥瘝鐗堬細{}锛�", restored, template.getName(), donor.getName());
+                }
+            } else {
+                log.warn("姹囨�诲伐浣滅翱鏈壘鍒板悓鏈熷浠芥瘝鐗堬紙_澶囦唤_{}骞磠}鏈堚�︼級锛岃緭鍑哄皢淇濇寔姣嶇増鐜扮姸锛堝彲鑳戒负 1-4 鏈堥鏋讹級", year, month);
+            }
+            if (toMonth != null && toMonth > 0 && toMonth < month) {
+                int trimmed = trimSummaryMonthlySheets(wb, toMonth);
+                log.info("姹囨�诲伐浣滅翱瑁佸壀鑷� {} 鏈堝畬鎴愶紝娓呯┖/鏀瑰啓鍗曞厓鏍兼暟={}", toMonth, trimmed);
+            }
             dynamicSummaryYear(wb, year);
+            applyTwoDecimalFormat(wb); // 姹囨�诲ぇ琛ㄤ繚鐣欎汉宸ユ瘝鐗堝垪瀹�/鐗堝紡锛氫笉鍋� autoFit锛堥暱鍏紡浼氭妸鍒楀椤剁垎锛�
+            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
+            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 杩樺師/鏀瑰啓鍏紡鍚庢墦寮�鍗抽噸绠�
             return toBytes(wb);
         }
     }
@@ -3472,6 +3496,153 @@
             if (a[5] <= 0 && a[2] > 0) a[5] = round(a[3] / a[2], 2);
         }
         return map;
+    }
+
+    /** 瀹氫綅鍚屾湡澶囦唤姣嶇増锛堟竻鐞嗗墠鐨勫畬鏁存湀姣嶇増锛夛紝鐢ㄤ簬鎶婇鏋舵瘝鐗堢己澶辩殑 5-7 鏈堟湀搴﹀潡鎸夊浠借繕鍘燂紱
+     *  涓� resolveAnyTemplate 鐩稿悓绛栫暐锛氬湪閰嶇疆鐩綍銆乽ser.dir 鐩稿鐩綍銆佷笂绾х洰褰曚笁涓�欓�夋牴涓壂鎻� */
+    private File resolveSummaryDonor(int year, int month) {
+        String rel = summaryTemplateDir;
+        while (rel.startsWith("./")) rel = rel.substring(2);
+        String[] roots = {
+            summaryTemplateDir,
+            System.getProperty("user.dir") + "/" + rel,
+            System.getProperty("user.dir") + "/../" + rel
+        };
+        for (String root : roots) {
+            if (root == null || root.trim().isEmpty()) continue;
+            File dir = new File(root);
+            File[] files = dir.listFiles((d, name) ->
+                name.startsWith("_澶囦唤_") && name.contains(year + "骞�" + month + "鏈�") && name.contains("閬撹矾杩愯緭閲忔眹鎬昏〃"));
+            if (files != null && files.length > 0) return files[0];
+        }
+        return null;
+    }
+
+    /** 鎸夊悓鏈熷浠芥瘝鐗堣繕鍘熸湀搴﹀潡锛氬嚒澶囦唤鏈夊唴瀹硅�屾瘝鐗堝悓鏍肩己澶�/鍐呭涓嶅悓锛堟竻鐞嗘椂琚鎺夌殑 5-7 鏈堝�间笌绱鍏紡锛夌殑鍗曞厓鏍硷紝鏁存牸鍥炲~骞惰鐩� */
+    private int restoreSummaryMonthlyBlocks(XSSFWorkbook wb, XSSFWorkbook donor) {
+        int restored = 0;
+        for (int i = 0; i < donor.getNumberOfSheets(); i++) {
+            Sheet ds = donor.getSheetAt(i);
+            if (ds == null) continue;
+            Sheet bs = wb.getSheet(ds.getSheetName());
+            if (bs == null) continue;
+            for (int r = ds.getFirstRowNum(); r <= ds.getLastRowNum(); r++) {
+                Row dr = ds.getRow(r);
+                if (dr == null) continue;
+                Row br = bs.getRow(r);
+                for (Cell dc : dr) {
+                    if (dc == null) continue;
+                    CellType t = dc.getCellType();
+                    if (t == CellType.BLANK) continue;
+                    String dtx = summaryCellText(dc);
+                    if (dtx == null || dtx.isEmpty()) continue;
+                    Cell bc = (br == null) ? null : br.getCell(dc.getColumnIndex());
+                    if (bc != null && dtx.equals(summaryCellText(bc))) continue;
+                    Row tr = (bc != null) ? br : bs.getRow(r);
+                    if (tr == null) tr = bs.createRow(r);
+                    Cell tc = (bc != null) ? bc : tr.getCell(dc.getColumnIndex());
+                    if (tc == null) tc = tr.createCell(dc.getColumnIndex());
+                    tc.setBlank();
+                    if (t == CellType.FORMULA) {
+                        String f = dc.getCellFormula();
+                        if (f != null) tc.setCellFormula(f);
+                    } else if (t == CellType.STRING) {
+                        tc.setCellValue(dc.getStringCellValue());
+                    } else if (t == CellType.NUMERIC) {
+                        tc.setCellValue(dc.getNumericCellValue());
+                    } else if (t == CellType.BOOLEAN) {
+                        tc.setCellValue(dc.getBooleanCellValue());
+                    }
+                    restored++;
+                }
+            }
+        }
+        return restored;
+    }
+
+    /** 鍗曞厓鏍煎唴瀹规枃鏈寲锛堝叕寮忓甫 = 鍓嶇紑锛夛紝鐢ㄤ簬璺ㄧ翱姣斿 */
+    private String summaryCellText(Cell c) {
+        if (c == null) return null;
+        switch (c.getCellType()) {
+            case FORMULA: {
+                String f = c.getCellFormula();
+                return f == null ? "" : "=" + f;
+            }
+            case STRING:
+                return c.getStringCellValue();
+            case NUMERIC:
+                return Double.toString(c.getNumericCellValue());
+            case BOOLEAN:
+                return Boolean.toString(c.getBooleanCellValue());
+            default:
+                return "";
+        }
+    }
+
+    /** 鎶� 8 涓彴璐�/鍚堟垚椤佃鍓埌 keepMonths 鏈堬細
+     *  鏈堝害鍊煎垪 2026 骞� m 鏈� = 绗� 3+2*(m-1) 鍒楋紙C/E/G/I/K/M/O锛夛紝鍚屾瘮鍒椾负鍏跺悗涓�鍒楋紱
+     *  绱 Q 鍒� = C+E+...+O锛岀疮璁″悓姣� R 鍒楀垎姣� = 2025 骞村悓鏈堝垪 T/V/X/Z/AB/AD/AF銆�
+     *  瑁佸壀 = 鏁版嵁琛岋紙鍚疮璁″叕寮忎笖浠讳竴 2026 鏈堝害鍒楁湁鍐呭锛変腑 keepMonths+1..7 鏈堢殑褰撴湀鍊�/鍚屾瘮鍗曞厓鏍兼竻绌猴紙琛ㄥご淇濈暀锛夛紝
+     *  骞舵妸璇ヨ绱鍏紡鍘绘帀瀵瑰簲 +鍒楋紝绱鍚屾瘮鍒嗘瘝鍘绘帀瀵瑰簲 2025 鍒椼�� */
+    private int trimSummaryMonthlySheets(XSSFWorkbook wb, int keepMonths) {
+        String[] scope = {" 璐ц繍", "鐝嚎鍖呰溅", "鍩庡競瀹㈣繍", "鍏氦", "鍑虹", "缃戠害杞�", "杞ㄩ亾銆佽疆娓�", "涓彛寰勬槑缁�"};
+        java.util.Set<String> scopeSet = new java.util.HashSet<>();
+        for (String s : scope) scopeSet.add(s);
+        int touched = 0;
+        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+            Sheet sh = wb.getSheetAt(i);
+            if (sh == null || !scopeSet.contains(sh.getSheetName())) continue;
+            for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
+                Row row = sh.getRow(r);
+                if (row == null) continue;
+                Cell q = row.getCell(16); // Q 鍒� = 绱
+                if (q == null || q.getCellType() != CellType.FORMULA) continue;
+                boolean hasMonthly = false;
+                for (int m = 1; m <= 7 && !hasMonthly; m++) {
+                    Cell v = row.getCell(2 + 2 * (m - 1)); // C,E,G,I,K,M,O
+                    if (v != null && v.getCellType() != CellType.BLANK) hasMonthly = true;
+                }
+                if (!hasMonthly) continue; // 鎺掗櫎椤甸/椤典腑璺ㄩ〉绛炬牎楠屽叕寮忚锛堟棤鏈堝害鍊硷級
+                int excelRow = r + 1; // 鍏紡鍐呭紩鐢ㄤ负 Excel 1-based 琛屽彿
+                for (int m = keepMonths + 1; m <= 7; m++) {
+                    int valueCol = 2 + 2 * (m - 1);     // 2026 褰撴湀鍊煎垪
+                    int yoyCol = valueCol + 1;          // 褰撴湀鍚屾瘮鍒�
+                    String vLetter = colLetter(valueCol);
+                    Cell vc = row.getCell(valueCol);
+                    if (vc != null) {
+                        vc.setBlank();
+                        touched++;
+                    }
+                    Cell yc = row.getCell(yoyCol);
+                    if (yc != null) {
+                        yc.setBlank();
+                        touched++;
+                    }
+                    String fq = q.getCellFormula();
+                    if (fq != null && fq.contains("+" + vLetter + excelRow)) {
+                        q.setCellFormula(fq.replace("+" + vLetter + excelRow, ""));
+                        touched++;
+                    }
+                    Cell rc = row.getCell(17); // R 鍒� = 绱鍚屾瘮
+                    if (rc != null && rc.getCellType() == CellType.FORMULA) {
+                        String fr = rc.getCellFormula();
+                        int lastCol = 19 + 2 * (m - 1); // 2025 鍚屾湀鍒� T..AF锛�0-based锛歍=19,AF=31锛�
+                        String lLetter = colLetter(lastCol);
+                        String txt = fr;
+                        if (txt != null && txt.contains("," + lLetter + excelRow)) {
+                            txt = txt.replace("," + lLetter + excelRow, "");
+                            touched++;
+                        }
+                        if (txt != null && txt.contains("+" + lLetter + excelRow)) {
+                            txt = txt.replace("+" + lLetter + excelRow, "");
+                            touched++;
+                        }
+                        if (txt != null && !txt.equals(fr)) rc.setCellFormula(txt);
+                    }
+                }
+            }
+        }
+        return touched;
     }
 
     /** 姹囨�诲伐浣滅翱骞翠唤鍔ㄦ�佸寲锛氫粎鎶� 2026骞� 鍓嶇紑鏇挎崲涓虹洰鏍囧勾锛�2025/2024 鍙傝�冨垪涓嶅姩锛� */
@@ -4574,7 +4745,13 @@
                         CellType ct = cell.getCellType();
                         if (ct != CellType.NUMERIC && ct != CellType.FORMULA) continue;
                         if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) continue;
-                        if (ct == CellType.NUMERIC && Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue()) 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();
@@ -4595,6 +4772,61 @@
         }
     }
 
+    /** 姹囨�昏〃瀵煎嚭鍚庢寜鍐呭鑷姩鍔犲鍒楀锛堝彧鍔犲涓嶇缉绐勶紱璺宠繃鍚堝苟鍗曞厓鏍兼爣棰橈紝闃查暱鏍囬鎶婂崟鍒楁拺鐖嗭級 */
+    private void autoFitContentColumns(org.apache.poi.ss.usermodel.Workbook wb) {
+        if (wb == null) return;
+        org.apache.poi.ss.usermodel.DataFormatter dfmt = new org.apache.poi.ss.usermodel.DataFormatter();
+        for (int s = 0; s < wb.getNumberOfSheets(); s++) {
+            org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(s);
+            if (sh == null) continue;
+            java.util.Set<String> merged = new java.util.HashSet<>();
+            for (org.apache.poi.ss.util.CellRangeAddress ra : sh.getMergedRegions()) {
+                for (int r = ra.getFirstRow(); r <= ra.getLastRow(); r++) {
+                    for (int c = ra.getFirstColumn(); c <= ra.getLastColumn(); c++) {
+                        merged.add(r + ":" + c);
+                    }
+                }
+            }
+            int maxCol = -1;
+            for (org.apache.poi.ss.usermodel.Row row : sh) {
+                if (row == null) continue;
+                maxCol = Math.max(maxCol, (int) row.getLastCellNum() - 1);
+            }
+            if (maxCol < 0) continue;
+            int[] need = new int[maxCol + 1];
+            for (org.apache.poi.ss.usermodel.Row row : sh) {
+                if (row == null) continue;
+                for (org.apache.poi.ss.usermodel.Cell cell : row) {
+                    int c = cell.getColumnIndex();
+                    if (c > maxCol || merged.contains(row.getRowNum() + ":" + c)) continue;
+                    String txt;
+                    try {
+                        txt = dfmt.formatCellValue(cell);
+                    } catch (Exception ignore) {
+                        continue;
+                    }
+                    if (txt == null || txt.isEmpty()) continue;
+                    int units = 0;
+                    for (int i = 0; i < txt.length(); i++) {
+                        units += isWideChar(txt.charAt(i)) ? 2 : 1;
+                    }
+                    if (units > need[c]) need[c] = Math.min(units, 34); // 闀挎枃鏈笉鎶婂崟鍒楁拺鐖�
+                }
+            }
+            for (int c = 0; c <= maxCol; c++) {
+                if (need[c] <= 0) continue;
+                int target = Math.min(need[c] + 2, 46) * 256;
+                if (target > sh.getColumnWidth(c)) sh.setColumnWidth(c, target);
+            }
+        }
+    }
+
+    /** CJK / 鍏ㄨ瀛楃鎸変袱鍊嶅搴﹁锛堣繎浼� Excel 鏄剧ず鍒楀锛� */
+    private boolean isWideChar(char ch) {
+        return (ch >= 0x2E80 && ch <= 0x9FFF) || (ch >= 0xF900 && ch <= 0xFAFF)
+            || (ch >= 0xFF00 && ch <= 0xFFEF);
+    }
+
     private byte[] toBytes(XSSFWorkbook wb) throws Exception {
         applyTwoDecimalFormat(wb);
         try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {

--
Gitblit v1.9.1