feat(功能用例): 功能用例excel导入

This commit is contained in:
WangXu10 2024-01-23 21:02:43 +08:00 committed by 刘瑞斌
parent ab87dcca8a
commit 99d294694c
21 changed files with 1127 additions and 70 deletions

View File

@ -520,7 +520,7 @@ swagger_parse_error=Swagger parsing failed or file format is incorrect!
permission.test_plan.name=Test plan
permission.test_plan_module.name=Test plan module
excel.template.id=Not mandatory, add a new use case when the ID is empty
excel.template.id=Non mandatory, add a new use case when the ID is empty or does not exist;
excel.template.case_edit_type=Not mandatory, fill in STEP for step description, fill in Text for text description, default to Text if not filled in
excel.template.tag=Not mandatory labels should be separated by semicolons or commas
excel.template.text_description=Not mandatory, when the editing mode is STEP, the step description will be based on the identifier [1] [2] [3] To determine whether to split a cell into multiple steps, if not, it is a single step
@ -536,7 +536,7 @@ 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
check_import_excel_error=Check import excel error
#关联
relate_source_id_not_blank=Source id cannot be empty
relate_source_id_length_range=The association source ID must be between {min} and {max}

View File

@ -516,7 +516,7 @@ swagger_parse_error=Swagger 解析失败,请确认文件格式是否正确!
permission.test_plan.name=测试计划
permission.test_plan_module.name=测试计划模块
excel.template.id=非必填ID为空时新增用例
excel.template.id=非必填ID为空或不存在时新增用例
excel.template.case_edit_type=非必填步骤描述填写STEP文本描述填写TEXT未填写默认为TEXT
excel.template.tag=非必填,标签之间以分号或者逗号隔开
excel.template.text_description=非必填编辑模式为STEP时步骤描述会根据标识[1] [2] [3]...来判断是否将单元格拆分为多个步骤,没有则为一个步骤
@ -532,6 +532,7 @@ custom_field_member_tip=[%s]必须当前项目成员
custom_field_select_tip=[%s]必须为%s
custom_field_int_tip=[%s]必须为整型
custom_field_float_tip=[%s]必须为数字
check_import_excel_error=检查导入Excel错误
#关联
relate_source_id_not_blank=关联来源ID不能为空
relate_source_id_length_range=关联来源ID必须在{min}和{max}之间

View File

@ -516,7 +516,7 @@ swagger_parse_error=Swagger 解析失敗
permission.test_plan.name=測試計劃
permission.test_plan_module.name=測試計劃模塊
excel.template.id=非必填ID為空時新增用例
excel.template.id=非必填ID為空時或不存在時新增用例
excel.template.case_edit_type=非必填步驟描述填寫STEP文本描述填寫TEXT為填寫默認為TEXT
excel.template.tag=非必填,標簽之間以分號或者逗號隔開
excel.template.text_description=非必填編輯模式為STEP時步驟描述會根據標識[1] [2] [3]...來判斷是否將單元格拆分為多個步驟,沒有則為一個步驟
@ -532,7 +532,7 @@ custom_field_member_tip=[%s]必須當前項目成員
custom_field_select_tip=[%s]必須為%s
custom_field_int_tip=[%s]必須為整型
custom_field_float_tip=[%s]必須為數字
check_import_excel_error=檢查導入Excel錯誤
#关联
relate_source_id_not_blank=關聯來源ID不能為空
relate_source_id_length_range=關聯來源ID必須在{min}和{max}之間

View File

@ -0,0 +1,11 @@
package io.metersphere.functional.constants;
/**
* @author wx
*/
public class FunctionalCaseTypeConstants {
public enum CaseEditType {
TEXT, STEP
}
}

View File

@ -226,4 +226,13 @@ public class FunctionalCaseController {
public FunctionalCaseImportResponse preCheckExcel(@RequestPart("request") FunctionalCaseImportRequest request, @RequestPart(value = "file", required = false) MultipartFile file) {
return functionalCaseFileService.preCheckExcel(request, file);
}
@PostMapping("/import/excel")
@Operation(summary = "用例管理-功能用例-excel导入")
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_UPDATE)
public FunctionalCaseImportResponse importExcel(@RequestPart("request") FunctionalCaseImportRequest request, @RequestPart(value = "file", required = false) MultipartFile file) {
String userId = SessionUtils.getUserId();
return functionalCaseFileService.importExcel(request, userId, file);
}
}

View File

@ -33,13 +33,13 @@ public class FunctionalCaseExcelData {
private String expectedResult;
@ExcelIgnore
private String caseEditType;
@ExcelIgnore
private String steps;
@ExcelIgnore
Map<String, Object> customData = new LinkedHashMap<>();
@ExcelIgnore
Map<String, String> otherFields;
@ExcelIgnore
Set<String> textFieldSet = new HashSet<>(1);
/**
* 合并文本描述
*/

View File

@ -11,7 +11,7 @@ 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.exception.MSException;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.dto.excel.ExcelValidateHelper;
@ -23,7 +23,6 @@ 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;
/**
@ -57,7 +56,6 @@ public class FunctionalCaseCheckEventListener extends AnalysisEventListener<Map<
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) {
@ -85,7 +83,7 @@ public class FunctionalCaseCheckEventListener extends AnalysisEventListener<Map<
@Override
public void invoke(Map<Integer, String> data, AnalysisContext analysisContext) {
if (headMap == null) {
throw new MSPluginException("case_import_table_header_missing");
throw new MSException(Translator.get("case_import_table_header_missing"));
}
Integer rowIndex = analysisContext.readRowHolder().getRowIndex();
//处理合并单元格
@ -192,7 +190,7 @@ public class FunctionalCaseCheckEventListener extends AnalysisEventListener<Map<
errList.add(excelErrData);
} else {
//通过数量
successCount.incrementAndGet();
list.add(functionalCaseExcelData);
}
}
@ -221,7 +219,11 @@ public class FunctionalCaseCheckEventListener extends AnalysisEventListener<Map<
//当前读取的数据有ID
if (StringUtils.isNotEmpty(data.getNum())) {
Integer num = -1;
try {
num = Integer.parseInt(data.getNum());
} catch (Exception e) {
return;
}
if (num < 0) {
errMsg.append(Translator.get("id_not_rightful"))
.append("[")
@ -350,7 +352,7 @@ public class FunctionalCaseCheckEventListener extends AnalysisEventListener<Map<
isMergeRow = false;
isMergeLastRow = false;
if (getNameColIndex() == null) {
throw new MSPluginException("缺少名称表头");
throw new MSException(Translator.get("case_import_table_header_missing"));
}
data.keySet().forEach(col -> {
Iterator<ExcelMergeInfo> iterator = mergeInfoSet.iterator();
@ -451,7 +453,7 @@ public class FunctionalCaseCheckEventListener extends AnalysisEventListener<Map<
return errList;
}
public Integer getSuccessCount() {
return successCount.get();
public List<FunctionalCaseExcelData> getList() {
return list;
}
}

View File

@ -0,0 +1,657 @@
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.constants.FunctionalCaseTypeConstants;
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.functional.service.FunctionalCaseModuleService;
import io.metersphere.functional.service.FunctionalCaseService;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.CommonBeanFactory;
import io.metersphere.sdk.util.JSON;
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.BaseTreeNode;
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
import io.metersphere.system.excel.domain.ExcelErrData;
import org.apache.commons.collections.CollectionUtils;
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.stream.Collectors;
/**
* @author wx
*/
public class FunctionalCaseImportEventListener extends AnalysisEventListener<Map<Integer, String>> {
private Class excelDataClass;
private FunctionalCaseImportRequest request;
private Map<Integer, String> headMap;
Map<String, TemplateCustomFieldDTO> customFieldsMap = new HashMap<>();
private Set<ExcelMergeInfo> mergeInfoSet;
/**
* 所有的模块集合
*/
private List<BaseTreeNode> moduleTree;
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;
/**
* 每隔5000条存储数据库然后清理list 方便内存回收
*/
protected static final int BATCH_COUNT = 5000;
protected List<FunctionalCaseExcelData> list = new ArrayList<>();
protected List<ExcelErrData<FunctionalCaseExcelData>> errList = new ArrayList<>();
/**
* 待更新用例的集合
*/
protected List<FunctionalCaseExcelData> updateList = new ArrayList<>();
private static final String ERROR_MSG_SEPARATOR = ";";
private HashMap<String, AbstractCustomFieldValidator> customFieldValidatorMap;
private FunctionalCaseService functionalCaseService;
private String userId;
private int successCount = 0;
private Map<String, String> pathMap = new HashMap<>();
public FunctionalCaseImportEventListener(FunctionalCaseImportRequest request, Class clazz, List<TemplateCustomFieldDTO> customFields, Set<ExcelMergeInfo> mergeInfoSet, String userId) {
this.mergeInfoSet = mergeInfoSet;
this.request = request;
excelDataClass = clazz;
//当前项目模板的自定义字段
customFieldsMap = customFields.stream().collect(Collectors.toMap(TemplateCustomFieldDTO::getFieldName, i -> i));
moduleTree = CommonBeanFactory.getBean(FunctionalCaseModuleService.class).getTree(request.getProjectId());
functionalCaseService = CommonBeanFactory.getBean(FunctionalCaseService.class);
customFieldValidatorMap = CustomFieldValidatorFactory.getValidatorMap();
this.userId = userId;
}
@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 MSException(Translator.get("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);
if (list.size() > BATCH_COUNT || updateList.size() > BATCH_COUNT) {
saveData();
this.successCount += list.size() + updateList.size();
list.clear();
updateList.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// 如果文件最后一行是没有内容的步骤这里处理最后一条合并单元格的数据
if (currentMergeData != null) {
buildUpdateOrErrorList(firstMergeRowIndex, currentMergeData);
}
saveData();
this.successCount += list.size() + updateList.size();
list.clear();
updateList.clear();
customFieldsMap.clear();
}
/**
* 执行保存数据
*/
private void saveData() {
if (CollectionUtils.isNotEmpty(list)) {
functionalCaseService.saveImportData(list, request, moduleTree, userId, customFieldsMap, pathMap);
}
if (CollectionUtils.isNotEmpty(updateList)) {
functionalCaseService.updateImportData(updateList, request, moduleTree, userId, customFieldsMap, pathMap);
}
}
/**
* 构建数据
*
* @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.isEmpty(errMsg)) {
//不存在错误信息说明可以新增或更新
handleImportDate(functionalCaseExcelData);
} else {
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);
}
}
/**
* 处理可以导入的数据
*
* @param functionalCaseExcelData
*/
private void handleImportDate(FunctionalCaseExcelData functionalCaseExcelData) {
//处理id判断是新增还是更新
handleId(functionalCaseExcelData);
//处理单元格
handleSteps(functionalCaseExcelData);
}
/**
* 处理步骤描述和预期结果
*
* @param functionalCaseExcelData
*/
private void handleSteps(FunctionalCaseExcelData functionalCaseExcelData) {
if (StringUtils.isNotBlank(functionalCaseExcelData.getCaseEditType()) && StringUtils.equalsIgnoreCase(functionalCaseExcelData.getCaseEditType(), FunctionalCaseTypeConstants.CaseEditType.TEXT.name())) {
functionalCaseExcelData.setTextDescription(functionalCaseExcelData.getTextDescription());
functionalCaseExcelData.setExpectedResult(functionalCaseExcelData.getExpectedResult());
} else {
String steps = getSteps(functionalCaseExcelData);
functionalCaseExcelData.setSteps(steps);
}
}
private String getSteps(FunctionalCaseExcelData data) {
List<Map<String, Object>> steps = new ArrayList<>();
if (CollectionUtils.isNotEmpty(data.getMergeTextDescription()) || CollectionUtils.isNotEmpty(data.getMergeExpectedResult())) {
// 如果是合并单元格则组合多条单元格的数据
for (int i = 0; i < data.getMergeTextDescription().size(); i++) {
List<Map<String, Object>> rowSteps = getSingleRowSteps(data.getMergeTextDescription().get(i), data.getMergeExpectedResult().get(i), steps.size());
steps.addAll(rowSteps);
}
} else {
// 如果不是合并单元格则直接解析单元格数据
steps.addAll(getSingleRowSteps(data.getTextDescription(), data.getExpectedResult(), steps.size()));
}
return JSON.toJSONString(steps);
}
/**
* 解析步骤描述预期结果
*
* @param cellDesc 步骤描述
* @param cellResult 预期结果
* @param startStepIndex 步骤序号
* @return
*/
private List<Map<String, Object>> getSingleRowSteps(String cellDesc, String cellResult, Integer startStepIndex) {
List<Map<String, Object>> steps = new ArrayList<>();
List<String> stepDescList = parseStepCell(cellDesc);
List<String> stepResList = parseStepCell(cellResult);
int index = Math.max(stepDescList.size(), stepResList.size());
for (int i = 0; i < index; i++) {
// 保持插入顺序判断用例是否有相同的steps
Map<String, Object> step = new LinkedHashMap<>();
step.put("num", startStepIndex + i + 1);
if (i < stepDescList.size()) {
step.put("desc", stepDescList.get(i));
} else {
step.put("desc", StringUtils.EMPTY);
}
if (i < stepResList.size()) {
step.put("result", stepResList.get(i));
} else {
step.put("result", StringUtils.EMPTY);
}
steps.add(step);
}
return steps;
}
/**
* 解析步骤类型的单元格内容
*
* @param cellContent 单元格内容
* @return 解析后的字符文本
*/
private List<String> parseStepCell(String cellContent) {
List<String> cellStepContentList = new ArrayList<>();
if (StringUtils.isNotEmpty(cellContent)) {
// 根据[1], [2]...分割步骤描述, 开头空字符去掉, 末尾保留
String[] cellContentArr = cellContent.split("\\[\\d+]", -1);
if (StringUtils.isEmpty(cellContentArr[0])) {
cellContentArr = Arrays.copyOfRange(cellContentArr, 1, cellContentArr.length);
}
for (String stepContent : cellContentArr) {
cellStepContentList.add(stepContent.replaceAll("(?m)^\\s*|\\s*$", StringUtils.EMPTY));
}
} else {
cellStepContentList.add(StringUtils.EMPTY);
}
return cellStepContentList;
}
/**
* 处理新增数据集合还是更新数据集合
*
* @param functionalCaseExcelData
*/
private void handleId(FunctionalCaseExcelData functionalCaseExcelData) {
if (StringUtils.isNotEmpty(functionalCaseExcelData.getNum())) {
String checkResult = functionalCaseService.checkNumExist(functionalCaseExcelData.getNum(), request.getProjectId());
if (StringUtils.isNotEmpty(checkResult)) {
if (request.isCover()) {
//如果是覆盖那么有id的需要更新
functionalCaseExcelData.setNum(checkResult);
updateList.add(functionalCaseExcelData);
}
} else {
list.add(functionalCaseExcelData);
}
} else {
list.add(functionalCaseExcelData);
}
}
/**
* 校验excel中的数据
*
* @param data
* @param errMsg
*/
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;
try {
num = Integer.parseInt(data.getNum());
} catch (Exception e) {
data.setNum(null);
return;
}
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);
String originFieldName = fieldName;
TemplateCustomFieldDTO templateCustomFieldDTO = customFieldsMap.get(fieldName);
if (templateCustomFieldDTO == null) {
continue;
}
AbstractCustomFieldValidator customFieldValidator = customFieldValidatorMap.get(templateCustomFieldDTO.getType());
try {
customFieldValidator.validate(templateCustomFieldDTO, value.toString());
if (customFieldValidator.isKVOption) {
// 这里如果填的是选项值替换成选项ID保存
customData.put(originFieldName, customFieldValidator.parse2Key(value.toString(), templateCustomFieldDTO));
}
} 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 MSException(Translator.get("case_import_table_header_missing"));
}
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 int getSuccessCount() {
return successCount;
}
}

View File

@ -55,25 +55,8 @@ public abstract class AbstractCustomFieldValidator {
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);
//a,b,c => ["a","b","c"]
return JSON.parseArray(JSON.toJSONString(value.split(",")));
} catch (Exception e) {
CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_array_tip"), name));
}

View File

@ -2,6 +2,7 @@ package io.metersphere.functional.excel.validate;
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
import org.apache.commons.lang3.StringUtils;
@ -44,6 +45,6 @@ public class CustomFieldMultipleMemberValidator extends CustomFieldMemberValidat
keyOrValues.set(i, userNameMap.get(item));
}
}
return keyOrValues;
return JSON.toJSONString(keyOrValues);
}
}

View File

@ -2,6 +2,7 @@ package io.metersphere.functional.excel.validate;
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
import org.apache.commons.lang3.StringUtils;
@ -44,6 +45,6 @@ public class CustomFieldMultipleSelectValidator extends CustomFieldSelectValidat
keyOrValues.set(i, nameMap.get(item));
}
}
return keyOrValues;
return JSON.toJSONString(keyOrValues);
}
}

View File

@ -2,10 +2,13 @@ package io.metersphere.functional.excel.validate;
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
/**
* @author wx
*/
@ -32,6 +35,8 @@ public class CustomFieldMultipleTextValidator extends AbstractCustomFieldValidat
if (StringUtils.isBlank(keyOrValuesStr)) {
return StringUtils.EMPTY;
}
return parse2Array(keyOrValuesStr);
List<String> keyOrValues = parse2Array(keyOrValuesStr);
return JSON.toJSONString(keyOrValues);
}
}

View File

@ -10,11 +10,13 @@ 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.FunctionalCaseImportEventListener;
import io.metersphere.functional.excel.listener.FunctionalCasePretreatmentListener;
import io.metersphere.functional.request.FunctionalCaseImportRequest;
import io.metersphere.plugin.sdk.util.MSPluginException;
import io.metersphere.project.mapper.ExtBaseProjectVersionMapper;
import io.metersphere.project.service.ProjectTemplateService;
import io.metersphere.sdk.constants.TemplateScene;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.domain.CustomFieldOption;
@ -41,6 +43,8 @@ public class FunctionalCaseFileService {
@Resource
private ProjectTemplateService projectTemplateService;
@Resource
private ExtBaseProjectVersionMapper extBaseProjectVersionMapper;
/**
@ -185,7 +189,7 @@ public class FunctionalCaseFileService {
*/
public FunctionalCaseImportResponse preCheckExcel(FunctionalCaseImportRequest request, MultipartFile file) {
if (file == null) {
throw new MSPluginException("file_cannot_be_null");
throw new MSException(Translator.get("file_cannot_be_null"));
}
FunctionalCaseImportResponse response = new FunctionalCaseImportResponse();
checkImportExcel(response, request, file);
@ -205,11 +209,11 @@ public class FunctionalCaseFileService {
FunctionalCaseCheckEventListener eventListener = new FunctionalCaseCheckEventListener(request, clazz, customFields, mergeInfoSet);
EasyExcelFactory.read(file.getInputStream(), eventListener).sheet().doRead();
response.setErrorMessages(eventListener.getErrList());
response.setSuccessCount(eventListener.getSuccessCount());
response.setSuccessCount(eventListener.getList().size());
response.setFailCount(eventListener.getErrList().size());
} catch (Exception e) {
LogUtils.error("checkImportExcel error", e);
throw new MSPluginException("checkImportExcel error");
throw new MSException(Translator.get("check_import_excel_error"));
}
}
@ -218,4 +222,42 @@ public class FunctionalCaseFileService {
List<TemplateCustomFieldDTO> customFields = Optional.ofNullable(defaultTemplateDTO.getCustomFields()).orElse(new ArrayList<>());
return customFields;
}
/**
* 导入excel
*
* @param request
* @param userId
* @param file
*/
public FunctionalCaseImportResponse importExcel(FunctionalCaseImportRequest request, String userId, MultipartFile file) {
if (file == null) {
throw new MSException(Translator.get("file_cannot_be_null"));
}
try {
FunctionalCaseImportResponse response = new FunctionalCaseImportResponse();
//设置默认版本
if (StringUtils.isEmpty(request.getVersionId())) {
request.setVersionId(extBaseProjectVersionMapper.getDefaultVersion(request.getProjectId()));
}
//根据本地语言环境选择用哪种数据对象进行存放读取的数据
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();
FunctionalCaseImportEventListener eventListener = new FunctionalCaseImportEventListener(request, clazz, customFields, mergeInfoSet, userId);
EasyExcelFactory.read(file.getInputStream(), eventListener).sheet().doRead();
response.setErrorMessages(eventListener.getErrList());
response.setSuccessCount(eventListener.getSuccessCount());
response.setFailCount(eventListener.getErrList().size());
return response;
} catch (Exception e) {
LogUtils.error("checkImportExcel error", e);
throw new MSException(Translator.get("check_import_excel_error"));
}
}
}

View File

@ -42,10 +42,8 @@ import org.mybatis.spring.SqlSessionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
@Service
@ -176,7 +174,6 @@ public class FunctionalCaseModuleService extends ModuleTreeService {
/**
* 查找当前项目下模块每个节点对应的资源统计
*
*/
public Map<String, Long> getModuleCountMap(String projectId, List<ModuleCountDTO> moduleCountDTOList) {
@ -212,7 +209,7 @@ public class FunctionalCaseModuleService extends ModuleTreeService {
}
example.clear();
}
example.createCriteria().andParentIdEqualTo(functionalCaseModule.getParentId()).andNameEqualTo(functionalCaseModule.getName()).andIdNotEqualTo(functionalCaseModule.getId());
example.createCriteria().andParentIdEqualTo(functionalCaseModule.getParentId()).andNameEqualTo(functionalCaseModule.getName()).andIdNotEqualTo(functionalCaseModule.getId()).andProjectIdEqualTo(functionalCaseModule.getProjectId());
if (functionalCaseModuleMapper.countByExample(example) > 0) {
throw new MSException(Translator.get("node.name.repeat"));
}
@ -283,4 +280,134 @@ public class FunctionalCaseModuleService extends ModuleTreeService {
return totalList.stream().distinct().toList();
}
/**
* 根据模块路径创建模块
*
* @param modulePath 模块路径
* @param projectId 项目ID
* @param moduleTree 已存在的模块树
* @param userId userId
*/
public Map<String, String> createCaseModule(List<String> modulePath, String projectId, List<BaseTreeNode> moduleTree, String userId, Map<String, String> pathMap) {
modulePath.forEach(path -> {
List<String> moduleNames = new ArrayList<>(List.of(path.split("/")));
Iterator<String> itemIterator = moduleNames.iterator();
AtomicReference<Boolean> hasNode = new AtomicReference<>(false);
//当前节点模块名称
String currentModuleName;
if (moduleNames.size() <= 1) {
throw new MSException(Translator.get("test_case_create_module_fail") + ":" + path);
} else {
itemIterator.next();
itemIterator.remove();
currentModuleName = itemIterator.next().trim();
moduleTree.forEach(module -> {
//根节点是否存在
if (StringUtils.equals(currentModuleName, module.getName())) {
hasNode.set(true);
//根节点存在检查子节点是否存在
createModuleByPathIterator(itemIterator, "/" + currentModuleName, module, pathMap, projectId, userId);
}
});
}
if (!hasNode.get()) {
//根节点不存在直接创建
createModuleByPath(itemIterator, currentModuleName, null, projectId, StringUtils.EMPTY, pathMap, userId);
}
});
return pathMap;
}
/**
* 根据模块路径迭代器遍历模块路径
*
* @param itemIterator 模块路径迭代器
* @param currentModulePath 当前节点路径 /模块1/模块2
* @param module 当前模块对象
* @param pathMap 记录新创建的模块路径和模块ID
* @param projectId 项目id
* @param userId userId
*/
private void createModuleByPathIterator(Iterator<String> itemIterator, String currentModulePath, BaseTreeNode module, Map<String, String> pathMap, String projectId, String userId) {
List<BaseTreeNode> children = module.getChildren();
if (CollectionUtils.isEmpty(children) || !itemIterator.hasNext()) {
//没有子节点根据当前模块目录创建模块节点
pathMap.put(currentModulePath, module.getId());
if (itemIterator.hasNext()) {
createModuleByPath(itemIterator, itemIterator.next().trim(), module, projectId, currentModulePath, pathMap, userId);
}
return;
}
String nodeName = itemIterator.next().trim();
AtomicReference<Boolean> hasNode = new AtomicReference<>(false);
children.forEach(child -> {
if (StringUtils.equals(nodeName, child.getName())) {
hasNode.set(true);
createModuleByPathIterator(itemIterator, currentModulePath + "/" + child.getName(), child, pathMap, projectId, userId);
}
});
//若子节点中不包含该目标节点则在该节点下创建
if (!hasNode.get()) {
createModuleByPath(itemIterator, nodeName, module, projectId, currentModulePath, pathMap, userId);
}
}
/**
* 遍历模块路径创建模块
*
* @param itemIterator 模块路径迭代器
* @param moduleName 当前模块名称 模块1
* @param parentModule 父模块对象
* @param projectId 项目id
* @param currentPath 当前模块路径 /模块1
* @param pathMap 记录新创建的模块路径和模块ID
*/
private void createModuleByPath(Iterator<String> itemIterator, String moduleName, BaseTreeNode parentModule, String projectId, String currentPath, Map<String, String> pathMap, String userId) {
StringBuilder path = new StringBuilder(currentPath);
path.append("/" + moduleName.trim());
//模块id
String pid;
if (pathMap.get(path.toString()) != null) {
//如果创建过直接获取模块ID
pid = pathMap.get(path.toString());
} else {
pid = insertNode(moduleName, parentModule == null ? ModuleConstants.ROOT_NODE_PARENT_ID : parentModule.getId(), projectId, userId);
pathMap.put(path.toString(), pid);
}
while (itemIterator.hasNext()) {
String nextModuleName = itemIterator.next().trim();
path.append("/" + nextModuleName);
if (pathMap.get(path.toString()) != null) {
pid = pathMap.get(path.toString());
} else {
pid = insertNode(nextModuleName, pid, projectId, userId);
pathMap.put(path.toString(), pid);
}
}
}
/**
* 创建模块
*
* @param moduleName 模块名称
* @param parentId 父模块ID
* @param projectId 项目ID
* @param userId userId
* @return
*/
private String insertNode(String moduleName, String parentId, String projectId, String userId) {
FunctionalCaseModuleCreateRequest request = new FunctionalCaseModuleCreateRequest();
request.setProjectId(projectId);
request.setName(moduleName);
request.setParentId(parentId);
return this.add(request, userId);
}
}

View File

@ -5,8 +5,10 @@ import io.metersphere.bug.mapper.BugRelationCaseMapper;
import io.metersphere.functional.constants.CaseEvent;
import io.metersphere.functional.constants.CaseFileSourceType;
import io.metersphere.functional.constants.FunctionalCaseReviewStatus;
import io.metersphere.functional.constants.FunctionalCaseTypeConstants;
import io.metersphere.functional.domain.*;
import io.metersphere.functional.dto.*;
import io.metersphere.functional.excel.domain.FunctionalCaseExcelData;
import io.metersphere.functional.mapper.*;
import io.metersphere.functional.request.*;
import io.metersphere.functional.result.CaseManagementResultCode;
@ -25,6 +27,7 @@ import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.domain.CustomFieldOption;
import io.metersphere.system.dto.sdk.BaseTreeNode;
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
import io.metersphere.system.dto.sdk.TemplateDTO;
import io.metersphere.system.dto.sdk.request.PosRequest;
@ -195,16 +198,6 @@ public class FunctionalCaseService {
return functionalCase;
}
private List<CaseCustomFieldDTO> getCustomFields(Map<String, Object> customFields) {
List<CaseCustomFieldDTO> list = new ArrayList<>();
customFields.keySet().forEach(key -> {
CaseCustomFieldDTO caseCustomFieldDTO = new CaseCustomFieldDTO();
caseCustomFieldDTO.setFieldId(key);
caseCustomFieldDTO.setValue(customFields.get(key).toString());
list.add(caseCustomFieldDTO);
});
return list;
}
public Long getNextOrder(String projectId) {
Long pos = extFunctionalCaseMapper.getPos(projectId);
@ -771,4 +764,183 @@ public class FunctionalCaseService {
extFunctionalCaseMapper::getLastPos,
functionalCaseMapper::updateByPrimaryKeySelective);
}
public String checkNumExist(String num, String projectId) {
FunctionalCaseExample example = new FunctionalCaseExample();
example.createCriteria().andNumEqualTo(Long.valueOf(num)).andProjectIdEqualTo(projectId).andDeletedEqualTo(false);
List<FunctionalCase> functionalCases = functionalCaseMapper.selectByExample(example);
if (CollectionUtils.isNotEmpty(functionalCases)) {
return functionalCases.get(0).getId();
}
return null;
}
/**
* 导入新建数据
*
* @param list 导入数据集合
* @param request request
* @param moduleTree 模块树
* @param userId 用户id
* @param customFieldsMap 当前默认模板的自定义字段
*/
public void saveImportData(List<FunctionalCaseExcelData> list, FunctionalCaseImportRequest request, List<BaseTreeNode> moduleTree, String userId, Map<String, TemplateCustomFieldDTO> customFieldsMap, Map<String, String> pathMap) {
//默认模板
TemplateDTO defaultTemplateDTO = projectTemplateService.getDefaultTemplateDTO(request.getProjectId(), TemplateScene.FUNCTIONAL.name());
//模块路径
List<String> modulePath = list.stream().map(FunctionalCaseExcelData::getModule).toList();
//构建模块树
Map<String, String> caseModulePathMap = functionalCaseModuleService.createCaseModule(modulePath, request.getProjectId(), moduleTree, userId, pathMap);
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
FunctionalCaseMapper caseMapper = sqlSession.getMapper(FunctionalCaseMapper.class);
FunctionalCaseBlobMapper caseBlobMapper = sqlSession.getMapper(FunctionalCaseBlobMapper.class);
FunctionalCaseCustomFieldMapper customFieldMapper = sqlSession.getMapper(FunctionalCaseCustomFieldMapper.class);
Long nextOrder = getNextOrder(request.getProjectId());
for (int i = 0; i < list.size(); i++) {
parseInsertDataToModule(list.get(i), request, userId, caseModulePathMap, defaultTemplateDTO, nextOrder, caseMapper, caseBlobMapper, customFieldMapper, customFieldsMap);
}
sqlSession.flushStatements();
SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
}
private void parseInsertDataToModule(FunctionalCaseExcelData functionalCaseExcelData, FunctionalCaseImportRequest request, String userId, Map<String, String> caseModulePathMap, TemplateDTO defaultTemplateDTO, Long nextOrder,
FunctionalCaseMapper caseMapper, FunctionalCaseBlobMapper caseBlobMapper, FunctionalCaseCustomFieldMapper customFieldMapper, Map<String, TemplateCustomFieldDTO> customFieldsMap) {
//构建用例
FunctionalCase functionalCase = new FunctionalCase();
String caseId = IDGenerator.nextStr();
functionalCase.setId(caseId);
functionalCase.setNum(getNextNum(request.getProjectId()));
functionalCase.setModuleId(caseModulePathMap.get(functionalCaseExcelData.getModule()));
functionalCase.setProjectId(request.getProjectId());
functionalCase.setTemplateId(defaultTemplateDTO.getId());
functionalCase.setName(functionalCaseExcelData.getName());
functionalCase.setReviewStatus(FunctionalCaseReviewStatus.UN_REVIEWED.name());
functionalCase.setTags(handleImportTags(functionalCaseExcelData.getTags()));
functionalCase.setCaseEditType(StringUtils.defaultIfBlank(functionalCaseExcelData.getCaseEditType(), FunctionalCaseTypeConstants.CaseEditType.TEXT.name()));
functionalCase.setPos(nextOrder + ServiceUtils.POS_STEP);
functionalCase.setVersionId(request.getVersionId());
functionalCase.setRefId(caseId);
functionalCase.setLastExecuteResult(FunctionalCaseExecuteResult.UN_EXECUTED.name());
functionalCase.setLatest(true);
functionalCase.setCreateUser(userId);
functionalCase.setCreateTime(System.currentTimeMillis());
functionalCase.setUpdateTime(System.currentTimeMillis());
caseMapper.insertSelective(functionalCase);
//用例附属表
FunctionalCaseBlob caseBlob = new FunctionalCaseBlob();
caseBlob.setId(caseId);
caseBlob.setSteps(StringUtils.defaultIfBlank(functionalCaseExcelData.getSteps(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8));
caseBlob.setTextDescription(StringUtils.defaultIfBlank(functionalCaseExcelData.getTextDescription(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8));
caseBlob.setExpectedResult(StringUtils.defaultIfBlank(functionalCaseExcelData.getExpectedResult(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8));
caseBlob.setPrerequisite(StringUtils.defaultIfBlank(functionalCaseExcelData.getPrerequisite(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8));
caseBlob.setDescription(StringUtils.defaultIfBlank(functionalCaseExcelData.getDescription(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8));
caseBlobMapper.insertSelective(caseBlob);
//自定义字段
handleImportCustomField(functionalCaseExcelData, caseId, customFieldMapper, customFieldsMap);
}
/**
* 处理导入标签
*
* @param tags 标签
* @return
*/
private List<String> handleImportTags(String tags) {
List<String> split = List.of(tags.split("[,;]"));
return split.stream().map(String::trim).filter(StringUtils::isNotEmpty).collect(Collectors.toList());
}
/**
* 处理导入自定义字段
*
* @param functionalCaseExcelData 导入数据
* @param caseId 用例id
* @param customFieldMapper 自定义字段mapper
* @param customFieldsMap 当前默认模板的自定义字段
*/
private void handleImportCustomField(FunctionalCaseExcelData functionalCaseExcelData, String caseId, FunctionalCaseCustomFieldMapper customFieldMapper, Map<String, TemplateCustomFieldDTO> customFieldsMap) {
//需要保存的自定义字段
Map<String, Object> customData = functionalCaseExcelData.getCustomData();
customData.forEach((k, v) -> {
if (customFieldsMap.containsKey(k)) {
TemplateCustomFieldDTO templateCustomFieldDTO = customFieldsMap.get(k);
FunctionalCaseCustomField caseCustomField = new FunctionalCaseCustomField();
caseCustomField.setCaseId(caseId);
caseCustomField.setFieldId(templateCustomFieldDTO.getFieldId());
caseCustomField.setValue(v.toString());
customFieldMapper.insertSelective(caseCustomField);
}
});
}
/**
* 导入更新数据
*
* @param updateList 更新数据集合
* @param request request
* @param moduleTree 模块树
* @param userId 用户id
* @param customFieldsMap 当前默认模板的自定义字段
*/
public void updateImportData(List<FunctionalCaseExcelData> updateList, FunctionalCaseImportRequest request, List<BaseTreeNode> moduleTree, String userId, Map<String, TemplateCustomFieldDTO> customFieldsMap, Map<String, String> pathMap) {
//默认模板
TemplateDTO defaultTemplateDTO = projectTemplateService.getDefaultTemplateDTO(request.getProjectId(), TemplateScene.FUNCTIONAL.name());
//模块路径
List<String> modulePath = updateList.stream().map(FunctionalCaseExcelData::getModule).toList();
//构建模块树
Map<String, String> caseModulePathMap = functionalCaseModuleService.createCaseModule(modulePath, request.getProjectId(), moduleTree, userId, pathMap);
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
FunctionalCaseMapper caseMapper = sqlSession.getMapper(FunctionalCaseMapper.class);
FunctionalCaseBlobMapper caseBlobMapper = sqlSession.getMapper(FunctionalCaseBlobMapper.class);
FunctionalCaseCustomFieldMapper customFieldMapper = sqlSession.getMapper(FunctionalCaseCustomFieldMapper.class);
for (int i = 0; i < updateList.size(); i++) {
parseUpdateDataToModule(updateList.get(i), request, userId, caseModulePathMap, defaultTemplateDTO, caseMapper, caseBlobMapper, customFieldMapper, customFieldsMap);
}
sqlSession.flushStatements();
SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
}
private void parseUpdateDataToModule(FunctionalCaseExcelData functionalCaseExcelData, FunctionalCaseImportRequest request, String userId, Map<String, String> caseModulePathMap, TemplateDTO defaultTemplateDTO, FunctionalCaseMapper caseMapper, FunctionalCaseBlobMapper caseBlobMapper, FunctionalCaseCustomFieldMapper customFieldMapper, Map<String, TemplateCustomFieldDTO> customFieldsMap) {
//用例表
FunctionalCase functionalCase = caseMapper.selectByPrimaryKey(functionalCaseExcelData.getNum());
functionalCase.setName(functionalCaseExcelData.getName());
functionalCase.setModuleId(caseModulePathMap.get(functionalCaseExcelData.getModule()));
functionalCase.setTags(handleImportTags(functionalCaseExcelData.getTags()));
functionalCase.setCaseEditType(StringUtils.defaultIfBlank(functionalCaseExcelData.getCaseEditType(), FunctionalCaseTypeConstants.CaseEditType.TEXT.name()));
//模板
functionalCase.setTemplateId(defaultTemplateDTO.getId());
functionalCase.setVersionId(request.getVersionId());
functionalCase.setUpdateUser(userId);
functionalCase.setUpdateTime(System.currentTimeMillis());
caseMapper.updateByPrimaryKeySelective(functionalCase);
//用例附属表
FunctionalCaseBlob caseBlob = new FunctionalCaseBlob();
caseBlob.setId(functionalCase.getId());
caseBlob.setSteps(StringUtils.defaultIfBlank(functionalCaseExcelData.getSteps(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8));
caseBlob.setTextDescription(StringUtils.defaultIfBlank(functionalCaseExcelData.getTextDescription(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8));
caseBlob.setExpectedResult(StringUtils.defaultIfBlank(functionalCaseExcelData.getExpectedResult(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8));
caseBlob.setPrerequisite(StringUtils.defaultIfBlank(functionalCaseExcelData.getPrerequisite(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8));
caseBlob.setDescription(StringUtils.defaultIfBlank(functionalCaseExcelData.getDescription(), StringUtils.EMPTY).getBytes(StandardCharsets.UTF_8));
caseBlobMapper.updateByPrimaryKeyWithBLOBs(caseBlob);
//自定义字段
handleUpdateCustomField(functionalCaseExcelData, functionalCase.getId(), customFieldMapper, customFieldsMap);
}
private void handleUpdateCustomField(FunctionalCaseExcelData functionalCaseExcelData, String caseId, FunctionalCaseCustomFieldMapper customFieldMapper, Map<String, TemplateCustomFieldDTO> customFieldsMap) {
FunctionalCaseCustomFieldExample fieldExample = new FunctionalCaseCustomFieldExample();
fieldExample.createCriteria().andCaseIdEqualTo(caseId);
customFieldMapper.deleteByExample(fieldExample);
handleImportCustomField(functionalCaseExcelData, caseId, customFieldMapper, customFieldsMap);
}
}

View File

@ -72,6 +72,7 @@ public class FunctionalCaseControllerTests extends BaseTest {
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";
public static final String IMPORT_EXCEL_URL = "/functional/case/import/excel";
@Resource
private NotificationMapper notificationMapper;
@ -559,7 +560,7 @@ public class FunctionalCaseControllerTests extends BaseTest {
@Test
@Order(20)
@Order(18)
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));
@ -589,4 +590,45 @@ public class FunctionalCaseControllerTests extends BaseTest {
paramMap.add("file", file2);
this.requestMultipart(CHECK_EXCEL_URL, paramMap);
}
@Test
@Order(19)
public void testImportExcel() throws Exception {
String filePath = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/3.xlsx")).getPath();
MockMultipartFile file = new MockMultipartFile("file", "11.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath));
FunctionalCaseImportRequest request = new FunctionalCaseImportRequest();
request.setCover(true);
request.setProjectId("100001100001");
LinkedMultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
paramMap.add("request", JSON.toJSONString(request));
paramMap.add("file", file);
MvcResult functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(IMPORT_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);
String filePath1 = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/4.xlsx")).getPath();
MockMultipartFile file1 = new MockMultipartFile("file", "14.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath1));
paramMap = new LinkedMultiValueMap<>();
paramMap.add("request", JSON.toJSONString(request));
paramMap.add("file", file1);
this.requestMultipart(IMPORT_EXCEL_URL, paramMap);
paramMap = new LinkedMultiValueMap<>();
paramMap.add("request", JSON.toJSONString(request));
this.requestMultipart(IMPORT_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(IMPORT_EXCEL_URL, paramMap);
}
}

View File

@ -72,3 +72,7 @@ VALUES ('wx_review_id',10006,'测试重新提审', 'test_module_one', 'TEST_MODU
INSERT INTO project_version(id, project_id, name, description, status, latest, publish_time, start_time, end_time, create_time, create_user)
VALUES ('v2.0.1', 'wx_relationship', 'v1.0', NULL, 'open', b'1', 1698810592000, 1698810592000, 1698810592000, 1698810592000, 'admin');
INSERT INTO functional_case(id, num, module_id, project_id, template_id, name, review_status, tags, case_edit_type, pos, version_id, ref_id, last_execute_result, deleted, public_case, latest, create_user, update_user, delete_user, create_time, update_time, delete_time)
VALUES ('TEST_IMPORT', 10, 'TEST_MODULE_ID_GYQ', '100001100001', '100001', '1223', 'UN_REVIEWED', NULL, 'STEP', 0, 'v3.0.0', 'wx_ref_id', 'UN_EXECUTED', b'0', b'0', b'1', 'admin', 'admin', '', 1698058347559, 1698058347559, NULL);
INSERT INTO functional_case_blob(id, steps, text_description, expected_result, prerequisite, description) VALUES ('TEST_IMPORT', 'STEP', '1111', '', '', 'TEST');