diff --git a/.idea/misc.xml b/.idea/misc.xml
index 72ad960..1ce6b8b 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -5,6 +5,9 @@
+
+
+
diff --git a/build.gradle b/build.gradle
index 78158f5..f51564b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,12 +4,16 @@ buildscript {
mavenCentral()
}
}
+
plugins {
id 'java'
id 'io.franzbecker.gradle-lombok' version '3.0.0'
id 'application'
id 'org.openjfx.javafxplugin' version '0.1.0'
- id 'org.beryx.jlink' version '2.26.0'
+ // 不再使用模块插件,移除 org.javamodularity.moduleplugin
+ // id 'org.javamodularity.moduleplugin' version '1.8.12'
+ // 你可以根据需要保留 jlink 插件,但推荐取消模块化后暂时不用它
+ id 'org.beryx.jlink' version '2.26.0' apply false
}
group = project_group
@@ -23,102 +27,122 @@ ext {
junitVersion = '5.10.0'
}
+// 指定 JDK 版本
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(17)
+ }
+}
+
sourceCompatibility = '17'
targetCompatibility = '17'
tasks.withType(JavaCompile).configureEach {
- options.release = 17 // 明确指定Java版本
+ options.release = 17
options.encoding = 'UTF-8'
}
+// 平台判定,自动添加 JavaFX 依赖的 classifier
+def osName = org.gradle.internal.os.OperatingSystem.current()
+def javafxPlatform
+if (osName.isWindows()) {
+ javafxPlatform = 'win'
+} else if (osName.isMacOsX()) {
+ javafxPlatform = 'mac'
+} else if (osName.isLinux()) {
+ javafxPlatform = 'linux'
+} else {
+ throw new GradleException("Unsupported OS for JavaFX: $osName")
+}
+
+def javafxVersion = '17.0.6'
+
+dependencies {
+ // JavaFX 必须带平台后缀,classpath 模式必须
+ implementation "org.openjfx:javafx-controls:$javafxVersion:$javafxPlatform"
+ implementation "org.openjfx:javafx-fxml:$javafxVersion:$javafxPlatform"
+
+ // 你项目的其他依赖(老库走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'
+ implementation 'com.github.albfernandez:javadbf:1.14.1'
+ implementation 'org.apache.poi:poi-ooxml:5.4.1'
+ implementation 'com.intellij:annotations:12.0'
+
+ compileOnly 'org.projectlombok:lombok:1.18.38'
+ annotationProcessor 'org.projectlombok:lombok:1.18.38'
+
+ testCompileOnly 'org.projectlombok:lombok:1.18.38'
+ testAnnotationProcessor 'org.projectlombok:lombok:1.18.38'
+
+ testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
+ testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
+}
+
application {
- mainModule = 'top.r3944realms.docchecktoolrefactored'
+ // 取消模块化,去掉 mainModule
mainClass = 'top.r3944realms.docchecktoolrefactored.Main'
}
javafx {
- version = '17.0.6'
+ version = javafxVersion
+ // 这里的 modules 只为插件识别,实际classpath方式运行时不起作用
modules = ['javafx.controls', 'javafx.fxml']
}
processResources {
-// exclude 'logback.xml'
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
-dependencies {
- // Logback classic (included slf4j &)
- implementation 'ch.qos.logback:logback-classic:1.5.6'
- implementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.20.0'
-
- // Lombok
- compileOnly("org.projectlombok:lombok:1.18.38")
- annotationProcessor("org.projectlombok:lombok:1.18.38")
-
- testCompileOnly("org.projectlombok:lombok:1.18.38")
- testAnnotationProcessor("org.projectlombok:lombok:1.18.38")
-
- testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
- testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")
-}
-
-
test {
useJUnitPlatform()
configurations.configureEach {
exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl'
}
}
-// 可选:添加 testJar 任务
-tasks.register('testJar', Jar) {
- from sourceSets.test.output
-}
+// 创建日志目录任务
tasks.register('createLogDir') {
doLast {
mkdir "${projectDir}/logs"
}
-
-}
-// 打包sourcesJar任务
-tasks.register('sourcesJar', Jar) {
- dependsOn classes
-
- from sourceSets.main.allSource
-}
-// 打包javadocJar任务
-tasks.register('javadocJar', Jar) {
- dependsOn javadoc
-
- from javadoc.destinationDir
}
-// 解决javadoc打包乱码
-javadoc {
- options {
- encoding "UTF-8"
- charSet 'UTF-8'
- author true
- version true
- title "DG_LAB"
+processResources.dependsOn createLogDir
+
+// 其他自定义任务按需保留或调整
+tasks.register('runCli', JavaExec) {
+ group = 'application'
+ description = 'Run the application in CLI mode'
+
+ classpath = sourceSets.main.runtimeClasspath
+ mainClass = 'top.r3944realms.docchecktoolrefactored.Main'
+
+ // 默认参数,可以覆盖
+ args '--cli', '-v'
+
+ // 可以添加更多默认参数
+ if (project.hasProperty('cliArgs')) {
+ args project.property('cliArgs').split()
}
}
+// 可选:创建生成可执行JAR的任务
+tasks.register('buildCliJar', Jar) {
+ group = 'build'
+ description = 'Builds a standalone JAR for CLI mode'
-jlink {
- imageZip = project.file("${buildDir}/distributions/app-${javafx.platform.classifier}.zip")
- options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
- launcher {
- name = 'DocCheckTool'
- jvmArgs = ['-Dlogback.configurationFile=./config/logback.xml'] // 支持外部配置
+ manifest {
+ attributes 'Main-Class': 'top.r3944realms.docchecktoolrefactored.Main'
}
- mergedModule {
- requires 'java.logging'
- requires 'java.xml'
- }
-}
-jlinkZip {
- group = 'distribution'
+ from {
+ configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
+ } with jar
+
+ archiveBaseName = 'doc-check-tool-cli'
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
-processResources.dependsOn createLogDir
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index c0e4621..f49fe27 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,2 +1,2 @@
-project_group ='top.r3944realms.docchecktoolrefacored'
-project_version = '1.0-SNAPSHOT'
\ No newline at end of file
+project_group =top.r3944realms.docchecktoolrefacored
+project_version = 1.0-SNAPSHOT
\ No newline at end of file
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
deleted file mode 100644
index 2db4a61..0000000
--- a/src/main/java/module-info.java
+++ /dev/null
@@ -1,22 +0,0 @@
-module top.r3944realms.docchecktoolrefactored {
- requires javafx.graphics;
- requires javafx.controls;
- requires javafx.fxml;
- requires static lombok;
- requires org.slf4j;
-
- opens top.r3944realms.docchecktoolrefactored to javafx.fxml;
- opens top.r3944realms.docchecktoolrefactored.ui to javafx.fxml;
- opens top.r3944realms.docchecktoolrefactored.ui.module to javafx.fxml;
- opens top.r3944realms.docchecktoolrefactored.deprecated to javafx.fxml;
-
- exports top.r3944realms.docchecktoolrefactored to javafx.graphics;
- exports top.r3944realms.docchecktoolrefactored.ui to javafx.fxml;
- exports top.r3944realms.docchecktoolrefactored.ui.module to javafx.fxml;
- exports top.r3944realms.docchecktoolrefactored.deprecated to javafx.graphics;
-
- exports top.r3944realms.docchecktoolrefactored.core ;
- exports top.r3944realms.docchecktoolrefactored.io.scanner;
- exports top.r3944realms.docchecktoolrefactored.io.reader;
- exports top.r3944realms.docchecktoolrefactored.model;
-}
\ No newline at end of file
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java b/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java
new file mode 100644
index 0000000..6b594bc
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java
@@ -0,0 +1,20 @@
+package top.r3944realms.docchecktoolrefactored;
+
+import javafx.application.Application;
+import javafx.stage.Stage;
+import top.r3944realms.docchecktoolrefactored.ui.SceneManager;
+
+public class JavaFxApplication extends Application {
+ @Override
+ public void init() throws Exception {
+ super.init();
+ }
+
+ @Override
+ public void start(Stage primaryStage) throws Exception {
+ SceneManager.init(primaryStage);
+ SceneManager.switchLoginView();
+ primaryStage.show();
+ }
+
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/Main.java b/src/main/java/top/r3944realms/docchecktoolrefactored/Main.java
index 6cd07e9..707c20b 100644
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/Main.java
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/Main.java
@@ -1,35 +1,36 @@
package top.r3944realms.docchecktoolrefactored;
-import javafx.application.Application;
-import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
-import top.r3944realms.docchecktoolrefactored.ui.SceneManager;
-import top.r3944realms.docchecktoolrefactored.util.StringUtil;
+import top.r3944realms.docchecktoolrefactored.cil.CliProcessor;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
/**
* The type Main.
*/
@Slf4j
-public class Main extends Application {
- @Override
- public void init() throws Exception {
- super.init();
- }
-
- @Override
- public void start(Stage primaryStage) throws Exception {
- SceneManager.init(primaryStage);
- SceneManager.switchLoginView();
- primaryStage.show();
- }
+public class Main {
/**
* The entry point of application.
*
* @param args the input arguments
*/
+ @SuppressWarnings("DataFlowIssue")
public static void main(String[] args) {
- log.info(StringUtil.NO_BUG);
- launch(args);
+// log.info(StringUtil.NO_BUG);
+ // 检查是否有 --cli 参数
+ List list = Arrays.asList(args);
+ if (list.contains("--cli")) {
+ // CLI 模式
+ List strList = new ArrayList<>();
+ list.stream().filter(i -> !i.equals("--cli")).forEach(strList::add);
+ new CliProcessor().process(strList.toArray(new String[0]));
+ } else {
+ // GUI 模式
+ JavaFxApplication.launch(JavaFxApplication.class, args);
+ }
}
}
\ No newline at end of file
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/cil/CliProcessor.java b/src/main/java/top/r3944realms/docchecktoolrefactored/cil/CliProcessor.java
new file mode 100644
index 0000000..7a5b00a
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/cil/CliProcessor.java
@@ -0,0 +1,440 @@
+package top.r3944realms.docchecktoolrefactored.cil;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.cli.*;
+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.model.DuplicateGroup;
+import top.r3944realms.docchecktoolrefactored.util.FileUtil;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * 命令行处理器,负责解析参数和执行重复文件检查
+ */
+@Slf4j
+public class CliProcessor {
+ // 参数定义
+ private static final String HELP_OPTION = "h";
+ private static final String DIRECTORY_OPTION = "d";
+ private static final String PROGRESS_OPTION = "p";
+ private static final String MIN_SIZE_OPTION = "m";
+ private static final String MAX_SIZE_OPTION = "M";
+ private static final String EXCLUDE_OPTION = "e";
+ private static final String FORMAT_OPTION = "f";
+ private static final String OUTPUT_OPTION = "o";
+ private static final String QUIET_OPTION = "q";
+ private static final String SORT_OPTION = "s";
+ private static final String VERSION_OPTION = "v";
+
+ // 支持的输出格式
+ private enum OutputFormat {
+ TEXT, JSON, CSV
+ }
+
+ // 支持的排序方式
+ private enum SortBy {
+ SIZE, COUNT, PATH
+ }
+
+ // 配置选项
+ private boolean showProgress = false;
+ private boolean quietMode = false;
+ private OutputFormat outputFormat = OutputFormat.TEXT;
+ private SortBy sortBy = SortBy.SIZE;
+ private Path outputFile = null;
+ private long minSize = 0;
+ private Long maxSize = null;
+ private List excludeExtensions = List.of();
+
+ /**
+ * 处理命令行参数并执行相应操作
+ * @param args 命令行参数
+ */
+ public void process(String[] args) {
+ Options options = createOptions();
+ CommandLineParser parser = new DefaultParser();
+
+ try {
+ CommandLine cmd = parser.parse(options, args);
+
+ // 处理帮助选项
+ if (cmd.hasOption(HELP_OPTION)) {
+ printHelp(options);
+ return;
+ }
+
+ // 处理版本选项
+ if (cmd.hasOption(VERSION_OPTION)) {
+ printVersion();
+ return;
+ }
+
+ // 解析参数
+ parseOptions(cmd);
+
+ // 验证目录参数
+ if (!cmd.hasOption(DIRECTORY_OPTION)) {
+ throw new IllegalArgumentException("Directory option (-d) is required");
+ }
+ Path scanDir = Paths.get(cmd.getOptionValue(DIRECTORY_OPTION));
+
+ // 执行扫描
+ List duplicates = scanForDuplicates(scanDir);
+
+ // 输出结果
+ outputResults(duplicates);
+
+ } catch (ParseException e) {
+ System.err.println("Error: " + e.getMessage());
+ printHelp(options);
+ System.exit(1);
+ } catch (Exception e) {
+ log.error("Error processing CLI command", e);
+ System.err.println("Error: " + e.getMessage());
+ System.exit(1);
+ }
+ }
+
+ /**
+ * 创建命令行选项
+ */
+ private Options createOptions() {
+ Options options = new Options();
+
+ // 基本选项
+ options.addOption(Option.builder(HELP_OPTION)
+ .longOpt("help")
+ .desc("Show this help message")
+ .build());
+ options.addOption(Option.builder(VERSION_OPTION)
+ .longOpt("version")
+ .desc("Show version information")
+ .build());
+ options.addOption(Option.builder(DIRECTORY_OPTION)
+ .longOpt("directory")
+ .hasArg()
+ .argName("PATH")
+ .desc("Directory to scan for duplicates")
+ .build());
+ options.addOption(Option.builder(PROGRESS_OPTION)
+ .longOpt("progress")
+ .desc("Show progress bar during processing")
+ .build());
+ options.addOption(Option.builder(QUIET_OPTION)
+ .longOpt("quiet")
+ .desc("Quiet mode, only show errors")
+ .build());
+
+ // 过滤选项
+ options.addOption(Option.builder(MIN_SIZE_OPTION)
+ .longOpt("min-size")
+ .hasArg()
+ .argName("BYTES")
+ .desc("Minimum file size to consider (in bytes)")
+ .build());
+ options.addOption(Option.builder(MAX_SIZE_OPTION)
+ .longOpt("max-size")
+ .hasArg()
+ .argName("BYTES")
+ .desc("Maximum file size to consider (in bytes)")
+ .build());
+ options.addOption(Option.builder(EXCLUDE_OPTION)
+ .longOpt("exclude")
+ .hasArg()
+ .argName("EXTENSIONS")
+ .desc("Comma-separated list of file extensions to exclude")
+ .build());
+
+ // 输出控制选项
+ options.addOption(Option.builder(FORMAT_OPTION)
+ .longOpt("format")
+ .hasArg()
+ .argName("FORMAT")
+ .desc("Output format (text/json/csv), default: text")
+ .build());
+ options.addOption(Option.builder(OUTPUT_OPTION)
+ .longOpt("output")
+ .hasArg()
+ .argName("FILE")
+ .desc("Write output to file instead of stdout")
+ .build());
+ options.addOption(Option.builder(SORT_OPTION)
+ .longOpt("sort")
+ .hasArg()
+ .argName("CRITERIA")
+ .desc("Sort results by (size/count/path), default: size")
+ .build());
+
+ return options;
+ }
+
+ /**
+ * 解析命令行选项
+ */
+ private void parseOptions(CommandLine cmd) throws ParseException {
+ showProgress = cmd.hasOption(PROGRESS_OPTION);
+ quietMode = cmd.hasOption(QUIET_OPTION);
+
+ // 解析输出格式
+ if (cmd.hasOption(FORMAT_OPTION)) {
+ try {
+ outputFormat = OutputFormat.valueOf(cmd.getOptionValue(FORMAT_OPTION).toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid output format. Allowed values: text, json, csv");
+ }
+ }
+
+ // 解析排序方式
+ if (cmd.hasOption(SORT_OPTION)) {
+ try {
+ sortBy = SortBy.valueOf(cmd.getOptionValue(SORT_OPTION).toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid sort criteria. Allowed values: size, count, path");
+ }
+ }
+
+ // 解析输出文件
+ if (cmd.hasOption(OUTPUT_OPTION)) {
+ outputFile = Paths.get(cmd.getOptionValue(OUTPUT_OPTION));
+ }
+
+ // 解析大小限制
+ if (cmd.hasOption(MIN_SIZE_OPTION)) {
+ try {
+ minSize = Long.parseLong(cmd.getOptionValue(MIN_SIZE_OPTION));
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid min-size value: must be a number");
+ }
+ }
+
+ if (cmd.hasOption(MAX_SIZE_OPTION)) {
+ try {
+ maxSize = Long.parseLong(cmd.getOptionValue(MAX_SIZE_OPTION));
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid max-size value: must be a number");
+ }
+ }
+
+ // 解析排除的扩展名
+ if (cmd.hasOption(EXCLUDE_OPTION)) {
+ excludeExtensions = Arrays.asList(cmd.getOptionValue(EXCLUDE_OPTION).split(","));
+ }
+ }
+
+ /**
+ * 执行重复文件扫描
+ */
+ private List scanForDuplicates(Path scanDir) throws IOException {
+ if (!quietMode) {
+ System.out.println("Scanning directory: " + scanDir.toAbsolutePath());
+ }
+
+ FileScanner scanner = new RobustParallelScanner(20);
+ FileHashCalculator calculator = FileHashCalculator.defaultInstance();
+ DuplicateFinder finder = new DuplicateFinder(scanner, calculator, showProgress);
+
+ List duplicates = finder.findDuplicates(scanDir);
+
+ // 应用过滤
+ duplicates = filterResults(duplicates);
+
+ // 应用排序
+ duplicates = sortResults(duplicates);
+
+ return duplicates;
+ }
+
+ /**
+ * 过滤结果
+ */
+ private List filterResults(List groups) {
+ return groups.stream()
+ .filter(group -> group.size() >= minSize)
+ .filter(group -> maxSize == null || group.size() <= maxSize)
+ .filter(group -> excludeExtensions.stream()
+ .noneMatch(ext -> group.fileMetas().stream()
+ .anyMatch(file -> file.getPath().toString().toLowerCase().endsWith(ext.toLowerCase()))))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 排序结果
+ */
+ private List sortResults(List groups) {
+ return switch (sortBy) {
+ case SIZE -> groups.stream()
+ .sorted((g1, g2) -> Long.compare(g2.size(), g1.size()))
+ .collect(Collectors.toList());
+ case COUNT -> groups.stream()
+ .sorted((g1, g2) -> Integer.compare(g2.fileMetas().size(), g1.fileMetas().size()))
+ .collect(Collectors.toList());
+ case PATH -> groups.stream()
+ .sorted(Comparator.comparing(g -> g.fileMetas().get(0).getPath()))
+ .collect(Collectors.toList());
+ };
+ }
+
+ /**
+ * 输出结果
+ */
+ private void outputResults(List duplicates) throws IOException {
+ if (quietMode && duplicates.isEmpty()) {
+ return;
+ }
+
+ String outputContent = switch (outputFormat) {
+ case JSON -> formatAsJson(duplicates);
+ case CSV -> formatAsCsv(duplicates);
+ default -> formatAsText(duplicates);
+ };
+
+ if (outputFile != null) {
+ try (BufferedWriter writer = Files.newBufferedWriter(outputFile)) {
+ writer.write(outputContent);
+ }
+ if (!quietMode) {
+ System.out.println("Results written to: " + outputFile.toAbsolutePath());
+ }
+ } else {
+ System.out.println(outputContent);
+ }
+ }
+
+ /**
+ * 文本格式输出
+ */
+ private String formatAsText(List duplicates) {
+ if (duplicates.isEmpty()) {
+ return "No duplicate files found.";
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format("\nFound %d groups of duplicate files:\n", duplicates.size()));
+
+ AtomicInteger groupNum = new AtomicInteger(1);
+ duplicates.forEach(group -> {
+ sb.append(String.format("\nGroup %d:\n", groupNum.getAndIncrement()));
+ sb.append(String.format("Hash: %s\n", group.hash()));
+ sb.append(String.format("Size: %s\n", FileUtil.humanReadableByteCount(group.size())));
+ sb.append("Files:\n");
+ group.fileMetas().forEach(file -> sb.append(String.format(" %s\n", file.getPath())));
+ });
+
+ // 添加统计信息
+ long totalWastedSpace = duplicates.stream()
+ .mapToLong(group -> group.size() * (group.fileMetas().size() - 1))
+ .sum();
+ sb.append(String.format("\nTotal wasted space: %s\n",
+ FileUtil.humanReadableByteCount(totalWastedSpace)));
+
+ return sb.toString();
+ }
+
+ /**
+ * JSON格式输出
+ */
+ private String formatAsJson(List duplicates) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{\n");
+
+ // 基本信息
+ sb.append(" \"duplicate_groups\": [\n");
+
+ // 各组数据
+ duplicates.forEach(group -> {
+ sb.append(" {\n");
+ sb.append(String.format(" \"hash\": \"%s\",\n", group.hash()));
+ sb.append(String.format(" \"size\": %d,\n", group.size()));
+ sb.append(String.format(" \"size_human\": \"%s\",\n",
+ FileUtil.humanReadableByteCount(group.size())));
+ sb.append(" \"files\": [\n");
+
+ group.fileMetas().forEach(file ->
+ sb.append(String.format(" \"%s\",\n", file.getPath())));
+
+ // 移除最后一个逗号
+ if (!group.fileMetas().isEmpty()) {
+ sb.delete(sb.length() - 2, sb.length());
+ sb.append("\n");
+ }
+
+ sb.append(" ]\n");
+ sb.append(" },\n");
+ });
+
+ // 移除最后一个逗号
+ if (!duplicates.isEmpty()) {
+ sb.delete(sb.length() - 2, sb.length());
+ sb.append("\n");
+ }
+
+ sb.append(" ],\n");
+
+ // 统计信息
+ long totalWastedSpace = duplicates.stream()
+ .mapToLong(group -> group.size() * (group.fileMetas().size() - 1))
+ .sum();
+ sb.append(" \"stats\": {\n");
+ sb.append(String.format(" \"total_groups\": %d,\n", duplicates.size()));
+ sb.append(String.format(" \"total_files\": %d,\n",
+ duplicates.stream().mapToInt(group -> group.fileMetas().size()).sum()));
+ sb.append(String.format(" \"total_wasted_space\": %d,\n", totalWastedSpace));
+ sb.append(String.format(" \"total_wasted_space_human\": \"%s\"\n",
+ FileUtil.humanReadableByteCount(totalWastedSpace)));
+ sb.append(" }\n");
+
+ sb.append("}\n");
+ return sb.toString();
+ }
+
+ /**
+ * CSV格式输出
+ */
+ private String formatAsCsv(List duplicates) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("group,hash,size,size_human,file_path\n");
+
+ AtomicInteger groupNum = new AtomicInteger(1);
+ duplicates.forEach(group -> {
+ String groupPrefix = groupNum.getAndIncrement() + "," +
+ group.hash() + "," +
+ group.size() + "," +
+ FileUtil.humanReadableByteCount(group.size());
+
+ group.fileMetas().forEach(file ->
+ sb.append(groupPrefix).append(",").append(file.getPath()).append("\n"));
+ });
+
+ return sb.toString();
+ }
+
+ /**
+ * 打印帮助信息
+ */
+ private void printHelp(Options options) {
+ HelpFormatter formatter = new HelpFormatter();
+ formatter.printHelp("doc-check-tool", options);
+ }
+
+ /**
+ * 打印版本信息
+ */
+ private void printVersion() {
+ Package pkg = getClass().getPackage();
+ String version = pkg.getImplementationVersion();
+ System.out.println("Doc Check Tool " + (version != null ? version : "DEV"));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileComparator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileComparator.java
new file mode 100644
index 0000000..22f2f71
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileComparator.java
@@ -0,0 +1,266 @@
+package top.r3944realms.docchecktoolrefactored.core;
+
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+public class AddressFileComparator {
+
+ public enum CompareMode {
+ PAGE_LEVEL, // 页面级比较
+ FILE_LEVEL // 文件级比较
+ }
+
+ @Getter
+ public static class ComparisonResult {
+ private final int physicalRecordsCount; // 物理文件记录数
+ private final int logicalRecordsCount; // 逻辑文件记录数
+ private final List forwardComparisonResults; // 正向比较文件名缺失结果
+ private final List backwardComparisonResults; // 反向比较文件名缺失结果
+ private final List pathMismatchResults; // 路径不一致的结果
+ private final List pageCountMismatchResults; // 页数不一致的结果(仅文件级比较使用)
+
+ public ComparisonResult(int physicalRecordsCount,
+ int logicalRecordsCount,
+ List forwardResults,
+ List backwardResults,
+ List pathMismatchResults,
+ List pageCountMismatchResults) {
+ this.physicalRecordsCount = physicalRecordsCount;
+ this.logicalRecordsCount = logicalRecordsCount;
+ this.forwardComparisonResults = forwardResults;
+ this.backwardComparisonResults = backwardResults;
+ this.pathMismatchResults = pathMismatchResults;
+ this.pageCountMismatchResults = pageCountMismatchResults;
+ }
+
+ // 为向后兼容保留原来的构造函数
+ public ComparisonResult(int physicalRecordsCount,
+ int logicalRecordsCount,
+ List forwardResults,
+ List backwardResults,
+ List pathMismatchResults) {
+ this(physicalRecordsCount, logicalRecordsCount, forwardResults,
+ backwardResults, pathMismatchResults, new ArrayList<>());
+ }
+
+ }
+
+ public ComparisonResult compareFiles(String physicalAddressFilePath, String logicalAddressFilePath) {
+ return compareFiles(physicalAddressFilePath, logicalAddressFilePath, CompareMode.PAGE_LEVEL);
+ }
+
+ public ComparisonResult compareFiles(String physicalAddressFilePath, String logicalAddressFilePath, CompareMode compareMode) {
+ List physicalRecords = readCSV(physicalAddressFilePath);
+ List logicalRecords = readCSV(logicalAddressFilePath);
+
+ // 记录读取的行数(不包括标题行)
+ int physicalCount = physicalRecords.size();
+ int logicalCount = logicalRecords.size();
+
+ log.info("读取物理地址文件记录数: {}", physicalCount);
+ log.info("读取逻辑地址文件记录数: {}", logicalCount);
+
+ List forwardComparisonResults = new ArrayList<>(); // 物理文件在逻辑文件中未找到
+ List backwardComparisonResults = new ArrayList<>(); // 逻辑文件在物理文件中未找到
+ List pathMismatchResults = new ArrayList<>(); // 文件名相同但路径不一致
+ List pageCountMismatchResults = new ArrayList<>(); // 文件名和路径相同但页数不一致(仅文件级)
+
+ // 正向比较:遍历物理文件,检查是否在逻辑文件中存在
+ for (String[] physicalRecord : physicalRecords) {
+ // 确保数据行有足够列数
+ if (compareMode == CompareMode.PAGE_LEVEL && physicalRecord.length < 2) continue;
+ if (compareMode == CompareMode.FILE_LEVEL && physicalRecord.length < 3) continue;
+
+ String physicalFileName = physicalRecord[0];
+ String physicalAddress = physicalRecord[1];
+ String physicalPageCount = compareMode == CompareMode.FILE_LEVEL ? physicalRecord[2] : null;
+
+ boolean found = false;
+ for (String[] logicalRecord : logicalRecords) {
+ // 确保数据行有足够列数
+ if (compareMode == CompareMode.PAGE_LEVEL && logicalRecord.length < 2) continue;
+ if (compareMode == CompareMode.FILE_LEVEL && logicalRecord.length < 3) continue;
+
+ String logicalFileName = logicalRecord[0];
+ String logicalAddress = logicalRecord[1];
+ String logicalPageCount = compareMode == CompareMode.FILE_LEVEL ? logicalRecord[2] : null;
+
+ if (physicalFileName.equals(logicalFileName)) {
+ found = true;
+ // 文件名相同,比较路径
+ if (!physicalAddress.equals(logicalAddress)) {
+ pathMismatchResults.add("文件名=" + physicalFileName +
+ ", 物理地址=" + physicalAddress +
+ ", 逻辑地址=" + logicalAddress);
+ }
+ // 如果是文件级比较且路径相同,再比较页数
+ else if (compareMode == CompareMode.FILE_LEVEL &&
+ physicalPageCount != null && logicalPageCount != null &&
+ !physicalPageCount.equals(logicalPageCount)) {
+ pageCountMismatchResults.add("文件名=" + physicalFileName +
+ ", 物理地址=" + physicalAddress +
+ ", 逻辑地址=" + logicalAddress +
+ ", 物理页数=" + physicalPageCount +
+ ", 逻辑页数=" + logicalPageCount);
+ }
+ break;
+ }
+ }
+
+ // 如果在逻辑文件中未找到该物理文件记录
+ if (!found) {
+ String result = "文件名=" + physicalFileName + ", 物理地址=" + physicalAddress;
+ if (compareMode == CompareMode.FILE_LEVEL && physicalPageCount != null) {
+ result += ", 物理页数=" + physicalPageCount;
+ }
+ forwardComparisonResults.add(result);
+ }
+ }
+
+ // 反向比较:遍历逻辑文件,检查是否在物理文件中存在
+ for (String[] logicalRecord : logicalRecords) {
+ // 确保数据行有足够列数
+ if (compareMode == CompareMode.PAGE_LEVEL && logicalRecord.length < 2) continue;
+ if (compareMode == CompareMode.FILE_LEVEL && logicalRecord.length < 3) continue;
+
+ String logicalFileName = logicalRecord[0];
+ String logicalAddress = logicalRecord[1];
+ String logicalPageCount = compareMode == CompareMode.FILE_LEVEL ? logicalRecord[2] : null;
+
+ boolean found = false;
+ for (String[] physicalRecord : physicalRecords) {
+ // 确保数据行有足够列数
+ if (compareMode == CompareMode.PAGE_LEVEL && physicalRecord.length < 2) continue;
+ if (compareMode == CompareMode.FILE_LEVEL && physicalRecord.length < 3) continue;
+
+ String physicalFileName = physicalRecord[0];
+
+ if (logicalFileName.equals(physicalFileName)) {
+ found = true;
+ break;
+ }
+ }
+
+ // 如果在物理文件中未找到该逻辑文件记录
+ if (!found) {
+ String result = "文件名=" + logicalFileName + ", 逻辑地址=" + logicalAddress;
+ if (compareMode == CompareMode.FILE_LEVEL && logicalPageCount != null) {
+ result += ", 逻辑页数=" + logicalPageCount;
+ }
+ backwardComparisonResults.add(result);
+ }
+ }
+
+ // 将比较结果记录到日志中
+ logComparisonResults(physicalCount, logicalCount, forwardComparisonResults,
+ backwardComparisonResults, pathMismatchResults, pageCountMismatchResults, compareMode);
+
+ return new ComparisonResult(
+ physicalCount,
+ logicalCount,
+ forwardComparisonResults,
+ backwardComparisonResults,
+ pathMismatchResults,
+ pageCountMismatchResults
+ );
+ }
+
+ private List readCSV(String filePath) {
+ List records = new ArrayList<>();
+
+ try {
+ File file = new File(filePath);
+ if (!file.exists()) {
+ log.error("CSV文件不存在: {}", filePath);
+ return records;
+ }
+
+ BufferedReader reader = new BufferedReader(new FileReader(file));
+ String line;
+ boolean isFirstLine = true;
+
+ while ((line = reader.readLine()) != null) {
+ // 跳过首行(标题行)
+ if (isFirstLine) {
+ isFirstLine = false;
+ continue;
+ }
+ // 按逗号分隔CSV行数据
+ String[] data = line.split(",");
+ records.add(data);
+ }
+
+ reader.close();
+ log.info("成功读取CSV文件,共 {} 行记录", records.size());
+
+ } catch (Exception e) {
+ log.error("读取CSV文件时出错: {}", e.getMessage(), e);
+ }
+
+ return records;
+ }
+
+ private void logComparisonResults(int physicalCount, int logicalCount,
+ List forwardResults, List backwardResults,
+ List pathMismatchResults, List pageCountMismatchResults,
+ CompareMode compareMode) {
+ log.info("=== 文件比较结果 ===");
+ log.info("物理地址文件记录数: {}", physicalCount);
+ log.info("逻辑地址文件记录数: {}", logicalCount);
+
+ if (pathMismatchResults.isEmpty()) {
+ log.info("没有路径错误");
+ } else {
+ log.info("文件名相同但路径不一致的记录数量: {}", pathMismatchResults.size());
+ for (String result : pathMismatchResults) {
+ log.info("\t{}", result);
+ }
+ }
+
+ if (compareMode == CompareMode.FILE_LEVEL) {
+ if (pageCountMismatchResults.isEmpty()) {
+ log.info("没有页数错误");
+ } else {
+ log.info("文件名和路径相同但页数不一致的记录数量: {}", pageCountMismatchResults.size());
+ for (String result : pageCountMismatchResults) {
+ log.info("\t{}", result);
+ }
+ }
+ }
+
+ if (forwardResults.isEmpty()) {
+ log.info("没有物理存在而逻辑不存在的文件");
+ } else {
+ log.info("物理文件在逻辑文件中未找到的记录数量: {}", forwardResults.size());
+ for (String result : forwardResults) {
+ log.info("\t{}", result);
+ }
+ }
+
+ if (backwardResults.isEmpty()) {
+ log.info("没有逻辑存在而物理不存在的文件");
+ } else {
+ log.info("逻辑文件在物理文件中未找到的记录数量: {}", backwardResults.size());
+ for (String result : backwardResults) {
+ log.info("\t{}", result);
+ }
+ }
+
+ log.info("=== 比较完成 ===");
+ }
+
+ // 为向后兼容保留原来的日志方法
+ private void logComparisonResults(int physicalCount, int logicalCount,
+ List forwardResults, List backwardResults,
+ List pathMismatchResults) {
+ logComparisonResults(physicalCount, logicalCount, forwardResults, backwardResults,
+ pathMismatchResults, new ArrayList<>(), CompareMode.PAGE_LEVEL);
+ }
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileGenerator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileGenerator.java
new file mode 100644
index 0000000..4e5d109
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/AddressFileGenerator.java
@@ -0,0 +1,40 @@
+package top.r3944realms.docchecktoolrefactored.core;
+
+import org.intellij.lang.annotations.MagicConstant;
+
+import java.io.File;
+
+public interface AddressFileGenerator {
+ /**
+ * 页面级
+ */
+ int PAGE_TYPE = 1;
+ /**
+ * 文件级
+ */
+ int FILE_TYPE = 2;
+
+ /**
+ * 回调接口
+ */
+ interface Callback {
+ void onProgress(String message);
+ void onSuccess(String outputPath);
+ void onError(String errorMessage);
+ }
+
+ /**
+ * 生成地址文件
+ *
+ * @param folderPath 文件夹路径
+ * @param outputFile 输出文件
+ * @param folderType 文件夹类型 ({@link AddressFileGenerator#PAGE_TYPE "页面级"} 或 {@link AddressFileGenerator#FILE_TYPE "文件级"})
+ * @param callback {@link Callback 回调接口}
+ */
+ void generateAddressFile(
+ String folderPath,
+ File outputFile,
+ @MagicConstant(intValues = {PAGE_TYPE, FILE_TYPE}) int folderType,
+ Callback callback
+ );
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java
index 13bbc87..ce4edb1 100644
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/DuplicateFinder.java
@@ -10,6 +10,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@@ -39,8 +40,19 @@ public class DuplicateFinder {
// 第一阶段:按文件大小分组
Map> sizeGroups = groupFilesBySize(rootDir);
+ // 计算需要处理的总文件数(大小分组中可能有重复的文件)
+ int totalFilesToProcess = sizeGroups.values().stream()
+ .filter(group -> group.size() > 1)
+ .mapToInt(List::size)
+ .sum();
+
+ if (totalFilesToProcess == 0) {
+ return Collections.emptyList();
+ }
+
// 第二阶段:对可能重复的文件计算哈希
Map> hashGroups = new ConcurrentHashMap<>();
+ AtomicInteger processedFiles = new AtomicInteger(0);
sizeGroups.values().parallelStream()
.filter(group -> group.size() > 1) // 只处理可能重复的文件
@@ -49,12 +61,19 @@ public class DuplicateFinder {
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(); // 完成进度条后换行
+ }
// 第三阶段:构建结果
return hashGroups.values().stream()
.filter(group -> group.size() > 1)
@@ -75,7 +94,11 @@ public class DuplicateFinder {
FileScanner.ProgressAwareListener listener = new FileScanner.ProgressAwareListener() {
@Override
public void onProgressUpdate(int current, int total) {
- log.info("Scanning progress: {}/{} ", current, total);
+ if (enableProgress) {
+ printProgress("Scanning files", current, total);
+ } else {
+ log.info("Scanning progress: {} / {} ", current, total);
+ }
}
@Override
@@ -101,4 +124,24 @@ public class DuplicateFinder {
fileScanner.scan(rootDir, listener);
return sizeGroups;
}
+ /**
+ * 打印进度条
+ * @param phase 当前阶段描述
+ * @param current 当前进度
+ * @param total 总进度
+ */
+ private void printProgress(String phase, int current, int total) {
+ int width = 50; // 进度条宽度
+ float percent = (float) current / total;
+ int progress = (int) (width * percent);
+
+ String progressBar = String.format("\r%s: [%-" + width + "s] %3d%% %d/%d",
+ phase,
+ "=".repeat(progress),
+ (int) (percent * 100),
+ current,
+ total);
+
+ System.out.print(progressBar);
+ }
}
\ No newline at end of file
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/FileHashCalculator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/FileHashCalculator.java
index b986ff2..269df2b 100644
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/FileHashCalculator.java
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/FileHashCalculator.java
@@ -14,6 +14,31 @@ public interface FileHashCalculator {
*/
String calculateHash(Path file) throws IOException;
+ /**
+ * 计算文件部分hash值方法
+ * @param file 要计算的文件路径
+ * @param partialChunkSize 分块大小
+ * @return 文件的哈希值字符串
+ */
+ String calculatePartialHash(Path file, int partialChunkSize) throws IOException;
+
+ /**
+ * 计算文件部分hash值方法(默认)
+ * @param file 要计算的文件路径
+ * @return 文件的哈希值字符串
+ */
+ default String calculatePartialHash(Path file) throws IOException {
+ return calculatePartialHash(file, getPartialSize());
+ }
+
+ /**
+ * 块大小
+ * @return 块大小
+ */
+ default int getPartialSize() {
+ return 4096;// 4 * 1024
+ }
+
/**
* 默认实现使用MD5
*/
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java
new file mode 100644
index 0000000..ad1bf36
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/HashFileGenerator.java
@@ -0,0 +1,92 @@
+package top.r3944realms.docchecktoolrefactored.core;
+
+
+import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
+import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class HashFileGenerator {
+
+ public interface ProgressListener {
+ void onProgressUpdate(int current, int total);
+ }
+
+ public void generateHashFile(List directories, Path outputFile, ProgressListener listener) throws IOException, InterruptedException {
+ List allFiles = new ArrayList<>();
+
+ // 扫描所有目录中的文件
+ for (Path directory : directories) {
+ if (!Files.isDirectory(directory)) {
+ throw new IllegalArgumentException("指定路径不是有效目录: " + directory);
+ }
+
+ List files = new ArrayList<>();
+ CompletableFuture scanFuture = new CompletableFuture<>();
+
+ // 使用 RobustParallelScanner 扫描文件
+ try (RobustParallelScanner scanner = new RobustParallelScanner(10)) {
+ scanner.scan(directory, new FileScanner.FileScanListener() {
+ @Override
+ public void onFileFound(Path file) {
+ files.add(file);
+ }
+
+ @Override
+ public void onScanComplete() {
+ scanFuture.complete(null);
+ }
+
+ @Override
+ public void onError(Path path, Exception e) {
+ System.err.println("Error scanning path: " + path + " - " + e.getMessage());
+ }
+ });
+
+ // 等待扫描完成
+ scanFuture.join();
+ allFiles.addAll(files);
+ }
+ }
+
+ // 计算每个文件的哈希值
+ List hashResults = new ArrayList<>();
+ AtomicInteger processedFiles = new AtomicInteger(0);
+ int totalFiles = allFiles.size();
+
+ allFiles.parallelStream().forEach(file -> {
+ try {
+ String hash = new MD5HashCalculator().calculatePartialHash(file);
+ String[] result = {file.getFileName().toString(), hash};
+ synchronized (hashResults) {
+ hashResults.add(result);
+ }
+ int processed = processedFiles.incrementAndGet();
+ if (listener != null) {
+ listener.onProgressUpdate(processed, totalFiles);
+ }
+ } catch (IOException e) {
+ System.err.println("无法计算该文件哈希值: " + file + " - " + e.getMessage());
+ }
+ });
+
+ // 写入结果到文件
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile.toFile()))) {
+ writer.write("文件名,哈希值");
+ writer.newLine();
+ for (String[] result : hashResults) {
+ writer.write(result[0] + "," + result[1]);
+ writer.newLine();
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/LogicalAddressFileGenerator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/LogicalAddressFileGenerator.java
new file mode 100644
index 0000000..564d045
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/LogicalAddressFileGenerator.java
@@ -0,0 +1,158 @@
+package top.r3944realms.docchecktoolrefactored.core;
+
+import top.r3944realms.docchecktoolrefactored.io.reader.CatalogFileReader;
+import top.r3944realms.docchecktoolrefactored.io.reader.CatalogFileReaderFactory;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+public class LogicalAddressFileGenerator implements AddressFileGenerator {
+ @Override
+ public void generateAddressFile(String catalogFilePath, File outputFile, int folderType, Callback callback) {
+ callback.onProgress("正在生成逻辑地址文件...");
+ try {
+ // 使用工厂模式创建相应的文件读取器
+ List records = readCatalogFile(catalogFilePath);
+
+ // 过滤掉 可能的 输出文件记录 -- 注2: 避免将输出文件作为输入处理
+ String outputFileName = outputFile.getName();
+ records = records.stream()
+ .filter(record -> !record.archiveCode.equalsIgnoreCase(outputFileName))
+ .toList();
+ try (PrintWriter writer = new PrintWriter(outputFile, StandardCharsets.UTF_8)) {
+ if (folderType == PAGE_TYPE) {
+ // 页面级逻辑:为每页生成一行数据
+ generatePageLevelFile(writer, records);
+ } else if (folderType == FILE_TYPE) {
+ // 文件级逻辑:每个档案只生成一行数据
+ generateFileLevelFile(writer, records);
+ } else {
+ throw new IllegalArgumentException("不支持的文件夹类型: " + folderType);
+ }
+ }
+
+ callback.onSuccess("逻辑地址文件生成成功: " + outputFile.getAbsolutePath());
+ } catch (Exception e) {
+ callback.onError("生成逻辑地址文件时出错: " + e.getMessage());
+ }
+ }
+ /**
+ * 生成页面级逻辑地址文件
+ */
+ private void generatePageLevelFile(PrintWriter writer, List records) {
+ // 写入CSV头部
+ writer.println("逻辑文件名,逻辑地址");
+
+ // 处理每条记录
+ for (Record record : records) {
+ String archiveCode = record.archiveCode;
+ int page = record.page;
+
+ // 为每页生成一行数据
+ for (int i = 1; i <= page; i++) {
+ // 生成逻辑文件名
+ String logicalFileName = String.format("%s-%04d", archiveCode, i);
+
+ // 生成逻辑地址
+ String logicalAddress = generatePageLevelLogicalAddress(archiveCode, i);
+
+ // 写入CSV行
+ writer.printf("%s,%s%n", logicalFileName, logicalAddress);
+ }
+ }
+ }
+
+ /**
+ * 生成文件级逻辑地址文件
+ */
+ private void generateFileLevelFile(PrintWriter writer, List records) {
+ // 写入CSV头部(包含页数列)
+ writer.println("逻辑文件名,逻辑地址,页数");
+
+ // 处理每条记录
+ for (Record record : records) {
+ String archiveCode = record.archiveCode;
+ int page = record.page;
+
+ // 生成逻辑地址(不包含页数)
+ String logicalAddress = generateFileLevelLogicalAddress(archiveCode);
+
+ // 写入CSV行,包含页数
+ writer.printf("%s,%s,%d%n", /* 逻辑文件名(就是档号)*/ archiveCode, logicalAddress, page);
+ }
+ }
+
+ /**
+ * 根据档号生成文件级逻辑地址(不包含页数)
+ *
+ * @param archiveCode 档号内容
+ * @return 文件级逻辑地址
+ */
+ private String generateFileLevelLogicalAddress(String archiveCode) {
+ StringBuilder address = csvStringBuilder(archiveCode);
+
+ // 添加最后的档号部分
+ if (!address.isEmpty()) {
+ address.append("\\").append(archiveCode);
+ }
+
+ return address.toString();
+ }
+
+
+ /**
+ * 根据档号生成逻辑地址
+ *
+ * @param archiveCode 档号内容
+ * @param pageNumber 页码
+ * @return 逻辑地址
+ */
+ private String generatePageLevelLogicalAddress(String archiveCode, int pageNumber) {
+ StringBuilder address = csvStringBuilder(archiveCode);
+
+ // 添加页码部分,同样去掉第一个
+ if (!address.isEmpty()) {
+ address.append("\\");
+ }
+ address.append(String.format("%s-%04d", archiveCode, pageNumber));
+
+ return address.toString();
+ }
+
+ private StringBuilder csvStringBuilder(String archiveCode) {
+ StringBuilder address = new StringBuilder();
+ String[] parts = archiveCode.split("-");
+
+ StringBuilder path = new StringBuilder();
+ for (int i = 0; i < parts.length; i++) {
+ if (i > 0) {
+ path.append("-");
+ }
+ path.append(parts[i]);
+ // 去掉第一个 \
+ if (address.isEmpty()) {
+ address.append(path);
+ } else {
+ address.append("\\").append(path);
+ }
+ }
+ return address;
+ }
+
+ /**
+ * 读取目录文件
+ *
+ * @param filePath 文件路径
+ * @return 记录列表
+ * @throws Exception 读取异常
+ */
+ private List readCatalogFile(String filePath) throws Exception {
+ // 使用工厂模式创建相应的文件读取器
+ CatalogFileReader reader = CatalogFileReaderFactory.createReader(filePath);
+ return reader.readCatalogFile(filePath);
+ }
+ public record Record(String archiveCode, int page) {
+ }
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/MD5HashCalculator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/MD5HashCalculator.java
index 70fb98f..0509cdf 100644
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/core/MD5HashCalculator.java
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/MD5HashCalculator.java
@@ -1,6 +1,7 @@
package top.r3944realms.docchecktoolrefactored.core;
import java.io.IOException;
+import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
@@ -29,6 +30,69 @@ public class MD5HashCalculator implements FileHashCalculator {
}
}
+ /**
+ * 读取文件头部:
+ *
+ * - 读取文件的前partialChunkSize字节(如果文件大小小于这个值,则读取整个文件)
+ * - 这些字节会被添加到MD5计算中
+ *
+ * 读取文件中间部分:
+ *
+ * - 只有当文件大小大于partialChunkSize * 2时才会执行
+ * - 从文件中间位置前后各取partialChunkSize/2字节(总共partialChunkSize字节)
+ * - 这些字节会被追加到MD5计算中
+ *
+ * 读取文件尾部:
+ *
+ * - 只有当文件大小大于partialChunkSize时才会执行
+ * - 读取文件最后的partialChunkSize字节
+ * - 这些字节会被追加到MD5计算中
+ *
+ */
+ @Override
+ public String calculatePartialHash(Path file, int partialChunkSize) throws IOException {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("MD5");
+
+ try (var channel = Files.newByteChannel(file)) {
+ long size = channel.size();
+
+ // Read head
+ if (size > 0) {
+ int chunkSize = (int) Math.min(partialChunkSize, size);
+ ByteBuffer buffer = ByteBuffer.allocate(chunkSize);
+ channel.read(buffer);
+ buffer.flip();
+ digest.update(buffer);
+ }
+
+ // Read middle
+ if (size > partialChunkSize * 2L) {
+ long midPos = size / 2 - partialChunkSize / 2;
+ channel.position(midPos);
+ ByteBuffer buffer = ByteBuffer.allocate(partialChunkSize);
+ channel.read(buffer);
+ buffer.flip();
+ digest.update(buffer);
+ }
+
+ // Read tail
+ if (size > partialChunkSize) {
+ long endPos = size - partialChunkSize;
+ channel.position(endPos);
+ ByteBuffer buffer = ByteBuffer.allocate(partialChunkSize);
+ channel.read(buffer);
+ buffer.flip();
+ digest.update(buffer);
+ }
+ }
+
+ return bytesToHex(digest.digest());
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("MD5算法不可用", e);
+ }
+ }
+
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java b/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java
new file mode 100644
index 0000000..36ec91f
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/core/PhysicalAddressFileGenerator.java
@@ -0,0 +1,241 @@
+package top.r3944realms.docchecktoolrefactored.core;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.pdmodel.PDDocument;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+
+@Slf4j
+public class PhysicalAddressFileGenerator implements AddressFileGenerator {
+ @Override
+ public void generateAddressFile(String folderPath, File outputFile, int folderType, Callback callback) {
+ callback.onProgress("正在生成物理地址文件...");
+ try {
+ File rootFolder = new File(folderPath);
+ if (!rootFolder.exists() || !rootFolder.isDirectory()) {
+ callback.onError("所选路径不存在或不是一个有效的文件夹。");
+ return;
+ }
+
+ // 保存输出文件的绝对路径,用于后续比较
+ String outputFilePath = outputFile.getAbsolutePath();
+
+ // 写入CSV文件
+ try (PrintWriter writer = new PrintWriter(outputFile, StandardCharsets.UTF_8)) {
+ if (folderType == AddressFileGenerator.PAGE_TYPE) {
+ // 页面级逻辑:处理所有图片文件
+ writer.println("物理文件名,物理地址");
+ processPageLevelFolder(rootFolder, writer, outputFilePath);
+ } else if (folderType == AddressFileGenerator.FILE_TYPE) {
+ // 文件级逻辑:处理PDF文件
+ writer.println("物理文件名,物理地址,页数");
+ processFileLevelFolder(rootFolder, writer, outputFilePath);
+ } else {
+ throw new IllegalArgumentException("不支持的文件夹类型: " + folderType);
+ }
+ }
+
+ callback.onSuccess("物理地址文件生成成功: " + outputFile.getAbsolutePath());
+ } catch (Exception e) {
+ callback.onError("生成物理地址文件时出错: " + e.getMessage());
+ }
+ }
+ /**
+ * 处理页面级文件夹及其内部文件
+ *
+ * @param folder 文件夹
+ * @param writer PrintWriter对象
+ * @param outputFilePath 输出文件的绝对路径
+ */
+ private void processPageLevelFolder(File folder, PrintWriter writer, String outputFilePath) {
+ // 获取该文件夹下的所有非隐藏文件和文件夹
+ File[] filesAndFolders = folder.listFiles(file -> !file.isHidden());
+
+ if (filesAndFolders != null) {
+ for (File file : filesAndFolders) {
+ // 跳过输出文件本身,避免将生成的CSV文件也作为数据处理
+ if (file.getAbsolutePath().equals(outputFilePath)) {
+ continue;
+ }
+
+ if (file.isFile()) {
+ String fileName = file.getName();
+ // 只处理图片文件,跳过其他类型的文件
+ if (!isImageFile(fileName)) {
+ continue;
+ }
+
+ // 移除文件扩展名
+ String fileNameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));
+
+ // 格式化文件名,确保最后一个部分是4位数字
+ String formattedFileName = formatFileName(fileNameWithoutExt);
+
+ // 生成物理地址路径
+ String physicalAddress = generatePhysicalAddress(file.getAbsolutePath(), formattedFileName);
+
+ // 写入CSV行
+ writer.printf("%s,%s%n", formattedFileName, physicalAddress);
+ } else if (file.isDirectory()) {
+ // 递归处理子文件夹
+ processPageLevelFolder(file, writer, outputFilePath);
+ }
+ }
+ }
+ }
+
+ /**
+ * 处理文件级文件夹(处理PDF文件)
+ *
+ * @param folder 文件夹
+ * @param writer PrintWriter对象
+ * @param outputFilePath 输出文件的绝对路径
+ */
+ private void processFileLevelFolder(File folder, PrintWriter writer, String outputFilePath) {
+ // 获取该文件夹下的所有非隐藏文件和文件夹
+ File[] filesAndFolders = folder.listFiles(file -> !file.isHidden());
+
+ if (filesAndFolders != null) {
+ for (File file : filesAndFolders) {
+ // 跳过输出文件本身,避免将生成的CSV文件也作为数据处理
+ if (file.getAbsolutePath().equals(outputFilePath)) {
+ continue;
+ }
+
+ if (file.isFile()) {
+ String fileName = file.getName();
+ // 只处理PDF文件
+ if (isPdfFile(fileName)) {
+ // 移除文件扩展名
+ String fileNameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));
+
+ // 生成物理地址路径(使用与页面级相同的逻辑)
+ String physicalAddress = generatePhysicalAddress(file.getAbsolutePath(), fileNameWithoutExt);
+
+ // 获取PDF页数
+ int pageCount = getPdfPageCount(file);
+
+ // 写入CSV行
+ writer.printf("%s,%s,%d%n", fileNameWithoutExt, physicalAddress, pageCount);
+ }
+ } else if (file.isDirectory()) {
+ // 递归处理子文件夹
+ processFileLevelFolder(file, writer, outputFilePath);
+ }
+ }
+ }
+ }
+
+ /**
+ * 获取PDF文件的页数
+ *
+ * @param pdfFile PDF文件
+ * @return 页数
+ */
+ private int getPdfPageCount(File pdfFile) {
+ try {
+ // 使用Apache PDFBox库获取PDF页数
+ PDDocument document = Loader.loadPDF(pdfFile);
+ int pageCount = document.getNumberOfPages();
+ document.close();
+ return pageCount;
+ } catch (Exception e) {
+ log.warn("无法获取PDF文件页数: {}", pdfFile.getAbsolutePath(), e);
+ return 0;
+ }
+ }
+
+
+ /**
+ * 判断是否为PDF文件
+ *
+ * @param fileName 文件名
+ * @return 是否为PDF文件
+ */
+ private boolean isPdfFile(String fileName) {
+ return fileName.toLowerCase().endsWith(".pdf");
+ }
+
+ /**
+ * 判断是否为图片文件
+ *
+ * @param fileName 文件名
+ * @return 是否为图片文件
+ */
+ private boolean isImageFile(String fileName) {
+ String lowerFileName = fileName.toLowerCase();
+ return lowerFileName.endsWith(".jpg") || lowerFileName.endsWith(".jpeg") ||
+ lowerFileName.endsWith(".png") || lowerFileName.endsWith(".bmp") ||
+ lowerFileName.endsWith(".gif") || lowerFileName.endsWith(".tiff");
+ }
+
+ /**
+ * 格式化文件名,确保最后一个部分是4位数字
+ *
+ * @param fileName 文件名(不含扩展名)
+ * @return 格式化后的文件名
+ */
+ private String formatFileName(String fileName) {
+ int lastDashIndex = fileName.lastIndexOf('-');
+ if (lastDashIndex != -1 && lastDashIndex < fileName.length() - 1) {
+ String lastPart = fileName.substring(lastDashIndex + 1);
+ // 检查是否为数字
+ if (lastPart.matches("\\d+")) {
+ // 格式化为4位数字
+ int number = Integer.parseInt(lastPart);
+ String formattedLastPart = String.format("%04d", number);
+ return fileName.substring(0, lastDashIndex + 1) + formattedLastPart;
+ }
+ }
+ return fileName;
+ }
+
+ /**
+ * 生成物理地址路径
+ *
+ * @param absolutePath 文件的绝对路径
+ * @param fileName 文件名(不含扩展名)
+ * @return 物理地址路径
+ */
+ private String generatePhysicalAddress(String absolutePath, String fileName) {
+ // 获取文件名第一个'-'前面的内容(如9027)
+ int firstDashIndex = fileName.indexOf('-');
+ String prefix = firstDashIndex != -1 ? fileName.substring(0, firstDashIndex) : fileName;
+
+ // 构建从根路径开始的相对路径
+ String relativePath = absolutePath.replace('\\', '/');
+ String[] pathParts = relativePath.split("/");
+
+ StringBuilder resultPath = new StringBuilder();
+ boolean foundPrefix = false;
+
+ for (int i = 0; i < pathParts.length; i++) {
+ if (!foundPrefix) {
+ // 检查当前部分是否包含prefix
+ if (pathParts[i].contains(prefix)) {
+ foundPrefix = true;
+ resultPath.append(pathParts[i]);
+ }
+ } else {
+ // 去掉文件类型的后缀(如.jpg)
+ if (i == pathParts.length - 1) {
+ String fileNameWithoutExt = pathParts[i].substring(0, pathParts[i].lastIndexOf('.'));
+ resultPath.append("/").append(fileNameWithoutExt);
+ } else {
+ resultPath.append("/").append(pathParts[i]);
+ }
+ }
+ }
+
+ // 如果结果路径为空,返回空字符串
+ if (resultPath.isEmpty()) {
+ return "";
+ }
+
+ // 将路径中的 / 替换为 \(适用于 Windows 系统)
+ return resultPath.toString().replace('/', '\\');
+ }
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/BaseFunctionPanel.java b/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/BaseFunctionPanel.java
deleted file mode 100644
index b8549a2..0000000
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/BaseFunctionPanel.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package top.r3944realms.docchecktoolrefactored.deprecated;
-
-import javafx.scene.control.TextArea;
-import javafx.scene.layout.Pane;
-import javafx.scene.paint.Color;
-
-public abstract class BaseFunctionPanel extends Pane {
- protected DocCheckToolMainFrame mainFrame;
- protected TextArea resultTextArea;
- protected static final Color PRIMARY_COLOR = Color.rgb(66, 133, 244);
- protected static final Color SECONDARY_COLOR = Color.rgb(30, 169, 80);
- protected static final Color BACKGROUND_COLOR = Color.rgb(245, 245, 245);
- protected static final Color BORDER_COLOR = Color.rgb(222, 226, 230);
-
- public BaseFunctionPanel(DocCheckToolMainFrame mainFrame) {
- this.mainFrame = mainFrame;
- setStyle("-fx-background-color: rgb(245,245,245);");
- initComponents();
- }
-
- protected abstract void initComponents();
-
- public void showStatusMessage(String message) {
- if (resultTextArea != null) {
- resultTextArea.appendText(message + "\n");
- resultTextArea.positionCaret(resultTextArea.getText().length());
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/DocCheckToolMainFrame.java b/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/DocCheckToolMainFrame.java
deleted file mode 100644
index 46d9a77..0000000
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/DocCheckToolMainFrame.java
+++ /dev/null
@@ -1,108 +0,0 @@
-package top.r3944realms.docchecktoolrefactored.deprecated;
-
-import javafx.geometry.Insets;
-import javafx.geometry.Pos;
-import javafx.scene.Scene;
-import javafx.scene.control.Alert;
-import javafx.scene.control.Button;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.layout.HBox;
-import javafx.scene.layout.StackPane;
-import javafx.scene.paint.Color;
-import javafx.scene.text.Font;
-import javafx.stage.Stage;
-
-public class DocCheckToolMainFrame {
- private StackPane cardPanel;
- private static final Color PRIMARY_COLOR = Color.rgb(66, 133, 244);
- private static final Color BACKGROUND_COLOR = Color.rgb(245, 245, 245);
-
- public void show(Stage primaryStage) {
- primaryStage.setTitle("淮阴区数字化档案检查验收系统");
- primaryStage.setWidth(1200);
- primaryStage.setHeight(600);
- primaryStage.setResizable(false);
-
- // Create top button panel
- HBox topButtonPanel = new HBox(10);
- topButtonPanel.setPadding(new Insets(10));
- topButtonPanel.setAlignment(Pos.CENTER_LEFT);
- topButtonPanel.setStyle("-fx-background-color: rgb(245,245,245);");
-
- String[] buttonTexts = {"1 查重复文件", "2 查遗漏、存储路径和命名规范", "3 查质量",
- "4 元数据", "5 查数据挂接", "6 查工作文件", "7 查存储载体"};
-
- for (String text : buttonTexts) {
- Button btn = new Button(text);
- btn.setFont(Font.font("Microsoft YaHei", 14));
- btn.setTextFill(Color.WHITE);
- btn.setStyle("-fx-background-color: rgb(66,133,244); -fx-padding: 5 15 5 15;");
- btn.setOnAction(e -> switchPanel(text));
- topButtonPanel.getChildren().add(btn);
- }
-
- // Initialize card panel
- cardPanel = new StackPane();
- cardPanel.setStyle("-fx-background-color: rgb(245,245,245);");
-
- // Add all function panels to card panel
- cardPanel.getChildren().addAll(
- new DuplicateCheckPanel(this),
- new PathCheckPanel(this),
- new OtherFunctionPanel(" 3 查质量"),
- new OtherFunctionPanel("4 元数据"),
- new OtherFunctionPanel("5 查数据挂接"),
- new OtherFunctionPanel("6 查工作文件"),
- new OtherFunctionPanel("7 查存储载体")
- );
-
- // Initially show the first panel
- switchPanel("1 查重复文件");
-
- // Create main layout
- BorderPane root = new BorderPane();
- root.setTop(topButtonPanel);
- root.setCenter(cardPanel);
- root.setStyle("-fx-background-color: rgb(245,245,245);");
-
- primaryStage.setScene(new Scene(root));
- primaryStage.show();
- }
-
- private void switchPanel(String panelName) {
- for (javafx.scene.Node node : cardPanel.getChildren()) {
- node.setVisible(false);
- }
-
- int index = switch (panelName) {
- case "1 查重复文件" -> 0;
- case "2 查遗漏、存储路径和命名规范" -> 1;
- case "3 查质量" -> 2;
- case "4 元数据" -> 3;
- case "5 查数据挂接" -> 4;
- case "6 查工作文件" -> 5;
- case "7 查存储载体" -> 6;
- default -> 0;
- };
-
- cardPanel.getChildren().get(index).setVisible(true);
- }
-
- public void showStatusMessage(String message) {
- for (javafx.scene.Node node : cardPanel.getChildren()) {
- if (node instanceof BaseFunctionPanel && node.isVisible()) {
- ((BaseFunctionPanel) node).showStatusMessage(message);
- break;
- }
- }
- }
-
- public void showErrorMessage(String message) {
- Alert alert = new Alert(Alert.AlertType.ERROR);
- alert.setTitle("错误");
- alert.setHeaderText(null);
- alert.setContentText(message);
- alert.showAndWait();
- showStatusMessage("【错误】" + message);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/DuplicateCheckPanel.java b/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/DuplicateCheckPanel.java
deleted file mode 100644
index b9541b6..0000000
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/DuplicateCheckPanel.java
+++ /dev/null
@@ -1,166 +0,0 @@
-package top.r3944realms.docchecktoolrefactored.deprecated;
-
-import javafx.geometry.Insets;
-import javafx.geometry.Pos;
-import javafx.scene.control.*;
-import javafx.scene.layout.HBox;
-import javafx.scene.layout.Priority;
-import javafx.scene.layout.VBox;
-import javafx.scene.paint.Color;
-import javafx.scene.text.Font;
-import javafx.scene.text.FontWeight;
-import javafx.stage.DirectoryChooser;
-import javafx.stage.Stage;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.*;
-
-public class DuplicateCheckPanel extends BaseFunctionPanel {
- private TextField folderPathTextField;
- private TextArea feedbackTextArea;
- private Set duplicateFolders = new HashSet<>();
-
- public DuplicateCheckPanel(DocCheckToolMainFrame mainFrame) {
- super(mainFrame);
- initComponents();
- }
-
- protected void initComponents() {
- // Main layout with padding
- this.setPadding(new Insets(20));
-
- // Folder selection components
- HBox folderSelectBox = new HBox(10);
- folderSelectBox.setAlignment(Pos.CENTER_LEFT);
-
- Label loadFolderLabel = new Label("载入文件夹");
- loadFolderLabel.setFont(Font.font("Microsoft YaHei", 14));
- loadFolderLabel.setTextFill(Color.DARKGRAY);
-
- folderPathTextField = new TextField();
- folderPathTextField.setEditable(false);
- folderPathTextField.setFont(Font.font("Microsoft YaHei", 14));
- folderPathTextField.setStyle("-fx-background-color: white; -fx-border-color: #CCCCCC; -fx-border-width: 1; -fx-padding: 5 10 5 10;");
- HBox.setHgrow(folderPathTextField, Priority.ALWAYS);
-
- Button selectFolderButton = new Button("选择文件夹");
- selectFolderButton.setFont(Font.font("Microsoft YaHei", 14));
- selectFolderButton.setTextFill(Color.WHITE);
- selectFolderButton.setStyle("-fx-background-color: #4285F4; -fx-padding: 5 15 5 15;");
- selectFolderButton.setOnAction(e -> {
- DirectoryChooser directoryChooser = new DirectoryChooser();
- directoryChooser.setTitle("选择要检查的文件夹");
- File selectedFolder = directoryChooser.showDialog(new Stage());
- if (selectedFolder != null) {
- folderPathTextField.setText(selectedFolder.getAbsolutePath());
- }
- });
-
- folderSelectBox.getChildren().addAll(loadFolderLabel, folderPathTextField, selectFolderButton);
-
- Button startCheckButton = new Button("开始检查");
- startCheckButton.setFont(Font.font("Microsoft YaHei", FontWeight.BOLD, 14));
- startCheckButton.setTextFill(Color.WHITE);
- startCheckButton.setStyle("-fx-background-color: #4285F4; -fx-padding: 5 20 5 20;");
- startCheckButton.setOnAction(e -> {
- String folderPath = folderPathTextField.getText();
- if (!folderPath.isEmpty()) {
- feedbackTextArea.setText("");
- File folder = new File(folderPath);
- duplicateFolders.clear();
- checkDuplicateFiles(folder);
- if (duplicateFolders.isEmpty()) {
- feedbackTextArea.setText("反馈结果:\n如无重复文件,则反馈:无重复文件;\n如有重复文件,则在此反馈档号。\n\n当前:无重复文件");
- } else {
- StringBuilder result = new StringBuilder("反馈结果:\n如无重复文件,则反馈:无重复文件;\n如有重复文件,则在此反馈档号。\n\n当前:");
- for (String folderName : duplicateFolders) {
- result.append("\n").append(folderName);
- }
- feedbackTextArea.setText(result.toString());
- }
- } else {
- mainFrame.showErrorMessage("请先选择一个文件夹!");
- }
- });
-
- // Feedback area
- feedbackTextArea = new TextArea();
- feedbackTextArea.setEditable(false);
- feedbackTextArea.setFont(Font.font("Microsoft YaHei", 14));
- feedbackTextArea.setText("反馈结果:\n如无重复文件,则反馈:无重复文件;\n如有重复文件,则在此反馈档号。");
- feedbackTextArea.setStyle("-fx-background-color: white; -fx-border-color: #CCCCCC; -fx-border-width: 1; -fx-padding: 10;");
-
- ScrollPane scrollPane = new ScrollPane(feedbackTextArea);
- scrollPane.setFitToWidth(true);
- scrollPane.setFitToHeight(true);
- scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
-
- // Main layout
- VBox mainBox = new VBox(10);
- mainBox.getChildren().addAll(folderSelectBox, startCheckButton, scrollPane);
- this.getChildren().add(mainBox);
-
- // Set result text area reference
- resultTextArea = feedbackTextArea;
- }
-
- /**
- * Recursively checks for duplicate fileMetadata in the specified folder
- */
- private void checkDuplicateFiles(File folder) {
- Map> fileHashMap = new HashMap<>();
- if (folder.isDirectory()) {
- File[] files = folder.listFiles();
- if (files != null) {
- for (File file : files) {
- if (file.isDirectory()) {
- checkDuplicateFiles(file);
- } else {
- try {
- String md5Hash = calculateMD5(file);
- fileHashMap.computeIfAbsent(md5Hash, k -> new ArrayList<>()).add(file);
- } catch (IOException | NoSuchAlgorithmException ex) {
- ex.printStackTrace();
- mainFrame.showErrorMessage("计算文件哈希值出错:" + ex.getMessage());
- }
- }
- }
- }
- }
-
- for (List fileList : fileHashMap.values()) {
- if (fileList.size() > 1) {
- for (File file : fileList) {
- File parentFolder = file.getParentFile();
- if (parentFolder != null) {
- duplicateFolders.add(parentFolder.getName());
- }
- }
- }
- }
- }
-
- /**
- * Calculates the MD5 hash of a file
- */
- private String calculateMD5(File file) throws IOException, NoSuchAlgorithmException {
- MessageDigest digest = MessageDigest.getInstance("MD5");
- try (FileInputStream fis = new FileInputStream(file)) {
- byte[] buffer = new byte[8192];
- int bytesRead;
- while ((bytesRead = fis.read(buffer)) != -1) {
- digest.update(buffer, 0, bytesRead);
- }
- }
- byte[] hashBytes = digest.digest();
- StringBuilder sb = new StringBuilder();
- for (byte b : hashBytes) {
- sb.append(String.format("%02x", b));
- }
- return sb.toString();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/OtherFunctionPanel.java b/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/OtherFunctionPanel.java
deleted file mode 100644
index 8808a52..0000000
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/OtherFunctionPanel.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package top.r3944realms.docchecktoolrefactored.deprecated;
-
-import javafx.geometry.Insets;
-import javafx.geometry.Pos;
-import javafx.scene.control.Label;
-import javafx.scene.layout.VBox;
-import javafx.scene.text.Font;
-
-public class OtherFunctionPanel extends BaseFunctionPanel {
- private String title;
-
- public OtherFunctionPanel(String title) {
- super(null);
- this.title = title;
- initComponents();
- }
-
- @Override
- protected void initComponents() {
- // Set padding
- this.setPadding(new Insets(50));
-
- //Create tip label
- Label tipLabel = new Label("【" + "" +"】功能界面待完善..."+title);
- tipLabel.setFont(Font.font("Microsoft YaHei", 28));
- tipLabel.setTextFill(javafx.scene.paint.Color.DARKGRAY);
-
- // Use VBox for vertical layout and center alignment
- VBox container = new VBox(tipLabel);
- container.setAlignment(Pos.CENTER);
-
- // Add to the panel
- this.getChildren().add(container);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/PathCheckPanel.java b/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/PathCheckPanel.java
deleted file mode 100644
index be88294..0000000
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/deprecated/PathCheckPanel.java
+++ /dev/null
@@ -1,287 +0,0 @@
-package top.r3944realms.docchecktoolrefactored.deprecated;
-
-import javafx.geometry.Insets;
-import javafx.geometry.Pos;
-import javafx.scene.control.*;
-import javafx.scene.layout.HBox;
-import javafx.scene.layout.VBox;
-import javafx.scene.paint.Color;
-import javafx.scene.text.Font;
-import javafx.scene.text.FontWeight;
-import javafx.stage.DirectoryChooser;
-import javafx.stage.Stage;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileWriter;
-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.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-public class PathCheckPanel extends BaseFunctionPanel {
- private TextField physicalFolderPathTextField;
- private TextField logicalFolderPathTextField;
- private TextArea checkResultTextArea;
- private Path physicalFolderPath;
- private Path logicalFolderPath;
- private List physicalPaths = new ArrayList<>();
- private List logicalPaths = new ArrayList<>();
-
- public PathCheckPanel(DocCheckToolMainFrame mainFrame) {
- super(mainFrame);
- initComponents();
- }
-
- @Override
- protected void initComponents() {
- // Main layout with spacing
- VBox mainLayout = new VBox(15);
- mainLayout.setPadding(new Insets(20));
-
- // Physical path selection
- TitledPane physicalPathPane = createPathPane(
- "物理存储路径",
- "选择物理文件夹",
- "生成物理存储路径文件",
- this::selectPhysicalFolder,
- this::generatePhysicalPathFile
- );
- physicalFolderPathTextField = (TextField) ((HBox) physicalPathPane.getContent()).getChildren().get(0);
-
- // Logical path selection
- TitledPane logicalPathPane = createPathPane(
- "逻辑存储路径",
- "选择逻辑文件夹",
- "生成逻辑存储路径文件",
- this::selectLogicalFolder,
- this::generateLogicalPathFile
- );
- logicalFolderPathTextField = (TextField) ((HBox) logicalPathPane.getContent()).getChildren().get(0);
-
- // Check buttons
- HBox buttonBox = new HBox(15);
- buttonBox.setAlignment(Pos.CENTER);
-
- Button checkFilesButton = new Button("检查文件");
- stylePrimaryButton(checkFilesButton, 16);
- checkFilesButton.setOnAction(e -> checkFiles());
-
- Button checkDirButton = new Button("检查目录");
- checkDirButton.setFont(Font.font("Microsoft YaHei", FontWeight.BOLD, 16));
- checkDirButton.setTextFill(Color.WHITE);
- checkDirButton.setStyle("-fx-background-color: #ef4444; -fx-border-color: #dc2626; -fx-border-width: 2; -fx-padding: 8 30 8 30;");
- checkDirButton.setOnAction(e -> checkDirectory());
-
- buttonBox.getChildren().addAll(checkFilesButton, checkDirButton);
-
- // Result area
- checkResultTextArea = new TextArea();
- checkResultTextArea.setEditable(false);
- checkResultTextArea.setFont(Font.font("Microsoft YaHei", 14));
- checkResultTextArea.setText("检查结果将显示在这里...\n请先选择文件夹并生成路径文件");
- checkResultTextArea.setStyle("-fx-background-color: white; -fx-border-color: #dee2e6; -fx-border-width: 1; -fx-padding: 10;");
-
- ScrollPane scrollPane = new ScrollPane(checkResultTextArea);
- scrollPane.setFitToWidth(true);
- scrollPane.setFitToHeight(true);
- scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
-
- // Assemble main layout
- VBox pathPanes = new VBox(15, physicalPathPane, logicalPathPane);
- mainLayout.getChildren().addAll(pathPanes, scrollPane, buttonBox);
- this.getChildren().add(mainLayout);
- }
-
- private TitledPane createPathPane(String title, String selectBtnText, String generateBtnText,
- Runnable selectAction, Runnable generateAction) {
- TextField pathField = new TextField();
- pathField.setEditable(false);
- pathField.setFont(Font.font("Microsoft YaHei", 14));
- pathField.setStyle("-fx-background-color: white; -fx-border-color: #dee2e6; -fx-border-width: 1; -fx-padding: 5 10 5 10;");
-
- Button selectButton = new Button(selectBtnText);
- stylePrimaryButton(selectButton, 14);
- selectButton.setOnAction(e -> selectAction.run());
-
- Button generateButton = new Button(generateBtnText);
- stylePrimaryButton(generateButton, 14);
- generateButton.setDisable(true);
- generateButton.setOnAction(e -> generateAction.run());
-
- HBox content = new HBox(10, pathField, selectButton, generateButton);
- content.setAlignment(Pos.CENTER_LEFT);
-
- TitledPane pane = new TitledPane(title, content);
- pane.setFont(Font.font("Microsoft YaHei", 14));
- pane.setStyle("-fx-border-color: #dee2e6; -fx-border-width: 1;");
-
- return pane;
- }
-
- private void stylePrimaryButton(Button button, double fontSize) {
- button.setFont(Font.font("Microsoft YaHei", FontWeight.BOLD, fontSize));
- button.setTextFill(Color.WHITE);
- button.setStyle("-fx-background-color: #4285f4; -fx-border-color: #3b71ca; -fx-border-width: 2; -fx-padding: 8 30 8 30;");
- }
-
- private void selectPhysicalFolder() {
- DirectoryChooser chooser = new DirectoryChooser();
- chooser.setTitle("选择物理存储文件夹");
- File selectedFolder = chooser.showDialog(new Stage());
- if (selectedFolder != null) {
- physicalFolderPathTextField.setText(selectedFolder.getAbsolutePath());
- physicalFolderPath = selectedFolder.toPath();
-
- try {
- collectFilePaths(physicalFolderPath, physicalPaths);
- showStatusMessage("已收集 " + physicalPaths.size() + " 个物理文件路径");
- enableGenerateButton(physicalFolderPathTextField, true);
- } catch (IOException e) {
- mainFrame.showErrorMessage("读取物理文件夹失败:" + e.getMessage());
- }
- }
- }
-
- private void selectLogicalFolder() {
- DirectoryChooser chooser = new DirectoryChooser();
- chooser.setTitle("选择逻辑存储文件夹");
- File selectedFolder = chooser.showDialog(new Stage());
- if (selectedFolder != null) {
- logicalFolderPathTextField.setText(selectedFolder.getAbsolutePath());
- logicalFolderPath = selectedFolder.toPath();
-
- try {
- collectFilePaths(logicalFolderPath, logicalPaths);
- showStatusMessage("已收集 " + logicalPaths.size() + " 个逻辑文件路径");
- enableGenerateButton(logicalFolderPathTextField, true);
- } catch (IOException e) {
- mainFrame.showErrorMessage("读取逻辑文件夹失败:" + e.getMessage());
- }
- }
- }
-
- private void enableGenerateButton(TextField pathField, boolean enable) {
- // Implementation would find the generate button and enable it
- // This depends on your exact UI structure
- }
-
- private void generatePhysicalPathFile() {
- if (physicalFolderPath == null || physicalPaths.isEmpty()) {
- mainFrame.showErrorMessage("请先选择有效的物理文件夹!");
- return;
- }
-
- Path outputPath = Paths.get("物理存储路径文件.txt");
- writePathsToFile(physicalPaths, outputPath);
- showStatusMessage("物理存储路径文件已生成:" + outputPath.toAbsolutePath());
- }
-
- private void generateLogicalPathFile() {
- if (logicalFolderPath == null || logicalPaths.isEmpty()) {
- mainFrame.showErrorMessage("请先选择有效的逻辑文件夹!");
- return;
- }
-
- Path outputPath = Paths.get("逻辑存储路径文件.txt");
- writePathsToFile(logicalPaths, outputPath);
- showStatusMessage("逻辑存储路径文件已生成:" + outputPath.toAbsolutePath());
- }
-
- // The following methods remain largely unchanged as they deal with file operations:
- private void collectFilePaths(Path folderPath, List paths) throws IOException {
- paths.clear();
- try (Stream walk = Files.walk(folderPath)) {
- paths.addAll(walk
- .filter(Files::isRegularFile)
- .map(p -> folderPath.relativize(p).toString())
- .collect(Collectors.toList()));
- }
- }
-
- private void writePathsToFile(List paths, Path outputPath) {
- try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputPath.toFile()))) {
- for (String path : paths) {
- writer.write(path);
- writer.newLine();
- }
- } catch (IOException e) {
- mainFrame.showErrorMessage("写入文件失败:" + e.getMessage());
- }
- }
-
- private void checkFiles() {
- if (physicalPaths.isEmpty() || logicalPaths.isEmpty()) {
- mainFrame.showErrorMessage("请先选择并生成物理和逻辑路径文件!");
- return;
- }
-
- clearResultArea();
- showStatusMessage("正在进行文件检查...");
-
- Set missingInPhysical = new HashSet<>();
- for (String logicalPath : logicalPaths) {
- if (!physicalPaths.contains(logicalPath)) {
- missingInPhysical.add(logicalPath);
- }
- }
-
- displayCheckResult("文件检查", logicalPaths.size(), missingInPhysical, null);
- }
-
- private void checkDirectory() {
- if (physicalPaths.isEmpty() || logicalPaths.isEmpty()) {
- mainFrame.showErrorMessage("请先选择并生成物理和逻辑路径文件!");
- return;
- }
-
- clearResultArea();
- showStatusMessage("正在进行目录检查...");
-
- Set missingInLogical = new HashSet<>();
- for (String physicalPath : physicalPaths) {
- if (!logicalPaths.contains(physicalPath)) {
- missingInLogical.add(physicalPath);
- }
- }
-
- displayCheckResult("目录检查", physicalPaths.size(), null, missingInLogical);
- }
-
- private void displayCheckResult(String checkType, int totalCount,
- Set missingInPhysical, Set missingInLogical) {
- StringBuilder result = new StringBuilder();
- result.append("=== ").append(checkType).append(" ===\n");
- result.append("共检查 ").append(totalCount).append(" 个").append(checkType.equals("文件检查") ? "文件" : "目录项").append("\n\n");
-
- if (checkType.equals("文件检查")) {
- if (missingInPhysical.isEmpty()) {
- result.append("✓ 未发现问题:所有逻辑文件在物理存储中均存在\n");
- } else {
- result.append("! 发现 ").append(missingInPhysical.size()).append(" 个问题:\n");
- missingInPhysical.forEach(path -> result.append(" - 物理存储中缺失文件:").append(path).append("\n"));
- }
- } else {
- if (missingInLogical.isEmpty()) {
- result.append("✓ 未发现问题:所有物理文件在逻辑目录中均存在\n");
- } else {
- result.append("! 发现 ").append(missingInLogical.size()).append(" 个问题:\n");
- missingInLogical.forEach(path -> result.append(" - 逻辑目录中缺失文件:").append(path).append("\n"));
- }
- }
-
- checkResultTextArea.appendText(result.toString());
- checkResultTextArea.appendText("\n\n");
- showStatusMessage(checkType + "完成");
- }
-
- private void clearResultArea() {
- checkResultTextArea.clear();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/CatalogFileReader.java b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/CatalogFileReader.java
new file mode 100644
index 0000000..1e5e6e6
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/CatalogFileReader.java
@@ -0,0 +1,20 @@
+package top.r3944realms.docchecktoolrefactored.io.reader;
+
+
+import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
+
+import java.util.List;
+
+/**
+ * 目录文件读取器接口
+ */
+public interface CatalogFileReader {
+ /**
+ * 读取目录文件
+ *
+ * @param filePath 文件路径
+ * @return 记录列表
+ * @throws Exception 读取异常
+ */
+ List readCatalogFile(String filePath) throws Exception;
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/CatalogFileReaderFactory.java b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/CatalogFileReaderFactory.java
new file mode 100644
index 0000000..d821a5f
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/CatalogFileReaderFactory.java
@@ -0,0 +1,29 @@
+package top.r3944realms.docchecktoolrefactored.io.reader;
+
+/**
+ * 目录文件读取器工厂类
+ */
+public class CatalogFileReaderFactory {
+ /**
+ * 根据文件扩展名创建相应的文件读取器
+ *
+ * @param filePath 文件路径
+ * @return 文件读取器实例
+ */
+ public static CatalogFileReader createReader(String filePath) {
+ if (filePath == null || filePath.isEmpty()) {
+ throw new IllegalArgumentException("文件路径不能为空");
+ }
+
+ String lowerPath = filePath.toLowerCase();
+ if (lowerPath.endsWith(".xlsx") || lowerPath.endsWith(".xls")) {
+ return new ExcelFileReader();
+ } else if (lowerPath.endsWith(".xml")) {
+ return new XmlFileReader();
+ } else if (lowerPath.endsWith(".dbf")) {
+ return new DbfFileReader();
+ } else {
+ throw new IllegalArgumentException("不支持的文件格式: " + filePath);
+ }
+ }
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/DbfFileReader.java b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/DbfFileReader.java
new file mode 100644
index 0000000..9e7a6c3
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/DbfFileReader.java
@@ -0,0 +1,112 @@
+package top.r3944realms.docchecktoolrefactored.io.reader;
+
+import com.linuxense.javadbf.DBFReader;
+import com.linuxense.javadbf.DBFRow;
+import lombok.extern.slf4j.Slf4j;
+import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
+import top.r3944realms.docchecktoolrefactored.util.FileUtil;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * DBF文件读取器实现
+ */
+@Slf4j
+public class DbfFileReader implements CatalogFileReader {
+ // 常量定义(避免硬编码)
+ private static final String FIELD_ARCHIVE_CODE = "档号";
+ private static final String FIELD_PAGE = "页数";
+
+ @Override
+ public List readCatalogFile(String filePath) throws Exception {
+
+ List records = new ArrayList<>();
+ File file = FileUtil.fileCheckAndGet(filePath, "dbf");
+ try (
+ FileInputStream fis = new FileInputStream(file);
+ DBFReader reader = new DBFReader(fis)
+ ) {
+ int fieldCount = reader.getFieldCount();
+ log.debug("开始读取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);
+ break;
+ }
+ String fieldName = reader.getField(i).getName();
+ log.debug("发现字段: {}", fieldName);
+ if (FIELD_ARCHIVE_CODE.equals(fieldName)) {
+ archiveCodeIndex = i;
+ log.debug("找到‘档号’字段,索引: {}", archiveCodeIndex);
+ } else if (FIELD_PAGE.equals(fieldName)) {
+ pageIndex = i;
+ log.debug("找到‘页数’字段,索引: {}", pageIndex);
+ }
+
+ }
+ if (archiveCodeIndex == -1 || pageIndex == -1) {
+ log.error("未找到必要字段,档号: {}, 页数: {}",
+ 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
+ )
+ );
+ }
+ int validRecords = 0;
+ int skippedRecords = 0;
+ DBFRow row;
+ while ((row = reader.nextRow()) != null) {
+ String archiveCode =
+ Optional.ofNullable(row.getObject(archiveCodeIndex))
+ .map(Object::toString)
+ .map(String::trim)
+ .orElse("");
+ int page =
+ Optional.ofNullable(row.getObject(pageIndex))
+ .map(i -> {
+ try {
+ if (i instanceof Number) {
+ return ((Number) i).intValue();
+ } else {
+ return Integer.parseInt(i.toString().trim());
+ }
+ } catch (NumberFormatException e) {
+ log.warn("无法将页数值转换为整数: {}", i);
+ return 0;
+ }
+ }).orElse(0);
+
+ // 只有当档号不为空且页数大于0时才添加记录
+ if (!archiveCode.isEmpty() && page > 0) {
+ records.add(new LogicalAddressFileGenerator.Record(archiveCode, page));
+ validRecords++;
+ log.debug("读取有效记录 - 档号: {}, 页数: {}", archiveCode, page);
+ } else {
+ skippedRecords++;
+ if (!archiveCode.isEmpty() || page > 0) {
+ log.debug("跳过无效记录 - 档号: {}, 页数: {}", archiveCode, page);
+ }
+ }
+ }
+ log.info("DBF文件读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords);
+ } catch (IOException e) {
+ log.error("读取DBF文件失败: {}", filePath, e);
+ throw new UncheckedIOException("DBF文件读取异常", e);
+ }
+ return records;
+ }
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/ExcelFileReader.java b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/ExcelFileReader.java
new file mode 100644
index 0000000..a0b3857
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/ExcelFileReader.java
@@ -0,0 +1,195 @@
+package top.r3944realms.docchecktoolrefactored.io.reader;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+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 java.io.File;
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+public class ExcelFileReader implements CatalogFileReader {
+ // 常量定义(避免硬编码)
+ private static final String FIELD_ARCHIVE_CODE = "档号";
+ private static final String FIELD_PAGE = "页数";
+
+ @Override
+ public List readCatalogFile(String filePath) throws Exception {
+ File file = FileUtil.fileCheckAndGet(filePath, "xlsx", "xls");
+ boolean isXlsx = file.getName().endsWith(".xlsx");
+ return readExcelFile(file, isXlsx);
+ }
+ private List readExcelFile(File file, boolean isXlsx) throws Exception {
+ List records = new ArrayList<>();
+ log.debug("开始解析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());
+
+ // 获取标题行
+ Row headerRow = sheet.getRow(0);
+ if (headerRow == null) {
+ log.error("Excel文件缺少标题行");
+ throw new IllegalArgumentException("Excel文件缺少标题行");
+ }
+ // 查找"档号"和"页数"列的索引
+ int archiveCodeIndex = -1;
+ int pageIndex = -1;
+ log.debug("开始查找'档号'和'页数'列的索引");
+ boolean foundExactMatch = false;
+ for (Cell cell : headerRow) {
+ String cellValue = getCellValueAsString(cell).trim();
+ if (FIELD_ARCHIVE_CODE.equals(cellValue)) {
+ archiveCodeIndex = cell.getColumnIndex();
+ log.debug("找到'档号'列,索引: {}", archiveCodeIndex);
+ } else if (FIELD_PAGE.equals(cellValue)) {
+ pageIndex = cell.getColumnIndex();
+ foundExactMatch = true;
+ log.debug("找到精确匹配'页数'列,索引: {}", pageIndex);
+ }
+ }
+ // 如果没有精确匹配,进行模糊查找
+ if (!foundExactMatch) {
+ for (Cell cell : headerRow) {
+ String cellValue = getCellValueAsString(cell).trim();
+ if (cellValue.contains(FIELD_PAGE)) {
+ pageIndex = cell.getColumnIndex();
+ log.debug("找到模糊匹配'页数'列,索引: {}", pageIndex);
+ }
+ }
+ }
+ // 检查是否找到必需的列
+ if (archiveCodeIndex == -1 || pageIndex == -1) {
+ log.error("未找到必要字段,档号: {}, 页数: {}",
+ archiveCodeIndex == -1 ? "未找到" : archiveCodeIndex,
+ pageIndex == -1 ? "未找到" : pageIndex
+ );
+ throw new IllegalArgumentException(
+ String.format("Excel文件缺少必要字段: %s=%s, %s=%s",
+ FIELD_ARCHIVE_CODE, archiveCodeIndex == -1,
+ FIELD_PAGE, pageIndex == -1
+ )
+ );
+ }
+ // 从第二行开始读取数据(跳过标题行)
+ int totalRows = sheet.getLastRowNum();
+ int validRecords = 0;
+ int skippedRecords = 0;
+
+ for (int i = 1; i <= totalRows; i++) {
+ Row row = sheet.getRow(i);
+ if (row == null) {
+ skippedRecords++;
+ continue;
+ }
+
+ // 读取档号
+ Cell archiveCodeCell = row.getCell(archiveCodeIndex);
+ String archiveCode = "";
+ if (archiveCodeCell != null) {
+ archiveCode = getCellValueAsString(archiveCodeCell).trim();
+ }
+
+ // 读取页数
+ Cell pageCell = row.getCell(pageIndex);
+ int page = 0;
+ if (pageCell != null) {
+ page = getCellValueAsInteger(pageCell);
+ }
+
+ // 验证数据有效性
+ if (archiveCode.isEmpty()) {
+ throw new IllegalArgumentException("第" + ( i + 1 ) + "行档号为空,停止处理");
+ }
+ if (page <= 0) {
+ throw new IllegalArgumentException("第" + ( i + 1 ) + "行页数无效: " + page + ",停止处理");
+ }
+
+ // 只有数据有效时才添加记录
+ records.add(new LogicalAddressFileGenerator.Record(archiveCode, page));
+ validRecords++;
+ log.debug("读取有效记录 - 档号: {}, 页数: {}", archiveCode, page);
+ }
+
+ log.info("数据读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords);
+ } catch (Exception e) {
+ log.error("读取Excel文件时发生错误: {}", e.getMessage(), e);
+ throw e;
+ }
+ return records;
+ }
+ /**
+ * 获取单元格值作为字符串
+ * @param cell 单元格
+ * @return 字符串值
+ */
+ private String getCellValueAsString(Cell cell) {
+ if (cell == null) {
+ return "";
+ }
+
+ switch (cell.getCellType()) {
+ case STRING:
+ return cell.getStringCellValue();
+ case NUMERIC:
+ // 判断是否为日期格式
+ if (DateUtil.isCellDateFormatted(cell)) {
+ return cell.getDateCellValue().toString();
+ } else {
+ // 数值转换为字符串,避免科学计数法
+ double numericValue = cell.getNumericCellValue();
+ if (numericValue == Math.floor(numericValue)) {
+ return String.valueOf((int) numericValue);
+ } else {
+ return String.valueOf(numericValue);
+ }
+ }
+ case BOOLEAN:
+ return String.valueOf(cell.getBooleanCellValue());
+ case FORMULA:
+ try {
+ return cell.getStringCellValue();
+ } catch (Exception e) {
+ return cell.getCellFormula();
+ }
+ default:
+ return "";
+ }
+ }
+
+ /**
+ * 获取单元格值作为整数
+ * @param cell 单元格
+ * @return 整数值
+ */
+ private int getCellValueAsInteger(Cell cell) {
+ if (cell == null) {
+ return 0;
+ }
+
+ try {
+ switch (cell.getCellType()) {
+ case NUMERIC:
+ return (int) Math.round(cell.getNumericCellValue());
+ case STRING:
+ String stringValue = cell.getStringCellValue().trim();
+ if (!stringValue.isEmpty()) {
+ return Integer.parseInt(stringValue);
+ }
+ return 0;
+ default:
+ return 0;
+ }
+ } catch (NumberFormatException e) {
+ log.warn("无法将单元格值转换为整数: {}", cell);
+ return 0;
+ }
+ }
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/FileReader.java b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/FileReader.java
deleted file mode 100644
index 15d05ab..0000000
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/FileReader.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package top.r3944realms.docchecktoolrefactored.io.reader;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.file.Path;
-
-/**
- * The interface File reader.
- */
-public interface FileReader {
- /**
- * 读取文件内容
- *
- * @param file 要读取的文件
- * @return 文件内容字节数组 byte [ ]
- * @throws IOException the io exception
- */
- byte[] readFully(Path file) throws IOException;
-
- /**
- * 流式读取文件内容
- *
- * @param file 要读取的文件
- * @param consumer 内容消费者
- * @throws IOException the io exception
- */
- void readStreaming(Path file, ByteBufferConsumer consumer) throws IOException;
-
- /**
- * The interface Byte buffer consumer.
- */
- @FunctionalInterface
- interface ByteBufferConsumer {
- /**
- * Consume.
- *
- * @param buffer the buffer
- * @param isLast the is last
- * @throws IOException the io exception
- */
- void consume(ByteBuffer buffer, boolean isLast) throws IOException;
- }
-}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/MemoryMappedFileReader.java b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/MemoryMappedFileReader.java
deleted file mode 100644
index abc459c..0000000
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/MemoryMappedFileReader.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package top.r3944realms.docchecktoolrefactored.io.reader;
-
-import java.io.IOException;
-import java.nio.MappedByteBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-
-/**
- * The type Memory mapped file reader.
- */
-public class MemoryMappedFileReader implements FileReader {
- private static final long MAX_MMAP_SIZE = 1 << 30; // 1GB
-
- @Override
- public byte[] readFully(Path file) throws IOException {
- long size = Files.size(file);
- if (size > MAX_MMAP_SIZE) {
- throw new IOException("File too large for memory mapping: " + file);
- }
-
- try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {
- MappedByteBuffer buffer = channel.map(
- FileChannel.MapMode.READ_ONLY, 0, size);
- byte[] bytes = new byte[(int) size];
- buffer.get(bytes);
- return bytes;
- }
- }
-
- @Override
- public void readStreaming(Path file, ByteBufferConsumer consumer) throws IOException {
- try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {
- long size = channel.size();
- long position = 0;
- long chunkSize = Math.min(size, 8 * 1024 * 1024); // 8MB chunks
-
- while (position < size) {
- long remaining = size - position;
- long currentChunk = Math.min(remaining, chunkSize);
-
- MappedByteBuffer buffer = channel.map(
- FileChannel.MapMode.READ_ONLY, position, currentChunk);
- consumer.consume(buffer, position + currentChunk >= size);
- position += currentChunk;
- }
- }
- }
-}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/XmlFileReader.java b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/XmlFileReader.java
new file mode 100644
index 0000000..d7e5cb9
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/reader/XmlFileReader.java
@@ -0,0 +1,146 @@
+package top.r3944realms.docchecktoolrefactored.io.reader;
+
+import lombok.extern.slf4j.Slf4j;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
+import top.r3944realms.docchecktoolrefactored.util.FileUtil;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@Slf4j
+public class XmlFileReader implements CatalogFileReader {
+ // 可配置的元素名列表,按优先级排序
+ private static final List RECORD_TAG_CANDIDATES =
+ Arrays.asList("row", "record", "data", "item", "档案");
+
+ private static final List ARCHIVE_CODE_TAG_CANDIDATES =
+ Arrays.asList("档号", "dangan", "fileNo");
+
+ private static final List PAGE_COUNT_TAG_CANDIDATES =
+ Arrays.asList("页数", "pages", "pageCount");
+
+
+ @Override
+ public List readCatalogFile(String filePath) throws Exception {
+ File file = FileUtil.fileCheckAndGet(filePath, "xml");
+ List records = new ArrayList<>();
+
+ try {
+ // 创建安全配置的DocumentBuilder
+ DocumentBuilder builder = createSecureDocumentBuilder();
+
+ // 解析XML文件
+ Document doc = builder.parse(file);
+ doc.getDocumentElement().normalize();
+
+ log.debug("开始解析XML文件: {}, 根元素: {}",
+ file.getName(), doc.getDocumentElement().getNodeName());
+ // 查找记录元素
+ NodeList recordNodes = findAllRecordNodes(doc);
+ log.debug("找到 {} 个潜在记录节点", recordNodes.getLength());
+
+ // 解析每个记录元素
+ int validCount = 0;
+ int invalidCount = 0;
+
+ for (int i = 0; i < recordNodes.getLength(); i++) {
+ Node node = recordNodes.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element recordElement = (Element) node;
+ LogicalAddressFileGenerator.Record record = parseRecordElement(recordElement);
+
+ if (record != null) {
+ records.add(record);
+ validCount++;
+ log.debug("解析到有效记录: {}", record);
+ } else {
+ invalidCount++;
+ log.debug("跳过无效记录节点");
+ }
+ }
+ }
+ log.info("XML解析完成 - 有效记录: {}, 无效记录: {}", validCount, invalidCount);
+ } catch (Exception e) {
+ log.error("解析XML文件失败: {}", filePath, e);
+ throw new Exception("解析XML文件失败: " + filePath, e);
+ }
+ return records;
+ }
+
+ @SuppressWarnings("HttpUrlsUsage")
+ private static DocumentBuilder createSecureDocumentBuilder() throws ParserConfigurationException {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ // 安全配置防止XXE攻击
+ factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ factory.setXIncludeAware(false);
+ factory.setExpandEntityReferences(false);
+ return factory.newDocumentBuilder();
+ }
+ private NodeList findAllRecordNodes(Document doc) {
+ // 尝试所有可能的记录标签
+ for (String tagName : RECORD_TAG_CANDIDATES) {
+ NodeList nodes = doc.getElementsByTagName(tagName);
+ if (nodes.getLength() > 0) {
+ log.debug("使用标签名 '{}' 找到 {} 个记录节点", tagName, nodes.getLength());
+ return nodes;
+ }
+ }
+ log.warn("未找到任何记录节点,尝试的标签名: {}", RECORD_TAG_CANDIDATES);
+ return new EmptyNodeList(); // 返回空节点列表而不是null
+ }
+
+ private LogicalAddressFileGenerator.Record parseRecordElement(Element recordElement) {
+ String archiveCode = findFirstNonEmptyTextContent(recordElement, ARCHIVE_CODE_TAG_CANDIDATES);
+ String pageStr = findFirstNonEmptyTextContent(recordElement, PAGE_COUNT_TAG_CANDIDATES);
+
+ if (archiveCode == null || archiveCode.isEmpty()) {
+ log.debug("记录缺少档号字段");
+ return null;
+ }
+
+ try {
+ int page = pageStr != null ? Integer.parseInt(pageStr.trim()) : 0;
+ if (page > 0) {
+ return new LogicalAddressFileGenerator.Record(archiveCode, page);
+ } else {
+ log.debug("无效的页数值: {}", pageStr);
+ return null;
+ }
+ } catch (NumberFormatException e) {
+ log.warn("页数字段格式错误: {}", pageStr);
+ return null;
+ }
+ }
+
+ private String findFirstNonEmptyTextContent(Element element, List tagNames) {
+ for (String tag : tagNames) {
+ NodeList nodes = element.getElementsByTagName(tag);
+ if (nodes.getLength() > 0) {
+ String content = nodes.item(0).getTextContent();
+ if (content != null && !content.trim().isEmpty()) {
+ return content.trim();
+ }
+ }
+ }
+ return null;
+ }
+
+ // 空节点列表实现
+ private static class EmptyNodeList implements NodeList {
+ @Override public Node item(int index) { return null; }
+ @Override public int getLength() { return 0; }
+ }
+
+
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/ParallelFileScanner.java b/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/ParallelFileScanner.java
index 57990b4..5f92c55 100644
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/ParallelFileScanner.java
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/io/scanner/ParallelFileScanner.java
@@ -7,9 +7,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
-import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/model/RecordData.java b/src/main/java/top/r3944realms/docchecktoolrefactored/model/RecordData.java
new file mode 100644
index 0000000..946ec81
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/model/RecordData.java
@@ -0,0 +1,23 @@
+package top.r3944realms.docchecktoolrefactored.model;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.annotation.write.style.ColumnWidth;
+import com.alibaba.excel.annotation.write.style.ContentRowHeight;
+import com.alibaba.excel.annotation.write.style.HeadRowHeight;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ContentRowHeight(20)
+@HeadRowHeight(25)
+@ColumnWidth(25)
+public class RecordData {
+ @ExcelProperty(value = "档号")
+ private String archivalCode;
+
+ @ExcelProperty(value = "页数")
+ private Integer page;
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/DuplicateDocumentPaneController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/DuplicateDocumentPaneController.java
index c9dc6a5..7ecc7b0 100644
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/DuplicateDocumentPaneController.java
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/DuplicateDocumentPaneController.java
@@ -7,12 +7,19 @@ import javafx.scene.control.TextArea;
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.ui.task.DuplicateDocumentDetectionTask;
import java.io.File;
/**
* The type Duplicate document pane controller.
*/
+@Slf4j
public class DuplicateDocumentPaneController {
@FXML private TextArea result1B;
@FXML private TextField loadFolder1TF;
@@ -39,6 +46,39 @@ public class DuplicateDocumentPaneController {
* @param actionEvent the action event
*/
@FXML void onStart(ActionEvent actionEvent) {
- // 触发异步逻辑 -> 调用
+ log.info("用户点击了开始查重按钮");
+ String folderPath = loadFolder1TF.getText();
+ if (folderPath == null || folderPath.trim().isEmpty()) {
+ log.warn("未选择文件夹,无法进行查重");
+ result1B.setText("请选择要检查的文件夹。");
+ return;
+ }
+
+ // 创建并启动后台任务
+ DuplicateDocumentDetectionTask task = new DuplicateDocumentDetectionTask(folderPath);
+
+ // 绑定任务的消息到结果文本区域
+ task.messageProperty().addListener((observable, oldValue, newValue) -> {
+ result1B.setText(newValue);
+ });
+
+ // 当任务完成时显示完整结果
+ task.setOnSucceeded(e -> {
+ result1B.setText(task.getValue());
+ log.info("查重任务完成,结果如下:{}", task.getValue());
+ });
+
+ // 处理任务失败情况
+ task.setOnFailed(e -> {
+ Throwable exception = task.getException();
+ result1B.setText("检测过程中发生错误: " + exception.getMessage());
+ log.error("error", exception);
+ log.info("查重任务失败,错误信息: {}", exception.getMessage());
+ });
+
+ // 在新线程中执行任务
+ Thread thread = new Thread(task);
+ thread.setDaemon(true);
+ thread.start();
}
}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java
index 1b59485..9dd6264 100644
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java
@@ -7,13 +7,23 @@ import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
+import javafx.stage.FileChooser;
+import lombok.extern.slf4j.Slf4j;
+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 java.io.File;
import java.net.URL;
import java.util.ResourceBundle;
+
/**
* The type Path check pane controller.
*/
+//TODO: 应该交给Platform:runLater;
+@Slf4j
public class PathCheckPaneController implements Initializable {
@FXML private ChoiceBox loadFolderType2CB;
@FXML private TextArea result2TA;
@@ -24,6 +34,14 @@ public class PathCheckPaneController implements Initializable {
@FXML private Button selectJPGFolder2B;
@FXML private Button generateLogicalAddress2B;
@FXML private Button generatePhysicalAddress2B;
+ // 存储生成的文件路径
+ private String logicalAddressFilePath = null;
+ private String physicalAddressFilePath = null;
+
+
+ // 逻辑地址文件生成器实例
+ private final LogicalAddressFileGenerator generator = new LogicalAddressFileGenerator();
+ private final PhysicalAddressFileGenerator paGenerator = new PhysicalAddressFileGenerator();
/**
* On select lc.
@@ -31,7 +49,30 @@ public class PathCheckPaneController implements Initializable {
* @param actionEvent the action event
*/
@FXML void onSelectLC(ActionEvent actionEvent) {
+ log.info("用户点击了选择目录文件按钮");
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("选择目录文件");
+ // 设置文件过滤器,只允许DBF、XML、xlsx、xls格式
+
+ FileChooser.ExtensionFilter xlsxFilter = new FileChooser.ExtensionFilter("Excel Files (*.xlsx)", "*.xlsx");
+ FileChooser.ExtensionFilter xlsFilter = new FileChooser.ExtensionFilter("Excel Files (*.xls)", "*.xls");
+ 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);
+
+ // 显示文件选择对话框
+ File selectedFile = fileChooser.showOpenDialog(selectLoadCatalog2B.getScene().getWindow());
+
+ // 如果选择了文件,则将文件路径显示在loadCatalog2TF上
+ if (selectedFile != null) {
+ loadCatalog2TF.setText(selectedFile.getAbsolutePath());
+ log.info("选择的目录文件路径为:{}", selectedFile.getAbsolutePath());
+ }else{
+ log.warn("用户未选择任何文件夹");
+ result2TA.setText("未选择任何文件夹,请重新选择。");
+ }
}
/**
@@ -40,7 +81,22 @@ public class PathCheckPaneController implements Initializable {
* @param actionEvent the action event
*/
@FXML void onSelectJPGF(ActionEvent actionEvent) {
+ javafx.stage.DirectoryChooser directoryChooser = new javafx.stage.DirectoryChooser();
+ // 正确获取当前选中的值
+ Mode selectedMode = loadFolderType2CB.getValue();
+ if (selectedMode == Mode.PAGE_TYPE) {
+ directoryChooser.setTitle("选择页面级文件夹");
+ } else if (selectedMode == Mode.FILE_TYPE) {
+ directoryChooser.setTitle("选择文件级文件夹");
+ }
+ log.info("用户选择的模式为:{}", selectedMode);
+ File selectedDirectory = directoryChooser.showDialog(selectJPGFolder2B.getScene().getWindow());
+
+ if (selectedDirectory != null) {
+ loadJPGFolder2TF.setText(selectedDirectory.getAbsolutePath());
+ log.info("选择的{}文件夹路径为:{}", selectedMode,selectedDirectory.getAbsolutePath());
+ }
}
/**
@@ -49,7 +105,62 @@ public class PathCheckPaneController implements Initializable {
* @param actionEvent the action event
*/
@FXML void onGenerateLA(ActionEvent actionEvent) {
+ log.info("用户点击了生成逻辑地址文件按钮");
+ String filePath = loadCatalog2TF.getText();
+ if (filePath.isEmpty()) {
+ result2TA.setText("请先选择目录文件。");
+ return;
+ }
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("选择保存逻辑地址文件的位置");
+ fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv"));
+ fileChooser.setInitialFileName("逻辑地址文件.csv");
+
+ File outputFile = fileChooser.showSaveDialog(generateLogicalAddress2B.getScene().getWindow());
+ if (outputFile == null) {
+ result2TA.setText("未选择保存位置");
+ log.warn("用户未选择任何文件");
+ return;
+ }
+
+ // 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加
+ final File finalOutputFile;
+ if (!outputFile.getName().toLowerCase().endsWith(".csv")) {
+ finalOutputFile = new File(outputFile.getAbsolutePath() + ".csv");
+ } else {
+ finalOutputFile = outputFile;
+ }
+
+ // 保存生成的文件路径
+ logicalAddressFilePath = finalOutputFile.getAbsolutePath();
+ log.info("选择的输出文件路径: {}", logicalAddressFilePath);
+ // 创建后台任务来处理文件生成
+ Thread backgroundThread = new Thread(() -> {
+ // 获取当前选择的文件夹类型
+ Mode selectedMode = loadFolderType2CB.getValue();
+ int folderType = (selectedMode == Mode.PAGE_TYPE) ? AddressFileGenerator.PAGE_TYPE : AddressFileGenerator.FILE_TYPE;
+
+ generator.generateAddressFile(filePath, finalOutputFile, folderType, new LogicalAddressFileGenerator.Callback() {
+ @Override
+ public void onProgress(String message) {
+ javafx.application.Platform.runLater(() -> result2TA.setText(message));
+ }
+
+ @Override
+ public void onSuccess(String outputPath) {
+ javafx.application.Platform.runLater(() -> result2TA.setText(outputPath));
+ }
+
+ @Override
+ public void onError(String errorMessage) {
+ javafx.application.Platform.runLater(() -> result2TA.setText(errorMessage));
+ }
+ });
+ });
+
+
+ backgroundThread.start();
}
/**
@@ -58,20 +169,152 @@ public class PathCheckPaneController implements Initializable {
* @param actionEvent the action event
*/
@FXML void onGeneratePA(ActionEvent actionEvent) {
+ String folderPath = loadJPGFolder2TF.getText();
+ if (folderPath.isEmpty()) {
+ result2TA.setText("请先选择文件夹。");
+ return;
+ }
+ File folder = new File(folderPath);
+ if(!folder.exists() || !folder.isDirectory()) {
+ result2TA.setText("所选路径不存在或不是一个有效的文件夹。");
+ return;
+ }
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("选择保存物理地址文件的位置");
+ fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv"));
+ fileChooser.setInitialFileName("物理地址文件.csv");
+ // 使用当前窗口作为父窗口显示文件选择对话框
+ File outputFile = fileChooser.showSaveDialog(selectJPGFolder2B.getScene().getWindow());
+
+ if (outputFile == null) {
+ result2TA.setText("未选择保存位置");
+ return;
+ }
+
+ // 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加
+ final File finalOutputFile;
+ if (!outputFile.getName().toLowerCase().endsWith(".csv")) {
+ finalOutputFile = new File(outputFile.getAbsolutePath() + ".csv");
+ } else {
+ finalOutputFile = outputFile;
+ }
+
+ // 保存生成的文件路径
+ physicalAddressFilePath = finalOutputFile.getAbsolutePath();
+
+ // 创建后台任务来处理文件生成
+ Thread backgroundThread = new Thread(() -> {
+ // 获取当前选择的文件夹类型
+ Mode selectedMode = loadFolderType2CB.getValue();
+ int folderType = (selectedMode == Mode.PAGE_TYPE) ? AddressFileGenerator.PAGE_TYPE : AddressFileGenerator.FILE_TYPE;
+
+ paGenerator.generateAddressFile(folderPath, finalOutputFile, folderType, new AddressFileGenerator.Callback() {
+ @Override
+ public void onProgress(String message) {
+ javafx.application.Platform.runLater(() -> result2TA.setText(message));
+ }
+
+ @Override
+ public void onSuccess(String outputPath) {
+ javafx.application.Platform.runLater(() -> result2TA.setText(outputPath));
+ }
+
+ @Override
+ public void onError(String errorMessage) {
+ javafx.application.Platform.runLater(() -> result2TA.setText(errorMessage));
+ }
+ });
+ });
+
+ backgroundThread.start();
}
+
+
+
/**
* On start.
*
* @param actionEvent the action event
*/
- @FXML void onStart(ActionEvent actionEvent) {
+ @FXML
+ void onStart(ActionEvent actionEvent) {
+ log.info("用户点击了开始对比按钮");
+ // 检查是否已生成两个文件
+ if (logicalAddressFilePath == null || physicalAddressFilePath == null) {
+ result2TA.setText("请先生成逻辑地址文件和物理地址文件。");
+ return;
+ }
+
+ log.info("逻辑地址文件路径为:{}", logicalAddressFilePath);
+ log.info("物理地址文件路径为:{}", physicalAddressFilePath);
+
+ // 使用新创建的核心类进行文件比较
+ AddressFileComparator comparator = new AddressFileComparator();
+ AddressFileComparator.ComparisonResult result = comparator.compareFiles(physicalAddressFilePath, logicalAddressFilePath);
+
+ // 显示比对结果
+ StringBuilder resultText = new StringBuilder();
+
+ // 显示读取的行数
+ resultText.append("读取物理地址文件记录数: ").append(result.getPhysicalRecordsCount()).append("\n");
+ resultText.append("读取逻辑地址文件记录数: ").append(result.getLogicalRecordsCount()).append("\n\n");
+
+ // 显示路径不一致的结果
+ if (!result.getPathMismatchResults().isEmpty()) {
+ resultText.append("文件名相同但路径不一致的记录数量: ").append(result.getPathMismatchResults().size()).append("\n");
+ for (String mismatch : result.getPathMismatchResults()) {
+ resultText.append("\t").append(mismatch).append("\n");
+ }
+ resultText.append("\n");
+ } else {
+ resultText.append("没有路径错误\n\n");
+ }
+
+ // 显示物理文件在逻辑文件中未找到的结果
+ if (!result.getForwardComparisonResults().isEmpty()) {
+ resultText.append("物理文件在逻辑文件中未找到的记录数量: ").append(result.getForwardComparisonResults().size()).append("\n");
+ for (String forward : result.getForwardComparisonResults()) {
+ resultText.append("\t").append(forward).append("\n");
+ }
+ resultText.append("\n");
+ } else {
+ resultText.append("没有物理存在而逻辑不存在的文件\n\n");
+ }
+
+ // 显示逻辑文件在物理文件中未找到的结果
+ if (!result.getBackwardComparisonResults().isEmpty()) {
+ resultText.append("逻辑文件在物理文件中未找到的记录数量: ").append(result.getBackwardComparisonResults().size()).append("\n");
+ for (String backward : result.getBackwardComparisonResults()) {
+ resultText.append("\t").append(backward).append("\n");
+ }
+ } else {
+ resultText.append("没有逻辑存在而物理不存在的文件\n");
+ }
+
+ // 如果所有结果都为空,则显示一致信息
+ if (result.getPathMismatchResults().isEmpty() &&
+ result.getForwardComparisonResults().isEmpty() &&
+ result.getBackwardComparisonResults().isEmpty()) {
+ resultText = new StringBuilder("所有文件比对一致,无差异。\n");
+ resultText.append("读取物理地址文件记录数: ").append(result.getPhysicalRecordsCount()).append("\n");
+ resultText.append("读取逻辑地址文件记录数: ").append(result.getLogicalRecordsCount()).append("\n");
+ }
+
+ result2TA.setText(resultText.toString());
}
+
+
+
+
+
+
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
+ loadFolderType2CB.setValue(Mode.PAGE_TYPE);
loadFolderType2CB.getItems().addAll(Mode.values());
}
@@ -82,11 +325,11 @@ public class PathCheckPaneController implements Initializable {
/**
* Jpg mode.
*/
- JPG("jpg"),
+ PAGE_TYPE("jpg"),
/**
* Pdf mode.
*/
- PDF("pdf");
+ FILE_TYPE("pdf");
/**
* The Id.
*/
@@ -97,3 +340,5 @@ public class PathCheckPaneController implements Initializable {
}
}
}
+
+
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/ProjectInfoPaneController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/ProjectInfoPaneController.java
index fe8b3fa..3aafc57 100644
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/ProjectInfoPaneController.java
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/ProjectInfoPaneController.java
@@ -1,5 +1,6 @@
package top.r3944realms.docchecktoolrefactored.ui.module;
+import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
@@ -12,5 +13,14 @@ public class ProjectInfoPaneController {
@FXML private TextField totalCatalogNumberTF;
@FXML private TextField fileCategoriesTF;
@FXML private TextField fileYearTF;
+ @FXML
+ void onReset(ActionEvent event) {
+ // 清空所有文本字段
+ projectNameTF.clear();
+ fileYearTF.clear();
+ fileCategoriesTF.clear();
+ totalCatalogNumberTF.clear();
+ AcceptanceTimeTF.clear();
+ }
}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java
new file mode 100644
index 0000000..db36bc0
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java
@@ -0,0 +1,254 @@
+// StorageCarrierPaneController.java
+package top.r3944realms.docchecktoolrefactored.ui.module;
+
+import javafx.concurrent.Task;
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.TextArea;
+import javafx.scene.control.TextField;
+import javafx.stage.DirectoryChooser;
+import javafx.stage.FileChooser;
+import javafx.stage.Stage;
+import lombok.extern.slf4j.Slf4j;
+import top.r3944realms.docchecktoolrefactored.core.HashFileGenerator;
+import top.r3944realms.docchecktoolrefactored.core.MD5HashCalculator;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+//TODO: 应该交给Platform:runLater;
+@Slf4j
+public class StorageCarrierPaneController {
+
+ @FXML
+ private Button caculateHash7B;
+
+ @FXML
+ private Button generateHashFile7B;
+
+ @FXML
+ private TextField loadCompressedFile;
+
+ @FXML
+ private TextField loadDigitalOutcomes;
+
+ @FXML
+ private TextArea result7TA;
+
+ @FXML
+ private Button selectLoadCompressedFile7B;
+
+ @FXML
+ private Button selectLoadDigitalOutcomes7B;
+
+ @FXML
+ private Button clearSelectedFoldersButton;
+
+
+
+
+ @FXML
+ void onSelectLD(ActionEvent event) {
+ log.info("用户点击选择文件夹按钮");
+ DirectoryChooser directoryChooser = new DirectoryChooser();
+ directoryChooser.setTitle("选择要检查的文件夹(页面级文件夹和文件级文件夹等不包括目录文件夹)");
+
+ File selectedFolder = directoryChooser.showDialog(new Stage());
+ if (selectedFolder != null) {
+ String currentText = loadDigitalOutcomes.getText();
+ String folderPath = selectedFolder.getAbsolutePath();
+
+ // 如果当前文本框为空,直接设置;否则追加路径
+ if (currentText == null || currentText.isEmpty()) {
+ loadDigitalOutcomes.setText(folderPath);
+ } else {
+ // 检查是否已经添加过该路径,避免重复
+ String[] existingPaths = currentText.split(File.pathSeparator);
+ boolean alreadyExists = false;
+ for (String path : existingPaths) {
+ if (path.equals(folderPath)) {
+ alreadyExists = true;
+ break;
+ }
+ }
+
+ if (!alreadyExists) {
+ loadDigitalOutcomes.setText(currentText + File.pathSeparator + folderPath);
+ }
+ }
+ log.info("用户选择了文件夹: {}", selectedFolder.getAbsolutePath());
+ } else {
+ log.info("用户取消了文件夹选择");
+ }
+ }
+
+ @FXML
+ void onClearSelectedFolders(ActionEvent event) {
+ log.info("用户点击清除已选择文件夹按钮");
+ loadDigitalOutcomes.setText("");
+ result7TA.setText("已清除所有已选择的文件夹");
+ log.info("已清除所有已选择的文件夹");
+ }
+
+
+
+
+ @FXML
+ void onSelectLC(ActionEvent event) {
+ log.info("用户点击选择RAR文件按钮");
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("选择一个 .rar 文件");
+ fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("RAR Files", "*.rar"));
+ File selectedFile = fileChooser.showOpenDialog(new Stage());
+ if (selectedFile != null) {
+ loadCompressedFile.setText(selectedFile.getAbsolutePath());
+ log.info("用户选择了RAR文件: {}", selectedFile.getAbsolutePath());
+ } else {
+ log.info("用户取消了RAR文件选择");
+ }
+ }
+
+ @FXML
+ void onCaculateHash(ActionEvent event) {
+ log.info("开始计算RAR文件的MD5哈希值");
+ String filePath = loadCompressedFile.getText();
+ if (filePath == null || filePath.isEmpty()) {
+ log.warn("未选择RAR文件,无法计算哈希值");
+ result7TA.setText("请先选择一个 .rar 文件");
+ return;
+ }
+
+ File file = new File(filePath);
+ if (!file.exists() || !file.isFile() || !filePath.endsWith(".rar")) {
+ log.warn("选择的文件无效或不是RAR文件: {}", filePath);
+ result7TA.setText("所选文件不存在或不是一个有效的 .rar 文件");
+ return;
+ }
+
+ try {
+ log.info("开始计算文件哈希值: {}", filePath);
+ MD5HashCalculator hashCalculator = new MD5HashCalculator();
+ String hashResult = hashCalculator.calculateHash(file.toPath());
+ result7TA.setText("计算结果:\n" + hashResult);
+ log.info("文件哈希值计算完成: {}", hashResult);
+ } catch (IOException e) {
+ log.error("计算文件哈希值时出错: {}", filePath, e);
+ result7TA.setText("计算哈希值时出错: " + e.getMessage());
+ }
+ }
+
+ @FXML
+ void onGenerateHF(ActionEvent event) {
+ log.info("开始生成哈希列表文件");
+ String folderPathsText = loadDigitalOutcomes.getText();
+ if (folderPathsText == null || folderPathsText.isEmpty()) {
+ log.warn("未选择文件夹,无法生成哈希列表文件");
+ result7TA.setText("请先选择一个文件夹");
+ return;
+ }
+
+ // 解析多个文件夹路径
+ String[] folderPaths = folderPathsText.split(File.pathSeparator);
+ List folders = new ArrayList<>();
+
+ for (String path : folderPaths) {
+ File folder = new File(path.trim());
+ if (folder.exists() && folder.isDirectory()) {
+ folders.add(folder);
+ } else {
+ log.warn("选择的路径无效或不是文件夹: {}", path);
+ result7TA.setText("所选路径不存在或不是一个有效的文件夹: " + path);
+ return;
+ }
+ }
+
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("选择保存哈希列表文件的位置");
+ fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv"));
+
+ // 设置默认文件名
+ fileChooser.setInitialFileName("哈希值列表文件.csv");
+
+ // 使用当前窗口作为父窗口显示文件选择对话框
+ File outputFile = fileChooser.showSaveDialog(selectLoadDigitalOutcomes7B.getScene().getWindow());
+
+ if (outputFile == null) {
+ log.info("用户取消了文件保存操作");
+ result7TA.setText("未选择保存位置");
+ return;
+ }
+
+ // 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加
+ final File finalOutputFile;
+ if (!outputFile.getName().toLowerCase().endsWith(".csv")) {
+ finalOutputFile = new File(outputFile.getAbsolutePath() + ".csv");
+ } else {
+ finalOutputFile = outputFile;
+ }
+
+ log.info("选择的输出文件路径: {}", finalOutputFile.getAbsolutePath());
+
+ // 创建后台任务
+ Task task = new Task() {
+ @Override
+ protected String call() throws Exception {
+ log.info("开始执行哈希文件生成任务");
+ updateMessage("开始生成哈希文件...");
+
+ HashFileGenerator generator = new HashFileGenerator();
+
+ // 传递多个文件夹路径
+ List folderPaths = folders.stream().map(File::toPath).collect(ArrayList::new,
+ ArrayList::add,
+ ArrayList::addAll);
+
+ generator.generateHashFile(folderPaths, finalOutputFile.toPath(), (current, total) -> {
+ updateProgress(current, total);
+ updateMessage("处理文件: " + current + "/" + total);
+ if (current % 500 == 0 || current == total) { // 每500个文件或完成时记录一次日志
+ log.info("处理进度: {}/{}", current, total);
+ }
+ });
+
+ log.info("哈希文件生成任务完成,输出文件: {}", finalOutputFile.getAbsolutePath());
+ return "哈希列表文件已生成: " + finalOutputFile.getAbsolutePath();
+ }
+ };
+
+ // 绑定任务的消息到结果文本区域,实时显示进度
+ task.messageProperty().addListener((observable, oldValue, newValue) -> {
+ result7TA.setText(newValue);
+ });
+
+ // 任务成功完成
+ task.setOnSucceeded(e -> {
+ log.info("哈希文件生成任务成功完成");
+ result7TA.setText(task.getValue());
+ });
+
+ // 任务失败处理
+ task.setOnFailed(e -> {
+ Throwable exception = task.getException();
+ String errorMsg = "生成哈希文件时出错: " + (exception != null ? exception.getMessage() : "未知错误");
+ log.error("哈希文件生成任务失败", exception);
+ result7TA.setText(errorMsg);
+ });
+
+ // 任务取消处理
+ task.setOnCancelled(e -> {
+ log.info("哈希文件生成任务被用户取消");
+ result7TA.setText("哈希文件生成操作已取消");
+ });
+
+ // 在新线程中执行任务
+ Thread thread = new Thread(task);
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+
+
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java
new file mode 100644
index 0000000..74178fa
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java
@@ -0,0 +1,177 @@
+package top.r3944realms.docchecktoolrefactored.ui.task;
+
+import javafx.concurrent.Task;
+import lombok.extern.slf4j.Slf4j;
+import top.r3944realms.docchecktoolrefactored.core.MD5HashCalculator;
+import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
+import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
+
+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.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+@Slf4j
+public class DuplicateDocumentDetectionTask extends Task {
+ private final String folderPath;
+ private final MD5HashCalculator hashCalculator;
+ private volatile RobustParallelScanner scanner;
+
+ public DuplicateDocumentDetectionTask(String folderPath) {
+ this.folderPath = folderPath;
+ this.hashCalculator = new MD5HashCalculator();
+ }
+
+ @Override
+ protected String call() throws Exception {
+ updateMessage("正在初始化扫描...");
+
+ Path rootPath = Paths.get(folderPath);
+ if (!Files.exists(rootPath) || !Files.isDirectory(rootPath)) {
+ throw new IllegalArgumentException("指定路径不是有效目录: " + folderPath);
+ }
+
+ // 使用 RobustParallelScanner 和 MD5HashCalculator 进行并行扫描和哈希计算
+ Map> hashToFileMap = new ConcurrentHashMap<>();
+ AtomicInteger processed = new AtomicInteger(0);
+ AtomicReference errorRef = new AtomicReference<>(null);
+ AtomicBoolean scanCompleted = new AtomicBoolean(false);
+
+ // 使用 CountDownLatch 等待扫描完成
+ CountDownLatch latch = new CountDownLatch(1);
+
+ // 创建扫描器
+ scanner = new RobustParallelScanner(10);
+
+ // 异步启动扫描任务
+ Thread scanThread = new Thread(() -> {
+ 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);
+ }
+ }
+ });
+ } catch (Exception e) {
+ errorRef.set(e);
+ latch.countDown();
+ }
+ });
+
+ scanThread.setDaemon(true);
+ scanThread.start();
+
+ // 等待扫描完成,设置更长的超时时间(例如5分钟)
+ if (!latch.await(5*60, TimeUnit.MINUTES)) {
+ scanner.cancel();
+ throw new TimeoutException("扫描超时(5分钟)");
+ }
+
+ // 检查是否被取消
+ if (isCancelled()) {
+ scanner.cancel();
+ return "操作已被取消";
+ }
+
+ // 如果有错误且扫描未完成,抛出异常
+ if (errorRef.get() != null && !scanCompleted.get()) {
+ throw errorRef.get();
+ }
+
+ // 分析重复文件并构建结果
+ updateMessage("正在分析重复文件...");
+ 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");
+
+ List>> duplicateGroups = hashToFileMap.entrySet().stream()
+ .filter(entry -> entry.getValue().size() > 1)
+ .collect(Collectors.toList());
+
+ if (!duplicateGroups.isEmpty()) {
+ result.append("有 ").append(duplicateGroups.size()).append(" 组重复文件\n\n");
+
+ int groupIndex = 1;
+ for (Map.Entry> entry : duplicateGroups) {
+ result.append("第 ").append(groupIndex).append(" 组\t");
+ result.append("哈希值: ").append(entry.getKey()).append("\n");
+
+ 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");
+ fileIndex++;
+ }
+ result.append("\n");
+ groupIndex++;
+ }
+ } else {
+ result.append("没有重复文件\n");
+ }
+
+ updateMessage("检测完成!");
+ return result.toString();
+ }
+
+ @Override
+ protected void cancelled() {
+ super.cancelled();
+ if (scanner != null) {
+ scanner.cancel();
+ }
+ }
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java
index 86bb67a..ba7f44e 100644
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java
@@ -95,4 +95,13 @@ public class DialogUtil {
alert.showAndWait();
});
}
+ public static void showErrorDialog(String title, String content) {
+ showErrorDialog(title, title, content);
+ }
+ public static void showWarningDialog(String title, String content) {
+ showWarningDialog(title, title, content);
+ }
+ public static void showInformationDialog(String title, String content) {
+ showInformationDialog(title, title, content);
+ }
}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/util/FileUtil.java b/src/main/java/top/r3944realms/docchecktoolrefactored/util/FileUtil.java
new file mode 100644
index 0000000..6b79207
--- /dev/null
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/util/FileUtil.java
@@ -0,0 +1,98 @@
+package top.r3944realms.docchecktoolrefactored.util;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.File;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * 文件工具类
+ */
+@Slf4j
+public class FileUtil {
+ /**
+ * 将字节数转换为易读的格式
+ */
+ public static String humanReadableByteCount(long bytes) {
+ if (bytes < 0) throw new IllegalArgumentException("字节数不能为负数");
+ if (bytes < 1024) return bytes + " B";
+ int exp = (int) (Math.log(bytes) / Math.log(1024));
+ String unit = "KMGTPE".charAt(exp-1) + "iB";
+ return String.format(Locale.US,"%.1f %s", bytes / Math.pow(1024, exp), unit);
+ }
+
+ public static String getFileExtension(String filePath) throws NoSuchFileException {
+ Path path = Paths.get(filePath).normalize();
+ File file = path.toFile();
+ if (!file.exists()) {
+ log.error("文件不存在: {}", filePath);
+ throw new NoSuchFileException("文件不存在: " + filePath);
+ }
+
+ String fileName = path.getFileName().toString();
+ int dotIndex = fileName.lastIndexOf('.');
+ if (dotIndex == -1 || dotIndex == fileName.length() - 1) {
+ throw new IllegalArgumentException("文件无有效扩展名: " + fileName);
+ }
+ return fileName.substring(dotIndex + 1).toLowerCase();
+ }
+
+ /**
+ * 检查文件路径并返回
+ *
+ * @param filePath 文件路径
+ * @return File
+ * @throws NoSuchFileException 当指定路径无此文件时抛出
+ */
+ public static File fileCheckAndGet(String filePath) throws NoSuchFileException {
+ return fileCheckAndGet(filePath, (String) null);
+ }
+
+ /**
+ * 检查文件路径并返回
+ *
+ * @param filePath 文件路径
+ * @param supportedFileExtensions 文件扩展名,例如 ["pdf", "txt]
+ * @throws IllegalArgumentException 当指定路径文件无扩展名(如README),或扩展名不符时抛出
+ * @throws NoSuchFileException 当指定路径无此文件时抛出
+ */
+ public static File fileCheckAndGet(String filePath, String... supportedFileExtensions)
+ throws NoSuchFileException, IllegalArgumentException {
+
+ Path path = Paths.get(filePath).normalize();
+ File file = path.toFile();
+
+ if (!file.exists()) {
+ log.error("文件不存在: {}", filePath);
+ throw new NoSuchFileException("文件不存在: " + filePath);
+ }
+
+ if (supportedFileExtensions == null || supportedFileExtensions.length == 0) {
+ return file;
+ }
+
+ String fileName = path.getFileName().toString();
+ int dotIndex = fileName.lastIndexOf('.');
+ if (dotIndex == -1 || dotIndex == fileName.length() - 1) {
+ throw new IllegalArgumentException("文件无有效扩展名: " + fileName);
+ }
+
+ String fileExtension = fileName.substring(dotIndex + 1).toLowerCase();
+ List supportedExtensions = Arrays.stream(supportedFileExtensions)
+ .map(String::toLowerCase)
+ .toList();
+
+ if (!supportedExtensions.contains(fileExtension)) {
+ log.error("不支持的文件格式: {}", fileExtension);
+ throw new IllegalArgumentException("不支持的文件格式,预期: "
+ + supportedExtensions + ",实际: " + fileExtension);
+ }
+
+ return file;
+ }
+}
diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/util/StringUtil.java b/src/main/java/top/r3944realms/docchecktoolrefactored/util/StringUtil.java
index c497fd1..863c25a 100644
--- a/src/main/java/top/r3944realms/docchecktoolrefactored/util/StringUtil.java
+++ b/src/main/java/top/r3944realms/docchecktoolrefactored/util/StringUtil.java
@@ -3,10 +3,10 @@ package top.r3944realms.docchecktoolrefactored.util;
public class StringUtil {
public static String NO_BUG = """
- _ooOoo_
- o8888888o
- 88" . "88
- (| -_- |)
+ _ooOoo_
+ o8888888o
+ 88" . "88
+ (| -_- |)
O\\ = /O
____/`---'\\____
.' \\\\| |// `.
diff --git a/src/main/resources/fxml/login-view.fxml b/src/main/resources/fxml/login-view.fxml
index bb44b54..a782a37 100644
--- a/src/main/resources/fxml/login-view.fxml
+++ b/src/main/resources/fxml/login-view.fxml
@@ -11,14 +11,13 @@
-
+
-