Junjie
2026-04-27 f955390da5d6fe785bfb9828b44f1603cd4ff0b8
fix: harden auto tune apply audit and rollback
2个文件已修改
275 ■■■■ 已修改文件
src/main/java/com/zy/ai/service/impl/AutoTuneApplyServiceImpl.java 165 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/impl/AutoTuneApplyServiceImpl.java
@@ -26,12 +26,15 @@
import com.zy.system.service.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@Service("autoTuneApplyService")
public class AutoTuneApplyServiceImpl implements AutoTuneApplyService {
@@ -53,9 +56,10 @@
    private BasDualCrnpService basDualCrnpService;
    @Autowired
    private StationFlowCapacityService stationFlowCapacityService;
    @Autowired(required = false)
    private PlatformTransactionManager transactionManager;
    @Override
    @Transactional
    public AutoTuneApplyResult apply(AutoTuneApplyRequest request) {
        AutoTuneApplyRequest safeRequest = request == null ? new AutoTuneApplyRequest() : request;
        boolean dryRun = Boolean.TRUE.equals(safeRequest.getDryRun());
@@ -69,7 +73,11 @@
            markAcceptedChangesAsBatchRejected(validatedChanges);
        }
        if (!dryRun && !hasRejectedChange) {
            applyValidatedChanges(validatedChanges);
            try {
                applyValidatedChangesInTransaction(validatedChanges);
            } catch (RuntimeException exception) {
                markWriteFailure(validatedChanges, exception);
            }
        }
        List<AiAutoTuneChange> auditChanges = buildAuditChanges(job.getId(), validatedChanges, now);
@@ -82,7 +90,6 @@
    }
    @Override
    @Transactional
    public AutoTuneApplyResult rollbackLastSuccessfulJob(String reason) {
        Date now = new Date();
        AiAutoTuneJob rollbackJob = createRollbackJob(reason, now);
@@ -101,30 +108,10 @@
        }
        List<AiAutoTuneChange> rollbackChanges = new ArrayList<>();
        boolean refreshedConfigCache = false;
        for (AiAutoTuneChange sourceChange : sourceChanges) {
            AiAutoTuneChange rollbackChange = buildRollbackChange(rollbackJob.getId(), sourceChange, now);
            try {
                String currentValue = readCurrentValue(
                        sourceChange.getTargetType(),
                        sourceChange.getTargetId(),
                        sourceChange.getTargetKey()
                );
                rollbackChange.setOldValue(currentValue);
                writeValue(sourceChange.getTargetType(), sourceChange.getTargetId(), sourceChange.getTargetKey(), sourceChange.getOldValue());
                if (AutoTuneTargetType.SYS_CONFIG.getCode().equals(sourceChange.getTargetType())) {
                    refreshedConfigCache = true;
                }
                rollbackChange.setAppliedValue(sourceChange.getOldValue());
                rollbackChange.setResultStatus(ChangeStatus.SUCCESS.getCode());
            } catch (Exception exception) {
                rollbackChange.setResultStatus(ChangeStatus.FAILED.getCode());
                rollbackChange.setRejectReason(exception.getMessage());
            }
            rollbackChanges.add(rollbackChange);
        }
        if (refreshedConfigCache) {
            configService.refreshSystemConfigCache();
        try {
            rollbackChanges = rollbackChangesInTransaction(rollbackJob.getId(), sourceChanges, now);
        } catch (RuntimeException exception) {
            rollbackChanges = buildFailedRollbackChanges(rollbackJob.getId(), sourceChanges, exception, now);
        }
        aiAutoTuneChangeService.saveBatch(rollbackChanges);
        finishRollbackJob(rollbackJob, rollbackChanges, now);
@@ -287,6 +274,15 @@
        return cooldownExpireTime;
    }
    private void applyValidatedChangesInTransaction(List<ValidatedChange> validatedChanges) {
        if (transactionManager == null) {
            applyValidatedChanges(validatedChanges);
            return;
        }
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.executeWithoutResult(status -> applyValidatedChanges(validatedChanges));
    }
    private void applyValidatedChanges(List<ValidatedChange> validatedChanges) {
        boolean refreshConfigCache = false;
        for (ValidatedChange validatedChange : validatedChanges) {
@@ -304,6 +300,41 @@
        if (refreshConfigCache) {
            configService.refreshSystemConfigCache();
        }
    }
    private List<AiAutoTuneChange> rollbackChangesInTransaction(Long rollbackJobId,
                                                                List<AiAutoTuneChange> sourceChanges,
                                                                Date now) {
        if (transactionManager == null) {
            return rollbackChanges(rollbackJobId, sourceChanges, now);
        }
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        return transactionTemplate.execute(status -> rollbackChanges(rollbackJobId, sourceChanges, now));
    }
    private List<AiAutoTuneChange> rollbackChanges(Long rollbackJobId, List<AiAutoTuneChange> sourceChanges, Date now) {
        List<AiAutoTuneChange> rollbackChanges = new ArrayList<>();
        boolean refreshConfigCache = false;
        for (AiAutoTuneChange sourceChange : sourceChanges) {
            AiAutoTuneChange rollbackChange = buildRollbackChange(rollbackJobId, sourceChange, now);
            String currentValue = readCurrentValue(
                    sourceChange.getTargetType(),
                    sourceChange.getTargetId(),
                    sourceChange.getTargetKey()
            );
            rollbackChange.setOldValue(currentValue);
            writeValue(sourceChange.getTargetType(), sourceChange.getTargetId(), sourceChange.getTargetKey(), sourceChange.getOldValue());
            if (AutoTuneTargetType.SYS_CONFIG.getCode().equals(sourceChange.getTargetType())) {
                refreshConfigCache = true;
            }
            rollbackChange.setAppliedValue(sourceChange.getOldValue());
            rollbackChange.setResultStatus(ChangeStatus.SUCCESS.getCode());
            rollbackChanges.add(rollbackChange);
        }
        if (refreshConfigCache) {
            configService.refreshSystemConfigCache();
        }
        return rollbackChanges;
    }
    private void writeValue(String targetType, String targetId, String targetKey, String value) {
@@ -443,6 +474,9 @@
        if (auditChanges.isEmpty()) {
            return AutoTuneJobStatus.NO_CHANGE.getCode();
        }
        if (hasFailedChange(auditChanges)) {
            return AutoTuneJobStatus.FAILED.getCode();
        }
        int rejectedCount = countRejected(auditChanges);
        int acceptedCount = countAccepted(auditChanges);
        if (rejectedCount == auditChanges.size()) {
@@ -497,17 +531,26 @@
    }
    private List<AiAutoTuneChange> findLatestSuccessfulChanges() {
        List<AiAutoTuneJob> jobs = aiAutoTuneJobService.list(
                new QueryWrapper<AiAutoTuneJob>()
                        .eq("status", AutoTuneJobStatus.SUCCESS.getCode())
                        .ne("trigger_type", AutoTuneTriggerType.ROLLBACK.getCode())
                        .orderByDesc("finish_time")
                        .last("limit 20")
        List<AiAutoTuneChange> successfulChanges = aiAutoTuneChangeService.list(
                new QueryWrapper<AiAutoTuneChange>()
                        .eq("result_status", ChangeStatus.SUCCESS.getCode())
                        .orderByDesc("create_time")
                        .orderByDesc("id")
        );
        if (jobs == null || jobs.isEmpty()) {
        if (successfulChanges == null || successfulChanges.isEmpty()) {
            return new ArrayList<>();
        }
        for (AiAutoTuneJob job : jobs) {
        Set<Long> checkedJobIds = new HashSet<>();
        for (AiAutoTuneChange successfulChange : successfulChanges) {
            Long jobId = successfulChange.getJobId();
            if (jobId == null || checkedJobIds.contains(jobId)) {
                continue;
            }
            checkedJobIds.add(jobId);
            AiAutoTuneJob job = aiAutoTuneJobService.getById(jobId);
            if (!isRollbackCandidate(job)) {
                continue;
            }
            List<AiAutoTuneChange> changes = aiAutoTuneChangeService.list(
                    new QueryWrapper<AiAutoTuneChange>()
                            .eq("job_id", job.getId())
@@ -519,6 +562,16 @@
            }
        }
        return new ArrayList<>();
    }
    private boolean isRollbackCandidate(AiAutoTuneJob job) {
        if (job == null) {
            return false;
        }
        if (!AutoTuneJobStatus.SUCCESS.getCode().equals(job.getStatus())) {
            return false;
        }
        return !AutoTuneTriggerType.ROLLBACK.getCode().equals(job.getTriggerType());
    }
    private AutoTuneApplyResult buildResult(AiAutoTuneJob job, List<AiAutoTuneChange> changes, boolean dryRun) {
@@ -544,6 +597,31 @@
            }
            validatedChange.reject("同批次存在被拒绝变更,未执行写入");
        }
    }
    private void markWriteFailure(List<ValidatedChange> validatedChanges, RuntimeException exception) {
        String reason = exception.getMessage() == null ? "目标写入失败" : exception.getMessage();
        for (ValidatedChange validatedChange : validatedChanges) {
            if (ChangeStatus.NO_CHANGE.equals(validatedChange.getStatus())) {
                continue;
            }
            validatedChange.fail(reason);
        }
    }
    private List<AiAutoTuneChange> buildFailedRollbackChanges(Long rollbackJobId,
                                                              List<AiAutoTuneChange> sourceChanges,
                                                              RuntimeException exception,
                                                              Date now) {
        String reason = exception.getMessage() == null ? "回滚写入失败" : exception.getMessage();
        List<AiAutoTuneChange> rollbackChanges = new ArrayList<>();
        for (AiAutoTuneChange sourceChange : sourceChanges) {
            AiAutoTuneChange rollbackChange = buildRollbackChange(rollbackJobId, sourceChange, now);
            rollbackChange.setResultStatus(ChangeStatus.FAILED.getCode());
            rollbackChange.setRejectReason(reason);
            rollbackChanges.add(rollbackChange);
        }
        return rollbackChanges;
    }
    private boolean hasRejectedChange(List<ValidatedChange> validatedChanges) {
@@ -636,6 +714,15 @@
        return count;
    }
    private boolean hasFailedChange(List<AiAutoTuneChange> changes) {
        for (AiAutoTuneChange change : changes) {
            if (ChangeStatus.FAILED.getCode().equals(change.getResultStatus())) {
                return true;
            }
        }
        return false;
    }
    private String firstRejectReason(List<AiAutoTuneChange> changes) {
        for (AiAutoTuneChange change : changes) {
            if (change.getRejectReason() != null && !change.getRejectReason().trim().isEmpty()) {
@@ -667,6 +754,12 @@
            return this;
        }
        private void fail(String reason) {
            this.status = ChangeStatus.FAILED;
            this.rejectReason = reason;
            this.appliedValue = null;
        }
        private ValidatedChange accept(ChangeStatus status, String reason) {
            this.status = status;
            this.rejectReason = reason;
src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java
@@ -115,6 +115,16 @@
    }
    @Test
    void rejectNonnumericNewValue() {
        AutoTuneApplyResult result = service.apply(request(true, command("sys_config", null, "aiAutoTuneIntervalMinutes", "abc")));
        List<AiAutoTuneChange> changes = savedChanges();
        assertFalse(result.getSuccess());
        assertEquals("rejected", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("必须为整数"));
    }
    @Test
    void rejectOverStepConveyorLimitChange() {
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
@@ -123,6 +133,38 @@
        List<AiAutoTuneChange> changes = savedChanges();
        assertEquals("rejected", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("步长不能超过 5"));
    }
    @Test
    void rejectCrnOutBatchRunningLimitRangeAndStepCases() {
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("crnOutBatchRunningLimit", "10"));
        service.apply(request(true,
                command("sys_config", null, "crnOutBatchRunningLimit", "13"),
                command("sys_config", null, "crnOutBatchRunningLimit", "21")
        ));
        List<AiAutoTuneChange> changes = savedChanges();
        assertEquals("rejected", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("步长不能超过 2"));
        assertEquals("rejected", changes.get(1).getResultStatus());
        assertTrue(changes.get(1).getRejectReason().contains("1~20"));
    }
    @Test
    void rejectMaxInTaskRangeAndStepCases() {
        when(basCrnpService.getById(1)).thenReturn(crn(1, 1, 5));
        service.apply(request(true,
                command("crn", "1", "maxInTask", "7"),
                command("crn", "1", "maxInTask", "11")
        ));
        List<AiAutoTuneChange> changes = savedChanges();
        assertEquals("rejected", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("步长不能超过 1"));
        assertEquals("rejected", changes.get(1).getResultStatus());
        assertTrue(changes.get(1).getRejectReason().contains("0~10"));
    }
    @Test
@@ -150,6 +192,40 @@
        List<AiAutoTuneChange> changes = savedChanges();
        assertEquals("rejected", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("冷却期"));
    }
    @Test
    void realMixedValidAndInvalidBatchRejectsAndDoesNotWriteTargets() {
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
        when(basStationService.getById(101)).thenReturn(station(101, 1));
        when(stationFlowCapacityService.getOne(any(Wrapper.class))).thenReturn(capacity(101, "OUT", 2));
        AutoTuneApplyResult result = service.apply(request(false,
                command("sys_config", null, "conveyorStationTaskLimit", "15"),
                command("station", "101", "outTaskLimit", "3")
        ));
        List<AiAutoTuneChange> changes = savedChanges();
        assertFalse(result.getSuccess());
        assertEquals(2, changes.size());
        assertTrue(changes.stream().allMatch(change -> "rejected".equals(change.getResultStatus())));
        verify(configService, never()).saveConfigValue(any(), any());
        verify(basStationService, never()).update(any(Wrapper.class));
    }
    @Test
    void acceptedDryRunDoesNotWriteTargetStores() {
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
        AutoTuneApplyResult result = service.apply(request(true, command("sys_config", null, "conveyorStationTaskLimit", "15")));
        List<AiAutoTuneChange> changes = savedChanges();
        assertTrue(result.getSuccess());
        assertEquals("dry_run", changes.get(0).getResultStatus());
        verify(configService, never()).saveConfigValue(any(), any());
        verify(basStationService, never()).update(any(Wrapper.class));
        verify(basCrnpService, never()).update(any(Wrapper.class));
        verify(basDualCrnpService, never()).update(any(Wrapper.class));
    }
    @Test
@@ -181,14 +257,30 @@
    }
    @Test
    void rollbackLastJobSuccessfully() {
        AiAutoTuneJob latestJob = new AiAutoTuneJob();
        latestJob.setId(10L);
        when(aiAutoTuneJobService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(latestJob));
    void writeFailureReturnsFailedAuditWithoutThrowing() {
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
        when(configService.saveConfigValue("conveyorStationTaskLimit", "15")).thenThrow(new IllegalStateException("db write failed"));
        AutoTuneApplyResult result = service.apply(request(false, command("sys_config", null, "conveyorStationTaskLimit", "15")));
        List<AiAutoTuneChange> changes = savedChanges();
        assertFalse(result.getSuccess());
        assertEquals("failed", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("db write failed"));
    }
    @Test
    void rollbackLastJobSuccessfully() {
        AiAutoTuneJob rollbackJob = job(20L, "rollback", "success");
        AiAutoTuneJob latestRealJob = job(10L, "manual", "success");
        AiAutoTuneChange rollbackChange = successChange(20L, "sys_config", "", "conveyorStationTaskLimit", "15", "18");
        AiAutoTuneChange configChange = successChange(10L, "sys_config", "", "conveyorStationTaskLimit", "10", "15");
        AiAutoTuneChange stationChange = successChange(10L, "station", "101", "outTaskLimit", "1", "2");
        when(aiAutoTuneChangeService.list(any(Wrapper.class))).thenReturn(List.of(configChange, stationChange));
        when(aiAutoTuneChangeService.list(any(Wrapper.class)))
                .thenReturn(List.of(rollbackChange, configChange, stationChange))
                .thenReturn(List.of(configChange, stationChange));
        when(aiAutoTuneJobService.getById(10L)).thenReturn(latestRealJob);
        when(aiAutoTuneJobService.getById(20L)).thenReturn(rollbackJob);
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "15"));
        when(basStationService.getById(101)).thenReturn(station(101, 2));
@@ -277,6 +369,14 @@
        return change;
    }
    private AiAutoTuneJob job(Long id, String triggerType, String status) {
        AiAutoTuneJob job = new AiAutoTuneJob();
        job.setId(id);
        job.setTriggerType(triggerType);
        job.setStatus(status);
        return job;
    }
    private List<AiAutoTuneChange> savedChanges() {
        ArgumentCaptor<Collection<AiAutoTuneChange>> captor = ArgumentCaptor.forClass(Collection.class);
        verify(aiAutoTuneChangeService).saveBatch(captor.capture());