| | |
| | | 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) { |
| | | v = midClassVal(mid, year, month, area, k, volume); |
| | | lv = midClassVal(mid, year - 1, month, area, k, volume); |
| | | } |
| | | Double y = midMonthYoy(mid, year, month, area, k, volume); |
| | | c.setBlank(); |
| | | if (v != 0.0 && lv != 0.0) c.setCellValue(round((v - lv) / lv, 4)); |
| | | if (y != null) c.setCellValue(y); |
| | | } |
| | | |
| | | /** 中口径累计同比列(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(); |
| | |
| | | if (blockOffset < 0 || blockOffset % 5 == 0) continue; |
| | | int k = blockOffset % 5; |
| | | 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); |
| | | } |
| | | } |
| | | Double y = midCumYoy(mid, year, area, months, k, volume); |
| | | Row row = sheet.getRow(r0); |
| | | if (row == null) continue; |
| | | Cell c = row.getCell(colIdx); |
| | | if (c == null) c = row.createCell(colIdx); |
| | | c.setBlank(); |
| | | if (v != 0.0 && lv != 0.0) c.setCellValue(round((v - lv) / lv, 4)); |
| | | if (y != null) c.setCellValue(y); |
| | | } |
| | | } |
| | | |
| | |
| | | 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); |
| | | } |
| | | // 总行值列:一律保留模板公式(1~6 月原公式 + 扩列克隆出来的公式), |
| | | // 由生成收尾的 recalcWorkbookFormulas 求值写回缓存。早期版本这里 keepCached |
| | | // 会把刚克隆出来的公式清成空格,导致 7 月及以后总行永远空白。 |
| | | 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! 同比公式 → 清空 |
| | | blankFutureMonthColumns(sheet, fillMonths); // 报表期之后的月份列不保留克隆公式/0 值 |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 清空「报表期之后」的月份列(扩列克隆出来的 7~12 月里、尚未到期的月份)。 |
| | | * 不清的话,克隆出来的总行公式会算出 0,报表上会出现"9~12 月全是 0"的假数据; |
| | | * 累计列(AA)的公式/缓存值已在重算阶段算好,清空格子不影响它(空 = 0)。 |
| | | */ |
| | | private void blankFutureMonthColumns(Sheet sheet, int filledMonths) { |
| | | for (int m = filledMonths + 1; m <= 12; m++) { |
| | | int col = 2 * m; // 0 基:1 月=2(C)、7 月=14(O)、12 月=24(Y) |
| | | for (int r = 4; r <= sheet.getLastRowNum(); r++) { // 0 基第 4 行 = 第 5 行(首个数据行),表头不动 |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | Cell val = row.getCell(col); |
| | | if (val != null) val.setBlank(); |
| | | Cell yoy = row.getCell(col + 1); |
| | | if (yoy != null) yoy.setBlank(); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | return (col - 1) / 2; |
| | | } |
| | | |
| | | /** 填中口径数据单元格:出租/网约车(3/4)显式 0,班线/公交填库内值(无值清空) */ |
| | | /** |
| | | * 填中口径数据单元格:班线/公交/出租城乡/网约车城乡 四维统一取库内值。 |
| | | * 库内该月该维无源(取值为 0)时**保留母版/模板原值**——绝不写空、也不写 0。 |
| | | * 依据:模板的城市级城乡行是「台账页 总量 − 城市内」的引用、并已业务定稿; |
| | | * 用库内缺失值覆盖会把定稿数清掉(如 2026-01~06 网约车订单缺失时, |
| | | * 全省网约车城乡 333.39 与各市州值会被清空)。 |
| | | */ |
| | | 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); |
| | | if (v == 0.0) return; // 库内无源:保留母版/定稿原值,缺数据不覆盖 |
| | | // POI setCellValue(double) 对公式单元格只更新缓存不移除公式,必须先 setBlank 再写值 |
| | | c.setBlank(); |
| | | if (v != 0.0) c.setCellValue(round(v, 4)); |
| | | c.setCellValue(round(v, 4)); |
| | | } |
| | | |
| | | /** 显式写入 0(POI setCellValue(0.0) 会转 blank,需操作底层 XML) */ |
| | |
| | | return volume ? arr[k][0] : arr[k][1]; |
| | | } |
| | | |
| | | /** B3:登记「该期该市州该维度有源行」。判据是源表有没有这一行,而不是算出来的值是否非 0 |
| | | * ——因为「值为 0」可能是真实业务(如武汉市巡游出租全部为城市内,城际城乡恒为 0)。 */ |
| | | private void markMid(Map<Integer, Map<String, boolean[]>> pres, int key, String city, int k) { |
| | | pres.computeIfAbsent(key, kk -> new HashMap<>()) |
| | | .computeIfAbsent(city, kk -> new boolean[5])[k] = true; |
| | | } |
| | | |
| | | /** |
| | | * B3:该月该维度是否「有源」(同比完备性校验用)。 |
| | | * k <= 0 表示「总量行」,要求四维(班线/公交/出租/网约车)都有源才算齐全。 |
| | | */ |
| | | private boolean midMonthHas(Map<Integer, Map<String, double[][]>> mid, int year, int month, String area, int k, boolean volume) { |
| | | Map<String, boolean[]> pm = midPresence.get().get(year * 100 + month); |
| | | if (pm == null) return false; |
| | | boolean[] b = pm.get(area); |
| | | if (b == null) return false; |
| | | if (k <= 0) { |
| | | for (int kk = 1; kk <= 4; kk++) { |
| | | if (!b[kk]) return false; |
| | | } |
| | | return true; |
| | | } |
| | | return k < b.length && b[k]; |
| | | } |
| | | |
| | | /** B3:去年 1..monthCount 月的命中月份数(用于判定同比基数是否齐全) */ |
| | | private int midCumHitMonths(Map<Integer, Map<String, double[][]>> mid, int year, String area, int monthCount, int k, boolean volume) { |
| | | int n = 0; |
| | | for (int m = 1; m <= monthCount; m++) { |
| | | if (midMonthHas(mid, year, m, area, k, volume)) n++; |
| | | } |
| | | return n; |
| | | } |
| | | |
| | | /** B3:当月同比——去年同月基数缺失则留空(缺数据不再当 0 计算) */ |
| | | private Double midMonthYoy(Map<Integer, Map<String, double[][]>> mid, int year, int month, String area, int k, boolean volume) { |
| | | if (!midMonthHas(mid, year - 1, month, area, k, volume)) return null; |
| | | double v = midClassVal(mid, year, month, area, k, volume); |
| | | if (v == 0.0) return null; |
| | | return yoyOf(v, midClassVal(mid, year - 1, month, area, k, volume)); |
| | | } |
| | | |
| | | /** B3:累计同比——去年 1..monthCount 各月齐全才计算,缺任何一月一律留空 */ |
| | | private Double midCumYoy(Map<Integer, Map<String, double[][]>> mid, int year, String area, int monthCount, int k, boolean volume) { |
| | | if (midCumHitMonths(mid, year - 1, area, monthCount, k, volume) < monthCount) return null; |
| | | double v = midCumClass(mid, year, area, monthCount, k, volume); |
| | | if (v == 0.0) return null; |
| | | return yoyOf(v, midCumClass(mid, year - 1, area, monthCount, k, volume)); |
| | | } |
| | | |
| | | /** 公式单元格替换为缓存数值(避免跨簿引用断链;非数值缓存置空) */ |
| | | private void keepCached(Cell c) { |
| | | try { |
| | |
| | | * 中口径分类月度数据:年*100+月 -> (市州/全省 -> double[5][2]) |
| | | * 维度 0=总量 1=公路班线(h2031+个体) 2=城际城乡公交(cityBus) 3=巡游出租 4=网约车;[客运量(万人), 周转量(万人公里)] |
| | | */ |
| | | /** B3:最近一次 loadMidClassMap 的「维度是否有源行」登记表(key -> city -> boolean[5]),用于同比完备性校验 */ |
| | | private final ThreadLocal<Map<Integer, Map<String, boolean[]>>> midPresence = |
| | | ThreadLocal.withInitial(HashMap::new); |
| | | |
| | | /** B4/B5:最近一次 loadMidClassMap 的数据问题清单(线程内,随导出体检输出/写生成件) */ |
| | | private final ThreadLocal<java.util.List<String>> midIssues = ThreadLocal.withInitial(java.util.ArrayList::new); |
| | | |
| | | /** B4/B5:取最近一次中口径数据加载的问题清单(只读副本) */ |
| | | public java.util.List<String> getLastMidIssues() { return new java.util.ArrayList<>(midIssues.get()); } |
| | | |
| | | private Map<Integer, Map<String, double[][]>> loadMidClassMap() { |
| | | midIssues.get().clear(); // B5:本次加载的问题清单 |
| | | Map<Integer, Map<String, boolean[]>> pres = new HashMap<>(); |
| | | midPresence.set(pres); // B3:本次加载的维度有源登记 |
| | | Map<Integer, Map<String, double[][]>> mid = new HashMap<>(); |
| | | for (PassengerEnterpriseMonthly e : passengerMapper.selectList(null)) { |
| | | if (e.getReportPeriod() == null) continue; |
| | |
| | | + nz(e.getTurnoverClass4()) + nz(e.getTurnoverCharter())) / 10000.0; |
| | | addMidClass(mid, key, city, 1, pass, turn); |
| | | addMidClass(mid, key, "湖北省", 1, pass, turn); |
| | | markMid(pres, key, city, 1); |
| | | markMid(pres, key, "湖北省", 1); |
| | | } |
| | | for (PassengerIndividualMonthly e : passengerIndividualMapper.selectList(null)) { // 个体并入公路班线/总量(09-03 口径) |
| | | if (e.getReportPeriod() == null) continue; |
| | |
| | | double turn = nz(e.getTurnover()) / 10000.0; |
| | | addMidClass(mid, key, city, 1, pass, turn); |
| | | addMidClass(mid, key, "湖北省", 1, pass, turn); |
| | | markMid(pres, key, city, 1); |
| | | markMid(pres, key, "湖北省", 1); |
| | | } |
| | | for (CityBusMonthly b : cityBusMapper.selectList(null)) { |
| | | if (b.getReportPeriod() == null || b.getCity() == null) continue; |
| | |
| | | String city = RegionUtil.normalizeCityName(b.getCity()); |
| | | if (city == null) continue; |
| | | int key = y * 100 + m; |
| | | addMidClass(mid, key, city, 2, nz(b.getPassengerChengxiang()), nz(b.getTurnoverChengxiang())); |
| | | addMidClass(mid, key, "湖北省", 2, nz(b.getPassengerChengxiang()), nz(b.getTurnoverChengxiang())); |
| | | // 口径统一(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); |
| | | addMidClass(mid, key, "湖北省", 2, cxPass, cxTurn); |
| | | markMid(pres, key, city, 2); |
| | | markMid(pres, key, "湖北省", 2); |
| | | } |
| | | // 维度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); |
| | | markMid(pres, key, city, 3); |
| | | markMid(pres, key, "湖北省", 3); |
| | | } |
| | | // 维度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) { |
| | | // B5:不再静默跳过——记录「期 + 原因」并写日志,随导出体检一并输出 |
| | | String msg = per + " 网约车城际城乡维度缺失:" + e.getMessage(); |
| | | midIssues.get().add(msg); |
| | | log.warn("loadMidClassMap {}", msg); |
| | | continue; |
| | | } |
| | | 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()); |
| | | markMid(pres, key, en.getKey(), 4); |
| | | markMid(pres, key, "湖北省", 4); |
| | | } |
| | | } |
| | | 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) */ |
| | |
| | | title.getCell(10).setCellValue(currentYear + "年" + maxMonth + "月全省分市州完成道路客运生产情况"); |
| | | } |
| | | // 全省行 r3:B3/F3 为 SUM 公式保留;D3/H3 同比填值(0-based 列 3/7) |
| | | setValOrBlank(sheet, 2, 3, yoyOf(midCumClass(mid, currentYear, "湖北省", maxMonth, 0, true), |
| | | midCumClass(mid, currentYear - 1, "湖北省", maxMonth, 0, true))); |
| | | setValOrBlank(sheet, 2, 7, yoyOf(midCumClass(mid, currentYear, "湖北省", maxMonth, 0, false), |
| | | midCumClass(mid, currentYear - 1, "湖北省", maxMonth, 0, false))); |
| | | setValOrBlank(sheet, 2, 3, midCumYoy(mid, currentYear, "湖北省", maxMonth, 0, true)); |
| | | setValOrBlank(sheet, 2, 7, midCumYoy(mid, currentYear, "湖北省", maxMonth, 0, false)); |
| | | // 右块全省行 N3/R3:当月同比(L3/P3 为 SUM 公式保留) |
| | | setValOrBlank(sheet, 2, 13, yoyOf(midClassVal(mid, currentYear, maxMonth, "湖北省", 0, true), |
| | | midClassVal(mid, currentYear - 1, maxMonth, "湖北省", 0, true))); |
| | | setValOrBlank(sheet, 2, 17, yoyOf(midClassVal(mid, currentYear, maxMonth, "湖北省", 0, false), |
| | | midClassVal(mid, currentYear - 1, maxMonth, "湖北省", 0, false))); |
| | | setValOrBlank(sheet, 2, 13, midMonthYoy(mid, currentYear, maxMonth, "湖北省", 0, true)); |
| | | setValOrBlank(sheet, 2, 17, midMonthYoy(mid, currentYear, maxMonth, "湖北省", 0, false)); |
| | | // 市州行 r4-20:B/F 累计值、D/H 同比;C/E/G/I 排名公式保留 |
| | | List<String> cities = RegionUtil.CITY_LIST; |
| | | for (int i = 0; i < cities.size(); i++) { |
| | |
| | | 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, 3, yoyOf(pass, lastPass)); |
| | | setValOrBlank(sheet, r0, 3, midCumYoy(mid, currentYear, city, maxMonth, 0, true)); |
| | | setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4)); |
| | | setValOrBlank(sheet, r0, 7, yoyOf(turn, lastTurn)); |
| | | 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, 13, mPass == 0 ? null : yoyOf(mPass, mLastPass)); |
| | | 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, 17, mTurn == 0 ? null : yoyOf(mTurn, mLastTurn)); |
| | | setValOrBlank(sheet, r0, 17, mTurn == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, city, 0, false)); |
| | | } |
| | | recalc(wb); |
| | | return toBytes(wb); |
| | |
| | | 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, 2, yoyOf(pass, lastPass)); |
| | | setValOrBlank(sheet, r0, 2, midCumYoy(mid, currentYear, "湖北省", maxMonth, dims[i], true)); |
| | | setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4)); |
| | | setValOrBlank(sheet, r0, 6, yoyOf(turn, lastTurn)); |
| | | setValOrBlank(sheet, r0, 6, midCumYoy(mid, currentYear, "湖北省", maxMonth, dims[i], false)); |
| | | } |
| | | // 下半块(当月):标题行9、数据行11-15(0-based 8、10-14) |
| | | Row bottomTitle = sheet.getRow(8); |
| | |
| | | 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, 2, pass == 0 ? null : yoyOf(pass, lastPass)); |
| | | 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, 6, turn == 0 ? null : yoyOf(turn, lastTurn)); |
| | | setValOrBlank(sheet, r0, 6, turn == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, "湖北省", dims[i], false)); |
| | | } |
| | | // 下半块占比公式由样例指向上半块(B$3/F$3),改写为本块总行(B$11/F$11),行12-15 |
| | | for (int i = 1; i < names.length; i++) { |
| | |
| | | int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth); |
| | | int monthFormulaFilled = summaryWorkbookFiller.ensureMonthFormulas(wb, year, effMonth); |
| | | int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month"); |
| | | int cumulativeRefreshed = refreshSummaryCumulativeSheets(wb, year, effMonth); // 四张跨页合成页随报表期刷新 |
| | | int widthApplied = applySummaryReferenceLayout(wb); |
| | | log.info("汇总工作簿数据驱动回填完成:写入当月值格数={},补写当月同比公式格数={},补写当月合计公式格数={}," |
| | | + "刷新排名页数值格数={},按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}", |
| | | dbFilled, yoyFilled, monthFormulaFilled, rankRefreshed, widthApplied, dbProblems); |
| | | + "刷新排名页数值格数={},刷新四页(城市汇总/城市分市州/中口径分析/道路运输周转量)数值格数={}," |
| | | + "按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}", |
| | | dbFilled, yoyFilled, monthFormulaFilled, rankRefreshed, cumulativeRefreshed, widthApplied, dbProblems); |
| | | 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); |
| | | } finally { |
| | | ZipSecureFile.setMinInflateRatio(savedZipRatio); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 汇总工作簿公式重算: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}; |
| | | } |
| | | try { |
| | | // 跨簿公式([1]出租!C13 之类)在外部工作簿缺失时默认抛异常,会让引用它的 |
| | | // 总行/累计/派生公式连锁失败(表现为「公式还在但没有缓存值」)。 |
| | | // 置 true 后改用公式格自身的缓存值(即定稿值)参与计算。 |
| | | ev.setIgnoreMissingWorkbooks(true); |
| | | } catch (Exception ignore) { |
| | | // 个别实现不支持该开关,忽略即可 |
| | | } |
| | | 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>, |
| | | * 不会清掉旧 <is>,于是产出 <c t="n"><v>67.0</v><is><t>67.00</t></is></c> 这种非法格, |
| | | * Excel 判定为「无法读取的内容」而直接拒绝打开(COM 表现为「不能取得类 Workbooks 的 Open 属性」)。 |
| | | * 2026-07 导出件即因此打不开(《公交》17 格 +《轨道、轮渡》2 格)。落盘前统一清理,幂等。 |
| | | */ |
| | | private int normalizeStaleInlineStrings(XSSFWorkbook wb) { |
| | | int fixed = 0; |
| | | for (int i = 0; i < wb.getNumberOfSheets(); i++) { |
| | | org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorksheet ctw = wb.getSheetAt(i).getCTWorksheet(); |
| | | if (ctw == null || ctw.getSheetData() == null) continue; |
| | | for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRow row : ctw.getSheetData().getRowArray()) { |
| | | for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCell c : row.getCArray()) { |
| | | if (!c.isSetIs()) continue; |
| | | STCellType.Enum t = c.getT(); |
| | | if (t == null || !STCellType.INLINE_STR.equals(t)) { |
| | | c.unsetIs(); |
| | | fixed++; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return fixed; |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** 定位版式参照件 docs/生成汇总大表/生成_道路运输量汇总表.xlsx(优先级同母版:配置目录 → user.dir 相对 → 上级) */ |
| | | // ==================== 汇总大表:四张“跨页合成”页随报表期刷新 ==================== |
| | | // 城市汇总 / 城市分市州 / 中口径分析 / 道路运输周转量 此前整页沿用母版静态值(标题停在 1-7 月)。 |
| | | // 本段按 2026-09-13 已验证口径(与 8 月母版逐格 0 差异)从母版台账页现算: |
| | | // 城市客运量 = 城市内公交 + 城市内巡游出租 + 城市内网约车 + 轨道 + 轮渡 |
| | | // 道路客运周转量 = 公路班线 + 城际城乡公交 + 城际城乡巡游出租 + 城际城乡网约车 |
| | | // 道路货运周转量 = 货运页 市州 规上货物周转量 + 规下货物周转量 |
| | | // 2026 年 1..N 月取台账页 2026 列(C/E/G…),去年同期取母版缓存 2025 列(V/X/Z…)——库里没有 2025 年城市客运数据。 |
| | | |
| | | /** 城市客运口径指标位(0..17 城市客运,18 货运周转量) */ |
| | | private static final int SM_BUS_PAX = 0, SM_BUS_TURN = 1; |
| | | private static final int SM_TAXI_PAX = 2, SM_TAXI_TURN = 3; |
| | | private static final int SM_WYC_PAX = 4, SM_WYC_TURN = 5; |
| | | private static final int SM_RAIL_PAX = 6, SM_RAIL_TURN = 7; |
| | | private static final int SM_FERRY_PAX = 8, SM_FERRY_TURN = 9; |
| | | private static final int SM_BX_PAX = 10, SM_BX_TURN = 11; |
| | | private static final int SM_BUS_CX_PAX = 12, SM_BUS_CX_TURN = 13; |
| | | private static final int SM_TAXI_CX_PAX = 14, SM_TAXI_CX_TURN = 15; |
| | | private static final int SM_WYC_CX_PAX = 16, SM_WYC_CX_TURN = 17; |
| | | private static final int SM_FREIGHT_TURN = 18; |
| | | private static final int SM_DIM = 19; |
| | | /** 2026 年 1 月当月值列(0 基,C 列);2025 年 1 月当月值列(0 基,V 列) */ |
| | | private static final int SM_CUR_COL0 = 2; |
| | | private static final int SM_LAST_COL0 = 21; |
| | | |
| | | /** |
| | | * 刷新四张跨页合成页:当年累计(1..month 月)、当月(month 月)及对应同比; |
| | | * 2026 与 2025 同期都取自母版台账页缓存,返回写入单元格数。 |
| | | */ |
| | | private int refreshSummaryCumulativeSheets(XSSFWorkbook wb, int year, int month) { |
| | | if (wb == null || month <= 0) return 0; |
| | | int[] cumMonths = new int[month]; |
| | | for (int m = 1; m <= month; m++) cumMonths[m - 1] = m; |
| | | int[] oneMonth = new int[]{month}; |
| | | Map<String, double[]> curCum = smReadLedgerMetrics(wb, true, cumMonths); |
| | | Map<String, double[]> lastCum = smReadLedgerMetrics(wb, false, cumMonths); |
| | | Map<String, double[]> curMon = smReadLedgerMetrics(wb, true, oneMonth); |
| | | Map<String, double[]> lastMon = smReadLedgerMetrics(wb, false, oneMonth); |
| | | if (curCum.isEmpty()) { |
| | | log.warn("汇总大表四页刷新:台账页未读到市州数据,跳过(保持母版原值)"); |
| | | return 0; |
| | | } |
| | | double[] pCurCum = smProvinceSum(curCum); |
| | | double[] pLastCum = smProvinceSum(lastCum); |
| | | double[] pCurMon = smProvinceSum(curMon); |
| | | double[] pLastMon = smProvinceSum(lastMon); |
| | | int n = 0; |
| | | n += smWriteCitySummary(wb, year, month, pCurCum, pLastCum, pCurMon, pLastMon); |
| | | n += smWriteCityByRegion(wb, month, curCum, lastCum, curMon, lastMon); |
| | | n += smWriteMidAnalysis(wb, month, pCurCum, pLastCum, pCurMon, pLastMon); |
| | | n += smWriteTurnoverSheets(wb, year, month, curCum, lastCum, curMon, lastMon); |
| | | return n; |
| | | } |
| | | |
| | | /** 17 市州合计(不含台账页“全省”公式行) */ |
| | | private double[] smProvinceSum(Map<String, double[]> m) { |
| | | double[] p = new double[SM_DIM]; |
| | | for (String c : RegionUtil.cityList()) { |
| | | double[] a = m.get(c); |
| | | if (a == null) continue; |
| | | for (int i = 0; i < SM_DIM; i++) p[i] += a[i]; |
| | | } |
| | | return p; |
| | | } |
| | | |
| | | /** 台账页累计取数:市州规范名 -> 19 维指标 */ |
| | | private Map<String, double[]> smReadLedgerMetrics(XSSFWorkbook wb, boolean curYear, int[] months) { |
| | | Map<String, double[]> out = new LinkedHashMap<>(); |
| | | smReadCityPaxPage(wb, "公交", curYear, months, out, SM_BUS_PAX, SM_BUS_TURN, SM_BUS_CX_PAX, SM_BUS_CX_TURN); |
| | | smReadCityPaxPage(wb, "出租", curYear, months, out, SM_TAXI_PAX, SM_TAXI_TURN, SM_TAXI_CX_PAX, SM_TAXI_CX_TURN); |
| | | smReadCityPaxPage(wb, "网约车", curYear, months, out, SM_WYC_PAX, SM_WYC_TURN, SM_WYC_CX_PAX, SM_WYC_CX_TURN); |
| | | smReadRailFerry(wb, curYear, months, out); |
| | | smReadBanxian(wb, curYear, months, out); |
| | | smReadFreightTurnover(wb, curYear, months, out); |
| | | return out; |
| | | } |
| | | |
| | | /** 公交/出租/网约车页:每市州 4 行(客运量/旅客周转量/其中城市内客运量/其中城市内旅客周转量) */ |
| | | private void smReadCityPaxPage(XSSFWorkbook wb, String sheetName, boolean curYear, int[] months, |
| | | Map<String, double[]> out, int paxIdx, int turnIdx, int cxPaxIdx, int cxTurnIdx) { |
| | | Sheet sh = wb.getSheet(sheetName); |
| | | if (sh == null) return; |
| | | int last = sh.getLastRowNum(); |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String city = smCityOf(row.getCell(0)); |
| | | if (city == null) continue; |
| | | int paxRow = -1, turnRow = -1, cxPaxRow = -1, cxTurnRow = -1; |
| | | for (int k = r; k <= Math.min(last, r + 4); k++) { |
| | | Row kr = sh.getRow(k); |
| | | if (kr == null) continue; |
| | | String b = smText(kr.getCell(1)); |
| | | if (b == null || b.contains("个体")) continue; |
| | | boolean inner = b.contains("城市内"); |
| | | boolean turn = b.contains("周转量"); |
| | | if (inner) { |
| | | if (turn) { if (cxTurnRow < 0) cxTurnRow = k; } |
| | | else if (cxPaxRow < 0) cxPaxRow = k; |
| | | } else { |
| | | if (turn) { if (turnRow < 0) turnRow = k; } |
| | | else if (paxRow < 0) paxRow = k; |
| | | } |
| | | } |
| | | double totalPax = smSumMonths(sh, paxRow, curYear, months); |
| | | double totalTurn = smSumMonths(sh, turnRow, curYear, months); |
| | | double cityPax = smSumMonths(sh, cxPaxRow, curYear, months); |
| | | double cityTurn = smSumMonths(sh, cxTurnRow, curYear, months); |
| | | double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]); |
| | | arr[paxIdx] += cityPax; // 城市客运口径取“城市内” |
| | | arr[turnIdx] += cityTurn; |
| | | arr[cxPaxIdx] += totalPax - cityPax; // 城际城乡 = 总量 - 城市内 |
| | | arr[cxTurnIdx] += totalTurn - cityTurn; |
| | | } |
| | | } |
| | | |
| | | /** 轨道、轮渡页:轨道块(武汉/黄石)+ 轮渡块(武汉) */ |
| | | private void smReadRailFerry(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) { |
| | | Sheet sh = wb.getSheet("轨道、轮渡"); |
| | | if (sh == null) return; |
| | | int last = sh.getLastRowNum(); |
| | | int ferryTitle = -1; |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String a = smText(row.getCell(0)); |
| | | if (a != null && a.contains("轮渡客运量")) { ferryTitle = r; break; } |
| | | } |
| | | smReadRailFerryBlock(sh, curYear, months, out, sh.getFirstRowNum(), |
| | | ferryTitle < 0 ? last : ferryTitle - 1, SM_RAIL_PAX); |
| | | if (ferryTitle >= 0) smReadRailFerryBlock(sh, curYear, months, out, ferryTitle, last, SM_FERRY_PAX); |
| | | } |
| | | |
| | | private void smReadRailFerryBlock(Sheet sh, boolean curYear, int[] months, Map<String, double[]> out, |
| | | int start, int end, int baseIdx) { |
| | | for (int r = start; r <= end; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String city = smCityOf(row.getCell(0)); |
| | | if (city == null) continue; |
| | | int paxRow = -1, turnRow = -1; |
| | | for (int k = r; k <= Math.min(end, r + 3); k++) { |
| | | Row kr = sh.getRow(k); |
| | | if (kr == null) continue; |
| | | String b = smText(kr.getCell(1)); |
| | | if (b == null) continue; |
| | | if (b.contains("周转量")) { if (turnRow < 0) turnRow = k; } |
| | | else if (paxRow < 0) paxRow = k; |
| | | } |
| | | double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]); |
| | | arr[baseIdx] += smSumMonths(sh, paxRow, curYear, months); |
| | | arr[baseIdx + 1] += smSumMonths(sh, turnRow, curYear, months); |
| | | } |
| | | } |
| | | |
| | | /** 班线包车页:每市州 客运量 / 旅客周转量(“其中个体”行不重复计入) */ |
| | | private void smReadBanxian(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) { |
| | | Sheet sh = wb.getSheet("班线包车"); |
| | | if (sh == null) return; |
| | | int last = sh.getLastRowNum(); |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String city = smCityOf(row.getCell(0)); |
| | | if (city == null) continue; |
| | | int paxRow = -1, turnRow = -1; |
| | | for (int k = r; k <= Math.min(last, r + 4); k++) { |
| | | Row kr = sh.getRow(k); |
| | | if (kr == null) continue; |
| | | String b = smText(kr.getCell(1)); |
| | | if (b == null || b.contains("个体")) continue; |
| | | if (b.contains("周转量")) { if (turnRow < 0) turnRow = k; } |
| | | else if (b.contains("客运量") && paxRow < 0) paxRow = k; |
| | | } |
| | | double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]); |
| | | arr[SM_BX_PAX] += smSumMonths(sh, paxRow, curYear, months); |
| | | arr[SM_BX_TURN] += smSumMonths(sh, turnRow, curYear, months); |
| | | } |
| | | } |
| | | |
| | | /** 货运页:每市州 规上货物周转量 + 规下货物周转量(“规上+规下”行本身是求和公式,不直接取) */ |
| | | private void smReadFreightTurnover(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) { |
| | | Sheet sh = wb.getSheet(" 货运"); |
| | | if (sh == null) sh = wb.getSheet("货运"); |
| | | if (sh == null) return; |
| | | int last = sh.getLastRowNum(); |
| | | for (int r = sh.getFirstRowNum(); r <= last; r++) { |
| | | Row row = sh.getRow(r); |
| | | if (row == null) continue; |
| | | String city = smCityOf(row.getCell(0)); |
| | | if (city == null) continue; |
| | | int above = -1, below = -1; |
| | | for (int k = r; k <= Math.min(last, r + 6); k++) { |
| | | Row kr = sh.getRow(k); |
| | | if (kr == null) continue; |
| | | String b = smText(kr.getCell(1)); |
| | | if (b == null || b.contains("其中")) continue; |
| | | if (b.contains("规上货物周转量")) { if (above < 0) above = k; } |
| | | else if (b.contains("规下货物周转量")) { if (below < 0) below = k; } |
| | | } |
| | | double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]); |
| | | arr[SM_FREIGHT_TURN] += smSumMonths(sh, above, curYear, months) + smSumMonths(sh, below, curYear, months); |
| | | } |
| | | } |
| | | |
| | | private double smSumMonths(Sheet sh, int rowIdx, boolean curYear, int[] months) { |
| | | if (sh == null || rowIdx < 0) return 0; |
| | | Row row = sh.getRow(rowIdx); |
| | | if (row == null) return 0; |
| | | double s = 0; |
| | | for (int m : months) { |
| | | int col = (curYear ? SM_CUR_COL0 : SM_LAST_COL0) + 2 * (m - 1); |
| | | s += smNum(row.getCell(col)); |
| | | } |
| | | return s; |
| | | } |
| | | |
| | | /** 台账页单元格取数(数字 / 数字文本 / 公式缓存) */ |
| | | private double smNum(Cell c) { |
| | | if (c == null) return 0; |
| | | try { |
| | | CellType t = c.getCellType(); |
| | | if (t == CellType.NUMERIC) return c.getNumericCellValue(); |
| | | if (t == CellType.FORMULA) { |
| | | return c.getCachedFormulaResultType() == CellType.NUMERIC ? c.getNumericCellValue() : 0; |
| | | } |
| | | if (t == CellType.STRING) { |
| | | String s = c.getStringCellValue(); |
| | | if (s == null) return 0; |
| | | s = s.replace(",", "").replace("%", "").trim(); |
| | | if (s.isEmpty()) return 0; |
| | | return Double.parseDouble(s); |
| | | } |
| | | } catch (Exception ignore) { |
| | | return 0; |
| | | } |
| | | return 0; |
| | | } |
| | | |
| | | private String smText(Cell c) { |
| | | if (c == null) return null; |
| | | try { |
| | | return c.getCellType() == CellType.STRING ? c.getStringCellValue() : null; |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** 列 A 文案 -> 规范市州名(“全省/湖北省”与非市州行返回 null) */ |
| | | private String smCityOf(Cell a) { |
| | | String t = smText(a); |
| | | if (t == null || t.trim().isEmpty()) return null; |
| | | String norm = RegionUtil.normalizeCityName(t.trim()); |
| | | if (norm == null) return null; |
| | | return RegionUtil.cityList().contains(norm) ? norm : null; |
| | | } |
| | | |
| | | private double smYoy(double cur, double last) { |
| | | return last == 0.0 ? 0.0 : (cur - last) / last; |
| | | } |
| | | |
| | | /** 写值:公式格不动(保留母版公式,打开即重算);0 且原为空格则不新增 */ |
| | | private int smSetVal(Sheet sh, int r0, int c0, double v) { |
| | | Row row = sh.getRow(r0); |
| | | if (row == null) return 0; |
| | | Cell c = row.getCell(c0); |
| | | if (c != null && c.getCellType() == CellType.FORMULA) return 0; |
| | | if (c == null) { |
| | | if (v == 0.0) return 0; |
| | | c = row.createCell(c0); |
| | | Cell left = row.getCell(c0 - 1); |
| | | if (left != null) c.setCellStyle(left.getCellStyle()); |
| | | } else if (v == 0.0 && c.getCellType() == CellType.BLANK) { |
| | | return 0; |
| | | } |
| | | c.setBlank(); |
| | | c.setCellValue(v); |
| | | return 1; |
| | | } |
| | | |
| | | private void smSetTitle(Sheet sh, int r0, int c0, String text) { |
| | | if (sh == null) return; |
| | | Row row = sh.getRow(r0); |
| | | if (row == null) return; |
| | | Cell c = row.getCell(c0); |
| | | if (c == null || c.getCellType() == CellType.FORMULA) return; |
| | | c.setBlank(); |
| | | c.setCellValue(text); |
| | | } |
| | | |
| | | /** 城市汇总页:块1(1..N 月累计)行 3-9,块2(当月)行 14-20;列 B/C/D/E = 客运量/同比/周转量/同比 */ |
| | | private int smWriteCitySummary(XSSFWorkbook wb, int year, int month, |
| | | double[] pCurCum, double[] pLastCum, double[] pCurMon, double[] pLastMon) { |
| | | Sheet sh = wb.getSheet("城市汇总"); |
| | | if (sh == null) return 0; |
| | | smSetTitle(sh, 0, 0, year + "年1-" + month + "月全省累计完成城市客运量情况"); |
| | | smSetTitle(sh, 11, 0, year + "年" + month + "月全省完成城市客运量情况"); |
| | | int n = 0; |
| | | int[] rows = {2, 3, 4, 5, 6, 7, 8}; |
| | | for (int i = 0; i < rows.length; i++) { |
| | | double[] v = smCitySummaryRow(pCurCum, pLastCum, i); |
| | | n += smSetVal(sh, rows[i], 1, v[0]); |
| | | n += smSetVal(sh, rows[i], 2, v[2]); |
| | | n += smSetVal(sh, rows[i], 3, v[1]); |
| | | n += smSetVal(sh, rows[i], 4, v[3]); |
| | | } |
| | | for (int i = 0; i < rows.length; i++) { |
| | | double[] v = smCitySummaryRow(pCurMon, pLastMon, i); |
| | | n += smSetVal(sh, rows[i] + 11, 1, v[0]); |
| | | n += smSetVal(sh, rows[i] + 11, 2, v[2]); |
| | | n += smSetVal(sh, rows[i] + 11, 3, v[1]); |
| | | n += smSetVal(sh, rows[i] + 11, 4, v[3]); |
| | | } |
| | | return n; |
| | | } |
| | | |
| | | /** i=0 总客运量 1 公交 2 出租车 3 巡游出租 4 网约车 5 轨道 6 轮渡;返回 [客运量, 周转量, 客运量同比, 周转量同比] */ |
| | | private double[] smCitySummaryRow(double[] p, double[] pl, int i) { |
| | | double pax, turn, lPax, lTurn; |
| | | if (i == 0) { |
| | | pax = p[SM_BUS_PAX] + p[SM_TAXI_PAX] + p[SM_WYC_PAX] + p[SM_RAIL_PAX] + p[SM_FERRY_PAX]; |
| | | turn = p[SM_BUS_TURN] + p[SM_TAXI_TURN] + p[SM_WYC_TURN] + p[SM_RAIL_TURN] + p[SM_FERRY_TURN]; |
| | | lPax = pl[SM_BUS_PAX] + pl[SM_TAXI_PAX] + pl[SM_WYC_PAX] + pl[SM_RAIL_PAX] + pl[SM_FERRY_PAX]; |
| | | lTurn = pl[SM_BUS_TURN] + pl[SM_TAXI_TURN] + pl[SM_WYC_TURN] + pl[SM_RAIL_TURN] + pl[SM_FERRY_TURN]; |
| | | } else if (i == 1) { |
| | | pax = p[SM_BUS_PAX]; turn = p[SM_BUS_TURN]; lPax = pl[SM_BUS_PAX]; lTurn = pl[SM_BUS_TURN]; |
| | | } else if (i == 2) { |
| | | pax = p[SM_TAXI_PAX] + p[SM_WYC_PAX]; turn = p[SM_TAXI_TURN] + p[SM_WYC_TURN]; |
| | | lPax = pl[SM_TAXI_PAX] + pl[SM_WYC_PAX]; lTurn = pl[SM_TAXI_TURN] + pl[SM_WYC_TURN]; |
| | | } else if (i == 3) { |
| | | pax = p[SM_TAXI_PAX]; turn = p[SM_TAXI_TURN]; lPax = pl[SM_TAXI_PAX]; lTurn = pl[SM_TAXI_TURN]; |
| | | } else if (i == 4) { |
| | | pax = p[SM_WYC_PAX]; turn = p[SM_WYC_TURN]; lPax = pl[SM_WYC_PAX]; lTurn = pl[SM_WYC_TURN]; |
| | | } else if (i == 5) { |
| | | pax = p[SM_RAIL_PAX]; turn = p[SM_RAIL_TURN]; lPax = pl[SM_RAIL_PAX]; lTurn = pl[SM_RAIL_TURN]; |
| | | } else { |
| | | pax = p[SM_FERRY_PAX]; turn = p[SM_FERRY_TURN]; lPax = pl[SM_FERRY_PAX]; lTurn = pl[SM_FERRY_TURN]; |
| | | } |
| | | return new double[]{pax, turn, smYoy(pax, lPax), smYoy(turn, lTurn)}; |
| | | } |
| | | |
| | | /** 城市分市州页:4 块(累计客运量/累计周转量/当月客运量/当月周转量),每块 全省 1 行 + 17 市州 */ |
| | | private int smWriteCityByRegion(XSSFWorkbook wb, int month, |
| | | Map<String, double[]> curCum, Map<String, double[]> lastCum, |
| | | Map<String, double[]> curMon, Map<String, double[]> lastMon) { |
| | | Sheet sh = wb.getSheet("城市分市州"); |
| | | if (sh == null) return 0; |
| | | smSetTitle(sh, 0, 0, "城市客运客运量分市州1-" + month + "月累计情况"); |
| | | smSetTitle(sh, 22, 0, "城市客运客运周转量分市州1-" + month + "月累计情况"); |
| | | smSetTitle(sh, 45, 0, "城市客运客运量分市州" + month + "月情况"); |
| | | smSetTitle(sh, 67, 0, "城市客运客运周转量分市州" + month + "月情况"); |
| | | int n = 0; |
| | | n += smWriteCityByRegionBlock(sh, 4, true, curCum, lastCum); |
| | | n += smWriteCityByRegionBlock(sh, 26, false, curCum, lastCum); |
| | | n += smWriteCityByRegionBlock(sh, 49, true, curMon, lastMon); |
| | | n += smWriteCityByRegionBlock(sh, 71, false, curMon, lastMon); |
| | | return n; |
| | | } |
| | | |
| | | private int smWriteCityByRegionBlock(Sheet sh, int provRow, boolean pax, |
| | | Map<String, double[]> cur, Map<String, double[]> last) { |
| | | int n = 0; |
| | | double[] provCur = smProvinceSum(cur); |
| | | double[] provLast = smProvinceSum(last); |
| | | List<String> cities = RegionUtil.cityList(); |
| | | for (int i = -1; i < cities.size(); i++) { |
| | | int r = provRow + i + 1; |
| | | double[] c = i < 0 ? provCur : cur.get(cities.get(i)); |
| | | double[] l = i < 0 ? provLast : last.get(cities.get(i)); |
| | | if (c == null) c = new double[SM_DIM]; |
| | | if (l == null) l = new double[SM_DIM]; |
| | | double[] g = smCityGroupValues(c, l, pax); |
| | | n += smSetVal(sh, r, 1, g[0]); |
| | | n += smSetVal(sh, r, 4, g[6]); |
| | | n += smSetVal(sh, r, 6, g[1]); |
| | | n += smSetVal(sh, r, 9, g[7]); |
| | | n += smSetVal(sh, r, 11, g[2]); |
| | | n += smSetVal(sh, r, 14, g[8]); |
| | | n += smSetVal(sh, r, 16, g[3]); |
| | | n += smSetVal(sh, r, 19, g[9]); |
| | | n += smSetVal(sh, r, 21, g[4]); |
| | | n += smSetVal(sh, r, 22, g[10]); |
| | | n += smSetVal(sh, r, 23, g[5]); |
| | | n += smSetVal(sh, r, 24, g[11]); |
| | | } |
| | | return n; |
| | | } |
| | | |
| | | /** 6 组值 + 6 组增速:[总量, 公交, 出租, 网约车, 轨道, 轮渡, 总量增速, 公交…, 轮渡] */ |
| | | private double[] smCityGroupValues(double[] c, double[] l, boolean pax) { |
| | | int busI = pax ? SM_BUS_PAX : SM_BUS_TURN; |
| | | int taxiI = pax ? SM_TAXI_PAX : SM_TAXI_TURN; |
| | | int wycI = pax ? SM_WYC_PAX : SM_WYC_TURN; |
| | | int railI = pax ? SM_RAIL_PAX : SM_RAIL_TURN; |
| | | int ferryI = pax ? SM_FERRY_PAX : SM_FERRY_TURN; |
| | | double bus = c[busI], taxi = c[taxiI], wyc = c[wycI], rail = c[railI], ferry = c[ferryI]; |
| | | double lBus = l[busI], lTaxi = l[taxiI], lWyc = l[wycI], lRail = l[railI], lFerry = l[ferryI]; |
| | | double total = bus + taxi + wyc + rail + ferry; |
| | | double lTotal = lBus + lTaxi + lWyc + lRail + lFerry; |
| | | return new double[]{total, bus, taxi, wyc, rail, ferry, |
| | | smYoy(total, lTotal), smYoy(bus, lBus), smYoy(taxi, lTaxi), smYoy(wyc, lWyc), |
| | | smYoy(rail, lRail), smYoy(ferry, lFerry)}; |
| | | } |
| | | |
| | | /** 中口径分析页:块1(累计)行 3-7,块2(当月)行 12-16;列 B/C/F/G = 客运量/同比/周转量/同比 */ |
| | | private int smWriteMidAnalysis(XSSFWorkbook wb, int month, |
| | | double[] pCurCum, double[] pLastCum, double[] pCurMon, double[] pLastMon) { |
| | | Sheet sh = wb.getSheet("中口径分析"); |
| | | if (sh == null) return 0; |
| | | smSetTitle(sh, 0, 0, "1-" + month + "月中口径客运量及周转量"); |
| | | smSetTitle(sh, 9, 0, month + "月中口径客运量及周转量"); |
| | | int n = 0; |
| | | int[] rows = {2, 3, 4, 5, 6}; |
| | | for (int i = 0; i < rows.length; i++) { |
| | | double[] v = smMidRow(pCurCum, pLastCum, i); |
| | | n += smSetVal(sh, rows[i], 1, v[0]); |
| | | n += smSetVal(sh, rows[i], 2, v[2]); |
| | | n += smSetVal(sh, rows[i], 5, v[1]); |
| | | n += smSetVal(sh, rows[i], 6, v[3]); |
| | | } |
| | | for (int i = 0; i < rows.length; i++) { |
| | | double[] v = smMidRow(pCurMon, pLastMon, i); |
| | | n += smSetVal(sh, rows[i] + 9, 1, v[0]); |
| | | n += smSetVal(sh, rows[i] + 9, 2, v[2]); |
| | | n += smSetVal(sh, rows[i] + 9, 5, v[1]); |
| | | n += smSetVal(sh, rows[i] + 9, 6, v[3]); |
| | | } |
| | | return n; |
| | | } |
| | | |
| | | /** i=0 总 1 公路班线 2 城际城乡公交 3 城际城乡巡游出租 4 城际城乡网约车;返回 [客运量, 周转量, 客运量同比, 周转量同比] */ |
| | | private double[] smMidRow(double[] p, double[] pl, int i) { |
| | | int[] paxIdx = {SM_BX_PAX, SM_BX_PAX, SM_BUS_CX_PAX, SM_TAXI_CX_PAX, SM_WYC_CX_PAX}; |
| | | int[] turnIdx = {SM_BX_TURN, SM_BX_TURN, SM_BUS_CX_TURN, SM_TAXI_CX_TURN, SM_WYC_CX_TURN}; |
| | | double pax, turn, lPax, lTurn; |
| | | if (i == 0) { |
| | | pax = p[SM_BX_PAX] + p[SM_BUS_CX_PAX] + p[SM_TAXI_CX_PAX] + p[SM_WYC_CX_PAX]; |
| | | turn = p[SM_BX_TURN] + p[SM_BUS_CX_TURN] + p[SM_TAXI_CX_TURN] + p[SM_WYC_CX_TURN]; |
| | | lPax = pl[SM_BX_PAX] + pl[SM_BUS_CX_PAX] + pl[SM_TAXI_CX_PAX] + pl[SM_WYC_CX_PAX]; |
| | | lTurn = pl[SM_BX_TURN] + pl[SM_BUS_CX_TURN] + pl[SM_TAXI_CX_TURN] + pl[SM_WYC_CX_TURN]; |
| | | } else { |
| | | pax = p[paxIdx[i]]; turn = p[turnIdx[i]]; |
| | | lPax = pl[paxIdx[i]]; lTurn = pl[turnIdx[i]]; |
| | | } |
| | | return new double[]{pax, turn, smYoy(pax, lPax), smYoy(turn, lTurn)}; |
| | | } |
| | | |
| | | /** 道路运输周转量页:块1(累计)行 4-21,块2(当月)行 26-43;只写值列(B/E/V 等公式保留) */ |
| | | private int smWriteTurnoverSheets(XSSFWorkbook wb, int year, int month, |
| | | Map<String, double[]> curCum, Map<String, double[]> lastCum, |
| | | Map<String, double[]> curMon, Map<String, double[]> lastMon) { |
| | | Sheet sh = wb.getSheet("道路运输周转量"); |
| | | if (sh == null) return 0; |
| | | smSetTitle(sh, 0, 0, year + "年1-" + month + "月全省累计完成交通周转量情况"); |
| | | smSetTitle(sh, 22, 0, year + "年" + month + "月全省完成交通周转量情况"); |
| | | int n = 0; |
| | | n += smWriteTurnoverBlock(sh, 3, curCum, lastCum); |
| | | n += smWriteTurnoverBlock(sh, 25, curMon, lastMon); |
| | | return n; |
| | | } |
| | | |
| | | private int smWriteTurnoverBlock(Sheet sh, int provRow, Map<String, double[]> cur, Map<String, double[]> last) { |
| | | int n = 0; |
| | | double[] provCur = smProvinceSum(cur); |
| | | double[] provLast = smProvinceSum(last); |
| | | List<String> cities = RegionUtil.cityList(); |
| | | for (int i = -1; i < cities.size(); i++) { |
| | | int r = provRow + i + 1; |
| | | double[] c = i < 0 ? provCur : cur.get(cities.get(i)); |
| | | double[] l = i < 0 ? provLast : last.get(cities.get(i)); |
| | | if (c == null) c = new double[SM_DIM]; |
| | | if (l == null) l = new double[SM_DIM]; |
| | | double freight = c[SM_FREIGHT_TURN], lFreight = l[SM_FREIGHT_TURN]; |
| | | double pax = c[SM_BUS_PAX] + c[SM_TAXI_PAX] + c[SM_WYC_PAX] + c[SM_RAIL_PAX] + c[SM_FERRY_PAX]; |
| | | double lPax = l[SM_BUS_PAX] + l[SM_TAXI_PAX] + l[SM_WYC_PAX] + l[SM_RAIL_PAX] + l[SM_FERRY_PAX]; |
| | | double road = c[SM_BX_TURN] + c[SM_BUS_CX_TURN] + c[SM_TAXI_CX_TURN] + c[SM_WYC_CX_TURN]; |
| | | double lRoad = l[SM_BX_TURN] + l[SM_BUS_CX_TURN] + l[SM_TAXI_CX_TURN] + l[SM_WYC_CX_TURN]; |
| | | n += smSetVal(sh, r, 6, freight); |
| | | n += smSetVal(sh, r, 9, smYoy(freight, lFreight)); |
| | | n += smSetVal(sh, r, 11, pax); |
| | | n += smSetVal(sh, r, 14, smYoy(pax, lPax)); |
| | | n += smSetVal(sh, r, 16, road); |
| | | n += smSetVal(sh, r, 19, smYoy(road, lRoad)); |
| | | n += smSetVal(sh, r, 22, lFreight); |
| | | n += smSetVal(sh, r, 23, lPax); |
| | | n += smSetVal(sh, r, 24, lRoad); |
| | | } |
| | | return n; |
| | | } |
| | | |
| | | |
| | | private File resolveSummaryLayoutRef() { |
| | | String rel = summaryTemplateDir; |
| | | while (rel != null && rel.startsWith("./")) rel = rel.substring(2); |
| | |
| | | if (bs == null) continue; |
| | | // 中口径排名页 A..H 陈旧左块已于 2026-09-09 拍板整块删除:该区域永不从备份母版回填,避免“生成又复活” |
| | | boolean skipMidRankLeftBlock = "中口径排名".equals(ds.getSheetName()); |
| | | // 《 货运》页 2025..2021 年度块(T 列起,含各年累计/累计同比列)在母版里是「市州派生公式」, |
| | | // 备份母版同区仍是旧的写死常量(全省规下等),回填会让 2021/2022/2023 累计又对不上(“生成又复活”),故该区不回填。 |
| | | boolean skipFreightHistoryBlocks = " 货运".equals(ds.getSheetName()); |
| | | for (int r = ds.getFirstRowNum(); r <= ds.getLastRowNum(); r++) { |
| | | Row dr = ds.getRow(r); |
| | | if (dr == null) continue; |
| | |
| | | for (Cell dc : dr) { |
| | | if (dc == null) continue; |
| | | if (skipMidRankLeftBlock && dc.getColumnIndex() < 8) continue; |
| | | if (skipFreightHistoryBlocks && dc.getColumnIndex() >= 19) continue; // T 列及以后 = 2025..2021 年度块 |
| | | CellType t = dc.getCellType(); |
| | | if (t == CellType.BLANK) continue; |
| | | String dtx = summaryCellText(dc); |
| | |
| | | case "investFiveYearLogistics": return investFiveYearLogistics(period, mode); |
| | | case "investBillion": return investBillion(period, mode); |
| | | case "investCounty": return investCounty(period, mode); |
| | | case "investPassengerDetail": return exportInvestPassengerDetail(period, mode); |
| | | case "investLogisticsDetail": return exportInvestLogisticsDetail(period, mode); |
| | | case "cityBusDetail": return exportCityBusDetail(period, mode); |
| | | case "cityTaxiDetail": return exportCityTaxiDetail(period, mode); |
| | | case "cityRailFerryDetail": return exportCityRailFerryDetail(period, mode); |
| | |
| | | REPORT_FILE_NAMES.put("investFiveYearLogistics", "生成_十五五规划物流项目进展情况.xls"); |
| | | REPORT_FILE_NAMES.put("investBillion", "生成_湖北省(客货站场)亿元投资项目.xls"); |
| | | REPORT_FILE_NAMES.put("investCounty", "生成_经济强县交通物流基础设施投资统计报表.xls"); |
| | | REPORT_FILE_NAMES.put("investPassengerDetail", "生成_客运站投资明细表.xlsx"); |
| | | REPORT_FILE_NAMES.put("investLogisticsDetail", "生成_物流站场投资明细.xls"); |
| | | REPORT_FILE_NAMES.put("cityBusDetail", "生成_城市公交客运量分市州明细.xlsx"); |
| | | REPORT_FILE_NAMES.put("cityTaxiDetail", "生成_巡游出租客运量分市州明细.xlsx"); |
| | | REPORT_FILE_NAMES.put("cityRailFerryDetail", "生成_轨道轮渡客运量分市州明细.xlsx"); |
| | |
| | | } |
| | | |
| | | |
| | | |
| | | // ==================== 投资模块:客运站投资明细表 / 物流站场投资明细(模板底稿原位替换) ==================== |
| | | |
| | | /** 物流站场明细表模板中的市州小节短名 -> 库内规范全名 */ |
| | | private static final Map<String, String> LOGI_CITY_LABELS = new LinkedHashMap<>(); |
| | | static { |
| | | LOGI_CITY_LABELS.put("武汉", "武汉市"); |
| | | LOGI_CITY_LABELS.put("黄石", "黄石市"); |
| | | LOGI_CITY_LABELS.put("十堰", "十堰市"); |
| | | LOGI_CITY_LABELS.put("宜昌", "宜昌市"); |
| | | LOGI_CITY_LABELS.put("襄阳", "襄阳市"); |
| | | LOGI_CITY_LABELS.put("鄂州", "鄂州市"); |
| | | LOGI_CITY_LABELS.put("荆门", "荆门市"); |
| | | LOGI_CITY_LABELS.put("孝感", "孝感市"); |
| | | LOGI_CITY_LABELS.put("荆州", "荆州市"); |
| | | LOGI_CITY_LABELS.put("黄冈", "黄冈市"); |
| | | LOGI_CITY_LABELS.put("咸宁", "咸宁市"); |
| | | LOGI_CITY_LABELS.put("随州", "随州市"); |
| | | LOGI_CITY_LABELS.put("恩施", "恩施州"); |
| | | LOGI_CITY_LABELS.put("仙桃", "仙桃市"); |
| | | LOGI_CITY_LABELS.put("潜江", "潜江市"); |
| | | LOGI_CITY_LABELS.put("天门", "天门市"); |
| | | LOGI_CITY_LABELS.put("林区", "神农架林区"); |
| | | } |
| | | |
| | | /** 该期各项目月度数据:projectId -> InvestmentMonthly */ |
| | | private Map<Long, InvestmentMonthly> investMonthlyOf(String period) { |
| | | Map<Long, InvestmentMonthly> map = new HashMap<>(); |
| | | for (InvestmentMonthly m : investMonthlyMapper.selectList( |
| | | new LambdaQueryWrapper<InvestmentMonthly>().eq(InvestmentMonthly::getReportPeriod, period))) { |
| | | map.put(m.getProjectId(), m); |
| | | } |
| | | return map; |
| | | } |
| | | |
| | | /** 写单元格数值(保留样式/合并):null 清空 */ |
| | | private void putNum(Row row, int idx, Double v) { |
| | | if (row == null) return; |
| | | Cell c = row.getCell(idx); |
| | | if (c == null) c = row.createCell(idx); |
| | | c.setBlank(); |
| | | if (v != null) c.setCellValue(v); |
| | | } |
| | | |
| | | /** 写单元格文本(保留样式/合并):空串清空 */ |
| | | private void putText(Row row, int idx, String v) { |
| | | if (row == null) return; |
| | | Cell c = row.getCell(idx); |
| | | if (c == null) c = row.createCell(idx); |
| | | c.setBlank(); |
| | | if (v != null && !v.trim().isEmpty()) c.setCellValue(v.trim()); |
| | | } |
| | | |
| | | /** 读单元格数值(空/非数值/公式无缓存一律 0) */ |
| | | private double numOf(Cell c) { |
| | | if (c == null) return 0.0; |
| | | try { |
| | | if (c.getCellType() == CellType.NUMERIC || c.getCellType() == CellType.FORMULA) { |
| | | return c.getNumericCellValue(); |
| | | } |
| | | String s = cellText(c); |
| | | if (s == null) return 0.0; |
| | | s = s.replace(",", "").trim(); |
| | | return s.isEmpty() ? 0.0 : Double.parseDouble(s); |
| | | } catch (Exception e) { |
| | | return 0.0; |
| | | } |
| | | } |
| | | |
| | | /** 模板标题里的「(N月月报)」「(N月)」改为报表期月份 */ |
| | | private void fixInvestMonthTitle(Sheet sheet, int rowIdx, int colIdx, int month) { |
| | | Row row = sheet.getRow(rowIdx); |
| | | if (row == null) return; |
| | | String t = cellText(row.getCell(colIdx)); |
| | | if (t == null) return; |
| | | String nv = t.replaceAll("(\\d+月月报)", "(" + month + "月月报)") |
| | | .replaceAll("(\\d+月)", "(" + month + "月)"); |
| | | if (!nv.equals(t)) putText(row, colIdx, nv); |
| | | } |
| | | |
| | | /** 模板行 A 列是否为「一二三四、」渠道分组标题 */ |
| | | private boolean isInvestChannelLabel(String a) { |
| | | return a != null && a.matches("^[一二三四五六七八九十]+、.*"); |
| | | } |
| | | |
| | | /** |
| | | * 生成_X月客运站投资明细表.xlsx(模板:docs/投资/模板/模板_X月客运站投资明细表.xlsx,sheet:明细 + 汇总)。 |
| | | * 口径:模板即上月台账底稿;本月只覆盖「库内有该期数据」的项目行 |
| | | * (H自开始建设累计 / I本年计划 / J自年初累计 / K本月完成 / L建设阶段 / M形象进度), |
| | | * 库内无该期的项目行保留母版原值(等同人工「上月结转 + 本月更新」); |
| | | * 城市/渠道/合计行沿用模板 SUM 公式,随表内数值自动重算。 |
| | | */ |
| | | public byte[] exportInvestPassengerDetail(String period, String mode) throws Exception { |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, mode); |
| | | List<InvestmentProject> projects = investProjectMapper.selectList( |
| | | new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, "客运站场")); |
| | | Map<Long, InvestmentMonthly> mByProject = investMonthlyOf(period); |
| | | File tpl = resolveAnyTemplate(investTemplateDir, "模板_X月客运站投资明细表.xlsx"); |
| | | try (InputStream in = new FileInputStream(tpl); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | Sheet sheet = wb.getSheet("明细"); |
| | | if (sheet == null) throw new RuntimeException("模板《模板_X月客运站投资明细表.xlsx》缺少「明细」工作表"); |
| | | fixInvestMonthTitle(sheet, 0, 1, month); |
| | | String curCity = null; |
| | | Set<Long> used = new HashSet<>(); |
| | | int filled = 0, kept = 0, dup = 0; |
| | | for (int r = 8; r <= sheet.getLastRowNum(); r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | String b = nvl(cellText(row.getCell(1))); |
| | | if (isCityLabel(a)) { curCity = templateCityFull(a); continue; } |
| | | if (isInvestChannelLabel(a)) continue; |
| | | if (a.contains("合计") || a.contains("总计")) continue; |
| | | if (!a.matches("\\d+(\\.\\d+)?")) continue; |
| | | if (b.isEmpty() || curCity == null) continue; |
| | | InvestmentProject p = matchInvest(projects, b, curCity, "客运站场", used); |
| | | InvestmentMonthly m = p == null ? null : mByProject.get(p.getId()); |
| | | if (p == null || m == null) { |
| | | kept++; |
| | | // 有库内项目但当月无数据 → 记为待核对;无匹配项目 → 结转上月 |
| | | if (p != null) dup++; |
| | | continue; |
| | | } |
| | | used.add(p.getId()); |
| | | // 口径(用户 2026-09-18 确认):只覆盖人工会随月报更新的四列—— |
| | | // H 自开始建设累计 / J 自年初累计 / K 本月完成 / M 形象进度; |
| | | // I 本年计划投资、L 建设阶段、G 计划总投资为省定/母版口径,一律不动;库内为空则保留母版原值 |
| | | if (m.getStartCum() != null) putNum(row, 7, m.getStartCum()); |
| | | if (m.getYearCum() != null) putNum(row, 9, m.getYearCum()); |
| | | if (m.getMonthDone() != null) putNum(row, 10, m.getMonthDone()); |
| | | if (m.getProgressDesc() != null && !m.getProgressDesc().trim().isEmpty()) putText(row, 12, m.getProgressDesc()); |
| | | filled++; |
| | | } |
| | | Sheet sumSheet = wb.getSheet("汇总"); |
| | | if (sumSheet != null) { |
| | | Row r2 = sumSheet.getRow(1); |
| | | if (r2 != null) { |
| | | Cell c = r2.getCell(0); |
| | | if (c == null) c = r2.createCell(0); |
| | | c.setBlank(); |
| | | c.setCellValue(java.time.LocalDate.of(year, month, 1).atStartOfDay()); |
| | | } |
| | | } |
| | | try { |
| | | wb.getCreationHelper().createFormulaEvaluator().evaluateAll(); |
| | | } catch (Exception e) { |
| | | log.warn("客运站投资明细表公式求值失败: {}", e.getMessage()); |
| | | } |
| | | log.info("客运站投资明细表 {}:写入 {} 行,结转母版原值 {} 行(其中库内有项目但当月无数据 {} 行)", |
| | | period, filled, kept, dup); |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 生成_X月物流站场投资明细.xls(模板:docs/投资/模板/模板_X月物流站场投资明细.xls)。 |
| | | * 覆盖「 分项目投资完成情况」项目行(口径同客运站明细表),并按表内数值重算 |
| | | * 渠道行 / 市州行 / 合计行,同步刷新「分市州规划内外项目完成情况」「市州汇总」两张汇总页。 |
| | | */ |
| | | public byte[] exportInvestLogisticsDetail(String period, String mode) throws Exception { |
| | | int year = Integer.parseInt(period.split("-")[0]); |
| | | int month = monthOf(period, mode); |
| | | List<InvestmentProject> projects = investProjectMapper.selectList( |
| | | new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, "物流园区")); |
| | | Map<Long, InvestmentMonthly> mByProject = investMonthlyOf(period); |
| | | HSSFWorkbook wb = loadInvestTemplate("模板_X月物流站场投资明细.xls"); |
| | | HSSFSheet sheet = wb.getSheet(" 分项目投资完成情况"); |
| | | if (sheet == null) throw new RuntimeException("模板《模板_X月物流站场投资明细.xls》缺少「 分项目投资完成情况」工作表"); |
| | | fixInvestMonthTitle(sheet, 0, 0, month); |
| | | Row head = sheet.getRow(3); |
| | | if (head != null) { |
| | | String t = cellText(head.getCell(0)); |
| | | if (t != null) { |
| | | String nv = t.replaceAll("\\d{4}年\\d+月", year + "年" + month + "月"); |
| | | if (!nv.equals(t)) putText(head, 0, nv); |
| | | } |
| | | } |
| | | // 1) 结构扫描:市州小节行 |
| | | List<Integer> cityRows = new ArrayList<>(); |
| | | for (int r = 7; r <= sheet.getLastRowNum(); r++) { |
| | | HSSFRow row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | String b = nvl(cellText(row.getCell(1))); |
| | | if (LOGI_CITY_LABELS.containsKey(a) && b.isEmpty()) cityRows.add(r); |
| | | } |
| | | Map<String, double[]> citySums = new LinkedHashMap<>(); // 全名 -> G,H,I,J,K |
| | | Map<String, double[]> cityStats = new LinkedHashMap<>(); // 全名 -> {总数, 累计, 规划内数, 规划内累计, 规划外数, 规划外累计} |
| | | Set<Long> used = new HashSet<>(); |
| | | int filled = 0, kept = 0; |
| | | for (int i = 0; i < cityRows.size(); i++) { |
| | | int cr = cityRows.get(i); |
| | | String cityLabel = nvl(cellText(sheet.getRow(cr).getCell(0))); |
| | | String city = LOGI_CITY_LABELS.getOrDefault(cityLabel, cityLabel); |
| | | int start = cr + 1; |
| | | int end = (i + 1 < cityRows.size() ? cityRows.get(i + 1) : sheet.getLastRowNum() + 1) - 1; |
| | | List<Integer> chRows = new ArrayList<>(); |
| | | for (int r = start; r <= end; r++) { |
| | | HSSFRow row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | if (isInvestChannelLabel(nvl(cellText(row.getCell(0))))) chRows.add(r); |
| | | } |
| | | // (1) 先按项目名+市州 覆盖有库内当期数据的项目行 |
| | | for (int r = start; r <= end; r++) { |
| | | HSSFRow row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | String b = nvl(cellText(row.getCell(1))); |
| | | if (isInvestChannelLabel(a)) continue; |
| | | if (!a.matches("\\d+(\\.\\d+)?")) continue; |
| | | if (b.isEmpty()) continue; |
| | | InvestmentProject p = matchInvest(projects, b, city, "物流园区", used); |
| | | InvestmentMonthly m = p == null ? null : mByProject.get(p.getId()); |
| | | if (p == null || m == null) { kept++; continue; } |
| | | used.add(p.getId()); |
| | | // 口径同客运站明细表:只覆盖 H 自开始建设累计 / J 自年初累计 / K 本月完成 / M 形象进度 |
| | | if (m.getStartCum() != null) putNum(row, 7, m.getStartCum()); |
| | | if (m.getYearCum() != null) putNum(row, 9, m.getYearCum()); |
| | | if (m.getMonthDone() != null) putNum(row, 10, m.getMonthDone()); |
| | | if (m.getProgressDesc() != null && !m.getProgressDesc().trim().isEmpty()) putText(row, 12, m.getProgressDesc()); |
| | | filled++; |
| | | } |
| | | // (2) 再按渠道块汇总,并写回渠道行 / 市州行 |
| | | double[] citySum = new double[5]; |
| | | double[] stat = new double[6]; |
| | | for (int j = 0; j < chRows.size(); j++) { |
| | | int r0 = chRows.get(j); |
| | | int r1 = (j + 1 < chRows.size() ? chRows.get(j + 1) : end + 1) - 1; |
| | | boolean planned = !nvl(cellText(sheet.getRow(r0).getCell(0))).startsWith("四、"); |
| | | double[] s = new double[5]; |
| | | for (int r = r0 + 1; r <= r1; r++) { |
| | | HSSFRow row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | String b = nvl(cellText(row.getCell(1))); |
| | | if (isInvestChannelLabel(a) || !a.matches("\\d+(\\.\\d+)?")) continue; |
| | | if (b.isEmpty()) continue; |
| | | for (int k = 0; k < 5; k++) s[k] += numOf(row.getCell(6 + k)); |
| | | stat[0] += 1; |
| | | stat[1] += numOf(row.getCell(9)); |
| | | if (planned) { stat[2] += 1; stat[3] += numOf(row.getCell(9)); } |
| | | else { stat[4] += 1; stat[5] += numOf(row.getCell(9)); } |
| | | } |
| | | HSSFRow crow = sheet.getRow(r0); |
| | | for (int k = 0; k < 5; k++) putNum(crow, 6 + k, s[k] == 0 ? null : s[k]); |
| | | for (int k = 0; k < 5; k++) citySum[k] += s[k]; |
| | | } |
| | | // 渠道块之外的游离项目行(模板未分组时)也计入市州合计 |
| | | for (int r = start; r <= end; r++) { |
| | | HSSFRow row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | String b = nvl(cellText(row.getCell(1))); |
| | | if (isInvestChannelLabel(a) || !a.matches("\\d+(\\.\\d+)?")) continue; |
| | | if (b.isEmpty()) continue; |
| | | boolean inChannel = false; |
| | | for (int j = 0; j < chRows.size(); j++) { |
| | | int r0 = chRows.get(j); |
| | | int r1 = (j + 1 < chRows.size() ? chRows.get(j + 1) : end + 1) - 1; |
| | | if (r > r0 && r <= r1) { inChannel = true; break; } |
| | | } |
| | | if (inChannel) continue; |
| | | for (int k = 0; k < 5; k++) citySum[k] += numOf(row.getCell(6 + k)); |
| | | stat[0] += 1; |
| | | stat[1] += numOf(row.getCell(9)); |
| | | } |
| | | HSSFRow cityRow = sheet.getRow(cr); |
| | | for (int k = 0; k < 5; k++) putNum(cityRow, 6 + k, citySum[k] == 0 ? null : citySum[k]); |
| | | citySums.put(city, citySum); |
| | | cityStats.put(city, stat); |
| | | } |
| | | // 2) 合计行(第 7 行) |
| | | double[] total = new double[5]; |
| | | for (double[] s : citySums.values()) { |
| | | for (int k = 0; k < 5; k++) total[k] += s[k]; |
| | | } |
| | | for (int k = 0; k < 5; k++) putNum(sheet.getRow(6), 6 + k, total[k] == 0 ? null : total[k]); |
| | | |
| | | // 3) 分市州规划内外项目完成情况 |
| | | HSSFSheet citySheet = wb.getSheet("分市州规划内外项目完成情况"); |
| | | if (citySheet != null) { |
| | | Row t1 = citySheet.getRow(0); |
| | | if (t1 != null) { |
| | | String t = cellText(t1.getCell(0)); |
| | | if (t != null) { |
| | | String nv = t.replaceAll("\\d{4}年", year + "年"); |
| | | if (!nv.equals(t)) putText(t1, 0, nv); |
| | | } |
| | | } |
| | | double[] st = new double[6]; |
| | | for (int r = 3; r <= citySheet.getLastRowNum(); r++) { |
| | | HSSFRow row = citySheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | String b = nvl(cellText(row.getCell(1))); |
| | | if (a.contains("合计") || b.contains("合计")) { |
| | | for (int k = 0; k < 6; k++) putNum(row, 2 + k, st[k] == 0 ? null : st[k]); |
| | | continue; |
| | | } |
| | | double[] s = cityStats.get(LOGI_CITY_LABELS.getOrDefault(b, b)); |
| | | if (s == null) continue; |
| | | for (int k = 0; k < 6; k++) { |
| | | putNum(row, 2 + k, s[k] == 0 ? null : s[k]); |
| | | st[k] += s[k]; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 4) 市州汇总(B 年度计划投资为官方口径,保持模板原值;C/D/E 按分项目页重算) |
| | | HSSFSheet sumSheet = wb.getSheet("市州汇总"); |
| | | if (sumSheet != null) { |
| | | HSSFRow r2 = sumSheet.getRow(1); |
| | | if (r2 != null) { |
| | | putNum(r2, 0, (double) org.apache.poi.ss.usermodel.DateUtil.getExcelDate( |
| | | java.time.LocalDate.of(year, month, 1).atStartOfDay())); |
| | | } |
| | | double cT = 0.0, dT = 0.0; |
| | | for (int r = 7; r <= 23; r++) { |
| | | HSSFRow row = sumSheet.getRow(r); |
| | | if (row == null) continue; |
| | | String a = nvl(cellText(row.getCell(0))); |
| | | double[] s = citySums.get(LOGI_CITY_LABELS.getOrDefault(a, a)); |
| | | if (s == null) continue; |
| | | 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); |
| | | cT += s[3]; |
| | | dT += s[4]; |
| | | } |
| | | HSSFRow tot = sumSheet.getRow(6); |
| | | if (tot != null) { |
| | | 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); |
| | | } |
| | | } |
| | | try { |
| | | wb.getCreationHelper().createFormulaEvaluator().evaluateAll(); |
| | | } catch (Exception e) { |
| | | log.warn("物流站场投资明细公式求值失败: {}", e.getMessage()); |
| | | } |
| | | log.info("物流站场投资明细 {}:写入 {} 行,结转母版原值 {} 行", period, filled, kept); |
| | | return toBytesHssf(wb); |
| | | } |
| | | |
| | | private static class InvestAgg { |
| | | double total, startCum, yearPlan, yearCum; |
| | | |