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 e45a1e5da docs: align the write contracts and deploy docs with what 
the server does (#5438)
e45a1e5da is described below

commit e45a1e5da5bb47608aa8751a99c82a52c9812a24
Author: 烤化の初雪 <[email protected]>
AuthorDate: Fri Oct 9 17:47:48 2026 +0800

    docs: align the write contracts and deploy docs with what the server does 
(#5438)
    
    Documentation and the contract tests that pin it, same author.
    
    - `docs/api-spec.md` §5.5 / §5.8 / §6.6 / §6.7 / §6.8 request and response 
tables now match the DTOs
      the server validates, including the required `instanceId` on topic 
creation. (#5438)
    - The five unimplemented NameServer write operations are marked as such: 
each validates the cluster
      and address and then throws a fixed `501`, so the documented success 
branch is unreachable. §4.4
      (restart Broker) is the same family and is now marked the same way. 
(#5435)
    - `deploy/README.md` gains the idempotent `docker network create` step for 
the local compose
      quickstart, since `docker-compose.yml` declares `rocketmq_net` as 
external. (#5431)
    - `CONTRIBUTING.md` / `README.md` / `README_zh.md` no longer claim the 
backend port is reachable after
      a full-stack start: 6789 serves the frontend and proxies `/api`, while 
the backend container listens
      on 8888 inside the compose network only. (#5439)
    
    New `GroupWriteRequestValidationTest` (4) and `DeleteTopicDTOTest` (3) pin 
the corrected tables;
    7 tests green, 0 checkstyle violations. Merged on top of today's spec 
changes - the DLQ `Body Base64`
    column (#5205) and the `/api/clients` section (#5453) are untouched.
    
    Folded in #5431, #5435 and #5439.
---
 CONTRIBUTING.md                                    |  6 +-
 README.md                                          |  2 +-
 README_zh.md                                       |  2 +-
 deploy/README.md                                   |  5 +-
 docs/api-spec.md                                   | 62 +++++++++++----
 .../group/GroupWriteRequestValidationTest.java     | 90 ++++++++++++++++++++++
 .../studio/instance/topic/DeleteTopicDTOTest.java  | 59 ++++++++++++++
 7 files changed, 205 insertions(+), 21 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 531055c61..4b2e2ffce 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -37,8 +37,10 @@ Recommended background reading:
   docker compose -f deploy/docker-compose.yml up -d --build
 ```
 
-Studio is then available at <http://127.0.0.1:6789> (frontend) and 
<http://127.0.0.1:8888>
-(backend). See [`deploy/README.md`](deploy/README.md) for the configuration 
options.
+Studio is then available at <http://127.0.0.1:6789> (frontend, which also 
proxies `/api` to the
+backend). The backend container listens on 8888 but is only reachable inside 
the compose network,
+so that URL answers only when you run the backend directly (see below). See
+[`deploy/README.md`](deploy/README.md) for the configuration options.
 
 ### Run the backend only
 
diff --git a/README.md b/README.md
index 0a4576e03..1b020eed0 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ default deployment. Import `upgrade-demo-instance.sql` first 
and then `upgrade-d
 target the current numeric-ID schema and are idempotent. They are sample-data 
loaders, not upgrade migrations,
 and should never be imported into a production database.
 
-**Studio ports:** Frontend 6789 (Nginx), Backend 8888 (Spring Boot)
+**Studio ports:** Frontend 6789 (Nginx, also proxies `/api`), Backend 8888 
(Spring Boot, container-internal — published only when the backend runs outside 
compose)
 
 To enable login protection for a shared environment, copy 
`deploy/.env.example` to
 `deploy/.env`, set `STUDIO_AUTH_LOGIN_REQUIRED=true`, and configure
diff --git a/README_zh.md b/README_zh.md
index a6827f93e..46560ee41 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -41,7 +41,7 @@ docker compose -f deploy/docker-compose.yml up -d --build
 RocketMQ 集群。要管理真实资源,请注册一个指向你自己 RocketMQ 的实例,或启动下方的
 「内置 RocketMQ 集群(可选)」。
 
-**Studio 服务端口:** 前端 6789(Nginx)、后端 8888(Spring Boot)
+**Studio 服务端口:** 前端 6789(Nginx,同时代理 `/api`)、后端 8888(Spring Boot,仅容器内可达;后端在 
compose 之外直接运行时才对外暴露)
 
 共享环境可复制 `deploy/.env.example` 为 `deploy/.env`,设置
 `STUDIO_AUTH_LOGIN_REQUIRED=true`,并配置 `STUDIO_AUTH_ADMIN_USERNAME` /
diff --git a/deploy/README.md b/deploy/README.md
index f080791eb..978a8de77 100644
--- a/deploy/README.md
+++ b/deploy/README.md
@@ -30,10 +30,13 @@ MAVEN_IMAGE=maven:3.9.9-eclipse-temurin-21
 
 ## 本地 Docker Compose
 
-复制示例配置后启动:
+`docker-compose.yml` 中的 `mysql` 与 `rocketmq-server` 加入共享网络 `rocketmq_net`(声明为
+`external`),因此首次启动前需要先创建该网络,否则 Compose 会以
+`network rocketmq_net declared as external, but could not be found` 终止:
 
 ```bash
 cp deploy/.env.example deploy/.env
+docker network inspect rocketmq_net >/dev/null 2>&1 || docker network create 
rocketmq_net
 cd deploy && docker compose up -d --build
 ```
 
diff --git a/docs/api-spec.md b/docs/api-spec.md
index a53e229ff..289ad8b4f 100644
--- a/docs/api-spec.md
+++ b/docs/api-spec.md
@@ -533,7 +533,16 @@ POST /api/clusters/:clusterId/brokers/:name/restart
 | `clusterId` | `string` | 集群 ID |
 | `name` | `string` | Broker 名称 |
 
-**Response `data`:** `{ success: boolean, message: string }`
+**Response `data`:** `{ message: string }`
+
+接口先校验集群存在、且该集群下登记了这个 Broker,再把动作交给集群 Provider。当前 Provider 尚未实现 Broker 
重启,校验通过后固定抛出 `501`,因此上面的成功分支不可达,调用方不应依赖任何 `success` 字段。
+
+**错误响应:**
+
+| HTTP 状态 | 场景 |
+|-----------|------|
+| `404` | 集群不存在,或该集群下没有这个 Broker |
+| `501` | 当前集群 Provider 未实现 Broker 重启 |
 
 ### 4.5 创建 NameServer
 
@@ -541,6 +550,8 @@ POST /api/clusters/:clusterId/brokers/:name/restart
 POST /api/nameservers/create
 ```
 
+> **未实现**:与 §4.11 的重启 Proxy 相同,本接口在完成集群与地址校验后固定抛出 
`501`,因此下表中的成功响应不会出现。NameServer 注册表的查询与增删改由 `GET /api/nameservers` 与 `POST 
/api/nameservers/registry/{create,update,delete}` 提供,本 spec 尚未收录。
+
 **Request Body:**
 
 | 字段 | 类型 | 必填 | 说明 |
@@ -556,6 +567,8 @@ POST /api/nameservers/create
 POST /api/nameservers/update
 ```
 
+> **未实现**:与 §4.11 的重启 Proxy 相同,本接口在完成集群与地址校验后固定抛出 
`501`,因此下表中的成功响应不会出现。NameServer 注册表的查询与增删改由 `GET /api/nameservers` 与 `POST 
/api/nameservers/registry/{create,update,delete}` 提供,本 spec 尚未收录。
+
 **Request Body:**
 
 | 字段 | 类型 | 必填 | 说明 |
@@ -572,6 +585,8 @@ POST /api/nameservers/update
 POST /api/nameservers/restart
 ```
 
+> **未实现**:与 §4.11 的重启 Proxy 相同,本接口在完成集群与地址校验后固定抛出 
`501`,因此下表中的成功响应不会出现。NameServer 注册表的查询与增删改由 `GET /api/nameservers` 与 `POST 
/api/nameservers/registry/{create,update,delete}` 提供,本 spec 尚未收录。
+
 **Request Body:**
 
 | 字段 | 类型 | 必填 | 说明 |
@@ -586,6 +601,8 @@ POST /api/nameservers/restart
 POST /api/nameservers/upgrade
 ```
 
+> **未实现**:与 §4.11 的重启 Proxy 相同,本接口在完成集群与地址校验后固定抛出 
`501`,因此下表中的成功响应不会出现。NameServer 注册表的查询与增删改由 `GET /api/nameservers` 与 `POST 
/api/nameservers/registry/{create,update,delete}` 提供,本 spec 尚未收录。
+
 **Request Body:**
 
 | 字段 | 类型 | 必填 | 说明 |
@@ -600,6 +617,8 @@ POST /api/nameservers/upgrade
 POST /api/nameservers/delete
 ```
 
+> **未实现**:与 §4.11 的重启 Proxy 相同,本接口在完成集群与地址校验后固定抛出 
`501`,因此下表中的成功响应不会出现。NameServer 注册表的查询与增删改由 `GET /api/nameservers` 与 `POST 
/api/nameservers/registry/{create,update,delete}` 提供,本 spec 尚未收录。
+
 **Request Body:**
 
 | 字段 | 类型 | 必填 | 说明 |
@@ -939,12 +958,13 @@ POST /api/topics/create
 
 | 字段 | 类型 | 必填 | 说明 |
 |------|------|------|------|
+| `instanceId` | `string` | 是 | 所属实例 ID,缺失返回 `400 instanceId is required` |
 | `name` | `string` | 是 | Topic 名称 |
 | `namespace` | `string` | 否 | 命名空间 |
 | `type` | `string` | 是 | 类型 |
-| `clusterId` | `string` | 是 | 所属集群 |
-| `writeQueues` | `number` | 是 | 写队列数 |
-| `readQueues` | `number` | 是 | 读队列数 |
+| `clusterId` | `string` | 否 | 所属集群;Apache 写路径按实例与所有权记录解析目标集群,不读取该字段 |
+| `writeQueues` | `number` | 否 | 写队列数,须 ≥ 0;缺省或非正数时取 8 |
+| `readQueues` | `number` | 否 | 读队列数,须 ≥ 0;缺省或非正数时取 8 |
 | `perm` | `string` | 否 | 权限,默认 `RW` |
 | `remark` | `string` | 否 | 备注 |
 
@@ -960,12 +980,13 @@ POST /api/topics/update
 
 | 字段 | 类型 | 必填 | 说明 |
 |------|------|------|------|
+| `instanceId` | `string` | 是 | 所属实例 ID,缺失返回 `400 instanceId is required` |
 | `name` | `string` | 是 | Topic 名称(不可修改) |
 | `namespace` | `string` | 否 | 命名空间 |
 | `type` | `string` | 是 | 类型 |
-| `clusterId` | `string` | 是 | 所属集群 |
-| `writeQueues` | `number` | 是 | 写队列数 |
-| `readQueues` | `number` | 是 | 读队列数 |
+| `clusterId` | `string` | 否 | 所属集群;Apache 写路径按实例与所有权记录解析目标集群,不读取该字段 |
+| `writeQueues` | `number` | 否 | 写队列数,须 ≥ 0;缺省或非正数时保留现值 |
+| `readQueues` | `number` | 否 | 读队列数,须 ≥ 0;缺省或非正数时保留现值 |
 | `perm` | `string` | 否 | 权限,默认 `RW` |
 | `remark` | `string` | 否 | 备注 |
 
@@ -982,6 +1003,7 @@ POST /api/topics/delete
 | 字段 | 类型 | 必填 | 说明 |
 |------|------|------|------|
 | `name` | `string` | 是 | Topic 名称 |
+| `instanceId` | `string` | 是 | 实例 ID(`DeleteTopicDTO` 校验非空) |
 
 **Response `data`:** `null`
 
@@ -1027,6 +1049,7 @@ POST /api/topics/send
 
 | 字段 | 类型 | 必填 | 说明 |
 |------|------|------|------|
+| `instanceId` | `string` | 是 | 实例 ID(消息写入该实例) |
 | `topic` | `string` | 是 | Topic 名称 |
 | `tag` | `string` | 否 | 消息 Tag |
 | `key` | `string` | 否 | 消息 Key(用于消息查询) |
@@ -1165,13 +1188,16 @@ POST /api/groups/create
 
 | 字段 | 类型 | 必填 | 说明 |
 |------|------|------|------|
+| `instanceId` | `string` | 是 | 所属实例 ID,缺失返回 `400 instanceId is required` |
 | `name` | `string` | 是 | 消费组名称 |
 | `namespace` | `string` | 否 | 命名空间 |
 | `clusterId` | `string` | 是 | 所属集群 |
 | `subscriptionMode` | `string` | 是 | `Push` / `Pop` |
 | `consumeType` | `string` | 是 | `CLUSTERING` / `BROADCASTING` |
-| `subscribedTopics` | `string[]` | 否 | 订阅 Topic |
-| `retryMaxTimes` | `number` | 否 | 最大重试次数 |
+| `subscriptionDataType` | `string` | 否 | 订阅数据类型: `NORMAL` / `FIFO` / `DELAY` 
/ `TRANSACTION` |
+| `deliveryOrderType` | `string` | 否 | 顺序类型(FIFO 时): `PARTITON_ORDER` / 
`MESSAGES_ORDER` |
+| `retryMaxTimes` | `number` | 否 | 最大重试次数,须 ≥ 0 |
+| `delaySeconds` | `number` | 否 | 延迟秒数,须 ≥ 0 |
 
 **Response `data`:** `ConsumerGroup`
 
@@ -1186,6 +1212,7 @@ POST /api/groups/delete
 | 字段 | 类型 | 必填 | 说明 |
 |------|------|------|------|
 | `name` | `string` | 是 | 消费组名称 |
+| `instanceId` | `string` | 是 | 实例 ID(`DeleteConsumerGroupDTO` 校验非空) |
 
 **Response `data`:** `null`
 
@@ -1200,8 +1227,9 @@ POST /api/groups/reset-offset
 | 字段 | 类型 | 必填 | 说明 |
 |------|------|------|------|
 | `name` | `string` | 是 | 消费组名称 |
-| `timestamp` | `string` | 是 | 重置到指定时间 (ISO 8601) |
-| `topic` | `string` | 否 | 指定 Topic,不传则全部重置 |
+| `instanceId` | `string` | 是 | 实例 ID |
+| `timestamp` | `number` | 是 | 重置到的时间(Unix 毫秒时间戳,必须为正数) |
+| `topic` | `string` | 是 | 要重置的 Topic;本接口没有「全部重置」分支,必须显式指定 |
 
 **Response `data`:** `null`
 
@@ -1211,11 +1239,12 @@ POST /api/groups/reset-offset
 POST /api/groups/import
 ```
 
-**Request Body:** `multipart/form-data`
+**Request Body:** `application/json`
 
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| `file` | `File` | JSON 配置文件 |
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `instanceId` | `string` | 是 | 目标实例 ID |
+| `groups` | `CreateConsumerGroupDTO[]` | 是 | 待导入的消费组,单次最多 100 条 |
 
 **Response `data`:**
 
@@ -1223,7 +1252,8 @@ POST /api/groups/import
 |------|------|------|
 | `imported` | `number` | 成功导入数量 |
 | `failed` | `number` | 失败数量 |
-| `errors` | `string[]` | 错误信息列表 |
+| `groups` | `ConsumerGroupVO[]` | 成功导入的消费组 |
+| `failures` | `{ index, name, message }[]` | 每条失败的原始下标、消费组名称与原因 |
 
 ### 6.9 导出消费组配置
 
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/group/GroupWriteRequestValidationTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/group/GroupWriteRequestValidationTest.java
new file mode 100644
index 000000000..61d121892
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/group/GroupWriteRequestValidationTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.Validation;
+import jakarta.validation.Validator;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Pins the request contract api-spec 6.6-6.8 documents: the owning instance 
is mandatory, a reset
+ * needs a topic and a positive epoch-millis timestamp, and an import is a 
bounded JSON list.
+ */
+class GroupWriteRequestValidationTest {
+
+    private final Validator validator = 
Validation.buildDefaultValidatorFactory().getValidator();
+
+    @Test
+    void deleteShouldRequireTheOwningInstanceTest() {
+        DeleteConsumerGroupDTO request = new DeleteConsumerGroupDTO();
+        request.setName("cg-orders");
+
+        assertThat(validator.validate(request))
+                .extracting(violation -> violation.getMessage())
+                .containsExactly("instanceId is required");
+    }
+
+    @Test
+    void 
resetOffsetShouldRequireTheInstanceTheTopicAndAPositiveTimestampTest() {
+        ResetConsumerOffsetDTO request = new ResetConsumerOffsetDTO();
+        request.setName("cg-orders");
+        request.setTimestamp(0L);
+
+        assertThat(validator.validate(request))
+                .extracting(violation -> violation.getMessage())
+                .containsExactlyInAnyOrder("instanceId is required", "topic is 
required",
+                        "timestamp must be positive");
+    }
+
+    @Test
+    void resetOffsetShouldAcceptAnEpochMillisTimestampTest() {
+        ResetConsumerOffsetDTO request = new ResetConsumerOffsetDTO();
+        request.setInstanceId("instance-a");
+        request.setName("cg-orders");
+        request.setTopic("orders");
+        request.setTimestamp(1_780_000_000_000L);
+
+        assertThat(validator.validate(request)).isEmpty();
+    }
+
+    @Test
+    void importShouldRequireTheInstanceAndANonEmptyBoundedListTest() {
+        ImportConsumerGroupsDTO empty = new ImportConsumerGroupsDTO();
+        assertThat(validator.validate(empty))
+                .extracting(violation -> violation.getMessage())
+                .containsExactlyInAnyOrder("instanceId is required", "groups 
is required");
+
+        List<CreateConsumerGroupDTO> tooMany = new ArrayList<>();
+        for (int index = 0; index < 101; index++) {
+            CreateConsumerGroupDTO group = new CreateConsumerGroupDTO();
+            group.setName("cg-" + index);
+            tooMany.add(group);
+        }
+        ImportConsumerGroupsDTO oversized = new ImportConsumerGroupsDTO();
+        oversized.setInstanceId("instance-a");
+        oversized.setGroups(tooMany);
+
+        assertThat(validator.validate(oversized))
+                .extracting(violation -> violation.getMessage())
+                .containsExactly("At most 100 consumer groups are allowed per 
import");
+    }
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTOTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTOTest.java
new file mode 100644
index 000000000..19d9575ae
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTOTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.topic;
+
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DeleteTopicDTOTest {
+
+    private final Validator validator = 
Validation.buildDefaultValidatorFactory().getValidator();
+
+    @Test
+    void shouldRequireTheOwningInstanceTest() {
+        // The console sends both fields and api-spec 5.5 documents instanceId 
as required, because
+        // deleting topic names is ambiguous across the instances that share a 
cluster.
+        DeleteTopicDTO request = new DeleteTopicDTO();
+        request.setName("orders");
+
+        assertThat(validator.validate(request))
+                .extracting(violation -> violation.getMessage())
+                .containsExactly("instanceId is required");
+    }
+
+    @Test
+    void shouldRequireTheTopicNameTest() {
+        DeleteTopicDTO request = new DeleteTopicDTO();
+        request.setInstanceId("instance-a");
+
+        assertThat(validator.validate(request))
+                .extracting(violation -> violation.getMessage())
+                .containsExactly("name is required");
+    }
+
+    @Test
+    void shouldAcceptBothFieldsTest() {
+        DeleteTopicDTO request = new DeleteTopicDTO();
+        request.setInstanceId("instance-a");
+        request.setName("orders");
+
+        assertThat(validator.validate(request)).isEmpty();
+    }
+}

Reply via email to