feat:1.以javafx SceneBuilder 17版本重新编辑了下FXML文件 2.添加线程池全局管理类(待完善:如何协作)3. 添加版本显示字段(非最终实现)

This commit is contained in:
叁玖领域 2025-08-22 20:01:55 +08:00
parent 204b1f38bf
commit 4625d30105
32 changed files with 788 additions and 251 deletions

View File

@ -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

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -0,0 +1,43 @@
package top.r3944realms.docchecktoolrefactored;
import java.util.Map;
import java.util.concurrent.*;
public class ThreadPoolManager {
private static final Map<String, ExecutorService> 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();
}
}

View File

@ -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<ComparisonResult> compareFiles(String physicalFilePath, String logicalFilePath, CompareMode compareMode) {

View File

@ -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);

View File

@ -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<Path> 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<Path> directories, Path outputFile)
throws IOException, InterruptedException {
// 开始时检查中断
if (Thread.interrupted()) {
throw new InterruptedException("任务被取消");
}
List<Path> 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<String[]> 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<Path> files, List<String[]> 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<String[]> 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("文件写入被取消");
}
}
}
}
}
}

View File

@ -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;

View File

@ -1,7 +1,6 @@
package top.r3944realms.docchecktoolrefactored.io.scanner;
import java.nio.file.Path;
import java.util.Scanner;
/**
* The interface File scanner.

View File

@ -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());
}
}

View File

@ -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();

View File

@ -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<Stage> 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); // 默认大小可调

View File

@ -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, "查重任务已被取消");
});
// 在新线程中执行任务

View File

@ -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);
});

View File

@ -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;

View File

@ -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<File> 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<String> task = new Task<>() {
@Override
protected String call() throws Exception {
log.info(LoggerMarker.DEBUG_MARKER, "开始执行哈希文件生成任务");
updateMessage("开始生成哈希文件...");
HashFileGenerator generator = new HashFileGenerator();
// 传递多个文件夹路径
List<Path> 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<String> task = new HashFileGenerationTask(finalOutputFile, folders);
// 绑定任务属性到UI
ChangeListener<Number> 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<String> 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("哈希文件生成操作已取消");
});

View File

@ -24,7 +24,7 @@ public class AddressFileComparisonTask extends Task<AddressFileComparator.Compar
this.logicalFilePath = logicalFilePath;
this.compareMode = compareMode;
this.timeoutSeconds = timeoutSeconds;
comparator = new AddressFileComparator();
this.comparator = new AddressFileComparator();
}
@Override
@ -75,7 +75,7 @@ public class AddressFileComparisonTask extends Task<AddressFileComparator.Compar
.get(timeoutSeconds, TimeUnit.SECONDS);
} catch (TimeoutException e) {
updateMessage("文件比对超时,请检查文件大小或电脑性能");
updateMessage("文件比对超时,请考虑在‘文件’-‘设置’里增加‘步骤任务超时时间’");
log.error("文件比对超时", e);
throw e;
} finally {
@ -86,6 +86,9 @@ public class AddressFileComparisonTask extends Task<AddressFileComparator.Compar
@Override
protected void cancelled() {
super.cancelled();
comparator.shutdown();
updateMessage("文件比较任务已取消");
}
}

View File

@ -3,15 +3,15 @@ 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.AddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.core.PhysicalAddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.File;
import java.util.concurrent.*;
import static java.util.concurrent.Executors.*;
@Slf4j
public class AddressFileGenerationTask extends Task<String> {
@ -19,6 +19,9 @@ public class AddressFileGenerationTask extends Task<String> {
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<String> {
}
@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("生成任务已取消");
}
}

View File

@ -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<String>{
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<String>{
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<String>{
});
AtomicReference<List<DuplicateGroup>> resultRef = new AtomicReference<>();
List<Exception> errors = new CopyOnWriteArrayList<>();
AtomicReference<Throwable> errorRef = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
// 在单独线程中执行查找
@ -102,7 +102,7 @@ public class DuplicateDocumentDetectionTask extends Task<String>{
List<DuplicateGroup> 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<String>{
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<DuplicateGroup> duplicateGroups = resultRef.get();
@ -198,8 +174,8 @@ public class DuplicateDocumentDetectionTask extends Task<String>{
@Override
protected void cancelled() {
super.cancelled();
if (scanner != null) {
scanner.cancel();
}
// 清理资源
duplicateFinder.shutdown();
updateMessage("操作已被取消");
}
}

View File

@ -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<String> {
private final HashFileGenerator generator;
private final File finalOutputFile;
private final List<File> folders;
private ExecutorService executor;
private Future<?> future;
public HashFileGenerationTask(File finalOutputFile, List<File> folders) {
this.finalOutputFile = finalOutputFile;
this.folders = folders;
this.generator = new HashFileGenerator();
}
@Override
protected String call() throws Exception {
log.info(LoggerMarker.DEBUG_MARKER, "开始执行哈希文件生成任务");
updateMessage("开始生成哈希文件...");
// 传递多个文件夹路径
List<Path> 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("哈希文件生成任务已取消");
}
}

View File

@ -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<ButtonType> 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();
});
}

View File

@ -8,6 +8,7 @@
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
@ -71,4 +72,12 @@
</rowConstraints>
</GridPane>
</center>
<bottom>
<HBox prefHeight="13.0" prefWidth="360.0" BorderPane.alignment="CENTER">
<children>
<Label text="版本号:" />
<Label fx:id="versionL" text="&lt;&gt;" />
</children>
</HBox>
</bottom>
</BorderPane>

View File

@ -12,7 +12,7 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="1000.0" prefWidth="1200.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.MainStageController">
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="1000.0" prefWidth="1200.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.MainStageController">
<children>
<MenuBar prefWidth="2558.0" VBox.vgrow="ALWAYS">
<menus>

View File

@ -10,7 +10,7 @@
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.DuplicateDocumentPaneController">
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.DuplicateDocumentPaneController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="288.0" minWidth="0.0" percentWidth="0.0" prefWidth="82.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="1263.9999633789064" minWidth="0.0" prefWidth="745.0" />
@ -55,7 +55,7 @@
<GridPane.margin>
<Insets left="10.0" />
</GridPane.margin></Label>
<TextArea editable="false" maxWidth="1.7976931348623157E308" prefWidth="400.0" text="1.点击“选择文件夹”按钮,载入需要查找重复文件的数据(一般页面级文件和文件级文件分批载入检查)。&#10;2.点击“开始检查”按钮,软件将对选定区域的数据批量计算文件哈希值,并对比查找重复文件,“结果反馈”区域将显示扫描文件数量、重复文件组和重复文件数量。&#10;3.根据软件反馈的重复文件组,逐一核实确认是否为重复文件。&#10;4.将确认后的检查结果填入查重登记表附件1&#10;;" wrapText="true" GridPane.columnIndex="3" GridPane.columnSpan="2" GridPane.rowIndex="3">
<TextArea editable="false" maxWidth="1.7976931348623157E308" prefWidth="400.0" text="1.点击“选择文件夹”按钮,载入需要查找重复文件的数据(一般页面级文件和文件级文件分批载入检查)。&#10;2.点击“开始检查”按钮,软件将对选定区域的数据批量计算文件哈希值,并对比查找重复文件,“结果反馈”区域将显示扫描文件数量、重复文件组和重复文件数量。&#10;3.根据软件反馈的重复文件组,逐一核实确认是否为重复文件。&#10;4.将确认后的检查结果填入查重登记表附件1。" wrapText="true" GridPane.columnIndex="3" GridPane.columnSpan="2" GridPane.rowIndex="3">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>

View File

@ -11,7 +11,7 @@
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.PathCheckPaneController">
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.PathCheckPaneController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="940.0" minWidth="0.0" percentWidth="0.0" prefWidth="104.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="940.0" minWidth="10.0" percentWidth="0.0" prefWidth="104.0" />

View File

@ -5,7 +5,7 @@
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1">
<children>
<TextArea editable="false" prefHeight="800.0" prefWidth="1000.0" scrollLeft="1.0" text="工作内容:&#10; 对照《元数据检查登记表》附件4检查并登记数字化项目信息、技术环境及技术参数的完整性等情况。" wrapText="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<font>

View File

@ -5,7 +5,7 @@
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>

View File

@ -5,7 +5,7 @@
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1">
<children>
<TextArea editable="false" prefHeight="800.0" prefWidth="1000.0" text="工作内容:&#10; 对照《工作记录检查登记表》附件6检查数字化工作台帐的规范性及与成果的一致性并在表格中登记检查情况。" wrapText="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<font>

View File

@ -10,12 +10,13 @@
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.StorageCarrierPaneController">
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.StorageCarrierPaneController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="226.33331298828125" minWidth="10.0" percentWidth="10.0" prefWidth="108.33333333333334" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="559.3333511352539" minWidth="10.0" percentWidth="40.0" prefWidth="373.00002034505206" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="345.3333435058594" minWidth="10.0" percentWidth="25.0" prefWidth="227.00004069010413" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="500.0" minWidth="10.0" percentWidth="25.0" prefWidth="400.0" />
<ColumnConstraints />
</columnConstraints>
<rowConstraints>
<RowConstraints maxHeight="151.33334350585938" percentHeight="7.0" prefHeight="55.00001017252603" vgrow="NEVER" />

View File

@ -12,7 +12,7 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.SettingDialogController">
<VBox xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.SettingDialogController">
<children>
<GridPane>
<columnConstraints>

View File

@ -1,10 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
<!-- 属性定义保持不变 -->
<property name="APP_NAME" value="DocCheckTool"/>
<property name="LOG_HOME" value="${APP_NAME}/${log.dir:-logs}"/>
<!-- 日志格式 -->
<property name="RELEASE_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss} [%level] %msg%n"/>
<property name="DEBUG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{80} - %msg%n"/>
<property name="TRACKER_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{80} - %msg%n"/>
@ -18,61 +17,63 @@
</encoder>
</appender>
<!-- RELEASE 文件日志 -->
<!-- RELEASE 文件日志 - 只接收 RELEASE marker -->
<appender name="RELEASE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/release.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/archive/release.%d{yyyy-MM-dd}.log</fileNamePattern>
<fileNamePattern>${LOG_HOME}/release.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.OnMarkerEvaluator">
<marker>RELEASE</marker>
</evaluator>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>${RELEASE_PATTERN}</pattern>
</encoder>
</appender>
<!-- DEBUG 文件日志 -->
<!-- DEBUG 文件日志 - 只接收 DEBUG marker -->
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/debug/debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/archive/debug.%d{yyyy-MM-dd}.log</fileNamePattern>
<fileNamePattern>${LOG_HOME}/debug/debug.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.OnMarkerEvaluator">
<marker>DEBUG</marker>
</evaluator>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>${DEBUG_PATTERN}</pattern>
</encoder>
</appender>
<!-- TRACKER 文件日志(默认不启用) -->
<!-- TRACKER 文件日志 - 只接收 TRACE marker -->
<appender name="TRACKER_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/debug/tracker.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/archive/tracker.%d{yyyy-MM-dd}.log</fileNamePattern>
<fileNamePattern>${LOG_HOME}/debug/tracker.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.OnMarkerEvaluator">
<marker>TRACE</marker>
</evaluator>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>${TRACKER_PATTERN}</pattern>
</encoder>
</appender>
<!-- 全局 Marker TurboFilter -->
<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
<Marker>RELEASE</Marker>
<OnMatch>ACCEPT</OnMatch>
<OnMismatch>DENY</OnMismatch>
</turboFilter>
<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
<Marker>DEBUG</Marker>
<OnMatch>ACCEPT</OnMatch>
<OnMismatch>NEUTRAL</OnMismatch>
</turboFilter>
<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
<Marker>TRACKER</Marker>
<OnMatch>ACCEPT</OnMatch>
<OnMismatch>DENY</OnMismatch>
</turboFilter>
<!-- Logger 定义 -->
<logger name="log.release" level="INFO" additivity="false">
<appender-ref ref="RELEASE_FILE"/>
@ -81,21 +82,17 @@
<logger name="log.debug" level="DEBUG" additivity="false">
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="RELEASE_FILE"/>
<appender-ref ref="STDOUT"/>
</logger>
<logger name="log.tracker" level="TRACE" additivity="false">
<appender-ref ref="TRACKER_FILE"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="RELEASE_FILE"/>
<appender-ref ref="STDOUT"/>
</logger>
<!-- ROOT Logger -->
<!-- ROOT Logger - 输出到控制台 -->
<root level="INFO">
<appender-ref ref="RELEASE_FILE"/>
<appender-ref ref="STDOUT"/>
</root>
</configuration>
</configuration>