From a76a8cce476ceccbc061a3f51681b6090a7d3741 Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期五, 11 九月 2026 17:32:41 +0800
Subject: [PATCH] feat: 汇总大表排名页随报表期刷新 + 同比列统一百分数显示

---
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java |  316 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 314 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 fb82de4..ceea4f1 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
@@ -53,6 +53,7 @@
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
@@ -3347,8 +3348,15 @@
             cleanSummarySheetPresentation(wb); // 鍙栨秷鍚勯〉绛涢�夈�佽繕鍘熼殣钘忚锛堝惁鍒欌�滅彮绾垮寘杞︹�濈瓑椤电湅璧锋潵鍍忕己鏁版嵁锛�
             int effMonth = (toMonth != null && toMonth > 0 && toMonth < month) ? toMonth : month;
             List<String> dbProblems = new ArrayList<>();
-            int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, year, effMonth, dbProblems);
-            log.info("姹囨�诲伐浣滅翱鏁版嵁椹卞姩鍥炲~瀹屾垚锛氬啓鍏ュ彴璐﹂〉褰撴湀鍊兼牸鏁�={}锛岀己鏁版彁绀�={}", dbFilled, dbProblems);
+            String effPeriod = String.format("%04d-%02d", year, effMonth);
+            int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, effPeriod, year, effMonth, dbProblems);
+            int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth);
+            int monthFormulaFilled = summaryWorkbookFiller.ensureMonthFormulas(wb, year, effMonth);
+            int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month");
+            int widthApplied = applySummaryReferenceLayout(wb);
+            log.info("姹囨�诲伐浣滅翱鏁版嵁椹卞姩鍥炲~瀹屾垚锛氬啓鍏ュ綋鏈堝�兼牸鏁�={}锛岃ˉ鍐欏綋鏈堝悓姣斿叕寮忔牸鏁�={}锛岃ˉ鍐欏綋鏈堝悎璁″叕寮忔牸鏁�={}锛�"
+                    + "鍒锋柊鎺掑悕椤垫暟鍊兼牸鏁�={}锛屾寜銆婄敓鎴恄閬撹矾杩愯緭閲忔眹鎬昏〃銆嬪榻愬垪瀹藉垪鏁�={}锛岀己鏁版彁绀�={}",
+                    dbFilled, yoyFilled, monthFormulaFilled, rankRefreshed, widthApplied, dbProblems);
             applyTwoDecimalFormat(wb); // 鏁板�兼樉绀轰袱浣嶅皬鏁帮紙淇濈暀鍏ㄧ簿搴︼級锛�% / 鏃ユ湡绛夋棦鏈夋牱寮忎笉鏀瑰彉
             if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
             wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 杩樺師/鏀瑰啓鍏紡鍚庢墦寮�鍗抽噸绠�
@@ -3358,6 +3366,310 @@
         }
     }
 
+    /**
+     * 鐗堝紡瀵归綈锛氭寜 docs/鐢熸垚姹囨�诲ぇ琛�/銆婄敓鎴恄閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx銆嬶紙鐢ㄦ埛鎻愪緵鐨勭増寮忓弬鐓э級閫愬垪濂楃敤鍒楀涓庨殣钘忔爣璁帮紝
+     * 浣跨敓鎴愪欢涓庡弬鐓т欢鈥滄牸寮忓畬鍏ㄤ竴鑷粹�濓紝骞惰姣嶇増娌″畾涔夊搴︾殑鈥滃悗缁墿鍒椻�濓紙濡� 8 鏈堝垪锛変篃鏈変笌 8 鏈堜竴鑷寸殑鍒楀銆�
+     * 鍙傜収浠剁己澶辨椂闈欓粯璺宠繃锛堣繑鍥� 0锛夛紝涓嶅奖鍝嶅師鏈夊鍑恒��
+     */
+    private int applySummaryReferenceLayout(XSSFWorkbook wb) {
+        File ref;
+        try {
+            ref = resolveSummaryLayoutRef();
+        } catch (Exception e) {
+            return 0;
+        }
+        if (ref == null) return 0;
+        double savedZipRatio = ZipSecureFile.getMinInflateRatio();
+        ZipSecureFile.setMinInflateRatio(0.0001);
+        int applied = 0;
+        try (InputStream in = new FileInputStream(ref); XSSFWorkbook rwb = new XSSFWorkbook(in)) {
+            for (int i = 0; i < rwb.getNumberOfSheets(); i++) {
+                org.apache.poi.xssf.usermodel.XSSFSheet rs = rwb.getSheetAt(i);
+                if (rs == null) continue;
+                org.apache.poi.xssf.usermodel.XSSFSheet ts = wb.getSheet(rs.getSheetName());
+                if (ts == null) continue;
+                int maxCol = Math.min(usedColumnCount(rs), usedColumnCount(ts) + 4);
+                if (maxCol <= 0) continue;
+                double[] width = new double[maxCol];
+                boolean[] hasWidth = new boolean[maxCol];
+                boolean[] hidden = new boolean[maxCol];
+                for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCols cols : rs.getCTWorksheet().getColsArray()) {
+                    for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCol col : cols.getColArray()) {
+                        long min = col.getMin();
+                        long max = col.getMax();
+                        for (long c = min; c <= max; c++) {
+                            int idx = (int) (c - 1);
+                            if (idx < 0 || idx >= maxCol) {
+                                if (idx >= maxCol) break;
+                                continue;
+                            }
+                            if (col.isSetWidth()) {
+                                width[idx] = col.getWidth();
+                                hasWidth[idx] = true;
+                            }
+                            hidden[idx] = col.isSetHidden() && col.getHidden();
+                        }
+                    }
+                }
+                for (int c = 0; c < maxCol; c++) {
+                    if (hasWidth[c]) {
+                        int w = (int) Math.round(width[c] * 256d);
+                        if (ts.getColumnWidth(c) != w) {
+                            ts.setColumnWidth(c, w);
+                            applied++;
+                        }
+                    }
+                    if (hidden[c] != ts.isColumnHidden(c)) ts.setColumnHidden(c, hidden[c]);
+                }
+            }
+            log.info("姹囨�诲伐浣滅翱鐗堝紡瀵归綈銆婄敓鎴恄閬撹矾杩愯緭閲忔眹鎬昏〃銆嬪畬鎴愶細濂楃敤鍒楀 {} 鍒楋紙鍙傜収浠讹細{}锛�", applied, ref.getName());
+        } catch (Exception e) {
+            log.warn("姹囨�诲伐浣滅翱鐗堝紡瀵归綈澶辫触锛堝拷鐣ワ紝缁х画鎸夋瘝鐗堢増寮忚緭鍑猴級锛歿}", e.getMessage());
+            return 0;
+        } finally {
+            ZipSecureFile.setMinInflateRatio(savedZipRatio);
+        }
+        return applied;
+    }
+
+    /** 涓�椤靛凡鐢ㄥ埌鐨勬渶澶у垪鏁帮紙0 鍩鸿鏁帮級 */
+    private int usedColumnCount(org.apache.poi.ss.usermodel.Sheet sh) {
+        int max = 0;
+        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
+            org.apache.poi.ss.usermodel.Row row = sh.getRow(r);
+            if (row == null) continue;
+            if (row.getLastCellNum() > max) max = row.getLastCellNum();
+        }
+        return max;
+    }
+
+    // ==================== 姹囨�诲伐浣滅翱鍐呯殑鎺掑悕椤靛埛鏂� ====================
+
+    /**
+     * 姹囨�诲伐浣滅翱閲岀殑銆岃揣杩愰噺鎺掑悕 / 璐ц繍鍛ㄨ浆閲忔帓鍚� / 涓彛寰勬帓鍚嶃�嶅湪姣嶇増閲屾槸涓婁竴鎶ヨ〃鏈熺殑闈欐�佸�硷紝
+     * 瀵煎嚭鏃剁敤鐙珛鎺掑悕琛紙銆婄敓鎴恄璐ц繍閲忔帓鍚嶃�嬨�婄敓鎴恄鍛ㄨ浆閲忔帓鍚嶃�嬨�婄敓鎴恄涓彛寰勬帓鍚嶃�嬶紝涓庣敤鎴锋棩甯告牳瀵圭殑鍙e緞涓�鑷达級
+     * 鐨勭粨鏋滃埛鏂版暟鍊硷紝閬垮厤姹囨�昏〃閲岀殑鎺掑悕椤靛仠鐣欏湪鏃ф湀浠姐��
+     * 鏁板�兼牸鎼�笺�佸叕寮忔牸鎸夊亸绉诲钩绉诲叕寮忓悗鍐欏叆锛涚洰鏍囨牸鏈韩宸叉槸鍏紡鐨勶紙SUM/RANK/鍗犳瘮/琛ユ暟锛変繚鐣欎笉鍔紝鎵撳紑鏃舵寜鏂版暟鎹噸绠椼��
+     */
+    private int refreshSummaryRankSheets(XSSFWorkbook wb, String period, String mode) throws Exception {
+        int n = 0;
+        n += copyRankValues(wb, "璐ц繍閲忔帓鍚�", exportFreightRank(period, mode), new int[][]{{0, 0}, {22, 0}});
+        n += copyRankValues(wb, "璐ц繍鍛ㄨ浆閲忔帓鍚�", exportTurnoverRank(period, mode), new int[][]{{0, 0}, {22, 0}});
+        n += copyRankValues(wb, "涓彛寰勬帓鍚�", exportPassengerMidRank(period, mode), new int[][]{{0, 0}, {0, 10}});
+        return n;
+    }
+
+    /**
+     * 鎶婄嫭绔嬫帓鍚嶈〃鐨勫悇涓潡鎸夋爣棰樺榻愭惉杩涙眹鎬诲ぇ琛ㄥ搴旈〉銆�
+     * srcBlocks[i] = 婧愬潡 i 鐨勮捣濮� {琛�, 鍒梷锛�0 鍩猴級锛涚洰鏍囧潡鎸夋爣棰橀『搴忥紙绱鍧楀湪鍓嶃�佸綋鏈堝潡鍦ㄥ悗锛夊搴斻��
+     * 鏁板�兼牸鐩存帴鎼�硷紱婧愪晶鏄叕寮忕殑锛堟帓鍚�/鍗犳瘮绛夛級鎸夌洰鏍囧潡鍋忕Щ鏁翠綋骞崇Щ鍚庡啓鍏ュ叕寮忥紝鎵撳紑鏃舵寜姹囨�昏〃鑷韩鏁版嵁閲嶇畻銆�
+     */
+    private int copyRankValues(XSSFWorkbook dstWb, String dstSheetName, byte[] srcBytes, int[][] srcBlocks) throws Exception {
+        XSSFSheet dst = dstWb.getSheet(dstSheetName);
+        if (dst == null) return 0;
+        List<XSSFCell> dstTitles = rankTitleCells(dst);
+        if (dstTitles.isEmpty()) return 0;
+        int copied = 0;
+        FormulaEvaluator dstEv = dstWb.getCreationHelper().createFormulaEvaluator();
+        try (InputStream sin = new ByteArrayInputStream(srcBytes);
+             XSSFWorkbook srcWb = new XSSFWorkbook(sin)) {
+            XSSFSheet src = srcWb.getSheetAt(0);
+            FormulaEvaluator ev = srcWb.getCreationHelper().createFormulaEvaluator();
+            int blocks = Math.min(srcBlocks.length, dstTitles.size());
+            for (int b = 0; b < blocks; b++) {
+                int sr0 = srcBlocks[b][0];
+                int sc0 = srcBlocks[b][1];
+                XSSFCell dstTitle = dstTitles.get(b);
+                int rowOff = dstTitle.getRowIndex() - sr0;
+                int colOff = dstTitle.getColumnIndex() - sc0;
+                int h = rankBlockHeight(src, sr0, sc0);
+                int w = rankBlockWidth(src, sr0, sc0);
+                for (int r = 0; r < h; r++) {
+                    Row srcRow = src.getRow(sr0 + r);
+                    if (srcRow == null) continue;
+                    Row dstRow = dst.getRow(sr0 + r + rowOff);
+                    if (dstRow == null) dstRow = dst.createRow(sr0 + r + rowOff);
+                    for (int c = 0; c < w; c++) {
+                        Cell sc = srcRow.getCell(sc0 + c);
+                        if (sc == null) continue;
+                        int dc = sc0 + c + colOff;
+                        if (r == 0) {
+                            // 鏍囬琛岋細鍚屾鍧楁爣棰樻枃妗堛�傛瘝鐗堟爣棰樻槸 inlineStr锛孭OI 鐨� setCellValue 鍙啓 <v>銆�
+                            // 涓嶆洿鏂� <is>锛孍xcel 浼氱户缁樉绀烘棫鏈堜唤锛屽繀椤诲厛 setBlank 娓呮帀鍐嶅啓銆�
+                            Cell dt = dstRow.getCell(dc);
+                            if (dt == null) dt = rankCell(dstRow, dc);
+                            if (sc.getCellType() == CellType.STRING && dt.getCellType() != CellType.FORMULA) {
+                                String text = sc.getStringCellValue();
+                                String old = dt.getCellType() == CellType.STRING ? dt.getStringCellValue() : null;
+                                if (text != null && !text.equals(old)) {
+                                    dt.setBlank();
+                                    dt.setCellValue(text);
+                                }
+                            }
+                            continue;
+                        }
+                        Cell dcCell = dstRow.getCell(dc);
+                        if (dcCell != null && dcCell.getCellType() == CellType.FORMULA) continue; // 淇濈暀姣嶇増鑷繁鐨勫叕寮�
+                        if (sc.getCellType() == CellType.FORMULA) {
+                            // 婧愪晶鍏紡锛堟帓鍚�/鍗犳瘮锛夛細鎸夌洰鏍囧潡鍋忕Щ骞崇Щ鍚庡師鏍峰啓鍏ワ紝鍐嶆寜姹囨�昏〃鑷韩鏁版嵁姹傚�肩紦瀛樸��
+                            // 涓嶇洿鎺ユ惉 POI 瀵规簮琛ㄧ殑姹傚�肩粨鏋滐紙濡� RANK 瀵规墜宸ョ暀绌虹殑鍚屾瘮涓�寰嬭繑鍥� 1锛夈��
+                            String f = sc.getCellFormula();
+                            if (f == null || f.isEmpty()) continue;
+                            String shifted = shiftFormula(f, colOff, rowOff);
+                            if (dcCell == null) dcCell = rankCell(dstRow, dc);
+                            dcCell.setCellFormula(shifted);
+                            try {
+                                dstEv.evaluateFormulaCell(dcCell);
+                            } catch (Exception ignore) {
+                                // 姹傚�煎け璐ヤ笉闃诲瀵煎嚭锛氭墦寮�鏃剁敱 Excel/WPS 閲嶇畻
+                            }
+                            copied++;
+                            continue;
+                        }
+                        Double v = rankNumeric(sc, ev);
+                        if (v == null) continue;
+                        if (dcCell == null) dcCell = rankCell(dstRow, dc);
+                        dcCell.setCellValue(v);
+                        copied++;
+                    }
+                }
+            }
+        }
+        return copied;
+    }
+
+    /** 鐩爣鍧楃己鏍兼椂鏂板缓锛屽苟娌跨敤鍚岃宸﹂偦鏍兼牱寮忥紙闃叉柊鏍间涪杈规/鐧惧垎鏁版牸寮忥級 */
+    private Cell rankCell(Row row, int col) {
+        Cell c = row.createCell(col);
+        Cell left = col > 0 ? row.getCell(col - 1) : null;
+        if (left != null) c.setCellStyle(left.getCellStyle());
+        return c;
+    }
+
+    /** 鍏紡鍐呮墍鏈� A1 寮曠敤鏁翠綋骞崇Щ锛氬垪 +dCol銆佽 +dRow锛圫UM/RANK 绛夊嚱鏁板悕鍚庢帴鎷彿涓嶄細琚尮閰嶏級 */
+    private String shiftFormula(String formula, int dCol, int dRow) {
+        if (formula == null || (dCol == 0 && dRow == 0)) return formula;
+        java.util.regex.Matcher m = java.util.regex.Pattern
+                .compile("(?<![A-Za-z0-9_$])([$]?)([A-Z]{1,3})([$]?)([0-9]+)")
+                .matcher(formula);
+        StringBuffer sb = new StringBuffer();
+        while (m.find()) {
+            String col = m.group(2);
+            int idx = 0;
+            for (int i = 0; i < col.length(); i++) idx = idx * 26 + (col.charAt(i) - 'A' + 1);
+            int nidx = Math.max(1, idx + dCol);
+            StringBuilder nc = new StringBuilder();
+            while (nidx > 0) {
+                int rem = (nidx - 1) % 26;
+                nc.insert(0, (char) ('A' + rem));
+                nidx = (nidx - 1) / 26;
+            }
+            int nrow = Math.max(1, Integer.parseInt(m.group(4)) + dRow);
+            m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(m.group(1) + nc + m.group(3) + nrow));
+        }
+        m.appendTail(sb);
+        return sb.toString();
+    }
+
+    /** 鎺掑悕椤甸噷鐨勨�滃潡鏍囬鈥濆崟鍏冩牸锛堝惈鈥滃叏鐪佸垎甯傚窞鈥濈殑璇存槑鏂囧瓧锛夛紝鎸夎銆佸垪椤哄簭杩斿洖 */
+    private List<XSSFCell> rankTitleCells(Sheet sh) {
+        List<XSSFCell> out = new ArrayList<>();
+        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
+            Row row = sh.getRow(r);
+            if (row == null) continue;
+            for (int c = row.getFirstCellNum(); c < row.getLastCellNum(); c++) {
+                Cell cell = row.getCell(c);
+                if (cell == null || cell.getCellType() != CellType.STRING) continue;
+                String t = cell.getStringCellValue();
+                if (t != null && t.contains("鍏ㄧ渷鍒嗗競宸�")) out.add((XSSFCell) cell);
+            }
+        }
+        return out;
+    }
+
+    /** 鍧楅珮搴︼細浠庤捣濮嬭鍚戜笅鐩村埌鏁磋涓虹┖锛堝湪鍧楀垪鑼冨洿鍐咃級 */
+    private int rankBlockHeight(Sheet sh, int r0, int c0) {
+        int h = 0;
+        for (int r = r0; r <= sh.getLastRowNum(); r++) {
+            Row row = sh.getRow(r);
+            boolean any = false;
+            if (row != null) {
+                int last = row.getLastCellNum();
+                for (int c = c0; c < last; c++) {
+                    if (rankHasContent(row.getCell(c))) { any = true; break; }
+                }
+            }
+            if (!any) break;
+            h++;
+        }
+        return h;
+    }
+
+    /** 鍧楀搴︼細浠庤捣濮嬪垪鍚戝彸鐩村埌鏁村垪涓虹┖锛堝湪鍧楄鑼冨洿鍐咃紝鎵弿涓婇檺 40 鍒楋級 */
+    private int rankBlockWidth(Sheet sh, int r0, int c0) {
+        int w = 0;
+        for (int c = c0; c < c0 + 40; c++) {
+            boolean any = false;
+            for (int r = r0; r < r0 + 30; r++) {
+                Row row = sh.getRow(r);
+                if (row != null && rankHasContent(row.getCell(c))) { any = true; break; }
+            }
+            if (!any) break;
+            w++;
+        }
+        return w;
+    }
+
+    private boolean rankHasContent(Cell c) {
+        if (c == null) return false;
+        CellType t = c.getCellType();
+        if (t == CellType.BLANK) return false;
+        if (t == CellType.STRING) {
+            String v = c.getStringCellValue();
+            return v != null && !v.trim().isEmpty();
+        }
+        return true;
+    }
+
+    /** 鍙栧崟鍏冩牸鏁板�硷紙鍏紡鍙栨眰鍊肩粨鏋滐紝绾暟瀛楁枃鏈寜鏁板�艰В鏋愶級锛屽彇涓嶅埌杩斿洖 null */
+    private Double rankNumeric(Cell c, FormulaEvaluator ev) {
+        try {
+            CellType t = c.getCellType();
+            if (t == CellType.NUMERIC) return c.getNumericCellValue();
+            if (t == CellType.FORMULA) {
+                org.apache.poi.ss.usermodel.CellValue cv = ev.evaluate(c);
+                return cv != null && cv.getCellType() == CellType.NUMERIC ? cv.getNumberValue() : null;
+            }
+            if (t == CellType.STRING) {
+                String v = c.getStringCellValue();
+                if (v == null) return null;
+                String x = v.replace(",", "").trim();
+                if (x.isEmpty()) return null;
+                try { return Double.parseDouble(x); } catch (NumberFormatException ignore) { return null; }
+            }
+        } catch (Exception ignore) {
+            return null;
+        }
+        return null;
+    }
+
+    /** 瀹氫綅鐗堝紡鍙傜収浠� docs/鐢熸垚姹囨�诲ぇ琛�/鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx锛堜紭鍏堢骇鍚屾瘝鐗堬細閰嶇疆鐩綍 鈫� user.dir 鐩稿 鈫� 涓婄骇锛� */
+    private File resolveSummaryLayoutRef() {
+        String rel = summaryTemplateDir;
+        while (rel != null && rel.startsWith("./")) rel = rel.substring(2);
+        String[] roots = {
+            summaryTemplateDir,
+            System.getProperty("user.dir") + "/" + rel,
+            System.getProperty("user.dir") + "/../" + rel
+        };
+        for (String root : roots) {
+            if (root == null || root.trim().isEmpty()) continue;
+            File f = new File(root, "鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx");
+            if (f.isFile()) return f;
+        }
+        return null;
+    }
+
     /** 姹囨�昏〃姣嶇増瀹氫綅锛氫粎绮剧‘鍖归厤 {year}骞磠month}鏈堚�︼紙P0-P1 楠ㄦ灦鏈熶笉鍏佽璺ㄦ湀/璺ㄥ勾闈欓粯鍥為��锛�
      *  鐩爣鏈熸棤瀵瑰簲姣嶇増鏃舵槑纭姤閿欙紝鏈堜唤鎵╁睍瑙勫垯寰� P0 姣嶇増瀹氱鍚庡啀鏀惧紑锛� */
     private File resolveSummaryMother(String period, String mode) throws Exception {

--
Gitblit v1.9.1