# Prompt First - 文件哈希对比和增量检测 ## 输入引用 引用相关的 docs 文档链接: - [REQ-010-12.md](../requirements/REQ-010-12.md) - 文件哈希对比和增量检测需求文档 - [0021-file-hash-comparison.md](../decisions/adr/0021-file-hash-comparison.md) - 文件哈希对比和增量检测架构决策 - [REQ-010-7.md](../requirements/REQ-010-7.md) - 文件存储和解压处理需求文档 - [REQ-010-11.md](../requirements/REQ-010-11.md) - 元数据组件索引和查询需求文档 - [file/index.md](../api-docs/file/index.md) - 文件模块 API 文档索引(唯一真源) ## Context Maps 强制列出本次 Prompt 依赖的 Canvas 文件: - [Authentication.canvas](../Authentication.canvas) - 项目架构视觉化展示 - **相关节点**: [集成核心](node_integration_core) - 提供与Salesforce的各种连接方式 ## 目标 实现文件哈希对比和增量检测功能,支持检测文件变化,只拉取或部署变化的文件。包括文件哈希计算、哈希对比、增量检测、增量拉取、增量部署等功能。 ## 输出格式 代码示例(语言:Java) ## 约束 - **技术栈限制**: 必须基于现有的 Spring Boot 3 + MyBatis Plus 技术栈 - **架构约束**: 必须遵循 Authentication.canvas 中定义的架构和调用关系 - **模块约束**: 必须在 datai-salesforce-metadata 模块下实现 - **数据库约束**: 必须使用 MyBatis Plus 作为持久层框架 - **性能约束**: 哈希计算和对比性能必须满足要求 - **算法约束**: 必须使用 SHA-256 算法计算哈希 - **依赖约束**: 必须复用现有的 IFileStorageService、IMetadataRetrieveService、IMetadataDeployService 服务接口 ## Rule Set "请严格参考 @Authentication.canvas 中的状态机转移逻辑,不要自行发挥。" **具体规则**: - 必须使用 Canvas 中定义的类名和方法名 - 必须遵循 Canvas 中定义的调用关系 - 必须参考 Canvas 中的流程图逻辑 ## 验收标准 - **功能完整性**: 所有文件哈希对比和增量检测功能能够正常工作 - **性能指标**: 哈希计算和对比性能满足要求,增量拉取和部署性能满足要求 - **准确性**: 增量检测结果准确,只拉取或部署变化的文件 - **代码规范性**: 代码符合项目编码规范,有清晰的注释 - **可维护性**: 代码结构清晰,易于扩展和维护 - **可测试性**: 代码易于单元测试和集成测试 ## 风险 - **哈希计算风险**: 哈希计算错误可能导致增量检测不准确 - **对比性能风险**: 对比性能不佳可能影响用户体验 - **增量检测风险**: 增量检测不准确可能导致遗漏或误判 - **增量拉取风险**: 增量拉取失败可能导致文件不一致 - **增量部署风险**: 增量部署失败可能导致部署不完整 ## 使用记录 | 日期 | 使用场景 | 输入参数 | 输出结果 | 反馈 | 改进措施 | |------|---------|---------|---------|------|----------| | 2026-01-19 | 实现文件哈希对比和增量检测功能 | REQ-010-12.md, 0021-file-hash-comparison.md | 完整的代码实现 | 无 | 无 | --- ## 核心代码实现 ### 1. IFileHashService 服务接口 ```java package com.datai.salesforce.metadata.service.filehash; import java.io.File; import java.util.Map; /** * 文件哈希服务接口 */ public interface IFileHashService { /** * 计算文件哈希值 * @param file 文件 * @return 哈希值 */ String calculateHash(File file); /** * 批量计算文件哈希值 * @param files 文件列表 * @return 哈希值映射(文件路径 -> 哈希值) */ Map calculateHashes(File[] files); /** * 计算文件哈希值(异步) * @param file 文件 * @return 哈希值 */ java.util.concurrent.Future calculateHashAsync(File file); } ``` ### 2. FileHashServiceImpl 服务实现 ```java package com.datai.salesforce.metadata.service.filehash.impl; import com.datai.salesforce.metadata.service.filehash.IFileHashService; import org.springframework.stereotype.Service; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.security.MessageDigest; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; /** * 文件哈希服务实现类 */ @Service public class FileHashServiceImpl implements IFileHashService { private static final int BUFFER_SIZE = 8192; private static final String HASH_ALGORITHM = "SHA-256"; @Override public String calculateHash(File file) { try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) { MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM); byte[] buffer = new byte[BUFFER_SIZE]; int bytesRead; while ((bytesRead = bis.read(buffer)) != -1) { digest.update(buffer, 0, bytesRead); } byte[] hashBytes = digest.digest(); StringBuilder hexString = new StringBuilder(); for (byte b : hashBytes) { String hex = Integer.toHexString(0xff & b); if (hex.length() == 1) { hexString.append('0'); } hexString.append(hex); } return hexString.toString(); } catch (Exception e) { throw new RuntimeException("Failed to calculate file hash", e); } } @Override public Map calculateHashes(File[] files) { Map hashMap = new HashMap<>(); for (File file : files) { String hash = calculateHash(file); hashMap.put(file.getAbsolutePath(), hash); } return hashMap; } @Override public java.util.concurrent.Future calculateHashAsync(File file) { return CompletableFuture.supplyAsync(() -> calculateHash(file)); } } ``` ### 3. IHashComparisonService 服务接口 ```java package com.datai.salesforce.metadata.service.hashcomparison; import java.util.Map; /** * 哈希对比服务接口 */ public interface IHashComparisonService { /** * 对比哈希值 * @param hash1 哈希值1 * @param hash2 哈希值2 * @return 是否相等 */ boolean compare(String hash1, String hash2); /** * 批量对比哈希值 * @param hashMap1 哈希值映射1 * @param hashMap2 哈希值映射2 * @return 对比结果(文件路径 -> 是否相等) */ Map compare(Map hashMap1, Map hashMap2); /** * 查找变化的文件 * @param hashMap1 哈希值映射1(旧) * @param hashMap2 哈希值映射2(新) * @return 变化的文件路径列表 */ java.util.Set findChangedFiles(Map hashMap1, Map hashMap2); } ``` ### 4. HashComparisonServiceImpl 服务实现 ```java package com.datai.salesforce.metadata.service.hashcomparison.impl; import com.datai.salesforce.metadata.service.hashcomparison.IHashComparisonService; import org.springframework.stereotype.Service; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * 哈希对比服务实现类 */ @Service public class HashComparisonServiceImpl implements IHashComparisonService { @Override public boolean compare(String hash1, String hash2) { if (hash1 == null && hash2 == null) { return true; } if (hash1 == null || hash2 == null) { return false; } return hash1.equals(hash2); } @Override public Map compare(Map hashMap1, Map hashMap2) { Map result = new ConcurrentHashMap<>(); Set allPaths = new HashSet<>(); allPaths.addAll(hashMap1.keySet()); allPaths.addAll(hashMap2.keySet()); allPaths.parallelStream().forEach(path -> { String hash1 = hashMap1.get(path); String hash2 = hashMap2.get(path); result.put(path, compare(hash1, hash2)); }); return result; } @Override public Set findChangedFiles(Map hashMap1, Map hashMap2) { Set changedFiles = new HashSet<>(); Set allPaths = new HashSet<>(); allPaths.addAll(hashMap1.keySet()); allPaths.addAll(hashMap2.keySet()); allPaths.parallelStream().forEach(path -> { String hash1 = hashMap1.get(path); String hash2 = hashMap2.get(path); if (!compare(hash1, hash2)) { changedFiles.add(path); } }); return changedFiles; } } ``` ### 5. ChangeType 枚举 ```java package com.datai.salesforce.metadata.enums; /** * 文件变化类型枚举 */ public enum ChangeType { /** * 新增 */ ADDED, /** * 修改 */ MODIFIED, /** * 删除 */ DELETED } ``` ### 6. FileChange 实体类 ```java package com.datai.salesforce.metadata.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.datai.salesforce.metadata.enums.ChangeType; import lombok.Data; import java.time.LocalDateTime; /** * 文件变化实体类 */ @Data @TableName("datai_file_change") public class FileChange { /** * 主键ID */ @TableId(type = IdType.AUTO) private Long id; /** * 文件路径 */ private String filePath; /** * 变化类型 */ private ChangeType changeType; /** * 旧哈希值 */ private String oldHash; /** * 新哈希值 */ private String newHash; /** * 检测时间 */ private LocalDateTime detectedTime; /** * 任务ID */ private Long taskId; /** * 组织配置ID */ private Long orgConfigId; /** * 部门ID */ private Long deptId; /** * 创建人 */ private String createBy; /** * 创建时间 */ private LocalDateTime createTime; /** * 更新人 */ private String updateBy; /** * 更新时间 */ private LocalDateTime updateTime; /** * 备注 */ private String remark; } ``` ### 7. IIncrementalDetectionService 服务接口 ```java package com.datai.salesforce.metadata.service.incremental; import com.datai.salesforce.metadata.entity.FileChange; import com.datai.salesforce.metadata.enums.ChangeType; import java.io.File; import java.util.List; import java.util.Map; /** * 增量检测服务接口 */ public interface IIncrementalDetectionService { /** * 检测文件变化 * @param oldHashMap 旧哈希值映射 * @param newHashMap 新哈希值映射 * @return 文件变化列表 */ List detectChanges(Map oldHashMap, Map newHashMap); /** * 检测文件变化(异步) * @param oldHashMap 旧哈希值映射 * @param newHashMap 新哈希值映射 * @return 文件变化列表 */ java.util.concurrent.Future> detectChangesAsync(Map oldHashMap, Map newHashMap); /** * 监听文件系统变化 * @param directory 目录 * @param callback 回调函数 */ void watchFileSystem(File directory, FileChangeCallback callback); /** * 停止监听文件系统变化 */ void stopWatching(); } /** * 文件变化回调接口 */ interface FileChangeCallback { void onFileChange(String filePath, ChangeType changeType); } ``` ### 8. IncrementalDetectionServiceImpl 服务实现 ```java package com.datai.salesforce.metadata.service.incremental.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.datai.salesforce.metadata.entity.FileChange; import com.datai.salesforce.metadata.enums.ChangeType; import com.datai.salesforce.metadata.mapper.FileChangeMapper; import com.datai.salesforce.metadata.service.incremental.IIncrementalDetectionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.File; import java.nio.file.*; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; /** * 增量检测服务实现类 */ @Service public class IncrementalDetectionServiceImpl implements IIncrementalDetectionService { @Autowired private FileChangeMapper fileChangeMapper; private WatchService watchService; private Map watchKeys; private Thread watchThread; private volatile boolean watching; @Override public List detectChanges(Map oldHashMap, Map newHashMap) { List changes = new ArrayList<>(); Set allPaths = new HashSet<>(); allPaths.addAll(oldHashMap.keySet()); allPaths.addAll(newHashMap.keySet()); for (String path : allPaths) { String oldHash = oldHashMap.get(path); String newHash = newHashMap.get(path); if (oldHash == null && newHash != null) { FileChange change = new FileChange(); change.setFilePath(path); change.setChangeType(ChangeType.ADDED); change.setOldHash(null); change.setNewHash(newHash); change.setDetectedTime(LocalDateTime.now()); changes.add(change); } else if (oldHash != null && newHash == null) { FileChange change = new FileChange(); change.setFilePath(path); change.setChangeType(ChangeType.DELETED); change.setOldHash(oldHash); change.setNewHash(null); change.setDetectedTime(LocalDateTime.now()); changes.add(change); } else if (oldHash != null && newHash != null && !oldHash.equals(newHash)) { FileChange change = new FileChange(); change.setFilePath(path); change.setChangeType(ChangeType.MODIFIED); change.setOldHash(oldHash); change.setNewHash(newHash); change.setDetectedTime(LocalDateTime.now()); changes.add(change); } } return changes; } @Override public CompletableFuture> detectChangesAsync(Map oldHashMap, Map newHashMap) { return CompletableFuture.supplyAsync(() -> detectChanges(oldHashMap, newHashMap)); } @Override public void watchFileSystem(File directory, FileChangeCallback callback) { try { watchService = FileSystems.getDefault().newWatchService(); watchKeys = new HashMap<>(); watching = true; Path path = directory.toPath(); WatchKey key = path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE); watchKeys.put(key, path); watchThread = new Thread(() -> { while (watching) { try { WatchKey key = watchService.take(); Path dir = watchKeys.get(key); for (WatchEvent event : key.pollEvents()) { WatchEvent.Kind kind = event.kind(); if (kind == StandardWatchEventKinds.OVERFLOW) { continue; } Path filePath = dir.resolve((Path) event.context()); ChangeType changeType; if (kind == StandardWatchEventKinds.ENTRY_CREATE) { changeType = ChangeType.ADDED; } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) { changeType = ChangeType.MODIFIED; } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) { changeType = ChangeType.DELETED; } else { continue; } callback.onFileChange(filePath.toString(), changeType); } boolean valid = key.reset(); if (!valid) { watchKeys.remove(key); if (watchKeys.isEmpty()) { break; } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }); watchThread.start(); } catch (Exception e) { throw new RuntimeException("Failed to watch file system", e); } } @Override public void stopWatching() { watching = false; if (watchThread != null) { watchThread.interrupt(); } if (watchService != null) { try { watchService.close(); } catch (Exception e) { // ignore } } } } ``` ### 9. IIncrementalRetrieveService 服务接口 ```java package com.datai.salesforce.metadata.service.incremental; import com.datai.salesforce.metadata.entity.FileChange; import java.util.List; /** * 增量拉取服务接口 */ public interface IIncrementalRetrieveService { /** * 增量拉取 * @param taskId 任务ID * @param orgConfigId 组织配置ID * @param changes 文件变化列表 * @return 拉取结果 */ String incrementalRetrieve(Long taskId, Long orgConfigId, List changes); /** * 增量拉取(异步) * @param taskId 任务ID * @param orgConfigId 组织配置ID * @param changes 文件变化列表 * @return 拉取结果 */ java.util.concurrent.Future incrementalRetrieveAsync(Long taskId, Long orgConfigId, List changes); } ``` ### 10. IncrementalRetrieveServiceImpl 服务实现 ```java package com.datai.salesforce.metadata.service.incremental.impl; import com.datai.salesforce.metadata.entity.FileChange; import com.datai.salesforce.metadata.service.incremental.IIncrementalRetrieveService; import com.datai.salesforce.metadata.service.retrieve.IMetadataRetrieveService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; /** * 增量拉取服务实现类 */ @Service public class IncrementalRetrieveServiceImpl implements IIncrementalRetrieveService { @Autowired private IMetadataRetrieveService metadataRetrieveService; @Override public String incrementalRetrieve(Long taskId, Long orgConfigId, List changes) { String packageXml = buildPackageXml(changes); return metadataRetrieveService.retrieve(taskId, orgConfigId, packageXml); } @Override @Async("retrieveExecutor") public Future incrementalRetrieveAsync(Long taskId, Long orgConfigId, List changes) { return CompletableFuture.completedFuture(incrementalRetrieve(taskId, orgConfigId, changes)); } private String buildPackageXml(List changes) { StringBuilder sb = new StringBuilder(); sb.append("\n"); sb.append("\n"); for (FileChange change : changes) { String componentType = extractComponentType(change.getFilePath()); String componentName = extractComponentName(change.getFilePath()); sb.append(" \n"); sb.append(" ").append(componentName).append("\n"); sb.append(" ").append(componentType).append("\n"); sb.append(" \n"); } sb.append(" 56.0\n"); sb.append(""); return sb.toString(); } private String extractComponentType(String filePath) { String[] parts = filePath.split("\\\\"); return parts[parts.length - 2]; } private String extractComponentName(String filePath) { String fileName = filePath.substring(filePath.lastIndexOf("\\") + 1); return fileName.substring(0, fileName.lastIndexOf(".")); } } ``` ### 11. IIncrementalDeployService 服务接口 ```java package com.datai.salesforce.metadata.service.incremental; import com.datai.salesforce.metadata.entity.FileChange; import java.util.List; /** * 增量部署服务接口 */ public interface IIncrementalDeployService { /** * 增量部署 * @param taskId 任务ID * @param orgConfigId 组织配置ID * @param changes 文件变化列表 * @return 部署结果 */ String incrementalDeploy(Long taskId, Long orgConfigId, List changes); /** * 增量部署(异步) * @param taskId 任务ID * @param orgConfigId 组织配置ID * @param changes 文件变化列表 * @return 部署结果 */ java.util.concurrent.Future incrementalDeployAsync(Long taskId, Long orgConfigId, List changes); } ``` ### 12. IncrementalDeployServiceImpl 服务实现 ```java package com.datai.salesforce.metadata.service.incremental.impl; import com.datai.salesforce.metadata.entity.FileChange; import com.datai.salesforce.metadata.service.deploy.IMetadataDeployService; import com.datai.salesforce.metadata.service.incremental.IIncrementalDeployService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; /** * 增量部署服务实现类 */ @Service public class IncrementalDeployServiceImpl implements IIncrementalDeployService { @Autowired private IMetadataDeployService metadataDeployService; @Override public String incrementalDeploy(Long taskId, Long orgConfigId, List changes) { String packageXml = buildPackageXml(changes); return metadataDeployService.deploy(taskId, orgConfigId, packageXml); } @Override @Async("deployExecutor") public Future incrementalDeployAsync(Long taskId, Long orgConfigId, List changes) { return CompletableFuture.completedFuture(incrementalDeploy(taskId, orgConfigId, changes)); } private String buildPackageXml(List changes) { StringBuilder sb = new StringBuilder(); sb.append("\n"); sb.append("\n"); for (FileChange change : changes) { String componentType = extractComponentType(change.getFilePath()); String componentName = extractComponentName(change.getFilePath()); sb.append(" \n"); sb.append(" ").append(componentName).append("\n"); sb.append(" ").append(componentType).append("\n"); sb.append(" \n"); } sb.append(" 56.0\n"); sb.append(""); return sb.toString(); } private String extractComponentType(String filePath) { String[] parts = filePath.split("\\\\"); return parts[parts.length - 2]; } private String extractComponentName(String filePath) { String fileName = filePath.substring(filePath.lastIndexOf("\\") + 1); return fileName.substring(0, fileName.lastIndexOf(".")); } } ``` ### 13. FileHashController 控制器 ```java package com.datai.salesforce.metadata.controller; import com.datai.salesforce.metadata.service.filehash.IFileHashService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.io.File; import java.util.Map; /** * 文件哈希控制器 */ @RestController @RequestMapping("/api/metadata/file-hash") public class FileHashController { @Autowired private IFileHashService fileHashService; @PostMapping("/calculate") public Map calculateHash(@RequestBody Map request) { String filePath = request.get("filePath"); File file = new File(filePath); String hash = fileHashService.calculateHash(file); Map response = new HashMap<>(); response.put("success", true); response.put("hash", hash); return response; } @PostMapping("/calculate-batch") public Map calculateHashes(@RequestBody Map request) { String[] filePaths = request.get("filePaths"); File[] files = new File[filePaths.length]; for (int i = 0; i < filePaths.length; i++) { files[i] = new File(filePaths[i]); } Map hashMap = fileHashService.calculateHashes(files); Map response = new HashMap<>(); response.put("success", true); response.put("hashMap", hashMap); return response; } } ``` ### 14. HashComparisonController 控制器 ```java package com.datai.salesforce.metadata.controller; import com.datai.salesforce.metadata.service.hashcomparison.IHashComparisonService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.Map; /** * 哈希对比控制器 */ @RestController @RequestMapping("/api/metadata/hash-comparison") public class HashComparisonController { @Autowired private IHashComparisonService hashComparisonService; @PostMapping("/compare") public Map compare(@RequestBody Map> request) { Map hashMap1 = request.get("hashMap1"); Map hashMap2 = request.get("hashMap2"); Map result = hashComparisonService.compare(hashMap1, hashMap2); Map response = new HashMap<>(); response.put("success", true); response.put("result", result); return response; } @PostMapping("/find-changed") public Map findChangedFiles(@RequestBody Map> request) { Map hashMap1 = request.get("hashMap1"); Map hashMap2 = request.get("hashMap2"); java.util.Set changedFiles = hashComparisonService.findChangedFiles(hashMap1, hashMap2); Map response = new HashMap<>(); response.put("success", true); response.put("changedFiles", changedFiles); return response; } } ``` ### 15. IncrementalDetectionController 控制器 ```java package com.datai.salesforce.metadata.controller; import com.datai.salesforce.metadata.entity.FileChange; import com.datai.salesforce.metadata.service.incremental.IIncrementalDetectionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; /** * 增量检测控制器 */ @RestController @RequestMapping("/api/metadata/incremental-detection") public class IncrementalDetectionController { @Autowired private IIncrementalDetectionService incrementalDetectionService; @PostMapping("/detect") public Map detectChanges(@RequestBody Map> request) { Map oldHashMap = request.get("oldHashMap"); Map newHashMap = request.get("newHashMap"); List changes = incrementalDetectionService.detectChanges(oldHashMap, newHashMap); Map response = new HashMap<>(); response.put("success", true); response.put("changes", changes); return response; } @PostMapping("/watch") public Map watchFileSystem(@RequestBody Map request) { String directory = request.get("directory"); incrementalDetectionService.watchFileSystem(new File(directory), (filePath, changeType) -> { System.out.println("File changed: " + filePath + ", Type: " + changeType); }); Map response = new HashMap<>(); response.put("success", true); response.put("message", "Watching file system"); return response; } @PostMapping("/stop-watching") public Map stopWatching() { incrementalDetectionService.stopWatching(); Map response = new HashMap<>(); response.put("success", true); response.put("message", "Stopped watching file system"); return response; } } ``` ### 16. IncrementalRetrieveController 控制器 ```java package com.datai.salesforce.metadata.controller; import com.datai.salesforce.metadata.entity.FileChange; import com.datai.salesforce.metadata.service.incremental.IIncrementalRetrieveService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; /** * 增量拉取控制器 */ @RestController @RequestMapping("/api/metadata/incremental-retrieve") public class IncrementalRetrieveController { @Autowired private IIncrementalRetrieveService incrementalRetrieveService; @PostMapping("/retrieve") public Map incrementalRetrieve(@RequestBody Map request) { Long taskId = Long.valueOf(request.get("taskId").toString()); Long orgConfigId = Long.valueOf(request.get("orgConfigId").toString()); List changes = (List) request.get("changes"); String result = incrementalRetrieveService.incrementalRetrieve(taskId, orgConfigId, changes); Map response = new HashMap<>(); response.put("success", true); response.put("result", result); return response; } } ``` ### 17. IncrementalDeployController 控制器 ```java package com.datai.salesforce.metadata.controller; import com.datai.salesforce.metadata.entity.FileChange; import com.datai.salesforce.metadata.service.incremental.IIncrementalDeployService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; /** * 增量部署控制器 */ @RestController @RequestMapping("/api/metadata/incremental-deploy") public class IncrementalDeployController { @Autowired private IIncrementalDeployService incrementalDeployService; @PostMapping("/deploy") public Map incrementalDeploy(@RequestBody Map request) { Long taskId = Long.valueOf(request.get("taskId").toString()); Long orgConfigId = Long.valueOf(request.get("orgConfigId").toString()); List changes = (List) request.get("changes"); String result = incrementalDeployService.incrementalDeploy(taskId, orgConfigId, changes); Map response = new HashMap<>(); response.put("success", true); response.put("result", result); return response; } } ``` ### 18. FileChangeMapper 接口 ```java package com.datai.salesforce.metadata.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.datai.salesforce.metadata.entity.FileChange; import org.apache.ibatis.annotations.Mapper; /** * 文件变化 Mapper 接口 */ @Mapper public interface FileChangeMapper extends BaseMapper { } ``` ### 19. DTO 类 ```java package com.datai.salesforce.metadata.dto; import lombok.Data; /** * 文件哈希计算请求 */ @Data public class FileHashCalculateRequest { private String filePath; } /** * 文件哈希计算响应 */ @Data public class FileHashCalculateResponse { private boolean success; private String hash; } /** * 文件哈希批量计算请求 */ @Data public class FileHashBatchCalculateRequest { private String[] filePaths; } /** * 文件哈希批量计算响应 */ @Data public class FileHashBatchCalculateResponse { private boolean success; private java.util.Map hashMap; } /** * 哈希对比请求 */ @Data public class HashComparisonRequest { private java.util.Map hashMap1; private java.util.Map hashMap2; } /** * 哈希对比响应 */ @Data public class HashComparisonResponse { private boolean success; private java.util.Map result; } /** * 变化文件查找请求 */ @Data public class ChangedFilesFindRequest { private java.util.Map hashMap1; private java.util.Map hashMap2; } /** * 变化文件查找响应 */ @Data public class ChangedFilesFindResponse { private boolean success; private java.util.Set changedFiles; } /** * 增量检测请求 */ @Data public class IncrementalDetectionRequest { private java.util.Map oldHashMap; private java.util.Map newHashMap; } /** * 增量检测响应 */ @Data public class IncrementalDetectionResponse { private boolean success; private java.util.List changes; } /** * 增量拉取请求 */ @Data public class IncrementalRetrieveRequest { private Long taskId; private Long orgConfigId; private java.util.List changes; } /** * 增量拉取响应 */ @Data public class IncrementalRetrieveResponse { private boolean success; private String result; } /** * 增量部署请求 */ @Data public class IncrementalDeployRequest { private Long taskId; private Long orgConfigId; private java.util.List changes; } /** * 增量部署响应 */ @Data public class IncrementalDeployResponse { private boolean success; private String result; } ``` ### 20. FileHashException 异常类 ```java package com.datai.salesforce.metadata.exception; /** * 文件哈希异常 */ public class FileHashException extends RuntimeException { public FileHashException(String message) { super(message); } public FileHashException(String message, Throwable cause) { super(message, cause); } } ``` ### 21. HashComparisonException 异常类 ```java package com.datai.salesforce.metadata.exception; /** * 哈希对比异常 */ public class HashComparisonException extends RuntimeException { public HashComparisonException(String message) { super(message); } public HashComparisonException(String message, Throwable cause) { super(message, cause); } } ``` ### 22. IncrementalDetectionException 异常类 ```java package com.datai.salesforce.metadata.exception; /** * 增量检测异常 */ public class IncrementalDetectionException extends RuntimeException { public IncrementalDetectionException(String message) { super(message); } public IncrementalDetectionException(String message, Throwable cause) { super(message, cause); } } ``` ### 23. IncrementalRetrieveException 异常类 ```java package com.datai.salesforce.metadata.exception; /** * 增量拉取异常 */ public class IncrementalRetrieveException extends RuntimeException { public IncrementalRetrieveException(String message) { super(message); } public IncrementalRetrieveException(String message, Throwable cause) { super(message, cause); } } ``` ### 24. IncrementalDeployException 异常类 ```java package com.datai.salesforce.metadata.exception; /** * 增量部署异常 */ public class IncrementalDeployException extends RuntimeException { public IncrementalDeployException(String message) { super(message); } public IncrementalDeployException(String message, Throwable cause) { super(message, cause); } } ``` --- ## 单元测试 ### FileHashServiceImplTest ```java package com.datai.salesforce.metadata.service.filehash.impl; import com.datai.salesforce.metadata.service.filehash.IFileHashService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.io.File; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest class FileHashServiceImplTest { @Autowired private IFileHashService fileHashService; @Test void testCalculateHash() { File file = new File("test.txt"); String hash = fileHashService.calculateHash(file); assertNotNull(hash); assertFalse(hash.isEmpty()); } @Test void testCalculateHashes() { File[] files = new File[]{ new File("test1.txt"), new File("test2.txt") }; Map hashMap = fileHashService.calculateHashes(files); assertNotNull(hashMap); assertEquals(2, hashMap.size()); } } ``` ### HashComparisonServiceImplTest ```java package com.datai.salesforce.metadata.service.hashcomparison.impl; import com.datai.salesforce.metadata.service.hashcomparison.IHashComparisonService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.HashMap; import java.util.Map; import java.util.Set; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest class HashComparisonServiceImplTest { @Autowired private IHashComparisonService hashComparisonService; @Test void testCompare() { boolean result = hashComparisonService.compare("abc123", "abc123"); assertTrue(result); result = hashComparisonService.compare("abc123", "def456"); assertFalse(result); } @Test void testFindChangedFiles() { Map hashMap1 = new HashMap<>(); hashMap1.put("file1.txt", "abc123"); hashMap1.put("file2.txt", "def456"); Map hashMap2 = new HashMap<>(); hashMap2.put("file1.txt", "abc123"); hashMap2.put("file2.txt", "xyz789"); Set changedFiles = hashComparisonService.findChangedFiles(hashMap1, hashMap2); assertNotNull(changedFiles); assertEquals(1, changedFiles.size()); assertTrue(changedFiles.contains("file2.txt")); } } ``` ### IncrementalDetectionServiceImplTest ```java package com.datai.salesforce.metadata.service.incremental.impl; import com.datai.salesforce.metadata.entity.FileChange; import com.datai.salesforce.metadata.service.incremental.IIncrementalDetectionService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest class IncrementalDetectionServiceImplTest { @Autowired private IIncrementalDetectionService incrementalDetectionService; @Test void testDetectChanges() { Map oldHashMap = new HashMap<>(); oldHashMap.put("file1.txt", "abc123"); oldHashMap.put("file2.txt", "def456"); Map newHashMap = new HashMap<>(); newHashMap.put("file1.txt", "abc123"); newHashMap.put("file2.txt", "xyz789"); newHashMap.put("file3.txt", "ghi012"); List changes = incrementalDetectionService.detectChanges(oldHashMap, newHashMap); assertNotNull(changes); assertEquals(2, changes.size()); } } ``` --- ## 配置文件 ### application.yml ```yaml spring: task: execution: pool: core-size: 5 max-size: 10 queue-capacity: 100 thread-name-prefix: hash-executor- file-hash: buffer-size: 8192 hash-algorithm: SHA-256 ``` --- ## 数据库表 ### datai_file_change ```sql CREATE TABLE `datai_file_change` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `file_path` varchar(500) NOT NULL COMMENT '文件路径', `change_type` varchar(20) NOT NULL COMMENT '变化类型', `old_hash` varchar(64) DEFAULT NULL COMMENT '旧哈希值', `new_hash` varchar(64) DEFAULT NULL COMMENT '新哈希值', `detected_time` datetime NOT NULL COMMENT '检测时间', `task_id` bigint DEFAULT NULL COMMENT '任务ID', `org_config_id` bigint DEFAULT NULL COMMENT '组织配置ID', `dept_id` bigint DEFAULT NULL COMMENT '部门ID', `create_by` varchar(64) DEFAULT NULL COMMENT '创建人', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_by` varchar(64) DEFAULT NULL COMMENT '更新人', `update_time` datetime DEFAULT NULL COMMENT '更新时间', `remark` varchar(500) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`), KEY `idx_task_id` (`task_id`), KEY `idx_org_config_id` (`org_config_id`), KEY `idx_file_path` (`file_path`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件变化表'; ``` --- ## 总结 本 Prompt 提供了文件哈希对比和增量检测功能的完整实现,包括: 1. **文件哈希计算** - 使用 SHA-256 算法计算文件哈希值 2. **哈希对比** - 对比文件的哈希值,检测文件变化 3. **增量检测** - 检测文件的新增、修改、删除 4. **增量拉取** - 只拉取变化的文件 5. **增量部署** - 只部署变化的文件 所有代码都遵循 Spring Boot 3 + MyBatis Plus 技术栈,复用现有的服务接口,保持代码一致性。