|
@@ -0,0 +1,564 @@
|
|
|
|
|
+package com.jeeplus.common.utils.excel.utils;
|
|
|
|
|
+
|
|
|
|
|
+import com.jeeplus.common.config.Global;
|
|
|
|
|
+import com.jeeplus.common.oss.OSSClientUtil;
|
|
|
|
|
+import org.apache.poi.ss.usermodel.*;
|
|
|
|
|
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.FileInputStream;
|
|
|
|
|
+import java.io.FileOutputStream;
|
|
|
|
|
+import java.io.InputStream;
|
|
|
|
|
+import java.nio.file.Files;
|
|
|
|
|
+import java.nio.file.Path;
|
|
|
|
|
+import java.nio.file.Paths;
|
|
|
|
|
+import java.text.Normalizer;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 知识库文件匹配工具
|
|
|
|
|
+ *
|
|
|
|
|
+ * 功能:读取 Excel 中的标准名称和文号,与指定目录下的文件进行匹配
|
|
|
|
|
+ * 匹配策略:文号归一化 + 名称模糊匹配,与 Python 版本逻辑一致
|
|
|
|
|
+ *
|
|
|
|
|
+ * 依赖:Apache POI (poi + poi-ooxml)
|
|
|
|
|
+ *
|
|
|
|
|
+ * Maven 依赖:
|
|
|
|
|
+ * <dependency>
|
|
|
|
|
+ * <groupId>org.apache.poi</groupId>
|
|
|
|
|
+ * <artifactId>poi</artifactId>
|
|
|
|
|
+ * <version>5.2.5</version>
|
|
|
|
|
+ * </dependency>
|
|
|
|
|
+ * <dependency>
|
|
|
|
|
+ * <groupId>org.apache.poi</groupId>
|
|
|
|
|
+ * <artifactId>poi-ooxml</artifactId>
|
|
|
|
|
+ * <version>5.2.5</version>
|
|
|
|
|
+ * </dependency>
|
|
|
|
|
+ */
|
|
|
|
|
+public class FileMatcher {
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 数据模型 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /** Excel 中的一条记录 */
|
|
|
|
|
+ public static class ExcelEntry {
|
|
|
|
|
+ public int row; // Excel 行号
|
|
|
|
|
+ public String node; // 所属节点名称
|
|
|
|
|
+ public String name; // 标准名称
|
|
|
|
|
+ public String docNum; // 文号
|
|
|
|
|
+
|
|
|
|
|
+ public ExcelEntry(int row, String node, String name, String docNum) {
|
|
|
|
|
+ this.row = row;
|
|
|
|
|
+ this.node = node;
|
|
|
|
|
+ this.name = name;
|
|
|
|
|
+ this.docNum = docNum;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 目录中的一个文件 */
|
|
|
|
|
+ public static class FileInfo {
|
|
|
|
|
+ public String filename; // 文件名(含扩展名)
|
|
|
|
|
+ public String subdir; // 子目录名
|
|
|
|
|
+ public String fullPath; // 完整路径
|
|
|
|
|
+
|
|
|
|
|
+ public FileInfo(String filename, String subdir, String fullPath) {
|
|
|
|
|
+ this.filename = filename;
|
|
|
|
|
+ this.subdir = subdir;
|
|
|
|
|
+ this.fullPath = fullPath;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 匹配结果 */
|
|
|
|
|
+ public static class MatchResult {
|
|
|
|
|
+ public ExcelEntry entry;
|
|
|
|
|
+ public FileInfo file;
|
|
|
|
|
+ public double score;
|
|
|
|
|
+
|
|
|
|
|
+ public MatchResult(ExcelEntry entry, FileInfo file, double score) {
|
|
|
|
|
+ this.entry = entry;
|
|
|
|
|
+ this.file = file;
|
|
|
|
|
+ this.score = score;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 核心匹配逻辑 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 归一化处理:统一括号、全角半角等
|
|
|
|
|
+ * 对应 Python 中的 normalize() 函数
|
|
|
|
|
+ */
|
|
|
|
|
+ public static String normalize(String s) {
|
|
|
|
|
+ if (s == null) return "";
|
|
|
|
|
+ // NFKC 归一化(全角转半角)
|
|
|
|
|
+ s = Normalizer.normalize(s, Normalizer.Form.NFKC);
|
|
|
|
|
+ // 统一中文方括号 〔〕 → []
|
|
|
|
|
+ s = s.replace('\u3014', '[').replace('\u3015', ']');
|
|
|
|
|
+ // 统一全角方括号 [] → []
|
|
|
|
|
+ s = s.replace('\uFF3B', '[').replace('\uFF3D', ']');
|
|
|
|
|
+ // 统一中文圆括号 () → ()
|
|
|
|
|
+ s = s.replace('\uFF08', '(').replace('\uFF09', ')');
|
|
|
|
|
+ // 全角空格 → 半角空格
|
|
|
|
|
+ s = s.replace('\u3000', ' ');
|
|
|
|
|
+ // 合并多余空格
|
|
|
|
|
+ s = s.replaceAll("\\s+", " ").trim();
|
|
|
|
|
+ return s;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 计算两个名称的相似度 (0.0 ~ 1.0)
|
|
|
|
|
+ * 对应 Python 中的 name_similarity() 函数
|
|
|
|
|
+ */
|
|
|
|
|
+ public static double nameSimilarity(String n1, String n2) {
|
|
|
|
|
+ // 只保留中文和字母数字
|
|
|
|
|
+ String clean1 = n1.replaceAll("[^\u4e00-\u9fff\\w]", "");
|
|
|
|
|
+ String clean2 = n2.replaceAll("[^\u4e00-\u9fff\\w]", "");
|
|
|
|
|
+
|
|
|
|
|
+ if (clean1.equals(clean2)) return 1.0;
|
|
|
|
|
+ if (clean1.contains(clean2) || clean2.contains(clean1)) return 0.9;
|
|
|
|
|
+
|
|
|
|
|
+ // 计算公共字符比例
|
|
|
|
|
+ int common = 0;
|
|
|
|
|
+ for (char c : clean1.toCharArray()) {
|
|
|
|
|
+ if (clean2.indexOf(c) >= 0) common++;
|
|
|
|
|
+ }
|
|
|
|
|
+ int total = Math.max(clean1.length(), clean2.length());
|
|
|
|
|
+ return total > 0 ? (double) common / total : 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从文件名中提取名称部分(去掉文号)
|
|
|
|
|
+ * 对应 Python 中的 extract_name_from_filename() 函数
|
|
|
|
|
+ */
|
|
|
|
|
+ public static String extractNameFromFilename(String filename) {
|
|
|
|
|
+ // 去掉扩展名
|
|
|
|
|
+ String name = filename.replaceFirst("\\.[^.]+$", "");
|
|
|
|
|
+ // 去掉开头的文号模式:xxx[20xx]xxx号 或 xxx(20xx)xxx号
|
|
|
|
|
+ name = name.replaceFirst("^[^\\s]+\\s*[\\[\\(][^\\s]+[\\]\\)][^\\s]*号\\s*", "");
|
|
|
|
|
+ // 去掉中间残留的括号内容
|
|
|
|
|
+ name = name.replaceAll("[\\[\\(][^\\s]*[\\]\\)]", "");
|
|
|
|
|
+ return name.trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 对单个 Excel 条目和单个文件计算匹配分数
|
|
|
|
|
+ */
|
|
|
|
|
+ private static double calcScore(ExcelEntry entry, FileInfo file) {
|
|
|
|
|
+ String normNum = normalize(entry.docNum);
|
|
|
|
|
+ String normName = normalize(entry.name);
|
|
|
|
|
+ String fnNoExt = file.filename.replaceFirst("\\.[^.]+$", "");
|
|
|
|
|
+ String normFn = normalize(fnNoExt);
|
|
|
|
|
+ String fnName = normalize(extractNameFromFilename(file.filename));
|
|
|
|
|
+
|
|
|
|
|
+ double score = 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (!normNum.isEmpty()) {
|
|
|
|
|
+ if (normFn.contains(normNum)) {
|
|
|
|
|
+ // 文号匹配 +50
|
|
|
|
|
+ score += 50;
|
|
|
|
|
+ // 名称相似度 *50
|
|
|
|
|
+ score += nameSimilarity(normName, fnName) * 50;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 文号不完全匹配,看名称
|
|
|
|
|
+ double sim = nameSimilarity(normName, fnName);
|
|
|
|
|
+ if (sim > 0.7) {
|
|
|
|
|
+ score = sim * 60;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 没有文号,只按名称匹配
|
|
|
|
|
+ score = nameSimilarity(normName, fnName) * 80;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return score;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 主匹配方法 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /** 匹配分数阈值,低于此分数视为未匹配 */
|
|
|
|
|
+ private static final double SCORE_THRESHOLD = 60.0;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 执行匹配
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param entries Excel 中读取的条目列表
|
|
|
|
|
+ * @param files 目录中扫描到的文件列表
|
|
|
|
|
+ * @return 匹配结果 Map:key=ExcelEntry, value=MatchResult(null 表示未匹配)
|
|
|
|
|
+ */
|
|
|
|
|
+ public static Map<ExcelEntry, MatchResult> match(List<ExcelEntry> entries, List<FileInfo> files) {
|
|
|
|
|
+ Map<ExcelEntry, MatchResult> results = new LinkedHashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ for (ExcelEntry entry : entries) {
|
|
|
|
|
+ FileInfo bestFile = null;
|
|
|
|
|
+ double bestScore = 0;
|
|
|
|
|
+
|
|
|
|
|
+ for (FileInfo file : files) {
|
|
|
|
|
+ double score = calcScore(entry, file);
|
|
|
|
|
+ if (score > bestScore) {
|
|
|
|
|
+ bestScore = score;
|
|
|
|
|
+ bestFile = file;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (bestFile != null && bestScore >= SCORE_THRESHOLD) {
|
|
|
|
|
+ results.put(entry, new MatchResult(entry, bestFile, bestScore));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ results.put(entry, null);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return results;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== Excel 读取 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从 Excel 文件读取条目
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param excelPath Excel 文件路径
|
|
|
|
|
+ * @param nameCol 标准名称所在列(从0开始)
|
|
|
|
|
+ * @param docNumCol 文号所在列(从0开始)
|
|
|
|
|
+ * @param nodeCol 所属节点所在列(从0开始),-1 表示没有此列
|
|
|
|
|
+ * @param startRow 数据起始行(从0开始,跳过表头)
|
|
|
|
|
+ */
|
|
|
|
|
+ public static List<ExcelEntry> readExcel(String excelPath, int nameCol, int docNumCol, int nodeCol, int startRow)
|
|
|
|
|
+ throws Exception {
|
|
|
|
|
+ List<ExcelEntry> entries = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ try (FileInputStream fis = new FileInputStream(excelPath);
|
|
|
|
|
+ Workbook wb = WorkbookFactory.create(fis)) {
|
|
|
|
|
+
|
|
|
|
|
+ Sheet sheet = wb.getSheetAt(0);
|
|
|
|
|
+ for (int i = startRow; i <= sheet.getLastRowNum(); i++) {
|
|
|
|
|
+ Row row = sheet.getRow(i);
|
|
|
|
|
+ if (row == null) continue;
|
|
|
|
|
+
|
|
|
|
|
+ String name = getCellString(row, nameCol);
|
|
|
|
|
+ if (name.isEmpty()) continue;
|
|
|
|
|
+
|
|
|
|
|
+ String docNum = getCellString(row, docNumCol);
|
|
|
|
|
+ String node = nodeCol >= 0 ? getCellString(row, nodeCol) : "";
|
|
|
|
|
+
|
|
|
|
|
+ entries.add(new ExcelEntry(i + 1, node, name, docNum));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return entries;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String getCellString(Row row, int col) {
|
|
|
|
|
+ Cell cell = row.getCell(col);
|
|
|
|
|
+ if (cell == null) return "";
|
|
|
|
|
+ cell.setCellType(CellType.STRING); // 强制按字符串读取
|
|
|
|
|
+ return cell.getStringCellValue().trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 目录扫描 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 递归扫描目录中的所有文件
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param dirPath 根目录路径
|
|
|
|
|
+ */
|
|
|
|
|
+ public static List<FileInfo> scanDirectory(String dirPath) throws Exception {
|
|
|
|
|
+ List<FileInfo> files = new ArrayList<>();
|
|
|
|
|
+ Path root = Paths.get(dirPath);
|
|
|
|
|
+
|
|
|
|
|
+ Files.walk(root)
|
|
|
|
|
+ .filter(Files::isRegularFile)
|
|
|
|
|
+ .forEach(path -> {
|
|
|
|
|
+ String relPath = root.relativize(path).toString();
|
|
|
|
|
+ String[] parts = relPath.replace("\\", "/").split("/");
|
|
|
|
|
+ String subdir = parts.length > 1 ? parts[0] : "";
|
|
|
|
|
+ files.add(new FileInfo(path.getFileName().toString(), subdir, path.toString()));
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return files;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 对照表 Excel 输出 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将匹配结果输出为对照表 Excel
|
|
|
|
|
+ * 列:序号 | 标准名称 | 文号 | 匹配文件路径 | 匹配百分比 | 匹配状态
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param results 匹配结果
|
|
|
|
|
+ * @param outputPath 输出 Excel 文件路径
|
|
|
|
|
+ */
|
|
|
|
|
+ public static void writeComparisonExcel(Map<ExcelEntry, MatchResult> results, String outputPath) throws Exception {
|
|
|
|
|
+ try (Workbook wb = new XSSFWorkbook()) {
|
|
|
|
|
+ Sheet sheet = wb.createSheet("文件匹配对照表");
|
|
|
|
|
+
|
|
|
|
|
+ // 创建表头样式
|
|
|
|
|
+ CellStyle headerStyle = wb.createCellStyle();
|
|
|
|
|
+ Font headerFont = wb.createFont();
|
|
|
|
|
+ headerFont.setBold(true);
|
|
|
|
|
+ headerFont.setFontHeightInPoints((short) 11);
|
|
|
|
|
+ headerStyle.setFont(headerFont);
|
|
|
|
|
+ headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
|
|
|
|
|
+ headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
|
|
|
|
+ headerStyle.setBorderBottom(BorderStyle.THIN);
|
|
|
|
|
+ headerStyle.setBorderTop(BorderStyle.THIN);
|
|
|
|
|
+ headerStyle.setBorderLeft(BorderStyle.THIN);
|
|
|
|
|
+ headerStyle.setBorderRight(BorderStyle.THIN);
|
|
|
|
|
+ headerStyle.setAlignment(HorizontalAlignment.CENTER);
|
|
|
|
|
+
|
|
|
|
|
+ // 创建百分比样式
|
|
|
|
|
+ CellStyle percentStyle = wb.createCellStyle();
|
|
|
|
|
+ percentStyle.setDataFormat(wb.createDataFormat().getFormat("0.00\"%\""));
|
|
|
|
|
+
|
|
|
|
|
+ // 创建匹配/未匹配状态样式
|
|
|
|
|
+ CellStyle matchedStyle = wb.createCellStyle();
|
|
|
|
|
+ Font greenFont = wb.createFont();
|
|
|
|
|
+ greenFont.setColor(IndexedColors.GREEN.getIndex());
|
|
|
|
|
+ greenFont.setBold(true);
|
|
|
|
|
+ matchedStyle.setFont(greenFont);
|
|
|
|
|
+ matchedStyle.setAlignment(HorizontalAlignment.CENTER);
|
|
|
|
|
+
|
|
|
|
|
+ CellStyle unmatchedStyle = wb.createCellStyle();
|
|
|
|
|
+ Font redFont = wb.createFont();
|
|
|
|
|
+ redFont.setColor(IndexedColors.RED.getIndex());
|
|
|
|
|
+ redFont.setBold(true);
|
|
|
|
|
+ unmatchedStyle.setFont(redFont);
|
|
|
|
|
+ unmatchedStyle.setAlignment(HorizontalAlignment.CENTER);
|
|
|
|
|
+
|
|
|
|
|
+ // 表头
|
|
|
|
|
+ String[] headers = {"序号", "标准名称", "文号", "匹配文件路径", "匹配百分比", "匹配状态"};
|
|
|
|
|
+ Row headerRow = sheet.createRow(0);
|
|
|
|
|
+ for (int i = 0; i < headers.length; i++) {
|
|
|
|
|
+ Cell cell = headerRow.createCell(i);
|
|
|
|
|
+ cell.setCellValue(headers[i]);
|
|
|
|
|
+ cell.setCellStyle(headerStyle);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 设置列宽
|
|
|
|
|
+ sheet.setColumnWidth(0, 8 * 256); // 序号
|
|
|
|
|
+ sheet.setColumnWidth(1, 50 * 256); // 标准名称
|
|
|
|
|
+ sheet.setColumnWidth(2, 35 * 256); // 文号
|
|
|
|
|
+ sheet.setColumnWidth(3, 70 * 256); // 匹配文件路径
|
|
|
|
|
+ sheet.setColumnWidth(4, 15 * 256); // 匹配百分比
|
|
|
|
|
+ sheet.setColumnWidth(5, 12 * 256); // 匹配状态
|
|
|
|
|
+
|
|
|
|
|
+ // 数据行
|
|
|
|
|
+ int rowNum = 1;
|
|
|
|
|
+ for (Map.Entry<ExcelEntry, MatchResult> e : results.entrySet()) {
|
|
|
|
|
+ ExcelEntry entry = e.getKey();
|
|
|
|
|
+ MatchResult result = e.getValue();
|
|
|
|
|
+
|
|
|
|
|
+ Row row = sheet.createRow(rowNum++);
|
|
|
|
|
+
|
|
|
|
|
+ // 序号
|
|
|
|
|
+ row.createCell(0).setCellValue(entry.row - 1); // 显示原始序号(去掉表头偏移)
|
|
|
|
|
+
|
|
|
|
|
+ // 标准名称
|
|
|
|
|
+ row.createCell(1).setCellValue(entry.name);
|
|
|
|
|
+
|
|
|
|
|
+ // 文号
|
|
|
|
|
+ row.createCell(2).setCellValue(entry.docNum != null ? entry.docNum : "");
|
|
|
|
|
+
|
|
|
|
|
+ if (result != null) {
|
|
|
|
|
+ // 匹配文件路径
|
|
|
|
|
+ row.createCell(3).setCellValue(result.file.fullPath);
|
|
|
|
|
+
|
|
|
|
|
+ // 匹配百分比(score 满分100)
|
|
|
|
|
+ Cell percentCell = row.createCell(4);
|
|
|
|
|
+ percentCell.setCellValue(result.score);
|
|
|
|
|
+ percentCell.setCellStyle(percentStyle);
|
|
|
|
|
+
|
|
|
|
|
+ // 匹配状态
|
|
|
|
|
+ Cell statusCell = row.createCell(5);
|
|
|
|
|
+ statusCell.setCellValue("匹配成功");
|
|
|
|
|
+ statusCell.setCellStyle(matchedStyle);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ row.createCell(3).setCellValue("");
|
|
|
|
|
+ Cell percentCell = row.createCell(4);
|
|
|
|
|
+ percentCell.setCellValue(0);
|
|
|
|
|
+ percentCell.setCellStyle(percentStyle);
|
|
|
|
|
+
|
|
|
|
|
+ Cell statusCell = row.createCell(5);
|
|
|
|
|
+ statusCell.setCellValue("未匹配");
|
|
|
|
|
+ statusCell.setCellStyle(unmatchedStyle);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 写入文件
|
|
|
|
|
+ try (FileOutputStream fos = new FileOutputStream(outputPath)) {
|
|
|
|
|
+ wb.write(fos);
|
|
|
|
|
+ }
|
|
|
|
|
+ System.out.println("对照表已输出到: " + outputPath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== OSS 上传 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将匹配成功的文件上传到 OSS
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param results 匹配结果
|
|
|
|
|
+ * @param ossDir OSS 目标目录(如 "app-data/knowledgeBase/")
|
|
|
|
|
+ * @return 上传结果 Map:key=文件完整路径, value=OSS URL
|
|
|
|
|
+ */
|
|
|
|
|
+ public static Map<String, String> uploadMatchedFilesToOSS(Map<ExcelEntry, MatchResult> results, String ossDir) {
|
|
|
|
|
+ Map<String, String> uploadResults = new LinkedHashMap<>();
|
|
|
|
|
+ OSSClientUtil ossUtil = new OSSClientUtil();
|
|
|
|
|
+ String baseUrl = Global.getAliDownloadUrl();
|
|
|
|
|
+
|
|
|
|
|
+ for (Map.Entry<ExcelEntry, MatchResult> e : results.entrySet()) {
|
|
|
|
|
+ MatchResult result = e.getValue();
|
|
|
|
|
+ if (result == null) continue;
|
|
|
|
|
+
|
|
|
|
|
+ String localPath = result.file.fullPath;
|
|
|
|
|
+ String fileName = result.file.filename;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 使用 OSSClientUtil 上传本地文件
|
|
|
|
|
+ ossUtil.uploadFileSignatureOSS(localPath, ossDir, fileName);
|
|
|
|
|
+ String ossUrl = baseUrl + "/" + ossDir + fileName;
|
|
|
|
|
+ uploadResults.put(localPath, ossUrl);
|
|
|
|
|
+ System.out.printf("[OSS上传成功] %s -> %s%n", fileName, ossUrl);
|
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
|
+ System.err.printf("[OSS上传失败] %s : %s%n", fileName, ex.getMessage());
|
|
|
|
|
+ uploadResults.put(localPath, "上传失败: " + ex.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return uploadResults;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== Web 接口适配 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从 InputStream 读取 Excel 条目(适配 Web 上传场景)
|
|
|
|
|
+ * 自动识别表头列名:标准名称、文号、所属节点名称
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param inputStream Excel 文件输入流
|
|
|
|
|
+ * @return Excel 条目列表
|
|
|
|
|
+ */
|
|
|
|
|
+ public static List<ExcelEntry> readExcelFromStream(InputStream inputStream) throws Exception {
|
|
|
|
|
+ List<ExcelEntry> entries = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ try (Workbook wb = WorkbookFactory.create(inputStream)) {
|
|
|
|
|
+ Sheet sheet = wb.getSheetAt(0);
|
|
|
|
|
+
|
|
|
|
|
+ // 第一行是表头,自动识别列索引
|
|
|
|
|
+ Row headerRow = sheet.getRow(0);
|
|
|
|
|
+ if (headerRow == null) {
|
|
|
|
|
+ throw new Exception("Excel 表头为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ int nameCol = -1, docNumCol = -1, nodeCol = -1;
|
|
|
|
|
+ for (int i = 0; i <= headerRow.getLastCellNum(); i++) {
|
|
|
|
|
+ Cell cell = headerRow.getCell(i);
|
|
|
|
|
+ if (cell == null) continue;
|
|
|
|
|
+ cell.setCellType(CellType.STRING);
|
|
|
|
|
+ String header = cell.getStringCellValue().trim();
|
|
|
|
|
+ if ("标准名称".equals(header)) {
|
|
|
|
|
+ nameCol = i;
|
|
|
|
|
+ } else if ("文号".equals(header)) {
|
|
|
|
|
+ docNumCol = i;
|
|
|
|
|
+ } else if ("所属节点名称".equals(header)) {
|
|
|
|
|
+ nodeCol = i;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (nameCol == -1) {
|
|
|
|
|
+ throw new Exception("表头中未找到【标准名称】列,请检查Excel格式");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 从第二行开始读取数据(跳过表头)
|
|
|
|
|
+ for (int i = 1; i <= sheet.getLastRowNum(); i++) {
|
|
|
|
|
+ Row row = sheet.getRow(i);
|
|
|
|
|
+ if (row == null) continue;
|
|
|
|
|
+
|
|
|
|
|
+ String name = getCellString(row, nameCol);
|
|
|
|
|
+ if (name.isEmpty()) continue;
|
|
|
|
|
+
|
|
|
|
|
+ String docNum = docNumCol >= 0 ? getCellString(row, docNumCol) : "";
|
|
|
|
|
+ String node = nodeCol >= 0 ? getCellString(row, nodeCol) : "";
|
|
|
|
|
+
|
|
|
|
|
+ entries.add(new ExcelEntry(i + 1, node, name, docNum));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return entries;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将匹配结果转换为 JSON 友好的 List<Map> 格式
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param results 匹配结果
|
|
|
|
|
+ * @return JSON 友好的结果列表
|
|
|
|
|
+ */
|
|
|
|
|
+ public static List<Map<String, Object>> resultsToJsonList(Map<ExcelEntry, MatchResult> results) {
|
|
|
|
|
+ List<Map<String, Object>> list = new ArrayList<>();
|
|
|
|
|
+ for (Map.Entry<ExcelEntry, MatchResult> e : results.entrySet()) {
|
|
|
|
|
+ ExcelEntry entry = e.getKey();
|
|
|
|
|
+ MatchResult result = e.getValue();
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
|
|
+ item.put("name", entry.name);
|
|
|
|
|
+ item.put("docNum", entry.docNum != null ? entry.docNum : "");
|
|
|
|
|
+
|
|
|
|
|
+ if (result != null) {
|
|
|
|
|
+ item.put("matched", true);
|
|
|
|
|
+ item.put("matchedFilePath", result.file.fullPath);
|
|
|
|
|
+ item.put("score", result.score);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ item.put("matched", false);
|
|
|
|
|
+ item.put("matchedFilePath", "");
|
|
|
|
|
+ item.put("score", 0.0);
|
|
|
|
|
+ }
|
|
|
|
|
+ list.add(item);
|
|
|
|
|
+ }
|
|
|
|
|
+ return list;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 使用示例 ====================
|
|
|
|
|
+
|
|
|
|
|
+ public static void main(String[] args) throws Exception {
|
|
|
|
|
+ // ===== 配置参数(根据实际路径修改)=====
|
|
|
|
|
+ String excelPath = "D:\\attachment-file\\知识库导入模板.xlsx"; // Excel 文件路径
|
|
|
|
|
+ String dirPath = "D:\\a_project_file"; // 本地文件目录路径
|
|
|
|
|
+ String outputPath = "D:\\attachment-file\\文件匹配对照表.xlsx"; // 对照表输出路径
|
|
|
|
|
+ String ossDir = "app-data/knowledgeBase/"; // OSS 目标目录
|
|
|
|
|
+
|
|
|
|
|
+ // Excel 列配置(3列模板:A=序号, B=标准名称, C=文号)
|
|
|
|
|
+ int nameCol = 1; // B列: 标准名称
|
|
|
|
|
+ int docNumCol = 2; // C列: 文号
|
|
|
|
|
+ int nodeCol = -1; // 无所属节点列
|
|
|
|
|
+ int startRow = 1; // 第2行开始(跳过表头行,0-indexed)
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 执行 =====
|
|
|
|
|
+ System.out.println("========== 知识库文件匹配工具 ==========\n");
|
|
|
|
|
+
|
|
|
|
|
+ System.out.println("[1/5] 正在读取 Excel...");
|
|
|
|
|
+ List<ExcelEntry> entries = readExcel(excelPath, nameCol, docNumCol, nodeCol, startRow);
|
|
|
|
|
+ System.out.println(" Excel 条目数: " + entries.size());
|
|
|
|
|
+
|
|
|
|
|
+ System.out.println("[2/5] 正在扫描目录: " + dirPath);
|
|
|
|
|
+ List<FileInfo> files = scanDirectory(dirPath);
|
|
|
|
|
+ System.out.println(" 目录文件数: " + files.size());
|
|
|
|
|
+
|
|
|
|
|
+ System.out.println("[3/5] 正在匹配...");
|
|
|
|
|
+ Map<ExcelEntry, MatchResult> results = match(entries, files);
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 输出对照表 =====
|
|
|
|
|
+ System.out.println("[4/5] 正在生成对照表...");
|
|
|
|
|
+ writeComparisonExcel(results, outputPath);
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 上传 OSS =====
|
|
|
|
|
+ System.out.println("[5/5] 正在上传匹配文件到 OSS...");
|
|
|
|
|
+ Map<String, String> uploadResults = uploadMatchedFilesToOSS(results, ossDir);
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 统计 =====
|
|
|
|
|
+ int matched = 0, unmatched = 0;
|
|
|
|
|
+ for (MatchResult r : results.values()) {
|
|
|
|
|
+ if (r != null) matched++; else unmatched++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ System.out.println("\n========== 匹配统计 ==========");
|
|
|
|
|
+ System.out.println("匹配成功: " + matched);
|
|
|
|
|
+ System.out.println("未匹配: " + unmatched);
|
|
|
|
|
+ System.out.println("总计: " + (matched + unmatched));
|
|
|
|
|
+ System.out.println("OSS上传成功: " + uploadResults.size());
|
|
|
|
|
+ System.out.println("================================");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|