feat(测试用例): 功能用例导出excel

This commit is contained in:
WangXu10 2024-08-01 12:33:39 +08:00 committed by 刘瑞斌
parent f76a8cbd7a
commit 279384539d
41 changed files with 1464 additions and 35 deletions

View File

@ -32,6 +32,7 @@ public class DefaultRepositoryDir {
* 会定时清理 * 会定时清理
*/ */
private static final String SYSTEM_TEMP_DIR = SYSTEM_ROOT_DIR + "/temp"; private static final String SYSTEM_TEMP_DIR = SYSTEM_ROOT_DIR + "/temp";
private static final String EXPORT_EXCEL_TEMP_DIR = SYSTEM_ROOT_DIR + "/export/excel";
/*------ end: 系统下资源目录 --------*/ /*------ end: 系统下资源目录 --------*/
@ -158,6 +159,9 @@ public class DefaultRepositoryDir {
return SYSTEM_TEMP_DIR; return SYSTEM_TEMP_DIR;
} }
public static String getExportExcelTempDir() {
return EXPORT_EXCEL_TEMP_DIR;
}
public static String getSystemTempCompressDir() { public static String getSystemTempCompressDir() {
return SYSTEM_TEMP_DIR + "/compress"; return SYSTEM_TEMP_DIR + "/compress";
} }

View File

@ -91,6 +91,7 @@ public class CompressUtils {
/** /**
* 将多个文件压缩 * 将多个文件压缩
*
* @param zipFilePath 压缩文件所在路径 * @param zipFilePath 压缩文件所在路径
* @param fileList 要压缩的文件 * @param fileList 要压缩的文件
* @return * @return
@ -113,6 +114,31 @@ public class CompressUtils {
return zipFile; return zipFile;
} }
/**
* 将多个文件压缩至指定路径
*
* @param fileList 待压缩的文件列表
* @param zipFilePath 压缩文件路径
* @return 返回压缩好的文件
* @throws IOException
*/
public static File zipFilesToPath(String zipFilePath, List<File> fileList) throws IOException {
File zipFile = new File(zipFilePath);
try( // 文件输出流
FileOutputStream outputStream = getFileStream(zipFile);
// 压缩流
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)
){
int size = fileList.size();
// 压缩列表中的文件
for (int i = 0; i < size; i++) {
File file = fileList.get(i);
zipFile(file, zipOutputStream);
}
}
return zipFile;
}
/*** /***
* Zip解压 * Zip解压
* *

View File

@ -1,6 +1,7 @@
package io.metersphere.sdk.util; package io.metersphere.sdk.util;
import io.metersphere.sdk.exception.MSException; import io.metersphere.sdk.exception.MSException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.io.File; import java.io.File;
@ -15,4 +16,9 @@ public class MsFileUtils {
} }
} }
} }
public static void deleteDir(String path) throws Exception {
File file = new File(path);
FileUtils.deleteDirectory(file);
}
} }

View File

@ -247,3 +247,13 @@ case.minder.all.case=All Case
case.minder.status.success=success case.minder.status.success=success
case.minder.status.error=error case.minder.status.error=error
case.minder.status.blocked=blocked case.minder.status.blocked=blocked
case.review.status.un_reviewed=Unreviewed
case.review.status.under_reviewed=Under review
case.review.status.pass=Passed
case.review.status.un_pass=Un pass
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

View File

@ -246,3 +246,12 @@ case.minder.status.success=成功
case.minder.status.error=失败 case.minder.status.error=失败
case.minder.status.blocked=阻塞 case.minder.status.blocked=阻塞
case.review.status.un_reviewed=未评审
case.review.status.under_reviewed=评审中
case.review.status.pass=已通过
case.review.status.un_pass=不通过
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

View File

@ -247,3 +247,13 @@ case.minder.status.success=成功
case.minder.status.error=失敗 case.minder.status.error=失敗
case.minder.status.blocked=阻塞 case.minder.status.blocked=阻塞
case.review.status.un_reviewed=未評審
case.review.status.under_reviewed=評審中
case.review.status.pass=已通過
case.review.status.un_pass=不通過
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

View File

@ -33,6 +33,7 @@ import io.metersphere.system.utils.SessionUtils;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.Logical;
@ -223,7 +224,7 @@ public class FunctionalCaseController {
@PostMapping("/pre-check/excel") @PostMapping("/pre-check/excel")
@Operation(summary = "用例管理-功能用例-excel导入检查") @Operation(summary = "用例管理-功能用例-excel导入检查")
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_UPDATE) @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_IMPORT)
@CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project") @CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project")
public FunctionalCaseImportResponse preCheckExcel(@RequestPart("request") FunctionalCaseImportRequest request, @RequestPart(value = "file", required = false) MultipartFile file) { public FunctionalCaseImportResponse preCheckExcel(@RequestPart("request") FunctionalCaseImportRequest request, @RequestPart(value = "file", required = false) MultipartFile file) {
return functionalCaseFileService.preCheckExcel(request, file); return functionalCaseFileService.preCheckExcel(request, file);
@ -232,7 +233,7 @@ public class FunctionalCaseController {
@PostMapping("/import/excel") @PostMapping("/import/excel")
@Operation(summary = "用例管理-功能用例-excel导入") @Operation(summary = "用例管理-功能用例-excel导入")
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_UPDATE) @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_IMPORT)
@CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project") @CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project")
public FunctionalCaseImportResponse importExcel(@RequestPart("request") FunctionalCaseImportRequest request, @RequestPart(value = "file", required = false) MultipartFile file) { public FunctionalCaseImportResponse importExcel(@RequestPart("request") FunctionalCaseImportRequest request, @RequestPart(value = "file", required = false) MultipartFile file) {
SessionUser user = SessionUtils.getUser(); SessionUser user = SessionUtils.getUser();
@ -248,4 +249,12 @@ public class FunctionalCaseController {
StringUtils.isNotBlank(request.getSortString()) ? request.getSortString() : "create_time desc"); StringUtils.isNotBlank(request.getSortString()) ? request.getSortString() : "create_time desc");
return PageUtils.setPageInfo(page, functionalCaseService.operationHistoryList(request)); return PageUtils.setPageInfo(page, functionalCaseService.operationHistoryList(request));
} }
@PostMapping("/export/excel")
@Operation(summary = "用例管理-功能用例-excel导出")
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_EXPORT)
public void testCaseExport(@Validated @RequestBody FunctionalCaseExportRequest request) {
functionalCaseFileService.exportFunctionalCaseZip(request);
}
} }

View File

@ -0,0 +1,24 @@
package io.metersphere.functional.excel.constants;
public enum FunctionalCaseExportOtherField {
CREATE_USER("createUser"),
CREATE_TIME("createTime"),
UPDATE_USER("updateUser"),
UPDATE_TIME("updateTime"),
REVIEW_STATUS("reviewStatus"),
LAST_EXECUTE_RESULT("lastExecuteResult"),
CASE_COMMENT("caseComment"),
EXECUTE_COMMENT("executeComment"),
REVIEW_COMMENT("reviewComment");
private String value;
FunctionalCaseExportOtherField(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}

View File

@ -0,0 +1,28 @@
package io.metersphere.functional.excel.converter;
/**
* @author wx
*/
public enum FunctionalCaseExecuteStatus {
PENDING("case.execute.status.pending", 1),
SUCCESS("case.minder.status.success", 2),
BLOCKED("case.minder.status.blocked", 3),
ERROR("case.minder.status.error", 4);
private String i18nKey;
private Integer order;
FunctionalCaseExecuteStatus(String i18nKey, int order) {
this.i18nKey = i18nKey;
this.order = order;
}
public String getI18nKey() {
return i18nKey;
}
public Integer getOrder() {
return order;
}
}

View File

@ -0,0 +1,34 @@
package io.metersphere.functional.excel.converter;
import io.metersphere.functional.domain.CaseReviewHistory;
import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.domain.FunctionalCaseComment;
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
import io.metersphere.sdk.util.DateUtils;
import io.metersphere.sdk.util.Translator;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.Map;
/**
* @author wx
*/
public class FunctionalCaseExportCaseCommentConverter implements FunctionalCaseExportConverter {
@Override
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
if (caseCommentMap.containsKey(functionalCase.getId())) {
StringBuilder result = new StringBuilder();
String template = Translator.get("functional_case_comment_template");
List<FunctionalCaseComment> caseComments = caseCommentMap.get(functionalCase.getId());
caseComments.forEach(item -> {
String updateTime = DateUtils.getTimeString(item.getUpdateTime());
String content = item.getContent();
result.append(String.format(template, item.getCreateUser(), updateTime, content));
});
return result.toString();
}
return StringUtils.EMPTY;
}
}

View File

@ -0,0 +1,36 @@
package io.metersphere.functional.excel.converter;
import io.metersphere.functional.domain.CaseReviewHistory;
import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.domain.FunctionalCaseComment;
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
import io.metersphere.sdk.util.Translator;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.Map;
/**
* 功能用例导出时解析其他字段对应的列
*
* @author wx
*/
public interface FunctionalCaseExportConverter {
String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap);
default String getFromMapOfNullable(Map<String, String> map, String key) {
if (StringUtils.isNotBlank(key)) {
return map.get(key);
}
return StringUtils.EMPTY;
}
default String getFromMapOfNullableWithTranslate(Map<String, String> map, String key) {
String value = getFromMapOfNullable(map, key);
if (StringUtils.isNotBlank(value)) {
return Translator.get(value);
}
return value;
}
}

View File

@ -0,0 +1,56 @@
package io.metersphere.functional.excel.converter;
import io.metersphere.functional.excel.constants.FunctionalCaseExportOtherField;
import io.metersphere.sdk.util.LogUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author wx
*/
public class FunctionalCaseExportConverterFactory {
public static Map<String, FunctionalCaseExportConverter> getConverters(List<String> keys) {
Map<String, FunctionalCaseExportConverter> converterMapResult = new HashMap<>();
try {
HashMap<String, Class<? extends FunctionalCaseExportConverter>> converterMap = getConverterMap();
for (String key : keys) {
Class<? extends FunctionalCaseExportConverter> clazz = converterMap.get(key);
if (clazz != null) {
converterMapResult.put(key, clazz.getDeclaredConstructor().newInstance());
}
}
} catch (Exception e) {
LogUtils.error(e);
}
return converterMapResult;
}
public static FunctionalCaseExportConverter getConverter(String key) {
try {
Class<? extends FunctionalCaseExportConverter> clazz = getConverterMap().get(key);
if (clazz != null) {
return clazz.getDeclaredConstructor().newInstance();
}
} catch (Exception e) {
LogUtils.error(e);
}
return null;
}
private static HashMap<String, Class<? extends FunctionalCaseExportConverter>> getConverterMap() {
return new HashMap<>() {{
put(FunctionalCaseExportOtherField.CREATE_USER.getValue(), FunctionalCaseExportCreateUserConverter.class);
put(FunctionalCaseExportOtherField.CREATE_TIME.getValue(), FunctionalCaseExportCreateTimeConverter.class);
put(FunctionalCaseExportOtherField.UPDATE_USER.getValue(), FunctionalCaseExportUpdateUserConverter.class);
put(FunctionalCaseExportOtherField.UPDATE_TIME.getValue(), FunctionalCaseExportUpdateTimeConverter.class);
put(FunctionalCaseExportOtherField.REVIEW_STATUS.getValue(), FunctionalCaseExportReviewStatusConverter.class);
put(FunctionalCaseExportOtherField.LAST_EXECUTE_RESULT.getValue(), FunctionalCaseExportExecuteStatusConverter.class);
put(FunctionalCaseExportOtherField.CASE_COMMENT.getValue(), FunctionalCaseExportCaseCommentConverter.class);
put(FunctionalCaseExportOtherField.EXECUTE_COMMENT.getValue(), FunctionalCaseExportExecuteCommentConverter.class);
put(FunctionalCaseExportOtherField.REVIEW_COMMENT.getValue(), FunctionalCaseExportReviewCommentConverter.class);
}};
}
}

View File

@ -0,0 +1,21 @@
package io.metersphere.functional.excel.converter;
import io.metersphere.functional.domain.CaseReviewHistory;
import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.domain.FunctionalCaseComment;
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
import io.metersphere.sdk.util.DateUtils;
import java.util.List;
import java.util.Map;
/**
* @author wx
*/
public class FunctionalCaseExportCreateTimeConverter implements FunctionalCaseExportConverter {
@Override
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
return DateUtils.getTimeString(functionalCase.getCreateTime());
}
}

View File

@ -0,0 +1,34 @@
package io.metersphere.functional.excel.converter;
import io.metersphere.functional.domain.CaseReviewHistory;
import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.domain.FunctionalCaseComment;
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
import io.metersphere.project.service.ProjectApplicationService;
import io.metersphere.sdk.util.CommonBeanFactory;
import io.metersphere.system.domain.User;
import io.metersphere.system.utils.SessionUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author wx
*/
public class FunctionalCaseExportCreateUserConverter implements FunctionalCaseExportConverter {
public Map<String, String> userMap = new HashMap<>();
public FunctionalCaseExportCreateUserConverter() {
ProjectApplicationService projectApplicationService = CommonBeanFactory.getBean(ProjectApplicationService.class);
List<User> memberOption = projectApplicationService.getProjectUserList(SessionUtils.getCurrentProjectId());
memberOption.forEach(option -> userMap.put(option.getId(), option.getName()));
}
@Override
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
return getFromMapOfNullable(userMap, functionalCase.getCreateUser());
}
}

View File

@ -0,0 +1,45 @@
package io.metersphere.functional.excel.converter;
import io.metersphere.functional.domain.CaseReviewHistory;
import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.domain.FunctionalCaseComment;
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
import io.metersphere.sdk.util.DateUtils;
import io.metersphere.sdk.util.Translator;
import org.apache.commons.lang3.StringUtils;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author wx
*/
public class FunctionalCaseExportExecuteCommentConverter implements FunctionalCaseExportConverter {
private Map<String, String> executeStatusMap = new HashMap<>();
public FunctionalCaseExportExecuteCommentConverter() {
for (FunctionalCaseExecuteStatus value : FunctionalCaseExecuteStatus.values()) {
executeStatusMap.put(value.name(), value.getI18nKey());
}
}
@Override
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
if (executeCommentMap.containsKey(functionalCase.getId())) {
StringBuilder result = new StringBuilder();
String template = Translator.get("functional_case_execute_comment_template");
List<TestPlanCaseExecuteHistory> executeComment = executeCommentMap.get(functionalCase.getId());
executeComment.forEach(item -> {
String status = getFromMapOfNullableWithTranslate(executeStatusMap, item.getStatus());
String createTime = DateUtils.getTimeString(item.getCreateTime());
String content = new String(item.getContent() == null ? new byte[0] : item.getContent(), StandardCharsets.UTF_8);
result.append(String.format(template, item.getCreateUser(), status, createTime, content));
});
}
return StringUtils.EMPTY;
}
}

View File

@ -0,0 +1,30 @@
package io.metersphere.functional.excel.converter;
import io.metersphere.functional.domain.CaseReviewHistory;
import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.domain.FunctionalCaseComment;
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author wx
*/
public class FunctionalCaseExportExecuteStatusConverter implements FunctionalCaseExportConverter {
private Map<String, String> executeStatusMap = new HashMap<>();
public FunctionalCaseExportExecuteStatusConverter() {
for (FunctionalCaseExecuteStatus value : FunctionalCaseExecuteStatus.values()) {
executeStatusMap.put(value.name(), value.getI18nKey());
}
}
@Override
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
return getFromMapOfNullableWithTranslate(executeStatusMap, functionalCase.getLastExecuteResult());
}
}

View File

@ -0,0 +1,44 @@
package io.metersphere.functional.excel.converter;
import io.metersphere.functional.domain.CaseReviewHistory;
import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.domain.FunctionalCaseComment;
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
import io.metersphere.sdk.util.DateUtils;
import io.metersphere.sdk.util.Translator;
import org.apache.commons.lang3.StringUtils;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author wx
*/
public class FunctionalCaseExportReviewCommentConverter implements FunctionalCaseExportConverter {
private Map<String, String> reviewStatusMap = new HashMap<>();
public FunctionalCaseExportReviewCommentConverter() {
for (FunctionalCaseReviewStatus value : FunctionalCaseReviewStatus.values()) {
reviewStatusMap.put(value.name(), value.getI18nKey());
}
}
@Override
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
if (reviewCommentMap.containsKey(functionalCase.getId())) {
StringBuilder result = new StringBuilder();
String template = Translator.get("functional_case_review_comment_template");
List<CaseReviewHistory> reviewComent = reviewCommentMap.get(functionalCase.getId());
reviewComent.forEach(item -> {
String status = getFromMapOfNullableWithTranslate(reviewStatusMap, item.getStatus());
String createTime = DateUtils.getTimeString(item.getCreateTime());
String content = new String(item.getContent() == null ? new byte[0] : item.getContent(), StandardCharsets.UTF_8);
result.append(String.format(template, item.getCreateUser(), status, createTime, content));
});
}
return StringUtils.EMPTY;
}
}

View File

@ -0,0 +1,29 @@
package io.metersphere.functional.excel.converter;
import io.metersphere.functional.domain.CaseReviewHistory;
import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.domain.FunctionalCaseComment;
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author wx
*/
public class FunctionalCaseExportReviewStatusConverter implements FunctionalCaseExportConverter {
private Map<String, String> caseReviewStatusMap = new HashMap<>();
public FunctionalCaseExportReviewStatusConverter() {
for (FunctionalCaseReviewStatus value : FunctionalCaseReviewStatus.values()) {
caseReviewStatusMap.put(value.name(), value.getI18nKey());
}
}
@Override
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
return getFromMapOfNullableWithTranslate(caseReviewStatusMap, functionalCase.getReviewStatus());
}
}

View File

@ -0,0 +1,21 @@
package io.metersphere.functional.excel.converter;
import io.metersphere.functional.domain.CaseReviewHistory;
import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.domain.FunctionalCaseComment;
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
import io.metersphere.sdk.util.DateUtils;
import java.util.List;
import java.util.Map;
/**
* @author wx
*/
public class FunctionalCaseExportUpdateTimeConverter implements FunctionalCaseExportConverter {
@Override
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
return DateUtils.getTimeString(functionalCase.getUpdateTime());
}
}

View File

@ -0,0 +1,21 @@
package io.metersphere.functional.excel.converter;
import io.metersphere.functional.domain.CaseReviewHistory;
import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.domain.FunctionalCaseComment;
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
import java.util.List;
import java.util.Map;
/**
* @author wx
*/
public class FunctionalCaseExportUpdateUserConverter extends FunctionalCaseExportCreateUserConverter {
@Override
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
return getFromMapOfNullable(userMap, functionalCase.getUpdateUser());
}
}

View File

@ -0,0 +1,29 @@
package io.metersphere.functional.excel.converter;
/**
* @author wx
*/
public enum FunctionalCaseReviewStatus {
UN_REVIEWED("case.review.status.un_reviewed", 1),
UNDER_REVIEWED("case.review.status.under_reviewed", 2),
PASS("case.review.status.pass", 3),
UN_PASS("case.review.status.un_pass", 4),
RE_REVIEWED("case.review.status.re_reviewed", 5);
private String i18nKey;
private Integer order;
FunctionalCaseReviewStatus(String i18nKey, int order) {
this.i18nKey = i18nKey;
this.order = order;
}
public String getI18nKey() {
return i18nKey;
}
public Integer getOrder() {
return order;
}
}

View File

@ -1,6 +1,7 @@
package io.metersphere.functional.excel.domain; package io.metersphere.functional.excel.domain;
import com.alibaba.excel.annotation.ExcelIgnore; import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.metadata.data.WriteCellData;
import io.metersphere.functional.excel.constants.FunctionalCaseImportFiled; import io.metersphere.functional.excel.constants.FunctionalCaseImportFiled;
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
import lombok.Getter; import lombok.Getter;
@ -40,6 +41,9 @@ public class FunctionalCaseExcelData {
@ExcelIgnore @ExcelIgnore
Map<String, String> otherFields; Map<String, String> otherFields;
@ExcelIgnore
private WriteCellData<String> hyperLinkName;
/** /**
* 合并文本描述 * 合并文本描述
*/ */

View File

@ -0,0 +1,21 @@
package io.metersphere.functional.excel.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @author wx
*/
@Data
public class FunctionalCaseHeader implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "字段英文名称")
private String id;
@Schema(description = "字段中文名称")
private String name;
}

View File

@ -0,0 +1,60 @@
package io.metersphere.functional.excel.handler;
import com.alibaba.excel.write.handler.RowWriteHandler;
import com.alibaba.excel.write.handler.context.RowWriteHandlerContext;
import io.metersphere.functional.excel.constants.FunctionalCaseImportFiled;
import org.apache.poi.ss.util.CellRangeAddress;
import java.util.List;
import java.util.Map;
/**
* @author wx
*/
public class FunctionCaseMergeWriteHandler implements RowWriteHandler {
/**
* 存储需要合并单元格的信息key 是需要合并的第一条的行号值是需要合并多少行
*/
Map<Integer, Integer> rowMergeInfo;
List<List<String>> headList;
int textDescriptionRowIndex;
int expectedResultRowIndex;
public FunctionCaseMergeWriteHandler(Map<Integer, Integer> rowMergeInfo, List<List<String>> headList) {
this.rowMergeInfo = rowMergeInfo;
this.headList = headList;
for (int i = 0; i < headList.size(); i++) {
List<String> list = headList.get(i);
for (String head : list) {
if (FunctionalCaseImportFiled.TEXT_DESCRIPTION.containsHead(head)) {
textDescriptionRowIndex = i;
} else if (FunctionalCaseImportFiled.EXPECTED_RESULT.containsHead(head)) {
expectedResultRowIndex = i;
}
}
}
}
@Override
public void afterRowDispose(RowWriteHandlerContext context) {
if (context.getHead() || context.getRelativeRowIndex() == null) {
return;
}
Integer mergeCount = rowMergeInfo.get(context.getRowIndex());
if (mergeCount == null || mergeCount <= 0) {
return;
}
for (int i = 0; i < headList.size(); i++) {
// 除了描述其他数据合并多行
if (i != textDescriptionRowIndex && i != expectedResultRowIndex) {
CellRangeAddress cellRangeAddress =
new CellRangeAddress(context.getRowIndex(), context.getRowIndex() + mergeCount - 1, i, i);
context.getWriteSheetHolder().getSheet().addMergedRegionUnsafe(cellRangeAddress);
}
}
}
}

View File

@ -29,7 +29,7 @@ import java.util.Map;
*/ */
public class FunctionCaseTemplateWriteHandler implements RowWriteHandler { public class FunctionCaseTemplateWriteHandler implements RowWriteHandler {
Map<String, List<String>> caseLevelAndStatusValueMap; Map<String, List<String>> customFieldOptionsMap;
private Sheet sheet; private Sheet sheet;
private Drawing<?> drawingPatriarch; private Drawing<?> drawingPatriarch;
@ -37,9 +37,9 @@ public class FunctionCaseTemplateWriteHandler implements RowWriteHandler {
private Map<String, TemplateCustomFieldDTO> customField; private Map<String, TemplateCustomFieldDTO> customField;
private Map<String, Integer> fieldMap = new HashMap<>(); private Map<String, Integer> fieldMap = new HashMap<>();
public FunctionCaseTemplateWriteHandler(List<List<String>> headList, Map<String, List<String>> caseLevelAndStatusValueMap, Map<String, TemplateCustomFieldDTO> customFieldMap) { public FunctionCaseTemplateWriteHandler(List<List<String>> headList, Map<String, List<String>> customFieldOptionsMap, Map<String, TemplateCustomFieldDTO> customFieldMap) {
initIndex(headList); initIndex(headList);
this.caseLevelAndStatusValueMap = caseLevelAndStatusValueMap; this.customFieldOptionsMap = customFieldOptionsMap;
this.customField = customFieldMap; this.customField = customFieldMap;
} }
@ -94,7 +94,7 @@ public class FunctionCaseTemplateWriteHandler implements RowWriteHandler {
//自定义字段 //自定义字段
if (customField.containsKey(entry.getKey())) { if (customField.containsKey(entry.getKey())) {
TemplateCustomFieldDTO templateCustomFieldDTO = customField.get(entry.getKey()); TemplateCustomFieldDTO templateCustomFieldDTO = customField.get(entry.getKey());
List<String> strings = caseLevelAndStatusValueMap.get(entry.getKey()); List<String> strings = customFieldOptionsMap.get(entry.getKey());
if (StringUtils.equalsAnyIgnoreCase(templateCustomFieldDTO.getType(), CustomFieldType.MULTIPLE_MEMBER.name(), CustomFieldType.MEMBER.name())) { if (StringUtils.equalsAnyIgnoreCase(templateCustomFieldDTO.getType(), CustomFieldType.MULTIPLE_MEMBER.name(), CustomFieldType.MEMBER.name())) {
if (templateCustomFieldDTO.getRequired()) { if (templateCustomFieldDTO.getRequired()) {
setComment(fieldMap.get(entry.getKey()), Translator.get("required").concat(",").concat(Translator.get("excel.template.member"))); setComment(fieldMap.get(entry.getKey()), Translator.get("required").concat(",").concat(Translator.get("excel.template.member")));
@ -104,13 +104,13 @@ public class FunctionCaseTemplateWriteHandler implements RowWriteHandler {
} else { } else {
if (templateCustomFieldDTO.getRequired()) { if (templateCustomFieldDTO.getRequired()) {
if (CollectionUtils.isNotEmpty(strings)) { if (CollectionUtils.isNotEmpty(strings)) {
setComment(fieldMap.get(entry.getKey()), Translator.get("required").concat("").concat(Translator.get("options")).concat(JSON.toJSONString(caseLevelAndStatusValueMap.get(entry.getKey())))); setComment(fieldMap.get(entry.getKey()), Translator.get("required").concat("").concat(Translator.get("options")).concat(JSON.toJSONString(customFieldOptionsMap.get(entry.getKey()))));
} else { } else {
setComment(fieldMap.get(entry.getKey()), Translator.get("required")); setComment(fieldMap.get(entry.getKey()), Translator.get("required"));
} }
} else { } else {
if (CollectionUtils.isNotEmpty(strings)) { if (CollectionUtils.isNotEmpty(strings)) {
setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.not_required").concat("").concat(Translator.get("options")).concat(JSON.toJSONString(caseLevelAndStatusValueMap.get(entry.getKey())))); setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.not_required").concat("").concat(Translator.get("options")).concat(JSON.toJSONString(customFieldOptionsMap.get(entry.getKey()))));
} else { } else {
setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.not_required")); setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.not_required"));
} }

View File

@ -71,4 +71,8 @@ public abstract class AbstractCustomFieldValidator {
} }
return new ArrayList<>(); return new ArrayList<>();
} }
public Object parse2Value(String value, TemplateCustomFieldDTO customField) {
return value;
}
} }

View File

@ -21,6 +21,7 @@ import java.util.stream.Collectors;
public class CustomFieldMemberValidator extends AbstractCustomFieldValidator { public class CustomFieldMemberValidator extends AbstractCustomFieldValidator {
protected Map<String, String> userIdMap; protected Map<String, String> userIdMap;
protected Map<String, String> userEmailMap;
protected Map<String, String> userNameMap; protected Map<String, String> userNameMap;
public CustomFieldMemberValidator() { public CustomFieldMemberValidator() {
@ -31,9 +32,12 @@ public class CustomFieldMemberValidator extends AbstractCustomFieldValidator {
.collect( .collect(
Collectors.toMap(user -> user.getId().toLowerCase(), User::getId) Collectors.toMap(user -> user.getId().toLowerCase(), User::getId)
); );
userEmailMap = new HashMap<>();
memberOption.stream()
.forEach(user -> userEmailMap.put(user.getEmail().toLowerCase(), user.getId()));
userNameMap = new HashMap<>(); userNameMap = new HashMap<>();
memberOption.stream() memberOption.stream()
.forEach(user -> userNameMap.put(user.getEmail().toLowerCase(), user.getId())); .forEach(user -> userNameMap.put(user.getId(), user.getName().toLowerCase()));
} }
@Override @Override
@ -43,7 +47,7 @@ public class CustomFieldMemberValidator extends AbstractCustomFieldValidator {
return; return;
} }
value = value.toLowerCase(); value = value.toLowerCase();
if (userIdMap.containsKey(value) || userNameMap.containsKey(value)) { if (userIdMap.containsKey(value) || userEmailMap.containsKey(value)) {
return; return;
} }
throw new CustomFieldValidateException(String.format(Translator.get("custom_field_member_tip"), customField.getFieldName())); throw new CustomFieldValidateException(String.format(Translator.get("custom_field_member_tip"), customField.getFieldName()));
@ -55,9 +59,19 @@ public class CustomFieldMemberValidator extends AbstractCustomFieldValidator {
if (userIdMap.containsKey(keyOrValue)) { if (userIdMap.containsKey(keyOrValue)) {
return userIdMap.get(keyOrValue); return userIdMap.get(keyOrValue);
} }
if (userEmailMap.containsKey(keyOrValue)) {
return userEmailMap.get(keyOrValue);
}
return keyOrValue;
}
@Override
public Object parse2Value(String keyOrValue, TemplateCustomFieldDTO customField) {
keyOrValue = keyOrValue.toLowerCase();
if (userNameMap.containsKey(keyOrValue)) { if (userNameMap.containsKey(keyOrValue)) {
return userNameMap.get(keyOrValue); return userNameMap.get(keyOrValue);
} }
return keyOrValue; return keyOrValue;
} }
} }

View File

@ -24,7 +24,7 @@ public class CustomFieldMultipleMemberValidator extends CustomFieldMemberValidat
for (String item : parse2Array(customField.getFieldName(), value)) { for (String item : parse2Array(customField.getFieldName(), value)) {
item = item.toLowerCase(); item = item.toLowerCase();
if (!userIdMap.containsKey(item) && !userNameMap.containsKey(item)) { if (!userIdMap.containsKey(item) && !userEmailMap.containsKey(item)) {
CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_member_tip"), customField.getFieldName())); CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_member_tip"), customField.getFieldName()));
} }
} }
@ -42,10 +42,27 @@ public class CustomFieldMultipleMemberValidator extends CustomFieldMemberValidat
if (userIdMap.containsKey(item)) { if (userIdMap.containsKey(item)) {
keyOrValues.set(i, userIdMap.get(item)); keyOrValues.set(i, userIdMap.get(item));
} }
if (userEmailMap.containsKey(item)) {
keyOrValues.set(i, userEmailMap.get(item));
}
}
return JSON.toJSONString(keyOrValues);
}
@Override
public Object parse2Value(String keyOrValuesStr, TemplateCustomFieldDTO customField) {
if (StringUtils.isBlank(keyOrValuesStr)) {
return JSON.toJSONString(new ArrayList<>());
}
List<String> keyOrValues = parse2Array(keyOrValuesStr);
for (int i = 0; i < keyOrValues.size(); i++) {
String item = keyOrValues.get(i).toLowerCase();
if (userNameMap.containsKey(item)) { if (userNameMap.containsKey(item)) {
keyOrValues.set(i, userNameMap.get(item)); keyOrValues.set(i, userNameMap.get(item));
} }
} }
return JSON.toJSONString(keyOrValues); return JSON.toJSONString(keyOrValues);
} }
} }

View File

@ -4,13 +4,12 @@ package io.metersphere.functional.excel.validate;
import io.metersphere.functional.excel.exception.CustomFieldValidateException; import io.metersphere.functional.excel.exception.CustomFieldValidateException;
import io.metersphere.sdk.util.JSON; import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.Translator; import io.metersphere.sdk.util.Translator;
import io.metersphere.system.domain.CustomFieldOption;
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList; import java.util.*;
import java.util.List; import java.util.stream.Collectors;
import java.util.Map;
import java.util.Set;
/** /**
* @author wx * @author wx
@ -48,4 +47,21 @@ public class CustomFieldMultipleSelectValidator extends CustomFieldSelectValidat
} }
return JSON.toJSONString(keyOrValues); return JSON.toJSONString(keyOrValues);
} }
@Override
public Object parse2Value(String keyOrValuesStr, TemplateCustomFieldDTO customField) {
Map<String, String> optionValueMap = customField.getOptions().stream().collect(Collectors.toMap(CustomFieldOption::getFieldId, CustomFieldOption::getValue));
if (StringUtils.isBlank(keyOrValuesStr)) {
return JSON.toJSONString(new ArrayList<>());
}
List<String> keyOrValues = parse2Array(keyOrValuesStr);
for (int i = 0; i < keyOrValues.size(); i++) {
String item = keyOrValues.get(i);
if (optionValueMap.containsKey(item)) {
keyOrValues.set(i, optionValueMap.get(item));
}
}
return JSON.toJSONString(keyOrValues);
}
} }

View File

@ -51,4 +51,14 @@ public class CustomFieldMultipleTextValidator extends AbstractCustomFieldValidat
return JSON.toJSONString(keyOrValues); return JSON.toJSONString(keyOrValues);
} }
@Override
public Object parse2Value(String keyOrValuesStr, TemplateCustomFieldDTO customField) {
if (StringUtils.isBlank(keyOrValuesStr)) {
return JSON.toJSONString(new ArrayList<>());
}
List<String> keyOrValues = parse2Array(keyOrValuesStr);
return JSON.toJSONString(keyOrValues);
}
} }

View File

@ -60,6 +60,15 @@ public class CustomFieldSelectValidator extends AbstractCustomFieldValidator {
return keyOrValuesStr; return keyOrValuesStr;
} }
@Override
public Object parse2Value(String keyOrValuesStr, TemplateCustomFieldDTO customField) {
Map<String, String> optionValueMap = customField.getOptions().stream().collect(Collectors.toMap(CustomFieldOption::getFieldId, CustomFieldOption::getValue));
if (optionValueMap.containsKey(keyOrValuesStr)) {
return optionValueMap.get(keyOrValuesStr);
}
return keyOrValuesStr;
}
/** /**
* 获取自定义字段的选项值和key * 获取自定义字段的选项值和key
* 存储到缓存中增强导入时性能 * 存储到缓存中增强导入时性能

View File

@ -0,0 +1,19 @@
package io.metersphere.functional.mapper;
import io.metersphere.functional.domain.CaseReviewHistory;
import io.metersphere.functional.domain.FunctionalCaseComment;
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @author wx
*/
public interface ExtFunctionalCaseCommentMapper {
List<FunctionalCaseComment> getCaseComment(@Param("ids") List<String> ids);
List<TestPlanCaseExecuteHistory> getExecuteComment(@Param("ids") List<String> ids);
List<CaseReviewHistory> getReviewComment(@Param("ids") List<String> ids);
}

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.metersphere.functional.mapper.ExtFunctionalCaseCommentMapper">
<select id="getCaseComment" resultType="io.metersphere.functional.domain.FunctionalCaseComment">
SELECT
functional_case_comment.case_id,
functional_case_comment.content,
functional_case_comment.update_time,
user.name as createUser
FROM
functional_case_comment
INNER JOIN user ON functional_case_comment.create_user = user.id
where functional_case_comment.case_id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<select id="getExecuteComment" resultType="io.metersphere.plan.domain.TestPlanCaseExecuteHistory">
SELECT
test_plan_case_execute_history.case_id,
test_plan_case_execute_history.content,
test_plan_case_execute_history.create_time,
user.name as createUser
FROM
test_plan_case_execute_history
INNER JOIN user ON test_plan_case_execute_history.create_user = user.id
where test_plan_case_execute_history.case_id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<select id="getReviewComment" resultType="io.metersphere.functional.domain.CaseReviewHistory">
SELECT
case_review_history.case_id,
case_review_history.content,
case_review_history.create_time,
user.name as createUser
FROM
case_review_history
INNER JOIN user ON case_review_history.create_user = user.id
where case_review_history.case_id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
</mapper>

View File

@ -0,0 +1,36 @@
package io.metersphere.functional.request;
import io.metersphere.functional.dto.BaseFunctionalCaseBatchDTO;
import io.metersphere.functional.excel.domain.FunctionalCaseHeader;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* @author wx
*/
@Data
public class FunctionalCaseExportRequest extends BaseFunctionalCaseBatchDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "系统字段", requiredMode = Schema.RequiredMode.REQUIRED)
private List<FunctionalCaseHeader> systemFields = new ArrayList<>();
@Schema(description = "自定义字段")
private List<FunctionalCaseHeader> customFields = new ArrayList<>();
@Schema(description = "其他字段")
private List<FunctionalCaseHeader> otherFields = new ArrayList<>();
@Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED)
private String projectId;
@Schema(description = "文件id")
private String fileId;
}

View File

@ -3,36 +3,72 @@ package io.metersphere.functional.service;
import com.alibaba.excel.EasyExcel; import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.EasyExcelFactory; import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.enums.CellExtraTypeEnum; import com.alibaba.excel.enums.CellExtraTypeEnum;
import com.alibaba.excel.metadata.data.HyperlinkData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import io.metersphere.functional.constants.FunctionalCaseTypeConstants;
import io.metersphere.functional.domain.*;
import io.metersphere.functional.dto.response.FunctionalCaseImportResponse; import io.metersphere.functional.dto.response.FunctionalCaseImportResponse;
import io.metersphere.functional.excel.constants.FunctionalCaseImportFiled; import io.metersphere.functional.excel.constants.FunctionalCaseImportFiled;
import io.metersphere.functional.excel.converter.FunctionalCaseExportConverter;
import io.metersphere.functional.excel.converter.FunctionalCaseExportConverterFactory;
import io.metersphere.functional.excel.domain.ExcelMergeInfo; import io.metersphere.functional.excel.domain.ExcelMergeInfo;
import io.metersphere.functional.excel.domain.FunctionalCaseExcelData; import io.metersphere.functional.excel.domain.FunctionalCaseExcelData;
import io.metersphere.functional.excel.domain.FunctionalCaseExcelDataFactory; import io.metersphere.functional.excel.domain.FunctionalCaseExcelDataFactory;
import io.metersphere.functional.excel.domain.FunctionalCaseHeader;
import io.metersphere.functional.excel.handler.FunctionCaseMergeWriteHandler;
import io.metersphere.functional.excel.handler.FunctionCaseTemplateWriteHandler; import io.metersphere.functional.excel.handler.FunctionCaseTemplateWriteHandler;
import io.metersphere.functional.excel.listener.FunctionalCaseCheckEventListener; import io.metersphere.functional.excel.listener.FunctionalCaseCheckEventListener;
import io.metersphere.functional.excel.listener.FunctionalCaseImportEventListener; import io.metersphere.functional.excel.listener.FunctionalCaseImportEventListener;
import io.metersphere.functional.excel.listener.FunctionalCasePretreatmentListener; 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.ExtFunctionalCaseCommentMapper;
import io.metersphere.functional.request.FunctionalCaseExportRequest;
import io.metersphere.functional.request.FunctionalCaseImportRequest; import io.metersphere.functional.request.FunctionalCaseImportRequest;
import io.metersphere.functional.socket.ExportWebSocketHandler;
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
import io.metersphere.project.domain.Project;
import io.metersphere.project.mapper.ExtBaseProjectVersionMapper; import io.metersphere.project.mapper.ExtBaseProjectVersionMapper;
import io.metersphere.project.mapper.ProjectMapper;
import io.metersphere.project.service.ProjectTemplateService; import io.metersphere.project.service.ProjectTemplateService;
import io.metersphere.sdk.constants.TemplateScene; import io.metersphere.sdk.constants.*;
import io.metersphere.sdk.dto.SocketMsgDTO;
import io.metersphere.sdk.exception.MSException; import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.LogUtils; import io.metersphere.sdk.file.FileRequest;
import io.metersphere.sdk.util.Translator; import io.metersphere.sdk.util.*;
import io.metersphere.system.domain.CustomFieldOption; import io.metersphere.system.domain.CustomFieldOption;
import io.metersphere.system.domain.SystemParameter;
import io.metersphere.system.dto.sdk.BaseTreeNode;
import io.metersphere.system.dto.sdk.SessionUser; import io.metersphere.system.dto.sdk.SessionUser;
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
import io.metersphere.system.dto.sdk.TemplateDTO; import io.metersphere.system.dto.sdk.TemplateDTO;
import io.metersphere.system.excel.utils.EasyExcelExporter; import io.metersphere.system.excel.utils.EasyExcelExporter;
import io.metersphere.system.mapper.SystemParameterMapper;
import io.metersphere.system.service.FileService;
import io.metersphere.system.uid.IDGenerator;
import io.metersphere.system.utils.ServiceUtils; import io.metersphere.system.utils.ServiceUtils;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.jetbrains.annotations.NotNull;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Serial;
import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -50,7 +86,22 @@ public class FunctionalCaseFileService {
@Resource @Resource
private FunctionalCaseService functionalCaseService; private FunctionalCaseService functionalCaseService;
@Resource
private FunctionalCaseModuleService functionalCaseModuleService;
@Resource
private FunctionalCaseCustomFieldService functionalCaseCustomFieldService;
private static final String EXPORT_CASE_TMP_DIR = "tmp";
private static final int EXPORT_CASE_MAX_COUNT = 2000;
@Resource
private ExtFunctionalCaseCommentMapper extFunctionalCaseCommentMapper;
@Resource
private ProjectMapper projectMapper;
@Resource
private FileService fileService;
@Resource
private FunctionalCaseLogService functionalCaseLogService;
@Resource
private SystemParameterMapper systemParameterMapper;
/** /**
* 下载excel导入模板 * 下载excel导入模板
@ -105,10 +156,17 @@ public class FunctionalCaseFileService {
for (String head : headList) { for (String head : headList) {
boolean isSystemField = false; boolean isSystemField = false;
for (FunctionalCaseImportFiled importFiled : importFields) { for (FunctionalCaseImportFiled importFiled : importFields) {
if (StringUtils.equals("name", importFiled.getValue()) && model.getHyperLinkName() != null) {
fields.add(model.getHyperLinkName());
isSystemField = true;
break;
}
if (importFiled.containsHead(head)) { if (importFiled.containsHead(head)) {
fields.add(importFiled.parseExcelDataValue(model)); fields.add(importFiled.parseExcelDataValue(model));
isSystemField = true; isSystemField = true;
break;
} }
} }
if (!isSystemField) { if (!isSystemField) {
Object value = customDataMaps.get(head); Object value = customDataMaps.get(head);
@ -267,4 +325,408 @@ public class FunctionalCaseFileService {
throw new MSException(Translator.get("check_import_excel_error")); throw new MSException(Translator.get("check_import_excel_error"));
} }
} }
/**
* 导出excel
*
* @param request
* @param url
*/
@Async
public void exportFunctionalCaseZip(FunctionalCaseExportRequest request) {
File tmpDir = null;
Project project = projectMapper.selectByPrimaryKey(request.getProjectId());
try {
tmpDir = new File(getClass().getClassLoader().getResource(StringUtils.EMPTY).getPath() +
EXPORT_CASE_TMP_DIR + File.separatorChar + EXPORT_CASE_TMP_DIR + "_" + IDGenerator.nextStr());
// 生成tmp随机目录
MsFileUtils.deleteDir(tmpDir.getPath());
tmpDir.mkdirs();
// 生成EXCEL
List<File> batchExcels = generateCaseExportExcel(tmpDir.getPath(), request, project);
if (batchExcels.size() > 1) {
// EXCEL -> ZIP (EXCEL数目大于1)
File zipFile = CompressUtils.zipFilesToPath(tmpDir.getPath() + File.separatorChar + "Metersphere_case_" + project.getName() + ".zip", batchExcels);
uploadFileToMinio(zipFile, request.getFileId());
} else {
// EXCEL (EXCEL数目等于1)
File singeFile = batchExcels.get(0);
uploadFileToMinio(singeFile, request.getFileId());
}
functionalCaseLogService.exportExcelLog(request);
SocketMsgDTO socketMsgDTO = new SocketMsgDTO(request.getFileId(), "", MsgType.CONNECT.name(), MsgType.CONNECT.name());
socketMsgDTO.setReportId(request.getFileId());
ExportWebSocketHandler.sendMessageSingle(socketMsgDTO);
} catch (Exception e) {
LogUtils.error(e);
throw new MSException(e);
}
}
private void uploadFileToMinio(File file, String fileId) {
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(file.getName());
fileRequest.setFolder(DefaultRepositoryDir.getExportExcelTempDir() + "/" + fileId);
fileRequest.setStorage(StorageType.MINIO.name());
try {
FileInputStream inputStream = new FileInputStream(file);
fileService.upload(inputStream, fileRequest);
} catch (Exception e) {
throw new MSException("save file error");
}
}
private List<File> generateCaseExportExcel(String tmpZipPath, FunctionalCaseExportRequest request, Project project) {
List<File> tmpExportExcelList = new ArrayList<>();
//excel表头
List<List<String>> headList = getFunctionalCaseExportHeads(request);
//获取导出的ids集合
List<String> ids = functionalCaseService.doSelectIds(request, request.getProjectId());
if (CollectionUtils.isEmpty(ids)) {
return tmpExportExcelList;
}
//获取当前项目下默认模板的自定义字段属性
List<TemplateCustomFieldDTO> customFields = getCustomFields(request.getProjectId());
//默认字段+自定义字段的 options集合
Map<String, List<String>> customFieldOptionsMap = getCustomFieldOptionsMap(customFields);
Map<String, TemplateCustomFieldDTO> customFieldMap = customFields.stream().collect(Collectors.toMap(TemplateCustomFieldDTO::getFieldName, templateCustomFieldDTO -> templateCustomFieldDTO));
//获取url
SystemParameter parameter = systemParameterMapper.selectByPrimaryKey(ParamConstants.BASE.URL.getValue());
//获取用例模块map
Map<String, String> moduleMap = getModuleMap(request.getProjectId());
//2000条分批导出
AtomicInteger count = new AtomicInteger(0);
SubListUtils.dealForSubList(ids, EXPORT_CASE_MAX_COUNT, (subIds) -> {
count.getAndIncrement();
// 生成writeHandler
Map<Integer, Integer> rowMergeInfo = new HashMap<>();
FunctionCaseMergeWriteHandler writeHandler = new FunctionCaseMergeWriteHandler(rowMergeInfo, headList);
//表头备注信息
FunctionCaseTemplateWriteHandler handler = new FunctionCaseTemplateWriteHandler(headList, customFieldOptionsMap, customFieldMap);
//获取导出数据
List<FunctionalCaseExcelData> excelData = parseCaseData2ExcelData(subIds, rowMergeInfo, request, customFields, moduleMap, parameter.getParamValue());
List<List<Object>> data = parseExcelData2List(headList, excelData);
File createFile = new File(tmpZipPath + File.separatorChar + "Metersphere_case_" + project.getName() + count.get() + ".xlsx");
if (!createFile.exists()) {
try {
createFile.createNewFile();
} catch (IOException e) {
throw new MSException(e);
}
}
//生成临时EXCEL
EasyExcel.write(createFile)
.head(Optional.ofNullable(headList).orElse(new ArrayList<>()))
.registerWriteHandler(handler)
.registerWriteHandler(writeHandler)
.registerWriteHandler(FunctionCaseTemplateWriteHandler.getHorizontalWrapStrategy())
.excelType(ExcelTypeEnum.XLSX).sheet(Translator.get("test_case_import_template_sheet")).doWrite(data);
tmpExportExcelList.add(createFile);
});
return tmpExportExcelList;
}
private List<FunctionalCaseExcelData> parseCaseData2ExcelData(List<String> ids, Map<Integer, Integer> rowMergeInfo, FunctionalCaseExportRequest request, List<TemplateCustomFieldDTO> customFields, Map<String, String> moduleMap, String url) {
List<FunctionalCaseExcelData> list = new ArrayList<>();
//基础信息
Map<String, FunctionalCase> functionalCaseMap = functionalCaseService.copyBaseInfo(request.getProjectId(), ids);
//大字段
Map<String, FunctionalCaseBlob> functionalCaseBlobMap = functionalCaseService.copyBlobInfo(ids);
//自定义字段
Map<String, List<FunctionalCaseCustomField>> customFieldMap = functionalCaseCustomFieldService.getCustomFieldMapByCaseIds(ids);
//用例评论
Map<String, List<FunctionalCaseComment>> caseCommentMap = getCaseComment(ids);
//执行评论
Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap = getExecuteComment(ids);
//评审评论
Map<String, List<CaseReviewHistory>> reviewCommentMap = getReviewComment(ids);
ids.forEach(id -> {
List<String> textDescriptionList = new ArrayList<>();
List<String> expectedResultList = new ArrayList<>();
//构建基本参数
FunctionalCaseExcelData data = new FunctionalCaseExcelData();
FunctionalCase functionalCase = functionalCaseMap.get(id);
FunctionalCaseBlob functionalCaseBlob = functionalCaseBlobMap.get(id);
//构建基本参数
buildBaseField(data, functionalCase, functionalCaseBlob, moduleMap, textDescriptionList, expectedResultList, url);
//构建自定义字段
buildExportCustomField(customFields, customFieldMap.get(id), data, request);
//构建其他字段
buildExportOtherField(functionalCase, data, caseCommentMap, executeCommentMap, reviewCommentMap, request);
validateExportTextField(data);
if (CollectionUtils.isNotEmpty(textDescriptionList)) {
// 如果有多条步骤则添加多条数据之后合并单元格
buildExportMergeData(rowMergeInfo, list, textDescriptionList, expectedResultList, data);
} else {
list.add(data);
}
});
return list;
}
/**
* 构建基本参数
*
* @param data
* @param functionalCase
* @param functionalCaseBlob
*/
private void buildBaseField(FunctionalCaseExcelData data, FunctionalCase functionalCase, FunctionalCaseBlob functionalCaseBlob, Map<String, String> moduleMap, List<String> textDescriptionList, List<String> expectedResultList, String url) {
data.setNum(functionalCase.getNum().toString());
data.setModule(moduleMap.get(functionalCase.getModuleId()));
data.setTags(functionalCase.getTags().toString());
//构建步骤
buildExportStep(data, functionalCaseBlob, functionalCase.getCaseEditType(), textDescriptionList, expectedResultList);
data.setPrerequisite(new String(functionalCaseBlob.getPrerequisite() == null ? new byte[0] : functionalCaseBlob.getPrerequisite(), StandardCharsets.UTF_8));
// 设置超链接
WriteCellData<String> hyperlink = new WriteCellData<>(functionalCase.getName());
data.setHyperLinkName(hyperlink);
HyperlinkData hyperlinkData = new HyperlinkData();
hyperlink.setHyperlinkData(hyperlinkData);
WriteFont writeFont = new WriteFont();
writeFont.setUnderline(Font.U_SINGLE);
writeFont.setColor(IndexedColors.BLUE.getIndex());
WriteCellStyle writeCellStyle = new WriteCellStyle();
writeCellStyle.setWriteFont(writeFont);
hyperlink.setWriteCellStyle(writeCellStyle);
hyperlinkData.setAddress(url + "/functional/case/detail/" + functionalCase.getId());
hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL);
}
/**
* 合并单元格
*
* @param rowMergeInfo
* @param list
* @param textDescriptionList
* @param expectedResultList
* @param data
*/
@NotNull
private void buildExportMergeData(Map<Integer, Integer> rowMergeInfo, List<FunctionalCaseExcelData> list, List<String> textDescriptionList, List<String> expectedResultList, FunctionalCaseExcelData data) {
for (int i = 0; i < textDescriptionList.size(); i++) {
FunctionalCaseExcelData excelData;
if (i == 0) {
// 第一行存全量元素
excelData = data;
if (textDescriptionList.size() > 1) {
// 保存合并单元格的下标和数量
rowMergeInfo.put(list.size() + 1, textDescriptionList.size());
}
} else {
// 之后的行只存步骤
excelData = new FunctionalCaseExcelData();
}
excelData.setTextDescription(textDescriptionList.get(i));
excelData.setExpectedResult(expectedResultList.get(i));
list.add(excelData);
}
}
private void validateExportTextField(FunctionalCaseExcelData data) {
data.setPrerequisite(validateExportText(data.getPrerequisite()));
data.setDescription(validateExportText(data.getDescription()));
data.setTextDescription(validateExportText(data.getTextDescription()));
data.setExpectedResult(validateExportText(data.getExpectedResult()));
}
/**
* 构建其他字段
*
* @param functionalCase
* @param data
* @param caseCommentMap
* @param executeCommentMap
* @param reviewCommentMap
* @param request
*/
private void buildExportOtherField(FunctionalCase functionalCase, FunctionalCaseExcelData data, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap, FunctionalCaseExportRequest request) {
if (CollectionUtils.isEmpty(request.getOtherFields())) {
return;
}
List<FunctionalCaseHeader> otherFields = request.getOtherFields();
List<String> keys = otherFields.stream().map(FunctionalCaseHeader::getId).toList();
Map<String, FunctionalCaseExportConverter> converterMaps = FunctionalCaseExportConverterFactory.getConverters(keys);
HashMap<String, String> other = new HashMap<>();
otherFields.forEach(header -> {
FunctionalCaseExportConverter converter = converterMaps.get(header.getId());
if (converter != null) {
other.put(header.getName(), converter.parse(functionalCase, caseCommentMap, executeCommentMap, reviewCommentMap));
} else {
other.put(header.getName(), StringUtils.EMPTY);
}
});
data.setOtherFields(other);
}
/**
* 评审评论
*
* @param ids
* @return
*/
private Map<String, List<CaseReviewHistory>> getReviewComment(List<String> ids) {
List<CaseReviewHistory> reviewHistories = extFunctionalCaseCommentMapper.getReviewComment(ids);
Map<String, List<CaseReviewHistory>> reviewHistoryMap = reviewHistories.stream().collect(Collectors.groupingBy(CaseReviewHistory::getCaseId));
return reviewHistoryMap;
}
/**
* 执行评论
*
* @param ids
* @return
*/
private Map<String, List<TestPlanCaseExecuteHistory>> getExecuteComment(List<String> ids) {
List<TestPlanCaseExecuteHistory> historyList = extFunctionalCaseCommentMapper.getExecuteComment(ids);
Map<String, List<TestPlanCaseExecuteHistory>> commentMap = historyList.stream().collect(Collectors.groupingBy(TestPlanCaseExecuteHistory::getCaseId));
return commentMap;
}
/**
* 用例评论
*
* @param ids
* @return
*/
private Map<String, List<FunctionalCaseComment>> getCaseComment(List<String> ids) {
List<FunctionalCaseComment> functionalCaseComments = extFunctionalCaseCommentMapper.getCaseComment(ids);
Map<String, List<FunctionalCaseComment>> commentMap = functionalCaseComments.stream().collect(Collectors.groupingBy(FunctionalCaseComment::getCaseId));
return commentMap;
}
/**
* 构建自定义字段
*
* @param templateCustomFields
* @param functionalCaseCustomFields
* @param data
* @param request
*/
private void buildExportCustomField(List<TemplateCustomFieldDTO> templateCustomFields, List<FunctionalCaseCustomField> functionalCaseCustomFields, FunctionalCaseExcelData data, FunctionalCaseExportRequest request) {
if (CollectionUtils.isEmpty(request.getCustomFields())) {
return;
}
HashMap<String, AbstractCustomFieldValidator> customFieldValidatorMap = CustomFieldValidatorFactory.getValidatorMap();
Map<String, TemplateCustomFieldDTO> customFieldsMap = templateCustomFields.stream().collect(Collectors.toMap(TemplateCustomFieldDTO::getFieldId, i -> i));
Map<String, String> caseFieldvalueMap = functionalCaseCustomFields.stream().collect(Collectors.toMap(FunctionalCaseCustomField::getFieldId, FunctionalCaseCustomField::getValue));
Map<String, Object> map = new HashMap<>();
customFieldsMap.forEach((k, v) -> {
if (caseFieldvalueMap.containsKey(k)) {
AbstractCustomFieldValidator customFieldValidator = customFieldValidatorMap.get(v.getType());
if (customFieldValidator.isKVOption) {
// 这里如果填的是选项值替换成选项ID保存
map.put(v.getFieldName(), customFieldValidator.parse2Value(caseFieldvalueMap.get(k), v));
}
}
});
data.setCustomData(map);
}
private String validateExportText(String textValue) {
// poi 导出的单个单元格最大字符数量为 32767 这里添加校验提示
int maxLength = 32767;
if (StringUtils.isNotBlank(textValue) && textValue.length() > maxLength) {
return String.format(Translator.get("case_export_text_validate_tip"), maxLength);
}
return textValue;
}
/**
* 构建步骤单元格
*
* @param data
* @param functionalCaseBlob
* @param caseEditType
* @param textDescriptionList
* @param expectedResultList
*/
private void buildExportStep(FunctionalCaseExcelData data, FunctionalCaseBlob functionalCaseBlob, String caseEditType, List<String> textDescriptionList, List<String> expectedResultList) {
if (StringUtils.equals(caseEditType, FunctionalCaseTypeConstants.CaseEditType.TEXT.name())) {
data.setTextDescription(new String(functionalCaseBlob.getTextDescription() == null ? new byte[0] : functionalCaseBlob.getTextDescription(), StandardCharsets.UTF_8));
data.setExpectedResult(new String(functionalCaseBlob.getExpectedResult() == null ? new byte[0] : functionalCaseBlob.getExpectedResult(), StandardCharsets.UTF_8));
} else {
String steps = new String(functionalCaseBlob.getSteps() == null ? new byte[0] : functionalCaseBlob.getSteps(), StandardCharsets.UTF_8);
List jsonArray = new ArrayList();
try {
jsonArray = JSON.parseArray(steps);
} catch (Exception e) {
if (steps.contains("null") && !steps.contains("\"null\"")) {
steps = steps.replace("null", "\"\"");
jsonArray = JSON.parseArray(steps);
}
}
for (int j = 0; j < jsonArray.size(); j++) {
// 将步骤存储起来之后生成多条数据再合并单元格
Map item = (Map) jsonArray.get(j);
String textDescription = Optional.ofNullable(item.get("desc")).orElse(StringUtils.EMPTY).toString();
String expectedResult = Optional.ofNullable(item.get("result")).orElse(StringUtils.EMPTY).toString();
if (StringUtils.isNotBlank(textDescription) || StringUtils.isNotBlank(expectedResult)) {
textDescriptionList.add(textDescription);
expectedResultList.add(expectedResult);
}
}
}
}
/**
* 获取模块map
*
* @param projectId
* @return
*/
private Map<String, String> getModuleMap(String projectId) {
List<BaseTreeNode> moduleTree = functionalCaseModuleService.getTree(projectId);
Map<String, String> moduleMap = moduleTree.stream().collect(Collectors.toMap(BaseTreeNode::getId, BaseTreeNode::getPath));
return moduleMap;
}
/**
* 获取导出表头
*
* @param request
* @return
*/
private List<List<String>> getFunctionalCaseExportHeads(FunctionalCaseExportRequest request) {
List<List<String>> headList = new ArrayList<>() {
@Serial
private static final long serialVersionUID = 5726921174161850104L;
{
addAll(request.getSystemFields()
.stream()
.map(item -> Arrays.asList(item.getName()))
.collect(Collectors.toList()));
addAll(request.getCustomFields()
.stream()
.map(item -> Arrays.asList(item.getName()))
.collect(Collectors.toList()));
addAll(request.getOtherFields()
.stream()
.map(item -> Arrays.asList(item.getName()))
.collect(Collectors.toList()));
}
};
return headList;
}
} }

View File

@ -51,19 +51,12 @@ public class FunctionalCaseLogService {
private FunctionalCaseAttachmentMapper functionalCaseAttachmentMapper; private FunctionalCaseAttachmentMapper functionalCaseAttachmentMapper;
@Resource @Resource
private FileAssociationMapper fileAssociationMapper; private FileAssociationMapper fileAssociationMapper;
@Resource
private CustomFieldMapper customFieldMapper;
@Resource @Resource
private BugRelationCaseMapper bugRelationCaseMapper; private BugRelationCaseMapper bugRelationCaseMapper;
@Resource @Resource
private BugMapper bugMapper; private BugMapper bugMapper;
@Resource
private ExtFunctionalCaseModuleMapper extFunctionalCaseModuleMapper;
/** /**
* 更新用例 日志 * 更新用例 日志
@ -434,4 +427,21 @@ public class FunctionalCaseLogService {
dto.setMethod(HttpMethodConstants.POST.name()); dto.setMethod(HttpMethodConstants.POST.name());
return dto; return dto;
} }
public LogDTO exportExcelLog(FunctionalCaseExportRequest request) {
LogDTO dto = new LogDTO(
request.getProjectId(),
null,
request.getFileId(),
null,
OperationLogType.EXPORT.name(),
OperationLogModule.FUNCTIONAL_CASE,
"");
dto.setHistory(true);
dto.setPath("/functional/case/export/excel");
dto.setMethod(HttpMethodConstants.POST.name());
return dto;
}
} }

View File

@ -303,7 +303,7 @@ public class FunctionalCaseModuleService extends ModuleTreeService {
currentModuleName = itemIterator.next().trim(); currentModuleName = itemIterator.next().trim();
moduleTree.forEach(module -> { moduleTree.forEach(module -> {
//根节点是否存在 //根节点是否存在
if (StringUtils.equals(currentModuleName, module.getName())) { if (StringUtils.equalsIgnoreCase(currentModuleName, module.getName())) {
hasNode.set(true); hasNode.set(true);
//根节点存在检查子节点是否存在 //根节点存在检查子节点是否存在
createModuleByPathIterator(itemIterator, "/" + currentModuleName, module, pathMap, projectId, userId); createModuleByPathIterator(itemIterator, "/" + currentModuleName, module, pathMap, projectId, userId);
@ -342,7 +342,7 @@ public class FunctionalCaseModuleService extends ModuleTreeService {
String nodeName = itemIterator.next().trim(); String nodeName = itemIterator.next().trim();
AtomicReference<Boolean> hasNode = new AtomicReference<>(false); AtomicReference<Boolean> hasNode = new AtomicReference<>(false);
children.forEach(child -> { children.forEach(child -> {
if (StringUtils.equals(nodeName, child.getName())) { if (StringUtils.equalsIgnoreCase(nodeName, child.getName())) {
hasNode.set(true); hasNode.set(true);
createModuleByPathIterator(itemIterator, currentModulePath + "/" + child.getName(), child, pathMap, projectId, userId); createModuleByPathIterator(itemIterator, currentModulePath + "/" + child.getName(), child, pathMap, projectId, userId);
} }

View File

@ -1158,7 +1158,7 @@ public class FunctionalCaseService {
private void noticeModule(List<FunctionalCaseDTO> noticeList, FunctionalCaseExcelData functionalCaseExcelData, FunctionalCaseImportRequest request, String userId, Map<String, TemplateCustomFieldDTO> customFieldsMap) { private void noticeModule(List<FunctionalCaseDTO> noticeList, FunctionalCaseExcelData functionalCaseExcelData, FunctionalCaseImportRequest request, String userId, Map<String, TemplateCustomFieldDTO> customFieldsMap) {
FunctionalCaseDTO functionalCaseDTO = new FunctionalCaseDTO(); FunctionalCaseDTO functionalCaseDTO = new FunctionalCaseDTO();
functionalCaseDTO.setTriggerMode(Translator.get("log.test_plan.functional_case")); functionalCaseDTO.setTriggerMode(Translator.get("log.test_plan.functional_case"));
functionalCaseDTO.setName(functionalCaseExcelData.getName()); functionalCaseDTO.setName(functionalCaseExcelData.getName().toString());
functionalCaseDTO.setProjectId(request.getProjectId()); functionalCaseDTO.setProjectId(request.getProjectId());
functionalCaseDTO.setCaseEditType(functionalCaseExcelData.getCaseEditType()); functionalCaseDTO.setCaseEditType(functionalCaseExcelData.getCaseEditType());
functionalCaseDTO.setCreateUser(userId); functionalCaseDTO.setCreateUser(userId);
@ -1187,7 +1187,7 @@ public class FunctionalCaseService {
functionalCase.setModuleId(caseModulePathMap.get(functionalCaseExcelData.getModule())); functionalCase.setModuleId(caseModulePathMap.get(functionalCaseExcelData.getModule()));
functionalCase.setProjectId(request.getProjectId()); functionalCase.setProjectId(request.getProjectId());
functionalCase.setTemplateId(defaultTemplateDTO.getId()); functionalCase.setTemplateId(defaultTemplateDTO.getId());
functionalCase.setName(functionalCaseExcelData.getName()); functionalCase.setName(functionalCaseExcelData.getName().toString());
functionalCase.setReviewStatus(FunctionalCaseReviewStatus.UN_REVIEWED.name()); functionalCase.setReviewStatus(FunctionalCaseReviewStatus.UN_REVIEWED.name());
functionalCase.setTags(handleImportTags(functionalCaseExcelData.getTags())); functionalCase.setTags(handleImportTags(functionalCaseExcelData.getTags()));
functionalCase.setCaseEditType(StringUtils.defaultIfBlank(functionalCaseExcelData.getCaseEditType(), FunctionalCaseTypeConstants.CaseEditType.TEXT.name())); functionalCase.setCaseEditType(StringUtils.defaultIfBlank(functionalCaseExcelData.getCaseEditType(), FunctionalCaseTypeConstants.CaseEditType.TEXT.name()));
@ -1384,7 +1384,7 @@ public class FunctionalCaseService {
//用例表 //用例表
FunctionalCase functionalCase = collect.get(functionalCaseExcelData.getNum()).getFirst(); FunctionalCase functionalCase = collect.get(functionalCaseExcelData.getNum()).getFirst();
functionalCase.setName(functionalCaseExcelData.getName()); functionalCase.setName(functionalCaseExcelData.getName().toString());
functionalCase.setModuleId(caseModulePathMap.get(functionalCaseExcelData.getModule())); functionalCase.setModuleId(caseModulePathMap.get(functionalCaseExcelData.getModule()));
functionalCase.setTags(handleImportTags(functionalCaseExcelData.getTags())); functionalCase.setTags(handleImportTags(functionalCaseExcelData.getTags()));
functionalCase.setCaseEditType(StringUtils.defaultIfBlank(functionalCaseExcelData.getCaseEditType(), FunctionalCaseTypeConstants.CaseEditType.TEXT.name())); functionalCase.setCaseEditType(StringUtils.defaultIfBlank(functionalCaseExcelData.getCaseEditType(), FunctionalCaseTypeConstants.CaseEditType.TEXT.name()));
@ -1417,7 +1417,7 @@ public class FunctionalCaseService {
FunctionalCase functionalCase = collect.get(functionalCaseExcelData.getNum()).getFirst(); FunctionalCase functionalCase = collect.get(functionalCaseExcelData.getNum()).getFirst();
if (CollectionUtils.isNotEmpty(projectApplications) && Boolean.valueOf(projectApplications.getFirst().getTypeValue())) { if (CollectionUtils.isNotEmpty(projectApplications) && Boolean.valueOf(projectApplications.getFirst().getTypeValue())) {
FunctionalCaseBlob blob = blobsCollect.get(functionalCaseExcelData.getNum()).getFirst(); FunctionalCaseBlob blob = blobsCollect.get(functionalCaseExcelData.getNum()).getFirst();
if (!StringUtils.equals(functionalCase.getName(), functionalCaseExcelData.getName()) if (!StringUtils.equals(functionalCase.getName(), functionalCaseExcelData.getName().toString())
|| !StringUtils.equals(new String(blob.getSteps(), StandardCharsets.UTF_8), StringUtils.defaultIfBlank(functionalCaseExcelData.getSteps(), StringUtils.EMPTY)) || !StringUtils.equals(new String(blob.getSteps(), StandardCharsets.UTF_8), StringUtils.defaultIfBlank(functionalCaseExcelData.getSteps(), StringUtils.EMPTY))
|| !StringUtils.equals(new String(blob.getTextDescription(), StandardCharsets.UTF_8), StringUtils.defaultIfBlank(functionalCaseExcelData.getTextDescription(), StringUtils.EMPTY)) || !StringUtils.equals(new String(blob.getTextDescription(), StandardCharsets.UTF_8), StringUtils.defaultIfBlank(functionalCaseExcelData.getTextDescription(), StringUtils.EMPTY))
|| !StringUtils.equals(new String(blob.getExpectedResult(), StandardCharsets.UTF_8), StringUtils.defaultIfBlank(functionalCaseExcelData.getExpectedResult(), StringUtils.EMPTY))) { || !StringUtils.equals(new String(blob.getExpectedResult(), StandardCharsets.UTF_8), StringUtils.defaultIfBlank(functionalCaseExcelData.getExpectedResult(), StringUtils.EMPTY))) {

View File

@ -0,0 +1,98 @@
package io.metersphere.functional.socket;
import io.metersphere.sdk.constants.MsgType;
import io.metersphere.sdk.dto.SocketMsgDTO;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.LogUtils;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@Component
@ServerEndpoint("/ws/export/{fileId}")
public class ExportWebSocketHandler {
public static final Map<String, Session> ONLINE_EXPORT_EXCEL_SESSIONS = new ConcurrentHashMap<>();
public static void sendMessage(Session session, SocketMsgDTO message) {
if (session == null) {
return;
}
// 替换了web容器后 jetty没有设置永久有效的参数这里暂时设置超时时间为一天
session.setMaxIdleTimeout(86400000L);
RemoteEndpoint.Async async = session.getAsyncRemote();
if (async == null) {
return;
}
async.sendText(JSON.toJSONString(message));
}
public static void sendMessageSingle(SocketMsgDTO dto) {
sendMessage(ONLINE_EXPORT_EXCEL_SESSIONS.get(Optional.ofNullable(dto.getReportId())
.orElse(StringUtils.EMPTY)), dto);
}
/**
* 连接成功响应
*/
@OnOpen
public void openSession(@PathParam("fileId") String fileId, Session session) {
ONLINE_EXPORT_EXCEL_SESSIONS.put(fileId, session);
RemoteEndpoint.Async async = session.getAsyncRemote();
if (async != null) {
async.sendText(JSON.toJSONString(new SocketMsgDTO(fileId, "", MsgType.CONNECT.name(), MsgType.CONNECT.name())));
session.setMaxIdleTimeout(180000);
}
LogUtils.info("客户端: [" + fileId + "] : 连接成功!" + ExportWebSocketHandler.ONLINE_EXPORT_EXCEL_SESSIONS.size(), fileId);
}
/**
* 收到消息响应
*/
@OnMessage
public void onMessage(@PathParam("fileId") String fileId, String message) {
LogUtils.info("服务器收到:[" + fileId + "] : " + message);
SocketMsgDTO dto = JSON.parseObject(message, SocketMsgDTO.class);
ExportWebSocketHandler.sendMessageSingle(dto);
}
/**
* 连接关闭响应
*/
@OnClose
public void onClose(@PathParam("fileId") String fileId, Session session) throws IOException {
//当前的Session 移除
ExportWebSocketHandler.ONLINE_EXPORT_EXCEL_SESSIONS.remove(fileId);
LogUtils.info("[" + fileId + "] : 断开连接!" + ExportWebSocketHandler.ONLINE_EXPORT_EXCEL_SESSIONS.size());
//并且通知其他人当前用户已经断开连接了
session.close();
}
/**
* 连接异常响应
*/
@OnError
public void onError(Session session, Throwable throwable) throws IOException {
LogUtils.error("连接异常响应", throwable);
session.close();
}
/**
* 每一分钟群发一次心跳检查
*/
@Scheduled(fixedRate = 60000)
public void heartbeatCheck() {
ExportWebSocketHandler.sendMessageSingle(
new SocketMsgDTO(MsgType.HEARTBEAT.name(), MsgType.HEARTBEAT.name(), MsgType.HEARTBEAT.name(), "heartbeat check")
);
}
}

View File

@ -8,6 +8,7 @@ import io.metersphere.functional.dto.CaseCustomFieldDTO;
import io.metersphere.functional.dto.FunctionalCaseAttachmentDTO; import io.metersphere.functional.dto.FunctionalCaseAttachmentDTO;
import io.metersphere.functional.dto.FunctionalCasePageDTO; import io.metersphere.functional.dto.FunctionalCasePageDTO;
import io.metersphere.functional.dto.response.FunctionalCaseImportResponse; import io.metersphere.functional.dto.response.FunctionalCaseImportResponse;
import io.metersphere.functional.excel.domain.FunctionalCaseHeader;
import io.metersphere.functional.mapper.FunctionalCaseAttachmentMapper; import io.metersphere.functional.mapper.FunctionalCaseAttachmentMapper;
import io.metersphere.functional.mapper.FunctionalCaseCustomFieldMapper; import io.metersphere.functional.mapper.FunctionalCaseCustomFieldMapper;
import io.metersphere.functional.request.*; import io.metersphere.functional.request.*;
@ -85,6 +86,7 @@ public class FunctionalCaseControllerTests extends BaseTest {
public static final String CHECK_EXCEL_URL = "/functional/case/pre-check/excel"; public static final String CHECK_EXCEL_URL = "/functional/case/pre-check/excel";
public static final String IMPORT_EXCEL_URL = "/functional/case/import/excel"; public static final String IMPORT_EXCEL_URL = "/functional/case/import/excel";
public static final String OPERATION_HISTORY_URL = "/functional/case/operation-history"; public static final String OPERATION_HISTORY_URL = "/functional/case/operation-history";
public static final String EXPORT_EXCEL_URL = "/functional/case/export/excel";
@Resource @Resource
private NotificationMapper notificationMapper; private NotificationMapper notificationMapper;
@ -795,4 +797,41 @@ public class FunctionalCaseControllerTests extends BaseTest {
Assertions.assertTrue(CollectionUtils.isNotEmpty(operationHistoryDTOS)); Assertions.assertTrue(CollectionUtils.isNotEmpty(operationHistoryDTOS));
} }
@Test
@Order(2)
public void exportExcel() 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_EXCEL_URL, request);
}
} }

View File

@ -28,6 +28,7 @@ public class MinioConfig {
// 设置临时目录下文件的过期时间 // 设置临时目录下文件的过期时间
setBucketLifecycle(minioClient); setBucketLifecycle(minioClient);
setBucketLifecycleByExcel(minioClient);
boolean exist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(BUCKET).build()); boolean exist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(BUCKET).build());
if (!exist) { if (!exist) {
@ -66,4 +67,36 @@ public class MinioConfig {
} }
} }
/**
* 设置生命周期规则-文件的过期时间
* system/export/excel/ 下的文件设置为 1 天后过期
* 参考 minio 8.5.2 版本的示例代码
* https://github.com/minio/minio-java/blob/8.5.2/examples/SetBucketLifecycle.java
*/
private static void setBucketLifecycleByExcel(MinioClient minioClient) {
List<LifecycleRule> rules = new LinkedList<>();
rules.add(
new LifecycleRule(
Status.ENABLED,
null,
new Expiration((ZonedDateTime) null, 1, null),
new RuleFilter("system/export/excel"),
"excel-file",
null,
null,
null));
LifecycleConfiguration config = new LifecycleConfiguration(rules);
try {
minioClient.setBucketLifecycle(
SetBucketLifecycleArgs.builder()
.bucket(BUCKET)
.config(config)
.build());
} catch (Exception e) {
LogUtils.error(e);
}
}
} }