mirror of
https://gitee.com/fit2cloud-feizhiyun/MeterSphere.git
synced 2024-11-30 02:58:31 +08:00
feat(测试用例): 功能用例导出excel
This commit is contained in:
parent
f76a8cbd7a
commit
279384539d
@ -32,6 +32,7 @@ public class DefaultRepositoryDir {
|
||||
* 会定时清理
|
||||
*/
|
||||
private static final String SYSTEM_TEMP_DIR = SYSTEM_ROOT_DIR + "/temp";
|
||||
private static final String EXPORT_EXCEL_TEMP_DIR = SYSTEM_ROOT_DIR + "/export/excel";
|
||||
|
||||
/*------ end: 系统下资源目录 --------*/
|
||||
|
||||
@ -158,6 +159,9 @@ public class DefaultRepositoryDir {
|
||||
return SYSTEM_TEMP_DIR;
|
||||
}
|
||||
|
||||
public static String getExportExcelTempDir() {
|
||||
return EXPORT_EXCEL_TEMP_DIR;
|
||||
}
|
||||
public static String getSystemTempCompressDir() {
|
||||
return SYSTEM_TEMP_DIR + "/compress";
|
||||
}
|
||||
|
@ -91,8 +91,9 @@ public class CompressUtils {
|
||||
|
||||
/**
|
||||
* 将多个文件压缩
|
||||
* @param zipFilePath 压缩文件所在路径
|
||||
* @param fileList 要压缩的文件
|
||||
*
|
||||
* @param zipFilePath 压缩文件所在路径
|
||||
* @param fileList 要压缩的文件
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
@ -113,6 +114,31 @@ public class CompressUtils {
|
||||
return zipFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将多个文件压缩至指定路径
|
||||
*
|
||||
* @param fileList 待压缩的文件列表
|
||||
* @param zipFilePath 压缩文件路径
|
||||
* @return 返回压缩好的文件
|
||||
* @throws IOException
|
||||
*/
|
||||
public static File zipFilesToPath(String zipFilePath, List<File> fileList) throws IOException {
|
||||
File zipFile = new File(zipFilePath);
|
||||
try( // 文件输出流
|
||||
FileOutputStream outputStream = getFileStream(zipFile);
|
||||
// 压缩流
|
||||
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)
|
||||
){
|
||||
int size = fileList.size();
|
||||
// 压缩列表中的文件
|
||||
for (int i = 0; i < size; i++) {
|
||||
File file = fileList.get(i);
|
||||
zipFile(file, zipOutputStream);
|
||||
}
|
||||
}
|
||||
return zipFile;
|
||||
}
|
||||
|
||||
/***
|
||||
* Zip解压
|
||||
*
|
||||
|
@ -1,6 +1,7 @@
|
||||
package io.metersphere.sdk.util;
|
||||
|
||||
import io.metersphere.sdk.exception.MSException;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
@ -15,4 +16,9 @@ public class MsFileUtils {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteDir(String path) throws Exception {
|
||||
File file = new File(path);
|
||||
FileUtils.deleteDirectory(file);
|
||||
}
|
||||
}
|
||||
|
@ -246,4 +246,14 @@ case.minder.all.case=All Case
|
||||
|
||||
case.minder.status.success=success
|
||||
case.minder.status.error=error
|
||||
case.minder.status.blocked=blocked
|
||||
case.minder.status.blocked=blocked
|
||||
|
||||
case.review.status.un_reviewed=Unreviewed
|
||||
case.review.status.under_reviewed=Under review
|
||||
case.review.status.pass=Passed
|
||||
case.review.status.un_pass=Un pass
|
||||
case.review.status.re_reviewed=Re reviewed
|
||||
case.execute.status.pending=Pending
|
||||
functional_case_comment_template=【评论:%s(%s)】\n%s\n
|
||||
functional_case_execute_comment_template=[Execute comment:%s %s(%s)]\n%s\n
|
||||
functional_case_review_comment_template=[Review comment:%s %s(%s)]\n%s\n
|
@ -246,3 +246,12 @@ case.minder.status.success=成功
|
||||
case.minder.status.error=失败
|
||||
case.minder.status.blocked=阻塞
|
||||
|
||||
case.review.status.un_reviewed=未评审
|
||||
case.review.status.under_reviewed=评审中
|
||||
case.review.status.pass=已通过
|
||||
case.review.status.un_pass=不通过
|
||||
case.review.status.re_reviewed=重新提审
|
||||
case.execute.status.pending=未执行
|
||||
functional_case_comment_template=【评论:%s(%s)】\n%s\n
|
||||
functional_case_execute_comment_template=【执行评论:%s %s(%s)】\n%s\n
|
||||
functional_case_review_comment_template=【评审评论:%s %s(%s)】\n%s\n
|
@ -247,3 +247,13 @@ case.minder.status.success=成功
|
||||
case.minder.status.error=失敗
|
||||
case.minder.status.blocked=阻塞
|
||||
|
||||
case.review.status.un_reviewed=未評審
|
||||
case.review.status.under_reviewed=評審中
|
||||
case.review.status.pass=已通過
|
||||
case.review.status.un_pass=不通過
|
||||
case.review.status.re_reviewed=重新提審
|
||||
case.execute.status.pending=未執行
|
||||
functional_case_comment_template=【评论:%s(%s)】\n%s\n
|
||||
functional_case_execute_comment_template=【執行評論:%s %s(%s)】\n%s\n
|
||||
functional_case_review_comment_template=【評審評論:%s %s(%s)】\n%s\n
|
||||
|
||||
|
@ -33,6 +33,7 @@ import io.metersphere.system.utils.SessionUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.apache.shiro.authz.annotation.Logical;
|
||||
@ -223,7 +224,7 @@ public class FunctionalCaseController {
|
||||
|
||||
@PostMapping("/pre-check/excel")
|
||||
@Operation(summary = "用例管理-功能用例-excel导入检查")
|
||||
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_UPDATE)
|
||||
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_IMPORT)
|
||||
@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);
|
||||
@ -232,7 +233,7 @@ public class FunctionalCaseController {
|
||||
|
||||
@PostMapping("/import/excel")
|
||||
@Operation(summary = "用例管理-功能用例-excel导入")
|
||||
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_UPDATE)
|
||||
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_IMPORT)
|
||||
@CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project")
|
||||
public FunctionalCaseImportResponse importExcel(@RequestPart("request") FunctionalCaseImportRequest request, @RequestPart(value = "file", required = false) MultipartFile file) {
|
||||
SessionUser user = SessionUtils.getUser();
|
||||
@ -248,4 +249,12 @@ public class FunctionalCaseController {
|
||||
StringUtils.isNotBlank(request.getSortString()) ? request.getSortString() : "create_time desc");
|
||||
return PageUtils.setPageInfo(page, functionalCaseService.operationHistoryList(request));
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/export/excel")
|
||||
@Operation(summary = "用例管理-功能用例-excel导出")
|
||||
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_EXPORT)
|
||||
public void testCaseExport(@Validated @RequestBody FunctionalCaseExportRequest request) {
|
||||
functionalCaseFileService.exportFunctionalCaseZip(request);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
package io.metersphere.functional.excel.constants;
|
||||
|
||||
public enum FunctionalCaseExportOtherField {
|
||||
|
||||
CREATE_USER("createUser"),
|
||||
CREATE_TIME("createTime"),
|
||||
UPDATE_USER("updateUser"),
|
||||
UPDATE_TIME("updateTime"),
|
||||
REVIEW_STATUS("reviewStatus"),
|
||||
LAST_EXECUTE_RESULT("lastExecuteResult"),
|
||||
CASE_COMMENT("caseComment"),
|
||||
EXECUTE_COMMENT("executeComment"),
|
||||
REVIEW_COMMENT("reviewComment");
|
||||
|
||||
private String value;
|
||||
|
||||
FunctionalCaseExportOtherField(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package io.metersphere.functional.excel.converter;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public enum FunctionalCaseExecuteStatus {
|
||||
|
||||
PENDING("case.execute.status.pending", 1),
|
||||
SUCCESS("case.minder.status.success", 2),
|
||||
BLOCKED("case.minder.status.blocked", 3),
|
||||
ERROR("case.minder.status.error", 4);
|
||||
|
||||
private String i18nKey;
|
||||
private Integer order;
|
||||
|
||||
FunctionalCaseExecuteStatus(String i18nKey, int order) {
|
||||
this.i18nKey = i18nKey;
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
public String getI18nKey() {
|
||||
return i18nKey;
|
||||
}
|
||||
|
||||
public Integer getOrder() {
|
||||
return order;
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package io.metersphere.functional.excel.converter;
|
||||
|
||||
import io.metersphere.functional.domain.CaseReviewHistory;
|
||||
import io.metersphere.functional.domain.FunctionalCase;
|
||||
import io.metersphere.functional.domain.FunctionalCaseComment;
|
||||
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
|
||||
import io.metersphere.sdk.util.DateUtils;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class FunctionalCaseExportCaseCommentConverter implements FunctionalCaseExportConverter {
|
||||
|
||||
@Override
|
||||
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
|
||||
if (caseCommentMap.containsKey(functionalCase.getId())) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
String template = Translator.get("functional_case_comment_template");
|
||||
List<FunctionalCaseComment> caseComments = caseCommentMap.get(functionalCase.getId());
|
||||
caseComments.forEach(item -> {
|
||||
String updateTime = DateUtils.getTimeString(item.getUpdateTime());
|
||||
String content = item.getContent();
|
||||
result.append(String.format(template, item.getCreateUser(), updateTime, content));
|
||||
});
|
||||
return result.toString();
|
||||
}
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package io.metersphere.functional.excel.converter;
|
||||
|
||||
import io.metersphere.functional.domain.CaseReviewHistory;
|
||||
import io.metersphere.functional.domain.FunctionalCase;
|
||||
import io.metersphere.functional.domain.FunctionalCaseComment;
|
||||
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 功能用例导出时解析其他字段对应的列
|
||||
*
|
||||
* @author wx
|
||||
*/
|
||||
public interface FunctionalCaseExportConverter {
|
||||
|
||||
String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap);
|
||||
|
||||
default String getFromMapOfNullable(Map<String, String> map, String key) {
|
||||
if (StringUtils.isNotBlank(key)) {
|
||||
return map.get(key);
|
||||
}
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
|
||||
default String getFromMapOfNullableWithTranslate(Map<String, String> map, String key) {
|
||||
String value = getFromMapOfNullable(map, key);
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
return Translator.get(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package io.metersphere.functional.excel.converter;
|
||||
|
||||
import io.metersphere.functional.excel.constants.FunctionalCaseExportOtherField;
|
||||
import io.metersphere.sdk.util.LogUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class FunctionalCaseExportConverterFactory {
|
||||
|
||||
public static Map<String, FunctionalCaseExportConverter> getConverters(List<String> keys) {
|
||||
Map<String, FunctionalCaseExportConverter> converterMapResult = new HashMap<>();
|
||||
try {
|
||||
HashMap<String, Class<? extends FunctionalCaseExportConverter>> converterMap = getConverterMap();
|
||||
for (String key : keys) {
|
||||
Class<? extends FunctionalCaseExportConverter> clazz = converterMap.get(key);
|
||||
if (clazz != null) {
|
||||
converterMapResult.put(key, clazz.getDeclaredConstructor().newInstance());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.error(e);
|
||||
}
|
||||
return converterMapResult;
|
||||
}
|
||||
|
||||
public static FunctionalCaseExportConverter getConverter(String key) {
|
||||
try {
|
||||
Class<? extends FunctionalCaseExportConverter> clazz = getConverterMap().get(key);
|
||||
if (clazz != null) {
|
||||
return clazz.getDeclaredConstructor().newInstance();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.error(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static HashMap<String, Class<? extends FunctionalCaseExportConverter>> getConverterMap() {
|
||||
return new HashMap<>() {{
|
||||
put(FunctionalCaseExportOtherField.CREATE_USER.getValue(), FunctionalCaseExportCreateUserConverter.class);
|
||||
put(FunctionalCaseExportOtherField.CREATE_TIME.getValue(), FunctionalCaseExportCreateTimeConverter.class);
|
||||
put(FunctionalCaseExportOtherField.UPDATE_USER.getValue(), FunctionalCaseExportUpdateUserConverter.class);
|
||||
put(FunctionalCaseExportOtherField.UPDATE_TIME.getValue(), FunctionalCaseExportUpdateTimeConverter.class);
|
||||
put(FunctionalCaseExportOtherField.REVIEW_STATUS.getValue(), FunctionalCaseExportReviewStatusConverter.class);
|
||||
put(FunctionalCaseExportOtherField.LAST_EXECUTE_RESULT.getValue(), FunctionalCaseExportExecuteStatusConverter.class);
|
||||
put(FunctionalCaseExportOtherField.CASE_COMMENT.getValue(), FunctionalCaseExportCaseCommentConverter.class);
|
||||
put(FunctionalCaseExportOtherField.EXECUTE_COMMENT.getValue(), FunctionalCaseExportExecuteCommentConverter.class);
|
||||
put(FunctionalCaseExportOtherField.REVIEW_COMMENT.getValue(), FunctionalCaseExportReviewCommentConverter.class);
|
||||
}};
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package io.metersphere.functional.excel.converter;
|
||||
|
||||
import io.metersphere.functional.domain.CaseReviewHistory;
|
||||
import io.metersphere.functional.domain.FunctionalCase;
|
||||
import io.metersphere.functional.domain.FunctionalCaseComment;
|
||||
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
|
||||
import io.metersphere.sdk.util.DateUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class FunctionalCaseExportCreateTimeConverter implements FunctionalCaseExportConverter {
|
||||
|
||||
@Override
|
||||
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
|
||||
return DateUtils.getTimeString(functionalCase.getCreateTime());
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package io.metersphere.functional.excel.converter;
|
||||
|
||||
import io.metersphere.functional.domain.CaseReviewHistory;
|
||||
import io.metersphere.functional.domain.FunctionalCase;
|
||||
import io.metersphere.functional.domain.FunctionalCaseComment;
|
||||
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
|
||||
import io.metersphere.project.service.ProjectApplicationService;
|
||||
import io.metersphere.sdk.util.CommonBeanFactory;
|
||||
import io.metersphere.system.domain.User;
|
||||
import io.metersphere.system.utils.SessionUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class FunctionalCaseExportCreateUserConverter implements FunctionalCaseExportConverter {
|
||||
|
||||
public Map<String, String> userMap = new HashMap<>();
|
||||
|
||||
public FunctionalCaseExportCreateUserConverter() {
|
||||
ProjectApplicationService projectApplicationService = CommonBeanFactory.getBean(ProjectApplicationService.class);
|
||||
List<User> memberOption = projectApplicationService.getProjectUserList(SessionUtils.getCurrentProjectId());
|
||||
memberOption.forEach(option -> userMap.put(option.getId(), option.getName()));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
|
||||
return getFromMapOfNullable(userMap, functionalCase.getCreateUser());
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package io.metersphere.functional.excel.converter;
|
||||
|
||||
import io.metersphere.functional.domain.CaseReviewHistory;
|
||||
import io.metersphere.functional.domain.FunctionalCase;
|
||||
import io.metersphere.functional.domain.FunctionalCaseComment;
|
||||
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
|
||||
import io.metersphere.sdk.util.DateUtils;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class FunctionalCaseExportExecuteCommentConverter implements FunctionalCaseExportConverter {
|
||||
|
||||
private Map<String, String> executeStatusMap = new HashMap<>();
|
||||
|
||||
public FunctionalCaseExportExecuteCommentConverter() {
|
||||
for (FunctionalCaseExecuteStatus value : FunctionalCaseExecuteStatus.values()) {
|
||||
executeStatusMap.put(value.name(), value.getI18nKey());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
|
||||
if (executeCommentMap.containsKey(functionalCase.getId())) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
String template = Translator.get("functional_case_execute_comment_template");
|
||||
List<TestPlanCaseExecuteHistory> executeComment = executeCommentMap.get(functionalCase.getId());
|
||||
executeComment.forEach(item -> {
|
||||
String status = getFromMapOfNullableWithTranslate(executeStatusMap, item.getStatus());
|
||||
String createTime = DateUtils.getTimeString(item.getCreateTime());
|
||||
String content = new String(item.getContent() == null ? new byte[0] : item.getContent(), StandardCharsets.UTF_8);
|
||||
result.append(String.format(template, item.getCreateUser(), status, createTime, content));
|
||||
});
|
||||
}
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package io.metersphere.functional.excel.converter;
|
||||
|
||||
import io.metersphere.functional.domain.CaseReviewHistory;
|
||||
import io.metersphere.functional.domain.FunctionalCase;
|
||||
import io.metersphere.functional.domain.FunctionalCaseComment;
|
||||
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class FunctionalCaseExportExecuteStatusConverter implements FunctionalCaseExportConverter {
|
||||
|
||||
private Map<String, String> executeStatusMap = new HashMap<>();
|
||||
|
||||
public FunctionalCaseExportExecuteStatusConverter() {
|
||||
for (FunctionalCaseExecuteStatus value : FunctionalCaseExecuteStatus.values()) {
|
||||
executeStatusMap.put(value.name(), value.getI18nKey());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
|
||||
return getFromMapOfNullableWithTranslate(executeStatusMap, functionalCase.getLastExecuteResult());
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package io.metersphere.functional.excel.converter;
|
||||
|
||||
import io.metersphere.functional.domain.CaseReviewHistory;
|
||||
import io.metersphere.functional.domain.FunctionalCase;
|
||||
import io.metersphere.functional.domain.FunctionalCaseComment;
|
||||
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
|
||||
import io.metersphere.sdk.util.DateUtils;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class FunctionalCaseExportReviewCommentConverter implements FunctionalCaseExportConverter {
|
||||
|
||||
private Map<String, String> reviewStatusMap = new HashMap<>();
|
||||
|
||||
public FunctionalCaseExportReviewCommentConverter() {
|
||||
for (FunctionalCaseReviewStatus value : FunctionalCaseReviewStatus.values()) {
|
||||
reviewStatusMap.put(value.name(), value.getI18nKey());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
|
||||
if (reviewCommentMap.containsKey(functionalCase.getId())) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
String template = Translator.get("functional_case_review_comment_template");
|
||||
List<CaseReviewHistory> reviewComent = reviewCommentMap.get(functionalCase.getId());
|
||||
reviewComent.forEach(item -> {
|
||||
String status = getFromMapOfNullableWithTranslate(reviewStatusMap, item.getStatus());
|
||||
String createTime = DateUtils.getTimeString(item.getCreateTime());
|
||||
String content = new String(item.getContent() == null ? new byte[0] : item.getContent(), StandardCharsets.UTF_8);
|
||||
result.append(String.format(template, item.getCreateUser(), status, createTime, content));
|
||||
});
|
||||
}
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package io.metersphere.functional.excel.converter;
|
||||
|
||||
import io.metersphere.functional.domain.CaseReviewHistory;
|
||||
import io.metersphere.functional.domain.FunctionalCase;
|
||||
import io.metersphere.functional.domain.FunctionalCaseComment;
|
||||
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class FunctionalCaseExportReviewStatusConverter implements FunctionalCaseExportConverter {
|
||||
|
||||
private Map<String, String> caseReviewStatusMap = new HashMap<>();
|
||||
|
||||
public FunctionalCaseExportReviewStatusConverter() {
|
||||
for (FunctionalCaseReviewStatus value : FunctionalCaseReviewStatus.values()) {
|
||||
caseReviewStatusMap.put(value.name(), value.getI18nKey());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
|
||||
return getFromMapOfNullableWithTranslate(caseReviewStatusMap, functionalCase.getReviewStatus());
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package io.metersphere.functional.excel.converter;
|
||||
|
||||
import io.metersphere.functional.domain.CaseReviewHistory;
|
||||
import io.metersphere.functional.domain.FunctionalCase;
|
||||
import io.metersphere.functional.domain.FunctionalCaseComment;
|
||||
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
|
||||
import io.metersphere.sdk.util.DateUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class FunctionalCaseExportUpdateTimeConverter implements FunctionalCaseExportConverter {
|
||||
|
||||
@Override
|
||||
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
|
||||
return DateUtils.getTimeString(functionalCase.getUpdateTime());
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package io.metersphere.functional.excel.converter;
|
||||
|
||||
import io.metersphere.functional.domain.CaseReviewHistory;
|
||||
import io.metersphere.functional.domain.FunctionalCase;
|
||||
import io.metersphere.functional.domain.FunctionalCaseComment;
|
||||
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class FunctionalCaseExportUpdateUserConverter extends FunctionalCaseExportCreateUserConverter {
|
||||
|
||||
|
||||
@Override
|
||||
public String parse(FunctionalCase functionalCase, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap) {
|
||||
return getFromMapOfNullable(userMap, functionalCase.getUpdateUser());
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package io.metersphere.functional.excel.converter;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public enum FunctionalCaseReviewStatus {
|
||||
|
||||
UN_REVIEWED("case.review.status.un_reviewed", 1),
|
||||
UNDER_REVIEWED("case.review.status.under_reviewed", 2),
|
||||
PASS("case.review.status.pass", 3),
|
||||
UN_PASS("case.review.status.un_pass", 4),
|
||||
RE_REVIEWED("case.review.status.re_reviewed", 5);
|
||||
|
||||
private String i18nKey;
|
||||
private Integer order;
|
||||
|
||||
FunctionalCaseReviewStatus(String i18nKey, int order) {
|
||||
this.i18nKey = i18nKey;
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
public String getI18nKey() {
|
||||
return i18nKey;
|
||||
}
|
||||
|
||||
public Integer getOrder() {
|
||||
return order;
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package io.metersphere.functional.excel.domain;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelIgnore;
|
||||
import com.alibaba.excel.metadata.data.WriteCellData;
|
||||
import io.metersphere.functional.excel.constants.FunctionalCaseImportFiled;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
import lombok.Getter;
|
||||
@ -40,6 +41,9 @@ public class FunctionalCaseExcelData {
|
||||
@ExcelIgnore
|
||||
Map<String, String> otherFields;
|
||||
|
||||
@ExcelIgnore
|
||||
private WriteCellData<String> hyperLinkName;
|
||||
|
||||
/**
|
||||
* 合并文本描述
|
||||
*/
|
||||
|
@ -0,0 +1,21 @@
|
||||
package io.metersphere.functional.excel.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
@Data
|
||||
public class FunctionalCaseHeader implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "字段英文名称")
|
||||
private String id;
|
||||
@Schema(description = "字段中文名称")
|
||||
private String name;
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package io.metersphere.functional.excel.handler;
|
||||
|
||||
import com.alibaba.excel.write.handler.RowWriteHandler;
|
||||
import com.alibaba.excel.write.handler.context.RowWriteHandlerContext;
|
||||
import io.metersphere.functional.excel.constants.FunctionalCaseImportFiled;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public class FunctionCaseMergeWriteHandler implements RowWriteHandler {
|
||||
|
||||
/**
|
||||
* 存储需要合并单元格的信息,key 是需要合并的第一条的行号,值是需要合并多少行
|
||||
*/
|
||||
Map<Integer, Integer> rowMergeInfo;
|
||||
List<List<String>> headList;
|
||||
int textDescriptionRowIndex;
|
||||
int expectedResultRowIndex;
|
||||
|
||||
public FunctionCaseMergeWriteHandler(Map<Integer, Integer> rowMergeInfo, List<List<String>> headList) {
|
||||
this.rowMergeInfo = rowMergeInfo;
|
||||
this.headList = headList;
|
||||
for (int i = 0; i < headList.size(); i++) {
|
||||
List<String> list = headList.get(i);
|
||||
for (String head : list) {
|
||||
if (FunctionalCaseImportFiled.TEXT_DESCRIPTION.containsHead(head)) {
|
||||
textDescriptionRowIndex = i;
|
||||
} else if (FunctionalCaseImportFiled.EXPECTED_RESULT.containsHead(head)) {
|
||||
expectedResultRowIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterRowDispose(RowWriteHandlerContext context) {
|
||||
if (context.getHead() || context.getRelativeRowIndex() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Integer mergeCount = rowMergeInfo.get(context.getRowIndex());
|
||||
|
||||
if (mergeCount == null || mergeCount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < headList.size(); i++) {
|
||||
// 除了描述其他数据合并多行
|
||||
if (i != textDescriptionRowIndex && i != expectedResultRowIndex) {
|
||||
CellRangeAddress cellRangeAddress =
|
||||
new CellRangeAddress(context.getRowIndex(), context.getRowIndex() + mergeCount - 1, i, i);
|
||||
context.getWriteSheetHolder().getSheet().addMergedRegionUnsafe(cellRangeAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ import java.util.Map;
|
||||
*/
|
||||
public class FunctionCaseTemplateWriteHandler implements RowWriteHandler {
|
||||
|
||||
Map<String, List<String>> caseLevelAndStatusValueMap;
|
||||
Map<String, List<String>> customFieldOptionsMap;
|
||||
|
||||
private Sheet sheet;
|
||||
private Drawing<?> drawingPatriarch;
|
||||
@ -37,9 +37,9 @@ public class FunctionCaseTemplateWriteHandler implements RowWriteHandler {
|
||||
private Map<String, TemplateCustomFieldDTO> customField;
|
||||
private Map<String, Integer> fieldMap = new HashMap<>();
|
||||
|
||||
public FunctionCaseTemplateWriteHandler(List<List<String>> headList, Map<String, List<String>> caseLevelAndStatusValueMap, Map<String, TemplateCustomFieldDTO> customFieldMap) {
|
||||
public FunctionCaseTemplateWriteHandler(List<List<String>> headList, Map<String, List<String>> customFieldOptionsMap, Map<String, TemplateCustomFieldDTO> customFieldMap) {
|
||||
initIndex(headList);
|
||||
this.caseLevelAndStatusValueMap = caseLevelAndStatusValueMap;
|
||||
this.customFieldOptionsMap = customFieldOptionsMap;
|
||||
this.customField = customFieldMap;
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ public class FunctionCaseTemplateWriteHandler implements RowWriteHandler {
|
||||
//自定义字段
|
||||
if (customField.containsKey(entry.getKey())) {
|
||||
TemplateCustomFieldDTO templateCustomFieldDTO = customField.get(entry.getKey());
|
||||
List<String> strings = caseLevelAndStatusValueMap.get(entry.getKey());
|
||||
List<String> strings = customFieldOptionsMap.get(entry.getKey());
|
||||
if (StringUtils.equalsAnyIgnoreCase(templateCustomFieldDTO.getType(), CustomFieldType.MULTIPLE_MEMBER.name(), CustomFieldType.MEMBER.name())) {
|
||||
if (templateCustomFieldDTO.getRequired()) {
|
||||
setComment(fieldMap.get(entry.getKey()), Translator.get("required").concat(",").concat(Translator.get("excel.template.member")));
|
||||
@ -104,13 +104,13 @@ public class FunctionCaseTemplateWriteHandler implements RowWriteHandler {
|
||||
} else {
|
||||
if (templateCustomFieldDTO.getRequired()) {
|
||||
if (CollectionUtils.isNotEmpty(strings)) {
|
||||
setComment(fieldMap.get(entry.getKey()), Translator.get("required").concat(":").concat(Translator.get("options")).concat(JSON.toJSONString(caseLevelAndStatusValueMap.get(entry.getKey()))));
|
||||
setComment(fieldMap.get(entry.getKey()), Translator.get("required").concat(":").concat(Translator.get("options")).concat(JSON.toJSONString(customFieldOptionsMap.get(entry.getKey()))));
|
||||
} else {
|
||||
setComment(fieldMap.get(entry.getKey()), Translator.get("required"));
|
||||
}
|
||||
} else {
|
||||
if (CollectionUtils.isNotEmpty(strings)) {
|
||||
setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.not_required").concat(":").concat(Translator.get("options")).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(customFieldOptionsMap.get(entry.getKey()))));
|
||||
} else {
|
||||
setComment(fieldMap.get(entry.getKey()), Translator.get("excel.template.not_required"));
|
||||
}
|
||||
|
@ -71,4 +71,8 @@ public abstract class AbstractCustomFieldValidator {
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
public Object parse2Value(String value, TemplateCustomFieldDTO customField) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import java.util.stream.Collectors;
|
||||
public class CustomFieldMemberValidator extends AbstractCustomFieldValidator {
|
||||
|
||||
protected Map<String, String> userIdMap;
|
||||
protected Map<String, String> userEmailMap;
|
||||
protected Map<String, String> userNameMap;
|
||||
|
||||
public CustomFieldMemberValidator() {
|
||||
@ -31,9 +32,12 @@ public class CustomFieldMemberValidator extends AbstractCustomFieldValidator {
|
||||
.collect(
|
||||
Collectors.toMap(user -> user.getId().toLowerCase(), User::getId)
|
||||
);
|
||||
userEmailMap = new HashMap<>();
|
||||
memberOption.stream()
|
||||
.forEach(user -> userEmailMap.put(user.getEmail().toLowerCase(), user.getId()));
|
||||
userNameMap = new HashMap<>();
|
||||
memberOption.stream()
|
||||
.forEach(user -> userNameMap.put(user.getEmail().toLowerCase(), user.getId()));
|
||||
.forEach(user -> userNameMap.put(user.getId(), user.getName().toLowerCase()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -43,7 +47,7 @@ public class CustomFieldMemberValidator extends AbstractCustomFieldValidator {
|
||||
return;
|
||||
}
|
||||
value = value.toLowerCase();
|
||||
if (userIdMap.containsKey(value) || userNameMap.containsKey(value)) {
|
||||
if (userIdMap.containsKey(value) || userEmailMap.containsKey(value)) {
|
||||
return;
|
||||
}
|
||||
throw new CustomFieldValidateException(String.format(Translator.get("custom_field_member_tip"), customField.getFieldName()));
|
||||
@ -55,9 +59,19 @@ public class CustomFieldMemberValidator extends AbstractCustomFieldValidator {
|
||||
if (userIdMap.containsKey(keyOrValue)) {
|
||||
return userIdMap.get(keyOrValue);
|
||||
}
|
||||
if (userEmailMap.containsKey(keyOrValue)) {
|
||||
return userEmailMap.get(keyOrValue);
|
||||
}
|
||||
return keyOrValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parse2Value(String keyOrValue, TemplateCustomFieldDTO customField) {
|
||||
keyOrValue = keyOrValue.toLowerCase();
|
||||
if (userNameMap.containsKey(keyOrValue)) {
|
||||
return userNameMap.get(keyOrValue);
|
||||
}
|
||||
return keyOrValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ public class CustomFieldMultipleMemberValidator extends CustomFieldMemberValidat
|
||||
|
||||
for (String item : parse2Array(customField.getFieldName(), value)) {
|
||||
item = item.toLowerCase();
|
||||
if (!userIdMap.containsKey(item) && !userNameMap.containsKey(item)) {
|
||||
if (!userIdMap.containsKey(item) && !userEmailMap.containsKey(item)) {
|
||||
CustomFieldValidateException.throwException(String.format(Translator.get("custom_field_member_tip"), customField.getFieldName()));
|
||||
}
|
||||
}
|
||||
@ -42,10 +42,27 @@ public class CustomFieldMultipleMemberValidator extends CustomFieldMemberValidat
|
||||
if (userIdMap.containsKey(item)) {
|
||||
keyOrValues.set(i, userIdMap.get(item));
|
||||
}
|
||||
if (userEmailMap.containsKey(item)) {
|
||||
keyOrValues.set(i, userEmailMap.get(item));
|
||||
}
|
||||
}
|
||||
return JSON.toJSONString(keyOrValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parse2Value(String keyOrValuesStr, TemplateCustomFieldDTO customField) {
|
||||
if (StringUtils.isBlank(keyOrValuesStr)) {
|
||||
return JSON.toJSONString(new ArrayList<>());
|
||||
}
|
||||
List<String> keyOrValues = parse2Array(keyOrValuesStr);
|
||||
|
||||
for (int i = 0; i < keyOrValues.size(); i++) {
|
||||
String item = keyOrValues.get(i).toLowerCase();
|
||||
if (userNameMap.containsKey(item)) {
|
||||
keyOrValues.set(i, userNameMap.get(item));
|
||||
}
|
||||
}
|
||||
return JSON.toJSONString(keyOrValues);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,13 +4,12 @@ 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.domain.CustomFieldOption;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
@ -48,4 +47,21 @@ public class CustomFieldMultipleSelectValidator extends CustomFieldSelectValidat
|
||||
}
|
||||
return JSON.toJSONString(keyOrValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parse2Value(String keyOrValuesStr, TemplateCustomFieldDTO customField) {
|
||||
Map<String, String> optionValueMap = customField.getOptions().stream().collect(Collectors.toMap(CustomFieldOption::getFieldId, CustomFieldOption::getValue));
|
||||
if (StringUtils.isBlank(keyOrValuesStr)) {
|
||||
return JSON.toJSONString(new ArrayList<>());
|
||||
}
|
||||
List<String> keyOrValues = parse2Array(keyOrValuesStr);
|
||||
for (int i = 0; i < keyOrValues.size(); i++) {
|
||||
String item = keyOrValues.get(i);
|
||||
if (optionValueMap.containsKey(item)) {
|
||||
keyOrValues.set(i, optionValueMap.get(item));
|
||||
}
|
||||
}
|
||||
return JSON.toJSONString(keyOrValues);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -51,4 +51,14 @@ public class CustomFieldMultipleTextValidator extends AbstractCustomFieldValidat
|
||||
|
||||
return JSON.toJSONString(keyOrValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parse2Value(String keyOrValuesStr, TemplateCustomFieldDTO customField) {
|
||||
if (StringUtils.isBlank(keyOrValuesStr)) {
|
||||
return JSON.toJSONString(new ArrayList<>());
|
||||
}
|
||||
List<String> keyOrValues = parse2Array(keyOrValuesStr);
|
||||
|
||||
return JSON.toJSONString(keyOrValues);
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,15 @@ public class CustomFieldSelectValidator extends AbstractCustomFieldValidator {
|
||||
return keyOrValuesStr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parse2Value(String keyOrValuesStr, TemplateCustomFieldDTO customField) {
|
||||
Map<String, String> optionValueMap = customField.getOptions().stream().collect(Collectors.toMap(CustomFieldOption::getFieldId, CustomFieldOption::getValue));
|
||||
if (optionValueMap.containsKey(keyOrValuesStr)) {
|
||||
return optionValueMap.get(keyOrValuesStr);
|
||||
}
|
||||
return keyOrValuesStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自定义字段的选项值和key
|
||||
* 存储到缓存中,增强导入时性能
|
||||
|
@ -0,0 +1,19 @@
|
||||
package io.metersphere.functional.mapper;
|
||||
|
||||
import io.metersphere.functional.domain.CaseReviewHistory;
|
||||
import io.metersphere.functional.domain.FunctionalCaseComment;
|
||||
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
public interface ExtFunctionalCaseCommentMapper {
|
||||
List<FunctionalCaseComment> getCaseComment(@Param("ids") List<String> ids);
|
||||
|
||||
List<TestPlanCaseExecuteHistory> getExecuteComment(@Param("ids") List<String> ids);
|
||||
|
||||
List<CaseReviewHistory> getReviewComment(@Param("ids") List<String> ids);
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="io.metersphere.functional.mapper.ExtFunctionalCaseCommentMapper">
|
||||
|
||||
<select id="getCaseComment" resultType="io.metersphere.functional.domain.FunctionalCaseComment">
|
||||
SELECT
|
||||
functional_case_comment.case_id,
|
||||
functional_case_comment.content,
|
||||
functional_case_comment.update_time,
|
||||
user.name as createUser
|
||||
FROM
|
||||
functional_case_comment
|
||||
INNER JOIN user ON functional_case_comment.create_user = user.id
|
||||
where functional_case_comment.case_id in
|
||||
<foreach collection="ids" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<select id="getExecuteComment" resultType="io.metersphere.plan.domain.TestPlanCaseExecuteHistory">
|
||||
SELECT
|
||||
test_plan_case_execute_history.case_id,
|
||||
test_plan_case_execute_history.content,
|
||||
test_plan_case_execute_history.create_time,
|
||||
user.name as createUser
|
||||
FROM
|
||||
test_plan_case_execute_history
|
||||
INNER JOIN user ON test_plan_case_execute_history.create_user = user.id
|
||||
where test_plan_case_execute_history.case_id in
|
||||
<foreach collection="ids" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
|
||||
<select id="getReviewComment" resultType="io.metersphere.functional.domain.CaseReviewHistory">
|
||||
SELECT
|
||||
case_review_history.case_id,
|
||||
case_review_history.content,
|
||||
case_review_history.create_time,
|
||||
user.name as createUser
|
||||
FROM
|
||||
case_review_history
|
||||
INNER JOIN user ON case_review_history.create_user = user.id
|
||||
where case_review_history.case_id in
|
||||
<foreach collection="ids" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
</mapper>
|
@ -0,0 +1,36 @@
|
||||
package io.metersphere.functional.request;
|
||||
|
||||
import io.metersphere.functional.dto.BaseFunctionalCaseBatchDTO;
|
||||
import io.metersphere.functional.excel.domain.FunctionalCaseHeader;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author wx
|
||||
*/
|
||||
@Data
|
||||
public class FunctionalCaseExportRequest extends BaseFunctionalCaseBatchDTO implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "系统字段", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<FunctionalCaseHeader> systemFields = new ArrayList<>();
|
||||
|
||||
@Schema(description = "自定义字段")
|
||||
private List<FunctionalCaseHeader> customFields = new ArrayList<>();
|
||||
|
||||
@Schema(description = "其他字段")
|
||||
private List<FunctionalCaseHeader> otherFields = new ArrayList<>();
|
||||
|
||||
@Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String projectId;
|
||||
|
||||
@Schema(description = "文件id")
|
||||
private String fileId;
|
||||
|
||||
}
|
@ -3,36 +3,72 @@ package io.metersphere.functional.service;
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.alibaba.excel.EasyExcelFactory;
|
||||
import com.alibaba.excel.enums.CellExtraTypeEnum;
|
||||
import com.alibaba.excel.metadata.data.HyperlinkData;
|
||||
import com.alibaba.excel.metadata.data.WriteCellData;
|
||||
import com.alibaba.excel.support.ExcelTypeEnum;
|
||||
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
|
||||
import com.alibaba.excel.write.metadata.style.WriteFont;
|
||||
import io.metersphere.functional.constants.FunctionalCaseTypeConstants;
|
||||
import io.metersphere.functional.domain.*;
|
||||
import io.metersphere.functional.dto.response.FunctionalCaseImportResponse;
|
||||
import io.metersphere.functional.excel.constants.FunctionalCaseImportFiled;
|
||||
import io.metersphere.functional.excel.converter.FunctionalCaseExportConverter;
|
||||
import io.metersphere.functional.excel.converter.FunctionalCaseExportConverterFactory;
|
||||
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.domain.FunctionalCaseHeader;
|
||||
import io.metersphere.functional.excel.handler.FunctionCaseMergeWriteHandler;
|
||||
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.excel.validate.AbstractCustomFieldValidator;
|
||||
import io.metersphere.functional.excel.validate.CustomFieldValidatorFactory;
|
||||
import io.metersphere.functional.mapper.ExtFunctionalCaseCommentMapper;
|
||||
import io.metersphere.functional.request.FunctionalCaseExportRequest;
|
||||
import io.metersphere.functional.request.FunctionalCaseImportRequest;
|
||||
import io.metersphere.functional.socket.ExportWebSocketHandler;
|
||||
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
|
||||
import io.metersphere.project.domain.Project;
|
||||
import io.metersphere.project.mapper.ExtBaseProjectVersionMapper;
|
||||
import io.metersphere.project.mapper.ProjectMapper;
|
||||
import io.metersphere.project.service.ProjectTemplateService;
|
||||
import io.metersphere.sdk.constants.TemplateScene;
|
||||
import io.metersphere.sdk.constants.*;
|
||||
import io.metersphere.sdk.dto.SocketMsgDTO;
|
||||
import io.metersphere.sdk.exception.MSException;
|
||||
import io.metersphere.sdk.util.LogUtils;
|
||||
import io.metersphere.sdk.util.Translator;
|
||||
import io.metersphere.sdk.file.FileRequest;
|
||||
import io.metersphere.sdk.util.*;
|
||||
import io.metersphere.system.domain.CustomFieldOption;
|
||||
import io.metersphere.system.domain.SystemParameter;
|
||||
import io.metersphere.system.dto.sdk.BaseTreeNode;
|
||||
import io.metersphere.system.dto.sdk.SessionUser;
|
||||
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
|
||||
import io.metersphere.system.dto.sdk.TemplateDTO;
|
||||
import io.metersphere.system.excel.utils.EasyExcelExporter;
|
||||
import io.metersphere.system.mapper.SystemParameterMapper;
|
||||
import io.metersphere.system.service.FileService;
|
||||
import io.metersphere.system.uid.IDGenerator;
|
||||
import io.metersphere.system.utils.ServiceUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.poi.ss.usermodel.Font;
|
||||
import org.apache.poi.ss.usermodel.IndexedColors;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.Serial;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@ -50,7 +86,22 @@ public class FunctionalCaseFileService {
|
||||
|
||||
@Resource
|
||||
private FunctionalCaseService functionalCaseService;
|
||||
|
||||
@Resource
|
||||
private FunctionalCaseModuleService functionalCaseModuleService;
|
||||
@Resource
|
||||
private FunctionalCaseCustomFieldService functionalCaseCustomFieldService;
|
||||
private static final String EXPORT_CASE_TMP_DIR = "tmp";
|
||||
private static final int EXPORT_CASE_MAX_COUNT = 2000;
|
||||
@Resource
|
||||
private ExtFunctionalCaseCommentMapper extFunctionalCaseCommentMapper;
|
||||
@Resource
|
||||
private ProjectMapper projectMapper;
|
||||
@Resource
|
||||
private FileService fileService;
|
||||
@Resource
|
||||
private FunctionalCaseLogService functionalCaseLogService;
|
||||
@Resource
|
||||
private SystemParameterMapper systemParameterMapper;
|
||||
|
||||
/**
|
||||
* 下载excel导入模板
|
||||
@ -105,10 +156,17 @@ public class FunctionalCaseFileService {
|
||||
for (String head : headList) {
|
||||
boolean isSystemField = false;
|
||||
for (FunctionalCaseImportFiled importFiled : importFields) {
|
||||
if (StringUtils.equals("name", importFiled.getValue()) && model.getHyperLinkName() != null) {
|
||||
fields.add(model.getHyperLinkName());
|
||||
isSystemField = true;
|
||||
break;
|
||||
}
|
||||
if (importFiled.containsHead(head)) {
|
||||
fields.add(importFiled.parseExcelDataValue(model));
|
||||
isSystemField = true;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
if (!isSystemField) {
|
||||
Object value = customDataMaps.get(head);
|
||||
@ -267,4 +325,408 @@ public class FunctionalCaseFileService {
|
||||
throw new MSException(Translator.get("check_import_excel_error"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param request
|
||||
* @param url
|
||||
*/
|
||||
@Async
|
||||
public void exportFunctionalCaseZip(FunctionalCaseExportRequest request) {
|
||||
File tmpDir = null;
|
||||
Project project = projectMapper.selectByPrimaryKey(request.getProjectId());
|
||||
try {
|
||||
tmpDir = new File(getClass().getClassLoader().getResource(StringUtils.EMPTY).getPath() +
|
||||
EXPORT_CASE_TMP_DIR + File.separatorChar + EXPORT_CASE_TMP_DIR + "_" + IDGenerator.nextStr());
|
||||
// 生成tmp随机目录
|
||||
MsFileUtils.deleteDir(tmpDir.getPath());
|
||||
tmpDir.mkdirs();
|
||||
// 生成EXCEL
|
||||
List<File> batchExcels = generateCaseExportExcel(tmpDir.getPath(), request, project);
|
||||
if (batchExcels.size() > 1) {
|
||||
// EXCEL -> ZIP (EXCEL数目大于1)
|
||||
File zipFile = CompressUtils.zipFilesToPath(tmpDir.getPath() + File.separatorChar + "Metersphere_case_" + project.getName() + ".zip", batchExcels);
|
||||
uploadFileToMinio(zipFile, request.getFileId());
|
||||
} else {
|
||||
// EXCEL (EXCEL数目等于1)
|
||||
File singeFile = batchExcels.get(0);
|
||||
uploadFileToMinio(singeFile, request.getFileId());
|
||||
}
|
||||
functionalCaseLogService.exportExcelLog(request);
|
||||
SocketMsgDTO socketMsgDTO = new SocketMsgDTO(request.getFileId(), "", MsgType.CONNECT.name(), MsgType.CONNECT.name());
|
||||
socketMsgDTO.setReportId(request.getFileId());
|
||||
ExportWebSocketHandler.sendMessageSingle(socketMsgDTO);
|
||||
} catch (Exception e) {
|
||||
LogUtils.error(e);
|
||||
throw new MSException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void uploadFileToMinio(File file, String fileId) {
|
||||
FileRequest fileRequest = new FileRequest();
|
||||
fileRequest.setFileName(file.getName());
|
||||
fileRequest.setFolder(DefaultRepositoryDir.getExportExcelTempDir() + "/" + fileId);
|
||||
fileRequest.setStorage(StorageType.MINIO.name());
|
||||
try {
|
||||
FileInputStream inputStream = new FileInputStream(file);
|
||||
fileService.upload(inputStream, fileRequest);
|
||||
} catch (Exception e) {
|
||||
throw new MSException("save file error");
|
||||
}
|
||||
}
|
||||
|
||||
private List<File> generateCaseExportExcel(String tmpZipPath, FunctionalCaseExportRequest request, Project project) {
|
||||
List<File> tmpExportExcelList = new ArrayList<>();
|
||||
//excel表头
|
||||
List<List<String>> headList = getFunctionalCaseExportHeads(request);
|
||||
//获取导出的ids集合
|
||||
List<String> ids = functionalCaseService.doSelectIds(request, request.getProjectId());
|
||||
if (CollectionUtils.isEmpty(ids)) {
|
||||
return tmpExportExcelList;
|
||||
}
|
||||
//获取当前项目下默认模板的自定义字段属性
|
||||
List<TemplateCustomFieldDTO> customFields = getCustomFields(request.getProjectId());
|
||||
//默认字段+自定义字段的 options集合
|
||||
Map<String, List<String>> customFieldOptionsMap = getCustomFieldOptionsMap(customFields);
|
||||
Map<String, TemplateCustomFieldDTO> customFieldMap = customFields.stream().collect(Collectors.toMap(TemplateCustomFieldDTO::getFieldName, templateCustomFieldDTO -> templateCustomFieldDTO));
|
||||
|
||||
//获取url
|
||||
SystemParameter parameter = systemParameterMapper.selectByPrimaryKey(ParamConstants.BASE.URL.getValue());
|
||||
|
||||
//获取用例模块map
|
||||
Map<String, String> moduleMap = getModuleMap(request.getProjectId());
|
||||
//2000条,分批导出
|
||||
AtomicInteger count = new AtomicInteger(0);
|
||||
SubListUtils.dealForSubList(ids, EXPORT_CASE_MAX_COUNT, (subIds) -> {
|
||||
count.getAndIncrement();
|
||||
// 生成writeHandler
|
||||
Map<Integer, Integer> rowMergeInfo = new HashMap<>();
|
||||
FunctionCaseMergeWriteHandler writeHandler = new FunctionCaseMergeWriteHandler(rowMergeInfo, headList);
|
||||
//表头备注信息
|
||||
FunctionCaseTemplateWriteHandler handler = new FunctionCaseTemplateWriteHandler(headList, customFieldOptionsMap, customFieldMap);
|
||||
|
||||
//获取导出数据
|
||||
List<FunctionalCaseExcelData> excelData = parseCaseData2ExcelData(subIds, rowMergeInfo, request, customFields, moduleMap, parameter.getParamValue());
|
||||
List<List<Object>> data = parseExcelData2List(headList, excelData);
|
||||
|
||||
File createFile = new File(tmpZipPath + File.separatorChar + "Metersphere_case_" + project.getName() + count.get() + ".xlsx");
|
||||
if (!createFile.exists()) {
|
||||
try {
|
||||
createFile.createNewFile();
|
||||
} catch (IOException e) {
|
||||
throw new MSException(e);
|
||||
}
|
||||
}
|
||||
//生成临时EXCEL
|
||||
EasyExcel.write(createFile)
|
||||
.head(Optional.ofNullable(headList).orElse(new ArrayList<>()))
|
||||
.registerWriteHandler(handler)
|
||||
.registerWriteHandler(writeHandler)
|
||||
.registerWriteHandler(FunctionCaseTemplateWriteHandler.getHorizontalWrapStrategy())
|
||||
.excelType(ExcelTypeEnum.XLSX).sheet(Translator.get("test_case_import_template_sheet")).doWrite(data);
|
||||
tmpExportExcelList.add(createFile);
|
||||
});
|
||||
|
||||
return tmpExportExcelList;
|
||||
}
|
||||
|
||||
|
||||
private List<FunctionalCaseExcelData> parseCaseData2ExcelData(List<String> ids, Map<Integer, Integer> rowMergeInfo, FunctionalCaseExportRequest request, List<TemplateCustomFieldDTO> customFields, Map<String, String> moduleMap, String url) {
|
||||
List<FunctionalCaseExcelData> list = new ArrayList<>();
|
||||
//基础信息
|
||||
Map<String, FunctionalCase> functionalCaseMap = functionalCaseService.copyBaseInfo(request.getProjectId(), ids);
|
||||
//大字段
|
||||
Map<String, FunctionalCaseBlob> functionalCaseBlobMap = functionalCaseService.copyBlobInfo(ids);
|
||||
//自定义字段
|
||||
Map<String, List<FunctionalCaseCustomField>> customFieldMap = functionalCaseCustomFieldService.getCustomFieldMapByCaseIds(ids);
|
||||
//用例评论
|
||||
Map<String, List<FunctionalCaseComment>> caseCommentMap = getCaseComment(ids);
|
||||
//执行评论
|
||||
Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap = getExecuteComment(ids);
|
||||
//评审评论
|
||||
Map<String, List<CaseReviewHistory>> reviewCommentMap = getReviewComment(ids);
|
||||
|
||||
ids.forEach(id -> {
|
||||
List<String> textDescriptionList = new ArrayList<>();
|
||||
List<String> expectedResultList = new ArrayList<>();
|
||||
//构建基本参数
|
||||
FunctionalCaseExcelData data = new FunctionalCaseExcelData();
|
||||
FunctionalCase functionalCase = functionalCaseMap.get(id);
|
||||
FunctionalCaseBlob functionalCaseBlob = functionalCaseBlobMap.get(id);
|
||||
//构建基本参数
|
||||
buildBaseField(data, functionalCase, functionalCaseBlob, moduleMap, textDescriptionList, expectedResultList, url);
|
||||
//构建自定义字段
|
||||
buildExportCustomField(customFields, customFieldMap.get(id), data, request);
|
||||
//构建其他字段
|
||||
buildExportOtherField(functionalCase, data, caseCommentMap, executeCommentMap, reviewCommentMap, request);
|
||||
validateExportTextField(data);
|
||||
if (CollectionUtils.isNotEmpty(textDescriptionList)) {
|
||||
// 如果有多条步骤则添加多条数据,之后合并单元格
|
||||
buildExportMergeData(rowMergeInfo, list, textDescriptionList, expectedResultList, data);
|
||||
} else {
|
||||
list.add(data);
|
||||
}
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建基本参数
|
||||
*
|
||||
* @param data
|
||||
* @param functionalCase
|
||||
* @param functionalCaseBlob
|
||||
*/
|
||||
private void buildBaseField(FunctionalCaseExcelData data, FunctionalCase functionalCase, FunctionalCaseBlob functionalCaseBlob, Map<String, String> moduleMap, List<String> textDescriptionList, List<String> expectedResultList, String url) {
|
||||
data.setNum(functionalCase.getNum().toString());
|
||||
data.setModule(moduleMap.get(functionalCase.getModuleId()));
|
||||
data.setTags(functionalCase.getTags().toString());
|
||||
//构建步骤
|
||||
buildExportStep(data, functionalCaseBlob, functionalCase.getCaseEditType(), textDescriptionList, expectedResultList);
|
||||
data.setPrerequisite(new String(functionalCaseBlob.getPrerequisite() == null ? new byte[0] : functionalCaseBlob.getPrerequisite(), StandardCharsets.UTF_8));
|
||||
|
||||
// 设置超链接
|
||||
WriteCellData<String> hyperlink = new WriteCellData<>(functionalCase.getName());
|
||||
data.setHyperLinkName(hyperlink);
|
||||
HyperlinkData hyperlinkData = new HyperlinkData();
|
||||
hyperlink.setHyperlinkData(hyperlinkData);
|
||||
|
||||
WriteFont writeFont = new WriteFont();
|
||||
writeFont.setUnderline(Font.U_SINGLE);
|
||||
writeFont.setColor(IndexedColors.BLUE.getIndex());
|
||||
WriteCellStyle writeCellStyle = new WriteCellStyle();
|
||||
writeCellStyle.setWriteFont(writeFont);
|
||||
hyperlink.setWriteCellStyle(writeCellStyle);
|
||||
|
||||
hyperlinkData.setAddress(url + "/functional/case/detail/" + functionalCase.getId());
|
||||
hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 合并单元格
|
||||
*
|
||||
* @param rowMergeInfo
|
||||
* @param list
|
||||
* @param textDescriptionList
|
||||
* @param expectedResultList
|
||||
* @param data
|
||||
*/
|
||||
@NotNull
|
||||
private void buildExportMergeData(Map<Integer, Integer> rowMergeInfo, List<FunctionalCaseExcelData> list, List<String> textDescriptionList, List<String> expectedResultList, FunctionalCaseExcelData data) {
|
||||
for (int i = 0; i < textDescriptionList.size(); i++) {
|
||||
FunctionalCaseExcelData excelData;
|
||||
if (i == 0) {
|
||||
// 第一行存全量元素
|
||||
excelData = data;
|
||||
if (textDescriptionList.size() > 1) {
|
||||
// 保存合并单元格的下标和数量
|
||||
rowMergeInfo.put(list.size() + 1, textDescriptionList.size());
|
||||
}
|
||||
} else {
|
||||
// 之后的行只存步骤
|
||||
excelData = new FunctionalCaseExcelData();
|
||||
}
|
||||
excelData.setTextDescription(textDescriptionList.get(i));
|
||||
excelData.setExpectedResult(expectedResultList.get(i));
|
||||
list.add(excelData);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateExportTextField(FunctionalCaseExcelData data) {
|
||||
data.setPrerequisite(validateExportText(data.getPrerequisite()));
|
||||
data.setDescription(validateExportText(data.getDescription()));
|
||||
data.setTextDescription(validateExportText(data.getTextDescription()));
|
||||
data.setExpectedResult(validateExportText(data.getExpectedResult()));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建其他字段
|
||||
*
|
||||
* @param functionalCase
|
||||
* @param data
|
||||
* @param caseCommentMap
|
||||
* @param executeCommentMap
|
||||
* @param reviewCommentMap
|
||||
* @param request
|
||||
*/
|
||||
private void buildExportOtherField(FunctionalCase functionalCase, FunctionalCaseExcelData data, Map<String, List<FunctionalCaseComment>> caseCommentMap, Map<String, List<TestPlanCaseExecuteHistory>> executeCommentMap, Map<String, List<CaseReviewHistory>> reviewCommentMap, FunctionalCaseExportRequest request) {
|
||||
if (CollectionUtils.isEmpty(request.getOtherFields())) {
|
||||
return;
|
||||
}
|
||||
List<FunctionalCaseHeader> otherFields = request.getOtherFields();
|
||||
List<String> keys = otherFields.stream().map(FunctionalCaseHeader::getId).toList();
|
||||
Map<String, FunctionalCaseExportConverter> converterMaps = FunctionalCaseExportConverterFactory.getConverters(keys);
|
||||
HashMap<String, String> other = new HashMap<>();
|
||||
otherFields.forEach(header -> {
|
||||
FunctionalCaseExportConverter converter = converterMaps.get(header.getId());
|
||||
if (converter != null) {
|
||||
other.put(header.getName(), converter.parse(functionalCase, caseCommentMap, executeCommentMap, reviewCommentMap));
|
||||
} else {
|
||||
other.put(header.getName(), StringUtils.EMPTY);
|
||||
}
|
||||
});
|
||||
data.setOtherFields(other);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 评审评论
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
private Map<String, List<CaseReviewHistory>> getReviewComment(List<String> ids) {
|
||||
List<CaseReviewHistory> reviewHistories = extFunctionalCaseCommentMapper.getReviewComment(ids);
|
||||
Map<String, List<CaseReviewHistory>> reviewHistoryMap = reviewHistories.stream().collect(Collectors.groupingBy(CaseReviewHistory::getCaseId));
|
||||
return reviewHistoryMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行评论
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
private Map<String, List<TestPlanCaseExecuteHistory>> getExecuteComment(List<String> ids) {
|
||||
List<TestPlanCaseExecuteHistory> historyList = extFunctionalCaseCommentMapper.getExecuteComment(ids);
|
||||
Map<String, List<TestPlanCaseExecuteHistory>> commentMap = historyList.stream().collect(Collectors.groupingBy(TestPlanCaseExecuteHistory::getCaseId));
|
||||
return commentMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用例评论
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
private Map<String, List<FunctionalCaseComment>> getCaseComment(List<String> ids) {
|
||||
List<FunctionalCaseComment> functionalCaseComments = extFunctionalCaseCommentMapper.getCaseComment(ids);
|
||||
Map<String, List<FunctionalCaseComment>> commentMap = functionalCaseComments.stream().collect(Collectors.groupingBy(FunctionalCaseComment::getCaseId));
|
||||
return commentMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建自定义字段
|
||||
*
|
||||
* @param templateCustomFields
|
||||
* @param functionalCaseCustomFields
|
||||
* @param data
|
||||
* @param request
|
||||
*/
|
||||
private void buildExportCustomField(List<TemplateCustomFieldDTO> templateCustomFields, List<FunctionalCaseCustomField> functionalCaseCustomFields, FunctionalCaseExcelData data, FunctionalCaseExportRequest request) {
|
||||
if (CollectionUtils.isEmpty(request.getCustomFields())) {
|
||||
return;
|
||||
}
|
||||
HashMap<String, AbstractCustomFieldValidator> customFieldValidatorMap = CustomFieldValidatorFactory.getValidatorMap();
|
||||
Map<String, TemplateCustomFieldDTO> customFieldsMap = templateCustomFields.stream().collect(Collectors.toMap(TemplateCustomFieldDTO::getFieldId, i -> i));
|
||||
Map<String, String> caseFieldvalueMap = functionalCaseCustomFields.stream().collect(Collectors.toMap(FunctionalCaseCustomField::getFieldId, FunctionalCaseCustomField::getValue));
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
customFieldsMap.forEach((k, v) -> {
|
||||
if (caseFieldvalueMap.containsKey(k)) {
|
||||
AbstractCustomFieldValidator customFieldValidator = customFieldValidatorMap.get(v.getType());
|
||||
if (customFieldValidator.isKVOption) {
|
||||
// 这里如果填的是选项值,替换成选项ID,保存
|
||||
map.put(v.getFieldName(), customFieldValidator.parse2Value(caseFieldvalueMap.get(k), v));
|
||||
}
|
||||
}
|
||||
});
|
||||
data.setCustomData(map);
|
||||
}
|
||||
|
||||
|
||||
private String validateExportText(String textValue) {
|
||||
// poi 导出的单个单元格最大字符数量为 32767 ,这里添加校验提示
|
||||
int maxLength = 32767;
|
||||
if (StringUtils.isNotBlank(textValue) && textValue.length() > maxLength) {
|
||||
return String.format(Translator.get("case_export_text_validate_tip"), maxLength);
|
||||
}
|
||||
return textValue;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建步骤单元格
|
||||
*
|
||||
* @param data
|
||||
* @param functionalCaseBlob
|
||||
* @param caseEditType
|
||||
* @param textDescriptionList
|
||||
* @param expectedResultList
|
||||
*/
|
||||
private void buildExportStep(FunctionalCaseExcelData data, FunctionalCaseBlob functionalCaseBlob, String caseEditType, List<String> textDescriptionList, List<String> expectedResultList) {
|
||||
if (StringUtils.equals(caseEditType, FunctionalCaseTypeConstants.CaseEditType.TEXT.name())) {
|
||||
data.setTextDescription(new String(functionalCaseBlob.getTextDescription() == null ? new byte[0] : functionalCaseBlob.getTextDescription(), StandardCharsets.UTF_8));
|
||||
data.setExpectedResult(new String(functionalCaseBlob.getExpectedResult() == null ? new byte[0] : functionalCaseBlob.getExpectedResult(), StandardCharsets.UTF_8));
|
||||
} else {
|
||||
String steps = new String(functionalCaseBlob.getSteps() == null ? new byte[0] : functionalCaseBlob.getSteps(), StandardCharsets.UTF_8);
|
||||
List jsonArray = new ArrayList();
|
||||
try {
|
||||
jsonArray = JSON.parseArray(steps);
|
||||
} catch (Exception e) {
|
||||
if (steps.contains("null") && !steps.contains("\"null\"")) {
|
||||
steps = steps.replace("null", "\"\"");
|
||||
jsonArray = JSON.parseArray(steps);
|
||||
}
|
||||
}
|
||||
for (int j = 0; j < jsonArray.size(); j++) {
|
||||
// 将步骤存储起来,之后生成多条数据,再合并单元格
|
||||
Map item = (Map) jsonArray.get(j);
|
||||
String textDescription = Optional.ofNullable(item.get("desc")).orElse(StringUtils.EMPTY).toString();
|
||||
String expectedResult = Optional.ofNullable(item.get("result")).orElse(StringUtils.EMPTY).toString();
|
||||
if (StringUtils.isNotBlank(textDescription) || StringUtils.isNotBlank(expectedResult)) {
|
||||
textDescriptionList.add(textDescription);
|
||||
expectedResultList.add(expectedResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取模块map
|
||||
*
|
||||
* @param projectId
|
||||
* @return
|
||||
*/
|
||||
private Map<String, String> getModuleMap(String projectId) {
|
||||
List<BaseTreeNode> moduleTree = functionalCaseModuleService.getTree(projectId);
|
||||
Map<String, String> moduleMap = moduleTree.stream().collect(Collectors.toMap(BaseTreeNode::getId, BaseTreeNode::getPath));
|
||||
return moduleMap;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取导出表头
|
||||
*
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
private List<List<String>> getFunctionalCaseExportHeads(FunctionalCaseExportRequest request) {
|
||||
List<List<String>> headList = new ArrayList<>() {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 5726921174161850104L;
|
||||
|
||||
{
|
||||
addAll(request.getSystemFields()
|
||||
.stream()
|
||||
.map(item -> Arrays.asList(item.getName()))
|
||||
.collect(Collectors.toList()));
|
||||
addAll(request.getCustomFields()
|
||||
.stream()
|
||||
.map(item -> Arrays.asList(item.getName()))
|
||||
.collect(Collectors.toList()));
|
||||
addAll(request.getOtherFields()
|
||||
.stream()
|
||||
.map(item -> Arrays.asList(item.getName()))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
};
|
||||
return headList;
|
||||
}
|
||||
}
|
||||
|
@ -51,19 +51,12 @@ public class FunctionalCaseLogService {
|
||||
private FunctionalCaseAttachmentMapper functionalCaseAttachmentMapper;
|
||||
@Resource
|
||||
private FileAssociationMapper fileAssociationMapper;
|
||||
|
||||
@Resource
|
||||
private CustomFieldMapper customFieldMapper;
|
||||
|
||||
@Resource
|
||||
private BugRelationCaseMapper bugRelationCaseMapper;
|
||||
|
||||
@Resource
|
||||
private BugMapper bugMapper;
|
||||
|
||||
@Resource
|
||||
private ExtFunctionalCaseModuleMapper extFunctionalCaseModuleMapper;
|
||||
|
||||
|
||||
/**
|
||||
* 更新用例 日志
|
||||
@ -434,4 +427,21 @@ public class FunctionalCaseLogService {
|
||||
dto.setMethod(HttpMethodConstants.POST.name());
|
||||
return dto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public LogDTO exportExcelLog(FunctionalCaseExportRequest request) {
|
||||
LogDTO dto = new LogDTO(
|
||||
request.getProjectId(),
|
||||
null,
|
||||
request.getFileId(),
|
||||
null,
|
||||
OperationLogType.EXPORT.name(),
|
||||
OperationLogModule.FUNCTIONAL_CASE,
|
||||
"");
|
||||
dto.setHistory(true);
|
||||
dto.setPath("/functional/case/export/excel");
|
||||
dto.setMethod(HttpMethodConstants.POST.name());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
@ -303,7 +303,7 @@ public class FunctionalCaseModuleService extends ModuleTreeService {
|
||||
currentModuleName = itemIterator.next().trim();
|
||||
moduleTree.forEach(module -> {
|
||||
//根节点是否存在
|
||||
if (StringUtils.equals(currentModuleName, module.getName())) {
|
||||
if (StringUtils.equalsIgnoreCase(currentModuleName, module.getName())) {
|
||||
hasNode.set(true);
|
||||
//根节点存在,检查子节点是否存在
|
||||
createModuleByPathIterator(itemIterator, "/" + currentModuleName, module, pathMap, projectId, userId);
|
||||
@ -342,7 +342,7 @@ public class FunctionalCaseModuleService extends ModuleTreeService {
|
||||
String nodeName = itemIterator.next().trim();
|
||||
AtomicReference<Boolean> hasNode = new AtomicReference<>(false);
|
||||
children.forEach(child -> {
|
||||
if (StringUtils.equals(nodeName, child.getName())) {
|
||||
if (StringUtils.equalsIgnoreCase(nodeName, child.getName())) {
|
||||
hasNode.set(true);
|
||||
createModuleByPathIterator(itemIterator, currentModulePath + "/" + child.getName(), child, pathMap, projectId, userId);
|
||||
}
|
||||
|
@ -1158,7 +1158,7 @@ public class FunctionalCaseService {
|
||||
private void noticeModule(List<FunctionalCaseDTO> noticeList, FunctionalCaseExcelData functionalCaseExcelData, FunctionalCaseImportRequest request, String userId, Map<String, TemplateCustomFieldDTO> customFieldsMap) {
|
||||
FunctionalCaseDTO functionalCaseDTO = new FunctionalCaseDTO();
|
||||
functionalCaseDTO.setTriggerMode(Translator.get("log.test_plan.functional_case"));
|
||||
functionalCaseDTO.setName(functionalCaseExcelData.getName());
|
||||
functionalCaseDTO.setName(functionalCaseExcelData.getName().toString());
|
||||
functionalCaseDTO.setProjectId(request.getProjectId());
|
||||
functionalCaseDTO.setCaseEditType(functionalCaseExcelData.getCaseEditType());
|
||||
functionalCaseDTO.setCreateUser(userId);
|
||||
@ -1187,7 +1187,7 @@ public class FunctionalCaseService {
|
||||
functionalCase.setModuleId(caseModulePathMap.get(functionalCaseExcelData.getModule()));
|
||||
functionalCase.setProjectId(request.getProjectId());
|
||||
functionalCase.setTemplateId(defaultTemplateDTO.getId());
|
||||
functionalCase.setName(functionalCaseExcelData.getName());
|
||||
functionalCase.setName(functionalCaseExcelData.getName().toString());
|
||||
functionalCase.setReviewStatus(FunctionalCaseReviewStatus.UN_REVIEWED.name());
|
||||
functionalCase.setTags(handleImportTags(functionalCaseExcelData.getTags()));
|
||||
functionalCase.setCaseEditType(StringUtils.defaultIfBlank(functionalCaseExcelData.getCaseEditType(), FunctionalCaseTypeConstants.CaseEditType.TEXT.name()));
|
||||
@ -1384,7 +1384,7 @@ public class FunctionalCaseService {
|
||||
//用例表
|
||||
FunctionalCase functionalCase = collect.get(functionalCaseExcelData.getNum()).getFirst();
|
||||
|
||||
functionalCase.setName(functionalCaseExcelData.getName());
|
||||
functionalCase.setName(functionalCaseExcelData.getName().toString());
|
||||
functionalCase.setModuleId(caseModulePathMap.get(functionalCaseExcelData.getModule()));
|
||||
functionalCase.setTags(handleImportTags(functionalCaseExcelData.getTags()));
|
||||
functionalCase.setCaseEditType(StringUtils.defaultIfBlank(functionalCaseExcelData.getCaseEditType(), FunctionalCaseTypeConstants.CaseEditType.TEXT.name()));
|
||||
@ -1417,7 +1417,7 @@ public class FunctionalCaseService {
|
||||
FunctionalCase functionalCase = collect.get(functionalCaseExcelData.getNum()).getFirst();
|
||||
if (CollectionUtils.isNotEmpty(projectApplications) && Boolean.valueOf(projectApplications.getFirst().getTypeValue())) {
|
||||
FunctionalCaseBlob blob = blobsCollect.get(functionalCaseExcelData.getNum()).getFirst();
|
||||
if (!StringUtils.equals(functionalCase.getName(), functionalCaseExcelData.getName())
|
||||
if (!StringUtils.equals(functionalCase.getName(), functionalCaseExcelData.getName().toString())
|
||||
|| !StringUtils.equals(new String(blob.getSteps(), StandardCharsets.UTF_8), StringUtils.defaultIfBlank(functionalCaseExcelData.getSteps(), StringUtils.EMPTY))
|
||||
|| !StringUtils.equals(new String(blob.getTextDescription(), StandardCharsets.UTF_8), StringUtils.defaultIfBlank(functionalCaseExcelData.getTextDescription(), StringUtils.EMPTY))
|
||||
|| !StringUtils.equals(new String(blob.getExpectedResult(), StandardCharsets.UTF_8), StringUtils.defaultIfBlank(functionalCaseExcelData.getExpectedResult(), StringUtils.EMPTY))) {
|
||||
|
@ -0,0 +1,98 @@
|
||||
package io.metersphere.functional.socket;
|
||||
|
||||
import io.metersphere.sdk.constants.MsgType;
|
||||
import io.metersphere.sdk.dto.SocketMsgDTO;
|
||||
import io.metersphere.sdk.util.JSON;
|
||||
import io.metersphere.sdk.util.LogUtils;
|
||||
import jakarta.websocket.*;
|
||||
import jakarta.websocket.server.PathParam;
|
||||
import jakarta.websocket.server.ServerEndpoint;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
@ServerEndpoint("/ws/export/{fileId}")
|
||||
public class ExportWebSocketHandler {
|
||||
|
||||
public static final Map<String, Session> ONLINE_EXPORT_EXCEL_SESSIONS = new ConcurrentHashMap<>();
|
||||
|
||||
public static void sendMessage(Session session, SocketMsgDTO message) {
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
// 替换了web容器后 jetty没有设置永久有效的参数,这里暂时设置超时时间为一天
|
||||
session.setMaxIdleTimeout(86400000L);
|
||||
RemoteEndpoint.Async async = session.getAsyncRemote();
|
||||
if (async == null) {
|
||||
return;
|
||||
}
|
||||
async.sendText(JSON.toJSONString(message));
|
||||
}
|
||||
|
||||
public static void sendMessageSingle(SocketMsgDTO dto) {
|
||||
sendMessage(ONLINE_EXPORT_EXCEL_SESSIONS.get(Optional.ofNullable(dto.getReportId())
|
||||
.orElse(StringUtils.EMPTY)), dto);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 连接成功响应
|
||||
*/
|
||||
@OnOpen
|
||||
public void openSession(@PathParam("fileId") String fileId, Session session) {
|
||||
ONLINE_EXPORT_EXCEL_SESSIONS.put(fileId, session);
|
||||
RemoteEndpoint.Async async = session.getAsyncRemote();
|
||||
if (async != null) {
|
||||
async.sendText(JSON.toJSONString(new SocketMsgDTO(fileId, "", MsgType.CONNECT.name(), MsgType.CONNECT.name())));
|
||||
session.setMaxIdleTimeout(180000);
|
||||
}
|
||||
LogUtils.info("客户端: [" + fileId + "] : 连接成功!" + ExportWebSocketHandler.ONLINE_EXPORT_EXCEL_SESSIONS.size(), fileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收到消息响应
|
||||
*/
|
||||
@OnMessage
|
||||
public void onMessage(@PathParam("fileId") String fileId, String message) {
|
||||
LogUtils.info("服务器收到:[" + fileId + "] : " + message);
|
||||
SocketMsgDTO dto = JSON.parseObject(message, SocketMsgDTO.class);
|
||||
ExportWebSocketHandler.sendMessageSingle(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接关闭响应
|
||||
*/
|
||||
@OnClose
|
||||
public void onClose(@PathParam("fileId") String fileId, Session session) throws IOException {
|
||||
//当前的Session 移除
|
||||
ExportWebSocketHandler.ONLINE_EXPORT_EXCEL_SESSIONS.remove(fileId);
|
||||
LogUtils.info("[" + fileId + "] : 断开连接!" + ExportWebSocketHandler.ONLINE_EXPORT_EXCEL_SESSIONS.size());
|
||||
//并且通知其他人当前用户已经断开连接了
|
||||
session.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接异常响应
|
||||
*/
|
||||
@OnError
|
||||
public void onError(Session session, Throwable throwable) throws IOException {
|
||||
LogUtils.error("连接异常响应", throwable);
|
||||
session.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 每一分钟群发一次心跳检查
|
||||
*/
|
||||
@Scheduled(fixedRate = 60000)
|
||||
public void heartbeatCheck() {
|
||||
ExportWebSocketHandler.sendMessageSingle(
|
||||
new SocketMsgDTO(MsgType.HEARTBEAT.name(), MsgType.HEARTBEAT.name(), MsgType.HEARTBEAT.name(), "heartbeat check")
|
||||
);
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import io.metersphere.functional.dto.CaseCustomFieldDTO;
|
||||
import io.metersphere.functional.dto.FunctionalCaseAttachmentDTO;
|
||||
import io.metersphere.functional.dto.FunctionalCasePageDTO;
|
||||
import io.metersphere.functional.dto.response.FunctionalCaseImportResponse;
|
||||
import io.metersphere.functional.excel.domain.FunctionalCaseHeader;
|
||||
import io.metersphere.functional.mapper.FunctionalCaseAttachmentMapper;
|
||||
import io.metersphere.functional.mapper.FunctionalCaseCustomFieldMapper;
|
||||
import io.metersphere.functional.request.*;
|
||||
@ -85,6 +86,7 @@ public class FunctionalCaseControllerTests extends BaseTest {
|
||||
public static final String CHECK_EXCEL_URL = "/functional/case/pre-check/excel";
|
||||
public static final String IMPORT_EXCEL_URL = "/functional/case/import/excel";
|
||||
public static final String OPERATION_HISTORY_URL = "/functional/case/operation-history";
|
||||
public static final String EXPORT_EXCEL_URL = "/functional/case/export/excel";
|
||||
|
||||
@Resource
|
||||
private NotificationMapper notificationMapper;
|
||||
@ -795,4 +797,41 @@ public class FunctionalCaseControllerTests extends BaseTest {
|
||||
Assertions.assertTrue(CollectionUtils.isNotEmpty(operationHistoryDTOS));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
public void exportExcel() throws Exception {
|
||||
FunctionalCaseExportRequest request = new FunctionalCaseExportRequest();
|
||||
request.setProjectId(DEFAULT_PROJECT_ID);
|
||||
request.setSelectIds(List.of("TEST_FUNCTIONAL_CASE_ID"));
|
||||
List<FunctionalCaseHeader> sysHeaders = new ArrayList<>() {{
|
||||
add(new FunctionalCaseHeader() {{
|
||||
setId("num");
|
||||
setName("ID");
|
||||
}});
|
||||
add(new FunctionalCaseHeader() {{
|
||||
setId("name");
|
||||
setName("用例名称");
|
||||
}});
|
||||
}};
|
||||
request.setSystemFields(sysHeaders);
|
||||
List<FunctionalCaseHeader> customHeaders = new ArrayList<>() {{
|
||||
add(new FunctionalCaseHeader() {{
|
||||
setId("A");
|
||||
setName("测试3");
|
||||
}});
|
||||
}};
|
||||
request.setCustomFields(customHeaders);
|
||||
List<FunctionalCaseHeader> otherHeaders = new ArrayList<>() {{
|
||||
add(new FunctionalCaseHeader() {{
|
||||
setId("createTime");
|
||||
setName("创建时间");
|
||||
}});
|
||||
}};
|
||||
request.setOtherFields(otherHeaders);
|
||||
|
||||
request.setFileId("123142342");
|
||||
this.requestPost(EXPORT_EXCEL_URL, request);
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ public class MinioConfig {
|
||||
|
||||
// 设置临时目录下文件的过期时间
|
||||
setBucketLifecycle(minioClient);
|
||||
setBucketLifecycleByExcel(minioClient);
|
||||
|
||||
boolean exist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(BUCKET).build());
|
||||
if (!exist) {
|
||||
@ -66,4 +67,36 @@ public class MinioConfig {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 设置生命周期规则-文件的过期时间
|
||||
* 将 system/export/excel/ 下的文件设置为 1 天后过期
|
||||
* 参考 minio 8.5.2 版本的示例代码
|
||||
* https://github.com/minio/minio-java/blob/8.5.2/examples/SetBucketLifecycle.java
|
||||
*/
|
||||
private static void setBucketLifecycleByExcel(MinioClient minioClient) {
|
||||
List<LifecycleRule> rules = new LinkedList<>();
|
||||
rules.add(
|
||||
new LifecycleRule(
|
||||
Status.ENABLED,
|
||||
null,
|
||||
new Expiration((ZonedDateTime) null, 1, null),
|
||||
new RuleFilter("system/export/excel"),
|
||||
"excel-file",
|
||||
null,
|
||||
null,
|
||||
null));
|
||||
LifecycleConfiguration config = new LifecycleConfiguration(rules);
|
||||
try {
|
||||
minioClient.setBucketLifecycle(
|
||||
SetBucketLifecycleArgs.builder()
|
||||
.bucket(BUCKET)
|
||||
.config(config)
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
LogUtils.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user