| | |
| | | package com.trafficaudit.reportexport.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.toolkit.support.SFunction; |
| | | import com.trafficaudit.common.util.RegionUtil; |
| | | import com.trafficaudit.dataimport.entity.EnergyVehicleQuarterly; |
| | | import com.trafficaudit.dataimport.entity.FreightTurnoverImport; |
| | | import com.trafficaudit.dataimport.entity.H2032EnterpriseMonthly; |
| | | import com.trafficaudit.dataimport.entity.InvestmentMonthly; |
| | | import com.trafficaudit.dataimport.entity.CityBusMonthly; |
| | | import com.trafficaudit.dataimport.entity.CityTaxiMonthly; |
| | | import com.trafficaudit.dataimport.entity.InvestmentProject; |
| | | import com.trafficaudit.dataimport.entity.PassengerEnterpriseMonthly; |
| | | import com.trafficaudit.dataimport.entity.PassengerIndividualMonthly; |
| | | import com.trafficaudit.dataimport.entity.ScaleSplitTransport; |
| | | import com.trafficaudit.auditengine.entity.AuditResult; |
| | | import com.trafficaudit.auditengine.entity.AuditRun; |
| | | import com.trafficaudit.auditengine.mapper.AuditResultMapper; |
| | | import com.trafficaudit.auditengine.mapper.AuditRunMapper; |
| | | import com.trafficaudit.rulemanage.entity.AuditRule; |
| | | import com.trafficaudit.rulemanage.mapper.AuditRuleMapper; |
| | | import com.trafficaudit.dataimport.mapper.EnergyVehicleQuarterlyMapper; |
| | | import com.trafficaudit.dataimport.mapper.FreightTurnoverImportMapper; |
| | | import com.trafficaudit.dataimport.mapper.H2032EnterpriseMonthlyMapper; |
| | | import com.trafficaudit.dataimport.mapper.InvestmentMonthlyMapper; |
| | | import com.trafficaudit.dataimport.mapper.CityBusMonthlyMapper; |
| | | import com.trafficaudit.dataimport.mapper.CityTaxiMonthlyMapper; |
| | | import com.trafficaudit.dataimport.mapper.InvestmentProjectMapper; |
| | | import com.trafficaudit.dataimport.mapper.PassengerEnterpriseMonthlyMapper; |
| | | import com.trafficaudit.dataimport.mapper.PassengerIndividualMonthlyMapper; |
| | | import com.trafficaudit.dataimport.mapper.ScaleSplitTransportMapper; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.poi.ss.usermodel.Cell; |
| | | import org.apache.poi.ss.usermodel.CellStyle; |
| | | import org.apache.poi.ss.usermodel.CellType; |
| | | import org.apache.poi.ss.usermodel.Row; |
| | | import org.apache.poi.ss.usermodel.Sheet; |
| | | import org.apache.poi.ss.usermodel.FormulaEvaluator; |
| | | import org.apache.poi.xssf.usermodel.XSSFSheet; |
| | | import org.apache.poi.xssf.usermodel.XSSFWorkbook; |
| | | import org.apache.poi.xssf.usermodel.XSSFCell; |
| | | import org.apache.poi.hssf.usermodel.HSSFCell; |
| | | import org.apache.poi.hssf.usermodel.HSSFRow; |
| | | import org.apache.poi.hssf.usermodel.HSSFSheet; |
| | | import org.apache.poi.hssf.usermodel.HSSFWorkbook; |
| | | import org.apache.poi.ss.util.CellRangeAddress; |
| | | 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.ByteArrayOutputStream; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.InputStream; |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.HashSet; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | import java.util.TreeSet; |
| | | |
| | | /** |
| | | * 报表生成服务:3 类输出报表 |
| | |
| | | private ScaleSplitTransportMapper scaleSplitMapper; |
| | | @Resource |
| | | private FreightTurnoverImportMapper freightTurnoverMapper; |
| | | @Resource |
| | | private PassengerEnterpriseMonthlyMapper passengerMapper; |
| | | @Resource |
| | | private PassengerIndividualMonthlyMapper passengerIndividualMapper; |
| | | @Resource |
| | | private EnergyVehicleQuarterlyMapper energyMapper; |
| | | @Resource |
| | | private InvestmentProjectMapper investProjectMapper; |
| | | @Resource |
| | | private InvestmentMonthlyMapper investMonthlyMapper; |
| | | @Resource |
| | | private CityBusMonthlyMapper cityBusMapper; |
| | | @Resource |
| | | private CityTaxiMonthlyMapper cityTaxiMapper; |
| | | @Resource |
| | | private AuditResultMapper auditResultMapper; |
| | | @Resource |
| | | private AuditRunMapper auditRunMapper; |
| | | @Resource |
| | | private AuditRuleMapper auditRuleMapper; |
| | | /** 城市客运模板目录(application.yml city-passenger.template-dir) */ |
| | | @Value("${city-passenger.template-dir:docs/城市客运}") |
| | | private String templateDir; |
| | | /** 能耗汇总模板目录(application.yml energy.template-dir) */ |
| | | @Value("${energy.template-dir:docs/公路旅客+能耗}") |
| | | private String energyTemplateDir; |
| | | /** 公路旅客输出模板目录(application.yml passenger.template-dir) */ |
| | | @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; |
| | | |
| | | /** 投资报表模板目录(application.yml investment.template-dir) */ |
| | | @Value("${investment.template-dir:docs/投资/模板}") |
| | | private String investTemplateDir; |
| | | |
| | | // ==================== 导出前数据就绪校验(P0:防止导出空表) ==================== |
| | | |
| | | /** 返回每张报表依赖的数据行数与缺失提示 */ |
| | | public Map<String, Object> checkExportReady(String period, String mode, List<String> types) { |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("period", period); |
| | | result.put("mode", mode); |
| | | List<Map<String, Object>> items = new ArrayList<>(); |
| | | for (String t : types) { |
| | | Map<String, Object> it = new LinkedHashMap<>(); |
| | | it.put("key", t); |
| | | List<String> notes = new ArrayList<>(); |
| | | int rows; |
| | | List<String> auditTypes = new ArrayList<>(); |
| | | switch (t) { |
| | | case "cityDetail": |
| | | case "freightRank": |
| | | case "turnoverRank": |
| | | rows = freightReadyRows(period, mode, notes); |
| | | auditTypes.add("H2032"); |
| | | break; |
| | | case "passengerCityDetail": |
| | | case "passengerMidDetail": |
| | | case "passengerMidRank": |
| | | case "passengerMidAnalysis": |
| | | rows = passengerReadyRows(period, mode, notes); |
| | | auditTypes.add("H2031"); |
| | | break; |
| | | case "cityBusDetail": |
| | | case "cityRailFerryDetail": |
| | | rows = addReadyCount(() -> cityBusMapper.selectCount(periodQw(period, mode, CityBusMonthly::getReportPeriod)), "城市公交月报", notes); |
| | | auditTypes.add("CITY_BUS"); |
| | | break; |
| | | case "cityTaxiDetail": |
| | | rows = addReadyCount(() -> cityTaxiMapper.selectCount(periodQw(period, mode, CityTaxiMonthly::getReportPeriod)), "巡游出租月报", notes); |
| | | auditTypes.add("CITY_TAXI"); |
| | | break; |
| | | case "cityPassengerCitySum": |
| | | case "cityPassengerDetailSum": |
| | | case "cityPassengerSummary": |
| | | rows = addReadyCount(() -> cityBusMapper.selectCount(periodQw(period, mode, CityBusMonthly::getReportPeriod)), "城市公交月报", notes) |
| | | + addReadyCount(() -> cityTaxiMapper.selectCount(periodQw(period, mode, CityTaxiMonthly::getReportPeriod)), "巡游出租月报", notes); |
| | | auditTypes.add("CITY_BUS"); |
| | | auditTypes.add("CITY_TAXI"); |
| | | break; |
| | | case "investPlan": |
| | | case "investFiveYearLogistics": |
| | | case "investBillion": |
| | | case "investCounty": |
| | | rows = addReadyCount(() -> investMonthlyMapper.selectCount(periodQw(period, mode, InvestmentMonthly::getReportPeriod)), "投资市州单表/汇总数据", notes); |
| | | auditTypes.add("INVEST"); |
| | | break; |
| | | case "energySummary": |
| | | 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); |
| | | } |
| | | int auditRows = 0; |
| | | boolean audited = false; |
| | | for (String at : auditTypes) { |
| | | 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); |
| | | it.put("audited", audited); |
| | | it.put("note", String.join(";", notes)); |
| | | items.add(it); |
| | | } |
| | | result.put("items", items); |
| | | return result; |
| | | } |
| | | |
| | | // ==================== 报表数据预览(轻量版):就绪 + 月度覆盖 + 核心摘要 + 问题清单 ==================== |
| | | |
| | | /** 报表数据预览:在导出前展示每张表的就绪状态、月度覆盖、核心指标摘要与问题清单 */ |
| | | public Map<String, Object> reportPreview(String period, String mode, List<String> types) { |
| | | Map<String, Object> ready = checkExportReady(period, mode, types); |
| | | @SuppressWarnings("unchecked") |
| | | List<Map<String, Object>> items = (List<Map<String, Object>>) ready.get("items"); |
| | | Map<String, Map<String, Object>> byKey = new LinkedHashMap<>(); |
| | | for (Map<String, Object> it : items) byKey.put((String) it.get("key"), it); |
| | | |
| | | String year = period != null && period.length() >= 4 ? period.substring(0, 4) : ""; |
| | | String yearPrefix = year + "-"; |
| | | int month = monthOf(period, mode); |
| | | int yearNum = 0; |
| | | try { yearNum = Integer.parseInt(year); } catch (Exception ignore) {} |
| | | |
| | | boolean freight = false, passenger = false, cityBus = false, cityTaxi = false, invest = false, energy = false; |
| | | for (String t : types) { |
| | | if (isFreightType(t)) freight = true; |
| | | else if (isPassengerType(t)) passenger = 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; |
| | | } |
| | | |
| | | // 月度覆盖:该年 1..month 中哪些月有源数据 |
| | | Set<Integer> freightCover = new TreeSet<>(); |
| | | Set<Integer> passengerCover = new TreeSet<>(); |
| | | Set<Integer> busCover = new TreeSet<>(); |
| | | Set<Integer> taxiCover = new TreeSet<>(); |
| | | Set<Integer> investCover = new TreeSet<>(); |
| | | if (freight) { |
| | | FreightTurnoverImport ftProv = loadFreightTurnover(period).get("湖北省"); |
| | | for (int m = 1; m <= month; m++) { |
| | | if (ftProv != null && freightMonth(ftProv, m) != null) freightCover.add(m); |
| | | } |
| | | } |
| | | if (passenger) { |
| | | passengerCover.addAll(monthsWithData(() -> passengerMapper.selectList(new LambdaQueryWrapper<PassengerEnterpriseMonthly>().likeRight(PassengerEnterpriseMonthly::getReportPeriod, yearPrefix)), |
| | | o -> ((PassengerEnterpriseMonthly) o).getReportPeriod(), month)); |
| | | passengerCover.addAll(monthsWithData(() -> passengerIndividualMapper.selectList(new LambdaQueryWrapper<PassengerIndividualMonthly>().likeRight(PassengerIndividualMonthly::getReportPeriod, yearPrefix)), |
| | | o -> ((PassengerIndividualMonthly) o).getReportPeriod(), month)); |
| | | } |
| | | if (cityBus) { |
| | | busCover.addAll(monthsWithData(() -> cityBusMapper.selectList(new LambdaQueryWrapper<CityBusMonthly>().likeRight(CityBusMonthly::getReportPeriod, yearPrefix)), |
| | | o -> ((CityBusMonthly) o).getReportPeriod(), month)); |
| | | } |
| | | if (cityTaxi) { |
| | | taxiCover.addAll(monthsWithData(() -> cityTaxiMapper.selectList(new LambdaQueryWrapper<CityTaxiMonthly>().likeRight(CityTaxiMonthly::getReportPeriod, yearPrefix)), |
| | | o -> ((CityTaxiMonthly) o).getReportPeriod(), month)); |
| | | } |
| | | if (invest) { |
| | | investCover.addAll(monthsWithData(() -> investMonthlyMapper.selectList(new LambdaQueryWrapper<InvestmentMonthly>().likeRight(InvestmentMonthly::getReportPeriod, yearPrefix)), |
| | | o -> ((InvestmentMonthly) o).getReportPeriod(), month)); |
| | | } |
| | | |
| | | for (Map.Entry<String, Map<String, Object>> e : byKey.entrySet()) { |
| | | String t = e.getKey(); |
| | | Map<String, Object> it = e.getValue(); |
| | | List<Map<String, Object>> summary = new ArrayList<>(); |
| | | List<String> problems = new ArrayList<>(); |
| | | List<Integer> missing = new ArrayList<>(); |
| | | List<Integer> coverage = new ArrayList<>(); |
| | | if (Boolean.FALSE.equals(it.get("audited"))) problems.add("该报表期尚未审核通过"); |
| | | 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); |
| | | } else if (isPassengerType(t)) { |
| | | coverage.addAll(passengerCover); |
| | | missing = missingMonths(passengerCover, month); |
| | | passengerPreview(yearNum, month, t, summary, problems); |
| | | } else if ("cityTaxiDetail".equals(t)) { |
| | | coverage.addAll(taxiCover); |
| | | missing = missingMonths(taxiCover, month); |
| | | cityPassengerPreview(yearPrefix, month, t, summary, problems); |
| | | } else if (isCityPassengerType(t)) { |
| | | if ("cityBusDetail".equals(t) || "cityRailFerryDetail".equals(t)) { |
| | | // 公交/轨道轮渡明细只依赖公交月报表(轨道/轮渡数据在公交表内) |
| | | coverage.addAll(busCover); |
| | | missing = missingMonths(busCover, month); |
| | | } else { |
| | | Set<Integer> cov = new TreeSet<>(); |
| | | cov.addAll(busCover); |
| | | cov.addAll(taxiCover); |
| | | coverage.addAll(cov); |
| | | missing = missingMonths(cov, month); |
| | | } |
| | | cityPassengerPreview(yearPrefix, month, t, summary, problems); |
| | | } else if (isInvestType(t)) { |
| | | coverage.addAll(investCover); |
| | | // 投资为当月报表(无 1..N 累计口径),只提示当月缺失 |
| | | if (!investCover.contains(month)) missing.add(month); |
| | | investPreview(period, mode, summary, problems); |
| | | } else if ("energySummary".equals(t)) { |
| | | Long ec = energyMapper.selectCount(periodQw(period, mode, EnergyVehicleQuarterly::getReportPeriod)); |
| | | if (ec != null && ec > 0) coverage.add(month); |
| | | energyPreview(period, mode, summary, problems); |
| | | } |
| | | if (!missing.isEmpty()) { |
| | | problems.add("缺月数据:" + joinMonths(missing) + "(1~" + month + "月累计需逐月齐全)"); |
| | | } |
| | | it.put("monthCoverage", coverage); |
| | | it.put("missingMonths", missing); |
| | | it.put("summary", summary); |
| | | it.put("problems", problems); |
| | | } |
| | | return ready; |
| | | } |
| | | |
| | | private boolean isFreightType(String t) { |
| | | return "cityDetail".equals(t) || "freightRank".equals(t) || "turnoverRank".equals(t); |
| | | } |
| | | |
| | | private boolean isPassengerType(String t) { |
| | | return "passengerCityDetail".equals(t) || "passengerMidDetail".equals(t) || "passengerMidRank".equals(t) || "passengerMidAnalysis".equals(t); |
| | | } |
| | | |
| | | private boolean isCityPassengerType(String t) { |
| | | return "cityBusDetail".equals(t) || "cityRailFerryDetail".equals(t) || "cityPassengerCitySum".equals(t) |
| | | || "cityPassengerDetailSum".equals(t) || "cityPassengerSummary".equals(t); |
| | | } |
| | | |
| | | private boolean isInvestType(String t) { |
| | | return "investPlan".equals(t) || "investFiveYearLogistics".equals(t) || "investBillion".equals(t) || "investCounty".equals(t); |
| | | } |
| | | |
| | | private boolean isEnergyType(String t) { |
| | | return "energySummary".equals(t); |
| | | } |
| | | |
| | | /** 按 report_period like 'yyyy-' 统计 1..limit 月中有数据的月份(升序) */ |
| | | private Set<Integer> monthsWithData(java.util.function.Supplier<java.util.List<?>> rows, java.util.function.Function<Object, String> periodOf, int limit) { |
| | | Set<Integer> set = new TreeSet<>(); |
| | | for (Object o : rows.get()) { |
| | | String rp = periodOf.apply(o); |
| | | if (rp == null) continue; |
| | | String[] parts = rp.split("-"); |
| | | if (parts.length < 2) continue; |
| | | try { |
| | | int m = Integer.parseInt(parts[1]); |
| | | if (m >= 1 && m <= limit) set.add(m); |
| | | } catch (Exception ignore) {} |
| | | } |
| | | return set; |
| | | } |
| | | |
| | | /** 1..limit 中缺失的月份 */ |
| | | private List<Integer> missingMonths(Set<Integer> coverage, int limit) { |
| | | List<Integer> missing = new ArrayList<>(); |
| | | for (int m = 1; m <= limit; m++) if (!coverage.contains(m)) missing.add(m); |
| | | return missing; |
| | | } |
| | | |
| | | private String joinMonths(List<Integer> months) { |
| | | StringBuilder sb = new StringBuilder(); |
| | | for (int m : months) { |
| | | if (sb.length() > 0) sb.append("、"); |
| | | sb.append(m).append("月"); |
| | | } |
| | | return sb.toString(); |
| | | } |
| | | |
| | | private void addSum(List<Map<String, Object>> summary, String label, Double value, String unit) { |
| | | Map<String, Object> m = new LinkedHashMap<>(); |
| | | m.put("label", label); |
| | | m.put("value", value); |
| | | m.put("unit", unit); |
| | | summary.add(m); |
| | | } |
| | | |
| | | /** 货运摘要:全省累计货运量(合计/规上/规下)+ 周转量(拆分表全省累计行) */ |
| | | private void freightPreview(String period, int month, List<Map<String, Object>> summary, List<String> problems) { |
| | | Map<String, FreightTurnoverImport> ft = loadFreightTurnover(period); |
| | | 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 above = 0.0; |
| | | for (Double v : h2032Cum.values()) if (v != null) above += v; |
| | | double aboveWan = round(above / 10000.0, 4); |
| | | Double belowWan = totalWan == null ? null : round(totalWan - aboveWan, 4); |
| | | addSum(summary, "全省累计货运量(合计)", totalWan, "万吨"); |
| | | addSum(summary, "其中:规上", aboveWan, "万吨"); |
| | | addSum(summary, "其中:规下", belowWan, "万吨"); |
| | | if (provCum != null) { |
| | | addSum(summary, "全省累计周转量(合计)", provCum.getTotalTurnover(), "万吨公里"); |
| | | addSum(summary, "其中:规上", provCum.getAboveScaleTurnover(), "万吨公里"); |
| | | addSum(summary, "其中:规下", provCum.getBelowScaleTurnover(), "万吨公里"); |
| | | } else { |
| | | problems.add("拆分表无全省累计行(周转量/规上规下缺)"); |
| | | } |
| | | if (totalWan == null) problems.add("模板_货运量周转量无全省累计,累计货运量为空"); |
| | | } |
| | | |
| | | /** 旅客摘要:企业+个体(分市州)或中口径企业(中口径 3 表) */ |
| | | private void passengerPreview(int yearNum, int month, String type, List<Map<String, Object>> summary, List<String> problems) { |
| | | Map<Integer, Map<String, PassengerAgg>> data = loadPassengerAggMap(); |
| | | Map<Integer, Map<String, double[]>> indi = loadPassengerIndividualMap(); |
| | | double entPax = 0.0, entTurn = 0.0, indiPax = 0.0, indiTurn = 0.0; |
| | | boolean anyEnt = false, anyIndi = false; |
| | | for (int m = 1; m <= month; m++) { |
| | | PassengerAgg agg = aggOf(data, yearNum, m, "湖北省"); |
| | | if (agg != null) { |
| | | entPax += agg.passengerTotal / 10000.0; |
| | | entTurn += agg.turnoverTotal / 10000.0; |
| | | anyEnt = true; |
| | | } |
| | | Map<String, double[]> mm = indi.get(yearNum * 100 + m); |
| | | if (mm != null) { |
| | | double[] pv = mm.get("湖北省"); |
| | | if (pv != null) { |
| | | indiPax += pv[0]; |
| | | indiTurn += pv[1]; |
| | | anyIndi = true; |
| | | } |
| | | } |
| | | } |
| | | if ("passengerCityDetail".equals(type)) { |
| | | addSum(summary, "企业客运量累计", round(entPax, 4), "万人次"); |
| | | addSum(summary, "个体客运量累计", round(indiPax, 4), "万人次"); |
| | | addSum(summary, "合计客运量累计", round(entPax + indiPax, 4), "万人次"); |
| | | addSum(summary, "合计周转量累计", round(entTurn + indiTurn, 4), "万人公里"); |
| | | if (!anyEnt && !anyIndi) problems.add("H203-1 旅客月报与个体数据均无"); |
| | | } else { |
| | | addSum(summary, "中口径客运量累计", round(entPax, 4), "万人次"); |
| | | addSum(summary, "中口径周转量累计", round(entTurn, 4), "万人公里"); |
| | | if (!anyEnt) problems.add("H203-1 旅客月报无企业数据"); |
| | | } |
| | | } |
| | | |
| | | /** 城市客运摘要:公交/出租/轨道/轮渡全省累计(指标 0..7) */ |
| | | private void cityPassengerPreview(String yearPrefix, int month, String type, List<Map<String, Object>> summary, List<String> problems) { |
| | | double[] prov = loadCityPassengerCumulative(yearPrefix, month).getOrDefault("全省", new double[8]); |
| | | boolean allZero = true; |
| | | for (double v : prov) if (v != 0.0) allZero = false; |
| | | switch (type) { |
| | | case "cityTaxiDetail": |
| | | addSum(summary, "出租客运量累计", round(prov[2], 4), "万人次"); |
| | | addSum(summary, "出租周转量累计", round(prov[3], 4), "万人公里"); |
| | | break; |
| | | case "cityRailFerryDetail": |
| | | addSum(summary, "轨道客运量累计", round(prov[4], 4), "万人次"); |
| | | addSum(summary, "轨道周转量累计", round(prov[5], 4), "万人公里"); |
| | | addSum(summary, "轮渡客运量累计", round(prov[6], 4), "万人次"); |
| | | addSum(summary, "轮渡周转量累计", round(prov[7], 4), "万人公里"); |
| | | break; |
| | | case "cityBusDetail": |
| | | addSum(summary, "公交客运量累计", round(prov[0], 4), "万人次"); |
| | | addSum(summary, "公交周转量累计", round(prov[1], 4), "万人公里"); |
| | | addSum(summary, "轨道客运量累计", round(prov[4], 4), "万人次"); |
| | | addSum(summary, "轨道周转量累计", round(prov[5], 4), "万人公里"); |
| | | addSum(summary, "轮渡客运量累计", round(prov[6], 4), "万人次"); |
| | | addSum(summary, "轮渡周转量累计", round(prov[7], 4), "万人公里"); |
| | | break; |
| | | default: |
| | | addSum(summary, "公交客运量累计", round(prov[0], 4), "万人次"); |
| | | addSum(summary, "公交周转量累计", round(prov[1], 4), "万人公里"); |
| | | addSum(summary, "出租客运量累计", round(prov[2], 4), "万人次"); |
| | | addSum(summary, "出租周转量累计", round(prov[3], 4), "万人公里"); |
| | | addSum(summary, "轨道客运量累计", round(prov[4], 4), "万人次"); |
| | | addSum(summary, "轨道周转量累计", round(prov[5], 4), "万人公里"); |
| | | addSum(summary, "轮渡客运量累计", round(prov[6], 4), "万人次"); |
| | | addSum(summary, "轮渡周转量累计", round(prov[7], 4), "万人公里"); |
| | | } |
| | | if (allZero) problems.add("全省累计值全为 0(公交/出租月报可能未导入)"); |
| | | } |
| | | |
| | | /** 投资摘要:项目数 + 本月完成/自年初/自开始累计 */ |
| | | private void investPreview(String period, String mode, List<Map<String, Object>> summary, List<String> problems) { |
| | | List<InvestmentMonthly> list = investMonthlyMapper.selectList(periodQw(period, mode, InvestmentMonthly::getReportPeriod)); |
| | | Set<Long> proj = new HashSet<>(); |
| | | double monthDone = 0.0, yearCum = 0.0, startCum = 0.0; |
| | | for (InvestmentMonthly m : list) { |
| | | if (m.getProjectId() != null) proj.add(m.getProjectId()); |
| | | monthDone += nz(m.getMonthDone()); |
| | | yearCum += nz(m.getYearCum()); |
| | | startCum += nz(m.getStartCum()); |
| | | } |
| | | addSum(summary, "有月度记录项目数", (double) proj.size(), "个"); |
| | | addSum(summary, "本月完成投资合计", round(monthDone, 2), "万元"); |
| | | addSum(summary, "自年初累计完成", round(yearCum, 2), "万元"); |
| | | addSum(summary, "自开始累计完成", round(startCum, 2), "万元"); |
| | | if (list.isEmpty()) problems.add("无投资月度数据"); |
| | | } |
| | | |
| | | /** 能耗摘要:车辆记录数 + 燃油消耗合计(季度表,按所选季度末月) */ |
| | | private void energyPreview(String period, String mode, List<Map<String, Object>> summary, List<String> problems) { |
| | | Long cnt = energyMapper.selectCount(periodQw(period, mode, EnergyVehicleQuarterly::getReportPeriod)); |
| | | int rows = cnt == null ? 0 : cnt.intValue(); |
| | | double fuel = 0.0; |
| | | if (rows > 0) { |
| | | for (EnergyVehicleQuarterly e : energyMapper.selectList(periodQw(period, mode, EnergyVehicleQuarterly::getReportPeriod))) { |
| | | fuel += nz(e.getFuelConsumption()); |
| | | } |
| | | } |
| | | addSum(summary, "车辆记录数", (double) rows, "条"); |
| | | addSum(summary, "燃油消耗合计", round(fuel, 2), "—"); |
| | | if (rows == 0) problems.add("该季度无能耗车辆数据"); |
| | | } |
| | | |
| | | private int freightReadyRows(String period, String mode, List<String> notes) { |
| | | return addReadyCount(() -> freightTurnoverMapper.selectCount(periodQw(period, mode, FreightTurnoverImport::getReportPeriod)), "模板_货运量周转量", notes) |
| | | + addReadyCount(() -> scaleSplitMapper.selectCount(periodQw(period, mode, ScaleSplitTransport::getReportPeriod)), "规上规下拆分表", notes) |
| | | + addReadyCount(() -> h2032Mapper.selectCount(periodQw(period, mode, H2032EnterpriseMonthly::getReportPeriod)), "H203-2 货运月报", notes); |
| | | } |
| | | |
| | | private int passengerReadyRows(String period, String mode, List<String> notes) { |
| | | return addReadyCount(() -> passengerMapper.selectCount(periodQw(period, mode, PassengerEnterpriseMonthly::getReportPeriod)), "H203-1 旅客月报", notes) |
| | | + addReadyCount(() -> passengerIndividualMapper.selectCount(periodQw(period, mode, PassengerIndividualMonthly::getReportPeriod)), "个体客运量/周转量", notes); |
| | | } |
| | | |
| | | private <T> LambdaQueryWrapper<T> periodQw(String period, String mode, SFunction<T, ?> periodGetter) { |
| | | LambdaQueryWrapper<T> qw = new LambdaQueryWrapper<>(); |
| | | if ("year".equals(mode)) { |
| | | qw.likeRight(periodGetter, period); |
| | | } else { |
| | | qw.eq(periodGetter, period); |
| | | } |
| | | return qw; |
| | | } |
| | | |
| | | private int addReadyCount(java.util.function.Supplier<Long> counter, String label, List<String> notes) { |
| | | Long cnt = counter.get(); |
| | | if (cnt == null || cnt <= 0) notes.add("缺数据:" + label); |
| | | return cnt == null ? 0 : cnt.intValue(); |
| | | } |
| | | |
| | | /** 该报表期该审核类型已产生的审核记录数(先审核再出表的前置检查) */ |
| | | private int auditRowsOf(String period, String mode, String ruleType) { |
| | | List<AuditRule> rules = auditRuleMapper.selectList(new LambdaQueryWrapper<AuditRule>() |
| | | .eq(AuditRule::getReportType, ruleType) |
| | | .eq(AuditRule::getIsEnabled, 1)); |
| | | if (rules.isEmpty()) return 0; |
| | | List<Long> ruleIds = new ArrayList<>(); |
| | | for (AuditRule r : rules) ruleIds.add(r.getId()); |
| | | LambdaQueryWrapper<AuditResult> qw = new LambdaQueryWrapper<>(); |
| | | if ("year".equals(mode)) qw.likeRight(AuditResult::getReportPeriod, period); |
| | | else qw.eq(AuditResult::getReportPeriod, period); |
| | | Long cnt = auditResultMapper.selectCount(qw.in(AuditResult::getRuleId, ruleIds)); |
| | | return cnt == null ? 0 : cnt.intValue(); |
| | | } |
| | | |
| | | /** 该报表期该审核类型是否已有「审核通过标记」(audit_run 表;线下审核通过/系统审核执行后写入,导出前不再提示未审核) */ |
| | | private boolean auditedMarked(String period, String mode, String ruleType) { |
| | | LambdaQueryWrapper<AuditRun> qw = new LambdaQueryWrapper<AuditRun>() |
| | | .eq(AuditRun::getReportType, ruleType); |
| | | if ("year".equals(mode)) qw.likeRight(AuditRun::getReportPeriod, period); |
| | | else qw.eq(AuditRun::getReportPeriod, period); |
| | | Long cnt = auditRunMapper.selectCount(qw); |
| | | return cnt != null && cnt > 0; |
| | | } |
| | | |
| | | // ==================== 1. 生成_货运量分市州明细.xlsx ==================== |
| | | |
| | | public byte[] exportCityDetail(String period) throws Exception { |
| | | int currentMonth = parseMonth(period); |
| | | String year = period.substring(0, 4); |
| | | public byte[] exportCityDetail(String period, String mode) throws Exception { |
| | | int currentMonth = monthOf(period, mode); |
| | | int fillMonths = currentMonth; // 1..N 月(N>6 时模板自动向右扩列到 12 月) |
| | | boolean extended = fillMonths > 6; |
| | | Map<String, Map<Integer, ScaleSplitTransport>> monthData = loadMonthData(period); |
| | | Map<Integer, ScaleSplitTransport> provinceMonthMap = loadProvinceMonthMap(period); |
| | | ScaleSplitTransport provinceCum = getProvinceCumulative(period); |
| | |
| | | Map<String, Double> h2032FreightCum = loadH2032FreightCumulative(period); |
| | | Map<String, FreightTurnoverImport> ftMap = loadFreightTurnover(period); |
| | | |
| | | XSSFWorkbook wb = new XSSFWorkbook(); |
| | | Sheet sheet = wb.createSheet("货运量分市州明细"); |
| | | |
| | | Row r1 = sheet.createRow(0); |
| | | r1.createCell(0).setCellValue("报表期:" + year + "年" + currentMonth + "月"); |
| | | sheet.createRow(1); |
| | | Row header = sheet.createRow(2); |
| | | header.createCell(0).setCellValue("地区 名称"); |
| | | header.createCell(1).setCellValue("指标"); |
| | | CellStyle dateStyle = wb.createCellStyle(); |
| | | dateStyle.setDataFormat(wb.getCreationHelper().createDataFormat().getFormat("yyyy-mm-dd")); |
| | | for (int m = 1; m <= currentMonth; m++) { |
| | | Cell c = header.createCell(2 + (m - 1) * 2); |
| | | c.setCellValue(java.sql.Date.valueOf(year + "-" + pad(m) + "-01")); |
| | | c.setCellStyle(dateStyle); |
| | | header.createCell(3 + (m - 1) * 2).setCellValue(m + "月与去年同比"); |
| | | } |
| | | int cumCol = 2 + currentMonth * 2; |
| | | header.createCell(cumCol).setCellValue("累计周转量"); |
| | | header.createCell(cumCol + 1).setCellValue("同比"); |
| | | sheet.createRow(3); |
| | | |
| | | int rowIdx = 4; |
| | | String[][] provinceMetrics = { |
| | | {"货运量 (万吨)", "freight", "total"}, |
| | | {"货物周转量 (万吨公里)", "turnover", "total"}, |
| | | {"其中规上货运量 (万吨)", "freight", "above"}, |
| | | {"其中规上货物周转量 (万吨公里)", "turnover", "above"}, |
| | | {"其中规下货运量 (万吨)", "freight", "below"}, |
| | | {"其中规下货物周转量 (万吨公里)", "turnover", "below"} |
| | | }; |
| | | FreightTurnoverImport ftProvince = ftMap.get("湖北省"); |
| | | for (String[] metric : provinceMetrics) { |
| | | Row row = sheet.createRow(rowIdx++); |
| | | row.createCell(0).setCellValue("全省"); |
| | | row.createCell(1).setCellValue(metric[0]); |
| | | for (int m = 1; m <= currentMonth; m++) { |
| | | setNumeric(row, 2 + (m - 1) * 2, getProvinceMonthValue(provinceMonthMap, ftProvince, m, metric[1], metric[2], h2032Freight)); |
| | | setNumeric(row, 3 + (m - 1) * 2, getProvinceMonthYoy(provinceMonthMap, ftProvince, m, metric[1], metric[2])); |
| | | } |
| | | setNumeric(row, cumCol, getProvinceCumValue(provinceCum, ftProvince, metric[1], metric[2], h2032FreightCum, currentMonth)); |
| | | setNumeric(row, cumCol + 1, getProvinceCumYoy(provinceCum, ftProvince, metric[1], metric[2], currentMonth)); |
| | | } |
| | | |
| | | for (String city : RegionUtil.cityList()) { |
| | | String[][] metrics = { |
| | | {"规上+规下货运量(万吨)", "freight", "total"}, |
| | | {"规上货运量 (万吨)", "freight", "above"}, |
| | | {"规下货运量 (万吨)", "freight", "below"}, |
| | | {"规上+规下周转量 (万吨公里)", "turnover", "total"}, |
| | | {"规上货物周转量 (万吨公里)", "turnover", "above"}, |
| | | {"规下货物周转量 (万吨公里)", "turnover", "below"} |
| | | // 以 docs/货运/生成_货运量分市州明细.xlsx 为底稿:保留标题/表头/合并/列宽/样式,仅替换数据区 |
| | | File template = resolveFreightTemplate("生成_货运量分市州明细.xlsx"); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheetAt(0); |
| | | if (extended) extendMonthlyColumns(sheet, 2, Integer.parseInt(period.substring(0, 4)), 4, sheet.getLastRowNum()); |
| | | String[][] provinceMetrics = { |
| | | {"货运量 (万吨)", "freight", "total"}, |
| | | {"货物周转量 (万吨公里)", "turnover", "total"}, |
| | | {"其中规上货运量 (万吨)", "freight", "above"}, |
| | | {"其中规上货物周转量 (万吨公里)", "turnover", "above"}, |
| | | {"其中规下货运量 (万吨)", "freight", "below"}, |
| | | {"其中规下货物周转量 (万吨公里)", "turnover", "below"} |
| | | }; |
| | | FreightTurnoverImport ftCity = ftMap.get(city); |
| | | ScaleSplitTransport cum = cumMap.get(city); |
| | | for (String[] metric : metrics) { |
| | | Row row = sheet.createRow(rowIdx++); |
| | | row.createCell(0).setCellValue(RegionUtil.shortName(city)); |
| | | row.createCell(1).setCellValue(metric[0]); |
| | | for (int m = 1; m <= currentMonth; m++) { |
| | | setNumeric(row, 2 + (m - 1) * 2, getCityMonthValue(monthData, ftCity, city, m, metric[1], metric[2], h2032Freight)); |
| | | setNumeric(row, 3 + (m - 1) * 2, getCityMonthYoy(monthData, ftCity, city, m, metric[1], metric[2])); |
| | | } |
| | | setNumeric(row, cumCol, getCityCumValue(cum, ftCity, metric[1], metric[2], h2032FreightCum, city, currentMonth)); |
| | | setNumeric(row, cumCol + 1, getCityCumYoy(cum, ftCity, metric[1], metric[2], currentMonth)); |
| | | FreightTurnoverImport ftProvince = ftMap.get("湖北省"); |
| | | int rowIdx = 4; // 0-based:第5行起为 全省 6 指标 |
| | | for (String[] metric : provinceMetrics) { |
| | | Row row = sheet.getRow(rowIdx); |
| | | if (row == null) row = sheet.createRow(rowIdx); |
| | | fillFreightDetailRow(row, fillMonths, extended ? 26 : 14, |
| | | m -> getProvinceMonthValue(provinceMonthMap, ftProvince, m, metric[1], metric[2], h2032Freight), |
| | | m -> getProvinceMonthYoy(provinceMonthMap, ftProvince, m, metric[1], metric[2]), |
| | | getProvinceCumValue(provinceCum, ftProvince, metric[1], metric[2], h2032FreightCum, currentMonth), |
| | | getProvinceCumYoy(provinceCum, ftProvince, metric[1], metric[2], currentMonth)); |
| | | rowIdx++; |
| | | } |
| | | for (String city : RegionUtil.cityList()) { |
| | | String[][] metrics = { |
| | | {"规上+规下货运量(万吨)", "freight", "total"}, |
| | | {"规上货运量 (万吨)", "freight", "above"}, |
| | | {"规下货运量 (万吨)", "freight", "below"}, |
| | | {"规上+规下周转量 (万吨公里)", "turnover", "total"}, |
| | | {"规上货物周转量 (万吨公里)", "turnover", "above"}, |
| | | {"规下货物周转量 (万吨公里)", "turnover", "below"} |
| | | }; |
| | | FreightTurnoverImport ftCity = ftMap.get(city); |
| | | ScaleSplitTransport cum = cumMap.get(city); |
| | | for (String[] metric : metrics) { |
| | | Row row = sheet.getRow(rowIdx); |
| | | if (row == null) row = sheet.createRow(rowIdx); |
| | | fillFreightDetailRow(row, fillMonths, extended ? 26 : 14, |
| | | m -> getCityMonthValue(monthData, ftCity, city, m, metric[1], metric[2], h2032Freight), |
| | | m -> getCityMonthYoy(monthData, ftCity, city, m, metric[1], metric[2]), |
| | | getCityCumValue(cum, ftCity, metric[1], metric[2], h2032FreightCum, city, currentMonth), |
| | | getCityCumYoy(cum, ftCity, metric[1], metric[2], currentMonth)); |
| | | rowIdx++; |
| | | } |
| | | } |
| | | return toBytes(wb); |
| | | } |
| | | |
| | | autoWidth(sheet, cumCol + 2); |
| | | return toBytes(wb); |
| | | } |
| | | // ==================== 2. 生成_货运量排名.xlsx ==================== |
| | | |
| | | public byte[] exportFreightRank(String period) throws Exception { |
| | | public byte[] exportFreightRank(String period, String mode) throws Exception { |
| | | String year = period.substring(0, 4); |
| | | int month = parseMonth(period); |
| | | int month = monthOf(period, mode); |
| | | Map<String, FreightTurnoverImport> ftMap = loadFreightTurnover(period); |
| | | Map<String, Double> h2032FreightCum = loadH2032FreightCumulative(period); |
| | | Map<String, Double> h2032FreightYoy = computeH2032Yoy(period); |
| | | Map<String, Double> h2032FreightCumLast = loadH2032FreightCumulative( |
| | | (Integer.parseInt(period.substring(0, 4)) - 1) + period.substring(4)); |
| | | |
| | | Map<String, Double> above = new HashMap<>(); |
| | | Map<String, Double> below = new HashMap<>(); |
| | |
| | | Double provTotal = freightCum(ftMap.get("湖北省"), month); |
| | | Double provBelow = provTotal == null ? null : round(provTotal - provAboveWan, 4); |
| | | Double provTotalYoy = freightCumYoy(ftMap.get("湖北省"), month); |
| | | double provAboveLast = 0.0; |
| | | for (Double v : h2032FreightCumLast.values()) { |
| | | if (v != null) provAboveLast += v; |
| | | } |
| | | Double provAboveYoy = provAboveLast == 0.0 ? null : round((provAbove - provAboveLast) / provAboveLast, 4); |
| | | Double provTotalLast = lastFreightCum(ftMap.get("湖北省"), month); |
| | | Double provBelowLast = provTotalLast == null ? null : round(provTotalLast - provAboveLast / 10000.0, 4); |
| | | Double provBelowYoy = provBelowLast == null || provBelowLast == 0.0 ? null |
| | | : round((provBelow - provBelowLast) / provBelowLast, 4); |
| | | |
| | | for (String city : RegionUtil.cityList()) { |
| | | Double a = h2032FreightCum.get(city); |
| | |
| | | total.put(city, t); |
| | | below.put(city, t == null ? null : round(t - aWan, 4)); |
| | | aboveYoy.put(city, h2032FreightYoy.get(city)); |
| | | belowYoy.put(city, null); |
| | | // 规下同比 = (规下今年 - 规下去年) / 规下去年;规下去年 = 模板去年合计 - H2032去年规上 |
| | | Double tLast = lastFreightCum(ftMap.get(city), month); |
| | | Double aLast = h2032FreightCumLast.get(city); |
| | | double aLastWan = aLast == null ? 0.0 : round(aLast / 10000.0, 4); |
| | | Double belowLast = tLast == null ? null : round(tLast - aLastWan, 4); |
| | | Double b = below.get(city); |
| | | belowYoy.put(city, belowLast == null || belowLast == 0.0 || b == null |
| | | ? null : round((b - belowLast) / belowLast, 4)); |
| | | totalYoy.put(city, freightCumYoy(ftMap.get(city), month)); |
| | | } |
| | | |
| | | XSSFWorkbook wb = new XSSFWorkbook(); |
| | | Sheet sheet = wb.createSheet("货运量排名"); |
| | | |
| | | Row title = sheet.createRow(0); |
| | | title.createCell(0).setCellValue(year + "年1-" + month + "月全省分市州累计完成公路货运量情况"); |
| | | |
| | | int rowIdx = 1; |
| | | rowIdx = writeRankHead(sheet, rowIdx, "货运量", "吨"); |
| | | rowIdx = writeFreightRankRows(sheet, rowIdx, null, above, below, total, aboveYoy, belowYoy, totalYoy, |
| | | provAboveWan, provBelow, provTotal, null, null, provTotalYoy, true); |
| | | // ===== 第二页(当月)数据:规上=H2032 当月、合计=模板_货运量周转量当月列、规下=差值;同比=去年同月 ===== |
| | | Map<String, Map<Integer, Double>> h2032MonthCur = loadH2032FreightByMonth(period); |
| | | String lastYearPeriod = (Integer.parseInt(year) - 1) + period.substring(4); |
| | | Map<String, Map<Integer, Double>> h2032MonthLast = loadH2032FreightByMonth(lastYearPeriod); |
| | | Map<String, Double> monthAbove = new HashMap<>(); |
| | | Map<String, Double> monthBelow = new HashMap<>(); |
| | | Map<String, Double> monthTotal = new HashMap<>(); |
| | | Map<String, Double> monthAboveYoy = new HashMap<>(); |
| | | Map<String, Double> monthBelowYoy = new HashMap<>(); |
| | | Map<String, Double> monthTotalYoy = new HashMap<>(); |
| | | double provMonthAbove = 0.0, provMonthAboveLast = 0.0; |
| | | for (String city : RegionUtil.cityList()) { |
| | | rowIdx = writeFreightRankRows(sheet, rowIdx, city, above, below, total, aboveYoy, belowYoy, totalYoy, |
| | | provAboveWan, provBelow, provTotal, null, null, provTotalYoy, false); |
| | | Double ma = monthFreightWan(h2032MonthCur, city, month); |
| | | Double maLast = monthFreightWan(h2032MonthLast, city, month); |
| | | Double mt = freightMonth(ftMap.get(city), month); |
| | | Double mtLast = lastFreightMonth(ftMap.get(city), month); |
| | | if (ma != null) provMonthAbove += ma; |
| | | if (maLast != null) provMonthAboveLast += maLast; |
| | | monthAbove.put(city, ma); |
| | | monthTotal.put(city, mt); |
| | | monthBelow.put(city, mt == null ? null : round(mt - (ma == null ? 0.0 : ma), 4)); |
| | | monthAboveYoy.put(city, growthYoy(ma, maLast)); |
| | | Double mb = monthBelow.get(city); |
| | | Double mbLast = mtLast == null ? null : round(mtLast - (maLast == null ? 0.0 : maLast), 4); |
| | | monthBelowYoy.put(city, growthYoy(mb, mbLast)); |
| | | monthTotalYoy.put(city, freightYoy(ftMap.get(city), month)); |
| | | } |
| | | Double provMonthAboveWan = provMonthAbove == 0.0 ? null : round(provMonthAbove, 4); |
| | | Double provMonthTotal = freightMonth(ftMap.get("湖北省"), month); |
| | | Double provMonthBelow = provMonthTotal == null ? null : round(provMonthTotal - provMonthAbove, 4); |
| | | Double provMonthAboveYoy = growthYoy(provMonthAbove == 0.0 ? null : provMonthAbove, provMonthAboveLast); |
| | | Double provMonthTotalLast = lastFreightMonth(ftMap.get("湖北省"), month); |
| | | Double provMonthBelowLast = provMonthTotalLast == null ? null |
| | | : round(provMonthTotalLast - provMonthAboveLast, 4); |
| | | Double provMonthBelowYoy = growthYoy(provMonthBelow, provMonthBelowLast); |
| | | Double provMonthTotalYoy = freightYoy(ftMap.get("湖北省"), month); |
| | | |
| | | // 本月区块(当月标题 + 累计数据,与样例结构一致) |
| | | sheet.createRow(rowIdx++); |
| | | Row title2 = sheet.createRow(rowIdx++); |
| | | title2.createCell(0).setCellValue(year + "年" + month + "月全省分市州累计完成" + "公路货运量情况"); |
| | | rowIdx = writeRankHead(sheet, rowIdx, "货运量", "吨"); |
| | | rowIdx = writeFreightRankRows(sheet, rowIdx, null, above, below, total, aboveYoy, belowYoy, totalYoy, |
| | | provAboveWan, provBelow, provTotal, null, null, provTotalYoy, true); |
| | | for (String city : RegionUtil.cityList()) { |
| | | rowIdx = writeFreightRankRows(sheet, rowIdx, city, above, below, total, aboveYoy, belowYoy, totalYoy, |
| | | provAboveWan, provBelow, provTotal, null, null, provTotalYoy, false); |
| | | // 以 docs/货运/生成_货运量排名.xlsx 为底稿:保留表头/合并/列宽/样式与 Q/R 占比公式,仅替换数据 |
| | | File template = resolveFreightTemplate("生成_货运量排名.xlsx"); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheetAt(0); |
| | | sheet.getRow(0).getCell(0).setCellValue(year + "年" + cumRange(month) + "全省分市州累计完成公路货运量情况"); |
| | | sheet.getRow(22).getCell(0).setCellValue(year + "年" + month + "月全省分市州完成公路货运量情况"); |
| | | fillFreightRankBlock(sheet, 3, above, below, total, aboveYoy, belowYoy, totalYoy, |
| | | provAboveWan, provBelow, provTotal, provAboveYoy, provBelowYoy, provTotalYoy); |
| | | renameRankMonthHeader(sheet, 24); |
| | | fillFreightRankBlock(sheet, 25, monthAbove, monthBelow, monthTotal, |
| | | monthAboveYoy, monthBelowYoy, monthTotalYoy, |
| | | provMonthAboveWan, provMonthBelow, provMonthTotal, |
| | | provMonthAboveYoy, provMonthBelowYoy, provMonthTotalYoy); |
| | | try { |
| | | wb.getCreationHelper().createFormulaEvaluator().evaluateAll(); |
| | | } catch (Exception e) { |
| | | log.warn("货运量排名模板公式求值失败: {}", e.getMessage()); |
| | | } |
| | | return toBytes(wb); |
| | | } |
| | | |
| | | // 末尾空行(与样例结构对齐) |
| | | for (int i = 0; i < 4; i++) sheet.createRow(rowIdx++); |
| | | |
| | | autoWidth(sheet, 18); |
| | | return toBytes(wb); |
| | | } |
| | | |
| | | // ==================== 3. 生成_周转量排名.xlsx ==================== |
| | | |
| | | public byte[] exportTurnoverRank(String period) throws Exception { |
| | | public byte[] exportTurnoverRank(String period, String mode) throws Exception { |
| | | String year = period.substring(0, 4); |
| | | int month = parseMonth(period); |
| | | int month = monthOf(period, mode); |
| | | Map<String, ScaleSplitTransport> cumMap = loadCumulativeMap(period); |
| | | ScaleSplitTransport province = getProvinceCumulative(period); |
| | | |
| | | XSSFWorkbook wb = new XSSFWorkbook(); |
| | | Sheet sheet = wb.createSheet("周转量排名"); |
| | | |
| | | Row title = sheet.createRow(0); |
| | | title.createCell(0).setCellValue(year + "年1-" + month + "月全省分市州累计完成公路货物运输周转量情况"); |
| | | |
| | | int rowIdx = 1; |
| | | rowIdx = writeRankHead(sheet, rowIdx, "周转量", "吨公里"); |
| | | rowIdx = writeTurnoverRankRows(sheet, rowIdx, null, cumMap, province, true); |
| | | for (String city : RegionUtil.cityList()) { |
| | | rowIdx = writeTurnoverRankRows(sheet, rowIdx, city, cumMap, province, false); |
| | | // 当月(第二页):周转量拆分表 MONTH 行(含当月同比) |
| | | Map<String, ScaleSplitTransport> monthTurnover = new HashMap<>(); |
| | | for (ScaleSplitTransport rec : scaleSplitMapper.selectList( |
| | | new LambdaQueryWrapper<ScaleSplitTransport>() |
| | | .eq(ScaleSplitTransport::getPeriodType, "MONTH") |
| | | .eq(ScaleSplitTransport::getReportPeriod, period))) { |
| | | String name = RegionUtil.normalizeCityName(rec.getRegionName()); |
| | | if (name != null) monthTurnover.put(name, rec); |
| | | } |
| | | ScaleSplitTransport provinceMonth = monthTurnover.remove("湖北省"); |
| | | |
| | | // 本月区块(当月标题 + 累计数据,与样例结构一致) |
| | | sheet.createRow(rowIdx++); |
| | | Row title2 = sheet.createRow(rowIdx++); |
| | | title2.createCell(0).setCellValue(year + "年" + month + "月全省分市州累计完成" + "公路货物运输周转量情况"); |
| | | rowIdx = writeRankHead(sheet, rowIdx, "周转量", "吨公里"); |
| | | rowIdx = writeTurnoverRankRows(sheet, rowIdx, null, cumMap, province, true); |
| | | for (String city : RegionUtil.cityList()) { |
| | | rowIdx = writeTurnoverRankRows(sheet, rowIdx, city, cumMap, province, false); |
| | | // 以 docs/货运/生成_周转量排名.xlsx 为底稿:保留表头/合并/列宽/样式,仅替换数据;Q/R 占比为模板缓存值,按新数据重算 |
| | | File template = resolveFreightTemplate("生成_周转量排名.xlsx"); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheetAt(0); |
| | | sheet.getRow(0).getCell(0).setCellValue(year + "年" + cumRange(month) + "全省分市州累计完成公路货物运输周转量情况"); |
| | | sheet.getRow(22).getCell(0).setCellValue(year + "年" + month + "月全省分市州完成公路货物运输周转量情况"); |
| | | fillTurnoverRankBlock(sheet, 3, cumMap, province); |
| | | renameRankMonthHeader(sheet, 24); |
| | | fillTurnoverRankBlock(sheet, 25, monthTurnover, provinceMonth); |
| | | return toBytes(wb); |
| | | } |
| | | |
| | | // 末尾空行(与样例结构对齐) |
| | | for (int i = 0; i < 4; i++) sheet.createRow(rowIdx++); |
| | | |
| | | autoWidth(sheet, 18); |
| | | return toBytes(wb); |
| | | } |
| | | /** 货运报表模板定位(docs/货运) */ |
| | | private File resolveFreightTemplate(String fileName) throws Exception { |
| | | return resolveAnyTemplate(freightTemplateDir, fileName); |
| | | } |
| | | |
| | | /** 写货运模板数据单元格:公式列不动;有值先清空再写(POI 对公式单元格只更新缓存),无值清空模板样例 */ |
| | | private void setFreightCell(Row row, int idx, Double v) { |
| | | Cell c = row.getCell(idx); |
| | | if (c != null && c.getCellType() == CellType.FORMULA) return; |
| | | if (c == null) c = row.createCell(idx); |
| | | c.setBlank(); |
| | | if (v != null) c.setCellValue(v); |
| | | } |
| | | |
| | | private void setFreightCell(Row row, int idx, Integer v) { |
| | | if (v == null) { |
| | | setFreightCell(row, idx, (Double) null); |
| | | } else { |
| | | setFreightCell(row, idx, v.doubleValue()); |
| | | } |
| | | } |
| | | |
| | | // ==================== 月度模板自动扩列(1-6月 → 1-12月) ==================== |
| | | |
| | | /** 月度模板向右扩列:累计列(14/15)移到末尾(26/27),7..12 月列由 1 月列克隆(表头/样式/公式引用同步) */ |
| | | private void extendMonthlyColumns(Sheet sheet, int headerRow, int year, int dataStartRow, int dataEndRow) { |
| | | moveColumnPair(sheet, headerRow, 14, 26, dataEndRow); // 累计/累计同比 → AA/AB |
| | | for (int m = 7; m <= 12; m++) { |
| | | int dst = 2 + (m - 1) * 2; |
| | | cloneColumnPair(sheet, headerRow, 2, 3, dst, dst + 1, year, m, dataStartRow, dataEndRow); |
| | | } |
| | | fixCumulativeFormulas(sheet, headerRow, dataEndRow); // AA 累计公式 = 12 个月值之和 |
| | | } |
| | | |
| | | /** 把一列对(值列+同比列)从 src 移到 dst(含表头/数据/样式/合并/列宽),src 内容清空 */ |
| | | private void moveColumnPair(Sheet sheet, int headerRow, int src, int dst, int lastRow) { |
| | | for (int r = headerRow; r <= lastRow; r++) { |
| | | copyCellForColumn(sheet, r, src, r, dst, false); |
| | | copyCellForColumn(sheet, r, src + 1, r, dst + 1, false); |
| | | clearCellContent(sheet, r, src); |
| | | clearCellContent(sheet, r, src + 1); |
| | | } |
| | | moveMergedRegion(sheet, src, dst); |
| | | moveMergedRegion(sheet, src + 1, dst + 1); |
| | | copyColumnWidth(sheet, src, dst); |
| | | copyColumnWidth(sheet, src + 1, dst + 1); |
| | | } |
| | | |
| | | /** 把 1 月列对克隆为 m 月列对:表头文本/日期修正、样式复制、非跨簿公式克隆并同步列引用 */ |
| | | private void cloneColumnPair(Sheet sheet, int headerRow, int srcVal, int srcYoy, int dstVal, int dstYoy, |
| | | int year, int month, int dataStartRow, int dataEndRow) { |
| | | for (int r = headerRow; r <= dataEndRow; r++) { |
| | | boolean dataZone = r >= dataStartRow; |
| | | cloneCellForMonth(sheet, r, srcVal, dstVal, year, month, dataZone); |
| | | cloneCellForMonth(sheet, r, srcYoy, dstYoy, year, month, dataZone); |
| | | } |
| | | cloneMergedRegion(sheet, srcVal, dstVal); |
| | | cloneMergedRegion(sheet, srcYoy, dstYoy); |
| | | copyColumnWidth(sheet, srcVal, dstVal); |
| | | copyColumnWidth(sheet, srcYoy, dstYoy); |
| | | } |
| | | |
| | | /** 复制单格:move=false 原样复制;move=true 表头文本/日期修正、数据区仅样式+非跨簿公式(列引用同步) */ |
| | | private void copyCellForColumn(Sheet sheet, int r, int srcC, int dstR, int dstC, boolean monthClone) { |
| | | Row srcRow = sheet.getRow(r); |
| | | if (srcRow == null) return; |
| | | Cell src = srcRow.getCell(srcC); |
| | | if (src == null) return; |
| | | Cell dst = getOrCreateCell(sheet, dstR, dstC); |
| | | dst.setBlank(); |
| | | dst.setCellStyle(src.getCellStyle()); |
| | | if (!monthClone) { |
| | | if (src.getCellType() == CellType.FORMULA) { |
| | | String f = src.getCellFormula(); |
| | | if (f != null && !f.contains("#REF!")) dst.setCellFormula(f); |
| | | } else if (src.getCellType() == CellType.STRING) { |
| | | dst.setCellValue(src.getStringCellValue()); |
| | | } else if (src.getCellType() == CellType.NUMERIC) { |
| | | dst.setCellValue(src.getNumericCellValue()); |
| | | } else if (src.getCellType() == CellType.BOOLEAN) { |
| | | dst.setCellValue(src.getBooleanCellValue()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 克隆单格到目标月列:表头行(dataZone=false)修正月份/年份文本或日期;数据区仅样式+非跨簿公式克隆 */ |
| | | private void cloneCellForMonth(Sheet sheet, int r, int srcC, int dstC, int year, int month, boolean dataZone) { |
| | | Row srcRow = sheet.getRow(r); |
| | | if (srcRow == null) return; |
| | | Cell src = srcRow.getCell(srcC); |
| | | if (src == null) return; |
| | | Cell dst = getOrCreateCell(sheet, r, dstC); |
| | | dst.setBlank(); |
| | | dst.setCellStyle(src.getCellStyle()); |
| | | if (!dataZone) { |
| | | if (src.getCellType() == CellType.NUMERIC && org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(src)) { |
| | | java.util.Calendar cal = java.util.Calendar.getInstance(); |
| | | cal.set(year, month - 1, 1, 0, 0, 0); |
| | | cal.clear(java.util.Calendar.MILLISECOND); |
| | | dst.setCellValue(cal.getTime()); |
| | | } else if (src.getCellType() == CellType.STRING) { |
| | | String t = src.getStringCellValue(); |
| | | if (t != null && !t.isEmpty()) { |
| | | dst.setCellValue(t.replaceFirst("^\\d{4}年", year + "年").replaceFirst("(\\d+)月", month + "月")); |
| | | } |
| | | } |
| | | return; |
| | | } |
| | | // 数据区:仅复制样式;非跨簿、非 #REF! 的公式克隆并同步月值列引用 |
| | | if (src.getCellType() == CellType.FORMULA) { |
| | | String f = src.getCellFormula(); |
| | | if (f != null && !f.contains("[") && !f.contains("#REF!")) { |
| | | dst.setCellFormula(shiftMonthFormula(f, colLetter(dstC))); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 公式列引用平移:本表月值列(C/E/G/I/K/M/O/Q/S/U/W/Y)→ 目标列字母;排除跨簿 [..]! 引用 */ |
| | | private String shiftMonthFormula(String formula, String dstColLetter) { |
| | | return formula.replaceAll("(?<![A-Za-z0-9\\[\\]!])([CEGIKMOQSUWY])(\\d+)", dstColLetter + "$2"); |
| | | } |
| | | |
| | | /** AA 累计列公式重写为 12 个月值之和(模板原为 1-6 月之和) */ |
| | | private void fixCumulativeFormulas(Sheet sheet, int headerRow, int dataEndRow) { |
| | | for (int r = headerRow; r <= dataEndRow; r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | Cell c = row.getCell(26); |
| | | if (c == null || c.getCellType() != CellType.FORMULA) continue; |
| | | String f = c.getCellFormula(); |
| | | if (f == null) continue; |
| | | java.util.regex.Matcher m = java.util.regex.Pattern |
| | | .compile("(?<![A-Za-z0-9\\[\\]!])([CEGIKMOQSUWY])(\\d+)").matcher(f); |
| | | if (!m.find()) continue; |
| | | int rowNum = Integer.parseInt(m.group(2)); |
| | | StringBuilder sb = new StringBuilder(); |
| | | for (char col : new char[]{'C', 'E', 'G', 'I', 'K', 'M', 'O', 'Q', 'S', 'U', 'W', 'Y'}) { |
| | | if (sb.length() > 0) sb.append("+"); |
| | | sb.append(col).append(rowNum); |
| | | } |
| | | c.setCellFormula(sb.toString()); |
| | | } |
| | | } |
| | | |
| | | /** 合并单元格:src 单列合并 → dst 列(同形状),并移除原合并 */ |
| | | private void moveMergedRegion(Sheet sheet, int srcCol, int dstCol) { |
| | | java.util.List<CellRangeAddress> moves = new java.util.ArrayList<>(); |
| | | for (CellRangeAddress m : sheet.getMergedRegions()) { |
| | | if (m.getFirstColumn() == srcCol && m.getLastColumn() == srcCol) moves.add(m); |
| | | } |
| | | for (CellRangeAddress m : moves) { |
| | | sheet.addMergedRegion(new CellRangeAddress(m.getFirstRow(), m.getLastRow(), dstCol, dstCol)); |
| | | for (int i = sheet.getNumMergedRegions() - 1; i >= 0; i--) { |
| | | if (sheet.getMergedRegion(i).equals(m)) { |
| | | sheet.removeMergedRegion(i); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 合并单元格:src 单列合并 → dst 列(同形状,保留原合并) */ |
| | | private void cloneMergedRegion(Sheet sheet, int srcCol, int dstCol) { |
| | | for (CellRangeAddress m : sheet.getMergedRegions()) { |
| | | if (m.getFirstColumn() == srcCol && m.getLastColumn() == srcCol) { |
| | | sheet.addMergedRegion(new CellRangeAddress(m.getFirstRow(), m.getLastRow(), dstCol, dstCol)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void copyColumnWidth(Sheet sheet, int src, int dst) { |
| | | try { |
| | | int w = sheet.getColumnWidth(src); |
| | | if (w > 0) sheet.setColumnWidth(dst, w); |
| | | } catch (Exception ignore) { } |
| | | } |
| | | |
| | | private Cell getOrCreateCell(Sheet sheet, int r, int c) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) row = sheet.createRow(r); |
| | | Cell cell = row.getCell(c); |
| | | if (cell == null) cell = row.createCell(c); |
| | | return cell; |
| | | } |
| | | |
| | | private void clearCellContent(Sheet sheet, int r, int c) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) return; |
| | | Cell cell = row.getCell(c); |
| | | if (cell != null) cell.setBlank(); |
| | | } |
| | | |
| | | private String colLetter(int colIdx) { |
| | | return org.apache.poi.ss.util.CellReference.convertNumToColString(colIdx); |
| | | } |
| | | |
| | | /** 旅客分市州月值(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 == 0 || metricIdx == 1) { // 客运量/周转量总行:企业+个体 |
| | | PassengerAgg agg = aggOf(data, year, month, region); |
| | | double ent = agg == null ? 0.0 |
| | | : (metricIdx == 0 ? agg.passengerTotal / 10000.0 : agg.turnoverTotal / 10000.0); |
| | | double ind = individualVal(indi, year, month, region, metricIdx == 0 ? 0 : 1); |
| | | return ent + ind; |
| | | } |
| | | return individualVal(indi, year, month, region, metricIdx - 2); |
| | | } |
| | | |
| | | /** 旅客累计同比(去年 1..months 月累计):无去年数据返回 null */ |
| | | private Double passengerCumYoy(Map<Integer, Map<String, PassengerAgg>> data, Map<Integer, Map<String, double[]>> indi, |
| | | int year, int months, String region, int metricIdx) { |
| | | double v = 0.0, lv = 0.0; |
| | | for (int m = 1; m <= months; m++) { |
| | | v += passengerMonthVal(data, indi, year, m, region, metricIdx); |
| | | lv += passengerMonthVal(data, indi, year - 1, m, region, metricIdx); |
| | | } |
| | | return lv == 0.0 ? null : (v - lv) / lv; |
| | | } |
| | | |
| | | /** 中口径月同比:k=-1 表示总行(班线+公交之和,出租/网约车无源数据按 0);无去年数据留空 */ |
| | | private void fillMidYoyCell(Cell c, Map<Integer, Map<String, double[][]>> mid, int year, int month, |
| | | String area, int k, boolean volume) { |
| | | double v = 0.0, lv = 0.0; |
| | | if (k == -1) { |
| | | for (int kk = 1; kk <= 4; kk++) { |
| | | if (kk >= 3) continue; |
| | | v += midClassVal(mid, year, month, area, kk, volume); |
| | | lv += midClassVal(mid, year - 1, month, area, kk, volume); |
| | | } |
| | | } else if (k <= 2) { |
| | | v = midClassVal(mid, year, month, area, k, volume); |
| | | lv = midClassVal(mid, year - 1, month, area, k, volume); |
| | | } |
| | | c.setBlank(); |
| | | if (v != 0.0 && lv != 0.0) c.setCellValue(round((v - lv) / lv, 4)); |
| | | } |
| | | |
| | | /** 中口径累计同比列(1..months 月累计;k=-1 总行 = 班线+公交) */ |
| | | private void fillMidCumYoyColumn(Sheet sheet, Map<Integer, Map<String, double[][]>> mid, int year, |
| | | int months, int colIdx) { |
| | | List<String> areas = midTemplateAreas(); |
| | | for (int r0 = 4; r0 <= sheet.getLastRowNum(); r0++) { |
| | | int rr = r0 + 1; |
| | | int blockOffset = -1; |
| | | boolean volume = false; |
| | | if (rr >= 5 && rr <= 94) { |
| | | blockOffset = rr - 5; |
| | | volume = true; |
| | | } else if (rr >= 99 && rr <= 188) { |
| | | blockOffset = rr - 99; |
| | | volume = false; |
| | | } |
| | | if (blockOffset < 0 || blockOffset % 5 == 0) continue; |
| | | int k = blockOffset % 5; |
| | | String area = areas.get(blockOffset / 5); |
| | | double v = 0.0, lv = 0.0; |
| | | for (int m = 1; m <= months; m++) { |
| | | if (k <= 2) { |
| | | v += midClassVal(mid, year, m, area, k, volume); |
| | | lv += midClassVal(mid, year - 1, m, area, k, volume); |
| | | } |
| | | } |
| | | Row row = sheet.getRow(r0); |
| | | if (row == null) continue; |
| | | Cell c = row.getCell(colIdx); |
| | | if (c == null) c = row.createCell(colIdx); |
| | | c.setBlank(); |
| | | if (v != 0.0 && lv != 0.0) c.setCellValue(round((v - lv) / lv, 4)); |
| | | } |
| | | } |
| | | |
| | | /** 货运分市州明细行填充:1..fillMonths 月值/同比(最多 12 月,其余清空)+ 累计/累计同比(cumCol 起两列) */ |
| | | private void fillFreightDetailRow(Row row, int fillMonths, int cumCol, |
| | | java.util.function.IntFunction<Double> monthValue, |
| | | java.util.function.IntFunction<Double> monthYoy, |
| | | Double cumValue, Double cumYoy) { |
| | | for (int m = 1; m <= (fillMonths > 6 ? 12 : 6); m++) { |
| | | if (m <= fillMonths) { |
| | | setFreightCell(row, 2 + (m - 1) * 2, monthValue.apply(m)); |
| | | setFreightCell(row, 3 + (m - 1) * 2, monthYoy.apply(m)); |
| | | } else { |
| | | setFreightCell(row, 2 + (m - 1) * 2, (Double) null); |
| | | setFreightCell(row, 3 + (m - 1) * 2, (Double) null); |
| | | } |
| | | } |
| | | setFreightCell(row, cumCol, cumValue); |
| | | setFreightCell(row, cumCol + 1, cumYoy); |
| | | } |
| | | |
| | | /** H2032 规上某月合计(吨→万吨);无该月数据返回 null */ |
| | | private Double monthFreightWan(Map<String, Map<Integer, Double>> byCityMonth, String city, int month) { |
| | | Map<Integer, Double> byMonth = byCityMonth == null ? null : byCityMonth.get(city); |
| | | Double v = byMonth == null ? null : byMonth.get(month); |
| | | return v == null ? null : round(v / 10000.0, 4); |
| | | } |
| | | |
| | | /** 同比 = (本期-基期)/基期;任一期缺失或基期为 0 返回 null */ |
| | | private Double growthYoy(Double cur, Double last) { |
| | | if (cur == null || last == null || last == 0.0) return null; |
| | | return (cur - last) / last; |
| | | } |
| | | |
| | | /** 排名表第二页列头「累计完成」→「当月完成」(仅导出内存替换,模板文件不改) */ |
| | | private void renameRankMonthHeader(Sheet sheet, int headerRowIdx) { |
| | | Row row = sheet.getRow(headerRowIdx); |
| | | if (row == null) return; |
| | | for (int col : new int[]{1, 6, 11}) { |
| | | Cell c = row.getCell(col); |
| | | if (c == null || c.getCellType() != CellType.STRING) continue; |
| | | String s = c.getStringCellValue(); |
| | | if (s != null && s.contains("累计完成")) c.setCellValue(s.replace("累计完成", "当月完成")); |
| | | } |
| | | } |
| | | |
| | | /** 货运量排名块填充:模板 1-based r4 起(全省+17市州),Q/R 占比公式保留不动 */ |
| | | private void fillFreightRankBlock(Sheet sheet, int start, |
| | | Map<String, Double> above, Map<String, Double> below, Map<String, Double> total, |
| | | Map<String, Double> aboveYoy, Map<String, Double> belowYoy, Map<String, Double> totalYoy, |
| | | Double provAbove, Double provBelow, Double provTotal, |
| | | Double provAboveYoy, Double provBelowYoy, Double provTotalYoy) { |
| | | Row provRow = sheet.getRow(start); |
| | | if (provRow == null) provRow = sheet.createRow(start); |
| | | setFreightCell(provRow, 1, provAbove); |
| | | setFreightCell(provRow, 4, provAboveYoy); |
| | | setFreightCell(provRow, 6, provBelow); |
| | | setFreightCell(provRow, 9, provBelowYoy); |
| | | setFreightCell(provRow, 11, provTotal); |
| | | setFreightCell(provRow, 14, provTotalYoy); |
| | | int rowIdx = start + 1; |
| | | for (String city : RegionUtil.cityList()) { |
| | | Row row = sheet.getRow(rowIdx); |
| | | if (row == null) row = sheet.createRow(rowIdx); |
| | | setFreightCell(row, 1, above.get(city)); |
| | | setFreightCell(row, 2, rankOfMap(above, city)); |
| | | setFreightCell(row, 3, ratioOf(above.get(city), provAbove)); |
| | | setFreightCell(row, 4, aboveYoy.get(city)); |
| | | setFreightCell(row, 5, yoyRankOfMap(aboveYoy, city)); |
| | | setFreightCell(row, 6, below.get(city)); |
| | | setFreightCell(row, 7, rankOfMap(below, city)); |
| | | setFreightCell(row, 8, ratioOf(below.get(city), provBelow)); |
| | | setFreightCell(row, 9, belowYoy.get(city)); |
| | | setFreightCell(row, 10, yoyRankOfMap(belowYoy, city)); |
| | | setFreightCell(row, 11, total.get(city)); |
| | | setFreightCell(row, 12, rankOfMap(total, city)); |
| | | setFreightCell(row, 13, ratioOf(total.get(city), provTotal)); |
| | | setFreightCell(row, 14, totalYoy.get(city)); |
| | | setFreightCell(row, 15, yoyRankOfMap(totalYoy, city)); |
| | | rowIdx++; |
| | | } |
| | | } |
| | | |
| | | /** 周转量排名块填充:模板 1-based r4 起(全省+17市州) */ |
| | | private void fillTurnoverRankBlock(Sheet sheet, int start, |
| | | Map<String, ScaleSplitTransport> cumMap, ScaleSplitTransport province) { |
| | | if (province == null) province = new ScaleSplitTransport(); |
| | | Double provAbove = province.getAboveScaleTurnover(); |
| | | Double provBelow = province.getBelowScaleTurnover(); |
| | | Double provTotal = province.getTotalTurnover(); |
| | | Row provRow = sheet.getRow(start); |
| | | if (provRow == null) provRow = sheet.createRow(start); |
| | | setFreightCell(provRow, 1, provAbove); |
| | | setFreightCell(provRow, 4, province.getAboveScaleYoy()); |
| | | setFreightCell(provRow, 6, provBelow); |
| | | setFreightCell(provRow, 9, province.getBelowScaleYoy()); |
| | | setFreightCell(provRow, 11, provTotal); |
| | | setFreightCell(provRow, 14, province.getTotalYoy()); |
| | | setFreightShare(provRow, provAbove, provTotal); |
| | | int rowIdx = start + 1; |
| | | for (String city : RegionUtil.cityList()) { |
| | | Row row = sheet.getRow(rowIdx); |
| | | if (row == null) row = sheet.createRow(rowIdx); |
| | | ScaleSplitTransport cum = cumMap.get(city); |
| | | Double above = cum == null ? null : cum.getAboveScaleTurnover(); |
| | | Double below = cum == null ? null : cum.getBelowScaleTurnover(); |
| | | Double total = cum == null ? null : cum.getTotalTurnover(); |
| | | setFreightCell(row, 1, above); |
| | | setFreightCell(row, 2, cum == null ? null : rankOfTurnover(cumMap, city, 0)); |
| | | setFreightCell(row, 3, ratioOf(above, provAbove)); |
| | | setFreightCell(row, 4, cum == null ? null : cum.getAboveScaleYoy()); |
| | | setFreightCell(row, 5, cum == null ? null : yoyRankOfTurnover(cumMap, city, 0)); |
| | | setFreightCell(row, 6, below); |
| | | setFreightCell(row, 7, cum == null ? null : rankOfTurnover(cumMap, city, 1)); |
| | | setFreightCell(row, 8, ratioOf(below, provBelow)); |
| | | setFreightCell(row, 9, cum == null ? null : cum.getBelowScaleYoy()); |
| | | setFreightCell(row, 10, cum == null ? null : yoyRankOfTurnover(cumMap, city, 1)); |
| | | setFreightCell(row, 11, total); |
| | | setFreightCell(row, 12, cum == null ? null : rankOfTurnover(cumMap, city, 2)); |
| | | setFreightCell(row, 13, ratioOf(total, provTotal)); |
| | | setFreightCell(row, 14, cum == null ? null : cum.getTotalYoy()); |
| | | setFreightCell(row, 15, cum == null ? null : yoyRankOfTurnover(cumMap, city, 2)); |
| | | setFreightShare(row, above, total); |
| | | rowIdx++; |
| | | } |
| | | } |
| | | |
| | | /** 周转量排名 Q/R 占比:模板为缓存值,按新数据重算(规上占比 1 位小数) */ |
| | | private void setFreightShare(Row row, Double above, Double total) { |
| | | if (total != null && total > 0 && above != null) { |
| | | double share = round(above * 10.0 / total, 1); |
| | | setFreightCell(row, 16, share); |
| | | setFreightCell(row, 17, round(10.0 - share, 1)); |
| | | } else { |
| | | setFreightCell(row, 16, (Double) null); |
| | | setFreightCell(row, 17, (Double) null); |
| | | } |
| | | } |
| | | |
| | | // ==================== 数据加载 ==================== |
| | | |
| | | /** 当月数据: city -> month -> record(不含全省) */ |
| | |
| | | /** H2032规上: city -> month -> 货运量(吨) */ |
| | | private Map<String, Map<Integer, Double>> loadH2032FreightByMonth(String period) { |
| | | Map<String, Map<Integer, Double>> result = new HashMap<>(); |
| | | String yearPrefix = period.length() >= 4 ? period.substring(0, 4) : null; |
| | | List<H2032EnterpriseMonthly> list = h2032Mapper.selectList(null); |
| | | for (H2032EnterpriseMonthly record : list) { |
| | | if (record.getReportPeriod() == null || record.getReportPeriod().compareTo(period) > 0) continue; |
| | | // H2032 表同时存有往年月报,统计当年规上值时只取与报表期同年的记录,避免去年数据叠加 |
| | | if (yearPrefix != null && !record.getReportPeriod().startsWith(yearPrefix)) continue; |
| | | String city = RegionUtil.cityByCode(record.getRegionCode()); |
| | | if (city == null) continue; |
| | | int month = parseMonth(record.getReportPeriod()); |
| | |
| | | /** H2032规上累计: city -> 货运量(吨) */ |
| | | private Map<String, Double> loadH2032FreightCumulative(String period) { |
| | | Map<String, Double> result = new HashMap<>(); |
| | | String yearPrefix = period.length() >= 4 ? period.substring(0, 4) : null; |
| | | List<H2032EnterpriseMonthly> list = h2032Mapper.selectList(null); |
| | | for (H2032EnterpriseMonthly record : list) { |
| | | if (record.getReportPeriod() == null || record.getReportPeriod().compareTo(period) > 0) continue; |
| | | if (yearPrefix != null && !record.getReportPeriod().startsWith(yearPrefix)) continue; |
| | | String city = RegionUtil.cityByCode(record.getRegionCode()); |
| | | if (city == null) continue; |
| | | double freight = record.getFreightTotal() == null ? 0.0 : record.getFreightTotal(); |
| | |
| | | return rank; |
| | | } |
| | | |
| | | // ==================== 4. 生成_公路旅客分市州明细.xlsx(月报/年报) ==================== |
| | | |
| | | /** |
| | | * 公路旅客分市州明细 |
| | | * @param period 月报: 2026-06(今年1-6月);年报: 2026(全年12个月) |
| | | * @param mode month/year |
| | | */ |
| | | /** |
| | | * 生成_公路旅客分市州.xlsx:以 docs/公路旅客+能耗/输出 模板为底稿, |
| | | * 保留表头/合并/列宽/公式/样式,仅替换 2026 年 1..6 月月度列数据(客运量/周转量/个体行)。 |
| | | * 2025/2024 历史列保留模板值;累计/同比/全省行公式保留并重算。 |
| | | */ |
| | | public byte[] exportPassengerCityDetail(String period, String mode) throws Exception { |
| | | int maxMonth = monthOf(period, mode); |
| | | int currentYear = Integer.parseInt(period.substring(0, 4)); |
| | | File template = resolvePassengerTemplate("生成_公路旅客分市州.xlsx"); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheetAt(0); |
| | | fixPassengerHeaderYear(sheet, currentYear); // 表头年份动态化(不写死 2026) |
| | | Map<Integer, Map<String, PassengerAgg>> data = loadPassengerAggMap(); |
| | | Map<Integer, Map<String, double[]>> indi = loadPassengerIndividualMap(); |
| | | int fillMonths = maxMonth; // 1..N 月(N>6 时模板自动向右扩列到 12 月) |
| | | boolean extended = fillMonths > 6; |
| | | if (extended) extendMonthlyColumns(sheet, 1, currentYear, 3, sheet.getLastRowNum()); |
| | | String currentRegion = null; |
| | | for (int r = 3; r <= sheet.getLastRowNum(); r++) { // 从 R4 起:A 列合并行是区域标记(R4 全省、R8 武汉市…) |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | Cell nameCell = row.getCell(0); |
| | | Cell metricCell = row.getCell(1); |
| | | if (metricCell == null) continue; |
| | | String areaName = cellText(nameCell); |
| | | if (areaName != null && !areaName.trim().isEmpty()) { |
| | | currentRegion = "全省".equals(areaName.trim()) ? "湖北省" : RegionUtil.normalizeCityName(areaName.trim()); |
| | | } |
| | | if (currentRegion == null) continue; |
| | | String metric = cellText(metricCell); |
| | | if (metric == null) continue; |
| | | int metricIdx = cityMetricIndexOf(metric); |
| | | if (metricIdx < 0) continue; |
| | | String region = currentRegion; |
| | | for (int m = 1; m <= fillMonths; m++) { |
| | | int col = 2 + (m - 1) * 2; // C,E,G,I,K,M,O,Q,S,U,W,Y(7-12 月为扩列列) |
| | | Cell c = row.getCell(col); |
| | | boolean keepFormula = c != null && c.getCellType() == CellType.FORMULA; // 全省/累计等公式保留 |
| | | double v = passengerMonthVal(data, indi, currentYear, m, region, metricIdx); |
| | | if (!keepFormula) { |
| | | if (c == null) c = row.createCell(col); |
| | | if (v == 0.0) c.setBlank(); else c.setCellValue(round(v, 4)); |
| | | } |
| | | // 同比列:库内去年同月同比(模板 #REF! 公式替换为数值,公式行也覆写) |
| | | double lv = passengerMonthVal(data, indi, currentYear - 1, m, region, metricIdx); |
| | | Cell yc = row.getCell(col + 1); |
| | | if (yc == null) yc = row.createCell(col + 1); |
| | | if (v == 0.0 || lv == 0.0) yc.setBlank(); else yc.setCellValue(round((v - lv) / lv, 4)); |
| | | } |
| | | // 累计同比列(模板 #REF! → 库内去年 1..N 月累计同比数值) |
| | | int cumCol = extended ? 26 : 14; |
| | | Double cumYoy = passengerCumYoy(data, indi, currentYear, fillMonths, region, metricIdx); |
| | | Cell yc2 = row.getCell(cumCol + 1); |
| | | if (yc2 == null) yc2 = row.createCell(cumCol + 1); |
| | | if (cumYoy == null) yc2.setBlank(); else yc2.setCellValue(round(cumYoy, 4)); |
| | | } |
| | | recalc(wb); |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | /** 分市州表指标行:0=客运量 1=周转量 2=个体客运量 3=个体周转量;无法识别返回 -1 */ |
| | | private int cityMetricIndexOf(String metric) { |
| | | if (metric == null) return -1; |
| | | boolean indi = metric.contains("个体"); |
| | | if (metric.contains("客运量")) return indi ? 2 : 0; |
| | | if (metric.contains("周转量")) return indi ? 3 : 1; |
| | | return -1; |
| | | } |
| | | |
| | | private String cellText(Cell c) { |
| | | if (c == null) return null; |
| | | if (c.getCellType() == CellType.STRING) return c.getStringCellValue(); |
| | | if (c.getCellType() == CellType.NUMERIC) return String.valueOf(c.getNumericCellValue()); |
| | | return null; |
| | | } |
| | | |
| | | /** 个体客运量/周转量(万人/万人公里) */ |
| | | private double individualVal(Map<Integer, Map<String, double[]>> indi, int year, int month, String region, int idx) { |
| | | Map<String, double[]> mm = indi.get(year * 100 + month); |
| | | if (mm == null) return 0; |
| | | double[] arr = mm.get(region); |
| | | if (arr == null) return 0; |
| | | return idx < arr.length ? arr[idx] : 0; |
| | | } |
| | | |
| | | /** 个体客运量/周转量数据:年*100+月 -> (市州/全省 -> [个体客运量(万人), 个体周转量(万人公里)]) */ |
| | | private Map<Integer, Map<String, double[]>> loadPassengerIndividualMap() { |
| | | Map<Integer, Map<String, double[]>> data = new HashMap<>(); |
| | | for (PassengerIndividualMonthly e : passengerIndividualMapper.selectList(null)) { |
| | | 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; |
| | | double[] arr = data.computeIfAbsent(key, k -> new HashMap<>()).computeIfAbsent(city, k -> new double[2]); |
| | | arr[0] += pass; |
| | | arr[1] += turn; |
| | | double[] prov = data.get(key).computeIfAbsent("湖北省", k -> new double[2]); |
| | | prov[0] += pass; |
| | | prov[1] += turn; |
| | | } |
| | | return data; |
| | | } |
| | | |
| | | private PassengerAgg aggOf(Map<Integer, Map<String, PassengerAgg>> data, int year, int month, String region) { |
| | | Map<String, PassengerAgg> monthMap = data.get(year * 100 + month); |
| | | return monthMap == null ? null : monthMap.get(region); |
| | | } |
| | | |
| | | private Double cumOf(Map<Integer, Map<String, PassengerAgg>> data, int year, String region, int monthCount, int metricIdx) { |
| | | double sum = 0.0; |
| | | boolean has = false; |
| | | for (int m = 1; m <= monthCount; m++) { |
| | | Double v = metricValue(aggOf(data, year, m, region), metricIdx); |
| | | if (v != null) { |
| | | sum += v; |
| | | has = true; |
| | | } |
| | | } |
| | | return has ? sum : null; |
| | | } |
| | | |
| | | private Double yoy(Double cur, Double base) { |
| | | if (cur == null || base == null || base == 0.0) return null; |
| | | return round((cur - base) / base, 4); |
| | | } |
| | | |
| | | /** 指标取值(已换算为万人/万人公里/公里;无数据返回 null) */ |
| | | private Double metricValue(PassengerAgg agg, int metric) { |
| | | if (agg == null) return null; |
| | | switch (metric) { |
| | | case 0: return agg.passengerTotal / 10000.0; |
| | | case 1: return agg.passengerTotal / 10000.0; |
| | | case 2: return agg.passengerClass1 / 10000.0; |
| | | case 3: return agg.passengerClass2 / 10000.0; |
| | | case 4: return agg.passengerClass3 / 10000.0; |
| | | case 5: return agg.passengerClass4 / 10000.0; |
| | | case 6: return agg.passengerCharter / 10000.0; |
| | | case 7: |
| | | case 8: |
| | | case 9: return null; // 城市客运模块数据,暂留空 |
| | | case 10: return agg.turnoverTotal / 10000.0; |
| | | case 11: return agg.turnoverClass1 / 10000.0; |
| | | case 12: return agg.turnoverClass2 / 10000.0; |
| | | case 13: return agg.turnoverClass3 / 10000.0; |
| | | case 14: return agg.turnoverClass4 / 10000.0; |
| | | case 15: return agg.turnoverCharter / 10000.0; |
| | | case 16: return ratioOrNull(agg.turnoverTotal, agg.passengerTotal); |
| | | case 17: return ratioOrNull(agg.turnoverClass1, agg.passengerClass1); |
| | | case 18: return ratioOrNull(agg.turnoverClass2, agg.passengerClass2); |
| | | case 19: return ratioOrNull(agg.turnoverClass3, agg.passengerClass3); |
| | | case 20: return ratioOrNull(agg.turnoverClass4, agg.passengerClass4); |
| | | case 21: return ratioOrNull(agg.turnoverCharter, agg.passengerCharter); |
| | | default: return null; |
| | | } |
| | | } |
| | | |
| | | private Double ratioOrNull(double numerator, double denominator) { |
| | | if (denominator == 0.0) return null; |
| | | return numerator / denominator; |
| | | } |
| | | |
| | | /** 单月全市州企业汇总 */ |
| | | private static class PassengerAgg { |
| | | double passengerTotal; |
| | | double passengerClass1; |
| | | double passengerClass2; |
| | | double passengerClass3; |
| | | double passengerClass4; |
| | | double passengerCharter; |
| | | double turnoverTotal; |
| | | double turnoverClass1; |
| | | double turnoverClass2; |
| | | double turnoverClass3; |
| | | double turnoverClass4; |
| | | double turnoverCharter; |
| | | |
| | | void add(PassengerEnterpriseMonthly e) { |
| | | passengerTotal += nz(e.getPassengerClass1()) + nz(e.getPassengerClass2()) + nz(e.getPassengerClass3()) |
| | | + nz(e.getPassengerClass4()) + nz(e.getPassengerCharter()); |
| | | passengerClass1 += nz(e.getPassengerClass1()); |
| | | passengerClass2 += nz(e.getPassengerClass2()); |
| | | passengerClass3 += nz(e.getPassengerClass3()); |
| | | passengerClass4 += nz(e.getPassengerClass4()); |
| | | passengerCharter += nz(e.getPassengerCharter()); |
| | | turnoverTotal += nz(e.getTurnoverClass1()) + nz(e.getTurnoverClass2()) + nz(e.getTurnoverClass3()) |
| | | + nz(e.getTurnoverClass4()) + nz(e.getTurnoverCharter()); |
| | | turnoverClass1 += nz(e.getTurnoverClass1()); |
| | | turnoverClass2 += nz(e.getTurnoverClass2()); |
| | | turnoverClass3 += nz(e.getTurnoverClass3()); |
| | | turnoverClass4 += nz(e.getTurnoverClass4()); |
| | | turnoverCharter += nz(e.getTurnoverCharter()); |
| | | } |
| | | |
| | | private double nz(Double v) { |
| | | return v == null ? 0.0 : v; |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | // ==================== 中口径明细/排名/分析(H203-1 公路旅客) ==================== |
| | | |
| | | /** 加载 年*100+月 -> (市州/全省 -> 企业汇总) 数据 */ |
| | | private Map<Integer, Map<String, PassengerAgg>> loadPassengerAggMap() { |
| | | Map<Integer, Map<String, PassengerAgg>> data = new HashMap<>(); |
| | | List<PassengerEnterpriseMonthly> list = passengerMapper.selectList(null); |
| | | for (PassengerEnterpriseMonthly e : list) { |
| | | if (e.getReportPeriod() == null) continue; |
| | | String[] parts = e.getReportPeriod().split("-"); |
| | | if (parts.length != 2) continue; |
| | | int y; |
| | | int 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; |
| | | Map<String, PassengerAgg> monthMap = data.computeIfAbsent(key, k -> new HashMap<>()); |
| | | monthMap.computeIfAbsent(city, k -> new PassengerAgg()).add(e); |
| | | monthMap.computeIfAbsent("湖北省", k -> new PassengerAgg()).add(e); |
| | | } |
| | | return data; |
| | | } |
| | | |
| | | /** |
| | | * 中口径明细:客运量块 + 旅客周转量块(总/公路班线/城际城乡公交/巡游出租/网约车) |
| | | */ |
| | | /** |
| | | * 生成_中口径明细.xlsx:以样例为底稿。数据区外部引用公式(班线/公交/出租/网约车)替换为库内数值, |
| | | * 2025 年列保留模板缓存值(去年数据),内部公式(累计/同比/求和)保留并重算。 |
| | | */ |
| | | public byte[] exportPassengerMidDetail(String period, String mode) throws Exception { |
| | | int maxMonth = monthOf(period, mode); |
| | | int currentYear = Integer.parseInt(period.substring(0, 4)); |
| | | File template = resolvePassengerTemplate("生成_中口径明细.xlsx"); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheetAt(0); |
| | | fixPassengerHeaderYear(sheet, currentYear); // 表头年份动态化(不写死 2026) |
| | | Map<Integer, Map<String, double[][]>> mid = loadMidClassMap(); |
| | | int fillMonths = maxMonth; // 1..N 月(N>6 时模板自动向右扩列到 12 月) |
| | | boolean extended = fillMonths > 6; |
| | | if (extended) extendMonthlyColumns(sheet, 2, currentYear, 4, sheet.getLastRowNum()); |
| | | List<String> areas = midTemplateAreas(); |
| | | for (Row row : sheet) { |
| | | if (row == null) continue; |
| | | for (Cell c : row) { |
| | | if (c.getCellType() != CellType.FORMULA) continue; |
| | | String f = c.getCellFormula(); |
| | | if (f == null) continue; |
| | | boolean crossBook = f.contains("["); |
| | | if (!crossBook && !isYoyMonthCol(c.getColumnIndex() + 1)) continue; // 内部公式(合计/累计)保留重算;同比列 #REF! 覆写为数值 |
| | | if (crossBook && !is2026MonthCol(c.getColumnIndex() + 1)) { |
| | | keepCached(c); // 2025 年列保留模板缓存值 |
| | | continue; |
| | | } |
| | | int r = c.getRowIndex() + 1; |
| | | int col = c.getColumnIndex() + 1; |
| | | int blockOffset = -1; |
| | | boolean volume = false; |
| | | if (r >= 5 && r <= 94) { |
| | | blockOffset = r - 5; |
| | | volume = true; |
| | | } else if (r >= 99 && r <= 188) { |
| | | blockOffset = r - 99; |
| | | volume = false; |
| | | } |
| | | if (blockOffset < 0) { |
| | | keepCached(c); // r1/r2 备注等 |
| | | continue; |
| | | } |
| | | if (blockOffset % 5 == 0) { |
| | | if (isYoyMonthCol(col)) { |
| | | fillMidYoyCell(c, mid, currentYear, monthOfCol(col), areas.get(blockOffset / 5), -1, volume); |
| | | } else { |
| | | keepCached(c); // 总行内部公式(防御) |
| | | } |
| | | continue; |
| | | } |
| | | int k = blockOffset % 5; // 1=班线 2=公交 3=出租 4=网约车 |
| | | String area = areas.get(blockOffset / 5); |
| | | if (is2026MonthCol(col)) { |
| | | fillMidCell(c, mid, currentYear, monthOfCol(col), area, k, volume); |
| | | } else if (isYoyMonthCol(col)) { |
| | | fillMidYoyCell(c, mid, currentYear, monthOfCol(col), area, k, volume); |
| | | } else { |
| | | keepCached(c); // 2025 年列保留模板缓存值 |
| | | } |
| | | } |
| | | } |
| | | // 累计同比列(模板 #REF! → 库内去年 1..N 月累计同比数值) |
| | | fillMidCumYoyColumn(sheet, mid, currentYear, fillMonths, extended ? 27 : 15); |
| | | recalc(wb); |
| | | clearFormulaErrorsAll(wb); // 去年同期明细页残留 #REF! 同比公式 → 清空 |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | /** 中口径明细模板地区顺序(与 RegionUtil.CITY_LIST 一致:武汉市…神农架林区) */ |
| | | private List<String> midTemplateAreas() { |
| | | List<String> areas = new java.util.ArrayList<>(); |
| | | areas.add("湖北省"); |
| | | areas.addAll(RegionUtil.CITY_LIST); |
| | | return areas; |
| | | } |
| | | |
| | | private boolean is2026MonthCol(int col) { |
| | | return col >= 3 && col <= 25 && col % 2 == 1; |
| | | } |
| | | |
| | | /** 月同比列(1-based 偶数列 4..26) */ |
| | | private boolean isYoyMonthCol(int col) { |
| | | return col >= 4 && col <= 26 && col % 2 == 0; |
| | | } |
| | | |
| | | private int monthOfCol(int col) { |
| | | return (col - 1) / 2; |
| | | } |
| | | |
| | | /** 填中口径数据单元格:出租/网约车(3/4)显式 0,班线/公交填库内值(无值清空) */ |
| | | private void fillMidCell(Cell c, Map<Integer, Map<String, double[][]>> mid, int year, int month, String area, int k, boolean volume) { |
| | | if (k == 3 || k == 4) { |
| | | writeExplicitZero(c); // 出租/网约车暂无数据源,显式 0 |
| | | return; |
| | | } |
| | | double v = midClassVal(mid, year, month, area, k, volume); |
| | | // POI setCellValue(double) 对公式单元格只更新缓存不移除公式,必须先 setBlank 再写值 |
| | | c.setBlank(); |
| | | if (v != 0.0) c.setCellValue(round(v, 4)); |
| | | } |
| | | |
| | | /** 显式写入 0(POI setCellValue(0.0) 会转 blank,需操作底层 XML) */ |
| | | private void writeExplicitZero(Cell c) { |
| | | c.setBlank(); |
| | | if (c instanceof XSSFCell) { |
| | | try { |
| | | ((XSSFCell) c).getCTCell().setT(STCellType.N); |
| | | ((XSSFCell) c).getCTCell().setV("0"); |
| | | } catch (Exception e) { |
| | | log.warn("writeExplicitZero failed: {}", e.getMessage()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private double midClassVal(Map<Integer, Map<String, double[][]>> mid, int year, int month, String area, int k, boolean volume) { |
| | | Map<String, double[][]> mm = mid.get(year * 100 + month); |
| | | if (mm == null) return 0; |
| | | double[][] arr = mm.get(area); |
| | | if (arr == null) return 0; |
| | | if (k < 0 || k >= arr.length) return 0; |
| | | return volume ? arr[k][0] : arr[k][1]; |
| | | } |
| | | |
| | | /** 公式单元格替换为缓存数值(避免跨簿引用断链;非数值缓存置空) */ |
| | | private void keepCached(Cell c) { |
| | | try { |
| | | double v = 0.0; |
| | | boolean has = false; |
| | | if (c.getCachedFormulaResultType() == CellType.NUMERIC) { |
| | | v = c.getNumericCellValue(); |
| | | has = true; |
| | | } |
| | | c.setBlank(); |
| | | if (has && v != 0.0) c.setCellValue(v); |
| | | } catch (Exception e) { |
| | | try { c.setBlank(); } catch (Exception ignore) { } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 中口径分类月度数据:年*100+月 -> (市州/全省 -> double[5][2]) |
| | | * 维度 0=总量 1=公路班线(h2031+个体) 2=城际城乡公交(cityBus) 3=巡游出租 4=网约车;[客运量(万人), 周转量(万人公里)] |
| | | */ |
| | | private Map<Integer, Map<String, double[][]>> loadMidClassMap() { |
| | | Map<Integer, Map<String, double[][]>> mid = new HashMap<>(); |
| | | for (PassengerEnterpriseMonthly e : passengerMapper.selectList(null)) { |
| | | 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.getPassengerClass1()) + nz(e.getPassengerClass2()) + nz(e.getPassengerClass3()) |
| | | + 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); |
| | | } |
| | | for (CityBusMonthly b : cityBusMapper.selectList(null)) { |
| | | if (b.getReportPeriod() == null || b.getCity() == null) continue; |
| | | String[] parts = b.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.normalizeCityName(b.getCity()); |
| | | if (city == null) continue; |
| | | int key = y * 100 + m; |
| | | addMidClass(mid, key, city, 2, nz(b.getPassengerChengxiang()), nz(b.getTurnoverChengxiang())); |
| | | addMidClass(mid, key, "湖北省", 2, nz(b.getPassengerChengxiang()), nz(b.getTurnoverChengxiang())); |
| | | } |
| | | return mid; |
| | | } |
| | | |
| | | /** 写入分类值并累加总量(维度0) */ |
| | | private void addMidClass(Map<Integer, Map<String, double[][]>> mid, int key, String city, int k, double pass, double turn) { |
| | | double[][] arr = mid.computeIfAbsent(key, kk -> new HashMap<>()).computeIfAbsent(city, kk -> new double[5][2]); |
| | | arr[k][0] += pass; |
| | | arr[k][1] += turn; |
| | | arr[0][0] += pass; |
| | | arr[0][1] += turn; |
| | | } |
| | | |
| | | private int writeMidBlock(Sheet sheet, Map<Integer, Map<String, PassengerAgg>> data, int[] years, |
| | | int currentYear, int maxMonth, int startRow, String title, boolean volume) { |
| | | Row titleRow = sheet.createRow(startRow); |
| | | titleRow.createCell(0).setCellValue(title); |
| | | Row noteRow = sheet.createRow(startRow + 1); |
| | | noteRow.createCell(0).setCellValue("注:中口径由公路班线、城际城乡公交、城际城乡出租、城际城乡网约车四部分构成"); |
| | | Row header = sheet.createRow(startRow + 2); |
| | | header.createCell(0).setCellValue("地区 名称"); |
| | | header.createCell(1).setCellValue("指标"); |
| | | int col = 2; |
| | | for (int y : years) { |
| | | int monthCount = (y == currentYear) ? maxMonth : 12; |
| | | for (int m = 1; m <= monthCount; m++) { |
| | | header.createCell(col++).setCellValue(y + "年" + m + "月"); |
| | | header.createCell(col++).setCellValue(m + "月与去年同比"); |
| | | } |
| | | header.createCell(col++).setCellValue(y + "年累计"); |
| | | header.createCell(col++).setCellValue("累计与去年同比"); |
| | | } |
| | | sheet.createRow(startRow + 3); |
| | | |
| | | String[] metrics = volume ? new String[]{ |
| | | "总客运量(万人次)", |
| | | "其中:公路班线客运量(万人次)", |
| | | "城际城乡公交客运量(万人次)", |
| | | "城际城乡巡游出租客运量(万人次)", |
| | | "城际城乡网约车客运量(万人次)" |
| | | } : new String[]{ |
| | | "总旅客周转量(万人公里)", |
| | | "其中:公路班线旅客周转量(万人公里)", |
| | | "城际城乡公交旅客周转量(万人公里)", |
| | | "城际城乡巡游出租旅客周转量(万人公里)", |
| | | "城际城乡网约车旅客周转量(万人公里)" |
| | | }; |
| | | |
| | | int rowIdx = startRow + 4; |
| | | List<String> regions = new java.util.ArrayList<>(); |
| | | regions.add("湖北省"); |
| | | regions.addAll(RegionUtil.cityList()); |
| | | for (String region : regions) { |
| | | String label = "湖北省".equals(region) ? "全省" : RegionUtil.shortName(region); |
| | | for (int i = 0; i < metrics.length; i++) { |
| | | Row row = sheet.createRow(rowIdx++); |
| | | row.createCell(0).setCellValue(label); |
| | | row.createCell(1).setCellValue(metrics[i]); |
| | | col = 2; |
| | | for (int y : years) { |
| | | int monthCount = (y == currentYear) ? maxMonth : 12; |
| | | for (int m = 1; m <= monthCount; m++) { |
| | | Double cur = midMetricValue(aggOf(data, y, m, region), i, volume); |
| | | setNumeric(row, col++, cur == null ? null : round(cur, 4)); |
| | | Double base = midMetricValue(aggOf(data, y - 1, m, region), i, volume); |
| | | setNumeric(row, col++, yoy(cur, base)); |
| | | } |
| | | Double cum = midCumOf(data, y, region, monthCount, i, volume); |
| | | setNumeric(row, col++, cum == null ? null : round(cum, 4)); |
| | | Double cumBase = midCumOf(data, y - 1, region, monthCount, i, volume); |
| | | setNumeric(row, col++, yoy(cum, cumBase)); |
| | | } |
| | | } |
| | | } |
| | | return rowIdx; |
| | | } |
| | | |
| | | /** 中口径指标取值:0=总量 1=公路班线 2-4=公交/出租/网约车(城市客运模块未接入,暂空) */ |
| | | private Double midMetricValue(PassengerAgg agg, int metric, boolean volume) { |
| | | if (agg == null) return null; |
| | | if (volume) { |
| | | switch (metric) { |
| | | case 0: return agg.passengerTotal / 10000.0; |
| | | case 1: return agg.passengerTotal / 10000.0; |
| | | default: return null; |
| | | } |
| | | } |
| | | switch (metric) { |
| | | case 0: return agg.turnoverTotal / 10000.0; |
| | | case 1: return agg.turnoverTotal / 10000.0; |
| | | default: return null; |
| | | } |
| | | } |
| | | |
| | | private Double midCumOf(Map<Integer, Map<String, PassengerAgg>> data, int year, String region, |
| | | int monthCount, int metricIdx, boolean volume) { |
| | | double sum = 0.0; |
| | | boolean has = false; |
| | | for (int m = 1; m <= monthCount; m++) { |
| | | Double v = midMetricValue(aggOf(data, year, m, region), metricIdx, volume); |
| | | if (v != null) { |
| | | sum += v; |
| | | has = true; |
| | | } |
| | | } |
| | | return has ? sum : null; |
| | | } |
| | | |
| | | /** |
| | | * 中口径排名:左块=当月(客运量/周转量及排名/同比/增速排名),右块=1-N月累计(同结构) |
| | | */ |
| | | /** |
| | | * 生成_中口径排名.xlsx:单块累计(选 1 个月=当月,选 1-6 月=累计),RANK/SUM 公式保留。 |
| | | */ |
| | | public byte[] exportPassengerMidRank(String period, String mode) throws Exception { |
| | | int maxMonth = monthOf(period, mode); |
| | | int currentYear = Integer.parseInt(period.substring(0, 4)); |
| | | File template = resolvePassengerTemplate("生成_中口径排名.xlsx"); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheetAt(0); |
| | | Map<Integer, Map<String, double[][]>> mid = loadMidClassMap(); |
| | | Row title = sheet.getRow(0); |
| | | if (title != null && title.getCell(0) != null) { |
| | | if (maxMonth == 12) { |
| | | title.getCell(0).setCellValue(currentYear + "年1-12月全省分市州累计完成道路客运生产情况"); |
| | | } else if (maxMonth == 1) { |
| | | title.getCell(0).setCellValue(currentYear + "年1月全省分市州完成道路客运生产情况"); |
| | | } else { |
| | | title.getCell(0).setCellValue(currentYear + "年1-" + maxMonth + "月全省分市州累计完成道路客运生产情况"); |
| | | } |
| | | } |
| | | // 右块(当月)标题 K1 |
| | | if (title != null && title.getCell(10) != null) { |
| | | title.getCell(10).setCellValue(currentYear + "年" + maxMonth + "月全省分市州完成道路客运生产情况"); |
| | | } |
| | | // 全省行 r3:B3/F3 为 SUM 公式保留;D3/H3 同比填值(0-based 列 3/7) |
| | | setValOrBlank(sheet, 2, 3, yoyOf(midCumClass(mid, currentYear, "湖北省", maxMonth, 0, true), |
| | | midCumClass(mid, currentYear - 1, "湖北省", maxMonth, 0, true))); |
| | | setValOrBlank(sheet, 2, 7, yoyOf(midCumClass(mid, currentYear, "湖北省", maxMonth, 0, false), |
| | | midCumClass(mid, currentYear - 1, "湖北省", maxMonth, 0, false))); |
| | | // 右块全省行 N3/R3:当月同比(L3/P3 为 SUM 公式保留) |
| | | setValOrBlank(sheet, 2, 13, yoyOf(midClassVal(mid, currentYear, maxMonth, "湖北省", 0, true), |
| | | midClassVal(mid, currentYear - 1, maxMonth, "湖北省", 0, true))); |
| | | setValOrBlank(sheet, 2, 17, yoyOf(midClassVal(mid, currentYear, maxMonth, "湖北省", 0, false), |
| | | midClassVal(mid, currentYear - 1, maxMonth, "湖北省", 0, false))); |
| | | // 市州行 r4-20:B/F 累计值、D/H 同比;C/E/G/I 排名公式保留 |
| | | List<String> cities = RegionUtil.CITY_LIST; |
| | | for (int i = 0; i < cities.size(); i++) { |
| | | String city = cities.get(i); |
| | | int r0 = 3 + i; |
| | | double pass = midCumClass(mid, currentYear, city, maxMonth, 0, true); |
| | | double turn = midCumClass(mid, currentYear, city, maxMonth, 0, false); |
| | | double lastPass = midCumClass(mid, currentYear - 1, city, maxMonth, 0, true); |
| | | double lastTurn = midCumClass(mid, currentYear - 1, city, maxMonth, 0, false); |
| | | setValOrBlank(sheet, r0, 1, pass == 0 ? null : round(pass, 4)); |
| | | setValOrBlank(sheet, r0, 3, yoyOf(pass, lastPass)); |
| | | setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4)); |
| | | setValOrBlank(sheet, r0, 7, yoyOf(turn, lastTurn)); |
| | | // 右块当月:L/P 值、N/R 当月同比(M/O/Q/S 排名公式保留) |
| | | double mPass = midClassVal(mid, currentYear, maxMonth, city, 0, true); |
| | | double mTurn = midClassVal(mid, currentYear, maxMonth, city, 0, false); |
| | | double mLastPass = midClassVal(mid, currentYear - 1, maxMonth, city, 0, true); |
| | | double mLastTurn = midClassVal(mid, currentYear - 1, maxMonth, city, 0, false); |
| | | setValOrBlank(sheet, r0, 11, mPass == 0 ? null : round(mPass, 4)); |
| | | setValOrBlank(sheet, r0, 13, mPass == 0 ? null : yoyOf(mPass, mLastPass)); |
| | | setValOrBlank(sheet, r0, 15, mTurn == 0 ? null : round(mTurn, 4)); |
| | | setValOrBlank(sheet, r0, 17, mTurn == 0 ? null : yoyOf(mTurn, mLastTurn)); |
| | | } |
| | | recalc(wb); |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | /** 写数值或清空(公式单元格保留不动) */ |
| | | private void setValOrBlank(Sheet sheet, int r0, int c0, Double v) { |
| | | Row row = sheet.getRow(r0); |
| | | if (row == null) return; |
| | | Cell c = row.getCell(c0); |
| | | if (c == null) return; |
| | | if (c.getCellType() == CellType.FORMULA) return; |
| | | if (v == null || v == 0.0) c.setBlank(); else c.setCellValue(v); |
| | | } |
| | | |
| | | private Double yoyOf(double cur, double base) { |
| | | if (base == 0.0) return null; |
| | | return round((cur - base) / base, 4); |
| | | } |
| | | |
| | | /** 中口径分类累计(1..monthCount 月求和) */ |
| | | private double midCumClass(Map<Integer, Map<String, double[][]>> mid, int year, String area, int monthCount, int k, boolean volume) { |
| | | double sum = 0.0; |
| | | boolean has = false; |
| | | for (int m = 1; m <= monthCount; m++) { |
| | | Map<String, double[][]> mm = mid.get(year * 100 + m); |
| | | if (mm == null) continue; |
| | | double[][] arr = mm.get(area); |
| | | if (arr == null) continue; |
| | | sum += volume ? arr[k][0] : arr[k][1]; |
| | | has = true; |
| | | } |
| | | return has ? sum : 0.0; |
| | | } |
| | | |
| | | /** |
| | | * 生成_中口径分析.xlsx:以样例为底稿,填累计客运量/周转量/同比,占比公式保留。 |
| | | */ |
| | | public byte[] exportPassengerMidAnalysis(String period, String mode) throws Exception { |
| | | int maxMonth = monthOf(period, mode); |
| | | int currentYear = Integer.parseInt(period.substring(0, 4)); |
| | | File template = resolvePassengerTemplate("生成_中口径分析.xlsx"); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheetAt(0); |
| | | Map<Integer, Map<String, double[][]>> mid = loadMidClassMap(); |
| | | Row title = sheet.getRow(0); |
| | | if (title != null && title.getCell(0) != null) { |
| | | title.getCell(0).setCellValue(currentYear + "年" + cumRange(maxMonth) + "中口径客运量及周转量"); |
| | | } |
| | | // r3=总 r4=班线 r5=公交 r6=出租 r7=网约车(1-based);列 B/C/F/G 填值,D/H 占比公式保留 |
| | | String[] names = {"总客运量", "公路班线", "城际城乡公交", "城际城乡巡游出租", "城际城乡网约车"}; |
| | | int[] dims = {0, 1, 2, 3, 4}; |
| | | for (int i = 0; i < names.length; i++) { |
| | | int r0 = 2 + i; |
| | | double pass = midCumClass(mid, currentYear, "湖北省", maxMonth, dims[i], true); |
| | | double turn = midCumClass(mid, currentYear, "湖北省", maxMonth, dims[i], false); |
| | | double lastPass = midCumClass(mid, currentYear - 1, "湖北省", maxMonth, dims[i], true); |
| | | double lastTurn = midCumClass(mid, currentYear - 1, "湖北省", maxMonth, dims[i], false); |
| | | setValOrBlank(sheet, r0, 1, pass == 0 ? null : round(pass, 4)); |
| | | setValOrBlank(sheet, r0, 2, yoyOf(pass, lastPass)); |
| | | setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4)); |
| | | setValOrBlank(sheet, r0, 6, yoyOf(turn, lastTurn)); |
| | | } |
| | | // 下半块(当月):标题行9、数据行11-15(0-based 8、10-14) |
| | | Row bottomTitle = sheet.getRow(8); |
| | | if (bottomTitle != null && bottomTitle.getCell(0) != null) { |
| | | bottomTitle.getCell(0).setCellValue(currentYear + "年" + maxMonth + "月中口径客运量及周转量"); |
| | | } |
| | | for (int i = 0; i < names.length; i++) { |
| | | int r0 = 10 + i; |
| | | double pass = midClassVal(mid, currentYear, maxMonth, "湖北省", dims[i], true); |
| | | double turn = midClassVal(mid, currentYear, maxMonth, "湖北省", dims[i], false); |
| | | double lastPass = midClassVal(mid, currentYear - 1, maxMonth, "湖北省", dims[i], true); |
| | | double lastTurn = midClassVal(mid, currentYear - 1, maxMonth, "湖北省", dims[i], false); |
| | | setValOrBlank(sheet, r0, 1, pass == 0 ? null : round(pass, 4)); |
| | | setValOrBlank(sheet, r0, 2, pass == 0 ? null : yoyOf(pass, lastPass)); |
| | | setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4)); |
| | | setValOrBlank(sheet, r0, 6, turn == 0 ? null : yoyOf(turn, lastTurn)); |
| | | } |
| | | // 下半块占比公式由样例指向上半块(B$3/F$3),改写为本块总行(B$11/F$11),行12-15 |
| | | for (int i = 1; i < names.length; i++) { |
| | | int r0 = 10 + i; |
| | | Row rr = sheet.getRow(r0); |
| | | if (rr == null) continue; |
| | | Cell dc = rr.getCell(3); |
| | | if (dc != null && dc.getCellType() == CellType.FORMULA) { |
| | | dc.setCellFormula("B" + (r0 + 1) + "/B$11"); |
| | | } |
| | | Cell hc = rr.getCell(7); |
| | | if (hc != null && hc.getCellType() == CellType.FORMULA) { |
| | | hc.setCellFormula("F" + (r0 + 1) + "/F$11"); |
| | | } |
| | | } |
| | | recalc(wb); |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | private double nvlOf(Double v) { |
| | | return v == null ? Double.NaN : v; |
| | | } |
| | | |
| | | private Double numOrNull(double v) { |
| | | return Double.isNaN(v) ? null : v; |
| | | } |
| | | |
| | | /** 降序排名(无数据 NaN 排最后),返回各下标对应名次 */ |
| | | private int[] rankValues(double[] values) { |
| | | Integer[] order = new Integer[values.length]; |
| | | for (int i = 0; i < values.length; i++) order[i] = i; |
| | | java.util.Arrays.sort(order, (a, b) -> { |
| | | boolean na = Double.isNaN(values[a]); |
| | | boolean nb = Double.isNaN(values[b]); |
| | | if (na && nb) return 0; |
| | | if (na) return 1; |
| | | if (nb) return -1; |
| | | return Double.compare(values[b], values[a]); |
| | | }); |
| | | int[] ranks = new int[values.length]; |
| | | for (int i = 0; i < order.length; i++) ranks[order[i]] = i + 1; |
| | | return ranks; |
| | | } |
| | | |
| | | // ==================== 5. 生成_能运汇总表(H204 道路货运车辆能源消耗情况) ==================== |
| | | |
| | | /** 燃料类型码 -> 周转量单耗折算系数(千克标准煤折算) */ |
| | | private static final Map<String, Double> ENERGY_FUEL_COEFF = new HashMap<>(); |
| | | |
| | | static { |
| | | ENERGY_FUEL_COEFF.put("01", 0.73 * 1.4714); // 汽油 |
| | | ENERGY_FUEL_COEFF.put("02", 0.86 * 1.4571); // 柴油 |
| | | ENERGY_FUEL_COEFF.put("03", 1.7572); // 压缩天然气 |
| | | ENERGY_FUEL_COEFF.put("04", 1.7572); // 液化天然气 |
| | | ENERGY_FUEL_COEFF.put("07", 0.1229); // 电动 |
| | | ENERGY_FUEL_COEFF.put("08", 0.3329 * 12.1951); // 燃料电池(氢气) |
| | | } |
| | | |
| | | /** 燃料码 -> 汇总表行名(与模板行顺序一致) */ |
| | | private static final String[][] ENERGY_FUEL_ROWS = { |
| | | {"柴油车", "02"}, |
| | | {"汽油车", "01"}, |
| | | {"液化天然气车", "04"}, |
| | | {"压缩天然气车", "03"}, |
| | | {"纯电动车", "07"}, |
| | | {"燃料电池车", "08"} |
| | | }; |
| | | |
| | | /** |
| | | * 能运汇总表:按燃料类型分组汇总,全省 + 各市州各一张 sheet |
| | | */ |
| | | public byte[] exportEnergySummary(String period) throws Exception { |
| | | List<EnergyVehicleQuarterly> list = energyMapper.selectList( |
| | | new LambdaQueryWrapper<EnergyVehicleQuarterly>() |
| | | .eq(EnergyVehicleQuarterly::getReportPeriod, period)); |
| | | int quarter = (parseMonth(period) + 2) / 3; |
| | | String[] QUARTER_CN = {"一", "二", "三", "四"}; |
| | | String title = period.substring(0, 4) + "年第" + QUARTER_CN[quarter - 1] + "季度能耗汇总情况表"; |
| | | |
| | | Map<String, FuelAgg> province = new HashMap<>(); |
| | | Map<String, Map<String, FuelAgg>> cityMap = new HashMap<>(); |
| | | for (EnergyVehicleQuarterly e : list) { |
| | | String code = e.getFuelTypeCode(); |
| | | if (code == null || code.trim().isEmpty()) continue; |
| | | province.computeIfAbsent(code.trim(), k -> new FuelAgg()).add(e); |
| | | String city = RegionUtil.cityByCode(e.getRegionCode()); |
| | | if (city != null) { |
| | | cityMap.computeIfAbsent(city, k -> new HashMap<>()) |
| | | .computeIfAbsent(code.trim(), k2 -> new FuelAgg()).add(e); |
| | | } |
| | | } |
| | | |
| | | // 以 模板_能运汇总表 .xlsx 为底稿:保留标题/表头/合并/公式/列宽/样式,仅替换数据区数值 |
| | | File template = resolveAnyTemplate(energyTemplateDir, "生成_能运汇总表.xlsx"); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet base = wb.getSheetAt(0); |
| | | base.getRow(0).getCell(0).setCellValue(title); |
| | | fillEnergyTemplateSheet(base, province); |
| | | for (String city : RegionUtil.cityList()) { |
| | | Map<String, FuelAgg> agg = cityMap.get(city); |
| | | if (agg == null || agg.isEmpty()) continue; |
| | | Sheet s = wb.cloneSheet(0); |
| | | wb.setSheetName(wb.getSheetIndex(s), RegionUtil.shortName(city) + "能耗汇总"); |
| | | s.getRow(0).getCell(0).setCellValue(title); |
| | | fillEnergyTemplateSheet(s, agg); |
| | | } |
| | | recalc(wb); |
| | | clearFormulaErrorsAll(wb); // 无去年数据的同比/增速公式 #DIV/0! → 清空 |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | /** 能耗汇总模板填充:1-based 第3~8行为 柴油/汽油/液化天然/压缩天然/纯电/燃料电池;D/J/L/M 为公式列,仅填数值列 */ |
| | | private void fillEnergyTemplateSheet(Sheet sheet, Map<String, FuelAgg> data) { |
| | | for (int i = 0; i < ENERGY_FUEL_ROWS.length; i++) { |
| | | Row row = sheet.getRow(2 + i); |
| | | if (row == null) row = sheet.createRow(2 + i); |
| | | FuelAgg agg = data.get(ENERGY_FUEL_ROWS[i][1]); |
| | | if (agg == null || agg.count == 0) { |
| | | setEnergyValue(row, 1, null); |
| | | setEnergyValue(row, 2, null); |
| | | setEnergyValue(row, 4, null); |
| | | setEnergyValue(row, 5, null); |
| | | setEnergyValue(row, 6, null); |
| | | setEnergyValue(row, 7, null); |
| | | setEnergyValue(row, 8, null); |
| | | setEnergyValue(row, 10, null); |
| | | continue; |
| | | } |
| | | setEnergyValue(row, 1, (double) agg.count); |
| | | setEnergyValue(row, 2, agg.totalTonnage); |
| | | setEnergyValue(row, 4, agg.totalMileage); |
| | | setEnergyValue(row, 5, agg.loadedMileage); |
| | | setEnergyValue(row, 6, agg.emptyMileage); |
| | | setEnergyValue(row, 7, agg.freight); |
| | | setEnergyValue(row, 8, agg.turnover); |
| | | setEnergyValue(row, 10, agg.fuelConsumption); |
| | | } |
| | | } |
| | | |
| | | /** 写能耗数值列:有值写入,无值清空模板样例;公式列(D/J/L/M)不动 */ |
| | | private void setEnergyValue(Row row, int colIdx, Double v) { |
| | | Cell c = row.getCell(colIdx); |
| | | if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) return; |
| | | if (c == null) c = row.createCell(colIdx); |
| | | if (v == null) c.setBlank(); else c.setCellValue(v); |
| | | } |
| | | |
| | | /** 能耗汇总:按燃料类型聚合 */ |
| | | private static class FuelAgg { |
| | | int count; |
| | | double totalTonnage; |
| | | double totalMileage; |
| | | double loadedMileage; |
| | | double emptyMileage; |
| | | double freight; |
| | | double turnover; |
| | | double fuelConsumption; |
| | | |
| | | void add(EnergyVehicleQuarterly e) { |
| | | count++; |
| | | totalTonnage += nz(e.getMarkedTonnage()); |
| | | totalMileage += nz(e.getTotalMileage()); |
| | | loadedMileage += nz(e.getLoadedMileage()); |
| | | emptyMileage += nz(e.getEmptyMileage()); |
| | | freight += nz(e.getFreight()); |
| | | turnover += nz(e.getTurnover()); |
| | | fuelConsumption += nz(e.getFuelConsumption()); |
| | | } |
| | | |
| | | private double nz(Double v) { |
| | | return v == null ? 0.0 : v; |
| | | } |
| | | } |
| | | |
| | | // ==================== 通用辅助 ==================== |
| | | |
| | | private int parseMonth(String period) { |
| | | return Integer.parseInt(period.split("-")[1]); |
| | | } |
| | | |
| | | private int monthOf(String period, String mode) { |
| | | return "year".equalsIgnoreCase(mode) ? 12 : parseMonth(period); |
| | | } |
| | | |
| | | /** 累计区间文案:1 月显示 "1月",其余显示 "1-N月"(避免出现 "1-1月") */ |
| | | private String cumRange(int month) { |
| | | return month <= 1 ? "1月" : "1-" + month + "月"; |
| | | } |
| | | |
| | | /** 旅客分市州/中口径明细模板表头年份动态化:把 "2026年1月"…"2026年6月"、"2026年累计" 的年份换成所选年 */ |
| | | private void fixPassengerHeaderYear(Sheet sheet, int year) { |
| | | java.util.regex.Pattern p = java.util.regex.Pattern.compile("^\\d{4}年"); |
| | | for (int r = 1; r <= 3; r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | for (Cell cell : row) { |
| | | if (cell.getCellType() != CellType.STRING) continue; |
| | | String v = cell.getStringCellValue(); |
| | | if (v == null || v.isEmpty()) continue; |
| | | String nv = p.matcher(v).replaceFirst(year + "年"); |
| | | if (!nv.equals(v)) cell.setCellValue(nv); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private String pad(int m) { |
| | |
| | | sheet.setColumnWidth(1, 22 * 256); |
| | | } |
| | | |
| | | private byte[] toBytes(XSSFWorkbook wb) throws Exception { |
| | | |
| | | // ==================== 城市客运(公交)分市州明细 ==================== |
| | | |
| | | /** 生成_城市公交客运量分市州明细.xlsx(以 城市公交_模板.xlsx 为底稿,保留模板格式与公式) */ |
| | | public byte[] exportCityBusDetail(String period, String mode) throws Exception { |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, mode); |
| | | return exportCityByTemplate("生成_城市公交客运量分市州明细.xlsx", period, mode, |
| | | loadCityBusByMonth(year + "-", month), |
| | | loadCityBusByMonth((year - 1) + "-", month)); |
| | | } |
| | | /** 加载某年 1..limit 月城市公交企业数据,按 月 → 市州 → [客运量,周转量,城市内客运量,城市内周转量] 汇总;"全省" 为全部市州合计 */ |
| | | private Map<Integer, Map<String, double[]>> loadCityBusByMonth(String yearPrefix, int limit) { |
| | | Map<Integer, Map<String, double[]>> monthCity = new java.util.LinkedHashMap<>(); |
| | | List<CityBusMonthly> rows = cityBusMapper.selectList( |
| | | new LambdaQueryWrapper<CityBusMonthly>() |
| | | .likeRight(CityBusMonthly::getReportPeriod, yearPrefix)); |
| | | for (CityBusMonthly r : rows) { |
| | | int m = monthOf(r.getReportPeriod()); |
| | | if (m < 1 || m > limit) continue; |
| | | String city = r.getCity() == null ? "未知" : r.getCity(); |
| | | Map<String, double[]> cityMap = monthCity.computeIfAbsent(m, k -> new HashMap<>()); |
| | | double[] arr = cityMap.computeIfAbsent(city, k -> new double[4]); |
| | | arr[0] += nz(r.getPassengerVolume()); |
| | | arr[1] += nz(r.getTurnover()); |
| | | arr[2] += nz(r.getPassengerCity()); |
| | | arr[3] += nz(r.getTurnoverCity()); |
| | | double[] prov = cityMap.computeIfAbsent("全省", k -> new double[4]); |
| | | prov[0] += nz(r.getPassengerVolume()); |
| | | prov[1] += nz(r.getTurnover()); |
| | | prov[2] += nz(r.getPassengerCity()); |
| | | prov[3] += nz(r.getTurnoverCity()); |
| | | } |
| | | return monthCity; |
| | | } |
| | | |
| | | private int monthOf(String reportPeriod) { |
| | | if (reportPeriod == null) return 0; |
| | | String[] parts = reportPeriod.split("-"); |
| | | if (parts.length < 2) return 0; |
| | | try { |
| | | return Integer.parseInt(parts[1]); |
| | | } catch (Exception e) { |
| | | return 0; |
| | | } |
| | | } |
| | | |
| | | private double cityBusVal(Map<Integer, Map<String, double[]>> monthCity, String area, int m, int idx) { |
| | | Map<String, double[]> cityMap = monthCity.get(m); |
| | | if (cityMap == null) return 0; |
| | | double[] arr = cityMap.get(area); |
| | | if (arr == null) return 0; |
| | | return idx < arr.length ? arr[idx] : 0; |
| | | } |
| | | |
| | | // ==================== 城市客运(巡游出租)分市州明细 ==================== |
| | | |
| | | /** 生成_巡游出租客运量分市州明细.xlsx(以 巡游出租_模板.xlsx 为底稿,保留模板格式与公式) */ |
| | | public byte[] exportCityTaxiDetail(String period, String mode) throws Exception { |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, mode); |
| | | return exportCityByTemplate("生成_巡游出租客运量分市州明细.xlsx", period, mode, |
| | | loadCityTaxiByMonth(year + "-", month), |
| | | loadCityTaxiByMonth((year - 1) + "-", month)); |
| | | } |
| | | /** 加载某年 1..limit 月巡游出租数据,按 月 → 市州 → [客运量,周转量,城市内客运量,城市内周转量] 汇总;"全省" 为全部市州合计 */ |
| | | private Map<Integer, Map<String, double[]>> loadCityTaxiByMonth(String yearPrefix, int limit) { |
| | | Map<Integer, Map<String, double[]>> monthCity = new java.util.LinkedHashMap<>(); |
| | | List<CityTaxiMonthly> rows = cityTaxiMapper.selectList( |
| | | new LambdaQueryWrapper<CityTaxiMonthly>() |
| | | .likeRight(CityTaxiMonthly::getReportPeriod, yearPrefix)); |
| | | for (CityTaxiMonthly r : rows) { |
| | | int m = monthOf(r.getReportPeriod()); |
| | | if (m < 1 || m > limit) continue; |
| | | String city = r.getCity() == null ? "未知" : r.getCity(); |
| | | Map<String, double[]> cityMap = monthCity.computeIfAbsent(m, k -> new HashMap<>()); |
| | | double[] arr = cityMap.computeIfAbsent(city, k -> new double[4]); |
| | | arr[0] += nz(r.getPassengerVolume()); |
| | | arr[1] += nz(r.getTurnover()); |
| | | arr[2] += nz(r.getPassengerCity()); |
| | | arr[3] += nz(r.getTurnoverCity()); |
| | | double[] prov = cityMap.computeIfAbsent("全省", k -> new double[4]); |
| | | prov[0] += nz(r.getPassengerVolume()); |
| | | prov[1] += nz(r.getTurnover()); |
| | | prov[2] += nz(r.getPassengerCity()); |
| | | prov[3] += nz(r.getTurnoverCity()); |
| | | } |
| | | return monthCity; |
| | | } |
| | | |
| | | private double cityTaxiVal(Map<Integer, Map<String, double[]>> monthCity, String area, int m, int idx) { |
| | | Map<String, double[]> cityMap = monthCity.get(m); |
| | | if (cityMap == null) return 0; |
| | | double[] arr = cityMap.get(area); |
| | | if (arr == null) return 0; |
| | | return idx < arr.length ? arr[idx] : 0; |
| | | } |
| | | |
| | | // ==================== 城市客运(轨道/轮渡)分市州明细 ==================== |
| | | |
| | | /** 生成_轨道轮渡客运量分市州明细.xlsx(以 轨道、轮渡_模板.xlsx 为底稿,轨道=全省/武汉/黄石,轮渡=全省/武汉) */ |
| | | public byte[] exportCityRailFerryDetail(String period, String mode) throws Exception { |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, mode); |
| | | Map<Integer, Map<String, double[]>> monthCity = loadCityRailFerryByMonth(year + "-", month); |
| | | File template = resolveTemplate("生成_轨道轮渡客运量分市州明细.xlsx"); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheetAt(0); |
| | | dynamicTitle(sheet, year, month); // 标题按报表期动态化(不写死年份/月份) |
| | | // 轨道:客运量行 4(全省)/6(武汉)/8(黄石),周转量行为 +1;指标 0=轨道客运量 1=轨道周转量 |
| | | fillRailFerryBlock(sheet, monthCity, month, new String[][]{{"全省", "3"}, {"武汉市", "5"}, {"黄石市", "7"}}, 0); |
| | | // 轮渡:客运量行 13(全省)/15(武汉);指标 2=轮渡客运量 3=轮渡周转量 |
| | | fillRailFerryBlock(sheet, monthCity, month, new String[][]{{"全省", "12"}, {"武汉市", "14"}}, 2); |
| | | recalc(wb); |
| | | clearFormulaErrorCells(sheet, 3, sheet.getLastRowNum(), 1, 39); |
| | | fixCityMonthHeaderText(sheet); |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | /** 加载某年 1..limit 月轨道/轮渡数据:月 → 市州 → [轨道客运量,轨道周转量,轮渡客运量,轮渡周转量],含"全省" */ |
| | | private Map<Integer, Map<String, double[]>> loadCityRailFerryByMonth(String yearPrefix, int limit) { |
| | | Map<Integer, Map<String, double[]>> monthCity = new java.util.LinkedHashMap<>(); |
| | | List<CityBusMonthly> rows = cityBusMapper.selectList( |
| | | new LambdaQueryWrapper<CityBusMonthly>().likeRight(CityBusMonthly::getReportPeriod, yearPrefix)); |
| | | for (CityBusMonthly r : rows) { |
| | | int m = monthOf(r.getReportPeriod()); |
| | | if (m < 1 || m > limit) continue; |
| | | String city = r.getCity() == null ? "未知" : r.getCity(); |
| | | Map<String, double[]> cityMap = monthCity.computeIfAbsent(m, k -> new HashMap<>()); |
| | | double[] arr = cityMap.computeIfAbsent(city, k -> new double[4]); |
| | | arr[0] += nz(r.getRailPassengerVolume()); |
| | | arr[1] += nz(r.getRailTurnover()); |
| | | arr[2] += nz(r.getFerryPassengerVolume()); |
| | | arr[3] += nz(r.getFerryTurnover()); |
| | | double[] prov = cityMap.computeIfAbsent("全省", k -> new double[4]); |
| | | prov[0] += nz(r.getRailPassengerVolume()); |
| | | prov[1] += nz(r.getRailTurnover()); |
| | | prov[2] += nz(r.getFerryPassengerVolume()); |
| | | prov[3] += nz(r.getFerryTurnover()); |
| | | } |
| | | return monthCity; |
| | | } |
| | | |
| | | /** 填充轨道/轮渡块:baseIdx 为客运量指标,周转量指标 = baseIdx+1;2026 月度列 C..O,2025 列 T.. 无数据则清空 */ |
| | | private void fillRailFerryBlock(Sheet sheet, Map<Integer, Map<String, double[]>> monthCity, int month, |
| | | String[][] areaRows, int baseIdx) { |
| | | for (String[] ar : areaRows) { |
| | | String area = ar[0]; |
| | | int paxRow = Integer.parseInt(ar[1]); |
| | | for (int m = 1; m <= month; m++) { |
| | | setDataCell(sheet, paxRow, 2 + (m - 1) * 2, railFerryVal(monthCity, area, m, baseIdx)); |
| | | setDataCell(sheet, paxRow + 1, 2 + (m - 1) * 2, railFerryVal(monthCity, area, m, baseIdx + 1)); |
| | | } |
| | | for (int m = 1; m <= 12; m++) { |
| | | setDataCell(sheet, paxRow, 19 + (m - 1) * 2, 0); |
| | | setDataCell(sheet, paxRow + 1, 19 + (m - 1) * 2, 0); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private double railFerryVal(Map<Integer, Map<String, double[]>> monthCity, String area, int m, int idx) { |
| | | Map<String, double[]> cityMap = monthCity.get(m); |
| | | if (cityMap == null) return 0; |
| | | double[] arr = cityMap.get(area); |
| | | if (arr == null) return 0; |
| | | return idx < arr.length ? arr[idx] : 0; |
| | | } |
| | | |
| | | /** 写数据单元格:原为公式则保留(全省求和/排名公式);否则有值写入、无值清空模板样例 */ |
| | | private void setDataCell(Sheet sheet, int rowIdx, int colIdx, double v) { |
| | | Row row = sheet.getRow(rowIdx); |
| | | if (row == null) row = sheet.createRow(rowIdx); |
| | | Cell c = row.getCell(colIdx); |
| | | if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) return; |
| | | if (c == null) c = row.createCell(colIdx); |
| | | if (v == 0) c.setBlank(); else c.setCellValue(round(v, 2)); |
| | | } |
| | | |
| | | /** 显式写入 0(setDataCell 对 0 置空;汇总表网约车行需要显示 0) */ |
| | | private void setZeroCell(Sheet sheet, int rowIdx, int colIdx) { |
| | | Row row = sheet.getRow(rowIdx); |
| | | if (row == null) row = sheet.createRow(rowIdx); |
| | | Cell c = row.getCell(colIdx); |
| | | if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) return; |
| | | if (c == null) c = row.createCell(colIdx); |
| | | c.setCellValue(0.0); |
| | | } |
| | | /** 重算公式缓存值(保留公式,求值异常忽略) */ |
| | | private void recalc(XSSFWorkbook wb) { |
| | | try { |
| | | wb.getCreationHelper().createFormulaEvaluator().evaluateAll(); |
| | | } catch (Exception e) { |
| | | log.warn("城市客运模板公式求值失败: {}", e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | // ==================== 城市客运 分市州累计(城市分市州_模板 / 城市客运各市州明细表_模板) ==================== |
| | | |
| | | /** 生成_城市客运客运量分市州累计.xlsx(以 城市分市州_模板.xlsx / 城市客运各市州明细表_模板.xlsx 为底稿;网约车列无源数据留空) */ |
| | | public byte[] exportCityPassengerCitySum(String templateFileName, String period, String mode) throws Exception { |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, mode); |
| | | Map<String, double[]> cum = loadCityPassengerCumulative(year + "-", month); |
| | | Map<String, double[]> monthMap = loadCityPassengerMonthMap(year + "-", month); |
| | | File template = resolveTemplate(templateFileName); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheetAt(0); |
| | | dynamicTitle(sheet, year, month); // 标题按报表期动态化 |
| | | List<String> areas = new java.util.ArrayList<>(); |
| | | areas.add("全省"); |
| | | areas.addAll(RegionUtil.cityList()); |
| | | fillCitySumBlock(sheet, cum, areas, 4, 0); // 累计客运量区(单位:万人次,POI 0 基,行5-22) |
| | | fillCitySumBlock(sheet, cum, areas, 26, 1); // 累计周转量区(单位:万人次公里,POI 0 基,行27-44) |
| | | // 当月块标题(0基行44/66)与数据(0基行48-65/70-87) |
| | | Row pTitle = sheet.getRow(44); |
| | | if (pTitle != null && pTitle.getCell(0) != null) { |
| | | pTitle.getCell(0).setCellValue("城市客运客运量分市州" + month + "月情况"); |
| | | } |
| | | Row tTitle = sheet.getRow(66); |
| | | if (tTitle != null && tTitle.getCell(0) != null) { |
| | | tTitle.getCell(0).setCellValue("城市客运客运周转量分市州" + month + "月情况"); |
| | | } |
| | | fillCitySumBlock(sheet, monthMap, areas, 48, 0); // 当月客运量区(行49-66) |
| | | fillCitySumBlock(sheet, monthMap, areas, 70, 1); // 当月周转量区(行71-88) |
| | | // 当月块排名/占比公式由累计块复制而来,RANK 范围与占比分母需改指当月块自身行 |
| | | repairCityMonthRankShare(sheet, 50, 66, 49); |
| | | repairCityMonthRankShare(sheet, 72, 88, 71); |
| | | recalc(wb); |
| | | clearFormulaErrorsAll(wb); // 无去年累计的占比/增速公式 #DIV/0! → 清空 |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | /** 填充一个累计区:startRow 起每地区 1 行;paxTurnIdx=0 用客运量、1 用周转量 */ |
| | | private void fillCitySumBlock(Sheet sheet, Map<String, double[]> cum, List<String> areas, int startRow, int paxTurnIdx) { |
| | | int rowIdx = startRow; |
| | | for (String area : areas) { |
| | | double[] v = cum.get(area); |
| | | double bus = v == null ? 0 : v[paxTurnIdx == 0 ? 0 : 1]; |
| | | double taxi = v == null ? 0 : v[paxTurnIdx == 0 ? 2 : 3]; |
| | | double rail = v == null ? 0 : v[paxTurnIdx == 0 ? 4 : 5]; |
| | | double ferry = v == null ? 0 : v[paxTurnIdx == 0 ? 6 : 7]; |
| | | double total = bus + taxi + rail + ferry; // 网约车无源数据,不计 |
| | | setDataCell(sheet, rowIdx, 1, total); // B 总累计 |
| | | setDataCell(sheet, rowIdx, 6, bus); // G 公交 |
| | | setDataCell(sheet, rowIdx, 11, taxi); // L 出租 |
| | | setDataCell(sheet, rowIdx, 16, 0); // Q 网约车(无源数据→清空样例) |
| | | setDataCell(sheet, rowIdx, 21, rail); // V 轨道 |
| | | setDataCell(sheet, rowIdx, 23, ferry); // X 轮渡 |
| | | // 增速列 E/J/O/T/W/Y:无去年同期累计 → 清空模板样例(排名/占比公式保留) |
| | | setDataCell(sheet, rowIdx, 4, 0); |
| | | setDataCell(sheet, rowIdx, 9, 0); |
| | | setDataCell(sheet, rowIdx, 14, 0); |
| | | setDataCell(sheet, rowIdx, 19, 0); |
| | | setDataCell(sheet, rowIdx, 22, 0); |
| | | setDataCell(sheet, rowIdx, 24, 0); |
| | | rowIdx++; |
| | | } |
| | | } |
| | | |
| | | /** 当月块排名/占比公式修正:模板当月块由累计块复制而来,RANK 范围与占比分母需改指当月块自身行 */ |
| | | private void repairCityMonthRankShare(Sheet sheet, int firstDataRow1, int lastDataRow1, int provRow1) { |
| | | String[] groups = {"B", "G", "L", "Q"}; |
| | | String[] growth = {"E", "J", "O", "T"}; |
| | | for (int r = firstDataRow1; r <= lastDataRow1; r++) { |
| | | int r0 = r - 1; |
| | | for (int g = 0; g < 4; g++) { |
| | | String base = groups[g]; |
| | | String grow = growth[g]; |
| | | setFormulaIfFormula(sheet, r0, 2 + g * 5, |
| | | "RANK(" + base + r + "," + base + "$" + firstDataRow1 + ":" + base + "$" + lastDataRow1 + ")"); |
| | | setFormulaIfFormula(sheet, r0, 3 + g * 5, base + r + "/" + base + "$" + provRow1); |
| | | setFormulaIfFormula(sheet, r0, 5 + g * 5, |
| | | "RANK(" + grow + r + "," + grow + "$" + firstDataRow1 + ":" + grow + "$" + lastDataRow1 + ")"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 单元格原为公式时改写公式(排名/占比列),非公式单元格不动(保持模板式样) */ |
| | | private void setFormulaIfFormula(Sheet sheet, int rowIdx, int colIdx, String formula) { |
| | | Row row = sheet.getRow(rowIdx); |
| | | if (row == null) return; |
| | | Cell c = row.getCell(colIdx); |
| | | if (c == null || c.getCellType() != CellType.FORMULA) return; |
| | | c.setCellFormula(formula); |
| | | } |
| | | |
| | | /** 某年 1..limit 月城市客运累计:city → [公交客运量,公交周转量,出租客运量,出租周转量,轨道客运量,轨道周转量,轮渡客运量,轮渡周转量],含"全省" */ |
| | | private Map<String, double[]> loadCityPassengerCumulative(String yearPrefix, int limit) { |
| | | Map<String, double[]> cum = new HashMap<>(); |
| | | for (CityBusMonthly r : cityBusMapper.selectList( |
| | | new LambdaQueryWrapper<CityBusMonthly>().likeRight(CityBusMonthly::getReportPeriod, yearPrefix))) { |
| | | int m = monthOf(r.getReportPeriod()); |
| | | if (m < 1 || m > limit) continue; |
| | | String city = r.getCity() == null ? "未知" : r.getCity(); |
| | | double[] a = cum.computeIfAbsent(city, k -> new double[8]); |
| | | a[0] += nz(r.getPassengerVolume()); |
| | | a[1] += nz(r.getTurnover()); |
| | | a[4] += nz(r.getRailPassengerVolume()); |
| | | a[5] += nz(r.getRailTurnover()); |
| | | a[6] += nz(r.getFerryPassengerVolume()); |
| | | a[7] += nz(r.getFerryTurnover()); |
| | | double[] p = cum.computeIfAbsent("全省", k -> new double[8]); |
| | | p[0] += nz(r.getPassengerVolume()); |
| | | p[1] += nz(r.getTurnover()); |
| | | p[4] += nz(r.getRailPassengerVolume()); |
| | | p[5] += nz(r.getRailTurnover()); |
| | | p[6] += nz(r.getFerryPassengerVolume()); |
| | | p[7] += nz(r.getFerryTurnover()); |
| | | } |
| | | for (CityTaxiMonthly r : cityTaxiMapper.selectList( |
| | | new LambdaQueryWrapper<CityTaxiMonthly>().likeRight(CityTaxiMonthly::getReportPeriod, yearPrefix))) { |
| | | int m = monthOf(r.getReportPeriod()); |
| | | if (m < 1 || m > limit) continue; |
| | | String city = r.getCity() == null ? "未知" : r.getCity(); |
| | | double[] a = cum.computeIfAbsent(city, k -> new double[8]); |
| | | a[2] += nz(r.getPassengerVolume()); |
| | | a[3] += nz(r.getTurnover()); |
| | | double[] p = cum.computeIfAbsent("全省", k -> new double[8]); |
| | | p[2] += nz(r.getPassengerVolume()); |
| | | p[3] += nz(r.getTurnover()); |
| | | } |
| | | return cum; |
| | | } |
| | | |
| | | /** 某年单月城市客运数据:city → 8 维数组(口径同累计),含"全省";= month 累计 - (month-1) 累计 */ |
| | | private Map<String, double[]> loadCityPassengerMonthMap(String yearPrefix, int month) { |
| | | Map<String, double[]> cum = loadCityPassengerCumulative(yearPrefix, month); |
| | | if (month <= 1) return cum; |
| | | Map<String, double[]> pre = loadCityPassengerCumulative(yearPrefix, month - 1); |
| | | Map<String, double[]> out = new HashMap<>(); |
| | | for (Map.Entry<String, double[]> e : cum.entrySet()) { |
| | | double[] prev = pre.get(e.getKey()); |
| | | double[] d = new double[8]; |
| | | for (int i = 0; i < 8; i++) { |
| | | d[i] = e.getValue()[i] - (prev == null ? 0 : prev[i]); |
| | | } |
| | | out.put(e.getKey(), d); |
| | | } |
| | | return out; |
| | | } |
| | | |
| | | // ==================== 城市客运 全省汇总(城市汇总_模板) ==================== |
| | | |
| | | /** 生成_城市客运汇总.xlsx(以 城市汇总_模板.xlsx 为底稿;2026 段填库内 1..N 月累计,同比清空,2025/2024 段保留模板历史参考值) */ |
| | | public byte[] exportCityPassengerSummary(String period, String mode) throws Exception { |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, mode); |
| | | double[] prov = loadCityPassengerCumulative(year + "-", month).getOrDefault("全省", new double[8]); |
| | | double busPax = prov[0], busTurn = prov[1]; |
| | | double taxiPax = prov[2], taxiTurn = prov[3]; |
| | | double railPax = prov[4], railTurn = prov[5]; |
| | | double ferryPax = prov[6], ferryTurn = prov[7]; |
| | | File template = resolveTemplate("生成_城市客运汇总.xlsx"); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheetAt(0); |
| | | dynamicTitle(sheet, year, month); // 标题按报表期动态化 |
| | | // 行3 总客运量=公交+出租车(巡游+网约车)+轨道+轮渡;行4 公交;行5 出租车=巡游+网约车;行6 巡游;行7 网约车;行8 轨道;行9 轮渡 |
| | | setDataCell(sheet, 2, 1, busPax + taxiPax + railPax + ferryPax); // B3(POI 0 基行2) |
| | | setDataCell(sheet, 2, 3, busTurn + taxiTurn + railTurn + ferryTurn); // D3 |
| | | setDataCell(sheet, 3, 1, busPax); // B4 城市公交 |
| | | setDataCell(sheet, 3, 3, busTurn); // D4 |
| | | setDataCell(sheet, 4, 1, taxiPax); // B5 城市出租车(网约车无源数据) |
| | | setDataCell(sheet, 4, 3, taxiTurn); // D5 |
| | | setDataCell(sheet, 5, 1, taxiPax); // B6 其中巡游出租 |
| | | setDataCell(sheet, 5, 3, taxiTurn); // D6 |
| | | setZeroCell(sheet, 6, 1); // B7 城市网约车(显式 0,setDataCell 对 0 会置空) |
| | | setZeroCell(sheet, 6, 3); // D7 |
| | | setDataCell(sheet, 7, 1, railPax); // B8 轨道 |
| | | setDataCell(sheet, 7, 3, railTurn); // D8 |
| | | setDataCell(sheet, 8, 1, ferryPax); // B9 轮渡 |
| | | setDataCell(sheet, 8, 3, ferryTurn);// D9 |
| | | for (int r = 2; r <= 8; r++) { // 同比 C/E:无去年同期 → 清空样例 |
| | | setDataCell(sheet, r, 2, 0); |
| | | setDataCell(sheet, r, 4, 0); |
| | | } |
| | | // 底部单月表(新版式):标题行11(0基10)、数据行13-19(0基12-18),填当月完成量 |
| | | Row mTitle = sheet.getRow(10); |
| | | if (mTitle != null && mTitle.getCell(0) != null) { |
| | | mTitle.getCell(0).setCellValue(year + "年" + month + "月全省完成城市客运量情况"); |
| | | } |
| | | double[] mProv = loadCityPassengerMonthMap(year + "-", month).getOrDefault("全省", new double[8]); |
| | | double mBusPax = mProv[0], mBusTurn = mProv[1]; |
| | | double mTaxiPax = mProv[2], mTaxiTurn = mProv[3]; |
| | | double mRailPax = mProv[4], mRailTurn = mProv[5]; |
| | | double mFerryPax = mProv[6], mFerryTurn = mProv[7]; |
| | | setDataCell(sheet, 12, 1, mBusPax + mTaxiPax + mRailPax + mFerryPax); // B13 总客运量 |
| | | setDataCell(sheet, 12, 3, mBusTurn + mTaxiTurn + mRailTurn + mFerryTurn); // D13 总周转量 |
| | | setDataCell(sheet, 13, 1, mBusPax); // B14 城市公交 |
| | | setDataCell(sheet, 13, 3, mBusTurn); // D14 |
| | | setDataCell(sheet, 14, 1, mTaxiPax); // B15 城市出租车 |
| | | setDataCell(sheet, 14, 3, mTaxiTurn); // D15 |
| | | setDataCell(sheet, 15, 1, mTaxiPax); // B16 其中巡游出租 |
| | | setDataCell(sheet, 15, 3, mTaxiTurn); // D16 |
| | | setZeroCell(sheet, 16, 1); // B17 城市网约车 |
| | | setZeroCell(sheet, 16, 3); // D17 |
| | | setDataCell(sheet, 17, 1, mRailPax); // B18 轨道 |
| | | setDataCell(sheet, 17, 3, mRailTurn); // D18 |
| | | setDataCell(sheet, 18, 1, mFerryPax); // B19 轮渡 |
| | | setDataCell(sheet, 18, 3, mFerryTurn);// D19 |
| | | for (int r = 12; r <= 18; r++) { // 单月表 C/E 同比:无上年单月源 → 清空样例 |
| | | setDataCell(sheet, r, 2, 0); |
| | | setDataCell(sheet, r, 4, 0); |
| | | } |
| | | recalc(wb); |
| | | clearFormulaErrorsAll(wb); // 无去年数据的同比公式 #DIV/0! → 清空 |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | // ==================== 城市客运 报表系列(一键打包) ==================== |
| | | |
| | | /** 生成_城市客运报表系列.zip:公交/出租/轨道轮渡明细 + 分市州累计 + 各市州明细表 + 汇总 */ |
| | | public byte[] exportCityPassengerSeries(String period, String mode) throws Exception { |
| | | java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream(); |
| | | try (java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(bos)) { |
| | | addZipEntry(zos, "生成_城市公交客运量分市州明细.xlsx", exportCityBusDetail(period, mode)); |
| | | addZipEntry(zos, "生成_巡游出租客运量分市州明细.xlsx", exportCityTaxiDetail(period, mode)); |
| | | addZipEntry(zos, "生成_轨道轮渡客运量分市州明细.xlsx", exportCityRailFerryDetail(period, mode)); |
| | | addZipEntry(zos, "生成_城市客运客运量分市州累计.xlsx", exportCityPassengerCitySum("生成_城市客运客运量分市州累计.xlsx", period, mode)); |
| | | addZipEntry(zos, "生成_城市客运各市州明细表.xlsx", exportCityPassengerCitySum("生成_城市客运各市州明细表.xlsx", period, mode)); |
| | | addZipEntry(zos, "生成_城市客运汇总.xlsx", exportCityPassengerSummary(period, mode)); |
| | | } |
| | | return bos.toByteArray(); |
| | | } |
| | | |
| | | /** 生成_货运报表系列.zip:货运量分市州明细 + 货运量排名 + 周转量排名 */ |
| | | public byte[] exportFreightSeries(String period, String mode) throws Exception { |
| | | java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream(); |
| | | try (java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(bos)) { |
| | | addZipEntry(zos, "生成_货运量分市州明细.xlsx", exportCityDetail(period, mode)); |
| | | addZipEntry(zos, "生成_货运量排名.xlsx", exportFreightRank(period, mode)); |
| | | addZipEntry(zos, "生成_周转量排名.xlsx", exportTurnoverRank(period, mode)); |
| | | } |
| | | return bos.toByteArray(); |
| | | } |
| | | |
| | | /** 生成_公路旅客报表系列.zip:分市州明细 + 中口径明细/排名/分析 */ |
| | | public byte[] exportPassengerSeries(String period, String mode) throws Exception { |
| | | java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream(); |
| | | try (java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(bos)) { |
| | | addZipEntry(zos, "生成_公路旅客分市州.xlsx", exportPassengerCityDetail(period, mode)); |
| | | addZipEntry(zos, "生成_中口径明细.xlsx", exportPassengerMidDetail(period, mode)); |
| | | addZipEntry(zos, "生成_中口径排名.xlsx", exportPassengerMidRank(period, mode)); |
| | | addZipEntry(zos, "生成_中口径分析.xlsx", exportPassengerMidAnalysis(period, mode)); |
| | | } |
| | | 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(); |
| | | try (java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(bos)) { |
| | | addZipEntry(zos, "生成_全省预安排计划进度情况汇总.xls", investPlan(period, mode)); |
| | | addZipEntry(zos, "生成_十五五规划物流项目进展情况.xls", investFiveYearLogistics(period, mode)); |
| | | addZipEntry(zos, "生成_湖北省(客货站场)亿元投资项目.xls", investBillion(period, mode)); |
| | | addZipEntry(zos, "生成_经济强县交通物流基础设施投资统计报表.xls", investCounty(period, mode)); |
| | | } |
| | | return bos.toByteArray(); |
| | | } |
| | | |
| | | /** 生成_所选报表.zip:按 type 列表打包(前端"生成选中"多选时用) */ |
| | | public byte[] exportSelectedSeries(String period, String mode, List<String> types) throws Exception { |
| | | java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream(); |
| | | try (java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(bos)) { |
| | | if (types != null) { |
| | | for (String t : types) { |
| | | if (t == null || t.trim().isEmpty()) continue; |
| | | byte[] data = selectedReportData(period, mode, t); |
| | | if (data != null) { |
| | | String name = REPORT_FILE_NAMES.get(t); |
| | | if (name == null) name = "生成_" + t + ".xlsx"; |
| | | addZipEntry(zos, name, data); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return bos.toByteArray(); |
| | | } |
| | | |
| | | private byte[] selectedReportData(String period, String mode, String type) throws Exception { |
| | | switch (type) { |
| | | case "cityDetail": return exportCityDetail(period, mode); |
| | | case "freightRank": return exportFreightRank(period, mode); |
| | | case "turnoverRank": return exportTurnoverRank(period, mode); |
| | | case "passengerCityDetail": return exportPassengerCityDetail(period, mode); |
| | | 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 "investBillion": return investBillion(period, mode); |
| | | case "investCounty": return investCounty(period, mode); |
| | | case "cityBusDetail": return exportCityBusDetail(period, mode); |
| | | case "cityTaxiDetail": return exportCityTaxiDetail(period, mode); |
| | | case "cityRailFerryDetail": return exportCityRailFerryDetail(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; |
| | | } |
| | | } |
| | | |
| | | private static final Map<String, String> REPORT_FILE_NAMES = new HashMap<>(); |
| | | static { |
| | | REPORT_FILE_NAMES.put("cityDetail", "生成_货运量分市州明细.xlsx"); |
| | | REPORT_FILE_NAMES.put("freightRank", "生成_货运量排名.xlsx"); |
| | | REPORT_FILE_NAMES.put("turnoverRank", "生成_周转量排名.xlsx"); |
| | | REPORT_FILE_NAMES.put("passengerCityDetail", "生成_公路旅客分市州明细.xlsx"); |
| | | REPORT_FILE_NAMES.put("passengerMidDetail", "生成_公路旅客中口径明细.xlsx"); |
| | | REPORT_FILE_NAMES.put("passengerMidRank", "生成_公路旅客中口径排名.xlsx"); |
| | | REPORT_FILE_NAMES.put("passengerMidAnalysis", "生成_公路旅客中口径分析.xlsx"); |
| | | REPORT_FILE_NAMES.put("energySummary", "生成_能运汇总表.xlsx"); |
| | | REPORT_FILE_NAMES.put("investPlan", "生成_全省预安排计划进度情况汇总.xls"); |
| | | REPORT_FILE_NAMES.put("investFiveYearLogistics", "生成_十五五规划物流项目进展情况.xls"); |
| | | REPORT_FILE_NAMES.put("investBillion", "生成_湖北省(客货站场)亿元投资项目.xls"); |
| | | REPORT_FILE_NAMES.put("investCounty", "生成_经济强县交通物流基础设施投资统计报表.xls"); |
| | | REPORT_FILE_NAMES.put("cityBusDetail", "生成_城市公交客运量分市州明细.xlsx"); |
| | | REPORT_FILE_NAMES.put("cityTaxiDetail", "生成_巡游出租客运量分市州明细.xlsx"); |
| | | REPORT_FILE_NAMES.put("cityRailFerryDetail", "生成_轨道轮渡客运量分市州明细.xlsx"); |
| | | 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 { |
| | | zos.putNextEntry(new java.util.zip.ZipEntry(name)); |
| | | zos.write(data); |
| | | zos.closeEntry(); |
| | | } |
| | | |
| | | /** 城市客运模板填充:以目录下模板为底稿,保留标题/表头/合并/公式/样式,仅替换数据区市州月度值 */ |
| | | public byte[] exportCityByTemplate(String templateFileName, String period, String mode, |
| | | Map<Integer, Map<String, double[]>> monthCity, |
| | | Map<Integer, Map<String, double[]>> lastYearMonthCity) throws Exception { |
| | | int month = monthOf(period, mode); |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | File template = resolveTemplate(templateFileName); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheetAt(0); |
| | | dynamicTitle(sheet, year, month); // 标题按报表期动态化(不写死年份/月份) |
| | | // 结构守卫:数据区应为 全省+17市州 × 4 指标 = 72 行(1-based 第5行起) |
| | | if (sheet.getLastRowNum() < 75) { |
| | | throw new RuntimeException("城市客运模板数据区行数不足,请确认模板未改版: " + templateFileName); |
| | | } |
| | | List<String> areas = new java.util.ArrayList<>(); |
| | | areas.add("全省"); |
| | | areas.addAll(RegionUtil.cityList()); |
| | | // 模板数据区:1-based 第5行起,每地区4行(客运量/周转量/城市内客运量/城市内周转量) |
| | | int rowIdx = 4; |
| | | for (int a = 0; a < areas.size(); a++) { |
| | | String area = areas.get(a); |
| | | for (int k = 0; k < 4; k++) { |
| | | Row row = sheet.getRow(rowIdx); |
| | | if (row == null) row = sheet.createRow(rowIdx); |
| | | if (a > 0) { |
| | | // 2026年 1..N 月:库内有值则填(2位小数),无值清空模板样例 |
| | | for (int m = 1; m <= month; m++) { |
| | | double v = cityVal(monthCity, area, m, k); |
| | | Cell c = row.getCell(2 + (m - 1) * 2); |
| | | if (c == null) c = row.createCell(2 + (m - 1) * 2); |
| | | if (v == 0) c.setBlank(); else c.setCellValue(round(v, 2)); |
| | | } |
| | | // 2025年 1..12 月:库内有去年数据则填,否则清空模板样例 |
| | | for (int m = 1; m <= 12; m++) { |
| | | double v = cityVal(lastYearMonthCity, area, m, k); |
| | | Cell c = row.getCell(19 + (m - 1) * 2); |
| | | if (c == null) c = row.createCell(19 + (m - 1) * 2); |
| | | if (v == 0) c.setBlank(); else c.setCellValue(round(v, 2)); |
| | | } |
| | | } |
| | | rowIdx++; |
| | | } |
| | | } |
| | | // 重算公式缓存值(全省求和/同比/累计,保留公式;求值异常忽略) |
| | | try { |
| | | wb.getCreationHelper().createFormulaEvaluator().evaluateAll(); |
| | | } catch (Exception e) { |
| | | log.warn("城市客运模板公式求值失败: {}", e.getMessage()); |
| | | } |
| | | clearFormulaErrorCells(sheet, 4, sheet.getLastRowNum(), 3, 39); |
| | | fixCityMonthHeaderText(sheet); |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | /** 清空求值结果为错误的公式单元格(模板跨簿同比公式残留 #REF! 时移除公式,避免导出显示错误) */ |
| | | private void clearFormulaErrorCells(Sheet sheet, int startRow, int endRow, int startCol, int endCol) { |
| | | for (int r = startRow; r <= endRow; r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | for (int c = startCol; c <= endCol; c++) { |
| | | Cell cell = row.getCell(c); |
| | | if (cell == null || cell.getCellType() != CellType.FORMULA) continue; |
| | | try { |
| | | if (cell.getCachedFormulaResultType() == CellType.ERROR) cell.setBlank(); |
| | | } catch (Exception ignore) { |
| | | // 缓存类型读取失败时忽略 |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 清空工作簿全部工作表内求值结果为错误的公式单元格(跨簿/跨表 #REF!、缺去年数据 #DIV/0! 等) */ |
| | | private void clearFormulaErrorsAll(XSSFWorkbook wb) { |
| | | for (int i = 0; i < wb.getNumberOfSheets(); i++) { |
| | | Sheet s = wb.getSheetAt(i); |
| | | clearFormulaErrorCells(s, 2, s.getLastRowNum(), 1, 60); |
| | | } |
| | | } |
| | | |
| | | /** 城市客运类模板表头月份列若为未格式化日期序列号(如 46082),改写为「YYYY年M月」文本 */ |
| | | private void fixCityMonthHeaderText(Sheet sheet) { |
| | | for (int r = 0; r <= Math.min(sheet.getLastRowNum(), 20); r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | Cell nameCell = row.getCell(0); |
| | | String nameText = nameCell == null ? null : cellText(nameCell); |
| | | if (nameText == null || !nameText.contains("地区")) continue; |
| | | for (int c = 2; c <= 40; c += 2) { |
| | | Cell cell = row.getCell(c); |
| | | if (cell == null || cell.getCellType() != CellType.NUMERIC) continue; |
| | | double v = cell.getNumericCellValue(); |
| | | if (v < 40000 || v > 60000) continue; |
| | | java.util.Date d = org.apache.poi.ss.usermodel.DateUtil.getJavaDate(v); |
| | | cell.setCellValue(new java.text.SimpleDateFormat("yyyy年M月").format(d)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 模板文件定位:配置目录 → user.dir → user.dir/.. 逐级回退 */ |
| | | private File resolveTemplate(String fileName) throws Exception { |
| | | return resolveAnyTemplate(templateDir, fileName); |
| | | } |
| | | |
| | | /** 公路旅客输出模板定位(docs/公路旅客+能耗/输出) */ |
| | | private File resolvePassengerTemplate(String fileName) throws Exception { |
| | | return resolveAnyTemplate(passengerTemplateDir, fileName); |
| | | } |
| | | |
| | | /** 通用模板文件定位(用于城市客运/能耗等不同模板目录) */ |
| | | private File resolveAnyTemplate(String dir, String fileName) throws Exception { |
| | | String rel = dir; |
| | | while (rel.startsWith("./")) rel = rel.substring(2); |
| | | String[] roots = { |
| | | dir, |
| | | 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, fileName); |
| | | if (f.exists() && f.isFile()) return f; |
| | | } |
| | | throw new RuntimeException("未找到模板文件: " + fileName); |
| | | } |
| | | |
| | | /** 城市客运模板标题动态化:扫描全部文本单元格,替换写死的年份与累计区间(如 "2026年1-6月" → "2027年1-8月"),列标题 "2026年1月" 只换年份;"1-12月" 整年对比块(2024/2025)标题保持原样 */ |
| | | private void dynamicTitle(Sheet sheet, int year, int month) { |
| | | java.util.regex.Pattern yearP = java.util.regex.Pattern.compile("\\d{4}年"); |
| | | java.util.regex.Pattern rangeP = java.util.regex.Pattern.compile("(\\d+)-(\\d+)月"); |
| | | String cum = cumRange(month); |
| | | for (int r = 0; r <= sheet.getLastRowNum(); r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | for (Cell cell : row) { |
| | | if (cell.getCellType() != CellType.STRING) continue; |
| | | String v = cell.getStringCellValue(); |
| | | if (v == null || v.isEmpty() || v.contains("1-12月")) continue; // 整年对比块标题保持原样 |
| | | String nv = yearP.matcher(v).replaceAll(year + "年"); |
| | | nv = rangeP.matcher(nv).replaceAll(java.util.regex.Matcher.quoteReplacement(cum)); |
| | | if (!nv.equals(v)) cell.setCellValue(nv); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 城市客运分市州取值(月 → 市州 → 指标数组) */ |
| | | private double cityVal(Map<Integer, Map<String, double[]>> monthCity, String area, int m, int idx) { |
| | | Map<String, double[]> cityMap = monthCity.get(m); |
| | | if (cityMap == null) return 0; |
| | | double[] arr = cityMap.get(area); |
| | | if (arr == null) return 0; |
| | | return idx < arr.length ? arr[idx] : 0; |
| | | } |
| | | |
| | | // ==================== 投资模块 4 张领导报表(模板底稿生成,输出 .xls) ==================== |
| | | |
| | | /** 加载投资 .xls 模板(HSSFWorkbook,保留列宽/表头/样式/合并) */ |
| | | private HSSFWorkbook loadInvestTemplate(String fileName) throws Exception { |
| | | File f = resolveAnyTemplate(investTemplateDir, fileName); |
| | | try (InputStream in = new FileInputStream(f)) { |
| | | return new HSSFWorkbook(in); |
| | | } |
| | | } |
| | | |
| | | /** 模板单元格写文本:先清空(保留样式/合并)再写,null/空白留空 */ |
| | | private void hssfText(Row row, int idx, String v) { |
| | | Cell c = row.getCell(idx); |
| | | if (c == null) c = row.createCell(idx); |
| | | c.setBlank(); |
| | | if (v != null && !v.trim().isEmpty()) c.setCellValue(v.trim()); |
| | | } |
| | | |
| | | /** 模板单元格写数值:先清空(保留样式/合并)再写 */ |
| | | private void hssfNum(Row row, int idx, Double v) { |
| | | Cell c = row.getCell(idx); |
| | | if (c == null) c = row.createCell(idx); |
| | | c.setBlank(); |
| | | if (v != null) c.setCellValue(v); |
| | | } |
| | | |
| | | /** 删除数据区多余行(fromRow..lastKeepRow-1):先移除合并单元格再删行记录;表头合并不受影响 */ |
| | | private void trimHssfTail(HSSFSheet sheet, int fromRow, int lastKeepRow) { |
| | | for (int i = sheet.getNumMergedRegions() - 1; i >= 0; i--) { |
| | | CellRangeAddress m = sheet.getMergedRegion(i); |
| | | if (m.getFirstRow() >= fromRow && m.getFirstRow() < lastKeepRow) { |
| | | sheet.removeMergedRegion(i); |
| | | } |
| | | } |
| | | for (int r = lastKeepRow - 1; r >= fromRow; r--) { |
| | | HSSFRow row = sheet.getRow(r); |
| | | if (row != null) sheet.removeRow(row); |
| | | } |
| | | } |
| | | |
| | | private void trimHssfTail(HSSFSheet sheet, int fromRow) { |
| | | trimHssfTail(sheet, fromRow, sheet.getLastRowNum() + 1); |
| | | } |
| | | |
| | | /** 新建行并复制模板行样式(含行高),保证与模板数据区外观一致 */ |
| | | private HSSFRow createRowLike(HSSFSheet sheet, int rowIdx, HSSFRow styleRow) { |
| | | HSSFRow row = sheet.createRow(rowIdx); |
| | | if (styleRow != null) { |
| | | row.setHeight(styleRow.getHeight()); |
| | | for (int c = 0; c < styleRow.getLastCellNum(); c++) { |
| | | HSSFCell sc = styleRow.getCell(c); |
| | | if (sc == null) continue; |
| | | row.createCell(c).setCellStyle(sc.getCellStyle()); |
| | | } |
| | | } |
| | | return row; |
| | | } |
| | | |
| | | /** 添加单行合并单元格(与模板数据区合并规则一致) */ |
| | | private void addMerge(HSSFSheet sheet, int r, int firstCol, int lastCol) { |
| | | sheet.addMergedRegion(new CellRangeAddress(r, r, firstCol, lastCol)); |
| | | } |
| | | |
| | | /** 解析 4 位年份为数值(写模板年份列),无法解析返回 null */ |
| | | private Double parseYearNum(String t) { |
| | | if (t == null || t.trim().isEmpty()) return null; |
| | | String s = t.trim().substring(0, Math.min(4, t.trim().length())); |
| | | try { |
| | | return Double.parseDouble(s); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** 解析 6 位 YYYYMM 为数值(写模板开工时间列) */ |
| | | private Double parseTimeYMNum(String t) { |
| | | if (t == null || t.trim().isEmpty()) return null; |
| | | String x = padTimeYM(t.trim()).replaceAll("[^0-9]", ""); |
| | | if (x.length() < 6) return null; |
| | | try { |
| | | return Double.parseDouble(x.substring(0, 6)); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private byte[] toBytesHssf(HSSFWorkbook wb) throws Exception { |
| | | applyTwoDecimalFormat(wb); |
| | | try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { |
| | | wb.write(out); |
| | | wb.close(); |
| | | return out.toByteArray(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 投资模板 市州小节结构(表1/表2 动态插行用) */ |
| | | private static class InvestSection { |
| | | String blockCategory; |
| | | String city; |
| | | int startRow; |
| | | int styleRowIdx = -1; // 该小节首个项目行(复制样式用) |
| | | List<Integer> projectRows = new ArrayList<>(); |
| | | Set<Long> matched = new HashSet<>(); |
| | | } |
| | | |
| | | /** 在 pos 行之后插入 n 行:下方行下移(合并区先摘除再按新行号恢复),新行复制 styleRow 样式/行高/单行合并;返回首个新行索引 */ |
| | | private int insertRowsDown(HSSFSheet sheet, int pos, int n, HSSFRow styleRow) { |
| | | if (n <= 0) return -1; |
| | | List<CellRangeAddress> below = new ArrayList<>(); |
| | | for (int i = sheet.getNumMergedRegions() - 1; i >= 0; i--) { |
| | | CellRangeAddress m = sheet.getMergedRegion(i); |
| | | if (m.getFirstRow() > pos) { |
| | | below.add(m); |
| | | sheet.removeMergedRegion(i); |
| | | } |
| | | } |
| | | int last = sheet.getLastRowNum(); |
| | | if (pos < last) { |
| | | sheet.shiftRows(pos + 1, last, n, true, false); |
| | | } |
| | | for (CellRangeAddress m : below) { |
| | | sheet.addMergedRegion(new CellRangeAddress(m.getFirstRow() + n, m.getLastRow() + n, m.getFirstColumn(), m.getLastColumn())); |
| | | } |
| | | int first = pos + 1; |
| | | for (int i = 0; i < n; i++) { |
| | | HSSFRow row = createRowLike(sheet, first + i, styleRow); |
| | | if (styleRow != null) copyRowMerges(sheet, styleRow.getRowNum(), first + i); |
| | | } |
| | | return first; |
| | | } |
| | | |
| | | /** 复制 styleRow 所在行的单行合并区到 toRow(插入行外观与模板行一致) */ |
| | | private void copyRowMerges(HSSFSheet sheet, int fromRow, int toRow) { |
| | | for (int i = 0; i < sheet.getNumMergedRegions(); i++) { |
| | | CellRangeAddress m = sheet.getMergedRegion(i); |
| | | if (m.getFirstRow() == fromRow && m.getLastRow() == fromRow) { |
| | | sheet.addMergedRegion(new CellRangeAddress(toRow, toRow, m.getFirstColumn(), m.getLastColumn())); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 投资表 序号重排:表1 按块重置、表2 按市州小节重置、表4 全表连续(模板/插入行统一排序) */ |
| | | private void renumberInvestSeq(HSSFSheet sheet, String kind) { |
| | | int seq = 0; |
| | | int lastRow = sheet.getLastRowNum(); |
| | | for (int r = 4; r <= lastRow; r++) { |
| | | HSSFRow row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | if ("investPlan".equals(kind)) { |
| | | if ("一、客运站场基础设施建设".equals(a) || "二、交通物流基础设施建设".equals(a) || "三、老旧营运货车报废更新".equals(a)) { |
| | | seq = 0; |
| | | continue; |
| | | } |
| | | } else if ("investFiveYear".equals(kind)) { |
| | | if (isCityLabel(a)) { |
| | | seq = 0; |
| | | continue; |
| | | } |
| | | } |
| | | String b = nvl(cellText(row.getCell(1))); |
| | | if (b.isEmpty()) continue; |
| | | hssfNum(row, 0, (double) (++seq)); |
| | | } |
| | | } |
| | | |
| | | /** 表1 项目行填充(模板行/插入行共用;cityText 与模板 B 列格式一致) */ |
| | | private void fillInvestPlanProjectRow(HSSFRow row, InvestmentProject p, InvestmentMonthly m, int seq, String cityText, String displayName) { |
| | | hssfNum(row, 0, (double) seq); |
| | | hssfText(row, 1, cityText); |
| | | hssfText(row, 2, valueOf(p.getCounty())); |
| | | hssfText(row, 3, displayName != null && !displayName.trim().isEmpty() ? displayName.trim() : p.getProjectName()); |
| | | hssfText(row, 4, valueOf(p.getConstructNature())); |
| | | hssfNum(row, 5, parseYearNum(p.getStartTime())); |
| | | hssfNum(row, 6, parseYearNum(p.getEndTime())); |
| | | hssfNum(row, 7, p.getTotalInvestment()); |
| | | hssfText(row, 20, p.getApprovalGk()); |
| | | hssfText(row, 21, p.getApprovalCs()); |
| | | if (m != null) { |
| | | hssfNum(row, 8, m.getStartCum()); |
| | | hssfNum(row, 9, m.getYearPlan()); |
| | | hssfText(row, 13, m.getProgressDesc()); |
| | | hssfNum(row, 22, m.getYearCum()); |
| | | hssfText(row, 23, valueOf(m.getProgressStage())); |
| | | } |
| | | } |
| | | |
| | | /** 表2 项目行填充(模板行/插入行共用) */ |
| | | private void fillFiveYearProjectRow(HSSFRow row, InvestmentProject p, InvestmentMonthly m, int seq) { |
| | | hssfNum(row, 0, (double) seq); |
| | | hssfText(row, 1, p.getProjectName()); |
| | | hssfText(row, 2, valueOf(p.getBuilderName())); |
| | | hssfNum(row, 3, parseYearNum(p.getStartTime())); |
| | | hssfNum(row, 4, parseYearNum(p.getEndTime())); |
| | | hssfNum(row, 5, p.getTotalInvestment()); |
| | | if (m != null) { |
| | | hssfNum(row, 6, m.getStartCum()); |
| | | hssfNum(row, 7, m.getYearCum()); |
| | | hssfText(row, 8, valueOf(m.getProgressStage())); |
| | | hssfText(row, 9, m.getProgressDesc()); |
| | | } |
| | | } |
| | | |
| | | /** 表4 项目行填充(模板行/插入行共用) */ |
| | | private void fillCountyProjectRow(HSSFRow row, InvestmentProject p, InvestmentMonthly m, int seq) { |
| | | hssfNum(row, 0, (double) seq); |
| | | hssfText(row, 1, p.getProjectName()); |
| | | hssfText(row, 2, valueOf(p.getBuilderName())); |
| | | hssfNum(row, 3, p.getTotalInvestment()); |
| | | if (m != null) { |
| | | hssfNum(row, 4, m.getStartCum()); |
| | | hssfNum(row, 5, m.getYearPlan()); |
| | | hssfNum(row, 6, m.getYearCum()); |
| | | hssfNum(row, 7, m.getMonthDone()); |
| | | hssfText(row, 8, valueOf(m.getProgressStage())); |
| | | hssfText(row, 9, m.getProgressDesc()); |
| | | if (m.getBuildingArea() != null && m.getBuildingArea() > 0) hssfNum(row, 12, m.getBuildingArea()); |
| | | } |
| | | hssfText(row, 15, p.getApprovalGk()); |
| | | hssfText(row, 16, p.getApprovalCs()); |
| | | } |
| | | |
| | | /** 表1:2026年全省预安排计划进度情况汇总(模板底稿原位替换:保留全部行/合并/列宽,仅替换数据;DB 项目多于模板预留行时动态插行) */ |
| | | public byte[] investPlan(String period, String mode) throws Exception { |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, mode); |
| | | List<InvestmentMonthly> monthlies = investMonthlyMapper.selectList( |
| | | new LambdaQueryWrapper<InvestmentMonthly>() |
| | | .eq(InvestmentMonthly::getReportPeriod, period)); |
| | | Map<Long, InvestmentProject> pmap = new HashMap<>(); |
| | | List<InvestmentProject> projects = investProjectMapper.selectList(null); |
| | | for (InvestmentProject p : projects) pmap.put(p.getId(), p); |
| | | Map<Long, InvestmentMonthly> mByProject = new HashMap<>(); |
| | | for (InvestmentMonthly m : monthlies) mByProject.put(m.getProjectId(), m); |
| | | |
| | | HSSFWorkbook wb = loadInvestTemplate("模板_全省预安排计划进度情况汇总.xls"); |
| | | HSSFSheet sheet = wb.getSheetAt(0); |
| | | // 标题/表头年份与累计月份 |
| | | hssfText(sheet.getRow(1), 0, year + "年全省客货运站场建设预安排计划表"); |
| | | hssfText(sheet.getRow(2), 0, "蓝色项目为" + year + "年市州拟申报资金项目 单位:万元"); |
| | | hssfText(sheet.getRow(3), 22, cumRange(month) + "累计完成投资"); |
| | | // 结构扫描:块(客运/物流/老旧) -> 市州小节 -> 项目行 |
| | | List<InvestSection> sections = new ArrayList<>(); |
| | | String blockCategory = null; |
| | | InvestSection cur = null; |
| | | int firstProjectRow = -1; |
| | | int lastRow = sheet.getLastRowNum(); |
| | | for (int r = 5; r <= lastRow; r++) { |
| | | HSSFRow row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | String b = nvl(cellText(row.getCell(1))); |
| | | String d = nvl(cellText(row.getCell(3))); |
| | | if (r == 5) continue; // 全省合计行单独处理 |
| | | if ("一、客运站场基础设施建设".equals(a) || "二、交通物流基础设施建设".equals(a) || "三、老旧营运货车报废更新".equals(a)) { |
| | | blockCategory = a.startsWith("一、") ? "客运站场" : (a.startsWith("二、") ? "物流园区" : null); |
| | | cur = null; |
| | | continue; |
| | | } |
| | | if (a.startsWith("一、") || a.startsWith("二、")) continue; // 预安排计划内/外 分组标题:归当前小节 |
| | | if (b.isEmpty() && d.isEmpty()) { |
| | | cur = new InvestSection(); |
| | | cur.blockCategory = blockCategory; |
| | | cur.city = templateCityFull(a); |
| | | cur.startRow = r; |
| | | sections.add(cur); |
| | | continue; |
| | | } |
| | | if (cur != null) { |
| | | cur.projectRows.add(r); |
| | | if (cur.styleRowIdx < 0) cur.styleRowIdx = r; |
| | | if (firstProjectRow < 0) firstProjectRow = r; |
| | | } |
| | | } |
| | | HSSFRow globalStyle = firstProjectRow >= 0 ? sheet.getRow(firstProjectRow) : null; |
| | | // 原位逐行替换:合计/块标题/市州小节/分组/项目行;未匹配的模板行清空样例数据(结构/合并/列宽不变) |
| | | int seq = 0; |
| | | int si = -1; |
| | | java.util.Set<Long> usedProjectIds = new HashSet<>(); |
| | | for (int r = 5; r <= lastRow; r++) { |
| | | HSSFRow row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | String b = nvl(cellText(row.getCell(1))); |
| | | String d = nvl(cellText(row.getCell(3))); |
| | | if (si + 1 < sections.size() && r >= sections.get(si + 1).startRow) si++; |
| | | InvestSection sec = si >= 0 && si < sections.size() ? sections.get(si) : null; |
| | | if (r == 5) { // 全省合计行 |
| | | blankInvestRow(row, 25); |
| | | hssfText(row, 0, "合计"); |
| | | InvestAgg prov = new InvestAgg(); |
| | | for (InvestmentMonthly m : monthlies) prov.add(m, pmap.get(m.getProjectId())); |
| | | writeInvestAggHssf(row, prov); |
| | | continue; |
| | | } |
| | | if ("一、客运站场基础设施建设".equals(a) || "二、交通物流基础设施建设".equals(a)) { |
| | | blockCategory = a.startsWith("一、") ? "客运站场" : "物流园区"; |
| | | blankInvestRow(row, 25); |
| | | hssfText(row, 0, a); |
| | | InvestAgg agg = new InvestAgg(); |
| | | for (InvestmentMonthly m : monthlies) { |
| | | InvestmentProject p = pmap.get(m.getProjectId()); |
| | | if (p != null && blockCategory.equals(p.getCategory())) agg.add(m, p); |
| | | } |
| | | writeInvestAggHssf(row, agg); |
| | | seq = 0; |
| | | continue; |
| | | } |
| | | if ("三、老旧营运货车报废更新".equals(a)) { |
| | | blockCategory = null; // 暂无数据源:整块清空 |
| | | blankInvestRow(row, 25); |
| | | hssfText(row, 0, a); |
| | | seq = 0; |
| | | continue; |
| | | } |
| | | if (a.startsWith("一、") || a.startsWith("二、")) { |
| | | // 预安排计划内/外 分组标题行:无数据,仅保留标签 |
| | | blankInvestRow(row, 25); |
| | | hssfText(row, 0, a); |
| | | continue; |
| | | } |
| | | if (b.isEmpty() && d.isEmpty()) { |
| | | // 市州小节行:重算该市州(当前块类别)合计 |
| | | String city = templateCityFull(a); |
| | | blankInvestRow(row, 25); |
| | | hssfText(row, 0, a); |
| | | InvestAgg agg = new InvestAgg(); |
| | | for (InvestmentMonthly m : monthlies) { |
| | | InvestmentProject p = pmap.get(m.getProjectId()); |
| | | if (p != null && city.equals(p.getCity()) |
| | | && (blockCategory == null || blockCategory.equals(p.getCategory()))) agg.add(m, p); |
| | | } |
| | | writeInvestAggHssf(row, agg); |
| | | continue; |
| | | } |
| | | // 项目行:按 模板名称+市州+块类别 匹配 DB 项目,未匹配整行清空 |
| | | blankInvestRow(row, 25); |
| | | if (blockCategory == null) continue; // 老旧货车段无数据源 |
| | | InvestmentProject p = matchInvest(projects, d, b, blockCategory, usedProjectIds); |
| | | if (p == null) continue; |
| | | usedProjectIds.add(p.getId()); |
| | | if (sec != null) sec.matched.add(p.getId()); |
| | | InvestmentMonthly m = mByProject.get(p.getId()); |
| | | fillInvestPlanProjectRow(row, p, m, ++seq, b, d); |
| | | } |
| | | // 动态插行:DB 项目数 > 模板预留行数时,在市州小节末尾补齐(自底向上插行,避免行号错位) |
| | | for (int i = sections.size() - 1; i >= 0; i--) { |
| | | InvestSection s = sections.get(i); |
| | | if (s.blockCategory == null || s.city == null) continue; // 老旧货车段无数据源 |
| | | List<InvestmentProject> extra = new ArrayList<>(); |
| | | for (InvestmentProject p : projects) { |
| | | if (s.blockCategory.equals(p.getCategory()) |
| | | && s.city.equals(RegionUtil.normalizeCityName(p.getCity())) |
| | | && !s.matched.contains(p.getId())) extra.add(p); |
| | | } |
| | | if (extra.isEmpty()) continue; |
| | | HSSFRow styleRow = s.styleRowIdx >= 0 ? sheet.getRow(s.styleRowIdx) : globalStyle; |
| | | int pos = s.projectRows.isEmpty() ? s.startRow : s.projectRows.get(s.projectRows.size() - 1); |
| | | int first = insertRowsDown(sheet, pos, extra.size(), styleRow); |
| | | for (int j = 0; j < extra.size(); j++) { |
| | | HSSFRow row = sheet.getRow(first + j); |
| | | if (row == null) continue; |
| | | InvestmentProject p = extra.get(j); |
| | | fillInvestPlanProjectRow(row, p, mByProject.get(p.getId()), 0, p.getCity(), p.getProjectName()); |
| | | } |
| | | } |
| | | renumberInvestSeq(sheet, "investPlan"); |
| | | return toBytesHssf(wb); |
| | | } |
| | | |
| | | /** 市州小节/合计行数值:7总投资 8自开始累计 9本年计划 22自年初累计 23实际进度(部/省资金与自筹无数据源,留空) */ |
| | | private void writeInvestAggHssf(HSSFRow row, InvestAgg agg) { |
| | | hssfNum(row, 7, agg.total == 0 ? null : agg.total); |
| | | hssfNum(row, 8, agg.startCum == 0 ? null : agg.startCum); |
| | | hssfNum(row, 9, agg.yearPlan == 0 ? null : agg.yearPlan); |
| | | hssfNum(row, 22, agg.yearCum == 0 ? null : agg.yearCum); |
| | | if (agg.yearPlan > 0) hssfNum(row, 23, round(agg.yearCum / agg.yearPlan, 4)); |
| | | } |
| | | |
| | | |
| | | private static class InvestAgg { |
| | | double total, startCum, yearPlan, yearCum; |
| | | |
| | | void add(InvestmentMonthly m, InvestmentProject p) { |
| | | if (p != null) total += nz(p.getTotalInvestment()); |
| | | startCum += nz(m.getStartCum()); |
| | | yearPlan += nz(m.getYearPlan()); |
| | | yearCum += nz(m.getYearCum()); |
| | | } |
| | | |
| | | private double nz(Double v) { |
| | | return v == null ? 0.0 : v; |
| | | } |
| | | } |
| | | |
| | | /** 表2:"十五五"规划物流项目进展情况(模板底稿原位替换:保留全部行/合并/列宽,仅替换数据;DB 项目多于模板预留行时动态插行) */ |
| | | public byte[] investFiveYearLogistics(String period, String mode) throws Exception { |
| | | List<InvestmentMonthly> monthlies = investMonthlyMapper.selectList( |
| | | new LambdaQueryWrapper<InvestmentMonthly>() |
| | | .eq(InvestmentMonthly::getReportPeriod, period)); |
| | | Map<Long, InvestmentProject> pmap = new HashMap<>(); |
| | | List<InvestmentProject> fiveProjects = new java.util.ArrayList<>(); |
| | | for (InvestmentProject p : investProjectMapper.selectList(null)) { |
| | | pmap.put(p.getId(), p); |
| | | if (Integer.valueOf(1).equals(p.getIsFiveYear())) fiveProjects.add(p); |
| | | } |
| | | Map<Long, InvestmentMonthly> mByProject = new HashMap<>(); |
| | | for (InvestmentMonthly m : monthlies) mByProject.put(m.getProjectId(), m); |
| | | List<InvestmentMonthly> mine = new java.util.ArrayList<>(); |
| | | for (InvestmentMonthly m : monthlies) { |
| | | InvestmentProject p = pmap.get(m.getProjectId()); |
| | | if (p != null && Integer.valueOf(1).equals(p.getIsFiveYear())) mine.add(m); |
| | | } |
| | | |
| | | HSSFWorkbook wb = loadInvestTemplate("模板_十五五规划物流项目进展情况.xls"); |
| | | HSSFSheet sheet = wb.getSheetAt(1); // 「 分项目投资完成情况」 |
| | | int lastRow = sheet.getLastRowNum(); |
| | | // 结构扫描:市州小节 -> 项目行 |
| | | List<InvestSection> sections = new ArrayList<>(); |
| | | InvestSection cur = null; |
| | | int firstProjectRow = -1; |
| | | for (int r = 4; r <= lastRow; r++) { |
| | | HSSFRow row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | String b = nvl(cellText(row.getCell(1))); |
| | | if (r == 4) continue; // 全省合计行单独处理 |
| | | if (b.isEmpty() && isCityLabel(a)) { |
| | | cur = new InvestSection(); |
| | | cur.city = templateCityFull(a); |
| | | cur.startRow = r; |
| | | sections.add(cur); |
| | | continue; |
| | | } |
| | | if (cur != null) { |
| | | cur.projectRows.add(r); |
| | | if (cur.styleRowIdx < 0) cur.styleRowIdx = r; |
| | | if (firstProjectRow < 0) firstProjectRow = r; |
| | | } |
| | | } |
| | | HSSFRow globalStyle = firstProjectRow >= 0 ? sheet.getRow(firstProjectRow) : null; |
| | | String currentCity = null; |
| | | int seq = 0; |
| | | int si = -1; |
| | | for (int r = 4; r <= lastRow; r++) { |
| | | HSSFRow row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | String b = nvl(cellText(row.getCell(1))); |
| | | if (si + 1 < sections.size() && r >= sections.get(si + 1).startRow) si++; |
| | | InvestSection sec = si >= 0 && si < sections.size() ? sections.get(si) : null; |
| | | if (r == 4) { // 全省合计行 |
| | | blankInvestRow(row, 12); |
| | | hssfText(row, 0, "合计"); |
| | | InvestAgg total = new InvestAgg(); |
| | | for (InvestmentMonthly m : mine) total.add(m, pmap.get(m.getProjectId())); |
| | | hssfNum(row, 5, total.total == 0 ? null : total.total); |
| | | hssfNum(row, 6, total.startCum == 0 ? null : total.startCum); |
| | | hssfNum(row, 7, total.yearCum == 0 ? null : total.yearCum); |
| | | continue; |
| | | } |
| | | if (b.isEmpty() && isCityLabel(a)) { |
| | | currentCity = templateCityFull(a); |
| | | seq = 0; |
| | | blankInvestRow(row, 12); |
| | | hssfText(row, 0, a); |
| | | InvestAgg agg = new InvestAgg(); |
| | | for (InvestmentMonthly m : mine) { |
| | | InvestmentProject p = pmap.get(m.getProjectId()); |
| | | if (p != null && currentCity.equals(p.getCity())) agg.add(m, p); |
| | | } |
| | | hssfNum(row, 5, agg.total == 0 ? null : agg.total); |
| | | hssfNum(row, 6, agg.startCum == 0 ? null : agg.startCum); |
| | | hssfNum(row, 7, agg.yearCum == 0 ? null : agg.yearCum); |
| | | continue; |
| | | } |
| | | // 项目行:按 模板名称+市州 匹配 DB 十五五项目,未匹配整行清空 |
| | | blankInvestRow(row, 12); |
| | | InvestmentProject p = matchInvest(fiveProjects, b, currentCity, null); |
| | | if (p == null) continue; |
| | | if (sec != null) sec.matched.add(p.getId()); |
| | | InvestmentMonthly m = mByProject.get(p.getId()); |
| | | fillFiveYearProjectRow(row, p, m, ++seq); |
| | | } |
| | | // 动态插行:DB 十五五项目数 > 模板预留行数时,在市州小节末尾补齐 |
| | | for (int i = sections.size() - 1; i >= 0; i--) { |
| | | InvestSection s = sections.get(i); |
| | | if (s.city == null) continue; |
| | | List<InvestmentProject> extra = new ArrayList<>(); |
| | | for (InvestmentProject p : fiveProjects) { |
| | | if (s.city.equals(RegionUtil.normalizeCityName(p.getCity())) |
| | | && !s.matched.contains(p.getId())) extra.add(p); |
| | | } |
| | | if (extra.isEmpty()) continue; |
| | | HSSFRow styleRow = s.styleRowIdx >= 0 ? sheet.getRow(s.styleRowIdx) : globalStyle; |
| | | int pos = s.projectRows.isEmpty() ? s.startRow : s.projectRows.get(s.projectRows.size() - 1); |
| | | int first = insertRowsDown(sheet, pos, extra.size(), styleRow); |
| | | for (int j = 0; j < extra.size(); j++) { |
| | | HSSFRow row = sheet.getRow(first + j); |
| | | if (row == null) continue; |
| | | InvestmentProject p = extra.get(j); |
| | | fillFiveYearProjectRow(row, p, mByProject.get(p.getId()), 0); |
| | | } |
| | | } |
| | | renumberInvestSeq(sheet, "investFiveYear"); |
| | | return toBytesHssf(wb); |
| | | } |
| | | |
| | | /** 表4:(经济强县项目)湖北省交通物流基础设施投资统计报表(模板底稿原位替换:保留全部行/合并/列宽,仅替换数据;DB 项目多于模板预留行时动态插行) */ |
| | | public byte[] investCounty(String period, String mode) throws Exception { |
| | | List<InvestmentMonthly> monthlies = investMonthlyMapper.selectList( |
| | | new LambdaQueryWrapper<InvestmentMonthly>() |
| | | .eq(InvestmentMonthly::getReportPeriod, period)); |
| | | Map<Long, InvestmentProject> pmap = new HashMap<>(); |
| | | List<InvestmentProject> countyProjects = new java.util.ArrayList<>(); |
| | | for (InvestmentProject p : investProjectMapper.selectList(null)) { |
| | | pmap.put(p.getId(), p); |
| | | if (Integer.valueOf(1).equals(p.getIsCounty())) countyProjects.add(p); |
| | | } |
| | | Map<Long, InvestmentMonthly> mByProject = new HashMap<>(); |
| | | for (InvestmentMonthly m : monthlies) mByProject.put(m.getProjectId(), m); |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, mode); |
| | | |
| | | HSSFWorkbook wb = loadInvestTemplate("模板_经济强县投资统计报表.xls"); |
| | | HSSFSheet sheet = wb.getSheetAt(0); |
| | | hssfText(sheet.getRow(0), 0, "(经济强县项目)湖北省交通物流基础设施投资统计报表(" + month + "月月报)"); |
| | | hssfText(sheet.getRow(1), 0, "全省货运(物流)基础设施建设投资月报(" + year + "年" + month + "月)"); |
| | | int lastRow = Math.min(sheet.getLastRowNum(), 65533); |
| | | int seq = 0; |
| | | int lastProjectRow = -1; |
| | | Set<Long> matched = new HashSet<>(); |
| | | for (int r = 4; r <= lastRow; r++) { |
| | | HSSFRow row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | String name = nvl(cellText(row.getCell(1))); |
| | | if (name.isEmpty()) continue; // 模板尾部空行不处理 |
| | | lastProjectRow = r; |
| | | blankInvestRow(row, 20); |
| | | InvestmentProject p = matchInvest(countyProjects, name, null, null); |
| | | if (p == null) continue; |
| | | matched.add(p.getId()); |
| | | InvestmentMonthly m = mByProject.get(p.getId()); |
| | | fillCountyProjectRow(row, p, m, ++seq); |
| | | } |
| | | // 动态插行:DB 经济强县项目数 > 模板预留行数时,在项目区末尾补齐 |
| | | List<InvestmentProject> extra = new ArrayList<>(); |
| | | for (InvestmentProject p : countyProjects) { |
| | | if (!matched.contains(p.getId())) extra.add(p); |
| | | } |
| | | if (!extra.isEmpty()) { |
| | | HSSFRow styleRow = sheet.getRow(4); |
| | | int pos = lastProjectRow >= 4 ? lastProjectRow : 3; |
| | | int first = insertRowsDown(sheet, pos, extra.size(), styleRow); |
| | | for (int j = 0; j < extra.size(); j++) { |
| | | HSSFRow row = sheet.getRow(first + j); |
| | | if (row == null) continue; |
| | | InvestmentProject p = extra.get(j); |
| | | fillCountyProjectRow(row, p, mByProject.get(p.getId()), 0); |
| | | } |
| | | } |
| | | renumberInvestSeq(sheet, "investCounty"); |
| | | return toBytesHssf(wb); |
| | | } |
| | | |
| | | |
| | | /** 表3:湖北省(客货站场)亿元投资项目(模板底稿 .xls,投资总表 + 规上项目表 两 sheet) */ |
| | | public byte[] investBillion(String period, String mode) throws Exception { |
| | | List<InvestmentMonthly> monthlies = investMonthlyMapper.selectList( |
| | | new LambdaQueryWrapper<InvestmentMonthly>() |
| | | .eq(InvestmentMonthly::getReportPeriod, period)); |
| | | Map<Long, InvestmentProject> pmap = new HashMap<>(); |
| | | for (InvestmentProject p : investProjectMapper.selectList(null)) pmap.put(p.getId(), p); |
| | | List<InvestmentMonthly> mine = new java.util.ArrayList<>(); |
| | | for (InvestmentMonthly m : monthlies) { |
| | | InvestmentProject p = pmap.get(m.getProjectId()); |
| | | if (p != null && Integer.valueOf(1).equals(p.getIsBillion())) mine.add(m); |
| | | } |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, mode); |
| | | |
| | | HSSFWorkbook wb = loadInvestTemplate("模板_亿元投资项目.xls"); |
| | | // ---- sheet1 投资总表:固定骨架,仅填(五)其他 全年预计(全省本年计划折亿) ---- |
| | | HSSFSheet s1 = wb.getSheetAt(0); |
| | | hssfText(s1.getRow(0), 0, year + "年固定资产投资预计完成情况"); |
| | | double provinceYearPlan = 0; |
| | | for (InvestmentMonthly m : monthlies) provinceYearPlan += nz(m.getYearPlan()); |
| | | HSSFRow rowOther = s1.getRow(9); |
| | | for (int c = 1; c <= 5; c++) hssfNum(rowOther, c, null); |
| | | hssfNum(rowOther, 5, provinceYearPlan == 0 ? null : round(provinceYearPlan / 10000, 2)); |
| | | // ---- sheet2 规上项目表:表头年份 + 合计 + 项目行 ---- |
| | | HSSFSheet s2 = wb.getSheetAt(1); |
| | | hssfText(s2.getRow(0), 0, year + "年交通建设项目储备情况(规模以上项目)"); |
| | | hssfText(s2.getRow(2), 5, "截止到" + year + "年" + month + "月底建设状态"); |
| | | hssfText(s2.getRow(2), 9, "截至" + (year - 1) + "年年底实际完成投资(万元)"); |
| | | hssfText(s2.getRow(2), 10, year + "年1-3月实际完成投资(万元)"); |
| | | hssfText(s2.getRow(2), 11, year + "年4月计划完成投资(万元)"); |
| | | hssfText(s2.getRow(2), 12, year + "年第二季度计划完成投资(万元)"); |
| | | hssfText(s2.getRow(2), 13, year + "年全年计划完成投资(万元)"); |
| | | hssfText(s2.getRow(2), 16, year + "年1-4月计划完成投资(万元)"); |
| | | hssfText(s2.getRow(2), 17, year + "年4月实际完成投资(万元)"); |
| | | hssfText(s2.getRow(2), 18, year + "年1-4月累计完成投资(万元)"); |
| | | hssfText(s2.getRow(2), 19, year + "年5月实际完成投资(万元)"); |
| | | hssfText(s2.getRow(2), 21, year + "年6月实际完成投资(万元)"); |
| | | hssfText(s2.getRow(2), 22, year + "年第二季度实际完成投资(万元)"); |
| | | hssfText(s2.getRow(2), 23, year + "年" + month + "月实际完成投资"); |
| | | // 合计行 R4(col0-1 属于表头合并区,保留;清空并重写数据列) |
| | | HSSFRow totalRow = s2.getRow(4); |
| | | for (int c = 2; c <= 23; c++) hssfNum(totalRow, c, null); |
| | | InvestAgg total = new InvestAgg(); |
| | | for (InvestmentMonthly m : mine) total.add(m, pmap.get(m.getProjectId())); |
| | | hssfNum(totalRow, 8, total.total == 0 ? null : total.total); |
| | | hssfNum(totalRow, 13, total.yearPlan == 0 ? null : total.yearPlan); |
| | | hssfNum(totalRow, 23, sumMonth(mine) == 0 ? null : sumMonth(mine)); |
| | | // 项目行:保留模板 R79 注释行 |
| | | HSSFRow styleProject = s2.getRow(5); |
| | | trimHssfTail(s2, 5, 79); |
| | | int rowIdx = 5; |
| | | for (InvestmentMonthly m : mine) { |
| | | InvestmentProject p = pmap.get(m.getProjectId()); |
| | | HSSFRow row = createRowLike(s2, rowIdx++, styleProject); |
| | | hssfText(row, 0, "湖北省"); |
| | | hssfText(row, 1, RegionUtil.shortName(p.getCity())); |
| | | hssfText(row, 2, p.getProjectName()); |
| | | hssfText(row, 3, "客运站场".equals(p.getCategory()) ? "综合客运枢纽" : "综合货运枢纽"); |
| | | hssfText(row, 5, valueOf(m.getProgressStage())); |
| | | hssfText(row, 6, valueOf(p.getConstructNature())); |
| | | hssfNum(row, 7, parseTimeYMNum(p.getStartTime())); |
| | | hssfNum(row, 8, p.getTotalInvestment()); |
| | | hssfNum(row, 13, m.getYearPlan()); |
| | | hssfNum(row, 23, m.getMonthDone()); |
| | | } |
| | | trimHssfTail(s2, rowIdx, 79); |
| | | return toBytesHssf(wb); |
| | | } |
| | | |
| | | /** 空串归一(cellText 可能返回 null) */ |
| | | private String nvl(String v) { |
| | | return v == null ? "" : v.trim(); |
| | | } |
| | | |
| | | /** 清空模板数据行指定范围单元格(保留样式与合并单元格) */ |
| | | private void blankInvestRow(HSSFRow row, int lastCol) { |
| | | if (row == null) return; |
| | | for (int c = 0; c <= lastCol; c++) { |
| | | Cell cell = row.getCell(c); |
| | | if (cell != null) cell.setBlank(); |
| | | } |
| | | } |
| | | |
| | | /** 模板市州短名 -> 规范市州名(武汉->武汉市、林区->神农架林区、恩施州->恩施州) */ |
| | | private String templateCityFull(String label) { |
| | | if (label == null) return null; |
| | | String t = label.trim(); |
| | | String canon = RegionUtil.normalizeCityName(t); |
| | | for (String city : RegionUtil.cityList()) { |
| | | if (city.equals(canon)) return city; |
| | | } |
| | | for (String city : RegionUtil.cityList()) { |
| | | if (RegionUtil.shortName(city).equals(canon) || RegionUtil.shortName(city).equals(t)) return city; |
| | | } |
| | | return canon; |
| | | } |
| | | |
| | | /** 判断模板行 A 列是否为市州小节标签 */ |
| | | private boolean isCityLabel(String label) { |
| | | if (label == null || label.isEmpty()) return false; |
| | | String full = templateCityFull(label); |
| | | return full != null && RegionUtil.cityList().contains(full); |
| | | } |
| | | |
| | | /** 在候选项目池中按名称匹配 DB 项目(可选 市州/类别 过滤 + 县区前缀消歧),未命中返回 null */ |
| | | private InvestmentProject matchInvest(List<InvestmentProject> pool, String name, String cityFull, String category) { |
| | | return matchInvest(pool, name, cityFull, category, null); |
| | | } |
| | | |
| | | /** 带已占用排除的匹配:同一 DB 项目只允许填充一行(避免模板一期/二期同名行重复输出) */ |
| | | private InvestmentProject matchInvest(List<InvestmentProject> pool, String name, String cityFull, String category, Set<Long> excludeIds) { |
| | | if (name == null || name.trim().isEmpty()) return null; |
| | | String norm = normInvestName(name); |
| | | if (norm.isEmpty()) return null; |
| | | String cityCanon = cityFull == null || cityFull.trim().isEmpty() ? null : RegionUtil.normalizeCityName(cityFull); |
| | | String county = extractInvestCounty(name); |
| | | List<InvestmentProject> cands = new java.util.ArrayList<>(); |
| | | for (InvestmentProject p : pool) { |
| | | if (excludeIds != null && excludeIds.contains(p.getId())) continue; |
| | | if (category != null && !category.equals(p.getCategory())) continue; |
| | | if (cityCanon != null && !cityCanon.equals(RegionUtil.normalizeCityName(p.getCity()))) continue; |
| | | cands.add(p); |
| | | } |
| | | if (cands.isEmpty()) return null; |
| | | // 归一化同名多候选:县区前缀消歧(如 罗田县综合物流园 vs 咸丰县综合物流园) |
| | | if (!county.isEmpty()) { |
| | | for (InvestmentProject p : cands) { |
| | | if (normInvestName(p.getProjectName()).equals(norm) |
| | | && p.getProjectName() != null && p.getProjectName().contains(county)) { |
| | | return p; |
| | | } |
| | | } |
| | | } |
| | | InvestmentProject best = null; |
| | | int bestScore = Integer.MIN_VALUE; |
| | | for (InvestmentProject p : cands) { |
| | | String pn = normInvestName(p.getProjectName()); |
| | | if (pn.isEmpty()) continue; |
| | | int s = investNameScore(norm, pn, county, p.getProjectName()); |
| | | if (s > bestScore) { |
| | | bestScore = s; |
| | | best = p; |
| | | } |
| | | } |
| | | return bestScore > 0 ? best : null; |
| | | } |
| | | |
| | | /** 项目名归一化:去括号字符、连接符,罗马数字统一,去省/市/县/区前缀与空白(与审核引擎一致) */ |
| | | private String normInvestName(String name) { |
| | | if (name == null) return ""; |
| | | String s = name.trim(); |
| | | s = s.replaceAll("[((]", "").replaceAll("[))]", ""); |
| | | s = s.replaceAll("[·•—-_\\-]", ""); |
| | | s = s.replaceAll("[ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹ]", "I"); |
| | | s = s.replaceAll("^(湖北省|武汉市|黄石市|十堰市|宜昌市|襄阳市|鄂州市|荆门市|孝感市|荆州市|黄冈市|咸宁市|随州市|恩施州|仙桃市|潜江市|天门市|神农架林区)", ""); |
| | | s = s.replaceAll("^(?:[\\u4e00-\\u9fa5]{2,4}(?:县|市|区))", ""); |
| | | s = s.replaceAll("\\s+", ""); |
| | | return s; |
| | | } |
| | | |
| | | /** 名称相似度评分:精确>前缀包含>中后部包含>5字>子序列>最长公共子串;县区前缀不一致拒绝/大扣分 */ |
| | | private int investNameScore(String norm, String key, String county, String rawB) { |
| | | if (norm.equals(key)) { |
| | | // 归一化后同名:若模板带县区前缀而候选原始名不含该县区 → 拒绝(防 宜都市 vs 当阳市 误配) |
| | | if (county != null && !county.isEmpty() && (rawB == null || !rawB.contains(county))) return -1; |
| | | return 2000; |
| | | } |
| | | if (key.length() < 5 || norm.length() < 5) return -1; |
| | | String shortSide = key.length() <= norm.length() ? key : norm; |
| | | String longSide = key.length() > norm.length() ? key : norm; |
| | | int idx = longSide.indexOf(shortSide); |
| | | int lenDiff = Math.abs(key.length() - norm.length()); |
| | | int base; |
| | | if (idx == 0) { |
| | | base = 1000 - lenDiff * 2; |
| | | } else if (idx > 0 && shortSide.length() >= 6) { |
| | | base = 500 - idx * 5 - lenDiff * 2 + shortSide.length(); |
| | | } else if (idx > 0 && shortSide.length() == 5 && lenDiff <= 5) { |
| | | String diff = longSide.substring(0, idx) + longSide.substring(idx + shortSide.length()); |
| | | if (diff.matches(".*(县|市|州).*")) return -1; |
| | | base = 400 - idx * 5 - lenDiff; |
| | | } else if (isInvestSubsequence(shortSide, longSide) && lenDiff <= 8) { |
| | | base = 300 - lenDiff * 3; |
| | | } else { |
| | | int l = investLcs(norm, key); |
| | | if (l >= 6) { |
| | | base = 200 + l * 5 - lenDiff; |
| | | } else if (l == 5 && lenDiff <= 4 && county != null && !county.isEmpty() |
| | | && rawB != null && rawB.contains(county)) { |
| | | base = 150 + l * 5 - lenDiff; |
| | | } else { |
| | | return -1; |
| | | } |
| | | } |
| | | if (county != null && !county.isEmpty() && (rawB == null || !rawB.contains(county))) { |
| | | base -= 400; |
| | | } |
| | | return base; |
| | | } |
| | | |
| | | /** 判断短名是否为长名的字符子序列 */ |
| | | private boolean isInvestSubsequence(String shortSide, String longSide) { |
| | | int i = 0; |
| | | for (int j = 0; i < shortSide.length() && j < longSide.length(); j++) { |
| | | if (shortSide.charAt(i) == longSide.charAt(j)) i++; |
| | | } |
| | | return i == shortSide.length(); |
| | | } |
| | | |
| | | /** 最长公共子串长度 */ |
| | | private int investLcs(String a, String b) { |
| | | int n = a.length(), m = b.length(); |
| | | if (n == 0 || m == 0) return 0; |
| | | int[][] dp = new int[n + 1][m + 1]; |
| | | int max = 0; |
| | | for (int i = 1; i <= n; i++) { |
| | | for (int j = 1; j <= m; j++) { |
| | | if (a.charAt(i - 1) == b.charAt(j - 1)) { |
| | | dp[i][j] = dp[i - 1][j - 1] + 1; |
| | | if (dp[i][j] > max) max = dp[i][j]; |
| | | } |
| | | } |
| | | } |
| | | return max; |
| | | } |
| | | |
| | | /** 提取名称中的 县/市/区 前缀(如 咸丰县、宜都市) */ |
| | | private String extractInvestCounty(String name) { |
| | | if (name == null) return ""; |
| | | java.util.regex.Matcher mm = java.util.regex.Pattern.compile("([\\u4e00-\\u9fa5]{2,4}(?:县|市|区))").matcher(name); |
| | | if (mm.find()) return mm.group(1); |
| | | return ""; |
| | | } |
| | | |
| | | |
| | | private double sumMonth(List<InvestmentMonthly> list) { |
| | | double s = 0; |
| | | for (InvestmentMonthly m : list) s += nz(m.getMonthDone()); |
| | | return s; |
| | | } |
| | | |
| | | private String padTimeYM(String t) { |
| | | if (t == null) return null; |
| | | String x = t.trim(); |
| | | if (x.matches("\\d{4}")) return x + "01"; |
| | | return x; |
| | | } |
| | | |
| | | private String valueOf(String v) { |
| | | return v == null || v.trim().isEmpty() ? "" : v.trim(); |
| | | } |
| | | 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(); |
| | | return out.toByteArray(); |
| | | } |
| | | } |
| | | } |