| | |
| | | import com.trafficaudit.dataimport.mapper.PassengerEnterpriseMonthlyMapper; |
| | | import com.trafficaudit.dataimport.mapper.PassengerIndividualMonthlyMapper; |
| | | import com.trafficaudit.dataimport.mapper.ScaleSplitTransportMapper; |
| | | import com.trafficaudit.reportexport.calc.MidCalc; |
| | | import com.trafficaudit.reportexport.calc.PaxCalc; |
| | | import com.trafficaudit.reportexport.calc.SummaryWorkbookFiller; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.poi.ss.usermodel.Cell; |
| | | import org.apache.poi.ss.usermodel.CellStyle; |
| | |
| | | private CityBusMonthlyMapper cityBusMapper; |
| | | @Resource |
| | | private CityTaxiMonthlyMapper cityTaxiMapper; |
| | | @Resource |
| | | private SummaryWorkbookFiller summaryWorkbookFiller; |
| | | @Resource |
| | | private AuditResultMapper auditResultMapper; |
| | | @Resource |
| | |
| | | if (resolveSummaryMother(period, mode) == null) { |
| | | problems.add("docs/生成汇总大表 下缺少可复制的母版汇总表"); |
| | | } else { |
| | | Map<Integer, Map<String, double[]>> busM = loadCityBusByMonth(yearPrefix, month); |
| | | Map<Integer, Map<String, double[]>> taxiM = loadCityTaxiByMonth(yearPrefix, month); |
| | | Map<String, double[]> bc = busM.get(month); |
| | | Map<String, double[]> tc = taxiM.get(month); |
| | | if (bc != null && bc.get("全省") != null) addSum(summary, "城市公交客运量(全省当月)", bc.get("全省")[0], "万人"); |
| | | if (tc != null && tc.get("全省") != null) addSum(summary, "巡游出租客运量(全省当月)", tc.get("全省")[0], "万人"); |
| | | summaryWorkbookPreview(period, yearNum, month, summary, problems); |
| | | } |
| | | } catch (Exception ex) { |
| | | problems.add(ex.getMessage()); |
| | |
| | | sb.append(m).append("月"); |
| | | } |
| | | 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) { |
| | |
| | | } |
| | | 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); |
| | | int widthApplied = applySummaryReferenceLayout(wb); |
| | | log.info("汇总工作簿数据驱动回填完成:写入当月值格数={},补写当月同比公式格数={},补写当月合计公式格数={}," |
| | | + "按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}", |
| | | dbFilled, yoyFilled, monthFormulaFilled, widthApplied, dbProblems); |
| | | applyTwoDecimalFormat(wb); // 数值显示两位小数(保留全精度);% / 日期等既有样式不改变 |
| | | if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr(); |
| | | wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 还原/改写公式后打开即重算 |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 版式对齐:按 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; |
| | | } |
| | | |
| | | /** 定位版式参照件 docs/生成汇总大表/生成_道路运输量汇总表.xlsx(优先级同母版:配置目录 → user.dir 相对 → 上级) */ |
| | | 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 { |