模型列表和建模历史接口
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
## 代码实现原则
|
||||||
|
- 优先保证业务正确性、可维护性、可读性,再考虑炫技。
|
||||||
|
- 修改代码时忽略.sql文件,如果遇到需要改表结构,直接发我sql。
|
||||||
|
## 日志规范
|
||||||
|
- 关键业务节点必须补充必要日志,方便线上排查。
|
||||||
|
- 不要打印密码、Token、手机号、身份证、支付密钥等敏感信息。
|
||||||
|
- 异常日志必须包含足够定位问题的信息。
|
||||||
|
## 回复规范
|
||||||
|
- 永远用中文回复。
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
package com.cadlabel;
|
package com.cadlabel;
|
||||||
|
|
||||||
import org.mybatis.spring.annotation.MapperScan;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||||
|
|
||||||
@MapperScan("com.cadlabel.mapper")
|
|
||||||
@ConfigurationPropertiesScan
|
@ConfigurationPropertiesScan
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class CadLabelApplication {
|
public class CadLabelApplication {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.cadlabel.controller;
|
||||||
|
|
||||||
|
import com.cadlabel.common.api.ApiResponse;
|
||||||
|
import com.cadlabel.common.api.PageResponse;
|
||||||
|
import com.cadlabel.dto.cad.CadModelHistoryResponse;
|
||||||
|
import com.cadlabel.dto.cad.CadModelListQuery;
|
||||||
|
import com.cadlabel.dto.cad.CadModelResponse;
|
||||||
|
import com.cadlabel.dto.cad.UpdateCadModelRequest;
|
||||||
|
import com.cadlabel.service.CadModelService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
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.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@Validated
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/cad-models")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CadModelController {
|
||||||
|
|
||||||
|
private final CadModelService cadModelService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<CadModelResponse>> listModels(
|
||||||
|
@RequestParam(defaultValue = "1") @Min(1) int page,
|
||||||
|
@RequestParam(defaultValue = "10") @Min(1) @Max(100) int size,
|
||||||
|
@RequestParam(required = false) @Positive Long categoryId,
|
||||||
|
@RequestParam(required = false) String convertStatus,
|
||||||
|
@RequestParam(required = false) String keyword
|
||||||
|
) {
|
||||||
|
return ApiResponse.success(cadModelService.listModels(
|
||||||
|
new CadModelListQuery(page, size, categoryId, convertStatus, keyword)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<CadModelResponse> getModel(@PathVariable @Positive Long id) {
|
||||||
|
return ApiResponse.success(cadModelService.getModel(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}")
|
||||||
|
public ApiResponse<CadModelResponse> updateModel(
|
||||||
|
@PathVariable @Positive Long id,
|
||||||
|
@Valid @RequestBody(required = false) UpdateCadModelRequest request
|
||||||
|
) {
|
||||||
|
return ApiResponse.success(cadModelService.updateModel(id, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{modelId}/histories")
|
||||||
|
public ApiResponse<List<CadModelHistoryResponse>> listHistories(@PathVariable @Positive Long modelId) {
|
||||||
|
return ApiResponse.success(cadModelService.listHistories(modelId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.cadlabel.dto.cad;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public record CadModelHistoryResponse(
|
||||||
|
Long id,
|
||||||
|
Long modelId,
|
||||||
|
String sourceFeatureId,
|
||||||
|
Integer historyIndex,
|
||||||
|
String historyName,
|
||||||
|
String operationType,
|
||||||
|
String stepPath,
|
||||||
|
String glbPath,
|
||||||
|
String convertStatus,
|
||||||
|
String errorMessage,
|
||||||
|
LocalDateTime createdAt,
|
||||||
|
LocalDateTime updatedAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.cadlabel.dto.cad;
|
||||||
|
|
||||||
|
public record CadModelListQuery(
|
||||||
|
int page,
|
||||||
|
int size,
|
||||||
|
Long categoryId,
|
||||||
|
String convertStatus,
|
||||||
|
String keyword
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.cadlabel.dto.cad;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public record CadModelResponse(
|
||||||
|
Long id,
|
||||||
|
Long categoryId,
|
||||||
|
String modelName,
|
||||||
|
String sourceSwFilename,
|
||||||
|
String sourceSwPath,
|
||||||
|
String featureTreeJsonPath,
|
||||||
|
String viewPath,
|
||||||
|
String applicationScenario,
|
||||||
|
String material,
|
||||||
|
String manufacturingMethod,
|
||||||
|
String unit,
|
||||||
|
String userRequirement,
|
||||||
|
String designDescription,
|
||||||
|
String convertStatus,
|
||||||
|
String errorMessage,
|
||||||
|
LocalDateTime createdAt,
|
||||||
|
LocalDateTime updatedAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.cadlabel.dto.cad;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
public record UpdateCadModelRequest(
|
||||||
|
@Positive Long categoryId,
|
||||||
|
@Size(max = 255) String modelName,
|
||||||
|
@Size(max = 255) String sourceSwFilename,
|
||||||
|
@Size(max = 1024) String sourceSwPath,
|
||||||
|
@Size(max = 1024) String featureTreeJsonPath,
|
||||||
|
@Size(max = 1024) String viewPath,
|
||||||
|
@Size(max = 255) String applicationScenario,
|
||||||
|
@Size(max = 255) String material,
|
||||||
|
@Size(max = 255) String manufacturingMethod,
|
||||||
|
@Size(max = 64) String unit,
|
||||||
|
String userRequirement,
|
||||||
|
String designDescription,
|
||||||
|
@Size(max = 32) String convertStatus,
|
||||||
|
String errorMessage
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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")
|
||||||
|
public class CadModel {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long categoryId;
|
||||||
|
private String modelName;
|
||||||
|
private String sourceSwFilename;
|
||||||
|
private String sourceSwPath;
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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")
|
||||||
|
public class CadModelHistory {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long modelId;
|
||||||
|
private String sourceFeatureId;
|
||||||
|
private Integer historyIndex;
|
||||||
|
private String historyName;
|
||||||
|
private String operationType;
|
||||||
|
private String stepPath;
|
||||||
|
private String glbPath;
|
||||||
|
private String convertStatus;
|
||||||
|
private String errorMessage;
|
||||||
|
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.CadModelHistory;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface CadModelHistoryMapper extends BaseMapper<CadModelHistory> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.cadlabel.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.cadlabel.entity.CadModel;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface CadModelMapper extends BaseMapper<CadModel> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.cadlabel.service;
|
||||||
|
|
||||||
|
import com.cadlabel.common.api.PageResponse;
|
||||||
|
import com.cadlabel.dto.cad.CadModelHistoryResponse;
|
||||||
|
import com.cadlabel.dto.cad.CadModelListQuery;
|
||||||
|
import com.cadlabel.dto.cad.CadModelResponse;
|
||||||
|
import com.cadlabel.dto.cad.UpdateCadModelRequest;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface CadModelService {
|
||||||
|
|
||||||
|
PageResponse<CadModelResponse> listModels(CadModelListQuery query);
|
||||||
|
|
||||||
|
CadModelResponse getModel(Long id);
|
||||||
|
|
||||||
|
CadModelResponse updateModel(Long id, UpdateCadModelRequest request);
|
||||||
|
|
||||||
|
List<CadModelHistoryResponse> listHistories(Long modelId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
package com.cadlabel.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.cadlabel.common.api.ApiCode;
|
||||||
|
import com.cadlabel.common.api.PageResponse;
|
||||||
|
import com.cadlabel.common.exception.BizException;
|
||||||
|
import com.cadlabel.dto.cad.CadModelHistoryResponse;
|
||||||
|
import com.cadlabel.dto.cad.CadModelListQuery;
|
||||||
|
import com.cadlabel.dto.cad.CadModelResponse;
|
||||||
|
import com.cadlabel.dto.cad.UpdateCadModelRequest;
|
||||||
|
import com.cadlabel.entity.CadModel;
|
||||||
|
import com.cadlabel.entity.CadModelHistory;
|
||||||
|
import com.cadlabel.mapper.CadModelHistoryMapper;
|
||||||
|
import com.cadlabel.mapper.CadModelMapper;
|
||||||
|
import com.cadlabel.service.CadModelService;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
|
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 CadModelServiceImpl implements CadModelService {
|
||||||
|
|
||||||
|
private static final int DEFAULT_PAGE = 1;
|
||||||
|
private static final int DEFAULT_SIZE = 10;
|
||||||
|
private static final int MAX_SIZE = 100;
|
||||||
|
private static final Set<String> MODEL_CONVERT_STATUSES = Set.of("pending", "success", "failed", "partial_failed");
|
||||||
|
|
||||||
|
private final CadModelMapper cadModelMapper;
|
||||||
|
private final CadModelHistoryMapper cadModelHistoryMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询 CAD 模型列表。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public PageResponse<CadModelResponse> listModels(CadModelListQuery query) {
|
||||||
|
int page = resolvePage(query == null ? DEFAULT_PAGE : query.page());
|
||||||
|
int size = resolveSize(query == null ? DEFAULT_SIZE : query.size());
|
||||||
|
Long categoryId = query == null ? null : query.categoryId();
|
||||||
|
String convertStatus = normalizeOptionalText(query == null ? null : query.convertStatus());
|
||||||
|
String keyword = normalizeOptionalText(query == null ? null : query.keyword());
|
||||||
|
|
||||||
|
if (categoryId != null && categoryId <= 0) {
|
||||||
|
throw invalidRequest("categoryId 必须为正数");
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(convertStatus)) {
|
||||||
|
validateModelConvertStatus(convertStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
Page<CadModel> result = cadModelMapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper<CadModel>()
|
||||||
|
.eq(categoryId != null, CadModel::getCategoryId, categoryId)
|
||||||
|
.eq(StringUtils.hasText(convertStatus), CadModel::getConvertStatus, convertStatus)
|
||||||
|
.and(StringUtils.hasText(keyword), nested -> nested
|
||||||
|
.like(CadModel::getModelName, keyword)
|
||||||
|
.or()
|
||||||
|
.like(CadModel::getSourceSwFilename, keyword))
|
||||||
|
.orderByDesc(CadModel::getCreatedAt)
|
||||||
|
.orderByDesc(CadModel::getId));
|
||||||
|
|
||||||
|
List<CadModelResponse> items = result.getRecords().stream().map(this::toModelResponse).toList();
|
||||||
|
return new PageResponse<>(items, result.getCurrent(), result.getSize(), result.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 CAD 模型详情。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public CadModelResponse getModel(Long id) {
|
||||||
|
return toModelResponse(requireModel(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改 CAD 模型基础信息。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CadModelResponse updateModel(Long id, UpdateCadModelRequest request) {
|
||||||
|
CadModel model = requireModel(id);
|
||||||
|
List<String> updatedFields = updatedFields(request);
|
||||||
|
if (updatedFields.isEmpty()) {
|
||||||
|
return toModelResponse(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateUpdateRequest(request);
|
||||||
|
ensureCategoryFilenameUnique(id, model, request);
|
||||||
|
applyUpdate(model, request);
|
||||||
|
model.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
|
log.info("Updating CAD model. id={}, fields={}", id, updatedFields);
|
||||||
|
try {
|
||||||
|
cadModelMapper.updateById(model);
|
||||||
|
} catch (DuplicateKeyException exception) {
|
||||||
|
throw duplicateModelException();
|
||||||
|
}
|
||||||
|
return getModel(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询模型完整建模历史。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<CadModelHistoryResponse> listHistories(Long modelId) {
|
||||||
|
requireModel(modelId);
|
||||||
|
return cadModelHistoryMapper.selectList(Wrappers.lambdaQuery(CadModelHistory.class)
|
||||||
|
.eq(CadModelHistory::getModelId, modelId)
|
||||||
|
.orderByAsc(CadModelHistory::getHistoryIndex)
|
||||||
|
.orderByAsc(CadModelHistory::getId))
|
||||||
|
.stream()
|
||||||
|
.map(this::toHistoryResponse)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CadModel requireModel(Long id) {
|
||||||
|
validatePositiveId(id);
|
||||||
|
CadModel model = cadModelMapper.selectById(id);
|
||||||
|
if (model == null) {
|
||||||
|
throw modelNotFound();
|
||||||
|
}
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePositiveId(Long id) {
|
||||||
|
if (id == null || id <= 0) {
|
||||||
|
throw invalidRequest("id 必须为正数");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int resolvePage(int page) {
|
||||||
|
if (page < 1) {
|
||||||
|
throw invalidRequest("page 必须大于等于 1");
|
||||||
|
}
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int resolveSize(int size) {
|
||||||
|
if (size < 1 || size > MAX_SIZE) {
|
||||||
|
throw invalidRequest("size 必须在 1 到 100 之间");
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureCategoryFilenameUnique(Long id, CadModel current, UpdateCadModelRequest request) {
|
||||||
|
if (request.categoryId() == null && request.sourceSwFilename() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long categoryId = request.categoryId() == null ? current.getCategoryId() : request.categoryId();
|
||||||
|
String sourceSwFilename = request.sourceSwFilename() == null
|
||||||
|
? current.getSourceSwFilename()
|
||||||
|
: request.sourceSwFilename();
|
||||||
|
Long count = cadModelMapper.selectCount(Wrappers.lambdaQuery(CadModel.class)
|
||||||
|
.eq(CadModel::getCategoryId, categoryId)
|
||||||
|
.eq(CadModel::getSourceSwFilename, sourceSwFilename)
|
||||||
|
.ne(CadModel::getId, id));
|
||||||
|
if (count != null && count > 0) {
|
||||||
|
throw duplicateModelException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateUpdateRequest(UpdateCadModelRequest request) {
|
||||||
|
if (request.categoryId() != null && request.categoryId() <= 0) {
|
||||||
|
throw invalidRequest("categoryId 必须为正数");
|
||||||
|
}
|
||||||
|
validateMaxLength("modelName", request.modelName(), 255);
|
||||||
|
validateMaxLength("sourceSwFilename", request.sourceSwFilename(), 255);
|
||||||
|
validateMaxLength("sourceSwPath", request.sourceSwPath(), 1024);
|
||||||
|
validateMaxLength("featureTreeJsonPath", request.featureTreeJsonPath(), 1024);
|
||||||
|
validateMaxLength("viewPath", request.viewPath(), 1024);
|
||||||
|
validateMaxLength("applicationScenario", request.applicationScenario(), 255);
|
||||||
|
validateMaxLength("material", request.material(), 255);
|
||||||
|
validateMaxLength("manufacturingMethod", request.manufacturingMethod(), 255);
|
||||||
|
validateMaxLength("unit", request.unit(), 64);
|
||||||
|
validateMaxLength("convertStatus", request.convertStatus(), 32);
|
||||||
|
if (request.convertStatus() != null) {
|
||||||
|
validateModelConvertStatus(request.convertStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateMaxLength(String field, String value, int maxLength) {
|
||||||
|
if (value != null && value.length() > maxLength) {
|
||||||
|
throw invalidRequest(field + " 长度不能超过 " + maxLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyUpdate(CadModel model, UpdateCadModelRequest request) {
|
||||||
|
if (request.categoryId() != null) {
|
||||||
|
model.setCategoryId(request.categoryId());
|
||||||
|
}
|
||||||
|
if (request.modelName() != null) {
|
||||||
|
model.setModelName(request.modelName());
|
||||||
|
}
|
||||||
|
if (request.sourceSwFilename() != null) {
|
||||||
|
model.setSourceSwFilename(request.sourceSwFilename());
|
||||||
|
}
|
||||||
|
if (request.sourceSwPath() != null) {
|
||||||
|
model.setSourceSwPath(request.sourceSwPath());
|
||||||
|
}
|
||||||
|
if (request.featureTreeJsonPath() != null) {
|
||||||
|
model.setFeatureTreeJsonPath(request.featureTreeJsonPath());
|
||||||
|
}
|
||||||
|
if (request.viewPath() != null) {
|
||||||
|
model.setViewPath(request.viewPath());
|
||||||
|
}
|
||||||
|
if (request.applicationScenario() != null) {
|
||||||
|
model.setApplicationScenario(request.applicationScenario());
|
||||||
|
}
|
||||||
|
if (request.material() != null) {
|
||||||
|
model.setMaterial(request.material());
|
||||||
|
}
|
||||||
|
if (request.manufacturingMethod() != null) {
|
||||||
|
model.setManufacturingMethod(request.manufacturingMethod());
|
||||||
|
}
|
||||||
|
if (request.unit() != null) {
|
||||||
|
model.setUnit(request.unit());
|
||||||
|
}
|
||||||
|
if (request.userRequirement() != null) {
|
||||||
|
model.setUserRequirement(request.userRequirement());
|
||||||
|
}
|
||||||
|
if (request.designDescription() != null) {
|
||||||
|
model.setDesignDescription(request.designDescription());
|
||||||
|
}
|
||||||
|
if (request.convertStatus() != null) {
|
||||||
|
model.setConvertStatus(request.convertStatus());
|
||||||
|
}
|
||||||
|
if (request.errorMessage() != null) {
|
||||||
|
model.setErrorMessage(request.errorMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> updatedFields(UpdateCadModelRequest request) {
|
||||||
|
List<String> fields = new ArrayList<>();
|
||||||
|
if (request == null) {
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
addField(fields, "categoryId", request.categoryId());
|
||||||
|
addField(fields, "modelName", request.modelName());
|
||||||
|
addField(fields, "sourceSwFilename", request.sourceSwFilename());
|
||||||
|
addField(fields, "sourceSwPath", request.sourceSwPath());
|
||||||
|
addField(fields, "featureTreeJsonPath", request.featureTreeJsonPath());
|
||||||
|
addField(fields, "viewPath", request.viewPath());
|
||||||
|
addField(fields, "applicationScenario", request.applicationScenario());
|
||||||
|
addField(fields, "material", request.material());
|
||||||
|
addField(fields, "manufacturingMethod", request.manufacturingMethod());
|
||||||
|
addField(fields, "unit", request.unit());
|
||||||
|
addField(fields, "userRequirement", request.userRequirement());
|
||||||
|
addField(fields, "designDescription", request.designDescription());
|
||||||
|
addField(fields, "convertStatus", request.convertStatus());
|
||||||
|
addField(fields, "errorMessage", request.errorMessage());
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addField(List<String> fields, String fieldName, Object value) {
|
||||||
|
if (value != null) {
|
||||||
|
fields.add(fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeOptionalText(String value) {
|
||||||
|
return StringUtils.hasText(value) ? value.trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateModelConvertStatus(String status) {
|
||||||
|
if (!MODEL_CONVERT_STATUSES.contains(status)) {
|
||||||
|
throw invalidRequest("convertStatus 取值非法");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BizException invalidRequest(String message) {
|
||||||
|
return new BizException(HttpStatus.UNPROCESSABLE_ENTITY.value(), ApiCode.INVALID_REQUEST, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BizException modelNotFound() {
|
||||||
|
return new BizException(HttpStatus.NOT_FOUND.value(), ApiCode.NOT_FOUND, "CAD 模型不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
private BizException duplicateModelException() {
|
||||||
|
return new BizException(HttpStatus.CONFLICT.value(), "CAD_MODEL_DUPLICATE", "同分类下源 SolidWorks 文件名已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
private CadModelResponse toModelResponse(CadModel model) {
|
||||||
|
return new CadModelResponse(
|
||||||
|
model.getId(),
|
||||||
|
model.getCategoryId(),
|
||||||
|
model.getModelName(),
|
||||||
|
model.getSourceSwFilename(),
|
||||||
|
model.getSourceSwPath(),
|
||||||
|
model.getFeatureTreeJsonPath(),
|
||||||
|
model.getViewPath(),
|
||||||
|
model.getApplicationScenario(),
|
||||||
|
model.getMaterial(),
|
||||||
|
model.getManufacturingMethod(),
|
||||||
|
model.getUnit(),
|
||||||
|
model.getUserRequirement(),
|
||||||
|
model.getDesignDescription(),
|
||||||
|
model.getConvertStatus(),
|
||||||
|
model.getErrorMessage(),
|
||||||
|
model.getCreatedAt(),
|
||||||
|
model.getUpdatedAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CadModelHistoryResponse toHistoryResponse(CadModelHistory history) {
|
||||||
|
return new CadModelHistoryResponse(
|
||||||
|
history.getId(),
|
||||||
|
history.getModelId(),
|
||||||
|
history.getSourceFeatureId(),
|
||||||
|
history.getHistoryIndex(),
|
||||||
|
history.getHistoryName(),
|
||||||
|
history.getOperationType(),
|
||||||
|
history.getStepPath(),
|
||||||
|
history.getGlbPath(),
|
||||||
|
history.getConvertStatus(),
|
||||||
|
history.getErrorMessage(),
|
||||||
|
history.getCreatedAt(),
|
||||||
|
history.getUpdatedAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ spring:
|
|||||||
activate:
|
activate:
|
||||||
on-profile: dev
|
on-profile: dev
|
||||||
datasource:
|
datasource:
|
||||||
url: ${APP_DB_URL:jdbc:mysql://49.232.4.148:3306/cadlabel?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&createDatabaseIfNotExist=true}
|
url: ${APP_DB_URL:jdbc:mysql://39.105.93.138:3306/cadlabel?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&createDatabaseIfNotExist=true}
|
||||||
username: ${APP_DB_USERNAME:root}
|
username: ${APP_DB_USERNAME:root}
|
||||||
password: ${APP_DB_PASSWORD:mysql_QRWm4M}
|
password: ${APP_DB_PASSWORD:mysql_QRWm4M}
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package com.cadlabel.controller;
|
||||||
|
|
||||||
|
import com.cadlabel.common.api.PageResponse;
|
||||||
|
import com.cadlabel.common.exception.BizException;
|
||||||
|
import com.cadlabel.common.exception.GlobalExceptionHandler;
|
||||||
|
import com.cadlabel.common.web.RequestIdFilter;
|
||||||
|
import com.cadlabel.config.JacksonConfig;
|
||||||
|
import com.cadlabel.dto.cad.CadModelHistoryResponse;
|
||||||
|
import com.cadlabel.dto.cad.CadModelListQuery;
|
||||||
|
import com.cadlabel.dto.cad.CadModelResponse;
|
||||||
|
import com.cadlabel.dto.cad.UpdateCadModelRequest;
|
||||||
|
import com.cadlabel.service.CadModelService;
|
||||||
|
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.HttpStatus;
|
||||||
|
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(CadModelController.class)
|
||||||
|
@Import({GlobalExceptionHandler.class, RequestIdFilter.class, JacksonConfig.class})
|
||||||
|
class CadModelControllerTest {
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
private CadModelService cadModelService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listModelsReturnsPagedUnifiedResponse() throws Exception {
|
||||||
|
CadModelResponse model = sampleModel(1L, "轴承座");
|
||||||
|
given(cadModelService.listModels(any(CadModelListQuery.class))).willReturn(
|
||||||
|
new PageResponse<>(List.of(model), 2, 5, 11)
|
||||||
|
);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/cad-models")
|
||||||
|
.param("page", "2")
|
||||||
|
.param("size", "5")
|
||||||
|
.param("categoryId", "7")
|
||||||
|
.param("convertStatus", "success")
|
||||||
|
.param("keyword", "轴承"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value("OK"))
|
||||||
|
.andExpect(jsonPath("$.data.page").value(2))
|
||||||
|
.andExpect(jsonPath("$.data.size").value(5))
|
||||||
|
.andExpect(jsonPath("$.data.total").value(11))
|
||||||
|
.andExpect(jsonPath("$.data.items[0].id").value(1))
|
||||||
|
.andExpect(jsonPath("$.data.items[0].modelName").value("轴承座"));
|
||||||
|
|
||||||
|
verify(cadModelService).listModels(new CadModelListQuery(2, 5, 7L, "success", "轴承"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getModelReturnsUnifiedResponse() throws Exception {
|
||||||
|
given(cadModelService.getModel(1L)).willReturn(sampleModel(1L, "连接板"));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/cad-models/1"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value("OK"))
|
||||||
|
.andExpect(jsonPath("$.data.id").value(1))
|
||||||
|
.andExpect(jsonPath("$.data.modelName").value("连接板"))
|
||||||
|
.andExpect(jsonPath("$.data.createdAt").value("2026-07-08 10:00:00"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getModelReturnsNotFoundWhenMissing() throws Exception {
|
||||||
|
given(cadModelService.getModel(404L)).willThrow(
|
||||||
|
new BizException(HttpStatus.NOT_FOUND.value(), "NOT_FOUND", "CAD 模型不存在")
|
||||||
|
);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/cad-models/404"))
|
||||||
|
.andExpect(status().isNotFound())
|
||||||
|
.andExpect(jsonPath("$.code").value("NOT_FOUND"))
|
||||||
|
.andExpect(jsonPath("$.message").value("CAD 模型不存在"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateModelAcceptsPartialJsonAndReturnsUpdatedModel() throws Exception {
|
||||||
|
given(cadModelService.updateModel(eq(1L), any())).willReturn(sampleModel(1L, "新名称"));
|
||||||
|
|
||||||
|
mockMvc.perform(post("/api/cad-models/1")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("""
|
||||||
|
{
|
||||||
|
"modelName": "新名称",
|
||||||
|
"userRequirement": null
|
||||||
|
}
|
||||||
|
"""))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value("OK"))
|
||||||
|
.andExpect(jsonPath("$.data.modelName").value("新名称"));
|
||||||
|
|
||||||
|
verify(cadModelService).updateModel(1L, new UpdateCadModelRequest(
|
||||||
|
null,
|
||||||
|
"新名称",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listHistoriesReturnsOrderedUnifiedResponse() throws Exception {
|
||||||
|
given(cadModelService.listHistories(1L)).willReturn(List.of(
|
||||||
|
new CadModelHistoryResponse(
|
||||||
|
10L,
|
||||||
|
1L,
|
||||||
|
"feat_001",
|
||||||
|
1,
|
||||||
|
"凸台-拉伸1",
|
||||||
|
"extrude_add",
|
||||||
|
"steps/1.step",
|
||||||
|
"glbs/1.glb",
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
LocalDateTime.of(2026, 7, 8, 10, 0, 0),
|
||||||
|
LocalDateTime.of(2026, 7, 8, 10, 5, 0)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/cad-models/1/histories"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value("OK"))
|
||||||
|
.andExpect(jsonPath("$.data[0].id").value(10))
|
||||||
|
.andExpect(jsonPath("$.data[0].historyIndex").value(1))
|
||||||
|
.andExpect(jsonPath("$.data[0].historyName").value("凸台-拉伸1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CadModelResponse sampleModel(Long id, String modelName) {
|
||||||
|
return new CadModelResponse(
|
||||||
|
id,
|
||||||
|
7L,
|
||||||
|
modelName,
|
||||||
|
"015133.sldprt",
|
||||||
|
"sw/015133.sldprt",
|
||||||
|
"features/015133.json",
|
||||||
|
"views/015133",
|
||||||
|
"工业夹具",
|
||||||
|
"铝合金",
|
||||||
|
"CNC",
|
||||||
|
"mm",
|
||||||
|
"轻量化",
|
||||||
|
"整体设计说明",
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
LocalDateTime.of(2026, 7, 8, 10, 0, 0),
|
||||||
|
LocalDateTime.of(2026, 7, 8, 10, 5, 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
package com.cadlabel.service.impl;
|
||||||
|
|
||||||
|
import com.cadlabel.common.api.PageResponse;
|
||||||
|
import com.cadlabel.common.exception.BizException;
|
||||||
|
import com.cadlabel.dto.cad.CadModelHistoryResponse;
|
||||||
|
import com.cadlabel.dto.cad.CadModelListQuery;
|
||||||
|
import com.cadlabel.dto.cad.CadModelResponse;
|
||||||
|
import com.cadlabel.dto.cad.UpdateCadModelRequest;
|
||||||
|
import com.cadlabel.service.CadModelService;
|
||||||
|
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-model-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 CadModelServiceIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CadModelService cadModelService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
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,
|
||||||
|
UNIQUE KEY uk_cad_model_category_filename (category_id, source_sw_filename)
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
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,
|
||||||
|
UNIQUE KEY uk_model_feature (model_id, source_feature_id)
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listModelsFiltersPaginatesAndSortsByCreatedAtDescending() {
|
||||||
|
insertModel(1L, 7L, "轴承座 A", "a.sldprt", "success", "2026-07-08 10:00:00");
|
||||||
|
insertModel(2L, 7L, "轴承座 B", "b.sldprt", "success", "2026-07-08 11:00:00");
|
||||||
|
insertModel(3L, 7L, "连接板", "c.sldprt", "failed", "2026-07-08 12:00:00");
|
||||||
|
insertModel(4L, 9L, "轴承座 C", "d.sldprt", "success", "2026-07-08 13:00:00");
|
||||||
|
|
||||||
|
PageResponse<CadModelResponse> response = cadModelService.listModels(
|
||||||
|
new CadModelListQuery(1, 1, 7L, "success", "轴承")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(response.page()).isEqualTo(1);
|
||||||
|
assertThat(response.size()).isEqualTo(1);
|
||||||
|
assertThat(response.total()).isEqualTo(2);
|
||||||
|
assertThat(response.items()).extracting(CadModelResponse::id).containsExactly(2L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listModelsRejectsInvalidQueryValues() {
|
||||||
|
assertThatThrownBy(() -> cadModelService.listModels(new CadModelListQuery(0, 10, null, null, null)))
|
||||||
|
.isInstanceOf(BizException.class)
|
||||||
|
.hasMessage("page 必须大于等于 1");
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> cadModelService.listModels(new CadModelListQuery(1, 101, null, null, null)))
|
||||||
|
.isInstanceOf(BizException.class)
|
||||||
|
.hasMessage("size 必须在 1 到 100 之间");
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> cadModelService.listModels(new CadModelListQuery(1, 10, 0L, null, null)))
|
||||||
|
.isInstanceOf(BizException.class)
|
||||||
|
.hasMessage("categoryId 必须为正数");
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> cadModelService.listModels(new CadModelListQuery(1, 10, null, "done", null)))
|
||||||
|
.isInstanceOf(BizException.class)
|
||||||
|
.hasMessage("convertStatus 取值非法");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getModelReturnsModelOrNotFound() {
|
||||||
|
insertModel(1L, 7L, "轴承座", "a.sldprt", "success", "2026-07-08 10:00:00");
|
||||||
|
|
||||||
|
CadModelResponse response = cadModelService.getModel(1L);
|
||||||
|
|
||||||
|
assertThat(response.id()).isEqualTo(1L);
|
||||||
|
assertThat(response.modelName()).isEqualTo("轴承座");
|
||||||
|
assertThat(response.sourceSwFilename()).isEqualTo("a.sldprt");
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> cadModelService.getModel(404L))
|
||||||
|
.isInstanceOf(BizException.class)
|
||||||
|
.hasMessage("CAD 模型不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateModelPartiallyAndIgnoresNullFields() {
|
||||||
|
insertModel(1L, 7L, "旧名称", "a.sldprt", "success", "2026-07-08 10:00:00");
|
||||||
|
UpdateCadModelRequest request = new UpdateCadModelRequest(
|
||||||
|
8L,
|
||||||
|
"新名称",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"partial_failed",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
CadModelResponse response = cadModelService.updateModel(1L, request);
|
||||||
|
|
||||||
|
assertThat(response.modelName()).isEqualTo("新名称");
|
||||||
|
assertThat(response.categoryId()).isEqualTo(8L);
|
||||||
|
assertThat(response.convertStatus()).isEqualTo("partial_failed");
|
||||||
|
assertThat(response.userRequirement()).isEqualTo("需求描述");
|
||||||
|
assertThat(jdbcTemplate.queryForObject(
|
||||||
|
"SELECT user_requirement FROM cad_model WHERE id = 1",
|
||||||
|
String.class
|
||||||
|
)).isEqualTo("需求描述");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateModelRejectsInvalidAndDuplicateValues() {
|
||||||
|
insertModel(1L, 7L, "模型 A", "a.sldprt", "success", "2026-07-08 10:00:00");
|
||||||
|
insertModel(2L, 7L, "模型 B", "b.sldprt", "success", "2026-07-08 11:00:00");
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> cadModelService.updateModel(1L, new UpdateCadModelRequest(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"done",
|
||||||
|
null
|
||||||
|
)))
|
||||||
|
.isInstanceOf(BizException.class)
|
||||||
|
.hasMessage("convertStatus 取值非法");
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> cadModelService.updateModel(2L, new UpdateCadModelRequest(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"a.sldprt",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)))
|
||||||
|
.isInstanceOf(BizException.class)
|
||||||
|
.hasMessage("同分类下源 SolidWorks 文件名已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listHistoriesReturnsHistoryIndexOrderAndRequiresExistingModel() {
|
||||||
|
insertModel(1L, 7L, "轴承座", "a.sldprt", "success", "2026-07-08 10:00:00");
|
||||||
|
insertHistory(11L, 1L, "feat_002", 2, "倒角1");
|
||||||
|
insertHistory(10L, 1L, "feat_001", 1, "凸台-拉伸1");
|
||||||
|
|
||||||
|
List<CadModelHistoryResponse> histories = cadModelService.listHistories(1L);
|
||||||
|
|
||||||
|
assertThat(histories).extracting(CadModelHistoryResponse::id).containsExactly(10L, 11L);
|
||||||
|
assertThat(histories).extracting(CadModelHistoryResponse::historyIndex).containsExactly(1, 2);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> cadModelService.listHistories(404L))
|
||||||
|
.isInstanceOf(BizException.class)
|
||||||
|
.hasMessage("CAD 模型不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertModel(Long id, Long categoryId, String modelName, String filename, String status, String createdAt) {
|
||||||
|
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,
|
||||||
|
categoryId,
|
||||||
|
modelName,
|
||||||
|
filename,
|
||||||
|
"sw/" + filename,
|
||||||
|
"features/" + filename + ".json",
|
||||||
|
"views/" + filename,
|
||||||
|
"工业夹具",
|
||||||
|
"铝合金",
|
||||||
|
"CNC",
|
||||||
|
"mm",
|
||||||
|
"需求描述",
|
||||||
|
"设计说明",
|
||||||
|
status,
|
||||||
|
null,
|
||||||
|
createdAt,
|
||||||
|
createdAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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