traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -29,6 +29,9 @@
import com.trafficaudit.dataimport.mapper.PassengerEnterpriseMonthlyMapper;
import com.trafficaudit.dataimport.mapper.PassengerIndividualMonthlyMapper;
import com.trafficaudit.dataimport.mapper.ScaleSplitTransportMapper;
import com.trafficaudit.reportexport.calc.MidCalc;
import com.trafficaudit.reportexport.calc.PaxCalc;
import com.trafficaudit.reportexport.calc.SummaryWorkbookFiller;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
@@ -44,6 +47,7 @@
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.openxml4j.util.ZipSecureFile;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.STCellType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@@ -92,6 +96,8 @@
    @Resource
    private CityTaxiMonthlyMapper cityTaxiMapper;
    @Resource
    private SummaryWorkbookFiller summaryWorkbookFiller;
    @Resource
    private AuditResultMapper auditResultMapper;
    @Resource
    private AuditRunMapper auditRunMapper;
@@ -106,6 +112,10 @@
    /** 公路旅客输出模板目录(application.yml passenger.template-dir) */
    @Value("${passenger.template-dir:docs/公路旅客+能耗/输出}")
    private String passengerTemplateDir;
    /** 汇总工作簿模板目录(application.yml summary.template-dir) */
    @Value("${summary.template-dir:docs/生成汇总大表}")
    private String summaryTemplateDir;
    /** 货运报表模板目录(application.yml freight.template-dir) */
    @Value("${freight.template-dir:docs/货运}")
@@ -171,6 +181,43 @@
                    rows = addReadyCount(() -> energyMapper.selectCount(periodQw(period, mode, EnergyVehicleQuarterly::getReportPeriod)), "能耗车辆季度数据", notes);
                    auditTypes.add("H204");
                    break;
                case "wycSplit": {
                    rows = addReadyCount(() -> cityTaxiMapper.selectCount(periodQw(period, mode, CityTaxiMonthly::getReportPeriod)), "巡游出租月报", notes);
                    try {
                        Map<String, Object> inp = loadWycMonthlyInput(period, mode);
                        if (inp == null) {
                            rows = 0;
                            notes.add("《网约车订单及全省总量.xlsx》缺 " + period + " 行(需先维护当月订单与全省 pin)");
                        } else {
                            double pinK = (Double) inp.get("pinK"), pinCK = (Double) inp.get("pinCK");
                            double pinZ = (Double) inp.get("pinZ"), pinCZ = (Double) inp.get("pinCZ");
                            double orderSum = (Double) inp.get("orderSum");
                            if (pinK <= 0 || pinCK <= 0 || pinZ <= 0 || pinCZ <= 0 || orderSum <= 0) {
                                rows = 0;
                                notes.add("网约车输入 " + period + " 行不完整(订单/全省 pin 缺失)");
                            }
                        }
                    } catch (Exception ex) {
                        rows = 0;
                        notes.add("网约车输入读取失败:" + ex.getMessage());
                    }
                    break;
                }
                case "summaryWorkbook": {
                    rows = addReadyCount(() -> cityTaxiMapper.selectCount(periodQw(period, mode, CityTaxiMonthly::getReportPeriod)), "巡游出租月报", notes)
                         + addReadyCount(() -> cityBusMapper.selectCount(periodQw(period, mode, CityBusMonthly::getReportPeriod)), "城市公交月报", notes);
                    if (rows <= 0) notes.add("汇总表依赖的公交/出租月报数据缺失");
                    try {
                        if (resolveSummaryMother(period, mode) == null) {
                            rows = 0;
                            notes.add("docs/生成汇总大表 下缺少可复制的母版汇总表");
                        }
                    } catch (Exception ex) {
                        rows = 0;
                        notes.add("母版定位失败:" + ex.getMessage());
                    }
                    break;
                }
                default:
                    rows = 0;
                    notes.add("未知报表类型:" + t);
@@ -181,6 +228,7 @@
                auditRows += auditRowsOf(period, mode, at);
                audited = audited || auditedMarked(period, mode, at);
            }
            if (auditTypes.isEmpty()) audited = true;
            it.put("rows", rows);
            it.put("ready", rows > 0);
            it.put("auditRows", auditRows);
@@ -212,7 +260,8 @@
        for (String t : types) {
            if (isFreightType(t)) freight = true;
            else if (isPassengerType(t)) passenger = true;
            else if ("cityTaxiDetail".equals(t) || isCityPassengerType(t)) { cityTaxi = true; cityBus = true; }
            else if ("cityTaxiDetail".equals(t) || "wycSplit".equals(t)) { cityTaxi = true; }
            else if (isCityPassengerType(t) || "summaryWorkbook".equals(t)) { cityTaxi = true; cityBus = true; }
            else if (isInvestType(t)) invest = true;
            else if ("energySummary".equals(t)) energy = true;
        }
@@ -256,7 +305,46 @@
            List<Integer> missing = new ArrayList<>();
            List<Integer> coverage = new ArrayList<>();
            if (Boolean.FALSE.equals(it.get("audited"))) problems.add("该报表期尚未审核通过");
            if (isFreightType(t)) {
            if ("wycSplit".equals(t)) {
                coverage.addAll(taxiCover);
                missing = missingMonths(taxiCover, month);
                try {
                    Map<String, Object> inp = loadWycMonthlyInput(period, mode);
                    if (inp == null) {
                        problems.add("缺网约车输入:请在《网约车订单及全省总量.xlsx》补 " + period + " 行");
                    } else {
                        double pinK = (Double) inp.get("pinK"), pinCK = (Double) inp.get("pinCK");
                        double pinZ = (Double) inp.get("pinZ"), pinCZ = (Double) inp.get("pinCZ");
                        double orderSum = (Double) inp.get("orderSum");
                        if (pinK <= 0 || pinCK <= 0 || pinZ <= 0 || pinCZ <= 0 || orderSum <= 0) {
                            problems.add("网约车输入 " + period + " 行不完整(订单/全省 pin 缺失)");
                        } else {
                            addSum(summary, "全省网约车客运量", pinK, "万人");
                            addSum(summary, "其中城市内客运量", pinCK, "万人");
                            addSum(summary, "全省网约车周转量", pinZ, "万人公里");
                            addSum(summary, "其中城市内周转量", pinCZ, "万人公里");
                            addSum(summary, "部级订单合计", orderSum, "单");
                        }
                    }
                } catch (Exception ex) {
                    problems.add("网约车输入读取失败:" + ex.getMessage());
                }
            } else if ("summaryWorkbook".equals(t)) {
                java.util.Set<Integer> cov = new java.util.TreeSet<>();
                cov.addAll(busCover);
                cov.addAll(taxiCover);
                coverage.addAll(cov);
                missing = missingMonths(cov, month);
                try {
                    if (resolveSummaryMother(period, mode) == null) {
                        problems.add("docs/生成汇总大表 下缺少可复制的母版汇总表");
                    } else {
                        summaryWorkbookPreview(period, yearNum, month, summary, problems);
                    }
                } catch (Exception ex) {
                    problems.add(ex.getMessage());
                }
            } else if (isFreightType(t)) {
                coverage.addAll(freightCover);
                missing = missingMonths(freightCover, month);
                freightPreview(period, month, summary, problems);
@@ -355,6 +443,118 @@
        return sb.toString();
    }
    /** 汇总整本关键指标预览:货运量/周转量、城市客运、中口径的 当月、1..M 累计、当月同比、累计同比(同比取库内去年同期,缺则显示—) */
    private void summaryWorkbookPreview(String period, int year, int month,
                                        List<Map<String, Object>> summary, List<String> problems) {
        String yearPrefix = year + "-";
        // ---- 货运(量=模板_货运量周转量全省行;周转量=规上规下拆分表全省行) ----
        FreightTurnoverImport ft = loadFreightTurnover(period).get("湖北省");
        Double fm = ft == null ? null : freightMonth(ft, month);
        Double fcum = ft == null ? null : freightCum(ft, month);
        Double fy = ft == null ? null : freightYoy(ft, month);
        Double fcy = ft == null ? null : freightCumYoy(ft, month);
        addKpi(summary, "全省货运量", fm, fcum, fy, fcy, "万吨");
        if (fm != null && fy == null) {
            problems.add((year - 1) + "-" + String.format("%02d", month) + " 同期货运量数据未导入库,货运量当月/累计同比暂不显示,待 2025 年定稿数据导入后自动出现");
        }
        ScaleSplitTransport tm = loadProvinceMonthMap(period).get(month);
        ScaleSplitTransport tc = getProvinceCumulative(period);
        Double tvM = tm == null ? null : tm.getTotalTurnover();
        Double tvC = tc == null ? null : tc.getTotalTurnover();
        Double tvMY = tm == null ? null : getYoyMetric(tm, "turnover", "total");
        Double tvCY = tc == null ? null : getYoyMetric(tc, "turnover", "total");
        addKpi(summary, "全省货物周转量", tvM, tvC, tvMY, tvCY, "万吨公里");
        if (fm == null) problems.add("模板_货运量周转量缺 " + period + " 当月数据,货运量仅能提供截至最近导入月的累计");
        if (tvM == null) problems.add("规上规下拆分缺 " + period + " 当月数据,周转量/规上规下当月缺,累计为截至最近导入月");
        // ---- 城市客运(公交+出租+轨道+轮渡 全省小计;指标位 0/2/4/6=客运,1/3/5/7=周转) ----
        double[] cumM = cityCumProv(yearPrefix, month);
        double[] cumPrev = cityCumProv(yearPrefix, month - 1);
        double[] monthVec = diff8(cumM, cumPrev);
        double cityPaxM = sumIdx(monthVec, new int[]{0, 2, 4, 6});
        double cityTurnM = sumIdx(monthVec, new int[]{1, 3, 5, 7});
        double cityPaxC = sumIdx(cumM, new int[]{0, 2, 4, 6});
        double cityTurnC = sumIdx(cumM, new int[]{1, 3, 5, 7});
        double[] lastCumM = cityCumProv((year - 1) + "-", month);
        double[] lastCumPrev = cityCumProv((year - 1) + "-", month - 1);
        double[] lastVec = diff8(lastCumM, lastCumPrev);
        double lastPaxM = sumIdx(lastVec, new int[]{0, 2, 4, 6});
        double lastPaxC = sumIdx(lastCumM, new int[]{0, 2, 4, 6});
        double lastTurnM = sumIdx(lastVec, new int[]{1, 3, 5, 7});
        double lastTurnC = sumIdx(lastCumM, new int[]{1, 3, 5, 7});
        addKpiRaw(summary, "城市客运量", cityPaxM, cityPaxC, lastPaxM, lastPaxC, "万人次");
        addKpiRaw(summary, "城市客运周转量", cityTurnM, cityTurnC, lastTurnM, lastTurnC, "万人公里");
        // ---- 中口径(公路旅客企业 H203-1 全省,单位换算 /10000) ----
        Map<Integer, Map<String, PassengerAgg>> data = loadPassengerAggMap();
        double paxM = 0.0, paxC = 0.0, turnM = 0.0, turnC = 0.0;
        double lastPaxM2 = 0.0, lastPaxC2 = 0.0, lastTurnM2 = 0.0, lastTurnC2 = 0.0;
        for (int m = 1; m <= month; m++) {
            PassengerAgg agg = aggOf(data, year, m, "湖北省");
            if (agg != null) {
                double pax = agg.passengerTotal / 10000.0;
                double turn = agg.turnoverTotal / 10000.0;
                if (m == month) { paxM = pax; turnM = turn; }
                paxC += pax;
                turnC += turn;
            }
            PassengerAgg last = aggOf(data, year - 1, m, "湖北省");
            if (last != null) {
                double lp = last.passengerTotal / 10000.0;
                double lt = last.turnoverTotal / 10000.0;
                if (m == month) { lastPaxM2 = lp; lastTurnM2 = lt; }
                lastPaxC2 += lp;
                lastTurnC2 += lt;
            }
        }
        addKpiRaw(summary, "中口径客运量", paxM, paxC, lastPaxM2, lastPaxC2, "万人次");
        addKpiRaw(summary, "中口径周转量", turnM, turnC, lastTurnM2, lastTurnC2, "万人公里");
        if (paxM <= 0 && paxC <= 0 && cityPaxC <= 0 && fm == null) {
            problems.add("货运量周转量/规上规下/公路旅客/城市客运当月数据均缺失");
        }
        if (cityPaxC > 0 && lastPaxC <= 0) {
            problems.add((year - 1) + "-" + String.format("%02d", month) + " 同期城市客运数据未导入库,城市客运同比暂不显示,待 2025 年定稿数据导入后自动出现");
        }
        if (paxC > 0 && lastPaxC2 <= 0) {
            problems.add((year - 1) + "-" + String.format("%02d", month) + " 同期公路旅客数据未导入库,中口径同比暂不显示,待 2025 年定稿数据导入后自动出现");
        }
    }
    /** 城市客运全省累计向量(8 指标:公交客运/周转、出租客运/周转、轨道客运/周转、轮渡客运/周转) */
    private double[] cityCumProv(String yearPrefix, int month) {
        if (month <= 0) return new double[8];
        return loadCityPassengerCumulative(yearPrefix, month).getOrDefault("全省", new double[8]);
    }
    private double[] diff8(double[] a, double[] b) {
        double[] out = new double[8];
        for (int i = 0; i < 8; i++) out[i] = a[i] - b[i];
        return out;
    }
    private double sumIdx(double[] arr, int[] idx) {
        double sum = 0.0;
        for (int i : idx) sum += (i < arr.length ? arr[i] : 0.0);
        return sum;
    }
    /** 百分比同比(值已为比率 0.xx):输出四行 当月/累计/当月同比/累计同比 */
    private void addKpi(List<Map<String, Object>> summary, String name, Double m, Double cum, Double yoyM, Double yoyC, String unit) {
        if (m != null) addSum(summary, name + "·当月", round(m, 2), unit);
        if (cum != null) addSum(summary, name + "·累计", round(cum, 2), unit);
        if (yoyM != null) addSum(summary, name + "·当月同比", round(yoyM * 100.0, 1), "%");
        if (yoyC != null) addSum(summary, name + "·累计同比", round(yoyC * 100.0, 1), "%");
    }
    /** 原始数值版(月/累计为 0 视为无数据不出行) */
    private void addKpiRaw(List<Map<String, Object>> summary, String name, double m, double cum, double lastM, double lastC, String unit) {
        if (m > 0) addSum(summary, name + "·当月", round(m, 2), unit);
        if (cum > 0) addSum(summary, name + "·累计", round(cum, 2), unit);
        if (m > 0 && lastM > 0) addSum(summary, name + "·当月同比", round((m - lastM) / lastM * 100.0, 1), "%");
        if (cum > 0 && lastC > 0) addSum(summary, name + "·累计同比", round((cum - lastC) / lastC * 100.0, 1), "%");
    }
    private void addSum(List<Map<String, Object>> summary, String label, Double value, String unit) {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("label", label);
@@ -369,7 +569,7 @@
        Map<String, Double> h2032Cum = loadH2032FreightCumulative(period);
        ScaleSplitTransport provCum = getProvinceCumulative(period);
        Double total = freightCum(ft.get("湖北省"), month);
        Double totalWan = total == null ? null : round(total / 10000.0, 4);
        Double totalWan = total == null ? null : round(total, 4); // 模板列已是万吨,不再 ÷10000
        double above = 0.0;
        for (Double v : h2032Cum.values()) if (v != null) above += v;
        double aboveWan = round(above / 10000.0, 4);
@@ -665,17 +865,58 @@
            totalYoy.put(city, freightCumYoy(ftMap.get(city), month));
        }
        // ===== 第二页(当月)数据:规上=H2032 当月、合计=模板_货运量周转量当月列、规下=差值;同比=去年同月 =====
        Map<String, Map<Integer, Double>> h2032MonthCur = loadH2032FreightByMonth(period);
        String lastYearPeriod = (Integer.parseInt(year) - 1) + period.substring(4);
        Map<String, Map<Integer, Double>> h2032MonthLast = loadH2032FreightByMonth(lastYearPeriod);
        Map<String, Double> monthAbove = new HashMap<>();
        Map<String, Double> monthBelow = new HashMap<>();
        Map<String, Double> monthTotal = new HashMap<>();
        Map<String, Double> monthAboveYoy = new HashMap<>();
        Map<String, Double> monthBelowYoy = new HashMap<>();
        Map<String, Double> monthTotalYoy = new HashMap<>();
        double provMonthAbove = 0.0, provMonthAboveLast = 0.0;
        for (String city : RegionUtil.cityList()) {
            Double ma = monthFreightWan(h2032MonthCur, city, month);
            Double maLast = monthFreightWan(h2032MonthLast, city, month);
            Double mt = freightMonth(ftMap.get(city), month);
            Double mtLast = lastFreightMonth(ftMap.get(city), month);
            if (ma != null) provMonthAbove += ma;
            if (maLast != null) provMonthAboveLast += maLast;
            monthAbove.put(city, ma);
            monthTotal.put(city, mt);
            monthBelow.put(city, mt == null ? null : round(mt - (ma == null ? 0.0 : ma), 4));
            monthAboveYoy.put(city, growthYoy(ma, maLast));
            Double mb = monthBelow.get(city);
            Double mbLast = mtLast == null ? null : round(mtLast - (maLast == null ? 0.0 : maLast), 4);
            monthBelowYoy.put(city, growthYoy(mb, mbLast));
            monthTotalYoy.put(city, freightYoy(ftMap.get(city), month));
        }
        Double provMonthAboveWan = provMonthAbove == 0.0 ? null : round(provMonthAbove, 4);
        Double provMonthTotal = freightMonth(ftMap.get("湖北省"), month);
        Double provMonthBelow = provMonthTotal == null ? null : round(provMonthTotal - provMonthAbove, 4);
        Double provMonthAboveYoy = growthYoy(provMonthAbove == 0.0 ? null : provMonthAbove, provMonthAboveLast);
        Double provMonthTotalLast = lastFreightMonth(ftMap.get("湖北省"), month);
        Double provMonthBelowLast = provMonthTotalLast == null ? null
            : round(provMonthTotalLast - provMonthAboveLast, 4);
        Double provMonthBelowYoy = growthYoy(provMonthBelow, provMonthBelowLast);
        Double provMonthTotalYoy = freightYoy(ftMap.get("湖北省"), month);
        // 以 docs/货运/生成_货运量排名.xlsx 为底稿:保留表头/合并/列宽/样式与 Q/R 占比公式,仅替换数据
        File template = resolveFreightTemplate("生成_货运量排名.xlsx");
        try (InputStream in = new FileInputStream(template);
             XSSFWorkbook wb = new XSSFWorkbook(in)) {
            Sheet sheet = wb.getSheetAt(0);
            sheet.getRow(0).getCell(0).setCellValue(year + "年" + cumRange(month) + "全省分市州累计完成公路货运量情况");
            sheet.getRow(22).getCell(0).setCellValue(year + "年" + month + "月全省分市州累计完成公路货运量情况");
            sheet.getRow(22).getCell(0).setCellValue(year + "年" + month + "月全省分市州完成公路货运量情况");
            fillFreightRankBlock(sheet, 3, above, below, total, aboveYoy, belowYoy, totalYoy,
                provAboveWan, provBelow, provTotal, provAboveYoy, provBelowYoy, provTotalYoy);
            fillFreightRankBlock(sheet, 25, above, below, total, aboveYoy, belowYoy, totalYoy,
                provAboveWan, provBelow, provTotal, provAboveYoy, provBelowYoy, provTotalYoy);
            renameRankMonthHeader(sheet, 24);
            fillFreightRankBlock(sheet, 25, monthAbove, monthBelow, monthTotal,
                monthAboveYoy, monthBelowYoy, monthTotalYoy,
                provMonthAboveWan, provMonthBelow, provMonthTotal,
                provMonthAboveYoy, provMonthBelowYoy, provMonthTotalYoy);
            try {
                wb.getCreationHelper().createFormulaEvaluator().evaluateAll();
            } catch (Exception e) {
@@ -693,15 +934,27 @@
        Map<String, ScaleSplitTransport> cumMap = loadCumulativeMap(period);
        ScaleSplitTransport province = getProvinceCumulative(period);
        // 当月(第二页):周转量拆分表 MONTH 行(含当月同比)
        Map<String, ScaleSplitTransport> monthTurnover = new HashMap<>();
        for (ScaleSplitTransport rec : scaleSplitMapper.selectList(
                new LambdaQueryWrapper<ScaleSplitTransport>()
                    .eq(ScaleSplitTransport::getPeriodType, "MONTH")
                    .eq(ScaleSplitTransport::getReportPeriod, period))) {
            String name = RegionUtil.normalizeCityName(rec.getRegionName());
            if (name != null) monthTurnover.put(name, rec);
        }
        ScaleSplitTransport provinceMonth = monthTurnover.remove("湖北省");
        // 以 docs/货运/生成_周转量排名.xlsx 为底稿:保留表头/合并/列宽/样式,仅替换数据;Q/R 占比为模板缓存值,按新数据重算
        File template = resolveFreightTemplate("生成_周转量排名.xlsx");
        try (InputStream in = new FileInputStream(template);
             XSSFWorkbook wb = new XSSFWorkbook(in)) {
            Sheet sheet = wb.getSheetAt(0);
            sheet.getRow(0).getCell(0).setCellValue(year + "年" + cumRange(month) + "全省分市州累计完成公路货物运输周转量情况");
            sheet.getRow(22).getCell(0).setCellValue(year + "年" + month + "月全省分市州累计完成公路货物运输周转量情况");
            sheet.getRow(22).getCell(0).setCellValue(year + "年" + month + "月全省分市州完成公路货物运输周转量情况");
            fillTurnoverRankBlock(sheet, 3, cumMap, province);
            fillTurnoverRankBlock(sheet, 25, cumMap, province);
            renameRankMonthHeader(sheet, 24);
            fillTurnoverRankBlock(sheet, 25, monthTurnover, provinceMonth);
            return toBytes(wb);
        }
    }
@@ -901,14 +1154,15 @@
        return org.apache.poi.ss.util.CellReference.convertNumToColString(colIdx);
    }
    /** 旅客分市州月值(0=客运量/1=周转量/2=个体客运量/3=个体周转量) */
    /** 旅客分市州月值(0=客运量/1=周转量/2=个体客运量/3=个体周转量;0/1=企业H2031+个体,2026-09-03 口径确认) */
    private double passengerMonthVal(Map<Integer, Map<String, PassengerAgg>> data, Map<Integer, Map<String, double[]>> indi,
                                     int year, int month, String region, int metricIdx) {
        if (metricIdx <= 1) {
        if (metricIdx == 0 || metricIdx == 1) { // 客运量/周转量总行:企业+个体
            PassengerAgg agg = aggOf(data, year, month, region);
            Double val = agg == null ? null
            double ent = agg == null ? 0.0
                : (metricIdx == 0 ? agg.passengerTotal / 10000.0 : agg.turnoverTotal / 10000.0);
            return val == null ? 0.0 : val;
            double ind = individualVal(indi, year, month, region, metricIdx == 0 ? 0 : 1);
            return ent + ind;
        }
        return individualVal(indi, year, month, region, metricIdx - 2);
    }
@@ -992,6 +1246,31 @@
        }
        setFreightCell(row, cumCol, cumValue);
        setFreightCell(row, cumCol + 1, cumYoy);
    }
    /** H2032 规上某月合计(吨→万吨);无该月数据返回 null */
    private Double monthFreightWan(Map<String, Map<Integer, Double>> byCityMonth, String city, int month) {
        Map<Integer, Double> byMonth = byCityMonth == null ? null : byCityMonth.get(city);
        Double v = byMonth == null ? null : byMonth.get(month);
        return v == null ? null : round(v / 10000.0, 4);
    }
    /** 同比 = (本期-基期)/基期;任一期缺失或基期为 0 返回 null */
    private Double growthYoy(Double cur, Double last) {
        if (cur == null || last == null || last == 0.0) return null;
        return (cur - last) / last;
    }
    /** 排名表第二页列头「累计完成」→「当月完成」(仅导出内存替换,模板文件不改) */
    private void renameRankMonthHeader(Sheet sheet, int headerRowIdx) {
        Row row = sheet.getRow(headerRowIdx);
        if (row == null) return;
        for (int col : new int[]{1, 6, 11}) {
            Cell c = row.getCell(col);
            if (c == null || c.getCellType() != CellType.STRING) continue;
            String s = c.getStringCellValue();
            if (s != null && s.contains("累计完成")) c.setCellValue(s.replace("累计完成", "当月完成"));
        }
    }
    /** 货运量排名块填充:模板 1-based r4 起(全省+17市州),Q/R 占比公式保留不动 */
@@ -1163,9 +1442,12 @@
    /** H2032规上: city -> month -> 货运量(吨) */
    private Map<String, Map<Integer, Double>> loadH2032FreightByMonth(String period) {
        Map<String, Map<Integer, Double>> result = new HashMap<>();
        String yearPrefix = period.length() >= 4 ? period.substring(0, 4) : null;
        List<H2032EnterpriseMonthly> list = h2032Mapper.selectList(null);
        for (H2032EnterpriseMonthly record : list) {
            if (record.getReportPeriod() == null || record.getReportPeriod().compareTo(period) > 0) continue;
            // H2032 表同时存有往年月报,统计当年规上值时只取与报表期同年的记录,避免去年数据叠加
            if (yearPrefix != null && !record.getReportPeriod().startsWith(yearPrefix)) continue;
            String city = RegionUtil.cityByCode(record.getRegionCode());
            if (city == null) continue;
            int month = parseMonth(record.getReportPeriod());
@@ -1178,9 +1460,11 @@
    /** H2032规上累计: city -> 货运量(吨) */
    private Map<String, Double> loadH2032FreightCumulative(String period) {
        Map<String, Double> result = new HashMap<>();
        String yearPrefix = period.length() >= 4 ? period.substring(0, 4) : null;
        List<H2032EnterpriseMonthly> list = h2032Mapper.selectList(null);
        for (H2032EnterpriseMonthly record : list) {
            if (record.getReportPeriod() == null || record.getReportPeriod().compareTo(period) > 0) continue;
            if (yearPrefix != null && !record.getReportPeriod().startsWith(yearPrefix)) continue;
            String city = RegionUtil.cityByCode(record.getRegionCode());
            if (city == null) continue;
            double freight = record.getFreightTotal() == null ? 0.0 : record.getFreightTotal();
@@ -1945,6 +2229,7 @@
            // 累计同比列(模板 #REF! → 库内去年 1..N 月累计同比数值)
            fillMidCumYoyColumn(sheet, mid, currentYear, fillMonths, extended ? 27 : 15);
            recalc(wb);
            clearFormulaErrorsAll(wb); // 去年同期明细页残留 #REF! 同比公式 → 清空
            return toBytes(wb);
        }
    }
@@ -2022,7 +2307,7 @@
    /**
     * 中口径分类月度数据:年*100+月 -> (市州/全省 -> double[5][2])
     * 维度 0=总量 1=公路班线(h2031) 2=城际城乡公交(cityBus) 3=巡游出租 4=网约车;[客运量(万人), 周转量(万人公里)]
     * 维度 0=总量 1=公路班线(h2031+个体) 2=城际城乡公交(cityBus) 3=巡游出租 4=网约车;[客运量(万人), 周转量(万人公里)]
     */
    private Map<Integer, Map<String, double[][]>> loadMidClassMap() {
        Map<Integer, Map<String, double[][]>> mid = new HashMap<>();
@@ -2044,6 +2329,25 @@
                + nz(e.getPassengerClass4()) + nz(e.getPassengerCharter())) / 10000.0;
            double turn = (nz(e.getTurnoverClass1()) + nz(e.getTurnoverClass2()) + nz(e.getTurnoverClass3())
                + nz(e.getTurnoverClass4()) + nz(e.getTurnoverCharter())) / 10000.0;
            addMidClass(mid, key, city, 1, pass, turn);
            addMidClass(mid, key, "湖北省", 1, pass, turn);
        }
        for (PassengerIndividualMonthly e : passengerIndividualMapper.selectList(null)) { // 个体并入公路班线/总量(09-03 口径)
            if (e.getReportPeriod() == null) continue;
            String[] parts = e.getReportPeriod().split("-");
            if (parts.length != 2) continue;
            int y, m;
            try {
                y = Integer.parseInt(parts[0]);
                m = Integer.parseInt(parts[1]);
            } catch (NumberFormatException ex) {
                continue;
            }
            String city = RegionUtil.cityByCode(e.getRegionCode());
            if (city == null) continue;
            int key = y * 100 + m;
            double pass = nz(e.getPassengerCount()) / 10000.0;
            double turn = nz(e.getTurnover()) / 10000.0;
            addMidClass(mid, key, city, 1, pass, turn);
            addMidClass(mid, key, "湖北省", 1, pass, turn);
        }
@@ -2195,11 +2499,20 @@
                    title.getCell(0).setCellValue(currentYear + "年1-" + maxMonth + "月全省分市州累计完成道路客运生产情况");
                }
            }
            // 右块(当月)标题 K1
            if (title != null && title.getCell(10) != null) {
                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)));
            // 右块全省行 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)));
            // 市州行 r4-20:B/F 累计值、D/H 同比;C/E/G/I 排名公式保留
            List<String> cities = RegionUtil.CITY_LIST;
            for (int i = 0; i < cities.size(); i++) {
@@ -2213,6 +2526,15 @@
                setValOrBlank(sheet, r0, 3, yoyOf(pass, lastPass));
                setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4));
                setValOrBlank(sheet, r0, 7, yoyOf(turn, lastTurn));
                // 右块当月: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, 15, mTurn == 0 ? null : round(mTurn, 4));
                setValOrBlank(sheet, r0, 17, mTurn == 0 ? null : yoyOf(mTurn, mLastTurn));
            }
            recalc(wb);
            return toBytes(wb);
@@ -2277,6 +2599,36 @@
                setValOrBlank(sheet, r0, 2, yoyOf(pass, lastPass));
                setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4));
                setValOrBlank(sheet, r0, 6, yoyOf(turn, lastTurn));
            }
            // 下半块(当月):标题行9、数据行11-15(0-based 8、10-14)
            Row bottomTitle = sheet.getRow(8);
            if (bottomTitle != null && bottomTitle.getCell(0) != null) {
                bottomTitle.getCell(0).setCellValue(currentYear + "年" + maxMonth + "月中口径客运量及周转量");
            }
            for (int i = 0; i < names.length; i++) {
                int r0 = 10 + i;
                double pass = midClassVal(mid, currentYear, maxMonth, "湖北省", dims[i], true);
                double turn = midClassVal(mid, currentYear, maxMonth, "湖北省", dims[i], false);
                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, 5, turn == 0 ? null : round(turn, 4));
                setValOrBlank(sheet, r0, 6, turn == 0 ? null : yoyOf(turn, lastTurn));
            }
            // 下半块占比公式由样例指向上半块(B$3/F$3),改写为本块总行(B$11/F$11),行12-15
            for (int i = 1; i < names.length; i++) {
                int r0 = 10 + i;
                Row rr = sheet.getRow(r0);
                if (rr == null) continue;
                Cell dc = rr.getCell(3);
                if (dc != null && dc.getCellType() == CellType.FORMULA) {
                    dc.setCellFormula("B" + (r0 + 1) + "/B$11");
                }
                Cell hc = rr.getCell(7);
                if (hc != null && hc.getCellType() == CellType.FORMULA) {
                    hc.setCellFormula("F" + (r0 + 1) + "/F$11");
                }
            }
            recalc(wb);
            return toBytes(wb);
@@ -2372,6 +2724,7 @@
                fillEnergyTemplateSheet(s, agg);
            }
            recalc(wb);
            clearFormulaErrorsAll(wb); // 无去年数据的同比/增速公式 #DIV/0! → 清空
            return toBytes(wb);
        }
    }
@@ -2508,7 +2861,7 @@
    public byte[] exportCityBusDetail(String period, String mode) throws Exception {
        int year = Integer.parseInt(period.split("-")[0]);
        int month = monthOf(period, mode);
        return exportCityByTemplate("导入模板_城市公交.xlsx", period, mode,
        return exportCityByTemplate("生成_城市公交客运量分市州明细.xlsx", period, mode,
            loadCityBusByMonth(year + "-", month),
            loadCityBusByMonth((year - 1) + "-", month));
    }
@@ -2562,7 +2915,7 @@
    public byte[] exportCityTaxiDetail(String period, String mode) throws Exception {
        int year = Integer.parseInt(period.split("-")[0]);
        int month = monthOf(period, mode);
        return exportCityByTemplate("导入模板_巡游出租.xlsx", period, mode,
        return exportCityByTemplate("生成_巡游出租客运量分市州明细.xlsx", period, mode,
            loadCityTaxiByMonth(year + "-", month),
            loadCityTaxiByMonth((year - 1) + "-", month));
    }
@@ -2606,7 +2959,7 @@
        int year = Integer.parseInt(period.split("-")[0]);
        int month = monthOf(period, mode);
        Map<Integer, Map<String, double[]>> monthCity = loadCityRailFerryByMonth(year + "-", month);
        File template = resolveTemplate("导入模板_轨道、轮渡.xlsx");
        File template = resolveTemplate("生成_轨道轮渡客运量分市州明细.xlsx");
        try (InputStream in = new FileInputStream(template);
             XSSFWorkbook wb = new XSSFWorkbook(in)) {
            Sheet sheet = wb.getSheetAt(0);
@@ -2616,6 +2969,8 @@
            // 轮渡:客运量行 13(全省)/15(武汉);指标 2=轮渡客运量 3=轮渡周转量
            fillRailFerryBlock(sheet, monthCity, month, new String[][]{{"全省", "12"}, {"武汉市", "14"}}, 2);
            recalc(wb);
            clearFormulaErrorCells(sheet, 3, sheet.getLastRowNum(), 1, 39);
            fixCityMonthHeaderText(sheet);
            return toBytes(wb);
        }
    }
@@ -2704,6 +3059,7 @@
        int year = Integer.parseInt(period.split("-")[0]);
        int month = monthOf(period, mode);
        Map<String, double[]> cum = loadCityPassengerCumulative(year + "-", month);
        Map<String, double[]> monthMap = loadCityPassengerMonthMap(year + "-", month);
        File template = resolveTemplate(templateFileName);
        try (InputStream in = new FileInputStream(template);
             XSSFWorkbook wb = new XSSFWorkbook(in)) {
@@ -2712,9 +3068,24 @@
            List<String> areas = new java.util.ArrayList<>();
            areas.add("全省");
            areas.addAll(RegionUtil.cityList());
            fillCitySumBlock(sheet, cum, areas, 4, 0);   // 客运量区(单位:万人次,POI 0 基)
            fillCitySumBlock(sheet, cum, areas, 26, 1);  // 周转量区(单位:万人次公里,POI 0 基)
            fillCitySumBlock(sheet, cum, areas, 4, 0);   // 累计客运量区(单位:万人次,POI 0 基,行5-22)
            fillCitySumBlock(sheet, cum, areas, 26, 1);  // 累计周转量区(单位:万人次公里,POI 0 基,行27-44)
            // 当月块标题(0基行44/66)与数据(0基行48-65/70-87)
            Row pTitle = sheet.getRow(44);
            if (pTitle != null && pTitle.getCell(0) != null) {
                pTitle.getCell(0).setCellValue("城市客运客运量分市州" + month + "月情况");
            }
            Row tTitle = sheet.getRow(66);
            if (tTitle != null && tTitle.getCell(0) != null) {
                tTitle.getCell(0).setCellValue("城市客运客运周转量分市州" + month + "月情况");
            }
            fillCitySumBlock(sheet, monthMap, areas, 48, 0);  // 当月客运量区(行49-66)
            fillCitySumBlock(sheet, monthMap, areas, 70, 1);  // 当月周转量区(行71-88)
            // 当月块排名/占比公式由累计块复制而来,RANK 范围与占比分母需改指当月块自身行
            repairCityMonthRankShare(sheet, 50, 66, 49);
            repairCityMonthRankShare(sheet, 72, 88, 71);
            recalc(wb);
            clearFormulaErrorsAll(wb); // 无去年累计的占比/增速公式 #DIV/0! → 清空
            return toBytes(wb);
        }
    }
@@ -2744,6 +3115,33 @@
            setDataCell(sheet, rowIdx, 24, 0);
            rowIdx++;
        }
    }
    /** 当月块排名/占比公式修正:模板当月块由累计块复制而来,RANK 范围与占比分母需改指当月块自身行 */
    private void repairCityMonthRankShare(Sheet sheet, int firstDataRow1, int lastDataRow1, int provRow1) {
        String[] groups = {"B", "G", "L", "Q"};
        String[] growth = {"E", "J", "O", "T"};
        for (int r = firstDataRow1; r <= lastDataRow1; r++) {
            int r0 = r - 1;
            for (int g = 0; g < 4; g++) {
                String base = groups[g];
                String grow = growth[g];
                setFormulaIfFormula(sheet, r0, 2 + g * 5,
                    "RANK(" + base + r + "," + base + "$" + firstDataRow1 + ":" + base + "$" + lastDataRow1 + ")");
                setFormulaIfFormula(sheet, r0, 3 + g * 5, base + r + "/" + base + "$" + provRow1);
                setFormulaIfFormula(sheet, r0, 5 + g * 5,
                    "RANK(" + grow + r + "," + grow + "$" + firstDataRow1 + ":" + grow + "$" + lastDataRow1 + ")");
            }
        }
    }
    /** 单元格原为公式时改写公式(排名/占比列),非公式单元格不动(保持模板式样) */
    private void setFormulaIfFormula(Sheet sheet, int rowIdx, int colIdx, String formula) {
        Row row = sheet.getRow(rowIdx);
        if (row == null) return;
        Cell c = row.getCell(colIdx);
        if (c == null || c.getCellType() != CellType.FORMULA) return;
        c.setCellFormula(formula);
    }
    /** 某年 1..limit 月城市客运累计:city → [公交客运量,公交周转量,出租客运量,出租周转量,轨道客运量,轨道周转量,轮渡客运量,轮渡周转量],含"全省" */
@@ -2784,6 +3182,23 @@
        return cum;
    }
    /** 某年单月城市客运数据:city → 8 维数组(口径同累计),含"全省";= month 累计 - (month-1) 累计 */
    private Map<String, double[]> loadCityPassengerMonthMap(String yearPrefix, int month) {
        Map<String, double[]> cum = loadCityPassengerCumulative(yearPrefix, month);
        if (month <= 1) return cum;
        Map<String, double[]> pre = loadCityPassengerCumulative(yearPrefix, month - 1);
        Map<String, double[]> out = new HashMap<>();
        for (Map.Entry<String, double[]> e : cum.entrySet()) {
            double[] prev = pre.get(e.getKey());
            double[] d = new double[8];
            for (int i = 0; i < 8; i++) {
                d[i] = e.getValue()[i] - (prev == null ? 0 : prev[i]);
            }
            out.put(e.getKey(), d);
        }
        return out;
    }
    // ==================== 城市客运 全省汇总(城市汇总_模板) ====================
    /** 生成_城市客运汇总.xlsx(以 城市汇总_模板.xlsx 为底稿;2026 段填库内 1..N 月累计,同比清空,2025/2024 段保留模板历史参考值) */
@@ -2795,7 +3210,7 @@
        double taxiPax = prov[2], taxiTurn = prov[3];
        double railPax = prov[4], railTurn = prov[5];
        double ferryPax = prov[6], ferryTurn = prov[7];
        File template = resolveTemplate("导入模板_城市汇总.xlsx");
        File template = resolveTemplate("生成_城市客运汇总.xlsx");
        try (InputStream in = new FileInputStream(template);
             XSSFWorkbook wb = new XSSFWorkbook(in)) {
            Sheet sheet = wb.getSheetAt(0);
@@ -2819,7 +3234,36 @@
                setDataCell(sheet, r, 2, 0);
                setDataCell(sheet, r, 4, 0);
            }
            // 底部单月表(新版式):标题行11(0基10)、数据行13-19(0基12-18),填当月完成量
            Row mTitle = sheet.getRow(10);
            if (mTitle != null && mTitle.getCell(0) != null) {
                mTitle.getCell(0).setCellValue(year + "年" + month + "月全省完成城市客运量情况");
            }
            double[] mProv = loadCityPassengerMonthMap(year + "-", month).getOrDefault("全省", new double[8]);
            double mBusPax = mProv[0], mBusTurn = mProv[1];
            double mTaxiPax = mProv[2], mTaxiTurn = mProv[3];
            double mRailPax = mProv[4], mRailTurn = mProv[5];
            double mFerryPax = mProv[6], mFerryTurn = mProv[7];
            setDataCell(sheet, 12, 1, mBusPax + mTaxiPax + mRailPax + mFerryPax); // B13 总客运量
            setDataCell(sheet, 12, 3, mBusTurn + mTaxiTurn + mRailTurn + mFerryTurn); // D13 总周转量
            setDataCell(sheet, 13, 1, mBusPax);   // B14 城市公交
            setDataCell(sheet, 13, 3, mBusTurn);  // D14
            setDataCell(sheet, 14, 1, mTaxiPax);  // B15 城市出租车
            setDataCell(sheet, 14, 3, mTaxiTurn); // D15
            setDataCell(sheet, 15, 1, mTaxiPax);  // B16 其中巡游出租
            setDataCell(sheet, 15, 3, mTaxiTurn); // D16
            setZeroCell(sheet, 16, 1);            // B17 城市网约车
            setZeroCell(sheet, 16, 3);            // D17
            setDataCell(sheet, 17, 1, mRailPax);  // B18 轨道
            setDataCell(sheet, 17, 3, mRailTurn); // D18
            setDataCell(sheet, 18, 1, mFerryPax); // B19 轮渡
            setDataCell(sheet, 18, 3, mFerryTurn);// D19
            for (int r = 12; r <= 18; r++) {      // 单月表 C/E 同比:无上年单月源 → 清空样例
                setDataCell(sheet, r, 2, 0);
                setDataCell(sheet, r, 4, 0);
            }
            recalc(wb);
            clearFormulaErrorsAll(wb); // 无去年数据的同比公式 #DIV/0! → 清空
            return toBytes(wb);
        }
    }
@@ -2833,8 +3277,8 @@
            addZipEntry(zos, "生成_城市公交客运量分市州明细.xlsx", exportCityBusDetail(period, mode));
            addZipEntry(zos, "生成_巡游出租客运量分市州明细.xlsx", exportCityTaxiDetail(period, mode));
            addZipEntry(zos, "生成_轨道轮渡客运量分市州明细.xlsx", exportCityRailFerryDetail(period, mode));
            addZipEntry(zos, "生成_城市客运客运量分市州累计.xlsx", exportCityPassengerCitySum("导入模板_城市分市州.xlsx", period, mode));
            addZipEntry(zos, "生成_城市客运各市州明细表.xlsx", exportCityPassengerCitySum("导入模板_城市客运各市州明细表.xlsx", period, mode));
            addZipEntry(zos, "生成_城市客运客运量分市州累计.xlsx", exportCityPassengerCitySum("生成_城市客运客运量分市州累计.xlsx", period, mode));
            addZipEntry(zos, "生成_城市客运各市州明细表.xlsx", exportCityPassengerCitySum("生成_城市客运各市州明细表.xlsx", period, mode));
            addZipEntry(zos, "生成_城市客运汇总.xlsx", exportCityPassengerSummary(period, mode));
        }
        return bos.toByteArray();
@@ -2861,6 +3305,509 @@
            addZipEntry(zos, "生成_中口径分析.xlsx", exportPassengerMidAnalysis(period, mode));
        }
        return bos.toByteArray();
    }
    /**
     * 生成_道路运输量汇总表.xlsx(P0-P1 骨架):整本复制母版《YYYY年M月道路运输量汇总表.xlsx》,
     * 保留全部页签/版式/公式/2025 年缓存值,仅把表头年份动态化为目标年;数据填充按 P2 起逐步接入。
     */
    public byte[] exportSummaryWorkbook(String period, String mode) throws Exception {
        return exportSummaryWorkbook(period, mode, null);
    }
    /** 生成_道路运输量汇总表.xlsx(P0-P1 骨架,支持 toMonth 裁剪):
     *  以《YYYY年M月道路运输量汇总表.xlsx》(M 由 period 决定)为母版,按同期备份母版还原月度块;
     *  toMonth 非空且小于 M 时,把 8 个台账/合成页裁到 toMonth 月(toMonth+1..M 的当月值/同比列清空、保留表头,
     *  累计与累计同比公式改为按 toMonth 月重算),用于“先生成 1-6 月、1-7 月扩列”验证。 */
    public byte[] exportSummaryWorkbook(String period, String mode, Integer toMonth) throws Exception {
        int year = Integer.parseInt(period.contains("-") ? period.split("-")[0] : period);
        int month = period.contains("-") ? monthOf(period, mode) : 12;
        File template = resolveSummaryMother(period, mode);
        if (template == null) {
            throw new RuntimeException("未找到汇总表母版《" + year + "年" + month + "月道路运输量汇总表.xlsx》(docs/生成汇总大表)");
        }
        double savedZipRatio = ZipSecureFile.getMinInflateRatio();
        ZipSecureFile.setMinInflateRatio(0.0001); // 新版母版 styles.xml 由 openpyxl 高压缩(0.0099<0.01)触发 POI Zip-bomb 误报,读取后还原
        try (InputStream in = new FileInputStream(template);
             XSSFWorkbook wb = new XSSFWorkbook(in)) {
            File donor = resolveSummaryDonor(year, month);
            if (donor != null) {
                try (InputStream din = new FileInputStream(donor); XSSFWorkbook dwb = new XSSFWorkbook(din)) {
                    int restored = restoreSummaryMonthlyBlocks(wb, dwb);
                    log.info("汇总工作簿月度块还原完成,补齐/覆盖单元格数={}(母版:{},备份母版:{})", restored, template.getName(), donor.getName());
                }
            } else {
                log.warn("汇总工作簿未找到同期备份母版(_备份_{}年{}月…),输出将保持母版现状(可能为 1-4 月骨架)", year, month);
            }
            if (toMonth != null && toMonth > 0 && toMonth < month) {
                int trimmed = trimSummaryMonthlySheets(wb, toMonth);
                log.info("汇总工作簿裁剪至 {} 月完成,清空/改写单元格数={}", toMonth, trimmed);
            }
            dynamicSummaryYear(wb, year);
            cleanSummarySheetPresentation(wb); // 取消各页筛选、还原隐藏行(否则“班线包车”等页看起来像缺数据)
            int effMonth = (toMonth != null && toMonth > 0 && toMonth < month) ? toMonth : month;
            List<String> dbProblems = new ArrayList<>();
            int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, year, effMonth, dbProblems);
            log.info("汇总工作簿数据驱动回填完成:写入台账页当月值格数={},缺数提示={}", dbFilled, dbProblems);
            applyTwoDecimalFormat(wb); // 数值显示两位小数(保留全精度);% / 日期等既有样式不改变
            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 还原/改写公式后打开即重算
            return toBytes(wb);
        } finally {
            ZipSecureFile.setMinInflateRatio(savedZipRatio);
        }
    }
    /** 汇总表母版定位:仅精确匹配 {year}年{month}月…(P0-P1 骨架期不允许跨月/跨年静默回退,
     *  目标期无对应母版时明确报错,月份扩展规则待 P0 母版定稿后再放开) */
    private File resolveSummaryMother(String period, String mode) throws Exception {
        String year = period != null && period.contains("-") ? period.split("-")[0] : (period == null ? "" : period);
        int month = period != null && period.contains("-") ? monthOf(period, mode) : 12;
        String name = year + "年" + month + "月道路运输量汇总表.xlsx";
        return resolveAnyTemplate(summaryTemplateDir, name);
    }
    // ==================== 网约车客运量拆分表(月度) ====================
    /** 网约车客运量拆分表:输入=部级订单(网约车订单及全省总量.xlsx G-W)+ 全省 pin(B-E)+ 巡游出租月报(DB);
     *  算法与《网约车拆分.xlsx》月页一致:客运量=订单占比×全省总量;城市内=巡游出租城市内占比代理、武汉保持自身其余按剩余分摊;
     *  周转量=客运量×巡游出租平均运距占比分摊全省周转量 pin,城市内周转量同法。 */
    public byte[] exportWycSplit(String period, String mode) throws Exception {
        if (period == null || !period.contains("-")) {
            throw new RuntimeException("网约车客运量拆分表仅支持按月生成,请选择具体月份");
        }
        int year = Integer.parseInt(period.split("-")[0]);
        int month = parseMonth(period);
        Map<String, Object> inp = loadWycMonthlyInput(period, mode);
        if (inp == null) {
            throw new RuntimeException("《网约车订单及全省总量.xlsx》缺少报表期 " + period + " 的数据行,请先维护当月订单与全省 pin");
        }
        double pinK = (Double) inp.get("pinK");
        double pinCK = (Double) inp.get("pinCK");
        double pinZ = (Double) inp.get("pinZ");
        double pinCZ = (Double) inp.get("pinCZ");
        double orderSum = (Double) inp.get("orderSum");
        double[] orderArr = (double[]) inp.get("orders");
        if (pinK <= 0 || pinCK <= 0 || pinZ <= 0 || pinCZ <= 0 || orderSum <= 0) {
            throw new RuntimeException("网约车输入 " + period + " 行不完整:全省 pin / 订单缺失,请核对《网约车订单及全省总量.xlsx》");
        }
        List<String> cities = RegionUtil.cityList();
        int n = cities.size();
        if (orderArr == null || orderArr.length < n) {
            throw new RuntimeException("网约车订单列数不足(应为 17 市州订单列)");
        }
        Map<String, double[]> taxi = taxiMonthlyOf(period);
        java.util.List<String> missingTaxi = new java.util.ArrayList<>();
        double[] Dtax = new double[n];
        double[] Etax = new double[n];
        double[] avgD = new double[n];
        double[] avgDc = new double[n];
        for (int i = 0; i < n; i++) {
            double[] a = taxi.get(cities.get(i));
            if (a == null || a[0] <= 0) {
                missingTaxi.add(cities.get(i));
                continue;
            }
            Dtax[i] = a[0];
            Etax[i] = a[2];
            avgD[i] = a[4];
            avgDc[i] = a[5];
        }
        if (!missingTaxi.isEmpty()) {
            throw new RuntimeException("巡游出租月报缺少以下市州数据:" + String.join("、", missingTaxi));
        }
        double[] C = new double[n];
        double[] H = new double[n];
        double[] I = new double[n];
        double[] Gl = new double[n];
        double[] Hl = new double[n];
        double[] Il = new double[n];
        double sumG = 0;
        double sumDl = 0;
        double sumEl = 0;
        for (int i = 0; i < n; i++) {
            C[i] = orderArr[i] / orderSum * pinK;
            double F = Etax[i] / Dtax[i];
            double G = C[i] * F;
            sumG += G;
            double Dl = C[i] * avgD[i];
            double El = H[i] * avgDc[i];
            sumDl += Dl;
            sumEl += El;
        }
        // 城市内:武汉保持自身,其余按 G 剩余占比分摊至全省城市内 pin
        for (int i = 0; i < n; i++) {
            if (i == 0) {
                H[i] = C[i] * (Etax[i] / Dtax[i]);
            } else {
                H[i] = (C[i] * (Etax[i] / Dtax[i])) / (sumG - C[0] * (Etax[0] / Dtax[0])) * (pinCK - C[0] * (Etax[0] / Dtax[0]));
            }
            I[i] = C[i] - H[i];
        }
        // 周转量:按 客运量×巡游出租平均运距 占比分摊全省周转量 pin
        double whG = C[0] * (Etax[0] / Dtax[0]);
        double whDl = C[0] * avgD[0];
        double[] El2 = new double[n];
        double sumEl2 = 0;
        for (int i = 0; i < n; i++) {
            double Dl = C[i] * avgD[i];
            double El2v = H[i] * avgDc[i];
            El2[i] = El2v;
            sumEl2 += El2v;
            Gl[i] = Dl / sumDl * pinZ;
        }
        for (int i = 0; i < n; i++) {
            if (i == 0) {
                Hl[i] = Gl[i];
            } else {
                Hl[i] = El2[i] / (sumEl2 - El2[0]) * (pinCZ - Gl[0]);
            }
            Il[i] = Gl[i] - Hl[i];
        }
        return writeWycSplitWorkbook(period, cities, orderArr, C, Dtax, Etax, H, I, avgD, avgDc, Gl, Hl, Il, pinK, pinCK, pinZ, pinCZ, orderSum);
    }
    private byte[] writeWycSplitWorkbook(String period, List<String> cities, double[] orders, double[] C, double[] Dtax,
                                         double[] Etax, double[] H, double[] I, double[] avgD, double[] avgDc,
                                         double[] Gl, double[] Hl, double[] Il,
                                         double pinK, double pinCK, double pinZ, double pinCZ, double orderSum) throws Exception {
        XSSFWorkbook wb = new XSSFWorkbook();
        XSSFSheet sh = wb.createSheet(period + "拆分");
        int n = cities.size();
        // 客运量块
        Row t = sh.createRow(0);
        t.createCell(0).setCellValue("网约车客运量拆分(万人)  报表期:" + period);
        String[] head1 = {"市州", "订单总数(单)", "网约车客运量(万人)", "巡游出租客运量(万人)", "巡游出租城市内客运量(万人)", "城市内占比", "网约车城市内客运量(万人)", "网约车城际城乡客运量(万人)"};
        Row h1 = sh.createRow(1);
        for (int j = 0; j < head1.length; j++) h1.createCell(j).setCellValue(head1[j]);
        int r = 2;
        writeWycRow(sh, r++, "全省", orderSum, pinK, sum(Dtax), sum(Etax), pinK > 0 ? pinCK / pinK : 0, pinCK, pinK - pinCK);
        for (int i = 0; i < n; i++) {
            writeWycRow(sh, r++, cities.get(i), orders[i], C[i], Dtax[i], Etax[i], Dtax[i] > 0 ? Etax[i] / Dtax[i] : 0, H[i], I[i]);
        }
        r += 1;
        Row t2 = sh.createRow(r++);
        t2.createCell(0).setCellValue("旅客周转量拆分(万人公里)");
        String[] head2 = {"市州", "出租车平均运距(公里)", "出租车城市内平均运距(公里)", "网约车旅客周转量(万人公里)", "网约车城市内旅客周转量(万人公里)", "网约车城际城乡旅客周转量(万人公里)"};
        Row h2 = sh.createRow(r++);
        for (int j = 0; j < head2.length; j++) h2.createCell(j).setCellValue(head2[j]);
        writeWycRow2(sh, r++, "全省", 0, 0, pinZ, pinCZ, pinZ - pinCZ);
        for (int i = 0; i < n; i++) {
            writeWycRow2(sh, r++, cities.get(i), avgD[i], avgDc[i], Gl[i], Hl[i], Il[i]);
        }
        r += 1;
        String[] notes = {
            "注:1) 客运量 C = 订单占比 × 全省网约车客运量;城市内 H:武汉=自身,其余按 G 剩余占比分摊至全省城市内 pin;城际城乡 = 总量 - 城市内。",
            "    2) 周转量 G = 客运量 × 巡游出租平均运距 的占比 × 全省网约车周转量 pin;城市内周转量同法(用城市内平均运距)。",
            "    3) 输入:订单 = 当月分析报告附件1(17 市州订单数);全省 pin = 网约车总数.xlsx;巡游出租 = 巡游出租月报(系统导入,含平均运距)。",
            "    4) 武汉无城际城乡;数值保留全精度,界面/文件显示两位小数。"
        };
        for (String nt : notes) {
            Row nr = sh.createRow(r++);
            nr.createCell(0).setCellValue(nt);
        }
        for (int j = 0; j < 8; j++) sh.setColumnWidth(j, 18 * 256);
        return toBytes(wb);
    }
    private void writeWycRow(Sheet sh, int r, String name, double ord, double k, double dt, double et, double f, double h, double i) {
        Row row = sh.createRow(r);
        row.createCell(0).setCellValue(name);
        row.createCell(1).setCellValue(ord);
        row.createCell(2).setCellValue(k);
        row.createCell(3).setCellValue(dt);
        row.createCell(4).setCellValue(et);
        row.createCell(5).setCellValue(f);
        row.createCell(6).setCellValue(h);
        row.createCell(7).setCellValue(i);
    }
    private void writeWycRow2(Sheet sh, int r, String name, double ad, double adc, double g, double h, double i) {
        Row row = sh.createRow(r);
        row.createCell(0).setCellValue(name);
        row.createCell(1).setCellValue(ad);
        row.createCell(2).setCellValue(adc);
        row.createCell(3).setCellValue(g);
        row.createCell(4).setCellValue(h);
        row.createCell(5).setCellValue(i);
    }
    private double sum(double[] a) {
        double s = 0;
        for (double v : a) s += v;
        return s;
    }
    /** 读取 docs/城市客运/网约车订单及全省总量.xlsx:按 period(yyyy-MM) 行取 pin(B-E) 与 17 市州订单(G-W) */
    private Map<String, Object> loadWycMonthlyInput(String period, String mode) throws Exception {
        String target = period;
        File f;
        try {
            f = resolveAnyTemplate(templateDir, "网约车订单及全省总量.xlsx");
        } catch (Exception e) {
            throw new RuntimeException("未找到《网约车订单及全省总量.xlsx》,请放到 docs/城市客运 目录");
        }
        try (InputStream in = new FileInputStream(f);
             org.apache.poi.ss.usermodel.Workbook wb = org.apache.poi.ss.usermodel.WorkbookFactory.create(in)) {
            org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(0);
            org.apache.poi.ss.usermodel.DataFormatter df = new org.apache.poi.ss.usermodel.DataFormatter();
            List<String> cities = RegionUtil.cityList();
            int n = cities.size();
            for (int i = 1; i <= sh.getLastRowNum(); i++) {
                Row row = sh.getRow(i);
                if (row == null) continue;
                Cell c0 = row.getCell(0);
                if (c0 == null) continue;
                String m = df.formatCellValue(c0).trim();
                if (m == null || m.isEmpty()) continue;
                if (m.length() > 7) m = m.substring(0, 7);
                if (!target.equals(m)) continue;
                Map<String, Object> map = new HashMap<>();
                map.put("pinK", cellNum(row, 1, df, 0.0));
                map.put("pinCK", cellNum(row, 2, df, 0.0));
                map.put("pinZ", cellNum(row, 3, df, 0.0));
                map.put("pinCZ", cellNum(row, 4, df, 0.0));
                double[] orders = new double[n];
                double orderSum = 0;
                for (int k = 0; k < n; k++) {
                    orders[k] = cellNum(row, 6 + k, df, 0.0);
                    orderSum += orders[k];
                }
                if (orderSum <= 0) {
                    orderSum = cellNum(row, 5, df, 0.0); // F 列合计兜底
                }
                map.put("orders", orders);
                map.put("orderSum", orderSum);
                return map;
            }
            return null;
        }
    }
    private double cellNum(Row row, int idx, org.apache.poi.ss.usermodel.DataFormatter df, double def) {
        Cell c = row.getCell(idx);
        if (c == null) return def;
        if (c.getCellType() == CellType.NUMERIC) return c.getNumericCellValue();
        String s = df.formatCellValue(c).replace(",", "").trim();
        if (s == null || s.isEmpty()) return def;
        try {
            return Double.parseDouble(s);
        } catch (Exception e) {
            return def;
        }
    }
    /** 巡游出租月报(DB)按月加载:市州 -> [客运量, 周转量, 城市内客运量, 城市内周转量, 平均运距, 城市内平均运距] */
    private Map<String, double[]> taxiMonthlyOf(String period) {
        Map<String, double[]> map = new java.util.LinkedHashMap<>();
        List<CityTaxiMonthly> rows = cityTaxiMapper.selectList(
            new LambdaQueryWrapper<CityTaxiMonthly>().eq(CityTaxiMonthly::getReportPeriod, period));
        for (CityTaxiMonthly r : rows) {
            String city = RegionUtil.normalizeCityName(r.getCity());
            if (city == null || city.isEmpty()) city = "未知";
            double[] a = map.computeIfAbsent(city, k -> new double[6]);
            a[0] += nz(r.getPassengerVolume());
            a[1] += nz(r.getTurnover());
            a[2] += nz(r.getPassengerCity());
            a[3] += nz(r.getTurnoverCity());
            if (r.getAvgDistance() != null && r.getAvgDistance() > 0) a[4] = r.getAvgDistance();
            if (r.getAvgDistanceCity() != null && r.getAvgDistanceCity() > 0) a[5] = r.getAvgDistanceCity();
        }
        for (double[] a : map.values()) {
            if (a[4] <= 0 && a[0] > 0) a[4] = round(a[1] / a[0], 2);
            if (a[5] <= 0 && a[2] > 0) a[5] = round(a[3] / a[2], 2);
        }
        return map;
    }
    /** 定位同期备份母版(清理前的完整月母版),用于把骨架母版缺失的 5-7 月月度块按备份还原;
     *  与 resolveAnyTemplate 相同策略:在配置目录、user.dir 相对目录、上级目录三个候选根中扫描 */
    private File resolveSummaryDonor(int year, int month) {
        String rel = summaryTemplateDir;
        while (rel.startsWith("./")) rel = rel.substring(2);
        String[] roots = {
            summaryTemplateDir,
            System.getProperty("user.dir") + "/" + rel,
            System.getProperty("user.dir") + "/../" + rel
        };
        for (String root : roots) {
            if (root == null || root.trim().isEmpty()) continue;
            File dir = new File(root);
            File[] files = dir.listFiles((d, name) ->
                name.startsWith("_备份_") && name.contains(year + "年" + month + "月") && name.contains("道路运输量汇总表"));
            if (files != null && files.length > 0) return files[0];
        }
        return null;
    }
    /** 按同期备份母版还原月度块:凡备份有内容而母版同格缺失/内容不同(清理时被裁掉的 5-7 月值与累计公式)的单元格,整格回填并覆盖 */
    private int restoreSummaryMonthlyBlocks(XSSFWorkbook wb, XSSFWorkbook donor) {
        int restored = 0;
        for (int i = 0; i < donor.getNumberOfSheets(); i++) {
            Sheet ds = donor.getSheetAt(i);
            if (ds == null) continue;
            Sheet bs = wb.getSheet(ds.getSheetName());
            if (bs == null) continue;
            // 中口径排名页 A..H 陈旧左块已于 2026-09-09 拍板整块删除:该区域永不从备份母版回填,避免“生成又复活”
            boolean skipMidRankLeftBlock = "中口径排名".equals(ds.getSheetName());
            for (int r = ds.getFirstRowNum(); r <= ds.getLastRowNum(); r++) {
                Row dr = ds.getRow(r);
                if (dr == null) continue;
                Row br = bs.getRow(r);
                for (Cell dc : dr) {
                    if (dc == null) continue;
                    if (skipMidRankLeftBlock && dc.getColumnIndex() < 8) continue;
                    CellType t = dc.getCellType();
                    if (t == CellType.BLANK) continue;
                    String dtx = summaryCellText(dc);
                    if (dtx == null || dtx.isEmpty()) continue;
                    Cell bc = (br == null) ? null : br.getCell(dc.getColumnIndex());
                    if (bc != null && dtx.equals(summaryCellText(bc))) continue;
                    Row tr = (bc != null) ? br : bs.getRow(r);
                    if (tr == null) tr = bs.createRow(r);
                    Cell tc = (bc != null) ? bc : tr.getCell(dc.getColumnIndex());
                    if (tc == null) tc = tr.createCell(dc.getColumnIndex());
                    tc.setBlank();
                    applyDonorNumberFormat(wb, tc, dc);
                    if (t == CellType.FORMULA) {
                        String f = dc.getCellFormula();
                        if (f != null) tc.setCellFormula(f);
                    } else if (t == CellType.STRING) {
                        tc.setCellValue(dc.getStringCellValue());
                    } else if (t == CellType.NUMERIC) {
                        tc.setCellValue(dc.getNumericCellValue());
                    } else if (t == CellType.BOOLEAN) {
                        tc.setCellValue(dc.getBooleanCellValue());
                    }
                    restored++;
                }
            }
        }
        return restored;
    }
    /** 还原回填时同步 donor(已做版式统一)的数字格式,保证同比/数值/排名格显示格式与母版一致 */
    private void applyDonorNumberFormat(XSSFWorkbook wb, Cell dst, Cell src) {
        if (wb == null || dst == null || src == null) return;
        try {
            String fmt = src.getCellStyle().getDataFormatString();
            if (fmt == null || fmt.isEmpty() || "General".equalsIgnoreCase(fmt)) return;
            org.apache.poi.ss.usermodel.DataFormat df = wb.createDataFormat();
            short idx = df.getFormat(fmt);
            CellStyle cur = dst.getCellStyle();
            if (cur != null && idx == cur.getDataFormat()) return;
            CellStyle ns = wb.createCellStyle();
            if (cur != null) ns.cloneStyleFrom(cur);
            ns.setDataFormat(idx);
            dst.setCellStyle(ns);
        } catch (Exception ignore) {
            // 个别格式异常不阻塞还原
        }
    }
    /** 单元格内容文本化(公式带 = 前缀),用于跨簿比对 */
    private String summaryCellText(Cell c) {
        if (c == null) return null;
        switch (c.getCellType()) {
            case FORMULA: {
                String f = c.getCellFormula();
                return f == null ? "" : "=" + f;
            }
            case STRING:
                return c.getStringCellValue();
            case NUMERIC:
                return Double.toString(c.getNumericCellValue());
            case BOOLEAN:
                return Boolean.toString(c.getBooleanCellValue());
            default:
                return "";
        }
    }
    /** 把 8 个台账/合成页裁剪到 keepMonths 月:
     *  月度值列 2026 年 m 月 = 第 3+2*(m-1) 列(C/E/G/I/K/M/O),同比列为其后一列;
     *  累计 Q 列 = C+E+...+O,累计同比 R 列分母 = 2025 年同月列 T/V/X/Z/AB/AD/AF。
     *  裁剪 = 数据行(含累计公式且任一 2026 月度列有内容)中 keepMonths+1..7 月的当月值/同比单元格清空(表头保留),
     *  并把该行累计公式去掉对应 +列,累计同比分母去掉对应 2025 列。 */
    private int trimSummaryMonthlySheets(XSSFWorkbook wb, int keepMonths) {
        String[] scope = {" 货运", "班线包车", "城市客运", "公交", "出租", "网约车", "轨道、轮渡", "中口径明细"};
        java.util.Set<String> scopeSet = new java.util.HashSet<>();
        for (String s : scope) scopeSet.add(s);
        int touched = 0;
        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
            Sheet sh = wb.getSheetAt(i);
            if (sh == null || !scopeSet.contains(sh.getSheetName())) continue;
            for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
                Row row = sh.getRow(r);
                if (row == null) continue;
                Cell q = row.getCell(16); // Q 列 = 累计
                if (q == null || q.getCellType() != CellType.FORMULA) continue;
                boolean hasMonthly = false;
                for (int m = 1; m <= 7 && !hasMonthly; m++) {
                    Cell v = row.getCell(2 + 2 * (m - 1)); // C,E,G,I,K,M,O
                    if (v != null && v.getCellType() != CellType.BLANK) hasMonthly = true;
                }
                if (!hasMonthly) continue; // 排除页首/页中跨页签校验公式行(无月度值)
                int excelRow = r + 1; // 公式内引用为 Excel 1-based 行号
                for (int m = keepMonths + 1; m <= 7; m++) {
                    int valueCol = 2 + 2 * (m - 1);     // 2026 当月值列
                    int yoyCol = valueCol + 1;          // 当月同比列
                    String vLetter = colLetter(valueCol);
                    Cell vc = row.getCell(valueCol);
                    if (vc != null) {
                        vc.setBlank();
                        touched++;
                    }
                    Cell yc = row.getCell(yoyCol);
                    if (yc != null) {
                        yc.setBlank();
                        touched++;
                    }
                    String fq = q.getCellFormula();
                    if (fq != null && fq.contains("+" + vLetter + excelRow)) {
                        q.setCellFormula(fq.replace("+" + vLetter + excelRow, ""));
                        touched++;
                    }
                    Cell rc = row.getCell(17); // R 列 = 累计同比
                    if (rc != null && rc.getCellType() == CellType.FORMULA) {
                        String fr = rc.getCellFormula();
                        int lastCol = 19 + 2 * (m - 1); // 2025 同月列 T..AF(0-based:T=19,AF=31)
                        String lLetter = colLetter(lastCol);
                        String txt = fr;
                        if (txt != null && txt.contains("," + lLetter + excelRow)) {
                            txt = txt.replace("," + lLetter + excelRow, "");
                            touched++;
                        }
                        if (txt != null && txt.contains("+" + lLetter + excelRow)) {
                            txt = txt.replace("+" + lLetter + excelRow, "");
                            touched++;
                        }
                        if (txt != null && !txt.equals(fr)) rc.setCellFormula(txt);
                    }
                }
            }
        }
        return touched;
    }
    /** 汇总工作簿年份动态化:仅把 2026年 前缀替换为目标年(2025/2024 参考列不动) */
    private void dynamicSummaryYear(XSSFWorkbook wb, int year) {
        String oldPrefix = "2026年";
        String newPrefix = year + "年";
        if (oldPrefix.equals(newPrefix)) return;
        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.STRING) continue;
                    String v = c.getStringCellValue();
                    if (v != null && v.contains(oldPrefix)) {
                        c.setCellValue(v.replace(oldPrefix, newPrefix));
                    }
                }
            }
        }
    }
    /** 生成_投资报表系列.zip:预安排 + 十五五 + 亿元 + 经济强县 */
@@ -2903,6 +3850,7 @@
            case "passengerMidDetail": return exportPassengerMidDetail(period, mode);
            case "passengerMidRank": return exportPassengerMidRank(period, mode);
            case "passengerMidAnalysis": return exportPassengerMidAnalysis(period, mode);
            case "summaryWorkbook": return exportSummaryWorkbook(period, mode);
            case "energySummary": return exportEnergySummary(period);
            case "investPlan": return investPlan(period, mode);
            case "investFiveYearLogistics": return investFiveYearLogistics(period, mode);
@@ -2911,9 +3859,10 @@
            case "cityBusDetail": return exportCityBusDetail(period, mode);
            case "cityTaxiDetail": return exportCityTaxiDetail(period, mode);
            case "cityRailFerryDetail": return exportCityRailFerryDetail(period, mode);
            case "cityPassengerCitySum": return exportCityPassengerCitySum("导入模板_城市分市州.xlsx", period, mode);
            case "cityPassengerDetailSum": return exportCityPassengerCitySum("导入模板_城市客运各市州明细表.xlsx", period, mode);
            case "cityPassengerCitySum": return exportCityPassengerCitySum("生成_城市客运客运量分市州累计.xlsx", period, mode);
            case "cityPassengerDetailSum": return exportCityPassengerCitySum("生成_城市客运各市州明细表.xlsx", period, mode);
            case "cityPassengerSummary": return exportCityPassengerSummary(period, mode);
            case "wycSplit": return exportWycSplit(period, mode);
            default: return null;
        }
    }
@@ -2938,6 +3887,8 @@
        REPORT_FILE_NAMES.put("cityPassengerCitySum", "生成_城市客运客运量分市州累计.xlsx");
        REPORT_FILE_NAMES.put("cityPassengerDetailSum", "生成_城市客运各市州明细表.xlsx");
        REPORT_FILE_NAMES.put("cityPassengerSummary", "生成_城市客运汇总.xlsx");
        REPORT_FILE_NAMES.put("summaryWorkbook", "生成_道路运输量汇总表.xlsx");
        REPORT_FILE_NAMES.put("wycSplit", "生成_网约车客运量拆分表.xlsx");
    }
    private void addZipEntry(java.util.zip.ZipOutputStream zos, String name, byte[] data) throws Exception {
@@ -2996,7 +3947,53 @@
            } catch (Exception e) {
                log.warn("城市客运模板公式求值失败: {}", e.getMessage());
            }
            clearFormulaErrorCells(sheet, 4, sheet.getLastRowNum(), 3, 39);
            fixCityMonthHeaderText(sheet);
            return toBytes(wb);
        }
    }
    /** 清空求值结果为错误的公式单元格(模板跨簿同比公式残留 #REF! 时移除公式,避免导出显示错误) */
    private void clearFormulaErrorCells(Sheet sheet, int startRow, int endRow, int startCol, int endCol) {
        for (int r = startRow; r <= endRow; r++) {
            Row row = sheet.getRow(r);
            if (row == null) continue;
            for (int c = startCol; c <= endCol; c++) {
                Cell cell = row.getCell(c);
                if (cell == null || cell.getCellType() != CellType.FORMULA) continue;
                try {
                    if (cell.getCachedFormulaResultType() == CellType.ERROR) cell.setBlank();
                } catch (Exception ignore) {
                    // 缓存类型读取失败时忽略
                }
            }
        }
    }
    /** 清空工作簿全部工作表内求值结果为错误的公式单元格(跨簿/跨表 #REF!、缺去年数据 #DIV/0! 等) */
    private void clearFormulaErrorsAll(XSSFWorkbook wb) {
        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
            Sheet s = wb.getSheetAt(i);
            clearFormulaErrorCells(s, 2, s.getLastRowNum(), 1, 60);
        }
    }
    /** 城市客运类模板表头月份列若为未格式化日期序列号(如 46082),改写为「YYYY年M月」文本 */
    private void fixCityMonthHeaderText(Sheet sheet) {
        for (int r = 0; r <= Math.min(sheet.getLastRowNum(), 20); r++) {
            Row row = sheet.getRow(r);
            if (row == null) continue;
            Cell nameCell = row.getCell(0);
            String nameText = nameCell == null ? null : cellText(nameCell);
            if (nameText == null || !nameText.contains("地区")) continue;
            for (int c = 2; c <= 40; c += 2) {
                Cell cell = row.getCell(c);
                if (cell == null || cell.getCellType() != CellType.NUMERIC) continue;
                double v = cell.getNumericCellValue();
                if (v < 40000 || v > 60000) continue;
                java.util.Date d = org.apache.poi.ss.usermodel.DateUtil.getJavaDate(v);
                cell.setCellValue(new java.text.SimpleDateFormat("yyyy年M月").format(d));
            }
        }
    }
@@ -3142,6 +4139,7 @@
    }
    private byte[] toBytesHssf(HSSFWorkbook wb) throws Exception {
        applyTwoDecimalFormat(wb);
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            wb.write(out);
            wb.close();
@@ -3221,11 +4219,11 @@
    }
    /** 表1 项目行填充(模板行/插入行共用;cityText 与模板 B 列格式一致) */
    private void fillInvestPlanProjectRow(HSSFRow row, InvestmentProject p, InvestmentMonthly m, int seq, String cityText) {
    private void fillInvestPlanProjectRow(HSSFRow row, InvestmentProject p, InvestmentMonthly m, int seq, String cityText, String displayName) {
        hssfNum(row, 0, (double) seq);
        hssfText(row, 1, cityText);
        hssfText(row, 2, valueOf(p.getCounty()));
        hssfText(row, 3, p.getProjectName());
        hssfText(row, 3, displayName != null && !displayName.trim().isEmpty() ? displayName.trim() : p.getProjectName());
        hssfText(row, 4, valueOf(p.getConstructNature()));
        hssfNum(row, 5, parseYearNum(p.getStartTime()));
        hssfNum(row, 6, parseYearNum(p.getEndTime()));
@@ -3332,6 +4330,7 @@
        // 原位逐行替换:合计/块标题/市州小节/分组/项目行;未匹配的模板行清空样例数据(结构/合并/列宽不变)
        int seq = 0;
        int si = -1;
        java.util.Set<Long> usedProjectIds = new HashSet<>();
        for (int r = 5; r <= lastRow; r++) {
            HSSFRow row = sheet.getRow(r);
            if (row == null) continue;
@@ -3391,11 +4390,12 @@
            // 项目行:按 模板名称+市州+块类别 匹配 DB 项目,未匹配整行清空
            blankInvestRow(row, 25);
            if (blockCategory == null) continue; // 老旧货车段无数据源
            InvestmentProject p = matchInvest(projects, d, b, blockCategory);
            InvestmentProject p = matchInvest(projects, d, b, blockCategory, usedProjectIds);
            if (p == null) continue;
            usedProjectIds.add(p.getId());
            if (sec != null) sec.matched.add(p.getId());
            InvestmentMonthly m = mByProject.get(p.getId());
            fillInvestPlanProjectRow(row, p, m, ++seq, b);
            fillInvestPlanProjectRow(row, p, m, ++seq, b, d);
        }
        // 动态插行:DB 项目数 > 模板预留行数时,在市州小节末尾补齐(自底向上插行,避免行号错位)
        for (int i = sections.size() - 1; i >= 0; i--) {
@@ -3415,7 +4415,7 @@
                HSSFRow row = sheet.getRow(first + j);
                if (row == null) continue;
                InvestmentProject p = extra.get(j);
                fillInvestPlanProjectRow(row, p, mByProject.get(p.getId()), 0, p.getCity());
                fillInvestPlanProjectRow(row, p, mByProject.get(p.getId()), 0, p.getCity(), p.getProjectName());
            }
        }
        renumberInvestSeq(sheet, "investPlan");
@@ -3725,6 +4725,11 @@
    /** 在候选项目池中按名称匹配 DB 项目(可选 市州/类别 过滤 + 县区前缀消歧),未命中返回 null */
    private InvestmentProject matchInvest(List<InvestmentProject> pool, String name, String cityFull, String category) {
        return matchInvest(pool, name, cityFull, category, null);
    }
    /** 带已占用排除的匹配:同一 DB 项目只允许填充一行(避免模板一期/二期同名行重复输出) */
    private InvestmentProject matchInvest(List<InvestmentProject> pool, String name, String cityFull, String category, Set<Long> excludeIds) {
        if (name == null || name.trim().isEmpty()) return null;
        String norm = normInvestName(name);
        if (norm.isEmpty()) return null;
@@ -3732,6 +4737,7 @@
        String county = extractInvestCounty(name);
        List<InvestmentProject> cands = new java.util.ArrayList<>();
        for (InvestmentProject p : pool) {
            if (excludeIds != null && excludeIds.contains(p.getId())) continue;
            if (category != null && !category.equals(p.getCategory())) continue;
            if (cityCanon != null && !cityCanon.equals(RegionUtil.normalizeCityName(p.getCity()))) continue;
            cands.add(p);
@@ -3867,7 +4873,73 @@
    private double nz(Double v) {
        return v == null ? 0.0 : v;
    }
    /** 生成表数值列统一样式:保留全精度数值,显示两位小数;整数值与 %/日期/科学计数等既有样式不改变 */
    private void applyTwoDecimalFormat(org.apache.poi.ss.usermodel.Workbook wb) {
        if (wb == null) return;
        org.apache.poi.ss.usermodel.DataFormat df = wb.createDataFormat();
        short fmtTwo = df.getFormat("0.00");
        for (int s = 0; s < wb.getNumberOfSheets(); s++) {
            org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(s);
            if (sh == null) continue;
            for (org.apache.poi.ss.usermodel.Row row : sh) {
                if (row == null) continue;
                for (org.apache.poi.ss.usermodel.Cell cell : row) {
                    if (cell == null) continue;
                    try {
                        CellType ct = cell.getCellType();
                        if (ct != CellType.NUMERIC && ct != CellType.FORMULA) continue;
                        if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) continue;
                        boolean integral = false;
                        if (ct == CellType.NUMERIC) {
                            integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue();
                        } else if (cell.getCachedFormulaResultType() == CellType.NUMERIC) {
                            integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue();
                        }
                        if (integral) continue; // 整数不显示 .00(含公式结果为整数的格)
                        org.apache.poi.ss.usermodel.CellStyle cs = cell.getCellStyle();
                        if (cs == null) continue;
                        String f = cs.getDataFormatString();
                        if (f == null) f = "";
                        if (f.contains("%") || f.contains("E") || f.contains("0.00")) continue;
                        boolean plain = f.isEmpty() || "General".equals(f) || "general".equals(f)
                            || "0".equals(f) || "#,##0".equals(f) || "0.0".equals(f) || "#,##0.0".equals(f);
                        if (!plain) continue;
                        org.apache.poi.ss.usermodel.CellStyle ns = wb.createCellStyle();
                        ns.cloneStyleFrom(cs);
                        ns.setDataFormat(fmtTwo);
                        cell.setCellStyle(ns);
                    } catch (Exception ignore) {
                        // 个别单元格格式异常不阻塞导出
                    }
                }
            }
        }
    }
    /** 汇总表导出后按内容自动加宽列宽(只加宽不缩窄;跳过合并单元格标题,防长标题把单列撑爆) */
    /** 汇总大表输出前清理母版残留下来的筛选与行隐藏:筛选箭头/隐藏行会让整页看起来“缺数据” */
    private void cleanSummarySheetPresentation(org.apache.poi.ss.usermodel.Workbook wb) {
        if (wb == null) return;
        for (int s = 0; s < wb.getNumberOfSheets(); s++) {
            org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(s);
            if (sh == null) continue;
            if (sh instanceof org.apache.poi.xssf.usermodel.XSSFSheet) {
                org.apache.poi.xssf.usermodel.XSSFSheet xs = (org.apache.poi.xssf.usermodel.XSSFSheet) sh;
                if (xs.getCTWorksheet().isSetAutoFilter()) xs.getCTWorksheet().unsetAutoFilter();
                for (org.apache.poi.ss.usermodel.Row row : xs) {
                    if (row == null) continue;
                    org.apache.poi.xssf.usermodel.XSSFRow xr = (org.apache.poi.xssf.usermodel.XSSFRow) row;
                    if (xr.getCTRow().isSetHidden()) xr.getCTRow().unsetHidden();
                }
            } else {
                sh.setAutoFilter(null);
            }
        }
    }
    private byte[] toBytes(XSSFWorkbook wb) throws Exception {
        applyTwoDecimalFormat(wb);
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            wb.write(out);
            wb.close();