feat:添加必要注释

This commit is contained in:
dark 2026-04-22 14:46:20 +08:00
parent 0162117ae4
commit 637513c1b3
29 changed files with 366 additions and 1 deletions

View File

@ -17,6 +17,10 @@ import org.springframework.scheduling.annotation.EnableScheduling;
GitRepoProperties.class,
ProdApiProperties.class
})
/**
* 应用启动入口
* 统一开启定时任务重试能力和配置属性绑定
*/
public class GitDirectSyncToolApplication {
public static void main(String[] args) {

View File

@ -7,12 +7,18 @@ import org.springframework.web.client.RestTemplate;
import java.time.Duration;
/**
* Spring 基础 Bean 配置
*/
@Configuration
public class AppConfig {
/**
* 统一构造访问生产接口的 RestTemplate
*/
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder, ProdApiProperties prodApiProperties) {
// 统一使用生产接口配置中的超时参数避免各调用点各自维护一套 HTTP 超时
// 统一使用生产接口配置中的超时参数避免各调用点维护不一致的 HTTP 超时
return builder
.setConnectTimeout(Duration.ofMillis(prodApiProperties.getConnectTimeoutMs()))
.setReadTimeout(Duration.ofMillis(prodApiProperties.getReadTimeoutMs()))

View File

@ -3,17 +3,30 @@ package com.ftptool.sync.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "git.repo")
/**
* Git 仓库访问配置
*/
public class GitRepoProperties {
/** 本地 Git 工作副本目录。 */
private String localPath;
/** 远端 Git 仓库地址。 */
private String remoteUri;
/** Git 访问用户名或账号标识。 */
private String username;
/** Git 访问密码或 Token。 */
private String password;
/** 开发主配置分支Git -> PROD 只读取此分支。 */
private String scanBranch;
/** 生产快照分支PROD -> Git 只写入此分支。 */
private String snapshotBranch;
/** Git 机器人提交用户名。 */
private String commitAuthorName;
/** Git 机器人提交邮箱。 */
private String commitAuthorEmail;
/** 生产快照回写 Git 时使用的提交信息前缀。 */
private String commitMessagePrefix;
/** pull 时是否使用 rebase。 */
private boolean pullRebase;
public String getLocalPath() {

View File

@ -3,13 +3,22 @@ package com.ftptool.sync.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "prod.api")
/**
* 生产系统 push/pull 接口配置
*/
public class ProdApiProperties {
/** 生产接口基础地址。 */
private String baseUrl;
/** 配置导入接口路径。 */
private String pushPath;
/** 生产快照导出接口路径。 */
private String pullPath;
/** 访问生产接口的 Bearer Token。 */
private String token;
/** HTTP 连接超时。 */
private int connectTimeoutMs = 10000;
/** HTTP 读取超时。 */
private int readTimeoutMs = 30000;
public String getBaseUrl() {

View File

@ -3,15 +3,26 @@ package com.ftptool.sync.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "sync")
/**
* 同步任务公共配置
*/
public class SyncProperties {
/** 当前节点标识,用于日志和运维定位。 */
private String nodeId;
/** 当前运行角色,当前主运行面为 PROD。 */
private String role;
/** 工作根目录。 */
private String workDir;
/** 同步包临时目录。 */
private String packageTempDir;
/** Git -> PROD 链路的本地 staging 目录。 */
private String devToProdStagingDir;
/** PROD -> Git 链路的本地 staging 目录。 */
private String prodToDevStagingDir;
/** 最大自动重试次数。 */
private int maxRetryCount = 5;
/** 生产 pull 接口返回内容保存的默认文件名。 */
private String pullResponseFileName;
public String getNodeId() {

View File

@ -19,30 +19,44 @@ import java.time.LocalDateTime;
@Table(name = "sync_checkpoint", uniqueConstraints = {
@UniqueConstraint(name = "uk_sync_checkpoint_direction", columnNames = "direction")
})
/**
* 同步检查点实体
* 用于记录某个同步方向最后一次成功版本
*/
public class SyncCheckpoint {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 同步方向。 */
@Enumerated(EnumType.STRING)
@Column(name = "direction", nullable = false, length = 32)
private SyncDirection direction;
/** 最后一次成功版本号。 */
@Column(name = "last_success_version", length = 128)
private String lastSuccessVersion;
/** 最后一次成功内容哈希。 */
@Column(name = "last_success_hash", length = 128)
private String lastSuccessHash;
/** 检查点更新时间。 */
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
/**
* 首次入库时补齐更新时间
*/
@PrePersist
public void prePersist() {
this.updatedAt = LocalDateTime.now();
}
/**
* 每次更新时刷新更新时间
*/
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();

View File

@ -22,45 +22,62 @@ import java.time.LocalDateTime;
@UniqueConstraint(name = "uk_sync_task_trace", columnNames = "trace_id"),
@UniqueConstraint(name = "uk_sync_task_business", columnNames = {"direction", "source_version", "content_hash"})
})
/**
* 同步任务实体
* 用于记录每次 Git -> PROD PROD -> Git 的执行状态
*/
public class SyncTask {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 链路追踪号。 */
@Column(name = "trace_id", nullable = false, length = 64)
private String traceId;
/** 同步方向。 */
@Enumerated(EnumType.STRING)
@Column(name = "direction", nullable = false, length = 32)
private SyncDirection direction;
/** 来源版本号,例如 Git commit 或生产配置版本号。 */
@Column(name = "source_version", nullable = false, length = 128)
private String sourceVersion;
/** 配置内容哈希,用于幂等控制。 */
@Column(name = "content_hash", nullable = false, length = 128)
private String contentHash;
/** 同步包文件名,仅在需要打包时有值。 */
@Column(name = "package_name", length = 255)
private String packageName;
/** 当前任务状态。 */
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 32)
private SyncStatus status;
/** 已发生的重试次数。 */
@Column(name = "retry_count", nullable = false)
private Integer retryCount;
/** 最近一次错误摘要。 */
@Lob
@Column(name = "error_msg")
private String errorMsg;
/** 创建时间。 */
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
/** 更新时间。 */
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
/**
* 首次入库时填充默认状态和时间戳
*/
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();

View File

@ -7,6 +7,9 @@ import org.springframework.stereotype.Component;
@Component
@Profile("prod-agent")
/**
* Git -> PROD 定时任务入口
*/
public class GitToProdSyncJob {
private final ProdSyncCoordinator prodSyncCoordinator;
@ -15,6 +18,9 @@ public class GitToProdSyncJob {
this.prodSyncCoordinator = prodSyncCoordinator;
}
/**
* 定时触发 Git -> PROD 主链路
*/
@Scheduled(cron = "${sync.jobs.prod-git-to-prod.cron}")
public void execute() {
prodSyncCoordinator.syncLatestGitToProd();

View File

@ -7,6 +7,9 @@ import org.springframework.stereotype.Component;
@Component
@Profile("prod-agent")
/**
* PROD -> Git 定时任务入口
*/
public class ProdToGitSnapshotJob {
private final ProdSyncCoordinator prodSyncCoordinator;
@ -15,6 +18,9 @@ public class ProdToGitSnapshotJob {
this.prodSyncCoordinator = prodSyncCoordinator;
}
/**
* 定时触发 PROD -> Git 主链路
*/
@Scheduled(cron = "${sync.jobs.prod-to-git.cron}")
public void execute() {
prodSyncCoordinator.syncProdSnapshotToGit();

View File

@ -2,10 +2,16 @@ package com.ftptool.sync.model;
import java.nio.file.Path;
/**
* 打包结果
*/
public class PackageBuildResult {
/** 构建完成后的 zip 文件路径。 */
private final Path zipFile;
/** 包文件名。 */
private final String packageName;
/** 包内配置内容哈希。 */
private final String contentHash;
public PackageBuildResult(Path zipFile, String packageName, String contentHash) {

View File

@ -1,13 +1,24 @@
package com.ftptool.sync.model;
/**
* 同步包元数据
* 用于描述一个 zip 包的来源方向和内容摘要
*/
public class PackageManifest {
/** 本次同步的唯一追踪号。 */
private String traceId;
/** 同步方向。 */
private SyncDirection direction;
/** 来源环境标识,例如 DEV、PROD。 */
private String sourceEnv;
/** 来源版本号,通常为 Git commit 或接口版本。 */
private String sourceVersion;
/** 配置内容哈希,用于幂等和校验。 */
private String contentHash;
/** 包构建时间ISO-8601 格式。 */
private String createdAt;
/** zip 包文件名。 */
private String packageName;
public String getTraceId() {

View File

@ -2,9 +2,14 @@ package com.ftptool.sync.model;
import java.nio.file.Path;
/**
* 解包结果
*/
public class PackageReadResult {
/** 包内 manifest 元数据。 */
private final PackageManifest manifest;
/** 解包后的 config 目录。 */
private final Path configDirectory;
public PackageReadResult(PackageManifest manifest, Path configDirectory) {

View File

@ -2,10 +2,16 @@ package com.ftptool.sync.model;
import java.nio.file.Path;
/**
* 生产 pull 接口返回结果在本地落盘后的封装对象
*/
public class ProdPullResult {
/** 保存生产快照内容的目录。 */
private final Path contentDirectory;
/** 来源版本号,优先取服务端显式返回值。 */
private final String sourceVersion;
/** 响应体内容哈希。 */
private final String contentHash;
public ProdPullResult(Path contentDirectory, String sourceVersion, String contentHash) {

View File

@ -1,6 +1,11 @@
package com.ftptool.sync.model;
/**
* 同步方向定义
*/
public enum SyncDirection {
/** 开发 Git 配置下发到生产。 */
DEV_TO_PROD,
/** 生产配置快照回写到 Git。 */
PROD_TO_DEV
}

View File

@ -1,7 +1,13 @@
package com.ftptool.sync.model;
/**
* 运行角色定义
*/
public enum SyncRole {
/** 开发侧角色,当前架构中已退役。 */
DEV,
/** 生产侧角色,当前主运行角色。 */
PROD,
/** 未指定角色。 */
UNSET
}

View File

@ -1,10 +1,19 @@
package com.ftptool.sync.model;
/**
* 同步任务状态定义
*/
public enum SyncStatus {
/** 任务已创建,尚未开始执行。 */
CREATED,
/** 预留状态,表示已进入中间暂存阶段。 */
STAGED,
/** 预留状态,表示已完成上传。 */
UPLOADED,
/** 任务正在处理。 */
CONSUMING,
/** 任务处理成功。 */
SUCCESS,
/** 任务处理失败,且已达到失败判定条件。 */
FAILED
}

View File

@ -26,6 +26,12 @@ import java.util.Optional;
@Service
@Profile("prod-agent")
/**
* 生产侧同步协调器
* 串联两条核心链路
* 1. Git -> PROD
* 2. PROD -> Git
*/
public class ProdSyncCoordinator {
private static final Logger log = LoggerFactory.getLogger(ProdSyncCoordinator.class);
@ -65,6 +71,9 @@ public class ProdSyncCoordinator {
this.syncMetadataService = syncMetadataService;
}
/**
* 主链路一 Git 拉取最新配置并推送到生产
*/
public void syncLatestGitToProd() {
String traceId = null;
try {
@ -180,6 +189,9 @@ public class ProdSyncCoordinator {
return existing.isPresent() && existing.get().getStatus() == SyncStatus.SUCCESS;
}
/**
* 统一失败处理逻辑
*/
private void handleFailure(String traceId, String logMessage, Exception e) {
log.error(logMessage, e);
if (traceId == null) {
@ -194,6 +206,9 @@ public class ProdSyncCoordinator {
}
}
/**
* 统一截断异常摘要避免错误信息过长污染数据库字段
*/
private String summarizeException(Exception e) {
String message = e.getMessage();
if (message == null || message.trim().isEmpty()) {

View File

@ -6,7 +6,13 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
/**
* 同步检查点仓储
*/
public interface SyncCheckpointRepository extends JpaRepository<SyncCheckpoint, Long> {
/**
* 按同步方向查询检查点
*/
Optional<SyncCheckpoint> findByDirection(SyncDirection direction);
}

View File

@ -8,24 +8,42 @@ import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Optional;
/**
* 同步任务仓储
*/
public interface SyncTaskRepository extends JpaRepository<SyncTask, Long> {
/**
* traceId 查询单个任务
*/
Optional<SyncTask> findByTraceId(String traceId);
/**
* 按业务幂等键查询任务
*/
Optional<SyncTask> findByDirectionAndSourceVersionAndContentHash(
SyncDirection direction,
String sourceVersion,
String contentHash
);
/**
* 判断某个业务幂等键是否已存在
*/
boolean existsByDirectionAndSourceVersionAndContentHash(
SyncDirection direction,
String sourceVersion,
String contentHash
);
/**
* 查询最近更新的任务列表
*/
List<SyncTask> findAllByOrderByUpdatedAtDesc(Pageable pageable);
/**
* 查询最近失败任务列表
*/
List<SyncTask> findByStatusOrderByUpdatedAtDesc(
com.ftptool.sync.model.SyncStatus status,
Pageable pageable

View File

@ -9,6 +9,9 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
/**
* 同步检查点服务
*/
@Service
public class CheckpointService {
@ -18,11 +21,17 @@ public class CheckpointService {
this.syncCheckpointRepository = syncCheckpointRepository;
}
/**
* 读取某个方向当前检查点
*/
@Transactional(readOnly = true)
public Optional<SyncCheckpoint> getCheckpoint(SyncDirection direction) {
return syncCheckpointRepository.findByDirection(direction);
}
/**
* 保存某个方向最后一次成功的版本和哈希
*/
@Transactional
public SyncCheckpoint saveCheckpoint(SyncDirection direction, String version, String hash) {
SyncCheckpoint checkpoint = syncCheckpointRepository.findByDirection(direction)
@ -33,6 +42,9 @@ public class CheckpointService {
return syncCheckpointRepository.save(checkpoint);
}
/**
* 查询全部检查点供管理接口展示
*/
@Transactional(readOnly = true)
public List<SyncCheckpoint> findAllCheckpoints() {
return syncCheckpointRepository.findAll();

View File

@ -24,6 +24,10 @@ import java.nio.file.StandardCopyOption;
import java.util.stream.Stream;
@Service
/**
* Git 客户端服务
* 负责 clone / pull / checkout / commit / push 等仓库操作
*/
public class GitClientService {
private static final Logger log = LoggerFactory.getLogger(GitClientService.class);
@ -35,6 +39,9 @@ public class GitClientService {
this.gitRepoProperties = gitRepoProperties;
}
/**
* 准备仓库并返回指定分支当前 HEAD
*/
public String prepareRepositoryAndGetHead(String branch) throws IOException, GitAPIException {
synchronized (lock) {
// 同一套本地仓库会被多个定时任务复用这里串行化避免分支切换互相踩工作区
@ -50,6 +57,9 @@ public class GitClientService {
return new File(gitRepoProperties.getLocalPath()).toPath().toAbsolutePath().normalize();
}
/**
* 导出指定分支的工作树快照供后续打包或哈希计算使用
*/
public Path exportBranchSnapshot(String branch, Path targetDirectory) throws IOException, GitAPIException {
synchronized (lock) {
try (Git git = openOrCloneRepository()) {
@ -114,6 +124,9 @@ public class GitClientService {
.call();
}
/**
* 切换到目标分支不存在时按远端或本地新建分支
*/
private void checkoutBranch(Git git, String branch) throws GitAPIException, IOException {
Repository repository = git.getRepository();
Ref localRef = repository.findRef(branch);
@ -166,6 +179,9 @@ public class GitClientService {
);
}
/**
* 复制仓库工作树内容显式排除 .git 目录
*/
private void copyWorkingTreeWithoutGit(Path repositoryPath, Path targetDirectory) throws IOException {
try (Stream<Path> stream = Files.list(repositoryPath)) {
stream.filter(path -> !".git".equals(path.getFileName().toString()))

View File

@ -22,6 +22,10 @@ import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
@Service
/**
* 同步包服务
* 负责目录打包解包以及 manifest / hash 校验
*/
public class PackageService {
private static final String CONFIG_DIR = "config";
@ -36,6 +40,9 @@ public class PackageService {
this.workDirectoryService = workDirectoryService;
}
/**
* 把目录构造成标准同步 zip
*/
public PackageBuildResult buildPackageFromDirectory(Path sourceDirectory, PackageManifest manifest) throws IOException {
String contentHash = calculateDirectoryHash(sourceDirectory);
manifest.setContentHash(contentHash);
@ -59,6 +66,9 @@ public class PackageService {
return FileHashUtils.sha256Directory(sourceDirectory);
}
/**
* 解包并返回 manifest config 目录
*/
public PackageReadResult extractPackage(Path zipFile) throws IOException {
Path extractDir = Files.createTempDirectory(workDirectoryService.getPackageTempDir(), "pkg-");
Path configDir = extractDir.resolve(CONFIG_DIR);
@ -126,12 +136,18 @@ public class PackageService {
}
}
/**
* 写入 JSON 类型的 zip 条目
*/
private void addJsonEntry(ZipOutputStream zipOutputStream, String fileName, Object object) throws IOException {
zipOutputStream.putNextEntry(new ZipEntry(fileName));
zipOutputStream.write(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(object));
zipOutputStream.closeEntry();
}
/**
* 写入普通文本类型的 zip 条目
*/
private void addTextEntry(ZipOutputStream zipOutputStream, String fileName, String value) throws IOException {
zipOutputStream.putNextEntry(new ZipEntry(fileName));
zipOutputStream.write(value.getBytes(StandardCharsets.UTF_8));

View File

@ -24,6 +24,10 @@ import java.nio.file.Files;
import java.nio.file.Path;
@Service
/**
* 生产接口访问服务
* 封装对生产 push/pull 接口的 HTTP 调用
*/
public class ProdConfigApiService {
private static final Logger log = LoggerFactory.getLogger(ProdConfigApiService.class);
@ -45,6 +49,9 @@ public class ProdConfigApiService {
this.workDirectoryService = workDirectoryService;
}
/**
* 调用生产 push 接口导入一个标准同步包
*/
public void pushPackage(PackageManifest manifest, Path zipFile) {
String url = buildUrl(prodApiProperties.getPushPath());
HttpHeaders headers = defaultHeaders();
@ -107,6 +114,9 @@ public class ProdConfigApiService {
return headers;
}
/**
* 按基础地址和接口路径拼接完整 URL
*/
private String buildUrl(String path) {
String base = prodApiProperties.getBaseUrl();
if (base.endsWith("/") && path.startsWith("/")) {
@ -118,6 +128,9 @@ public class ProdConfigApiService {
return base + path;
}
/**
* 从多个候选值中选取第一个非空版本号
*/
private String firstNonBlank(String... candidates) {
for (String candidate : candidates) {
if (candidate != null && !candidate.trim().isEmpty()) {

View File

@ -7,13 +7,23 @@ import org.springframework.stereotype.Service;
import java.time.OffsetDateTime;
import java.util.UUID;
/**
* 同步元数据服务
* 负责生成 traceId 和标准同步包文件名等元数据
*/
@Service
public class SyncMetadataService {
/**
* 生成新的 traceId
*/
public String newTraceId() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
* 根据当前任务上下文组装 manifest
*/
public PackageManifest createManifest(
String traceId,
SyncDirection direction,
@ -32,10 +42,16 @@ public class SyncMetadataService {
return manifest;
}
/**
* 统一生成同步包文件名
*/
public String buildPackageFileName(SyncDirection direction, String sourceVersion, String traceId) {
return direction.name().toLowerCase() + "-" + sanitize(sourceVersion) + "-" + sanitize(traceId) + ".zip";
}
/**
* 清理文件名中的非法字符避免生成不安全的包名
*/
private String sanitize(String value) {
if (value == null || value.trim().isEmpty()) {
return "unknown";

View File

@ -12,6 +12,10 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* 同步任务服务
* 封装任务创建状态变更和失败重试次数更新逻辑
*/
@Service
public class SyncTaskService {
@ -21,11 +25,18 @@ public class SyncTaskService {
this.syncTaskRepository = syncTaskRepository;
}
/**
* 按默认策略创建或加载任务
*/
@Transactional
public SyncTask createOrLoadTask(SyncDirection direction, String sourceVersion, String contentHash, String packageName) {
return createOrLoadTask(direction, sourceVersion, contentHash, packageName, null);
}
/**
* 基于业务幂等键创建或加载任务
* 如果任务已存在直接返回已有记录不重复插入
*/
@Transactional
public SyncTask createOrLoadTask(
SyncDirection direction,
@ -53,16 +64,25 @@ public class SyncTaskService {
return syncTaskRepository.save(task);
}
/**
* traceId 查询任务
*/
@Transactional(readOnly = true)
public Optional<SyncTask> findByTraceId(String traceId) {
return syncTaskRepository.findByTraceId(traceId);
}
/**
* 按业务幂等键查询任务
*/
@Transactional(readOnly = true)
public Optional<SyncTask> findByBusinessKey(SyncDirection direction, String sourceVersion, String contentHash) {
return syncTaskRepository.findByDirectionAndSourceVersionAndContentHash(direction, sourceVersion, contentHash);
}
/**
* 更新任务状态和最近错误信息
*/
@Transactional
public void markStatus(String traceId, SyncStatus status, String errorMsg) {
syncTaskRepository.findByTraceId(traceId).ifPresent(task -> {
@ -72,6 +92,9 @@ public class SyncTaskService {
});
}
/**
* 增加失败重试次数并记录最近错误摘要
*/
@Transactional
public void increaseRetryCount(String traceId, String errorMsg) {
syncTaskRepository.findByTraceId(traceId).ifPresent(task -> {
@ -82,16 +105,25 @@ public class SyncTaskService {
});
}
/**
* 判断某个业务幂等键是否已处理过
*/
@Transactional(readOnly = true)
public boolean existsProcessed(SyncDirection direction, String sourceVersion, String contentHash) {
return syncTaskRepository.existsByDirectionAndSourceVersionAndContentHash(direction, sourceVersion, contentHash);
}
/**
* 查询最近任务供管理接口展示
*/
@Transactional(readOnly = true)
public List<SyncTask> findRecentTasks(int limit) {
return syncTaskRepository.findAllByOrderByUpdatedAtDesc(PageRequest.of(0, limit));
}
/**
* 查询最近失败任务供管理接口展示
*/
@Transactional(readOnly = true)
public List<SyncTask> findFailedTasks(int limit) {
return syncTaskRepository.findByStatusOrderByUpdatedAtDesc(SyncStatus.FAILED, PageRequest.of(0, limit));

View File

@ -9,6 +9,10 @@ import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 工作目录服务
* 负责统一生成和初始化本地运行目录
*/
@Service
public class WorkDirectoryService {
@ -18,6 +22,9 @@ public class WorkDirectoryService {
this.syncProperties = syncProperties;
}
/**
* 应用启动时预先创建运行过程需要的目录
*/
@PostConstruct
public void initialize() throws IOException {
FileTreeUtils.ensureDirectory(getWorkDir());
@ -26,18 +33,30 @@ public class WorkDirectoryService {
FileTreeUtils.ensureDirectory(getProdToDevStagingDir());
}
/**
* 工作根目录
*/
public Path getWorkDir() {
return Paths.get(syncProperties.getWorkDir()).toAbsolutePath().normalize();
}
/**
* 同步包临时目录
*/
public Path getPackageTempDir() {
return Paths.get(syncProperties.getPackageTempDir()).toAbsolutePath().normalize();
}
/**
* Git -> PROD 链路的 staging 目录
*/
public Path getDevToProdStagingDir() {
return Paths.get(syncProperties.getDevToProdStagingDir()).toAbsolutePath().normalize();
}
/**
* PROD -> Git 链路的 staging 目录
*/
public Path getProdToDevStagingDir() {
return Paths.get(syncProperties.getProdToDevStagingDir()).toAbsolutePath().normalize();
}

View File

@ -14,11 +14,17 @@ import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 文件与目录哈希工具类
*/
public final class FileHashUtils {
private FileHashUtils() {
}
/**
* 计算单个文件的 SHA-256
*/
public static String sha256(Path file) throws IOException {
MessageDigest digest = newDigest();
try (InputStream inputStream = Files.newInputStream(file);
@ -31,12 +37,19 @@ public final class FileHashUtils {
return toHex(digest.digest());
}
/**
* 计算字节数组的 SHA-256
*/
public static String sha256(byte[] bytes) {
MessageDigest digest = newDigest();
digest.update(bytes);
return toHex(digest.digest());
}
/**
* 计算目录内容哈希
* 哈希时同时包含相对路径和文件内容保证目录结构变化也会反映到结果中
*/
public static String sha256Directory(Path directory) throws IOException {
MessageDigest digest = newDigest();
List<Path> files = listRegularFiles(directory);
@ -50,6 +63,9 @@ public final class FileHashUtils {
return toHex(digest.digest());
}
/**
* 列出目录下的全部普通文件并按相对路径稳定排序
*/
private static List<Path> listRegularFiles(Path directory) throws IOException {
try (Stream<Path> stream = Files.walk(directory)) {
return stream
@ -59,6 +75,9 @@ public final class FileHashUtils {
}
}
/**
* 创建 SHA-256 摘要器
*/
private static MessageDigest newDigest() {
try {
return MessageDigest.getInstance("SHA-256");
@ -67,6 +86,9 @@ public final class FileHashUtils {
}
}
/**
* 把摘要字节转成十六进制字符串
*/
private static String toHex(byte[] bytes) {
StringBuilder builder = new StringBuilder(bytes.length * 2);
for (byte aByte : bytes) {

View File

@ -10,17 +10,26 @@ import java.nio.file.attribute.BasicFileAttributes;
import java.util.Comparator;
import java.util.stream.Stream;
/**
* 文件树操作工具类
*/
public final class FileTreeUtils {
private FileTreeUtils() {
}
/**
* 确保目录存在不存在时递归创建
*/
public static void ensureDirectory(Path path) throws IOException {
if (path != null) {
Files.createDirectories(path);
}
}
/**
* 删除目录下除保留名称外的所有子项
*/
public static void deleteChildrenExcept(Path directory, String reservedName) throws IOException {
try (Stream<Path> stream = Files.list(directory)) {
for (Path child : stream.sorted(Comparator.reverseOrder()).toArray(Path[]::new)) {
@ -32,6 +41,9 @@ public final class FileTreeUtils {
}
}
/**
* 递归删除目录或文件
*/
public static void deleteRecursively(Path path) throws IOException {
if (path == null || Files.notExists(path)) {
return;
@ -51,6 +63,9 @@ public final class FileTreeUtils {
});
}
/**
* 递归复制目录
*/
public static void copyDirectory(Path source, Path target) throws IOException {
ensureDirectory(target);
Files.walkFileTree(source, new SimpleFileVisitor<Path>() {

View File

@ -16,6 +16,10 @@ import java.util.List;
@RestController
@RequestMapping("/api/admin/sync")
/**
* 同步管理接口
* 当前提供最近任务失败任务和检查点的只读查询能力
*/
public class SyncManagementController {
private final SyncTaskService syncTaskService;
@ -26,6 +30,9 @@ public class SyncManagementController {
this.checkpointService = checkpointService;
}
/**
* 综合返回检查点最近任务和失败任务
*/
@GetMapping("/overview")
public SyncOverviewResponse overview(
@RequestParam(name = "recentLimit", defaultValue = "10") int recentLimit,
@ -40,16 +47,25 @@ public class SyncManagementController {
);
}
/**
* 查询最近同步任务
*/
@GetMapping("/tasks/recent")
public List<SyncTaskView> recentTasks(@RequestParam(name = "limit", defaultValue = "20") int limit) {
return toTaskViews(syncTaskService.findRecentTasks(normalizeLimit(limit)));
}
/**
* 查询最近失败任务
*/
@GetMapping("/tasks/failed")
public List<SyncTaskView> failedTasks(@RequestParam(name = "limit", defaultValue = "20") int limit) {
return toTaskViews(syncTaskService.findFailedTasks(normalizeLimit(limit)));
}
/**
* 统一限制分页上限避免管理接口一次性返回过多数据
*/
private int normalizeLimit(int limit) {
if (limit < 1) {
return 1;
@ -57,6 +73,9 @@ public class SyncManagementController {
return Math.min(limit, 100);
}
/**
* 将实体对象转换为接口输出视图
*/
private List<SyncTaskView> toTaskViews(List<SyncTask> tasks) {
List<SyncTaskView> result = new ArrayList<SyncTaskView>();
for (SyncTask task : tasks) {
@ -75,6 +94,9 @@ public class SyncManagementController {
return result;
}
/**
* 将检查点实体转换为接口输出视图
*/
private List<SyncCheckpointView> toCheckpointViews(List<SyncCheckpoint> checkpoints) {
List<SyncCheckpointView> result = new ArrayList<SyncCheckpointView>();
checkpoints.sort(Comparator.comparing(checkpoint -> checkpoint.getDirection().name()));
@ -89,6 +111,9 @@ public class SyncManagementController {
return result;
}
/**
* 管理页总览响应
*/
public static class SyncOverviewResponse {
private final List<SyncCheckpointView> checkpoints;
@ -118,6 +143,9 @@ public class SyncManagementController {
}
}
/**
* 检查点输出视图
*/
public static class SyncCheckpointView {
private final String direction;
@ -154,6 +182,9 @@ public class SyncManagementController {
}
}
/**
* 同步任务输出视图
*/
public static class SyncTaskView {
private final String traceId;