feat(测试用例): 用例脑图导出

This commit is contained in:
WangXu10 2024-08-08 10:34:44 +08:00 committed by Craftsman
parent 781d59d649
commit 495f4c9463
14 changed files with 576 additions and 77 deletions

View File

@ -1,12 +1,15 @@
package io.metersphere.functional.domain;
import io.metersphere.validation.groups.*;
import io.metersphere.validation.groups.Created;
import io.metersphere.validation.groups.Updated;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import lombok.Data;
@Data
public class ExportTask implements Serializable {
@ -43,6 +46,11 @@ public class ExportTask implements Serializable {
@Schema(description = "创建时间")
private Long updateTime;
@Schema(description = "项目id", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{export_task.project_id.not_blank}", groups = {Created.class})
@Size(min = 1, max = 50, message = "{export_task.project_id.length_range}", groups = {Created.class, Updated.class})
private String projectId;
private static final long serialVersionUID = 1L;
public enum Column {
@ -54,7 +62,8 @@ public class ExportTask implements Serializable {
createUser("create_user", "createUser", "VARCHAR", false),
createTime("create_time", "createTime", "BIGINT", false),
updateUser("update_user", "updateUser", "VARCHAR", false),
updateTime("update_time", "updateTime", "BIGINT", false);
updateTime("update_time", "updateTime", "BIGINT", false),
projectId("project_id", "projectId", "VARCHAR", false);
private static final String BEGINNING_DELIMITER = "`";

View File

@ -713,6 +713,76 @@ public class ExportTaskExample {
addCriterion("update_time not between", value1, value2, "updateTime");
return (Criteria) this;
}
public Criteria andProjectIdIsNull() {
addCriterion("project_id is null");
return (Criteria) this;
}
public Criteria andProjectIdIsNotNull() {
addCriterion("project_id is not null");
return (Criteria) this;
}
public Criteria andProjectIdEqualTo(String value) {
addCriterion("project_id =", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdNotEqualTo(String value) {
addCriterion("project_id <>", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdGreaterThan(String value) {
addCriterion("project_id >", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdGreaterThanOrEqualTo(String value) {
addCriterion("project_id >=", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdLessThan(String value) {
addCriterion("project_id <", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdLessThanOrEqualTo(String value) {
addCriterion("project_id <=", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdLike(String value) {
addCriterion("project_id like", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdNotLike(String value) {
addCriterion("project_id not like", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdIn(List<String> values) {
addCriterion("project_id in", values, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdNotIn(List<String> values) {
addCriterion("project_id not in", values, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdBetween(String value1, String value2) {
addCriterion("project_id between", value1, value2, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdNotBetween(String value1, String value2) {
addCriterion("project_id not between", value1, value2, "projectId");
return (Criteria) this;
}
}
public static class Criteria extends GeneratedCriteria {

View File

@ -11,6 +11,7 @@
<result column="create_time" jdbcType="BIGINT" property="createTime" />
<result column="update_user" jdbcType="VARCHAR" property="updateUser" />
<result column="update_time" jdbcType="BIGINT" property="updateTime" />
<result column="project_id" jdbcType="VARCHAR" property="projectId" />
</resultMap>
<sql id="Example_Where_Clause">
<where>
@ -71,7 +72,8 @@
</where>
</sql>
<sql id="Base_Column_List">
id, `name`, `type`, fileId, `state`, create_user, create_time, update_user, update_time
id, `name`, `type`, fileId, `state`, create_user, create_time, update_user, update_time,
project_id
</sql>
<select id="selectByExample" parameterType="io.metersphere.functional.domain.ExportTaskExample" resultMap="BaseResultMap">
select
@ -106,12 +108,12 @@
<insert id="insert" parameterType="io.metersphere.functional.domain.ExportTask">
insert into export_task (id, `name`, `type`,
fileId, `state`, create_user,
create_time, update_user, update_time
)
create_time, update_user, update_time,
project_id)
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}
)
#{createTime,jdbcType=BIGINT}, #{updateUser,jdbcType=VARCHAR}, #{updateTime,jdbcType=BIGINT},
#{projectId,jdbcType=VARCHAR})
</insert>
<insert id="insertSelective" parameterType="io.metersphere.functional.domain.ExportTask">
insert into export_task
@ -143,6 +145,9 @@
<if test="updateTime != null">
update_time,
</if>
<if test="projectId != null">
project_id,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
@ -172,6 +177,9 @@
<if test="updateTime != null">
#{updateTime,jdbcType=BIGINT},
</if>
<if test="projectId != null">
#{projectId,jdbcType=VARCHAR},
</if>
</trim>
</insert>
<select id="countByExample" parameterType="io.metersphere.functional.domain.ExportTaskExample" resultType="java.lang.Long">
@ -210,6 +218,9 @@
<if test="record.updateTime != null">
update_time = #{record.updateTime,jdbcType=BIGINT},
</if>
<if test="record.projectId != null">
project_id = #{record.projectId,jdbcType=VARCHAR},
</if>
</set>
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
@ -225,7 +236,8 @@
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_time = #{record.updateTime,jdbcType=BIGINT},
project_id = #{record.projectId,jdbcType=VARCHAR}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
@ -257,6 +269,9 @@
<if test="updateTime != null">
update_time = #{updateTime,jdbcType=BIGINT},
</if>
<if test="projectId != null">
project_id = #{projectId,jdbcType=VARCHAR},
</if>
</set>
where id = #{id,jdbcType=VARCHAR}
</update>
@ -269,19 +284,20 @@
create_user = #{createUser,jdbcType=VARCHAR},
create_time = #{createTime,jdbcType=BIGINT},
update_user = #{updateUser,jdbcType=VARCHAR},
update_time = #{updateTime,jdbcType=BIGINT}
update_time = #{updateTime,jdbcType=BIGINT},
project_id = #{projectId,jdbcType=VARCHAR}
where id = #{id,jdbcType=VARCHAR}
</update>
<insert id="batchInsert" parameterType="map">
insert into export_task
(id, `name`, `type`, fileId, `state`, create_user, create_time, update_user, update_time
)
(id, `name`, `type`, fileId, `state`, create_user, create_time, update_user, update_time,
project_id)
values
<foreach collection="list" item="item" separator=",">
(#{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}
)
#{item.createTime,jdbcType=BIGINT}, #{item.updateUser,jdbcType=VARCHAR}, #{item.updateTime,jdbcType=BIGINT},
#{item.projectId,jdbcType=VARCHAR})
</foreach>
</insert>
<insert id="batchInsertSelective" parameterType="map">
@ -321,6 +337,9 @@
<if test="'update_time'.toString() == column.value">
#{item.updateTime,jdbcType=BIGINT}
</if>
<if test="'project_id'.toString() == column.value">
#{item.projectId,jdbcType=VARCHAR}
</if>
</foreach>
)
</foreach>

View File

@ -12,6 +12,7 @@ CREATE TABLE export_task(
`name` VARCHAR(255) COMMENT '名称' ,
`type` VARCHAR(50) NOT NULL COMMENT '资源类型' ,
`fileId` VARCHAR(255) COMMENT '文件id' ,
`project_id` VARCHAR(255) NOT NULL COMMENT '项目id' ,
`state` VARCHAR(50) NOT NULL COMMENT '状态' ,
`create_user` VARCHAR(50) NOT NULL COMMENT '创建人' ,
`create_time` BIGINT NOT NULL COMMENT '创建时间' ,
@ -24,6 +25,7 @@ CREATE TABLE export_task(
CREATE INDEX idx_create_user ON export_task(`create_user`);
CREATE INDEX idx_project_id ON export_task(`project_id`);
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`);

View File

@ -258,12 +258,12 @@ public class FunctionalCaseController {
functionalCaseFileService.export(SessionUtils.getUserId(), request);
}
@GetMapping("/stop/{projectId}")
@GetMapping("/stop/{taskId}")
@Operation(summary = "用例管理-功能用例-导出-停止导出")
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_EXPORT)
@CheckOwner(resourceId = "#projectId", resourceType = "project")
public void caseStopExport(@PathVariable String projectId) {
functionalCaseFileService.stopExport(projectId, SessionUtils.getUserId());
public void caseStopExport(@PathVariable String taskId) {
functionalCaseFileService.stopExport(taskId, SessionUtils.getUserId());
}
@GetMapping("/download/xmind/template/{projectId}")
@ -289,4 +289,12 @@ public class FunctionalCaseController {
public ResponseEntity<byte[]> downloadImgById(@PathVariable String projectId, @PathVariable String fileId) {
return functionalCaseFileService.downloadFile(projectId, fileId);
}
@PostMapping("/export/xmind")
@Operation(summary = "用例管理-功能用例-xmind导出")
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_EXPORT)
public void caseExportXmind(@Validated @RequestBody FunctionalCaseExportRequest request) {
functionalCaseXmindService.exportFunctionalCaseXmind(request, SessionUtils.getUserId());
}
}

View File

@ -64,10 +64,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Serial;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
@ -104,11 +102,12 @@ public class FunctionalCaseFileService {
private FunctionalCaseLogService functionalCaseLogService;
@Resource
private SystemParameterMapper systemParameterMapper;
private static final String EXPORT_FILE_NAME = "case_export";
@Resource
private ExportTaskManager exportTaskManager;
@Resource
private ExportTaskMapper exportTaskMapper;
private static final String XMIND = ".xmind";
private static final String XLSX = ".xlsx";
/**
* 下载excel导入模板
@ -341,7 +340,7 @@ public class FunctionalCaseFileService {
if (preparedCount > 0) {
throw new MSException(Translator.get("export_case_task_existed"));
}
exportTaskManager.exportAsyncTask(userId, ExportConstants.ExportType.CASE.toString(), request, t->exportFunctionalCaseZip(request));
exportTaskManager.exportAsyncTask(request.getProjectId(), userId, ExportConstants.ExportType.CASE.toString(), request, t -> exportFunctionalCaseZip(request, userId));
} catch (InterruptedException e) {
LogUtils.error("导出失败:" + e);
throw new MSException(e);
@ -354,7 +353,7 @@ public class FunctionalCaseFileService {
*
* @param request
*/
public String exportFunctionalCaseZip(FunctionalCaseExportRequest request) {
public String exportFunctionalCaseZip(FunctionalCaseExportRequest request, String userId) {
File tmpDir = null;
Project project = projectMapper.selectByPrimaryKey(request.getProjectId());
try {
@ -375,11 +374,11 @@ public class FunctionalCaseFileService {
uploadFileToMinio(singeFile, request.getFileId());
}
functionalCaseLogService.exportExcelLog(request);
List<ExportTask> exportTasks = getExportTasks();
List<ExportTask> exportTasks = getExportTasks(request.getProjectId(), userId);
String taskId;
if (CollectionUtils.isNotEmpty(exportTasks)) {
taskId = exportTasks.getFirst().getId();
updateExportTask(ExportConstants.ExportState.SUCCESS.toString(), taskId);
updateExportTask(ExportConstants.ExportState.SUCCESS.name(), taskId, request.getFileId());
} else {
taskId = MsgType.CONNECT.name();
}
@ -387,9 +386,9 @@ public class FunctionalCaseFileService {
socketMsgDTO.setReportId(request.getFileId());
ExportWebSocketHandler.sendMessageSingle(socketMsgDTO);
} catch (Exception e) {
List<ExportTask> exportTasks = getExportTasks();
List<ExportTask> exportTasks = getExportTasks(request.getProjectId(), userId);
if (CollectionUtils.isNotEmpty(exportTasks)) {
updateExportTask(ExportConstants.ExportState.SUCCESS.toString(), exportTasks.getFirst().getId());
updateExportTask(ExportConstants.ExportState.ERROR.name(), exportTasks.getFirst().getId(), request.getFileId());
}
LogUtils.error(e);
throw new MSException(e);
@ -397,23 +396,26 @@ public class FunctionalCaseFileService {
return null;
}
private List<ExportTask> getExportTasks() {
public List<ExportTask> getExportTasks(String projectId, String userId) {
ExportTaskExample exportTaskExample = new ExportTaskExample();
exportTaskExample.createCriteria().andTypeEqualTo(ExportConstants.ExportType.CASE.toString()).andStateEqualTo(ExportConstants.ExportState.PREPARED.toString());
exportTaskExample.createCriteria().andTypeEqualTo(ExportConstants.ExportType.CASE.toString()).andStateEqualTo(ExportConstants.ExportState.PREPARED.toString())
.andCreateUserEqualTo(userId).andProjectIdEqualTo(projectId);
exportTaskExample.setOrderByClause("create_time desc");
return exportTaskMapper.selectByExample(exportTaskExample);
}
private void updateExportTask(String state, String taskId) {
public void updateExportTask(String state, String taskId, String fileId) {
ExportTask exportTask = new ExportTask();
exportTask.setState(state);
exportTask.setFileid(fileId);
exportTask.setId(taskId);
exportTaskMapper.updateByPrimaryKey(exportTask);
exportTaskMapper.updateByPrimaryKeySelective(exportTask);
}
private void uploadFileToMinio(File file, String fileId) {
public void uploadFileToMinio(File file, String fileId) {
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(EXPORT_FILE_NAME);
fileRequest.setFolder(DefaultRepositoryDir.getExportExcelTempDir() + "/" + fileId);
fileRequest.setFileName(fileId);
fileRequest.setFolder(DefaultRepositoryDir.getExportExcelTempDir());
fileRequest.setStorage(StorageType.MINIO.name());
try {
FileInputStream inputStream = new FileInputStream(file);
@ -787,22 +789,33 @@ public class FunctionalCaseFileService {
Project project = projectMapper.selectByPrimaryKey(projectId);
byte[] bytes;
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(EXPORT_FILE_NAME);
fileRequest.setFolder(DefaultRepositoryDir.getExportExcelTempDir() + "/" + fileId);
fileRequest.setFileName(fileId);
fileRequest.setFolder(DefaultRepositoryDir.getExportExcelTempDir());
fileRequest.setStorage(StorageType.MINIO.name());
try {
bytes = fileService.download(fileRequest);
} catch (Exception e) {
throw new MSException("get file error");
}
String fileName = "";
if (StringUtils.endsWith(fileId, XMIND)) {
fileName = "Metersphere_case_" + project.getName() + XMIND;
}
if (StringUtils.endsWith(fileId, XLSX)) {
fileName = "Metersphere_case_" + project.getName() + XLSX;
}
try {
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + "Metersphere_case_" + project.getName() + "\"")
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()))
.body(bytes);
} catch (UnsupportedEncodingException e) {
throw new MSException("Utf-8 encoding is not supported");
}
}
public void stopExport(String projectId, String userId) {
exportTaskManager.sendStopMessage(projectId, userId);
public void stopExport(String taskId, String userId) {
exportTaskManager.sendStopMessage(taskId, userId);
}
}

View File

@ -1372,7 +1372,7 @@ public class FunctionalCaseService {
* @param ids
* @return
*/
private List<FunctionalCase> getCaseDataByIds(List<String> ids) {
public List<FunctionalCase> getCaseDataByIds(List<String> ids) {
FunctionalCaseExample example = new FunctionalCaseExample();
example.createCriteria().andIdIn(ids);
return functionalCaseMapper.selectByExample(example);

View File

@ -1,21 +1,41 @@
package io.metersphere.functional.service;
import io.metersphere.functional.domain.ExportTask;
import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.domain.FunctionalCaseBlob;
import io.metersphere.functional.domain.FunctionalCaseCustomField;
import io.metersphere.functional.request.FunctionalCaseExportRequest;
import io.metersphere.functional.socket.ExportWebSocketHandler;
import io.metersphere.functional.xmind.domain.FunctionalCaseXmindDTO;
import io.metersphere.functional.xmind.domain.FunctionalCaseXmindData;
import io.metersphere.functional.xmind.utils.XmindExportUtil;
import io.metersphere.sdk.constants.ModuleConstants;
import io.metersphere.sdk.constants.MsgType;
import io.metersphere.sdk.dto.SocketMsgDTO;
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.constants.ExportConstants;
import io.metersphere.system.dto.sdk.BaseTreeNode;
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
import io.metersphere.system.manager.ExportTaskManager;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author wx
@ -28,7 +48,17 @@ public class FunctionalCaseXmindService {
@Resource
private FunctionalCaseFileService functionalCaseFileService;
@Resource
private FunctionalCaseService functionalCaseService;
@Resource
private FunctionalCaseCustomFieldService functionalCaseCustomFieldService;
@Resource
private FunctionalCaseModuleService functionalCaseModuleService;
private static final String EXPORT_CASE_TMP_DIR = "tmp";
@Resource
private ExportTaskManager exportTaskManager;
@Resource
private FunctionalCaseLogService functionalCaseLogService;
public void downloadXmindTemplate(String projectId, HttpServletResponse response) {
List<TemplateCustomFieldDTO> customFields = functionalCaseFileService.getCustomFields(projectId);
@ -59,5 +89,141 @@ public class FunctionalCaseXmindService {
}
}
/**
* 导出xmind
*
* @param request
*/
public void exportFunctionalCaseXmind(FunctionalCaseExportRequest request, String userId) {
try {
exportTaskManager.exportAsyncTask(request.getProjectId(), userId, ExportConstants.ExportType.CASE.toString(), request, t -> exportXmind(request, userId));
} catch (InterruptedException e) {
LogUtils.error("导出失败:" + e);
throw new MSException(e);
}
}
private String exportXmind(FunctionalCaseExportRequest request, String userId) {
//获取导出的ids集合
List<String> ids = functionalCaseService.doSelectIds(request, request.getProjectId());
if (CollectionUtils.isEmpty(ids)) {
return null;
}
try {
FunctionalCaseXmindData xmindData = buildXmindData(ids, request);
File tmpFile = new File(getClass().getClassLoader().getResource(StringUtils.EMPTY).getPath() +
File.separatorChar + EXPORT_CASE_TMP_DIR + "_" + IDGenerator.nextStr() + ".xmind");
List<TemplateCustomFieldDTO> templateCustomFields = functionalCaseFileService.getCustomFields(request.getProjectId());
TemplateCustomFieldDTO templateCustomFieldDTO = templateCustomFields.stream().filter(item -> StringUtils.equalsIgnoreCase(item.getFieldName(), Translator.get("custom_field.functional_priority"))).findFirst().get();
XmindExportUtil.export(xmindData, request, tmpFile, templateCustomFieldDTO);
functionalCaseFileService.uploadFileToMinio(tmpFile, request.getFileId());
functionalCaseLogService.exportExcelLog(request);
List<ExportTask> exportTasks = functionalCaseFileService.getExportTasks(request.getProjectId(), userId);
String taskId;
if (CollectionUtils.isNotEmpty(exportTasks)) {
taskId = exportTasks.getFirst().getId();
functionalCaseFileService.updateExportTask(ExportConstants.ExportState.SUCCESS.name(), taskId, request.getFileId());
} 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<ExportTask> exportTasks = functionalCaseFileService.getExportTasks(request.getProjectId(), userId);
if (CollectionUtils.isNotEmpty(exportTasks)) {
functionalCaseFileService.updateExportTask(ExportConstants.ExportState.ERROR.name(), exportTasks.getFirst().getId(), request.getFileId());
}
LogUtils.error(e);
throw new MSException(e);
}
return null;
}
private FunctionalCaseXmindData buildXmindData(List<String> ids, FunctionalCaseExportRequest request) {
FunctionalCaseXmindData xmindData = new FunctionalCaseXmindData();
xmindData.setModuleId("MODULE");
xmindData.setModuleName("MODULE");
//基础信息
List<FunctionalCase> caseList = functionalCaseService.getCaseDataByIds(ids);
//大字段
Map<String, FunctionalCaseBlob> functionalCaseBlobMap = functionalCaseService.copyBlobInfo(ids);
//自定义字段
Map<String, List<FunctionalCaseCustomField>> customFieldMap = functionalCaseCustomFieldService.getCustomFieldMapByCaseIds(ids);
Map<String, List<FunctionalCase>> moduleCaseMap = caseList.stream().collect(Collectors.groupingBy(FunctionalCase::getModuleId));
List<BaseTreeNode> tree = functionalCaseModuleService.getTree(request.getProjectId());
for (Map.Entry<String, List<FunctionalCase>> entry : moduleCaseMap.entrySet()) {
String moduleId = entry.getKey();
List<FunctionalCase> dataList = entry.getValue();
List<FunctionalCaseXmindDTO> dtos = buildXmindDTO(dataList, functionalCaseBlobMap, customFieldMap);
if (StringUtils.equals(moduleId, ModuleConstants.DEFAULT_NODE_ID)) {
xmindData.setFunctionalCaseList(dtos);
} else {
LinkedList<BaseTreeNode> returnList = new LinkedList<>();
LinkedList<BaseTreeNode> modulePathDataList = getModuleById(moduleId, tree, returnList);
xmindData.setItem(modulePathDataList, dtos);
System.out.println("modulePathDataList: " + modulePathDataList);
}
}
return xmindData;
}
private LinkedList<BaseTreeNode> getModuleById(String moduleId, List<BaseTreeNode> tree, LinkedList<BaseTreeNode> returnList) {
for (BaseTreeNode baseTreeNode : tree) {
if (StringUtils.equals(baseTreeNode.getId(), moduleId)) {
BaseTreeNode node = new BaseTreeNode();
node.setId(baseTreeNode.getId());
node.setName(baseTreeNode.getName());
returnList.addFirst(node);
return returnList;
} else {
List<BaseTreeNode> children = baseTreeNode.getChildren();
if (CollectionUtils.isNotEmpty(children)) {
LinkedList<BaseTreeNode> result = getModuleById(moduleId, children, returnList);
if (CollectionUtils.isNotEmpty(result)) {
BaseTreeNode node = new BaseTreeNode();
node.setId(baseTreeNode.getId());
node.setName(baseTreeNode.getName());
returnList.addFirst(node);
return returnList;
}
}
}
}
return returnList;
}
private List<FunctionalCaseXmindDTO> buildXmindDTO(List<FunctionalCase> dataList, Map<String, FunctionalCaseBlob> functionalCaseBlobMap, Map<String, List<FunctionalCaseCustomField>> customFieldMap) {
List<FunctionalCaseXmindDTO> caseXmindDTOS = new ArrayList<>();
dataList.forEach(item -> {
FunctionalCaseBlob functionalCaseBlob = functionalCaseBlobMap.get(item.getId());
List<FunctionalCaseCustomField> customFields = customFieldMap.get(item.getId());
FunctionalCaseXmindDTO dto = new FunctionalCaseXmindDTO();
dto.setId(item.getId());
dto.setNum(item.getNum().toString());
dto.setProjectId(item.getProjectId());
dto.setName(item.getName());
dto.setTags(item.getTags().toString());
dto.setCaseEditType(item.getCaseEditType());
dto.setSteps(new String(functionalCaseBlob.getSteps() == null ? new byte[0] : functionalCaseBlob.getSteps(), StandardCharsets.UTF_8));
dto.setTextDescription(new String(functionalCaseBlob.getTextDescription() == null ? new byte[0] : functionalCaseBlob.getTextDescription(), StandardCharsets.UTF_8));
dto.setExpectedResult(new String(functionalCaseBlob.getExpectedResult() == null ? new byte[0] : functionalCaseBlob.getExpectedResult(), StandardCharsets.UTF_8));
dto.setPrerequisite(new String(functionalCaseBlob.getPrerequisite() == null ? new byte[0] : functionalCaseBlob.getPrerequisite(), StandardCharsets.UTF_8));
dto.setDescription(new String(functionalCaseBlob.getDescription() == null ? new byte[0] : functionalCaseBlob.getDescription(), StandardCharsets.UTF_8));
dto.setCustomFieldDTOList(customFields);
caseXmindDTOS.add(dto);
});
return caseXmindDTOS;
}
}

View File

@ -1,6 +1,6 @@
package io.metersphere.functional.xmind.domain;
import io.metersphere.functional.dto.FunctionalCaseCustomFieldDTO;
import io.metersphere.functional.domain.FunctionalCaseCustomField;
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -47,7 +47,7 @@ public class FunctionalCaseXmindDTO {
private String description;
@Schema(description = "自定义字段")
private List<FunctionalCaseCustomFieldDTO> customFieldDTOList;
private List<FunctionalCaseCustomField> customFieldDTOList;
@Schema(description = "模板自定义字段")
private List<TemplateCustomFieldDTO> templateCustomFieldDTOList;

View File

@ -1,10 +1,14 @@
package io.metersphere.functional.xmind.domain;
import io.metersphere.system.dto.sdk.BaseTreeNode;
import lombok.Data;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
@ -21,4 +25,49 @@ public class FunctionalCaseXmindData implements Serializable {
private List<FunctionalCaseXmindData> children = new ArrayList<>();
public void setItem(LinkedList<BaseTreeNode> modulePathDataList, List<FunctionalCaseXmindDTO> dataList) {
if (CollectionUtils.isNotEmpty(modulePathDataList) && CollectionUtils.isNotEmpty(dataList)) {
if (modulePathDataList.size() == 1) {
this.setData(modulePathDataList.getFirst(), dataList);
} else {
BaseTreeNode caseNode = modulePathDataList.getFirst();
if (caseNode != null) {
FunctionalCaseXmindData matchedData = null;
for (FunctionalCaseXmindData item : children) {
if (StringUtils.equals(item.getModuleId(), caseNode.getId())) {
matchedData = item;
break;
}
}
if(matchedData == null){
matchedData = new FunctionalCaseXmindData();
matchedData.setModuleId(caseNode.getId());
matchedData.setModuleName(caseNode.getName());
this.children.add(matchedData);
}
modulePathDataList.removeFirst();
matchedData.setItem(modulePathDataList,dataList);
}
}
}
}
private void setData(BaseTreeNode caseNode, List<FunctionalCaseXmindDTO> dataList) {
if (caseNode != null && CollectionUtils.isNotEmpty(dataList)) {
boolean matching = false;
for (FunctionalCaseXmindData item : children) {
if (StringUtils.equals(item.getModuleId(), caseNode.getId())) {
matching = true;
item.setFunctionalCaseList(dataList);
}
}
if (!matching) {
FunctionalCaseXmindData child = new FunctionalCaseXmindData();
child.setModuleId(caseNode.getId());
child.setModuleName(caseNode.getName());
child.setFunctionalCaseList(dataList);
this.children.add(child);
}
}
}
}

View File

@ -1,6 +1,10 @@
package io.metersphere.functional.xmind.utils;
import io.metersphere.functional.constants.FunctionalCaseTypeConstants;
import io.metersphere.functional.domain.FunctionalCaseCustomField;
import io.metersphere.functional.excel.domain.FunctionalCaseExportColumns;
import io.metersphere.functional.excel.domain.FunctionalCaseHeader;
import io.metersphere.functional.request.FunctionalCaseExportRequest;
import io.metersphere.functional.xmind.domain.FunctionalCaseXmindDTO;
import io.metersphere.functional.xmind.domain.FunctionalCaseXmindData;
import io.metersphere.sdk.constants.CustomFieldType;
@ -16,12 +20,14 @@ import org.xmind.core.*;
import org.xmind.core.style.IStyle;
import org.xmind.core.style.IStyleSheet;
import java.io.File;
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;
import java.util.stream.Collectors;
/**
* @author wx
@ -37,7 +43,7 @@ public class XmindExportUtil {
* @param template
*/
public static void downloadTemplate(HttpServletResponse response, FunctionalCaseXmindData caseData, boolean template, Map<String, List<String>> customFieldOptionsMap) {
IWorkbook workBook = createXmindByCaseData(caseData, template, customFieldOptionsMap);
IWorkbook workBook = createXmindByCaseData(caseData, template, customFieldOptionsMap, null, null);
response.setContentType("application/octet-stream");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
@ -53,7 +59,7 @@ public class XmindExportUtil {
}
}
private static IWorkbook createXmindByCaseData(FunctionalCaseXmindData caseData, boolean template, Map<String, List<String>> customFieldOptionsMap) {
private static IWorkbook createXmindByCaseData(FunctionalCaseXmindData caseData, boolean template, Map<String, List<String>> customFieldOptionsMap, FunctionalCaseExportRequest request, TemplateCustomFieldDTO priority) {
// 创建思维导图的工作空间
IWorkbookBuilder workbookBuilder = Core.getWorkbookBuilder();
IWorkbook workbook = workbookBuilder.createWorkbook();
@ -75,14 +81,18 @@ public class XmindExportUtil {
if (CollectionUtils.isNotEmpty(caseData.getChildren())) {
for (FunctionalCaseXmindData data : caseData.getChildren()) {
addItemTopic(rootTopic, workbook, styleMap, data, true, template, customFieldOptionsMap);
if (template) {
addTemplateTopic(rootTopic, workbook, styleMap, data, true, customFieldOptionsMap);
} else {
addTopic(rootTopic, workbook, styleMap, data, true, request, priority);
}
}
}
return workbook;
}
private static void addItemTopic(ITopic parentTpoic, IWorkbook workbook, Map<String, IStyle> styleMap, FunctionalCaseXmindData xmindData,
boolean isFirstLevel, boolean template, Map<String, List<String>> customFieldOptionsMap) {
private static void addTemplateTopic(ITopic parentTopic, IWorkbook workbook, Map<String, IStyle> styleMap, FunctionalCaseXmindData xmindData,
boolean isFirstLevel, Map<String, List<String>> customFieldOptionsMap) {
ITopic topic = workbook.createTopic();
topic.setTitleText(xmindData.getModuleName());
if (isFirstLevel) {
@ -94,7 +104,7 @@ public class XmindExportUtil {
topic.setStyleId(styleMap.get("subTopicStyle").getId());
}
}
parentTpoic.add(topic);
parentTopic.add(topic);
if (CollectionUtils.isNotEmpty(xmindData.getFunctionalCaseList())) {
IStyle style = null;
@ -107,16 +117,13 @@ public class XmindExportUtil {
if (style != null) {
itemTopic.setStyleId(style.getId());
}
if (template) {
// 模板
buildTemplateTopic(topic, style, dto, itemTopic, workbook, customFieldOptionsMap);
}
}
}
if (CollectionUtils.isNotEmpty(xmindData.getChildren())) {
for (FunctionalCaseXmindData data : xmindData.getChildren()) {
addItemTopic(topic, workbook, styleMap, data, false, template, customFieldOptionsMap);
addTemplateTopic(topic, workbook, styleMap, data, false, customFieldOptionsMap);
}
}
}
@ -356,4 +363,112 @@ public class XmindExportUtil {
return styleMap;
}
public static void export(FunctionalCaseXmindData xmindData, FunctionalCaseExportRequest request, File tmpFile, TemplateCustomFieldDTO priority) {
IWorkbook workBook = createXmindByCaseData(xmindData, false, null, request, priority);
try {
workBook.save(tmpFile.getAbsolutePath());
} 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 void addTopic(ITopic parentTopic, IWorkbook workbook, Map<String, IStyle> styleMap, FunctionalCaseXmindData xmindData, boolean isFirstLevel, FunctionalCaseExportRequest request, TemplateCustomFieldDTO priority) {
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());
}
}
parentTopic.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());
}
buildTopic(topic, style, dto, itemTopic, workbook, request, xmindData.getModuleName(), priority);
}
}
if (CollectionUtils.isNotEmpty(xmindData.getChildren())) {
for (FunctionalCaseXmindData data : xmindData.getChildren()) {
addTopic(topic, workbook, styleMap, data, false, request, priority);
}
}
}
private static void buildTopic(ITopic topic, IStyle style, FunctionalCaseXmindDTO dto, ITopic itemTopic, IWorkbook workbook, FunctionalCaseExportRequest request, String moduleName, TemplateCustomFieldDTO priority) {
List<String> systemColumns = request.getSystemFields().stream().map(FunctionalCaseHeader::getId).toList();
FunctionalCaseExportColumns columns = new FunctionalCaseExportColumns();
Map<String, String> customFieldMap = dto.getCustomFieldDTOList().stream().collect(Collectors.toMap(FunctionalCaseCustomField::getFieldId, FunctionalCaseCustomField::getValue));
//用例名称
String casePriority = customFieldMap.get(priority.getFieldId());
itemTopic.setTitleText("case-".concat(StringUtils.defaultIfBlank(casePriority,StringUtils.EMPTY)).concat(": ").concat(dto.getName()));
//系统字段
systemColumns.forEach(item -> {
if (columns.getSystemColumns().containsKey(item) && !StringUtils.equalsIgnoreCase(item, "name")) {
ITopic preTopic = workbook.createTopic();
switch (item) {
case "num":
preTopic.setTitleText(columns.getSystemColumns().get(item).concat(": ").concat(dto.getNum()));
break;
case "prerequisite":
preTopic.setTitleText(columns.getSystemColumns().get(item).concat(": ").concat(dto.getPrerequisite()));
break;
case "module":
preTopic.setTitleText(columns.getSystemColumns().get(item).concat(": ").concat(moduleName));
break;
case "text_description":
preTopic.setTitleText(columns.getSystemColumns().get(item).concat(": ").concat(dto.getTextDescription()));
break;
case "expected_result":
preTopic.setTitleText(columns.getSystemColumns().get(item).concat(": ").concat(dto.getExpectedResult()));
break;
default:
break;
}
if (style != null) {
preTopic.setStyleId(style.getId());
}
itemTopic.add(preTopic, ITopic.ATTACHED);
}
});
//自定义字段
Map<String, String> customColumnsMap = request.getCustomFields().stream().collect(Collectors.toMap(FunctionalCaseHeader::getId, FunctionalCaseHeader::getName));
customColumnsMap.forEach((k, v) -> {
if (customFieldMap.containsKey(k)) {
ITopic preTopic = workbook.createTopic();
preTopic.setTitleText(v.concat(": ").concat(customFieldMap.get(k)));
if (style != null) {
preTopic.setStyleId(style.getId());
}
itemTopic.add(preTopic, ITopic.ATTACHED);
}
});
topic.add(itemTopic);
}
}

View File

@ -91,6 +91,7 @@ public class FunctionalCaseControllerTests extends BaseTest {
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/";
public static final String EXPORT_XMIND_URL = "/functional/case/export/xmind";
@Resource
private NotificationMapper notificationMapper;
@ -865,4 +866,44 @@ public class FunctionalCaseControllerTests extends BaseTest {
public void stopExport() throws Exception {
this.requestGetExcel(STOP_EXPORT_URL + DEFAULT_PROJECT_ID);
}
@Test
@Order(3)
public void exportXmind() throws Exception {
FunctionalCaseExportRequest request = new FunctionalCaseExportRequest();
request.setProjectId(DEFAULT_PROJECT_ID);
request.setSelectIds(List.of("TEST_FUNCTIONAL_CASE_ID"));
List<FunctionalCaseHeader> sysHeaders = new ArrayList<>() {{
add(new FunctionalCaseHeader() {{
setId("num");
setName("ID");
}});
add(new FunctionalCaseHeader() {{
setId("name");
setName("用例名称");
}});
}};
request.setSystemFields(sysHeaders);
List<FunctionalCaseHeader> customHeaders = new ArrayList<>() {{
add(new FunctionalCaseHeader() {{
setId("A");
setName("测试3");
}});
}};
request.setCustomFields(customHeaders);
List<FunctionalCaseHeader> otherHeaders = new ArrayList<>() {{
add(new FunctionalCaseHeader() {{
setId("createTime");
setName("创建时间");
}});
}};
request.setOtherFields(otherHeaders);
request.setFileId("123142342");
this.requestPost(EXPORT_XMIND_URL, request);
request.setSelectIds(new ArrayList<>());
this.requestPost(EXPORT_XMIND_URL, request);
request.setSelectIds(List.of("TEST_FUNCTIONAL_CASE_ID_8"));
this.requestPost(EXPORT_XMIND_URL, request);
}
}

View File

@ -30,6 +30,10 @@ VALUES ('TEST_FUNCTIONAL_CASE_ID_6', 6, 'TEST_MODULE_ID_GYQ', '100001100001', '1
INSERT INTO functional_case(id, num, module_id, project_id, template_id, name, review_status, tags, case_edit_type, pos, version_id, ref_id, last_execute_result, deleted, public_case, latest, create_user, update_user, delete_user, create_time, update_time, delete_time)
VALUES ('TEST_FUNCTIONAL_CASE_ID_7', 7, 'TEST_MODULE_ID_GYQ', '100001100001', '100001', 'copy_long_name_11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111', 'UN_REVIEWED', NULL, 'STEP', 0, 'v3.0.0', 'TEST_REF_ID_2', 'UN_EXECUTED', b'0', b'0', b'1', 'admin', 'admin', '', 1698058347559, 1698058347559, NULL);
INSERT INTO functional_case(id, num, module_id, project_id, template_id, name, review_status, tags, case_edit_type, pos, version_id, ref_id, last_execute_result, deleted, public_case, latest, create_user, update_user, delete_user, create_time, update_time, delete_time)
VALUES ('TEST_FUNCTIONAL_CASE_ID_8', 1, 'root', '100001100001', '100001', '测试', 'UN_REVIEWED', NULL, 'STEP', 0, 'v1.0.0', 'v1.0.0', 'UN_EXECUTED', b'0', b'0', b'1', 'admin', 'admin', '', 1698058347559, 1698058347559, NULL);
INSERT INTO functional_case_blob(id, steps, text_description, expected_result, prerequisite, description) VALUES ('TEST_FUNCTIONAL_CASE_ID', 'STEP', '1111', '', '', 'TEST');
INSERT INTO functional_case_blob(id, steps, text_description, expected_result, prerequisite, description) VALUES ('TEST_FUNCTIONAL_CASE_ID_1', 'STEP', '1111', '', '', '1111');
INSERT INTO functional_case_blob(id, steps, text_description, expected_result, prerequisite, description) VALUES ('TEST_FUNCTIONAL_CASE_ID_2', 'STEP', '2222', '', '', '2222');
@ -38,6 +42,7 @@ INSERT INTO functional_case_blob(id, steps, text_description, expected_result, p
INSERT INTO functional_case_blob(id, steps, text_description, expected_result, prerequisite, description) VALUES ('TEST_FUNCTIONAL_CASE_ID_5', 'STEP', '5555', '', '', '5555');
INSERT INTO functional_case_blob(id, steps, text_description, expected_result, prerequisite, description) VALUES ('TEST_FUNCTIONAL_CASE_ID_6', 'STEP', '6666', '', '', '6666');
INSERT INTO functional_case_blob(id, steps, text_description, expected_result, prerequisite, description) VALUES ('TEST_FUNCTIONAL_CASE_ID_7', 'STEP', '7777', '', '', '7777');
INSERT INTO functional_case_blob(id, steps, text_description, expected_result, prerequisite, description) VALUES ('TEST_FUNCTIONAL_CASE_ID_8', NULL, NUll, NUll, NUll, NUll);
INSERT INTO functional_case_custom_field(case_id, field_id, value) VALUES ('TEST_FUNCTIONAL_CASE_ID', '100548878725546079', '22');

View File

@ -34,7 +34,8 @@ public class ExportTaskManager {
public static final String EXPORT_CONSUME = "export_consume";
public <T> void exportAsyncTask(String userId, String type, T t, Function<Object, Object> selectListFunc) throws InterruptedException {
public <T> void exportAsyncTask(String projectId, String userId, String type, T t, Function<Object, Object> selectListFunc) throws InterruptedException {
ExportTask exportTask = buildExportTask(projectId, userId, type);
ExecutorService executorService = Executors.newFixedThreadPool(1);
Future<?> future = executorService.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
@ -44,12 +45,10 @@ public class ExportTaskManager {
}
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) {
private ExportTask buildExportTask(String projectId, String userId, String type) {
ExportTask exportTask = new ExportTask();
exportTask.setId(IDGenerator.nextStr());
exportTask.setType(type);
@ -58,6 +57,7 @@ public class ExportTaskManager {
exportTask.setState(ExportConstants.ExportState.PREPARED.toString());
exportTask.setUpdateUser(userId);
exportTask.setUpdateTime(System.currentTimeMillis());
exportTask.setProjectId(projectId);
exportTaskMapper.insert(exportTask);
return exportTask;
}
@ -77,9 +77,11 @@ public class ExportTaskManager {
ExportTask exportTask = JSON.parseObject(record.value(), ExportTask.class);
if (exportTask != null && StringUtils.isNotBlank(exportTask.getId())) {
String id = exportTask.getId();
if (map.containsKey(id)) {
map.get(id).cancel(true);
map.remove(id);
exportTaskMapper.updateByPrimaryKey(exportTask);
}
exportTaskMapper.updateByPrimaryKeySelective(exportTask);
}
}