feat(功能用例): 功能用例导入检查

This commit is contained in:
WangXu10 2024-01-18 15:05:36 +08:00 committed by 刘瑞斌
parent f2df25ba0a
commit 5570d2ba17
30 changed files with 1364 additions and 8 deletions

View File

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

View File

@ -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}之间

View File

@ -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不能為空

View File

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

View 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;
}

View File

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

View File

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

View File

@ -33,7 +33,7 @@ public class FunctionalCaseExcelDataCn extends FunctionalCaseExcelData {
@Length(max = 50)
@ExcelProperty("所属模块")
@ColumnWidth(30)
private String moduleId;
private String module;
@ColumnWidth(50)
@ExcelProperty("标签")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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