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 模型服务实现。 * *

负责模型列表、详情、基础信息修改和建模历史查询。

*/ @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 MODEL_CONVERT_STATUSES = Set.of("pending", "success", "failed", "partial_failed"); private final CadModelMapper cadModelMapper; private final CadModelHistoryMapper cadModelHistoryMapper; /** * 分页查询 CAD 模型列表。 */ @Override public PageResponse 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 result = cadModelMapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper() .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 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 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 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 updatedFields(UpdateCadModelRequest request) { List 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 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() ); } }