| | |
| | | import com.trafficaudit.dataimport.entity.PassengerEnterpriseMonthly; |
| | | import com.trafficaudit.dataimport.entity.PassengerIndividualMonthly; |
| | | import com.trafficaudit.dataimport.entity.ScaleSplitTransport; |
| | | import com.trafficaudit.dataimport.entity.WycOrderMonthly; |
| | | import com.trafficaudit.auditengine.entity.AuditResult; |
| | | import com.trafficaudit.auditengine.entity.AuditRun; |
| | | import com.trafficaudit.auditengine.mapper.AuditResultMapper; |
| | |
| | | import com.trafficaudit.dataimport.mapper.PassengerEnterpriseMonthlyMapper; |
| | | import com.trafficaudit.dataimport.mapper.PassengerIndividualMonthlyMapper; |
| | | import com.trafficaudit.dataimport.mapper.ScaleSplitTransportMapper; |
| | | import com.trafficaudit.dataimport.mapper.WycOrderMonthlyMapper; |
| | | import com.trafficaudit.reportexport.calc.MidCalc; |
| | | import com.trafficaudit.reportexport.calc.PaxCalc; |
| | | import com.trafficaudit.reportexport.calc.SummaryWorkbookFiller; |
| | | import com.trafficaudit.reportexport.calc.WycSplitCalc; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.poi.ss.usermodel.Cell; |
| | | import org.apache.poi.ss.usermodel.CellStyle; |
| | |
| | | import org.apache.poi.hssf.usermodel.HSSFSheet; |
| | | import org.apache.poi.hssf.usermodel.HSSFWorkbook; |
| | | import org.apache.poi.ss.util.CellRangeAddress; |
| | | import org.apache.poi.openxml4j.util.ZipSecureFile; |
| | | import org.openxmlformats.schemas.spreadsheetml.x2006.main.STCellType; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.io.ByteArrayInputStream; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | |
| | | @Resource |
| | | private CityTaxiMonthlyMapper cityTaxiMapper; |
| | | @Resource |
| | | private SummaryWorkbookFiller summaryWorkbookFiller; |
| | | @Resource |
| | | private WycSplitCalc wycSplitCalc; |
| | | @Resource |
| | | private WycOrderMonthlyMapper wycOrderMapper; |
| | | @Resource |
| | | private AuditResultMapper auditResultMapper; |
| | | @Resource |
| | | private AuditRunMapper auditRunMapper; |
| | |
| | | /** 公路旅客输出模板目录(application.yml passenger.template-dir) */ |
| | | @Value("${passenger.template-dir:docs/公路旅客+能耗/输出}") |
| | | private String passengerTemplateDir; |
| | | |
| | | /** 汇总工作簿模板目录(application.yml summary.template-dir) */ |
| | | @Value("${summary.template-dir:docs/生成汇总大表}") |
| | | private String summaryTemplateDir; |
| | | |
| | | /** 货运报表模板目录(application.yml freight.template-dir) */ |
| | | @Value("${freight.template-dir:docs/货运}") |
| | |
| | | rows = addReadyCount(() -> energyMapper.selectCount(periodQw(period, mode, EnergyVehicleQuarterly::getReportPeriod)), "能耗车辆季度数据", notes); |
| | | auditTypes.add("H204"); |
| | | break; |
| | | case "wycSplit": { |
| | | rows = addReadyCount(() -> cityTaxiMapper.selectCount(periodQw(period, mode, CityTaxiMonthly::getReportPeriod)), "巡游出租月报", notes); |
| | | try { |
| | | Map<String, Object> inp = loadWycMonthlyInput(period, mode); |
| | | if (inp == null) { |
| | | rows = 0; |
| | | notes.add("《网约车订单及全省总量.xlsx》缺 " + period + " 行(需先维护当月订单与全省 pin)"); |
| | | } else { |
| | | double pinK = (Double) inp.get("pinK"), pinCK = (Double) inp.get("pinCK"); |
| | | double pinZ = (Double) inp.get("pinZ"), pinCZ = (Double) inp.get("pinCZ"); |
| | | double orderSum = (Double) inp.get("orderSum"); |
| | | if (pinK <= 0 || pinCK <= 0 || pinZ <= 0 || pinCZ <= 0 || orderSum <= 0) { |
| | | rows = 0; |
| | | notes.add("网约车输入 " + period + " 行不完整(订单/全省 pin 缺失)"); |
| | | } |
| | | } |
| | | } catch (Exception ex) { |
| | | rows = 0; |
| | | notes.add("网约车输入读取失败:" + ex.getMessage()); |
| | | } |
| | | break; |
| | | } |
| | | case "summaryWorkbook": { |
| | | rows = addReadyCount(() -> cityTaxiMapper.selectCount(periodQw(period, mode, CityTaxiMonthly::getReportPeriod)), "巡游出租月报", notes) |
| | | + addReadyCount(() -> cityBusMapper.selectCount(periodQw(period, mode, CityBusMonthly::getReportPeriod)), "城市公交月报", notes); |
| | | if (rows <= 0) notes.add("汇总表依赖的公交/出租月报数据缺失"); |
| | | try { |
| | | if (resolveSummaryMother(period, mode) == null) { |
| | | rows = 0; |
| | | notes.add("docs/生成汇总大表 下缺少可复制的母版汇总表"); |
| | | } |
| | | } catch (Exception ex) { |
| | | rows = 0; |
| | | notes.add("母版定位失败:" + ex.getMessage()); |
| | | } |
| | | break; |
| | | } |
| | | case "investPassengerDetail": |
| | | case "investLogisticsDetail": { |
| | | boolean paxDetail = "investPassengerDetail".equals(t); |
| | | String catName = paxDetail ? "客运站场" : "物流园区"; |
| | | String catLabel = paxDetail ? "客运站场投资月报" : "物流园区投资月报"; |
| | | List<InvestmentProject> catProjects = investProjectMapper.selectList( |
| | | new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, catName)); |
| | | final List<Long> catIds = new ArrayList<>(); |
| | | for (InvestmentProject cp : catProjects) catIds.add(cp.getId()); |
| | | if (catIds.isEmpty()) { |
| | | rows = 0; |
| | | notes.add("缺数据:" + catLabel); |
| | | } else { |
| | | rows = addReadyCount(() -> investMonthlyMapper.selectCount( |
| | | periodQw(period, mode, InvestmentMonthly::getReportPeriod) |
| | | .in(InvestmentMonthly::getProjectId, catIds)), catLabel, notes); |
| | | } |
| | | String detTpl = paxDetail ? "模板_X月客运站投资明细表.xlsx" : "模板_X月物流站场投资明细.xls"; |
| | | try { |
| | | if (resolveAnyTemplate(investTemplateDir, detTpl) == null) { |
| | | rows = 0; |
| | | notes.add("docs/投资/模板 下缺少《" + detTpl + "》"); |
| | | } |
| | | } catch (Exception ex) { |
| | | rows = 0; |
| | | notes.add("母版定位失败:" + ex.getMessage()); |
| | | } |
| | | auditTypes.add("INVEST"); |
| | | break; |
| | | } |
| | | default: |
| | | rows = 0; |
| | | notes.add("未知报表类型:" + t); |
| | |
| | | auditRows += auditRowsOf(period, mode, at); |
| | | audited = audited || auditedMarked(period, mode, at); |
| | | } |
| | | if (auditTypes.isEmpty()) audited = true; |
| | | it.put("rows", rows); |
| | | it.put("ready", rows > 0); |
| | | it.put("auditRows", auditRows); |
| | |
| | | for (String t : types) { |
| | | if (isFreightType(t)) freight = true; |
| | | else if (isPassengerType(t)) passenger = true; |
| | | else if ("cityTaxiDetail".equals(t) || isCityPassengerType(t)) { cityTaxi = true; cityBus = true; } |
| | | else if ("cityTaxiDetail".equals(t) || "wycSplit".equals(t)) { cityTaxi = true; } |
| | | else if (isCityPassengerType(t) || "summaryWorkbook".equals(t)) { cityTaxi = true; cityBus = true; } |
| | | else if (isInvestType(t)) invest = true; |
| | | else if ("energySummary".equals(t)) energy = true; |
| | | } |
| | |
| | | List<Integer> missing = new ArrayList<>(); |
| | | List<Integer> coverage = new ArrayList<>(); |
| | | if (Boolean.FALSE.equals(it.get("audited"))) problems.add("该报表期尚未审核通过"); |
| | | if (isFreightType(t)) { |
| | | if ("wycSplit".equals(t)) { |
| | | coverage.addAll(taxiCover); |
| | | missing = missingMonths(taxiCover, month); |
| | | try { |
| | | Map<String, Object> inp = loadWycMonthlyInput(period, mode); |
| | | if (inp == null) { |
| | | problems.add("缺网约车输入:请在《网约车订单及全省总量.xlsx》补 " + period + " 行"); |
| | | } else { |
| | | double pinK = (Double) inp.get("pinK"), pinCK = (Double) inp.get("pinCK"); |
| | | double pinZ = (Double) inp.get("pinZ"), pinCZ = (Double) inp.get("pinCZ"); |
| | | double orderSum = (Double) inp.get("orderSum"); |
| | | if (pinK <= 0 || pinCK <= 0 || pinZ <= 0 || pinCZ <= 0 || orderSum <= 0) { |
| | | problems.add("网约车输入 " + period + " 行不完整(订单/全省 pin 缺失)"); |
| | | } else { |
| | | addSum(summary, "全省网约车客运量", pinK, "万人"); |
| | | addSum(summary, "其中城市内客运量", pinCK, "万人"); |
| | | addSum(summary, "全省网约车周转量", pinZ, "万人公里"); |
| | | addSum(summary, "其中城市内周转量", pinCZ, "万人公里"); |
| | | addSum(summary, "部级订单合计", orderSum, "单"); |
| | | } |
| | | } |
| | | } catch (Exception ex) { |
| | | problems.add("网约车输入读取失败:" + ex.getMessage()); |
| | | } |
| | | } else if ("summaryWorkbook".equals(t)) { |
| | | java.util.Set<Integer> cov = new java.util.TreeSet<>(); |
| | | cov.addAll(busCover); |
| | | cov.addAll(taxiCover); |
| | | coverage.addAll(cov); |
| | | missing = missingMonths(cov, month); |
| | | try { |
| | | if (resolveSummaryMother(period, mode) == null) { |
| | | problems.add("docs/生成汇总大表 下缺少可复制的母版汇总表"); |
| | | } else { |
| | | summaryWorkbookPreview(period, yearNum, month, summary, problems); |
| | | } |
| | | } catch (Exception ex) { |
| | | problems.add(ex.getMessage()); |
| | | } |
| | | } else if (isFreightType(t)) { |
| | | coverage.addAll(freightCover); |
| | | missing = missingMonths(freightCover, month); |
| | | freightPreview(period, month, summary, problems); |
| | |
| | | coverage.addAll(investCover); |
| | | // 投资为当月报表(无 1..N 累计口径),只提示当月缺失 |
| | | if (!investCover.contains(month)) missing.add(month); |
| | | investPreview(period, mode, summary, problems); |
| | | if ("investPassengerDetail".equals(t)) investPreview(period, mode, "客运站场", summary, problems); |
| | | else if ("investLogisticsDetail".equals(t)) investPreview(period, mode, "物流园区", summary, problems); |
| | | else 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); |
| | |
| | | } |
| | | |
| | | private boolean isInvestType(String t) { |
| | | return "investPlan".equals(t) || "investFiveYearLogistics".equals(t) || "investBillion".equals(t) || "investCounty".equals(t); |
| | | return "investPlan".equals(t) || "investFiveYearLogistics".equals(t) || "investBillion".equals(t) || "investCounty".equals(t) |
| | | || "investPassengerDetail".equals(t) || "investLogisticsDetail".equals(t); |
| | | } |
| | | |
| | | private boolean isEnergyType(String t) { |
| | |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** 汇总整本关键指标预览:货运量/周转量、城市客运、中口径的 当月、1..M 累计、当月同比、累计同比(同比取库内去年同期,缺则显示—) */ |
| | | private void summaryWorkbookPreview(String period, int year, int month, |
| | | List<Map<String, Object>> summary, List<String> problems) { |
| | | String yearPrefix = year + "-"; |
| | | // ---- 货运(量=模板_货运量周转量全省行;周转量=规上规下拆分表全省行) ---- |
| | | FreightTurnoverImport ft = loadFreightTurnover(period).get("湖北省"); |
| | | Double fm = ft == null ? null : freightMonth(ft, month); |
| | | Double fcum = ft == null ? null : freightCum(ft, month); |
| | | Double fy = ft == null ? null : freightYoy(ft, month); |
| | | Double fcy = ft == null ? null : freightCumYoy(ft, month); |
| | | addKpi(summary, "全省货运量", fm, fcum, fy, fcy, "万吨"); |
| | | if (fm != null && fy == null) { |
| | | problems.add((year - 1) + "-" + String.format("%02d", month) + " 同期货运量数据未导入库,货运量当月/累计同比暂不显示,待 2025 年定稿数据导入后自动出现"); |
| | | } |
| | | ScaleSplitTransport tm = loadProvinceMonthMap(period).get(month); |
| | | ScaleSplitTransport tc = getProvinceCumulative(period); |
| | | Double tvM = tm == null ? null : tm.getTotalTurnover(); |
| | | Double tvC = tc == null ? null : tc.getTotalTurnover(); |
| | | Double tvMY = tm == null ? null : getYoyMetric(tm, "turnover", "total"); |
| | | Double tvCY = tc == null ? null : getYoyMetric(tc, "turnover", "total"); |
| | | addKpi(summary, "全省货物周转量", tvM, tvC, tvMY, tvCY, "万吨公里"); |
| | | if (fm == null) problems.add("模板_货运量周转量缺 " + period + " 当月数据,货运量仅能提供截至最近导入月的累计"); |
| | | if (tvM == null) problems.add("规上规下拆分缺 " + period + " 当月数据,周转量/规上规下当月缺,累计为截至最近导入月"); |
| | | |
| | | // ---- 城市客运(公交+出租+轨道+轮渡 全省小计;指标位 0/2/4/6=客运,1/3/5/7=周转) ---- |
| | | double[] cumM = cityCumProv(yearPrefix, month); |
| | | double[] cumPrev = cityCumProv(yearPrefix, month - 1); |
| | | double[] monthVec = diff8(cumM, cumPrev); |
| | | double cityPaxM = sumIdx(monthVec, new int[]{0, 2, 4, 6}); |
| | | double cityTurnM = sumIdx(monthVec, new int[]{1, 3, 5, 7}); |
| | | double cityPaxC = sumIdx(cumM, new int[]{0, 2, 4, 6}); |
| | | double cityTurnC = sumIdx(cumM, new int[]{1, 3, 5, 7}); |
| | | double[] lastCumM = cityCumProv((year - 1) + "-", month); |
| | | double[] lastCumPrev = cityCumProv((year - 1) + "-", month - 1); |
| | | double[] lastVec = diff8(lastCumM, lastCumPrev); |
| | | double lastPaxM = sumIdx(lastVec, new int[]{0, 2, 4, 6}); |
| | | double lastPaxC = sumIdx(lastCumM, new int[]{0, 2, 4, 6}); |
| | | double lastTurnM = sumIdx(lastVec, new int[]{1, 3, 5, 7}); |
| | | double lastTurnC = sumIdx(lastCumM, new int[]{1, 3, 5, 7}); |
| | | addKpiRaw(summary, "城市客运量", cityPaxM, cityPaxC, lastPaxM, lastPaxC, "万人次"); |
| | | addKpiRaw(summary, "城市客运周转量", cityTurnM, cityTurnC, lastTurnM, lastTurnC, "万人公里"); |
| | | |
| | | // ---- 中口径(公路旅客企业 H203-1 全省,单位换算 /10000) ---- |
| | | Map<Integer, Map<String, PassengerAgg>> data = loadPassengerAggMap(); |
| | | double paxM = 0.0, paxC = 0.0, turnM = 0.0, turnC = 0.0; |
| | | double lastPaxM2 = 0.0, lastPaxC2 = 0.0, lastTurnM2 = 0.0, lastTurnC2 = 0.0; |
| | | for (int m = 1; m <= month; m++) { |
| | | PassengerAgg agg = aggOf(data, year, m, "湖北省"); |
| | | if (agg != null) { |
| | | double pax = agg.passengerTotal / 10000.0; |
| | | double turn = agg.turnoverTotal / 10000.0; |
| | | if (m == month) { paxM = pax; turnM = turn; } |
| | | paxC += pax; |
| | | turnC += turn; |
| | | } |
| | | PassengerAgg last = aggOf(data, year - 1, m, "湖北省"); |
| | | if (last != null) { |
| | | double lp = last.passengerTotal / 10000.0; |
| | | double lt = last.turnoverTotal / 10000.0; |
| | | if (m == month) { lastPaxM2 = lp; lastTurnM2 = lt; } |
| | | lastPaxC2 += lp; |
| | | lastTurnC2 += lt; |
| | | } |
| | | } |
| | | addKpiRaw(summary, "中口径客运量", paxM, paxC, lastPaxM2, lastPaxC2, "万人次"); |
| | | addKpiRaw(summary, "中口径周转量", turnM, turnC, lastTurnM2, lastTurnC2, "万人公里"); |
| | | |
| | | if (paxM <= 0 && paxC <= 0 && cityPaxC <= 0 && fm == null) { |
| | | problems.add("货运量周转量/规上规下/公路旅客/城市客运当月数据均缺失"); |
| | | } |
| | | if (cityPaxC > 0 && lastPaxC <= 0) { |
| | | problems.add((year - 1) + "-" + String.format("%02d", month) + " 同期城市客运数据未导入库,城市客运同比暂不显示,待 2025 年定稿数据导入后自动出现"); |
| | | } |
| | | if (paxC > 0 && lastPaxC2 <= 0) { |
| | | problems.add((year - 1) + "-" + String.format("%02d", month) + " 同期公路旅客数据未导入库,中口径同比暂不显示,待 2025 年定稿数据导入后自动出现"); |
| | | } |
| | | } |
| | | |
| | | /** 城市客运全省累计向量(8 指标:公交客运/周转、出租客运/周转、轨道客运/周转、轮渡客运/周转) */ |
| | | private double[] cityCumProv(String yearPrefix, int month) { |
| | | if (month <= 0) return new double[8]; |
| | | return loadCityPassengerCumulative(yearPrefix, month).getOrDefault("全省", new double[8]); |
| | | } |
| | | |
| | | private double[] diff8(double[] a, double[] b) { |
| | | double[] out = new double[8]; |
| | | for (int i = 0; i < 8; i++) out[i] = a[i] - b[i]; |
| | | return out; |
| | | } |
| | | |
| | | private double sumIdx(double[] arr, int[] idx) { |
| | | double sum = 0.0; |
| | | for (int i : idx) sum += (i < arr.length ? arr[i] : 0.0); |
| | | return sum; |
| | | } |
| | | |
| | | /** 百分比同比(值已为比率 0.xx):输出四行 当月/累计/当月同比/累计同比 */ |
| | | private void addKpi(List<Map<String, Object>> summary, String name, Double m, Double cum, Double yoyM, Double yoyC, String unit) { |
| | | if (m != null) addSum(summary, name + "·当月", round(m, 2), unit); |
| | | if (cum != null) addSum(summary, name + "·累计", round(cum, 2), unit); |
| | | if (yoyM != null) addSum(summary, name + "·当月同比", round(yoyM * 100.0, 1), "%"); |
| | | if (yoyC != null) addSum(summary, name + "·累计同比", round(yoyC * 100.0, 1), "%"); |
| | | } |
| | | |
| | | /** 原始数值版(月/累计为 0 视为无数据不出行) */ |
| | | private void addKpiRaw(List<Map<String, Object>> summary, String name, double m, double cum, double lastM, double lastC, String unit) { |
| | | if (m > 0) addSum(summary, name + "·当月", round(m, 2), unit); |
| | | if (cum > 0) addSum(summary, name + "·累计", round(cum, 2), unit); |
| | | if (m > 0 && lastM > 0) addSum(summary, name + "·当月同比", round((m - lastM) / lastM * 100.0, 1), "%"); |
| | | if (cum > 0 && lastC > 0) addSum(summary, name + "·累计同比", round((cum - lastC) / lastC * 100.0, 1), "%"); |
| | | } |
| | | |
| | | private void addSum(List<Map<String, Object>> summary, String label, Double value, String unit) { |
| | | Map<String, Object> m = new LinkedHashMap<>(); |
| | | m.put("label", label); |
| | |
| | | Map<String, Double> h2032Cum = loadH2032FreightCumulative(period); |
| | | ScaleSplitTransport provCum = getProvinceCumulative(period); |
| | | Double total = freightCum(ft.get("湖北省"), month); |
| | | Double totalWan = total == null ? null : round(total / 10000.0, 4); |
| | | Double totalWan = total == null ? null : round(total, 4); // 模板列已是万吨,不再 ÷10000 |
| | | double above = 0.0; |
| | | for (Double v : h2032Cum.values()) if (v != null) above += v; |
| | | double aboveWan = round(above / 10000.0, 4); |
| | |
| | | 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)); |
| | | investPreview(period, mode, null, summary, problems); |
| | | } |
| | | |
| | | /** 投资摘要(category 非空时只统计该类别,如客运站场/物流园区投资明细表) */ |
| | | private void investPreview(String period, String mode, String category, List<Map<String, Object>> summary, List<String> problems) { |
| | | LambdaQueryWrapper<InvestmentMonthly> qw = periodQw(period, mode, InvestmentMonthly::getReportPeriod); |
| | | if (category != null) { |
| | | List<InvestmentProject> ps = investProjectMapper.selectList( |
| | | new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, category)); |
| | | List<Long> ids = new ArrayList<>(); |
| | | for (InvestmentProject p : ps) ids.add(p.getId()); |
| | | if (ids.isEmpty()) { problems.add("缺数据:库内没有「" + category + "」项目主档"); return; } |
| | | qw.in(InvestmentMonthly::getProjectId, ids); |
| | | } |
| | | List<InvestmentMonthly> list = investMonthlyMapper.selectList(qw); |
| | | Set<Long> proj = new HashSet<>(); |
| | | double monthDone = 0.0, yearCum = 0.0, startCum = 0.0; |
| | | for (InvestmentMonthly m : list) { |
| | |
| | | for (Double v : h2032FreightCum.values()) { |
| | | if (v != null) provAbove += v; |
| | | } |
| | | Double provAboveWan = round(provAbove / 10000.0, 4); |
| | | Double provAboveWan = provAbove / 10000.0; |
| | | Double provTotal = freightCum(ftMap.get("湖北省"), month); |
| | | Double provBelow = provTotal == null ? null : round(provTotal - provAboveWan, 4); |
| | | Double provBelow = provTotal == null ? null : provTotal - provAboveWan; |
| | | 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 provAboveYoy = provAboveLast == 0.0 ? null : (provAbove - provAboveLast) / provAboveLast; |
| | | Double provTotalLast = lastFreightCum(ftMap.get("湖北省"), month); |
| | | Double provBelowLast = provTotalLast == null ? null : round(provTotalLast - provAboveLast / 10000.0, 4); |
| | | Double provBelowLast = provTotalLast == null ? null : provTotalLast - provAboveLast / 10000.0; |
| | | Double provBelowYoy = provBelowLast == null || provBelowLast == 0.0 ? null |
| | | : round((provBelow - provBelowLast) / provBelowLast, 4); |
| | | : (provBelow - provBelowLast) / provBelowLast; |
| | | |
| | | for (String city : RegionUtil.cityList()) { |
| | | Double a = h2032FreightCum.get(city); |
| | | double aWan = a == null ? 0.0 : round(a / 10000.0, 4); |
| | | double aWan = a == null ? 0.0 : a / 10000.0; |
| | | 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)); |
| | | below.put(city, t == null ? null : t - aWan); |
| | | 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 aLastWan = aLast == null ? 0.0 : aLast / 10000.0; |
| | | Double belowLast = tLast == null ? null : tLast - aLastWan; |
| | | Double b = below.get(city); |
| | | belowYoy.put(city, belowLast == null || belowLast == 0.0 || b == null |
| | | ? null : round((b - belowLast) / belowLast, 4)); |
| | | ? null : (b - belowLast) / belowLast); |
| | | totalYoy.put(city, freightCumYoy(ftMap.get(city), month)); |
| | | } |
| | | |
| | | |
| | | // ===== 第二页(当月)数据:规上=H2032 当月、合计=模板_货运量周转量当月列、规下=差值;同比=去年同月 ===== |
| | | Map<String, Map<Integer, Double>> h2032MonthCur = loadH2032FreightByMonth(period); |
| | | String lastYearPeriod = (Integer.parseInt(year) - 1) + period.substring(4); |
| | | Map<String, Map<Integer, Double>> h2032MonthLast = loadH2032FreightByMonth(lastYearPeriod); |
| | | Map<String, Double> monthAbove = new HashMap<>(); |
| | | Map<String, Double> monthBelow = new HashMap<>(); |
| | | Map<String, Double> monthTotal = new HashMap<>(); |
| | | Map<String, Double> monthAboveYoy = new HashMap<>(); |
| | | Map<String, Double> monthBelowYoy = new HashMap<>(); |
| | | Map<String, Double> monthTotalYoy = new HashMap<>(); |
| | | double provMonthAbove = 0.0, provMonthAboveLast = 0.0; |
| | | for (String city : RegionUtil.cityList()) { |
| | | Double ma = monthFreightWan(h2032MonthCur, city, month); |
| | | Double maLast = monthFreightWan(h2032MonthLast, city, month); |
| | | Double mt = freightMonth(ftMap.get(city), month); |
| | | Double mtLast = lastFreightMonth(ftMap.get(city), month); |
| | | if (ma != null) provMonthAbove += ma; |
| | | if (maLast != null) provMonthAboveLast += maLast; |
| | | monthAbove.put(city, ma); |
| | | monthTotal.put(city, mt); |
| | | monthBelow.put(city, mt == null ? null : mt - (ma == null ? 0.0 : ma)); |
| | | monthAboveYoy.put(city, growthYoy(ma, maLast)); |
| | | Double mb = monthBelow.get(city); |
| | | Double mbLast = mtLast == null ? null : mtLast - (maLast == null ? 0.0 : maLast); |
| | | monthBelowYoy.put(city, growthYoy(mb, mbLast)); |
| | | monthTotalYoy.put(city, freightYoy(ftMap.get(city), month)); |
| | | } |
| | | Double provMonthAboveWan = provMonthAbove == 0.0 ? null : provMonthAbove; |
| | | Double provMonthTotal = freightMonth(ftMap.get("湖北省"), month); |
| | | Double provMonthBelow = provMonthTotal == null ? null : provMonthTotal - provMonthAbove; |
| | | Double provMonthAboveYoy = growthYoy(provMonthAbove == 0.0 ? null : provMonthAbove, provMonthAboveLast); |
| | | Double provMonthTotalLast = lastFreightMonth(ftMap.get("湖北省"), month); |
| | | Double provMonthBelowLast = provMonthTotalLast == null ? null |
| | | : provMonthTotalLast - provMonthAboveLast; |
| | | Double provMonthBelowYoy = growthYoy(provMonthBelow, provMonthBelowLast); |
| | | Double provMonthTotalYoy = freightYoy(ftMap.get("湖北省"), month); |
| | | |
| | | // 以 docs/货运/生成_货运量排名.xlsx 为底稿:保留表头/合并/列宽/样式与 Q/R 占比公式,仅替换数据 |
| | | File template = resolveFreightTemplate("生成_货运量排名.xlsx"); |
| | |
| | | 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 + "月全省分市州累计完成公路货运量情况"); |
| | | 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); |
| | | renameRankMonthHeader(sheet, 24); |
| | | fillFreightRankBlock(sheet, 25, monthAbove, monthBelow, monthTotal, |
| | | monthAboveYoy, monthBelowYoy, monthTotalYoy, |
| | | provMonthAboveWan, provMonthBelow, provMonthTotal, |
| | | provMonthAboveYoy, provMonthBelowYoy, provMonthTotalYoy); |
| | | try { |
| | | wb.getCreationHelper().createFormulaEvaluator().evaluateAll(); |
| | | } catch (Exception e) { |
| | |
| | | Map<String, ScaleSplitTransport> cumMap = loadCumulativeMap(period); |
| | | ScaleSplitTransport province = getProvinceCumulative(period); |
| | | |
| | | // 当月(第二页):周转量拆分表 MONTH 行(含当月同比) |
| | | Map<String, ScaleSplitTransport> monthTurnover = new HashMap<>(); |
| | | for (ScaleSplitTransport rec : scaleSplitMapper.selectList( |
| | | new LambdaQueryWrapper<ScaleSplitTransport>() |
| | | .eq(ScaleSplitTransport::getPeriodType, "MONTH") |
| | | .eq(ScaleSplitTransport::getReportPeriod, period))) { |
| | | String name = RegionUtil.normalizeCityName(rec.getRegionName()); |
| | | if (name != null) monthTurnover.put(name, rec); |
| | | } |
| | | ScaleSplitTransport provinceMonth = monthTurnover.remove("湖北省"); |
| | | |
| | | // 以 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 + "月全省分市州累计完成公路货物运输周转量情况"); |
| | | sheet.getRow(22).getCell(0).setCellValue(year + "年" + month + "月全省分市州完成公路货物运输周转量情况"); |
| | | fillTurnoverRankBlock(sheet, 3, cumMap, province); |
| | | fillTurnoverRankBlock(sheet, 25, cumMap, province); |
| | | renameRankMonthHeader(sheet, 24); |
| | | fillTurnoverRankBlock(sheet, 25, monthTurnover, provinceMonth); |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | |
| | | return org.apache.poi.ss.util.CellReference.convertNumToColString(colIdx); |
| | | } |
| | | |
| | | /** 旅客分市州月值(0=客运量/1=周转量/2=个体客运量/3=个体周转量) */ |
| | | /** 旅客分市州月值(0=客运量/1=周转量/2=个体客运量/3=个体周转量;0/1=企业H2031+个体,2026-09-03 口径确认) */ |
| | | private double passengerMonthVal(Map<Integer, Map<String, PassengerAgg>> data, Map<Integer, Map<String, double[]>> indi, |
| | | int year, int month, String region, int metricIdx) { |
| | | if (metricIdx <= 1) { |
| | | if (metricIdx == 0 || metricIdx == 1) { // 客运量/周转量总行:企业+个体 |
| | | PassengerAgg agg = aggOf(data, year, month, region); |
| | | Double val = agg == null ? null |
| | | double ent = agg == null ? 0.0 |
| | | : (metricIdx == 0 ? agg.passengerTotal / 10000.0 : agg.turnoverTotal / 10000.0); |
| | | return val == null ? 0.0 : val; |
| | | double ind = individualVal(indi, year, month, region, metricIdx == 0 ? 0 : 1); |
| | | return ent + ind; |
| | | } |
| | | return individualVal(indi, year, month, region, metricIdx - 2); |
| | | } |
| | |
| | | return lv == 0.0 ? null : (v - lv) / lv; |
| | | } |
| | | |
| | | /** 中口径月同比:k=-1 表示总行(班线+公交之和,出租/网约车无源数据按 0);无去年数据留空 */ |
| | | /** 中口径月同比:k=-1 表示总行(班线+公交+出租城乡+网约车城乡 四项之和);无去年数据留空 */ |
| | | 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); |
| | | } |
| | | Double y = midMonthYoy(mid, year, month, area, k, volume); |
| | | c.setBlank(); |
| | | if (v != 0.0 && lv != 0.0) c.setCellValue(round((v - lv) / lv, 4)); |
| | | if (y != null) c.setCellValue(y); |
| | | } |
| | | |
| | | /** 中口径累计同比列(1..months 月累计;k=-1 总行 = 班线+公交) */ |
| | | /** 中口径累计同比列(1..months 月累计;四维齐全) */ |
| | | private void fillMidCumYoyColumn(Sheet sheet, Map<Integer, Map<String, double[][]>> mid, int year, |
| | | int months, int colIdx) { |
| | | List<String> areas = midTemplateAreas(); |
| | |
| | | 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); |
| | | } |
| | | } |
| | | Double y = midCumYoy(mid, year, area, months, 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)); |
| | | if (y != null) c.setCellValue(y); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | setFreightCell(row, cumCol, cumValue); |
| | | setFreightCell(row, cumCol + 1, cumYoy); |
| | | } |
| | | |
| | | /** H2032 规上某月合计(吨→万吨);无该月数据返回 null */ |
| | | private Double monthFreightWan(Map<String, Map<Integer, Double>> byCityMonth, String city, int month) { |
| | | Map<Integer, Double> byMonth = byCityMonth == null ? null : byCityMonth.get(city); |
| | | Double v = byMonth == null ? null : byMonth.get(month); |
| | | return v == null ? null : v / 10000.0; |
| | | } |
| | | |
| | | /** 同比 = (本期-基期)/基期;任一期缺失或基期为 0 返回 null */ |
| | | private Double growthYoy(Double cur, Double last) { |
| | | if (cur == null || last == null || last == 0.0) return null; |
| | | return (cur - last) / last; |
| | | } |
| | | |
| | | /** 排名表第二页列头「累计完成」→「当月完成」(仅导出内存替换,模板文件不改) */ |
| | | private void renameRankMonthHeader(Sheet sheet, int headerRowIdx) { |
| | | Row row = sheet.getRow(headerRowIdx); |
| | | if (row == null) return; |
| | | for (int col : new int[]{1, 6, 11}) { |
| | | Cell c = row.getCell(col); |
| | | if (c == null || c.getCellType() != CellType.STRING) continue; |
| | | String s = c.getStringCellValue(); |
| | | if (s != null && s.contains("累计完成")) c.setCellValue(s.replace("累计完成", "当月完成")); |
| | | } |
| | | } |
| | | |
| | | /** 货运量排名块填充:模板 1-based r4 起(全省+17市州),Q/R 占比公式保留不动 */ |
| | |
| | | } |
| | | } |
| | | |
| | | /** 周转量排名 Q/R 占比:模板为缓存值,按新数据重算(规上占比 1 位小数) */ |
| | | /** 周转量排名 Q/R 占比:模板为缓存值,按新数据重算(全精度写入,两位小数展示交给单元格格式) */ |
| | | private void setFreightShare(Row row, Double above, Double total) { |
| | | if (total != null && total > 0 && above != null) { |
| | | double share = round(above * 10.0 / total, 1); |
| | | double share = above * 10.0 / total; |
| | | setFreightCell(row, 16, share); |
| | | setFreightCell(row, 17, round(10.0 - share, 1)); |
| | | setFreightCell(row, 17, 10.0 - share); |
| | | } else { |
| | | setFreightCell(row, 16, (Double) null); |
| | | setFreightCell(row, 17, (Double) null); |
| | |
| | | /** H2032规上: city -> month -> 货运量(吨) */ |
| | | private Map<String, Map<Integer, Double>> loadH2032FreightByMonth(String period) { |
| | | Map<String, Map<Integer, Double>> result = new HashMap<>(); |
| | | String yearPrefix = period.length() >= 4 ? period.substring(0, 4) : null; |
| | | List<H2032EnterpriseMonthly> list = h2032Mapper.selectList(null); |
| | | for (H2032EnterpriseMonthly record : list) { |
| | | if (record.getReportPeriod() == null || record.getReportPeriod().compareTo(period) > 0) continue; |
| | | // H2032 表同时存有往年月报,统计当年规上值时只取与报表期同年的记录,避免去年数据叠加 |
| | | if (yearPrefix != null && !record.getReportPeriod().startsWith(yearPrefix)) continue; |
| | | String city = RegionUtil.cityByCode(record.getRegionCode()); |
| | | if (city == null) continue; |
| | | int month = parseMonth(record.getReportPeriod()); |
| | |
| | | /** H2032规上累计: city -> 货运量(吨) */ |
| | | private Map<String, Double> loadH2032FreightCumulative(String period) { |
| | | Map<String, Double> result = new HashMap<>(); |
| | | String yearPrefix = period.length() >= 4 ? period.substring(0, 4) : null; |
| | | List<H2032EnterpriseMonthly> list = h2032Mapper.selectList(null); |
| | | for (H2032EnterpriseMonthly record : list) { |
| | | if (record.getReportPeriod() == null || record.getReportPeriod().compareTo(period) > 0) continue; |
| | | if (yearPrefix != null && !record.getReportPeriod().startsWith(yearPrefix)) continue; |
| | | String city = RegionUtil.cityByCode(record.getRegionCode()); |
| | | if (city == null) continue; |
| | | double freight = record.getFreightTotal() == null ? 0.0 : record.getFreightTotal(); |
| | |
| | | setNumeric(row, 15, isProvince ? null : yoyRankOfMap(totalYoy, city)); |
| | | |
| | | if (t != null && t > 0 && a != null) { |
| | | double share = round(a * 10.0 / t, 1); |
| | | double share = a * 10.0 / t; |
| | | setNumeric(row, 16, share); |
| | | setNumeric(row, 17, round(10.0 - share, 1)); |
| | | setNumeric(row, 17, 10.0 - share); |
| | | } |
| | | return rowIdx + 1; |
| | | } |
| | |
| | | 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); |
| | | double share = above * 10.0 / total; |
| | | setNumeric(row, 16, share); |
| | | setNumeric(row, 17, round(10.0 - share, 1)); |
| | | setNumeric(row, 17, 10.0 - share); |
| | | } |
| | | return rowIdx + 1; |
| | | } |
| | |
| | | Double v = byMonth == null ? null : byMonth.get(month); |
| | | if (v != null) sum += v; |
| | | } |
| | | return round(sum / 10000.0, 4); |
| | | return sum / 10000.0; |
| | | } |
| | | Double total = freightMonth(ft, month); |
| | | Double above = getProvinceMonthValue(provinceMonthMap, ft, month, "freight", "above", h2032Freight); |
| | |
| | | for (Double v : h2032FreightCum.values()) { |
| | | if (v != null) sum += v; |
| | | } |
| | | return round(sum / 10000.0, 4); |
| | | return sum / 10000.0; |
| | | } |
| | | Double total = freightCum(ft, monthCount); |
| | | Double above = getProvinceCumValue(provinceCum, ft, "freight", "above", h2032FreightCum, monthCount); |
| | |
| | | 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); |
| | | return v == null ? 0.0 : v / 10000.0; |
| | | } |
| | | Double total = freightMonth(ft, month); |
| | | Double above = getCityMonthValue(monthData, ft, city, month, "freight", "above", h2032Freight); |
| | |
| | | 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); |
| | | return v == null ? 0.0 : v / 10000.0; |
| | | } |
| | | Double total = freightCum(ft, monthCount); |
| | | Double above = getCityCumValue(cum, ft, "freight", "above", h2032FreightCum, city, monthCount); |
| | |
| | | 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)); |
| | | if (v == 0.0) c.setBlank(); else c.setCellValue(v); |
| | | } |
| | | // 同比列:库内去年同月同比(模板 #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)); |
| | | if (v == 0.0 || lv == 0.0) yc.setBlank(); else yc.setCellValue((v - lv) / lv); |
| | | } |
| | | // 累计同比列(模板 #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)); |
| | | if (cumYoy == null) yc2.setBlank(); else yc2.setCellValue(cumYoy); |
| | | } |
| | | recalc(wb); |
| | | return toBytes(wb); |
| | |
| | | |
| | | private Double yoy(Double cur, Double base) { |
| | | if (cur == null || base == null || base == 0.0) return null; |
| | | return round((cur - base) / base, 4); |
| | | return (cur - base) / base; |
| | | } |
| | | |
| | | /** 指标取值(已换算为万人/万人公里/公里;无数据返回 null) */ |
| | |
| | | boolean extended = fillMonths > 6; |
| | | if (extended) extendMonthlyColumns(sheet, 2, currentYear, 4, sheet.getLastRowNum()); |
| | | List<String> areas = midTemplateAreas(); |
| | | // 2026-09-18:改为「按单元格类型无关」遍历——扩列(7~12 月)出来的值列在模板里是空格, |
| | | // 旧实现只处理 CellType.FORMULA 的格,导致 7 月及以后月份永远填不上(8 月整列 0、累计只到 6 月)。 |
| | | 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; |
| | | } |
| | | for (int c0 = 0; c0 < Math.max(row.getLastCellNum(), 0); c0++) { |
| | | Cell c = row.getCell(c0); |
| | | if (c == null) continue; |
| | | int r = c.getRowIndex() + 1; |
| | | int col = c.getColumnIndex() + 1; |
| | | int col = c0 + 1; |
| | | int blockOffset = -1; |
| | | boolean volume = false; |
| | | if (r >= 5 && r <= 94) { |
| | |
| | | volume = false; |
| | | } |
| | | if (blockOffset < 0) { |
| | | keepCached(c); // r1/r2 备注等 |
| | | if (c.getCellType() == CellType.FORMULA) 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); // 总行内部公式(防御) |
| | | boolean isVal = is2026MonthCol(col); |
| | | boolean isYoy = isYoyMonthCol(col); |
| | | if (!isVal && !isYoy) { |
| | | // 2025 年列等:跨簿公式取缓存值断链;内部公式(合计/累计)保留,稍后 recalc 重算 |
| | | if (c.getCellType() == CellType.FORMULA) { |
| | | String f = c.getCellFormula(); |
| | | if (f != null && f.contains("[")) keepCached(c); |
| | | } |
| | | continue; |
| | | } |
| | | int k = blockOffset % 5; // 1=班线 2=公交 3=出租 4=网约车 |
| | | String area = areas.get(blockOffset / 5); |
| | | if (is2026MonthCol(col)) { |
| | | if (blockOffset % 5 == 0) { |
| | | if (isYoy) { |
| | | fillMidYoyCell(c, mid, currentYear, monthOfCol(col), area, -1, volume); |
| | | } |
| | | // 总行值列:一律保留模板公式(1~6 月原公式 + 扩列克隆出来的公式), |
| | | // 由生成收尾的 recalcWorkbookFormulas 求值写回缓存。早期版本这里 keepCached |
| | | // 会把刚克隆出来的公式清成空格,导致 7 月及以后总行永远空白。 |
| | | continue; |
| | | } |
| | | int k = blockOffset % 5; // 1=班线 2=公交 3=出租城乡 4=网约车城乡 |
| | | if (isVal) { |
| | | 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 年列保留模板缓存值 |
| | | fillMidYoyCell(c, mid, currentYear, monthOfCol(col), area, k, volume); |
| | | } |
| | | } |
| | | } |
| | | // 累计同比列(模板 #REF! → 库内去年 1..N 月累计同比数值) |
| | | fillMidCumYoyColumn(sheet, mid, currentYear, fillMonths, extended ? 27 : 15); |
| | | recalc(wb); |
| | | // evaluateAll 会在跨簿公式(Sheet2 的 [1]xx!A1)处整本中断,累计列 AA 就永远拿不到新缓存值, |
| | | // 改用逐格容错的 recalcWorkbookFormulas:坏格跳过、好格照算。 |
| | | int[] recalc = recalcWorkbookFormulas(wb); |
| | | log.info("中口径明细公式重算完成:成功 {} 格,失败 {} 格", recalc[0], recalc[1]); |
| | | clearFormulaErrorsAll(wb); // 去年同期明细页残留 #REF! 同比公式 → 清空 |
| | | blankFutureMonthColumns(sheet, fillMonths); // 报表期之后的月份列不保留克隆公式/0 值 |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 清空「报表期之后」的月份列(扩列克隆出来的 7~12 月里、尚未到期的月份)。 |
| | | * 不清的话,克隆出来的总行公式会算出 0,报表上会出现"9~12 月全是 0"的假数据; |
| | | * 累计列(AA)的公式/缓存值已在重算阶段算好,清空格子不影响它(空 = 0)。 |
| | | */ |
| | | private void blankFutureMonthColumns(Sheet sheet, int filledMonths) { |
| | | for (int m = filledMonths + 1; m <= 12; m++) { |
| | | int col = 2 * m; // 0 基:1 月=2(C)、7 月=14(O)、12 月=24(Y) |
| | | for (int r = 4; r <= sheet.getLastRowNum(); r++) { // 0 基第 4 行 = 第 5 行(首个数据行),表头不动 |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | Cell val = row.getCell(col); |
| | | if (val != null) val.setBlank(); |
| | | Cell yoy = row.getCell(col + 1); |
| | | if (yoy != null) yoy.setBlank(); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | return (col - 1) / 2; |
| | | } |
| | | |
| | | /** 填中口径数据单元格:出租/网约车(3/4)显式 0,班线/公交填库内值(无值清空) */ |
| | | /** |
| | | * 填中口径数据单元格:班线/公交/出租城乡/网约车城乡 四维统一取库内值。 |
| | | * 库内该月该维无源(取值为 0)时**保留母版/模板原值**——绝不写空、也不写 0。 |
| | | * 依据:模板的城市级城乡行是「台账页 总量 − 城市内」的引用、并已业务定稿; |
| | | * 用库内缺失值覆盖会把定稿数清掉(如 2026-01~06 网约车订单缺失时, |
| | | * 全省网约车城乡 333.39 与各市州值会被清空)。 |
| | | */ |
| | | 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); |
| | | if (v == 0.0) return; // 库内无源:保留母版/定稿原值,缺数据不覆盖 |
| | | // POI setCellValue(double) 对公式单元格只更新缓存不移除公式,必须先 setBlank 再写值 |
| | | c.setBlank(); |
| | | if (v != 0.0) c.setCellValue(round(v, 4)); |
| | | c.setCellValue(v); |
| | | } |
| | | |
| | | /** 显式写入 0(POI setCellValue(0.0) 会转 blank,需操作底层 XML) */ |
| | |
| | | return volume ? arr[k][0] : arr[k][1]; |
| | | } |
| | | |
| | | /** B3:登记「该期该市州该维度有源行」。判据是源表有没有这一行,而不是算出来的值是否非 0 |
| | | * ——因为「值为 0」可能是真实业务(如武汉市巡游出租全部为城市内,城际城乡恒为 0)。 */ |
| | | private void markMid(Map<Integer, Map<String, boolean[]>> pres, int key, String city, int k) { |
| | | pres.computeIfAbsent(key, kk -> new HashMap<>()) |
| | | .computeIfAbsent(city, kk -> new boolean[5])[k] = true; |
| | | } |
| | | |
| | | /** |
| | | * B3:该月该维度是否「有源」(同比完备性校验用)。 |
| | | * k <= 0 表示「总量行」,要求四维(班线/公交/出租/网约车)都有源才算齐全。 |
| | | */ |
| | | private boolean midMonthHas(Map<Integer, Map<String, double[][]>> mid, int year, int month, String area, int k, boolean volume) { |
| | | Map<String, boolean[]> pm = midPresence.get().get(year * 100 + month); |
| | | if (pm == null) return false; |
| | | boolean[] b = pm.get(area); |
| | | if (b == null) return false; |
| | | if (k <= 0) { |
| | | for (int kk = 1; kk <= 4; kk++) { |
| | | if (!b[kk]) return false; |
| | | } |
| | | return true; |
| | | } |
| | | return k < b.length && b[k]; |
| | | } |
| | | |
| | | /** B3:去年 1..monthCount 月的命中月份数(用于判定同比基数是否齐全) */ |
| | | private int midCumHitMonths(Map<Integer, Map<String, double[][]>> mid, int year, String area, int monthCount, int k, boolean volume) { |
| | | int n = 0; |
| | | for (int m = 1; m <= monthCount; m++) { |
| | | if (midMonthHas(mid, year, m, area, k, volume)) n++; |
| | | } |
| | | return n; |
| | | } |
| | | |
| | | /** B3:当月同比——去年同月基数缺失则留空(缺数据不再当 0 计算) */ |
| | | private Double midMonthYoy(Map<Integer, Map<String, double[][]>> mid, int year, int month, String area, int k, boolean volume) { |
| | | if (!midMonthHas(mid, year - 1, month, area, k, volume)) return null; |
| | | double v = midClassVal(mid, year, month, area, k, volume); |
| | | if (v == 0.0) return null; |
| | | return yoyOf(v, midClassVal(mid, year - 1, month, area, k, volume)); |
| | | } |
| | | |
| | | /** B3:累计同比——去年 1..monthCount 各月齐全才计算,缺任何一月一律留空 */ |
| | | private Double midCumYoy(Map<Integer, Map<String, double[][]>> mid, int year, String area, int monthCount, int k, boolean volume) { |
| | | if (midCumHitMonths(mid, year - 1, area, monthCount, k, volume) < monthCount) return null; |
| | | double v = midCumClass(mid, year, area, monthCount, k, volume); |
| | | if (v == 0.0) return null; |
| | | return yoyOf(v, midCumClass(mid, year - 1, area, monthCount, k, volume)); |
| | | } |
| | | |
| | | /** 公式单元格替换为缓存数值(避免跨簿引用断链;非数值缓存置空) */ |
| | | private void keepCached(Cell c) { |
| | | try { |
| | |
| | | |
| | | /** |
| | | * 中口径分类月度数据:年*100+月 -> (市州/全省 -> double[5][2]) |
| | | * 维度 0=总量 1=公路班线(h2031) 2=城际城乡公交(cityBus) 3=巡游出租 4=网约车;[客运量(万人), 周转量(万人公里)] |
| | | * 维度 0=总量 1=公路班线(h2031+个体) 2=城际城乡公交(cityBus) 3=巡游出租 4=网约车;[客运量(万人), 周转量(万人公里)] |
| | | */ |
| | | /** B3:最近一次 loadMidClassMap 的「维度是否有源行」登记表(key -> city -> boolean[5]),用于同比完备性校验 */ |
| | | private final ThreadLocal<Map<Integer, Map<String, boolean[]>>> midPresence = |
| | | ThreadLocal.withInitial(HashMap::new); |
| | | |
| | | /** B4/B5:最近一次 loadMidClassMap 的数据问题清单(线程内,随导出体检输出/写生成件) */ |
| | | private final ThreadLocal<java.util.List<String>> midIssues = ThreadLocal.withInitial(java.util.ArrayList::new); |
| | | |
| | | /** B4/B5:取最近一次中口径数据加载的问题清单(只读副本) */ |
| | | public java.util.List<String> getLastMidIssues() { return new java.util.ArrayList<>(midIssues.get()); } |
| | | |
| | | private Map<Integer, Map<String, double[][]>> loadMidClassMap() { |
| | | midIssues.get().clear(); // B5:本次加载的问题清单 |
| | | Map<Integer, Map<String, boolean[]>> pres = new HashMap<>(); |
| | | midPresence.set(pres); // B3:本次加载的维度有源登记 |
| | | Map<Integer, Map<String, double[][]>> mid = new HashMap<>(); |
| | | for (PassengerEnterpriseMonthly e : passengerMapper.selectList(null)) { |
| | | if (e.getReportPeriod() == null) continue; |
| | |
| | | + nz(e.getTurnoverClass4()) + nz(e.getTurnoverCharter())) / 10000.0; |
| | | addMidClass(mid, key, city, 1, pass, turn); |
| | | addMidClass(mid, key, "湖北省", 1, pass, turn); |
| | | markMid(pres, key, city, 1); |
| | | markMid(pres, key, "湖北省", 1); |
| | | } |
| | | for (PassengerIndividualMonthly e : passengerIndividualMapper.selectList(null)) { // 个体并入公路班线/总量(09-03 口径) |
| | | if (e.getReportPeriod() == null) continue; |
| | | String[] parts = e.getReportPeriod().split("-"); |
| | | if (parts.length != 2) continue; |
| | | int y, m; |
| | | try { |
| | | y = Integer.parseInt(parts[0]); |
| | | m = Integer.parseInt(parts[1]); |
| | | } catch (NumberFormatException ex) { |
| | | continue; |
| | | } |
| | | String city = RegionUtil.cityByCode(e.getRegionCode()); |
| | | if (city == null) continue; |
| | | int key = y * 100 + m; |
| | | double pass = nz(e.getPassengerCount()) / 10000.0; |
| | | double turn = nz(e.getTurnover()) / 10000.0; |
| | | addMidClass(mid, key, city, 1, pass, turn); |
| | | addMidClass(mid, key, "湖北省", 1, pass, turn); |
| | | markMid(pres, key, city, 1); |
| | | markMid(pres, key, "湖北省", 1); |
| | | } |
| | | for (CityBusMonthly b : cityBusMapper.selectList(null)) { |
| | | if (b.getReportPeriod() == null || b.getCity() == null) 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())); |
| | | // 口径统一(2026-09-19,用户核对「中口径排名」周转量 vs 人工列):城际城乡一律用「总量 − 城市内」现算。 |
| | | // 原逻辑「存储的城际城乡 > 0 就优先取字段值,<=0 才回退到差额法」会在个别企业行上与差额法差 ±0.01 |
| | | // (源报表里总量/城市内/城际城乡各自两位小数舍入,2026 年 1-8 月全省共 72 行),累加后 |
| | | // 「中口径排名」页比「中口径明细」/人工口径多 0.32 万人公里。改为差额法后与台账页、明细页、四页合成口径一致。 |
| | | double cxPass = nz(b.getPassengerVolume()) - nz(b.getPassengerCity()); |
| | | double cxTurn = nz(b.getTurnover()) - nz(b.getTurnoverCity()); |
| | | if (cxPass < 0) cxPass = 0.0; |
| | | if (cxTurn < 0) cxTurn = 0.0; |
| | | addMidClass(mid, key, city, 2, cxPass, cxTurn); |
| | | addMidClass(mid, key, "湖北省", 2, cxPass, cxTurn); |
| | | markMid(pres, key, city, 2); |
| | | markMid(pres, key, "湖北省", 2); |
| | | } |
| | | // 维度3:城际城乡巡游出租 = 台账页「出租!总量 − 出租!城市内」(源 city_taxi_monthly) |
| | | for (CityTaxiMonthly t : cityTaxiMapper.selectList(null)) { |
| | | if (t.getReportPeriod() == null || t.getCity() == null) continue; |
| | | int key = midPeriodKey(t.getReportPeriod()); |
| | | if (key <= 0) continue; |
| | | String city = RegionUtil.normalizeCityName(t.getCity()); |
| | | if (city == null) continue; |
| | | double pass = nz(t.getPassengerVolume()) - nz(t.getPassengerCity()); |
| | | double turn = nz(t.getTurnover()) - nz(t.getTurnoverCity()); |
| | | addMidClass(mid, key, city, 3, pass, turn); |
| | | addMidClass(mid, key, "湖北省", 3, pass, turn); |
| | | markMid(pres, key, city, 3); |
| | | markMid(pres, key, "湖北省", 3); |
| | | } |
| | | // 维度4:城际城乡网约车 = 网约车拆分结果(与台账「网约车」页同源同法) |
| | | for (WycOrderMonthly o : wycOrderMapper.selectList(null)) { |
| | | String per = o.getReportPeriod(); |
| | | if (per == null) continue; |
| | | int key = midPeriodKey(per); |
| | | if (key <= 0) continue; |
| | | WycSplitCalc.WycResult wr; |
| | | try { |
| | | wr = wycSplitCalc.calc(per); |
| | | } catch (Exception e) { |
| | | // B5:不再静默跳过——记录「期 + 原因」并写日志,随导出体检一并输出 |
| | | String msg = per + " 网约车城际城乡维度缺失:" + e.getMessage(); |
| | | midIssues.get().add(msg); |
| | | log.warn("loadMidClassMap {}", msg); |
| | | continue; |
| | | } |
| | | for (Map.Entry<String, WycSplitCalc.WycMetrics> en : wr.getByCity().entrySet()) { |
| | | WycSplitCalc.WycMetrics wm = en.getValue(); |
| | | addMidClass(mid, key, en.getKey(), 4, wm.getSuburbanPax(), wm.getSuburbanTurnover()); |
| | | addMidClass(mid, key, "湖北省", 4, wm.getSuburbanPax(), wm.getSuburbanTurnover()); |
| | | markMid(pres, key, en.getKey(), 4); |
| | | markMid(pres, key, "湖北省", 4); |
| | | } |
| | | } |
| | | return mid; |
| | | } |
| | | |
| | | /** "yyyy-MM" -> yyyy*100+MM;非法返回 0(与 loadMidClassMap 的 key 口径一致) */ |
| | | private int midPeriodKey(String period) { |
| | | if (period == null) return 0; |
| | | String[] parts = period.trim().split("-"); |
| | | if (parts.length != 2) return 0; |
| | | try { |
| | | int y = Integer.parseInt(parts[0]); |
| | | int m = Integer.parseInt(parts[1]); |
| | | if (y <= 0 || m < 1 || m > 12) return 0; |
| | | return y * 100 + m; |
| | | } catch (NumberFormatException e) { |
| | | return 0; |
| | | } |
| | | } |
| | | |
| | | /** 写入分类值并累加总量(维度0) */ |
| | |
| | | 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)); |
| | | setNumeric(row, col++, cur); |
| | | 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)); |
| | | setNumeric(row, col++, cum); |
| | | Double cumBase = midCumOf(data, y - 1, region, monthCount, i, volume); |
| | | setNumeric(row, col++, yoy(cum, cumBase)); |
| | | } |
| | |
| | | title.getCell(0).setCellValue(currentYear + "年1-" + maxMonth + "月全省分市州累计完成道路客运生产情况"); |
| | | } |
| | | } |
| | | // 右块(当月)标题 K1 |
| | | if (title != null && title.getCell(10) != null) { |
| | | title.getCell(10).setCellValue(currentYear + "年" + maxMonth + "月全省分市州完成道路客运生产情况"); |
| | | } |
| | | // 全省行 r3:B3/F3 为 SUM 公式保留;D3/H3 同比填值(0-based 列 3/7) |
| | | setValOrBlank(sheet, 2, 3, yoyOf(midCumClass(mid, currentYear, "湖北省", maxMonth, 0, true), |
| | | midCumClass(mid, currentYear - 1, "湖北省", maxMonth, 0, true))); |
| | | setValOrBlank(sheet, 2, 7, yoyOf(midCumClass(mid, currentYear, "湖北省", maxMonth, 0, false), |
| | | midCumClass(mid, currentYear - 1, "湖北省", maxMonth, 0, false))); |
| | | setValOrBlank(sheet, 2, 3, midCumYoy(mid, currentYear, "湖北省", maxMonth, 0, true)); |
| | | setValOrBlank(sheet, 2, 7, midCumYoy(mid, currentYear, "湖北省", maxMonth, 0, false)); |
| | | // 右块全省行 N3/R3:当月同比(L3/P3 为 SUM 公式保留) |
| | | setValOrBlank(sheet, 2, 13, midMonthYoy(mid, currentYear, maxMonth, "湖北省", 0, true)); |
| | | setValOrBlank(sheet, 2, 17, midMonthYoy(mid, currentYear, 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++) { |
| | |
| | | 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)); |
| | | setValOrBlank(sheet, r0, 1, pass == 0 ? null : pass); |
| | | setValOrBlank(sheet, r0, 3, midCumYoy(mid, currentYear, city, maxMonth, 0, true)); |
| | | setValOrBlank(sheet, r0, 5, turn == 0 ? null : turn); |
| | | setValOrBlank(sheet, r0, 7, midCumYoy(mid, currentYear, city, maxMonth, 0, false)); |
| | | // 右块当月:L/P 值、N/R 当月同比(M/O/Q/S 排名公式保留) |
| | | double mPass = midClassVal(mid, currentYear, maxMonth, city, 0, true); |
| | | double mTurn = midClassVal(mid, currentYear, maxMonth, city, 0, false); |
| | | double mLastPass = midClassVal(mid, currentYear - 1, maxMonth, city, 0, true); |
| | | double mLastTurn = midClassVal(mid, currentYear - 1, maxMonth, city, 0, false); |
| | | setValOrBlank(sheet, r0, 11, mPass == 0 ? null : mPass); |
| | | setValOrBlank(sheet, r0, 13, mPass == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, city, 0, true)); |
| | | setValOrBlank(sheet, r0, 15, mTurn == 0 ? null : mTurn); |
| | | setValOrBlank(sheet, r0, 17, mTurn == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, city, 0, false)); |
| | | } |
| | | recalc(wb); |
| | | return toBytes(wb); |
| | |
| | | |
| | | private Double yoyOf(double cur, double base) { |
| | | if (base == 0.0) return null; |
| | | return round((cur - base) / base, 4); |
| | | return (cur - base) / base; |
| | | } |
| | | |
| | | /** 中口径分类累计(1..monthCount 月求和) */ |
| | |
| | | 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)); |
| | | setValOrBlank(sheet, r0, 1, pass == 0 ? null : pass); |
| | | setValOrBlank(sheet, r0, 2, midCumYoy(mid, currentYear, "湖北省", maxMonth, dims[i], true)); |
| | | setValOrBlank(sheet, r0, 5, turn == 0 ? null : turn); |
| | | setValOrBlank(sheet, r0, 6, midCumYoy(mid, currentYear, "湖北省", maxMonth, dims[i], false)); |
| | | } |
| | | // 下半块(当月):标题行9、数据行11-15(0-based 8、10-14) |
| | | Row bottomTitle = sheet.getRow(8); |
| | | if (bottomTitle != null && bottomTitle.getCell(0) != null) { |
| | | bottomTitle.getCell(0).setCellValue(currentYear + "年" + maxMonth + "月中口径客运量及周转量"); |
| | | } |
| | | for (int i = 0; i < names.length; i++) { |
| | | int r0 = 10 + i; |
| | | double pass = midClassVal(mid, currentYear, maxMonth, "湖北省", dims[i], true); |
| | | double turn = midClassVal(mid, currentYear, maxMonth, "湖北省", dims[i], false); |
| | | double lastPass = midClassVal(mid, currentYear - 1, maxMonth, "湖北省", dims[i], true); |
| | | double lastTurn = midClassVal(mid, currentYear - 1, maxMonth, "湖北省", dims[i], false); |
| | | setValOrBlank(sheet, r0, 1, pass == 0 ? null : pass); |
| | | setValOrBlank(sheet, r0, 2, pass == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, "湖北省", dims[i], true)); |
| | | setValOrBlank(sheet, r0, 5, turn == 0 ? null : turn); |
| | | setValOrBlank(sheet, r0, 6, turn == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, "湖北省", dims[i], false)); |
| | | } |
| | | // 下半块占比公式由样例指向上半块(B$3/F$3),改写为本块总行(B$11/F$11),行12-15 |
| | | for (int i = 1; i < names.length; i++) { |
| | | int r0 = 10 + i; |
| | | Row rr = sheet.getRow(r0); |
| | | if (rr == null) continue; |
| | | Cell dc = rr.getCell(3); |
| | | if (dc != null && dc.getCellType() == CellType.FORMULA) { |
| | | dc.setCellFormula("B" + (r0 + 1) + "/B$11"); |
| | | } |
| | | Cell hc = rr.getCell(7); |
| | | if (hc != null && hc.getCellType() == CellType.FORMULA) { |
| | | hc.setCellFormula("F" + (r0 + 1) + "/F$11"); |
| | | } |
| | | } |
| | | recalc(wb); |
| | | return toBytes(wb); |
| | |
| | | fillEnergyTemplateSheet(s, agg); |
| | | } |
| | | recalc(wb); |
| | | clearFormulaErrorsAll(wb); // 无去年数据的同比/增速公式 #DIV/0! → 清空 |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | |
| | | 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, |
| | | return exportCityByTemplate("生成_城市公交客运量分市州明细.xlsx", period, mode, |
| | | loadCityBusByMonth(year + "-", month), |
| | | loadCityBusByMonth((year - 1) + "-", month)); |
| | | } |
| | |
| | | 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, |
| | | return exportCityByTemplate("生成_巡游出租客运量分市州明细.xlsx", period, mode, |
| | | loadCityTaxiByMonth(year + "-", month), |
| | | loadCityTaxiByMonth((year - 1) + "-", month)); |
| | | } |
| | |
| | | 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"); |
| | | File template = resolveTemplate("生成_轨道轮渡客运量分市州明细.xlsx"); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheetAt(0); |
| | |
| | | // 轮渡:客运量行 13(全省)/15(武汉);指标 2=轮渡客运量 3=轮渡周转量 |
| | | fillRailFerryBlock(sheet, monthCity, month, new String[][]{{"全省", "12"}, {"武汉市", "14"}}, 2); |
| | | recalc(wb); |
| | | clearFormulaErrorCells(sheet, 3, sheet.getLastRowNum(), 1, 39); |
| | | fixCityMonthHeaderText(sheet); |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | |
| | | 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)); |
| | | if (v == 0) c.setBlank(); else c.setCellValue(v); |
| | | } |
| | | |
| | | /** 显式写入 0(setDataCell 对 0 置空;汇总表网约车行需要显示 0) */ |
| | |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, mode); |
| | | Map<String, double[]> cum = loadCityPassengerCumulative(year + "-", month); |
| | | Map<String, double[]> monthMap = loadCityPassengerMonthMap(year + "-", month); |
| | | File template = resolveTemplate(templateFileName); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | |
| | | 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 基) |
| | | fillCitySumBlock(sheet, cum, areas, 4, 0); // 累计客运量区(单位:万人次,POI 0 基,行5-22) |
| | | fillCitySumBlock(sheet, cum, areas, 26, 1); // 累计周转量区(单位:万人次公里,POI 0 基,行27-44) |
| | | // 当月块标题(0基行44/66)与数据(0基行48-65/70-87) |
| | | Row pTitle = sheet.getRow(44); |
| | | if (pTitle != null && pTitle.getCell(0) != null) { |
| | | pTitle.getCell(0).setCellValue("城市客运客运量分市州" + month + "月情况"); |
| | | } |
| | | Row tTitle = sheet.getRow(66); |
| | | if (tTitle != null && tTitle.getCell(0) != null) { |
| | | tTitle.getCell(0).setCellValue("城市客运客运周转量分市州" + month + "月情况"); |
| | | } |
| | | fillCitySumBlock(sheet, monthMap, areas, 48, 0); // 当月客运量区(行49-66) |
| | | fillCitySumBlock(sheet, monthMap, areas, 70, 1); // 当月周转量区(行71-88) |
| | | // 当月块排名/占比公式由累计块复制而来,RANK 范围与占比分母需改指当月块自身行 |
| | | repairCityMonthRankShare(sheet, 50, 66, 49); |
| | | repairCityMonthRankShare(sheet, 72, 88, 71); |
| | | recalc(wb); |
| | | clearFormulaErrorsAll(wb); // 无去年累计的占比/增速公式 #DIV/0! → 清空 |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | |
| | | setDataCell(sheet, rowIdx, 24, 0); |
| | | rowIdx++; |
| | | } |
| | | } |
| | | |
| | | /** 当月块排名/占比公式修正:模板当月块由累计块复制而来,RANK 范围与占比分母需改指当月块自身行 */ |
| | | private void repairCityMonthRankShare(Sheet sheet, int firstDataRow1, int lastDataRow1, int provRow1) { |
| | | String[] groups = {"B", "G", "L", "Q"}; |
| | | String[] growth = {"E", "J", "O", "T"}; |
| | | for (int r = firstDataRow1; r <= lastDataRow1; r++) { |
| | | int r0 = r - 1; |
| | | for (int g = 0; g < 4; g++) { |
| | | String base = groups[g]; |
| | | String grow = growth[g]; |
| | | setFormulaIfFormula(sheet, r0, 2 + g * 5, |
| | | "RANK(" + base + r + "," + base + "$" + firstDataRow1 + ":" + base + "$" + lastDataRow1 + ")"); |
| | | setFormulaIfFormula(sheet, r0, 3 + g * 5, base + r + "/" + base + "$" + provRow1); |
| | | setFormulaIfFormula(sheet, r0, 5 + g * 5, |
| | | "RANK(" + grow + r + "," + grow + "$" + firstDataRow1 + ":" + grow + "$" + lastDataRow1 + ")"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 单元格原为公式时改写公式(排名/占比列),非公式单元格不动(保持模板式样) */ |
| | | private void setFormulaIfFormula(Sheet sheet, int rowIdx, int colIdx, String formula) { |
| | | Row row = sheet.getRow(rowIdx); |
| | | if (row == null) return; |
| | | Cell c = row.getCell(colIdx); |
| | | if (c == null || c.getCellType() != CellType.FORMULA) return; |
| | | c.setCellFormula(formula); |
| | | } |
| | | |
| | | /** 某年 1..limit 月城市客运累计:city → [公交客运量,公交周转量,出租客运量,出租周转量,轨道客运量,轨道周转量,轮渡客运量,轮渡周转量],含"全省" */ |
| | |
| | | return cum; |
| | | } |
| | | |
| | | /** 某年单月城市客运数据:city → 8 维数组(口径同累计),含"全省";= month 累计 - (month-1) 累计 */ |
| | | private Map<String, double[]> loadCityPassengerMonthMap(String yearPrefix, int month) { |
| | | Map<String, double[]> cum = loadCityPassengerCumulative(yearPrefix, month); |
| | | if (month <= 1) return cum; |
| | | Map<String, double[]> pre = loadCityPassengerCumulative(yearPrefix, month - 1); |
| | | Map<String, double[]> out = new HashMap<>(); |
| | | for (Map.Entry<String, double[]> e : cum.entrySet()) { |
| | | double[] prev = pre.get(e.getKey()); |
| | | double[] d = new double[8]; |
| | | for (int i = 0; i < 8; i++) { |
| | | d[i] = e.getValue()[i] - (prev == null ? 0 : prev[i]); |
| | | } |
| | | out.put(e.getKey(), d); |
| | | } |
| | | return out; |
| | | } |
| | | |
| | | // ==================== 城市客运 全省汇总(城市汇总_模板) ==================== |
| | | |
| | | /** 生成_城市客运汇总.xlsx(以 城市汇总_模板.xlsx 为底稿;2026 段填库内 1..N 月累计,同比清空,2025/2024 段保留模板历史参考值) */ |
| | |
| | | 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"); |
| | | File template = resolveTemplate("生成_城市客运汇总.xlsx"); |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheetAt(0); |
| | |
| | | setDataCell(sheet, r, 2, 0); |
| | | setDataCell(sheet, r, 4, 0); |
| | | } |
| | | // 底部单月表(新版式):标题行11(0基10)、数据行13-19(0基12-18),填当月完成量 |
| | | Row mTitle = sheet.getRow(10); |
| | | if (mTitle != null && mTitle.getCell(0) != null) { |
| | | mTitle.getCell(0).setCellValue(year + "年" + month + "月全省完成城市客运量情况"); |
| | | } |
| | | double[] mProv = loadCityPassengerMonthMap(year + "-", month).getOrDefault("全省", new double[8]); |
| | | double mBusPax = mProv[0], mBusTurn = mProv[1]; |
| | | double mTaxiPax = mProv[2], mTaxiTurn = mProv[3]; |
| | | double mRailPax = mProv[4], mRailTurn = mProv[5]; |
| | | double mFerryPax = mProv[6], mFerryTurn = mProv[7]; |
| | | setDataCell(sheet, 12, 1, mBusPax + mTaxiPax + mRailPax + mFerryPax); // B13 总客运量 |
| | | setDataCell(sheet, 12, 3, mBusTurn + mTaxiTurn + mRailTurn + mFerryTurn); // D13 总周转量 |
| | | setDataCell(sheet, 13, 1, mBusPax); // B14 城市公交 |
| | | setDataCell(sheet, 13, 3, mBusTurn); // D14 |
| | | setDataCell(sheet, 14, 1, mTaxiPax); // B15 城市出租车 |
| | | setDataCell(sheet, 14, 3, mTaxiTurn); // D15 |
| | | setDataCell(sheet, 15, 1, mTaxiPax); // B16 其中巡游出租 |
| | | setDataCell(sheet, 15, 3, mTaxiTurn); // D16 |
| | | setZeroCell(sheet, 16, 1); // B17 城市网约车 |
| | | setZeroCell(sheet, 16, 3); // D17 |
| | | setDataCell(sheet, 17, 1, mRailPax); // B18 轨道 |
| | | setDataCell(sheet, 17, 3, mRailTurn); // D18 |
| | | setDataCell(sheet, 18, 1, mFerryPax); // B19 轮渡 |
| | | setDataCell(sheet, 18, 3, mFerryTurn);// D19 |
| | | for (int r = 12; r <= 18; r++) { // 单月表 C/E 同比:无上年单月源 → 清空样例 |
| | | setDataCell(sheet, r, 2, 0); |
| | | setDataCell(sheet, r, 4, 0); |
| | | } |
| | | recalc(wb); |
| | | clearFormulaErrorsAll(wb); // 无去年数据的同比公式 #DIV/0! → 清空 |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | |
| | | 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", exportCityPassengerCitySum("生成_城市客运客运量分市州累计.xlsx", period, mode)); |
| | | addZipEntry(zos, "生成_城市客运各市州明细表.xlsx", exportCityPassengerCitySum("生成_城市客运各市州明细表.xlsx", period, mode)); |
| | | addZipEntry(zos, "生成_城市客运汇总.xlsx", exportCityPassengerSummary(period, mode)); |
| | | } |
| | | return bos.toByteArray(); |
| | |
| | | addZipEntry(zos, "生成_中口径分析.xlsx", exportPassengerMidAnalysis(period, mode)); |
| | | } |
| | | return bos.toByteArray(); |
| | | } |
| | | |
| | | /** |
| | | * 生成_道路运输量汇总表.xlsx(P0-P1 骨架):整本复制母版《YYYY年M月道路运输量汇总表.xlsx》, |
| | | * 保留全部页签/版式/公式/2025 年缓存值,仅把表头年份动态化为目标年;数据填充按 P2 起逐步接入。 |
| | | */ |
| | | public byte[] exportSummaryWorkbook(String period, String mode) throws Exception { |
| | | return exportSummaryWorkbook(period, mode, null); |
| | | } |
| | | |
| | | /** 生成_道路运输量汇总表.xlsx(P0-P1 骨架,支持 toMonth 裁剪): |
| | | * 以《YYYY年M月道路运输量汇总表.xlsx》(M 由 period 决定)为母版,按同期备份母版还原月度块; |
| | | * toMonth 非空且小于 M 时,把 8 个台账/合成页裁到 toMonth 月(toMonth+1..M 的当月值/同比列清空、保留表头, |
| | | * 累计与累计同比公式改为按 toMonth 月重算),用于“先生成 1-6 月、1-7 月扩列”验证。 */ |
| | | public byte[] exportSummaryWorkbook(String period, String mode, Integer toMonth) throws Exception { |
| | | int year = Integer.parseInt(period.contains("-") ? period.split("-")[0] : period); |
| | | int month = period.contains("-") ? monthOf(period, mode) : 12; |
| | | File template = resolveSummaryMother(period, mode); |
| | | if (template == null) { |
| | | throw new RuntimeException("未找到汇总表母版《" + year + "年" + month + "月道路运输量汇总表.xlsx》(docs/生成汇总大表)"); |
| | | } |
| | | double savedZipRatio = ZipSecureFile.getMinInflateRatio(); |
| | | ZipSecureFile.setMinInflateRatio(0.0001); // 新版母版 styles.xml 由 openpyxl 高压缩(0.0099<0.01)触发 POI Zip-bomb 误报,读取后还原 |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | File donor = resolveSummaryDonor(year, month); |
| | | if (donor != null) { |
| | | try (InputStream din = new FileInputStream(donor); XSSFWorkbook dwb = new XSSFWorkbook(din)) { |
| | | int restored = restoreSummaryMonthlyBlocks(wb, dwb); |
| | | log.info("汇总工作簿月度块还原完成,补齐/覆盖单元格数={}(母版:{},备份母版:{})", restored, template.getName(), donor.getName()); |
| | | } |
| | | } else { |
| | | log.warn("汇总工作簿未找到同期备份母版(_备份_{}年{}月…),输出将保持母版现状(可能为 1-4 月骨架)", year, month); |
| | | } |
| | | if (toMonth != null && toMonth > 0 && toMonth < month) { |
| | | int trimmed = trimSummaryMonthlySheets(wb, toMonth); |
| | | log.info("汇总工作簿裁剪至 {} 月完成,清空/改写单元格数={}", toMonth, trimmed); |
| | | } |
| | | dynamicSummaryYear(wb, year); |
| | | cleanSummarySheetPresentation(wb); // 取消各页筛选、还原隐藏行(否则“班线包车”等页看起来像缺数据) |
| | | int effMonth = (toMonth != null && toMonth > 0 && toMonth < month) ? toMonth : month; |
| | | List<String> dbProblems = new ArrayList<>(); |
| | | String effPeriod = String.format("%04d-%02d", year, effMonth); |
| | | int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, effPeriod, year, effMonth, dbProblems); |
| | | int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth); |
| | | int monthFormulaFilled = summaryWorkbookFiller.ensureMonthFormulas(wb, year, effMonth); |
| | | // 累计同比分母修正:母版把「去年各月」与「去年1-M月累计」辅助列同时计入(重复计/漏当月) |
| | | int cumYoyFixed = summaryWorkbookFiller.repairCumulativeYoyFormulas(wb, year, effMonth); |
| | | int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month"); |
| | | int cumulativeRefreshed = refreshSummaryCumulativeSheets(wb, year, effMonth); // 四张跨页合成页随报表期刷新 |
| | | int widthApplied = applySummaryReferenceLayout(wb); |
| | | log.info("汇总工作簿数据驱动回填完成:写入当月值格数={},补写当月同比公式格数={},补写当月合计公式格数={}," |
| | | + "修正累计同比分母格数={},刷新排名页数值格数={},刷新四页(城市汇总/城市分市州/中口径分析/道路运输周转量)数值格数={}," |
| | | + "按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}", |
| | | dbFilled, yoyFilled, monthFormulaFilled, cumYoyFixed, rankRefreshed, cumulativeRefreshed, widthApplied, dbProblems); |
| | | applyTwoDecimalFormat(wb); // 数值显示两位小数(保留全精度);% / 日期等既有样式不改变 |
| | | int inlineFixed = normalizeStaleInlineStrings(wb); // 清掉「非 inlineStr 却残留 <is>」的非法格,否则 Excel 拒绝打开 |
| | | if (inlineFixed > 0) log.warn("汇总工作簿清掉残留 inlineStr 非法格 {} 个(母版由 openpyxl 写出时的典型问题)", inlineFixed); |
| | | int[] recalc = recalcWorkbookFormulas(wb); // 公式重算并写回缓存值:全省行(Σ市州)/累计列/排名 SUM 才能被预览与二次处理读到 |
| | | log.info("汇总工作簿公式重算完成:成功 {} 格,失败 {} 格(失败格保留母版旧缓存值,打开时按 fullCalcOnLoad 重算)", recalc[0], recalc[1]); |
| | | if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr(); |
| | | wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 还原/改写公式后打开即重算 |
| | | return toBytes(wb); |
| | | } finally { |
| | | ZipSecureFile.setMinInflateRatio(savedZipRatio); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 汇总工作簿公式重算:POI 写文件不带缓存值,导致「全省行(Σ17市州)」「累计列」「排名 SUM」 |
| | | * 在预览/二次处理里只看到母版上一期的旧值(2026-09-18 用户报“8月没数据 / 全省行停在上一期”的根因之一)。 |
| | | * 逐格 evaluate 并把结果写回缓存值;个别不支持函数/外部引用格失败则跳过并计数,不阻塞导出。 |
| | | */ |
| | | private int[] recalcWorkbookFormulas(XSSFWorkbook wb) { |
| | | if (wb == null) return new int[]{0, 0}; |
| | | org.apache.poi.ss.usermodel.FormulaEvaluator ev; |
| | | try { |
| | | ev = wb.getCreationHelper().createFormulaEvaluator(); |
| | | } catch (Exception e) { |
| | | log.warn("汇总工作簿公式重算:创建求值器失败,跳过({})", e.getMessage()); |
| | | return new int[]{0, 0}; |
| | | } |
| | | try { |
| | | // 跨簿公式([1]出租!C13 之类)在外部工作簿缺失时默认抛异常,会让引用它的 |
| | | // 总行/累计/派生公式连锁失败(表现为「公式还在但没有缓存值」)。 |
| | | // 置 true 后改用公式格自身的缓存值(即定稿值)参与计算。 |
| | | ev.setIgnoreMissingWorkbooks(true); |
| | | } catch (Exception ignore) { |
| | | // 个别实现不支持该开关,忽略即可 |
| | | } |
| | | int ok = 0, fail = 0; |
| | | java.util.List<String> samples = new java.util.ArrayList<>(); |
| | | for (int i = 0; i < wb.getNumberOfSheets(); i++) { |
| | | Sheet sh = wb.getSheetAt(i); |
| | | if (sh == null) continue; |
| | | for (Row row : sh) { |
| | | if (row == null) continue; |
| | | for (Cell c : row) { |
| | | if (c == null || c.getCellType() != CellType.FORMULA) continue; |
| | | try { |
| | | ev.evaluateFormulaCell(c); // 注意:evaluate() 无副作用,必须用 evaluateFormulaCell 才会把结果写回缓存值 |
| | | ok++; |
| | | } catch (Exception ex) { |
| | | fail++; |
| | | if (samples.size() < 10) { |
| | | samples.add(sh.getSheetName() + "!" + c.getAddress() + " :: " + ex.getClass().getSimpleName()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | if (!samples.isEmpty()) log.warn("汇总工作簿公式重算失败样例:{}", samples); |
| | | return new int[]{ok, fail}; |
| | | } |
| | | |
| | | /** |
| | | * 清掉「非 inlineStr 却残留 <is>」的非法单元格。 |
| | | * |
| | | * 母版若由 openpyxl 写出,文本单元格用 <is>(inlineStr)存储;POI 的 setCellValue 只改写 t 与 <v>, |
| | | * 不会清掉旧 <is>,于是产出 <c t="n"><v>67.0</v><is><t>67.00</t></is></c> 这种非法格, |
| | | * Excel 判定为「无法读取的内容」而直接拒绝打开(COM 表现为「不能取得类 Workbooks 的 Open 属性」)。 |
| | | * 2026-07 导出件即因此打不开(《公交》17 格 +《轨道、轮渡》2 格)。落盘前统一清理,幂等。 |
| | | */ |
| | | private int normalizeStaleInlineStrings(XSSFWorkbook wb) { |
| | | int fixed = 0; |
| | | for (int i = 0; i < wb.getNumberOfSheets(); i++) { |
| | | org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorksheet ctw = wb.getSheetAt(i).getCTWorksheet(); |
| | | if (ctw == null || ctw.getSheetData() == null) continue; |
| | | for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRow row : ctw.getSheetData().getRowArray()) { |
| | | for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCell c : row.getCArray()) { |
| | | if (!c.isSetIs()) continue; |
| | | STCellType.Enum t = c.getT(); |
| | | if (t == null || !STCellType.INLINE_STR.equals(t)) { |
| | | c.unsetIs(); |
| | | fixed++; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return fixed; |
| | | } |
| | | |
| | | /** |
| | | * 版式对齐:按 docs/生成汇总大表/《生成_道路运输量汇总表.xlsx》(用户提供的版式参照)逐列套用列宽与隐藏标记, |
| | | * 使生成件与参照件“格式完全一致”,并让母版没定义宽度的“后续扩列”(如 8 月列)也有与 8 月一致的列宽。 |
| | | * 参照件缺失时静默跳过(返回 0),不影响原有导出。 |
| | | */ |
| | | private int applySummaryReferenceLayout(XSSFWorkbook wb) { |
| | | File ref; |
| | | try { |
| | | ref = resolveSummaryLayoutRef(); |
| | | } catch (Exception e) { |
| | | return 0; |
| | | } |
| | | if (ref == null) return 0; |
| | | double savedZipRatio = ZipSecureFile.getMinInflateRatio(); |
| | | ZipSecureFile.setMinInflateRatio(0.0001); |
| | | int applied = 0; |
| | | try (InputStream in = new FileInputStream(ref); XSSFWorkbook rwb = new XSSFWorkbook(in)) { |
| | | for (int i = 0; i < rwb.getNumberOfSheets(); i++) { |
| | | org.apache.poi.xssf.usermodel.XSSFSheet rs = rwb.getSheetAt(i); |
| | | if (rs == null) continue; |
| | | org.apache.poi.xssf.usermodel.XSSFSheet ts = wb.getSheet(rs.getSheetName()); |
| | | if (ts == null) continue; |
| | | int maxCol = Math.min(usedColumnCount(rs), usedColumnCount(ts) + 4); |
| | | if (maxCol <= 0) continue; |
| | | double[] width = new double[maxCol]; |
| | | boolean[] hasWidth = new boolean[maxCol]; |
| | | boolean[] hidden = new boolean[maxCol]; |
| | | for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCols cols : rs.getCTWorksheet().getColsArray()) { |
| | | for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCol col : cols.getColArray()) { |
| | | long min = col.getMin(); |
| | | long max = col.getMax(); |
| | | for (long c = min; c <= max; c++) { |
| | | int idx = (int) (c - 1); |
| | | if (idx < 0 || idx >= maxCol) { |
| | | if (idx >= maxCol) break; |
| | | continue; |
| | | } |
| | | if (col.isSetWidth()) { |
| | | width[idx] = col.getWidth(); |
| | | hasWidth[idx] = true; |
| | | } |
| | | hidden[idx] = col.isSetHidden() && col.getHidden(); |
| | | } |
| | | } |
| | | } |
| | | for (int c = 0; c < maxCol; c++) { |
| | | if (hasWidth[c]) { |
| | | int w = (int) Math.round(width[c] * 256d); |
| | | if (ts.getColumnWidth(c) != w) { |
| | | ts.setColumnWidth(c, w); |
| | | applied++; |
| | | } |
| | | } |
| | | if (hidden[c] != ts.isColumnHidden(c)) ts.setColumnHidden(c, hidden[c]); |
| | | } |
| | | } |
| | | log.info("汇总工作簿版式对齐《生成_道路运输量汇总表》完成:套用列宽 {} 列(参照件:{})", applied, ref.getName()); |
| | | } catch (Exception e) { |
| | | log.warn("汇总工作簿版式对齐失败(忽略,继续按母版版式输出):{}", e.getMessage()); |
| | | return 0; |
| | | } finally { |
| | | ZipSecureFile.setMinInflateRatio(savedZipRatio); |
| | | } |
| | | return applied; |
| | | } |
| | | |
| | | /** 一页已用到的最大列数(0 基计数) */ |
| | | private int usedColumnCount(org.apache.poi.ss.usermodel.Sheet sh) { |
| | | int max = 0; |
| | | for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) { |
| | | org.apache.poi.ss.usermodel.Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | if (row.getLastCellNum() > max) max = row.getLastCellNum(); |
| | | } |
| | | return max; |
| | | } |
| | | |
| | | // ==================== 汇总工作簿内的排名页刷新 ==================== |
| | | |
| | | /** |
| | | * 汇总工作簿里的「货运量排名 / 货运周转量排名 / 中口径排名」在母版里是上一报表期的静态值, |
| | | * 导出时用独立排名表(《生成_货运量排名》《生成_周转量排名》《生成_中口径排名》,与用户日常核对的口径一致) |
| | | * 的结果刷新数值,避免汇总表里的排名页停留在旧月份。 |
| | | * 数值格搬值、公式格按偏移平移公式后写入;目标格本身已是公式的(SUM/RANK/占比/补数)保留不动,打开时按新数据重算。 |
| | | */ |
| | | private int refreshSummaryRankSheets(XSSFWorkbook wb, String period, String mode) throws Exception { |
| | | int n = 0; |
| | | n += copyRankValues(wb, "货运量排名", exportFreightRank(period, mode), new int[][]{{0, 0}, {22, 0}}); |
| | | n += copyRankValues(wb, "货运周转量排名", exportTurnoverRank(period, mode), new int[][]{{0, 0}, {22, 0}}); |
| | | n += copyRankValues(wb, "中口径排名", exportPassengerMidRank(period, mode), new int[][]{{0, 0}, {0, 10}}); |
| | | return n; |
| | | } |
| | | |
| | | /** |
| | | * 把独立排名表的各个块按标题对齐搬进汇总大表对应页。 |
| | | * srcBlocks[i] = 源块 i 的起始 {行, 列}(0 基);目标块按标题顺序(累计块在前、当月块在后)对应。 |
| | | * 数值格直接搬值;源侧是公式的(排名/占比等)按目标块偏移整体平移后写入公式,打开时按汇总表自身数据重算。 |
| | | */ |
| | | private int copyRankValues(XSSFWorkbook dstWb, String dstSheetName, byte[] srcBytes, int[][] srcBlocks) throws Exception { |
| | | XSSFSheet dst = dstWb.getSheet(dstSheetName); |
| | | if (dst == null) return 0; |
| | | List<XSSFCell> dstTitles = rankTitleCells(dst); |
| | | if (dstTitles.isEmpty()) return 0; |
| | | int copied = 0; |
| | | FormulaEvaluator dstEv = dstWb.getCreationHelper().createFormulaEvaluator(); |
| | | try (InputStream sin = new ByteArrayInputStream(srcBytes); |
| | | XSSFWorkbook srcWb = new XSSFWorkbook(sin)) { |
| | | XSSFSheet src = srcWb.getSheetAt(0); |
| | | FormulaEvaluator ev = srcWb.getCreationHelper().createFormulaEvaluator(); |
| | | int blocks = Math.min(srcBlocks.length, dstTitles.size()); |
| | | for (int b = 0; b < blocks; b++) { |
| | | int sr0 = srcBlocks[b][0]; |
| | | int sc0 = srcBlocks[b][1]; |
| | | XSSFCell dstTitle = dstTitles.get(b); |
| | | int rowOff = dstTitle.getRowIndex() - sr0; |
| | | int colOff = dstTitle.getColumnIndex() - sc0; |
| | | int h = rankBlockHeight(src, sr0, sc0); |
| | | int w = rankBlockWidth(src, sr0, sc0); |
| | | for (int r = 0; r < h; r++) { |
| | | Row srcRow = src.getRow(sr0 + r); |
| | | if (srcRow == null) continue; |
| | | Row dstRow = dst.getRow(sr0 + r + rowOff); |
| | | if (dstRow == null) dstRow = dst.createRow(sr0 + r + rowOff); |
| | | for (int c = 0; c < w; c++) { |
| | | Cell sc = srcRow.getCell(sc0 + c); |
| | | if (sc == null) continue; |
| | | int dc = sc0 + c + colOff; |
| | | if (r == 0) { |
| | | // 标题行:同步块标题文案。母版标题是 inlineStr,POI 的 setCellValue 只写 <v>、 |
| | | // 不更新 <is>,Excel 会继续显示旧月份,必须先 setBlank 清掉再写。 |
| | | Cell dt = dstRow.getCell(dc); |
| | | if (dt == null) dt = rankCell(dstRow, dc); |
| | | if (sc.getCellType() == CellType.STRING && dt.getCellType() != CellType.FORMULA) { |
| | | String text = sc.getStringCellValue(); |
| | | String old = dt.getCellType() == CellType.STRING ? dt.getStringCellValue() : null; |
| | | if (text != null && !text.equals(old)) { |
| | | dt.setBlank(); |
| | | dt.setCellValue(text); |
| | | } |
| | | } |
| | | continue; |
| | | } |
| | | Cell dcCell = dstRow.getCell(dc); |
| | | if (dcCell != null && dcCell.getCellType() == CellType.FORMULA) continue; // 保留母版自己的公式 |
| | | if (sc.getCellType() == CellType.FORMULA) { |
| | | // 源侧公式(排名/占比):按目标块偏移平移后原样写入,再按汇总表自身数据求值缓存。 |
| | | // 不直接搬 POI 对源表的求值结果(如 RANK 对手工留空的同比一律返回 1)。 |
| | | String f = sc.getCellFormula(); |
| | | if (f == null || f.isEmpty()) continue; |
| | | String shifted = shiftFormula(f, colOff, rowOff); |
| | | if (dcCell == null) dcCell = rankCell(dstRow, dc); |
| | | dcCell.setCellFormula(shifted); |
| | | try { |
| | | dstEv.evaluateFormulaCell(dcCell); |
| | | } catch (Exception ignore) { |
| | | // 求值失败不阻塞导出:打开时由 Excel/WPS 重算 |
| | | } |
| | | copied++; |
| | | continue; |
| | | } |
| | | Double v = rankNumeric(sc, ev); |
| | | if (v == null) continue; |
| | | if (dcCell == null) dcCell = rankCell(dstRow, dc); |
| | | dcCell.setCellValue(v); |
| | | copied++; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return copied; |
| | | } |
| | | |
| | | /** 目标块缺格时新建,并沿用同行左邻格样式(防新格丢边框/百分数格式) */ |
| | | private Cell rankCell(Row row, int col) { |
| | | Cell c = row.createCell(col); |
| | | Cell left = col > 0 ? row.getCell(col - 1) : null; |
| | | if (left != null) c.setCellStyle(left.getCellStyle()); |
| | | return c; |
| | | } |
| | | |
| | | /** 公式内所有 A1 引用整体平移:列 +dCol、行 +dRow(SUM/RANK 等函数名后接括号不会被匹配) */ |
| | | private String shiftFormula(String formula, int dCol, int dRow) { |
| | | if (formula == null || (dCol == 0 && dRow == 0)) return formula; |
| | | java.util.regex.Matcher m = java.util.regex.Pattern |
| | | .compile("(?<![A-Za-z0-9_$])([$]?)([A-Z]{1,3})([$]?)([0-9]+)") |
| | | .matcher(formula); |
| | | StringBuffer sb = new StringBuffer(); |
| | | while (m.find()) { |
| | | String col = m.group(2); |
| | | int idx = 0; |
| | | for (int i = 0; i < col.length(); i++) idx = idx * 26 + (col.charAt(i) - 'A' + 1); |
| | | int nidx = Math.max(1, idx + dCol); |
| | | StringBuilder nc = new StringBuilder(); |
| | | while (nidx > 0) { |
| | | int rem = (nidx - 1) % 26; |
| | | nc.insert(0, (char) ('A' + rem)); |
| | | nidx = (nidx - 1) / 26; |
| | | } |
| | | int nrow = Math.max(1, Integer.parseInt(m.group(4)) + dRow); |
| | | m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(m.group(1) + nc + m.group(3) + nrow)); |
| | | } |
| | | m.appendTail(sb); |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** 排名页里的“块标题”单元格(含“全省分市州”的说明文字),按行、列顺序返回 */ |
| | | private List<XSSFCell> rankTitleCells(Sheet sh) { |
| | | List<XSSFCell> out = new ArrayList<>(); |
| | | for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | for (int c = row.getFirstCellNum(); c < row.getLastCellNum(); c++) { |
| | | Cell cell = row.getCell(c); |
| | | if (cell == null || cell.getCellType() != CellType.STRING) continue; |
| | | String t = cell.getStringCellValue(); |
| | | if (t != null && t.contains("全省分市州")) out.add((XSSFCell) cell); |
| | | } |
| | | } |
| | | return out; |
| | | } |
| | | |
| | | /** 块高度:从起始行向下直到整行为空(在块列范围内) */ |
| | | private int rankBlockHeight(Sheet sh, int r0, int c0) { |
| | | int h = 0; |
| | | for (int r = r0; r <= sh.getLastRowNum(); r++) { |
| | | Row row = sh.getRow(r); |
| | | boolean any = false; |
| | | if (row != null) { |
| | | int last = row.getLastCellNum(); |
| | | for (int c = c0; c < last; c++) { |
| | | if (rankHasContent(row.getCell(c))) { any = true; break; } |
| | | } |
| | | } |
| | | if (!any) break; |
| | | h++; |
| | | } |
| | | return h; |
| | | } |
| | | |
| | | /** 块宽度:从起始列向右直到整列为空(在块行范围内,扫描上限 40 列) */ |
| | | private int rankBlockWidth(Sheet sh, int r0, int c0) { |
| | | int w = 0; |
| | | for (int c = c0; c < c0 + 40; c++) { |
| | | boolean any = false; |
| | | for (int r = r0; r < r0 + 30; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row != null && rankHasContent(row.getCell(c))) { any = true; break; } |
| | | } |
| | | if (!any) break; |
| | | w++; |
| | | } |
| | | return w; |
| | | } |
| | | |
| | | private boolean rankHasContent(Cell c) { |
| | | if (c == null) return false; |
| | | CellType t = c.getCellType(); |
| | | if (t == CellType.BLANK) return false; |
| | | if (t == CellType.STRING) { |
| | | String v = c.getStringCellValue(); |
| | | return v != null && !v.trim().isEmpty(); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** 取单元格数值(公式取求值结果,纯数字文本按数值解析),取不到返回 null */ |
| | | private Double rankNumeric(Cell c, FormulaEvaluator ev) { |
| | | try { |
| | | CellType t = c.getCellType(); |
| | | if (t == CellType.NUMERIC) return c.getNumericCellValue(); |
| | | if (t == CellType.FORMULA) { |
| | | org.apache.poi.ss.usermodel.CellValue cv = ev.evaluate(c); |
| | | return cv != null && cv.getCellType() == CellType.NUMERIC ? cv.getNumberValue() : null; |
| | | } |
| | | if (t == CellType.STRING) { |
| | | String v = c.getStringCellValue(); |
| | | if (v == null) return null; |
| | | String x = v.replace(",", "").trim(); |
| | | if (x.isEmpty()) return null; |
| | | try { return Double.parseDouble(x); } catch (NumberFormatException ignore) { return null; } |
| | | } |
| | | } catch (Exception ignore) { |
| | | return null; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** 定位版式参照件 docs/生成汇总大表/生成_道路运输量汇总表.xlsx(优先级同母版:配置目录 → user.dir 相对 → 上级) */ |
| | | // ==================== 汇总大表:四张“跨页合成”页随报表期刷新 ==================== |
| | | // 城市汇总 / 城市分市州 / 中口径分析 / 道路运输周转量 此前整页沿用母版静态值(标题停在 1-7 月)。 |
| | | // 本段按 2026-09-13 已验证口径(与 8 月母版逐格 0 差异)从母版台账页现算: |
| | | // 城市客运量 = 城市内公交 + 城市内巡游出租 + 城市内网约车 + 轨道 + 轮渡 |
| | | // 道路客运周转量 = 公路班线 + 城际城乡公交 + 城际城乡巡游出租 + 城际城乡网约车 |
| | | // 道路货运周转量 = 货运页 市州 规上货物周转量 + 规下货物周转量 |
| | | // 2026 年 1..N 月取台账页 2026 列(C/E/G…),去年同期取母版缓存 2025 列(V/X/Z…)——库里没有 2025 年城市客运数据。 |
| | | |
| | | /** 城市客运口径指标位(0..17 城市客运,18 货运周转量) */ |
| | | private static final int SM_BUS_PAX = 0, SM_BUS_TURN = 1; |
| | | private static final int SM_TAXI_PAX = 2, SM_TAXI_TURN = 3; |
| | | private static final int SM_WYC_PAX = 4, SM_WYC_TURN = 5; |
| | | private static final int SM_RAIL_PAX = 6, SM_RAIL_TURN = 7; |
| | | private static final int SM_FERRY_PAX = 8, SM_FERRY_TURN = 9; |
| | | private static final int SM_BX_PAX = 10, SM_BX_TURN = 11; |
| | | private static final int SM_BUS_CX_PAX = 12, SM_BUS_CX_TURN = 13; |
| | | private static final int SM_TAXI_CX_PAX = 14, SM_TAXI_CX_TURN = 15; |
| | | private static final int SM_WYC_CX_PAX = 16, SM_WYC_CX_TURN = 17; |
| | | private static final int SM_FREIGHT_TURN = 18; |
| | | private static final int SM_DIM = 19; |
| | | /** 2026 年 1 月当月值列(0 基,C 列);2025 年 1 月当月值列(0 基,V 列) */ |
| | | private static final int SM_CUR_COL0 = 2; |
| | | private static final int SM_LAST_COL0 = 21; |
| | | |
| | | /** |
| | | * 刷新四张跨页合成页:当年累计(1..month 月)、当月(month 月)及对应同比; |
| | | * 2026 与 2025 同期都取自母版台账页缓存,返回写入单元格数。 |
| | | */ |
| | | private int refreshSummaryCumulativeSheets(XSSFWorkbook wb, int year, int month) { |
| | | if (wb == null || month <= 0) return 0; |
| | | int[] cumMonths = new int[month]; |
| | | for (int m = 1; m <= month; m++) cumMonths[m - 1] = m; |
| | | int[] oneMonth = new int[]{month}; |
| | | Map<String, double[]> curCum = smReadLedgerMetrics(wb, true, cumMonths); |
| | | Map<String, double[]> lastCum = smReadLedgerMetrics(wb, false, cumMonths); |
| | | Map<String, double[]> curMon = smReadLedgerMetrics(wb, true, oneMonth); |
| | | Map<String, double[]> lastMon = smReadLedgerMetrics(wb, false, oneMonth); |
| | | if (curCum.isEmpty()) { |
| | | log.warn("汇总大表四页刷新:台账页未读到市州数据,跳过(保持母版原值)"); |
| | | return 0; |
| | | } |
| | | double[] pCurCum = smProvinceSum(curCum); |
| | | double[] pLastCum = smProvinceSum(lastCum); |
| | | double[] pCurMon = smProvinceSum(curMon); |
| | | double[] pLastMon = smProvinceSum(lastMon); |
| | | int n = 0; |
| | | n += smWriteCitySummary(wb, year, month, pCurCum, pLastCum, pCurMon, pLastMon); |
| | | n += smWriteCityByRegion(wb, month, curCum, lastCum, curMon, lastMon); |
| | | n += smWriteMidAnalysis(wb, month, pCurCum, pLastCum, pCurMon, pLastMon); |
| | | n += smWriteTurnoverSheets(wb, year, month, curCum, lastCum, curMon, lastMon); |
| | | return n; |
| | | } |
| | | |
| | | /** 17 市州合计(不含台账页“全省”公式行) */ |
| | | private double[] smProvinceSum(Map<String, double[]> m) { |
| | | double[] p = new double[SM_DIM]; |
| | | for (String c : RegionUtil.cityList()) { |
| | | double[] a = m.get(c); |
| | | if (a == null) continue; |
| | | for (int i = 0; i < SM_DIM; i++) p[i] += a[i]; |
| | | } |
| | | return p; |
| | | } |
| | | |
| | | /** 台账页累计取数:市州规范名 -> 19 维指标 */ |
| | | private Map<String, double[]> smReadLedgerMetrics(XSSFWorkbook wb, boolean curYear, int[] months) { |
| | | Map<String, double[]> out = new LinkedHashMap<>(); |
| | | smReadCityPaxPage(wb, "公交", curYear, months, out, SM_BUS_PAX, SM_BUS_TURN, SM_BUS_CX_PAX, SM_BUS_CX_TURN); |
| | | smReadCityPaxPage(wb, "出租", curYear, months, out, SM_TAXI_PAX, SM_TAXI_TURN, SM_TAXI_CX_PAX, SM_TAXI_CX_TURN); |
| | | smReadCityPaxPage(wb, "网约车", curYear, months, out, SM_WYC_PAX, SM_WYC_TURN, SM_WYC_CX_PAX, SM_WYC_CX_TURN); |
| | | smReadRailFerry(wb, curYear, months, out); |
| | | smReadBanxian(wb, curYear, months, out); |
| | | smReadFreightTurnover(wb, curYear, months, out); |
| | | return out; |
| | | } |
| | | |
| | | /** 公交/出租/网约车页:每市州 4 行(客运量/旅客周转量/其中城市内客运量/其中城市内旅客周转量) */ |
| | | private void smReadCityPaxPage(XSSFWorkbook wb, String sheetName, boolean curYear, int[] months, |
| | | Map<String, double[]> out, int paxIdx, int turnIdx, int cxPaxIdx, int cxTurnIdx) { |
| | | Sheet sh = wb.getSheet(sheetName); |
| | | if (sh == null) return; |
| | | int last = sh.getLastRowNum(); |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String city = smCityOf(row.getCell(0)); |
| | | if (city == null) continue; |
| | | int paxRow = -1, turnRow = -1, cxPaxRow = -1, cxTurnRow = -1; |
| | | for (int k = r; k <= Math.min(last, r + 4); k++) { |
| | | Row kr = sh.getRow(k); |
| | | if (kr == null) continue; |
| | | String b = smText(kr.getCell(1)); |
| | | if (b == null || b.contains("个体")) continue; |
| | | boolean inner = b.contains("城市内"); |
| | | boolean turn = b.contains("周转量"); |
| | | if (inner) { |
| | | if (turn) { if (cxTurnRow < 0) cxTurnRow = k; } |
| | | else if (cxPaxRow < 0) cxPaxRow = k; |
| | | } else { |
| | | if (turn) { if (turnRow < 0) turnRow = k; } |
| | | else if (paxRow < 0) paxRow = k; |
| | | } |
| | | } |
| | | double totalPax = smSumMonths(sh, paxRow, curYear, months); |
| | | double totalTurn = smSumMonths(sh, turnRow, curYear, months); |
| | | double cityPax = smSumMonths(sh, cxPaxRow, curYear, months); |
| | | double cityTurn = smSumMonths(sh, cxTurnRow, curYear, months); |
| | | double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]); |
| | | arr[paxIdx] += cityPax; // 城市客运口径取“城市内” |
| | | arr[turnIdx] += cityTurn; |
| | | arr[cxPaxIdx] += totalPax - cityPax; // 城际城乡 = 总量 - 城市内 |
| | | arr[cxTurnIdx] += totalTurn - cityTurn; |
| | | } |
| | | } |
| | | |
| | | /** 轨道、轮渡页:轨道块(武汉/黄石)+ 轮渡块(武汉) */ |
| | | private void smReadRailFerry(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) { |
| | | Sheet sh = wb.getSheet("轨道、轮渡"); |
| | | if (sh == null) return; |
| | | int last = sh.getLastRowNum(); |
| | | int ferryTitle = -1; |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String a = smText(row.getCell(0)); |
| | | if (a != null && a.contains("轮渡客运量")) { ferryTitle = r; break; } |
| | | } |
| | | smReadRailFerryBlock(sh, curYear, months, out, sh.getFirstRowNum(), |
| | | ferryTitle < 0 ? last : ferryTitle - 1, SM_RAIL_PAX); |
| | | if (ferryTitle >= 0) smReadRailFerryBlock(sh, curYear, months, out, ferryTitle, last, SM_FERRY_PAX); |
| | | } |
| | | |
| | | private void smReadRailFerryBlock(Sheet sh, boolean curYear, int[] months, Map<String, double[]> out, |
| | | int start, int end, int baseIdx) { |
| | | for (int r = start; r <= end; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String city = smCityOf(row.getCell(0)); |
| | | if (city == null) continue; |
| | | int paxRow = -1, turnRow = -1; |
| | | for (int k = r; k <= Math.min(end, r + 3); k++) { |
| | | Row kr = sh.getRow(k); |
| | | if (kr == null) continue; |
| | | String b = smText(kr.getCell(1)); |
| | | if (b == null) continue; |
| | | if (b.contains("周转量")) { if (turnRow < 0) turnRow = k; } |
| | | else if (paxRow < 0) paxRow = k; |
| | | } |
| | | double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]); |
| | | arr[baseIdx] += smSumMonths(sh, paxRow, curYear, months); |
| | | arr[baseIdx + 1] += smSumMonths(sh, turnRow, curYear, months); |
| | | } |
| | | } |
| | | |
| | | /** 班线包车页:每市州 客运量 / 旅客周转量(“其中个体”行不重复计入) */ |
| | | private void smReadBanxian(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) { |
| | | Sheet sh = wb.getSheet("班线包车"); |
| | | if (sh == null) return; |
| | | int last = sh.getLastRowNum(); |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String city = smCityOf(row.getCell(0)); |
| | | if (city == null) continue; |
| | | int paxRow = -1, turnRow = -1; |
| | | for (int k = r; k <= Math.min(last, r + 4); k++) { |
| | | Row kr = sh.getRow(k); |
| | | if (kr == null) continue; |
| | | String b = smText(kr.getCell(1)); |
| | | if (b == null || b.contains("个体")) continue; |
| | | if (b.contains("周转量")) { if (turnRow < 0) turnRow = k; } |
| | | else if (b.contains("客运量") && paxRow < 0) paxRow = k; |
| | | } |
| | | double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]); |
| | | arr[SM_BX_PAX] += smSumMonths(sh, paxRow, curYear, months); |
| | | arr[SM_BX_TURN] += smSumMonths(sh, turnRow, curYear, months); |
| | | } |
| | | } |
| | | |
| | | /** 货运页:每市州 规上货物周转量 + 规下货物周转量(“规上+规下”行本身是求和公式,不直接取) */ |
| | | private void smReadFreightTurnover(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) { |
| | | Sheet sh = wb.getSheet(" 货运"); |
| | | if (sh == null) sh = wb.getSheet("货运"); |
| | | if (sh == null) return; |
| | | int last = sh.getLastRowNum(); |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String city = smCityOf(row.getCell(0)); |
| | | if (city == null) continue; |
| | | int above = -1, below = -1; |
| | | for (int k = r; k <= Math.min(last, r + 6); k++) { |
| | | Row kr = sh.getRow(k); |
| | | if (kr == null) continue; |
| | | String b = smText(kr.getCell(1)); |
| | | if (b == null || b.contains("其中")) continue; |
| | | if (b.contains("规上货物周转量")) { if (above < 0) above = k; } |
| | | else if (b.contains("规下货物周转量")) { if (below < 0) below = k; } |
| | | } |
| | | double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]); |
| | | arr[SM_FREIGHT_TURN] += smSumMonths(sh, above, curYear, months) + smSumMonths(sh, below, curYear, months); |
| | | } |
| | | } |
| | | |
| | | private double smSumMonths(Sheet sh, int rowIdx, boolean curYear, int[] months) { |
| | | if (sh == null || rowIdx < 0) return 0; |
| | | Row row = sh.getRow(rowIdx); |
| | | if (row == null) return 0; |
| | | double s = 0; |
| | | for (int m : months) { |
| | | int col = (curYear ? SM_CUR_COL0 : SM_LAST_COL0) + 2 * (m - 1); |
| | | s += smNum(row.getCell(col)); |
| | | } |
| | | return s; |
| | | } |
| | | |
| | | /** 台账页单元格取数(数字 / 数字文本 / 公式缓存) */ |
| | | private double smNum(Cell c) { |
| | | if (c == null) return 0; |
| | | try { |
| | | CellType t = c.getCellType(); |
| | | if (t == CellType.NUMERIC) return c.getNumericCellValue(); |
| | | if (t == CellType.FORMULA) { |
| | | return c.getCachedFormulaResultType() == CellType.NUMERIC ? c.getNumericCellValue() : 0; |
| | | } |
| | | if (t == CellType.STRING) { |
| | | String s = c.getStringCellValue(); |
| | | if (s == null) return 0; |
| | | s = s.replace(",", "").replace("%", "").trim(); |
| | | if (s.isEmpty()) return 0; |
| | | return Double.parseDouble(s); |
| | | } |
| | | } catch (Exception ignore) { |
| | | return 0; |
| | | } |
| | | return 0; |
| | | } |
| | | |
| | | private String smText(Cell c) { |
| | | if (c == null) return null; |
| | | try { |
| | | return c.getCellType() == CellType.STRING ? c.getStringCellValue() : null; |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** 列 A 文案 -> 规范市州名(“全省/湖北省”与非市州行返回 null) */ |
| | | private String smCityOf(Cell a) { |
| | | String t = smText(a); |
| | | if (t == null || t.trim().isEmpty()) return null; |
| | | String norm = RegionUtil.normalizeCityName(t.trim()); |
| | | if (norm == null) return null; |
| | | return RegionUtil.cityList().contains(norm) ? norm : null; |
| | | } |
| | | |
| | | private double smYoy(double cur, double last) { |
| | | return last == 0.0 ? 0.0 : (cur - last) / last; |
| | | } |
| | | |
| | | /** 写值:公式格不动(保留母版公式,打开即重算);0 且原为空格则不新增 */ |
| | | private int smSetVal(Sheet sh, int r0, int c0, double v) { |
| | | Row row = sh.getRow(r0); |
| | | if (row == null) return 0; |
| | | Cell c = row.getCell(c0); |
| | | if (c != null && c.getCellType() == CellType.FORMULA) return 0; |
| | | if (c == null) { |
| | | if (v == 0.0) return 0; |
| | | c = row.createCell(c0); |
| | | Cell left = row.getCell(c0 - 1); |
| | | if (left != null) c.setCellStyle(left.getCellStyle()); |
| | | } else if (v == 0.0 && c.getCellType() == CellType.BLANK) { |
| | | return 0; |
| | | } |
| | | c.setBlank(); |
| | | c.setCellValue(v); |
| | | return 1; |
| | | } |
| | | |
| | | private void smSetTitle(Sheet sh, int r0, int c0, String text) { |
| | | if (sh == null) return; |
| | | Row row = sh.getRow(r0); |
| | | if (row == null) return; |
| | | Cell c = row.getCell(c0); |
| | | if (c == null || c.getCellType() == CellType.FORMULA) return; |
| | | c.setBlank(); |
| | | c.setCellValue(text); |
| | | } |
| | | |
| | | /** 城市汇总页:块1(1..N 月累计)行 3-9,块2(当月)行 14-20;列 B/C/D/E = 客运量/同比/周转量/同比 */ |
| | | private int smWriteCitySummary(XSSFWorkbook wb, int year, int month, |
| | | double[] pCurCum, double[] pLastCum, double[] pCurMon, double[] pLastMon) { |
| | | Sheet sh = wb.getSheet("城市汇总"); |
| | | if (sh == null) return 0; |
| | | smSetTitle(sh, 0, 0, year + "年1-" + month + "月全省累计完成城市客运量情况"); |
| | | smSetTitle(sh, 11, 0, year + "年" + month + "月全省完成城市客运量情况"); |
| | | int n = 0; |
| | | int[] rows = {2, 3, 4, 5, 6, 7, 8}; |
| | | for (int i = 0; i < rows.length; i++) { |
| | | double[] v = smCitySummaryRow(pCurCum, pLastCum, i); |
| | | n += smSetVal(sh, rows[i], 1, v[0]); |
| | | n += smSetVal(sh, rows[i], 2, v[2]); |
| | | n += smSetVal(sh, rows[i], 3, v[1]); |
| | | n += smSetVal(sh, rows[i], 4, v[3]); |
| | | } |
| | | for (int i = 0; i < rows.length; i++) { |
| | | double[] v = smCitySummaryRow(pCurMon, pLastMon, i); |
| | | n += smSetVal(sh, rows[i] + 11, 1, v[0]); |
| | | n += smSetVal(sh, rows[i] + 11, 2, v[2]); |
| | | n += smSetVal(sh, rows[i] + 11, 3, v[1]); |
| | | n += smSetVal(sh, rows[i] + 11, 4, v[3]); |
| | | } |
| | | return n; |
| | | } |
| | | |
| | | /** i=0 总客运量 1 公交 2 出租车 3 巡游出租 4 网约车 5 轨道 6 轮渡;返回 [客运量, 周转量, 客运量同比, 周转量同比] */ |
| | | private double[] smCitySummaryRow(double[] p, double[] pl, int i) { |
| | | double pax, turn, lPax, lTurn; |
| | | if (i == 0) { |
| | | pax = p[SM_BUS_PAX] + p[SM_TAXI_PAX] + p[SM_WYC_PAX] + p[SM_RAIL_PAX] + p[SM_FERRY_PAX]; |
| | | turn = p[SM_BUS_TURN] + p[SM_TAXI_TURN] + p[SM_WYC_TURN] + p[SM_RAIL_TURN] + p[SM_FERRY_TURN]; |
| | | lPax = pl[SM_BUS_PAX] + pl[SM_TAXI_PAX] + pl[SM_WYC_PAX] + pl[SM_RAIL_PAX] + pl[SM_FERRY_PAX]; |
| | | lTurn = pl[SM_BUS_TURN] + pl[SM_TAXI_TURN] + pl[SM_WYC_TURN] + pl[SM_RAIL_TURN] + pl[SM_FERRY_TURN]; |
| | | } else if (i == 1) { |
| | | pax = p[SM_BUS_PAX]; turn = p[SM_BUS_TURN]; lPax = pl[SM_BUS_PAX]; lTurn = pl[SM_BUS_TURN]; |
| | | } else if (i == 2) { |
| | | pax = p[SM_TAXI_PAX] + p[SM_WYC_PAX]; turn = p[SM_TAXI_TURN] + p[SM_WYC_TURN]; |
| | | lPax = pl[SM_TAXI_PAX] + pl[SM_WYC_PAX]; lTurn = pl[SM_TAXI_TURN] + pl[SM_WYC_TURN]; |
| | | } else if (i == 3) { |
| | | pax = p[SM_TAXI_PAX]; turn = p[SM_TAXI_TURN]; lPax = pl[SM_TAXI_PAX]; lTurn = pl[SM_TAXI_TURN]; |
| | | } else if (i == 4) { |
| | | pax = p[SM_WYC_PAX]; turn = p[SM_WYC_TURN]; lPax = pl[SM_WYC_PAX]; lTurn = pl[SM_WYC_TURN]; |
| | | } else if (i == 5) { |
| | | pax = p[SM_RAIL_PAX]; turn = p[SM_RAIL_TURN]; lPax = pl[SM_RAIL_PAX]; lTurn = pl[SM_RAIL_TURN]; |
| | | } else { |
| | | pax = p[SM_FERRY_PAX]; turn = p[SM_FERRY_TURN]; lPax = pl[SM_FERRY_PAX]; lTurn = pl[SM_FERRY_TURN]; |
| | | } |
| | | return new double[]{pax, turn, smYoy(pax, lPax), smYoy(turn, lTurn)}; |
| | | } |
| | | |
| | | /** 城市分市州页:4 块(累计客运量/累计周转量/当月客运量/当月周转量),每块 全省 1 行 + 17 市州 */ |
| | | private int smWriteCityByRegion(XSSFWorkbook wb, int month, |
| | | Map<String, double[]> curCum, Map<String, double[]> lastCum, |
| | | Map<String, double[]> curMon, Map<String, double[]> lastMon) { |
| | | Sheet sh = wb.getSheet("城市分市州"); |
| | | if (sh == null) return 0; |
| | | smSetTitle(sh, 0, 0, "城市客运客运量分市州1-" + month + "月累计情况"); |
| | | smSetTitle(sh, 22, 0, "城市客运客运周转量分市州1-" + month + "月累计情况"); |
| | | smSetTitle(sh, 45, 0, "城市客运客运量分市州" + month + "月情况"); |
| | | smSetTitle(sh, 67, 0, "城市客运客运周转量分市州" + month + "月情况"); |
| | | int n = 0; |
| | | n += smWriteCityByRegionBlock(sh, 4, true, curCum, lastCum); |
| | | n += smWriteCityByRegionBlock(sh, 26, false, curCum, lastCum); |
| | | n += smWriteCityByRegionBlock(sh, 49, true, curMon, lastMon); |
| | | n += smWriteCityByRegionBlock(sh, 71, false, curMon, lastMon); |
| | | return n; |
| | | } |
| | | |
| | | private int smWriteCityByRegionBlock(Sheet sh, int provRow, boolean pax, |
| | | Map<String, double[]> cur, Map<String, double[]> last) { |
| | | int n = 0; |
| | | double[] provCur = smProvinceSum(cur); |
| | | double[] provLast = smProvinceSum(last); |
| | | List<String> cities = RegionUtil.cityList(); |
| | | for (int i = -1; i < cities.size(); i++) { |
| | | int r = provRow + i + 1; |
| | | double[] c = i < 0 ? provCur : cur.get(cities.get(i)); |
| | | double[] l = i < 0 ? provLast : last.get(cities.get(i)); |
| | | if (c == null) c = new double[SM_DIM]; |
| | | if (l == null) l = new double[SM_DIM]; |
| | | double[] g = smCityGroupValues(c, l, pax); |
| | | n += smSetVal(sh, r, 1, g[0]); |
| | | n += smSetVal(sh, r, 4, g[6]); |
| | | n += smSetVal(sh, r, 6, g[1]); |
| | | n += smSetVal(sh, r, 9, g[7]); |
| | | n += smSetVal(sh, r, 11, g[2]); |
| | | n += smSetVal(sh, r, 14, g[8]); |
| | | n += smSetVal(sh, r, 16, g[3]); |
| | | n += smSetVal(sh, r, 19, g[9]); |
| | | n += smSetVal(sh, r, 21, g[4]); |
| | | n += smSetVal(sh, r, 22, g[10]); |
| | | n += smSetVal(sh, r, 23, g[5]); |
| | | n += smSetVal(sh, r, 24, g[11]); |
| | | } |
| | | return n; |
| | | } |
| | | |
| | | /** 6 组值 + 6 组增速:[总量, 公交, 出租, 网约车, 轨道, 轮渡, 总量增速, 公交…, 轮渡] */ |
| | | private double[] smCityGroupValues(double[] c, double[] l, boolean pax) { |
| | | int busI = pax ? SM_BUS_PAX : SM_BUS_TURN; |
| | | int taxiI = pax ? SM_TAXI_PAX : SM_TAXI_TURN; |
| | | int wycI = pax ? SM_WYC_PAX : SM_WYC_TURN; |
| | | int railI = pax ? SM_RAIL_PAX : SM_RAIL_TURN; |
| | | int ferryI = pax ? SM_FERRY_PAX : SM_FERRY_TURN; |
| | | double bus = c[busI], taxi = c[taxiI], wyc = c[wycI], rail = c[railI], ferry = c[ferryI]; |
| | | double lBus = l[busI], lTaxi = l[taxiI], lWyc = l[wycI], lRail = l[railI], lFerry = l[ferryI]; |
| | | double total = bus + taxi + wyc + rail + ferry; |
| | | double lTotal = lBus + lTaxi + lWyc + lRail + lFerry; |
| | | return new double[]{total, bus, taxi, wyc, rail, ferry, |
| | | smYoy(total, lTotal), smYoy(bus, lBus), smYoy(taxi, lTaxi), smYoy(wyc, lWyc), |
| | | smYoy(rail, lRail), smYoy(ferry, lFerry)}; |
| | | } |
| | | |
| | | /** 中口径分析页:块1(累计)行 3-7,块2(当月)行 12-16;列 B/C/F/G = 客运量/同比/周转量/同比 */ |
| | | private int smWriteMidAnalysis(XSSFWorkbook wb, int month, |
| | | double[] pCurCum, double[] pLastCum, double[] pCurMon, double[] pLastMon) { |
| | | Sheet sh = wb.getSheet("中口径分析"); |
| | | if (sh == null) return 0; |
| | | smSetTitle(sh, 0, 0, "1-" + month + "月中口径客运量及周转量"); |
| | | smSetTitle(sh, 9, 0, month + "月中口径客运量及周转量"); |
| | | int n = 0; |
| | | int[] rows = {2, 3, 4, 5, 6}; |
| | | for (int i = 0; i < rows.length; i++) { |
| | | double[] v = smMidRow(pCurCum, pLastCum, i); |
| | | n += smSetVal(sh, rows[i], 1, v[0]); |
| | | n += smSetVal(sh, rows[i], 2, v[2]); |
| | | n += smSetVal(sh, rows[i], 5, v[1]); |
| | | n += smSetVal(sh, rows[i], 6, v[3]); |
| | | } |
| | | for (int i = 0; i < rows.length; i++) { |
| | | double[] v = smMidRow(pCurMon, pLastMon, i); |
| | | n += smSetVal(sh, rows[i] + 9, 1, v[0]); |
| | | n += smSetVal(sh, rows[i] + 9, 2, v[2]); |
| | | n += smSetVal(sh, rows[i] + 9, 5, v[1]); |
| | | n += smSetVal(sh, rows[i] + 9, 6, v[3]); |
| | | } |
| | | return n; |
| | | } |
| | | |
| | | /** i=0 总 1 公路班线 2 城际城乡公交 3 城际城乡巡游出租 4 城际城乡网约车;返回 [客运量, 周转量, 客运量同比, 周转量同比] */ |
| | | private double[] smMidRow(double[] p, double[] pl, int i) { |
| | | int[] paxIdx = {SM_BX_PAX, SM_BX_PAX, SM_BUS_CX_PAX, SM_TAXI_CX_PAX, SM_WYC_CX_PAX}; |
| | | int[] turnIdx = {SM_BX_TURN, SM_BX_TURN, SM_BUS_CX_TURN, SM_TAXI_CX_TURN, SM_WYC_CX_TURN}; |
| | | double pax, turn, lPax, lTurn; |
| | | if (i == 0) { |
| | | pax = p[SM_BX_PAX] + p[SM_BUS_CX_PAX] + p[SM_TAXI_CX_PAX] + p[SM_WYC_CX_PAX]; |
| | | turn = p[SM_BX_TURN] + p[SM_BUS_CX_TURN] + p[SM_TAXI_CX_TURN] + p[SM_WYC_CX_TURN]; |
| | | lPax = pl[SM_BX_PAX] + pl[SM_BUS_CX_PAX] + pl[SM_TAXI_CX_PAX] + pl[SM_WYC_CX_PAX]; |
| | | lTurn = pl[SM_BX_TURN] + pl[SM_BUS_CX_TURN] + pl[SM_TAXI_CX_TURN] + pl[SM_WYC_CX_TURN]; |
| | | } else { |
| | | pax = p[paxIdx[i]]; turn = p[turnIdx[i]]; |
| | | lPax = pl[paxIdx[i]]; lTurn = pl[turnIdx[i]]; |
| | | } |
| | | return new double[]{pax, turn, smYoy(pax, lPax), smYoy(turn, lTurn)}; |
| | | } |
| | | |
| | | /** 道路运输周转量页:块1(累计)行 4-21,块2(当月)行 26-43;只写值列(B/E/V 等公式保留) */ |
| | | private int smWriteTurnoverSheets(XSSFWorkbook wb, int year, int month, |
| | | Map<String, double[]> curCum, Map<String, double[]> lastCum, |
| | | Map<String, double[]> curMon, Map<String, double[]> lastMon) { |
| | | Sheet sh = wb.getSheet("道路运输周转量"); |
| | | if (sh == null) return 0; |
| | | smSetTitle(sh, 0, 0, year + "年1-" + month + "月全省累计完成交通周转量情况"); |
| | | smSetTitle(sh, 22, 0, year + "年" + month + "月全省完成交通周转量情况"); |
| | | int n = 0; |
| | | n += smWriteTurnoverBlock(sh, 3, curCum, lastCum); |
| | | n += smWriteTurnoverBlock(sh, 25, curMon, lastMon); |
| | | return n; |
| | | } |
| | | |
| | | private int smWriteTurnoverBlock(Sheet sh, int provRow, Map<String, double[]> cur, Map<String, double[]> last) { |
| | | int n = 0; |
| | | double[] provCur = smProvinceSum(cur); |
| | | double[] provLast = smProvinceSum(last); |
| | | List<String> cities = RegionUtil.cityList(); |
| | | for (int i = -1; i < cities.size(); i++) { |
| | | int r = provRow + i + 1; |
| | | double[] c = i < 0 ? provCur : cur.get(cities.get(i)); |
| | | double[] l = i < 0 ? provLast : last.get(cities.get(i)); |
| | | if (c == null) c = new double[SM_DIM]; |
| | | if (l == null) l = new double[SM_DIM]; |
| | | double freight = c[SM_FREIGHT_TURN], lFreight = l[SM_FREIGHT_TURN]; |
| | | double pax = c[SM_BUS_PAX] + c[SM_TAXI_PAX] + c[SM_WYC_PAX] + c[SM_RAIL_PAX] + c[SM_FERRY_PAX]; |
| | | double lPax = l[SM_BUS_PAX] + l[SM_TAXI_PAX] + l[SM_WYC_PAX] + l[SM_RAIL_PAX] + l[SM_FERRY_PAX]; |
| | | double road = c[SM_BX_TURN] + c[SM_BUS_CX_TURN] + c[SM_TAXI_CX_TURN] + c[SM_WYC_CX_TURN]; |
| | | double lRoad = l[SM_BX_TURN] + l[SM_BUS_CX_TURN] + l[SM_TAXI_CX_TURN] + l[SM_WYC_CX_TURN]; |
| | | n += smSetVal(sh, r, 6, freight); |
| | | n += smSetVal(sh, r, 9, smYoy(freight, lFreight)); |
| | | n += smSetVal(sh, r, 11, pax); |
| | | n += smSetVal(sh, r, 14, smYoy(pax, lPax)); |
| | | n += smSetVal(sh, r, 16, road); |
| | | n += smSetVal(sh, r, 19, smYoy(road, lRoad)); |
| | | n += smSetVal(sh, r, 22, lFreight); |
| | | n += smSetVal(sh, r, 23, lPax); |
| | | n += smSetVal(sh, r, 24, lRoad); |
| | | } |
| | | return n; |
| | | } |
| | | |
| | | |
| | | private File resolveSummaryLayoutRef() { |
| | | String rel = summaryTemplateDir; |
| | | while (rel != null && rel.startsWith("./")) rel = rel.substring(2); |
| | | String[] roots = { |
| | | summaryTemplateDir, |
| | | System.getProperty("user.dir") + "/" + rel, |
| | | System.getProperty("user.dir") + "/../" + rel |
| | | }; |
| | | for (String root : roots) { |
| | | if (root == null || root.trim().isEmpty()) continue; |
| | | File f = new File(root, "生成_道路运输量汇总表.xlsx"); |
| | | if (f.isFile()) return f; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** 汇总表母版定位:仅精确匹配 {year}年{month}月…(P0-P1 骨架期不允许跨月/跨年静默回退, |
| | | * 目标期无对应母版时明确报错,月份扩展规则待 P0 母版定稿后再放开) */ |
| | | private File resolveSummaryMother(String period, String mode) throws Exception { |
| | | String year = period != null && period.contains("-") ? period.split("-")[0] : (period == null ? "" : period); |
| | | int month = period != null && period.contains("-") ? monthOf(period, mode) : 12; |
| | | String name = year + "年" + month + "月道路运输量汇总表.xlsx"; |
| | | return resolveAnyTemplate(summaryTemplateDir, name); |
| | | } |
| | | |
| | | // ==================== 网约车客运量拆分表(月度) ==================== |
| | | |
| | | /** 网约车客运量拆分表:输入=部级订单(网约车订单及全省总量.xlsx G-W)+ 全省 pin(B-E)+ 巡游出租月报(DB); |
| | | * 算法与《网约车拆分.xlsx》月页一致:客运量=订单占比×全省总量;城市内=巡游出租城市内占比代理、武汉保持自身其余按剩余分摊; |
| | | * 周转量=客运量×巡游出租平均运距占比分摊全省周转量 pin,城市内周转量同法。 */ |
| | | public byte[] exportWycSplit(String period, String mode) throws Exception { |
| | | if (period == null || !period.contains("-")) { |
| | | throw new RuntimeException("网约车客运量拆分表仅支持按月生成,请选择具体月份"); |
| | | } |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = parseMonth(period); |
| | | Map<String, Object> inp = loadWycMonthlyInput(period, mode); |
| | | if (inp == null) { |
| | | throw new RuntimeException("《网约车订单及全省总量.xlsx》缺少报表期 " + period + " 的数据行,请先维护当月订单与全省 pin"); |
| | | } |
| | | double pinK = (Double) inp.get("pinK"); |
| | | double pinCK = (Double) inp.get("pinCK"); |
| | | double pinZ = (Double) inp.get("pinZ"); |
| | | double pinCZ = (Double) inp.get("pinCZ"); |
| | | double orderSum = (Double) inp.get("orderSum"); |
| | | double[] orderArr = (double[]) inp.get("orders"); |
| | | if (pinK <= 0 || pinCK <= 0 || pinZ <= 0 || pinCZ <= 0 || orderSum <= 0) { |
| | | throw new RuntimeException("网约车输入 " + period + " 行不完整:全省 pin / 订单缺失,请核对《网约车订单及全省总量.xlsx》"); |
| | | } |
| | | List<String> cities = RegionUtil.cityList(); |
| | | int n = cities.size(); |
| | | if (orderArr == null || orderArr.length < n) { |
| | | throw new RuntimeException("网约车订单列数不足(应为 17 市州订单列)"); |
| | | } |
| | | Map<String, double[]> taxi = taxiMonthlyOf(period); |
| | | java.util.List<String> missingTaxi = new java.util.ArrayList<>(); |
| | | double[] Dtax = new double[n]; |
| | | double[] Etax = new double[n]; |
| | | double[] avgD = new double[n]; |
| | | double[] avgDc = new double[n]; |
| | | for (int i = 0; i < n; i++) { |
| | | double[] a = taxi.get(cities.get(i)); |
| | | if (a == null || a[0] <= 0) { |
| | | missingTaxi.add(cities.get(i)); |
| | | continue; |
| | | } |
| | | Dtax[i] = a[0]; |
| | | Etax[i] = a[2]; |
| | | avgD[i] = a[4]; |
| | | avgDc[i] = a[5]; |
| | | } |
| | | if (!missingTaxi.isEmpty()) { |
| | | throw new RuntimeException("巡游出租月报缺少以下市州数据:" + String.join("、", missingTaxi)); |
| | | } |
| | | double[] C = new double[n]; |
| | | double[] H = new double[n]; |
| | | double[] I = new double[n]; |
| | | double[] Gl = new double[n]; |
| | | double[] Hl = new double[n]; |
| | | double[] Il = new double[n]; |
| | | double sumG = 0; |
| | | double sumDl = 0; |
| | | double sumEl = 0; |
| | | for (int i = 0; i < n; i++) { |
| | | C[i] = orderArr[i] / orderSum * pinK; |
| | | double F = Etax[i] / Dtax[i]; |
| | | double G = C[i] * F; |
| | | sumG += G; |
| | | double Dl = C[i] * avgD[i]; |
| | | double El = H[i] * avgDc[i]; |
| | | sumDl += Dl; |
| | | sumEl += El; |
| | | } |
| | | // 城市内:武汉保持自身,其余按 G 剩余占比分摊至全省城市内 pin |
| | | for (int i = 0; i < n; i++) { |
| | | if (i == 0) { |
| | | H[i] = C[i] * (Etax[i] / Dtax[i]); |
| | | } else { |
| | | H[i] = (C[i] * (Etax[i] / Dtax[i])) / (sumG - C[0] * (Etax[0] / Dtax[0])) * (pinCK - C[0] * (Etax[0] / Dtax[0])); |
| | | } |
| | | I[i] = C[i] - H[i]; |
| | | } |
| | | // 周转量:按 客运量×巡游出租平均运距 占比分摊全省周转量 pin |
| | | double whG = C[0] * (Etax[0] / Dtax[0]); |
| | | double whDl = C[0] * avgD[0]; |
| | | double[] El2 = new double[n]; |
| | | double sumEl2 = 0; |
| | | for (int i = 0; i < n; i++) { |
| | | double Dl = C[i] * avgD[i]; |
| | | double El2v = H[i] * avgDc[i]; |
| | | El2[i] = El2v; |
| | | sumEl2 += El2v; |
| | | Gl[i] = Dl / sumDl * pinZ; |
| | | } |
| | | for (int i = 0; i < n; i++) { |
| | | if (i == 0) { |
| | | Hl[i] = Gl[i]; |
| | | } else { |
| | | Hl[i] = El2[i] / (sumEl2 - El2[0]) * (pinCZ - Gl[0]); |
| | | } |
| | | Il[i] = Gl[i] - Hl[i]; |
| | | } |
| | | return writeWycSplitWorkbook(period, cities, orderArr, C, Dtax, Etax, H, I, avgD, avgDc, Gl, Hl, Il, pinK, pinCK, pinZ, pinCZ, orderSum); |
| | | } |
| | | |
| | | private byte[] writeWycSplitWorkbook(String period, List<String> cities, double[] orders, double[] C, double[] Dtax, |
| | | double[] Etax, double[] H, double[] I, double[] avgD, double[] avgDc, |
| | | double[] Gl, double[] Hl, double[] Il, |
| | | double pinK, double pinCK, double pinZ, double pinCZ, double orderSum) throws Exception { |
| | | XSSFWorkbook wb = new XSSFWorkbook(); |
| | | XSSFSheet sh = wb.createSheet(period + "拆分"); |
| | | int n = cities.size(); |
| | | // 客运量块 |
| | | Row t = sh.createRow(0); |
| | | t.createCell(0).setCellValue("网约车客运量拆分(万人) 报表期:" + period); |
| | | String[] head1 = {"市州", "订单总数(单)", "网约车客运量(万人)", "巡游出租客运量(万人)", "巡游出租城市内客运量(万人)", "城市内占比", "网约车城市内客运量(万人)", "网约车城际城乡客运量(万人)"}; |
| | | Row h1 = sh.createRow(1); |
| | | for (int j = 0; j < head1.length; j++) h1.createCell(j).setCellValue(head1[j]); |
| | | int r = 2; |
| | | writeWycRow(sh, r++, "全省", orderSum, pinK, sum(Dtax), sum(Etax), pinK > 0 ? pinCK / pinK : 0, pinCK, pinK - pinCK); |
| | | for (int i = 0; i < n; i++) { |
| | | writeWycRow(sh, r++, cities.get(i), orders[i], C[i], Dtax[i], Etax[i], Dtax[i] > 0 ? Etax[i] / Dtax[i] : 0, H[i], I[i]); |
| | | } |
| | | r += 1; |
| | | Row t2 = sh.createRow(r++); |
| | | t2.createCell(0).setCellValue("旅客周转量拆分(万人公里)"); |
| | | String[] head2 = {"市州", "出租车平均运距(公里)", "出租车城市内平均运距(公里)", "网约车旅客周转量(万人公里)", "网约车城市内旅客周转量(万人公里)", "网约车城际城乡旅客周转量(万人公里)"}; |
| | | Row h2 = sh.createRow(r++); |
| | | for (int j = 0; j < head2.length; j++) h2.createCell(j).setCellValue(head2[j]); |
| | | writeWycRow2(sh, r++, "全省", 0, 0, pinZ, pinCZ, pinZ - pinCZ); |
| | | for (int i = 0; i < n; i++) { |
| | | writeWycRow2(sh, r++, cities.get(i), avgD[i], avgDc[i], Gl[i], Hl[i], Il[i]); |
| | | } |
| | | r += 1; |
| | | String[] notes = { |
| | | "注:1) 客运量 C = 订单占比 × 全省网约车客运量;城市内 H:武汉=自身,其余按 G 剩余占比分摊至全省城市内 pin;城际城乡 = 总量 - 城市内。", |
| | | " 2) 周转量 G = 客运量 × 巡游出租平均运距 的占比 × 全省网约车周转量 pin;城市内周转量同法(用城市内平均运距)。", |
| | | " 3) 输入:订单 = 当月分析报告附件1(17 市州订单数);全省 pin = 网约车总数.xlsx;巡游出租 = 巡游出租月报(系统导入,含平均运距)。", |
| | | " 4) 武汉无城际城乡;数值保留全精度,界面/文件显示两位小数。" |
| | | }; |
| | | for (String nt : notes) { |
| | | Row nr = sh.createRow(r++); |
| | | nr.createCell(0).setCellValue(nt); |
| | | } |
| | | for (int j = 0; j < 8; j++) sh.setColumnWidth(j, 18 * 256); |
| | | return toBytes(wb); |
| | | } |
| | | |
| | | private void writeWycRow(Sheet sh, int r, String name, double ord, double k, double dt, double et, double f, double h, double i) { |
| | | Row row = sh.createRow(r); |
| | | row.createCell(0).setCellValue(name); |
| | | row.createCell(1).setCellValue(ord); |
| | | row.createCell(2).setCellValue(k); |
| | | row.createCell(3).setCellValue(dt); |
| | | row.createCell(4).setCellValue(et); |
| | | row.createCell(5).setCellValue(f); |
| | | row.createCell(6).setCellValue(h); |
| | | row.createCell(7).setCellValue(i); |
| | | } |
| | | |
| | | private void writeWycRow2(Sheet sh, int r, String name, double ad, double adc, double g, double h, double i) { |
| | | Row row = sh.createRow(r); |
| | | row.createCell(0).setCellValue(name); |
| | | row.createCell(1).setCellValue(ad); |
| | | row.createCell(2).setCellValue(adc); |
| | | row.createCell(3).setCellValue(g); |
| | | row.createCell(4).setCellValue(h); |
| | | row.createCell(5).setCellValue(i); |
| | | } |
| | | |
| | | private double sum(double[] a) { |
| | | double s = 0; |
| | | for (double v : a) s += v; |
| | | return s; |
| | | } |
| | | |
| | | /** 读取 docs/城市客运/网约车订单及全省总量.xlsx:按 period(yyyy-MM) 行取 pin(B-E) 与 17 市州订单(G-W) */ |
| | | private Map<String, Object> loadWycMonthlyInput(String period, String mode) throws Exception { |
| | | String target = period; |
| | | File f; |
| | | try { |
| | | f = resolveAnyTemplate(templateDir, "网约车订单及全省总量.xlsx"); |
| | | } catch (Exception e) { |
| | | throw new RuntimeException("未找到《网约车订单及全省总量.xlsx》,请放到 docs/城市客运 目录"); |
| | | } |
| | | try (InputStream in = new FileInputStream(f); |
| | | org.apache.poi.ss.usermodel.Workbook wb = org.apache.poi.ss.usermodel.WorkbookFactory.create(in)) { |
| | | org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(0); |
| | | org.apache.poi.ss.usermodel.DataFormatter df = new org.apache.poi.ss.usermodel.DataFormatter(); |
| | | List<String> cities = RegionUtil.cityList(); |
| | | int n = cities.size(); |
| | | for (int i = 1; i <= sh.getLastRowNum(); i++) { |
| | | Row row = sh.getRow(i); |
| | | if (row == null) continue; |
| | | Cell c0 = row.getCell(0); |
| | | if (c0 == null) continue; |
| | | String m = df.formatCellValue(c0).trim(); |
| | | if (m == null || m.isEmpty()) continue; |
| | | if (m.length() > 7) m = m.substring(0, 7); |
| | | if (!target.equals(m)) continue; |
| | | Map<String, Object> map = new HashMap<>(); |
| | | map.put("pinK", cellNum(row, 1, df, 0.0)); |
| | | map.put("pinCK", cellNum(row, 2, df, 0.0)); |
| | | map.put("pinZ", cellNum(row, 3, df, 0.0)); |
| | | map.put("pinCZ", cellNum(row, 4, df, 0.0)); |
| | | double[] orders = new double[n]; |
| | | double orderSum = 0; |
| | | for (int k = 0; k < n; k++) { |
| | | orders[k] = cellNum(row, 6 + k, df, 0.0); |
| | | orderSum += orders[k]; |
| | | } |
| | | if (orderSum <= 0) { |
| | | orderSum = cellNum(row, 5, df, 0.0); // F 列合计兜底 |
| | | } |
| | | map.put("orders", orders); |
| | | map.put("orderSum", orderSum); |
| | | return map; |
| | | } |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private double cellNum(Row row, int idx, org.apache.poi.ss.usermodel.DataFormatter df, double def) { |
| | | Cell c = row.getCell(idx); |
| | | if (c == null) return def; |
| | | if (c.getCellType() == CellType.NUMERIC) return c.getNumericCellValue(); |
| | | String s = df.formatCellValue(c).replace(",", "").trim(); |
| | | if (s == null || s.isEmpty()) return def; |
| | | try { |
| | | return Double.parseDouble(s); |
| | | } catch (Exception e) { |
| | | return def; |
| | | } |
| | | } |
| | | |
| | | /** 巡游出租月报(DB)按月加载:市州 -> [客运量, 周转量, 城市内客运量, 城市内周转量, 平均运距, 城市内平均运距] */ |
| | | private Map<String, double[]> taxiMonthlyOf(String period) { |
| | | Map<String, double[]> map = new java.util.LinkedHashMap<>(); |
| | | List<CityTaxiMonthly> rows = cityTaxiMapper.selectList( |
| | | new LambdaQueryWrapper<CityTaxiMonthly>().eq(CityTaxiMonthly::getReportPeriod, period)); |
| | | for (CityTaxiMonthly r : rows) { |
| | | String city = RegionUtil.normalizeCityName(r.getCity()); |
| | | if (city == null || city.isEmpty()) city = "未知"; |
| | | double[] a = map.computeIfAbsent(city, k -> new double[6]); |
| | | a[0] += nz(r.getPassengerVolume()); |
| | | a[1] += nz(r.getTurnover()); |
| | | a[2] += nz(r.getPassengerCity()); |
| | | a[3] += nz(r.getTurnoverCity()); |
| | | if (r.getAvgDistance() != null && r.getAvgDistance() > 0) a[4] = r.getAvgDistance(); |
| | | if (r.getAvgDistanceCity() != null && r.getAvgDistanceCity() > 0) a[5] = r.getAvgDistanceCity(); |
| | | } |
| | | for (double[] a : map.values()) { |
| | | if (a[4] <= 0 && a[0] > 0) a[4] = a[1] / a[0]; |
| | | if (a[5] <= 0 && a[2] > 0) a[5] = a[3] / a[2]; |
| | | } |
| | | return map; |
| | | } |
| | | |
| | | /** 定位同期备份母版(清理前的完整月母版),用于把骨架母版缺失的 5-7 月月度块按备份还原; |
| | | * 与 resolveAnyTemplate 相同策略:在配置目录、user.dir 相对目录、上级目录三个候选根中扫描 */ |
| | | private File resolveSummaryDonor(int year, int month) { |
| | | String rel = summaryTemplateDir; |
| | | while (rel.startsWith("./")) rel = rel.substring(2); |
| | | String[] roots = { |
| | | summaryTemplateDir, |
| | | System.getProperty("user.dir") + "/" + rel, |
| | | System.getProperty("user.dir") + "/../" + rel |
| | | }; |
| | | for (String root : roots) { |
| | | if (root == null || root.trim().isEmpty()) continue; |
| | | File dir = new File(root); |
| | | File[] files = dir.listFiles((d, name) -> |
| | | name.startsWith("_备份_") && name.contains(year + "年" + month + "月") && name.contains("道路运输量汇总表")); |
| | | if (files != null && files.length > 0) return files[0]; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** 按同期备份母版还原月度块:凡备份有内容而母版同格缺失/内容不同(清理时被裁掉的 5-7 月值与累计公式)的单元格,整格回填并覆盖 */ |
| | | private int restoreSummaryMonthlyBlocks(XSSFWorkbook wb, XSSFWorkbook donor) { |
| | | int restored = 0; |
| | | for (int i = 0; i < donor.getNumberOfSheets(); i++) { |
| | | Sheet ds = donor.getSheetAt(i); |
| | | if (ds == null) continue; |
| | | Sheet bs = wb.getSheet(ds.getSheetName()); |
| | | if (bs == null) continue; |
| | | // 中口径排名页 A..H 陈旧左块已于 2026-09-09 拍板整块删除:该区域永不从备份母版回填,避免“生成又复活” |
| | | boolean skipMidRankLeftBlock = "中口径排名".equals(ds.getSheetName()); |
| | | // 《 货运》页 2025..2021 年度块(T 列起,含各年累计/累计同比列)在母版里是「市州派生公式」, |
| | | // 备份母版同区仍是旧的写死常量(全省规下等),回填会让 2021/2022/2023 累计又对不上(“生成又复活”),故该区不回填。 |
| | | boolean skipFreightHistoryBlocks = " 货运".equals(ds.getSheetName()); |
| | | for (int r = ds.getFirstRowNum(); r <= ds.getLastRowNum(); r++) { |
| | | Row dr = ds.getRow(r); |
| | | if (dr == null) continue; |
| | | Row br = bs.getRow(r); |
| | | for (Cell dc : dr) { |
| | | if (dc == null) continue; |
| | | if (skipMidRankLeftBlock && dc.getColumnIndex() < 8) continue; |
| | | if (skipFreightHistoryBlocks && dc.getColumnIndex() >= 19) continue; // T 列及以后 = 2025..2021 年度块 |
| | | CellType t = dc.getCellType(); |
| | | if (t == CellType.BLANK) continue; |
| | | String dtx = summaryCellText(dc); |
| | | if (dtx == null || dtx.isEmpty()) continue; |
| | | Cell bc = (br == null) ? null : br.getCell(dc.getColumnIndex()); |
| | | if (bc != null && dtx.equals(summaryCellText(bc))) continue; |
| | | Row tr = (bc != null) ? br : bs.getRow(r); |
| | | if (tr == null) tr = bs.createRow(r); |
| | | Cell tc = (bc != null) ? bc : tr.getCell(dc.getColumnIndex()); |
| | | if (tc == null) tc = tr.createCell(dc.getColumnIndex()); |
| | | tc.setBlank(); |
| | | applyDonorNumberFormat(wb, tc, dc); |
| | | if (t == CellType.FORMULA) { |
| | | String f = dc.getCellFormula(); |
| | | if (f != null) tc.setCellFormula(f); |
| | | } else if (t == CellType.STRING) { |
| | | tc.setCellValue(dc.getStringCellValue()); |
| | | } else if (t == CellType.NUMERIC) { |
| | | tc.setCellValue(dc.getNumericCellValue()); |
| | | } else if (t == CellType.BOOLEAN) { |
| | | tc.setCellValue(dc.getBooleanCellValue()); |
| | | } |
| | | restored++; |
| | | } |
| | | } |
| | | } |
| | | return restored; |
| | | } |
| | | |
| | | /** 还原回填时同步 donor(已做版式统一)的数字格式,保证同比/数值/排名格显示格式与母版一致 */ |
| | | private void applyDonorNumberFormat(XSSFWorkbook wb, Cell dst, Cell src) { |
| | | if (wb == null || dst == null || src == null) return; |
| | | try { |
| | | String fmt = src.getCellStyle().getDataFormatString(); |
| | | if (fmt == null || fmt.isEmpty() || "General".equalsIgnoreCase(fmt)) return; |
| | | org.apache.poi.ss.usermodel.DataFormat df = wb.createDataFormat(); |
| | | short idx = df.getFormat(fmt); |
| | | CellStyle cur = dst.getCellStyle(); |
| | | if (cur != null && idx == cur.getDataFormat()) return; |
| | | CellStyle ns = wb.createCellStyle(); |
| | | if (cur != null) ns.cloneStyleFrom(cur); |
| | | ns.setDataFormat(idx); |
| | | dst.setCellStyle(ns); |
| | | } catch (Exception ignore) { |
| | | // 个别格式异常不阻塞还原 |
| | | } |
| | | } |
| | | /** 单元格内容文本化(公式带 = 前缀),用于跨簿比对 */ |
| | | private String summaryCellText(Cell c) { |
| | | if (c == null) return null; |
| | | switch (c.getCellType()) { |
| | | case FORMULA: { |
| | | String f = c.getCellFormula(); |
| | | return f == null ? "" : "=" + f; |
| | | } |
| | | case STRING: |
| | | return c.getStringCellValue(); |
| | | case NUMERIC: |
| | | return Double.toString(c.getNumericCellValue()); |
| | | case BOOLEAN: |
| | | return Boolean.toString(c.getBooleanCellValue()); |
| | | default: |
| | | return ""; |
| | | } |
| | | } |
| | | |
| | | /** 把 8 个台账/合成页裁剪到 keepMonths 月: |
| | | * 月度值列 2026 年 m 月 = 第 3+2*(m-1) 列(C/E/G/I/K/M/O),同比列为其后一列; |
| | | * 累计 Q 列 = C+E+...+O,累计同比 R 列分母 = 2025 年同月列 T/V/X/Z/AB/AD/AF。 |
| | | * 裁剪 = 数据行(含累计公式且任一 2026 月度列有内容)中 keepMonths+1..7 月的当月值/同比单元格清空(表头保留), |
| | | * 并把该行累计公式去掉对应 +列,累计同比分母去掉对应 2025 列。 */ |
| | | private int trimSummaryMonthlySheets(XSSFWorkbook wb, int keepMonths) { |
| | | String[] scope = {" 货运", "班线包车", "城市客运", "公交", "出租", "网约车", "轨道、轮渡", "中口径明细"}; |
| | | java.util.Set<String> scopeSet = new java.util.HashSet<>(); |
| | | for (String s : scope) scopeSet.add(s); |
| | | int touched = 0; |
| | | for (int i = 0; i < wb.getNumberOfSheets(); i++) { |
| | | Sheet sh = wb.getSheetAt(i); |
| | | if (sh == null || !scopeSet.contains(sh.getSheetName())) continue; |
| | | for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | Cell q = row.getCell(16); // Q 列 = 累计 |
| | | if (q == null || q.getCellType() != CellType.FORMULA) continue; |
| | | boolean hasMonthly = false; |
| | | for (int m = 1; m <= 7 && !hasMonthly; m++) { |
| | | Cell v = row.getCell(2 + 2 * (m - 1)); // C,E,G,I,K,M,O |
| | | if (v != null && v.getCellType() != CellType.BLANK) hasMonthly = true; |
| | | } |
| | | if (!hasMonthly) continue; // 排除页首/页中跨页签校验公式行(无月度值) |
| | | int excelRow = r + 1; // 公式内引用为 Excel 1-based 行号 |
| | | for (int m = keepMonths + 1; m <= 7; m++) { |
| | | int valueCol = 2 + 2 * (m - 1); // 2026 当月值列 |
| | | int yoyCol = valueCol + 1; // 当月同比列 |
| | | String vLetter = colLetter(valueCol); |
| | | Cell vc = row.getCell(valueCol); |
| | | if (vc != null) { |
| | | vc.setBlank(); |
| | | touched++; |
| | | } |
| | | Cell yc = row.getCell(yoyCol); |
| | | if (yc != null) { |
| | | yc.setBlank(); |
| | | touched++; |
| | | } |
| | | String fq = q.getCellFormula(); |
| | | if (fq != null && fq.contains("+" + vLetter + excelRow)) { |
| | | q.setCellFormula(fq.replace("+" + vLetter + excelRow, "")); |
| | | touched++; |
| | | } |
| | | Cell rc = row.getCell(17); // R 列 = 累计同比 |
| | | if (rc != null && rc.getCellType() == CellType.FORMULA) { |
| | | String fr = rc.getCellFormula(); |
| | | int lastCol = 19 + 2 * (m - 1); // 2025 同月列 T..AF(0-based:T=19,AF=31) |
| | | String lLetter = colLetter(lastCol); |
| | | String txt = fr; |
| | | if (txt != null && txt.contains("," + lLetter + excelRow)) { |
| | | txt = txt.replace("," + lLetter + excelRow, ""); |
| | | touched++; |
| | | } |
| | | if (txt != null && txt.contains("+" + lLetter + excelRow)) { |
| | | txt = txt.replace("+" + lLetter + excelRow, ""); |
| | | touched++; |
| | | } |
| | | if (txt != null && !txt.equals(fr)) rc.setCellFormula(txt); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return touched; |
| | | } |
| | | |
| | | /** 汇总工作簿年份动态化:仅把 2026年 前缀替换为目标年(2025/2024 参考列不动) */ |
| | | private void dynamicSummaryYear(XSSFWorkbook wb, int year) { |
| | | String oldPrefix = "2026年"; |
| | | String newPrefix = year + "年"; |
| | | if (oldPrefix.equals(newPrefix)) return; |
| | | for (int i = 0; i < wb.getNumberOfSheets(); i++) { |
| | | Sheet sh = wb.getSheetAt(i); |
| | | if (sh == null) continue; |
| | | for (Row row : sh) { |
| | | if (row == null) continue; |
| | | for (Cell c : row) { |
| | | if (c == null || c.getCellType() != CellType.STRING) continue; |
| | | String v = c.getStringCellValue(); |
| | | if (v != null && v.contains(oldPrefix)) { |
| | | c.setCellValue(v.replace(oldPrefix, newPrefix)); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 生成_投资报表系列.zip:预安排 + 十五五 + 亿元 + 经济强县 */ |
| | |
| | | case "passengerMidDetail": return exportPassengerMidDetail(period, mode); |
| | | case "passengerMidRank": return exportPassengerMidRank(period, mode); |
| | | case "passengerMidAnalysis": return exportPassengerMidAnalysis(period, mode); |
| | | case "summaryWorkbook": return exportSummaryWorkbook(period, mode); |
| | | case "energySummary": return exportEnergySummary(period); |
| | | case "investPlan": return investPlan(period, mode); |
| | | case "investFiveYearLogistics": return investFiveYearLogistics(period, mode); |
| | | case "investBillion": return investBillion(period, mode); |
| | | case "investCounty": return investCounty(period, mode); |
| | | case "investPassengerDetail": return exportInvestPassengerDetail(period, mode); |
| | | case "investLogisticsDetail": return exportInvestLogisticsDetail(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 "cityPassengerCitySum": return exportCityPassengerCitySum("生成_城市客运客运量分市州累计.xlsx", period, mode); |
| | | case "cityPassengerDetailSum": return exportCityPassengerCitySum("生成_城市客运各市州明细表.xlsx", period, mode); |
| | | case "cityPassengerSummary": return exportCityPassengerSummary(period, mode); |
| | | case "wycSplit": return exportWycSplit(period, mode); |
| | | default: return null; |
| | | } |
| | | } |
| | |
| | | REPORT_FILE_NAMES.put("investFiveYearLogistics", "生成_十五五规划物流项目进展情况.xls"); |
| | | REPORT_FILE_NAMES.put("investBillion", "生成_湖北省(客货站场)亿元投资项目.xls"); |
| | | REPORT_FILE_NAMES.put("investCounty", "生成_经济强县交通物流基础设施投资统计报表.xls"); |
| | | REPORT_FILE_NAMES.put("investPassengerDetail", "生成_客运站投资明细表.xlsx"); |
| | | REPORT_FILE_NAMES.put("investLogisticsDetail", "生成_物流站场投资明细.xls"); |
| | | REPORT_FILE_NAMES.put("cityBusDetail", "生成_城市公交客运量分市州明细.xlsx"); |
| | | REPORT_FILE_NAMES.put("cityTaxiDetail", "生成_巡游出租客运量分市州明细.xlsx"); |
| | | REPORT_FILE_NAMES.put("cityRailFerryDetail", "生成_轨道轮渡客运量分市州明细.xlsx"); |
| | | REPORT_FILE_NAMES.put("cityPassengerCitySum", "生成_城市客运客运量分市州累计.xlsx"); |
| | | REPORT_FILE_NAMES.put("cityPassengerDetailSum", "生成_城市客运各市州明细表.xlsx"); |
| | | REPORT_FILE_NAMES.put("cityPassengerSummary", "生成_城市客运汇总.xlsx"); |
| | | REPORT_FILE_NAMES.put("summaryWorkbook", "生成_道路运输量汇总表.xlsx"); |
| | | REPORT_FILE_NAMES.put("wycSplit", "生成_网约车客运量拆分表.xlsx"); |
| | | } |
| | | |
| | | private void addZipEntry(java.util.zip.ZipOutputStream zos, String name, byte[] data) throws Exception { |
| | |
| | | Row row = sheet.getRow(rowIdx); |
| | | if (row == null) row = sheet.createRow(rowIdx); |
| | | if (a > 0) { |
| | | // 2026年 1..N 月:库内有值则填(2位小数),无值清空模板样例 |
| | | // 2026年 1..N 月:库内有值则填(全精度写入,两位小数由单元格格式展示),无值清空模板样例 |
| | | 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)); |
| | | if (v == 0) c.setBlank(); else c.setCellValue(v); |
| | | } |
| | | // 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)); |
| | | if (v == 0) c.setBlank(); else c.setCellValue(v); |
| | | } |
| | | } |
| | | rowIdx++; |
| | |
| | | } catch (Exception e) { |
| | | log.warn("城市客运模板公式求值失败: {}", e.getMessage()); |
| | | } |
| | | clearFormulaErrorCells(sheet, 4, sheet.getLastRowNum(), 3, 39); |
| | | fixCityMonthHeaderText(sheet); |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | /** 清空求值结果为错误的公式单元格(模板跨簿同比公式残留 #REF! 时移除公式,避免导出显示错误) */ |
| | | private void clearFormulaErrorCells(Sheet sheet, int startRow, int endRow, int startCol, int endCol) { |
| | | for (int r = startRow; r <= endRow; r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | for (int c = startCol; c <= endCol; c++) { |
| | | Cell cell = row.getCell(c); |
| | | if (cell == null || cell.getCellType() != CellType.FORMULA) continue; |
| | | try { |
| | | if (cell.getCachedFormulaResultType() == CellType.ERROR) cell.setBlank(); |
| | | } catch (Exception ignore) { |
| | | // 缓存类型读取失败时忽略 |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 清空工作簿全部工作表内求值结果为错误的公式单元格(跨簿/跨表 #REF!、缺去年数据 #DIV/0! 等) */ |
| | | private void clearFormulaErrorsAll(XSSFWorkbook wb) { |
| | | for (int i = 0; i < wb.getNumberOfSheets(); i++) { |
| | | Sheet s = wb.getSheetAt(i); |
| | | clearFormulaErrorCells(s, 2, s.getLastRowNum(), 1, 60); |
| | | } |
| | | } |
| | | |
| | | /** 城市客运类模板表头月份列若为未格式化日期序列号(如 46082),改写为「YYYY年M月」文本 */ |
| | | private void fixCityMonthHeaderText(Sheet sheet) { |
| | | for (int r = 0; r <= Math.min(sheet.getLastRowNum(), 20); r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | Cell nameCell = row.getCell(0); |
| | | String nameText = nameCell == null ? null : cellText(nameCell); |
| | | if (nameText == null || !nameText.contains("地区")) continue; |
| | | for (int c = 2; c <= 40; c += 2) { |
| | | Cell cell = row.getCell(c); |
| | | if (cell == null || cell.getCellType() != CellType.NUMERIC) continue; |
| | | double v = cell.getNumericCellValue(); |
| | | if (v < 40000 || v > 60000) continue; |
| | | java.util.Date d = org.apache.poi.ss.usermodel.DateUtil.getJavaDate(v); |
| | | cell.setCellValue(new java.text.SimpleDateFormat("yyyy年M月").format(d)); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | private byte[] toBytesHssf(HSSFWorkbook wb) throws Exception { |
| | | applyTwoDecimalFormat(wb); |
| | | try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { |
| | | wb.write(out); |
| | | wb.close(); |
| | |
| | | } |
| | | |
| | | /** 表1 项目行填充(模板行/插入行共用;cityText 与模板 B 列格式一致) */ |
| | | private void fillInvestPlanProjectRow(HSSFRow row, InvestmentProject p, InvestmentMonthly m, int seq, String cityText) { |
| | | 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, p.getProjectName()); |
| | | 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())); |
| | |
| | | // 原位逐行替换:合计/块标题/市州小节/分组/项目行;未匹配的模板行清空样例数据(结构/合并/列宽不变) |
| | | 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; |
| | |
| | | hssfText(row, 0, "合计"); |
| | | InvestAgg prov = new InvestAgg(); |
| | | for (InvestmentMonthly m : monthlies) prov.add(m, pmap.get(m.getProjectId())); |
| | | addProjectsWithoutMonthly(prov, projects, mByProject, null, null); |
| | | writeInvestAggHssf(row, prov); |
| | | continue; |
| | | } |
| | |
| | | InvestmentProject p = pmap.get(m.getProjectId()); |
| | | if (p != null && blockCategory.equals(p.getCategory())) agg.add(m, p); |
| | | } |
| | | addProjectsWithoutMonthly(agg, projects, mByProject, blockCategory, null); |
| | | writeInvestAggHssf(row, agg); |
| | | seq = 0; |
| | | continue; |
| | |
| | | if (p != null && city.equals(p.getCity()) |
| | | && (blockCategory == null || blockCategory.equals(p.getCategory()))) agg.add(m, p); |
| | | } |
| | | addProjectsWithoutMonthly(agg, projects, mByProject, blockCategory, city); |
| | | writeInvestAggHssf(row, agg); |
| | | continue; |
| | | } |
| | | // 项目行:按 模板名称+市州+块类别 匹配 DB 项目,未匹配整行清空 |
| | | blankInvestRow(row, 25); |
| | | if (blockCategory == null) continue; // 老旧货车段无数据源 |
| | | InvestmentProject p = matchInvest(projects, d, b, blockCategory); |
| | | 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); |
| | | fillInvestPlanProjectRow(row, p, m, ++seq, b, d); |
| | | } |
| | | // 动态插行:DB 项目数 > 模板预留行数时,在市州小节末尾补齐(自底向上插行,避免行号错位) |
| | | for (int i = sections.size() - 1; i >= 0; i--) { |
| | |
| | | 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()); |
| | | fillInvestPlanProjectRow(row, p, mByProject.get(p.getId()), 0, p.getCity(), p.getProjectName()); |
| | | } |
| | | } |
| | | renumberInvestSeq(sheet, "investPlan"); |
| | |
| | | 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)); |
| | | if (agg.yearPlan > 0) hssfNum(row, 23, agg.yearCum / agg.yearPlan); |
| | | } |
| | | |
| | | /** |
| | | * 补计「本期没有月度记录、但项目主档有总投资」的项目(只计总投资这一列)。 |
| | | * 场景:某市州本月未报送单表(如武汉 8 月物流),主档里项目仍在,若不补计,该市州小计/合计会整块为空。 |
| | | * |
| | | * @param category 块类别(客运站场/物流园区);null 表示不限 |
| | | * @param city 市州全称(如 武汉市);null 表示不限 |
| | | */ |
| | | private void addProjectsWithoutMonthly(InvestAgg agg, List<InvestmentProject> projects, |
| | | Map<Long, InvestmentMonthly> mByProject, |
| | | String category, String city) { |
| | | for (InvestmentProject p : projects) { |
| | | if (p == null || mByProject.containsKey(p.getId())) continue; |
| | | if (category != null && !category.equals(p.getCategory())) continue; |
| | | if (city != null && !city.equals(p.getCity())) continue; |
| | | if (p.getTotalInvestment() != null) agg.total += p.getTotalInvestment(); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | // ==================== 投资模块:客运站投资明细表 / 物流站场投资明细(模板底稿原位替换) ==================== |
| | | |
| | | /** 物流站场明细表模板中的市州小节短名 -> 库内规范全名 */ |
| | | private static final Map<String, String> LOGI_CITY_LABELS = new LinkedHashMap<>(); |
| | | static { |
| | | LOGI_CITY_LABELS.put("武汉", "武汉市"); |
| | | LOGI_CITY_LABELS.put("黄石", "黄石市"); |
| | | LOGI_CITY_LABELS.put("十堰", "十堰市"); |
| | | LOGI_CITY_LABELS.put("宜昌", "宜昌市"); |
| | | LOGI_CITY_LABELS.put("襄阳", "襄阳市"); |
| | | LOGI_CITY_LABELS.put("鄂州", "鄂州市"); |
| | | LOGI_CITY_LABELS.put("荆门", "荆门市"); |
| | | LOGI_CITY_LABELS.put("孝感", "孝感市"); |
| | | LOGI_CITY_LABELS.put("荆州", "荆州市"); |
| | | LOGI_CITY_LABELS.put("黄冈", "黄冈市"); |
| | | LOGI_CITY_LABELS.put("咸宁", "咸宁市"); |
| | | LOGI_CITY_LABELS.put("随州", "随州市"); |
| | | LOGI_CITY_LABELS.put("恩施", "恩施州"); |
| | | LOGI_CITY_LABELS.put("仙桃", "仙桃市"); |
| | | LOGI_CITY_LABELS.put("潜江", "潜江市"); |
| | | LOGI_CITY_LABELS.put("天门", "天门市"); |
| | | LOGI_CITY_LABELS.put("林区", "神农架林区"); |
| | | } |
| | | |
| | | /** 该期各项目月度数据:projectId -> InvestmentMonthly */ |
| | | private Map<Long, InvestmentMonthly> investMonthlyOf(String period) { |
| | | Map<Long, InvestmentMonthly> map = new HashMap<>(); |
| | | for (InvestmentMonthly m : investMonthlyMapper.selectList( |
| | | new LambdaQueryWrapper<InvestmentMonthly>().eq(InvestmentMonthly::getReportPeriod, period))) { |
| | | map.put(m.getProjectId(), m); |
| | | } |
| | | return map; |
| | | } |
| | | |
| | | /** 写单元格数值(保留样式/合并):null 清空 */ |
| | | private void putNum(Row row, int idx, Double v) { |
| | | if (row == null) return; |
| | | Cell c = row.getCell(idx); |
| | | if (c == null) c = row.createCell(idx); |
| | | c.setBlank(); |
| | | if (v != null) c.setCellValue(v); |
| | | } |
| | | |
| | | /** 写单元格文本(保留样式/合并):空串清空 */ |
| | | private void putText(Row row, int idx, String v) { |
| | | if (row == null) return; |
| | | Cell c = row.getCell(idx); |
| | | if (c == null) c = row.createCell(idx); |
| | | c.setBlank(); |
| | | if (v != null && !v.trim().isEmpty()) c.setCellValue(v.trim()); |
| | | } |
| | | |
| | | /** 读单元格数值(空/非数值/公式无缓存一律 0) */ |
| | | private double numOf(Cell c) { |
| | | if (c == null) return 0.0; |
| | | try { |
| | | if (c.getCellType() == CellType.NUMERIC || c.getCellType() == CellType.FORMULA) { |
| | | return c.getNumericCellValue(); |
| | | } |
| | | String s = cellText(c); |
| | | if (s == null) return 0.0; |
| | | s = s.replace(",", "").trim(); |
| | | return s.isEmpty() ? 0.0 : Double.parseDouble(s); |
| | | } catch (Exception e) { |
| | | return 0.0; |
| | | } |
| | | } |
| | | |
| | | /** 模板标题里的「(N月月报)」「(N月)」改为报表期月份 */ |
| | | private void fixInvestMonthTitle(Sheet sheet, int rowIdx, int colIdx, int month) { |
| | | Row row = sheet.getRow(rowIdx); |
| | | if (row == null) return; |
| | | String t = cellText(row.getCell(colIdx)); |
| | | if (t == null) return; |
| | | String nv = t.replaceAll("(\\d+月月报)", "(" + month + "月月报)") |
| | | .replaceAll("(\\d+月)", "(" + month + "月)"); |
| | | if (!nv.equals(t)) putText(row, colIdx, nv); |
| | | } |
| | | |
| | | /** 模板行 A 列是否为「一二三四、」渠道分组标题 */ |
| | | private boolean isInvestChannelLabel(String a) { |
| | | return a != null && a.matches("^[一二三四五六七八九十]+、.*"); |
| | | } |
| | | |
| | | /** |
| | | * 生成_X月客运站投资明细表.xlsx(模板:docs/投资/模板/模板_X月客运站投资明细表.xlsx,sheet:明细 + 汇总)。 |
| | | * 口径:模板即上月台账底稿;本月只覆盖「库内有该期数据」的项目行 |
| | | * (H自开始建设累计 / I本年计划 / J自年初累计 / K本月完成 / L建设阶段 / M形象进度), |
| | | * 库内无该期的项目行保留母版原值(等同人工「上月结转 + 本月更新」); |
| | | * 城市/渠道/合计行沿用模板 SUM 公式,随表内数值自动重算。 |
| | | */ |
| | | public byte[] exportInvestPassengerDetail(String period, String mode) throws Exception { |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, mode); |
| | | List<InvestmentProject> projects = investProjectMapper.selectList( |
| | | new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, "客运站场")); |
| | | Map<Long, InvestmentMonthly> mByProject = investMonthlyOf(period); |
| | | File tpl = resolveAnyTemplate(investTemplateDir, "模板_X月客运站投资明细表.xlsx"); |
| | | try (InputStream in = new FileInputStream(tpl); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheet("明细"); |
| | | if (sheet == null) throw new RuntimeException("模板《模板_X月客运站投资明细表.xlsx》缺少「明细」工作表"); |
| | | fixInvestMonthTitle(sheet, 0, 1, month); |
| | | String curCity = null; |
| | | Set<Long> used = new HashSet<>(); |
| | | int filled = 0, kept = 0, dup = 0; |
| | | for (int r = 8; r <= sheet.getLastRowNum(); r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | String b = nvl(cellText(row.getCell(1))); |
| | | if (isCityLabel(a)) { curCity = templateCityFull(a); continue; } |
| | | if (isInvestChannelLabel(a)) continue; |
| | | if (a.contains("合计") || a.contains("总计")) continue; |
| | | if (!a.matches("\\d+(\\.\\d+)?")) continue; |
| | | if (b.isEmpty() || curCity == null) continue; |
| | | InvestmentProject p = matchInvest(projects, b, curCity, "客运站场", used); |
| | | InvestmentMonthly m = p == null ? null : mByProject.get(p.getId()); |
| | | if (p == null || m == null) { |
| | | kept++; |
| | | // 有库内项目但当月无数据 → 记为待核对;无匹配项目 → 结转上月 |
| | | if (p != null) dup++; |
| | | continue; |
| | | } |
| | | used.add(p.getId()); |
| | | // 口径(用户 2026-09-18 确认):只覆盖人工会随月报更新的四列—— |
| | | // H 自开始建设累计 / J 自年初累计 / K 本月完成 / M 形象进度; |
| | | // I 本年计划投资、L 建设阶段、G 计划总投资为省定/母版口径,一律不动;库内为空则保留母版原值 |
| | | if (m.getStartCum() != null) putNum(row, 7, m.getStartCum()); |
| | | if (m.getYearCum() != null) putNum(row, 9, m.getYearCum()); |
| | | if (m.getMonthDone() != null) putNum(row, 10, m.getMonthDone()); |
| | | if (m.getProgressDesc() != null && !m.getProgressDesc().trim().isEmpty()) putText(row, 12, m.getProgressDesc()); |
| | | filled++; |
| | | } |
| | | Sheet sumSheet = wb.getSheet("汇总"); |
| | | if (sumSheet != null) { |
| | | Row r2 = sumSheet.getRow(1); |
| | | if (r2 != null) { |
| | | Cell c = r2.getCell(0); |
| | | if (c == null) c = r2.createCell(0); |
| | | c.setBlank(); |
| | | c.setCellValue(java.time.LocalDate.of(year, month, 1).atStartOfDay()); |
| | | } |
| | | } |
| | | try { |
| | | wb.getCreationHelper().createFormulaEvaluator().evaluateAll(); |
| | | } catch (Exception e) { |
| | | log.warn("客运站投资明细表公式求值失败: {}", e.getMessage()); |
| | | } |
| | | log.info("客运站投资明细表 {}:写入 {} 行,结转母版原值 {} 行(其中库内有项目但当月无数据 {} 行)", |
| | | period, filled, kept, dup); |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 生成_X月物流站场投资明细.xls(模板:docs/投资/模板/模板_X月物流站场投资明细.xls)。 |
| | | * 覆盖「 分项目投资完成情况」项目行(口径同客运站明细表),并按表内数值重算 |
| | | * 渠道行 / 市州行 / 合计行,同步刷新「分市州规划内外项目完成情况」「市州汇总」两张汇总页。 |
| | | */ |
| | | public byte[] exportInvestLogisticsDetail(String period, String mode) throws Exception { |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, mode); |
| | | List<InvestmentProject> projects = investProjectMapper.selectList( |
| | | new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, "物流园区")); |
| | | Map<Long, InvestmentMonthly> mByProject = investMonthlyOf(period); |
| | | HSSFWorkbook wb = loadInvestTemplate("模板_X月物流站场投资明细.xls"); |
| | | HSSFSheet sheet = wb.getSheet(" 分项目投资完成情况"); |
| | | if (sheet == null) throw new RuntimeException("模板《模板_X月物流站场投资明细.xls》缺少「 分项目投资完成情况」工作表"); |
| | | fixInvestMonthTitle(sheet, 0, 0, month); |
| | | Row head = sheet.getRow(3); |
| | | if (head != null) { |
| | | String t = cellText(head.getCell(0)); |
| | | if (t != null) { |
| | | String nv = t.replaceAll("\\d{4}年\\d+月", year + "年" + month + "月"); |
| | | if (!nv.equals(t)) putText(head, 0, nv); |
| | | } |
| | | } |
| | | // 1) 结构扫描:市州小节行 |
| | | List<Integer> cityRows = new ArrayList<>(); |
| | | for (int r = 7; r <= sheet.getLastRowNum(); 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 (LOGI_CITY_LABELS.containsKey(a) && b.isEmpty()) cityRows.add(r); |
| | | } |
| | | Map<String, double[]> citySums = new LinkedHashMap<>(); // 全名 -> G,H,I,J,K |
| | | Map<String, double[]> cityStats = new LinkedHashMap<>(); // 全名 -> {总数, 累计, 规划内数, 规划内累计, 规划外数, 规划外累计} |
| | | Set<Long> used = new HashSet<>(); |
| | | int filled = 0, kept = 0; |
| | | for (int i = 0; i < cityRows.size(); i++) { |
| | | int cr = cityRows.get(i); |
| | | String cityLabel = nvl(cellText(sheet.getRow(cr).getCell(0))); |
| | | String city = LOGI_CITY_LABELS.getOrDefault(cityLabel, cityLabel); |
| | | int start = cr + 1; |
| | | int end = (i + 1 < cityRows.size() ? cityRows.get(i + 1) : sheet.getLastRowNum() + 1) - 1; |
| | | List<Integer> chRows = new ArrayList<>(); |
| | | for (int r = start; r <= end; r++) { |
| | | HSSFRow row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | if (isInvestChannelLabel(nvl(cellText(row.getCell(0))))) chRows.add(r); |
| | | } |
| | | // (1) 先按项目名+市州 覆盖有库内当期数据的项目行 |
| | | for (int r = start; r <= end; 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 (isInvestChannelLabel(a)) continue; |
| | | if (!a.matches("\\d+(\\.\\d+)?")) continue; |
| | | if (b.isEmpty()) continue; |
| | | InvestmentProject p = matchInvest(projects, b, city, "物流园区", used); |
| | | InvestmentMonthly m = p == null ? null : mByProject.get(p.getId()); |
| | | if (p == null || m == null) { kept++; continue; } |
| | | used.add(p.getId()); |
| | | // 口径同客运站明细表:只覆盖 H 自开始建设累计 / J 自年初累计 / K 本月完成 / M 形象进度 |
| | | if (m.getStartCum() != null) putNum(row, 7, m.getStartCum()); |
| | | if (m.getYearCum() != null) putNum(row, 9, m.getYearCum()); |
| | | if (m.getMonthDone() != null) putNum(row, 10, m.getMonthDone()); |
| | | if (m.getProgressDesc() != null && !m.getProgressDesc().trim().isEmpty()) putText(row, 12, m.getProgressDesc()); |
| | | filled++; |
| | | } |
| | | // (2) 再按渠道块汇总,并写回渠道行 / 市州行 |
| | | double[] citySum = new double[5]; |
| | | double[] stat = new double[6]; |
| | | for (int j = 0; j < chRows.size(); j++) { |
| | | int r0 = chRows.get(j); |
| | | int r1 = (j + 1 < chRows.size() ? chRows.get(j + 1) : end + 1) - 1; |
| | | boolean planned = !nvl(cellText(sheet.getRow(r0).getCell(0))).startsWith("四、"); |
| | | double[] s = new double[5]; |
| | | for (int r = r0 + 1; r <= r1; 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 (isInvestChannelLabel(a) || !a.matches("\\d+(\\.\\d+)?")) continue; |
| | | if (b.isEmpty()) continue; |
| | | for (int k = 0; k < 5; k++) s[k] += numOf(row.getCell(6 + k)); |
| | | stat[0] += 1; |
| | | stat[1] += numOf(row.getCell(9)); |
| | | if (planned) { stat[2] += 1; stat[3] += numOf(row.getCell(9)); } |
| | | else { stat[4] += 1; stat[5] += numOf(row.getCell(9)); } |
| | | } |
| | | HSSFRow crow = sheet.getRow(r0); |
| | | for (int k = 0; k < 5; k++) putNum(crow, 6 + k, s[k] == 0 ? null : s[k]); |
| | | for (int k = 0; k < 5; k++) citySum[k] += s[k]; |
| | | } |
| | | // 渠道块之外的游离项目行(模板未分组时)也计入市州合计 |
| | | for (int r = start; r <= end; 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 (isInvestChannelLabel(a) || !a.matches("\\d+(\\.\\d+)?")) continue; |
| | | if (b.isEmpty()) continue; |
| | | boolean inChannel = false; |
| | | for (int j = 0; j < chRows.size(); j++) { |
| | | int r0 = chRows.get(j); |
| | | int r1 = (j + 1 < chRows.size() ? chRows.get(j + 1) : end + 1) - 1; |
| | | if (r > r0 && r <= r1) { inChannel = true; break; } |
| | | } |
| | | if (inChannel) continue; |
| | | for (int k = 0; k < 5; k++) citySum[k] += numOf(row.getCell(6 + k)); |
| | | stat[0] += 1; |
| | | stat[1] += numOf(row.getCell(9)); |
| | | } |
| | | HSSFRow cityRow = sheet.getRow(cr); |
| | | for (int k = 0; k < 5; k++) putNum(cityRow, 6 + k, citySum[k] == 0 ? null : citySum[k]); |
| | | citySums.put(city, citySum); |
| | | cityStats.put(city, stat); |
| | | } |
| | | // 2) 合计行(第 7 行) |
| | | double[] total = new double[5]; |
| | | for (double[] s : citySums.values()) { |
| | | for (int k = 0; k < 5; k++) total[k] += s[k]; |
| | | } |
| | | for (int k = 0; k < 5; k++) putNum(sheet.getRow(6), 6 + k, total[k] == 0 ? null : total[k]); |
| | | |
| | | // 3) 分市州规划内外项目完成情况 |
| | | HSSFSheet citySheet = wb.getSheet("分市州规划内外项目完成情况"); |
| | | if (citySheet != null) { |
| | | Row t1 = citySheet.getRow(0); |
| | | if (t1 != null) { |
| | | String t = cellText(t1.getCell(0)); |
| | | if (t != null) { |
| | | String nv = t.replaceAll("\\d{4}年", year + "年"); |
| | | if (!nv.equals(t)) putText(t1, 0, nv); |
| | | } |
| | | } |
| | | double[] st = new double[6]; |
| | | for (int r = 3; r <= citySheet.getLastRowNum(); r++) { |
| | | HSSFRow row = citySheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | String b = nvl(cellText(row.getCell(1))); |
| | | if (a.contains("合计") || b.contains("合计")) { |
| | | for (int k = 0; k < 6; k++) putNum(row, 2 + k, st[k] == 0 ? null : st[k]); |
| | | continue; |
| | | } |
| | | double[] s = cityStats.get(LOGI_CITY_LABELS.getOrDefault(b, b)); |
| | | if (s == null) continue; |
| | | for (int k = 0; k < 6; k++) { |
| | | putNum(row, 2 + k, s[k] == 0 ? null : s[k]); |
| | | st[k] += s[k]; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 4) 市州汇总(B 年度计划投资为官方口径,保持模板原值;C/D/E 按分项目页重算) |
| | | HSSFSheet sumSheet = wb.getSheet("市州汇总"); |
| | | if (sumSheet != null) { |
| | | HSSFRow r2 = sumSheet.getRow(1); |
| | | if (r2 != null) { |
| | | putNum(r2, 0, (double) org.apache.poi.ss.usermodel.DateUtil.getExcelDate( |
| | | java.time.LocalDate.of(year, month, 1).atStartOfDay())); |
| | | } |
| | | double cT = 0.0, dT = 0.0; |
| | | for (int r = 7; r <= 23; r++) { |
| | | HSSFRow row = sumSheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | double[] s = citySums.get(LOGI_CITY_LABELS.getOrDefault(a, a)); |
| | | if (s == null) continue; |
| | | putNum(row, 2, s[3] == 0 ? null : s[3]); |
| | | putNum(row, 3, s[4] == 0 ? null : s[4]); |
| | | double b = numOf(row.getCell(1)); |
| | | putNum(row, 4, b > 0 ? s[3] / b * 100 : null); |
| | | cT += s[3]; |
| | | dT += s[4]; |
| | | } |
| | | HSSFRow tot = sumSheet.getRow(6); |
| | | if (tot != null) { |
| | | putNum(tot, 2, cT == 0 ? null : cT); |
| | | putNum(tot, 3, dT == 0 ? null : dT); |
| | | double b = numOf(tot.getCell(1)); |
| | | putNum(tot, 4, b > 0 ? cT / b * 100 : null); |
| | | } |
| | | } |
| | | try { |
| | | wb.getCreationHelper().createFormulaEvaluator().evaluateAll(); |
| | | } catch (Exception e) { |
| | | log.warn("物流站场投资明细公式求值失败: {}", e.getMessage()); |
| | | } |
| | | log.info("物流站场投资明细 {}:写入 {} 行,结转母版原值 {} 行", period, filled, kept); |
| | | return toBytesHssf(wb); |
| | | } |
| | | |
| | | private static class InvestAgg { |
| | | double total, startCum, yearPlan, yearCum; |
| | |
| | | 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)); |
| | | hssfNum(rowOther, 5, provinceYearPlan == 0 ? null : provinceYearPlan / 10000); |
| | | // ---- sheet2 规上项目表:表头年份 + 合计 + 项目行 ---- |
| | | HSSFSheet s2 = wb.getSheetAt(1); |
| | | hssfText(s2.getRow(0), 0, year + "年交通建设项目储备情况(规模以上项目)"); |
| | |
| | | |
| | | /** 在候选项目池中按名称匹配 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 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); |
| | |
| | | private double nz(Double v) { |
| | | return v == null ? 0.0 : v; |
| | | } |
| | | /** 数值格式里 3 位及以上的小数部分(如 0.0000 / 0.000_ ) */ |
| | | private static final java.util.regex.Pattern DEC_MORE_THAN_TWO = java.util.regex.Pattern.compile("\\.0{3,}"); |
| | | /** 生成表数值列统一样式:保留全精度数值,显示两位小数;整数值与 %/日期/科学计数等既有样式不改变 */ |
| | | private void applyTwoDecimalFormat(org.apache.poi.ss.usermodel.Workbook wb) { |
| | | if (wb == null) return; |
| | | org.apache.poi.ss.usermodel.DataFormat df = wb.createDataFormat(); |
| | | short fmtTwo = df.getFormat("0.00"); |
| | | for (int s = 0; s < wb.getNumberOfSheets(); s++) { |
| | | org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(s); |
| | | if (sh == null) continue; |
| | | for (org.apache.poi.ss.usermodel.Row row : sh) { |
| | | if (row == null) continue; |
| | | for (org.apache.poi.ss.usermodel.Cell cell : row) { |
| | | if (cell == null) continue; |
| | | try { |
| | | CellType ct = cell.getCellType(); |
| | | if (ct != CellType.NUMERIC && ct != CellType.FORMULA) continue; |
| | | if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) continue; |
| | | org.apache.poi.ss.usermodel.CellStyle cs = cell.getCellStyle(); |
| | | if (cs == null) continue; |
| | | String f = cs.getDataFormatString(); |
| | | if (f == null) f = ""; |
| | | if (f.contains("%") || f.contains("E") || f.contains("@")) continue; |
| | | String normalized = null; |
| | | if (DEC_MORE_THAN_TWO.matcher(f).find()) { |
| | | // 母版个别格带 3~4 位小数格式(如 0.000_ 、0.0000_);[Red]\(0.0000\)):数值仍全精度, |
| | | // 展示统一收敛到两位。这类格本就按数值格式展示,故不受「整数不显示 .00」保护影响。 |
| | | normalized = DEC_MORE_THAN_TWO.matcher(f).replaceAll(".00"); |
| | | } else { |
| | | boolean integral = false; |
| | | if (ct == CellType.NUMERIC) { |
| | | integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue(); |
| | | } else if (cell.getCachedFormulaResultType() == CellType.NUMERIC) { |
| | | integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue(); |
| | | } |
| | | if (integral) continue; // 整数不显示 .00(含公式结果为整数的格) |
| | | if (f.contains("0.00")) continue; |
| | | boolean plain = f.isEmpty() || "General".equals(f) || "general".equals(f) |
| | | || "0".equals(f) || "#,##0".equals(f) || "0.0".equals(f) || "#,##0.0".equals(f); |
| | | if (!plain) continue; |
| | | } |
| | | org.apache.poi.ss.usermodel.CellStyle ns = wb.createCellStyle(); |
| | | ns.cloneStyleFrom(cs); |
| | | ns.setDataFormat(normalized == null ? fmtTwo : df.getFormat(normalized)); |
| | | cell.setCellStyle(ns); |
| | | } catch (Exception ignore) { |
| | | // 个别单元格格式异常不阻塞导出 |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 汇总表导出后按内容自动加宽列宽(只加宽不缩窄;跳过合并单元格标题,防长标题把单列撑爆) */ |
| | | /** 汇总大表输出前清理母版残留下来的筛选与行隐藏:筛选箭头/隐藏行会让整页看起来“缺数据” */ |
| | | private void cleanSummarySheetPresentation(org.apache.poi.ss.usermodel.Workbook wb) { |
| | | if (wb == null) return; |
| | | for (int s = 0; s < wb.getNumberOfSheets(); s++) { |
| | | org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(s); |
| | | if (sh == null) continue; |
| | | if (sh instanceof org.apache.poi.xssf.usermodel.XSSFSheet) { |
| | | org.apache.poi.xssf.usermodel.XSSFSheet xs = (org.apache.poi.xssf.usermodel.XSSFSheet) sh; |
| | | if (xs.getCTWorksheet().isSetAutoFilter()) xs.getCTWorksheet().unsetAutoFilter(); |
| | | for (org.apache.poi.ss.usermodel.Row row : xs) { |
| | | if (row == null) continue; |
| | | org.apache.poi.xssf.usermodel.XSSFRow xr = (org.apache.poi.xssf.usermodel.XSSFRow) row; |
| | | if (xr.getCTRow().isSetHidden()) xr.getCTRow().unsetHidden(); |
| | | } |
| | | } else { |
| | | sh.setAutoFilter(null); |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | private byte[] toBytes(XSSFWorkbook wb) throws Exception { |
| | | applyTwoDecimalFormat(wb); |
| | | try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { |
| | | wb.write(out); |
| | | wb.close(); |