From 6b921d4aa350760c89bb887e9bbadb7ebbfe4548 Mon Sep 17 00:00:00 2001 From: guoyuqi Date: Tue, 6 Aug 2024 16:42:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=94=A8=E4=BE=8B=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=81=9C=E6=AD=A2=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../functional/domain/ExportTask.java | 126 +++ .../functional/domain/ExportTaskExample.java | 810 ++++++++++++++++++ .../functional/mapper/ExportTaskMapper.java | 34 + .../functional/mapper/ExportTaskMapper.xml | 328 +++++++ .../migration/3.2.0/ddl/V3.2.0_2__ga_ddl.sql | 23 + .../sdk/constants/KafkaTopicConstants.java | 1 + .../io/metersphere/sdk/util/MsFileUtils.java | 39 + .../io/metersphere/sdk/util/XMLUtils.java | 127 ++- .../src/main/resources/i18n/case.properties | 4 + .../main/resources/i18n/case_en_US.properties | 8 +- .../main/resources/i18n/case_zh_CN.properties | 7 +- .../main/resources/i18n/case_zh_TW.properties | 10 +- .../controller/FunctionalCaseController.java | 10 +- .../service/FunctionalCaseFileService.java | 58 +- .../functional/xmind/parser/XMindLegacy.java | 130 +++ .../functional/xmind/parser/XMindParser.java | 125 +++ .../functional/xmind/parser/XMindZen.java | 66 ++ .../functional/xmind/parser/ZipUtils.java | 84 ++ .../functional/xmind/pojo/Attached.java | 22 + .../functional/xmind/pojo/Children.java | 12 + .../functional/xmind/pojo/Comments.java | 12 + .../functional/xmind/pojo/JsonRootBean.java | 12 + .../functional/xmind/pojo/Notes.java | 10 + .../functional/xmind/pojo/RootTopic.java | 16 + .../FunctionalCaseControllerTests.java | 7 + .../system/constants/ExportConstants.java | 12 + .../system/manager/ExportTaskManager.java | 94 ++ 27 files changed, 2174 insertions(+), 13 deletions(-) create mode 100644 backend/framework/domain/src/main/java/io/metersphere/functional/domain/ExportTask.java create mode 100644 backend/framework/domain/src/main/java/io/metersphere/functional/domain/ExportTaskExample.java create mode 100644 backend/framework/domain/src/main/java/io/metersphere/functional/mapper/ExportTaskMapper.java create mode 100644 backend/framework/domain/src/main/java/io/metersphere/functional/mapper/ExportTaskMapper.xml create mode 100644 backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/XMindLegacy.java create mode 100644 backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/XMindParser.java create mode 100644 backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/XMindZen.java create mode 100644 backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/ZipUtils.java create mode 100644 backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Attached.java create mode 100644 backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Children.java create mode 100644 backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Comments.java create mode 100644 backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/JsonRootBean.java create mode 100644 backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Notes.java create mode 100644 backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/RootTopic.java create mode 100644 backend/services/system-setting/src/main/java/io/metersphere/system/constants/ExportConstants.java create mode 100644 backend/services/system-setting/src/main/java/io/metersphere/system/manager/ExportTaskManager.java diff --git a/backend/framework/domain/src/main/java/io/metersphere/functional/domain/ExportTask.java b/backend/framework/domain/src/main/java/io/metersphere/functional/domain/ExportTask.java new file mode 100644 index 0000000000..756ad134d1 --- /dev/null +++ b/backend/framework/domain/src/main/java/io/metersphere/functional/domain/ExportTask.java @@ -0,0 +1,126 @@ +package io.metersphere.functional.domain; + +import io.metersphere.validation.groups.*; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.*; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import lombok.Data; + +@Data +public class ExportTask implements Serializable { + @Schema(description = "任务唯一ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{export_task.id.not_blank}", groups = {Updated.class}) + @Size(min = 1, max = 50, message = "{export_task.id.length_range}", groups = {Created.class, Updated.class}) + private String id; + + @Schema(description = "名称") + private String name; + + @Schema(description = "资源类型", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{export_task.type.not_blank}", groups = {Created.class}) + @Size(min = 1, max = 50, message = "{export_task.type.length_range}", groups = {Created.class, Updated.class}) + private String type; + + @Schema(description = "文件id") + private String fileid; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{export_task.state.not_blank}", groups = {Created.class}) + @Size(min = 1, max = 50, message = "{export_task.state.length_range}", groups = {Created.class, Updated.class}) + private String state; + + @Schema(description = "创建人") + private String createUser; + + @Schema(description = "创建时间") + private Long createTime; + + @Schema(description = "创建人") + private String updateUser; + + @Schema(description = "创建时间") + private Long updateTime; + + private static final long serialVersionUID = 1L; + + public enum Column { + id("id", "id", "VARCHAR", false), + name("name", "name", "VARCHAR", true), + type("type", "type", "VARCHAR", true), + fileid("fileId", "fileid", "VARCHAR", false), + state("state", "state", "VARCHAR", true), + createUser("create_user", "createUser", "VARCHAR", false), + createTime("create_time", "createTime", "BIGINT", false), + updateUser("update_user", "updateUser", "VARCHAR", false), + updateTime("update_time", "updateTime", "BIGINT", false); + + private static final String BEGINNING_DELIMITER = "`"; + + private static final String ENDING_DELIMITER = "`"; + + private final String column; + + private final boolean isColumnNameDelimited; + + private final String javaProperty; + + private final String jdbcType; + + public String value() { + return this.column; + } + + public String getValue() { + return this.column; + } + + public String getJavaProperty() { + return this.javaProperty; + } + + public String getJdbcType() { + return this.jdbcType; + } + + Column(String column, String javaProperty, String jdbcType, boolean isColumnNameDelimited) { + this.column = column; + this.javaProperty = javaProperty; + this.jdbcType = jdbcType; + this.isColumnNameDelimited = isColumnNameDelimited; + } + + public String desc() { + return this.getEscapedColumnName() + " DESC"; + } + + public String asc() { + return this.getEscapedColumnName() + " ASC"; + } + + public static Column[] excludes(Column ... excludes) { + ArrayList columns = new ArrayList<>(Arrays.asList(Column.values())); + if (excludes != null && excludes.length > 0) { + columns.removeAll(new ArrayList<>(Arrays.asList(excludes))); + } + return columns.toArray(new Column[]{}); + } + + public static Column[] all() { + return Column.values(); + } + + public String getEscapedColumnName() { + if (this.isColumnNameDelimited) { + return new StringBuilder().append(BEGINNING_DELIMITER).append(this.column).append(ENDING_DELIMITER).toString(); + } else { + return this.column; + } + } + + public String getAliasedEscapedColumnName() { + return this.getEscapedColumnName(); + } + } +} \ No newline at end of file diff --git a/backend/framework/domain/src/main/java/io/metersphere/functional/domain/ExportTaskExample.java b/backend/framework/domain/src/main/java/io/metersphere/functional/domain/ExportTaskExample.java new file mode 100644 index 0000000000..343c2f5791 --- /dev/null +++ b/backend/framework/domain/src/main/java/io/metersphere/functional/domain/ExportTaskExample.java @@ -0,0 +1,810 @@ +package io.metersphere.functional.domain; + +import java.util.ArrayList; +import java.util.List; + +public class ExportTaskExample { + protected String orderByClause; + + protected boolean distinct; + + protected List oredCriteria; + + public ExportTaskExample() { + oredCriteria = new ArrayList(); + } + + public void setOrderByClause(String orderByClause) { + this.orderByClause = orderByClause; + } + + public String getOrderByClause() { + return orderByClause; + } + + public void setDistinct(boolean distinct) { + this.distinct = distinct; + } + + public boolean isDistinct() { + return distinct; + } + + public List getOredCriteria() { + return oredCriteria; + } + + public void or(Criteria criteria) { + oredCriteria.add(criteria); + } + + public Criteria or() { + Criteria criteria = createCriteriaInternal(); + oredCriteria.add(criteria); + return criteria; + } + + public Criteria createCriteria() { + Criteria criteria = createCriteriaInternal(); + if (oredCriteria.size() == 0) { + oredCriteria.add(criteria); + } + return criteria; + } + + protected Criteria createCriteriaInternal() { + Criteria criteria = new Criteria(); + return criteria; + } + + public void clear() { + oredCriteria.clear(); + orderByClause = null; + distinct = false; + } + + protected abstract static class GeneratedCriteria { + protected List criteria; + + protected GeneratedCriteria() { + super(); + criteria = new ArrayList(); + } + + public boolean isValid() { + return criteria.size() > 0; + } + + public List getAllCriteria() { + return criteria; + } + + public List getCriteria() { + return criteria; + } + + protected void addCriterion(String condition) { + if (condition == null) { + throw new RuntimeException("Value for condition cannot be null"); + } + criteria.add(new Criterion(condition)); + } + + protected void addCriterion(String condition, Object value, String property) { + if (value == null) { + throw new RuntimeException("Value for " + property + " cannot be null"); + } + criteria.add(new Criterion(condition, value)); + } + + protected void addCriterion(String condition, Object value1, Object value2, String property) { + if (value1 == null || value2 == null) { + throw new RuntimeException("Between values for " + property + " cannot be null"); + } + criteria.add(new Criterion(condition, value1, value2)); + } + + public Criteria andIdIsNull() { + addCriterion("id is null"); + return (Criteria) this; + } + + public Criteria andIdIsNotNull() { + addCriterion("id is not null"); + return (Criteria) this; + } + + public Criteria andIdEqualTo(String value) { + addCriterion("id =", value, "id"); + return (Criteria) this; + } + + public Criteria andIdNotEqualTo(String value) { + addCriterion("id <>", value, "id"); + return (Criteria) this; + } + + public Criteria andIdGreaterThan(String value) { + addCriterion("id >", value, "id"); + return (Criteria) this; + } + + public Criteria andIdGreaterThanOrEqualTo(String value) { + addCriterion("id >=", value, "id"); + return (Criteria) this; + } + + public Criteria andIdLessThan(String value) { + addCriterion("id <", value, "id"); + return (Criteria) this; + } + + public Criteria andIdLessThanOrEqualTo(String value) { + addCriterion("id <=", value, "id"); + return (Criteria) this; + } + + public Criteria andIdLike(String value) { + addCriterion("id like", value, "id"); + return (Criteria) this; + } + + public Criteria andIdNotLike(String value) { + addCriterion("id not like", value, "id"); + return (Criteria) this; + } + + public Criteria andIdIn(List values) { + addCriterion("id in", values, "id"); + return (Criteria) this; + } + + public Criteria andIdNotIn(List values) { + addCriterion("id not in", values, "id"); + return (Criteria) this; + } + + public Criteria andIdBetween(String value1, String value2) { + addCriterion("id between", value1, value2, "id"); + return (Criteria) this; + } + + public Criteria andIdNotBetween(String value1, String value2) { + addCriterion("id not between", value1, value2, "id"); + return (Criteria) this; + } + + public Criteria andNameIsNull() { + addCriterion("`name` is null"); + return (Criteria) this; + } + + public Criteria andNameIsNotNull() { + addCriterion("`name` is not null"); + return (Criteria) this; + } + + public Criteria andNameEqualTo(String value) { + addCriterion("`name` =", value, "name"); + return (Criteria) this; + } + + public Criteria andNameNotEqualTo(String value) { + addCriterion("`name` <>", value, "name"); + return (Criteria) this; + } + + public Criteria andNameGreaterThan(String value) { + addCriterion("`name` >", value, "name"); + return (Criteria) this; + } + + public Criteria andNameGreaterThanOrEqualTo(String value) { + addCriterion("`name` >=", value, "name"); + return (Criteria) this; + } + + public Criteria andNameLessThan(String value) { + addCriterion("`name` <", value, "name"); + return (Criteria) this; + } + + public Criteria andNameLessThanOrEqualTo(String value) { + addCriterion("`name` <=", value, "name"); + return (Criteria) this; + } + + public Criteria andNameLike(String value) { + addCriterion("`name` like", value, "name"); + return (Criteria) this; + } + + public Criteria andNameNotLike(String value) { + addCriterion("`name` not like", value, "name"); + return (Criteria) this; + } + + public Criteria andNameIn(List values) { + addCriterion("`name` in", values, "name"); + return (Criteria) this; + } + + public Criteria andNameNotIn(List values) { + addCriterion("`name` not in", values, "name"); + return (Criteria) this; + } + + public Criteria andNameBetween(String value1, String value2) { + addCriterion("`name` between", value1, value2, "name"); + return (Criteria) this; + } + + public Criteria andNameNotBetween(String value1, String value2) { + addCriterion("`name` not between", value1, value2, "name"); + return (Criteria) this; + } + + public Criteria andTypeIsNull() { + addCriterion("`type` is null"); + return (Criteria) this; + } + + public Criteria andTypeIsNotNull() { + addCriterion("`type` is not null"); + return (Criteria) this; + } + + public Criteria andTypeEqualTo(String value) { + addCriterion("`type` =", value, "type"); + return (Criteria) this; + } + + public Criteria andTypeNotEqualTo(String value) { + addCriterion("`type` <>", value, "type"); + return (Criteria) this; + } + + public Criteria andTypeGreaterThan(String value) { + addCriterion("`type` >", value, "type"); + return (Criteria) this; + } + + public Criteria andTypeGreaterThanOrEqualTo(String value) { + addCriterion("`type` >=", value, "type"); + return (Criteria) this; + } + + public Criteria andTypeLessThan(String value) { + addCriterion("`type` <", value, "type"); + return (Criteria) this; + } + + public Criteria andTypeLessThanOrEqualTo(String value) { + addCriterion("`type` <=", value, "type"); + return (Criteria) this; + } + + public Criteria andTypeLike(String value) { + addCriterion("`type` like", value, "type"); + return (Criteria) this; + } + + public Criteria andTypeNotLike(String value) { + addCriterion("`type` not like", value, "type"); + return (Criteria) this; + } + + public Criteria andTypeIn(List values) { + addCriterion("`type` in", values, "type"); + return (Criteria) this; + } + + public Criteria andTypeNotIn(List values) { + addCriterion("`type` not in", values, "type"); + return (Criteria) this; + } + + public Criteria andTypeBetween(String value1, String value2) { + addCriterion("`type` between", value1, value2, "type"); + return (Criteria) this; + } + + public Criteria andTypeNotBetween(String value1, String value2) { + addCriterion("`type` not between", value1, value2, "type"); + return (Criteria) this; + } + + public Criteria andFileidIsNull() { + addCriterion("fileId is null"); + return (Criteria) this; + } + + public Criteria andFileidIsNotNull() { + addCriterion("fileId is not null"); + return (Criteria) this; + } + + public Criteria andFileidEqualTo(String value) { + addCriterion("fileId =", value, "fileid"); + return (Criteria) this; + } + + public Criteria andFileidNotEqualTo(String value) { + addCriterion("fileId <>", value, "fileid"); + return (Criteria) this; + } + + public Criteria andFileidGreaterThan(String value) { + addCriterion("fileId >", value, "fileid"); + return (Criteria) this; + } + + public Criteria andFileidGreaterThanOrEqualTo(String value) { + addCriterion("fileId >=", value, "fileid"); + return (Criteria) this; + } + + public Criteria andFileidLessThan(String value) { + addCriterion("fileId <", value, "fileid"); + return (Criteria) this; + } + + public Criteria andFileidLessThanOrEqualTo(String value) { + addCriterion("fileId <=", value, "fileid"); + return (Criteria) this; + } + + public Criteria andFileidLike(String value) { + addCriterion("fileId like", value, "fileid"); + return (Criteria) this; + } + + public Criteria andFileidNotLike(String value) { + addCriterion("fileId not like", value, "fileid"); + return (Criteria) this; + } + + public Criteria andFileidIn(List values) { + addCriterion("fileId in", values, "fileid"); + return (Criteria) this; + } + + public Criteria andFileidNotIn(List values) { + addCriterion("fileId not in", values, "fileid"); + return (Criteria) this; + } + + public Criteria andFileidBetween(String value1, String value2) { + addCriterion("fileId between", value1, value2, "fileid"); + return (Criteria) this; + } + + public Criteria andFileidNotBetween(String value1, String value2) { + addCriterion("fileId not between", value1, value2, "fileid"); + return (Criteria) this; + } + + public Criteria andStateIsNull() { + addCriterion("`state` is null"); + return (Criteria) this; + } + + public Criteria andStateIsNotNull() { + addCriterion("`state` is not null"); + return (Criteria) this; + } + + public Criteria andStateEqualTo(String value) { + addCriterion("`state` =", value, "state"); + return (Criteria) this; + } + + public Criteria andStateNotEqualTo(String value) { + addCriterion("`state` <>", value, "state"); + return (Criteria) this; + } + + public Criteria andStateGreaterThan(String value) { + addCriterion("`state` >", value, "state"); + return (Criteria) this; + } + + public Criteria andStateGreaterThanOrEqualTo(String value) { + addCriterion("`state` >=", value, "state"); + return (Criteria) this; + } + + public Criteria andStateLessThan(String value) { + addCriterion("`state` <", value, "state"); + return (Criteria) this; + } + + public Criteria andStateLessThanOrEqualTo(String value) { + addCriterion("`state` <=", value, "state"); + return (Criteria) this; + } + + public Criteria andStateLike(String value) { + addCriterion("`state` like", value, "state"); + return (Criteria) this; + } + + public Criteria andStateNotLike(String value) { + addCriterion("`state` not like", value, "state"); + return (Criteria) this; + } + + public Criteria andStateIn(List values) { + addCriterion("`state` in", values, "state"); + return (Criteria) this; + } + + public Criteria andStateNotIn(List values) { + addCriterion("`state` not in", values, "state"); + return (Criteria) this; + } + + public Criteria andStateBetween(String value1, String value2) { + addCriterion("`state` between", value1, value2, "state"); + return (Criteria) this; + } + + public Criteria andStateNotBetween(String value1, String value2) { + addCriterion("`state` not between", value1, value2, "state"); + return (Criteria) this; + } + + public Criteria andCreateUserIsNull() { + addCriterion("create_user is null"); + return (Criteria) this; + } + + public Criteria andCreateUserIsNotNull() { + addCriterion("create_user is not null"); + return (Criteria) this; + } + + public Criteria andCreateUserEqualTo(String value) { + addCriterion("create_user =", value, "createUser"); + return (Criteria) this; + } + + public Criteria andCreateUserNotEqualTo(String value) { + addCriterion("create_user <>", value, "createUser"); + return (Criteria) this; + } + + public Criteria andCreateUserGreaterThan(String value) { + addCriterion("create_user >", value, "createUser"); + return (Criteria) this; + } + + public Criteria andCreateUserGreaterThanOrEqualTo(String value) { + addCriterion("create_user >=", value, "createUser"); + return (Criteria) this; + } + + public Criteria andCreateUserLessThan(String value) { + addCriterion("create_user <", value, "createUser"); + return (Criteria) this; + } + + public Criteria andCreateUserLessThanOrEqualTo(String value) { + addCriterion("create_user <=", value, "createUser"); + return (Criteria) this; + } + + public Criteria andCreateUserLike(String value) { + addCriterion("create_user like", value, "createUser"); + return (Criteria) this; + } + + public Criteria andCreateUserNotLike(String value) { + addCriterion("create_user not like", value, "createUser"); + return (Criteria) this; + } + + public Criteria andCreateUserIn(List values) { + addCriterion("create_user in", values, "createUser"); + return (Criteria) this; + } + + public Criteria andCreateUserNotIn(List values) { + addCriterion("create_user not in", values, "createUser"); + return (Criteria) this; + } + + public Criteria andCreateUserBetween(String value1, String value2) { + addCriterion("create_user between", value1, value2, "createUser"); + return (Criteria) this; + } + + public Criteria andCreateUserNotBetween(String value1, String value2) { + addCriterion("create_user not between", value1, value2, "createUser"); + return (Criteria) this; + } + + public Criteria andCreateTimeIsNull() { + addCriterion("create_time is null"); + return (Criteria) this; + } + + public Criteria andCreateTimeIsNotNull() { + addCriterion("create_time is not null"); + return (Criteria) this; + } + + public Criteria andCreateTimeEqualTo(Long value) { + addCriterion("create_time =", value, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeNotEqualTo(Long value) { + addCriterion("create_time <>", value, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeGreaterThan(Long value) { + addCriterion("create_time >", value, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeGreaterThanOrEqualTo(Long value) { + addCriterion("create_time >=", value, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeLessThan(Long value) { + addCriterion("create_time <", value, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeLessThanOrEqualTo(Long value) { + addCriterion("create_time <=", value, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeIn(List values) { + addCriterion("create_time in", values, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeNotIn(List values) { + addCriterion("create_time not in", values, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeBetween(Long value1, Long value2) { + addCriterion("create_time between", value1, value2, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeNotBetween(Long value1, Long value2) { + addCriterion("create_time not between", value1, value2, "createTime"); + return (Criteria) this; + } + + public Criteria andUpdateUserIsNull() { + addCriterion("update_user is null"); + return (Criteria) this; + } + + public Criteria andUpdateUserIsNotNull() { + addCriterion("update_user is not null"); + return (Criteria) this; + } + + public Criteria andUpdateUserEqualTo(String value) { + addCriterion("update_user =", value, "updateUser"); + return (Criteria) this; + } + + public Criteria andUpdateUserNotEqualTo(String value) { + addCriterion("update_user <>", value, "updateUser"); + return (Criteria) this; + } + + public Criteria andUpdateUserGreaterThan(String value) { + addCriterion("update_user >", value, "updateUser"); + return (Criteria) this; + } + + public Criteria andUpdateUserGreaterThanOrEqualTo(String value) { + addCriterion("update_user >=", value, "updateUser"); + return (Criteria) this; + } + + public Criteria andUpdateUserLessThan(String value) { + addCriterion("update_user <", value, "updateUser"); + return (Criteria) this; + } + + public Criteria andUpdateUserLessThanOrEqualTo(String value) { + addCriterion("update_user <=", value, "updateUser"); + return (Criteria) this; + } + + public Criteria andUpdateUserLike(String value) { + addCriterion("update_user like", value, "updateUser"); + return (Criteria) this; + } + + public Criteria andUpdateUserNotLike(String value) { + addCriterion("update_user not like", value, "updateUser"); + return (Criteria) this; + } + + public Criteria andUpdateUserIn(List values) { + addCriterion("update_user in", values, "updateUser"); + return (Criteria) this; + } + + public Criteria andUpdateUserNotIn(List values) { + addCriterion("update_user not in", values, "updateUser"); + return (Criteria) this; + } + + public Criteria andUpdateUserBetween(String value1, String value2) { + addCriterion("update_user between", value1, value2, "updateUser"); + return (Criteria) this; + } + + public Criteria andUpdateUserNotBetween(String value1, String value2) { + addCriterion("update_user not between", value1, value2, "updateUser"); + return (Criteria) this; + } + + public Criteria andUpdateTimeIsNull() { + addCriterion("update_time is null"); + return (Criteria) this; + } + + public Criteria andUpdateTimeIsNotNull() { + addCriterion("update_time is not null"); + return (Criteria) this; + } + + public Criteria andUpdateTimeEqualTo(Long value) { + addCriterion("update_time =", value, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeNotEqualTo(Long value) { + addCriterion("update_time <>", value, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeGreaterThan(Long value) { + addCriterion("update_time >", value, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeGreaterThanOrEqualTo(Long value) { + addCriterion("update_time >=", value, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeLessThan(Long value) { + addCriterion("update_time <", value, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeLessThanOrEqualTo(Long value) { + addCriterion("update_time <=", value, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeIn(List values) { + addCriterion("update_time in", values, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeNotIn(List values) { + addCriterion("update_time not in", values, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeBetween(Long value1, Long value2) { + addCriterion("update_time between", value1, value2, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeNotBetween(Long value1, Long value2) { + addCriterion("update_time not between", value1, value2, "updateTime"); + return (Criteria) this; + } + } + + public static class Criteria extends GeneratedCriteria { + + protected Criteria() { + super(); + } + } + + public static class Criterion { + private String condition; + + private Object value; + + private Object secondValue; + + private boolean noValue; + + private boolean singleValue; + + private boolean betweenValue; + + private boolean listValue; + + private String typeHandler; + + public String getCondition() { + return condition; + } + + public Object getValue() { + return value; + } + + public Object getSecondValue() { + return secondValue; + } + + public boolean isNoValue() { + return noValue; + } + + public boolean isSingleValue() { + return singleValue; + } + + public boolean isBetweenValue() { + return betweenValue; + } + + public boolean isListValue() { + return listValue; + } + + public String getTypeHandler() { + return typeHandler; + } + + protected Criterion(String condition) { + super(); + this.condition = condition; + this.typeHandler = null; + this.noValue = true; + } + + protected Criterion(String condition, Object value, String typeHandler) { + super(); + this.condition = condition; + this.value = value; + this.typeHandler = typeHandler; + if (value instanceof List) { + this.listValue = true; + } else { + this.singleValue = true; + } + } + + protected Criterion(String condition, Object value) { + this(condition, value, null); + } + + protected Criterion(String condition, Object value, Object secondValue, String typeHandler) { + super(); + this.condition = condition; + this.value = value; + this.secondValue = secondValue; + this.typeHandler = typeHandler; + this.betweenValue = true; + } + + protected Criterion(String condition, Object value, Object secondValue) { + this(condition, value, secondValue, null); + } + } +} \ No newline at end of file diff --git a/backend/framework/domain/src/main/java/io/metersphere/functional/mapper/ExportTaskMapper.java b/backend/framework/domain/src/main/java/io/metersphere/functional/mapper/ExportTaskMapper.java new file mode 100644 index 0000000000..78ecf2e6c4 --- /dev/null +++ b/backend/framework/domain/src/main/java/io/metersphere/functional/mapper/ExportTaskMapper.java @@ -0,0 +1,34 @@ +package io.metersphere.functional.mapper; + +import io.metersphere.functional.domain.ExportTask; +import io.metersphere.functional.domain.ExportTaskExample; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +public interface ExportTaskMapper { + long countByExample(ExportTaskExample example); + + int deleteByExample(ExportTaskExample example); + + int deleteByPrimaryKey(String id); + + int insert(ExportTask record); + + int insertSelective(ExportTask record); + + List selectByExample(ExportTaskExample example); + + ExportTask selectByPrimaryKey(String id); + + int updateByExampleSelective(@Param("record") ExportTask record, @Param("example") ExportTaskExample example); + + int updateByExample(@Param("record") ExportTask record, @Param("example") ExportTaskExample example); + + int updateByPrimaryKeySelective(ExportTask record); + + int updateByPrimaryKey(ExportTask record); + + int batchInsert(@Param("list") List list); + + int batchInsertSelective(@Param("list") List list, @Param("selective") ExportTask.Column ... selective); +} \ No newline at end of file diff --git a/backend/framework/domain/src/main/java/io/metersphere/functional/mapper/ExportTaskMapper.xml b/backend/framework/domain/src/main/java/io/metersphere/functional/mapper/ExportTaskMapper.xml new file mode 100644 index 0000000000..52252b9dbc --- /dev/null +++ b/backend/framework/domain/src/main/java/io/metersphere/functional/mapper/ExportTaskMapper.xml @@ -0,0 +1,328 @@ + + + + + + + + + + + + + + + + + + + + + + + and ${criterion.condition} + + + and ${criterion.condition} #{criterion.value} + + + and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} + + + and ${criterion.condition} + + #{listItem} + + + + + + + + + + + + + + + + + + and ${criterion.condition} + + + and ${criterion.condition} #{criterion.value} + + + and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} + + + and ${criterion.condition} + + #{listItem} + + + + + + + + + + + id, `name`, `type`, fileId, `state`, create_user, create_time, update_user, update_time + + + + + delete from export_task + where id = #{id,jdbcType=VARCHAR} + + + delete from export_task + + + + + + insert into export_task (id, `name`, `type`, + fileId, `state`, create_user, + create_time, update_user, update_time + ) + values (#{id,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{type,jdbcType=VARCHAR}, + #{fileid,jdbcType=VARCHAR}, #{state,jdbcType=VARCHAR}, #{createUser,jdbcType=VARCHAR}, + #{createTime,jdbcType=BIGINT}, #{updateUser,jdbcType=VARCHAR}, #{updateTime,jdbcType=BIGINT} + ) + + + insert into export_task + + + id, + + + `name`, + + + `type`, + + + fileId, + + + `state`, + + + create_user, + + + create_time, + + + update_user, + + + update_time, + + + + + #{id,jdbcType=VARCHAR}, + + + #{name,jdbcType=VARCHAR}, + + + #{type,jdbcType=VARCHAR}, + + + #{fileid,jdbcType=VARCHAR}, + + + #{state,jdbcType=VARCHAR}, + + + #{createUser,jdbcType=VARCHAR}, + + + #{createTime,jdbcType=BIGINT}, + + + #{updateUser,jdbcType=VARCHAR}, + + + #{updateTime,jdbcType=BIGINT}, + + + + + + update export_task + + + id = #{record.id,jdbcType=VARCHAR}, + + + `name` = #{record.name,jdbcType=VARCHAR}, + + + `type` = #{record.type,jdbcType=VARCHAR}, + + + fileId = #{record.fileid,jdbcType=VARCHAR}, + + + `state` = #{record.state,jdbcType=VARCHAR}, + + + create_user = #{record.createUser,jdbcType=VARCHAR}, + + + create_time = #{record.createTime,jdbcType=BIGINT}, + + + update_user = #{record.updateUser,jdbcType=VARCHAR}, + + + update_time = #{record.updateTime,jdbcType=BIGINT}, + + + + + + + + update export_task + set id = #{record.id,jdbcType=VARCHAR}, + `name` = #{record.name,jdbcType=VARCHAR}, + `type` = #{record.type,jdbcType=VARCHAR}, + fileId = #{record.fileid,jdbcType=VARCHAR}, + `state` = #{record.state,jdbcType=VARCHAR}, + create_user = #{record.createUser,jdbcType=VARCHAR}, + create_time = #{record.createTime,jdbcType=BIGINT}, + update_user = #{record.updateUser,jdbcType=VARCHAR}, + update_time = #{record.updateTime,jdbcType=BIGINT} + + + + + + update export_task + + + `name` = #{name,jdbcType=VARCHAR}, + + + `type` = #{type,jdbcType=VARCHAR}, + + + fileId = #{fileid,jdbcType=VARCHAR}, + + + `state` = #{state,jdbcType=VARCHAR}, + + + create_user = #{createUser,jdbcType=VARCHAR}, + + + create_time = #{createTime,jdbcType=BIGINT}, + + + update_user = #{updateUser,jdbcType=VARCHAR}, + + + update_time = #{updateTime,jdbcType=BIGINT}, + + + where id = #{id,jdbcType=VARCHAR} + + + update export_task + set `name` = #{name,jdbcType=VARCHAR}, + `type` = #{type,jdbcType=VARCHAR}, + fileId = #{fileid,jdbcType=VARCHAR}, + `state` = #{state,jdbcType=VARCHAR}, + create_user = #{createUser,jdbcType=VARCHAR}, + create_time = #{createTime,jdbcType=BIGINT}, + update_user = #{updateUser,jdbcType=VARCHAR}, + update_time = #{updateTime,jdbcType=BIGINT} + where id = #{id,jdbcType=VARCHAR} + + + insert into export_task + (id, `name`, `type`, fileId, `state`, create_user, create_time, update_user, update_time + ) + values + + (#{item.id,jdbcType=VARCHAR}, #{item.name,jdbcType=VARCHAR}, #{item.type,jdbcType=VARCHAR}, + #{item.fileid,jdbcType=VARCHAR}, #{item.state,jdbcType=VARCHAR}, #{item.createUser,jdbcType=VARCHAR}, + #{item.createTime,jdbcType=BIGINT}, #{item.updateUser,jdbcType=VARCHAR}, #{item.updateTime,jdbcType=BIGINT} + ) + + + + insert into export_task ( + + ${column.escapedColumnName} + + ) + values + + ( + + + #{item.id,jdbcType=VARCHAR} + + + #{item.name,jdbcType=VARCHAR} + + + #{item.type,jdbcType=VARCHAR} + + + #{item.fileid,jdbcType=VARCHAR} + + + #{item.state,jdbcType=VARCHAR} + + + #{item.createUser,jdbcType=VARCHAR} + + + #{item.createTime,jdbcType=BIGINT} + + + #{item.updateUser,jdbcType=VARCHAR} + + + #{item.updateTime,jdbcType=BIGINT} + + + ) + + + \ No newline at end of file diff --git a/backend/framework/domain/src/main/resources/migration/3.2.0/ddl/V3.2.0_2__ga_ddl.sql b/backend/framework/domain/src/main/resources/migration/3.2.0/ddl/V3.2.0_2__ga_ddl.sql index 2198debf7e..ae917e6fe7 100644 --- a/backend/framework/domain/src/main/resources/migration/3.2.0/ddl/V3.2.0_2__ga_ddl.sql +++ b/backend/framework/domain/src/main/resources/migration/3.2.0/ddl/V3.2.0_2__ga_ddl.sql @@ -7,6 +7,29 @@ ALTER TABLE api_test_case ADD ignore_api_diff BIT(1) DEFAULT 0 NOT NULL COMMENT ALTER TABLE test_plan_report_bug MODIFY bug_handle_user VARCHAR(255) NULL COMMENT '缺陷处理人'; +CREATE TABLE export_task( + `id` VARCHAR(50) NOT NULL COMMENT '任务唯一ID' , + `name` VARCHAR(255) COMMENT '名称' , + `type` VARCHAR(50) NOT NULL COMMENT '资源类型' , + `fileId` VARCHAR(255) COMMENT '文件id' , + `state` VARCHAR(50) NOT NULL COMMENT '状态' , + `create_user` VARCHAR(50) NOT NULL COMMENT '创建人' , + `create_time` BIGINT NOT NULL COMMENT '创建时间' , + `update_user` VARCHAR(50) NOT NULL COMMENT '创建人' , + `update_time` BIGINT NOT NULL COMMENT '创建时间' , + PRIMARY KEY (id) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT = '导出任务'; + + +CREATE INDEX idx_create_user ON export_task(`create_user`); +CREATE INDEX idx_state ON export_task(`state`); +CREATE INDEX idx_create_time ON export_task(`create_time`); +CREATE INDEX idx_type ON export_task(`type`); +CREATE INDEX idx_update_user ON export_task(`update_user`); +CREATE INDEX idx_update_time ON export_task(`update_time`); + -- set innodb lock wait timeout to default SET SESSION innodb_lock_wait_timeout = DEFAULT; diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaTopicConstants.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaTopicConstants.java index 3399d30819..dc243117a7 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaTopicConstants.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaTopicConstants.java @@ -2,6 +2,7 @@ package io.metersphere.sdk.constants; public class KafkaTopicConstants { public static final String PLUGIN = "PLUGIN"; + public static final String EXPORT = "EXPORT"; // API TOPIC public static final String API_REPORT_TOPIC = "API_REPORT_TOPIC"; public static final String API_REPORT_TASK_TOPIC = "API_REPORT_TASK_TOPIC"; diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/MsFileUtils.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/MsFileUtils.java index 90bb8a55c7..71f2c0a7db 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/MsFileUtils.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/MsFileUtils.java @@ -3,8 +3,13 @@ package io.metersphere.sdk.util; import io.metersphere.sdk.exception.MSException; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; +import org.springframework.web.multipart.MultipartFile; import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Objects; public class MsFileUtils { public static void validateFileName(String... fileNames) { @@ -21,4 +26,38 @@ public class MsFileUtils { File file = new File(path); FileUtils.deleteDirectory(file); } + + /** + * 获取流文件 + */ + private static void inputStreamToFile(InputStream ins, File file) { + try (OutputStream os = new FileOutputStream(file);) { + int bytesRead = 0; + byte[] buffer = new byte[8192]; + while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) { + os.write(buffer, 0, bytesRead); + } + } catch (Exception e) { + LogUtils.error(e); + } + } + + /** + * MultipartFile 转 File + * + * @param file += */ + public static File multipartFileToFile(MultipartFile file) { + if (file != null && file.getSize() > 0) { + try (InputStream ins = file.getInputStream()) { + validateFileName(file.getOriginalFilename()); + File toFile = new File(Objects.requireNonNull(file.getOriginalFilename())); + inputStreamToFile(ins, toFile); + return toFile; + } catch (Exception e) { + LogUtils.error(e); + } + } + return null; + } } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/XMLUtils.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/XMLUtils.java index fbc9fcb724..274a4d3934 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/XMLUtils.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/XMLUtils.java @@ -1,11 +1,20 @@ package io.metersphere.sdk.util; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.StreamReadConstraints; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +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.datatype.jsr310.JavaTimeModule; import org.apache.commons.lang3.StringUtils; -import org.dom4j.Document; -import org.dom4j.DocumentException; -import org.dom4j.Element; -import org.dom4j.Node; +import org.dom4j.*; import org.dom4j.io.OutputFormat; import org.dom4j.io.SAXReader; import org.dom4j.io.XMLWriter; @@ -22,6 +31,29 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; public class XMLUtils { + private static final ObjectMapper objectMapper = JsonMapper.builder() + .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS) + .build(); + 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 final boolean IS_TRANS = false; public static Document getDocument(InputStream source) throws DocumentException { @@ -173,4 +205,91 @@ public class XMLUtils { matcher.appendTail(result); return result.toString(); } + + 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) {// 遍历所有一级子节点 + JsonNode jsonObject = getJsonObjectByDC(e); + //加xml标签上的属性 eg: RB + //这里添加 length scale type + if (!e.attributes().isEmpty()) { + ObjectNode attributeJson = objectMapper.createObjectNode();; + for (Attribute attribute : e.attributes()) { + try { + attributeJson.putIfAbsent(attribute.getName(), objectMapper.readTree(attribute.getValue())); + } catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } + } + ObjectNode jsonObjectNode = (ObjectNode) jsonObject; + jsonObjectNode.putIfAbsent("attribute", attributeJson); + } + list.add(jsonObject); + } + if (list.size() == 1) { + result.putIfAbsent(node.getName(), list.get(0)); + } else { + try { + String s = objectMapper.writeValueAsString(list); + result.putIfAbsent(node.getName(), objectMapper.readTree(s)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + } + } else { + if (!StringUtils.isAllBlank(node.getName(), node.getText())) { + try { + result.putIfAbsent(node.getName(), objectMapper.readTree(node.getText())); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + return result; + } } diff --git a/backend/framework/sdk/src/main/resources/i18n/case.properties b/backend/framework/sdk/src/main/resources/i18n/case.properties index 014a6fd4ca..19e28db001 100644 --- a/backend/framework/sdk/src/main/resources/i18n/case.properties +++ b/backend/framework/sdk/src/main/resources/i18n/case.properties @@ -173,5 +173,9 @@ case.minder.all.case=全部用例 case.minder.status.success=成功 case.minder.status.error=失败 case.minder.status.blocked=阻塞 +#import +case.find_file_error=找不到该文件 +export_case_task_stop=停止导出 +export_case_task_existed=已有导出任务 diff --git a/backend/framework/sdk/src/main/resources/i18n/case_en_US.properties b/backend/framework/sdk/src/main/resources/i18n/case_en_US.properties index 8cb1fd2779..d4ef5ada72 100644 --- a/backend/framework/sdk/src/main/resources/i18n/case_en_US.properties +++ b/backend/framework/sdk/src/main/resources/i18n/case_en_US.properties @@ -257,6 +257,9 @@ case.execute.status.pending=Pending functional_case_comment_template=【评论:%s(%s)】\n%s\n functional_case_execute_comment_template=[Execute comment:%s %s(%s)]\n%s\n functional_case_review_comment_template=[Review comment:%s %s(%s)]\n%s\n + +#import +case.find_file_error=The file cannot be found functional_case_xmind_template=Functional case xmind template download_template_failed=Download template failed functional_case=Functional case @@ -278,4 +281,7 @@ case.export.system.other.columns.review_status=Review status case.export.system.other.columns.create_user=Create user case.export.system.other.columns.create_time=Create time case.export.system.other.columns.update_user=Update user -case.export.system.other.columns.update_time=Update time \ No newline at end of file +case.export.system.other.columns.update_time=Update time + +export_case_task_stop=Stop export +export_case_task_existed=Export task already exists \ No newline at end of file diff --git a/backend/framework/sdk/src/main/resources/i18n/case_zh_CN.properties b/backend/framework/sdk/src/main/resources/i18n/case_zh_CN.properties index 88cc601d1e..11e38a81e6 100644 --- a/backend/framework/sdk/src/main/resources/i18n/case_zh_CN.properties +++ b/backend/framework/sdk/src/main/resources/i18n/case_zh_CN.properties @@ -255,6 +255,8 @@ case.execute.status.pending=未执行 functional_case_comment_template=【评论:%s(%s)】\n%s\n functional_case_execute_comment_template=【执行评论:%s %s(%s)】\n%s\n functional_case_review_comment_template=【评审评论:%s %s(%s)】\n%s\n +#import +case.find_file_error=找不到该文件 functional_case_xmind_template=思维导图用例模版 download_template_failed=下载思维导图模版失败 functional_case=功能用例 @@ -276,4 +278,7 @@ case.export.system.other.columns.review_status=评审结果 case.export.system.other.columns.create_user=创建人 case.export.system.other.columns.create_time=创建时间 case.export.system.other.columns.update_user=更新人 -case.export.system.other.columns.update_time=更新时间 \ No newline at end of file +case.export.system.other.columns.update_time=更新时间 + +export_case_task_stop=停止导出 +export_case_task_existed=已有导出任务 \ No newline at end of file diff --git a/backend/framework/sdk/src/main/resources/i18n/case_zh_TW.properties b/backend/framework/sdk/src/main/resources/i18n/case_zh_TW.properties index 2b84299711..fb94306e2c 100644 --- a/backend/framework/sdk/src/main/resources/i18n/case_zh_TW.properties +++ b/backend/framework/sdk/src/main/resources/i18n/case_zh_TW.properties @@ -266,6 +266,11 @@ xmind_textDescription=文本描述 xmind_expectedResult=預期結果 xmind_step=用例步驟 xmind_stepDescription=步驟描述 + +#import +case.find_file_error=找不到該文件 + + # case export columns case.export.system.columns.name=用例名稱 case.export.system.columns.id=ID @@ -277,4 +282,7 @@ case.export.system.other.columns.review_status=評審結果 case.export.system.other.columns.create_user=創建人 case.export.system.other.columns.create_time=創建時間 case.export.system.other.columns.update_user=更新人 -case.export.system.other.columns.update_time=更新時間 \ No newline at end of file +case.export.system.other.columns.update_time=更新時間 + +export_case_task_stop=停止導出 +export_case_task_existed=已有導出任務 \ No newline at end of file diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseController.java b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseController.java index 5ac5beb299..da4fd77e17 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseController.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseController.java @@ -255,7 +255,15 @@ public class FunctionalCaseController { @Operation(summary = "用例管理-功能用例-excel导出") @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_EXPORT) public void testCaseExport(@Validated @RequestBody FunctionalCaseExportRequest request) { - functionalCaseFileService.exportFunctionalCaseZip(request); + functionalCaseFileService.export(SessionUtils.getUserId(), request); + } + + @GetMapping("/stop/{projectId}") + @Operation(summary = "用例管理-功能用例-导出-停止导出") + @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_EXPORT) + @CheckOwner(resourceId = "#projectId", resourceType = "project") + public void caseStopExport(@PathVariable String projectId) { + functionalCaseFileService.stopExport(projectId, SessionUtils.getUserId()); } @GetMapping("/download/xmind/template/{projectId}") diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseFileService.java b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseFileService.java index 62f216c74f..e88a5c3953 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseFileService.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseFileService.java @@ -22,6 +22,7 @@ import io.metersphere.functional.excel.listener.FunctionalCaseImportEventListene import io.metersphere.functional.excel.listener.FunctionalCasePretreatmentListener; import io.metersphere.functional.excel.validate.AbstractCustomFieldValidator; import io.metersphere.functional.excel.validate.CustomFieldValidatorFactory; +import io.metersphere.functional.mapper.ExportTaskMapper; import io.metersphere.functional.mapper.ExtFunctionalCaseCommentMapper; import io.metersphere.functional.request.FunctionalCaseExportRequest; import io.metersphere.functional.request.FunctionalCaseImportRequest; @@ -36,6 +37,7 @@ import io.metersphere.sdk.dto.SocketMsgDTO; 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.CustomFieldOption; import io.metersphere.system.domain.SystemParameter; import io.metersphere.system.dto.sdk.BaseTreeNode; @@ -43,6 +45,7 @@ import io.metersphere.system.dto.sdk.SessionUser; import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; import io.metersphere.system.dto.sdk.TemplateDTO; import io.metersphere.system.excel.utils.EasyExcelExporter; +import io.metersphere.system.manager.ExportTaskManager; import io.metersphere.system.mapper.SystemParameterMapper; import io.metersphere.system.service.FileService; import io.metersphere.system.uid.IDGenerator; @@ -57,7 +60,6 @@ import org.jetbrains.annotations.NotNull; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -103,6 +105,10 @@ public class FunctionalCaseFileService { @Resource private SystemParameterMapper systemParameterMapper; private static final String EXPORT_FILE_NAME = "case_export"; + @Resource + private ExportTaskManager exportTaskManager; + @Resource + private ExportTaskMapper exportTaskMapper; /** * 下载excel导入模板 @@ -327,15 +333,28 @@ public class FunctionalCaseFileService { } } + public void export(String userId, FunctionalCaseExportRequest request){ + try { + ExportTaskExample exportTaskExample = new ExportTaskExample(); + exportTaskExample.createCriteria().andTypeEqualTo(ExportConstants.ExportType.CASE.toString()).andStateEqualTo(ExportConstants.ExportState.PREPARED.toString()); + long preparedCount = exportTaskMapper.countByExample(exportTaskExample); + if (preparedCount>0) { + throw new MSException(Translator.get("export_case_task_existed")); + } + exportTaskManager.exportAsyncTask(userId, ExportConstants.ExportType.CASE.toString(), request, t->exportFunctionalCaseZip(request)); + } catch (InterruptedException e) { + LogUtils.error("导出失败:"+e); + throw new MSException(e); + } + } + /** * 导出excel * * @param request - * @param url */ - @Async - public void exportFunctionalCaseZip(FunctionalCaseExportRequest request) { + public String exportFunctionalCaseZip(FunctionalCaseExportRequest request) { File tmpDir = null; Project project = projectMapper.selectByPrimaryKey(request.getProjectId()); try { @@ -356,14 +375,39 @@ public class FunctionalCaseFileService { uploadFileToMinio(singeFile, request.getFileId()); } functionalCaseLogService.exportExcelLog(request); - SocketMsgDTO socketMsgDTO = new SocketMsgDTO(request.getFileId(), "", MsgType.CONNECT.name(), MsgType.CONNECT.name()); + List exportTasks = getExportTasks(); + String taskId; + if (CollectionUtils.isNotEmpty(exportTasks)) { + taskId = exportTasks.getFirst().getId(); + updateExportTask(ExportConstants.ExportState.SUCCESS.toString(), taskId); + } else { + taskId = MsgType.CONNECT.name(); + } + SocketMsgDTO socketMsgDTO = new SocketMsgDTO(request.getFileId(), "", MsgType.CONNECT.name(), taskId); socketMsgDTO.setReportId(request.getFileId()); ExportWebSocketHandler.sendMessageSingle(socketMsgDTO); } catch (Exception e) { + List exportTasks = getExportTasks(); + if (CollectionUtils.isNotEmpty(exportTasks)) { + updateExportTask(ExportConstants.ExportState.SUCCESS.toString(), exportTasks.getFirst().getId()); + } LogUtils.error(e); throw new MSException(e); } + return null; + } + private List getExportTasks() { + ExportTaskExample exportTaskExample = new ExportTaskExample(); + exportTaskExample.createCriteria().andTypeEqualTo(ExportConstants.ExportType.CASE.toString()).andStateEqualTo(ExportConstants.ExportState.PREPARED.toString()); + return exportTaskMapper.selectByExample(exportTaskExample); + } + + private void updateExportTask(String state, String taskId) { + ExportTask exportTask = new ExportTask(); + exportTask.setState(state); + exportTask.setId(taskId); + exportTaskMapper.updateByPrimaryKey(exportTask); } private void uploadFileToMinio(File file, String fileId) { @@ -757,4 +801,8 @@ public class FunctionalCaseFileService { .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + "Metersphere_case_" + project.getName() + "\"") .body(bytes); } + + public void stopExport(String projectId, String userId) { + exportTaskManager.sendStopMessage(projectId, userId); + } } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/XMindLegacy.java b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/XMindLegacy.java new file mode 100644 index 0000000000..651f4414fc --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/XMindLegacy.java @@ -0,0 +1,130 @@ +package io.metersphere.functional.xmind.parser; + +import com.fasterxml.jackson.databind.JsonNode; +import io.metersphere.sdk.util.LogUtils; +import io.metersphere.sdk.util.XMLUtils; +import org.apache.commons.lang3.StringUtils; +import org.dom4j.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class XMindLegacy { + + /** + * 返回content.xml和comments.xml合并后的json + * + */ + public static List getContent(String xmlContent, String xmlComments) throws IOException, DocumentException { + // 删除content.xml里面不能识别的字符串 + xmlContent = xmlContent.replace("xmlns=\"urn:xmind:xmap:xmlns:content:2.0\"", StringUtils.EMPTY); + xmlContent = xmlContent.replace("xmlns:fo=\"http://www.w3.org/1999/XSL/Format\"", StringUtils.EMPTY); + try { + xmlContent = removeTopicsFromString(xmlContent); + } catch (Exception e) { + LogUtils.error("移除xml中的Topic出错:", e); + } + // 去除title中svg:width属性 + xmlContent = xmlContent.replaceAll("", "<title>"); + Document document = DocumentHelper.parseText(xmlContent);// 读取XML文件,获得document对象 + Element root = document.getRootElement(); + List<Node> topics = root.selectNodes("//topic"); + + if (xmlComments != null) { + // 删除comments.xml里面不能识别的字符串 + xmlComments = xmlComments.replace("xmlns=\"urn:xmind:xmap:xmlns:comments:2.0\"", StringUtils.EMPTY); + + // 添加评论到content中 + Document commentDocument = DocumentHelper.parseText(xmlComments); + List<Node> commentsList = commentDocument.selectNodes("//comment"); + + for (Node topic : topics) { + for (Node commentNode : commentsList) { + Element commentElement = (Element) commentNode; + Element topicElement = (Element) topic; + if (topicElement.attribute("id").getValue() + .equals(commentElement.attribute("object-id").getValue())) { + Element comment = topicElement.addElement("comments"); + comment.addAttribute("creationTime", commentElement.attribute("time").getValue()); + comment.addAttribute("author", commentElement.attribute("author").getValue()); + comment.addAttribute("content", commentElement.element("content").getText()); + } + } + + } + } + + // 第一个topic转换为json中的rootTopic + List<Node> rootTopics = root.selectNodes("/xmap-content/sheet/topic"); + for (Node rootTopic : rootTopics) { + rootTopic.setName("rootTopic"); + + // 将xml中topic节点转换为attached节点 + List<Node> topicList = rootTopic.selectNodes("//topic"); + for (Node node : topicList) { + node.setName("attached"); + } + + } + + List<String> sheets = new ArrayList<>(); + for (Element sheet : root.elements("sheet")) { + String res = sheet.asXML(); + // 将xml转为json + JsonNode xmlJSONObj = XMLUtils.xmlConvertJson(res); + JsonNode jsonNode = xmlJSONObj.get("sheet"); + sheets.add(jsonNode.toString()); + } + // 设置缩进 + return sheets; + } + + + /** + * 删除topics节点 + * + */ + private static String removeTopicsFromString(String xmlContent) throws Exception { + Document doc = DocumentHelper.parseText(xmlContent); + if (doc != null) { + Element root = doc.getRootElement(); + List<Element> childrenElement = root.elements(); + for (Element child : childrenElement) { + removeTopicsFromElement(child); + } + xmlContent = doc.asXML(); + } + return xmlContent; + } + + /** + * 递归删除topics节点 + * + */ + private static void removeTopicsFromElement(Element element) { + if (element != null) { + List<Element> childrenElement = element.elements(); + List<Element> removeElements = new ArrayList<>(); + List<Element> addElements = new ArrayList<>(); + for (Element child : childrenElement) { + if (StringUtils.equalsIgnoreCase("topics", child.getName()) && StringUtils.equalsAnyIgnoreCase(child.attributeValue("type"), "attached", "detached")) { + removeElements.add(child); + addElements.addAll(child.elements()); + } + } + removeElements.forEach(item -> { + item.getParent().remove(item); + }); + addElements.forEach(item -> { + item.setParent(null); + element.add(item); + }); + childrenElement = element.elements(); + for (Element child : childrenElement) { + removeTopicsFromElement(child); + } + } + + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/XMindParser.java b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/XMindParser.java new file mode 100644 index 0000000000..d331bf87ab --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/XMindParser.java @@ -0,0 +1,125 @@ +package io.metersphere.functional.xmind.parser; + +import io.metersphere.functional.xmind.pojo.JsonRootBean; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.util.JSON; +import io.metersphere.sdk.util.MsFileUtils; +import io.metersphere.sdk.util.Translator; +import org.apache.commons.compress.archivers.ArchiveException; +import org.apache.commons.io.FileUtils; +import org.dom4j.DocumentException; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * @Description 解析主体 + */ +public class XMindParser { + public static final String CONTENT_JSON = "content.json"; + public static final String CONTENT_XML = "content.xml"; + public static final String COMMENTS_XML = "comments.xml"; + + /** + * 解析脑图文件,返回content整合后的内容 + * + */ + public static List<String> parseJson(MultipartFile multipartFile) throws IOException { + + File file = MsFileUtils.multipartFileToFile(multipartFile); + List<String> contents; + String res = null; + if (file == null || !file.exists()) { + throw new MSException (Translator.get("incorrect_format")); + } + try { + res = ZipUtils.extract(file); + if (isXMindZen(res)) { + contents = (getXMindZenContent(res)); + } else { + contents = getXMindLegacyContent(res); + } + } catch (Exception e) { + throw new MSException (e.getMessage()); + } finally { + // 删除生成的文件夹 + if (res != null) { + File dir = new File(res); + FileUtils.deleteDirectory(dir); + } + // 删除零时文件 + if (file != null) { + file.delete(); + } + } + return contents; + } + + public static List<JsonRootBean> parseObject(MultipartFile multipartFile) throws DocumentException, ArchiveException, IOException { + List<String> contents = parseJson(multipartFile); + int caseCount = 0; + List<JsonRootBean> jsonRootBeans = new ArrayList<>(); + if (contents != null) { + for (String content : contents) { + caseCount += content.split("(case-:)").length; + JsonRootBean jsonRootBean = JSON.parseObject(content, JsonRootBean.class); + jsonRootBeans.add(jsonRootBean); + } + if (caseCount > 800) { + throw new MSException (Translator.get("import_xmind_count_error")); + } + } + return jsonRootBeans; + + } + + + /** + * 解析xmind zen 格式的文件 + * @param extractFileDir 解压后的文件夹名字 + */ + public static List<String> getXMindZenContent(String extractFileDir) + throws IOException { + List<String> keys = new ArrayList<>(); + keys.add(CONTENT_JSON); + Map<String, String> map = ZipUtils.getContents(keys, extractFileDir); + String content = map.get(CONTENT_JSON); + return XMindZen.getContent(content); + } + + /** + * 解析正常xmind 格式的文件 + * @param extractFileDir 解压后的文件夹名字 + */ + public static List<String> getXMindLegacyContent(String extractFileDir) + throws IOException, DocumentException { + List<String> keys = new ArrayList<>(); + keys.add(CONTENT_XML); + keys.add(COMMENTS_XML); + Map<String, String> map = ZipUtils.getContents(keys, extractFileDir); + + String contentXml = map.get(CONTENT_XML); + String commentsXml = map.get(COMMENTS_XML); + + return XMindLegacy.getContent(contentXml, commentsXml); + } + + private static boolean isXMindZen(String res){ + // 解压 + File parent = new File(res); + if (parent.isDirectory()) { + String[] files = parent.list(new ZipUtils.FileFilter()); + for (int i = 0; i < Objects.requireNonNull(files).length; i++) { + if (files[i].equals(CONTENT_JSON)) { + return true; + } + } + } + return false; + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/XMindZen.java b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/XMindZen.java new file mode 100644 index 0000000000..ccf8431dbc --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/XMindZen.java @@ -0,0 +1,66 @@ +package io.metersphere.functional.xmind.parser; + +import io.metersphere.sdk.util.JSON; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class XMindZen { + + /** + * 返回content.json 解析后的的json + */ + public static List<String> getContent(String jsonContent) { + List jsonArray = JSON.parseArray(jsonContent);//.getJSONObject(0); + List<String> contents = new ArrayList<>(); + for (Object object : jsonArray) { + Map<String, Map> jsonObject = (Map) object; + Map<String, Map> rootTopic = jsonObject.get("rootTopic"); + transferNotes(rootTopic); + Map children = rootTopic.get("children"); + recursionChildren(children); + contents.add(JSON.toJSONString(jsonObject)); + } + return contents; + } + + /** + * 递归转换children + * + */ + private static void recursionChildren(Map<String, List> children) { + if (children == null) { + return; + } + List<Map> attachedArray = children.get("attached"); + if (attachedArray == null) { + return; + } + for (Object attached : attachedArray) { + Map<String, Map> attachedObject = (Map) attached; + transferNotes(attachedObject); + Map<String, List> childrenObject = attachedObject.get("children"); + if (childrenObject == null) { + continue; + } + recursionChildren(childrenObject); + } + } + + private static void transferNotes(Map object) { + Map notes = (Map) object.get("notes"); + if (notes == null) { + return; + } + Map plain = (Map) notes.get("plain"); + if (plain != null) { + String content = plain.get("content").toString(); + notes.remove("plain"); + notes.put("content", content); + } else { + notes.put("content", null); + } + } + +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/ZipUtils.java b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/ZipUtils.java new file mode 100644 index 0000000000..7038d43346 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/parser/ZipUtils.java @@ -0,0 +1,84 @@ +package io.metersphere.functional.xmind.parser; + +import io.metersphere.sdk.util.Translator; +import org.apache.commons.compress.archivers.ArchiveException; +import org.apache.commons.compress.archivers.examples.Expander; + +import java.io.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * @Description zip解压工具 + */ +public class ZipUtils { + + private static final String CURRENT_PATH = System.getProperty("user.dir"); + + /** + * 找到压缩文件中匹配的子文件,返回的为 getContents("comments.xml, unzip + * + */ + public static Map<String, String> getContents(List<String> subFileNames, String extractFileDir) + throws IOException { + Map<String, String> map = new HashMap<>(16); + File destFile = new File(extractFileDir); + if (destFile.isDirectory()) { + String[] res = destFile.list(new FileFilter()); + for (int i = 0; i < Objects.requireNonNull(res).length; i++) { + if (subFileNames.contains(res[i])) { + String s = extractFileDir + File.separator + res[i]; + String content = getFileContent(s); + map.put(res[i], content); + } + } + } + return map; + } + + /** + * 返回解压后的文件夹名字 + * + */ + public static String extract(File file) throws IOException, ArchiveException { + Expander expander = new Expander(); + String destFileName = CURRENT_PATH + File.separator + "XMind" + System.currentTimeMillis(); + expander.expand(file, new File(destFileName)); + return destFileName; + } + + /** + * 这是一个内部类过滤器,策略模式 + */ + static class FileFilter implements FilenameFilter { + @Override + public boolean accept(File dir, String name) { + // String的 endsWith(String str)方法 筛选出以str结尾的字符串 + return name.endsWith(".xml") || name.endsWith(".json"); + } + } + + public static String getFileContent(String fileName) throws IOException { + File file; + try { + file = new File(fileName); + } catch (Exception e) { + throw new RuntimeException(Translator.get("case.find_file_error")); + } + FileReader fileReader = new FileReader(file); + BufferedReader bufferedReader = new BufferedReader(fileReader); + StringBuilder stringBuffer = new StringBuilder(); + while (bufferedReader.ready()) { + if(!stringBuffer.isEmpty()){ + stringBuffer.append("\r\n"); + } + stringBuffer.append(bufferedReader.readLine()); + } + // 打开的文件需关闭,在unix下可以删除,否则在windows下不能删除(file.delete()) + bufferedReader.close(); + fileReader.close(); + return stringBuffer.toString(); + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Attached.java b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Attached.java new file mode 100644 index 0000000000..12dc4d34b7 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Attached.java @@ -0,0 +1,22 @@ + +package io.metersphere.functional.xmind.pojo; + +import lombok.Data; + +import java.util.List; + +/** + * XMind 节点对象 + */ +@Data +public class Attached { + + private String id; + private String title; + private Notes notes; + private String path; + private Attached parent; + private List<Comments> comments; + private Children children; + +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Children.java b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Children.java new file mode 100644 index 0000000000..f457a03ec5 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Children.java @@ -0,0 +1,12 @@ +package io.metersphere.functional.xmind.pojo; + +import lombok.Data; + +import java.util.List; + +@Data +public class Children { + + private List<Attached> attached; + +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Comments.java b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Comments.java new file mode 100644 index 0000000000..09806860fa --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Comments.java @@ -0,0 +1,12 @@ +package io.metersphere.functional.xmind.pojo; + +import lombok.Data; + +@Data +public class Comments { + + private long creationTime; + private String author; + private String content; + +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/JsonRootBean.java b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/JsonRootBean.java new file mode 100644 index 0000000000..a929c0e0c1 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/JsonRootBean.java @@ -0,0 +1,12 @@ +package io.metersphere.functional.xmind.pojo; + +import lombok.Data; + +@Data +public class JsonRootBean { + + private String id; + private String title; + private RootTopic rootTopic; + +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Notes.java b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Notes.java new file mode 100644 index 0000000000..db8630e580 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/Notes.java @@ -0,0 +1,10 @@ +package io.metersphere.functional.xmind.pojo; + +import lombok.Data; + +@Data +public class Notes { + + private String content; + +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/RootTopic.java b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/RootTopic.java new file mode 100644 index 0000000000..4a606da6d9 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/pojo/RootTopic.java @@ -0,0 +1,16 @@ +package io.metersphere.functional.xmind.pojo; + +import lombok.Data; + +import java.util.List; + +@Data +public class RootTopic { + + private String id; + private String title; + private Notes notes; + private List<Comments> comments; + private Children children; + +} diff --git a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseControllerTests.java b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseControllerTests.java index f13dd64ac5..690347ffd9 100644 --- a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseControllerTests.java +++ b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseControllerTests.java @@ -90,6 +90,7 @@ public class FunctionalCaseControllerTests extends BaseTest { public static final String DOWNLOAD_XMIND_TEMPLATE_URL = "/functional/case/download/xmind/template/"; public static final String EXPORT_COLUMNS_URL = "/functional/case/export/columns/"; public static final String DOWNLOAD_FILE_URL = "/functional/case/download/file/"; + public static final String STOP_EXPORT_URL = "/functional/case/stop/"; @Resource private NotificationMapper notificationMapper; @@ -858,4 +859,10 @@ public class FunctionalCaseControllerTests extends BaseTest { .header(SessionConstants.HEADER_TOKEN, sessionId) .header(SessionConstants.CSRF_TOKEN, csrfToken)); } + + @Test + @Order(25) + public void stopExport() throws Exception { + this.requestGetExcel(STOP_EXPORT_URL + DEFAULT_PROJECT_ID); + } } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/constants/ExportConstants.java b/backend/services/system-setting/src/main/java/io/metersphere/system/constants/ExportConstants.java new file mode 100644 index 0000000000..08139a15f4 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/constants/ExportConstants.java @@ -0,0 +1,12 @@ +package io.metersphere.system.constants; + +public class ExportConstants { + + public enum ExportType { + API, CASE + } + + public enum ExportState { + PREPARED, STOP, SUCCESS, ERROR + } +} 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 new file mode 100644 index 0000000000..ea39a82f14 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/manager/ExportTaskManager.java @@ -0,0 +1,94 @@ +package io.metersphere.system.manager; + +import io.metersphere.functional.domain.ExportTask; +import io.metersphere.functional.mapper.ExportTaskMapper; +import io.metersphere.sdk.constants.KafkaTopicConstants; +import io.metersphere.sdk.util.JSON; +import io.metersphere.sdk.util.LogUtils; +import io.metersphere.system.constants.ExportConstants; +import io.metersphere.system.uid.IDGenerator; +import jakarta.annotation.Resource; +import org.apache.commons.lang3.StringUtils; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.function.Function; + +@Service +public class ExportTaskManager { + + @Resource + private KafkaTemplate<String, String> kafkaTemplate; + @Resource + private ExportTaskMapper exportTaskMapper; + + public static Map<String, Future<?>> map = new ConcurrentHashMap<>(); + public static final String EXPORT_CONSUME = "export_consume"; + + + public <T> void exportAsyncTask(String userId, String type, T t, Function<Object, Object> selectListFunc) throws InterruptedException { + ExecutorService executorService = Executors.newFixedThreadPool(1); + Future<?> future = executorService.submit(() -> { + while (!Thread.currentThread().isInterrupted()) { + // 线程任务逻辑 + LogUtils.info("Thread has been start."); + selectListFunc.apply(t); + } + LogUtils.info("Thread has been interrupted."); + }); + Thread.sleep(6000); + ExportTask exportTask = buildExportTask(userId, type); + map.put(exportTask.getId(), future); + } + + private ExportTask buildExportTask(String userId, String type) { + ExportTask exportTask = new ExportTask(); + exportTask.setId(IDGenerator.nextStr()); + exportTask.setType(type); + exportTask.setCreateUser(userId); + exportTask.setCreateTime(System.currentTimeMillis()); + exportTask.setState(ExportConstants.ExportState.PREPARED.toString()); + exportTask.setUpdateUser(userId); + exportTask.setUpdateTime(System.currentTimeMillis()); + exportTaskMapper.insert(exportTask); + return exportTask; + } + + public void sendStopMessage(String id, String userId) { + ExportTask exportTask = new ExportTask(); + exportTask.setId(id); + exportTask.setState(ExportConstants.ExportState.STOP.toString()); + exportTask.setUpdateUser(userId); + exportTask.setUpdateTime(System.currentTimeMillis()); + kafkaTemplate.send(KafkaTopicConstants.EXPORT, JSON.toJSONString(exportTask)); + } + + @KafkaListener(id=EXPORT_CONSUME, topics = KafkaTopicConstants.EXPORT, groupId = EXPORT_CONSUME + "_" + "${random.uuid}") + public void stop(ConsumerRecord<?, String> record) { + LogUtils.info("Service consume platform_plugin message: " + record.value()); + ExportTask exportTask = JSON.parseObject(record.value(), ExportTask.class); + if (exportTask!=null && StringUtils.isNotBlank(exportTask.getId())) { + String id = exportTask.getId(); + map.get(id).cancel(true); + map.remove(id); + exportTaskMapper.updateByPrimaryKey(exportTask); + } + } + + @Scheduled(fixedDelay = 10000) + public void checkStop() { + for (String next : map.keySet()) { + if (map.get(next) != null && map.get(next).isDone()) { + map.remove(next); + } + } + } +}