| | |
| | | String curCity = null; |
| | | Set<Long> used = new HashSet<>(); |
| | | int filled = 0, kept = 0, dup = 0; |
| | | List<String> skipped = new ArrayList<>(); |
| | | Map<String, Integer> skippedByCity = new LinkedHashMap<>(); |
| | | for (int r = 8; r <= sheet.getLastRowNum(); r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) continue; |
| | |
| | | if (!a.matches("\\d+(\\.\\d+)?")) continue; |
| | | if (b.isEmpty() || curCity == null) continue; |
| | | InvestmentProject p = matchInvest(projects, b, curCity, "客运站场", used); |
| | | p = preferMonthlyCandidate(projects, p, mByProject, "客运站场", used); |
| | | InvestmentMonthly m = p == null ? null : mByProject.get(p.getId()); |
| | | if (p == null || m == null) { |
| | | kept++; |
| | | // 有库内项目但当月无数据 → 记为待核对;无匹配项目 → 结转上月 |
| | | if (p != null) dup++; |
| | | // 2026-09-22:不再静默结转上月,未覆盖的行必须留痕(行号+市州+项目名+原因) |
| | | skipped.add("第" + (r + 1) + "行 " + curCity + " " + b |
| | | + (p == null ? " → 库内无匹配项目" : " → 库内无 " + period + " 月度数据")); |
| | | skippedByCity.merge(curCity, 1, Integer::sum); |
| | | continue; |
| | | } |
| | | used.add(p.getId()); |
| | |
| | | } |
| | | log.info("客运站投资明细表 {}:写入 {} 行,结转母版原值 {} 行(其中库内有项目但当月无数据 {} 行)", |
| | | period, filled, kept, dup); |
| | | logSkippedRows("客运站投资明细表", period, skipped, skippedByCity); |
| | | return toBytes(wb); |
| | | } |
| | | } |
| | |
| | | Map<String, double[]> cityStats = new LinkedHashMap<>(); // 全名 -> {总数, 累计, 规划内数, 规划内累计, 规划外数, 规划外累计} |
| | | Set<Long> used = new HashSet<>(); |
| | | int filled = 0, kept = 0; |
| | | List<String> skipped = new ArrayList<>(); |
| | | Map<String, Integer> skippedByCity = new LinkedHashMap<>(); |
| | | for (int i = 0; i < cityRows.size(); i++) { |
| | | int cr = cityRows.get(i); |
| | | String cityLabel = nvl(cellText(sheet.getRow(cr).getCell(0))); |
| | |
| | | if (!a.matches("\\d+(\\.\\d+)?")) continue; |
| | | if (b.isEmpty()) continue; |
| | | InvestmentProject p = matchInvest(projects, b, city, "物流园区", used); |
| | | p = preferMonthlyCandidate(projects, p, mByProject, "物流园区", used); |
| | | InvestmentMonthly m = p == null ? null : mByProject.get(p.getId()); |
| | | if (p == null || m == null) { kept++; continue; } |
| | | if (p == null || m == null) { |
| | | kept++; |
| | | // 2026-09-22:不再静默结转上月,未覆盖的行必须留痕 |
| | | skipped.add("第" + (r + 1) + "行 " + city + " " + b |
| | | + (p == null ? " → 库内无匹配项目" : " → 库内无 " + period + " 月度数据")); |
| | | skippedByCity.merge(city, 1, Integer::sum); |
| | | continue; |
| | | } |
| | | used.add(p.getId()); |
| | | // 口径同客运站明细表:只覆盖 H 自开始建设累计 / J 自年初累计 / K 本月完成 / M 形象进度 |
| | | if (m.getStartCum() != null) putNum(row, 7, m.getStartCum()); |
| | |
| | | log.warn("物流站场投资明细公式求值失败: {}", e.getMessage()); |
| | | } |
| | | log.info("物流站场投资明细 {}:写入 {} 行,结转母版原值 {} 行", period, filled, kept); |
| | | logSkippedRows("物流站场投资明细", period, skipped, skippedByCity); |
| | | return toBytesHssf(wb); |
| | | } |
| | | |
| | |
| | | return full != null && RegionUtil.cityList().contains(full); |
| | | } |
| | | |
| | | /** |
| | | * 同名(归一化后一致)多档时,优先选「当月有数据」的那一档。 |
| | | * 背景(2026-09-22):十堰「快递物流产业园二期」在库内有两档 id=154(竣工 2026,月度只到 7 月)与 |
| | | * id=181(竣工 2029,有 8 月),模板行名前缀与 154 一致 → 导出取到 154 的 7 月值。 |
| | | */ |
| | | private InvestmentProject preferMonthlyCandidate(List<InvestmentProject> pool, InvestmentProject p, |
| | | Map<Long, InvestmentMonthly> mByProject, |
| | | String category, Set<Long> excludeIds) { |
| | | if (p == null || mByProject.containsKey(p.getId())) return p; |
| | | String norm = normInvestName(p.getProjectName()); |
| | | if (norm.isEmpty()) return p; |
| | | InvestmentProject better = null; |
| | | for (InvestmentProject c : pool) { |
| | | if (c == null || c.getId().equals(p.getId())) continue; |
| | | if (excludeIds != null && excludeIds.contains(c.getId())) continue; |
| | | if (category != null && !category.equals(c.getCategory())) continue; |
| | | if (!mByProject.containsKey(c.getId())) continue; |
| | | if (!norm.equals(normInvestName(c.getProjectName()))) continue; |
| | | if (better == null || c.getId() > better.getId()) better = c; |
| | | } |
| | | return better != null ? better : p; |
| | | } |
| | | |
| | | /** 未按当月数据覆盖的行必须留痕:逐行列出行号+市州+项目名,避免「静默结转上期值」再次发生 */ |
| | | private void logSkippedRows(String reportName, String period, List<String> skipped, Map<String, Integer> byCity) { |
| | | if (skipped == null || skipped.isEmpty()) return; |
| | | log.warn("{} {}:{} 行未按当月数据覆盖(结转母版原值),需人工核对源文件/库内数据:", |
| | | reportName, period, skipped.size()); |
| | | int limit = Math.min(skipped.size(), 50); |
| | | for (int i = 0; i < limit; i++) log.warn(" [未覆盖] {}", skipped.get(i)); |
| | | if (skipped.size() > limit) log.warn(" …另有 {} 行未逐行列出", skipped.size() - limit); |
| | | if (byCity != null && !byCity.isEmpty()) log.warn(" 未覆盖行按市州统计:{}", byCity); |
| | | } |
| | | |
| | | /** 在候选项目池中按名称匹配 DB 项目(可选 市州/类别 过滤 + 县区前缀消歧),未命中返回 null */ |
| | | private InvestmentProject matchInvest(List<InvestmentProject> pool, String name, String cityFull, String category) { |
| | | return matchInvest(pool, name, cityFull, category, null); |
| | |
| | | if (name == null) return ""; |
| | | String s = name.trim(); |
| | | s = s.replaceAll("[((]", "").replaceAll("[))]", ""); |
| | | s = s.replaceAll("[·•—-_\\-]", ""); |
| | | // 2026-09-22:补充半/全角句点、顿号、分号等(库内「中国供销.公安商贸物流园」因半角句点匹配不上模板行) |
| | | s = s.replaceAll("[·•—-_\\-..。、,,;;::]", ""); |
| | | s = s.replaceAll("[ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹ]", "I"); |
| | | s = s.replaceAll("^(湖北省|武汉市|黄石市|十堰市|宜昌市|襄阳市|鄂州市|荆门市|孝感市|荆州市|黄冈市|咸宁市|随州市|恩施州|仙桃市|潜江市|天门市|神农架林区)", ""); |
| | | s = s.replaceAll("^(?:[\\u4e00-\\u9fa5]{2,4}(?:县|市|区))", ""); |