xyc
3 天以前 488cf7a7127fc137864d4fdeee85942986c05108
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());
        }
    }
@@ -250,23 +264,48 @@
        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) {
        Map<Integer, Integer> cols = new LinkedHashMap<>();
        int lastRow = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + HEADER_SCAN_ROWS);