feat(缺陷管理): 补充缺陷管理附件相关接口

This commit is contained in:
song-cc-rock 2024-01-22 10:34:32 +08:00 committed by Craftsman
parent 4f6cbed856
commit 7e5967a688
22 changed files with 1144 additions and 69 deletions

View File

@ -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<BugFileDTO> 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<List<FileInformationResponse>> 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<byte[]> 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<byte[]> 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<BaseTreeNode> 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<String> checkUpdate(@RequestBody List<String> 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<byte[]> previewMdImg(@Validated @RequestBody BugFileSourceRequest request) {
return bugAttachmentService.downloadOrPreview(request);
}
}

View File

@ -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)

View File

@ -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;
}

View File

@ -46,7 +46,7 @@ public class BugEditRequest {
@Schema(description = "自定义字段集合")
private List<BugCustomFieldDTO> customFields;
@Schema(description = "删除的本地附件集合, 文件ID")
@Schema(description = "删除的本地附件集合, {文件ID")
private List<String> deleteLocalFileIds;
@Schema(description = "取消关联附件关系ID集合, 关联关系ID")

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<String> tags;
@Schema(description = "缺陷内容")
private String description;
@Schema(description = "自定义字段集合")
private List<BugCustomFieldDTO> customFields;
}

View File

@ -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<String> 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<String> moduleIds;
@Schema(description = "文件类型")
private String fileType;
@Schema(description = "关键字")
private String keyword;
}

View File

@ -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 = "缺陷内容")

View File

@ -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<BugFileDTO> attachments;
}

View File

@ -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")

View File

@ -7,7 +7,7 @@ public enum BugAttachmentSourceType {
*/
ATTACHMENT,
/**
* MD图片
* 缺陷内容
*/
MD_PIC;
CONTENT;
}

View File

@ -15,7 +15,7 @@
<select id="list" resultMap="BugDTO">
select b.id, b.num, b.title, b.handle_user, b.create_user, b.create_time, b.update_time, b.delete_time, b.delete_user,
b.project_id, b.template_id, b.platform, b.status, b.tags, bc.description from bug b left join bug_content bc on b.id = bc.bug_id
b.project_id, b.template_id, b.platform, b.status, b.tags from bug b
<include refid="queryWhereCondition"/>
</select>

View File

@ -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<BugFileDTO> getAllBugFiles(String bugId) {
List<BugFileDTO> bugFiles = new ArrayList<>();
BugLocalAttachmentExample localAttachmentExample = new BugLocalAttachmentExample();
localAttachmentExample.createCriteria().andBugIdEqualTo(bugId);
localAttachmentExample.createCriteria().andBugIdEqualTo(bugId).andSourceEqualTo(BugAttachmentSourceType.ATTACHMENT.name());
List<BugLocalAttachment> bugLocalAttachments = bugLocalAttachmentMapper.selectByExample(localAttachmentExample);
if (!CollectionUtils.isEmpty(bugLocalAttachments)) {
bugLocalAttachments.forEach(localFile -> {
@ -53,10 +106,7 @@ public class BugAttachmentService {
List<FileAssociation> fileAssociations = fileAssociationMapper.selectByExample(associationExample);
if (!CollectionUtils.isEmpty(fileAssociations)) {
List<String> associateFileIds = fileAssociations.stream().map(FileAssociation::getFileId).toList();
FileMetadataExample metadataExample = new FileMetadataExample();
metadataExample.createCriteria().andIdIn(associateFileIds);
List<FileMetadata> fileMetadataList = fileMetadataMapper.selectByExample(metadataExample);
Map<String, FileMetadata> fileMetadataMap = fileMetadataList.stream().collect(Collectors.toMap(FileMetadata::getId, v -> v));
Map<String, FileMetadata> 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<SyncAttachmentToPlatformRequest> platformAttachments = new ArrayList<>();
if (file == null) {
// 关联文件
List<String> relateFileIds;
if (request.isSelectAll()) {
// 全选
FileMetadataTableRequest metadataTableRequest = new FileMetadataTableRequest();
BeanUtils.copyBean(metadataTableRequest, request);
List<FileInformationResponse> 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<SyncAttachmentToPlatformRequest> syncLinkFiles = uploadLinkFile(bug.getId(), bug.getPlatformBugId(), request.getProjectId(), tempFileDir, relateFileIds, currentUser, bug.getPlatform(), false);
platformAttachments.addAll(syncLinkFiles);
} else {
// 上传文件
List<SyncAttachmentToPlatformRequest> 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<SyncAttachmentToPlatformRequest> platformAttachments = new ArrayList<>();
if (request.getAssociated()) {
// 取消关联
List<SyncAttachmentToPlatformRequest> syncLinkFiles = unLinkFile(bug.getPlatformBugId(), request.getProjectId(),
tempFileDir, request.getRefId(), bug.getCreateUser(), bug.getPlatform(), false);
platformAttachments.addAll(syncLinkFiles);
} else {
// 删除本地上传的文件
List<SyncAttachmentToPlatformRequest> 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<byte[]> 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<SyncAttachmentToPlatformRequest> syncUnlinkFiles = unLinkFile(bug.getPlatformBugId(), request.getProjectId(),
tempFileDir, request.getRefId(), currentUser, bug.getPlatform(), true);
// 更新后的文件需要同步
String upgradeFileId = fileAssociationService.upgrade(request.getRefId(), createFileLogRecord(currentUser, request.getProjectId()));
// 关联附件->同步
List<SyncAttachmentToPlatformRequest> syncLinkFiles = uploadLinkFile(bug.getId(), bug.getPlatformBugId(), request.getProjectId(), tempFileDir, List.of(upgradeFileId), currentUser, bug.getPlatform(), true);
List<SyncAttachmentToPlatformRequest> 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<SyncAttachmentToPlatformRequest> uploadLinkFile(String bugId, String platformBugKey, String projectId, File tmpFileDir,
List<String> linkFileIds, String currentUser, String platformName, boolean syncOnly) {
if (!syncOnly) {
fileAssociationService.association(bugId, FileAssociationSourceUtil.SOURCE_TYPE_BUG, linkFileIds, createFileLogRecord(currentUser, projectId));
}
// 同步新关联的附件至平台
List<SyncAttachmentToPlatformRequest> linkSyncFiles = new ArrayList<>();
if (!StringUtils.equals(platformName, BugPlatform.LOCAL.getName())) {
Map<String, FileMetadata> 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<SyncAttachmentToPlatformRequest> 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<SyncAttachmentToPlatformRequest> 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<SyncAttachmentToPlatformRequest> unLinkFile(String platformBugKey, String projectId, File tmpFileDir,
String refId, String currentUser, String platformName, boolean syncOnly) {
List<SyncAttachmentToPlatformRequest> 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<SyncAttachmentToPlatformRequest> deleteLocalFile(String bugId, String platformBugKey, String projectId, File tmpFileDir,
String refId, String platformName, boolean syncToPlatform) {
List<SyncAttachmentToPlatformRequest> 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<String, FileMetadata> getLinkFileMetaMap(List<String> linkFileIds) {
FileMetadataExample metadataExample = new FileMetadataExample();
metadataExample.createCriteria().andIdIn(linkFileIds);
List<FileMetadata> 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<BugLocalAttachment> bugLocalAttachments = bugLocalAttachmentMapper.selectByExample(example);
if (CollectionUtils.isEmpty(bugLocalAttachments)) {
return null;
}
return bugLocalAttachments.get(0);
}
}

View File

@ -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<String, String> statusMap = getStatusMap(bug.getProjectId());
Map<String, String> 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<BugCustomFieldDTO> customFields = extBugCustomFieldMapper.getBugAllCustomFields(List.of(bug.getId()), bug.getProjectId());
List<OptionDTO> 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<String, String> defaultTemplateMap = MessageTemplateUtils.getDefaultTemplateMap();
String template = defaultTemplateMap.get(NoticeConstants.TemplateText.BUG_TASK_DELETE);
Map<String, String> 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<String, String> getStatusMap(String projectId) {
List<SelectOption> statusOption = bugStatusService.getHeaderStatusOption(projectId);
return statusOption.stream().collect(Collectors.toMap(SelectOption::getValue, SelectOption::getText));

View File

@ -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<BugCustomFieldDTO> 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<SyncAttachmentToPlatformRequest> 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<String, BugLocalAttachment> 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<BugLocalAttachment> 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<SelectOption> statusOption = bugStatusService.getHeaderStatusOption(bug.getProjectId());
Map<String, String> statusMap = statusOption.stream().collect(Collectors.toMap(SelectOption::getValue, SelectOption::getText));
List<SelectOption> handlerOption = getHeaderHandlerOption(bug.getProjectId());
Map<String, String> 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<BugCustomFieldDTO> customFields = extBugCustomFieldMapper.getBugAllCustomFields(List.of(bug.getId()), bug.getProjectId());
List<OptionDTO> 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<String, String> defaultTemplateMap = MessageTemplateUtils.getDefaultTemplateMap();
String template = defaultTemplateMap.get(NoticeConstants.TemplateText.BUG_TASK_DELETE);
Map<String, String> 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);
}
}

View File

@ -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<FileInformationResponse> unRelatedFiles = getRelatedFiles();
Assertions.assertEquals(2, unRelatedFiles.size());
}
@Test
@Order(3)
void testUpload() throws Exception {
List<FileInformationResponse> 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<String, Object> 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<String, Object> paramMap2 = getDefaultMultiPartParam(request, null);
this.requestMultipartWithOk(BUG_ATTACHMENT_UPLOAD, paramMap2);
request.setSelectAll(true);
request.setExcludeIds(null);
MultiValueMap<String, Object> 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<String, Object> 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<String, Object> paramMap4 = getDefaultMultiPartParam(request, null);
this.requestMultipartWithOk(BUG_ATTACHMENT_UPLOAD, paramMap4);
request.setSelectIds(List.of(unRelatedFiles.get(0).getId()));
MultiValueMap<String, Object> paramMap5 = getDefaultMultiPartParam(request, null);
this.requestMultipartWithOk(BUG_ATTACHMENT_UPLOAD, paramMap5);
MultiValueMap<String, Object> 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<BugFileDTO> 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<BugFileDTO> 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<BugFileDTO> 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<BugFileDTO> 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<BugFileDTO> 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<BugFileDTO> 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<FileInformationResponse> 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<BugFileDTO> 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);
}
}

View File

@ -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;

View File

@ -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<String, Object> 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

View File

@ -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<BugFileDTO> 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, () ->

View File

@ -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);

View File

@ -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);
}