From 0162117ae48a48564f8eaa34be4ce5d500ac96dc Mon Sep 17 00:00:00 2001 From: dark Date: Wed, 22 Apr 2026 13:51:27 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E6=95=9B=E4=B8=BA=20Git?= =?UTF-8?q?=20=E7=9B=B4=E8=BF=9E=E5=90=8C=E6=AD=A5=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E5=B9=B6=E5=AE=8C=E6=88=90=E6=94=B6=E5=B0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将整体方案从 FTP 中转双端代理重构为单 prod-agent 的 Git 直连架构 - 重写主设计文档、详细设计文档和补充方案文档,统一新架构口径 - 新增 GitDirectSyncToolApplication 并调整 Maven 坐标与运行命名 - 清理 application.properties、SyncProperties 和 schema.sql 中的 FTP/ACK 遗留配置与表结构 - 将生产侧主流程收敛为 Git -> PROD 与 PROD -> Git 两条正式链路 - 新增 SyncManagementController,提供最近同步状态与失败任务查询接口 - 补充 ProdSyncCoordinatorIntegrationTest,覆盖两条主链路的最小集成测试与幂等行为 - 为主链路代码补充必要中文注释,提升可读性 - 删除旧架构源码主体并同步修正文档中的过期引用 - 完成编译与测试验证 --- docs/ftp-sync-tool-design.md | 4 +- docs/ftp-sync-tool-detail-design.md | 34 +-- docs/git-direct-sync-tool-design.md | 40 +-- pom.xml | 6 +- ...java => GitDirectSyncToolApplication.java} | 4 +- .../ftptool/sync/config/FtpProperties.java | 5 - .../java/com/ftptool/sync/entity/SyncAck.java | 5 - .../com/ftptool/sync/job/DevAckScanJob.java | 5 - .../sync/job/DevConsumeProdPackageJob.java | 5 - .../com/ftptool/sync/job/DevGitScanJob.java | 5 - .../com/ftptool/sync/job/ProdAckScanJob.java | 5 - .../sync/job/ProdConsumeDevPackageJob.java | 5 - .../ftptool/sync/job/ProdPullConfigJob.java | 5 - .../ftptool/sync/model/RemoteFileInfo.java | 5 - .../com/ftptool/sync/model/SyncAckFile.java | 5 - .../sync/orchestrator/DevSyncCoordinator.java | 5 - .../sync/repository/SyncAckRepository.java | 5 - .../sync/repository/SyncTaskRepository.java | 9 + .../ftptool/sync/service/AckFileService.java | 5 - .../com/ftptool/sync/service/AckService.java | 5 - .../sync/service/CheckpointService.java | 6 + .../sync/service/FtpClientService.java | 5 - .../ftptool/sync/service/SyncTaskService.java | 12 + .../sync/web/SyncManagementController.java | 227 ++++++++++++++++++ .../application-dev-agent.properties | 2 - .../ProdSyncCoordinatorIntegrationTest.java | 149 ++++++++++++ 26 files changed, 453 insertions(+), 115 deletions(-) rename src/main/java/com/ftptool/sync/{FtpSyncToolApplication.java => GitDirectSyncToolApplication.java} (86%) delete mode 100644 src/main/java/com/ftptool/sync/config/FtpProperties.java delete mode 100644 src/main/java/com/ftptool/sync/entity/SyncAck.java delete mode 100644 src/main/java/com/ftptool/sync/job/DevAckScanJob.java delete mode 100644 src/main/java/com/ftptool/sync/job/DevConsumeProdPackageJob.java delete mode 100644 src/main/java/com/ftptool/sync/job/DevGitScanJob.java delete mode 100644 src/main/java/com/ftptool/sync/job/ProdAckScanJob.java delete mode 100644 src/main/java/com/ftptool/sync/job/ProdConsumeDevPackageJob.java delete mode 100644 src/main/java/com/ftptool/sync/job/ProdPullConfigJob.java delete mode 100644 src/main/java/com/ftptool/sync/model/RemoteFileInfo.java delete mode 100644 src/main/java/com/ftptool/sync/model/SyncAckFile.java delete mode 100644 src/main/java/com/ftptool/sync/orchestrator/DevSyncCoordinator.java delete mode 100644 src/main/java/com/ftptool/sync/repository/SyncAckRepository.java delete mode 100644 src/main/java/com/ftptool/sync/service/AckFileService.java delete mode 100644 src/main/java/com/ftptool/sync/service/AckService.java delete mode 100644 src/main/java/com/ftptool/sync/service/FtpClientService.java create mode 100644 src/main/java/com/ftptool/sync/web/SyncManagementController.java delete mode 100644 src/main/resources/application-dev-agent.properties create mode 100644 src/test/java/com/ftptool/sync/orchestrator/ProdSyncCoordinatorIntegrationTest.java diff --git a/docs/ftp-sync-tool-design.md b/docs/ftp-sync-tool-design.md index b9493be..36f88c8 100644 --- a/docs/ftp-sync-tool-design.md +++ b/docs/ftp-sync-tool-design.md @@ -378,9 +378,9 @@ sync-tool 已退出主运行面: -- `FtpClientService` - FTP 包上传下载逻辑 - FTP ACK 逻辑 +- 双端代理运行路径 ## 16. 定时任务建议 @@ -481,6 +481,6 @@ sync-tool 1. 先确认生产环境对开发 Git 是否具备推送权限 2. 确认生产 `push/pull` 接口最终协议 -3. 在文件系统允许时物理删除退役占位类 +3. 删除退役标记文件 `application-dev-agent.properties` 4. 将工程命名中残留的 `ftp` 语义继续清理 5. 补充新的 `application-prod-agent.properties` 配置说明 diff --git a/docs/ftp-sync-tool-detail-design.md b/docs/ftp-sync-tool-detail-design.md index 4ef88df..127fef6 100644 --- a/docs/ftp-sync-tool-detail-design.md +++ b/docs/ftp-sync-tool-detail-design.md @@ -114,9 +114,8 @@ Git 配置: 当前状态: -- `FtpClientService` 已降为占位类 - FTP 调度主路径已退出运行面 -- 旧 FTP 类仍保留在源码树中,但不再作为正式能力 +- FTP 相关主类已从当前源码树中移除 ## 6. H2 状态设计 @@ -174,25 +173,19 @@ Git 配置: ### 7.4 当前遗留占位类 -- [ProdConsumeDevPackageJob.java](e:/AIcoding/FtpTool/src/main/java/com/ftptool/sync/job/ProdConsumeDevPackageJob.java) -- [ProdPullConfigJob.java](e:/AIcoding/FtpTool/src/main/java/com/ftptool/sync/job/ProdPullConfigJob.java) -- [DevGitScanJob.java](e:/AIcoding/FtpTool/src/main/java/com/ftptool/sync/job/DevGitScanJob.java) -- [DevConsumeProdPackageJob.java](e:/AIcoding/FtpTool/src/main/java/com/ftptool/sync/job/DevConsumeProdPackageJob.java) -- [DevAckScanJob.java](e:/AIcoding/FtpTool/src/main/java/com/ftptool/sync/job/DevAckScanJob.java) +这批旧调度类已经从当前源码树中删除: 说明: -- 这些类目前保留为兼容占位 - 已不再作为正式运行入口 -- 由于当前环境删除权限受限,暂时保留为空占位实现 +- 当前代码树只保留 Git 直连架构需要的正式 job ### 7.5 当前遗留代码 以下内容仍然存在于代码库,但属于旧架构遗留: -- `dev-agent` 相关 job/coordinator -- `FtpClientService` -- ACK 文件相关模型和服务 +- `application-dev-agent.properties` 退役标记文件 +- 少量文件名或文档中的 `ftp` 语义残留 这些不是当前推荐运行路径。 @@ -234,6 +227,18 @@ Git 配置: - [prod-api-v1.md](e:/AIcoding/FtpTool/docs/prod-api-v1.md) +## 9.1 管理接口 + +当前已新增一组只读管理接口,用于查看最近同步状态和失败任务: + +- `GET /api/admin/sync/overview` +- `GET /api/admin/sync/tasks/recent` +- `GET /api/admin/sync/tasks/failed` + +对应实现: + +- [SyncManagementController.java](e:/AIcoding/FtpTool/src/main/java/com/ftptool/sync/web/SyncManagementController.java) + ## 10. 当前主要风险 ### 10.1 Git 写权限 @@ -247,7 +252,8 @@ Git 配置: 当前状态: - 文件名和类名仍可能误导维护者 -- 极少量退役文件仍保留在源码树中 +- 旧架构源码主体已经删除 +- 主要剩余问题转为命名和文档口径统一 ### 10.3 双向同步闭环 @@ -258,7 +264,7 @@ Git 配置: 建议按下面顺序继续: 1. 删除或隔离 `dev-agent` 运行路径 -2. 在文件系统允许时物理删除退役占位类 +2. 删除退役标记文件 `application-dev-agent.properties` 3. 统一清理残留的 `ftp` 命名 4. 补充管理接口和健康检查接口 5. 增加集成测试 diff --git a/docs/git-direct-sync-tool-design.md b/docs/git-direct-sync-tool-design.md index 7e615b4..ab378b8 100644 --- a/docs/git-direct-sync-tool-design.md +++ b/docs/git-direct-sync-tool-design.md @@ -1,4 +1,4 @@ -# 基于 Git 直连的配置双向同步工具设计方案 +# Git 直连架构补充方案 ## 1. 背景变化 @@ -94,7 +94,7 @@ 这里有一个必须先确认的关键点: -- **生产环境对开发 Git 是否有写权限** +- **生产环境对开发 Git 是否有 push 权限** 如果只有读权限,没有 push 权限,那么第二条链路“生产 -> 开发 Git”无法闭环。 @@ -109,8 +109,8 @@ 同步规则: -- `Git -> PROD` 只读取 `config-dev-main` -- `PROD -> Git` 只写入 `config-prod-snapshot` +- `Git -> PROD` 只读 `config-dev-main` +- `PROD -> Git` 只写 `config-prod-snapshot` 这样做的好处: @@ -127,10 +127,12 @@ - `sync_checkpoint` - `sync_task` -`sync_ack` 在新架构下不再承担跨节点 ACK 作用,可以: +`sync_ack` 在新架构下不再承担跨节点 ACK 作用。 -- 继续保留为接口调用结果日志表 -- 或后续简化下线 +当前建议: + +- 已退出主 schema +- 如后续需要审计,可独立恢复 ## 8. 幂等设计 @@ -183,7 +185,7 @@ direction + sourceVersion + contentHash 生产环境访问 Git 的账号建议最小权限化: -- 对 `config-dev-main` 至少有读权限 +- 对 `config-dev-main` 有读权限 - 对 `config-prod-snapshot` 有写权限 更理想的做法: @@ -205,26 +207,30 @@ direction + sourceVersion + contentHash - H2 表结构 - 定时任务框架 -### 11.2 应该逐步下线的部分 +### 11.2 已退出主运行面的部分 -- `FtpClientService` - FTP 目录配置 - 包上传/下载流程 - ACK 文件机制 - 双端部署假设 -### 11.3 推荐重构方向 +### 11.3 当前代码状态 -把系统收敛为: +当前代码已经收敛为: -- 一个 `prod-agent` -- 两个核心任务 +- 一个正式运行角色 `prod-agent` +- 两个正式调度任务 即: 1. `GitToProdSyncJob` 2. `ProdToGitSnapshotJob` +旧架构源码主体已经删除,当前剩余问题主要是: + +- 个别资源文件仍保留退役标记 +- 工程内少量 `ftp` 命名仍待统一 + ## 12. 结论 现在最合理的做法不是“在旧 FTP 方案上修修补补”,而是直接把架构收敛成: @@ -242,6 +248,6 @@ direction + sourceVersion + contentHash 1. 先确认生产环境是否具备开发 Git 的 push 权限 2. 确认生产 `push/pull` 接口最终协议 -3. 重写主设计文档和详细设计文档,去掉 FTP 相关内容 -4. 收敛代码为单 `prod-agent` -5. 删除 FTP 相关配置和服务 +3. 删除退役标记文件 `application-dev-agent.properties` +4. 继续清理工程中残留的 `ftp` 命名 +5. 补充健康检查和管理能力 diff --git a/pom.xml b/pom.xml index 7f686eb..a21bd80 100644 --- a/pom.xml +++ b/pom.xml @@ -11,10 +11,10 @@ - com.ftptool - ftp-sync-tool + com.gitdirect + git-direct-sync-tool 0.0.1-SNAPSHOT - ftp-sync-tool + git-direct-sync-tool Git direct based configuration sync tool diff --git a/src/main/java/com/ftptool/sync/FtpSyncToolApplication.java b/src/main/java/com/ftptool/sync/GitDirectSyncToolApplication.java similarity index 86% rename from src/main/java/com/ftptool/sync/FtpSyncToolApplication.java rename to src/main/java/com/ftptool/sync/GitDirectSyncToolApplication.java index 8156fa8..bdb5abc 100644 --- a/src/main/java/com/ftptool/sync/FtpSyncToolApplication.java +++ b/src/main/java/com/ftptool/sync/GitDirectSyncToolApplication.java @@ -17,9 +17,9 @@ import org.springframework.scheduling.annotation.EnableScheduling; GitRepoProperties.class, ProdApiProperties.class }) -public class FtpSyncToolApplication { +public class GitDirectSyncToolApplication { public static void main(String[] args) { - SpringApplication.run(FtpSyncToolApplication.class, args); + SpringApplication.run(GitDirectSyncToolApplication.class, args); } } diff --git a/src/main/java/com/ftptool/sync/config/FtpProperties.java b/src/main/java/com/ftptool/sync/config/FtpProperties.java deleted file mode 100644 index c96d904..0000000 --- a/src/main/java/com/ftptool/sync/config/FtpProperties.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.config; - -@Deprecated -public final class FtpProperties { -} diff --git a/src/main/java/com/ftptool/sync/entity/SyncAck.java b/src/main/java/com/ftptool/sync/entity/SyncAck.java deleted file mode 100644 index 24c3d4e..0000000 --- a/src/main/java/com/ftptool/sync/entity/SyncAck.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.entity; - -@Deprecated -public final class SyncAck { -} diff --git a/src/main/java/com/ftptool/sync/job/DevAckScanJob.java b/src/main/java/com/ftptool/sync/job/DevAckScanJob.java deleted file mode 100644 index 3280251..0000000 --- a/src/main/java/com/ftptool/sync/job/DevAckScanJob.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.job; - -@Deprecated -public final class DevAckScanJob { -} diff --git a/src/main/java/com/ftptool/sync/job/DevConsumeProdPackageJob.java b/src/main/java/com/ftptool/sync/job/DevConsumeProdPackageJob.java deleted file mode 100644 index 4e717d3..0000000 --- a/src/main/java/com/ftptool/sync/job/DevConsumeProdPackageJob.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.job; - -@Deprecated -public final class DevConsumeProdPackageJob { -} diff --git a/src/main/java/com/ftptool/sync/job/DevGitScanJob.java b/src/main/java/com/ftptool/sync/job/DevGitScanJob.java deleted file mode 100644 index d3f8dda..0000000 --- a/src/main/java/com/ftptool/sync/job/DevGitScanJob.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.job; - -@Deprecated -public final class DevGitScanJob { -} diff --git a/src/main/java/com/ftptool/sync/job/ProdAckScanJob.java b/src/main/java/com/ftptool/sync/job/ProdAckScanJob.java deleted file mode 100644 index e9b50e6..0000000 --- a/src/main/java/com/ftptool/sync/job/ProdAckScanJob.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.job; - -@Deprecated -public final class ProdAckScanJob { -} diff --git a/src/main/java/com/ftptool/sync/job/ProdConsumeDevPackageJob.java b/src/main/java/com/ftptool/sync/job/ProdConsumeDevPackageJob.java deleted file mode 100644 index 9c35234..0000000 --- a/src/main/java/com/ftptool/sync/job/ProdConsumeDevPackageJob.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.job; - -@Deprecated -public final class ProdConsumeDevPackageJob { -} diff --git a/src/main/java/com/ftptool/sync/job/ProdPullConfigJob.java b/src/main/java/com/ftptool/sync/job/ProdPullConfigJob.java deleted file mode 100644 index 9e37277..0000000 --- a/src/main/java/com/ftptool/sync/job/ProdPullConfigJob.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.job; - -@Deprecated -public final class ProdPullConfigJob { -} diff --git a/src/main/java/com/ftptool/sync/model/RemoteFileInfo.java b/src/main/java/com/ftptool/sync/model/RemoteFileInfo.java deleted file mode 100644 index e672738..0000000 --- a/src/main/java/com/ftptool/sync/model/RemoteFileInfo.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.model; - -@Deprecated -public final class RemoteFileInfo { -} diff --git a/src/main/java/com/ftptool/sync/model/SyncAckFile.java b/src/main/java/com/ftptool/sync/model/SyncAckFile.java deleted file mode 100644 index e5d097b..0000000 --- a/src/main/java/com/ftptool/sync/model/SyncAckFile.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.model; - -@Deprecated -public final class SyncAckFile { -} diff --git a/src/main/java/com/ftptool/sync/orchestrator/DevSyncCoordinator.java b/src/main/java/com/ftptool/sync/orchestrator/DevSyncCoordinator.java deleted file mode 100644 index 8f77256..0000000 --- a/src/main/java/com/ftptool/sync/orchestrator/DevSyncCoordinator.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.orchestrator; - -@Deprecated -public final class DevSyncCoordinator { -} diff --git a/src/main/java/com/ftptool/sync/repository/SyncAckRepository.java b/src/main/java/com/ftptool/sync/repository/SyncAckRepository.java deleted file mode 100644 index 42ba8db..0000000 --- a/src/main/java/com/ftptool/sync/repository/SyncAckRepository.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.repository; - -@Deprecated -public final class SyncAckRepository { -} diff --git a/src/main/java/com/ftptool/sync/repository/SyncTaskRepository.java b/src/main/java/com/ftptool/sync/repository/SyncTaskRepository.java index 3ed11be..1f9668d 100644 --- a/src/main/java/com/ftptool/sync/repository/SyncTaskRepository.java +++ b/src/main/java/com/ftptool/sync/repository/SyncTaskRepository.java @@ -3,7 +3,9 @@ package com.ftptool.sync.repository; import com.ftptool.sync.entity.SyncTask; import com.ftptool.sync.model.SyncDirection; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.domain.Pageable; +import java.util.List; import java.util.Optional; public interface SyncTaskRepository extends JpaRepository { @@ -21,4 +23,11 @@ public interface SyncTaskRepository extends JpaRepository { 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/AckFileService.java b/src/main/java/com/ftptool/sync/service/AckFileService.java deleted file mode 100644 index ce68f94..0000000 --- a/src/main/java/com/ftptool/sync/service/AckFileService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.service; - -@Deprecated -public final class AckFileService { -} diff --git a/src/main/java/com/ftptool/sync/service/AckService.java b/src/main/java/com/ftptool/sync/service/AckService.java deleted file mode 100644 index f86ca43..0000000 --- a/src/main/java/com/ftptool/sync/service/AckService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.service; - -@Deprecated -public final class AckService { -} diff --git a/src/main/java/com/ftptool/sync/service/CheckpointService.java b/src/main/java/com/ftptool/sync/service/CheckpointService.java index 9adc88b..60e0a19 100644 --- a/src/main/java/com/ftptool/sync/service/CheckpointService.java +++ b/src/main/java/com/ftptool/sync/service/CheckpointService.java @@ -6,6 +6,7 @@ import com.ftptool.sync.repository.SyncCheckpointRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.Optional; @Service @@ -31,4 +32,9 @@ public class CheckpointService { checkpoint.setLastSuccessHash(hash); return syncCheckpointRepository.save(checkpoint); } + + @Transactional(readOnly = true) + public List findAllCheckpoints() { + return syncCheckpointRepository.findAll(); + } } diff --git a/src/main/java/com/ftptool/sync/service/FtpClientService.java b/src/main/java/com/ftptool/sync/service/FtpClientService.java deleted file mode 100644 index 55f3b4f..0000000 --- a/src/main/java/com/ftptool/sync/service/FtpClientService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ftptool.sync.service; - -@Deprecated -public final class FtpClientService { -} diff --git a/src/main/java/com/ftptool/sync/service/SyncTaskService.java b/src/main/java/com/ftptool/sync/service/SyncTaskService.java index b67b095..e15b949 100644 --- a/src/main/java/com/ftptool/sync/service/SyncTaskService.java +++ b/src/main/java/com/ftptool/sync/service/SyncTaskService.java @@ -4,9 +4,11 @@ import com.ftptool.sync.entity.SyncTask; import com.ftptool.sync.model.SyncDirection; import com.ftptool.sync.model.SyncStatus; import com.ftptool.sync.repository.SyncTaskRepository; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -84,4 +86,14 @@ public class SyncTaskService { 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/web/SyncManagementController.java b/src/main/java/com/ftptool/sync/web/SyncManagementController.java new file mode 100644 index 0000000..f316beb --- /dev/null +++ b/src/main/java/com/ftptool/sync/web/SyncManagementController.java @@ -0,0 +1,227 @@ +package com.ftptool.sync.web; + +import com.ftptool.sync.entity.SyncCheckpoint; +import com.ftptool.sync.entity.SyncTask; +import com.ftptool.sync.service.CheckpointService; +import com.ftptool.sync.service.SyncTaskService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +@RestController +@RequestMapping("/api/admin/sync") +public class SyncManagementController { + + private final SyncTaskService syncTaskService; + private final CheckpointService checkpointService; + + public SyncManagementController(SyncTaskService syncTaskService, CheckpointService checkpointService) { + this.syncTaskService = syncTaskService; + this.checkpointService = checkpointService; + } + + @GetMapping("/overview") + public SyncOverviewResponse overview( + @RequestParam(name = "recentLimit", defaultValue = "10") int recentLimit, + @RequestParam(name = "failedLimit", defaultValue = "10") int failedLimit + ) { + int normalizedRecentLimit = normalizeLimit(recentLimit); + int normalizedFailedLimit = normalizeLimit(failedLimit); + return new SyncOverviewResponse( + toCheckpointViews(checkpointService.findAllCheckpoints()), + toTaskViews(syncTaskService.findRecentTasks(normalizedRecentLimit)), + toTaskViews(syncTaskService.findFailedTasks(normalizedFailedLimit)) + ); + } + + @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; + } + return Math.min(limit, 100); + } + + private List toTaskViews(List tasks) { + List result = new ArrayList(); + for (SyncTask task : tasks) { + result.add(new SyncTaskView( + task.getTraceId(), + task.getDirection().name(), + task.getSourceVersion(), + task.getContentHash(), + task.getStatus().name(), + task.getRetryCount(), + task.getErrorMsg(), + task.getCreatedAt(), + task.getUpdatedAt() + )); + } + return result; + } + + private List toCheckpointViews(List checkpoints) { + List result = new ArrayList(); + checkpoints.sort(Comparator.comparing(checkpoint -> checkpoint.getDirection().name())); + for (SyncCheckpoint checkpoint : checkpoints) { + result.add(new SyncCheckpointView( + checkpoint.getDirection().name(), + checkpoint.getLastSuccessVersion(), + checkpoint.getLastSuccessHash(), + checkpoint.getUpdatedAt() + )); + } + return result; + } + + public static class SyncOverviewResponse { + + private final List checkpoints; + private final List recentTasks; + private final List failedTasks; + + public SyncOverviewResponse( + List checkpoints, + List recentTasks, + List failedTasks + ) { + this.checkpoints = checkpoints; + this.recentTasks = recentTasks; + this.failedTasks = failedTasks; + } + + public List getCheckpoints() { + return checkpoints; + } + + public List getRecentTasks() { + return recentTasks; + } + + public List getFailedTasks() { + return failedTasks; + } + } + + public static class SyncCheckpointView { + + private final String direction; + private final String lastSuccessVersion; + private final String lastSuccessHash; + private final LocalDateTime updatedAt; + + public SyncCheckpointView( + String direction, + String lastSuccessVersion, + String lastSuccessHash, + LocalDateTime updatedAt + ) { + this.direction = direction; + this.lastSuccessVersion = lastSuccessVersion; + this.lastSuccessHash = lastSuccessHash; + this.updatedAt = updatedAt; + } + + public String getDirection() { + return direction; + } + + public String getLastSuccessVersion() { + return lastSuccessVersion; + } + + public String getLastSuccessHash() { + return lastSuccessHash; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + } + + public static class SyncTaskView { + + private final String traceId; + private final String direction; + private final String sourceVersion; + private final String contentHash; + private final String status; + private final Integer retryCount; + private final String errorMsg; + private final LocalDateTime createdAt; + private final LocalDateTime updatedAt; + + public SyncTaskView( + String traceId, + String direction, + String sourceVersion, + String contentHash, + String status, + Integer retryCount, + String errorMsg, + LocalDateTime createdAt, + LocalDateTime updatedAt + ) { + this.traceId = traceId; + this.direction = direction; + this.sourceVersion = sourceVersion; + this.contentHash = contentHash; + this.status = status; + this.retryCount = retryCount; + this.errorMsg = errorMsg; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public String getTraceId() { + return traceId; + } + + public String getDirection() { + return direction; + } + + public String getSourceVersion() { + return sourceVersion; + } + + public String getContentHash() { + return contentHash; + } + + public String getStatus() { + return status; + } + + public Integer getRetryCount() { + return retryCount; + } + + public String getErrorMsg() { + return errorMsg; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + } +} diff --git a/src/main/resources/application-dev-agent.properties b/src/main/resources/application-dev-agent.properties deleted file mode 100644 index a4e9ec2..0000000 --- a/src/main/resources/application-dev-agent.properties +++ /dev/null @@ -1,2 +0,0 @@ -# Retired profile. -# The Git direct architecture no longer requires a dev-agent deployment. diff --git a/src/test/java/com/ftptool/sync/orchestrator/ProdSyncCoordinatorIntegrationTest.java b/src/test/java/com/ftptool/sync/orchestrator/ProdSyncCoordinatorIntegrationTest.java new file mode 100644 index 0000000..a79a8f1 --- /dev/null +++ b/src/test/java/com/ftptool/sync/orchestrator/ProdSyncCoordinatorIntegrationTest.java @@ -0,0 +1,149 @@ +package com.ftptool.sync.orchestrator; + +import com.ftptool.sync.GitDirectSyncToolApplication; +import com.ftptool.sync.entity.SyncCheckpoint; +import com.ftptool.sync.entity.SyncTask; +import com.ftptool.sync.model.PackageBuildResult; +import com.ftptool.sync.model.PackageManifest; +import com.ftptool.sync.model.ProdPullResult; +import com.ftptool.sync.model.SyncDirection; +import com.ftptool.sync.model.SyncStatus; +import com.ftptool.sync.repository.SyncCheckpointRepository; +import com.ftptool.sync.repository.SyncTaskRepository; +import com.ftptool.sync.service.GitClientService; +import com.ftptool.sync.service.PackageService; +import com.ftptool.sync.service.ProdConfigApiService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest( + classes = GitDirectSyncToolApplication.class, + properties = { + "spring.datasource.url=jdbc:h2:mem:gitdirect-it;DB_CLOSE_DELAY=-1;MODE=MYSQL", + "spring.datasource.driver-class-name=org.h2.Driver", + "spring.datasource.username=sa", + "spring.datasource.password=", + "spring.jpa.hibernate.ddl-auto=none", + "spring.sql.init.mode=always", + "sync.work-dir=./target/test-work", + "sync.package-temp-dir=./target/test-work/package", + "sync.dev-to-prod-staging-dir=./target/test-work/dev-to-prod", + "sync.prod-to-dev-staging-dir=./target/test-work/prod-to-dev", + "git.repo.scan-branch=config-dev-main", + "git.repo.snapshot-branch=config-prod-snapshot", + "git.repo.local-path=./target/test-work/git/config-repo" + } +) +@ActiveProfiles("prod-agent") +class ProdSyncCoordinatorIntegrationTest { + + @Autowired + private ProdSyncCoordinator prodSyncCoordinator; + + @Autowired + private SyncTaskRepository syncTaskRepository; + + @Autowired + private SyncCheckpointRepository syncCheckpointRepository; + + @MockBean + private GitClientService gitClientService; + + @MockBean + private PackageService packageService; + + @MockBean + private ProdConfigApiService prodConfigApiService; + + @BeforeEach + void setUp() { + syncTaskRepository.deleteAll(); + syncCheckpointRepository.deleteAll(); + } + + @Test + void shouldSyncGitToProdAndKeepItIdempotent() throws Exception { + Path zipFile = Files.createTempFile("git-to-prod-", ".zip"); + when(gitClientService.prepareRepositoryAndGetHead("config-dev-main")).thenReturn("commit-a"); + when(gitClientService.exportBranchSnapshot(eq("config-dev-main"), any(Path.class))) + .thenAnswer(invocation -> invocation.getArgument(1)); + when(packageService.calculateDirectoryHash(any(Path.class))).thenReturn("hash-a"); + when(packageService.buildPackageFromDirectory(any(Path.class), any(PackageManifest.class))) + .thenAnswer(invocation -> { + PackageManifest manifest = invocation.getArgument(1); + return new PackageBuildResult(zipFile, manifest.getPackageName(), "hash-a"); + }); + doNothing().when(prodConfigApiService).pushPackage(any(PackageManifest.class), eq(zipFile)); + + prodSyncCoordinator.syncLatestGitToProd(); + prodSyncCoordinator.syncLatestGitToProd(); + + List tasks = syncTaskRepository.findAll(); + assertEquals(1, tasks.size()); + SyncTask task = tasks.get(0); + assertEquals(SyncDirection.DEV_TO_PROD, task.getDirection()); + assertEquals("commit-a", task.getSourceVersion()); + assertEquals("hash-a", task.getContentHash()); + assertEquals(SyncStatus.SUCCESS, task.getStatus()); + + Optional checkpoint = syncCheckpointRepository.findByDirection(SyncDirection.DEV_TO_PROD); + assertTrue(checkpoint.isPresent()); + assertEquals("commit-a", checkpoint.get().getLastSuccessVersion()); + assertEquals("hash-a", checkpoint.get().getLastSuccessHash()); + + verify(prodConfigApiService, times(1)).pushPackage(any(PackageManifest.class), eq(zipFile)); + } + + @Test + void shouldSyncProdSnapshotToGitAndKeepItIdempotent() throws Exception { + Path contentDirectory = Files.createTempDirectory("prod-to-git-"); + Files.write(contentDirectory.resolve("prod-config.json"), "{\"version\":\"prod-v1\"}".getBytes("UTF-8")); + when(prodConfigApiService.pullConfigSnapshot()).thenReturn(new ProdPullResult(contentDirectory, "prod-v1", "hash-b")); + when(gitClientService.syncDirectoryToBranch( + eq(contentDirectory), + eq("config-prod-snapshot"), + contains("prod-v1") + )).thenReturn(true); + + prodSyncCoordinator.syncProdSnapshotToGit(); + prodSyncCoordinator.syncProdSnapshotToGit(); + + List tasks = syncTaskRepository.findAll(); + assertEquals(1, tasks.size()); + SyncTask task = tasks.get(0); + assertEquals(SyncDirection.PROD_TO_DEV, task.getDirection()); + assertEquals("prod-v1", task.getSourceVersion()); + assertEquals("hash-b", task.getContentHash()); + assertEquals(SyncStatus.SUCCESS, task.getStatus()); + + Optional checkpoint = syncCheckpointRepository.findByDirection(SyncDirection.PROD_TO_DEV); + assertTrue(checkpoint.isPresent()); + assertEquals("prod-v1", checkpoint.get().getLastSuccessVersion()); + assertEquals("hash-b", checkpoint.get().getLastSuccessHash()); + + verify(gitClientService, times(1)).syncDirectoryToBranch( + eq(contentDirectory), + eq("config-prod-snapshot"), + contains("prod-v1") + ); + } +}