| | |
| | | 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; |
| | |
| | | private CityTaxiMonthlyMapper cityTaxiMapper; |
| | | @Resource |
| | | private SummaryWorkbookFiller summaryWorkbookFiller; |
| | | @Resource |
| | | private WycSplitCalc wycSplitCalc; |
| | | @Resource |
| | | private WycOrderMonthlyMapper wycOrderMapper; |
| | | @Resource |
| | | private AuditResultMapper auditResultMapper; |
| | | @Resource |
| | |
| | | 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) { |
| | | } else { |
| | | v = midClassVal(mid, year, month, area, k, volume); |
| | | lv = midClassVal(mid, year - 1, month, area, k, volume); |
| | | } |
| | |
| | | if (v != 0.0 && lv != 0.0) c.setCellValue(round((v - lv) / lv, 4)); |
| | | } |
| | | |
| | | /** 中口径累计同比列(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(); |
| | |
| | | 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); |
| | | } |
| | |
| | | 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); |
| | | } else if (monthOfCol(col) > 6) { |
| | | // 扩列出来的 7~12 月总行值列:模板里本无此列,克隆后既没有公式也没有缓存值, |
| | | // 直接写库内四维合计(维度0)。1~6 月保留模板缓存值,避免丢掉模板里的定稿数。 |
| | | double total = midClassVal(mid, currentYear, monthOfCol(col), area, 0, volume); |
| | | c.setBlank(); |
| | | if (total != 0.0) c.setCellValue(round(total, 4)); |
| | | } else if (c.getCellType() == CellType.FORMULA) { |
| | | keepCached(c); // 1~6 月总行值列 = 模板缓存值,保留 |
| | | } |
| | | 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! 同比公式 → 清空 |
| | | return toBytes(wb); |
| | | } |
| | |
| | | return (col - 1) / 2; |
| | | } |
| | | |
| | | /** 填中口径数据单元格:出租/网约车(3/4)显式 0,班线/公交填库内值(无值清空) */ |
| | | /** 填中口径数据单元格:班线/公交/出租城乡/网约车城乡 四维统一取库内值(无值清空) */ |
| | | 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); |
| | | // POI setCellValue(double) 对公式单元格只更新缓存不移除公式,必须先 setBlank 再写值 |
| | | c.setBlank(); |
| | |
| | | addMidClass(mid, key, city, 2, nz(b.getPassengerChengxiang()), nz(b.getTurnoverChengxiang())); |
| | | addMidClass(mid, key, "湖北省", 2, nz(b.getPassengerChengxiang()), nz(b.getTurnoverChengxiang())); |
| | | } |
| | | // 维度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); |
| | | } |
| | | // 维度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) { |
| | | continue; // 该期缺订单/pin 或出租源数据,跳过(不影响其它期) |
| | | } |
| | | 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()); |
| | | } |
| | | } |
| | | 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) */ |
| | |
| | | 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); |
| | |
| | | } |
| | | |
| | | /** |
| | | * 汇总工作簿公式重算: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}; |
| | | } |
| | | 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>, |