From 710d51a0bc21e5faba404788a36053d645981e66 Mon Sep 17 00:00:00 2001 From: Jianguo-Genius Date: Tue, 15 Oct 2024 15:05:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8E=A5=E5=8F=A3=E6=B5=8B=E8=AF=95):=20?= =?UTF-8?q?=E5=9C=BA=E6=99=AF=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sdk/constants/LocalRepositoryDir.java | 1 - .../io/metersphere/sdk/file/FileRequest.java | 8 + .../metersphere/sdk/util/TempFileUtils.java | 18 ++ .../api/constants/ApiScenarioExportType.java | 6 + .../scenario/ApiScenarioController.java | 17 ++ ....java => ApiDefinitionExportResponse.java} | 2 +- .../dto/export/ApiScenarioExportResponse.java | 21 ++ .../dto/export/ApiScenarioStepExportDTO.java | 10 + ...tersphereApiDefinitionExportResponse.java} | 2 +- .../MetersphereApiScenarioExportResponse.java | 65 ++++++ ...> SwaggerApiDefinitionExportResponse.java} | 2 +- .../scenario/ApiScenarioImportRequest.java | 2 +- .../api/mapper/ExtApiScenarioMapper.xml | 1 - .../parser/api/MetersphereExportParser.java | 8 +- .../api/parser/api/Swagger3ExportParser.java | 6 +- .../MetersphereParserApiDefinition.java | 6 +- .../ApiScenarioDataTransferService.java | 221 ++++++++++++++++-- .../ApiDefinitionExportService.java | 84 ++----- .../scenario/ApiScenarioLogService.java | 16 ++ .../service/scenario/ApiScenarioService.java | 49 ++++ .../api/utils/ApiScenarioUtils.java | 28 +++ .../ApiDefinitionControllerTests.java | 4 +- ...cenarioControllerImportAndExportTests.java | 57 ++++- .../ApiDefinitionImportTestService.java | 4 +- .../system/manager/ExportTaskManager.java | 13 ++ .../system/service/FileService.java | 8 + 26 files changed, 560 insertions(+), 99 deletions(-) create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/constants/ApiScenarioExportType.java rename backend/services/api-test/src/main/java/io/metersphere/api/dto/export/{ApiExportResponse.java => ApiDefinitionExportResponse.java} (75%) create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiScenarioExportResponse.java create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiScenarioStepExportDTO.java rename backend/services/api-test/src/main/java/io/metersphere/api/dto/export/{MetersphereApiExportResponse.java => MetersphereApiDefinitionExportResponse.java} (76%) create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/dto/export/MetersphereApiScenarioExportResponse.java rename backend/services/api-test/src/main/java/io/metersphere/api/dto/export/{SwaggerApiExportResponse.java => SwaggerApiDefinitionExportResponse.java} (81%) create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/utils/ApiScenarioUtils.java diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/LocalRepositoryDir.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/LocalRepositoryDir.java index ae1d36d751..4387db6fe8 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/LocalRepositoryDir.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/LocalRepositoryDir.java @@ -55,7 +55,6 @@ public class LocalRepositoryDir { public static String getSystemTempDir() { return SYSTEM_TEMP_DIR; } - public static String getSystemCacheDir() { return SYSTEM_CACHE_DIR; } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileRequest.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileRequest.java index df4a6a2ed6..f7ac30a3a6 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileRequest.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileRequest.java @@ -4,8 +4,10 @@ import io.metersphere.sdk.dto.FileMetadataRepositoryDTO; import io.metersphere.sdk.dto.FileModuleRepositoryDTO; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor public class FileRequest { private String folder; @@ -22,6 +24,12 @@ public class FileRequest { public void setGitFileRequest(FileModuleRepositoryDTO repository, FileMetadataRepositoryDTO file) { gitFileRequest = new GitFileRequest(repository.getUrl(), repository.getToken(), repository.getUserName(), file.getBranch(), file.getCommitId()); } + + public FileRequest(String folder, String storage, String fileName) { + this.folder = folder; + this.storage = storage; + this.fileName = fileName; + } } @Data diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/TempFileUtils.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/TempFileUtils.java index 1159d051cc..d46d995c9d 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/TempFileUtils.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/TempFileUtils.java @@ -1,5 +1,6 @@ package io.metersphere.sdk.util; +import io.metersphere.sdk.exception.MSException; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; @@ -138,4 +139,21 @@ public class TempFileUtils { } return previewByte; } + + public static void writeExportFile(String fileAbsoluteName, Object exportResponse) { + File createFile = new File(fileAbsoluteName); + if (!createFile.exists()) { + try { + createFile.getParentFile().mkdirs(); + createFile.createNewFile(); + } catch (IOException e) { + throw new MSException(e); + } + } + try { + FileUtils.writeByteArrayToFile(createFile, JSON.toJSONString(exportResponse).getBytes()); + } catch (Exception e) { + LogUtils.error(e); + } + } } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/constants/ApiScenarioExportType.java b/backend/services/api-test/src/main/java/io/metersphere/api/constants/ApiScenarioExportType.java new file mode 100644 index 0000000000..8f44cc22c3 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/constants/ApiScenarioExportType.java @@ -0,0 +1,6 @@ +package io.metersphere.api.constants; + +public enum ApiScenarioExportType { + METERSPHERE_SIMPLE, + METERSPHERE_ALL_DATA +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/controller/scenario/ApiScenarioController.java b/backend/services/api-test/src/main/java/io/metersphere/api/controller/scenario/ApiScenarioController.java index ac5b42b204..a16b349e52 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/controller/scenario/ApiScenarioController.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/controller/scenario/ApiScenarioController.java @@ -37,6 +37,7 @@ 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 jakarta.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.RequiresPermissions; @@ -330,4 +331,20 @@ public class ApiScenarioController { request.setOperator(SessionUtils.getUserId()); apiScenarioDataTransferService.importScenario(file, request); } + + @GetMapping("/stop/{taskId}") + @Operation(summary = "接口测试-接口场景管理-导出-停止导出") + @RequiresPermissions(PermissionConstants.PROJECT_API_SCENARIO_EXPORT) + @CheckOwner(resourceId = "#projectId", resourceType = "project") + public void caseStopExport(@PathVariable String taskId) { + apiScenarioDataTransferService.stopExport(taskId, SessionUtils.getUserId()); + } + + @GetMapping(value = "/download/file/{projectId}/{fileId}") + @Operation(summary = "接口测试-接口场景管理-下载文件") + @RequiresPermissions(PermissionConstants.PROJECT_API_SCENARIO_EXPORT) + @CheckOwner(resourceId = "#projectId", resourceType = "project") + public void downloadImgById(@PathVariable String projectId, @PathVariable String fileId, HttpServletResponse httpServletResponse) { + apiScenarioDataTransferService.downloadFile(projectId, fileId, SessionUtils.getUserId(), httpServletResponse); + } } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiExportResponse.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiDefinitionExportResponse.java similarity index 75% rename from backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiExportResponse.java rename to backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiDefinitionExportResponse.java index 0046784477..5cf95a71b7 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiExportResponse.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiDefinitionExportResponse.java @@ -9,7 +9,7 @@ import java.io.Serializable; * @author wx */ @Data -public class ApiExportResponse implements Serializable { +public class ApiDefinitionExportResponse implements Serializable { @Serial private static final long serialVersionUID = 1L; diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiScenarioExportResponse.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiScenarioExportResponse.java new file mode 100644 index 0000000000..8cc5c929cf --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiScenarioExportResponse.java @@ -0,0 +1,21 @@ +package io.metersphere.api.dto.export; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @author wx + */ +@Data +public class ApiScenarioExportResponse implements Serializable { + + private String organizationId; + + private String projectId; + + @Serial + private static final long serialVersionUID = 1L; + +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiScenarioStepExportDTO.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiScenarioStepExportDTO.java new file mode 100644 index 0000000000..db6c08c40f --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiScenarioStepExportDTO.java @@ -0,0 +1,10 @@ +package io.metersphere.api.dto.export; + +import io.metersphere.api.domain.ApiScenarioStep; +import io.metersphere.plugin.api.spi.AbstractMsTestElement; +import lombok.Data; + +@Data +public class ApiScenarioStepExportDTO extends ApiScenarioStep { + private AbstractMsTestElement stepComponent; +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/MetersphereApiExportResponse.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/MetersphereApiDefinitionExportResponse.java similarity index 76% rename from backend/services/api-test/src/main/java/io/metersphere/api/dto/export/MetersphereApiExportResponse.java rename to backend/services/api-test/src/main/java/io/metersphere/api/dto/export/MetersphereApiDefinitionExportResponse.java index 7bfa58bfe3..89132181a9 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/MetersphereApiExportResponse.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/MetersphereApiDefinitionExportResponse.java @@ -10,7 +10,7 @@ import java.util.List; * @author wx */ @Data -public class MetersphereApiExportResponse extends ApiExportResponse { +public class MetersphereApiDefinitionExportResponse extends ApiDefinitionExportResponse { private List apiDefinitions = new ArrayList<>(); diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/MetersphereApiScenarioExportResponse.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/MetersphereApiScenarioExportResponse.java new file mode 100644 index 0000000000..264b674a1d --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/MetersphereApiScenarioExportResponse.java @@ -0,0 +1,65 @@ +package io.metersphere.api.dto.export; + +import io.metersphere.api.constants.ApiScenarioStepType; +import io.metersphere.api.domain.ApiScenarioCsv; +import io.metersphere.api.dto.converter.ApiDefinitionDetail; +import io.metersphere.api.dto.definition.ApiTestCaseDTO; +import io.metersphere.api.dto.scenario.ApiScenarioDetail; +import io.metersphere.api.dto.scenario.ApiScenarioStepDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author wx + */ +@Data +public class MetersphereApiScenarioExportResponse extends ApiScenarioExportResponse { + + @Schema(description = "导出的场景") + private List exportScenarioList = new ArrayList<>(); + + @Schema(description = "场景CSV相关的数据") + private List apiScenarioCsvList = new ArrayList<>(); + + @Schema(description = "所有场景步骤") + private List scenarioStepList = new ArrayList<>(); + + @Schema(description = "所有场景步骤内容") + private Map scenarioStepBlobMap = new HashMap<>(); + + + @Schema(description = "有关联的接口定义") + private List relatedApiDefinitions = new ArrayList<>(); + + @Schema(description = "有关联的接口用例") + private List relatedApiTestCaseList = new ArrayList<>(); + + @Schema(description = "有关联的场景") + private List relatedScenarioList = new ArrayList<>(); + + public void addExportApi(ApiDefinitionDetail detail) { + relatedApiDefinitions.add(detail); + } + + public void addExportApiCase(ApiTestCaseDTO apiCase) { + relatedApiTestCaseList.add(apiCase); + } + + public void addExportScenario(ApiScenarioDetail apiScenarioDetail) { + exportScenarioList.add(apiScenarioDetail); + } + + public void setStepTypeToCustomRequest() { + scenarioStepList.forEach(step -> { + if (StringUtils.equalsAnyIgnoreCase(step.getStepType(), ApiScenarioStepType.API.name(), ApiScenarioStepType.API_CASE.name())) { + step.setStepType(ApiScenarioStepType.CUSTOM_REQUEST.name()); + } + }); + } +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerApiExportResponse.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerApiDefinitionExportResponse.java similarity index 81% rename from backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerApiExportResponse.java rename to backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerApiDefinitionExportResponse.java index 486f47e93d..13562c5058 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerApiExportResponse.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerApiDefinitionExportResponse.java @@ -9,7 +9,7 @@ import java.util.List; * @author wx */ @Data -public class SwaggerApiExportResponse extends ApiExportResponse{ +public class SwaggerApiDefinitionExportResponse extends ApiDefinitionExportResponse { private String openapi; private SwaggerInfo info; diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioImportRequest.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioImportRequest.java index 15b51450e3..abe5d1bdbd 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioImportRequest.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/scenario/ApiScenarioImportRequest.java @@ -14,7 +14,7 @@ public class ApiScenarioImportRequest { @NotBlank private String projectId; - @Schema(description = "导入的类型 暂定 METERSPHERE JMETER") + @Schema(description = "导入的类型 METERSPHERE/JMETER") @NotBlank private String type; diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiScenarioMapper.xml b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiScenarioMapper.xml index d85f1f7010..2fdfd815ef 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiScenarioMapper.xml +++ b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiScenarioMapper.xml @@ -764,5 +764,4 @@ WHERE module_id = #{0} AND deleted = false - diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/parser/api/MetersphereExportParser.java b/backend/services/api-test/src/main/java/io/metersphere/api/parser/api/MetersphereExportParser.java index 9ca9df65e3..282056fb3f 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/parser/api/MetersphereExportParser.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/parser/api/MetersphereExportParser.java @@ -2,8 +2,8 @@ package io.metersphere.api.parser.api; import io.metersphere.api.dto.converter.ApiDefinitionExportDetail; import io.metersphere.api.dto.definition.*; -import io.metersphere.api.dto.export.ApiExportResponse; -import io.metersphere.api.dto.export.MetersphereApiExportResponse; +import io.metersphere.api.dto.export.ApiDefinitionExportResponse; +import io.metersphere.api.dto.export.MetersphereApiDefinitionExportResponse; import io.metersphere.api.dto.mockserver.MockMatchRule; import io.metersphere.api.dto.mockserver.MockResponse; import io.metersphere.api.utils.ApiDataUtils; @@ -18,12 +18,12 @@ import java.util.stream.Collectors; public class MetersphereExportParser { - public ApiExportResponse parse(List apiDefinitionList, List apiTestCaseList, List apiMockList, Map moduleMap) { + public ApiDefinitionExportResponse parse(List apiDefinitionList, List apiTestCaseList, List apiMockList, Map moduleMap) { Map> apiTestCaseMap = apiTestCaseList.stream().collect(Collectors.groupingBy(ApiTestCaseWithBlob::getApiDefinitionId)); Map> apiMockMap = apiMockList.stream().collect(Collectors.groupingBy(ApiMockWithBlob::getApiDefinitionId)); - MetersphereApiExportResponse response = new MetersphereApiExportResponse(); + MetersphereApiDefinitionExportResponse response = new MetersphereApiDefinitionExportResponse(); for (ApiDefinitionWithBlob blob : apiDefinitionList) { ApiDefinitionExportDetail detail = new ApiDefinitionExportDetail(); if (blob.getRequest() != null) { diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/parser/api/Swagger3ExportParser.java b/backend/services/api-test/src/main/java/io/metersphere/api/parser/api/Swagger3ExportParser.java index afd1ff2d97..82de3fd5a1 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/parser/api/Swagger3ExportParser.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/parser/api/Swagger3ExportParser.java @@ -24,12 +24,12 @@ import org.springframework.http.MediaType; import java.nio.charset.StandardCharsets; import java.util.*; -public class Swagger3ExportParser implements ExportParser { +public class Swagger3ExportParser implements ExportParser { @Override - public ApiExportResponse parse(List list, Project project, Map moduleMap) throws Exception { - SwaggerApiExportResponse response = new SwaggerApiExportResponse(); + public ApiDefinitionExportResponse parse(List list, Project project, Map moduleMap) throws Exception { + SwaggerApiDefinitionExportResponse response = new SwaggerApiDefinitionExportResponse(); //openapi response.setOpenapi("3.0.2"); //info diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/parser/api/dataimport/MetersphereParserApiDefinition.java b/backend/services/api-test/src/main/java/io/metersphere/api/parser/api/dataimport/MetersphereParserApiDefinition.java index ecab845443..97297eef05 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/parser/api/dataimport/MetersphereParserApiDefinition.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/parser/api/dataimport/MetersphereParserApiDefinition.java @@ -7,7 +7,7 @@ import io.metersphere.api.dto.converter.ApiDefinitionImportDataAnalysisResult; import io.metersphere.api.dto.converter.ApiDefinitionImportFileParseResult; import io.metersphere.api.dto.definition.ApiDefinitionMockDTO; import io.metersphere.api.dto.definition.ApiTestCaseDTO; -import io.metersphere.api.dto.export.MetersphereApiExportResponse; +import io.metersphere.api.dto.export.MetersphereApiDefinitionExportResponse; import io.metersphere.api.dto.request.ImportRequest; import io.metersphere.api.parser.ApiDefinitionImportParser; import io.metersphere.api.utils.ApiDataUtils; @@ -29,9 +29,9 @@ public class MetersphereParserApiDefinition implements ApiDefinitionImportParser @Override public ApiDefinitionImportFileParseResult parse(InputStream source, ImportRequest request) throws Exception { - MetersphereApiExportResponse metersphereApiExportResponse = null; + MetersphereApiDefinitionExportResponse metersphereApiExportResponse = null; try { - metersphereApiExportResponse = ApiDataUtils.parseObject(source, MetersphereApiExportResponse.class); + metersphereApiExportResponse = ApiDataUtils.parseObject(source, MetersphereApiDefinitionExportResponse.class); } catch (Exception e) { LogUtils.error(e.getMessage(), e); throw new MSException(e.getMessage()); diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/service/ApiScenarioDataTransferService.java b/backend/services/api-test/src/main/java/io/metersphere/api/service/ApiScenarioDataTransferService.java index 626976baad..b32bf2d04d 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/service/ApiScenarioDataTransferService.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/service/ApiScenarioDataTransferService.java @@ -1,23 +1,35 @@ package io.metersphere.api.service; +import io.metersphere.api.constants.ApiScenarioExportType; +import io.metersphere.api.constants.ApiScenarioStepType; import io.metersphere.api.domain.*; import io.metersphere.api.dto.ApiFile; +import io.metersphere.api.dto.converter.ApiDefinitionExportDetail; import io.metersphere.api.dto.converter.ApiScenarioPreImportAnalysisResult; -import io.metersphere.api.dto.definition.ApiScenarioBatchExportRequest; +import io.metersphere.api.dto.definition.*; +import io.metersphere.api.dto.export.ApiScenarioExportResponse; +import io.metersphere.api.dto.export.MetersphereApiScenarioExportResponse; import io.metersphere.api.dto.scenario.*; import io.metersphere.api.mapper.*; import io.metersphere.api.parser.ApiScenarioImportParser; import io.metersphere.api.parser.ImportParserFactory; +import io.metersphere.api.service.definition.ApiDefinitionExportService; +import io.metersphere.api.service.scenario.ApiScenarioLogService; import io.metersphere.api.service.scenario.ApiScenarioModuleService; import io.metersphere.api.service.scenario.ApiScenarioService; +import io.metersphere.api.utils.ApiDataUtils; import io.metersphere.api.utils.ApiDefinitionImportUtils; import io.metersphere.api.utils.ApiScenarioImportUtils; +import io.metersphere.functional.domain.ExportTask; +import io.metersphere.plugin.api.spi.AbstractMsTestElement; import io.metersphere.project.domain.Project; import io.metersphere.project.mapper.ExtBaseProjectVersionMapper; import io.metersphere.project.mapper.ProjectMapper; -import io.metersphere.sdk.constants.DefaultRepositoryDir; -import io.metersphere.sdk.constants.ModuleConstants; +import io.metersphere.project.utils.FileDownloadUtils; +import io.metersphere.sdk.constants.*; +import io.metersphere.sdk.dto.ExportMsgDTO; import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.file.FileRequest; import io.metersphere.sdk.util.*; import io.metersphere.system.constants.ExportConstants; import io.metersphere.system.domain.User; @@ -30,9 +42,13 @@ import io.metersphere.system.manager.ExportTaskManager; import io.metersphere.system.mapper.UserMapper; import io.metersphere.system.notice.constants.NoticeConstants; import io.metersphere.system.service.CommonNoticeSendService; +import io.metersphere.system.service.FileService; +import io.metersphere.system.service.NoticeSendService; +import io.metersphere.system.socket.ExportWebSocketHandler; import io.metersphere.system.uid.IDGenerator; import io.metersphere.system.utils.TreeNodeParseUtils; import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.collections.MapUtils; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; @@ -46,6 +62,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.io.File; +import java.io.InputStream; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -56,6 +74,12 @@ import static io.metersphere.project.utils.NodeSortUtils.DEFAULT_NODE_INTERVAL_P @Transactional(rollbackFor = Exception.class) public class ApiScenarioDataTransferService { + private final ThreadLocal currentApiScenarioOrder = new ThreadLocal<>(); + + private final ThreadLocal currentModuleOrder = new ThreadLocal<>(); + + private static final String EXPORT_CASE_TMP_DIR = "apiScenario"; + @Resource private ProjectMapper projectMapper; @Resource @@ -66,7 +90,13 @@ public class ApiScenarioDataTransferService { private ExportTaskManager exportTaskManager; @Resource private ExtApiScenarioMapper extApiScenarioMapper; + @Resource + private ExtApiDefinitionMapper extApiDefinitionMapper; + @Resource + private ExtApiTestCaseMapper extApiTestCaseMapper; + @Resource + private FileService fileService; @Resource private CommonNoticeSendService commonNoticeSendService; @Resource @@ -79,17 +109,17 @@ public class ApiScenarioDataTransferService { private SqlSessionFactory sqlSessionFactory; @Resource private OperationLogService operationLogService; - - private final ThreadLocal currentApiScenarioOrder = new ThreadLocal<>(); - - private final ThreadLocal currentModuleOrder = new ThreadLocal<>(); - + @Resource + private NoticeSendService noticeSendService; + @Resource + private ApiScenarioLogService apiScenarioLogService; + @Resource + private ApiDefinitionExportService apiDefinitionExportService; public String exportScenario(ApiScenarioBatchExportRequest request, String type, String userId) { String returnId; try { exportTaskManager.exportCheck(request.getProjectId(), ExportConstants.ExportType.API_SCENARIO.toString(), userId); - returnId = exportTaskManager.exportAsyncTask( request.getProjectId(), request.getFileId(), userId, @@ -323,7 +353,7 @@ public class ApiScenarioDataTransferService { module.setParentId(t.getParentId()); module.setProjectId(projectId); module.setCreateUser(operator); - module.setPos(getImportNextModuleOrder(apiScenarioModuleService::getNextOrder, projectId)); + module.setPos(getImportNextOrder(apiScenarioModuleService::getNextOrder, currentModuleOrder, projectId)); module.setCreateTime(System.currentTimeMillis()); module.setUpdateUser(operator); module.setUpdateTime(module.getCreateTime()); @@ -352,7 +382,7 @@ public class ApiScenarioDataTransferService { ApiScenario scenario = new ApiScenario(); BeanUtils.copyBean(scenario, t); scenario.setNum(apiScenarioService.getNextNum(projectId)); - scenario.setPos(getImportNextModuleOrder(apiScenarioService::getNextOrder, projectId)); + scenario.setPos(getImportNextOrder(apiScenarioService::getNextOrder, currentApiScenarioOrder, projectId)); scenario.setLatest(true); scenario.setCreateUser(operator); scenario.setUpdateUser(operator); @@ -479,13 +509,13 @@ public class ApiScenarioDataTransferService { } } - private Long getImportNextModuleOrder(Function subFunc, String projectId) { - Long order = currentModuleOrder.get(); + private Long getImportNextOrder(Function subFunc, ThreadLocal nextOrder, String projectId) { + Long order = nextOrder.get(); if (order == null) { order = subFunc.apply(projectId); } order = order + DEFAULT_NODE_INTERVAL_POS; - currentModuleOrder.set(order); + nextOrder.set(order); return order; } @@ -542,8 +572,167 @@ public class ApiScenarioDataTransferService { return analysisResult; } - private String exportApiScenarioZip(ApiScenarioBatchExportRequest request, String type, String userId) { - // todo 场景导出 + private String exportApiScenarioZip(ApiScenarioBatchExportRequest request, String exportType, String userId) throws Exception { + File tmpDir = new File(LocalRepositoryDir.getSystemTempDir() + File.separatorChar + EXPORT_CASE_TMP_DIR + "_" + IDGenerator.nextStr()); + if (!tmpDir.mkdirs()) { + throw new MSException(Translator.get("upload_fail")); + } + String fileType = "zip"; + try { + User user = userMapper.selectByPrimaryKey(userId); + noticeSendService.setLanguage(user.getLanguage()); + //获取导出的ids集合 + List ids = apiScenarioService.doSelectIds(request, false); + if (CollectionUtils.isEmpty(ids)) { + return null; + } + Map moduleMap = this.apiScenarioModuleService.getTree(request.getProjectId()).stream().collect(Collectors.toMap(BaseTreeNode::getId, BaseTreeNode::getPath)); + + String fileFolder = tmpDir.getPath() + File.separatorChar + request.getFileId(); + int fileIndex = 1; + SubListUtils.dealForSubList(ids, 500, subList -> { + request.setSelectIds(subList); + ApiScenarioExportResponse exportResponse = this.genMetersphereExportResponse(request, moduleMap, exportType, userId); + TempFileUtils.writeExportFile(fileFolder + File.separatorChar + "scenario_export_" + fileIndex + ".ms", exportResponse); + }); + File zipFile = MsFileUtils.zipFile(tmpDir.getPath(), request.getFileId()); + if (zipFile == null) { + return null; + } + fileService.upload(zipFile, new FileRequest(DefaultRepositoryDir.getExportApiTempDir(), StorageType.MINIO.name(), request.getFileId())); + + // 生成日志 + LogDTO logDTO = apiScenarioLogService.exportExcelLog(request.getFileId(), exportType, userId, projectMapper.selectByPrimaryKey(request.getProjectId())); + operationLogService.add(logDTO); + + String taskId = exportTaskManager.getExportTaskId(request.getProjectId(), ExportConstants.ExportType.API_SCENARIO.name(), ExportConstants.ExportState.PREPARED.toString(), userId, null, fileType); + ExportWebSocketHandler.sendMessageSingle( + new ExportMsgDTO(request.getFileId(), taskId, ids.size(), true, MsgType.EXEC_RESULT.name()) + ); + } catch (Exception e) { + LogUtils.error(e); + List exportTasks = exportTaskManager.getExportTasks(request.getProjectId(), ExportConstants.ExportType.API_SCENARIO.name(), ExportConstants.ExportState.PREPARED.toString(), userId, null); + if (CollectionUtils.isNotEmpty(exportTasks)) { + exportTaskManager.updateExportTask(ExportConstants.ExportState.ERROR.name(), exportTasks.getFirst().getId(), fileType); + } + ExportMsgDTO exportMsgDTO = new ExportMsgDTO(request.getFileId(), "", 0, false, MsgType.EXEC_RESULT.name()); + ExportWebSocketHandler.sendMessageSingle(exportMsgDTO); + throw new MSException(e); + } finally { + MsFileUtils.deleteDir(tmpDir.getPath()); + } return null; } + + private ApiScenarioExportResponse genMetersphereExportResponse(ApiScenarioBatchExportRequest request, Map moduleMap, String exportType, String userId) { + Project project = projectMapper.selectByPrimaryKey(request.getProjectId()); + MetersphereApiScenarioExportResponse response = apiScenarioService.selectAndSortScenarioDetailWithIds(request.getSelectIds()); + response.setProjectId(project.getId()); + response.setOrganizationId(project.getOrganizationId()); + + if (StringUtils.equalsIgnoreCase(ApiScenarioExportType.METERSPHERE_ALL_DATA.name(), exportType)) { + // 全量导出,导出引用的api、apiCase + List apiDefinitionIdList = new ArrayList<>(); + List apiCaseIdList = new ArrayList<>(); + response.getScenarioStepList().forEach(step -> { + if (StringUtils.isNotBlank(step.getResourceId())) { + if (StringUtils.equalsIgnoreCase(step.getStepType(), ApiScenarioStepType.API.name())) { + if (!apiDefinitionIdList.contains(step.getResourceId())) { + apiDefinitionIdList.add(step.getResourceId()); + } + } else if (StringUtils.equalsIgnoreCase(step.getStepType(), ApiScenarioStepType.API_CASE.name())) { + if (!apiCaseIdList.contains(step.getResourceId())) { + apiCaseIdList.add(step.getResourceId()); + } + } + } + }); + List apiTestCaseWithBlobs = extApiTestCaseMapper.selectAllDetailByApiIds(apiCaseIdList); + if (CollectionUtils.isNotEmpty(apiCaseIdList)) { + apiTestCaseWithBlobs.forEach(item -> { + if (!apiDefinitionIdList.contains(item.getApiDefinitionId())) { + apiDefinitionIdList.add(item.getApiDefinitionId()); + } + }); + } + + Map> projectApiModuleIdMap = new HashMap<>(); + if (CollectionUtils.isNotEmpty(apiDefinitionIdList)) { + List apiList = extApiDefinitionMapper.selectApiDefinitionWithBlob(apiDefinitionIdList); + List projectIdList = apiList.stream().map(ApiDefinitionWithBlob::getProjectId).distinct().toList(); + projectIdList.forEach(projectId -> projectApiModuleIdMap.put(projectId, apiDefinitionExportService.buildModuleIdPathMap(request.getProjectId()))); + + for (ApiDefinitionWithBlob blob : apiList) { + ApiDefinitionExportDetail detail = new ApiDefinitionExportDetail(); + if (blob.getRequest() != null) { + detail.setRequest(ApiDataUtils.parseObject(new String(blob.getRequest()), AbstractMsTestElement.class)); + } + if (blob.getResponse() != null) { + detail.setResponse(ApiDataUtils.parseArray(new String(blob.getResponse()), HttpResponse.class)); + } + detail.setName(blob.getName()); + detail.setProtocol(blob.getProtocol()); + detail.setMethod(blob.getMethod()); + detail.setPath(blob.getPath()); + detail.setStatus(blob.getStatus()); + detail.setTags(blob.getTags()); + detail.setPos(blob.getPos()); + detail.setDescription(blob.getDescription()); + + String moduleName; + if (StringUtils.equals(blob.getModuleId(), ModuleConstants.DEFAULT_NODE_ID)) { + moduleName = Translator.get("api_unplanned_request"); + } else if (projectApiModuleIdMap.containsKey(detail.getProjectId()) && projectApiModuleIdMap.get(detail.getProjectId()).containsKey(blob.getModuleId())) { + moduleName = projectApiModuleIdMap.get(detail.getProjectId()).get(blob.getModuleId()); + } else { + detail.setModuleId(ModuleConstants.DEFAULT_NODE_ID); + moduleName = Translator.get("api_unplanned_request"); + } + detail.setModulePath(moduleName); + response.addExportApi(detail); + + } + } + if (CollectionUtils.isNotEmpty(apiTestCaseWithBlobs)) { + for (ApiTestCaseWithBlob apiTestCaseWithBlob : apiTestCaseWithBlobs) { + ApiTestCaseDTO dto = new ApiTestCaseDTO(); + dto.setName(apiTestCaseWithBlob.getName()); + dto.setPriority(apiTestCaseWithBlob.getPriority()); + dto.setStatus(apiTestCaseWithBlob.getStatus()); + dto.setTags(apiTestCaseWithBlob.getTags()); + dto.setRequest(ApiDataUtils.parseObject(new String(apiTestCaseWithBlob.getRequest()), AbstractMsTestElement.class)); + response.addExportApiCase(dto); + } + } + + } else { + // 普通导出,所有的引用都改为复制,并且Api、ApiCase改为CUSTOM_REQUEST + response.setStepTypeToCustomRequest(); + } + return response; + } + + public void stopExport(String taskId, String userId) { + exportTaskManager.sendStopMessage(taskId, userId); + } + + public void downloadFile(String projectId, String fileId, String userId, HttpServletResponse httpServletResponse) { + List exportTasks = exportTaskManager.getExportTasks(projectId, ExportConstants.ExportType.API_SCENARIO.name(), ExportConstants.ExportState.SUCCESS.toString(), userId, fileId); + if (CollectionUtils.isEmpty(exportTasks)) { + return; + } + ExportTask tasksFirst = exportTasks.getFirst(); + Project project = projectMapper.selectByPrimaryKey(projectId); + FileRequest fileRequest = new FileRequest(); + fileRequest.setFileName(tasksFirst.getFileId()); + fileRequest.setFolder(DefaultRepositoryDir.getExportApiTempDir()); + fileRequest.setStorage(StorageType.MINIO.name()); + String fileName = "Metersphere_scenario_" + project.getName() + "." + tasksFirst.getFileType(); + try { + InputStream fileInputStream = fileService.getFileAsStream(fileRequest); + FileDownloadUtils.zipFilesWithResponse(fileName, fileInputStream, httpServletResponse); + } catch (Exception e) { + throw new MSException("get file error"); + } + } } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/service/definition/ApiDefinitionExportService.java b/backend/services/api-test/src/main/java/io/metersphere/api/service/definition/ApiDefinitionExportService.java index d3dbe001a8..0930803a6e 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/service/definition/ApiDefinitionExportService.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/service/definition/ApiDefinitionExportService.java @@ -1,8 +1,7 @@ package io.metersphere.api.service.definition; import io.metersphere.api.dto.definition.*; -import io.metersphere.api.dto.export.ApiExportResponse; -import io.metersphere.api.mapper.ApiDefinitionModuleMapper; +import io.metersphere.api.dto.export.ApiDefinitionExportResponse; import io.metersphere.api.mapper.ExtApiDefinitionMapper; import io.metersphere.api.mapper.ExtApiDefinitionMockMapper; import io.metersphere.api.mapper.ExtApiTestCaseMapper; @@ -31,13 +30,10 @@ import io.metersphere.system.uid.IDGenerator; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; @@ -52,12 +48,9 @@ import java.util.stream.Collectors; @Service public class ApiDefinitionExportService { - @Resource private ExtApiDefinitionMapper extApiDefinitionMapper; @Resource - private ApiDefinitionModuleMapper apiDefinitionModuleMapper; - @Resource private ExtApiTestCaseMapper extApiTestCaseMapper; @Resource private ExtApiDefinitionMockMapper extApiDefinitionMockMapper; @@ -75,10 +68,12 @@ public class ApiDefinitionExportService { private OperationLogService operationLogService; @Resource private ApiDefinitionImportService apiDefinitionImportService; + @Resource + private FileService fileService; - private static final String EXPORT_CASE_TMP_DIR = "tmp"; + private static final String EXPORT_CASE_TMP_DIR = "apiDefinition"; - private Map buildModuleIdPathMap(String projectId) { + public Map buildModuleIdPathMap(String projectId) { List apiModules = apiDefinitionImportService.buildTreeData(projectId, null); Map modulePathMap = new HashMap<>(); apiModules.forEach(item -> { @@ -87,12 +82,12 @@ public class ApiDefinitionExportService { return modulePathMap; } - public ApiExportResponse genApiExportResponse(ApiDefinitionBatchExportRequest request, Map moduleMap, String type, String userId) { + public ApiDefinitionExportResponse genApiExportResponse(ApiDefinitionBatchExportRequest request, Map moduleMap, String type, String userId) { List list = this.selectAndSortByIds(request.getSelectIds()); return switch (type.toLowerCase()) { case "swagger" -> exportSwagger(request, list, moduleMap); case "metersphere" -> exportMetersphere(request, list, moduleMap); - default -> new ApiExportResponse(); + default -> new ApiDefinitionExportResponse(); }; } @@ -118,31 +113,17 @@ public class ApiDefinitionExportService { return returnId; } - @Resource - private FileService fileService; - - public void uploadFileToMinioExportFolder(File file, String fileName) throws Exception { - FileRequest fileRequest = new FileRequest(); - fileRequest.setFileName(fileName); - fileRequest.setFolder(DefaultRepositoryDir.getExportApiTempDir()); - fileRequest.setStorage(StorageType.MINIO.name()); - try (FileInputStream inputStream = new FileInputStream(file)) { - fileService.upload(inputStream, fileRequest); - } - } - public String exportApiDefinitionZip(ApiDefinitionBatchExportRequest request, String exportType, String userId) throws Exception { // 为避免客户端未及时开启ws,此处延迟1s Thread.sleep(1000); - File tmpDir = null; - String fileType = ""; + File tmpDir = new File(LocalRepositoryDir.getSystemTempDir() + File.separatorChar + EXPORT_CASE_TMP_DIR + "_" + IDGenerator.nextStr()); + if (!tmpDir.mkdirs()) { + throw new MSException(Translator.get("upload_fail")); + } + String fileType = "zip"; try { User user = userMapper.selectByPrimaryKey(userId); noticeSendService.setLanguage(user.getLanguage()); - tmpDir = new File(LocalRepositoryDir.getSystemTempDir() + File.separatorChar + EXPORT_CASE_TMP_DIR + "_" + IDGenerator.nextStr()); - if (!tmpDir.exists() && !tmpDir.mkdirs()) { - throw new MSException(Translator.get("upload_fail")); - } //获取导出的ids集合 List ids = this.getBatchExportApiIds(request, request.getProjectId(), userId); if (CollectionUtils.isEmpty(ids)) { @@ -155,44 +136,23 @@ public class ApiDefinitionExportService { int fileIndex = 1; SubListUtils.dealForSubList(ids, 1000, subList -> { request.setSelectIds(subList); - ApiExportResponse exportResponse = this.genApiExportResponse(request, moduleMap, exportType, userId); - File createFile = new File(fileFolder + File.separatorChar + fileIndex + ".json"); - if (!createFile.exists()) { - try { - createFile.getParentFile().mkdirs(); - createFile.createNewFile(); - } catch (IOException e) { - throw new MSException(e); - } - } - try { - FileUtils.writeByteArrayToFile(createFile, JSON.toJSONString(exportResponse).getBytes()); - } catch (Exception e) { - LogUtils.error(e); - } + ApiDefinitionExportResponse exportResponse = this.genApiExportResponse(request, moduleMap, exportType, userId); + TempFileUtils.writeExportFile(fileFolder + File.separatorChar + fileIndex + ".json", exportResponse); }); File zipFile = MsFileUtils.zipFile(tmpDir.getPath(), request.getFileId()); if (zipFile == null) { return null; } - - uploadFileToMinioExportFolder(zipFile, request.getFileId()); + fileService.upload(zipFile, new FileRequest(DefaultRepositoryDir.getExportApiTempDir(), StorageType.MINIO.name(), request.getFileId())); // 生成日志 LogDTO logDTO = apiDefinitionLogService.exportExcelLog(request, exportType, userId, projectMapper.selectByPrimaryKey(request.getProjectId()).getOrganizationId()); operationLogService.add(logDTO); - List exportTasks = exportTaskManager.getExportTasks(request.getProjectId(), ExportConstants.ExportType.API_DEFINITION.name(), ExportConstants.ExportState.PREPARED.toString(), userId, null); - String taskId; - if (CollectionUtils.isNotEmpty(exportTasks)) { - taskId = exportTasks.getFirst().getId(); - exportTaskManager.updateExportTask(ExportConstants.ExportState.SUCCESS.name(), taskId, fileType); - } else { - taskId = MsgType.CONNECT.name(); - } - - ExportMsgDTO exportMsgDTO = new ExportMsgDTO(request.getFileId(), taskId, ids.size(), true, MsgType.EXEC_RESULT.name()); - ExportWebSocketHandler.sendMessageSingle(exportMsgDTO); + String taskId = exportTaskManager.getExportTaskId(request.getProjectId(), ExportConstants.ExportType.API_DEFINITION.name(), ExportConstants.ExportState.PREPARED.toString(), userId, null, fileType); + ExportWebSocketHandler.sendMessageSingle( + new ExportMsgDTO(request.getFileId(), taskId, ids.size(), true, MsgType.EXEC_RESULT.name()) + ); } catch (Exception e) { LogUtils.error(e); List exportTasks = exportTaskManager.getExportTasks(request.getProjectId(), ExportConstants.ExportType.API_DEFINITION.name(), ExportConstants.ExportState.PREPARED.toString(), userId, null); @@ -231,7 +191,7 @@ public class ApiDefinitionExportService { } } - private ApiExportResponse exportSwagger(ApiDefinitionBatchRequest request, List list, Map moduleMap) { + private ApiDefinitionExportResponse exportSwagger(ApiDefinitionBatchRequest request, List list, Map moduleMap) { Project project = projectMapper.selectByPrimaryKey(request.getProjectId()); Swagger3ExportParser swagger3Parser = new Swagger3ExportParser(); try { @@ -241,7 +201,7 @@ public class ApiDefinitionExportService { } } - private ApiExportResponse exportMetersphere(ApiDefinitionBatchExportRequest request, List list, Map moduleMap) { + private ApiDefinitionExportResponse exportMetersphere(ApiDefinitionBatchExportRequest request, List list, Map moduleMap) { try { List apiIds = list.stream().map(ApiDefinitionWithBlob::getId).toList(); List apiTestCaseWithBlobs = new ArrayList<>(); @@ -273,7 +233,7 @@ public class ApiDefinitionExportService { fileRequest.setFileName(tasksFirst.getFileId()); fileRequest.setFolder(DefaultRepositoryDir.getExportApiTempDir()); fileRequest.setStorage(StorageType.MINIO.name()); - String fileName = "Metersphere_case_" + project.getName() + "." + tasksFirst.getFileType(); + String fileName = "Metersphere_api_" + project.getName() + "." + tasksFirst.getFileType(); try { InputStream fileInputStream = fileService.getFileAsStream(fileRequest); FileDownloadUtils.zipFilesWithResponse(fileName, fileInputStream, httpServletResponse); diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioLogService.java b/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioLogService.java index fcbf582111..b571dc32bc 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioLogService.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioLogService.java @@ -18,6 +18,7 @@ import io.metersphere.system.log.constants.OperationLogType; import io.metersphere.system.log.dto.LogDTO; import io.metersphere.system.log.service.OperationLogService; import jakarta.annotation.Resource; +import jakarta.validation.constraints.NotNull; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; @@ -233,4 +234,19 @@ public class ApiScenarioLogService { ); operationLogService.batchAdd(logs); } + + public LogDTO exportExcelLog(String sourceId, String exportType, String userId, @NotNull Project project) { + LogDTO dto = new LogDTO( + project.getId(), + project.getOrganizationId(), + sourceId, + userId, + OperationLogType.EXPORT.name(), + OperationLogModule.API_SCENARIO_MANAGEMENT_SCENARIO, + ""); + dto.setHistory(true); + dto.setPath("/api/scenario/export/" + exportType); + dto.setMethod(HttpMethodConstants.POST.name()); + return dto; + } } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioService.java b/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioService.java index 51b465adca..dbb4ba17ea 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioService.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/service/scenario/ApiScenarioService.java @@ -8,6 +8,7 @@ import io.metersphere.api.dto.*; import io.metersphere.api.dto.debug.ApiFileResourceUpdateRequest; import io.metersphere.api.dto.definition.ExecutePageRequest; import io.metersphere.api.dto.definition.ExecuteReportDTO; +import io.metersphere.api.dto.export.MetersphereApiScenarioExportResponse; import io.metersphere.api.dto.request.ApiTransferRequest; import io.metersphere.api.dto.request.controller.MsScriptElement; import io.metersphere.api.dto.request.http.MsHTTPElement; @@ -23,6 +24,7 @@ import io.metersphere.api.service.definition.ApiDefinitionService; import io.metersphere.api.service.definition.ApiTestCaseService; import io.metersphere.api.utils.ApiDataUtils; import io.metersphere.api.utils.ApiScenarioBatchOperationUtils; +import io.metersphere.api.utils.ApiScenarioUtils; import io.metersphere.functional.domain.FunctionalCaseTestExample; import io.metersphere.functional.mapper.FunctionalCaseTestMapper; import io.metersphere.plugin.api.spi.AbstractMsTestElement; @@ -64,6 +66,7 @@ import io.metersphere.system.uid.NumGenerator; import io.metersphere.system.utils.ScheduleUtils; import io.metersphere.system.utils.ServiceUtils; import jakarta.annotation.Resource; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import org.apache.commons.collections.MapUtils; @@ -2463,4 +2466,50 @@ public class ApiScenarioService extends MoveNodeService { setParamFunc.accept(apiStepResourceInfo, resource); return apiStepResourceInfo; } + + public MetersphereApiScenarioExportResponse selectAndSortScenarioDetailWithIds(@Valid List<@NotBlank(message = "{id must not be blank}") String> scenarioIds) { + MetersphereApiScenarioExportResponse response = new MetersphereApiScenarioExportResponse(); + + // 数据准备 + { + ApiScenarioExample scenarioExample = new ApiScenarioExample(); + scenarioExample.createCriteria().andIdIn(scenarioIds); + List exportApiScenarios = apiScenarioMapper.selectByExample(scenarioExample); + + // 获取所有步骤(包含引用的场景) + List allSteps = getAllStepsByScenarioIds(scenarioIds) + .stream() + .distinct() // 这里可能存在多次引用相同场景,步骤可能会重复,去重 + .collect(Collectors.toList()); + + //查询引用的场景ID + List stepScenarioIds = allSteps.stream().filter(step -> isScenarioStep(step.getStepType())).map(ApiScenarioStepDTO::getResourceId).toList(); + + // 查询所有场景的blob + List allScenarioIds = new ArrayList<>(scenarioIds); + allScenarioIds.addAll(stepScenarioIds); + ApiScenarioBlobExample scenarioBlobExample = new ApiScenarioBlobExample(); + scenarioBlobExample.createCriteria().andIdIn(allScenarioIds); + Map scenarioBlobMap = apiScenarioBlobMapper.selectByExampleWithBLOBs(scenarioBlobExample).stream().collect(Collectors.toMap(ApiScenarioBlob::getId, Function.identity())); + // 场景CSV相关的信息 + ApiScenarioCsvExample apiScenarioCsvExample = new ApiScenarioCsvExample(); + apiScenarioCsvExample.createCriteria().andScenarioIdIn(allScenarioIds); + response.setApiScenarioCsvList(apiScenarioCsvMapper.selectByExample(apiScenarioCsvExample)); + + // 导出的场景 + response.setExportScenarioList(ApiScenarioUtils.parseApiScenarioDetail(exportApiScenarios, scenarioBlobMap)); + + //步骤引用的场景 + if (CollectionUtils.isNotEmpty(stepScenarioIds)) { + scenarioExample.clear(); + scenarioExample.createCriteria().andIdIn(stepScenarioIds); + List otherScenarios = apiScenarioMapper.selectByExample(scenarioExample); + response.setExportScenarioList(ApiScenarioUtils.parseApiScenarioDetail(otherScenarios, scenarioBlobMap)); + } + + response.setScenarioStepBlobMap(getPartialRefStepDetailMap(allSteps)); + } + + return response; + } } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/utils/ApiScenarioUtils.java b/backend/services/api-test/src/main/java/io/metersphere/api/utils/ApiScenarioUtils.java new file mode 100644 index 0000000000..3f5eb1b714 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/utils/ApiScenarioUtils.java @@ -0,0 +1,28 @@ +package io.metersphere.api.utils; + +import io.metersphere.api.domain.ApiScenario; +import io.metersphere.api.domain.ApiScenarioBlob; +import io.metersphere.api.dto.scenario.ApiScenarioDetail; +import io.metersphere.api.dto.scenario.ScenarioConfig; +import io.metersphere.sdk.util.BeanUtils; +import io.metersphere.sdk.util.JSON; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ApiScenarioUtils { + public static List parseApiScenarioDetail(List apiScenarios, Map scenarioBlobMap) { + List returnList = new ArrayList<>(); + for (ApiScenario apiScenario : apiScenarios) { + ApiScenarioDetail apiScenarioDetail = BeanUtils.copyBean(new ApiScenarioDetail(), apiScenario); + apiScenarioDetail.setSteps(List.of()); + ApiScenarioBlob apiScenarioBlob = scenarioBlobMap.get(apiScenario.getId()); + if (apiScenarioBlob != null) { + apiScenarioDetail.setScenarioConfig(JSON.parseObject(new String(apiScenarioBlob.getConfig()), ScenarioConfig.class)); + } + returnList.add(apiScenarioDetail); + } + return returnList; + } +} diff --git a/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiDefinitionControllerTests.java b/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiDefinitionControllerTests.java index d107b899d9..0e6fee14e9 100644 --- a/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiDefinitionControllerTests.java +++ b/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiDefinitionControllerTests.java @@ -9,7 +9,7 @@ import io.metersphere.api.domain.*; import io.metersphere.api.dto.ApiFile; import io.metersphere.api.dto.ReferenceRequest; import io.metersphere.api.dto.definition.*; -import io.metersphere.api.dto.export.MetersphereApiExportResponse; +import io.metersphere.api.dto.export.MetersphereApiDefinitionExportResponse; import io.metersphere.api.dto.request.ApiEditPosRequest; import io.metersphere.api.dto.request.ApiTransferRequest; import io.metersphere.api.dto.request.ImportRequest; @@ -2117,7 +2117,7 @@ public class ApiDefinitionControllerTests extends BaseTest { Assertions.assertEquals(files.length, 1); String fileContent = FileUtils.readFileToString(files[0], StandardCharsets.UTF_8); - MetersphereApiExportResponse exportResponse = ApiDataUtils.parseObject(fileContent, MetersphereApiExportResponse.class); + MetersphereApiDefinitionExportResponse exportResponse = ApiDataUtils.parseObject(fileContent, MetersphereApiDefinitionExportResponse.class); apiDefinitionImportTestService.compareApiExport(exportResponse, exportApiBlobs); diff --git a/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiScenarioControllerImportAndExportTests.java b/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiScenarioControllerImportAndExportTests.java index fb0b34555b..dba0e9a921 100644 --- a/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiScenarioControllerImportAndExportTests.java +++ b/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiScenarioControllerImportAndExportTests.java @@ -1,21 +1,33 @@ package io.metersphere.api.controller; import io.metersphere.api.dto.definition.ApiDefinitionBatchExportRequest; +import io.metersphere.api.dto.export.MetersphereApiScenarioExportResponse; import io.metersphere.api.dto.scenario.ApiScenarioImportRequest; +import io.metersphere.api.utils.ApiDataUtils; +import io.metersphere.functional.domain.ExportTask; import io.metersphere.project.domain.Project; +import io.metersphere.sdk.constants.SessionConstants; import io.metersphere.sdk.util.JSON; +import io.metersphere.sdk.util.MsFileUtils; import io.metersphere.system.base.BaseTest; +import io.metersphere.system.constants.ExportConstants; +import io.metersphere.system.controller.handler.ResultHolder; import io.metersphere.system.dto.AddProjectRequest; import io.metersphere.system.log.constants.OperationLogModule; +import io.metersphere.system.manager.ExportTaskManager; import io.metersphere.system.service.CommonProjectService; import io.metersphere.system.uid.IDGenerator; import jakarta.annotation.Resource; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.*; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -81,9 +93,14 @@ public class ApiScenarioControllerImportAndExportTests extends BaseTest { } } + @Resource + private ExportTaskManager exportTaskManager; + @Test - @Order(1) + @Order(2) public void testExport() throws Exception { + MsFileUtils.deleteDir("/tmp/api-scenario-export/"); + ApiDefinitionBatchExportRequest exportRequest = new ApiDefinitionBatchExportRequest(); String fileId = IDGenerator.nextStr(); exportRequest.setProjectId(project.getId()); @@ -93,5 +110,43 @@ public class ApiScenarioControllerImportAndExportTests extends BaseTest { exportRequest.setExportApiMock(true); MvcResult mvcResult = this.requestPostWithOkAndReturn(URL_POST_EXPORT + "metersphere", exportRequest); String returnData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + + JSON.parseObject(returnData, ResultHolder.class).getData().toString(); + Assertions.assertTrue(StringUtils.isNotBlank(fileId)); + List taskList = exportTaskManager.getExportTasks(exportRequest.getProjectId(), null, null, "admin", fileId); + while (CollectionUtils.isEmpty(taskList)) { + Thread.sleep(1000); + taskList = exportTaskManager.getExportTasks(exportRequest.getProjectId(), null, null, "admin", fileId); + } + + ExportTask task = taskList.getFirst(); + while (!StringUtils.equalsIgnoreCase(task.getState(), ExportConstants.ExportState.SUCCESS.name())) { + Thread.sleep(1000); + task = exportTaskManager.getExportTasks(exportRequest.getProjectId(), null, null, "admin", fileId).getFirst(); + } + + mvcResult = this.download(exportRequest.getProjectId(), fileId); + + byte[] fileBytes = mvcResult.getResponse().getContentAsByteArray(); + + File zipFile = new File("/tmp/api-scenario-export/downloadFiles.zip"); + FileUtils.writeByteArrayToFile(zipFile, fileBytes); + + File[] files = MsFileUtils.unZipFile(zipFile, "/tmp/api-scenario-export/unzip/"); + assert files != null; + Assertions.assertEquals(files.length, 1); + String fileContent = FileUtils.readFileToString(files[0], StandardCharsets.UTF_8); + + MetersphereApiScenarioExportResponse exportResponse = ApiDataUtils.parseObject(fileContent, MetersphereApiScenarioExportResponse.class); + + Assertions.assertEquals(exportResponse.getExportScenarioList().size(), 3); + + MsFileUtils.deleteDir("/tmp/api-scenario-export/"); + } + + private MvcResult download(String projectId, String fileId) throws Exception { + return mockMvc.perform(MockMvcRequestBuilders.get("/api/scenario/download/file/" + projectId + "/" + fileId) + .header(SessionConstants.HEADER_TOKEN, sessionId) + .header(SessionConstants.CSRF_TOKEN, csrfToken)).andReturn(); } } \ No newline at end of file diff --git a/backend/services/api-test/src/test/java/io/metersphere/api/service/ApiDefinitionImportTestService.java b/backend/services/api-test/src/test/java/io/metersphere/api/service/ApiDefinitionImportTestService.java index 87091e7f84..52ef46228f 100644 --- a/backend/services/api-test/src/test/java/io/metersphere/api/service/ApiDefinitionImportTestService.java +++ b/backend/services/api-test/src/test/java/io/metersphere/api/service/ApiDefinitionImportTestService.java @@ -2,7 +2,7 @@ package io.metersphere.api.service; import io.metersphere.api.domain.*; import io.metersphere.api.dto.converter.ApiDefinitionExportDetail; -import io.metersphere.api.dto.export.MetersphereApiExportResponse; +import io.metersphere.api.dto.export.MetersphereApiDefinitionExportResponse; import io.metersphere.api.dto.request.http.MsHTTPElement; import io.metersphere.api.mapper.ApiDefinitionBlobMapper; import io.metersphere.api.mapper.ApiDefinitionMapper; @@ -99,7 +99,7 @@ public class ApiDefinitionImportTestService extends ApiDefinitionImportService { Assertions.assertEquals(moduleChangeCount, diffApiCount); } - public void compareApiExport(MetersphereApiExportResponse exportResponse, List exportApiBlobs) { + public void compareApiExport(MetersphereApiDefinitionExportResponse exportResponse, List exportApiBlobs) { Assertions.assertEquals(exportResponse.getApiDefinitions().size(), exportApiBlobs.size()); List compareList = new ArrayList<>(); for (ApiDefinitionBlob blob : exportApiBlobs) { diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/manager/ExportTaskManager.java b/backend/services/system-setting/src/main/java/io/metersphere/system/manager/ExportTaskManager.java index 91d07535e8..f352801f99 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/manager/ExportTaskManager.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/manager/ExportTaskManager.java @@ -4,6 +4,7 @@ import io.metersphere.functional.domain.ExportTask; import io.metersphere.functional.domain.ExportTaskExample; import io.metersphere.functional.mapper.ExportTaskMapper; import io.metersphere.sdk.constants.KafkaTopicConstants; +import io.metersphere.sdk.constants.MsgType; import io.metersphere.sdk.exception.MSException; import io.metersphere.sdk.util.JSON; import io.metersphere.sdk.util.LogUtils; @@ -11,6 +12,7 @@ import io.metersphere.sdk.util.Translator; import io.metersphere.system.constants.ExportConstants; import io.metersphere.system.uid.IDGenerator; import jakarta.annotation.Resource; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.springframework.kafka.annotation.KafkaListener; @@ -111,6 +113,17 @@ public class ExportTaskManager { } } + public String getExportTaskId(String projectId, String exportType, String exportState, String userId, String fileId, String fileType) { + List exportTasks = this.getExportTasks(projectId, exportType, exportState, userId, fileId); + String taskId; + if (CollectionUtils.isNotEmpty(exportTasks)) { + taskId = exportTasks.getFirst().getId(); + this.updateExportTask(ExportConstants.ExportState.SUCCESS.name(), taskId, fileType); + } else { + taskId = MsgType.CONNECT.name(); + } + return taskId; + } public List getExportTasks(String projectId, String exportType, String exportState, String userId, String fileId) { ExportTaskExample exportTaskExample = new ExportTaskExample(); ExportTaskExample.Criteria criteria = exportTaskExample.createCriteria().andCreateUserEqualTo(userId).andProjectIdEqualTo(projectId); diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/FileService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/FileService.java index 2f08ae58c9..6fdebbbcee 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/service/FileService.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/FileService.java @@ -5,6 +5,8 @@ import io.metersphere.sdk.file.FileRequest; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import java.io.File; +import java.io.FileInputStream; import java.io.InputStream; @Service @@ -13,6 +15,12 @@ public class FileService { return FileCenter.getRepository(request.getStorage()).saveFile(file, request); } + public void upload(File file, FileRequest request) throws Exception { + try (FileInputStream inputStream = new FileInputStream(file)) { + this.upload(inputStream, request); + } + } + public String upload(InputStream inputStream, FileRequest request) throws Exception { return FileCenter.getRepository(request.getStorage()).saveFile(inputStream, request); }