feat:1.提供打包成 exe 任务 2.给第一、二步添加进度条,第三步暂未添加 3.规范第二步UI控制器里方法逻辑,只负责提交Task给另一线程执行,实际任务由Taskc类的 call方法组织调用相关的生成类和比较类来完成任务的回调 4.调整日志输出格式,release.log为标准输出格式,其它则仅供调试使用

This commit is contained in:
叁玖领域 2025-08-21 18:36:49 +08:00
parent 1bf1579e43
commit 204b1f38bf
37 changed files with 1179 additions and 627 deletions

View File

@ -1,3 +1,5 @@
import org.panteleyev.jpackage.ImageType
buildscript {
repositories {
google()
@ -12,8 +14,7 @@ plugins {
id 'org.openjfx.javafxplugin' version '0.1.0'
// 使 org.javamodularity.moduleplugin
// id 'org.javamodularity.moduleplugin' version '1.8.12'
// jlink
id 'org.beryx.jlink' version '2.26.0' apply false
id("org.panteleyev.jpackageplugin") version "1.6.1"
}
group = project_group
@ -62,7 +63,8 @@ dependencies {
implementation "org.openjfx:javafx-controls:$javafxVersion:$javafxPlatform"
implementation "org.openjfx:javafx-fxml:$javafxVersion:$javafxPlatform"
// classpath
implementation 'ch.qos.logback:logback-classic:1.5.18'
implementation 'ch.qos.logback:logback-core:1.5.18'
implementation 'ch.qos.logback:logback-classic:1.5.6'
implementation 'commons-cli:commons-cli:1.9.0'
implementation 'com.alibaba:easyexcel:4.0.3'
@ -145,7 +147,35 @@ tasks.register('buildFatJar', Jar) {
}
with jar // jar
archiveBaseName.set('doc-check-tool-cli')
archiveVersion.set('1.0')
archiveBaseName.set(project_name)
archiveVersion.set(project_version)
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
// =================== 便 ===================
tasks.register('buildPortable', Exec) {
group = 'distribution'
description = 'Build portable EXE (no installer)'
dependsOn buildFatJar
commandLine 'jpackage',
'--name', 'DocCheckTool',
'--input', "$buildDir/libs",
'--main-jar', "${project_name}-${project_version}.jar",
'--main-class', application.mainClass.get(),
'--type', 'app-image', // 便
'--app-version', project_version,
'--vendor', 'r3944realms',
'--dest', "$buildDir/distributions",
'--java-options', '-Dfile.encoding=UTF-8',
'--java-options', '-Xmx512m',
'--java-options', '-Xms256m',
'--verbose',
'--icon', file('src/main/resources/img/logo256x.ico').absolutePath
doFirst {
mkdir "$buildDir/distributions"
}
}

View File

@ -1,2 +1,3 @@
project_group =top.r3944realms.docchecktoolrefacored
project_version = 1.0-SNAPSHOT
project_name = doc-check-tool
project_version = 1.0

View File

@ -4,7 +4,7 @@ import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.core.Setting;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.*;
import java.nio.charset.StandardCharsets;
@ -38,10 +38,10 @@ public enum System {
if (Files.exists(configPath)) {
try (InputStream input = new FileInputStream(configPath.toFile())) {
properties.load(input);
log.debug(LoggerHelper.DEBUG_MARKER, "配置文件加载成功: {}", configPath);
log.debug(LoggerMarker.DEBUG_MARKER, "配置文件加载成功: {}", configPath);
INSTANCE.setting = propertiesToSetting(properties);
} catch (IOException e) {
log.error(LoggerHelper.DEBUG_MARKER, "读取配置文件失败: {}, 使用默认配置", e.getMessage());
log.error(LoggerMarker.DEBUG_MARKER, "读取配置文件失败: {}, 使用默认配置", e.getMessage());
INSTANCE.setting = defaultSetting();
settingToProperties(INSTANCE.setting, properties);
}
@ -49,7 +49,7 @@ public enum System {
INSTANCE.setting = defaultSetting();
settingToProperties(INSTANCE.setting, properties);
saveSettings(); // 首次启动保存默认配置
log.debug(LoggerHelper.DEBUG_MARKER, "配置文件不存在,已创建默认配置: {}", configPath);
log.debug(LoggerMarker.DEBUG_MARKER, "配置文件不存在,已创建默认配置: {}", configPath);
}
} finally {
lock.writeLock().unlock();
@ -72,13 +72,13 @@ public enum System {
try (OutputStream output = new FileOutputStream(configPath.toFile())) {
properties.store(new OutputStreamWriter(output, StandardCharsets.UTF_8),
"DocCheckTool Configuration");
log.debug(LoggerHelper.DEBUG_MARKER, "配置文件保存成功: {}", configPath);
log.debug(LoggerMarker.DEBUG_MARKER, "配置文件保存成功: {}", configPath);
} catch (IOException e) {
log.error(LoggerHelper.DEBUG_MARKER, "保存配置文件失败: {}", e.getMessage());
log.error(LoggerMarker.DEBUG_MARKER, "保存配置文件失败: {}", e.getMessage());
}
}
} catch (IOException e) {
log.error(LoggerHelper.DEBUG_MARKER, "创建配置目录失败: {}", e.getMessage());
log.error(LoggerMarker.DEBUG_MARKER, "创建配置目录失败: {}", e.getMessage());
} finally {
lock.readLock().unlock();
}
@ -86,14 +86,28 @@ public enum System {
/** 获取配置文件路径 */
private static Path getConfigPath() {
String userHome = java.lang.System.getProperty("user.home");
return Paths.get(userHome, ".docchecktool", CONFIG_FILE_NAME);
try {
// 获取程序运行目录Jar 所在目录
File jarFile = new File(System.class.getProtectionDomain()
.getCodeSource().getLocation().toURI());
File appDir = jarFile.getParentFile();
Path configDir = appDir.toPath().resolve("config");
if (!Files.exists(configDir)) {
Files.createDirectories(configDir);
}
return configDir.resolve(CONFIG_FILE_NAME);
} catch (Exception e) {
// 出错 fallback 到当前工作目录
Path fallbackDir = Paths.get(java.lang.System.getProperty("user.dir"), "config");
try { Files.createDirectories(fallbackDir); } catch (IOException ignored) {}
return fallbackDir.resolve(CONFIG_FILE_NAME);
}
}
/** 将Setting对象转换为Properties */
private static void settingToProperties(Setting setting, Properties props) {
props.setProperty("singleTimeout", String.valueOf(setting.getSingleTimeout()));
props.setProperty("totalTimeout", String.valueOf(setting.getTotalTimeout()));
props.setProperty("singleTimeout", String.valueOf(setting.getScanTimeout()));
props.setProperty("totalTimeout", String.valueOf(setting.getTaskTimeout()));
props.setProperty("enableStep", String.valueOf(setting.isEnableStep()));
}
@ -102,23 +116,23 @@ public enum System {
Setting s = new Setting();
try {
s.setSingleTimeout(Long.parseLong(props.getProperty("singleTimeout", String.valueOf(DEFAULT_SINGLE_TIMEOUT))));
s.setScanTimeout(Long.parseLong(props.getProperty("scanTimeOutS", String.valueOf(DEFAULT_SINGLE_TIMEOUT))));
} catch (NumberFormatException e) {
s.setSingleTimeout(DEFAULT_SINGLE_TIMEOUT);
log.error(LoggerHelper.DEBUG_MARKER, "singleTimeout格式错误,使用默认值{}", DEFAULT_SINGLE_TIMEOUT);
s.setScanTimeout(DEFAULT_SINGLE_TIMEOUT);
log.error(LoggerMarker.DEBUG_MARKER, "scanTimeOutS格式错误,使用默认值{}", DEFAULT_SINGLE_TIMEOUT);
}
try {
s.setTotalTimeout(Long.parseLong(props.getProperty("totalTimeout", String.valueOf(DEFAULT_TOTAL_TIMEOUT))));
s.setTaskTimeout(Long.parseLong(props.getProperty("taskTimeOutS", String.valueOf(DEFAULT_TOTAL_TIMEOUT))));
} catch (NumberFormatException e) {
s.setTotalTimeout(DEFAULT_TOTAL_TIMEOUT);
log.error(LoggerHelper.DEBUG_MARKER, "totalTimeout格式错误,使用默认值{}", DEFAULT_TOTAL_TIMEOUT);
s.setTaskTimeout(DEFAULT_TOTAL_TIMEOUT);
log.error(LoggerMarker.DEBUG_MARKER, "taskTimeOutS格式错误,使用默认值{}", DEFAULT_TOTAL_TIMEOUT);
}
try {
s.setEnableStep(Boolean.parseBoolean(props.getProperty("enableStep", String.valueOf(Boolean.FALSE))));
} catch (Exception e) {
s.setEnableStep(DEFAULT_ENABLE_STEP);
log.error(LoggerHelper.DEBUG_MARKER, "enableStep格式错误使用默认值{}", DEFAULT_TOTAL_TIMEOUT);
log.error(LoggerMarker.DEBUG_MARKER, "enableStep格式错误使用默认值{}", DEFAULT_TOTAL_TIMEOUT);
}
return s;
@ -127,8 +141,8 @@ public enum System {
/** 获取默认Setting */
private static Setting defaultSetting() {
Setting s = new Setting();
s.setSingleTimeout(DEFAULT_SINGLE_TIMEOUT);
s.setTotalTimeout(DEFAULT_TOTAL_TIMEOUT);
s.setScanTimeout(DEFAULT_SINGLE_TIMEOUT);
s.setTaskTimeout(DEFAULT_TOTAL_TIMEOUT);
return s;
}
@ -185,4 +199,11 @@ public enum System {
}
return directoryChooser;
}
/**
* @return CPU核心数
*/
public static Integer getAvailableProcessors() {
return Runtime.getRuntime().availableProcessors();
}
}

View File

@ -8,7 +8,7 @@ import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
import top.r3944realms.docchecktoolrefactored.model.DuplicateGroup;
import top.r3944realms.docchecktoolrefactored.util.FileUtil;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.BufferedWriter;
import java.io.IOException;
@ -103,7 +103,7 @@ public class CliProcessor {
printHelp(options);
System.exit(1);
} catch (Exception e) {
log.error(LoggerHelper.DEBUG_MARKER, "Error processing CLI command", e);
log.error(LoggerMarker.DEBUG_MARKER, "Error processing CLI command", e);
System.err.println("Error: " + e.getMessage());
System.exit(1);
}

View File

@ -1,14 +1,24 @@
package top.r3944realms.docchecktoolrefactored.core;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.docchecktoolrefactored.System;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Slf4j
public class AddressFileComparator {
@ -17,7 +27,20 @@ public class AddressFileComparator {
PAGE_LEVEL, // 页面级比较
FILE_LEVEL // 文件级比较
}
// 进度回调接口
public interface ProgressCallback {
default void onPhaseStarted(Phase phase) {}
default void onPhaseProgress(Phase phase, int current, int total) {}
default void onPhaseCompleted(Phase phase) {}
}
// 比较阶段
public enum Phase {
READ_PHYSICAL_CSV,
READ_LOGICAL_CSV,
COMPARE_FORWARD,
COMPARE_BACKWARD
}
@Getter
public static class ComparisonResult {
private final int physicalRecordsCount; // 物理文件记录数
@ -40,224 +63,270 @@ public class AddressFileComparator {
this.pathMismatchResults = pathMismatchResults;
this.pageCountMismatchResults = pageCountMismatchResults;
}
public static @NotNull String generateResult(AddressFileComparator.CompareMode compareMode, AddressFileComparator.ComparisonResult result) {
StringBuilder resultText = new StringBuilder();
resultText.append("读取物理地址文件记录数: ").append(result.getPhysicalRecordsCount()).append("\n");
resultText.append("读取逻辑地址文件记录数: ").append(result.getLogicalRecordsCount()).append("\n\n");
// 为向后兼容保留原来的构造函数
public ComparisonResult(int physicalRecordsCount,
int logicalRecordsCount,
List<String> forwardResults,
List<String> backwardResults,
List<String> pathMismatchResults) {
this(physicalRecordsCount, logicalRecordsCount, forwardResults,
backwardResults, pathMismatchResults, new ArrayList<>());
if (!result.getPathMismatchResults().isEmpty()) {
resultText.append("文件名相同但路径不一致的记录数量: ")
.append(result.getPathMismatchResults().size()).append("\n");
result.getPathMismatchResults().forEach(s -> resultText.append("\t").append(s).append("\n"));
} else resultText.append("没有路径错误\n\n");
if (compareMode == AddressFileComparator.CompareMode.FILE_LEVEL &&
!result.getPageCountMismatchResults().isEmpty()) {
resultText.append("文件名和路径相同但页数不一致的记录数量: ")
.append(result.getPageCountMismatchResults().size()).append("\n");
result.getPageCountMismatchResults().forEach(s -> resultText.append("\t").append(s).append("\n"));
}
if (!result.getForwardComparisonResults().isEmpty()) {
resultText.append("物理文件在逻辑文件中未找到的记录数量: ")
.append(result.getForwardComparisonResults().size()).append("\n");
result.getForwardComparisonResults().forEach(s -> resultText.append("\t").append(s).append("\n"));
} else resultText.append("没有物理存在而逻辑不存在的文件\n\n");
if (!result.getBackwardComparisonResults().isEmpty()) {
resultText.append("逻辑文件在物理文件中未找到的记录数量: ")
.append(result.getBackwardComparisonResults().size()).append("\n");
result.getBackwardComparisonResults().forEach(s -> resultText.append("\t").append(s).append("\n"));
} else resultText.append("没有逻辑存在而物理不存在的文件\n");
if (result.getPathMismatchResults().isEmpty() &&
result.getForwardComparisonResults().isEmpty() &&
result.getBackwardComparisonResults().isEmpty()) {
resultText.append("\n所有文件比对一致无差异。\n");
}
return resultText.toString();
}
public static String generateComparisonResults(ComparisonResult result, CompareMode compareMode) {
return generateComparisonResults(
result.physicalRecordsCount,
result.logicalRecordsCount,
result.forwardComparisonResults,
result.backwardComparisonResults,
result.pathMismatchResults,
result.pageCountMismatchResults,
compareMode
);
}
private static String generateComparisonResults(int physicalCount, int logicalCount,
List<String> forwardResults, List<String> backwardResults,
List<String> pathMismatchResults, List<String> pageCountMismatchResults,
CompareMode compareMode) {
StringBuilder sb = new StringBuilder();
sb.append("=== 文件比较结果 ===\n");
sb.append("物理地址文件记录数: ").append(physicalCount).append("\n");
sb.append("逻辑地址文件记录数: ").append(logicalCount).append("\n");
if (pathMismatchResults.isEmpty()) {
sb.append("没有路径错误\n");
} else {
sb.append("文件名相同但路径不一致的记录数量: ").append(pathMismatchResults.size()).append("\n");
pathMismatchResults.forEach(result -> sb.append("\t").append(result).append("\n"));
}
if (compareMode == CompareMode.FILE_LEVEL) {
if (pageCountMismatchResults.isEmpty()) {
sb.append("没有页数错误\n");
} else {
sb.append("文件名和路径相同但页数不一致的记录数量: ")
.append(pageCountMismatchResults.size()).append("\n");
pageCountMismatchResults.forEach(result -> sb.append("\t").append(result).append("\n"));
}
}
if (forwardResults.isEmpty()) {
sb.append("没有物理存在而逻辑不存在的文件\n");
} else {
sb.append("物理文件在逻辑文件中未找到的记录数量: ").append(forwardResults.size()).append("\n");
forwardResults.forEach(result -> sb.append("\t").append(result).append("\n"));
}
if (backwardResults.isEmpty()) {
sb.append("没有逻辑存在而物理不存在的文件\n");
} else {
sb.append("逻辑文件在物理文件中未找到的记录数量: ").append(backwardResults.size()).append("\n");
backwardResults.forEach(result -> sb.append("\t").append(result).append("\n"));
}
sb.append("=== 比较完成 ===");
return sb.toString();
}
}
public ComparisonResult compareFiles(String physicalAddressFilePath, String logicalAddressFilePath) {
return compareFiles(physicalAddressFilePath, logicalAddressFilePath, CompareMode.PAGE_LEVEL);
private final ExecutorService executor;
public AddressFileComparator(int threadPoolSize) {
this.executor = Executors.newFixedThreadPool(threadPoolSize);
}
public ComparisonResult compareFiles(String physicalAddressFilePath, String logicalAddressFilePath, CompareMode compareMode) {
List<String[]> physicalRecords = readCSV(physicalAddressFilePath);
List<String[]> logicalRecords = readCSV(logicalAddressFilePath);
// 记录读取的行数不包括标题行
int physicalCount = physicalRecords.size();
int logicalCount = logicalRecords.size();
log.info(LoggerHelper.DEBUG_MARKER, "读取物理地址文件记录数: {}, 读取逻辑地址文件记录数: {}", physicalCount, logicalCount);
List<String> forwardComparisonResults = new ArrayList<>(); // 物理文件在逻辑文件中未找到
List<String> backwardComparisonResults = new ArrayList<>(); // 逻辑文件在物理文件中未找到
List<String> pathMismatchResults = new ArrayList<>(); // 文件名相同但路径不一致
List<String> pageCountMismatchResults = new ArrayList<>(); // 文件名和路径相同但页数不一致仅文件级
// 正向比较遍历物理文件检查是否在逻辑文件中存在
for (String[] physicalRecord : physicalRecords) {
// 确保数据行有足够列数
if (compareMode == CompareMode.PAGE_LEVEL && physicalRecord.length < 2) continue;
if (compareMode == CompareMode.FILE_LEVEL && physicalRecord.length < 3) continue;
String physicalFileName = physicalRecord[0];
String physicalAddress = physicalRecord[1];
String physicalPageCount = compareMode == CompareMode.FILE_LEVEL ? physicalRecord[2] : null;
boolean found = false;
for (String[] logicalRecord : logicalRecords) {
// 确保数据行有足够列数
if (compareMode == CompareMode.PAGE_LEVEL && logicalRecord.length < 2) continue;
if (compareMode == CompareMode.FILE_LEVEL && logicalRecord.length < 3) continue;
String logicalFileName = logicalRecord[0];
String logicalAddress = logicalRecord[1];
String logicalPageCount = compareMode == CompareMode.FILE_LEVEL ? logicalRecord[2] : null;
if (physicalFileName.equals(logicalFileName)) {
found = true;
// 文件名相同比较路径
if (!physicalAddress.equals(logicalAddress)) {
pathMismatchResults.add("文件名=" + physicalFileName +
", 物理地址=" + physicalAddress +
", 逻辑地址=" + logicalAddress);
}
// 如果是文件级比较且路径相同再比较页数
else if (compareMode == CompareMode.FILE_LEVEL &&
physicalPageCount != null && logicalPageCount != null &&
!physicalPageCount.equals(logicalPageCount)) {
pageCountMismatchResults.add("文件名=" + physicalFileName +
", 物理地址=" + physicalAddress +
", 逻辑地址=" + logicalAddress +
", 物理页数=" + physicalPageCount +
", 逻辑页数=" + logicalPageCount);
}
break;
}
}
// 如果在逻辑文件中未找到该物理文件记录
if (!found) {
String result = "文件名=" + physicalFileName + ", 物理地址=" + physicalAddress;
if (compareMode == CompareMode.FILE_LEVEL && physicalPageCount != null) {
result += ", 物理页数=" + physicalPageCount;
}
forwardComparisonResults.add(result);
}
public AddressFileComparator() {
this.executor = Executors.newFixedThreadPool(System.getAvailableProcessors());
}
@Setter
private ProgressCallback progressCallback;
// 安全调用回调方法
private void safeOnPhaseStarted(Phase phase) {
if (progressCallback != null) {
progressCallback.onPhaseStarted(phase);
}
// 反向比较遍历逻辑文件检查是否在物理文件中存在
for (String[] logicalRecord : logicalRecords) {
// 确保数据行有足够列数
if (compareMode == CompareMode.PAGE_LEVEL && logicalRecord.length < 2) continue;
if (compareMode == CompareMode.FILE_LEVEL && logicalRecord.length < 3) continue;
String logicalFileName = logicalRecord[0];
String logicalAddress = logicalRecord[1];
String logicalPageCount = compareMode == CompareMode.FILE_LEVEL ? logicalRecord[2] : null;
boolean found = false;
for (String[] physicalRecord : physicalRecords) {
// 确保数据行有足够列数
if (compareMode == CompareMode.PAGE_LEVEL && physicalRecord.length < 2) continue;
if (compareMode == CompareMode.FILE_LEVEL && physicalRecord.length < 3) continue;
String physicalFileName = physicalRecord[0];
if (logicalFileName.equals(physicalFileName)) {
found = true;
break;
}
}
// 如果在物理文件中未找到该逻辑文件记录
if (!found) {
String result = "文件名=" + logicalFileName + ", 逻辑地址=" + logicalAddress;
if (compareMode == CompareMode.FILE_LEVEL && logicalPageCount != null) {
result += ", 逻辑页数=" + logicalPageCount;
}
backwardComparisonResults.add(result);
}
}
// 将比较结果记录到日志中
logComparisonResults(physicalCount, logicalCount, forwardComparisonResults,
backwardComparisonResults, pathMismatchResults, pageCountMismatchResults, compareMode);
return new ComparisonResult(
physicalCount,
logicalCount,
forwardComparisonResults,
backwardComparisonResults,
pathMismatchResults,
pageCountMismatchResults
);
}
private List<String[]> readCSV(String filePath) {
private void safeOnPhaseProgress(Phase phase, int current, int total) {
if (progressCallback != null) {
progressCallback.onPhaseProgress(phase, current, total);
}
}
private void safeOnPhaseCompleted(Phase phase) {
if (progressCallback != null) {
progressCallback.onPhaseCompleted(phase);
}
}
public void shutdown() {
executor.shutdown();
}
public CompletableFuture<ComparisonResult> compareFiles(String physicalFilePath, String logicalFilePath, CompareMode compareMode) {
// 读取物理文件
CompletableFuture<List<String[]>> physicalFuture = CompletableFuture.supplyAsync(() -> {
safeOnPhaseStarted(Phase.READ_PHYSICAL_CSV);
List<String[]> list = readCSV(physicalFilePath, progressCallback, Phase.READ_PHYSICAL_CSV);
safeOnPhaseCompleted(Phase.READ_PHYSICAL_CSV);
return list;
}, executor);
// 读取逻辑文件
CompletableFuture<List<String[]>> logicalFuture = CompletableFuture.supplyAsync(() -> {
safeOnPhaseStarted(Phase.READ_LOGICAL_CSV);
List<String[]> list = readCSV(logicalFilePath, progressCallback, Phase.READ_LOGICAL_CSV);
safeOnPhaseCompleted(Phase.READ_LOGICAL_CSV);
return list;
}, executor);
// 两个 CSV 读取完成后进行比较
return physicalFuture.thenCombineAsync(logicalFuture, (physicalRecords, logicalRecords) -> {
int physicalCount = physicalRecords.size();
int logicalCount = logicalRecords.size();
// 线程安全集合
List<String> forwardComparisonResults = Collections.synchronizedList(new ArrayList<>());
List<String> backwardComparisonResults = Collections.synchronizedList(new ArrayList<>());
List<String> pathMismatchResults = Collections.synchronizedList(new ArrayList<>());
List<String> pageCountMismatchResults = Collections.synchronizedList(new ArrayList<>());
Map<String, String[]> logicalMap = logicalRecords.parallelStream()
.filter(r -> r.length >= (compareMode == CompareMode.FILE_LEVEL ? 3 : 2))
.collect(Collectors.toConcurrentMap(r -> r[0], r -> r));
// 正向比较
safeOnPhaseStarted(Phase.COMPARE_FORWARD);
AtomicInteger forwardCounter = new AtomicInteger(0);
physicalRecords.parallelStream()
.filter(r -> r.length >= (compareMode == CompareMode.FILE_LEVEL ? 3 : 2))
.forEach(physicalRecord -> {
String physicalFileName = physicalRecord[0];
String physicalAddress = physicalRecord[1];
String physicalPageCount = compareMode == CompareMode.FILE_LEVEL ? physicalRecord[2] : null;
String[] logicalRecord = logicalMap.get(physicalFileName);
if (logicalRecord == null) {
String result = "文件名=" + physicalFileName + ", 物理地址=" + physicalAddress;
if (compareMode == CompareMode.FILE_LEVEL && physicalPageCount != null) {
result += ", 物理页数=" + physicalPageCount;
}
forwardComparisonResults.add(result);
} else {
if (!physicalAddress.equals(logicalRecord[1])) {
pathMismatchResults.add("文件名=" + physicalFileName +
", 物理地址=" + physicalAddress +
", 逻辑地址=" + logicalRecord[1]);
}
if (compareMode == CompareMode.FILE_LEVEL &&
physicalPageCount != null && !physicalPageCount.equals(logicalRecord[2])) {
pageCountMismatchResults.add("文件名=" + physicalFileName +
", 物理地址=" + physicalAddress +
", 逻辑地址=" + logicalRecord[1] +
", 物理页数=" + physicalPageCount +
", 逻辑页数=" + logicalRecord[2]);
}
}
int done = forwardCounter.incrementAndGet();
safeOnPhaseProgress(Phase.COMPARE_FORWARD, done, physicalRecords.size());
});
safeOnPhaseCompleted(Phase.COMPARE_FORWARD);
// 反向比较
safeOnPhaseStarted(Phase.COMPARE_BACKWARD);
AtomicInteger backwardCounter = new AtomicInteger(0);
Map<String, String[]> physicalMap = physicalRecords.parallelStream()
.filter(r -> r.length >= (compareMode == CompareMode.FILE_LEVEL ? 3 : 2))
.collect(Collectors.toConcurrentMap(r -> r[0], r -> r));
logicalRecords.parallelStream()
.filter(r -> r.length >= (compareMode == CompareMode.FILE_LEVEL ? 3 : 2))
.forEach(logicalRecord -> {
String logicalFileName = logicalRecord[0];
String logicalAddress = logicalRecord[1];
String logicalPageCount = compareMode == CompareMode.FILE_LEVEL ? logicalRecord[2] : null;
if (!physicalMap.containsKey(logicalFileName)) {
String result = "文件名=" + logicalFileName + ", 逻辑地址=" + logicalAddress;
if (compareMode == CompareMode.FILE_LEVEL && logicalPageCount != null) {
result += ", 逻辑页数=" + logicalPageCount;
}
backwardComparisonResults.add(result);
}
int done = backwardCounter.incrementAndGet();
safeOnPhaseProgress(Phase.COMPARE_BACKWARD, done, logicalRecords.size());
});
safeOnPhaseCompleted(Phase.COMPARE_BACKWARD);
return new ComparisonResult(
physicalCount,
logicalCount,
forwardComparisonResults,
backwardComparisonResults,
pathMismatchResults,
pageCountMismatchResults
);
}, executor);
}
private List<String[]> readCSV(String filePath, ProgressCallback callback, Phase phase) {
List<String[]> records = new ArrayList<>();
try {
File file = new File(filePath);
if (!file.exists()) {
log.error(LoggerHelper.RELEASE_MARKER, "CSV文件不存在: {}", filePath);
log.error(LoggerMarker.RELEASE_MARKER, "{} CSV文件不存在: {}", phase, filePath);
return records;
}
BufferedReader reader = new BufferedReader(new FileReader(file));
String line;
boolean isFirstLine = true;
while ((line = reader.readLine()) != null) {
// 跳过首行标题行
if (isFirstLine) {
isFirstLine = false;
continue;
}
// 按逗号分隔CSV行数据
String[] data = line.split(",");
records.add(data);
}
List<String> allLines = reader.lines().skip(1).toList(); // 跳过标题行
reader.close();
log.info(LoggerHelper.DEBUG_MARKER, "成功读取CSV文件共 {} 行记录", records.size());
int total = allLines.size();
AtomicInteger counter = new AtomicInteger(0);
for (String line : allLines) {
records.add(line.split(","));
int done = counter.incrementAndGet();
if (done % 100 == 0 || done == total) { // 每100行或结束更新
callback.onPhaseProgress(phase, done, total);
}
}
} catch (Exception e) {
log.error(LoggerHelper.RELEASE_MARKER, "读取CSV文件时出错: {}", e.getMessage(), e);
log.error(LoggerMarker.RELEASE_MARKER, "{} CSV文件读取出错: {}", phase, e.getMessage(), e);
}
return records;
}
private void logComparisonResults(int physicalCount, int logicalCount,
List<String> forwardResults, List<String> backwardResults,
List<String> pathMismatchResults, List<String> pageCountMismatchResults,
CompareMode compareMode) {
StringBuilder sb = new StringBuilder();
sb.append("=== 文件比较结果 ===\n");
sb.append("物理地址文件记录数: ").append(physicalCount).append("\n");
sb.append("逻辑地址文件记录数: ").append(logicalCount).append("\n");
if (pathMismatchResults.isEmpty()) {
sb.append("没有路径错误\n");
} else {
sb.append("文件名相同但路径不一致的记录数量: ").append(pathMismatchResults.size()).append("\n");
pathMismatchResults.forEach(result -> sb.append("\t").append(result).append("\n"));
}
if (compareMode == CompareMode.FILE_LEVEL) {
if (pageCountMismatchResults.isEmpty()) {
sb.append("没有页数错误\n");
} else {
sb.append("文件名和路径相同但页数不一致的记录数量: ")
.append(pageCountMismatchResults.size()).append("\n");
pageCountMismatchResults.forEach(result -> sb.append("\t").append(result).append("\n"));
}
}
if (forwardResults.isEmpty()) {
sb.append("没有物理存在而逻辑不存在的文件\n");
} else {
sb.append("物理文件在逻辑文件中未找到的记录数量: ").append(forwardResults.size()).append("\n");
forwardResults.forEach(result -> sb.append("\t").append(result).append("\n"));
}
if (backwardResults.isEmpty()) {
sb.append("没有逻辑存在而物理不存在的文件\n");
} else {
sb.append("逻辑文件在物理文件中未找到的记录数量: ").append(backwardResults.size()).append("\n");
backwardResults.forEach(result -> sb.append("\t").append(result).append("\n"));
}
sb.append("=== 比较完成 ===");
log.info(LoggerHelper.RELEASE_MARKER, sb.toString()); // 一次性输出, 减少 I/O
}
// 为向后兼容保留原来的日志方法
private void logComparisonResults(int physicalCount, int logicalCount,
List<String> forwardResults, List<String> backwardResults,
List<String> pathMismatchResults) {
logComparisonResults(physicalCount, logicalCount, forwardResults, backwardResults,
pathMismatchResults, new ArrayList<>(), CompareMode.PAGE_LEVEL);
}
}

View File

@ -15,13 +15,19 @@ public interface AddressFileGenerator {
int FILE_TYPE = 2;
/**
* 回调接口
*/
interface Callback {
void onProgress(String message);
void onSuccess(String outputPath);
void onError(String errorMessage);
* 进度回调接口
*/
interface ProgressCallback {
default void onPhaseStarted(Phase phase) {}
default void onPhaseProgress(Phase phase, int current, int total) {}
default void onPhaseCompleted(Phase phase) {}
}
enum Phase {
GENERATE_LOGICAL,
GENERATE_PHYSICAL
}
void setProgressCallback(ProgressCallback callback);
/**
* 生成地址文件
@ -29,12 +35,10 @@ public interface AddressFileGenerator {
* @param folderPath 文件夹路径
* @param outputFile 输出文件
* @param folderType 文件夹类型 ({@link AddressFileGenerator#PAGE_TYPE "页面级"} {@link AddressFileGenerator#FILE_TYPE "文件级"})
* @param callback {@link Callback 回调接口}
*/
void generateAddressFile(
String folderPath,
File outputFile,
@MagicConstant(intValues = {PAGE_TYPE, FILE_TYPE}) int folderType,
Callback callback
@MagicConstant(intValues = {PAGE_TYPE, FILE_TYPE}) int folderType
);
}

View File

@ -1,13 +1,13 @@
package top.r3944realms.docchecktoolrefactored.core;
import com.sun.scenario.Settings;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.System;
import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
import top.r3944realms.docchecktoolrefactored.model.DuplicateGroup;
import top.r3944realms.docchecktoolrefactored.model.FileMetadata;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.IOException;
import java.nio.file.Files;
@ -41,7 +41,7 @@ public class DuplicateFinder {
@Setter
private ProgressCallback progressCallback;
private static final int PROGRESS_REPORT_INTERVAL = 100;
private static final int BATCH_SIZE = 100;
@Getter
private final List<Exception> errors = new CopyOnWriteArrayList<>();
@Getter
private long timeout = -1;
@ -49,16 +49,14 @@ public class DuplicateFinder {
this.fileScanner = Objects.requireNonNull(fileScanner);
this.hashCalculator = Objects.requireNonNull(hashCalculator);
this.enableProgress = enableProgress;
// 根据CPU核心数设置线程池大小
int poolSize = Runtime.getRuntime().availableProcessors();
this.executorService = Executors.newFixedThreadPool(poolSize);
this.executorService = Executors.newFixedThreadPool(System.getAvailableProcessors());
}
public DuplicateFinder(FileScanner fileScanner, FileHashCalculator hashCalculator) {
this(fileScanner, hashCalculator, false);
}
public DuplicateFinder applySetting(Setting setting) {
this.timeout = setting.getSingleTimeout();
this.timeout = setting.getScanTimeout();
return this;
}
@ -113,7 +111,7 @@ public class DuplicateFinder {
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
if (progressCallback != null) progressCallback.onPhaseCompleted(Phase.CALCULATE_HASH);
if (enableProgress) System.out.println();
if (enableProgress) java.lang.System.out.println();
// -----------------------------
// 第三阶段构建结果
@ -154,13 +152,13 @@ public class DuplicateFinder {
meta.setSize(Files.size(file));
sizeGroups.computeIfAbsent(meta.getSize(), k -> new ArrayList<>()).add(meta);
} catch (IOException e) {
log.error(LoggerHelper.TRACE_MARKER, "Failed to get file's size: {}", file);
log.error(LoggerMarker.TRACE_MARKER, "Failed to get file's size: {}", file);
}
}
@Override public void onScanComplete() {}
@Override public void onError(Path file, Exception e) {
log.error(LoggerHelper.TRACE_MARKER, "Error on scanning file: {}, {}", file, e.getMessage());
log.error(LoggerMarker.TRACE_MARKER, "Error on scanning file: {}, {}", file, e.getMessage());
errors.add(e);
}
};
@ -188,7 +186,7 @@ public class DuplicateFinder {
current,
total);
System.out.print(progressBar);
java.lang.System.out.print(progressBar);
}
private void processFile(FileMetadata file,
Map<String, List<FileMetadata>> hashGroups,
@ -214,11 +212,12 @@ public class DuplicateFinder {
}
} catch (IOException e) {
log.error(LoggerHelper.TRACE_MARKER, "Failed to calculate file's hash: {}, {}", file.getPath(), e.getMessage());
log.error(LoggerMarker.TRACE_MARKER, "Failed to calculate file's hash: {}, {}", file.getPath(), e.getMessage());
errors.add(e);
}
}
public void shutdown() {
fileScanner.cancel();
executorService.shutdown();
}
}

View File

@ -4,7 +4,7 @@ package top.r3944realms.docchecktoolrefactored.core;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.BufferedWriter;
import java.io.FileWriter;
@ -49,7 +49,7 @@ public class HashFileGenerator {
@Override
public void onError(Path path, Exception e) {
log.error(LoggerHelper.TRACE_MARKER, "Error scanning path: {} - {}", path, e.getMessage());
log.error(LoggerMarker.TRACE_MARKER, "Error scanning path: {} - {}", path, e.getMessage());
}
});
@ -76,7 +76,7 @@ public class HashFileGenerator {
listener.onProgressUpdate(processed, totalFiles);
}
} catch (IOException e) {
log.error(LoggerHelper.DEBUG_MARKER, "无法计算该文件哈希值: {} - {}", file, e.getMessage());
log.error(LoggerMarker.DEBUG_MARKER, "无法计算该文件哈希值: {} - {}", file, e.getMessage());
}
});

View File

@ -1,49 +1,78 @@
package top.r3944realms.docchecktoolrefactored.core;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.io.reader.CatalogFileReader;
import top.r3944realms.docchecktoolrefactored.io.reader.CatalogFileReaderFactory;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.File;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Slf4j
public class LogicalAddressFileGenerator implements AddressFileGenerator {
private ProgressCallback callback;
// 安全调用回调方法
private void safeOnPhaseStarted(Phase phase) {
if (callback != null) {
callback.onPhaseStarted(phase);
}
}
private void safeOnPhaseProgress(Phase phase, int current, int total) {
if (callback != null) {
callback.onPhaseProgress(phase, current, total);
}
}
private void safeOnPhaseCompleted(Phase phase) {
if (callback != null) {
callback.onPhaseCompleted(phase);
}
}
@Override
public void generateAddressFile(String catalogFilePath, File outputFile, int folderType, Callback callback) {
callback.onProgress("正在生成逻辑地址文件...");
public void setProgressCallback(ProgressCallback callback) {
this.callback = callback;
}
@Override
public void generateAddressFile(String catalogFilePath, File outputFile, int folderType) {
safeOnPhaseStarted(Phase.GENERATE_LOGICAL);
try {
// 使用工厂模式创建相应的文件读取器
List<Record> records = readCatalogFile(catalogFilePath);
safeOnPhaseProgress(Phase.GENERATE_LOGICAL, 1, 3); // 1/3
// 过滤掉 可能的 输出文件记录 -- 注2: 避免将输出文件作为输入处理
String outputFileName = outputFile.getName();
records = records.stream()
.filter(record -> !record.archiveCode.equalsIgnoreCase(outputFileName))
.toList();
safeOnPhaseProgress(Phase.GENERATE_LOGICAL, 2, 3); // 2/3
try (PrintWriter writer = new PrintWriter(outputFile, StandardCharsets.UTF_8)) {
if (folderType == PAGE_TYPE) {
// 页面级逻辑为每页生成一行数据
generatePageLevelFile(writer, records);
generatePageLevelFile(writer, records, callback);
} else if (folderType == FILE_TYPE) {
// 文件级逻辑每个档案只生成一行数据
generateFileLevelFile(writer, records);
generateFileLevelFile(writer, records, callback);
} else {
throw new IllegalArgumentException("不支持的文件夹类型: " + folderType);
}
}
callback.onSuccess("逻辑地址文件生成成功: " + outputFile.getAbsolutePath());
safeOnPhaseProgress(Phase.GENERATE_LOGICAL, 3, 3); // 3/3
safeOnPhaseCompleted(Phase.GENERATE_LOGICAL);
} catch (Exception e) {
callback.onError("生成逻辑地址文件时出错: " + e.getMessage());
log.info(LoggerMarker.RELEASE_MARKER, "生成逻辑地址文件时出错: {}", e.getMessage());
}
}
/**
* 生成页面级逻辑地址文件
*/
private void generatePageLevelFile(PrintWriter writer, List<Record> records) {
private void generatePageLevelFile(PrintWriter writer, List<Record> records, ProgressCallback callback) {
// 写入CSV头部
writer.println("逻辑文件名,逻辑地址");
int totalRecords = records.stream().mapToInt(r -> r.page).sum();
int current = 0;
// 处理每条记录
for (Record record : records) {
@ -60,6 +89,8 @@ public class LogicalAddressFileGenerator implements AddressFileGenerator {
// 写入CSV行
writer.printf("%s,%s%n", logicalFileName, logicalAddress);
current++;
safeOnPhaseProgress(Phase.GENERATE_LOGICAL, current, totalRecords);
}
}
}
@ -67,9 +98,11 @@ public class LogicalAddressFileGenerator implements AddressFileGenerator {
/**
* 生成文件级逻辑地址文件
*/
private void generateFileLevelFile(PrintWriter writer, List<Record> records) {
private void generateFileLevelFile(PrintWriter writer, List<Record> records, ProgressCallback callback) {
// 写入CSV头部包含页数列
writer.println("逻辑文件名,逻辑地址,页数");
int totalRecords = records.stream().mapToInt(r -> r.page).sum();
int current = 0;
// 处理每条记录
for (Record record : records) {
@ -81,6 +114,7 @@ public class LogicalAddressFileGenerator implements AddressFileGenerator {
// 写入CSV行包含页数
writer.printf("%s,%s,%d%n", /* 逻辑文件名(就是档号)*/ archiveCode, logicalAddress, page);
safeOnPhaseProgress(Phase.GENERATE_LOGICAL, current, totalRecords);
}
}

View File

@ -1,9 +1,10 @@
package top.r3944realms.docchecktoolrefactored.core;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.File;
import java.io.PrintWriter;
@ -11,39 +12,85 @@ import java.nio.charset.StandardCharsets;
@Slf4j
public class PhysicalAddressFileGenerator implements AddressFileGenerator {
private ProgressCallback callback;
// 安全调用回调方法
private void safeOnPhaseStarted(Phase phase) {
if (callback != null) {
callback.onPhaseStarted(phase);
}
}
private void safeOnPhaseProgress(Phase phase, int current, int total) {
if (callback != null) {
callback.onPhaseProgress(phase, current, total);
}
}
private void safeOnPhaseCompleted(Phase phase) {
if (callback != null) {
callback.onPhaseCompleted(phase);
}
}
@Override
public void generateAddressFile(String folderPath, File outputFile, int folderType, Callback callback) {
callback.onProgress("正在生成物理地址文件...");
public void setProgressCallback(ProgressCallback callback) {
this.callback = callback;
}
@Override
public void generateAddressFile(String folderPath, File outputFile, int folderType) {
safeOnPhaseStarted(Phase.GENERATE_PHYSICAL);
try {
File rootFolder = new File(folderPath);
if (!rootFolder.exists() || !rootFolder.isDirectory()) {
callback.onError("所选路径不存在或不是一个有效的文件夹。");
safeOnPhaseCompleted(Phase.GENERATE_PHYSICAL);
safeOnPhaseProgress(Phase.GENERATE_PHYSICAL, 0, 0);
return;
}
// 保存输出文件的绝对路径用于后续比较
String outputFilePath = outputFile.getAbsolutePath();
// 统计总文件数页面级图片数文件级PDF数
int totalFiles = countFiles(rootFolder, folderType);
// 计数器
int[] counter = new int[]{0};
// 写入CSV文件
try (PrintWriter writer = new PrintWriter(outputFile, StandardCharsets.UTF_8)) {
if (folderType == AddressFileGenerator.PAGE_TYPE) {
// 页面级逻辑处理所有图片文件
if (folderType == PAGE_TYPE) {
writer.println("物理文件名,物理地址");
processPageLevelFolder(rootFolder, writer, outputFilePath);
} else if (folderType == AddressFileGenerator.FILE_TYPE) {
// 文件级逻辑处理PDF文件
processPageLevelFolder(rootFolder, writer, outputFile.getAbsolutePath(), callback, counter, totalFiles);
} else if (folderType == FILE_TYPE) {
writer.println("物理文件名,物理地址,页数");
processFileLevelFolder(rootFolder, writer, outputFilePath);
processFileLevelFolder(rootFolder, writer, outputFile.getAbsolutePath(), callback, counter, totalFiles);
} else {
throw new IllegalArgumentException("不支持的文件夹类型: " + folderType);
}
}
callback.onSuccess("物理地址文件生成成功: " + outputFile.getAbsolutePath());
safeOnPhaseCompleted(Phase.GENERATE_PHYSICAL);
} catch (Exception e) {
callback.onError("生成物理地址文件时出错: " + e.getMessage());
safeOnPhaseCompleted(Phase.GENERATE_PHYSICAL);
safeOnPhaseProgress(Phase.GENERATE_PHYSICAL, 0, 0);
log.error("生成物理地址文件失败: {}", e.getMessage(), e);
}
}
/** 递归统计总文件数 */
private int countFiles(File folder, int folderType) {
int count = 0;
File[] files = folder.listFiles(file -> !file.isHidden());
if (files != null) {
for (File file : files) {
if (file.isFile()) {
if ((folderType == PAGE_TYPE && isImageFile(file.getName()))
|| (folderType == FILE_TYPE && isPdfFile(file.getName()))) {
count++;
}
} else if (file.isDirectory()) {
count += countFiles(file, folderType);
}
}
}
return count;
}
/**
* 处理页面级文件夹及其内部文件
*
@ -51,26 +98,20 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator {
* @param writer PrintWriter对象
* @param outputFilePath 输出文件的绝对路径
*/
private void processPageLevelFolder(File folder, PrintWriter writer, String outputFilePath) {
private void processPageLevelFolder(File folder, PrintWriter writer, String outputFilePath, ProgressCallback callback, int[] counter, int total) {
// 获取该文件夹下的所有非隐藏文件和文件夹
File[] filesAndFolders = folder.listFiles(file -> !file.isHidden());
if (filesAndFolders != null) {
for (File file : filesAndFolders) {
// 跳过输出文件本身避免将生成的CSV文件也作为数据处理
if (file.getAbsolutePath().equals(outputFilePath)) {
continue;
}
if (file.getAbsolutePath().equals(outputFilePath)) continue;
if (file.isFile()) {
String fileName = file.getName();
// 只处理图片文件跳过其他类型的文件
if (!isImageFile(fileName)) {
continue;
}
// 只处理图片文件跳过其他类型的文件
if (file.isFile() && isImageFile(file.getName())) {
// 移除文件扩展名
String fileNameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));
String fileNameWithoutExt = file.getName().substring(0, file.getName().lastIndexOf('.'));
// 格式化文件名确保最后一个部分是4位数字
String formattedFileName = formatFileName(fileNameWithoutExt);
@ -80,9 +121,12 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator {
// 写入CSV行
writer.printf("%s,%s%n", formattedFileName, physicalAddress);
counter[0]++;
safeOnPhaseProgress(Phase.GENERATE_PHYSICAL, counter[0], total);
} else if (file.isDirectory()) {
// 递归处理子文件夹
processPageLevelFolder(file, writer, outputFilePath);
processPageLevelFolder(file, writer, outputFilePath, callback, counter, total);
}
}
}
@ -95,7 +139,7 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator {
* @param writer PrintWriter对象
* @param outputFilePath 输出文件的绝对路径
*/
private void processFileLevelFolder(File folder, PrintWriter writer, String outputFilePath) {
private void processFileLevelFolder(File folder, PrintWriter writer, String outputFilePath, ProgressCallback callback, int[] counter, int total) {
// 获取该文件夹下的所有非隐藏文件和文件夹
File[] filesAndFolders = folder.listFiles(file -> !file.isHidden());
@ -106,12 +150,9 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator {
continue;
}
if (file.isFile()) {
String fileName = file.getName();
// 只处理PDF文件
if (isPdfFile(fileName)) {
if (file.isFile()&& isPdfFile(file.getName())) {
// 移除文件扩展名
String fileNameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));
String fileNameWithoutExt = file.getName().substring(0, file.getName().lastIndexOf('.'));
// 生成物理地址路径使用与页面级相同的逻辑
String physicalAddress = generatePhysicalAddress(file.getAbsolutePath(), fileNameWithoutExt);
@ -121,10 +162,12 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator {
// 写入CSV行
writer.printf("%s,%s,%d%n", fileNameWithoutExt, physicalAddress, pageCount);
}
counter[0]++;
safeOnPhaseProgress(Phase.GENERATE_PHYSICAL, counter[0], total);
} else if (file.isDirectory()) {
// 递归处理子文件夹
processFileLevelFolder(file, writer, outputFilePath);
processFileLevelFolder(file, writer, outputFilePath, callback, counter, total);
}
}
}
@ -137,14 +180,13 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator {
* @return 页数
*/
private int getPdfPageCount(File pdfFile) {
try {
// 使用Apache PDFBox库获取PDF页数
PDDocument document = Loader.loadPDF(pdfFile);
// 使用Apache PDFBox库获取PDF页数
try (PDDocument document = Loader.loadPDF(pdfFile)){
int pageCount = document.getNumberOfPages();
document.close();
return pageCount;
} catch (Exception e) {
log.warn(LoggerHelper.RELEASE_MARKER, "无法获取PDF文件页数: {}", pdfFile.getAbsolutePath(), e);
log.warn(LoggerMarker.RELEASE_MARKER, "无法获取PDF文件页数: {}", pdfFile.getAbsolutePath(), e);
return 0;
}
}

View File

@ -8,7 +8,7 @@ import lombok.extern.slf4j.Slf4j;
@Getter
@Slf4j
public class Setting {
private long singleTimeout = 30;
private long totalTimeout = 60 * 5;
private long scanTimeout = 30;
private long taskTimeout = 60 * 5;
private boolean enableStep = false;
}

View File

@ -5,7 +5,7 @@ import com.linuxense.javadbf.DBFRow;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.util.FileUtil;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.File;
import java.io.FileInputStream;
@ -38,29 +38,29 @@ public class DbfFileReader implements CatalogFileReader {
DBFReader reader = new DBFReader(fis)
) {
int fieldCount = reader.getFieldCount();
log.debug(LoggerHelper.DEBUG_MARKER, "开始读取DBF文件: {}, DBF文件字段数: {}",filePath, fieldCount);
log.debug(LoggerMarker.DEBUG_MARKER, "开始读取DBF文件: {}, DBF文件字段数: {}",filePath, fieldCount);
// 查找"档号""页数"字段的索引
int archiveCodeIndex = -1;
int pageIndex = -1;
for (int i = 0; i < fieldCount; i++) {
if (archiveCodeIndex != -1 && pageIndex != -1) {
log.debug(LoggerHelper.DEBUG_MARKER, "已找到所需字段,跳出循环,档号: {}, 页数: {}", archiveCodeIndex, pageIndex);
log.debug(LoggerMarker.DEBUG_MARKER, "已找到所需字段,跳出循环,档号: {}, 页数: {}", archiveCodeIndex, pageIndex);
break;
}
String fieldName = reader.getField(i).getName();
log.debug(LoggerHelper.DEBUG_MARKER, "发现字段: {}", fieldName);
log.debug(LoggerMarker.DEBUG_MARKER, "发现字段: {}", fieldName);
if (ARCHIVE_CODE_TAG_CANDIDATES.contains(fieldName)) {
archiveCodeIndex = i;
log.debug(LoggerHelper.DEBUG_MARKER, "匹配到档号字段: {}, 索引: {}", fieldName, archiveCodeIndex);
log.debug(LoggerMarker.DEBUG_MARKER, "匹配到档号字段: {}, 索引: {}", fieldName, archiveCodeIndex);
} else if (PAGE_COUNT_TAG_CANDIDATES.contains(fieldName)) {
pageIndex = i;
log.debug(LoggerHelper.DEBUG_MARKER, "匹配到页数字段: {}, 索引: {}", fieldName, pageIndex);
log.debug(LoggerMarker.DEBUG_MARKER, "匹配到页数字段: {}, 索引: {}", fieldName, pageIndex);
}
}
if (archiveCodeIndex == -1 || pageIndex == -1) {
log.error(LoggerHelper.RELEASE_MARKER, "未找到必要字段,档号: {}, 页数: {}",
log.error(LoggerMarker.RELEASE_MARKER, "未找到必要字段,档号: {}, 页数: {}",
archiveCodeIndex == -1 ? "未找到" : archiveCodeIndex,
pageIndex == -1 ? "未找到" : pageIndex
);
@ -89,7 +89,7 @@ public class DbfFileReader implements CatalogFileReader {
return Integer.parseInt(i.toString().trim());
}
} catch (NumberFormatException e) {
log.warn(LoggerHelper.DEBUG_MARKER, "无法将页数值转换为整数: {}", i);
log.warn(LoggerMarker.DEBUG_MARKER, "无法将页数值转换为整数: {}", i);
return 0;
}
}).orElse(0);
@ -98,17 +98,17 @@ public class DbfFileReader implements CatalogFileReader {
if (!archiveCode.isEmpty() && page > 0) {
records.add(new LogicalAddressFileGenerator.Record(archiveCode, page));
validRecords++;
log.debug(LoggerHelper.DEBUG_MARKER, "读取有效记录 - 档号: {}, 页数: {}", archiveCode, page);
log.debug(LoggerMarker.DEBUG_MARKER, "读取有效记录 - 档号: {}, 页数: {}", archiveCode, page);
} else {
skippedRecords++;
if (!archiveCode.isEmpty() || page > 0) {
log.debug(LoggerHelper.DEBUG_MARKER, "跳过无效记录 - 档号: {}, 页数: {}", archiveCode, page);
log.debug(LoggerMarker.DEBUG_MARKER, "跳过无效记录 - 档号: {}, 页数: {}", archiveCode, page);
}
}
}
log.info(LoggerHelper.RELEASE_MARKER, "DBF文件读取完成有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords);
log.info(LoggerMarker.RELEASE_MARKER, "DBF文件读取完成有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords);
} catch (IOException e) {
log.error(LoggerHelper.RELEASE_MARKER, "读取DBF文件失败: {}", filePath, e);
log.error(LoggerMarker.RELEASE_MARKER, "读取DBF文件失败: {}", filePath, e);
throw new UncheckedIOException("DBF文件读取异常", e);
}
return records;

View File

@ -6,7 +6,7 @@ import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.util.FileUtil;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.File;
import java.io.FileInputStream;
@ -27,33 +27,33 @@ public class ExcelFileReader implements CatalogFileReader {
}
private List<LogicalAddressFileGenerator.Record> readExcelFile(File file, boolean isXlsx) throws Exception {
List<LogicalAddressFileGenerator.Record> records = new ArrayList<>();
log.debug(LoggerHelper.DEBUG_MARKER, "开始解析Excel文件格式: {}", isXlsx ? "xlsx" : "xls");
log.debug(LoggerMarker.DEBUG_MARKER, "开始解析Excel文件格式: {}", isXlsx ? "xlsx" : "xls");
try (FileInputStream fis = new FileInputStream(file);
Workbook workbook = isXlsx ? new XSSFWorkbook(fis) : new HSSFWorkbook(fis)) {
// 获取第一个工作表
Sheet sheet = workbook.getSheetAt(0);
log.debug(LoggerHelper.DEBUG_MARKER, "读取工作表: {}", sheet.getSheetName());
log.debug(LoggerMarker.DEBUG_MARKER, "读取工作表: {}", sheet.getSheetName());
// 获取标题行
Row headerRow = sheet.getRow(0);
if (headerRow == null) {
log.error(LoggerHelper.RELEASE_MARKER, "Excel文件缺少标题行");
log.error(LoggerMarker.RELEASE_MARKER, "Excel文件缺少标题行");
throw new IllegalArgumentException("Excel文件缺少标题行");
}
// 查找"档号""页数"列的索引
int archiveCodeIndex = -1;
int pageIndex = -1;
log.debug(LoggerHelper.DEBUG_MARKER, "开始查找'档号'和'页数'列的索引");
log.debug(LoggerMarker.DEBUG_MARKER, "开始查找'档号'和'页数'列的索引");
boolean foundExactMatch = false;
for (Cell cell : headerRow) {
String cellValue = getCellValueAsString(cell).trim();
if (FIELD_ARCHIVE_CODE.equals(cellValue)) {
archiveCodeIndex = cell.getColumnIndex();
log.debug(LoggerHelper.DEBUG_MARKER, "找到'档号'列,索引: {}", archiveCodeIndex);
log.debug(LoggerMarker.DEBUG_MARKER, "找到'档号'列,索引: {}", archiveCodeIndex);
} else if (FIELD_PAGE.equals(cellValue)) {
pageIndex = cell.getColumnIndex();
foundExactMatch = true;
log.debug(LoggerHelper.DEBUG_MARKER, "找到精确匹配'页数'列,索引: {}", pageIndex);
log.debug(LoggerMarker.DEBUG_MARKER, "找到精确匹配'页数'列,索引: {}", pageIndex);
}
}
// 如果没有精确匹配进行模糊查找
@ -62,13 +62,13 @@ public class ExcelFileReader implements CatalogFileReader {
String cellValue = getCellValueAsString(cell).trim();
if (cellValue.contains(FIELD_PAGE)) {
pageIndex = cell.getColumnIndex();
log.debug(LoggerHelper.DEBUG_MARKER, "找到模糊匹配'页数'列,索引: {}", pageIndex);
log.debug(LoggerMarker.DEBUG_MARKER, "找到模糊匹配'页数'列,索引: {}", pageIndex);
}
}
}
// 检查是否找到必需的列
if (archiveCodeIndex == -1 || pageIndex == -1) {
log.error(LoggerHelper.RELEASE_MARKER, "未找到必要字段,档号: {}, 页数: {}",
log.error(LoggerMarker.RELEASE_MARKER, "未找到必要字段,档号: {}, 页数: {}",
archiveCodeIndex == -1 ? "未找到" : archiveCodeIndex,
pageIndex == -1 ? "未找到" : pageIndex
);
@ -84,7 +84,7 @@ public class ExcelFileReader implements CatalogFileReader {
int validRecords = 0;
int skippedRecords = 0;
for (int i = 1; i < totalRows; i++) {
for (int i = 1; i <= totalRows; i++) {
Row row = sheet.getRow(i);
if (row == null) {
skippedRecords++;
@ -116,12 +116,12 @@ public class ExcelFileReader implements CatalogFileReader {
// 只有数据有效时才添加记录
records.add(new LogicalAddressFileGenerator.Record(archiveCode, page));
validRecords++;
log.debug(LoggerHelper.DEBUG_MARKER, "读取有效记录 - 档号: {}, 页数: {}", archiveCode, page);
log.debug(LoggerMarker.DEBUG_MARKER, "读取有效记录 - 档号: {}, 页数: {}", archiveCode, page);
}
log.info(LoggerHelper.RELEASE_MARKER, "数据读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords);
log.info(LoggerMarker.RELEASE_MARKER, "数据读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords);
} catch (Exception e) {
log.error(LoggerHelper.RELEASE_MARKER, "读取Excel文件时发生错误: {}", e.getMessage(), e);
log.error(LoggerMarker.RELEASE_MARKER, "读取Excel文件时发生错误: {}", e.getMessage(), e);
throw e;
}
return records;
@ -189,7 +189,7 @@ public class ExcelFileReader implements CatalogFileReader {
return 0;
}
} catch (NumberFormatException e) {
log.warn(LoggerHelper.DEBUG_MARKER, "无法将单元格值转换为整数: {}", cell);
log.warn(LoggerMarker.DEBUG_MARKER, "无法将单元格值转换为整数: {}", cell);
return 0;
}
}

View File

@ -7,7 +7,7 @@ import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.util.FileUtil;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
@ -43,11 +43,11 @@ public class XmlFileReader implements CatalogFileReader {
Document doc = builder.parse(file);
doc.getDocumentElement().normalize();
log.debug(LoggerHelper.DEBUG_MARKER, "开始解析XML文件: {}, 根元素: {}",
log.debug(LoggerMarker.DEBUG_MARKER, "开始解析XML文件: {}, 根元素: {}",
file.getName(), doc.getDocumentElement().getNodeName());
// 查找记录元素
NodeList recordNodes = findAllRecordNodes(doc);
log.debug(LoggerHelper.DEBUG_MARKER, "找到 {} 个潜在记录节点", recordNodes.getLength());
log.debug(LoggerMarker.DEBUG_MARKER, "找到 {} 个潜在记录节点", recordNodes.getLength());
// 解析每个记录元素
int validCount = 0;
@ -62,16 +62,16 @@ public class XmlFileReader implements CatalogFileReader {
if (record != null) {
records.add(record);
validCount++;
log.debug(LoggerHelper.DEBUG_MARKER, "解析到有效记录: {}", record);
log.debug(LoggerMarker.DEBUG_MARKER, "解析到有效记录: {}", record);
} else {
invalidCount++;
log.debug(LoggerHelper.DEBUG_MARKER, "跳过无效记录节点");
log.debug(LoggerMarker.DEBUG_MARKER, "跳过无效记录节点");
}
}
}
log.info(LoggerHelper.RELEASE_MARKER, "XML解析完成 - 有效记录: {}, 无效记录: {}", validCount, invalidCount);
log.info(LoggerMarker.RELEASE_MARKER, "XML解析完成 - 有效记录: {}, 无效记录: {}", validCount, invalidCount);
} catch (Exception e) {
log.error(LoggerHelper.RELEASE_MARKER, "解析XML文件失败: {}", filePath, e);
log.error(LoggerMarker.RELEASE_MARKER, "解析XML文件失败: {}", filePath, e);
throw new Exception("解析XML文件失败: " + filePath, e);
}
return records;
@ -93,11 +93,11 @@ public class XmlFileReader implements CatalogFileReader {
for (String tagName : RECORD_TAG_CANDIDATES) {
NodeList nodes = doc.getElementsByTagName(tagName);
if (nodes.getLength() > 0) {
log.debug(LoggerHelper.DEBUG_MARKER, "使用标签名 '{}' 找到 {} 个记录节点", tagName, nodes.getLength());
log.debug(LoggerMarker.DEBUG_MARKER, "使用标签名 '{}' 找到 {} 个记录节点", tagName, nodes.getLength());
return nodes;
}
}
log.warn(LoggerHelper.DEBUG_MARKER, "未找到任何记录节点,尝试的标签名: {}", RECORD_TAG_CANDIDATES);
log.warn(LoggerMarker.DEBUG_MARKER, "未找到任何记录节点,尝试的标签名: {}", RECORD_TAG_CANDIDATES);
return new EmptyNodeList(); // 返回空节点列表而不是null
}
@ -106,7 +106,7 @@ public class XmlFileReader implements CatalogFileReader {
String pageStr = findFirstNonEmptyTextContent(recordElement, PAGE_COUNT_TAG_CANDIDATES);
if (archiveCode == null || archiveCode.isEmpty()) {
log.debug(LoggerHelper.RELEASE_MARKER, "记录缺少档号字段");
log.debug(LoggerMarker.RELEASE_MARKER, "记录缺少档号字段");
return null;
}
@ -115,11 +115,11 @@ public class XmlFileReader implements CatalogFileReader {
if (page > 0) {
return new LogicalAddressFileGenerator.Record(archiveCode, page);
} else {
log.debug(LoggerHelper.RELEASE_MARKER, "无效的页数值: {}", pageStr);
log.debug(LoggerMarker.RELEASE_MARKER, "无效的页数值: {}", pageStr);
return null;
}
} catch (NumberFormatException e) {
log.warn(LoggerHelper.RELEASE_MARKER, "页数字段格式错误: {}", pageStr);
log.warn(LoggerMarker.RELEASE_MARKER, "页数字段格式错误: {}", pageStr);
return null;
}
}

View File

@ -1,11 +1,12 @@
package top.r3944realms.docchecktoolrefactored.io.scanner;
import java.nio.file.Path;
import java.util.Scanner;
/**
* The interface File scanner.
*/
public interface FileScanner {
public interface FileScanner extends AutoCloseable {
/**
* 扫描指定路径下的文件
*
@ -39,6 +40,11 @@ public interface FileScanner {
throw new UnsupportedOperationException("Please implement FileScanner, ProgressAwareListener.");
}
/**
* 取消扫描
*/
void cancel();
/**
* 文件扫描监听器
*/

View File

@ -1,7 +1,7 @@
package top.r3944realms.docchecktoolrefactored.io.scanner;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.FileNotFoundException;
import java.io.IOException;
@ -16,7 +16,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@Slf4j
public class RobustParallelScanner implements FileScanner, AutoCloseable {
public class RobustParallelScanner implements FileScanner {
private final ForkJoinPool forkJoinPool;
private volatile boolean cancelled = false;
private final int maxDepth;
@ -60,7 +60,7 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable {
}
}
} catch (IOException e) {
log.warn(LoggerHelper.TRACE_MARKER, "Failed to pre-scan: {}", dir, e);
log.warn(LoggerMarker.TRACE_MARKER, "Failed to pre-scan: {}", dir, e);
}
}
private void scanInternal(Path rootPath, FileScanListener listener, AtomicLong totalFiles) {
@ -83,7 +83,7 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable {
}
}).get(timeout, TimeUnit.SECONDS);
} catch (TimeoutException e) {
log.error(LoggerHelper.TRACE_MARKER, "Scan timeout: {}", rootPath, e);
log.error(LoggerMarker.TRACE_MARKER, "Scan timeout: {}", rootPath, e);
forkJoinPool.shutdownNow();
listener.onError(rootPath, new TimeoutException("扫描超时30秒"));
} catch (Exception e) {
@ -140,6 +140,8 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable {
throw new IOException("系统目录禁止访问: " + path);
}
}
@Override
public void cancel() {
cancelled = true;
forkJoinPool.shutdownNow();

View File

@ -12,7 +12,7 @@ import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.net.URL;
import java.util.ResourceBundle;
@ -39,10 +39,10 @@ public class LoginStageController implements Initializable {
String password = passwordField.getText();
if ("admin".equals(username) && "admin".equals(password)) {
log.info(LoggerHelper.DEBUG_MARKER, "{} Login successful", username);
log.info(LoggerMarker.DEBUG_MARKER, "{} Login successful", username);
SceneManager.switchMainView();
} else {
log.info(LoggerHelper.DEBUG_MARKER, "Invalid username or password");
log.info(LoggerMarker.DEBUG_MARKER, "Invalid username or password");
DialogUtil.showErrorDialog("错误", null, "用户名或密码错误!");
}
}

View File

@ -14,7 +14,7 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.Main;
import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.IOException;
import java.util.ArrayList;
@ -76,7 +76,7 @@ public class SceneManager {
});
} catch (IOException e) {
log.error(LoggerHelper.TRACE_MARKER, "Failed to load main view", e);
log.error(LoggerMarker.TRACE_MARKER, "Failed to load main view", e);
DialogUtil.showErrorDialog("错误", "加载主界面失败", e.getMessage());
}
}
@ -99,7 +99,7 @@ public class SceneManager {
openStages.add(settingStage);
} catch (IOException e) {
log.error(LoggerHelper.TRACE_MARKER, "Failed to open setting view: {}", e.getMessage(), e);
log.error(LoggerMarker.TRACE_MARKER, "Failed to open setting view: {}", e.getMessage(), e);
DialogUtil.showErrorDialog("错误", "加载设置窗口失败", "无法加载设置窗口: " + e.getMessage());
}
}
@ -149,7 +149,7 @@ public class SceneManager {
}
});
} catch (IOException e) {
log.error(LoggerHelper.TRACE_MARKER, "Failed to load view: {}", fxmlPath, e);
log.error(LoggerMarker.TRACE_MARKER, "Failed to load view: {}", fxmlPath, e);
DialogUtil.showErrorDialog("错误", "加载视图失败", "无法加载视图: " + e.getMessage());
}
}

View File

@ -20,7 +20,7 @@ public class SettingDialogController implements Initializable {
@FXML private CheckBox enableStepCB;
@FXML private Button resetB, saveB, cancelB;
@FXML private Spinner<Long> scanTotalTimeOutS, scanSingleTimeOutS;
@FXML private Spinner<Long> scanTimeOutS, taskTimeOutS;
private Setting setting;
@ -35,14 +35,14 @@ public class SettingDialogController implements Initializable {
setting = System.getSetting();
// 初始化 Spinner
scanSingleTimeOutS.setValueFactory(new LongSpinnerValueFactory(1, 3600, setting.getSingleTimeout()));
scanSingleTimeOutS.setEditable(true);
scanTotalTimeOutS.setValueFactory(new LongSpinnerValueFactory(1, 3600 * 24, setting.getTotalTimeout()));
scanTotalTimeOutS.setEditable(true);
scanTimeOutS.setValueFactory(new LongSpinnerValueFactory(1, 3600, setting.getScanTimeout()));
scanTimeOutS.setEditable(true);
taskTimeOutS.setValueFactory(new LongSpinnerValueFactory(1, 3600 * 24, setting.getTaskTimeout()));
taskTimeOutS.setEditable(true);
// 添加焦点离开时校验
addSpinnerValidation(scanSingleTimeOutS, SINGLE_MIN, SINGLE_MAX);
addSpinnerValidation(scanTotalTimeOutS, TOTAL_MIN, TOTAL_MAX);
addSpinnerValidation(scanTimeOutS, SINGLE_MIN, SINGLE_MAX);
addSpinnerValidation(taskTimeOutS, TOTAL_MIN, TOTAL_MAX);
enableStepCB.setSelected(setting.isEnableStep());
}
@ -50,8 +50,8 @@ public class SettingDialogController implements Initializable {
@FXML
void onSave(ActionEvent actionEvent) {
// 更新配置对象
setting.setSingleTimeout(scanSingleTimeOutS.getValue());
setting.setTotalTimeout(scanTotalTimeOutS.getValue());
setting.setScanTimeout(scanTimeOutS.getValue());
setting.setTaskTimeout(taskTimeOutS.getValue());
setting.setEnableStep(enableStepCB.isSelected());
// 保存到配置文件
System.saveSettingsNow();
@ -66,8 +66,8 @@ public class SettingDialogController implements Initializable {
/** 重置为默认值 */
@FXML
void onReset(ActionEvent actionEvent) {
scanSingleTimeOutS.getValueFactory().setValue(30L); // 默认单次超时
scanTotalTimeOutS.getValueFactory().setValue(300L); // 默认总超时
scanTimeOutS.getValueFactory().setValue(30L); // 默认单次超时
taskTimeOutS.getValueFactory().setValue(300L); // 默认总超时
enableStepCB.setSelected(false);
}
@ -79,12 +79,12 @@ public class SettingDialogController implements Initializable {
@FXML
void onCheckOne(MouseDragEvent mouseDragEvent) {
validateSpinnerValue(scanSingleTimeOutS, 30, 3600);
validateSpinnerValue(scanTimeOutS, 30, 3600);
}
@FXML
void onCheckTwo(MouseDragEvent mouseDragEvent) {
validateSpinnerValue(scanTotalTimeOutS, 60, 3600 * 24);
validateSpinnerValue(taskTimeOutS, 60, 3600 * 24);
}
@FXML
void onSettingThree(ActionEvent actionEvent) {

View File

@ -12,8 +12,8 @@ import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.System;
import top.r3944realms.docchecktoolrefactored.ui.SceneManager;
import top.r3944realms.docchecktoolrefactored.ui.task.DuplicateDocumentDetectionTask;
import top.r3944realms.docchecktoolrefactored.ui.utils.ProgressBarUtil;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import top.r3944realms.docchecktoolrefactored.ui.utils.ProgressBar;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.File;
@ -23,12 +23,12 @@ import java.io.File;
@Slf4j
public class DuplicateDocumentPaneController {
@FXML private TextArea result1B;
@FXML private TextArea result1TA;
@FXML private TextField loadFolder1TF;
@FXML private Button selectLoadFolder1B;
@FXML private Button start1B;
@FXML private Button cancel1B;
private final ProgressBarUtil progressBarUtil = new ProgressBarUtil();
private final ProgressBar progressBar = new ProgressBar();
private DuplicateDocumentDetectionTask currentTask; // 保存任务引用
/**
* On select folder.
@ -51,72 +51,75 @@ public class DuplicateDocumentPaneController {
* @param actionEvent the action event
*/
@FXML void onStart(ActionEvent actionEvent) {
log.info(LoggerHelper.DEBUG_MARKER, "用户点击了开始查重按钮");
String folderPath = loadFolder1TF.getText();
if (folderPath == null || folderPath.trim().isEmpty()) {
log.warn(LoggerHelper.DEBUG_MARKER, "未选择文件夹,无法进行查重");
result1B.setText("请选择要检查的文件夹。");
return;
}
// 禁用开始按钮避免重复点击
start1B.setDisable(true);
log.info(LoggerMarker.DEBUG_MARKER, "用户点击了开始查重按钮");
String folderPath = loadFolder1TF.getText();
if (folderPath == null || folderPath.trim().isEmpty()) {
log.warn(LoggerMarker.DEBUG_MARKER, "未选择文件夹,无法进行查重");
result1TA.setText("请选择要检查的文件夹。");
start1B.setDisable(false);
return;
}
cancel1B.setDisable(false);
// 显示进度条窗口
progressBarUtil.showProgress(SceneManager.getPrimaryStage(), "重复文件检测", "正在初始化扫描...");
progressBar.showProgress(SceneManager.getPrimaryStage(), "重复文件检测", "正在初始化扫描...");
// 创建并启动后台任务
// 创建后台任务
DuplicateDocumentDetectionTask task = new DuplicateDocumentDetectionTask(folderPath);
// 保存到字段
currentTask = task;
// 绑定任务属性到UI
ChangeListener<Number> progressChangeListener = (obs, oldVal, newVal) -> {
if (newVal != null) {
progressBarUtil.updateProgress(newVal.doubleValue(),
task.getMessage() != null ? task.getMessage() : "处理中...");
if (task.getMessage() != null) {
progressBar.updateProgress(newVal.doubleValue(), task.getMessage());
}
}
};
task.progressProperty().addListener(progressChangeListener);
// 绑定任务的消息到结果文本区域
ChangeListener<String> messageChangeListener = (observable, oldValue, newValue) -> {
result1B.setText(newValue);
result1TA.setText(newValue);
};
task.messageProperty().addListener(messageChangeListener);
// 绑定取消按钮 -> task.cancel()
progressBar.setOnCancel(() -> {
if (currentTask != null && currentTask.isRunning()) {
currentTask.cancel();
}
});
// 当任务完成时显示完整结果
task.setOnSucceeded(e -> {
progressBarUtil.closeProgress();
result1B.setText(task.getValue());
progressBar.closeProgress();
result1TA.setText(task.getValue());
start1B.setDisable(false);
cancel1B.setDisable(true);
log.info(LoggerHelper.RELEASE_MARKER, "查重任务完成,结果如下:{}", task.getValue());
log.info(LoggerMarker.RELEASE_MARKER, "查重任务完成,结果如下:{}", task.getValue());
});
// 处理任务失败情况
task.setOnFailed(e -> {
progressBarUtil.closeProgress();
progressBar.closeProgress();
Throwable exception = task.getException();
result1B.setText("检测过程中发生错误: " + exception.getMessage());
result1TA.setText("检测过程中发生错误: " + exception.getMessage());
start1B.setDisable(false);
cancel1B.setDisable(true);
log.error(LoggerHelper.RELEASE_MARKER, "查重任务失败", exception);
log.error(LoggerMarker.RELEASE_MARKER, "查重任务失败", exception);
});
// 处理任务取消情况
task.setOnCancelled(e -> {
progressBarUtil.closeProgress();
result1B.appendText("\n检测已取消");
progressBar.closeProgress();
result1TA.appendText("\n检测已取消");
start1B.setDisable(false);
cancel1B.setDisable(true);
currentTask.progressProperty().removeListener(progressChangeListener);
currentTask.messageProperty().removeListener(messageChangeListener);
log.info(LoggerHelper.RELEASE_MARKER, "查重任务已被取消");
});
// 绑定取消按钮 -> task.cancel()
progressBarUtil.setOnCancel(() -> {
if (currentTask != null && currentTask.isRunning()) {
currentTask.cancel();
}
log.info(LoggerMarker.RELEASE_MARKER, "查重任务已被取消");
});
// 在新线程中执行任务
Thread thread = new Thread(task);
@ -128,7 +131,7 @@ public class DuplicateDocumentPaneController {
if (currentTask != null && currentTask.isRunning()) {
currentTask.cancel(); // 触发 setOnCancelled
} else {
log.warn(LoggerHelper.DEBUG_MARKER, "没有正在运行的任务可取消");
log.warn(LoggerMarker.DEBUG_MARKER, "没有正在运行的任务可取消");
}
}
}

View File

@ -1,5 +1,7 @@
package top.r3944realms.docchecktoolrefactored.ui.module;
import javafx.beans.value.ChangeListener;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
@ -9,12 +11,15 @@ import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.stage.FileChooser;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.docchecktoolrefactored.System;
import top.r3944realms.docchecktoolrefactored.core.AddressFileComparator;
import top.r3944realms.docchecktoolrefactored.core.AddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.core.PhysicalAddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import top.r3944realms.docchecktoolrefactored.ui.SceneManager;
import top.r3944realms.docchecktoolrefactored.ui.task.AddressFileComparisonTask;
import top.r3944realms.docchecktoolrefactored.ui.task.AddressFileGenerationTask;
import top.r3944realms.docchecktoolrefactored.ui.utils.ProgressBar;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.File;
import java.net.URL;
@ -24,7 +29,6 @@ import java.util.ResourceBundle;
/**
* The type Path check pane controller.
*/
//TODO: 应该交给Platform:runLater;
@Slf4j
public class PathCheckPaneController implements Initializable {
@FXML private ChoiceBox<Mode> loadFolderType2CB;
@ -41,17 +45,16 @@ public class PathCheckPaneController implements Initializable {
private String physicalAddressFilePath = null;
// 逻辑地址文件生成器实例
private final LogicalAddressFileGenerator generator = new LogicalAddressFileGenerator();
private final PhysicalAddressFileGenerator paGenerator = new PhysicalAddressFileGenerator();
private final ProgressBar progressBar = new ProgressBar(false);
private final ProgressBar cancelableProgressBar = new ProgressBar();
private Task<?> currentTask; // 保存任务引用
/**
* On select lc.
*
* @param actionEvent the action event
*/
@FXML void onSelectLC(ActionEvent actionEvent) {
log.info(LoggerHelper.DEBUG_MARKER, "用户点击了选择目录文件按钮");
log.info(LoggerMarker.DEBUG_MARKER, "用户点击了选择目录文件按钮");
FileChooser fileChooser = System.getFileChooser();
fileChooser.setTitle("选择目录文件");
@ -71,9 +74,9 @@ public class PathCheckPaneController implements Initializable {
if (selectedFile != null) {
loadCatalog2TF.setText(selectedFile.getAbsolutePath());
System.setLastModifiedFile(selectedFile);
log.info(LoggerHelper.DEBUG_MARKER, "选择的目录文件路径为:{}", selectedFile.getAbsolutePath());
log.info(LoggerMarker.DEBUG_MARKER, "选择的目录文件路径为:{}", selectedFile.getAbsolutePath());
}else{
log.warn(LoggerHelper.DEBUG_MARKER, "用户未选择任何文件夹");
log.warn(LoggerMarker.DEBUG_MARKER, "用户未选择任何文件夹");
result2TA.setText("未选择任何文件夹,请重新选择。");
}
}
@ -92,13 +95,13 @@ public class PathCheckPaneController implements Initializable {
} else if (selectedMode == Mode.FILE_TYPE) {
directoryChooser.setTitle("选择文件级文件夹");
}
log.info(LoggerHelper.DEBUG_MARKER, "用户选择的模式为:{}", selectedMode);
log.info(LoggerMarker.DEBUG_MARKER, "用户选择的模式为:{}", selectedMode);
File selectedDirectory = directoryChooser.showDialog(selectJPGFolder2B.getScene().getWindow());
if (selectedDirectory != null) {
loadJPGFolder2TF.setText(selectedDirectory.getAbsolutePath());
log.info(LoggerHelper.DEBUG_MARKER, "选择的{}文件夹路径为:{}", selectedMode,selectedDirectory.getAbsolutePath());
log.info(LoggerMarker.DEBUG_MARKER, "选择的{}文件夹路径为:{}", selectedMode,selectedDirectory.getAbsolutePath());
}
System.setLastModifiedFile(selectedDirectory);
}
@ -109,10 +112,12 @@ public class PathCheckPaneController implements Initializable {
* @param actionEvent the action event
*/
@FXML void onGenerateLA(ActionEvent actionEvent) {
log.info(LoggerHelper.DEBUG_MARKER, "用户点击了生成逻辑地址文件按钮");
generateLogicalAddress2B.setDisable(true);
log.info(LoggerMarker.DEBUG_MARKER, "用户点击了生成逻辑地址文件按钮");
String filePath = loadCatalog2TF.getText();
if (filePath.isEmpty()) {
result2TA.setText("请先选择目录文件。");
generateLogicalAddress2B.setDisable(false);
return;
}
@ -124,10 +129,12 @@ public class PathCheckPaneController implements Initializable {
File outputFile = fileChooser.showSaveDialog(generateLogicalAddress2B.getScene().getWindow());
if (outputFile == null) {
result2TA.setText("未选择保存位置");
log.warn(LoggerHelper.DEBUG_MARKER, "用户未选择任何文件");
log.warn(LoggerMarker.DEBUG_MARKER, "用户未选择任何文件");
generateLogicalAddress2B.setDisable(true);
return;
}
System.setLastModifiedFile(outputFile);
progressBar.showProgress(SceneManager.getPrimaryStage(), "生成逻辑路径文件", "正在初始化...");
// 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加
final File finalOutputFile;
if (!outputFile.getName().toLowerCase().endsWith(".csv")) {
@ -138,33 +145,54 @@ public class PathCheckPaneController implements Initializable {
// 保存生成的文件路径
logicalAddressFilePath = finalOutputFile.getAbsolutePath();
log.info(LoggerHelper.DEBUG_MARKER, "选择的输出文件路径: {}", logicalAddressFilePath);
// 创建后台任务来处理文件生成
Thread backgroundThread = new Thread(() -> {
// 获取当前选择的文件夹类型
Mode selectedMode = loadFolderType2CB.getValue();
int folderType = (selectedMode == Mode.PAGE_TYPE) ? AddressFileGenerator.PAGE_TYPE : AddressFileGenerator.FILE_TYPE;
generator.generateAddressFile(filePath, finalOutputFile, folderType, new LogicalAddressFileGenerator.Callback() {
@Override
public void onProgress(String message) {
javafx.application.Platform.runLater(() -> result2TA.setText(message));
log.info(LoggerMarker.DEBUG_MARKER, "选择的输出文件路径: {}", logicalAddressFilePath);
Mode selectedMode = loadFolderType2CB.getValue();
// 创建后台任务
AddressFileGenerationTask task = new AddressFileGenerationTask(filePath, outputFile, selectedMode.number, true);
// 绑定任务属性到UI
ChangeListener<Number> progressChangeListener = (obs, oldVal, newVal) -> {
if (newVal != null) {
if (task.getMessage() != null) {
progressBar.updateProgress(newVal.doubleValue(), task.getMessage());
}
}
};
task.progressProperty().addListener(progressChangeListener);
@Override
public void onSuccess(String outputPath) {
javafx.application.Platform.runLater(() -> result2TA.setText(outputPath));
}
@Override
public void onError(String errorMessage) {
javafx.application.Platform.runLater(() -> result2TA.setText(errorMessage));
}
});
// 绑定任务的消息到结果文本区域
ChangeListener<String> messageChangeListener = (observable, oldValue, newValue) -> {
result2TA.setText(newValue);
};
task.messageProperty().addListener(messageChangeListener);
// 当任务完成时显示完整结果
task.setOnSucceeded(e -> {
progressBar.closeProgress();
result2TA.setText(task.getValue());
generateLogicalAddress2B.setDisable(false);
log.info(LoggerMarker.RELEASE_MARKER, "生成逻辑路径 csv 文件任务完成输出csv文件路径{}", task.getValue());
});
// 处理任务失败情况
task.setOnFailed(e -> {
progressBar.closeProgress();
Throwable exception = task.getException();
result2TA.setText("检测过程中发生错误: " + exception.getMessage());
generateLogicalAddress2B.setDisable(false);
log.error(LoggerMarker.RELEASE_MARKER, "生成逻辑路径 csv 文件任务失败", exception);
});
backgroundThread.start();
// 处理任务取消情况
task.setOnCancelled(e -> {
progressBar.closeProgress();
result2TA.appendText("\n检测已取消");
generateLogicalAddress2B.setDisable(false);
currentTask.progressProperty().removeListener(progressChangeListener);
currentTask.messageProperty().removeListener(messageChangeListener);
log.info(LoggerMarker.RELEASE_MARKER, "生成逻辑路径 csv 文件任务已被取消");
});
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
/**
@ -173,14 +201,17 @@ public class PathCheckPaneController implements Initializable {
* @param actionEvent the action event
*/
@FXML void onGeneratePA(ActionEvent actionEvent) {
generatePhysicalAddress2B.setDisable(true);
String folderPath = loadJPGFolder2TF.getText();
if (folderPath.isEmpty()) {
result2TA.setText("请先选择文件夹。");
generatePhysicalAddress2B.setDisable(false);
return;
}
File folder = new File(folderPath);
if(!folder.exists() || !folder.isDirectory()) {
result2TA.setText("所选路径不存在或不是一个有效的文件夹。");
generatePhysicalAddress2B.setDisable(false);
return;
}
@ -193,10 +224,11 @@ public class PathCheckPaneController implements Initializable {
if (outputFile == null) {
result2TA.setText("未选择保存位置");
generatePhysicalAddress2B.setDisable(false);
return;
}
System.setLastModifiedFile(outputFile);
progressBar.showProgress(SceneManager.getPrimaryStage(), "生成物理路径文件", "正在初始化...");
// 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加
final File finalOutputFile;
if (!outputFile.getName().toLowerCase().endsWith(".csv")) {
@ -208,31 +240,56 @@ public class PathCheckPaneController implements Initializable {
// 保存生成的文件路径
physicalAddressFilePath = finalOutputFile.getAbsolutePath();
// 创建后台任务来处理文件生成
Thread backgroundThread = new Thread(() -> {
// 获取当前选择的文件夹类型
Mode selectedMode = loadFolderType2CB.getValue();
int folderType = (selectedMode == Mode.PAGE_TYPE) ? AddressFileGenerator.PAGE_TYPE : AddressFileGenerator.FILE_TYPE;
paGenerator.generateAddressFile(folderPath, finalOutputFile, folderType, new AddressFileGenerator.Callback() {
@Override
public void onProgress(String message) {
javafx.application.Platform.runLater(() -> result2TA.setText(message));
//
Mode selectedMode = loadFolderType2CB.getValue();
// 创建后台任务
AddressFileGenerationTask task = new AddressFileGenerationTask(folderPath, outputFile, selectedMode.number, false);
// 保存到字段
currentTask = task;
// 绑定任务属性到UI
ChangeListener<Number> progressChangeListener = (obs, oldVal, newVal) -> {
if (newVal != null) {
if (task.getMessage() != null) {
progressBar.updateProgress(newVal.doubleValue(), task.getMessage());
}
}
};
task.progressProperty().addListener(progressChangeListener);
@Override
public void onSuccess(String outputPath) {
javafx.application.Platform.runLater(() -> result2TA.setText(outputPath));
}
@Override
public void onError(String errorMessage) {
javafx.application.Platform.runLater(() -> result2TA.setText(errorMessage));
}
});
// 绑定任务的消息到结果文本区域
ChangeListener<String> messageChangeListener = (observable, oldValue, newValue) -> {
result2TA.setText(newValue);
};
task.messageProperty().addListener(messageChangeListener);
// 当任务完成时显示完整结果
task.setOnSucceeded(e -> {
progressBar.closeProgress();
result2TA.setText(task.getValue());
generatePhysicalAddress2B.setDisable(false);
log.info(LoggerMarker.RELEASE_MARKER, "生成物理路径 csv 文件任务完成输出csv文件路径{}", task.getValue());
});
backgroundThread.start();
// 处理任务失败情况
task.setOnFailed(e -> {
progressBar.closeProgress();
Throwable exception = task.getException();
result2TA.setText("检测过程中发生错误: " + exception.getMessage());
generatePhysicalAddress2B.setDisable(false);
log.error(LoggerMarker.RELEASE_MARKER, "生成物理路径 csv 文件任务失败", exception);
});
// 处理任务取消情况
task.setOnCancelled(e -> {
progressBar.closeProgress();
result2TA.appendText("\n检测已取消");
generatePhysicalAddress2B.setDisable(false);
currentTask.progressProperty().removeListener(progressChangeListener);
currentTask.messageProperty().removeListener(messageChangeListener);
log.info(LoggerMarker.RELEASE_MARKER, "生成物理路径 csv 文件任务已被取消");
});
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
@ -245,76 +302,88 @@ public class PathCheckPaneController implements Initializable {
*/
@FXML
void onStart(ActionEvent actionEvent) {
log.info(LoggerHelper.DEBUG_MARKER, "用户点击了开始对比按钮");
log.info(LoggerMarker.DEBUG_MARKER, "用户点击了开始对比按钮");
// 检查是否已生成两个文件
if (logicalAddressFilePath == null || physicalAddressFilePath == null) {
result2TA.setText("请先生成逻辑地址文件和物理地址文件。");
return;
}
start2B.setDisable(true);
log.info(LoggerHelper.DEBUG_MARKER, "逻辑地址文件路径为:{}", logicalAddressFilePath);
log.info(LoggerHelper.DEBUG_MARKER, "物理地址文件路径为:{}", physicalAddressFilePath);
// 使用新创建的核心类进行文件比较
AddressFileComparator comparator = new AddressFileComparator();
AddressFileComparator.ComparisonResult result = comparator.compareFiles(physicalAddressFilePath, logicalAddressFilePath);
// 显示比对结果
StringBuilder resultText = new StringBuilder();
// 显示读取的行数
resultText.append("读取物理地址文件记录数: ").append(result.getPhysicalRecordsCount()).append("\n");
resultText.append("读取逻辑地址文件记录数: ").append(result.getLogicalRecordsCount()).append("\n\n");
// 显示路径不一致的结果
if (!result.getPathMismatchResults().isEmpty()) {
resultText.append("文件名相同但路径不一致的记录数量: ").append(result.getPathMismatchResults().size()).append("\n");
for (String mismatch : result.getPathMismatchResults()) {
resultText.append("\t").append(mismatch).append("\n");
// 显示进度条窗口
cancelableProgressBar.showProgress(SceneManager.getPrimaryStage(), "文件查漏检查", "正在初始化...");
// 创建后台任务
AddressFileComparisonTask task = getAddressFileComparisonTask();
// 保存到字段
this.currentTask = task;
// 绑定任务属性到UI
ChangeListener<Number> progressChangeListener = (obs, oldVal, newVal) -> {
if (newVal != null) {
if (task.getMessage() != null) {
cancelableProgressBar.updateProgress(newVal.doubleValue(), task.getMessage());
}
}
resultText.append("\n");
} else {
resultText.append("没有路径错误\n\n");
}
};
task.progressProperty().addListener(progressChangeListener);
// 显示物理文件在逻辑文件中未找到的结果
if (!result.getForwardComparisonResults().isEmpty()) {
resultText.append("物理文件在逻辑文件中未找到的记录数量: ").append(result.getForwardComparisonResults().size()).append("\n");
for (String forward : result.getForwardComparisonResults()) {
resultText.append("\t").append(forward).append("\n");
// 绑定任务的消息到结果文本区域
ChangeListener<String> messageChangeListener = (observable, oldValue, newValue) -> {
result2TA.setText(newValue);
};
task.messageProperty().addListener(messageChangeListener);
// 绑定取消按钮 -> task.cancel()
cancelableProgressBar.setOnCancel(() -> {
if (currentTask != null && currentTask.isRunning()) {
currentTask.cancel();
}
resultText.append("\n");
} else {
resultText.append("没有物理存在而逻辑不存在的文件\n\n");
}
});
// 显示逻辑文件在物理文件中未找到的结果
if (!result.getBackwardComparisonResults().isEmpty()) {
resultText.append("逻辑文件在物理文件中未找到的记录数量: ").append(result.getBackwardComparisonResults().size()).append("\n");
for (String backward : result.getBackwardComparisonResults()) {
resultText.append("\t").append(backward).append("\n");
}
} else {
resultText.append("没有逻辑存在而物理不存在的文件\n");
}
task.setOnSucceeded(event -> {
cancelableProgressBar.closeProgress();
start2B.setDisable(false);
result2TA.setText(AddressFileComparator.ComparisonResult.generateComparisonResults(task.getValue(), loadFolderType2CB.getValue().compareMode));
result2TA.setText(AddressFileComparator.ComparisonResult.generateComparisonResults(task.getValue(), loadFolderType2CB.getValue().compareMode));
//内部比较器已有此处忽略日志打印
});
// 如果所有结果都为空则显示一致信息
if (result.getPathMismatchResults().isEmpty() &&
result.getForwardComparisonResults().isEmpty() &&
result.getBackwardComparisonResults().isEmpty()) {
resultText = new StringBuilder("所有文件比对一致,无差异。\n");
resultText.append("读取物理地址文件记录数: ").append(result.getPhysicalRecordsCount()).append("\n");
resultText.append("读取逻辑地址文件记录数: ").append(result.getLogicalRecordsCount()).append("\n");
}
task.setOnFailed(event -> {
cancelableProgressBar.closeProgress();
Throwable exception = task.getException();
result2TA.setText("文件比对失败: " + task.getException().getMessage());
start2B.setDisable(false);
log.error(LoggerMarker.RELEASE_MARKER, "查漏任务失败", exception);
});
result2TA.setText(resultText.toString());
task.setOnCancelled(event -> {
cancelableProgressBar.closeProgress();
result2TA.appendText("\n检测已取消");
start2B.setDisable(false);
currentTask.progressProperty().removeListener(progressChangeListener);
currentTask.messageProperty().removeListener(messageChangeListener);
log.info(LoggerMarker.RELEASE_MARKER, "查漏任务取消");
});
// 异步执行任务
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
private @NotNull AddressFileComparisonTask getAddressFileComparisonTask() {
Mode selectedMode = loadFolderType2CB.getValue();
AddressFileComparator.CompareMode compareMode = (selectedMode == Mode.PAGE_TYPE) ?
AddressFileComparator.CompareMode.PAGE_LEVEL :
AddressFileComparator.CompareMode.FILE_LEVEL;
// 创建异步任务
return new AddressFileComparisonTask(
physicalAddressFilePath,
logicalAddressFilePath,
compareMode,
System.getSetting().getTaskTimeout()
);
}
@Override
@ -330,7 +399,7 @@ public class PathCheckPaneController implements Initializable {
/**
* Jpg mode. 文件以JPG
*/
PAGE_TYPE("jpg") {
PAGE_TYPE("jpg", AddressFileGenerator.PAGE_TYPE, AddressFileComparator.CompareMode.PAGE_LEVEL) {
@Override
public String toString() {
return "页面级";
@ -339,7 +408,7 @@ public class PathCheckPaneController implements Initializable {
/**
* Pdf mode. 文件以PDF
*/
FILE_TYPE("pdf") {
FILE_TYPE("pdf", AddressFileGenerator.FILE_TYPE, AddressFileComparator.CompareMode.FILE_LEVEL) {
@Override
public String toString() {
return "文件级";
@ -348,10 +417,14 @@ public class PathCheckPaneController implements Initializable {
/**
* The Id.
*/
final String id;
public final String id;
public final int number;
public final AddressFileComparator.CompareMode compareMode;
Mode(String id) {
Mode(String id, int number, AddressFileComparator.CompareMode compareMode) {
this.id = id;
this.number = number;
this.compareMode = compareMode;
}
}
}

View File

@ -13,7 +13,7 @@ import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.System;
import top.r3944realms.docchecktoolrefactored.core.HashFileGenerator;
import top.r3944realms.docchecktoolrefactored.core.MD5HashCalculator;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import top.r3944realms.docchecktoolrefactored.util.LoggerMarker;
import java.io.File;
import java.io.IOException;
@ -53,7 +53,7 @@ public class StorageCarrierPaneController {
@FXML
void onSelectLD(ActionEvent event) {
log.info(LoggerHelper.DEBUG_MARKER, "用户点击选择文件夹按钮");
log.info(LoggerMarker.DEBUG_MARKER, "用户点击选择文件夹按钮");
DirectoryChooser directoryChooser = System.getDirectoryChooser();
directoryChooser.setTitle("选择要检查的文件夹(页面级文件夹和文件级文件夹等不包括目录文件夹)");
@ -81,18 +81,18 @@ public class StorageCarrierPaneController {
loadDigitalOutcomes.setText(currentText + File.pathSeparator + folderPath);
}
}
log.info(LoggerHelper.DEBUG_MARKER, "用户选择了文件夹: {}", selectedFolder.getAbsolutePath());
log.info(LoggerMarker.DEBUG_MARKER, "用户选择了文件夹: {}", selectedFolder.getAbsolutePath());
} else {
log.info(LoggerHelper.DEBUG_MARKER, "用户取消了文件夹选择");
log.info(LoggerMarker.DEBUG_MARKER, "用户取消了文件夹选择");
}
}
@FXML
void onClearSelectedFolders(ActionEvent event) {
log.info(LoggerHelper.DEBUG_MARKER, "用户点击清除已选择文件夹按钮");
log.info(LoggerMarker.DEBUG_MARKER, "用户点击清除已选择文件夹按钮");
loadDigitalOutcomes.setText("");
result7TA.setText("已清除所有已选择的文件夹");
log.info(LoggerHelper.DEBUG_MARKER, "已清除所有已选择的文件夹");
log.info(LoggerMarker.DEBUG_MARKER, "已清除所有已选择的文件夹");
}
@ -100,7 +100,7 @@ public class StorageCarrierPaneController {
@FXML
void onSelectLC(ActionEvent event) {
log.info(LoggerHelper.DEBUG_MARKER, "用户点击选择RAR文件按钮");
log.info(LoggerMarker.DEBUG_MARKER, "用户点击选择RAR文件按钮");
FileChooser fileChooser = System.getFileChooser();
fileChooser.setTitle("选择一个 .rar 文件");
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("RAR Files", "*.rar"));
@ -108,48 +108,48 @@ public class StorageCarrierPaneController {
if (selectedFile != null) {
System.setLastModifiedFile(selectedFile);
loadCompressedFile.setText(selectedFile.getAbsolutePath());
log.info(LoggerHelper.DEBUG_MARKER, "用户选择了RAR文件: {}", selectedFile.getAbsolutePath());
log.info(LoggerMarker.DEBUG_MARKER, "用户选择了RAR文件: {}", selectedFile.getAbsolutePath());
} else {
log.info(LoggerHelper.DEBUG_MARKER, "用户取消了RAR文件选择");
log.info(LoggerMarker.DEBUG_MARKER, "用户取消了RAR文件选择");
}
}
@FXML
void onCaculateHash(ActionEvent event) {
log.info(LoggerHelper.DEBUG_MARKER, "开始计算RAR文件的MD5哈希值");
log.info(LoggerMarker.DEBUG_MARKER, "开始计算RAR文件的MD5哈希值");
String filePath = loadCompressedFile.getText();
if (filePath == null || filePath.isEmpty()) {
log.warn(LoggerHelper.DEBUG_MARKER, "未选择RAR文件无法计算哈希值");
log.warn(LoggerMarker.DEBUG_MARKER, "未选择RAR文件无法计算哈希值");
result7TA.setText("请先选择一个 .rar 文件");
return;
}
File file = new File(filePath);
if (!file.exists() || !file.isFile() || !filePath.endsWith(".rar")) {
log.warn(LoggerHelper.DEBUG_MARKER, "选择的文件无效或不是RAR文件: {}", filePath);
log.warn(LoggerMarker.DEBUG_MARKER, "选择的文件无效或不是RAR文件: {}", filePath);
result7TA.setText("所选文件不存在或不是一个有效的 .rar 文件");
return;
}
try {
log.info(LoggerHelper.DEBUG_MARKER, "开始计算文件哈希值: {}", filePath);
log.info(LoggerMarker.DEBUG_MARKER, "开始计算文件哈希值: {}", filePath);
MD5HashCalculator hashCalculator = new MD5HashCalculator();
String hashResult = hashCalculator.calculateHash(file.toPath());
result7TA.setText("计算结果:\n" + hashResult);
log.info(LoggerHelper.DEBUG_MARKER, "文件哈希值计算完成: {}", hashResult);
log.info(LoggerMarker.DEBUG_MARKER, "文件哈希值计算完成: {}", hashResult);
} catch (IOException e) {
log.error(LoggerHelper.DEBUG_MARKER, "计算文件哈希值时出错: {}", filePath, e);
log.error(LoggerMarker.DEBUG_MARKER, "计算文件哈希值时出错: {}", filePath, e);
result7TA.setText("计算哈希值时出错: " + e.getMessage());
}
}
@FXML
void onGenerateHF(ActionEvent event) {
log.info(LoggerHelper.DEBUG_MARKER, "开始生成哈希列表文件");
log.info(LoggerMarker.DEBUG_MARKER, "开始生成哈希列表文件");
String folderPathsText = loadDigitalOutcomes.getText();
if (folderPathsText == null || folderPathsText.isEmpty()) {
log.warn(LoggerHelper.DEBUG_MARKER, "未选择文件夹,无法生成哈希列表文件");
log.warn(LoggerMarker.DEBUG_MARKER, "未选择文件夹,无法生成哈希列表文件");
result7TA.setText("请先选择一个文件夹");
return;
}
@ -163,7 +163,7 @@ public class StorageCarrierPaneController {
if (folder.exists() && folder.isDirectory()) {
folders.add(folder);
} else {
log.warn(LoggerHelper.DEBUG_MARKER, "选择的路径无效或不是文件夹: {}", path);
log.warn(LoggerMarker.DEBUG_MARKER, "选择的路径无效或不是文件夹: {}", path);
result7TA.setText("所选路径不存在或不是一个有效的文件夹: " + path);
return;
}
@ -180,7 +180,7 @@ public class StorageCarrierPaneController {
File outputFile = fileChooser.showSaveDialog(selectLoadDigitalOutcomes7B.getScene().getWindow());
if (outputFile == null) {
log.info(LoggerHelper.DEBUG_MARKER, "用户取消了文件保存操作");
log.info(LoggerMarker.DEBUG_MARKER, "用户取消了文件保存操作");
result7TA.setText("未选择保存位置");
return;
}
@ -193,13 +193,13 @@ public class StorageCarrierPaneController {
finalOutputFile = outputFile;
}
log.info(LoggerHelper.DEBUG_MARKER, "选择的输出文件路径: {}", finalOutputFile.getAbsolutePath());
log.info(LoggerMarker.DEBUG_MARKER, "选择的输出文件路径: {}", finalOutputFile.getAbsolutePath());
// 创建后台任务
Task<String> task = new Task<>() {
@Override
protected String call() throws Exception {
log.info(LoggerHelper.DEBUG_MARKER, "开始执行哈希文件生成任务");
log.info(LoggerMarker.DEBUG_MARKER, "开始执行哈希文件生成任务");
updateMessage("开始生成哈希文件...");
HashFileGenerator generator = new HashFileGenerator();
@ -213,11 +213,11 @@ public class StorageCarrierPaneController {
updateProgress(current, total);
updateMessage("处理文件: " + current + "/" + total);
if (current % 500 == 0 || current == total) { // 每500个文件或完成时记录一次日志
log.info(LoggerHelper.DEBUG_MARKER, "处理进度: {}/{}", current, total);
log.info(LoggerMarker.DEBUG_MARKER, "处理进度: {}/{}", current, total);
}
});
log.info(LoggerHelper.DEBUG_MARKER, "哈希文件生成任务完成,输出文件: {}", finalOutputFile.getAbsolutePath());
log.info(LoggerMarker.DEBUG_MARKER, "哈希文件生成任务完成,输出文件: {}", finalOutputFile.getAbsolutePath());
return "哈希列表文件已生成: " + finalOutputFile.getAbsolutePath();
}
};
@ -229,7 +229,7 @@ public class StorageCarrierPaneController {
// 任务成功完成
task.setOnSucceeded(e -> {
log.info(LoggerHelper.DEBUG_MARKER, "哈希文件生成任务成功完成");
log.info(LoggerMarker.DEBUG_MARKER, "哈希文件生成任务成功完成");
result7TA.setText(task.getValue());
});
@ -237,13 +237,13 @@ public class StorageCarrierPaneController {
task.setOnFailed(e -> {
Throwable exception = task.getException();
String errorMsg = "生成哈希文件时出错: " + (exception != null ? exception.getMessage() : "未知错误");
log.error(LoggerHelper.RELEASE_MARKER, "哈希文件生成任务失败", exception);
log.error(LoggerMarker.RELEASE_MARKER, "哈希文件生成任务失败", exception);
result7TA.setText(errorMsg);
});
// 任务取消处理
task.setOnCancelled(e -> {
log.info(LoggerHelper.DEBUG_MARKER, "哈希文件生成任务被用户取消");
log.info(LoggerMarker.DEBUG_MARKER, "哈希文件生成任务被用户取消");
result7TA.setText("哈希文件生成操作已取消");
});

View File

@ -0,0 +1,91 @@
package top.r3944realms.docchecktoolrefactored.ui.task;
import javafx.concurrent.Task;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.core.AddressFileComparator;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@Slf4j
public class AddressFileComparisonTask extends Task<AddressFileComparator.ComparisonResult> {
private final String physicalFilePath;
private final String logicalFilePath;
private final AddressFileComparator.CompareMode compareMode;
private final AddressFileComparator comparator;
private final long timeoutSeconds; // 超时秒数
public AddressFileComparisonTask(String physicalFilePath,
String logicalFilePath,
AddressFileComparator.CompareMode compareMode,
long timeoutSeconds) {
this.physicalFilePath = physicalFilePath;
this.logicalFilePath = logicalFilePath;
this.compareMode = compareMode;
this.timeoutSeconds = timeoutSeconds;
comparator = new AddressFileComparator();
}
@Override
protected AddressFileComparator.ComparisonResult call() throws Exception {
updateMessage("初始化文件比较...");
comparator.setProgressCallback(new AddressFileComparator.ProgressCallback() {
@Override
public void onPhaseStarted(AddressFileComparator.Phase phase) {
// 阶段开始提示
switch (phase) {
case READ_PHYSICAL_CSV -> updateMessage("正在读取物理地址 CSV 文件...");
case READ_LOGICAL_CSV -> updateMessage("正在读取逻辑地址 CSV 文件...");
case COMPARE_FORWARD -> updateMessage("正在执行正向比较(物理 → 逻辑)...");
case COMPARE_BACKWARD -> updateMessage("正在执行反向比较(逻辑 → 物理)...");
}
}
@Override
public void onPhaseProgress(AddressFileComparator.Phase phase, int current, int total) {
if (total <= 0) return;
updateProgress(current, total);
// 阶段进度提示
switch (phase) {
case READ_PHYSICAL_CSV -> updateMessage(String.format("读取物理地址 CSV: %d/%d", current, total));
case READ_LOGICAL_CSV -> updateMessage(String.format("读取逻辑地址 CSV: %d/%d", current, total));
case COMPARE_FORWARD -> updateMessage(String.format("正向比较: %d/%d", current, total));
case COMPARE_BACKWARD -> updateMessage(String.format("反向比较: %d/%d", current, total));
}
}
@Override
public void onPhaseCompleted(AddressFileComparator.Phase phase) {
// 阶段完成提示
switch (phase) {
case READ_PHYSICAL_CSV -> updateMessage("物理地址 CSV 文件读取完成");
case READ_LOGICAL_CSV -> updateMessage("逻辑地址 CSV 文件读取完成");
case COMPARE_FORWARD -> updateMessage("正向比较完成");
case COMPARE_BACKWARD -> updateMessage("反向比较完成");
}
}
});
try {
// 构建显示文本
return comparator.compareFiles(physicalFilePath, logicalFilePath, compareMode)
.get(timeoutSeconds, TimeUnit.SECONDS);
} catch (TimeoutException e) {
updateMessage("文件比对超时,请检查文件大小或电脑性能。");
log.error("文件比对超时", e);
throw e;
} finally {
comparator.shutdown();
}
}
@Override
protected void cancelled() {
comparator.shutdown();
}
}

View File

@ -0,0 +1,90 @@
package top.r3944realms.docchecktoolrefactored.ui.task;
import javafx.concurrent.Task;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.System;
import top.r3944realms.docchecktoolrefactored.core.AddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.core.PhysicalAddressFileGenerator;
import java.io.File;
import java.util.concurrent.*;
import static java.util.concurrent.Executors.*;
@Slf4j
public class AddressFileGenerationTask extends Task<String> {
private final String sourcePath;
private final File outputFile;
private final int folderType;
private final AddressFileGenerator generator;
public AddressFileGenerationTask(String sourcePath,
File outputFile,
int folderType,
boolean isLogic) {
this.sourcePath = sourcePath;
this.outputFile = outputFile;
this.folderType = folderType;
this.generator = isLogic ? new LogicalAddressFileGenerator() : new PhysicalAddressFileGenerator();
}
@Override
protected String call() {
updateMessage("初始化生成任务...");
generator.setProgressCallback(new AddressFileGenerator.ProgressCallback() {
@Override
public void onPhaseStarted(AddressFileGenerator.Phase phase) {
switch (phase) {
case GENERATE_LOGICAL -> updateMessage("正在生成逻辑地址 CSV 文件 ...");
case GENERATE_PHYSICAL -> updateMessage("正在生成物理地址 CSV 文件 ...");
}
}
@Override
public void onPhaseProgress(AddressFileGenerator.Phase phase, int current, int total) {
if (total > 0) {
updateProgress(current, total);
switch (phase) {
case GENERATE_LOGICAL -> updateMessage(String.format("在生成逻辑地址 CSV : %d/%d", current, total));
case GENERATE_PHYSICAL -> updateMessage(String.format("在生成物理地址 CSV : %d/%d", current, total));
}
}
}
@Override
public void onPhaseCompleted(AddressFileGenerator.Phase phase) {
switch (phase) {
case GENERATE_LOGICAL -> updateMessage("已完成生成逻辑地址 CSV 文件任务");
case GENERATE_PHYSICAL -> updateMessage("已完成生成物理地址 CSV 文件任务");
}
}
}
);
ExecutorService executor = newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
generator.generateAddressFile(sourcePath, outputFile, folderType);
});
try {
// 等待执行完成或超时
future.get(System.getSetting().getTaskTimeout(), TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true); // 尝试中断
this.cancel(); // 取消 Task
throw new RuntimeException("生成任务超时 (>" + System.getSetting().getTaskTimeout() + "s)", e);
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException("生成任务失败", e.getCause());
} finally {
executor.shutdownNow();
}
return outputFile.getAbsolutePath();
}
@Override
protected void cancelled() {
log.info("生成任务已取消: {}", outputFile.getAbsolutePath());
}
}

View File

@ -2,6 +2,7 @@ package top.r3944realms.docchecktoolrefactored.ui.task;
import javafx.concurrent.Task;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.docchecktoolrefactored.System;
import top.r3944realms.docchecktoolrefactored.core.DuplicateFinder;
import top.r3944realms.docchecktoolrefactored.core.MD5HashCalculator;
@ -111,9 +112,9 @@ public class DuplicateDocumentDetectionTask extends Task<String>{
findThread.start();
// 等待扫描完成设置超时时间例如5分钟
long totalTimeout = System.getSetting().getTotalTimeout();
long totalTimeout = System.getSetting().getTaskTimeout();
if (!latch.await(totalTimeout, TimeUnit.SECONDS)) {
scanner.cancel();
duplicateFinder.shutdown();
throw new TimeoutException(String.format("扫描超时(%d秒", totalTimeout));
}
@ -127,18 +128,18 @@ public class DuplicateDocumentDetectionTask extends Task<String>{
// 检查是否被取消
if (isCancelled()) {
scanner.cancel();
duplicateFinder.shutdown();
return "操作已被取消";
}
// 检查是否超时
if (java.lang.System.currentTimeMillis() - start > totalTimeout * 1000L) {
scanner.cancel();
duplicateFinder.shutdown();
throw new TimeoutException(String.format("扫描超时(%d秒", totalTimeout));
}
}
} catch (InterruptedException e) {
scanner.cancel();
duplicateFinder.shutdown();
Thread.currentThread().interrupt();
return "操作被中断";
}
@ -149,8 +150,11 @@ public class DuplicateDocumentDetectionTask extends Task<String>{
}
List<DuplicateGroup> duplicateGroups = resultRef.get();
// 构建最终结果
return generateResult(duplicateGroups, totalFiles);
}
private static @NotNull String generateResult(List<DuplicateGroup> duplicateGroups, AtomicInteger totalFiles) {
StringBuilder result = new StringBuilder();

View File

@ -21,6 +21,15 @@ public class DialogUtil {
public static boolean showExitConfirmation(Window owner) {
return showConfirmationDialog("确认退出", "您确定要退出程序吗?", "请确认您的操作");
}
/**
* Show exit confirmation boolean.
*
* @param owner the owner
* @return the boolean
*/
public static boolean showCancelConfirmation(Window owner) {
return showConfirmationDialog("确认取消", "您确定要取消该任务吗?", "请确认您的操作");
}
/**
* Show confirmation dialog boolean.

View File

@ -4,7 +4,6 @@ import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
@ -15,14 +14,23 @@ import java.util.concurrent.atomic.AtomicBoolean;
/**
* 进度条窗口工具类支持取消按钮
*/
public class ProgressBarUtil {
public class ProgressBar {
private Stage progressStage;
private ProgressBar progressBar;
private javafx.scene.control.ProgressBar progressBar;
private Label messageLabel;
private Button cancelButton;
private final boolean isCancelable;
private final AtomicBoolean cancelled = new AtomicBoolean(false);
private Runnable onCancelCallback;
public ProgressBar(boolean isCancelable) {
this.isCancelable = isCancelable;
}
public ProgressBar() {
this(true);
}
/**
* 显示进度条窗口
* @param ownerStage 父窗口
@ -34,41 +42,57 @@ public class ProgressBarUtil {
progressStage = new Stage();
progressStage.initOwner(ownerStage);
progressStage.initStyle(StageStyle.UTILITY);
if (isCancelable) {
progressStage.setOnCloseRequest(windowEvent -> {
if (!DialogUtil.showCancelConfirmation(progressStage.getOwner())) {
windowEvent.consume();
}
else {
cancelled.set(true);
if (onCancelCallback != null) {
onCancelCallback.run();
}
closeProgress();
}
});
}
progressStage.initModality(Modality.APPLICATION_MODAL);
progressStage.setTitle(title);
progressStage.setResizable(false);
// 创建进度条
progressBar = new ProgressBar();
progressBar = new javafx.scene.control.ProgressBar();
progressBar.setPrefWidth(300);
progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
progressBar.setProgress(javafx.scene.control.ProgressBar.INDETERMINATE_PROGRESS);
// 创建消息标签
messageLabel = new Label(initialMessage);
// 创建取消按钮
cancelButton = new Button("取消");
cancelButton.setOnAction(e -> {
cancelled.set(true);
if (onCancelCallback != null) {
onCancelCallback.run();
}
closeProgress();
});
// 如果 setOnCancel 先调用过绑定回调
if (onCancelCallback != null) {
if (isCancelable) {
// 创建取消按钮
cancelButton = new Button("取消");
cancelButton.setOnAction(e -> {
cancelled.set(true);
onCancelCallback.run();
if (onCancelCallback != null) {
onCancelCallback.run();
}
closeProgress();
});
// 如果 setOnCancel 先调用过绑定回调
if (onCancelCallback != null) {
cancelButton.setOnAction(e -> {
cancelled.set(true);
onCancelCallback.run();
closeProgress();
});
}
}
// 布局
VBox root = new VBox(10, messageLabel, progressBar, cancelButton);
root.setStyle("-fx-padding: 20; -fx-alignment: center;");
VBox root = new VBox(10, messageLabel, progressBar);
if (isCancelable) root.getChildren().add(cancelButton);
root.setStyle("-fx-padding: 20; -fx-alignment: center;");
Scene scene = new Scene(root);
progressStage.setScene(scene);
progressStage.sizeToScene();
@ -118,33 +142,35 @@ public class ProgressBarUtil {
* @param task 要执行的任务可检查 isCancelled() 中途退出
*/
public static void showAndExecute(Stage ownerStage, String title, String initialMessage, CancellableTask task) {
ProgressBarUtil progressBarUtil = new ProgressBarUtil();
progressBarUtil.showProgress(ownerStage, title, initialMessage);
ProgressBar progressBar = new ProgressBar();
progressBar.showProgress(ownerStage, title, initialMessage);
new Thread(() -> {
try {
task.run(progressBarUtil);
task.run(progressBar);
} finally {
progressBarUtil.closeProgress();
progressBar.closeProgress();
}
}).start();
}
@FunctionalInterface
public interface CancellableTask {
void run(ProgressBarUtil util);
void run(ProgressBar util);
}
public void setOnCancel(Runnable onCancel) {
if (cancelButton != null) {
cancelButton.setOnAction(e -> {
cancelled.set(true);
if (onCancel != null) {
onCancel.run();
}
closeProgress();
});
} else {
// 保存回调稍后在按钮创建后绑定
this.onCancelCallback = onCancel;
}
if (isCancelable) {
if (cancelButton != null) {
cancelButton.setOnAction(e -> {
cancelled.set(true);
if (onCancel != null) {
onCancel.run();
}
closeProgress();
});
} else {
// 保存回调稍后在按钮创建后绑定
this.onCancelCallback = onCancel;
}
} else throw new UnsupportedOperationException("Cancellable task is not supported");
}
}

View File

@ -30,7 +30,7 @@ public class FileUtil {
Path path = Paths.get(filePath).normalize();
File file = path.toFile();
if (!file.exists()) {
log.error(LoggerHelper.TRACE_MARKER, "文件不存在: {}", filePath);
log.error(LoggerMarker.TRACE_MARKER, "文件不存在: {}", filePath);
throw new NoSuchFileException("文件不存在: " + filePath);
}
@ -68,7 +68,7 @@ public class FileUtil {
File file = path.toFile();
if (!file.exists()) {
log.error(LoggerHelper.TRACE_MARKER, "文件不存在: {}", filePath);
log.error(LoggerMarker.TRACE_MARKER, "文件不存在: {}", filePath);
throw new NoSuchFileException("文件不存在: " + filePath);
}
@ -88,7 +88,7 @@ public class FileUtil {
.toList();
if (!supportedExtensions.contains(fileExtension)) {
log.error(LoggerHelper.TRACE_MARKER, "不支持的文件格式: {}", fileExtension);
log.error(LoggerMarker.TRACE_MARKER, "不支持的文件格式: {}", fileExtension);
throw new IllegalArgumentException("不支持的文件格式,预期: "
+ supportedExtensions + ",实际: " + fileExtension);
}

View File

@ -3,7 +3,7 @@ package top.r3944realms.docchecktoolrefactored.util;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
public class LoggerHelper {
public class LoggerMarker {
public static final Marker TRACE_MARKER = MarkerFactory.getMarker("TRACE");
public static final Marker DEBUG_MARKER = MarkerFactory.getMarker("DEBUG");
public static final Marker RELEASE_MARKER = MarkerFactory.getMarker("RELEASE");

View File

@ -25,7 +25,7 @@
<RowConstraints maxHeight="592.6666666666666" prefHeight="581.3333536783855" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<TextArea fx:id="result1B" editable="false" prefHeight="414.0" prefWidth="683.0" GridPane.columnSpan="3" GridPane.rowIndex="3">
<TextArea fx:id="result1TA" editable="false" prefHeight="414.0" prefWidth="683.0" GridPane.columnSpan="3" GridPane.rowIndex="3">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
@ -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;" 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。&#10;;" 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

@ -118,7 +118,7 @@
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</Button>
<TextArea editable="false" prefHeight="450.0" prefWidth="400.0" text="①获取数字化成果文件的物理存储路径(实际存储位置)&#10;②处理案卷级/文件级目录生成逻辑存储路径(理论存储位置)&#10;③自动对比物理路径与逻辑路径,识别以下问题:&#10; 1)文件漏扫/存储路径错误/命名不规范的文件&#10; 2)目录数据库档号错误或页数著录。&#10;④统计结果填入《查遗漏、查存储路径和命名规范登记表》附件2。&#10;&#10;" wrapText="true" GridPane.columnIndex="4" GridPane.rowIndex="4">
<TextArea editable="false" prefHeight="450.0" prefWidth="400.0" text="1.选择“页面级”,载入目录,生成页面级逻辑地址;载入目录对应的页面级数据,生成页面级物理地址,用软件比对出差异,记录软件反馈结果。&#10;2.选择“文件级”,载入目录,生成文件级逻辑地址;载入目录对应的文件级数据,生成文件级物理地址,用软件比对出差异,记录软件反馈结果。&#10;3.逐一核实差异存在的原因,以判断目录和扫描数据存在的问题。&#10;4.根据核实的情况填写《查遗漏、查存储路径和命名规范登记表》附件2。&#10;&#10;" wrapText="true" GridPane.columnIndex="4" GridPane.rowIndex="4">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>

View File

@ -7,7 +7,7 @@
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17">
<children>
<TextArea editable="false" prefHeight="800.0" prefWidth="1000.0" scrollLeft="1.0" text="工作内容:&#10; ①汇总前两步检查结果计算合格率要求100%&#10; ②若合格率达标,按总页数5%比例抽检:&#10; 著录准确性/规范性/完整性要求100%合格率)&#10; 图像清晰度/倾斜度/黑边要求95%以上的合格率)&#10; ③结果填入《质量检查登记表》附件3。" wrapText="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<TextArea editable="false" prefHeight="800.0" prefWidth="1000.0" scrollLeft="1.0" text="工作内容:&#10; 1.汇总前两步检查结果计算合格率合格率不是100%则验收不通过,要求整改。&#10; 2.若合格率达到100%,则按总页数5%比例抽检:&#10; 著录准确性/规范性/完整性要求100%合格率)&#10; 图像清晰度/倾斜度/黑边要求95%以上的合格率)&#10; ③结果填入《质量检查登记表》附件3。" wrapText="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<font>
<Font size="18.0" />
</font></TextArea>

View File

@ -10,7 +10,7 @@
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
<children>
<TextArea editable="false" prefHeight="800.0" prefWidth="1000.0" text="工作内容:&#10; ①检查档案管理系统或电子目录的挂接准确率要求100%&#10; ②逐件验证数字化成果与目录的关联性&#10; ③结果填入《挂接检查登记表》附件5。" wrapText="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<TextArea editable="false" prefHeight="800.0" prefWidth="1000.0" text="工作内容:&#10; 1.检查档案管理系统或电子目录的挂接准确率要求100%&#10; 2.逐件验证数字化成果与目录的关联性。如挂接内容与档案目录不一致,则验收不通过,要求整改。&#10; 3.验证结果填入《挂接检查登记表》附件5。" wrapText="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<font>
<Font size="18.0" />
</font></TextArea>

View File

@ -124,7 +124,7 @@
<Font size="14.0" />
</font>
</Button>
<TextArea editable="false" maxWidth="1.7976931348623157E308" prefWidth="400.0" text="对照《存储载体检查登记表》附件7检查并记录存储载体的类型/数量/内容/可读性情况。&#10;②将数字化成果(包括单页、多页文件及目录)打包生成&quot;数字化验收检测包.rar&quot;(含目录、哈希值列表、检测文件)&#10;③计算并验证压缩包的MD5或哈希值" wrapText="true" GridPane.columnIndex="3" GridPane.rowIndex="3">
<TextArea editable="false" maxWidth="1.7976931348623157E308" prefWidth="400.0" text="1.对照《存储载体检查登记表》附件7检查并记录存储载体的类型/数量/内容/可读性情况。&#10;2.计算数字化成果(包括单页、多页文件)的MD5码生成列表文件保存在目录所在文件夹&#10;3.将列表文件、目录文件、检测过程文件第2步生成的逻辑地址、物理地址等csv文件打包生成&quot;数字化验收检测包.rar&quot;&#10;4.计算并验证压缩包的MD5码或哈希值&#10;5.结果填入《存储载体检查登记表》附件7" wrapText="true" GridPane.columnIndex="3" GridPane.rowIndex="3">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>

View File

@ -27,22 +27,22 @@
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Label text="单个扫描超时时间:" GridPane.rowIndex="1">
<Label text="步骤扫描超时时间:" GridPane.rowIndex="1">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</Label>
<Spinner fx:id="scanSingleTimeOutS" onMouseDragExited="#onCheckOne" GridPane.columnIndex="1" GridPane.rowIndex="1">
<Spinner fx:id="scanTimeOutS" onMouseDragExited="#onCheckOne" GridPane.columnIndex="1" GridPane.rowIndex="1">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</Spinner>
<Label prefWidth="114.0" text="总扫描超时时间:" GridPane.rowIndex="2">
<Label prefWidth="114.0" text="步骤任务超时时间:" GridPane.rowIndex="2">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</Label>
<Spinner fx:id="scanTotalTimeOutS" onMouseDragExited="#onCheckTwo" GridPane.columnIndex="1" GridPane.rowIndex="2">
<Spinner fx:id="taskTimeOutS" onMouseDragExited="#onCheckTwo" GridPane.columnIndex="1" GridPane.rowIndex="2">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@ -1,53 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
<property name="APP_NAME" value="DocCheckTool" />
<property name="LOG_HOME" value="${log.dir:-logs}/${APP_NAME}"/>
<property name="ENCODER_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{80} - %msg%n" />
<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"/>
<contextName>${APP_NAME}</contextName>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<Pattern>${ENCODER_PATTERN}</Pattern>
<encoder>
<pattern>${DEBUG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- RELEASE 文件日志 -->
<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}/output.%d{yyyy-MM-dd}.log</fileNamePattern>
<fileNamePattern>${LOG_HOME}/archive/release.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${ENCODER_PATTERN}</pattern>
<encoder>
<pattern>${RELEASE_PATTERN}</pattern>
</encoder>
</appender>
<appender name="ERROR_FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- DEBUG 文件日志 -->
<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}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<fileNamePattern>${LOG_HOME}/archive/debug.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${ENCODER_PATTERN}</pattern>
<encoder>
<pattern>${DEBUG_PATTERN}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
</appender>
<appender name="SYNC_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- TRACKER 文件日志(默认不启用) -->
<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}/sync.%d{yyyy-MM-dd}.log</fileNamePattern>
<fileNamePattern>${LOG_HOME}/archive/tracker.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${ENCODER_PATTERN}</pattern>
<encoder>
<pattern>${TRACKER_PATTERN}</pattern>
</encoder>
</appender>
<logger name="log.sync" level="DEBUG" addtivity="true">
<appender-ref ref="SYNC_FILE" />
<!-- 全局 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"/>
<appender-ref ref="STDOUT"/>
</logger>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
<appender-ref ref="ERROR_FILE" />
<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 level="INFO">
<appender-ref ref="RELEASE_FILE"/>
<appender-ref ref="STDOUT"/>
</root>
</configuration>
</configuration>