基于[提交](f853f03bac)修改的版本

This commit is contained in:
叁玖领域 2025-08-12 20:39:57 +08:00
parent 5e2fbfe1f9
commit 63790e4c51
49 changed files with 3215 additions and 919 deletions

View File

@ -5,6 +5,9 @@
<groovy codeStyle="LEGACY" />
</component>
<component name="EntryPointsManager">
<list size="1">
<item index="0" class="java.lang.String" itemvalue="javafx.fxml.FXML" />
</list>
<writeAnnotations>
<writeAnnotation name="lombok.Getter" />
</writeAnnotations>

View File

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

View File

@ -1,2 +1,2 @@
project_group ='top.r3944realms.docchecktoolrefacored'
project_version = '1.0-SNAPSHOT'
project_group =top.r3944realms.docchecktoolrefacored
project_version = 1.0-SNAPSHOT

View File

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

View File

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

View File

@ -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<String> list = Arrays.asList(args);
if (list.contains("--cli")) {
// CLI 模式
List<String> 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);
}
}
}

View File

@ -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<String> 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<DuplicateGroup> 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<DuplicateGroup> 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<DuplicateGroup> duplicates = finder.findDuplicates(scanDir);
// 应用过滤
duplicates = filterResults(duplicates);
// 应用排序
duplicates = sortResults(duplicates);
return duplicates;
}
/**
* 过滤结果
*/
private List<DuplicateGroup> filterResults(List<DuplicateGroup> 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<DuplicateGroup> sortResults(List<DuplicateGroup> 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<DuplicateGroup> 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<DuplicateGroup> 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<DuplicateGroup> 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<DuplicateGroup> 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"));
}
}

View File

@ -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<String> forwardComparisonResults; // 正向比较文件名缺失结果
private final List<String> backwardComparisonResults; // 反向比较文件名缺失结果
private final List<String> pathMismatchResults; // 路径不一致的结果
private final List<String> pageCountMismatchResults; // 页数不一致的结果仅文件级比较使用
public ComparisonResult(int physicalRecordsCount,
int logicalRecordsCount,
List<String> forwardResults,
List<String> backwardResults,
List<String> pathMismatchResults,
List<String> 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<String> forwardResults,
List<String> backwardResults,
List<String> 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<String[]> physicalRecords = readCSV(physicalAddressFilePath);
List<String[]> logicalRecords = readCSV(logicalAddressFilePath);
// 记录读取的行数不包括标题行
int physicalCount = physicalRecords.size();
int logicalCount = logicalRecords.size();
log.info("读取物理地址文件记录数: {}", physicalCount);
log.info("读取逻辑地址文件记录数: {}", logicalCount);
List<String> forwardComparisonResults = new ArrayList<>(); // 物理文件在逻辑文件中未找到
List<String> backwardComparisonResults = new ArrayList<>(); // 逻辑文件在物理文件中未找到
List<String> pathMismatchResults = new ArrayList<>(); // 文件名相同但路径不一致
List<String> pageCountMismatchResults = new ArrayList<>(); // 文件名和路径相同但页数不一致仅文件级
// 正向比较遍历物理文件检查是否在逻辑文件中存在
for (String[] physicalRecord : physicalRecords) {
// 确保数据行有足够列数
if (compareMode == CompareMode.PAGE_LEVEL && physicalRecord.length < 2) continue;
if (compareMode == CompareMode.FILE_LEVEL && physicalRecord.length < 3) continue;
String physicalFileName = physicalRecord[0];
String physicalAddress = physicalRecord[1];
String physicalPageCount = compareMode == CompareMode.FILE_LEVEL ? physicalRecord[2] : null;
boolean found = false;
for (String[] logicalRecord : logicalRecords) {
// 确保数据行有足够列数
if (compareMode == CompareMode.PAGE_LEVEL && logicalRecord.length < 2) continue;
if (compareMode == CompareMode.FILE_LEVEL && logicalRecord.length < 3) continue;
String logicalFileName = logicalRecord[0];
String logicalAddress = logicalRecord[1];
String logicalPageCount = compareMode == CompareMode.FILE_LEVEL ? logicalRecord[2] : null;
if (physicalFileName.equals(logicalFileName)) {
found = true;
// 文件名相同比较路径
if (!physicalAddress.equals(logicalAddress)) {
pathMismatchResults.add("文件名=" + physicalFileName +
", 物理地址=" + physicalAddress +
", 逻辑地址=" + logicalAddress);
}
// 如果是文件级比较且路径相同再比较页数
else if (compareMode == CompareMode.FILE_LEVEL &&
physicalPageCount != null && logicalPageCount != null &&
!physicalPageCount.equals(logicalPageCount)) {
pageCountMismatchResults.add("文件名=" + physicalFileName +
", 物理地址=" + physicalAddress +
", 逻辑地址=" + logicalAddress +
", 物理页数=" + physicalPageCount +
", 逻辑页数=" + logicalPageCount);
}
break;
}
}
// 如果在逻辑文件中未找到该物理文件记录
if (!found) {
String result = "文件名=" + physicalFileName + ", 物理地址=" + physicalAddress;
if (compareMode == CompareMode.FILE_LEVEL && physicalPageCount != null) {
result += ", 物理页数=" + physicalPageCount;
}
forwardComparisonResults.add(result);
}
}
// 反向比较遍历逻辑文件检查是否在物理文件中存在
for (String[] logicalRecord : logicalRecords) {
// 确保数据行有足够列数
if (compareMode == CompareMode.PAGE_LEVEL && logicalRecord.length < 2) continue;
if (compareMode == CompareMode.FILE_LEVEL && logicalRecord.length < 3) continue;
String logicalFileName = logicalRecord[0];
String logicalAddress = logicalRecord[1];
String logicalPageCount = compareMode == CompareMode.FILE_LEVEL ? logicalRecord[2] : null;
boolean found = false;
for (String[] physicalRecord : physicalRecords) {
// 确保数据行有足够列数
if (compareMode == CompareMode.PAGE_LEVEL && physicalRecord.length < 2) continue;
if (compareMode == CompareMode.FILE_LEVEL && physicalRecord.length < 3) continue;
String physicalFileName = physicalRecord[0];
if (logicalFileName.equals(physicalFileName)) {
found = true;
break;
}
}
// 如果在物理文件中未找到该逻辑文件记录
if (!found) {
String result = "文件名=" + logicalFileName + ", 逻辑地址=" + logicalAddress;
if (compareMode == CompareMode.FILE_LEVEL && logicalPageCount != null) {
result += ", 逻辑页数=" + logicalPageCount;
}
backwardComparisonResults.add(result);
}
}
// 将比较结果记录到日志中
logComparisonResults(physicalCount, logicalCount, forwardComparisonResults,
backwardComparisonResults, pathMismatchResults, pageCountMismatchResults, compareMode);
return new ComparisonResult(
physicalCount,
logicalCount,
forwardComparisonResults,
backwardComparisonResults,
pathMismatchResults,
pageCountMismatchResults
);
}
private List<String[]> readCSV(String filePath) {
List<String[]> 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<String> forwardResults, List<String> backwardResults,
List<String> pathMismatchResults, List<String> 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<String> forwardResults, List<String> backwardResults,
List<String> pathMismatchResults) {
logComparisonResults(physicalCount, logicalCount, forwardResults, backwardResults,
pathMismatchResults, new ArrayList<>(), CompareMode.PAGE_LEVEL);
}
}

View File

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

View File

@ -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<Long, List<FileMetadata>> sizeGroups = groupFilesBySize(rootDir);
// 计算需要处理的总文件数大小分组中可能有重复的文件
int totalFilesToProcess = sizeGroups.values().stream()
.filter(group -> group.size() > 1)
.mapToInt(List::size)
.sum();
if (totalFilesToProcess == 0) {
return Collections.emptyList();
}
// 第二阶段对可能重复的文件计算哈希
Map<String, List<FileMetadata>> 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);
}
}

View File

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

View File

@ -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<Path> directories, Path outputFile, ProgressListener listener) throws IOException, InterruptedException {
List<Path> allFiles = new ArrayList<>();
// 扫描所有目录中的文件
for (Path directory : directories) {
if (!Files.isDirectory(directory)) {
throw new IllegalArgumentException("指定路径不是有效目录: " + directory);
}
List<Path> files = new ArrayList<>();
CompletableFuture<Void> 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<String[]> hashResults = new ArrayList<>();
AtomicInteger processedFiles = new AtomicInteger(0);
int totalFiles = allFiles.size();
allFiles.parallelStream().forEach(file -> {
try {
String hash = new MD5HashCalculator().calculatePartialHash(file);
String[] result = {file.getFileName().toString(), hash};
synchronized (hashResults) {
hashResults.add(result);
}
int processed = processedFiles.incrementAndGet();
if (listener != null) {
listener.onProgressUpdate(processed, totalFiles);
}
} catch (IOException e) {
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();
}
}
}
}

View File

@ -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<Record> 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<Record> 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<Record> 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<Record> readCatalogFile(String filePath) throws Exception {
// 使用工厂模式创建相应的文件读取器
CatalogFileReader reader = CatalogFileReaderFactory.createReader(filePath);
return reader.readCatalogFile(filePath);
}
public record Record(String archiveCode, int page) {
}
}

View File

@ -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 {
}
}
/**
* <h1>读取文件头部</h1>
* <ul>
* <li>读取文件的前partialChunkSize字节如果文件大小小于这个值则读取整个文件</li>
* <li>这些字节会被添加到MD5计算中</li>
* </ul>
* <h1>读取文件中间部分</h1>
* <ul>
* <li>只有当文件大小大于partialChunkSize * 2时才会执行</li>
* <li>从文件中间位置前后各取partialChunkSize/2字节总共partialChunkSize字节</li>
* <li>这些字节会被追加到MD5计算中 </li>
* </ul>
* <h1>读取文件尾部</h1>
* <ul>
* <li>只有当文件大小大于partialChunkSize时才会执行</li>
* <li>读取文件最后的partialChunkSize字节</li>
* <li>这些字节会被追加到MD5计算中</li>
* </ul>
*/
@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) {

View File

@ -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('/', '\\');
}
}

View File

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

View File

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

View File

@ -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<String> 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<String, List<File>> 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<File> 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();
}
}

View File

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

View File

@ -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<String> physicalPaths = new ArrayList<>();
private List<String> 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<String> paths) throws IOException {
paths.clear();
try (Stream<Path> walk = Files.walk(folderPath)) {
paths.addAll(walk
.filter(Files::isRegularFile)
.map(p -> folderPath.relativize(p).toString())
.collect(Collectors.toList()));
}
}
private void writePathsToFile(List<String> 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<String> 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<String> 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<String> missingInPhysical, Set<String> 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();
}
}

View File

@ -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<LogicalAddressFileGenerator.Record> readCatalogFile(String filePath) throws Exception;
}

View File

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

View File

@ -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<LogicalAddressFileGenerator.Record> readCatalogFile(String filePath) throws Exception {
List<LogicalAddressFileGenerator.Record> 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;
}
}

View File

@ -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<LogicalAddressFileGenerator.Record> readCatalogFile(String filePath) throws Exception {
File file = FileUtil.fileCheckAndGet(filePath, "xlsx", "xls");
boolean isXlsx = file.getName().endsWith(".xlsx");
return readExcelFile(file, isXlsx);
}
private List<LogicalAddressFileGenerator.Record> readExcelFile(File file, boolean isXlsx) throws Exception {
List<LogicalAddressFileGenerator.Record> 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;
}
}
}

View File

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

View File

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

View File

@ -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<String> RECORD_TAG_CANDIDATES =
Arrays.asList("row", "record", "data", "item", "档案");
private static final List<String> ARCHIVE_CODE_TAG_CANDIDATES =
Arrays.asList("档号", "dangan", "fileNo");
private static final List<String> PAGE_COUNT_TAG_CANDIDATES =
Arrays.asList("页数", "pages", "pageCount");
@Override
public List<LogicalAddressFileGenerator.Record> readCatalogFile(String filePath) throws Exception {
File file = FileUtil.fileCheckAndGet(filePath, "xml");
List<LogicalAddressFileGenerator.Record> 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<String> 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; }
}
}

View File

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

View File

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

View File

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

View File

@ -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<Mode> 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("选择目录文件");
// 设置文件过滤器只允许DBFXMLxlsxxls格式
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 {
}
}
}

View File

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

View File

@ -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<File> 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<String> task = new Task<String>() {
@Override
protected String call() throws Exception {
log.info("开始执行哈希文件生成任务");
updateMessage("开始生成哈希文件...");
HashFileGenerator generator = new HashFileGenerator();
// 传递多个文件夹路径
List<Path> folderPaths = folders.stream().map(File::toPath).collect(ArrayList::new,
ArrayList::add,
ArrayList::addAll);
generator.generateHashFile(folderPaths, finalOutputFile.toPath(), (current, total) -> {
updateProgress(current, total);
updateMessage("处理文件: " + current + "/" + total);
if (current % 500 == 0 || current == total) { // 每500个文件或完成时记录一次日志
log.info("处理进度: {}/{}", 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();
}
}

View File

@ -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<String> {
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<String, List<Path>> hashToFileMap = new ConcurrentHashMap<>();
AtomicInteger processed = new AtomicInteger(0);
AtomicReference<Exception> errorRef = new AtomicReference<>(null);
AtomicBoolean scanCompleted = new AtomicBoolean(false);
// 使用 CountDownLatch 等待扫描完成
CountDownLatch latch = new CountDownLatch(1);
// 创建扫描器
scanner = new RobustParallelScanner(10);
// 异步启动扫描任务
Thread scanThread = new Thread(() -> {
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<Map.Entry<String, List<Path>>> 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<String, List<Path>> 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();
}
}
}

View File

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

View File

@ -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<String> supportedExtensions = Arrays.stream(supportedFileExtensions)
.map(String::toLowerCase)
.toList();
if (!supportedExtensions.contains(fileExtension)) {
log.error("不支持的文件格式: {}", fileExtension);
throw new IllegalArgumentException("不支持的文件格式,预期: "
+ supportedExtensions + ",实际: " + fileExtension);
}
return file;
}
}

View File

@ -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
____/`---'\\____
.' \\\\| |// `.

View File

@ -11,14 +11,13 @@
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<BorderPane fx:id="mainPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="400.0" style="-fx-background-color: rgb(245,245,245);" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="top.r3944realms.docchecktoolrefactored.ui.LoginStageController">
<BorderPane fx:id="mainPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="400.0" style="-fx-background-color: rgb(245,245,245);" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.LoginStageController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
<top>
<Label alignment="CENTER" text="淮阴区数字化档案检查验收系统" textFill="rgb(66,133,244)">
<Label alignment="CENTER" text="淮阴区数字化档案检查验收系统" textAlignment="CENTER" textFill="rgb(66,133,244)">
<font>
<Font name="Microsoft YaHei" size="24.0" />
</font>

View File

@ -1,13 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="1000.0" prefWidth="1200.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.MainStageController">
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="1000.0" prefWidth="1200.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.MainStageController">
<children>
<MenuBar prefWidth="2558.0" VBox.vgrow="ALWAYS">
<menus>
@ -28,7 +24,9 @@
</Menu>
</menus>
</MenuBar>
<fx:include source="module/project-info-pane.fxml" />
<!-- 导入项目信息面板 -->
<fx:include source="module/project-info-pane.fxml" VBox.vgrow="ALWAYS"/>
<!-- 导入项目内容面板 -->
<TabPane stylesheets="@../css/custom-tab.css" tabClosingPolicy="UNAVAILABLE" VBox.vgrow="ALWAYS">
<tabs>
<Tab id="startTab" fx:id="step1T" text="1. 查重复文件">
@ -36,11 +34,11 @@
<fx:include source="module/step-1-pane.fxml" />
</content>
</Tab>
<Tab fx:id="step2T" text="2. 查遗漏、存储路径和命名规范">
<content>
<fx:include source="module/step-2-pane.fxml" />
</content>
</Tab>
<Tab fx:id="step2T" styleClass="long-text-tab" text="2. 查遗漏、存储路径和命名规范">
<content>
<fx:include source="module/step-2-pane.fxml" />
</content>
</Tab>
<Tab fx:id="step3T" text="3. 查质量">
<content>
<fx:include source="module/step-3-pane.fxml" />

View File

@ -1,25 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<GridPane xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.ProjectInfoPaneController">
<GridPane xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.ProjectInfoPaneController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="-Infinity" minWidth="10.0" prefWidth="80.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="-Infinity" minWidth="10.0" prefWidth="80.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="ALWAYS" maxWidth="-Infinity" minWidth="10.0" prefWidth="80.0" />
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="ALWAYS" maxWidth="-Infinity" minWidth="10.0" prefWidth="80.0" />
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" percentWidth="20.0" prefWidth="100.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="30.0" prefHeight="45.0" vgrow="ALWAYS" />
<RowConstraints minHeight="30.0" prefHeight="45.0" vgrow="ALWAYS" />
<RowConstraints minHeight="30.0" prefHeight="45.0" vgrow="ALWAYS" />
<RowConstraints minHeight="30.0" prefHeight="45.0" vgrow="ALWAYS" />
</rowConstraints>
<children>
<Label text="项目名称:">
@ -29,6 +31,9 @@
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</Label>
<Label text="验收时间:" GridPane.rowIndex="1">
<GridPane.margin>
@ -37,6 +42,9 @@
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</Label>
<Label text="全宗号:" GridPane.rowIndex="2">
<GridPane.margin>
@ -45,6 +53,9 @@
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</Label>
<Label text="档案门类:" GridPane.rowIndex="3">
<GridPane.margin>
@ -53,14 +64,20 @@
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</Label>
<Label text="档案年度:" GridPane.columnIndex="2" GridPane.rowIndex="3">
<Label text="档年度:" GridPane.columnIndex="2" GridPane.rowIndex="3">
<GridPane.margin>
<Insets />
</GridPane.margin>
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</Label>
<TextField fx:id="projectNameTF" GridPane.columnIndex="1" GridPane.columnSpan="3">
<GridPane.margin>
@ -69,6 +86,9 @@
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</TextField>
<TextField fx:id="AcceptanceTimeTF" GridPane.columnIndex="1" GridPane.columnSpan="3" GridPane.rowIndex="1">
<GridPane.margin>
@ -77,6 +97,9 @@
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</TextField>
<TextField fx:id="totalCatalogNumberTF" GridPane.columnIndex="1" GridPane.columnSpan="3" GridPane.rowIndex="2">
<GridPane.margin>
@ -85,14 +108,20 @@
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</TextField>
<TextField fx:id="fileCategoriesTF" GridPane.columnIndex="1" GridPane.rowIndex="3">
<TextField fx:id="fileCategoriesTF" prefHeight="24.0" prefWidth="100.0" GridPane.columnIndex="1" GridPane.rowIndex="3">
<GridPane.margin>
<Insets />
</GridPane.margin>
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</TextField>
<TextField fx:id="fileYearTF" GridPane.columnIndex="3" GridPane.rowIndex="3">
<GridPane.margin>
@ -101,9 +130,20 @@
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</TextField>
<Button fx:id="infoResetB" mnemonicParsing="false" onAction="#onReset" prefHeight="93.0" prefWidth="55.0" text="重置" GridPane.columnIndex="4" GridPane.rowSpan="4">
<GridPane.margin>
<Insets bottom="10.0" left="20.0" right="10.0" top="10.0" />
</GridPane.margin>
<font>
<Font size="14.0" />
</font>
</Button>
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</GridPane>
</GridPane>

View File

@ -8,50 +8,69 @@
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.DuplicateDocumentPaneController">
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.DuplicateDocumentPaneController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="288.0" minWidth="0.0" percentWidth="0.0" prefWidth="82.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="1263.9999633789064" minWidth="0.0" prefWidth="745.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="873.0" minWidth="0.0" prefWidth="82.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="873.0" minWidth="10.0" prefWidth="407.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="450.0" minWidth="10.0" prefWidth="400.0" />
<ColumnConstraints />
</columnConstraints>
<rowConstraints>
<RowConstraints maxHeight="173.0" minHeight="10.0" prefHeight="53.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="173.0" minHeight="10.0" prefHeight="83.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="173.0" minHeight="10.0" prefHeight="31.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="549.0" minHeight="10.0" prefHeight="549.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="173.0" minHeight="10.0" prefHeight="71.00000127156576" vgrow="SOMETIMES" />
<RowConstraints maxHeight="173.0" minHeight="10.0" prefHeight="77.66666539510092" vgrow="SOMETIMES" />
<RowConstraints maxHeight="173.0" minHeight="10.0" prefHeight="49.33331298828125" vgrow="SOMETIMES" />
<RowConstraints maxHeight="592.6666666666666" minHeight="10.0" prefHeight="581.3333536783855" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<TextArea fx:id="result1B" GridPane.columnSpan="3" GridPane.rowIndex="3">
<TextArea fx:id="result1B" editable="false" GridPane.columnSpan="3" GridPane.rowIndex="3">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</TextArea>
<Label text="载入文件夹:" />
<Label text="载入文件夹:">
<GridPane.margin>
<Insets left="10.0" />
</GridPane.margin></Label>
<TextField fx:id="loadFolder1TF" GridPane.columnIndex="1">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</TextField>
<Button fx:id="selectLoadFolder1B" mnemonicParsing="false" onAction="#onSelectFolder" text="选择文件夹" GridPane.columnIndex="2" GridPane.columnSpan="2">
<Button fx:id="selectLoadFolder1B" mnemonicParsing="false" onAction="#onSelectFolder" text="选择文件夹" GridPane.columnIndex="3" GridPane.columnSpan="2" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin></Button>
<Button fx:id="start1B" minWidth="-Infinity" mnemonicParsing="false" onAction="#onStart" prefHeight="75.0" prefWidth="800.0" text="开始检查" GridPane.columnSpan="4" GridPane.rowIndex="1">
<Button fx:id="start1B" minWidth="-Infinity" mnemonicParsing="false" onAction="#onStart" prefHeight="58.0" prefWidth="650.0" text="开始检查" GridPane.columnSpan="3" GridPane.halignment="LEFT" GridPane.rowIndex="1" GridPane.valignment="CENTER">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
<Insets bottom="2.0" left="10.0" right="2.0" top="2.0" />
</GridPane.margin>
</Button>
<Label text="反馈结果:" GridPane.rowIndex="2" />
<TextArea editable="false" prefHeight="530.0" prefWidth="300.0" text="在此反馈结果:&#10;如无重复文件,则反馈:无重复文件;&#10;如有重复文件,则在此反馈档号。&#10;" GridPane.columnIndex="3" GridPane.columnSpan="2" GridPane.rowIndex="3">
<Label text="反馈结果:" GridPane.rowIndex="2">
<GridPane.margin>
<Insets left="10.0" />
</GridPane.margin></Label>
<TextArea editable="false" maxWidth="1.7976931348623157E308" prefWidth="400.0" text="批量取出数字化成果的哈希值采用对比法查找重复文件导出重复文件搜索结果进行人工一一比对并将比对台帐和统计结果填入查重登记表附件1。&#10;" wrapText="true" GridPane.columnIndex="3" GridPane.columnSpan="2" GridPane.rowIndex="3">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
<font>
<Font size="14.0" />
</font>
</TextArea>
<Label text="工作内容:" GridPane.columnIndex="3" GridPane.rowIndex="2">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<GridPane.margin>
<Insets left="10.0" />
</GridPane.margin>
</Label>
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />

View File

@ -9,8 +9,9 @@
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.PathCheckPaneController">
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.PathCheckPaneController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="940.0" minWidth="0.0" percentWidth="0.0" prefWidth="104.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="940.0" minWidth="10.0" percentWidth="0.0" prefWidth="104.0" />
@ -19,29 +20,38 @@
<ColumnConstraints hgrow="SOMETIMES" maxWidth="965.0" minWidth="10.0" prefWidth="867.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints maxHeight="224.0" minHeight="0.0" prefHeight="34.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="289.0" minHeight="10.0" prefHeight="40.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="373.0" minHeight="10.0" prefHeight="131.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="224.0" minHeight="0.0" prefHeight="29.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="500.0" minHeight="10.0" prefHeight="493.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="224.0" minHeight="0.0" prefHeight="49.333343505859375" vgrow="SOMETIMES" />
<RowConstraints maxHeight="289.0" minHeight="10.0" prefHeight="68.66666920979819" vgrow="SOMETIMES" />
<RowConstraints maxHeight="373.0" minHeight="10.0" prefHeight="76.66666412353516" vgrow="SOMETIMES" />
<RowConstraints maxHeight="224.0" minHeight="0.0" prefHeight="31.333333333333343" vgrow="SOMETIMES" />
<RowConstraints maxHeight="573.3333180745443" minHeight="10.0" prefHeight="553.999989827474" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Label text="载入目录:">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<GridPane.margin>
<Insets left="10.0" />
</GridPane.margin>
</Label>
<Label text="载入文件夹:" GridPane.rowIndex="1">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<GridPane.margin>
<Insets left="10.0" />
</GridPane.margin>
</Label>
<Label text="反馈结果:" GridPane.rowIndex="3">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<GridPane.margin>
<Insets left="10.0" />
</GridPane.margin>
</Label>
<TextArea fx:id="result2TA" prefHeight="473.0" prefWidth="1754.0" GridPane.columnSpan="4" GridPane.rowIndex="4">
<TextArea fx:id="result2TA" editable="false" GridPane.columnSpan="4" GridPane.rowIndex="4">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
@ -49,15 +59,18 @@
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</TextArea>
<Button fx:id="start2B" mnemonicParsing="false" onAction="#onStart" prefHeight="75.0" prefWidth="800.0" text="开始比对" GridPane.columnSpan="5" GridPane.rowIndex="2">
<Button fx:id="start2B" mnemonicParsing="false" onAction="#onStart" prefHeight="75.0" prefWidth="800.0" text="开始比对" GridPane.columnSpan="4" GridPane.halignment="LEFT" GridPane.rowIndex="2" GridPane.valignment="CENTER">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
<font>
<Font size="14.0" />
</font>
</Button>
<TextField fx:id="loadCatalog2TF" GridPane.columnIndex="1" GridPane.columnSpan="2">
<TextField fx:id="loadCatalog2TF" GridPane.columnIndex="2">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
@ -73,7 +86,7 @@
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</TextField>
<Button fx:id="selectLoadCatalog2B" mnemonicParsing="false" onAction="#onSelectLC" text="选择文件" GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
<Button fx:id="selectLoadCatalog2B" mnemonicParsing="false" onAction="#onSelectLC" text=" 选择文件 " GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
@ -81,7 +94,7 @@
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</Button>
<Button fx:id="selectJPGFolder2B" mnemonicParsing="false" onAction="#onSelectJPGF" text="选择文件夹" GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
<Button fx:id="selectJPGFolder2B" mnemonicParsing="false" onAction="#onSelectJPGF" text=" 选择文件夹" GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
@ -89,7 +102,7 @@
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</Button>
<Button fx:id="generateLogicalAddress2B" mnemonicParsing="false" onAction="#onGenerateLA" text="生成逻辑地址" GridPane.columnIndex="4" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
<Button fx:id="generateLogicalAddress2B" mnemonicParsing="false" onAction="#onGenerateLA" text="生成逻辑地址文件" GridPane.columnIndex="4" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
@ -97,7 +110,7 @@
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</Button>
<Button fx:id="generatePhysicalAddress2B" mnemonicParsing="false" onAction="#onGeneratePA" text="生成物理地址" GridPane.columnIndex="4" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
<Button fx:id="generatePhysicalAddress2B" mnemonicParsing="false" onAction="#onGeneratePA" text="生成物理地址文件" GridPane.columnIndex="4" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
@ -105,19 +118,27 @@
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</Button>
<TextArea editable="false" prefHeight="473.0" prefWidth="673.0" text="在此反馈结果:&#10;目录对应文件XXXX件XXXX页&#10;实际文件XXXX件XXXX页&#10;如比对一致,告知文件路径一致,文件名无误,无遗漏;&#10;如有问题,则&#10;①文件路径不一致,请检查,可反馈档号,也可不反馈;&#10;②反馈遗漏档号,&#10;③或文件名错误错误文件名为XXXX" GridPane.columnIndex="4" GridPane.rowIndex="4">
<TextArea editable="false" prefHeight="450.0" prefWidth="400.0" text="①获取数字化成果文件的物理存储路径(实际存储位置)&#10;②处理案卷级/文件级目录生成逻辑存储路径(理论存储位置)&#10;③自动对比物理路径与逻辑路径,识别以下问题:&#10; 1)文件漏扫/存储路径错误/命名不规范的文件&#10; 2)目录数据库档号错误或页数著录。&#10;④统计结果填入《查遗漏、查存储路径和命名规范登记表》附件2。&#10;&#10;" wrapText="true" GridPane.columnIndex="4" GridPane.rowIndex="4">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
<font>
<Font size="14.0" />
</font>
</TextArea>
<ChoiceBox fx:id="loadFolderType2CB" prefHeight="23.0" prefWidth="98.0" GridPane.columnIndex="1" GridPane.rowIndex="1">
<ChoiceBox fx:id="loadFolderType2CB" prefHeight="40.0" prefWidth="98.0" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowSpan="2" GridPane.valignment="CENTER">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
<Insets left="10.0" right="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
</ChoiceBox>
<Label text="工作内容:" GridPane.columnIndex="4" GridPane.rowIndex="3">
<GridPane.margin>
<Insets left="10.0" />
</GridPane.margin>
</Label>
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />

View File

@ -1,7 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>
<AnchorPane xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
</AnchorPane>
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17">
<children>
<TextArea editable="false" prefHeight="800.0" prefWidth="1000.0" scrollLeft="1.0" text="工作内容:&#10; ①汇总前两步检查结果计算合格率要求100%&#10; ②若合格率达标按总页数5%比例抽检:&#10; 著录准确性/规范性/完整性要求100%合格率)&#10; 图像清晰度/倾斜度/黑边要求95%以上的合格率)&#10; ③结果填入《质量检查登记表》附件3。" wrapText="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<font>
<Font size="18.0" />
</font></TextArea>
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</AnchorPane>

View File

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

View File

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

View File

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

View File

@ -1,7 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<AnchorPane xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
</AnchorPane>
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.StorageCarrierPaneController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="226.33331298828125" minWidth="10.0" percentWidth="10.0" prefWidth="108.33333333333334" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="559.3333511352539" minWidth="10.0" percentWidth="40.0" prefWidth="373.00002034505206" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="345.3333435058594" minWidth="10.0" percentWidth="25.0" prefWidth="227.00004069010413" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="500.0" minWidth="10.0" percentWidth="25.0" prefWidth="400.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints maxHeight="151.33334350585938" minHeight="0.0" percentHeight="7.0" prefHeight="55.00001017252603" vgrow="SOMETIMES" />
<RowConstraints maxHeight="407.0" minHeight="0.0" percentHeight="7.0" prefHeight="73.33333841959634" vgrow="SOMETIMES" />
<RowConstraints maxHeight="508.33333333333326" minHeight="0.0" percentHeight="7.0" prefHeight="73.99999491373697" vgrow="SOMETIMES" />
<RowConstraints maxHeight="591.6666666666667" minHeight="10.0" percentHeight="7.0" prefHeight="43.33332316080731" vgrow="SOMETIMES" />
<RowConstraints maxHeight="602.0000152587891" minHeight="10.0" prefHeight="534.6666615804037" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Label text="载入数字化成果:">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<GridPane.margin>
<Insets left="5.0" />
</GridPane.margin>
</Label>
<Label text="将档案目录、哈希值列表文件和检测过程文件打包制成打包制成“数字化验收检测包.rar”压缩包" GridPane.columnSpan="4" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<font>
<Font name="System Bold" size="14.0" />
</font>
</Label>
<Label text="反馈结果:" GridPane.rowIndex="3">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<GridPane.margin>
<Insets left="5.0" />
</GridPane.margin>
</Label>
<Label text="载入压缩包:" GridPane.rowIndex="2">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<GridPane.margin>
<Insets left="5.0" />
</GridPane.margin>
</Label>
<TextField fx:id="loadDigitalOutcomes" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
</TextField>
<TextField fx:id="loadCompressedFile" GridPane.columnIndex="1" GridPane.rowIndex="2">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
</TextField>
<TextArea fx:id="result7TA" editable="false" GridPane.columnSpan="3" GridPane.hgrow="ALWAYS" GridPane.rowIndex="4">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
</TextArea>
<Button fx:id="selectLoadDigitalOutcomes7B" mnemonicParsing="false" onAction="#onSelectLD" text="选择文件" textAlignment="CENTER" GridPane.columnIndex="2" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</Button>
<Button fx:id="generateHashFile7B" mnemonicParsing="false" onAction="#onGenerateHF" text="生成哈希值列表文件" GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</Button>
<Button fx:id="selectLoadCompressedFile7B" mnemonicParsing="false" onAction="#onSelectLC" text="选择文件" GridPane.columnIndex="2" GridPane.halignment="CENTER" GridPane.rowIndex="2" GridPane.valignment="CENTER">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</Button>
<Button fx:id="caculateHash7B" mnemonicParsing="false" onAction="#onCaculateHash" text="计算哈希值" GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.rowIndex="2" GridPane.valignment="CENTER">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</Button>
<TextArea editable="false" prefWidth="400.0" text="①对照《存储载体检查登记表》附件7检查并记录存储载体的类型/数量/内容/可读性情况。&#10;②将数字化成果(包括单页、多页文件及目录)打包生成&quot;数字化验收检测包.rar&quot;(含目录、哈希值列表、检测文件)&#10;③计算并验证压缩包的MD5或哈希值" wrapText="true" GridPane.columnIndex="3" GridPane.rowIndex="4">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<font>
<Font size="14.0" />
</font>
</TextArea>
<Label text="工作内容:" GridPane.columnIndex="3" GridPane.rowIndex="3">
<GridPane.margin>
<Insets left="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
</Label>
<Button fx:id="clearSelectedFoldersButton" mnemonicParsing="false" onAction="#onClearSelectedFolders" text="清除" GridPane.columnIndex="2">
<font>
<Font size="14.0" />
</font>
</Button>
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</GridPane>

View File

@ -1,9 +0,0 @@
module top.r3944realms.docchecktoolrefactored.test {
requires static lombok;
requires org.slf4j;
requires top.r3944realms.docchecktoolrefactored;
requires org.junit.jupiter.api;
exports top.r3944realms.docchecktoolrefactored.test;
opens top.r3944realms.docchecktoolrefactored.test;
}

View File

@ -9,6 +9,7 @@ import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;
@ -28,16 +29,17 @@ public class DuplicateFinderPerformanceTest {
@RepeatedTest(5)
void compareFinderPerformance() throws IOException {
Path rootDir = Paths.get(TEST_PATH);
// Test finder WITHOUT pre-counting
long startWithoutPrecount = System.nanoTime();
DuplicateFinder finderWithoutPrecount = new DuplicateFinder(scanner, hashCalculator, false);
finderWithoutPrecount.findDuplicates(Paths.get(TEST_PATH));
finderWithoutPrecount.findDuplicates(rootDir);
long durationWithoutPrecount = System.nanoTime() - startWithoutPrecount;
// Test finder WITH pre-counting
long startWithPrecount = System.nanoTime();
DuplicateFinder finderWithPrecount = new DuplicateFinder(scanner, hashCalculator, true);
finderWithPrecount.findDuplicates(Paths.get(TEST_PATH));
finderWithPrecount.findDuplicates(rootDir);
long durationWithPrecount = System.nanoTime() - startWithPrecount;
// Convert to milliseconds