zhizhijie
2026-09-11 a76a8cce476ceccbc061a3f51681b6090a7d3741
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;
@@ -3351,10 +3352,11 @@
            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, widthApplied, dbProblems);
                    + "刷新排名页数值格数={},按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}",
                    dbFilled, yoyFilled, monthFormulaFilled, rankRefreshed, widthApplied, dbProblems);
            applyTwoDecimalFormat(wb); // 数值显示两位小数(保留全精度);% / 日期等既有样式不改变
            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 还原/改写公式后打开即重算
@@ -3441,6 +3443,216 @@
        return max;
    }
    // ==================== 汇总工作簿内的排名页刷新 ====================
    /**
     * 汇总工作簿里的「货运量排名 / 货运周转量排名 / 中口径排名」在母版里是上一报表期的静态值,
     * 导出时用独立排名表(《生成_货运量排名》《生成_周转量排名》《生成_中口径排名》,与用户日常核对的口径一致)
     * 的结果刷新数值,避免汇总表里的排名页停留在旧月份。
     * 数值格搬值、公式格按偏移平移公式后写入;目标格本身已是公式的(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,POI 的 setCellValue 只写 <v>、
                            // 不更新 <is>,Excel 会继续显示旧月份,必须先 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(SUM/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;