feat:添加必要注释
This commit is contained in:
parent
0162117ae4
commit
637513c1b3
@ -17,6 +17,10 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
GitRepoProperties.class,
|
||||
ProdApiProperties.class
|
||||
})
|
||||
/**
|
||||
* 应用启动入口。
|
||||
* 统一开启定时任务、重试能力和配置属性绑定。
|
||||
*/
|
||||
public class GitDirectSyncToolApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@ -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()))
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
package com.ftptool.sync.model;
|
||||
|
||||
/**
|
||||
* 同步方向定义。
|
||||
*/
|
||||
public enum SyncDirection {
|
||||
/** 开发 Git 配置下发到生产。 */
|
||||
DEV_TO_PROD,
|
||||
/** 生产配置快照回写到 Git。 */
|
||||
PROD_TO_DEV
|
||||
}
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
package com.ftptool.sync.model;
|
||||
|
||||
/**
|
||||
* 运行角色定义。
|
||||
*/
|
||||
public enum SyncRole {
|
||||
/** 开发侧角色,当前架构中已退役。 */
|
||||
DEV,
|
||||
/** 生产侧角色,当前主运行角色。 */
|
||||
PROD,
|
||||
/** 未指定角色。 */
|
||||
UNSET
|
||||
}
|
||||
|
||||
@ -1,10 +1,19 @@
|
||||
package com.ftptool.sync.model;
|
||||
|
||||
/**
|
||||
* 同步任务状态定义。
|
||||
*/
|
||||
public enum SyncStatus {
|
||||
/** 任务已创建,尚未开始执行。 */
|
||||
CREATED,
|
||||
/** 预留状态,表示已进入中间暂存阶段。 */
|
||||
STAGED,
|
||||
/** 预留状态,表示已完成上传。 */
|
||||
UPLOADED,
|
||||
/** 任务正在处理。 */
|
||||
CONSUMING,
|
||||
/** 任务处理成功。 */
|
||||
SUCCESS,
|
||||
/** 任务处理失败,且已达到失败判定条件。 */
|
||||
FAILED
|
||||
}
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -6,7 +6,13 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 同步检查点仓储。
|
||||
*/
|
||||
public interface SyncCheckpointRepository extends JpaRepository<SyncCheckpoint, Long> {
|
||||
|
||||
/**
|
||||
* 按同步方向查询检查点。
|
||||
*/
|
||||
Optional<SyncCheckpoint> findByDirection(SyncDirection direction);
|
||||
}
|
||||
|
||||
@ -8,24 +8,42 @@ import org.springframework.data.domain.Pageable;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 同步任务仓储。
|
||||
*/
|
||||
public interface SyncTaskRepository extends JpaRepository<SyncTask, Long> {
|
||||
|
||||
/**
|
||||
* 按 traceId 查询单个任务。
|
||||
*/
|
||||
Optional<SyncTask> findByTraceId(String traceId);
|
||||
|
||||
/**
|
||||
* 按业务幂等键查询任务。
|
||||
*/
|
||||
Optional<SyncTask> findByDirectionAndSourceVersionAndContentHash(
|
||||
SyncDirection direction,
|
||||
String sourceVersion,
|
||||
String contentHash
|
||||
);
|
||||
|
||||
/**
|
||||
* 判断某个业务幂等键是否已存在。
|
||||
*/
|
||||
boolean existsByDirectionAndSourceVersionAndContentHash(
|
||||
SyncDirection direction,
|
||||
String sourceVersion,
|
||||
String contentHash
|
||||
);
|
||||
|
||||
/**
|
||||
* 查询最近更新的任务列表。
|
||||
*/
|
||||
List<SyncTask> findAllByOrderByUpdatedAtDesc(Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查询最近失败任务列表。
|
||||
*/
|
||||
List<SyncTask> findByStatusOrderByUpdatedAtDesc(
|
||||
com.ftptool.sync.model.SyncStatus status,
|
||||
Pageable pageable
|
||||
|
||||
@ -9,6 +9,9 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 同步检查点服务。
|
||||
*/
|
||||
@Service
|
||||
public class CheckpointService {
|
||||
|
||||
@ -18,11 +21,17 @@ public class CheckpointService {
|
||||
this.syncCheckpointRepository = syncCheckpointRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取某个方向当前检查点。
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<SyncCheckpoint> getCheckpoint(SyncDirection direction) {
|
||||
return syncCheckpointRepository.findByDirection(direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存某个方向最后一次成功的版本和哈希。
|
||||
*/
|
||||
@Transactional
|
||||
public SyncCheckpoint saveCheckpoint(SyncDirection direction, String version, String hash) {
|
||||
SyncCheckpoint checkpoint = syncCheckpointRepository.findByDirection(direction)
|
||||
@ -33,6 +42,9 @@ public class CheckpointService {
|
||||
return syncCheckpointRepository.save(checkpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部检查点,供管理接口展示。
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<SyncCheckpoint> findAllCheckpoints() {
|
||||
return syncCheckpointRepository.findAll();
|
||||
|
||||
@ -24,6 +24,10 @@ import java.nio.file.StandardCopyOption;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Service
|
||||
/**
|
||||
* Git 客户端服务。
|
||||
* 负责 clone / pull / checkout / commit / push 等仓库操作。
|
||||
*/
|
||||
public class GitClientService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GitClientService.class);
|
||||
@ -35,6 +39,9 @@ public class GitClientService {
|
||||
this.gitRepoProperties = gitRepoProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备仓库并返回指定分支当前 HEAD。
|
||||
*/
|
||||
public String prepareRepositoryAndGetHead(String branch) throws IOException, GitAPIException {
|
||||
synchronized (lock) {
|
||||
// 同一套本地仓库会被多个定时任务复用,这里串行化避免分支切换互相踩工作区。
|
||||
@ -50,6 +57,9 @@ public class GitClientService {
|
||||
return new File(gitRepoProperties.getLocalPath()).toPath().toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出指定分支的工作树快照,供后续打包或哈希计算使用。
|
||||
*/
|
||||
public Path exportBranchSnapshot(String branch, Path targetDirectory) throws IOException, GitAPIException {
|
||||
synchronized (lock) {
|
||||
try (Git git = openOrCloneRepository()) {
|
||||
@ -114,6 +124,9 @@ public class GitClientService {
|
||||
.call();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到目标分支,不存在时按远端或本地新建分支。
|
||||
*/
|
||||
private void checkoutBranch(Git git, String branch) throws GitAPIException, IOException {
|
||||
Repository repository = git.getRepository();
|
||||
Ref localRef = repository.findRef(branch);
|
||||
@ -166,6 +179,9 @@ public class GitClientService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制仓库工作树内容,显式排除 .git 目录。
|
||||
*/
|
||||
private void copyWorkingTreeWithoutGit(Path repositoryPath, Path targetDirectory) throws IOException {
|
||||
try (Stream<Path> stream = Files.list(repositoryPath)) {
|
||||
stream.filter(path -> !".git".equals(path.getFileName().toString()))
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -12,6 +12,10 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 同步任务服务。
|
||||
* 封装任务创建、状态变更和失败重试次数更新逻辑。
|
||||
*/
|
||||
@Service
|
||||
public class SyncTaskService {
|
||||
|
||||
@ -21,11 +25,18 @@ public class SyncTaskService {
|
||||
this.syncTaskRepository = syncTaskRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按默认策略创建或加载任务。
|
||||
*/
|
||||
@Transactional
|
||||
public SyncTask createOrLoadTask(SyncDirection direction, String sourceVersion, String contentHash, String packageName) {
|
||||
return createOrLoadTask(direction, sourceVersion, contentHash, packageName, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于业务幂等键创建或加载任务。
|
||||
* 如果任务已存在,直接返回已有记录,不重复插入。
|
||||
*/
|
||||
@Transactional
|
||||
public SyncTask createOrLoadTask(
|
||||
SyncDirection direction,
|
||||
@ -53,16 +64,25 @@ public class SyncTaskService {
|
||||
return syncTaskRepository.save(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 traceId 查询任务。
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<SyncTask> findByTraceId(String traceId) {
|
||||
return syncTaskRepository.findByTraceId(traceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按业务幂等键查询任务。
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<SyncTask> findByBusinessKey(SyncDirection direction, String sourceVersion, String contentHash) {
|
||||
return syncTaskRepository.findByDirectionAndSourceVersionAndContentHash(direction, sourceVersion, contentHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务状态和最近错误信息。
|
||||
*/
|
||||
@Transactional
|
||||
public void markStatus(String traceId, SyncStatus status, String errorMsg) {
|
||||
syncTaskRepository.findByTraceId(traceId).ifPresent(task -> {
|
||||
@ -72,6 +92,9 @@ public class SyncTaskService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加失败重试次数,并记录最近错误摘要。
|
||||
*/
|
||||
@Transactional
|
||||
public void increaseRetryCount(String traceId, String errorMsg) {
|
||||
syncTaskRepository.findByTraceId(traceId).ifPresent(task -> {
|
||||
@ -82,16 +105,25 @@ public class SyncTaskService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断某个业务幂等键是否已处理过。
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public boolean existsProcessed(SyncDirection direction, String sourceVersion, String contentHash) {
|
||||
return syncTaskRepository.existsByDirectionAndSourceVersionAndContentHash(direction, sourceVersion, contentHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询最近任务,供管理接口展示。
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<SyncTask> findRecentTasks(int limit) {
|
||||
return syncTaskRepository.findAllByOrderByUpdatedAtDesc(PageRequest.of(0, limit));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询最近失败任务,供管理接口展示。
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<SyncTask> findFailedTasks(int limit) {
|
||||
return syncTaskRepository.findByStatusOrderByUpdatedAtDesc(SyncStatus.FAILED, PageRequest.of(0, limit));
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -14,11 +14,17 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 文件与目录哈希工具类。
|
||||
*/
|
||||
public final class FileHashUtils {
|
||||
|
||||
private FileHashUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算单个文件的 SHA-256。
|
||||
*/
|
||||
public static String sha256(Path file) throws IOException {
|
||||
MessageDigest digest = newDigest();
|
||||
try (InputStream inputStream = Files.newInputStream(file);
|
||||
@ -31,12 +37,19 @@ public final class FileHashUtils {
|
||||
return toHex(digest.digest());
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算字节数组的 SHA-256。
|
||||
*/
|
||||
public static String sha256(byte[] bytes) {
|
||||
MessageDigest digest = newDigest();
|
||||
digest.update(bytes);
|
||||
return toHex(digest.digest());
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算目录内容哈希。
|
||||
* 哈希时同时包含相对路径和文件内容,保证目录结构变化也会反映到结果中。
|
||||
*/
|
||||
public static String sha256Directory(Path directory) throws IOException {
|
||||
MessageDigest digest = newDigest();
|
||||
List<Path> files = listRegularFiles(directory);
|
||||
@ -50,6 +63,9 @@ public final class FileHashUtils {
|
||||
return toHex(digest.digest());
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出目录下的全部普通文件,并按相对路径稳定排序。
|
||||
*/
|
||||
private static List<Path> listRegularFiles(Path directory) throws IOException {
|
||||
try (Stream<Path> stream = Files.walk(directory)) {
|
||||
return stream
|
||||
@ -59,6 +75,9 @@ public final class FileHashUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 SHA-256 摘要器。
|
||||
*/
|
||||
private static MessageDigest newDigest() {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-256");
|
||||
@ -67,6 +86,9 @@ public final class FileHashUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把摘要字节转成十六进制字符串。
|
||||
*/
|
||||
private static String toHex(byte[] bytes) {
|
||||
StringBuilder builder = new StringBuilder(bytes.length * 2);
|
||||
for (byte aByte : bytes) {
|
||||
|
||||
@ -10,17 +10,26 @@ import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Comparator;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 文件树操作工具类。
|
||||
*/
|
||||
public final class FileTreeUtils {
|
||||
|
||||
private FileTreeUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保目录存在,不存在时递归创建。
|
||||
*/
|
||||
public static void ensureDirectory(Path path) throws IOException {
|
||||
if (path != null) {
|
||||
Files.createDirectories(path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除目录下除保留名称外的所有子项。
|
||||
*/
|
||||
public static void deleteChildrenExcept(Path directory, String reservedName) throws IOException {
|
||||
try (Stream<Path> stream = Files.list(directory)) {
|
||||
for (Path child : stream.sorted(Comparator.reverseOrder()).toArray(Path[]::new)) {
|
||||
@ -32,6 +41,9 @@ public final class FileTreeUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归删除目录或文件。
|
||||
*/
|
||||
public static void deleteRecursively(Path path) throws IOException {
|
||||
if (path == null || Files.notExists(path)) {
|
||||
return;
|
||||
@ -51,6 +63,9 @@ public final class FileTreeUtils {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归复制目录。
|
||||
*/
|
||||
public static void copyDirectory(Path source, Path target) throws IOException {
|
||||
ensureDirectory(target);
|
||||
Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
|
||||
|
||||
@ -16,6 +16,10 @@ import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/sync")
|
||||
/**
|
||||
* 同步管理接口。
|
||||
* 当前提供最近任务、失败任务和检查点的只读查询能力。
|
||||
*/
|
||||
public class SyncManagementController {
|
||||
|
||||
private final SyncTaskService syncTaskService;
|
||||
@ -26,6 +30,9 @@ public class SyncManagementController {
|
||||
this.checkpointService = checkpointService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合返回检查点、最近任务和失败任务。
|
||||
*/
|
||||
@GetMapping("/overview")
|
||||
public SyncOverviewResponse overview(
|
||||
@RequestParam(name = "recentLimit", defaultValue = "10") int recentLimit,
|
||||
@ -40,16 +47,25 @@ public class SyncManagementController {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询最近同步任务。
|
||||
*/
|
||||
@GetMapping("/tasks/recent")
|
||||
public List<SyncTaskView> recentTasks(@RequestParam(name = "limit", defaultValue = "20") int limit) {
|
||||
return toTaskViews(syncTaskService.findRecentTasks(normalizeLimit(limit)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询最近失败任务。
|
||||
*/
|
||||
@GetMapping("/tasks/failed")
|
||||
public List<SyncTaskView> failedTasks(@RequestParam(name = "limit", defaultValue = "20") int limit) {
|
||||
return toTaskViews(syncTaskService.findFailedTasks(normalizeLimit(limit)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一限制分页上限,避免管理接口一次性返回过多数据。
|
||||
*/
|
||||
private int normalizeLimit(int limit) {
|
||||
if (limit < 1) {
|
||||
return 1;
|
||||
@ -57,6 +73,9 @@ public class SyncManagementController {
|
||||
return Math.min(limit, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体对象转换为接口输出视图。
|
||||
*/
|
||||
private List<SyncTaskView> toTaskViews(List<SyncTask> tasks) {
|
||||
List<SyncTaskView> result = new ArrayList<SyncTaskView>();
|
||||
for (SyncTask task : tasks) {
|
||||
@ -75,6 +94,9 @@ public class SyncManagementController {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将检查点实体转换为接口输出视图。
|
||||
*/
|
||||
private List<SyncCheckpointView> toCheckpointViews(List<SyncCheckpoint> checkpoints) {
|
||||
List<SyncCheckpointView> result = new ArrayList<SyncCheckpointView>();
|
||||
checkpoints.sort(Comparator.comparing(checkpoint -> checkpoint.getDirection().name()));
|
||||
@ -89,6 +111,9 @@ public class SyncManagementController {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理页总览响应。
|
||||
*/
|
||||
public static class SyncOverviewResponse {
|
||||
|
||||
private final List<SyncCheckpointView> checkpoints;
|
||||
@ -118,6 +143,9 @@ public class SyncManagementController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查点输出视图。
|
||||
*/
|
||||
public static class SyncCheckpointView {
|
||||
|
||||
private final String direction;
|
||||
@ -154,6 +182,9 @@ public class SyncManagementController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步任务输出视图。
|
||||
*/
|
||||
public static class SyncTaskView {
|
||||
|
||||
private final String traceId;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user