diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/controller/definition/ApiDefinitionController.java b/backend/services/api-test/src/main/java/io/metersphere/api/controller/definition/ApiDefinitionController.java index 6394fd7e8d..7cd1e3efa0 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/controller/definition/ApiDefinitionController.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/controller/definition/ApiDefinitionController.java @@ -6,15 +6,13 @@ import io.metersphere.api.domain.ApiDefinition; import io.metersphere.api.dto.ReferenceDTO; import io.metersphere.api.dto.ReferenceRequest; import io.metersphere.api.dto.definition.*; +import io.metersphere.api.dto.export.ApiExportResponse; import io.metersphere.api.dto.request.ApiEditPosRequest; import io.metersphere.api.dto.request.ApiTransferRequest; import io.metersphere.api.dto.request.ImportRequest; import io.metersphere.api.dto.schema.JsonSchemaItem; import io.metersphere.api.service.ApiFileResourceService; -import io.metersphere.api.service.definition.ApiDefinitionImportService; -import io.metersphere.api.service.definition.ApiDefinitionLogService; -import io.metersphere.api.service.definition.ApiDefinitionNoticeService; -import io.metersphere.api.service.definition.ApiDefinitionService; +import io.metersphere.api.service.definition.*; import io.metersphere.project.service.FileModuleService; import io.metersphere.sdk.constants.DefaultRepositoryDir; import io.metersphere.sdk.constants.PermissionConstants; @@ -60,6 +58,8 @@ public class ApiDefinitionController { private ApiFileResourceService apiFileResourceService; @Resource private ApiDefinitionImportService apiDefinitionImportService; + @Resource + private ApiDefinitionExportService apiDefinitionExportService; @PostMapping(value = "/add") @Operation(summary = "接口测试-接口管理-添加接口定义") @@ -301,4 +301,10 @@ public class ApiDefinitionController { StringUtils.isNotBlank(request.getSortString()) ? request.getSortString() : "id desc"); return PageUtils.setPageInfo(page, apiDefinitionService.getReference(request)); } + + @PostMapping(value = "/export/{type}") + @RequiresPermissions(PermissionConstants.PROJECT_API_DEFINITION_EXPORT) + public ApiExportResponse export(@RequestBody ApiDefinitionBatchRequest request, @PathVariable String type) { + return apiDefinitionExportService.export(request, type, SessionUtils.getUserId()); + } } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/definition/ApiDefinitionWithBlob.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/definition/ApiDefinitionWithBlob.java new file mode 100644 index 0000000000..64c9d07383 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/definition/ApiDefinitionWithBlob.java @@ -0,0 +1,81 @@ +package io.metersphere.api.dto.definition; + +import io.metersphere.api.domain.ApiDefinitionBlob; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @author wx + */ +@Data +public class ApiDefinitionWithBlob extends ApiDefinitionBlob { + + @Schema(description = "接口pk", requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + + @Schema(description = "接口名称", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @Schema(description = "接口协议", requiredMode = Schema.RequiredMode.REQUIRED) + private String protocol; + + @Schema(description = "http协议类型post/get/其它协议则是协议名(mqtt)") + private String method; + + @Schema(description = "http协议路径/其它协议则为空") + private String path; + + @Schema(description = "接口状态/进行中/已完成", requiredMode = Schema.RequiredMode.REQUIRED) + private String status; + + @Schema(description = "自定义id") + private Long num; + + @Schema(description = "标签") + private java.util.List tags; + + @Schema(description = "自定义排序", requiredMode = Schema.RequiredMode.REQUIRED) + private Long pos; + + @Schema(description = "项目fk", requiredMode = Schema.RequiredMode.REQUIRED) + private String projectId; + + @Schema(description = "模块fk", requiredMode = Schema.RequiredMode.REQUIRED) + private String moduleId; + + @Schema(description = "是否为最新版本 0:否,1:是", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean latest; + + @Schema(description = "版本fk", requiredMode = Schema.RequiredMode.REQUIRED) + private String versionId; + + @Schema(description = "版本引用fk", requiredMode = Schema.RequiredMode.REQUIRED) + private String refId; + + @Schema(description = "描述") + private String description; + + @Schema(description = "创建时间") + private Long createTime; + + @Schema(description = "创建人") + private String createUser; + + @Schema(description = "修改时间") + private Long updateTime; + + @Schema(description = "修改人") + private String updateUser; + + @Schema(description = "删除人") + private String deleteUser; + + @Schema(description = "删除时间") + private Long deleteTime; + + @Schema(description = "删除状态", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean deleted; + + @Schema(description = "模块名称") + private String moduleName; +} 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/ApiExportResponse.java new file mode 100644 index 0000000000..0046784477 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/ApiExportResponse.java @@ -0,0 +1,17 @@ +package io.metersphere.api.dto.export; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @author wx + */ +@Data +public class ApiExportResponse implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + +} 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/SwaggerApiExportResponse.java new file mode 100644 index 0000000000..486f47e93d --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerApiExportResponse.java @@ -0,0 +1,21 @@ +package io.metersphere.api.dto.export; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; + +import java.util.List; + +/** + * @author wx + */ +@Data +public class SwaggerApiExportResponse extends ApiExportResponse{ + + private String openapi; + private SwaggerInfo info; + private JsonNode externalDocs; + private List servers; + private List tags; + private JsonNode paths; + private JsonNode components; +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerApiInfo.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerApiInfo.java new file mode 100644 index 0000000000..f3151010ab --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerApiInfo.java @@ -0,0 +1,24 @@ +package io.metersphere.api.dto.export; + + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; + +import java.util.List; + +/* +该类表示 swagger3 的 paths 字段下每个请求类型中的 value,即表示一个 api 定义 + */ + +/** + * @author wx + */ +@Data +public class SwaggerApiInfo { + private List tags; // 对应一个 API 在MS项目中所在的 module 名称 + private String summary; // 对应 API 的名字 + private List parameters; // 对应 API 的请求参数 + private JsonNode requestBody; + private JsonNode responses; + private String description; +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerInfo.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerInfo.java new file mode 100644 index 0000000000..74254a3102 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerInfo.java @@ -0,0 +1,14 @@ +package io.metersphere.api.dto.export; + +import lombok.Data; + +/** + * @author wx + */ +@Data +public class SwaggerInfo { + private String version; + private String title; + private String description; + private String termsOfService; +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerParams.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerParams.java new file mode 100644 index 0000000000..31554d0b87 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerParams.java @@ -0,0 +1,19 @@ +package io.metersphere.api.dto.export; + + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; + +@Data +public class SwaggerParams { + //对应 API 请求参数名 + private String name; + //参数值 + private String value; + //参数类型,可选值为 path,header,query 等 + private String in; + private String description; + //是否是必填参数 + private boolean enable; + private JsonNode schema; +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerTag.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerTag.java new file mode 100644 index 0000000000..cbe5882f9c --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/export/SwaggerTag.java @@ -0,0 +1,12 @@ +package io.metersphere.api.dto.export; + +import lombok.Data; + +/** + * @author wx + */ +@Data +public class SwaggerTag { + private String name; + private String description; +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.java b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.java index 1cc3d30da7..ba741cd59d 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.java @@ -83,4 +83,6 @@ public interface ExtApiDefinitionMapper { List getReference(@Param("request") ReferenceRequest request); List selectByProjectNum(String projectNum); + + List selectApiDefinitionWithBlob(@Param("ids") List ids); } diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.xml b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.xml index 24390c2727..0785cf658b 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.xml +++ b/backend/services/api-test/src/main/java/io/metersphere/api/mapper/ExtApiDefinitionMapper.xml @@ -9,6 +9,13 @@ + + + + + + + update api_definition set delete_user = #{userId},delete_time = #{time}, deleted = 1 , module_id = 'root' @@ -631,4 +638,21 @@ group by a.id, ass.ref_type + + + diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/parser/ExportParser.java b/backend/services/api-test/src/main/java/io/metersphere/api/parser/ExportParser.java index 0556936bda..c4cd1766e8 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/parser/ExportParser.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/parser/ExportParser.java @@ -1,7 +1,10 @@ package io.metersphere.api.parser; -import io.metersphere.api.dto.request.ExportRequest; +import io.metersphere.api.dto.definition.ApiDefinitionWithBlob; +import io.metersphere.project.domain.Project; + +import java.util.List; public interface ExportParser { - T parse(ExportRequest request) throws Exception; + T parse(List list, Project project) throws Exception; } 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 new file mode 100644 index 0000000000..191206f872 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/parser/api/Swagger3ExportParser.java @@ -0,0 +1,575 @@ +package io.metersphere.api.parser.api; + +import com.fasterxml.jackson.databind.JsonNode; +import io.metersphere.api.dto.definition.ApiDefinitionWithBlob; +import io.metersphere.api.dto.export.*; +import io.metersphere.api.dto.request.http.body.Body; +import io.metersphere.api.parser.ExportParser; +import io.metersphere.api.utils.JSONUtil; +import io.metersphere.api.utils.XMLUtil; +import io.metersphere.project.constants.PropertyConstant; +import io.metersphere.project.domain.Project; +import io.metersphere.sdk.util.JSON; +import io.swagger.v3.oas.models.responses.ApiResponse; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.nio.charset.StandardCharsets; +import java.util.*; + +public class Swagger3ExportParser implements ExportParser { + + + @Override + public ApiExportResponse parse(List list, Project project) throws Exception { + SwaggerApiExportResponse response = new SwaggerApiExportResponse(); + //openapi + response.setOpenapi("3.0.2"); + //info + SwaggerInfo swaggerInfo = new SwaggerInfo(); + swaggerInfo.setVersion("3.0"); + swaggerInfo.setTitle("ms-" + project.getName()); + swaggerInfo.setDescription(StringUtils.EMPTY); + swaggerInfo.setTermsOfService(StringUtils.EMPTY); + response.setInfo(swaggerInfo); + //servers + response.setServers(new ArrayList<>()); + //tags + response.setTags(new ArrayList<>()); + + response.setComponents(JSONUtil.createObj()); + response.setExternalDocs(JSONUtil.createObj()); + + //path + JSONObject paths = new JSONObject(); + JSONObject components = new JSONObject(); + List schemas = new LinkedList<>(); + for (ApiDefinitionWithBlob apiDefinition : list) { + SwaggerApiInfo swaggerApiInfo = new SwaggerApiInfo(); + swaggerApiInfo.setSummary(apiDefinition.getName()); + swaggerApiInfo.setTags(Arrays.asList(apiDefinition.getModuleName())); + //请求体 + JSONObject requestObject = JSONUtil.parseObject(new String(apiDefinition.getRequest() == null ? new byte[0] : apiDefinition.getRequest(), StandardCharsets.UTF_8)); + JSONObject requestBody = buildRequestBody(requestObject, schemas); + + swaggerApiInfo.setRequestBody(JSONUtil.parseObjectNode(requestBody.toString())); + JSONArray responseObject = new JSONArray(); + try { + // 设置响应体 + responseObject = JSONUtil.parseArray(new String(apiDefinition.getResponse() == null ? new byte[0] : apiDefinition.getResponse(), StandardCharsets.UTF_8)); + } catch (Exception e) { + responseObject = new JSONArray(new ApiResponse()); + } + JSONObject jsonObject = buildResponseBody(responseObject, schemas); + swaggerApiInfo.setResponses(JSONUtil.parseObjectNode(jsonObject.toString())); + // 设置请求参数列表 + List paramsList = buildParameters(requestObject); + List nodes = new LinkedList<>(); + paramsList.forEach(item -> { + nodes.add(JSONUtil.parseObjectNode(item.toString())); + }); + swaggerApiInfo.setParameters(nodes); + swaggerApiInfo.setDescription(apiDefinition.getDescription()); + JSONObject methodDetail = JSONUtil.parseObject(JSON.toJSONString(swaggerApiInfo)); + if (paths.optJSONObject(apiDefinition.getPath()) == null) { + paths.put(apiDefinition.getPath(), new JSONObject()); + } // 一个路径下有多个发方法,如post,get,因此是一个 JSONObject 类型 + paths.optJSONObject(apiDefinition.getPath()).put(apiDefinition.getMethod().toLowerCase(), methodDetail); + + } + response.setPaths(JSONUtil.parseObjectNode(paths.toString())); + if (CollectionUtils.isNotEmpty(schemas)) { + components.put("schemas", schemas.get(0)); + } + response.setComponents(JSONUtil.parseObjectNode(components.toString())); + + return response; + } + + + private List buildParameters(JSONObject request) { + List paramsList = new ArrayList<>(); + Hashtable typeMap = new Hashtable() {{ + put("headers", "header"); + put("rest", "path"); + put("arguments", "query"); + }}; + Set typeKeys = typeMap.keySet(); + for (String type : typeKeys) { + JSONArray params = request.optJSONArray(type); // 获得请求参数列表 + if (params != null) { + for (int i = 0; i < params.length(); ++i) { + JSONObject param = params.optJSONObject(i); // 对于每个参数: + if (StringUtils.isEmpty(param.optString("key"))) { + continue; + } // 否则无参数的情况,可能多出一行空行 + SwaggerParams swaggerParam = new SwaggerParams(); + swaggerParam.setIn(typeMap.get(type)); // 利用 map,根据 request 的 key 设置对应的参数类型 + swaggerParam.setDescription(param.optString("description")); + swaggerParam.setName(param.optString("key")); + swaggerParam.setEnable(param.optBoolean(PropertyConstant.ENABLE)); + swaggerParam.setValue(param.optString("value")); + JSONObject schema = new JSONObject(); + schema.put(PropertyConstant.TYPE, PropertyConstant.STRING); + swaggerParam.setSchema(JSONUtil.parseObjectNode(schema.toString())); + paramsList.add(JSONUtil.parseObject(JSON.toJSONString(swaggerParam))); + } + } + } + return paramsList; + } + + private JSONObject buildResponseBody(JSONArray response, List schemas) { + if (response.length() == 0) { + return new JSONObject(); + } + JSONObject responseBody = new JSONObject(); + for (int i = 0; i < response.length(); i++) { + JSONObject responseJSONObject = response.getJSONObject(i); + JSONObject headers = new JSONObject(); + JSONArray headValueList = responseJSONObject.optJSONArray("headers"); + if (headValueList != null) { + for (Object item : headValueList) { + if (item instanceof JSONObject && ((JSONObject) item).optString("key") != null) { + JSONObject head = new JSONObject(), headSchema = new JSONObject(); + head.put("description", ((JSONObject) item).optString("description")); + head.put("example", ((JSONObject) item).optString("value")); + headSchema.put(PropertyConstant.TYPE, PropertyConstant.STRING); + head.put("schema", headSchema); + headers.put(((JSONObject) item).optString("key"), head); + } + } + } + String statusCode = responseJSONObject.optString("statusCode"); + if (StringUtils.isNotBlank(statusCode)) { + JSONObject statusCodeInfo = new JSONObject(); + statusCodeInfo.put("headers", headers); + statusCodeInfo.put("content", buildContent(responseJSONObject, schemas)); + statusCodeInfo.put("description", StringUtils.EMPTY); + if (StringUtils.isNotBlank(responseJSONObject.optString("value"))) { + statusCodeInfo.put("description", responseJSONObject.optString("value")); + } + if (StringUtils.isNotBlank(responseJSONObject.optString("name"))) { + responseBody.put(responseJSONObject.optString("name"), statusCodeInfo); + } + } + } + return responseBody; + } + + private JSONObject buildRequestBody(JSONObject request, List schemas) { + JSONObject requestBody = new JSONObject(); + requestBody.put("content", buildContent(request, schemas)); + return requestBody; + } + + private JSONObject buildContent(JSONObject respOrReq, List schemas) { + Hashtable typeMap = new Hashtable() {{ + put(Body.BodyType.XML.name(), org.springframework.http.MediaType.APPLICATION_XML_VALUE); + put(Body.BodyType.JSON.name(), org.springframework.http.MediaType.APPLICATION_JSON_VALUE); + put(Body.BodyType.RAW.name(), "application/urlencoded"); + put(Body.BodyType.BINARY.name(), org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE); + put(Body.BodyType.FORM_DATA.name(), org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE); + put(Body.BodyType.WWW_FORM.name(), org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE); + }}; + Object bodyInfo = null; + Object jsonInfo = null; + JSONObject body = respOrReq.optJSONObject("body"); + + if (body != null) { // 将请求体转换成相应的格式导出 + String bodyType = body.optString(PropertyConstant.BODYTYPE); + if (StringUtils.isNotBlank(bodyType) && bodyType.equalsIgnoreCase(Body.BodyType.JSON.name())) { + try { + // json + String jsonValue = body.optJSONObject("jsonBody").optString("jsonValue"); + if (StringUtils.isNotBlank(jsonValue)) { + jsonInfo = buildJson(jsonValue); + } + // jsonSchema + String jsonSchema = body.optJSONObject("jsonBody").optString("jsonSchema"); + if (StringUtils.isNotBlank(jsonSchema)) { + JSONObject jsonSchemaObject = JSONUtil.parseObject(jsonSchema); + bodyInfo = buildJsonSchema(jsonSchemaObject); + } + } catch (Exception e1) { // 若请求体 json 不合法,则忽略错误,原样字符串导出/导入 + bodyInfo = new JSONObject(); + ((JSONObject) bodyInfo).put(PropertyConstant.TYPE, PropertyConstant.STRING); + if (body != null && body.optString("rawBody") != null) { + ((JSONObject) bodyInfo).put("example", body.optString("rawBody")); + } + } + } else if (bodyType != null && bodyType.equalsIgnoreCase(Body.BodyType.RAW.name())) { + bodyInfo = new JSONObject(); + ((JSONObject) bodyInfo).put(PropertyConstant.TYPE, PropertyConstant.STRING); + if (body != null && body.optString("rawBody") != null) { + ((JSONObject) bodyInfo).put("example", body.optString("rawBody")); + } + } else if (bodyType != null && bodyType.equalsIgnoreCase(Body.BodyType.XML.name())) { + String xmlText = body.optString("xmlBody"); + JSONObject xmlObject = JSONUtil.parseObject(xmlText); + xmlText = xmlObject.optString("value"); + String xml = XMLUtil.delXmlHeader(xmlText); + int startIndex = xml.indexOf("<", 0); + int endIndex = xml.indexOf(">", 0); + if (endIndex > startIndex + 1) { + String substring = xml.substring(startIndex + 1, endIndex); + bodyInfo = buildRefSchema(substring); + } + JSONObject xmlToJson = XMLUtil.xmlConvertJson(xmlText); + JSONObject jsonObject = buildRequestBodyXmlSchema(xmlToJson); + if (schemas == null) { + schemas = new LinkedList<>(); + } + schemas.add(jsonObject); + } else if (bodyType != null && (bodyType.equalsIgnoreCase(Body.BodyType.WWW_FORM.name()) || bodyType.equalsIgnoreCase(Body.BodyType.FORM_DATA.name()))) { // key-value 类格式 + String wwwFormBody = body.optString("wwwFormBody"); + JSONObject wwwFormObject = JSONUtil.parseObject(wwwFormBody); + JSONObject formData = getformDataProperties(wwwFormObject.optJSONArray("formValues")); + bodyInfo = buildFormDataSchema(formData); + } else if (bodyType != null && bodyType.equalsIgnoreCase(Body.BodyType.BINARY.name())) { + bodyInfo = buildBinary(); + } + } + + String type = null; + if (respOrReq.optJSONObject("body") != null) { + type = respOrReq.optJSONObject("body").optString(PropertyConstant.BODYTYPE); + } + JSONObject content = new JSONObject(); + Object schema = bodyInfo; // 请求体部分 + JSONObject typeName = new JSONObject(); + if (schema != null) { + typeName.put("schema", schema); + } + if (jsonInfo != null) { + typeName.put("example", jsonInfo); + } + if (StringUtils.isNotBlank(type) && typeMap.containsKey(type)) { + content.put(typeMap.get(type), typeName); + } + return content; + } + + private JSONObject buildBinary() { + JSONObject parsedParam = new JSONObject(); + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.STRING); + parsedParam.put("format", "binary"); + return parsedParam; + } + + /** + * requestBody 中jsonSchema + * + * @param jsonSchemaObject + * @return + */ + private JSONObject buildJsonSchema(JSONObject jsonSchemaObject) { + JSONObject parsedParam = new JSONObject(); + String type = jsonSchemaObject.optString(PropertyConstant.TYPE); + if (StringUtils.isNotBlank(type)) { + if (StringUtils.equals(type, PropertyConstant.OBJECT)) { + parsedParam = jsonSchemaObject; + } else if (StringUtils.equals(type, PropertyConstant.ARRAY)) { + JSONArray items = jsonSchemaObject.optJSONArray(PropertyConstant.ITEMS); + JSONObject itemProperties = new JSONObject(); + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.ARRAY); + if (items != null) { + JSONObject itemsObject = new JSONObject(); + if (items.length() > 0) { + items.forEach(item -> { + if (item instanceof JSONObject) { + JSONObject itemJson = buildJsonSchema((JSONObject) item); + if (itemJson != null) { + Set keys = itemJson.keySet(); + for (String key : keys) { + itemProperties.put(key, itemJson.get(key)); + } + } + } + }); + } + itemsObject.put(PropertyConstant.PROPERTIES, itemProperties); + parsedParam.put(PropertyConstant.ITEMS, itemsObject.optJSONObject(PropertyConstant.PROPERTIES)); + } else { + parsedParam.put(PropertyConstant.ITEMS, new JSONObject()); + } + } else if (StringUtils.equals(type, PropertyConstant.INTEGER)) { + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.INTEGER); + parsedParam.put("format", "int64"); + setCommonJsonSchemaParam(parsedParam, jsonSchemaObject); + + } else if (StringUtils.equals(type, PropertyConstant.BOOLEAN)) { + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.BOOLEAN); + setCommonJsonSchemaParam(parsedParam, jsonSchemaObject); + } else if (StringUtils.equals(type, PropertyConstant.NUMBER)) { + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.NUMBER); + setCommonJsonSchemaParam(parsedParam, jsonSchemaObject); + } else { + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.STRING); + setCommonJsonSchemaParam(parsedParam, jsonSchemaObject); + } + + } + return parsedParam; + } + + /** + * requestBody 中json + * + * @param jsonValue + * @return + */ + private JSONObject buildJson(String jsonValue) { + JSONObject jsonObject = JSONUtil.parseObject(jsonValue); + return jsonObject; + } + + private JSONObject getformDataProperties(JSONArray requestBody) { + JSONObject result = new JSONObject(); + for (Object item : requestBody) { + if (item instanceof JSONObject) { + String name = ((JSONObject) item).optString("key"); + if (name != null) { + result.put(name, item); + } + } + } + return result; + } + + private static JSONObject buildRequestBodyXmlSchema(JSONObject requestBody) { + if (requestBody == null) return null; + JSONObject schema = new JSONObject(); + for (String key : requestBody.keySet()) { + Object param = requestBody.get(key); + JSONObject parsedParam = new JSONObject(); + if (param instanceof String) { + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.STRING); + parsedParam.put("example", param == null ? StringUtils.EMPTY : param); + } else if (param instanceof Integer) { + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.INTEGER); + parsedParam.put("format", "int64"); + parsedParam.put("example", param); + } else if (param instanceof JSONObject) { + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.OBJECT); + Object attribute = ((JSONObject) param).opt("attribute"); + //build properties + JSONObject paramObject = buildRequestBodyXmlSchema((JSONObject) param); + if (attribute != null && attribute instanceof JSONArray) { + JSONObject jsonObject = buildXmlProperties(((JSONArray) attribute).getJSONObject(0)); + paramObject.remove("attribute"); + for (String paramKey : paramObject.keySet()) { + Object paramChild = paramObject.get(paramKey); + if (paramChild instanceof String) { + JSONObject one = new JSONObject(); + one.put(PropertyConstant.TYPE, PropertyConstant.OBJECT); + one.put("properties", jsonObject); + paramObject.remove("example"); + paramObject.remove(paramKey); + paramObject.put(paramKey, one); + } + if (paramChild instanceof JSONObject) { + Object properties = ((JSONObject) paramChild).opt("properties"); + if (properties != null) { + for (String aa : jsonObject.keySet()) { + Object value = jsonObject.get(aa); + if (((JSONObject) properties).opt(aa) == null) { + ((JSONObject) properties).put(aa, value); + } + } + } else { + ((JSONObject) paramChild).put("properties", jsonObject); + } + if (((JSONObject) paramChild).opt("type") == "string") { + ((JSONObject) paramChild).put("type", "object"); + ((JSONObject) paramChild).remove("example"); + } + } + } + } + parsedParam.put("properties", paramObject); + if (StringUtils.isNotBlank(requestBody.optString("description"))) { + parsedParam.put("description", requestBody.optString("description")); + } + } else if (param instanceof Boolean) { + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.BOOLEAN); + parsedParam.put("example", param); + } else if (param instanceof java.math.BigDecimal) { // double 类型会被 fastJson 转换为 BigDecimal + parsedParam.put(PropertyConstant.TYPE, "double"); + parsedParam.put("example", param); + } else { // JSONOArray + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.OBJECT); + + if (param == null) { + param = new JSONArray(); + } + JSONObject jsonObjects = new JSONObject(); + if (((JSONArray) param).length() > 0) { + ((JSONArray) param).forEach(t -> { + JSONObject item = buildRequestBodyXmlSchema((JSONObject) t); + for (String s : item.keySet()) { + jsonObjects.put(s, item.get(s)); + } + }); + } + parsedParam.put(PropertyConstant.PROPERTIES, jsonObjects); + } + schema.put(key, parsedParam); + } + + return schema; + } + + private static JSONObject buildXmlProperties(JSONObject kvs) { + JSONObject properties = new JSONObject(); + for (String key : kvs.keySet()) { + JSONObject property = new JSONObject(); + Object param = kvs.opt(key); + if (param instanceof String) { + property.put(PropertyConstant.TYPE, PropertyConstant.STRING); + property.put("example", param == null ? StringUtils.EMPTY : param); + } + if (param instanceof JSONObject) { + JSONObject obj = ((JSONObject) param); + property.put(PropertyConstant.TYPE, StringUtils.isNotEmpty(obj.optString(PropertyConstant.TYPE)) ? obj.optString(PropertyConstant.TYPE) : PropertyConstant.STRING); + String value = obj.optString("value"); + if (StringUtils.isBlank(value)) { + JSONObject mock = obj.optJSONObject(PropertyConstant.MOCK); + if (mock != null) { + Object mockValue = mock.get(PropertyConstant.MOCK); + property.put("example", mockValue); + } else { + property.put("example", value); + } + } else { + property.put("example", value); + } + } + JSONObject xml = new JSONObject(); + xml.put("attribute", true); + property.put("xml", xml); + properties.put(key, property); + } + + return properties; + } + + private Object buildRefSchema(String substring) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("$ref", "#/components/schemas/" + substring); + return jsonObject; + } + + private JSONObject buildRequestBodyJsonInfo(JSONObject requestBody) { + if (requestBody == null) return null; + JSONObject schema = new JSONObject(); + schema.put(PropertyConstant.TYPE, PropertyConstant.OBJECT); + JSONObject properties = buildSchema(requestBody); + schema.put(PropertyConstant.PROPERTIES, properties); + return schema; + } + + private JSONObject buildSchema(JSONObject requestBody) { + JSONObject schema = new JSONObject(); + for (String key : requestBody.keySet()) { + Object param = requestBody.get(key); + JSONObject parsedParam = new JSONObject(); + if (param instanceof String) { + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.STRING); + parsedParam.put("example", param == null ? StringUtils.EMPTY : param); + } else if (param instanceof Integer) { + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.INTEGER); + parsedParam.put("format", "int64"); + parsedParam.put("example", param); + } else if (param instanceof JSONObject) { + parsedParam = buildRequestBodyJsonInfo((JSONObject) param); + } else if (param instanceof Boolean) { + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.BOOLEAN); + parsedParam.put("example", param); + } else if (param instanceof java.math.BigDecimal) { // double 类型会被 fastJson 转换为 BigDecimal + parsedParam.put(PropertyConstant.TYPE, "double"); + parsedParam.put("example", param); + } else { // JSONOArray + parsedParam.put(PropertyConstant.TYPE, PropertyConstant.ARRAY); + JSONObject item = new JSONObject(); + if (param == null) { + param = new JSONArray(); + } + if (((JSONArray) param).length() > 0) { + if (((JSONArray) param).get(0) instanceof JSONObject) { /// + item = buildRequestBodyJsonInfo((JSONObject) ((JSONArray) param).get(0)); + } + } + parsedParam.put(PropertyConstant.ITEMS, item); + } + schema.put(key, parsedParam); + } + return schema; + } + + + private JSONObject buildFormDataSchema(JSONObject kvs) { + JSONObject schema = new JSONObject(); + JSONObject properties = new JSONObject(); + for (String key : kvs.keySet()) { + JSONObject property = new JSONObject(); + JSONObject obj = ((JSONObject) kvs.get(key)); + property.put(PropertyConstant.TYPE, StringUtils.isNotEmpty(obj.optString(PropertyConstant.PARAMTYPE)) ? obj.optString(PropertyConstant.PARAMTYPE) : PropertyConstant.STRING); + String value = obj.optString("value"); + if (StringUtils.isBlank(value)) { + JSONObject mock = obj.optJSONObject(PropertyConstant.MOCK); + if (mock != null && StringUtils.isNotBlank(mock.optString("mock"))) { + Object mockValue = mock.get(PropertyConstant.MOCK); + property.put("example", mockValue); + } else { + property.put("example", value); + } + } else { + property.put("example", value); + } + property.put("description", obj.optString("description")); + property.put(PropertyConstant.REQUIRED, obj.optString(PropertyConstant.REQUIRED)); + if (obj.optJSONObject(PropertyConstant.PROPERTIES) != null) { + JSONObject childProperties = buildFormDataSchema(obj.optJSONObject(PropertyConstant.PROPERTIES)); + property.put(PropertyConstant.PROPERTIES, childProperties.optJSONObject(PropertyConstant.PROPERTIES)); + } else { + JSONObject childProperties = buildJsonSchema(obj); + if (StringUtils.equalsIgnoreCase(obj.optString(PropertyConstant.PARAMTYPE), PropertyConstant.ARRAY)) { + if (childProperties.optJSONObject(PropertyConstant.ITEMS) != null) { + property.put(PropertyConstant.ITEMS, childProperties.optJSONObject(PropertyConstant.ITEMS)); + } + } else { + if (childProperties.optJSONObject(PropertyConstant.PROPERTIES) != null) { + property.put(PropertyConstant.PROPERTIES, childProperties.optJSONObject(PropertyConstant.PROPERTIES)); + } + } + } + properties.put(key, property); + } + schema.put(PropertyConstant.PROPERTIES, properties); + return schema; + } + + public void setCommonJsonSchemaParam(JSONObject parsedParam, JSONObject requestBody) { + if (StringUtils.isNotBlank(requestBody.optString("description"))) { + parsedParam.put("description", requestBody.optString("description")); + } + Object jsonSchemaValue = getJsonSchemaValue(requestBody); + if (jsonSchemaValue != null) { + parsedParam.put("example", jsonSchemaValue); + } + } + + public Object getJsonSchemaValue(JSONObject item) { + JSONObject mock = item.optJSONObject(PropertyConstant.MOCK); + if (mock != null) { + if (StringUtils.isNotBlank(mock.optString("mock"))) { + Object value = mock.get(PropertyConstant.MOCK); + return value; + } + } + return null; + } +} 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 new file mode 100644 index 0000000000..a91c376fb7 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/service/definition/ApiDefinitionExportService.java @@ -0,0 +1,51 @@ +package io.metersphere.api.service.definition; + +import io.metersphere.api.dto.definition.ApiDefinitionBatchRequest; +import io.metersphere.api.dto.definition.ApiDefinitionWithBlob; +import io.metersphere.api.dto.export.ApiExportResponse; +import io.metersphere.api.mapper.ExtApiDefinitionMapper; +import io.metersphere.api.parser.api.Swagger3ExportParser; +import io.metersphere.project.domain.Project; +import io.metersphere.project.mapper.ProjectMapper; +import io.metersphere.sdk.exception.MSException; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.List; + + +/** + * @author wx + */ +@Service +public class ApiDefinitionExportService { + + @Resource + private ApiDefinitionService apiDefinitionService; + @Resource + private ExtApiDefinitionMapper extApiDefinitionMapper; + @Resource + private ProjectMapper projectMapper; + + public ApiExportResponse export(ApiDefinitionBatchRequest request, String type, String userId) { + List ids = apiDefinitionService.getBatchApiIds(request, request.getProjectId(), request.getProtocols(), false, userId); + List list = extApiDefinitionMapper.selectApiDefinitionWithBlob(ids); + switch (type) { + case "swagger": + return exportSwagger(request, list); + default: + return new ApiExportResponse(); + } + + } + + private ApiExportResponse exportSwagger(ApiDefinitionBatchRequest request, List list) { + Project project = projectMapper.selectByPrimaryKey(request.getProjectId()); + Swagger3ExportParser swagger3Parser = new Swagger3ExportParser(); + try { + return swagger3Parser.parse(list, project); + } catch (Exception e) { + throw new MSException(e); + } + } +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/utils/JSONUtil.java b/backend/services/api-test/src/main/java/io/metersphere/api/utils/JSONUtil.java new file mode 100644 index 0000000000..5fa75a3f31 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/utils/JSONUtil.java @@ -0,0 +1,80 @@ +package io.metersphere.api.utils; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.StreamReadConstraints; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.util.JSON; +import io.metersphere.sdk.util.LogUtils; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.List; +import java.util.Map; + +/** + * @author wx + */ +public class JSONUtil { + private static final ObjectMapper objectMapper = JsonMapper.builder() + .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS) + .build(); + private static final TypeFactory typeFactory = objectMapper.getTypeFactory(); + public static final int DEFAULT_MAX_STRING_LEN = Integer.MAX_VALUE; + + static { + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + // 自动检测所有类的全部属性 + objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + // 如果一个对象中没有任何的属性,那么在序列化的时候就会报错 + objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + // 使用BigDecimal来序列化 + objectMapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true); + // 设置JSON处理字符长度限制 + objectMapper.getFactory() + .setStreamReadConstraints(StreamReadConstraints.builder().maxStringLength(DEFAULT_MAX_STRING_LEN).build()); + // 处理时间格式 + objectMapper.registerModule(new JavaTimeModule()); + } + + + public static JSONObject parseObject(String value) { + try { + if (StringUtils.isEmpty(value)) { + throw new MSException("value is null"); + } + Map map = JSON.parseObject(value, Map.class); + return new JSONObject(map); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static JSONArray parseArray(String text) { + List list = JSON.parseObject(text, List.class); + return new JSONArray(list); + } + + public static ObjectNode parseObjectNode(String text) { + try { + return (ObjectNode) objectMapper.readTree(text); + } catch (Exception e) { + LogUtils.error(e); + } + return objectMapper.createObjectNode(); + } + + public static ObjectNode createObj() { + return objectMapper.createObjectNode(); + } +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/utils/XMLUtil.java b/backend/services/api-test/src/main/java/io/metersphere/api/utils/XMLUtil.java new file mode 100644 index 0000000000..3115e90729 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/utils/XMLUtil.java @@ -0,0 +1,97 @@ +package io.metersphere.api.utils; + +import io.metersphere.sdk.util.LogUtils; +import io.metersphere.sdk.util.XMLUtils; +import org.apache.commons.lang3.StringUtils; +import org.dom4j.Attribute; +import org.dom4j.Document; +import org.dom4j.Element; +import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author wx + */ +public class XMLUtil { + + @Nullable + public static String delXmlHeader(String xml) { + int begin = xml.indexOf("?>"); + if (begin != -1) { + if (begin + 2 >= xml.length()) { + return null; + } + xml = xml.substring(begin + 2); + } // 若存在,则去除 + String rgex = ">"; + Pattern pattern = Pattern.compile(rgex); + Matcher m = pattern.matcher(xml); + xml = m.replaceAll("> "); + rgex = "\\s* listElement = node.elements();// 所有一级子节点的list + if (!listElement.isEmpty()) { + List list = new LinkedList<>(); + for (Element e : listElement) {// 遍历所有一级子节点 + JSONObject jsonObject = getJsonObjectByDC(e); + //加xml标签上的属性 eg: RB + //这里添加 length scale type + if (!e.attributes().isEmpty()) { + JSONObject attributeJson = new JSONObject(); + for (Attribute attribute : e.attributes()) { + attributeJson.put(attribute.getName(), attribute.getValue()); + } + jsonObject.append("attribute", attributeJson); + } + list.add(jsonObject); + } + if (list.size() == 1) { + result.put(node.getName(), list.get(0)); + } else { + result.put(node.getName(), list); + } + } else { + if (!StringUtils.isAllBlank(node.getName(), node.getText())) { + result.put(node.getName(), node.getText()); + } + } + return result; + } +} 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 4d8e8d5ac0..6f0fe1a27c 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 @@ -117,6 +117,8 @@ public class ApiDefinitionControllerTests extends BaseTest { private static final String ALL_API = "api_definition_module.api.all"; private static final String UNPLANNED_API = "api_unplanned_request"; + + private static final String EXPORT = "/export/"; private static ApiDefinition apiDefinition; @Resource @@ -1854,4 +1856,21 @@ public class ApiDefinitionControllerTests extends BaseTest { Assertions.assertNotNull(pageData); } + @Test + @Order(104) + public void testExport() throws Exception { + ApiDefinitionBatchRequest request = new ApiDefinitionBatchRequest(); + request.setProjectId(DEFAULT_PROJECT_ID); + request.setProtocols(List.of("HTTP")); + request.setSelectAll(false); + request.setSelectIds(List.of("1001")); + MvcResult mvcResult = this.requestPostWithOkAndReturn(EXPORT + "swagger", request); + String returnData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + ResultHolder resultHolder = JSON.parseObject(returnData, ResultHolder.class); + // 返回请求正常 + Assertions.assertNotNull(resultHolder); + Pager pageData = JSON.parseObject(JSON.toJSONString(resultHolder.getData()), Pager.class); + Assertions.assertNotNull(pageData); + } + } diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/constants/PropertyConstant.java b/backend/services/project-management/src/main/java/io/metersphere/project/constants/PropertyConstant.java index 4b91272486..287b384408 100644 --- a/backend/services/project-management/src/main/java/io/metersphere/project/constants/PropertyConstant.java +++ b/backend/services/project-management/src/main/java/io/metersphere/project/constants/PropertyConstant.java @@ -19,5 +19,8 @@ public class PropertyConstant { public final static String ITEMS = "items"; public final static String PROPERTIES = "properties"; public final static String ENABLE = "enable"; + public final static String MOCK = "mock"; + public final static String BODYTYPE = "bodyType"; + public final static String PARAMTYPE = "paramtype"; } diff --git a/frontend/src/api/modules/api-test/management.ts b/frontend/src/api/modules/api-test/management.ts index 7edc5efe47..c2af6bde7e 100644 --- a/frontend/src/api/modules/api-test/management.ts +++ b/frontend/src/api/modules/api-test/management.ts @@ -36,6 +36,7 @@ import { DeleteRecycleApiUrl, DeleteRecycleCaseUrl, ExecuteCaseUrl, + ExportDefinitionUrl, GetCaseDetailUrl, GetCaseReportByIdUrl, GetCaseReportDetailUrl, @@ -101,6 +102,7 @@ import { ApiCasePageParams, ApiDefinitionBatchDeleteParams, ApiDefinitionBatchMoveParams, + ApiDefinitionBatchParams, ApiDefinitionBatchUpdateParams, ApiDefinitionCreateParams, ApiDefinitionDeleteParams, @@ -556,3 +558,8 @@ export function getReportById(id: string) { export function getCaseReportDetail(reportId: string, stepId: string) { return MSR.get({ url: `${GetCaseReportDetailUrl + reportId}/${stepId}` }); } + +// 导出定义 +export function exportApiDefinition(data: ApiDefinitionBatchParams, type: string) { + return MSR.post({ url: `${ExportDefinitionUrl}/${type}`, data }); +} diff --git a/frontend/src/api/requrls/api-test/management.ts b/frontend/src/api/requrls/api-test/management.ts index 39fa65afa0..70ce2a0795 100644 --- a/frontend/src/api/requrls/api-test/management.ts +++ b/frontend/src/api/requrls/api-test/management.ts @@ -18,6 +18,7 @@ export const TransferFileModuleOptionUrl = '/api/definition/transfer/options'; / export const UploadTempFileUrl = '/api/definition/upload/temp/file'; // 临时文件上传 export const DeleteDefinitionUrl = '/api/definition/delete-to-gc'; // 删除接口定义 export const ImportDefinitionUrl = '/api/definition/import'; // 导入接口定义 +export const ExportDefinitionUrl = '/api/definition/export'; // 导入接口定义 export const SortDefinitionUrl = '/api/definition/edit/pos'; // 接口定义拖拽 export const CopyDefinitionUrl = '/api/definition/copy'; // 复制接口定义 export const BatchUpdateDefinitionUrl = '/api/definition/batch-update'; // 批量更新接口定义 diff --git a/frontend/src/views/api-test/management/components/management/api/apiTable.vue b/frontend/src/views/api-test/management/components/management/api/apiTable.vue index 7abcb55732..3101d60be7 100644 --- a/frontend/src/views/api-test/management/components/management/api/apiTable.vue +++ b/frontend/src/views/api-test/management/components/management/api/apiTable.vue @@ -276,6 +276,7 @@ batchMoveDefinition, batchUpdateDefinition, deleteDefinition, + exportApiDefinition, getDefinitionPage, sortDefinition, updateDefinition, @@ -284,7 +285,7 @@ import useModal from '@/hooks/useModal'; import useTableStore from '@/hooks/useTableStore'; import useAppStore from '@/store/modules/app'; - import { characterLimit, operationWidth } from '@/utils'; + import { characterLimit, downloadByteFile, operationWidth } from '@/utils'; import { hasAnyPermission } from '@/utils/permission'; import { ProtocolItem } from '@/models/apiTest/common'; @@ -555,10 +556,16 @@ ); const batchActions = { baseAction: [ - // { - // label: 'common.export', - // eventTag: 'export', - // }, + { + label: 'common.export', + eventTag: 'export', + children: [ + { + label: 'apiTestManagement.swagger.export', + eventTag: 'exportSwagger', + }, + ], + }, { label: 'common.edit', eventTag: 'edit', @@ -906,6 +913,28 @@ selectedModuleKeys.value = keys; } + /** + * 导出接口 + */ + async function exportApi(type: string, record?: ApiDefinitionDetail, params?: BatchActionQueryParams) { + const result = await exportApiDefinition( + { + selectIds: tableSelected.value as string[], + selectAll: !!params?.selectAll, + excludeIds: params?.excludeIds || [], + condition: { + keyword: keyword.value, + filter: propsRes.value.filter, + }, + projectId: appStore.currentProjectId, + moduleIds: await getModuleIds(), + protocols: props.selectedProtocols, + }, + type + ); + downloadByteFile(new Blob([JSON.stringify(result)]), 'Swagger_Api_Case.json'); + } + /** * 处理表格选中后批量操作 * @param event 批量操作事件对象 @@ -914,6 +943,9 @@ tableSelected.value = params?.selectedIds || []; batchParams.value = params; switch (event.eventTag) { + case 'exportSwagger': + exportApi('swagger', undefined, params); + break; case 'delete': deleteApi(undefined, true, params); break; diff --git a/frontend/src/views/api-test/management/locale/en-US.ts b/frontend/src/views/api-test/management/locale/en-US.ts index 77963fd50c..245446b111 100644 --- a/frontend/src/views/api-test/management/locale/en-US.ts +++ b/frontend/src/views/api-test/management/locale/en-US.ts @@ -80,6 +80,7 @@ export default { 'apiTestManagement.moreSetting': 'More Settings', 'apiTestManagement.importType': 'Import Type', 'apiTestManagement.urlImport': 'URL Import', + 'apiTestManagement.swagger.export': 'Export Swagger3.0', 'apiTestManagement.syncImportCase': 'Sync Import API Cases', 'apiTestManagement.syncUpdateDirectory': 'Sync Update API Directory', 'apiTestManagement.importSwaggerFileTip1': 'Supports Swagger 3.0 version JSON files,', diff --git a/frontend/src/views/api-test/management/locale/zh-CN.ts b/frontend/src/views/api-test/management/locale/zh-CN.ts index 0fe1fb32cd..a79262c664 100644 --- a/frontend/src/views/api-test/management/locale/zh-CN.ts +++ b/frontend/src/views/api-test/management/locale/zh-CN.ts @@ -75,6 +75,7 @@ export default { 'apiTestManagement.moreSetting': '更多设置', 'apiTestManagement.importType': '导入方式', 'apiTestManagement.urlImport': 'URL 导入', + 'apiTestManagement.swagger.export': '导出 Swagger3.0 格式', 'apiTestManagement.syncImportCase': '同步导入接口用例', 'apiTestManagement.syncUpdateDirectory': '同步更新接口所在目录', 'apiTestManagement.importSwaggerFileTip1': '支持 Swagger 3.0 版本的 json 文件,',