添加必要注释

This commit is contained in:
dark 2026-04-20 14:51:59 +08:00
parent dcfdc83444
commit 49c9155533
5 changed files with 17 additions and 0 deletions

View File

@ -12,6 +12,7 @@ public class AppConfig {
@Bean @Bean
public RestTemplate restTemplate(RestTemplateBuilder builder, ProdApiProperties prodApiProperties) { public RestTemplate restTemplate(RestTemplateBuilder builder, ProdApiProperties prodApiProperties) {
// 统一使用生产接口配置中的超时参数避免各调用点各自维护一套 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

@ -77,6 +77,7 @@ public class ProdSyncCoordinator {
String branch = gitRepoProperties.getScanBranch(); String branch = gitRepoProperties.getScanBranch();
String sourceVersion = gitClientService.prepareRepositoryAndGetHead(branch); String sourceVersion = gitClientService.prepareRepositoryAndGetHead(branch);
Path exportDirectory = workDirectoryService.getDevToProdStagingDir().resolve("git-" + sourceVersion); Path exportDirectory = workDirectoryService.getDevToProdStagingDir().resolve("git-" + sourceVersion);
// 先导出 Git 工作树快照再计算内容哈希避免直接拿工作目录参与后续修改
gitClientService.exportBranchSnapshot(branch, exportDirectory); gitClientService.exportBranchSnapshot(branch, exportDirectory);
String contentHash = packageService.calculateDirectoryHash(exportDirectory); String contentHash = packageService.calculateDirectoryHash(exportDirectory);
@ -110,6 +111,7 @@ public class ProdSyncCoordinator {
packageBuildResult.getPackageName(), packageBuildResult.getPackageName(),
traceId traceId
); );
// Git 提交哈希 + 内容哈希作为业务幂等键避免同一版本重复推送到生产
syncTaskService.markStatus(task.getTraceId(), SyncStatus.CONSUMING, null); syncTaskService.markStatus(task.getTraceId(), SyncStatus.CONSUMING, null);
prodConfigApiService.pushPackage(manifest, packageBuildResult.getZipFile()); prodConfigApiService.pushPackage(manifest, packageBuildResult.getZipFile());
syncTaskService.markStatus(task.getTraceId(), SyncStatus.SUCCESS, null); syncTaskService.markStatus(task.getTraceId(), SyncStatus.SUCCESS, null);
@ -151,6 +153,7 @@ public class ProdSyncCoordinator {
); );
syncTaskService.markStatus(task.getTraceId(), SyncStatus.CONSUMING, null); syncTaskService.markStatus(task.getTraceId(), SyncStatus.CONSUMING, null);
// 生产快照只写入独立 snapshot 分支避免与开发主分支形成闭环
String commitMessage = gitRepoProperties.getCommitMessagePrefix() String commitMessage = gitRepoProperties.getCommitMessagePrefix()
+ ": traceId=" + task.getTraceId() + ": traceId=" + task.getTraceId()
+ " version=" + task.getSourceVersion(); + " version=" + task.getSourceVersion();
@ -182,6 +185,7 @@ public class ProdSyncCoordinator {
if (traceId == null) { if (traceId == null) {
return; return;
} }
// 只有达到最大重试次数后才把任务标记为失败之前保留为可重试状态
syncTaskService.increaseRetryCount(traceId, summarizeException(e)); syncTaskService.increaseRetryCount(traceId, summarizeException(e));
Optional<SyncTask> task = syncTaskService.findByTraceId(traceId); Optional<SyncTask> task = syncTaskService.findByTraceId(traceId);
int retryCount = task.map(SyncTask::getRetryCount).orElse(0); int retryCount = task.map(SyncTask::getRetryCount).orElse(0);

View File

@ -37,6 +37,7 @@ public class GitClientService {
public String prepareRepositoryAndGetHead(String branch) throws IOException, GitAPIException { public String prepareRepositoryAndGetHead(String branch) throws IOException, GitAPIException {
synchronized (lock) { synchronized (lock) {
// 同一套本地仓库会被多个定时任务复用这里串行化避免分支切换互相踩工作区
try (Git git = openOrCloneRepository()) { try (Git git = openOrCloneRepository()) {
checkoutBranch(git, branch); checkoutBranch(git, branch);
pullIfRemoteBranchExists(git, branch); pullIfRemoteBranchExists(git, branch);
@ -54,6 +55,7 @@ public class GitClientService {
try (Git git = openOrCloneRepository()) { try (Git git = openOrCloneRepository()) {
checkoutBranch(git, branch); checkoutBranch(git, branch);
pullIfRemoteBranchExists(git, branch); pullIfRemoteBranchExists(git, branch);
// 导出纯工作树内容显式排除 .git避免把仓库内部文件带入同步包
FileTreeUtils.deleteRecursively(targetDirectory); FileTreeUtils.deleteRecursively(targetDirectory);
FileTreeUtils.ensureDirectory(targetDirectory); FileTreeUtils.ensureDirectory(targetDirectory);
copyWorkingTreeWithoutGit(getRepositoryPath(), targetDirectory); copyWorkingTreeWithoutGit(getRepositoryPath(), targetDirectory);
@ -70,6 +72,7 @@ public class GitClientService {
if (!Files.exists(repositoryPath.resolve(".git"))) { if (!Files.exists(repositoryPath.resolve(".git"))) {
throw new IOException("Git repository does not exist: " + repositoryPath); throw new IOException("Git repository does not exist: " + repositoryPath);
} }
// 生产快照回写采用整目录覆盖语义确保 Git 分支内容与当前生产快照一致
FileTreeUtils.deleteChildrenExcept(repositoryPath, ".git"); FileTreeUtils.deleteChildrenExcept(repositoryPath, ".git");
FileTreeUtils.copyDirectory(sourceDirectory, repositoryPath); FileTreeUtils.copyDirectory(sourceDirectory, repositoryPath);
git.add().addFilepattern(".").call(); git.add().addFilepattern(".").call();
@ -117,6 +120,7 @@ public class GitClientService {
Ref remoteRef = repository.findRef("refs/remotes/origin/" + branch); Ref remoteRef = repository.findRef("refs/remotes/origin/" + branch);
if (localRef == null) { if (localRef == null) {
if (remoteRef != null) { if (remoteRef != null) {
// 本地不存在分支时优先跟踪远端分支保持与仓库约定一致
git.checkout() git.checkout()
.setCreateBranch(true) .setCreateBranch(true)
.setName(branch) .setName(branch)
@ -138,6 +142,7 @@ public class GitClientService {
Repository repository = git.getRepository(); Repository repository = git.getRepository();
Ref remoteRef = repository.findRef("refs/remotes/origin/" + branch); Ref remoteRef = repository.findRef("refs/remotes/origin/" + branch);
if (remoteRef == null) { if (remoteRef == null) {
// 首次启动时本地未必有远端分支缓存先显式 fetch 一次
git.fetch() git.fetch()
.setCredentialsProvider(credentialsProvider()) .setCredentialsProvider(credentialsProvider())
.setRemote("origin") .setRemote("origin")

View File

@ -47,6 +47,7 @@ public class PackageService {
FileTreeUtils.ensureDirectory(zipFile.getParent()); FileTreeUtils.ensureDirectory(zipFile.getParent());
try (OutputStream outputStream = Files.newOutputStream(zipFile); try (OutputStream outputStream = Files.newOutputStream(zipFile);
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream, StandardCharsets.UTF_8)) { ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream, StandardCharsets.UTF_8)) {
// 包内始终保留 manifest hash方便生产接口及后续排障直接核对来源版本
addJsonEntry(zipOutputStream, MANIFEST_FILE, manifest); addJsonEntry(zipOutputStream, MANIFEST_FILE, manifest);
addTextEntry(zipOutputStream, HASH_FILE, contentHash); addTextEntry(zipOutputStream, HASH_FILE, contentHash);
addDirectoryEntries(zipOutputStream, sourceDirectory, CONFIG_DIR); addDirectoryEntries(zipOutputStream, sourceDirectory, CONFIG_DIR);
@ -68,6 +69,7 @@ public class PackageService {
ZipEntry entry; ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) { while ((entry = zipInputStream.getNextEntry()) != null) {
Path target = extractDir.resolve(entry.getName()).normalize(); Path target = extractDir.resolve(entry.getName()).normalize();
// 拒绝 Zip Slip避免恶意或损坏压缩包把文件写到目标目录之外
if (!target.startsWith(extractDir)) { if (!target.startsWith(extractDir)) {
throw new IOException("Zip entry escapes target directory: " + entry.getName()); 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"); throw new IOException("Package config directory is missing");
} }
String actualHash = calculateDirectoryHash(configDir); String actualHash = calculateDirectoryHash(configDir);
// 解包后重新计算内容哈希确保传输过程没有损坏或被篡改
if (manifest.getContentHash() != null if (manifest.getContentHash() != null
&& !manifest.getContentHash().trim().isEmpty() && !manifest.getContentHash().trim().isEmpty()
&& !manifest.getContentHash().equals(actualHash)) { && !manifest.getContentHash().equals(actualHash)) {
@ -101,6 +104,7 @@ public class PackageService {
private void addDirectoryEntries(ZipOutputStream zipOutputStream, Path sourceDirectory, String rootName) throws IOException { private void addDirectoryEntries(ZipOutputStream zipOutputStream, Path sourceDirectory, String rootName) throws IOException {
Path gitDirectory = sourceDirectory.resolve(".git"); Path gitDirectory = sourceDirectory.resolve(".git");
try (Stream<Path> stream = Files.walk(sourceDirectory)) { try (Stream<Path> stream = Files.walk(sourceDirectory)) {
// 打包时显式排除 .git避免仓库元数据进入同步内容
stream.filter(path -> !path.equals(sourceDirectory)) stream.filter(path -> !path.equals(sourceDirectory))
.filter(path -> !path.startsWith(gitDirectory)) .filter(path -> !path.startsWith(gitDirectory))
.forEach(path -> { .forEach(path -> {

View File

@ -50,6 +50,7 @@ public class ProdConfigApiService {
HttpHeaders headers = defaultHeaders(); HttpHeaders headers = defaultHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA); headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// 当前协议约定 push 使用 multipart/form-data 上传标准同步包
MultiValueMap<String, Object> body = new LinkedMultiValueMap<String, Object>(); MultiValueMap<String, Object> body = new LinkedMultiValueMap<String, Object>();
body.add("file", new FileSystemResource(zipFile.toFile())); body.add("file", new FileSystemResource(zipFile.toFile()));
body.add("traceId", manifest.getTraceId()); body.add("traceId", manifest.getTraceId());
@ -67,6 +68,7 @@ public class ProdConfigApiService {
public ProdPullResult pullConfigSnapshot() throws IOException { public ProdPullResult pullConfigSnapshot() throws IOException {
String url = buildUrl(prodApiProperties.getPullPath()); String url = buildUrl(prodApiProperties.getPullPath());
HttpHeaders headers = defaultHeaders(); HttpHeaders headers = defaultHeaders();
// 当前协议约定 pull 直接返回原始配置字节流由同步工具落成本地文件后再回写 Git
ResponseEntity<byte[]> response = restTemplate.exchange( ResponseEntity<byte[]> response = restTemplate.exchange(
url, url,
HttpMethod.GET, HttpMethod.GET,
@ -87,6 +89,7 @@ public class ProdConfigApiService {
Files.write(targetFile, body); Files.write(targetFile, body);
String contentHash = FileHashUtils.sha256(body); String contentHash = FileHashUtils.sha256(body);
// 优先取服务端显式版本号如果服务端没给就退化为内容哈希做幂等判断
String sourceVersion = firstNonBlank( String sourceVersion = firstNonBlank(
response.getHeaders().getFirst("X-Config-Version"), response.getHeaders().getFirst("X-Config-Version"),
response.getHeaders().getETag(), response.getHeaders().getETag(),