| | |
| | | import org.apache.poi.ss.usermodel.Cell; |
| | | import org.apache.poi.ss.usermodel.CellStyle; |
| | | import org.apache.poi.ss.usermodel.CellType; |
| | | import org.apache.poi.ss.usermodel.Font; |
| | | import org.apache.poi.ss.usermodel.HorizontalAlignment; |
| | | import org.apache.poi.ss.usermodel.IndexedColors; |
| | | import org.apache.poi.ss.usermodel.Row; |
| | | import org.apache.poi.ss.usermodel.Sheet; |
| | | import org.apache.poi.ss.usermodel.VerticalAlignment; |
| | | import org.apache.poi.ss.usermodel.FormulaEvaluator; |
| | | import org.apache.poi.xssf.usermodel.XSSFSheet; |
| | | import org.apache.poi.xssf.usermodel.XSSFWorkbook; |
| | |
| | | Map<String, ScaleSplitTransport> result = new HashMap<>(); |
| | | List<ScaleSplitTransport> list = scaleSplitMapper.selectList( |
| | | new LambdaQueryWrapper<ScaleSplitTransport>() |
| | | .eq(ScaleSplitTransport::getPeriodType, "CUMULATIVE")); |
| | | .eq(ScaleSplitTransport::getPeriodType, "CUMULATIVE") |
| | | .eq(ScaleSplitTransport::getReportPeriod, period)); // 2026-10-12:只认目标期,缺数留空、不静默回退上一期 |
| | | for (ScaleSplitTransport record : list) { |
| | | if (record.getReportPeriod() == null) continue; |
| | | if (record.getReportPeriod().compareTo(period) > 0) continue; |
| | | if (!period.equals(record.getReportPeriod())) continue; |
| | | String city = RegionUtil.normalizeCityName(record.getRegionName()); |
| | | if (city == null) continue; |
| | | if ("湖北省".equals(city)) continue; |
| | |
| | | private ScaleSplitTransport getProvinceCumulative(String period) { |
| | | List<ScaleSplitTransport> list = scaleSplitMapper.selectList( |
| | | new LambdaQueryWrapper<ScaleSplitTransport>() |
| | | .eq(ScaleSplitTransport::getPeriodType, "CUMULATIVE")); |
| | | .eq(ScaleSplitTransport::getPeriodType, "CUMULATIVE") |
| | | .eq(ScaleSplitTransport::getReportPeriod, period)); // 2026-10-12:同上(只认目标期) |
| | | ScaleSplitTransport best = null; |
| | | for (ScaleSplitTransport record : list) { |
| | | if (record.getReportPeriod() == null) continue; |
| | | if (record.getReportPeriod().compareTo(period) > 0) continue; |
| | | if (!period.equals(record.getReportPeriod())) continue; |
| | | String name = RegionUtil.normalizeCityName(record.getRegionName()); |
| | | if (!"湖北省".equals(name)) continue; |
| | | if (best == null || record.getReportPeriod().compareTo(best.getReportPeriod()) > 0) best = record; |
| | |
| | | reportProgress(progress, 45, "刷新排名页与跨页合成页"); |
| | | int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month"); |
| | | int cumulativeRefreshed = refreshSummaryCumulativeSheets(wb, year, effMonth); // 四张跨页合成页随报表期刷新 |
| | | int widthApplied = applySummaryReferenceLayout(wb); |
| | | // 2026-10-12:停用「套用外部参照件《生成_道路运输量汇总表.xlsx》列宽」——版式一律以当期母版为准, |
| | | // 避免母版与参照件两处维护(母版已并入参照件的列宽/隐藏)。 |
| | | int widthApplied = 0; |
| | | log.info("汇总工作簿数据驱动回填完成:写入当月值格数={},补写当月同比公式格数={},补写当月合计公式格数={}," |
| | | + "修正累计同比分母格数={},刷新排名页数值格数={},刷新四页(城市汇总/城市分市州/中口径分析/道路运输周转量)数值格数={}," |
| | | + "按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}", |
| | |
| | | log.info("汇总工作簿公式重算完成:成功 {} 格,失败 {} 格(失败格保留母版旧缓存值,打开时按 fullCalcOnLoad 重算)", recalc[0], recalc[1]); |
| | | if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr(); |
| | | wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 还原/改写公式后打开即重算 |
| | | appendMissingDataSheet(wb, effPeriod, dbProblems); // 2026-10-12:缺数/体检结果写进生成件(用户要求) |
| | | reportProgress(progress, 95, "写出文件"); |
| | | return toBytes(wb); |
| | | } finally { |
| | | ZipSecureFile.setMinInflateRatio(savedZipRatio); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 生成件《数据缺失清单》页(2026-10-12,用户要求)。 |
| | | * |
| | | * <p>把本次导出的「缺数清单」与「货运基数体检」结果显式写进产物:用户拿到文件即可看到本报表期 |
| | | * 有哪些数没到、哪些数不合理,不必翻导出日志。只读提示页,不参与本表计算。 |
| | | */ |
| | | private void appendMissingDataSheet(XSSFWorkbook wb, String period, List<String> problems) { |
| | | String sheetName = "数据缺失清单"; |
| | | int old = wb.getSheetIndex(sheetName); |
| | | if (old >= 0) wb.removeSheetAt(old); // 母版由「当期产物」派生时可能已带本页,先移除避免重名 |
| | | XSSFSheet sh = wb.createSheet(sheetName); |
| | | |
| | | CellStyle titleStyle = wb.createCellStyle(); |
| | | Font titleFont = wb.createFont(); |
| | | titleFont.setBold(true); |
| | | titleFont.setFontHeightInPoints((short) 14); |
| | | titleStyle.setFont(titleFont); |
| | | |
| | | CellStyle metaStyle = wb.createCellStyle(); |
| | | Font metaFont = wb.createFont(); |
| | | metaFont.setFontHeightInPoints((short) 10); |
| | | metaFont.setColor(IndexedColors.GREY_50_PERCENT.getIndex()); |
| | | metaStyle.setFont(metaFont); |
| | | |
| | | CellStyle sectionStyle = wb.createCellStyle(); |
| | | Font sectionFont = wb.createFont(); |
| | | sectionFont.setBold(true); |
| | | sectionStyle.setFont(sectionFont); |
| | | |
| | | CellStyle bodyStyle = wb.createCellStyle(); |
| | | bodyStyle.setWrapText(true); |
| | | bodyStyle.setVerticalAlignment(VerticalAlignment.TOP); |
| | | |
| | | List<String> missing = new ArrayList<>(); |
| | | List<String> check = new ArrayList<>(); |
| | | List<String> other = new ArrayList<>(); |
| | | if (problems != null) { |
| | | for (String p : problems) { |
| | | if (p == null || p.trim().isEmpty()) continue; |
| | | if (p.startsWith("缺数")) missing.add(p); |
| | | else if (p.startsWith("货运体检")) check.add(p); |
| | | else other.add(p); |
| | | } |
| | | } |
| | | |
| | | int r = 0; |
| | | Row tRow = sh.createRow(r++); |
| | | Cell tCell = tRow.createCell(0); |
| | | tCell.setCellValue("数据缺失清单"); |
| | | tCell.setCellStyle(titleStyle); |
| | | |
| | | Row mRow = sh.createRow(r++); |
| | | Cell mCell = mRow.createCell(0); |
| | | mCell.setCellValue("报表期 " + period + " 生成时间 " |
| | | + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm").format(new java.util.Date()) |
| | | + " 共 " + (missing.size() + check.size() + other.size()) + " 条"); |
| | | mCell.setCellStyle(metaStyle); |
| | | |
| | | Row nRow = sh.createRow(r++); |
| | | Cell nCell = nRow.createCell(0); |
| | | nCell.setCellValue("本页由程序自动生成(只读提示):一、缺数清单=该有的数没有;二、数据体检=数值不合理(如规下为负)。不参与本表计算。"); |
| | | nCell.setCellStyle(metaStyle); |
| | | |
| | | r++; |
| | | r = writeProblemSection(sh, r, "一、缺数清单(该有的数没有)", missing, sectionStyle, bodyStyle); |
| | | r = writeProblemSection(sh, r, "二、数据体检(数值不合理)", check, sectionStyle, bodyStyle); |
| | | if (!other.isEmpty()) { |
| | | r = writeProblemSection(sh, r, "三、其它提示", other, sectionStyle, bodyStyle); |
| | | } |
| | | |
| | | sh.setColumnWidth(0, 6 * 256); |
| | | sh.setColumnWidth(1, 120 * 256); |
| | | sh.createFreezePane(0, 3); |
| | | } |
| | | |
| | | /** 写一个清单分区(标题 + 逐条),返回下一个可写行号 */ |
| | | private int writeProblemSection(XSSFSheet sh, int rowIdx, String title, List<String> lines, |
| | | CellStyle sectionStyle, CellStyle bodyStyle) { |
| | | Row hr = sh.createRow(rowIdx++); |
| | | Cell hc = hr.createCell(0); |
| | | hc.setCellValue(title); |
| | | hc.setCellStyle(sectionStyle); |
| | | sh.addMergedRegion(new CellRangeAddress(rowIdx - 1, rowIdx - 1, 0, 1)); |
| | | if (lines.isEmpty()) { |
| | | Row er = sh.createRow(rowIdx++); |
| | | er.createCell(0).setCellValue("—"); |
| | | er.createCell(1).setCellValue("无"); |
| | | return rowIdx; |
| | | } |
| | | int i = 1; |
| | | for (String line : lines) { |
| | | Row row = sh.createRow(rowIdx++); |
| | | row.createCell(0).setCellValue(i++); |
| | | Cell c = row.createCell(1); |
| | | c.setCellValue(line); |
| | | c.setCellStyle(bodyStyle); |
| | | } |
| | | return rowIdx; |
| | | } |
| | | |
| | | /** 导出进度回调(异步导出任务用):回调自身异常不影响导出 */ |
| | |
| | | int colOff = dstTitle.getColumnIndex() - sc0; |
| | | int h = rankBlockHeight(src, sr0, sc0); |
| | | int w = rankBlockWidth(src, sr0, sc0); |
| | | // 2026-10-12:整块缺数(源侧一个数值都没有,如 9 月缺②③时的周转量排名)→ 目标块只留标题与标签, |
| | | // 数据/排名/占比一律留空;否则母版的 RANK/占比公式会在空块上算出「全 1 + #DIV/0!」。 |
| | | boolean blockHasData = false; |
| | | for (int r = 1; r < h && !blockHasData; r++) { |
| | | Row sr = src.getRow(sr0 + r); |
| | | if (sr == null) continue; |
| | | for (int c = 0; c < w; c++) { |
| | | Cell sc0c = sr.getCell(sc0 + c); |
| | | if (sc0c != null && rankNumeric(sc0c, ev) != null) { |
| | | blockHasData = true; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | 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; |
| | | Cell sc = srcRow.getCell(sc0 + c); |
| | | if (!blockHasData && r > 0) { |
| | | // 整块缺数:只保留标签(市州名/指标名),数据与排名/占比重算结果一律留空 |
| | | if (sc == null || !isRankLabel(sc)) clearRankCell(dstRow, dc); |
| | | continue; |
| | | } |
| | | if (sc == null) { |
| | | // 2026-10-12:源侧整格缺失 = 缺数 → 目标格按「缺数留空」清空(不再沿用母版上一报表期的旧值) |
| | | if (r > 0) clearRankCell(dstRow, dc); |
| | | continue; |
| | | } |
| | | if (r == 0) { |
| | | // 标题行:同步块标题文案。母版标题是 inlineStr,POI 的 setCellValue 只写 <v>、 |
| | | // 不更新 <is>,Excel 会继续显示旧月份,必须先 setBlank 清掉再写。 |
| | |
| | | continue; |
| | | } |
| | | Double v = rankNumeric(sc, ev); |
| | | if (v == null) continue; |
| | | if (v == null) { |
| | | // 2026-10-12:源侧数值位为空(如缺②全口径时排名表的「规下」列)= 缺数 → 留空; |
| | | // 源侧是标签文本(市州名/指标名)时不是缺数,保留母版自带内容。 |
| | | if (!isRankLabel(sc)) clearRankCell(dstRow, dc); |
| | | continue; |
| | | } |
| | | if (dcCell == null) dcCell = rankCell(dstRow, dc); |
| | | dcCell.setCellValue(v); |
| | | copied++; |
| | |
| | | return c; |
| | | } |
| | | |
| | | /** |
| | | * 目标格按「缺数留空」清空。 |
| | | * |
| | | * <p>2026-10-12:这里连母版自带的排名/占比公式格一起清(缺数期不显示"上一期残留值 + 全 1 排名 + #DIV/0!"), |
| | | * 与《生成_货运量排名/周转量排名/中口径排名》这些用户日常核对件的空态一致;下期源侧有数时按源侧重写回来。 |
| | | * 文字标签格不动。 |
| | | */ |
| | | private void clearRankCell(Row dstRow, int col) { |
| | | Cell c = dstRow.getCell(col); |
| | | if (c == null) return; |
| | | if (c.getCellType() != CellType.STRING) c.setBlank(); |
| | | } |
| | | |
| | | /** 源侧是否为文字标签(市州名/指标名)——标签不是缺数,不清空目标格 */ |
| | | private boolean isRankLabel(Cell c) { |
| | | if (c.getCellType() != CellType.STRING) return false; |
| | | String v = c.getStringCellValue(); |
| | | if (v == null) return false; |
| | | String x = v.replace(",", "").trim(); |
| | | if (x.isEmpty()) return false; |
| | | try { |
| | | Double.parseDouble(x); |
| | | return false; |
| | | } catch (NumberFormatException ignore) { |
| | | return true; |
| | | } |
| | | } |
| | | |
| | | /** 公式内所有 A1 引用整体平移:列 +dCol、行 +dRow(SUM/RANK 等函数名后接括号不会被匹配) */ |
| | | private String shiftFormula(String formula, int dCol, int dRow) { |
| | | if (formula == null || (dCol == 0 && dRow == 0)) return formula; |