From d2ee9bc0f19694ca491a5c718d0eb7a78d9f8f33 Mon Sep 17 00:00:00 2001 From: song-tianyang Date: Mon, 10 May 2021 14:32:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=A1=88=E4=BE=8B=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 功能案例导入增加自定义ID的判断、增加忽略错误继续导入功能 --- .../service/ApiTestEnvironmentService.java | 46 +++ .../FunctionCaseTemplateWriteHandler.java | 87 +++++ .../TestCaseDataIgnoreErrorListener.java | 338 ++++++++++++++++++ .../excel/utils/EasyExcelExporter.java | 16 + .../service/SystemParameterService.java | 8 + .../track/controller/TestCaseController.java | 7 + .../track/service/TestCaseService.java | 120 ++++++- .../io/metersphere/xmind/XmindCaseParser.java | 47 +++ .../track/case/components/TestCaseImport.vue | 98 ++++- frontend/src/i18n/en-US.js | 2 + frontend/src/i18n/zh-CN.js | 2 + frontend/src/i18n/zh-TW.js | 2 + 12 files changed, 751 insertions(+), 22 deletions(-) create mode 100644 backend/src/main/java/io/metersphere/excel/handler/FunctionCaseTemplateWriteHandler.java create mode 100644 backend/src/main/java/io/metersphere/excel/listener/TestCaseDataIgnoreErrorListener.java diff --git a/backend/src/main/java/io/metersphere/api/service/ApiTestEnvironmentService.java b/backend/src/main/java/io/metersphere/api/service/ApiTestEnvironmentService.java index 88950302f2..8566837a20 100644 --- a/backend/src/main/java/io/metersphere/api/service/ApiTestEnvironmentService.java +++ b/backend/src/main/java/io/metersphere/api/service/ApiTestEnvironmentService.java @@ -7,8 +7,11 @@ import io.metersphere.base.domain.ApiTestEnvironmentExample; import io.metersphere.base.domain.ApiTestEnvironmentWithBLOBs; import io.metersphere.base.mapper.ApiTestEnvironmentMapper; import io.metersphere.commons.exception.MSException; +import io.metersphere.commons.utils.CommonBeanFactory; import io.metersphere.controller.request.EnvironmentRequest; +import io.metersphere.dto.BaseSystemConfigDTO; import io.metersphere.i18n.Translator; +import io.metersphere.service.SystemParameterService; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -90,6 +93,18 @@ public class ApiTestEnvironmentService { * @return */ public synchronized ApiTestEnvironmentWithBLOBs getMockEnvironmentByProjectId(String projectId, String protocal, String baseUrl) { + //创建的时候检查当前站点 + SystemParameterService systemParameterService = CommonBeanFactory.getBean(SystemParameterService.class); + BaseSystemConfigDTO baseSystemConfigDTO = systemParameterService.getBaseInfo(); + if (baseSystemConfigDTO != null && StringUtils.isNotEmpty(baseSystemConfigDTO.getUrl())) { + baseUrl = baseSystemConfigDTO.getUrl(); + if (baseUrl.startsWith("http:")) { + protocal = "http"; + } else if (baseUrl.startsWith("https:")) { + protocal = "https"; + } + } + String apiName = MockConfigStaticData.MOCK_EVN_NAME; ApiTestEnvironmentWithBLOBs returnModel = null; ApiTestEnvironmentExample example = new ApiTestEnvironmentExample(); @@ -117,6 +132,21 @@ public class ApiTestEnvironmentService { JSONArray conditions = httpObj.getJSONArray("conditions"); if (conditions.isEmpty()) { needUpdate = true; + } else { + for (int i = 0; i < conditions.size(); i++) { + JSONObject obj = conditions.getJSONObject(i); + String socket = url; + if (socket.startsWith("http://")) { + socket = socket.substring(7); + } else if (socket.startsWith("https://")) { + socket = socket.substring(8); + } + if (!obj.containsKey("socket") || !StringUtils.equals(socket, String.valueOf(obj.get("socket")))) { + needUpdate = true; + } else if (!obj.containsKey("protocol") || !StringUtils.equals(protocal, String.valueOf(obj.get("protocol")))) { + needUpdate = true; + } + } } } } @@ -228,4 +258,20 @@ public class ApiTestEnvironmentService { return blobs; } + + public void checkMockEvnInfoByBaseUrl(String baseUrl) { + List allEvnList = this.selectByExampleWithBLOBs(null); + for (ApiTestEnvironmentWithBLOBs model : allEvnList) { + if (StringUtils.equals(model.getName(), MockConfigStaticData.MOCK_EVN_NAME)) { + String protocal = ""; + if (baseUrl.startsWith("http:")) { + protocal = "http"; + } else if (baseUrl.startsWith("https:")) { + protocal = "https"; + } + + model = this.checkMockEvnIsRightful(model, protocal, model.getProjectId(), model.getName(), baseUrl); + } + } + } } diff --git a/backend/src/main/java/io/metersphere/excel/handler/FunctionCaseTemplateWriteHandler.java b/backend/src/main/java/io/metersphere/excel/handler/FunctionCaseTemplateWriteHandler.java new file mode 100644 index 0000000000..1e666b1726 --- /dev/null +++ b/backend/src/main/java/io/metersphere/excel/handler/FunctionCaseTemplateWriteHandler.java @@ -0,0 +1,87 @@ +package io.metersphere.excel.handler; + +import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; +import com.alibaba.excel.write.metadata.holder.WriteTableHolder; +import com.alibaba.excel.write.style.row.AbstractRowHeightStyleStrategy; +import io.metersphere.i18n.Translator; +import org.apache.poi.ss.usermodel.Comment; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.usermodel.XSSFClientAnchor; +import org.apache.poi.xssf.usermodel.XSSFRichTextString; + +/** + * @author song.tianyang + * @Date 2021/5/7 2:17 下午 + * @Description + */ +public class FunctionCaseTemplateWriteHandler extends AbstractRowHeightStyleStrategy { + + @Override + public void beforeRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Integer rowIndex, Integer relativeRowIndex, Boolean isHead) { + + } + + @Override + public void afterRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) { + + } + + @Override + public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) { + super.afterRowDispose(writeSheetHolder, writeTableHolder, row, relativeRowIndex, isHead); + if (isHead) { + Sheet sheet = writeSheetHolder.getSheet(); + Drawing drawingPatriarch = sheet.createDrawingPatriarch(); + + // 在第一行 第3列创建一个批注 + Comment comment1 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 2, 0, (short) 3, 1)); + // 输入批注信息 + comment1.setString(new XSSFRichTextString(Translator.get("do_not_modify_header_order") + "," + Translator.get("num_needed_modify_testcase") + "," + Translator.get("num_needless_create_testcase"))); + + // 在第一行 第4列创建一个批注 + Comment comment2 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 3, 0, (short) 3, 1)); + // 输入批注信息 + comment2.setString(new XSSFRichTextString(Translator.get("module_created_automatically"))); + + // 在第一行 第5列创建一个批注 + Comment comment3 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 4, 0, (short) 3, 1)); + // 输入批注信息 + comment3.setString(new XSSFRichTextString(Translator.get("options") + "(functional、performance、api)")); + + + // 在第一行 第6列创建一个批注 + Comment comment4 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 5, 0, (short) 3, 1)); + // 输入批注信息 + comment4.setString(new XSSFRichTextString(Translator.get("please_input_workspace_member"))); + + // 在第一行 第7列创建一个批注 + Comment comment5 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 6, 0, (short) 3, 1)); + // 输入批注信息 + comment5.setString(new XSSFRichTextString(Translator.get("options") + "(P0、P1、P2、P3)")); + + // 在第一行 第8列创建一个批注 + Comment comment6 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 7, 0, (short) 3, 1)); + // 输入批注信息 + comment6.setString(new XSSFRichTextString(Translator.get("tag_tip_pattern"))); + + // 将批注添加到单元格对象中 + sheet.getRow(0).getCell(1).setCellComment(comment1); + sheet.getRow(0).getCell(1).setCellComment(comment2); + sheet.getRow(0).getCell(1).setCellComment(comment3); + sheet.getRow(0).getCell(1).setCellComment(comment4); + sheet.getRow(0).getCell(1).setCellComment(comment5); + sheet.getRow(0).getCell(1).setCellComment(comment6); + } + } + + @Override + protected void setHeadColumnHeight(Row row, int relativeRowIndex) { + } + + @Override + protected void setContentColumnHeight(Row row, int relativeRowIndex) { + + } +} diff --git a/backend/src/main/java/io/metersphere/excel/listener/TestCaseDataIgnoreErrorListener.java b/backend/src/main/java/io/metersphere/excel/listener/TestCaseDataIgnoreErrorListener.java new file mode 100644 index 0000000000..6ac55e5be6 --- /dev/null +++ b/backend/src/main/java/io/metersphere/excel/listener/TestCaseDataIgnoreErrorListener.java @@ -0,0 +1,338 @@ +package io.metersphere.excel.listener; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import io.metersphere.base.domain.TestCaseWithBLOBs; +import io.metersphere.commons.constants.TestCaseConstants; +import io.metersphere.commons.utils.BeanUtils; +import io.metersphere.commons.utils.CommonBeanFactory; +import io.metersphere.commons.utils.LogUtil; +import io.metersphere.excel.domain.ExcelErrData; +import io.metersphere.excel.domain.TestCaseExcelData; +import io.metersphere.excel.utils.ExcelValidateHelper; +import io.metersphere.i18n.Translator; +import io.metersphere.track.service.TestCaseService; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class TestCaseDataIgnoreErrorListener extends EasyExcelListener { + + private TestCaseService testCaseService; + + private String projectId; + + protected List updateList = new ArrayList<>(); //存储待更新用例的集合 + + protected boolean isUpdated = false; //判断是否更新过用例,将会传给前端 + + Set testCaseNames; + + Set userIds; + + public boolean isUpdated() { + return isUpdated; + } + + public TestCaseDataIgnoreErrorListener(Class clazz, String projectId, Set testCaseNames, Set userIds) { + this.clazz = clazz; + this.testCaseService = (TestCaseService) CommonBeanFactory.getBean("testCaseService"); + this.projectId = projectId; + this.testCaseNames = testCaseNames; + this.userIds = userIds; + } + + @Override + public String validate(TestCaseExcelData data, String errMsg) { + String nodePath = data.getNodePath(); + StringBuilder stringBuilder = new StringBuilder(errMsg); + //校验”所属模块" + if (nodePath != null) { + String[] nodes = nodePath.split("/"); + //校验模块深度 + if (nodes.length > TestCaseConstants.MAX_NODE_DEPTH + 1) { + stringBuilder.append(Translator.get("test_case_node_level_tip") + + TestCaseConstants.MAX_NODE_DEPTH + Translator.get("test_case_node_level") + "; "); + } + //模块名不能为空 + for (int i = 0; i < nodes.length; i++) { + if (i != 0 && StringUtils.equals(nodes[i].trim(), "")) { + stringBuilder.append(Translator.get("module_not_null") + "; "); + break; + } + } + //增加字数校验,每一层不能超过100字 + for (int i = 0; i < nodes.length; i++) { + String nodeStr = nodes[i]; + if (StringUtils.isNotEmpty(nodeStr)) { + if (nodeStr.trim().length() > 100) { + stringBuilder.append(Translator.get("module") + Translator.get("test_track.length_less_than") + "100:" + nodeStr); + break; + } + } + } + } + //校验维护人 + if (!userIds.contains(data.getMaintainer())) { + stringBuilder.append(Translator.get("user_not_exists") + ":" + data.getMaintainer() + "; "); + } + + /* + 校验Excel中是否有ID + 有的话校验ID是否已在当前项目中存在,存在则更新用例, + 不存在则继续校验看是否重复,不重复则新建用例。 + */ + if (null != data.getNum()) { //当前读取的数据有ID + if (null != testCaseService.checkIdExist(data.getNum(), projectId)) { //该ID在当前项目中存在 + //如果前面所经过的校验都没报错 + if (StringUtils.isEmpty(stringBuilder)) { + updateList.add(data); //将当前数据存入更新列表 + stringBuilder.append("update_testcase"); //该信息用于在invoke方法中判断是否该更新用例 + } + return stringBuilder.toString(); + } else { + /* + 该ID在当前数据库中不存在,应当继续校验用例是否重复, + 在下面的校验过程中,num的值会被用于判断是否重复,所以应当先设置为null + */ + data.setNum(null); + } + } + /* + 校验用例 + */ + if (testCaseNames.contains(data.getName())) { + TestCaseWithBLOBs testCase = new TestCaseWithBLOBs(); + BeanUtils.copyBean(testCase, data); + testCase.setProjectId(projectId); + String steps = getSteps(data); + testCase.setSteps(steps); + boolean dbExist = testCaseService.exist(testCase); + boolean excelExist = false; + if (dbExist) { + // db exist + stringBuilder.append(Translator.get("test_case_already_exists") + ":" + data.getName() + "; "); + } else { + // @Data 重写了 equals 和 hashCode 方法 + excelExist = excelDataList.contains(data); + } + if (excelExist) { + // excel exist + stringBuilder.append(Translator.get("test_case_already_exists_excel") + ":" + data.getName() + "; "); + } else { + excelDataList.add(data); + } + } else { + testCaseNames.add(data.getName()); + excelDataList.add(data); + } + return stringBuilder.toString(); + } + + @Override + public void saveData() { + if (!(list.size() == 0)) { + Collections.reverse(list); //因为saveImportData里面是先分配最大的ID,这个ID应该先发给list中最后的数据,所以要reverse + List result = list.stream() + .map(item -> this.convert2TestCase(item)) + .collect(Collectors.toList()); + testCaseService.saveImportData(result, projectId); + this.isUpdated = true; + } + + if (!(updateList.size() == 0)) { + List result2 = updateList.stream() + .map(item -> this.convert2TestCaseForUpdate(item)) + .collect(Collectors.toList()); + testCaseService.updateImportDataCarryId(result2, projectId); + this.isUpdated = true; + updateList.clear(); + } + + } + + + private TestCaseWithBLOBs convert2TestCase(TestCaseExcelData data) { + TestCaseWithBLOBs testCase = new TestCaseWithBLOBs(); + BeanUtils.copyBean(testCase, data); + testCase.setId(UUID.randomUUID().toString()); + testCase.setProjectId(this.projectId); + testCase.setCreateTime(System.currentTimeMillis()); + testCase.setUpdateTime(System.currentTimeMillis()); + testCase.setCustomNum(data.getCustomNum()); + String nodePath = data.getNodePath(); + + if (!nodePath.startsWith("/")) { + nodePath = "/" + nodePath; + } + if (nodePath.endsWith("/")) { + nodePath = nodePath.substring(0, nodePath.length() - 1); + } + testCase.setNodePath(nodePath); + + //将标签设置为前端可解析的格式 + String modifiedTags = modifyTagPattern(data); + testCase.setTags(modifiedTags); + + if (StringUtils.isNotBlank(data.getStepModel()) + && StringUtils.equals(data.getStepModel(), TestCaseConstants.StepModel.TEXT.name())) { + testCase.setStepDescription(data.getStepDesc()); + testCase.setExpectedResult(data.getStepResult()); + } else { + String steps = getSteps(data); + testCase.setSteps(steps); + } + return testCase; + } + + /** + * 将Excel中的数据对象转换为用于更新操作的用例数据对象, + * + * @param data + * @return + */ + private TestCaseWithBLOBs convert2TestCaseForUpdate(TestCaseExcelData data) { + TestCaseWithBLOBs testCase = new TestCaseWithBLOBs(); + BeanUtils.copyBean(testCase, data); + testCase.setProjectId(this.projectId); + testCase.setUpdateTime(System.currentTimeMillis()); + + //调整nodePath格式 + String nodePath = data.getNodePath(); + if (!nodePath.startsWith("/")) { + nodePath = "/" + nodePath; + } + if (nodePath.endsWith("/")) { + nodePath = nodePath.substring(0, nodePath.length() - 1); + } + testCase.setNodePath(nodePath); + + String steps = getSteps(data); + testCase.setSteps(steps); + + //将标签设置为前端可解析的格式 + String modifiedTags = modifyTagPattern(data); + testCase.setTags(modifiedTags); + + return testCase; + } + + /** + * 调整tags格式,便于前端进行解析。 + * 例如对于:标签1,标签2。将调整为:["标签1","标签2"]。 + */ + public String modifyTagPattern(TestCaseExcelData data) { + String tags = data.getTags(); + try { + if (StringUtils.isNotBlank(tags)) { + JSONArray.parse(tags); + return tags; + } + return "[]"; + } catch (Exception e) { + if (tags != null) { + Stream stringStream = Arrays.stream(tags.split("[,;,;]")); //当标签值以中英文的逗号和分号分隔时才能正确解析 + List tagList = stringStream.map(tag -> tag = "\"" + tag + "\"") + .collect(Collectors.toList()); + String modifiedTags = StringUtils.join(tagList, ","); + modifiedTags = "[" + modifiedTags + "]"; + return modifiedTags; + } else { + return "[]"; + } + } + } + + + public String getSteps(TestCaseExcelData data) { + JSONArray jsonArray = new JSONArray(); + + String[] stepDesc = new String[1]; + String[] stepRes = new String[1]; + + if (data.getStepDesc() != null) { + stepDesc = data.getStepDesc().split("\r\n|\n"); + } else { + stepDesc[0] = ""; + } + if (data.getStepResult() != null) { + stepRes = data.getStepResult().split("\r\n|\n"); + } else { + stepRes[0] = ""; + } + + String pattern = "(^\\d+)(\\.)?"; + int index = stepDesc.length > stepRes.length ? stepDesc.length : stepRes.length; + + for (int i = 0; i < index; i++) { + + // 保持插入顺序,判断用例是否有相同的steps + JSONObject step = new JSONObject(true); + step.put("num", i + 1); + + Pattern descPattern = Pattern.compile(pattern); + Pattern resPattern = Pattern.compile(pattern); + + if (i < stepDesc.length) { + Matcher descMatcher = descPattern.matcher(stepDesc[i]); + if (descMatcher.find()) { + step.put("desc", descMatcher.replaceAll("")); + } else { + step.put("desc", stepDesc[i]); + } + } + + if (i < stepRes.length) { + Matcher resMatcher = resPattern.matcher(stepRes[i]); + if (resMatcher.find()) { + step.put("result", resMatcher.replaceAll("")); + } else { + step.put("result", stepRes[i]); + } + } + + jsonArray.add(step); + } + return jsonArray.toJSONString(); + } + + @Override + public void invoke(TestCaseExcelData testCaseExcelData, AnalysisContext analysisContext) { + String errMsg; + Integer rowIndex = analysisContext.readRowHolder().getRowIndex(); + String updateMsg = "update_testcase"; + try { + //根据excel数据实体中的javax.validation + 正则表达式来校验excel数据 + errMsg = ExcelValidateHelper.validateEntity(testCaseExcelData); + //自定义校验规则 + errMsg = validate(testCaseExcelData, errMsg); + } catch (NoSuchFieldException e) { + errMsg = Translator.get("parse_data_error"); + LogUtil.error(e.getMessage(), e); + } + + if (!StringUtils.isEmpty(errMsg)) { + + //如果errMsg只有"update testcase",说明用例待更新 + if (!errMsg.equals(updateMsg)) { + ExcelErrData excelErrData = new ExcelErrData(testCaseExcelData, rowIndex, + Translator.get("number") + " " + rowIndex + " " + Translator.get("row") + Translator.get("error") + + ":" + errMsg); + + errList.add(excelErrData); + } + } else { + list.add(testCaseExcelData); + } + + if (list.size() > BATCH_COUNT) { + saveData(); + list.clear(); + } + } +} diff --git a/backend/src/main/java/io/metersphere/excel/utils/EasyExcelExporter.java b/backend/src/main/java/io/metersphere/excel/utils/EasyExcelExporter.java index 8183208f0c..43071895e4 100644 --- a/backend/src/main/java/io/metersphere/excel/utils/EasyExcelExporter.java +++ b/backend/src/main/java/io/metersphere/excel/utils/EasyExcelExporter.java @@ -1,6 +1,7 @@ package io.metersphere.excel.utils; import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.write.handler.WriteHandler; import com.alibaba.excel.write.metadata.style.WriteCellStyle; import com.alibaba.excel.write.style.HorizontalCellStyleStrategy; import io.metersphere.commons.utils.LogUtil; @@ -38,4 +39,19 @@ public class EasyExcelExporter { } } + public void exportByCustomWriteHandler(HttpServletResponse response, List data, String fileName, String sheetName, WriteHandler writeHandler) { + response.setContentType("application/vnd.ms-excel"); + response.setCharacterEncoding("utf-8"); + try { + response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8") + ".xlsx"); + EasyExcel.write(response.getOutputStream(), this.clazz).registerWriteHandler(writeHandler).sheet(sheetName).doWrite(data); + } catch (UnsupportedEncodingException e) { + LogUtil.error(e.getMessage(), e); + throw new ExcelException("Utf-8 encoding is not supported"); + } catch (IOException e) { + LogUtil.error(e.getMessage(), e); + throw new ExcelException("IO exception"); + } + } + } diff --git a/backend/src/main/java/io/metersphere/service/SystemParameterService.java b/backend/src/main/java/io/metersphere/service/SystemParameterService.java index e008f2b1a5..43ab958b1c 100644 --- a/backend/src/main/java/io/metersphere/service/SystemParameterService.java +++ b/backend/src/main/java/io/metersphere/service/SystemParameterService.java @@ -1,5 +1,6 @@ package io.metersphere.service; +import io.metersphere.api.service.ApiTestEnvironmentService; import io.metersphere.base.domain.*; import io.metersphere.base.mapper.SystemHeaderMapper; import io.metersphere.base.mapper.SystemParameterMapper; @@ -42,6 +43,8 @@ public class SystemParameterService { private ExtSystemParameterMapper extSystemParameterMapper; @Resource private SystemHeaderMapper systemHeaderMapper; + @Resource + private ApiTestEnvironmentService apiTestEnvironmentService; public String searchEmail() { return extSystemParameterMapper.email(); @@ -237,6 +240,7 @@ public class SystemParameterService { public void saveBaseInfo(List parameters) { SystemParameterExample example = new SystemParameterExample(); + parameters.forEach(param -> { // 去掉路径最后的 / param.setParamValue(StringUtils.removeEnd(param.getParamValue(), "/")); @@ -247,6 +251,10 @@ public class SystemParameterService { systemParameterMapper.insert(param); } example.clear(); + + if (StringUtils.equals(param.getParamKey(), "base.url")) { + apiTestEnvironmentService.checkMockEvnInfoByBaseUrl(param.getParamValue()); + } }); } diff --git a/backend/src/main/java/io/metersphere/track/controller/TestCaseController.java b/backend/src/main/java/io/metersphere/track/controller/TestCaseController.java index daa7f054db..67bc403c15 100644 --- a/backend/src/main/java/io/metersphere/track/controller/TestCaseController.java +++ b/backend/src/main/java/io/metersphere/track/controller/TestCaseController.java @@ -154,6 +154,13 @@ public class TestCaseController { return testCaseService.testCaseImport(file, projectId, userId); } + @PostMapping("/importIgnoreError/{projectId}/{userId}") + @RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR) + public ExcelResponse testCaseImportIgnoreError(MultipartFile file, @PathVariable String projectId, @PathVariable String userId) { + checkPermissionService.checkProjectOwner(projectId); + return testCaseService.testCaseImportIgnoreError(file, projectId, userId); + } + @GetMapping("/export/template") @RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR) public void testCaseTemplateExport(HttpServletResponse response) { diff --git a/backend/src/main/java/io/metersphere/track/service/TestCaseService.java b/backend/src/main/java/io/metersphere/track/service/TestCaseService.java index 33baea3b91..1e8b7d689d 100644 --- a/backend/src/main/java/io/metersphere/track/service/TestCaseService.java +++ b/backend/src/main/java/io/metersphere/track/service/TestCaseService.java @@ -19,6 +19,8 @@ import io.metersphere.excel.domain.ExcelErrData; import io.metersphere.excel.domain.ExcelResponse; import io.metersphere.excel.domain.TestCaseExcelData; import io.metersphere.excel.domain.TestCaseExcelDataFactory; +import io.metersphere.excel.handler.FunctionCaseTemplateWriteHandler; +import io.metersphere.excel.listener.TestCaseDataIgnoreErrorListener; import io.metersphere.excel.listener.TestCaseDataListener; import io.metersphere.excel.utils.EasyExcelExporter; import io.metersphere.i18n.Translator; @@ -97,7 +99,8 @@ public class TestCaseService { TestCaseFileMapper testCaseFileMapper; @Resource TestCaseTestMapper testCaseTestMapper; - private void setNode(TestCaseWithBLOBs testCase){ + + private void setNode(TestCaseWithBLOBs testCase) { if (StringUtils.isEmpty(testCase.getNodeId()) || "default-module".equals(testCase.getNodeId())) { TestCaseNodeExample example = new TestCaseNodeExample(); example.createCriteria().andProjectIdEqualTo(testCase.getProjectId()).andNameEqualTo("默认模块"); @@ -224,7 +227,7 @@ public class TestCaseService { String remark = tc.getRemark(); String prerequisite = tc.getPrerequisite(); if (StringUtils.equals(steps, caseSteps) && StringUtils.equals(remark, caseRemark) && StringUtils.equals(prerequisite, casePrerequisite)) { - //MSException.throwException(Translator.get("test_case_already_exists")); + //MSException.throwException(Translator.get("test_case_already_exists")); return tc; } } @@ -487,7 +490,7 @@ public class TestCaseService { testcase.setCustomNum(String.valueOf(number)); } testcase.setReviewStatus(TestCaseReviewStatus.Prepare.name()); - mapper.insert(testcase); + mapper.insert(testcase); }); } sqlSession.flushStatements(); @@ -516,6 +519,7 @@ public class TestCaseService { /** * 把Excel中带ID的数据更新到数据库 * feat(测试跟踪):通过Excel导入导出时有ID字段,可通过Excel导入来更新用例。 (#1727) + * * @param testCases * @param projectId */ @@ -555,8 +559,9 @@ public class TestCaseService { public void testCaseTemplateExport(HttpServletResponse response) { try { EasyExcelExporter easyExcelExporter = new EasyExcelExporter(new TestCaseExcelDataFactory().getExcelDataByLocal()); - easyExcelExporter.export(response, generateExportTemplate(), - Translator.get("test_case_import_template_name"), Translator.get("test_case_import_template_sheet")); + FunctionCaseTemplateWriteHandler handler = new FunctionCaseTemplateWriteHandler(); + easyExcelExporter.exportByCustomWriteHandler(response, generateExportTemplate(), + Translator.get("test_case_import_template_name"), Translator.get("test_case_import_template_sheet"), handler); } catch (Exception e) { MSException.throwException(e); } @@ -619,16 +624,15 @@ public class TestCaseService { } list.add(new TestCaseExcelData()); - TestCaseExcelData explain = new TestCaseExcelData(); - explain.setName(Translator.get("do_not_modify_header_order") + "," + Translator.get("num_needed_modify_testcase") + "," + Translator.get("num_needless_create_testcase")); - explain.setNodePath(Translator.get("module_created_automatically")); - explain.setType(Translator.get("options") + "(functional、performance、api)"); - explain.setTags(Translator.get("tag_tip_pattern")); -// explain.setMethod(Translator.get("options") + "(manual、auto)"); - explain.setPriority(Translator.get("options") + "(P0、P1、P2、P3)"); - explain.setMaintainer(Translator.get("please_input_workspace_member")); - - list.add(explain); +// TestCaseExcelData explain = new TestCaseExcelData(); +// explain.setName(Translator.get("do_not_modify_header_order") + "," + Translator.get("num_needed_modify_testcase") + "," + Translator.get("num_needless_create_testcase")); +// explain.setNodePath(Translator.get("module_created_automatically")); +// explain.setType(Translator.get("options") + "(functional、performance、api)"); +// explain.setTags(Translator.get("tag_tip_pattern")); +//// explain.setMethod(Translator.get("options") + "(manual、auto)"); +// explain.setPriority(Translator.get("options") + "(P0、P1、P2、P3)"); +// explain.setMaintainer(Translator.get("please_input_workspace_member")); +// list.add(explain); return list; } @@ -1020,9 +1024,95 @@ public class TestCaseService { /** * 更新项目下用例的CustomNum值 + * * @param projectId 项目ID */ public void updateTestCaseCustomNumByProjectId(String projectId) { extTestCaseMapper.updateTestCaseCustomNumByProjectId(projectId); } + + public ExcelResponse testCaseImportIgnoreError(MultipartFile multipartFile, String projectId, String userId) { + + ExcelResponse excelResponse = new ExcelResponse(); + boolean isUpdated = false; //判断是否更新了用例 + String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId(); + QueryTestCaseRequest queryTestCaseRequest = new QueryTestCaseRequest(); + queryTestCaseRequest.setProjectId(projectId); + List testCases = extTestCaseMapper.getTestCaseNames(queryTestCaseRequest); + Set testCaseNames = testCases.stream() + .map(TestCase::getName) + .collect(Collectors.toSet()); + List> errList = null; + if (multipartFile == null) { + MSException.throwException(Translator.get("upload_fail")); + } + if (multipartFile.getOriginalFilename().endsWith(".xmind")) { + try { + XmindCaseParser xmindParser = new XmindCaseParser(this, userId, projectId, testCaseNames); + errList = xmindParser.parse(multipartFile); + if (CollectionUtils.isEmpty(xmindParser.getNodePaths()) + && CollectionUtils.isEmpty(xmindParser.getTestCase()) + && CollectionUtils.isEmpty(xmindParser.getUpdateTestCase())) { + if (errList == null) { + errList = new ArrayList<>(); + } + ExcelErrData excelErrData = new ExcelErrData(null, 1, Translator.get("upload_fail") + ":" + Translator.get("upload_content_is_null")); + errList.add(excelErrData); + excelResponse.setErrList(errList); + } + List continueCaseList = xmindParser.getContinueValidatedCase(); + if (CollectionUtils.isNotEmpty(continueCaseList) || CollectionUtils.isNotEmpty(xmindParser.getUpdateTestCase())) { + if (CollectionUtils.isNotEmpty(xmindParser.getUpdateTestCase())) { + continueCaseList.removeAll(xmindParser.getUpdateTestCase()); + this.updateImportData(xmindParser.getUpdateTestCase(), projectId); + } + List nodePathList = xmindParser.getValidatedNodePath(); + if (CollectionUtils.isNotEmpty(nodePathList)) { + testCaseNodeService.createNodes(nodePathList, projectId); + } + if (CollectionUtils.isNotEmpty(continueCaseList)) { + Collections.reverse(continueCaseList); + this.saveImportData(continueCaseList, projectId); + } + } + xmindParser.clear(); + } catch (Exception e) { + LogUtil.error(e.getMessage(), e); + MSException.throwException(e.getMessage()); + } + } else { + UserRoleExample userRoleExample = new UserRoleExample(); + userRoleExample.createCriteria() + .andRoleIdIn(Arrays.asList(RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER)) + .andSourceIdEqualTo(currentWorkspaceId); + + Set userIds = userRoleMapper.selectByExample(userRoleExample).stream().map(UserRole::getUserId).collect(Collectors.toSet()); + + try { + //根据本地语言环境选择用哪种数据对象进行存放读取的数据 + Class clazz = new TestCaseExcelDataFactory().getExcelDataByLocal(); + + TestCaseDataIgnoreErrorListener easyExcelListener = new TestCaseDataIgnoreErrorListener(clazz, projectId, testCaseNames, userIds); + + //读取excel数据 + EasyExcelFactory.read(multipartFile.getInputStream(), clazz, easyExcelListener).sheet().doRead(); + + errList = easyExcelListener.getErrList(); + isUpdated = easyExcelListener.isUpdated(); + } catch (Exception e) { + LogUtil.error(e.getMessage(), e); + MSException.throwException(e.getMessage()); + } + } + //如果包含错误信息就导出错误信息 + if (!errList.isEmpty()) { + excelResponse.setSuccess(false); + excelResponse.setErrList(errList); + excelResponse.setIsUpdated(isUpdated); + } else { + excelResponse.setSuccess(true); + } + + return excelResponse; + } } diff --git a/backend/src/main/java/io/metersphere/xmind/XmindCaseParser.java b/backend/src/main/java/io/metersphere/xmind/XmindCaseParser.java index 5409fbd6a4..060824af6b 100644 --- a/backend/src/main/java/io/metersphere/xmind/XmindCaseParser.java +++ b/backend/src/main/java/io/metersphere/xmind/XmindCaseParser.java @@ -59,6 +59,10 @@ public class XmindCaseParser { */ private List nodePaths; + private List continueValidatedCase; + + private List errorPath; + public XmindCaseParser(TestCaseService testCaseService, String userId, String projectId, Set testCaseNames) { this.testCaseService = testCaseService; this.maintainer = userId; @@ -69,11 +73,14 @@ public class XmindCaseParser { compartDatas = new ArrayList<>(); process = new DetailUtil(); nodePaths = new ArrayList<>(); + continueValidatedCase = new ArrayList<>(); + errorPath = new ArrayList<>(); } private static final String TC_REGEX = "(?:tc:|tc:|tc)"; private static final String PC_REGEX = "(?:pc:|pc:|pc)"; private static final String RC_REGEX = "(?:rc:|rc:|rc)"; + private static final String ID_REGEX = "(?:id:|id:|id)"; private static final String TAG_REGEX = "(?:tag:|tag:|tag)"; public void clear() { @@ -126,6 +133,7 @@ public class XmindCaseParser { * 验证用例的合规性 */ private boolean validate(TestCaseWithBLOBs data) { + boolean validatePass = true; String nodePath = data.getNodePath(); if (!nodePath.startsWith("/")) { nodePath = "/" + nodePath; @@ -137,27 +145,41 @@ public class XmindCaseParser { if (data.getName().length() > 200) { + validatePass = false; process.add(Translator.get("test_case") + Translator.get("test_track.length_less_than") + "200", nodePath + data.getName()); } if (!StringUtils.isEmpty(nodePath)) { String[] nodes = nodePath.split("/"); if (nodes.length > TestCaseConstants.MAX_NODE_DEPTH + 1) { + validatePass = false; process.add(Translator.get("test_case_node_level_tip") + TestCaseConstants.MAX_NODE_DEPTH + Translator.get("test_case_node_level"), nodePath); + if (!errorPath.contains(nodePath)) { + errorPath.add(nodePath); + } } for (int i = 0; i < nodes.length; i++) { if (i != 0 && StringUtils.equals(nodes[i].trim(), "")) { + validatePass = false; process.add(Translator.get("test_case") + Translator.get("module_not_null"), nodePath + data.getName()); + if (!errorPath.contains(nodePath)) { + errorPath.add(nodePath); + } break; } else if (nodes[i].trim().length() > 100) { + validatePass = false; process.add(Translator.get("module") + Translator.get("test_track.length_less_than") + "100 ", nodes[i].trim()); + if (!errorPath.contains(nodePath)) { + errorPath.add(nodePath); + } break; } } } if (StringUtils.equals(data.getType(), TestCaseConstants.Type.Functional.getValue()) && StringUtils.equals(data.getMethod(), TestCaseConstants.Method.Auto.getValue())) { + validatePass = false; process.add(Translator.get("functional_method_tip"), nodePath + data.getName()); } @@ -176,9 +198,11 @@ public class XmindCaseParser { // 用例等级和用例性质处理 if (!priorityList.contains(data.getPriority())) { + validatePass = false; process.add(Translator.get("test_case_priority") + Translator.get("incorrect_format"), nodePath + data.getName()); } if (data.getType() == null) { + validatePass = false; process.add(Translator.get("test_case_type") + Translator.get("incorrect_format"), nodePath + data.getName()); } @@ -186,9 +210,13 @@ public class XmindCaseParser { TestCaseExcelData compartData = new TestCaseExcelData(); BeanUtils.copyBean(compartData, data); if (compartDatas.contains(compartData)) { + validatePass = false; process.add(Translator.get("test_case_already_exists_excel"), nodePath + "/" + compartData.getName()); } compartDatas.add(compartData); + if (validatePass) { + this.continueValidatedCase.add(data); + } return true; } @@ -305,6 +333,7 @@ public class XmindCaseParser { List steps = new LinkedList<>(); StringBuilder rc = new StringBuilder(); List tags = new LinkedList<>(); + StringBuilder customId = new StringBuilder(); if (attacheds != null && !attacheds.isEmpty()) { attacheds.forEach(item -> { if (isAvailable(item.getTitle(), PC_REGEX)) { @@ -314,12 +343,15 @@ public class XmindCaseParser { rc.append("\n"); } else if (isAvailable(item.getTitle(), TAG_REGEX)) { tags.add(replace(item.getTitle(), TAG_REGEX)); + } else if (isAvailable(item.getTitle(), ID_REGEX)) { + customId.append(replace(item.getTitle(), ID_REGEX)); } else { steps.add(item); } }); } testCase.setRemark(rc.toString()); + testCase.setCustomNum(customId.toString()); testCase.setTags(JSON.toJSONString(tags)); testCase.setSteps(this.getSteps(steps)); // 校验合规性 @@ -364,8 +396,23 @@ public class XmindCaseParser { //检查目录合规性 this.validate(); } catch (Exception ex) { + ex.printStackTrace(); return process.parse(ex.getMessage()); } return process.parse(); } + + public List getContinueValidatedCase() { + return this.continueValidatedCase; + } + + public List getValidatedNodePath() { + List returnPathList = new ArrayList<>(nodePaths); + if (CollectionUtils.isNotEmpty(returnPathList)) { + if (CollectionUtils.isNotEmpty(errorPath)) { + returnPathList.removeAll(errorPath); + } + } + return returnPathList; + } } diff --git a/frontend/src/business/components/track/case/components/TestCaseImport.vue b/frontend/src/business/components/track/case/components/TestCaseImport.vue index 1410afc46f..c9729be9c3 100644 --- a/frontend/src/business/components/track/case/components/TestCaseImport.vue +++ b/frontend/src/business/components/track/case/components/TestCaseImport.vue @@ -2,13 +2,13 @@ - + {{$t('test_track.case.import.download_template')}} + >{{ $t('test_track.case.import.download_template') }} @@ -39,10 +39,20 @@
  • - {{errFile.errMsg}} + {{ errFile.errMsg }}
+ + +
+ {{ $t('test_track.case.import.ignore_error') }} +
+ {{ $t('test_track.case.import.continue_upload') }} + + {{ $t('commons.cancel') }} +
+
@@ -98,10 +108,18 @@
  • - {{errFile.errMsg}} + {{ errFile.errMsg }}
+ +
+ {{ $t('test_track.case.import.ignore_error') }} +
+ {{ $t('test_track.case.import.continue_upload') }} + + {{ $t('commons.cancel') }} +
@@ -124,16 +142,34 @@ activeName: 'excelImport', dialogVisible: false, fileList: [], + lastXmindFile: null, + lastExcelFile: null, errList: [], xmindErrList: [], isLoading: false, - isUpdated: false + isUpdated: false, + clickTabsName: "", + showExcelImportContinueBtn: false, + showXmindImportContinueBtn: false, + uploadIgnoreError: false, + uploadXmindIgnoreError: false, } }, + created() { + this.showExcelImportContinueBtn = false; + this.showXmindImportContinueBtn = false; + }, + activated() { + this.showExcelImportContinueBtn = false; + this.showXmindImportContinueBtn = false; + }, methods: { handleExceed(files, fileList) { this.$warning(this.$t('test_track.case.import.upload_limit_count')); }, + clickTabs(tab, event) { + this.clickTabsName = tab.name; + }, uploadValidate(file) { let suffix = file.name.substring(file.name.lastIndexOf('.') + 1); if (suffix != 'xls' && suffix != 'xlsx') { @@ -179,11 +215,13 @@ removeGoBackListener(this.close); this.dialogVisible = false; this.fileList = []; + this.showExcelImportContinueBtn = false; + this.showXmindImportContinueBtn = false; this.errList = []; this.xmindErrList = []; //通过excel导入更新过数据的话就刷新页面 - if (this.isUpdated === true){ + if (this.isUpdated === true) { this.$emit("refreshAll"); this.isUpdated = false; } @@ -202,8 +240,10 @@ }); }, upload(file) { - this.isLoading = false; + this.lastExcelFile = file.file; this.fileList.push(file.file); + this.isLoading = false; + let user = JSON.parse(localStorage.getItem(TokenKey)); this.result = this.$fileUpload('/test/case/import/' + this.projectId + '/' + user.id, file.file, null, {}, response => { @@ -212,18 +252,57 @@ this.$success(this.$t('test_track.case.import.success')); this.dialogVisible = false; this.$emit("refreshAll"); + this.lastXmindFile = null; + this.lastExcelFile = null; + this.showExcelImportContinueBtn = false; + this.showXmindImportContinueBtn = false; } else { this.errList = res.errList; this.isUpdated = res.isUpdated; + this.showExcelImportContinueBtn = true; } this.fileList = []; }, erro => { this.fileList = []; + this.lastXmindFile = null; + this.lastExcelFile = null; + }); + }, + uploadContinue(isImportXmind) { + this.isLoading = false; + let user = JSON.parse(localStorage.getItem(TokenKey)); + let file = null; + if (isImportXmind) { + this.uploadXmindIgnoreError = true; + file = this.lastXmindFile; + } else { + this.uploadIgnoreError = true; + file = this.lastExcelFile; + } + this.result = this.$fileUpload('/test/case/importIgnoreError/' + this.projectId + '/' + user.id, file, null, {}, response => { + let res = response.data; + this.$success(this.$t('test_track.case.import.success')); + this.dialogVisible = false; + this.$emit("refreshAll"); + this.fileList = []; + this.lastXmindFile = null; + this.lastExcelFile = null; + this.showExcelImportContinueBtn = false; + this.showXmindImportContinueBtn = false; + this.uploadIgnoreError = false; + this.uploadXmindIgnoreError = false; + }, erro => { + this.fileList = []; + this.lastXmindFile = null; + this.lastExcelFile = null; + this.uploadIgnoreError = false; + this.uploadXmindIgnoreError = false; }); }, uploadXmind(file) { this.isLoading = false; this.fileList.push(file.file); + this.lastXmindFile = file.file; let user = JSON.parse(localStorage.getItem(TokenKey)); this.result = this.$fileUpload('/test/case/import/' + this.projectId + '/' + user.id, file.file, null, {}, response => { @@ -232,8 +311,13 @@ this.$success(this.$t('test_track.case.import.success')); this.dialogVisible = false; this.$emit("refreshAll"); + this.lastXmindFile = null; + this.lastExcelFile = null; + this.showExcelImportContinueBtn = false; + this.showXmindImportContinueBtn = false; } else { this.xmindErrList = res.errList; + this.showXmindImportContinueBtn = true; } this.fileList = []; }, erro => { diff --git a/frontend/src/i18n/en-US.js b/frontend/src/i18n/en-US.js index 9a91ba7469..4dfc1d4a47 100644 --- a/frontend/src/i18n/en-US.js +++ b/frontend/src/i18n/en-US.js @@ -1342,6 +1342,8 @@ export default { xmind_title: "Xmind", import_desc: "Import instructions", import_file: "upload files", + ignore_error: "Ignore errors ", + continue_upload: "Upload continue", }, export: { export: "Export cases" diff --git a/frontend/src/i18n/zh-CN.js b/frontend/src/i18n/zh-CN.js index 7201ebada4..d4b32d8737 100644 --- a/frontend/src/i18n/zh-CN.js +++ b/frontend/src/i18n/zh-CN.js @@ -1347,6 +1347,8 @@ export default { xmind_title: "思维导图", import_desc: "导入说明", import_file: "上传文件", + ignore_error: "忽略错误", + continue_upload: "继续上传", }, export: { export: "导出用例" diff --git a/frontend/src/i18n/zh-TW.js b/frontend/src/i18n/zh-TW.js index cf7be57fad..44d10d6488 100644 --- a/frontend/src/i18n/zh-TW.js +++ b/frontend/src/i18n/zh-TW.js @@ -1347,6 +1347,8 @@ export default { xmind_title: "思維導圖", import_desc: "導入說明", import_file: "上傳文件", + ignore_error: "忽略錯誤", + continue_upload: "繼續上傳", }, export: { export: "導出用例"