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 类输出报表 * 货运量口径:合计=模板_货运量周转量(左半今年1-N月,右半去年1-12月);规上=H203-2按17市州汇总;规下=合计-规上 * 周转量口径:全部来自规上规下拆分表(用户手动上传,含忠实导入的全省行) */ @Slf4j @Service public class ReportExportService { @Resource private H2032EnterpriseMonthlyMapper h2032Mapper; @Resource 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 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 checkExportReady(String period, String mode, List types) { Map result = new LinkedHashMap<>(); result.put("period", period); result.put("mode", mode); List> items = new ArrayList<>(); for (String t : types) { Map it = new LinkedHashMap<>(); it.put("key", t); List notes = new ArrayList<>(); int rows; List 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; 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); } 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 reportPreview(String period, String mode, List types) { Map ready = checkExportReady(period, mode, types); @SuppressWarnings("unchecked") List> items = (List>) ready.get("items"); Map> byKey = new LinkedHashMap<>(); for (Map 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) || isCityPassengerType(t)) { cityTaxi = true; cityBus = true; } else if (isInvestType(t)) invest = true; else if ("energySummary".equals(t)) energy = true; } // 月度覆盖:该年 1..month 中哪些月有源数据 Set freightCover = new TreeSet<>(); Set passengerCover = new TreeSet<>(); Set busCover = new TreeSet<>(); Set taxiCover = new TreeSet<>(); Set 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().likeRight(PassengerEnterpriseMonthly::getReportPeriod, yearPrefix)), o -> ((PassengerEnterpriseMonthly) o).getReportPeriod(), month)); passengerCover.addAll(monthsWithData(() -> passengerIndividualMapper.selectList(new LambdaQueryWrapper().likeRight(PassengerIndividualMonthly::getReportPeriod, yearPrefix)), o -> ((PassengerIndividualMonthly) o).getReportPeriod(), month)); } if (cityBus) { busCover.addAll(monthsWithData(() -> cityBusMapper.selectList(new LambdaQueryWrapper().likeRight(CityBusMonthly::getReportPeriod, yearPrefix)), o -> ((CityBusMonthly) o).getReportPeriod(), month)); } if (cityTaxi) { taxiCover.addAll(monthsWithData(() -> cityTaxiMapper.selectList(new LambdaQueryWrapper().likeRight(CityTaxiMonthly::getReportPeriod, yearPrefix)), o -> ((CityTaxiMonthly) o).getReportPeriod(), month)); } if (invest) { investCover.addAll(monthsWithData(() -> investMonthlyMapper.selectList(new LambdaQueryWrapper().likeRight(InvestmentMonthly::getReportPeriod, yearPrefix)), o -> ((InvestmentMonthly) o).getReportPeriod(), month)); } for (Map.Entry> e : byKey.entrySet()) { String t = e.getKey(); Map it = e.getValue(); List> summary = new ArrayList<>(); List problems = new ArrayList<>(); List missing = new ArrayList<>(); List coverage = new ArrayList<>(); if (Boolean.FALSE.equals(it.get("audited"))) problems.add("该报表期尚未审核通过"); 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 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 monthsWithData(java.util.function.Supplier> rows, java.util.function.Function periodOf, int limit) { Set 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 missingMonths(Set coverage, int limit) { List missing = new ArrayList<>(); for (int m = 1; m <= limit; m++) if (!coverage.contains(m)) missing.add(m); return missing; } private String joinMonths(List 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> summary, String label, Double value, String unit) { Map 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> summary, List problems) { Map ft = loadFreightTurnover(period); Map 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> summary, List problems) { Map> data = loadPassengerAggMap(); Map> 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 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> summary, List 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> summary, List problems) { List list = investMonthlyMapper.selectList(periodQw(period, mode, InvestmentMonthly::getReportPeriod)); Set 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> summary, List 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 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 notes) { return addReadyCount(() -> passengerMapper.selectCount(periodQw(period, mode, PassengerEnterpriseMonthly::getReportPeriod)), "H203-1 旅客月报", notes) + addReadyCount(() -> passengerIndividualMapper.selectCount(periodQw(period, mode, PassengerIndividualMonthly::getReportPeriod)), "个体客运量/周转量", notes); } private LambdaQueryWrapper periodQw(String period, String mode, SFunction periodGetter) { LambdaQueryWrapper 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 counter, String label, List 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 rules = auditRuleMapper.selectList(new LambdaQueryWrapper() .eq(AuditRule::getReportType, ruleType) .eq(AuditRule::getIsEnabled, 1)); if (rules.isEmpty()) return 0; List ruleIds = new ArrayList<>(); for (AuditRule r : rules) ruleIds.add(r.getId()); LambdaQueryWrapper 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 qw = new LambdaQueryWrapper() .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, String mode) throws Exception { int currentMonth = monthOf(period, mode); int fillMonths = currentMonth; // 1..N 月(N>6 时模板自动向右扩列到 12 月) boolean extended = fillMonths > 6; Map> monthData = loadMonthData(period); Map provinceMonthMap = loadProvinceMonthMap(period); ScaleSplitTransport provinceCum = getProvinceCumulative(period); Map cumMap = loadCumulativeMap(period); Map> h2032Freight = loadH2032FreightByMonth(period); Map h2032FreightCum = loadH2032FreightCumulative(period); Map ftMap = loadFreightTurnover(period); // 以 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 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); } } // ==================== 2. 生成_货运量排名.xlsx ==================== public byte[] exportFreightRank(String period, String mode) throws Exception { String year = period.substring(0, 4); int month = monthOf(period, mode); Map ftMap = loadFreightTurnover(period); Map h2032FreightCum = loadH2032FreightCumulative(period); Map h2032FreightYoy = computeH2032Yoy(period); Map h2032FreightCumLast = loadH2032FreightCumulative( (Integer.parseInt(period.substring(0, 4)) - 1) + period.substring(4)); Map above = new HashMap<>(); Map below = new HashMap<>(); Map total = new HashMap<>(); Map aboveYoy = new HashMap<>(); Map belowYoy = new HashMap<>(); Map totalYoy = new HashMap<>(); double provAbove = 0.0; for (Double v : h2032FreightCum.values()) { if (v != null) provAbove += v; } Double provAboveWan = round(provAbove / 10000.0, 4); 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); double aWan = a == null ? 0.0 : round(a / 10000.0, 4); Double t = freightCum(ftMap.get(city), month); above.put(city, aWan); total.put(city, t); below.put(city, t == null ? null : round(t - aWan, 4)); aboveYoy.put(city, h2032FreightYoy.get(city)); // 规下同比 = (规下今年 - 规下去年) / 规下去年;规下去年 = 模板去年合计 - 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)); } // 以 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); fillFreightRankBlock(sheet, 25, above, below, total, aboveYoy, belowYoy, totalYoy, provAboveWan, provBelow, provTotal, provAboveYoy, provBelowYoy, provTotalYoy); try { wb.getCreationHelper().createFormulaEvaluator().evaluateAll(); } catch (Exception e) { log.warn("货运量排名模板公式求值失败: {}", e.getMessage()); } return toBytes(wb); } } // ==================== 3. 生成_周转量排名.xlsx ==================== public byte[] exportTurnoverRank(String period, String mode) throws Exception { String year = period.substring(0, 4); int month = monthOf(period, mode); Map cumMap = loadCumulativeMap(period); ScaleSplitTransport province = getProvinceCumulative(period); // 以 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); fillTurnoverRankBlock(sheet, 25, cumMap, province); 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("(? 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 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=个体周转量) */ private double passengerMonthVal(Map> data, Map> indi, int year, int month, String region, int metricIdx) { if (metricIdx <= 1) { PassengerAgg agg = aggOf(data, year, month, region); Double val = agg == null ? null : (metricIdx == 0 ? agg.passengerTotal / 10000.0 : agg.turnoverTotal / 10000.0); return val == null ? 0.0 : val; } return individualVal(indi, year, month, region, metricIdx - 2); } /** 旅客累计同比(去年 1..months 月累计):无去年数据返回 null */ private Double passengerCumYoy(Map> data, Map> 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> 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> mid, int year, int months, int colIdx) { List 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 monthValue, java.util.function.IntFunction 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); } /** 货运量排名块填充:模板 1-based r4 起(全省+17市州),Q/R 占比公式保留不动 */ private void fillFreightRankBlock(Sheet sheet, int start, Map above, Map below, Map total, Map aboveYoy, Map belowYoy, Map 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 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(不含全省) */ private Map> loadMonthData(String period) { Map> result = new HashMap<>(); List list = scaleSplitMapper.selectList( new LambdaQueryWrapper() .eq(ScaleSplitTransport::getPeriodType, "MONTH")); for (ScaleSplitTransport record : list) { if (record.getReportPeriod() == null) continue; if (record.getReportPeriod().compareTo(period) > 0) continue; String city = RegionUtil.normalizeCityName(record.getRegionName()); if (city == null) continue; if ("湖北省".equals(city)) continue; int month = parseMonth(record.getReportPeriod()); result.computeIfAbsent(city, k -> new HashMap<>()).put(month, record); } return result; } /** 累计数据: city -> 目标期及之前最近一期(不含全省) */ private Map loadCumulativeMap(String period) { Map result = new HashMap<>(); List list = scaleSplitMapper.selectList( new LambdaQueryWrapper() .eq(ScaleSplitTransport::getPeriodType, "CUMULATIVE")); for (ScaleSplitTransport record : list) { if (record.getReportPeriod() == null) continue; if (record.getReportPeriod().compareTo(period) > 0) continue; String city = RegionUtil.normalizeCityName(record.getRegionName()); if (city == null) continue; if ("湖北省".equals(city)) continue; ScaleSplitTransport existing = result.get(city); if (existing == null || record.getReportPeriod().compareTo(existing.getReportPeriod()) > 0) { result.put(city, record); } } return result; } /** 全省当月: month -> record(忠实读取导入的全省行) */ private Map loadProvinceMonthMap(String period) { Map result = new HashMap<>(); List list = scaleSplitMapper.selectList( new LambdaQueryWrapper() .eq(ScaleSplitTransport::getPeriodType, "MONTH")); for (ScaleSplitTransport record : list) { if (record.getReportPeriod() == null) continue; if (record.getReportPeriod().compareTo(period) > 0) continue; String name = RegionUtil.normalizeCityName(record.getRegionName()); if (!"湖北省".equals(name)) continue; int month = parseMonth(record.getReportPeriod()); result.put(month, record); } return result; } /** 全省累计(忠实读取导入的全省行,不求和) */ private ScaleSplitTransport getProvinceCumulative(String period) { List list = scaleSplitMapper.selectList( new LambdaQueryWrapper() .eq(ScaleSplitTransport::getPeriodType, "CUMULATIVE")); ScaleSplitTransport best = null; for (ScaleSplitTransport record : list) { if (record.getReportPeriod() == null) continue; if (record.getReportPeriod().compareTo(period) > 0) continue; String name = RegionUtil.normalizeCityName(record.getRegionName()); if (!"湖北省".equals(name)) continue; if (best == null || record.getReportPeriod().compareTo(best.getReportPeriod()) > 0) best = record; } return best; } /** H2032规上: city -> month -> 货运量(吨) */ private Map> loadH2032FreightByMonth(String period) { Map> result = new HashMap<>(); List list = h2032Mapper.selectList(null); for (H2032EnterpriseMonthly record : list) { if (record.getReportPeriod() == null || record.getReportPeriod().compareTo(period) > 0) continue; String city = RegionUtil.cityByCode(record.getRegionCode()); if (city == null) continue; int month = parseMonth(record.getReportPeriod()); double freight = record.getFreightTotal() == null ? 0.0 : record.getFreightTotal(); result.computeIfAbsent(city, k -> new HashMap<>()).merge(month, freight, Double::sum); } return result; } /** H2032规上累计: city -> 货运量(吨) */ private Map loadH2032FreightCumulative(String period) { Map result = new HashMap<>(); List list = h2032Mapper.selectList(null); for (H2032EnterpriseMonthly record : list) { if (record.getReportPeriod() == null || record.getReportPeriod().compareTo(period) > 0) continue; String city = RegionUtil.cityByCode(record.getRegionCode()); if (city == null) continue; double freight = record.getFreightTotal() == null ? 0.0 : record.getFreightTotal(); result.merge(city, freight, Double::sum); } return result; } /** H2032规上同比(需去年同期的H2032数据,暂无则为空) */ private Map computeH2032Yoy(String period) { Map result = new HashMap<>(); String lastYear = (Integer.parseInt(period.substring(0, 4)) - 1) + period.substring(4); Map current = loadH2032FreightCumulative(period); Map last = loadH2032FreightCumulative(lastYear); for (Map.Entry entry : current.entrySet()) { double lastVal = last.getOrDefault(entry.getKey(), 0.0); if (lastVal != 0.0) { result.put(entry.getKey(), (entry.getValue() - lastVal) / lastVal); } } return result; } /** 模板_货运量周转量: city -> 行数据(含湖北省) */ private Map loadFreightTurnover(String period) { Map result = new HashMap<>(); List list = freightTurnoverMapper.selectList( new LambdaQueryWrapper() .eq(FreightTurnoverImport::getReportPeriod, period)); for (FreightTurnoverImport e : list) { String city = RegionUtil.normalizeCityName(e.getRegionName()); if (city != null) result.put(city, e); } return result; } // ==================== 排名行写入 ==================== /** 排名表双行表头 */ private int writeRankHead(Sheet sheet, int rowIdx, String metric, String unit) { Row h1 = sheet.createRow(rowIdx++); h1.createCell(0).setCellValue("市州"); h1.createCell(1).setCellValue("规上" + metric); h1.createCell(4).setCellValue("规上增速"); h1.createCell(6).setCellValue("规下" + metric); h1.createCell(9).setCellValue("规下增速"); h1.createCell(11).setCellValue("合计" + metric); h1.createCell(14).setCellValue("合计增速"); h1.createCell(16).setCellValue("分市州规上规下占比"); Row h2 = sheet.createRow(rowIdx++); h2.createCell(1).setCellValue("累计完成 (万" + unit + ")"); h2.createCell(2).setCellValue("排名"); h2.createCell(3).setCellValue("占全省比重"); h2.createCell(4).setCellValue("同比"); h2.createCell(5).setCellValue("增速 排名"); h2.createCell(6).setCellValue("累计完成 (万" + unit + ")"); h2.createCell(7).setCellValue("排名"); h2.createCell(8).setCellValue("占全省比重"); h2.createCell(9).setCellValue("同比"); h2.createCell(10).setCellValue("增速排名"); h2.createCell(11).setCellValue("累计完成 (万" + unit + ")"); h2.createCell(12).setCellValue("排名"); h2.createCell(13).setCellValue("占全省比重"); h2.createCell(14).setCellValue("同比"); h2.createCell(15).setCellValue("增速排名"); return rowIdx; } /** 货运量排名行:规上=H2032累计、合计=模板累计、规下=合计-规上(单位:万吨) */ private int writeFreightRankRows(Sheet sheet, int rowIdx, String city, Map above, Map below, Map total, Map aboveYoy, Map belowYoy, Map totalYoy, Double provAbove, Double provBelow, Double provTotal, Double provAboveYoy, Double provBelowYoy, Double provTotalYoy, boolean isProvince) { Row row = sheet.createRow(rowIdx); row.createCell(0).setCellValue(isProvince ? "全省" : RegionUtil.shortName(city)); Double a = isProvince ? provAbove : above.get(city); Double b = isProvince ? provBelow : below.get(city); Double t = isProvince ? provTotal : total.get(city); Double ay = isProvince ? provAboveYoy : aboveYoy.get(city); Double by = isProvince ? provBelowYoy : belowYoy.get(city); Double ty = isProvince ? provTotalYoy : totalYoy.get(city); setNumeric(row, 1, a); setNumeric(row, 2, isProvince ? null : rankOfMap(above, city)); setNumeric(row, 3, isProvince ? null : ratioOf(a, provAbove)); setNumeric(row, 4, ay); setNumeric(row, 5, isProvince ? null : yoyRankOfMap(aboveYoy, city)); setNumeric(row, 6, b); setNumeric(row, 7, isProvince ? null : rankOfMap(below, city)); setNumeric(row, 8, isProvince ? null : ratioOf(b, provBelow)); setNumeric(row, 9, by); setNumeric(row, 10, isProvince ? null : yoyRankOfMap(belowYoy, city)); setNumeric(row, 11, t); setNumeric(row, 12, isProvince ? null : rankOfMap(total, city)); setNumeric(row, 13, isProvince ? null : ratioOf(t, provTotal)); setNumeric(row, 14, ty); setNumeric(row, 15, isProvince ? null : yoyRankOfMap(totalYoy, city)); if (t != null && t > 0 && a != null) { double share = round(a * 10.0 / t, 1); setNumeric(row, 16, share); setNumeric(row, 17, round(10.0 - share, 1)); } return rowIdx + 1; } private int writeTurnoverRankRows(Sheet sheet, int rowIdx, String city, Map cumMap, ScaleSplitTransport province, boolean isProvince) { if (province == null) province = new ScaleSplitTransport(); Row row = sheet.createRow(rowIdx); row.createCell(0).setCellValue(isProvince ? "全省" : RegionUtil.shortName(city)); ScaleSplitTransport cum = isProvince ? null : cumMap.get(city); Double above = isProvince ? province.getAboveScaleTurnover() : (cum == null ? null : cum.getAboveScaleTurnover()); Double below = isProvince ? province.getBelowScaleTurnover() : (cum == null ? null : cum.getBelowScaleTurnover()); Double total = isProvince ? province.getTotalTurnover() : (cum == null ? null : cum.getTotalTurnover()); Double provinceAbove = province.getAboveScaleTurnover(); Double provinceBelow = province.getBelowScaleTurnover(); Double provinceTotal = province.getTotalTurnover(); setNumeric(row, 1, above); setNumeric(row, 2, isProvince || cum == null ? null : rankOfTurnover(cumMap, city, 0)); setNumeric(row, 3, isProvince ? null : ratioOf(above, provinceAbove)); setNumeric(row, 4, isProvince ? province.getAboveScaleYoy() : (cum == null ? null : cum.getAboveScaleYoy())); setNumeric(row, 5, isProvince || cum == null ? null : yoyRankOfTurnover(cumMap, city, 0)); setNumeric(row, 6, below); setNumeric(row, 7, isProvince || cum == null ? null : rankOfTurnover(cumMap, city, 1)); setNumeric(row, 8, isProvince ? null : ratioOf(below, provinceBelow)); setNumeric(row, 9, isProvince ? province.getBelowScaleYoy() : (cum == null ? null : cum.getBelowScaleYoy())); setNumeric(row, 10, isProvince || cum == null ? null : yoyRankOfTurnover(cumMap, city, 1)); setNumeric(row, 11, total); setNumeric(row, 12, isProvince || cum == null ? null : rankOfTurnover(cumMap, city, 2)); setNumeric(row, 13, isProvince ? null : ratioOf(total, provinceTotal)); setNumeric(row, 14, isProvince ? province.getTotalYoy() : (cum == null ? null : cum.getTotalYoy())); setNumeric(row, 15, isProvince || cum == null ? null : yoyRankOfTurnover(cumMap, city, 2)); if (total != null && total > 0 && above != null) { double share = round(above * 10.0 / total, 1); setNumeric(row, 16, share); setNumeric(row, 17, round(10.0 - share, 1)); } return rowIdx + 1; } // ==================== 明细取值辅助 ==================== /** 全省当月值:周转量取拆分表,货运量取模板(合计)/H2032(规上)/差值(规下) */ private Double getProvinceMonthValue(Map provinceMonthMap, FreightTurnoverImport ft, int month, String metric, String scale, Map> h2032Freight) { if ("turnover".equals(metric)) { return getMetric(provinceMonthMap.get(month), metric, scale); } if ("total".equals(scale)) return freightMonth(ft, month); if ("above".equals(scale)) { double sum = 0.0; for (Map byMonth : h2032Freight.values()) { Double v = byMonth == null ? null : byMonth.get(month); if (v != null) sum += v; } return round(sum / 10000.0, 4); } Double total = freightMonth(ft, month); Double above = getProvinceMonthValue(provinceMonthMap, ft, month, "freight", "above", h2032Freight); if (total == null) return null; return total - above; } /** 全省当月同比:周转量取拆分表,货运量合计取模板(左半vs右半) */ private Double getProvinceMonthYoy(Map provinceMonthMap, FreightTurnoverImport ft, int month, String metric, String scale) { if ("turnover".equals(metric)) { return getYoyMetric(provinceMonthMap.get(month), metric, scale); } if ("total".equals(scale)) return freightYoy(ft, month); return null; } /** 全省累计:周转量取拆分表累计,货运量合计=模板1-N月之和,规上=H2032累计,规下=差值 */ private Double getProvinceCumValue(ScaleSplitTransport provinceCum, FreightTurnoverImport ft, String metric, String scale, Map h2032FreightCum, int monthCount) { if ("turnover".equals(metric)) { return getMetric(provinceCum, metric, scale); } if ("total".equals(scale)) return freightCum(ft, monthCount); if ("above".equals(scale)) { double sum = 0.0; for (Double v : h2032FreightCum.values()) { if (v != null) sum += v; } return round(sum / 10000.0, 4); } Double total = freightCum(ft, monthCount); Double above = getProvinceCumValue(provinceCum, ft, "freight", "above", h2032FreightCum, monthCount); if (total == null) return null; return total - above; } /** 全省累计同比:周转量取拆分表,货运量合计=模板累计同比 */ private Double getProvinceCumYoy(ScaleSplitTransport provinceCum, FreightTurnoverImport ft, String metric, String scale, int monthCount) { if ("turnover".equals(metric)) { return getYoyMetric(provinceCum, metric, scale); } if ("total".equals(scale)) return freightCumYoy(ft, monthCount); return null; } /** 市州当月值 */ private Double getCityMonthValue(Map> monthData, FreightTurnoverImport ft, String city, int month, String metric, String scale, Map> h2032Freight) { if ("turnover".equals(metric)) { return getMetric(getRecord(monthData, city, month), metric, scale); } if ("total".equals(scale)) return freightMonth(ft, month); if ("above".equals(scale)) { Map byMonth = h2032Freight.get(city); Double v = byMonth == null ? null : byMonth.get(month); return v == null ? 0.0 : round(v / 10000.0, 4); } Double total = freightMonth(ft, month); Double above = getCityMonthValue(monthData, ft, city, month, "freight", "above", h2032Freight); if (total == null) return null; return total - above; } /** 市州当月同比 */ private Double getCityMonthYoy(Map> monthData, FreightTurnoverImport ft, String city, int month, String metric, String scale) { if ("turnover".equals(metric)) { return getYoyMetric(getRecord(monthData, city, month), metric, scale); } if ("total".equals(scale)) return freightYoy(ft, month); return null; } /** 市州累计 */ private Double getCityCumValue(ScaleSplitTransport cum, FreightTurnoverImport ft, String metric, String scale, Map h2032FreightCum, String city, int monthCount) { if ("turnover".equals(metric)) { return cum == null ? null : getMetric(cum, metric, scale); } if ("total".equals(scale)) return freightCum(ft, monthCount); if ("above".equals(scale)) { Double v = h2032FreightCum.get(city); return v == null ? 0.0 : round(v / 10000.0, 4); } Double total = freightCum(ft, monthCount); Double above = getCityCumValue(cum, ft, "freight", "above", h2032FreightCum, city, monthCount); if (total == null) return null; return total - above; } /** 市州累计同比 */ private Double getCityCumYoy(ScaleSplitTransport cum, FreightTurnoverImport ft, String metric, String scale, int monthCount) { if ("turnover".equals(metric)) { return cum == null ? null : getYoyMetric(cum, metric, scale); } if ("total".equals(scale)) return freightCumYoy(ft, monthCount); return null; } // ==================== 模板_货运量周转量取值 ==================== private Double freightMonth(FreightTurnoverImport e, int m) { if (e == null) return null; switch (m) { case 1: return e.getFreightM01(); case 2: return e.getFreightM02(); case 3: return e.getFreightM03(); case 4: return e.getFreightM04(); case 5: return e.getFreightM05(); case 6: return e.getFreightM06(); case 7: return e.getFreightM07(); case 8: return e.getFreightM08(); case 9: return e.getFreightM09(); case 10: return e.getFreightM10(); case 11: return e.getFreightM11(); case 12: return e.getFreightM12(); default: return null; } } private Double lastFreightMonth(FreightTurnoverImport e, int m) { if (e == null) return null; switch (m) { case 1: return e.getLastFreightM01(); case 2: return e.getLastFreightM02(); case 3: return e.getLastFreightM03(); case 4: return e.getLastFreightM04(); case 5: return e.getLastFreightM05(); case 6: return e.getLastFreightM06(); case 7: return e.getLastFreightM07(); case 8: return e.getLastFreightM08(); case 9: return e.getLastFreightM09(); case 10: return e.getLastFreightM10(); case 11: return e.getLastFreightM11(); case 12: return e.getLastFreightM12(); default: return null; } } /** 模板货运量当月同比 = (今年m - 去年m) / 去年m */ private Double freightYoy(FreightTurnoverImport e, int m) { Double cur = freightMonth(e, m); Double last = lastFreightMonth(e, m); if (cur == null || last == null || last == 0.0) return null; return (cur - last) / last; } /** 模板货运量累计 = 左半 1..monthCount 之和 */ private Double freightCum(FreightTurnoverImport e, int monthCount) { if (e == null) return null; double sum = 0.0; boolean any = false; for (int m = 1; m <= monthCount; m++) { Double v = freightMonth(e, m); if (v != null) { sum += v; any = true; } } return any ? sum : null; } /** 模板去年货运量累计 = 右半 1..monthCount 之和(去年1-12月固定,取前N个月) */ private Double lastFreightCum(FreightTurnoverImport e, int monthCount) { if (e == null) return null; double sum = 0.0; boolean any = false; for (int m = 1; m <= monthCount; m++) { Double v = lastFreightMonth(e, m); if (v != null) { sum += v; any = true; } } return any ? sum : null; } /** 模板货运量累计同比 = (今年1-N累计 - 去年1-N累计) / 去年1-N累计 */ private Double freightCumYoy(FreightTurnoverImport e, int monthCount) { Double cur = freightCum(e, monthCount); Double last = lastFreightCum(e, monthCount); if (cur == null || last == null || last == 0.0) return null; return (cur - last) / last; } // ==================== 拆分表取值辅助 ==================== private Double getMetric(ScaleSplitTransport record, String metric, String scale) { if (record == null) return null; if ("freight".equals(metric)) { if ("above".equals(scale)) return record.getAboveScaleFreight(); if ("below".equals(scale)) return record.getBelowScaleFreight(); return record.getTotalFreight(); } else { if ("above".equals(scale)) return record.getAboveScaleTurnover(); if ("below".equals(scale)) return record.getBelowScaleTurnover(); return record.getTotalTurnover(); } } private Double getYoyMetric(ScaleSplitTransport record, String metric, String scale) { if (record == null) return null; if ("above".equals(scale)) return record.getAboveScaleYoy(); if ("below".equals(scale)) return record.getBelowScaleYoy(); return record.getTotalYoy(); } private ScaleSplitTransport getRecord(Map> monthData, String city, int month) { Map map = monthData.get(city); return map == null ? null : map.get(month); } private Integer rankOfMap(Map map, String city) { Double self = map.get(city); if (self == null) return null; int rank = 1; for (Double v : map.values()) { if (v != null && v > self) rank++; } return rank; } private Integer yoyRankOfMap(Map map, String city) { Double self = map.get(city); if (self == null) return null; int rank = 1; for (Double v : map.values()) { if (v != null && v > self) rank++; } return rank; } private Integer rankOfTurnover(Map cumMap, String city, int which) { Double self = null; for (Map.Entry e : cumMap.entrySet()) { ScaleSplitTransport r = e.getValue(); Double v = which == 0 ? r.getAboveScaleTurnover() : which == 1 ? r.getBelowScaleTurnover() : r.getTotalTurnover(); if (v == null) continue; if (RegionUtil.normalizeCityName(e.getKey()).equals(RegionUtil.normalizeCityName(city))) self = v; } if (self == null) return null; int rank = 1; for (ScaleSplitTransport r : cumMap.values()) { Double v = which == 0 ? r.getAboveScaleTurnover() : which == 1 ? r.getBelowScaleTurnover() : r.getTotalTurnover(); if (v != null && v > self) rank++; } return rank; } private Integer yoyRankOfTurnover(Map cumMap, String city, int which) { Double self = null; for (Map.Entry e : cumMap.entrySet()) { ScaleSplitTransport r = e.getValue(); Double v = which == 0 ? r.getAboveScaleYoy() : which == 1 ? r.getBelowScaleYoy() : r.getTotalYoy(); if (v == null) continue; if (RegionUtil.normalizeCityName(e.getKey()).equals(RegionUtil.normalizeCityName(city))) self = v; } if (self == null) return null; int rank = 1; for (ScaleSplitTransport r : cumMap.values()) { Double v = which == 0 ? r.getAboveScaleYoy() : which == 1 ? r.getBelowScaleYoy() : r.getTotalYoy(); if (v != null && v > self) rank++; } 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> data = loadPassengerAggMap(); Map> 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> indi, int year, int month, String region, int idx) { Map 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> loadPassengerIndividualMap() { Map> 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> data, int year, int month, String region) { Map monthMap = data.get(year * 100 + month); return monthMap == null ? null : monthMap.get(region); } private Double cumOf(Map> 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> loadPassengerAggMap() { Map> data = new HashMap<>(); List 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 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> mid = loadMidClassMap(); int fillMonths = maxMonth; // 1..N 月(N>6 时模板自动向右扩列到 12 月) boolean extended = fillMonths > 6; if (extended) extendMonthlyColumns(sheet, 2, currentYear, 4, sheet.getLastRowNum()); List 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); return toBytes(wb); } } /** 中口径明细模板地区顺序(与 RegionUtil.CITY_LIST 一致:武汉市…神农架林区) */ private List midTemplateAreas() { List 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> 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> mid, int year, int month, String area, int k, boolean volume) { Map 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> loadMidClassMap() { Map> 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 (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> 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> 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 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> 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> 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 + "月全省分市州累计完成道路客运生产情况"); } } // 全省行 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))); // 市州行 r4-20:B/F 累计值、D/H 同比;C/E/G/I 排名公式保留 List 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)); } 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> 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 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> 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)); } 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 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 list = energyMapper.selectList( new LambdaQueryWrapper() .eq(EnergyVehicleQuarterly::getReportPeriod, period)); int quarter = (parseMonth(period) + 2) / 3; String[] QUARTER_CN = {"一", "二", "三", "四"}; String title = period.substring(0, 4) + "年第" + QUARTER_CN[quarter - 1] + "季度能耗汇总情况表"; Map province = new HashMap<>(); Map> 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 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); return toBytes(wb); } } /** 能耗汇总模板填充:1-based 第3~8行为 柴油/汽油/液化天然/压缩天然/纯电/燃料电池;D/J/L/M 为公式列,仅填数值列 */ private void fillEnergyTemplateSheet(Sheet sheet, Map 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) { return m < 10 ? "0" + m : String.valueOf(m); } private double round(double v, int scale) { double factor = Math.pow(10, scale); return Math.round(v * factor) / factor; } private Double ratioOf(Double part, Double total) { if (part == null || total == null || total == 0.0) return null; return part / total; } private void setNumeric(Row row, int idx, Double value) { Cell cell = row.createCell(idx); if (value != null) cell.setCellValue(value); } private void setNumeric(Row row, int idx, Integer value) { if (value != null) setNumeric(row, idx, value.doubleValue()); } private void autoWidth(Sheet sheet, int colCount) { for (int i = 0; i < colCount; i++) { sheet.setColumnWidth(i, 12 * 256); } sheet.setColumnWidth(0, 16 * 256); sheet.setColumnWidth(1, 22 * 256); } // ==================== 城市客运(公交)分市州明细 ==================== /** 生成_城市公交客运量分市州明细.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> loadCityBusByMonth(String yearPrefix, int limit) { Map> monthCity = new java.util.LinkedHashMap<>(); List rows = cityBusMapper.selectList( new LambdaQueryWrapper() .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 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> monthCity, String area, int m, int idx) { Map 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> loadCityTaxiByMonth(String yearPrefix, int limit) { Map> monthCity = new java.util.LinkedHashMap<>(); List rows = cityTaxiMapper.selectList( new LambdaQueryWrapper() .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 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> monthCity, String area, int m, int idx) { Map 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> 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); return toBytes(wb); } } /** 加载某年 1..limit 月轨道/轮渡数据:月 → 市州 → [轨道客运量,轨道周转量,轮渡客运量,轮渡周转量],含"全省" */ private Map> loadCityRailFerryByMonth(String yearPrefix, int limit) { Map> monthCity = new java.util.LinkedHashMap<>(); List rows = cityBusMapper.selectList( new LambdaQueryWrapper().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 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> 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> monthCity, String area, int m, int idx) { Map 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 cum = loadCityPassengerCumulative(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 areas = new java.util.ArrayList<>(); areas.add("全省"); areas.addAll(RegionUtil.cityList()); fillCitySumBlock(sheet, cum, areas, 4, 0); // 客运量区(单位:万人次,POI 0 基) fillCitySumBlock(sheet, cum, areas, 26, 1); // 周转量区(单位:万人次公里,POI 0 基) recalc(wb); return toBytes(wb); } } /** 填充一个累计区:startRow 起每地区 1 行;paxTurnIdx=0 用客运量、1 用周转量 */ private void fillCitySumBlock(Sheet sheet, Map cum, List 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++; } } /** 某年 1..limit 月城市客运累计:city → [公交客运量,公交周转量,出租客运量,出租周转量,轨道客运量,轨道周转量,轮渡客运量,轮渡周转量],含"全省" */ private Map loadCityPassengerCumulative(String yearPrefix, int limit) { Map cum = new HashMap<>(); for (CityBusMonthly r : cityBusMapper.selectList( new LambdaQueryWrapper().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().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; } // ==================== 城市客运 全省汇总(城市汇总_模板) ==================== /** 生成_城市客运汇总.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); } recalc(wb); 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(); } /** 生成_投资报表系列.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 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 "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); default: return null; } } private static final Map 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"); } 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> monthCity, Map> 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 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()); } return toBytes(wb); } } /** 模板文件定位:配置目录 → 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> monthCity, String area, int m, int idx) { Map 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 { 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 projectRows = new ArrayList<>(); Set matched = new HashSet<>(); } /** 在 pos 行之后插入 n 行:下方行下移(合并区先摘除再按新行号恢复),新行复制 styleRow 样式/行高/单行合并;返回首个新行索引 */ private int insertRowsDown(HSSFSheet sheet, int pos, int n, HSSFRow styleRow) { if (n <= 0) return -1; List 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) { hssfNum(row, 0, (double) seq); hssfText(row, 1, cityText); hssfText(row, 2, valueOf(p.getCounty())); hssfText(row, 3, 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 monthlies = investMonthlyMapper.selectList( new LambdaQueryWrapper() .eq(InvestmentMonthly::getReportPeriod, period)); Map pmap = new HashMap<>(); List projects = investProjectMapper.selectList(null); for (InvestmentProject p : projects) pmap.put(p.getId(), p); Map 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 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; 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); if (p == null) continue; if (sec != null) sec.matched.add(p.getId()); InvestmentMonthly m = mByProject.get(p.getId()); fillInvestPlanProjectRow(row, p, m, ++seq, b); } // 动态插行:DB 项目数 > 模板预留行数时,在市州小节末尾补齐(自底向上插行,避免行号错位) for (int i = sections.size() - 1; i >= 0; i--) { InvestSection s = sections.get(i); if (s.blockCategory == null || s.city == null) continue; // 老旧货车段无数据源 List 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()); } } 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 monthlies = investMonthlyMapper.selectList( new LambdaQueryWrapper() .eq(InvestmentMonthly::getReportPeriod, period)); Map pmap = new HashMap<>(); List 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 mByProject = new HashMap<>(); for (InvestmentMonthly m : monthlies) mByProject.put(m.getProjectId(), m); List 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 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 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 monthlies = investMonthlyMapper.selectList( new LambdaQueryWrapper() .eq(InvestmentMonthly::getReportPeriod, period)); Map pmap = new HashMap<>(); List 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 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 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 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 monthlies = investMonthlyMapper.selectList( new LambdaQueryWrapper() .eq(InvestmentMonthly::getReportPeriod, period)); Map pmap = new HashMap<>(); for (InvestmentProject p : investProjectMapper.selectList(null)) pmap.put(p.getId(), p); List 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 pool, String name, String cityFull, String category) { 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 cands = new java.util.ArrayList<>(); for (InvestmentProject p : pool) { 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 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 byte[] toBytes(XSSFWorkbook wb) throws Exception { try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { wb.write(out); wb.close(); return out.toByteArray(); } } }