| | |
| | | * 生成_中口径排名.xlsx:单块累计(选 1 个月=当月,选 1-6 月=累计),RANK/SUM 公式保留。 |
| | | */ |
| | | public byte[] exportPassengerMidRank(String period, String mode) throws Exception { |
| | | return exportPassengerMidRank(period, mode, null); |
| | | } |
| | | |
| | | /** |
| | | * 方案 A(2026-09-21):中口径排名的累计同比基数与“中口径明细”保持同源。 |
| | | * 明细页的 2025 年列是母版静态定稿值,排名页原先查库现算,两者在 2025-03/04 网约车拆分上不同。 |
| | | * summaryWb 非空时直接复用当前正在生成的汇总工作簿;独立导出时再打开对应母版。 |
| | | */ |
| | | private byte[] exportPassengerMidRank(String period, String mode, XSSFWorkbook summaryWb) throws Exception { |
| | | int maxMonth = monthOf(period, mode); |
| | | int currentYear = Integer.parseInt(period.substring(0, 4)); |
| | | File template = resolvePassengerTemplate("生成_中口径排名.xlsx"); |
| | | Map<String, double[]> lastYearCumBase = loadMidRankStaticBase(period, mode, summaryWb, currentYear, maxMonth); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheetAt(0); |
| | |
| | | title.getCell(10).setCellValue(currentYear + "年" + maxMonth + "月全省分市州完成道路客运生产情况"); |
| | | } |
| | | // 全省行 r3:B3/F3 为 SUM 公式保留;D3/H3 同比填值(0-based 列 3/7) |
| | | setValOrBlank(sheet, 2, 3, midCumYoy(mid, currentYear, "湖北省", maxMonth, 0, true)); |
| | | setValOrBlank(sheet, 2, 7, midCumYoy(mid, currentYear, "湖北省", maxMonth, 0, false)); |
| | | setValOrBlank(sheet, 2, 3, midRankCumYoy(mid, currentYear, "湖北省", maxMonth, true, lastYearCumBase)); |
| | | setValOrBlank(sheet, 2, 7, midRankCumYoy(mid, currentYear, "湖北省", maxMonth, false, lastYearCumBase)); |
| | | // 右块全省行 N3/R3:当月同比(L3/P3 为 SUM 公式保留) |
| | | setValOrBlank(sheet, 2, 13, midMonthYoy(mid, currentYear, maxMonth, "湖北省", 0, true)); |
| | | setValOrBlank(sheet, 2, 17, midMonthYoy(mid, currentYear, maxMonth, "湖北省", 0, false)); |
| | |
| | | int r0 = 3 + i; |
| | | double pass = midCumClass(mid, currentYear, city, maxMonth, 0, true); |
| | | double turn = midCumClass(mid, currentYear, city, maxMonth, 0, false); |
| | | double lastPass = midCumClass(mid, currentYear - 1, city, maxMonth, 0, true); |
| | | double lastTurn = midCumClass(mid, currentYear - 1, city, maxMonth, 0, false); |
| | | setValOrBlank(sheet, r0, 1, pass == 0 ? null : pass); |
| | | setValOrBlank(sheet, r0, 3, midCumYoy(mid, currentYear, city, maxMonth, 0, true)); |
| | | setValOrBlank(sheet, r0, 3, midRankCumYoy(mid, currentYear, city, maxMonth, true, lastYearCumBase)); |
| | | setValOrBlank(sheet, r0, 5, turn == 0 ? null : turn); |
| | | setValOrBlank(sheet, r0, 7, midCumYoy(mid, currentYear, city, maxMonth, 0, false)); |
| | | setValOrBlank(sheet, r0, 7, midRankCumYoy(mid, currentYear, city, maxMonth, false, lastYearCumBase)); |
| | | // 右块当月:L/P 值、N/R 当月同比(M/O/Q/S 排名公式保留) |
| | | double mPass = midClassVal(mid, currentYear, maxMonth, city, 0, true); |
| | | double mTurn = midClassVal(mid, currentYear, maxMonth, city, 0, false); |
| | | double mLastPass = midClassVal(mid, currentYear - 1, maxMonth, city, 0, true); |
| | | double mLastTurn = midClassVal(mid, currentYear - 1, maxMonth, city, 0, false); |
| | | setValOrBlank(sheet, r0, 11, mPass == 0 ? null : mPass); |
| | | setValOrBlank(sheet, r0, 13, mPass == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, city, 0, true)); |
| | | setValOrBlank(sheet, r0, 15, mTurn == 0 ? null : mTurn); |
| | |
| | | recalc(wb); |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | /** 读取母版《中口径明细》的 2025 年逐月静态值,汇总成排名页所需的去年同期累计基数。 */ |
| | | private Map<String, double[]> readMidDetailLastYearCumulative(XSSFWorkbook sourceWb, int currentYear, int maxMonth) { |
| | | Map<String, double[]> out = new LinkedHashMap<>(); |
| | | if (sourceWb == null || maxMonth <= 0) return out; |
| | | Sheet sheet = sourceWb.getSheet("中口径明细"); |
| | | if (sheet == null) return out; |
| | | Map<Integer, Integer> monthCols = new LinkedHashMap<>(); |
| | | java.util.regex.Pattern monthHeader = java.util.regex.Pattern.compile("^(\\d{4})年(\\d{1,2})月$"); |
| | | int headerEnd = Math.min(sheet.getLastRowNum(), sheet.getFirstRowNum() + 5); |
| | | for (int r = sheet.getFirstRowNum(); r <= headerEnd; r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) { |
| | | String text = cellText(row.getCell(c)); |
| | | if (text == null) continue; |
| | | java.util.regex.Matcher matcher = monthHeader.matcher(text.trim()); |
| | | if (!matcher.matches()) continue; |
| | | int y = Integer.parseInt(matcher.group(1)); |
| | | int m = Integer.parseInt(matcher.group(2)); |
| | | if (y == currentYear - 1 && m >= 1 && m <= maxMonth) monthCols.putIfAbsent(m, c); |
| | | } |
| | | } |
| | | if (monthCols.size() < maxMonth) return out; |
| | | for (int r = sheet.getFirstRowNum(); r <= sheet.getLastRowNum(); r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | String city = RegionUtil.normalizeCityName(cellText(row.getCell(0))); |
| | | if (city == null || !RegionUtil.CITY_LIST.contains(city)) continue; |
| | | String metric = cellText(row.getCell(1)); |
| | | if (metric == null) continue; |
| | | int idx = metric.contains("总旅客周转量") ? 1 : (metric.contains("总客运量") ? 0 : -1); |
| | | if (idx < 0) continue; |
| | | double sum = 0.0; |
| | | boolean complete = true; |
| | | for (int m = 1; m <= maxMonth; m++) { |
| | | Cell c = row.getCell(monthCols.get(m)); |
| | | if (c == null) { complete = false; break; } |
| | | if (c.getCellType() == CellType.NUMERIC) { |
| | | sum += c.getNumericCellValue(); |
| | | } else if (c.getCellType() == CellType.FORMULA && c.getCachedFormulaResultType() == CellType.NUMERIC) { |
| | | sum += c.getNumericCellValue(); |
| | | } else { |
| | | complete = false; |
| | | break; |
| | | } |
| | | } |
| | | double[] values = out.computeIfAbsent(city, k -> new double[]{Double.NaN, Double.NaN}); |
| | | values[idx] = complete ? sum : Double.NaN; |
| | | } |
| | | double[] province = new double[2]; |
| | | for (String city : RegionUtil.CITY_LIST) { |
| | | double[] values = out.get(city); |
| | | if (values == null || Double.isNaN(values[0]) || Double.isNaN(values[1])) return new LinkedHashMap<>(); |
| | | province[0] += values[0]; |
| | | province[1] += values[1]; |
| | | } |
| | | out.put("湖北省", province); |
| | | return out; |
| | | } |
| | | |
| | | /** 独立导出时从对应汇总母版读取静态基数;汇总导出时直接复用内存中的工作簿,避免重复 I/O。 */ |
| | | private Map<String, double[]> loadMidRankStaticBase(String period, String mode, XSSFWorkbook summaryWb, |
| | | int currentYear, int maxMonth) { |
| | | if (summaryWb != null) { |
| | | Map<String, double[]> base = readMidDetailLastYearCumulative(summaryWb, currentYear, maxMonth); |
| | | if (!base.isEmpty()) return base; |
| | | log.warn("中口径排名:当前汇总工作簿的中口径明细未读到完整 2025 静态累计基数,回退库内现算"); |
| | | return null; |
| | | } |
| | | double savedZipRatio = ZipSecureFile.getMinInflateRatio(); |
| | | ZipSecureFile.setMinInflateRatio(0.0001); |
| | | try { |
| | | File mother = resolveSummaryMother(period, mode); |
| | | if (mother == null || !mother.isFile()) { |
| | | log.warn("中口径排名:未找到 {} 的汇总母版,2025 累计基数回退库内现算", period); |
| | | return null; |
| | | } |
| | | try (InputStream in = new FileInputStream(mother); XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Map<String, double[]> base = readMidDetailLastYearCumulative(wb, currentYear, maxMonth); |
| | | if (base.isEmpty()) { |
| | | log.warn("中口径排名:母版 {} 的中口径明细未读到完整 2025 静态累计基数,回退库内现算", mother.getName()); |
| | | return null; |
| | | } |
| | | log.info("中口径排名:2025 累计基数改用母版静态列({})", mother.getName()); |
| | | return base; |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("中口径排名:读取母版 2025 静态累计基数失败,回退库内现算:{}", e.getMessage()); |
| | | return null; |
| | | } finally { |
| | | ZipSecureFile.setMinInflateRatio(savedZipRatio); |
| | | } |
| | | } |
| | | |
| | | /** 中口径排名累计同比:方案 A 有静态基数时与其对齐,否则保留原库内现算逻辑。 */ |
| | | private Double midRankCumYoy(Map<Integer, Map<String, double[][]>> mid, int year, String area, |
| | | int monthCount, boolean volume, Map<String, double[]> staticBase) { |
| | | if (staticBase == null) return midCumYoy(mid, year, area, monthCount, 0, volume); |
| | | double[] values = staticBase.get(area); |
| | | if (values == null) return null; |
| | | double current = midCumClass(mid, year, area, monthCount, 0, volume); |
| | | if (current == 0.0) return null; |
| | | double base = volume ? values[0] : values[1]; |
| | | return base == 0.0 ? null : yoyOf(current, base); |
| | | } |
| | | |
| | | /** 写数值或清空(公式单元格保留不动) */ |
| | |
| | | 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}}); |
| | | n += copyRankValues(wb, "中口径排名", exportPassengerMidRank(period, mode, wb), new int[][]{{0, 0}, {0, 10}}); |
| | | return n; |
| | | } |
| | | |