From eabdb649f14390e5ae3274747f513d7ab271a16e Mon Sep 17 00:00:00 2001 From: czhqwer Date: Tue, 1 Apr 2025 01:43:24 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E8=AE=BE=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E6=96=87=E4=BB=B6=E6=90=9C=E7=B4=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/cn/czh/config/AsyncConfig.java | 22 ++ .../czh/controller/FileSearchController.java | 42 +++ .../src/main/java/cn/czh/dto/IndexResult.java | 25 ++ .../cn/czh/service/IFileSearchService.java | 15 + .../service/impl/FileSearchServiceImpl.java | 79 +++++ .../java/cn/czh/utils/FileSearchUtil.java | 269 ++++++++++++++++++ 6 files changed, 452 insertions(+) create mode 100644 upload-file-backend/src/main/java/cn/czh/config/AsyncConfig.java create mode 100644 upload-file-backend/src/main/java/cn/czh/controller/FileSearchController.java create mode 100644 upload-file-backend/src/main/java/cn/czh/dto/IndexResult.java create mode 100644 upload-file-backend/src/main/java/cn/czh/service/IFileSearchService.java create mode 100644 upload-file-backend/src/main/java/cn/czh/service/impl/FileSearchServiceImpl.java create mode 100644 upload-file-backend/src/main/java/cn/czh/utils/FileSearchUtil.java diff --git a/upload-file-backend/src/main/java/cn/czh/config/AsyncConfig.java b/upload-file-backend/src/main/java/cn/czh/config/AsyncConfig.java new file mode 100644 index 0000000..3b7b2aa --- /dev/null +++ b/upload-file-backend/src/main/java/cn/czh/config/AsyncConfig.java @@ -0,0 +1,22 @@ +package cn.czh.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@Configuration +public class AsyncConfig { + + @Bean(name = "taskExecutor") + public Executor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); // 核心线程数 + executor.setMaxPoolSize(10); // 最大线程数 + executor.setQueueCapacity(100); // 任务队列大小 + executor.setThreadNamePrefix("Async-Task-Executor-"); + executor.initialize(); + return executor; + } +} diff --git a/upload-file-backend/src/main/java/cn/czh/controller/FileSearchController.java b/upload-file-backend/src/main/java/cn/czh/controller/FileSearchController.java new file mode 100644 index 0000000..57a4dea --- /dev/null +++ b/upload-file-backend/src/main/java/cn/czh/controller/FileSearchController.java @@ -0,0 +1,42 @@ +package cn.czh.controller; + +import cn.czh.base.Result; +import cn.czh.service.IFileSearchService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +@Slf4j +@RequestMapping("/fileSearch") +@RestController +public class FileSearchController { + + @Resource + private IFileSearchService fileSearchService; + + /** + * 获取系统磁盘列表 + */ + @GetMapping("/getDrives") + public Result getDrives() { + return Result.success(fileSearchService.getDrives()); + } + + /** + * 根据指定的磁盘列表构建文件索引 + */ + @PostMapping("/buildIndex") + public Result buildIndex(@RequestBody List drives) { + return Result.success(fileSearchService.buildIndex(drives)); + } + + /** + * 根据文件名称模糊查询 + */ + @GetMapping("/search") + public Result search(@RequestParam String keyword) { + return Result.success(fileSearchService.searchFiles(keyword)); + } +} diff --git a/upload-file-backend/src/main/java/cn/czh/dto/IndexResult.java b/upload-file-backend/src/main/java/cn/czh/dto/IndexResult.java new file mode 100644 index 0000000..d2f88be --- /dev/null +++ b/upload-file-backend/src/main/java/cn/czh/dto/IndexResult.java @@ -0,0 +1,25 @@ +package cn.czh.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 文件索引构建结果类 + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +public class IndexResult { + + private long indexTime; // 索引构建耗时(毫秒) + + private List indexedDrives; // 已索引的磁盘列表 + + private String memoryUsage; // 内存占用(MB,格式化为字符串) + + private long fileCount; // 索引文件总数 + +} \ No newline at end of file diff --git a/upload-file-backend/src/main/java/cn/czh/service/IFileSearchService.java b/upload-file-backend/src/main/java/cn/czh/service/IFileSearchService.java new file mode 100644 index 0000000..6f8f8a7 --- /dev/null +++ b/upload-file-backend/src/main/java/cn/czh/service/IFileSearchService.java @@ -0,0 +1,15 @@ +package cn.czh.service; + +import cn.czh.dto.IndexResult; + +import java.util.List; + +public interface IFileSearchService { + + List getDrives(); + + IndexResult buildIndex(List drives); + + List searchFiles(String keyword); + +} diff --git a/upload-file-backend/src/main/java/cn/czh/service/impl/FileSearchServiceImpl.java b/upload-file-backend/src/main/java/cn/czh/service/impl/FileSearchServiceImpl.java new file mode 100644 index 0000000..4971ce7 --- /dev/null +++ b/upload-file-backend/src/main/java/cn/czh/service/impl/FileSearchServiceImpl.java @@ -0,0 +1,79 @@ +package cn.czh.service.impl; + +import cn.czh.dto.IndexResult; +import cn.czh.service.IFileSearchService; +import cn.czh.utils.FileSearchUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.PreDestroy; +import javax.annotation.Resource; +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class FileSearchServiceImpl implements IFileSearchService { + + @Resource + private FileSearchUtil fileSearchUtil; + + @PreDestroy + public void destroy() { + log.info("关闭 FileSearchUtil 资源..."); + fileSearchUtil.shutdown(); + } + + @Override + public List getDrives() { + List availableDrives = fileSearchUtil.getAvailableDrives(); + return availableDrives.isEmpty() ? Collections.emptyList() : availableDrives.stream().map(File::getAbsolutePath).collect(Collectors.toList()); + } + + @Override + public IndexResult buildIndex(List driveNames) { + List availableDrives = fileSearchUtil.getAvailableDrives(); + if (availableDrives.isEmpty()) { + throw new RuntimeException("未检测到可用磁盘!"); + } + + List selectedDrives = new ArrayList<>(); + Set selectedNames = new HashSet<>(); // 用于去重 + for (String driveName : driveNames) { + if (driveName.isEmpty()) continue; + boolean found = false; + for (File drive : availableDrives) { + String normalizedDriveName = driveName.endsWith(File.separator) ? driveName : driveName + File.separator; + if (drive.getAbsolutePath().equalsIgnoreCase(driveName) || + drive.getAbsolutePath().equalsIgnoreCase(normalizedDriveName)) { + if (selectedNames.add(drive.getAbsolutePath().toLowerCase())) { + selectedDrives.add(drive); + } + found = true; + break; + } + } + if (!found) { + log.warn("警告:无效的磁盘名 '{}',将被忽略", driveName); + } + } + + if (selectedDrives.isEmpty()) { + throw new RuntimeException("未选择任何有效磁盘!"); + } + + long indexTime = fileSearchUtil.buildIndex(selectedDrives); + return new IndexResult( + indexTime, + selectedDrives.stream().map(File::getAbsolutePath).collect(Collectors.toList()), + String.format("%.2f", fileSearchUtil.getMemoryUsageMB()), + fileSearchUtil.getIndexedFileCount() + ); + } + + @Override + public List searchFiles(String keyword) { + return fileSearchUtil.search(keyword); + } +} diff --git a/upload-file-backend/src/main/java/cn/czh/utils/FileSearchUtil.java b/upload-file-backend/src/main/java/cn/czh/utils/FileSearchUtil.java new file mode 100644 index 0000000..cb13bb9 --- /dev/null +++ b/upload-file-backend/src/main/java/cn/czh/utils/FileSearchUtil.java @@ -0,0 +1,269 @@ +package cn.czh.utils; + +import cn.czh.base.BusinessException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.Collectors; + +/** + * 文件搜索工具类,使用 ForkJoinPool 实现高效并行文件索引和搜索 + */ +@Component +public class FileSearchUtil { + private static final Logger log = LoggerFactory.getLogger(FileSearchUtil.class); + private final Map> fileIndex = new ConcurrentHashMap<>(); + private final ForkJoinPool forkJoinPool; + private volatile boolean isIndexed = false; + + /** + * 构造函数,初始化线程池 + * + * @param threadMultiplier 线程数倍乘因子,默认使用 CPU 核心数 * 2 + */ + public FileSearchUtil(int threadMultiplier) { + this.forkJoinPool = new ForkJoinPool( + Math.max(4, Runtime.getRuntime().availableProcessors() * threadMultiplier)); + } + + /** + * 默认构造函数,使用 CPU 核心数 * 2 作为线程数 + */ + public FileSearchUtil() { + this(2); + } + + /** + * 获取系统中所有可用磁盘列表 + * + * @return 磁盘根目录列表 + */ + public List getAvailableDrives() { + File[] roots = File.listRoots(); + return roots != null ? Arrays.asList(roots) : Collections.emptyList(); + } + + /** + * 根据指定的磁盘列表构建文件索引 + * + * @param selectedDrives 要索引的磁盘列表 + * @return 索引构建所用时间(毫秒) + * @throws IllegalArgumentException 如果传入的磁盘列表为空或无效 + */ + public long buildIndex(List selectedDrives) { + if (isIndexed) { + return 0; // 已构建过索引,直接返回 + } + + if (selectedDrives == null || selectedDrives.isEmpty()) { + throw new BusinessException("所选驱动器列表不能为空"); + } + + long startTime = System.currentTimeMillis(); + List> tasks = new ArrayList<>(); + + for (File drive : selectedDrives) { + if (drive != null && drive.exists() && drive.isDirectory()) { + tasks.add(forkJoinPool.submit(new IndexTask(drive))); + } + } + + if (tasks.isEmpty()) { + throw new BusinessException("没有选择索引的有效驱动器"); + } + + // 等待所有任务完成 + for (Future task : tasks) { + try { + task.get(); + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + throw new BusinessException("索引构建中断", e); + } + } + + isIndexed = true; + return System.currentTimeMillis() - startTime; + } + + /** + * 搜索文件(模糊匹配) + * + * @param keyword 搜索关键词 + * @return 匹配的文件路径列表 + */ + public List search(String keyword) { + if (!isIndexed) { + throw new BusinessException("请先构建索引"); + } + + if (keyword == null || keyword.trim().isEmpty()) { + return Collections.emptyList(); + } + + String searchKey = keyword.toLowerCase(); + return fileIndex.entrySet().stream() + .filter(entry -> entry.getKey().contains(searchKey)) + .flatMap(entry -> entry.getValue().stream()) + .collect(Collectors.toList()); + } + + /** + * 获取索引中的文件总数 + * + * @return 文件总数 + */ + public long getIndexedFileCount() { + return fileIndex.values().stream() + .mapToLong(ConcurrentLinkedQueue::size) + .sum(); + } + + /** + * 获取索引占用内存估计值(MB) + * + * @return 内存占用(MB) + */ + public double getMemoryUsageMB() { + return fileIndex.size() * 16.0 / 1024 / 1024; + } + + /** + * 关闭线程池,释放资源 + */ + public void shutdown() { + if (!forkJoinPool.isShutdown()) { + forkJoinPool.shutdown(); + log.info("ForkJoinPool 已关闭"); + } + } + + // 内部索引任务类 + private class IndexTask extends RecursiveAction { + private final File directory; + + public IndexTask(File directory) { + this.directory = directory; + } + + @Override + protected void compute() { + try { + File[] files = directory.listFiles(); + if (files == null) return; + + List subTasks = new ArrayList<>(); + + for (File file : files) { + if (file.isDirectory()) { + IndexTask subTask = new IndexTask(file); + subTasks.add(subTask); + } else { + String fileName = file.getName().toLowerCase(); + fileIndex.computeIfAbsent(fileName, k -> new ConcurrentLinkedQueue<>()) + .add(file.getAbsolutePath()); + } + } + + if (subTasks.size() < 4) { + for (IndexTask subTask : subTasks) { + subTask.compute(); + } + } else { + invokeAll(subTasks); + } + } catch (Exception e) { + log.error("访问目录失败: {}", directory, e); + } + } + } + + // 交互式多选磁盘和搜索 + /*public static void main(String[] args) { + FileSearchUtil searchUtil = new FileSearchUtil(); + Scanner scanner = new Scanner(System.in); + + // 1. 获取并显示所有磁盘 + List drives = searchUtil.getAvailableDrives(); + if (drives.isEmpty()) { + System.out.println("未检测到可用磁盘!"); + searchUtil.shutdown(); + return; + } + + System.out.println("可用磁盘列表:"); + for (int i = 0; i < drives.size(); i++) { + System.out.println((i + 1) + ". " + drives.get(i).getAbsolutePath()); + } + + // 2. 提示用户输入磁盘名(支持多选,用逗号分隔) + System.out.print("请输入要搜索的磁盘名(多个磁盘用逗号分隔,例如 C:,D:):"); + String input = scanner.nextLine().trim(); + + // 3. 解析用户输入并选择磁盘 + String[] driveNames = input.split("\\s*,\\s*"); // 支持带空格的逗号分隔 + List selectedDrives = new ArrayList<>(); + Set selectedNames = new HashSet<>(); // 用于去重 + + for (String driveName : driveNames) { + if (driveName.isEmpty()) continue; + boolean found = false; + for (File drive : drives) { + String normalizedDriveName = driveName.endsWith(File.separator) ? driveName : driveName + File.separator; + if (drive.getAbsolutePath().equalsIgnoreCase(driveName) || + drive.getAbsolutePath().equalsIgnoreCase(normalizedDriveName)) { + if (selectedNames.add(drive.getAbsolutePath().toLowerCase())) { + selectedDrives.add(drive); + } + found = true; + break; + } + } + if (!found) { + System.out.println("警告:无效的磁盘名 '" + driveName + "',将被忽略"); + } + } + + if (selectedDrives.isEmpty()) { + System.out.println("未选择任何有效磁盘!"); + searchUtil.shutdown(); + scanner.close(); + return; + } + + // 4. 构建指定磁盘的索引 + long indexTime = searchUtil.buildIndex(selectedDrives); + System.out.println("索引建立完成,用时:" + indexTime + "ms"); + System.out.println("已索引磁盘:" + selectedDrives.stream() + .map(File::getAbsolutePath) + .collect(Collectors.joining(", "))); + System.out.println("索引内存占用:" + String.format("%.2f", searchUtil.getMemoryUsageMB()) + "MB"); + System.out.println("索引文件总数:" + searchUtil.getIndexedFileCount()); + + // 5. 提示用户输入搜索关键词 + System.out.print("请输入搜索关键词:"); + String keyword = scanner.nextLine().trim(); + + // 6. 执行搜索 + long startTime = System.currentTimeMillis(); + List results = searchUtil.search(keyword); + long searchTime = System.currentTimeMillis() - startTime; + + // 7. 输出结果 + System.out.println("搜索耗时:" + searchTime + "ms"); + if (!results.isEmpty()) { + System.out.println("找到 " + results.size() + " 个匹配文件:"); + results.forEach(System.out::println); + } else { + System.out.println("未找到文件:" + keyword); + } + + // 8. 清理资源 + searchUtil.shutdown(); + scanner.close(); + }*/ +} \ No newline at end of file