feat(测试跟踪): 缺陷管理导入自定义字段校验

--story=1010310 --user=宋昌昌 【测试跟踪】缺陷管理支持通过excel导入导出 https://www.tapd.cn/55049933/s/1295113
This commit is contained in:
song-cc-rock 2022-11-14 17:18:34 +08:00
parent 820c163cb7
commit ba22b7c7f7
6 changed files with 123 additions and 18 deletions

View File

@ -74,6 +74,7 @@ public class IssueTemplateHeadWriteHandler implements RowWriteHandler, SheetWrit
if (BooleanUtils.isTrue(context.getHead())) {
sheet = context.getWriteSheetHolder().getSheet();
drawingPatriarch = sheet.createDrawingPatriarch();
// 设置表头内容
headCommentIndexMap.forEach(this::setComment);
}
}

View File

@ -31,6 +31,7 @@ import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
/**
@ -42,10 +43,11 @@ public class IssueExcelListener extends AnalysisEventListener<Map<Integer, Strin
private Class dataClass;
private IssueImportRequest request;
private Boolean isThirdPlatform = false;
private Boolean isThirdPlatform;
private Map<Integer, String> headMap;
private List<CustomFieldDao> customFields = new ArrayList<>();
private List<CustomFieldDao> customFields;
private IssuesService issuesService;
private Map<String, String> memberMap;
/**
* excel表头字段字典值
*/
@ -66,12 +68,13 @@ public class IssueExcelListener extends AnalysisEventListener<Map<Integer, Strin
protected List<IssueExcelData> updateList = new ArrayList<>();
protected List<ExcelErrData<IssueExcelData>> errList = new ArrayList<>();
public IssueExcelListener(IssueImportRequest request, Class clazz, Boolean isThirdPlatform, List<CustomFieldDao> customFields) {
public IssueExcelListener(IssueImportRequest request, Class clazz, Boolean isThirdPlatform, List<CustomFieldDao> customFields, Map<String, String> memberMap) {
this.request = request;
this.dataClass = clazz;
this.isThirdPlatform = isThirdPlatform;
this.customFields = customFields;
this.issuesService = CommonBeanFactory.getBean(IssuesService.class);
this.memberMap = memberMap;
}
@Override
@ -83,9 +86,9 @@ public class IssueExcelListener extends AnalysisEventListener<Map<Integer, Strin
issueExcelData = this.parseDataToModel(data);
// EXCEL校验, 如果不是第三方模板则需要校验
errMsg = new StringBuilder(!isThirdPlatform ? ExcelValidateHelper.validateEntity(issueExcelData) : StringUtils.EMPTY);
//自定义校验规则
// 校验自定义字段
if (StringUtils.isEmpty(errMsg)) {
validate(issueExcelData, errMsg);
validateCustomField(issueExcelData, errMsg);
}
} catch (Exception e) {
errMsg = new StringBuilder(Translator.get("parse_data_error"));
@ -177,8 +180,29 @@ public class IssueExcelListener extends AnalysisEventListener<Map<Integer, Strin
}
}
public void validate(IssueExcelData data, StringBuilder errMsg) {
// TODO 校验自定义字段的数据是否合法
public void validateCustomField(IssueExcelData data, StringBuilder errMsg) {
Map<String, List<CustomFieldDao>> customFieldMap = customFields.stream().collect(Collectors.groupingBy(CustomFieldDao::getName));
data.getCustomData().forEach((k, v) -> {
List<CustomFieldDao> customFieldDaos = customFieldMap.get(k);
if (CollectionUtils.isNotEmpty(customFieldDaos) && customFieldDaos.size() > 0) {
CustomFieldDao customFieldDao = customFieldDaos.get(0);
String type = customFieldDao.getType();
Boolean required = customFieldDao.getRequired();
String options = StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.MEMBER.getValue(), CustomFieldType.MULTIPLE_MEMBER.getValue()) ?
this.memberMap.toString() : customFieldDao.getOptions();
if (required && StringUtils.isEmpty(v.toString())) {
errMsg.append(k).append(Translator.get("can_not_be_null")).append(";");
} else if (StringUtils.isNotEmpty(v.toString()) && isSelect(type) && !isOptionInclude(v, options)) {
errMsg.append(k).append(Translator.get("options_not_exist")).append(";");
} else if (StringUtils.isNotEmpty(v.toString()) && isIllegalFormat(type, v.toString())) {
errMsg.append(k).append(Translator.get("format_error")).append(";");
}
} else {
if (!exportFieldsContains(k)) {
errMsg.append(k).append(Translator.get("excel_field_not_exist")).append(";");
}
}
});
}
private IssueExcelData parseDataToModel(Map<Integer, String> rowData) {
@ -309,7 +333,7 @@ public class IssueExcelListener extends AnalysisEventListener<Map<Integer, Strin
CustomFieldType.CHECKBOX.getValue(), CustomFieldType.MULTIPLE_INPUT.getValue(),
CustomFieldType.MULTIPLE_MEMBER.getValue(), CustomFieldType.CASCADING_SELECT.getValue())) {
if (!v.toString().contains("[")) {
v = List.of("\"" + v.toString() + "\"");
v = List.of("\"" + v + "\"");
}
customFieldResourceDTO.setValue(v.toString());
} else if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.DATE.getValue())) {
@ -353,4 +377,51 @@ public class IssueExcelListener extends AnalysisEventListener<Map<Integer, Strin
private String getPlatformId(String issueId) {
return issuesService.getIssue(issueId).getPlatformId();
}
private Boolean isSelect(String type) {
return StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.SELECT.getValue(), CustomFieldType.RADIO.getValue(),
CustomFieldType.MULTIPLE_SELECT.getValue(), CustomFieldType.CHECKBOX.getValue(),
CustomFieldType.CASCADING_SELECT.getValue(), CustomFieldType.MEMBER.getValue(), CustomFieldType.MULTIPLE_MEMBER.getValue());
}
private Boolean isIllegalFormat(String type, String value) {
try {
if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.DATE.getValue())) {
DateUtils.parseDate(value, "yyyy/MM/dd");
} else if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.DATETIME.getValue())) {
DateUtils.parseDate(value);
} else if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.INT.getValue())) {
Integer.parseInt(value);
} else if (StringUtils.equalsAnyIgnoreCase(type, CustomFieldType.FLOAT.getValue())) {
Float.parseFloat(value);
}
return Boolean.FALSE;
} catch (Exception e) {
return Boolean.TRUE;
}
}
private Boolean isOptionInclude(Object value, String options) {
AtomicReference<Boolean> isInclude = new AtomicReference<>(Boolean.TRUE);
if (value instanceof List) {
((List<?>) value).forEach(item -> {
String s = item.toString().replaceAll("\"", StringUtils.EMPTY);
if (!StringUtils.contains(options, s)) {
isInclude.set(Boolean.FALSE);
}
});
} else {
isInclude.set(StringUtils.contains(options, value.toString()));
}
return isInclude.get();
}
public Boolean exportFieldsContains(String name) {
for (IssueExportHeadField issueExportHeadField : IssueExportHeadField.values()) {
if (StringUtils.equals(name, issueExportHeadField.getName())) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
}

View File

@ -495,13 +495,13 @@ public class IssuesService {
issuesRequest.setProjectId(SessionUtils.getCurrentProjectId());
List<IssuesDao> issuesDaos = listByWorkspaceId(issuesRequest);
if (CollectionUtils.isNotEmpty(issuesDaos)) {
issuesDaos.forEach(issuesDao -> {
issuesDaos.parallelStream().forEach(issuesDao -> {
delete(issuesDao.getId());
});
}
} else {
if (CollectionUtils.isNotEmpty(request.getBatchDeleteIds())) {
request.getBatchDeleteIds().forEach(id -> delete(id));
request.getBatchDeleteIds().parallelStream().forEach(id -> delete(id));
}
}
}
@ -610,17 +610,19 @@ public class IssuesService {
if (StringUtils.equalsAnyIgnoreCase(customField.getType(), CustomFieldType.RICH_TEXT.getValue(), CustomFieldType.TEXTAREA.getValue())) {
fieldDao.setValue(fieldDao.getTextValue());
}
if (StringUtils.equalsAnyIgnoreCase(customField.getType(), CustomFieldType.DATE.getValue()) && StringUtils.isNotEmpty(fieldDao.getValue())) {
if (StringUtils.equalsAnyIgnoreCase(customField.getType(), CustomFieldType.DATE.getValue()) && StringUtils.isNotEmpty(fieldDao.getValue()) && !StringUtils.equals(fieldDao.getValue(), "null")) {
Date date = DateUtils.parseDate(fieldDao.getValue().replaceAll("\"", StringUtils.EMPTY), "yyyy-MM-dd");
String format = DateUtils.format(date, "yyyy/MM/dd");
fieldDao.setValue("\"" + format + "\"");
}
if (StringUtils.equalsAnyIgnoreCase(customField.getType(), CustomFieldType.DATETIME.getValue()) && StringUtils.isNotEmpty(fieldDao.getValue())) {
if (StringUtils.equalsAnyIgnoreCase(customField.getType(), CustomFieldType.DATETIME.getValue()) && StringUtils.isNotEmpty(fieldDao.getValue()) && !StringUtils.equals(fieldDao.getValue(), "null")) {
Date date = null;
if (fieldDao.getValue().contains("T") && fieldDao.getValue().length() == 18) {
date = DateUtils.parseDate(fieldDao.getValue().replaceAll("\"", StringUtils.EMPTY), "yyyy-MM-dd'T'HH:mm");
} else if (fieldDao.getValue().contains("T") && fieldDao.getValue().length() == 21) {
date = DateUtils.parseDate(fieldDao.getValue().replaceAll("\"", StringUtils.EMPTY), "yyyy-MM-dd'T'HH:mm:ss");
} else if (fieldDao.getValue().contains("T") && fieldDao.getValue().length() > 21) {
date = DateUtils.parseDate(fieldDao.getValue().replaceAll("\"", StringUtils.EMPTY).substring(0, 19), "yyyy-MM-dd'T'HH:mm:ss");
} else {
date = DateUtils.parseDate(fieldDao.getValue().replaceAll("\"", StringUtils.EMPTY));
}
@ -1194,9 +1196,12 @@ public class IssuesService {
public void issueImportTemplate(String projectId, HttpServletResponse response) {
Map<String, String> userMap = baseUserService.getProjectMemberOption(projectId).stream().collect(Collectors.toMap(User::getId, User::getName));
// 获取缺陷模板及自定义字段
IssueTemplateDao issueTemplate = getIssueTemplateByProjectId(projectId);
List<CustomFieldDao> customFields = Optional.ofNullable(issueTemplate.getCustomFields()).orElse(new ArrayList<>());
// 根据自定义字段获取表头
List<List<String>> heads = new IssueExcelDataFactory().getIssueExcelDataLocal().getHead(issueTemplate.getIsThirdTemplate(), customFields, null);
// 导出空模板, heads->表头, headHandler->表头处理
IssueTemplateHeadWriteHandler headHandler = new IssueTemplateHeadWriteHandler(userMap, heads, issueTemplate.getCustomFields());
new EasyExcelExporter(new IssueExcelDataFactory().getExcelDataByLocal())
.exportByCustomWriteHandler(response, heads, null, Translator.get("issue_import_template_name"),
@ -1207,16 +1212,21 @@ public class IssuesService {
if (importFile == null) {
MSException.throwException(Translator.get("upload_fail"));
}
Map<String, String> userMap = baseUserService.getProjectMemberOption(request.getProjectId()).stream().collect(Collectors.toMap(User::getId, User::getName));
// 获取缺陷模板及自定义字段
IssueTemplateDao issueTemplate = getIssueTemplateByProjectId(request.getProjectId());
List<CustomFieldDao> customFields = Optional.ofNullable(issueTemplate.getCustomFields()).orElse(new ArrayList<>());
// 获取本地EXCEL数据对象
Class clazz = new IssueExcelDataFactory().getExcelDataByLocal();
IssueExcelListener issueExcelListener = new IssueExcelListener(request, clazz, issueTemplate.getIsThirdTemplate(), customFields);
// IssueExcelListener读取file内容
IssueExcelListener issueExcelListener = new IssueExcelListener(request, clazz, issueTemplate.getIsThirdTemplate(), customFields, userMap);
try {
EasyExcelFactory.read(importFile.getInputStream(), issueExcelListener).sheet().doRead();
} catch (IOException e) {
LogUtil.error(e.getMessage(), e);
e.printStackTrace();
}
// 获取错误信息并返回
List<ExcelErrData<IssueExcelData>> errList = issueExcelListener.getErrList();
ExcelResponse excelResponse = new ExcelResponse();
if (CollectionUtils.isNotEmpty(errList)) {
@ -1230,24 +1240,33 @@ public class IssuesService {
public void issueExport(IssueExportRequest request, HttpServletResponse response) {
Map<String, String> userMap = baseUserService.getProjectMemberOption(request.getProjectId()).stream().collect(Collectors.toMap(User::getId, User::getName));
// 获取缺陷模板及自定义字段
IssueTemplateDao issueTemplate = getIssueTemplateByProjectId(request.getProjectId());
List<CustomFieldDao> customFields = Optional.ofNullable(issueTemplate.getCustomFields()).orElse(new ArrayList<>());
// 根据自定义字段获取表头内容
List<List<String>> heads = new IssueExcelDataFactory().getIssueExcelDataLocal().getHead(issueTemplate.getIsThirdTemplate(), customFields, request);
// 获取导出缺陷列表
List<IssuesDao> exportIssues = getExportIssues(request, issueTemplate.getIsThirdTemplate(), customFields);
// 解析issue对象数据->excel对象数据
List<IssueExcelData> excelDataList = parseIssueDataToExcelData(exportIssues);
// 解析excel对象数据->excel列表数据
List<List<Object>> data = parseExcelDataToList(heads, excelDataList);
// 导出EXCEL
IssueTemplateHeadWriteHandler headHandler = new IssueTemplateHeadWriteHandler(userMap, heads, issueTemplate.getCustomFields());
// heads-> 表头内容, data -> 导出EXCEL列表数据, headHandler -> 表头处理
new EasyExcelExporter(new IssueExcelDataFactory().getExcelDataByLocal())
.exportByCustomWriteHandler(response, heads, data, Translator.get("issue_list_export_excel"),
Translator.get("issue_list_export_excel_sheet"), headHandler);
}
public List<IssuesDao> getExportIssues(IssueExportRequest exportRequest, Boolean isThirdTemplate, List<CustomFieldDao> customFields) {
// 根据列表条件获取符合缺陷集合
IssuesRequest request = new IssuesRequest();
request.setProjectId(exportRequest.getProjectId());
request.setWorkspaceId(exportRequest.getWorkspaceId());
request.setSelectAll(exportRequest.getIsSelectAll());
request.setExportIds(exportRequest.getExportIds());
// 列表排序
request.setOrders(exportRequest.getOrders());
request.setOrders(ServiceUtils.getDefaultOrderByField(request.getOrders(), "create_time"));
request.getOrders().forEach(order -> {
@ -1266,6 +1285,7 @@ public class IssuesService {
Map<String, String> planMap = getPlanMap(issues);
Map<String, List<IssueCommentDTO>> commentMap = getCommentMap(issues);
// 设置creator, caseCount, commnet
issues.forEach(item -> {
User createUser = userMap.get(item.getCreator());
if (createUser != null) {
@ -1288,6 +1308,7 @@ public class IssuesService {
item.setComment(StringUtils.join(comments, ";"));
}
});
// 解析自定义字段
buildCustomField(issues, isThirdTemplate, customFields);
return issues;
}
@ -1424,13 +1445,13 @@ public class IssuesService {
}
public void saveImportData(List<IssuesUpdateRequest> issues) {
issues.forEach(issue -> {
issues.parallelStream().forEach(issue -> {
addIssues(issue, null);
});
}
public void updateImportData(List<IssuesUpdateRequest> issues) {
issues.forEach(issue -> {
issues.parallelStream().forEach(issue -> {
updateIssues(issue);
});
}

View File

@ -51,7 +51,7 @@ int_import_cell_format_comment=cell format: 100001
float_import_cell_format_comment=cell format: 24
multiple_input_import_cell_format_comment=This field has multiple values. Separate multiple values with commas or semicolons
options_tips=(format{key:value}, please fill in the corresponding value)Option value:
# issue export
# issue import and issue export
title==Title
description=Description
case_count=Case count
@ -59,6 +59,10 @@ comment=Comment
issue_resource=Issue resource
issue_platform=Issue platform
create_time=CreateTime
can_not_be_null=Can not be null
excel_field_not_exist=Not exist
options_not_exist=Incorrect option value
format_error=Format error
#project
project_name_is_null=Project name cannot be null
project_name_already_exists=The project name already exists

View File

@ -28,7 +28,7 @@ int_import_cell_format_comment=整型单元格格式为: 100001
float_import_cell_format_comment=浮点单元格格式为: 24
multiple_input_import_cell_format_comment=该单元格可输入多个值,多个值请用逗号或分号隔开(v1;v2)
options_tips=(格式{key:value},请填写对应的value)选项:
# issue export
# issue import and issue export
title=缺陷标题
description=缺陷描述
case_count=用例数
@ -36,6 +36,10 @@ comment=评论
issue_resource=缺陷来源
issue_platform=缺陷平台
create_time=创建时间
can_not_be_null=不能为空
excel_field_not_exist=不存在该字段
options_not_exist=选项值有误
format_error=格式有误
#project
project_name_is_null=项目名称不能为空
project_name_already_exists=项目名称已存在

View File

@ -28,7 +28,7 @@ int_import_cell_format_comment=單元格格式: 100001
float_import_cell_format_comment=單元格格式: 24
multiple_input_import_cell_format_comment=該單元格可輸入多個值,多個值請用逗號或分號隔開(v1;v2)
options_tips=(格式{key:value},請填寫對應value)選項:
# issue export
# issue import and issue export
title=缺陷標題
description=缺陷描述
case_count=用例數
@ -36,6 +36,10 @@ comment=評論
issue_resource=缺陷來源
issue_platform=缺陷平臺
create_time=創建時間
can_not_be_null=不能爲空
excel_field_not_exist=不存在該字段
options_not_exist=選項值有誤
format_error=格式有誤
#project
project_name_is_null=項目名稱不能為空
project_name_already_exists=項目名稱已存在