diff --git a/backend/framework/sdk/src/main/resources/i18n/commons_en_US.properties b/backend/framework/sdk/src/main/resources/i18n/commons_en_US.properties index 8aebfae9be..910da4ea68 100644 --- a/backend/framework/sdk/src/main/resources/i18n/commons_en_US.properties +++ b/backend/framework/sdk/src/main/resources/i18n/commons_en_US.properties @@ -520,7 +520,7 @@ swagger_parse_error=Swagger parsing failed or file format is incorrect! permission.test_plan.name=Test plan permission.test_plan_module.name=Test plan module -excel.template.id=Not mandatory, add a new use case when the ID is empty +excel.template.id=Non mandatory, add a new use case when the ID is empty or does not exist; excel.template.case_edit_type=Not mandatory, fill in STEP for step description, fill in Text for text description, default to Text if not filled in excel.template.tag=Not mandatory labels should be separated by semicolons or commas excel.template.text_description=Not mandatory, when the editing mode is STEP, the step description will be based on the identifier [1] [2] [3] To determine whether to split a cell into multiple steps, if not, it is a single step @@ -536,7 +536,7 @@ custom_field_member_tip=[%s] must be current project member custom_field_select_tip=[%s] must be %s custom_field_int_tip=[%s] must be integer custom_field_float_tip=[%s] must be number - +check_import_excel_error=Check import excel error #关联 relate_source_id_not_blank=Source id cannot be empty relate_source_id_length_range=The association source ID must be between {min} and {max} diff --git a/backend/framework/sdk/src/main/resources/i18n/commons_zh_CN.properties b/backend/framework/sdk/src/main/resources/i18n/commons_zh_CN.properties index fe3c46da44..4b7a75a5ac 100644 --- a/backend/framework/sdk/src/main/resources/i18n/commons_zh_CN.properties +++ b/backend/framework/sdk/src/main/resources/i18n/commons_zh_CN.properties @@ -516,7 +516,7 @@ swagger_parse_error=Swagger 解析失败,请确认文件格式是否正确! permission.test_plan.name=测试计划 permission.test_plan_module.name=测试计划模块 -excel.template.id=非必填,ID为空时新增用例 +excel.template.id=非必填,ID为空或不存在时新增用例; excel.template.case_edit_type=非必填,步骤描述填写STEP,文本描述填写TEXT,未填写默认为TEXT excel.template.tag=非必填,标签之间以分号或者逗号隔开 excel.template.text_description=非必填,编辑模式为STEP时,步骤描述会根据标识[1] [2] [3]...来判断是否将单元格拆分为多个步骤,没有则为一个步骤 @@ -532,6 +532,7 @@ custom_field_member_tip=[%s]必须当前项目成员 custom_field_select_tip=[%s]必须为%s custom_field_int_tip=[%s]必须为整型 custom_field_float_tip=[%s]必须为数字 +check_import_excel_error=检查导入Excel错误 #关联 relate_source_id_not_blank=关联来源ID不能为空 relate_source_id_length_range=关联来源ID必须在{min}和{max}之间 diff --git a/backend/framework/sdk/src/main/resources/i18n/commons_zh_TW.properties b/backend/framework/sdk/src/main/resources/i18n/commons_zh_TW.properties index 6001c3c619..29d4792e06 100644 --- a/backend/framework/sdk/src/main/resources/i18n/commons_zh_TW.properties +++ b/backend/framework/sdk/src/main/resources/i18n/commons_zh_TW.properties @@ -516,7 +516,7 @@ swagger_parse_error=Swagger 解析失敗 permission.test_plan.name=測試計劃 permission.test_plan_module.name=測試計劃模塊 -excel.template.id=非必填,ID為空時新增用例 +excel.template.id=非必填,ID為空時或不存在時新增用例 excel.template.case_edit_type=非必填,步驟描述填寫STEP,文本描述填寫TEXT,為填寫默認為TEXT excel.template.tag=非必填,標簽之間以分號或者逗號隔開 excel.template.text_description=非必填,編輯模式為STEP時,步驟描述會根據標識[1] [2] [3]...來判斷是否將單元格拆分為多個步驟,沒有則為一個步驟 @@ -532,7 +532,7 @@ custom_field_member_tip=[%s]必須當前項目成員 custom_field_select_tip=[%s]必須為%s custom_field_int_tip=[%s]必須為整型 custom_field_float_tip=[%s]必須為數字 - +check_import_excel_error=檢查導入Excel錯誤 #关联 relate_source_id_not_blank=關聯來源ID不能為空 relate_source_id_length_range=關聯來源ID必須在{min}和{max}之間 diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/constants/FunctionalCaseTypeConstants.java b/backend/services/case-management/src/main/java/io/metersphere/functional/constants/FunctionalCaseTypeConstants.java new file mode 100644 index 0000000000..50598d9090 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/constants/FunctionalCaseTypeConstants.java @@ -0,0 +1,11 @@ +package io.metersphere.functional.constants; + +/** + * @author wx + */ +public class FunctionalCaseTypeConstants { + + public enum CaseEditType { + TEXT, STEP + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseController.java b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseController.java index 5d7d91ec37..ece370bb44 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseController.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseController.java @@ -226,4 +226,13 @@ public class FunctionalCaseController { public FunctionalCaseImportResponse preCheckExcel(@RequestPart("request") FunctionalCaseImportRequest request, @RequestPart(value = "file", required = false) MultipartFile file) { return functionalCaseFileService.preCheckExcel(request, file); } + + + @PostMapping("/import/excel") + @Operation(summary = "用例管理-功能用例-excel导入") + @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_UPDATE) + public FunctionalCaseImportResponse importExcel(@RequestPart("request") FunctionalCaseImportRequest request, @RequestPart(value = "file", required = false) MultipartFile file) { + String userId = SessionUtils.getUserId(); + return functionalCaseFileService.importExcel(request, userId, file); + } } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelData.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelData.java index 094a814695..9ed595f6f1 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelData.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelData.java @@ -33,13 +33,13 @@ public class FunctionalCaseExcelData { private String expectedResult; @ExcelIgnore private String caseEditType; - + @ExcelIgnore + private String steps; @ExcelIgnore Map customData = new LinkedHashMap<>(); @ExcelIgnore Map otherFields; - @ExcelIgnore - Set textFieldSet = new HashSet<>(1); + /** * 合并文本描述 */ diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/listener/FunctionalCaseCheckEventListener.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/listener/FunctionalCaseCheckEventListener.java index eb279de800..cd5a45095f 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/listener/FunctionalCaseCheckEventListener.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/listener/FunctionalCaseCheckEventListener.java @@ -11,7 +11,7 @@ import io.metersphere.functional.excel.exception.CustomFieldValidateException; import io.metersphere.functional.excel.validate.AbstractCustomFieldValidator; import io.metersphere.functional.excel.validate.CustomFieldValidatorFactory; import io.metersphere.functional.request.FunctionalCaseImportRequest; -import io.metersphere.plugin.sdk.util.MSPluginException; +import io.metersphere.sdk.exception.MSException; import io.metersphere.sdk.util.LogUtils; import io.metersphere.sdk.util.Translator; import io.metersphere.system.dto.excel.ExcelValidateHelper; @@ -23,7 +23,6 @@ import org.jetbrains.annotations.Nullable; import java.io.Serial; import java.lang.reflect.Field; import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; /** @@ -57,7 +56,6 @@ public class FunctionalCaseCheckEventListener extends AnalysisEventListener> errList = new ArrayList<>(); private static final String ERROR_MSG_SEPARATOR = ";"; private HashMap customFieldValidatorMap; - private static AtomicInteger successCount = new AtomicInteger(0); public FunctionalCaseCheckEventListener(FunctionalCaseImportRequest request, Class clazz, List customFields, Set mergeInfoSet) { @@ -85,7 +83,7 @@ public class FunctionalCaseCheckEventListener extends AnalysisEventListener data, AnalysisContext analysisContext) { if (headMap == null) { - throw new MSPluginException("case_import_table_header_missing"); + throw new MSException(Translator.get("case_import_table_header_missing")); } Integer rowIndex = analysisContext.readRowHolder().getRowIndex(); //处理合并单元格 @@ -192,7 +190,7 @@ public class FunctionalCaseCheckEventListener extends AnalysisEventListener { Iterator iterator = mergeInfoSet.iterator(); @@ -451,7 +453,7 @@ public class FunctionalCaseCheckEventListener extends AnalysisEventListener getList() { + return list; } } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/listener/FunctionalCaseImportEventListener.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/listener/FunctionalCaseImportEventListener.java new file mode 100644 index 0000000000..1f0c7ec6cf --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/listener/FunctionalCaseImportEventListener.java @@ -0,0 +1,657 @@ +package io.metersphere.functional.excel.listener; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import io.metersphere.functional.constants.FunctionalCaseTypeConstants; +import io.metersphere.functional.excel.annotation.NotRequired; +import io.metersphere.functional.excel.domain.ExcelMergeInfo; +import io.metersphere.functional.excel.domain.FunctionalCaseExcelData; +import io.metersphere.functional.excel.domain.FunctionalCaseExcelDataFactory; +import io.metersphere.functional.excel.exception.CustomFieldValidateException; +import io.metersphere.functional.excel.validate.AbstractCustomFieldValidator; +import io.metersphere.functional.excel.validate.CustomFieldValidatorFactory; +import io.metersphere.functional.request.FunctionalCaseImportRequest; +import io.metersphere.functional.service.FunctionalCaseModuleService; +import io.metersphere.functional.service.FunctionalCaseService; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.util.CommonBeanFactory; +import io.metersphere.sdk.util.JSON; +import io.metersphere.sdk.util.LogUtils; +import io.metersphere.sdk.util.Translator; +import io.metersphere.system.dto.excel.ExcelValidateHelper; +import io.metersphere.system.dto.sdk.BaseTreeNode; +import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; +import io.metersphere.system.excel.domain.ExcelErrData; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.Serial; +import java.lang.reflect.Field; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author wx + */ +public class FunctionalCaseImportEventListener extends AnalysisEventListener> { + + private Class excelDataClass; + private FunctionalCaseImportRequest request; + private Map headMap; + Map customFieldsMap = new HashMap<>(); + private Set mergeInfoSet; + /** + * 所有的模块集合 + */ + private List moduleTree; + private Map excelHeadToFieldNameDic = new HashMap<>(); + /** + * 标记下当前遍历的行是不是有合并单元格 + */ + private Boolean isMergeRow; + /** + * 标记下当前遍历的行是不是合并单元格的最后一行 + */ + private Boolean isMergeLastRow; + /** + * 存储合并单元格对应的数据,key 为重写了 compareTo 的 ExcelMergeInfo + */ + private HashMap mergeCellDataMap = new HashMap<>(); + /** + * 存储当前合并的一条完整数据,其中步骤没有合并是多行 + */ + private FunctionalCaseExcelData currentMergeData; + private Integer firstMergeRowIndex; + /** + * 每隔5000条存储数据库,然后清理list ,方便内存回收 + */ + protected static final int BATCH_COUNT = 5000; + protected List list = new ArrayList<>(); + protected List> errList = new ArrayList<>(); + /** + * 待更新用例的集合 + */ + protected List updateList = new ArrayList<>(); + private static final String ERROR_MSG_SEPARATOR = ";"; + private HashMap customFieldValidatorMap; + private FunctionalCaseService functionalCaseService; + private String userId; + private int successCount = 0; + private Map pathMap = new HashMap<>(); + + + public FunctionalCaseImportEventListener(FunctionalCaseImportRequest request, Class clazz, List customFields, Set mergeInfoSet, String userId) { + this.mergeInfoSet = mergeInfoSet; + this.request = request; + excelDataClass = clazz; + //当前项目模板的自定义字段 + customFieldsMap = customFields.stream().collect(Collectors.toMap(TemplateCustomFieldDTO::getFieldName, i -> i)); + moduleTree = CommonBeanFactory.getBean(FunctionalCaseModuleService.class).getTree(request.getProjectId()); + functionalCaseService = CommonBeanFactory.getBean(FunctionalCaseService.class); + customFieldValidatorMap = CustomFieldValidatorFactory.getValidatorMap(); + this.userId = userId; + + } + + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + this.headMap = headMap; + try { + genExcelHeadToFieldNameDicAndGetNotRequiredFields(); + } catch (NoSuchFieldException e) { + LogUtils.error(e); + } + formatHeadMap(); + super.invokeHeadMap(headMap, context); + } + + + @Override + public void invoke(Map data, AnalysisContext analysisContext) { + if (headMap == null) { + throw new MSException(Translator.get("case_import_table_header_missing")); + } + + Integer rowIndex = analysisContext.readRowHolder().getRowIndex(); + //处理合并单元格 + handleMergeData(data, rowIndex); + + FunctionalCaseExcelData functionalCaseExcelData; + // 读取名称列,如果该列是合并单元格,则读取多行数据后合并步骤 + if (isMergeRow) { + if (currentMergeData == null) { + firstMergeRowIndex = rowIndex; + // 如果是合并单元格的首行 + functionalCaseExcelData = parseDataToModel(data); + functionalCaseExcelData.setMergeTextDescription(new ArrayList<>() { + @Serial + private static final long serialVersionUID = -2563948462432733672L; + + { + add(functionalCaseExcelData.getTextDescription()); + } + }); + functionalCaseExcelData.setMergeExpectedResult(new ArrayList<>() { + @Serial + private static final long serialVersionUID = 8985001651375529701L; + + { + add(functionalCaseExcelData.getExpectedResult()); + } + }); + // 记录下数据并返回 + currentMergeData = functionalCaseExcelData; + if (!isMergeLastRow) { + return; + } else { + currentMergeData = null; + } + } else { + // 获取存储的数据,并添加多个步骤 + currentMergeData.getMergeTextDescription() + .add(data.get(getTextDescriptionColIndex())); + currentMergeData.getMergeExpectedResult() + .add(data.get(getExpectedResultColIndex())); + // 是最后一行的合并单元格,保存并清空 currentMergeData,走之后的逻辑 + if (isMergeLastRow) { + functionalCaseExcelData = currentMergeData; + currentMergeData = null; + } else { + return; + } + } + } else { + firstMergeRowIndex = null; + functionalCaseExcelData = parseDataToModel(data); + } + + //校验数据 + buildUpdateOrErrorList(rowIndex, functionalCaseExcelData); + + if (list.size() > BATCH_COUNT || updateList.size() > BATCH_COUNT) { + saveData(); + this.successCount += list.size() + updateList.size(); + list.clear(); + updateList.clear(); + } + + } + + + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + // 如果文件最后一行是没有内容的步骤,这里处理最后一条合并单元格的数据 + if (currentMergeData != null) { + buildUpdateOrErrorList(firstMergeRowIndex, currentMergeData); + } + saveData(); + this.successCount += list.size() + updateList.size(); + list.clear(); + updateList.clear(); + customFieldsMap.clear(); + } + + + /** + * 执行保存数据 + */ + private void saveData() { + if (CollectionUtils.isNotEmpty(list)) { + functionalCaseService.saveImportData(list, request, moduleTree, userId, customFieldsMap, pathMap); + } + + if (CollectionUtils.isNotEmpty(updateList)) { + functionalCaseService.updateImportData(updateList, request, moduleTree, userId, customFieldsMap, pathMap); + } + } + + + /** + * 构建数据 + * + * @param rowIndex + * @param functionalCaseExcelData + */ + private void buildUpdateOrErrorList(Integer rowIndex, FunctionalCaseExcelData functionalCaseExcelData) { + StringBuilder errMsg; + try { + //根据excel数据实体中的javax.validation + 正则表达式来校验excel数据 + errMsg = new StringBuilder(ExcelValidateHelper.validateEntity(functionalCaseExcelData)); + //自定义校验规则 + if (StringUtils.isEmpty(errMsg)) { + validate(functionalCaseExcelData, errMsg); + } + } catch (NoSuchFieldException e) { + errMsg = new StringBuilder(Translator.get("parse_data_error")); + LogUtils.error(e.getMessage(), e); + } + + if (StringUtils.isEmpty(errMsg)) { + //不存在错误信息,说明可以新增或更新 + handleImportDate(functionalCaseExcelData); + + } else { + Integer errorRowIndex = rowIndex; + if (firstMergeRowIndex != null) { + errorRowIndex = firstMergeRowIndex; + } + ExcelErrData excelErrData = new ExcelErrData(rowIndex, + Translator.get("number") + .concat(StringUtils.SPACE) + .concat(String.valueOf(errorRowIndex + 1)).concat(StringUtils.SPACE) + .concat(Translator.get("row")) + .concat(Translator.get("error")) + .concat(":") + .concat(errMsg.toString())); + //错误信息 + errList.add(excelErrData); + } + + } + + /** + * 处理可以导入的数据 + * + * @param functionalCaseExcelData + */ + private void handleImportDate(FunctionalCaseExcelData functionalCaseExcelData) { + //处理id判断是新增还是更新 + handleId(functionalCaseExcelData); + //处理单元格 + handleSteps(functionalCaseExcelData); + } + + /** + * 处理步骤描述和预期结果 + * + * @param functionalCaseExcelData + */ + private void handleSteps(FunctionalCaseExcelData functionalCaseExcelData) { + + if (StringUtils.isNotBlank(functionalCaseExcelData.getCaseEditType()) && StringUtils.equalsIgnoreCase(functionalCaseExcelData.getCaseEditType(), FunctionalCaseTypeConstants.CaseEditType.TEXT.name())) { + functionalCaseExcelData.setTextDescription(functionalCaseExcelData.getTextDescription()); + functionalCaseExcelData.setExpectedResult(functionalCaseExcelData.getExpectedResult()); + } else { + String steps = getSteps(functionalCaseExcelData); + functionalCaseExcelData.setSteps(steps); + } + + } + + private String getSteps(FunctionalCaseExcelData data) { + List> steps = new ArrayList<>(); + + if (CollectionUtils.isNotEmpty(data.getMergeTextDescription()) || CollectionUtils.isNotEmpty(data.getMergeExpectedResult())) { + // 如果是合并单元格,则组合多条单元格的数据 + for (int i = 0; i < data.getMergeTextDescription().size(); i++) { + List> rowSteps = getSingleRowSteps(data.getMergeTextDescription().get(i), data.getMergeExpectedResult().get(i), steps.size()); + steps.addAll(rowSteps); + } + } else { + // 如果不是合并单元格,则直接解析单元格数据 + steps.addAll(getSingleRowSteps(data.getTextDescription(), data.getExpectedResult(), steps.size())); + } + return JSON.toJSONString(steps); + } + + + /** + * 解析步骤描述。预期结果 + * + * @param cellDesc 步骤描述 + * @param cellResult 预期结果 + * @param startStepIndex 步骤序号 + * @return + */ + private List> getSingleRowSteps(String cellDesc, String cellResult, Integer startStepIndex) { + List> steps = new ArrayList<>(); + + List stepDescList = parseStepCell(cellDesc); + List stepResList = parseStepCell(cellResult); + + int index = Math.max(stepDescList.size(), stepResList.size()); + for (int i = 0; i < index; i++) { + // 保持插入顺序,判断用例是否有相同的steps + Map step = new LinkedHashMap<>(); + step.put("num", startStepIndex + i + 1); + if (i < stepDescList.size()) { + step.put("desc", stepDescList.get(i)); + } else { + step.put("desc", StringUtils.EMPTY); + } + + if (i < stepResList.size()) { + step.put("result", stepResList.get(i)); + } else { + step.put("result", StringUtils.EMPTY); + } + + steps.add(step); + } + return steps; + } + + + /** + * 解析步骤类型的单元格内容 + * + * @param cellContent 单元格内容 + * @return 解析后的字符文本 + */ + private List parseStepCell(String cellContent) { + List cellStepContentList = new ArrayList<>(); + if (StringUtils.isNotEmpty(cellContent)) { + // 根据[1], [2]...分割步骤描述, 开头空字符去掉, 末尾保留 + String[] cellContentArr = cellContent.split("\\[\\d+]", -1); + if (StringUtils.isEmpty(cellContentArr[0])) { + cellContentArr = Arrays.copyOfRange(cellContentArr, 1, cellContentArr.length); + } + for (String stepContent : cellContentArr) { + cellStepContentList.add(stepContent.replaceAll("(?m)^\\s*|\\s*$", StringUtils.EMPTY)); + } + } else { + cellStepContentList.add(StringUtils.EMPTY); + } + return cellStepContentList; + } + + + /** + * 处理新增数据集合还是更新数据集合 + * + * @param functionalCaseExcelData + */ + private void handleId(FunctionalCaseExcelData functionalCaseExcelData) { + if (StringUtils.isNotEmpty(functionalCaseExcelData.getNum())) { + String checkResult = functionalCaseService.checkNumExist(functionalCaseExcelData.getNum(), request.getProjectId()); + if (StringUtils.isNotEmpty(checkResult)) { + if (request.isCover()) { + //如果是覆盖,那么有id的需要更新 + functionalCaseExcelData.setNum(checkResult); + updateList.add(functionalCaseExcelData); + } + } else { + list.add(functionalCaseExcelData); + } + } else { + list.add(functionalCaseExcelData); + } + } + + + /** + * 校验excel中的数据 + * + * @param data + * @param errMsg + */ + public void validate(FunctionalCaseExcelData data, StringBuilder errMsg) { + //模块校验 + validateModule(data, errMsg); + //校验自定义字段 + validateCustomField(data, errMsg); + //校验id + validateIdExist(data, errMsg); + } + + + /** + * 校验Excel中是否有ID + * 是否覆盖: + * 1.覆盖:id存在则更新,id不存在则新增 + * 2.不覆盖:id存在不处理,id不存在新增 + * + * @param data + * @param errMsg + */ + @Nullable + private void validateIdExist(FunctionalCaseExcelData data, StringBuilder errMsg) { + //当前读取的数据有ID + if (StringUtils.isNotEmpty(data.getNum())) { + Integer num = -1; + try { + num = Integer.parseInt(data.getNum()); + } catch (Exception e) { + data.setNum(null); + return; + } + if (num < 0) { + errMsg.append(Translator.get("id_not_rightful")) + .append("[") + .append(data.getNum()) + .append("]; "); + } + } + } + + + /** + * 校验自定义字段,并记录错误提示 + * 如果填写的是自定义字段的选项值,则转换成ID保存 + * + * @param data + * @param errMsg + */ + private void validateCustomField(FunctionalCaseExcelData data, StringBuilder errMsg) { + Map customData = data.getCustomData(); + for (String fieldName : customData.keySet()) { + Object value = customData.get(fieldName); + String originFieldName = fieldName; + TemplateCustomFieldDTO templateCustomFieldDTO = customFieldsMap.get(fieldName); + if (templateCustomFieldDTO == null) { + continue; + } + AbstractCustomFieldValidator customFieldValidator = customFieldValidatorMap.get(templateCustomFieldDTO.getType()); + try { + customFieldValidator.validate(templateCustomFieldDTO, value.toString()); + if (customFieldValidator.isKVOption) { + // 这里如果填的是选项值,替换成选项ID,保存 + customData.put(originFieldName, customFieldValidator.parse2Key(value.toString(), templateCustomFieldDTO)); + } + } catch (CustomFieldValidateException e) { + errMsg.append(e.getMessage().concat(ERROR_MSG_SEPARATOR)); + } + } + } + + + /** + * 校验模块 + * + * @param data + * @param errMsg + */ + private void validateModule(FunctionalCaseExcelData data, StringBuilder errMsg) { + String module = data.getModule(); + if (StringUtils.isNotEmpty(module)) { + String[] nodes = module.split("/"); + //模块名不能为空 + for (int i = 0; i < nodes.length; i++) { + if (i != 0 && StringUtils.equals(nodes[i].trim(), StringUtils.EMPTY)) { + errMsg.append(Translator.get("module_not_null")) + .append(ERROR_MSG_SEPARATOR); + break; + } + } + //增加字数校验,每一层不能超过100个字 + for (int i = 0; i < nodes.length; i++) { + String nodeStr = nodes[i]; + if (StringUtils.isNotEmpty(nodeStr)) { + if (nodeStr.trim().length() > 100) { + errMsg.append(Translator.get("module")) + .append(Translator.get("functional_case.module.length_less_than")) + .append("100:") + .append(nodeStr); + break; + } + } + } + } + } + + + /** + * 数据转换 + * + * @param row + * @return + */ + private FunctionalCaseExcelData parseDataToModel(Map row) { + FunctionalCaseExcelData data = new FunctionalCaseExcelDataFactory().getFunctionalCaseExcelDataLocal(); + for (Map.Entry headEntry : headMap.entrySet()) { + Integer index = headEntry.getKey(); + String field = headEntry.getValue(); + if (StringUtils.isBlank(field)) { + continue; + } + String value = StringUtils.isEmpty(row.get(index)) ? StringUtils.EMPTY : row.get(index); + + if (excelHeadToFieldNameDic.containsKey(field)) { + field = excelHeadToFieldNameDic.get(field); + } + + if (StringUtils.equals(field, "id")) { + data.setName(value); + } else if (StringUtils.equals(field, "num")) { + data.setNum(value); + } else if (StringUtils.equals(field, "name")) { + data.setName(value); + } else if (StringUtils.equals(field, "module")) { + data.setModule(value); + } else if (StringUtils.equals(field, "tags")) { + data.setTags(value); + } else if (StringUtils.equals(field, "prerequisite")) { + data.setPrerequisite(value); + } else if (StringUtils.equals(field, "description")) { + data.setDescription(value); + } else if (StringUtils.equals(field, "textDescription")) { + data.setTextDescription(value); + } else if (StringUtils.equals(field, "expectedResult")) { + data.setExpectedResult(value); + } else if (StringUtils.equals(field, "caseEditType")) { + data.setCaseEditType(value); + } else { + data.getCustomData().put(field, value); + } + } + return data; + } + + + /** + * 处理合并单元格 + * + * @param data + * @param rowIndex + */ + private void handleMergeData(Map data, Integer rowIndex) { + isMergeRow = false; + isMergeLastRow = false; + if (getNameColIndex() == null) { + throw new MSException(Translator.get("case_import_table_header_missing")); + } + data.keySet().forEach(col -> { + Iterator iterator = mergeInfoSet.iterator(); + while (iterator.hasNext()) { + ExcelMergeInfo mergeInfo = iterator.next(); + // 如果单元格的行号在合并单元格的范围之间,并且列号相等,说明该单元格是合并单元格中的一部分 + if (mergeInfo.getFirstRowIndex() <= rowIndex && rowIndex <= mergeInfo.getLastRowIndex() + && col.equals(mergeInfo.getFirstColumnIndex())) { + // 根据名称列是否是合并单元格判断是不是同一条用例 + if (getNameColIndex().equals(col)) { + isMergeRow = true; + } + // 如果是合并单元格的第一个cell,则把这个单元格的数据存起来 + if (rowIndex.equals(mergeInfo.getFirstRowIndex())) { + if (StringUtils.isNotBlank(data.get(col))) { + mergeCellDataMap.put(mergeInfo, data.get(col)); + } + } else { + // 非第一个,获取存储的数据填充 + String cellData = mergeCellDataMap.get(mergeInfo); + if (StringUtils.isNotBlank(cellData)) { + data.put(col, cellData); + } + } + // 如果合并单元格的最后一个单元格,标记下 + if (rowIndex.equals(mergeInfo.getLastRowIndex())) { + // 根据名称列是否是合并单元格判断是不是同一条用例 + if (getNameColIndex().equals(col)) { + isMergeLastRow = true; + // 清除掉上一次已经遍历完成的数据,提高查询效率 + iterator.remove(); + break; + } + } + } + } + }); + } + + private Integer getNameColIndex() { + return findColIndex("name"); + } + + private Integer getTextDescriptionColIndex() { + return findColIndex("textDescription"); + } + + private Integer getExpectedResultColIndex() { + return findColIndex("expectedResult"); + } + + private Integer findColIndex(String colName) { + for (Integer key : headMap.keySet()) { + if (StringUtils.equals(headMap.get(key), colName)) { + return key; + } + } + return null; + } + + /** + * @description: 获取注解里ExcelProperty的value + */ + public Set genExcelHeadToFieldNameDicAndGetNotRequiredFields() throws NoSuchFieldException { + + Set result = new HashSet<>(); + Field field; + Field[] fields = excelDataClass.getDeclaredFields(); + for (int i = 0; i < fields.length; i++) { + field = excelDataClass.getDeclaredField(fields[i].getName()); + field.setAccessible(true); + ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); + if (excelProperty != null) { + StringBuilder value = new StringBuilder(); + for (String v : excelProperty.value()) { + value.append(v); + } + excelHeadToFieldNameDic.put(value.toString(), field.getName()); + // 检查是否必有的头部信息 + if (field.getAnnotation(NotRequired.class) != null) { + result.add(value.toString()); + } + } + } + return result; + } + + private void formatHeadMap() { + for (Integer key : headMap.keySet()) { + String name = headMap.get(key); + if (excelHeadToFieldNameDic.containsKey(name)) { + headMap.put(key, excelHeadToFieldNameDic.get(name)); + } + } + } + + public List> getErrList() { + return errList; + } + + public int getSuccessCount() { + return successCount; + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/AbstractCustomFieldValidator.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/AbstractCustomFieldValidator.java index a7bae43dff..635b11dc9f 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/AbstractCustomFieldValidator.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/AbstractCustomFieldValidator.java @@ -55,25 +55,8 @@ public abstract class AbstractCustomFieldValidator { protected List parse2Array(String name, String value) throws CustomFieldValidateException { try { - // [a, b] => ["a","b"] - if (!StringUtils.equals(value, "[]")) { - if (!value.contains("[\"")) { - value = value.replace("[", "[\""); - } - if (!value.contains("\"]")) { - value = value.replace("]", "\"]"); - - } - if (!value.contains("\",\"")) { - value = value.replace(",", "\",\""); - - } - if (!value.contains("\",\"")) { - value = value.replace(",", "\",\""); - } - value = value.replace(StringUtils.SPACE, StringUtils.EMPTY); - } - return JSON.parseArray(value, String.class); + //a,b,c => ["a","b","c"] + return JSON.parseArray(JSON.toJSONString(value.split(","))); } catch (Exception e) { CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_array_tip"), name)); } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleMemberValidator.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleMemberValidator.java index 6a8097d6f9..88dcf9d4d9 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleMemberValidator.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleMemberValidator.java @@ -2,6 +2,7 @@ package io.metersphere.functional.excel.validate; import io.metersphere.functional.excel.exception.CustomFieldValidateException; +import io.metersphere.sdk.util.JSON; import io.metersphere.sdk.util.Translator; import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; import org.apache.commons.lang3.StringUtils; @@ -44,6 +45,6 @@ public class CustomFieldMultipleMemberValidator extends CustomFieldMemberValidat keyOrValues.set(i, userNameMap.get(item)); } } - return keyOrValues; + return JSON.toJSONString(keyOrValues); } } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleSelectValidator.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleSelectValidator.java index a4799b287a..b55d2bf97b 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleSelectValidator.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleSelectValidator.java @@ -2,6 +2,7 @@ package io.metersphere.functional.excel.validate; import io.metersphere.functional.excel.exception.CustomFieldValidateException; +import io.metersphere.sdk.util.JSON; import io.metersphere.sdk.util.Translator; import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; import org.apache.commons.lang3.StringUtils; @@ -44,6 +45,6 @@ public class CustomFieldMultipleSelectValidator extends CustomFieldSelectValidat keyOrValues.set(i, nameMap.get(item)); } } - return keyOrValues; + return JSON.toJSONString(keyOrValues); } } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleTextValidator.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleTextValidator.java index 0052b5f996..833c1535f6 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleTextValidator.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleTextValidator.java @@ -2,10 +2,13 @@ package io.metersphere.functional.excel.validate; import io.metersphere.functional.excel.exception.CustomFieldValidateException; +import io.metersphere.sdk.util.JSON; import io.metersphere.sdk.util.Translator; import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; import org.apache.commons.lang3.StringUtils; +import java.util.List; + /** * @author wx */ @@ -32,6 +35,8 @@ public class CustomFieldMultipleTextValidator extends AbstractCustomFieldValidat if (StringUtils.isBlank(keyOrValuesStr)) { return StringUtils.EMPTY; } - return parse2Array(keyOrValuesStr); + List keyOrValues = parse2Array(keyOrValuesStr); + + return JSON.toJSONString(keyOrValues); } } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseFileService.java b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseFileService.java index 5f9e92abe2..6037500cff 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseFileService.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseFileService.java @@ -10,11 +10,13 @@ import io.metersphere.functional.excel.domain.FunctionalCaseExcelData; import io.metersphere.functional.excel.domain.FunctionalCaseExcelDataFactory; import io.metersphere.functional.excel.handler.FunctionCaseTemplateWriteHandler; import io.metersphere.functional.excel.listener.FunctionalCaseCheckEventListener; +import io.metersphere.functional.excel.listener.FunctionalCaseImportEventListener; import io.metersphere.functional.excel.listener.FunctionalCasePretreatmentListener; import io.metersphere.functional.request.FunctionalCaseImportRequest; -import io.metersphere.plugin.sdk.util.MSPluginException; +import io.metersphere.project.mapper.ExtBaseProjectVersionMapper; import io.metersphere.project.service.ProjectTemplateService; import io.metersphere.sdk.constants.TemplateScene; +import io.metersphere.sdk.exception.MSException; import io.metersphere.sdk.util.LogUtils; import io.metersphere.sdk.util.Translator; import io.metersphere.system.domain.CustomFieldOption; @@ -41,6 +43,8 @@ public class FunctionalCaseFileService { @Resource private ProjectTemplateService projectTemplateService; + @Resource + private ExtBaseProjectVersionMapper extBaseProjectVersionMapper; /** @@ -185,7 +189,7 @@ public class FunctionalCaseFileService { */ public FunctionalCaseImportResponse preCheckExcel(FunctionalCaseImportRequest request, MultipartFile file) { if (file == null) { - throw new MSPluginException("file_cannot_be_null"); + throw new MSException(Translator.get("file_cannot_be_null")); } FunctionalCaseImportResponse response = new FunctionalCaseImportResponse(); checkImportExcel(response, request, file); @@ -205,11 +209,11 @@ public class FunctionalCaseFileService { FunctionalCaseCheckEventListener eventListener = new FunctionalCaseCheckEventListener(request, clazz, customFields, mergeInfoSet); EasyExcelFactory.read(file.getInputStream(), eventListener).sheet().doRead(); response.setErrorMessages(eventListener.getErrList()); - response.setSuccessCount(eventListener.getSuccessCount()); + response.setSuccessCount(eventListener.getList().size()); response.setFailCount(eventListener.getErrList().size()); } catch (Exception e) { LogUtils.error("checkImportExcel error", e); - throw new MSPluginException("checkImportExcel error"); + throw new MSException(Translator.get("check_import_excel_error")); } } @@ -218,4 +222,42 @@ public class FunctionalCaseFileService { List customFields = Optional.ofNullable(defaultTemplateDTO.getCustomFields()).orElse(new ArrayList<>()); return customFields; } + + + /** + * 导入excel + * + * @param request + * @param userId + * @param file + */ + public FunctionalCaseImportResponse importExcel(FunctionalCaseImportRequest request, String userId, MultipartFile file) { + if (file == null) { + throw new MSException(Translator.get("file_cannot_be_null")); + } + try { + FunctionalCaseImportResponse response = new FunctionalCaseImportResponse(); + //设置默认版本 + if (StringUtils.isEmpty(request.getVersionId())) { + request.setVersionId(extBaseProjectVersionMapper.getDefaultVersion(request.getProjectId())); + } + //根据本地语言环境选择用哪种数据对象进行存放读取的数据 + Class clazz = new FunctionalCaseExcelDataFactory().getExcelDataByLocal(); + //获取当前项目默认模板的自定义字段 + List customFields = getCustomFields(request.getProjectId()); + Set mergeInfoSet = new TreeSet<>(); + // 预处理,查询合并单元格信息 + EasyExcel.read(file.getInputStream(), null, new FunctionalCasePretreatmentListener(mergeInfoSet)) + .extraRead(CellExtraTypeEnum.MERGE).sheet().doRead(); + FunctionalCaseImportEventListener eventListener = new FunctionalCaseImportEventListener(request, clazz, customFields, mergeInfoSet, userId); + EasyExcelFactory.read(file.getInputStream(), eventListener).sheet().doRead(); + response.setErrorMessages(eventListener.getErrList()); + response.setSuccessCount(eventListener.getSuccessCount()); + response.setFailCount(eventListener.getErrList().size()); + return response; + } catch (Exception e) { + LogUtils.error("checkImportExcel error", e); + throw new MSException(Translator.get("check_import_excel_error")); + } + } } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseModuleService.java b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseModuleService.java index ac4d96e6d2..09d3549846 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseModuleService.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseModuleService.java @@ -42,10 +42,8 @@ import org.mybatis.spring.SqlSessionUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; @Service @@ -68,7 +66,7 @@ public class FunctionalCaseModuleService extends ModuleTreeService { List functionalModuleList = extFunctionalCaseModuleMapper.selectBaseByProjectId(projectId); return super.buildTreeAndCountResource(functionalModuleList, true, Translator.get("default.module")); } - + public String add(FunctionalCaseModuleCreateRequest request, String userId) { FunctionalCaseModule functionalCaseModule = new FunctionalCaseModule(); functionalCaseModule.setId(IDGenerator.nextStr()); @@ -146,7 +144,7 @@ public class FunctionalCaseModuleService extends ModuleTreeService { operationLogService.batchAdd(dtoList); } - public List deleteModuleByIds(ListdeleteIds, ListfunctionalCases){ + public List deleteModuleByIds(List deleteIds, List functionalCases) { if (CollectionUtils.isEmpty(deleteIds)) { return functionalCases; } @@ -176,7 +174,6 @@ public class FunctionalCaseModuleService extends ModuleTreeService { /** * 查找当前项目下模块每个节点对应的资源统计 - * */ public Map getModuleCountMap(String projectId, List moduleCountDTOList) { @@ -212,7 +209,7 @@ public class FunctionalCaseModuleService extends ModuleTreeService { } example.clear(); } - example.createCriteria().andParentIdEqualTo(functionalCaseModule.getParentId()).andNameEqualTo(functionalCaseModule.getName()).andIdNotEqualTo(functionalCaseModule.getId()); + example.createCriteria().andParentIdEqualTo(functionalCaseModule.getParentId()).andNameEqualTo(functionalCaseModule.getName()).andIdNotEqualTo(functionalCaseModule.getId()).andProjectIdEqualTo(functionalCaseModule.getProjectId()); if (functionalCaseModuleMapper.countByExample(example) > 0) { throw new MSException(Translator.get("node.name.repeat")); } @@ -269,18 +266,148 @@ public class FunctionalCaseModuleService extends ModuleTreeService { return super.buildTreeAndCountResource(nodeByNodeIds, true, Translator.get("default.module")); } - public List getNodeByNodeIds(ListmoduleIds){ + public List getNodeByNodeIds(List moduleIds) { List finalModuleIds = new ArrayList<>(moduleIds); List totalList = new ArrayList<>(); - while (CollectionUtils.isNotEmpty(finalModuleIds)) { + while (CollectionUtils.isNotEmpty(finalModuleIds)) { List modules = extFunctionalCaseModuleMapper.selectBaseByIds(finalModuleIds); totalList.addAll(modules); List finalModuleIdList = finalModuleIds; - List parentModuleIds = modules.stream().map(BaseTreeNode::getParentId).filter(parentId -> !StringUtils.equalsIgnoreCase(parentId,ModuleConstants.ROOT_NODE_PARENT_ID) && !finalModuleIdList.contains(parentId)).toList(); + List parentModuleIds = modules.stream().map(BaseTreeNode::getParentId).filter(parentId -> !StringUtils.equalsIgnoreCase(parentId, ModuleConstants.ROOT_NODE_PARENT_ID) && !finalModuleIdList.contains(parentId)).toList(); finalModuleIds.clear(); finalModuleIds = new ArrayList<>(parentModuleIds); } return totalList.stream().distinct().toList(); } + + /** + * 根据模块路径创建模块 + * + * @param modulePath 模块路径 + * @param projectId 项目ID + * @param moduleTree 已存在的模块树 + * @param userId userId + */ + public Map createCaseModule(List modulePath, String projectId, List moduleTree, String userId, Map pathMap) { + modulePath.forEach(path -> { + List moduleNames = new ArrayList<>(List.of(path.split("/"))); + Iterator itemIterator = moduleNames.iterator(); + AtomicReference hasNode = new AtomicReference<>(false); + //当前节点模块名称 + String currentModuleName; + if (moduleNames.size() <= 1) { + throw new MSException(Translator.get("test_case_create_module_fail") + ":" + path); + } else { + itemIterator.next(); + itemIterator.remove(); + currentModuleName = itemIterator.next().trim(); + moduleTree.forEach(module -> { + //根节点是否存在 + if (StringUtils.equals(currentModuleName, module.getName())) { + hasNode.set(true); + //根节点存在,检查子节点是否存在 + createModuleByPathIterator(itemIterator, "/" + currentModuleName, module, pathMap, projectId, userId); + } + }); + } + if (!hasNode.get()) { + //根节点不存在,直接创建 + createModuleByPath(itemIterator, currentModuleName, null, projectId, StringUtils.EMPTY, pathMap, userId); + } + }); + return pathMap; + } + + + /** + * 根据模块路径迭代器遍历模块路径 + * + * @param itemIterator 模块路径迭代器 + * @param currentModulePath 当前节点路径: /模块1/模块2 + * @param module 当前模块对象 + * @param pathMap 记录新创建的模块路径和模块ID + * @param projectId 项目id + * @param userId userId + */ + private void createModuleByPathIterator(Iterator itemIterator, String currentModulePath, BaseTreeNode module, Map pathMap, String projectId, String userId) { + List children = module.getChildren(); + if (CollectionUtils.isEmpty(children) || !itemIterator.hasNext()) { + //没有子节点,根据当前模块目录创建模块节点 + pathMap.put(currentModulePath, module.getId()); + if (itemIterator.hasNext()) { + createModuleByPath(itemIterator, itemIterator.next().trim(), module, projectId, currentModulePath, pathMap, userId); + } + return; + } + String nodeName = itemIterator.next().trim(); + AtomicReference hasNode = new AtomicReference<>(false); + children.forEach(child -> { + if (StringUtils.equals(nodeName, child.getName())) { + hasNode.set(true); + createModuleByPathIterator(itemIterator, currentModulePath + "/" + child.getName(), child, pathMap, projectId, userId); + } + }); + + //若子节点中不包含该目标节点,则在该节点下创建 + if (!hasNode.get()) { + createModuleByPath(itemIterator, nodeName, module, projectId, currentModulePath, pathMap, userId); + } + + } + + /** + * 遍历模块路径,创建模块 + * + * @param itemIterator 模块路径迭代器 + * @param moduleName 当前模块名称: 模块1 + * @param parentModule 父模块对象 + * @param projectId 项目id + * @param currentPath 当前模块路径: /模块1 + * @param pathMap 记录新创建的模块路径和模块ID + */ + private void createModuleByPath(Iterator itemIterator, String moduleName, BaseTreeNode parentModule, String projectId, String currentPath, Map pathMap, String userId) { + StringBuilder path = new StringBuilder(currentPath); + path.append("/" + moduleName.trim()); + + //模块id + String pid; + if (pathMap.get(path.toString()) != null) { + //如果创建过,直接获取模块ID + pid = pathMap.get(path.toString()); + } else { + pid = insertNode(moduleName, parentModule == null ? ModuleConstants.ROOT_NODE_PARENT_ID : parentModule.getId(), projectId, userId); + pathMap.put(path.toString(), pid); + } + + while (itemIterator.hasNext()) { + String nextModuleName = itemIterator.next().trim(); + path.append("/" + nextModuleName); + if (pathMap.get(path.toString()) != null) { + pid = pathMap.get(path.toString()); + } else { + pid = insertNode(nextModuleName, pid, projectId, userId); + pathMap.put(path.toString(), pid); + } + } + } + + + /** + * 创建模块 + * + * @param moduleName 模块名称 + * @param parentId 父模块ID + * @param projectId 项目ID + * @param userId userId + * @return + */ + private String insertNode(String moduleName, String parentId, String projectId, String userId) { + FunctionalCaseModuleCreateRequest request = new FunctionalCaseModuleCreateRequest(); + request.setProjectId(projectId); + request.setName(moduleName); + request.setParentId(parentId); + return this.add(request, userId); + } + } \ No newline at end of file diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseService.java b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseService.java index ae0564c971..5821b4986c 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseService.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseService.java @@ -5,8 +5,10 @@ import io.metersphere.bug.mapper.BugRelationCaseMapper; import io.metersphere.functional.constants.CaseEvent; import io.metersphere.functional.constants.CaseFileSourceType; import io.metersphere.functional.constants.FunctionalCaseReviewStatus; +import io.metersphere.functional.constants.FunctionalCaseTypeConstants; import io.metersphere.functional.domain.*; import io.metersphere.functional.dto.*; +import io.metersphere.functional.excel.domain.FunctionalCaseExcelData; import io.metersphere.functional.mapper.*; import io.metersphere.functional.request.*; import io.metersphere.functional.result.CaseManagementResultCode; @@ -25,6 +27,7 @@ import io.metersphere.sdk.exception.MSException; import io.metersphere.sdk.util.BeanUtils; import io.metersphere.sdk.util.Translator; import io.metersphere.system.domain.CustomFieldOption; +import io.metersphere.system.dto.sdk.BaseTreeNode; import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; import io.metersphere.system.dto.sdk.TemplateDTO; import io.metersphere.system.dto.sdk.request.PosRequest; @@ -195,16 +198,6 @@ public class FunctionalCaseService { return functionalCase; } - private List getCustomFields(Map customFields) { - List list = new ArrayList<>(); - customFields.keySet().forEach(key -> { - CaseCustomFieldDTO caseCustomFieldDTO = new CaseCustomFieldDTO(); - caseCustomFieldDTO.setFieldId(key); - caseCustomFieldDTO.setValue(customFields.get(key).toString()); - list.add(caseCustomFieldDTO); - }); - return list; - } public Long getNextOrder(String projectId) { Long pos = extFunctionalCaseMapper.getPos(projectId); @@ -771,4 +764,183 @@ public class FunctionalCaseService { extFunctionalCaseMapper::getLastPos, functionalCaseMapper::updateByPrimaryKeySelective); } + + public String checkNumExist(String num, String projectId) { + FunctionalCaseExample example = new FunctionalCaseExample(); + example.createCriteria().andNumEqualTo(Long.valueOf(num)).andProjectIdEqualTo(projectId).andDeletedEqualTo(false); + List functionalCases = functionalCaseMapper.selectByExample(example); + if (CollectionUtils.isNotEmpty(functionalCases)) { + return functionalCases.get(0).getId(); + } + return null; + } + + + /** + * 导入新建数据 + * + * @param list 导入数据集合 + * @param request request + * @param moduleTree 模块树 + * @param userId 用户id + * @param customFieldsMap 当前默认模板的自定义字段 + */ + public void saveImportData(List list, FunctionalCaseImportRequest request, List moduleTree, String userId, Map customFieldsMap, Map pathMap) { + //默认模板 + TemplateDTO defaultTemplateDTO = projectTemplateService.getDefaultTemplateDTO(request.getProjectId(), TemplateScene.FUNCTIONAL.name()); + //模块路径 + List modulePath = list.stream().map(FunctionalCaseExcelData::getModule).toList(); + //构建模块树 + Map caseModulePathMap = functionalCaseModuleService.createCaseModule(modulePath, request.getProjectId(), moduleTree, userId, pathMap); + + SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); + FunctionalCaseMapper caseMapper = sqlSession.getMapper(FunctionalCaseMapper.class); + FunctionalCaseBlobMapper caseBlobMapper = sqlSession.getMapper(FunctionalCaseBlobMapper.class); + FunctionalCaseCustomFieldMapper customFieldMapper = sqlSession.getMapper(FunctionalCaseCustomFieldMapper.class); + Long nextOrder = getNextOrder(request.getProjectId()); + for (int i = 0; i < list.size(); i++) { + parseInsertDataToModule(list.get(i), request, userId, caseModulePathMap, defaultTemplateDTO, nextOrder, caseMapper, caseBlobMapper, customFieldMapper, customFieldsMap); + } + sqlSession.flushStatements(); + SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory); + } + + private void parseInsertDataToModule(FunctionalCaseExcelData functionalCaseExcelData, FunctionalCaseImportRequest request, String userId, Map caseModulePathMap, TemplateDTO defaultTemplateDTO, Long nextOrder, + FunctionalCaseMapper caseMapper, FunctionalCaseBlobMapper caseBlobMapper, FunctionalCaseCustomFieldMapper customFieldMapper, Map customFieldsMap) { + //构建用例 + FunctionalCase functionalCase = new FunctionalCase(); + String caseId = IDGenerator.nextStr(); + functionalCase.setId(caseId); + functionalCase.setNum(getNextNum(request.getProjectId())); + functionalCase.setModuleId(caseModulePathMap.get(functionalCaseExcelData.getModule())); + functionalCase.setProjectId(request.getProjectId()); + functionalCase.setTemplateId(defaultTemplateDTO.getId()); + functionalCase.setName(functionalCaseExcelData.getName()); + functionalCase.setReviewStatus(FunctionalCaseReviewStatus.UN_REVIEWED.name()); + functionalCase.setTags(handleImportTags(functionalCaseExcelData.getTags())); + functionalCase.setCaseEditType(StringUtils.defaultIfBlank(functionalCaseExcelData.getCaseEditType(), FunctionalCaseTypeConstants.CaseEditType.TEXT.name())); + functionalCase.setPos(nextOrder + ServiceUtils.POS_STEP); + functionalCase.setVersionId(request.getVersionId()); + functionalCase.setRefId(caseId); + functionalCase.setLastExecuteResult(FunctionalCaseExecuteResult.UN_EXECUTED.name()); + functionalCase.setLatest(true); + functionalCase.setCreateUser(userId); + functionalCase.setCreateTime(System.currentTimeMillis()); + functionalCase.setUpdateTime(System.currentTimeMillis()); + caseMapper.insertSelective(functionalCase); + + //用例附属表 + FunctionalCaseBlob caseBlob = new FunctionalCaseBlob(); + caseBlob.setId(caseId); + caseBlob.setSteps(StringUtils.defaultIfBlank(functionalCaseExcelData.getSteps(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8)); + caseBlob.setTextDescription(StringUtils.defaultIfBlank(functionalCaseExcelData.getTextDescription(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8)); + caseBlob.setExpectedResult(StringUtils.defaultIfBlank(functionalCaseExcelData.getExpectedResult(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8)); + caseBlob.setPrerequisite(StringUtils.defaultIfBlank(functionalCaseExcelData.getPrerequisite(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8)); + caseBlob.setDescription(StringUtils.defaultIfBlank(functionalCaseExcelData.getDescription(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8)); + caseBlobMapper.insertSelective(caseBlob); + + //自定义字段 + handleImportCustomField(functionalCaseExcelData, caseId, customFieldMapper, customFieldsMap); + } + + + /** + * 处理导入标签 + * + * @param tags 标签 + * @return + */ + private List handleImportTags(String tags) { + List split = List.of(tags.split("[,;]")); + return split.stream().map(String::trim).filter(StringUtils::isNotEmpty).collect(Collectors.toList()); + } + + + /** + * 处理导入自定义字段 + * + * @param functionalCaseExcelData 导入数据 + * @param caseId 用例id + * @param customFieldMapper 自定义字段mapper + * @param customFieldsMap 当前默认模板的自定义字段 + */ + private void handleImportCustomField(FunctionalCaseExcelData functionalCaseExcelData, String caseId, FunctionalCaseCustomFieldMapper customFieldMapper, Map customFieldsMap) { + //需要保存的自定义字段 + Map customData = functionalCaseExcelData.getCustomData(); + customData.forEach((k, v) -> { + if (customFieldsMap.containsKey(k)) { + TemplateCustomFieldDTO templateCustomFieldDTO = customFieldsMap.get(k); + FunctionalCaseCustomField caseCustomField = new FunctionalCaseCustomField(); + caseCustomField.setCaseId(caseId); + caseCustomField.setFieldId(templateCustomFieldDTO.getFieldId()); + caseCustomField.setValue(v.toString()); + customFieldMapper.insertSelective(caseCustomField); + } + }); + } + + + /** + * 导入更新数据 + * + * @param updateList 更新数据集合 + * @param request request + * @param moduleTree 模块树 + * @param userId 用户id + * @param customFieldsMap 当前默认模板的自定义字段 + */ + public void updateImportData(List updateList, FunctionalCaseImportRequest request, List moduleTree, String userId, Map customFieldsMap, Map pathMap) { + //默认模板 + TemplateDTO defaultTemplateDTO = projectTemplateService.getDefaultTemplateDTO(request.getProjectId(), TemplateScene.FUNCTIONAL.name()); + //模块路径 + List modulePath = updateList.stream().map(FunctionalCaseExcelData::getModule).toList(); + //构建模块树 + Map caseModulePathMap = functionalCaseModuleService.createCaseModule(modulePath, request.getProjectId(), moduleTree, userId, pathMap); + SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); + FunctionalCaseMapper caseMapper = sqlSession.getMapper(FunctionalCaseMapper.class); + FunctionalCaseBlobMapper caseBlobMapper = sqlSession.getMapper(FunctionalCaseBlobMapper.class); + FunctionalCaseCustomFieldMapper customFieldMapper = sqlSession.getMapper(FunctionalCaseCustomFieldMapper.class); + for (int i = 0; i < updateList.size(); i++) { + parseUpdateDataToModule(updateList.get(i), request, userId, caseModulePathMap, defaultTemplateDTO, caseMapper, caseBlobMapper, customFieldMapper, customFieldsMap); + } + + sqlSession.flushStatements(); + SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory); + } + + private void parseUpdateDataToModule(FunctionalCaseExcelData functionalCaseExcelData, FunctionalCaseImportRequest request, String userId, Map caseModulePathMap, TemplateDTO defaultTemplateDTO, FunctionalCaseMapper caseMapper, FunctionalCaseBlobMapper caseBlobMapper, FunctionalCaseCustomFieldMapper customFieldMapper, Map customFieldsMap) { + //用例表 + FunctionalCase functionalCase = caseMapper.selectByPrimaryKey(functionalCaseExcelData.getNum()); + functionalCase.setName(functionalCaseExcelData.getName()); + functionalCase.setModuleId(caseModulePathMap.get(functionalCaseExcelData.getModule())); + functionalCase.setTags(handleImportTags(functionalCaseExcelData.getTags())); + functionalCase.setCaseEditType(StringUtils.defaultIfBlank(functionalCaseExcelData.getCaseEditType(), FunctionalCaseTypeConstants.CaseEditType.TEXT.name())); + //模板 + functionalCase.setTemplateId(defaultTemplateDTO.getId()); + functionalCase.setVersionId(request.getVersionId()); + functionalCase.setUpdateUser(userId); + functionalCase.setUpdateTime(System.currentTimeMillis()); + caseMapper.updateByPrimaryKeySelective(functionalCase); + + //用例附属表 + FunctionalCaseBlob caseBlob = new FunctionalCaseBlob(); + caseBlob.setId(functionalCase.getId()); + caseBlob.setSteps(StringUtils.defaultIfBlank(functionalCaseExcelData.getSteps(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8)); + caseBlob.setTextDescription(StringUtils.defaultIfBlank(functionalCaseExcelData.getTextDescription(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8)); + caseBlob.setExpectedResult(StringUtils.defaultIfBlank(functionalCaseExcelData.getExpectedResult(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8)); + caseBlob.setPrerequisite(StringUtils.defaultIfBlank(functionalCaseExcelData.getPrerequisite(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8)); + caseBlob.setDescription(StringUtils.defaultIfBlank(functionalCaseExcelData.getDescription(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8)); + caseBlobMapper.updateByPrimaryKeyWithBLOBs(caseBlob); + + //自定义字段 + handleUpdateCustomField(functionalCaseExcelData, functionalCase.getId(), customFieldMapper, customFieldsMap); + + } + + private void handleUpdateCustomField(FunctionalCaseExcelData functionalCaseExcelData, String caseId, FunctionalCaseCustomFieldMapper customFieldMapper, Map customFieldsMap) { + FunctionalCaseCustomFieldExample fieldExample = new FunctionalCaseCustomFieldExample(); + fieldExample.createCriteria().andCaseIdEqualTo(caseId); + customFieldMapper.deleteByExample(fieldExample); + handleImportCustomField(functionalCaseExcelData, caseId, customFieldMapper, customFieldsMap); + } } diff --git a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseControllerTests.java b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseControllerTests.java index e1158276c4..5a8047146e 100644 --- a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseControllerTests.java +++ b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseControllerTests.java @@ -72,6 +72,7 @@ public class FunctionalCaseControllerTests extends BaseTest { public static final String FUNCTIONAL_CASE_POS_URL = "/functional/case/edit/pos"; public static final String DOWNLOAD_EXCEL_TEMPLATE_URL = "/functional/case/download/excel/template/"; public static final String CHECK_EXCEL_URL = "/functional/case/pre-check/excel"; + public static final String IMPORT_EXCEL_URL = "/functional/case/import/excel"; @Resource private NotificationMapper notificationMapper; @@ -132,7 +133,7 @@ public class FunctionalCaseControllerTests extends BaseTest { paramMap = new LinkedMultiValueMap<>(); paramMap.add("request", JSON.toJSONString(request)); paramMap.add("files", new LinkedMultiValueMap<>()); - functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(FUNCTIONAL_CASE_ADD_URL, paramMap); + functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(FUNCTIONAL_CASE_ADD_URL, paramMap); // 获取返回值 returnData = functionalCaseMvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); resultHolder = JSON.parseObject(returnData, ResultHolder.class); @@ -140,7 +141,7 @@ public class FunctionalCaseControllerTests extends BaseTest { Assertions.assertNotNull(resultHolder); functionalCase = JSON.parseObject(JSON.toJSONString(resultHolder.getData()), FunctionalCase.class); FunctionalCaseEditRequest request1 = new FunctionalCaseEditRequest(); - BeanUtils.copyBean(request1,request); + BeanUtils.copyBean(request1, request); request1.setId(functionalCase.getId()); request1.setRelateFileMetaIds(new ArrayList<>()); paramMap = new LinkedMultiValueMap<>(); @@ -559,7 +560,7 @@ public class FunctionalCaseControllerTests extends BaseTest { @Test - @Order(20) + @Order(18) public void testImportCheckExcel() throws Exception { String filePath = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/1.xlsx")).getPath(); MockMultipartFile file = new MockMultipartFile("file", "11.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath)); @@ -569,7 +570,7 @@ public class FunctionalCaseControllerTests extends BaseTest { LinkedMultiValueMap paramMap = new LinkedMultiValueMap<>(); paramMap.add("request", JSON.toJSONString(request)); paramMap.add("file", file); - MvcResult functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(CHECK_EXCEL_URL,paramMap); + MvcResult functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(CHECK_EXCEL_URL, paramMap); String functionalCaseImportResponseData = functionalCaseMvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); ResultHolder functionalCaseResultHolder = JSON.parseObject(functionalCaseImportResponseData, ResultHolder.class); @@ -579,7 +580,7 @@ public class FunctionalCaseControllerTests extends BaseTest { paramMap = new LinkedMultiValueMap<>(); paramMap.add("request", JSON.toJSONString(request)); - this.requestMultipart(CHECK_EXCEL_URL,paramMap); + this.requestMultipart(CHECK_EXCEL_URL, paramMap); //覆盖异常 String filePath2 = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/2.xlsx")).getPath(); @@ -587,6 +588,47 @@ public class FunctionalCaseControllerTests extends BaseTest { paramMap = new LinkedMultiValueMap<>(); paramMap.add("request", JSON.toJSONString(request)); paramMap.add("file", file2); - this.requestMultipart(CHECK_EXCEL_URL,paramMap); + this.requestMultipart(CHECK_EXCEL_URL, paramMap); + } + + + @Test + @Order(19) + public void testImportExcel() throws Exception { + String filePath = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/3.xlsx")).getPath(); + MockMultipartFile file = new MockMultipartFile("file", "11.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath)); + FunctionalCaseImportRequest request = new FunctionalCaseImportRequest(); + request.setCover(true); + request.setProjectId("100001100001"); + LinkedMultiValueMap paramMap = new LinkedMultiValueMap<>(); + paramMap.add("request", JSON.toJSONString(request)); + paramMap.add("file", file); + MvcResult functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(IMPORT_EXCEL_URL, paramMap); + + String functionalCaseImportResponseData = functionalCaseMvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + ResultHolder functionalCaseResultHolder = JSON.parseObject(functionalCaseImportResponseData, ResultHolder.class); + FunctionalCaseImportResponse functionalCaseImportResponse = JSON.parseObject(JSON.toJSONString(functionalCaseResultHolder.getData()), FunctionalCaseImportResponse.class); + Assertions.assertNotNull(functionalCaseImportResponse); + + + String filePath1 = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/4.xlsx")).getPath(); + MockMultipartFile file1 = new MockMultipartFile("file", "14.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath1)); + paramMap = new LinkedMultiValueMap<>(); + paramMap.add("request", JSON.toJSONString(request)); + paramMap.add("file", file1); + this.requestMultipart(IMPORT_EXCEL_URL, paramMap); + + + paramMap = new LinkedMultiValueMap<>(); + paramMap.add("request", JSON.toJSONString(request)); + this.requestMultipart(IMPORT_EXCEL_URL, paramMap); + + //覆盖异常 + String filePath2 = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/2.xlsx")).getPath(); + MockMultipartFile file2 = new MockMultipartFile("file", "22.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath2)); + paramMap = new LinkedMultiValueMap<>(); + paramMap.add("request", JSON.toJSONString(request)); + paramMap.add("file", file2); + this.requestMultipart(IMPORT_EXCEL_URL, paramMap); } } diff --git a/backend/services/case-management/src/test/resources/dml/init_file_metadata_test.sql b/backend/services/case-management/src/test/resources/dml/init_file_metadata_test.sql index 994e9c6c8a..678b4a579b 100644 --- a/backend/services/case-management/src/test/resources/dml/init_file_metadata_test.sql +++ b/backend/services/case-management/src/test/resources/dml/init_file_metadata_test.sql @@ -72,3 +72,7 @@ VALUES ('wx_review_id',10006,'测试重新提审', 'test_module_one', 'TEST_MODU INSERT INTO project_version(id, project_id, name, description, status, latest, publish_time, start_time, end_time, create_time, create_user) VALUES ('v2.0.1', 'wx_relationship', 'v1.0', NULL, 'open', b'1', 1698810592000, 1698810592000, 1698810592000, 1698810592000, 'admin'); +INSERT INTO functional_case(id, num, module_id, project_id, template_id, name, review_status, tags, case_edit_type, pos, version_id, ref_id, last_execute_result, deleted, public_case, latest, create_user, update_user, delete_user, create_time, update_time, delete_time) +VALUES ('TEST_IMPORT', 10, 'TEST_MODULE_ID_GYQ', '100001100001', '100001', '1223', 'UN_REVIEWED', NULL, 'STEP', 0, 'v3.0.0', 'wx_ref_id', 'UN_EXECUTED', b'0', b'0', b'1', 'admin', 'admin', '', 1698058347559, 1698058347559, NULL); + +INSERT INTO functional_case_blob(id, steps, text_description, expected_result, prerequisite, description) VALUES ('TEST_IMPORT', 'STEP', '1111', '', '', 'TEST'); \ No newline at end of file diff --git a/backend/services/case-management/src/test/resources/file/1.xlsx b/backend/services/case-management/src/test/resources/file/1.xlsx index ba2e84155d..f11ebed386 100644 Binary files a/backend/services/case-management/src/test/resources/file/1.xlsx and b/backend/services/case-management/src/test/resources/file/1.xlsx differ diff --git a/backend/services/case-management/src/test/resources/file/2.xlsx b/backend/services/case-management/src/test/resources/file/2.xlsx index 65b5cd6b2c..a23e7f5271 100644 Binary files a/backend/services/case-management/src/test/resources/file/2.xlsx and b/backend/services/case-management/src/test/resources/file/2.xlsx differ diff --git a/backend/services/case-management/src/test/resources/file/3.xlsx b/backend/services/case-management/src/test/resources/file/3.xlsx new file mode 100644 index 0000000000..02d730c6ad Binary files /dev/null and b/backend/services/case-management/src/test/resources/file/3.xlsx differ diff --git a/backend/services/case-management/src/test/resources/file/4.xlsx b/backend/services/case-management/src/test/resources/file/4.xlsx new file mode 100644 index 0000000000..c285adae5b Binary files /dev/null and b/backend/services/case-management/src/test/resources/file/4.xlsx differ