feat:添加必要注释

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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