| | |
| | | } |
| | | 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); |
| | |
| | | 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) { |
| | |
| | | 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)); |
| | | } |
| | | |
| | |
| | | if (maLast != null) provMonthAboveLast += maLast; |
| | | monthAbove.put(city, ma); |
| | | monthTotal.put(city, mt); |
| | | monthBelow.put(city, mt == null ? null : round(mt - (ma == null ? 0.0 : ma), 4)); |
| | | 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 : round(mtLast - (maLast == null ? 0.0 : maLast), 4); |
| | | 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 : round(provMonthAbove, 4); |
| | | Double provMonthAboveWan = provMonthAbove == 0.0 ? null : provMonthAbove; |
| | | Double provMonthTotal = freightMonth(ftMap.get("湖北省"), month); |
| | | Double provMonthBelow = provMonthTotal == null ? null : round(provMonthTotal - provMonthAbove, 4); |
| | | 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 |
| | | : round(provMonthTotalLast - provMonthAboveLast, 4); |
| | | : provMonthTotalLast - provMonthAboveLast; |
| | | Double provMonthBelowYoy = growthYoy(provMonthBelow, provMonthBelowLast); |
| | | Double provMonthTotalYoy = freightYoy(ftMap.get("湖北省"), month); |
| | | |
| | |
| | | private Double monthFreightWan(Map<String, Map<Integer, Double>> byCityMonth, String city, int month) { |
| | | Map<Integer, Double> byMonth = byCityMonth == null ? null : byCityMonth.get(city); |
| | | Double v = byMonth == null ? null : byMonth.get(month); |
| | | return v == null ? null : round(v / 10000.0, 4); |
| | | return v == null ? null : v / 10000.0; |
| | | } |
| | | |
| | | /** 同比 = (本期-基期)/基期;任一期缺失或基期为 0 返回 null */ |
| | |
| | | } |
| | | } |
| | | |
| | | /** 周转量排名 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); |
| | |
| | | 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) */ |
| | |
| | | if (v == 0.0) return; // 库内无源:保留母版/定稿原值,缺数据不覆盖 |
| | | // POI setCellValue(double) 对公式单元格只更新缓存不移除公式,必须先 setBlank 再写值 |
| | | c.setBlank(); |
| | | c.setCellValue(round(v, 4)); |
| | | c.setCellValue(v); |
| | | } |
| | | |
| | | /** 显式写入 0(POI setCellValue(0.0) 会转 blank,需操作底层 XML) */ |
| | |
| | | String city = RegionUtil.normalizeCityName(b.getCity()); |
| | | if (city == null) continue; |
| | | int key = y * 100 + m; |
| | | // B1 兜底:存储的「城际城乡」缺失(<=0)时用「总客运量 − 城市内」现算,避免 2025 全年公交维度被当成 0 |
| | | double cxPass = nz(b.getPassengerChengxiang()); |
| | | if (cxPass <= 0) cxPass = nz(b.getPassengerVolume()) - nz(b.getPassengerCity()); |
| | | double cxTurn = nz(b.getTurnoverChengxiang()); |
| | | if (cxTurn <= 0) cxTurn = nz(b.getTurnover()) - nz(b.getTurnoverCity()); |
| | | // 口径统一(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); |
| | |
| | | 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)); |
| | | } |
| | |
| | | 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, 1, pass == 0 ? null : pass); |
| | | setValOrBlank(sheet, r0, 3, midCumYoy(mid, currentYear, city, maxMonth, 0, true)); |
| | | setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4)); |
| | | 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 : round(mPass, 4)); |
| | | 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 : round(mTurn, 4)); |
| | | setValOrBlank(sheet, r0, 15, mTurn == 0 ? null : mTurn); |
| | | setValOrBlank(sheet, r0, 17, mTurn == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, city, 0, false)); |
| | | } |
| | | recalc(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, 1, pass == 0 ? null : pass); |
| | | setValOrBlank(sheet, r0, 2, midCumYoy(mid, currentYear, "湖北省", maxMonth, dims[i], true)); |
| | | setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4)); |
| | | 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) |
| | |
| | | double turn = midClassVal(mid, currentYear, maxMonth, "湖北省", dims[i], false); |
| | | double lastPass = midClassVal(mid, currentYear - 1, maxMonth, "湖北省", dims[i], true); |
| | | double lastTurn = midClassVal(mid, currentYear - 1, maxMonth, "湖北省", dims[i], false); |
| | | setValOrBlank(sheet, r0, 1, pass == 0 ? null : round(pass, 4)); |
| | | setValOrBlank(sheet, r0, 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 : round(turn, 4)); |
| | | 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 |
| | |
| | | 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) */ |
| | |
| | | * 保留全部页签/版式/公式/2025 年缓存值,仅把表头年份动态化为目标年;数据填充按 P2 起逐步接入。 |
| | | */ |
| | | public byte[] exportSummaryWorkbook(String period, String mode) throws Exception { |
| | | return exportSummaryWorkbook(period, mode, null); |
| | | return exportSummaryWorkbook(period, mode, null, null); |
| | | } |
| | | |
| | | /** 生成_道路运输量汇总表.xlsx(P0-P1 骨架,支持 toMonth 裁剪): |
| | |
| | | * toMonth 非空且小于 M 时,把 8 个台账/合成页裁到 toMonth 月(toMonth+1..M 的当月值/同比列清空、保留表头, |
| | | * 累计与累计同比公式改为按 toMonth 月重算),用于“先生成 1-6 月、1-7 月扩列”验证。 */ |
| | | public byte[] exportSummaryWorkbook(String period, String mode, Integer toMonth) throws Exception { |
| | | return exportSummaryWorkbook(period, mode, toMonth, null); |
| | | } |
| | | |
| | | /** 同上,额外通过 progress 回调报告进度(异步导出任务用:步骤百分比 + 步骤文案) */ |
| | | public byte[] exportSummaryWorkbook(String period, String mode, Integer toMonth, |
| | | java.util.function.BiConsumer<Integer, String> progress) throws Exception { |
| | | reportProgress(progress, 2, "校验报表期与母版"); |
| | | int year = Integer.parseInt(period.contains("-") ? period.split("-")[0] : period); |
| | | int month = period.contains("-") ? monthOf(period, mode) : 12; |
| | | File template = resolveSummaryMother(period, mode); |
| | |
| | | throw new RuntimeException("未找到汇总表母版《" + year + "年" + month + "月道路运输量汇总表.xlsx》(docs/生成汇总大表)"); |
| | | } |
| | | double savedZipRatio = ZipSecureFile.getMinInflateRatio(); |
| | | reportProgress(progress, 6, "读取母版工作簿"); |
| | | 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)) { |
| | | reportProgress(progress, 12, "还原同期月度块"); |
| | | File donor = resolveSummaryDonor(year, month); |
| | | if (donor != null) { |
| | | try (InputStream din = new FileInputStream(donor); XSSFWorkbook dwb = new XSSFWorkbook(din)) { |
| | |
| | | int effMonth = (toMonth != null && toMonth > 0 && toMonth < month) ? toMonth : month; |
| | | List<String> dbProblems = new ArrayList<>(); |
| | | String effPeriod = String.format("%04d-%02d", year, effMonth); |
| | | reportProgress(progress, 22, "回填当月数据"); |
| | | 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); |
| | | reportProgress(progress, 45, "刷新排名页与跨页合成页"); |
| | | int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month"); |
| | | int cumulativeRefreshed = refreshSummaryCumulativeSheets(wb, year, effMonth); // 四张跨页合成页随报表期刷新 |
| | | int widthApplied = applySummaryReferenceLayout(wb); |
| | | log.info("汇总工作簿数据驱动回填完成:写入当月值格数={},补写当月同比公式格数={},补写当月合计公式格数={}," |
| | | + "刷新排名页数值格数={},刷新四页(城市汇总/城市分市州/中口径分析/道路运输周转量)数值格数={}," |
| | | + "修正累计同比分母格数={},刷新排名页数值格数={},刷新四页(城市汇总/城市分市州/中口径分析/道路运输周转量)数值格数={}," |
| | | + "按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}", |
| | | dbFilled, yoyFilled, monthFormulaFilled, rankRefreshed, cumulativeRefreshed, widthApplied, dbProblems); |
| | | 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); |
| | | reportProgress(progress, 70, "重算整本公式(约 4 万格)"); |
| | | int[] recalc = recalcWorkbookFormulas(wb); // 公式重算并写回缓存值:全省行(Σ市州)/累计列/排名 SUM 才能被预览与二次处理读到 |
| | | log.info("汇总工作簿公式重算完成:成功 {} 格,失败 {} 格(失败格保留母版旧缓存值,打开时按 fullCalcOnLoad 重算)", recalc[0], recalc[1]); |
| | | if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr(); |
| | | wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 还原/改写公式后打开即重算 |
| | | reportProgress(progress, 95, "写出文件"); |
| | | return toBytes(wb); |
| | | } finally { |
| | | ZipSecureFile.setMinInflateRatio(savedZipRatio); |
| | | } |
| | | } |
| | | |
| | | /** 导出进度回调(异步导出任务用):回调自身异常不影响导出 */ |
| | | private static void reportProgress(java.util.function.BiConsumer<Integer, String> progress, int percent, String step) { |
| | | if (progress == null) return; |
| | | try { |
| | | progress.accept(percent, step); |
| | | } catch (Exception ignore) { |
| | | // 进度回调失败不影响导出 |
| | | } |
| | | } |
| | | |
| | |
| | | if (r.getAvgDistanceCity() != null && r.getAvgDistanceCity() > 0) a[5] = r.getAvgDistanceCity(); |
| | | } |
| | | for (double[] a : map.values()) { |
| | | if (a[4] <= 0 && a[0] > 0) a[4] = round(a[1] / a[0], 2); |
| | | if (a[5] <= 0 && a[2] > 0) a[5] = round(a[3] / a[2], 2); |
| | | 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; |
| | | } |
| | |
| | | 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++; |
| | |
| | | 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; |
| | | } |
| | |
| | | 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(); |
| | | } |
| | | } |
| | | |
| | | |
| | |
| | | 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 ? round(s[3] / b * 100, 4) : null); |
| | | putNum(row, 4, b > 0 ? s[3] / b * 100 : null); |
| | | cT += s[3]; |
| | | dT += s[4]; |
| | | } |
| | |
| | | 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 ? round(cT / b * 100, 4) : null); |
| | | putNum(tot, 4, b > 0 ? cT / b * 100 : null); |
| | | } |
| | | } |
| | | try { |
| | |
| | | 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 + "年交通建设项目储备情况(规模以上项目)"); |
| | |
| | | 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; |
| | |
| | | CellType ct = cell.getCellType(); |
| | | if (ct != CellType.NUMERIC && ct != CellType.FORMULA) continue; |
| | | if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) continue; |
| | | boolean integral = false; |
| | | if (ct == CellType.NUMERIC) { |
| | | integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue(); |
| | | } else if (cell.getCachedFormulaResultType() == CellType.NUMERIC) { |
| | | integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue(); |
| | | } |
| | | if (integral) continue; // 整数不显示 .00(含公式结果为整数的格) |
| | | org.apache.poi.ss.usermodel.CellStyle cs = cell.getCellStyle(); |
| | | if (cs == null) continue; |
| | | String f = cs.getDataFormatString(); |
| | | if (f == null) f = ""; |
| | | if (f.contains("%") || f.contains("E") || f.contains("0.00")) continue; |
| | | boolean plain = f.isEmpty() || "General".equals(f) || "general".equals(f) |
| | | || "0".equals(f) || "#,##0".equals(f) || "0.0".equals(f) || "#,##0.0".equals(f); |
| | | if (!plain) continue; |
| | | 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(fmtTwo); |
| | | ns.setDataFormat(normalized == null ? fmtTwo : df.getFormat(normalized)); |
| | | cell.setCellStyle(ns); |
| | | } catch (Exception ignore) { |
| | | // 个别单元格格式异常不阻塞导出 |