From bc18930b7a41387e726eb3c759fe866b708b38e3 Mon Sep 17 00:00:00 2001 From: lxp <2770281812@qq.com> Date: Wed, 8 Jul 2026 16:01:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=87=E6=B3=A8=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CadAnnotationController.java | 146 ++++ .../cad/annotation/CadFunctionResponse.java | 17 + .../CadFunctionStructureItemResponse.java | 10 + .../CadModelHistoryAnnotationResponse.java | 15 + .../CadStructureHistoryItemResponse.java | 11 + .../cad/annotation/CadStructureResponse.java | 19 + .../annotation/CreateCadFunctionRequest.java | 16 + .../annotation/CreateCadStructureRequest.java | 18 + .../SaveHistoryAnnotationRequest.java | 8 + .../annotation/UpdateCadFunctionRequest.java | 14 + .../annotation/UpdateCadStructureRequest.java | 16 + .../java/com/cadlabel/entity/CadFunction.java | 40 + .../entity/CadFunctionStructureRel.java | 34 + .../java/com/cadlabel/entity/CadModel.java | 33 + .../com/cadlabel/entity/CadModelHistory.java | 23 + .../entity/CadModelHistoryAnnotation.java | 37 + .../com/cadlabel/entity/CadStructure.java | 46 ++ .../entity/CadStructureHistoryRel.java | 34 + .../cadlabel/mapper/CadFunctionMapper.java | 9 + .../mapper/CadFunctionStructureRelMapper.java | 9 + .../CadModelHistoryAnnotationMapper.java | 9 + .../mapper/CadStructureHistoryRelMapper.java | 9 + .../cadlabel/mapper/CadStructureMapper.java | 9 + .../service/CadAnnotationService.java | 65 ++ .../impl/CadAnnotationServiceImpl.java | 698 ++++++++++++++++++ src/main/resources/application.yml | 2 +- .../CadAnnotationControllerTest.java | 281 +++++++ .../CadAnnotationServiceIntegrationTest.java | 413 +++++++++++ 28 files changed, 2040 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/cadlabel/controller/CadAnnotationController.java create mode 100644 src/main/java/com/cadlabel/dto/cad/annotation/CadFunctionResponse.java create mode 100644 src/main/java/com/cadlabel/dto/cad/annotation/CadFunctionStructureItemResponse.java create mode 100644 src/main/java/com/cadlabel/dto/cad/annotation/CadModelHistoryAnnotationResponse.java create mode 100644 src/main/java/com/cadlabel/dto/cad/annotation/CadStructureHistoryItemResponse.java create mode 100644 src/main/java/com/cadlabel/dto/cad/annotation/CadStructureResponse.java create mode 100644 src/main/java/com/cadlabel/dto/cad/annotation/CreateCadFunctionRequest.java create mode 100644 src/main/java/com/cadlabel/dto/cad/annotation/CreateCadStructureRequest.java create mode 100644 src/main/java/com/cadlabel/dto/cad/annotation/SaveHistoryAnnotationRequest.java create mode 100644 src/main/java/com/cadlabel/dto/cad/annotation/UpdateCadFunctionRequest.java create mode 100644 src/main/java/com/cadlabel/dto/cad/annotation/UpdateCadStructureRequest.java create mode 100644 src/main/java/com/cadlabel/entity/CadFunction.java create mode 100644 src/main/java/com/cadlabel/entity/CadFunctionStructureRel.java create mode 100644 src/main/java/com/cadlabel/entity/CadModelHistoryAnnotation.java create mode 100644 src/main/java/com/cadlabel/entity/CadStructure.java create mode 100644 src/main/java/com/cadlabel/entity/CadStructureHistoryRel.java create mode 100644 src/main/java/com/cadlabel/mapper/CadFunctionMapper.java create mode 100644 src/main/java/com/cadlabel/mapper/CadFunctionStructureRelMapper.java create mode 100644 src/main/java/com/cadlabel/mapper/CadModelHistoryAnnotationMapper.java create mode 100644 src/main/java/com/cadlabel/mapper/CadStructureHistoryRelMapper.java create mode 100644 src/main/java/com/cadlabel/mapper/CadStructureMapper.java create mode 100644 src/main/java/com/cadlabel/service/CadAnnotationService.java create mode 100644 src/main/java/com/cadlabel/service/impl/CadAnnotationServiceImpl.java create mode 100644 src/test/java/com/cadlabel/controller/CadAnnotationControllerTest.java create mode 100644 src/test/java/com/cadlabel/service/impl/CadAnnotationServiceIntegrationTest.java diff --git a/src/main/java/com/cadlabel/controller/CadAnnotationController.java b/src/main/java/com/cadlabel/controller/CadAnnotationController.java new file mode 100644 index 0000000..c744b5b --- /dev/null +++ b/src/main/java/com/cadlabel/controller/CadAnnotationController.java @@ -0,0 +1,146 @@ +package com.cadlabel.controller; + +import com.cadlabel.common.api.ApiResponse; +import com.cadlabel.dto.cad.annotation.CadFunctionResponse; +import com.cadlabel.dto.cad.annotation.CadModelHistoryAnnotationResponse; +import com.cadlabel.dto.cad.annotation.CadStructureResponse; +import com.cadlabel.dto.cad.annotation.CreateCadFunctionRequest; +import com.cadlabel.dto.cad.annotation.CreateCadStructureRequest; +import com.cadlabel.dto.cad.annotation.SaveHistoryAnnotationRequest; +import com.cadlabel.dto.cad.annotation.UpdateCadFunctionRequest; +import com.cadlabel.dto.cad.annotation.UpdateCadStructureRequest; +import com.cadlabel.service.CadAnnotationService; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Validated +@RestController +@RequestMapping("/api/cad-models/{modelId}") +@RequiredArgsConstructor +public class CadAnnotationController { + + private final CadAnnotationService cadAnnotationService; + + /** + * 保存建模历史步骤标注。 + * + *

同一个建模历史只能有一条标注;重复保存时按 historyId 覆盖原有标注内容。

+ */ + @PostMapping("/histories/{historyId}/annotation") + public ApiResponse saveHistoryAnnotation( + @PathVariable @Positive Long modelId, + @PathVariable @Positive Long historyId, + @Valid @RequestBody(required = false) SaveHistoryAnnotationRequest request + ) { + return ApiResponse.success(cadAnnotationService.saveHistoryAnnotation(modelId, historyId, request)); + } + + /** + * 创建 CAD 结构。 + * + *

创建时必须选择至少一个建模历史步骤,关系排序按 historyIds 请求顺序生成。

+ */ + @PostMapping("/structures") + public ApiResponse createStructure( + @PathVariable @Positive Long modelId, + @Valid @RequestBody CreateCadStructureRequest request + ) { + return ApiResponse.success(cadAnnotationService.createStructure(modelId, request)); + } + + /** + * 修改 CAD 结构。 + * + *

普通字段支持部分更新;传入 historyIds 时按最终集合全量替换结构下的建模历史关系。

+ */ + @PostMapping("/structures/{structureId}") + public ApiResponse updateStructure( + @PathVariable @Positive Long modelId, + @PathVariable @Positive Long structureId, + @Valid @RequestBody(required = false) UpdateCadStructureRequest request + ) { + return ApiResponse.success(cadAnnotationService.updateStructure(modelId, structureId, request)); + } + + /** + * 查询模型下的结构列表。 + * + *

只返回未逻辑删除的结构,并携带结构关联的建模历史步骤。

+ */ + @GetMapping("/structures") + public ApiResponse> listStructures(@PathVariable @Positive Long modelId) { + return ApiResponse.success(cadAnnotationService.listStructures(modelId)); + } + + /** + * 查询结构详情。 + * + *

用于结构编辑回显,返回结构基础信息和已关联的建模历史步骤。

+ */ + @GetMapping("/structures/{structureId}") + public ApiResponse getStructure( + @PathVariable @Positive Long modelId, + @PathVariable @Positive Long structureId + ) { + return ApiResponse.success(cadAnnotationService.getStructure(modelId, structureId)); + } + + /** + * 创建 CAD 功能。 + * + *

创建时必须选择至少一个结构,关系排序按 structureIds 请求顺序生成。

+ */ + @PostMapping("/functions") + public ApiResponse createFunction( + @PathVariable @Positive Long modelId, + @Valid @RequestBody CreateCadFunctionRequest request + ) { + return ApiResponse.success(cadAnnotationService.createFunction(modelId, request)); + } + + /** + * 修改 CAD 功能。 + * + *

普通字段支持部分更新;传入 structureIds 时按最终集合全量替换功能下的结构关系。

+ */ + @PostMapping("/functions/{functionId}") + public ApiResponse updateFunction( + @PathVariable @Positive Long modelId, + @PathVariable @Positive Long functionId, + @Valid @RequestBody(required = false) UpdateCadFunctionRequest request + ) { + return ApiResponse.success(cadAnnotationService.updateFunction(modelId, functionId, request)); + } + + /** + * 查询模型下的功能列表。 + * + *

只返回未逻辑删除的功能,并携带功能关联的结构。

+ */ + @GetMapping("/functions") + public ApiResponse> listFunctions(@PathVariable @Positive Long modelId) { + return ApiResponse.success(cadAnnotationService.listFunctions(modelId)); + } + + /** + * 查询功能详情。 + * + *

用于功能编辑回显,返回功能基础信息和已关联的结构。

+ */ + @GetMapping("/functions/{functionId}") + public ApiResponse getFunction( + @PathVariable @Positive Long modelId, + @PathVariable @Positive Long functionId + ) { + return ApiResponse.success(cadAnnotationService.getFunction(modelId, functionId)); + } +} diff --git a/src/main/java/com/cadlabel/dto/cad/annotation/CadFunctionResponse.java b/src/main/java/com/cadlabel/dto/cad/annotation/CadFunctionResponse.java new file mode 100644 index 0000000..8574acf --- /dev/null +++ b/src/main/java/com/cadlabel/dto/cad/annotation/CadFunctionResponse.java @@ -0,0 +1,17 @@ +package com.cadlabel.dto.cad.annotation; + +import java.time.LocalDateTime; +import java.util.List; + +public record CadFunctionResponse( + Long id, + Long modelId, + String functionName, + String functionType, + String goalDescription, + String remark, + List structureItems, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { +} diff --git a/src/main/java/com/cadlabel/dto/cad/annotation/CadFunctionStructureItemResponse.java b/src/main/java/com/cadlabel/dto/cad/annotation/CadFunctionStructureItemResponse.java new file mode 100644 index 0000000..4850a79 --- /dev/null +++ b/src/main/java/com/cadlabel/dto/cad/annotation/CadFunctionStructureItemResponse.java @@ -0,0 +1,10 @@ +package com.cadlabel.dto.cad.annotation; + +public record CadFunctionStructureItemResponse( + Long relationId, + Long structureId, + String structureName, + String structureType, + Integer sortOrder +) { +} diff --git a/src/main/java/com/cadlabel/dto/cad/annotation/CadModelHistoryAnnotationResponse.java b/src/main/java/com/cadlabel/dto/cad/annotation/CadModelHistoryAnnotationResponse.java new file mode 100644 index 0000000..7fcfea3 --- /dev/null +++ b/src/main/java/com/cadlabel/dto/cad/annotation/CadModelHistoryAnnotationResponse.java @@ -0,0 +1,15 @@ +package com.cadlabel.dto.cad.annotation; + +import java.time.LocalDateTime; + +public record CadModelHistoryAnnotationResponse( + Long id, + Long modelId, + Long historyId, + String modelingDescription, + String supplementDescription, + String remark, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { +} diff --git a/src/main/java/com/cadlabel/dto/cad/annotation/CadStructureHistoryItemResponse.java b/src/main/java/com/cadlabel/dto/cad/annotation/CadStructureHistoryItemResponse.java new file mode 100644 index 0000000..f0f5340 --- /dev/null +++ b/src/main/java/com/cadlabel/dto/cad/annotation/CadStructureHistoryItemResponse.java @@ -0,0 +1,11 @@ +package com.cadlabel.dto.cad.annotation; + +public record CadStructureHistoryItemResponse( + Long relationId, + Long historyId, + Integer historyIndex, + String historyName, + String operationType, + Integer sortOrder +) { +} diff --git a/src/main/java/com/cadlabel/dto/cad/annotation/CadStructureResponse.java b/src/main/java/com/cadlabel/dto/cad/annotation/CadStructureResponse.java new file mode 100644 index 0000000..bd677ce --- /dev/null +++ b/src/main/java/com/cadlabel/dto/cad/annotation/CadStructureResponse.java @@ -0,0 +1,19 @@ +package com.cadlabel.dto.cad.annotation; + +import java.time.LocalDateTime; +import java.util.List; + +public record CadStructureResponse( + Long id, + Long modelId, + String structureName, + String structureType, + String purpose, + String solution, + String reason, + String remark, + List historyItems, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { +} diff --git a/src/main/java/com/cadlabel/dto/cad/annotation/CreateCadFunctionRequest.java b/src/main/java/com/cadlabel/dto/cad/annotation/CreateCadFunctionRequest.java new file mode 100644 index 0000000..b577b18 --- /dev/null +++ b/src/main/java/com/cadlabel/dto/cad/annotation/CreateCadFunctionRequest.java @@ -0,0 +1,16 @@ +package com.cadlabel.dto.cad.annotation; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import java.util.List; + +public record CreateCadFunctionRequest( + @NotBlank @Size(max = 255) String functionName, + @Size(max = 64) String functionType, + String goalDescription, + String remark, + @NotEmpty List<@Positive Long> structureIds +) { +} diff --git a/src/main/java/com/cadlabel/dto/cad/annotation/CreateCadStructureRequest.java b/src/main/java/com/cadlabel/dto/cad/annotation/CreateCadStructureRequest.java new file mode 100644 index 0000000..5666298 --- /dev/null +++ b/src/main/java/com/cadlabel/dto/cad/annotation/CreateCadStructureRequest.java @@ -0,0 +1,18 @@ +package com.cadlabel.dto.cad.annotation; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import java.util.List; + +public record CreateCadStructureRequest( + @NotBlank @Size(max = 255) String structureName, + @Size(max = 64) String structureType, + @Size(max = 1024) String purpose, + @Size(max = 1024) String solution, + String reason, + String remark, + @NotEmpty List<@Positive Long> historyIds +) { +} diff --git a/src/main/java/com/cadlabel/dto/cad/annotation/SaveHistoryAnnotationRequest.java b/src/main/java/com/cadlabel/dto/cad/annotation/SaveHistoryAnnotationRequest.java new file mode 100644 index 0000000..98dfbf5 --- /dev/null +++ b/src/main/java/com/cadlabel/dto/cad/annotation/SaveHistoryAnnotationRequest.java @@ -0,0 +1,8 @@ +package com.cadlabel.dto.cad.annotation; + +public record SaveHistoryAnnotationRequest( + String modelingDescription, + String supplementDescription, + String remark +) { +} diff --git a/src/main/java/com/cadlabel/dto/cad/annotation/UpdateCadFunctionRequest.java b/src/main/java/com/cadlabel/dto/cad/annotation/UpdateCadFunctionRequest.java new file mode 100644 index 0000000..3bcf012 --- /dev/null +++ b/src/main/java/com/cadlabel/dto/cad/annotation/UpdateCadFunctionRequest.java @@ -0,0 +1,14 @@ +package com.cadlabel.dto.cad.annotation; + +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import java.util.List; + +public record UpdateCadFunctionRequest( + @Size(max = 255) String functionName, + @Size(max = 64) String functionType, + String goalDescription, + String remark, + List<@Positive Long> structureIds +) { +} diff --git a/src/main/java/com/cadlabel/dto/cad/annotation/UpdateCadStructureRequest.java b/src/main/java/com/cadlabel/dto/cad/annotation/UpdateCadStructureRequest.java new file mode 100644 index 0000000..eb7549d --- /dev/null +++ b/src/main/java/com/cadlabel/dto/cad/annotation/UpdateCadStructureRequest.java @@ -0,0 +1,16 @@ +package com.cadlabel.dto.cad.annotation; + +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import java.util.List; + +public record UpdateCadStructureRequest( + @Size(max = 255) String structureName, + @Size(max = 64) String structureType, + @Size(max = 1024) String purpose, + @Size(max = 1024) String solution, + String reason, + String remark, + List<@Positive Long> historyIds +) { +} diff --git a/src/main/java/com/cadlabel/entity/CadFunction.java b/src/main/java/com/cadlabel/entity/CadFunction.java new file mode 100644 index 0000000..00262be --- /dev/null +++ b/src/main/java/com/cadlabel/entity/CadFunction.java @@ -0,0 +1,40 @@ +package com.cadlabel.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.time.LocalDateTime; +import lombok.Data; + +@Data +@TableName("cad_function") +public class CadFunction { + + /** 功能ID。 */ + @TableId(type = IdType.AUTO) + private Long id; + + /** 关联 cad_model.id。 */ + private Long modelId; + + /** 功能名称。 */ + private String functionName; + + /** 功能类型。 */ + private String functionType; + + /** 功能目标或说明。 */ + private String goalDescription; + + /** 备注。 */ + private String remark; + + /** 创建时间。 */ + private LocalDateTime createdAt; + + /** 更新时间。 */ + private LocalDateTime updatedAt; + + /** 逻辑删除标记:0 未删除,1 已删除。 */ + private Integer deleted; +} diff --git a/src/main/java/com/cadlabel/entity/CadFunctionStructureRel.java b/src/main/java/com/cadlabel/entity/CadFunctionStructureRel.java new file mode 100644 index 0000000..d8034b6 --- /dev/null +++ b/src/main/java/com/cadlabel/entity/CadFunctionStructureRel.java @@ -0,0 +1,34 @@ +package com.cadlabel.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.time.LocalDateTime; +import lombok.Data; + +@Data +@TableName("cad_function_structure_rel") +public class CadFunctionStructureRel { + + /** 功能-结构关系ID。 */ + @TableId(type = IdType.AUTO) + private Long id; + + /** 关联 cad_model.id。 */ + private Long modelId; + + /** 关联 cad_function.id。 */ + private Long functionId; + + /** 关联 cad_structure.id。 */ + private Long structureId; + + /** 功能内结构排序。 */ + private Integer sortOrder; + + /** 创建时间。 */ + private LocalDateTime createdAt; + + /** 更新时间。 */ + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/cadlabel/entity/CadModel.java b/src/main/java/com/cadlabel/entity/CadModel.java index 0cc3925..fdbc494 100644 --- a/src/main/java/com/cadlabel/entity/CadModel.java +++ b/src/main/java/com/cadlabel/entity/CadModel.java @@ -10,22 +10,55 @@ import lombok.Data; @TableName("cad_model") public class CadModel { + /** 模型ID。 */ @TableId(type = IdType.AUTO) private Long id; + + /** 分类ID。 */ private Long categoryId; + + /** 模型名称。 */ private String modelName; + + /** 源 SolidWorks 文件名。 */ private String sourceSwFilename; + + /** 源 SolidWorks 文件存储路径。 */ private String sourceSwPath; + + /** 特征树 JSON 文件路径。 */ private String featureTreeJsonPath; + + /** 模型视图资源路径。 */ private String viewPath; + + /** 应用场景。 */ private String applicationScenario; + + /** 材料。 */ private String material; + + /** 制造方式。 */ private String manufacturingMethod; + + /** 单位。 */ private String unit; + + /** 用户需求描述。 */ private String userRequirement; + + /** 整体设计说明。 */ private String designDescription; + + /** 转换状态。 */ private String convertStatus; + + /** 转换或处理错误信息。 */ private String errorMessage; + + /** 创建时间。 */ private LocalDateTime createdAt; + + /** 更新时间。 */ private LocalDateTime updatedAt; } diff --git a/src/main/java/com/cadlabel/entity/CadModelHistory.java b/src/main/java/com/cadlabel/entity/CadModelHistory.java index d342b19..473b7ad 100644 --- a/src/main/java/com/cadlabel/entity/CadModelHistory.java +++ b/src/main/java/com/cadlabel/entity/CadModelHistory.java @@ -10,17 +10,40 @@ import lombok.Data; @TableName("cad_model_history") public class CadModelHistory { + /** 建模历史ID。 */ @TableId(type = IdType.AUTO) private Long id; + + /** 关联 cad_model.id。 */ private Long modelId; + + /** 源特征ID。 */ private String sourceFeatureId; + + /** 建模历史步骤序号。 */ private Integer historyIndex; + + /** 建模历史步骤名称。 */ private String historyName; + + /** 操作类型。 */ private String operationType; + + /** STEP 文件路径。 */ private String stepPath; + + /** GLB 文件路径。 */ private String glbPath; + + /** 转换状态。 */ private String convertStatus; + + /** 转换或处理错误信息。 */ private String errorMessage; + + /** 创建时间。 */ private LocalDateTime createdAt; + + /** 更新时间。 */ private LocalDateTime updatedAt; } diff --git a/src/main/java/com/cadlabel/entity/CadModelHistoryAnnotation.java b/src/main/java/com/cadlabel/entity/CadModelHistoryAnnotation.java new file mode 100644 index 0000000..267ded1 --- /dev/null +++ b/src/main/java/com/cadlabel/entity/CadModelHistoryAnnotation.java @@ -0,0 +1,37 @@ +package com.cadlabel.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.time.LocalDateTime; +import lombok.Data; + +@Data +@TableName("cad_model_history_annotation") +public class CadModelHistoryAnnotation { + + /** 步骤标注ID。 */ + @TableId(type = IdType.AUTO) + private Long id; + + /** 关联 cad_model.id。 */ + private Long modelId; + + /** 关联 cad_model_history.id。 */ + private Long historyId; + + /** 建模说明:这个 CAD 操作自身做了什么。 */ + private String modelingDescription; + + /** 补充描述:几何范围、解析异常、与前后步骤关系。 */ + private String supplementDescription; + + /** 备注:人工复核说明、异常说明。 */ + private String remark; + + /** 创建时间。 */ + private LocalDateTime createdAt; + + /** 更新时间。 */ + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/cadlabel/entity/CadStructure.java b/src/main/java/com/cadlabel/entity/CadStructure.java new file mode 100644 index 0000000..792123b --- /dev/null +++ b/src/main/java/com/cadlabel/entity/CadStructure.java @@ -0,0 +1,46 @@ +package com.cadlabel.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.time.LocalDateTime; +import lombok.Data; + +@Data +@TableName("cad_structure") +public class CadStructure { + + /** 结构ID。 */ + @TableId(type = IdType.AUTO) + private Long id; + + /** 关联 cad_model.id。 */ + private Long modelId; + + /** 结构名称。 */ + private String structureName; + + /** 结构类型。 */ + private String structureType; + + /** 目的。 */ + private String purpose; + + /** 方案。 */ + private String solution; + + /** 原因。 */ + private String reason; + + /** 备注。 */ + private String remark; + + /** 创建时间。 */ + private LocalDateTime createdAt; + + /** 更新时间。 */ + private LocalDateTime updatedAt; + + /** 逻辑删除标记:0 未删除,1 已删除。 */ + private Integer deleted; +} diff --git a/src/main/java/com/cadlabel/entity/CadStructureHistoryRel.java b/src/main/java/com/cadlabel/entity/CadStructureHistoryRel.java new file mode 100644 index 0000000..5d173a0 --- /dev/null +++ b/src/main/java/com/cadlabel/entity/CadStructureHistoryRel.java @@ -0,0 +1,34 @@ +package com.cadlabel.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.time.LocalDateTime; +import lombok.Data; + +@Data +@TableName("cad_structure_history_rel") +public class CadStructureHistoryRel { + + /** 结构-步骤关系ID。 */ + @TableId(type = IdType.AUTO) + private Long id; + + /** 关联 cad_model.id。 */ + private Long modelId; + + /** 关联 cad_structure.id。 */ + private Long structureId; + + /** 关联 cad_model_history.id。 */ + private Long historyId; + + /** 结构内步骤排序。 */ + private Integer sortOrder; + + /** 创建时间。 */ + private LocalDateTime createdAt; + + /** 更新时间。 */ + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/cadlabel/mapper/CadFunctionMapper.java b/src/main/java/com/cadlabel/mapper/CadFunctionMapper.java new file mode 100644 index 0000000..b295eee --- /dev/null +++ b/src/main/java/com/cadlabel/mapper/CadFunctionMapper.java @@ -0,0 +1,9 @@ +package com.cadlabel.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.cadlabel.entity.CadFunction; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface CadFunctionMapper extends BaseMapper { +} diff --git a/src/main/java/com/cadlabel/mapper/CadFunctionStructureRelMapper.java b/src/main/java/com/cadlabel/mapper/CadFunctionStructureRelMapper.java new file mode 100644 index 0000000..0455401 --- /dev/null +++ b/src/main/java/com/cadlabel/mapper/CadFunctionStructureRelMapper.java @@ -0,0 +1,9 @@ +package com.cadlabel.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.cadlabel.entity.CadFunctionStructureRel; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface CadFunctionStructureRelMapper extends BaseMapper { +} diff --git a/src/main/java/com/cadlabel/mapper/CadModelHistoryAnnotationMapper.java b/src/main/java/com/cadlabel/mapper/CadModelHistoryAnnotationMapper.java new file mode 100644 index 0000000..a771d62 --- /dev/null +++ b/src/main/java/com/cadlabel/mapper/CadModelHistoryAnnotationMapper.java @@ -0,0 +1,9 @@ +package com.cadlabel.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.cadlabel.entity.CadModelHistoryAnnotation; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface CadModelHistoryAnnotationMapper extends BaseMapper { +} diff --git a/src/main/java/com/cadlabel/mapper/CadStructureHistoryRelMapper.java b/src/main/java/com/cadlabel/mapper/CadStructureHistoryRelMapper.java new file mode 100644 index 0000000..43d8983 --- /dev/null +++ b/src/main/java/com/cadlabel/mapper/CadStructureHistoryRelMapper.java @@ -0,0 +1,9 @@ +package com.cadlabel.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.cadlabel.entity.CadStructureHistoryRel; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface CadStructureHistoryRelMapper extends BaseMapper { +} diff --git a/src/main/java/com/cadlabel/mapper/CadStructureMapper.java b/src/main/java/com/cadlabel/mapper/CadStructureMapper.java new file mode 100644 index 0000000..544bc08 --- /dev/null +++ b/src/main/java/com/cadlabel/mapper/CadStructureMapper.java @@ -0,0 +1,9 @@ +package com.cadlabel.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.cadlabel.entity.CadStructure; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface CadStructureMapper extends BaseMapper { +} diff --git a/src/main/java/com/cadlabel/service/CadAnnotationService.java b/src/main/java/com/cadlabel/service/CadAnnotationService.java new file mode 100644 index 0000000..42670c5 --- /dev/null +++ b/src/main/java/com/cadlabel/service/CadAnnotationService.java @@ -0,0 +1,65 @@ +package com.cadlabel.service; + +import com.cadlabel.dto.cad.annotation.CadFunctionResponse; +import com.cadlabel.dto.cad.annotation.CadModelHistoryAnnotationResponse; +import com.cadlabel.dto.cad.annotation.CadStructureResponse; +import com.cadlabel.dto.cad.annotation.CreateCadFunctionRequest; +import com.cadlabel.dto.cad.annotation.CreateCadStructureRequest; +import com.cadlabel.dto.cad.annotation.SaveHistoryAnnotationRequest; +import com.cadlabel.dto.cad.annotation.UpdateCadFunctionRequest; +import com.cadlabel.dto.cad.annotation.UpdateCadStructureRequest; +import java.util.List; + +public interface CadAnnotationService { + + /** + * 保存建模历史步骤标注。 + * + *

如果指定 historyId 已存在标注,则覆盖原有标注文本;否则新建标注记录。

+ */ + CadModelHistoryAnnotationResponse saveHistoryAnnotation( + Long modelId, + Long historyId, + SaveHistoryAnnotationRequest request + ); + + /** + * 创建结构并建立结构与建模历史步骤的关系。 + */ + CadStructureResponse createStructure(Long modelId, CreateCadStructureRequest request); + + /** + * 修改结构基础信息,并在传入 historyIds 时全量替换结构与建模历史步骤的关系。 + */ + CadStructureResponse updateStructure(Long modelId, Long structureId, UpdateCadStructureRequest request); + + /** + * 查询模型下未删除的结构列表。 + */ + List listStructures(Long modelId); + + /** + * 查询结构详情。 + */ + CadStructureResponse getStructure(Long modelId, Long structureId); + + /** + * 创建功能并建立功能与结构的关系。 + */ + CadFunctionResponse createFunction(Long modelId, CreateCadFunctionRequest request); + + /** + * 修改功能基础信息,并在传入 structureIds 时全量替换功能与结构的关系。 + */ + CadFunctionResponse updateFunction(Long modelId, Long functionId, UpdateCadFunctionRequest request); + + /** + * 查询模型下未删除的功能列表。 + */ + List listFunctions(Long modelId); + + /** + * 查询功能详情。 + */ + CadFunctionResponse getFunction(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 new file mode 100644 index 0000000..f74a628 --- /dev/null +++ b/src/main/java/com/cadlabel/service/impl/CadAnnotationServiceImpl.java @@ -0,0 +1,698 @@ +package com.cadlabel.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.cadlabel.common.api.ApiCode; +import com.cadlabel.common.exception.BizException; +import com.cadlabel.dto.cad.annotation.CadFunctionResponse; +import com.cadlabel.dto.cad.annotation.CadFunctionStructureItemResponse; +import com.cadlabel.dto.cad.annotation.CadModelHistoryAnnotationResponse; +import com.cadlabel.dto.cad.annotation.CadStructureHistoryItemResponse; +import com.cadlabel.dto.cad.annotation.CadStructureResponse; +import com.cadlabel.dto.cad.annotation.CreateCadFunctionRequest; +import com.cadlabel.dto.cad.annotation.CreateCadStructureRequest; +import com.cadlabel.dto.cad.annotation.SaveHistoryAnnotationRequest; +import com.cadlabel.dto.cad.annotation.UpdateCadFunctionRequest; +import com.cadlabel.dto.cad.annotation.UpdateCadStructureRequest; +import com.cadlabel.entity.CadFunction; +import com.cadlabel.entity.CadFunctionStructureRel; +import com.cadlabel.entity.CadModel; +import com.cadlabel.entity.CadModelHistory; +import com.cadlabel.entity.CadModelHistoryAnnotation; +import com.cadlabel.entity.CadStructure; +import com.cadlabel.entity.CadStructureHistoryRel; +import com.cadlabel.mapper.CadFunctionMapper; +import com.cadlabel.mapper.CadFunctionStructureRelMapper; +import com.cadlabel.mapper.CadModelHistoryAnnotationMapper; +import com.cadlabel.mapper.CadModelHistoryMapper; +import com.cadlabel.mapper.CadModelMapper; +import com.cadlabel.mapper.CadStructureHistoryRelMapper; +import com.cadlabel.mapper.CadStructureMapper; +import com.cadlabel.service.CadAnnotationService; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +/** + * CAD 标注编排服务。 + * + *

负责建模历史标注、结构、功能以及两层关系维护。

+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CadAnnotationServiceImpl implements CadAnnotationService { + + private final CadModelMapper cadModelMapper; + private final CadModelHistoryMapper cadModelHistoryMapper; + private final CadModelHistoryAnnotationMapper annotationMapper; + private final CadStructureMapper structureMapper; + private final CadStructureHistoryRelMapper structureHistoryRelMapper; + private final CadFunctionMapper functionMapper; + private final CadFunctionStructureRelMapper functionStructureRelMapper; + + /** + * 保存建模历史步骤标注。 + * + *

先校验模型和历史步骤归属关系,再按 historyId 做插入或更新,保证前端重复保存是幂等的。

+ */ + @Override + @Transactional + public CadModelHistoryAnnotationResponse saveHistoryAnnotation( + Long modelId, + Long historyId, + SaveHistoryAnnotationRequest request + ) { + requireModel(modelId); + requireHistory(modelId, historyId); + SaveHistoryAnnotationRequest safeRequest = request == null + ? new SaveHistoryAnnotationRequest(null, null, null) + : request; + + CadModelHistoryAnnotation existing = annotationMapper.selectOne(Wrappers.lambdaQuery(CadModelHistoryAnnotation.class) + .eq(CadModelHistoryAnnotation::getHistoryId, historyId)); + if (existing == null) { + CadModelHistoryAnnotation annotation = new CadModelHistoryAnnotation(); + annotation.setModelId(modelId); + annotation.setHistoryId(historyId); + annotation.setModelingDescription(safeRequest.modelingDescription()); + annotation.setSupplementDescription(safeRequest.supplementDescription()); + annotation.setRemark(safeRequest.remark()); + log.info("Creating CAD history annotation. modelId={}, historyId={}", modelId, historyId); + annotationMapper.insert(annotation); + return toAnnotationResponse(annotationMapper.selectById(annotation.getId())); + } + + LocalDateTime now = LocalDateTime.now(); + log.info("Updating CAD history annotation. modelId={}, historyId={}, annotationId={}", + modelId, historyId, existing.getId()); + annotationMapper.update(null, Wrappers.lambdaUpdate(CadModelHistoryAnnotation.class) + .eq(CadModelHistoryAnnotation::getId, existing.getId()) + .set(CadModelHistoryAnnotation::getModelId, modelId) + .set(CadModelHistoryAnnotation::getModelingDescription, safeRequest.modelingDescription()) + .set(CadModelHistoryAnnotation::getSupplementDescription, safeRequest.supplementDescription()) + .set(CadModelHistoryAnnotation::getRemark, safeRequest.remark()) + .set(CadModelHistoryAnnotation::getUpdatedAt, now)); + return toAnnotationResponse(annotationMapper.selectById(existing.getId())); + } + + /** + * 创建结构并绑定建模历史步骤。 + * + *

结构基础记录和结构-历史关系在同一事务内写入,任一环节失败都会回滚。

+ */ + @Override + @Transactional + public CadStructureResponse createStructure(Long modelId, CreateCadStructureRequest request) { + requireModel(modelId); + if (request == null) { + throw invalidRequest("请求体不能为空"); + } + validateStructureName(request.structureName()); + validateMaxLength("structureType", request.structureType(), 64); + validateMaxLength("purpose", request.purpose(), 1024); + validateMaxLength("solution", request.solution(), 1024); + List historyIds = validateIdList("historyIds", request.historyIds()); + requireHistories(modelId, historyIds); + + CadStructure structure = new CadStructure(); + structure.setModelId(modelId); + structure.setStructureName(request.structureName()); + structure.setStructureType(defaultText(request.structureType())); + structure.setPurpose(defaultText(request.purpose())); + structure.setSolution(defaultText(request.solution())); + structure.setReason(request.reason()); + structure.setRemark(request.remark()); + structure.setDeleted(0); + + log.info("Creating CAD structure. modelId={}, historyCount={}", modelId, historyIds.size()); + structureMapper.insert(structure); + replaceStructureHistoryRelations(modelId, structure.getId(), historyIds); + return getStructure(modelId, structure.getId()); + } + + /** + * 修改结构基础信息和历史步骤关系。 + * + *

请求未传的普通字段保持原值;传入 historyIds 时会按请求数组全量替换关系。

+ */ + @Override + @Transactional + public CadStructureResponse updateStructure(Long modelId, Long structureId, UpdateCadStructureRequest request) { + CadStructure structure = requireStructure(modelId, structureId); + if (request == null) { + return toStructureResponse(structure); + } + + List updatedFields = structureUpdatedFields(request); + validateStructureUpdateRequest(request); + if (request.historyIds() != null) { + List historyIds = validateIdList("historyIds", request.historyIds()); + requireHistories(modelId, historyIds); + replaceStructureHistoryRelations(modelId, structureId, historyIds); + updatedFields.add("historyIds"); + } + + if (!updatedFields.isEmpty()) { + applyStructureUpdate(structure, request); + structure.setUpdatedAt(LocalDateTime.now()); + log.info("Updating CAD structure. modelId={}, structureId={}, fields={}", + modelId, structureId, updatedFields); + structureMapper.updateById(structure); + } + return getStructure(modelId, structureId); + } + + /** + * 查询模型下的结构列表。 + * + *

只查询 deleted=0 的结构,并为每个结构组装已关联的建模历史步骤。

+ */ + @Override + public List listStructures(Long modelId) { + requireModel(modelId); + return structureMapper.selectList(Wrappers.lambdaQuery(CadStructure.class) + .eq(CadStructure::getModelId, modelId) + .eq(CadStructure::getDeleted, 0) + .orderByAsc(CadStructure::getId)) + .stream() + .map(this::toStructureResponse) + .toList(); + } + + /** + * 查询结构详情。 + * + *

详情查询会校验结构归属当前模型,并排除已逻辑删除的结构。

+ */ + @Override + public CadStructureResponse getStructure(Long modelId, Long structureId) { + return toStructureResponse(requireStructure(modelId, structureId)); + } + + /** + * 创建功能并绑定结构。 + * + *

功能基础记录和功能-结构关系在同一事务内写入,关系排序按 structureIds 请求顺序生成。

+ */ + @Override + @Transactional + public CadFunctionResponse createFunction(Long modelId, CreateCadFunctionRequest request) { + requireModel(modelId); + if (request == null) { + throw invalidRequest("请求体不能为空"); + } + validateFunctionName(request.functionName()); + validateMaxLength("functionType", request.functionType(), 64); + List structureIds = validateIdList("structureIds", request.structureIds()); + requireStructures(modelId, structureIds); + + CadFunction cadFunction = new CadFunction(); + cadFunction.setModelId(modelId); + cadFunction.setFunctionName(request.functionName()); + cadFunction.setFunctionType(defaultText(request.functionType())); + cadFunction.setGoalDescription(request.goalDescription()); + cadFunction.setRemark(request.remark()); + cadFunction.setDeleted(0); + + log.info("Creating CAD function. modelId={}, structureCount={}", modelId, structureIds.size()); + functionMapper.insert(cadFunction); + replaceFunctionStructureRelations(modelId, cadFunction.getId(), structureIds); + return getFunction(modelId, cadFunction.getId()); + } + + /** + * 修改功能基础信息和结构关系。 + * + *

请求未传的普通字段保持原值;传入 structureIds 时会按请求数组全量替换关系。

+ */ + @Override + @Transactional + public CadFunctionResponse updateFunction(Long modelId, Long functionId, UpdateCadFunctionRequest request) { + CadFunction cadFunction = requireFunction(modelId, functionId); + if (request == null) { + return toFunctionResponse(cadFunction); + } + + List updatedFields = functionUpdatedFields(request); + validateFunctionUpdateRequest(request); + if (request.structureIds() != null) { + List structureIds = validateIdList("structureIds", request.structureIds()); + requireStructures(modelId, structureIds); + replaceFunctionStructureRelations(modelId, functionId, structureIds); + updatedFields.add("structureIds"); + } + + if (!updatedFields.isEmpty()) { + applyFunctionUpdate(cadFunction, request); + cadFunction.setUpdatedAt(LocalDateTime.now()); + log.info("Updating CAD function. modelId={}, functionId={}, fields={}", + modelId, functionId, updatedFields); + functionMapper.updateById(cadFunction); + } + return getFunction(modelId, functionId); + } + + /** + * 查询模型下的功能列表。 + * + *

只查询 deleted=0 的功能,并为每个功能组装已关联的结构。

+ */ + @Override + public List listFunctions(Long modelId) { + requireModel(modelId); + return functionMapper.selectList(Wrappers.lambdaQuery(CadFunction.class) + .eq(CadFunction::getModelId, modelId) + .eq(CadFunction::getDeleted, 0) + .orderByAsc(CadFunction::getId)) + .stream() + .map(this::toFunctionResponse) + .toList(); + } + + /** + * 查询功能详情。 + * + *

详情查询会校验功能归属当前模型,并排除已逻辑删除的功能。

+ */ + @Override + public CadFunctionResponse getFunction(Long modelId, Long functionId) { + return toFunctionResponse(requireFunction(modelId, functionId)); + } + + /** + * 校验模型存在并返回模型记录。 + */ + private CadModel requireModel(Long modelId) { + validatePositiveId(modelId, "modelId"); + CadModel model = cadModelMapper.selectById(modelId); + if (model == null) { + throw new BizException(HttpStatus.NOT_FOUND.value(), ApiCode.NOT_FOUND, "CAD 模型不存在"); + } + return model; + } + + /** + * 校验建模历史存在且归属指定模型。 + */ + private CadModelHistory requireHistory(Long modelId, Long historyId) { + validatePositiveId(historyId, "historyId"); + CadModelHistory history = cadModelHistoryMapper.selectOne(Wrappers.lambdaQuery(CadModelHistory.class) + .eq(CadModelHistory::getId, historyId) + .eq(CadModelHistory::getModelId, modelId)); + if (history == null) { + throw new BizException(HttpStatus.NOT_FOUND.value(), ApiCode.NOT_FOUND, "建模历史不存在"); + } + return history; + } + + /** + * 校验结构存在、归属指定模型且未逻辑删除。 + */ + private CadStructure requireStructure(Long modelId, Long structureId) { + requireModel(modelId); + validatePositiveId(structureId, "structureId"); + CadStructure structure = structureMapper.selectOne(Wrappers.lambdaQuery(CadStructure.class) + .eq(CadStructure::getId, structureId) + .eq(CadStructure::getModelId, modelId) + .eq(CadStructure::getDeleted, 0)); + if (structure == null) { + throw new BizException(HttpStatus.NOT_FOUND.value(), ApiCode.NOT_FOUND, "结构不存在"); + } + return structure; + } + + /** + * 校验功能存在、归属指定模型且未逻辑删除。 + */ + private CadFunction requireFunction(Long modelId, Long functionId) { + requireModel(modelId); + validatePositiveId(functionId, "functionId"); + CadFunction cadFunction = functionMapper.selectOne(Wrappers.lambdaQuery(CadFunction.class) + .eq(CadFunction::getId, functionId) + .eq(CadFunction::getModelId, modelId) + .eq(CadFunction::getDeleted, 0)); + if (cadFunction == null) { + throw new BizException(HttpStatus.NOT_FOUND.value(), ApiCode.NOT_FOUND, "功能不存在"); + } + return cadFunction; + } + + /** + * 批量校验建模历史 ID 列表全部归属指定模型。 + */ + private Map requireHistories(Long modelId, List historyIds) { + List histories = cadModelHistoryMapper.selectList(Wrappers.lambdaQuery(CadModelHistory.class) + .eq(CadModelHistory::getModelId, modelId) + .in(CadModelHistory::getId, historyIds)); + if (histories.size() != historyIds.size()) { + throw new BizException(HttpStatus.NOT_FOUND.value(), ApiCode.NOT_FOUND, "建模历史不存在"); + } + return histories.stream().collect(Collectors.toMap(CadModelHistory::getId, history -> history)); + } + + /** + * 批量校验结构 ID 列表全部归属指定模型且未逻辑删除。 + */ + private Map requireStructures(Long modelId, List structureIds) { + List structures = structureMapper.selectList(Wrappers.lambdaQuery(CadStructure.class) + .eq(CadStructure::getModelId, modelId) + .eq(CadStructure::getDeleted, 0) + .in(CadStructure::getId, structureIds)); + if (structures.size() != structureIds.size()) { + throw new BizException(HttpStatus.NOT_FOUND.value(), ApiCode.NOT_FOUND, "结构不存在"); + } + return structures.stream().collect(Collectors.toMap(CadStructure::getId, structure -> structure)); + } + + /** + * 全量替换结构与建模历史步骤关系。 + * + *

先删除结构原有关联,再按 historyIds 数组顺序写入 sortOrder。

+ */ + private void replaceStructureHistoryRelations(Long modelId, Long structureId, List historyIds) { + structureHistoryRelMapper.delete(Wrappers.lambdaQuery(CadStructureHistoryRel.class) + .eq(CadStructureHistoryRel::getStructureId, structureId)); + for (int index = 0; index < historyIds.size(); index++) { + CadStructureHistoryRel relation = new CadStructureHistoryRel(); + relation.setModelId(modelId); + relation.setStructureId(structureId); + relation.setHistoryId(historyIds.get(index)); + relation.setSortOrder(index); + structureHistoryRelMapper.insert(relation); + } + log.info("Replaced CAD structure-history relations. modelId={}, structureId={}, historyCount={}", + modelId, structureId, historyIds.size()); + } + + /** + * 全量替换功能与结构关系。 + * + *

先删除功能原有关联,再按 structureIds 数组顺序写入 sortOrder。

+ */ + private void replaceFunctionStructureRelations(Long modelId, Long functionId, List structureIds) { + functionStructureRelMapper.delete(Wrappers.lambdaQuery(CadFunctionStructureRel.class) + .eq(CadFunctionStructureRel::getFunctionId, functionId)); + for (int index = 0; index < structureIds.size(); index++) { + CadFunctionStructureRel relation = new CadFunctionStructureRel(); + relation.setModelId(modelId); + relation.setFunctionId(functionId); + relation.setStructureId(structureIds.get(index)); + relation.setSortOrder(index); + functionStructureRelMapper.insert(relation); + } + log.info("Replaced CAD function-structure relations. modelId={}, functionId={}, structureCount={}", + modelId, functionId, structureIds.size()); + } + + /** + * 将标注实体转换为接口响应。 + */ + private CadModelHistoryAnnotationResponse toAnnotationResponse(CadModelHistoryAnnotation annotation) { + return new CadModelHistoryAnnotationResponse( + annotation.getId(), + annotation.getModelId(), + annotation.getHistoryId(), + annotation.getModelingDescription(), + annotation.getSupplementDescription(), + annotation.getRemark(), + annotation.getCreatedAt(), + annotation.getUpdatedAt() + ); + } + + /** + * 将结构实体转换为接口响应。 + * + *

响应中会按 sortOrder 升序组装结构关联的建模历史步骤。

+ */ + private CadStructureResponse toStructureResponse(CadStructure structure) { + List relations = structureHistoryRelMapper.selectList(Wrappers.lambdaQuery(CadStructureHistoryRel.class) + .eq(CadStructureHistoryRel::getStructureId, structure.getId()) + .orderByAsc(CadStructureHistoryRel::getSortOrder) + .orderByAsc(CadStructureHistoryRel::getId)); + Map histories = relations.isEmpty() + ? Map.of() + : cadModelHistoryMapper.selectList(Wrappers.lambdaQuery(CadModelHistory.class) + .eq(CadModelHistory::getModelId, structure.getModelId()) + .in(CadModelHistory::getId, relations.stream().map(CadStructureHistoryRel::getHistoryId).toList())) + .stream() + .collect(Collectors.toMap(CadModelHistory::getId, history -> history)); + + List historyItems = relations.stream() + .map(relation -> { + CadModelHistory history = histories.get(relation.getHistoryId()); + return new CadStructureHistoryItemResponse( + relation.getId(), + relation.getHistoryId(), + history == null ? null : history.getHistoryIndex(), + history == null ? null : history.getHistoryName(), + history == null ? null : history.getOperationType(), + relation.getSortOrder() + ); + }) + .toList(); + + return new CadStructureResponse( + structure.getId(), + structure.getModelId(), + structure.getStructureName(), + structure.getStructureType(), + structure.getPurpose(), + structure.getSolution(), + structure.getReason(), + structure.getRemark(), + historyItems, + structure.getCreatedAt(), + structure.getUpdatedAt() + ); + } + + /** + * 将功能实体转换为接口响应。 + * + *

响应中会按 sortOrder 升序组装功能关联的未删除结构。

+ */ + private CadFunctionResponse toFunctionResponse(CadFunction cadFunction) { + List relations = functionStructureRelMapper.selectList(Wrappers.lambdaQuery(CadFunctionStructureRel.class) + .eq(CadFunctionStructureRel::getFunctionId, cadFunction.getId()) + .orderByAsc(CadFunctionStructureRel::getSortOrder) + .orderByAsc(CadFunctionStructureRel::getId)); + Map structures = relations.isEmpty() + ? Map.of() + : structureMapper.selectList(Wrappers.lambdaQuery(CadStructure.class) + .eq(CadStructure::getModelId, cadFunction.getModelId()) + .eq(CadStructure::getDeleted, 0) + .in(CadStructure::getId, relations.stream().map(CadFunctionStructureRel::getStructureId).toList())) + .stream() + .collect(Collectors.toMap(CadStructure::getId, structure -> structure)); + + List structureItems = relations.stream() + .map(relation -> { + CadStructure structure = structures.get(relation.getStructureId()); + if (structure == null) { + return null; + } + return new CadFunctionStructureItemResponse( + relation.getId(), + relation.getStructureId(), + structure.getStructureName(), + structure.getStructureType(), + relation.getSortOrder() + ); + }) + .filter(item -> item != null) + .toList(); + + return new CadFunctionResponse( + cadFunction.getId(), + cadFunction.getModelId(), + cadFunction.getFunctionName(), + cadFunction.getFunctionType(), + cadFunction.getGoalDescription(), + cadFunction.getRemark(), + structureItems, + cadFunction.getCreatedAt(), + cadFunction.getUpdatedAt() + ); + } + + /** + * 校验 ID 字段必须为正数。 + */ + private void validatePositiveId(Long id, String fieldName) { + if (id == null || id <= 0) { + throw invalidRequest(fieldName + " 必须为正数"); + } + } + + /** + * 校验关联 ID 列表非空、均为正数且不重复。 + */ + private List validateIdList(String fieldName, List ids) { + if (ids == null || ids.isEmpty()) { + throw invalidRequest(fieldName + " 不能为空"); + } + Set uniqueIds = new LinkedHashSet<>(); + for (Long id : ids) { + if (id == null || id <= 0) { + throw invalidRequest(fieldName + " 必须为正数"); + } + if (!uniqueIds.add(id)) { + throw invalidRequest(fieldName + " 不能包含重复值"); + } + } + return new ArrayList<>(uniqueIds); + } + + /** + * 校验结构修改请求中的字段长度和必填约束。 + */ + private void validateStructureUpdateRequest(UpdateCadStructureRequest request) { + if (request.structureName() != null) { + validateStructureName(request.structureName()); + } + validateMaxLength("structureType", request.structureType(), 64); + validateMaxLength("purpose", request.purpose(), 1024); + validateMaxLength("solution", request.solution(), 1024); + } + + /** + * 校验功能修改请求中的字段长度和必填约束。 + */ + private void validateFunctionUpdateRequest(UpdateCadFunctionRequest request) { + if (request.functionName() != null) { + validateFunctionName(request.functionName()); + } + validateMaxLength("functionType", request.functionType(), 64); + } + + /** + * 校验结构名称不能为空且不超过数据库字段长度。 + */ + private void validateStructureName(String structureName) { + if (!StringUtils.hasText(structureName)) { + throw invalidRequest("structureName 不能为空"); + } + validateMaxLength("structureName", structureName, 255); + } + + /** + * 校验功能名称不能为空且不超过数据库字段长度。 + */ + private void validateFunctionName(String functionName) { + if (!StringUtils.hasText(functionName)) { + throw invalidRequest("functionName 不能为空"); + } + validateMaxLength("functionName", functionName, 255); + } + + /** + * 校验字符串字段最大长度。 + */ + private void validateMaxLength(String field, String value, int maxLength) { + if (value != null && value.length() > maxLength) { + throw invalidRequest(field + " 长度不能超过 " + maxLength); + } + } + + /** + * 将可空字符串转换为数据库非空字段默认值。 + */ + private String defaultText(String value) { + return value == null ? "" : value; + } + + /** + * 收集结构修改请求中实际传入的字段名,用于日志记录。 + */ + private List structureUpdatedFields(UpdateCadStructureRequest request) { + List fields = new ArrayList<>(); + addField(fields, "structureName", request.structureName()); + addField(fields, "structureType", request.structureType()); + addField(fields, "purpose", request.purpose()); + addField(fields, "solution", request.solution()); + addField(fields, "reason", request.reason()); + addField(fields, "remark", request.remark()); + return fields; + } + + /** + * 收集功能修改请求中实际传入的字段名,用于日志记录。 + */ + private List functionUpdatedFields(UpdateCadFunctionRequest request) { + List fields = new ArrayList<>(); + addField(fields, "functionName", request.functionName()); + addField(fields, "functionType", request.functionType()); + addField(fields, "goalDescription", request.goalDescription()); + addField(fields, "remark", request.remark()); + return fields; + } + + /** + * 非空字段加入更新字段集合。 + */ + private void addField(List fields, String fieldName, Object value) { + if (value != null) { + fields.add(fieldName); + } + } + + /** + * 将结构修改请求应用到实体对象。 + */ + private void applyStructureUpdate(CadStructure structure, UpdateCadStructureRequest request) { + if (request.structureName() != null) { + structure.setStructureName(request.structureName()); + } + if (request.structureType() != null) { + structure.setStructureType(request.structureType()); + } + if (request.purpose() != null) { + structure.setPurpose(request.purpose()); + } + if (request.solution() != null) { + structure.setSolution(request.solution()); + } + if (request.reason() != null) { + structure.setReason(request.reason()); + } + if (request.remark() != null) { + structure.setRemark(request.remark()); + } + } + + /** + * 将功能修改请求应用到实体对象。 + */ + private void applyFunctionUpdate(CadFunction cadFunction, UpdateCadFunctionRequest request) { + if (request.functionName() != null) { + cadFunction.setFunctionName(request.functionName()); + } + if (request.functionType() != null) { + cadFunction.setFunctionType(request.functionType()); + } + if (request.goalDescription() != null) { + cadFunction.setGoalDescription(request.goalDescription()); + } + if (request.remark() != null) { + cadFunction.setRemark(request.remark()); + } + } + + /** + * 构造统一的请求参数错误异常。 + */ + private BizException invalidRequest(String message) { + return new BizException(HttpStatus.UNPROCESSABLE_ENTITY.value(), ApiCode.INVALID_REQUEST, message); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8c72286..0ec5eba 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,7 +14,7 @@ spring: time-zone: Asia/Shanghai server: - port: ${SERVER_PORT:8080} + port: ${SERVER_PORT:8891} shutdown: graceful forward-headers-strategy: framework compression: diff --git a/src/test/java/com/cadlabel/controller/CadAnnotationControllerTest.java b/src/test/java/com/cadlabel/controller/CadAnnotationControllerTest.java new file mode 100644 index 0000000..cf0742c --- /dev/null +++ b/src/test/java/com/cadlabel/controller/CadAnnotationControllerTest.java @@ -0,0 +1,281 @@ +package com.cadlabel.controller; + +import com.cadlabel.common.exception.GlobalExceptionHandler; +import com.cadlabel.common.web.RequestIdFilter; +import com.cadlabel.config.JacksonConfig; +import com.cadlabel.dto.cad.annotation.CadFunctionResponse; +import com.cadlabel.dto.cad.annotation.CadFunctionStructureItemResponse; +import com.cadlabel.dto.cad.annotation.CadModelHistoryAnnotationResponse; +import com.cadlabel.dto.cad.annotation.CadStructureHistoryItemResponse; +import com.cadlabel.dto.cad.annotation.CadStructureResponse; +import com.cadlabel.dto.cad.annotation.CreateCadFunctionRequest; +import com.cadlabel.dto.cad.annotation.CreateCadStructureRequest; +import com.cadlabel.dto.cad.annotation.SaveHistoryAnnotationRequest; +import com.cadlabel.dto.cad.annotation.UpdateCadFunctionRequest; +import com.cadlabel.dto.cad.annotation.UpdateCadStructureRequest; +import com.cadlabel.service.CadAnnotationService; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +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.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(CadAnnotationController.class) +@Import({GlobalExceptionHandler.class, RequestIdFilter.class, JacksonConfig.class}) +class CadAnnotationControllerTest { + + @MockitoBean + private CadAnnotationService cadAnnotationService; + + @Autowired + private MockMvc mockMvc; + + @Test + void saveHistoryAnnotationReturnsUnifiedResponse() throws Exception { + given(cadAnnotationService.saveHistoryAnnotation(eq(1L), eq(10L), any())) + .willReturn(sampleAnnotation()); + + mockMvc.perform(post("/api/cad-models/1/histories/10/annotation") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "modelingDescription": "创建底座拉伸", + "supplementDescription": "范围覆盖底面", + "remark": "人工确认" + } + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("OK")) + .andExpect(jsonPath("$.data.historyId").value(10)) + .andExpect(jsonPath("$.data.modelingDescription").value("创建底座拉伸")); + + verify(cadAnnotationService).saveHistoryAnnotation(1L, 10L, new SaveHistoryAnnotationRequest( + "创建底座拉伸", + "范围覆盖底面", + "人工确认" + )); + } + + @Test + void createStructureBindsHistoryIdsAndReturnsRelations() throws Exception { + given(cadAnnotationService.createStructure(eq(1L), any())).willReturn(sampleStructure(100L, "加强筋")); + + mockMvc.perform(post("/api/cad-models/1/structures") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "structureName": "加强筋", + "structureType": "rib", + "purpose": "提高强度", + "solution": "多段拉伸", + "reason": "受力集中", + "remark": "优先复核", + "historyIds": [10, 11] + } + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("OK")) + .andExpect(jsonPath("$.data.id").value(100)) + .andExpect(jsonPath("$.data.structureName").value("加强筋")) + .andExpect(jsonPath("$.data.historyItems[0].historyId").value(10)) + .andExpect(jsonPath("$.data.historyItems[0].sortOrder").value(0)); + + verify(cadAnnotationService).createStructure(1L, new CreateCadStructureRequest( + "加强筋", + "rib", + "提高强度", + "多段拉伸", + "受力集中", + "优先复核", + List.of(10L, 11L) + )); + } + + @Test + void updateStructureAcceptsPartialJson() throws Exception { + given(cadAnnotationService.updateStructure(eq(1L), eq(100L), any())) + .willReturn(sampleStructure(100L, "加强筋-修改")); + + mockMvc.perform(post("/api/cad-models/1/structures/100") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "structureName": "加强筋-修改", + "historyIds": [11, 10] + } + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.structureName").value("加强筋-修改")); + + verify(cadAnnotationService).updateStructure(1L, 100L, new UpdateCadStructureRequest( + "加强筋-修改", + null, + null, + null, + null, + null, + List.of(11L, 10L) + )); + } + + @Test + void listAndGetStructuresReturnUnifiedResponses() throws Exception { + given(cadAnnotationService.listStructures(1L)).willReturn(List.of(sampleStructure(100L, "加强筋"))); + given(cadAnnotationService.getStructure(1L, 100L)).willReturn(sampleStructure(100L, "加强筋")); + + mockMvc.perform(get("/api/cad-models/1/structures")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[0].id").value(100)); + + mockMvc.perform(get("/api/cad-models/1/structures/100")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.id").value(100)); + } + + @Test + void createAndUpdateFunctionBindStructureIds() throws Exception { + given(cadAnnotationService.createFunction(eq(1L), any())).willReturn(sampleFunction(200L, "固定功能")); + given(cadAnnotationService.updateFunction(eq(1L), eq(200L), any())) + .willReturn(sampleFunction(200L, "固定功能-修改")); + + mockMvc.perform(post("/api/cad-models/1/functions") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "functionName": "固定功能", + "functionType": "mounting", + "goalDescription": "固定外部零件", + "remark": "人工确认", + "structureIds": [100] + } + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.id").value(200)) + .andExpect(jsonPath("$.data.structureItems[0].structureId").value(100)); + + verify(cadAnnotationService).createFunction(1L, new CreateCadFunctionRequest( + "固定功能", + "mounting", + "固定外部零件", + "人工确认", + List.of(100L) + )); + + mockMvc.perform(post("/api/cad-models/1/functions/200") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "functionName": "固定功能-修改", + "structureIds": [100] + } + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.functionName").value("固定功能-修改")); + + verify(cadAnnotationService).updateFunction(1L, 200L, new UpdateCadFunctionRequest( + "固定功能-修改", + null, + null, + null, + List.of(100L) + )); + } + + @Test + void listAndGetFunctionsReturnUnifiedResponses() throws Exception { + given(cadAnnotationService.listFunctions(1L)).willReturn(List.of(sampleFunction(200L, "固定功能"))); + given(cadAnnotationService.getFunction(1L, 200L)).willReturn(sampleFunction(200L, "固定功能")); + + mockMvc.perform(get("/api/cad-models/1/functions")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[0].id").value(200)); + + mockMvc.perform(get("/api/cad-models/1/functions/200")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.id").value(200)); + } + + @Test + void createStructureRejectsEmptyHistoryIdsBeforeService() throws Exception { + mockMvc.perform(post("/api/cad-models/1/structures") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "structureName": "加强筋", + "historyIds": [] + } + """)) + .andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.code").value("INVALID_REQUEST")); + } + + @Test + void createFunctionRejectsEmptyStructureIdsBeforeService() throws Exception { + mockMvc.perform(post("/api/cad-models/1/functions") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "functionName": "固定功能", + "structureIds": [] + } + """)) + .andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.code").value("INVALID_REQUEST")); + } + + private static CadModelHistoryAnnotationResponse sampleAnnotation() { + return new CadModelHistoryAnnotationResponse( + 1L, + 1L, + 10L, + "创建底座拉伸", + "范围覆盖底面", + "人工确认", + LocalDateTime.of(2026, 7, 8, 10, 0, 0), + LocalDateTime.of(2026, 7, 8, 10, 5, 0) + ); + } + + private static CadStructureResponse sampleStructure(Long id, String structureName) { + return new CadStructureResponse( + id, + 1L, + structureName, + "rib", + "提高强度", + "多段拉伸", + "受力集中", + "优先复核", + List.of(new CadStructureHistoryItemResponse(1000L, 10L, 1, "凸台-拉伸1", "extrude_add", 0)), + LocalDateTime.of(2026, 7, 8, 10, 0, 0), + LocalDateTime.of(2026, 7, 8, 10, 5, 0) + ); + } + + private static CadFunctionResponse sampleFunction(Long id, String functionName) { + return new CadFunctionResponse( + id, + 1L, + functionName, + "mounting", + "固定外部零件", + "人工确认", + List.of(new CadFunctionStructureItemResponse(2000L, 100L, "加强筋", "rib", 0)), + LocalDateTime.of(2026, 7, 8, 10, 0, 0), + LocalDateTime.of(2026, 7, 8, 10, 5, 0) + ); + } +} diff --git a/src/test/java/com/cadlabel/service/impl/CadAnnotationServiceIntegrationTest.java b/src/test/java/com/cadlabel/service/impl/CadAnnotationServiceIntegrationTest.java new file mode 100644 index 0000000..97ba58d --- /dev/null +++ b/src/test/java/com/cadlabel/service/impl/CadAnnotationServiceIntegrationTest.java @@ -0,0 +1,413 @@ +package com.cadlabel.service.impl; + +import com.cadlabel.common.exception.BizException; +import com.cadlabel.dto.cad.annotation.CadFunctionResponse; +import com.cadlabel.dto.cad.annotation.CadModelHistoryAnnotationResponse; +import com.cadlabel.dto.cad.annotation.CadStructureResponse; +import com.cadlabel.dto.cad.annotation.CreateCadFunctionRequest; +import com.cadlabel.dto.cad.annotation.CreateCadStructureRequest; +import com.cadlabel.dto.cad.annotation.SaveHistoryAnnotationRequest; +import com.cadlabel.dto.cad.annotation.UpdateCadFunctionRequest; +import com.cadlabel.dto.cad.annotation.UpdateCadStructureRequest; +import com.cadlabel.service.CadAnnotationService; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest(properties = { + "spring.datasource.url=jdbc:h2:mem:cad-annotation-service;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false", + "spring.datasource.driver-class-name=org.h2.Driver", + "spring.datasource.username=sa", + "spring.datasource.password=" +}) +@ActiveProfiles("test") +class CadAnnotationServiceIntegrationTest { + + @Autowired + private CadAnnotationService cadAnnotationService; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + jdbcTemplate.execute("DROP TABLE IF EXISTS cad_function_structure_rel"); + jdbcTemplate.execute("DROP TABLE IF EXISTS cad_function"); + jdbcTemplate.execute("DROP TABLE IF EXISTS cad_structure_history_rel"); + jdbcTemplate.execute("DROP TABLE IF EXISTS cad_structure"); + jdbcTemplate.execute("DROP TABLE IF EXISTS cad_model_history_annotation"); + jdbcTemplate.execute("DROP TABLE IF EXISTS cad_model_history"); + jdbcTemplate.execute("DROP TABLE IF EXISTS cad_model"); + + jdbcTemplate.execute(""" + CREATE TABLE cad_model ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + category_id BIGINT NOT NULL, + model_name VARCHAR(255) NOT NULL DEFAULT '', + source_sw_filename VARCHAR(255) NOT NULL, + source_sw_path VARCHAR(1024) NOT NULL, + feature_tree_json_path VARCHAR(1024) NOT NULL DEFAULT '', + view_path VARCHAR(1024) NOT NULL DEFAULT '', + application_scenario VARCHAR(255) NOT NULL DEFAULT '', + material VARCHAR(255) NOT NULL DEFAULT '', + manufacturing_method VARCHAR(255) NOT NULL DEFAULT '', + unit VARCHAR(64) NOT NULL DEFAULT '', + user_requirement TEXT, + design_description TEXT, + convert_status VARCHAR(32) NOT NULL DEFAULT 'pending', + error_message TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + """); + jdbcTemplate.execute(""" + CREATE TABLE cad_model_history ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + model_id BIGINT NOT NULL, + source_feature_id VARCHAR(128) NOT NULL DEFAULT '', + history_index INT NOT NULL, + history_name VARCHAR(255) NOT NULL DEFAULT '', + operation_type VARCHAR(64) NOT NULL DEFAULT '', + step_path VARCHAR(1024) NOT NULL DEFAULT '', + glb_path VARCHAR(1024) NOT NULL DEFAULT '', + convert_status VARCHAR(32) NOT NULL DEFAULT 'pending', + error_message TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + """); + jdbcTemplate.execute(""" + CREATE TABLE cad_model_history_annotation ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + model_id BIGINT NOT NULL, + history_id BIGINT NOT NULL, + modeling_description TEXT, + supplement_description TEXT, + remark TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_history_annotation_history (history_id) + ) + """); + jdbcTemplate.execute(""" + CREATE TABLE cad_structure ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + model_id BIGINT NOT NULL, + structure_name VARCHAR(255) NOT NULL DEFAULT '', + structure_type VARCHAR(64) NOT NULL DEFAULT '', + purpose VARCHAR(1024) NOT NULL DEFAULT '', + solution VARCHAR(1024) NOT NULL DEFAULT '', + reason TEXT, + remark TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted TINYINT NOT NULL DEFAULT 0 + ) + """); + jdbcTemplate.execute(""" + CREATE TABLE cad_structure_history_rel ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + model_id BIGINT NOT NULL, + structure_id BIGINT NOT NULL, + history_id BIGINT NOT NULL, + sort_order INT NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_structure_history (structure_id, history_id) + ) + """); + jdbcTemplate.execute(""" + CREATE TABLE cad_function ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + model_id BIGINT NOT NULL, + function_name VARCHAR(255) NOT NULL DEFAULT '', + function_type VARCHAR(64) NOT NULL DEFAULT '', + goal_description TEXT, + remark TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted TINYINT NOT NULL DEFAULT 0 + ) + """); + jdbcTemplate.execute(""" + CREATE TABLE cad_function_structure_rel ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + model_id BIGINT NOT NULL, + function_id BIGINT NOT NULL, + structure_id BIGINT NOT NULL, + sort_order INT NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_function_structure (function_id, structure_id) + ) + """); + + insertModel(1L, "轴承座"); + insertModel(2L, "连接板"); + insertHistory(10L, 1L, "feat_001", 1, "凸台-拉伸1"); + insertHistory(11L, 1L, "feat_002", 2, "倒角1"); + insertHistory(20L, 2L, "feat_101", 1, "拉伸2"); + } + + @Test + void saveHistoryAnnotationInsertsThenUpdatesSameHistory() { + CadModelHistoryAnnotationResponse created = cadAnnotationService.saveHistoryAnnotation(1L, 10L, + new SaveHistoryAnnotationRequest("创建底座", "覆盖底面", "初次标注")); + + CadModelHistoryAnnotationResponse updated = cadAnnotationService.saveHistoryAnnotation(1L, 10L, + new SaveHistoryAnnotationRequest("创建底座-修改", "覆盖底面-修改", "复核通过")); + + assertThat(updated.id()).isEqualTo(created.id()); + assertThat(updated.modelingDescription()).isEqualTo("创建底座-修改"); + assertThat(jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM cad_model_history_annotation WHERE history_id = 10", + Long.class + )).isEqualTo(1L); + } + + @Test + void saveHistoryAnnotationRejectsHistoryOutsideModel() { + assertThatThrownBy(() -> cadAnnotationService.saveHistoryAnnotation(1L, 20L, + new SaveHistoryAnnotationRequest("错误", null, null))) + .isInstanceOf(BizException.class) + .hasMessage("建模历史不存在"); + } + + @Test + void createStructureWritesRelationsInRequestOrder() { + CadStructureResponse response = cadAnnotationService.createStructure(1L, new CreateCadStructureRequest( + "加强筋", + "rib", + "提高强度", + "多段拉伸", + "受力集中", + "人工确认", + List.of(11L, 10L) + )); + + assertThat(response.id()).isPositive(); + assertThat(response.historyItems()).extracting(item -> item.historyId()).containsExactly(11L, 10L); + assertThat(response.historyItems()).extracting(item -> item.sortOrder()).containsExactly(0, 1); + assertThat(jdbcTemplate.queryForList( + "SELECT history_id FROM cad_structure_history_rel WHERE structure_id = ? ORDER BY sort_order", + Long.class, + response.id() + )).containsExactly(11L, 10L); + } + + @Test + void createStructureRejectsDuplicateOrInvalidHistoryIds() { + assertThatThrownBy(() -> cadAnnotationService.createStructure(1L, new CreateCadStructureRequest( + "加强筋", + "rib", + null, + null, + null, + null, + List.of(10L, 10L) + ))) + .isInstanceOf(BizException.class) + .hasMessage("historyIds 不能包含重复值"); + + assertThatThrownBy(() -> cadAnnotationService.createStructure(1L, new CreateCadStructureRequest( + "加强筋", + "rib", + null, + null, + null, + null, + List.of(20L) + ))) + .isInstanceOf(BizException.class) + .hasMessage("建模历史不存在"); + } + + @Test + void updateStructurePartiallyAndReplacesRelations() { + CadStructureResponse created = cadAnnotationService.createStructure(1L, new CreateCadStructureRequest( + "加强筋", + "rib", + "提高强度", + "多段拉伸", + null, + null, + List.of(10L, 11L) + )); + + CadStructureResponse updated = cadAnnotationService.updateStructure(1L, created.id(), new UpdateCadStructureRequest( + "加强筋-修改", + null, + null, + null, + null, + "已复核", + List.of(11L) + )); + + assertThat(updated.structureName()).isEqualTo("加强筋-修改"); + assertThat(updated.structureType()).isEqualTo("rib"); + assertThat(updated.remark()).isEqualTo("已复核"); + assertThat(updated.historyItems()).extracting(item -> item.historyId()).containsExactly(11L); + assertThat(jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM cad_structure_history_rel WHERE structure_id = ?", + Long.class, + created.id() + )).isEqualTo(1L); + } + + @Test + void createAndUpdateFunctionReplaceStructureRelations() { + CadStructureResponse first = cadAnnotationService.createStructure(1L, new CreateCadStructureRequest( + "加强筋", + "rib", + null, + null, + null, + null, + List.of(10L) + )); + CadStructureResponse second = cadAnnotationService.createStructure(1L, new CreateCadStructureRequest( + "安装孔", + "hole", + null, + null, + null, + null, + List.of(11L) + )); + + CadFunctionResponse created = cadAnnotationService.createFunction(1L, new CreateCadFunctionRequest( + "固定功能", + "mounting", + "固定外部零件", + "人工确认", + List.of(second.id(), first.id()) + )); + + assertThat(created.structureItems()).extracting(item -> item.structureId()) + .containsExactly(second.id(), first.id()); + assertThat(created.structureItems()).extracting(item -> item.sortOrder()).containsExactly(0, 1); + + CadFunctionResponse updated = cadAnnotationService.updateFunction(1L, created.id(), new UpdateCadFunctionRequest( + "固定功能-修改", + null, + null, + null, + List.of(first.id()) + )); + + assertThat(updated.functionName()).isEqualTo("固定功能-修改"); + assertThat(updated.functionType()).isEqualTo("mounting"); + assertThat(updated.structureItems()).extracting(item -> item.structureId()).containsExactly(first.id()); + } + + @Test + void queryStructuresAndFunctionsExcludeDeletedAndKeepRelationOrder() { + CadStructureResponse active = cadAnnotationService.createStructure(1L, new CreateCadStructureRequest( + "加强筋", + "rib", + null, + null, + null, + null, + List.of(11L, 10L) + )); + CadStructureResponse deleted = cadAnnotationService.createStructure(1L, new CreateCadStructureRequest( + "待删除结构", + "temp", + null, + null, + null, + null, + List.of(10L) + )); + jdbcTemplate.update("UPDATE cad_structure SET deleted = 1 WHERE id = ?", deleted.id()); + + CadFunctionResponse function = cadAnnotationService.createFunction(1L, new CreateCadFunctionRequest( + "固定功能", + "mounting", + null, + null, + List.of(active.id()) + )); + CadFunctionResponse deletedFunction = cadAnnotationService.createFunction(1L, new CreateCadFunctionRequest( + "待删除功能", + "temp", + null, + null, + List.of(active.id()) + )); + jdbcTemplate.update("UPDATE cad_function SET deleted = 1 WHERE id = ?", deletedFunction.id()); + + List structures = cadAnnotationService.listStructures(1L); + List functions = cadAnnotationService.listFunctions(1L); + + assertThat(structures).extracting(CadStructureResponse::id).containsExactly(active.id()); + assertThat(structures.getFirst().historyItems()).extracting(item -> item.historyId()).containsExactly(11L, 10L); + assertThat(cadAnnotationService.getStructure(1L, active.id()).historyItems()) + .extracting(item -> item.sortOrder()).containsExactly(0, 1); + + assertThat(functions).extracting(CadFunctionResponse::id).containsExactly(function.id()); + assertThat(cadAnnotationService.getFunction(1L, function.id()).structureItems()) + .extracting(item -> item.structureId()).containsExactly(active.id()); + } + + private void insertModel(Long id, String modelName) { + jdbcTemplate.update(""" + INSERT INTO cad_model ( + id, category_id, model_name, source_sw_filename, source_sw_path, + feature_tree_json_path, view_path, application_scenario, material, + manufacturing_method, unit, user_requirement, design_description, + convert_status, error_message, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + id, + 7L, + modelName, + id + ".sldprt", + "sw/" + id + ".sldprt", + "features/" + id + ".json", + "views/" + id, + "工业夹具", + "铝合金", + "CNC", + "mm", + "需求描述", + "设计说明", + "success", + null, + "2026-07-08 10:00:00", + "2026-07-08 10:00:00" + ); + } + + private void insertHistory(Long id, Long modelId, String sourceFeatureId, int historyIndex, String historyName) { + jdbcTemplate.update(""" + INSERT INTO cad_model_history ( + id, model_id, source_feature_id, history_index, history_name, + operation_type, step_path, glb_path, convert_status, error_message, + created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + id, + modelId, + sourceFeatureId, + historyIndex, + historyName, + "extrude_add", + "steps/" + historyIndex + ".step", + "glbs/" + historyIndex + ".glb", + "success", + null, + "2026-07-08 10:00:00", + "2026-07-08 10:00:00" + ); + } +}