From 43545aa715407ae6de4ba77d285a9e4cab930551 Mon Sep 17 00:00:00 2001
From: lxp <2770281812@qq.com>
Date: Wed, 8 Jul 2026 18:29:51 +0800
Subject: [PATCH] =?UTF-8?q?=E7=BB=93=E6=9E=84=E5=92=8C=E5=8A=9F=E8=83=BD?=
=?UTF-8?q?=E7=9A=84=E5=88=A0=E9=99=A4=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/cad-model-annotation-api.md | 76 +++++++++++++++++-
.../controller/CadAnnotationController.java | 29 +++++++
.../service/CadAnnotationService.java | 10 +++
.../impl/CadAnnotationServiceImpl.java | 47 +++++++++++
.../CadAnnotationControllerTest.java | 19 +++++
.../CadAnnotationServiceIntegrationTest.java | 80 +++++++++++++++++++
6 files changed, 259 insertions(+), 2 deletions(-)
diff --git a/docs/cad-model-annotation-api.md b/docs/cad-model-annotation-api.md
index 245f71d..8d41756 100644
--- a/docs/cad-model-annotation-api.md
+++ b/docs/cad-model-annotation-api.md
@@ -386,7 +386,43 @@
`CadStructure`
-### 4.5 CadStructure 响应字段
+### 4.5 删除 CAD 结构
+
+`DELETE /api/cad-models/{modelId}/structures/{structureId}`
+
+按模型归属校验后逻辑删除结构。删除后该结构不会再出现在结构列表和结构详情中;后端会同步清理该结构关联的建模历史关系,以及功能中引用该结构的关系。
+
+#### Path 参数
+
+| 参数 | 类型 | 必填 | 约束 | 说明 |
+| --- | --- | --- | --- | --- |
+| `modelId` | number | 是 | 正整数 | 模型 ID |
+| `structureId` | number | 是 | 正整数 | 结构 ID,必须归属当前模型且未逻辑删除 |
+
+#### 请求体
+
+无。
+
+#### 响应 data
+
+无。由于响应字段全局忽略 `null`,成功时通常不返回 `data` 字段。
+
+```json
+{
+ "code": "OK",
+ "message": "success",
+ "requestId": "b3f8..."
+}
+```
+
+#### 失败说明
+
+| HTTP 状态码 | code | 场景 |
+| --- | --- | --- |
+| 404 | `NOT_FOUND` | 模型不存在,或结构不存在、已删除、不是当前模型下的结构 |
+| 422 | `INVALID_REQUEST` | `modelId` 或 `structureId` 不是正整数 |
+
+### 4.6 CadStructure 响应字段
```json
{
@@ -547,7 +583,43 @@
`CadFunction`
-### 5.5 CadFunction 响应字段
+### 5.5 删除 CAD 功能
+
+`DELETE /api/cad-models/{modelId}/functions/{functionId}`
+
+按模型归属校验后逻辑删除功能。删除后该功能不会再出现在功能列表和功能详情中;后端会同步清理该功能下的结构关系。
+
+#### Path 参数
+
+| 参数 | 类型 | 必填 | 约束 | 说明 |
+| --- | --- | --- | --- | --- |
+| `modelId` | number | 是 | 正整数 | 模型 ID |
+| `functionId` | number | 是 | 正整数 | 功能 ID,必须归属当前模型且未逻辑删除 |
+
+#### 请求体
+
+无。
+
+#### 响应 data
+
+无。由于响应字段全局忽略 `null`,成功时通常不返回 `data` 字段。
+
+```json
+{
+ "code": "OK",
+ "message": "success",
+ "requestId": "b3f8..."
+}
+```
+
+#### 失败说明
+
+| HTTP 状态码 | code | 场景 |
+| --- | --- | --- |
+| 404 | `NOT_FOUND` | 模型不存在,或功能不存在、已删除、不是当前模型下的功能 |
+| 422 | `INVALID_REQUEST` | `modelId` 或 `functionId` 不是正整数 |
+
+### 5.6 CadFunction 响应字段
```json
{
diff --git a/src/main/java/com/cadlabel/controller/CadAnnotationController.java b/src/main/java/com/cadlabel/controller/CadAnnotationController.java
index c744b5b..d9fa94f 100644
--- a/src/main/java/com/cadlabel/controller/CadAnnotationController.java
+++ b/src/main/java/com/cadlabel/controller/CadAnnotationController.java
@@ -15,6 +15,7 @@ import jakarta.validation.constraints.Positive;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -94,6 +95,20 @@ public class CadAnnotationController {
return ApiResponse.success(cadAnnotationService.getStructure(modelId, structureId));
}
+ /**
+ * 删除 CAD 结构。
+ *
+ *
按模型归属校验后逻辑删除结构,并清理结构下的关联关系。
+ */
+ @DeleteMapping("/structures/{structureId}")
+ public ApiResponse deleteStructure(
+ @PathVariable @Positive Long modelId,
+ @PathVariable @Positive Long structureId
+ ) {
+ cadAnnotationService.deleteStructure(modelId, structureId);
+ return ApiResponse.success(null);
+ }
+
/**
* 创建 CAD 功能。
*
@@ -143,4 +158,18 @@ public class CadAnnotationController {
) {
return ApiResponse.success(cadAnnotationService.getFunction(modelId, functionId));
}
+
+ /**
+ * 删除 CAD 功能。
+ *
+ * 按模型归属校验后逻辑删除功能,并清理功能下的结构关系。
+ */
+ @DeleteMapping("/functions/{functionId}")
+ public ApiResponse deleteFunction(
+ @PathVariable @Positive Long modelId,
+ @PathVariable @Positive Long functionId
+ ) {
+ cadAnnotationService.deleteFunction(modelId, functionId);
+ return ApiResponse.success(null);
+ }
}
diff --git a/src/main/java/com/cadlabel/service/CadAnnotationService.java b/src/main/java/com/cadlabel/service/CadAnnotationService.java
index 42670c5..38c92a4 100644
--- a/src/main/java/com/cadlabel/service/CadAnnotationService.java
+++ b/src/main/java/com/cadlabel/service/CadAnnotationService.java
@@ -43,6 +43,11 @@ public interface CadAnnotationService {
*/
CadStructureResponse getStructure(Long modelId, Long structureId);
+ /**
+ * 逻辑删除结构,并清理结构关联关系。
+ */
+ void deleteStructure(Long modelId, Long structureId);
+
/**
* 创建功能并建立功能与结构的关系。
*/
@@ -62,4 +67,9 @@ public interface CadAnnotationService {
* 查询功能详情。
*/
CadFunctionResponse getFunction(Long modelId, Long functionId);
+
+ /**
+ * 逻辑删除功能,并清理功能关联关系。
+ */
+ void deleteFunction(Long modelId, Long functionId);
}
diff --git a/src/main/java/com/cadlabel/service/impl/CadAnnotationServiceImpl.java b/src/main/java/com/cadlabel/service/impl/CadAnnotationServiceImpl.java
index f74a628..9540392 100644
--- a/src/main/java/com/cadlabel/service/impl/CadAnnotationServiceImpl.java
+++ b/src/main/java/com/cadlabel/service/impl/CadAnnotationServiceImpl.java
@@ -199,6 +199,31 @@ public class CadAnnotationServiceImpl implements CadAnnotationService {
return toStructureResponse(requireStructure(modelId, structureId));
}
+ /**
+ * 逻辑删除结构,并清理结构关联的建模历史关系和功能引用关系。
+ */
+ @Override
+ @Transactional
+ public void deleteStructure(Long modelId, Long structureId) {
+ requireStructure(modelId, structureId);
+
+ int historyRelationCount = structureHistoryRelMapper.delete(Wrappers.lambdaQuery(CadStructureHistoryRel.class)
+ .eq(CadStructureHistoryRel::getModelId, modelId)
+ .eq(CadStructureHistoryRel::getStructureId, structureId));
+ int functionRelationCount = functionStructureRelMapper.delete(Wrappers.lambdaQuery(CadFunctionStructureRel.class)
+ .eq(CadFunctionStructureRel::getModelId, modelId)
+ .eq(CadFunctionStructureRel::getStructureId, structureId));
+ structureMapper.update(null, Wrappers.lambdaUpdate(CadStructure.class)
+ .eq(CadStructure::getId, structureId)
+ .eq(CadStructure::getModelId, modelId)
+ .eq(CadStructure::getDeleted, 0)
+ .set(CadStructure::getDeleted, 1)
+ .set(CadStructure::getUpdatedAt, LocalDateTime.now()));
+
+ log.info("Deleted CAD structure. modelId={}, structureId={}, historyRelationCount={}, functionRelationCount={}",
+ modelId, structureId, historyRelationCount, functionRelationCount);
+ }
+
/**
* 创建功能并绑定结构。
*
@@ -289,6 +314,28 @@ public class CadAnnotationServiceImpl implements CadAnnotationService {
return toFunctionResponse(requireFunction(modelId, functionId));
}
+ /**
+ * 逻辑删除功能,并清理功能关联的结构关系。
+ */
+ @Override
+ @Transactional
+ public void deleteFunction(Long modelId, Long functionId) {
+ requireFunction(modelId, functionId);
+
+ int structureRelationCount = functionStructureRelMapper.delete(Wrappers.lambdaQuery(CadFunctionStructureRel.class)
+ .eq(CadFunctionStructureRel::getModelId, modelId)
+ .eq(CadFunctionStructureRel::getFunctionId, functionId));
+ functionMapper.update(null, Wrappers.lambdaUpdate(CadFunction.class)
+ .eq(CadFunction::getId, functionId)
+ .eq(CadFunction::getModelId, modelId)
+ .eq(CadFunction::getDeleted, 0)
+ .set(CadFunction::getDeleted, 1)
+ .set(CadFunction::getUpdatedAt, LocalDateTime.now()));
+
+ log.info("Deleted CAD function. modelId={}, functionId={}, structureRelationCount={}",
+ modelId, functionId, structureRelationCount);
+ }
+
/**
* 校验模型存在并返回模型记录。
*/
diff --git a/src/test/java/com/cadlabel/controller/CadAnnotationControllerTest.java b/src/test/java/com/cadlabel/controller/CadAnnotationControllerTest.java
index cf0742c..87a75d3 100644
--- a/src/test/java/com/cadlabel/controller/CadAnnotationControllerTest.java
+++ b/src/test/java/com/cadlabel/controller/CadAnnotationControllerTest.java
@@ -28,6 +28,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -145,6 +146,15 @@ class CadAnnotationControllerTest {
.andExpect(jsonPath("$.data.id").value(100));
}
+ @Test
+ void deleteStructureReturnsUnifiedResponse() throws Exception {
+ mockMvc.perform(delete("/api/cad-models/1/structures/100"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value("OK"));
+
+ verify(cadAnnotationService).deleteStructure(1L, 100L);
+ }
+
@Test
void createAndUpdateFunctionBindStructureIds() throws Exception {
given(cadAnnotationService.createFunction(eq(1L), any())).willReturn(sampleFunction(200L, "固定功能"));
@@ -208,6 +218,15 @@ class CadAnnotationControllerTest {
.andExpect(jsonPath("$.data.id").value(200));
}
+ @Test
+ void deleteFunctionReturnsUnifiedResponse() throws Exception {
+ mockMvc.perform(delete("/api/cad-models/1/functions/200"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value("OK"));
+
+ verify(cadAnnotationService).deleteFunction(1L, 200L);
+ }
+
@Test
void createStructureRejectsEmptyHistoryIdsBeforeService() throws Exception {
mockMvc.perform(post("/api/cad-models/1/structures")
diff --git a/src/test/java/com/cadlabel/service/impl/CadAnnotationServiceIntegrationTest.java b/src/test/java/com/cadlabel/service/impl/CadAnnotationServiceIntegrationTest.java
index 97ba58d..c8b6943 100644
--- a/src/test/java/com/cadlabel/service/impl/CadAnnotationServiceIntegrationTest.java
+++ b/src/test/java/com/cadlabel/service/impl/CadAnnotationServiceIntegrationTest.java
@@ -262,6 +262,49 @@ class CadAnnotationServiceIntegrationTest {
)).isEqualTo(1L);
}
+ @Test
+ void deleteStructureMarksDeletedAndRemovesRelations() {
+ CadStructureResponse structure = cadAnnotationService.createStructure(1L, new CreateCadStructureRequest(
+ "加强筋",
+ "rib",
+ null,
+ null,
+ null,
+ null,
+ List.of(10L)
+ ));
+ CadFunctionResponse cadFunction = cadAnnotationService.createFunction(1L, new CreateCadFunctionRequest(
+ "固定功能",
+ "mounting",
+ null,
+ null,
+ List.of(structure.id())
+ ));
+
+ cadAnnotationService.deleteStructure(1L, structure.id());
+
+ assertThat(jdbcTemplate.queryForObject(
+ "SELECT deleted FROM cad_structure WHERE id = ?",
+ Integer.class,
+ structure.id()
+ )).isEqualTo(1);
+ assertThat(cadAnnotationService.listStructures(1L)).isEmpty();
+ assertThat(cadAnnotationService.getFunction(1L, cadFunction.id()).structureItems()).isEmpty();
+ assertThat(jdbcTemplate.queryForObject(
+ "SELECT COUNT(*) FROM cad_structure_history_rel WHERE structure_id = ?",
+ Long.class,
+ structure.id()
+ )).isEqualTo(0L);
+ assertThat(jdbcTemplate.queryForObject(
+ "SELECT COUNT(*) FROM cad_function_structure_rel WHERE structure_id = ?",
+ Long.class,
+ structure.id()
+ )).isEqualTo(0L);
+ assertThatThrownBy(() -> cadAnnotationService.getStructure(1L, structure.id()))
+ .isInstanceOf(BizException.class)
+ .hasMessage("结构不存在");
+ }
+
@Test
void createAndUpdateFunctionReplaceStructureRelations() {
CadStructureResponse first = cadAnnotationService.createStructure(1L, new CreateCadStructureRequest(
@@ -308,6 +351,43 @@ class CadAnnotationServiceIntegrationTest {
assertThat(updated.structureItems()).extracting(item -> item.structureId()).containsExactly(first.id());
}
+ @Test
+ void deleteFunctionMarksDeletedAndRemovesRelations() {
+ CadStructureResponse structure = cadAnnotationService.createStructure(1L, new CreateCadStructureRequest(
+ "加强筋",
+ "rib",
+ null,
+ null,
+ null,
+ null,
+ List.of(10L)
+ ));
+ CadFunctionResponse cadFunction = cadAnnotationService.createFunction(1L, new CreateCadFunctionRequest(
+ "固定功能",
+ "mounting",
+ null,
+ null,
+ List.of(structure.id())
+ ));
+
+ cadAnnotationService.deleteFunction(1L, cadFunction.id());
+
+ assertThat(jdbcTemplate.queryForObject(
+ "SELECT deleted FROM cad_function WHERE id = ?",
+ Integer.class,
+ cadFunction.id()
+ )).isEqualTo(1);
+ assertThat(cadAnnotationService.listFunctions(1L)).isEmpty();
+ assertThat(jdbcTemplate.queryForObject(
+ "SELECT COUNT(*) FROM cad_function_structure_rel WHERE function_id = ?",
+ Long.class,
+ cadFunction.id()
+ )).isEqualTo(0L);
+ assertThatThrownBy(() -> cadAnnotationService.getFunction(1L, cadFunction.id()))
+ .isInstanceOf(BizException.class)
+ .hasMessage("功能不存在");
+ }
+
@Test
void queryStructuresAndFunctionsExcludeDeletedAndKeepRelationOrder() {
CadStructureResponse active = cadAnnotationService.createStructure(1L, new CreateCadStructureRequest(