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/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java |   75 +++++++++++++++++++++++++++++++++++--
 1 files changed, 71 insertions(+), 4 deletions(-)

diff --git a/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java b/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java
index 2d538bb..9519218 100644
--- a/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java
+++ b/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java
@@ -15,6 +15,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.junit.jupiter.api.BeforeEach;
@@ -40,9 +42,13 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -67,6 +73,8 @@
     private BasDualCrnpService basDualCrnpService;
     @Mock
     private StationFlowCapacityService stationFlowCapacityService;
+    @Mock
+    private RedisUtil redisUtil;
     private RecordingTransactionManager transactionManager;
 
     @BeforeEach
@@ -81,6 +89,7 @@
         ReflectionTestUtils.setField(service, "basDualCrnpService", basDualCrnpService);
         ReflectionTestUtils.setField(service, "stationFlowCapacityService", stationFlowCapacityService);
         ReflectionTestUtils.setField(service, "transactionManager", transactionManager);
+        ReflectionTestUtils.setField(service, "redisUtil", redisUtil);
 
         AtomicLong jobId = new AtomicLong(100);
         when(aiAutoTuneJobService.save(any(AiAutoTuneJob.class))).thenAnswer(invocation -> {
@@ -96,6 +105,7 @@
         when(basStationService.update(any(Wrapper.class))).thenReturn(true);
         when(basCrnpService.update(any(Wrapper.class))).thenReturn(true);
         when(basDualCrnpService.update(any(Wrapper.class))).thenReturn(true);
+        when(redisUtil.trySetStringIfAbsent(anyString(), anyString(), anyLong())).thenReturn(true);
     }
 
     @Test
@@ -311,6 +321,61 @@
         assertEquals("failed", changes.get(0).getResultStatus());
         assertTrue(changes.get(0).getRejectReason().contains("db write failed"));
         assertEquals(1, transactionManager.getRollbackCount());
+        verify(redisUtil).compareAndDelete(eq(RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key), anyString());
+    }
+
+    @Test
+    void auditSaveBatchFailureRollsBackTargetWriteAndReturnsFailedAudit() {
+        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
+        when(aiAutoTuneChangeService.saveBatch(any(Collection.class)))
+                .thenThrow(new IllegalStateException("audit failed"))
+                .thenReturn(true);
+
+        AutoTuneApplyResult result = service.apply(request(false, command("sys_config", null, "conveyorStationTaskLimit", "15")));
+
+        List<AiAutoTuneChange> changes = savedChanges();
+        AiAutoTuneJob updatedJob = updatedJob();
+        assertFalse(result.getSuccess());
+        assertEquals("failed", updatedJob.getStatus());
+        assertEquals("failed", changes.get(0).getResultStatus());
+        assertTrue(changes.get(0).getRejectReason().contains("audit failed"));
+        assertEquals(1, transactionManager.getRollbackCount());
+        verify(configService).saveConfigValue("conveyorStationTaskLimit", "15");
+        verify(configService, never()).refreshSystemConfigCache();
+        verify(redisUtil).compareAndDelete(eq(RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key), anyString());
+    }
+
+    @Test
+    void jobUpdateFailureRollsBackTargetWriteTransaction() {
+        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
+        when(aiAutoTuneJobService.updateById(any(AiAutoTuneJob.class))).thenReturn(false);
+
+        IllegalStateException exception = assertThrows(IllegalStateException.class,
+                () -> service.apply(request(false, command("sys_config", null, "conveyorStationTaskLimit", "15"))));
+
+        assertTrue(exception.getMessage().contains("鏇存柊璋冨弬浠诲姟鐘舵�佸け璐�"));
+        assertEquals(2, transactionManager.getRollbackCount());
+        assertEquals(0, transactionManager.getCommitCount());
+        verify(configService).saveConfigValue("conveyorStationTaskLimit", "15");
+        verify(configService, never()).refreshSystemConfigCache();
+        verify(redisUtil).compareAndDelete(eq(RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key), anyString());
+    }
+
+    @Test
+    void realApplyLockNotAcquiredRejectsWithoutTargetWrite() {
+        when(redisUtil.trySetStringIfAbsent(anyString(), anyString(), anyLong())).thenReturn(false);
+
+        AutoTuneApplyResult result = service.apply(request(false, command("sys_config", null, "conveyorStationTaskLimit", "15")));
+
+        List<AiAutoTuneChange> changes = savedChanges();
+        AiAutoTuneJob updatedJob = updatedJob();
+        assertFalse(result.getSuccess());
+        assertEquals("failed", updatedJob.getStatus());
+        assertEquals("failed", changes.get(0).getResultStatus());
+        assertTrue(changes.get(0).getRejectReason().contains("姝e湪鎵ц"));
+        verify(configService, never()).getOne(any(Wrapper.class));
+        verify(configService, never()).saveConfigValue(any(), any());
+        verify(redisUtil, never()).compareAndDelete(anyString(), anyString());
     }
 
     @Test
@@ -445,14 +510,16 @@
 
     private List<AiAutoTuneChange> savedChanges() {
         ArgumentCaptor<Collection<AiAutoTuneChange>> captor = ArgumentCaptor.forClass(Collection.class);
-        verify(aiAutoTuneChangeService).saveBatch(captor.capture());
-        return new ArrayList<>(captor.getValue());
+        verify(aiAutoTuneChangeService, atLeastOnce()).saveBatch(captor.capture());
+        List<Collection<AiAutoTuneChange>> allValues = captor.getAllValues();
+        return new ArrayList<>(allValues.get(allValues.size() - 1));
     }
 
     private AiAutoTuneJob updatedJob() {
         ArgumentCaptor<AiAutoTuneJob> captor = ArgumentCaptor.forClass(AiAutoTuneJob.class);
-        verify(aiAutoTuneJobService).updateById(captor.capture());
-        return captor.getValue();
+        verify(aiAutoTuneJobService, atLeastOnce()).updateById(captor.capture());
+        List<AiAutoTuneJob> allValues = captor.getAllValues();
+        return allValues.get(allValues.size() - 1);
     }
 
     private static class RecordingTransactionManager implements PlatformTransactionManager {

--
Gitblit v1.9.1