| | |
| | | // 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》列宽」——版式一律以当期母版为准, |
| | | // 避免母版与参照件两处维护(母版已并入参照件的列宽/隐藏)。 |
| | |
| | | * 的结果刷新数值,避免汇总表里的排名页停留在旧月份。 |
| | | * 数值格搬值、公式格按偏移平移公式后写入;目标格本身已是公式的(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; |
| | | } |
| | | |
| | |
| | | * 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); |
| | |
| | | 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; |
| | |
| | | } |
| | | } |
| | | } |
| | | // 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; |
| | | } |
| | |
| | | } |
| | | |
| | | /** 排名页里的“块标题”单元格(含“全省分市州”的说明文字),按行、列顺序返回 */ |
| | | /** 在 [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++) { |