From 8e8069362418d8db5179f7716d71d9fb3e4a0034 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期一, 27 四月 2026 13:05:42 +0800
Subject: [PATCH] fix: make auto tune apply writes auditable atomically

---
 src/main/java/com/zy/ai/service/impl/AutoTuneApplyServiceImpl.java |  201 +++++++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 181 insertions(+), 20 deletions(-)

diff --git a/src/main/java/com/zy/ai/service/impl/AutoTuneApplyServiceImpl.java b/src/main/java/com/zy/ai/service/impl/AutoTuneApplyServiceImpl.java
index e6a1e20..f443517 100644
--- a/src/main/java/com/zy/ai/service/impl/AutoTuneApplyServiceImpl.java
+++ b/src/main/java/com/zy/ai/service/impl/AutoTuneApplyServiceImpl.java
@@ -22,6 +22,8 @@
 import com.zy.asrs.service.BasDualCrnpService;
 import com.zy.asrs.service.BasStationService;
 import com.zy.asrs.service.StationFlowCapacityService;
+import com.zy.common.utils.RedisUtil;
+import com.zy.core.enums.RedisKeyType;
 import com.zy.system.entity.Config;
 import com.zy.system.service.ConfigService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -35,12 +37,15 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.UUID;
 
 @Service("autoTuneApplyService")
 public class AutoTuneApplyServiceImpl implements AutoTuneApplyService {
 
     private static final String PROMPT_SCENE_CODE = "auto_tune_apply";
     private static final String DIRECTION_OUT = "OUT";
+    private static final long APPLY_LOCK_SECONDS = 120L;
+    private static final String APPLY_LOCK_BUSY_REASON = "AI鑷姩璋冨弬姝e湪鎵ц锛岃绋嶅悗閲嶈瘯";
 
     @Autowired
     private AiAutoTuneJobService aiAutoTuneJobService;
@@ -58,6 +63,8 @@
     private StationFlowCapacityService stationFlowCapacityService;
     @Autowired
     private PlatformTransactionManager transactionManager;
+    @Autowired
+    private RedisUtil redisUtil;
 
     @Override
     public AutoTuneApplyResult apply(AutoTuneApplyRequest request) {
@@ -67,26 +74,114 @@
         AiAutoTuneJob job = createJob(safeRequest, dryRun, now);
         aiAutoTuneJobService.save(job);
 
-        List<ValidatedChange> validatedChanges = validateChanges(safeRequest, dryRun, now);
-        boolean hasRejectedChange = hasRejectedChange(validatedChanges);
-        if (!dryRun && hasRejectedChange) {
-            markAcceptedChangesAsBatchRejected(validatedChanges);
+        if (dryRun) {
+            return applyDryRun(safeRequest, job, now);
         }
-        if (!dryRun && !hasRejectedChange) {
-            try {
-                applyValidatedChangesInTransaction(validatedChanges);
-            } catch (RuntimeException exception) {
-                markWriteFailure(validatedChanges, exception);
-            }
+        return applyRealWithLock(safeRequest, job, now);
+    }
+
+    private AutoTuneApplyResult applyDryRun(AutoTuneApplyRequest request, AiAutoTuneJob job, Date now) {
+        List<ValidatedChange> validatedChanges = validateChanges(request, true, now);
+        ApplyPersistenceResult persistenceResult = persistApplyResultInTransaction(
+                job,
+                request,
+                validatedChanges,
+                true,
+                now,
+                false
+        );
+        return buildResult(job, persistenceResult.getAuditChanges(), true);
+    }
+
+    private AutoTuneApplyResult applyRealWithLock(AutoTuneApplyRequest request, AiAutoTuneJob job, Date now) {
+        if (request.getChanges() == null || request.getChanges().isEmpty()) {
+            ApplyPersistenceResult persistenceResult = persistApplyResultInTransaction(
+                    job,
+                    request,
+                    new ArrayList<>(),
+                    false,
+                    now,
+                    false
+            );
+            return buildResult(job, persistenceResult.getAuditChanges(), false);
         }
 
-        List<AiAutoTuneChange> auditChanges = buildAuditChanges(job.getId(), validatedChanges, now);
-        if (!auditChanges.isEmpty()) {
-            aiAutoTuneChangeService.saveBatch(auditChanges);
+        String lockKey = RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key;
+        String lockToken = UUID.randomUUID().toString();
+        if (!redisUtil.trySetStringIfAbsent(lockKey, lockToken, APPLY_LOCK_SECONDS)) {
+            return rejectRealApplyForBusyLock(request, job, now);
         }
-        finishJob(job, safeRequest, auditChanges, dryRun, now);
-        aiAutoTuneJobService.updateById(job);
-        return buildResult(job, auditChanges, dryRun);
+
+        try {
+            List<ValidatedChange> validatedChanges = validateChanges(request, false, now);
+            boolean hasRejectedChange = hasRejectedChange(validatedChanges);
+            if (hasRejectedChange) {
+                markAcceptedChangesAsBatchRejected(validatedChanges);
+            }
+            if (hasRejectedChange) {
+                ApplyPersistenceResult persistenceResult = persistApplyResultInTransaction(
+                        job,
+                        request,
+                        validatedChanges,
+                        false,
+                        now,
+                        false
+                );
+                return buildResult(job, persistenceResult.getAuditChanges(), false);
+            }
+
+            try {
+                ApplyPersistenceResult persistenceResult = persistApplyResultInTransaction(
+                        job,
+                        request,
+                        validatedChanges,
+                        false,
+                        now,
+                        true
+                );
+                refreshSystemConfigCacheIfNeeded(persistenceResult);
+                return buildResult(job, persistenceResult.getAuditChanges(), false);
+            } catch (RuntimeException exception) {
+                markWriteFailure(validatedChanges, exception);
+                ApplyPersistenceResult persistenceResult = persistApplyResultInTransaction(
+                        job,
+                        request,
+                        validatedChanges,
+                        false,
+                        now,
+                        false
+                );
+                return buildResult(job, persistenceResult.getAuditChanges(), false);
+            }
+        } finally {
+            redisUtil.compareAndDelete(lockKey, lockToken);
+        }
+    }
+
+    private AutoTuneApplyResult rejectRealApplyForBusyLock(AutoTuneApplyRequest request, AiAutoTuneJob job, Date now) {
+        List<ValidatedChange> validatedChanges = buildLockBusyChanges(request);
+        ApplyPersistenceResult persistenceResult = persistApplyResultInTransaction(
+                job,
+                request,
+                validatedChanges,
+                false,
+                now,
+                false
+        );
+        return buildResult(job, persistenceResult.getAuditChanges(), false);
+    }
+
+    private List<ValidatedChange> buildLockBusyChanges(AutoTuneApplyRequest request) {
+        List<ValidatedChange> validatedChanges = new ArrayList<>();
+        if (request.getChanges() == null || request.getChanges().isEmpty()) {
+            return validatedChanges;
+        }
+        for (AutoTuneChangeCommand command : request.getChanges()) {
+            ValidatedChange validatedChange = new ValidatedChange(command);
+            validatedChange.fail(APPLY_LOCK_BUSY_REASON);
+            validatedChanges.add(validatedChange);
+        }
+        return validatedChanges;
     }
 
     @Override
@@ -279,12 +374,41 @@
         return cooldownExpireTime;
     }
 
-    private void applyValidatedChangesInTransaction(List<ValidatedChange> validatedChanges) {
+    private ApplyPersistenceResult persistApplyResultInTransaction(AiAutoTuneJob job,
+                                                                   AutoTuneApplyRequest request,
+                                                                   List<ValidatedChange> validatedChanges,
+                                                                   boolean dryRun,
+                                                                   Date now,
+                                                                   boolean writeTargets) {
         TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
-        transactionTemplate.executeWithoutResult(status -> applyValidatedChanges(validatedChanges));
+        return transactionTemplate.execute(status -> persistApplyResult(
+                job,
+                request,
+                validatedChanges,
+                dryRun,
+                now,
+                writeTargets
+        ));
     }
 
-    private void applyValidatedChanges(List<ValidatedChange> validatedChanges) {
+    private ApplyPersistenceResult persistApplyResult(AiAutoTuneJob job,
+                                                     AutoTuneApplyRequest request,
+                                                     List<ValidatedChange> validatedChanges,
+                                                     boolean dryRun,
+                                                     Date now,
+                                                     boolean writeTargets) {
+        boolean refreshConfigCache = false;
+        if (writeTargets) {
+            refreshConfigCache = applyValidatedChanges(validatedChanges);
+        }
+        List<AiAutoTuneChange> auditChanges = buildAuditChanges(job.getId(), validatedChanges, now);
+        saveAuditChanges(auditChanges);
+        finishJob(job, request, auditChanges, dryRun, now);
+        updateJob(job);
+        return new ApplyPersistenceResult(auditChanges, refreshConfigCache);
+    }
+
+    private boolean applyValidatedChanges(List<ValidatedChange> validatedChanges) {
         boolean refreshConfigCache = false;
         for (ValidatedChange validatedChange : validatedChanges) {
             if (ChangeStatus.NO_CHANGE.equals(validatedChange.getStatus())) {
@@ -302,7 +426,26 @@
                 refreshConfigCache = true;
             }
         }
-        if (refreshConfigCache) {
+        return refreshConfigCache;
+    }
+
+    private void saveAuditChanges(List<AiAutoTuneChange> auditChanges) {
+        if (auditChanges.isEmpty()) {
+            return;
+        }
+        if (!aiAutoTuneChangeService.saveBatch(auditChanges)) {
+            throw new IllegalStateException("淇濆瓨璋冨弬瀹¤澶辫触");
+        }
+    }
+
+    private void updateJob(AiAutoTuneJob job) {
+        if (!aiAutoTuneJobService.updateById(job)) {
+            throw new IllegalStateException("鏇存柊璋冨弬浠诲姟鐘舵�佸け璐�");
+        }
+    }
+
+    private void refreshSystemConfigCacheIfNeeded(ApplyPersistenceResult persistenceResult) {
+        if (persistenceResult != null && persistenceResult.isRefreshConfigCache()) {
             configService.refreshSystemConfigCache();
         }
     }
@@ -759,6 +902,24 @@
         return null;
     }
 
+    private static class ApplyPersistenceResult {
+        private final List<AiAutoTuneChange> auditChanges;
+        private final boolean refreshConfigCache;
+
+        private ApplyPersistenceResult(List<AiAutoTuneChange> auditChanges, boolean refreshConfigCache) {
+            this.auditChanges = auditChanges == null ? new ArrayList<>() : auditChanges;
+            this.refreshConfigCache = refreshConfigCache;
+        }
+
+        public List<AiAutoTuneChange> getAuditChanges() {
+            return auditChanges;
+        }
+
+        public boolean isRefreshConfigCache() {
+            return refreshConfigCache;
+        }
+    }
+
     private static class ValidatedChange {
         private final AutoTuneChangeCommand command;
         private final String targetType;

--
Gitblit v1.9.1