feat(功能用例): 功能用例导入excel模板下载

This commit is contained in:
WangXu10 2024-01-12 14:49:07 +08:00 committed by 刘瑞斌
parent f6fb6d384b
commit 2b695f6e1e
19 changed files with 883 additions and 8 deletions

View File

@ -18,7 +18,7 @@ public class BugProviderDTO implements Serializable {
@Schema(description = "id") @Schema(description = "id")
private String id; private String id;
@Schema(description = "bugId") @Schema(description = "缺陷id")
private String bugId; private String bugId;
@Schema(description = "缺陷名称") @Schema(description = "缺陷名称")

View File

@ -514,3 +514,10 @@ swagger_parse_error=Swagger parsing failed or file format is incorrect!
#测试计划 #测试计划
permission.test_plan.name=Test plan permission.test_plan.name=Test plan
permission.test_plan_module.name=Test plan module 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

View File

@ -510,3 +510,10 @@ swagger_parse_error=Swagger 解析失败,请确认文件格式是否正确!
#测试计划 #测试计划
permission.test_plan.name=测试计划 permission.test_plan.name=测试计划
permission.test_plan_module.name=测试计划模块 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=非必填

View File

@ -510,3 +510,10 @@ swagger_parse_error=Swagger 解析失敗
#測試計劃 #測試計劃
permission.test_plan.name=測試計劃 permission.test_plan.name=測試計劃
permission.test_plan_module.name=測試計劃模塊 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=非必填

View File

@ -8,6 +8,7 @@ import io.metersphere.functional.dto.FunctionalCaseDetailDTO;
import io.metersphere.functional.dto.FunctionalCasePageDTO; import io.metersphere.functional.dto.FunctionalCasePageDTO;
import io.metersphere.functional.dto.FunctionalCaseVersionDTO; import io.metersphere.functional.dto.FunctionalCaseVersionDTO;
import io.metersphere.functional.request.*; import io.metersphere.functional.request.*;
import io.metersphere.functional.service.FunctionalCaseFileService;
import io.metersphere.functional.service.FunctionalCaseLogService; import io.metersphere.functional.service.FunctionalCaseLogService;
import io.metersphere.functional.service.FunctionalCaseNoticeService; import io.metersphere.functional.service.FunctionalCaseNoticeService;
import io.metersphere.functional.service.FunctionalCaseService; 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.project.service.ProjectTemplateService;
import io.metersphere.sdk.constants.PermissionConstants; import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.constants.TemplateScene; 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.TemplateDTO;
import io.metersphere.system.dto.sdk.request.PosRequest;
import io.metersphere.system.log.annotation.Log; import io.metersphere.system.log.annotation.Log;
import io.metersphere.system.log.constants.OperationLogType; import io.metersphere.system.log.constants.OperationLogType;
import io.metersphere.system.notice.annotation.SendNotice; 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.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@ -50,6 +52,8 @@ public class FunctionalCaseController {
@Resource @Resource
private ProjectTemplateService projectTemplateService; private ProjectTemplateService projectTemplateService;
@Resource
private FunctionalCaseFileService functionalCaseFileService;
//TODO 获取模板列表(多模板功能暂时不做) //TODO 获取模板列表(多模板功能暂时不做)
@ -205,4 +209,12 @@ public class FunctionalCaseController {
functionalCaseService.editPos(request); 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);
}
} }

View File

@ -40,7 +40,7 @@ public class FunctionalCaseRelationshipController {
@GetMapping("/get-ids/{caseId}") @GetMapping("/get-ids/{caseId}")
@Operation(summary = "用例管理-功能用例-用例详情-前后置关系-获取已关联用例id集合(关联用例弹窗前调用)") @Operation(summary = "用例管理-功能用例-用例详情-前后置关系-获取已关联用例id集合(关联用例弹窗前调用)")
@CheckOwner(resourceId = "#reviewId", resourceType = "case_review") @CheckOwner(resourceId = "#caseId", resourceType = "functional_case")
public List<String> getCaseIds(@PathVariable String caseId) { public List<String> getCaseIds(@PathVariable String caseId) {
return functionalCaseRelationshipEdgeService.getExcludeIds(caseId); return functionalCaseRelationshipEdgeService.getExcludeIds(caseId);
} }

View File

@ -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 {
}

View File

@ -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<Locale, String> filedLangMap;
private Function<FunctionalCaseExcelData, String> parseFunc;
private String value;
FunctionalCaseImportFiled(String value, String zn, String chineseTw, String us, Function<FunctionalCaseExcelData, String> parseFunc) {
this.filedLangMap = new HashMap<Locale, String>();
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<Locale, String> 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);
}
}

View File

@ -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<String, Object> customData = new LinkedHashMap<>();
@ExcelIgnore
Map<String, String> otherFields;
public List<List<String>> getHead(List<TemplateCustomFieldDTO> customFields) {
return new ArrayList<>();
}
public List<List<String>> getHead(List<TemplateCustomFieldDTO> customFields, Locale lang) {
List<List<String>> 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<String> list = new ArrayList<>();
list.add(dto.getFieldName());
heads.add(list);
}
}
return heads;
}
}

View File

@ -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<List<String>> getHead(List<TemplateCustomFieldDTO> customFields) {
return super.getHead(customFields, Locale.SIMPLIFIED_CHINESE);
}
}

View File

@ -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();
}
}

View File

@ -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<List<String>> getHead(List<TemplateCustomFieldDTO> customFields) {
return super.getHead(customFields, Locale.TRADITIONAL_CHINESE);
}
}

View File

@ -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<List<String>> getHead(List<TemplateCustomFieldDTO> customFields) {
return super.getHead(customFields, Locale.US);
}
}

View File

@ -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<String, List<String>> caseLevelAndStatusValueMap;
private Sheet sheet;
private Drawing<?> drawingPatriarch;
private Map<String, TemplateCustomFieldDTO> customField;
private Map<String, Integer> fieldMap = new HashMap<>();
public FunctionCaseTemplateWriteHandler(List<List<String>> headList, Map<String, List<String>> caseLevelAndStatusValueMap, Map<String, TemplateCustomFieldDTO> customFieldMap) {
initIndex(headList);
this.caseLevelAndStatusValueMap = caseLevelAndStatusValueMap;
this.customField = customFieldMap;
}
private void initIndex(List<List<String>> headList) {
int index = 0;
for (List<String> 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<Map.Entry<String, Integer>> iterator = fieldMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> 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<String> 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);
}
}

View File

@ -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<TemplateCustomFieldDTO> customFields = Optional.ofNullable(defaultTemplateDTO.getCustomFields()).orElse(new ArrayList<>());
//获取表头字段 当前项目下默认模板的自定义字段 heads:默认表头名称+自定义字段名称
List<List<String>> heads = getTemplateHead(projectId, customFields);
FunctionalCaseExcelData caseExcelData = new FunctionalCaseExcelDataFactory().getFunctionalCaseExcelDataLocal();
//默认字段+自定义字段的 options集合
Map<String, List<String>> customFieldOptionsMap = getCustomFieldOptionsMap(customFields);
Map<String, TemplateCustomFieldDTO> customFieldMap = customFields.stream().collect(Collectors.toMap(TemplateCustomFieldDTO::getFieldName, templateCustomFieldDTO -> templateCustomFieldDTO));
//表头备注信息
FunctionCaseTemplateWriteHandler handler = new FunctionCaseTemplateWriteHandler(heads, customFieldOptionsMap, customFieldMap);
List<FunctionalCaseExcelData> functionalCaseExcelData = generateExportData();
List<List<Object>> 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<List<Object>> parseExcelData2List(List<List<String>> headListParams, List<FunctionalCaseExcelData> data) {
List<List<Object>> result = new ArrayList<>();
//转化excel头
List<String> headList = new ArrayList<>();
for (List<String> list : headListParams) {
for (String head : list) {
headList.add(head);
}
}
FunctionalCaseImportFiled[] importFields = FunctionalCaseImportFiled.values();
for (FunctionalCaseExcelData model : data) {
List<Object> fields = new ArrayList<>();
Map<String, Object> customDataMaps = Optional.ofNullable(model.getCustomData())
.orElse(new HashMap<>());
Map<String, String> 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<FunctionalCaseExcelData> generateExportData() {
List<FunctionalCaseExcelData> 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<String, List<String>> getCustomFieldOptionsMap(List<TemplateCustomFieldDTO> customFields) {
Map<String, List<String>> returnMap = new HashMap<>();
customFields.forEach(item -> {
List<String> values = getOptionValues(Optional.ofNullable(item.getOptions()).orElse(new ArrayList<>()));
returnMap.put(item.getFieldName(), values);
});
return returnMap;
}
private List<String> getOptionValues(List<CustomFieldOption> options) {
List<String> values = new ArrayList<>();
options.forEach(item -> {
values.add(item.getText());
});
return values;
}
/**
* 获取表头字段
*
* @param projectId
* @param customFields
* @return
*/
private List<List<String>> getTemplateHead(String projectId, List<TemplateCustomFieldDTO> customFields) {
List<List<String>> heads = new FunctionalCaseExcelDataFactory().getFunctionalCaseExcelDataLocal().getHead(customFields);
return heads;
}
}

View File

@ -13,6 +13,7 @@ import io.metersphere.project.domain.NotificationExample;
import io.metersphere.project.mapper.NotificationMapper; import io.metersphere.project.mapper.NotificationMapper;
import io.metersphere.project.service.ProjectTemplateService; import io.metersphere.project.service.ProjectTemplateService;
import io.metersphere.sdk.constants.CustomFieldType; import io.metersphere.sdk.constants.CustomFieldType;
import io.metersphere.sdk.constants.SessionConstants;
import io.metersphere.sdk.constants.TemplateScene; import io.metersphere.sdk.constants.TemplateScene;
import io.metersphere.sdk.constants.TemplateScopeType; import io.metersphere.sdk.constants.TemplateScopeType;
import io.metersphere.sdk.util.JSON; 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.Sql;
import org.springframework.test.context.jdbc.SqlConfig; import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@AutoConfigureMockMvc @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_VERSION_URL = "/functional/case/version/";
public static final String FUNCTIONAL_CASE_BATCH_EDIT_URL = "/functional/case/batch/edit"; 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 FUNCTIONAL_CASE_POS_URL = "/functional/case/edit/pos";
public static final String DOWNLOAD_EXCEL_TEMPLATE_URL = "/functional/case/download/excel/template/";
@Resource @Resource
private NotificationMapper notificationMapper; private NotificationMapper notificationMapper;
@ -147,7 +152,7 @@ public class FunctionalCaseControllerTests extends BaseTest {
//增加覆盖率 //增加覆盖率
TemplateDTO templateDTO = projectTemplateService.getTemplateDTOById("21312", "100001100001", TemplateScene.FUNCTIONAL.name()); TemplateDTO templateDTO = projectTemplateService.getTemplateDTOById("21312", "100001100001", TemplateScene.FUNCTIONAL.name());
List<TemplateCustomFieldDTO> customFields = templateDTO.getCustomFields(); List<TemplateCustomFieldDTO> customFields = templateDTO.getCustomFields();
customFields.forEach(item ->{ customFields.forEach(item -> {
if (Translator.get("custom_field.functional_priority").equals(item.getFieldName())) { if (Translator.get("custom_field.functional_priority").equals(item.getFieldName())) {
FunctionalCaseCustomField functionalCaseCustomField = new FunctionalCaseCustomField(); FunctionalCaseCustomField functionalCaseCustomField = new FunctionalCaseCustomField();
functionalCaseCustomField.setCaseId("TEST_FUNCTIONAL_CASE_ID_3"); functionalCaseCustomField.setCaseId("TEST_FUNCTIONAL_CASE_ID_3");
@ -440,7 +445,6 @@ public class FunctionalCaseControllerTests extends BaseTest {
} }
@Test @Test
@Order(18) @Order(18)
public void testPos() throws Exception { public void testPos() throws Exception {
@ -455,4 +459,19 @@ public class FunctionalCaseControllerTests extends BaseTest {
this.requestPostWithOkAndReturn(FUNCTIONAL_CASE_POS_URL, posRequest); 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();
}
} }

View File

@ -0,0 +1,8 @@
package io.metersphere.system.excel.domain;
/**
* @author wx
*/
public interface ExcelDataFactory {
Object getExcelDataByLocal();
}

View File

@ -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<List<String>> headList,
List<List<Object>> 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<List<String>> headList, List<List<Object>> 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<List<String>> headList, List<List<Object>> 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());
}
}
}
}

View File

@ -245,6 +245,7 @@
<exclude>io/metersphere/**/dto/**</exclude> <exclude>io/metersphere/**/dto/**</exclude>
<exclude>io/metersphere/**/config/**</exclude> <exclude>io/metersphere/**/config/**</exclude>
<exclude>io/metersphere/**/constants/**</exclude> <exclude>io/metersphere/**/constants/**</exclude>
<exclude>io/metersphere/*/excel/**</exclude>
<exclude>io/metersphere/sdk/**</exclude> <exclude>io/metersphere/sdk/**</exclude>
<exclude>io/metersphere/provider/**</exclude> <exclude>io/metersphere/provider/**</exclude>
<exclude>io/metersphere/plugin/**</exclude> <exclude>io/metersphere/plugin/**</exclude>