diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/controller/BugAttachmentController.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/controller/BugAttachmentController.java new file mode 100644 index 0000000000..6a4f75b5ae --- /dev/null +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/controller/BugAttachmentController.java @@ -0,0 +1,150 @@ +package io.metersphere.bug.controller; + +import io.metersphere.bug.dto.request.BugDeleteFileRequest; +import io.metersphere.bug.dto.request.BugFileSourceRequest; +import io.metersphere.bug.dto.request.BugFileTransferRequest; +import io.metersphere.bug.dto.request.BugUploadFileRequest; +import io.metersphere.bug.dto.response.BugFileDTO; +import io.metersphere.bug.service.BugAttachmentService; +import io.metersphere.project.dto.filemanagement.request.FileMetadataTableRequest; +import io.metersphere.project.dto.filemanagement.response.FileInformationResponse; +import io.metersphere.project.service.FileAssociationService; +import io.metersphere.project.service.FileMetadataService; +import io.metersphere.project.service.FileModuleService; +import io.metersphere.sdk.constants.PermissionConstants; +import io.metersphere.system.dto.sdk.BaseTreeNode; +import io.metersphere.system.security.CheckOwner; +import io.metersphere.system.utils.Pager; +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 org.apache.shiro.authz.annotation.Logical; +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.List; + +@Tag(name = "缺陷管理") +@RestController +@RequestMapping("/bug/attachment") +public class BugAttachmentController { + + @Resource + private FileModuleService fileModuleService; + @Resource + private FileMetadataService fileMetadataService; + @Resource + private BugAttachmentService bugAttachmentService; + @Resource + private FileAssociationService fileAssociationService; + + @GetMapping("/list/{bugId}") + @Operation(summary = "缺陷管理-附件-列表") + @RequiresPermissions(PermissionConstants.PROJECT_BUG_READ) + @CheckOwner(resourceId = "#bugId", resourceType = "bug") + public List page(@PathVariable String bugId) { + return bugAttachmentService.getAllBugFiles(bugId); + } + + @PostMapping("/file/page") + @Operation(summary = "缺陷管理-附件-关联文件分页接口") + @RequiresPermissions(PermissionConstants.PROJECT_BUG_READ) + @CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project") + public Pager> page(@Validated @RequestBody FileMetadataTableRequest request) { + return fileMetadataService.page(request); + } + + @PostMapping("/upload") + @Operation(summary = "缺陷管理-附件-上传/关联文件") + @RequiresPermissions(PermissionConstants.PROJECT_BUG_UPDATE) + @CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project") + public void uploadFile(@Validated @RequestPart("request") BugUploadFileRequest request, @RequestPart(value = "file", required = false) MultipartFile file) { + bugAttachmentService.uploadFile(request, file, SessionUtils.getUserId()); + } + + @PostMapping("/delete") + @Operation(summary = "缺陷管理-附件-删除/取消关联文件") + @RequiresPermissions(PermissionConstants.PROJECT_BUG_UPDATE) + @CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project") + public void deleteFile(@RequestBody BugDeleteFileRequest request) { + bugAttachmentService.deleteFile(request); + } + + @PostMapping("/preview") + @Operation(summary = "缺陷管理-附件-预览") + @RequiresPermissions(PermissionConstants.PROJECT_BUG_READ) + @CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project") + public ResponseEntity preview(@Validated @RequestBody BugFileSourceRequest request) throws Exception { + if (request.getAssociated()) { + // 文件库 + return fileMetadataService.downloadPreviewImgById(request.getFileId()); + } else { + // 本地 + return bugAttachmentService.downloadOrPreview(request); + } + } + + @PostMapping("/download") + @Operation(summary = "缺陷管理-附件-下载") + @RequiresPermissions(PermissionConstants.PROJECT_BUG_READ) + @CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project") + public ResponseEntity download(@Validated @RequestBody BugFileSourceRequest request) throws Exception { + if (request.getAssociated()) { + // 文件库 + return fileMetadataService.downloadById(request.getFileId()); + } else { + // 本地 + return bugAttachmentService.downloadOrPreview(request); + } + } + + @GetMapping("/transfer/options/{projectId}") + @Operation(summary = "缺陷管理-附件-转存选项") + @RequiresPermissions(PermissionConstants.PROJECT_BUG_READ) + @CheckOwner(resourceId = "#projectId", resourceType = "project") + public List options(@PathVariable String projectId) { + return fileModuleService.getTree(projectId); + } + + @PostMapping("/transfer") + @Operation(summary = "缺陷管理-附件-本地转存") + @RequiresPermissions(PermissionConstants.PROJECT_BUG_READ) + @CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project") + public String transfer(@Validated @RequestBody BugFileTransferRequest request) { + return bugAttachmentService.transfer(request, SessionUtils.getUserId()); + } + + @PostMapping("/check-update") + @Operation(summary = "缺陷管理-附件-检查关联文件是否存在更新") + @RequiresPermissions(PermissionConstants.PROJECT_BUG_READ) + public List checkUpdate(@RequestBody List fileIds) { + return fileAssociationService.checkFilesVersion(fileIds); + } + + @PostMapping("/update") + @Operation(summary = "缺陷管理-附件-更新关联文件") + @RequiresPermissions(PermissionConstants.PROJECT_BUG_READ) + @CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project") + public String update(@Validated @RequestBody BugDeleteFileRequest request) { + return bugAttachmentService.upgrade(request, SessionUtils.getUserId()); + } + + @PostMapping("/upload/md/file") + @Operation(summary = "缺陷管理-富文本附件-上传") + @RequiresPermissions(logical = Logical.OR, value = {PermissionConstants.PROJECT_BUG_ADD, PermissionConstants.PROJECT_BUG_UPDATE}) + public String upload(@RequestParam("file") MultipartFile file) throws Exception { + return bugAttachmentService.uploadMdFile(file); + } + + @PostMapping(value = "/preview/md/compressed") + @Operation(summary = "缺陷管理-富文本缩略图-预览") + @RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ) + @CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project") + public ResponseEntity previewMdImg(@Validated @RequestBody BugFileSourceRequest request) { + return bugAttachmentService.downloadOrPreview(request); + } +} diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/controller/BugController.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/controller/BugController.java index 4fda08eae3..4d8a163385 100644 --- a/backend/services/bug-management/src/main/java/io/metersphere/bug/controller/BugController.java +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/controller/BugController.java @@ -110,6 +110,13 @@ public class BugController { bugService.addOrUpdate(request, files, SessionUtils.getUserId(), SessionUtils.getCurrentOrganizationId(), true); } + @GetMapping("/get/{id}") + @Operation(summary = "缺陷管理-列表-详情&&编辑&&复制") + @RequiresPermissions(PermissionConstants.PROJECT_BUG_READ) + public void get(@PathVariable String id) { + bugService.get(id); + } + @GetMapping("/delete/{id}") @Operation(summary = "缺陷管理-列表-删除缺陷") @RequiresPermissions(PermissionConstants.PROJECT_BUG_DELETE) diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugDeleteFileRequest.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugDeleteFileRequest.java new file mode 100644 index 0000000000..c4b68e8061 --- /dev/null +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugDeleteFileRequest.java @@ -0,0 +1,25 @@ +package io.metersphere.bug.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class BugDeleteFileRequest implements Serializable { + + @Schema(description = "缺陷ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{bug.id.not_blank}") + private String bugId; + + @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{bug.project_id.not_blank}") + private String projectId; + + @Schema(description = "文件关系ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String refId; + + @Schema(description = "是否关联", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean associated; +} diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugEditRequest.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugEditRequest.java index 5f38dd04e3..1d8162f372 100644 --- a/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugEditRequest.java +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugEditRequest.java @@ -46,7 +46,7 @@ public class BugEditRequest { @Schema(description = "自定义字段集合") private List customFields; - @Schema(description = "删除的本地附件集合, 文件ID") + @Schema(description = "删除的本地附件集合, {文件ID") private List deleteLocalFileIds; @Schema(description = "取消关联附件关系ID集合, 关联关系ID") diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugFileSourceRequest.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugFileSourceRequest.java new file mode 100644 index 0000000000..7548b8e70e --- /dev/null +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugFileSourceRequest.java @@ -0,0 +1,25 @@ +package io.metersphere.bug.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class BugFileSourceRequest implements Serializable { + + @Schema(description = "缺陷ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{bug.id.not_blank}") + private String bugId; + + @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{bug.project_id.not_blank}") + private String projectId; + + @Schema(description = "文件关系ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String fileId; + + @Schema(description = "是否关联", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean associated; +} diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugFileTransferRequest.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugFileTransferRequest.java new file mode 100644 index 0000000000..e6c5bdb0f6 --- /dev/null +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugFileTransferRequest.java @@ -0,0 +1,13 @@ +package io.metersphere.bug.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class BugFileTransferRequest extends BugFileSourceRequest{ + + @Schema(description = "转存的模块id",requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{functional_case.module_id.not_blank}") + private String moduleId; +} diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugQuickEditRequest.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugQuickEditRequest.java new file mode 100644 index 0000000000..56608655b8 --- /dev/null +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugQuickEditRequest.java @@ -0,0 +1,45 @@ +package io.metersphere.bug.dto.request; + +import io.metersphere.bug.dto.response.BugCustomFieldDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = false) +public class BugQuickEditRequest { + + @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{bug.id.not_blank}") + @Size(min = 1, max = 50, message = "{bug.id.length_range}") + private String id; + + @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{bug.project_id.not_blank}") + @Size(min = 1, max = 50, message = "{bug.project_id.length_range}") + private String projectId; + + @Schema(description = "模板ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{bug.template_id.not_blank}") + @Size(min = 1, max = 50, message = "{bug.template_id.length_range}") + private String templateId; + + @Schema(description = "处理人") + private String handleUser; + + @Schema(description = "状态") + private String status; + + @Schema(description = "标签") + private List tags; + + @Schema(description = "缺陷内容") + private String description; + + @Schema(description = "自定义字段集合") + private List customFields; +} diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugUploadFileRequest.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugUploadFileRequest.java new file mode 100644 index 0000000000..43966dc0c1 --- /dev/null +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/request/BugUploadFileRequest.java @@ -0,0 +1,46 @@ +package io.metersphere.bug.dto.request; + +import io.metersphere.validation.groups.Created; +import io.metersphere.validation.groups.Updated; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@Data +public class BugUploadFileRequest implements Serializable { + + @Schema(description = "缺陷ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{bug.id.not_blank}") + private String bugId; + + @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{bug.project_id.not_blank}") + private String projectId; + + @Schema(description = "不勾选的ID") + private List excludeIds; + + @Schema(description = "勾选的ID") + @Valid + private List< + @NotBlank(message = "{id must not be blank}", groups = {Created.class, Updated.class}) + String + > selectIds = new ArrayList<>(); + + @Schema(description = "是否全选", requiredMode = Schema.RequiredMode.REQUIRED) + private boolean selectAll; + + @Schema(description = "模块ID(根据模块树查询时要把当前节点以及子节点都放在这里。)") + private List moduleIds; + + @Schema(description = "文件类型") + private String fileType; + + @Schema(description = "关键字") + private String keyword; +} diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/response/BugDTO.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/response/BugDTO.java index 60e8f69ce1..05e6dd4b71 100644 --- a/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/response/BugDTO.java +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/response/BugDTO.java @@ -3,6 +3,7 @@ package io.metersphere.bug.dto.response; import io.metersphere.bug.domain.Bug; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import lombok.EqualsAndHashCode; import java.util.List; @@ -10,6 +11,7 @@ import java.util.List; * @author song-cc-rock */ @Data +@EqualsAndHashCode(callSuper = false) public class BugDTO extends Bug { @Schema(description = "缺陷内容") diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/response/BugDetailDTO.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/response/BugDetailDTO.java new file mode 100644 index 0000000000..d331311190 --- /dev/null +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/response/BugDetailDTO.java @@ -0,0 +1,18 @@ +package io.metersphere.bug.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * @author song-cc-rock + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class BugDetailDTO extends BugDTO { + + @Schema(description = "附件集合") + List attachments; +} diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/response/BugFileDTO.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/response/BugFileDTO.java index 1500b9b855..958e92b565 100644 --- a/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/response/BugFileDTO.java +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/dto/response/BugFileDTO.java @@ -1,11 +1,15 @@ package io.metersphere.bug.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class BugFileDTO { @Schema(description = "关系ID") diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/enums/BugAttachmentSourceType.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/enums/BugAttachmentSourceType.java index 3bb8d7a464..d437589ba9 100644 --- a/backend/services/bug-management/src/main/java/io/metersphere/bug/enums/BugAttachmentSourceType.java +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/enums/BugAttachmentSourceType.java @@ -7,7 +7,7 @@ public enum BugAttachmentSourceType { */ ATTACHMENT, /** - * MD图片 + * 缺陷内容 */ - MD_PIC; + CONTENT; } diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/mapper/ExtBugMapper.xml b/backend/services/bug-management/src/main/java/io/metersphere/bug/mapper/ExtBugMapper.xml index 72bccc7720..a7d6bc514d 100644 --- a/backend/services/bug-management/src/main/java/io/metersphere/bug/mapper/ExtBugMapper.xml +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/mapper/ExtBugMapper.xml @@ -15,7 +15,7 @@ diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/service/BugAttachmentService.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/service/BugAttachmentService.java index 398d5d8381..079a843ef8 100644 --- a/backend/services/bug-management/src/main/java/io/metersphere/bug/service/BugAttachmentService.java +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/service/BugAttachmentService.java @@ -1,36 +1,89 @@ package io.metersphere.bug.service; +import io.metersphere.bug.domain.Bug; import io.metersphere.bug.domain.BugLocalAttachment; import io.metersphere.bug.domain.BugLocalAttachmentExample; +import io.metersphere.bug.dto.request.BugDeleteFileRequest; +import io.metersphere.bug.dto.request.BugFileSourceRequest; +import io.metersphere.bug.dto.request.BugFileTransferRequest; +import io.metersphere.bug.dto.request.BugUploadFileRequest; import io.metersphere.bug.dto.response.BugFileDTO; +import io.metersphere.bug.enums.BugAttachmentSourceType; +import io.metersphere.bug.enums.BugPlatform; import io.metersphere.bug.mapper.BugLocalAttachmentMapper; +import io.metersphere.bug.mapper.BugMapper; +import io.metersphere.plugin.platform.dto.request.SyncAttachmentToPlatformRequest; +import io.metersphere.plugin.platform.enums.SyncAttachmentType; import io.metersphere.project.domain.FileAssociation; import io.metersphere.project.domain.FileAssociationExample; import io.metersphere.project.domain.FileMetadata; import io.metersphere.project.domain.FileMetadataExample; +import io.metersphere.project.dto.filemanagement.FileAssociationDTO; +import io.metersphere.project.dto.filemanagement.FileLogRecord; +import io.metersphere.project.dto.filemanagement.request.FileMetadataTableRequest; +import io.metersphere.project.dto.filemanagement.response.FileInformationResponse; import io.metersphere.project.mapper.FileAssociationMapper; import io.metersphere.project.mapper.FileMetadataMapper; +import io.metersphere.project.service.FileAssociationService; +import io.metersphere.project.service.FileMetadataService; +import io.metersphere.project.service.FileService; +import io.metersphere.sdk.constants.DefaultRepositoryDir; +import io.metersphere.sdk.constants.StorageType; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.file.FileCenter; +import io.metersphere.sdk.file.FileRequest; +import io.metersphere.sdk.util.BeanUtils; import io.metersphere.sdk.util.FileAssociationSourceUtil; +import io.metersphere.sdk.util.LogUtils; +import io.metersphere.sdk.util.Translator; +import io.metersphere.system.log.constants.OperationLogModule; +import io.metersphere.system.uid.IDGenerator; import jakarta.annotation.Resource; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import org.springframework.util.unit.DataSize; +import org.springframework.web.multipart.MultipartFile; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; +import java.util.stream.Stream; @Service public class BugAttachmentService { @Resource - private FileAssociationMapper fileAssociationMapper; + private BugMapper bugMapper; + @Resource + private FileService fileService; @Resource private FileMetadataMapper fileMetadataMapper; @Resource + private FileMetadataService fileMetadataService; + @Resource + @Lazy + private BugSyncExtraService bugSyncExtraService; + @Resource + private FileAssociationMapper fileAssociationMapper; + @Resource + private FileAssociationService fileAssociationService; + @Resource private BugLocalAttachmentMapper bugLocalAttachmentMapper; + @Value("50MB") + private DataSize maxFileSize; + /** * 查询缺陷的附件集合 * @param bugId 缺陷ID @@ -39,7 +92,7 @@ public class BugAttachmentService { public List getAllBugFiles(String bugId) { List bugFiles = new ArrayList<>(); BugLocalAttachmentExample localAttachmentExample = new BugLocalAttachmentExample(); - localAttachmentExample.createCriteria().andBugIdEqualTo(bugId); + localAttachmentExample.createCriteria().andBugIdEqualTo(bugId).andSourceEqualTo(BugAttachmentSourceType.ATTACHMENT.name()); List bugLocalAttachments = bugLocalAttachmentMapper.selectByExample(localAttachmentExample); if (!CollectionUtils.isEmpty(bugLocalAttachments)) { bugLocalAttachments.forEach(localFile -> { @@ -53,10 +106,7 @@ public class BugAttachmentService { List fileAssociations = fileAssociationMapper.selectByExample(associationExample); if (!CollectionUtils.isEmpty(fileAssociations)) { List associateFileIds = fileAssociations.stream().map(FileAssociation::getFileId).toList(); - FileMetadataExample metadataExample = new FileMetadataExample(); - metadataExample.createCriteria().andIdIn(associateFileIds); - List fileMetadataList = fileMetadataMapper.selectByExample(metadataExample); - Map fileMetadataMap = fileMetadataList.stream().collect(Collectors.toMap(FileMetadata::getId, v -> v)); + Map fileMetadataMap = getLinkFileMetaMap(associateFileIds); fileAssociations.forEach(associatedFile -> { FileMetadata associatedFileMetadata = fileMetadataMap.get(associatedFile.getFileId()); BugFileDTO associatedFileDTO = BugFileDTO.builder().refId(associatedFile.getId()).fileId(associatedFile.getFileId()).fileName(associatedFileMetadata.getName() + "." + associatedFileMetadata.getType()) @@ -68,6 +118,329 @@ public class BugAttachmentService { return bugFiles; } + /** + * 上传附件->缺陷 (同步至平台) + * @param request 缺陷关联文件请求参数 + * @param file 文件 + * @param currentUser 当前用户 + */ + public void uploadFile(BugUploadFileRequest request, MultipartFile file, String currentUser) { + Bug bug = bugMapper.selectByPrimaryKey(request.getBugId()); + File tempFileDir = new File(Objects.requireNonNull(this.getClass().getClassLoader().getResource(StringUtils.EMPTY)).getPath() + File.separator + "tmp" + + File.separator); + List platformAttachments = new ArrayList<>(); + if (file == null) { + // 关联文件 + List relateFileIds; + if (request.isSelectAll()) { + // 全选 + FileMetadataTableRequest metadataTableRequest = new FileMetadataTableRequest(); + BeanUtils.copyBean(metadataTableRequest, request); + List relateAllFiles = fileMetadataService.list(metadataTableRequest); + if (!CollectionUtils.isEmpty(request.getExcludeIds())) { + relateAllFiles.removeIf(relateFile -> request.getExcludeIds().contains(relateFile.getId())); + } + relateFileIds = relateAllFiles.stream().map(FileInformationResponse::getId).collect(Collectors.toList()); + } else { + // 非全选 + relateFileIds= request.getSelectIds(); + } + // 缺陷与文件库关联 + if (CollectionUtils.isEmpty(relateFileIds)) { + return; + } + List syncLinkFiles = uploadLinkFile(bug.getId(), bug.getPlatformBugId(), request.getProjectId(), tempFileDir, relateFileIds, currentUser, bug.getPlatform(), false); + platformAttachments.addAll(syncLinkFiles); + } else { + // 上传文件 + List syncLocalFiles = uploadLocalFile(bug.getId(), bug.getPlatformBugId(), request.getProjectId(), tempFileDir, file, currentUser, bug.getPlatform()); + platformAttachments.addAll(syncLocalFiles); + } + + // 同步至第三方(异步调用) + if (!StringUtils.equals(bug.getPlatform(), BugPlatform.LOCAL.getName())) { + bugSyncExtraService.syncAttachmentToPlatform(platformAttachments, request.getProjectId(), tempFileDir); + } + } + + /** + * 删除或取消关联附件->缺陷 (同步至平台) + * @param request 删除文件请求参数 + */ + public void deleteFile(BugDeleteFileRequest request) { + Bug bug = bugMapper.selectByPrimaryKey(request.getBugId()); + File tempFileDir = new File(Objects.requireNonNull(this.getClass().getClassLoader().getResource(StringUtils.EMPTY)).getPath() + File.separator + "tmp" + + File.separator); + List platformAttachments = new ArrayList<>(); + if (request.getAssociated()) { + // 取消关联 + List syncLinkFiles = unLinkFile(bug.getPlatformBugId(), request.getProjectId(), + tempFileDir, request.getRefId(), bug.getCreateUser(), bug.getPlatform(), false); + platformAttachments.addAll(syncLinkFiles); + } else { + // 删除本地上传的文件 + List syncLocalFiles = + deleteLocalFile(bug.getId(), bug.getPlatformBugId(), request.getProjectId(), tempFileDir, request.getRefId(), bug.getPlatform(), true); + platformAttachments.addAll(syncLocalFiles); + } + // 同步至第三方(异步调用) + if (!StringUtils.equals(bug.getPlatform(), BugPlatform.LOCAL.getName())) { + bugSyncExtraService.syncAttachmentToPlatform(platformAttachments, request.getProjectId(), tempFileDir); + } + } + + /** + * 下载或预览本地文件 + * @param request 文件请求参数 + * @return 文件字节流 + */ + public ResponseEntity downloadOrPreview(BugFileSourceRequest request) { + BugLocalAttachment attachment = getLocalFile(request); + if (attachment == null) { + return ResponseEntity.ok().contentType(MediaType.parseMediaType("application/octet-stream")).body(null); + } + byte[] bytes = getLocalFileBytes(attachment, request.getProjectId(), request.getBugId()); + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType("application/octet-stream")) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + attachment.getFileName() + "\"") + .body(bytes); + } + + /** + * 转存附近至文件库 + * @param request 请求参数 + * @param currentUser 当前用户 + * @return 文件ID + */ + public String transfer(BugFileTransferRequest request, String currentUser) { + BugLocalAttachment attachment = getLocalFile(request); + if (attachment == null) { + throw new MSException(Translator.get("file.transfer.error")); + } + byte[] bytes = getLocalFileBytes(attachment, request.getProjectId(), request.getBugId()); + String fileId; + try { + FileAssociationDTO association = new FileAssociationDTO(attachment.getFileName(), bytes, attachment.getBugId(), + FileAssociationSourceUtil.SOURCE_TYPE_BUG, createFileLogRecord(currentUser, request.getProjectId())); + association.setModuleId(request.getModuleId()); + fileId = fileAssociationService.transferAndAssociation(association); + // 删除本地上传的附件 + deleteLocalFile(request.getBugId(), null, request.getProjectId(), null, attachment.getId(), null, false); + } catch (Exception e) { + throw new MSException(Translator.get("file.transfer.error")); + } + return fileId; + } + + /** + * 更新文件至最新版本 + * @param request 请求参数 + * @param currentUser 当前用户 + * @return 文件ID + */ + public String upgrade(BugDeleteFileRequest request, String currentUser) { + Bug bug = bugMapper.selectByPrimaryKey(request.getBugId()); + File tempFileDir = new File(Objects.requireNonNull(this.getClass().getClassLoader().getResource(StringUtils.EMPTY)).getPath() + File.separator + "tmp" + + File.separator); + // 取消关联附件->同步 + List syncUnlinkFiles = unLinkFile(bug.getPlatformBugId(), request.getProjectId(), + tempFileDir, request.getRefId(), currentUser, bug.getPlatform(), true); + // 更新后的文件需要同步 + String upgradeFileId = fileAssociationService.upgrade(request.getRefId(), createFileLogRecord(currentUser, request.getProjectId())); + // 关联附件->同步 + List syncLinkFiles = uploadLinkFile(bug.getId(), bug.getPlatformBugId(), request.getProjectId(), tempFileDir, List.of(upgradeFileId), currentUser, bug.getPlatform(), true); + List platformAttachments = Stream.concat(syncUnlinkFiles.stream(), syncLinkFiles.stream()).toList(); + if (!StringUtils.equals(bug.getPlatform(), BugPlatform.LOCAL.getName())) { + bugSyncExtraService.syncAttachmentToPlatform(platformAttachments, request.getProjectId(), tempFileDir); + } + return upgradeFileId; + } + + /** + * 上传MD文件 + * @param file 文件 + * @return 文件ID + */ + public String uploadMdFile(MultipartFile file) { + String fileName = StringUtils.trim(file.getOriginalFilename()); + if (file.getSize() > maxFileSize.toBytes()) { + throw new MSException(Translator.get("file.size.is.too.large")); + } + if (StringUtils.isBlank(fileName)) { + throw new MSException(Translator.get("file.name.cannot.be.empty")); + } + String fileId = IDGenerator.nextStr(); + FileRequest fileRequest = new FileRequest(); + fileRequest.setFileName(file.getOriginalFilename()); + String systemTempDir = DefaultRepositoryDir.getSystemTempDir(); + fileRequest.setFolder(systemTempDir + "/" + fileId); + try { + FileCenter.getDefaultRepository().saveFile(file, fileRequest); + } catch (Exception e) { + LogUtils.error(e); + throw new MSException(e.getMessage()); + } + return fileId; + } + + + /** + * 获取本地文件字节流 + * @param attachment 本地附件信息 + * @param projectId 项目ID + * @param bugId 缺陷ID + * @return 文件字节流 + */ + public byte[] getLocalFileBytes(BugLocalAttachment attachment, String projectId, String bugId) { + FileRequest fileRequest = buildBugFileRequest(projectId, bugId, attachment.getFileId(), attachment.getFileName()); + byte[] bytes; + try { + bytes = fileService.download(fileRequest); + } catch (Exception e) { + throw new MSException("download file error!"); + } + return bytes; + } + + /** + * 上传关联的文件(同步至平台) + * @param bugId 缺陷ID + * @param platformBugKey 平台缺陷ID + * @param projectId 项目ID + * @param tmpFileDir 临时文件目录 + * @param linkFileIds 关联文件ID集合 + * @param currentUser 创建人 + * @param platformName 平台名称 + * @return 同步至平台的附件集合 + */ + private List uploadLinkFile(String bugId, String platformBugKey, String projectId, File tmpFileDir, + List linkFileIds, String currentUser, String platformName, boolean syncOnly) { + if (!syncOnly) { + fileAssociationService.association(bugId, FileAssociationSourceUtil.SOURCE_TYPE_BUG, linkFileIds, createFileLogRecord(currentUser, projectId)); + } + // 同步新关联的附件至平台 + List linkSyncFiles = new ArrayList<>(); + if (!StringUtils.equals(platformName, BugPlatform.LOCAL.getName())) { + Map fileMetadataMap = getLinkFileMetaMap(linkFileIds); + linkFileIds.forEach(fileId -> { + // 平台同步附件集合 + FileMetadata meta = fileMetadataMap.get(fileId); + if (meta != null) { + try { + File uploadTmpFile = new File(tmpFileDir, meta.getName() + "." + meta.getType()); + byte[] fileByte = fileMetadataService.getFileByte(meta); + FileUtils.writeByteArrayToFile(uploadTmpFile, fileByte); + linkSyncFiles.add(new SyncAttachmentToPlatformRequest(platformBugKey, uploadTmpFile, SyncAttachmentType.UPLOAD.syncOperateType())); + } catch (IOException e) { + throw new MSException(Translator.get("bug_attachment_upload_error")); + } + } + }); + } + return linkSyncFiles; + } + + /** + * 上传本地的文件(同步至平台) + * @param bugId 缺陷ID + * @param platformBugKey 平台缺陷ID + * @param projectId 项目ID + * @param tmpFileDir 临时文件目录 + * @param file 上传的本地文件 + * @param currentUser 创建人 + * @param platformName 平台名称 + * @return 同步至平台的附件集合 + */ + private List uploadLocalFile(String bugId, String platformBugKey, String projectId, File tmpFileDir, + MultipartFile file, String currentUser, String platformName) { + BugLocalAttachment record = new BugLocalAttachment(); + record.setId(IDGenerator.nextStr()); + record.setBugId(bugId); + record.setFileId(IDGenerator.nextStr()); + record.setFileName(file.getOriginalFilename()); + record.setSize(file.getSize()); + record.setSource(BugAttachmentSourceType.ATTACHMENT.name()); + record.setCreateTime(System.currentTimeMillis()); + record.setCreateUser(currentUser); + bugLocalAttachmentMapper.insert(record); + List localSyncFiles = new ArrayList<>(); + FileRequest fileRequest = buildBugFileRequest(projectId, bugId, record.getFileId(), file.getOriginalFilename()); + try { + fileService.upload(file, fileRequest); + if (!StringUtils.equals(platformName, BugPlatform.LOCAL.getName())) { + // 非本地平台,同步附件到平台 + File uploadTmpFile = new File(tmpFileDir, Objects.requireNonNull(file.getOriginalFilename())).toPath().normalize().toFile(); + FileUtils.writeByteArrayToFile(uploadTmpFile, file.getBytes()); + localSyncFiles.add(new SyncAttachmentToPlatformRequest(platformBugKey, uploadTmpFile, SyncAttachmentType.UPLOAD.syncOperateType())); + } + } catch (Exception e) { + throw new MSException(Translator.get("bug_attachment_upload_error")); + } + return localSyncFiles; + } + + /** + * 取消关联文件(同步至平台) + * @param platformBugKey 平台缺陷ID + * @param projectId 项目ID + * @param tmpFileDir 临时文件目录 + * @param refId 取消关联的文件引用ID + * @param currentUser 创建人 + * @param platformName 平台名称 + * @return 同步至平台的附件集合 + */ + private List unLinkFile(String platformBugKey, String projectId, File tmpFileDir, + String refId, String currentUser, String platformName, boolean syncOnly) { + List linkSyncFiles = new ArrayList<>(); + FileAssociation association = fileAssociationMapper.selectByPrimaryKey(refId); + FileMetadataExample example = new FileMetadataExample(); + example.createCriteria().andIdEqualTo(association.getFileId()); + FileMetadata fileMetadata = fileMetadataMapper.selectByExample(example).get(0); + // 取消关联的附件同步至平台 + if (!StringUtils.equals(platformName, BugPlatform.LOCAL.getName())) { + File deleteTmpFile = new File(tmpFileDir, fileMetadata.getName() + "." + fileMetadata.getType()); + linkSyncFiles.add(new SyncAttachmentToPlatformRequest(platformBugKey, deleteTmpFile, SyncAttachmentType.DELETE.syncOperateType())); + } + // 取消关联的附件, FILE_ASSOCIATION表 + if (!syncOnly) { + fileAssociationService.deleteByIds(List.of(refId), createFileLogRecord(currentUser, projectId)); + } + return linkSyncFiles; + } + + /** + * 删除本地上传的文件(同步至平台) + * @param bugId 缺陷ID + * @param platformBugKey 平台缺陷ID + * @param projectId 项目ID + * @param tmpFileDir 临时文件目录 + * @param refId 关联ID + * @param platformName 平台名称 + * @return 同步至平台的附件集合 + */ + private List deleteLocalFile(String bugId, String platformBugKey, String projectId, File tmpFileDir, + String refId, String platformName, boolean syncToPlatform) { + List syncLocalFiles = new ArrayList<>(); + BugLocalAttachment localAttachment = bugLocalAttachmentMapper.selectByPrimaryKey(refId); + // 删除本地上传的附件, BUG_LOCAL_ATTACHMENT表 + FileRequest fileRequest = buildBugFileRequest(projectId, bugId, localAttachment.getFileId(), localAttachment.getFileName()); + try { + // 删除MINIO附件 + fileService.deleteFile(fileRequest); + // 删除的本地的附件同步至平台 + if (!StringUtils.equals(platformName, BugPlatform.LOCAL.getName()) && syncToPlatform) { + File deleteTmpFile = new File(tmpFileDir, localAttachment.getFileName()); + syncLocalFiles.add(new SyncAttachmentToPlatformRequest(platformBugKey, deleteTmpFile, SyncAttachmentType.DELETE.syncOperateType())); + } + } catch (Exception e) { + throw new MSException(Translator.get("bug_attachment_delete_error")); + } + bugLocalAttachmentMapper.deleteByPrimaryKey(refId); + return syncLocalFiles; + } + + + /** * 获取本地文件类型 * @param fileName 文件名 @@ -81,4 +454,61 @@ public class BugAttachmentService { return StringUtils.EMPTY; } } + + /** + * + * @param operator 操作人 + * @param projectId 项目ID + * @return 文件操作日志记录 + */ + private FileLogRecord createFileLogRecord(String operator, String projectId){ + return FileLogRecord.builder() + .logModule(OperationLogModule.BUG_MANAGEMENT) + .operator(operator) + .projectId(projectId) + .build(); + } + + /** + * 构建缺陷文件请求 + * @param projectId 项目ID + * @param resourceId 资源ID + * @param fileId 文件ID + * @param fileName 文件名称 + * @return 文件请求对象 + */ + private FileRequest buildBugFileRequest(String projectId, String resourceId, String fileId, String fileName) { + FileRequest fileRequest = new FileRequest(); + fileRequest.setFolder(DefaultRepositoryDir.getBugDir(projectId, resourceId) + "/" + fileId); + fileRequest.setFileName(StringUtils.isEmpty(fileName) ? null : fileName); + fileRequest.setStorage(StorageType.MINIO.name()); + return fileRequest; + } + + /** + * 获取关联文件数据 + * @param linkFileIds 关联文件ID集合 + * @return 文件集合 + */ + private Map getLinkFileMetaMap(List linkFileIds) { + FileMetadataExample metadataExample = new FileMetadataExample(); + metadataExample.createCriteria().andIdIn(linkFileIds); + List fileMetadataList = fileMetadataMapper.selectByExample(metadataExample); + return fileMetadataList.stream().collect(Collectors.toMap(FileMetadata::getId, v -> v)); + } + + /** + * 获取本地文件 + * @param request 请求参数 + * @return 本地文件信息 + */ + private BugLocalAttachment getLocalFile(BugFileSourceRequest request) { + BugLocalAttachmentExample example = new BugLocalAttachmentExample(); + example.createCriteria().andFileIdEqualTo(request.getFileId()).andBugIdEqualTo(request.getBugId()); + List bugLocalAttachments = bugLocalAttachmentMapper.selectByExample(example); + if (CollectionUtils.isEmpty(bugLocalAttachments)) { + return null; + } + return bugLocalAttachments.get(0); + } } diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/service/BugNoticeService.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/service/BugNoticeService.java index 6057724001..ad33234166 100644 --- a/backend/services/bug-management/src/main/java/io/metersphere/bug/service/BugNoticeService.java +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/service/BugNoticeService.java @@ -1,16 +1,27 @@ package io.metersphere.bug.service; +import io.metersphere.bug.domain.Bug; import io.metersphere.bug.dto.request.BugEditRequest; +import io.metersphere.bug.dto.response.BugCustomFieldDTO; +import io.metersphere.bug.mapper.ExtBugCustomFieldMapper; import io.metersphere.plugin.platform.dto.SelectOption; +import io.metersphere.system.domain.User; import io.metersphere.system.dto.BugNoticeDTO; import io.metersphere.system.dto.sdk.OptionDTO; +import io.metersphere.system.mapper.UserMapper; +import io.metersphere.system.notice.NoticeModel; +import io.metersphere.system.notice.constants.NoticeConstants; +import io.metersphere.system.notice.utils.MessageTemplateUtils; +import io.metersphere.system.service.NoticeSendService; import jakarta.annotation.Resource; +import org.apache.commons.beanutils.BeanMap; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -23,10 +34,16 @@ public class BugNoticeService { public static final String CUSTOM_STATUS = "status"; public static final String CUSTOM_HANDLE_USER = "处理人"; + @Resource + private UserMapper userMapper; @Resource private BugService bugService; @Resource private BugStatusService bugStatusService; + @Resource + private NoticeSendService noticeSendService; + @Resource + private ExtBugCustomFieldMapper extBugCustomFieldMapper; public BugNoticeDTO getNoticeByRequest(BugEditRequest request) { // 获取状态选项, 处理人选项 @@ -61,6 +78,35 @@ public class BugNoticeService { return notice; } + public void sendDeleteNotice(Bug bug, String currentUser) { + Map statusMap = getStatusMap(bug.getProjectId()); + Map handlerMap = getHandleMap(bug.getProjectId()); + // 缺陷相关内容 + BugNoticeDTO notice = new BugNoticeDTO(); + notice.setTitle(bug.getTitle()); + notice.setStatus(statusMap.get(bug.getStatus())); + notice.setHandleUser(handlerMap.get(bug.getHandleUser())); + List customFields = extBugCustomFieldMapper.getBugAllCustomFields(List.of(bug.getId()), bug.getProjectId()); + List fields = customFields.stream().map(field -> { + OptionDTO fieldDTO = new OptionDTO(); + fieldDTO.setId(field.getName()); + fieldDTO.setName(field.getValue()); + return fieldDTO; + }).toList(); + notice.setCustomFields(fields); + BeanMap beanMap = new BeanMap(notice); + User user = userMapper.selectByPrimaryKey(currentUser); + Map paramMap = new HashMap<>(beanMap); + paramMap.put(NoticeConstants.RelatedUser.OPERATOR, user.getName()); + Map defaultTemplateMap = MessageTemplateUtils.getDefaultTemplateMap(); + String template = defaultTemplateMap.get(NoticeConstants.TemplateText.BUG_TASK_DELETE); + Map defaultSubjectMap = MessageTemplateUtils.getDefaultTemplateSubjectMap(); + String subject = defaultSubjectMap.get(NoticeConstants.TemplateText.BUG_TASK_DELETE); + NoticeModel noticeModel = NoticeModel.builder().operator(currentUser) + .context(template).subject(subject).paramMap(paramMap).event(NoticeConstants.Event.DELETE).build(); + noticeSendService.send(NoticeConstants.TaskType.BUG_TASK, noticeModel); + } + private Map getStatusMap(String projectId) { List statusOption = bugStatusService.getHeaderStatusOption(projectId); return statusOption.stream().collect(Collectors.toMap(SelectOption::getValue, SelectOption::getText)); diff --git a/backend/services/bug-management/src/main/java/io/metersphere/bug/service/BugService.java b/backend/services/bug-management/src/main/java/io/metersphere/bug/service/BugService.java index ea854b8069..1afd9ff760 100644 --- a/backend/services/bug-management/src/main/java/io/metersphere/bug/service/BugService.java +++ b/backend/services/bug-management/src/main/java/io/metersphere/bug/service/BugService.java @@ -4,10 +4,7 @@ import io.metersphere.bug.constants.BugExportColumns; import io.metersphere.bug.domain.*; import io.metersphere.bug.dto.BugTemplateInjectField; import io.metersphere.bug.dto.request.*; -import io.metersphere.bug.dto.response.BugCustomFieldDTO; -import io.metersphere.bug.dto.response.BugDTO; -import io.metersphere.bug.dto.response.BugRelateCaseCountDTO; -import io.metersphere.bug.dto.response.BugTagEditDTO; +import io.metersphere.bug.dto.response.*; import io.metersphere.bug.enums.BugAttachmentSourceType; import io.metersphere.bug.enums.BugPlatform; import io.metersphere.bug.enums.BugTemplateCustomField; @@ -38,8 +35,6 @@ import io.metersphere.sdk.util.*; import io.metersphere.system.domain.ServiceIntegration; import io.metersphere.system.domain.Template; import io.metersphere.system.domain.TemplateCustomField; -import io.metersphere.system.domain.User; -import io.metersphere.system.dto.BugNoticeDTO; import io.metersphere.system.dto.sdk.OptionDTO; import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO; import io.metersphere.system.dto.sdk.TemplateDTO; @@ -49,16 +44,11 @@ import io.metersphere.system.log.dto.LogDTO; import io.metersphere.system.log.service.OperationLogService; import io.metersphere.system.mapper.BaseUserMapper; import io.metersphere.system.mapper.TemplateMapper; -import io.metersphere.system.mapper.UserMapper; -import io.metersphere.system.notice.NoticeModel; -import io.metersphere.system.notice.constants.NoticeConstants; -import io.metersphere.system.notice.utils.MessageTemplateUtils; import io.metersphere.system.service.*; import io.metersphere.system.uid.IDGenerator; import io.metersphere.system.uid.NumGenerator; import jakarta.annotation.Resource; import jodd.util.StringUtil; -import org.apache.commons.beanutils.BeanMap; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.MapUtils; @@ -68,6 +58,7 @@ 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.context.annotation.Lazy; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -95,14 +86,10 @@ public class BugService { @Resource private BugMapper bugMapper; @Resource - private UserMapper userMapper; - @Resource private ExtBugMapper extBugMapper; @Resource private ProjectMapper projectMapper; @Resource - private NoticeSendService noticeSendService; - @Resource private BaseUserMapper baseUserMapper; @Resource protected TemplateMapper templateMapper; @@ -121,6 +108,9 @@ public class BugService { @Resource private BaseTemplateCustomFieldService baseTemplateCustomFieldService; @Resource + @Lazy + private BugNoticeService bugNoticeService; + @Resource private BugCustomFieldMapper bugCustomFieldMapper; @Resource private ExtBugCustomFieldMapper extBugCustomFieldMapper; @@ -160,6 +150,8 @@ public class BugService { private BugStatusService bugStatusService; @Resource private ProjectMemberService projectMemberService; + @Resource + private BugAttachmentService bugAttachmentService; /** * 缺陷列表查询 @@ -191,7 +183,6 @@ public class BugService { * 2. 第三方平台缺陷需调用插件同步缺陷至其他平台(自定义字段需处理); * 3. 保存MS缺陷(基础字段, 自定义字段) * 4. 处理附件(第三方平台缺陷需异步调用接口同步附件至第三方) - * 4. 变更历史, 操作记录; */ String platformName = projectApplicationService.getPlatformName(request.getProjectId()); PlatformBugUpdateDTO platformBug = null; @@ -224,6 +215,28 @@ public class BugService { handleAndSaveAttachments(request, files, currentUser, platformName, platformBug); } + /** + * 获取缺陷详情 + * @param id 缺陷ID + * @return 缺陷详情 + */ + public BugDetailDTO get(String id) { + BugDetailDTO bugDetail = new BugDetailDTO(); + Bug bug = checkBugExist(id); + BeanUtils.copyBean(bugDetail, bug); + // 缺陷内容 + BugContent bugContent = bugContentMapper.selectByPrimaryKey(id); + if (bugContent != null) { + bugDetail.setDescription(bugContent.getDescription()); + } + // 缺陷自定义字段 + List customFields = extBugCustomFieldMapper.getBugAllCustomFields(List.of(id), bug.getProjectId()); + bugDetail.setCustomFields(customFields); + // 缺陷附件信息 + bugDetail.setAttachments(bugAttachmentService.getAllBugFiles(id)); + return bugDetail; + } + /** * 删除缺陷 * @@ -246,7 +259,7 @@ public class BugService { bugMapper.deleteByPrimaryKey(id); } // 发送通知 - sendDeleteNotice(bug, currentUser); + bugNoticeService.sendDeleteNotice(bug, currentUser); } /** @@ -554,7 +567,6 @@ public class BugService { // 来自平台模板 templateDTO.setPlatformDefault(false); String platformName = projectApplicationService.getPlatformName(projectId); - // TODO: 严重程度 // 状态字段 attachTemplateStatusField(templateDTO, projectId, fromStatusId, platformBugKey); @@ -690,10 +702,15 @@ public class BugService { bug.setUpdateUser(currentUser); bug.setUpdateTime(System.currentTimeMillis()); bugMapper.updateByPrimaryKeySelective(bug); + BugContent originalContent = bugContentMapper.selectByPrimaryKey(bug.getId()); BugContent bugContent = new BugContent(); bugContent.setBugId(bug.getId()); bugContent.setDescription(request.getDescription()); - bugContentMapper.updateByPrimaryKeySelective(bugContent); + if (originalContent == null) { + bugContentMapper.insert(bugContent); + } else { + bugContentMapper.updateByPrimaryKeySelective(bugContent); + } } } @@ -792,7 +809,7 @@ public class BugService { List allSyncAttachments = Stream.concat(removeAttachments.stream(), uploadAttachments.stream()).toList(); // 同步至第三方(异步调用) - if (!StringUtils.equals(platformName, BugPlatform.LOCAL.getName())) { + if (!StringUtils.equals(platformName, BugPlatform.LOCAL.getName()) && CollectionUtils.isNotEmpty(allSyncAttachments)) { bugSyncExtraService.syncAttachmentToPlatform(allSyncAttachments, request.getProjectId(), tempFileDir); } } @@ -815,7 +832,7 @@ public class BugService { Map localAttachmentMap = bugLocalAttachments.stream().collect(Collectors.toMap(BugLocalAttachment::getFileId, v -> v)); // 删除本地上传的附件, BUG_LOCAL_ATTACHMENT表 request.getDeleteLocalFileIds().forEach(deleteFileId -> { - FileRequest fileRequest = buildBugFileRequest(request.getProjectId(), request.getId(), localAttachmentMap.get(deleteFileId).getFileName()); + FileRequest fileRequest = buildBugFileRequest(request.getProjectId(), request.getId(), deleteFileId, localAttachmentMap.get(deleteFileId).getFileName()); try { fileService.deleteFile(fileRequest); // 删除的本地的附件同步至平台 @@ -884,12 +901,12 @@ public class BugService { }); extBugLocalAttachmentMapper.batchInsert(addFiles); uploadMinioFiles.forEach((fileId, file) -> { - FileRequest fileRequest = buildBugFileRequest(request.getProjectId(), request.getId(), file.getOriginalFilename()); + FileRequest fileRequest = buildBugFileRequest(request.getProjectId(), request.getId(), fileId, file.getOriginalFilename()); try { fileService.upload(file, fileRequest); // 同步新上传的附件至平台 if (!StringUtils.equals(platformName, BugPlatform.LOCAL.getName())) { - File uploadTmpFile = new File(tempFileDir, Objects.requireNonNull(file.getOriginalFilename())); + File uploadTmpFile = new File(tempFileDir, Objects.requireNonNull(file.getOriginalFilename())).toPath().normalize().toFile();; FileUtils.writeByteArrayToFile(uploadTmpFile, file.getBytes()); uploadPlatformAttachments.add(new SyncAttachmentToPlatformRequest(platformBug.getPlatformBugKey(), uploadTmpFile, SyncAttachmentType.UPLOAD.syncOperateType())); } @@ -1093,7 +1110,7 @@ public class BugService { attachmentExample.createCriteria().andBugIdEqualTo(bugId); List bugLocalAttachments = bugLocalAttachmentMapper.selectByExample(attachmentExample); bugLocalAttachments.forEach(bugLocalAttachment -> { - FileRequest fileRequest = buildBugFileRequest(projectId, bugId, bugLocalAttachment.getFileName()); + FileRequest fileRequest = buildBugFileRequest(projectId, bugId, bugLocalAttachment.getFileId(), bugLocalAttachment.getFileName()); try { fileService.deleteFile(fileRequest); } catch (Exception e) { @@ -1133,12 +1150,13 @@ public class BugService { * 构建缺陷文件请求 * @param projectId 项目ID * @param resourceId 资源ID + * @param fileId 文件ID * @param fileName 文件名称 * @return 文件请求对象 */ - private FileRequest buildBugFileRequest(String projectId, String resourceId, String fileName) { + private FileRequest buildBugFileRequest(String projectId, String resourceId, String fileId, String fileName) { FileRequest fileRequest = new FileRequest(); - fileRequest.setFolder(DefaultRepositoryDir.getBugDir(projectId, resourceId)); + fileRequest.setFolder(DefaultRepositoryDir.getBugDir(projectId, resourceId) + "/" + fileId); fileRequest.setFileName(StringUtils.isEmpty(fileName) ? null : fileName); fileRequest.setStorage(StorageType.MINIO.name()); return fileRequest; @@ -1351,35 +1369,4 @@ public class BugService { }); return logs; } - - private void sendDeleteNotice(Bug bug, String currentUser) { - List statusOption = bugStatusService.getHeaderStatusOption(bug.getProjectId()); - Map statusMap = statusOption.stream().collect(Collectors.toMap(SelectOption::getValue, SelectOption::getText)); - List handlerOption = getHeaderHandlerOption(bug.getProjectId()); - Map handlerMap = handlerOption.stream().collect(Collectors.toMap(SelectOption::getValue, SelectOption::getText)); - // 缺陷相关内容 - BugNoticeDTO notice = new BugNoticeDTO(); - notice.setTitle(bug.getTitle()); - notice.setStatus(statusMap.get(bug.getStatus())); - notice.setHandleUser(handlerMap.get(bug.getHandleUser())); - List customFields = extBugCustomFieldMapper.getBugAllCustomFields(List.of(bug.getId()), bug.getProjectId()); - List fields = customFields.stream().map(field -> { - OptionDTO fieldDTO = new OptionDTO(); - fieldDTO.setId(field.getName()); - fieldDTO.setName(field.getValue()); - return fieldDTO; - }).toList(); - notice.setCustomFields(fields); - BeanMap beanMap = new BeanMap(notice); - User user = userMapper.selectByPrimaryKey(currentUser); - Map paramMap = new HashMap<>(beanMap); - paramMap.put(NoticeConstants.RelatedUser.OPERATOR, user.getName()); - Map defaultTemplateMap = MessageTemplateUtils.getDefaultTemplateMap(); - String template = defaultTemplateMap.get(NoticeConstants.TemplateText.BUG_TASK_DELETE); - Map defaultSubjectMap = MessageTemplateUtils.getDefaultTemplateSubjectMap(); - String subject = defaultSubjectMap.get(NoticeConstants.TemplateText.BUG_TASK_DELETE); - NoticeModel noticeModel = NoticeModel.builder().operator(currentUser) - .context(template).subject(subject).paramMap(paramMap).event(NoticeConstants.Event.DELETE).build(); - noticeSendService.send(NoticeConstants.TaskType.BUG_TASK, noticeModel); - } } \ No newline at end of file diff --git a/backend/services/bug-management/src/test/java/io/metersphere/bug/controller/BugAttachmentControllerTests.java b/backend/services/bug-management/src/test/java/io/metersphere/bug/controller/BugAttachmentControllerTests.java new file mode 100644 index 0000000000..ce58c264c1 --- /dev/null +++ b/backend/services/bug-management/src/test/java/io/metersphere/bug/controller/BugAttachmentControllerTests.java @@ -0,0 +1,249 @@ +package io.metersphere.bug.controller; + +import io.metersphere.bug.dto.request.BugDeleteFileRequest; +import io.metersphere.bug.dto.request.BugFileSourceRequest; +import io.metersphere.bug.dto.request.BugFileTransferRequest; +import io.metersphere.bug.dto.request.BugUploadFileRequest; +import io.metersphere.bug.dto.response.BugFileDTO; +import io.metersphere.project.dto.filemanagement.request.FileMetadataTableRequest; +import io.metersphere.project.dto.filemanagement.request.FileUploadRequest; +import io.metersphere.project.dto.filemanagement.response.FileInformationResponse; +import io.metersphere.project.service.FileMetadataService; +import io.metersphere.sdk.util.JSON; +import io.metersphere.system.base.BaseTest; +import io.metersphere.system.controller.handler.ResultHolder; +import io.metersphere.system.utils.Pager; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.*; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +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.MultiValueMap; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class BugAttachmentControllerTests extends BaseTest { + + @Resource + private FileMetadataService fileMetadataService; + public static final String BUG_ATTACHMENT_LIST = "/bug/attachment/list"; + public static final String BUG_ATTACHMENT_RELATED_PAGE = "/bug/attachment/file/page"; + public static final String BUG_ATTACHMENT_UPLOAD = "/bug/attachment/upload"; + public static final String BUG_ATTACHMENT_DELETE = "/bug/attachment/delete"; + public static final String BUG_ATTACHMENT_PREVIEW = "/bug/attachment/preview"; + public static final String BUG_ATTACHMENT_DOWNLOAD = "/bug/attachment/download"; + public static final String BUG_ATTACHMENT_TRANSFER_OPTION = "/bug/attachment/transfer/options"; + public static final String BUG_ATTACHMENT_TRANSFER = "/bug/attachment/transfer"; + public static final String BUG_ATTACHMENT_CHECK_UPDATE = "/bug/attachment/check-update"; + public static final String BUG_ATTACHMENT_UPDATE = "/bug/attachment/update"; + public static final String BUG_ATTACHMENT_UPLOAD_MD = "/bug/attachment/upload/md/file"; + + @Test + @Order(0) + void testUploadMdFile() throws Exception { + MockMultipartFile fileTooLarge = new MockMultipartFile("file", "test.txt", MediaType.APPLICATION_OCTET_STREAM_VALUE, new byte[50 * 1024 * 1024 + 1]); + this.requestUploadFile(BUG_ATTACHMENT_UPLOAD_MD, fileTooLarge).andExpect(status().is5xxServerError()); + MockMultipartFile fileWithNoName = new MockMultipartFile("file", "", MediaType.APPLICATION_OCTET_STREAM_VALUE, "aa".getBytes()); + this.requestUploadFile(BUG_ATTACHMENT_UPLOAD_MD, fileWithNoName).andExpect(status().is5xxServerError()); + // Mock minio save file exception + MockMultipartFile file = new MockMultipartFile("file", "test.txt", MediaType.APPLICATION_OCTET_STREAM_VALUE, "aa".getBytes()); + this.requestUploadFile(BUG_ATTACHMENT_UPLOAD_MD, file); + } + + @Test + @Order(1) + @Sql(scripts = {"/dml/init_bug_attachment.sql"}, config = @SqlConfig(encoding = "utf-8", transactionMode = SqlConfig.TransactionMode.ISOLATED)) + void prepareData() throws Exception { + // 准备文件库数据 + FileUploadRequest fileUploadRequest = new FileUploadRequest(); + fileUploadRequest.setProjectId("default-project-for-attachment"); + MockMultipartFile file1 = new MockMultipartFile("file", "TEST1.JPG", MediaType.APPLICATION_OCTET_STREAM_VALUE, "aa".getBytes()); + fileMetadataService.upload(fileUploadRequest, "admin", file1); + MockMultipartFile file2 = new MockMultipartFile("file", "TEST2.JPG", MediaType.APPLICATION_OCTET_STREAM_VALUE, "bb".getBytes()); + fileMetadataService.upload(fileUploadRequest, "admin", file2); + } + + @Test + @Order(2) + void testFilePage() throws Exception { + List unRelatedFiles = getRelatedFiles(); + Assertions.assertEquals(2, unRelatedFiles.size()); + } + + @Test + @Order(3) + void testUpload() throws Exception { + List unRelatedFiles = getRelatedFiles(); + // 非全选关联 + BugUploadFileRequest request = new BugUploadFileRequest(); + request.setBugId("default-attachment-bug-id"); + request.setProjectId("default-project-for-attachment"); + request.setSelectAll(false); + request.setSelectIds(List.of(unRelatedFiles.get(0).getId())); + MultiValueMap paramMap1 = getDefaultMultiPartParam(request, null); + this.requestMultipartWithOk(BUG_ATTACHMENT_UPLOAD, paramMap1); + // 全选关联 + request.setSelectAll(true); + request.setExcludeIds(List.of(unRelatedFiles.get(0).getId(), unRelatedFiles.get(1).getId())); + MultiValueMap paramMap2 = getDefaultMultiPartParam(request, null); + this.requestMultipartWithOk(BUG_ATTACHMENT_UPLOAD, paramMap2); + request.setSelectAll(true); + request.setExcludeIds(null); + MultiValueMap paramMap3 = getDefaultMultiPartParam(request, null); + this.requestMultipartWithOk(BUG_ATTACHMENT_UPLOAD, paramMap3); + String filePath = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/test.xlsx")).getPath(); + File file = new File(filePath); + MultiValueMap paramMapWithFile = getDefaultMultiPartParam(request, file); + this.requestMultipartWithOk(BUG_ATTACHMENT_UPLOAD, paramMapWithFile); + // 第三方平台的缺陷关联文件 + request.setBugId("default-bug-id-tapd"); + request.setSelectAll(false); + request.setSelectIds(List.of("not-exist-file-id")); + MultiValueMap paramMap4 = getDefaultMultiPartParam(request, null); + this.requestMultipartWithOk(BUG_ATTACHMENT_UPLOAD, paramMap4); + request.setSelectIds(List.of(unRelatedFiles.get(0).getId())); + MultiValueMap paramMap5 = getDefaultMultiPartParam(request, null); + this.requestMultipartWithOk(BUG_ATTACHMENT_UPLOAD, paramMap5); + MultiValueMap paramMap6 = getDefaultMultiPartParam(request, file); + this.requestMultipartWithOk(BUG_ATTACHMENT_UPLOAD, paramMap6); + } + + @Test + @Order(4) + void previewOrDownload() throws Exception { + BugFileSourceRequest request = new BugFileSourceRequest(); + request.setBugId("default-attachment-bug-id"); + request.setProjectId("default-project-for-attachment"); + request.setAssociated(false); + request.setFileId("not-exist-file-id"); + this.requestPostDownloadFile(BUG_ATTACHMENT_PREVIEW, null, request); + List files = getBugFiles("default-attachment-bug-id"); + files.forEach(file -> { + request.setFileId(file.getFileId()); + request.setAssociated(file.getAssociated()); + try { + this.requestPostDownloadFile(BUG_ATTACHMENT_PREVIEW, null, request); + this.requestPostDownloadFile(BUG_ATTACHMENT_DOWNLOAD, null, request); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + @Test + @Order(5) + void testTransfer() throws Exception { + this.requestGetWithOk(BUG_ATTACHMENT_TRANSFER_OPTION + "/default-project-for-attachment"); + BugFileTransferRequest request = new BugFileTransferRequest(); + request.setBugId("default-attachment-bug-id"); + request.setProjectId("default-project-for-attachment"); + request.setModuleId("root"); + request.setAssociated(false); + request.setFileId("not-exist-file-id"); + this.requestPost(BUG_ATTACHMENT_TRANSFER, request).andExpect(status().is5xxServerError()); + List files = getBugFiles("default-attachment-bug-id"); + files.stream().filter(file -> !file.getAssociated()).forEach(file -> { + request.setFileId(file.getFileId()); + request.setAssociated(file.getAssociated()); + try { + this.requestPostWithOk(BUG_ATTACHMENT_TRANSFER, request); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + @Test + @Order(6) + void testUpgrade() throws Exception { + // 检查更新 + this.requestPostWithOk(BUG_ATTACHMENT_CHECK_UPDATE, List.of("test-id")); + List bugFiles = getBugFiles("default-attachment-bug-id"); + BugDeleteFileRequest request = new BugDeleteFileRequest(); + request.setBugId("default-attachment-bug-id"); + request.setProjectId("default-project-for-attachment"); + request.setAssociated(true); + bugFiles.forEach(file -> { + try { + request.setRefId(file.getRefId()); + this.requestPostWithOk(BUG_ATTACHMENT_UPDATE, request); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + List tapdFiles = getBugFiles("default-bug-id-tapd"); + tapdFiles.stream().filter(BugFileDTO::getAssociated).forEach(file -> { + try { + request.setBugId("default-bug-id-tapd"); + request.setRefId(file.getRefId()); + this.requestPostWithOk(BUG_ATTACHMENT_UPDATE, request); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + @Test + @Order(7) + void testDelete() throws Exception { + // Local缺陷附件删除 + List files = getBugFiles("default-attachment-bug-id"); + files.forEach(file -> { + BugDeleteFileRequest request = new BugDeleteFileRequest(); + request.setBugId("default-attachment-bug-id"); + request.setProjectId("default-project-for-attachment"); + request.setRefId(file.getRefId()); + request.setAssociated(file.getAssociated()); + try { + this.requestPostWithOk(BUG_ATTACHMENT_DELETE, request); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + List tapdBugFiles = getBugFiles("default-bug-id-tapd"); + tapdBugFiles.forEach(file -> { + BugDeleteFileRequest request = new BugDeleteFileRequest(); + request.setBugId("default-bug-id-tapd"); + request.setProjectId("default-project-for-attachment"); + request.setRefId(file.getRefId()); + request.setAssociated(file.getAssociated()); + try { + this.requestPostWithOk(BUG_ATTACHMENT_DELETE, request); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + private List getRelatedFiles() throws Exception { + FileMetadataTableRequest request = new FileMetadataTableRequest(); + request.setProjectId("default-project-for-attachment"); + request.setCurrent(1); + request.setPageSize(10); + MvcResult mvcResult = this.requestPostWithOkAndReturn(BUG_ATTACHMENT_RELATED_PAGE, request); + String returnData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + ResultHolder resultHolder = JSON.parseObject(returnData, ResultHolder.class); + Pager pageData = JSON.parseObject(JSON.toJSONString(resultHolder.getData()), Pager.class); + // 返回的数据量不超过规定要返回的数据量相同 + return JSON.parseArray(JSON.toJSONString(pageData.getList()), FileInformationResponse.class); + } + + private List getBugFiles(String bugId) throws Exception { + MvcResult mvcResult = this.requestGetWithOkAndReturn(BUG_ATTACHMENT_LIST + "/" + bugId); + String returnData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + ResultHolder resultHolder = JSON.parseObject(returnData, ResultHolder.class); + return JSON.parseArray(JSON.toJSONString(resultHolder.getData()), BugFileDTO.class); + } +} diff --git a/backend/services/bug-management/src/test/java/io/metersphere/bug/controller/BugCommentTests.java b/backend/services/bug-management/src/test/java/io/metersphere/bug/controller/BugCommentControllerTests.java similarity index 99% rename from backend/services/bug-management/src/test/java/io/metersphere/bug/controller/BugCommentTests.java rename to backend/services/bug-management/src/test/java/io/metersphere/bug/controller/BugCommentControllerTests.java index d59de95789..d58ba234e4 100644 --- a/backend/services/bug-management/src/test/java/io/metersphere/bug/controller/BugCommentTests.java +++ b/backend/services/bug-management/src/test/java/io/metersphere/bug/controller/BugCommentControllerTests.java @@ -26,7 +26,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class BugCommentTests extends BaseTest { +public class BugCommentControllerTests extends BaseTest { @Resource private BugCommentMapper bugCommentMapper; diff --git a/backend/services/bug-management/src/test/java/io/metersphere/bug/controller/BugControllerTests.java b/backend/services/bug-management/src/test/java/io/metersphere/bug/controller/BugControllerTests.java index 191e249f3a..91c21acbc2 100644 --- a/backend/services/bug-management/src/test/java/io/metersphere/bug/controller/BugControllerTests.java +++ b/backend/services/bug-management/src/test/java/io/metersphere/bug/controller/BugControllerTests.java @@ -76,6 +76,8 @@ public class BugControllerTests extends BaseTest { public static final String BUG_PAGE = "/bug/page"; public static final String BUG_ADD = "/bug/add"; public static final String BUG_UPDATE = "/bug/update"; + public static final String BUG_DETAIL = "/bug/get"; + public static final String BUG_QUICK_UPDATE = "/bug/quick-update"; public static final String BUG_DELETE = "/bug/delete"; public static final String BUG_TEMPLATE_OPTION = "/bug/template/option"; public static final String BUG_TEMPLATE_DETAIL = "/bug/template/detail"; @@ -227,6 +229,7 @@ public class BugControllerTests extends BaseTest { File file = new File(filePath); MultiValueMap paramMap = getDefaultMultiPartParam(request, file); this.requestMultipartWithOkAndReturn(BUG_ADD, paramMap); + } @Test @@ -271,8 +274,17 @@ public class BugControllerTests extends BaseTest { request.setLinkFileIds(null); request.setUnLinkRefIds(null); request.setDeleteLocalFileIds(null); + request.setDescription("1111"); noFileParamMap.add("request", JSON.toJSONString(request)); this.requestMultipartWithOkAndReturn(BUG_UPDATE, noFileParamMap); + // 获取缺陷详情 + this.requestGetWithOk(BUG_DETAIL + "/default-bug-id"); + this.requestGetWithOk(BUG_DETAIL + "/" + request.getId()); + // 更新部分 + BugQuickEditRequest quickEditRequest = new BugQuickEditRequest(); + quickEditRequest.setId(request.getId()); + quickEditRequest.setTags(List.of("TEST")); + this.requestPost(BUG_QUICK_UPDATE, quickEditRequest, status().isOk()); } @Test diff --git a/backend/services/bug-management/src/test/java/io/metersphere/bug/service/BugSyncExtraServiceTests.java b/backend/services/bug-management/src/test/java/io/metersphere/bug/service/BugSyncExtraServiceTests.java index 1378c0b124..f6a4c537ea 100644 --- a/backend/services/bug-management/src/test/java/io/metersphere/bug/service/BugSyncExtraServiceTests.java +++ b/backend/services/bug-management/src/test/java/io/metersphere/bug/service/BugSyncExtraServiceTests.java @@ -14,6 +14,8 @@ import org.mockito.Mockito; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.SqlConfig; @@ -44,6 +46,11 @@ public class BugSyncExtraServiceTests extends BaseTest { @Sql(scripts = {"/dml/init_bug_sync_extra.sql"}, config = @SqlConfig(encoding = "utf-8", transactionMode = SqlConfig.TransactionMode.ISOLATED)) void test() throws Exception { List allBugFile = bugAttachmentService.getAllBugFiles("bug-for-sync-extra"); + // Mock minio upload exception + MockMultipartFile file = new MockMultipartFile("file", "test.txt", MediaType.APPLICATION_OCTET_STREAM_VALUE, "aa".getBytes()); + Mockito.doThrow(new MSException("save minio error!")).when(minioMock).saveFile(Mockito.eq(file), Mockito.any()); + MSException uploadException = assertThrows(MSException.class, () -> bugAttachmentService.uploadMdFile(file)); + assertEquals(uploadException.getMessage(), "save minio error!"); // Mock minio delete exception Mockito.doThrow(new MSException("delete minio error!")).when(minioMock).delete(Mockito.any()); MSException deleteException = assertThrows(MSException.class, () -> diff --git a/backend/services/bug-management/src/test/resources/dml/init_bug_attachment.sql b/backend/services/bug-management/src/test/resources/dml/init_bug_attachment.sql new file mode 100644 index 0000000000..89e4ea090f --- /dev/null +++ b/backend/services/bug-management/src/test/resources/dml/init_bug_attachment.sql @@ -0,0 +1,9 @@ +INSERT INTO project (id, num, organization_id, name, description, create_user, update_user, create_time, update_time) + VALUE ('default-project-for-bug-tmp-1', null, '100001', '测试项目(缺陷)', '系统默认创建的项目(缺陷)', 'admin', 'admin', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000); + +INSERT INTO project (id, num, organization_id, name, description, create_user, update_user, create_time, update_time) + VALUE ('default-project-for-attachment', null, '100001', '测试项目(缺陷)', '系统默认创建的项目(缺陷)', 'admin', 'admin', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000); + +INSERT INTO bug (id, num, title, handle_users, handle_user, create_user, create_time, update_user, update_time, delete_user, delete_time, project_id, template_id, platform, status, tags, platform_bug_id, deleted) +VALUES ('default-attachment-bug-id', 100000, 'default-bug', 'oasis', 'oasis', 'admin', UNIX_TIMESTAMP() * 1000, 'admin', UNIX_TIMESTAMP() * 1000, 'admin', UNIX_TIMESTAMP() * 1000, 'default-project-for-attachment', 'bug-template-id', 'Local', 'open', '["default-tag"]', null, 1), + ('default-bug-id-tapd', 100000, 'default-bug', 'oasis', 'oasis', 'admin', UNIX_TIMESTAMP() * 1000, 'admin', UNIX_TIMESTAMP() * 1000, 'admin', UNIX_TIMESTAMP() * 1000, 'default-project-for-attachment', 'default-bug-template-id', 'Tapd', 'open', '["default-tag"]', null, 0); \ No newline at end of file diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/service/FileMetadataService.java b/backend/services/project-management/src/main/java/io/metersphere/project/service/FileMetadataService.java index 9168a29b89..58bb016af5 100644 --- a/backend/services/project-management/src/main/java/io/metersphere/project/service/FileMetadataService.java +++ b/backend/services/project-management/src/main/java/io/metersphere/project/service/FileMetadataService.java @@ -328,7 +328,7 @@ public class FileMetadataService { byte[] bytes = this.getFileByte(fileMetadata); return ResponseEntity.ok() .contentType(MediaType.parseMediaType("application/octet-stream")) - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + this.getFileName(fileMetadata.getId(), fileMetadata.getType()) + "\"") + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + this.getFileName(fileMetadata.getName(), fileMetadata.getType()) + "\"") .body(bytes); }