| | |
| | | import com.trafficaudit.dataimport.mapper.PassengerEnterpriseMonthlyMapper; |
| | | import com.trafficaudit.dataimport.mapper.PassengerIndividualMonthlyMapper; |
| | | import com.trafficaudit.dataimport.mapper.ScaleSplitTransportMapper; |
| | | import com.trafficaudit.reportexport.calc.MidCalc; |
| | | import com.trafficaudit.reportexport.calc.PaxCalc; |
| | | import com.trafficaudit.reportexport.calc.SummaryWorkbookFiller; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.poi.ss.usermodel.Cell; |
| | | import org.apache.poi.ss.usermodel.CellStyle; |
| | |
| | | import org.apache.poi.hssf.usermodel.HSSFSheet; |
| | | import org.apache.poi.hssf.usermodel.HSSFWorkbook; |
| | | import org.apache.poi.ss.util.CellRangeAddress; |
| | | import org.apache.poi.openxml4j.util.ZipSecureFile; |
| | | import org.openxmlformats.schemas.spreadsheetml.x2006.main.STCellType; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.io.ByteArrayInputStream; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | |
| | | private CityBusMonthlyMapper cityBusMapper; |
| | | @Resource |
| | | private CityTaxiMonthlyMapper cityTaxiMapper; |
| | | @Resource |
| | | private SummaryWorkbookFiller summaryWorkbookFiller; |
| | | @Resource |
| | | private AuditResultMapper auditResultMapper; |
| | | @Resource |
| | |
| | | if (resolveSummaryMother(period, mode) == null) { |
| | | problems.add("docs/生成汇总大表 下缺少可复制的母版汇总表"); |
| | | } else { |
| | | Map<Integer, Map<String, double[]>> busM = loadCityBusByMonth(yearPrefix, month); |
| | | Map<Integer, Map<String, double[]>> taxiM = loadCityTaxiByMonth(yearPrefix, month); |
| | | Map<String, double[]> bc = busM.get(month); |
| | | Map<String, double[]> tc = taxiM.get(month); |
| | | if (bc != null && bc.get("全省") != null) addSum(summary, "城市公交客运量(全省当月)", bc.get("全省")[0], "万人"); |
| | | if (tc != null && tc.get("全省") != null) addSum(summary, "巡游出租客运量(全省当月)", tc.get("全省")[0], "万人"); |
| | | summaryWorkbookPreview(period, yearNum, month, summary, problems); |
| | | } |
| | | } catch (Exception ex) { |
| | | problems.add(ex.getMessage()); |
| | |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** 汇总整本关键指标预览:货运量/周转量、城市客运、中口径的 当月、1..M 累计、当月同比、累计同比(同比取库内去年同期,缺则显示—) */ |
| | | private void summaryWorkbookPreview(String period, int year, int month, |
| | | List<Map<String, Object>> summary, List<String> problems) { |
| | | String yearPrefix = year + "-"; |
| | | // ---- 货运(量=模板_货运量周转量全省行;周转量=规上规下拆分表全省行) ---- |
| | | FreightTurnoverImport ft = loadFreightTurnover(period).get("湖北省"); |
| | | Double fm = ft == null ? null : freightMonth(ft, month); |
| | | Double fcum = ft == null ? null : freightCum(ft, month); |
| | | Double fy = ft == null ? null : freightYoy(ft, month); |
| | | Double fcy = ft == null ? null : freightCumYoy(ft, month); |
| | | addKpi(summary, "全省货运量", fm, fcum, fy, fcy, "万吨"); |
| | | if (fm != null && fy == null) { |
| | | problems.add((year - 1) + "-" + String.format("%02d", month) + " 同期货运量数据未导入库,货运量当月/累计同比暂不显示,待 2025 年定稿数据导入后自动出现"); |
| | | } |
| | | ScaleSplitTransport tm = loadProvinceMonthMap(period).get(month); |
| | | ScaleSplitTransport tc = getProvinceCumulative(period); |
| | | Double tvM = tm == null ? null : tm.getTotalTurnover(); |
| | | Double tvC = tc == null ? null : tc.getTotalTurnover(); |
| | | Double tvMY = tm == null ? null : getYoyMetric(tm, "turnover", "total"); |
| | | Double tvCY = tc == null ? null : getYoyMetric(tc, "turnover", "total"); |
| | | addKpi(summary, "全省货物周转量", tvM, tvC, tvMY, tvCY, "万吨公里"); |
| | | if (fm == null) problems.add("模板_货运量周转量缺 " + period + " 当月数据,货运量仅能提供截至最近导入月的累计"); |
| | | if (tvM == null) problems.add("规上规下拆分缺 " + period + " 当月数据,周转量/规上规下当月缺,累计为截至最近导入月"); |
| | | |
| | | // ---- 城市客运(公交+出租+轨道+轮渡 全省小计;指标位 0/2/4/6=客运,1/3/5/7=周转) ---- |
| | | double[] cumM = cityCumProv(yearPrefix, month); |
| | | double[] cumPrev = cityCumProv(yearPrefix, month - 1); |
| | | double[] monthVec = diff8(cumM, cumPrev); |
| | | double cityPaxM = sumIdx(monthVec, new int[]{0, 2, 4, 6}); |
| | | double cityTurnM = sumIdx(monthVec, new int[]{1, 3, 5, 7}); |
| | | double cityPaxC = sumIdx(cumM, new int[]{0, 2, 4, 6}); |
| | | double cityTurnC = sumIdx(cumM, new int[]{1, 3, 5, 7}); |
| | | double[] lastCumM = cityCumProv((year - 1) + "-", month); |
| | | double[] lastCumPrev = cityCumProv((year - 1) + "-", month - 1); |
| | | double[] lastVec = diff8(lastCumM, lastCumPrev); |
| | | double lastPaxM = sumIdx(lastVec, new int[]{0, 2, 4, 6}); |
| | | double lastPaxC = sumIdx(lastCumM, new int[]{0, 2, 4, 6}); |
| | | double lastTurnM = sumIdx(lastVec, new int[]{1, 3, 5, 7}); |
| | | double lastTurnC = sumIdx(lastCumM, new int[]{1, 3, 5, 7}); |
| | | addKpiRaw(summary, "城市客运量", cityPaxM, cityPaxC, lastPaxM, lastPaxC, "万人次"); |
| | | addKpiRaw(summary, "城市客运周转量", cityTurnM, cityTurnC, lastTurnM, lastTurnC, "万人公里"); |
| | | |
| | | // ---- 中口径(公路旅客企业 H203-1 全省,单位换算 /10000) ---- |
| | | Map<Integer, Map<String, PassengerAgg>> data = loadPassengerAggMap(); |
| | | double paxM = 0.0, paxC = 0.0, turnM = 0.0, turnC = 0.0; |
| | | double lastPaxM2 = 0.0, lastPaxC2 = 0.0, lastTurnM2 = 0.0, lastTurnC2 = 0.0; |
| | | for (int m = 1; m <= month; m++) { |
| | | PassengerAgg agg = aggOf(data, year, m, "湖北省"); |
| | | if (agg != null) { |
| | | double pax = agg.passengerTotal / 10000.0; |
| | | double turn = agg.turnoverTotal / 10000.0; |
| | | if (m == month) { paxM = pax; turnM = turn; } |
| | | paxC += pax; |
| | | turnC += turn; |
| | | } |
| | | PassengerAgg last = aggOf(data, year - 1, m, "湖北省"); |
| | | if (last != null) { |
| | | double lp = last.passengerTotal / 10000.0; |
| | | double lt = last.turnoverTotal / 10000.0; |
| | | if (m == month) { lastPaxM2 = lp; lastTurnM2 = lt; } |
| | | lastPaxC2 += lp; |
| | | lastTurnC2 += lt; |
| | | } |
| | | } |
| | | addKpiRaw(summary, "中口径客运量", paxM, paxC, lastPaxM2, lastPaxC2, "万人次"); |
| | | addKpiRaw(summary, "中口径周转量", turnM, turnC, lastTurnM2, lastTurnC2, "万人公里"); |
| | | |
| | | if (paxM <= 0 && paxC <= 0 && cityPaxC <= 0 && fm == null) { |
| | | problems.add("货运量周转量/规上规下/公路旅客/城市客运当月数据均缺失"); |
| | | } |
| | | if (cityPaxC > 0 && lastPaxC <= 0) { |
| | | problems.add((year - 1) + "-" + String.format("%02d", month) + " 同期城市客运数据未导入库,城市客运同比暂不显示,待 2025 年定稿数据导入后自动出现"); |
| | | } |
| | | if (paxC > 0 && lastPaxC2 <= 0) { |
| | | problems.add((year - 1) + "-" + String.format("%02d", month) + " 同期公路旅客数据未导入库,中口径同比暂不显示,待 2025 年定稿数据导入后自动出现"); |
| | | } |
| | | } |
| | | |
| | | /** 城市客运全省累计向量(8 指标:公交客运/周转、出租客运/周转、轨道客运/周转、轮渡客运/周转) */ |
| | | private double[] cityCumProv(String yearPrefix, int month) { |
| | | if (month <= 0) return new double[8]; |
| | | return loadCityPassengerCumulative(yearPrefix, month).getOrDefault("全省", new double[8]); |
| | | } |
| | | |
| | | private double[] diff8(double[] a, double[] b) { |
| | | double[] out = new double[8]; |
| | | for (int i = 0; i < 8; i++) out[i] = a[i] - b[i]; |
| | | return out; |
| | | } |
| | | |
| | | private double sumIdx(double[] arr, int[] idx) { |
| | | double sum = 0.0; |
| | | for (int i : idx) sum += (i < arr.length ? arr[i] : 0.0); |
| | | return sum; |
| | | } |
| | | |
| | | /** 百分比同比(值已为比率 0.xx):输出四行 当月/累计/当月同比/累计同比 */ |
| | | private void addKpi(List<Map<String, Object>> summary, String name, Double m, Double cum, Double yoyM, Double yoyC, String unit) { |
| | | if (m != null) addSum(summary, name + "·当月", round(m, 2), unit); |
| | | if (cum != null) addSum(summary, name + "·累计", round(cum, 2), unit); |
| | | if (yoyM != null) addSum(summary, name + "·当月同比", round(yoyM * 100.0, 1), "%"); |
| | | if (yoyC != null) addSum(summary, name + "·累计同比", round(yoyC * 100.0, 1), "%"); |
| | | } |
| | | |
| | | /** 原始数值版(月/累计为 0 视为无数据不出行) */ |
| | | private void addKpiRaw(List<Map<String, Object>> summary, String name, double m, double cum, double lastM, double lastC, String unit) { |
| | | if (m > 0) addSum(summary, name + "·当月", round(m, 2), unit); |
| | | if (cum > 0) addSum(summary, name + "·累计", round(cum, 2), unit); |
| | | if (m > 0 && lastM > 0) addSum(summary, name + "·当月同比", round((m - lastM) / lastM * 100.0, 1), "%"); |
| | | if (cum > 0 && lastC > 0) addSum(summary, name + "·累计同比", round((cum - lastC) / lastC * 100.0, 1), "%"); |
| | | } |
| | | |
| | | private void addSum(List<Map<String, Object>> summary, String label, Double value, String unit) { |
| | | Map<String, Object> m = new LinkedHashMap<>(); |
| | | m.put("label", label); |
| | |
| | | Map<String, Double> h2032Cum = loadH2032FreightCumulative(period); |
| | | ScaleSplitTransport provCum = getProvinceCumulative(period); |
| | | Double total = freightCum(ft.get("湖北省"), month); |
| | | Double totalWan = total == null ? null : round(total / 10000.0, 4); |
| | | Double totalWan = total == null ? null : round(total, 4); // 模板列已是万吨,不再 ÷10000 |
| | | double above = 0.0; |
| | | for (Double v : h2032Cum.values()) if (v != null) above += v; |
| | | double aboveWan = round(above / 10000.0, 4); |
| | |
| | | * 保留全部页签/版式/公式/2025 年缓存值,仅把表头年份动态化为目标年;数据填充按 P2 起逐步接入。 |
| | | */ |
| | | public byte[] exportSummaryWorkbook(String period, String mode) throws Exception { |
| | | return exportSummaryWorkbook(period, mode, null); |
| | | } |
| | | |
| | | /** 生成_道路运输量汇总表.xlsx(P0-P1 骨架,支持 toMonth 裁剪): |
| | | * 以《YYYY年M月道路运输量汇总表.xlsx》(M 由 period 决定)为母版,按同期备份母版还原月度块; |
| | | * toMonth 非空且小于 M 时,把 8 个台账/合成页裁到 toMonth 月(toMonth+1..M 的当月值/同比列清空、保留表头, |
| | | * 累计与累计同比公式改为按 toMonth 月重算),用于“先生成 1-6 月、1-7 月扩列”验证。 */ |
| | | public byte[] exportSummaryWorkbook(String period, String mode, Integer toMonth) throws Exception { |
| | | int year = Integer.parseInt(period.contains("-") ? period.split("-")[0] : period); |
| | | int month = period.contains("-") ? monthOf(period, mode) : 12; |
| | | File template = resolveSummaryMother(period, mode); |
| | | if (template == null) { |
| | | throw new RuntimeException("未找到汇总表母版《" + year + "年" + month + "月道路运输量汇总表.xlsx》(docs/生成汇总大表)"); |
| | | } |
| | | double savedZipRatio = ZipSecureFile.getMinInflateRatio(); |
| | | ZipSecureFile.setMinInflateRatio(0.0001); // 新版母版 styles.xml 由 openpyxl 高压缩(0.0099<0.01)触发 POI Zip-bomb 误报,读取后还原 |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | File donor = resolveSummaryDonor(year, month); |
| | |
| | | } else { |
| | | log.warn("汇总工作簿未找到同期备份母版(_备份_{}年{}月…),输出将保持母版现状(可能为 1-4 月骨架)", year, month); |
| | | } |
| | | if (toMonth != null && toMonth > 0 && toMonth < month) { |
| | | int trimmed = trimSummaryMonthlySheets(wb, toMonth); |
| | | log.info("汇总工作簿裁剪至 {} 月完成,清空/改写单元格数={}", toMonth, trimmed); |
| | | } |
| | | dynamicSummaryYear(wb, year); |
| | | applyTwoDecimalFormat(wb); |
| | | autoFitContentColumns(wb); // 汇总表导出后按内容加宽列宽(只加宽不缩窄) |
| | | cleanSummarySheetPresentation(wb); // 取消各页筛选、还原隐藏行(否则“班线包车”等页看起来像缺数据) |
| | | int effMonth = (toMonth != null && toMonth > 0 && toMonth < month) ? toMonth : month; |
| | | List<String> dbProblems = new ArrayList<>(); |
| | | String effPeriod = String.format("%04d-%02d", year, effMonth); |
| | | int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, effPeriod, year, effMonth, dbProblems); |
| | | int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth); |
| | | int monthFormulaFilled = summaryWorkbookFiller.ensureMonthFormulas(wb, year, effMonth); |
| | | int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month"); |
| | | int cumulativeRefreshed = refreshSummaryCumulativeSheets(wb, year, effMonth); // 四张跨页合成页随报表期刷新 |
| | | int widthApplied = applySummaryReferenceLayout(wb); |
| | | log.info("汇总工作簿数据驱动回填完成:写入当月值格数={},补写当月同比公式格数={},补写当月合计公式格数={}," |
| | | + "刷新排名页数值格数={},刷新四页(城市汇总/城市分市州/中口径分析/道路运输周转量)数值格数={}," |
| | | + "按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}", |
| | | dbFilled, yoyFilled, monthFormulaFilled, rankRefreshed, cumulativeRefreshed, widthApplied, dbProblems); |
| | | applyTwoDecimalFormat(wb); // 数值显示两位小数(保留全精度);% / 日期等既有样式不改变 |
| | | if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr(); |
| | | wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 还原/改写公式后打开即重算 |
| | | return toBytes(wb); |
| | | } finally { |
| | | ZipSecureFile.setMinInflateRatio(savedZipRatio); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 版式对齐:按 docs/生成汇总大表/《生成_道路运输量汇总表.xlsx》(用户提供的版式参照)逐列套用列宽与隐藏标记, |
| | | * 使生成件与参照件“格式完全一致”,并让母版没定义宽度的“后续扩列”(如 8 月列)也有与 8 月一致的列宽。 |
| | | * 参照件缺失时静默跳过(返回 0),不影响原有导出。 |
| | | */ |
| | | private int applySummaryReferenceLayout(XSSFWorkbook wb) { |
| | | File ref; |
| | | try { |
| | | ref = resolveSummaryLayoutRef(); |
| | | } catch (Exception e) { |
| | | return 0; |
| | | } |
| | | if (ref == null) return 0; |
| | | double savedZipRatio = ZipSecureFile.getMinInflateRatio(); |
| | | ZipSecureFile.setMinInflateRatio(0.0001); |
| | | int applied = 0; |
| | | try (InputStream in = new FileInputStream(ref); XSSFWorkbook rwb = new XSSFWorkbook(in)) { |
| | | for (int i = 0; i < rwb.getNumberOfSheets(); i++) { |
| | | org.apache.poi.xssf.usermodel.XSSFSheet rs = rwb.getSheetAt(i); |
| | | if (rs == null) continue; |
| | | org.apache.poi.xssf.usermodel.XSSFSheet ts = wb.getSheet(rs.getSheetName()); |
| | | if (ts == null) continue; |
| | | int maxCol = Math.min(usedColumnCount(rs), usedColumnCount(ts) + 4); |
| | | if (maxCol <= 0) continue; |
| | | double[] width = new double[maxCol]; |
| | | boolean[] hasWidth = new boolean[maxCol]; |
| | | boolean[] hidden = new boolean[maxCol]; |
| | | for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCols cols : rs.getCTWorksheet().getColsArray()) { |
| | | for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCol col : cols.getColArray()) { |
| | | long min = col.getMin(); |
| | | long max = col.getMax(); |
| | | for (long c = min; c <= max; c++) { |
| | | int idx = (int) (c - 1); |
| | | if (idx < 0 || idx >= maxCol) { |
| | | if (idx >= maxCol) break; |
| | | continue; |
| | | } |
| | | if (col.isSetWidth()) { |
| | | width[idx] = col.getWidth(); |
| | | hasWidth[idx] = true; |
| | | } |
| | | hidden[idx] = col.isSetHidden() && col.getHidden(); |
| | | } |
| | | } |
| | | } |
| | | for (int c = 0; c < maxCol; c++) { |
| | | if (hasWidth[c]) { |
| | | int w = (int) Math.round(width[c] * 256d); |
| | | if (ts.getColumnWidth(c) != w) { |
| | | ts.setColumnWidth(c, w); |
| | | applied++; |
| | | } |
| | | } |
| | | if (hidden[c] != ts.isColumnHidden(c)) ts.setColumnHidden(c, hidden[c]); |
| | | } |
| | | } |
| | | log.info("汇总工作簿版式对齐《生成_道路运输量汇总表》完成:套用列宽 {} 列(参照件:{})", applied, ref.getName()); |
| | | } catch (Exception e) { |
| | | log.warn("汇总工作簿版式对齐失败(忽略,继续按母版版式输出):{}", e.getMessage()); |
| | | return 0; |
| | | } finally { |
| | | ZipSecureFile.setMinInflateRatio(savedZipRatio); |
| | | } |
| | | return applied; |
| | | } |
| | | |
| | | /** 一页已用到的最大列数(0 基计数) */ |
| | | private int usedColumnCount(org.apache.poi.ss.usermodel.Sheet sh) { |
| | | int max = 0; |
| | | for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) { |
| | | org.apache.poi.ss.usermodel.Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | if (row.getLastCellNum() > max) max = row.getLastCellNum(); |
| | | } |
| | | return max; |
| | | } |
| | | |
| | | // ==================== 汇总工作簿内的排名页刷新 ==================== |
| | | |
| | | /** |
| | | * 汇总工作簿里的「货运量排名 / 货运周转量排名 / 中口径排名」在母版里是上一报表期的静态值, |
| | | * 导出时用独立排名表(《生成_货运量排名》《生成_周转量排名》《生成_中口径排名》,与用户日常核对的口径一致) |
| | | * 的结果刷新数值,避免汇总表里的排名页停留在旧月份。 |
| | | * 数值格搬值、公式格按偏移平移公式后写入;目标格本身已是公式的(SUM/RANK/占比/补数)保留不动,打开时按新数据重算。 |
| | | */ |
| | | private int refreshSummaryRankSheets(XSSFWorkbook wb, String period, String mode) 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), new int[][]{{0, 0}, {0, 10}}); |
| | | return n; |
| | | } |
| | | |
| | | /** |
| | | * 把独立排名表的各个块按标题对齐搬进汇总大表对应页。 |
| | | * srcBlocks[i] = 源块 i 的起始 {行, 列}(0 基);目标块按标题顺序(累计块在前、当月块在后)对应。 |
| | | * 数值格直接搬值;源侧是公式的(排名/占比等)按目标块偏移整体平移后写入公式,打开时按汇总表自身数据重算。 |
| | | */ |
| | | private int copyRankValues(XSSFWorkbook dstWb, String dstSheetName, byte[] srcBytes, int[][] srcBlocks) 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(); |
| | | try (InputStream sin = new ByteArrayInputStream(srcBytes); |
| | | XSSFWorkbook srcWb = new XSSFWorkbook(sin)) { |
| | | XSSFSheet src = srcWb.getSheetAt(0); |
| | | FormulaEvaluator ev = srcWb.getCreationHelper().createFormulaEvaluator(); |
| | | int blocks = Math.min(srcBlocks.length, dstTitles.size()); |
| | | for (int b = 0; b < blocks; b++) { |
| | | 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 h = rankBlockHeight(src, sr0, sc0); |
| | | int w = rankBlockWidth(src, sr0, sc0); |
| | | 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; |
| | | if (r == 0) { |
| | | // 标题行:同步块标题文案。母版标题是 inlineStr,POI 的 setCellValue 只写 <v>、 |
| | | // 不更新 <is>,Excel 会继续显示旧月份,必须先 setBlank 清掉再写。 |
| | | Cell dt = dstRow.getCell(dc); |
| | | if (dt == null) dt = rankCell(dstRow, dc); |
| | | if (sc.getCellType() == CellType.STRING && dt.getCellType() != CellType.FORMULA) { |
| | | String text = sc.getStringCellValue(); |
| | | String old = dt.getCellType() == CellType.STRING ? dt.getStringCellValue() : null; |
| | | if (text != null && !text.equals(old)) { |
| | | dt.setBlank(); |
| | | dt.setCellValue(text); |
| | | } |
| | | } |
| | | continue; |
| | | } |
| | | Cell dcCell = dstRow.getCell(dc); |
| | | if (dcCell != null && dcCell.getCellType() == CellType.FORMULA) continue; // 保留母版自己的公式 |
| | | if (sc.getCellType() == CellType.FORMULA) { |
| | | // 源侧公式(排名/占比):按目标块偏移平移后原样写入,再按汇总表自身数据求值缓存。 |
| | | // 不直接搬 POI 对源表的求值结果(如 RANK 对手工留空的同比一律返回 1)。 |
| | | String f = sc.getCellFormula(); |
| | | if (f == null || f.isEmpty()) continue; |
| | | String shifted = shiftFormula(f, colOff, rowOff); |
| | | if (dcCell == null) dcCell = rankCell(dstRow, dc); |
| | | dcCell.setCellFormula(shifted); |
| | | try { |
| | | dstEv.evaluateFormulaCell(dcCell); |
| | | } catch (Exception ignore) { |
| | | // 求值失败不阻塞导出:打开时由 Excel/WPS 重算 |
| | | } |
| | | copied++; |
| | | continue; |
| | | } |
| | | Double v = rankNumeric(sc, ev); |
| | | if (v == null) continue; |
| | | if (dcCell == null) dcCell = rankCell(dstRow, dc); |
| | | dcCell.setCellValue(v); |
| | | copied++; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return copied; |
| | | } |
| | | |
| | | /** 目标块缺格时新建,并沿用同行左邻格样式(防新格丢边框/百分数格式) */ |
| | | private Cell rankCell(Row row, int col) { |
| | | Cell c = row.createCell(col); |
| | | Cell left = col > 0 ? row.getCell(col - 1) : null; |
| | | if (left != null) c.setCellStyle(left.getCellStyle()); |
| | | return c; |
| | | } |
| | | |
| | | /** 公式内所有 A1 引用整体平移:列 +dCol、行 +dRow(SUM/RANK 等函数名后接括号不会被匹配) */ |
| | | private String shiftFormula(String formula, int dCol, int dRow) { |
| | | if (formula == null || (dCol == 0 && dRow == 0)) return formula; |
| | | java.util.regex.Matcher m = java.util.regex.Pattern |
| | | .compile("(?<![A-Za-z0-9_$])([$]?)([A-Z]{1,3})([$]?)([0-9]+)") |
| | | .matcher(formula); |
| | | StringBuffer sb = new StringBuffer(); |
| | | while (m.find()) { |
| | | String col = m.group(2); |
| | | int idx = 0; |
| | | for (int i = 0; i < col.length(); i++) idx = idx * 26 + (col.charAt(i) - 'A' + 1); |
| | | int nidx = Math.max(1, idx + dCol); |
| | | StringBuilder nc = new StringBuilder(); |
| | | while (nidx > 0) { |
| | | int rem = (nidx - 1) % 26; |
| | | nc.insert(0, (char) ('A' + rem)); |
| | | nidx = (nidx - 1) / 26; |
| | | } |
| | | int nrow = Math.max(1, Integer.parseInt(m.group(4)) + dRow); |
| | | m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(m.group(1) + nc + m.group(3) + nrow)); |
| | | } |
| | | m.appendTail(sb); |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** 排名页里的“块标题”单元格(含“全省分市州”的说明文字),按行、列顺序返回 */ |
| | | private List<XSSFCell> rankTitleCells(Sheet sh) { |
| | | List<XSSFCell> out = new ArrayList<>(); |
| | | for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | for (int c = row.getFirstCellNum(); c < row.getLastCellNum(); c++) { |
| | | Cell cell = row.getCell(c); |
| | | if (cell == null || cell.getCellType() != CellType.STRING) continue; |
| | | String t = cell.getStringCellValue(); |
| | | if (t != null && t.contains("全省分市州")) out.add((XSSFCell) cell); |
| | | } |
| | | } |
| | | return out; |
| | | } |
| | | |
| | | /** 块高度:从起始行向下直到整行为空(在块列范围内) */ |
| | | private int rankBlockHeight(Sheet sh, int r0, int c0) { |
| | | int h = 0; |
| | | for (int r = r0; r <= sh.getLastRowNum(); r++) { |
| | | Row row = sh.getRow(r); |
| | | boolean any = false; |
| | | if (row != null) { |
| | | int last = row.getLastCellNum(); |
| | | for (int c = c0; c < last; c++) { |
| | | if (rankHasContent(row.getCell(c))) { any = true; break; } |
| | | } |
| | | } |
| | | if (!any) break; |
| | | h++; |
| | | } |
| | | return h; |
| | | } |
| | | |
| | | /** 块宽度:从起始列向右直到整列为空(在块行范围内,扫描上限 40 列) */ |
| | | private int rankBlockWidth(Sheet sh, int r0, int c0) { |
| | | int w = 0; |
| | | for (int c = c0; c < c0 + 40; c++) { |
| | | boolean any = false; |
| | | for (int r = r0; r < r0 + 30; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row != null && rankHasContent(row.getCell(c))) { any = true; break; } |
| | | } |
| | | if (!any) break; |
| | | w++; |
| | | } |
| | | return w; |
| | | } |
| | | |
| | | private boolean rankHasContent(Cell c) { |
| | | if (c == null) return false; |
| | | CellType t = c.getCellType(); |
| | | if (t == CellType.BLANK) return false; |
| | | if (t == CellType.STRING) { |
| | | String v = c.getStringCellValue(); |
| | | return v != null && !v.trim().isEmpty(); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** 取单元格数值(公式取求值结果,纯数字文本按数值解析),取不到返回 null */ |
| | | private Double rankNumeric(Cell c, FormulaEvaluator ev) { |
| | | try { |
| | | CellType t = c.getCellType(); |
| | | if (t == CellType.NUMERIC) return c.getNumericCellValue(); |
| | | if (t == CellType.FORMULA) { |
| | | org.apache.poi.ss.usermodel.CellValue cv = ev.evaluate(c); |
| | | return cv != null && cv.getCellType() == CellType.NUMERIC ? cv.getNumberValue() : null; |
| | | } |
| | | if (t == CellType.STRING) { |
| | | String v = c.getStringCellValue(); |
| | | if (v == null) return null; |
| | | String x = v.replace(",", "").trim(); |
| | | if (x.isEmpty()) return null; |
| | | try { return Double.parseDouble(x); } catch (NumberFormatException ignore) { return null; } |
| | | } |
| | | } catch (Exception ignore) { |
| | | return null; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** 定位版式参照件 docs/生成汇总大表/生成_道路运输量汇总表.xlsx(优先级同母版:配置目录 → user.dir 相对 → 上级) */ |
| | | // ==================== 汇总大表:四张“跨页合成”页随报表期刷新 ==================== |
| | | // 城市汇总 / 城市分市州 / 中口径分析 / 道路运输周转量 此前整页沿用母版静态值(标题停在 1-7 月)。 |
| | | // 本段按 2026-09-13 已验证口径(与 8 月母版逐格 0 差异)从母版台账页现算: |
| | | // 城市客运量 = 城市内公交 + 城市内巡游出租 + 城市内网约车 + 轨道 + 轮渡 |
| | | // 道路客运周转量 = 公路班线 + 城际城乡公交 + 城际城乡巡游出租 + 城际城乡网约车 |
| | | // 道路货运周转量 = 货运页 市州 规上货物周转量 + 规下货物周转量 |
| | | // 2026 年 1..N 月取台账页 2026 列(C/E/G…),去年同期取母版缓存 2025 列(V/X/Z…)——库里没有 2025 年城市客运数据。 |
| | | |
| | | /** 城市客运口径指标位(0..17 城市客运,18 货运周转量) */ |
| | | private static final int SM_BUS_PAX = 0, SM_BUS_TURN = 1; |
| | | private static final int SM_TAXI_PAX = 2, SM_TAXI_TURN = 3; |
| | | private static final int SM_WYC_PAX = 4, SM_WYC_TURN = 5; |
| | | private static final int SM_RAIL_PAX = 6, SM_RAIL_TURN = 7; |
| | | private static final int SM_FERRY_PAX = 8, SM_FERRY_TURN = 9; |
| | | private static final int SM_BX_PAX = 10, SM_BX_TURN = 11; |
| | | private static final int SM_BUS_CX_PAX = 12, SM_BUS_CX_TURN = 13; |
| | | private static final int SM_TAXI_CX_PAX = 14, SM_TAXI_CX_TURN = 15; |
| | | 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; |
| | | |
| | | /** |
| | | * 刷新四张跨页合成页:当年累计(1..month 月)、当月(month 月)及对应同比; |
| | | * 2026 与 2025 同期都取自母版台账页缓存,返回写入单元格数。 |
| | | */ |
| | | private int refreshSummaryCumulativeSheets(XSSFWorkbook wb, int year, int month) { |
| | | if (wb == null || month <= 0) return 0; |
| | | 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); |
| | | if (curCum.isEmpty()) { |
| | | log.warn("汇总大表四页刷新:台账页未读到市州数据,跳过(保持母版原值)"); |
| | | return 0; |
| | | } |
| | | double[] pCurCum = smProvinceSum(curCum); |
| | | double[] pLastCum = smProvinceSum(lastCum); |
| | | double[] pCurMon = smProvinceSum(curMon); |
| | | double[] pLastMon = smProvinceSum(lastMon); |
| | | int n = 0; |
| | | n += smWriteCitySummary(wb, year, month, pCurCum, pLastCum, pCurMon, pLastMon); |
| | | n += smWriteCityByRegion(wb, month, curCum, lastCum, curMon, lastMon); |
| | | n += smWriteMidAnalysis(wb, month, pCurCum, pLastCum, pCurMon, pLastMon); |
| | | n += smWriteTurnoverSheets(wb, year, month, curCum, lastCum, curMon, lastMon); |
| | | return n; |
| | | } |
| | | |
| | | /** 17 市州合计(不含台账页“全省”公式行) */ |
| | | private double[] smProvinceSum(Map<String, double[]> m) { |
| | | double[] p = new double[SM_DIM]; |
| | | for (String c : RegionUtil.cityList()) { |
| | | double[] a = m.get(c); |
| | | if (a == null) continue; |
| | | for (int i = 0; i < SM_DIM; i++) p[i] += a[i]; |
| | | } |
| | | return p; |
| | | } |
| | | |
| | | /** 台账页累计取数:市州规范名 -> 19 维指标 */ |
| | | private Map<String, double[]> smReadLedgerMetrics(XSSFWorkbook wb, boolean curYear, 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); |
| | | return out; |
| | | } |
| | | |
| | | /** 公交/出租/网约车页:每市州 4 行(客运量/旅客周转量/其中城市内客运量/其中城市内旅客周转量) */ |
| | | private void smReadCityPaxPage(XSSFWorkbook wb, String sheetName, boolean curYear, int[] months, |
| | | Map<String, double[]> out, int paxIdx, int turnIdx, int cxPaxIdx, int cxTurnIdx) { |
| | | Sheet sh = wb.getSheet(sheetName); |
| | | if (sh == null) return; |
| | | int last = sh.getLastRowNum(); |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String city = smCityOf(row.getCell(0)); |
| | | if (city == null) continue; |
| | | int paxRow = -1, turnRow = -1, cxPaxRow = -1, cxTurnRow = -1; |
| | | for (int k = r; k <= Math.min(last, r + 4); k++) { |
| | | Row kr = sh.getRow(k); |
| | | if (kr == null) continue; |
| | | String b = smText(kr.getCell(1)); |
| | | if (b == null || b.contains("个体")) continue; |
| | | boolean inner = b.contains("城市内"); |
| | | boolean turn = b.contains("周转量"); |
| | | if (inner) { |
| | | if (turn) { if (cxTurnRow < 0) cxTurnRow = k; } |
| | | else if (cxPaxRow < 0) cxPaxRow = k; |
| | | } else { |
| | | if (turn) { if (turnRow < 0) turnRow = k; } |
| | | 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[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]); |
| | | arr[paxIdx] += cityPax; // 城市客运口径取“城市内” |
| | | arr[turnIdx] += cityTurn; |
| | | arr[cxPaxIdx] += totalPax - cityPax; // 城际城乡 = 总量 - 城市内 |
| | | arr[cxTurnIdx] += totalTurn - cityTurn; |
| | | } |
| | | } |
| | | |
| | | /** 轨道、轮渡页:轨道块(武汉/黄石)+ 轮渡块(武汉) */ |
| | | private void smReadRailFerry(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) { |
| | | Sheet sh = wb.getSheet("轨道、轮渡"); |
| | | if (sh == null) return; |
| | | int last = sh.getLastRowNum(); |
| | | int ferryTitle = -1; |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String a = smText(row.getCell(0)); |
| | | if (a != null && a.contains("轮渡客运量")) { ferryTitle = r; break; } |
| | | } |
| | | smReadRailFerryBlock(sh, curYear, 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); |
| | | } |
| | | |
| | | private void smReadRailFerryBlock(Sheet sh, boolean curYear, int[] months, Map<String, double[]> out, |
| | | int start, int end, int baseIdx) { |
| | | for (int r = start; r <= end; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String city = smCityOf(row.getCell(0)); |
| | | if (city == null) continue; |
| | | int paxRow = -1, turnRow = -1; |
| | | for (int k = r; k <= Math.min(end, r + 3); k++) { |
| | | Row kr = sh.getRow(k); |
| | | if (kr == null) continue; |
| | | String b = smText(kr.getCell(1)); |
| | | if (b == null) continue; |
| | | if (b.contains("周转量")) { if (turnRow < 0) turnRow = k; } |
| | | 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); |
| | | } |
| | | } |
| | | |
| | | /** 班线包车页:每市州 客运量 / 旅客周转量(“其中个体”行不重复计入) */ |
| | | private void smReadBanxian(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) { |
| | | Sheet sh = wb.getSheet("班线包车"); |
| | | if (sh == null) return; |
| | | int last = sh.getLastRowNum(); |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String city = smCityOf(row.getCell(0)); |
| | | if (city == null) continue; |
| | | int paxRow = -1, turnRow = -1; |
| | | for (int k = r; k <= Math.min(last, r + 4); k++) { |
| | | Row kr = sh.getRow(k); |
| | | if (kr == null) continue; |
| | | String b = smText(kr.getCell(1)); |
| | | if (b == null || b.contains("个体")) continue; |
| | | if (b.contains("周转量")) { if (turnRow < 0) turnRow = k; } |
| | | 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); |
| | | } |
| | | } |
| | | |
| | | /** 货运页:每市州 规上货物周转量 + 规下货物周转量(“规上+规下”行本身是求和公式,不直接取) */ |
| | | private void smReadFreightTurnover(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) { |
| | | Sheet sh = wb.getSheet(" 货运"); |
| | | if (sh == null) sh = wb.getSheet("货运"); |
| | | if (sh == null) return; |
| | | int last = sh.getLastRowNum(); |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String city = smCityOf(row.getCell(0)); |
| | | if (city == null) continue; |
| | | int above = -1, below = -1; |
| | | for (int k = r; k <= Math.min(last, r + 6); k++) { |
| | | Row kr = sh.getRow(k); |
| | | if (kr == null) continue; |
| | | String b = smText(kr.getCell(1)); |
| | | if (b == null || b.contains("其中")) continue; |
| | | if (b.contains("规上货物周转量")) { if (above < 0) above = k; } |
| | | 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); |
| | | } |
| | | } |
| | | |
| | | private double smSumMonths(Sheet sh, int rowIdx, boolean curYear, int[] months) { |
| | | if (sh == null || rowIdx < 0) 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); |
| | | s += smNum(row.getCell(col)); |
| | | } |
| | | return s; |
| | | } |
| | | |
| | | /** 台账页单元格取数(数字 / 数字文本 / 公式缓存) */ |
| | | private double smNum(Cell c) { |
| | | if (c == null) return 0; |
| | | try { |
| | | CellType t = c.getCellType(); |
| | | if (t == CellType.NUMERIC) return c.getNumericCellValue(); |
| | | if (t == CellType.FORMULA) { |
| | | return c.getCachedFormulaResultType() == CellType.NUMERIC ? c.getNumericCellValue() : 0; |
| | | } |
| | | if (t == CellType.STRING) { |
| | | String s = c.getStringCellValue(); |
| | | if (s == null) return 0; |
| | | s = s.replace(",", "").replace("%", "").trim(); |
| | | if (s.isEmpty()) return 0; |
| | | return Double.parseDouble(s); |
| | | } |
| | | } catch (Exception ignore) { |
| | | return 0; |
| | | } |
| | | return 0; |
| | | } |
| | | |
| | | private String smText(Cell c) { |
| | | if (c == null) return null; |
| | | try { |
| | | return c.getCellType() == CellType.STRING ? c.getStringCellValue() : null; |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** 列 A 文案 -> 规范市州名(“全省/湖北省”与非市州行返回 null) */ |
| | | private String smCityOf(Cell a) { |
| | | String t = smText(a); |
| | | if (t == null || t.trim().isEmpty()) return null; |
| | | String norm = RegionUtil.normalizeCityName(t.trim()); |
| | | if (norm == null) return null; |
| | | return RegionUtil.cityList().contains(norm) ? norm : null; |
| | | } |
| | | |
| | | private double smYoy(double cur, double last) { |
| | | return last == 0.0 ? 0.0 : (cur - last) / last; |
| | | } |
| | | |
| | | /** 写值:公式格不动(保留母版公式,打开即重算);0 且原为空格则不新增 */ |
| | | private int smSetVal(Sheet sh, int r0, int c0, double v) { |
| | | Row row = sh.getRow(r0); |
| | | if (row == null) return 0; |
| | | Cell c = row.getCell(c0); |
| | | if (c != null && c.getCellType() == CellType.FORMULA) return 0; |
| | | if (c == null) { |
| | | if (v == 0.0) return 0; |
| | | c = row.createCell(c0); |
| | | Cell left = row.getCell(c0 - 1); |
| | | if (left != null) c.setCellStyle(left.getCellStyle()); |
| | | } else if (v == 0.0 && c.getCellType() == CellType.BLANK) { |
| | | return 0; |
| | | } |
| | | c.setBlank(); |
| | | c.setCellValue(v); |
| | | return 1; |
| | | } |
| | | |
| | | private void smSetTitle(Sheet sh, int r0, int c0, String text) { |
| | | if (sh == null) return; |
| | | Row row = sh.getRow(r0); |
| | | if (row == null) return; |
| | | Cell c = row.getCell(c0); |
| | | if (c == null || c.getCellType() == CellType.FORMULA) return; |
| | | c.setBlank(); |
| | | c.setCellValue(text); |
| | | } |
| | | |
| | | /** 城市汇总页:块1(1..N 月累计)行 3-9,块2(当月)行 14-20;列 B/C/D/E = 客运量/同比/周转量/同比 */ |
| | | private int smWriteCitySummary(XSSFWorkbook wb, int year, int month, |
| | | double[] pCurCum, double[] pLastCum, double[] pCurMon, double[] pLastMon) { |
| | | Sheet sh = wb.getSheet("城市汇总"); |
| | | if (sh == null) return 0; |
| | | smSetTitle(sh, 0, 0, year + "年1-" + month + "月全省累计完成城市客运量情况"); |
| | | smSetTitle(sh, 11, 0, year + "年" + month + "月全省完成城市客运量情况"); |
| | | int n = 0; |
| | | int[] rows = {2, 3, 4, 5, 6, 7, 8}; |
| | | for (int i = 0; i < rows.length; i++) { |
| | | double[] v = smCitySummaryRow(pCurCum, pLastCum, i); |
| | | n += smSetVal(sh, rows[i], 1, v[0]); |
| | | n += smSetVal(sh, rows[i], 2, v[2]); |
| | | n += smSetVal(sh, rows[i], 3, v[1]); |
| | | n += smSetVal(sh, rows[i], 4, v[3]); |
| | | } |
| | | for (int i = 0; i < rows.length; i++) { |
| | | double[] v = smCitySummaryRow(pCurMon, pLastMon, i); |
| | | n += smSetVal(sh, rows[i] + 11, 1, v[0]); |
| | | n += smSetVal(sh, rows[i] + 11, 2, v[2]); |
| | | n += smSetVal(sh, rows[i] + 11, 3, v[1]); |
| | | n += smSetVal(sh, rows[i] + 11, 4, v[3]); |
| | | } |
| | | return n; |
| | | } |
| | | |
| | | /** i=0 总客运量 1 公交 2 出租车 3 巡游出租 4 网约车 5 轨道 6 轮渡;返回 [客运量, 周转量, 客运量同比, 周转量同比] */ |
| | | private double[] smCitySummaryRow(double[] p, double[] pl, int i) { |
| | | double pax, turn, lPax, lTurn; |
| | | if (i == 0) { |
| | | pax = p[SM_BUS_PAX] + p[SM_TAXI_PAX] + p[SM_WYC_PAX] + p[SM_RAIL_PAX] + p[SM_FERRY_PAX]; |
| | | turn = p[SM_BUS_TURN] + p[SM_TAXI_TURN] + p[SM_WYC_TURN] + p[SM_RAIL_TURN] + p[SM_FERRY_TURN]; |
| | | lPax = pl[SM_BUS_PAX] + pl[SM_TAXI_PAX] + pl[SM_WYC_PAX] + pl[SM_RAIL_PAX] + pl[SM_FERRY_PAX]; |
| | | lTurn = pl[SM_BUS_TURN] + pl[SM_TAXI_TURN] + pl[SM_WYC_TURN] + pl[SM_RAIL_TURN] + pl[SM_FERRY_TURN]; |
| | | } else if (i == 1) { |
| | | pax = p[SM_BUS_PAX]; turn = p[SM_BUS_TURN]; lPax = pl[SM_BUS_PAX]; lTurn = pl[SM_BUS_TURN]; |
| | | } else if (i == 2) { |
| | | pax = p[SM_TAXI_PAX] + p[SM_WYC_PAX]; turn = p[SM_TAXI_TURN] + p[SM_WYC_TURN]; |
| | | lPax = pl[SM_TAXI_PAX] + pl[SM_WYC_PAX]; lTurn = pl[SM_TAXI_TURN] + pl[SM_WYC_TURN]; |
| | | } else if (i == 3) { |
| | | pax = p[SM_TAXI_PAX]; turn = p[SM_TAXI_TURN]; lPax = pl[SM_TAXI_PAX]; lTurn = pl[SM_TAXI_TURN]; |
| | | } else if (i == 4) { |
| | | pax = p[SM_WYC_PAX]; turn = p[SM_WYC_TURN]; lPax = pl[SM_WYC_PAX]; lTurn = pl[SM_WYC_TURN]; |
| | | } else if (i == 5) { |
| | | pax = p[SM_RAIL_PAX]; turn = p[SM_RAIL_TURN]; lPax = pl[SM_RAIL_PAX]; lTurn = pl[SM_RAIL_TURN]; |
| | | } else { |
| | | pax = p[SM_FERRY_PAX]; turn = p[SM_FERRY_TURN]; lPax = pl[SM_FERRY_PAX]; lTurn = pl[SM_FERRY_TURN]; |
| | | } |
| | | return new double[]{pax, turn, smYoy(pax, lPax), smYoy(turn, lTurn)}; |
| | | } |
| | | |
| | | /** 城市分市州页:4 块(累计客运量/累计周转量/当月客运量/当月周转量),每块 全省 1 行 + 17 市州 */ |
| | | private int smWriteCityByRegion(XSSFWorkbook wb, int month, |
| | | Map<String, double[]> curCum, Map<String, double[]> lastCum, |
| | | Map<String, double[]> curMon, Map<String, double[]> lastMon) { |
| | | Sheet sh = wb.getSheet("城市分市州"); |
| | | 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 + "月情况"); |
| | | int n = 0; |
| | | n += smWriteCityByRegionBlock(sh, 4, true, curCum, lastCum); |
| | | n += smWriteCityByRegionBlock(sh, 26, false, curCum, lastCum); |
| | | n += smWriteCityByRegionBlock(sh, 49, true, curMon, lastMon); |
| | | n += smWriteCityByRegionBlock(sh, 71, false, curMon, lastMon); |
| | | return n; |
| | | } |
| | | |
| | | private int smWriteCityByRegionBlock(Sheet sh, int provRow, boolean pax, |
| | | Map<String, double[]> cur, Map<String, double[]> last) { |
| | | int n = 0; |
| | | double[] provCur = smProvinceSum(cur); |
| | | double[] provLast = smProvinceSum(last); |
| | | List<String> cities = RegionUtil.cityList(); |
| | | for (int i = -1; i < cities.size(); i++) { |
| | | int r = provRow + i + 1; |
| | | double[] c = i < 0 ? provCur : cur.get(cities.get(i)); |
| | | double[] l = i < 0 ? provLast : last.get(cities.get(i)); |
| | | if (c == null) c = new double[SM_DIM]; |
| | | if (l == null) l = new double[SM_DIM]; |
| | | double[] g = smCityGroupValues(c, l, pax); |
| | | n += smSetVal(sh, r, 1, g[0]); |
| | | n += smSetVal(sh, r, 4, g[6]); |
| | | n += smSetVal(sh, r, 6, g[1]); |
| | | n += smSetVal(sh, r, 9, g[7]); |
| | | n += smSetVal(sh, r, 11, g[2]); |
| | | n += smSetVal(sh, r, 14, g[8]); |
| | | n += smSetVal(sh, r, 16, g[3]); |
| | | n += smSetVal(sh, r, 19, g[9]); |
| | | n += smSetVal(sh, r, 21, g[4]); |
| | | n += smSetVal(sh, r, 22, g[10]); |
| | | n += smSetVal(sh, r, 23, g[5]); |
| | | n += smSetVal(sh, r, 24, g[11]); |
| | | } |
| | | return n; |
| | | } |
| | | |
| | | /** 6 组值 + 6 组增速:[总量, 公交, 出租, 网约车, 轨道, 轮渡, 总量增速, 公交…, 轮渡] */ |
| | | private double[] smCityGroupValues(double[] c, double[] l, boolean pax) { |
| | | int busI = pax ? SM_BUS_PAX : SM_BUS_TURN; |
| | | int taxiI = pax ? SM_TAXI_PAX : SM_TAXI_TURN; |
| | | int wycI = pax ? SM_WYC_PAX : SM_WYC_TURN; |
| | | int railI = pax ? SM_RAIL_PAX : SM_RAIL_TURN; |
| | | int ferryI = pax ? SM_FERRY_PAX : SM_FERRY_TURN; |
| | | double bus = c[busI], taxi = c[taxiI], wyc = c[wycI], rail = c[railI], ferry = c[ferryI]; |
| | | double lBus = l[busI], lTaxi = l[taxiI], lWyc = l[wycI], lRail = l[railI], lFerry = l[ferryI]; |
| | | double total = bus + taxi + wyc + rail + ferry; |
| | | double lTotal = lBus + lTaxi + lWyc + lRail + lFerry; |
| | | return new double[]{total, bus, taxi, wyc, rail, ferry, |
| | | smYoy(total, lTotal), smYoy(bus, lBus), smYoy(taxi, lTaxi), smYoy(wyc, lWyc), |
| | | smYoy(rail, lRail), smYoy(ferry, lFerry)}; |
| | | } |
| | | |
| | | /** 中口径分析页:块1(累计)行 3-7,块2(当月)行 12-16;列 B/C/F/G = 客运量/同比/周转量/同比 */ |
| | | private int smWriteMidAnalysis(XSSFWorkbook wb, int month, |
| | | double[] pCurCum, double[] pLastCum, double[] pCurMon, double[] pLastMon) { |
| | | Sheet sh = wb.getSheet("中口径分析"); |
| | | if (sh == null) return 0; |
| | | smSetTitle(sh, 0, 0, "1-" + month + "月中口径客运量及周转量"); |
| | | smSetTitle(sh, 9, 0, month + "月中口径客运量及周转量"); |
| | | int n = 0; |
| | | int[] rows = {2, 3, 4, 5, 6}; |
| | | for (int i = 0; i < rows.length; i++) { |
| | | double[] v = smMidRow(pCurCum, pLastCum, i); |
| | | n += smSetVal(sh, rows[i], 1, v[0]); |
| | | n += smSetVal(sh, rows[i], 2, v[2]); |
| | | n += smSetVal(sh, rows[i], 5, v[1]); |
| | | n += smSetVal(sh, rows[i], 6, v[3]); |
| | | } |
| | | for (int i = 0; i < rows.length; i++) { |
| | | double[] v = smMidRow(pCurMon, pLastMon, i); |
| | | n += smSetVal(sh, rows[i] + 9, 1, v[0]); |
| | | n += smSetVal(sh, rows[i] + 9, 2, v[2]); |
| | | n += smSetVal(sh, rows[i] + 9, 5, v[1]); |
| | | n += smSetVal(sh, rows[i] + 9, 6, v[3]); |
| | | } |
| | | return n; |
| | | } |
| | | |
| | | /** i=0 总 1 公路班线 2 城际城乡公交 3 城际城乡巡游出租 4 城际城乡网约车;返回 [客运量, 周转量, 客运量同比, 周转量同比] */ |
| | | private double[] smMidRow(double[] p, double[] pl, int i) { |
| | | int[] paxIdx = {SM_BX_PAX, SM_BX_PAX, SM_BUS_CX_PAX, SM_TAXI_CX_PAX, SM_WYC_CX_PAX}; |
| | | int[] turnIdx = {SM_BX_TURN, SM_BX_TURN, SM_BUS_CX_TURN, SM_TAXI_CX_TURN, SM_WYC_CX_TURN}; |
| | | double pax, turn, lPax, lTurn; |
| | | if (i == 0) { |
| | | pax = p[SM_BX_PAX] + p[SM_BUS_CX_PAX] + p[SM_TAXI_CX_PAX] + p[SM_WYC_CX_PAX]; |
| | | turn = p[SM_BX_TURN] + p[SM_BUS_CX_TURN] + p[SM_TAXI_CX_TURN] + p[SM_WYC_CX_TURN]; |
| | | lPax = pl[SM_BX_PAX] + pl[SM_BUS_CX_PAX] + pl[SM_TAXI_CX_PAX] + pl[SM_WYC_CX_PAX]; |
| | | lTurn = pl[SM_BX_TURN] + pl[SM_BUS_CX_TURN] + pl[SM_TAXI_CX_TURN] + pl[SM_WYC_CX_TURN]; |
| | | } else { |
| | | pax = p[paxIdx[i]]; turn = p[turnIdx[i]]; |
| | | lPax = pl[paxIdx[i]]; lTurn = pl[turnIdx[i]]; |
| | | } |
| | | return new double[]{pax, turn, smYoy(pax, lPax), smYoy(turn, lTurn)}; |
| | | } |
| | | |
| | | /** 道路运输周转量页:块1(累计)行 4-21,块2(当月)行 26-43;只写值列(B/E/V 等公式保留) */ |
| | | private int smWriteTurnoverSheets(XSSFWorkbook wb, int year, int month, |
| | | Map<String, double[]> curCum, Map<String, double[]> lastCum, |
| | | Map<String, double[]> curMon, Map<String, double[]> lastMon) { |
| | | Sheet sh = wb.getSheet("道路运输周转量"); |
| | | if (sh == null) return 0; |
| | | smSetTitle(sh, 0, 0, year + "年1-" + month + "月全省累计完成交通周转量情况"); |
| | | smSetTitle(sh, 22, 0, year + "年" + month + "月全省完成交通周转量情况"); |
| | | int n = 0; |
| | | n += smWriteTurnoverBlock(sh, 3, curCum, lastCum); |
| | | n += smWriteTurnoverBlock(sh, 25, curMon, lastMon); |
| | | return n; |
| | | } |
| | | |
| | | private int smWriteTurnoverBlock(Sheet sh, int provRow, Map<String, double[]> cur, Map<String, double[]> last) { |
| | | int n = 0; |
| | | double[] provCur = smProvinceSum(cur); |
| | | double[] provLast = smProvinceSum(last); |
| | | List<String> cities = RegionUtil.cityList(); |
| | | for (int i = -1; i < cities.size(); i++) { |
| | | int r = provRow + i + 1; |
| | | double[] c = i < 0 ? provCur : cur.get(cities.get(i)); |
| | | double[] l = i < 0 ? provLast : last.get(cities.get(i)); |
| | | if (c == null) c = new double[SM_DIM]; |
| | | if (l == null) l = new double[SM_DIM]; |
| | | double freight = c[SM_FREIGHT_TURN], lFreight = l[SM_FREIGHT_TURN]; |
| | | double pax = c[SM_BUS_PAX] + c[SM_TAXI_PAX] + c[SM_WYC_PAX] + c[SM_RAIL_PAX] + c[SM_FERRY_PAX]; |
| | | double lPax = l[SM_BUS_PAX] + l[SM_TAXI_PAX] + l[SM_WYC_PAX] + l[SM_RAIL_PAX] + l[SM_FERRY_PAX]; |
| | | double road = c[SM_BX_TURN] + c[SM_BUS_CX_TURN] + c[SM_TAXI_CX_TURN] + c[SM_WYC_CX_TURN]; |
| | | double lRoad = l[SM_BX_TURN] + l[SM_BUS_CX_TURN] + l[SM_TAXI_CX_TURN] + l[SM_WYC_CX_TURN]; |
| | | n += smSetVal(sh, r, 6, freight); |
| | | n += smSetVal(sh, r, 9, smYoy(freight, lFreight)); |
| | | n += smSetVal(sh, r, 11, pax); |
| | | n += smSetVal(sh, r, 14, smYoy(pax, lPax)); |
| | | n += smSetVal(sh, r, 16, road); |
| | | n += smSetVal(sh, r, 19, smYoy(road, lRoad)); |
| | | n += smSetVal(sh, r, 22, lFreight); |
| | | n += smSetVal(sh, r, 23, lPax); |
| | | n += smSetVal(sh, r, 24, lRoad); |
| | | } |
| | | return n; |
| | | } |
| | | |
| | | |
| | | private File resolveSummaryLayoutRef() { |
| | | 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 |
| | | }; |
| | | for (String root : roots) { |
| | | if (root == null || root.trim().isEmpty()) continue; |
| | | File f = new File(root, "生成_道路运输量汇总表.xlsx"); |
| | | if (f.isFile()) return f; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** 汇总表母版定位:仅精确匹配 {year}年{month}月…(P0-P1 骨架期不允许跨月/跨年静默回退, |
| | |
| | | if (ds == null) continue; |
| | | Sheet bs = wb.getSheet(ds.getSheetName()); |
| | | if (bs == null) continue; |
| | | // 中口径排名页 A..H 陈旧左块已于 2026-09-09 拍板整块删除:该区域永不从备份母版回填,避免“生成又复活” |
| | | boolean skipMidRankLeftBlock = "中口径排名".equals(ds.getSheetName()); |
| | | for (int r = ds.getFirstRowNum(); r <= ds.getLastRowNum(); r++) { |
| | | Row dr = ds.getRow(r); |
| | | if (dr == null) continue; |
| | | Row br = bs.getRow(r); |
| | | for (Cell dc : dr) { |
| | | if (dc == null) continue; |
| | | if (skipMidRankLeftBlock && dc.getColumnIndex() < 8) continue; |
| | | CellType t = dc.getCellType(); |
| | | if (t == CellType.BLANK) continue; |
| | | String dtx = summaryCellText(dc); |
| | |
| | | Cell tc = (bc != null) ? bc : tr.getCell(dc.getColumnIndex()); |
| | | if (tc == null) tc = tr.createCell(dc.getColumnIndex()); |
| | | tc.setBlank(); |
| | | applyDonorNumberFormat(wb, tc, dc); |
| | | if (t == CellType.FORMULA) { |
| | | String f = dc.getCellFormula(); |
| | | if (f != null) tc.setCellFormula(f); |
| | |
| | | return restored; |
| | | } |
| | | |
| | | /** 还原回填时同步 donor(已做版式统一)的数字格式,保证同比/数值/排名格显示格式与母版一致 */ |
| | | private void applyDonorNumberFormat(XSSFWorkbook wb, Cell dst, Cell src) { |
| | | if (wb == null || dst == null || src == null) return; |
| | | try { |
| | | String fmt = src.getCellStyle().getDataFormatString(); |
| | | if (fmt == null || fmt.isEmpty() || "General".equalsIgnoreCase(fmt)) return; |
| | | org.apache.poi.ss.usermodel.DataFormat df = wb.createDataFormat(); |
| | | short idx = df.getFormat(fmt); |
| | | CellStyle cur = dst.getCellStyle(); |
| | | if (cur != null && idx == cur.getDataFormat()) return; |
| | | CellStyle ns = wb.createCellStyle(); |
| | | if (cur != null) ns.cloneStyleFrom(cur); |
| | | ns.setDataFormat(idx); |
| | | dst.setCellStyle(ns); |
| | | } catch (Exception ignore) { |
| | | // 个别格式异常不阻塞还原 |
| | | } |
| | | } |
| | | /** 单元格内容文本化(公式带 = 前缀),用于跨簿比对 */ |
| | | private String summaryCellText(Cell c) { |
| | | if (c == null) return null; |
| | |
| | | return ""; |
| | | } |
| | | } |
| | | |
| | | /** 把 8 个台账/合成页裁剪到 keepMonths 月: |
| | | * 月度值列 2026 年 m 月 = 第 3+2*(m-1) 列(C/E/G/I/K/M/O),同比列为其后一列; |
| | | * 累计 Q 列 = C+E+...+O,累计同比 R 列分母 = 2025 年同月列 T/V/X/Z/AB/AD/AF。 |
| | | * 裁剪 = 数据行(含累计公式且任一 2026 月度列有内容)中 keepMonths+1..7 月的当月值/同比单元格清空(表头保留), |
| | | * 并把该行累计公式去掉对应 +列,累计同比分母去掉对应 2025 列。 */ |
| | | private int trimSummaryMonthlySheets(XSSFWorkbook wb, int keepMonths) { |
| | | String[] scope = {" 货运", "班线包车", "城市客运", "公交", "出租", "网约车", "轨道、轮渡", "中口径明细"}; |
| | | java.util.Set<String> scopeSet = new java.util.HashSet<>(); |
| | | for (String s : scope) scopeSet.add(s); |
| | | int touched = 0; |
| | | for (int i = 0; i < wb.getNumberOfSheets(); i++) { |
| | | Sheet sh = wb.getSheetAt(i); |
| | | if (sh == null || !scopeSet.contains(sh.getSheetName())) continue; |
| | | for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | Cell q = row.getCell(16); // Q 列 = 累计 |
| | | if (q == null || q.getCellType() != CellType.FORMULA) continue; |
| | | boolean hasMonthly = false; |
| | | for (int m = 1; m <= 7 && !hasMonthly; m++) { |
| | | Cell v = row.getCell(2 + 2 * (m - 1)); // C,E,G,I,K,M,O |
| | | if (v != null && v.getCellType() != CellType.BLANK) hasMonthly = true; |
| | | } |
| | | if (!hasMonthly) continue; // 排除页首/页中跨页签校验公式行(无月度值) |
| | | int excelRow = r + 1; // 公式内引用为 Excel 1-based 行号 |
| | | for (int m = keepMonths + 1; m <= 7; m++) { |
| | | int valueCol = 2 + 2 * (m - 1); // 2026 当月值列 |
| | | int yoyCol = valueCol + 1; // 当月同比列 |
| | | String vLetter = colLetter(valueCol); |
| | | Cell vc = row.getCell(valueCol); |
| | | if (vc != null) { |
| | | vc.setBlank(); |
| | | touched++; |
| | | } |
| | | Cell yc = row.getCell(yoyCol); |
| | | if (yc != null) { |
| | | yc.setBlank(); |
| | | touched++; |
| | | } |
| | | String fq = q.getCellFormula(); |
| | | if (fq != null && fq.contains("+" + vLetter + excelRow)) { |
| | | q.setCellFormula(fq.replace("+" + vLetter + excelRow, "")); |
| | | touched++; |
| | | } |
| | | Cell rc = row.getCell(17); // R 列 = 累计同比 |
| | | if (rc != null && rc.getCellType() == CellType.FORMULA) { |
| | | String fr = rc.getCellFormula(); |
| | | int lastCol = 19 + 2 * (m - 1); // 2025 同月列 T..AF(0-based:T=19,AF=31) |
| | | String lLetter = colLetter(lastCol); |
| | | String txt = fr; |
| | | if (txt != null && txt.contains("," + lLetter + excelRow)) { |
| | | txt = txt.replace("," + lLetter + excelRow, ""); |
| | | touched++; |
| | | } |
| | | if (txt != null && txt.contains("+" + lLetter + excelRow)) { |
| | | txt = txt.replace("+" + lLetter + excelRow, ""); |
| | | touched++; |
| | | } |
| | | if (txt != null && !txt.equals(fr)) rc.setCellFormula(txt); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return touched; |
| | | } |
| | | |
| | | /** 汇总工作簿年份动态化:仅把 2026年 前缀替换为目标年(2025/2024 参考列不动) */ |
| | | private void dynamicSummaryYear(XSSFWorkbook wb, int year) { |
| | | String oldPrefix = "2026年"; |
| | |
| | | } |
| | | |
| | | /** 汇总表导出后按内容自动加宽列宽(只加宽不缩窄;跳过合并单元格标题,防长标题把单列撑爆) */ |
| | | private void autoFitContentColumns(org.apache.poi.ss.usermodel.Workbook wb) { |
| | | /** 汇总大表输出前清理母版残留下来的筛选与行隐藏:筛选箭头/隐藏行会让整页看起来“缺数据” */ |
| | | private void cleanSummarySheetPresentation(org.apache.poi.ss.usermodel.Workbook wb) { |
| | | if (wb == null) return; |
| | | org.apache.poi.ss.usermodel.DataFormatter dfmt = new org.apache.poi.ss.usermodel.DataFormatter(); |
| | | for (int s = 0; s < wb.getNumberOfSheets(); s++) { |
| | | org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(s); |
| | | if (sh == null) continue; |
| | | java.util.Set<String> merged = new java.util.HashSet<>(); |
| | | for (org.apache.poi.ss.util.CellRangeAddress ra : sh.getMergedRegions()) { |
| | | for (int r = ra.getFirstRow(); r <= ra.getLastRow(); r++) { |
| | | for (int c = ra.getFirstColumn(); c <= ra.getLastColumn(); c++) { |
| | | merged.add(r + ":" + c); |
| | | } |
| | | if (sh instanceof org.apache.poi.xssf.usermodel.XSSFSheet) { |
| | | org.apache.poi.xssf.usermodel.XSSFSheet xs = (org.apache.poi.xssf.usermodel.XSSFSheet) sh; |
| | | if (xs.getCTWorksheet().isSetAutoFilter()) xs.getCTWorksheet().unsetAutoFilter(); |
| | | for (org.apache.poi.ss.usermodel.Row row : xs) { |
| | | if (row == null) continue; |
| | | org.apache.poi.xssf.usermodel.XSSFRow xr = (org.apache.poi.xssf.usermodel.XSSFRow) row; |
| | | if (xr.getCTRow().isSetHidden()) xr.getCTRow().unsetHidden(); |
| | | } |
| | | } |
| | | int maxCol = -1; |
| | | for (org.apache.poi.ss.usermodel.Row row : sh) { |
| | | if (row == null) continue; |
| | | maxCol = Math.max(maxCol, (int) row.getLastCellNum() - 1); |
| | | } |
| | | if (maxCol < 0) continue; |
| | | int[] need = new int[maxCol + 1]; |
| | | for (org.apache.poi.ss.usermodel.Row row : sh) { |
| | | if (row == null) continue; |
| | | for (org.apache.poi.ss.usermodel.Cell cell : row) { |
| | | int c = cell.getColumnIndex(); |
| | | if (c > maxCol || merged.contains(row.getRowNum() + ":" + c)) continue; |
| | | String txt; |
| | | try { |
| | | txt = dfmt.formatCellValue(cell); |
| | | } catch (Exception ignore) { |
| | | continue; |
| | | } |
| | | if (txt == null || txt.isEmpty()) continue; |
| | | int units = 0; |
| | | for (int i = 0; i < txt.length(); i++) { |
| | | units += isWideChar(txt.charAt(i)) ? 2 : 1; |
| | | } |
| | | if (units > need[c]) need[c] = Math.min(units, 34); // 长文本不把单列撑爆 |
| | | } |
| | | } |
| | | for (int c = 0; c <= maxCol; c++) { |
| | | if (need[c] <= 0) continue; |
| | | int target = Math.min(need[c] + 2, 46) * 256; |
| | | if (target > sh.getColumnWidth(c)) sh.setColumnWidth(c, target); |
| | | } else { |
| | | sh.setAutoFilter(null); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** CJK / 全角字符按两倍宽度计(近似 Excel 显示列宽) */ |
| | | private boolean isWideChar(char ch) { |
| | | return (ch >= 0x2E80 && ch <= 0x9FFF) || (ch >= 0xF900 && ch <= 0xFAFF) |
| | | || (ch >= 0xFF00 && ch <= 0xFFEF); |
| | | } |
| | | |
| | | private byte[] toBytes(XSSFWorkbook wb) throws Exception { |
| | | applyTwoDecimalFormat(wb); |