diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseAttachmentController.java b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseAttachmentController.java index c48c1d83f5..64607c24c9 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseAttachmentController.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseAttachmentController.java @@ -1,6 +1,8 @@ package io.metersphere.functional.controller; import io.metersphere.functional.domain.FunctionalCaseAttachment; +import io.metersphere.functional.request.FunctionalCaseAssociationFileRequest; +import io.metersphere.functional.request.FunctionalCaseDeleteFileRequest; import io.metersphere.functional.request.FunctionalCaseFileRequest; import io.metersphere.functional.service.FunctionalCaseAttachmentService; import io.metersphere.project.dto.filemanagement.FileLogRecord; @@ -22,6 +24,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.util.Arrays; import java.util.List; @@ -124,4 +127,21 @@ public class FunctionalCaseAttachmentController { } + + @PostMapping("/upload/file") + @Operation(summary = "用例管理-功能用例-上传文件并关联用例") + @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_UPDATE) + public void uploadFile(@Validated @RequestPart("request") FunctionalCaseAssociationFileRequest request, @RequestPart(value = "file", required = false) MultipartFile file) { + String userId = SessionUtils.getUserId(); + functionalCaseAttachmentService.uploadOrAssociationFile(request, file, userId); + } + + @PostMapping("/delete/file") + @Operation(summary = "用例管理-功能用例-删除文件并取消关联用例") + @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_UPDATE) + public void deleteFile(@Validated @RequestBody FunctionalCaseDeleteFileRequest request) { + String userId = SessionUtils.getUserId(); + functionalCaseAttachmentService.deleteFile(request, userId); + } + } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/request/FunctionalCaseAssociationFileRequest.java b/backend/services/case-management/src/main/java/io/metersphere/functional/request/FunctionalCaseAssociationFileRequest.java new file mode 100644 index 0000000000..23515d4e01 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/request/FunctionalCaseAssociationFileRequest.java @@ -0,0 +1,26 @@ +package io.metersphere.functional.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * @author wx + */ +@Data +public class FunctionalCaseAssociationFileRequest implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "文件id列表") + private List fileIds; + + @Schema(description = "用例id", requiredMode = Schema.RequiredMode.REQUIRED) + private String caseId; + + @Schema(description = "项目id", requiredMode = Schema.RequiredMode.REQUIRED) + private String projectId; +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/request/FunctionalCaseDeleteFileRequest.java b/backend/services/case-management/src/main/java/io/metersphere/functional/request/FunctionalCaseDeleteFileRequest.java new file mode 100644 index 0000000000..2bc5c4d4b9 --- /dev/null +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/request/FunctionalCaseDeleteFileRequest.java @@ -0,0 +1,26 @@ +package io.metersphere.functional.request; + +import io.metersphere.functional.dto.FunctionalCaseAttachmentDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @author wx + */ +@Data +public class FunctionalCaseDeleteFileRequest implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "附件信息", requiredMode = Schema.RequiredMode.REQUIRED) + private FunctionalCaseAttachmentDTO attachment; + + @Schema(description = "用例id", requiredMode = Schema.RequiredMode.REQUIRED) + private String caseId; + + @Schema(description = "项目id", requiredMode = Schema.RequiredMode.REQUIRED) + private String projectId; +} diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseAttachmentService.java b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseAttachmentService.java index fa6896a373..85c6e83f6e 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseAttachmentService.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseAttachmentService.java @@ -7,7 +7,8 @@ import io.metersphere.functional.domain.FunctionalCaseAttachmentExample; 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.functional.request.FunctionalCaseAssociationFileRequest; +import io.metersphere.functional.request.FunctionalCaseDeleteFileRequest; import io.metersphere.functional.request.FunctionalCaseFileRequest; import io.metersphere.project.domain.FileAssociation; import io.metersphere.project.dto.filemanagement.FileInfo; @@ -24,6 +25,7 @@ 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.commons.lang3.BooleanUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -52,6 +54,9 @@ public class FunctionalCaseAttachmentService { @Resource private FileAssociationService fileAssociationService; + private static final String UPLOAD_FILE = "/attachment/upload/file"; + private static final String DELETED_FILE = "/attachment/delete/file"; + /** * 保存本地上传文件和用例关联关系 * @@ -70,16 +75,16 @@ public class FunctionalCaseAttachmentService { /** * 功能用例上传附件 * - * @param request request - * @param files files + * @param projectId projectId + * @param files files */ - public void uploadFile(FunctionalCaseAddRequest request, String caseId, List files, Boolean isLocal, String userId) { + public void uploadFile(String projectId, String caseId, List files, Boolean isLocal, String userId) { if (CollectionUtils.isNotEmpty(files)) { files.forEach(file -> { String fileId = IDGenerator.nextStr(); FileRequest fileRequest = new FileRequest(); fileRequest.setFileName(file.getOriginalFilename()); - fileRequest.setFolder(DefaultRepositoryDir.getFunctionalCaseDir(request.getProjectId(), caseId) + "/" + fileId); + fileRequest.setFolder(DefaultRepositoryDir.getFunctionalCaseDir(projectId, caseId) + "/" + fileId); fileRequest.setStorage(StorageType.MINIO.name()); try { fileService.upload(file, fileRequest); @@ -289,4 +294,21 @@ public class FunctionalCaseAttachmentService { } return fileAssociationMap; } + + public void uploadOrAssociationFile(FunctionalCaseAssociationFileRequest request, MultipartFile file, String userId) { + Optional.ofNullable(file).ifPresent(item -> this.uploadFile(request.getProjectId(), request.getCaseId(), Arrays.asList(file), Boolean.TRUE, userId)); + + if (CollectionUtils.isNotEmpty(request.getFileIds())) { + this.association(request.getFileIds(), request.getCaseId(), userId, UPLOAD_FILE, request.getProjectId()); + } + } + + public void deleteFile(FunctionalCaseDeleteFileRequest request, String userId) { + if (BooleanUtils.isTrue(request.getAttachment().getLocal())) { + this.deleteCaseAttachment(Arrays.asList(request.getAttachment().getId()), request.getCaseId(), userId); + } + if (BooleanUtils.isFalse(request.getAttachment().getLocal())) { + this.unAssociation(Arrays.asList(request.getAttachment().getId()), DELETED_FILE, userId, request.getProjectId()); + } + } } \ No newline at end of file diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseService.java b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseService.java index c3c9b5e592..8d887d01c6 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseService.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseService.java @@ -95,7 +95,7 @@ public class FunctionalCaseService { FunctionalCase functionalCase = addCase(caseId, request, userId); //上传文件 - functionalCaseAttachmentService.uploadFile(request, caseId, files, true, userId); + functionalCaseAttachmentService.uploadFile(request.getProjectId(), caseId, files, true, userId); //关联附件 if (CollectionUtils.isNotEmpty(request.getRelateFileMetaIds())) { @@ -275,7 +275,7 @@ public class FunctionalCaseService { } //上传新文件 - functionalCaseAttachmentService.uploadFile(request, request.getId(), files, true, userId); + functionalCaseAttachmentService.uploadFile(request.getProjectId(), request.getId(), files, true, userId); //关联新附件 if (CollectionUtils.isNotEmpty(request.getRelateFileMetaIds())) { diff --git a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseAttachmentControllerTests.java b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseAttachmentControllerTests.java index ab684c37ae..87653c8d61 100644 --- a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseAttachmentControllerTests.java +++ b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseAttachmentControllerTests.java @@ -1,5 +1,8 @@ package io.metersphere.functional.controller; +import io.metersphere.functional.dto.FunctionalCaseAttachmentDTO; +import io.metersphere.functional.request.FunctionalCaseAssociationFileRequest; +import io.metersphere.functional.request.FunctionalCaseDeleteFileRequest; import io.metersphere.functional.request.FunctionalCaseFileRequest; import io.metersphere.functional.utils.FileBaseUtils; import io.metersphere.project.dto.filemanagement.request.FileMetadataTableRequest; @@ -23,6 +26,7 @@ import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.SqlConfig; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.LinkedMultiValueMap; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -50,6 +54,9 @@ public class FunctionalCaseAttachmentControllerTests extends BaseTest { public static final String ATTACHMENT_CHECK_UPDATE_URL = "/attachment/check-update"; public static final String ATTACHMENT_UPDATE_URL = "/attachment/update/"; public static final String ATTACHMENT_TRANSFER_URL = "/attachment/transfer"; + public static final String UPLOAD_FILE_URL = "/attachment/upload/file"; + public static final String DELETE_FILE_URL = "/attachment/delete/file"; + @Test @Order(1) @@ -171,4 +178,49 @@ public class FunctionalCaseAttachmentControllerTests extends BaseTest { this.requestPost(ATTACHMENT_TRANSFER_URL, request); } + + + @Test + @Order(7) + public void testUploadFile() throws Exception { + String filePath = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/test.JPG")).getPath(); + MockMultipartFile file = new MockMultipartFile("file", "file_re-upload.JPG", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath)); + LinkedMultiValueMap paramMap = new LinkedMultiValueMap<>(); + FunctionalCaseAssociationFileRequest request = new FunctionalCaseAssociationFileRequest(); + request.setCaseId("TEST_FUNCTIONAL_CASE_ATTACHMENT_ID_1"); + request.setProjectId("WX_TEST_PROJECT_ID"); + paramMap.add("request", JSON.toJSONString(request)); + paramMap.add("file", file); + this.requestMultipart(UPLOAD_FILE_URL, paramMap); + + request.setFileIds(Arrays.asList("wx_test_file_association_1")); + paramMap = new LinkedMultiValueMap<>(); + paramMap.add("request", JSON.toJSONString(request)); + paramMap.add("file", file); + this.requestMultipart(UPLOAD_FILE_URL, paramMap); + + } + + @Test + @Order(8) + public void testDeleteFile() throws Exception { + FunctionalCaseDeleteFileRequest request = new FunctionalCaseDeleteFileRequest(); + FunctionalCaseAttachmentDTO attachmentDTO = new FunctionalCaseAttachmentDTO(); + //覆盖率 + request.setCaseId("TEST_FUNCTIONAL_CASE_ATTACHMENT_ID_1"); + request.setProjectId("WX_TEST_PROJECT_ID"); + request.setAttachment(attachmentDTO); + this.requestPost(DELETE_FILE_URL, request); + + + attachmentDTO.setId("wx_test_file_association_1"); + attachmentDTO.setLocal(false); + this.requestPost(DELETE_FILE_URL, request); + + attachmentDTO.setId("TEST_ATTACHMENT_ID"); + attachmentDTO.setLocal(true); + this.requestPost(DELETE_FILE_URL, request); + } + + } diff --git a/backend/services/case-management/src/test/resources/dml/init_attachment_test.sql b/backend/services/case-management/src/test/resources/dml/init_attachment_test.sql index a43313eb11..7dfdf6c052 100644 --- a/backend/services/case-management/src/test/resources/dml/init_attachment_test.sql +++ b/backend/services/case-management/src/test/resources/dml/init_attachment_test.sql @@ -19,3 +19,7 @@ INSERT INTO functional_case_attachment(id, case_id, file_id, file_name, size, lo 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 ('wx_test_file_association', 'functional_case', 'WX_TEST_FUNCTIONAL_CASE_ID', 'wx_file_id', '1', '1', 1698983271536, 'admin', 1698983271536, 'admin'); 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 ('wx_file_id', '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 ('wx_test_file_association_1', 'functional_case', 'WX_TEST_FUNCTIONAL_CASE_ID', 'wx_file_id_1', '1', '1', 1698983271536, 'admin', 1698983271536, 'admin'); +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 ('wx_file_id_1', 'formItem', 'ts', 2502, 1698058347559, 1698058347559, '100001100001', 'MINIO', 'admin', 'admin', NULL, NULL, 'root', '100001100001/1127016598347779', b'1', '1127016598347779', '1127016598347779');