traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -3770,7 +3770,7 @@
            // 2026-10-13:累计列 / 累计同比分母按表头重建(根治扩列器早期“累计漏月”的历史欠账)
            int cumRebuilt = summaryWorkbookFiller.repairCumulativeFormulas(wb, year, effMonth);
            reportProgress(progress, 45, "刷新排名页与跨页合成页");
            int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month");
            int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month", dbProblems);
            int cumulativeRefreshed = refreshSummaryCumulativeSheets(wb, year, effMonth); // 四张跨页合成页随报表期刷新
            // 2026-10-12:停用「套用外部参照件《生成_道路运输量汇总表.xlsx》列宽」——版式一律以当期母版为准,
            // 避免母版与参照件两处维护(母版已并入参照件的列宽/隐藏)。
@@ -4250,11 +4250,11 @@
     * 的结果刷新数值,避免汇总表里的排名页停留在旧月份。
     * 数值格搬值、公式格按偏移平移公式后写入;目标格本身已是公式的(SUM/RANK/占比/补数)保留不动,打开时按新数据重算。
     */
    private int refreshSummaryRankSheets(XSSFWorkbook wb, String period, String mode) throws Exception {
    private int refreshSummaryRankSheets(XSSFWorkbook wb, String period, String mode, List<String> problems) 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, wb), new int[][]{{0, 0}, {0, 10}});
        n += copyRankValues(wb, "货运量排名", exportFreightRank(period, mode), new int[][]{{0, 0}, {22, 0}}, problems);
        n += copyRankValues(wb, "货运周转量排名", exportTurnoverRank(period, mode), new int[][]{{0, 0}, {22, 0}}, problems);
        n += copyRankValues(wb, "中口径排名", exportPassengerMidRank(period, mode, wb), new int[][]{{0, 0}, {0, 10}}, problems);
        return n;
    }
@@ -4263,13 +4263,15 @@
     * srcBlocks[i] = 源块 i 的起始 {行, 列}(0 基);目标块按标题顺序(累计块在前、当月块在后)对应。
     * 数值格直接搬值;源侧是公式的(排名/占比等)按目标块偏移整体平移后写入公式,打开时按汇总表自身数据重算。
     */
    private int copyRankValues(XSSFWorkbook dstWb, String dstSheetName, byte[] srcBytes, int[][] srcBlocks) throws Exception {
    private int copyRankValues(XSSFWorkbook dstWb, String dstSheetName, byte[] srcBytes, int[][] srcBlocks,
                              List<String> problems) 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();
        List<int[]> anchors = new ArrayList<>(); // {源全省行, 目标全省行, 块首列, 列偏移, 块宽}
        try (InputStream sin = new ByteArrayInputStream(srcBytes);
             XSSFWorkbook srcWb = new XSSFWorkbook(sin)) {
            XSSFSheet src = srcWb.getSheetAt(0);
@@ -4279,10 +4281,27 @@
                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 rowOff = dstTitle.getRowIndex() - sr0;
                // 2026-10-13:《货运周转量排名》母版块标题曾占两行(合并),"标题行+固定偏移"会把源行
                // 整体写错位一行(全省值落到表头行、市州值与市州名错开)。改为用「全省」行做锚点对齐:
                // rowOff = 目标全省行 - 源全省行;识别不到锚点再回退标题行偏移并告警。
                Integer srcAnchor = rankAnchorRow(src, sr0, sc0);
                Integer dstAnchor = rankAnchorRow(dst, dstTitle.getRowIndex(), dstTitle.getColumnIndex());
                if (srcAnchor != null && dstAnchor != null) {
                    int anchored = dstAnchor - srcAnchor;
                    if (anchored != rowOff) {
                        log.info("排名页《{}》第 {} 块:标题行偏移 {} 与「全省」锚点偏移 {} 不一致,按锚点对齐(源全省行={},目标全省行={})",
                                dstSheetName, b + 1, rowOff, anchored, srcAnchor + 1, dstAnchor + 1);
                    }
                    rowOff = anchored;
                } else {
                    log.warn("排名页《{}》第 {} 块:未识别到「全省」锚点(源={},目标={}),回退按标题行偏移 {} 对齐",
                            dstSheetName, b + 1, srcAnchor, dstAnchor, rowOff);
                }
                int h = rankBlockHeight(src, sr0, sc0);
                int w = rankBlockWidth(src, sr0, sc0);
                anchors.add(new int[]{srcAnchor == null ? -1 : srcAnchor, dstAnchor == null ? -1 : dstAnchor, sc0, colOff, w});
                // 2026-10-12:整块缺数(源侧一个数值都没有,如 9 月缺②③时的周转量排名)→ 目标块只留标题与标签,
                // 数据/排名/占比一律留空;否则母版的 RANK/占比公式会在空块上算出「全 1 + #DIV/0!」。
                boolean blockHasData = false;
@@ -4361,6 +4380,34 @@
                    }
                }
            }
        // 2026-10-13 自检:目标「全省」行应与源侧一致(偏移错位、汇总区间不对时报警并写进缺数清单)
        for (int[] an : anchors) {
            if (an[0] < 0 || an[1] < 0) continue;
            Row srr = src.getRow(an[0]);
            Row drr = dst.getRow(an[1]);
            if (srr == null || drr == null) continue;
            int badCols = 0;
            StringBuilder ex = new StringBuilder();
            for (int c = 0; c < an[4]; c++) {
                Cell sCell = srr.getCell(an[2] + c);
                Cell dCell = drr.getCell(an[2] + c + an[3]);
                Double sv = sCell == null ? null : rankNumeric(sCell, ev);
                Double dv = dCell == null ? null : rankNumeric(dCell, dstEv);
                if (sv == null && dv == null) continue;
                if (sv == null || dv == null || Math.abs(sv - dv) > Math.max(1.0, Math.abs(sv) * 1e-4)) {
                    badCols++;
                    if (badCols <= 3) {
                        if (ex.length() > 0) ex.append(";");
                        ex.append(colLetter(an[2] + c)).append(" 源=").append(sv).append(" 目标=").append(dv);
                    }
                }
            }
            if (badCols >= 2) {
                String msg = "排名页《" + dstSheetName + "》「全省」行与源侧不一致(" + badCols + " 列):" + ex;
                log.warn(msg);
                if (problems != null) problems.add("生成校验:" + msg);
            }
        }
        }
        return copied;
    }
@@ -4427,6 +4474,23 @@
    }
    /** 排名页里的“块标题”单元格(含“全省分市州”的说明文字),按行、列顺序返回 */
    /** 在 [fromRow, fromRow+40) 范围内、块起始两列里找整格文本为「全省」的行号(0 基);找不到返回 null。
     *  2026-10-13:排名页块结构可能因母版版式不同而整体上/下移一行,用「全省」行做锚点可自动对齐。 */
    private Integer rankAnchorRow(Sheet sh, int fromRow, int col0) {
        int last = Math.min(sh.getLastRowNum(), fromRow + 40);
        for (int r = fromRow; r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (int c = Math.max(0, col0); c <= col0 + 1; c++) {
                Cell cell = row.getCell(c);
                if (cell == null || cell.getCellType() != CellType.STRING) continue;
                String v = cell.getStringCellValue();
                if (v != null && v.replace(" ", "").equals("全省")) return r;
            }
        }
        return null;
    }
    private List<XSSFCell> rankTitleCells(Sheet sh) {
        List<XSSFCell> out = new ArrayList<>();
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {