zhizhijie
4 天以前 978526b474da9a495554ddc924bd736c52bb549b
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/layout/SummaryLayoutExpander.java
@@ -3,6 +3,7 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.FormulaEvaluator;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
@@ -80,6 +81,7 @@
                if (sh == null || expanded.contains(sh.getSheetName())) continue;
                shiftQualifiedRefs(sh, insertColOf);
            }
            recalcAndCache(wb);
            int titleFixed = applyTitles(wb, year, targetMonth);
            if (titleFixed > 0) {
                log.info("扩列:标题按 {}年{}月 重写 {} 格", year, targetMonth, titleFixed);
@@ -96,6 +98,7 @@
    /** 只把 7 个标题页的标题按给定期次重写(用于修正母版里滞后的标题)。 */
    public byte[] fixTitles(byte[] source, int year, int month) {
        try (XSSFWorkbook wb = new XSSFWorkbook(new ByteArrayInputStream(source))) {
            recalcAndCache(wb);
            int n = applyTitles(wb, year, month);
            wb.setForceFormulaRecalculation(true);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -104,6 +107,17 @@
            return out.toByteArray();
        } catch (Exception e) {
            throw new IllegalStateException("汇总大表标题修正失败: " + e.getMessage(), e);
        }
    }
    /** 整本重算并把结果写入缓存值:否则母版里只有公式没有值,后续读取会把空值当 0(2026-10-12 修) */
    private void recalcAndCache(XSSFWorkbook wb) {
        try {
            FormulaEvaluator ev = wb.getCreationHelper().createFormulaEvaluator();
            ev.clearAllCachedResultValues();
            ev.evaluateAll();
        } catch (Exception e) {
            log.warn("扩列:整本重算未完成(公式仍在,Excel 打开会再算):{}", e.getMessage());
        }
    }
@@ -208,8 +222,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;
@@ -250,21 +264,46 @@
        return insertCol;
    }
    /** 表头行(0 基):含「年+月」表头单元格的行;轨道、轮渡有两个块 → 两行。 */
    /**
     * 表头行(0 基):轨道、轮渡这类一页两个块的有两行。
     *
     * <p>2026-10-12 二次修正:先取「月份表头格最多的那一行」作基准,把它的月份列当作标准列,
     * 只有这些列上大半是月份格的行才算表头行。前版只要求「整行 ≥2 个月份格」,
     * 会把数据行里恰好落成日期序列的数值当成表头 → 扩列时把月份日期写进数据行
     * (2026-09 母版 ` 货运!S11`、`出租!S6` 等 13 格被写成 46266)。
     */
    private List<Integer> headerRowsOf(Sheet sh) {
        List<Integer> rows = new ArrayList<>();
        int lastRow = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + HEADER_SCAN_ROWS);
        for (int r = sh.getFirstRowNum(); r <= lastRow; r++) {
        int first = sh.getFirstRowNum();
        int lastRow = Math.min(sh.getLastRowNum(), first + HEADER_SCAN_ROWS);
        List<Integer> base = new ArrayList<>();
        for (int r = first; r <= lastRow; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) {
                if (yearMonthOf(row.getCell(c)) != null) {
                    rows.add(r);
                    break;
                }
            List<Integer> cols = monthColsOf(row);
            if (cols.size() > base.size()) base = cols;
        }
        List<Integer> rows = new ArrayList<>();
        if (base.size() < 2) return rows;
        int need = Math.max(2, (base.size() + 1) / 2);
        for (int r = first; r <= lastRow; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            int hit = 0;
            for (int c : base) {
                if (yearMonthOf(row.getCell(c)) != null) hit++;
            }
            if (hit >= need) rows.add(r);
        }
        return rows;
    }
    /** 行内所有「年+月」表头格的列号(0 基) */
    private List<Integer> monthColsOf(Row row) {
        List<Integer> cols = new ArrayList<>();
        for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) {
            if (yearMonthOf(row.getCell(c)) != null) cols.add(c);
        }
        return cols;
    }
    private Map<Integer, Integer> monthCols(Sheet sh, int year) {
@@ -348,17 +387,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);