FtpTool/src/main/java/com/ftptool/sync/service/ProdPullAckService.java
dark 114bcf33d8 feat: 同步链路支持版本分支目录映射、动态快照分支和 ackFail 定向重拉
- Git -> PROD 改为按 branch 作为 configVersion
- 按 airportId/appName/fileName 目录结构解析 pushConfig 参数
- PROD -> Git 改为写入 snapshot-branch/<configVersion> 动态分支
- pullConfig 支持 configVersion/fileName 可选过滤
- 抽出 ConfigCryptoService,统一收口加解密扩展点
- ackFail 落库增加重试上下文,支持按 airportId/appName/configVersion/fileName 定向重拉
- 同步更新测试、接口文档和 current.md
2026-04-28 14:49:33 +08:00

278 lines
10 KiB
Java

package com.ftptool.sync.service;
import com.ftptool.sync.entity.ProdPullAckRecord;
import com.ftptool.sync.model.ProdPullAckStatus;
import com.ftptool.sync.model.ProdPullResult;
import com.ftptool.sync.repository.ProdPullAckRecordRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Stores pullConfig ack states and retry plans.
*/
@Service
public class ProdPullAckService {
private static final int RETRY_DELAY_BASE_SECONDS = 30;
private final ProdPullAckRecordRepository prodPullAckRecordRepository;
public ProdPullAckService(ProdPullAckRecordRepository prodPullAckRecordRepository) {
this.prodPullAckRecordRepository = prodPullAckRecordRepository;
}
@Transactional(readOnly = true)
public PendingAckSummary getPendingAckSummary() {
List<String> successIds = toRemoteIds(
prodPullAckRecordRepository.findByAckStatusAndReportedFalseOrderByUpdatedAtAsc(ProdPullAckStatus.SUCCESS)
);
List<String> failedIds = toRemoteIds(
prodPullAckRecordRepository.findByAckStatusAndReportedFalseOrderByUpdatedAtAsc(ProdPullAckStatus.FAILED)
);
return new PendingAckSummary(successIds, failedIds);
}
@Transactional
public void recordAckSuccess(Collection<ProdPullResult.PulledConfigRef> pulledConfigs) {
if (pulledConfigs == null) {
return;
}
for (ProdPullResult.PulledConfigRef pulledConfig : pulledConfigs) {
if (!hasRemoteId(pulledConfig)) {
continue;
}
ProdPullAckRecord record = prodPullAckRecordRepository.findByRemoteConfigId(pulledConfig.getRemoteConfigId())
.orElseGet(ProdPullAckRecord::new);
record.setRemoteConfigId(pulledConfig.getRemoteConfigId());
record.setAckStatus(ProdPullAckStatus.SUCCESS);
record.setReported(Boolean.FALSE);
applyPullMetadata(record, pulledConfig);
record.setRetryCount(0);
record.setNextRetryAt(null);
record.setLastErrorMsg(null);
prodPullAckRecordRepository.save(record);
}
}
@Transactional
public void recordPullFailure(
Collection<ProdPullResult.PulledConfigRef> pulledConfigs,
String errorMsg,
boolean retryAttempt
) {
if (pulledConfigs == null) {
return;
}
for (ProdPullResult.PulledConfigRef pulledConfig : pulledConfigs) {
if (!hasRemoteId(pulledConfig)) {
continue;
}
ProdPullAckRecord record = prodPullAckRecordRepository.findByRemoteConfigId(pulledConfig.getRemoteConfigId())
.orElseGet(ProdPullAckRecord::new);
record.setRemoteConfigId(pulledConfig.getRemoteConfigId());
record.setAckStatus(ProdPullAckStatus.FAILED);
record.setReported(Boolean.FALSE);
applyPullMetadata(record, pulledConfig);
record.setLastErrorMsg(errorMsg);
if (retryAttempt) {
int retryCount = safeRetryCount(record) + 1;
record.setRetryCount(retryCount);
record.setNextRetryAt(calculateNextRetryAt(retryCount));
} else {
record.setRetryCount(0);
record.setNextRetryAt(LocalDateTime.now());
}
prodPullAckRecordRepository.save(record);
}
}
@Transactional
public void markRetryAttemptFailed(RetryPullRequest retryPullRequest, String errorMsg) {
if (retryPullRequest == null || retryPullRequest.getRemoteConfigIds().isEmpty()) {
return;
}
for (String remoteConfigId : retryPullRequest.getRemoteConfigIds()) {
prodPullAckRecordRepository.findByRemoteConfigId(remoteConfigId).ifPresent(record -> {
int retryCount = safeRetryCount(record) + 1;
record.setRetryCount(retryCount);
record.setNextRetryAt(calculateNextRetryAt(retryCount));
record.setLastErrorMsg(errorMsg);
record.setAckStatus(ProdPullAckStatus.FAILED);
prodPullAckRecordRepository.save(record);
});
}
}
@Transactional(readOnly = true)
public List<RetryPullRequest> getRetryPullRequests(int maxRetryCount) {
List<ProdPullAckRecord> failedRecords = prodPullAckRecordRepository.findByAckStatusOrderByUpdatedAtAsc(ProdPullAckStatus.FAILED);
LocalDateTime now = LocalDateTime.now();
Map<String, RetryPullRequest> retryPullRequests = new LinkedHashMap<String, RetryPullRequest>();
for (ProdPullAckRecord failedRecord : failedRecords) {
if (!hasRetryMetadata(failedRecord)) {
continue;
}
if (safeRetryCount(failedRecord) >= maxRetryCount) {
continue;
}
if (failedRecord.getNextRetryAt() != null && failedRecord.getNextRetryAt().isAfter(now)) {
continue;
}
String groupKey = buildRetryGroupKey(failedRecord);
RetryPullRequest retryPullRequest = retryPullRequests.get(groupKey);
if (retryPullRequest == null) {
retryPullRequest = new RetryPullRequest(
failedRecord.getSourceVersion(),
failedRecord.getAirportId(),
failedRecord.getAppName(),
failedRecord.getFileName()
);
retryPullRequests.put(groupKey, retryPullRequest);
}
retryPullRequest.addRemoteConfigId(failedRecord.getRemoteConfigId());
}
return new ArrayList<RetryPullRequest>(retryPullRequests.values());
}
@Transactional
public void markPendingAsReported() {
List<ProdPullAckRecord> records = prodPullAckRecordRepository.findByReportedFalseOrderByUpdatedAtAsc();
for (ProdPullAckRecord record : records) {
record.setReported(Boolean.TRUE);
prodPullAckRecordRepository.save(record);
}
}
private void applyPullMetadata(ProdPullAckRecord record, ProdPullResult.PulledConfigRef pulledConfig) {
if (pulledConfig == null) {
return;
}
if (StringUtils.hasText(pulledConfig.getConfigVersion())) {
record.setSourceVersion(pulledConfig.getConfigVersion().trim());
}
if (StringUtils.hasText(pulledConfig.getAirportId())) {
record.setAirportId(pulledConfig.getAirportId().trim());
}
if (StringUtils.hasText(pulledConfig.getAppName())) {
record.setAppName(pulledConfig.getAppName().trim());
}
if (StringUtils.hasText(pulledConfig.getFileName())) {
record.setFileName(pulledConfig.getFileName().trim());
}
}
private boolean hasRemoteId(ProdPullResult.PulledConfigRef pulledConfig) {
return pulledConfig != null && StringUtils.hasText(pulledConfig.getRemoteConfigId());
}
private boolean hasRetryMetadata(ProdPullAckRecord failedRecord) {
return StringUtils.hasText(failedRecord.getSourceVersion())
&& StringUtils.hasText(failedRecord.getAirportId())
&& StringUtils.hasText(failedRecord.getAppName())
&& StringUtils.hasText(failedRecord.getFileName());
}
private int safeRetryCount(ProdPullAckRecord record) {
return record.getRetryCount() == null ? 0 : record.getRetryCount().intValue();
}
private LocalDateTime calculateNextRetryAt(int retryCount) {
long delaySeconds = RETRY_DELAY_BASE_SECONDS * (1L << Math.max(0, retryCount - 1));
return LocalDateTime.now().plusSeconds(delaySeconds);
}
private String buildRetryGroupKey(ProdPullAckRecord failedRecord) {
return failedRecord.getSourceVersion() + "|"
+ failedRecord.getAirportId() + "|"
+ failedRecord.getAppName() + "|"
+ failedRecord.getFileName();
}
private List<String> toRemoteIds(List<ProdPullAckRecord> records) {
List<String> ids = new ArrayList<String>();
for (ProdPullAckRecord record : records) {
ids.add(record.getRemoteConfigId());
}
return ids;
}
public static class PendingAckSummary {
private final List<String> ackSucIds;
private final List<String> ackFailIds;
public PendingAckSummary(List<String> ackSucIds, List<String> ackFailIds) {
this.ackSucIds = ackSucIds;
this.ackFailIds = ackFailIds;
}
public List<String> getAckSucIds() {
return ackSucIds;
}
public List<String> getAckFailIds() {
return ackFailIds;
}
public boolean hasPendingAck() {
return !(ackSucIds == null || ackSucIds.isEmpty()) || !(ackFailIds == null || ackFailIds.isEmpty());
}
}
public static class RetryPullRequest {
private final String sourceVersion;
private final String airportId;
private final String appName;
private final String fileName;
private final List<String> remoteConfigIds = new ArrayList<String>();
public RetryPullRequest(String sourceVersion, String airportId, String appName, String fileName) {
this.sourceVersion = sourceVersion;
this.airportId = airportId;
this.appName = appName;
this.fileName = fileName;
}
public String getSourceVersion() {
return sourceVersion;
}
public String getAirportId() {
return airportId;
}
public String getAppName() {
return appName;
}
public String getFileName() {
return fileName;
}
public List<String> getRemoteConfigIds() {
return remoteConfigIds;
}
private void addRemoteConfigId(String remoteConfigId) {
this.remoteConfigIds.add(remoteConfigId);
}
}
}