| | |
| | | } |
| | | } |
| | | |
| | | /** 按表头关键字定位列号:从第 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<>(); |
| | |
| | | || 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) { |
| | |
| | | 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(" ", ""); |
| | |
| | | /** 单个表头单元格 -> 字段键(null 表示不是表头字段);供表头行判定与列映射共用 */ |
| | | private String investHeaderKey(String v) { |
| | | if (v == null) return null; |
| | | String flat = v.replace(" ", ""); |
| | | if (v.contains("项目名称")) return "name"; |
| | | // 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 (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"; |
| | | 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; |
| | | } |
| | | |
| | |
| | | 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) { |