标注接口
This commit is contained in:
@@ -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;
|
||||
|
||||
/**
|
||||
* 保存建模历史步骤标注。
|
||||
*
|
||||
* <p>同一个建模历史只能有一条标注;重复保存时按 historyId 覆盖原有标注内容。</p>
|
||||
*/
|
||||
@PostMapping("/histories/{historyId}/annotation")
|
||||
public ApiResponse<CadModelHistoryAnnotationResponse> saveHistoryAnnotation(
|
||||
@PathVariable @Positive Long modelId,
|
||||
@PathVariable @Positive Long historyId,
|
||||
@Valid @RequestBody(required = false) SaveHistoryAnnotationRequest request
|
||||
) {
|
||||
return ApiResponse.success(cadAnnotationService.saveHistoryAnnotation(modelId, historyId, request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 CAD 结构。
|
||||
*
|
||||
* <p>创建时必须选择至少一个建模历史步骤,关系排序按 historyIds 请求顺序生成。</p>
|
||||
*/
|
||||
@PostMapping("/structures")
|
||||
public ApiResponse<CadStructureResponse> createStructure(
|
||||
@PathVariable @Positive Long modelId,
|
||||
@Valid @RequestBody CreateCadStructureRequest request
|
||||
) {
|
||||
return ApiResponse.success(cadAnnotationService.createStructure(modelId, request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 CAD 结构。
|
||||
*
|
||||
* <p>普通字段支持部分更新;传入 historyIds 时按最终集合全量替换结构下的建模历史关系。</p>
|
||||
*/
|
||||
@PostMapping("/structures/{structureId}")
|
||||
public ApiResponse<CadStructureResponse> updateStructure(
|
||||
@PathVariable @Positive Long modelId,
|
||||
@PathVariable @Positive Long structureId,
|
||||
@Valid @RequestBody(required = false) UpdateCadStructureRequest request
|
||||
) {
|
||||
return ApiResponse.success(cadAnnotationService.updateStructure(modelId, structureId, request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询模型下的结构列表。
|
||||
*
|
||||
* <p>只返回未逻辑删除的结构,并携带结构关联的建模历史步骤。</p>
|
||||
*/
|
||||
@GetMapping("/structures")
|
||||
public ApiResponse<List<CadStructureResponse>> listStructures(@PathVariable @Positive Long modelId) {
|
||||
return ApiResponse.success(cadAnnotationService.listStructures(modelId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询结构详情。
|
||||
*
|
||||
* <p>用于结构编辑回显,返回结构基础信息和已关联的建模历史步骤。</p>
|
||||
*/
|
||||
@GetMapping("/structures/{structureId}")
|
||||
public ApiResponse<CadStructureResponse> getStructure(
|
||||
@PathVariable @Positive Long modelId,
|
||||
@PathVariable @Positive Long structureId
|
||||
) {
|
||||
return ApiResponse.success(cadAnnotationService.getStructure(modelId, structureId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 CAD 功能。
|
||||
*
|
||||
* <p>创建时必须选择至少一个结构,关系排序按 structureIds 请求顺序生成。</p>
|
||||
*/
|
||||
@PostMapping("/functions")
|
||||
public ApiResponse<CadFunctionResponse> createFunction(
|
||||
@PathVariable @Positive Long modelId,
|
||||
@Valid @RequestBody CreateCadFunctionRequest request
|
||||
) {
|
||||
return ApiResponse.success(cadAnnotationService.createFunction(modelId, request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 CAD 功能。
|
||||
*
|
||||
* <p>普通字段支持部分更新;传入 structureIds 时按最终集合全量替换功能下的结构关系。</p>
|
||||
*/
|
||||
@PostMapping("/functions/{functionId}")
|
||||
public ApiResponse<CadFunctionResponse> updateFunction(
|
||||
@PathVariable @Positive Long modelId,
|
||||
@PathVariable @Positive Long functionId,
|
||||
@Valid @RequestBody(required = false) UpdateCadFunctionRequest request
|
||||
) {
|
||||
return ApiResponse.success(cadAnnotationService.updateFunction(modelId, functionId, request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询模型下的功能列表。
|
||||
*
|
||||
* <p>只返回未逻辑删除的功能,并携带功能关联的结构。</p>
|
||||
*/
|
||||
@GetMapping("/functions")
|
||||
public ApiResponse<List<CadFunctionResponse>> listFunctions(@PathVariable @Positive Long modelId) {
|
||||
return ApiResponse.success(cadAnnotationService.listFunctions(modelId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询功能详情。
|
||||
*
|
||||
* <p>用于功能编辑回显,返回功能基础信息和已关联的结构。</p>
|
||||
*/
|
||||
@GetMapping("/functions/{functionId}")
|
||||
public ApiResponse<CadFunctionResponse> getFunction(
|
||||
@PathVariable @Positive Long modelId,
|
||||
@PathVariable @Positive Long functionId
|
||||
) {
|
||||
return ApiResponse.success(cadAnnotationService.getFunction(modelId, functionId));
|
||||
}
|
||||
}
|
||||
@@ -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<CadFunctionStructureItemResponse> structureItems,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cadlabel.dto.cad.annotation;
|
||||
|
||||
public record CadFunctionStructureItemResponse(
|
||||
Long relationId,
|
||||
Long structureId,
|
||||
String structureName,
|
||||
String structureType,
|
||||
Integer sortOrder
|
||||
) {
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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<CadStructureHistoryItemResponse> historyItems,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cadlabel.dto.cad.annotation;
|
||||
|
||||
public record SaveHistoryAnnotationRequest(
|
||||
String modelingDescription,
|
||||
String supplementDescription,
|
||||
String remark
|
||||
) {
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<CadFunction> {
|
||||
}
|
||||
@@ -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<CadFunctionStructureRel> {
|
||||
}
|
||||
@@ -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<CadModelHistoryAnnotation> {
|
||||
}
|
||||
@@ -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<CadStructureHistoryRel> {
|
||||
}
|
||||
@@ -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<CadStructure> {
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
* 保存建模历史步骤标注。
|
||||
*
|
||||
* <p>如果指定 historyId 已存在标注,则覆盖原有标注文本;否则新建标注记录。</p>
|
||||
*/
|
||||
CadModelHistoryAnnotationResponse saveHistoryAnnotation(
|
||||
Long modelId,
|
||||
Long historyId,
|
||||
SaveHistoryAnnotationRequest request
|
||||
);
|
||||
|
||||
/**
|
||||
* 创建结构并建立结构与建模历史步骤的关系。
|
||||
*/
|
||||
CadStructureResponse createStructure(Long modelId, CreateCadStructureRequest request);
|
||||
|
||||
/**
|
||||
* 修改结构基础信息,并在传入 historyIds 时全量替换结构与建模历史步骤的关系。
|
||||
*/
|
||||
CadStructureResponse updateStructure(Long modelId, Long structureId, UpdateCadStructureRequest request);
|
||||
|
||||
/**
|
||||
* 查询模型下未删除的结构列表。
|
||||
*/
|
||||
List<CadStructureResponse> 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<CadFunctionResponse> listFunctions(Long modelId);
|
||||
|
||||
/**
|
||||
* 查询功能详情。
|
||||
*/
|
||||
CadFunctionResponse getFunction(Long modelId, Long functionId);
|
||||
}
|
||||
@@ -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 标注编排服务。
|
||||
*
|
||||
* <p>负责建模历史标注、结构、功能以及两层关系维护。</p>
|
||||
*/
|
||||
@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;
|
||||
|
||||
/**
|
||||
* 保存建模历史步骤标注。
|
||||
*
|
||||
* <p>先校验模型和历史步骤归属关系,再按 historyId 做插入或更新,保证前端重复保存是幂等的。</p>
|
||||
*/
|
||||
@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()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建结构并绑定建模历史步骤。
|
||||
*
|
||||
* <p>结构基础记录和结构-历史关系在同一事务内写入,任一环节失败都会回滚。</p>
|
||||
*/
|
||||
@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<Long> 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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改结构基础信息和历史步骤关系。
|
||||
*
|
||||
* <p>请求未传的普通字段保持原值;传入 historyIds 时会按请求数组全量替换关系。</p>
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public CadStructureResponse updateStructure(Long modelId, Long structureId, UpdateCadStructureRequest request) {
|
||||
CadStructure structure = requireStructure(modelId, structureId);
|
||||
if (request == null) {
|
||||
return toStructureResponse(structure);
|
||||
}
|
||||
|
||||
List<String> updatedFields = structureUpdatedFields(request);
|
||||
validateStructureUpdateRequest(request);
|
||||
if (request.historyIds() != null) {
|
||||
List<Long> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询模型下的结构列表。
|
||||
*
|
||||
* <p>只查询 deleted=0 的结构,并为每个结构组装已关联的建模历史步骤。</p>
|
||||
*/
|
||||
@Override
|
||||
public List<CadStructureResponse> 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询结构详情。
|
||||
*
|
||||
* <p>详情查询会校验结构归属当前模型,并排除已逻辑删除的结构。</p>
|
||||
*/
|
||||
@Override
|
||||
public CadStructureResponse getStructure(Long modelId, Long structureId) {
|
||||
return toStructureResponse(requireStructure(modelId, structureId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建功能并绑定结构。
|
||||
*
|
||||
* <p>功能基础记录和功能-结构关系在同一事务内写入,关系排序按 structureIds 请求顺序生成。</p>
|
||||
*/
|
||||
@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<Long> 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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改功能基础信息和结构关系。
|
||||
*
|
||||
* <p>请求未传的普通字段保持原值;传入 structureIds 时会按请求数组全量替换关系。</p>
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public CadFunctionResponse updateFunction(Long modelId, Long functionId, UpdateCadFunctionRequest request) {
|
||||
CadFunction cadFunction = requireFunction(modelId, functionId);
|
||||
if (request == null) {
|
||||
return toFunctionResponse(cadFunction);
|
||||
}
|
||||
|
||||
List<String> updatedFields = functionUpdatedFields(request);
|
||||
validateFunctionUpdateRequest(request);
|
||||
if (request.structureIds() != null) {
|
||||
List<Long> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询模型下的功能列表。
|
||||
*
|
||||
* <p>只查询 deleted=0 的功能,并为每个功能组装已关联的结构。</p>
|
||||
*/
|
||||
@Override
|
||||
public List<CadFunctionResponse> 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询功能详情。
|
||||
*
|
||||
* <p>详情查询会校验功能归属当前模型,并排除已逻辑删除的功能。</p>
|
||||
*/
|
||||
@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<Long, CadModelHistory> requireHistories(Long modelId, List<Long> historyIds) {
|
||||
List<CadModelHistory> 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<Long, CadStructure> requireStructures(Long modelId, List<Long> structureIds) {
|
||||
List<CadStructure> 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量替换结构与建模历史步骤关系。
|
||||
*
|
||||
* <p>先删除结构原有关联,再按 historyIds 数组顺序写入 sortOrder。</p>
|
||||
*/
|
||||
private void replaceStructureHistoryRelations(Long modelId, Long structureId, List<Long> 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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量替换功能与结构关系。
|
||||
*
|
||||
* <p>先删除功能原有关联,再按 structureIds 数组顺序写入 sortOrder。</p>
|
||||
*/
|
||||
private void replaceFunctionStructureRelations(Long modelId, Long functionId, List<Long> 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()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将结构实体转换为接口响应。
|
||||
*
|
||||
* <p>响应中会按 sortOrder 升序组装结构关联的建模历史步骤。</p>
|
||||
*/
|
||||
private CadStructureResponse toStructureResponse(CadStructure structure) {
|
||||
List<CadStructureHistoryRel> relations = structureHistoryRelMapper.selectList(Wrappers.lambdaQuery(CadStructureHistoryRel.class)
|
||||
.eq(CadStructureHistoryRel::getStructureId, structure.getId())
|
||||
.orderByAsc(CadStructureHistoryRel::getSortOrder)
|
||||
.orderByAsc(CadStructureHistoryRel::getId));
|
||||
Map<Long, CadModelHistory> 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<CadStructureHistoryItemResponse> 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()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将功能实体转换为接口响应。
|
||||
*
|
||||
* <p>响应中会按 sortOrder 升序组装功能关联的未删除结构。</p>
|
||||
*/
|
||||
private CadFunctionResponse toFunctionResponse(CadFunction cadFunction) {
|
||||
List<CadFunctionStructureRel> relations = functionStructureRelMapper.selectList(Wrappers.lambdaQuery(CadFunctionStructureRel.class)
|
||||
.eq(CadFunctionStructureRel::getFunctionId, cadFunction.getId())
|
||||
.orderByAsc(CadFunctionStructureRel::getSortOrder)
|
||||
.orderByAsc(CadFunctionStructureRel::getId));
|
||||
Map<Long, CadStructure> 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<CadFunctionStructureItemResponse> 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<Long> validateIdList(String fieldName, List<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
throw invalidRequest(fieldName + " 不能为空");
|
||||
}
|
||||
Set<Long> 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<String> structureUpdatedFields(UpdateCadStructureRequest request) {
|
||||
List<String> 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<String> functionUpdatedFields(UpdateCadFunctionRequest request) {
|
||||
List<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<CadStructureResponse> structures = cadAnnotationService.listStructures(1L);
|
||||
List<CadFunctionResponse> 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user