| New file |
| | |
| | | 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<String, String> TYPE_LABELS = new LinkedHashMap<>(); |
| | | private static final Map<String, List<Col>> 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<Map<String, Object>> supportedTypes() { |
| | | List<Map<String, Object>> list = new ArrayList<>(); |
| | | for (Map.Entry<String, String> e : TYPE_LABELS.entrySet()) { |
| | | Map<String, Object> m = new LinkedHashMap<>(); |
| | | m.put("type", e.getKey()); |
| | | m.put("label", e.getValue()); |
| | | list.add(m); |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | /** 按市州汇总 */ |
| | | public Map<String, Object> summary(String type, String from, String to, boolean recomputeRank) { |
| | | List<Col> 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<String, Map<String, Double>> cur = sumByCity(longRows(type, f, t), cols); |
| | | Map<String, Map<String, Double>> last = sumByCity(longRows(type, lastFrom, lastTo), cols); |
| | | |
| | | List<Map<String, Object>> records = new ArrayList<>(); |
| | | for (String city : RegionUtil.cityList()) { |
| | | records.add(outRow(cols, cur.get(city), last.get(city), city)); |
| | | } |
| | | records.add(outRow(cols, mergeAll(cur), mergeAll(last), TOTAL_CITY)); |
| | | if (recomputeRank) { |
| | | applyRank(records, cols); |
| | | } |
| | | |
| | | List<Map<String, Object>> columns = new ArrayList<>(); |
| | | columns.add(colMeta("city", "市州", 120)); |
| | | for (Col c : cols) { |
| | | columns.add(colMeta(c.prop, c.label, c.width)); |
| | | } |
| | | Map<String, Object> 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<String, Object> data = summary(type, from, to, recomputeRank); |
| | | @SuppressWarnings("unchecked") |
| | | List<Map<String, Object>> columns = (List<Map<String, Object>>) data.get("columns"); |
| | | @SuppressWarnings("unchecked") |
| | | List<Map<String, Object>> records = (List<Map<String, Object>>) 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); |
| | | } |
| | | int r = 2; |
| | | for (Map<String, Object> rec : records) { |
| | | 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)); |
| | | } |
| | | } |
| | | } |
| | | 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<String, Object> colMeta(String prop, String label, int width) { |
| | | Map<String, Object> 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<Map<String, Object>> longRows(String type, String from, String to) { |
| | | List<Map<String, Object>> rows = new ArrayList<>(); |
| | | switch (type) { |
| | | case "cityBus": { |
| | | List<CityBusMonthly> list = cityBusMapper.selectList(new LambdaQueryWrapper<CityBusMonthly>() |
| | | .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<String, Object> m = BeanUtil.beanToMap(e); |
| | | m.put("_city", city); |
| | | rows.add(m); |
| | | } |
| | | break; |
| | | } |
| | | case "cityTaxi": { |
| | | List<CityTaxiMonthly> list = cityTaxiMapper.selectList(new LambdaQueryWrapper<CityTaxiMonthly>() |
| | | .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<String, Object> m = BeanUtil.beanToMap(e); |
| | | m.put("_city", city); |
| | | rows.add(m); |
| | | } |
| | | break; |
| | | } |
| | | case "h2032": { |
| | | List<H2032EnterpriseMonthly> list = h2032Mapper.selectList(new LambdaQueryWrapper<H2032EnterpriseMonthly>() |
| | | .ge(H2032EnterpriseMonthly::getReportPeriod, from).le(H2032EnterpriseMonthly::getReportPeriod, to)); |
| | | for (H2032EnterpriseMonthly e : list) { |
| | | String city = cityOfCode(e.getRegionCode()); |
| | | if (city == null) { |
| | | continue; |
| | | } |
| | | Map<String, Object> m = BeanUtil.beanToMap(e); |
| | | m.put("_city", city); |
| | | rows.add(m); |
| | | } |
| | | break; |
| | | } |
| | | case "h2031": { |
| | | List<PassengerEnterpriseMonthly> list = h2031Mapper.selectList(new LambdaQueryWrapper<PassengerEnterpriseMonthly>() |
| | | .ge(PassengerEnterpriseMonthly::getReportPeriod, from).le(PassengerEnterpriseMonthly::getReportPeriod, to)); |
| | | for (PassengerEnterpriseMonthly e : list) { |
| | | String city = cityOfCode(e.getRegionCode()); |
| | | if (city == null) { |
| | | continue; |
| | | } |
| | | Map<String, Object> m = BeanUtil.beanToMap(e); |
| | | m.put("_city", city); |
| | | rows.add(m); |
| | | } |
| | | break; |
| | | } |
| | | case "passengerIndividual": { |
| | | List<PassengerIndividualMonthly> list = individualMapper.selectList(new LambdaQueryWrapper<PassengerIndividualMonthly>() |
| | | .ge(PassengerIndividualMonthly::getReportPeriod, from).le(PassengerIndividualMonthly::getReportPeriod, to)); |
| | | for (PassengerIndividualMonthly e : list) { |
| | | String city = cityOfCode(e.getRegionCode()); |
| | | if (city == null) { |
| | | continue; |
| | | } |
| | | Map<String, Object> m = BeanUtil.beanToMap(e); |
| | | m.put("_city", city); |
| | | rows.add(m); |
| | | } |
| | | break; |
| | | } |
| | | case "wycOrder": { |
| | | List<WycOrderMonthly> list = wycOrderMapper.selectList(new LambdaQueryWrapper<WycOrderMonthly>() |
| | | .ge(WycOrderMonthly::getReportPeriod, from).le(WycOrderMonthly::getReportPeriod, to)); |
| | | List<String> cities = RegionUtil.cityList(); |
| | | for (WycOrderMonthly e : list) { |
| | | Map<String, WycSplitCalc.WycMetrics> 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<String, Object> 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<FreightTurnoverImport> list = freightTurnoverMapper.selectList(new LambdaQueryWrapper<FreightTurnoverImport>() |
| | | .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<FreightTurnoverImport>() |
| | | .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<String, Object> 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<String, Object> 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<String, Map<String, Double>> sumByCity(List<Map<String, Object>> rows, List<Col> cols) { |
| | | Map<String, Map<String, Double>> acc = new LinkedHashMap<>(); |
| | | for (Map<String, Object> row : rows) { |
| | | Object cityObj = row.get("_city"); |
| | | if (cityObj == null) { |
| | | continue; |
| | | } |
| | | String city = String.valueOf(cityObj); |
| | | Map<String, Double> sums = acc.computeIfAbsent(city, k -> new LinkedHashMap<>()); |
| | | Set<String> 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<String, Double> mergeAll(Map<String, Map<String, Double>> acc) { |
| | | Map<String, Double> total = new LinkedHashMap<>(); |
| | | for (Map<String, Double> sums : acc.values()) { |
| | | for (Map.Entry<String, Double> e : sums.entrySet()) { |
| | | total.merge(e.getKey(), e.getValue(), Double::sum); |
| | | } |
| | | } |
| | | return total; |
| | | } |
| | | |
| | | private Map<String, Object> outRow(List<Col> cols, Map<String, Double> cur, Map<String, Double> last, String city) { |
| | | Map<String, Object> 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<Map<String, Object>> records, List<Col> cols) { |
| | | List<Map<String, Object>> cities = new ArrayList<>(); |
| | | for (Map<String, Object> r : records) { |
| | | if (!TOTAL_CITY.equals(r.get("city"))) { |
| | | cities.add(r); |
| | | } |
| | | } |
| | | for (Col c : cols) { |
| | | if (!"RANK".equals(c.kind)) { |
| | | continue; |
| | | } |
| | | List<Map<String, Object>> sorted = new ArrayList<>(cities); |
| | | sorted.sort(Comparator.comparingDouble((Map<String, Object> 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); |
| | | } |
| | | } |
| | | } |
| | | } |