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")
+ );
+ }
+}