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 9e0ed86e5 feat(studio): update consumer group settings (#2528)
9e0ed86e5 is described below

commit 9e0ed86e5d567aaa2ba89e869ff3170387703bc5
Author: aias00 <[email protected]>
AuthorDate: Sat Aug 22 15:37:05 2026 +0800

    feat(studio): update consumer group settings (#2528)
---
 .../instance/group/ConsumerGroupController.java    | 13 ++++
 .../instance/group/ConsumerGroupSettingsVO.java    | 32 ++++++++
 .../group/UpdateConsumerGroupSettingsDTO.java      | 36 +++++++++
 .../studio/instance/topic/MetadataService.java     | 21 +++++
 .../studio/provider/apache/AdminClient.java        |  4 +
 .../provider/apache/RocketMQAdminClientImpl.java   | 65 ++++++++++++++++
 .../group/ConsumerGroupControllerTest.java         | 29 +++++++
 .../apache/RocketMQAdminClientImplTest.java        | 39 ++++++++++
 web/src/api/consumerGroups.test.ts                 | 24 ++++++
 web/src/api/metadata.ts                            | 21 +++++
 web/src/pages/instance/consumer.tsx                | 90 ++++++++++++++++++++++
 web/src/services/consumerService.ts                | 21 +++++
 12 files changed, 395 insertions(+)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
index bf7988d31..d6d0733ca 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
@@ -66,6 +66,19 @@ public class ConsumerGroupController {
         return Result.ok(metadataService.getConsumerGroup(instanceId, name));
     }
 
+    @GetMapping("/{name}/settings")
+    public Result<ConsumerGroupSettingsVO> 
getConsumerGroupSettings(@PathVariable String name,
+                                                                      
@RequestParam String instanceId) {
+        return Result.ok(metadataService.getConsumerGroupSettings(instanceId, 
name));
+    }
+
+    @PostMapping("/settings")
+    public Result<ConsumerGroupSettingsVO> updateConsumerGroupSettings(
+            @Valid @RequestBody UpdateConsumerGroupSettingsDTO request) {
+        return 
Result.ok(metadataService.updateConsumerGroupSettings(request.getInstanceId(), 
request.getName(),
+                request.getRetryQueueNums(), request.getRetryMaxTimes()));
+    }
+
     @GetMapping("/{name}/progress")
     public Result<List<QueueProgressVO>> getGroupProgress(
             @PathVariable String name,
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupSettingsVO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupSettingsVO.java
new file mode 100644
index 000000000..7cbff0e5e
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupSettingsVO.java
@@ -0,0 +1,32 @@
+/*
+ * 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.instance.group;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ConsumerGroupSettingsVO {
+    private String groupName;
+    private int retryQueueNums;
+    private int retryMaxTimes;
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/UpdateConsumerGroupSettingsDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/UpdateConsumerGroupSettingsDTO.java
new file mode 100644
index 000000000..0c3cf330f
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/UpdateConsumerGroupSettingsDTO.java
@@ -0,0 +1,36 @@
+/*
+ * 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.instance.group;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import lombok.Data;
+
+@Data
+public class UpdateConsumerGroupSettingsDTO {
+    @NotBlank(message = "instanceId is required")
+    private String instanceId;
+    @NotBlank(message = "name is required")
+    private String name;
+    @NotNull(message = "retryQueueNums is required")
+    @Positive(message = "retryQueueNums must be positive")
+    private Integer retryQueueNums;
+    @NotNull(message = "retryMaxTimes is required")
+    @Positive(message = "retryMaxTimes must be positive")
+    private Integer retryMaxTimes;
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java
index bfe9a6a44..17d492a34 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java
@@ -24,6 +24,7 @@ import 
org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.springframework.util.StringUtils;
 import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupSettingsVO;
 import org.apache.rocketmq.studio.instance.group.QueueProgressVO;
 import org.apache.rocketmq.studio.instance.group.SubscriptionEntryVO;
 import org.apache.rocketmq.studio.provider.InstanceProvider;
@@ -235,6 +236,20 @@ public class MetadataService {
         return resolve(instanceId).createConsumerGroup(instanceId, group);
     }
 
+    public ConsumerGroupSettingsVO getConsumerGroupSettings(String instanceId, 
String name) {
+        instanceId = normalizeInstanceId(instanceId);
+        requireApacheInstance(instanceId);
+        return adminClient.getConsumerGroupSettings(instanceId, 
requireName(name, "consumer group name"));
+    }
+
+    public ConsumerGroupSettingsVO updateConsumerGroupSettings(String 
instanceId, String name, int retryQueueNums,
+                                                                 int 
retryMaxTimes) {
+        instanceId = normalizeInstanceId(instanceId);
+        requireApacheInstance(instanceId);
+        return adminClient.updateConsumerGroupSettings(instanceId, 
requireName(name, "consumer group name"),
+                retryQueueNums, retryMaxTimes);
+    }
+
 
     public void deleteConsumerGroup(String name) {
         deleteConsumerGroup(null, name);
@@ -265,6 +280,12 @@ public class MetadataService {
                 .orElseGet(() -> 
providerRegistry.forVendor(InstanceVendor.APACHE));
     }
 
+    private void requireApacheInstance(String instanceId) {
+        if (resolve(instanceId).vendor() != InstanceVendor.APACHE) {
+            throw new BusinessException(501, "Consumer group settings are not 
supported for cloud instances");
+        }
+    }
+
     private String normalizeFilter(String value) {
         return !StringUtils.hasText(value) ? null : value.trim();
     }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/AdminClient.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/AdminClient.java
index 8d65f1266..36ecea467 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/AdminClient.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/AdminClient.java
@@ -20,6 +20,7 @@ import 
org.apache.rocketmq.studio.instance.topic.SendMessageVO;
 import org.apache.rocketmq.studio.instance.topic.SendMessageDTO;
 import org.apache.rocketmq.studio.instance.topic.TopicVO;
 import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupSettingsVO;
 
 
 public interface AdminClient {
@@ -30,6 +31,9 @@ public interface AdminClient {
     void deleteTopic(String instanceId, String name);
     SendMessageVO sendMessage(SendMessageDTO request);
     ConsumerGroupVO createConsumerGroup(ConsumerGroupVO group);
+    ConsumerGroupSettingsVO getConsumerGroupSettings(String instanceId, String 
name);
+    ConsumerGroupSettingsVO updateConsumerGroupSettings(String instanceId, 
String name, int retryQueueNums,
+                                                         int retryMaxTimes);
     void deleteConsumerGroup(String instanceId, String name);
     void resetOffset(String instanceId, String name, long timestamp, String 
topic);
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImpl.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImpl.java
index 3190569f9..8c223a3d8 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImpl.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImpl.java
@@ -37,6 +37,7 @@ import 
org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
 import org.apache.rocketmq.studio.common.domain.enums.TopicType;
 import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupSettingsVO;
 import org.apache.rocketmq.studio.instance.topic.SendMessageDTO;
 import org.apache.rocketmq.studio.instance.topic.SendMessageVO;
 import org.apache.rocketmq.studio.instance.topic.TopicVO;
@@ -499,6 +500,70 @@ public class RocketMQAdminClientImpl implements 
AdminClient {
         return adminFactory.execute(namesrvAddr(), null, admin -> 
createConsumerGroup(admin, group));
     }
 
+    @Override
+    public ConsumerGroupSettingsVO getConsumerGroupSettings(String instanceId, 
String name) {
+        return executeForInstance(instanceId, admin -> {
+            try {
+                Set<String> brokerAddrs = 
getMasterBrokerAddrsForCluster(admin, getClusterName(admin));
+                if (brokerAddrs.isEmpty()) {
+                    throw new BusinessException(502, "No broker available to 
get consumer group settings");
+                }
+                SubscriptionGroupConfig config = 
admin.examineSubscriptionGroupConfig(brokerAddrs.iterator().next(), name);
+                if (config == null) {
+                    throw new BusinessException(404, "Consumer group not 
found: " + name);
+                }
+                return 
ConsumerGroupSettingsVO.builder().groupName(name).retryQueueNums(config.getRetryQueueNums())
+                        .retryMaxTimes(config.getRetryMaxTimes()).build();
+            } catch (BusinessException exception) {
+                throw exception;
+            } catch (Exception exception) {
+                throw classifyBrokerFailure(exception, "get consumer group 
settings");
+            }
+        });
+    }
+
+    @Override
+    public ConsumerGroupSettingsVO updateConsumerGroupSettings(String 
instanceId, String name, int retryQueueNums,
+                                                                 int 
retryMaxTimes) {
+        return executeForInstance(instanceId, admin -> {
+            try {
+                String clusterName = getClusterName(admin);
+                Set<String> brokerAddrs = 
getMasterBrokerAddrsForCluster(admin, clusterName);
+                if (brokerAddrs.isEmpty()) {
+                    throw new BusinessException(502, "No broker available to 
update consumer group settings");
+                }
+                for (String brokerAddr : brokerAddrs) {
+                    SubscriptionGroupConfig config = 
admin.examineSubscriptionGroupConfig(brokerAddr, name);
+                    if (config == null) {
+                        throw new BusinessException(404, "Consumer group not 
found: " + name);
+                    }
+                    config.setRetryQueueNums(retryQueueNums);
+                    config.setRetryMaxTimes(retryMaxTimes);
+                    admin.createAndUpdateSubscriptionGroupConfig(brokerAddr, 
config);
+                }
+                RmqGroup group = groupMapper.selectOne(new 
LambdaQueryWrapper<RmqGroup>()
+                        .eq(RmqGroup::getClusterId, clusterName)
+                        .eq(RmqGroup::getInstanceId, metadataScope(instanceId))
+                        .eq(RmqGroup::getName, name));
+                if (group != null) {
+                    group.setMaxRetry(retryMaxTimes);
+                    group.setGmtModified(LocalDateTime.now());
+                    groupMapper.updateById(group);
+                }
+                recordAudit("UPDATE_GROUP_SETTINGS", name, "retryQueueNums=" + 
retryQueueNums
+                        + ", retryMaxTimes=" + retryMaxTimes, "SUCCESS");
+                return 
ConsumerGroupSettingsVO.builder().groupName(name).retryQueueNums(retryQueueNums)
+                        .retryMaxTimes(retryMaxTimes).build();
+            } catch (BusinessException exception) {
+                recordAudit("UPDATE_GROUP_SETTINGS", name, 
exception.getMessage(), "FAILED");
+                throw exception;
+            } catch (Exception exception) {
+                recordAudit("UPDATE_GROUP_SETTINGS", name, 
exception.getMessage(), "FAILED");
+                throw classifyBrokerFailure(exception, "update consumer group 
settings");
+            }
+        });
+    }
+
     private ConsumerGroupVO createConsumerGroup(MQAdminExt admin, 
ConsumerGroupVO group) {
         String groupName = group.getName();
 
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
index 205a6d7cc..a7a260f57 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
@@ -167,6 +167,35 @@ class ConsumerGroupControllerTest {
         verifyNoInteractions(metadataService);
     }
 
+    @Test
+    void consumerGroupSettingsShouldUseTheSelectedInstance() throws Exception {
+        ConsumerGroupSettingsVO settings = 
ConsumerGroupSettingsVO.builder().groupName("cg-orders")
+                .retryQueueNums(2).retryMaxTimes(8).build();
+        when(metadataService.getConsumerGroupSettings("instance-a", 
"cg-orders")).thenReturn(settings);
+
+        
mockMvc.perform(get("/api/groups/cg-orders/settings").param("instanceId", 
"instance-a"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.retryQueueNums").value(2));
+
+        verify(metadataService).getConsumerGroupSettings("instance-a", 
"cg-orders");
+    }
+
+    @Test
+    void consumerGroupSettingsUpdateShouldValidateAndDelegate() throws 
Exception {
+        Map<String, Object> body = Map.of("instanceId", "instance-a", "name", 
"cg-orders",
+                "retryQueueNums", 2, "retryMaxTimes", 8);
+        when(metadataService.updateConsumerGroupSettings("instance-a", 
"cg-orders", 2, 8))
+                
.thenReturn(ConsumerGroupSettingsVO.builder().groupName("cg-orders").retryQueueNums(2)
+                        .retryMaxTimes(8).build());
+
+        
mockMvc.perform(post("/api/groups/settings").contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(body)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.retryMaxTimes").value(8));
+
+        verify(metadataService).updateConsumerGroupSettings("instance-a", 
"cg-orders", 2, 8);
+    }
+
     @Test
     void getConsumerStackShouldReturnStackTrace() throws Exception {
         ConsumerThreadStackVO thread = ConsumerThreadStackVO.builder()
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImplTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImplTest.java
index b19ee8a1e..fba406cac 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImplTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImplTest.java
@@ -27,11 +27,13 @@ import 
org.apache.rocketmq.remoting.exception.RemotingTimeoutException;
 import org.apache.rocketmq.remoting.protocol.ResponseCode;
 import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
 import org.apache.rocketmq.remoting.protocol.route.BrokerData;
+import 
org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig;
 import org.apache.rocketmq.studio.cluster.broker.MqAdminExtFactory;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.common.domain.enums.TopicType;
 import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
 import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupSettingsVO;
 import org.apache.rocketmq.studio.instance.group.ConsumerInstanceVO;
 import org.apache.rocketmq.studio.instance.topic.TopicVO;
 import org.apache.rocketmq.studio.instance.topic.SendMessageDTO;
@@ -576,6 +578,43 @@ class RocketMQAdminClientImplTest {
         assertThat(captor.getValue().getSqlSegment()).contains("cluster_id", 
"instance_id", "name");
     }
 
+    @Test
+    void updateConsumerGroupSettingsPreservesBrokerConfiguration() throws 
Exception {
+        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new 
MybatisConfiguration(), ""), RmqGroup.class);
+        DefaultMQAdminExt selectedAdmin = 
org.mockito.Mockito.mock(DefaultMQAdminExt.class);
+        ClusterInfo clusterInfo = new ClusterInfo();
+        clusterInfo.setClusterAddrTable(new HashMap<>(Map.of("cluster-1", new 
HashSet<>(List.of("broker-1")))));
+        BrokerData brokerData = new BrokerData();
+        brokerData.setBrokerName("broker-1");
+        brokerData.setBrokerAddrs(new HashMap<>(Map.of(0L, "10.0.0.1:10911")));
+        clusterInfo.setBrokerAddrTable(new HashMap<>(Map.of("broker-1", 
brokerData)));
+        SubscriptionGroupConfig config = new SubscriptionGroupConfig();
+        config.setGroupName("cg-orders");
+        config.setConsumeEnable(false);
+        config.setRetryQueueNums(1);
+        config.setRetryMaxTimes(16);
+        when(selectedAdmin.examineBrokerClusterInfo()).thenReturn(clusterInfo);
+        when(selectedAdmin.examineSubscriptionGroupConfig("10.0.0.1:10911", 
"cg-orders")).thenReturn(config);
+        when(groupMapper.selectOne(any())).thenReturn(null);
+        
doNothing().when(selectedAdmin).createAndUpdateSubscriptionGroupConfig(anyString(),
 any());
+        
when(runtimeAdminClientResolver.execute(org.mockito.ArgumentMatchers.eq("instance-a"),
 any()))
+                .thenAnswer(invocation -> {
+                    MqAdminExtFactory.AdminAction<?> action = 
invocation.getArgument(1);
+                    return action.apply(selectedAdmin);
+                });
+
+        ConsumerGroupSettingsVO settings = 
adminClient.updateConsumerGroupSettings("instance-a", "cg-orders", 2, 8);
+
+        assertThat(settings.getRetryQueueNums()).isEqualTo(2);
+        assertThat(settings.getRetryMaxTimes()).isEqualTo(8);
+        ArgumentCaptor<SubscriptionGroupConfig> captor = 
ArgumentCaptor.forClass(SubscriptionGroupConfig.class);
+        verify(selectedAdmin).createAndUpdateSubscriptionGroupConfig(
+                org.mockito.ArgumentMatchers.eq("10.0.0.1:10911"), 
captor.capture());
+        assertThat(captor.getValue().isConsumeEnable()).isFalse();
+        assertThat(captor.getValue().getRetryQueueNums()).isEqualTo(2);
+        assertThat(captor.getValue().getRetryMaxTimes()).isEqualTo(8);
+    }
+
     @Test
     void deleteConsumerGroupUsesSelectedInstanceAdmin() throws Exception {
         TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new 
MybatisConfiguration(), ""), RmqGroup.class);
diff --git a/web/src/api/consumerGroups.test.ts 
b/web/src/api/consumerGroups.test.ts
index 8ad59ece1..a25044ea1 100644
--- a/web/src/api/consumerGroups.test.ts
+++ b/web/src/api/consumerGroups.test.ts
@@ -20,12 +20,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } 
from 'vitest';
 import client from './client';
 import {
   getConsumerGroup,
+  getConsumerGroupSettings,
   getConsumerProgress,
   getConsumerSubscriptions,
   listConsumerGroupPage,
   listConsumerGroups,
   deleteConsumerGroup,
   resetConsumerOffset,
+  updateConsumerGroupSettings,
 } from './metadata';
 
 const mock = new MockAdapter(client);
@@ -124,4 +126,26 @@ describe('consumer groups API contract', () => {
 
     await expect(deleteConsumerGroup(group.name, 
'instance-1')).resolves.toBeUndefined();
   });
+
+  it('gets and updates settings in the selected Apache instance', async () => {
+    const settings = { groupName: group.name, retryQueueNums: 2, 
retryMaxTimes: 8 };
+    mock
+      .onGet(`/groups/${encodeURIComponent(group.name)}/settings`, {
+        params: { instanceId: 'instance-1' },
+      })
+      .reply(200, { code: 200, data: settings });
+    await expect(getConsumerGroupSettings(group.name, 
'instance-1')).resolves.toEqual(settings);
+
+    const payload = {
+      instanceId: 'instance-1',
+      name: group.name,
+      retryQueueNums: 2,
+      retryMaxTimes: 8,
+    };
+    mock.onPost('/groups/settings').reply((config) => {
+      expect(JSON.parse(config.data)).toEqual(payload);
+      return [200, { code: 200, data: settings }];
+    });
+    await 
expect(updateConsumerGroupSettings(payload)).resolves.toEqual(settings);
+  });
 });
diff --git a/web/src/api/metadata.ts b/web/src/api/metadata.ts
index 003fc52cb..662ea21ba 100644
--- a/web/src/api/metadata.ts
+++ b/web/src/api/metadata.ts
@@ -111,6 +111,12 @@ export interface ConsumerGroupDetail extends ConsumerGroup 
{
   instances: ConsumerInstance[];
 }
 
+export interface ConsumerGroupSettings {
+  groupName: string;
+  retryQueueNums: number;
+  retryMaxTimes: number;
+}
+
 export interface QueueProgress {
   topic: string;
   broker: string;
@@ -274,6 +280,21 @@ export async function createConsumerGroup(data: 
Partial<ConsumerGroup>) {
   return res.data.data;
 }
 
+export async function getConsumerGroupSettings(name: string, instanceId: 
string) {
+  const res = await client.get<{ data: ConsumerGroupSettings }>(
+    `/groups/${encodeURIComponent(name)}/settings`,
+    { params: { instanceId } },
+  );
+  return res.data.data;
+}
+
+export async function updateConsumerGroupSettings(
+  data: Omit<ConsumerGroupSettings, 'groupName'> & { instanceId: string; name: 
string },
+) {
+  const res = await client.post<{ data: ConsumerGroupSettings 
}>('/groups/settings', data);
+  return res.data.data;
+}
+
 export async function deleteConsumerGroup(name: string, instanceId?: string) {
   await client.post('/groups/delete', { name, ...(instanceId ? { instanceId } 
: {}) });
 }
diff --git a/web/src/pages/instance/consumer.tsx 
b/web/src/pages/instance/consumer.tsx
index ea364b57c..6001176a1 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -54,6 +54,7 @@ import {
   ListBullets,
   Info,
   ArrowsClockwise,
+  SlidersHorizontal,
 } from '@phosphor-icons/react';
 import { ImportOutlined, ExportOutlined, DeleteOutlined, SyncOutlined } from 
'@ant-design/icons';
 import type { ColumnsType } from 'antd/es/table';
@@ -81,6 +82,8 @@ import {
   getConsumerSubscriptions,
   listConsumerGroupPage,
   resetConsumerOffset,
+  getConsumerGroupSettings,
+  updateConsumerGroupSettings,
 } from '../../services/consumerService';
 import { useInstanceFilter } from '../../hooks/useInstanceFilter';
 import {
@@ -193,6 +196,10 @@ const ConsumerPageContent = ({
   const [sortKey, setSortKey] = useState<string>('name_asc');
   const [modalOpen, setModalOpen] = useState(false);
   const [selectedGroup, setSelectedGroup] = useState<ConsumerGroup | 
null>(null);
+  const [settingsGroup, setSettingsGroup] = useState<ConsumerGroup | 
null>(null);
+  const [settingsLoading, setSettingsLoading] = useState(false);
+  const [settingsSubmitting, setSettingsSubmitting] = useState(false);
+  const [settingsForm] = Form.useForm<{ retryQueueNums: number; retryMaxTimes: 
number }>();
   const [createModalOpen, setCreateModalOpen] = useState(false);
   const [form] = Form.useForm();
   const [dataTypeValue, setDataTypeValue] = useState<string | 
undefined>(undefined);
@@ -350,6 +357,47 @@ const ConsumerPageContent = ({
     void loadProgress(group.name);
   };
 
+  const openSettingsModal = async (group: ConsumerGroup) => {
+    if (!selectedInstanceId) return;
+    setSettingsGroup(group);
+    setSettingsLoading(true);
+    try {
+      const settings = await getConsumerGroupSettings(group.name, 
selectedInstanceId);
+      settingsForm.setFieldsValue(settings);
+    } catch {
+      setSettingsGroup(null);
+      message.error('加载消费组配置失败,请稍后重试');
+    } finally {
+      setSettingsLoading(false);
+    }
+  };
+
+  const saveSettings = async () => {
+    if (!settingsGroup || !selectedInstanceId) return;
+    const values = await settingsForm.validateFields();
+    setSettingsSubmitting(true);
+    try {
+      const saved = await updateConsumerGroupSettings({
+        instanceId: selectedInstanceId,
+        name: settingsGroup.name,
+        ...values,
+      });
+      setGroups((current) =>
+        current.map((group) =>
+          group.name === settingsGroup.name
+            ? { ...group, retryMaxTimes: saved.retryMaxTimes }
+            : group,
+        ),
+      );
+      message.success('消费组配置已保存');
+      setSettingsGroup(null);
+    } catch {
+      message.error('保存消费组配置失败,请稍后重试');
+    } finally {
+      setSettingsSubmitting(false);
+    }
+  };
+
   const selectedDiagnosticKey = selectedGroup
     ? diagnosticCacheKey(selectedInstanceId, selectedGroup.name)
     : '';
@@ -638,6 +686,17 @@ const ConsumerPageContent = ({
       width: 210,
       render: (_: unknown, record: ConsumerGroup) => (
         <Flex gap={6}>
+          <Button
+            size="small"
+            icon={<SlidersHorizontal size={14} />}
+            disabled={isCloudInstance}
+            onClick={(e) => {
+              e.stopPropagation();
+              void openSettingsModal(record);
+            }}
+          >
+            配置
+          </Button>
           <Button
             size="small"
             icon={<Eye size={14} />}
@@ -1426,6 +1485,37 @@ const ConsumerPageContent = ({
         )}
       </Modal>
 
+      <Modal
+        title="消费组配置"
+        open={Boolean(settingsGroup)}
+        onCancel={() => setSettingsGroup(null)}
+        onOk={() => void saveSettings()}
+        confirmLoading={settingsSubmitting}
+        okText="保存"
+        cancelText="取消"
+        destroyOnHidden
+      >
+        <Form form={settingsForm} layout="vertical" style={{ marginTop: 16 }}>
+          <Form.Item label="Group 名称">
+            <Text strong>{settingsGroup?.name}</Text>
+          </Form.Item>
+          <Form.Item
+            label="重试队列数"
+            name="retryQueueNums"
+            rules={[{ required: true, message: '请输入重试队列数' }]}
+          >
+            <InputNumber min={1} max={128} style={{ width: '100%' }} 
disabled={settingsLoading} />
+          </Form.Item>
+          <Form.Item
+            label="最大重试次数"
+            name="retryMaxTimes"
+            rules={[{ required: true, message: '请输入最大重试次数' }]}
+          >
+            <InputNumber min={1} max={128} style={{ width: '100%' }} 
disabled={settingsLoading} />
+          </Form.Item>
+        </Form>
+      </Modal>
+
       {/* ═══════════════════════════════════════════
          Consumer Stack Modal
          ═══════════════════════════════════════════ */}
diff --git a/web/src/services/consumerService.ts 
b/web/src/services/consumerService.ts
index 7ef632a68..db00f75a5 100644
--- a/web/src/services/consumerService.ts
+++ b/web/src/services/consumerService.ts
@@ -4,6 +4,7 @@ import type {
   ConsumerGroup,
   ConsumerGroupPageQuery,
   ConsumerGroupQuery,
+  ConsumerGroupSettings,
   ConsumerGroupDetail,
   ConsumerStackTrace,
   PageResult,
@@ -104,6 +105,26 @@ export async function getConsumerGroup(
   return normalizeConsumerGroup(await metadataApi.getConsumerGroup(name, 
instanceId));
 }
 
+export async function getConsumerGroupSettings(
+  name: string,
+  instanceId: string,
+): Promise<ConsumerGroupSettings> {
+  if (isMockMode()) return { groupName: name, retryQueueNums: 1, 
retryMaxTimes: 16 };
+  return metadataApi.getConsumerGroupSettings(name, instanceId);
+}
+
+export async function updateConsumerGroupSettings(
+  data: Omit<ConsumerGroupSettings, 'groupName'> & { instanceId: string; name: 
string },
+) {
+  if (isMockMode())
+    return {
+      groupName: data.name,
+      retryQueueNums: data.retryQueueNums,
+      retryMaxTimes: data.retryMaxTimes,
+    };
+  return metadataApi.updateConsumerGroupSettings(data);
+}
+
 export async function getConsumerSubscriptions(
   name: string,
   instanceId?: string,

Reply via email to