src/main/java/com/ruoyi/basic/service/impl/ProductModelServiceImpl.java
@@ -58,9 +58,19 @@
    public int addOrEditProductModel(ProductModelDto productModelDto) {
        String model = StringUtils.trim(productModelDto.getModel());
        String productCode = StringUtils.trim(productModelDto.getProductCode());
        String drawingNo = StringUtils.trim(productModelDto.getDrawingNo());
        productModelDto.setModel(model);
        productModelDto.setProductCode(productCode);
        productModelDto.setDrawingNo(drawingNo);
        // 图号必填
        if (StringUtils.isEmpty(drawingNo)) {
            throw new ServiceException("图号不能为空");
        }
        if (drawingNo.length() > 255) {
            throw new ServiceException("图号长度不能超过255个字符");
        }
        checkModelAndProductCodeUnique(model, productCode, productModelDto.getId());
        checkDrawingNoUnique(drawingNo, productModelDto.getId());
        if (productModelDto.getId() == null) {
            ProductModel productModel = new ProductModel();
@@ -68,6 +78,23 @@
            return productModelMapper.insert(productModel);
        } else {
            return productModelMapper.updateById(productModelDto);
        }
    }
    /**
     * 图号唯一校验(全局,编辑时排除自身)
     */
    private void checkDrawingNoUnique(String drawingNo, Long currentId) {
        if (StringUtils.isEmpty(drawingNo)) {
            return;
        }
        LambdaQueryWrapper<ProductModel> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ProductModel::getDrawingNo, drawingNo)
                .ne(currentId != null, ProductModel::getId, currentId)
                .last("limit 1");
        ProductModel duplicateProductModel = productModelMapper.selectOne(queryWrapper);
        if (duplicateProductModel != null) {
            throw new ServiceException("图号" + drawingNo + "已经存在");
        }
    }
@@ -161,12 +188,16 @@
            Map<String, Long> nodeCache = new HashMap<>();
            for (int i = 0; i < rows.size(); i++) {
                ProductImportRowDto row = rows.get(i);
                int rowNum = i + 2;
                //  没有名称/编码/规格的行构不成产品数据,按空行跳过
                if (hasNoProductContent(row)) {
                    continue;
                }
                String parentName = trimToNull(row.getProductParentName());
                String categoryName = trimToNull(row.getProductCategoryName());
                String productName = trimToNull(row.getProductName());
                if (productName == null) {
                    throw new ServiceException("第 " + rowNum + " 行 [产品名称] 不能为空");
                    throw new ServiceException(String.format("工作表【%s】第 %d 行 [产品名称] 不能为空(该行已填:%s)",
                            sheetName, resolveRowNum(row, i), describeRow(row)));
                }
                String model = trimToNull(row.getModel());
@@ -196,10 +227,20 @@
                    code = generateUniqueCode(generatedCodes);
                }
                // 图号:导入时可选,填写则校验唯一
                String drawingNo = trimToNull(row.getDrawingNo());
                if (drawingNo != null) {
                    if (drawingNo.length() > 255) {
                        throw new ServiceException("工作簿【" + sheetName + "】第 " + resolveRowNum(row, i) + " 行:图号长度不能超过255个字符");
                    }
                    checkDrawingNoUnique(drawingNo, null);
                }
                ProductModel productModel = new ProductModel();
                productModel.setProductId(leafId);
                productModel.setProductCode(code);
                productModel.setModel(model);
                productModel.setDrawingNo(drawingNo);
                productModel.setUnit(trimToNull(row.getUnit()));
                productModelMapper.insert(productModel);
                successCount++;
@@ -291,6 +332,41 @@
        return keys;
    }
    /**
     * 解析时会跳过空行
     */
    private static int resolveRowNum(ProductImportRowDto row, int index) {
        return row.getExcelRowNum() != null ? row.getExcelRowNum() : index + 2;
    }
    private static boolean hasNoProductContent(ProductImportRowDto row) {
        return trimToNull(row.getProductName()) == null
                && trimToNull(row.getProductCode()) == null
                && trimToNull(row.getModel()) == null;
    }
    private static String describeRow(ProductImportRowDto row) {
        StringBuilder description = new StringBuilder();
        appendFilledColumn(description, "产品父类", row.getProductParentName());
        appendFilledColumn(description, "产品大类", row.getProductCategoryName());
        appendFilledColumn(description, "产品名称", row.getProductName());
        appendFilledColumn(description, "产品编码", row.getProductCode());
        appendFilledColumn(description, "规格型号", row.getModel());
        appendFilledColumn(description, "单位", row.getUnit());
        return description.length() == 0 ? "无" : description.toString();
    }
    private static void appendFilledColumn(StringBuilder description, String columnName, String value) {
        String trimmed = trimToNull(value);
        if (trimmed == null) {
            return;
        }
        if (description.length() > 0) {
            description.append(",");
        }
        description.append(columnName).append('=').append(trimmed);
    }
    private static String dedupKey(String productName, String model, String productCode) {
        String normalizedModel = trimToNull(model) == null ? "/" : model.trim();
        String normalizedCode = productCode == null ? "" : productCode.trim();