feat(功能用例): 功能用例导入检查
This commit is contained in:
parent
f2df25ba0a
commit
5570d2ba17
|
@ -525,6 +525,16 @@ excel.template.tag=Not mandatory labels should be separated by semicolons or com
|
|||
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
|
||||
case_import_table_header_missing=Header information is missing!
|
||||
functional_case.module.length_less_than=The title is too long, the length must be less than
|
||||
custom_field_required_tip=[%s] is required
|
||||
custom_field_array_tip=[%s] must be array
|
||||
custom_field_datetime_tip=[%s] must be in time-date format [%s]
|
||||
custom_field_date_tip=[%s] must be in date format [%s]
|
||||
custom_field_member_tip=[%s] must be current project member
|
||||
custom_field_select_tip=[%s] must be %s
|
||||
custom_field_int_tip=[%s] must be integer
|
||||
custom_field_float_tip=[%s] must be number
|
||||
|
||||
#关联
|
||||
relate_source_id_not_blank=Source id cannot be empty
|
||||
|
|
|
@ -521,7 +521,16 @@ excel.template.tag=非必填,标签之间以分号或者逗号隔开
|
|||
excel.template.text_description=非必填,编辑模式为STEP时,步骤描述会根据标识[1] [2] [3]...来判断是否将单元格拆分为多个步骤,没有则为一个步骤
|
||||
excel.template.member=非必填,请填写该项目下的相关人员ID或邮箱
|
||||
excel.template.not_required=非必填
|
||||
|
||||
case_import_table_header_missing=表头信息缺失!
|
||||
functional_case.module.length_less_than=标题过长,字数必须小于
|
||||
custom_field_required_tip=[%s]为必填项
|
||||
custom_field_array_tip=[%s]必须是数组
|
||||
custom_field_datetime_tip=[%s]必须为时间日期格式[%s]
|
||||
custom_field_date_tip=[%s]必须为日期格式[%s]
|
||||
custom_field_member_tip=[%s]必须当前项目成员
|
||||
custom_field_select_tip=[%s]必须为%s
|
||||
custom_field_int_tip=[%s]必须为整型
|
||||
custom_field_float_tip=[%s]必须为数字
|
||||
#关联
|
||||
relate_source_id_not_blank=关联来源ID不能为空
|
||||
relate_source_id_length_range=关联来源ID必须在{min}和{max}之间
|
||||
|
|
|
@ -521,6 +521,16 @@ excel.template.tag=非必填,標簽之間以分號或者逗號隔開
|
|||
excel.template.text_description=非必填,編輯模式為STEP時,步驟描述會根據標識[1] [2] [3]...來判斷是否將單元格拆分為多個步驟,沒有則為一個步驟
|
||||
excel.template.member=非必填,請填寫該項目下的相關人員ID或郵箱
|
||||
excel.template.not_required=非必填
|
||||
case_import_table_header_missing=表頭信息缺失!
|
||||
functional_case.module.length_less_than=標題過長,字數必須小於
|
||||
custom_field_required_tip=[%s]為必填項
|
||||
custom_field_array_tip=[%s]必須是數組
|
||||
custom_field_datetime_tip=[%s]必須為時間日期格式[%s]
|
||||
custom_field_date_tip=[%s]必須為日期格式[%s]
|
||||
custom_field_member_tip=[%s]必須當前項目成員
|
||||
custom_field_select_tip=[%s]必須為%s
|
||||
custom_field_int_tip=[%s]必須為整型
|
||||
custom_field_float_tip=[%s]必須為數字
|
||||
|
||||
#关联
|
||||
relate_source_id_not_blank=關聯來源ID不能為空
|
||||
|
|
|
@ -7,6 +7,7 @@ import io.metersphere.functional.domain.FunctionalCase;
|
|||
import io.metersphere.functional.dto.FunctionalCaseDetailDTO;
|
||||
import io.metersphere.functional.dto.FunctionalCasePageDTO;
|
||||
import io.metersphere.functional.dto.FunctionalCaseVersionDTO;
|
||||
import io.metersphere.functional.dto.response.FunctionalCaseImportResponse;
|
||||
import io.metersphere.functional.request.*;
|
||||
import io.metersphere.functional.service.FunctionalCaseFileService;
|
||||
import io.metersphere.functional.service.FunctionalCaseLogService;
|
||||
|
@ -217,4 +218,12 @@ public class FunctionalCaseController {
|
|||
public void testCaseTemplateExport(@PathVariable String projectId, HttpServletResponse response) {
|
||||
functionalCaseFileService.downloadExcelTemplate(projectId, response);
|
||||
}
|
||||
|
||||
@PostMapping("/pre-check/excel")
|
||||
@Operation(summary = "用例管理-功能用例-excel导入检查")
|
||||
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_UPDATE)
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package io.metersphere.functional.dto.response;
|
||||
|
||||
import io.metersphere.functional.excel.domain.FunctionalCaseExcelData;
|
||||
import io.metersphere.system.excel.domain.ExcelErrData;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
@Data
|
||||
public class FunctionalCaseImportResponse implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "成功数量")
|
||||
private int successCount;
|
||||
@Schema(description = "失败数量")
|
||||
private int failCount;
|
||||
@Schema(description = "报错信息")
|
||||
private List<ExcelErrData<FunctionalCaseExcelData>> errorMessages;
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package io.metersphere.functional.excel.domain;
|
||||
|
||||
import lombok.Data;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
@Data
|
||||
public class ExcelMergeInfo implements Comparable<ExcelMergeInfo> {
|
||||
|
||||
/**
|
||||
* 合并单元格的第一行
|
||||
*/
|
||||
private Integer firstRowIndex;
|
||||
/**
|
||||
* 合并单元格的最后一行
|
||||
*/
|
||||
private Integer lastRowIndex;
|
||||
/**
|
||||
* 合并单元格的第一列,不考虑同一行合并单元格的情况
|
||||
*/
|
||||
private Integer firstColumnIndex;
|
||||
|
||||
/**
|
||||
* 根据 firstRowIndex, firstColumnIndex 重写 compareTo
|
||||
* 使用 TreeSet 按 Excel 表格顺序查找时,可以优化效率
|
||||
*
|
||||
* @param o
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(@NotNull ExcelMergeInfo o) {
|
||||
int compare = Integer.compare(this.getFirstRowIndex(), o.getFirstRowIndex());
|
||||
if (compare == 0) {
|
||||
return Integer.compare(this.getFirstColumnIndex(), o.getFirstColumnIndex());
|
||||
}
|
||||
return compare;
|
||||
}
|
||||
}
|
|
@ -15,8 +15,6 @@ import java.util.*;
|
|||
@Getter
|
||||
@Setter
|
||||
public class FunctionalCaseExcelData {
|
||||
@ExcelIgnore
|
||||
private String id;
|
||||
@ExcelIgnore
|
||||
private String num;
|
||||
@ExcelIgnore
|
||||
|
@ -40,6 +38,18 @@ public class FunctionalCaseExcelData {
|
|||
Map<String, Object> customData = new LinkedHashMap<>();
|
||||
@ExcelIgnore
|
||||
Map<String, String> otherFields;
|
||||
@ExcelIgnore
|
||||
Set<String> textFieldSet = new HashSet<>(1);
|
||||
/**
|
||||
* 合并文本描述
|
||||
*/
|
||||
@ExcelIgnore
|
||||
List<String> MergeTextDescription;
|
||||
/**
|
||||
* 合并步骤结果
|
||||
*/
|
||||
@ExcelIgnore
|
||||
List<String> mergeExpectedResult;
|
||||
|
||||
|
||||
public List<List<String>> getHead(List<TemplateCustomFieldDTO> customFields) {
|
||||
|
|
|
@ -33,7 +33,7 @@ public class FunctionalCaseExcelDataCn extends FunctionalCaseExcelData {
|
|||
@Length(max = 50)
|
||||
@ExcelProperty("所属模块")
|
||||
@ColumnWidth(30)
|
||||
private String moduleId;
|
||||
private String module;
|
||||
|
||||
@ColumnWidth(50)
|
||||
@ExcelProperty("标签")
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package io.metersphere.functional.excel.exception;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class CustomFieldValidateException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public CustomFieldValidateException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public static void throwException(String message) throws CustomFieldValidateException {
|
||||
throw new CustomFieldValidateException(message);
|
||||
}
|
||||
}
|
|
@ -106,7 +106,7 @@ public class FunctionCaseTemplateWriteHandler implements RowWriteHandler {
|
|||
}
|
||||
} else {
|
||||
if (CollectionUtils.isNotEmpty(strings)) {
|
||||
setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.not_required").concat(":").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(caseLevelAndStatusValueMap.get(entry.getKey()))));
|
||||
} else {
|
||||
setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.not_required"));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,457 @@
|
|||
package io.metersphere.functional.excel.listener;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.context.AnalysisContext;
|
||||
import com.alibaba.excel.event.AnalysisEventListener;
|
||||
import io.metersphere.functional.excel.annotation.NotRequired;
|
||||
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.exception.CustomFieldValidateException;
|
||||
import io.metersphere.functional.excel.validate.AbstractCustomFieldValidator;
|
||||
import io.metersphere.functional.excel.validate.CustomFieldValidatorFactory;
|
||||
import io.metersphere.functional.request.FunctionalCaseImportRequest;
|
||||
import io.metersphere.plugin.sdk.util.MSPluginException;
|
||||
import io.metersphere.sdk.util.LogUtils;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import io.metersphere.system.dto.excel.ExcelValidateHelper;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
import io.metersphere.system.excel.domain.ExcelErrData;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class FunctionalCaseCheckEventListener extends AnalysisEventListener<Map<Integer, String>> {
|
||||
|
||||
private Class excelDataClass;
|
||||
private Map<Integer, String> headMap;
|
||||
private Map<String, TemplateCustomFieldDTO> customFieldsMap = new HashMap<>();
|
||||
private Set<ExcelMergeInfo> mergeInfoSet;
|
||||
private Map<String, String> excelHeadToFieldNameDic = new HashMap<>();
|
||||
/**
|
||||
* 标记下当前遍历的行是不是有合并单元格
|
||||
*/
|
||||
private Boolean isMergeRow;
|
||||
/**
|
||||
* 标记下当前遍历的行是不是合并单元格的最后一行
|
||||
*/
|
||||
private Boolean isMergeLastRow;
|
||||
/**
|
||||
* 存储合并单元格对应的数据,key 为重写了 compareTo 的 ExcelMergeInfo
|
||||
*/
|
||||
private HashMap<ExcelMergeInfo, String> mergeCellDataMap = new HashMap<>();
|
||||
/**
|
||||
* 存储当前合并的一条完整数据,其中步骤没有合并是多行
|
||||
*/
|
||||
private FunctionalCaseExcelData currentMergeData;
|
||||
private Integer firstMergeRowIndex;
|
||||
protected List<FunctionalCaseExcelData> list = new ArrayList<>();
|
||||
protected List<ExcelErrData<FunctionalCaseExcelData>> errList = new ArrayList<>();
|
||||
private static final String ERROR_MSG_SEPARATOR = ";";
|
||||
private HashMap<String, AbstractCustomFieldValidator> customFieldValidatorMap;
|
||||
private static AtomicInteger successCount = new AtomicInteger(0);
|
||||
|
||||
|
||||
public FunctionalCaseCheckEventListener(FunctionalCaseImportRequest request, Class clazz, List<TemplateCustomFieldDTO> customFields, Set<ExcelMergeInfo> mergeInfoSet) {
|
||||
this.mergeInfoSet = mergeInfoSet;
|
||||
excelDataClass = clazz;
|
||||
//当前项目模板的自定义字段
|
||||
customFieldsMap = customFields.stream().collect(Collectors.toMap(TemplateCustomFieldDTO::getFieldName, i -> i));
|
||||
customFieldValidatorMap = CustomFieldValidatorFactory.getValidatorMap();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
|
||||
this.headMap = headMap;
|
||||
try {
|
||||
genExcelHeadToFieldNameDicAndGetNotRequiredFields();
|
||||
} catch (NoSuchFieldException e) {
|
||||
LogUtils.error(e);
|
||||
}
|
||||
formatHeadMap();
|
||||
super.invokeHeadMap(headMap, context);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void invoke(Map<Integer, String> data, AnalysisContext analysisContext) {
|
||||
if (headMap == null) {
|
||||
throw new MSPluginException("case_import_table_header_missing");
|
||||
}
|
||||
Integer rowIndex = analysisContext.readRowHolder().getRowIndex();
|
||||
//处理合并单元格
|
||||
handleMergeData(data, rowIndex);
|
||||
FunctionalCaseExcelData functionalCaseExcelData;
|
||||
// 读取名称列,如果该列是合并单元格,则读取多行数据后合并步骤
|
||||
if (isMergeRow) {
|
||||
if (currentMergeData == null) {
|
||||
firstMergeRowIndex = rowIndex;
|
||||
// 如果是合并单元格的首行
|
||||
functionalCaseExcelData = parseDataToModel(data);
|
||||
//合并文本描述
|
||||
functionalCaseExcelData.setMergeTextDescription(new ArrayList<>() {
|
||||
@Serial
|
||||
private static final long serialVersionUID = -2563948462432733672L;
|
||||
|
||||
{
|
||||
add(functionalCaseExcelData.getTextDescription());
|
||||
}
|
||||
});
|
||||
//合并
|
||||
functionalCaseExcelData.setMergeExpectedResult(new ArrayList<>() {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 8985001651375529701L;
|
||||
|
||||
{
|
||||
add(functionalCaseExcelData.getExpectedResult());
|
||||
}
|
||||
});
|
||||
// 记录下数据并返回
|
||||
currentMergeData = functionalCaseExcelData;
|
||||
if (!isMergeLastRow) {
|
||||
return;
|
||||
} else {
|
||||
currentMergeData = null;
|
||||
}
|
||||
} else {
|
||||
// 获取存储的数据,并添加多个步骤
|
||||
currentMergeData.getMergeTextDescription()
|
||||
.add(data.get(getTextDescriptionColIndex()));
|
||||
currentMergeData.getMergeExpectedResult()
|
||||
.add(data.get(getExpectedResultColIndex()));
|
||||
// 是最后一行的合并单元格,保存并清空 currentMergeData,走之后的逻辑
|
||||
if (isMergeLastRow) {
|
||||
functionalCaseExcelData = currentMergeData;
|
||||
currentMergeData = null;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
firstMergeRowIndex = null;
|
||||
functionalCaseExcelData = parseDataToModel(data);
|
||||
}
|
||||
//校验数据
|
||||
buildUpdateOrErrorList(rowIndex, functionalCaseExcelData);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
|
||||
// 如果文件最后一行是没有内容的步骤,这里处理最后一条合并单元格的数据
|
||||
if (currentMergeData != null) {
|
||||
buildUpdateOrErrorList(firstMergeRowIndex, currentMergeData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建数据
|
||||
*
|
||||
* @param rowIndex
|
||||
* @param functionalCaseExcelData
|
||||
*/
|
||||
private void buildUpdateOrErrorList(Integer rowIndex, FunctionalCaseExcelData functionalCaseExcelData) {
|
||||
StringBuilder errMsg;
|
||||
try {
|
||||
//根据excel数据实体中的javax.validation + 正则表达式来校验excel数据
|
||||
errMsg = new StringBuilder(ExcelValidateHelper.validateEntity(functionalCaseExcelData));
|
||||
//自定义校验规则
|
||||
if (StringUtils.isEmpty(errMsg)) {
|
||||
//开始校验
|
||||
validate(functionalCaseExcelData, errMsg);
|
||||
}
|
||||
} catch (NoSuchFieldException e) {
|
||||
errMsg = new StringBuilder(Translator.get("parse_data_error"));
|
||||
LogUtils.error(e.getMessage(), e);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(errMsg)) {
|
||||
Integer errorRowIndex = rowIndex;
|
||||
if (firstMergeRowIndex != null) {
|
||||
errorRowIndex = firstMergeRowIndex;
|
||||
}
|
||||
ExcelErrData excelErrData = new ExcelErrData(rowIndex,
|
||||
Translator.get("number")
|
||||
.concat(StringUtils.SPACE)
|
||||
.concat(String.valueOf(errorRowIndex + 1)).concat(StringUtils.SPACE)
|
||||
.concat(Translator.get("row"))
|
||||
.concat(Translator.get("error"))
|
||||
.concat(":")
|
||||
.concat(errMsg.toString()));
|
||||
//错误信息
|
||||
errList.add(excelErrData);
|
||||
} else {
|
||||
//通过数量
|
||||
successCount.incrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void validate(FunctionalCaseExcelData data, StringBuilder errMsg) {
|
||||
//模块校验
|
||||
validateModule(data, errMsg);
|
||||
//校验自定义字段
|
||||
validateCustomField(data, errMsg);
|
||||
//校验id
|
||||
validateIdExist(data, errMsg);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 校验Excel中是否有ID
|
||||
* 是否覆盖:
|
||||
* 1.覆盖:id存在则更新,id不存在则新增
|
||||
* 2.不覆盖:id存在不处理,id不存在新增
|
||||
*
|
||||
* @param data
|
||||
* @param errMsg
|
||||
*/
|
||||
@Nullable
|
||||
private void validateIdExist(FunctionalCaseExcelData data, StringBuilder errMsg) {
|
||||
//当前读取的数据有ID
|
||||
if (StringUtils.isNotEmpty(data.getNum())) {
|
||||
Integer num = -1;
|
||||
num = Integer.parseInt(data.getNum());
|
||||
if (num < 0) {
|
||||
errMsg.append(Translator.get("id_not_rightful"))
|
||||
.append("[")
|
||||
.append(data.getNum())
|
||||
.append("]; ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 校验自定义字段,并记录错误提示
|
||||
* 如果填写的是自定义字段的选项值,则转换成ID保存
|
||||
*
|
||||
* @param data
|
||||
* @param errMsg
|
||||
*/
|
||||
private void validateCustomField(FunctionalCaseExcelData data, StringBuilder errMsg) {
|
||||
Map<String, Object> customData = data.getCustomData();
|
||||
for (String fieldName : customData.keySet()) {
|
||||
Object value = customData.get(fieldName);
|
||||
TemplateCustomFieldDTO templateCustomFieldDTO = customFieldsMap.get(fieldName);
|
||||
if (templateCustomFieldDTO == null) {
|
||||
continue;
|
||||
}
|
||||
AbstractCustomFieldValidator customFieldValidator = customFieldValidatorMap.get(templateCustomFieldDTO.getType());
|
||||
try {
|
||||
customFieldValidator.validate(templateCustomFieldDTO, value.toString());
|
||||
} catch (CustomFieldValidateException e) {
|
||||
errMsg.append(e.getMessage().concat(ERROR_MSG_SEPARATOR));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 校验模块
|
||||
*
|
||||
* @param data
|
||||
* @param errMsg
|
||||
*/
|
||||
private void validateModule(FunctionalCaseExcelData data, StringBuilder errMsg) {
|
||||
String module = data.getModule();
|
||||
if (StringUtils.isNotEmpty(module)) {
|
||||
String[] nodes = module.split("/");
|
||||
//模块名不能为空
|
||||
for (int i = 0; i < nodes.length; i++) {
|
||||
if (i != 0 && StringUtils.equals(nodes[i].trim(), StringUtils.EMPTY)) {
|
||||
errMsg.append(Translator.get("module_not_null"))
|
||||
.append(ERROR_MSG_SEPARATOR);
|
||||
break;
|
||||
}
|
||||
}
|
||||
//增加字数校验,每一层不能超过100个字
|
||||
for (int i = 0; i < nodes.length; i++) {
|
||||
String nodeStr = nodes[i];
|
||||
if (StringUtils.isNotEmpty(nodeStr)) {
|
||||
if (nodeStr.trim().length() > 100) {
|
||||
errMsg.append(Translator.get("module"))
|
||||
.append(Translator.get("functional_case.module.length_less_than"))
|
||||
.append("100:")
|
||||
.append(nodeStr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 数据转换
|
||||
*
|
||||
* @param row
|
||||
* @return
|
||||
*/
|
||||
private FunctionalCaseExcelData parseDataToModel(Map<Integer, String> row) {
|
||||
FunctionalCaseExcelData data = new FunctionalCaseExcelDataFactory().getFunctionalCaseExcelDataLocal();
|
||||
for (Map.Entry<Integer, String> headEntry : headMap.entrySet()) {
|
||||
Integer index = headEntry.getKey();
|
||||
String field = headEntry.getValue();
|
||||
if (StringUtils.isBlank(field)) {
|
||||
continue;
|
||||
}
|
||||
String value = StringUtils.isEmpty(row.get(index)) ? StringUtils.EMPTY : row.get(index);
|
||||
|
||||
if (excelHeadToFieldNameDic.containsKey(field)) {
|
||||
field = excelHeadToFieldNameDic.get(field);
|
||||
}
|
||||
|
||||
if (StringUtils.equals(field, "id")) {
|
||||
data.setName(value);
|
||||
} else if (StringUtils.equals(field, "num")) {
|
||||
data.setNum(value);
|
||||
} else if (StringUtils.equals(field, "name")) {
|
||||
data.setName(value);
|
||||
} else if (StringUtils.equals(field, "module")) {
|
||||
data.setModule(value);
|
||||
} else if (StringUtils.equals(field, "tags")) {
|
||||
data.setTags(value);
|
||||
} else if (StringUtils.equals(field, "prerequisite")) {
|
||||
data.setPrerequisite(value);
|
||||
} else if (StringUtils.equals(field, "description")) {
|
||||
data.setDescription(value);
|
||||
} else if (StringUtils.equals(field, "textDescription")) {
|
||||
data.setTextDescription(value);
|
||||
} else if (StringUtils.equals(field, "expectedResult")) {
|
||||
data.setExpectedResult(value);
|
||||
} else if (StringUtils.equals(field, "caseEditType")) {
|
||||
data.setCaseEditType(value);
|
||||
} else {
|
||||
data.getCustomData().put(field, value);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理合并单元格
|
||||
*
|
||||
* @param data
|
||||
* @param rowIndex
|
||||
*/
|
||||
private void handleMergeData(Map<Integer, String> data, Integer rowIndex) {
|
||||
isMergeRow = false;
|
||||
isMergeLastRow = false;
|
||||
if (getNameColIndex() == null) {
|
||||
throw new MSPluginException("缺少名称表头");
|
||||
}
|
||||
data.keySet().forEach(col -> {
|
||||
Iterator<ExcelMergeInfo> iterator = mergeInfoSet.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
ExcelMergeInfo mergeInfo = iterator.next();
|
||||
// 如果单元格的行号在合并单元格的范围之间,并且列号相等,说明该单元格是合并单元格中的一部分
|
||||
if (mergeInfo.getFirstRowIndex() <= rowIndex && rowIndex <= mergeInfo.getLastRowIndex()
|
||||
&& col.equals(mergeInfo.getFirstColumnIndex())) {
|
||||
// 根据名称列是否是合并单元格判断是不是同一条用例
|
||||
if (getNameColIndex().equals(col)) {
|
||||
isMergeRow = true;
|
||||
}
|
||||
// 如果是合并单元格的第一个cell,则把这个单元格的数据存起来
|
||||
if (rowIndex.equals(mergeInfo.getFirstRowIndex())) {
|
||||
if (StringUtils.isNotBlank(data.get(col))) {
|
||||
mergeCellDataMap.put(mergeInfo, data.get(col));
|
||||
}
|
||||
} else {
|
||||
// 非第一个,获取存储的数据填充
|
||||
String cellData = mergeCellDataMap.get(mergeInfo);
|
||||
if (StringUtils.isNotBlank(cellData)) {
|
||||
data.put(col, cellData);
|
||||
}
|
||||
}
|
||||
// 如果合并单元格的最后一个单元格,标记下
|
||||
if (rowIndex.equals(mergeInfo.getLastRowIndex())) {
|
||||
// 根据名称列是否是合并单元格判断是不是同一条用例
|
||||
if (getNameColIndex().equals(col)) {
|
||||
isMergeLastRow = true;
|
||||
// 清除掉上一次已经遍历完成的数据,提高查询效率
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Integer getNameColIndex() {
|
||||
return findColIndex("name");
|
||||
}
|
||||
|
||||
private Integer getTextDescriptionColIndex() {
|
||||
return findColIndex("textDescription");
|
||||
}
|
||||
|
||||
private Integer getExpectedResultColIndex() {
|
||||
return findColIndex("expectedResult");
|
||||
}
|
||||
|
||||
private Integer findColIndex(String colName) {
|
||||
for (Integer key : headMap.keySet()) {
|
||||
if (StringUtils.equals(headMap.get(key), colName)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取注解里ExcelProperty的value
|
||||
*/
|
||||
public Set<String> genExcelHeadToFieldNameDicAndGetNotRequiredFields() throws NoSuchFieldException {
|
||||
|
||||
Set<String> result = new HashSet<>();
|
||||
Field field;
|
||||
Field[] fields = excelDataClass.getDeclaredFields();
|
||||
for (int i = 0; i < fields.length; i++) {
|
||||
field = excelDataClass.getDeclaredField(fields[i].getName());
|
||||
field.setAccessible(true);
|
||||
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
|
||||
if (excelProperty != null) {
|
||||
StringBuilder value = new StringBuilder();
|
||||
for (String v : excelProperty.value()) {
|
||||
value.append(v);
|
||||
}
|
||||
excelHeadToFieldNameDic.put(value.toString(), field.getName());
|
||||
// 检查是否必有的头部信息
|
||||
if (field.getAnnotation(NotRequired.class) != null) {
|
||||
result.add(value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void formatHeadMap() {
|
||||
for (Integer key : headMap.keySet()) {
|
||||
String name = headMap.get(key);
|
||||
if (excelHeadToFieldNameDic.containsKey(name)) {
|
||||
headMap.put(key, excelHeadToFieldNameDic.get(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<ExcelErrData<FunctionalCaseExcelData>> getErrList() {
|
||||
return errList;
|
||||
}
|
||||
|
||||
public Integer getSuccessCount() {
|
||||
return successCount.get();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package io.metersphere.functional.excel.listener;
|
||||
|
||||
import com.alibaba.excel.context.AnalysisContext;
|
||||
import com.alibaba.excel.enums.CellExtraTypeEnum;
|
||||
import com.alibaba.excel.event.AnalysisEventListener;
|
||||
import com.alibaba.excel.metadata.CellExtra;
|
||||
import io.metersphere.functional.excel.domain.ExcelMergeInfo;
|
||||
import io.metersphere.sdk.util.BeanUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 数据预处理,读取合并的单元格信息
|
||||
* @author wx
|
||||
*/
|
||||
public class FunctionalCasePretreatmentListener extends AnalysisEventListener {
|
||||
|
||||
Set<ExcelMergeInfo> mergeInfoSet;
|
||||
private Integer lastRowIndex = 0;
|
||||
Map<Integer, Integer> emptyRowIndexMap = new HashMap<>();
|
||||
|
||||
public FunctionalCasePretreatmentListener(Set<ExcelMergeInfo> mergeInfoSet) {
|
||||
this.mergeInfoSet = mergeInfoSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(Object integerStringMap, AnalysisContext analysisContext) {
|
||||
Integer rowIndex = analysisContext.readRowHolder().getRowIndex();
|
||||
if (rowIndex - lastRowIndex > 1) {
|
||||
// 记录空行的行号
|
||||
for (int i = lastRowIndex + 1; i < rowIndex; i++) {
|
||||
emptyRowIndexMap.put(i, lastRowIndex);
|
||||
}
|
||||
}
|
||||
this.lastRowIndex = rowIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extra(CellExtra extra, AnalysisContext context) {
|
||||
if (extra.getType() == CellExtraTypeEnum.MERGE) {
|
||||
// 将合并单元格信息保留
|
||||
ExcelMergeInfo mergeInfo = new ExcelMergeInfo();
|
||||
BeanUtils.copyBean(mergeInfo, extra);
|
||||
if (emptyRowIndexMap.keySet().contains(mergeInfo.getLastRowIndex())) {
|
||||
// 如果合并单元格的最后一行是空行,则将最后一行设置成非空的行
|
||||
mergeInfo.setLastRowIndex(emptyRowIndexMap.get(mergeInfo.getLastRowIndex()));
|
||||
}
|
||||
this.mergeInfoSet.add(mergeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void doAfterAllAnalysed(AnalysisContext analysisContext) {}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package io.metersphere.functional.excel.validate;
|
||||
|
||||
|
||||
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
|
||||
import io.metersphere.sdk.util.JSON;
|
||||
import io.metersphere.sdk.util.LogUtils;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public abstract class AbstractCustomFieldValidator {
|
||||
|
||||
/**
|
||||
* 标记是否是键值对的选项
|
||||
* 需要校验时可以填键也可以填值
|
||||
*/
|
||||
public Boolean isKVOption = false;
|
||||
|
||||
/**
|
||||
* 校验参数是否合法
|
||||
*
|
||||
* @param customField
|
||||
* @param value
|
||||
*/
|
||||
abstract public void validate(TemplateCustomFieldDTO customField, String value) throws CustomFieldValidateException;
|
||||
|
||||
|
||||
/**
|
||||
* 将选项的值转化为对应的key
|
||||
*
|
||||
* @param keyOrValue
|
||||
* @return
|
||||
*/
|
||||
public Object parse2Key(String keyOrValue, TemplateCustomFieldDTO customField) {
|
||||
return keyOrValue;
|
||||
}
|
||||
|
||||
protected void validateRequired(TemplateCustomFieldDTO customField, String value) throws CustomFieldValidateException {
|
||||
if (customField.getRequired() && StringUtils.isBlank(value)) {
|
||||
CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_required_tip"), customField.getFieldName()));
|
||||
}
|
||||
}
|
||||
|
||||
protected void validateArrayRequired(TemplateCustomFieldDTO customField, String value) throws CustomFieldValidateException {
|
||||
if (customField.getRequired() && (StringUtils.isBlank(value) || StringUtils.equals(value, "[]"))) {
|
||||
CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_required_tip"), customField.getFieldId()));
|
||||
}
|
||||
}
|
||||
|
||||
protected List<String> parse2Array(String name, String value) throws CustomFieldValidateException {
|
||||
try {
|
||||
// [a, b] => ["a","b"]
|
||||
if (!StringUtils.equals(value, "[]")) {
|
||||
if (!value.contains("[\"")) {
|
||||
value = value.replace("[", "[\"");
|
||||
}
|
||||
if (!value.contains("\"]")) {
|
||||
value = value.replace("]", "\"]");
|
||||
|
||||
}
|
||||
if (!value.contains("\",\"")) {
|
||||
value = value.replace(",", "\",\"");
|
||||
|
||||
}
|
||||
if (!value.contains("\",\"")) {
|
||||
value = value.replace(",", "\",\"");
|
||||
}
|
||||
value = value.replace(StringUtils.SPACE, StringUtils.EMPTY);
|
||||
}
|
||||
return JSON.parseArray(value, String.class);
|
||||
} catch (Exception e) {
|
||||
CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_array_tip"), name));
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
protected List<String> parse2Array(String value) {
|
||||
try {
|
||||
return parse2Array(null, value);
|
||||
} catch (CustomFieldValidateException e) {
|
||||
LogUtils.error(e);
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package io.metersphere.functional.excel.validate;
|
||||
|
||||
|
||||
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
|
||||
import io.metersphere.sdk.util.DateUtils;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class CustomFieldDateTimeValidator extends AbstractCustomFieldValidator {
|
||||
|
||||
@Override
|
||||
public void validate(TemplateCustomFieldDTO customField, String value) throws CustomFieldValidateException {
|
||||
validateRequired(customField, value);
|
||||
try {
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
DateUtils.getTime(value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_datetime_tip"), customField.getFieldName(), DateUtils.TIME_PATTERN));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package io.metersphere.functional.excel.validate;
|
||||
|
||||
|
||||
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
|
||||
import io.metersphere.sdk.util.DateUtils;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class CustomFieldDateValidator extends AbstractCustomFieldValidator {
|
||||
|
||||
public void validate(TemplateCustomFieldDTO customField, String value) throws CustomFieldValidateException {
|
||||
validateRequired(customField, value);
|
||||
try {
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
DateUtils.getDate(value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_date_tip"), customField.getFieldName(), DateUtils.DATE_PATTERN));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package io.metersphere.functional.excel.validate;
|
||||
|
||||
|
||||
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class CustomFieldFloatValidator extends AbstractCustomFieldValidator {
|
||||
|
||||
public void validate(TemplateCustomFieldDTO customField, String value) throws CustomFieldValidateException {
|
||||
validateRequired(customField, value);
|
||||
try {
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
Float.parseFloat(value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_float_tip"), customField.getFieldName()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package io.metersphere.functional.excel.validate;
|
||||
|
||||
|
||||
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class CustomFieldIntegerValidator extends AbstractCustomFieldValidator {
|
||||
|
||||
public void validate(TemplateCustomFieldDTO customField, String value) throws CustomFieldValidateException {
|
||||
validateRequired(customField, value);
|
||||
try {
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
Integer.parseInt(value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_int_tip"), customField.getFieldName()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package io.metersphere.functional.excel.validate;
|
||||
|
||||
|
||||
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
|
||||
import io.metersphere.project.service.ProjectApplicationService;
|
||||
import io.metersphere.sdk.util.CommonBeanFactory;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import io.metersphere.system.domain.User;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
import io.metersphere.system.utils.SessionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class CustomFieldMemberValidator extends AbstractCustomFieldValidator {
|
||||
|
||||
protected Map<String, String> userIdMap;
|
||||
protected Map<String, String> userNameMap;
|
||||
|
||||
public CustomFieldMemberValidator() {
|
||||
this.isKVOption = true;
|
||||
ProjectApplicationService projectApplicationService = CommonBeanFactory.getBean(ProjectApplicationService.class);
|
||||
List<User> memberOption = projectApplicationService.getProjectUserList(SessionUtils.getCurrentProjectId());
|
||||
userIdMap = memberOption.stream()
|
||||
.collect(
|
||||
Collectors.toMap(user -> user.getId().toLowerCase(), User::getId)
|
||||
);
|
||||
userNameMap = new HashMap<>();
|
||||
memberOption.stream()
|
||||
.forEach(user -> userNameMap.put(user.getName().toLowerCase(), user.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(TemplateCustomFieldDTO customField, String value) throws CustomFieldValidateException {
|
||||
validateRequired(customField, value);
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return;
|
||||
}
|
||||
value = value.toLowerCase();
|
||||
if (userIdMap.containsKey(value) || userNameMap.containsKey(value)) {
|
||||
return;
|
||||
}
|
||||
throw new CustomFieldValidateException(String.format(Translator.get("custom_field_member_tip"), customField.getFieldName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parse2Key(String keyOrValue, TemplateCustomFieldDTO customField) {
|
||||
keyOrValue = keyOrValue.toLowerCase();
|
||||
if (userIdMap.containsKey(keyOrValue)) {
|
||||
return userIdMap.get(keyOrValue);
|
||||
}
|
||||
if (userNameMap.containsKey(keyOrValue)) {
|
||||
return userNameMap.get(keyOrValue);
|
||||
}
|
||||
return keyOrValue;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package io.metersphere.functional.excel.validate;
|
||||
|
||||
|
||||
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class CustomFieldMultipleMemberValidator extends CustomFieldMemberValidator {
|
||||
|
||||
@Override
|
||||
public void validate(TemplateCustomFieldDTO customField, String value) throws CustomFieldValidateException {
|
||||
validateArrayRequired(customField, value);
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String item : parse2Array(customField.getFieldName(), value)) {
|
||||
item = item.toLowerCase();
|
||||
if (!userIdMap.containsKey(item) && !userNameMap.containsKey(item)) {
|
||||
CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_member_tip"), customField.getFieldName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parse2Key(String keyOrValuesStr, TemplateCustomFieldDTO customField) {
|
||||
if (StringUtils.isBlank(keyOrValuesStr)) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
List<String> keyOrValues = parse2Array(keyOrValuesStr);
|
||||
|
||||
for (int i = 0; i < keyOrValues.size(); i++) {
|
||||
String item = keyOrValues.get(i).toLowerCase();
|
||||
if (userIdMap.containsKey(item)) {
|
||||
keyOrValues.set(i, userIdMap.get(item));
|
||||
}
|
||||
if (userNameMap.containsKey(item)) {
|
||||
keyOrValues.set(i, userNameMap.get(item));
|
||||
}
|
||||
}
|
||||
return keyOrValues;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package io.metersphere.functional.excel.validate;
|
||||
|
||||
|
||||
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class CustomFieldMultipleSelectValidator extends CustomFieldSelectValidator {
|
||||
|
||||
@Override
|
||||
public void validate(TemplateCustomFieldDTO customField, String value) throws CustomFieldValidateException {
|
||||
validateArrayRequired(customField, value);
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return;
|
||||
}
|
||||
prepareCache(customField);
|
||||
Set<String> idSet = optionValueSetCache.get(customField.getFieldId());
|
||||
Set<String> textSet = optionTextSetCache.get(customField.getFieldId());
|
||||
for (String item : parse2Array(customField.getFieldName(), value)) {
|
||||
if (!idSet.contains(item) && !textSet.contains(item)) {
|
||||
CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_select_tip"), customField.getFieldName(), textSet));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parse2Key(String keyOrValuesStr, TemplateCustomFieldDTO customField) {
|
||||
if (StringUtils.isBlank(keyOrValuesStr)) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
List<String> keyOrValues = parse2Array(keyOrValuesStr);
|
||||
Map<String, String> nameMap = optionTextMapCache.get(customField.getFieldId());
|
||||
for (int i = 0; i < keyOrValues.size(); i++) {
|
||||
String item = keyOrValues.get(i);
|
||||
if (nameMap.containsKey(item)) {
|
||||
keyOrValues.set(i, nameMap.get(item));
|
||||
}
|
||||
}
|
||||
return keyOrValues;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package io.metersphere.functional.excel.validate;
|
||||
|
||||
|
||||
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class CustomFieldMultipleTextValidator extends AbstractCustomFieldValidator {
|
||||
|
||||
public CustomFieldMultipleTextValidator() {
|
||||
this.isKVOption = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(TemplateCustomFieldDTO customField, String value) throws CustomFieldValidateException {
|
||||
validateRequired(customField, value);
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
try {
|
||||
parse2Array(customField.getFieldName(), value);
|
||||
} catch (Exception e) {
|
||||
CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_array_tip"), customField.getFieldName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parse2Key(String keyOrValuesStr, TemplateCustomFieldDTO customField) {
|
||||
if (StringUtils.isBlank(keyOrValuesStr)) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
return parse2Array(keyOrValuesStr);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package io.metersphere.functional.excel.validate;
|
||||
|
||||
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
|
||||
import io.metersphere.sdk.util.LogUtils;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import io.metersphere.system.domain.CustomFieldOption;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
import org.apache.commons.collections.MapUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class CustomFieldSelectValidator extends AbstractCustomFieldValidator {
|
||||
|
||||
/**
|
||||
* 缓存每个字段对应的选项值
|
||||
*/
|
||||
Map<String, List<CustomFieldOption>> optionCache = new HashMap<>();
|
||||
Map<String, Set<String>> optionValueSetCache = new HashMap<>();
|
||||
Map<String, Set<String>> optionTextSetCache = new HashMap<>();
|
||||
Map<String, Map<String, String>> optionTextMapCache = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 保存系统字段中选项翻译后的值
|
||||
* key 为字段名称,value 为选项value,和选项值的映射
|
||||
*/
|
||||
Map<String, Map<String, String>> i18nMap = new HashMap<>();
|
||||
|
||||
public CustomFieldSelectValidator() {
|
||||
this.isKVOption = true;
|
||||
}
|
||||
|
||||
public void validate(TemplateCustomFieldDTO customField, String value) throws CustomFieldValidateException {
|
||||
validateRequired(customField, value);
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return;
|
||||
}
|
||||
prepareCache(customField);
|
||||
Set<String> idSet = optionValueSetCache.get(customField.getFieldId());
|
||||
Set<String> textSet = optionTextSetCache.get(customField.getFieldId());
|
||||
if (!idSet.contains(value) && !textSet.contains(value)) {
|
||||
CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_select_tip"), customField.getFieldName(), textSet));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parse2Key(String keyOrValuesStr, TemplateCustomFieldDTO customField) {
|
||||
Map<String, String> textMap = optionTextMapCache.get(customField.getFieldId());
|
||||
if (MapUtils.isNotEmpty(textMap) && textMap.containsKey(keyOrValuesStr)) {
|
||||
return textMap.get(keyOrValuesStr);
|
||||
}
|
||||
return keyOrValuesStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自定义字段的选项值和key
|
||||
* 存储到缓存中,增强导入时性能
|
||||
*
|
||||
* @param customField
|
||||
*/
|
||||
protected void prepareCache(TemplateCustomFieldDTO customField) {
|
||||
if (optionValueSetCache.get(customField.getFieldId()) == null) {
|
||||
List<CustomFieldOption> options = getOptions(customField.getFieldId(), customField.getOptions());
|
||||
translateSystemOption(customField, options);
|
||||
|
||||
optionValueSetCache.put(customField.getFieldId(), getIdSet(options));
|
||||
optionTextSetCache.put(customField.getFieldId(), getNameSet(options));
|
||||
optionTextMapCache.put(customField.getFieldId(), getTextMap(options));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译系统字段的选项名称
|
||||
*
|
||||
* @param customField
|
||||
* @param options
|
||||
*/
|
||||
private void translateSystemOption(TemplateCustomFieldDTO customField, List<CustomFieldOption> options) {
|
||||
Map<String, String> fieldI18nMap = i18nMap.get(customField.getFieldName());
|
||||
// 不为空,说明需要翻译
|
||||
if (fieldI18nMap != null) {
|
||||
Iterator<CustomFieldOption> iterator = options.iterator();
|
||||
// 替换成翻译后的值
|
||||
while (iterator.hasNext()) {
|
||||
CustomFieldOption option = iterator.next();
|
||||
if (option.getInternal() && fieldI18nMap.keySet().contains(option.getValue())) {
|
||||
option.setText(fieldI18nMap.get(option.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<String, String> getTextMap(List<CustomFieldOption> options) {
|
||||
HashMap<String, String> textMap = new HashMap<>();
|
||||
options.forEach(item -> textMap.put(item.getText(), item.getValue()));
|
||||
return textMap;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected Set<String> getNameSet(List<CustomFieldOption> options) {
|
||||
return options.stream()
|
||||
.map(CustomFieldOption::getText)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected Set<String> getIdSet(List<CustomFieldOption> options) {
|
||||
return options.stream()
|
||||
.map(CustomFieldOption::getValue)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
protected List<CustomFieldOption> getOptions(String id, List<CustomFieldOption> customFieldOptions) {
|
||||
List<CustomFieldOption> options = optionCache.get(id);
|
||||
if (options != null) {
|
||||
return options;
|
||||
}
|
||||
try {
|
||||
return customFieldOptions;
|
||||
} catch (Exception e) {
|
||||
LogUtils.error(e);
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package io.metersphere.functional.excel.validate;
|
||||
|
||||
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class CustomFieldTextValidator extends AbstractCustomFieldValidator {
|
||||
|
||||
public void validate(TemplateCustomFieldDTO customField, String value) throws CustomFieldValidateException {
|
||||
validateRequired(customField, value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package io.metersphere.functional.excel.validate;
|
||||
|
||||
|
||||
import io.metersphere.sdk.constants.CustomFieldType;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class CustomFieldValidatorFactory {
|
||||
|
||||
private static final HashMap<String, AbstractCustomFieldValidator> validatorMap = new HashMap<>();
|
||||
public static HashMap<String, AbstractCustomFieldValidator> getValidatorMap() {
|
||||
validatorMap.put(CustomFieldType.SELECT.name(), new CustomFieldSelectValidator());
|
||||
validatorMap.put(CustomFieldType.SELECT.name(), new CustomFieldSelectValidator());
|
||||
validatorMap.put(CustomFieldType.RADIO.name(), new CustomFieldSelectValidator());
|
||||
|
||||
validatorMap.put(CustomFieldType.MULTIPLE_SELECT.name(), new CustomFieldMultipleSelectValidator());
|
||||
validatorMap.put(CustomFieldType.CHECKBOX.name(), new CustomFieldMultipleSelectValidator());
|
||||
|
||||
validatorMap.put(CustomFieldType.INPUT.name(), new CustomFieldTextValidator());
|
||||
validatorMap.put(CustomFieldType.TEXTAREA.name(), new CustomFieldTextValidator());
|
||||
|
||||
validatorMap.put(CustomFieldType.MULTIPLE_INPUT.name(), new CustomFieldMultipleTextValidator());
|
||||
|
||||
validatorMap.put(CustomFieldType.DATE.name(), new CustomFieldDateValidator());
|
||||
validatorMap.put(CustomFieldType.DATETIME.name(), new CustomFieldDateTimeValidator());
|
||||
|
||||
validatorMap.put(CustomFieldType.MEMBER.name(), new CustomFieldMemberValidator());
|
||||
validatorMap.put(CustomFieldType.MULTIPLE_MEMBER.name(), new CustomFieldMultipleMemberValidator());
|
||||
|
||||
validatorMap.put(CustomFieldType.INT.name(), new CustomFieldIntegerValidator());
|
||||
validatorMap.put(CustomFieldType.FLOAT.name(), new CustomFieldFloatValidator());
|
||||
return validatorMap;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package io.metersphere.functional.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
@Data
|
||||
public class FunctionalCaseImportRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "{functional_case.project_id.not_blank}")
|
||||
private String projectId;
|
||||
|
||||
|
||||
@Schema(description = "版本ID")
|
||||
private String versionId;
|
||||
|
||||
@Schema(description = "是否覆盖原用例", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "{is_null}")
|
||||
private boolean cover;
|
||||
}
|
|
@ -1,11 +1,21 @@
|
|||
package io.metersphere.functional.service;
|
||||
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.alibaba.excel.EasyExcelFactory;
|
||||
import com.alibaba.excel.enums.CellExtraTypeEnum;
|
||||
import io.metersphere.functional.dto.response.FunctionalCaseImportResponse;
|
||||
import io.metersphere.functional.excel.constants.FunctionalCaseImportFiled;
|
||||
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.handler.FunctionCaseTemplateWriteHandler;
|
||||
import io.metersphere.functional.excel.listener.FunctionalCaseCheckEventListener;
|
||||
import io.metersphere.functional.excel.listener.FunctionalCasePretreatmentListener;
|
||||
import io.metersphere.functional.request.FunctionalCaseImportRequest;
|
||||
import io.metersphere.plugin.sdk.util.MSPluginException;
|
||||
import io.metersphere.project.service.ProjectTemplateService;
|
||||
import io.metersphere.sdk.constants.TemplateScene;
|
||||
import io.metersphere.sdk.util.LogUtils;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import io.metersphere.system.domain.CustomFieldOption;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
|
@ -16,6 +26,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -40,8 +51,7 @@ public class FunctionalCaseFileService {
|
|||
*/
|
||||
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<>());
|
||||
List<TemplateCustomFieldDTO> customFields = getCustomFields(projectId);
|
||||
|
||||
//获取表头字段 当前项目下默认模板的自定义字段 heads:默认表头名称+自定义字段名称
|
||||
List<List<String>> heads = getTemplateHead(projectId, customFields);
|
||||
|
@ -115,7 +125,7 @@ public class FunctionalCaseFileService {
|
|||
for (int i = 1; i <= 4; i++) {
|
||||
path.append("/" + Translator.get("module") + i);
|
||||
FunctionalCaseExcelData testCaseDTO = new FunctionalCaseExcelData();
|
||||
testCaseDTO.setId(StringUtils.EMPTY);
|
||||
testCaseDTO.setNum(StringUtils.EMPTY);
|
||||
testCaseDTO.setName(Translator.get("test_case") + i);
|
||||
testCaseDTO.setModule(path.toString());
|
||||
testCaseDTO.setPrerequisite(Translator.get("test_case_prerequisite"));
|
||||
|
@ -164,4 +174,48 @@ public class FunctionalCaseFileService {
|
|||
List<List<String>> heads = new FunctionalCaseExcelDataFactory().getFunctionalCaseExcelDataLocal().getHead(customFields);
|
||||
return heads;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 导入前校验excel模板
|
||||
*
|
||||
* @param request
|
||||
* @param file
|
||||
* @return
|
||||
*/
|
||||
public FunctionalCaseImportResponse preCheckExcel(FunctionalCaseImportRequest request, MultipartFile file) {
|
||||
if (file == null) {
|
||||
throw new MSPluginException("file_cannot_be_null");
|
||||
}
|
||||
FunctionalCaseImportResponse response = new FunctionalCaseImportResponse();
|
||||
checkImportExcel(response, request, file);
|
||||
return response;
|
||||
}
|
||||
|
||||
private void checkImportExcel(FunctionalCaseImportResponse response, FunctionalCaseImportRequest request, MultipartFile file) {
|
||||
try {
|
||||
//根据本地语言环境选择用哪种数据对象进行存放读取的数据
|
||||
Class clazz = new FunctionalCaseExcelDataFactory().getExcelDataByLocal();
|
||||
//获取当前项目默认模板的自定义字段
|
||||
List<TemplateCustomFieldDTO> customFields = getCustomFields(request.getProjectId());
|
||||
Set<ExcelMergeInfo> mergeInfoSet = new TreeSet<>();
|
||||
// 预处理,查询合并单元格信息
|
||||
EasyExcel.read(file.getInputStream(), null, new FunctionalCasePretreatmentListener(mergeInfoSet))
|
||||
.extraRead(CellExtraTypeEnum.MERGE).sheet().doRead();
|
||||
FunctionalCaseCheckEventListener eventListener = new FunctionalCaseCheckEventListener(request, clazz, customFields, mergeInfoSet);
|
||||
EasyExcelFactory.read(file.getInputStream(), eventListener).sheet().doRead();
|
||||
response.setErrorMessages(eventListener.getErrList());
|
||||
response.setSuccessCount(eventListener.getSuccessCount());
|
||||
response.setFailCount(eventListener.getErrList().size());
|
||||
} catch (Exception e) {
|
||||
LogUtils.error("checkImportExcel error", e);
|
||||
throw new MSPluginException("checkImportExcel error");
|
||||
}
|
||||
}
|
||||
|
||||
private List<TemplateCustomFieldDTO> getCustomFields(String projectId) {
|
||||
TemplateDTO defaultTemplateDTO = projectTemplateService.getDefaultTemplateDTO(projectId, TemplateScene.FUNCTIONAL.name());
|
||||
List<TemplateCustomFieldDTO> customFields = Optional.ofNullable(defaultTemplateDTO.getCustomFields()).orElse(new ArrayList<>());
|
||||
return customFields;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import io.metersphere.functional.domain.FunctionalCase;
|
|||
import io.metersphere.functional.domain.FunctionalCaseCustomField;
|
||||
import io.metersphere.functional.dto.CaseCustomFieldDTO;
|
||||
import io.metersphere.functional.dto.FunctionalCasePageDTO;
|
||||
import io.metersphere.functional.dto.response.FunctionalCaseImportResponse;
|
||||
import io.metersphere.functional.mapper.FunctionalCaseCustomFieldMapper;
|
||||
import io.metersphere.functional.request.*;
|
||||
import io.metersphere.functional.result.CaseManagementResultCode;
|
||||
|
@ -65,6 +66,7 @@ public class FunctionalCaseControllerTests extends BaseTest {
|
|||
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/";
|
||||
public static final String CHECK_EXCEL_URL = "/functional/case/pre-check/excel";
|
||||
|
||||
@Resource
|
||||
private NotificationMapper notificationMapper;
|
||||
|
@ -496,4 +498,37 @@ public class FunctionalCaseControllerTests extends BaseTest {
|
|||
.header(SessionConstants.CSRF_TOKEN, csrfToken))
|
||||
.andExpect(status().isOk()).andReturn();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@Order(20)
|
||||
public void testImportCheckExcel() throws Exception {
|
||||
String filePath = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/1.xlsx")).getPath();
|
||||
MockMultipartFile file = new MockMultipartFile("file", "11.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath));
|
||||
FunctionalCaseImportRequest request = new FunctionalCaseImportRequest();
|
||||
request.setCover(false);
|
||||
request.setProjectId("100001100001");
|
||||
LinkedMultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
|
||||
paramMap.add("request", JSON.toJSONString(request));
|
||||
paramMap.add("file", file);
|
||||
MvcResult functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(CHECK_EXCEL_URL,paramMap);
|
||||
|
||||
String functionalCaseImportResponseData = functionalCaseMvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||
ResultHolder functionalCaseResultHolder = JSON.parseObject(functionalCaseImportResponseData, ResultHolder.class);
|
||||
FunctionalCaseImportResponse functionalCaseImportResponse = JSON.parseObject(JSON.toJSONString(functionalCaseResultHolder.getData()), FunctionalCaseImportResponse.class);
|
||||
Assertions.assertNotNull(functionalCaseImportResponse);
|
||||
|
||||
|
||||
paramMap = new LinkedMultiValueMap<>();
|
||||
paramMap.add("request", JSON.toJSONString(request));
|
||||
this.requestMultipart(CHECK_EXCEL_URL,paramMap);
|
||||
|
||||
//覆盖异常
|
||||
String filePath2 = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/2.xlsx")).getPath();
|
||||
MockMultipartFile file2 = new MockMultipartFile("file", "22.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath2));
|
||||
paramMap = new LinkedMultiValueMap<>();
|
||||
paramMap.add("request", JSON.toJSONString(request));
|
||||
paramMap.add("file", file2);
|
||||
this.requestMultipart(CHECK_EXCEL_URL,paramMap);
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,21 @@
|
|||
package io.metersphere.system.excel.domain;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ExcelErrData<T> {
|
||||
|
||||
|
||||
|
||||
private Integer rowNum;
|
||||
|
||||
private String errMsg;
|
||||
|
||||
public ExcelErrData() {
|
||||
}
|
||||
|
||||
public ExcelErrData(Integer rowNum, String errMsg) {
|
||||
this.rowNum = rowNum;
|
||||
this.errMsg = errMsg;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue