package com.trafficaudit.dataimport.service; import cn.hutool.core.bean.BeanUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.trafficaudit.common.util.RegionUtil; import com.trafficaudit.dataimport.entity.CityBusMonthly; import com.trafficaudit.dataimport.entity.CityTaxiMonthly; import com.trafficaudit.dataimport.entity.FreightTurnoverImport; import com.trafficaudit.dataimport.entity.H2032EnterpriseMonthly; import com.trafficaudit.dataimport.entity.PassengerEnterpriseMonthly; import com.trafficaudit.dataimport.entity.PassengerIndividualMonthly; import com.trafficaudit.dataimport.entity.WycOrderMonthly; import com.trafficaudit.dataimport.mapper.CityBusMonthlyMapper; import com.trafficaudit.dataimport.mapper.CityTaxiMonthlyMapper; import com.trafficaudit.dataimport.mapper.FreightTurnoverImportMapper; import com.trafficaudit.dataimport.mapper.H2032EnterpriseMonthlyMapper; import com.trafficaudit.dataimport.mapper.PassengerEnterpriseMonthlyMapper; import com.trafficaudit.dataimport.mapper.PassengerIndividualMonthlyMapper; import com.trafficaudit.dataimport.mapper.WycOrderMonthlyMapper; import com.trafficaudit.reportexport.calc.WycSplitCalc; import org.apache.poi.ss.usermodel.BorderStyle; import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.HorizontalAlignment; import org.apache.poi.xssf.usermodel.XSSFCell; import org.apache.poi.xssf.usermodel.XSSFCellStyle; import org.apache.poi.xssf.usermodel.XSSFRow; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * 数据查看 - 按市州汇总(任意起止月)。 * * 口径(2026-09-22 与用户确认): * 1) 流量类列(客运量/周转量/货运量/订单数)跨期求和; * 2) 时点类列(车辆数、企业数、站点数)不参与汇总,不出现在结果里; * 3) 比率类列按累计口径重算:累计同比 = 本期累计 ÷ 上年同期累计 − 1;平均运距 = 累计周转量 ÷ 累计客运量; * 4) 排名默认留空,可由前端选择"按累计值重排"; * 5) 结果 = 17 市州(RegionUtil 规范名/顺序)+ 全省合计行。 */ @Service public class CityRollupService { public static final String TOTAL_CITY = "全省合计"; @Resource private CityBusMonthlyMapper cityBusMapper; @Resource private CityTaxiMonthlyMapper cityTaxiMapper; @Resource private H2032EnterpriseMonthlyMapper h2032Mapper; @Resource private PassengerEnterpriseMonthlyMapper h2031Mapper; @Resource private PassengerIndividualMonthlyMapper individualMapper; @Resource private WycOrderMonthlyMapper wycOrderMapper; @Resource private FreightTurnoverImportMapper freightTurnoverMapper; @Resource private WycSplitCalc wycSplitCalc; /** 汇总列定义。kind:FLOW 求和 / AVG 分子÷分母 / YOY 本期累计↔上年同期累计 / YOYS 同行去年字段 / RANK 按某输出列重排名次 */ private static class Col { final String prop; final String label; final int width; final String kind; final String src; final String den; final String last; final String rankBy; Col(String prop, String label, int width, String kind, String src, String den, String last, String rankBy) { this.prop = prop; this.label = label; this.width = width; this.kind = kind; this.src = src; this.den = den; this.last = last; this.rankBy = rankBy; } } private static Col flow(String prop, String label, int width, String src) { return new Col(prop, label, width, "FLOW", src, null, null, null); } private static Col avg(String prop, String label, String src, String den) { return new Col(prop, label, 130, "AVG", src, den, null, null); } private static Col yoy(String prop, String label, String src) { return new Col(prop, label, 150, "YOY", src, null, null, null); } private static Col yoys(String prop, String label, String src, String last) { return new Col(prop, label, 150, "YOYS", src, null, last, null); } private static Col rank(String prop, String label, String rankBy) { return new Col(prop, label, 110, "RANK", null, null, null, rankBy); } private static final Map TYPE_LABELS = new LinkedHashMap<>(); private static final Map> SPECS = new LinkedHashMap<>(); static { TYPE_LABELS.put("cityBus", "城市公共交通(按市州汇总)"); SPECS.put("cityBus", Arrays.asList( flow("passengerVolume", "客运量(万人次)", 140, "passengerVolume"), flow("turnover", "旅客周转量(万人公里)", 180, "turnover"), flow("passengerCity", "城市内客运量(万人次)", 170, "passengerCity"), flow("turnoverCity", "城市内周转量(万人公里)", 190, "turnoverCity"), flow("passengerChengxiang", "城际城乡客运量(万人次)", 190, "passengerChengxiang"), flow("turnoverChengxiang", "城际城乡周转量(万人公里)", 210, "turnoverChengxiang"), avg("avgDistance", "平均运距(公里)", "turnover", "passengerVolume"), yoy("passengerYoy", "客运量累计同比(%)", "passengerVolume"), rank("passengerRank", "客运量排名", "passengerVolume"))); TYPE_LABELS.put("cityTaxi", "巡游出租汽车(按市州汇总)"); SPECS.put("cityTaxi", Arrays.asList( flow("passengerVolume", "客运量(万人次)", 140, "passengerVolume"), flow("turnover", "旅客周转量(万人公里)", 180, "turnover"), flow("passengerCity", "城市内客运量(万人次)", 170, "passengerCity"), flow("turnoverCity", "城市内周转量(万人公里)", 190, "turnoverCity"), flow("tripTotal", "载客车次总数(车次)", 170, "tripTotal"), flow("tripCity", "载客车次_城市内(车次)", 190, "tripCity"), avg("avgDistance", "平均运距(公里)", "turnover", "passengerVolume"), yoy("passengerYoy", "客运量累计同比(%)", "passengerVolume"), rank("passengerRank", "客运量排名", "passengerVolume"))); TYPE_LABELS.put("h2032", "道路货物运输企业(按市州汇总)"); SPECS.put("h2032", Arrays.asList( flow("freightTotal", "货运量(吨)", 150, "freightTotal"), flow("turnoverTotal", "货运周转量(吨公里)", 180, "turnoverTotal"), avg("avgDistance", "平均运距(公里)", "turnoverTotal", "freightTotal"), yoy("freightYoy", "货运量累计同比(%)", "freightTotal"), rank("freightRank", "货运量排名", "freightTotal"), rank("turnoverRank", "周转量排名", "turnoverTotal"))); TYPE_LABELS.put("h2031", "公路旅客运输企业(按市州汇总)"); SPECS.put("h2031", Arrays.asList( flow("passengerTotal", "客运量(人)", 140, "passengerTotal"), flow("turnoverTotal", "旅客周转量(人公里)", 180, "turnoverTotal"), avg("avgDistance", "平均运距(公里)", "turnoverTotal", "passengerTotal"), yoy("passengerYoy", "客运量累计同比(%)", "passengerTotal"), rank("passengerRank", "客运量排名", "passengerTotal"))); TYPE_LABELS.put("passengerIndividual", "个体客运量/周转量(按市州汇总)"); SPECS.put("passengerIndividual", Arrays.asList( flow("passengerCount", "个体客运量(人)", 150, "passengerCount"), flow("turnover", "个体旅客周转量(人公里)", 190, "turnover"), yoy("passengerYoy", "客运量累计同比(%)", "passengerCount"))); TYPE_LABELS.put("wycOrder", "网约车(按市州、拆分口径汇总)"); SPECS.put("wycOrder", Arrays.asList( flow("orderCount", "订单数(单)", 150, "orderCount"), flow("pax", "客运量(万人次)", 140, "pax"), flow("turnover", "旅客周转量(万人公里)", 180, "turnover"), flow("cityPax", "城市内客运量(万人次)", 170, "cityPax"), flow("cityTurnover", "城市内旅客周转量(万人公里)", 210, "cityTurnover"), yoy("paxYoy", "客运量累计同比(%)", "pax"), rank("paxRank", "客运量排名", "pax"))); TYPE_LABELS.put("freightTurnover", "货运量周转量(按市州汇总)"); SPECS.put("freightTurnover", Arrays.asList( flow("freight", "货运量(万吨)", 140, "freightM"), flow("turnover", "周转量(万吨公里)", 170, "turnoverM"), yoys("freightYoy", "货运量累计同比(%)", "freightM", "lastFreightM"), yoys("turnoverYoy", "周转量累计同比(%)", "turnoverM", "lastTurnoverM"), rank("freightRank", "货运量排名", "freight"), rank("turnoverRank", "周转量排名", "turnover"))); } public List> supportedTypes() { List> list = new ArrayList<>(); for (Map.Entry e : TYPE_LABELS.entrySet()) { Map m = new LinkedHashMap<>(); m.put("type", e.getKey()); m.put("label", e.getValue()); list.add(m); } return list; } /** 按市州汇总 */ public Map summary(String type, String from, String to, boolean recomputeRank) { List cols = SPECS.get(type); if (cols == null) { throw new RuntimeException("该数据类型暂不支持按市州汇总:" + type); } String f = trimPeriod(from); String t = trimPeriod(to); if (f == null || t == null) { throw new RuntimeException("请先选择起止月(例如 2026-01 ~ 2026-08)"); } if (f.compareTo(t) > 0) { String tmp = f; f = t; t = tmp; } String lastFrom = yearBefore(f); String lastTo = yearBefore(t); Map> cur = sumByCity(longRows(type, f, t), cols); Map> last = sumByCity(longRows(type, lastFrom, lastTo), cols); List> records = new ArrayList<>(); // 全省合计行放在最前面(用户要求,放在武汉市之上) records.add(outRow(cols, mergeAll(cur), mergeAll(last), TOTAL_CITY)); for (String city : RegionUtil.cityList()) { records.add(outRow(cols, cur.get(city), last.get(city), city)); } if (recomputeRank) { applyRank(records, cols); } List> columns = new ArrayList<>(); columns.add(colMeta("city", "市州", 120)); for (Col c : cols) { columns.add(colMeta(c.prop, c.label, c.width)); } Map res = new LinkedHashMap<>(); res.put("type", type); res.put("label", TYPE_LABELS.get(type)); res.put("from", f); res.put("to", t); res.put("lastFrom", lastFrom); res.put("lastTo", lastTo); res.put("rankMode", recomputeRank ? "recompute" : "blank"); res.put("columns", columns); res.put("records", records); return res; } /** 导出为 xlsx(数值保留全精度,显示格式 2 位小数 / 百分比) */ public byte[] exportExcel(String type, String from, String to, boolean recomputeRank) throws IOException { Map data = summary(type, from, to, recomputeRank); @SuppressWarnings("unchecked") List> columns = (List>) data.get("columns"); @SuppressWarnings("unchecked") List> records = (List>) data.get("records"); String label = String.valueOf(data.get("label")); String title = label + " " + data.get("from") + " ~ " + data.get("to") + "(累计)"; try (XSSFWorkbook wb = new XSSFWorkbook(); ByteArrayOutputStream bos = new ByteArrayOutputStream()) { XSSFSheet sheet = wb.createSheet("按市州汇总"); Font bold = wb.createFont(); bold.setBold(true); XSSFCellStyle titleStyle = wb.createCellStyle(); titleStyle.setFont(bold); XSSFCellStyle headStyle = wb.createCellStyle(); headStyle.setFont(bold); headStyle.setAlignment(HorizontalAlignment.CENTER); headStyle.setBorderBottom(BorderStyle.THIN); XSSFCellStyle numStyle = wb.createCellStyle(); numStyle.setDataFormat(wb.createDataFormat().getFormat("0.00")); XSSFCellStyle pctStyle = wb.createCellStyle(); pctStyle.setDataFormat(wb.createDataFormat().getFormat("0.00%")); XSSFRow titleRow = sheet.createRow(0); XSSFCell titleCell = titleRow.createCell(0); titleCell.setCellValue(title); titleCell.setCellStyle(titleStyle); XSSFRow headRow = sheet.createRow(1); for (int i = 0; i < columns.size(); i++) { XSSFCell cell = headRow.createCell(i); cell.setCellValue(String.valueOf(columns.get(i).get("label"))); cell.setCellStyle(headStyle); } Font totalFont = wb.createFont(); totalFont.setBold(true); XSSFCellStyle totalStyle = wb.createCellStyle(); totalStyle.setFont(totalFont); int r = 2; for (Map rec : records) { boolean isTotal = TOTAL_CITY.equals(rec.get("city")); XSSFRow row = sheet.createRow(r++); for (int i = 0; i < columns.size(); i++) { String prop = String.valueOf(columns.get(i).get("prop")); String colLabel = String.valueOf(columns.get(i).get("label")); Object v = rec.get(prop); XSSFCell cell = row.createCell(i); if (v instanceof Number) { cell.setCellValue(((Number) v).doubleValue()); cell.setCellStyle(colLabel.contains("同比") ? pctStyle : numStyle); } else { cell.setCellValue(v == null ? "" : String.valueOf(v)); if (isTotal) { cell.setCellStyle(totalStyle); } } } } for (int i = 0; i < columns.size(); i++) { sheet.setColumnWidth(i, Math.min(40, Math.max(10, Integer.parseInt(String.valueOf(columns.get(i).get("width"))) / 7)) * 256); } wb.write(bos); return bos.toByteArray(); } } // ==================== 内部实现 ==================== private static String trimPeriod(String p) { if (p == null) { return null; } String s = p.trim(); if (s.length() >= 7) { return s.substring(0, 7); } return s.isEmpty() ? null : s; } private static String yearBefore(String period) { int year = Integer.parseInt(period.substring(0, 4)) - 1; return year + period.substring(4); } private static Map colMeta(String prop, String label, int width) { Map m = new LinkedHashMap<>(); m.put("prop", prop); m.put("label", label); m.put("width", width); return m; } /** 只有 17 个规范市州名才算市州;"湖北省""全省"等合计行返回 null(避免重复计入) */ private String cityOrNull(String name) { if (name == null) { return null; } String normalized = RegionUtil.normalizeCityName(name); return normalized != null && RegionUtil.cityList().contains(normalized) ? normalized : null; } private String cityOfCode(String regionCode) { if (regionCode == null || regionCode.trim().isEmpty()) { return null; } return RegionUtil.cityByCode(regionCode.trim()); } private static double nz(Double v) { return v == null ? 0.0 : v; } private static double num(Object v) { if (v instanceof Number) { return ((Number) v).doubleValue(); } if (v instanceof String) { try { return Double.parseDouble(((String) v).trim()); } catch (Exception ignore) { return 0.0; } } return 0.0; } /** 取某类型在 [from,to] 的"长表"行:每行含 _city 与参与汇总的源字段 */ private List> longRows(String type, String from, String to) { List> rows = new ArrayList<>(); switch (type) { case "cityBus": { List list = cityBusMapper.selectList(new LambdaQueryWrapper() .ge(CityBusMonthly::getReportPeriod, from).le(CityBusMonthly::getReportPeriod, to)); for (CityBusMonthly e : list) { String city = e.getCity() != null ? cityOrNull(e.getCity()) : cityOfCode(e.getRegionCode()); if (city == null) { continue; } Map m = BeanUtil.beanToMap(e); m.put("_city", city); rows.add(m); } break; } case "cityTaxi": { List list = cityTaxiMapper.selectList(new LambdaQueryWrapper() .ge(CityTaxiMonthly::getReportPeriod, from).le(CityTaxiMonthly::getReportPeriod, to)); for (CityTaxiMonthly e : list) { String city = e.getCity() != null ? cityOrNull(e.getCity()) : cityOfCode(e.getRegionCode()); if (city == null) { continue; } Map m = BeanUtil.beanToMap(e); m.put("_city", city); rows.add(m); } break; } case "h2032": { List list = h2032Mapper.selectList(new LambdaQueryWrapper() .ge(H2032EnterpriseMonthly::getReportPeriod, from).le(H2032EnterpriseMonthly::getReportPeriod, to)); for (H2032EnterpriseMonthly e : list) { String city = cityOfCode(e.getRegionCode()); if (city == null) { continue; } Map m = BeanUtil.beanToMap(e); m.put("_city", city); rows.add(m); } break; } case "h2031": { List list = h2031Mapper.selectList(new LambdaQueryWrapper() .ge(PassengerEnterpriseMonthly::getReportPeriod, from).le(PassengerEnterpriseMonthly::getReportPeriod, to)); for (PassengerEnterpriseMonthly e : list) { String city = cityOfCode(e.getRegionCode()); if (city == null) { continue; } Map m = BeanUtil.beanToMap(e); m.put("_city", city); rows.add(m); } break; } case "passengerIndividual": { List list = individualMapper.selectList(new LambdaQueryWrapper() .ge(PassengerIndividualMonthly::getReportPeriod, from).le(PassengerIndividualMonthly::getReportPeriod, to)); for (PassengerIndividualMonthly e : list) { String city = cityOfCode(e.getRegionCode()); if (city == null) { continue; } Map m = BeanUtil.beanToMap(e); m.put("_city", city); rows.add(m); } break; } case "wycOrder": { List list = wycOrderMapper.selectList(new LambdaQueryWrapper() .ge(WycOrderMonthly::getReportPeriod, from).le(WycOrderMonthly::getReportPeriod, to)); List cities = RegionUtil.cityList(); for (WycOrderMonthly e : list) { Map byCity = null; try { byCity = wycSplitCalc.calc(e.getReportPeriod()).getByCity(); } catch (Exception ignore) { // 拆分输入不全时只汇总订单数 } double[] orders = {nz(e.getOrderWuhan()), nz(e.getOrderHuangshi()), nz(e.getOrderShiyan()), nz(e.getOrderYichang()), nz(e.getOrderXiangyang()), nz(e.getOrderEzhou()), nz(e.getOrderJingmen()), nz(e.getOrderXiaogan()), nz(e.getOrderJingzhou()), nz(e.getOrderHuanggang()), nz(e.getOrderXianning()), nz(e.getOrderSuizhou()), nz(e.getOrderEnshi()), nz(e.getOrderXiantao()), nz(e.getOrderQianjiang()), nz(e.getOrderTianmen()), nz(e.getOrderShennong())}; for (int i = 0; i < cities.size() && i < orders.length; i++) { Map m = new LinkedHashMap<>(); m.put("_city", cities.get(i)); m.put("orderCount", orders[i]); WycSplitCalc.WycMetrics mt = byCity == null ? null : byCity.get(cities.get(i)); if (mt != null) { m.put("pax", mt.getTotalPax()); m.put("turnover", mt.getTotalTurnover()); m.put("cityPax", mt.getCityPax()); m.put("cityTurnover", mt.getCityTurnover()); } rows.add(m); } } break; } case "freightTurnover": { // 该表每行含"年初至当期"的各月列(行内累计),故累计只用 to 那一行、对区间月份求和; // 且"湖北省"行等于 17 市州之和,必须排除,否则重复计入。 List list = freightTurnoverMapper.selectList(new LambdaQueryWrapper() .ge(FreightTurnoverImport::getReportPeriod, from) .le(FreightTurnoverImport::getReportPeriod, to) .orderByDesc(FreightTurnoverImport::getReportPeriod)); if (list.isEmpty()) { break; } // 该表每行是"年初至当期"的行内累计,取区间内最新一期那一行;结束月无数据时自动回退到最新有数月份 String latestPeriod = list.get(0).getReportPeriod(); list = new ArrayList<>(); for (FreightTurnoverImport e : freightTurnoverMapper.selectList(new LambdaQueryWrapper() .eq(FreightTurnoverImport::getReportPeriod, latestPeriod))) { list.add(e); } int m1 = Integer.parseInt(from.substring(5, 7)); int m2 = Integer.parseInt(latestPeriod.substring(5, 7)); if (m2 < m1) { break; } for (FreightTurnoverImport e : list) { String city = cityOrNull(e.getRegionName()); if (city == null) { continue; } Map src = BeanUtil.beanToMap(e); double freight = 0, turnover = 0, lastFreight = 0, lastTurnover = 0; for (int mm = m1; mm <= m2; mm++) { freight += num(src.get(String.format("freightM%02d", mm))); turnover += num(src.get(String.format("turnoverM%02d", mm))); lastFreight += num(src.get(String.format("lastFreightM%02d", mm))); lastTurnover += num(src.get(String.format("lastTurnoverM%02d", mm))); } Map m = new LinkedHashMap<>(); m.put("_city", city); m.put("freightM", freight); m.put("turnoverM", turnover); m.put("lastFreightM", lastFreight); m.put("lastTurnoverM", lastTurnover); rows.add(m); } break; } default: break; } return rows; } /** 按市州累计各列用到的源字段 */ private Map> sumByCity(List> rows, List cols) { Map> acc = new LinkedHashMap<>(); for (Map row : rows) { Object cityObj = row.get("_city"); if (cityObj == null) { continue; } String city = String.valueOf(cityObj); Map sums = acc.computeIfAbsent(city, k -> new LinkedHashMap<>()); Set keys = new LinkedHashSet<>(); for (Col c : cols) { if (c.src != null) { keys.add(c.src); } if (c.den != null) { keys.add(c.den); } if (c.last != null) { keys.add(c.last); } } for (String key : keys) { sums.merge(key, num(row.get(key)), Double::sum); } } return acc; } private Map mergeAll(Map> acc) { Map total = new LinkedHashMap<>(); for (Map sums : acc.values()) { for (Map.Entry e : sums.entrySet()) { total.merge(e.getKey(), e.getValue(), Double::sum); } } return total; } private Map outRow(List cols, Map cur, Map last, String city) { Map row = new LinkedHashMap<>(); row.put("city", city); for (Col c : cols) { Double value = null; switch (c.kind) { case "FLOW": { value = cur == null ? null : cur.get(c.src); break; } case "AVG": { double den = cur == null ? 0.0 : num(cur.get(c.den)); double nume = cur == null ? 0.0 : num(cur.get(c.src)); value = den > 0 ? nume / den : null; break; } case "YOY": { double base = last == null ? 0.0 : num(last.get(c.src)); double now = cur == null ? 0.0 : num(cur.get(c.src)); value = base > 0 ? now / base - 1 : null; break; } case "YOYS": { double base = cur == null ? 0.0 : num(cur.get(c.last)); double now = cur == null ? 0.0 : num(cur.get(c.src)); value = base > 0 ? now / base - 1 : null; break; } default: { value = null; // RANK: 由 applyRank 填 } } row.put(c.prop, value); } return row; } /** 按累计值重新排名(1 = 最大);全省合计行不排名 */ private void applyRank(List> records, List cols) { List> cities = new ArrayList<>(); for (Map r : records) { if (!TOTAL_CITY.equals(r.get("city"))) { cities.add(r); } } for (Col c : cols) { if (!"RANK".equals(c.kind)) { continue; } List> sorted = new ArrayList<>(cities); sorted.sort(Comparator.comparingDouble((Map r) -> { Object v = r.get(c.rankBy); return v instanceof Number ? ((Number) v).doubleValue() : Double.NEGATIVE_INFINITY; }).reversed()); for (int i = 0; i < sorted.size(); i++) { Object v = sorted.get(i).get(c.rankBy); sorted.get(i).put(c.prop, v instanceof Number ? (i + 1) : null); } } } }