diff --git a/backend/framework/sdk/pom.xml b/backend/framework/sdk/pom.xml
index 6bdb841b3c..fe22f7a8ef 100644
--- a/backend/framework/sdk/pom.xml
+++ b/backend/framework/sdk/pom.xml
@@ -34,6 +34,12 @@
org.apache.httpcomponents.client5
httpclient5
+
+
+ com.github.eljah
+ xmindjbehaveplugin
+ ${xmindjbehaveplugin.version}
+
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 8d028c8793..38972ba026 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
@@ -256,4 +256,14 @@ case.review.status.re_reviewed=Re reviewed
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
\ No newline at end of file
+functional_case_review_comment_template=[Review comment:%s %s(%s)]\n%s\n
+functional_case_xmind_template=Functional case xmind template
+download_template_failed=Download template failed
+functional_case=Functional case
+xmind_prerequisite=Prerequisite
+xmind_description=Remark
+xmind_tags=tags
+xmind_textDescription=Text description
+xmind_expectedResult=Expected result
+xmind_step=Step
+xmind_stepDescription=Step description
\ 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 ca8855aa06..1eacd1ed3e 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
@@ -254,4 +254,14 @@ case.review.status.re_reviewed=重新提审
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
\ No newline at end of file
+functional_case_review_comment_template=【评审评论:%s %s(%s)】\n%s\n
+functional_case_xmind_template=思维导图用例模版
+download_template_failed=下载思维导图模版失败
+functional_case=功能用例
+xmind_prerequisite=前置条件
+xmind_description=备注
+xmind_tags=标签
+xmind_textDescription=文本描述
+xmind_expectedResult=预期结果
+xmind_step=用例步骤
+xmind_stepDescription=步骤描述
\ 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 d8985e485a..6f56c26b06 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
@@ -256,4 +256,13 @@ 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
-
+functional_case_xmind_template=思維導圖用例模板
+download_template_failed=下載思維導圖模板失敗
+functional_case=功能用例
+xmind_prerequisite=前置條件
+xmind_description=備注
+xmind_tags=標簽
+xmind_textDescription=文本描述
+xmind_expectedResult=預期結果
+xmind_step=用例步驟
+xmind_stepDescription=步驟描述
\ 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 af0992ab30..398de21753 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
@@ -9,10 +9,7 @@ import io.metersphere.functional.dto.FunctionalCasePageDTO;
import io.metersphere.functional.dto.FunctionalCaseVersionDTO;
import io.metersphere.functional.dto.response.FunctionalCaseImportResponse;
import io.metersphere.functional.request.*;
-import io.metersphere.functional.service.FunctionalCaseFileService;
-import io.metersphere.functional.service.FunctionalCaseLogService;
-import io.metersphere.functional.service.FunctionalCaseNoticeService;
-import io.metersphere.functional.service.FunctionalCaseService;
+import io.metersphere.functional.service.*;
import io.metersphere.project.dto.CustomFieldOptions;
import io.metersphere.project.service.ProjectTemplateService;
import io.metersphere.sdk.constants.PermissionConstants;
@@ -33,7 +30,6 @@ import io.metersphere.system.utils.SessionUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
-import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotBlank;
import org.apache.shiro.authz.annotation.Logical;
@@ -60,6 +56,8 @@ public class FunctionalCaseController {
private ProjectTemplateService projectTemplateService;
@Resource
private FunctionalCaseFileService functionalCaseFileService;
+ @Resource
+ private FunctionalCaseXmindService functionalCaseXmindService;
//TODO 获取模板列表(多模板功能暂时不做)
@@ -257,4 +255,12 @@ public class FunctionalCaseController {
public void testCaseExport(@Validated @RequestBody FunctionalCaseExportRequest request) {
functionalCaseFileService.exportFunctionalCaseZip(request);
}
+
+ @GetMapping("/download/xmind/template/{projectId}")
+ @Operation(summary = "用例管理-功能用例-xmind导入-下载模板")
+ @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ)
+ @CheckOwner(resourceId = "#projectId", resourceType = "project")
+ public void xmindTemplateExport(@PathVariable String projectId, HttpServletResponse response) {
+ functionalCaseXmindService.downloadXmindTemplate(projectId, response);
+ }
}
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 e7ec14c5e4..38fff91237 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
@@ -280,7 +280,7 @@ public class FunctionalCaseFileService {
}
}
- private List getCustomFields(String projectId) {
+ public List getCustomFields(String projectId) {
TemplateDTO defaultTemplateDTO = projectTemplateService.getDefaultTemplateDTO(projectId, TemplateScene.FUNCTIONAL.name());
List customFields = Optional.ofNullable(defaultTemplateDTO.getCustomFields()).orElse(new ArrayList<>());
return customFields;
diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseXmindService.java b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseXmindService.java
new file mode 100644
index 0000000000..ca963fb38e
--- /dev/null
+++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseXmindService.java
@@ -0,0 +1,60 @@
+package io.metersphere.functional.service;
+
+import io.metersphere.functional.xmind.domain.FunctionalCaseXmindData;
+import io.metersphere.functional.xmind.utils.XmindExportUtil;
+import io.metersphere.sdk.exception.MSException;
+import io.metersphere.sdk.util.JSON;
+import io.metersphere.sdk.util.LogUtils;
+import io.metersphere.sdk.util.Translator;
+import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.collections.CollectionUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * @author wx
+ */
+@Service
+@Transactional(rollbackFor = Exception.class)
+public class FunctionalCaseXmindService {
+
+ public static final String template = "/template/template.json";
+
+ @Resource
+ private FunctionalCaseFileService functionalCaseFileService;
+
+
+ public void downloadXmindTemplate(String projectId, HttpServletResponse response) {
+ List customFields = functionalCaseFileService.getCustomFields(projectId);
+ try (InputStream stream = FunctionalCaseXmindService.class.getResourceAsStream(template)) {
+ FunctionalCaseXmindData functionalCaseXmindData = JSON.parseObject(stream, FunctionalCaseXmindData.class);
+ setTemplateCustomFields(functionalCaseXmindData.getChildren(), customFields);
+
+ XmindExportUtil.downloadTemplate(response, functionalCaseXmindData, true);
+
+ } catch (Exception e) {
+ LogUtils.error(e.getMessage());
+ throw new MSException(Translator.get("download_template_failed"));
+ }
+ }
+
+ private void setTemplateCustomFields(List children, List customFields) {
+ if (CollectionUtils.isNotEmpty(children)) {
+ children.forEach(data -> {
+ data.getFunctionalCaseList().forEach(item -> {
+ item.setTemplateCustomFieldDTOList(customFields);
+ });
+ if (CollectionUtils.isNotEmpty(data.getChildren())) {
+ setTemplateCustomFields(data.getChildren(), customFields);
+ }
+ });
+ }
+ }
+
+
+}
diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/domain/FunctionalCaseXmindDTO.java b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/domain/FunctionalCaseXmindDTO.java
new file mode 100644
index 0000000000..277542a347
--- /dev/null
+++ b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/domain/FunctionalCaseXmindDTO.java
@@ -0,0 +1,54 @@
+package io.metersphere.functional.xmind.domain;
+
+import io.metersphere.functional.dto.FunctionalCaseCustomFieldDTO;
+import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author wx
+ */
+@Data
+public class FunctionalCaseXmindDTO {
+
+ @Schema(description = "ID")
+ private String id;
+
+ @Schema(description = "业务ID")
+ private String num;
+
+ @Schema(description = "项目ID")
+ private String projectId;
+
+ @Schema(description = "名称")
+ private String name;
+
+ @Schema(description = "标签(JSON)")
+ private String tags;
+
+ @Schema(description = "编辑模式:步骤模式/文本模式")
+ private String caseEditType;
+
+ @Schema(description = "用例步骤(JSON),step_model 为 Step 时启用")
+ private String steps;
+
+ @Schema(description = "步骤描述,step_model 为 Text 时启用")
+ private String textDescription;
+
+ @Schema(description = "预期结果,step_model 为 Text 时启用")
+ private String expectedResult;
+
+ @Schema(description = "前置条件")
+ private String prerequisite;
+
+ @Schema(description = "备注")
+ private String description;
+
+ @Schema(description = "自定义字段")
+ private List customFieldDTOList;
+
+ @Schema(description = "模板自定义字段")
+ private List templateCustomFieldDTOList;
+}
diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/domain/FunctionalCaseXmindData.java b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/domain/FunctionalCaseXmindData.java
new file mode 100644
index 0000000000..3b1ce2ffc8
--- /dev/null
+++ b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/domain/FunctionalCaseXmindData.java
@@ -0,0 +1,24 @@
+package io.metersphere.functional.xmind.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author wx
+ */
+@Data
+public class FunctionalCaseXmindData implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private List functionalCaseList;
+ private String moduleName;
+ private String moduleId;
+ private List children = new ArrayList<>();
+
+
+}
diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/utils/XmindExportUtil.java b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/utils/XmindExportUtil.java
new file mode 100644
index 0000000000..4e9711d6bd
--- /dev/null
+++ b/backend/services/case-management/src/main/java/io/metersphere/functional/xmind/utils/XmindExportUtil.java
@@ -0,0 +1,334 @@
+package io.metersphere.functional.xmind.utils;
+
+import io.metersphere.functional.constants.FunctionalCaseTypeConstants;
+import io.metersphere.functional.xmind.domain.FunctionalCaseXmindDTO;
+import io.metersphere.functional.xmind.domain.FunctionalCaseXmindData;
+import io.metersphere.sdk.exception.MSException;
+import io.metersphere.sdk.util.JSON;
+import io.metersphere.sdk.util.LogUtils;
+import io.metersphere.sdk.util.Translator;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.xmind.core.*;
+import org.xmind.core.style.IStyle;
+import org.xmind.core.style.IStyleSheet;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author wx
+ */
+public class XmindExportUtil {
+
+
+ /**
+ * 下载xmind模板
+ *
+ * @param response
+ * @param caseData
+ * @param template
+ */
+ public static void downloadTemplate(HttpServletResponse response, FunctionalCaseXmindData caseData, boolean template) {
+ IWorkbook workBook = createXmindByCaseData(caseData, template);
+
+ response.setContentType("application/octet-stream");
+ response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+ try {
+ response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(Translator.get("functional_case_xmind_template"), StandardCharsets.UTF_8.name()) + ".xmind");
+ workBook.save(response.getOutputStream());
+ } catch (UnsupportedEncodingException e) {
+ LogUtils.error(e.getMessage(), e);
+ throw new MSException("Utf-8 encoding is not supported");
+ } catch (Exception e) {
+ LogUtils.error(e.getMessage(), e);
+ throw new MSException("IO exception");
+ }
+ }
+
+ private static IWorkbook createXmindByCaseData(FunctionalCaseXmindData caseData, boolean template) {
+ // 创建思维导图的工作空间
+ IWorkbookBuilder workbookBuilder = Core.getWorkbookBuilder();
+ IWorkbook workbook = workbookBuilder.createWorkbook();
+
+ Map styleMap = initTheme(workbook);
+
+ // 获得默认sheet
+ ISheet primarySheet = workbook.getPrimarySheet();
+ if (styleMap.containsKey("mapStyle")) {
+ primarySheet.setStyleId(styleMap.get("mapStyle").getId());
+ }
+ // 获得根主题
+ ITopic rootTopic = primarySheet.getRootTopic();
+ if (styleMap.containsKey("centralTopicStyle")) {
+ rootTopic.setStyleId(styleMap.get("centralTopicStyle").getId());
+ }
+ // 设置根主题的标题
+ rootTopic.setTitleText(Translator.get("functional_case"));
+
+ if (CollectionUtils.isNotEmpty(caseData.getChildren())) {
+ for (FunctionalCaseXmindData data : caseData.getChildren()) {
+ addItemTopic(rootTopic, workbook, styleMap, data, true, template);
+ }
+ }
+ return workbook;
+ }
+
+ private static void addItemTopic(ITopic parentTpoic, IWorkbook workbook, Map styleMap, FunctionalCaseXmindData xmindData, boolean isFirstLevel, boolean template) {
+ ITopic topic = workbook.createTopic();
+ topic.setTitleText(xmindData.getModuleName());
+ if (isFirstLevel) {
+ if (styleMap.containsKey("mainTopicStyle")) {
+ topic.setStyleId(styleMap.get("mainTopicStyle").getId());
+ }
+ } else {
+ if (styleMap.containsKey("subTopicStyle")) {
+ topic.setStyleId(styleMap.get("subTopicStyle").getId());
+ }
+ }
+ parentTpoic.add(topic);
+
+ if (CollectionUtils.isNotEmpty(xmindData.getFunctionalCaseList())) {
+ IStyle style = null;
+ if (styleMap.containsKey("subTopicStyle")) {
+ style = styleMap.get("subTopicStyle");
+ }
+ for (FunctionalCaseXmindDTO dto : xmindData.getFunctionalCaseList()) {
+ // 创建小节节点
+ ITopic itemTopic = workbook.createTopic();
+ if (style != null) {
+ itemTopic.setStyleId(style.getId());
+ }
+ if (template) {
+ // 模板
+ buildTemplateTopic(topic, style, dto, itemTopic, workbook);
+ }
+ }
+ }
+
+ if (CollectionUtils.isNotEmpty(xmindData.getChildren())) {
+ for (FunctionalCaseXmindData data : xmindData.getChildren()) {
+ addItemTopic(topic, workbook, styleMap, data, false, template);
+ }
+ }
+ }
+
+ private static void buildTemplateTopic(ITopic topic, IStyle style, FunctionalCaseXmindDTO dto, ITopic itemTopic, IWorkbook workbook) {
+
+ //用例名称
+ itemTopic.setTitleText("case-P0: " + dto.getName());
+
+ //前置条件
+ if (StringUtils.isNotBlank(dto.getPrerequisite())) {
+ ITopic preTopic = workbook.createTopic();
+ preTopic.setTitleText(Translator.get("xmind_prerequisite") + ": " + dto.getPrerequisite());
+ if (style != null) {
+ preTopic.setStyleId(style.getId());
+ }
+ itemTopic.add(preTopic, ITopic.ATTACHED);
+ }
+
+ //备注
+ if (StringUtils.isNotBlank(dto.getDescription())) {
+ ITopic deTopic = workbook.createTopic();
+ deTopic.setTitleText(Translator.get("xmind_description") + ": " + dto.getDescription());
+ if (style != null) {
+ deTopic.setStyleId(style.getId());
+ }
+ itemTopic.add(deTopic, ITopic.ATTACHED);
+ }
+
+ //标签
+ if (StringUtils.isNotBlank(dto.getTags())) {
+ try {
+ List arr = JSON.parseArray(dto.getTags());
+ String tagStr = StringUtils.EMPTY;
+ for (int i = 0; i < arr.size(); i++) {
+ tagStr = tagStr + arr.get(i) + "|";
+ }
+ if (tagStr.endsWith("|")) {
+ tagStr = tagStr.substring(0, tagStr.length() - 1);
+ }
+ ITopic tagTopic = workbook.createTopic();
+ tagTopic.setTitleText(Translator.get("xmind_tags") + ":" + tagStr);
+ if (style != null) {
+ tagTopic.setStyleId(style.getId());
+ }
+ itemTopic.add(tagTopic, ITopic.ATTACHED);
+ } catch (Exception e) {
+ }
+ }
+
+ if (StringUtils.equalsIgnoreCase(dto.getCaseEditType(), FunctionalCaseTypeConstants.CaseEditType.TEXT.name())) {
+ //文本描述
+ ITopic textDesTopic = workbook.createTopic();
+ String desc = dto.getTextDescription();
+ textDesTopic.setTitleText(desc == null ? Translator.get("xmind_textDescription") + ": " : Translator.get("xmind_textDescription") + ": " + desc);
+ if (style != null) {
+ textDesTopic.setStyleId(style.getId());
+ }
+
+ String result = dto.getExpectedResult();
+ ITopic resultTopic = workbook.createTopic();
+ resultTopic.setTitleText(result == null ? Translator.get("xmind_expectedResult") + ": " : Translator.get("xmind_expectedResult") + ": " + result);
+ if (style != null) {
+ resultTopic.setStyleId(style.getId());
+ }
+ textDesTopic.add(resultTopic, ITopic.ATTACHED);
+
+ if (StringUtils.isNotEmpty(desc) || StringUtils.isNotEmpty(result)) {
+ itemTopic.add(textDesTopic, ITopic.ATTACHED);
+ }
+ } else {
+ //步骤描述
+ try {
+ ITopic stepDesTopic = workbook.createTopic();
+ stepDesTopic.setTitleText(Translator.get("xmind_stepDescription"));
+ if (style != null) {
+ stepDesTopic.setStyleId(style.getId());
+ }
+ List