Junjie
2 天以前 63b01db83d9aad8a15276b4236a9a22e4aeef065
src/main/java/com/zy/ai/service/impl/AutoTuneApplyServiceImpl.java
@@ -5,6 +5,7 @@
import com.zy.ai.domain.autotune.AutoTuneApplyRequest;
import com.zy.ai.domain.autotune.AutoTuneApplyResult;
import com.zy.ai.domain.autotune.AutoTuneChangeCommand;
import com.zy.ai.domain.autotune.AutoTuneControlModeSnapshot;
import com.zy.ai.domain.autotune.AutoTuneJobStatus;
import com.zy.ai.domain.autotune.AutoTuneRuleDefinition;
import com.zy.ai.domain.autotune.AutoTuneTargetType;
@@ -14,33 +15,52 @@
import com.zy.ai.service.AiAutoTuneChangeService;
import com.zy.ai.service.AiAutoTuneJobService;
import com.zy.ai.service.AutoTuneApplyService;
import com.zy.ai.service.AutoTuneControlModeService;
import com.zy.asrs.entity.BasCrnp;
import com.zy.asrs.entity.BasDualCrnp;
import com.zy.asrs.entity.BasStation;
import com.zy.asrs.entity.StationFlowCapacity;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.BasCrnpService;
import com.zy.asrs.service.BasDualCrnpService;
import com.zy.asrs.service.BasStationService;
import com.zy.asrs.service.StationFlowCapacityService;
import com.zy.asrs.service.WrkMastService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.WrkStsType;
import com.zy.system.entity.Config;
import com.zy.system.service.ConfigService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
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 Logger LOGGER = LoggerFactory.getLogger(AutoTuneApplyServiceImpl.class);
    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 = "申请调参锁失败,锁不可用,可能已有任务或 Redis 异常";
    private static final String ANALYSIS_ONLY_REJECT_REASON = "仅分析模式禁止实际应用/回滚,未修改运行参数";
    private static final List<Long> FINAL_WRK_STS_LIST = Arrays.asList(
            WrkStsType.COMPLETE_INBOUND.sts,
            WrkStsType.SETTLE_INBOUND.sts,
            WrkStsType.COMPLETE_OUTBOUND.sts,
            WrkStsType.SETTLE_OUTBOUND.sts,
            WrkStsType.COMPLETE_LOC_MOVE.sts,
            WrkStsType.COMPLETE_CRN_MOVE.sts
    );
    @Autowired
    private AiAutoTuneJobService aiAutoTuneJobService;
@@ -49,74 +69,236 @@
    @Autowired
    private ConfigService configService;
    @Autowired
    private AutoTuneControlModeService autoTuneControlModeService;
    @Autowired
    private BasStationService basStationService;
    @Autowired
    private BasCrnpService basCrnpService;
    @Autowired
    private BasDualCrnpService basDualCrnpService;
    @Autowired
    private StationFlowCapacityService stationFlowCapacityService;
    @Autowired(required = false)
    private WrkMastService wrkMastService;
    @Autowired
    private PlatformTransactionManager transactionManager;
    @Autowired
    private RedisUtil redisUtil;
    @Override
    public AutoTuneApplyResult apply(AutoTuneApplyRequest request) {
        AutoTuneApplyRequest safeRequest = request == null ? new AutoTuneApplyRequest() : request;
        AutoTuneControlModeSnapshot controlMode = currentControlModeSnapshot();
        boolean dryRun = Boolean.TRUE.equals(safeRequest.getDryRun());
        Date now = new Date();
        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, controlMode);
        }
        if (!dryRun && !hasRejectedChange) {
        if (isAnalysisOnly(controlMode)) {
            return rejectRealApplyForAnalysisOnly(safeRequest, job, now, controlMode);
        }
        return applyRealWithLock(safeRequest, job, now, controlMode);
    }
    private AutoTuneApplyResult applyDryRun(AutoTuneApplyRequest request,
                                            AiAutoTuneJob job,
                                            Date now,
                                            AutoTuneControlModeSnapshot controlMode) {
        List<ValidatedChange> validatedChanges = validateChanges(request, true, now);
        ApplyPersistenceResult persistenceResult = persistApplyResultInTransaction(
                job,
                request,
                validatedChanges,
                true,
                now,
                false
        );
        return buildResult(job, persistenceResult.getAuditChanges(), true, controlMode);
    }
    private AutoTuneApplyResult applyRealWithLock(AutoTuneApplyRequest request,
                                                  AiAutoTuneJob job,
                                                  Date now,
                                                  AutoTuneControlModeSnapshot controlMode) {
        if (request.getChanges() == null || request.getChanges().isEmpty()) {
            ApplyPersistenceResult persistenceResult = persistApplyResultInTransaction(
                    job,
                    request,
                    new ArrayList<>(),
                    false,
                    now,
                    false
            );
            return buildResult(job, persistenceResult.getAuditChanges(), false, controlMode);
        }
        String lockKey = RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key;
        String lockToken = UUID.randomUUID().toString();
        if (!redisUtil.trySetStringIfAbsent(lockKey, lockToken, APPLY_LOCK_SECONDS)) {
            return rejectRealApplyForUnavailableLock(request, job, now, lockKey, controlMode);
        }
        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, controlMode);
            }
            try {
                applyValidatedChangesInTransaction(validatedChanges);
                ApplyPersistenceResult persistenceResult = persistApplyResultInTransaction(
                        job,
                        request,
                        validatedChanges,
                        false,
                        now,
                        true
                );
                refreshSystemConfigCacheSafely(persistenceResult);
                return buildResult(job, persistenceResult.getAuditChanges(), false, controlMode);
            } catch (RuntimeException exception) {
                markWriteFailure(validatedChanges, exception);
                Date failureNow = new Date();
                AiAutoTuneJob failureJob = createJob(request, false, failureNow);
                ApplyPersistenceResult persistenceResult = persistApplyResultInTransaction(
                        failureJob,
                        request,
                        validatedChanges,
                        false,
                        failureNow,
                        false
                );
                return buildResult(failureJob, persistenceResult.getAuditChanges(), false, controlMode);
            }
        } finally {
            redisUtil.compareAndDelete(lockKey, lockToken);
        }
    }
        List<AiAutoTuneChange> auditChanges = buildAuditChanges(job.getId(), validatedChanges, now);
        if (!auditChanges.isEmpty()) {
            aiAutoTuneChangeService.saveBatch(auditChanges);
    private AutoTuneApplyResult rejectRealApplyForUnavailableLock(AutoTuneApplyRequest request,
                                                                  AiAutoTuneJob job,
                                                                  Date now,
                                                                  String lockKey,
                                                                  AutoTuneControlModeSnapshot controlMode) {
        boolean lockKeyExists = redisUtil.hasKey(lockKey);
        LOGGER.warn("申请AI自动调参 apply 锁失败,lockKey={}, lockKeyExists={}", lockKey, lockKeyExists);
        List<ValidatedChange> validatedChanges = buildLockBusyChanges(request);
        ApplyPersistenceResult persistenceResult = persistApplyResultInTransaction(
                job,
                request,
                validatedChanges,
                false,
                now,
                false
        );
        return buildResult(job, persistenceResult.getAuditChanges(), false, controlMode);
    }
    private AutoTuneApplyResult rejectRealApplyForAnalysisOnly(AutoTuneApplyRequest request,
                                                               AiAutoTuneJob job,
                                                               Date now,
                                                               AutoTuneControlModeSnapshot controlMode) {
        ApplyPersistenceResult persistenceResult = persistAnalysisOnlyApplyRejectionInTransaction(job, request, now);
        AutoTuneApplyResult result = buildResult(job, persistenceResult.getAuditChanges(), false, controlMode);
        result.setAnalysisOnly(true);
        result.setNoApply(true);
        return result;
    }
    private List<ValidatedChange> buildLockBusyChanges(AutoTuneApplyRequest request) {
        List<ValidatedChange> validatedChanges = new ArrayList<>();
        if (request.getChanges() == null || request.getChanges().isEmpty()) {
            return validatedChanges;
        }
        finishJob(job, safeRequest, auditChanges, dryRun, now);
        aiAutoTuneJobService.updateById(job);
        return buildResult(job, auditChanges, dryRun);
        for (AutoTuneChangeCommand command : request.getChanges()) {
            ValidatedChange validatedChange = new ValidatedChange(command);
            validatedChange.fail(APPLY_LOCK_BUSY_REASON);
            validatedChanges.add(validatedChange);
        }
        return validatedChanges;
    }
    @Override
    public AutoTuneApplyResult rollbackLastSuccessfulJob(String reason) {
        AutoTuneControlModeSnapshot controlMode = currentControlModeSnapshot();
        Date now = new Date();
        AiAutoTuneJob rollbackJob = createRollbackJob(reason, now);
        aiAutoTuneJobService.save(rollbackJob);
        List<AiAutoTuneChange> sourceChanges = findLatestSuccessfulChanges();
        if (sourceChanges.isEmpty()) {
            rollbackJob.setStatus(AutoTuneJobStatus.REJECTED.getCode());
            rollbackJob.setFinishTime(now);
            rollbackJob.setRejectCount(0);
            rollbackJob.setSuccessCount(0);
            rollbackJob.setSummary("未找到可回滚的成功调参记录");
            rollbackJob.setErrorMessage("未找到可回滚的成功调参记录");
            aiAutoTuneJobService.updateById(rollbackJob);
            return buildResult(rollbackJob, new ArrayList<>(), false);
        if (isAnalysisOnly(controlMode)) {
            return rejectRollbackForAnalysisOnly(rollbackJob, now, controlMode);
        }
        List<AiAutoTuneChange> rollbackChanges = new ArrayList<>();
        String lockKey = RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key;
        String lockToken = UUID.randomUUID().toString();
        if (!redisUtil.trySetStringIfAbsent(lockKey, lockToken, APPLY_LOCK_SECONDS)) {
            return rejectRollbackForUnavailableLock(reason, now, lockKey, controlMode);
        }
        try {
            rollbackChanges = rollbackChangesInTransaction(rollbackJob.getId(), sourceChanges, now);
        } catch (RuntimeException exception) {
            rollbackChanges = buildFailedRollbackChanges(rollbackJob.getId(), sourceChanges, exception, now);
            List<AiAutoTuneChange> sourceChanges = findLatestSuccessfulChanges();
            if (sourceChanges.isEmpty()) {
                persistNoRollbackSourceJobInTransaction(rollbackJob, now);
                return buildResult(rollbackJob, new ArrayList<>(), false, controlMode);
            }
            try {
                RollbackPersistenceResult persistenceResult = persistRollbackResultInTransaction(
                        rollbackJob,
                        sourceChanges,
                        now
                );
                refreshRollbackConfigCacheSafely(persistenceResult);
                return buildResult(rollbackJob, persistenceResult.getRollbackChanges(), false, controlMode);
            } catch (RuntimeException exception) {
                Date failureNow = new Date();
                AiAutoTuneJob failureJob = createRollbackJob(reason, failureNow);
                RollbackPersistenceResult persistenceResult = persistFailedRollbackResultInTransaction(
                        failureJob,
                        sourceChanges,
                        exception,
                        failureNow
                );
                return buildResult(failureJob, persistenceResult.getRollbackChanges(), false, controlMode);
            }
        } finally {
            redisUtil.compareAndDelete(lockKey, lockToken);
        }
        aiAutoTuneChangeService.saveBatch(rollbackChanges);
        finishRollbackJob(rollbackJob, rollbackChanges, now);
        aiAutoTuneJobService.updateById(rollbackJob);
        return buildResult(rollbackJob, rollbackChanges, false);
    }
    private AutoTuneApplyResult rejectRollbackForUnavailableLock(String reason,
                                                                 Date now,
                                                                 String lockKey,
                                                                 AutoTuneControlModeSnapshot controlMode) {
        boolean lockKeyExists = redisUtil.hasKey(lockKey);
        LOGGER.warn("申请AI自动调参 rollback 锁失败,lockKey={}, lockKeyExists={}", lockKey, lockKeyExists);
        AiAutoTuneJob rollbackJob = createRollbackJob(reason, now);
        RollbackPersistenceResult persistenceResult = persistRollbackLockFailureInTransaction(
                rollbackJob,
                now
        );
        return buildResult(rollbackJob, persistenceResult.getRollbackChanges(), false, controlMode);
    }
    private AutoTuneApplyResult rejectRollbackForAnalysisOnly(AiAutoTuneJob rollbackJob,
                                                              Date now,
                                                              AutoTuneControlModeSnapshot controlMode) {
        RollbackPersistenceResult persistenceResult = persistAnalysisOnlyRollbackRejectionInTransaction(
                rollbackJob,
                now
        );
        AutoTuneApplyResult result = buildResult(rollbackJob, persistenceResult.getRollbackChanges(), false, controlMode);
        result.setAnalysisOnly(true);
        result.setNoApply(true);
        return result;
    }
    private List<ValidatedChange> validateChanges(AutoTuneApplyRequest request, boolean dryRun, Date now) {
@@ -132,31 +314,40 @@
    private ValidatedChange validateChange(AutoTuneChangeCommand command, boolean dryRun, Date now) {
        ValidatedChange validatedChange = new ValidatedChange(command);
        AutoTuneRuleDefinition.Rule rule = AutoTuneRuleDefinition.findRule(command.getTargetType(), command.getTargetKey());
        AutoTuneRuleDefinition.Rule rule = AutoTuneRuleDefinition.findRule(
                validatedChange.getTargetType(),
                validatedChange.getTargetKey()
        );
        if (rule == null) {
            return validatedChange.reject("不支持的调参目标: " + command.getTargetType() + "/" + command.getTargetKey());
            return validatedChange.reject("不支持的调参目标: "
                    + validatedChange.getTargetType() + "/" + validatedChange.getTargetKey());
        }
        validatedChange.setRule(rule);
        Integer requestedValue = parseRequestedInt(command.getNewValue());
        Integer requestedValue = parseRequestedInt(validatedChange.getRawRequestedValue());
        if (requestedValue == null) {
            return validatedChange.reject(command.getTargetKey() + " 必须为整数");
            return validatedChange.reject(validatedChange.getTargetKey() + " 必须为整数");
        }
        validatedChange.setRequestedIntValue(requestedValue);
        validatedChange.setRequestedValue(String.valueOf(requestedValue));
        CurrentValue currentValue = readCurrentValueForValidation(command, rule);
        CurrentValue currentValue = readCurrentValueForValidation(validatedChange, rule);
        if (currentValue.getRejectReason() != null) {
            return validatedChange.reject(currentValue.getRejectReason());
        }
        validatedChange.setOldValue(currentValue.getOldValue());
        Integer maxValue = resolveMaxValue(command, rule, requestedValue);
        if (maxValue == null) {
            return validatedChange.reject("站点 " + command.getTargetId() + " 缺少 OUT 方向 bufferCapacity,无法证明 outTaskLimit 上限");
        Integer maxValue = resolveMaxValue(validatedChange, rule, requestedValue);
        if (requestedValue < rule.getMinValue()) {
            if (maxValue != null) {
                return validatedChange.reject(validatedChange.getTargetKey() + " 必须在 "
                        + rule.getMinValue() + "~" + maxValue + " 范围内");
            }
            return validatedChange.reject(validatedChange.getTargetKey() + " 必须不小于 " + rule.getMinValue());
        }
        if (requestedValue < rule.getMinValue() || requestedValue > maxValue) {
            return validatedChange.reject(command.getTargetKey() + " 必须在 " + rule.getMinValue() + "~" + maxValue + " 范围内");
        if (maxValue != null && requestedValue > maxValue) {
            return validatedChange.reject(validatedChange.getTargetKey() + " 必须在 "
                    + rule.getMinValue() + "~" + maxValue + " 范围内");
        }
        if (Objects.equals(currentValue.getNumericValue(), requestedValue)) {
@@ -164,10 +355,10 @@
        }
        int step = Math.abs(requestedValue - currentValue.getNumericValue());
        if (step > rule.getMaxStep()) {
            return validatedChange.reject(command.getTargetKey() + " 单次调整步长不能超过 " + rule.getMaxStep());
            return validatedChange.reject(validatedChange.getTargetKey() + " 单次调整步长不能超过 " + rule.getMaxStep());
        }
        Date cooldownExpireTime = findCooldownExpireTime(command, now);
        Date cooldownExpireTime = findCooldownExpireTime(validatedChange, now);
        if (cooldownExpireTime != null) {
            validatedChange.setCooldownExpireTime(cooldownExpireTime);
            return validatedChange.reject("目标仍在冷却期,冷却截止时间: " + cooldownExpireTime);
@@ -178,86 +369,81 @@
        return validatedChange.accept(dryRun ? ChangeStatus.DRY_RUN : ChangeStatus.PENDING, null);
    }
    private CurrentValue readCurrentValueForValidation(AutoTuneChangeCommand command, AutoTuneRuleDefinition.Rule rule) {
    private CurrentValue readCurrentValueForValidation(ValidatedChange validatedChange, AutoTuneRuleDefinition.Rule rule) {
        AutoTuneTargetType targetType = rule.getTargetType();
        Integer targetId = parseTargetId(command.getTargetId(), targetType);
        Integer targetId = parseTargetId(validatedChange.getTargetId(), targetType);
        if (!AutoTuneTargetType.SYS_CONFIG.equals(targetType) && targetId == null) {
            return CurrentValue.rejected("targetId 必须为整数");
        }
        if (AutoTuneTargetType.SYS_CONFIG.equals(targetType)) {
            Config config = configService.getOne(new QueryWrapper<Config>().eq("code", command.getTargetKey()).last("limit 1"));
            Config config = configService.getOne(new QueryWrapper<Config>().eq("code", validatedChange.getTargetKey()).last("limit 1"));
            if (config == null) {
                return CurrentValue.rejected("运行参数不存在: " + command.getTargetKey());
                return CurrentValue.rejected("运行参数不存在: " + validatedChange.getTargetKey());
            }
            return numericCurrentValue(config.getValue(), false, command.getTargetKey());
            return numericCurrentValue(config.getValue(), validatedChange.getTargetKey());
        }
        if (AutoTuneTargetType.STATION.equals(targetType)) {
            BasStation station = basStationService.getById(targetId);
            if (station == null) {
                return CurrentValue.rejected("站点不存在: " + command.getTargetId());
                return CurrentValue.rejected("站点不存在: " + validatedChange.getTargetId());
            }
            return numericCurrentValue(toText(station.getOutTaskLimit()), true, command.getTargetKey());
            return stationOutTaskLimitCurrentValue(station.getOutTaskLimit(), validatedChange.getTargetKey());
        }
        if (AutoTuneTargetType.CRN.equals(targetType)) {
            BasCrnp crnp = basCrnpService.getById(targetId);
            if (crnp == null) {
                return CurrentValue.rejected("堆垛机不存在: " + command.getTargetId());
                return CurrentValue.rejected("堆垛机不存在: " + validatedChange.getTargetId());
            }
            Integer value = "maxOutTask".equals(command.getTargetKey()) ? crnp.getMaxOutTask() : crnp.getMaxInTask();
            return numericCurrentValue(toText(value), false, command.getTargetKey());
            Integer value = "maxOutTask".equals(validatedChange.getTargetKey()) ? crnp.getMaxOutTask() : crnp.getMaxInTask();
            return numericCurrentValue(toText(value), validatedChange.getTargetKey());
        }
        BasDualCrnp dualCrnp = basDualCrnpService.getById(targetId);
        if (dualCrnp == null) {
            return CurrentValue.rejected("双工位堆垛机不存在: " + command.getTargetId());
            return CurrentValue.rejected("双工位堆垛机不存在: " + validatedChange.getTargetId());
        }
        Integer value = "maxOutTask".equals(command.getTargetKey()) ? dualCrnp.getMaxOutTask() : dualCrnp.getMaxInTask();
        return numericCurrentValue(toText(value), false, command.getTargetKey());
        Integer value = "maxOutTask".equals(validatedChange.getTargetKey()) ? dualCrnp.getMaxOutTask() : dualCrnp.getMaxInTask();
        return numericCurrentValue(toText(value), validatedChange.getTargetKey());
    }
    private CurrentValue numericCurrentValue(String oldValue, boolean nullOrNegativeAsZero, String targetKey) {
    private CurrentValue stationOutTaskLimitCurrentValue(Integer outTaskLimit, String targetKey) {
        if (outTaskLimit == null || outTaskLimit < 0) {
            return CurrentValue.rejected(targetKey + " 当前为不限制,需要人工先初始化为有限值后才能自动调参");
        }
        return numericCurrentValue(toText(outTaskLimit), targetKey);
    }
    private CurrentValue numericCurrentValue(String oldValue, String targetKey) {
        if (oldValue == null || oldValue.trim().isEmpty()) {
            if (nullOrNegativeAsZero) {
                return CurrentValue.accepted(null, 0);
            }
            return CurrentValue.rejected(targetKey + " 当前值为空,无法计算步长");
        }
        try {
            Integer parsedValue = Integer.valueOf(oldValue.trim());
            if (nullOrNegativeAsZero && parsedValue < 0) {
                return CurrentValue.accepted(oldValue, 0);
            }
            return CurrentValue.accepted(oldValue, parsedValue);
        } catch (Exception exception) {
            return CurrentValue.rejected(targetKey + " 当前值不是整数,无法计算步长");
        }
    }
    private Integer resolveMaxValue(AutoTuneChangeCommand command,
    private Integer resolveMaxValue(ValidatedChange validatedChange,
                                    AutoTuneRuleDefinition.Rule rule,
                                    Integer requestedValue) {
        if (!rule.isDynamicMaxValue()) {
            return rule.getMaxValue();
        }
        Integer targetId = parseTargetId(command.getTargetId(), rule.getTargetType());
        StationFlowCapacity capacity = stationFlowCapacityService.getOne(
                new QueryWrapper<StationFlowCapacity>()
                        .eq("station_id", targetId)
                        .eq("direction_code", DIRECTION_OUT)
                        .last("limit 1")
        );
        if (capacity == null || capacity.getBufferCapacity() == null) {
        Integer targetId = parseTargetId(validatedChange.getTargetId(), rule.getTargetType());
        BasStation station = basStationService.getById(targetId);
        if (station == null || station.getOutBufferCapacity() == null) {
            return requestedValue == 0 ? 0 : null;
        }
        return Math.max(0, capacity.getBufferCapacity());
        return Math.max(0, station.getOutBufferCapacity());
    }
    private Date findCooldownExpireTime(AutoTuneChangeCommand command, Date now) {
        String targetId = normalizeTargetId(command.getTargetType(), command.getTargetId());
    private Date findCooldownExpireTime(ValidatedChange validatedChange, Date now) {
        List<AiAutoTuneChange> recentChanges = aiAutoTuneChangeService.list(
                new QueryWrapper<AiAutoTuneChange>()
                        .eq("target_type", command.getTargetType())
                        .eq("target_id", targetId)
                        .eq("target_key", command.getTargetKey())
                        .eq("target_type", validatedChange.getTargetType())
                        .eq("target_id", validatedChange.getTargetId())
                        .eq("target_key", validatedChange.getTargetKey())
                        .eq("result_status", ChangeStatus.SUCCESS.getCode())
                        .gt("cooldown_expire_time", now)
                        .orderByDesc("create_time")
@@ -274,45 +460,177 @@
        return cooldownExpireTime;
    }
    private void applyValidatedChangesInTransaction(List<ValidatedChange> validatedChanges) {
        if (transactionManager == null) {
            applyValidatedChanges(validatedChanges);
            return;
        }
    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) {
        saveJob(job);
        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 void saveJob(AiAutoTuneJob job) {
        if (!aiAutoTuneJobService.save(job)) {
            throw new IllegalStateException("保存调参任务失败");
        }
    }
    private boolean applyValidatedChanges(List<ValidatedChange> validatedChanges) {
        boolean refreshConfigCache = false;
        for (ValidatedChange validatedChange : validatedChanges) {
            if (ChangeStatus.NO_CHANGE.equals(validatedChange.getStatus())) {
                continue;
            }
            AutoTuneChangeCommand command = validatedChange.getCommand();
            writeValue(command.getTargetType(), command.getTargetId(), command.getTargetKey(), validatedChange.getRequestedValue());
            writeValue(
                    validatedChange.getTargetType(),
                    validatedChange.getTargetId(),
                    validatedChange.getTargetKey(),
                    validatedChange.getRequestedValue()
            );
            validatedChange.accept(ChangeStatus.SUCCESS, null);
            validatedChange.setAppliedValue(validatedChange.getRequestedValue());
            if (AutoTuneTargetType.SYS_CONFIG.getCode().equals(command.getTargetType())) {
            if (AutoTuneTargetType.SYS_CONFIG.getCode().equals(validatedChange.getTargetType())) {
                refreshConfigCache = true;
            }
        }
        if (refreshConfigCache) {
            configService.refreshSystemConfigCache();
        return refreshConfigCache;
    }
    private void saveAuditChanges(List<AiAutoTuneChange> auditChanges) {
        if (auditChanges.isEmpty()) {
            return;
        }
        if (!aiAutoTuneChangeService.saveBatch(auditChanges)) {
            throw new IllegalStateException("保存调参审计失败");
        }
    }
    private List<AiAutoTuneChange> rollbackChangesInTransaction(Long rollbackJobId,
                                                                List<AiAutoTuneChange> sourceChanges,
                                                                Date now) {
        if (transactionManager == null) {
            return rollbackChanges(rollbackJobId, sourceChanges, now);
    private void updateJob(AiAutoTuneJob job) {
        if (!aiAutoTuneJobService.updateById(job)) {
            throw new IllegalStateException("更新调参任务状态失败");
        }
    }
    private void refreshSystemConfigCacheSafely(ApplyPersistenceResult persistenceResult) {
        if (persistenceResult != null && persistenceResult.isRefreshConfigCache()) {
            refreshSystemConfigCacheSafely("apply");
        }
    }
    private void persistNoRollbackSourceJobInTransaction(AiAutoTuneJob rollbackJob, Date now) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        return transactionTemplate.execute(status -> rollbackChanges(rollbackJobId, sourceChanges, now));
        transactionTemplate.executeWithoutResult(status -> {
            saveJob(rollbackJob);
            rollbackJob.setStatus(AutoTuneJobStatus.REJECTED.getCode());
            rollbackJob.setFinishTime(now);
            rollbackJob.setRejectCount(0);
            rollbackJob.setSuccessCount(0);
            rollbackJob.setSummary("未找到可回滚的成功调参记录");
            rollbackJob.setErrorMessage("未找到可回滚的成功调参记录");
            updateJob(rollbackJob);
        });
    }
    private List<AiAutoTuneChange> rollbackChanges(Long rollbackJobId, List<AiAutoTuneChange> sourceChanges, Date now) {
    private RollbackPersistenceResult persistRollbackResultInTransaction(AiAutoTuneJob rollbackJob,
                                                                        List<AiAutoTuneChange> sourceChanges,
                                                                        Date now) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        return transactionTemplate.execute(status -> {
            saveJob(rollbackJob);
            RollbackPersistenceResult rollbackResult = rollbackChanges(rollbackJob.getId(), sourceChanges, now);
            saveAuditChanges(rollbackResult.getRollbackChanges());
            finishRollbackJob(rollbackJob, rollbackResult.getRollbackChanges(), now);
            updateJob(rollbackJob);
            return rollbackResult;
        });
    }
    private RollbackPersistenceResult persistFailedRollbackResultInTransaction(AiAutoTuneJob rollbackJob,
                                                                              List<AiAutoTuneChange> sourceChanges,
                                                                              RuntimeException exception,
                                                                              Date now) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        return transactionTemplate.execute(status -> {
            saveJob(rollbackJob);
            List<AiAutoTuneChange> rollbackChanges = buildFailedRollbackChanges(
                    rollbackJob.getId(),
                    sourceChanges,
                    exception,
                    now
            );
            saveAuditChanges(rollbackChanges);
            finishRollbackJob(rollbackJob, rollbackChanges, now);
            updateJob(rollbackJob);
            return new RollbackPersistenceResult(rollbackChanges, false);
        });
    }
    private RollbackPersistenceResult persistRollbackLockFailureInTransaction(AiAutoTuneJob rollbackJob, Date now) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        return transactionTemplate.execute(status -> {
            saveJob(rollbackJob);
            List<AiAutoTuneChange> rollbackChanges = buildRollbackLockFailureChanges(rollbackJob.getId(), now);
            saveAuditChanges(rollbackChanges);
            finishRollbackJob(rollbackJob, rollbackChanges, now);
            updateJob(rollbackJob);
            return new RollbackPersistenceResult(rollbackChanges, false);
        });
    }
    private ApplyPersistenceResult persistAnalysisOnlyApplyRejectionInTransaction(AiAutoTuneJob job,
                                                                                  AutoTuneApplyRequest request,
                                                                                  Date now) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        return transactionTemplate.execute(status -> {
            saveJob(job);
            List<AiAutoTuneChange> auditChanges = buildAnalysisOnlyApplyChanges(job.getId(), request, now);
            saveAuditChanges(auditChanges);
            finishAnalysisOnlyRejectedJob(job, auditChanges, now);
            updateJob(job);
            return new ApplyPersistenceResult(auditChanges, false);
        });
    }
    private RollbackPersistenceResult persistAnalysisOnlyRollbackRejectionInTransaction(AiAutoTuneJob rollbackJob,
                                                                                       Date now) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        return transactionTemplate.execute(status -> {
            saveJob(rollbackJob);
            List<AiAutoTuneChange> rollbackChanges = buildAnalysisOnlyRollbackChanges(rollbackJob.getId(), now);
            saveAuditChanges(rollbackChanges);
            finishAnalysisOnlyRejectedJob(rollbackJob, rollbackChanges, now);
            updateJob(rollbackJob);
            return new RollbackPersistenceResult(rollbackChanges, false);
        });
    }
    private RollbackPersistenceResult rollbackChanges(Long rollbackJobId, List<AiAutoTuneChange> sourceChanges, Date now) {
        List<AiAutoTuneChange> rollbackChanges = new ArrayList<>();
        boolean refreshConfigCache = false;
        for (AiAutoTuneChange sourceChange : sourceChanges) {
@@ -331,55 +649,142 @@
            rollbackChange.setResultStatus(ChangeStatus.SUCCESS.getCode());
            rollbackChanges.add(rollbackChange);
        }
        if (refreshConfigCache) {
            configService.refreshSystemConfigCache();
        return new RollbackPersistenceResult(rollbackChanges, refreshConfigCache);
    }
    private List<AiAutoTuneChange> buildRollbackLockFailureChanges(Long rollbackJobId, Date now) {
        AiAutoTuneChange rollbackChange = new AiAutoTuneChange();
        rollbackChange.setJobId(rollbackJobId);
        rollbackChange.setTargetType(AutoTuneTriggerType.ROLLBACK.getCode());
        rollbackChange.setTargetId("");
        rollbackChange.setTargetKey("latest_successful_job");
        rollbackChange.setResultStatus(ChangeStatus.FAILED.getCode());
        rollbackChange.setRejectReason(APPLY_LOCK_BUSY_REASON);
        rollbackChange.setCreateTime(now);
        return List.of(rollbackChange);
    }
    private List<AiAutoTuneChange> buildAnalysisOnlyApplyChanges(Long jobId,
                                                                 AutoTuneApplyRequest request,
                                                                 Date now) {
        List<AiAutoTuneChange> changes = new ArrayList<>();
        List<AutoTuneChangeCommand> commands = request == null ? null : request.getChanges();
        if (commands == null || commands.isEmpty()) {
            changes.add(buildAnalysisOnlyChange(jobId, "analysis_only", "", "apply", null, now));
            return changes;
        }
        return rollbackChanges;
        for (AutoTuneChangeCommand command : commands) {
            changes.add(buildAnalysisOnlyChange(
                    jobId,
                    normalizeText(command == null ? null : command.getTargetType()),
                    normalizeText(command == null ? null : command.getTargetId()),
                    normalizeText(command == null ? null : command.getTargetKey()),
                    command == null ? null : command.getNewValue(),
                    now
            ));
        }
        return changes;
    }
    private List<AiAutoTuneChange> buildAnalysisOnlyRollbackChanges(Long jobId, Date now) {
        AiAutoTuneChange rollbackChange = buildAnalysisOnlyChange(
                jobId,
                "analysis_only",
                "",
                "rollback",
                null,
                now
        );
        return List.of(rollbackChange);
    }
    private AiAutoTuneChange buildAnalysisOnlyChange(Long jobId,
                                                     String targetType,
                                                     String targetId,
                                                     String targetKey,
                                                     String requestedValue,
                                                     Date now) {
        AiAutoTuneChange change = new AiAutoTuneChange();
        change.setJobId(jobId);
        change.setTargetType(targetType);
        change.setTargetId(targetId);
        change.setTargetKey(targetKey);
        change.setRequestedValue(requestedValue);
        change.setResultStatus(ChangeStatus.REJECTED.getCode());
        change.setRejectReason(ANALYSIS_ONLY_REJECT_REASON);
        change.setCreateTime(now);
        return change;
    }
    private void refreshRollbackConfigCacheSafely(RollbackPersistenceResult persistenceResult) {
        if (persistenceResult != null && persistenceResult.isRefreshConfigCache()) {
            refreshSystemConfigCacheSafely("rollback");
        }
    }
    private void refreshSystemConfigCacheSafely(String scene) {
        try {
            configService.refreshSystemConfigCache();
        } catch (RuntimeException exception) {
            LOGGER.warn("AI自动调参{}已提交,但刷新系统配置缓存失败", scene, exception);
        }
    }
    private void writeValue(String targetType, String targetId, String targetKey, String value) {
        AutoTuneTargetType parsedTargetType = AutoTuneTargetType.fromCode(targetType);
        String normalizedTargetType = normalizeText(targetType);
        String normalizedTargetKey = normalizeText(targetKey);
        String normalizedTargetId = normalizeTargetId(normalizedTargetType, targetId);
        AutoTuneTargetType parsedTargetType = AutoTuneTargetType.fromCode(normalizedTargetType);
        if (parsedTargetType == null) {
            throw new IllegalArgumentException("不支持的调参目标: " + normalizedTargetType + "/" + normalizedTargetKey);
        }
        if (AutoTuneTargetType.SYS_CONFIG.equals(parsedTargetType)) {
            if (!configService.saveConfigValue(targetKey, value)) {
                throw new IllegalStateException("保存运行参数失败: " + targetKey);
            if (!configService.saveConfigValue(normalizedTargetKey, value)) {
                throw new IllegalStateException("保存运行参数失败: " + normalizedTargetKey);
            }
            return;
        }
        Integer parsedTargetId = parseTargetId(targetId, parsedTargetType);
        Integer parsedTargetId = parseTargetId(normalizedTargetId, parsedTargetType);
        Integer intValue = value == null ? null : Integer.valueOf(value);
        if (AutoTuneTargetType.STATION.equals(parsedTargetType)) {
            boolean updated = basStationService.update(new UpdateWrapper<BasStation>()
                    .eq("station_id", parsedTargetId)
                    .set("out_task_limit", intValue));
            if (!updated) {
                throw new IllegalStateException("保存站点参数失败: " + targetId + "/" + targetKey);
                throw new IllegalStateException("保存站点参数失败: " + normalizedTargetId + "/" + normalizedTargetKey);
            }
            return;
        }
        if (AutoTuneTargetType.CRN.equals(parsedTargetType)) {
            String column = "maxOutTask".equals(targetKey) ? "max_out_task" : "max_in_task";
            String column = "maxOutTask".equals(normalizedTargetKey) ? "max_out_task" : "max_in_task";
            boolean updated = basCrnpService.update(new UpdateWrapper<BasCrnp>()
                    .eq("crn_no", parsedTargetId)
                    .set(column, intValue));
            if (!updated) {
                throw new IllegalStateException("保存堆垛机参数失败: " + targetId + "/" + targetKey);
                throw new IllegalStateException("保存堆垛机参数失败: " + normalizedTargetId + "/" + normalizedTargetKey);
            }
            return;
        }
        String column = "maxOutTask".equals(targetKey) ? "max_out_task" : "max_in_task";
        String column = "maxOutTask".equals(normalizedTargetKey) ? "max_out_task" : "max_in_task";
        boolean updated = basDualCrnpService.update(new UpdateWrapper<BasDualCrnp>()
                .eq("crn_no", parsedTargetId)
                .set(column, intValue));
        if (!updated) {
            throw new IllegalStateException("保存双工位堆垛机参数失败: " + targetId + "/" + targetKey);
            throw new IllegalStateException("保存双工位堆垛机参数失败: " + normalizedTargetId + "/" + normalizedTargetKey);
        }
    }
    private String readCurrentValue(String targetType, String targetId, String targetKey) {
        AutoTuneTargetType parsedTargetType = AutoTuneTargetType.fromCode(targetType);
        Integer parsedTargetId = parseTargetId(targetId, parsedTargetType);
        String normalizedTargetType = normalizeText(targetType);
        String normalizedTargetKey = normalizeText(targetKey);
        String normalizedTargetId = normalizeTargetId(normalizedTargetType, targetId);
        AutoTuneTargetType parsedTargetType = AutoTuneTargetType.fromCode(normalizedTargetType);
        if (parsedTargetType == null) {
            throw new IllegalArgumentException("不支持的调参目标: " + normalizedTargetType + "/" + normalizedTargetKey);
        }
        Integer parsedTargetId = parseTargetId(normalizedTargetId, parsedTargetType);
        if (AutoTuneTargetType.SYS_CONFIG.equals(parsedTargetType)) {
            Config config = configService.getOne(new QueryWrapper<Config>().eq("code", targetKey).last("limit 1"));
            Config config = configService.getOne(new QueryWrapper<Config>().eq("code", normalizedTargetKey).last("limit 1"));
            return config == null ? null : config.getValue();
        }
        if (AutoTuneTargetType.STATION.equals(parsedTargetType)) {
@@ -391,13 +796,13 @@
            if (crnp == null) {
                return null;
            }
            return toText("maxOutTask".equals(targetKey) ? crnp.getMaxOutTask() : crnp.getMaxInTask());
            return toText("maxOutTask".equals(normalizedTargetKey) ? crnp.getMaxOutTask() : crnp.getMaxInTask());
        }
        BasDualCrnp dualCrnp = basDualCrnpService.getById(parsedTargetId);
        if (dualCrnp == null) {
            return null;
        }
        return toText("maxOutTask".equals(targetKey) ? dualCrnp.getMaxOutTask() : dualCrnp.getMaxInTask());
        return toText("maxOutTask".equals(normalizedTargetKey) ? dualCrnp.getMaxOutTask() : dualCrnp.getMaxInTask());
    }
    private AiAutoTuneJob createJob(AutoTuneApplyRequest request, boolean dryRun, Date now) {
@@ -405,7 +810,7 @@
        job.setTriggerType(AutoTuneTriggerType.normalize(request.getTriggerType()));
        job.setStatus(AutoTuneJobStatus.RUNNING.getCode());
        job.setStartTime(now);
        job.setHasActiveTasks(0);
        job.setHasActiveTasks(resolveHasActiveTasksForAudit());
        job.setPromptSceneCode(PROMPT_SCENE_CODE);
        job.setSummary(dryRun ? "AI自动调参 dry-run: " + safeReason(request.getReason()) : safeReason(request.getReason()));
        job.setIntervalBefore(readIntervalMinutes());
@@ -424,7 +829,7 @@
        job.setTriggerType(AutoTuneTriggerType.ROLLBACK.getCode());
        job.setStatus(AutoTuneJobStatus.RUNNING.getCode());
        job.setStartTime(now);
        job.setHasActiveTasks(0);
        job.setHasActiveTasks(resolveHasActiveTasksForAudit());
        job.setPromptSceneCode(PROMPT_SCENE_CODE);
        job.setSummary(safeReason(reason));
        job.setIntervalBefore(readIntervalMinutes());
@@ -436,6 +841,21 @@
        job.setTotalTokens(0);
        job.setCreateTime(now);
        return job;
    }
    private int resolveHasActiveTasksForAudit() {
        if (wrkMastService == null) {
            LOGGER.warn("AI自动调参审计无法获取 WrkMastService,hasActiveTasks 按 0 记录");
            return 0;
        }
        try {
            QueryWrapper<WrkMast> queryWrapper = new QueryWrapper<>();
            queryWrapper.and(wrapper -> wrapper.notIn("wrk_sts", FINAL_WRK_STS_LIST).or().isNull("wrk_sts"));
            return wrkMastService.count(queryWrapper) > 0 ? 1 : 0;
        } catch (RuntimeException exception) {
            LOGGER.warn("AI自动调参审计查询未完成任务失败,hasActiveTasks 按 0 记录", exception);
            return 0;
        }
    }
    private void finishJob(AiAutoTuneJob job,
@@ -456,6 +876,17 @@
        }
    }
    private void finishAnalysisOnlyRejectedJob(AiAutoTuneJob job, List<AiAutoTuneChange> changes, Date now) {
        int rejectCount = Math.max(1, countRejected(changes));
        job.setFinishTime(now);
        job.setSuccessCount(0);
        job.setRejectCount(rejectCount);
        job.setIntervalAfter(readIntervalMinutes());
        job.setStatus(AutoTuneJobStatus.REJECTED.getCode());
        job.setSummary(ANALYSIS_ONLY_REJECT_REASON);
        job.setErrorMessage(ANALYSIS_ONLY_REJECT_REASON);
    }
    private void finishRollbackJob(AiAutoTuneJob job, List<AiAutoTuneChange> changes, Date now) {
        int successCount = countAccepted(changes);
        int rejectCount = countRejected(changes);
@@ -463,7 +894,15 @@
        job.setSuccessCount(successCount);
        job.setRejectCount(rejectCount);
        job.setIntervalAfter(readIntervalMinutes());
        job.setStatus(rejectCount == 0 ? AutoTuneJobStatus.SUCCESS.getCode() : AutoTuneJobStatus.PARTIAL_SUCCESS.getCode());
        if (rejectCount == 0) {
            job.setStatus(AutoTuneJobStatus.SUCCESS.getCode());
        } else if (successCount == 0 && hasFailedChange(changes)) {
            job.setStatus(AutoTuneJobStatus.FAILED.getCode());
        } else if (successCount == 0) {
            job.setStatus(AutoTuneJobStatus.REJECTED.getCode());
        } else {
            job.setStatus(AutoTuneJobStatus.PARTIAL_SUCCESS.getCode());
        }
        job.setSummary("回滚最近一次成功调参,成功 " + successCount + " 项,失败 " + rejectCount + " 项");
        if (rejectCount > 0) {
            job.setErrorMessage(firstRejectReason(changes));
@@ -502,13 +941,14 @@
        List<AiAutoTuneChange> auditChanges = new ArrayList<>();
        for (ValidatedChange validatedChange : validatedChanges) {
            AiAutoTuneChange change = new AiAutoTuneChange();
            AutoTuneChangeCommand command = validatedChange.getCommand();
            change.setJobId(jobId);
            change.setTargetType(command.getTargetType());
            change.setTargetId(normalizeTargetId(command.getTargetType(), command.getTargetId()));
            change.setTargetKey(command.getTargetKey());
            change.setTargetType(validatedChange.getTargetType());
            change.setTargetId(validatedChange.getTargetId());
            change.setTargetKey(validatedChange.getTargetKey());
            change.setOldValue(validatedChange.getOldValue());
            change.setRequestedValue(validatedChange.getRequestedValue() == null ? command.getNewValue() : validatedChange.getRequestedValue());
            change.setRequestedValue(validatedChange.getRequestedValue() == null
                    ? validatedChange.getRawRequestedValue()
                    : validatedChange.getRequestedValue());
            change.setAppliedValue(validatedChange.getAppliedValue());
            change.setResultStatus(validatedChange.getStatus().getCode());
            change.setRejectReason(validatedChange.getRejectReason());
@@ -574,11 +1014,16 @@
        return !AutoTuneTriggerType.ROLLBACK.getCode().equals(job.getTriggerType());
    }
    private AutoTuneApplyResult buildResult(AiAutoTuneJob job, List<AiAutoTuneChange> changes, boolean dryRun) {
    private AutoTuneApplyResult buildResult(AiAutoTuneJob job,
                                            List<AiAutoTuneChange> changes,
                                            boolean dryRun,
                                            AutoTuneControlModeSnapshot controlMode) {
        AutoTuneApplyResult result = new AutoTuneApplyResult();
        result.setDryRun(dryRun);
        result.setSuccess(AutoTuneJobStatus.SUCCESS.getCode().equals(job.getStatus())
                || AutoTuneJobStatus.NO_CHANGE.getCode().equals(job.getStatus()));
        result.setAnalysisOnly(isAnalysisOnly(controlMode));
        result.setNoApply(false);
        result.setJobId(job.getId());
        result.setSummary(job.getSummary());
        result.setSuccessCount(job.getSuccessCount());
@@ -658,12 +1103,16 @@
        }
    }
    private String normalizeTargetId(String targetType, String targetId) {
    private static String normalizeTargetId(String targetType, String targetId) {
        AutoTuneTargetType parsedTargetType = AutoTuneTargetType.fromCode(targetType);
        if (AutoTuneTargetType.SYS_CONFIG.equals(parsedTargetType)) {
            return "";
        }
        return targetId == null ? "" : targetId.trim();
        return normalizeText(targetId);
    }
    private static String normalizeText(String value) {
        return value == null ? "" : value.trim();
    }
    private int readIntervalMinutes() {
@@ -676,6 +1125,14 @@
        } catch (Exception exception) {
            return 0;
        }
    }
    private AutoTuneControlModeSnapshot currentControlModeSnapshot() {
        return autoTuneControlModeService.currentMode();
    }
    private boolean isAnalysisOnly(AutoTuneControlModeSnapshot controlMode) {
        return controlMode == null || Boolean.TRUE.equals(controlMode.getAnalysisOnly());
    }
    private String toText(Integer value) {
@@ -732,8 +1189,48 @@
        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 RollbackPersistenceResult {
        private final List<AiAutoTuneChange> rollbackChanges;
        private final boolean refreshConfigCache;
        private RollbackPersistenceResult(List<AiAutoTuneChange> rollbackChanges, boolean refreshConfigCache) {
            this.rollbackChanges = rollbackChanges == null ? new ArrayList<>() : rollbackChanges;
            this.refreshConfigCache = refreshConfigCache;
        }
        public List<AiAutoTuneChange> getRollbackChanges() {
            return rollbackChanges;
        }
        public boolean isRefreshConfigCache() {
            return refreshConfigCache;
        }
    }
    private static class ValidatedChange {
        private final AutoTuneChangeCommand command;
        private final String targetType;
        private final String targetId;
        private final String targetKey;
        private final String rawRequestedValue;
        private AutoTuneRuleDefinition.Rule rule;
        private String oldValue;
        private String requestedValue;
@@ -745,6 +1242,10 @@
        private ValidatedChange(AutoTuneChangeCommand command) {
            this.command = command == null ? new AutoTuneChangeCommand() : command;
            this.targetType = normalizeText(this.command.getTargetType());
            this.targetId = normalizeTargetId(this.targetType, this.command.getTargetId());
            this.targetKey = normalizeText(this.command.getTargetKey());
            this.rawRequestedValue = this.command.getNewValue();
        }
        private ValidatedChange reject(String reason) {
@@ -773,6 +1274,22 @@
            return command;
        }
        public String getTargetType() {
            return targetType;
        }
        public String getTargetId() {
            return targetId;
        }
        public String getTargetKey() {
            return targetKey;
        }
        public String getRawRequestedValue() {
            return rawRequestedValue;
        }
        public AutoTuneRuleDefinition.Rule getRule() {
            return rule;
        }