From 99d294694c6b6a4e2fd29518598813b0b13d447d Mon Sep 17 00:00:00 2001 From: WangXu10 Date: Tue, 23 Jan 2024 21:02:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=8A=9F=E8=83=BD=E7=94=A8=E4=BE=8B):=20?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E7=94=A8=E4=BE=8Bexcel=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/i18n/commons_en_US.properties | 4 +- .../resources/i18n/commons_zh_CN.properties | 3 +- .../resources/i18n/commons_zh_TW.properties | 4 +- .../FunctionalCaseTypeConstants.java | 11 + .../controller/FunctionalCaseController.java | 9 + .../excel/domain/FunctionalCaseExcelData.java | 6 +- .../FunctionalCaseCheckEventListener.java | 20 +- .../FunctionalCaseImportEventListener.java | 657 ++++++++++++++++++ .../AbstractCustomFieldValidator.java | 21 +- .../CustomFieldMultipleMemberValidator.java | 3 +- .../CustomFieldMultipleSelectValidator.java | 3 +- .../CustomFieldMultipleTextValidator.java | 7 +- .../service/FunctionalCaseFileService.java | 50 +- .../service/FunctionalCaseModuleService.java | 149 +++- .../service/FunctionalCaseService.java | 192 ++++- .../FunctionalCaseControllerTests.java | 54 +- .../resources/dml/init_file_metadata_test.sql | 4 + .../src/test/resources/file/1.xlsx | Bin 6436 -> 12090 bytes .../src/test/resources/file/2.xlsx | Bin 11916 -> 11939 bytes .../src/test/resources/file/3.xlsx | Bin 0 -> 12125 bytes .../src/test/resources/file/4.xlsx | Bin 0 -> 11936 bytes 21 files changed, 1127 insertions(+), 70 deletions(-) create mode 100644 backend/services/case-management/src/main/java/io/metersphere/functional/constants/FunctionalCaseTypeConstants.java create mode 100644 backend/services/case-management/src/main/java/io/metersphere/functional/excel/listener/FunctionalCaseImportEventListener.java create mode 100644 backend/services/case-management/src/test/resources/file/3.xlsx create mode 100644 backend/services/case-management/src/test/resources/file/4.xlsx 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 ba2e84155d69afd0e77adfb85ef1405c95a2d152..f11ebed38645494808d1d52fd01572140e063133 100644 GIT binary patch literal 12090 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 delta 2612 zcmY+GS5y-S6NN*S(3={GgeENnWLd;b=nE({5DXopsuU@r5Eg=w(2EF!2mv7=H4u7J zx*$yj>5*PkY62oGinP_2-T$A5Idjha-tL(@Q{a^3RK3CuJeF${D`Ex!cnRC=Al#f^ zwz6pIy+0t1D!Qewd0_y z!D6dvg^81#Tl|paSBp*R5kCIqcv)j__|-E2hFambW!YSlekAAQAr2lQYPkpv?(^ko zqN2ZFvtBxfsC#MPV%95n zFGVwn`Ynf6t$5Q>GC19(HHE{Q_j(pxBHG2RL0*C*r1m@+J>na8jf7$wkuLIR{h}M< zcBq#JJiOSX8OL#=M^dd>YUcOvru>Dwpl)#kRo>23z~5@#OU`FrSQ8y*Da-Xs-eeU- zBAO&o0x!cs!7=$5gb3ne-NQa$YxgjGJtsLw3m6)K6Wl}K=7ywL3%cY|>s6+T$Il^p znZ|?O2O(({u_36QFlX;%?d&BXx z)7?Yb0$E!k&wOUmUiO02*(uHOOVU9CGYf)Qvf}sDr})~Yry9S$(yr?Q8Zz^yN!(0s zVF3V6xd8wWfZzfX!963)_!TJg$86hdsN1&lu^X2ua#E}xm+e2dY$yrqgW%7Kl`@{8 z87yk(EQIXrI zMd34SlXHxfosg&?Wev+RUC`NR!!h>-$l(vhlCmZ7%ShLwU5PN*13;w@Y*iZP09(ziAd)k{7 z84pnlqLPPeSBjDc(4>~vOPk2*161Ssay=Gv$hE^(XUn(71r+MR_bC5N{$7F+7>auh zyd^xN`2f6UZHs|wf*icKQvwTTi)=4bA%FZ z80D74^;5{e>2+=3fIZf_>O!`hjBcE-#WMu}i|ge*mR8qD+43h_b7$D|mGK262-$Q?x+(#UgNSaZTO=R^ZN&*d3y1A=Lr!X zU)hCZ+p6q~?KE%M!Fp{AuhD61w=QTMDJ zk}YDF&K}tOxU}oDR9||+#z}y_4}KV-U)g8Qx?qoAHe-F(u#9*MR2Ukfl0@Xa3aNrc z@#@`Q)y2Eemi9JAcG4ROII$`%#}PI0lKxhc5nG}?*^o$~yosYirD~zbVw7~(S2z7k zV-Ks)OoBGN=+x`NCaTY@u%vK(AdV_4RSQ8Dk4SZWRr$qehGrI)Xi#!%WP*$h?q&jE*nHq^d}2r;YUuTFJ4z2`AU|3wTWS$7c;(#{?jxg9Guyi=OEDG z8Sp}QdFKY($((CShu)AFpe{!A9tZ2+iOe!}htjG{IFB*7-prC1JH9*OY)@0ltZ$DD zkI{pfrmMb9)A@W_E4lY^-)2olr;t`7=WT5!5qhc;a?Fa}b{j!4HxvHSFi72XG`Tq; zqEde*_V&Sn9h6(BA-br7$JQC2$ZVu8V;ZA2500r30$a>Vzsiq%QC6|6rNIyZD`|Zf z`4X1`#91%C{FM8V9h99L$7?7bT6j=z#g)RW$HrNY$&?X34bimyd~(;57;%I6(7y{A z`$Df#IOjtPrjq`6hM_ic!<$|9WTC0{sL{CTbBcCyhX$q%mTh5N<)RYMTaQT@80{@9Wr%{5sk`|K{Ayi3<9is3{ F`UmX$#tHxc 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 0000000000000000000000000000000000000000..02d730c6adf9428129c1b3b695776f91a96d15d2 GIT binary patch literal 12125 zcmeHt^;;cTv-LrPyF-xR5Zv9}HMj(KcZXm>gImzx?oM!bcXubae{+%dP z3cT&}v1H~?(S2UeJgB!s%7LOP)uHjOyXi+4hN7{hWw9)-B_#t3^tct^;Wb5!VQF4t zYf~l(dPt(7i#YuY2+=aVw=OyNqab8)R`e!DP^W&qU!$ce z0Wa)W+;(O%0MyBG(~1jIm_W4|BDbZGj+i=0u41HWhm z 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 S{-ovM0ct?tpHct(_5T2meMmF_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c285adae5becfd398707df3db2f7347d7af91cd7 GIT binary patch literal 11936 zcmeHtWmjF<()B@u1a}GU1b26L4esvlPJrOxCaX^!QI{6?LA4~JGz_Ocf3E~ zx%LD}p?5P>LJ?DDeINZU2Wype$ifwu>H7_6FPB{NppN`BJsc(R1 zc1&)2a~S~2_)oLSGc%YVwJAdPh4GzM6a<#4O=tYsN(ZuxFVvKv99x$8BNTi7F$yLT z9aRpu{H&O^q-EwiRDLvJk^xl?h5cTlcS>}u$|b%4xO?CBj#=`l=V@;$y2r3Bhe?T~X z$ue;E`p+OiG}9{T0S1U5BmjT_92qw&I#*i8Wgv z4oy@JHLLSbq=wQ_pb-s5RA>%(8@3-XbP$LQOQp>#NTSbp*lwLppO^X-_nJ!;U7-L4 zq4p-$Sa)suSac;EiloVUsg&CCuu?oXGNl|cGChqn4&Twhb9Lnj&#(!{={n@AQ}w*c z)Ku)uX9&`ECPBU9L&urTAUvV91g^AIJfzV``K3i@553{S6gu+ywWwr@IT~7tv}?xg zLQZ1wM;$Mq-jIL&7K;Xq5HKk+)Ul4C}>bQNE6!EX^!oKK059e0$>#kBqB#1a!(3TF%7hYgS=NHgRbohr;GF7-U79K_oGBaq= z0lG;du#eG{$2o3^tPPl@-ggs$7^#@o|A1g{&olN=V}Hpt8PNLV^=WKA`s(4r&7X_Y z0@c%;CE%w2me}ZsTALqUfO4;>n>>bOye&~$Z1Y+lH@J1f$8;)ugR9Eky@r8|;2E-0O0~ z0>2K1a@9dy5{3-!fns&S1z|gUF$@M}a8!;LAO?Yge7Jw`

r&%@L)MH?Hq}7ApJp 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 literal 0 HcmV?d00001