From 637513c1b35c1ee62024e1bd4ff5f9e3047f6a1c Mon Sep 17 00:00:00 2001 From: dark Date: Wed, 22 Apr 2026 14:46:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=B7=BB=E5=8A=A0=E5=BF=85=E8=A6=81?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sync/GitDirectSyncToolApplication.java | 4 +++ .../com/ftptool/sync/config/AppConfig.java | 8 ++++- .../sync/config/GitRepoProperties.java | 13 ++++++++ .../sync/config/ProdApiProperties.java | 9 ++++++ .../ftptool/sync/config/SyncProperties.java | 11 +++++++ .../ftptool/sync/entity/SyncCheckpoint.java | 14 ++++++++ .../com/ftptool/sync/entity/SyncTask.java | 17 ++++++++++ .../ftptool/sync/job/GitToProdSyncJob.java | 6 ++++ .../sync/job/ProdToGitSnapshotJob.java | 6 ++++ .../sync/model/PackageBuildResult.java | 6 ++++ .../ftptool/sync/model/PackageManifest.java | 11 +++++++ .../ftptool/sync/model/PackageReadResult.java | 5 +++ .../ftptool/sync/model/ProdPullResult.java | 6 ++++ .../com/ftptool/sync/model/SyncDirection.java | 5 +++ .../java/com/ftptool/sync/model/SyncRole.java | 6 ++++ .../com/ftptool/sync/model/SyncStatus.java | 9 ++++++ .../orchestrator/ProdSyncCoordinator.java | 15 +++++++++ .../repository/SyncCheckpointRepository.java | 6 ++++ .../sync/repository/SyncTaskRepository.java | 18 +++++++++++ .../sync/service/CheckpointService.java | 12 +++++++ .../sync/service/GitClientService.java | 16 ++++++++++ .../ftptool/sync/service/PackageService.java | 16 ++++++++++ .../sync/service/ProdConfigApiService.java | 13 ++++++++ .../sync/service/SyncMetadataService.java | 16 ++++++++++ .../ftptool/sync/service/SyncTaskService.java | 32 +++++++++++++++++++ .../sync/service/WorkDirectoryService.java | 19 +++++++++++ .../com/ftptool/sync/util/FileHashUtils.java | 22 +++++++++++++ .../com/ftptool/sync/util/FileTreeUtils.java | 15 +++++++++ .../sync/web/SyncManagementController.java | 31 ++++++++++++++++++ 29 files changed, 366 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/ftptool/sync/GitDirectSyncToolApplication.java b/src/main/java/com/ftptool/sync/GitDirectSyncToolApplication.java index bdb5abc..37c6735 100644 --- a/src/main/java/com/ftptool/sync/GitDirectSyncToolApplication.java +++ b/src/main/java/com/ftptool/sync/GitDirectSyncToolApplication.java @@ -17,6 +17,10 @@ import org.springframework.scheduling.annotation.EnableScheduling; GitRepoProperties.class, ProdApiProperties.class }) +/** + * 应用启动入口。 + * 统一开启定时任务、重试能力和配置属性绑定。 + */ public class GitDirectSyncToolApplication { public static void main(String[] args) { diff --git a/src/main/java/com/ftptool/sync/config/AppConfig.java b/src/main/java/com/ftptool/sync/config/AppConfig.java index f45ea35..b4b6e2a 100644 --- a/src/main/java/com/ftptool/sync/config/AppConfig.java +++ b/src/main/java/com/ftptool/sync/config/AppConfig.java @@ -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())) diff --git a/src/main/java/com/ftptool/sync/config/GitRepoProperties.java b/src/main/java/com/ftptool/sync/config/GitRepoProperties.java index acb4fac..4abfd82 100644 --- a/src/main/java/com/ftptool/sync/config/GitRepoProperties.java +++ b/src/main/java/com/ftptool/sync/config/GitRepoProperties.java @@ -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() { diff --git a/src/main/java/com/ftptool/sync/config/ProdApiProperties.java b/src/main/java/com/ftptool/sync/config/ProdApiProperties.java index 817cb49..b04b142 100644 --- a/src/main/java/com/ftptool/sync/config/ProdApiProperties.java +++ b/src/main/java/com/ftptool/sync/config/ProdApiProperties.java @@ -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() { diff --git a/src/main/java/com/ftptool/sync/config/SyncProperties.java b/src/main/java/com/ftptool/sync/config/SyncProperties.java index 203825f..02ca76f 100644 --- a/src/main/java/com/ftptool/sync/config/SyncProperties.java +++ b/src/main/java/com/ftptool/sync/config/SyncProperties.java @@ -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() { diff --git a/src/main/java/com/ftptool/sync/entity/SyncCheckpoint.java b/src/main/java/com/ftptool/sync/entity/SyncCheckpoint.java index 3c92ba6..6896e31 100644 --- a/src/main/java/com/ftptool/sync/entity/SyncCheckpoint.java +++ b/src/main/java/com/ftptool/sync/entity/SyncCheckpoint.java @@ -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(); diff --git a/src/main/java/com/ftptool/sync/entity/SyncTask.java b/src/main/java/com/ftptool/sync/entity/SyncTask.java index b6ca2d4..38d4cd7 100644 --- a/src/main/java/com/ftptool/sync/entity/SyncTask.java +++ b/src/main/java/com/ftptool/sync/entity/SyncTask.java @@ -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(); diff --git a/src/main/java/com/ftptool/sync/job/GitToProdSyncJob.java b/src/main/java/com/ftptool/sync/job/GitToProdSyncJob.java index 539ec84..91654e6 100644 --- a/src/main/java/com/ftptool/sync/job/GitToProdSyncJob.java +++ b/src/main/java/com/ftptool/sync/job/GitToProdSyncJob.java @@ -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(); diff --git a/src/main/java/com/ftptool/sync/job/ProdToGitSnapshotJob.java b/src/main/java/com/ftptool/sync/job/ProdToGitSnapshotJob.java index e0e95b5..3b70ae5 100644 --- a/src/main/java/com/ftptool/sync/job/ProdToGitSnapshotJob.java +++ b/src/main/java/com/ftptool/sync/job/ProdToGitSnapshotJob.java @@ -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(); diff --git a/src/main/java/com/ftptool/sync/model/PackageBuildResult.java b/src/main/java/com/ftptool/sync/model/PackageBuildResult.java index f279ed7..f5cac67 100644 --- a/src/main/java/com/ftptool/sync/model/PackageBuildResult.java +++ b/src/main/java/com/ftptool/sync/model/PackageBuildResult.java @@ -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) { diff --git a/src/main/java/com/ftptool/sync/model/PackageManifest.java b/src/main/java/com/ftptool/sync/model/PackageManifest.java index 1563121..1d5e78b 100644 --- a/src/main/java/com/ftptool/sync/model/PackageManifest.java +++ b/src/main/java/com/ftptool/sync/model/PackageManifest.java @@ -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() { diff --git a/src/main/java/com/ftptool/sync/model/PackageReadResult.java b/src/main/java/com/ftptool/sync/model/PackageReadResult.java index bc3c1c5..a6b86fa 100644 --- a/src/main/java/com/ftptool/sync/model/PackageReadResult.java +++ b/src/main/java/com/ftptool/sync/model/PackageReadResult.java @@ -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) { diff --git a/src/main/java/com/ftptool/sync/model/ProdPullResult.java b/src/main/java/com/ftptool/sync/model/ProdPullResult.java index fa86c1c..987a78d 100644 --- a/src/main/java/com/ftptool/sync/model/ProdPullResult.java +++ b/src/main/java/com/ftptool/sync/model/ProdPullResult.java @@ -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) { diff --git a/src/main/java/com/ftptool/sync/model/SyncDirection.java b/src/main/java/com/ftptool/sync/model/SyncDirection.java index 536c91b..273462b 100644 --- a/src/main/java/com/ftptool/sync/model/SyncDirection.java +++ b/src/main/java/com/ftptool/sync/model/SyncDirection.java @@ -1,6 +1,11 @@ package com.ftptool.sync.model; +/** + * 同步方向定义。 + */ public enum SyncDirection { + /** 开发 Git 配置下发到生产。 */ DEV_TO_PROD, + /** 生产配置快照回写到 Git。 */ PROD_TO_DEV } diff --git a/src/main/java/com/ftptool/sync/model/SyncRole.java b/src/main/java/com/ftptool/sync/model/SyncRole.java index c7519cf..6c7de7e 100644 --- a/src/main/java/com/ftptool/sync/model/SyncRole.java +++ b/src/main/java/com/ftptool/sync/model/SyncRole.java @@ -1,7 +1,13 @@ package com.ftptool.sync.model; +/** + * 运行角色定义。 + */ public enum SyncRole { + /** 开发侧角色,当前架构中已退役。 */ DEV, + /** 生产侧角色,当前主运行角色。 */ PROD, + /** 未指定角色。 */ UNSET } diff --git a/src/main/java/com/ftptool/sync/model/SyncStatus.java b/src/main/java/com/ftptool/sync/model/SyncStatus.java index 6cfba08..60a918c 100644 --- a/src/main/java/com/ftptool/sync/model/SyncStatus.java +++ b/src/main/java/com/ftptool/sync/model/SyncStatus.java @@ -1,10 +1,19 @@ package com.ftptool.sync.model; +/** + * 同步任务状态定义。 + */ public enum SyncStatus { + /** 任务已创建,尚未开始执行。 */ CREATED, + /** 预留状态,表示已进入中间暂存阶段。 */ STAGED, + /** 预留状态,表示已完成上传。 */ UPLOADED, + /** 任务正在处理。 */ CONSUMING, + /** 任务处理成功。 */ SUCCESS, + /** 任务处理失败,且已达到失败判定条件。 */ FAILED } diff --git a/src/main/java/com/ftptool/sync/orchestrator/ProdSyncCoordinator.java b/src/main/java/com/ftptool/sync/orchestrator/ProdSyncCoordinator.java index 30cce7d..450806b 100644 --- a/src/main/java/com/ftptool/sync/orchestrator/ProdSyncCoordinator.java +++ b/src/main/java/com/ftptool/sync/orchestrator/ProdSyncCoordinator.java @@ -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()) { diff --git a/src/main/java/com/ftptool/sync/repository/SyncCheckpointRepository.java b/src/main/java/com/ftptool/sync/repository/SyncCheckpointRepository.java index 914c96b..b4ec1af 100644 --- a/src/main/java/com/ftptool/sync/repository/SyncCheckpointRepository.java +++ b/src/main/java/com/ftptool/sync/repository/SyncCheckpointRepository.java @@ -6,7 +6,13 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; +/** + * 同步检查点仓储。 + */ public interface SyncCheckpointRepository extends JpaRepository { + /** + * 按同步方向查询检查点。 + */ Optional findByDirection(SyncDirection direction); } diff --git a/src/main/java/com/ftptool/sync/repository/SyncTaskRepository.java b/src/main/java/com/ftptool/sync/repository/SyncTaskRepository.java index 1f9668d..593342a 100644 --- a/src/main/java/com/ftptool/sync/repository/SyncTaskRepository.java +++ b/src/main/java/com/ftptool/sync/repository/SyncTaskRepository.java @@ -8,24 +8,42 @@ import org.springframework.data.domain.Pageable; import java.util.List; import java.util.Optional; +/** + * 同步任务仓储。 + */ public interface SyncTaskRepository extends JpaRepository { + /** + * 按 traceId 查询单个任务。 + */ Optional findByTraceId(String traceId); + /** + * 按业务幂等键查询任务。 + */ Optional findByDirectionAndSourceVersionAndContentHash( SyncDirection direction, String sourceVersion, String contentHash ); + /** + * 判断某个业务幂等键是否已存在。 + */ boolean existsByDirectionAndSourceVersionAndContentHash( SyncDirection direction, String sourceVersion, String contentHash ); + /** + * 查询最近更新的任务列表。 + */ List findAllByOrderByUpdatedAtDesc(Pageable pageable); + /** + * 查询最近失败任务列表。 + */ List findByStatusOrderByUpdatedAtDesc( com.ftptool.sync.model.SyncStatus status, Pageable pageable diff --git a/src/main/java/com/ftptool/sync/service/CheckpointService.java b/src/main/java/com/ftptool/sync/service/CheckpointService.java index 60e0a19..4ae4def 100644 --- a/src/main/java/com/ftptool/sync/service/CheckpointService.java +++ b/src/main/java/com/ftptool/sync/service/CheckpointService.java @@ -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 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 findAllCheckpoints() { return syncCheckpointRepository.findAll(); diff --git a/src/main/java/com/ftptool/sync/service/GitClientService.java b/src/main/java/com/ftptool/sync/service/GitClientService.java index 43d7f28..d978a6e 100644 --- a/src/main/java/com/ftptool/sync/service/GitClientService.java +++ b/src/main/java/com/ftptool/sync/service/GitClientService.java @@ -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 stream = Files.list(repositoryPath)) { stream.filter(path -> !".git".equals(path.getFileName().toString())) diff --git a/src/main/java/com/ftptool/sync/service/PackageService.java b/src/main/java/com/ftptool/sync/service/PackageService.java index a108ddf..feefe33 100644 --- a/src/main/java/com/ftptool/sync/service/PackageService.java +++ b/src/main/java/com/ftptool/sync/service/PackageService.java @@ -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)); diff --git a/src/main/java/com/ftptool/sync/service/ProdConfigApiService.java b/src/main/java/com/ftptool/sync/service/ProdConfigApiService.java index c9efdfc..169e902 100644 --- a/src/main/java/com/ftptool/sync/service/ProdConfigApiService.java +++ b/src/main/java/com/ftptool/sync/service/ProdConfigApiService.java @@ -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()) { diff --git a/src/main/java/com/ftptool/sync/service/SyncMetadataService.java b/src/main/java/com/ftptool/sync/service/SyncMetadataService.java index 3fd3e9f..6d9ab5d 100644 --- a/src/main/java/com/ftptool/sync/service/SyncMetadataService.java +++ b/src/main/java/com/ftptool/sync/service/SyncMetadataService.java @@ -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"; diff --git a/src/main/java/com/ftptool/sync/service/SyncTaskService.java b/src/main/java/com/ftptool/sync/service/SyncTaskService.java index e15b949..7a8abce 100644 --- a/src/main/java/com/ftptool/sync/service/SyncTaskService.java +++ b/src/main/java/com/ftptool/sync/service/SyncTaskService.java @@ -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 findByTraceId(String traceId) { return syncTaskRepository.findByTraceId(traceId); } + /** + * 按业务幂等键查询任务。 + */ @Transactional(readOnly = true) public Optional 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 findRecentTasks(int limit) { return syncTaskRepository.findAllByOrderByUpdatedAtDesc(PageRequest.of(0, limit)); } + /** + * 查询最近失败任务,供管理接口展示。 + */ @Transactional(readOnly = true) public List findFailedTasks(int limit) { return syncTaskRepository.findByStatusOrderByUpdatedAtDesc(SyncStatus.FAILED, PageRequest.of(0, limit)); diff --git a/src/main/java/com/ftptool/sync/service/WorkDirectoryService.java b/src/main/java/com/ftptool/sync/service/WorkDirectoryService.java index 15d7cb0..de1f2a7 100644 --- a/src/main/java/com/ftptool/sync/service/WorkDirectoryService.java +++ b/src/main/java/com/ftptool/sync/service/WorkDirectoryService.java @@ -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(); } diff --git a/src/main/java/com/ftptool/sync/util/FileHashUtils.java b/src/main/java/com/ftptool/sync/util/FileHashUtils.java index 454ad9a..b8e2468 100644 --- a/src/main/java/com/ftptool/sync/util/FileHashUtils.java +++ b/src/main/java/com/ftptool/sync/util/FileHashUtils.java @@ -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 files = listRegularFiles(directory); @@ -50,6 +63,9 @@ public final class FileHashUtils { return toHex(digest.digest()); } + /** + * 列出目录下的全部普通文件,并按相对路径稳定排序。 + */ private static List listRegularFiles(Path directory) throws IOException { try (Stream 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) { diff --git a/src/main/java/com/ftptool/sync/util/FileTreeUtils.java b/src/main/java/com/ftptool/sync/util/FileTreeUtils.java index 8fa4e94..1bf2bde 100644 --- a/src/main/java/com/ftptool/sync/util/FileTreeUtils.java +++ b/src/main/java/com/ftptool/sync/util/FileTreeUtils.java @@ -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 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() { diff --git a/src/main/java/com/ftptool/sync/web/SyncManagementController.java b/src/main/java/com/ftptool/sync/web/SyncManagementController.java index f316beb..9f4da4a 100644 --- a/src/main/java/com/ftptool/sync/web/SyncManagementController.java +++ b/src/main/java/com/ftptool/sync/web/SyncManagementController.java @@ -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 recentTasks(@RequestParam(name = "limit", defaultValue = "20") int limit) { return toTaskViews(syncTaskService.findRecentTasks(normalizeLimit(limit))); } + /** + * 查询最近失败任务。 + */ @GetMapping("/tasks/failed") public List 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 toTaskViews(List tasks) { List result = new ArrayList(); for (SyncTask task : tasks) { @@ -75,6 +94,9 @@ public class SyncManagementController { return result; } + /** + * 将检查点实体转换为接口输出视图。 + */ private List toCheckpointViews(List checkpoints) { List result = new ArrayList(); checkpoints.sort(Comparator.comparing(checkpoint -> checkpoint.getDirection().name())); @@ -89,6 +111,9 @@ public class SyncManagementController { return result; } + /** + * 管理页总览响应。 + */ public static class SyncOverviewResponse { private final List 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;