This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 002449ba refactor: replace remaining in-memory repositories with 
MySQL-backed ones (#955)
002449ba is described below

commit 002449ba24160db6542774cd4ed7229d42b82e69
Author: lizhimins <[email protected]>
AuthorDate: Tue Aug 4 15:51:56 2026 +0800

    refactor: replace remaining in-memory repositories with MySQL-backed ones 
(#955)
    
    Drop InMemorySettingsRepository, InMemoryK8sCertRepository,
    InMemoryAlertRepository and InMemoryAuditRepository now that every
    store is database-backed. Add MyBatis-Plus repositories for alert
    rules and system alerts (new rmq_alert_rule / rmq_system_alert tables
    plus the upgrade-demo-alert migration), audit records
    (rmq_operation_audit) and K8s certificates (rmq_k8s_certificate, SAN
    stored as a JSON array). Service tests that relied on the in-memory
    fakes now use Mockito stubs expressing the repository contracts.
---
 deploy/mysql/upgrade-demo-alert.sql                |  43 +++++
 .../cluster/k8s/InMemoryK8sCertRepository.java     |  56 ------
 .../cluster/k8s/MybatisPlusK8sCertRepository.java  | 159 +++++++++++++++++
 .../studio/ops/alert/InMemoryAlertRepository.java  |  84 ---------
 .../ops/alert/MybatisPlusAlertRepository.java      | 190 +++++++++++++++++++++
 .../studio/ops/audit/InMemoryAuditRepository.java  |  91 ----------
 .../ops/audit/MybatisPlusAuditRepository.java      |  91 ++++++++++
 .../{RmqK8sCertificate.java => RmqAlertRule.java}  |  34 ++--
 .../persistence/entity/RmqK8sCertificate.java      |   4 +
 ...{RmqK8sCertificate.java => RmqSystemAlert.java} |  26 ++-
 .../RmqAlertRuleMapper.java}                       |  37 +---
 .../RmqSystemAlertMapper.java}                     |  37 +---
 .../settings/InMemorySettingsRepository.java       |  81 ---------
 server/src/main/resources/db/schema.sql            |  34 ++++
 .../cluster/k8s/InMemoryK8sCertRepositoryTest.java |  64 -------
 .../ops/alert/InMemoryAlertRepositoryTest.java     |  57 -------
 .../ops/audit/InMemoryAuditRepositoryTest.java     |  83 ---------
 .../settings/InMemorySettingsRepositoryTest.java   |  74 --------
 .../studio/settings/SettingsServiceTest.java       |   6 +-
 19 files changed, 564 insertions(+), 687 deletions(-)

diff --git a/deploy/mysql/upgrade-demo-alert.sql 
b/deploy/mysql/upgrade-demo-alert.sql
new file mode 100644
index 00000000..904be1f9
--- /dev/null
+++ b/deploy/mysql/upgrade-demo-alert.sql
@@ -0,0 +1,43 @@
+-- deploy/mysql/upgrade-demo-alert.sql
+-- 存量 MySQL 数据卷增量迁移:告警规则与系统告警入库(rmq_alert_rule / rmq_system_alert)
+-- 适用:数据卷已初始化、docker-entrypoint-initdb.d 不会再执行的存量部署。
+-- 全新数据卷由 server/src/main/resources/db/schema.sql 直接覆盖,无需本脚本。
+-- 幂等:可重复执行。
+--
+-- 用法(远程容器内执行):
+--   docker exec -i rocketmq-studio-mysql mysql -uroot -pstudio123 
rocketmq_studio < upgrade-demo-alert.sql
+
+-- 固定连接编码,防止 mysql 客户端以 latin1 解释 UTF-8 字节导致中文双重编码
+SET NAMES utf8mb4;
+
+CREATE TABLE IF NOT EXISTS rmq_alert_rule (
+  id VARCHAR(64) PRIMARY KEY,
+  name VARCHAR(128) NOT NULL,
+  metric VARCHAR(128),
+  operator VARCHAR(16),
+  threshold DOUBLE,
+  threshold_unit VARCHAR(32),
+  duration VARCHAR(32),
+  channels VARCHAR(512) COMMENT '逗号分隔的通知渠道',
+  enabled TINYINT(1) DEFAULT 1,
+  last_triggered VARCHAR(64),
+  description VARCHAR(512),
+  broker_name VARCHAR(128),
+  cluster_name VARCHAR(128),
+  severity VARCHAR(32),
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS rmq_system_alert (
+  id VARCHAR(64) PRIMARY KEY,
+  level VARCHAR(32),
+  title VARCHAR(255),
+  description TEXT,
+  time DATETIME,
+  acknowledged TINYINT(1) DEFAULT 0,
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  INDEX idx_level (level),
+  INDEX idx_acknowledged (acknowledged)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/InMemoryK8sCertRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/InMemoryK8sCertRepository.java
deleted file mode 100644
index 622242c2..00000000
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/InMemoryK8sCertRepository.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.cluster.k8s;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.ConcurrentHashMap;
-
-@Slf4j
-@Component
-public class InMemoryK8sCertRepository implements K8sCertRepository {
-
-    private final Map<String, K8sCertVO> store = new ConcurrentHashMap<>();
-
-    @Override
-    public List<K8sCertVO> findAll() {
-        return new ArrayList<>(store.values());
-    }
-
-    @Override
-    public Optional<K8sCertVO> findById(String id) {
-        return Optional.ofNullable(store.get(id));
-    }
-
-    @Override
-    public K8sCertVO save(K8sCertVO cert) {
-        store.put(cert.getId(), cert);
-        log.info("Saved certificate: {} (id={})", cert.getName(), 
cert.getId());
-        return cert;
-    }
-
-    @Override
-    public void deleteById(String id) {
-        store.remove(id);
-        log.info("Deleted certificate: {}", id);
-    }
-}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/MybatisPlusK8sCertRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/MybatisPlusK8sCertRepository.java
new file mode 100644
index 00000000..acf66fb8
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/MybatisPlusK8sCertRepository.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.cluster.k8s;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.rocketmq.studio.common.domain.enums.CertStatus;
+import org.apache.rocketmq.studio.common.domain.enums.CertType;
+import org.apache.rocketmq.studio.persistence.entity.RmqK8sCertificate;
+import org.apache.rocketmq.studio.persistence.mapper.RmqK8sCertificateMapper;
+import org.springframework.stereotype.Repository;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * MySQL-backed K8s certificate repository. The SAN list is stored as a JSON
+ * array string.
+ */
+@Repository
+public class MybatisPlusK8sCertRepository implements K8sCertRepository {
+
+    private final RmqK8sCertificateMapper certMapper;
+    private final ObjectMapper objectMapper;
+
+    public MybatisPlusK8sCertRepository(RmqK8sCertificateMapper certMapper, 
ObjectMapper objectMapper) {
+        this.certMapper = certMapper;
+        this.objectMapper = objectMapper;
+    }
+
+    @Override
+    public List<K8sCertVO> findAll() {
+        return certMapper.selectList(new 
QueryWrapper<RmqK8sCertificate>().orderByAsc("name")).stream()
+                .map(this::toVO)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public Optional<K8sCertVO> findById(String id) {
+        return Optional.ofNullable(certMapper.selectById(id)).map(this::toVO);
+    }
+
+    @Override
+    public K8sCertVO save(K8sCertVO cert) {
+        RmqK8sCertificate entity = toEntity(cert);
+        if (entity.getId() != null && certMapper.selectById(entity.getId()) != 
null) {
+            certMapper.updateById(entity);
+        } else {
+            certMapper.insert(entity);
+            cert.setId(entity.getId());
+        }
+        return cert;
+    }
+
+    @Override
+    public void deleteById(String id) {
+        certMapper.deleteById(id);
+    }
+
+    private K8sCertVO toVO(RmqK8sCertificate entity) {
+        K8sCertVO vo = new K8sCertVO();
+        vo.setId(entity.getId());
+        vo.setName(entity.getName());
+        vo.setNamespace(entity.getNamespace());
+        vo.setCluster(entity.getCluster());
+        vo.setType(parseCertType(entity.getCertType()));
+        vo.setIssuer(entity.getIssuer());
+        vo.setNotBefore(entity.getNotBefore());
+        vo.setNotAfter(entity.getNotAfter());
+        vo.setStatus(parseCertStatus(entity.getStatus()));
+        vo.setDaysRemaining(entity.getDaysRemaining() == null ? 0 : 
entity.getDaysRemaining());
+        vo.setSan(parseSan(entity.getSan()));
+        vo.setCreatedAt(entity.getCreatedAt());
+        vo.setUpdatedAt(entity.getUpdatedAt());
+        return vo;
+    }
+
+    private RmqK8sCertificate toEntity(K8sCertVO cert) {
+        RmqK8sCertificate entity = new RmqK8sCertificate();
+        entity.setId(cert.getId());
+        entity.setName(cert.getName());
+        entity.setNamespace(cert.getNamespace());
+        entity.setCluster(cert.getCluster());
+        entity.setCertType(cert.getType() == null ? null : 
cert.getType().name());
+        entity.setIssuer(cert.getIssuer());
+        entity.setNotBefore(cert.getNotBefore());
+        entity.setNotAfter(cert.getNotAfter());
+        entity.setStatus(cert.getStatus() == null ? null : 
cert.getStatus().name());
+        entity.setDaysRemaining(cert.getDaysRemaining());
+        entity.setSan(writeSan(cert.getSan()));
+        entity.setCreatedAt(cert.getCreatedAt());
+        entity.setUpdatedAt(LocalDateTime.now());
+        return entity;
+    }
+
+    private CertType parseCertType(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        try {
+            return CertType.valueOf(value);
+        } catch (IllegalArgumentException exception) {
+            return null;
+        }
+    }
+
+    private CertStatus parseCertStatus(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        try {
+            return CertStatus.valueOf(value);
+        } catch (IllegalArgumentException exception) {
+            return null;
+        }
+    }
+
+    private List<String> parseSan(String json) {
+        if (!StringUtils.hasText(json)) {
+            return List.of();
+        }
+        try {
+            return objectMapper.readValue(json, new 
TypeReference<List<String>>() {
+            });
+        } catch (JsonProcessingException exception) {
+            return List.of();
+        }
+    }
+
+    private String writeSan(List<String> san) {
+        if (san == null) {
+            return null;
+        }
+        try {
+            return objectMapper.writeValueAsString(san);
+        } catch (JsonProcessingException exception) {
+            return "[]";
+        }
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/InMemoryAlertRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/InMemoryAlertRepository.java
deleted file mode 100644
index 167d5682..00000000
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/InMemoryAlertRepository.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.alert;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
-
-@Slf4j
-@Component
-public class InMemoryAlertRepository implements AlertRepository {
-
-    private final Map<String, AlertRuleVO> rules = new ConcurrentHashMap<>();
-    private final Map<String, SystemAlertVO> alerts = new 
ConcurrentHashMap<>();
-
-    @Override
-    public List<AlertRuleVO> findAllRules() {
-        return new ArrayList<>(rules.values());
-    }
-
-    @Override
-    public AlertRuleVO saveRule(AlertRuleVO rule) {
-        rules.put(rule.getId(), rule);
-        log.debug("Saved alert rule id={}", rule.getId());
-        return rule;
-    }
-
-    @Override
-    public boolean replaceRule(AlertRuleVO rule) {
-        boolean replaced = rules.replace(rule.getId(), rule) != null;
-        log.debug("Replaced alert rule id={}, replaced={}", rule.getId(), 
replaced);
-        return replaced;
-    }
-
-    @Override
-    public void deleteRule(String id) {
-        rules.remove(id);
-        log.debug("Deleted alert rule id={}", id);
-    }
-
-    @Override
-    public List<SystemAlertVO> findAlerts(String level) {
-        return alerts.values().stream()
-                .filter(a -> level == null || 
level.equalsIgnoreCase(a.getLevel().name()))
-                .collect(Collectors.toList());
-    }
-
-    @Override
-    public SystemAlertVO saveAlert(SystemAlertVO alert) {
-        alerts.put(alert.getId(), alert);
-        log.debug("Saved system alert id={}", alert.getId());
-        return alert;
-    }
-
-    @Override
-    public int deleteAcknowledgedAlerts() {
-        List<String> toRemove = alerts.values().stream()
-                .filter(SystemAlertVO::isAcknowledged)
-                .map(SystemAlertVO::getId)
-                .collect(Collectors.toList());
-        toRemove.forEach(alerts::remove);
-        log.debug("Cleared {} acknowledged system alerts", toRemove.size());
-        return toRemove.size();
-    }
-}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertRepository.java
new file mode 100644
index 00000000..4aa0a9d8
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertRepository.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.alert;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import org.apache.rocketmq.studio.common.domain.enums.AlertLevel;
+import org.apache.rocketmq.studio.persistence.entity.RmqAlertRule;
+import org.apache.rocketmq.studio.persistence.entity.RmqSystemAlert;
+import org.apache.rocketmq.studio.persistence.mapper.RmqAlertRuleMapper;
+import org.apache.rocketmq.studio.persistence.mapper.RmqSystemAlertMapper;
+import org.springframework.stereotype.Repository;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * MySQL-backed alert repository for alert rules and system alert events.
+ */
+@Repository
+public class MybatisPlusAlertRepository implements AlertRepository {
+
+    private final RmqAlertRuleMapper ruleMapper;
+    private final RmqSystemAlertMapper alertMapper;
+
+    public MybatisPlusAlertRepository(RmqAlertRuleMapper ruleMapper, 
RmqSystemAlertMapper alertMapper) {
+        this.ruleMapper = ruleMapper;
+        this.alertMapper = alertMapper;
+    }
+
+    @Override
+    public List<AlertRuleVO> findAllRules() {
+        return ruleMapper.selectList(new 
QueryWrapper<RmqAlertRule>().orderByAsc("name")).stream()
+                .map(MybatisPlusAlertRepository::toRuleVO)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public AlertRuleVO saveRule(AlertRuleVO rule) {
+        RmqAlertRule entity = toRuleEntity(rule);
+        if (entity.getId() != null && ruleMapper.selectById(entity.getId()) != 
null) {
+            ruleMapper.updateById(entity);
+        } else {
+            ruleMapper.insert(entity);
+        }
+        return rule;
+    }
+
+    @Override
+    public boolean replaceRule(AlertRuleVO rule) {
+        if (ruleMapper.selectById(rule.getId()) == null) {
+            return false;
+        }
+        ruleMapper.updateById(toRuleEntity(rule));
+        return true;
+    }
+
+    @Override
+    public void deleteRule(String id) {
+        ruleMapper.deleteById(id);
+    }
+
+    @Override
+    public List<SystemAlertVO> findAlerts(String level) {
+        QueryWrapper<RmqSystemAlert> query = new QueryWrapper<RmqSystemAlert>()
+                .eq(StringUtils.hasText(level), "level", level == null ? null 
: level.toLowerCase())
+                .orderByDesc("time");
+        return alertMapper.selectList(query).stream()
+                .map(MybatisPlusAlertRepository::toAlertVO)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public SystemAlertVO saveAlert(SystemAlertVO alert) {
+        RmqSystemAlert entity = toAlertEntity(alert);
+        if (entity.getId() != null && alertMapper.selectById(entity.getId()) 
!= null) {
+            alertMapper.updateById(entity);
+        } else {
+            alertMapper.insert(entity);
+        }
+        return alert;
+    }
+
+    @Override
+    public int deleteAcknowledgedAlerts() {
+        return Math.toIntExact(alertMapper.delete(
+                new QueryWrapper<RmqSystemAlert>().eq("acknowledged", true)));
+    }
+
+    // ── Mapping ────────────────────────────────────────────────────
+
+    private static AlertRuleVO toRuleVO(RmqAlertRule entity) {
+        AlertRuleVO vo = new AlertRuleVO();
+        vo.setId(entity.getId());
+        vo.setName(entity.getName());
+        vo.setMetric(entity.getMetric());
+        vo.setOperator(entity.getOperator());
+        vo.setThreshold(entity.getThreshold() == null ? 0 : 
entity.getThreshold());
+        vo.setThresholdUnit(entity.getThresholdUnit());
+        vo.setDuration(entity.getDuration());
+        vo.setChannels(splitCsv(entity.getChannels()));
+        vo.setEnabled(Boolean.TRUE.equals(entity.getEnabled()));
+        vo.setLastTriggered(entity.getLastTriggered());
+        vo.setDescription(entity.getDescription());
+        vo.setBrokerName(entity.getBrokerName());
+        vo.setClusterName(entity.getClusterName());
+        vo.setSeverity(entity.getSeverity());
+        return vo;
+    }
+
+    private static RmqAlertRule toRuleEntity(AlertRuleVO rule) {
+        RmqAlertRule entity = new RmqAlertRule();
+        entity.setId(rule.getId());
+        entity.setName(rule.getName());
+        entity.setMetric(rule.getMetric());
+        entity.setOperator(rule.getOperator());
+        entity.setThreshold(rule.getThreshold());
+        entity.setThresholdUnit(rule.getThresholdUnit());
+        entity.setDuration(rule.getDuration());
+        entity.setChannels(rule.getChannels() == null ? null : 
String.join(",", rule.getChannels()));
+        entity.setEnabled(rule.isEnabled());
+        entity.setLastTriggered(rule.getLastTriggered());
+        entity.setDescription(rule.getDescription());
+        entity.setBrokerName(rule.getBrokerName());
+        entity.setClusterName(rule.getClusterName());
+        entity.setSeverity(rule.getSeverity());
+        entity.setUpdatedAt(LocalDateTime.now());
+        return entity;
+    }
+
+    private static SystemAlertVO toAlertVO(RmqSystemAlert entity) {
+        SystemAlertVO vo = new SystemAlertVO();
+        vo.setId(entity.getId());
+        vo.setLevel(parseLevel(entity.getLevel()));
+        vo.setTitle(entity.getTitle());
+        vo.setDescription(entity.getDescription());
+        vo.setTime(entity.getTime());
+        vo.setAcknowledged(Boolean.TRUE.equals(entity.getAcknowledged()));
+        return vo;
+    }
+
+    private static RmqSystemAlert toAlertEntity(SystemAlertVO alert) {
+        RmqSystemAlert entity = new RmqSystemAlert();
+        entity.setId(alert.getId());
+        entity.setLevel(alert.getLevel() == null ? null : 
alert.getLevel().name());
+        entity.setTitle(alert.getTitle());
+        entity.setDescription(alert.getDescription());
+        entity.setTime(alert.getTime());
+        entity.setAcknowledged(alert.isAcknowledged());
+        entity.setUpdatedAt(LocalDateTime.now());
+        return entity;
+    }
+
+    private static AlertLevel parseLevel(String level) {
+        if (!StringUtils.hasText(level)) {
+            return AlertLevel.info;
+        }
+        try {
+            return AlertLevel.valueOf(level);
+        } catch (IllegalArgumentException exception) {
+            return AlertLevel.info;
+        }
+    }
+
+    private static List<String> splitCsv(String value) {
+        if (!StringUtils.hasText(value)) {
+            return List.of();
+        }
+        return Arrays.stream(value.split(","))
+                .map(String::trim)
+                .filter(part -> !part.isEmpty())
+                .collect(Collectors.toList());
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/InMemoryAuditRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/InMemoryAuditRepository.java
deleted file mode 100644
index 0b1ee895..00000000
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/InMemoryAuditRepository.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.audit;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-import java.time.LocalDateTime;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
-
-@Slf4j
-@Component
-public class InMemoryAuditRepository implements AuditRepository {
-
-    private final Map<String, AuditRecordVO> records = new 
ConcurrentHashMap<>();
-
-    @Override
-    public List<AuditRecordVO> findAll(String search, String operationType,
-                                     LocalDateTime startDate, LocalDateTime 
endDate,
-                                     String result) {
-        String normalizedSearch = normalize(search);
-        return records.values().stream()
-                .filter(r -> normalizedSearch == null
-                        || containsIgnoreCase(r.getDetail(), normalizedSearch)
-                        || containsIgnoreCase(r.getOperator(), 
normalizedSearch)
-                        || containsIgnoreCase(r.getTarget(), normalizedSearch))
-                .filter(r -> operationType == null || operationType.isEmpty()
-                        || operationType.equals(r.getOperationType()))
-                .filter(r -> startDate == null || r.getTimestamp() != null && 
!r.getTimestamp().isBefore(startDate))
-                .filter(r -> endDate == null || r.getTimestamp() != null && 
!r.getTimestamp().isAfter(endDate))
-                .filter(r -> result == null || result.isEmpty() || 
result.equals(r.getResult()))
-                .sorted((a, b) -> {
-                    if (a.getTimestamp() == null || b.getTimestamp() == null) {
-                        return 0;
-                    }
-                    return b.getTimestamp().compareTo(a.getTimestamp());
-                })
-                .collect(Collectors.toList());
-    }
-
-    private String normalize(String value) {
-        if (value == null || value.isBlank()) {
-            return null;
-        }
-        return value.toLowerCase();
-    }
-
-    private boolean containsIgnoreCase(String value, String normalizedSearch) {
-        return value != null && value.toLowerCase().contains(normalizedSearch);
-    }
-
-    @Override
-    public void save(AuditRecordVO record) {
-        if (record.getId() == null) {
-            record.setId(java.util.UUID.randomUUID().toString());
-        }
-        if (record.getTimestamp() == null) {
-            record.setTimestamp(LocalDateTime.now());
-        }
-        records.put(record.getId(), record);
-        log.debug("Saved audit record: {} - {}", record.getOperationType(), 
record.getTarget());
-    }
-
-    @Override
-    public int deleteBefore(LocalDateTime cutoff) {
-        List<String> toRemove = records.values().stream()
-                .filter(r -> r.getTimestamp() != null && 
r.getTimestamp().isBefore(cutoff))
-                .map(AuditRecordVO::getId)
-                .collect(Collectors.toList());
-        toRemove.forEach(records::remove);
-        log.debug("Deleted {} audit records before {}", toRemove.size(), 
cutoff);
-        return toRemove.size();
-    }
-}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepository.java
new file mode 100644
index 00000000..5640d95d
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepository.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.audit;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import org.apache.rocketmq.studio.persistence.entity.RmqOperationAudit;
+import org.apache.rocketmq.studio.persistence.mapper.RmqOperationAuditMapper;
+import org.springframework.stereotype.Repository;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * MySQL-backed audit repository (rmq_operation_audit). The VO's ipAddress has
+ * no dedicated column and is not persisted.
+ */
+@Repository
+public class MybatisPlusAuditRepository implements AuditRepository {
+
+    private final RmqOperationAuditMapper auditMapper;
+
+    public MybatisPlusAuditRepository(RmqOperationAuditMapper auditMapper) {
+        this.auditMapper = auditMapper;
+    }
+
+    @Override
+    public List<AuditRecordVO> findAll(String search, String operationType,
+                                       LocalDateTime startDate, LocalDateTime 
endDate,
+                                       String result) {
+        QueryWrapper<RmqOperationAudit> query = new 
QueryWrapper<RmqOperationAudit>()
+                .and(StringUtils.hasText(search), w -> w
+                        .like("operator", search)
+                        .or().like("resource_name", search)
+                        .or().like("detail", search))
+                .eq(StringUtils.hasText(operationType), "operation", 
operationType)
+                .ge(startDate != null, "operated_at", startDate)
+                .le(endDate != null, "operated_at", endDate)
+                .eq(StringUtils.hasText(result), "result", result)
+                .orderByDesc("operated_at");
+        return auditMapper.selectList(query).stream()
+                .map(MybatisPlusAuditRepository::toVO)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public void save(AuditRecordVO record) {
+        RmqOperationAudit entity = new RmqOperationAudit();
+        entity.setOperation(record.getOperationType());
+        entity.setResourceType("GENERAL");
+        entity.setResourceName(record.getTarget());
+        entity.setDetail(record.getDetail());
+        entity.setResult(record.getResult());
+        entity.setOperator(record.getOperator());
+        entity.setOperatedAt(record.getTimestamp() == null ? 
LocalDateTime.now() : record.getTimestamp());
+        auditMapper.insert(entity);
+    }
+
+    @Override
+    public int deleteBefore(LocalDateTime cutoff) {
+        return Math.toIntExact(auditMapper.delete(
+                new QueryWrapper<RmqOperationAudit>().lt("operated_at", 
cutoff)));
+    }
+
+    private static AuditRecordVO toVO(RmqOperationAudit entity) {
+        AuditRecordVO vo = new AuditRecordVO();
+        vo.setId(entity.getId() == null ? null : 
String.valueOf(entity.getId()));
+        vo.setTimestamp(entity.getOperatedAt());
+        vo.setOperator(entity.getOperator());
+        vo.setOperationType(entity.getOperation());
+        vo.setTarget(entity.getResourceName());
+        vo.setDetail(entity.getDetail());
+        vo.setResult(entity.getResult());
+        return vo;
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqAlertRule.java
similarity index 67%
copy from 
server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
copy to 
server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqAlertRule.java
index 69307dff..402003c4 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqAlertRule.java
@@ -24,29 +24,39 @@ import lombok.Data;
 import java.time.LocalDateTime;
 
 @Data
-@TableName("rmq_k8s_certificate")
-public class RmqK8sCertificate {
+@TableName("rmq_alert_rule")
+public class RmqAlertRule {
 
-    @TableId(type = IdType.ASSIGN_UUID)
+    @TableId(type = IdType.INPUT)
     private String id;
 
     private String name;
 
-    private String namespace;
+    private String metric;
 
-    private String cluster;
+    private String operator;
 
-    private String certType;
+    private Double threshold;
 
-    private String issuer;
+    private String thresholdUnit;
 
-    private LocalDateTime notBefore;
+    private String duration;
 
-    private LocalDateTime notAfter;
+    private String channels;
 
-    private String status;
+    private Boolean enabled;
 
-    private Integer daysRemaining;
+    private String lastTriggered;
 
-    private String san;
+    private String description;
+
+    private String brokerName;
+
+    private String clusterName;
+
+    private String severity;
+
+    private LocalDateTime createdAt;
+
+    private LocalDateTime updatedAt;
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
index 69307dff..3eea9b35 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
@@ -49,4 +49,8 @@ public class RmqK8sCertificate {
     private Integer daysRemaining;
 
     private String san;
+
+    private LocalDateTime createdAt;
+
+    private LocalDateTime updatedAt;
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqSystemAlert.java
similarity index 73%
copy from 
server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
copy to 
server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqSystemAlert.java
index 69307dff..3167a9a1 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqSystemAlert.java
@@ -24,29 +24,23 @@ import lombok.Data;
 import java.time.LocalDateTime;
 
 @Data
-@TableName("rmq_k8s_certificate")
-public class RmqK8sCertificate {
+@TableName("rmq_system_alert")
+public class RmqSystemAlert {
 
-    @TableId(type = IdType.ASSIGN_UUID)
+    @TableId(type = IdType.INPUT)
     private String id;
 
-    private String name;
+    private String level;
 
-    private String namespace;
+    private String title;
 
-    private String cluster;
+    private String description;
 
-    private String certType;
+    private LocalDateTime time;
 
-    private String issuer;
+    private Boolean acknowledged;
 
-    private LocalDateTime notBefore;
+    private LocalDateTime createdAt;
 
-    private LocalDateTime notAfter;
-
-    private String status;
-
-    private Integer daysRemaining;
-
-    private String san;
+    private LocalDateTime updatedAt;
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/mapper/RmqAlertRuleMapper.java
similarity index 53%
copy from 
server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
copy to 
server/src/main/java/org/apache/rocketmq/studio/persistence/mapper/RmqAlertRuleMapper.java
index 69307dff..a3b47d7d 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/mapper/RmqAlertRuleMapper.java
@@ -14,39 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.persistence.entity;
+package org.apache.rocketmq.studio.persistence.mapper;
 
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.rocketmq.studio.persistence.entity.RmqAlertRule;
 
-import java.time.LocalDateTime;
-
-@Data
-@TableName("rmq_k8s_certificate")
-public class RmqK8sCertificate {
-
-    @TableId(type = IdType.ASSIGN_UUID)
-    private String id;
-
-    private String name;
-
-    private String namespace;
-
-    private String cluster;
-
-    private String certType;
-
-    private String issuer;
-
-    private LocalDateTime notBefore;
-
-    private LocalDateTime notAfter;
-
-    private String status;
-
-    private Integer daysRemaining;
-
-    private String san;
+public interface RmqAlertRuleMapper extends BaseMapper<RmqAlertRule> {
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/mapper/RmqSystemAlertMapper.java
similarity index 53%
copy from 
server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
copy to 
server/src/main/java/org/apache/rocketmq/studio/persistence/mapper/RmqSystemAlertMapper.java
index 69307dff..b6d64340 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqK8sCertificate.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/mapper/RmqSystemAlertMapper.java
@@ -14,39 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.persistence.entity;
+package org.apache.rocketmq.studio.persistence.mapper;
 
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.rocketmq.studio.persistence.entity.RmqSystemAlert;
 
-import java.time.LocalDateTime;
-
-@Data
-@TableName("rmq_k8s_certificate")
-public class RmqK8sCertificate {
-
-    @TableId(type = IdType.ASSIGN_UUID)
-    private String id;
-
-    private String name;
-
-    private String namespace;
-
-    private String cluster;
-
-    private String certType;
-
-    private String issuer;
-
-    private LocalDateTime notBefore;
-
-    private LocalDateTime notAfter;
-
-    private String status;
-
-    private Integer daysRemaining;
-
-    private String san;
+public interface RmqSystemAlertMapper extends BaseMapper<RmqSystemAlert> {
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/settings/InMemorySettingsRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/settings/InMemorySettingsRepository.java
deleted file mode 100644
index 692068fe..00000000
--- 
a/server/src/main/java/org/apache/rocketmq/studio/settings/InMemorySettingsRepository.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.settings;
-
-import lombok.extern.slf4j.Slf4j;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.ConcurrentHashMap;
-
-@Slf4j
-// @Component — replaced by MybatisPlusSettingsRepository
-public class InMemorySettingsRepository implements SettingsRepository {
-
-    private GeneralSettingsVO generalSettings = GeneralSettingsVO.builder()
-            .theme("system")
-            .compact(false)
-            .desktopNotify(true)
-            .notifySound(false)
-            .sessionTimeout(30)
-            .requireLogin(false)
-            .llmProvider("openai")
-            .apiKey("")
-            .model("gpt-4")
-            .baseUrl("")
-            .build();
-
-    private final Map<String, DataSourceVO> dataSources = new 
ConcurrentHashMap<>();
-
-    @Override
-    public GeneralSettingsVO loadGeneralSettings() {
-        return generalSettings;
-    }
-
-    @Override
-    public void saveGeneralSettings(GeneralSettingsVO settings) {
-        this.generalSettings = settings;
-    }
-
-    @Override
-    public List<DataSourceVO> findAllDataSources() {
-        return new ArrayList<>(dataSources.values());
-    }
-
-    @Override
-    public DataSourceVO saveDataSource(DataSourceVO dataSource) {
-        dataSources.put(dataSource.getKey(), dataSource);
-        return dataSource;
-    }
-
-    @Override
-    public boolean replaceDataSource(DataSourceVO dataSource) {
-        return dataSources.replace(dataSource.getKey(), dataSource) != null;
-    }
-
-    @Override
-    public boolean deleteDataSource(String key) {
-        return dataSources.remove(key) != null;
-    }
-
-    @Override
-    public Optional<DataSourceVO> findDataSourceByKey(String key) {
-        return Optional.ofNullable(dataSources.get(key));
-    }
-}
diff --git a/server/src/main/resources/db/schema.sql 
b/server/src/main/resources/db/schema.sql
index f85700b1..a8952cf0 100644
--- a/server/src/main/resources/db/schema.sql
+++ b/server/src/main/resources/db/schema.sql
@@ -173,6 +173,40 @@ CREATE TABLE IF NOT EXISTS rmq_acl_user (
   UNIQUE KEY uk_username (username)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
+-- 13. 告警规则
+CREATE TABLE IF NOT EXISTS rmq_alert_rule (
+  id VARCHAR(64) PRIMARY KEY,
+  name VARCHAR(128) NOT NULL,
+  metric VARCHAR(128),
+  operator VARCHAR(16),
+  threshold DOUBLE,
+  threshold_unit VARCHAR(32),
+  duration VARCHAR(32),
+  channels VARCHAR(512) COMMENT '逗号分隔的通知渠道',
+  enabled TINYINT(1) DEFAULT 1,
+  last_triggered VARCHAR(64),
+  description VARCHAR(512),
+  broker_name VARCHAR(128),
+  cluster_name VARCHAR(128),
+  severity VARCHAR(32),
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 14. 系统告警事件
+CREATE TABLE IF NOT EXISTS rmq_system_alert (
+  id VARCHAR(64) PRIMARY KEY,
+  level VARCHAR(32),
+  title VARCHAR(255),
+  description TEXT,
+  time DATETIME,
+  acknowledged TINYINT(1) DEFAULT 0,
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  INDEX idx_level (level),
+  INDEX idx_acknowledged (acknowledged)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
 -- ============================================================
 -- 样例数据(幂等):instance / topic / group 列表以本库为准,创建时写库、读取时读库。
 -- 实例管理页默认 5 个实例:2 个 DIRECT(instance-direct-1/2)+ 3 个 
PROXY(instance-proxy-1/2/3)。
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/k8s/InMemoryK8sCertRepositoryTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/k8s/InMemoryK8sCertRepositoryTest.java
deleted file mode 100644
index e2f0a916..00000000
--- 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/k8s/InMemoryK8sCertRepositoryTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.cluster.k8s;
-
-import org.apache.rocketmq.studio.common.domain.enums.CertStatus;
-import org.apache.rocketmq.studio.common.domain.enums.CertType;
-import org.junit.jupiter.api.Test;
-
-import java.time.LocalDateTime;
-import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-class InMemoryK8sCertRepositoryTest {
-
-    @Test
-    void repositoryShouldStartEmpty() {
-        InMemoryK8sCertRepository repository = new InMemoryK8sCertRepository();
-
-        assertThat(repository.findAll()).isEmpty();
-    }
-
-    @Test
-    void saveFindAndDeleteShouldManageExplicitRecordsOnly() {
-        InMemoryK8sCertRepository repository = new InMemoryK8sCertRepository();
-        K8sCertVO cert = K8sCertVO.builder()
-                .name("rocketmq-tls")
-                .namespace("rocketmq")
-                .cluster("prod")
-                .type(CertType.TLS)
-                .issuer("issuer")
-                .notBefore(LocalDateTime.now().minusDays(1))
-                .notAfter(LocalDateTime.now().plusDays(30))
-                .status(CertStatus.valid)
-                .daysRemaining(30)
-                .san(List.of("broker.example.com"))
-                .build();
-        cert.setId("cert-explicit");
-
-        repository.save(cert);
-
-        assertThat(repository.findAll()).containsExactly(cert);
-        assertThat(repository.findById("cert-explicit")).contains(cert);
-
-        repository.deleteById("cert-explicit");
-
-        assertThat(repository.findAll()).isEmpty();
-        assertThat(repository.findById("cert-explicit")).isEmpty();
-    }
-}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/InMemoryAlertRepositoryTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/InMemoryAlertRepositoryTest.java
deleted file mode 100644
index 4076bdf3..00000000
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/InMemoryAlertRepositoryTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.alert;
-
-import org.junit.jupiter.api.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-class InMemoryAlertRepositoryTest {
-
-    private final InMemoryAlertRepository repository = new 
InMemoryAlertRepository();
-
-    @Test
-    void replaceRuleShouldUpdateExistingRule() {
-        AlertRuleVO existing = AlertRuleVO.builder()
-                .id("rule-1")
-                .name("Original rule")
-                .build();
-        AlertRuleVO replacement = AlertRuleVO.builder()
-                .id("rule-1")
-                .name("Updated rule")
-                .build();
-        repository.saveRule(existing);
-
-        boolean replaced = repository.replaceRule(replacement);
-
-        assertThat(replaced).isTrue();
-        assertThat(repository.findAllRules()).containsExactly(replacement);
-    }
-
-    @Test
-    void replaceRuleShouldNotInsertUnknownRule() {
-        AlertRuleVO replacement = AlertRuleVO.builder()
-                .id("missing")
-                .name("Missing rule")
-                .build();
-
-        boolean replaced = repository.replaceRule(replacement);
-
-        assertThat(replaced).isFalse();
-        assertThat(repository.findAllRules()).isEmpty();
-    }
-}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/InMemoryAuditRepositoryTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/InMemoryAuditRepositoryTest.java
deleted file mode 100644
index 25516302..00000000
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/InMemoryAuditRepositoryTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.audit;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.test.util.ReflectionTestUtils;
-
-import java.time.LocalDateTime;
-import java.util.Map;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-class InMemoryAuditRepositoryTest {
-
-    private final InMemoryAuditRepository repository = new 
InMemoryAuditRepository();
-
-    @Test
-    void findAllShouldSearchAcrossNullableFields() {
-        AuditRecordVO missingTextFields = AuditRecordVO.builder()
-                .timestamp(LocalDateTime.now())
-                .operationType("CREATE")
-                .result("SUCCESS")
-                .build();
-        missingTextFields.setId("record-null-fields");
-        AuditRecordVO targetMatch = AuditRecordVO.builder()
-                .timestamp(LocalDateTime.now().minusMinutes(1))
-                .operator("admin")
-                .operationType("UPDATE")
-                .target("Topic-Order")
-                .detail(null)
-                .result("SUCCESS")
-                .build();
-        targetMatch.setId("record-target-match");
-
-        putRecords(missingTextFields, targetMatch);
-
-        assertThat(repository.findAll("order", null, null, null, null))
-                .extracting(AuditRecordVO::getId)
-                .containsExactly("record-target-match");
-    }
-
-    @Test
-    void findAllShouldTreatBlankSearchAsNoSearchFilter() {
-        AuditRecordVO record = AuditRecordVO.builder()
-                .timestamp(LocalDateTime.now())
-                .operationType("DELETE")
-                .result("FAILURE")
-                .build();
-        record.setId("record-blank-search");
-
-        putRecords(record);
-
-        assertThat(repository.findAll("   ", null, null, null, null))
-                .extracting(AuditRecordVO::getId)
-                .containsExactly("record-blank-search");
-    }
-
-    @SafeVarargs
-    @SuppressWarnings("unchecked")
-    private final void putRecords(AuditRecordVO... records) {
-        Map<String, AuditRecordVO> store =
-                (Map<String, AuditRecordVO>) 
ReflectionTestUtils.getField(repository, "records");
-        assertThat(store).isNotNull();
-        store.clear();
-        for (AuditRecordVO record : records) {
-            store.put(record.getId(), record);
-        }
-    }
-}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/settings/InMemorySettingsRepositoryTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/settings/InMemorySettingsRepositoryTest.java
deleted file mode 100644
index aac58463..00000000
--- 
a/server/src/test/java/org/apache/rocketmq/studio/settings/InMemorySettingsRepositoryTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.settings;
-
-import org.junit.jupiter.api.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-class InMemorySettingsRepositoryTest {
-
-    private final InMemorySettingsRepository repository = new 
InMemorySettingsRepository();
-
-    @Test
-    void saveDataSourceShouldSupportCreateAndDeleteByKey() {
-        DataSourceVO dataSource = DataSourceVO.builder()
-                .key("source-1")
-                .name("Prometheus")
-                .type("Prometheus")
-                .url("http://localhost:9090";)
-                .build();
-
-        repository.saveDataSource(dataSource);
-
-        
assertThat(repository.findDataSourceByKey("source-1")).containsSame(dataSource);
-        
assertThat(repository.findAllDataSources()).containsExactly(dataSource);
-
-        boolean deleted = repository.deleteDataSource("source-1");
-
-        assertThat(deleted).isTrue();
-        assertThat(repository.findDataSourceByKey("source-1")).isEmpty();
-        assertThat(repository.findAllDataSources()).isEmpty();
-    }
-
-    @Test
-    void deleteDataSourceShouldReportMissingEntry() {
-        assertThat(repository.deleteDataSource("missing")).isFalse();
-    }
-
-    @Test
-    void replaceDataSourceShouldUpdateExistingEntry() {
-        DataSourceVO existing = 
DataSourceVO.builder().key("source-1").name("Prometheus").build();
-        DataSourceVO replacement = 
DataSourceVO.builder().key("source-1").name("Updated Prometheus").build();
-        repository.saveDataSource(existing);
-
-        boolean replaced = repository.replaceDataSource(replacement);
-
-        assertThat(replaced).isTrue();
-        
assertThat(repository.findAllDataSources()).containsExactly(replacement);
-    }
-
-    @Test
-    void replaceDataSourceShouldNotInsertUnknownEntry() {
-        DataSourceVO replacement = 
DataSourceVO.builder().key("missing").name("Unexpected DS").build();
-
-        boolean replaced = repository.replaceDataSource(replacement);
-
-        assertThat(replaced).isFalse();
-        assertThat(repository.findAllDataSources()).isEmpty();
-    }
-}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
index aaa343e8..785f3d5b 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
@@ -229,7 +229,7 @@ class SettingsServiceTest {
 
     @Test
     void updateDataSourceShouldRejectUnknownKey() {
-        SettingsService service = new SettingsService(new 
InMemorySettingsRepository(), RestClient.builder(), new ObjectMapper(), 
operationAuditService);
+        SettingsService service = new SettingsService(settingsRepository, 
RestClient.builder(), new ObjectMapper(), operationAuditService);
         DataSourceVO input = 
DataSourceVO.builder().key("missing").name("Unexpected DS").type("rocketmq")
                 .url("unexpected-host:9876").build();
 
@@ -243,7 +243,7 @@ class SettingsServiceTest {
 
     @Test
     void updateDataSourceShouldRejectBlankKey() {
-        SettingsService service = new SettingsService(new 
InMemorySettingsRepository(), RestClient.builder(), new ObjectMapper(),
+        SettingsService service = new SettingsService(settingsRepository, 
RestClient.builder(), new ObjectMapper(),
                 operationAuditService);
         DataSourceVO input = DataSourceVO.builder().key(" ").name("Unexpected 
DS").type("rocketmq")
                 .url("unexpected-host:9876").build();
@@ -267,7 +267,7 @@ class SettingsServiceTest {
 
     @Test
     void deleteDataSourceShouldRejectUnknownKey() {
-        SettingsService service = new SettingsService(new 
InMemorySettingsRepository(), RestClient.builder(), new ObjectMapper(),
+        SettingsService service = new SettingsService(settingsRepository, 
RestClient.builder(), new ObjectMapper(),
                 operationAuditService);
 
         assertThatThrownBy(() -> service.deleteDataSource("missing"))

Reply via email to