| | |
| | | + "按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}", |
| | | dbFilled, yoyFilled, monthFormulaFilled, rankRefreshed, cumulativeRefreshed, widthApplied, dbProblems); |
| | | applyTwoDecimalFormat(wb); // 数值显示两位小数(保留全精度);% / 日期等既有样式不改变 |
| | | int inlineFixed = normalizeStaleInlineStrings(wb); // 清掉「非 inlineStr 却残留 <is>」的非法格,否则 Excel 拒绝打开 |
| | | if (inlineFixed > 0) log.warn("汇总工作簿清掉残留 inlineStr 非法格 {} 个(母版由 openpyxl 写出时的典型问题)", inlineFixed); |
| | | int[] recalc = recalcWorkbookFormulas(wb); // 公式重算并写回缓存值:全省行(Σ市州)/累计列/排名 SUM 才能被预览与二次处理读到 |
| | | log.info("汇总工作簿公式重算完成:成功 {} 格,失败 {} 格(失败格保留母版旧缓存值,打开时按 fullCalcOnLoad 重算)", recalc[0], recalc[1]); |
| | | if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr(); |
| | | wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 还原/改写公式后打开即重算 |
| | | return toBytes(wb); |
| | | } finally { |
| | | ZipSecureFile.setMinInflateRatio(savedZipRatio); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 汇总工作簿公式重算:POI 写文件不带缓存值,导致「全省行(Σ17市州)」「累计列」「排名 SUM」 |
| | | * 在预览/二次处理里只看到母版上一期的旧值(2026-09-18 用户报“8月没数据 / 全省行停在上一期”的根因之一)。 |
| | | * 逐格 evaluate 并把结果写回缓存值;个别不支持函数/外部引用格失败则跳过并计数,不阻塞导出。 |
| | | */ |
| | | private int[] recalcWorkbookFormulas(XSSFWorkbook wb) { |
| | | if (wb == null) return new int[]{0, 0}; |
| | | org.apache.poi.ss.usermodel.FormulaEvaluator ev; |
| | | try { |
| | | ev = wb.getCreationHelper().createFormulaEvaluator(); |
| | | } catch (Exception e) { |
| | | log.warn("汇总工作簿公式重算:创建求值器失败,跳过({})", e.getMessage()); |
| | | return new int[]{0, 0}; |
| | | } |
| | | int ok = 0, fail = 0; |
| | | java.util.List<String> samples = new java.util.ArrayList<>(); |
| | | for (int i = 0; i < wb.getNumberOfSheets(); i++) { |
| | | Sheet sh = wb.getSheetAt(i); |
| | | if (sh == null) continue; |
| | | for (Row row : sh) { |
| | | if (row == null) continue; |
| | | for (Cell c : row) { |
| | | if (c == null || c.getCellType() != CellType.FORMULA) continue; |
| | | try { |
| | | ev.evaluateFormulaCell(c); // 注意:evaluate() 无副作用,必须用 evaluateFormulaCell 才会把结果写回缓存值 |
| | | ok++; |
| | | } catch (Exception ex) { |
| | | fail++; |
| | | if (samples.size() < 10) { |
| | | samples.add(sh.getSheetName() + "!" + c.getAddress() + " :: " + ex.getClass().getSimpleName()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | if (!samples.isEmpty()) log.warn("汇总工作簿公式重算失败样例:{}", samples); |
| | | return new int[]{ok, fail}; |
| | | } |
| | | |
| | | /** |
| | | * 清掉「非 inlineStr 却残留 <is>」的非法单元格。 |
| | | * |
| | | * 母版若由 openpyxl 写出,文本单元格用 <is>(inlineStr)存储;POI 的 setCellValue 只改写 t 与 <v>, |
| | | * 不会清掉旧 <is>,于是产出 <c t="n"><v>67.0</v><is><t>67.00</t></is></c> 这种非法格, |
| | | * Excel 判定为「无法读取的内容」而直接拒绝打开(COM 表现为「不能取得类 Workbooks 的 Open 属性」)。 |
| | | * 2026-07 导出件即因此打不开(《公交》17 格 +《轨道、轮渡》2 格)。落盘前统一清理,幂等。 |
| | | */ |
| | | private int normalizeStaleInlineStrings(XSSFWorkbook wb) { |
| | | int fixed = 0; |
| | | for (int i = 0; i < wb.getNumberOfSheets(); i++) { |
| | | org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorksheet ctw = wb.getSheetAt(i).getCTWorksheet(); |
| | | if (ctw == null || ctw.getSheetData() == null) continue; |
| | | for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRow row : ctw.getSheetData().getRowArray()) { |
| | | for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCell c : row.getCArray()) { |
| | | if (!c.isSetIs()) continue; |
| | | STCellType.Enum t = c.getT(); |
| | | if (t == null || !STCellType.INLINE_STR.equals(t)) { |
| | | c.unsetIs(); |
| | | fixed++; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return fixed; |
| | | } |
| | | |
| | | /** |
| | |
| | | if (bs == null) continue; |
| | | // 中口径排名页 A..H 陈旧左块已于 2026-09-09 拍板整块删除:该区域永不从备份母版回填,避免“生成又复活” |
| | | boolean skipMidRankLeftBlock = "中口径排名".equals(ds.getSheetName()); |
| | | // 《 货运》页 2025..2021 年度块(T 列起,含各年累计/累计同比列)在母版里是「市州派生公式」, |
| | | // 备份母版同区仍是旧的写死常量(全省规下等),回填会让 2021/2022/2023 累计又对不上(“生成又复活”),故该区不回填。 |
| | | boolean skipFreightHistoryBlocks = " 货运".equals(ds.getSheetName()); |
| | | for (int r = ds.getFirstRowNum(); r <= ds.getLastRowNum(); r++) { |
| | | Row dr = ds.getRow(r); |
| | | if (dr == null) continue; |
| | |
| | | for (Cell dc : dr) { |
| | | if (dc == null) continue; |
| | | if (skipMidRankLeftBlock && dc.getColumnIndex() < 8) continue; |
| | | if (skipFreightHistoryBlocks && dc.getColumnIndex() >= 19) continue; // T 列及以后 = 2025..2021 年度块 |
| | | CellType t = dc.getCellType(); |
| | | if (t == CellType.BLANK) continue; |
| | | String dtx = summaryCellText(dc); |