1517 lines
43 KiB
Markdown
1517 lines
43 KiB
Markdown
# 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 技术栈,复用现有的服务接口,保持代码一致性。
|