diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/DefaultRepositoryDir.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/DefaultRepositoryDir.java index 791508ab60..11ebe15cf1 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/DefaultRepositoryDir.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/DefaultRepositoryDir.java @@ -32,6 +32,7 @@ public class DefaultRepositoryDir { * 会定时清理 */ 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: 系统下资源目录 --------*/ @@ -158,6 +159,9 @@ public class DefaultRepositoryDir { return SYSTEM_TEMP_DIR; } + public static String getExportExcelTempDir() { + return EXPORT_EXCEL_TEMP_DIR; + } public static String getSystemTempCompressDir() { return SYSTEM_TEMP_DIR + "/compress"; } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/CompressUtils.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/CompressUtils.java index 33ff621863..d10db7074a 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/CompressUtils.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/CompressUtils.java @@ -91,8 +91,9 @@ public class CompressUtils { /** * 将多个文件压缩 - * @param zipFilePath 压缩文件所在路径 - * @param fileList 要压缩的文件 + * + * @param zipFilePath 压缩文件所在路径 + * @param fileList 要压缩的文件 * @return * @throws IOException */ @@ -113,6 +114,31 @@ public class CompressUtils { return zipFile; } + /** + * 将多个文件压缩至指定路径 + * + * @param fileList 待压缩的文件列表 + * @param zipFilePath 压缩文件路径 + * @return 返回压缩好的文件 + * @throws IOException + */ + public static File zipFilesToPath(String zipFilePath, List 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解压 * diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/MsFileUtils.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/MsFileUtils.java index b76ee893d2..90bb8a55c7 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/MsFileUtils.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/util/MsFileUtils.java @@ -1,6 +1,7 @@ package io.metersphere.sdk.util; import io.metersphere.sdk.exception.MSException; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; 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); + } } diff --git a/backend/framework/sdk/src/main/resources/i18n/case_en_US.properties b/backend/framework/sdk/src/main/resources/i18n/case_en_US.properties index 95ef817343..8d028c8793 100644 --- a/backend/framework/sdk/src/main/resources/i18n/case_en_US.properties +++ b/backend/framework/sdk/src/main/resources/i18n/case_en_US.properties @@ -246,4 +246,14 @@ case.minder.all.case=All Case case.minder.status.success=success case.minder.status.error=error -case.minder.status.blocked=blocked \ No newline at end of file +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 \ No newline at end of file diff --git a/backend/framework/sdk/src/main/resources/i18n/case_zh_CN.properties b/backend/framework/sdk/src/main/resources/i18n/case_zh_CN.properties index bb6861da5d..ca8855aa06 100644 --- a/backend/framework/sdk/src/main/resources/i18n/case_zh_CN.properties +++ b/backend/framework/sdk/src/main/resources/i18n/case_zh_CN.properties @@ -246,3 +246,12 @@ case.minder.status.success=成功 case.minder.status.error=失败 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 \ No newline at end of file diff --git a/backend/framework/sdk/src/main/resources/i18n/case_zh_TW.properties b/backend/framework/sdk/src/main/resources/i18n/case_zh_TW.properties index 016fb9c644..d8985e485a 100644 --- a/backend/framework/sdk/src/main/resources/i18n/case_zh_TW.properties +++ b/backend/framework/sdk/src/main/resources/i18n/case_zh_TW.properties @@ -247,3 +247,13 @@ case.minder.status.success=成功 case.minder.status.error=失敗 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 + diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseController.java b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseController.java index d1992ba7cb..af0992ab30 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseController.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseController.java @@ -33,6 +33,7 @@ import io.metersphere.system.utils.SessionUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotBlank; import org.apache.shiro.authz.annotation.Logical; @@ -223,7 +224,7 @@ public class FunctionalCaseController { @PostMapping("/pre-check/excel") @Operation(summary = "用例管理-功能用例-excel导入检查") - @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_UPDATE) + @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_IMPORT) @CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project") public FunctionalCaseImportResponse preCheckExcel(@RequestPart("request") FunctionalCaseImportRequest request, @RequestPart(value = "file", required = false) MultipartFile file) { return functionalCaseFileService.preCheckExcel(request, file); @@ -232,7 +233,7 @@ public class FunctionalCaseController { @PostMapping("/import/excel") @Operation(summary = "用例管理-功能用例-excel导入") - @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_UPDATE) + @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_IMPORT) @CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project") public FunctionalCaseImportResponse importExcel(@RequestPart("request") FunctionalCaseImportRequest request, @RequestPart(value = "file", required = false) MultipartFile file) { SessionUser user = SessionUtils.getUser(); @@ -248,4 +249,12 @@ public class FunctionalCaseController { StringUtils.isNotBlank(request.getSortString()) ? request.getSortString() : "create_time desc"); 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); + } } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/constants/FunctionalCaseExportOtherField.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/constants/FunctionalCaseExportOtherField.java new file mode 100644 index 0000000000..b4cca2f9e5 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/constants/FunctionalCaseExportOtherField.java @@ -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; + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExecuteStatus.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExecuteStatus.java new file mode 100644 index 0000000000..4c2e733029 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExecuteStatus.java @@ -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; + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportCaseCommentConverter.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportCaseCommentConverter.java new file mode 100644 index 0000000000..56439de4ed --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportCaseCommentConverter.java @@ -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> caseCommentMap, Map> executeCommentMap, Map> reviewCommentMap) { + if (caseCommentMap.containsKey(functionalCase.getId())) { + StringBuilder result = new StringBuilder(); + String template = Translator.get("functional_case_comment_template"); + List 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; + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportConverter.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportConverter.java new file mode 100644 index 0000000000..72f335c4e9 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportConverter.java @@ -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> caseCommentMap, Map> executeCommentMap, Map> reviewCommentMap); + + default String getFromMapOfNullable(Map map, String key) { + if (StringUtils.isNotBlank(key)) { + return map.get(key); + } + return StringUtils.EMPTY; + } + + default String getFromMapOfNullableWithTranslate(Map map, String key) { + String value = getFromMapOfNullable(map, key); + if (StringUtils.isNotBlank(value)) { + return Translator.get(value); + } + return value; + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportConverterFactory.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportConverterFactory.java new file mode 100644 index 0000000000..858d61b40b --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportConverterFactory.java @@ -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 getConverters(List keys) { + Map converterMapResult = new HashMap<>(); + try { + HashMap> converterMap = getConverterMap(); + for (String key : keys) { + Class 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 clazz = getConverterMap().get(key); + if (clazz != null) { + return clazz.getDeclaredConstructor().newInstance(); + } + } catch (Exception e) { + LogUtils.error(e); + } + return null; + } + + private static HashMap> 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); + }}; + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportCreateTimeConverter.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportCreateTimeConverter.java new file mode 100644 index 0000000000..03e8e1657f --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportCreateTimeConverter.java @@ -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> caseCommentMap, Map> executeCommentMap, Map> reviewCommentMap) { + return DateUtils.getTimeString(functionalCase.getCreateTime()); + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportCreateUserConverter.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportCreateUserConverter.java new file mode 100644 index 0000000000..4801c319af --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportCreateUserConverter.java @@ -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 userMap = new HashMap<>(); + + public FunctionalCaseExportCreateUserConverter() { + ProjectApplicationService projectApplicationService = CommonBeanFactory.getBean(ProjectApplicationService.class); + List memberOption = projectApplicationService.getProjectUserList(SessionUtils.getCurrentProjectId()); + memberOption.forEach(option -> userMap.put(option.getId(), option.getName())); + } + + + @Override + public String parse(FunctionalCase functionalCase, Map> caseCommentMap, Map> executeCommentMap, Map> reviewCommentMap) { + return getFromMapOfNullable(userMap, functionalCase.getCreateUser()); + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportExecuteCommentConverter.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportExecuteCommentConverter.java new file mode 100644 index 0000000000..e75affb7fd --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportExecuteCommentConverter.java @@ -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 executeStatusMap = new HashMap<>(); + + public FunctionalCaseExportExecuteCommentConverter() { + for (FunctionalCaseExecuteStatus value : FunctionalCaseExecuteStatus.values()) { + executeStatusMap.put(value.name(), value.getI18nKey()); + } + } + + + @Override + public String parse(FunctionalCase functionalCase, Map> caseCommentMap, Map> executeCommentMap, Map> reviewCommentMap) { + if (executeCommentMap.containsKey(functionalCase.getId())) { + StringBuilder result = new StringBuilder(); + String template = Translator.get("functional_case_execute_comment_template"); + List 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; + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportExecuteStatusConverter.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportExecuteStatusConverter.java new file mode 100644 index 0000000000..8f4493b266 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportExecuteStatusConverter.java @@ -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 executeStatusMap = new HashMap<>(); + + public FunctionalCaseExportExecuteStatusConverter() { + for (FunctionalCaseExecuteStatus value : FunctionalCaseExecuteStatus.values()) { + executeStatusMap.put(value.name(), value.getI18nKey()); + } + } + + + @Override + public String parse(FunctionalCase functionalCase, Map> caseCommentMap, Map> executeCommentMap, Map> reviewCommentMap) { + return getFromMapOfNullableWithTranslate(executeStatusMap, functionalCase.getLastExecuteResult()); + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportReviewCommentConverter.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportReviewCommentConverter.java new file mode 100644 index 0000000000..dc5eca86dc --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportReviewCommentConverter.java @@ -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 reviewStatusMap = new HashMap<>(); + + public FunctionalCaseExportReviewCommentConverter() { + for (FunctionalCaseReviewStatus value : FunctionalCaseReviewStatus.values()) { + reviewStatusMap.put(value.name(), value.getI18nKey()); + } + } + + @Override + public String parse(FunctionalCase functionalCase, Map> caseCommentMap, Map> executeCommentMap, Map> reviewCommentMap) { + if (reviewCommentMap.containsKey(functionalCase.getId())) { + StringBuilder result = new StringBuilder(); + String template = Translator.get("functional_case_review_comment_template"); + List 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; + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportReviewStatusConverter.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportReviewStatusConverter.java new file mode 100644 index 0000000000..e27c331702 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportReviewStatusConverter.java @@ -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 caseReviewStatusMap = new HashMap<>(); + + public FunctionalCaseExportReviewStatusConverter() { + for (FunctionalCaseReviewStatus value : FunctionalCaseReviewStatus.values()) { + caseReviewStatusMap.put(value.name(), value.getI18nKey()); + } + } + + @Override + public String parse(FunctionalCase functionalCase, Map> caseCommentMap, Map> executeCommentMap, Map> reviewCommentMap) { + return getFromMapOfNullableWithTranslate(caseReviewStatusMap, functionalCase.getReviewStatus()); + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportUpdateTimeConverter.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportUpdateTimeConverter.java new file mode 100644 index 0000000000..3d8fde62bc --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportUpdateTimeConverter.java @@ -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> caseCommentMap, Map> executeCommentMap, Map> reviewCommentMap) { + return DateUtils.getTimeString(functionalCase.getUpdateTime()); + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportUpdateUserConverter.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportUpdateUserConverter.java new file mode 100644 index 0000000000..71630204d8 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseExportUpdateUserConverter.java @@ -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> caseCommentMap, Map> executeCommentMap, Map> reviewCommentMap) { + return getFromMapOfNullable(userMap, functionalCase.getUpdateUser()); + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseReviewStatus.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseReviewStatus.java new file mode 100644 index 0000000000..2183079377 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/converter/FunctionalCaseReviewStatus.java @@ -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; + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelData.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelData.java index 9ed595f6f1..e0052a455d 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelData.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelData.java @@ -1,6 +1,7 @@ package io.metersphere.functional.excel.domain; import com.alibaba.excel.annotation.ExcelIgnore; +import com.alibaba.excel.metadata.data.WriteCellData; import io.metersphere.functional.excel.constants.FunctionalCaseImportFiled; import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; import lombok.Getter; @@ -40,6 +41,9 @@ public class FunctionalCaseExcelData { @ExcelIgnore Map otherFields; + @ExcelIgnore + private WriteCellData hyperLinkName; + /** * 合并文本描述 */ diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseHeader.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseHeader.java new file mode 100644 index 0000000000..87fca934ae --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseHeader.java @@ -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; +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/handler/FunctionCaseMergeWriteHandler.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/handler/FunctionCaseMergeWriteHandler.java new file mode 100644 index 0000000000..523405d6ca --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/handler/FunctionCaseMergeWriteHandler.java @@ -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 rowMergeInfo; + List> headList; + int textDescriptionRowIndex; + int expectedResultRowIndex; + + public FunctionCaseMergeWriteHandler(Map rowMergeInfo, List> headList) { + this.rowMergeInfo = rowMergeInfo; + this.headList = headList; + for (int i = 0; i < headList.size(); i++) { + List 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); + } + } + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/handler/FunctionCaseTemplateWriteHandler.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/handler/FunctionCaseTemplateWriteHandler.java index a1d184a812..876f56b22c 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/handler/FunctionCaseTemplateWriteHandler.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/handler/FunctionCaseTemplateWriteHandler.java @@ -29,7 +29,7 @@ import java.util.Map; */ public class FunctionCaseTemplateWriteHandler implements RowWriteHandler { - Map> caseLevelAndStatusValueMap; + Map> customFieldOptionsMap; private Sheet sheet; private Drawing drawingPatriarch; @@ -37,9 +37,9 @@ public class FunctionCaseTemplateWriteHandler implements RowWriteHandler { private Map customField; private Map fieldMap = new HashMap<>(); - public FunctionCaseTemplateWriteHandler(List> headList, Map> caseLevelAndStatusValueMap, Map customFieldMap) { + public FunctionCaseTemplateWriteHandler(List> headList, Map> customFieldOptionsMap, Map customFieldMap) { initIndex(headList); - this.caseLevelAndStatusValueMap = caseLevelAndStatusValueMap; + this.customFieldOptionsMap = customFieldOptionsMap; this.customField = customFieldMap; } @@ -94,7 +94,7 @@ public class FunctionCaseTemplateWriteHandler implements RowWriteHandler { //自定义字段 if (customField.containsKey(entry.getKey())) { TemplateCustomFieldDTO templateCustomFieldDTO = customField.get(entry.getKey()); - List strings = caseLevelAndStatusValueMap.get(entry.getKey()); + List strings = customFieldOptionsMap.get(entry.getKey()); if (StringUtils.equalsAnyIgnoreCase(templateCustomFieldDTO.getType(), CustomFieldType.MULTIPLE_MEMBER.name(), CustomFieldType.MEMBER.name())) { if (templateCustomFieldDTO.getRequired()) { 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 { if (templateCustomFieldDTO.getRequired()) { 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 { setComment(fieldMap.get(entry.getKey()), Translator.get("required")); } } else { 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 { setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.not_required")); } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/AbstractCustomFieldValidator.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/AbstractCustomFieldValidator.java index 3a2c194238..e9f1954b81 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/AbstractCustomFieldValidator.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/AbstractCustomFieldValidator.java @@ -71,4 +71,8 @@ public abstract class AbstractCustomFieldValidator { } return new ArrayList<>(); } + + public Object parse2Value(String value, TemplateCustomFieldDTO customField) { + return value; + } } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMemberValidator.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMemberValidator.java index 8462a0c6ac..e16b977890 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMemberValidator.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMemberValidator.java @@ -21,6 +21,7 @@ import java.util.stream.Collectors; public class CustomFieldMemberValidator extends AbstractCustomFieldValidator { protected Map userIdMap; + protected Map userEmailMap; protected Map userNameMap; public CustomFieldMemberValidator() { @@ -31,9 +32,12 @@ public class CustomFieldMemberValidator extends AbstractCustomFieldValidator { .collect( 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<>(); memberOption.stream() - .forEach(user -> userNameMap.put(user.getEmail().toLowerCase(), user.getId())); + .forEach(user -> userNameMap.put(user.getId(), user.getName().toLowerCase())); } @Override @@ -43,7 +47,7 @@ public class CustomFieldMemberValidator extends AbstractCustomFieldValidator { return; } value = value.toLowerCase(); - if (userIdMap.containsKey(value) || userNameMap.containsKey(value)) { + if (userIdMap.containsKey(value) || userEmailMap.containsKey(value)) { return; } 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)) { 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)) { return userNameMap.get(keyOrValue); } return keyOrValue; } + } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleMemberValidator.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleMemberValidator.java index e36d1a3b36..2fe521f02e 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleMemberValidator.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleMemberValidator.java @@ -24,7 +24,7 @@ public class CustomFieldMultipleMemberValidator extends CustomFieldMemberValidat for (String item : parse2Array(customField.getFieldName(), value)) { 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())); } } @@ -42,10 +42,27 @@ public class CustomFieldMultipleMemberValidator extends CustomFieldMemberValidat if (userIdMap.containsKey(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 keyOrValues = parse2Array(keyOrValuesStr); + + for (int i = 0; i < keyOrValues.size(); i++) { + String item = keyOrValues.get(i).toLowerCase(); if (userNameMap.containsKey(item)) { keyOrValues.set(i, userNameMap.get(item)); } } return JSON.toJSONString(keyOrValues); } + } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleSelectValidator.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleSelectValidator.java index f4b3540457..293daeb49c 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleSelectValidator.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleSelectValidator.java @@ -4,13 +4,12 @@ package io.metersphere.functional.excel.validate; import io.metersphere.functional.excel.exception.CustomFieldValidateException; import io.metersphere.sdk.util.JSON; import io.metersphere.sdk.util.Translator; +import io.metersphere.system.domain.CustomFieldOption; import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; import org.apache.commons.lang3.StringUtils; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; +import java.util.stream.Collectors; /** * @author wx @@ -48,4 +47,21 @@ public class CustomFieldMultipleSelectValidator extends CustomFieldSelectValidat } return JSON.toJSONString(keyOrValues); } + + @Override + public Object parse2Value(String keyOrValuesStr, TemplateCustomFieldDTO customField) { + Map optionValueMap = customField.getOptions().stream().collect(Collectors.toMap(CustomFieldOption::getFieldId, CustomFieldOption::getValue)); + if (StringUtils.isBlank(keyOrValuesStr)) { + return JSON.toJSONString(new ArrayList<>()); + } + List 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); + } + } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleTextValidator.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleTextValidator.java index 0753c90ff3..4f78e74e69 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleTextValidator.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldMultipleTextValidator.java @@ -51,4 +51,14 @@ public class CustomFieldMultipleTextValidator extends AbstractCustomFieldValidat return JSON.toJSONString(keyOrValues); } + + @Override + public Object parse2Value(String keyOrValuesStr, TemplateCustomFieldDTO customField) { + if (StringUtils.isBlank(keyOrValuesStr)) { + return JSON.toJSONString(new ArrayList<>()); + } + List keyOrValues = parse2Array(keyOrValuesStr); + + return JSON.toJSONString(keyOrValues); + } } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldSelectValidator.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldSelectValidator.java index e180e1138d..e4007b1000 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldSelectValidator.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/validate/CustomFieldSelectValidator.java @@ -60,6 +60,15 @@ public class CustomFieldSelectValidator extends AbstractCustomFieldValidator { return keyOrValuesStr; } + @Override + public Object parse2Value(String keyOrValuesStr, TemplateCustomFieldDTO customField) { + Map optionValueMap = customField.getOptions().stream().collect(Collectors.toMap(CustomFieldOption::getFieldId, CustomFieldOption::getValue)); + if (optionValueMap.containsKey(keyOrValuesStr)) { + return optionValueMap.get(keyOrValuesStr); + } + return keyOrValuesStr; + } + /** * 获取自定义字段的选项值和key * 存储到缓存中,增强导入时性能 diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/mapper/ExtFunctionalCaseCommentMapper.java b/backend/services/case-management/src/main/java/io/metersphere/functional/mapper/ExtFunctionalCaseCommentMapper.java new file mode 100644 index 0000000000..c2e0866e4e --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/mapper/ExtFunctionalCaseCommentMapper.java @@ -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 getCaseComment(@Param("ids") List ids); + + List getExecuteComment(@Param("ids") List ids); + + List getReviewComment(@Param("ids") List ids); +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/mapper/ExtFunctionalCaseCommentMapper.xml b/backend/services/case-management/src/main/java/io/metersphere/functional/mapper/ExtFunctionalCaseCommentMapper.xml new file mode 100644 index 0000000000..29ac9225d1 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/mapper/ExtFunctionalCaseCommentMapper.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/request/FunctionalCaseExportRequest.java b/backend/services/case-management/src/main/java/io/metersphere/functional/request/FunctionalCaseExportRequest.java new file mode 100644 index 0000000000..03628d8b7b --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/request/FunctionalCaseExportRequest.java @@ -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 systemFields = new ArrayList<>(); + + @Schema(description = "自定义字段") + private List customFields = new ArrayList<>(); + + @Schema(description = "其他字段") + private List otherFields = new ArrayList<>(); + + @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String projectId; + + @Schema(description = "文件id") + private String fileId; + +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseFileService.java b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseFileService.java index 6dafd42124..69480f5e81 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseFileService.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseFileService.java @@ -3,36 +3,72 @@ package io.metersphere.functional.service; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.EasyExcelFactory; 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.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.FunctionalCaseExcelData; 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.listener.FunctionalCaseCheckEventListener; import io.metersphere.functional.excel.listener.FunctionalCaseImportEventListener; 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.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.ProjectMapper; 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.util.LogUtils; -import io.metersphere.sdk.util.Translator; +import io.metersphere.sdk.file.FileRequest; +import io.metersphere.sdk.util.*; 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.TemplateCustomFieldDTO; import io.metersphere.system.dto.sdk.TemplateDTO; 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 jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.collections.CollectionUtils; 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.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.nio.charset.StandardCharsets; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; /** @@ -50,7 +86,22 @@ public class FunctionalCaseFileService { @Resource 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导入模板 @@ -105,10 +156,17 @@ public class FunctionalCaseFileService { for (String head : headList) { boolean isSystemField = false; for (FunctionalCaseImportFiled importFiled : importFields) { + if (StringUtils.equals("name", importFiled.getValue()) && model.getHyperLinkName() != null) { + fields.add(model.getHyperLinkName()); + isSystemField = true; + break; + } if (importFiled.containsHead(head)) { fields.add(importFiled.parseExcelDataValue(model)); isSystemField = true; + break; } + } if (!isSystemField) { Object value = customDataMaps.get(head); @@ -267,4 +325,408 @@ public class FunctionalCaseFileService { 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 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 generateCaseExportExcel(String tmpZipPath, FunctionalCaseExportRequest request, Project project) { + List tmpExportExcelList = new ArrayList<>(); + //excel表头 + List> headList = getFunctionalCaseExportHeads(request); + //获取导出的ids集合 + List ids = functionalCaseService.doSelectIds(request, request.getProjectId()); + if (CollectionUtils.isEmpty(ids)) { + return tmpExportExcelList; + } + //获取当前项目下默认模板的自定义字段属性 + List customFields = getCustomFields(request.getProjectId()); + //默认字段+自定义字段的 options集合 + Map> customFieldOptionsMap = getCustomFieldOptionsMap(customFields); + Map customFieldMap = customFields.stream().collect(Collectors.toMap(TemplateCustomFieldDTO::getFieldName, templateCustomFieldDTO -> templateCustomFieldDTO)); + + //获取url + SystemParameter parameter = systemParameterMapper.selectByPrimaryKey(ParamConstants.BASE.URL.getValue()); + + //获取用例模块map + Map moduleMap = getModuleMap(request.getProjectId()); + //2000条,分批导出 + AtomicInteger count = new AtomicInteger(0); + SubListUtils.dealForSubList(ids, EXPORT_CASE_MAX_COUNT, (subIds) -> { + count.getAndIncrement(); + // 生成writeHandler + Map rowMergeInfo = new HashMap<>(); + FunctionCaseMergeWriteHandler writeHandler = new FunctionCaseMergeWriteHandler(rowMergeInfo, headList); + //表头备注信息 + FunctionCaseTemplateWriteHandler handler = new FunctionCaseTemplateWriteHandler(headList, customFieldOptionsMap, customFieldMap); + + //获取导出数据 + List excelData = parseCaseData2ExcelData(subIds, rowMergeInfo, request, customFields, moduleMap, parameter.getParamValue()); + List> 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 parseCaseData2ExcelData(List ids, Map rowMergeInfo, FunctionalCaseExportRequest request, List customFields, Map moduleMap, String url) { + List list = new ArrayList<>(); + //基础信息 + Map functionalCaseMap = functionalCaseService.copyBaseInfo(request.getProjectId(), ids); + //大字段 + Map functionalCaseBlobMap = functionalCaseService.copyBlobInfo(ids); + //自定义字段 + Map> customFieldMap = functionalCaseCustomFieldService.getCustomFieldMapByCaseIds(ids); + //用例评论 + Map> caseCommentMap = getCaseComment(ids); + //执行评论 + Map> executeCommentMap = getExecuteComment(ids); + //评审评论 + Map> reviewCommentMap = getReviewComment(ids); + + ids.forEach(id -> { + List textDescriptionList = new ArrayList<>(); + List 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 moduleMap, List textDescriptionList, List 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 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 rowMergeInfo, List list, List textDescriptionList, List 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> caseCommentMap, Map> executeCommentMap, Map> reviewCommentMap, FunctionalCaseExportRequest request) { + if (CollectionUtils.isEmpty(request.getOtherFields())) { + return; + } + List otherFields = request.getOtherFields(); + List keys = otherFields.stream().map(FunctionalCaseHeader::getId).toList(); + Map converterMaps = FunctionalCaseExportConverterFactory.getConverters(keys); + HashMap 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> getReviewComment(List ids) { + List reviewHistories = extFunctionalCaseCommentMapper.getReviewComment(ids); + Map> reviewHistoryMap = reviewHistories.stream().collect(Collectors.groupingBy(CaseReviewHistory::getCaseId)); + return reviewHistoryMap; + } + + /** + * 执行评论 + * + * @param ids + * @return + */ + private Map> getExecuteComment(List ids) { + List historyList = extFunctionalCaseCommentMapper.getExecuteComment(ids); + Map> commentMap = historyList.stream().collect(Collectors.groupingBy(TestPlanCaseExecuteHistory::getCaseId)); + return commentMap; + } + + /** + * 用例评论 + * + * @param ids + * @return + */ + private Map> getCaseComment(List ids) { + List functionalCaseComments = extFunctionalCaseCommentMapper.getCaseComment(ids); + Map> commentMap = functionalCaseComments.stream().collect(Collectors.groupingBy(FunctionalCaseComment::getCaseId)); + return commentMap; + } + + /** + * 构建自定义字段 + * + * @param templateCustomFields + * @param functionalCaseCustomFields + * @param data + * @param request + */ + private void buildExportCustomField(List templateCustomFields, List functionalCaseCustomFields, FunctionalCaseExcelData data, FunctionalCaseExportRequest request) { + if (CollectionUtils.isEmpty(request.getCustomFields())) { + return; + } + HashMap customFieldValidatorMap = CustomFieldValidatorFactory.getValidatorMap(); + Map customFieldsMap = templateCustomFields.stream().collect(Collectors.toMap(TemplateCustomFieldDTO::getFieldId, i -> i)); + Map caseFieldvalueMap = functionalCaseCustomFields.stream().collect(Collectors.toMap(FunctionalCaseCustomField::getFieldId, FunctionalCaseCustomField::getValue)); + Map 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 textDescriptionList, List 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 getModuleMap(String projectId) { + List moduleTree = functionalCaseModuleService.getTree(projectId); + Map moduleMap = moduleTree.stream().collect(Collectors.toMap(BaseTreeNode::getId, BaseTreeNode::getPath)); + return moduleMap; + } + + + /** + * 获取导出表头 + * + * @param request + * @return + */ + private List> getFunctionalCaseExportHeads(FunctionalCaseExportRequest request) { + List> 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; + } } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseLogService.java b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseLogService.java index a39ecd984c..e522ea5794 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseLogService.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseLogService.java @@ -51,19 +51,12 @@ public class FunctionalCaseLogService { private FunctionalCaseAttachmentMapper functionalCaseAttachmentMapper; @Resource private FileAssociationMapper fileAssociationMapper; - - @Resource - private CustomFieldMapper customFieldMapper; - @Resource private BugRelationCaseMapper bugRelationCaseMapper; @Resource private BugMapper bugMapper; - @Resource - private ExtFunctionalCaseModuleMapper extFunctionalCaseModuleMapper; - /** * 更新用例 日志 @@ -434,4 +427,21 @@ public class FunctionalCaseLogService { dto.setMethod(HttpMethodConstants.POST.name()); 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; + } } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseModuleService.java b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseModuleService.java index e84b1629b7..17361a6867 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseModuleService.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseModuleService.java @@ -303,7 +303,7 @@ public class FunctionalCaseModuleService extends ModuleTreeService { currentModuleName = itemIterator.next().trim(); moduleTree.forEach(module -> { //根节点是否存在 - if (StringUtils.equals(currentModuleName, module.getName())) { + if (StringUtils.equalsIgnoreCase(currentModuleName, module.getName())) { hasNode.set(true); //根节点存在,检查子节点是否存在 createModuleByPathIterator(itemIterator, "/" + currentModuleName, module, pathMap, projectId, userId); @@ -342,7 +342,7 @@ public class FunctionalCaseModuleService extends ModuleTreeService { String nodeName = itemIterator.next().trim(); AtomicReference hasNode = new AtomicReference<>(false); children.forEach(child -> { - if (StringUtils.equals(nodeName, child.getName())) { + if (StringUtils.equalsIgnoreCase(nodeName, child.getName())) { hasNode.set(true); createModuleByPathIterator(itemIterator, currentModulePath + "/" + child.getName(), child, pathMap, projectId, userId); } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseService.java b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseService.java index 625fbcd970..9dbab35081 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseService.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseService.java @@ -1158,7 +1158,7 @@ public class FunctionalCaseService { private void noticeModule(List noticeList, FunctionalCaseExcelData functionalCaseExcelData, FunctionalCaseImportRequest request, String userId, Map customFieldsMap) { FunctionalCaseDTO functionalCaseDTO = new FunctionalCaseDTO(); functionalCaseDTO.setTriggerMode(Translator.get("log.test_plan.functional_case")); - functionalCaseDTO.setName(functionalCaseExcelData.getName()); + functionalCaseDTO.setName(functionalCaseExcelData.getName().toString()); functionalCaseDTO.setProjectId(request.getProjectId()); functionalCaseDTO.setCaseEditType(functionalCaseExcelData.getCaseEditType()); functionalCaseDTO.setCreateUser(userId); @@ -1187,7 +1187,7 @@ public class FunctionalCaseService { functionalCase.setModuleId(caseModulePathMap.get(functionalCaseExcelData.getModule())); functionalCase.setProjectId(request.getProjectId()); functionalCase.setTemplateId(defaultTemplateDTO.getId()); - functionalCase.setName(functionalCaseExcelData.getName()); + functionalCase.setName(functionalCaseExcelData.getName().toString()); functionalCase.setReviewStatus(FunctionalCaseReviewStatus.UN_REVIEWED.name()); functionalCase.setTags(handleImportTags(functionalCaseExcelData.getTags())); 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.setName(functionalCaseExcelData.getName()); + functionalCase.setName(functionalCaseExcelData.getName().toString()); functionalCase.setModuleId(caseModulePathMap.get(functionalCaseExcelData.getModule())); functionalCase.setTags(handleImportTags(functionalCaseExcelData.getTags())); functionalCase.setCaseEditType(StringUtils.defaultIfBlank(functionalCaseExcelData.getCaseEditType(), FunctionalCaseTypeConstants.CaseEditType.TEXT.name())); @@ -1417,7 +1417,7 @@ public class FunctionalCaseService { FunctionalCase functionalCase = collect.get(functionalCaseExcelData.getNum()).getFirst(); if (CollectionUtils.isNotEmpty(projectApplications) && Boolean.valueOf(projectApplications.getFirst().getTypeValue())) { 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.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))) { diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/socket/ExportWebSocketHandler.java b/backend/services/case-management/src/main/java/io/metersphere/functional/socket/ExportWebSocketHandler.java new file mode 100644 index 0000000000..a107987ce0 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/socket/ExportWebSocketHandler.java @@ -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 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") + ); + } +} diff --git a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseControllerTests.java b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseControllerTests.java index f144c71eb6..bdab2875ed 100644 --- a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseControllerTests.java +++ b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseControllerTests.java @@ -8,6 +8,7 @@ import io.metersphere.functional.dto.CaseCustomFieldDTO; import io.metersphere.functional.dto.FunctionalCaseAttachmentDTO; import io.metersphere.functional.dto.FunctionalCasePageDTO; 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.FunctionalCaseCustomFieldMapper; 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 IMPORT_EXCEL_URL = "/functional/case/import/excel"; public static final String OPERATION_HISTORY_URL = "/functional/case/operation-history"; + public static final String EXPORT_EXCEL_URL = "/functional/case/export/excel"; @Resource private NotificationMapper notificationMapper; @@ -795,4 +797,41 @@ public class FunctionalCaseControllerTests extends BaseTest { 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 sysHeaders = new ArrayList<>() {{ + add(new FunctionalCaseHeader() {{ + setId("num"); + setName("ID"); + }}); + add(new FunctionalCaseHeader() {{ + setId("name"); + setName("用例名称"); + }}); + }}; + request.setSystemFields(sysHeaders); + List customHeaders = new ArrayList<>() {{ + add(new FunctionalCaseHeader() {{ + setId("A"); + setName("测试3"); + }}); + }}; + request.setCustomFields(customHeaders); + List otherHeaders = new ArrayList<>() {{ + add(new FunctionalCaseHeader() {{ + setId("createTime"); + setName("创建时间"); + }}); + }}; + request.setOtherFields(otherHeaders); + + request.setFileId("123142342"); + this.requestPost(EXPORT_EXCEL_URL, request); + } } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/config/MinioConfig.java b/backend/services/system-setting/src/main/java/io/metersphere/system/config/MinioConfig.java index 49b02af6e0..544ae1f3f3 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/config/MinioConfig.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/config/MinioConfig.java @@ -28,6 +28,7 @@ public class MinioConfig { // 设置临时目录下文件的过期时间 setBucketLifecycle(minioClient); + setBucketLifecycleByExcel(minioClient); boolean exist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(BUCKET).build()); 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 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); + } + } + } \ No newline at end of file