zhizhijie
4 天以前 978526b474da9a495554ddc924bd736c52bb549b
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
@@ -1808,6 +1808,72 @@
        }
    }
    /** 只校验「必需列按名存在」(不校验列位):用于兼容同事版模板的列位差异 */
    private void requireHeaderColsByName(Sheet sheet, String typeLabel, String templateHint, String[] keywords) {
        Row header = sheet == null ? null : sheet.getRow(0);
        if (header == null) {
            throw new RuntimeException("所选数据类型【" + typeLabel + "】文件表头缺失,请使用系统模板《" + templateHint + "》整理后再导入");
        }
        List<String> missing = new ArrayList<>();
        for (String keyword : keywords) {
            if (findColByKeywords(header, keyword) < 0) missing.add(keyword);
        }
        if (!missing.isEmpty()) {
            throw new RuntimeException("所选数据类型【" + typeLabel + "】与文件内容不符:表头缺少必需列【"
                + String.join("】【", missing) + "】,请确认是否选错了数据类型(模板:" + templateHint + ")");
        }
    }
    /** 表头名 → 列号(首次出现);未命中时回退 fallback,保持对旧模板兼容 */
    private int colByName(Row header, int fallback, String name) {
        return colByName(header, fallback, name, 1);
    }
    /** 表头名 → 列号(第 occurrence 次出现);未命中时回退 fallback */
    private int colByName(Row header, int fallback, String name, int occurrence) {
        if (header == null) return fallback;
        int hit = 0;
        for (int c = 0; c <= header.getLastCellNum(); c++) {
            Cell cell = header.getCell(c);
            if (cell == null) continue;
            String v = FORMATTER.formatCellValue(cell).trim();
            if (v.contains(name)) {
                hit++;
                if (hit == occurrence) return c;
            }
        }
        return fallback;
    }
    /** 按表头关键字定位列号:从第 1 列向右扫,命中任一关键字即返回该列;未命中返回 -1 */
    private int findColByKeywords(Row header, String... keywords) {
        if (header == null) return -1;
        for (int c = 0; c <= header.getLastCellNum(); c++) {
            Cell cell = header.getCell(c);
            if (cell == null) continue;
            String h = FORMATTER.formatCellValue(cell).trim();
            if (h.isEmpty()) continue;
            for (String k : keywords) {
                if (h.contains(k)) return c;
            }
        }
        return -1;
    }
    /** 表头行文本(报错提示用),最多取 12 列 */
    private String headerText(Row header) {
        if (header == null) return "空";
        StringBuilder sb = new StringBuilder();
        for (int c = 0; c < Math.min(header.getLastCellNum(), 12); c++) {
            Cell cell = header.getCell(c);
            String h = cell == null ? "" : FORMATTER.formatCellValue(cell).trim();
            if (h.isEmpty()) continue;
            if (sb.length() > 0) sb.append(" | ");
            sb.append(h);
        }
        return sb.length() == 0 ? "空" : sb.toString();
    }
    /** 按表头名称建立列号映射,兼容新旧模板列位差异 */
    private Map<String, Integer> buildHeaderMap(Row header) {
        Map<String, Integer> colMap = new HashMap<>();
@@ -2413,6 +2479,41 @@
            || normDash.contains(period) || normDash.contains(period.replace("-0", "-"));
    }
    /**
     * 报表期单元格命中:扫描 sheet 前 8 行,找形如 2026.08 / 2026年8月 / 2026-08-24 的报表期文字或日期单元格。
     * 背景(2026-09-22):潜江单表含 20 个历史 sheet(潜江(1月)…潜江(8月)…),旧逻辑只按「8月」匹配 sheet 名,
     * 会把 2018 年的「潜江(8月)」当成 2026-08,导入成 2018 年数据;当期 sheet 名是「潜江(2022.3)」,
     * 只能靠表内报表期(2026.08.24)识别。
     */
    private boolean investSheetPeriodCellHit(Sheet sh, String period) {
        if (sh == null || period == null || !period.matches("\\d{4}-\\d{2}")) return false;
        String year = period.substring(0, 4);
        int month = Integer.parseInt(period.substring(5, 7));
        int last = Math.min(sh.getLastRowNum(), 7);
        for (int r = 0; r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            int lastCell = Math.max(1, row.getLastCellNum());
            for (int c = 0; c < Math.min(lastCell, 12); c++) {
                Cell cell = row.getCell(c);
                if (cell == null) continue;
                if (cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) {
                    java.time.LocalDate d = cell.getLocalDateTimeCellValue().toLocalDate();
                    if (d.getYear() == Integer.parseInt(year) && d.getMonthValue() == month) return true;
                    continue;
                }
                String v = cellStr(row, c);
                if (v == null) continue;
                String t = v.trim().replace(" ", "");
                if (t.isEmpty() || t.length() > 30) continue;
                java.util.regex.Matcher mm = java.util.regex.Pattern
                        .compile("(\\d{4})\\s*[年.\\-/]\\s*(\\d{1,2})").matcher(t);
                if (mm.find() && year.equals(mm.group(1)) && month == Integer.parseInt(mm.group(2))) return true;
            }
        }
        return false;
    }
    /** 解析单个市州报表:选 sheet(报表期名优先 + 数据行数),按列名映射提取项目行 */
    private void parseInvestCityWorkbook(Workbook wb, String fileName, String category, String period,
                                         Set<String> fiveYearNames, List<InvestCityRow> rows, Map<String, Object> fr) {
@@ -2424,7 +2525,10 @@
            Map<String, Integer> col = findInvestHeader(sh);
            if (col == null) continue;
            int cnt = countInvestRows(sh, col);
            int score = cnt + (cnt > 0 && investSheetMatchesPeriod(sh, period) ? 1000000 : 0);
            // 2026-09-22:表内报表期(单元格)命中权重最高——历史同月 sheet(如潜江(8月))不能盖过当期 sheet
            boolean cellHit = cnt > 0 && investSheetPeriodCellHit(sh, period);
            int score = cnt + (cnt > 0 && investSheetMatchesPeriod(sh, period) ? 1000000 : 0)
                    + (cellHit ? 2000000 : 0);
            if (cnt > 0 && investSheetMatchesPeriod(sh, period)) {
                // 当期真实报表 sheet 名通常带年份(如 2026.8/2026年8月),历史sheet多为纯月份名(8月):带年份的优先
                String norm = sh.getSheetName() == null ? "" : sh.getSheetName().replace(" ", "");
@@ -2489,6 +2593,8 @@
            project.setProjectName(nt);
            project.setCategory(category);
            project.setCity(currentCity);
            String countyCell = cellStr(row, col.get("county"));
            if (countyCell != null && !countyCell.trim().isEmpty()) project.setCounty(countyCell.trim());
            project.setBuilderName(cellStr(row, col.get("builder")));
            project.setConstructNature(cellStr(row, col.get("nature")));
            project.setStartTime(investTime(cellStr(row, col.get("start"))));
@@ -2524,11 +2630,19 @@
            Row row = sh.getRow(r);
            if (row == null) continue;
            boolean hasName = false;
            int headerCells = 0;
            for (int c = 0; c <= row.getLastCellNum(); c++) {
                String v = cellStr(row, c);
                if (v != null && v.contains("项目名称")) { hasName = true; break; }
                if (v == null) continue;
                String t = v.trim();
                if (t.isEmpty() || t.length() > 20) continue;   // 说明性长句不算表头字段
                String k = investHeaderKey(t);
                if (k == null) continue;
                headerCells++;
                if ("name".equals(k)) hasName = true;
            }
            if (!hasName) continue;
            // 真表头行至少有 2 个短表头字段;只有一格长句含「项目名称」的说明页会被排除
            if (!hasName || headerCells < 2) continue;
            Map<Integer, String> colTexts = new HashMap<>();
            int scanEnd = investHeaderScanEnd(sh, r, row);
            for (int rr = r; rr <= scanEnd; rr++) {
@@ -2542,23 +2656,8 @@
            }
            Map<String, Integer> col = new HashMap<>();
            for (Map.Entry<Integer, String> e : colTexts.entrySet()) {
                int c = e.getKey();
                String v = e.getValue();
                if (v.contains("项目名称")) col.putIfAbsent("name", c);
                else if (v.contains("投资建设") || v.contains("建设单位") || v.contains("项目业主")) col.putIfAbsent("builder", c);
                else if (v.contains("建设性质")) col.putIfAbsent("nature", c);
                else if (v.contains("开工")) col.putIfAbsent("start", c);
                else if (v.contains("竣工") || v.contains("建成")) col.putIfAbsent("end", c);
                else if ((v.contains("计划总投资") || v.contains("总投资")) && !v.contains("累计")) col.putIfAbsent("total", c);
                else if (v.contains("自开始建设")) col.putIfAbsent("startcum", c);
                else if (v.contains("本年计划")) col.putIfAbsent("yearplan", c);
                else if (v.contains("自年初累计")) col.putIfAbsent("yearcum", c);
                else if (v.contains("本月完成")) col.putIfAbsent("monthdone", c);
                else if (v.contains("建设阶段")) col.putIfAbsent("stage", c);
                else if (v.contains("形象进度") || v.contains("建设进展")) col.putIfAbsent("desc", c);
                else if (v.contains("工可") && !v.contains("项目名称")) col.putIfAbsent("gk", c);
                else if (v.contains("初设") && !v.contains("项目名称")) col.putIfAbsent("cs", c);
                else if (v.contains("建筑面积") && !v.contains("新增")) col.putIfAbsent("area", c);
                String k = investHeaderKey(e.getValue());
                if (k != null) col.putIfAbsent(k, e.getKey());
            }
            if (!col.containsKey("name")) continue;
            for (int c = 0; c <= row.getLastCellNum(); c++) {
@@ -2582,12 +2681,40 @@
                if (dc <= row.getLastCellNum()) col.put("desc", dc);
            }
            for (String k : new String[]{"builder", "nature", "start", "end", "total", "startcum",
                "yearplan", "yearcum", "monthdone", "stage", "desc", "gk", "cs", "area"}) {
                "yearplan", "yearcum", "monthdone", "stage", "desc", "gk", "cs", "area", "county"}) {
                col.putIfAbsent(k, -1);
            }
            col.put("headerRow", r);
            return col;
        }
        return null;
    }
    /** 单个表头单元格 -> 字段键(null 表示不是表头字段);供表头行判定与列映射共用 */
    private String investHeaderKey(String v) {
        if (v == null) return null;
        // 2026-09-22:表头单元格常带换行/全角空格(如天门「自开始建\n设累计完\n成投资\n(万元)」),
        // 统一去掉全部空白后再判定关键词;否则 H(自开始建设累计)/J(自年初累计)/K(本月完成) 会匹配失败,
        // 列映射落到 -1 → 三个数值被写成 0(天门 2026-08 就是这样丢的 16677.7/16677.7/500)。
        String flat = v.replaceAll("[\\s\\u3000]+", "");
        if (flat.contains("项目名称")) return "name";
        // 「县(市、区)」的各种写法:半/全角括号、顿号/逗号、有无括号,统一归一化后再判定
        String flatCounty = flat.replace("(", "(").replace(")", ")").replace("、", "").replace(",", "").replace(",", "");
        if (flatCounty.contains("县(市区)") || flatCounty.contains("县市区") || flatCounty.contains("区县")) return "county";
        if (flat.contains("投资建设") || flat.contains("建设单位") || flat.contains("项目业主")) return "builder";
        if (flat.contains("建设性质")) return "nature";
        if (flat.contains("开工")) return "start";
        if (flat.contains("竣工") || flat.contains("建成")) return "end";
        if ((flat.contains("计划总投资") || flat.contains("总投资")) && !flat.contains("累计")) return "total";
        if (flat.contains("自开始建设")) return "startcum";
        if (flat.contains("本年计划")) return "yearplan";
        if (flat.contains("自年初累计")) return "yearcum";
        if (flat.contains("本月完成")) return "monthdone";
        if (flat.contains("建设阶段")) return "stage";
        if (flat.contains("形象进度") || flat.contains("建设进展")) return "desc";
        if (flat.contains("工可") && !flat.contains("项目名称")) return "gk";
        if (flat.contains("初设") && !flat.contains("项目名称")) return "cs";
        if (flat.contains("建筑面积") && !flat.contains("新增")) return "area";
        return null;
    }
@@ -2911,32 +3038,42 @@
            this.monthly = monthly;
        }
    }
    /** 投资系统导出导入(审核比对基准),固定8列:行号/单位/时期/计划总投资/自开始累计/本年计划/自年初累计/当月完成 */
    private static final int[] INVEST_SYSTEM_HEADER_COLS = {0, 1, 2, 3, 4, 5, 6, 7};
    private static final String[] INVEST_SYSTEM_HEADER_KEYWORDS =
        {"行号", "单位", "时期", "计划总投资", "自开始建设", "本年计划投资", "自年初", "当月完成投资"};
    /** 投资系统导出导入(审核比对基准)。
     *  列位**按表头关键字定位**,兼容两种模板:
     *   - 老模板《模板_查询结果(投资系统-客运+物流 2026.7).xls》:行号/单位/时期/计划总投资/自开始建设累计/本年计划投资/自年初累计/当月完成投资
     *   - 新模板《2026年X月投资系统项目明细.xls》(用户 2026-09-21 更换):行号/投资项目/时期/项目所处阶段/计划总投资/本年计划投资/自开始建设至本年(月)底-累计完成投资/当月完成投资
     *  新模板**没有「自年初累计」列** → year_cum 留空(审核时按「基准缺失」跳过,不再当成 0 参与比对)。 */
    public int importInvestmentSystem(MultipartFile file, String period) throws Exception {
        List<InvestmentSystem> list = new ArrayList<>();
        int failRows = 0;
        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
            Sheet sheet = wb.getSheetAt(0);
            requireHeaderKeyword(sheet, "投资系统导出", "项目名称", "计划总投资", "单位");
            requireHeaderLayout(sheet, "投资系统导出", "模板_查询结果(投资系统-客运+物流 2026.7).xls",
                INVEST_SYSTEM_HEADER_COLS, INVEST_SYSTEM_HEADER_KEYWORDS);
            requireHeaderKeyword(sheet, "投资系统导出", "投资项目", "项目名称", "单位", "计划总投资");
            Row header = sheet.getRow(0);
            int cName = findColByKeywords(header, "投资项目", "项目名称", "单位");
            int cTotal = findColByKeywords(header, "计划总投资");
            int cYearPlan = findColByKeywords(header, "本年计划投资");
            int cStartCum = findColByKeywords(header, "自开始建设");
            int cYearCum = findColByKeywords(header, "自年初");
            int cMonthDone = findColByKeywords(header, "当月完成投资");
            if (cName < 0 || cTotal < 0 || cYearPlan < 0 || cStartCum < 0 || cMonthDone < 0) {
                throw new RuntimeException("所选数据类型【投资系统导出】表头列不完整:需要「投资项目(或单位)、计划总投资、本年计划投资、"
                    + "自开始建设…累计完成投资、当月完成投资」等列,实际表头为「" + headerText(header)
                    + "」。请使用系统模板《2026年X月投资系统项目明细》或《模板_查询结果(投资系统-客运+物流)》整理后再导入");
            }
            for (int r = 1; r <= sheet.getLastRowNum(); r++) {
                Row row = sheet.getRow(r);
                if (row == null) continue;
                String name = getString(row, 1);
                String name = getString(row, cName);
                if (name == null || name.trim().isEmpty()) continue;
                InvestmentSystem sys = new InvestmentSystem();
                sys.setReportPeriod(period);
                sys.setProjectName(name.trim());
                sys.setTotalInvestment(getDecimal(row, 3));
                sys.setStartCum(getDecimal(row, 4));
                sys.setYearPlan(getDecimal(row, 5));
                sys.setYearCum(getDecimal(row, 6));
                sys.setMonthDone(getDecimal(row, 7));
                sys.setTotalInvestment(getDecimal(row, cTotal));
                sys.setStartCum(getDecimal(row, cStartCum));
                sys.setYearPlan(getDecimal(row, cYearPlan));
                sys.setYearCum(cYearCum < 0 ? null : getDecimal(row, cYearCum));
                sys.setMonthDone(getDecimal(row, cMonthDone));
                list.add(sys);
            }
        } catch (Exception e) {
@@ -2971,59 +3108,59 @@
        List<CityBusRow> rows = new ArrayList<>();
        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
            Sheet sheet = wb.getSheetAt(0);
            requireHeaderLayout(sheet, "城市公交", "模板_交企统U203表_城市公共交通月度运营情况(2026年7月).xls",
                CITY_BUS_HEADER_COLS, CITY_BUS_HEADER_KEYWORDS);
            Row hdr = sheet.getRow(0);
            requireHeaderColsByName(sheet, "城市公交", "模板_交企统U203表_城市公共交通月度运营情况(2026年7月).xls", CITY_BUS_HEADER_KEYWORDS);
            for (int r = 1; r <= sheet.getLastRowNum(); r++) {
                Row row = sheet.getRow(r);
                if (row == null) continue;
                String name = getString(row, 2);
                String name = getString(row, colByName(hdr, 2, "企业名称"));
                if (name == null || name.trim().isEmpty()) continue;
                CityBusMonthly item = new CityBusMonthly();
                item.setReportPeriod(period);
                item.setRegionCode(getString(row, 0));
                item.setCity(RegionUtil.cityByCode(getString(row, 0)));
                item.setEnterpriseCode(getString(row, 1));
                item.setRegionCode(getString(row, colByName(hdr, 0, "所属地区")));
                item.setCity(RegionUtil.cityByCode(getString(row, colByName(hdr, 0, "所属地区"))));
                item.setEnterpriseCode(getString(row, colByName(hdr, 1, "企业代码")));
                item.setEnterpriseName(name.trim());
                item.setOpVehicles(getDecimal(row, 5));
                item.setStopTotal(getDecimal(row, 6));
                item.setStopCity(getDecimal(row, 7));
                item.setPassengerVolume(getDecimal(row, 8));
                item.setPassengerCity(getDecimal(row, 9));
                item.setTurnover(getDecimal(row, 10));
                item.setTurnoverCity(getDecimal(row, 11));
                item.setRailPassengerVolume(getDecimal(row, 13));
                item.setRailTurnover(getDecimal(row, 14));
                item.setFerryPassengerVolume(getDecimal(row, 17));
                item.setFerryTurnover(getDecimal(row, 18));
                item.setAvgDistance(getDecimal(row, 19));
                item.setAvgDistanceCity(getDecimal(row, 20));
                item.setContactPerson(getString(row, 24));
                item.setContactPhone(getString(row, 25));
                item.setReportDate(getDateString(row, 26));
                item.setRateOpVehiclesMom(getDecimal(row, 29));
                item.setRatePassengerMom(getDecimal(row, 30));
                item.setRatePassengerCityMom(getDecimal(row, 31));
                item.setRateChengxiangMom(getDecimal(row, 32));
                item.setRateChengxiangTurnoverMom(getDecimal(row, 33));
                item.setAvgDistanceMomChange(getDecimal(row, 34));
                item.setAvgDistanceCityMomChange(getDecimal(row, 35));
                item.setDailyPassengerPerVehicle(getDecimal(row, 36));
                item.setRateOpVehiclesYoy(getDecimal(row, 39));
                item.setRatePassengerYoy(getDecimal(row, 40));
                item.setRatePassengerCityYoy(getDecimal(row, 41));
                item.setRateChengxiangYoy(getDecimal(row, 42));
                item.setRateChengxiangTurnoverYoy(getDecimal(row, 43));
                item.setStopCityRatio(getDecimal(row, 44));
                item.setPassengerCityRatio(getDecimal(row, 45));
                item.setChengxiangRatio(getDecimal(row, 46));
                item.setRateTurnoverYoy(getDecimal(row, 47));
                item.setRateTurnoverMom(getDecimal(row, 48));
                item.setPassengerChengxiang(getDecimal(row, 49));
                item.setTurnoverChengxiang(getDecimal(row, 50));
                item.setAvgDistanceChengxiang(getDecimal(row, 51));
                item.setVerifyExplanation(getString(row, 65));
                item.setReportNote(getString(row, 66));
                item.setModifyRecord(getString(row, 67));
                item.setOpVehicles(getDecimal(row, colByName(hdr, 5, "公共汽电车_运营车数")));
                item.setStopTotal(getDecimal(row, colByName(hdr, 6, "运营站点总个数")));
                item.setStopCity(getDecimal(row, colByName(hdr, 7, "其中:城市内")));
                item.setPassengerVolume(getDecimal(row, colByName(hdr, 8, "公共汽电车_客运量")));
                item.setPassengerCity(getDecimal(row, colByName(hdr, 9, "其中:城市内", 2)));
                item.setTurnover(getDecimal(row, colByName(hdr, 10, "新增公共汽电车旅客周转量")));
                item.setTurnoverCity(getDecimal(row, colByName(hdr, 11, "新增公共汽电车旅客周转量_其中城市内")));
                item.setRailPassengerVolume(getDecimal(row, colByName(hdr, 13, "轨道交通_客运量")));
                item.setRailTurnover(getDecimal(row, colByName(hdr, 14, "轨道交通_旅客周转量")));
                item.setFerryPassengerVolume(getDecimal(row, colByName(hdr, 17, "城市客运轮渡_客运量")));
                item.setFerryTurnover(getDecimal(row, colByName(hdr, 18, "城市客运轮渡旅客周转量")));
                item.setAvgDistance(getDecimal(row, colByName(hdr, 19, "公共汽电车平均运距")));
                item.setAvgDistanceCity(getDecimal(row, colByName(hdr, 20, "公共汽电车城市内平均运距")));
                item.setContactPerson(getString(row, colByName(hdr, 24, "填表人")));
                item.setContactPhone(getString(row, colByName(hdr, 25, "联系电话")));
                item.setReportDate(getDateString(row, colByName(hdr, 26, "报出日期")));
                item.setRateOpVehiclesMom(getDecimal(row, colByName(hdr, 29, "公共汽电车_运营车数环比增速")));
                item.setRatePassengerMom(getDecimal(row, colByName(hdr, 30, "公共汽电车_客运量环比增速")));
                item.setRatePassengerCityMom(getDecimal(row, colByName(hdr, 31, "城市内客运量环比")));
                item.setRateChengxiangMom(getDecimal(row, colByName(hdr, 32, "城际城乡客运量环比增速")));
                item.setRateChengxiangTurnoverMom(getDecimal(row, colByName(hdr, 33, "城际城乡旅客周转量环比增速")));
                item.setAvgDistanceMomChange(getDecimal(row, colByName(hdr, 34, "平均运距环比增减(公里)")));
                item.setAvgDistanceCityMomChange(getDecimal(row, colByName(hdr, 35, "平均运距_城市内环比增减(公里)")));
                item.setDailyPassengerPerVehicle(getDecimal(row, colByName(hdr, 36, "公共汽电车_单车日均客运量")));
                item.setRateOpVehiclesYoy(getDecimal(row, colByName(hdr, 39, "公共汽电车_运营车数同比增速")));
                item.setRatePassengerYoy(getDecimal(row, colByName(hdr, 40, "公共汽电车_客运量同比增速")));
                item.setRatePassengerCityYoy(getDecimal(row, colByName(hdr, 41, "客运量_城市内同比增速")));
                item.setRateChengxiangYoy(getDecimal(row, colByName(hdr, 42, "城际城乡客运量同比增速")));
                item.setRateChengxiangTurnoverYoy(getDecimal(row, colByName(hdr, 43, "城际城乡旅客周转量同比增速")));
                item.setStopCityRatio(getDecimal(row, colByName(hdr, 44, "城市内运营站点占比")));
                item.setPassengerCityRatio(getDecimal(row, colByName(hdr, 45, "城市内客运量占比")));
                item.setChengxiangRatio(getDecimal(row, colByName(hdr, 46, "城际城乡客运量占比")));
                item.setRateTurnoverYoy(getDecimal(row, colByName(hdr, 47, "旅客周转量同比增速")));
                item.setRateTurnoverMom(getDecimal(row, colByName(hdr, 48, "旅客周转量环比增速")));
                item.setPassengerChengxiang(getDecimal(row, colByName(hdr, 49, "城际城乡客运量")));
                item.setTurnoverChengxiang(getDecimal(row, colByName(hdr, 50, "城际城乡旅客周转量")));
                item.setAvgDistanceChengxiang(getDecimal(row, colByName(hdr, 51, "城际城乡平均运距")));
                item.setVerifyExplanation(getString(row, colByName(hdr, 65, "核实性原因解释")));
                item.setReportNote(getString(row, colByName(hdr, 66, "上报说明")));
                item.setModifyRecord(getString(row, colByName(hdr, 67, "修改记录")));
                rows.add(new CityBusRow(r + 1, item));
            }
        } catch (Exception e) {
@@ -3086,52 +3223,52 @@
        List<TaxiRow> rows = new ArrayList<>();
        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
            Sheet sheet = wb.getSheetAt(0);
            requireHeaderLayout(sheet, "巡游出租", "模板_交行统U402表-巡游出租汽车运营服务情况月报(2026).xls",
                CITY_TAXI_HEADER_COLS, CITY_TAXI_HEADER_KEYWORDS);
            Row hdr = sheet.getRow(0);
            requireHeaderColsByName(sheet, "巡游出租", "模板_交行统U402表-巡游出租汽车运营服务情况月报(2026).xls", CITY_TAXI_HEADER_KEYWORDS);
            for (int r = 1; r <= sheet.getLastRowNum(); r++) {
                Row row = sheet.getRow(r);
                if (row == null) continue;
                String name = getString(row, 2);
                String name = getString(row, colByName(hdr, 2, "企业名称"));
                if (name == null || name.trim().isEmpty()) continue;
                CityTaxiMonthly item = new CityTaxiMonthly();
                item.setReportPeriod(period);
                item.setRegionCode(getString(row, 0));
                item.setCity(cityOf(getString(row, 0), name));
                item.setTripTotal(getDecimal(row, 4));
                item.setTripCity(getDecimal(row, 5));
                item.setPassengerVolume(getDecimal(row, 6));
                item.setPassengerCity(getDecimal(row, 7));
                item.setTurnover(getDecimal(row, 8));
                item.setTurnoverCity(getDecimal(row, 9));
                item.setOpVehicles(getDecimal(row, 10));
                item.setAvgDistance(getDecimal(row, 11));
                item.setAvgDistanceCity(getDecimal(row, 12));
                item.setContactPerson(getString(row, 15));
                item.setContactPhone(getString(row, 16));
                item.setReportDate(getDateString(row, 17));
                item.setDailyTripsPerVehicle(getDecimal(row, 18));
                item.setDailyPassengerPerVehicle(getDecimal(row, 19));
                item.setPassengersPerTrip(getDecimal(row, 20));
                item.setRateOpVehiclesMom(getDecimal(row, 21));
                item.setRateOpVehiclesYoy(getDecimal(row, 22));
                item.setRateTripMom(getDecimal(row, 23));
                item.setRateTripYoy(getDecimal(row, 24));
                item.setRatePassengerMom(getDecimal(row, 25));
                item.setRatePassengerYoy(getDecimal(row, 26));
                item.setAvgDistanceMomChange(getDecimal(row, 27));
                item.setPassengersPerTripCity(getDecimal(row, 28));
                item.setRateTripCityMom(getDecimal(row, 29));
                item.setRatePassengerCityMom(getDecimal(row, 30));
                item.setAvgDistanceCityMomChange(getDecimal(row, 31));
                item.setTripCityRatio(getDecimal(row, 32));
                item.setPassengerCityRatio(getDecimal(row, 33));
                item.setPassengersPerTripChengxiang(getDecimal(row, 34));
                item.setRateTripChengxiangMom(getDecimal(row, 35));
                item.setRatePassengerChengxiangMom(getDecimal(row, 36));
                item.setAvgDistanceChengxiang(getDecimal(row, 37));
                item.setVerifyExplanation(getString(row, 38));
                item.setReportNote(getString(row, 39));
                item.setModifyRecord(getString(row, 40));
                item.setRegionCode(getString(row, colByName(hdr, 0, "所属地区")));
                item.setCity(cityOf(getString(row, colByName(hdr, 0, "所属地区")), name));
                item.setTripTotal(getDecimal(row, colByName(hdr, 4, "载客车次总数")));
                item.setTripCity(getDecimal(row, colByName(hdr, 5, "载客车次总数_城市内")));
                item.setPassengerVolume(getDecimal(row, colByName(hdr, 6, "客运量")));
                item.setPassengerCity(getDecimal(row, colByName(hdr, 7, "客运量_其中城市内")));
                item.setTurnover(getDecimal(row, colByName(hdr, 8, "旅客周转量")));
                item.setTurnoverCity(getDecimal(row, colByName(hdr, 9, "旅客周转量_城市内")));
                item.setOpVehicles(getDecimal(row, colByName(hdr, 10, "运营车辆数")));
                item.setAvgDistance(getDecimal(row, colByName(hdr, 11, "巡游出租汽车平均运距")));
                item.setAvgDistanceCity(getDecimal(row, colByName(hdr, 12, "城市内平均运距")));
                item.setContactPerson(getString(row, colByName(hdr, 15, "填表人")));
                item.setContactPhone(getString(row, colByName(hdr, 16, "联系电话")));
                item.setReportDate(getDateString(row, colByName(hdr, 17, "报出日期")));
                item.setDailyTripsPerVehicle(getDecimal(row, colByName(hdr, 18, "单车日均载客车次")));
                item.setDailyPassengerPerVehicle(getDecimal(row, colByName(hdr, 19, "单车日均客运量")));
                item.setPassengersPerTrip(getDecimal(row, colByName(hdr, 20, "单车次载客人数")));
                item.setRateOpVehiclesMom(getDecimal(row, colByName(hdr, 21, "运营车数环比增速")));
                item.setRateOpVehiclesYoy(getDecimal(row, colByName(hdr, 22, "运营车数同比增速")));
                item.setRateTripMom(getDecimal(row, colByName(hdr, 23, "载客车次总数环比增速")));
                item.setRateTripYoy(getDecimal(row, colByName(hdr, 24, "载客车次同比增速")));
                item.setRatePassengerMom(getDecimal(row, colByName(hdr, 25, "客运量环比增速")));
                item.setRatePassengerYoy(getDecimal(row, colByName(hdr, 26, "客运量同比增速")));
                item.setAvgDistanceMomChange(getDecimal(row, colByName(hdr, 27, "平均运距环比增减")));
                item.setPassengersPerTripCity(getDecimal(row, colByName(hdr, 28, "单车次载客人数_城市内")));
                item.setRateTripCityMom(getDecimal(row, colByName(hdr, 29, "载客车次总数环比增速_城市内")));
                item.setRatePassengerCityMom(getDecimal(row, colByName(hdr, 30, "客运量环比增速_城市内")));
                item.setAvgDistanceCityMomChange(getDecimal(row, colByName(hdr, 31, "平均运距_城市内环比增减")));
                item.setTripCityRatio(getDecimal(row, colByName(hdr, 32, "载客车次总数_城市内占比")));
                item.setPassengerCityRatio(getDecimal(row, colByName(hdr, 33, "客运量_城市内占比")));
                item.setPassengersPerTripChengxiang(getDecimal(row, colByName(hdr, 34, "单车次载客人数_城际城乡")));
                item.setRateTripChengxiangMom(getDecimal(row, colByName(hdr, 35, "载客车次总数_城际城乡环比增速")));
                item.setRatePassengerChengxiangMom(getDecimal(row, colByName(hdr, 36, "客运量_城际城乡环比增速")));
                item.setAvgDistanceChengxiang(getDecimal(row, colByName(hdr, 37, "平均运距_城际城乡")));
                item.setVerifyExplanation(getString(row, colByName(hdr, 38, "核实性原因解释")));
                item.setReportNote(getString(row, colByName(hdr, 39, "上报说明")));
                item.setModifyRecord(getString(row, colByName(hdr, 40, "修改记录")));
                rows.add(new TaxiRow(r + 1, item));
            }
        } catch (Exception e) {