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