模型列表和建模历史接口

This commit is contained in:
lxp
2026-07-08 15:06:28 +08:00
parent 234a166924
commit 3f30d574d6
16 changed files with 1024 additions and 3 deletions
@@ -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()
);
}
}