zhizhijie
2026-09-21 791a12762b4b3ab8b781e85d99c4316089f2a3f0
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
@@ -2465,10 +2465,10 @@
            }
            if (name == null || name.trim().isEmpty()) continue;
            String nt = name.trim().replaceAll("[\r\n]+", "");
            if (nt.contains("合计") || nt.contains("总计")) continue;
            String ntFlat = nt.replaceAll("\\s+", "");
            if (ntFlat.contains("合计") || ntFlat.contains("总计") || ntFlat.contains("小计")) continue;
            if (nt.matches("^[一二三四五六七八九十]+、.*")) continue;
            if (nt.matches("^([一二三四五六七八九十]+).*")) continue;
            if (nt.replace(" ", "").matches(".*小\\s*计.*")) continue;
            if (nt.startsWith("填报") || nt.startsWith("单位负责人") || nt.matches("^\\d+、.*")) continue;
            if (hasSeq) {
                if (seq == null || seq.trim().isEmpty()) {
@@ -2489,6 +2489,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,13 +2526,22 @@
            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<>();
            for (int rr = r; rr <= Math.min(r + 3, sh.getLastRowNum()); rr++) {
            int scanEnd = investHeaderScanEnd(sh, r, row);
            for (int rr = r; rr <= scanEnd; rr++) {
                Row rr2 = sh.getRow(rr);
                if (rr2 == null) continue;
                for (int c = 0; c <= rr2.getLastCellNum(); c++) {
@@ -2541,23 +2552,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("形象进度")) 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++) {
@@ -2574,14 +2570,85 @@
                int md = col.get("yearcum") + 1;
                if (md <= row.getLastCellNum()) col.put("monthdone", md);
            }
            // desc 缺失时按 stage+1 兜底:省标「项目建设阶段、进度情况」是 L/M 合并表头,
            // 孝感/恩施/林区/随州等市州只在 L 写「建设阶段」、M 直接写进度文字而不写标题
            if (!col.containsKey("desc") && col.containsKey("stage") && col.get("stage") >= 0) {
                int dc = col.get("stage") + 1;
                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;
        String flat = v.replace(" ", "");
        if (v.contains("项目名称")) return "name";
        // 「县(市、区)」的各种写法:半/全角括号、顿号/逗号、有无括号,统一归一化后再判定
        String flatCounty = flat.replace("(", "(").replace(")", ")").replace("、", "").replace(",", "").replace(",", "");
        if (flatCounty.contains("县(市区)") || flatCounty.contains("县市区") || flatCounty.contains("区县")) return "county";
        if (v.contains("投资建设") || v.contains("建设单位") || v.contains("项目业主")) return "builder";
        if (v.contains("建设性质")) return "nature";
        if (v.contains("开工")) return "start";
        if (v.contains("竣工") || v.contains("建成")) return "end";
        if ((v.contains("计划总投资") || v.contains("总投资")) && !v.contains("累计")) return "total";
        if (v.contains("自开始建设")) return "startcum";
        if (v.contains("本年计划")) return "yearplan";
        if (v.contains("自年初累计")) return "yearcum";
        if (v.contains("本月完成")) return "monthdone";
        if (v.contains("建设阶段")) return "stage";
        if (v.contains("形象进度") || v.contains("建设进展")) return "desc";
        if (v.contains("工可") && !v.contains("项目名称")) return "gk";
        if (v.contains("初设") && !v.contains("项目名称")) return "cs";
        if (v.contains("建筑面积") && !v.contains("新增")) return "area";
        return null;
    }
    /** 表头文本扫描下界:默认表头行 +3;若「项目名称」列首个数据行更靠下,则扩展到数据行前一行(上限 +12)。
     *  背景:咸宁/十堰等市州把「建设阶段/形象进度」标签写在表头下第 4~8 行,固定 +3 会漏列(形象进度读不到)。 */
    private int investHeaderScanEnd(Sheet sh, int headerRow, Row headerRowObj) {
        int hardEnd = Math.min(headerRow + 12, sh.getLastRowNum());
        int nameCol = -1;
        if (headerRowObj != null) {
            for (int c = 0; c <= headerRowObj.getLastCellNum(); c++) {
                String v = cellStr(headerRowObj, c);
                if (v != null && v.contains("项目名称")) { nameCol = c; break; }
            }
        }
        int firstData = findFirstInvestDataRow(sh, headerRow, nameCol);
        int scanEnd = (firstData > headerRow)
            ? Math.min(hardEnd, firstData - 1)
            : Math.min(headerRow + 3, sh.getLastRowNum());
        if (scanEnd < headerRow) scanEnd = headerRow;
        return scanEnd;
    }
    /** 「项目名称」列首个数据行下标(跳过表头续行与空行);找不到返回 -1 */
    private int findFirstInvestDataRow(Sheet sh, int headerRow, int nameCol) {
        if (nameCol < 0) return -1;
        for (int r = headerRow + 1; r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String name = cellStr(row, nameCol);
            if (name == null || name.trim().isEmpty()) continue;
            String nt = name.trim().replaceAll("[\r\n]+", "");
            if (nt.contains("项目名称")) continue;
            String flat = nt.replaceAll("\\s+", "");
            if (flat.contains("合计") || flat.contains("总计") || flat.contains("小计")) continue;
            if (nt.matches("^[一二三四五六七八九十]+、.*")) continue;
            if (nt.matches("^([一二三四五六七八九十]+).*")) continue;
            if (nt.startsWith("填报") || nt.startsWith("单位负责人") || nt.matches("^\\d+、.*")) continue;
            if (investmentCity(nt) != null) continue;
            return r;
        }
        return -1;
    }
    /** 统计 sheet 内项目数据行数(用于多 sheet 历史文件选当月 sheet) */
@@ -2596,9 +2663,9 @@
            String name = cellStr(row, nameCol);
            if (name == null || name.trim().isEmpty()) continue;
            String nt = name.trim().replaceAll("[\r\n]+", "");
            if (nt.contains("合计") || nt.contains("总计")) continue;
            String ntFlat = nt.replaceAll("\\s+", "");
            if (ntFlat.contains("合计") || ntFlat.contains("总计") || ntFlat.contains("小计")) continue;
            if (nt.matches("^[一二三四五六七八九十]+、.*")) continue;
            if (nt.replace(" ", "").matches(".*小\\s*计.*")) continue;
            if (nt.startsWith("填报") || nt.startsWith("单位负责人") || nt.matches("^\\d+、.*")) continue;
            if (!hasSeq) {
                if (nt.length() > 4) count++;