datai/datai-scenes/datai-scene-salesforce/docs/prompts/022-file-hash-comparison.md
Kris 2e6f087732 docs: 完成REQ-010-17和REQ-010-2的文档创建
- 完成REQ-010-17(性能优化和限流处理)的所有6个阶段
  - 创建ADR文档:0026-performance-optimization.md
  - 创建Prompt文档:027-performance-optimization.md
  - 创建会话记录:20260119-performance-optimization.md
  - 创建变更记录:20260119-performance-optimization.md
  - 创建复盘报告:20260119-performance-optimization-retro.md
  - 更新index.md和CHANGELOG.md

- 完成REQ-010-2(基础实体类和Mapper创建)的前3个阶段
  - 更新ADR文档:0011-entity-mapper-create.md
  - 创建Prompt文档:002-entity-mapper-create.md
  - 更新index.md

所有文档均按照SSOT方法论创建,包括需求定义、架构决策、提示词资产化、执行会话、变更记录和闭环复盘。
2026-01-19 10:06:09 +08:00

1517 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<String, String> calculateHashes(File[] files);
/**
* 计算文件哈希值(异步)
* @param file 文件
* @return 哈希值
*/
java.util.concurrent.Future<String> 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<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 服务接口
```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<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 服务实现
```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<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 枚举
```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<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 服务实现
```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<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 服务接口
```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<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 服务实现
```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<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 服务接口
```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<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 服务实现
```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<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 控制器
```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<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 控制器
```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<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 控制器
```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<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 控制器
```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<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 控制器
```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<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 接口
```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<FileChange> {
}
```
### 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<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 异常类
```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<String, String> 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<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
```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<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
```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 技术栈,复用现有的服务接口,保持代码一致性。