From 49c915553371a4fc8422a34ca68791495da23b55 Mon Sep 17 00:00:00 2001 From: dark Date: Mon, 20 Apr 2026 14:51:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=BF=85=E8=A6=81=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/ftptool/sync/config/AppConfig.java | 1 + .../com/ftptool/sync/orchestrator/ProdSyncCoordinator.java | 4 ++++ src/main/java/com/ftptool/sync/service/GitClientService.java | 5 +++++ src/main/java/com/ftptool/sync/service/PackageService.java | 4 ++++ .../java/com/ftptool/sync/service/ProdConfigApiService.java | 3 +++ 5 files changed, 17 insertions(+) diff --git a/src/main/java/com/ftptool/sync/config/AppConfig.java b/src/main/java/com/ftptool/sync/config/AppConfig.java index 5481b12..f45ea35 100644 --- a/src/main/java/com/ftptool/sync/config/AppConfig.java +++ b/src/main/java/com/ftptool/sync/config/AppConfig.java @@ -12,6 +12,7 @@ public class AppConfig { @Bean public RestTemplate restTemplate(RestTemplateBuilder builder, ProdApiProperties prodApiProperties) { + // 统一使用生产接口配置中的超时参数,避免各调用点各自维护一套 HTTP 超时。 return builder .setConnectTimeout(Duration.ofMillis(prodApiProperties.getConnectTimeoutMs())) .setReadTimeout(Duration.ofMillis(prodApiProperties.getReadTimeoutMs())) diff --git a/src/main/java/com/ftptool/sync/orchestrator/ProdSyncCoordinator.java b/src/main/java/com/ftptool/sync/orchestrator/ProdSyncCoordinator.java index 00e8df4..30cce7d 100644 --- a/src/main/java/com/ftptool/sync/orchestrator/ProdSyncCoordinator.java +++ b/src/main/java/com/ftptool/sync/orchestrator/ProdSyncCoordinator.java @@ -77,6 +77,7 @@ public class ProdSyncCoordinator { String branch = gitRepoProperties.getScanBranch(); String sourceVersion = gitClientService.prepareRepositoryAndGetHead(branch); Path exportDirectory = workDirectoryService.getDevToProdStagingDir().resolve("git-" + sourceVersion); + // 先导出 Git 工作树快照,再计算内容哈希,避免直接拿工作目录参与后续修改。 gitClientService.exportBranchSnapshot(branch, exportDirectory); String contentHash = packageService.calculateDirectoryHash(exportDirectory); @@ -110,6 +111,7 @@ public class ProdSyncCoordinator { packageBuildResult.getPackageName(), traceId ); + // Git 提交哈希 + 内容哈希作为业务幂等键,避免同一版本重复推送到生产。 syncTaskService.markStatus(task.getTraceId(), SyncStatus.CONSUMING, null); prodConfigApiService.pushPackage(manifest, packageBuildResult.getZipFile()); syncTaskService.markStatus(task.getTraceId(), SyncStatus.SUCCESS, null); @@ -151,6 +153,7 @@ public class ProdSyncCoordinator { ); syncTaskService.markStatus(task.getTraceId(), SyncStatus.CONSUMING, null); + // 生产快照只写入独立 snapshot 分支,避免与开发主分支形成闭环。 String commitMessage = gitRepoProperties.getCommitMessagePrefix() + ": traceId=" + task.getTraceId() + " version=" + task.getSourceVersion(); @@ -182,6 +185,7 @@ public class ProdSyncCoordinator { if (traceId == null) { return; } + // 只有达到最大重试次数后才把任务标记为失败,之前保留为可重试状态。 syncTaskService.increaseRetryCount(traceId, summarizeException(e)); Optional task = syncTaskService.findByTraceId(traceId); int retryCount = task.map(SyncTask::getRetryCount).orElse(0); diff --git a/src/main/java/com/ftptool/sync/service/GitClientService.java b/src/main/java/com/ftptool/sync/service/GitClientService.java index 7e092e1..43d7f28 100644 --- a/src/main/java/com/ftptool/sync/service/GitClientService.java +++ b/src/main/java/com/ftptool/sync/service/GitClientService.java @@ -37,6 +37,7 @@ public class GitClientService { public String prepareRepositoryAndGetHead(String branch) throws IOException, GitAPIException { synchronized (lock) { + // 同一套本地仓库会被多个定时任务复用,这里串行化避免分支切换互相踩工作区。 try (Git git = openOrCloneRepository()) { checkoutBranch(git, branch); pullIfRemoteBranchExists(git, branch); @@ -54,6 +55,7 @@ public class GitClientService { try (Git git = openOrCloneRepository()) { checkoutBranch(git, branch); pullIfRemoteBranchExists(git, branch); + // 导出纯工作树内容,显式排除 .git,避免把仓库内部文件带入同步包。 FileTreeUtils.deleteRecursively(targetDirectory); FileTreeUtils.ensureDirectory(targetDirectory); copyWorkingTreeWithoutGit(getRepositoryPath(), targetDirectory); @@ -70,6 +72,7 @@ public class GitClientService { if (!Files.exists(repositoryPath.resolve(".git"))) { throw new IOException("Git repository does not exist: " + repositoryPath); } + // 生产快照回写采用“整目录覆盖”语义,确保 Git 分支内容与当前生产快照一致。 FileTreeUtils.deleteChildrenExcept(repositoryPath, ".git"); FileTreeUtils.copyDirectory(sourceDirectory, repositoryPath); git.add().addFilepattern(".").call(); @@ -117,6 +120,7 @@ public class GitClientService { Ref remoteRef = repository.findRef("refs/remotes/origin/" + branch); if (localRef == null) { if (remoteRef != null) { + // 本地不存在分支时优先跟踪远端分支,保持与仓库约定一致。 git.checkout() .setCreateBranch(true) .setName(branch) @@ -138,6 +142,7 @@ public class GitClientService { Repository repository = git.getRepository(); Ref remoteRef = repository.findRef("refs/remotes/origin/" + branch); if (remoteRef == null) { + // 首次启动时本地未必有远端分支缓存,先显式 fetch 一次。 git.fetch() .setCredentialsProvider(credentialsProvider()) .setRemote("origin") diff --git a/src/main/java/com/ftptool/sync/service/PackageService.java b/src/main/java/com/ftptool/sync/service/PackageService.java index 86db6dd..a108ddf 100644 --- a/src/main/java/com/ftptool/sync/service/PackageService.java +++ b/src/main/java/com/ftptool/sync/service/PackageService.java @@ -47,6 +47,7 @@ public class PackageService { FileTreeUtils.ensureDirectory(zipFile.getParent()); try (OutputStream outputStream = Files.newOutputStream(zipFile); ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream, StandardCharsets.UTF_8)) { + // 包内始终保留 manifest 和 hash,方便生产接口及后续排障直接核对来源版本。 addJsonEntry(zipOutputStream, MANIFEST_FILE, manifest); addTextEntry(zipOutputStream, HASH_FILE, contentHash); addDirectoryEntries(zipOutputStream, sourceDirectory, CONFIG_DIR); @@ -68,6 +69,7 @@ public class PackageService { ZipEntry entry; while ((entry = zipInputStream.getNextEntry()) != null) { Path target = extractDir.resolve(entry.getName()).normalize(); + // 拒绝 Zip Slip,避免恶意或损坏压缩包把文件写到目标目录之外。 if (!target.startsWith(extractDir)) { throw new IOException("Zip entry escapes target directory: " + entry.getName()); } @@ -90,6 +92,7 @@ public class PackageService { throw new IOException("Package config directory is missing"); } String actualHash = calculateDirectoryHash(configDir); + // 解包后重新计算内容哈希,确保传输过程没有损坏或被篡改。 if (manifest.getContentHash() != null && !manifest.getContentHash().trim().isEmpty() && !manifest.getContentHash().equals(actualHash)) { @@ -101,6 +104,7 @@ public class PackageService { private void addDirectoryEntries(ZipOutputStream zipOutputStream, Path sourceDirectory, String rootName) throws IOException { Path gitDirectory = sourceDirectory.resolve(".git"); try (Stream stream = Files.walk(sourceDirectory)) { + // 打包时显式排除 .git,避免仓库元数据进入同步内容。 stream.filter(path -> !path.equals(sourceDirectory)) .filter(path -> !path.startsWith(gitDirectory)) .forEach(path -> { diff --git a/src/main/java/com/ftptool/sync/service/ProdConfigApiService.java b/src/main/java/com/ftptool/sync/service/ProdConfigApiService.java index cfdeba8..c9efdfc 100644 --- a/src/main/java/com/ftptool/sync/service/ProdConfigApiService.java +++ b/src/main/java/com/ftptool/sync/service/ProdConfigApiService.java @@ -50,6 +50,7 @@ public class ProdConfigApiService { HttpHeaders headers = defaultHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); + // 当前协议约定 push 使用 multipart/form-data 上传标准同步包。 MultiValueMap body = new LinkedMultiValueMap(); body.add("file", new FileSystemResource(zipFile.toFile())); body.add("traceId", manifest.getTraceId()); @@ -67,6 +68,7 @@ public class ProdConfigApiService { public ProdPullResult pullConfigSnapshot() throws IOException { String url = buildUrl(prodApiProperties.getPullPath()); HttpHeaders headers = defaultHeaders(); + // 当前协议约定 pull 直接返回原始配置字节流,由同步工具落成本地文件后再回写 Git。 ResponseEntity response = restTemplate.exchange( url, HttpMethod.GET, @@ -87,6 +89,7 @@ public class ProdConfigApiService { Files.write(targetFile, body); String contentHash = FileHashUtils.sha256(body); + // 优先取服务端显式版本号;如果服务端没给,就退化为内容哈希做幂等判断。 String sourceVersion = firstNonBlank( response.getHeaders().getFirst("X-Config-Version"), response.getHeaders().getETag(),