43 KiB
43 KiB
Prompt First - 文件哈希对比和增量检测
输入引用
引用相关的 docs 文档链接:
- REQ-010-12.md - 文件哈希对比和增量检测需求文档
- 0021-file-hash-comparison.md - 文件哈希对比和增量检测架构决策
- REQ-010-7.md - 文件存储和解压处理需求文档
- REQ-010-11.md - 元数据组件索引和查询需求文档
- file/index.md - 文件模块 API 文档索引(唯一真源)
Context Maps
强制列出本次 Prompt 依赖的 Canvas 文件:
- Authentication.canvas - 项目架构视觉化展示
- 相关节点: 集成核心 - 提供与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 服务接口
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<String, String> calculateHashes(File[] files);
/**
* 计算文件哈希值(异步)
* @param file 文件
* @return 哈希值
*/
java.util.concurrent.Future<String> calculateHashAsync(File file);
}
2. FileHashServiceImpl 服务实现
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<String, String> calculateHashes(File[] files) {
Map<String, String> hashMap = new HashMap<>();
for (File file : files) {
String hash = calculateHash(file);
hashMap.put(file.getAbsolutePath(), hash);
}
return hashMap;
}
@Override
public java.util.concurrent.Future<String> calculateHashAsync(File file) {
return CompletableFuture.supplyAsync(() -> calculateHash(file));
}
}
3. IHashComparisonService 服务接口
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<String, Boolean> compare(Map<String, String> hashMap1, Map<String, String> hashMap2);
/**
* 查找变化的文件
* @param hashMap1 哈希值映射1(旧)
* @param hashMap2 哈希值映射2(新)
* @return 变化的文件路径列表
*/
java.util.Set<String> findChangedFiles(Map<String, String> hashMap1, Map<String, String> hashMap2);
}
4. HashComparisonServiceImpl 服务实现
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<String, Boolean> compare(Map<String, String> hashMap1, Map<String, String> hashMap2) {
Map<String, Boolean> result = new ConcurrentHashMap<>();
Set<String> 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<String> findChangedFiles(Map<String, String> hashMap1, Map<String, String> hashMap2) {
Set<String> changedFiles = new HashSet<>();
Set<String> 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 枚举
package com.datai.salesforce.metadata.enums;
/**
* 文件变化类型枚举
*/
public enum ChangeType {
/**
* 新增
*/
ADDED,
/**
* 修改
*/
MODIFIED,
/**
* 删除
*/
DELETED
}
6. FileChange 实体类
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 服务接口
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<FileChange> detectChanges(Map<String, String> oldHashMap, Map<String, String> newHashMap);
/**
* 检测文件变化(异步)
* @param oldHashMap 旧哈希值映射
* @param newHashMap 新哈希值映射
* @return 文件变化列表
*/
java.util.concurrent.Future<List<FileChange>> detectChangesAsync(Map<String, String> oldHashMap, Map<String, String> newHashMap);
/**
* 监听文件系统变化
* @param directory 目录
* @param callback 回调函数
*/
void watchFileSystem(File directory, FileChangeCallback callback);
/**
* 停止监听文件系统变化
*/
void stopWatching();
}
/**
* 文件变化回调接口
*/
interface FileChangeCallback {
void onFileChange(String filePath, ChangeType changeType);
}
8. IncrementalDetectionServiceImpl 服务实现
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<WatchKey, Path> watchKeys;
private Thread watchThread;
private volatile boolean watching;
@Override
public List<FileChange> detectChanges(Map<String, String> oldHashMap, Map<String, String> newHashMap) {
List<FileChange> changes = new ArrayList<>();
Set<String> 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<List<FileChange>> detectChangesAsync(Map<String, String> oldHashMap, Map<String, String> 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 服务接口
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<FileChange> changes);
/**
* 增量拉取(异步)
* @param taskId 任务ID
* @param orgConfigId 组织配置ID
* @param changes 文件变化列表
* @return 拉取结果
*/
java.util.concurrent.Future<String> incrementalRetrieveAsync(Long taskId, Long orgConfigId, List<FileChange> changes);
}
10. IncrementalRetrieveServiceImpl 服务实现
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<FileChange> changes) {
String packageXml = buildPackageXml(changes);
return metadataRetrieveService.retrieve(taskId, orgConfigId, packageXml);
}
@Override
@Async("retrieveExecutor")
public Future<String> incrementalRetrieveAsync(Long taskId, Long orgConfigId, List<FileChange> changes) {
return CompletableFuture.completedFuture(incrementalRetrieve(taskId, orgConfigId, changes));
}
private String buildPackageXml(List<FileChange> changes) {
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
sb.append("<Package xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n");
for (FileChange change : changes) {
String componentType = extractComponentType(change.getFilePath());
String componentName = extractComponentName(change.getFilePath());
sb.append(" <types>\n");
sb.append(" <members>").append(componentName).append("</members>\n");
sb.append(" <name>").append(componentType).append("</name>\n");
sb.append(" </types>\n");
}
sb.append(" <version>56.0</version>\n");
sb.append("</Package>");
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 服务接口
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<FileChange> changes);
/**
* 增量部署(异步)
* @param taskId 任务ID
* @param orgConfigId 组织配置ID
* @param changes 文件变化列表
* @return 部署结果
*/
java.util.concurrent.Future<String> incrementalDeployAsync(Long taskId, Long orgConfigId, List<FileChange> changes);
}
12. IncrementalDeployServiceImpl 服务实现
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<FileChange> changes) {
String packageXml = buildPackageXml(changes);
return metadataDeployService.deploy(taskId, orgConfigId, packageXml);
}
@Override
@Async("deployExecutor")
public Future<String> incrementalDeployAsync(Long taskId, Long orgConfigId, List<FileChange> changes) {
return CompletableFuture.completedFuture(incrementalDeploy(taskId, orgConfigId, changes));
}
private String buildPackageXml(List<FileChange> changes) {
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
sb.append("<Package xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n");
for (FileChange change : changes) {
String componentType = extractComponentType(change.getFilePath());
String componentName = extractComponentName(change.getFilePath());
sb.append(" <types>\n");
sb.append(" <members>").append(componentName).append("</members>\n");
sb.append(" <name>").append(componentType).append("</name>\n");
sb.append(" </types>\n");
}
sb.append(" <version>56.0</version>\n");
sb.append("</Package>");
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 控制器
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<String, Object> calculateHash(@RequestBody Map<String, String> request) {
String filePath = request.get("filePath");
File file = new File(filePath);
String hash = fileHashService.calculateHash(file);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("hash", hash);
return response;
}
@PostMapping("/calculate-batch")
public Map<String, Object> calculateHashes(@RequestBody Map<String, String[]> 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<String, String> hashMap = fileHashService.calculateHashes(files);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("hashMap", hashMap);
return response;
}
}
14. HashComparisonController 控制器
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<String, Object> compare(@RequestBody Map<String, Map<String, String>> request) {
Map<String, String> hashMap1 = request.get("hashMap1");
Map<String, String> hashMap2 = request.get("hashMap2");
Map<String, Boolean> result = hashComparisonService.compare(hashMap1, hashMap2);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("result", result);
return response;
}
@PostMapping("/find-changed")
public Map<String, Object> findChangedFiles(@RequestBody Map<String, Map<String, String>> request) {
Map<String, String> hashMap1 = request.get("hashMap1");
Map<String, String> hashMap2 = request.get("hashMap2");
java.util.Set<String> changedFiles = hashComparisonService.findChangedFiles(hashMap1, hashMap2);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("changedFiles", changedFiles);
return response;
}
}
15. IncrementalDetectionController 控制器
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<String, Object> detectChanges(@RequestBody Map<String, Map<String, String>> request) {
Map<String, String> oldHashMap = request.get("oldHashMap");
Map<String, String> newHashMap = request.get("newHashMap");
List<FileChange> changes = incrementalDetectionService.detectChanges(oldHashMap, newHashMap);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("changes", changes);
return response;
}
@PostMapping("/watch")
public Map<String, Object> watchFileSystem(@RequestBody Map<String, String> request) {
String directory = request.get("directory");
incrementalDetectionService.watchFileSystem(new File(directory), (filePath, changeType) -> {
System.out.println("File changed: " + filePath + ", Type: " + changeType);
});
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "Watching file system");
return response;
}
@PostMapping("/stop-watching")
public Map<String, Object> stopWatching() {
incrementalDetectionService.stopWatching();
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "Stopped watching file system");
return response;
}
}
16. IncrementalRetrieveController 控制器
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<String, Object> incrementalRetrieve(@RequestBody Map<String, Object> request) {
Long taskId = Long.valueOf(request.get("taskId").toString());
Long orgConfigId = Long.valueOf(request.get("orgConfigId").toString());
List<FileChange> changes = (List<FileChange>) request.get("changes");
String result = incrementalRetrieveService.incrementalRetrieve(taskId, orgConfigId, changes);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("result", result);
return response;
}
}
17. IncrementalDeployController 控制器
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<String, Object> incrementalDeploy(@RequestBody Map<String, Object> request) {
Long taskId = Long.valueOf(request.get("taskId").toString());
Long orgConfigId = Long.valueOf(request.get("orgConfigId").toString());
List<FileChange> changes = (List<FileChange>) request.get("changes");
String result = incrementalDeployService.incrementalDeploy(taskId, orgConfigId, changes);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("result", result);
return response;
}
}
18. FileChangeMapper 接口
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<FileChange> {
}
19. DTO 类
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<String, String> hashMap;
}
/**
* 哈希对比请求
*/
@Data
public class HashComparisonRequest {
private java.util.Map<String, String> hashMap1;
private java.util.Map<String, String> hashMap2;
}
/**
* 哈希对比响应
*/
@Data
public class HashComparisonResponse {
private boolean success;
private java.util.Map<String, Boolean> result;
}
/**
* 变化文件查找请求
*/
@Data
public class ChangedFilesFindRequest {
private java.util.Map<String, String> hashMap1;
private java.util.Map<String, String> hashMap2;
}
/**
* 变化文件查找响应
*/
@Data
public class ChangedFilesFindResponse {
private boolean success;
private java.util.Set<String> changedFiles;
}
/**
* 增量检测请求
*/
@Data
public class IncrementalDetectionRequest {
private java.util.Map<String, String> oldHashMap;
private java.util.Map<String, String> newHashMap;
}
/**
* 增量检测响应
*/
@Data
public class IncrementalDetectionResponse {
private boolean success;
private java.util.List<FileChange> changes;
}
/**
* 增量拉取请求
*/
@Data
public class IncrementalRetrieveRequest {
private Long taskId;
private Long orgConfigId;
private java.util.List<FileChange> 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<FileChange> changes;
}
/**
* 增量部署响应
*/
@Data
public class IncrementalDeployResponse {
private boolean success;
private String result;
}
20. FileHashException 异常类
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 异常类
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 异常类
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 异常类
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 异常类
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
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<String, String> hashMap = fileHashService.calculateHashes(files);
assertNotNull(hashMap);
assertEquals(2, hashMap.size());
}
}
HashComparisonServiceImplTest
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<String, String> hashMap1 = new HashMap<>();
hashMap1.put("file1.txt", "abc123");
hashMap1.put("file2.txt", "def456");
Map<String, String> hashMap2 = new HashMap<>();
hashMap2.put("file1.txt", "abc123");
hashMap2.put("file2.txt", "xyz789");
Set<String> changedFiles = hashComparisonService.findChangedFiles(hashMap1, hashMap2);
assertNotNull(changedFiles);
assertEquals(1, changedFiles.size());
assertTrue(changedFiles.contains("file2.txt"));
}
}
IncrementalDetectionServiceImplTest
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<String, String> oldHashMap = new HashMap<>();
oldHashMap.put("file1.txt", "abc123");
oldHashMap.put("file2.txt", "def456");
Map<String, String> newHashMap = new HashMap<>();
newHashMap.put("file1.txt", "abc123");
newHashMap.put("file2.txt", "xyz789");
newHashMap.put("file3.txt", "ghi012");
List<FileChange> changes = incrementalDetectionService.detectChanges(oldHashMap, newHashMap);
assertNotNull(changes);
assertEquals(2, changes.size());
}
}
配置文件
application.yml
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
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 提供了文件哈希对比和增量检测功能的完整实现,包括:
- 文件哈希计算 - 使用 SHA-256 算法计算文件哈希值
- 哈希对比 - 对比文件的哈希值,检测文件变化
- 增量检测 - 检测文件的新增、修改、删除
- 增量拉取 - 只拉取变化的文件
- 增量部署 - 只部署变化的文件
所有代码都遵循 Spring Boot 3 + MyBatis Plus 技术栈,复用现有的服务接口,保持代码一致性。