From 1bf1579e437dea090dfaff2c026d5755a1fa2cd3 Mon Sep 17 00:00:00 2001 From: 3944Realms Date: Wed, 20 Aug 2025 21:45:05 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E7=95=8C=E9=9D=A2=E5=8F=98?= =?UTF-8?q?=E5=8A=A8=EF=BC=8C=E9=80=BB=E8=BE=91=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 15 +- doc/request.md | 4 + .../JavaFxApplication.java | 4 + .../docchecktoolrefactored/Main.java | 4 +- .../docchecktoolrefactored/System.java | 188 +++++++++++++++ .../cil/CliProcessor.java | 3 +- .../core/AddressFileComparator.java | 55 +++-- .../core/AddressFileGenerator.java | 4 +- .../core/DuplicateFinder.java | 147 +++++++++--- .../core/FileHashCalculator.java | 17 +- .../core/HashFileGenerator.java | 8 +- .../core/MD5HashCalculator.java | 25 ++ .../core/PhysicalAddressFileGenerator.java | 5 +- .../core/ScanningException.java | 10 + .../docchecktoolrefactored/core/Setting.java | 14 ++ .../io/reader/DbfFileReader.java | 40 ++-- .../io/reader/ExcelFileReader.java | 27 +-- .../io/reader/XmlFileReader.java | 27 +-- .../io/scanner/FileScanner.java | 17 ++ .../io/scanner/ParallelFileScanner.java | 135 ----------- .../io/scanner/RobustParallelScanner.java | 27 ++- .../ui/LoginStageController.java | 5 +- .../ui/MainStageController.java | 118 +++++++++- .../ui/SceneManager.java | 57 ++++- .../ui/SettingDialogController.java | 126 ++++++++++ .../ui/extend/LongSpinnerValueFactory.java | 48 ++++ .../DuplicateDocumentPaneController.java | 76 ++++-- .../ui/module/PathCheckPaneController.java | 57 +++-- .../ui/module/ProjectInfoPaneController.java | 15 +- .../module/StorageCarrierPaneController.java | 64 ++--- .../task/DuplicateDocumentDetectionTask.java | 220 ++++++++++-------- .../ui/utils/DialogUtil.java | 15 +- .../ui/utils/ProgressBarUtil.java | 150 ++++++++++++ .../docchecktoolrefactored/util/FileUtil.java | 6 +- .../util/LoggerHelper.java | 10 + src/main/resources/css/custom-tab.css | 4 +- src/main/resources/fxml/login-view.fxml | 2 +- src/main/resources/fxml/main-view.fxml | 63 ++++- .../resources/fxml/module/step-1-pane.fxml | 11 +- .../resources/fxml/module/step-2-pane.fxml | 2 +- .../resources/fxml/module/step-4-pane.fxml | 12 +- .../resources/fxml/module/step-5-pane.fxml | 4 +- .../resources/fxml/module/step-6-pane.fxml | 4 +- .../resources/fxml/module/step-7-pane.fxml | 29 ++- src/main/resources/fxml/setting-view.fxml | 83 +++++++ src/main/resources/img/icon.jpg | Bin 0 -> 81519 bytes 46 files changed, 1454 insertions(+), 503 deletions(-) create mode 100644 src/main/java/top/r3944realms/docchecktoolrefactored/System.java create mode 100644 src/main/java/top/r3944realms/docchecktoolrefactored/core/ScanningException.java create mode 100644 src/main/java/top/r3944realms/docchecktoolrefactored/core/Setting.java delete mode 100644 src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/ParallelFileScanner.java create mode 100644 src/main/java/top/r3944realms/docchecktoolrefactored/ui/SettingDialogController.java create mode 100644 src/main/java/top/r3944realms/docchecktoolrefactored/ui/extend/LongSpinnerValueFactory.java create mode 100644 src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/ProgressBarUtil.java create mode 100644 src/main/java/top/r3944realms/docchecktoolrefactored/util/LoggerHelper.java create mode 100644 src/main/resources/fxml/setting-view.fxml create mode 100644 src/main/resources/img/icon.jpg diff --git a/build.gradle b/build.gradle index f51564b..cc2a9ee 100644 --- a/build.gradle +++ b/build.gradle @@ -64,7 +64,6 @@ dependencies { // 你项目的其他依赖(老库走classpath) implementation 'ch.qos.logback:logback-classic:1.5.6' - implementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.20.0' implementation 'commons-cli:commons-cli:1.9.0' implementation 'com.alibaba:easyexcel:4.0.3' implementation 'org.apache.pdfbox:pdfbox:3.0.5' @@ -131,18 +130,22 @@ tasks.register('runCli', JavaExec) { } // 可选:创建生成可执行JAR的任务 -tasks.register('buildCliJar', Jar) { +tasks.register('buildFatJar', Jar) { group = 'build' - description = 'Builds a standalone JAR for CLI mode' + description = 'Builds a standalone JAR with all dependencies' manifest { - attributes 'Main-Class': 'top.r3944realms.docchecktoolrefactored.Main' + attributes( + 'Main-Class': 'top.r3944realms.docchecktoolrefactored.Main' + ) } from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } with jar + } + with jar // 继承主 jar 的内容 - archiveBaseName = 'doc-check-tool-cli' + archiveBaseName.set('doc-check-tool-cli') + archiveVersion.set('1.0') duplicatesStrategy = DuplicatesStrategy.EXCLUDE } diff --git a/doc/request.md b/doc/request.md index e69de29..5977e71 100644 --- a/doc/request.md +++ b/doc/request.md @@ -0,0 +1,4 @@ +# 改动 +1. 改名 +2. 界面颜色 +3. \ No newline at end of file diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java b/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java index 6b594bc..5e3bbfa 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java @@ -1,9 +1,12 @@ package top.r3944realms.docchecktoolrefactored; import javafx.application.Application; +import javafx.scene.image.Image; import javafx.stage.Stage; import top.r3944realms.docchecktoolrefactored.ui.SceneManager; +import java.util.Objects; + public class JavaFxApplication extends Application { @Override public void init() throws Exception { @@ -13,6 +16,7 @@ public class JavaFxApplication extends Application { @Override public void start(Stage primaryStage) throws Exception { SceneManager.init(primaryStage); + primaryStage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/img/icon.jpg")))); SceneManager.switchLoginView(); primaryStage.show(); } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/Main.java b/src/main/java/top/r3944realms/docchecktoolrefactored/Main.java index 707c20b..cc35809 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/Main.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/Main.java @@ -2,6 +2,7 @@ package top.r3944realms.docchecktoolrefactored; import lombok.extern.slf4j.Slf4j; import top.r3944realms.docchecktoolrefactored.cil.CliProcessor; +import top.r3944realms.docchecktoolrefactored.core.Setting; import java.util.ArrayList; import java.util.Arrays; @@ -12,14 +13,13 @@ import java.util.List; */ @Slf4j public class Main { - /** * The entry point of application. * * @param args the input arguments */ - @SuppressWarnings("DataFlowIssue") public static void main(String[] args) { + System.init(); // log.info(StringUtil.NO_BUG); // 检查是否有 --cli 参数 List list = Arrays.asList(args); diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/System.java b/src/main/java/top/r3944realms/docchecktoolrefactored/System.java new file mode 100644 index 0000000..1100ee7 --- /dev/null +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/System.java @@ -0,0 +1,188 @@ +package top.r3944realms.docchecktoolrefactored; + +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 java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.Properties; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +@Slf4j +public enum System { + INSTANCE; + private volatile Setting setting; + private volatile File lastModifiedFile; + private static final String CONFIG_FILE_NAME = "config.ini"; + private static final Properties properties = new Properties(); + private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + // 默认值 + private static final long DEFAULT_SINGLE_TIMEOUT = 30; + private static final long DEFAULT_TOTAL_TIMEOUT = 300; + private static final boolean DEFAULT_ENABLE_STEP = false; + + public static void init() { + loadSettings(); + Runtime.getRuntime().addShutdownHook(new Thread(System::saveSettings)); + } + + /** 加载配置文件 */ + private static void loadSettings() { + lock.writeLock().lock(); + try { + Path configPath = getConfigPath(); + if (Files.exists(configPath)) { + try (InputStream input = new FileInputStream(configPath.toFile())) { + properties.load(input); + log.debug(LoggerHelper.DEBUG_MARKER, "配置文件加载成功: {}", configPath); + INSTANCE.setting = propertiesToSetting(properties); + } catch (IOException e) { + log.error(LoggerHelper.DEBUG_MARKER, "读取配置文件失败: {}, 使用默认配置", e.getMessage()); + INSTANCE.setting = defaultSetting(); + settingToProperties(INSTANCE.setting, properties); + } + } else { + INSTANCE.setting = defaultSetting(); + settingToProperties(INSTANCE.setting, properties); + saveSettings(); // 首次启动保存默认配置 + log.debug(LoggerHelper.DEBUG_MARKER, "配置文件不存在,已创建默认配置: {}", configPath); + } + } finally { + lock.writeLock().unlock(); + } + } + + /** 保存配置文件 */ + private static void saveSettings() { + lock.readLock().lock(); + try { + if (INSTANCE.setting != null) { + Path configPath = getConfigPath(); + Path configDir = configPath.getParent(); + if (!Files.exists(configDir)) { + Files.createDirectories(configDir); + } + + settingToProperties(INSTANCE.setting, properties); + + try (OutputStream output = new FileOutputStream(configPath.toFile())) { + properties.store(new OutputStreamWriter(output, StandardCharsets.UTF_8), + "DocCheckTool Configuration"); + log.debug(LoggerHelper.DEBUG_MARKER, "配置文件保存成功: {}", configPath); + } catch (IOException e) { + log.error(LoggerHelper.DEBUG_MARKER, "保存配置文件失败: {}", e.getMessage()); + } + } + } catch (IOException e) { + log.error(LoggerHelper.DEBUG_MARKER, "创建配置目录失败: {}", e.getMessage()); + } finally { + lock.readLock().unlock(); + } + } + + /** 获取配置文件路径 */ + private static Path getConfigPath() { + String userHome = java.lang.System.getProperty("user.home"); + return Paths.get(userHome, ".docchecktool", 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("enableStep", String.valueOf(setting.isEnableStep())); + } + + /** 将Properties转换为Setting对象 */ + private static Setting propertiesToSetting(Properties props) { + Setting s = new Setting(); + + try { + s.setSingleTimeout(Long.parseLong(props.getProperty("singleTimeout", String.valueOf(DEFAULT_SINGLE_TIMEOUT)))); + } catch (NumberFormatException e) { + s.setSingleTimeout(DEFAULT_SINGLE_TIMEOUT); + log.error(LoggerHelper.DEBUG_MARKER, "singleTimeout格式错误,使用默认值{}", DEFAULT_SINGLE_TIMEOUT); + } + + try { + s.setTotalTimeout(Long.parseLong(props.getProperty("totalTimeout", String.valueOf(DEFAULT_TOTAL_TIMEOUT)))); + } catch (NumberFormatException e) { + s.setTotalTimeout(DEFAULT_TOTAL_TIMEOUT); + log.error(LoggerHelper.DEBUG_MARKER, "totalTimeout格式错误,使用默认值{}", 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); + } + + return s; + } + + /** 获取默认Setting */ + private static Setting defaultSetting() { + Setting s = new Setting(); + s.setSingleTimeout(DEFAULT_SINGLE_TIMEOUT); + s.setTotalTimeout(DEFAULT_TOTAL_TIMEOUT); + return s; + } + + /** 手动保存 */ + public static void saveSettingsNow() { + saveSettings(); + } + + /** 重新加载 */ + public static void reloadSettings() { + loadSettings(); + } + + /** 获取Setting对象 */ + public static Setting getSetting() { + return INSTANCE.setting; + } + /** 获取File 对象 */ + public static File getlastModifiedFile() { + return INSTANCE.lastModifiedFile; + } + + /** 获取配置目录路径 */ + public static String getConfigDirectory() { + return getConfigPath().getParent().toString(); + } + + /** 获取配置文件路径 */ + public static String getConfigFilePath() { + return getConfigPath().toString(); + } + public static void setLastModifiedFile(File lastModifiedFile) { + INSTANCE.lastModifiedFile = lastModifiedFile; + } + public static FileChooser getFileChooser() { + FileChooser fileChooser = new FileChooser(); + File lastFile = getlastModifiedFile(); + if (lastFile != null) { + File parentDir = lastFile.getParentFile(); + if (parentDir != null && parentDir.exists() && parentDir.isDirectory()) { + fileChooser.setInitialDirectory(parentDir); + } + } + return fileChooser; + } + public static DirectoryChooser getDirectoryChooser() { + DirectoryChooser directoryChooser = new DirectoryChooser(); + File lastFile = getlastModifiedFile(); + if (lastFile != null) { + File parentDir = lastFile.getParentFile(); + if (parentDir != null && parentDir.exists() && parentDir.isDirectory()) { + directoryChooser.setInitialDirectory(parentDir); + } + } + return directoryChooser; + } +} diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/cil/CliProcessor.java b/src/main/java/top/r3944realms/docchecktoolrefactored/cil/CliProcessor.java index 7a5b00a..e1a8c04 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/cil/CliProcessor.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/cil/CliProcessor.java @@ -8,6 +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 java.io.BufferedWriter; import java.io.IOException; @@ -102,7 +103,7 @@ public class CliProcessor { printHelp(options); System.exit(1); } catch (Exception e) { - log.error("Error processing CLI command", e); + log.error(LoggerHelper.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 22f2f71..15d2cfa 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileComparator.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileComparator.java @@ -2,6 +2,7 @@ package top.r3944realms.docchecktoolrefactored.core; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; import java.io.BufferedReader; import java.io.File; @@ -64,8 +65,7 @@ public class AddressFileComparator { int physicalCount = physicalRecords.size(); int logicalCount = logicalRecords.size(); - log.info("读取物理地址文件记录数: {}", physicalCount); - log.info("读取逻辑地址文件记录数: {}", logicalCount); + log.info(LoggerHelper.DEBUG_MARKER, "读取物理地址文件记录数: {}, 读取逻辑地址文件记录数: {}", physicalCount, logicalCount); List forwardComparisonResults = new ArrayList<>(); // 物理文件在逻辑文件中未找到 List backwardComparisonResults = new ArrayList<>(); // 逻辑文件在物理文件中未找到 @@ -178,7 +178,7 @@ public class AddressFileComparator { try { File file = new File(filePath); if (!file.exists()) { - log.error("CSV文件不存在: {}", filePath); + log.error(LoggerHelper.RELEASE_MARKER, "CSV文件不存在: {}", filePath); return records; } @@ -198,10 +198,10 @@ public class AddressFileComparator { } reader.close(); - log.info("成功读取CSV文件,共 {} 行记录", records.size()); + log.info(LoggerHelper.DEBUG_MARKER, "成功读取CSV文件,共 {} 行记录", records.size()); } catch (Exception e) { - log.error("读取CSV文件时出错: {}", e.getMessage(), e); + log.error(LoggerHelper.RELEASE_MARKER, "读取CSV文件时出错: {}", e.getMessage(), e); } return records; @@ -211,49 +211,46 @@ public class AddressFileComparator { List forwardResults, List backwardResults, List pathMismatchResults, List pageCountMismatchResults, CompareMode compareMode) { - log.info("=== 文件比较结果 ==="); - log.info("物理地址文件记录数: {}", physicalCount); - log.info("逻辑地址文件记录数: {}", logicalCount); + StringBuilder sb = new StringBuilder(); + + sb.append("=== 文件比较结果 ===\n"); + sb.append("物理地址文件记录数: ").append(physicalCount).append("\n"); + sb.append("逻辑地址文件记录数: ").append(logicalCount).append("\n"); if (pathMismatchResults.isEmpty()) { - log.info("没有路径错误"); + sb.append("没有路径错误\n"); } else { - log.info("文件名相同但路径不一致的记录数量: {}", pathMismatchResults.size()); - for (String result : pathMismatchResults) { - log.info("\t{}", result); - } + 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()) { - log.info("没有页数错误"); + sb.append("没有页数错误\n"); } else { - log.info("文件名和路径相同但页数不一致的记录数量: {}", pageCountMismatchResults.size()); - for (String result : pageCountMismatchResults) { - log.info("\t{}", result); - } + sb.append("文件名和路径相同但页数不一致的记录数量: ") + .append(pageCountMismatchResults.size()).append("\n"); + pageCountMismatchResults.forEach(result -> sb.append("\t").append(result).append("\n")); } } if (forwardResults.isEmpty()) { - log.info("没有物理存在而逻辑不存在的文件"); + sb.append("没有物理存在而逻辑不存在的文件\n"); } else { - log.info("物理文件在逻辑文件中未找到的记录数量: {}", forwardResults.size()); - for (String result : forwardResults) { - log.info("\t{}", result); - } + sb.append("物理文件在逻辑文件中未找到的记录数量: ").append(forwardResults.size()).append("\n"); + forwardResults.forEach(result -> sb.append("\t").append(result).append("\n")); } if (backwardResults.isEmpty()) { - log.info("没有逻辑存在而物理不存在的文件"); + sb.append("没有逻辑存在而物理不存在的文件\n"); } else { - log.info("逻辑文件在物理文件中未找到的记录数量: {}", backwardResults.size()); - for (String result : backwardResults) { - log.info("\t{}", result); - } + sb.append("逻辑文件在物理文件中未找到的记录数量: ").append(backwardResults.size()).append("\n"); + backwardResults.forEach(result -> sb.append("\t").append(result).append("\n")); } - log.info("=== 比较完成 ==="); + sb.append("=== 比较完成 ==="); + + log.info(LoggerHelper.RELEASE_MARKER, sb.toString()); // 一次性输出, 减少 I/O } // 为向后兼容保留原来的日志方法 diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileGenerator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileGenerator.java index 4e5d109..e43e2a7 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileGenerator.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileGenerator.java @@ -6,11 +6,11 @@ import java.io.File; public interface AddressFileGenerator { /** - * 页面级 + * 页面级 JPG那种 */ int PAGE_TYPE = 1; /** - * 文件级 + * 文件级 PDF那种 */ int FILE_TYPE = 2; diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java index ce4edb1..6045b70 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java @@ -1,15 +1,19 @@ 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.io.scanner.FileScanner; import top.r3944realms.docchecktoolrefactored.model.DuplicateGroup; import top.r3944realms.docchecktoolrefactored.model.FileMetadata; +import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -17,64 +21,103 @@ import java.util.stream.Collectors; /** * 重复文件查找核心类 */ +//TODO;代替DuplicateDocumentDetectionTask @Slf4j public class DuplicateFinder { private final FileScanner fileScanner; private final FileHashCalculator hashCalculator; private final boolean enableProgress; + private final ExecutorService executorService; + // 进度回调接口 + 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 { + GROUP_BY_SIZE, // 按大小分组阶段 + CALCULATE_HASH // 计算哈希阶段 + } + @Setter + private ProgressCallback progressCallback; + private static final int PROGRESS_REPORT_INTERVAL = 100; + private static final int BATCH_SIZE = 100; + private final List errors = new CopyOnWriteArrayList<>(); + @Getter + private long timeout = -1; public DuplicateFinder(FileScanner fileScanner, FileHashCalculator hashCalculator, boolean enableProgress) { this.fileScanner = Objects.requireNonNull(fileScanner); this.hashCalculator = Objects.requireNonNull(hashCalculator); this.enableProgress = enableProgress; + // 根据CPU核心数设置线程池大小 + int poolSize = Runtime.getRuntime().availableProcessors(); + this.executorService = Executors.newFixedThreadPool(poolSize); } public DuplicateFinder(FileScanner fileScanner, FileHashCalculator hashCalculator) { this(fileScanner, hashCalculator, false); } + public DuplicateFinder applySetting(Setting setting) { + this.timeout = setting.getSingleTimeout(); + return this; + } + /** * 查找重复文件 * @param rootDir 要扫描的根目录 * @return 按哈希值分组的重复文件列表 */ public List findDuplicates(Path rootDir) throws IOException { + // 清理错误列表 + errors.clear(); + // ----------------------------- // 第一阶段:按文件大小分组 + // ----------------------------- + if (progressCallback != null) { + progressCallback.onPhaseStarted(Phase.GROUP_BY_SIZE); + } Map> sizeGroups = groupFilesBySize(rootDir); - // 计算需要处理的总文件数(大小分组中可能有重复的文件) - int totalFilesToProcess = sizeGroups.values().stream() - .filter(group -> group.size() > 1) - .mapToInt(List::size) - .sum(); - - if (totalFilesToProcess == 0) { - return Collections.emptyList(); + if (progressCallback != null) { + progressCallback.onPhaseCompleted(Phase.GROUP_BY_SIZE); } - // 第二阶段:对可能重复的文件计算哈希 + // ----------------------------- + // 第二阶段:按文件组计算哈希 + // ----------------------------- + if (progressCallback != null) progressCallback.onPhaseStarted(Phase.CALCULATE_HASH); + Map> hashGroups = new ConcurrentHashMap<>(); AtomicInteger processedFiles = new AtomicInteger(0); - sizeGroups.values().parallelStream() - .filter(group -> group.size() > 1) // 只处理可能重复的文件 - .forEach(group -> group.parallelStream().forEach(file -> { - try { - String hash = hashCalculator.calculateHash(file.getPath()); - file.setHash(hash); - hashGroups.computeIfAbsent(hash, k -> new ArrayList<>()).add(file); - // 更新进度 - int current = processedFiles.incrementAndGet(); - if (enableProgress) { - printProgress("Calculating hashes", current, totalFilesToProcess); - } - } catch (IOException e) { - // 记录错误但继续处理其他文件 - log.error("Failed to calculate file's hash: {}, {}", file.getPath(), e.getMessage()); - } - })); - if (enableProgress) { - System.out.println(); // 完成进度条后换行 + // 获取候选文件组(每组内至少2个文件) + List> candidateGroups = sizeGroups.values().stream() + .filter(group -> group.size() > 1) + .toList(); + + int totalFilesToProcess = candidateGroups.stream().mapToInt(List::size).sum(); + if (totalFilesToProcess == 0) return Collections.emptyList(); + + List> futures = new ArrayList<>(); + + // 分批提交线程池,每组作为一个批次 + for (List group : candidateGroups) { + CompletableFuture future = CompletableFuture.runAsync(() -> { + for (FileMetadata file : group) { + processFile(file, hashGroups, processedFiles, totalFilesToProcess); + } + }, executorService); + futures.add(future); } + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + + if (progressCallback != null) progressCallback.onPhaseCompleted(Phase.CALCULATE_HASH); + if (enableProgress) System.out.println(); + + // ----------------------------- // 第三阶段:构建结果 + // ----------------------------- return hashGroups.values().stream() .filter(group -> group.size() > 1) .map(group -> new DuplicateGroup( @@ -91,13 +134,15 @@ public class DuplicateFinder { */ private Map> groupFilesBySize(Path rootDir) throws IOException { Map> sizeGroups = new ConcurrentHashMap<>(); + boolean flag = timeout != -1 && timeout > 0; FileScanner.ProgressAwareListener listener = new FileScanner.ProgressAwareListener() { @Override public void onProgressUpdate(int current, int total) { if (enableProgress) { printProgress("Scanning files", current, total); - } else { - log.info("Scanning progress: {} / {} ", current, total); + } + if (progressCallback != null) { + progressCallback.onPhaseProgress(Phase.GROUP_BY_SIZE, current, total); } } @@ -109,19 +154,20 @@ public class DuplicateFinder { meta.setSize(Files.size(file)); sizeGroups.computeIfAbsent(meta.getSize(), k -> new ArrayList<>()).add(meta); } catch (IOException e) { - log.error("Failed to get file's size: {}", file); + log.error(LoggerHelper.TRACE_MARKER, "Failed to get file's size: {}", file); } } @Override public void onScanComplete() {} @Override public void onError(Path file, Exception e) { - log.error("Error on scanning file: {}, {}", file, e.getMessage()); + log.error(LoggerHelper.TRACE_MARKER, "Error on scanning file: {}, {}", file, e.getMessage()); + errors.add(e); } }; if(enableProgress) - fileScanner.scanWithProgress(rootDir, listener); + if (flag) fileScanner.scanWithProgress(rootDir, listener, timeout); else fileScanner.scanWithProgress(rootDir, listener); else - fileScanner.scan(rootDir, listener); + if (flag) fileScanner.scan(rootDir, listener, timeout); else fileScanner.scan(rootDir, listener); return sizeGroups; } /** @@ -144,4 +190,35 @@ public class DuplicateFinder { System.out.print(progressBar); } + private void processFile(FileMetadata file, + Map> hashGroups, + AtomicInteger processedFiles, + int totalFilesToProcess) { + try { + String hash = file.getSize() > 10_000_000 ? + hashCalculator.calculateHashStreaming(file.getPath()) : + hashCalculator.calculateHash(file.getPath()); + + file.setHash(hash); + synchronized (hashGroups) { + hashGroups.computeIfAbsent(hash, k -> new ArrayList<>()).add(file); + } + + int current = processedFiles.incrementAndGet(); + if (enableProgress && (current % PROGRESS_REPORT_INTERVAL == 0 || current == totalFilesToProcess)) { + printProgress("Calculating hashes", current, totalFilesToProcess); + } + + if (progressCallback != null) { + progressCallback.onPhaseProgress(Phase.CALCULATE_HASH, current, totalFilesToProcess); + } + + } catch (IOException e) { + log.error(LoggerHelper.TRACE_MARKER, "Failed to calculate file's hash: {}, {}", file.getPath(), e.getMessage()); + errors.add(e); + } + } + public void shutdown() { + executorService.shutdown(); + } } \ No newline at end of file diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/FileHashCalculator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/FileHashCalculator.java index 269df2b..a292450 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/FileHashCalculator.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/FileHashCalculator.java @@ -14,6 +14,13 @@ public interface FileHashCalculator { */ String calculateHash(Path file) throws IOException; + /** + * 流式计算文件哈希值(适用于大文件) + * @param file 要计算的文件路径 + * @return 文件的哈希值字符串 + */ + String calculateHashStreaming(Path file) throws IOException; + /** * 计算文件部分hash值方法 * @param file 要计算的文件路径 @@ -36,7 +43,15 @@ public interface FileHashCalculator { * @return 块大小 */ default int getPartialSize() { - return 4096;// 4 * 1024 + return 4096;// 4KB + } + + /** + * 获取流式处理的缓冲区大小 + * @return 缓冲区大小 + */ + default int getStreamingBufferSize() { + return 8192; // 8KB } /** diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java index ad1bf36..5784c19 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java @@ -1,8 +1,10 @@ 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 java.io.BufferedWriter; import java.io.FileWriter; @@ -13,7 +15,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; - +@Slf4j public class HashFileGenerator { public interface ProgressListener { @@ -47,7 +49,7 @@ public class HashFileGenerator { @Override public void onError(Path path, Exception e) { - System.err.println("Error scanning path: " + path + " - " + e.getMessage()); + log.error(LoggerHelper.TRACE_MARKER, "Error scanning path: {} - {}", path, e.getMessage()); } }); @@ -74,7 +76,7 @@ public class HashFileGenerator { listener.onProgressUpdate(processed, totalFiles); } } catch (IOException e) { - System.err.println("无法计算该文件哈希值: " + file + " - " + e.getMessage()); + log.error(LoggerHelper.DEBUG_MARKER, "无法计算该文件哈希值: {} - {}", file, e.getMessage()); } }); diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/MD5HashCalculator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/MD5HashCalculator.java index 0509cdf..e58cad3 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/MD5HashCalculator.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/MD5HashCalculator.java @@ -1,17 +1,23 @@ package top.r3944realms.docchecktoolrefactored.core; import java.io.IOException; +import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; +import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; + +import static org.apache.commons.codec.digest.MessageDigestAlgorithms.MD5; /** * MD5哈希计算实现 */ public class MD5HashCalculator implements FileHashCalculator { private static final int BUFFER_SIZE = 8192; + private static final HexFormat HEX_FORMAT = HexFormat.of(); @Override public String calculateHash(Path file) throws IOException { @@ -29,6 +35,25 @@ public class MD5HashCalculator implements FileHashCalculator { throw new RuntimeException("MD5算法不可用", e); } } + @Override + public String calculateHashStreaming(Path file) throws IOException { + try (InputStream is = Files.newInputStream(file); + DigestInputStream dis = new DigestInputStream(is, MessageDigest.getInstance(MD5))) { + + byte[] buffer = new byte[getStreamingBufferSize()]; + // 读取整个文件以更新摘要 + while (dis.read(buffer) != -1) { + // 只需读取即可,DigestInputStream会自动更新摘要 + //noinspection UnnecessaryContinue + continue; + } + + byte[] digest = dis.getMessageDigest().digest(); + return HEX_FORMAT.formatHex(digest); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("MD5 algorithm not available", e); + } + } /** *

读取文件头部:

diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java index 36ec91f..4cbd4a6 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java @@ -3,6 +3,7 @@ package top.r3944realms.docchecktoolrefactored.core; import lombok.extern.slf4j.Slf4j; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; +import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; import java.io.File; import java.io.PrintWriter; @@ -143,7 +144,7 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator { document.close(); return pageCount; } catch (Exception e) { - log.warn("无法获取PDF文件页数: {}", pdfFile.getAbsolutePath(), e); + log.warn(LoggerHelper.RELEASE_MARKER, "无法获取PDF文件页数: {}", pdfFile.getAbsolutePath(), e); return 0; } } @@ -215,7 +216,7 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator { for (int i = 0; i < pathParts.length; i++) { if (!foundPrefix) { // 检查当前部分是否包含prefix - if (pathParts[i].contains(prefix)) { + if (pathParts[i].equals(prefix)) { foundPrefix = true; resultPath.append(pathParts[i]); } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/ScanningException.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/ScanningException.java new file mode 100644 index 0000000..1a67197 --- /dev/null +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/ScanningException.java @@ -0,0 +1,10 @@ +package top.r3944realms.docchecktoolrefactored.core; + +import java.util.List; + +public class ScanningException extends RuntimeException { + public final List exceptions; + public ScanningException(List exceptions) { + this.exceptions = exceptions; + } +} diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/Setting.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/Setting.java new file mode 100644 index 0000000..55d63b6 --- /dev/null +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/Setting.java @@ -0,0 +1,14 @@ +package top.r3944realms.docchecktoolrefactored.core; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Setter +@Getter +@Slf4j +public class Setting { + private long singleTimeout = 30; + private long totalTimeout = 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 9e7a6c3..0f50175 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/DbfFileReader.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/DbfFileReader.java @@ -5,12 +5,14 @@ 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 java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.UncheckedIOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -20,8 +22,11 @@ import java.util.Optional; @Slf4j public class DbfFileReader implements CatalogFileReader { // 常量定义(避免硬编码) - private static final String FIELD_ARCHIVE_CODE = "档号"; - private static final String FIELD_PAGE = "页数"; + private static final List ARCHIVE_CODE_TAG_CANDIDATES = + Arrays.asList("档号", "dangan", "fileNo", "DH"); + + private static final List PAGE_COUNT_TAG_CANDIDATES = + Arrays.asList("页数", "pages", "pageCount", "YS"); @Override public List readCatalogFile(String filePath) throws Exception { @@ -33,36 +38,35 @@ public class DbfFileReader implements CatalogFileReader { DBFReader reader = new DBFReader(fis) ) { int fieldCount = reader.getFieldCount(); - log.debug("开始读取DBF文件: {}, DBF文件字段数: {}",filePath, fieldCount); + log.debug(LoggerHelper.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("已找到所需字段,跳出循环,档号: {}, 页数: {}", archiveCodeIndex, pageIndex); + log.debug(LoggerHelper.DEBUG_MARKER, "已找到所需字段,跳出循环,档号: {}, 页数: {}", archiveCodeIndex, pageIndex); break; } String fieldName = reader.getField(i).getName(); - log.debug("发现字段: {}", fieldName); - if (FIELD_ARCHIVE_CODE.equals(fieldName)) { + log.debug(LoggerHelper.DEBUG_MARKER, "发现字段: {}", fieldName); + if (ARCHIVE_CODE_TAG_CANDIDATES.contains(fieldName)) { archiveCodeIndex = i; - log.debug("找到‘档号’字段,索引: {}", archiveCodeIndex); - } else if (FIELD_PAGE.equals(fieldName)) { + log.debug(LoggerHelper.DEBUG_MARKER, "匹配到档号字段: {}, 索引: {}", fieldName, archiveCodeIndex); + } else if (PAGE_COUNT_TAG_CANDIDATES.contains(fieldName)) { pageIndex = i; - log.debug("找到‘页数’字段,索引: {}", pageIndex); + log.debug(LoggerHelper.DEBUG_MARKER, "匹配到页数字段: {}, 索引: {}", fieldName, pageIndex); } } if (archiveCodeIndex == -1 || pageIndex == -1) { - log.error("未找到必要字段,档号: {}, 页数: {}", + log.error(LoggerHelper.RELEASE_MARKER, "未找到必要字段,档号: {}, 页数: {}", archiveCodeIndex == -1 ? "未找到" : archiveCodeIndex, pageIndex == -1 ? "未找到" : pageIndex ); throw new IllegalArgumentException( - String.format("DBF文件缺少必要字段: %s=%s, %s=%s", - FIELD_ARCHIVE_CODE, archiveCodeIndex == -1, - FIELD_PAGE, pageIndex == -1 + String.format("DBF文件缺少必要字段: 档号=%s, 页数=%s", + archiveCodeIndex == -1, pageIndex == -1 ) ); } @@ -85,7 +89,7 @@ public class DbfFileReader implements CatalogFileReader { return Integer.parseInt(i.toString().trim()); } } catch (NumberFormatException e) { - log.warn("无法将页数值转换为整数: {}", i); + log.warn(LoggerHelper.DEBUG_MARKER, "无法将页数值转换为整数: {}", i); return 0; } }).orElse(0); @@ -94,17 +98,17 @@ public class DbfFileReader implements CatalogFileReader { if (!archiveCode.isEmpty() && page > 0) { records.add(new LogicalAddressFileGenerator.Record(archiveCode, page)); validRecords++; - log.debug("读取有效记录 - 档号: {}, 页数: {}", archiveCode, page); + log.debug(LoggerHelper.DEBUG_MARKER, "读取有效记录 - 档号: {}, 页数: {}", archiveCode, page); } else { skippedRecords++; if (!archiveCode.isEmpty() || page > 0) { - log.debug("跳过无效记录 - 档号: {}, 页数: {}", archiveCode, page); + log.debug(LoggerHelper.DEBUG_MARKER, "跳过无效记录 - 档号: {}, 页数: {}", archiveCode, page); } } } - log.info("DBF文件读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords); + log.info(LoggerHelper.RELEASE_MARKER, "DBF文件读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords); } catch (IOException e) { - log.error("读取DBF文件失败: {}", filePath, e); + log.error(LoggerHelper.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 a0b3857..abfc324 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/ExcelFileReader.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/ExcelFileReader.java @@ -6,6 +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 java.io.File; import java.io.FileInputStream; @@ -26,33 +27,33 @@ public class ExcelFileReader implements CatalogFileReader { } private List readExcelFile(File file, boolean isXlsx) throws Exception { List records = new ArrayList<>(); - log.debug("开始解析Excel文件,格式: {}", isXlsx ? "xlsx" : "xls"); + log.debug(LoggerHelper.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("读取工作表: {}", sheet.getSheetName()); + log.debug(LoggerHelper.DEBUG_MARKER, "读取工作表: {}", sheet.getSheetName()); // 获取标题行 Row headerRow = sheet.getRow(0); if (headerRow == null) { - log.error("Excel文件缺少标题行"); + log.error(LoggerHelper.RELEASE_MARKER, "Excel文件缺少标题行"); throw new IllegalArgumentException("Excel文件缺少标题行"); } // 查找"档号"和"页数"列的索引 int archiveCodeIndex = -1; int pageIndex = -1; - log.debug("开始查找'档号'和'页数'列的索引"); + log.debug(LoggerHelper.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("找到'档号'列,索引: {}", archiveCodeIndex); + log.debug(LoggerHelper.DEBUG_MARKER, "找到'档号'列,索引: {}", archiveCodeIndex); } else if (FIELD_PAGE.equals(cellValue)) { pageIndex = cell.getColumnIndex(); foundExactMatch = true; - log.debug("找到精确匹配'页数'列,索引: {}", pageIndex); + log.debug(LoggerHelper.DEBUG_MARKER, "找到精确匹配'页数'列,索引: {}", pageIndex); } } // 如果没有精确匹配,进行模糊查找 @@ -61,13 +62,13 @@ public class ExcelFileReader implements CatalogFileReader { String cellValue = getCellValueAsString(cell).trim(); if (cellValue.contains(FIELD_PAGE)) { pageIndex = cell.getColumnIndex(); - log.debug("找到模糊匹配'页数'列,索引: {}", pageIndex); + log.debug(LoggerHelper.DEBUG_MARKER, "找到模糊匹配'页数'列,索引: {}", pageIndex); } } } // 检查是否找到必需的列 if (archiveCodeIndex == -1 || pageIndex == -1) { - log.error("未找到必要字段,档号: {}, 页数: {}", + log.error(LoggerHelper.RELEASE_MARKER, "未找到必要字段,档号: {}, 页数: {}", archiveCodeIndex == -1 ? "未找到" : archiveCodeIndex, pageIndex == -1 ? "未找到" : pageIndex ); @@ -83,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++; @@ -115,12 +116,12 @@ public class ExcelFileReader implements CatalogFileReader { // 只有数据有效时才添加记录 records.add(new LogicalAddressFileGenerator.Record(archiveCode, page)); validRecords++; - log.debug("读取有效记录 - 档号: {}, 页数: {}", archiveCode, page); + log.debug(LoggerHelper.DEBUG_MARKER, "读取有效记录 - 档号: {}, 页数: {}", archiveCode, page); } - log.info("数据读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords); + log.info(LoggerHelper.RELEASE_MARKER, "数据读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords); } catch (Exception e) { - log.error("读取Excel文件时发生错误: {}", e.getMessage(), e); + log.error(LoggerHelper.RELEASE_MARKER, "读取Excel文件时发生错误: {}", e.getMessage(), e); throw e; } return records; @@ -188,7 +189,7 @@ public class ExcelFileReader implements CatalogFileReader { return 0; } } catch (NumberFormatException e) { - log.warn("无法将单元格值转换为整数: {}", cell); + log.warn(LoggerHelper.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 d7e5cb9..f3db04a 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/XmlFileReader.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/XmlFileReader.java @@ -7,6 +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 javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -23,10 +24,10 @@ public class XmlFileReader implements CatalogFileReader { Arrays.asList("row", "record", "data", "item", "档案"); private static final List ARCHIVE_CODE_TAG_CANDIDATES = - Arrays.asList("档号", "dangan", "fileNo"); + Arrays.asList("档号", "dangan", "fileNo", "DH"); private static final List PAGE_COUNT_TAG_CANDIDATES = - Arrays.asList("页数", "pages", "pageCount"); + Arrays.asList("页数", "pages", "pageCount", "YS"); @Override @@ -42,11 +43,11 @@ public class XmlFileReader implements CatalogFileReader { Document doc = builder.parse(file); doc.getDocumentElement().normalize(); - log.debug("开始解析XML文件: {}, 根元素: {}", + log.debug(LoggerHelper.DEBUG_MARKER, "开始解析XML文件: {}, 根元素: {}", file.getName(), doc.getDocumentElement().getNodeName()); // 查找记录元素 NodeList recordNodes = findAllRecordNodes(doc); - log.debug("找到 {} 个潜在记录节点", recordNodes.getLength()); + log.debug(LoggerHelper.DEBUG_MARKER, "找到 {} 个潜在记录节点", recordNodes.getLength()); // 解析每个记录元素 int validCount = 0; @@ -61,16 +62,16 @@ public class XmlFileReader implements CatalogFileReader { if (record != null) { records.add(record); validCount++; - log.debug("解析到有效记录: {}", record); + log.debug(LoggerHelper.DEBUG_MARKER, "解析到有效记录: {}", record); } else { invalidCount++; - log.debug("跳过无效记录节点"); + log.debug(LoggerHelper.DEBUG_MARKER, "跳过无效记录节点"); } } } - log.info("XML解析完成 - 有效记录: {}, 无效记录: {}", validCount, invalidCount); + log.info(LoggerHelper.RELEASE_MARKER, "XML解析完成 - 有效记录: {}, 无效记录: {}", validCount, invalidCount); } catch (Exception e) { - log.error("解析XML文件失败: {}", filePath, e); + log.error(LoggerHelper.RELEASE_MARKER, "解析XML文件失败: {}", filePath, e); throw new Exception("解析XML文件失败: " + filePath, e); } return records; @@ -92,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("使用标签名 '{}' 找到 {} 个记录节点", tagName, nodes.getLength()); + log.debug(LoggerHelper.DEBUG_MARKER, "使用标签名 '{}' 找到 {} 个记录节点", tagName, nodes.getLength()); return nodes; } } - log.warn("未找到任何记录节点,尝试的标签名: {}", RECORD_TAG_CANDIDATES); + log.warn(LoggerHelper.DEBUG_MARKER, "未找到任何记录节点,尝试的标签名: {}", RECORD_TAG_CANDIDATES); return new EmptyNodeList(); // 返回空节点列表而不是null } @@ -105,7 +106,7 @@ public class XmlFileReader implements CatalogFileReader { String pageStr = findFirstNonEmptyTextContent(recordElement, PAGE_COUNT_TAG_CANDIDATES); if (archiveCode == null || archiveCode.isEmpty()) { - log.debug("记录缺少档号字段"); + log.debug(LoggerHelper.RELEASE_MARKER, "记录缺少档号字段"); return null; } @@ -114,11 +115,11 @@ public class XmlFileReader implements CatalogFileReader { if (page > 0) { return new LogicalAddressFileGenerator.Record(archiveCode, page); } else { - log.debug("无效的页数值: {}", pageStr); + log.debug(LoggerHelper.RELEASE_MARKER, "无效的页数值: {}", pageStr); return null; } } catch (NumberFormatException e) { - log.warn("页数字段格式错误: {}", pageStr); + log.warn(LoggerHelper.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 4c08da6..c882278 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/FileScanner.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/FileScanner.java @@ -21,6 +21,23 @@ public interface FileScanner { default void scanWithProgress(Path rootPath, ProgressAwareListener listener) { throw new UnsupportedOperationException("Please implement FileScanner, ProgressAwareListener."); } + /** + * 扫描指定路径下的文件 (带超时) + * + * @param rootPath 根路径 + * @param listener 文件发现监听器 + * @param timeout 超时(s) + */ + default void scan(Path rootPath, FileScanListener listener, long timeout) { + throw new UnsupportedOperationException("Please implement FileScanner, FileScannerListener."); + } + /** + * 扫描指定路径下的文件(带进度反馈)(带超时) + * @param timeout 超时(s) + */ + default void scanWithProgress(Path rootPath, ProgressAwareListener listener, long timeout) { + throw new UnsupportedOperationException("Please implement FileScanner, ProgressAwareListener."); + } /** * 文件扫描监听器 diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/ParallelFileScanner.java b/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/ParallelFileScanner.java deleted file mode 100644 index 5f92c55..0000000 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/ParallelFileScanner.java +++ /dev/null @@ -1,135 +0,0 @@ -package top.r3944realms.docchecktoolrefactored.io.scanner; - -import lombok.extern.slf4j.Slf4j; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Stream; - -/** - * The type Parallel file scanner. - *

- * 这个没法正常使用,目前遇到的问题 - *

- * * 目录遍历时遇到权限问题(静默失败) - *

- * * 存在符号链接循环 - *

- * * 文件系统驱动程序卡死 - *

- * * JVM与NTFS文件系统兼容性问题 - */ -@Slf4j -@Deprecated -public class ParallelFileScanner implements FileScanner ,AutoCloseable { - private final ForkJoinPool forkJoinPool; - private volatile boolean cancelled = false; - /** - * 使用默认并行度(CPU核心数) - */ - public ParallelFileScanner() { - this(Runtime.getRuntime().availableProcessors()); - } - - /** - * Instantiates a new Parallel file scanner. - * - * @param parallelism 并行度(线程数) - */ - public ParallelFileScanner(int parallelism) { - this.forkJoinPool = new ForkJoinPool(parallelism); - } - - @Override - public void scan(Path rootPath, FileScanListener listener) { - scanInternal(rootPath, listener, null); - } - @Override - public void scanWithProgress(Path rootPath, ProgressAwareListener listener) { - // 先快速统计总文件数 - long totalFiles = countFiles(rootPath); - scanInternal(rootPath, listener, totalFiles); - } - - private long countFiles(Path rootPath) { - try(Stream pathStream = Files.walk(rootPath) - .parallel() - .filter(Files::isRegularFile)) { - return pathStream.count(); - } catch (IOException e) { - return -1; // 表示无法确定总数 - } - } - private void scanInternal(Path rootPath, FileScanListener listener, Long totalFiles) { - log.debug("ThreadPool Status: {}", forkJoinPool.isShutdown() ? "Closed" : "Running"); - forkJoinPool.submit(() -> { // 方法没问题,可能就是在线程这里被卡死了 - try { - AtomicInteger processed = new AtomicInteger(0); - log.debug("Scanning files in {}", rootPath); - // 收集所有文件到List(避免Stream被重复使用) - @SuppressWarnings("resource") List files = Files.walk(rootPath) - .peek(p -> log.trace("visiting: {}", p)) - .parallel() - .filter(p -> { - boolean isRegular = Files.isRegularFile(p); - if (!isRegular) { - log.debug("Skip non-regular : {} ", p); - } - return isRegular; - }) - .peek(p -> log.trace("Found file: {}", p)) - .toList(); // 立即消费Stream - if (files.isEmpty()) { - log.warn("No files found in directory: {}", rootPath); - } else log.debug("Found {} files in {}", files.size(), rootPath); - files.forEach(file -> { - if (cancelled) { - log.debug("Cancelled scanning file {}", file); - return; - } - - try { - log.debug("Handle file {}", file); - listener.onFileFound(file); - - // 进度更新 - if (listener instanceof ProgressAwareListener progressListener) { - int current = processed.incrementAndGet(); - progressListener.onProgressUpdate( - current, - totalFiles != null ? totalFiles.intValue() : -1 - ); - } - } catch (Exception e) { - log.debug("Error Handle file {}", file, e); - listener.onError(file, e); - } - }); - if (!cancelled) { - log.debug("Finished scanning files in {}", rootPath); - listener.onScanComplete(); - } - } catch (IOException e) { - listener.onError(rootPath, e); - } catch (Exception e) { - log.error("Unexpected error in scan thread", e); - listener.onError(rootPath, e); - } - }); - log.debug("Task submitted to thread pool"); - } - - public void cancel() { - cancelled = true; - forkJoinPool.shutdownNow(); - } - - @Override - public void close() { - cancel(); - } -} \ No newline at end of file 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 4e6882c..85ef97a 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/RobustParallelScanner.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/RobustParallelScanner.java @@ -1,6 +1,7 @@ package top.r3944realms.docchecktoolrefactored.io.scanner; import lombok.extern.slf4j.Slf4j; +import top.r3944realms.docchecktoolrefactored.util.LoggerHelper; import java.io.FileNotFoundException; import java.io.IOException; @@ -26,17 +27,24 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable { this.forkJoinPool = new ForkJoinPool(parallelism); this.maxDepth = maxDepth; // 防止无限递归 } - @Override - public void scan(Path rootPath, FileScanListener listener) { - scanInternal(rootPath, listener, null); + public void scan(Path rootPath, FileScanListener listener, long timeout) { + scanInternal(rootPath, listener, null, timeout); } @Override - public void scanWithProgress(Path rootPath, ProgressAwareListener listener) { + public void scan(Path rootPath, FileScanListener listener) { + scan(rootPath, listener, 30); + } + @Override + public void scanWithProgress(Path rootPath, ProgressAwareListener listener, long timeout) { // 预扫描阶段:计算总文件数 AtomicLong totalFiles = new AtomicLong(0); countFiles(rootPath, totalFiles); - scanInternal(rootPath, listener, totalFiles); + scanInternal(rootPath, listener, totalFiles, timeout); + } + @Override + public void scanWithProgress(Path rootPath, ProgressAwareListener listener) { + scanWithProgress(rootPath, listener, 30); } private void countFiles(Path dir, AtomicLong counter) { if (cancelled) return; @@ -52,10 +60,13 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable { } } } catch (IOException e) { - log.warn("Failed to pre-scan: {}", dir, e); + log.warn(LoggerHelper.TRACE_MARKER, "Failed to pre-scan: {}", dir, e); } } private void scanInternal(Path rootPath, FileScanListener listener, AtomicLong totalFiles) { + scanInternal(rootPath, listener, totalFiles, 30); + } + private void scanInternal(Path rootPath, FileScanListener listener, AtomicLong totalFiles, long timeout) { try { validateDirectory(rootPath); @@ -70,9 +81,9 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable { } catch (Exception e) { listener.onError(rootPath, e); } - }).get(30, TimeUnit.SECONDS); + }).get(timeout, TimeUnit.SECONDS); } catch (TimeoutException e) { - log.error("Scan timeout: {}", rootPath, e); + log.error(LoggerHelper.TRACE_MARKER, "Scan timeout: {}", rootPath, e); forkJoinPool.shutdownNow(); listener.onError(rootPath, new TimeoutException("扫描超时30秒")); } catch (Exception e) { diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/LoginStageController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/LoginStageController.java index b577202..c6a3a39 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/LoginStageController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/LoginStageController.java @@ -12,6 +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 java.net.URL; import java.util.ResourceBundle; @@ -38,10 +39,10 @@ public class LoginStageController implements Initializable { String password = passwordField.getText(); if ("admin".equals(username) && "admin".equals(password)) { - log.info("{} Login successful", username); + log.info(LoggerHelper.DEBUG_MARKER, "{} Login successful", username); SceneManager.switchMainView(); } else { - log.info("Invalid username or password"); + log.info(LoggerHelper.DEBUG_MARKER, "Invalid username or password"); DialogUtil.showErrorDialog("错误", null, "用户名或密码错误!"); } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/MainStageController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/MainStageController.java index af9c3c7..b5355cc 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/MainStageController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/MainStageController.java @@ -1,14 +1,25 @@ package top.r3944realms.docchecktoolrefactored.ui; +import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.scene.control.Menu; -import javafx.scene.control.Tab; +import javafx.scene.control.*; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; +import javafx.scene.input.KeyEvent; +import top.r3944realms.docchecktoolrefactored.System; +import top.r3944realms.docchecktoolrefactored.core.Setting; + +import java.util.ArrayList; +import java.util.List; /** * The type Main stage controller. */ public class MainStageController { + @FXML public Button nextB; + @FXML public Button prevB; @FXML private Tab step1T; @FXML private Tab step2T; @FXML private Tab step3T; @@ -16,11 +27,110 @@ public class MainStageController { @FXML private Tab step5T; @FXML private Tab step6T; @FXML private Tab step7T; - + @FXML private TabPane tabPane; @FXML private Menu helpM; + @FXML private MenuItem aboutSoftwareMI; + @FXML private MenuItem helpDocMI; + @FXML private MenuItem exitMI; + @FXML private MenuItem logoutMI; + @FXML private MenuItem settingMI; + private List tabs; + private int currentIndex = 0; + @FXML + public void initialize() { + // 初始化所有Tab的集合 + tabs = new ArrayList<>(); + tabs.add(step1T); + tabs.add(step2T); + tabs.add(step3T); + tabs.add(step4T); + tabs.add(step5T); + tabs.add(step6T); + tabs.add(step7T); + // 初始状态设置 + updateButtonStates(); + updateStepButtonsVisibility(); // 根据配置初始可见性 + // 监听Tab切换事件 + tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) -> { + currentIndex = tabs.indexOf(newTab); + updateButtonStates(); + }); + } + /** + * 更新按钮状态 + */ + private void updateButtonStates() { + prevB.setDisable(currentIndex == 0); + nextB.setDisable(currentIndex == tabs.size() - 1); + } -} + /** + * 切换到下一个Tab + * @param actionEvent 事件 + */ + @FXML + void onNext(ActionEvent actionEvent) { + if (currentIndex < tabs.size() - 1) { + tabPane.getSelectionModel().select(tabs.get(currentIndex + 1)); + } + } + + /** + * 切换到上一个Tab + * @param actionEvent 事件 + */ + @FXML + void onPrev(ActionEvent actionEvent) { + if (currentIndex > 0) { + tabPane.getSelectionModel().select(tabs.get(currentIndex - 1)); + } + } + + /** + * 处理键盘快捷键 + * @param keyEvent 键盘事件 + */ + @FXML + void handleKeyPressed(KeyEvent keyEvent) { + // Ctrl+> 或 Ctrl+. 切换到下一个Tab + if (new KeyCodeCombination(KeyCode.PERIOD, KeyCombination.CONTROL_DOWN).match(keyEvent) || + new KeyCodeCombination(KeyCode.GREATER, KeyCombination.CONTROL_DOWN).match(keyEvent)) { + onNext(null); + keyEvent.consume(); + } + // Ctrl+< 或 Ctrl+, 切换到上一个Tab + else if (new KeyCodeCombination(KeyCode.COMMA, KeyCombination.CONTROL_DOWN).match(keyEvent) || + new KeyCodeCombination(KeyCode.LESS, KeyCombination.CONTROL_DOWN).match(keyEvent)) { + onPrev(null); + keyEvent.consume(); + } + } + + @FXML void onLogout(ActionEvent actionEvent) { + SceneManager.switchLoginView(); + } + + @FXML void onExit(ActionEvent actionEvent) { + + } + + @FXML void onOpenSetting(ActionEvent actionEvent) { + SceneManager.openSettingView(); + } + + @FXML void onOpenHelpDoc(ActionEvent actionEvent) { + } + + @FXML void onAbout(ActionEvent actionEvent) { + } + public void updateStepButtonsVisibility() { + Setting setting = System.getSetting(); + boolean visible = setting.isEnableStep(); // 由enableStep控制 + nextB.setVisible(visible); + prevB.setVisible(visible); + } +} \ No newline at end of file diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SceneManager.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SceneManager.java index 8c43890..76b26e1 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SceneManager.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SceneManager.java @@ -5,6 +5,7 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; +import javafx.stage.Modality; import javafx.stage.Stage; import javafx.util.Duration; import lombok.Getter; @@ -13,6 +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 java.io.IOException; import java.util.ArrayList; @@ -29,7 +31,8 @@ public class SceneManager { @Getter @Setter private static Stage primaryStage; - + @Getter + private static MainStageController mainController; @Getter private static final List openStages = new ArrayList<>(); @@ -46,14 +49,59 @@ public class SceneManager { * Switch login view. */ public static void switchLoginView() { - SceneManager.loadView("/fxml/login-view.fxml","淮阴区数字化档案检查验收系统 - 登录", 400, 300); + SceneManager.loadView("/fxml/login-view.fxml","数字化验收工具 - 登录", 400, 300); } /** * Switch main view. */ public static void switchMainView() { - SceneManager.loadView("/fxml/main-view.fxml", "淮阴区数字化档案检查验收系统 - 主界面", 1200, 900); + try { + FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(Main.class.getResource("/fxml/main-view.fxml"))); + Parent root = loader.load(); + mainController = loader.getController(); // 保存控制器引用 + Scene newScene = new Scene(root, 1200, 900); + + applyFadeTransition(root); + + primaryStage.setScene(newScene); + primaryStage.centerOnScreen(); + primaryStage.setTitle("数字化验收工具 - 主界面"); + + // 设置窗口关闭确认 + primaryStage.setOnCloseRequest(event -> { + if (!DialogUtil.showExitConfirmation(primaryStage)) { + event.consume(); + } + }); + + } catch (IOException e) { + log.error(LoggerHelper.TRACE_MARKER, "Failed to load main view", e); + DialogUtil.showErrorDialog("错误", "加载主界面失败", e.getMessage()); + } + } + /** + * 打开设置窗口 + */ + public static void openSettingView() { + try { + Parent root = FXMLLoader.load(Objects.requireNonNull(Main.class.getResource("/fxml/setting-view.fxml"))); + Stage settingStage = new Stage(); + settingStage.setTitle("数字化验收工具 - 设置"); + Scene scene = new Scene(root, 300, 206); + settingStage.setScene(scene); // 默认大小可调 + settingStage.initOwner(primaryStage); // 设置父窗口 + settingStage.initModality(Modality.WINDOW_MODAL); + settingStage.setResizable(false); + settingStage.centerOnScreen(); + + settingStage.show(); + openStages.add(settingStage); + + } catch (IOException e) { + log.error(LoggerHelper.TRACE_MARKER, "Failed to open setting view: {}", e.getMessage(), e); + DialogUtil.showErrorDialog("错误", "加载设置窗口失败", "无法加载设置窗口: " + e.getMessage()); + } } /** @@ -100,9 +148,8 @@ public class SceneManager { event.consume(); } }); - } catch (IOException e) { - log.error("Failed to load view: {}", fxmlPath, e); + log.error(LoggerHelper.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 new file mode 100644 index 0000000..ac71912 --- /dev/null +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SettingDialogController.java @@ -0,0 +1,126 @@ +package top.r3944realms.docchecktoolrefactored.ui; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Spinner; +import javafx.scene.input.MouseDragEvent; +import javafx.stage.Stage; +import top.r3944realms.docchecktoolrefactored.System; +import top.r3944realms.docchecktoolrefactored.core.Setting; +import top.r3944realms.docchecktoolrefactored.ui.extend.LongSpinnerValueFactory; +import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil; + +import java.net.URL; +import java.util.ResourceBundle; + +public class SettingDialogController implements Initializable { + + @FXML private CheckBox enableStepCB; + @FXML private Button resetB, saveB, cancelB; + @FXML private Spinner scanTotalTimeOutS, scanSingleTimeOutS; + + private Setting setting; + + // Spinner 范围常量 + private static final int SINGLE_MIN = 1; + private static final int SINGLE_MAX = 3600; + private static final int TOTAL_MIN = 1; + private static final int TOTAL_MAX = 86400; + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + // 获取当前配置 + 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); + + // 添加焦点离开时校验 + addSpinnerValidation(scanSingleTimeOutS, SINGLE_MIN, SINGLE_MAX); + addSpinnerValidation(scanTotalTimeOutS, TOTAL_MIN, TOTAL_MAX); + enableStepCB.setSelected(setting.isEnableStep()); + } + + /** 保存修改 */ + @FXML + void onSave(ActionEvent actionEvent) { + // 更新配置对象 + setting.setSingleTimeout(scanSingleTimeOutS.getValue()); + setting.setTotalTimeout(scanTotalTimeOutS.getValue()); + setting.setEnableStep(enableStepCB.isSelected()); + // 保存到配置文件 + System.saveSettingsNow(); + // 通知主界面刷新按钮状态 + MainStageController mainController = SceneManager.getMainController(); + if(mainController != null){ + mainController.updateStepButtonsVisibility(); + } + closeDialog(); + } + + /** 重置为默认值 */ + @FXML + void onReset(ActionEvent actionEvent) { + scanSingleTimeOutS.getValueFactory().setValue(30L); // 默认单次超时 + scanTotalTimeOutS.getValueFactory().setValue(300L); // 默认总超时 + enableStepCB.setSelected(false); + } + + /** 取消修改 */ + @FXML + void onCancel(ActionEvent actionEvent) { + closeDialog(); + } + + @FXML + void onCheckOne(MouseDragEvent mouseDragEvent) { + validateSpinnerValue(scanSingleTimeOutS, 30, 3600); + } + + @FXML + void onCheckTwo(MouseDragEvent mouseDragEvent) { + validateSpinnerValue(scanTotalTimeOutS, 60, 3600 * 24); + } + @FXML + void onSettingThree(ActionEvent actionEvent) { + + } + + /** 给 Spinner 添加离开焦点校验 */ + private void addSpinnerValidation(Spinner spinner, long min, long max) { + spinner.getEditor().focusedProperty().addListener((obs, oldVal, newVal) -> { + if (!newVal) { // 焦点离开时触发 + validateSpinnerValue(spinner, min, max); + } + }); + } + + /** 校验 Spinner 的值是否在范围内,如果超出则纠正并提示 */ + private void validateSpinnerValue(Spinner spinner, long min, long max) { + try { + long value = Integer.parseInt(spinner.getEditor().getText()); + if (value < min || value > max) { + // 超出范围则纠正 + long corrected = Math.max(min, Math.min(value, max)); + spinner.getValueFactory().setValue(corrected); + + // 弹出提示 + DialogUtil.showWarningDialog("输入警告", "输入的值超出范围,已自动调整为 " + corrected); + } + } catch (NumberFormatException e) { + // 非数字输入恢复原来的值 + spinner.getEditor().setText(spinner.getValue().toString()); + DialogUtil.showWarningDialog("输入警告","输入非法,已恢复为原值 " + spinner.getValue()); + } + } + /** 关闭对话框 */ + private void closeDialog() { + Stage stage = (Stage) cancelB.getScene().getWindow(); + stage.close(); + } +} diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/extend/LongSpinnerValueFactory.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/extend/LongSpinnerValueFactory.java new file mode 100644 index 0000000..c3d09f8 --- /dev/null +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/extend/LongSpinnerValueFactory.java @@ -0,0 +1,48 @@ +package top.r3944realms.docchecktoolrefactored.ui.extend; + +import javafx.scene.control.SpinnerValueFactory; +import lombok.Setter; + +@Setter +public class LongSpinnerValueFactory extends SpinnerValueFactory { + + public LongSpinnerValueFactory(long min, long max, long initialValue) { + setMin(min); + setMax(max); + setValue(initialValue); + + setConverter(new javafx.util.StringConverter<>() { + @Override + public String toString(Long object) { + return object.toString(); + } + + @Override + public Long fromString(String string) { + try { + return Long.parseLong(string); + } catch (NumberFormatException e) { + return getValue(); + } + } + }); + } + + private long min; + private long max; + + @Override + public void decrement(int steps) { + long newValue = getValue() - steps; + if (newValue < min) newValue = min; + setValue(newValue); + } + + @Override + public void increment(int steps) { + long newValue = getValue() + steps; + if (newValue > max) newValue = max; + setValue(newValue); + } +} + 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 7ecc7b0..e8f8b7c 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/DuplicateDocumentPaneController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/DuplicateDocumentPaneController.java @@ -1,5 +1,6 @@ package top.r3944realms.docchecktoolrefactored.ui.module; +import javafx.beans.value.ChangeListener; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; @@ -8,11 +9,11 @@ import javafx.scene.control.TextField; import javafx.stage.DirectoryChooser; import javafx.stage.Stage; import lombok.extern.slf4j.Slf4j; -import top.r3944realms.docchecktoolrefactored.core.DuplicateFinder; -import top.r3944realms.docchecktoolrefactored.core.FileHashCalculator; -import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner; -import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner; +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 java.io.File; @@ -21,22 +22,26 @@ import java.io.File; */ @Slf4j public class DuplicateDocumentPaneController { + @FXML private TextArea result1B; @FXML private TextField loadFolder1TF; @FXML private Button selectLoadFolder1B; @FXML private Button start1B; - + @FXML private Button cancel1B; + private final ProgressBarUtil progressBarUtil = new ProgressBarUtil(); + private DuplicateDocumentDetectionTask currentTask; // 保存任务引用 /** * On select folder. * * @param actionEvent the action event */ @FXML void onSelectFolder(ActionEvent actionEvent) { - DirectoryChooser directoryChooser = new DirectoryChooser(); + DirectoryChooser directoryChooser = System.getDirectoryChooser(); directoryChooser.setTitle("选择要检查的文件夹"); File selectedFolder = directoryChooser.showDialog(new Stage()); if (selectedFolder != null) { loadFolder1TF.setText(selectedFolder.getAbsolutePath()); + System.setLastModifiedFile(selectedFolder); } } @@ -46,39 +51,84 @@ public class DuplicateDocumentPaneController { * @param actionEvent the action event */ @FXML void onStart(ActionEvent actionEvent) { - log.info("用户点击了开始查重按钮"); + log.info(LoggerHelper.DEBUG_MARKER, "用户点击了开始查重按钮"); String folderPath = loadFolder1TF.getText(); if (folderPath == null || folderPath.trim().isEmpty()) { - log.warn("未选择文件夹,无法进行查重"); + log.warn(LoggerHelper.DEBUG_MARKER, "未选择文件夹,无法进行查重"); result1B.setText("请选择要检查的文件夹。"); return; } + // 禁用开始按钮避免重复点击 + start1B.setDisable(true); + cancel1B.setDisable(false); + // 显示进度条窗口 + progressBarUtil.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() : "处理中..."); + } + }; + task.progressProperty().addListener(progressChangeListener); // 绑定任务的消息到结果文本区域 - task.messageProperty().addListener((observable, oldValue, newValue) -> { + ChangeListener messageChangeListener = (observable, oldValue, newValue) -> { result1B.setText(newValue); - }); + }; + task.messageProperty().addListener(messageChangeListener); // 当任务完成时显示完整结果 task.setOnSucceeded(e -> { + progressBarUtil.closeProgress(); result1B.setText(task.getValue()); - log.info("查重任务完成,结果如下:{}", task.getValue()); + start1B.setDisable(false); + cancel1B.setDisable(true); + log.info(LoggerHelper.RELEASE_MARKER, "查重任务完成,结果如下:{}", task.getValue()); }); // 处理任务失败情况 task.setOnFailed(e -> { + progressBarUtil.closeProgress(); Throwable exception = task.getException(); result1B.setText("检测过程中发生错误: " + exception.getMessage()); - log.error("error", exception); - log.info("查重任务失败,错误信息: {}", exception.getMessage()); + start1B.setDisable(false); + cancel1B.setDisable(true); + log.error(LoggerHelper.RELEASE_MARKER, "查重任务失败", exception); }); + // 处理任务取消情况 + task.setOnCancelled(e -> { + progressBarUtil.closeProgress(); + result1B.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(); + } + }); // 在新线程中执行任务 Thread thread = new Thread(task); thread.setDaemon(true); thread.start(); } + + public void onCancel(ActionEvent actionEvent) { + if (currentTask != null && currentTask.isRunning()) { + currentTask.cancel(); // 触发 setOnCancelled + } else { + log.warn(LoggerHelper.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 9dd6264..2bc51a1 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java @@ -9,10 +9,12 @@ import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.stage.FileChooser; import lombok.extern.slf4j.Slf4j; +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 java.io.File; import java.net.URL; @@ -49,8 +51,8 @@ public class PathCheckPaneController implements Initializable { * @param actionEvent the action event */ @FXML void onSelectLC(ActionEvent actionEvent) { - log.info("用户点击了选择目录文件按钮"); - FileChooser fileChooser = new FileChooser(); + log.info(LoggerHelper.DEBUG_MARKER, "用户点击了选择目录文件按钮"); + FileChooser fileChooser = System.getFileChooser(); fileChooser.setTitle("选择目录文件"); // 设置文件过滤器,只允许DBF、XML、xlsx、xls格式 @@ -60,7 +62,7 @@ public class PathCheckPaneController implements Initializable { FileChooser.ExtensionFilter dbfFilter = new FileChooser.ExtensionFilter("DBF Files (*.dbf)", "*.dbf"); FileChooser.ExtensionFilter xmlFilter = new FileChooser.ExtensionFilter("XML Files (*.xml)", "*.xml"); - fileChooser.getExtensionFilters().addAll( xlsxFilter, xlsFilter,dbfFilter, xmlFilter); + fileChooser.getExtensionFilters().addAll(xlsxFilter, xlsFilter,dbfFilter, xmlFilter); // 显示文件选择对话框 File selectedFile = fileChooser.showOpenDialog(selectLoadCatalog2B.getScene().getWindow()); @@ -68,9 +70,10 @@ public class PathCheckPaneController implements Initializable { // 如果选择了文件,则将文件路径显示在loadCatalog2TF上 if (selectedFile != null) { loadCatalog2TF.setText(selectedFile.getAbsolutePath()); - log.info("选择的目录文件路径为:{}", selectedFile.getAbsolutePath()); + System.setLastModifiedFile(selectedFile); + log.info(LoggerHelper.DEBUG_MARKER, "选择的目录文件路径为:{}", selectedFile.getAbsolutePath()); }else{ - log.warn("用户未选择任何文件夹"); + log.warn(LoggerHelper.DEBUG_MARKER, "用户未选择任何文件夹"); result2TA.setText("未选择任何文件夹,请重新选择。"); } } @@ -81,7 +84,7 @@ public class PathCheckPaneController implements Initializable { * @param actionEvent the action event */ @FXML void onSelectJPGF(ActionEvent actionEvent) { - javafx.stage.DirectoryChooser directoryChooser = new javafx.stage.DirectoryChooser(); + javafx.stage.DirectoryChooser directoryChooser = System.getDirectoryChooser(); // 正确获取当前选中的值 Mode selectedMode = loadFolderType2CB.getValue(); if (selectedMode == Mode.PAGE_TYPE) { @@ -89,14 +92,15 @@ public class PathCheckPaneController implements Initializable { } else if (selectedMode == Mode.FILE_TYPE) { directoryChooser.setTitle("选择文件级文件夹"); } - log.info("用户选择的模式为:{}", selectedMode); + log.info(LoggerHelper.DEBUG_MARKER, "用户选择的模式为:{}", selectedMode); File selectedDirectory = directoryChooser.showDialog(selectJPGFolder2B.getScene().getWindow()); if (selectedDirectory != null) { loadJPGFolder2TF.setText(selectedDirectory.getAbsolutePath()); - log.info("选择的{}文件夹路径为:{}", selectedMode,selectedDirectory.getAbsolutePath()); + log.info(LoggerHelper.DEBUG_MARKER, "选择的{}文件夹路径为:{}", selectedMode,selectedDirectory.getAbsolutePath()); } + System.setLastModifiedFile(selectedDirectory); } /** @@ -105,14 +109,14 @@ public class PathCheckPaneController implements Initializable { * @param actionEvent the action event */ @FXML void onGenerateLA(ActionEvent actionEvent) { - log.info("用户点击了生成逻辑地址文件按钮"); + log.info(LoggerHelper.DEBUG_MARKER, "用户点击了生成逻辑地址文件按钮"); String filePath = loadCatalog2TF.getText(); if (filePath.isEmpty()) { result2TA.setText("请先选择目录文件。"); return; } - FileChooser fileChooser = new FileChooser(); + FileChooser fileChooser = System.getFileChooser(); fileChooser.setTitle("选择保存逻辑地址文件的位置"); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv")); fileChooser.setInitialFileName("逻辑地址文件.csv"); @@ -120,10 +124,10 @@ public class PathCheckPaneController implements Initializable { File outputFile = fileChooser.showSaveDialog(generateLogicalAddress2B.getScene().getWindow()); if (outputFile == null) { result2TA.setText("未选择保存位置"); - log.warn("用户未选择任何文件"); + log.warn(LoggerHelper.DEBUG_MARKER, "用户未选择任何文件"); return; } - + System.setLastModifiedFile(outputFile); // 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加 final File finalOutputFile; if (!outputFile.getName().toLowerCase().endsWith(".csv")) { @@ -134,7 +138,7 @@ public class PathCheckPaneController implements Initializable { // 保存生成的文件路径 logicalAddressFilePath = finalOutputFile.getAbsolutePath(); - log.info("选择的输出文件路径: {}", logicalAddressFilePath); + log.info(LoggerHelper.DEBUG_MARKER, "选择的输出文件路径: {}", logicalAddressFilePath); // 创建后台任务来处理文件生成 Thread backgroundThread = new Thread(() -> { // 获取当前选择的文件夹类型 @@ -180,7 +184,7 @@ public class PathCheckPaneController implements Initializable { return; } - FileChooser fileChooser = new FileChooser(); + FileChooser fileChooser = System.getFileChooser(); fileChooser.setTitle("选择保存物理地址文件的位置"); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv")); fileChooser.setInitialFileName("物理地址文件.csv"); @@ -191,6 +195,7 @@ public class PathCheckPaneController implements Initializable { result2TA.setText("未选择保存位置"); return; } + System.setLastModifiedFile(outputFile); // 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加 final File finalOutputFile; @@ -240,7 +245,7 @@ public class PathCheckPaneController implements Initializable { */ @FXML void onStart(ActionEvent actionEvent) { - log.info("用户点击了开始对比按钮"); + log.info(LoggerHelper.DEBUG_MARKER, "用户点击了开始对比按钮"); // 检查是否已生成两个文件 if (logicalAddressFilePath == null || physicalAddressFilePath == null) { @@ -248,8 +253,8 @@ public class PathCheckPaneController implements Initializable { return; } - log.info("逻辑地址文件路径为:{}", logicalAddressFilePath); - log.info("物理地址文件路径为:{}", physicalAddressFilePath); + log.info(LoggerHelper.DEBUG_MARKER, "逻辑地址文件路径为:{}", logicalAddressFilePath); + log.info(LoggerHelper.DEBUG_MARKER, "物理地址文件路径为:{}", physicalAddressFilePath); // 使用新创建的核心类进行文件比较 AddressFileComparator comparator = new AddressFileComparator(); @@ -323,13 +328,23 @@ public class PathCheckPaneController implements Initializable { */ enum Mode { /** - * Jpg mode. + * Jpg mode. 文件以JPG */ - PAGE_TYPE("jpg"), + PAGE_TYPE("jpg") { + @Override + public String toString() { + return "页面级"; + } + }, /** - * Pdf mode. + * Pdf mode. 文件以PDF */ - FILE_TYPE("pdf"); + FILE_TYPE("pdf") { + @Override + public String toString() { + return "文件级"; + } + }; /** * The Id. */ diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/ProjectInfoPaneController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/ProjectInfoPaneController.java index 3aafc57..2dfc41c 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/ProjectInfoPaneController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/ProjectInfoPaneController.java @@ -2,7 +2,9 @@ package top.r3944realms.docchecktoolrefactored.ui.module; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.scene.control.Dialog; import javafx.scene.control.TextField; +import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil; /** * The type Project info pane controller. @@ -15,12 +17,13 @@ public class ProjectInfoPaneController { @FXML private TextField fileYearTF; @FXML void onReset(ActionEvent event) { - // 清空所有文本字段 - projectNameTF.clear(); - fileYearTF.clear(); - fileCategoriesTF.clear(); - totalCatalogNumberTF.clear(); - AcceptanceTimeTF.clear(); + if (DialogUtil.showConfirmationDialog("确认","是否清除(该操作不可逆)", "")){// 清空所有文本字段 + projectNameTF.clear(); + fileYearTF.clear(); + fileCategoriesTF.clear(); + totalCatalogNumberTF.clear(); + AcceptanceTimeTF.clear(); + } } } 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 db36bc0..490fa38 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java @@ -1,4 +1,3 @@ -// StorageCarrierPaneController.java package top.r3944realms.docchecktoolrefactored.ui.module; import javafx.concurrent.Task; @@ -11,8 +10,10 @@ import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Stage; 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 java.io.File; import java.io.IOException; @@ -52,12 +53,13 @@ public class StorageCarrierPaneController { @FXML void onSelectLD(ActionEvent event) { - log.info("用户点击选择文件夹按钮"); - DirectoryChooser directoryChooser = new DirectoryChooser(); + log.info(LoggerHelper.DEBUG_MARKER, "用户点击选择文件夹按钮"); + DirectoryChooser directoryChooser = System.getDirectoryChooser(); directoryChooser.setTitle("选择要检查的文件夹(页面级文件夹和文件级文件夹等不包括目录文件夹)"); File selectedFolder = directoryChooser.showDialog(new Stage()); if (selectedFolder != null) { + System.setLastModifiedFile(selectedFolder); String currentText = loadDigitalOutcomes.getText(); String folderPath = selectedFolder.getAbsolutePath(); @@ -79,18 +81,18 @@ public class StorageCarrierPaneController { loadDigitalOutcomes.setText(currentText + File.pathSeparator + folderPath); } } - log.info("用户选择了文件夹: {}", selectedFolder.getAbsolutePath()); + log.info(LoggerHelper.DEBUG_MARKER, "用户选择了文件夹: {}", selectedFolder.getAbsolutePath()); } else { - log.info("用户取消了文件夹选择"); + log.info(LoggerHelper.DEBUG_MARKER, "用户取消了文件夹选择"); } } @FXML void onClearSelectedFolders(ActionEvent event) { - log.info("用户点击清除已选择文件夹按钮"); + log.info(LoggerHelper.DEBUG_MARKER, "用户点击清除已选择文件夹按钮"); loadDigitalOutcomes.setText(""); result7TA.setText("已清除所有已选择的文件夹"); - log.info("已清除所有已选择的文件夹"); + log.info(LoggerHelper.DEBUG_MARKER, "已清除所有已选择的文件夹"); } @@ -98,54 +100,56 @@ public class StorageCarrierPaneController { @FXML void onSelectLC(ActionEvent event) { - log.info("用户点击选择RAR文件按钮"); - FileChooser fileChooser = new FileChooser(); + log.info(LoggerHelper.DEBUG_MARKER, "用户点击选择RAR文件按钮"); + FileChooser fileChooser = System.getFileChooser(); fileChooser.setTitle("选择一个 .rar 文件"); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("RAR Files", "*.rar")); File selectedFile = fileChooser.showOpenDialog(new Stage()); if (selectedFile != null) { + System.setLastModifiedFile(selectedFile); loadCompressedFile.setText(selectedFile.getAbsolutePath()); - log.info("用户选择了RAR文件: {}", selectedFile.getAbsolutePath()); + log.info(LoggerHelper.DEBUG_MARKER, "用户选择了RAR文件: {}", selectedFile.getAbsolutePath()); } else { - log.info("用户取消了RAR文件选择"); + log.info(LoggerHelper.DEBUG_MARKER, "用户取消了RAR文件选择"); } + } @FXML void onCaculateHash(ActionEvent event) { - log.info("开始计算RAR文件的MD5哈希值"); + log.info(LoggerHelper.DEBUG_MARKER, "开始计算RAR文件的MD5哈希值"); String filePath = loadCompressedFile.getText(); if (filePath == null || filePath.isEmpty()) { - log.warn("未选择RAR文件,无法计算哈希值"); + log.warn(LoggerHelper.DEBUG_MARKER, "未选择RAR文件,无法计算哈希值"); result7TA.setText("请先选择一个 .rar 文件"); return; } File file = new File(filePath); if (!file.exists() || !file.isFile() || !filePath.endsWith(".rar")) { - log.warn("选择的文件无效或不是RAR文件: {}", filePath); + log.warn(LoggerHelper.DEBUG_MARKER, "选择的文件无效或不是RAR文件: {}", filePath); result7TA.setText("所选文件不存在或不是一个有效的 .rar 文件"); return; } try { - log.info("开始计算文件哈希值: {}", filePath); + log.info(LoggerHelper.DEBUG_MARKER, "开始计算文件哈希值: {}", filePath); MD5HashCalculator hashCalculator = new MD5HashCalculator(); String hashResult = hashCalculator.calculateHash(file.toPath()); result7TA.setText("计算结果:\n" + hashResult); - log.info("文件哈希值计算完成: {}", hashResult); + log.info(LoggerHelper.DEBUG_MARKER, "文件哈希值计算完成: {}", hashResult); } catch (IOException e) { - log.error("计算文件哈希值时出错: {}", filePath, e); + log.error(LoggerHelper.DEBUG_MARKER, "计算文件哈希值时出错: {}", filePath, e); result7TA.setText("计算哈希值时出错: " + e.getMessage()); } } @FXML void onGenerateHF(ActionEvent event) { - log.info("开始生成哈希列表文件"); + log.info(LoggerHelper.DEBUG_MARKER, "开始生成哈希列表文件"); String folderPathsText = loadDigitalOutcomes.getText(); if (folderPathsText == null || folderPathsText.isEmpty()) { - log.warn("未选择文件夹,无法生成哈希列表文件"); + log.warn(LoggerHelper.DEBUG_MARKER, "未选择文件夹,无法生成哈希列表文件"); result7TA.setText("请先选择一个文件夹"); return; } @@ -159,13 +163,13 @@ public class StorageCarrierPaneController { if (folder.exists() && folder.isDirectory()) { folders.add(folder); } else { - log.warn("选择的路径无效或不是文件夹: {}", path); + log.warn(LoggerHelper.DEBUG_MARKER, "选择的路径无效或不是文件夹: {}", path); result7TA.setText("所选路径不存在或不是一个有效的文件夹: " + path); return; } } - FileChooser fileChooser = new FileChooser(); + FileChooser fileChooser = System.getFileChooser(); fileChooser.setTitle("选择保存哈希列表文件的位置"); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv")); @@ -176,7 +180,7 @@ public class StorageCarrierPaneController { File outputFile = fileChooser.showSaveDialog(selectLoadDigitalOutcomes7B.getScene().getWindow()); if (outputFile == null) { - log.info("用户取消了文件保存操作"); + log.info(LoggerHelper.DEBUG_MARKER, "用户取消了文件保存操作"); result7TA.setText("未选择保存位置"); return; } @@ -189,13 +193,13 @@ public class StorageCarrierPaneController { finalOutputFile = outputFile; } - log.info("选择的输出文件路径: {}", finalOutputFile.getAbsolutePath()); + log.info(LoggerHelper.DEBUG_MARKER, "选择的输出文件路径: {}", finalOutputFile.getAbsolutePath()); // 创建后台任务 - Task task = new Task() { + Task task = new Task<>() { @Override protected String call() throws Exception { - log.info("开始执行哈希文件生成任务"); + log.info(LoggerHelper.DEBUG_MARKER, "开始执行哈希文件生成任务"); updateMessage("开始生成哈希文件..."); HashFileGenerator generator = new HashFileGenerator(); @@ -209,11 +213,11 @@ public class StorageCarrierPaneController { updateProgress(current, total); updateMessage("处理文件: " + current + "/" + total); if (current % 500 == 0 || current == total) { // 每500个文件或完成时记录一次日志 - log.info("处理进度: {}/{}", current, total); + log.info(LoggerHelper.DEBUG_MARKER, "处理进度: {}/{}", current, total); } }); - log.info("哈希文件生成任务完成,输出文件: {}", finalOutputFile.getAbsolutePath()); + log.info(LoggerHelper.DEBUG_MARKER, "哈希文件生成任务完成,输出文件: {}", finalOutputFile.getAbsolutePath()); return "哈希列表文件已生成: " + finalOutputFile.getAbsolutePath(); } }; @@ -225,7 +229,7 @@ public class StorageCarrierPaneController { // 任务成功完成 task.setOnSucceeded(e -> { - log.info("哈希文件生成任务成功完成"); + log.info(LoggerHelper.DEBUG_MARKER, "哈希文件生成任务成功完成"); result7TA.setText(task.getValue()); }); @@ -233,13 +237,13 @@ public class StorageCarrierPaneController { task.setOnFailed(e -> { Throwable exception = task.getException(); String errorMsg = "生成哈希文件时出错: " + (exception != null ? exception.getMessage() : "未知错误"); - log.error("哈希文件生成任务失败", exception); + log.error(LoggerHelper.RELEASE_MARKER, "哈希文件生成任务失败", exception); result7TA.setText(errorMsg); }); // 任务取消处理 task.setOnCancelled(e -> { - log.info("哈希文件生成任务被用户取消"); + log.info(LoggerHelper.DEBUG_MARKER, "哈希文件生成任务被用户取消"); result7TA.setText("哈希文件生成操作已取消"); }); 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 74178fa..df2fd34 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java @@ -2,28 +2,23 @@ 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.DuplicateFinder; import top.r3944realms.docchecktoolrefactored.core.MD5HashCalculator; -import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner; +import top.r3944realms.docchecktoolrefactored.core.ScanningException; import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner; +import top.r3944realms.docchecktoolrefactored.model.DuplicateGroup; -import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; @Slf4j -public class DuplicateDocumentDetectionTask extends Task { +public class DuplicateDocumentDetectionTask extends Task{ private final String folderPath; private final MD5HashCalculator hashCalculator; private volatile RobustParallelScanner scanner; @@ -33,137 +28,166 @@ public class DuplicateDocumentDetectionTask extends Task { this.hashCalculator = new MD5HashCalculator(); } + @Override protected String call() throws Exception { updateMessage("正在初始化扫描..."); + Path rootPath = Paths.get(folderPath); if (!Files.exists(rootPath) || !Files.isDirectory(rootPath)) { throw new IllegalArgumentException("指定路径不是有效目录: " + folderPath); } - - // 使用 RobustParallelScanner 和 MD5HashCalculator 进行并行扫描和哈希计算 - Map> hashToFileMap = new ConcurrentHashMap<>(); - AtomicInteger processed = new AtomicInteger(0); - AtomicReference errorRef = new AtomicReference<>(null); - AtomicBoolean scanCompleted = new AtomicBoolean(false); - - // 使用 CountDownLatch 等待扫描完成 - CountDownLatch latch = new CountDownLatch(1); - - // 创建扫描器 + // 创建带进度更新的扫描器 scanner = new RobustParallelScanner(10); - // 异步启动扫描任务 - Thread scanThread = new Thread(() -> { + // 创建带有进度监听的 DuplicateFinder + DuplicateFinder duplicateFinder = new DuplicateFinder(scanner, hashCalculator, true) + .applySetting(System.getSetting()); + + // 用于统计文件总数 + AtomicInteger totalFiles = new AtomicInteger(0); + // 使用 RobustParallelScanner 和 MD5HashCalculator 进行并行扫描和哈希计算 + // 设置进度回调 + duplicateFinder.setProgressCallback(new DuplicateFinder.ProgressCallback() { + @Override + public void onPhaseStarted(DuplicateFinder.Phase phase) { + switch (phase) { + case GROUP_BY_SIZE: + updateMessage("正在按文件大小分组..."); + break; + case CALCULATE_HASH: + updateMessage("正在计算可能重复文件的哈希值..."); + break; + } + } + + @Override + public void onPhaseProgress(DuplicateFinder.Phase phase, int current, int total) { + if (total > 0) { + updateProgress(current, total); + switch (phase) { + case GROUP_BY_SIZE: + totalFiles.set(total); + updateMessage(String.format("正在按文件大小分组: %d/%d", current, total)); + break; + case CALCULATE_HASH: + updateMessage(String.format("正在计算哈希值: %d/%d", current, total)); + break; + } + } + } + + @Override + public void onPhaseCompleted(DuplicateFinder.Phase phase) { + switch (phase) { + case GROUP_BY_SIZE: + updateMessage("文件大小分组完成"); + break; + case CALCULATE_HASH: + updateMessage("哈希值计算完成"); + break; + } + } + }); + + AtomicReference> resultRef = new AtomicReference<>(); + List errors = new CopyOnWriteArrayList<>(); + CountDownLatch latch = new CountDownLatch(1); + + // 在单独线程中执行查找 + Thread findThread = new Thread(() -> { try { - scanner.scanWithProgress(rootPath, new FileScanner.ProgressAwareListener() { - @Override - public void onFileFound(Path file) { - if (isCancelled()) { - scanner.cancel(); - return; - } - try { - String hash = hashCalculator.calculatePartialHash(file); - hashToFileMap.computeIfAbsent(hash, k -> new ArrayList<>()).add(file); - } catch (IOException e) { - // 记录无法计算哈希的文件,但不中断整个过程 - updateMessage("警告: 无法处理文件 " + file.toString() + " - " + e.getMessage()); - } - processed.incrementAndGet(); - } - - @Override - public void onError(Path path, Exception e) { - // 记录错误但不中断扫描过程 - errorRef.set(e); - updateMessage("扫描错误: " + path.toString() + " - " + e.getMessage()); - } - - @Override - public void onScanComplete() { - // 扫描完成 - scanCompleted.set(true); - latch.countDown(); - } - - @Override - public void onProgressUpdate(int current, int total) { - if (isCancelled()) { - scanner.cancel(); - return; - } - updateProgress(current, total); - updateMessage("正在处理文件: " + current + "/" + total); - if (current % 500 == 0 || current == total) { // 每500个文件或完成时记录一次日志 - log.info("处理进度: {}/{}", current, total); - } - } - }); + List duplicates = duplicateFinder.findDuplicates(rootPath); + resultRef.set(duplicates); } catch (Exception e) { - errorRef.set(e); + errors.add(e); + } finally { latch.countDown(); } }); - scanThread.setDaemon(true); - scanThread.start(); + findThread.setDaemon(true); + findThread.start(); - // 等待扫描完成,设置更长的超时时间(例如5分钟) - if (!latch.await(5*60, TimeUnit.MINUTES)) { + // 等待扫描完成,设置超时时间(例如5分钟) + long totalTimeout = System.getSetting().getTotalTimeout(); + if (!latch.await(totalTimeout, TimeUnit.SECONDS)) { scanner.cancel(); - throw new TimeoutException("扫描超时(5分钟)"); + throw new TimeoutException(String.format("扫描超时(%d秒)", totalTimeout)); } // 检查是否被取消 - if (isCancelled()) { + long start = java.lang.System.currentTimeMillis(); + try { + boolean finished = false; + while (!finished) { + // 每 200ms 等待一次 latch,避免忙等待 + finished = latch.await(200, TimeUnit.MILLISECONDS); + + // 检查是否被取消 + if (isCancelled()) { + scanner.cancel(); + return "操作已被取消"; + } + + // 检查是否超时 + if (java.lang.System.currentTimeMillis() - start > totalTimeout * 1000L) { + scanner.cancel(); + throw new TimeoutException(String.format("扫描超时(%d秒)", totalTimeout)); + } + } + } catch (InterruptedException e) { scanner.cancel(); - return "操作已被取消"; + Thread.currentThread().interrupt(); + return "操作被中断"; } - // 如果有错误且扫描未完成,抛出异常 - if (errorRef.get() != null && !scanCompleted.get()) { - throw errorRef.get(); + // 检查是否有错误 + if (!errors.isEmpty()) { + throw new ScanningException(errors); } - // 分析重复文件并构建结果 - updateMessage("正在分析重复文件..."); + List duplicateGroups = resultRef.get(); + + // 构建最终结果 StringBuilder result = new StringBuilder(); - result.append("重复文件检测结果:\n"); - if (errorRef.get() != null) { - result.append("警告: 扫描过程中发生错误 - ").append(errorRef.get().getMessage()).append("\n\n"); - } - result.append("总共处理 ").append(processed.get()).append(" 个文件\n"); + // 计算总文件数(所有组中的文件数) + int totalDuplicateFiles = duplicateGroups.stream() + .mapToInt(group -> group.fileMetas().size()) + .sum(); - List>> duplicateGroups = hashToFileMap.entrySet().stream() - .filter(entry -> entry.getValue().size() > 1) - .collect(Collectors.toList()); + int totalGroups = duplicateGroups.size(); + + result.append(String.format("总共扫描文件数: %d\n", totalFiles.get())); + result.append(String.format("发现重复文件组数: %d\n", totalGroups)); + result.append(String.format("重复文件总数: %d\n", totalDuplicateFiles)); if (!duplicateGroups.isEmpty()) { - result.append("有 ").append(duplicateGroups.size()).append(" 组重复文件\n\n"); + result.append("\n详细重复文件信息:\n"); + result.append("----------------------------------------\n"); int groupIndex = 1; - for (Map.Entry> entry : duplicateGroups) { - result.append("第 ").append(groupIndex).append(" 组\t"); - result.append("哈希值: ").append(entry.getKey()).append("\n"); + for (DuplicateGroup group : duplicateGroups) { + result.append(String.format("第 %d 组 (哈希值: %s, 大小: %d 字节)\n", + groupIndex, group.hash(), group.size())); int fileIndex = 1; - for (Path file : entry.getValue()) { - result.append("文件名").append(fileIndex).append(": ").append(file.getFileName()).append("\t\t"); - result.append("文件路径").append(fileIndex).append(": ").append(file.toAbsolutePath()).append("\n"); + for (var file : group.fileMetas()) { + Path filePath = file.getPath(); + result.append(String.format(" 文件%d: %s\n", fileIndex, filePath.toAbsolutePath())); fileIndex++; } result.append("\n"); groupIndex++; } } else { - result.append("没有重复文件\n"); + result.append("\n没有发现重复文件\n"); } - updateMessage("检测完成!"); + result.append("检测完成!\n"); return result.toString(); } 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 ba7f44e..5125a37 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java @@ -2,6 +2,7 @@ package top.r3944realms.docchecktoolrefactored.ui.utils; import javafx.application.Platform; import javafx.scene.control.Alert; +import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonType; import javafx.stage.Window; @@ -18,14 +19,7 @@ public class DialogUtil { * @return the boolean */ public static boolean showExitConfirmation(Window owner) { - Alert alert = new Alert(Alert.AlertType.CONFIRMATION); - alert.initOwner(owner); - alert.setTitle("确认退出"); - alert.setHeaderText("您确定要退出程序吗?"); - alert.setContentText("请确认您的操作"); - - Optional result = alert.showAndWait(); - return result.isPresent() && result.get() == ButtonType.OK; + return showConfirmationDialog("确认退出", "您确定要退出程序吗?", "请确认您的操作"); } /** @@ -41,8 +35,11 @@ public class DialogUtil { alert.setTitle(title); alert.setHeaderText(header); alert.setContentText(content); + ButtonType yesButton = new ButtonType("Yes", ButtonBar.ButtonData.YES); + ButtonType noButton = new ButtonType("No", ButtonBar.ButtonData.NO); + alert.getButtonTypes().setAll(yesButton, noButton); Optional result = alert.showAndWait(); - return result.isPresent() && result.get() == ButtonType.OK; + return result.isPresent() && result.get() == yesButton; } /** diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/ProgressBarUtil.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/ProgressBarUtil.java new file mode 100644 index 0000000..b88a1ac --- /dev/null +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/ProgressBarUtil.java @@ -0,0 +1,150 @@ +package top.r3944realms.docchecktoolrefactored.ui.utils; + +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; +import javafx.stage.StageStyle; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * 进度条窗口工具类,支持取消按钮 + */ +public class ProgressBarUtil { + + private Stage progressStage; + private ProgressBar progressBar; + private Label messageLabel; + private Button cancelButton; + private final AtomicBoolean cancelled = new AtomicBoolean(false); + private Runnable onCancelCallback; + /** + * 显示进度条窗口 + * @param ownerStage 父窗口 + * @param title 窗口标题 + * @param initialMessage 初始消息 + */ + public void showProgress(Stage ownerStage, String title, String initialMessage) { + Platform.runLater(() -> { + progressStage = new Stage(); + progressStage.initOwner(ownerStage); + progressStage.initStyle(StageStyle.UTILITY); + progressStage.initModality(Modality.APPLICATION_MODAL); + progressStage.setTitle(title); + progressStage.setResizable(false); + + // 创建进度条 + progressBar = new ProgressBar(); + progressBar.setPrefWidth(300); + progressBar.setProgress(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) { + 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;"); + + Scene scene = new Scene(root); + progressStage.setScene(scene); + progressStage.sizeToScene(); + progressStage.show(); + }); + } + + /** + * 更新进度和消息 + * @param progress 进度值 (0.0 - 1.0) + * @param message 要显示的消息 + */ + public void updateProgress(double progress, String message) { + Platform.runLater(() -> { + if (progressBar != null) { + progressBar.setProgress(progress); + } + if (messageLabel != null) { + messageLabel.setText(message); + } + }); + } + + /** + * 关闭进度条窗口 + */ + public void closeProgress() { + Platform.runLater(() -> { + if (progressStage != null) { + progressStage.close(); + } + }); + } + + /** + * 是否已取消 + */ + public boolean isCancelled() { + return cancelled.get(); + } + + /** + * 快速显示一个进度条窗口并执行任务 + * @param ownerStage 父窗口 + * @param title 窗口标题 + * @param initialMessage 初始消息 + * @param task 要执行的任务(可检查 isCancelled() 中途退出) + */ + public static void showAndExecute(Stage ownerStage, String title, String initialMessage, CancellableTask task) { + ProgressBarUtil progressBarUtil = new ProgressBarUtil(); + progressBarUtil.showProgress(ownerStage, title, initialMessage); + + new Thread(() -> { + try { + task.run(progressBarUtil); + } finally { + progressBarUtil.closeProgress(); + } + }).start(); + } + @FunctionalInterface + public interface CancellableTask { + void run(ProgressBarUtil 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; + } + } +} diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/util/FileUtil.java b/src/main/java/top/r3944realms/docchecktoolrefactored/util/FileUtil.java index 6b79207..9ad95cb 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("文件不存在: {}", filePath); + log.error(LoggerHelper.TRACE_MARKER, "文件不存在: {}", filePath); throw new NoSuchFileException("文件不存在: " + filePath); } @@ -68,7 +68,7 @@ public class FileUtil { File file = path.toFile(); if (!file.exists()) { - log.error("文件不存在: {}", filePath); + log.error(LoggerHelper.TRACE_MARKER, "文件不存在: {}", filePath); throw new NoSuchFileException("文件不存在: " + filePath); } @@ -88,7 +88,7 @@ public class FileUtil { .toList(); if (!supportedExtensions.contains(fileExtension)) { - log.error("不支持的文件格式: {}", fileExtension); + log.error(LoggerHelper.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/LoggerHelper.java new file mode 100644 index 0000000..09d7345 --- /dev/null +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/util/LoggerHelper.java @@ -0,0 +1,10 @@ +package top.r3944realms.docchecktoolrefactored.util; + +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +public class LoggerHelper { + 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/css/custom-tab.css b/src/main/resources/css/custom-tab.css index 7c6cee3..35aa07d 100644 --- a/src/main/resources/css/custom-tab.css +++ b/src/main/resources/css/custom-tab.css @@ -8,7 +8,7 @@ /* 默认Tab形状容器 */ .tab .tab-container { - -fx-background-color: #13b72b; + -fx-background-color: #7BB0D9; -fx-shape: "M0,5 L65,5 L75,15 L65,25 L0,25 L10,15 Z"; -fx-background-insets: 0; -fx-border-width: 0; /* 确保无边框 */ @@ -37,7 +37,7 @@ } .tab:selected .tab-container { - -fx-background-color: #9b0e0e; /* 只改变背景色 */ + -fx-background-color: #0063b0; /* 只改变背景色 */ -fx-effect: null; /* 移除所有效果 */ } diff --git a/src/main/resources/fxml/login-view.fxml b/src/main/resources/fxml/login-view.fxml index a782a37..c95fe1b 100644 --- a/src/main/resources/fxml/login-view.fxml +++ b/src/main/resources/fxml/login-view.fxml @@ -17,7 +17,7 @@ -

- - - - - - + + + + + - + + + - + - + @@ -66,5 +76,34 @@ + + + + + + + + + diff --git a/src/main/resources/fxml/module/step-1-pane.fxml b/src/main/resources/fxml/module/step-1-pane.fxml index db27a7e..93a01bf 100644 --- a/src/main/resources/fxml/module/step-1-pane.fxml +++ b/src/main/resources/fxml/module/step-1-pane.fxml @@ -10,7 +10,7 @@ - + @@ -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 7afdef4..eb3fa6d 100644 --- a/src/main/resources/fxml/module/step-5-pane.fxml +++ b/src/main/resources/fxml/module/step-5-pane.fxml @@ -5,12 +5,12 @@ - + - diff --git a/src/main/resources/fxml/module/step-6-pane.fxml b/src/main/resources/fxml/module/step-6-pane.fxml index 00ec030..cb5c5ba 100644 --- a/src/main/resources/fxml/module/step-6-pane.fxml +++ b/src/main/resources/fxml/module/step-6-pane.fxml @@ -5,9 +5,9 @@ - + - diff --git a/src/main/resources/fxml/module/step-7-pane.fxml b/src/main/resources/fxml/module/step-7-pane.fxml index 31d8b5e..2ef8926 100644 --- a/src/main/resources/fxml/module/step-7-pane.fxml +++ b/src/main/resources/fxml/module/step-7-pane.fxml @@ -10,7 +10,7 @@ - + @@ -18,11 +18,10 @@ - - - - - + + + + -