traffic-audit-server/src/main/java/com/trafficaudit/reportexport/layout/SummaryLayoutExpander.java
@@ -41,6 +41,19 @@
    private static final int HEADER_SCAN_ROWS = 15;
    /** 标题由程序在出表时现写的 7 个页签(母版里的标题文字需按母版期次重写)。 */
    private static final List<String> TITLE_PAGES = Arrays.asList(
            "货运量排名", "货运周转量排名", "城市汇总", "城市分市州", "中口径排名", "中口径分析", "道路运输周转量");
    /** 标题识别标记(命中其一才改写,避免误伤备注等文字)。 */
    private static final List<String> TITLE_MARKERS = Arrays.asList(
            "全省分市州累计完成公路货运量情况", "全省分市州完成公路货运量情况",
            "全省分市州累计完成公路货物运输周转量情况", "全省分市州完成公路货物运输周转量情况",
            "全省累计完成城市客运量情况", "全省完成城市客运量情况",
            "分市州1-", "全省分市州累计完成道路客运生产情况", "全省分市州完成道路客运生产情况",
            "中口径客运量及周转量",
            "全省累计完成交通周转量情况", "全省完成交通周转量情况");
    /** 公式里的 A1 引用(可带工作表前缀)。 */
    private static final Pattern REF = Pattern.compile(
            "(?:('([^']+)'|([A-Za-z_\\u4e00-\\u9fff][A-Za-z0-9_.\\u4e00-\\u9fff\u3001]*))!)?(\\$?)([A-Z]{1,3})(\\$?)(\\d+)");
@@ -67,6 +80,10 @@
                if (sh == null || expanded.contains(sh.getSheetName())) continue;
                shiftQualifiedRefs(sh, insertColOf);
            }
            int titleFixed = applyTitles(wb, year, targetMonth);
            if (titleFixed > 0) {
                log.info("扩列:标题按 {}年{}月 重写 {} 格", year, targetMonth, titleFixed);
            }
            wb.setForceFormulaRecalculation(true);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            wb.write(out);
@@ -74,6 +91,54 @@
        } catch (Exception e) {
            throw new IllegalStateException("汇总大表扩列失败: " + e.getMessage(), e);
        }
    }
    /** 只把 7 个标题页的标题按给定期次重写(用于修正母版里滞后的标题)。 */
    public byte[] fixTitles(byte[] source, int year, int month) {
        try (XSSFWorkbook wb = new XSSFWorkbook(new ByteArrayInputStream(source))) {
            int n = applyTitles(wb, year, month);
            wb.setForceFormulaRecalculation(true);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            wb.write(out);
            log.info("汇总大表标题修正完成:{}年{}月,改写 {} 格", year, month, n);
            return out.toByteArray();
        } catch (Exception e) {
            throw new IllegalStateException("汇总大表标题修正失败: " + e.getMessage(), e);
        }
    }
    /** 把 7 个页签的标题文字按「累计=1-M月、当月=M月」重写;命中标记才改,避免误伤备注。 */
    private int applyTitles(XSSFWorkbook wb, int year, int month) {
        int changed = 0;
        for (String page : TITLE_PAGES) {
            Sheet sh = wb.getSheet(page);
            if (sh == null) continue;
            int lastRow = Math.min(sh.getLastRowNum(), 40);
            for (int r = 0; r <= lastRow; r++) {
                Row row = sh.getRow(r);
                if (row == null) continue;
                for (int c = 0; c <= 1; c++) {
                    Cell cell = row.getCell(c);
                    if (cell == null || cell.getCellType() != CellType.STRING) continue;
                    String t = cell.getStringCellValue();
                    if (t == null || !looksLikeTitle(t)) continue;
                    String nt = t.replaceAll("(\\d{4})年", year + "年")
                                 .replaceAll("(\\d{1,2})月", month + "月");
                    if (!nt.equals(t)) {
                        cell.setCellValue(nt);
                        changed++;
                    }
                }
            }
        }
        return changed;
    }
    private boolean looksLikeTitle(String t) {
        for (String m : TITLE_MARKERS) {
            if (t.contains(m)) return true;
        }
        return false;
    }
    /** 对单页扩列,返回插入位置(0 基列号)。 */
@@ -143,8 +208,8 @@
            if (headerRows.contains(r)) continue;
            Row row = sh.getRow(r);
            if (row == null) continue;
            cloneDataCell(row, srcVal, insertCol, shiftable, insertCol);
            cloneDataCell(row, srcYoy, insertCol + 1, shiftable, insertCol);
            cloneDataCell(row, srcVal, insertCol, shiftable);
            cloneYoyCell(row, srcYoy, insertCol + 1, insertCol, lastYearSameMonth + 2);
        }
        // 5) 累计列补当月项;累计同比分母补去年同月列
        int cumCol = insertCol + 2, cumYoyCol = insertCol + 3;
@@ -283,17 +348,47 @@
        }
    }
    private void cloneDataCell(Row row, int srcCol, int dstCol, Set<String> shiftable, int insertCol) {
    private void cloneDataCell(Row row, int srcCol, int dstCol, Set<String> shiftable) {
        Cell s = row.getCell(srcCol);
        if (s == null) return;
        Cell d = getOrCreate(row, dstCol);
        d.setBlank();
        d.setCellStyle(s.getCellStyle());
        if (s.getCellType() == CellType.FORMULA) {
            d.setCellFormula(shiftRefs(s.getCellFormula(), insertCol, shiftable));
            d.setCellFormula(shiftRefsAll(s.getCellFormula(), shiftable));
        }
    }
    /** 当月同比列:不克隆上月公式,直接按「本月值 / 去年同月 − 1」重写(去年同月列已按表头定位) */
    private void cloneYoyCell(Row row, int srcCol, int dstCol, int valCol0, int denCol0) {
        Cell s = row.getCell(srcCol);
        if (s == null) return;
        Cell d = getOrCreate(row, dstCol);
        d.setBlank();
        d.setCellStyle(s.getCellStyle());
        if (s.getCellType() == CellType.FORMULA) {
            int excelRow = row.getRowNum() + 1;
            d.setCellFormula(colLetter(valCol0) + excelRow + "/" + colLetter(denCol0) + excelRow + "-1");
        }
    }
    /** 相对复制用的平移:把本页/已扩列页的引用整体 +2(不做列号阈值判断) */
    private String shiftRefsAll(String formula, Set<String> shiftable) {
        Matcher m = REF.matcher(formula);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            String sheet = m.group(2) != null ? m.group(2) : m.group(3);
            if ((sheet != null && !shiftable.contains(sheet)) || isFunctionName(formula, m)) {
                m.appendReplacement(sb, Matcher.quoteReplacement(m.group(0)));
                continue;
            }
            int colIdx = colIndex(m.group(5));
            m.appendReplacement(sb, Matcher.quoteReplacement(rebuild(m, sheet, colIdx + 2)));
        }
        m.appendTail(sb);
        return sb.toString();
    }
    private void shiftAllRefs(Sheet sh, int insertCol, Set<String> shiftable) {
        for (int r = 0; r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);