diff --git a/backend/framework/provider/src/main/java/io/metersphere/dto/BugProviderDTO.java b/backend/framework/provider/src/main/java/io/metersphere/dto/BugProviderDTO.java index dd791e264f..7730997c3b 100644 --- a/backend/framework/provider/src/main/java/io/metersphere/dto/BugProviderDTO.java +++ b/backend/framework/provider/src/main/java/io/metersphere/dto/BugProviderDTO.java @@ -18,7 +18,7 @@ public class BugProviderDTO implements Serializable { @Schema(description = "id") private String id; - @Schema(description = "bugId") + @Schema(description = "缺陷id") private String bugId; @Schema(description = "缺陷名称") diff --git a/backend/framework/sdk/src/main/resources/i18n/commons_en_US.properties b/backend/framework/sdk/src/main/resources/i18n/commons_en_US.properties index 8fb73d3641..64b2acd6a3 100644 --- a/backend/framework/sdk/src/main/resources/i18n/commons_en_US.properties +++ b/backend/framework/sdk/src/main/resources/i18n/commons_en_US.properties @@ -513,4 +513,11 @@ swagger_parse_error_with_auth=Swagger parsing failed, please confirm whether the swagger_parse_error=Swagger parsing failed or file format is incorrect! #测试计划 permission.test_plan.name=Test plan -permission.test_plan_module.name=Test plan module \ No newline at end of file +permission.test_plan_module.name=Test plan module + +excel.template.id=Not mandatory, add a new use case when the ID is empty +excel.template.case_edit_type=Not mandatory, fill in STEP for step description, fill in Text for text description, default to Text if not filled in +excel.template.tag=Not mandatory labels should be separated by semicolons or commas +excel.template.text_description=Not mandatory, when the editing mode is STEP, the step description will be based on the identifier [1] [2] [3] To determine whether to split a cell into multiple steps, if not, it is a single step +excel.template.member=Not mandatory, please fill in the relevant personnel ID or email under this project +excel.template.not_required=Not required \ No newline at end of file diff --git a/backend/framework/sdk/src/main/resources/i18n/commons_zh_CN.properties b/backend/framework/sdk/src/main/resources/i18n/commons_zh_CN.properties index 43ede25e3c..1696f6ed95 100644 --- a/backend/framework/sdk/src/main/resources/i18n/commons_zh_CN.properties +++ b/backend/framework/sdk/src/main/resources/i18n/commons_zh_CN.properties @@ -509,4 +509,11 @@ swagger_parse_error_with_auth=Swagger 解析失败,请确认认证信息是否 swagger_parse_error=Swagger 解析失败,请确认文件格式是否正确! #测试计划 permission.test_plan.name=测试计划 -permission.test_plan_module.name=测试计划模块 \ No newline at end of file +permission.test_plan_module.name=测试计划模块 + +excel.template.id=非必填,ID为空时新增用例 +excel.template.case_edit_type=非必填,步骤描述填写STEP,文本描述填写TEXT,未填写默认为TEXT +excel.template.tag=非必填,标签之间以分号或者逗号隔开 +excel.template.text_description=非必填,编辑模式为STEP时,步骤描述会根据标识[1] [2] [3]...来判断是否将单元格拆分为多个步骤,没有则为一个步骤 +excel.template.member=非必填,请填写该项目下的相关人员ID或邮箱 +excel.template.not_required=非必填 \ No newline at end of file diff --git a/backend/framework/sdk/src/main/resources/i18n/commons_zh_TW.properties b/backend/framework/sdk/src/main/resources/i18n/commons_zh_TW.properties index 5a5067eadc..d1dff6a3ce 100644 --- a/backend/framework/sdk/src/main/resources/i18n/commons_zh_TW.properties +++ b/backend/framework/sdk/src/main/resources/i18n/commons_zh_TW.properties @@ -509,4 +509,11 @@ swagger_parse_error_with_auth=Swagger 解析失敗,請檢查 Swagger 接口是 swagger_parse_error=Swagger 解析失敗 #測試計劃 permission.test_plan.name=測試計劃 -permission.test_plan_module.name=測試計劃模塊 \ No newline at end of file +permission.test_plan_module.name=測試計劃模塊 + +excel.template.id=非必填,ID為空時新增用例 +excel.template.case_edit_type=非必填,步驟描述填寫STEP,文本描述填寫TEXT,為填寫默認為TEXT +excel.template.tag=非必填,標簽之間以分號或者逗號隔開 +excel.template.text_description=非必填,編輯模式為STEP時,步驟描述會根據標識[1] [2] [3]...來判斷是否將單元格拆分為多個步驟,沒有則為一個步驟 +excel.template.member=非必填,請填寫該項目下的相關人員ID或郵箱 +excel.template.not_required=非必填 \ No newline at end of file diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseController.java b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseController.java index c6b1d1e063..9479ced247 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 @@ -8,6 +8,7 @@ import io.metersphere.functional.dto.FunctionalCaseDetailDTO; import io.metersphere.functional.dto.FunctionalCasePageDTO; import io.metersphere.functional.dto.FunctionalCaseVersionDTO; import io.metersphere.functional.request.*; +import io.metersphere.functional.service.FunctionalCaseFileService; import io.metersphere.functional.service.FunctionalCaseLogService; import io.metersphere.functional.service.FunctionalCaseNoticeService; import io.metersphere.functional.service.FunctionalCaseService; @@ -15,8 +16,8 @@ import io.metersphere.project.dto.CustomFieldOptions; import io.metersphere.project.service.ProjectTemplateService; import io.metersphere.sdk.constants.PermissionConstants; import io.metersphere.sdk.constants.TemplateScene; -import io.metersphere.system.dto.sdk.request.PosRequest; import io.metersphere.system.dto.sdk.TemplateDTO; +import io.metersphere.system.dto.sdk.request.PosRequest; import io.metersphere.system.log.annotation.Log; import io.metersphere.system.log.constants.OperationLogType; import io.metersphere.system.notice.annotation.SendNotice; @@ -28,6 +29,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.HttpServletResponse; import jakarta.validation.constraints.NotBlank; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.validation.annotation.Validated; @@ -50,6 +52,8 @@ public class FunctionalCaseController { @Resource private ProjectTemplateService projectTemplateService; + @Resource + private FunctionalCaseFileService functionalCaseFileService; //TODO 获取模板列表(多模板功能暂时不做) @@ -205,4 +209,12 @@ public class FunctionalCaseController { functionalCaseService.editPos(request); } + + @GetMapping("/download/excel/template/{projectId}") + @Operation(summary = "用例管理-功能用例-excel导入-下载模板") + @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ) + @CheckOwner(resourceId = "#projectId", resourceType = "project") + public void testCaseTemplateExport(@PathVariable String projectId, HttpServletResponse response) { + functionalCaseFileService.downloadExcelTemplate(projectId, response); + } } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseRelationshipController.java b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseRelationshipController.java index f5dcc785e6..fbb47a9802 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseRelationshipController.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseRelationshipController.java @@ -40,7 +40,7 @@ public class FunctionalCaseRelationshipController { @GetMapping("/get-ids/{caseId}") @Operation(summary = "用例管理-功能用例-用例详情-前后置关系-获取已关联用例id集合(关联用例弹窗前调用)") - @CheckOwner(resourceId = "#reviewId", resourceType = "case_review") + @CheckOwner(resourceId = "#caseId", resourceType = "functional_case") public List getCaseIds(@PathVariable String caseId) { return functionalCaseRelationshipEdgeService.getExcludeIds(caseId); } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/annotation/NotRequired.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/annotation/NotRequired.java new file mode 100644 index 0000000000..3796bb910b --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/annotation/NotRequired.java @@ -0,0 +1,12 @@ +package io.metersphere.functional.excel.annotation; + +import java.lang.annotation.*; + +/** + * @author wx + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface NotRequired { +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/constants/FunctionalCaseImportFiled.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/constants/FunctionalCaseImportFiled.java new file mode 100644 index 0000000000..89c2120560 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/constants/FunctionalCaseImportFiled.java @@ -0,0 +1,75 @@ +package io.metersphere.functional.excel.constants; + + +import io.metersphere.functional.excel.domain.FunctionalCaseExcelData; +import io.metersphere.sdk.util.JSON; +import io.metersphere.sdk.util.LogUtils; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; + +/** + * @author wx + */ + +public enum FunctionalCaseImportFiled { + + ID("id", "ID", "ID", "ID", FunctionalCaseExcelData::getNum), + NAME("name", "用例名称", "用例名稱", "Name", FunctionalCaseExcelData::getName), + MODULE("module", "所属模块", "所屬模塊", "Module", FunctionalCaseExcelData::getModule), + TAGS("tags", "标签", "標簽", "Tag", FunctionalCaseImportFiled::parseTags), + PREREQUISITE("prerequisite", "前置条件", "前置條件", "Prerequisite", FunctionalCaseExcelData::getPrerequisite), + TEXT_DESCRIPTION("textDescription", "步骤描述", "步驟描述", "Text description", FunctionalCaseExcelData::getTextDescription), + EXPECTED_RESULT("expectedResult", "预期结果", "預期結果", "Expected result", FunctionalCaseExcelData::getExpectedResult), + CASE_EDIT_TYPE("caseEditType", "编辑模式", "編輯模式", "Case edit type", FunctionalCaseExcelData::getCaseEditType), + DESCRIPTION("description", "备注", "備註", "Description", FunctionalCaseExcelData::getDescription); + + private Map filedLangMap; + private Function parseFunc; + private String value; + + FunctionalCaseImportFiled(String value, String zn, String chineseTw, String us, Function parseFunc) { + this.filedLangMap = new HashMap(); + filedLangMap.put(Locale.SIMPLIFIED_CHINESE, zn); + filedLangMap.put(Locale.TRADITIONAL_CHINESE, chineseTw); + filedLangMap.put(Locale.US, us); + this.value = value; + this.parseFunc = parseFunc; + } + + public Map getFiledLangMap() { + return this.filedLangMap; + } + + public String getValue() { + return value; + } + + public String parseExcelDataValue(FunctionalCaseExcelData excelData) { + return parseFunc.apply(excelData); + } + + private static String parseTags(FunctionalCaseExcelData excelData) { + String tags = StringUtils.EMPTY; + try { + if (excelData.getTags() != null) { + List arr = JSON.parseArray(excelData.getTags()); + if (CollectionUtils.isNotEmpty(arr)) { + tags = StringUtils.joinWith(",", arr.toArray()); + } + } + } catch (Exception e) { + LogUtils.error(e); + } + return tags; + } + + public boolean containsHead(String head) { + return filedLangMap.values().contains(head); + } +} 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 new file mode 100644 index 0000000000..375e15ed7f --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelData.java @@ -0,0 +1,65 @@ +package io.metersphere.functional.excel.domain; + +import com.alibaba.excel.annotation.ExcelIgnore; +import io.metersphere.functional.excel.constants.FunctionalCaseImportFiled; +import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.collections.CollectionUtils; + +import java.util.*; + +/** + * @author wx + */ +@Getter +@Setter +public class FunctionalCaseExcelData { + @ExcelIgnore + private String id; + @ExcelIgnore + private String num; + @ExcelIgnore + private String name; + @ExcelIgnore + private String module; + @ExcelIgnore + private String tags; + @ExcelIgnore + private String prerequisite; + @ExcelIgnore + private String description; + @ExcelIgnore + private String textDescription; + @ExcelIgnore + private String expectedResult; + @ExcelIgnore + private String caseEditType; + + @ExcelIgnore + Map customData = new LinkedHashMap<>(); + @ExcelIgnore + Map otherFields; + + + public List> getHead(List customFields) { + return new ArrayList<>(); + } + + public List> getHead(List customFields, Locale lang) { + List> heads = new ArrayList<>(); + FunctionalCaseImportFiled[] fields = FunctionalCaseImportFiled.values(); + for (FunctionalCaseImportFiled field : fields) { + heads.add(Arrays.asList(field.getFiledLangMap().get(lang))); + } + + if (CollectionUtils.isNotEmpty(customFields)) { + for (TemplateCustomFieldDTO dto : customFields) { + List list = new ArrayList<>(); + list.add(dto.getFieldName()); + heads.add(list); + } + } + return heads; + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelDataCn.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelDataCn.java new file mode 100644 index 0000000000..44e0ce5d8e --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelDataCn.java @@ -0,0 +1,70 @@ +package io.metersphere.functional.excel.domain; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.write.style.ColumnWidth; +import io.metersphere.functional.excel.annotation.NotRequired; +import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import java.util.List; +import java.util.Locale; + +/** + * @author wx + */ +@Data +@ColumnWidth(15) +public class FunctionalCaseExcelDataCn extends FunctionalCaseExcelData { + + @ColumnWidth(50) + @ExcelProperty("ID") + @NotRequired + private String num; + + @NotBlank(message = "{cannot_be_null}") + @Length(max = 255) + @ExcelProperty("用例名称") + private String name; + + @NotBlank(message = "{cannot_be_null}") + @Length(max = 50) + @ExcelProperty("所属模块") + @ColumnWidth(30) + private String moduleId; + + @ColumnWidth(50) + @ExcelProperty("标签") + @NotRequired + @Length(min = 0, max = 1000) + private String tags; + + @ColumnWidth(50) + @ExcelProperty("前置条件") + private String prerequisite; + + @ColumnWidth(50) + @ExcelProperty("备注") + private String description; + + @ColumnWidth(50) + @ExcelProperty("步骤描述") + private String textDescription; + + @ColumnWidth(50) + @ExcelProperty("预期结果") + private String expectedResult; + + @ColumnWidth(50) + @ExcelProperty("编辑模式") + @NotRequired + @Pattern(regexp = "(^TEXT$)|(^STEP$)|(.{0})", message = "{test_case_step_model_validate}") + private String caseEditType; + + @Override + public List> getHead(List customFields) { + return super.getHead(customFields, Locale.SIMPLIFIED_CHINESE); + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelDataFactory.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelDataFactory.java new file mode 100644 index 0000000000..29162af92a --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelDataFactory.java @@ -0,0 +1,34 @@ +package io.metersphere.functional.excel.domain; + +import io.metersphere.system.excel.domain.ExcelDataFactory; +import org.springframework.context.i18n.LocaleContextHolder; + +import java.util.Locale; + +/** + * @author wx + */ +public class FunctionalCaseExcelDataFactory implements ExcelDataFactory { + + @Override + public Class getExcelDataByLocal() { + Locale locale = LocaleContextHolder.getLocale(); + if (Locale.US.toString().equalsIgnoreCase(locale.toString())) { + return FunctionalCaseExcelDataUs.class; + } else if (Locale.TRADITIONAL_CHINESE.toString().equalsIgnoreCase(locale.toString())) { + return FunctionalCaseExcelDataTw.class; + } + return FunctionalCaseExcelDataCn.class; + } + + public FunctionalCaseExcelData getFunctionalCaseExcelDataLocal() { + Locale locale = LocaleContextHolder.getLocale(); + if (Locale.US.toString().equalsIgnoreCase(locale.toString())) { + return new FunctionalCaseExcelDataUs(); + } else if (Locale.TRADITIONAL_CHINESE.toString().equalsIgnoreCase(locale.toString())) { + return new FunctionalCaseExcelDataTw(); + } + return new FunctionalCaseExcelDataCn(); + } + +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelDataTw.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelDataTw.java new file mode 100644 index 0000000000..e5e03f415a --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelDataTw.java @@ -0,0 +1,70 @@ +package io.metersphere.functional.excel.domain; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.write.style.ColumnWidth; +import io.metersphere.functional.excel.annotation.NotRequired; +import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import java.util.List; +import java.util.Locale; + +/** + * @author wx + */ +@Data +@ColumnWidth(15) +public class FunctionalCaseExcelDataTw extends FunctionalCaseExcelData { + + @ColumnWidth(50) + @ExcelProperty("ID") + @NotRequired + private String num; + + @NotBlank(message = "{cannot_be_null}") + @Length(max = 255) + @ExcelProperty("用例名稱") + private String name; + + @NotBlank(message = "{cannot_be_null}") + @Length(max = 50) + @ExcelProperty("所屬模塊") + @ColumnWidth(30) + private String module; + + @ColumnWidth(50) + @ExcelProperty("標簽") + @NotRequired + @Length(min = 0, max = 1000) + private String tags; + + @ColumnWidth(50) + @ExcelProperty("前置條件") + private String prerequisite; + + @ColumnWidth(50) + @ExcelProperty("備註") + private String description; + + @ColumnWidth(50) + @ExcelProperty("步驟描述") + private String textDescription; + + @ColumnWidth(50) + @ExcelProperty("預期結果") + private String expectedResult; + + @ColumnWidth(50) + @ExcelProperty("編輯模式") + @NotRequired + @Pattern(regexp = "(^TEXT$)|(^STEP$)|(.{0})", message = "{test_case_step_model_validate}") + private String caseEditType; + + @Override + public List> getHead(List customFields) { + return super.getHead(customFields, Locale.TRADITIONAL_CHINESE); + } +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelDataUs.java b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelDataUs.java new file mode 100644 index 0000000000..4394688776 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/domain/FunctionalCaseExcelDataUs.java @@ -0,0 +1,70 @@ +package io.metersphere.functional.excel.domain; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.write.style.ColumnWidth; +import io.metersphere.functional.excel.annotation.NotRequired; +import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import java.util.List; +import java.util.Locale; + +/** + * @author wx + */ +@Data +@ColumnWidth(15) +public class FunctionalCaseExcelDataUs extends FunctionalCaseExcelData { + + @ColumnWidth(50) + @ExcelProperty("ID") + @NotRequired + private String num; + + @NotBlank(message = "{cannot_be_null}") + @Length(max = 255) + @ExcelProperty("Name") + private String name; + + @NotBlank(message = "{cannot_be_null}") + @Length(max = 50) + @ExcelProperty("Module") + @ColumnWidth(30) + private String module; + + @ColumnWidth(50) + @ExcelProperty("Tag") + @NotRequired + @Length(min = 0, max = 1000) + private String tags; + + @ColumnWidth(50) + @ExcelProperty("Prerequisite") + private String prerequisite; + + @ColumnWidth(50) + @ExcelProperty("Description") + private String description; + + @ColumnWidth(50) + @ExcelProperty("Text description") + private String textDescription; + + @ColumnWidth(50) + @ExcelProperty("Expected result") + private String expectedResult; + + @ColumnWidth(50) + @ExcelProperty("Case edit type") + @NotRequired + @Pattern(regexp = "(^TEXT$)|(^STEP$)|(.{0})", message = "{test_case_step_model_validate}") + private String caseEditType; + + @Override + public List> getHead(List customFields) { + return super.getHead(customFields, Locale.US); + } +} 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 new file mode 100644 index 0000000000..374645809c --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/excel/handler/FunctionCaseTemplateWriteHandler.java @@ -0,0 +1,136 @@ +package io.metersphere.functional.excel.handler; + +import com.alibaba.excel.util.BooleanUtils; +import com.alibaba.excel.write.handler.RowWriteHandler; +import com.alibaba.excel.write.handler.context.RowWriteHandlerContext; +import com.alibaba.excel.write.metadata.style.WriteCellStyle; +import com.alibaba.excel.write.style.HorizontalCellStyleStrategy; +import io.metersphere.functional.excel.constants.FunctionalCaseImportFiled; +import io.metersphere.sdk.constants.CustomFieldType; +import io.metersphere.sdk.util.JSON; +import io.metersphere.sdk.util.Translator; +import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.ss.usermodel.Comment; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.usermodel.XSSFClientAnchor; +import org.apache.poi.xssf.usermodel.XSSFRichTextString; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + + +/** + * @author wx + */ +public class FunctionCaseTemplateWriteHandler implements RowWriteHandler { + + Map> caseLevelAndStatusValueMap; + + private Sheet sheet; + private Drawing drawingPatriarch; + + private Map customField; + private Map fieldMap = new HashMap<>(); + + public FunctionCaseTemplateWriteHandler(List> headList, Map> caseLevelAndStatusValueMap, Map customFieldMap) { + initIndex(headList); + this.caseLevelAndStatusValueMap = caseLevelAndStatusValueMap; + this.customField = customFieldMap; + } + + private void initIndex(List> headList) { + int index = 0; + for (List list : headList) { + for (String head : list) { + this.fieldMap.put(head, index); + index++; + } + } + } + + @Override + public void afterRowDispose(RowWriteHandlerContext context) { + if (BooleanUtils.isTrue(context.getHead())) { + sheet = context.getWriteSheetHolder().getSheet(); + drawingPatriarch = sheet.createDrawingPatriarch(); + + Iterator> iterator = fieldMap.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + //默认字段 + if (FunctionalCaseImportFiled.ID.containsHead(entry.getKey())) { + setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.id")); + } + if (FunctionalCaseImportFiled.NAME.containsHead(entry.getKey())) { + setComment(fieldMap.get(entry.getKey()), Translator.get("required")); + } + if (FunctionalCaseImportFiled.MODULE.containsHead(entry.getKey())) { + setComment(fieldMap.get(entry.getKey()), Translator.get("required").concat(",").concat(Translator.get("module_created_automatically"))); + } + if (FunctionalCaseImportFiled.TAGS.containsHead(entry.getKey())) { + setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.tag")); + } + if (FunctionalCaseImportFiled.CASE_EDIT_TYPE.containsHead(entry.getKey())) { + setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.case_edit_type")); + } + if (FunctionalCaseImportFiled.TEXT_DESCRIPTION.containsHead(entry.getKey())) { + setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.text_description")); + } + if (FunctionalCaseImportFiled.PREREQUISITE.containsHead(entry.getKey())) { + setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.not_required")); + } + if (FunctionalCaseImportFiled.EXPECTED_RESULT.containsHead(entry.getKey())) { + setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.not_required")); + } + if (FunctionalCaseImportFiled.DESCRIPTION.containsHead(entry.getKey())) { + setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.not_required")); + } + + //自定义字段 + if (customField.containsKey(entry.getKey())) { + TemplateCustomFieldDTO templateCustomFieldDTO = customField.get(entry.getKey()); + List strings = caseLevelAndStatusValueMap.get(entry.getKey()); + if (StringUtils.equalsAnyIgnoreCase(templateCustomFieldDTO.getType(), CustomFieldType.MULTIPLE_MEMBER.name(), CustomFieldType.MEMBER.name())) { + setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.member")); + } 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())))); + } 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(JSON.toJSONString(caseLevelAndStatusValueMap.get(entry.getKey())))); + } else { + setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.not_required")); + } + } + } + } + } + } + + } + + private void setComment(Integer index, String text) { + if (index == null) { + return; + } + Comment comment = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, index, 0, index + 3, 1)); + comment.setString(new XSSFRichTextString(text)); + sheet.getRow(0).getCell(1).setCellComment(comment); + } + + public static HorizontalCellStyleStrategy getHorizontalWrapStrategy() { + WriteCellStyle contentWriteCellStyle = new WriteCellStyle(); + // 设置自动换行 + contentWriteCellStyle.setWrapped(true); + return new HorizontalCellStyleStrategy(null, contentWriteCellStyle); + } +} 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 new file mode 100644 index 0000000000..d2c78bcc74 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseFileService.java @@ -0,0 +1,167 @@ +package io.metersphere.functional.service; + +import io.metersphere.functional.excel.constants.FunctionalCaseImportFiled; +import io.metersphere.functional.excel.domain.FunctionalCaseExcelData; +import io.metersphere.functional.excel.domain.FunctionalCaseExcelDataFactory; +import io.metersphere.functional.excel.handler.FunctionCaseTemplateWriteHandler; +import io.metersphere.project.service.ProjectTemplateService; +import io.metersphere.sdk.constants.TemplateScene; +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.TemplateDTO; +import io.metersphere.system.excel.utils.EasyExcelExporter; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author wx + */ +@Service +@Transactional(rollbackFor = Exception.class) +public class FunctionalCaseFileService { + + + @Resource + private ProjectTemplateService projectTemplateService; + + + /** + * 下载excel导入模板 + * + * @param projectId + * @param response + */ + public void downloadExcelTemplate(String projectId, HttpServletResponse response) { + //获取当前项目下默认模板的自定义字段属性 + TemplateDTO defaultTemplateDTO = projectTemplateService.getDefaultTemplateDTO(projectId, TemplateScene.FUNCTIONAL.name()); + List customFields = Optional.ofNullable(defaultTemplateDTO.getCustomFields()).orElse(new ArrayList<>()); + + //获取表头字段 当前项目下默认模板的自定义字段 heads:默认表头名称+自定义字段名称 + List> heads = getTemplateHead(projectId, customFields); + + FunctionalCaseExcelData caseExcelData = new FunctionalCaseExcelDataFactory().getFunctionalCaseExcelDataLocal(); + + //默认字段+自定义字段的 options集合 + Map> customFieldOptionsMap = getCustomFieldOptionsMap(customFields); + Map customFieldMap = customFields.stream().collect(Collectors.toMap(TemplateCustomFieldDTO::getFieldName, templateCustomFieldDTO -> templateCustomFieldDTO)); + + //表头备注信息 + FunctionCaseTemplateWriteHandler handler = new FunctionCaseTemplateWriteHandler(heads, customFieldOptionsMap, customFieldMap); + + List functionalCaseExcelData = generateExportData(); + List> data = parseExcelData2List(heads, functionalCaseExcelData); + + new EasyExcelExporter(caseExcelData.getClass()) + .exportByCustomWriteHandler(response, heads, data, Translator.get("test_case_import_template_name"), + Translator.get("test_case_import_template_sheet"), handler); + } + + + private List> parseExcelData2List(List> headListParams, List data) { + List> result = new ArrayList<>(); + //转化excel头 + List headList = new ArrayList<>(); + for (List list : headListParams) { + for (String head : list) { + headList.add(head); + } + } + + FunctionalCaseImportFiled[] importFields = FunctionalCaseImportFiled.values(); + + + for (FunctionalCaseExcelData model : data) { + List fields = new ArrayList<>(); + Map customDataMaps = Optional.ofNullable(model.getCustomData()) + .orElse(new HashMap<>()); + Map otherFieldMaps = Optional.ofNullable(model.getOtherFields()) + .orElse(new HashMap<>()); + for (String head : headList) { + boolean isSystemField = false; + for (FunctionalCaseImportFiled importFiled : importFields) { + if (importFiled.containsHead(head)) { + fields.add(importFiled.parseExcelDataValue(model)); + isSystemField = true; + } + } + if (!isSystemField) { + Object value = customDataMaps.get(head); + if (value == null) { + value = otherFieldMaps.get(head); + } + if (value == null) { + value = StringUtils.EMPTY; + } + fields.add(value); + } + } + result.add(fields); + } + + return result; + } + + + private List generateExportData() { + List list = new ArrayList<>(); + StringBuilder path = new StringBuilder(); + for (int i = 1; i <= 4; i++) { + path.append("/" + Translator.get("module") + i); + FunctionalCaseExcelData testCaseDTO = new FunctionalCaseExcelData(); + testCaseDTO.setId(StringUtils.EMPTY); + testCaseDTO.setName(Translator.get("test_case") + i); + testCaseDTO.setModule(path.toString()); + testCaseDTO.setPrerequisite(Translator.get("test_case_prerequisite")); + testCaseDTO.setCaseEditType("STEP"); + String textDescription = ""; + String expectedResult = ""; + for (int j = 1; j < 5; j++) { + textDescription = textDescription + "[" + j + "]" + Translator.get("test_case_step_desc") + i + "\n"; + + expectedResult = expectedResult + "[" + j + "]" + Translator.get("test_case_step_result") + i + "\n"; + + } + testCaseDTO.setTextDescription(textDescription); + testCaseDTO.setExpectedResult(expectedResult); + + list.add(testCaseDTO); + } + return list; + } + + private Map> getCustomFieldOptionsMap(List customFields) { + Map> returnMap = new HashMap<>(); + customFields.forEach(item -> { + List values = getOptionValues(Optional.ofNullable(item.getOptions()).orElse(new ArrayList<>())); + returnMap.put(item.getFieldName(), values); + }); + return returnMap; + } + + private List getOptionValues(List options) { + List values = new ArrayList<>(); + options.forEach(item -> { + values.add(item.getText()); + }); + return values; + } + + /** + * 获取表头字段 + * + * @param projectId + * @param customFields + * @return + */ + private List> getTemplateHead(String projectId, List customFields) { + List> heads = new FunctionalCaseExcelDataFactory().getFunctionalCaseExcelDataLocal().getHead(customFields); + return heads; + } +} 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 77c98da97b..c0d36106cb 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 @@ -13,6 +13,7 @@ import io.metersphere.project.domain.NotificationExample; import io.metersphere.project.mapper.NotificationMapper; import io.metersphere.project.service.ProjectTemplateService; import io.metersphere.sdk.constants.CustomFieldType; +import io.metersphere.sdk.constants.SessionConstants; import io.metersphere.sdk.constants.TemplateScene; import io.metersphere.sdk.constants.TemplateScopeType; import io.metersphere.sdk.util.JSON; @@ -35,11 +36,14 @@ import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.SqlConfig; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.util.LinkedMultiValueMap; import java.nio.charset.StandardCharsets; import java.util.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @AutoConfigureMockMvc @@ -60,6 +64,7 @@ public class FunctionalCaseControllerTests extends BaseTest { public static final String FUNCTIONAL_CASE_VERSION_URL = "/functional/case/version/"; public static final String FUNCTIONAL_CASE_BATCH_EDIT_URL = "/functional/case/batch/edit"; public static final String FUNCTIONAL_CASE_POS_URL = "/functional/case/edit/pos"; + public static final String DOWNLOAD_EXCEL_TEMPLATE_URL = "/functional/case/download/excel/template/"; @Resource private NotificationMapper notificationMapper; @@ -147,7 +152,7 @@ public class FunctionalCaseControllerTests extends BaseTest { //增加覆盖率 TemplateDTO templateDTO = projectTemplateService.getTemplateDTOById("21312", "100001100001", TemplateScene.FUNCTIONAL.name()); List customFields = templateDTO.getCustomFields(); - customFields.forEach(item ->{ + customFields.forEach(item -> { if (Translator.get("custom_field.functional_priority").equals(item.getFieldName())) { FunctionalCaseCustomField functionalCaseCustomField = new FunctionalCaseCustomField(); functionalCaseCustomField.setCaseId("TEST_FUNCTIONAL_CASE_ID_3"); @@ -440,7 +445,6 @@ public class FunctionalCaseControllerTests extends BaseTest { } - @Test @Order(18) public void testPos() throws Exception { @@ -455,4 +459,19 @@ public class FunctionalCaseControllerTests extends BaseTest { this.requestPostWithOkAndReturn(FUNCTIONAL_CASE_POS_URL, posRequest); } + + + @Test + @Order(19) + public void testDownloadExcelTemplate() throws Exception { + this.requestGetExcel(DOWNLOAD_EXCEL_TEMPLATE_URL + DEFAULT_PROJECT_ID); + + } + + private MvcResult requestGetExcel(String url) throws Exception { + return mockMvc.perform(MockMvcRequestBuilders.get(url) + .header(SessionConstants.HEADER_TOKEN, sessionId) + .header(SessionConstants.CSRF_TOKEN, csrfToken)) + .andExpect(status().isOk()).andReturn(); + } } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/excel/domain/ExcelDataFactory.java b/backend/services/system-setting/src/main/java/io/metersphere/system/excel/domain/ExcelDataFactory.java new file mode 100644 index 0000000000..c106005023 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/excel/domain/ExcelDataFactory.java @@ -0,0 +1,8 @@ +package io.metersphere.system.excel.domain; + +/** + * @author wx + */ +public interface ExcelDataFactory { + Object getExcelDataByLocal(); +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/excel/utils/EasyExcelExporter.java b/backend/services/system-setting/src/main/java/io/metersphere/system/excel/utils/EasyExcelExporter.java new file mode 100644 index 0000000000..3d7f415fa7 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/excel/utils/EasyExcelExporter.java @@ -0,0 +1,115 @@ +package io.metersphere.system.excel.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.write.handler.WriteHandler; +import com.alibaba.excel.write.metadata.style.WriteCellStyle; +import com.alibaba.excel.write.style.HorizontalCellStyleStrategy; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.util.LogUtils; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.poi.ss.SpreadsheetVersion; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class EasyExcelExporter { + + + private Class clazz; + + public EasyExcelExporter(Class clazz) { + this.clazz = clazz; + } + + public void export(HttpServletResponse response, List data, String fileName, String sheetName) { + buildExportResponse(response, fileName); + WriteCellStyle contentWriteCellStyle = new WriteCellStyle(); + contentWriteCellStyle.setWrapped(true); + try { + HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(null, contentWriteCellStyle); + EasyExcel.write(response.getOutputStream(), this.clazz) + .registerWriteHandler(horizontalCellStyleStrategy) + .sheet(sheetName) + .doWrite(data); + } catch (IOException e) { + LogUtils.error(e); + throw new MSException(e.getMessage()); + } + } + + public void exportByCustomWriteHandler(HttpServletResponse response, List> headList, + List> data, String fileName, String sheetName) { + buildExportResponse(response, fileName); + try { + EasyExcel.write(response.getOutputStream()) + .head(Optional.ofNullable(headList).orElse(new ArrayList<>())) + .sheet(sheetName) + .doWrite(data); + } catch (IOException e) { + LogUtils.error(e); + throw new MSException(e.getMessage()); + } + } + + public void buildExportResponse(HttpServletResponse response, String fileName) { + try { + response.setContentType("application/vnd.ms-excel"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()) + ".xlsx"); + } catch (IOException e) { + LogUtils.error(e); + throw new MSException(e.getMessage()); + } + } + + public void exportByCustomWriteHandler(HttpServletResponse response, List> headList, List> data, + String fileName, String sheetName, WriteHandler writeHandler) { + buildExportResponse(response, fileName); + try { + EasyExcel.write(response.getOutputStream()) + .head(Optional.ofNullable(headList).orElse(new ArrayList<>())) + .registerWriteHandler(writeHandler) + .sheet(sheetName) + .doWrite(data); + } catch (IOException e) { + LogUtils.error(e); + throw new MSException(e.getMessage()); + } + } + + public void exportByCustomWriteHandler(HttpServletResponse response, List> headList, List> data, + String fileName, String sheetName, WriteHandler writeHandler1, WriteHandler writeHandler2) { + buildExportResponse(response, fileName); + try { + EasyExcel.write(response.getOutputStream()) + .head(Optional.ofNullable(headList).orElse(new ArrayList<>())) + .registerWriteHandler(writeHandler1) + .registerWriteHandler(writeHandler2) + .sheet(sheetName) + .doWrite(data); + } catch (IOException e) { + LogUtils.error(e); + throw new MSException(e.getMessage()); + } + } + + public static void resetCellMaxTextLength() { + SpreadsheetVersion excel2007 = SpreadsheetVersion.EXCEL2007; + if (excel2007.getMaxTextLength() < Integer.MAX_VALUE) { + Field field; + try { + field = excel2007.getClass().getDeclaredField("_maxTextLength"); + field.setAccessible(true); + field.set(excel2007, Integer.MAX_VALUE); + } catch (Exception e) { + LogUtils.error(e); + throw new MSException(e.getMessage()); + } + } + } +} diff --git a/pom.xml b/pom.xml index e77115372a..0f1c8ef8c5 100644 --- a/pom.xml +++ b/pom.xml @@ -245,6 +245,7 @@ io/metersphere/**/dto/** io/metersphere/**/config/** io/metersphere/**/constants/** + io/metersphere/*/excel/** io/metersphere/sdk/** io/metersphere/provider/** io/metersphere/plugin/**