feat:界面变动,逻辑优化

This commit is contained in:
叁玖领域 2025-08-20 21:45:05 +08:00
parent dcfc19aafa
commit 1bf1579e43
46 changed files with 1454 additions and 503 deletions

View File

@ -64,7 +64,6 @@ dependencies {
// classpath
implementation 'ch.qos.logback:logback-classic:1.5.6'
implementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.20.0'
implementation 'commons-cli:commons-cli:1.9.0'
implementation 'com.alibaba:easyexcel:4.0.3'
implementation 'org.apache.pdfbox:pdfbox:3.0.5'
@ -131,18 +130,22 @@ tasks.register('runCli', JavaExec) {
}
// JAR的任务
tasks.register('buildCliJar', Jar) {
tasks.register('buildFatJar', Jar) {
group = 'build'
description = 'Builds a standalone JAR for CLI mode'
description = 'Builds a standalone JAR with all dependencies'
manifest {
attributes 'Main-Class': 'top.r3944realms.docchecktoolrefactored.Main'
attributes(
'Main-Class': 'top.r3944realms.docchecktoolrefactored.Main'
)
}
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
} with jar
}
with jar // jar
archiveBaseName = 'doc-check-tool-cli'
archiveBaseName.set('doc-check-tool-cli')
archiveVersion.set('1.0')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

View File

@ -0,0 +1,4 @@
# 改动
1. 改名
2. 界面颜色
3.

View File

@ -1,9 +1,12 @@
package top.r3944realms.docchecktoolrefactored;
import javafx.application.Application;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import top.r3944realms.docchecktoolrefactored.ui.SceneManager;
import java.util.Objects;
public class JavaFxApplication extends Application {
@Override
public void init() throws Exception {
@ -13,6 +16,7 @@ public class JavaFxApplication extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
SceneManager.init(primaryStage);
primaryStage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/img/icon.jpg"))));
SceneManager.switchLoginView();
primaryStage.show();
}

View File

@ -2,6 +2,7 @@ package top.r3944realms.docchecktoolrefactored;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.cil.CliProcessor;
import top.r3944realms.docchecktoolrefactored.core.Setting;
import java.util.ArrayList;
import java.util.Arrays;
@ -12,14 +13,13 @@ import java.util.List;
*/
@Slf4j
public class Main {
/**
* The entry point of application.
*
* @param args the input arguments
*/
@SuppressWarnings("DataFlowIssue")
public static void main(String[] args) {
System.init();
// log.info(StringUtil.NO_BUG);
// 检查是否有 --cli 参数
List<String> list = Arrays.asList(args);

View File

@ -0,0 +1,188 @@
package top.r3944realms.docchecktoolrefactored;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.core.Setting;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.Properties;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Slf4j
public enum System {
INSTANCE;
private volatile Setting setting;
private volatile File lastModifiedFile;
private static final String CONFIG_FILE_NAME = "config.ini";
private static final Properties properties = new Properties();
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 默认值
private static final long DEFAULT_SINGLE_TIMEOUT = 30;
private static final long DEFAULT_TOTAL_TIMEOUT = 300;
private static final boolean DEFAULT_ENABLE_STEP = false;
public static void init() {
loadSettings();
Runtime.getRuntime().addShutdownHook(new Thread(System::saveSettings));
}
/** 加载配置文件 */
private static void loadSettings() {
lock.writeLock().lock();
try {
Path configPath = getConfigPath();
if (Files.exists(configPath)) {
try (InputStream input = new FileInputStream(configPath.toFile())) {
properties.load(input);
log.debug(LoggerHelper.DEBUG_MARKER, "配置文件加载成功: {}", configPath);
INSTANCE.setting = propertiesToSetting(properties);
} catch (IOException e) {
log.error(LoggerHelper.DEBUG_MARKER, "读取配置文件失败: {}, 使用默认配置", e.getMessage());
INSTANCE.setting = defaultSetting();
settingToProperties(INSTANCE.setting, properties);
}
} else {
INSTANCE.setting = defaultSetting();
settingToProperties(INSTANCE.setting, properties);
saveSettings(); // 首次启动保存默认配置
log.debug(LoggerHelper.DEBUG_MARKER, "配置文件不存在,已创建默认配置: {}", configPath);
}
} finally {
lock.writeLock().unlock();
}
}
/** 保存配置文件 */
private static void saveSettings() {
lock.readLock().lock();
try {
if (INSTANCE.setting != null) {
Path configPath = getConfigPath();
Path configDir = configPath.getParent();
if (!Files.exists(configDir)) {
Files.createDirectories(configDir);
}
settingToProperties(INSTANCE.setting, properties);
try (OutputStream output = new FileOutputStream(configPath.toFile())) {
properties.store(new OutputStreamWriter(output, StandardCharsets.UTF_8),
"DocCheckTool Configuration");
log.debug(LoggerHelper.DEBUG_MARKER, "配置文件保存成功: {}", configPath);
} catch (IOException e) {
log.error(LoggerHelper.DEBUG_MARKER, "保存配置文件失败: {}", e.getMessage());
}
}
} catch (IOException e) {
log.error(LoggerHelper.DEBUG_MARKER, "创建配置目录失败: {}", e.getMessage());
} finally {
lock.readLock().unlock();
}
}
/** 获取配置文件路径 */
private static Path getConfigPath() {
String userHome = java.lang.System.getProperty("user.home");
return Paths.get(userHome, ".docchecktool", CONFIG_FILE_NAME);
}
/** 将Setting对象转换为Properties */
private static void settingToProperties(Setting setting, Properties props) {
props.setProperty("singleTimeout", String.valueOf(setting.getSingleTimeout()));
props.setProperty("totalTimeout", String.valueOf(setting.getTotalTimeout()));
props.setProperty("enableStep", String.valueOf(setting.isEnableStep()));
}
/** 将Properties转换为Setting对象 */
private static Setting propertiesToSetting(Properties props) {
Setting s = new Setting();
try {
s.setSingleTimeout(Long.parseLong(props.getProperty("singleTimeout", String.valueOf(DEFAULT_SINGLE_TIMEOUT))));
} catch (NumberFormatException e) {
s.setSingleTimeout(DEFAULT_SINGLE_TIMEOUT);
log.error(LoggerHelper.DEBUG_MARKER, "singleTimeout格式错误使用默认值{}", DEFAULT_SINGLE_TIMEOUT);
}
try {
s.setTotalTimeout(Long.parseLong(props.getProperty("totalTimeout", String.valueOf(DEFAULT_TOTAL_TIMEOUT))));
} catch (NumberFormatException e) {
s.setTotalTimeout(DEFAULT_TOTAL_TIMEOUT);
log.error(LoggerHelper.DEBUG_MARKER, "totalTimeout格式错误使用默认值{}", DEFAULT_TOTAL_TIMEOUT);
}
try {
s.setEnableStep(Boolean.parseBoolean(props.getProperty("enableStep", String.valueOf(Boolean.FALSE))));
} catch (Exception e) {
s.setEnableStep(DEFAULT_ENABLE_STEP);
log.error(LoggerHelper.DEBUG_MARKER, "enableStep格式错误使用默认值{}", DEFAULT_TOTAL_TIMEOUT);
}
return s;
}
/** 获取默认Setting */
private static Setting defaultSetting() {
Setting s = new Setting();
s.setSingleTimeout(DEFAULT_SINGLE_TIMEOUT);
s.setTotalTimeout(DEFAULT_TOTAL_TIMEOUT);
return s;
}
/** 手动保存 */
public static void saveSettingsNow() {
saveSettings();
}
/** 重新加载 */
public static void reloadSettings() {
loadSettings();
}
/** 获取Setting对象 */
public static Setting getSetting() {
return INSTANCE.setting;
}
/** 获取File 对象 */
public static File getlastModifiedFile() {
return INSTANCE.lastModifiedFile;
}
/** 获取配置目录路径 */
public static String getConfigDirectory() {
return getConfigPath().getParent().toString();
}
/** 获取配置文件路径 */
public static String getConfigFilePath() {
return getConfigPath().toString();
}
public static void setLastModifiedFile(File lastModifiedFile) {
INSTANCE.lastModifiedFile = lastModifiedFile;
}
public static FileChooser getFileChooser() {
FileChooser fileChooser = new FileChooser();
File lastFile = getlastModifiedFile();
if (lastFile != null) {
File parentDir = lastFile.getParentFile();
if (parentDir != null && parentDir.exists() && parentDir.isDirectory()) {
fileChooser.setInitialDirectory(parentDir);
}
}
return fileChooser;
}
public static DirectoryChooser getDirectoryChooser() {
DirectoryChooser directoryChooser = new DirectoryChooser();
File lastFile = getlastModifiedFile();
if (lastFile != null) {
File parentDir = lastFile.getParentFile();
if (parentDir != null && parentDir.exists() && parentDir.isDirectory()) {
directoryChooser.setInitialDirectory(parentDir);
}
}
return directoryChooser;
}
}

View File

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

View File

@ -2,6 +2,7 @@ package top.r3944realms.docchecktoolrefactored.core;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import java.io.BufferedReader;
import java.io.File;
@ -64,8 +65,7 @@ public class AddressFileComparator {
int physicalCount = physicalRecords.size();
int logicalCount = logicalRecords.size();
log.info("读取物理地址文件记录数: {}", physicalCount);
log.info("读取逻辑地址文件记录数: {}", logicalCount);
log.info(LoggerHelper.DEBUG_MARKER, "读取物理地址文件记录数: {}, 读取逻辑地址文件记录数: {}", physicalCount, logicalCount);
List<String> forwardComparisonResults = new ArrayList<>(); // 物理文件在逻辑文件中未找到
List<String> backwardComparisonResults = new ArrayList<>(); // 逻辑文件在物理文件中未找到
@ -178,7 +178,7 @@ public class AddressFileComparator {
try {
File file = new File(filePath);
if (!file.exists()) {
log.error("CSV文件不存在: {}", filePath);
log.error(LoggerHelper.RELEASE_MARKER, "CSV文件不存在: {}", filePath);
return records;
}
@ -198,10 +198,10 @@ public class AddressFileComparator {
}
reader.close();
log.info("成功读取CSV文件共 {} 行记录", records.size());
log.info(LoggerHelper.DEBUG_MARKER, "成功读取CSV文件共 {} 行记录", records.size());
} catch (Exception e) {
log.error("读取CSV文件时出错: {}", e.getMessage(), e);
log.error(LoggerHelper.RELEASE_MARKER, "读取CSV文件时出错: {}", e.getMessage(), e);
}
return records;
@ -211,49 +211,46 @@ public class AddressFileComparator {
List<String> forwardResults, List<String> backwardResults,
List<String> pathMismatchResults, List<String> pageCountMismatchResults,
CompareMode compareMode) {
log.info("=== 文件比较结果 ===");
log.info("物理地址文件记录数: {}", physicalCount);
log.info("逻辑地址文件记录数: {}", logicalCount);
StringBuilder sb = new StringBuilder();
sb.append("=== 文件比较结果 ===\n");
sb.append("物理地址文件记录数: ").append(physicalCount).append("\n");
sb.append("逻辑地址文件记录数: ").append(logicalCount).append("\n");
if (pathMismatchResults.isEmpty()) {
log.info("没有路径错误");
sb.append("没有路径错误\n");
} else {
log.info("文件名相同但路径不一致的记录数量: {}", pathMismatchResults.size());
for (String result : pathMismatchResults) {
log.info("\t{}", result);
}
sb.append("文件名相同但路径不一致的记录数量: ").append(pathMismatchResults.size()).append("\n");
pathMismatchResults.forEach(result -> sb.append("\t").append(result).append("\n"));
}
if (compareMode == CompareMode.FILE_LEVEL) {
if (pageCountMismatchResults.isEmpty()) {
log.info("没有页数错误");
sb.append("没有页数错误\n");
} else {
log.info("文件名和路径相同但页数不一致的记录数量: {}", pageCountMismatchResults.size());
for (String result : pageCountMismatchResults) {
log.info("\t{}", result);
}
sb.append("文件名和路径相同但页数不一致的记录数量: ")
.append(pageCountMismatchResults.size()).append("\n");
pageCountMismatchResults.forEach(result -> sb.append("\t").append(result).append("\n"));
}
}
if (forwardResults.isEmpty()) {
log.info("没有物理存在而逻辑不存在的文件");
sb.append("没有物理存在而逻辑不存在的文件\n");
} else {
log.info("物理文件在逻辑文件中未找到的记录数量: {}", forwardResults.size());
for (String result : forwardResults) {
log.info("\t{}", result);
}
sb.append("物理文件在逻辑文件中未找到的记录数量: ").append(forwardResults.size()).append("\n");
forwardResults.forEach(result -> sb.append("\t").append(result).append("\n"));
}
if (backwardResults.isEmpty()) {
log.info("没有逻辑存在而物理不存在的文件");
sb.append("没有逻辑存在而物理不存在的文件\n");
} else {
log.info("逻辑文件在物理文件中未找到的记录数量: {}", backwardResults.size());
for (String result : backwardResults) {
log.info("\t{}", result);
}
sb.append("逻辑文件在物理文件中未找到的记录数量: ").append(backwardResults.size()).append("\n");
backwardResults.forEach(result -> sb.append("\t").append(result).append("\n"));
}
log.info("=== 比较完成 ===");
sb.append("=== 比较完成 ===");
log.info(LoggerHelper.RELEASE_MARKER, sb.toString()); // 一次性输出, 减少 I/O
}
// 为向后兼容保留原来的日志方法

View File

@ -6,11 +6,11 @@ import java.io.File;
public interface AddressFileGenerator {
/**
* 页面级
* 页面级 JPG那种
*/
int PAGE_TYPE = 1;
/**
* 文件级
* 文件级 PDF那种
*/
int FILE_TYPE = 2;

View File

@ -1,15 +1,19 @@
package top.r3944realms.docchecktoolrefactored.core;
import com.sun.scenario.Settings;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
import top.r3944realms.docchecktoolrefactored.model.DuplicateGroup;
import top.r3944realms.docchecktoolrefactored.model.FileMetadata;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@ -17,64 +21,103 @@ import java.util.stream.Collectors;
/**
* 重复文件查找核心类
*/
//TODO代替DuplicateDocumentDetectionTask
@Slf4j
public class DuplicateFinder {
private final FileScanner fileScanner;
private final FileHashCalculator hashCalculator;
private final boolean enableProgress;
private final ExecutorService executorService;
// 进度回调接口
public interface ProgressCallback {
default void onPhaseStarted(Phase phase) {}
default void onPhaseProgress(Phase phase, int current, int total) {}
default void onPhaseCompleted(Phase phase) {}
}
public enum Phase {
GROUP_BY_SIZE, // 按大小分组阶段
CALCULATE_HASH // 计算哈希阶段
}
@Setter
private ProgressCallback progressCallback;
private static final int PROGRESS_REPORT_INTERVAL = 100;
private static final int BATCH_SIZE = 100;
private final List<Exception> errors = new CopyOnWriteArrayList<>();
@Getter
private long timeout = -1;
public DuplicateFinder(FileScanner fileScanner, FileHashCalculator hashCalculator, boolean enableProgress) {
this.fileScanner = Objects.requireNonNull(fileScanner);
this.hashCalculator = Objects.requireNonNull(hashCalculator);
this.enableProgress = enableProgress;
// 根据CPU核心数设置线程池大小
int poolSize = Runtime.getRuntime().availableProcessors();
this.executorService = Executors.newFixedThreadPool(poolSize);
}
public DuplicateFinder(FileScanner fileScanner, FileHashCalculator hashCalculator) {
this(fileScanner, hashCalculator, false);
}
public DuplicateFinder applySetting(Setting setting) {
this.timeout = setting.getSingleTimeout();
return this;
}
/**
* 查找重复文件
* @param rootDir 要扫描的根目录
* @return 按哈希值分组的重复文件列表
*/
public List<DuplicateGroup> findDuplicates(Path rootDir) throws IOException {
// 清理错误列表
errors.clear();
// -----------------------------
// 第一阶段按文件大小分组
// -----------------------------
if (progressCallback != null) {
progressCallback.onPhaseStarted(Phase.GROUP_BY_SIZE);
}
Map<Long, List<FileMetadata>> sizeGroups = groupFilesBySize(rootDir);
// 计算需要处理的总文件数大小分组中可能有重复的文件
int totalFilesToProcess = sizeGroups.values().stream()
.filter(group -> group.size() > 1)
.mapToInt(List::size)
.sum();
if (totalFilesToProcess == 0) {
return Collections.emptyList();
if (progressCallback != null) {
progressCallback.onPhaseCompleted(Phase.GROUP_BY_SIZE);
}
// 第二阶段对可能重复的文件计算哈希
// -----------------------------
// 第二阶段按文件组计算哈希
// -----------------------------
if (progressCallback != null) progressCallback.onPhaseStarted(Phase.CALCULATE_HASH);
Map<String, List<FileMetadata>> hashGroups = new ConcurrentHashMap<>();
AtomicInteger processedFiles = new AtomicInteger(0);
sizeGroups.values().parallelStream()
.filter(group -> group.size() > 1) // 只处理可能重复的文件
.forEach(group -> group.parallelStream().forEach(file -> {
try {
String hash = hashCalculator.calculateHash(file.getPath());
file.setHash(hash);
hashGroups.computeIfAbsent(hash, k -> new ArrayList<>()).add(file);
// 更新进度
int current = processedFiles.incrementAndGet();
if (enableProgress) {
printProgress("Calculating hashes", current, totalFilesToProcess);
}
} catch (IOException e) {
// 记录错误但继续处理其他文件
log.error("Failed to calculate file's hash: {}, {}", file.getPath(), e.getMessage());
}
}));
if (enableProgress) {
System.out.println(); // 完成进度条后换行
// 获取候选文件组每组内至少2个文件
List<List<FileMetadata>> candidateGroups = sizeGroups.values().stream()
.filter(group -> group.size() > 1)
.toList();
int totalFilesToProcess = candidateGroups.stream().mapToInt(List::size).sum();
if (totalFilesToProcess == 0) return Collections.emptyList();
List<CompletableFuture<Void>> futures = new ArrayList<>();
// 分批提交线程池每组作为一个批次
for (List<FileMetadata> group : candidateGroups) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
for (FileMetadata file : group) {
processFile(file, hashGroups, processedFiles, totalFilesToProcess);
}
}, executorService);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
if (progressCallback != null) progressCallback.onPhaseCompleted(Phase.CALCULATE_HASH);
if (enableProgress) System.out.println();
// -----------------------------
// 第三阶段构建结果
// -----------------------------
return hashGroups.values().stream()
.filter(group -> group.size() > 1)
.map(group -> new DuplicateGroup(
@ -91,13 +134,15 @@ public class DuplicateFinder {
*/
private Map<Long, List<FileMetadata>> groupFilesBySize(Path rootDir) throws IOException {
Map<Long, List<FileMetadata>> sizeGroups = new ConcurrentHashMap<>();
boolean flag = timeout != -1 && timeout > 0;
FileScanner.ProgressAwareListener listener = new FileScanner.ProgressAwareListener() {
@Override
public void onProgressUpdate(int current, int total) {
if (enableProgress) {
printProgress("Scanning files", current, total);
} else {
log.info("Scanning progress: {} / {} ", current, total);
}
if (progressCallback != null) {
progressCallback.onPhaseProgress(Phase.GROUP_BY_SIZE, current, total);
}
}
@ -109,19 +154,20 @@ public class DuplicateFinder {
meta.setSize(Files.size(file));
sizeGroups.computeIfAbsent(meta.getSize(), k -> new ArrayList<>()).add(meta);
} catch (IOException e) {
log.error("Failed to get file's size: {}", file);
log.error(LoggerHelper.TRACE_MARKER, "Failed to get file's size: {}", file);
}
}
@Override public void onScanComplete() {}
@Override public void onError(Path file, Exception e) {
log.error("Error on scanning file: {}, {}", file, e.getMessage());
log.error(LoggerHelper.TRACE_MARKER, "Error on scanning file: {}, {}", file, e.getMessage());
errors.add(e);
}
};
if(enableProgress)
fileScanner.scanWithProgress(rootDir, listener);
if (flag) fileScanner.scanWithProgress(rootDir, listener, timeout); else fileScanner.scanWithProgress(rootDir, listener);
else
fileScanner.scan(rootDir, listener);
if (flag) fileScanner.scan(rootDir, listener, timeout); else fileScanner.scan(rootDir, listener);
return sizeGroups;
}
/**
@ -144,4 +190,35 @@ public class DuplicateFinder {
System.out.print(progressBar);
}
private void processFile(FileMetadata file,
Map<String, List<FileMetadata>> hashGroups,
AtomicInteger processedFiles,
int totalFilesToProcess) {
try {
String hash = file.getSize() > 10_000_000 ?
hashCalculator.calculateHashStreaming(file.getPath()) :
hashCalculator.calculateHash(file.getPath());
file.setHash(hash);
synchronized (hashGroups) {
hashGroups.computeIfAbsent(hash, k -> new ArrayList<>()).add(file);
}
int current = processedFiles.incrementAndGet();
if (enableProgress && (current % PROGRESS_REPORT_INTERVAL == 0 || current == totalFilesToProcess)) {
printProgress("Calculating hashes", current, totalFilesToProcess);
}
if (progressCallback != null) {
progressCallback.onPhaseProgress(Phase.CALCULATE_HASH, current, totalFilesToProcess);
}
} catch (IOException e) {
log.error(LoggerHelper.TRACE_MARKER, "Failed to calculate file's hash: {}, {}", file.getPath(), e.getMessage());
errors.add(e);
}
}
public void shutdown() {
executorService.shutdown();
}
}

View File

@ -14,6 +14,13 @@ public interface FileHashCalculator {
*/
String calculateHash(Path file) throws IOException;
/**
* 流式计算文件哈希值适用于大文件
* @param file 要计算的文件路径
* @return 文件的哈希值字符串
*/
String calculateHashStreaming(Path file) throws IOException;
/**
* 计算文件部分hash值方法
* @param file 要计算的文件路径
@ -36,7 +43,15 @@ public interface FileHashCalculator {
* @return 块大小
*/
default int getPartialSize() {
return 4096;// 4 * 1024
return 4096;// 4KB
}
/**
* 获取流式处理的缓冲区大小
* @return 缓冲区大小
*/
default int getStreamingBufferSize() {
return 8192; // 8KB
}
/**

View File

@ -1,8 +1,10 @@
package top.r3944realms.docchecktoolrefactored.core;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import java.io.BufferedWriter;
import java.io.FileWriter;
@ -13,7 +15,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class HashFileGenerator {
public interface ProgressListener {
@ -47,7 +49,7 @@ public class HashFileGenerator {
@Override
public void onError(Path path, Exception e) {
System.err.println("Error scanning path: " + path + " - " + e.getMessage());
log.error(LoggerHelper.TRACE_MARKER, "Error scanning path: {} - {}", path, e.getMessage());
}
});
@ -74,7 +76,7 @@ public class HashFileGenerator {
listener.onProgressUpdate(processed, totalFiles);
}
} catch (IOException e) {
System.err.println("无法计算该文件哈希值: " + file + " - " + e.getMessage());
log.error(LoggerHelper.DEBUG_MARKER, "无法计算该文件哈希值: {} - {}", file, e.getMessage());
}
});

View File

@ -1,17 +1,23 @@
package top.r3944realms.docchecktoolrefactored.core;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import static org.apache.commons.codec.digest.MessageDigestAlgorithms.MD5;
/**
* MD5哈希计算实现
*/
public class MD5HashCalculator implements FileHashCalculator {
private static final int BUFFER_SIZE = 8192;
private static final HexFormat HEX_FORMAT = HexFormat.of();
@Override
public String calculateHash(Path file) throws IOException {
@ -29,6 +35,25 @@ public class MD5HashCalculator implements FileHashCalculator {
throw new RuntimeException("MD5算法不可用", e);
}
}
@Override
public String calculateHashStreaming(Path file) throws IOException {
try (InputStream is = Files.newInputStream(file);
DigestInputStream dis = new DigestInputStream(is, MessageDigest.getInstance(MD5))) {
byte[] buffer = new byte[getStreamingBufferSize()];
// 读取整个文件以更新摘要
while (dis.read(buffer) != -1) {
// 只需读取即可DigestInputStream会自动更新摘要
//noinspection UnnecessaryContinue
continue;
}
byte[] digest = dis.getMessageDigest().digest();
return HEX_FORMAT.formatHex(digest);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("MD5 algorithm not available", e);
}
}
/**
* <h1>读取文件头部</h1>

View File

@ -3,6 +3,7 @@ package top.r3944realms.docchecktoolrefactored.core;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import java.io.File;
import java.io.PrintWriter;
@ -143,7 +144,7 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator {
document.close();
return pageCount;
} catch (Exception e) {
log.warn("无法获取PDF文件页数: {}", pdfFile.getAbsolutePath(), e);
log.warn(LoggerHelper.RELEASE_MARKER, "无法获取PDF文件页数: {}", pdfFile.getAbsolutePath(), e);
return 0;
}
}
@ -215,7 +216,7 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator {
for (int i = 0; i < pathParts.length; i++) {
if (!foundPrefix) {
// 检查当前部分是否包含prefix
if (pathParts[i].contains(prefix)) {
if (pathParts[i].equals(prefix)) {
foundPrefix = true;
resultPath.append(pathParts[i]);
}

View File

@ -0,0 +1,10 @@
package top.r3944realms.docchecktoolrefactored.core;
import java.util.List;
public class ScanningException extends RuntimeException {
public final List<Exception> exceptions;
public ScanningException(List<Exception> exceptions) {
this.exceptions = exceptions;
}
}

View File

@ -0,0 +1,14 @@
package top.r3944realms.docchecktoolrefactored.core;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@Setter
@Getter
@Slf4j
public class Setting {
private long singleTimeout = 30;
private long totalTimeout = 60 * 5;
private boolean enableStep = false;
}

View File

@ -5,12 +5,14 @@ import com.linuxense.javadbf.DBFRow;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.util.FileUtil;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@ -20,8 +22,11 @@ import java.util.Optional;
@Slf4j
public class DbfFileReader implements CatalogFileReader {
// 常量定义避免硬编码
private static final String FIELD_ARCHIVE_CODE = "档号";
private static final String FIELD_PAGE = "页数";
private static final List<String> ARCHIVE_CODE_TAG_CANDIDATES =
Arrays.asList("档号", "dangan", "fileNo", "DH");
private static final List<String> PAGE_COUNT_TAG_CANDIDATES =
Arrays.asList("页数", "pages", "pageCount", "YS");
@Override
public List<LogicalAddressFileGenerator.Record> readCatalogFile(String filePath) throws Exception {
@ -33,36 +38,35 @@ public class DbfFileReader implements CatalogFileReader {
DBFReader reader = new DBFReader(fis)
) {
int fieldCount = reader.getFieldCount();
log.debug("开始读取DBF文件: {}, DBF文件字段数: {}",filePath, fieldCount);
log.debug(LoggerHelper.DEBUG_MARKER, "开始读取DBF文件: {}, DBF文件字段数: {}",filePath, fieldCount);
// 查找"档号""页数"字段的索引
int archiveCodeIndex = -1;
int pageIndex = -1;
for (int i = 0; i < fieldCount; i++) {
if (archiveCodeIndex != -1 && pageIndex != -1) {
log.debug("已找到所需字段,跳出循环,档号: {}, 页数: {}", archiveCodeIndex, pageIndex);
log.debug(LoggerHelper.DEBUG_MARKER, "已找到所需字段,跳出循环,档号: {}, 页数: {}", archiveCodeIndex, pageIndex);
break;
}
String fieldName = reader.getField(i).getName();
log.debug("发现字段: {}", fieldName);
if (FIELD_ARCHIVE_CODE.equals(fieldName)) {
log.debug(LoggerHelper.DEBUG_MARKER, "发现字段: {}", fieldName);
if (ARCHIVE_CODE_TAG_CANDIDATES.contains(fieldName)) {
archiveCodeIndex = i;
log.debug("找到‘档号’字段,索引: {}", archiveCodeIndex);
} else if (FIELD_PAGE.equals(fieldName)) {
log.debug(LoggerHelper.DEBUG_MARKER, "匹配到档号字段: {}, 索引: {}", fieldName, archiveCodeIndex);
} else if (PAGE_COUNT_TAG_CANDIDATES.contains(fieldName)) {
pageIndex = i;
log.debug("找到‘页数’字段,索引: {}", pageIndex);
log.debug(LoggerHelper.DEBUG_MARKER, "匹配到页数字段: {}, 索引: {}", fieldName, pageIndex);
}
}
if (archiveCodeIndex == -1 || pageIndex == -1) {
log.error("未找到必要字段,档号: {}, 页数: {}",
log.error(LoggerHelper.RELEASE_MARKER, "未找到必要字段,档号: {}, 页数: {}",
archiveCodeIndex == -1 ? "未找到" : archiveCodeIndex,
pageIndex == -1 ? "未找到" : pageIndex
);
throw new IllegalArgumentException(
String.format("DBF文件缺少必要字段: %s=%s, %s=%s",
FIELD_ARCHIVE_CODE, archiveCodeIndex == -1,
FIELD_PAGE, pageIndex == -1
String.format("DBF文件缺少必要字段: 档号=%s, 页数=%s",
archiveCodeIndex == -1, pageIndex == -1
)
);
}
@ -85,7 +89,7 @@ public class DbfFileReader implements CatalogFileReader {
return Integer.parseInt(i.toString().trim());
}
} catch (NumberFormatException e) {
log.warn("无法将页数值转换为整数: {}", i);
log.warn(LoggerHelper.DEBUG_MARKER, "无法将页数值转换为整数: {}", i);
return 0;
}
}).orElse(0);
@ -94,17 +98,17 @@ public class DbfFileReader implements CatalogFileReader {
if (!archiveCode.isEmpty() && page > 0) {
records.add(new LogicalAddressFileGenerator.Record(archiveCode, page));
validRecords++;
log.debug("读取有效记录 - 档号: {}, 页数: {}", archiveCode, page);
log.debug(LoggerHelper.DEBUG_MARKER, "读取有效记录 - 档号: {}, 页数: {}", archiveCode, page);
} else {
skippedRecords++;
if (!archiveCode.isEmpty() || page > 0) {
log.debug("跳过无效记录 - 档号: {}, 页数: {}", archiveCode, page);
log.debug(LoggerHelper.DEBUG_MARKER, "跳过无效记录 - 档号: {}, 页数: {}", archiveCode, page);
}
}
}
log.info("DBF文件读取完成有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords);
log.info(LoggerHelper.RELEASE_MARKER, "DBF文件读取完成有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords);
} catch (IOException e) {
log.error("读取DBF文件失败: {}", filePath, e);
log.error(LoggerHelper.RELEASE_MARKER, "读取DBF文件失败: {}", filePath, e);
throw new UncheckedIOException("DBF文件读取异常", e);
}
return records;

View File

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

View File

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

View File

@ -21,6 +21,23 @@ public interface FileScanner {
default void scanWithProgress(Path rootPath, ProgressAwareListener listener) {
throw new UnsupportedOperationException("Please implement FileScanner, ProgressAwareListener.");
}
/**
* 扫描指定路径下的文件 (带超时)
*
* @param rootPath 根路径
* @param listener 文件发现监听器
* @param timeout 超时s
*/
default void scan(Path rootPath, FileScanListener listener, long timeout) {
throw new UnsupportedOperationException("Please implement FileScanner, FileScannerListener.");
}
/**
* 扫描指定路径下的文件带进度反馈(带超时)
* @param timeout 超时s
*/
default void scanWithProgress(Path rootPath, ProgressAwareListener listener, long timeout) {
throw new UnsupportedOperationException("Please implement FileScanner, ProgressAwareListener.");
}
/**
* 文件扫描监听器

View File

@ -1,135 +0,0 @@
package top.r3944realms.docchecktoolrefactored.io.scanner;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
/**
* The type Parallel file scanner.
* <p>
* 这个没法正常使用目前遇到的问题
* <p>
* * 目录遍历时遇到权限问题静默失败
* <p>
* * 存在符号链接循环
* <p>
* * 文件系统驱动程序卡死
* <p>
* * JVM与NTFS文件系统兼容性问题
*/
@Slf4j
@Deprecated
public class ParallelFileScanner implements FileScanner ,AutoCloseable {
private final ForkJoinPool forkJoinPool;
private volatile boolean cancelled = false;
/**
* 使用默认并行度CPU核心数
*/
public ParallelFileScanner() {
this(Runtime.getRuntime().availableProcessors());
}
/**
* Instantiates a new Parallel file scanner.
*
* @param parallelism 并行度线程数
*/
public ParallelFileScanner(int parallelism) {
this.forkJoinPool = new ForkJoinPool(parallelism);
}
@Override
public void scan(Path rootPath, FileScanListener listener) {
scanInternal(rootPath, listener, null);
}
@Override
public void scanWithProgress(Path rootPath, ProgressAwareListener listener) {
// 先快速统计总文件数
long totalFiles = countFiles(rootPath);
scanInternal(rootPath, listener, totalFiles);
}
private long countFiles(Path rootPath) {
try(Stream<Path> pathStream = Files.walk(rootPath)
.parallel()
.filter(Files::isRegularFile)) {
return pathStream.count();
} catch (IOException e) {
return -1; // 表示无法确定总数
}
}
private void scanInternal(Path rootPath, FileScanListener listener, Long totalFiles) {
log.debug("ThreadPool Status: {}", forkJoinPool.isShutdown() ? "Closed" : "Running");
forkJoinPool.submit(() -> { // 方法没问题可能就是在线程这里被卡死了
try {
AtomicInteger processed = new AtomicInteger(0);
log.debug("Scanning files in {}", rootPath);
// 收集所有文件到List避免Stream被重复使用
@SuppressWarnings("resource") List<Path> files = Files.walk(rootPath)
.peek(p -> log.trace("visiting: {}", p))
.parallel()
.filter(p -> {
boolean isRegular = Files.isRegularFile(p);
if (!isRegular) {
log.debug("Skip non-regular : {} ", p);
}
return isRegular;
})
.peek(p -> log.trace("Found file: {}", p))
.toList(); // 立即消费Stream
if (files.isEmpty()) {
log.warn("No files found in directory: {}", rootPath);
} else log.debug("Found {} files in {}", files.size(), rootPath);
files.forEach(file -> {
if (cancelled) {
log.debug("Cancelled scanning file {}", file);
return;
}
try {
log.debug("Handle file {}", file);
listener.onFileFound(file);
// 进度更新
if (listener instanceof ProgressAwareListener progressListener) {
int current = processed.incrementAndGet();
progressListener.onProgressUpdate(
current,
totalFiles != null ? totalFiles.intValue() : -1
);
}
} catch (Exception e) {
log.debug("Error Handle file {}", file, e);
listener.onError(file, e);
}
});
if (!cancelled) {
log.debug("Finished scanning files in {}", rootPath);
listener.onScanComplete();
}
} catch (IOException e) {
listener.onError(rootPath, e);
} catch (Exception e) {
log.error("Unexpected error in scan thread", e);
listener.onError(rootPath, e);
}
});
log.debug("Task submitted to thread pool");
}
public void cancel() {
cancelled = true;
forkJoinPool.shutdownNow();
}
@Override
public void close() {
cancel();
}
}

View File

@ -1,6 +1,7 @@
package top.r3944realms.docchecktoolrefactored.io.scanner;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import java.io.FileNotFoundException;
import java.io.IOException;
@ -26,17 +27,24 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable {
this.forkJoinPool = new ForkJoinPool(parallelism);
this.maxDepth = maxDepth; // 防止无限递归
}
@Override
public void scan(Path rootPath, FileScanListener listener) {
scanInternal(rootPath, listener, null);
public void scan(Path rootPath, FileScanListener listener, long timeout) {
scanInternal(rootPath, listener, null, timeout);
}
@Override
public void scanWithProgress(Path rootPath, ProgressAwareListener listener) {
public void scan(Path rootPath, FileScanListener listener) {
scan(rootPath, listener, 30);
}
@Override
public void scanWithProgress(Path rootPath, ProgressAwareListener listener, long timeout) {
// 预扫描阶段计算总文件数
AtomicLong totalFiles = new AtomicLong(0);
countFiles(rootPath, totalFiles);
scanInternal(rootPath, listener, totalFiles);
scanInternal(rootPath, listener, totalFiles, timeout);
}
@Override
public void scanWithProgress(Path rootPath, ProgressAwareListener listener) {
scanWithProgress(rootPath, listener, 30);
}
private void countFiles(Path dir, AtomicLong counter) {
if (cancelled) return;
@ -52,10 +60,13 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable {
}
}
} catch (IOException e) {
log.warn("Failed to pre-scan: {}", dir, e);
log.warn(LoggerHelper.TRACE_MARKER, "Failed to pre-scan: {}", dir, e);
}
}
private void scanInternal(Path rootPath, FileScanListener listener, AtomicLong totalFiles) {
scanInternal(rootPath, listener, totalFiles, 30);
}
private void scanInternal(Path rootPath, FileScanListener listener, AtomicLong totalFiles, long timeout) {
try {
validateDirectory(rootPath);
@ -70,9 +81,9 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable {
} catch (Exception e) {
listener.onError(rootPath, e);
}
}).get(30, TimeUnit.SECONDS);
}).get(timeout, TimeUnit.SECONDS);
} catch (TimeoutException e) {
log.error("Scan timeout: {}", rootPath, e);
log.error(LoggerHelper.TRACE_MARKER, "Scan timeout: {}", rootPath, e);
forkJoinPool.shutdownNow();
listener.onError(rootPath, new TimeoutException("扫描超时30秒"));
} catch (Exception e) {

View File

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

View File

@ -1,14 +1,25 @@
package top.r3944realms.docchecktoolrefactored.ui;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Menu;
import javafx.scene.control.Tab;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import top.r3944realms.docchecktoolrefactored.System;
import top.r3944realms.docchecktoolrefactored.core.Setting;
import java.util.ArrayList;
import java.util.List;
/**
* The type Main stage controller.
*/
public class MainStageController {
@FXML public Button nextB;
@FXML public Button prevB;
@FXML private Tab step1T;
@FXML private Tab step2T;
@FXML private Tab step3T;
@ -16,11 +27,110 @@ public class MainStageController {
@FXML private Tab step5T;
@FXML private Tab step6T;
@FXML private Tab step7T;
@FXML private TabPane tabPane;
@FXML private Menu helpM;
@FXML private MenuItem aboutSoftwareMI;
@FXML private MenuItem helpDocMI;
@FXML private MenuItem exitMI;
@FXML private MenuItem logoutMI;
@FXML private MenuItem settingMI;
private List<Tab> tabs;
private int currentIndex = 0;
@FXML
public void initialize() {
// 初始化所有Tab的集合
tabs = new ArrayList<>();
tabs.add(step1T);
tabs.add(step2T);
tabs.add(step3T);
tabs.add(step4T);
tabs.add(step5T);
tabs.add(step6T);
tabs.add(step7T);
// 初始状态设置
updateButtonStates();
updateStepButtonsVisibility(); // 根据配置初始可见性
// 监听Tab切换事件
tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) -> {
currentIndex = tabs.indexOf(newTab);
updateButtonStates();
});
}
/**
* 更新按钮状态
*/
private void updateButtonStates() {
prevB.setDisable(currentIndex == 0);
nextB.setDisable(currentIndex == tabs.size() - 1);
}
}
/**
* 切换到下一个Tab
* @param actionEvent 事件
*/
@FXML
void onNext(ActionEvent actionEvent) {
if (currentIndex < tabs.size() - 1) {
tabPane.getSelectionModel().select(tabs.get(currentIndex + 1));
}
}
/**
* 切换到上一个Tab
* @param actionEvent 事件
*/
@FXML
void onPrev(ActionEvent actionEvent) {
if (currentIndex > 0) {
tabPane.getSelectionModel().select(tabs.get(currentIndex - 1));
}
}
/**
* 处理键盘快捷键
* @param keyEvent 键盘事件
*/
@FXML
void handleKeyPressed(KeyEvent keyEvent) {
// Ctrl+> Ctrl+. 切换到下一个Tab
if (new KeyCodeCombination(KeyCode.PERIOD, KeyCombination.CONTROL_DOWN).match(keyEvent) ||
new KeyCodeCombination(KeyCode.GREATER, KeyCombination.CONTROL_DOWN).match(keyEvent)) {
onNext(null);
keyEvent.consume();
}
// Ctrl+< Ctrl+, 切换到上一个Tab
else if (new KeyCodeCombination(KeyCode.COMMA, KeyCombination.CONTROL_DOWN).match(keyEvent) ||
new KeyCodeCombination(KeyCode.LESS, KeyCombination.CONTROL_DOWN).match(keyEvent)) {
onPrev(null);
keyEvent.consume();
}
}
@FXML void onLogout(ActionEvent actionEvent) {
SceneManager.switchLoginView();
}
@FXML void onExit(ActionEvent actionEvent) {
}
@FXML void onOpenSetting(ActionEvent actionEvent) {
SceneManager.openSettingView();
}
@FXML void onOpenHelpDoc(ActionEvent actionEvent) {
}
@FXML void onAbout(ActionEvent actionEvent) {
}
public void updateStepButtonsVisibility() {
Setting setting = System.getSetting();
boolean visible = setting.isEnableStep(); // 由enableStep控制
nextB.setVisible(visible);
prevB.setVisible(visible);
}
}

View File

@ -5,6 +5,7 @@ import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.util.Duration;
import lombok.Getter;
@ -13,6 +14,7 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.Main;
import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import java.io.IOException;
import java.util.ArrayList;
@ -29,7 +31,8 @@ public class SceneManager {
@Getter
@Setter
private static Stage primaryStage;
@Getter
private static MainStageController mainController;
@Getter
private static final List<Stage> openStages = new ArrayList<>();
@ -46,14 +49,59 @@ public class SceneManager {
* Switch login view.
*/
public static void switchLoginView() {
SceneManager.loadView("/fxml/login-view.fxml","淮阴区数字化档案检查验收系统 - 登录", 400, 300);
SceneManager.loadView("/fxml/login-view.fxml","数字化验收工具 - 登录", 400, 300);
}
/**
* Switch main view.
*/
public static void switchMainView() {
SceneManager.loadView("/fxml/main-view.fxml", "淮阴区数字化档案检查验收系统 - 主界面", 1200, 900);
try {
FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(Main.class.getResource("/fxml/main-view.fxml")));
Parent root = loader.load();
mainController = loader.getController(); // 保存控制器引用
Scene newScene = new Scene(root, 1200, 900);
applyFadeTransition(root);
primaryStage.setScene(newScene);
primaryStage.centerOnScreen();
primaryStage.setTitle("数字化验收工具 - 主界面");
// 设置窗口关闭确认
primaryStage.setOnCloseRequest(event -> {
if (!DialogUtil.showExitConfirmation(primaryStage)) {
event.consume();
}
});
} catch (IOException e) {
log.error(LoggerHelper.TRACE_MARKER, "Failed to load main view", e);
DialogUtil.showErrorDialog("错误", "加载主界面失败", e.getMessage());
}
}
/**
* 打开设置窗口
*/
public static void openSettingView() {
try {
Parent root = FXMLLoader.load(Objects.requireNonNull(Main.class.getResource("/fxml/setting-view.fxml")));
Stage settingStage = new Stage();
settingStage.setTitle("数字化验收工具 - 设置");
Scene scene = new Scene(root, 300, 206);
settingStage.setScene(scene); // 默认大小可调
settingStage.initOwner(primaryStage); // 设置父窗口
settingStage.initModality(Modality.WINDOW_MODAL);
settingStage.setResizable(false);
settingStage.centerOnScreen();
settingStage.show();
openStages.add(settingStage);
} catch (IOException e) {
log.error(LoggerHelper.TRACE_MARKER, "Failed to open setting view: {}", e.getMessage(), e);
DialogUtil.showErrorDialog("错误", "加载设置窗口失败", "无法加载设置窗口: " + e.getMessage());
}
}
/**
@ -100,9 +148,8 @@ public class SceneManager {
event.consume();
}
});
} catch (IOException e) {
log.error("Failed to load view: {}", fxmlPath, e);
log.error(LoggerHelper.TRACE_MARKER, "Failed to load view: {}", fxmlPath, e);
DialogUtil.showErrorDialog("错误", "加载视图失败", "无法加载视图: " + e.getMessage());
}
}

View File

@ -0,0 +1,126 @@
package top.r3944realms.docchecktoolrefactored.ui;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Spinner;
import javafx.scene.input.MouseDragEvent;
import javafx.stage.Stage;
import top.r3944realms.docchecktoolrefactored.System;
import top.r3944realms.docchecktoolrefactored.core.Setting;
import top.r3944realms.docchecktoolrefactored.ui.extend.LongSpinnerValueFactory;
import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil;
import java.net.URL;
import java.util.ResourceBundle;
public class SettingDialogController implements Initializable {
@FXML private CheckBox enableStepCB;
@FXML private Button resetB, saveB, cancelB;
@FXML private Spinner<Long> scanTotalTimeOutS, scanSingleTimeOutS;
private Setting setting;
// Spinner 范围常量
private static final int SINGLE_MIN = 1;
private static final int SINGLE_MAX = 3600;
private static final int TOTAL_MIN = 1;
private static final int TOTAL_MAX = 86400;
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
// 获取当前配置
setting = System.getSetting();
// 初始化 Spinner
scanSingleTimeOutS.setValueFactory(new LongSpinnerValueFactory(1, 3600, setting.getSingleTimeout()));
scanSingleTimeOutS.setEditable(true);
scanTotalTimeOutS.setValueFactory(new LongSpinnerValueFactory(1, 3600 * 24, setting.getTotalTimeout()));
scanTotalTimeOutS.setEditable(true);
// 添加焦点离开时校验
addSpinnerValidation(scanSingleTimeOutS, SINGLE_MIN, SINGLE_MAX);
addSpinnerValidation(scanTotalTimeOutS, TOTAL_MIN, TOTAL_MAX);
enableStepCB.setSelected(setting.isEnableStep());
}
/** 保存修改 */
@FXML
void onSave(ActionEvent actionEvent) {
// 更新配置对象
setting.setSingleTimeout(scanSingleTimeOutS.getValue());
setting.setTotalTimeout(scanTotalTimeOutS.getValue());
setting.setEnableStep(enableStepCB.isSelected());
// 保存到配置文件
System.saveSettingsNow();
// 通知主界面刷新按钮状态
MainStageController mainController = SceneManager.getMainController();
if(mainController != null){
mainController.updateStepButtonsVisibility();
}
closeDialog();
}
/** 重置为默认值 */
@FXML
void onReset(ActionEvent actionEvent) {
scanSingleTimeOutS.getValueFactory().setValue(30L); // 默认单次超时
scanTotalTimeOutS.getValueFactory().setValue(300L); // 默认总超时
enableStepCB.setSelected(false);
}
/** 取消修改 */
@FXML
void onCancel(ActionEvent actionEvent) {
closeDialog();
}
@FXML
void onCheckOne(MouseDragEvent mouseDragEvent) {
validateSpinnerValue(scanSingleTimeOutS, 30, 3600);
}
@FXML
void onCheckTwo(MouseDragEvent mouseDragEvent) {
validateSpinnerValue(scanTotalTimeOutS, 60, 3600 * 24);
}
@FXML
void onSettingThree(ActionEvent actionEvent) {
}
/** 给 Spinner 添加离开焦点校验 */
private void addSpinnerValidation(Spinner<Long> spinner, long min, long max) {
spinner.getEditor().focusedProperty().addListener((obs, oldVal, newVal) -> {
if (!newVal) { // 焦点离开时触发
validateSpinnerValue(spinner, min, max);
}
});
}
/** 校验 Spinner 的值是否在范围内,如果超出则纠正并提示 */
private void validateSpinnerValue(Spinner<Long> spinner, long min, long max) {
try {
long value = Integer.parseInt(spinner.getEditor().getText());
if (value < min || value > max) {
// 超出范围则纠正
long corrected = Math.max(min, Math.min(value, max));
spinner.getValueFactory().setValue(corrected);
// 弹出提示
DialogUtil.showWarningDialog("输入警告", "输入的值超出范围,已自动调整为 " + corrected);
}
} catch (NumberFormatException e) {
// 非数字输入恢复原来的值
spinner.getEditor().setText(spinner.getValue().toString());
DialogUtil.showWarningDialog("输入警告","输入非法,已恢复为原值 " + spinner.getValue());
}
}
/** 关闭对话框 */
private void closeDialog() {
Stage stage = (Stage) cancelB.getScene().getWindow();
stage.close();
}
}

View File

@ -0,0 +1,48 @@
package top.r3944realms.docchecktoolrefactored.ui.extend;
import javafx.scene.control.SpinnerValueFactory;
import lombok.Setter;
@Setter
public class LongSpinnerValueFactory extends SpinnerValueFactory<Long> {
public LongSpinnerValueFactory(long min, long max, long initialValue) {
setMin(min);
setMax(max);
setValue(initialValue);
setConverter(new javafx.util.StringConverter<>() {
@Override
public String toString(Long object) {
return object.toString();
}
@Override
public Long fromString(String string) {
try {
return Long.parseLong(string);
} catch (NumberFormatException e) {
return getValue();
}
}
});
}
private long min;
private long max;
@Override
public void decrement(int steps) {
long newValue = getValue() - steps;
if (newValue < min) newValue = min;
setValue(newValue);
}
@Override
public void increment(int steps) {
long newValue = getValue() + steps;
if (newValue > max) newValue = max;
setValue(newValue);
}
}

View File

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

View File

@ -9,10 +9,12 @@ import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.stage.FileChooser;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.System;
import top.r3944realms.docchecktoolrefactored.core.AddressFileComparator;
import top.r3944realms.docchecktoolrefactored.core.AddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.core.PhysicalAddressFileGenerator;
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
import java.io.File;
import java.net.URL;
@ -49,8 +51,8 @@ public class PathCheckPaneController implements Initializable {
* @param actionEvent the action event
*/
@FXML void onSelectLC(ActionEvent actionEvent) {
log.info("用户点击了选择目录文件按钮");
FileChooser fileChooser = new FileChooser();
log.info(LoggerHelper.DEBUG_MARKER, "用户点击了选择目录文件按钮");
FileChooser fileChooser = System.getFileChooser();
fileChooser.setTitle("选择目录文件");
// 设置文件过滤器只允许DBFXMLxlsxxls格式
@ -60,7 +62,7 @@ public class PathCheckPaneController implements Initializable {
FileChooser.ExtensionFilter dbfFilter = new FileChooser.ExtensionFilter("DBF Files (*.dbf)", "*.dbf");
FileChooser.ExtensionFilter xmlFilter = new FileChooser.ExtensionFilter("XML Files (*.xml)", "*.xml");
fileChooser.getExtensionFilters().addAll( xlsxFilter, xlsFilter,dbfFilter, xmlFilter);
fileChooser.getExtensionFilters().addAll(xlsxFilter, xlsFilter,dbfFilter, xmlFilter);
// 显示文件选择对话框
File selectedFile = fileChooser.showOpenDialog(selectLoadCatalog2B.getScene().getWindow());
@ -68,9 +70,10 @@ public class PathCheckPaneController implements Initializable {
// 如果选择了文件则将文件路径显示在loadCatalog2TF上
if (selectedFile != null) {
loadCatalog2TF.setText(selectedFile.getAbsolutePath());
log.info("选择的目录文件路径为:{}", selectedFile.getAbsolutePath());
System.setLastModifiedFile(selectedFile);
log.info(LoggerHelper.DEBUG_MARKER, "选择的目录文件路径为:{}", selectedFile.getAbsolutePath());
}else{
log.warn("用户未选择任何文件夹");
log.warn(LoggerHelper.DEBUG_MARKER, "用户未选择任何文件夹");
result2TA.setText("未选择任何文件夹,请重新选择。");
}
}
@ -81,7 +84,7 @@ public class PathCheckPaneController implements Initializable {
* @param actionEvent the action event
*/
@FXML void onSelectJPGF(ActionEvent actionEvent) {
javafx.stage.DirectoryChooser directoryChooser = new javafx.stage.DirectoryChooser();
javafx.stage.DirectoryChooser directoryChooser = System.getDirectoryChooser();
// 正确获取当前选中的值
Mode selectedMode = loadFolderType2CB.getValue();
if (selectedMode == Mode.PAGE_TYPE) {
@ -89,14 +92,15 @@ public class PathCheckPaneController implements Initializable {
} else if (selectedMode == Mode.FILE_TYPE) {
directoryChooser.setTitle("选择文件级文件夹");
}
log.info("用户选择的模式为:{}", selectedMode);
log.info(LoggerHelper.DEBUG_MARKER, "用户选择的模式为:{}", selectedMode);
File selectedDirectory = directoryChooser.showDialog(selectJPGFolder2B.getScene().getWindow());
if (selectedDirectory != null) {
loadJPGFolder2TF.setText(selectedDirectory.getAbsolutePath());
log.info("选择的{}文件夹路径为:{}", selectedMode,selectedDirectory.getAbsolutePath());
log.info(LoggerHelper.DEBUG_MARKER, "选择的{}文件夹路径为:{}", selectedMode,selectedDirectory.getAbsolutePath());
}
System.setLastModifiedFile(selectedDirectory);
}
/**
@ -105,14 +109,14 @@ public class PathCheckPaneController implements Initializable {
* @param actionEvent the action event
*/
@FXML void onGenerateLA(ActionEvent actionEvent) {
log.info("用户点击了生成逻辑地址文件按钮");
log.info(LoggerHelper.DEBUG_MARKER, "用户点击了生成逻辑地址文件按钮");
String filePath = loadCatalog2TF.getText();
if (filePath.isEmpty()) {
result2TA.setText("请先选择目录文件。");
return;
}
FileChooser fileChooser = new FileChooser();
FileChooser fileChooser = System.getFileChooser();
fileChooser.setTitle("选择保存逻辑地址文件的位置");
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv"));
fileChooser.setInitialFileName("逻辑地址文件.csv");
@ -120,10 +124,10 @@ public class PathCheckPaneController implements Initializable {
File outputFile = fileChooser.showSaveDialog(generateLogicalAddress2B.getScene().getWindow());
if (outputFile == null) {
result2TA.setText("未选择保存位置");
log.warn("用户未选择任何文件");
log.warn(LoggerHelper.DEBUG_MARKER, "用户未选择任何文件");
return;
}
System.setLastModifiedFile(outputFile);
// 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加
final File finalOutputFile;
if (!outputFile.getName().toLowerCase().endsWith(".csv")) {
@ -134,7 +138,7 @@ public class PathCheckPaneController implements Initializable {
// 保存生成的文件路径
logicalAddressFilePath = finalOutputFile.getAbsolutePath();
log.info("选择的输出文件路径: {}", logicalAddressFilePath);
log.info(LoggerHelper.DEBUG_MARKER, "选择的输出文件路径: {}", logicalAddressFilePath);
// 创建后台任务来处理文件生成
Thread backgroundThread = new Thread(() -> {
// 获取当前选择的文件夹类型
@ -180,7 +184,7 @@ public class PathCheckPaneController implements Initializable {
return;
}
FileChooser fileChooser = new FileChooser();
FileChooser fileChooser = System.getFileChooser();
fileChooser.setTitle("选择保存物理地址文件的位置");
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv"));
fileChooser.setInitialFileName("物理地址文件.csv");
@ -191,6 +195,7 @@ public class PathCheckPaneController implements Initializable {
result2TA.setText("未选择保存位置");
return;
}
System.setLastModifiedFile(outputFile);
// 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加
final File finalOutputFile;
@ -240,7 +245,7 @@ public class PathCheckPaneController implements Initializable {
*/
@FXML
void onStart(ActionEvent actionEvent) {
log.info("用户点击了开始对比按钮");
log.info(LoggerHelper.DEBUG_MARKER, "用户点击了开始对比按钮");
// 检查是否已生成两个文件
if (logicalAddressFilePath == null || physicalAddressFilePath == null) {
@ -248,8 +253,8 @@ public class PathCheckPaneController implements Initializable {
return;
}
log.info("逻辑地址文件路径为:{}", logicalAddressFilePath);
log.info("物理地址文件路径为:{}", physicalAddressFilePath);
log.info(LoggerHelper.DEBUG_MARKER, "逻辑地址文件路径为:{}", logicalAddressFilePath);
log.info(LoggerHelper.DEBUG_MARKER, "物理地址文件路径为:{}", physicalAddressFilePath);
// 使用新创建的核心类进行文件比较
AddressFileComparator comparator = new AddressFileComparator();
@ -323,13 +328,23 @@ public class PathCheckPaneController implements Initializable {
*/
enum Mode {
/**
* Jpg mode.
* Jpg mode. 文件以JPG
*/
PAGE_TYPE("jpg"),
PAGE_TYPE("jpg") {
@Override
public String toString() {
return "页面级";
}
},
/**
* Pdf mode.
* Pdf mode. 文件以PDF
*/
FILE_TYPE("pdf");
FILE_TYPE("pdf") {
@Override
public String toString() {
return "文件级";
}
};
/**
* The Id.
*/

View File

@ -2,7 +2,9 @@ package top.r3944realms.docchecktoolrefactored.ui.module;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Dialog;
import javafx.scene.control.TextField;
import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil;
/**
* The type Project info pane controller.
@ -15,12 +17,13 @@ public class ProjectInfoPaneController {
@FXML private TextField fileYearTF;
@FXML
void onReset(ActionEvent event) {
// 清空所有文本字段
projectNameTF.clear();
fileYearTF.clear();
fileCategoriesTF.clear();
totalCatalogNumberTF.clear();
AcceptanceTimeTF.clear();
if (DialogUtil.showConfirmationDialog("确认","是否清除(该操作不可逆)", "")){// 清空所有文本字段
projectNameTF.clear();
fileYearTF.clear();
fileCategoriesTF.clear();
totalCatalogNumberTF.clear();
AcceptanceTimeTF.clear();
}
}
}

View File

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

View File

@ -2,28 +2,23 @@ package top.r3944realms.docchecktoolrefactored.ui.task;
import javafx.concurrent.Task;
import lombok.extern.slf4j.Slf4j;
import top.r3944realms.docchecktoolrefactored.System;
import top.r3944realms.docchecktoolrefactored.core.DuplicateFinder;
import top.r3944realms.docchecktoolrefactored.core.MD5HashCalculator;
import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
import top.r3944realms.docchecktoolrefactored.core.ScanningException;
import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
import top.r3944realms.docchecktoolrefactored.model.DuplicateGroup;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@Slf4j
public class DuplicateDocumentDetectionTask extends Task<String> {
public class DuplicateDocumentDetectionTask extends Task<String>{
private final String folderPath;
private final MD5HashCalculator hashCalculator;
private volatile RobustParallelScanner scanner;
@ -33,137 +28,166 @@ public class DuplicateDocumentDetectionTask extends Task<String> {
this.hashCalculator = new MD5HashCalculator();
}
@Override
protected String call() throws Exception {
updateMessage("正在初始化扫描...");
Path rootPath = Paths.get(folderPath);
if (!Files.exists(rootPath) || !Files.isDirectory(rootPath)) {
throw new IllegalArgumentException("指定路径不是有效目录: " + folderPath);
}
// 使用 RobustParallelScanner MD5HashCalculator 进行并行扫描和哈希计算
Map<String, List<Path>> hashToFileMap = new ConcurrentHashMap<>();
AtomicInteger processed = new AtomicInteger(0);
AtomicReference<Exception> errorRef = new AtomicReference<>(null);
AtomicBoolean scanCompleted = new AtomicBoolean(false);
// 使用 CountDownLatch 等待扫描完成
CountDownLatch latch = new CountDownLatch(1);
// 创建扫描器
// 创建带进度更新的扫描器
scanner = new RobustParallelScanner(10);
// 异步启动扫描任务
Thread scanThread = new Thread(() -> {
// 创建带有进度监听的 DuplicateFinder
DuplicateFinder duplicateFinder = new DuplicateFinder(scanner, hashCalculator, true)
.applySetting(System.getSetting());
// 用于统计文件总数
AtomicInteger totalFiles = new AtomicInteger(0);
// 使用 RobustParallelScanner MD5HashCalculator 进行并行扫描和哈希计算
// 设置进度回调
duplicateFinder.setProgressCallback(new DuplicateFinder.ProgressCallback() {
@Override
public void onPhaseStarted(DuplicateFinder.Phase phase) {
switch (phase) {
case GROUP_BY_SIZE:
updateMessage("正在按文件大小分组...");
break;
case CALCULATE_HASH:
updateMessage("正在计算可能重复文件的哈希值...");
break;
}
}
@Override
public void onPhaseProgress(DuplicateFinder.Phase phase, int current, int total) {
if (total > 0) {
updateProgress(current, total);
switch (phase) {
case GROUP_BY_SIZE:
totalFiles.set(total);
updateMessage(String.format("正在按文件大小分组: %d/%d", current, total));
break;
case CALCULATE_HASH:
updateMessage(String.format("正在计算哈希值: %d/%d", current, total));
break;
}
}
}
@Override
public void onPhaseCompleted(DuplicateFinder.Phase phase) {
switch (phase) {
case GROUP_BY_SIZE:
updateMessage("文件大小分组完成");
break;
case CALCULATE_HASH:
updateMessage("哈希值计算完成");
break;
}
}
});
AtomicReference<List<DuplicateGroup>> resultRef = new AtomicReference<>();
List<Exception> errors = new CopyOnWriteArrayList<>();
CountDownLatch latch = new CountDownLatch(1);
// 在单独线程中执行查找
Thread findThread = new Thread(() -> {
try {
scanner.scanWithProgress(rootPath, new FileScanner.ProgressAwareListener() {
@Override
public void onFileFound(Path file) {
if (isCancelled()) {
scanner.cancel();
return;
}
try {
String hash = hashCalculator.calculatePartialHash(file);
hashToFileMap.computeIfAbsent(hash, k -> new ArrayList<>()).add(file);
} catch (IOException e) {
// 记录无法计算哈希的文件但不中断整个过程
updateMessage("警告: 无法处理文件 " + file.toString() + " - " + e.getMessage());
}
processed.incrementAndGet();
}
@Override
public void onError(Path path, Exception e) {
// 记录错误但不中断扫描过程
errorRef.set(e);
updateMessage("扫描错误: " + path.toString() + " - " + e.getMessage());
}
@Override
public void onScanComplete() {
// 扫描完成
scanCompleted.set(true);
latch.countDown();
}
@Override
public void onProgressUpdate(int current, int total) {
if (isCancelled()) {
scanner.cancel();
return;
}
updateProgress(current, total);
updateMessage("正在处理文件: " + current + "/" + total);
if (current % 500 == 0 || current == total) { // 每500个文件或完成时记录一次日志
log.info("处理进度: {}/{}", current, total);
}
}
});
List<DuplicateGroup> duplicates = duplicateFinder.findDuplicates(rootPath);
resultRef.set(duplicates);
} catch (Exception e) {
errorRef.set(e);
errors.add(e);
} finally {
latch.countDown();
}
});
scanThread.setDaemon(true);
scanThread.start();
findThread.setDaemon(true);
findThread.start();
// 等待扫描完成设置更长的超时时间例如5分钟
if (!latch.await(5*60, TimeUnit.MINUTES)) {
// 等待扫描完成设置超时时间例如5分钟
long totalTimeout = System.getSetting().getTotalTimeout();
if (!latch.await(totalTimeout, TimeUnit.SECONDS)) {
scanner.cancel();
throw new TimeoutException("扫描超时5分钟");
throw new TimeoutException(String.format("扫描超时(%d秒", totalTimeout));
}
// 检查是否被取消
if (isCancelled()) {
long start = java.lang.System.currentTimeMillis();
try {
boolean finished = false;
while (!finished) {
// 200ms 等待一次 latch避免忙等待
finished = latch.await(200, TimeUnit.MILLISECONDS);
// 检查是否被取消
if (isCancelled()) {
scanner.cancel();
return "操作已被取消";
}
// 检查是否超时
if (java.lang.System.currentTimeMillis() - start > totalTimeout * 1000L) {
scanner.cancel();
throw new TimeoutException(String.format("扫描超时(%d秒", totalTimeout));
}
}
} catch (InterruptedException e) {
scanner.cancel();
return "操作已被取消";
Thread.currentThread().interrupt();
return "操作被中断";
}
// 如果有错误且扫描未完成抛出异常
if (errorRef.get() != null && !scanCompleted.get()) {
throw errorRef.get();
// 检查是否有错误
if (!errors.isEmpty()) {
throw new ScanningException(errors);
}
// 分析重复文件并构建结果
updateMessage("正在分析重复文件...");
List<DuplicateGroup> duplicateGroups = resultRef.get();
// 构建最终结果
StringBuilder result = new StringBuilder();
result.append("重复文件检测结果:\n");
if (errorRef.get() != null) {
result.append("警告: 扫描过程中发生错误 - ").append(errorRef.get().getMessage()).append("\n\n");
}
result.append("总共处理 ").append(processed.get()).append(" 个文件\n");
// 计算总文件数所有组中的文件数
int totalDuplicateFiles = duplicateGroups.stream()
.mapToInt(group -> group.fileMetas().size())
.sum();
List<Map.Entry<String, List<Path>>> duplicateGroups = hashToFileMap.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.collect(Collectors.toList());
int totalGroups = duplicateGroups.size();
result.append(String.format("总共扫描文件数: %d\n", totalFiles.get()));
result.append(String.format("发现重复文件组数: %d\n", totalGroups));
result.append(String.format("重复文件总数: %d\n", totalDuplicateFiles));
if (!duplicateGroups.isEmpty()) {
result.append("").append(duplicateGroups.size()).append(" 组重复文件\n\n");
result.append("\n详细重复文件信息:\n");
result.append("----------------------------------------\n");
int groupIndex = 1;
for (Map.Entry<String, List<Path>> entry : duplicateGroups) {
result.append("").append(groupIndex).append("\t");
result.append("哈希值: ").append(entry.getKey()).append("\n");
for (DuplicateGroup group : duplicateGroups) {
result.append(String.format("第 %d 组 (哈希值: %s, 大小: %d 字节)\n",
groupIndex, group.hash(), group.size()));
int fileIndex = 1;
for (Path file : entry.getValue()) {
result.append("文件名").append(fileIndex).append(": ").append(file.getFileName()).append("\t\t");
result.append("文件路径").append(fileIndex).append(": ").append(file.toAbsolutePath()).append("\n");
for (var file : group.fileMetas()) {
Path filePath = file.getPath();
result.append(String.format(" 文件%d: %s\n", fileIndex, filePath.toAbsolutePath()));
fileIndex++;
}
result.append("\n");
groupIndex++;
}
} else {
result.append("没有重复文件\n");
result.append("\n没有发现重复文件\n");
}
updateMessage("检测完成!");
result.append("检测完成!\n");
return result.toString();
}

View File

@ -2,6 +2,7 @@ package top.r3944realms.docchecktoolrefactored.ui.utils;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.stage.Window;
@ -18,14 +19,7 @@ public class DialogUtil {
* @return the boolean
*/
public static boolean showExitConfirmation(Window owner) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.initOwner(owner);
alert.setTitle("确认退出");
alert.setHeaderText("您确定要退出程序吗?");
alert.setContentText("请确认您的操作");
Optional<ButtonType> result = alert.showAndWait();
return result.isPresent() && result.get() == ButtonType.OK;
return showConfirmationDialog("确认退出", "您确定要退出程序吗?", "请确认您的操作");
}
/**
@ -41,8 +35,11 @@ public class DialogUtil {
alert.setTitle(title);
alert.setHeaderText(header);
alert.setContentText(content);
ButtonType yesButton = new ButtonType("Yes", ButtonBar.ButtonData.YES);
ButtonType noButton = new ButtonType("No", ButtonBar.ButtonData.NO);
alert.getButtonTypes().setAll(yesButton, noButton);
Optional<ButtonType> result = alert.showAndWait();
return result.isPresent() && result.get() == ButtonType.OK;
return result.isPresent() && result.get() == yesButton;
}
/**

View File

@ -0,0 +1,150 @@
package top.r3944realms.docchecktoolrefactored.ui.utils;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 进度条窗口工具类支持取消按钮
*/
public class ProgressBarUtil {
private Stage progressStage;
private ProgressBar progressBar;
private Label messageLabel;
private Button cancelButton;
private final AtomicBoolean cancelled = new AtomicBoolean(false);
private Runnable onCancelCallback;
/**
* 显示进度条窗口
* @param ownerStage 父窗口
* @param title 窗口标题
* @param initialMessage 初始消息
*/
public void showProgress(Stage ownerStage, String title, String initialMessage) {
Platform.runLater(() -> {
progressStage = new Stage();
progressStage.initOwner(ownerStage);
progressStage.initStyle(StageStyle.UTILITY);
progressStage.initModality(Modality.APPLICATION_MODAL);
progressStage.setTitle(title);
progressStage.setResizable(false);
// 创建进度条
progressBar = new ProgressBar();
progressBar.setPrefWidth(300);
progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
// 创建消息标签
messageLabel = new Label(initialMessage);
// 创建取消按钮
cancelButton = new Button("取消");
cancelButton.setOnAction(e -> {
cancelled.set(true);
if (onCancelCallback != null) {
onCancelCallback.run();
}
closeProgress();
});
// 如果 setOnCancel 先调用过绑定回调
if (onCancelCallback != null) {
cancelButton.setOnAction(e -> {
cancelled.set(true);
onCancelCallback.run();
closeProgress();
});
}
// 布局
VBox root = new VBox(10, messageLabel, progressBar, cancelButton);
root.setStyle("-fx-padding: 20; -fx-alignment: center;");
Scene scene = new Scene(root);
progressStage.setScene(scene);
progressStage.sizeToScene();
progressStage.show();
});
}
/**
* 更新进度和消息
* @param progress 进度值 (0.0 - 1.0)
* @param message 要显示的消息
*/
public void updateProgress(double progress, String message) {
Platform.runLater(() -> {
if (progressBar != null) {
progressBar.setProgress(progress);
}
if (messageLabel != null) {
messageLabel.setText(message);
}
});
}
/**
* 关闭进度条窗口
*/
public void closeProgress() {
Platform.runLater(() -> {
if (progressStage != null) {
progressStage.close();
}
});
}
/**
* 是否已取消
*/
public boolean isCancelled() {
return cancelled.get();
}
/**
* 快速显示一个进度条窗口并执行任务
* @param ownerStage 父窗口
* @param title 窗口标题
* @param initialMessage 初始消息
* @param task 要执行的任务可检查 isCancelled() 中途退出
*/
public static void showAndExecute(Stage ownerStage, String title, String initialMessage, CancellableTask task) {
ProgressBarUtil progressBarUtil = new ProgressBarUtil();
progressBarUtil.showProgress(ownerStage, title, initialMessage);
new Thread(() -> {
try {
task.run(progressBarUtil);
} finally {
progressBarUtil.closeProgress();
}
}).start();
}
@FunctionalInterface
public interface CancellableTask {
void run(ProgressBarUtil util);
}
public void setOnCancel(Runnable onCancel) {
if (cancelButton != null) {
cancelButton.setOnAction(e -> {
cancelled.set(true);
if (onCancel != null) {
onCancel.run();
}
closeProgress();
});
} else {
// 保存回调稍后在按钮创建后绑定
this.onCancelCallback = onCancel;
}
}
}

View File

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

View File

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

View File

@ -8,7 +8,7 @@
/* 默认Tab形状容器 */
.tab .tab-container {
-fx-background-color: #13b72b;
-fx-background-color: #7BB0D9;
-fx-shape: "M0,5 L65,5 L75,15 L65,25 L0,25 L10,15 Z";
-fx-background-insets: 0;
-fx-border-width: 0; /* 确保无边框 */
@ -37,7 +37,7 @@
}
.tab:selected .tab-container {
-fx-background-color: #9b0e0e; /* 只改变背景色 */
-fx-background-color: #0063b0; /* 只改变背景色 */
-fx-effect: null; /* 移除所有效果 */
}

View File

@ -17,7 +17,7 @@
</padding>
<top>
<Label alignment="CENTER" text="淮阴区数字化档案检查验收系统" textAlignment="CENTER" textFill="rgb(66,133,244)">
<Label alignment="CENTER" text="数字化验收工具" textAlignment="CENTER" textFill="rgb(66,133,244)" BorderPane.alignment="TOP_CENTER">
<font>
<Font name="Microsoft YaHei" size="24.0" />
</font>

View File

@ -1,33 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.SeparatorMenuItem?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="1000.0" prefWidth="1200.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.MainStageController">
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="1000.0" prefWidth="1200.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.MainStageController">
<children>
<MenuBar prefWidth="2558.0" VBox.vgrow="ALWAYS">
<menus>
<Menu mnemonicParsing="false" text="文件">
<items>
<MenuItem mnemonicParsing="false" text="关闭" />
</items>
</Menu>
<Menu mnemonicParsing="false" text="编辑">
<items>
<MenuItem mnemonicParsing="false" text="未完成" />
<MenuItem fx:id="settingMI" mnemonicParsing="false" onAction="#onOpenSetting" text="设置" />
<SeparatorMenuItem mnemonicParsing="false" />
<MenuItem fx:id="logoutMI" mnemonicParsing="false" onAction="#onLogout" text="登出" />
<SeparatorMenuItem mnemonicParsing="false" />
<MenuItem fx:id="exitMI" mnemonicParsing="false" onAction="#onExit" text="退出" />
</items>
</Menu>
<Menu fx:id="helpM" mnemonicParsing="false" text="帮助">
<items>
<MenuItem mnemonicParsing="false" text="关于" />
<MenuItem fx:id="helpDocMI" mnemonicParsing="false" onAction="#onOpenHelpDoc" text="帮助文档" />
<SeparatorMenuItem mnemonicParsing="false" />
<MenuItem fx:id="aboutSoftwareMI" mnemonicParsing="false" onAction="#onAbout" text="关于软件" />
</items>
</Menu>
</menus>
</MenuBar>
<!-- 导入项目信息面板 -->
<fx:include source="module/project-info-pane.fxml" VBox.vgrow="ALWAYS"/>
<fx:include source="module/project-info-pane.fxml" VBox.vgrow="ALWAYS" />
<!-- 导入项目内容面板 -->
<TabPane stylesheets="@../css/custom-tab.css" tabClosingPolicy="UNAVAILABLE" VBox.vgrow="ALWAYS">
<TabPane fx:id="tabPane" onKeyPressed="#handleKeyPressed" stylesheets="@../css/custom-tab.css" tabClosingPolicy="UNAVAILABLE" VBox.vgrow="ALWAYS">
<tabs>
<Tab id="startTab" fx:id="step1T" text="1. 查重复文件">
<content>
@ -66,5 +76,34 @@
</Tab>
</tabs>
</TabPane>
<HBox nodeOrientation="RIGHT_TO_LEFT" prefHeight="100.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
<children>
<Button fx:id="nextB" mnemonicParsing="false" onAction="#onNext" prefHeight="100.0" prefWidth="500.0" text="下一步">
<HBox.margin>
<Insets bottom="20.0" left="40.0" right="40.0" top="20.0" />
</HBox.margin>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
<font>
<Font name="System Bold" size="19.0" />
</font>
</Button>
<Button fx:id="prevB" mnemonicParsing="false" onAction="#onPrev" prefHeight="100.0" prefWidth="500.0" text="上一步">
<HBox.margin>
<Insets bottom="20.0" left="40.0" right="40.0" top="20.0" />
</HBox.margin>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
<font>
<Font name="System Bold" size="19.0" />
</font>
</Button>
</children>
<VBox.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</VBox.margin>
</HBox>
</children>
</VBox>

View File

@ -10,7 +10,7 @@
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.DuplicateDocumentPaneController">
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.DuplicateDocumentPaneController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="288.0" minWidth="0.0" percentWidth="0.0" prefWidth="82.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="1263.9999633789064" minWidth="0.0" prefWidth="745.0" />
@ -25,7 +25,7 @@
<RowConstraints maxHeight="592.6666666666666" prefHeight="581.3333536783855" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<TextArea fx:id="result1B" editable="false" GridPane.columnSpan="3" GridPane.rowIndex="3">
<TextArea fx:id="result1B" 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>
@ -51,7 +51,7 @@
<Insets bottom="2.0" left="10.0" right="2.0" top="2.0" />
</GridPane.margin>
</Button>
<Label text="反馈结果" GridPane.rowIndex="2">
<Label text="结果反馈:" GridPane.rowIndex="2">
<GridPane.margin>
<Insets left="10.0" />
</GridPane.margin></Label>
@ -71,6 +71,11 @@
<Insets left="10.0" />
</GridPane.margin>
</Label>
<Button fx:id="cancel1B" alignment="CENTER" mnemonicParsing="false" onAction="#onCancel" prefHeight="52.0" prefWidth="117.0" text="取消检查" GridPane.columnIndex="3" GridPane.rowIndex="1">
<GridPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</GridPane.margin>
</Button>
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />

View File

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

View File

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

View File

@ -5,12 +5,12 @@
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17">
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
<children>
<TextArea editable="false" prefHeight="200.0" prefWidth="200.0" text="工作内容:&#10; ①检查档案管理系统或电子目录的挂接准确率要求100%&#10; ②逐件验证数字化成果与目录的关联性&#10; ③结果填入《挂接检查登记表》附件5。" wrapText="true" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="10.0">
<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">
<font>
<Font size="18.0" />
</font></TextArea>

View File

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

View File

@ -10,7 +10,7 @@
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.StorageCarrierPaneController">
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.StorageCarrierPaneController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="226.33331298828125" minWidth="10.0" percentWidth="10.0" prefWidth="108.33333333333334" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="559.3333511352539" minWidth="10.0" percentWidth="40.0" prefWidth="373.00002034505206" />
@ -18,11 +18,10 @@
<ColumnConstraints hgrow="SOMETIMES" maxWidth="500.0" minWidth="10.0" percentWidth="25.0" prefWidth="400.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints maxHeight="151.33334350585938" percentHeight="7.0" prefHeight="55.00001017252603" vgrow="SOMETIMES" />
<RowConstraints maxHeight="407.0" percentHeight="7.0" prefHeight="73.33333841959634" vgrow="SOMETIMES" />
<RowConstraints maxHeight="508.33333333333326" percentHeight="7.0" prefHeight="73.99999491373697" vgrow="SOMETIMES" />
<RowConstraints maxHeight="591.6666666666667" percentHeight="7.0" prefHeight="43.33332316080731" vgrow="SOMETIMES" />
<RowConstraints maxHeight="602.0000152587891" minHeight="10.0" prefHeight="534.6666615804037" vgrow="SOMETIMES" />
<RowConstraints maxHeight="151.33334350585938" percentHeight="7.0" prefHeight="55.00001017252603" vgrow="NEVER" />
<RowConstraints maxHeight="508.33333333333326" percentHeight="7.0" prefHeight="73.99999491373697" vgrow="NEVER" />
<RowConstraints maxHeight="592.6666666666666" percentHeight="7.0" prefHeight="581.3333536783855" vgrow="NEVER" />
<RowConstraints maxHeight="1.7976931348623157E308" prefHeight="581.3333536783855" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Label text="载入数字化成果:">
@ -33,7 +32,7 @@
<Insets left="5.0" />
</GridPane.margin>
</Label>
<Label text="将档案目录、哈希值列表文件和检测过程文件打包制成打包制成“数字化验收检测包.rar”压缩包" GridPane.columnSpan="4" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
<Label text="将档案目录、哈希值列表文件和检测过程文件打包制成打包制成“数字化验收检测包.rar”压缩包" GridPane.columnSpan="5" GridPane.halignment="CENTER" GridPane.rowSpan="2" GridPane.valignment="CENTER">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
@ -41,7 +40,7 @@
<Font name="System Bold" size="14.0" />
</font>
</Label>
<Label text="反馈结果:" GridPane.rowIndex="3">
<Label text="结果反馈:" GridPane.rowIndex="2">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
@ -49,7 +48,7 @@
<Insets left="5.0" />
</GridPane.margin>
</Label>
<Label text="载入压缩包:" GridPane.rowIndex="2">
<Label text="载入压缩包:" GridPane.rowIndex="1">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
@ -65,7 +64,7 @@
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
</TextField>
<TextField fx:id="loadCompressedFile" GridPane.columnIndex="1" GridPane.rowIndex="2">
<TextField fx:id="loadCompressedFile" GridPane.columnIndex="1" GridPane.rowIndex="1">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
@ -73,7 +72,7 @@
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
</TextField>
<TextArea fx:id="result7TA" editable="false" GridPane.columnSpan="3" GridPane.hgrow="ALWAYS" GridPane.rowIndex="4">
<TextArea fx:id="result7TA" editable="false" prefWidth="400.0" GridPane.columnSpan="3" GridPane.rowIndex="3">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
@ -103,7 +102,7 @@
<Font size="14.0" />
</font>
</Button>
<Button fx:id="selectLoadCompressedFile7B" mnemonicParsing="false" onAction="#onSelectLC" text="选择文件" GridPane.columnIndex="2" GridPane.halignment="CENTER" GridPane.rowIndex="2" GridPane.valignment="CENTER">
<Button fx:id="selectLoadCompressedFile7B" mnemonicParsing="false" onAction="#onSelectLC" text="选择文件" GridPane.columnIndex="2" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
@ -114,7 +113,7 @@
<Font size="14.0" />
</font>
</Button>
<Button fx:id="caculateHash7B" mnemonicParsing="false" onAction="#onCaculateHash" text="计算哈希值" GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.rowIndex="2" GridPane.valignment="CENTER">
<Button fx:id="caculateHash7B" mnemonicParsing="false" onAction="#onCaculateHash" text="计算哈希值" GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
@ -125,7 +124,7 @@
<Font size="14.0" />
</font>
</Button>
<TextArea editable="false" prefWidth="400.0" text="①对照《存储载体检查登记表》附件7检查并记录存储载体的类型/数量/内容/可读性情况。&#10;②将档案目录、哈希值列表文件和检测过程文件打包制成打包制成“数字化验收检测包.rar”压缩包&#10;③计算并验证压缩包的MD5或哈希值" wrapText="true" GridPane.columnIndex="3" GridPane.rowIndex="4">
<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">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
@ -136,7 +135,7 @@
<Font size="14.0" />
</font>
</TextArea>
<Label text="工作内容:" GridPane.columnIndex="3" GridPane.rowIndex="3">
<Label text="工作内容:" GridPane.columnIndex="3" GridPane.rowIndex="2">
<GridPane.margin>
<Insets left="10.0" />
</GridPane.margin>

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Spinner?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.SettingDialogController">
<children>
<GridPane>
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="138.0" minWidth="10.0" prefWidth="133.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="109.0" minWidth="10.0" prefWidth="94.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="95.0" minWidth="10.0" prefWidth="73.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<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">
<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">
<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">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</Spinner>
<Label alignment="CENTER" contentDisplay="CENTER" prefHeight="28.0" prefWidth="332.0" text="设置" GridPane.columnSpan="3">
<font>
<Font size="21.0" />
</font>
</Label>
<Label text="秒" GridPane.columnIndex="2" GridPane.rowIndex="1" />
<Label text="秒" GridPane.columnIndex="2" GridPane.rowIndex="2" />
<CheckBox fx:id="enableStepCB" mnemonicParsing="false" onAction="#onSettingThree" text="启用步骤辅助" GridPane.columnSpan="3" GridPane.halignment="CENTER" GridPane.rowIndex="3" GridPane.valignment="CENTER" />
</children>
</GridPane>
<HBox alignment="CENTER">
<children>
<Button fx:id="resetB" mnemonicParsing="false" onAction="#onReset" text="恢复默认值" />
</children>
<VBox.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</VBox.margin>
</HBox>
<HBox alignment="CENTER" nodeOrientation="RIGHT_TO_LEFT">
<children>
<Button fx:id="cancelB" mnemonicParsing="false" onAction="#onCancel" prefWidth="100.0" text="取消">
<HBox.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</HBox.margin>
</Button>
<Button fx:id="saveB" mnemonicParsing="false" onAction="#onSave" prefWidth="100.0" text="保存">
<HBox.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</HBox.margin>
</Button>
</children>
</HBox>
</children>
</VBox>

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB