diff --git a/build.gradle b/build.gradle index bd97633..d461f6c 100644 --- a/build.gradle +++ b/build.gradle @@ -170,7 +170,7 @@ tasks.register('buildPortable', Exec) { '--vendor', 'r3944realms', '--dest', "$buildDir/distributions", '--java-options', '-Dfile.encoding=UTF-8', - '--java-options', '-Xmx512m', + '--java-options', '-Xmx4G', '--java-options', '-Xms256m', '--verbose', '--icon', file('src/main/resources/img/logo256x.ico').absolutePath diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java b/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java index 5e3bbfa..5e0fc9e 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java @@ -8,17 +8,24 @@ import top.r3944realms.docchecktoolrefactored.ui.SceneManager; import java.util.Objects; public class JavaFxApplication extends Application { + public Image logo = new Image(Objects.requireNonNull(getClass().getResourceAsStream("/img/icon.jpg"))); @Override public void init() throws Exception { super.init(); + System.setVersion("1.0.0-beta"); } @Override - public void start(Stage primaryStage) throws Exception { - SceneManager.init(primaryStage); - primaryStage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/img/icon.jpg")))); + public void start(Stage primaryStage) { + SceneManager.init(primaryStage, logo); SceneManager.switchLoginView(); primaryStage.show(); } + @Override + public void stop() throws Exception { + // 关闭所有线程池 + ThreadPoolManager.shutdownAll(); + super.stop(); + } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/Main.java b/src/main/java/top/r3944realms/docchecktoolrefactored/Main.java index cc35809..1ac3551 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/Main.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/Main.java @@ -2,7 +2,6 @@ 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; diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/System.java b/src/main/java/top/r3944realms/docchecktoolrefactored/System.java index e351ce1..e09ca8e 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/System.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/System.java @@ -2,13 +2,16 @@ package top.r3944realms.docchecktoolrefactored; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import top.r3944realms.docchecktoolrefactored.core.Setting; import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.*; import java.nio.charset.StandardCharsets; -import java.nio.file.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Properties; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -16,6 +19,8 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; public enum System { INSTANCE; private volatile Setting setting; + @Getter + private volatile String version; private volatile File lastModifiedFile; private static final String CONFIG_FILE_NAME = "config.ini"; private static final Properties properties = new Properties(); @@ -106,8 +111,8 @@ public enum System { /** 将Setting对象转换为Properties */ private static void settingToProperties(Setting setting, Properties props) { - props.setProperty("singleTimeout", String.valueOf(setting.getScanTimeout())); - props.setProperty("totalTimeout", String.valueOf(setting.getTaskTimeout())); + props.setProperty("scanTimeOutS", String.valueOf(setting.getScanTimeout())); + props.setProperty("taskTimeOutS", String.valueOf(setting.getTaskTimeout())); props.setProperty("enableStep", String.valueOf(setting.isEnableStep())); } @@ -206,4 +211,10 @@ public enum System { public static Integer getAvailableProcessors() { return Runtime.getRuntime().availableProcessors(); } + public static String version() { + return System.INSTANCE.getVersion(); + } + public static void setVersion(String version) { + System.INSTANCE.version = version; + } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ThreadPoolManager.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ThreadPoolManager.java new file mode 100644 index 0000000..2553087 --- /dev/null +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ThreadPoolManager.java @@ -0,0 +1,43 @@ +package top.r3944realms.docchecktoolrefactored; + +import java.util.Map; +import java.util.concurrent.*; + +public class ThreadPoolManager { + private static final Map pools = new ConcurrentHashMap<>(); + + public static ExecutorService createPool(String name, int size) { + return createPool(name, size, true); // 默认使用守护线程 + } + + public static ExecutorService createPool(String name, int size, boolean daemon) { + ThreadFactory factory = daemon ? + r -> { + Thread t = new Thread(r, name + "-thread"); + t.setDaemon(true); + return t; + } : + r -> new Thread(r, name + "-thread"); + + ExecutorService pool = Executors.newFixedThreadPool(size, factory); + pools.put(name, pool); + return pool; + } + + public static void shutdownAll() { + pools.forEach((name, pool) -> { + if (!pool.isShutdown()) { + try { + pool.shutdown(); + if (!pool.awaitTermination(5, TimeUnit.SECONDS)) { + pool.shutdownNow(); + } + } catch (InterruptedException e) { + pool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + }); + pools.clear(); + } +} diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileComparator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileComparator.java index e45f22a..c78cbc7 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileComparator.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileComparator.java @@ -5,6 +5,7 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import top.r3944realms.docchecktoolrefactored.System; +import top.r3944realms.docchecktoolrefactored.ThreadPoolManager; import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.BufferedReader; @@ -16,7 +17,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -162,14 +163,16 @@ public class AddressFileComparator { } private final ExecutorService executor; public AddressFileComparator(int threadPoolSize) { - this.executor = Executors.newFixedThreadPool(threadPoolSize); + this.executor = ThreadPoolManager.createPool("address-comparator", threadPoolSize); } public AddressFileComparator() { - this.executor = Executors.newFixedThreadPool(System.getAvailableProcessors()); + this.executor = ThreadPoolManager.createPool("address-comparator", System.getAvailableProcessors()); } @Setter private ProgressCallback progressCallback; + + // 安全调用回调方法 private void safeOnPhaseStarted(Phase phase) { if (progressCallback != null) { @@ -189,7 +192,25 @@ public class AddressFileComparator { } } public void shutdown() { - executor.shutdown(); + executor.shutdown(); // 先尝试正常关闭 + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); // 强制关闭 + // 等待一段时间让任务响应中断 + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + log.error(LoggerMarker.DEBUG_MARKER, "线程池无法正常关闭"); + } + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + public void safeShutdown() { + if (!executor.isShutdown()) { + shutdown(); + } } public CompletableFuture compareFiles(String physicalFilePath, String logicalFilePath, CompareMode compareMode) { diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java index 152bab4..88e8da5 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import top.r3944realms.docchecktoolrefactored.System; +import top.r3944realms.docchecktoolrefactored.ThreadPoolManager; import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner; import top.r3944realms.docchecktoolrefactored.model.DuplicateGroup; import top.r3944realms.docchecktoolrefactored.model.FileMetadata; @@ -13,7 +14,10 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; -import java.util.concurrent.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -21,7 +25,6 @@ import java.util.stream.Collectors; /** * 重复文件查找核心类 */ -//TODO;代替DuplicateDocumentDetectionTask @Slf4j public class DuplicateFinder { private final FileScanner fileScanner; @@ -49,7 +52,7 @@ public class DuplicateFinder { this.fileScanner = Objects.requireNonNull(fileScanner); this.hashCalculator = Objects.requireNonNull(hashCalculator); this.enableProgress = enableProgress; - this.executorService = Executors.newFixedThreadPool(System.getAvailableProcessors()); + this.executorService = ThreadPoolManager.createPool("duplicate-finder-pool", System.getAvailableProcessors()); } public DuplicateFinder(FileScanner fileScanner, FileHashCalculator hashCalculator) { this(fileScanner, hashCalculator, false); diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java index 297ccf1..2901ed3 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java @@ -1,7 +1,9 @@ package top.r3944realms.docchecktoolrefactored.core; - +import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import top.r3944realms.docchecktoolrefactored.System; +import top.r3944realms.docchecktoolrefactored.ThreadPoolManager; import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner; import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner; import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; @@ -13,20 +15,49 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CompletableFuture; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; + +@Setter @Slf4j public class HashFileGenerator { + private ProgressCallback callback; + private final FileHashCalculator hashCalculator; - public interface ProgressListener { + public HashFileGenerator() { + this.hashCalculator = FileHashCalculator.defaultInstance(); + } + + public HashFileGenerator(FileHashCalculator hashCalculator) { + this.hashCalculator = hashCalculator; + } + + public interface ProgressCallback { void onProgressUpdate(int current, int total); } - public void generateHashFile(List directories, Path outputFile, ProgressListener listener) throws IOException, InterruptedException { + private void safeOnProgressUpdate(int current, int total) { + if (callback != null) { + callback.onProgressUpdate(current, total); + } + } + + public void generateHashFile(List directories, Path outputFile) + throws IOException, InterruptedException { + + // 开始时检查中断 + if (Thread.interrupted()) { + throw new InterruptedException("任务被取消"); + } + List allFiles = new ArrayList<>(); // 扫描所有目录中的文件 for (Path directory : directories) { + if (Thread.interrupted()) { + throw new InterruptedException("任务被取消"); + } + if (!Files.isDirectory(directory)) { throw new IllegalArgumentException("指定路径不是有效目录: " + directory); } @@ -49,46 +80,115 @@ public class HashFileGenerator { @Override public void onError(Path path, Exception e) { - log.error(LoggerMarker.TRACE_MARKER, "Error scanning path: {} - {}", path, e.getMessage()); + log.error(LoggerMarker.TRACE_MARKER, "扫描错误: {} - {}", path, e.getMessage()); } }); - // 等待扫描完成 - scanFuture.join(); + // 使用带超时的等待,并响应中断 + try { + scanFuture.get(System.getSetting().getScanTimeout(), TimeUnit.SECONDS); // 设置合理的超时时间 + } catch (TimeoutException e) { + throw new IOException("扫描超时: " + directory, e); + } catch (ExecutionException e) { + throw new IOException("扫描失败: " + directory, e); + } + allFiles.addAll(files); } } + // 检查是否被中断 + if (Thread.interrupted()) { + throw new InterruptedException("任务被取消"); + } + // 计算每个文件的哈希值 List hashResults = new ArrayList<>(); AtomicInteger processedFiles = new AtomicInteger(0); int totalFiles = allFiles.size(); - allFiles.parallelStream().forEach(file -> { - try { - String hash = new MD5HashCalculator().calculatePartialHash(file); - String[] result = {file.getFileName().toString(), hash}; - synchronized (hashResults) { - hashResults.add(result); - } - int processed = processedFiles.incrementAndGet(); - if (listener != null) { - listener.onProgressUpdate(processed, totalFiles); - } - } catch (IOException e) { - log.error(LoggerMarker.DEBUG_MARKER, "无法计算该文件哈希值: {} - {}", file, e.getMessage()); - } - }); + processFilesInParallel(allFiles, hashResults, processedFiles, totalFiles); + + // 检查是否被中断 + if (Thread.interrupted()) { + throw new InterruptedException("任务被取消"); + } // 写入结果到文件 - try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile.toFile()))) { - writer.write("文件名,哈希值"); - writer.newLine(); - for (String[] result : hashResults) { - writer.write(result[0] + "," + result[1]); - writer.newLine(); + writeResultsToFile(outputFile, hashResults); + } + + private void processFilesInParallel(List files, List results, + AtomicInteger processedFiles, int totalFiles) + throws InterruptedException { + + int processors = Runtime.getRuntime().availableProcessors(); + ExecutorService executor = ThreadPoolManager.createPool("hash-generator", Runtime.getRuntime().availableProcessors()); + + try { + CountDownLatch latch = new CountDownLatch(files.size()); + + for (Path file : files) { + executor.submit(() -> { + try { + // 检查中断 + if (Thread.interrupted()) { + return; + } + + String hash = hashCalculator.calculatePartialHash(file); + String[] result = {file.getFileName().toString(), hash}; + + synchronized (results) { + results.add(result); + } + + int processed = processedFiles.incrementAndGet(); + safeOnProgressUpdate(processed, totalFiles); + + } catch (IOException e) { + log.error(LoggerMarker.DEBUG_MARKER, "无法计算文件哈希值: {} - {}", file, e.getMessage()); + } finally { + latch.countDown(); + } + }); } + + // 等待所有任务完成,但响应中断 + while (!latch.await(100, TimeUnit.MILLISECONDS)) { + if (Thread.interrupted()) { + throw new InterruptedException("哈希计算被取消"); + } + } + + } finally { + executor.shutdownNow(); } } -} + private void writeResultsToFile(Path outputFile, List results) + throws IOException, InterruptedException { + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile.toFile()))) { + writer.write("文件名,哈希值"); + writer.newLine(); + + for (String[] result : results) { + // 检查中断 + if (Thread.interrupted()) { + throw new InterruptedException("文件写入被取消"); + } + + writer.write(result[0] + "," + result[1]); + writer.newLine(); + + // 每写入100行检查一次中断,减少检查频率 + if (results.indexOf(result) % 100 == 0) { + if (Thread.interrupted()) { + throw new InterruptedException("文件写入被取消"); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java index 461d434..00e964a 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java @@ -1,6 +1,5 @@ package top.r3944realms.docchecktoolrefactored.core; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; 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 a1fa2fe..8133961 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/FileScanner.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/FileScanner.java @@ -1,7 +1,6 @@ package top.r3944realms.docchecktoolrefactored.io.scanner; import java.nio.file.Path; -import java.util.Scanner; /** * The interface File scanner. diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/LoginStageController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/LoginStageController.java index 98997c3..54cbc6a 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/LoginStageController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/LoginStageController.java @@ -3,6 +3,7 @@ package top.r3944realms.docchecktoolrefactored.ui; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Button; +import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; import javafx.scene.input.KeyCode; @@ -11,6 +12,7 @@ import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.BorderPane; import lombok.extern.slf4j.Slf4j; +import top.r3944realms.docchecktoolrefactored.System; import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil; import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; @@ -25,11 +27,12 @@ public class LoginStageController implements Initializable { /** * The Login button. */ - public Button loginButton; + @FXML private Button loginButton; /** * The Main pane. */ - public BorderPane mainPane; + @FXML private BorderPane mainPane; + @FXML private Label versionL; @FXML private TextField usernameField; @FXML private PasswordField passwordField; @@ -43,7 +46,7 @@ public class LoginStageController implements Initializable { SceneManager.switchMainView(); } else { log.info(LoggerMarker.DEBUG_MARKER, "Invalid username or password"); - DialogUtil.showErrorDialog("错误", null, "用户名或密码错误!"); + DialogUtil.showErrorDialog("错误", "输入错误", "用户名或密码错误!"); } } @@ -59,5 +62,6 @@ public class LoginStageController implements Initializable { } }) ); + versionL.setText(System.version()); } } \ No newline at end of file diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/MainStageController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/MainStageController.java index b5355cc..b4e424e 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/MainStageController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/MainStageController.java @@ -7,8 +7,11 @@ import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; +import lombok.extern.slf4j.Slf4j; import top.r3944realms.docchecktoolrefactored.System; import top.r3944realms.docchecktoolrefactored.core.Setting; +import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.util.ArrayList; import java.util.List; @@ -16,6 +19,7 @@ import java.util.List; /** * The type Main stage controller. */ +@Slf4j public class MainStageController { @FXML public Button nextB; @@ -115,7 +119,34 @@ public class MainStageController { } @FXML void onExit(ActionEvent actionEvent) { + // 显示退出确认对话框 + boolean confirmExit = DialogUtil.showExitConfirmation(tabPane.getScene().getWindow()); + if (confirmExit) { + // 执行退出操作 + exitApplication(); + } + } + /** + * 退出应用程序 + */ + private void exitApplication() { + try { + // 保存当前设置(如果需要) + System.saveSettingsNow(); + log.info(LoggerMarker.DEBUG_MARKER, "应用程序正常退出"); + + // 关闭应用程序 + SceneManager.getPrimaryStage().close(); + + // 强制退出JVM(确保所有线程都终止) + java.lang.System.exit(0); + + } catch (Exception e) { + // 退出过程中发生错误,仍然强制退出 + log.error(LoggerMarker.DEBUG_MARKER, "退出应用程序时发生错误", e); + java.lang.System.exit(1); // 非正常退出 + } } @FXML void onOpenSetting(ActionEvent actionEvent) { @@ -123,9 +154,11 @@ public class MainStageController { } @FXML void onOpenHelpDoc(ActionEvent actionEvent) { + DialogUtil.showDetailedInformationDialog("未实现", "敬请期待","待完善文档后内置"); } @FXML void onAbout(ActionEvent actionEvent) { + DialogUtil.showDetailedInformationDialog("版本", "版本信息","这里写些信息"); } public void updateStepButtonsVisibility() { Setting setting = System.getSetting(); diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SceneManager.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/SceneManager.java index 2d579b4..40645e2 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.scene.image.Image; import javafx.stage.Modality; import javafx.stage.Stage; import javafx.util.Duration; @@ -32,6 +33,9 @@ public class SceneManager { @Setter private static Stage primaryStage; @Getter + @Setter + public static Image logo; + @Getter private static MainStageController mainController; @Getter private static final List openStages = new ArrayList<>(); @@ -40,9 +44,12 @@ public class SceneManager { * Init. * * @param primaryStage the primary stage + * @param logo the logo image */ - public static void init(Stage primaryStage) { + public static void init(Stage primaryStage, Image logo) { SceneManager.primaryStage = primaryStage; + SceneManager.logo = logo; + primaryStage.getIcons().add(logo); } /** @@ -87,6 +94,7 @@ public class SceneManager { try { Parent root = FXMLLoader.load(Objects.requireNonNull(Main.class.getResource("/fxml/setting-view.fxml"))); Stage settingStage = new Stage(); + settingStage.getIcons().add(logo); settingStage.setTitle("数字化验收工具 - 设置"); Scene scene = new Scene(root, 300, 206); settingStage.setScene(scene); // 默认大小可调 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 6a40dc9..7a4f13c 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/DuplicateDocumentPaneController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/DuplicateDocumentPaneController.java @@ -12,6 +12,7 @@ import lombok.extern.slf4j.Slf4j; import top.r3944realms.docchecktoolrefactored.System; import top.r3944realms.docchecktoolrefactored.ui.SceneManager; import top.r3944realms.docchecktoolrefactored.ui.task.DuplicateDocumentDetectionTask; +import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil; import top.r3944realms.docchecktoolrefactored.ui.utils.ProgressBar; import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; @@ -58,6 +59,7 @@ public class DuplicateDocumentPaneController { if (folderPath == null || folderPath.trim().isEmpty()) { log.warn(LoggerMarker.DEBUG_MARKER, "未选择文件夹,无法进行查重"); result1TA.setText("请选择要检查的文件夹。"); + DialogUtil.showWarningDialog("警告", "操作有误", "请选择要检查的文件夹"); start1B.setDisable(false); return; } @@ -88,6 +90,7 @@ public class DuplicateDocumentPaneController { // 绑定取消按钮 -> task.cancel() progressBar.setOnCancel(() -> { if (currentTask != null && currentTask.isRunning()) { + cancel1B.setDisable(false); currentTask.cancel(); } }); @@ -105,7 +108,10 @@ public class DuplicateDocumentPaneController { task.setOnFailed(e -> { progressBar.closeProgress(); Throwable exception = task.getException(); + currentTask.progressProperty().removeListener(progressChangeListener); + currentTask.messageProperty().removeListener(messageChangeListener); result1TA.setText("检测过程中发生错误: " + exception.getMessage()); + DialogUtil.showDetailedErrorDialog("错误", "检测过程中发生错误", exception.getMessage()); start1B.setDisable(false); cancel1B.setDisable(true); log.error(LoggerMarker.RELEASE_MARKER, "查重任务失败", exception); @@ -114,11 +120,11 @@ public class DuplicateDocumentPaneController { // 处理任务取消情况 task.setOnCancelled(e -> { progressBar.closeProgress(); + currentTask.progressProperty().removeListener(progressChangeListener); + currentTask.messageProperty().removeListener(messageChangeListener); result1TA.appendText("\n检测已取消"); start1B.setDisable(false); cancel1B.setDisable(true); - currentTask.progressProperty().removeListener(progressChangeListener); - currentTask.messageProperty().removeListener(messageChangeListener); log.info(LoggerMarker.RELEASE_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 945e16f..20d533e 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java @@ -18,6 +18,7 @@ import top.r3944realms.docchecktoolrefactored.core.AddressFileGenerator; import top.r3944realms.docchecktoolrefactored.ui.SceneManager; import top.r3944realms.docchecktoolrefactored.ui.task.AddressFileComparisonTask; import top.r3944realms.docchecktoolrefactored.ui.task.AddressFileGenerationTask; +import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil; import top.r3944realms.docchecktoolrefactored.ui.utils.ProgressBar; import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; @@ -75,9 +76,10 @@ public class PathCheckPaneController implements Initializable { loadCatalog2TF.setText(selectedFile.getAbsolutePath()); System.setLastModifiedFile(selectedFile); log.info(LoggerMarker.DEBUG_MARKER, "选择的目录文件路径为:{}", selectedFile.getAbsolutePath()); - }else{ - log.warn(LoggerMarker.DEBUG_MARKER, "用户未选择任何文件夹"); - result2TA.setText("未选择任何文件夹,请重新选择。"); + } else { + log.warn(LoggerMarker.DEBUG_MARKER, "用户未选择任何文件"); + DialogUtil.showWarningDialog("警告", "操作有误", "未选择任何文件夹,请重新选择"); + result2TA.setText("未选择载入目录文件,请重新选择。"); } } @@ -102,6 +104,10 @@ public class PathCheckPaneController implements Initializable { if (selectedDirectory != null) { loadJPGFolder2TF.setText(selectedDirectory.getAbsolutePath()); log.info(LoggerMarker.DEBUG_MARKER, "选择的{}文件夹路径为:{}", selectedMode,selectedDirectory.getAbsolutePath()); + } else { + log.warn(LoggerMarker.DEBUG_MARKER, "用户未选择任何文件夹"); + DialogUtil.showWarningDialog("警告", "操作有误", "未选择任何文件夹,请重新选择"); + result2TA.setText("未选择载入文件夹,请重新选择。"); } System.setLastModifiedFile(selectedDirectory); } @@ -117,19 +123,23 @@ public class PathCheckPaneController implements Initializable { String filePath = loadCatalog2TF.getText(); if (filePath.isEmpty()) { result2TA.setText("请先选择目录文件。"); + DialogUtil.showWarningDialog("警告", "操作有误", "请先选择目录文件"); + log.warn(LoggerMarker.DEBUG_MARKER, "未选择目录文件"); generateLogicalAddress2B.setDisable(false); return; } + Mode selectedMode = loadFolderType2CB.getValue(); FileChooser fileChooser = System.getFileChooser(); fileChooser.setTitle("选择保存逻辑地址文件的位置"); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv")); - fileChooser.setInitialFileName("逻辑地址文件.csv"); + fileChooser.setInitialFileName(selectedMode.toString() + "逻辑地址文件.csv"); File outputFile = fileChooser.showSaveDialog(generateLogicalAddress2B.getScene().getWindow()); if (outputFile == null) { result2TA.setText("未选择保存位置"); log.warn(LoggerMarker.DEBUG_MARKER, "用户未选择任何文件"); + DialogUtil.showWarningDialog("警告", "操作有误", "未选择保存位置"); generateLogicalAddress2B.setDisable(true); return; } @@ -146,7 +156,7 @@ public class PathCheckPaneController implements Initializable { // 保存生成的文件路径 logicalAddressFilePath = finalOutputFile.getAbsolutePath(); log.info(LoggerMarker.DEBUG_MARKER, "选择的输出文件路径: {}", logicalAddressFilePath); - Mode selectedMode = loadFolderType2CB.getValue(); + // 创建后台任务 AddressFileGenerationTask task = new AddressFileGenerationTask(filePath, outputFile, selectedMode.number, true); // 绑定任务属性到UI @@ -176,7 +186,10 @@ public class PathCheckPaneController implements Initializable { task.setOnFailed(e -> { progressBar.closeProgress(); Throwable exception = task.getException(); + currentTask.progressProperty().removeListener(progressChangeListener); + currentTask.messageProperty().removeListener(messageChangeListener); result2TA.setText("检测过程中发生错误: " + exception.getMessage()); + DialogUtil.showDetailedErrorDialog("错误", "检测过程中发生错误: ", exception.getMessage()); generateLogicalAddress2B.setDisable(false); log.error(LoggerMarker.RELEASE_MARKER, "生成逻辑路径 csv 文件任务失败", exception); }); @@ -184,10 +197,10 @@ public class PathCheckPaneController implements Initializable { // 处理任务取消情况 task.setOnCancelled(e -> { progressBar.closeProgress(); - result2TA.appendText("\n检测已取消"); - generateLogicalAddress2B.setDisable(false); currentTask.progressProperty().removeListener(progressChangeListener); currentTask.messageProperty().removeListener(messageChangeListener); + result2TA.appendText("\n检测已取消"); + generateLogicalAddress2B.setDisable(false); log.info(LoggerMarker.RELEASE_MARKER, "生成逻辑路径 csv 文件任务已被取消"); }); Thread thread = new Thread(task); @@ -205,25 +218,30 @@ public class PathCheckPaneController implements Initializable { String folderPath = loadJPGFolder2TF.getText(); if (folderPath.isEmpty()) { result2TA.setText("请先选择文件夹。"); + log.warn(LoggerMarker.DEBUG_MARKER, "请先选择文件夹"); + DialogUtil.showWarningDialog("警告", "操作有误", "请先选择文件夹"); generatePhysicalAddress2B.setDisable(false); return; } File folder = new File(folderPath); if(!folder.exists() || !folder.isDirectory()) { result2TA.setText("所选路径不存在或不是一个有效的文件夹。"); + DialogUtil.showWarningDialog("警告", "操作有误", "所选路径不存在或不是一个有效的文件夹"); + log.warn(LoggerMarker.DEBUG_MARKER, "所选路径不存在或不是一个有效的文件夹"); generatePhysicalAddress2B.setDisable(false); return; } - + Mode selectedMode = loadFolderType2CB.getValue(); FileChooser fileChooser = System.getFileChooser(); fileChooser.setTitle("选择保存物理地址文件的位置"); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv")); - fileChooser.setInitialFileName("物理地址文件.csv"); + fileChooser.setInitialFileName(selectedMode.toString() + "物理地址文件.csv"); // 使用当前窗口作为父窗口显示文件选择对话框 File outputFile = fileChooser.showSaveDialog(selectJPGFolder2B.getScene().getWindow()); if (outputFile == null) { result2TA.setText("未选择保存位置"); + DialogUtil.showWarningDialog("警告", "操作有误", "未选择保存位置"); generatePhysicalAddress2B.setDisable(false); return; } @@ -240,8 +258,6 @@ public class PathCheckPaneController implements Initializable { // 保存生成的文件路径 physicalAddressFilePath = finalOutputFile.getAbsolutePath(); - // - Mode selectedMode = loadFolderType2CB.getValue(); // 创建后台任务 AddressFileGenerationTask task = new AddressFileGenerationTask(folderPath, outputFile, selectedMode.number, false); // 保存到字段 @@ -273,7 +289,10 @@ public class PathCheckPaneController implements Initializable { task.setOnFailed(e -> { progressBar.closeProgress(); Throwable exception = task.getException(); + currentTask.progressProperty().removeListener(progressChangeListener); + currentTask.messageProperty().removeListener(messageChangeListener); result2TA.setText("检测过程中发生错误: " + exception.getMessage()); + DialogUtil.showDetailedErrorDialog("错误", "检测过程中发生错误: ", exception.getMessage()); generatePhysicalAddress2B.setDisable(false); log.error(LoggerMarker.RELEASE_MARKER, "生成物理路径 csv 文件任务失败", exception); }); @@ -281,10 +300,10 @@ public class PathCheckPaneController implements Initializable { // 处理任务取消情况 task.setOnCancelled(e -> { progressBar.closeProgress(); - result2TA.appendText("\n检测已取消"); - generatePhysicalAddress2B.setDisable(false); currentTask.progressProperty().removeListener(progressChangeListener); currentTask.messageProperty().removeListener(messageChangeListener); + result2TA.appendText("\n检测已取消"); + generatePhysicalAddress2B.setDisable(false); log.info(LoggerMarker.RELEASE_MARKER, "生成物理路径 csv 文件任务已被取消"); }); Thread thread = new Thread(task); @@ -350,8 +369,9 @@ public class PathCheckPaneController implements Initializable { task.setOnFailed(event -> { cancelableProgressBar.closeProgress(); Throwable exception = task.getException(); - result2TA.setText("文件比对失败: " + task.getException().getMessage()); + result2TA.setText("文件比对失败: " + exception.getMessage()); start2B.setDisable(false); + DialogUtil.showDetailedErrorDialog("错误", "文件比对失败:", exception.getMessage()); log.error(LoggerMarker.RELEASE_MARKER, "查漏任务失败", exception); }); 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 2dfc41c..5f78036 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,6 @@ 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; 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 12216ad..29e4c75 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java @@ -1,5 +1,6 @@ package top.r3944realms.docchecktoolrefactored.ui.module; +import javafx.beans.value.ChangeListener; import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -11,13 +12,14 @@ 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.ui.SceneManager; +import top.r3944realms.docchecktoolrefactored.ui.task.HashFileGenerationTask; +import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil; import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.File; import java.io.IOException; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; //TODO: 应该交给Platform:runLater; @@ -48,9 +50,7 @@ public class StorageCarrierPaneController { @FXML private Button clearSelectedFoldersButton; - - - + private final top.r3944realms.docchecktoolrefactored.ui.utils.ProgressBar progressBar = new top.r3944realms.docchecktoolrefactored.ui.utils.ProgressBar(false); @FXML void onSelectLD(ActionEvent event) { log.info(LoggerMarker.DEBUG_MARKER, "用户点击选择文件夹按钮"); @@ -117,11 +117,13 @@ public class StorageCarrierPaneController { @FXML void onCaculateHash(ActionEvent event) { - log.info(LoggerMarker.DEBUG_MARKER, "开始计算RAR文件的MD5哈希值"); + generateHashFile7B.setDisable(true); String filePath = loadCompressedFile.getText(); if (filePath == null || filePath.isEmpty()) { log.warn(LoggerMarker.DEBUG_MARKER, "未选择RAR文件,无法计算哈希值"); result7TA.setText("请先选择一个 .rar 文件"); + DialogUtil.showWarningDialog("警告", "操作有误", "请先选择一个 .rar 文件"); + generateHashFile7B.setDisable(false); return; } @@ -129,31 +131,38 @@ public class StorageCarrierPaneController { if (!file.exists() || !file.isFile() || !filePath.endsWith(".rar")) { log.warn(LoggerMarker.DEBUG_MARKER, "选择的文件无效或不是RAR文件: {}", filePath); result7TA.setText("所选文件不存在或不是一个有效的 .rar 文件"); + DialogUtil.showWarningDialog("警告", "操作有误", "所选文件不存在或不是一个有效的 .rar 文件"); + generateHashFile7B.setDisable(false); return; } - try { - log.info(LoggerMarker.DEBUG_MARKER, "开始计算文件哈希值: {}", filePath); + log.info(LoggerMarker.DEBUG_MARKER, "开始计算RAR文件MD5哈希值: {}", filePath); MD5HashCalculator hashCalculator = new MD5HashCalculator(); String hashResult = hashCalculator.calculateHash(file.toPath()); result7TA.setText("计算结果:\n" + hashResult); + generateHashFile7B.setDisable(false); log.info(LoggerMarker.DEBUG_MARKER, "文件哈希值计算完成: {}", hashResult); } catch (IOException e) { log.error(LoggerMarker.DEBUG_MARKER, "计算文件哈希值时出错: {}", filePath, e); + DialogUtil.showDetailedErrorDialog("错误", "生成哈希文件时出错:", e.getMessage()); + generateHashFile7B.setDisable(false); result7TA.setText("计算哈希值时出错: " + e.getMessage()); } } @FXML void onGenerateHF(ActionEvent event) { - log.info(LoggerMarker.DEBUG_MARKER, "开始生成哈希列表文件"); + caculateHash7B.setDisable(true); + String folderPathsText = loadDigitalOutcomes.getText(); if (folderPathsText == null || folderPathsText.isEmpty()) { log.warn(LoggerMarker.DEBUG_MARKER, "未选择文件夹,无法生成哈希列表文件"); + DialogUtil.showWarningDialog("警告", "操作有误", "请先选择一个文件夹"); result7TA.setText("请先选择一个文件夹"); + caculateHash7B.setDisable(false); return; } - + log.info(LoggerMarker.DEBUG_MARKER, "开始生成哈希列表文件"); // 解析多个文件夹路径 String[] folderPaths = folderPathsText.split(File.pathSeparator); List folders = new ArrayList<>(); @@ -165,6 +174,8 @@ public class StorageCarrierPaneController { } else { log.warn(LoggerMarker.DEBUG_MARKER, "选择的路径无效或不是文件夹: {}", path); result7TA.setText("所选路径不存在或不是一个有效的文件夹: " + path); + DialogUtil.showWarningDialog("警告", "操作有误", ("所选路径不存在或不是一个有效的文件夹: " + path)); + caculateHash7B.setDisable(false); return; } } @@ -182,6 +193,7 @@ public class StorageCarrierPaneController { if (outputFile == null) { log.info(LoggerMarker.DEBUG_MARKER, "用户取消了文件保存操作"); result7TA.setText("未选择保存位置"); + DialogUtil.showWarningDialog("警告", "操作有误", "未选择保存位置"); return; } @@ -194,55 +206,51 @@ public class StorageCarrierPaneController { } log.info(LoggerMarker.DEBUG_MARKER, "选择的输出文件路径: {}", finalOutputFile.getAbsolutePath()); - + progressBar.showProgress(SceneManager.getPrimaryStage(), "生成哈希值列表文件", "正在初始化..."); // 创建后台任务 - Task task = new Task<>() { - @Override - protected String call() throws Exception { - log.info(LoggerMarker.DEBUG_MARKER, "开始执行哈希文件生成任务"); - updateMessage("开始生成哈希文件..."); - - HashFileGenerator generator = new HashFileGenerator(); - - // 传递多个文件夹路径 - List folderPaths = folders.stream().map(File::toPath).collect(ArrayList::new, - ArrayList::add, - ArrayList::addAll); - - generator.generateHashFile(folderPaths, finalOutputFile.toPath(), (current, total) -> { - updateProgress(current, total); - updateMessage("处理文件: " + current + "/" + total); - if (current % 500 == 0 || current == total) { // 每500个文件或完成时记录一次日志 - log.info(LoggerMarker.DEBUG_MARKER, "处理进度: {}/{}", current, total); - } - }); - - log.info(LoggerMarker.DEBUG_MARKER, "哈希文件生成任务完成,输出文件: {}", finalOutputFile.getAbsolutePath()); - return "哈希列表文件已生成: " + finalOutputFile.getAbsolutePath(); + Task task = new HashFileGenerationTask(finalOutputFile, folders); + // 绑定任务属性到UI + ChangeListener progressChangeListener = (obs, oldVal, newVal) -> { + if (newVal != null) { + if (task.getMessage() != null) { + progressBar.updateProgress(newVal.doubleValue(), task.getMessage()); + } } }; - + task.progressProperty().addListener(progressChangeListener); // 绑定任务的消息到结果文本区域,实时显示进度 - task.messageProperty().addListener((observable, oldValue, newValue) -> { + ChangeListener messageChangeListener = (observable, oldValue, newValue) -> { result7TA.setText(newValue); - }); + }; + task.messageProperty().addListener(messageChangeListener); // 任务成功完成 task.setOnSucceeded(e -> { + progressBar.closeProgress(); log.info(LoggerMarker.DEBUG_MARKER, "哈希文件生成任务成功完成"); + caculateHash7B.setDisable(false); result7TA.setText(task.getValue()); }); // 任务失败处理 task.setOnFailed(e -> { + progressBar.closeProgress(); Throwable exception = task.getException(); + task.progressProperty().removeListener(progressChangeListener); + task.messageProperty().removeListener(messageChangeListener); + caculateHash7B.setDisable(false); String errorMsg = "生成哈希文件时出错: " + (exception != null ? exception.getMessage() : "未知错误"); + DialogUtil.showDetailedErrorDialog("错误", "生成哈希文件时出错", errorMsg); log.error(LoggerMarker.RELEASE_MARKER, "哈希文件生成任务失败", exception); result7TA.setText(errorMsg); }); // 任务取消处理 task.setOnCancelled(e -> { + progressBar.closeProgress(); + task.progressProperty().removeListener(progressChangeListener); + task.messageProperty().removeListener(messageChangeListener); + caculateHash7B.setDisable(false); log.info(LoggerMarker.DEBUG_MARKER, "哈希文件生成任务被用户取消"); result7TA.setText("哈希文件生成操作已取消"); }); diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/AddressFileComparisonTask.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/AddressFileComparisonTask.java index fa2c2e6..a33d5fc 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/AddressFileComparisonTask.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/AddressFileComparisonTask.java @@ -24,7 +24,7 @@ public class AddressFileComparisonTask extends Task { @@ -19,6 +19,9 @@ public class AddressFileGenerationTask extends Task { private final File outputFile; private final int folderType; private final AddressFileGenerator generator; + private ExecutorService executor; + private Future future; + public AddressFileGenerationTask(String sourcePath, File outputFile, @@ -31,60 +34,107 @@ public class AddressFileGenerationTask extends Task { } @Override - protected String call() { + protected String call() throws Exception { updateMessage("初始化生成任务..."); generator.setProgressCallback(new AddressFileGenerator.ProgressCallback() { - @Override - public void onPhaseStarted(AddressFileGenerator.Phase phase) { - switch (phase) { - case GENERATE_LOGICAL -> updateMessage("正在生成逻辑地址 CSV 文件 ..."); - case GENERATE_PHYSICAL -> updateMessage("正在生成物理地址 CSV 文件 ..."); - } - } + @Override + public void onPhaseStarted(AddressFileGenerator.Phase phase) { + switch (phase) { + case GENERATE_LOGICAL -> updateMessage("正在生成逻辑地址 CSV 文件 ..."); + case GENERATE_PHYSICAL -> updateMessage("正在生成物理地址 CSV 文件 ..."); + } + } - @Override - public void onPhaseProgress(AddressFileGenerator.Phase phase, int current, int total) { - if (total > 0) { - updateProgress(current, total); - switch (phase) { - case GENERATE_LOGICAL -> updateMessage(String.format("在生成逻辑地址 CSV : %d/%d", current, total)); - case GENERATE_PHYSICAL -> updateMessage(String.format("在生成物理地址 CSV : %d/%d", current, total)); - } - } - } + @Override + public void onPhaseProgress(AddressFileGenerator.Phase phase, int current, int total) { + if (total > 0) { + updateProgress(current, total); + switch (phase) { + case GENERATE_LOGICAL -> updateMessage(String.format("在生成逻辑地址 CSV : %d/%d", current, total)); + case GENERATE_PHYSICAL -> updateMessage(String.format("在生成物理地址 CSV : %d/%d", current, total)); + } + } + } - @Override - public void onPhaseCompleted(AddressFileGenerator.Phase phase) { - switch (phase) { - case GENERATE_LOGICAL -> updateMessage("已完成生成逻辑地址 CSV 文件任务"); - case GENERATE_PHYSICAL -> updateMessage("已完成生成物理地址 CSV 文件任务"); - } - } - } - ); - ExecutorService executor = newSingleThreadExecutor(); - Future future = executor.submit(() -> { - generator.generateAddressFile(sourcePath, outputFile, folderType); + @Override + public void onPhaseCompleted(AddressFileGenerator.Phase phase) { + switch (phase) { + case GENERATE_LOGICAL -> updateMessage("已完成生成逻辑地址 CSV 文件任务"); + case GENERATE_PHYSICAL -> updateMessage("已完成生成物理地址 CSV 文件任务"); + } + } + }); + executor = ThreadPoolManager.createPool("address-file-generate-pool", System.getAvailableProcessors()); + + // 提交任务到线程池 + future = executor.submit(() -> { + try { + generator.generateAddressFile(sourcePath, outputFile, folderType); + } catch (Exception e) { + throw new RuntimeException("地址文件生成失败", e); + } }); try { // 等待执行完成或超时 future.get(System.getSetting().getTaskTimeout(), TimeUnit.SECONDS); - } catch (TimeoutException e) { - future.cancel(true); // 尝试中断 - this.cancel(); // 取消 Task - throw new RuntimeException("生成任务超时 (>" + System.getSetting().getTaskTimeout() + "s)", e); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException("生成任务失败", e.getCause()); - } finally { - executor.shutdownNow(); - } + return outputFile.getAbsolutePath(); - return outputFile.getAbsolutePath(); + } catch (TimeoutException e) { + String errorMsg = "生成任务超时 (>" + System.getSetting().getTaskTimeout() + "s)"; + updateMessage(errorMsg); + throw new RuntimeException(errorMsg, e); + + } catch (ExecutionException e) { + String errorMsg = "生成任务失败: " + e.getCause().getMessage(); + updateMessage(errorMsg); + throw new RuntimeException(errorMsg, e.getCause()); + + } catch (InterruptedException e) { + updateMessage("生成任务被取消"); + Thread.currentThread().interrupt(); + throw e; + + } finally { + // 清理资源 + if (executor != null) { + try { + executor.shutdown(); + if (!executor.awaitTermination(1, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } } @Override protected void cancelled() { - log.info("生成任务已取消: {}", outputFile.getAbsolutePath()); + super.cancelled(); + log.info(LoggerMarker.DEBUG_MARKER, "生成任务已取消: {}", outputFile.getAbsolutePath()); + updateMessage("正在取消生成任务..."); + + // 先取消 Future + if (future != null && !future.isDone()) { + future.cancel(true); + } + + // 给任务一些时间响应中断 + try { + if (executor != null) { + executor.shutdown(); // 先尝试正常关闭 + if (!executor.awaitTermination(2, TimeUnit.SECONDS)) { + executor.shutdownNow(); // 强制关闭 + } + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + + updateMessage("生成任务已取消"); } } 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 ef40b69..1e6b057 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java @@ -14,19 +14,24 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; -import java.util.concurrent.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @Slf4j public class DuplicateDocumentDetectionTask extends Task{ private final String folderPath; - private final MD5HashCalculator hashCalculator; - private volatile RobustParallelScanner scanner; + private final DuplicateFinder duplicateFinder; public DuplicateDocumentDetectionTask(String folderPath) { this.folderPath = folderPath; - this.hashCalculator = new MD5HashCalculator(); + // 创建带进度更新的扫描器 + RobustParallelScanner scanner = new RobustParallelScanner(10); + MD5HashCalculator hashCalculator = new MD5HashCalculator(); + // 进度监听的 DuplicateFinder + this.duplicateFinder = new DuplicateFinder(scanner, hashCalculator, true); } @@ -39,12 +44,7 @@ public class DuplicateDocumentDetectionTask extends Task{ if (!Files.exists(rootPath) || !Files.isDirectory(rootPath)) { throw new IllegalArgumentException("指定路径不是有效目录: " + folderPath); } - // 创建带进度更新的扫描器 - scanner = new RobustParallelScanner(10); - - // 创建带有进度监听的 DuplicateFinder - DuplicateFinder duplicateFinder = new DuplicateFinder(scanner, hashCalculator, true) - .applySetting(System.getSetting()); + duplicateFinder.applySetting(System.getSetting()); // 用于统计文件总数 AtomicInteger totalFiles = new AtomicInteger(0); @@ -93,7 +93,7 @@ public class DuplicateDocumentDetectionTask extends Task{ }); AtomicReference> resultRef = new AtomicReference<>(); - List errors = new CopyOnWriteArrayList<>(); + AtomicReference errorRef = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); // 在单独线程中执行查找 @@ -102,7 +102,7 @@ public class DuplicateDocumentDetectionTask extends Task{ List duplicates = duplicateFinder.findDuplicates(rootPath); resultRef.set(duplicates); } catch (Exception e) { - errors.add(e); + errorRef.set(e); } finally { latch.countDown(); } @@ -111,42 +111,18 @@ public class DuplicateDocumentDetectionTask extends Task{ findThread.setDaemon(true); findThread.start(); - // 等待扫描完成,设置超时时间(例如5分钟) + // 简单等待,Task取消时会自动中断 long totalTimeout = System.getSetting().getTaskTimeout(); if (!latch.await(totalTimeout, TimeUnit.SECONDS)) { duplicateFinder.shutdown(); - throw new TimeoutException(String.format("扫描超时(%d秒)", totalTimeout)); - } - - // 检查是否被取消 - long start = java.lang.System.currentTimeMillis(); - try { - boolean finished = false; - while (!finished) { - // 每 200ms 等待一次 latch,避免忙等待 - finished = latch.await(200, TimeUnit.MILLISECONDS); - - // 检查是否被取消 - if (isCancelled()) { - duplicateFinder.shutdown(); - return "操作已被取消"; - } - - // 检查是否超时 - if (java.lang.System.currentTimeMillis() - start > totalTimeout * 1000L) { - duplicateFinder.shutdown(); - throw new TimeoutException(String.format("扫描超时(%d秒)", totalTimeout)); - } - } - } catch (InterruptedException e) { - duplicateFinder.shutdown(); - Thread.currentThread().interrupt(); - return "操作被中断"; + throw new TimeoutException(String.format("任务超时(%d秒),请考虑在‘文件’-‘设置’里增加‘步骤任务超时时间’", totalTimeout)); } // 检查是否有错误 - if (!errors.isEmpty()) { - throw new ScanningException(errors); + if (errorRef.get() != null) { + throw new RuntimeException(errorRef.get()); + } else if (!duplicateFinder.getErrors().isEmpty()) { + throw new ScanningException(duplicateFinder.getErrors()); } List duplicateGroups = resultRef.get(); @@ -198,8 +174,8 @@ public class DuplicateDocumentDetectionTask extends Task{ @Override protected void cancelled() { super.cancelled(); - if (scanner != null) { - scanner.cancel(); - } + // 清理资源 + duplicateFinder.shutdown(); + updateMessage("操作已被取消"); } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/HashFileGenerationTask.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/HashFileGenerationTask.java new file mode 100644 index 0000000..94a7f2a --- /dev/null +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/HashFileGenerationTask.java @@ -0,0 +1,120 @@ +package top.r3944realms.docchecktoolrefactored.ui.task; + +import javafx.concurrent.Task; +import lombok.extern.slf4j.Slf4j; +import top.r3944realms.docchecktoolrefactored.System; +import top.r3944realms.docchecktoolrefactored.ThreadPoolManager; +import top.r3944realms.docchecktoolrefactored.core.HashFileGenerator; +import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + +@Slf4j +public class HashFileGenerationTask extends Task { + private final HashFileGenerator generator; + private final File finalOutputFile; + private final List folders; + private ExecutorService executor; + private Future future; + + public HashFileGenerationTask(File finalOutputFile, List folders) { + this.finalOutputFile = finalOutputFile; + this.folders = folders; + this.generator = new HashFileGenerator(); + } + + @Override + protected String call() throws Exception { + log.info(LoggerMarker.DEBUG_MARKER, "开始执行哈希文件生成任务"); + updateMessage("开始生成哈希文件..."); + + // 传递多个文件夹路径 + List folderPaths = folders.stream().map(File::toPath).collect(ArrayList::new, + ArrayList::add, + ArrayList::addAll); + + generator.setCallback((current, total) -> { + updateProgress(current, total); + updateMessage("处理文件: " + current + "/" + total); + if (current % 500 == 0 || current == total) { + log.info(LoggerMarker.DEBUG_MARKER, "处理进度: {}/{}", current, total); + } + }); + + // 使用单独的线程执行生成任务,以便支持超时和取消 + executor = ThreadPoolManager.createPool("hash-file-generate-pool", 1); + future = executor.submit(() -> { + try { + generator.generateHashFile(folderPaths, finalOutputFile.toPath()); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + }); + + try { + // 设置超时时间 + long timeoutSeconds = System.getSetting().getTaskTimeout(); + future.get(timeoutSeconds, TimeUnit.SECONDS); + + log.info(LoggerMarker.DEBUG_MARKER, "哈希文件生成任务完成,输出文件: {}", finalOutputFile.getAbsolutePath()); + return "哈希列表文件已生成: " + finalOutputFile.getAbsolutePath(); + + } catch (TimeoutException e) { + String errorMsg = "哈希文件生成任务超时 (" + System.getSetting().getTaskTimeout() + "秒,请考虑在‘文件’-‘设置’里增加‘步骤任务超时时间’)"; + updateMessage(errorMsg); + log.warn(LoggerMarker.DEBUG_MARKER, errorMsg); + throw new RuntimeException(errorMsg, e); + + } catch (ExecutionException e) { + String errorMsg = "哈希文件生成失败: " + e.getCause().getMessage(); + updateMessage(errorMsg); + log.error(LoggerMarker.DEBUG_MARKER, errorMsg, e); + throw new RuntimeException(errorMsg, e.getCause()); + + } catch (InterruptedException e) { + updateMessage("哈希文件生成任务被取消"); + log.info(LoggerMarker.DEBUG_MARKER, "哈希文件生成任务被取消"); + Thread.currentThread().interrupt(); + throw e; + + } finally { + // 清理资源 + if (executor != null) { + executor.shutdownNow(); + } + } + } + + @Override + protected void cancelled() { + super.cancelled(); + log.info(LoggerMarker.DEBUG_MARKER, "哈希文件生成任务已取消"); + updateMessage("正在取消哈希文件生成任务..."); + + // 先取消 Future + if (future != null && !future.isDone()) { + future.cancel(true); + } + + // 优雅关闭执行器 + if (executor != null) { + try { + executor.shutdown(); // 先尝试正常关闭 + if (!executor.awaitTermination(2, TimeUnit.SECONDS)) { + executor.shutdownNow(); // 强制关闭 + executor.awaitTermination(1, TimeUnit.SECONDS); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + updateMessage("哈希文件生成任务已取消"); + } +} 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 430cf8a..984b70c 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java @@ -1,9 +1,9 @@ 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.scene.control.*; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; import javafx.stage.Window; import java.util.Optional; @@ -44,8 +44,8 @@ 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); + ButtonType yesButton = new ButtonType("是", ButtonBar.ButtonData.YES); + ButtonType noButton = new ButtonType("否", ButtonBar.ButtonData.NO); alert.getButtonTypes().setAll(yesButton, noButton); Optional result = alert.showAndWait(); return result.isPresent() && result.get() == yesButton; @@ -60,10 +60,7 @@ public class DialogUtil { */ public static void showInformationDialog(String title, String header, String content) { Platform.runLater(() -> { - Alert alert = new Alert(Alert.AlertType.INFORMATION); - alert.setTitle(title); - alert.setHeaderText(header); - alert.setContentText(content); + Alert alert = createAlert(Alert.AlertType.INFORMATION, title, header, content); alert.showAndWait(); }); } @@ -77,10 +74,7 @@ public class DialogUtil { */ public static void showWarningDialog(String title, String header, String content) { Platform.runLater(() -> { - Alert alert = new Alert(Alert.AlertType.WARNING); - alert.setTitle(title); - alert.setHeaderText(header); - alert.setContentText(content); + Alert alert = createAlert(Alert.AlertType.WARNING, title, header, content); alert.showAndWait(); }); } @@ -93,11 +87,138 @@ public class DialogUtil { * @param content the content */ public static void showErrorDialog(String title, String header, String content) { + Platform.runLater(() -> { + Alert alert = createAlert(Alert.AlertType.ERROR, title, header, content); + alert.showAndWait(); + }); + } + /** + * 创建支持长文本的对话框 + */ + private static Alert createAlert(Alert.AlertType alertType, String title, String header, String content) { + Alert alert = new Alert(alertType); + alert.setTitle(title); + alert.setHeaderText(header); + + // 如果内容过长,使用 TextArea 来显示 + if (content != null && content.length() > 100) { + // 创建可滚动的文本区域 + TextArea textArea = new TextArea(content); + textArea.setEditable(false); + textArea.setWrapText(true); + textArea.setMaxWidth(Double.MAX_VALUE); + textArea.setMaxHeight(Double.MAX_VALUE); + + // 设置文本区域样式 + textArea.setStyle("-fx-font-family: monospace; -fx-font-size: 12px;"); + + GridPane.setVgrow(textArea, Priority.ALWAYS); + GridPane.setHgrow(textArea, Priority.ALWAYS); + + GridPane expContent = new GridPane(); + expContent.setMaxWidth(Double.MAX_VALUE); + expContent.add(new Label("详细信息:"), 0, 0); + expContent.add(textArea, 0, 1); + + alert.getDialogPane().setContent(expContent); + + // 设置对话框大小 + alert.getDialogPane().setPrefSize(600, 400); + } else { + // 短文本直接显示 + alert.setContentText(content); + } + + return alert; + } + + /** + * 显示带详细信息的错误对话框 + */ + public static void showDetailedErrorDialog(String title, String summary, String details) { Platform.runLater(() -> { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle(title); - alert.setHeaderText(header); - alert.setContentText(content); + alert.setHeaderText(summary); + + // 创建可滚动的详细文本区域 + TextArea textArea = new TextArea(details); + textArea.setEditable(false); + textArea.setWrapText(true); + textArea.setMaxWidth(Double.MAX_VALUE); + textArea.setMaxHeight(Double.MAX_VALUE); + textArea.setStyle("-fx-font-family: monospace; -fx-font-size: 11px;"); + + GridPane.setVgrow(textArea, Priority.ALWAYS); + GridPane.setHgrow(textArea, Priority.ALWAYS); + + GridPane expContent = new GridPane(); + expContent.setMaxWidth(Double.MAX_VALUE); + expContent.add(new Label("错误详情:"), 0, 0); + expContent.add(textArea, 0, 1); + + alert.getDialogPane().setContent(expContent); + alert.getDialogPane().setPrefSize(700, 500); + alert.showAndWait(); + }); + } + + /** + * 显示带详细信息的警告对话框 + */ + public static void showDetailedWarningDialog(String title, String summary, String details) { + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle(title); + alert.setHeaderText(summary); + + TextArea textArea = new TextArea(details); + textArea.setEditable(false); + textArea.setWrapText(true); + textArea.setMaxWidth(Double.MAX_VALUE); + textArea.setMaxHeight(Double.MAX_VALUE); + textArea.setStyle("-fx-font-family: monospace; -fx-font-size: 11px;"); + + GridPane.setVgrow(textArea, Priority.ALWAYS); + GridPane.setHgrow(textArea, Priority.ALWAYS); + + GridPane expContent = new GridPane(); + expContent.setMaxWidth(Double.MAX_VALUE); + expContent.add(new Label("警告详情:"), 0, 0); + expContent.add(textArea, 0, 1); + + alert.getDialogPane().setContent(expContent); + alert.getDialogPane().setPrefSize(700, 500); + alert.showAndWait(); + }); + } + + /** + * 显示带详细信息的信息对话框 + */ + public static void showDetailedInformationDialog(String title, String summary, String details) { + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle(title); + alert.setHeaderText(summary); + + TextArea textArea = new TextArea(details); + textArea.setEditable(false); + textArea.setWrapText(true); + textArea.setMaxWidth(Double.MAX_VALUE); + textArea.setMaxHeight(Double.MAX_VALUE); + textArea.setStyle("-fx-font-family: monospace; -fx-font-size: 11px;"); + + GridPane.setVgrow(textArea, Priority.ALWAYS); + GridPane.setHgrow(textArea, Priority.ALWAYS); + + GridPane expContent = new GridPane(); + expContent.setMaxWidth(Double.MAX_VALUE); + expContent.add(new Label("详细信息:"), 0, 0); + expContent.add(textArea, 0, 1); + + alert.getDialogPane().setContent(expContent); + alert.getDialogPane().setPrefSize(700, 500); alert.showAndWait(); }); } diff --git a/src/main/resources/fxml/login-view.fxml b/src/main/resources/fxml/login-view.fxml index c95fe1b..3b5ea48 100644 --- a/src/main/resources/fxml/login-view.fxml +++ b/src/main/resources/fxml/login-view.fxml @@ -8,6 +8,7 @@ + @@ -71,4 +72,12 @@ + + + + + + diff --git a/src/main/resources/fxml/main-view.fxml b/src/main/resources/fxml/main-view.fxml index 0cae5a8..5c249ec 100644 --- a/src/main/resources/fxml/main-view.fxml +++ b/src/main/resources/fxml/main-view.fxml @@ -12,7 +12,7 @@ - + diff --git a/src/main/resources/fxml/module/step-1-pane.fxml b/src/main/resources/fxml/module/step-1-pane.fxml index 5796261..d68287e 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 @@ - + @@ -55,7 +55,7 @@ -