| | |
| | | @Value("${passenger.template-dir:docs/公路旅客+能耗/输出}") |
| | | private String passengerTemplateDir; |
| | | |
| | | /** 汇总工作簿模板目录(application.yml summary.template-dir) */ |
| | | @Value("${summary.template-dir:docs/生成汇总大表}") |
| | | private String summaryTemplateDir; |
| | | |
| | | /** 货运报表模板目录(application.yml freight.template-dir) */ |
| | | @Value("${freight.template-dir:docs/货运}") |
| | | private String freightTemplateDir; |
| | |
| | | rows = addReadyCount(() -> energyMapper.selectCount(periodQw(period, mode, EnergyVehicleQuarterly::getReportPeriod)), "能耗车辆季度数据", notes); |
| | | auditTypes.add("H204"); |
| | | break; |
| | | case "wycSplit": { |
| | | rows = addReadyCount(() -> cityTaxiMapper.selectCount(periodQw(period, mode, CityTaxiMonthly::getReportPeriod)), "巡游出租月报", notes); |
| | | try { |
| | | Map<String, Object> inp = loadWycMonthlyInput(period, mode); |
| | | if (inp == null) { |
| | | rows = 0; |
| | | notes.add("《网约车订单及全省总量.xlsx》缺 " + period + " 行(需先维护当月订单与全省 pin)"); |
| | | } else { |
| | | double pinK = (Double) inp.get("pinK"), pinCK = (Double) inp.get("pinCK"); |
| | | double pinZ = (Double) inp.get("pinZ"), pinCZ = (Double) inp.get("pinCZ"); |
| | | double orderSum = (Double) inp.get("orderSum"); |
| | | if (pinK <= 0 || pinCK <= 0 || pinZ <= 0 || pinCZ <= 0 || orderSum <= 0) { |
| | | rows = 0; |
| | | notes.add("网约车输入 " + period + " 行不完整(订单/全省 pin 缺失)"); |
| | | } |
| | | } |
| | | } catch (Exception ex) { |
| | | rows = 0; |
| | | notes.add("网约车输入读取失败:" + ex.getMessage()); |
| | | } |
| | | break; |
| | | } |
| | | case "summaryWorkbook": { |
| | | rows = addReadyCount(() -> cityTaxiMapper.selectCount(periodQw(period, mode, CityTaxiMonthly::getReportPeriod)), "巡游出租月报", notes) |
| | | + addReadyCount(() -> cityBusMapper.selectCount(periodQw(period, mode, CityBusMonthly::getReportPeriod)), "城市公交月报", notes); |
| | | if (rows <= 0) notes.add("汇总表依赖的公交/出租月报数据缺失"); |
| | | try { |
| | | if (resolveSummaryMother(period, mode) == null) { |
| | | rows = 0; |
| | | notes.add("docs/生成汇总大表 下缺少可复制的母版汇总表"); |
| | | } |
| | | } catch (Exception ex) { |
| | | rows = 0; |
| | | notes.add("母版定位失败:" + ex.getMessage()); |
| | | } |
| | | break; |
| | | } |
| | | default: |
| | | rows = 0; |
| | | notes.add("未知报表类型:" + t); |
| | |
| | | auditRows += auditRowsOf(period, mode, at); |
| | | audited = audited || auditedMarked(period, mode, at); |
| | | } |
| | | if (auditTypes.isEmpty()) audited = true; |
| | | it.put("rows", rows); |
| | | it.put("ready", rows > 0); |
| | | it.put("auditRows", auditRows); |
| | |
| | | for (String t : types) { |
| | | if (isFreightType(t)) freight = true; |
| | | else if (isPassengerType(t)) passenger = true; |
| | | else if ("cityTaxiDetail".equals(t) || isCityPassengerType(t)) { cityTaxi = true; cityBus = true; } |
| | | else if ("cityTaxiDetail".equals(t) || "wycSplit".equals(t)) { cityTaxi = true; } |
| | | else if (isCityPassengerType(t) || "summaryWorkbook".equals(t)) { cityTaxi = true; cityBus = true; } |
| | | else if (isInvestType(t)) invest = true; |
| | | else if ("energySummary".equals(t)) energy = true; |
| | | } |
| | |
| | | List<Integer> missing = new ArrayList<>(); |
| | | List<Integer> coverage = new ArrayList<>(); |
| | | if (Boolean.FALSE.equals(it.get("audited"))) problems.add("该报表期尚未审核通过"); |
| | | if (isFreightType(t)) { |
| | | if ("wycSplit".equals(t)) { |
| | | coverage.addAll(taxiCover); |
| | | missing = missingMonths(taxiCover, month); |
| | | try { |
| | | Map<String, Object> inp = loadWycMonthlyInput(period, mode); |
| | | if (inp == null) { |
| | | problems.add("缺网约车输入:请在《网约车订单及全省总量.xlsx》补 " + period + " 行"); |
| | | } else { |
| | | double pinK = (Double) inp.get("pinK"), pinCK = (Double) inp.get("pinCK"); |
| | | double pinZ = (Double) inp.get("pinZ"), pinCZ = (Double) inp.get("pinCZ"); |
| | | double orderSum = (Double) inp.get("orderSum"); |
| | | if (pinK <= 0 || pinCK <= 0 || pinZ <= 0 || pinCZ <= 0 || orderSum <= 0) { |
| | | problems.add("网约车输入 " + period + " 行不完整(订单/全省 pin 缺失)"); |
| | | } else { |
| | | addSum(summary, "全省网约车客运量", pinK, "万人"); |
| | | addSum(summary, "其中城市内客运量", pinCK, "万人"); |
| | | addSum(summary, "全省网约车周转量", pinZ, "万人公里"); |
| | | addSum(summary, "其中城市内周转量", pinCZ, "万人公里"); |
| | | addSum(summary, "部级订单合计", orderSum, "单"); |
| | | } |
| | | } |
| | | } catch (Exception ex) { |
| | | problems.add("网约车输入读取失败:" + ex.getMessage()); |
| | | } |
| | | } else if ("summaryWorkbook".equals(t)) { |
| | | java.util.Set<Integer> cov = new java.util.TreeSet<>(); |
| | | cov.addAll(busCover); |
| | | cov.addAll(taxiCover); |
| | | coverage.addAll(cov); |
| | | missing = missingMonths(cov, month); |
| | | try { |
| | | 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], "万人"); |
| | | } |
| | | } catch (Exception ex) { |
| | | problems.add(ex.getMessage()); |
| | | } |
| | | } else if (isFreightType(t)) { |
| | | coverage.addAll(freightCover); |
| | | missing = missingMonths(freightCover, month); |
| | | freightPreview(period, month, summary, problems); |
| | |
| | | return org.apache.poi.ss.util.CellReference.convertNumToColString(colIdx); |
| | | } |
| | | |
| | | /** 旅客分市州月值(0=客运量/1=周转量/2=个体客运量/3=个体周转量) */ |
| | | /** 旅客分市州月值(0=客运量/1=周转量/2=个体客运量/3=个体周转量;0/1=企业H2031+个体,2026-09-03 口径确认) */ |
| | | private double passengerMonthVal(Map<Integer, Map<String, PassengerAgg>> data, Map<Integer, Map<String, double[]>> indi, |
| | | int year, int month, String region, int metricIdx) { |
| | | if (metricIdx <= 1) { |
| | | if (metricIdx == 0 || metricIdx == 1) { // 客运量/周转量总行:企业+个体 |
| | | PassengerAgg agg = aggOf(data, year, month, region); |
| | | Double val = agg == null ? null |
| | | double ent = agg == null ? 0.0 |
| | | : (metricIdx == 0 ? agg.passengerTotal / 10000.0 : agg.turnoverTotal / 10000.0); |
| | | return val == null ? 0.0 : val; |
| | | double ind = individualVal(indi, year, month, region, metricIdx == 0 ? 0 : 1); |
| | | return ent + ind; |
| | | } |
| | | return individualVal(indi, year, month, region, metricIdx - 2); |
| | | } |
| | |
| | | |
| | | /** |
| | | * 中口径分类月度数据:年*100+月 -> (市州/全省 -> double[5][2]) |
| | | * 维度 0=总量 1=公路班线(h2031) 2=城际城乡公交(cityBus) 3=巡游出租 4=网约车;[客运量(万人), 周转量(万人公里)] |
| | | * 维度 0=总量 1=公路班线(h2031+个体) 2=城际城乡公交(cityBus) 3=巡游出租 4=网约车;[客运量(万人), 周转量(万人公里)] |
| | | */ |
| | | private Map<Integer, Map<String, double[][]>> loadMidClassMap() { |
| | | Map<Integer, Map<String, double[][]>> mid = new HashMap<>(); |
| | |
| | | + nz(e.getPassengerClass4()) + nz(e.getPassengerCharter())) / 10000.0; |
| | | double turn = (nz(e.getTurnoverClass1()) + nz(e.getTurnoverClass2()) + nz(e.getTurnoverClass3()) |
| | | + nz(e.getTurnoverClass4()) + nz(e.getTurnoverCharter())) / 10000.0; |
| | | addMidClass(mid, key, city, 1, pass, turn); |
| | | addMidClass(mid, key, "湖北省", 1, pass, turn); |
| | | } |
| | | for (PassengerIndividualMonthly e : passengerIndividualMapper.selectList(null)) { // 个体并入公路班线/总量(09-03 口径) |
| | | if (e.getReportPeriod() == null) continue; |
| | | String[] parts = e.getReportPeriod().split("-"); |
| | | if (parts.length != 2) continue; |
| | | int y, m; |
| | | try { |
| | | y = Integer.parseInt(parts[0]); |
| | | m = Integer.parseInt(parts[1]); |
| | | } catch (NumberFormatException ex) { |
| | | continue; |
| | | } |
| | | String city = RegionUtil.cityByCode(e.getRegionCode()); |
| | | if (city == null) continue; |
| | | int key = y * 100 + m; |
| | | double pass = nz(e.getPassengerCount()) / 10000.0; |
| | | double turn = nz(e.getTurnover()) / 10000.0; |
| | | addMidClass(mid, key, city, 1, pass, turn); |
| | | addMidClass(mid, key, "湖北省", 1, pass, turn); |
| | | } |
| | |
| | | return bos.toByteArray(); |
| | | } |
| | | |
| | | /** |
| | | * 生成_道路运输量汇总表.xlsx(P0-P1 骨架):整本复制母版《YYYY年M月道路运输量汇总表.xlsx》, |
| | | * 保留全部页签/版式/公式/2025 年缓存值,仅把表头年份动态化为目标年;数据填充按 P2 起逐步接入。 |
| | | */ |
| | | public byte[] exportSummaryWorkbook(String period, String mode) 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/生成汇总大表)"); |
| | | } |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | File donor = resolveSummaryDonor(year, month); |
| | | if (donor != null) { |
| | | try (InputStream din = new FileInputStream(donor); XSSFWorkbook dwb = new XSSFWorkbook(din)) { |
| | | int restored = restoreSummaryMonthlyBlocks(wb, dwb); |
| | | log.info("汇总工作簿月度块还原完成,补齐/覆盖单元格数={}(母版:{},备份母版:{})", restored, template.getName(), donor.getName()); |
| | | } |
| | | } else { |
| | | log.warn("汇总工作簿未找到同期备份母版(_备份_{}年{}月…),输出将保持母版现状(可能为 1-4 月骨架)", year, month); |
| | | } |
| | | dynamicSummaryYear(wb, year); |
| | | applyTwoDecimalFormat(wb); // 汇总大表保留人工母版列宽/版式:不做 autoFit(长公式会把列宽顶爆) |
| | | if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr(); |
| | | wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 还原/改写公式后打开即重算 |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | /** 汇总表母版定位:仅精确匹配 {year}年{month}月…(P0-P1 骨架期不允许跨月/跨年静默回退, |
| | | * 目标期无对应母版时明确报错,月份扩展规则待 P0 母版定稿后再放开) */ |
| | | private File resolveSummaryMother(String period, String mode) throws Exception { |
| | | String year = period != null && period.contains("-") ? period.split("-")[0] : (period == null ? "" : period); |
| | | int month = period != null && period.contains("-") ? monthOf(period, mode) : 12; |
| | | String name = year + "年" + month + "月道路运输量汇总表.xlsx"; |
| | | return resolveAnyTemplate(summaryTemplateDir, name); |
| | | } |
| | | |
| | | // ==================== 网约车客运量拆分表(月度) ==================== |
| | | |
| | | /** 网约车客运量拆分表:输入=部级订单(网约车订单及全省总量.xlsx G-W)+ 全省 pin(B-E)+ 巡游出租月报(DB); |
| | | * 算法与《网约车拆分.xlsx》月页一致:客运量=订单占比×全省总量;城市内=巡游出租城市内占比代理、武汉保持自身其余按剩余分摊; |
| | | * 周转量=客运量×巡游出租平均运距占比分摊全省周转量 pin,城市内周转量同法。 */ |
| | | public byte[] exportWycSplit(String period, String mode) throws Exception { |
| | | if (period == null || !period.contains("-")) { |
| | | throw new RuntimeException("网约车客运量拆分表仅支持按月生成,请选择具体月份"); |
| | | } |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = parseMonth(period); |
| | | Map<String, Object> inp = loadWycMonthlyInput(period, mode); |
| | | if (inp == null) { |
| | | throw new RuntimeException("《网约车订单及全省总量.xlsx》缺少报表期 " + period + " 的数据行,请先维护当月订单与全省 pin"); |
| | | } |
| | | double pinK = (Double) inp.get("pinK"); |
| | | double pinCK = (Double) inp.get("pinCK"); |
| | | double pinZ = (Double) inp.get("pinZ"); |
| | | double pinCZ = (Double) inp.get("pinCZ"); |
| | | double orderSum = (Double) inp.get("orderSum"); |
| | | double[] orderArr = (double[]) inp.get("orders"); |
| | | if (pinK <= 0 || pinCK <= 0 || pinZ <= 0 || pinCZ <= 0 || orderSum <= 0) { |
| | | throw new RuntimeException("网约车输入 " + period + " 行不完整:全省 pin / 订单缺失,请核对《网约车订单及全省总量.xlsx》"); |
| | | } |
| | | List<String> cities = RegionUtil.cityList(); |
| | | int n = cities.size(); |
| | | if (orderArr == null || orderArr.length < n) { |
| | | throw new RuntimeException("网约车订单列数不足(应为 17 市州订单列)"); |
| | | } |
| | | Map<String, double[]> taxi = taxiMonthlyOf(period); |
| | | java.util.List<String> missingTaxi = new java.util.ArrayList<>(); |
| | | double[] Dtax = new double[n]; |
| | | double[] Etax = new double[n]; |
| | | double[] avgD = new double[n]; |
| | | double[] avgDc = new double[n]; |
| | | for (int i = 0; i < n; i++) { |
| | | double[] a = taxi.get(cities.get(i)); |
| | | if (a == null || a[0] <= 0) { |
| | | missingTaxi.add(cities.get(i)); |
| | | continue; |
| | | } |
| | | Dtax[i] = a[0]; |
| | | Etax[i] = a[2]; |
| | | avgD[i] = a[4]; |
| | | avgDc[i] = a[5]; |
| | | } |
| | | if (!missingTaxi.isEmpty()) { |
| | | throw new RuntimeException("巡游出租月报缺少以下市州数据:" + String.join("、", missingTaxi)); |
| | | } |
| | | double[] C = new double[n]; |
| | | double[] H = new double[n]; |
| | | double[] I = new double[n]; |
| | | double[] Gl = new double[n]; |
| | | double[] Hl = new double[n]; |
| | | double[] Il = new double[n]; |
| | | double sumG = 0; |
| | | double sumDl = 0; |
| | | double sumEl = 0; |
| | | for (int i = 0; i < n; i++) { |
| | | C[i] = orderArr[i] / orderSum * pinK; |
| | | double F = Etax[i] / Dtax[i]; |
| | | double G = C[i] * F; |
| | | sumG += G; |
| | | double Dl = C[i] * avgD[i]; |
| | | double El = H[i] * avgDc[i]; |
| | | sumDl += Dl; |
| | | sumEl += El; |
| | | } |
| | | // 城市内:武汉保持自身,其余按 G 剩余占比分摊至全省城市内 pin |
| | | for (int i = 0; i < n; i++) { |
| | | if (i == 0) { |
| | | H[i] = C[i] * (Etax[i] / Dtax[i]); |
| | | } else { |
| | | H[i] = (C[i] * (Etax[i] / Dtax[i])) / (sumG - C[0] * (Etax[0] / Dtax[0])) * (pinCK - C[0] * (Etax[0] / Dtax[0])); |
| | | } |
| | | I[i] = C[i] - H[i]; |
| | | } |
| | | // 周转量:按 客运量×巡游出租平均运距 占比分摊全省周转量 pin |
| | | double whG = C[0] * (Etax[0] / Dtax[0]); |
| | | double whDl = C[0] * avgD[0]; |
| | | double[] El2 = new double[n]; |
| | | double sumEl2 = 0; |
| | | for (int i = 0; i < n; i++) { |
| | | double Dl = C[i] * avgD[i]; |
| | | double El2v = H[i] * avgDc[i]; |
| | | El2[i] = El2v; |
| | | sumEl2 += El2v; |
| | | Gl[i] = Dl / sumDl * pinZ; |
| | | } |
| | | for (int i = 0; i < n; i++) { |
| | | if (i == 0) { |
| | | Hl[i] = Gl[i]; |
| | | } else { |
| | | Hl[i] = El2[i] / (sumEl2 - El2[0]) * (pinCZ - Gl[0]); |
| | | } |
| | | Il[i] = Gl[i] - Hl[i]; |
| | | } |
| | | return writeWycSplitWorkbook(period, cities, orderArr, C, Dtax, Etax, H, I, avgD, avgDc, Gl, Hl, Il, pinK, pinCK, pinZ, pinCZ, orderSum); |
| | | } |
| | | |
| | | private byte[] writeWycSplitWorkbook(String period, List<String> cities, double[] orders, double[] C, double[] Dtax, |
| | | double[] Etax, double[] H, double[] I, double[] avgD, double[] avgDc, |
| | | double[] Gl, double[] Hl, double[] Il, |
| | | double pinK, double pinCK, double pinZ, double pinCZ, double orderSum) throws Exception { |
| | | XSSFWorkbook wb = new XSSFWorkbook(); |
| | | XSSFSheet sh = wb.createSheet(period + "拆分"); |
| | | int n = cities.size(); |
| | | // 客运量块 |
| | | Row t = sh.createRow(0); |
| | | t.createCell(0).setCellValue("网约车客运量拆分(万人) 报表期:" + period); |
| | | String[] head1 = {"市州", "订单总数(单)", "网约车客运量(万人)", "巡游出租客运量(万人)", "巡游出租城市内客运量(万人)", "城市内占比", "网约车城市内客运量(万人)", "网约车城际城乡客运量(万人)"}; |
| | | Row h1 = sh.createRow(1); |
| | | for (int j = 0; j < head1.length; j++) h1.createCell(j).setCellValue(head1[j]); |
| | | int r = 2; |
| | | writeWycRow(sh, r++, "全省", orderSum, pinK, sum(Dtax), sum(Etax), pinK > 0 ? pinCK / pinK : 0, pinCK, pinK - pinCK); |
| | | for (int i = 0; i < n; i++) { |
| | | writeWycRow(sh, r++, cities.get(i), orders[i], C[i], Dtax[i], Etax[i], Dtax[i] > 0 ? Etax[i] / Dtax[i] : 0, H[i], I[i]); |
| | | } |
| | | r += 1; |
| | | Row t2 = sh.createRow(r++); |
| | | t2.createCell(0).setCellValue("旅客周转量拆分(万人公里)"); |
| | | String[] head2 = {"市州", "出租车平均运距(公里)", "出租车城市内平均运距(公里)", "网约车旅客周转量(万人公里)", "网约车城市内旅客周转量(万人公里)", "网约车城际城乡旅客周转量(万人公里)"}; |
| | | Row h2 = sh.createRow(r++); |
| | | for (int j = 0; j < head2.length; j++) h2.createCell(j).setCellValue(head2[j]); |
| | | writeWycRow2(sh, r++, "全省", 0, 0, pinZ, pinCZ, pinZ - pinCZ); |
| | | for (int i = 0; i < n; i++) { |
| | | writeWycRow2(sh, r++, cities.get(i), avgD[i], avgDc[i], Gl[i], Hl[i], Il[i]); |
| | | } |
| | | r += 1; |
| | | String[] notes = { |
| | | "注:1) 客运量 C = 订单占比 × 全省网约车客运量;城市内 H:武汉=自身,其余按 G 剩余占比分摊至全省城市内 pin;城际城乡 = 总量 - 城市内。", |
| | | " 2) 周转量 G = 客运量 × 巡游出租平均运距 的占比 × 全省网约车周转量 pin;城市内周转量同法(用城市内平均运距)。", |
| | | " 3) 输入:订单 = 当月分析报告附件1(17 市州订单数);全省 pin = 网约车总数.xlsx;巡游出租 = 巡游出租月报(系统导入,含平均运距)。", |
| | | " 4) 武汉无城际城乡;数值保留全精度,界面/文件显示两位小数。" |
| | | }; |
| | | for (String nt : notes) { |
| | | Row nr = sh.createRow(r++); |
| | | nr.createCell(0).setCellValue(nt); |
| | | } |
| | | for (int j = 0; j < 8; j++) sh.setColumnWidth(j, 18 * 256); |
| | | return toBytes(wb); |
| | | } |
| | | |
| | | private void writeWycRow(Sheet sh, int r, String name, double ord, double k, double dt, double et, double f, double h, double i) { |
| | | Row row = sh.createRow(r); |
| | | row.createCell(0).setCellValue(name); |
| | | row.createCell(1).setCellValue(ord); |
| | | row.createCell(2).setCellValue(k); |
| | | row.createCell(3).setCellValue(dt); |
| | | row.createCell(4).setCellValue(et); |
| | | row.createCell(5).setCellValue(f); |
| | | row.createCell(6).setCellValue(h); |
| | | row.createCell(7).setCellValue(i); |
| | | } |
| | | |
| | | private void writeWycRow2(Sheet sh, int r, String name, double ad, double adc, double g, double h, double i) { |
| | | Row row = sh.createRow(r); |
| | | row.createCell(0).setCellValue(name); |
| | | row.createCell(1).setCellValue(ad); |
| | | row.createCell(2).setCellValue(adc); |
| | | row.createCell(3).setCellValue(g); |
| | | row.createCell(4).setCellValue(h); |
| | | row.createCell(5).setCellValue(i); |
| | | } |
| | | |
| | | private double sum(double[] a) { |
| | | double s = 0; |
| | | for (double v : a) s += v; |
| | | return s; |
| | | } |
| | | |
| | | /** 读取 docs/城市客运/网约车订单及全省总量.xlsx:按 period(yyyy-MM) 行取 pin(B-E) 与 17 市州订单(G-W) */ |
| | | private Map<String, Object> loadWycMonthlyInput(String period, String mode) throws Exception { |
| | | String target = period; |
| | | File f; |
| | | try { |
| | | f = resolveAnyTemplate(templateDir, "网约车订单及全省总量.xlsx"); |
| | | } catch (Exception e) { |
| | | throw new RuntimeException("未找到《网约车订单及全省总量.xlsx》,请放到 docs/城市客运 目录"); |
| | | } |
| | | try (InputStream in = new FileInputStream(f); |
| | | org.apache.poi.ss.usermodel.Workbook wb = org.apache.poi.ss.usermodel.WorkbookFactory.create(in)) { |
| | | org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(0); |
| | | org.apache.poi.ss.usermodel.DataFormatter df = new org.apache.poi.ss.usermodel.DataFormatter(); |
| | | List<String> cities = RegionUtil.cityList(); |
| | | int n = cities.size(); |
| | | for (int i = 1; i <= sh.getLastRowNum(); i++) { |
| | | Row row = sh.getRow(i); |
| | | if (row == null) continue; |
| | | Cell c0 = row.getCell(0); |
| | | if (c0 == null) continue; |
| | | String m = df.formatCellValue(c0).trim(); |
| | | if (m == null || m.isEmpty()) continue; |
| | | if (m.length() > 7) m = m.substring(0, 7); |
| | | if (!target.equals(m)) continue; |
| | | Map<String, Object> map = new HashMap<>(); |
| | | map.put("pinK", cellNum(row, 1, df, 0.0)); |
| | | map.put("pinCK", cellNum(row, 2, df, 0.0)); |
| | | map.put("pinZ", cellNum(row, 3, df, 0.0)); |
| | | map.put("pinCZ", cellNum(row, 4, df, 0.0)); |
| | | double[] orders = new double[n]; |
| | | double orderSum = 0; |
| | | for (int k = 0; k < n; k++) { |
| | | orders[k] = cellNum(row, 6 + k, df, 0.0); |
| | | orderSum += orders[k]; |
| | | } |
| | | if (orderSum <= 0) { |
| | | orderSum = cellNum(row, 5, df, 0.0); // F 列合计兜底 |
| | | } |
| | | map.put("orders", orders); |
| | | map.put("orderSum", orderSum); |
| | | return map; |
| | | } |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private double cellNum(Row row, int idx, org.apache.poi.ss.usermodel.DataFormatter df, double def) { |
| | | Cell c = row.getCell(idx); |
| | | if (c == null) return def; |
| | | if (c.getCellType() == CellType.NUMERIC) return c.getNumericCellValue(); |
| | | String s = df.formatCellValue(c).replace(",", "").trim(); |
| | | if (s == null || s.isEmpty()) return def; |
| | | try { |
| | | return Double.parseDouble(s); |
| | | } catch (Exception e) { |
| | | return def; |
| | | } |
| | | } |
| | | |
| | | /** 巡游出租月报(DB)按月加载:市州 -> [客运量, 周转量, 城市内客运量, 城市内周转量, 平均运距, 城市内平均运距] */ |
| | | private Map<String, double[]> taxiMonthlyOf(String period) { |
| | | Map<String, double[]> map = new java.util.LinkedHashMap<>(); |
| | | List<CityTaxiMonthly> rows = cityTaxiMapper.selectList( |
| | | new LambdaQueryWrapper<CityTaxiMonthly>().eq(CityTaxiMonthly::getReportPeriod, period)); |
| | | for (CityTaxiMonthly r : rows) { |
| | | String city = RegionUtil.normalizeCityName(r.getCity()); |
| | | if (city == null || city.isEmpty()) city = "未知"; |
| | | double[] a = map.computeIfAbsent(city, k -> new double[6]); |
| | | a[0] += nz(r.getPassengerVolume()); |
| | | a[1] += nz(r.getTurnover()); |
| | | a[2] += nz(r.getPassengerCity()); |
| | | a[3] += nz(r.getTurnoverCity()); |
| | | if (r.getAvgDistance() != null && r.getAvgDistance() > 0) a[4] = r.getAvgDistance(); |
| | | if (r.getAvgDistanceCity() != null && r.getAvgDistanceCity() > 0) a[5] = r.getAvgDistanceCity(); |
| | | } |
| | | for (double[] a : map.values()) { |
| | | if (a[4] <= 0 && a[0] > 0) a[4] = round(a[1] / a[0], 2); |
| | | if (a[5] <= 0 && a[2] > 0) a[5] = round(a[3] / a[2], 2); |
| | | } |
| | | return map; |
| | | } |
| | | |
| | | /** 定位同期备份母版(清理前的完整月母版),用于把骨架母版缺失的 5-7 月月度块按备份还原; |
| | | * 与 resolveAnyTemplate 相同策略:在配置目录、user.dir 相对目录、上级目录三个候选根中扫描 */ |
| | | private File resolveSummaryDonor(int year, int month) { |
| | | String rel = summaryTemplateDir; |
| | | while (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 dir = new File(root); |
| | | File[] files = dir.listFiles((d, name) -> |
| | | name.startsWith("_备份_") && name.contains(year + "年" + month + "月") && name.contains("道路运输量汇总表")); |
| | | if (files != null && files.length > 0) return files[0]; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** 按同期备份母版还原月度块:凡备份有内容而母版同格缺失/内容不同(清理时被裁掉的 5-7 月值与累计公式)的单元格,整格回填并覆盖 */ |
| | | private int restoreSummaryMonthlyBlocks(XSSFWorkbook wb, XSSFWorkbook donor) { |
| | | int restored = 0; |
| | | for (int i = 0; i < donor.getNumberOfSheets(); i++) { |
| | | Sheet ds = donor.getSheetAt(i); |
| | | if (ds == null) continue; |
| | | Sheet bs = wb.getSheet(ds.getSheetName()); |
| | | if (bs == null) continue; |
| | | 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; |
| | | CellType t = dc.getCellType(); |
| | | if (t == CellType.BLANK) continue; |
| | | String dtx = summaryCellText(dc); |
| | | if (dtx == null || dtx.isEmpty()) continue; |
| | | Cell bc = (br == null) ? null : br.getCell(dc.getColumnIndex()); |
| | | if (bc != null && dtx.equals(summaryCellText(bc))) continue; |
| | | Row tr = (bc != null) ? br : bs.getRow(r); |
| | | if (tr == null) tr = bs.createRow(r); |
| | | Cell tc = (bc != null) ? bc : tr.getCell(dc.getColumnIndex()); |
| | | if (tc == null) tc = tr.createCell(dc.getColumnIndex()); |
| | | tc.setBlank(); |
| | | if (t == CellType.FORMULA) { |
| | | String f = dc.getCellFormula(); |
| | | if (f != null) tc.setCellFormula(f); |
| | | } else if (t == CellType.STRING) { |
| | | tc.setCellValue(dc.getStringCellValue()); |
| | | } else if (t == CellType.NUMERIC) { |
| | | tc.setCellValue(dc.getNumericCellValue()); |
| | | } else if (t == CellType.BOOLEAN) { |
| | | tc.setCellValue(dc.getBooleanCellValue()); |
| | | } |
| | | restored++; |
| | | } |
| | | } |
| | | } |
| | | return restored; |
| | | } |
| | | |
| | | /** 单元格内容文本化(公式带 = 前缀),用于跨簿比对 */ |
| | | private String summaryCellText(Cell c) { |
| | | if (c == null) return null; |
| | | switch (c.getCellType()) { |
| | | case FORMULA: { |
| | | String f = c.getCellFormula(); |
| | | return f == null ? "" : "=" + f; |
| | | } |
| | | case STRING: |
| | | return c.getStringCellValue(); |
| | | case NUMERIC: |
| | | return Double.toString(c.getNumericCellValue()); |
| | | case BOOLEAN: |
| | | return Boolean.toString(c.getBooleanCellValue()); |
| | | default: |
| | | return ""; |
| | | } |
| | | } |
| | | /** 汇总工作簿年份动态化:仅把 2026年 前缀替换为目标年(2025/2024 参考列不动) */ |
| | | private void dynamicSummaryYear(XSSFWorkbook wb, int year) { |
| | | String oldPrefix = "2026年"; |
| | | String newPrefix = year + "年"; |
| | | if (oldPrefix.equals(newPrefix)) return; |
| | | for (int i = 0; i < wb.getNumberOfSheets(); i++) { |
| | | Sheet sh = wb.getSheetAt(i); |
| | | if (sh == null) continue; |
| | | for (Row row : sh) { |
| | | if (row == null) continue; |
| | | for (Cell c : row) { |
| | | if (c == null || c.getCellType() != CellType.STRING) continue; |
| | | String v = c.getStringCellValue(); |
| | | if (v != null && v.contains(oldPrefix)) { |
| | | c.setCellValue(v.replace(oldPrefix, newPrefix)); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 生成_投资报表系列.zip:预安排 + 十五五 + 亿元 + 经济强县 */ |
| | | public byte[] exportInvestSeries(String period, String mode) throws Exception { |
| | | java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream(); |
| | |
| | | case "passengerMidDetail": return exportPassengerMidDetail(period, mode); |
| | | case "passengerMidRank": return exportPassengerMidRank(period, mode); |
| | | case "passengerMidAnalysis": return exportPassengerMidAnalysis(period, mode); |
| | | case "summaryWorkbook": return exportSummaryWorkbook(period, mode); |
| | | case "energySummary": return exportEnergySummary(period); |
| | | case "investPlan": return investPlan(period, mode); |
| | | case "investFiveYearLogistics": return investFiveYearLogistics(period, mode); |
| | |
| | | case "cityPassengerCitySum": return exportCityPassengerCitySum("生成_城市客运客运量分市州累计.xlsx", period, mode); |
| | | case "cityPassengerDetailSum": return exportCityPassengerCitySum("生成_城市客运各市州明细表.xlsx", period, mode); |
| | | case "cityPassengerSummary": return exportCityPassengerSummary(period, mode); |
| | | case "wycSplit": return exportWycSplit(period, mode); |
| | | default: return null; |
| | | } |
| | | } |
| | |
| | | REPORT_FILE_NAMES.put("cityPassengerCitySum", "生成_城市客运客运量分市州累计.xlsx"); |
| | | REPORT_FILE_NAMES.put("cityPassengerDetailSum", "生成_城市客运各市州明细表.xlsx"); |
| | | REPORT_FILE_NAMES.put("cityPassengerSummary", "生成_城市客运汇总.xlsx"); |
| | | REPORT_FILE_NAMES.put("summaryWorkbook", "生成_道路运输量汇总表.xlsx"); |
| | | REPORT_FILE_NAMES.put("wycSplit", "生成_网约车客运量拆分表.xlsx"); |
| | | } |
| | | |
| | | private void addZipEntry(java.util.zip.ZipOutputStream zos, String name, byte[] data) throws Exception { |
| | |
| | | } |
| | | |
| | | private byte[] toBytesHssf(HSSFWorkbook wb) throws Exception { |
| | | applyTwoDecimalFormat(wb); |
| | | try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { |
| | | wb.write(out); |
| | | wb.close(); |
| | |
| | | private double nz(Double v) { |
| | | return v == null ? 0.0 : v; |
| | | } |
| | | /** 生成表数值列统一样式:保留全精度数值,显示两位小数;整数值与 %/日期/科学计数等既有样式不改变 */ |
| | | private void applyTwoDecimalFormat(org.apache.poi.ss.usermodel.Workbook wb) { |
| | | if (wb == null) return; |
| | | org.apache.poi.ss.usermodel.DataFormat df = wb.createDataFormat(); |
| | | short fmtTwo = df.getFormat("0.00"); |
| | | for (int s = 0; s < wb.getNumberOfSheets(); s++) { |
| | | org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(s); |
| | | if (sh == null) continue; |
| | | for (org.apache.poi.ss.usermodel.Row row : sh) { |
| | | if (row == null) continue; |
| | | for (org.apache.poi.ss.usermodel.Cell cell : row) { |
| | | if (cell == null) continue; |
| | | try { |
| | | CellType ct = cell.getCellType(); |
| | | if (ct != CellType.NUMERIC && ct != CellType.FORMULA) continue; |
| | | if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) continue; |
| | | boolean integral = false; |
| | | if (ct == CellType.NUMERIC) { |
| | | integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue(); |
| | | } else if (cell.getCachedFormulaResultType() == CellType.NUMERIC) { |
| | | integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue(); |
| | | } |
| | | if (integral) continue; // 整数不显示 .00(含公式结果为整数的格) |
| | | org.apache.poi.ss.usermodel.CellStyle cs = cell.getCellStyle(); |
| | | if (cs == null) continue; |
| | | String f = cs.getDataFormatString(); |
| | | if (f == null) f = ""; |
| | | if (f.contains("%") || f.contains("E") || f.contains("0.00")) continue; |
| | | boolean plain = f.isEmpty() || "General".equals(f) || "general".equals(f) |
| | | || "0".equals(f) || "#,##0".equals(f) || "0.0".equals(f) || "#,##0.0".equals(f); |
| | | if (!plain) continue; |
| | | org.apache.poi.ss.usermodel.CellStyle ns = wb.createCellStyle(); |
| | | ns.cloneStyleFrom(cs); |
| | | ns.setDataFormat(fmtTwo); |
| | | cell.setCellStyle(ns); |
| | | } catch (Exception ignore) { |
| | | // 个别单元格格式异常不阻塞导出 |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 汇总表导出后按内容自动加宽列宽(只加宽不缩窄;跳过合并单元格标题,防长标题把单列撑爆) */ |
| | | private void autoFitContentColumns(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); |
| | | } |
| | | } |
| | | } |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 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); |
| | | try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { |
| | | wb.write(out); |
| | | wb.close(); |