diff --git a/backend/framework/sdk/src/main/resources/i18n/case_en_US.properties b/backend/framework/sdk/src/main/resources/i18n/case_en_US.properties index 72226a18ca..29cd270550 100644 --- a/backend/framework/sdk/src/main/resources/i18n/case_en_US.properties +++ b/backend/framework/sdk/src/main/resources/i18n/case_en_US.properties @@ -165,4 +165,5 @@ case_review.not.exist=Case review does not exist case_review_content.not.exist = Review comments cannot be empty case_review_history.system=System trigger case_review.viewFlag.not_blank=View flag cannot be empty -functional_case_relationship_edge.type.not_blank=Relationship type cannot be empty \ No newline at end of file +functional_case_relationship_edge.type.not_blank=Relationship type cannot be empty +cycle_relationship=There is a circular dependency after association, please check the dependency relationship \ No newline at end of file diff --git a/backend/framework/sdk/src/main/resources/i18n/case_zh_CN.properties b/backend/framework/sdk/src/main/resources/i18n/case_zh_CN.properties index ceb663a71c..012a6d36d0 100644 --- a/backend/framework/sdk/src/main/resources/i18n/case_zh_CN.properties +++ b/backend/framework/sdk/src/main/resources/i18n/case_zh_CN.properties @@ -165,3 +165,4 @@ case_review_content.not.exist = 评审意见不能为空 case_review_history.system=系统触发 case_review.viewFlag.not_blank=查看标识不能为空 functional_case_relationship_edge.type.not_blank=类型不能为空 +cycle_relationship=关联后存在循环依赖,请检查依赖关系 diff --git a/backend/framework/sdk/src/main/resources/i18n/case_zh_TW.properties b/backend/framework/sdk/src/main/resources/i18n/case_zh_TW.properties index ba6aa5f090..7e4fac31fd 100644 --- a/backend/framework/sdk/src/main/resources/i18n/case_zh_TW.properties +++ b/backend/framework/sdk/src/main/resources/i18n/case_zh_TW.properties @@ -165,4 +165,5 @@ case_review.not.exist=用例評審不存在 case_review_content.not.exist = 評審意見不能為空 case_review_history.system=系統觸發 case_review.viewFlag.not_blank=查看標誌不能為空 -functional_case_relationship_edge.type.not_blank=類型不能為空 \ No newline at end of file +functional_case_relationship_edge.type.not_blank=類型不能為空 +cycle_relationship=關聯后存在循環依賴,請檢查依賴關係 \ No newline at end of file diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseRelationshipController.java b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseRelationshipController.java index 3bb3f048c2..1d7004c8b2 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseRelationshipController.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseRelationshipController.java @@ -4,6 +4,7 @@ import com.alibaba.excel.util.StringUtils; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; import io.metersphere.functional.dto.FunctionalCasePageDTO; +import io.metersphere.functional.request.RelationshipAddRequest; import io.metersphere.functional.request.RelationshipPageRequest; import io.metersphere.functional.service.FunctionalCaseRelationshipEdgeService; import io.metersphere.functional.service.FunctionalCaseService; @@ -11,9 +12,11 @@ import io.metersphere.sdk.constants.PermissionConstants; import io.metersphere.system.security.CheckOwner; import io.metersphere.system.utils.PageUtils; import io.metersphere.system.utils.Pager; +import io.metersphere.system.utils.SessionUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import org.apache.commons.collections.CollectionUtils; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; @@ -49,5 +52,16 @@ public class FunctionalCaseRelationshipController { return PageUtils.setPageInfo(page, functionalCaseService.getFunctionalCasePage(request, false)); } - + @PostMapping("/add") + @Operation(summary = "用例管理-功能用例-用例详情-前后置关系-添加前后置关系") + @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_UPDATE) + @CheckOwner(resourceId = "#request.getCaseId()", resourceType = "project") + public void add(@Validated @RequestBody RelationshipAddRequest request) { + List excludeIds = functionalCaseRelationshipEdgeService.getExcludeIds(request.getId()); + request.setExcludeIds(excludeIds); + List ids = functionalCaseService.doSelectIds(request, request.getProjectId()); + if (CollectionUtils.isNotEmpty(ids)) { + functionalCaseRelationshipEdgeService.add(request, ids, SessionUtils.getUserId()); + } + } } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/mapper/ExtFunctionalCaseRelationshipEdgeMapper.java b/backend/services/case-management/src/main/java/io/metersphere/functional/mapper/ExtFunctionalCaseRelationshipEdgeMapper.java new file mode 100644 index 0000000000..fd200ee920 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/mapper/ExtFunctionalCaseRelationshipEdgeMapper.java @@ -0,0 +1,11 @@ +package io.metersphere.functional.mapper; + +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface ExtFunctionalCaseRelationshipEdgeMapper { + + + List getGraphIds(@Param("ids") List ids); +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/mapper/ExtFunctionalCaseRelationshipEdgeMapper.xml b/backend/services/case-management/src/main/java/io/metersphere/functional/mapper/ExtFunctionalCaseRelationshipEdgeMapper.xml new file mode 100644 index 0000000000..56fae9bc1e --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/mapper/ExtFunctionalCaseRelationshipEdgeMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/request/RelationshipAddRequest.java b/backend/services/case-management/src/main/java/io/metersphere/functional/request/RelationshipAddRequest.java index db6b10f40b..48fb15a95e 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/request/RelationshipAddRequest.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/request/RelationshipAddRequest.java @@ -1,7 +1,7 @@ package io.metersphere.functional.request; -import io.metersphere.system.dto.table.TableBatchProcessDTO; +import io.metersphere.functional.dto.BaseFunctionalCaseBatchDTO; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; @@ -10,7 +10,7 @@ import lombok.Data; * @author wx */ @Data -public class RelationshipAddRequest extends TableBatchProcessDTO { +public class RelationshipAddRequest extends BaseFunctionalCaseBatchDTO { @Schema(description = "用例id", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "{functional_case.id.not_blank}") diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseRelationshipEdgeService.java b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseRelationshipEdgeService.java index 9df60db530..2e4cc373c7 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseRelationshipEdgeService.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseRelationshipEdgeService.java @@ -9,11 +9,24 @@ package io.metersphere.functional.service; import io.metersphere.functional.domain.FunctionalCaseRelationshipEdge; import io.metersphere.functional.domain.FunctionalCaseRelationshipEdgeExample; +import io.metersphere.functional.mapper.ExtFunctionalCaseRelationshipEdgeMapper; import io.metersphere.functional.mapper.FunctionalCaseRelationshipEdgeMapper; +import io.metersphere.functional.request.RelationshipAddRequest; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.util.Translator; +import io.metersphere.system.uid.IDGenerator; import jakarta.annotation.Resource; +import org.apache.commons.collections.CollectionUtils; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionUtils; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; @@ -22,6 +35,10 @@ public class FunctionalCaseRelationshipEdgeService { @Resource private FunctionalCaseRelationshipEdgeMapper functionalCaseRelationshipEdgeMapper; + @Resource + private ExtFunctionalCaseRelationshipEdgeMapper extFunctionalCaseRelationshipEdgeMapper; + @Resource + SqlSessionFactory sqlSessionFactory; public List getExcludeIds(String id) { @@ -43,4 +60,163 @@ public class FunctionalCaseRelationshipEdgeService { return functionalCaseRelationshipEdgeMapper.selectByExample(example); } + + /** + * 添加前后置用例 + * + * @param request request + * @param ids ids + */ + public void add(RelationshipAddRequest request, List ids, String userId) { + List edgeList = getExistRelationshipEdges(ids, request.getId()); + Set addEdgesIds = new HashSet<>(); + String graphId = IDGenerator.nextStr(); + switch (request.getType()) { + case "PRE": + addPre(request, ids, userId, edgeList, addEdgesIds, graphId); + break; + case "POST": + addPost(request, ids, userId, edgeList, addEdgesIds, graphId); + break; + default: + break; + } + HashSet nodeIds = new HashSet<>(); + nodeIds.addAll(edgeList.stream().map(FunctionalCaseRelationshipEdge::getSourceId).collect(Collectors.toSet())); + nodeIds.addAll(edgeList.stream().map(FunctionalCaseRelationshipEdge::getTargetId).collect(Collectors.toSet())); + // 判断是否有环 + HashSet visitedSet = new HashSet<>(); + nodeIds.forEach(nodeId -> { + if (!visitedSet.contains(nodeId) && directedCycle(nodeId, edgeList, new HashSet<>(), visitedSet)) { + throw new MSException(Translator.get("cycle_relationship")); + } + ; + }); + SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); + FunctionalCaseRelationshipEdgeMapper batchMapper = sqlSession.getMapper(FunctionalCaseRelationshipEdgeMapper.class); + + edgeList.forEach(item -> { + if (addEdgesIds.contains(item.getSourceId() + item.getTargetId())) { + if (batchMapper.selectByPrimaryKey(item.getId()) == null) { + batchMapper.insert(item); + } else { + item.setGraphId(graphId); // 把原来图的id设置成合并后新的图的id + batchMapper.updateByPrimaryKey(item); + } + } else { + item.setGraphId(graphId); // 把原来图的id设置成合并后新的图的id + batchMapper.updateByPrimaryKey(item); + } + }); + sqlSession.flushStatements(); + SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory); + + } + + /** + * 添加后置用例 + * + * @param request + * @param ids + * @param userId + * @param edgeList + * @param addEdgesIds + * @param graphId + */ + private void addPost(RelationshipAddRequest request, List ids, String userId, List edgeList, Set addEdgesIds, String graphId) { + ids.forEach(sourceId -> { + FunctionalCaseRelationshipEdge edge = createRelationshipEdge(sourceId, request.getId(), graphId, userId); + edgeList.add(edge); + addEdgesIds.add(edge.getSourceId() + edge.getTargetId()); + }); + } + + + /** + * 给定一点,深度搜索该连通图中是否存在环 + * + * @param id + * @param edges + * @param markSet 标记该路径上经过的节点 + * @param visitedSet 标记访问过的节点 + * @return + */ + public boolean directedCycle(String id, List edges, Set markSet, Set visitedSet) { + + if (markSet.contains(id)) { + // 如果已经访问过该节点,则说明存在环 + return true; + } + + markSet.add(id); + visitedSet.add(id); + + ArrayList nextLevelNodes = new ArrayList(); + for (FunctionalCaseRelationshipEdge relationshipEdge : edges) { + if (id.equals(relationshipEdge.getSourceId())) { + nextLevelNodes.add(relationshipEdge.getTargetId()); + } + } + + for (String nextNode : nextLevelNodes) { + if (directedCycle(nextNode, edges, markSet, visitedSet)) { + return true; + } + } + + // 关键,递归完这一条路径要把这个标记去掉,否则会误判为有环 + // 比如 1->3, 1->2->3 , 3 经过多次但是无环 + markSet.remove(id); + + return false; + } + + /** + * 获取已经存在的依赖关系图 + * + * @param ids + * @param id + * @return + */ + private List getExistRelationshipEdges(List ids, String id) { + List graphNodes = new ArrayList<>(); + graphNodes.add(id); + graphNodes.addAll(ids); + List graphIds = extFunctionalCaseRelationshipEdgeMapper.getGraphIds(graphNodes); + if (CollectionUtils.isEmpty(graphIds)) { + return new ArrayList<>(); + } + FunctionalCaseRelationshipEdgeExample example = new FunctionalCaseRelationshipEdgeExample(); + example.createCriteria() + .andGraphIdIn(graphIds); + + return functionalCaseRelationshipEdgeMapper.selectByExample(example); + } + + + /** + * 添加前置用例 + * + * @param request request + * @param ids ids + */ + private void addPre(RelationshipAddRequest request, List ids, String userId, List edgeList, Set addEdgesIds, String graphId) { + ids.forEach(targetId -> { + FunctionalCaseRelationshipEdge edge = createRelationshipEdge(request.getId(), targetId, graphId, userId); + edgeList.add(edge); + addEdgesIds.add(edge.getSourceId() + edge.getTargetId()); + }); + } + + private FunctionalCaseRelationshipEdge createRelationshipEdge(String sourceId, String targetId, String graphId, String userId) { + FunctionalCaseRelationshipEdge edge = new FunctionalCaseRelationshipEdge(); + edge.setId(IDGenerator.nextStr()); + edge.setSourceId(sourceId); + edge.setTargetId(targetId); + edge.setGraphId(graphId); + edge.setCreateUser(userId); + edge.setCreateTime(System.currentTimeMillis()); + edge.setUpdateTime(System.currentTimeMillis()); + return edge; + } } \ No newline at end of file diff --git a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseRelationshipControllerTests.java b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseRelationshipControllerTests.java index 9ad82dddd2..08fdbb89ea 100644 --- a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseRelationshipControllerTests.java +++ b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseRelationshipControllerTests.java @@ -1,9 +1,11 @@ package io.metersphere.functional.controller; +import io.metersphere.functional.request.RelationshipAddRequest; import io.metersphere.functional.request.RelationshipPageRequest; import io.metersphere.sdk.util.JSON; import io.metersphere.system.base.BaseTest; import io.metersphere.system.controller.handler.ResultHolder; +import io.metersphere.system.controller.handler.result.MsHttpResultCode; import org.junit.jupiter.api.*; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -13,6 +15,7 @@ import org.springframework.test.web.servlet.MvcResult; import java.nio.charset.StandardCharsets; import java.util.HashMap; +import java.util.List; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @@ -20,6 +23,7 @@ import java.util.HashMap; public class FunctionalCaseRelationshipControllerTests extends BaseTest { public static final String RELATE_PAGE = "/functional/case/relationship/relate/page"; + public static final String ADD = "/functional/case/relationship/add"; @Test @@ -44,4 +48,41 @@ public class FunctionalCaseRelationshipControllerTests extends BaseTest { }}); this.requestPostWithOkAndReturn(RELATE_PAGE, request); } + + + @Test + @Order(2) + public void testAddRelationship() throws Exception { + //前置用例 + RelationshipAddRequest request = new RelationshipAddRequest(); + request.setId("wx_relationship_1"); + request.setProjectId("wx_relationship"); + request.setType("PRE"); + request.setSelectIds(List.of("wx_relationship_3")); + MvcResult mvcResult = this.requestPostWithOkAndReturn(ADD, request); + // 获取返回值 + String returnData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + ResultHolder resultHolder = JSON.parseObject(returnData, ResultHolder.class); + // 返回请求正常 + Assertions.assertNotNull(resultHolder); + + //测试依赖关系 形成环 + request.setId("wx_relationship_3"); + request.setProjectId("wx_relationship"); + request.setType("PRE"); + request.setSelectIds(List.of("wx_relationship_1")); + assertErrorCode(this.requestPost(ADD, request), MsHttpResultCode.FAILED); + + //后置用例 + request.setId("wx_relationship_4"); + request.setType("POST"); + request.setSelectIds(List.of("wx_relationship_5")); + MvcResult postResult = this.requestPostWithOkAndReturn(ADD, request); + // 获取返回值 + String postReturnData = postResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + ResultHolder postResultHolder = JSON.parseObject(postReturnData, ResultHolder.class); + // 返回请求正常 + Assertions.assertNotNull(postResultHolder); + + } }