diff --git a/build.gradle b/build.gradle index cc2a9ee..bd97633 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,5 @@ +import org.panteleyev.jpackage.ImageType + buildscript { repositories { google() @@ -12,8 +14,7 @@ plugins { id 'org.openjfx.javafxplugin' version '0.1.0' // 不再使用模块插件,移除 org.javamodularity.moduleplugin // id 'org.javamodularity.moduleplugin' version '1.8.12' - // 你可以根据需要保留 jlink 插件,但推荐取消模块化后暂时不用它 - id 'org.beryx.jlink' version '2.26.0' apply false + id("org.panteleyev.jpackageplugin") version "1.6.1" } group = project_group @@ -62,7 +63,8 @@ dependencies { implementation "org.openjfx:javafx-controls:$javafxVersion:$javafxPlatform" implementation "org.openjfx:javafx-fxml:$javafxVersion:$javafxPlatform" - // 你项目的其他依赖(老库走classpath) + implementation 'ch.qos.logback:logback-classic:1.5.18' + implementation 'ch.qos.logback:logback-core:1.5.18' implementation 'ch.qos.logback:logback-classic:1.5.6' implementation 'commons-cli:commons-cli:1.9.0' implementation 'com.alibaba:easyexcel:4.0.3' @@ -145,7 +147,35 @@ tasks.register('buildFatJar', Jar) { } with jar // 继承主 jar 的内容 - archiveBaseName.set('doc-check-tool-cli') - archiveVersion.set('1.0') + archiveBaseName.set(project_name) + archiveVersion.set(project_version) duplicatesStrategy = DuplicatesStrategy.EXCLUDE } + +// =================== 轻便版打包 =================== + +tasks.register('buildPortable', Exec) { + group = 'distribution' + description = 'Build portable EXE (no installer)' + + dependsOn buildFatJar + + commandLine 'jpackage', + '--name', 'DocCheckTool', + '--input', "$buildDir/libs", + '--main-jar', "${project_name}-${project_version}.jar", + '--main-class', application.mainClass.get(), + '--type', 'app-image', // ⚠️ 轻便版不生成安装器 + '--app-version', project_version, + '--vendor', 'r3944realms', + '--dest', "$buildDir/distributions", + '--java-options', '-Dfile.encoding=UTF-8', + '--java-options', '-Xmx512m', + '--java-options', '-Xms256m', + '--verbose', + '--icon', file('src/main/resources/img/logo256x.ico').absolutePath + + doFirst { + mkdir "$buildDir/distributions" + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index f49fe27..de312de 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ project_group =top.r3944realms.docchecktoolrefacored -project_version = 1.0-SNAPSHOT \ No newline at end of file +project_name = doc-check-tool +project_version = 1.0 \ No newline at end of file diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/System.java b/src/main/java/top/r3944realms/docchecktoolrefactored/System.java index 1100ee7..e351ce1 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/System.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/System.java @@ -4,7 +4,7 @@ import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import lombok.extern.slf4j.Slf4j; import top.r3944realms.docchecktoolrefactored.core.Setting; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.*; import java.nio.charset.StandardCharsets; @@ -38,10 +38,10 @@ public enum System { if (Files.exists(configPath)) { try (InputStream input = new FileInputStream(configPath.toFile())) { properties.load(input); - log.debug(LoggerHelper.DEBUG_MARKER, "配置文件加载成功: {}", configPath); + log.debug(LoggerMarker.DEBUG_MARKER, "配置文件加载成功: {}", configPath); INSTANCE.setting = propertiesToSetting(properties); } catch (IOException e) { - log.error(LoggerHelper.DEBUG_MARKER, "读取配置文件失败: {}, 使用默认配置", e.getMessage()); + log.error(LoggerMarker.DEBUG_MARKER, "读取配置文件失败: {}, 使用默认配置", e.getMessage()); INSTANCE.setting = defaultSetting(); settingToProperties(INSTANCE.setting, properties); } @@ -49,7 +49,7 @@ public enum System { INSTANCE.setting = defaultSetting(); settingToProperties(INSTANCE.setting, properties); saveSettings(); // 首次启动保存默认配置 - log.debug(LoggerHelper.DEBUG_MARKER, "配置文件不存在,已创建默认配置: {}", configPath); + log.debug(LoggerMarker.DEBUG_MARKER, "配置文件不存在,已创建默认配置: {}", configPath); } } finally { lock.writeLock().unlock(); @@ -72,13 +72,13 @@ public enum System { try (OutputStream output = new FileOutputStream(configPath.toFile())) { properties.store(new OutputStreamWriter(output, StandardCharsets.UTF_8), "DocCheckTool Configuration"); - log.debug(LoggerHelper.DEBUG_MARKER, "配置文件保存成功: {}", configPath); + log.debug(LoggerMarker.DEBUG_MARKER, "配置文件保存成功: {}", configPath); } catch (IOException e) { - log.error(LoggerHelper.DEBUG_MARKER, "保存配置文件失败: {}", e.getMessage()); + log.error(LoggerMarker.DEBUG_MARKER, "保存配置文件失败: {}", e.getMessage()); } } } catch (IOException e) { - log.error(LoggerHelper.DEBUG_MARKER, "创建配置目录失败: {}", e.getMessage()); + log.error(LoggerMarker.DEBUG_MARKER, "创建配置目录失败: {}", e.getMessage()); } finally { lock.readLock().unlock(); } @@ -86,14 +86,28 @@ public enum System { /** 获取配置文件路径 */ private static Path getConfigPath() { - String userHome = java.lang.System.getProperty("user.home"); - return Paths.get(userHome, ".docchecktool", CONFIG_FILE_NAME); + try { + // 获取程序运行目录(Jar 所在目录) + File jarFile = new File(System.class.getProtectionDomain() + .getCodeSource().getLocation().toURI()); + File appDir = jarFile.getParentFile(); + Path configDir = appDir.toPath().resolve("config"); + if (!Files.exists(configDir)) { + Files.createDirectories(configDir); + } + return configDir.resolve(CONFIG_FILE_NAME); + } catch (Exception e) { + // 出错 fallback 到当前工作目录 + Path fallbackDir = Paths.get(java.lang.System.getProperty("user.dir"), "config"); + try { Files.createDirectories(fallbackDir); } catch (IOException ignored) {} + return fallbackDir.resolve(CONFIG_FILE_NAME); + } } /** 将Setting对象转换为Properties */ private static void settingToProperties(Setting setting, Properties props) { - props.setProperty("singleTimeout", String.valueOf(setting.getSingleTimeout())); - props.setProperty("totalTimeout", String.valueOf(setting.getTotalTimeout())); + props.setProperty("singleTimeout", String.valueOf(setting.getScanTimeout())); + props.setProperty("totalTimeout", String.valueOf(setting.getTaskTimeout())); props.setProperty("enableStep", String.valueOf(setting.isEnableStep())); } @@ -102,23 +116,23 @@ public enum System { Setting s = new Setting(); try { - s.setSingleTimeout(Long.parseLong(props.getProperty("singleTimeout", String.valueOf(DEFAULT_SINGLE_TIMEOUT)))); + s.setScanTimeout(Long.parseLong(props.getProperty("scanTimeOutS", String.valueOf(DEFAULT_SINGLE_TIMEOUT)))); } catch (NumberFormatException e) { - s.setSingleTimeout(DEFAULT_SINGLE_TIMEOUT); - log.error(LoggerHelper.DEBUG_MARKER, "singleTimeout格式错误,使用默认值{}", DEFAULT_SINGLE_TIMEOUT); + s.setScanTimeout(DEFAULT_SINGLE_TIMEOUT); + log.error(LoggerMarker.DEBUG_MARKER, "scanTimeOutS格式错误,使用默认值{}", DEFAULT_SINGLE_TIMEOUT); } try { - s.setTotalTimeout(Long.parseLong(props.getProperty("totalTimeout", String.valueOf(DEFAULT_TOTAL_TIMEOUT)))); + s.setTaskTimeout(Long.parseLong(props.getProperty("taskTimeOutS", String.valueOf(DEFAULT_TOTAL_TIMEOUT)))); } catch (NumberFormatException e) { - s.setTotalTimeout(DEFAULT_TOTAL_TIMEOUT); - log.error(LoggerHelper.DEBUG_MARKER, "totalTimeout格式错误,使用默认值{}", DEFAULT_TOTAL_TIMEOUT); + s.setTaskTimeout(DEFAULT_TOTAL_TIMEOUT); + log.error(LoggerMarker.DEBUG_MARKER, "taskTimeOutS格式错误,使用默认值{}", DEFAULT_TOTAL_TIMEOUT); } try { s.setEnableStep(Boolean.parseBoolean(props.getProperty("enableStep", String.valueOf(Boolean.FALSE)))); } catch (Exception e) { s.setEnableStep(DEFAULT_ENABLE_STEP); - log.error(LoggerHelper.DEBUG_MARKER, "enableStep格式错误,使用默认值{}", DEFAULT_TOTAL_TIMEOUT); + log.error(LoggerMarker.DEBUG_MARKER, "enableStep格式错误,使用默认值{}", DEFAULT_TOTAL_TIMEOUT); } return s; @@ -127,8 +141,8 @@ public enum System { /** 获取默认Setting */ private static Setting defaultSetting() { Setting s = new Setting(); - s.setSingleTimeout(DEFAULT_SINGLE_TIMEOUT); - s.setTotalTimeout(DEFAULT_TOTAL_TIMEOUT); + s.setScanTimeout(DEFAULT_SINGLE_TIMEOUT); + s.setTaskTimeout(DEFAULT_TOTAL_TIMEOUT); return s; } @@ -185,4 +199,11 @@ public enum System { } return directoryChooser; } + + /** + * @return CPU核心数 + */ + public static Integer getAvailableProcessors() { + return Runtime.getRuntime().availableProcessors(); + } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/cil/CliProcessor.java b/src/main/java/top/r3944realms/docchecktoolrefactored/cil/CliProcessor.java index e1a8c04..9e671c6 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/cil/CliProcessor.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/cil/CliProcessor.java @@ -8,7 +8,7 @@ import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner; import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner; import top.r3944realms.docchecktoolrefactored.model.DuplicateGroup; import top.r3944realms.docchecktoolrefactored.util.FileUtil; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.BufferedWriter; import java.io.IOException; @@ -103,7 +103,7 @@ public class CliProcessor { printHelp(options); System.exit(1); } catch (Exception e) { - log.error(LoggerHelper.DEBUG_MARKER, "Error processing CLI command", e); + log.error(LoggerMarker.DEBUG_MARKER, "Error processing CLI command", e); System.err.println("Error: " + e.getMessage()); System.exit(1); } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileComparator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileComparator.java index 15d2cfa..e45f22a 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileComparator.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileComparator.java @@ -1,14 +1,24 @@ package top.r3944realms.docchecktoolrefactored.core; import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import org.jetbrains.annotations.NotNull; +import top.r3944realms.docchecktoolrefactored.System; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; @Slf4j public class AddressFileComparator { @@ -17,7 +27,20 @@ public class AddressFileComparator { PAGE_LEVEL, // 页面级比较 FILE_LEVEL // 文件级比较 } + // 进度回调接口 + public interface ProgressCallback { + default void onPhaseStarted(Phase phase) {} + default void onPhaseProgress(Phase phase, int current, int total) {} + default void onPhaseCompleted(Phase phase) {} + } + // 比较阶段 + public enum Phase { + READ_PHYSICAL_CSV, + READ_LOGICAL_CSV, + COMPARE_FORWARD, + COMPARE_BACKWARD + } @Getter public static class ComparisonResult { private final int physicalRecordsCount; // 物理文件记录数 @@ -40,224 +63,270 @@ public class AddressFileComparator { this.pathMismatchResults = pathMismatchResults; this.pageCountMismatchResults = pageCountMismatchResults; } + public static @NotNull String generateResult(AddressFileComparator.CompareMode compareMode, AddressFileComparator.ComparisonResult result) { + StringBuilder resultText = new StringBuilder(); + resultText.append("读取物理地址文件记录数: ").append(result.getPhysicalRecordsCount()).append("\n"); + resultText.append("读取逻辑地址文件记录数: ").append(result.getLogicalRecordsCount()).append("\n\n"); - // 为向后兼容保留原来的构造函数 - public ComparisonResult(int physicalRecordsCount, - int logicalRecordsCount, - List forwardResults, - List backwardResults, - List pathMismatchResults) { - this(physicalRecordsCount, logicalRecordsCount, forwardResults, - backwardResults, pathMismatchResults, new ArrayList<>()); + if (!result.getPathMismatchResults().isEmpty()) { + resultText.append("文件名相同但路径不一致的记录数量: ") + .append(result.getPathMismatchResults().size()).append("\n"); + result.getPathMismatchResults().forEach(s -> resultText.append("\t").append(s).append("\n")); + } else resultText.append("没有路径错误\n\n"); + + if (compareMode == AddressFileComparator.CompareMode.FILE_LEVEL && + !result.getPageCountMismatchResults().isEmpty()) { + resultText.append("文件名和路径相同但页数不一致的记录数量: ") + .append(result.getPageCountMismatchResults().size()).append("\n"); + result.getPageCountMismatchResults().forEach(s -> resultText.append("\t").append(s).append("\n")); + } + + if (!result.getForwardComparisonResults().isEmpty()) { + resultText.append("物理文件在逻辑文件中未找到的记录数量: ") + .append(result.getForwardComparisonResults().size()).append("\n"); + result.getForwardComparisonResults().forEach(s -> resultText.append("\t").append(s).append("\n")); + } else resultText.append("没有物理存在而逻辑不存在的文件\n\n"); + + if (!result.getBackwardComparisonResults().isEmpty()) { + resultText.append("逻辑文件在物理文件中未找到的记录数量: ") + .append(result.getBackwardComparisonResults().size()).append("\n"); + result.getBackwardComparisonResults().forEach(s -> resultText.append("\t").append(s).append("\n")); + } else resultText.append("没有逻辑存在而物理不存在的文件\n"); + + if (result.getPathMismatchResults().isEmpty() && + result.getForwardComparisonResults().isEmpty() && + result.getBackwardComparisonResults().isEmpty()) { + resultText.append("\n所有文件比对一致,无差异。\n"); + } + + return resultText.toString(); + } + public static String generateComparisonResults(ComparisonResult result, CompareMode compareMode) { + return generateComparisonResults( + result.physicalRecordsCount, + result.logicalRecordsCount, + result.forwardComparisonResults, + result.backwardComparisonResults, + result.pathMismatchResults, + result.pageCountMismatchResults, + compareMode + ); + } + + private static String generateComparisonResults(int physicalCount, int logicalCount, + List forwardResults, List backwardResults, + List pathMismatchResults, List pageCountMismatchResults, + CompareMode compareMode) { + StringBuilder sb = new StringBuilder(); + + sb.append("=== 文件比较结果 ===\n"); + sb.append("物理地址文件记录数: ").append(physicalCount).append("\n"); + sb.append("逻辑地址文件记录数: ").append(logicalCount).append("\n"); + + if (pathMismatchResults.isEmpty()) { + sb.append("没有路径错误\n"); + } else { + sb.append("文件名相同但路径不一致的记录数量: ").append(pathMismatchResults.size()).append("\n"); + pathMismatchResults.forEach(result -> sb.append("\t").append(result).append("\n")); + } + + if (compareMode == CompareMode.FILE_LEVEL) { + if (pageCountMismatchResults.isEmpty()) { + sb.append("没有页数错误\n"); + } else { + sb.append("文件名和路径相同但页数不一致的记录数量: ") + .append(pageCountMismatchResults.size()).append("\n"); + pageCountMismatchResults.forEach(result -> sb.append("\t").append(result).append("\n")); + } + } + + if (forwardResults.isEmpty()) { + sb.append("没有物理存在而逻辑不存在的文件\n"); + } else { + sb.append("物理文件在逻辑文件中未找到的记录数量: ").append(forwardResults.size()).append("\n"); + forwardResults.forEach(result -> sb.append("\t").append(result).append("\n")); + } + + if (backwardResults.isEmpty()) { + sb.append("没有逻辑存在而物理不存在的文件\n"); + } else { + sb.append("逻辑文件在物理文件中未找到的记录数量: ").append(backwardResults.size()).append("\n"); + backwardResults.forEach(result -> sb.append("\t").append(result).append("\n")); + } + + sb.append("=== 比较完成 ==="); + + return sb.toString(); } } - - public ComparisonResult compareFiles(String physicalAddressFilePath, String logicalAddressFilePath) { - return compareFiles(physicalAddressFilePath, logicalAddressFilePath, CompareMode.PAGE_LEVEL); + private final ExecutorService executor; + public AddressFileComparator(int threadPoolSize) { + this.executor = Executors.newFixedThreadPool(threadPoolSize); } - public ComparisonResult compareFiles(String physicalAddressFilePath, String logicalAddressFilePath, CompareMode compareMode) { - List physicalRecords = readCSV(physicalAddressFilePath); - List logicalRecords = readCSV(logicalAddressFilePath); - - // 记录读取的行数(不包括标题行) - int physicalCount = physicalRecords.size(); - int logicalCount = logicalRecords.size(); - - log.info(LoggerHelper.DEBUG_MARKER, "读取物理地址文件记录数: {}, 读取逻辑地址文件记录数: {}", physicalCount, logicalCount); - - List forwardComparisonResults = new ArrayList<>(); // 物理文件在逻辑文件中未找到 - List backwardComparisonResults = new ArrayList<>(); // 逻辑文件在物理文件中未找到 - List pathMismatchResults = new ArrayList<>(); // 文件名相同但路径不一致 - List pageCountMismatchResults = new ArrayList<>(); // 文件名和路径相同但页数不一致(仅文件级) - - // 正向比较:遍历物理文件,检查是否在逻辑文件中存在 - for (String[] physicalRecord : physicalRecords) { - // 确保数据行有足够列数 - if (compareMode == CompareMode.PAGE_LEVEL && physicalRecord.length < 2) continue; - if (compareMode == CompareMode.FILE_LEVEL && physicalRecord.length < 3) continue; - - String physicalFileName = physicalRecord[0]; - String physicalAddress = physicalRecord[1]; - String physicalPageCount = compareMode == CompareMode.FILE_LEVEL ? physicalRecord[2] : null; - - boolean found = false; - for (String[] logicalRecord : logicalRecords) { - // 确保数据行有足够列数 - if (compareMode == CompareMode.PAGE_LEVEL && logicalRecord.length < 2) continue; - if (compareMode == CompareMode.FILE_LEVEL && logicalRecord.length < 3) continue; - - String logicalFileName = logicalRecord[0]; - String logicalAddress = logicalRecord[1]; - String logicalPageCount = compareMode == CompareMode.FILE_LEVEL ? logicalRecord[2] : null; - - if (physicalFileName.equals(logicalFileName)) { - found = true; - // 文件名相同,比较路径 - if (!physicalAddress.equals(logicalAddress)) { - pathMismatchResults.add("文件名=" + physicalFileName + - ", 物理地址=" + physicalAddress + - ", 逻辑地址=" + logicalAddress); - } - // 如果是文件级比较且路径相同,再比较页数 - else if (compareMode == CompareMode.FILE_LEVEL && - physicalPageCount != null && logicalPageCount != null && - !physicalPageCount.equals(logicalPageCount)) { - pageCountMismatchResults.add("文件名=" + physicalFileName + - ", 物理地址=" + physicalAddress + - ", 逻辑地址=" + logicalAddress + - ", 物理页数=" + physicalPageCount + - ", 逻辑页数=" + logicalPageCount); - } - break; - } - } - - // 如果在逻辑文件中未找到该物理文件记录 - if (!found) { - String result = "文件名=" + physicalFileName + ", 物理地址=" + physicalAddress; - if (compareMode == CompareMode.FILE_LEVEL && physicalPageCount != null) { - result += ", 物理页数=" + physicalPageCount; - } - forwardComparisonResults.add(result); - } + public AddressFileComparator() { + this.executor = Executors.newFixedThreadPool(System.getAvailableProcessors()); + } + @Setter + private ProgressCallback progressCallback; + // 安全调用回调方法 + private void safeOnPhaseStarted(Phase phase) { + if (progressCallback != null) { + progressCallback.onPhaseStarted(phase); } - - // 反向比较:遍历逻辑文件,检查是否在物理文件中存在 - for (String[] logicalRecord : logicalRecords) { - // 确保数据行有足够列数 - if (compareMode == CompareMode.PAGE_LEVEL && logicalRecord.length < 2) continue; - if (compareMode == CompareMode.FILE_LEVEL && logicalRecord.length < 3) continue; - - String logicalFileName = logicalRecord[0]; - String logicalAddress = logicalRecord[1]; - String logicalPageCount = compareMode == CompareMode.FILE_LEVEL ? logicalRecord[2] : null; - - boolean found = false; - for (String[] physicalRecord : physicalRecords) { - // 确保数据行有足够列数 - if (compareMode == CompareMode.PAGE_LEVEL && physicalRecord.length < 2) continue; - if (compareMode == CompareMode.FILE_LEVEL && physicalRecord.length < 3) continue; - - String physicalFileName = physicalRecord[0]; - - if (logicalFileName.equals(physicalFileName)) { - found = true; - break; - } - } - - // 如果在物理文件中未找到该逻辑文件记录 - if (!found) { - String result = "文件名=" + logicalFileName + ", 逻辑地址=" + logicalAddress; - if (compareMode == CompareMode.FILE_LEVEL && logicalPageCount != null) { - result += ", 逻辑页数=" + logicalPageCount; - } - backwardComparisonResults.add(result); - } - } - - // 将比较结果记录到日志中 - logComparisonResults(physicalCount, logicalCount, forwardComparisonResults, - backwardComparisonResults, pathMismatchResults, pageCountMismatchResults, compareMode); - - return new ComparisonResult( - physicalCount, - logicalCount, - forwardComparisonResults, - backwardComparisonResults, - pathMismatchResults, - pageCountMismatchResults - ); } - private List readCSV(String filePath) { + private void safeOnPhaseProgress(Phase phase, int current, int total) { + if (progressCallback != null) { + progressCallback.onPhaseProgress(phase, current, total); + } + } + + private void safeOnPhaseCompleted(Phase phase) { + if (progressCallback != null) { + progressCallback.onPhaseCompleted(phase); + } + } + public void shutdown() { + executor.shutdown(); + } + + public CompletableFuture compareFiles(String physicalFilePath, String logicalFilePath, CompareMode compareMode) { + // 读取物理文件 + CompletableFuture> physicalFuture = CompletableFuture.supplyAsync(() -> { + safeOnPhaseStarted(Phase.READ_PHYSICAL_CSV); + List list = readCSV(physicalFilePath, progressCallback, Phase.READ_PHYSICAL_CSV); + safeOnPhaseCompleted(Phase.READ_PHYSICAL_CSV); + return list; + }, executor); + + // 读取逻辑文件 + CompletableFuture> logicalFuture = CompletableFuture.supplyAsync(() -> { + safeOnPhaseStarted(Phase.READ_LOGICAL_CSV); + List list = readCSV(logicalFilePath, progressCallback, Phase.READ_LOGICAL_CSV); + safeOnPhaseCompleted(Phase.READ_LOGICAL_CSV); + return list; + }, executor); + + // 两个 CSV 读取完成后进行比较 + return physicalFuture.thenCombineAsync(logicalFuture, (physicalRecords, logicalRecords) -> { + int physicalCount = physicalRecords.size(); + int logicalCount = logicalRecords.size(); + + // 线程安全集合 + List forwardComparisonResults = Collections.synchronizedList(new ArrayList<>()); + List backwardComparisonResults = Collections.synchronizedList(new ArrayList<>()); + List pathMismatchResults = Collections.synchronizedList(new ArrayList<>()); + List pageCountMismatchResults = Collections.synchronizedList(new ArrayList<>()); + + Map logicalMap = logicalRecords.parallelStream() + .filter(r -> r.length >= (compareMode == CompareMode.FILE_LEVEL ? 3 : 2)) + .collect(Collectors.toConcurrentMap(r -> r[0], r -> r)); + + // 正向比较 + safeOnPhaseStarted(Phase.COMPARE_FORWARD); + AtomicInteger forwardCounter = new AtomicInteger(0); + physicalRecords.parallelStream() + .filter(r -> r.length >= (compareMode == CompareMode.FILE_LEVEL ? 3 : 2)) + .forEach(physicalRecord -> { + String physicalFileName = physicalRecord[0]; + String physicalAddress = physicalRecord[1]; + String physicalPageCount = compareMode == CompareMode.FILE_LEVEL ? physicalRecord[2] : null; + + String[] logicalRecord = logicalMap.get(physicalFileName); + if (logicalRecord == null) { + String result = "文件名=" + physicalFileName + ", 物理地址=" + physicalAddress; + if (compareMode == CompareMode.FILE_LEVEL && physicalPageCount != null) { + result += ", 物理页数=" + physicalPageCount; + } + forwardComparisonResults.add(result); + } else { + if (!physicalAddress.equals(logicalRecord[1])) { + pathMismatchResults.add("文件名=" + physicalFileName + + ", 物理地址=" + physicalAddress + + ", 逻辑地址=" + logicalRecord[1]); + } + if (compareMode == CompareMode.FILE_LEVEL && + physicalPageCount != null && !physicalPageCount.equals(logicalRecord[2])) { + pageCountMismatchResults.add("文件名=" + physicalFileName + + ", 物理地址=" + physicalAddress + + ", 逻辑地址=" + logicalRecord[1] + + ", 物理页数=" + physicalPageCount + + ", 逻辑页数=" + logicalRecord[2]); + } + } + + int done = forwardCounter.incrementAndGet(); + safeOnPhaseProgress(Phase.COMPARE_FORWARD, done, physicalRecords.size()); + }); + safeOnPhaseCompleted(Phase.COMPARE_FORWARD); + + // 反向比较 + safeOnPhaseStarted(Phase.COMPARE_BACKWARD); + AtomicInteger backwardCounter = new AtomicInteger(0); + Map physicalMap = physicalRecords.parallelStream() + .filter(r -> r.length >= (compareMode == CompareMode.FILE_LEVEL ? 3 : 2)) + .collect(Collectors.toConcurrentMap(r -> r[0], r -> r)); + + logicalRecords.parallelStream() + .filter(r -> r.length >= (compareMode == CompareMode.FILE_LEVEL ? 3 : 2)) + .forEach(logicalRecord -> { + String logicalFileName = logicalRecord[0]; + String logicalAddress = logicalRecord[1]; + String logicalPageCount = compareMode == CompareMode.FILE_LEVEL ? logicalRecord[2] : null; + + if (!physicalMap.containsKey(logicalFileName)) { + String result = "文件名=" + logicalFileName + ", 逻辑地址=" + logicalAddress; + if (compareMode == CompareMode.FILE_LEVEL && logicalPageCount != null) { + result += ", 逻辑页数=" + logicalPageCount; + } + backwardComparisonResults.add(result); + } + + int done = backwardCounter.incrementAndGet(); + safeOnPhaseProgress(Phase.COMPARE_BACKWARD, done, logicalRecords.size()); + }); + safeOnPhaseCompleted(Phase.COMPARE_BACKWARD); + + return new ComparisonResult( + physicalCount, + logicalCount, + forwardComparisonResults, + backwardComparisonResults, + pathMismatchResults, + pageCountMismatchResults + ); + }, executor); + } + + private List readCSV(String filePath, ProgressCallback callback, Phase phase) { List records = new ArrayList<>(); - try { File file = new File(filePath); if (!file.exists()) { - log.error(LoggerHelper.RELEASE_MARKER, "CSV文件不存在: {}", filePath); + log.error(LoggerMarker.RELEASE_MARKER, "{} CSV文件不存在: {}", phase, filePath); return records; } BufferedReader reader = new BufferedReader(new FileReader(file)); - String line; - boolean isFirstLine = true; - - while ((line = reader.readLine()) != null) { - // 跳过首行(标题行) - if (isFirstLine) { - isFirstLine = false; - continue; - } - // 按逗号分隔CSV行数据 - String[] data = line.split(","); - records.add(data); - } - + List allLines = reader.lines().skip(1).toList(); // 跳过标题行 reader.close(); - log.info(LoggerHelper.DEBUG_MARKER, "成功读取CSV文件,共 {} 行记录", records.size()); + int total = allLines.size(); + AtomicInteger counter = new AtomicInteger(0); + for (String line : allLines) { + records.add(line.split(",")); + int done = counter.incrementAndGet(); + if (done % 100 == 0 || done == total) { // 每100行或结束更新 + callback.onPhaseProgress(phase, done, total); + } + } } catch (Exception e) { - log.error(LoggerHelper.RELEASE_MARKER, "读取CSV文件时出错: {}", e.getMessage(), e); + log.error(LoggerMarker.RELEASE_MARKER, "{} CSV文件读取出错: {}", phase, e.getMessage(), e); } - return records; } - private void logComparisonResults(int physicalCount, int logicalCount, - List forwardResults, List backwardResults, - List pathMismatchResults, List pageCountMismatchResults, - CompareMode compareMode) { - StringBuilder sb = new StringBuilder(); - sb.append("=== 文件比较结果 ===\n"); - sb.append("物理地址文件记录数: ").append(physicalCount).append("\n"); - sb.append("逻辑地址文件记录数: ").append(logicalCount).append("\n"); - - if (pathMismatchResults.isEmpty()) { - sb.append("没有路径错误\n"); - } else { - sb.append("文件名相同但路径不一致的记录数量: ").append(pathMismatchResults.size()).append("\n"); - pathMismatchResults.forEach(result -> sb.append("\t").append(result).append("\n")); - } - - if (compareMode == CompareMode.FILE_LEVEL) { - if (pageCountMismatchResults.isEmpty()) { - sb.append("没有页数错误\n"); - } else { - sb.append("文件名和路径相同但页数不一致的记录数量: ") - .append(pageCountMismatchResults.size()).append("\n"); - pageCountMismatchResults.forEach(result -> sb.append("\t").append(result).append("\n")); - } - } - - if (forwardResults.isEmpty()) { - sb.append("没有物理存在而逻辑不存在的文件\n"); - } else { - sb.append("物理文件在逻辑文件中未找到的记录数量: ").append(forwardResults.size()).append("\n"); - forwardResults.forEach(result -> sb.append("\t").append(result).append("\n")); - } - - if (backwardResults.isEmpty()) { - sb.append("没有逻辑存在而物理不存在的文件\n"); - } else { - sb.append("逻辑文件在物理文件中未找到的记录数量: ").append(backwardResults.size()).append("\n"); - backwardResults.forEach(result -> sb.append("\t").append(result).append("\n")); - } - - sb.append("=== 比较完成 ==="); - - log.info(LoggerHelper.RELEASE_MARKER, sb.toString()); // 一次性输出, 减少 I/O - } - - // 为向后兼容保留原来的日志方法 - private void logComparisonResults(int physicalCount, int logicalCount, - List forwardResults, List backwardResults, - List pathMismatchResults) { - logComparisonResults(physicalCount, logicalCount, forwardResults, backwardResults, - pathMismatchResults, new ArrayList<>(), CompareMode.PAGE_LEVEL); - } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileGenerator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileGenerator.java index e43e2a7..5b2ca4a 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileGenerator.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileGenerator.java @@ -15,13 +15,19 @@ public interface AddressFileGenerator { int FILE_TYPE = 2; /** - * 回调接口 - */ - interface Callback { - void onProgress(String message); - void onSuccess(String outputPath); - void onError(String errorMessage); + * 进度回调接口 + */ + + interface ProgressCallback { + default void onPhaseStarted(Phase phase) {} + default void onPhaseProgress(Phase phase, int current, int total) {} + default void onPhaseCompleted(Phase phase) {} } + enum Phase { + GENERATE_LOGICAL, + GENERATE_PHYSICAL + } + void setProgressCallback(ProgressCallback callback); /** * 生成地址文件 @@ -29,12 +35,10 @@ public interface AddressFileGenerator { * @param folderPath 文件夹路径 * @param outputFile 输出文件 * @param folderType 文件夹类型 ({@link AddressFileGenerator#PAGE_TYPE "页面级"} 或 {@link AddressFileGenerator#FILE_TYPE "文件级"}) - * @param callback {@link Callback 回调接口} */ void generateAddressFile( String folderPath, File outputFile, - @MagicConstant(intValues = {PAGE_TYPE, FILE_TYPE}) int folderType, - Callback callback + @MagicConstant(intValues = {PAGE_TYPE, FILE_TYPE}) int folderType ); } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java index 6045b70..152bab4 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java @@ -1,13 +1,13 @@ package top.r3944realms.docchecktoolrefactored.core; -import com.sun.scenario.Settings; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import top.r3944realms.docchecktoolrefactored.System; import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner; import top.r3944realms.docchecktoolrefactored.model.DuplicateGroup; import top.r3944realms.docchecktoolrefactored.model.FileMetadata; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.IOException; import java.nio.file.Files; @@ -41,7 +41,7 @@ public class DuplicateFinder { @Setter private ProgressCallback progressCallback; private static final int PROGRESS_REPORT_INTERVAL = 100; - private static final int BATCH_SIZE = 100; + @Getter private final List errors = new CopyOnWriteArrayList<>(); @Getter private long timeout = -1; @@ -49,16 +49,14 @@ public class DuplicateFinder { this.fileScanner = Objects.requireNonNull(fileScanner); this.hashCalculator = Objects.requireNonNull(hashCalculator); this.enableProgress = enableProgress; - // 根据CPU核心数设置线程池大小 - int poolSize = Runtime.getRuntime().availableProcessors(); - this.executorService = Executors.newFixedThreadPool(poolSize); + this.executorService = Executors.newFixedThreadPool(System.getAvailableProcessors()); } public DuplicateFinder(FileScanner fileScanner, FileHashCalculator hashCalculator) { this(fileScanner, hashCalculator, false); } public DuplicateFinder applySetting(Setting setting) { - this.timeout = setting.getSingleTimeout(); + this.timeout = setting.getScanTimeout(); return this; } @@ -113,7 +111,7 @@ public class DuplicateFinder { CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); if (progressCallback != null) progressCallback.onPhaseCompleted(Phase.CALCULATE_HASH); - if (enableProgress) System.out.println(); + if (enableProgress) java.lang.System.out.println(); // ----------------------------- // 第三阶段:构建结果 @@ -154,13 +152,13 @@ public class DuplicateFinder { meta.setSize(Files.size(file)); sizeGroups.computeIfAbsent(meta.getSize(), k -> new ArrayList<>()).add(meta); } catch (IOException e) { - log.error(LoggerHelper.TRACE_MARKER, "Failed to get file's size: {}", file); + log.error(LoggerMarker.TRACE_MARKER, "Failed to get file's size: {}", file); } } @Override public void onScanComplete() {} @Override public void onError(Path file, Exception e) { - log.error(LoggerHelper.TRACE_MARKER, "Error on scanning file: {}, {}", file, e.getMessage()); + log.error(LoggerMarker.TRACE_MARKER, "Error on scanning file: {}, {}", file, e.getMessage()); errors.add(e); } }; @@ -188,7 +186,7 @@ public class DuplicateFinder { current, total); - System.out.print(progressBar); + java.lang.System.out.print(progressBar); } private void processFile(FileMetadata file, Map> hashGroups, @@ -214,11 +212,12 @@ public class DuplicateFinder { } } catch (IOException e) { - log.error(LoggerHelper.TRACE_MARKER, "Failed to calculate file's hash: {}, {}", file.getPath(), e.getMessage()); + log.error(LoggerMarker.TRACE_MARKER, "Failed to calculate file's hash: {}, {}", file.getPath(), e.getMessage()); errors.add(e); } } public void shutdown() { + fileScanner.cancel(); executorService.shutdown(); } } \ No newline at end of file diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java index 5784c19..297ccf1 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java @@ -4,7 +4,7 @@ package top.r3944realms.docchecktoolrefactored.core; import lombok.extern.slf4j.Slf4j; import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner; import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.BufferedWriter; import java.io.FileWriter; @@ -49,7 +49,7 @@ public class HashFileGenerator { @Override public void onError(Path path, Exception e) { - log.error(LoggerHelper.TRACE_MARKER, "Error scanning path: {} - {}", path, e.getMessage()); + log.error(LoggerMarker.TRACE_MARKER, "Error scanning path: {} - {}", path, e.getMessage()); } }); @@ -76,7 +76,7 @@ public class HashFileGenerator { listener.onProgressUpdate(processed, totalFiles); } } catch (IOException e) { - log.error(LoggerHelper.DEBUG_MARKER, "无法计算该文件哈希值: {} - {}", file, e.getMessage()); + log.error(LoggerMarker.DEBUG_MARKER, "无法计算该文件哈希值: {} - {}", file, e.getMessage()); } }); diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/LogicalAddressFileGenerator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/LogicalAddressFileGenerator.java index 564d045..e54bbe4 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/LogicalAddressFileGenerator.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/LogicalAddressFileGenerator.java @@ -1,49 +1,78 @@ package top.r3944realms.docchecktoolrefactored.core; +import lombok.extern.slf4j.Slf4j; import top.r3944realms.docchecktoolrefactored.io.reader.CatalogFileReader; import top.r3944realms.docchecktoolrefactored.io.reader.CatalogFileReaderFactory; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.File; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.util.List; - +@Slf4j public class LogicalAddressFileGenerator implements AddressFileGenerator { + private ProgressCallback callback; + // 安全调用回调方法 + private void safeOnPhaseStarted(Phase phase) { + if (callback != null) { + callback.onPhaseStarted(phase); + } + } + + private void safeOnPhaseProgress(Phase phase, int current, int total) { + if (callback != null) { + callback.onPhaseProgress(phase, current, total); + } + } + + private void safeOnPhaseCompleted(Phase phase) { + if (callback != null) { + callback.onPhaseCompleted(phase); + } + } + @Override - public void generateAddressFile(String catalogFilePath, File outputFile, int folderType, Callback callback) { - callback.onProgress("正在生成逻辑地址文件..."); + public void setProgressCallback(ProgressCallback callback) { + this.callback = callback; + } + @Override + public void generateAddressFile(String catalogFilePath, File outputFile, int folderType) { + safeOnPhaseStarted(Phase.GENERATE_LOGICAL); try { // 使用工厂模式创建相应的文件读取器 List records = readCatalogFile(catalogFilePath); - + safeOnPhaseProgress(Phase.GENERATE_LOGICAL, 1, 3); // 1/3 // 过滤掉 可能的 输出文件记录 -- 注2: 避免将输出文件作为输入处理 String outputFileName = outputFile.getName(); records = records.stream() .filter(record -> !record.archiveCode.equalsIgnoreCase(outputFileName)) .toList(); + safeOnPhaseProgress(Phase.GENERATE_LOGICAL, 2, 3); // 2/3 try (PrintWriter writer = new PrintWriter(outputFile, StandardCharsets.UTF_8)) { if (folderType == PAGE_TYPE) { // 页面级逻辑:为每页生成一行数据 - generatePageLevelFile(writer, records); + generatePageLevelFile(writer, records, callback); } else if (folderType == FILE_TYPE) { // 文件级逻辑:每个档案只生成一行数据 - generateFileLevelFile(writer, records); + generateFileLevelFile(writer, records, callback); } else { throw new IllegalArgumentException("不支持的文件夹类型: " + folderType); } } - - callback.onSuccess("逻辑地址文件生成成功: " + outputFile.getAbsolutePath()); + safeOnPhaseProgress(Phase.GENERATE_LOGICAL, 3, 3); // 3/3 + safeOnPhaseCompleted(Phase.GENERATE_LOGICAL); } catch (Exception e) { - callback.onError("生成逻辑地址文件时出错: " + e.getMessage()); + log.info(LoggerMarker.RELEASE_MARKER, "生成逻辑地址文件时出错: {}", e.getMessage()); } } /** * 生成页面级逻辑地址文件 */ - private void generatePageLevelFile(PrintWriter writer, List records) { + private void generatePageLevelFile(PrintWriter writer, List records, ProgressCallback callback) { // 写入CSV头部 writer.println("逻辑文件名,逻辑地址"); + int totalRecords = records.stream().mapToInt(r -> r.page).sum(); + int current = 0; // 处理每条记录 for (Record record : records) { @@ -60,6 +89,8 @@ public class LogicalAddressFileGenerator implements AddressFileGenerator { // 写入CSV行 writer.printf("%s,%s%n", logicalFileName, logicalAddress); + current++; + safeOnPhaseProgress(Phase.GENERATE_LOGICAL, current, totalRecords); } } } @@ -67,9 +98,11 @@ public class LogicalAddressFileGenerator implements AddressFileGenerator { /** * 生成文件级逻辑地址文件 */ - private void generateFileLevelFile(PrintWriter writer, List records) { + private void generateFileLevelFile(PrintWriter writer, List records, ProgressCallback callback) { // 写入CSV头部(包含页数列) writer.println("逻辑文件名,逻辑地址,页数"); + int totalRecords = records.stream().mapToInt(r -> r.page).sum(); + int current = 0; // 处理每条记录 for (Record record : records) { @@ -81,6 +114,7 @@ public class LogicalAddressFileGenerator implements AddressFileGenerator { // 写入CSV行,包含页数 writer.printf("%s,%s,%d%n", /* 逻辑文件名(就是档号)*/ archiveCode, logicalAddress, page); + safeOnPhaseProgress(Phase.GENERATE_LOGICAL, current, totalRecords); } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java index 4cbd4a6..461d434 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java @@ -1,9 +1,10 @@ package top.r3944realms.docchecktoolrefactored.core; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.File; import java.io.PrintWriter; @@ -11,39 +12,85 @@ import java.nio.charset.StandardCharsets; @Slf4j public class PhysicalAddressFileGenerator implements AddressFileGenerator { + private ProgressCallback callback; + // 安全调用回调方法 + private void safeOnPhaseStarted(Phase phase) { + if (callback != null) { + callback.onPhaseStarted(phase); + } + } + + private void safeOnPhaseProgress(Phase phase, int current, int total) { + if (callback != null) { + callback.onPhaseProgress(phase, current, total); + } + } + + private void safeOnPhaseCompleted(Phase phase) { + if (callback != null) { + callback.onPhaseCompleted(phase); + } + } + @Override - public void generateAddressFile(String folderPath, File outputFile, int folderType, Callback callback) { - callback.onProgress("正在生成物理地址文件..."); + public void setProgressCallback(ProgressCallback callback) { + this.callback = callback; + } + + @Override + public void generateAddressFile(String folderPath, File outputFile, int folderType) { + safeOnPhaseStarted(Phase.GENERATE_PHYSICAL); try { File rootFolder = new File(folderPath); if (!rootFolder.exists() || !rootFolder.isDirectory()) { - callback.onError("所选路径不存在或不是一个有效的文件夹。"); + safeOnPhaseCompleted(Phase.GENERATE_PHYSICAL); + safeOnPhaseProgress(Phase.GENERATE_PHYSICAL, 0, 0); return; } - // 保存输出文件的绝对路径,用于后续比较 - String outputFilePath = outputFile.getAbsolutePath(); + // 统计总文件数(页面级:图片数;文件级:PDF数) + int totalFiles = countFiles(rootFolder, folderType); + + // 计数器 + int[] counter = new int[]{0}; - // 写入CSV文件 try (PrintWriter writer = new PrintWriter(outputFile, StandardCharsets.UTF_8)) { - if (folderType == AddressFileGenerator.PAGE_TYPE) { - // 页面级逻辑:处理所有图片文件 + if (folderType == PAGE_TYPE) { writer.println("物理文件名,物理地址"); - processPageLevelFolder(rootFolder, writer, outputFilePath); - } else if (folderType == AddressFileGenerator.FILE_TYPE) { - // 文件级逻辑:处理PDF文件 + processPageLevelFolder(rootFolder, writer, outputFile.getAbsolutePath(), callback, counter, totalFiles); + } else if (folderType == FILE_TYPE) { writer.println("物理文件名,物理地址,页数"); - processFileLevelFolder(rootFolder, writer, outputFilePath); + processFileLevelFolder(rootFolder, writer, outputFile.getAbsolutePath(), callback, counter, totalFiles); } else { throw new IllegalArgumentException("不支持的文件夹类型: " + folderType); } } - callback.onSuccess("物理地址文件生成成功: " + outputFile.getAbsolutePath()); + safeOnPhaseCompleted(Phase.GENERATE_PHYSICAL); } catch (Exception e) { - callback.onError("生成物理地址文件时出错: " + e.getMessage()); + safeOnPhaseCompleted(Phase.GENERATE_PHYSICAL); + safeOnPhaseProgress(Phase.GENERATE_PHYSICAL, 0, 0); + log.error("生成物理地址文件失败: {}", e.getMessage(), e); } } + /** 递归统计总文件数 */ + private int countFiles(File folder, int folderType) { + int count = 0; + File[] files = folder.listFiles(file -> !file.isHidden()); + if (files != null) { + for (File file : files) { + if (file.isFile()) { + if ((folderType == PAGE_TYPE && isImageFile(file.getName())) + || (folderType == FILE_TYPE && isPdfFile(file.getName()))) { + count++; + } + } else if (file.isDirectory()) { + count += countFiles(file, folderType); + } + } + } + return count; + } /** * 处理页面级文件夹及其内部文件 * @@ -51,26 +98,20 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator { * @param writer PrintWriter对象 * @param outputFilePath 输出文件的绝对路径 */ - private void processPageLevelFolder(File folder, PrintWriter writer, String outputFilePath) { + private void processPageLevelFolder(File folder, PrintWriter writer, String outputFilePath, ProgressCallback callback, int[] counter, int total) { // 获取该文件夹下的所有非隐藏文件和文件夹 File[] filesAndFolders = folder.listFiles(file -> !file.isHidden()); if (filesAndFolders != null) { for (File file : filesAndFolders) { // 跳过输出文件本身,避免将生成的CSV文件也作为数据处理 - if (file.getAbsolutePath().equals(outputFilePath)) { - continue; - } + if (file.getAbsolutePath().equals(outputFilePath)) continue; - if (file.isFile()) { - String fileName = file.getName(); - // 只处理图片文件,跳过其他类型的文件 - if (!isImageFile(fileName)) { - continue; - } + // 只处理图片文件,跳过其他类型的文件 + if (file.isFile() && isImageFile(file.getName())) { // 移除文件扩展名 - String fileNameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.')); + String fileNameWithoutExt = file.getName().substring(0, file.getName().lastIndexOf('.')); // 格式化文件名,确保最后一个部分是4位数字 String formattedFileName = formatFileName(fileNameWithoutExt); @@ -80,9 +121,12 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator { // 写入CSV行 writer.printf("%s,%s%n", formattedFileName, physicalAddress); + + counter[0]++; + safeOnPhaseProgress(Phase.GENERATE_PHYSICAL, counter[0], total); } else if (file.isDirectory()) { // 递归处理子文件夹 - processPageLevelFolder(file, writer, outputFilePath); + processPageLevelFolder(file, writer, outputFilePath, callback, counter, total); } } } @@ -95,7 +139,7 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator { * @param writer PrintWriter对象 * @param outputFilePath 输出文件的绝对路径 */ - private void processFileLevelFolder(File folder, PrintWriter writer, String outputFilePath) { + private void processFileLevelFolder(File folder, PrintWriter writer, String outputFilePath, ProgressCallback callback, int[] counter, int total) { // 获取该文件夹下的所有非隐藏文件和文件夹 File[] filesAndFolders = folder.listFiles(file -> !file.isHidden()); @@ -106,12 +150,9 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator { continue; } - if (file.isFile()) { - String fileName = file.getName(); - // 只处理PDF文件 - if (isPdfFile(fileName)) { + if (file.isFile()&& isPdfFile(file.getName())) { // 移除文件扩展名 - String fileNameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.')); + String fileNameWithoutExt = file.getName().substring(0, file.getName().lastIndexOf('.')); // 生成物理地址路径(使用与页面级相同的逻辑) String physicalAddress = generatePhysicalAddress(file.getAbsolutePath(), fileNameWithoutExt); @@ -121,10 +162,12 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator { // 写入CSV行 writer.printf("%s,%s,%d%n", fileNameWithoutExt, physicalAddress, pageCount); - } + + counter[0]++; + safeOnPhaseProgress(Phase.GENERATE_PHYSICAL, counter[0], total); } else if (file.isDirectory()) { // 递归处理子文件夹 - processFileLevelFolder(file, writer, outputFilePath); + processFileLevelFolder(file, writer, outputFilePath, callback, counter, total); } } } @@ -137,14 +180,13 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator { * @return 页数 */ private int getPdfPageCount(File pdfFile) { - try { - // 使用Apache PDFBox库获取PDF页数 - PDDocument document = Loader.loadPDF(pdfFile); + // 使用Apache PDFBox库获取PDF页数 + try (PDDocument document = Loader.loadPDF(pdfFile)){ int pageCount = document.getNumberOfPages(); document.close(); return pageCount; } catch (Exception e) { - log.warn(LoggerHelper.RELEASE_MARKER, "无法获取PDF文件页数: {}", pdfFile.getAbsolutePath(), e); + log.warn(LoggerMarker.RELEASE_MARKER, "无法获取PDF文件页数: {}", pdfFile.getAbsolutePath(), e); return 0; } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/Setting.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/Setting.java index 55d63b6..c5112ef 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/Setting.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/Setting.java @@ -8,7 +8,7 @@ import lombok.extern.slf4j.Slf4j; @Getter @Slf4j public class Setting { - private long singleTimeout = 30; - private long totalTimeout = 60 * 5; + private long scanTimeout = 30; + private long taskTimeout = 60 * 5; private boolean enableStep = false; } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/DbfFileReader.java b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/DbfFileReader.java index 0f50175..4c78b94 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/DbfFileReader.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/DbfFileReader.java @@ -5,7 +5,7 @@ import com.linuxense.javadbf.DBFRow; import lombok.extern.slf4j.Slf4j; import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator; import top.r3944realms.docchecktoolrefactored.util.FileUtil; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.File; import java.io.FileInputStream; @@ -38,29 +38,29 @@ public class DbfFileReader implements CatalogFileReader { DBFReader reader = new DBFReader(fis) ) { int fieldCount = reader.getFieldCount(); - log.debug(LoggerHelper.DEBUG_MARKER, "开始读取DBF文件: {}, DBF文件字段数: {}",filePath, fieldCount); + log.debug(LoggerMarker.DEBUG_MARKER, "开始读取DBF文件: {}, DBF文件字段数: {}",filePath, fieldCount); // 查找"档号"和"页数"字段的索引 int archiveCodeIndex = -1; int pageIndex = -1; for (int i = 0; i < fieldCount; i++) { if (archiveCodeIndex != -1 && pageIndex != -1) { - log.debug(LoggerHelper.DEBUG_MARKER, "已找到所需字段,跳出循环,档号: {}, 页数: {}", archiveCodeIndex, pageIndex); + log.debug(LoggerMarker.DEBUG_MARKER, "已找到所需字段,跳出循环,档号: {}, 页数: {}", archiveCodeIndex, pageIndex); break; } String fieldName = reader.getField(i).getName(); - log.debug(LoggerHelper.DEBUG_MARKER, "发现字段: {}", fieldName); + log.debug(LoggerMarker.DEBUG_MARKER, "发现字段: {}", fieldName); if (ARCHIVE_CODE_TAG_CANDIDATES.contains(fieldName)) { archiveCodeIndex = i; - log.debug(LoggerHelper.DEBUG_MARKER, "匹配到档号字段: {}, 索引: {}", fieldName, archiveCodeIndex); + log.debug(LoggerMarker.DEBUG_MARKER, "匹配到档号字段: {}, 索引: {}", fieldName, archiveCodeIndex); } else if (PAGE_COUNT_TAG_CANDIDATES.contains(fieldName)) { pageIndex = i; - log.debug(LoggerHelper.DEBUG_MARKER, "匹配到页数字段: {}, 索引: {}", fieldName, pageIndex); + log.debug(LoggerMarker.DEBUG_MARKER, "匹配到页数字段: {}, 索引: {}", fieldName, pageIndex); } } if (archiveCodeIndex == -1 || pageIndex == -1) { - log.error(LoggerHelper.RELEASE_MARKER, "未找到必要字段,档号: {}, 页数: {}", + log.error(LoggerMarker.RELEASE_MARKER, "未找到必要字段,档号: {}, 页数: {}", archiveCodeIndex == -1 ? "未找到" : archiveCodeIndex, pageIndex == -1 ? "未找到" : pageIndex ); @@ -89,7 +89,7 @@ public class DbfFileReader implements CatalogFileReader { return Integer.parseInt(i.toString().trim()); } } catch (NumberFormatException e) { - log.warn(LoggerHelper.DEBUG_MARKER, "无法将页数值转换为整数: {}", i); + log.warn(LoggerMarker.DEBUG_MARKER, "无法将页数值转换为整数: {}", i); return 0; } }).orElse(0); @@ -98,17 +98,17 @@ public class DbfFileReader implements CatalogFileReader { if (!archiveCode.isEmpty() && page > 0) { records.add(new LogicalAddressFileGenerator.Record(archiveCode, page)); validRecords++; - log.debug(LoggerHelper.DEBUG_MARKER, "读取有效记录 - 档号: {}, 页数: {}", archiveCode, page); + log.debug(LoggerMarker.DEBUG_MARKER, "读取有效记录 - 档号: {}, 页数: {}", archiveCode, page); } else { skippedRecords++; if (!archiveCode.isEmpty() || page > 0) { - log.debug(LoggerHelper.DEBUG_MARKER, "跳过无效记录 - 档号: {}, 页数: {}", archiveCode, page); + log.debug(LoggerMarker.DEBUG_MARKER, "跳过无效记录 - 档号: {}, 页数: {}", archiveCode, page); } } } - log.info(LoggerHelper.RELEASE_MARKER, "DBF文件读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords); + log.info(LoggerMarker.RELEASE_MARKER, "DBF文件读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords); } catch (IOException e) { - log.error(LoggerHelper.RELEASE_MARKER, "读取DBF文件失败: {}", filePath, e); + log.error(LoggerMarker.RELEASE_MARKER, "读取DBF文件失败: {}", filePath, e); throw new UncheckedIOException("DBF文件读取异常", e); } return records; diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/ExcelFileReader.java b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/ExcelFileReader.java index abfc324..1a4cc85 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/ExcelFileReader.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/ExcelFileReader.java @@ -6,7 +6,7 @@ import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator; import top.r3944realms.docchecktoolrefactored.util.FileUtil; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.File; import java.io.FileInputStream; @@ -27,33 +27,33 @@ public class ExcelFileReader implements CatalogFileReader { } private List readExcelFile(File file, boolean isXlsx) throws Exception { List records = new ArrayList<>(); - log.debug(LoggerHelper.DEBUG_MARKER, "开始解析Excel文件,格式: {}", isXlsx ? "xlsx" : "xls"); + log.debug(LoggerMarker.DEBUG_MARKER, "开始解析Excel文件,格式: {}", isXlsx ? "xlsx" : "xls"); try (FileInputStream fis = new FileInputStream(file); Workbook workbook = isXlsx ? new XSSFWorkbook(fis) : new HSSFWorkbook(fis)) { // 获取第一个工作表 Sheet sheet = workbook.getSheetAt(0); - log.debug(LoggerHelper.DEBUG_MARKER, "读取工作表: {}", sheet.getSheetName()); + log.debug(LoggerMarker.DEBUG_MARKER, "读取工作表: {}", sheet.getSheetName()); // 获取标题行 Row headerRow = sheet.getRow(0); if (headerRow == null) { - log.error(LoggerHelper.RELEASE_MARKER, "Excel文件缺少标题行"); + log.error(LoggerMarker.RELEASE_MARKER, "Excel文件缺少标题行"); throw new IllegalArgumentException("Excel文件缺少标题行"); } // 查找"档号"和"页数"列的索引 int archiveCodeIndex = -1; int pageIndex = -1; - log.debug(LoggerHelper.DEBUG_MARKER, "开始查找'档号'和'页数'列的索引"); + log.debug(LoggerMarker.DEBUG_MARKER, "开始查找'档号'和'页数'列的索引"); boolean foundExactMatch = false; for (Cell cell : headerRow) { String cellValue = getCellValueAsString(cell).trim(); if (FIELD_ARCHIVE_CODE.equals(cellValue)) { archiveCodeIndex = cell.getColumnIndex(); - log.debug(LoggerHelper.DEBUG_MARKER, "找到'档号'列,索引: {}", archiveCodeIndex); + log.debug(LoggerMarker.DEBUG_MARKER, "找到'档号'列,索引: {}", archiveCodeIndex); } else if (FIELD_PAGE.equals(cellValue)) { pageIndex = cell.getColumnIndex(); foundExactMatch = true; - log.debug(LoggerHelper.DEBUG_MARKER, "找到精确匹配'页数'列,索引: {}", pageIndex); + log.debug(LoggerMarker.DEBUG_MARKER, "找到精确匹配'页数'列,索引: {}", pageIndex); } } // 如果没有精确匹配,进行模糊查找 @@ -62,13 +62,13 @@ public class ExcelFileReader implements CatalogFileReader { String cellValue = getCellValueAsString(cell).trim(); if (cellValue.contains(FIELD_PAGE)) { pageIndex = cell.getColumnIndex(); - log.debug(LoggerHelper.DEBUG_MARKER, "找到模糊匹配'页数'列,索引: {}", pageIndex); + log.debug(LoggerMarker.DEBUG_MARKER, "找到模糊匹配'页数'列,索引: {}", pageIndex); } } } // 检查是否找到必需的列 if (archiveCodeIndex == -1 || pageIndex == -1) { - log.error(LoggerHelper.RELEASE_MARKER, "未找到必要字段,档号: {}, 页数: {}", + log.error(LoggerMarker.RELEASE_MARKER, "未找到必要字段,档号: {}, 页数: {}", archiveCodeIndex == -1 ? "未找到" : archiveCodeIndex, pageIndex == -1 ? "未找到" : pageIndex ); @@ -84,7 +84,7 @@ public class ExcelFileReader implements CatalogFileReader { int validRecords = 0; int skippedRecords = 0; - for (int i = 1; i < totalRows; i++) { + for (int i = 1; i <= totalRows; i++) { Row row = sheet.getRow(i); if (row == null) { skippedRecords++; @@ -116,12 +116,12 @@ public class ExcelFileReader implements CatalogFileReader { // 只有数据有效时才添加记录 records.add(new LogicalAddressFileGenerator.Record(archiveCode, page)); validRecords++; - log.debug(LoggerHelper.DEBUG_MARKER, "读取有效记录 - 档号: {}, 页数: {}", archiveCode, page); + log.debug(LoggerMarker.DEBUG_MARKER, "读取有效记录 - 档号: {}, 页数: {}", archiveCode, page); } - log.info(LoggerHelper.RELEASE_MARKER, "数据读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords); + log.info(LoggerMarker.RELEASE_MARKER, "数据读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords); } catch (Exception e) { - log.error(LoggerHelper.RELEASE_MARKER, "读取Excel文件时发生错误: {}", e.getMessage(), e); + log.error(LoggerMarker.RELEASE_MARKER, "读取Excel文件时发生错误: {}", e.getMessage(), e); throw e; } return records; @@ -189,7 +189,7 @@ public class ExcelFileReader implements CatalogFileReader { return 0; } } catch (NumberFormatException e) { - log.warn(LoggerHelper.DEBUG_MARKER, "无法将单元格值转换为整数: {}", cell); + log.warn(LoggerMarker.DEBUG_MARKER, "无法将单元格值转换为整数: {}", cell); return 0; } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/XmlFileReader.java b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/XmlFileReader.java index f3db04a..4ac6bf7 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/XmlFileReader.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/XmlFileReader.java @@ -7,7 +7,7 @@ import org.w3c.dom.Node; import org.w3c.dom.NodeList; import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator; import top.r3944realms.docchecktoolrefactored.util.FileUtil; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -43,11 +43,11 @@ public class XmlFileReader implements CatalogFileReader { Document doc = builder.parse(file); doc.getDocumentElement().normalize(); - log.debug(LoggerHelper.DEBUG_MARKER, "开始解析XML文件: {}, 根元素: {}", + log.debug(LoggerMarker.DEBUG_MARKER, "开始解析XML文件: {}, 根元素: {}", file.getName(), doc.getDocumentElement().getNodeName()); // 查找记录元素 NodeList recordNodes = findAllRecordNodes(doc); - log.debug(LoggerHelper.DEBUG_MARKER, "找到 {} 个潜在记录节点", recordNodes.getLength()); + log.debug(LoggerMarker.DEBUG_MARKER, "找到 {} 个潜在记录节点", recordNodes.getLength()); // 解析每个记录元素 int validCount = 0; @@ -62,16 +62,16 @@ public class XmlFileReader implements CatalogFileReader { if (record != null) { records.add(record); validCount++; - log.debug(LoggerHelper.DEBUG_MARKER, "解析到有效记录: {}", record); + log.debug(LoggerMarker.DEBUG_MARKER, "解析到有效记录: {}", record); } else { invalidCount++; - log.debug(LoggerHelper.DEBUG_MARKER, "跳过无效记录节点"); + log.debug(LoggerMarker.DEBUG_MARKER, "跳过无效记录节点"); } } } - log.info(LoggerHelper.RELEASE_MARKER, "XML解析完成 - 有效记录: {}, 无效记录: {}", validCount, invalidCount); + log.info(LoggerMarker.RELEASE_MARKER, "XML解析完成 - 有效记录: {}, 无效记录: {}", validCount, invalidCount); } catch (Exception e) { - log.error(LoggerHelper.RELEASE_MARKER, "解析XML文件失败: {}", filePath, e); + log.error(LoggerMarker.RELEASE_MARKER, "解析XML文件失败: {}", filePath, e); throw new Exception("解析XML文件失败: " + filePath, e); } return records; @@ -93,11 +93,11 @@ public class XmlFileReader implements CatalogFileReader { for (String tagName : RECORD_TAG_CANDIDATES) { NodeList nodes = doc.getElementsByTagName(tagName); if (nodes.getLength() > 0) { - log.debug(LoggerHelper.DEBUG_MARKER, "使用标签名 '{}' 找到 {} 个记录节点", tagName, nodes.getLength()); + log.debug(LoggerMarker.DEBUG_MARKER, "使用标签名 '{}' 找到 {} 个记录节点", tagName, nodes.getLength()); return nodes; } } - log.warn(LoggerHelper.DEBUG_MARKER, "未找到任何记录节点,尝试的标签名: {}", RECORD_TAG_CANDIDATES); + log.warn(LoggerMarker.DEBUG_MARKER, "未找到任何记录节点,尝试的标签名: {}", RECORD_TAG_CANDIDATES); return new EmptyNodeList(); // 返回空节点列表而不是null } @@ -106,7 +106,7 @@ public class XmlFileReader implements CatalogFileReader { String pageStr = findFirstNonEmptyTextContent(recordElement, PAGE_COUNT_TAG_CANDIDATES); if (archiveCode == null || archiveCode.isEmpty()) { - log.debug(LoggerHelper.RELEASE_MARKER, "记录缺少档号字段"); + log.debug(LoggerMarker.RELEASE_MARKER, "记录缺少档号字段"); return null; } @@ -115,11 +115,11 @@ public class XmlFileReader implements CatalogFileReader { if (page > 0) { return new LogicalAddressFileGenerator.Record(archiveCode, page); } else { - log.debug(LoggerHelper.RELEASE_MARKER, "无效的页数值: {}", pageStr); + log.debug(LoggerMarker.RELEASE_MARKER, "无效的页数值: {}", pageStr); return null; } } catch (NumberFormatException e) { - log.warn(LoggerHelper.RELEASE_MARKER, "页数字段格式错误: {}", pageStr); + log.warn(LoggerMarker.RELEASE_MARKER, "页数字段格式错误: {}", pageStr); return null; } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/FileScanner.java b/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/FileScanner.java index c882278..a1fa2fe 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/FileScanner.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/FileScanner.java @@ -1,11 +1,12 @@ package top.r3944realms.docchecktoolrefactored.io.scanner; import java.nio.file.Path; +import java.util.Scanner; /** * The interface File scanner. */ -public interface FileScanner { +public interface FileScanner extends AutoCloseable { /** * 扫描指定路径下的文件 * @@ -39,6 +40,11 @@ public interface FileScanner { throw new UnsupportedOperationException("Please implement FileScanner, ProgressAwareListener."); } + /** + * 取消扫描 + */ + void cancel(); + /** * 文件扫描监听器 */ diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/RobustParallelScanner.java b/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/RobustParallelScanner.java index 85ef97a..37b299f 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/RobustParallelScanner.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/RobustParallelScanner.java @@ -1,7 +1,7 @@ package top.r3944realms.docchecktoolrefactored.io.scanner; import lombok.extern.slf4j.Slf4j; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.FileNotFoundException; import java.io.IOException; @@ -16,7 +16,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @Slf4j -public class RobustParallelScanner implements FileScanner, AutoCloseable { +public class RobustParallelScanner implements FileScanner { private final ForkJoinPool forkJoinPool; private volatile boolean cancelled = false; private final int maxDepth; @@ -60,7 +60,7 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable { } } } catch (IOException e) { - log.warn(LoggerHelper.TRACE_MARKER, "Failed to pre-scan: {}", dir, e); + log.warn(LoggerMarker.TRACE_MARKER, "Failed to pre-scan: {}", dir, e); } } private void scanInternal(Path rootPath, FileScanListener listener, AtomicLong totalFiles) { @@ -83,7 +83,7 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable { } }).get(timeout, TimeUnit.SECONDS); } catch (TimeoutException e) { - log.error(LoggerHelper.TRACE_MARKER, "Scan timeout: {}", rootPath, e); + log.error(LoggerMarker.TRACE_MARKER, "Scan timeout: {}", rootPath, e); forkJoinPool.shutdownNow(); listener.onError(rootPath, new TimeoutException("扫描超时30秒")); } catch (Exception e) { @@ -140,6 +140,8 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable { throw new IOException("系统目录禁止访问: " + path); } } + + @Override public void cancel() { cancelled = true; forkJoinPool.shutdownNow(); diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/LoginStageController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/LoginStageController.java index c6a3a39..98997c3 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/LoginStageController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/LoginStageController.java @@ -12,7 +12,7 @@ import javafx.scene.input.KeyEvent; import javafx.scene.layout.BorderPane; import lombok.extern.slf4j.Slf4j; import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.net.URL; import java.util.ResourceBundle; @@ -39,10 +39,10 @@ public class LoginStageController implements Initializable { String password = passwordField.getText(); if ("admin".equals(username) && "admin".equals(password)) { - log.info(LoggerHelper.DEBUG_MARKER, "{} Login successful", username); + log.info(LoggerMarker.DEBUG_MARKER, "{} Login successful", username); SceneManager.switchMainView(); } else { - log.info(LoggerHelper.DEBUG_MARKER, "Invalid username or password"); + log.info(LoggerMarker.DEBUG_MARKER, "Invalid username or password"); DialogUtil.showErrorDialog("错误", null, "用户名或密码错误!"); } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SceneManager.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SceneManager.java index 76b26e1..2d579b4 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SceneManager.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SceneManager.java @@ -14,7 +14,7 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import top.r3944realms.docchecktoolrefactored.Main; import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.IOException; import java.util.ArrayList; @@ -76,7 +76,7 @@ public class SceneManager { }); } catch (IOException e) { - log.error(LoggerHelper.TRACE_MARKER, "Failed to load main view", e); + log.error(LoggerMarker.TRACE_MARKER, "Failed to load main view", e); DialogUtil.showErrorDialog("错误", "加载主界面失败", e.getMessage()); } } @@ -99,7 +99,7 @@ public class SceneManager { openStages.add(settingStage); } catch (IOException e) { - log.error(LoggerHelper.TRACE_MARKER, "Failed to open setting view: {}", e.getMessage(), e); + log.error(LoggerMarker.TRACE_MARKER, "Failed to open setting view: {}", e.getMessage(), e); DialogUtil.showErrorDialog("错误", "加载设置窗口失败", "无法加载设置窗口: " + e.getMessage()); } } @@ -149,7 +149,7 @@ public class SceneManager { } }); } catch (IOException e) { - log.error(LoggerHelper.TRACE_MARKER, "Failed to load view: {}", fxmlPath, e); + log.error(LoggerMarker.TRACE_MARKER, "Failed to load view: {}", fxmlPath, e); DialogUtil.showErrorDialog("错误", "加载视图失败", "无法加载视图: " + e.getMessage()); } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SettingDialogController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SettingDialogController.java index ac71912..d47291e 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SettingDialogController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SettingDialogController.java @@ -20,7 +20,7 @@ public class SettingDialogController implements Initializable { @FXML private CheckBox enableStepCB; @FXML private Button resetB, saveB, cancelB; - @FXML private Spinner scanTotalTimeOutS, scanSingleTimeOutS; + @FXML private Spinner scanTimeOutS, taskTimeOutS; private Setting setting; @@ -35,14 +35,14 @@ public class SettingDialogController implements Initializable { setting = System.getSetting(); // 初始化 Spinner - scanSingleTimeOutS.setValueFactory(new LongSpinnerValueFactory(1, 3600, setting.getSingleTimeout())); - scanSingleTimeOutS.setEditable(true); - scanTotalTimeOutS.setValueFactory(new LongSpinnerValueFactory(1, 3600 * 24, setting.getTotalTimeout())); - scanTotalTimeOutS.setEditable(true); + scanTimeOutS.setValueFactory(new LongSpinnerValueFactory(1, 3600, setting.getScanTimeout())); + scanTimeOutS.setEditable(true); + taskTimeOutS.setValueFactory(new LongSpinnerValueFactory(1, 3600 * 24, setting.getTaskTimeout())); + taskTimeOutS.setEditable(true); // 添加焦点离开时校验 - addSpinnerValidation(scanSingleTimeOutS, SINGLE_MIN, SINGLE_MAX); - addSpinnerValidation(scanTotalTimeOutS, TOTAL_MIN, TOTAL_MAX); + addSpinnerValidation(scanTimeOutS, SINGLE_MIN, SINGLE_MAX); + addSpinnerValidation(taskTimeOutS, TOTAL_MIN, TOTAL_MAX); enableStepCB.setSelected(setting.isEnableStep()); } @@ -50,8 +50,8 @@ public class SettingDialogController implements Initializable { @FXML void onSave(ActionEvent actionEvent) { // 更新配置对象 - setting.setSingleTimeout(scanSingleTimeOutS.getValue()); - setting.setTotalTimeout(scanTotalTimeOutS.getValue()); + setting.setScanTimeout(scanTimeOutS.getValue()); + setting.setTaskTimeout(taskTimeOutS.getValue()); setting.setEnableStep(enableStepCB.isSelected()); // 保存到配置文件 System.saveSettingsNow(); @@ -66,8 +66,8 @@ public class SettingDialogController implements Initializable { /** 重置为默认值 */ @FXML void onReset(ActionEvent actionEvent) { - scanSingleTimeOutS.getValueFactory().setValue(30L); // 默认单次超时 - scanTotalTimeOutS.getValueFactory().setValue(300L); // 默认总超时 + scanTimeOutS.getValueFactory().setValue(30L); // 默认单次超时 + taskTimeOutS.getValueFactory().setValue(300L); // 默认总超时 enableStepCB.setSelected(false); } @@ -79,12 +79,12 @@ public class SettingDialogController implements Initializable { @FXML void onCheckOne(MouseDragEvent mouseDragEvent) { - validateSpinnerValue(scanSingleTimeOutS, 30, 3600); + validateSpinnerValue(scanTimeOutS, 30, 3600); } @FXML void onCheckTwo(MouseDragEvent mouseDragEvent) { - validateSpinnerValue(scanTotalTimeOutS, 60, 3600 * 24); + validateSpinnerValue(taskTimeOutS, 60, 3600 * 24); } @FXML void onSettingThree(ActionEvent actionEvent) { diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/DuplicateDocumentPaneController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/DuplicateDocumentPaneController.java index e8f8b7c..6a40dc9 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/DuplicateDocumentPaneController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/DuplicateDocumentPaneController.java @@ -12,8 +12,8 @@ import lombok.extern.slf4j.Slf4j; import top.r3944realms.docchecktoolrefactored.System; import top.r3944realms.docchecktoolrefactored.ui.SceneManager; import top.r3944realms.docchecktoolrefactored.ui.task.DuplicateDocumentDetectionTask; -import top.r3944realms.docchecktoolrefactored.ui.utils.ProgressBarUtil; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import top.r3944realms.docchecktoolrefactored.ui.utils.ProgressBar; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.File; @@ -23,12 +23,12 @@ import java.io.File; @Slf4j public class DuplicateDocumentPaneController { - @FXML private TextArea result1B; + @FXML private TextArea result1TA; @FXML private TextField loadFolder1TF; @FXML private Button selectLoadFolder1B; @FXML private Button start1B; @FXML private Button cancel1B; - private final ProgressBarUtil progressBarUtil = new ProgressBarUtil(); + private final ProgressBar progressBar = new ProgressBar(); private DuplicateDocumentDetectionTask currentTask; // 保存任务引用 /** * On select folder. @@ -51,72 +51,75 @@ public class DuplicateDocumentPaneController { * @param actionEvent the action event */ @FXML void onStart(ActionEvent actionEvent) { - log.info(LoggerHelper.DEBUG_MARKER, "用户点击了开始查重按钮"); - String folderPath = loadFolder1TF.getText(); - if (folderPath == null || folderPath.trim().isEmpty()) { - log.warn(LoggerHelper.DEBUG_MARKER, "未选择文件夹,无法进行查重"); - result1B.setText("请选择要检查的文件夹。"); - return; - } // 禁用开始按钮避免重复点击 start1B.setDisable(true); + log.info(LoggerMarker.DEBUG_MARKER, "用户点击了开始查重按钮"); + String folderPath = loadFolder1TF.getText(); + if (folderPath == null || folderPath.trim().isEmpty()) { + log.warn(LoggerMarker.DEBUG_MARKER, "未选择文件夹,无法进行查重"); + result1TA.setText("请选择要检查的文件夹。"); + start1B.setDisable(false); + return; + } cancel1B.setDisable(false); // 显示进度条窗口 - progressBarUtil.showProgress(SceneManager.getPrimaryStage(), "重复文件检测", "正在初始化扫描..."); + progressBar.showProgress(SceneManager.getPrimaryStage(), "重复文件检测", "正在初始化扫描..."); - // 创建并启动后台任务 + // 创建后台任务 DuplicateDocumentDetectionTask task = new DuplicateDocumentDetectionTask(folderPath); // 保存到字段 currentTask = task; // 绑定任务属性到UI ChangeListener progressChangeListener = (obs, oldVal, newVal) -> { if (newVal != null) { - progressBarUtil.updateProgress(newVal.doubleValue(), - task.getMessage() != null ? task.getMessage() : "处理中..."); + if (task.getMessage() != null) { + progressBar.updateProgress(newVal.doubleValue(), task.getMessage()); + } } }; task.progressProperty().addListener(progressChangeListener); // 绑定任务的消息到结果文本区域 ChangeListener messageChangeListener = (observable, oldValue, newValue) -> { - result1B.setText(newValue); + result1TA.setText(newValue); }; task.messageProperty().addListener(messageChangeListener); + // 绑定取消按钮 -> task.cancel() + progressBar.setOnCancel(() -> { + if (currentTask != null && currentTask.isRunning()) { + currentTask.cancel(); + } + }); + // 当任务完成时显示完整结果 task.setOnSucceeded(e -> { - progressBarUtil.closeProgress(); - result1B.setText(task.getValue()); + progressBar.closeProgress(); + result1TA.setText(task.getValue()); start1B.setDisable(false); cancel1B.setDisable(true); - log.info(LoggerHelper.RELEASE_MARKER, "查重任务完成,结果如下:{}", task.getValue()); + log.info(LoggerMarker.RELEASE_MARKER, "查重任务完成,结果如下:{}", task.getValue()); }); // 处理任务失败情况 task.setOnFailed(e -> { - progressBarUtil.closeProgress(); + progressBar.closeProgress(); Throwable exception = task.getException(); - result1B.setText("检测过程中发生错误: " + exception.getMessage()); + result1TA.setText("检测过程中发生错误: " + exception.getMessage()); start1B.setDisable(false); cancel1B.setDisable(true); - log.error(LoggerHelper.RELEASE_MARKER, "查重任务失败", exception); + log.error(LoggerMarker.RELEASE_MARKER, "查重任务失败", exception); }); // 处理任务取消情况 task.setOnCancelled(e -> { - progressBarUtil.closeProgress(); - result1B.appendText("\n检测已取消"); + progressBar.closeProgress(); + result1TA.appendText("\n检测已取消"); start1B.setDisable(false); cancel1B.setDisable(true); currentTask.progressProperty().removeListener(progressChangeListener); currentTask.messageProperty().removeListener(messageChangeListener); - log.info(LoggerHelper.RELEASE_MARKER, "查重任务已被取消"); - }); - // 绑定取消按钮 -> task.cancel() - progressBarUtil.setOnCancel(() -> { - if (currentTask != null && currentTask.isRunning()) { - currentTask.cancel(); - } + log.info(LoggerMarker.RELEASE_MARKER, "查重任务已被取消"); }); // 在新线程中执行任务 Thread thread = new Thread(task); @@ -128,7 +131,7 @@ public class DuplicateDocumentPaneController { if (currentTask != null && currentTask.isRunning()) { currentTask.cancel(); // 触发 setOnCancelled } else { - log.warn(LoggerHelper.DEBUG_MARKER, "没有正在运行的任务可取消"); + log.warn(LoggerMarker.DEBUG_MARKER, "没有正在运行的任务可取消"); } } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java index 2bc51a1..945e16f 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java @@ -1,5 +1,7 @@ package top.r3944realms.docchecktoolrefactored.ui.module; +import javafx.beans.value.ChangeListener; +import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; @@ -9,12 +11,15 @@ import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.stage.FileChooser; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import top.r3944realms.docchecktoolrefactored.System; import top.r3944realms.docchecktoolrefactored.core.AddressFileComparator; import top.r3944realms.docchecktoolrefactored.core.AddressFileGenerator; -import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator; -import top.r3944realms.docchecktoolrefactored.core.PhysicalAddressFileGenerator; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import top.r3944realms.docchecktoolrefactored.ui.SceneManager; +import top.r3944realms.docchecktoolrefactored.ui.task.AddressFileComparisonTask; +import top.r3944realms.docchecktoolrefactored.ui.task.AddressFileGenerationTask; +import top.r3944realms.docchecktoolrefactored.ui.utils.ProgressBar; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.File; import java.net.URL; @@ -24,7 +29,6 @@ import java.util.ResourceBundle; /** * The type Path check pane controller. */ -//TODO: 应该交给Platform:runLater; @Slf4j public class PathCheckPaneController implements Initializable { @FXML private ChoiceBox loadFolderType2CB; @@ -41,17 +45,16 @@ public class PathCheckPaneController implements Initializable { private String physicalAddressFilePath = null; - // 逻辑地址文件生成器实例 - private final LogicalAddressFileGenerator generator = new LogicalAddressFileGenerator(); - private final PhysicalAddressFileGenerator paGenerator = new PhysicalAddressFileGenerator(); - + private final ProgressBar progressBar = new ProgressBar(false); + private final ProgressBar cancelableProgressBar = new ProgressBar(); + private Task currentTask; // 保存任务引用 /** * On select lc. * * @param actionEvent the action event */ @FXML void onSelectLC(ActionEvent actionEvent) { - log.info(LoggerHelper.DEBUG_MARKER, "用户点击了选择目录文件按钮"); + log.info(LoggerMarker.DEBUG_MARKER, "用户点击了选择目录文件按钮"); FileChooser fileChooser = System.getFileChooser(); fileChooser.setTitle("选择目录文件"); @@ -71,9 +74,9 @@ public class PathCheckPaneController implements Initializable { if (selectedFile != null) { loadCatalog2TF.setText(selectedFile.getAbsolutePath()); System.setLastModifiedFile(selectedFile); - log.info(LoggerHelper.DEBUG_MARKER, "选择的目录文件路径为:{}", selectedFile.getAbsolutePath()); + log.info(LoggerMarker.DEBUG_MARKER, "选择的目录文件路径为:{}", selectedFile.getAbsolutePath()); }else{ - log.warn(LoggerHelper.DEBUG_MARKER, "用户未选择任何文件夹"); + log.warn(LoggerMarker.DEBUG_MARKER, "用户未选择任何文件夹"); result2TA.setText("未选择任何文件夹,请重新选择。"); } } @@ -92,13 +95,13 @@ public class PathCheckPaneController implements Initializable { } else if (selectedMode == Mode.FILE_TYPE) { directoryChooser.setTitle("选择文件级文件夹"); } - log.info(LoggerHelper.DEBUG_MARKER, "用户选择的模式为:{}", selectedMode); + log.info(LoggerMarker.DEBUG_MARKER, "用户选择的模式为:{}", selectedMode); File selectedDirectory = directoryChooser.showDialog(selectJPGFolder2B.getScene().getWindow()); if (selectedDirectory != null) { loadJPGFolder2TF.setText(selectedDirectory.getAbsolutePath()); - log.info(LoggerHelper.DEBUG_MARKER, "选择的{}文件夹路径为:{}", selectedMode,selectedDirectory.getAbsolutePath()); + log.info(LoggerMarker.DEBUG_MARKER, "选择的{}文件夹路径为:{}", selectedMode,selectedDirectory.getAbsolutePath()); } System.setLastModifiedFile(selectedDirectory); } @@ -109,10 +112,12 @@ public class PathCheckPaneController implements Initializable { * @param actionEvent the action event */ @FXML void onGenerateLA(ActionEvent actionEvent) { - log.info(LoggerHelper.DEBUG_MARKER, "用户点击了生成逻辑地址文件按钮"); + generateLogicalAddress2B.setDisable(true); + log.info(LoggerMarker.DEBUG_MARKER, "用户点击了生成逻辑地址文件按钮"); String filePath = loadCatalog2TF.getText(); if (filePath.isEmpty()) { result2TA.setText("请先选择目录文件。"); + generateLogicalAddress2B.setDisable(false); return; } @@ -124,10 +129,12 @@ public class PathCheckPaneController implements Initializable { File outputFile = fileChooser.showSaveDialog(generateLogicalAddress2B.getScene().getWindow()); if (outputFile == null) { result2TA.setText("未选择保存位置"); - log.warn(LoggerHelper.DEBUG_MARKER, "用户未选择任何文件"); + log.warn(LoggerMarker.DEBUG_MARKER, "用户未选择任何文件"); + generateLogicalAddress2B.setDisable(true); return; } System.setLastModifiedFile(outputFile); + progressBar.showProgress(SceneManager.getPrimaryStage(), "生成逻辑路径文件", "正在初始化..."); // 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加 final File finalOutputFile; if (!outputFile.getName().toLowerCase().endsWith(".csv")) { @@ -138,33 +145,54 @@ public class PathCheckPaneController implements Initializable { // 保存生成的文件路径 logicalAddressFilePath = finalOutputFile.getAbsolutePath(); - log.info(LoggerHelper.DEBUG_MARKER, "选择的输出文件路径: {}", logicalAddressFilePath); - // 创建后台任务来处理文件生成 - Thread backgroundThread = new Thread(() -> { - // 获取当前选择的文件夹类型 - Mode selectedMode = loadFolderType2CB.getValue(); - int folderType = (selectedMode == Mode.PAGE_TYPE) ? AddressFileGenerator.PAGE_TYPE : AddressFileGenerator.FILE_TYPE; - - generator.generateAddressFile(filePath, finalOutputFile, folderType, new LogicalAddressFileGenerator.Callback() { - @Override - public void onProgress(String message) { - javafx.application.Platform.runLater(() -> result2TA.setText(message)); + log.info(LoggerMarker.DEBUG_MARKER, "选择的输出文件路径: {}", logicalAddressFilePath); + Mode selectedMode = loadFolderType2CB.getValue(); + // 创建后台任务 + AddressFileGenerationTask task = new AddressFileGenerationTask(filePath, outputFile, selectedMode.number, true); + // 绑定任务属性到UI + ChangeListener progressChangeListener = (obs, oldVal, newVal) -> { + if (newVal != null) { + if (task.getMessage() != null) { + progressBar.updateProgress(newVal.doubleValue(), task.getMessage()); } + } + }; + task.progressProperty().addListener(progressChangeListener); - @Override - public void onSuccess(String outputPath) { - javafx.application.Platform.runLater(() -> result2TA.setText(outputPath)); - } - - @Override - public void onError(String errorMessage) { - javafx.application.Platform.runLater(() -> result2TA.setText(errorMessage)); - } - }); + // 绑定任务的消息到结果文本区域 + ChangeListener messageChangeListener = (observable, oldValue, newValue) -> { + result2TA.setText(newValue); + }; + task.messageProperty().addListener(messageChangeListener); + // 当任务完成时显示完整结果 + task.setOnSucceeded(e -> { + progressBar.closeProgress(); + result2TA.setText(task.getValue()); + generateLogicalAddress2B.setDisable(false); + log.info(LoggerMarker.RELEASE_MARKER, "生成逻辑路径 csv 文件任务完成,输出csv文件路径:{}", task.getValue()); }); + // 处理任务失败情况 + task.setOnFailed(e -> { + progressBar.closeProgress(); + Throwable exception = task.getException(); + result2TA.setText("检测过程中发生错误: " + exception.getMessage()); + generateLogicalAddress2B.setDisable(false); + log.error(LoggerMarker.RELEASE_MARKER, "生成逻辑路径 csv 文件任务失败", exception); + }); - backgroundThread.start(); + // 处理任务取消情况 + task.setOnCancelled(e -> { + progressBar.closeProgress(); + result2TA.appendText("\n检测已取消"); + generateLogicalAddress2B.setDisable(false); + currentTask.progressProperty().removeListener(progressChangeListener); + currentTask.messageProperty().removeListener(messageChangeListener); + log.info(LoggerMarker.RELEASE_MARKER, "生成逻辑路径 csv 文件任务已被取消"); + }); + Thread thread = new Thread(task); + thread.setDaemon(true); + thread.start(); } /** @@ -173,14 +201,17 @@ public class PathCheckPaneController implements Initializable { * @param actionEvent the action event */ @FXML void onGeneratePA(ActionEvent actionEvent) { + generatePhysicalAddress2B.setDisable(true); String folderPath = loadJPGFolder2TF.getText(); if (folderPath.isEmpty()) { result2TA.setText("请先选择文件夹。"); + generatePhysicalAddress2B.setDisable(false); return; } File folder = new File(folderPath); if(!folder.exists() || !folder.isDirectory()) { result2TA.setText("所选路径不存在或不是一个有效的文件夹。"); + generatePhysicalAddress2B.setDisable(false); return; } @@ -193,10 +224,11 @@ public class PathCheckPaneController implements Initializable { if (outputFile == null) { result2TA.setText("未选择保存位置"); + generatePhysicalAddress2B.setDisable(false); return; } System.setLastModifiedFile(outputFile); - + progressBar.showProgress(SceneManager.getPrimaryStage(), "生成物理路径文件", "正在初始化..."); // 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加 final File finalOutputFile; if (!outputFile.getName().toLowerCase().endsWith(".csv")) { @@ -208,31 +240,56 @@ public class PathCheckPaneController implements Initializable { // 保存生成的文件路径 physicalAddressFilePath = finalOutputFile.getAbsolutePath(); - // 创建后台任务来处理文件生成 - Thread backgroundThread = new Thread(() -> { - // 获取当前选择的文件夹类型 - Mode selectedMode = loadFolderType2CB.getValue(); - int folderType = (selectedMode == Mode.PAGE_TYPE) ? AddressFileGenerator.PAGE_TYPE : AddressFileGenerator.FILE_TYPE; - - paGenerator.generateAddressFile(folderPath, finalOutputFile, folderType, new AddressFileGenerator.Callback() { - @Override - public void onProgress(String message) { - javafx.application.Platform.runLater(() -> result2TA.setText(message)); + // + Mode selectedMode = loadFolderType2CB.getValue(); + // 创建后台任务 + AddressFileGenerationTask task = new AddressFileGenerationTask(folderPath, outputFile, selectedMode.number, false); + // 保存到字段 + currentTask = task; + // 绑定任务属性到UI + ChangeListener progressChangeListener = (obs, oldVal, newVal) -> { + if (newVal != null) { + if (task.getMessage() != null) { + progressBar.updateProgress(newVal.doubleValue(), task.getMessage()); } + } + }; + task.progressProperty().addListener(progressChangeListener); - @Override - public void onSuccess(String outputPath) { - javafx.application.Platform.runLater(() -> result2TA.setText(outputPath)); - } - - @Override - public void onError(String errorMessage) { - javafx.application.Platform.runLater(() -> result2TA.setText(errorMessage)); - } - }); + // 绑定任务的消息到结果文本区域 + ChangeListener messageChangeListener = (observable, oldValue, newValue) -> { + result2TA.setText(newValue); + }; + task.messageProperty().addListener(messageChangeListener); + // 当任务完成时显示完整结果 + task.setOnSucceeded(e -> { + progressBar.closeProgress(); + result2TA.setText(task.getValue()); + generatePhysicalAddress2B.setDisable(false); + log.info(LoggerMarker.RELEASE_MARKER, "生成物理路径 csv 文件任务完成,输出csv文件路径:{}", task.getValue()); }); - backgroundThread.start(); + // 处理任务失败情况 + task.setOnFailed(e -> { + progressBar.closeProgress(); + Throwable exception = task.getException(); + result2TA.setText("检测过程中发生错误: " + exception.getMessage()); + generatePhysicalAddress2B.setDisable(false); + log.error(LoggerMarker.RELEASE_MARKER, "生成物理路径 csv 文件任务失败", exception); + }); + + // 处理任务取消情况 + task.setOnCancelled(e -> { + progressBar.closeProgress(); + result2TA.appendText("\n检测已取消"); + generatePhysicalAddress2B.setDisable(false); + currentTask.progressProperty().removeListener(progressChangeListener); + currentTask.messageProperty().removeListener(messageChangeListener); + log.info(LoggerMarker.RELEASE_MARKER, "生成物理路径 csv 文件任务已被取消"); + }); + Thread thread = new Thread(task); + thread.setDaemon(true); + thread.start(); } @@ -245,76 +302,88 @@ public class PathCheckPaneController implements Initializable { */ @FXML void onStart(ActionEvent actionEvent) { - log.info(LoggerHelper.DEBUG_MARKER, "用户点击了开始对比按钮"); + log.info(LoggerMarker.DEBUG_MARKER, "用户点击了开始对比按钮"); - // 检查是否已生成两个文件 if (logicalAddressFilePath == null || physicalAddressFilePath == null) { result2TA.setText("请先生成逻辑地址文件和物理地址文件。"); return; } + start2B.setDisable(true); - log.info(LoggerHelper.DEBUG_MARKER, "逻辑地址文件路径为:{}", logicalAddressFilePath); - log.info(LoggerHelper.DEBUG_MARKER, "物理地址文件路径为:{}", physicalAddressFilePath); - - // 使用新创建的核心类进行文件比较 - AddressFileComparator comparator = new AddressFileComparator(); - AddressFileComparator.ComparisonResult result = comparator.compareFiles(physicalAddressFilePath, logicalAddressFilePath); - - // 显示比对结果 - StringBuilder resultText = new StringBuilder(); - - // 显示读取的行数 - resultText.append("读取物理地址文件记录数: ").append(result.getPhysicalRecordsCount()).append("\n"); - resultText.append("读取逻辑地址文件记录数: ").append(result.getLogicalRecordsCount()).append("\n\n"); - - // 显示路径不一致的结果 - if (!result.getPathMismatchResults().isEmpty()) { - resultText.append("文件名相同但路径不一致的记录数量: ").append(result.getPathMismatchResults().size()).append("\n"); - for (String mismatch : result.getPathMismatchResults()) { - resultText.append("\t").append(mismatch).append("\n"); + // 显示进度条窗口 + cancelableProgressBar.showProgress(SceneManager.getPrimaryStage(), "文件查漏检查", "正在初始化..."); + // 创建后台任务 + AddressFileComparisonTask task = getAddressFileComparisonTask(); + // 保存到字段 + this.currentTask = task; + // 绑定任务属性到UI + ChangeListener progressChangeListener = (obs, oldVal, newVal) -> { + if (newVal != null) { + if (task.getMessage() != null) { + cancelableProgressBar.updateProgress(newVal.doubleValue(), task.getMessage()); + } } - resultText.append("\n"); - } else { - resultText.append("没有路径错误\n\n"); - } + }; + task.progressProperty().addListener(progressChangeListener); - // 显示物理文件在逻辑文件中未找到的结果 - if (!result.getForwardComparisonResults().isEmpty()) { - resultText.append("物理文件在逻辑文件中未找到的记录数量: ").append(result.getForwardComparisonResults().size()).append("\n"); - for (String forward : result.getForwardComparisonResults()) { - resultText.append("\t").append(forward).append("\n"); + // 绑定任务的消息到结果文本区域 + ChangeListener messageChangeListener = (observable, oldValue, newValue) -> { + result2TA.setText(newValue); + }; + task.messageProperty().addListener(messageChangeListener); + + // 绑定取消按钮 -> task.cancel() + cancelableProgressBar.setOnCancel(() -> { + if (currentTask != null && currentTask.isRunning()) { + currentTask.cancel(); } - resultText.append("\n"); - } else { - resultText.append("没有物理存在而逻辑不存在的文件\n\n"); - } + }); - // 显示逻辑文件在物理文件中未找到的结果 - if (!result.getBackwardComparisonResults().isEmpty()) { - resultText.append("逻辑文件在物理文件中未找到的记录数量: ").append(result.getBackwardComparisonResults().size()).append("\n"); - for (String backward : result.getBackwardComparisonResults()) { - resultText.append("\t").append(backward).append("\n"); - } - } else { - resultText.append("没有逻辑存在而物理不存在的文件\n"); - } + task.setOnSucceeded(event -> { + cancelableProgressBar.closeProgress(); + start2B.setDisable(false); + result2TA.setText(AddressFileComparator.ComparisonResult.generateComparisonResults(task.getValue(), loadFolderType2CB.getValue().compareMode)); + result2TA.setText(AddressFileComparator.ComparisonResult.generateComparisonResults(task.getValue(), loadFolderType2CB.getValue().compareMode)); + //内部比较器已有,此处忽略日志打印 + }); - // 如果所有结果都为空,则显示一致信息 - if (result.getPathMismatchResults().isEmpty() && - result.getForwardComparisonResults().isEmpty() && - result.getBackwardComparisonResults().isEmpty()) { - resultText = new StringBuilder("所有文件比对一致,无差异。\n"); - resultText.append("读取物理地址文件记录数: ").append(result.getPhysicalRecordsCount()).append("\n"); - resultText.append("读取逻辑地址文件记录数: ").append(result.getLogicalRecordsCount()).append("\n"); - } + task.setOnFailed(event -> { + cancelableProgressBar.closeProgress(); + Throwable exception = task.getException(); + result2TA.setText("文件比对失败: " + task.getException().getMessage()); + start2B.setDisable(false); + log.error(LoggerMarker.RELEASE_MARKER, "查漏任务失败", exception); + }); - result2TA.setText(resultText.toString()); + task.setOnCancelled(event -> { + cancelableProgressBar.closeProgress(); + result2TA.appendText("\n检测已取消"); + start2B.setDisable(false); + currentTask.progressProperty().removeListener(progressChangeListener); + currentTask.messageProperty().removeListener(messageChangeListener); + log.info(LoggerMarker.RELEASE_MARKER, "查漏任务取消"); + }); + + // 异步执行任务 + Thread thread = new Thread(task); + thread.setDaemon(true); + thread.start(); } + private @NotNull AddressFileComparisonTask getAddressFileComparisonTask() { + Mode selectedMode = loadFolderType2CB.getValue(); + AddressFileComparator.CompareMode compareMode = (selectedMode == Mode.PAGE_TYPE) ? + AddressFileComparator.CompareMode.PAGE_LEVEL : + AddressFileComparator.CompareMode.FILE_LEVEL; - - - + // 创建异步任务 + return new AddressFileComparisonTask( + physicalAddressFilePath, + logicalAddressFilePath, + compareMode, + System.getSetting().getTaskTimeout() + ); + } @Override @@ -330,7 +399,7 @@ public class PathCheckPaneController implements Initializable { /** * Jpg mode. 文件以JPG */ - PAGE_TYPE("jpg") { + PAGE_TYPE("jpg", AddressFileGenerator.PAGE_TYPE, AddressFileComparator.CompareMode.PAGE_LEVEL) { @Override public String toString() { return "页面级"; @@ -339,7 +408,7 @@ public class PathCheckPaneController implements Initializable { /** * Pdf mode. 文件以PDF */ - FILE_TYPE("pdf") { + FILE_TYPE("pdf", AddressFileGenerator.FILE_TYPE, AddressFileComparator.CompareMode.FILE_LEVEL) { @Override public String toString() { return "文件级"; @@ -348,10 +417,14 @@ public class PathCheckPaneController implements Initializable { /** * The Id. */ - final String id; + public final String id; + public final int number; + public final AddressFileComparator.CompareMode compareMode; - Mode(String id) { + Mode(String id, int number, AddressFileComparator.CompareMode compareMode) { this.id = id; + this.number = number; + this.compareMode = compareMode; } } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java index 490fa38..12216ad 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java @@ -13,7 +13,7 @@ import lombok.extern.slf4j.Slf4j; import top.r3944realms.docchecktoolrefactored.System; import top.r3944realms.docchecktoolrefactored.core.HashFileGenerator; import top.r3944realms.docchecktoolrefactored.core.MD5HashCalculator; -import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.File; import java.io.IOException; @@ -53,7 +53,7 @@ public class StorageCarrierPaneController { @FXML void onSelectLD(ActionEvent event) { - log.info(LoggerHelper.DEBUG_MARKER, "用户点击选择文件夹按钮"); + log.info(LoggerMarker.DEBUG_MARKER, "用户点击选择文件夹按钮"); DirectoryChooser directoryChooser = System.getDirectoryChooser(); directoryChooser.setTitle("选择要检查的文件夹(页面级文件夹和文件级文件夹等不包括目录文件夹)"); @@ -81,18 +81,18 @@ public class StorageCarrierPaneController { loadDigitalOutcomes.setText(currentText + File.pathSeparator + folderPath); } } - log.info(LoggerHelper.DEBUG_MARKER, "用户选择了文件夹: {}", selectedFolder.getAbsolutePath()); + log.info(LoggerMarker.DEBUG_MARKER, "用户选择了文件夹: {}", selectedFolder.getAbsolutePath()); } else { - log.info(LoggerHelper.DEBUG_MARKER, "用户取消了文件夹选择"); + log.info(LoggerMarker.DEBUG_MARKER, "用户取消了文件夹选择"); } } @FXML void onClearSelectedFolders(ActionEvent event) { - log.info(LoggerHelper.DEBUG_MARKER, "用户点击清除已选择文件夹按钮"); + log.info(LoggerMarker.DEBUG_MARKER, "用户点击清除已选择文件夹按钮"); loadDigitalOutcomes.setText(""); result7TA.setText("已清除所有已选择的文件夹"); - log.info(LoggerHelper.DEBUG_MARKER, "已清除所有已选择的文件夹"); + log.info(LoggerMarker.DEBUG_MARKER, "已清除所有已选择的文件夹"); } @@ -100,7 +100,7 @@ public class StorageCarrierPaneController { @FXML void onSelectLC(ActionEvent event) { - log.info(LoggerHelper.DEBUG_MARKER, "用户点击选择RAR文件按钮"); + log.info(LoggerMarker.DEBUG_MARKER, "用户点击选择RAR文件按钮"); FileChooser fileChooser = System.getFileChooser(); fileChooser.setTitle("选择一个 .rar 文件"); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("RAR Files", "*.rar")); @@ -108,48 +108,48 @@ public class StorageCarrierPaneController { if (selectedFile != null) { System.setLastModifiedFile(selectedFile); loadCompressedFile.setText(selectedFile.getAbsolutePath()); - log.info(LoggerHelper.DEBUG_MARKER, "用户选择了RAR文件: {}", selectedFile.getAbsolutePath()); + log.info(LoggerMarker.DEBUG_MARKER, "用户选择了RAR文件: {}", selectedFile.getAbsolutePath()); } else { - log.info(LoggerHelper.DEBUG_MARKER, "用户取消了RAR文件选择"); + log.info(LoggerMarker.DEBUG_MARKER, "用户取消了RAR文件选择"); } } @FXML void onCaculateHash(ActionEvent event) { - log.info(LoggerHelper.DEBUG_MARKER, "开始计算RAR文件的MD5哈希值"); + log.info(LoggerMarker.DEBUG_MARKER, "开始计算RAR文件的MD5哈希值"); String filePath = loadCompressedFile.getText(); if (filePath == null || filePath.isEmpty()) { - log.warn(LoggerHelper.DEBUG_MARKER, "未选择RAR文件,无法计算哈希值"); + log.warn(LoggerMarker.DEBUG_MARKER, "未选择RAR文件,无法计算哈希值"); result7TA.setText("请先选择一个 .rar 文件"); return; } File file = new File(filePath); if (!file.exists() || !file.isFile() || !filePath.endsWith(".rar")) { - log.warn(LoggerHelper.DEBUG_MARKER, "选择的文件无效或不是RAR文件: {}", filePath); + log.warn(LoggerMarker.DEBUG_MARKER, "选择的文件无效或不是RAR文件: {}", filePath); result7TA.setText("所选文件不存在或不是一个有效的 .rar 文件"); return; } try { - log.info(LoggerHelper.DEBUG_MARKER, "开始计算文件哈希值: {}", filePath); + log.info(LoggerMarker.DEBUG_MARKER, "开始计算文件哈希值: {}", filePath); MD5HashCalculator hashCalculator = new MD5HashCalculator(); String hashResult = hashCalculator.calculateHash(file.toPath()); result7TA.setText("计算结果:\n" + hashResult); - log.info(LoggerHelper.DEBUG_MARKER, "文件哈希值计算完成: {}", hashResult); + log.info(LoggerMarker.DEBUG_MARKER, "文件哈希值计算完成: {}", hashResult); } catch (IOException e) { - log.error(LoggerHelper.DEBUG_MARKER, "计算文件哈希值时出错: {}", filePath, e); + log.error(LoggerMarker.DEBUG_MARKER, "计算文件哈希值时出错: {}", filePath, e); result7TA.setText("计算哈希值时出错: " + e.getMessage()); } } @FXML void onGenerateHF(ActionEvent event) { - log.info(LoggerHelper.DEBUG_MARKER, "开始生成哈希列表文件"); + log.info(LoggerMarker.DEBUG_MARKER, "开始生成哈希列表文件"); String folderPathsText = loadDigitalOutcomes.getText(); if (folderPathsText == null || folderPathsText.isEmpty()) { - log.warn(LoggerHelper.DEBUG_MARKER, "未选择文件夹,无法生成哈希列表文件"); + log.warn(LoggerMarker.DEBUG_MARKER, "未选择文件夹,无法生成哈希列表文件"); result7TA.setText("请先选择一个文件夹"); return; } @@ -163,7 +163,7 @@ public class StorageCarrierPaneController { if (folder.exists() && folder.isDirectory()) { folders.add(folder); } else { - log.warn(LoggerHelper.DEBUG_MARKER, "选择的路径无效或不是文件夹: {}", path); + log.warn(LoggerMarker.DEBUG_MARKER, "选择的路径无效或不是文件夹: {}", path); result7TA.setText("所选路径不存在或不是一个有效的文件夹: " + path); return; } @@ -180,7 +180,7 @@ public class StorageCarrierPaneController { File outputFile = fileChooser.showSaveDialog(selectLoadDigitalOutcomes7B.getScene().getWindow()); if (outputFile == null) { - log.info(LoggerHelper.DEBUG_MARKER, "用户取消了文件保存操作"); + log.info(LoggerMarker.DEBUG_MARKER, "用户取消了文件保存操作"); result7TA.setText("未选择保存位置"); return; } @@ -193,13 +193,13 @@ public class StorageCarrierPaneController { finalOutputFile = outputFile; } - log.info(LoggerHelper.DEBUG_MARKER, "选择的输出文件路径: {}", finalOutputFile.getAbsolutePath()); + log.info(LoggerMarker.DEBUG_MARKER, "选择的输出文件路径: {}", finalOutputFile.getAbsolutePath()); // 创建后台任务 Task task = new Task<>() { @Override protected String call() throws Exception { - log.info(LoggerHelper.DEBUG_MARKER, "开始执行哈希文件生成任务"); + log.info(LoggerMarker.DEBUG_MARKER, "开始执行哈希文件生成任务"); updateMessage("开始生成哈希文件..."); HashFileGenerator generator = new HashFileGenerator(); @@ -213,11 +213,11 @@ public class StorageCarrierPaneController { updateProgress(current, total); updateMessage("处理文件: " + current + "/" + total); if (current % 500 == 0 || current == total) { // 每500个文件或完成时记录一次日志 - log.info(LoggerHelper.DEBUG_MARKER, "处理进度: {}/{}", current, total); + log.info(LoggerMarker.DEBUG_MARKER, "处理进度: {}/{}", current, total); } }); - log.info(LoggerHelper.DEBUG_MARKER, "哈希文件生成任务完成,输出文件: {}", finalOutputFile.getAbsolutePath()); + log.info(LoggerMarker.DEBUG_MARKER, "哈希文件生成任务完成,输出文件: {}", finalOutputFile.getAbsolutePath()); return "哈希列表文件已生成: " + finalOutputFile.getAbsolutePath(); } }; @@ -229,7 +229,7 @@ public class StorageCarrierPaneController { // 任务成功完成 task.setOnSucceeded(e -> { - log.info(LoggerHelper.DEBUG_MARKER, "哈希文件生成任务成功完成"); + log.info(LoggerMarker.DEBUG_MARKER, "哈希文件生成任务成功完成"); result7TA.setText(task.getValue()); }); @@ -237,13 +237,13 @@ public class StorageCarrierPaneController { task.setOnFailed(e -> { Throwable exception = task.getException(); String errorMsg = "生成哈希文件时出错: " + (exception != null ? exception.getMessage() : "未知错误"); - log.error(LoggerHelper.RELEASE_MARKER, "哈希文件生成任务失败", exception); + log.error(LoggerMarker.RELEASE_MARKER, "哈希文件生成任务失败", exception); result7TA.setText(errorMsg); }); // 任务取消处理 task.setOnCancelled(e -> { - log.info(LoggerHelper.DEBUG_MARKER, "哈希文件生成任务被用户取消"); + log.info(LoggerMarker.DEBUG_MARKER, "哈希文件生成任务被用户取消"); result7TA.setText("哈希文件生成操作已取消"); }); diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/AddressFileComparisonTask.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/AddressFileComparisonTask.java new file mode 100644 index 0000000..fa2c2e6 --- /dev/null +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/AddressFileComparisonTask.java @@ -0,0 +1,91 @@ +package top.r3944realms.docchecktoolrefactored.ui.task; + +import javafx.concurrent.Task; +import lombok.extern.slf4j.Slf4j; +import top.r3944realms.docchecktoolrefactored.core.AddressFileComparator; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +@Slf4j +public class AddressFileComparisonTask extends Task { + + private final String physicalFilePath; + private final String logicalFilePath; + private final AddressFileComparator.CompareMode compareMode; + private final AddressFileComparator comparator; + private final long timeoutSeconds; // 超时秒数 + + public AddressFileComparisonTask(String physicalFilePath, + String logicalFilePath, + AddressFileComparator.CompareMode compareMode, + long timeoutSeconds) { + this.physicalFilePath = physicalFilePath; + this.logicalFilePath = logicalFilePath; + this.compareMode = compareMode; + this.timeoutSeconds = timeoutSeconds; + comparator = new AddressFileComparator(); + } + + @Override + protected AddressFileComparator.ComparisonResult call() throws Exception { + updateMessage("初始化文件比较..."); + comparator.setProgressCallback(new AddressFileComparator.ProgressCallback() { + @Override + public void onPhaseStarted(AddressFileComparator.Phase phase) { + // 阶段开始提示 + switch (phase) { + case READ_PHYSICAL_CSV -> updateMessage("正在读取物理地址 CSV 文件..."); + case READ_LOGICAL_CSV -> updateMessage("正在读取逻辑地址 CSV 文件..."); + case COMPARE_FORWARD -> updateMessage("正在执行正向比较(物理 → 逻辑)..."); + case COMPARE_BACKWARD -> updateMessage("正在执行反向比较(逻辑 → 物理)..."); + } + } + + @Override + public void onPhaseProgress(AddressFileComparator.Phase phase, int current, int total) { + if (total <= 0) return; + + updateProgress(current, total); + + // 阶段进度提示 + switch (phase) { + case READ_PHYSICAL_CSV -> updateMessage(String.format("读取物理地址 CSV: %d/%d", current, total)); + case READ_LOGICAL_CSV -> updateMessage(String.format("读取逻辑地址 CSV: %d/%d", current, total)); + case COMPARE_FORWARD -> updateMessage(String.format("正向比较: %d/%d", current, total)); + case COMPARE_BACKWARD -> updateMessage(String.format("反向比较: %d/%d", current, total)); + } + } + + @Override + public void onPhaseCompleted(AddressFileComparator.Phase phase) { + // 阶段完成提示 + switch (phase) { + case READ_PHYSICAL_CSV -> updateMessage("物理地址 CSV 文件读取完成"); + case READ_LOGICAL_CSV -> updateMessage("逻辑地址 CSV 文件读取完成"); + case COMPARE_FORWARD -> updateMessage("正向比较完成"); + case COMPARE_BACKWARD -> updateMessage("反向比较完成"); + } + } + }); + + try { + // 构建显示文本 + return comparator.compareFiles(physicalFilePath, logicalFilePath, compareMode) + .get(timeoutSeconds, TimeUnit.SECONDS); + + } catch (TimeoutException e) { + updateMessage("文件比对超时,请检查文件大小或电脑性能。"); + log.error("文件比对超时", e); + throw e; + } finally { + comparator.shutdown(); + } + } + + + @Override + protected void cancelled() { + comparator.shutdown(); + } +} diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/AddressFileGenerationTask.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/AddressFileGenerationTask.java new file mode 100644 index 0000000..c9a22d7 --- /dev/null +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/AddressFileGenerationTask.java @@ -0,0 +1,90 @@ +package top.r3944realms.docchecktoolrefactored.ui.task; + +import javafx.concurrent.Task; +import lombok.extern.slf4j.Slf4j; +import top.r3944realms.docchecktoolrefactored.System; +import top.r3944realms.docchecktoolrefactored.core.AddressFileGenerator; +import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator; +import top.r3944realms.docchecktoolrefactored.core.PhysicalAddressFileGenerator; + +import java.io.File; +import java.util.concurrent.*; + +import static java.util.concurrent.Executors.*; + +@Slf4j +public class AddressFileGenerationTask extends Task { + + private final String sourcePath; + private final File outputFile; + private final int folderType; + private final AddressFileGenerator generator; + + public AddressFileGenerationTask(String sourcePath, + File outputFile, + int folderType, + boolean isLogic) { + this.sourcePath = sourcePath; + this.outputFile = outputFile; + this.folderType = folderType; + this.generator = isLogic ? new LogicalAddressFileGenerator() : new PhysicalAddressFileGenerator(); + } + + @Override + protected String call() { + updateMessage("初始化生成任务..."); + generator.setProgressCallback(new AddressFileGenerator.ProgressCallback() { + @Override + public void onPhaseStarted(AddressFileGenerator.Phase phase) { + switch (phase) { + case GENERATE_LOGICAL -> updateMessage("正在生成逻辑地址 CSV 文件 ..."); + case GENERATE_PHYSICAL -> updateMessage("正在生成物理地址 CSV 文件 ..."); + } + } + + @Override + public void onPhaseProgress(AddressFileGenerator.Phase phase, int current, int total) { + if (total > 0) { + updateProgress(current, total); + switch (phase) { + case GENERATE_LOGICAL -> updateMessage(String.format("在生成逻辑地址 CSV : %d/%d", current, total)); + case GENERATE_PHYSICAL -> updateMessage(String.format("在生成物理地址 CSV : %d/%d", current, total)); + } + } + } + + @Override + public void onPhaseCompleted(AddressFileGenerator.Phase phase) { + switch (phase) { + case GENERATE_LOGICAL -> updateMessage("已完成生成逻辑地址 CSV 文件任务"); + case GENERATE_PHYSICAL -> updateMessage("已完成生成物理地址 CSV 文件任务"); + } + } + } + ); + ExecutorService executor = newSingleThreadExecutor(); + Future future = executor.submit(() -> { + generator.generateAddressFile(sourcePath, outputFile, folderType); + }); + + try { + // 等待执行完成或超时 + future.get(System.getSetting().getTaskTimeout(), TimeUnit.SECONDS); + } catch (TimeoutException e) { + future.cancel(true); // 尝试中断 + this.cancel(); // 取消 Task + throw new RuntimeException("生成任务超时 (>" + System.getSetting().getTaskTimeout() + "s)", e); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException("生成任务失败", e.getCause()); + } finally { + executor.shutdownNow(); + } + + return outputFile.getAbsolutePath(); + } + + @Override + protected void cancelled() { + log.info("生成任务已取消: {}", outputFile.getAbsolutePath()); + } +} diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java index df2fd34..ef40b69 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java @@ -2,6 +2,7 @@ package top.r3944realms.docchecktoolrefactored.ui.task; import javafx.concurrent.Task; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import top.r3944realms.docchecktoolrefactored.System; import top.r3944realms.docchecktoolrefactored.core.DuplicateFinder; import top.r3944realms.docchecktoolrefactored.core.MD5HashCalculator; @@ -111,9 +112,9 @@ public class DuplicateDocumentDetectionTask extends Task{ findThread.start(); // 等待扫描完成,设置超时时间(例如5分钟) - long totalTimeout = System.getSetting().getTotalTimeout(); + long totalTimeout = System.getSetting().getTaskTimeout(); if (!latch.await(totalTimeout, TimeUnit.SECONDS)) { - scanner.cancel(); + duplicateFinder.shutdown(); throw new TimeoutException(String.format("扫描超时(%d秒)", totalTimeout)); } @@ -127,18 +128,18 @@ public class DuplicateDocumentDetectionTask extends Task{ // 检查是否被取消 if (isCancelled()) { - scanner.cancel(); + duplicateFinder.shutdown(); return "操作已被取消"; } // 检查是否超时 if (java.lang.System.currentTimeMillis() - start > totalTimeout * 1000L) { - scanner.cancel(); + duplicateFinder.shutdown(); throw new TimeoutException(String.format("扫描超时(%d秒)", totalTimeout)); } } } catch (InterruptedException e) { - scanner.cancel(); + duplicateFinder.shutdown(); Thread.currentThread().interrupt(); return "操作被中断"; } @@ -149,8 +150,11 @@ public class DuplicateDocumentDetectionTask extends Task{ } List duplicateGroups = resultRef.get(); - // 构建最终结果 + return generateResult(duplicateGroups, totalFiles); + } + + private static @NotNull String generateResult(List duplicateGroups, AtomicInteger totalFiles) { StringBuilder result = new StringBuilder(); diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java index 5125a37..430cf8a 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java @@ -21,6 +21,15 @@ public class DialogUtil { public static boolean showExitConfirmation(Window owner) { return showConfirmationDialog("确认退出", "您确定要退出程序吗?", "请确认您的操作"); } + /** + * Show exit confirmation boolean. + * + * @param owner the owner + * @return the boolean + */ + public static boolean showCancelConfirmation(Window owner) { + return showConfirmationDialog("确认取消", "您确定要取消该任务吗?", "请确认您的操作"); + } /** * Show confirmation dialog boolean. diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/ProgressBarUtil.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/ProgressBar.java similarity index 56% rename from src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/ProgressBarUtil.java rename to src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/ProgressBar.java index b88a1ac..790308e 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/ProgressBarUtil.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/ProgressBar.java @@ -4,7 +4,6 @@ import javafx.application.Platform; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; -import javafx.scene.control.ProgressBar; import javafx.scene.layout.VBox; import javafx.stage.Modality; import javafx.stage.Stage; @@ -15,14 +14,23 @@ import java.util.concurrent.atomic.AtomicBoolean; /** * 进度条窗口工具类,支持取消按钮 */ -public class ProgressBarUtil { +public class ProgressBar { private Stage progressStage; - private ProgressBar progressBar; + private javafx.scene.control.ProgressBar progressBar; private Label messageLabel; private Button cancelButton; + private final boolean isCancelable; private final AtomicBoolean cancelled = new AtomicBoolean(false); private Runnable onCancelCallback; + + public ProgressBar(boolean isCancelable) { + this.isCancelable = isCancelable; + } + public ProgressBar() { + this(true); + } + /** * 显示进度条窗口 * @param ownerStage 父窗口 @@ -34,41 +42,57 @@ public class ProgressBarUtil { progressStage = new Stage(); progressStage.initOwner(ownerStage); progressStage.initStyle(StageStyle.UTILITY); + if (isCancelable) { + progressStage.setOnCloseRequest(windowEvent -> { + if (!DialogUtil.showCancelConfirmation(progressStage.getOwner())) { + windowEvent.consume(); + } + else { + cancelled.set(true); + if (onCancelCallback != null) { + onCancelCallback.run(); + } + closeProgress(); + } + }); + } progressStage.initModality(Modality.APPLICATION_MODAL); progressStage.setTitle(title); progressStage.setResizable(false); // 创建进度条 - progressBar = new ProgressBar(); + progressBar = new javafx.scene.control.ProgressBar(); progressBar.setPrefWidth(300); - progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS); + progressBar.setProgress(javafx.scene.control.ProgressBar.INDETERMINATE_PROGRESS); // 创建消息标签 messageLabel = new Label(initialMessage); - // 创建取消按钮 - cancelButton = new Button("取消"); - cancelButton.setOnAction(e -> { - cancelled.set(true); - if (onCancelCallback != null) { - onCancelCallback.run(); - } - closeProgress(); - }); - - // 如果 setOnCancel 先调用过,绑定回调 - if (onCancelCallback != null) { + if (isCancelable) { + // 创建取消按钮 + cancelButton = new Button("取消"); cancelButton.setOnAction(e -> { cancelled.set(true); - onCancelCallback.run(); + if (onCancelCallback != null) { + onCancelCallback.run(); + } closeProgress(); }); + + // 如果 setOnCancel 先调用过,绑定回调 + if (onCancelCallback != null) { + cancelButton.setOnAction(e -> { + cancelled.set(true); + onCancelCallback.run(); + closeProgress(); + }); + } } - // 布局 - VBox root = new VBox(10, messageLabel, progressBar, cancelButton); - root.setStyle("-fx-padding: 20; -fx-alignment: center;"); + VBox root = new VBox(10, messageLabel, progressBar); + if (isCancelable) root.getChildren().add(cancelButton); + root.setStyle("-fx-padding: 20; -fx-alignment: center;"); Scene scene = new Scene(root); progressStage.setScene(scene); progressStage.sizeToScene(); @@ -118,33 +142,35 @@ public class ProgressBarUtil { * @param task 要执行的任务(可检查 isCancelled() 中途退出) */ public static void showAndExecute(Stage ownerStage, String title, String initialMessage, CancellableTask task) { - ProgressBarUtil progressBarUtil = new ProgressBarUtil(); - progressBarUtil.showProgress(ownerStage, title, initialMessage); + ProgressBar progressBar = new ProgressBar(); + progressBar.showProgress(ownerStage, title, initialMessage); new Thread(() -> { try { - task.run(progressBarUtil); + task.run(progressBar); } finally { - progressBarUtil.closeProgress(); + progressBar.closeProgress(); } }).start(); } @FunctionalInterface public interface CancellableTask { - void run(ProgressBarUtil util); + void run(ProgressBar util); } public void setOnCancel(Runnable onCancel) { - if (cancelButton != null) { - cancelButton.setOnAction(e -> { - cancelled.set(true); - if (onCancel != null) { - onCancel.run(); - } - closeProgress(); - }); - } else { - // 保存回调,稍后在按钮创建后绑定 - this.onCancelCallback = onCancel; - } + if (isCancelable) { + if (cancelButton != null) { + cancelButton.setOnAction(e -> { + cancelled.set(true); + if (onCancel != null) { + onCancel.run(); + } + closeProgress(); + }); + } else { + // 保存回调,稍后在按钮创建后绑定 + this.onCancelCallback = onCancel; + } + } else throw new UnsupportedOperationException("Cancellable task is not supported"); } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/util/FileUtil.java b/src/main/java/top/r3944realms/docchecktoolrefactored/util/FileUtil.java index 9ad95cb..c311110 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/util/FileUtil.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/util/FileUtil.java @@ -30,7 +30,7 @@ public class FileUtil { Path path = Paths.get(filePath).normalize(); File file = path.toFile(); if (!file.exists()) { - log.error(LoggerHelper.TRACE_MARKER, "文件不存在: {}", filePath); + log.error(LoggerMarker.TRACE_MARKER, "文件不存在: {}", filePath); throw new NoSuchFileException("文件不存在: " + filePath); } @@ -68,7 +68,7 @@ public class FileUtil { File file = path.toFile(); if (!file.exists()) { - log.error(LoggerHelper.TRACE_MARKER, "文件不存在: {}", filePath); + log.error(LoggerMarker.TRACE_MARKER, "文件不存在: {}", filePath); throw new NoSuchFileException("文件不存在: " + filePath); } @@ -88,7 +88,7 @@ public class FileUtil { .toList(); if (!supportedExtensions.contains(fileExtension)) { - log.error(LoggerHelper.TRACE_MARKER, "不支持的文件格式: {}", fileExtension); + log.error(LoggerMarker.TRACE_MARKER, "不支持的文件格式: {}", fileExtension); throw new IllegalArgumentException("不支持的文件格式,预期: " + supportedExtensions + ",实际: " + fileExtension); } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/util/LoggerHelper.java b/src/main/java/top/r3944realms/docchecktoolrefactored/util/LoggerMarker.java similarity index 92% rename from src/main/java/top/r3944realms/docchecktoolrefactored/util/LoggerHelper.java rename to src/main/java/top/r3944realms/docchecktoolrefactored/util/LoggerMarker.java index 09d7345..eb2e260 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/util/LoggerHelper.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/util/LoggerMarker.java @@ -3,7 +3,7 @@ package top.r3944realms.docchecktoolrefactored.util; import org.slf4j.Marker; import org.slf4j.MarkerFactory; -public class LoggerHelper { +public class LoggerMarker { public static final Marker TRACE_MARKER = MarkerFactory.getMarker("TRACE"); public static final Marker DEBUG_MARKER = MarkerFactory.getMarker("DEBUG"); public static final Marker RELEASE_MARKER = MarkerFactory.getMarker("RELEASE"); diff --git a/src/main/resources/fxml/module/step-1-pane.fxml b/src/main/resources/fxml/module/step-1-pane.fxml index 93a01bf..5796261 100644 --- a/src/main/resources/fxml/module/step-1-pane.fxml +++ b/src/main/resources/fxml/module/step-1-pane.fxml @@ -25,7 +25,7 @@ - diff --git a/src/main/resources/fxml/module/step-5-pane.fxml b/src/main/resources/fxml/module/step-5-pane.fxml index eb3fa6d..fb8bfe9 100644 --- a/src/main/resources/fxml/module/step-5-pane.fxml +++ b/src/main/resources/fxml/module/step-5-pane.fxml @@ -10,7 +10,7 @@ - diff --git a/src/main/resources/fxml/module/step-7-pane.fxml b/src/main/resources/fxml/module/step-7-pane.fxml index 2ef8926..735d492 100644 --- a/src/main/resources/fxml/module/step-7-pane.fxml +++ b/src/main/resources/fxml/module/step-7-pane.fxml @@ -124,7 +124,7 @@ -