| | |
| | | } |
| | | } |
| | | |
| | | /** 按表头关键字定位列号:从第 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<>(); |
| | |
| | | } |
| | | 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()) { |
| | |
| | | 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")))); |
| | |
| | | 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++) { |
| | |
| | | } |
| | | 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++) { |
| | |
| | | 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) */ |
| | |
| | | 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++; |
| | |
| | | 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) { |