| | |
| | | import com.trafficaudit.reportexport.calc.MidCalc; |
| | | import com.trafficaudit.reportexport.calc.PaxCalc; |
| | | import com.trafficaudit.reportexport.calc.SummaryWorkbookFiller; |
| | | import com.trafficaudit.reportexport.calc.FreightReadinessCheck; |
| | | import com.trafficaudit.reportexport.calc.SummaryMissingDataCheck; |
| | | import com.trafficaudit.reportexport.layout.SummaryLayoutExpander; |
| | | import com.trafficaudit.reportexport.calc.WycSplitCalc; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.poi.ss.usermodel.Cell; |
| | |
| | | private CityTaxiMonthlyMapper cityTaxiMapper; |
| | | @Resource |
| | | private SummaryWorkbookFiller summaryWorkbookFiller; |
| | | @Resource |
| | | private SummaryLayoutExpander summaryLayoutExpander; |
| | | @Resource |
| | | private FreightReadinessCheck freightReadinessCheck; |
| | | @Resource |
| | | private SummaryMissingDataCheck missingDataCheck; |
| | | @Resource |
| | | private WycSplitCalc wycSplitCalc; |
| | | @Resource |
| | |
| | | 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; |
| | |
| | | return exportSummaryWorkbook(period, mode, null, null); |
| | | } |
| | | |
| | | /** |
| | | * 汇总大表「按月扩列」(2026-10-09 Step 1):把 period 当月母版扩列成下一月版式。 |
| | | * 只做版式——当月数据列留空;不落盘,由调用方决定是否确认后另存为下期母版。 |
| | | */ |
| | | public byte[] expandSummaryWorkbook(String period) throws Exception { |
| | | if (period == null || !period.contains("-")) { |
| | | throw new RuntimeException("扩列仅支持按月,请指定 period=YYYY-MM"); |
| | | } |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, "month"); |
| | | if (month >= 12) { |
| | | throw new RuntimeException("扩列:12 月之后需新开一本,跨年规则另行处理"); |
| | | } |
| | | // 2026-10-12:扩列以「当期产物」为底(产物含当期列的值与公式;母版的当期列是空的, |
| | | // 直接拿母版扩列会把当期数据丢成空洞,导致累计漏月)。产物取自导出落盘缓存;缺产物时回退母版并告警。 |
| | | File source = resolveLatestProduct(period); |
| | | boolean fromProduct = source != null; |
| | | if (source == null) { |
| | | source = resolveSummaryMother(period, "month"); |
| | | if (source != null) { |
| | | log.warn("汇总大表扩列:未找到 {} 的产物缓存,回退用母版扩列——历史列里缺少当期数据,累计可能漏月", period); |
| | | } |
| | | } |
| | | if (source == null) { |
| | | throw new RuntimeException("未找到汇总表母版或产物(docs/生成汇总大表):" + year + "年" + month + "月道路运输量汇总表.xlsx"); |
| | | } |
| | | try (InputStream in = new FileInputStream(source)) { |
| | | byte[] out = summaryLayoutExpander.expand(org.apache.poi.util.IOUtils.toByteArray(in), year, month + 1); |
| | | log.info("汇总大表扩列完成:{}{} -> {}年{}月版式(只做版式、当期列留空)", |
| | | fromProduct ? "产物" : "母版", source.getName(), year, month + 1); |
| | | return out; |
| | | } |
| | | } |
| | | |
| | | /** 取该期「最新产物」:导出落盘缓存 _cache/summaryWorkbook_<period>_month_v*.xlsx(按修改时间取最新) */ |
| | | private File resolveLatestProduct(String period) { |
| | | try { |
| | | String rel = summaryTemplateDir; |
| | | while (rel != null && rel.startsWith("./")) rel = rel.substring(2); |
| | | String[] roots = { |
| | | summaryTemplateDir, |
| | | System.getProperty("user.dir") + "/" + rel, |
| | | System.getProperty("user.dir") + "/../" + rel |
| | | }; |
| | | String prefix = "summaryWorkbook_" + period + "_month_"; |
| | | for (String root : roots) { |
| | | if (root == null || root.trim().isEmpty()) continue; |
| | | File dir = new File(root, "_cache"); |
| | | if (!dir.isDirectory()) continue; |
| | | File[] files = dir.listFiles((d, name) -> name.startsWith(prefix) && name.endsWith(".xlsx")); |
| | | if (files == null || files.length == 0) continue; |
| | | File latest = files[0]; |
| | | for (File f : files) { |
| | | if (f.lastModified() > latest.lastModified()) latest = f; |
| | | } |
| | | return latest; |
| | | } |
| | | return null; |
| | | } catch (Exception e) { |
| | | log.warn("汇总大表扩列:查找产物缓存失败(忽略):{}", e.getMessage()); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** 只修正当期母版里 7 个标题页的标题文字(按母版期次重写,用于修复滞后标题)。 */ |
| | | public byte[] fixSummaryTitles(String period) throws Exception { |
| | | if (period == null || !period.contains("-")) { |
| | | throw new RuntimeException("标题修正仅支持按月,请指定 period=YYYY-MM"); |
| | | } |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, "month"); |
| | | File mother = resolveSummaryMother(period, "month"); |
| | | if (mother == null) { |
| | | throw new RuntimeException("未找到汇总表母版(docs/生成汇总大表):" + year + "年" + month + "月道路运输量汇总表.xlsx"); |
| | | } |
| | | try (InputStream in = new FileInputStream(mother)) { |
| | | return summaryLayoutExpander.fixTitles(org.apache.poi.util.IOUtils.toByteArray(in), year, month); |
| | | } |
| | | } |
| | | |
| | | /** 生成_道路运输量汇总表.xlsx(P0-P1 骨架,支持 toMonth 裁剪): |
| | | * 以《YYYY年M月道路运输量汇总表.xlsx》(M 由 period 决定)为母版,按同期备份母版还原月度块; |
| | | * toMonth 非空且小于 M 时,把 8 个台账/合成页裁到 toMonth 月(toMonth+1..M 的当月值/同比列清空、保留表头, |
| | |
| | | int effMonth = (toMonth != null && toMonth > 0 && toMonth < month) ? toMonth : month; |
| | | List<String> dbProblems = new ArrayList<>(); |
| | | String effPeriod = String.format("%04d-%02d", year, effMonth); |
| | | dbProblems.addAll(freightReadinessCheck.check(effPeriod)); // B4:货运口径导出前体检(6 条规则) |
| | | dbProblems.addAll(missingDataCheck.check(effPeriod)); // 缺数清单(该有的数没有) |
| | | reportProgress(progress, 22, "回填当月数据"); |
| | | int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, effPeriod, year, effMonth, dbProblems); |
| | | int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth); |
| | |
| | | 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("汇总工作簿数据驱动回填完成:写入当月值格数={},补写当月同比公式格数={},补写当月合计公式格数={}," |
| | | + "修正累计同比分母格数={},刷新排名页数值格数={},刷新四页(城市汇总/城市分市州/中口径分析/道路运输周转量)数值格数={}," |
| | | + "按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}", |
| | |
| | | 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++; |
| | |
| | | Cell left = col > 0 ? row.getCell(col - 1) : null; |
| | | if (left != null) c.setCellStyle(left.getCellStyle()); |
| | | 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 static final int SM_WYC_CX_PAX = 16, SM_WYC_CX_TURN = 17; |
| | | private static final int SM_FREIGHT_TURN = 18; |
| | | private static final int SM_DIM = 19; |
| | | /** 2026 年 1 月当月值列(0 基,C 列);2025 年 1 月当月值列(0 基,V 列) */ |
| | | private static final int SM_CUR_COL0 = 2; |
| | | private static final int SM_LAST_COL0 = 21; |
| | | // 2026-10-09:原先写死的 SM_CUR_COL0=2(2026年1月=C)/ SM_LAST_COL0=21(2025年1月=V)已废弃, |
| | | // 改为按表头动态定位(见 smMonthCols / smYearMonthOf),兼容「文本 / 日期序列号 / 日期格式」三种表头写法。 |
| | | |
| | | /** |
| | | * 刷新四张跨页合成页:当年累计(1..month 月)、当月(month 月)及对应同比; |
| | |
| | | int[] cumMonths = new int[month]; |
| | | for (int m = 1; m <= month; m++) cumMonths[m - 1] = m; |
| | | int[] oneMonth = new int[]{month}; |
| | | Map<String, double[]> curCum = smReadLedgerMetrics(wb, true, cumMonths); |
| | | Map<String, double[]> lastCum = smReadLedgerMetrics(wb, false, cumMonths); |
| | | Map<String, double[]> curMon = smReadLedgerMetrics(wb, true, oneMonth); |
| | | Map<String, double[]> lastMon = smReadLedgerMetrics(wb, false, oneMonth); |
| | | Map<String, double[]> curCum = smReadLedgerMetrics(wb, year, cumMonths); |
| | | Map<String, double[]> lastCum = smReadLedgerMetrics(wb, year - 1, cumMonths); |
| | | Map<String, double[]> curMon = smReadLedgerMetrics(wb, year, oneMonth); |
| | | Map<String, double[]> lastMon = smReadLedgerMetrics(wb, year - 1, oneMonth); |
| | | if (curCum.isEmpty()) { |
| | | log.warn("汇总大表四页刷新:台账页未读到市州数据,跳过(保持母版原值)"); |
| | | return 0; |
| | |
| | | } |
| | | |
| | | /** 台账页累计取数:市州规范名 -> 19 维指标 */ |
| | | private Map<String, double[]> smReadLedgerMetrics(XSSFWorkbook wb, boolean curYear, int[] months) { |
| | | private Map<String, double[]> smReadLedgerMetrics(XSSFWorkbook wb, int blockYear, int[] months) { |
| | | Map<String, double[]> out = new LinkedHashMap<>(); |
| | | smReadCityPaxPage(wb, "公交", curYear, months, out, SM_BUS_PAX, SM_BUS_TURN, SM_BUS_CX_PAX, SM_BUS_CX_TURN); |
| | | smReadCityPaxPage(wb, "出租", curYear, months, out, SM_TAXI_PAX, SM_TAXI_TURN, SM_TAXI_CX_PAX, SM_TAXI_CX_TURN); |
| | | smReadCityPaxPage(wb, "网约车", curYear, months, out, SM_WYC_PAX, SM_WYC_TURN, SM_WYC_CX_PAX, SM_WYC_CX_TURN); |
| | | smReadRailFerry(wb, curYear, months, out); |
| | | smReadBanxian(wb, curYear, months, out); |
| | | smReadFreightTurnover(wb, curYear, months, out); |
| | | smReadCityPaxPage(wb, "公交", blockYear, months, out, SM_BUS_PAX, SM_BUS_TURN, SM_BUS_CX_PAX, SM_BUS_CX_TURN); |
| | | smReadCityPaxPage(wb, "出租", blockYear, months, out, SM_TAXI_PAX, SM_TAXI_TURN, SM_TAXI_CX_PAX, SM_TAXI_CX_TURN); |
| | | smReadCityPaxPage(wb, "网约车", blockYear, months, out, SM_WYC_PAX, SM_WYC_TURN, SM_WYC_CX_PAX, SM_WYC_CX_TURN); |
| | | smReadRailFerry(wb, blockYear, months, out); |
| | | smReadBanxian(wb, blockYear, months, out); |
| | | smReadFreightTurnover(wb, blockYear, months, out); |
| | | return out; |
| | | } |
| | | |
| | | /** 公交/出租/网约车页:每市州 4 行(客运量/旅客周转量/其中城市内客运量/其中城市内旅客周转量) */ |
| | | private void smReadCityPaxPage(XSSFWorkbook wb, String sheetName, boolean curYear, int[] months, |
| | | private void smReadCityPaxPage(XSSFWorkbook wb, String sheetName, int blockYear, int[] months, |
| | | Map<String, double[]> out, int paxIdx, int turnIdx, int cxPaxIdx, int cxTurnIdx) { |
| | | Sheet sh = wb.getSheet(sheetName); |
| | | if (sh == null) return; |
| | | Map<Integer, Integer> monthCols = smMonthCols(sh, blockYear); |
| | | int last = sh.getLastRowNum(); |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | |
| | | else if (paxRow < 0) paxRow = k; |
| | | } |
| | | } |
| | | double totalPax = smSumMonths(sh, paxRow, curYear, months); |
| | | double totalTurn = smSumMonths(sh, turnRow, curYear, months); |
| | | double cityPax = smSumMonths(sh, cxPaxRow, curYear, months); |
| | | double cityTurn = smSumMonths(sh, cxTurnRow, curYear, months); |
| | | double totalPax = smSumMonths(sh, paxRow, monthCols, months); |
| | | double totalTurn = smSumMonths(sh, turnRow, monthCols, months); |
| | | double cityPax = smSumMonths(sh, cxPaxRow, monthCols, months); |
| | | double cityTurn = smSumMonths(sh, cxTurnRow, monthCols, months); |
| | | double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]); |
| | | arr[paxIdx] += cityPax; // 城市客运口径取“城市内” |
| | | arr[turnIdx] += cityTurn; |
| | |
| | | } |
| | | |
| | | /** 轨道、轮渡页:轨道块(武汉/黄石)+ 轮渡块(武汉) */ |
| | | private void smReadRailFerry(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) { |
| | | private void smReadRailFerry(XSSFWorkbook wb, int blockYear, int[] months, Map<String, double[]> out) { |
| | | Sheet sh = wb.getSheet("轨道、轮渡"); |
| | | if (sh == null) return; |
| | | Map<Integer, Integer> monthCols = smMonthCols(sh, blockYear); |
| | | int last = sh.getLastRowNum(); |
| | | int ferryTitle = -1; |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | |
| | | String a = smText(row.getCell(0)); |
| | | if (a != null && a.contains("轮渡客运量")) { ferryTitle = r; break; } |
| | | } |
| | | smReadRailFerryBlock(sh, curYear, months, out, sh.getFirstRowNum(), |
| | | smReadRailFerryBlock(sh, monthCols, months, out, sh.getFirstRowNum(), |
| | | ferryTitle < 0 ? last : ferryTitle - 1, SM_RAIL_PAX); |
| | | if (ferryTitle >= 0) smReadRailFerryBlock(sh, curYear, months, out, ferryTitle, last, SM_FERRY_PAX); |
| | | if (ferryTitle >= 0) smReadRailFerryBlock(sh, monthCols, months, out, ferryTitle, last, SM_FERRY_PAX); |
| | | } |
| | | |
| | | private void smReadRailFerryBlock(Sheet sh, boolean curYear, int[] months, Map<String, double[]> out, |
| | | private void smReadRailFerryBlock(Sheet sh, Map<Integer, Integer> monthCols, int[] months, Map<String, double[]> out, |
| | | int start, int end, int baseIdx) { |
| | | for (int r = start; r <= end; r++) { |
| | | Row row = sh.getRow(r); |
| | |
| | | else if (paxRow < 0) paxRow = k; |
| | | } |
| | | double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]); |
| | | arr[baseIdx] += smSumMonths(sh, paxRow, curYear, months); |
| | | arr[baseIdx + 1] += smSumMonths(sh, turnRow, curYear, months); |
| | | arr[baseIdx] += smSumMonths(sh, paxRow, monthCols, months); |
| | | arr[baseIdx + 1] += smSumMonths(sh, turnRow, monthCols, months); |
| | | } |
| | | } |
| | | |
| | | /** 班线包车页:每市州 客运量 / 旅客周转量(“其中个体”行不重复计入) */ |
| | | private void smReadBanxian(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) { |
| | | private void smReadBanxian(XSSFWorkbook wb, int blockYear, int[] months, Map<String, double[]> out) { |
| | | Sheet sh = wb.getSheet("班线包车"); |
| | | if (sh == null) return; |
| | | Map<Integer, Integer> monthCols = smMonthCols(sh, blockYear); |
| | | int last = sh.getLastRowNum(); |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | |
| | | else if (b.contains("客运量") && paxRow < 0) paxRow = k; |
| | | } |
| | | double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]); |
| | | arr[SM_BX_PAX] += smSumMonths(sh, paxRow, curYear, months); |
| | | arr[SM_BX_TURN] += smSumMonths(sh, turnRow, curYear, months); |
| | | arr[SM_BX_PAX] += smSumMonths(sh, paxRow, monthCols, months); |
| | | arr[SM_BX_TURN] += smSumMonths(sh, turnRow, monthCols, months); |
| | | } |
| | | } |
| | | |
| | | /** 货运页:每市州 规上货物周转量 + 规下货物周转量(“规上+规下”行本身是求和公式,不直接取) */ |
| | | private void smReadFreightTurnover(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) { |
| | | private void smReadFreightTurnover(XSSFWorkbook wb, int blockYear, int[] months, Map<String, double[]> out) { |
| | | Sheet sh = wb.getSheet(" 货运"); |
| | | if (sh == null) sh = wb.getSheet("货运"); |
| | | if (sh == null) return; |
| | | Map<Integer, Integer> monthCols = smMonthCols(sh, blockYear); |
| | | int last = sh.getLastRowNum(); |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | |
| | | else if (b.contains("规下货物周转量")) { if (below < 0) below = k; } |
| | | } |
| | | double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]); |
| | | arr[SM_FREIGHT_TURN] += smSumMonths(sh, above, curYear, months) + smSumMonths(sh, below, curYear, months); |
| | | arr[SM_FREIGHT_TURN] += smSumMonths(sh, above, monthCols, months) + smSumMonths(sh, below, monthCols, months); |
| | | } |
| | | } |
| | | |
| | | private double smSumMonths(Sheet sh, int rowIdx, boolean curYear, int[] months) { |
| | | if (sh == null || rowIdx < 0) return 0; |
| | | private double smSumMonths(Sheet sh, int rowIdx, Map<Integer, Integer> monthCols, int[] months) { |
| | | if (sh == null || rowIdx < 0 || monthCols == null) return 0; |
| | | Row row = sh.getRow(rowIdx); |
| | | if (row == null) return 0; |
| | | double s = 0; |
| | | for (int m : months) { |
| | | int col = (curYear ? SM_CUR_COL0 : SM_LAST_COL0) + 2 * (m - 1); |
| | | Integer col = monthCols.get(m); |
| | | if (col == null) continue; |
| | | s += smNum(row.getCell(col)); |
| | | } |
| | | return s; |
| | | } |
| | | |
| | | /** 台账页「某年 M 月」的 0 基列号:按表头动态定位(不再写死 C/V),兼容文本 / 日期序列号 / 日期格式三种写法 */ |
| | | private Map<Integer, Integer> smMonthCols(Sheet sh, int wantedYear) { |
| | | Map<Integer, Integer> cols = new LinkedHashMap<>(); |
| | | if (sh == null) return cols; |
| | | int lastRow = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + 4); |
| | | for (int r = sh.getFirstRowNum(); r <= lastRow; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | int lastCol = row.getLastCellNum(); |
| | | for (int c = row.getFirstCellNum(); c >= 0 && c < lastCol; c++) { |
| | | Integer ym = smYearMonthOf(row.getCell(c)); |
| | | if (ym == null || ym / 100 != wantedYear) continue; |
| | | int mo = ym % 100; |
| | | if (!cols.containsKey(mo)) cols.put(mo, c); |
| | | } |
| | | } |
| | | if (cols.isEmpty()) { |
| | | log.warn("汇总大表四页刷新:{} 页未识别到 {} 年的月份表头列(支持文本/序列号/日期三种形式)", sh.getSheetName(), wantedYear); |
| | | } |
| | | return cols; |
| | | } |
| | | |
| | | /** 表头单元格 -> 年*100+月;只认「YYYY年M月」文本或「某月 1 号」的日期序列号,排除累计/同比/环比 */ |
| | | private Integer smYearMonthOf(Cell c) { |
| | | if (c == null) return null; |
| | | try { |
| | | CellType t = c.getCellType(); |
| | | if (t == CellType.STRING) { |
| | | String v = c.getStringCellValue(); |
| | | if (v == null) return null; |
| | | v = v.trim(); |
| | | if (v.isEmpty() || v.contains("累计") || v.contains("同比") || v.contains("环比")) return null; |
| | | java.util.regex.Matcher m = java.util.regex.Pattern |
| | | .compile("^(\\d{4})\\s*年\\s*(\\d{1,2})\\s*月$").matcher(v); |
| | | if (!m.matches()) return null; |
| | | int y = Integer.parseInt(m.group(1)); |
| | | int mo = Integer.parseInt(m.group(2)); |
| | | return mo >= 1 && mo <= 12 ? y * 100 + mo : null; |
| | | } |
| | | if (t == CellType.NUMERIC) { |
| | | double v = c.getNumericCellValue(); |
| | | if (v < 36526 || v > 73415) return null; |
| | | java.util.Calendar cal = java.util.Calendar.getInstance(); |
| | | cal.setTime(org.apache.poi.ss.usermodel.DateUtil.getJavaDate(v)); |
| | | if (cal.get(java.util.Calendar.DAY_OF_MONTH) != 1) return null; |
| | | return cal.get(java.util.Calendar.YEAR) * 100 + (cal.get(java.util.Calendar.MONTH) + 1); |
| | | } |
| | | } catch (Exception ignore) { |
| | | return null; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** 台账页单元格取数(数字 / 数字文本 / 公式缓存) */ |
| | |
| | | if (sh == null) return 0; |
| | | smSetTitle(sh, 0, 0, "城市客运客运量分市州1-" + month + "月累计情况"); |
| | | smSetTitle(sh, 22, 0, "城市客运客运周转量分市州1-" + month + "月累计情况"); |
| | | smSetTitle(sh, 45, 0, "城市客运客运量分市州" + month + "月情况"); |
| | | smSetTitle(sh, 67, 0, "城市客运客运周转量分市州" + month + "月情况"); |
| | | smSetTitle(sh, 44, 0, "城市客运客运量分市州" + month + "月情况"); // 0基44 = Excel 第45行(母版里标题在 A45,A46 是"单位"行) |
| | | smSetTitle(sh, 66, 0, "城市客运客运周转量分市州" + month + "月情况"); // 0基66 = Excel 第67行(同上) |
| | | int n = 0; |
| | | n += smWriteCityByRegionBlock(sh, 4, true, curCum, lastCum); |
| | | n += smWriteCityByRegionBlock(sh, 26, false, curCum, lastCum); |