模型列表和建模历史接口
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
## 代码实现原则
|
||||
- 优先保证业务正确性、可维护性、可读性,再考虑炫技。
|
||||
- 修改代码时忽略.sql文件,如果遇到需要改表结构,直接发我sql。
|
||||
## 日志规范
|
||||
- 关键业务节点必须补充必要日志,方便线上排查。
|
||||
- 不要打印密码、Token、手机号、身份证、支付密钥等敏感信息。
|
||||
- 异常日志必须包含足够定位问题的信息。
|
||||
## 回复规范
|
||||
- 永远用中文回复。
|
||||
@@ -1,11 +1,9 @@
|
||||
package com.cadlabel;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
|
||||
@MapperScan("com.cadlabel.mapper")
|
||||
@ConfigurationPropertiesScan
|
||||
@SpringBootApplication
|
||||
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:
|
||||
on-profile: dev
|
||||
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}
|
||||
password: ${APP_DB_PASSWORD:mysql_QRWm4M}
|
||||
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