traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -13,6 +13,7 @@
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;
@@ -29,9 +30,11 @@
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;
@@ -98,6 +101,10 @@
    private CityTaxiMonthlyMapper cityTaxiMapper;
    @Resource
    private SummaryWorkbookFiller summaryWorkbookFiller;
    @Resource
    private WycSplitCalc wycSplitCalc;
    @Resource
    private WycOrderMonthlyMapper wycOrderMapper;
    @Resource
    private AuditResultMapper auditResultMapper;
    @Resource
@@ -1179,17 +1186,16 @@
        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);
        }
@@ -1197,7 +1203,7 @@
        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();
@@ -1217,7 +1223,7 @@
            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);
                }
@@ -2181,20 +2187,15 @@
            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) {
@@ -2205,33 +2206,65 @@
                        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();
            }
        }
    }
@@ -2256,16 +2289,19 @@
        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) */
@@ -2369,7 +2405,52 @@
            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) */
@@ -3353,17 +3434,97 @@
            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 却残留 &lt;is&gt;」的非法单元格。
     *
     * 母版若由 openpyxl 写出,文本单元格用 &lt;is&gt;(inlineStr)存储;POI 的 setCellValue 只改写 t 与 &lt;v&gt;,
     * 不会清掉旧 &lt;is&gt;,于是产出 &lt;c t="n"&gt;&lt;v&gt;67.0&lt;/v&gt;&lt;is&gt;&lt;t&gt;67.00&lt;/t&gt;&lt;/is&gt;&lt;/c&gt; 这种非法格,
     * 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;
    }
    /**
@@ -3654,6 +3815,496 @@
    }
    /** 定位版式参照件 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);
@@ -3962,6 +4613,9 @@
            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;
@@ -3969,6 +4623,7 @@
                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);