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(