refactor(用例管理): 优化用例功能关联附件逻辑

This commit is contained in:
WangXu10 2023-11-20 13:40:03 +08:00 committed by Craftsman
parent 733b063bb8
commit 9cfd5022aa
16 changed files with 210 additions and 56 deletions

View File

@ -12,10 +12,12 @@ import java.util.Map;
*/
public class FileAssociationSourceUtil {
public static final String SOURCE_TYPE_BUG = "BUG";
public static final String SOURCE_TYPE_FUNCTIONAL_CASE = "FUNCTIONAL_CASE";
public static final Map<String, String> QUERY_SQL = new HashMap<>();
static {
QUERY_SQL.put(SOURCE_TYPE_BUG, "SELECT id AS sourceId,title AS sourceName FROM bug");
QUERY_SQL.put(SOURCE_TYPE_FUNCTIONAL_CASE, "SELECT id AS sourceId,name AS sourceName FROM functional_case");
}
public static void validate(String type) {

View File

@ -256,13 +256,23 @@
<sql id="queryVersionCondition">
<if test="request.versionId != null">
${versionTable}.version_id = #{request.versionId}
<include refid="queryType">
<property name="searchMode" value="request.searchMode"/>
</include>
</if>
<if test="request.refId != null">
${versionTable}.ref_id = #{request.refId}
<include refid="queryType">
<property name="searchMode" value="request.searchMode"/>
</include>
</if>
<if test="request.versionId == null and request.refId == null">
${versionTable}.latest = 1
<include refid="queryType">
<property name="searchMode" value="request.searchMode"/>
</include>
</if>
1 = 1
</sql>
<sql id="queryAssociationCase">

View File

@ -0,0 +1,28 @@
package io.metersphere.functional.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* @author wx
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class FileDumpRequest implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "项目id",requiredMode = Schema.RequiredMode.REQUIRED)
private String projectId;
@Schema(description = "用例id",requiredMode = Schema.RequiredMode.REQUIRED)
private String caseId;
@Schema(description = "文件id",requiredMode = Schema.RequiredMode.REQUIRED)
private String fileId;
}

View File

@ -18,6 +18,9 @@ public class FunctionalCaseEditRequest extends FunctionalCaseAddRequest {
@NotBlank(message = "{functional_case.id.not_blank}")
private String id;
@Schema(description = "取消关联id")
@Schema(description = "删除本地上传的文件id")
private List<String> deleteFileMetaIds;
@Schema(description = "取消关联的文件id")
private List<String> unLinkFilesIds;
}

View File

@ -8,28 +8,26 @@ import io.metersphere.functional.dto.FunctionalCaseAttachmentDTO;
import io.metersphere.functional.dto.FunctionalCaseDetailDTO;
import io.metersphere.functional.mapper.FunctionalCaseAttachmentMapper;
import io.metersphere.functional.request.FunctionalCaseAddRequest;
import io.metersphere.project.domain.FileMetadata;
import io.metersphere.project.mapper.FileMetadataMapper;
import io.metersphere.project.dto.filemanagement.FileInfo;
import io.metersphere.project.dto.filemanagement.FileLogRecord;
import io.metersphere.project.service.FileAssociationService;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
import io.metersphere.sdk.constants.HttpMethodConstants;
import io.metersphere.sdk.constants.StorageType;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.sdk.util.FileAssociationSourceUtil;
import io.metersphere.system.file.FileRequest;
import io.metersphere.system.file.MinioRepository;
import io.metersphere.system.log.constants.OperationLogModule;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
@ -40,17 +38,15 @@ import java.util.stream.Collectors;
@Transactional(rollbackFor = Exception.class)
public class FunctionalCaseAttachmentService {
@Resource
SqlSessionFactory sqlSessionFactory;
@Resource
private FunctionalCaseAttachmentMapper functionalCaseAttachmentMapper;
@Resource
private FileMetadataMapper fileMetadataMapper;
private MinioRepository minioRepository;
@Resource
private MinioRepository minioRepository;
private FileAssociationService fileAssociationService;
/**
* 保存本地上传文件和用例关联关系
@ -91,30 +87,6 @@ public class FunctionalCaseAttachmentService {
}
}
/**
* 保存文件库文件与用例关联关系
*
* @param relateFileMetaIds
* @param caseId
* @param userId
*/
public void relateFileMeta(List<String> relateFileMetaIds, String caseId, String userId) {
if (CollectionUtils.isNotEmpty(relateFileMetaIds)) {
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
FunctionalCaseAttachmentMapper sessionMapper = sqlSession.getMapper(FunctionalCaseAttachmentMapper.class);
relateFileMetaIds.forEach(fileMetaId -> {
FileMetadata fileMetadata = fileMetadataMapper.selectByPrimaryKey(fileMetaId);
FunctionalCaseAttachment caseAttachment = creatModule(fileMetadata.getId(), fileMetadata.getName(), fileMetadata.getSize(), caseId, false, userId);
sessionMapper.insertSelective(caseAttachment);
});
sqlSession.flushStatements();
if (sqlSession != null && sqlSessionFactory != null) {
SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
}
}
}
private FunctionalCaseAttachment creatModule(String fileId, String fileName, long fileSize, String caseId, Boolean isLocal, String userId) {
FunctionalCaseAttachment caseAttachment = new FunctionalCaseAttachment();
caseAttachment.setId(IDGenerator.nextStr());
@ -138,17 +110,21 @@ public class FunctionalCaseAttachmentService {
FunctionalCaseAttachmentExample example = new FunctionalCaseAttachmentExample();
example.createCriteria().andCaseIdEqualTo(functionalCaseDetailDTO.getId());
List<FunctionalCaseAttachment> caseAttachments = functionalCaseAttachmentMapper.selectByExample(example);
if (CollectionUtils.isNotEmpty(caseAttachments)) {
caseAttachments.stream().filter(caseAttachment -> !caseAttachment.getLocal()).forEach(caseAttachment -> {
FileMetadata fileMetadata = fileMetadataMapper.selectByPrimaryKey(caseAttachment.getFileId());
caseAttachment.setFileName(fileMetadata.getName());
});
}
List<FunctionalCaseAttachmentDTO> attachmentDTOs = Lists.transform(caseAttachments, (functionalCaseAttachment) -> {
List<FunctionalCaseAttachmentDTO> attachmentDTOs = new ArrayList<>(Lists.transform(caseAttachments, (functionalCaseAttachment) -> {
FunctionalCaseAttachmentDTO attachmentDTO = new FunctionalCaseAttachmentDTO();
BeanUtils.copyBean(attachmentDTO, functionalCaseAttachment);
return attachmentDTO;
});
}));
//获取关联的附件信息
List<FileInfo> files = fileAssociationService.getFiles(functionalCaseDetailDTO.getId(), FileAssociationSourceUtil.SOURCE_TYPE_FUNCTIONAL_CASE);
List<FunctionalCaseAttachmentDTO> filesDTOs = new ArrayList<>(Lists.transform(files, (fileInfo) -> {
FunctionalCaseAttachmentDTO attachmentDTO = new FunctionalCaseAttachmentDTO();
BeanUtils.copyBean(attachmentDTO, fileInfo);
return attachmentDTO;
}));
attachmentDTOs.addAll(filesDTOs);
Collections.sort(attachmentDTOs, Comparator.comparing(FunctionalCaseAttachmentDTO::getCreateTime));
functionalCaseDetailDTO.setAttachments(attachmentDTOs);
}
@ -205,4 +181,41 @@ public class FunctionalCaseAttachmentService {
public void batchSaveAttachment(List<FunctionalCaseAttachment> attachments) {
functionalCaseAttachmentMapper.batchInsert(attachments);
}
/**
* 保存文件库文件与用例关联关系
*
* @param relateFileMetaIds
* @param caseId
* @param userId
* @param logUrl
* @param projectId
*/
public void association(List<String> relateFileMetaIds, String caseId, String userId, String logUrl, String projectId) {
fileAssociationService.association(caseId, FileAssociationSourceUtil.SOURCE_TYPE_FUNCTIONAL_CASE, relateFileMetaIds, false, createFileLogRecord(logUrl, userId, projectId));
}
private FileLogRecord createFileLogRecord(String logUrl, String operator, String projectId) {
return FileLogRecord.builder()
.logModule(OperationLogModule.FUNCTIONAL_CASE)
.requestMethod(HttpMethodConstants.POST.name())
.requestUrl(logUrl)
.operator(operator)
.projectId(projectId)
.build();
}
/**
* 取消关联 删除文件库文件和用例关联关系
*
* @param unLinkFilesIds
* @param logUrl
* @param userId
* @param projectId
*/
public void unAssociation(List<String> unLinkFilesIds, String logUrl, String userId, String projectId) {
fileAssociationService.deleteBySourceId(unLinkFilesIds, createFileLogRecord(logUrl, userId, projectId));
}
}

View File

@ -74,6 +74,9 @@ public class FunctionalCaseService {
@Resource
private FunctionalCaseModuleMapper functionalCaseModuleMapper;
private static final String ADD_FUNCTIONAL_CASE_FILE_LOG_URL = "/functional/case/add";
private static final String UPDATE_FUNCTIONAL_CASE_FILE_LOG_URL = "/functional/case/update";
public FunctionalCase addFunctionalCase(FunctionalCaseAddRequest request, List<MultipartFile> files, String userId) {
String caseId = IDGenerator.nextStr();
//添加功能用例
@ -83,7 +86,9 @@ public class FunctionalCaseService {
functionalCaseAttachmentService.uploadFile(request, caseId, files, true, userId);
//关联附件
functionalCaseAttachmentService.relateFileMeta(request.getRelateFileMetaIds(), caseId, userId);
if (CollectionUtils.isNotEmpty(request.getRelateFileMetaIds())) {
functionalCaseAttachmentService.association(request.getRelateFileMetaIds(), caseId, userId, ADD_FUNCTIONAL_CASE_FILE_LOG_URL, request.getProjectId());
}
//TODO 记录变更历史
@ -242,11 +247,18 @@ public class FunctionalCaseService {
functionalCaseAttachmentService.deleteCaseAttachment(request.getDeleteFileMetaIds(), request.getId(), request.getProjectId());
}
//处理取消关联文件id
if (CollectionUtils.isNotEmpty(request.getUnLinkFilesIds())) {
functionalCaseAttachmentService.unAssociation(request.getUnLinkFilesIds(), UPDATE_FUNCTIONAL_CASE_FILE_LOG_URL, userId, request.getProjectId());
}
//上传新文件
functionalCaseAttachmentService.uploadFile(request, request.getId(), files, true, userId);
//关联新附件
functionalCaseAttachmentService.relateFileMeta(request.getRelateFileMetaIds(), request.getId(), userId);
if (CollectionUtils.isNotEmpty(request.getRelateFileMetaIds())) {
functionalCaseAttachmentService.association(request.getRelateFileMetaIds(), request.getId(), userId, UPDATE_FUNCTIONAL_CASE_FILE_LOG_URL, request.getProjectId());
}
//TODO 记录变更历史 addFunctionalCaseHistory
@ -563,7 +575,8 @@ public class FunctionalCaseService {
if (StringUtils.isNotBlank(collect.get(id).getTags())) {
List<String> tags = JSON.parseArray(collect.get(id).getTags(), String.class);
tags.addAll(request.getTags());
functionalCase.setTags(JSON.toJSONString(tags));
List<String> newTags = tags.stream().distinct().collect(Collectors.toList());
functionalCase.setTags(JSON.toJSONString(newTags));
} else {
functionalCase.setTags(JSON.toJSONString(request.getTags()));
}

View File

@ -188,6 +188,8 @@ public class FunctionalCaseControllerTests extends BaseTest {
//设置删除文件id
request.setDeleteFileMetaIds(Arrays.asList("delete_file_meta_id_1"));
request.setUnLinkFilesIds(Arrays.asList("relate_file_meta_id_1"));
request.setRelateFileMetaIds(Arrays.asList("relate_file_meta_id_1", "relate_file_meta_id_2"));
paramMap = new LinkedMultiValueMap<>();
paramMap.add("request", JSON.toJSONString(request));
paramMap.add("files", files);

View File

@ -1,6 +1,9 @@
INSERT INTO file_metadata(id, name, type, size, create_time, update_time, project_id, storage, create_user, update_user, tags, description, module_id, path, latest, ref_id, file_version) VALUES ('relate_file_meta_id_1', 'formItem', 'ts', 2502, 1698058347559, 1698058347559, '100001100001', 'MINIO', 'admin', 'admin', NULL, NULL, 'root', '100001100001/1127016598347779', b'1', '1127016598347779', '1127016598347779');
INSERT INTO file_metadata(id, name, type, size, create_time, update_time, project_id, storage, create_user, update_user, tags, description, module_id, path, latest, ref_id, file_version) VALUES ('relate_file_meta_id_2', 'formItem', 'ts', 2502, 1698058347559, 1698058347559, '100001100001', 'MINIO', 'admin', 'admin', NULL, NULL, 'root', '100001100001/1127016598347779', b'1', '1127016598347779', '1127016598347779');
INSERT INTO file_association(id, source_type, source_id, file_id, file_ref_id, file_version, create_time, update_user, update_time, create_user) VALUES ('file_association_1', 'functional_case', 'TEST_FUNCTIONAL_CASE_ID', 'relate_file_meta_id_1', '1', '1', 1698983271536, 'admin', 1698983271536, 'admin');
INSERT INTO functional_case(id, num, module_id, project_id, template_id, name, review_status, tags, case_edit_type, pos, version_id, ref_id, last_execute_result, deleted, public_case, latest, create_user, update_user, delete_user, create_time, update_time, delete_time)
VALUES ('TEST_FUNCTIONAL_CASE_ID', 1, 'TEST_MODULE_ID', '100001100001', '100001', '测试', 'UN_REVIEWED', NULL, 'STEP', 0, 'v1.0.0', 'v1.0.0', 'UN_EXECUTED', b'0', b'0', b'1', 'admin', 'admin', '', 1698058347559, 1698058347559, NULL);

View File

@ -245,7 +245,7 @@ public class ProjectApplicationController {
@PostMapping("/update/bug")
@Operation(summary = "缺陷管理-配置")
@RequiresPermissions(PermissionConstants.PROJECT_APPLICATION_BUG_UPDATE)
@Log(type = OperationLogType.UPDATE, expression = "#msClass.updateWorkstationLog(#application)", msClass = ProjectApplicationService.class)
@Log(type = OperationLogType.UPDATE, expression = "#msClass.updateBugLog(#application)", msClass = ProjectApplicationService.class)
public void updateBug(@Validated({Updated.class}) @RequestBody ProjectApplication application) {
if (ProjectApplicationType.BUG_SYNC_CONFIG.SYNC_ENABLE.name().equals(application.getType())) {
String projectBugThirdPartConfig = projectApplicationService.getProjectBugThirdPartConfig(application.getProjectId());

View File

@ -0,0 +1,36 @@
package io.metersphere.project.dto.filemanagement;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* @author wx
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class FileInfo implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "文件ID")
private String fileId;
@Schema(description = "文件名称")
private String fileName;
@Schema(description = "文件大小")
private Long size;
@Schema(description = "是否本地")
private Boolean local;
@Schema(description = "创建人")
private String createUser;
@Schema(description = "创建时间")
private Long createTime;
}

View File

@ -1,6 +1,7 @@
package io.metersphere.project.mapper;
import io.metersphere.project.dto.filemanagement.FileAssociationSource;
import io.metersphere.project.dto.filemanagement.FileInfo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@ -12,4 +13,6 @@ import java.util.List;
public interface ExtFileAssociationMapper {
FileAssociationSource selectNameBySourceTableAndId(@Param("querySql") String querySql, @Param("sourceId") String sourceId);
List<FileAssociationSource> selectAssociationSourceBySourceTableAndIdList(@Param("querySql") String querySql, @Param("idList") List<String> sourceIdList);
List<FileInfo> selectAssociationFileInfo(@Param("sourceId") String sourceId, @Param("sourceType") String sourceType);
}

View File

@ -14,4 +14,20 @@
#{id}
</foreach>
</select>
<select id="selectAssociationFileInfo" resultType="io.metersphere.project.dto.filemanagement.FileInfo">
SELECT
file_association.file_id AS fileId,
CONCAT( file_metadata.`name`, file_metadata.type ) AS fileName,
file_metadata.size AS size,
'false' AS local,
file_association.create_user AS createUser,
file_association.create_time AS createTime
FROM
file_association
LEFT JOIN file_metadata ON file_association.file_id = file_metadata.id
WHERE
file_association.source_id = #{sourceId}
AND file_association.source_type = #{sourceType}
</select>
</mapper>

View File

@ -4,6 +4,7 @@ import io.metersphere.project.domain.FileAssociation;
import io.metersphere.project.domain.FileAssociationExample;
import io.metersphere.project.domain.FileMetadata;
import io.metersphere.project.dto.filemanagement.FileAssociationSource;
import io.metersphere.project.dto.filemanagement.FileInfo;
import io.metersphere.project.dto.filemanagement.FileLogRecord;
import io.metersphere.project.dto.filemanagement.response.FileAssociationResponse;
import io.metersphere.project.dto.filemanagement.response.FileInformationResponse;
@ -326,4 +327,16 @@ public class FileAssociationService {
throw new MSException(Translator.get("file.association.source.not.exist"));
}
}
/**
* 获取文件列表接口
*
* @param sourceId
* @param sourceType
* @return
*/
public List<FileInfo> getFiles(String sourceId, String sourceType) {
return extFileAssociationMapper.selectAssociationFileInfo(sourceId, sourceType);
}
}

View File

@ -316,7 +316,7 @@ public class ProjectApplicationService {
* @return
*/
public LogDTO updateTestPlanLog(ProjectApplication application) {
return delLog(application, OperationLogModule.PROJECT_PROJECT_MANAGER, "测试计划配置");
return delLog(application, OperationLogModule.PROJECT_MANAGEMENT_PERMISSION_MENU_MANAGEMENT, "测试计划配置");
}
@ -327,7 +327,7 @@ public class ProjectApplicationService {
* @return
*/
public LogDTO updateUiLog(ProjectApplication application) {
return delLog(application, OperationLogModule.PROJECT_PROJECT_MANAGER, "UI配置");
return delLog(application, OperationLogModule.PROJECT_MANAGEMENT_PERMISSION_MENU_MANAGEMENT, "UI配置");
}
/**
@ -337,7 +337,7 @@ public class ProjectApplicationService {
* @return
*/
public LogDTO updatePerformanceLog(ProjectApplication application) {
return delLog(application, OperationLogModule.PROJECT_PROJECT_MANAGER, "性能测试配置");
return delLog(application, OperationLogModule.PROJECT_MANAGEMENT_PERMISSION_MENU_MANAGEMENT, "性能测试配置");
}
/**
@ -347,7 +347,7 @@ public class ProjectApplicationService {
* @return
*/
public LogDTO updateApiLog(ProjectApplication application) {
return delLog(application, OperationLogModule.PROJECT_PROJECT_MANAGER, "接口测试配置");
return delLog(application, OperationLogModule.PROJECT_MANAGEMENT_PERMISSION_MENU_MANAGEMENT, "接口测试配置");
}
@ -358,7 +358,7 @@ public class ProjectApplicationService {
* @return
*/
public LogDTO updateCaseLog(ProjectApplication application) {
return delLog(application, OperationLogModule.PROJECT_PROJECT_MANAGER, "用例管理配置");
return delLog(application, OperationLogModule.PROJECT_MANAGEMENT_PERMISSION_MENU_MANAGEMENT, "功能测试配置");
}
/**
@ -368,7 +368,11 @@ public class ProjectApplicationService {
* @return
*/
public LogDTO updateWorkstationLog(ProjectApplication application) {
return delLog(application, OperationLogModule.PROJECT_PROJECT_MANAGER, "工作台配置");
return delLog(application, OperationLogModule.PROJECT_MANAGEMENT_PERMISSION_MENU_MANAGEMENT, "工作台配置");
}
public LogDTO updateBugLog(ProjectApplication application) {
return delLog(application, OperationLogModule.PROJECT_MANAGEMENT_PERMISSION_MENU_MANAGEMENT, "缺陷管理配置");
}
private LogDTO delLog(ProjectApplication application, String module, String content) {

View File

@ -2205,4 +2205,10 @@ public class FileManagementControllerTests extends BaseTest {
example.createCriteria().andProjectIdEqualTo(projectId).andModuleIdNotEqualTo(moduleId).andLatestEqualTo(true);
Assertions.assertEquals(fileMetadataMapper.countByExample(example), 0);
}
@Test
@Order(91)
public void testQuery() throws Exception {
fileAssociationService.getFiles("TEST", FileAssociationSourceUtil.SOURCE_TYPE_FUNCTIONAL_CASE);
}
}

View File

@ -92,6 +92,8 @@ public class OperationLogModule {
//项目管理-消息设置
public static final String PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT_CONFIG = "PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT_CONFIG";
public static final String PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT_ROBOT = "PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT_ROBOT";
//项目管理-应用管理
public static final String PROJECT_MANAGEMENT_PERMISSION_MENU_MANAGEMENT = "PROJECT_MANAGEMENT_PERMISSION_MENU_MANAGEMENT";
public static final String PROJECT_TEMPLATE = "PROJECT_TEMPLATE";// 项目模板
public static final String PROJECT_CUSTOM_FIELD = "PROJECT_CUSTOM_FIELD";// 项目字段