diff --git a/backend/pom.xml b/backend/pom.xml index ab143b2730..4b4e6db8c9 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -36,10 +36,6 @@ spring-boot-starter-tomcat org.springframework.boot - - hibernate-validator - org.hibernate.validator - @@ -133,6 +129,13 @@ 5.1 + + + com.alibaba + easyexcel + 2.1.7 + + diff --git a/backend/src/main/java/io/metersphere/base/domain/ZaleniumTest.java b/backend/src/main/java/io/metersphere/base/domain/ZaleniumTest.java deleted file mode 100644 index 3631aba0d8..0000000000 --- a/backend/src/main/java/io/metersphere/base/domain/ZaleniumTest.java +++ /dev/null @@ -1,194 +0,0 @@ -package io.metersphere.base.domain; - -public class ZaleniumTest { - - private String seleniumSessionId; - private String testName; - private String timestamp; - private String addedToDashboardTime; - private String browser; - private String browserVersion; - private String proxyName; - private String platform; - private String fileName; - private String fileExtension; - private String videoFolderPath; - private String logsFolderPath; - private String testNameNoExtension; - private String screenDimension; - private String timeZone; - private String build; - private String testFileNameTemplate; - private String browserDriverLogFileName; - private String retentionDate; - private String testStatus; - private boolean videoRecorded; - - public String getSeleniumSessionId() { - return seleniumSessionId; - } - - public void setSeleniumSessionId(String seleniumSessionId) { - this.seleniumSessionId = seleniumSessionId; - } - - public String getTestName() { - return testName; - } - - public void setTestName(String testName) { - this.testName = testName; - } - - public String getTimestamp() { - return timestamp; - } - - public void setTimestamp(String timestamp) { - this.timestamp = timestamp; - } - - public String getAddedToDashboardTime() { - return addedToDashboardTime; - } - - public void setAddedToDashboardTime(String addedToDashboardTime) { - this.addedToDashboardTime = addedToDashboardTime; - } - - public String getBrowser() { - return browser; - } - - public void setBrowser(String browser) { - this.browser = browser; - } - - public String getBrowserVersion() { - return browserVersion; - } - - public void setBrowserVersion(String browserVersion) { - this.browserVersion = browserVersion; - } - - public String getProxyName() { - return proxyName; - } - - public void setProxyName(String proxyName) { - this.proxyName = proxyName; - } - - public String getPlatform() { - return platform; - } - - public void setPlatform(String platform) { - this.platform = platform; - } - - public String getFileName() { - return fileName; - } - - public void setFileName(String fileName) { - this.fileName = fileName; - } - - public String getFileExtension() { - return fileExtension; - } - - public void setFileExtension(String fileExtension) { - this.fileExtension = fileExtension; - } - - public String getVideoFolderPath() { - return videoFolderPath; - } - - public void setVideoFolderPath(String videoFolderPath) { - this.videoFolderPath = videoFolderPath; - } - - public String getLogsFolderPath() { - return logsFolderPath; - } - - public void setLogsFolderPath(String logsFolderPath) { - this.logsFolderPath = logsFolderPath; - } - - public String getTestNameNoExtension() { - return testNameNoExtension; - } - - public void setTestNameNoExtension(String testNameNoExtension) { - this.testNameNoExtension = testNameNoExtension; - } - - public String getScreenDimension() { - return screenDimension; - } - - public void setScreenDimension(String screenDimension) { - this.screenDimension = screenDimension; - } - - public String getTimeZone() { - return timeZone; - } - - public void setTimeZone(String timeZone) { - this.timeZone = timeZone; - } - - public String getBuild() { - return build; - } - - public void setBuild(String build) { - this.build = build; - } - - public String getTestFileNameTemplate() { - return testFileNameTemplate; - } - - public void setTestFileNameTemplate(String testFileNameTemplate) { - this.testFileNameTemplate = testFileNameTemplate; - } - - public String getBrowserDriverLogFileName() { - return browserDriverLogFileName; - } - - public void setBrowserDriverLogFileName(String browserDriverLogFileName) { - this.browserDriverLogFileName = browserDriverLogFileName; - } - - public String getRetentionDate() { - return retentionDate; - } - - public void setRetentionDate(String retentionDate) { - this.retentionDate = retentionDate; - } - - public String getTestStatus() { - return testStatus; - } - - public void setTestStatus(String testStatus) { - this.testStatus = testStatus; - } - - public boolean isVideoRecorded() { - return videoRecorded; - } - - public void setVideoRecorded(boolean videoRecorded) { - this.videoRecorded = videoRecorded; - } -} diff --git a/backend/src/main/java/io/metersphere/commons/constants/TestCaseConstants.java b/backend/src/main/java/io/metersphere/commons/constants/TestCaseConstants.java new file mode 100644 index 0000000000..06410341dc --- /dev/null +++ b/backend/src/main/java/io/metersphere/commons/constants/TestCaseConstants.java @@ -0,0 +1,5 @@ +package io.metersphere.commons.constants; + +public class TestCaseConstants { + public static final int MAX_NODE_DEPTH = 5; +} diff --git a/backend/src/main/java/io/metersphere/controller/TestCaseController.java b/backend/src/main/java/io/metersphere/controller/TestCaseController.java index 69be699528..fee1de1d96 100644 --- a/backend/src/main/java/io/metersphere/controller/TestCaseController.java +++ b/backend/src/main/java/io/metersphere/controller/TestCaseController.java @@ -6,14 +6,11 @@ import io.metersphere.base.domain.*; import io.metersphere.commons.utils.PageUtils; import io.metersphere.commons.utils.Pager; import io.metersphere.controller.request.testcase.QueryTestCaseRequest; -import io.metersphere.controller.request.testplan.QueryTestPlanRequest; -import io.metersphere.dto.LoadTestDTO; -import io.metersphere.dto.TestCaseNodeDTO; -import io.metersphere.dto.TestPlanCaseDTO; -import io.metersphere.service.TestCaseNodeService; +import io.metersphere.excel.domain.ExcelResponse; import io.metersphere.service.TestCaseService; import io.metersphere.user.SessionUtils; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.util.List; @@ -74,5 +71,10 @@ public class TestCaseController { return testCaseService.deleteTestCase(testCaseId); } + @PostMapping("/import/{projectId}") + public ExcelResponse testCaseImport(MultipartFile file, @PathVariable String projectId){ + return testCaseService.testCaseImport(file, projectId); + } + } diff --git a/backend/src/main/java/io/metersphere/excel/domain/ExcelErrData.java b/backend/src/main/java/io/metersphere/excel/domain/ExcelErrData.java new file mode 100644 index 0000000000..66544759bb --- /dev/null +++ b/backend/src/main/java/io/metersphere/excel/domain/ExcelErrData.java @@ -0,0 +1,42 @@ +package io.metersphere.excel.domain; + +public class ExcelErrData { + + private T t; + + private Integer rowNum; + + private String errMsg; + + public ExcelErrData(){} + + public ExcelErrData(T t, Integer rowNum,String errMsg){ + this.t = t; + this.rowNum = rowNum; + this.errMsg = errMsg; + } + + public T getT() { + return t; + } + + public void setT(T t) { + this.t = t; + } + + public String getErrMsg() { + return errMsg; + } + + public void setErrMsg(String errMsg) { + this.errMsg = errMsg; + } + + public Integer getRowNum() { + return rowNum; + } + + public void setRowNum(Integer rowNum) { + this.rowNum = rowNum; + } +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/excel/domain/ExcelResponse.java b/backend/src/main/java/io/metersphere/excel/domain/ExcelResponse.java new file mode 100644 index 0000000000..aac623316f --- /dev/null +++ b/backend/src/main/java/io/metersphere/excel/domain/ExcelResponse.java @@ -0,0 +1,25 @@ +package io.metersphere.excel.domain; + +import java.util.List; + +public class ExcelResponse { + + private Boolean success; + private List> errList; + + public Boolean getSuccess() { + return success; + } + + public void setSuccess(Boolean success) { + this.success = success; + } + + public List> getErrList() { + return errList; + } + + public void setErrList(List> errList) { + this.errList = errList; + } +} diff --git a/backend/src/main/java/io/metersphere/excel/domain/TestCaseExcelData.java b/backend/src/main/java/io/metersphere/excel/domain/TestCaseExcelData.java new file mode 100644 index 0000000000..d7a311ebf6 --- /dev/null +++ b/backend/src/main/java/io/metersphere/excel/domain/TestCaseExcelData.java @@ -0,0 +1,137 @@ +package io.metersphere.excel.domain; + +import com.alibaba.excel.annotation.ExcelProperty; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +public class TestCaseExcelData { + + @NotBlank + @Length(max=1000) + @ExcelProperty("所属模块") + @Pattern(regexp = "^(?!.*//).*$", message = "格式不正确") + private String nodePath; + + @NotBlank + @Length(max=50) + @ExcelProperty("用例名称") + private String name; + + @NotBlank + @ExcelProperty("用例类型") + @Pattern(regexp = "(^functional$)|(^performance$)|(^api$)", message = "必须为functional、performance、api") + private String type; + + @NotBlank + @ExcelProperty("维护人") + private String maintainer; + + @NotBlank + @ExcelProperty("优先级") + @Pattern(regexp = "(^P0$)|(^P1$)|(^P2$)|(^P3$)", message = "必须为P0、P1、P2、P3") + private String priority; + + @NotBlank + @ExcelProperty("测试方式") + @Pattern(regexp = "(^manual$)|(^auto$)", message = "必须为manual、auto") + private String method; + + @ExcelProperty("前置条件") + @Length(min=0, max=1000) + private String prerequisite; + + @ExcelProperty("备注") + @Length(max=1000) + private String remark; + + @ExcelProperty("步骤描述") + @Length(max=1000) + private String stepDesc; + + @ExcelProperty("预期结果") + @Length(max=1000) + private String stepResult; + + public String getNodePath() { + return nodePath; + } + + public void setNodePath(String nodePath) { + this.nodePath = nodePath; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getMaintainer() { + return maintainer; + } + + public void setMaintainer(String maintainer) { + this.maintainer = maintainer; + } + + public String getPriority() { + return priority; + } + + public void setPriority(String priority) { + this.priority = priority; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getPrerequisite() { + return prerequisite; + } + + public void setPrerequisite(String prerequisite) { + this.prerequisite = prerequisite; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public String getStepDesc() { + return stepDesc; + } + + public void setStepDesc(String stepDesc) { + this.stepDesc = stepDesc; + } + + public String getStepResult() { + return stepResult; + } + + public void setStepResult(String stepResult) { + this.stepResult = stepResult; + } + +} diff --git a/backend/src/main/java/io/metersphere/excel/listener/EasyExcelListener.java b/backend/src/main/java/io/metersphere/excel/listener/EasyExcelListener.java new file mode 100644 index 0000000000..bbb45f7d8c --- /dev/null +++ b/backend/src/main/java/io/metersphere/excel/listener/EasyExcelListener.java @@ -0,0 +1,140 @@ +package io.metersphere.excel.listener; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.alibaba.excel.exception.ExcelAnalysisException; +import com.alibaba.excel.util.StringUtils; +import io.metersphere.commons.utils.LogUtil; +import io.metersphere.excel.util.ExcelValidateHelper; +import io.metersphere.excel.domain.ExcelErrData; + +import java.lang.reflect.Field; +import java.util.*; + + +public abstract class EasyExcelListener extends AnalysisEventListener { + + protected List> errList = new ArrayList<>(); + + protected List list = new ArrayList<>(); + + /** + * 每隔2000条存储数据库,然后清理list ,方便内存回收 + */ + protected static final int BATCH_COUNT = 2000; + + protected Class clazz; + + + public EasyExcelListener(Class clazz){ + this.clazz = clazz; + } + + /** + * 这个每一条数据解析都会来调用 + * + * @param t + * @param analysisContext + */ + @Override + public void invoke(T t, AnalysisContext analysisContext) { + String errMsg; + Integer rowIndex = analysisContext.readRowHolder().getRowIndex(); + try { + //根据excel数据实体中的javax.validation + 正则表达式来校验excel数据 + errMsg = ExcelValidateHelper.validateEntity(t); + //自定义校验规则 + errMsg = validate(t, errMsg); + } catch (NoSuchFieldException e) { + errMsg = "解析数据出错"; + LogUtil.error(e.getMessage(), e); + } + + if (!StringUtils.isEmpty(errMsg)) { + ExcelErrData excelErrData = new ExcelErrData(t, rowIndex, "第" + rowIndex + "行出错:" + errMsg); + errList.add(excelErrData); + } else { + list.add(t); + } + + if (list.size() > BATCH_COUNT) { + saveData(); + list.clear(); + } + } + + /** + * 可重写该方法 + * 自定义校验规则 + * @param data + * @param errMsg + * @return + */ + public String validate(T data, String errMsg) { + return errMsg; + } + + /** + * 自定义数据保存操作 + */ + public abstract void saveData(); + + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + saveData(); + list.clear(); + } + + + /** + * 校验excel头部 + * @param headMap 传入excel的头部(第一行数据)数据的index,name + * @param context + */ + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + super.invokeHeadMap(headMap, context); + if (clazz != null){ + try { + Set fieldNameSet = getFieldNameSet(clazz); + Collection values = headMap.values(); + for (String key : fieldNameSet) { + if (!values.contains(key)){ + throw new ExcelAnalysisException("缺少头部信息:" + key); + } + } + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + } + } + + /** + * @description: 获取注解里ExcelProperty的value + */ + public Set getFieldNameSet(Class clazz) throws NoSuchFieldException { + Set result = new HashSet<>(); + Field field; + Field[] fields = clazz.getDeclaredFields(); + for (int i = 0; i < fields.length ; i++) { + field = clazz.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); + } + result.add(value.toString()); + } + } + return result; + } + + + public List> getErrList() { + return errList; + } + +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/excel/listener/TestCaseDataListener.java b/backend/src/main/java/io/metersphere/excel/listener/TestCaseDataListener.java new file mode 100644 index 0000000000..268a444328 --- /dev/null +++ b/backend/src/main/java/io/metersphere/excel/listener/TestCaseDataListener.java @@ -0,0 +1,138 @@ +package io.metersphere.excel.listener; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import io.metersphere.excel.domain.TestCaseExcelData; +import io.metersphere.base.domain.TestCaseWithBLOBs; +import io.metersphere.commons.constants.TestCaseConstants; +import io.metersphere.commons.utils.BeanUtils; +import io.metersphere.service.TestCaseService; + +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class TestCaseDataListener extends EasyExcelListener { + + private TestCaseService testCaseService; + + private String projectId; + + Set testCaseNames; + + Set userNames; + + public TestCaseDataListener(TestCaseService testCaseService, String projectId, + Set testCaseNames, Set userNames, Class clazz) { + super(clazz); + this.testCaseService = testCaseService; + this.projectId = projectId; + this.testCaseNames = testCaseNames; + this.userNames = userNames; + } + + @Override + public String validate(TestCaseExcelData data, String errMsg) { + String nodePath = data.getNodePath(); + StringBuilder stringBuilder = new StringBuilder(errMsg); + if ( nodePath.split("/").length > TestCaseConstants.MAX_NODE_DEPTH + 1) { + stringBuilder.append("节点最多为" + TestCaseConstants.MAX_NODE_DEPTH + "层;"); + } + if (!userNames.contains(data.getMaintainer())) { + stringBuilder.append("该工作空间下无该用户:" + data.getMaintainer() + ";"); + } + if (testCaseNames.contains(data.getName())) { + stringBuilder.append("该项目下已存在该测试用例:" + data.getName() + ";"); + } + return stringBuilder.toString(); + } + + @Override + public void saveData() { + + //无错误数据才插入数据 + if (!errList.isEmpty()) { + return; + } + + List result = list.stream() + .map(item -> this.convert2TestCase(item)) + .collect(Collectors.toList()); + + testCaseService.saveImportData(result, projectId); + + } + + + 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()); + String nodePath = data.getNodePath(); + + if (!nodePath.startsWith("/")) { + nodePath = "/" + nodePath; + } + if (nodePath.endsWith("/")) { + nodePath = nodePath.substring(0, nodePath.length() - 1); + } + + testCase.setNodePath(nodePath); + + + JSONArray jsonArray = new JSONArray(); + + String[] stepDesc = new String[0]; + String[] stepRes = new String[0]; + + if (data.getStepDesc() != null) { + stepDesc = data.getStepDesc().split("\n"); + } + if (data.getStepResult() != null) { + stepRes = data.getStepResult().split("\n"); + } + + String pattern = "(^\\d+)(\\.)?"; + int index = stepDesc.length > stepRes.length ? stepDesc.length : stepRes.length; + + for (int i = 0; i < index; i++){ + + JSONObject step = new JSONObject(); + 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); + } + + testCase.setSteps(jsonArray.toJSONString()); + + return testCase; + } + +} diff --git a/backend/src/main/java/io/metersphere/excel/util/ExcelValidateHelper.java b/backend/src/main/java/io/metersphere/excel/util/ExcelValidateHelper.java new file mode 100644 index 0000000000..8156bbcc6d --- /dev/null +++ b/backend/src/main/java/io/metersphere/excel/util/ExcelValidateHelper.java @@ -0,0 +1,32 @@ +package io.metersphere.excel.util; + +import com.alibaba.excel.annotation.ExcelProperty; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.groups.Default; +import java.lang.reflect.Field; +import java.util.Set; + + +public class ExcelValidateHelper { + + private ExcelValidateHelper(){} + + private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + public static String validateEntity(T obj) throws NoSuchFieldException { + StringBuilder result = new StringBuilder(); + Set> set = validator.validate(obj, Default.class); + if (set != null && !set.isEmpty()) { + for (ConstraintViolation cv : set) { + Field declaredField = obj.getClass().getDeclaredField(cv.getPropertyPath().toString()); + ExcelProperty annotation = declaredField.getAnnotation(ExcelProperty.class); + //拼接错误信息,包含当前出错数据的标题名字+错误信息 + result.append(annotation.value()[0]+cv.getMessage()).append(";"); + } + } + return result.toString(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/exception/ExcelImportException.java b/backend/src/main/java/io/metersphere/exception/ExcelImportException.java new file mode 100644 index 0000000000..8ac826ab74 --- /dev/null +++ b/backend/src/main/java/io/metersphere/exception/ExcelImportException.java @@ -0,0 +1,18 @@ +package io.metersphere.exception; + +/** + * @author jianxing.chen + */ +public class ExcelImportException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ExcelImportException(String message, Exception e){ + super(message, e); + } + + public ExcelImportException(String message){ + super(message); + } + +} diff --git a/backend/src/main/java/io/metersphere/service/TestCaseNodeService.java b/backend/src/main/java/io/metersphere/service/TestCaseNodeService.java index fcf74b3250..63e457f9de 100644 --- a/backend/src/main/java/io/metersphere/service/TestCaseNodeService.java +++ b/backend/src/main/java/io/metersphere/service/TestCaseNodeService.java @@ -6,8 +6,11 @@ import io.metersphere.base.mapper.TestCaseMapper; import io.metersphere.base.mapper.TestCaseNodeMapper; import io.metersphere.base.mapper.TestPlanMapper; import io.metersphere.base.mapper.TestPlanTestCaseMapper; +import io.metersphere.commons.constants.TestCaseConstants; import io.metersphere.commons.utils.BeanUtils; import io.metersphere.dto.TestCaseNodeDTO; +import io.metersphere.exception.ExcelImportException; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,8 +33,8 @@ public class TestCaseNodeService { public int addNode(TestCaseNode node) { - if(node.getLevel() > 5){ - throw new RuntimeException("模块树最大深度为5层!"); + if(node.getLevel() > TestCaseConstants.MAX_NODE_DEPTH){ + throw new RuntimeException("模块树最大深度为" + TestCaseConstants.MAX_NODE_DEPTH + "层!"); } node.setCreateTime(System.currentTimeMillis()); node.setUpdateTime(System.currentTimeMillis()); @@ -196,4 +199,124 @@ public class TestCaseNodeService { TestPlan testPlan = testPlanMapper.selectByPrimaryKey(planId); return getNodeTreeByProjectId(testPlan.getProjectId()); } + + public Map createNodeByTestCases(List testCases, String projectId) { + + List nodeTrees = getNodeTreeByProjectId(projectId); + + Map pathMap = new HashMap<>(); + + List nodePaths = testCases.stream() + .map(TestCase::getNodePath) + .collect(Collectors.toList()); + + nodePaths.forEach(path -> { + + if (path == null) { + throw new ExcelImportException("所属模块不能为空!"); + } + List nodeNameList = new ArrayList<>(Arrays.asList(path.split("/"))); + Iterator pathIterator = nodeNameList.iterator(); + + Boolean hasNode = false; + String rootNodeName = null; + + if (nodeNameList.size() <= 1) { + throw new ExcelImportException("创建模块失败:" + path); + } else { + pathIterator.next(); + pathIterator.remove(); + + rootNodeName = pathIterator.next().trim(); + for (TestCaseNodeDTO nodeTree : nodeTrees) { + if (StringUtils.equals(rootNodeName, nodeTree.getName())) { + hasNode = true; + createNodeByPathIterator(pathIterator, "/" + rootNodeName, nodeTree, + pathMap, projectId, 2); + }; + } + } + + if (!hasNode) { + createNodeByPath(pathIterator, rootNodeName, null, projectId, 1, "", pathMap); + } + }); + + return pathMap; + + } + + /** + * 根据目标节点路径,创建相关节点 + * @param pathIterator 遍历子路径 + * @param path 当前路径 + * @param treeNode 当前节点 + * @param pathMap 记录节点路径对应的nodeId + */ + private void createNodeByPathIterator(Iterator pathIterator, String path, TestCaseNodeDTO treeNode, + Map pathMap, String projectId, Integer level) { + + List children = treeNode.getChildren(); + + if (children == null || children.isEmpty() || !pathIterator.hasNext()) { + pathMap.put(path , treeNode.getId()); + if (pathIterator.hasNext()) { + createNodeByPath(pathIterator, pathIterator.next().trim(), treeNode, projectId, level, path, pathMap); + } + return; + } + + String nodeName = pathIterator.next().trim(); + + Boolean hasNode = false; + + for (TestCaseNodeDTO child : children) { + if (StringUtils.equals(nodeName, child.getName())) { + hasNode = true; + createNodeByPathIterator(pathIterator, path + "/" + child.getName(), + child, pathMap, projectId, level + 1); + }; + } + + //若子节点中不包含该目标节点,则在该节点下创建 + if (!hasNode) { + createNodeByPath(pathIterator, nodeName, treeNode, projectId, level, path, pathMap); + } + + } + + /** + * + * @param pathIterator 迭代器,遍历子节点 + * @param nodeName 当前节点 + * @param pNode 父节点 + */ + private void createNodeByPath(Iterator pathIterator, String nodeName, + TestCaseNodeDTO pNode, String projectId, Integer level, + String rootPath, Map pathMap) { + + StringBuilder path = new StringBuilder(rootPath); + + Integer pid = insertTestCaseNode(nodeName, pNode == null ? null : pNode.getId(), projectId, level); + path.append("/" + nodeName); + pathMap.put(path.toString(), pid); + while (pathIterator.hasNext()) { + String nextNodeName = pathIterator.next(); + path.append("/" + nextNodeName); + pid = insertTestCaseNode(nextNodeName, pid, projectId, ++level); + pathMap.put(path.toString(), pid); + } + } + + private Integer insertTestCaseNode(String nodName, Integer pId, String projectId, Integer level) { + TestCaseNode testCaseNode = new TestCaseNode(); + testCaseNode.setName(nodName.trim()); + testCaseNode.setpId(pId); + testCaseNode.setProjectId(projectId); + testCaseNode.setCreateTime(System.currentTimeMillis()); + testCaseNode.setUpdateTime(System.currentTimeMillis()); + testCaseNode.setLevel(level); + testCaseNodeMapper.insert(testCaseNode); + return testCaseNode.getId(); + } } diff --git a/backend/src/main/java/io/metersphere/service/TestCaseService.java b/backend/src/main/java/io/metersphere/service/TestCaseService.java index 6dbf5bec4f..371eaf5197 100644 --- a/backend/src/main/java/io/metersphere/service/TestCaseService.java +++ b/backend/src/main/java/io/metersphere/service/TestCaseService.java @@ -1,26 +1,34 @@ package io.metersphere.service; +import com.alibaba.excel.EasyExcelFactory; import com.github.pagehelper.PageHelper; import io.metersphere.base.domain.*; -import io.metersphere.base.mapper.ProjectMapper; -import io.metersphere.base.mapper.TestCaseMapper; -import io.metersphere.base.mapper.TestPlanMapper; -import io.metersphere.base.mapper.TestPlanTestCaseMapper; +import io.metersphere.base.mapper.*; import io.metersphere.base.mapper.ext.ExtTestCaseMapper; +import io.metersphere.commons.utils.LogUtil; import io.metersphere.controller.request.testcase.QueryTestCaseRequest; -import io.metersphere.dto.TestPlanCaseDTO; +import io.metersphere.excel.domain.ExcelErrData; +import io.metersphere.excel.domain.ExcelResponse; +import io.metersphere.excel.domain.TestCaseExcelData; +import io.metersphere.excel.listener.EasyExcelListener; +import io.metersphere.excel.listener.TestCaseDataListener; +import io.metersphere.user.SessionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; -import java.util.ArrayList; +import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; -import java.util.stream.Stream; @Service @Transactional(rollbackFor = Exception.class) @@ -41,6 +49,15 @@ public class TestCaseService { @Resource ProjectMapper projectMapper; + @Resource + SqlSessionFactory sqlSessionFactory; + + @Resource + TestCaseNodeService testCaseNodeService; + + @Resource + UserMapper userMapper; + public void addTestCase(TestCaseWithBLOBs testCase) { testCase.setId(UUID.randomUUID().toString()); testCase.setCreateTime(System.currentTimeMillis()); @@ -144,4 +161,59 @@ public class TestCaseService { } return projectMapper.selectByPrimaryKey(testCaseWithBLOBs.getProjectId()); } + + public ExcelResponse testCaseImport(MultipartFile file, String projectId) { + + try { + + ExcelResponse excelResponse = new ExcelResponse(); + + 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()); + + UserExample userExample = new UserExample(); + userExample.createCriteria().andLastWorkspaceIdEqualTo(currentWorkspaceId); + List users = userMapper.selectByExample(userExample); + Set userNames = users.stream().map(User::getName).collect(Collectors.toSet()); + + EasyExcelListener easyExcelListener = new TestCaseDataListener(this, projectId, + testCaseNames, userNames, TestCaseExcelData.class); + EasyExcelFactory.read(file.getInputStream(), TestCaseExcelData.class, easyExcelListener).sheet().doRead(); + + List> errList = easyExcelListener.getErrList(); + //如果包含错误信息就导出错误信息 + if (!errList.isEmpty()) { + excelResponse.setSuccess(false); + excelResponse.setErrList(errList); + } else { + excelResponse.setSuccess(true); + } + return excelResponse; + + } catch (IOException e) { + LogUtil.error(e.getMessage(), e); + e.printStackTrace(); + } + + return null; + } + + public void saveImportData(List testCases, String projectId) { + + Map nodePathMap = testCaseNodeService.createNodeByTestCases(testCases, projectId); + SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); + TestCaseMapper mapper = sqlSession.getMapper(TestCaseMapper.class); + if (!testCases.isEmpty()) { + testCases.forEach(testcase -> { + testcase.setNodeId(nodePathMap.get(testcase.getNodePath())); + mapper.insert(testcase); + }); + } + sqlSession.flushStatements(); + } } diff --git a/frontend/src/business/components/track/case/TestCase.vue b/frontend/src/business/components/track/case/TestCase.vue index 7dce809631..921e3c1d17 100644 --- a/frontend/src/business/components/track/case/TestCase.vue +++ b/frontend/src/business/components/track/case/TestCase.vue @@ -26,6 +26,7 @@ :current-project="currentProject" @openTestCaseEditDialog="openTestCaseEditDialog" @testCaseEdit="openTestCaseEditDialog" + @refresh="refresh" ref="testCaseList"> diff --git a/frontend/src/business/components/track/case/components/TestCaseExport.vue b/frontend/src/business/components/track/case/components/TestCaseExport.vue new file mode 100644 index 0000000000..596d20b78e --- /dev/null +++ b/frontend/src/business/components/track/case/components/TestCaseExport.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/frontend/src/business/components/track/case/components/TestCaseImport.vue b/frontend/src/business/components/track/case/components/TestCaseImport.vue new file mode 100644 index 0000000000..07e70c0f77 --- /dev/null +++ b/frontend/src/business/components/track/case/components/TestCaseImport.vue @@ -0,0 +1,134 @@ + + + + + + + diff --git a/frontend/src/business/components/track/case/components/TestCaseList.vue b/frontend/src/business/components/track/case/components/TestCaseList.vue index 48f9ab4e25..d87ed3f552 100644 --- a/frontend/src/business/components/track/case/components/TestCaseList.vue +++ b/frontend/src/business/components/track/case/components/TestCaseList.vue @@ -4,12 +4,21 @@