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

43 KiB
Raw Blame History

Prompt First - 文件哈希对比和增量检测

输入引用

引用相关的 docs 文档链接:

Context Maps

强制列出本次 Prompt 依赖的 Canvas 文件:

目标

实现文件哈希对比和增量检测功能,支持检测文件变化,只拉取或部署变化的文件。包括文件哈希计算、哈希对比、增量检测、增量拉取、增量部署等功能。

输出格式

代码示例语言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 提供了文件哈希对比和增量检测功能的完整实现,包括:

  1. 文件哈希计算 - 使用 SHA-256 算法计算文件哈希值
  2. 哈希对比 - 对比文件的哈希值,检测文件变化
  3. 增量检测 - 检测文件的新增、修改、删除
  4. 增量拉取 - 只拉取变化的文件
  5. 增量部署 - 只部署变化的文件

所有代码都遵循 Spring Boot 3 + MyBatis Plus 技术栈,复用现有的服务接口,保持代码一致性。