refactor(缺陷管理): 缺陷导出功能开发

This commit is contained in:
song-tianyang 2023-11-30 13:54:27 +08:00 committed by 刘瑞斌
parent 42b1bcc60d
commit 589415ac05
24 changed files with 672 additions and 84 deletions

View File

@ -129,6 +129,8 @@ INSERT INTO user_role_permission (id, role_id, permission_id) VALUES (UUID_SHORT
INSERT INTO user_role_permission (id, role_id, permission_id) VALUES (UUID_SHORT(), 'project_admin', 'BUG:READ+ADD');
INSERT INTO user_role_permission (id, role_id, permission_id) VALUES (UUID_SHORT(), 'project_admin', 'BUG:READ+UPDATE');
INSERT INTO user_role_permission (id, role_id, permission_id) VALUES (UUID_SHORT(), 'project_admin', 'BUG:READ+DELETE');
INSERT INTO user_role_permission (id, role_id, permission_id)
VALUES (UUID_SHORT(), 'project_admin', 'PROJECT_BUG:READ+EXPORT');
INSERT INTO user_role_permission (id, role_id, permission_id) VALUES (UUID_SHORT(), 'project_admin', 'PROJECT_BASE_INFO:READ+UPDATE');
INSERT INTO user_role_permission (id, role_id, permission_id) VALUES (UUID_SHORT(), 'project_admin', 'PROJECT_API_DEBUG:READ');
INSERT INTO user_role_permission (id, role_id, permission_id) VALUES (UUID_SHORT(), 'project_admin', 'PROJECT_API_DEBUG:READ+ADD');
@ -200,6 +202,8 @@ INSERT INTO user_role_permission (id, role_id, permission_id) VALUES (UUID_SHORT
INSERT INTO user_role_permission (id, role_id, permission_id) VALUES (UUID_SHORT(), 'project_member', 'BUG:READ+ADD');
INSERT INTO user_role_permission (id, role_id, permission_id) VALUES (UUID_SHORT(), 'project_member', 'BUG:READ+UPDATE');
INSERT INTO user_role_permission (id, role_id, permission_id) VALUES (UUID_SHORT(), 'project_member', 'BUG:READ+DELETE');
INSERT INTO user_role_permission (id, role_id, permission_id)
VALUES (UUID_SHORT(), 'project_member', 'PROJECT_BUG:READ+EXPORT');
INSERT INTO user_role_permission (id, role_id, permission_id) VALUES (UUID_SHORT(), 'project_member', 'PROJECT_BASE_INFO:READ+UPDATE');
INSERT INTO user_role_permission (id, role_id, permission_id) VALUES (UUID_SHORT(), 'project_member', 'PROJECT_API_DEBUG:READ');
INSERT INTO user_role_permission (id, role_id, permission_id) VALUES (UUID_SHORT(), 'project_member', 'PROJECT_API_DEBUG:READ+ADD');

View File

@ -243,8 +243,8 @@ public class PermissionConstants {
public static final String BUG_ADD = "BUG:READ+ADD";
public static final String BUG_UPDATE = "BUG:READ+UPDATE";
public static final String BUG_DELETE = "BUG:READ+DELETE";
public static final String BUG_EXPORT = "PROJECT_BUG:READ+EXPORT";
/*------ end: BUG ------*/
/*------ start: API_MANAGEMENT ------*/
public static final String PROJECT_API_DEFINITION_READ = "PROJECT_API_DEFINITION:READ";
public static final String PROJECT_API_DEFINITION_ADD = "PROJECT_API_DEFINITION:READ+ADD";

View File

@ -1,6 +1,5 @@
package io.metersphere.sdk.util;
import io.metersphere.sdk.exception.MSException;
import org.apache.commons.codec.binary.Base64;
import java.io.*;
@ -9,7 +8,6 @@ import java.util.List;
import java.util.zip.*;
public class CompressUtils {
private final static String ZIP_PATH = "/opt/metersphere/data/tmp/";
/***
* Zip压缩
@ -40,12 +38,12 @@ public class CompressUtils {
}
private static File getFile(String fileName) throws IOException {
private static File getFile(String filePath) throws IOException {
// 创建文件对象
File file;
file = new File(ZIP_PATH, fileName);
file = new File(filePath);
if (!file.exists() && !file.createNewFile()) {
throw new MSException("创建文件失败");
file.createNewFile();
}
// 返回文件
return file;
@ -93,20 +91,18 @@ public class CompressUtils {
/**
* 将多个文件压缩
*
* @param fileList 待压缩的文件列表
* @param zipFileName 压缩文件名
* @return 返回压缩好的文件
* @param zipFilePath 压缩文件所在路径
* @param fileList 要压缩的文件
* @return
* @throws IOException
*/
public static File zipFiles(String zipFileName, List<File> fileList) throws IOException {
File zipFile = getFile(zipFileName);
public static File zipFiles(String zipFilePath, List<File> fileList) throws IOException {
File zipFile = getFile(zipFilePath);
// 文件输出流
FileOutputStream outputStream = getFileStream(zipFile);
// 压缩流
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream);
int size = fileList.size();
// 压缩列表中的文件
for (File file : fileList) {
zipFile(file, zipOutputStream);

View File

@ -95,4 +95,16 @@ bug_comment.reply_user.not_blank=缺陷回复人不能为空
bug_comment_not_exist=缺陷评论不存在
bug_relate_case_not_found=未查询到关联的用例
bug_relate_case_type_unknown=关联的用例类型未知, 无法查看
bug_relate_case_permission_error=无权限查看, 请联系管理员
bug_relate_case_permission_error=无权限查看, 请联系管理员
# bug export
bug.system_columns.not_empty=系统字段不能为空
bug.export.system.columns.name=缺陷名称
bug.export.system.columns.id=ID
bug.export.system.columns.content=缺陷内容
bug.export.system.columns.status=缺陷状态
bug.export.system.columns.handle_user=处理人
bug.export.system.other.columns.create_user=创建人
bug.export.system.other.columns.create_time=创建时间
bug.export.system.other.columns.case_count=用例数
bug.export.system.other.columns.comment=评论
bug.export.system.other.columns.platform=所属平台

View File

@ -96,3 +96,15 @@ bug_comment_not_exist=Bug comment does not exist
bug_relate_case_not_found=Bug related case not found
bug_relate_case_type_unknown=Bug related case type unknown
bug_relate_case_permission_error=No permission to show the case
# bug export
bug.system_columns.not_empty=System columns cannot be empty
bug.export.system.columns.name=Name
bug.export.system.columns.id=ID
bug.export.system.columns.content=Content
bug.export.system.columns.status=Status
bug.export.system.columns.handle_user=HandleUser
bug.export.system.other.columns.create_user=Create user
bug.export.system.other.columns.create_time=Create time
bug.export.system.other.columns.case_count=Case count
bug.export.system.other.columns.comment=Comment
bug.export.system.other.columns.platform=Platform

View File

@ -95,4 +95,16 @@ bug_comment.reply_user.not_blank=缺陷回复人不能为空
bug_comment_not_exist=缺陷评论不存在
bug_relate_case_not_found=未查询到关联的用例
bug_relate_case_type_unknown=关联的用例类型未知, 无法查看
bug_relate_case_permission_error=无用例查看权限, 请联系管理员
bug_relate_case_permission_error=无用例查看权限, 请联系管理员
# bug export
bug.system_columns.not_empty=系统字段不能为空
bug.export.system.columns.name=缺陷名称
bug.export.system.columns.id=ID
bug.export.system.columns.content=缺陷内容
bug.export.system.columns.status=缺陷状态
bug.export.system.columns.handle_user=处理人
bug.export.system.other.columns.create_user=创建人
bug.export.system.other.columns.create_time=创建时间
bug.export.system.other.columns.case_count=用例数
bug.export.system.other.columns.comment=评论
bug.export.system.other.columns.platform=所属平台

View File

@ -96,3 +96,15 @@ bug_comment_not_exist=缺陷評論不存在
bug_relate_case_not_found=未查詢到關聯的用例
bug_relate_case_type_unknown=關聯的用例類型未知, 無法查看
bug_relate_case_permission_error=無權限查看, 請聯繫管理員
# bug export
bug.system_columns.not_empty=系統字段不能為空
bug.export.system.columns.name=缺陷名稱
bug.export.system.columns.id=ID
bug.export.system.columns.content=缺陷內容
bug.export.system.columns.status=缺陷狀態
bug.export.system.columns.handle_user=處理人
bug.export.system.other.columns.create_user=創建人
bug.export.system.other.columns.create_time=創建時間
bug.export.system.other.columns.case_count=用例數
bug.export.system.other.columns.comment=評論
bug.export.system.other.columns.platform=所屬平台

View File

@ -0,0 +1,39 @@
package io.metersphere.bug.constants;
import io.metersphere.bug.dto.BugCustomFieldDTO;
import io.metersphere.sdk.util.Translator;
import lombok.Data;
import java.util.LinkedHashMap;
import java.util.List;
/**
* 缺陷导出字段配置
*/
@Data
public class BugExportColumns {
private LinkedHashMap<String, String> systemColumns = new LinkedHashMap<>();
private LinkedHashMap<String, String> otherColumns = new LinkedHashMap<>();
private LinkedHashMap<String, String> customColumns = new LinkedHashMap<>();
public BugExportColumns() {
systemColumns.put("name", Translator.get("bug.export.system.columns.name"));
systemColumns.put("id", Translator.get("bug.export.system.columns.id"));
systemColumns.put("content", Translator.get("bug.export.system.columns.content"));
systemColumns.put("status", Translator.get("bug.export.system.columns.status"));
systemColumns.put("handle_user", Translator.get("bug.export.system.columns.handle_user"));
otherColumns.put("create_user", Translator.get("bug.export.system.other.columns.create_user"));
otherColumns.put("create_time", Translator.get("bug.export.system.other.columns.create_time"));
otherColumns.put("case_count", Translator.get("bug.export.system.other.columns.case_count"));
otherColumns.put("comment", Translator.get("bug.export.system.other.columns.comment"));
otherColumns.put("platform", Translator.get("bug.export.system.other.columns.platform"));
}
public void initCustomColumns(List<BugCustomFieldDTO> customFieldList) {
customFieldList.forEach(item -> {
customColumns.put(item.getId(), item.getName());
});
}
}

View File

@ -2,11 +2,9 @@ package io.metersphere.bug.controller;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.bug.constants.BugExportColumns;
import io.metersphere.bug.dto.BugDTO;
import io.metersphere.bug.dto.request.BugBatchRequest;
import io.metersphere.bug.dto.request.BugBatchUpdateRequest;
import io.metersphere.bug.dto.request.BugEditRequest;
import io.metersphere.bug.dto.request.BugPageRequest;
import io.metersphere.bug.dto.request.*;
import io.metersphere.bug.service.BugService;
import io.metersphere.project.dto.ProjectTemplateOptionDTO;
import io.metersphere.project.service.ProjectTemplateService;
@ -23,6 +21,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
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;
@ -116,4 +115,19 @@ public class BugController {
public void unfollow(@PathVariable String id) {
bugService.unfollow(id, SessionUtils.getUserId());
}
@GetMapping("/export/columns/{projectId}")
@Operation(summary = "缺陷管理-获取导出字段配置")
@RequiresPermissions(PermissionConstants.BUG_EXPORT)
public BugExportColumns getExportColumns(@PathVariable String projectId) {
return bugService.getExportColumns(projectId);
}
@PostMapping("/export")
@Operation(summary = "缺陷管理-批量导出缺陷")
@RequiresPermissions(PermissionConstants.BUG_EXPORT)
public ResponseEntity<byte[]> export(@Validated @RequestBody BugExportRequest request) throws Exception {
return bugService.export(request);
}
}

View File

@ -0,0 +1,26 @@
package io.metersphere.bug.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* 缺陷导出DTO
*/
@Data
public class BugExportDTO {
@Schema(description = "缺陷ID")
private String id;
@Schema(description = "缺陷名称")
private String name;
@Schema(description = "缺陷内容")
private String content;
@Schema(description = "缺陷状态")
private String status;
@Schema(description = "缺陷处理人")
private List<String> handleUsers;
@Schema(description = "自定义字段集合")
private Map<String, String> customFields;
}

View File

@ -0,0 +1,122 @@
package io.metersphere.bug.dto;
import io.metersphere.bug.domain.BugContent;
import io.metersphere.bug.dto.request.BugExportColumn;
import io.metersphere.sdk.util.DateUtils;
import lombok.Data;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 缺陷导出数据结构模型
*/
@Data
public class BugExportExcelModel {
// <key,text>. 如果是自定义字段则是 <id,字段显示>
private LinkedHashMap<String, String> excelHeader;
// <key,value>. 如果是自定义字段则是 <id,数据>
private List<LinkedHashMap<String, String>> excelRows;
public BugExportExcelModel(List<BugExportColumn> exportColumns,
List<BugDTO> bugList,
Map<String, List<BugCommentDTO>> bugComment,
Map<String, BugContent> bugContents,
Map<String, Long> bugCountMap) {
this.excelHeader = new LinkedHashMap<>();
//注入表头
for (BugExportColumn exportColumn : exportColumns) {
this.excelHeader.put(exportColumn.getKey(), exportColumn.getText());
}
this.excelRows = new ArrayList<>();
for (BugDTO bugDTO : bugList) {
LinkedHashMap<String, String> excelRow = new LinkedHashMap<>();
for (String key : excelHeader.keySet()) {
switch (key) {
case "name" -> excelRow.put(key, bugDTO.getTitle());
case "id" -> excelRow.put(key, bugDTO.getId());
case "content" -> excelRow.put(key, this.getBugContent(bugContents, bugDTO.getId()));
case "status" -> excelRow.put(key, bugDTO.getStatus());
case "handleUser" -> excelRow.put(key, bugDTO.getHandleUserName());
case "createUser" -> excelRow.put(key, bugDTO.getCreateUserName());
case "createTime" -> excelRow.put(key, DateUtils.getTimeString(bugDTO.getCreateTime()));
case "caseCount" -> excelRow.put(key, this.getBugCaseCount(bugCountMap, bugDTO.getId()));
case "comment" -> excelRow.put(key, this.getBugComment(bugComment.get(bugDTO.getId())));
case "platform" -> excelRow.put(key, bugDTO.getPlatform());
default -> excelRow.put(key, this.getCustomFieldValue(bugDTO.getCustomFields(), key));
}
}
excelRows.add(excelRow);
}
}
private String getCustomFieldValue(List<BugCustomFieldDTO> customFields, String key) {
if (CollectionUtils.isNotEmpty(customFields)) {
for (BugCustomFieldDTO customField : customFields) {
if (key.equals(customField.getId())) {
return customField.getValue();
}
}
}
return StringUtils.EMPTY;
}
private String getBugCaseCount(Map<String, Long> bugCountMap, String id) {
long count = 0;
if (bugCountMap.containsKey(id)) {
count = bugCountMap.get(id);
}
return String.valueOf(count);
}
private String getBugContent(Map<String, BugContent> bugContents, String id) {
if (bugContents.containsKey(id)) {
return bugContents.get(id).getDescription();
} else {
return StringUtils.EMPTY;
}
}
public String getBugComment(List<BugCommentDTO> bugCommentList) {
if (CollectionUtils.isEmpty(bugCommentList)) {
return StringUtils.EMPTY;
} else {
StringBuilder commentBuilder = new StringBuilder();
for (BugCommentDTO bugCommentDTO : bugCommentList) {
commentBuilder.append(bugCommentDTO.getCreateUser());
commentBuilder.append(StringUtils.SPACE);
commentBuilder.append(DateUtils.getTimeString(bugCommentDTO.getCreateTime()));
commentBuilder.append(StringUtils.LF);
commentBuilder.append(bugCommentDTO.getContent());
commentBuilder.append(StringUtils.LF);
}
return commentBuilder.toString();
}
}
public List<String> getHeadTexts() {
return new ArrayList<>(excelHeader.values());
}
public List<String> getHeadKeys() {
return new ArrayList<>(excelHeader.keySet());
}
public List<List<String>> getData() {
List<List<String>> returnList = new ArrayList<>();
returnList.add(this.getHeadTexts());
for (LinkedHashMap<String, String> excelRow : excelRows) {
List<String> row = new ArrayList<>();
for (String key : excelHeader.keySet()) {
row.add(excelRow.get(key));
}
returnList.add(row);
}
return returnList;
}
}

View File

@ -0,0 +1,22 @@
package io.metersphere.bug.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BugExportColumn {
@Schema(description = "字段key", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank
private String key;
@Schema(description = "字段名称", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank
private String text;
@Schema(description = "字段类型: 系统字段-system, 自定义字段-custom, 其他字段-other")
@NotBlank
private String columnType;
}

View File

@ -0,0 +1,14 @@
package io.metersphere.bug.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
@Data
public class BugExportRequest extends BugBatchRequest {
@Schema(description = "导出的字段", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "{bug.system_columns.not_empty}")
private List<BugExportColumn> exportColumns;
}

View File

@ -2,6 +2,7 @@ package io.metersphere.bug.dto.request;
import io.metersphere.system.dto.sdk.BasePageRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -10,6 +11,7 @@ import lombok.EqualsAndHashCode;
public class BugPageRequest extends BasePageRequest {
@Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{bug.project_id.not_blank}")
private String projectId;
@Schema(description = "是否回收站")

View File

@ -18,6 +18,7 @@ public interface ExtBugMapper {
*/
List<BugDTO> list(@Param("request") BugPageRequest request);
List<BugDTO> listByIds(@Param("ids") List<String> ids);
/**
* 获取缺陷业务ID
*

View File

@ -7,6 +7,28 @@
<include refid="queryWhereCondition"/>
</select>
<select id="listByIds" resultType="io.metersphere.bug.dto.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,
bc.description
from bug b
left join bug_content bc on b.id = bc.bug_id
WHERE b.id IN
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</select>
<select id="getMaxNum" resultType="java.lang.Long">
select max(num) from bug where project_id = #{projectId}
</select>

View File

@ -17,6 +17,7 @@ import io.metersphere.system.mapper.BaseUserMapper;
import io.metersphere.system.notice.constants.NoticeConstants;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.NotEmpty;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@ -47,6 +48,16 @@ public class BugCommentService {
BugCommentExample example = new BugCommentExample();
example.createCriteria().andBugIdEqualTo(bugId);
List<BugComment> bugComments = bugCommentMapper.selectByExample(example);
return this.generateCommentDTOs(bugComments);
}
/**
* 生成缺陷评论DTO
*
* @param bugComments 缺陷评论集合
* @return 缺陷评论DTO
*/
private List<BugCommentDTO> generateCommentDTOs(List<BugComment> bugComments) {
if (CollectionUtils.isEmpty(bugComments)) {
return new ArrayList<>();
}
@ -75,6 +86,23 @@ public class BugCommentService {
return parentComments;
}
/**
* 批量获取缺陷ID
*/
public Map<String, List<BugCommentDTO>> getComments(@NotEmpty List<String> bugIds) {
BugCommentExample example = new BugCommentExample();
example.createCriteria().andBugIdIn(bugIds);
List<BugComment> bugComments = bugCommentMapper.selectByExample(example);
Map<String, List<BugComment>> bugCommentByBugId = bugComments.stream().collect(Collectors.groupingBy(BugComment::getBugId));
Map<String, List<BugCommentDTO>> returnMap = new HashMap<>();
for (Map.Entry<String, List<BugComment>> entry : bugCommentByBugId.entrySet()) {
returnMap.put(entry.getKey(), generateCommentDTOs(entry.getValue()));
}
return returnMap;
}
/**
* 添加评论
* @param request 评论请求参数

View File

@ -0,0 +1,110 @@
package io.metersphere.bug.service;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import io.metersphere.bug.domain.BugContent;
import io.metersphere.bug.domain.BugContentExample;
import io.metersphere.bug.dto.BugCommentDTO;
import io.metersphere.bug.dto.BugDTO;
import io.metersphere.bug.dto.BugExportExcelModel;
import io.metersphere.bug.dto.request.BugExportColumn;
import io.metersphere.bug.mapper.BugContentMapper;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@Transactional(rollbackFor = Exception.class)
public class BugExportService {
private static final int BATCH_PROCESS_QUANTITY = 2000;
private static final String EXPORT_TEMP_BASE_FOLDER = "/tmp/metersphere/export/bug/";
@Resource
private BugContentMapper bugContentMapper;
@Resource
private BugCommentService bugCommentService;
/**
* @param list 缺陷数据
* @param exportColumns excel导出的列
* @return excel所在的文件夹
* @throws Exception
*/
public String generateExcelFiles(List<BugDTO> list, List<BugExportColumn> exportColumns) {
String filesFolder = EXPORT_TEMP_BASE_FOLDER + IDGenerator.nextStr();
try {
FileUtils.forceMkdir(new File(filesFolder));
int index = 1;
while (list.size() > 2000) {
List<BugDTO> excelBugList = list.subList(0, BATCH_PROCESS_QUANTITY);
this.generateExcelFile(excelBugList, index, filesFolder, exportColumns);
list.removeAll(excelBugList);
index = 1;
}
this.generateExcelFile(list, index, filesFolder, exportColumns);
} catch (Exception ignore) {
}
return filesFolder;
}
private void generateExcelFile(List<BugDTO> list, int fileIndex, String excelPath, List<BugExportColumn> exportColumns) throws Exception {
if (CollectionUtils.isNotEmpty(list)) {
boolean exportComment = this.exportComment(exportColumns);
boolean exportContent = this.exportContent(exportColumns);
List<String> bugIdList = list.stream().map(BugDTO::getId).toList();
Map<String, List<BugCommentDTO>> bugCommentMap = new HashMap<>();
Map<String, BugContent> bugContentMap = new HashMap<>();
//todo 等昌昌需求确定再实现
Map<String, Long> bugCountMap = new HashMap<>();
if (exportContent) {
BugContentExample example = new BugContentExample();
example.createCriteria().andBugIdIn(bugIdList);
bugContentMap = bugContentMapper.selectByExample(example).stream().collect(Collectors.toMap(BugContent::getBugId, bugContent -> bugContent));
}
if (exportComment) {
bugCommentMap = bugCommentService.getComments(bugIdList);
}
//生成excel对象
BugExportExcelModel bugExportExcelModel = new BugExportExcelModel(exportColumns, list, bugCommentMap, bugContentMap, bugCountMap);
//生成excel文件
List<List<String>> data = bugExportExcelModel.getData();
File createFile = new File(excelPath + File.separatorChar + "bug_" + fileIndex + ".xlsx");
createFile.createNewFile();
EasyExcel.write(createFile).excelType(ExcelTypeEnum.XLSX).sheet("sheet").doWrite(data);
}
}
//是否包含缺陷评论
public boolean exportComment(List<BugExportColumn> exportColumns) {
for (BugExportColumn exportColumn : exportColumns) {
if ("comment".equals(exportColumn.getKey()) && "other".equals(exportColumn.getColumnType())) {
return true;
}
}
return false;
}
//是否包含缺陷内容
public boolean exportContent(List<BugExportColumn> exportColumns) {
for (BugExportColumn exportColumn : exportColumns) {
if ("content".equals(exportColumn.getKey()) && "system".equals(exportColumn.getColumnType())) {
return true;
}
}
return false;
}
}

View File

@ -1,22 +1,24 @@
package io.metersphere.bug.service;
import io.metersphere.bug.constants.BugExportColumns;
import io.metersphere.bug.domain.*;
import io.metersphere.bug.dto.BugCustomFieldDTO;
import io.metersphere.bug.dto.BugDTO;
import io.metersphere.bug.dto.BugRelateCaseCountDTO;
import io.metersphere.bug.dto.BugTagEditDTO;
import io.metersphere.bug.dto.request.BugBatchRequest;
import io.metersphere.bug.dto.request.BugBatchUpdateRequest;
import io.metersphere.bug.dto.request.BugEditRequest;
import io.metersphere.bug.dto.request.BugPageRequest;
import io.metersphere.bug.dto.request.*;
import io.metersphere.bug.enums.BugPlatform;
import io.metersphere.bug.mapper.*;
import io.metersphere.bug.utils.CustomFieldUtils;
import io.metersphere.bug.utils.ExportUtils;
import io.metersphere.project.dto.filemanagement.FileLogRecord;
import io.metersphere.project.service.FileAssociationService;
import io.metersphere.project.service.FileService;
import io.metersphere.project.service.ProjectTemplateService;
import io.metersphere.sdk.constants.*;
import io.metersphere.sdk.constants.ApplicationNumScope;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
import io.metersphere.sdk.constants.StorageType;
import io.metersphere.sdk.constants.TemplateScene;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.sdk.util.FileAssociationSourceUtil;
@ -39,6 +41,9 @@ 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.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@ -87,6 +92,8 @@ public class BugService {
private BaseTemplateService baseTemplateService;
@Resource
private BugFollowerMapper bugFollowerMapper;
@Resource
private BugExportService bugExportService;
/**
* 缺陷列表查询
@ -214,41 +221,46 @@ public class BugService {
}
}
/**
* 批量删除缺陷
* @param request 请求参数
*/
public void batchDelete(BugBatchRequest request) {
private List<BugDTO> selectByBatchRequest(BugBatchRequest request) {
// 非Local直接删除, Local移入回收站
if (request.isSelectAll()) {
// 全选
BugPageRequest bugPageRequest = new BugPageRequest();
BeanUtils.copyBean(bugPageRequest, request);
bugPageRequest.setUseTrash(false);
CustomFieldUtils.setBaseQueryRequestCustomMultipleFields(bugPageRequest);
List<BugDTO> bugs = extBugMapper.list(bugPageRequest);
if (CollectionUtils.isNotEmpty(bugs)) {
List<String> deleteIds = bugs.stream().filter(bug -> !StringUtils.equals(BugPlatform.LOCAL.getName(), bug.getPlatform())).map(BugDTO::getId).toList();
if (CollectionUtils.isNotEmpty(deleteIds)) {
BugExample bugExample = new BugExample();
bugExample.createCriteria().andIdIn(deleteIds);
bugMapper.deleteByExample(bugExample);
}
bugs.stream().filter(bug -> StringUtils.equals(bug.getPlatform(), BugPlatform.LOCAL.getName())).forEach(bug -> {
Bug record = new Bug();
record.setId(bug.getId());
record.setDeleted(true);
bugMapper.updateByPrimaryKeySelective(record);
});
}
return extBugMapper.list(bugPageRequest);
} else {
// 勾选部分
if (CollectionUtils.isEmpty(request.getIncludeBugIds())) {
throw new MSException(Translator.get("no_bug_select"));
}
// 勾选操作数据较少, 可逐条删除
request.getIncludeBugIds().forEach(this::delete);
return extBugMapper.listByIds(request.getIncludeBugIds());
}
}
/**
* 批量删除缺陷
* @param request 请求参数
*/
public void batchDelete(BugBatchRequest request) {
List<BugDTO> bugs = this.selectByBatchRequest(request);
// 勾选部分
if (CollectionUtils.isEmpty(request.getIncludeBugIds())) {
throw new MSException(Translator.get("no_bug_select"));
}
List<String> deleteIds = bugs.stream().filter(bug -> !StringUtils.equals(BugPlatform.LOCAL.getName(), bug.getPlatform())).map(BugDTO::getId).toList();
if (CollectionUtils.isNotEmpty(deleteIds)) {
BugExample bugExample = new BugExample();
bugExample.createCriteria().andIdIn(deleteIds);
bugMapper.deleteByExample(bugExample);
}
bugs.stream().filter(bug -> StringUtils.equals(bug.getPlatform(), BugPlatform.LOCAL.getName())).forEach(bug -> {
Bug record = new Bug();
record.setId(bug.getId());
record.setDeleted(true);
bugMapper.updateByPrimaryKeySelective(record);
});
}
/**
@ -639,4 +651,24 @@ public class BugService {
fileRequest.setStorage(StorageType.MINIO.name());
return fileRequest;
}
public ResponseEntity<byte[]> export(BugExportRequest request) throws Exception {
List<BugDTO> bugs = this.selectByBatchRequest(request);
if (CollectionUtils.isEmpty(bugs)) {
throw new MSException(Translator.get("no_bug_select"));
}
ExportUtils exportUtils = new ExportUtils(bugs, request.getExportColumns());
byte[] bytes = exportUtils.exportToZipFile(bugExportService::generateExcelFiles);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"bug-export.zip\"")
.body(bytes);
}
public BugExportColumns getExportColumns(String projectId) {
BugExportColumns bugExportColumns = new BugExportColumns();
//todo 等待Scc提供自定义字段的查询方法
return bugExportColumns;
}
}

View File

@ -0,0 +1,40 @@
package io.metersphere.bug.utils;
import io.metersphere.bug.dto.BugDTO;
import io.metersphere.bug.dto.request.BugExportColumn;
import io.metersphere.sdk.util.CompressUtils;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.util.List;
import java.util.function.BiFunction;
public class ExportUtils {
private List<BugDTO> bugs;
private List<BugExportColumn> exportColumns;
public ExportUtils(
List<BugDTO> bugs,
List<BugExportColumn> exportColumns) {
this.bugs = bugs;
this.exportColumns = exportColumns;
}
/*
1.生成包含excel文件目录
2.压缩
3.删除该目录
*/
public byte[] exportToZipFile(BiFunction<List, List, String> generateExcelFilesFunction) throws Exception {
//生成包含excel文件目录
String folderPath = generateExcelFilesFunction.apply(bugs, exportColumns);
File excelFolder = new File(folderPath);
//压缩文件
File zipFile = CompressUtils.zipFiles(folderPath + File.separatorChar + "bug-export.zip", List.of(excelFolder.listFiles()));
byte[] returnByte = FileUtils.readFileToByteArray(zipFile);
//删除目录
FileUtils.deleteDirectory(excelFolder);
return returnByte;
}
}

View File

@ -19,6 +19,9 @@
},
{
"id": "BUG:READ+DELETE"
},
{
"id": "PROJECT_BUG:READ+EXPORT"
}
]
}

View File

@ -2,16 +2,15 @@ package io.metersphere.bug.controller;
import io.metersphere.bug.dto.BugCustomFieldDTO;
import io.metersphere.bug.dto.BugDTO;
import io.metersphere.bug.dto.request.BugBatchRequest;
import io.metersphere.bug.dto.request.BugBatchUpdateRequest;
import io.metersphere.bug.dto.request.BugEditRequest;
import io.metersphere.bug.dto.request.BugPageRequest;
import io.metersphere.bug.dto.request.*;
import io.metersphere.bug.utils.CustomFieldUtils;
import io.metersphere.project.dto.ProjectTemplateOptionDTO;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.util.JSON;
import io.metersphere.system.base.BaseTest;
import io.metersphere.system.controller.handler.ResultHolder;
import io.metersphere.system.dto.sdk.TemplateDTO;
import io.metersphere.system.uid.IDGenerator;
import io.metersphere.system.utils.Pager;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.*;
@ -44,6 +43,8 @@ public class BugControllerTests extends BaseTest {
public static final String BUG_BATCH_UPDATE = "/bug/batch-update";
public static final String BUG_FOLLOW = "/bug/follow";
public static final String BUG_UN_FOLLOW = "/bug/unfollow";
public static final String BUG_EXPORT_COLUMNS = "/bug/export/columns/%s";
public static final String BUG_EXPORT = "/bug/export";
@Test
@Order(0)
@ -94,6 +95,7 @@ public class BugControllerTests extends BaseTest {
bugPageRequest.setCurrent(1);
bugPageRequest.setPageSize(10);
bugPageRequest.setKeyword("default-x");
bugPageRequest.setProjectId("default-project-for-bug");
MvcResult mvcResult = this.requestPostWithOkAndReturn(BUG_PAGE, bugPageRequest);
// 获取返回值
String returnData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
@ -340,6 +342,68 @@ public class BugControllerTests extends BaseTest {
@Test
@Order(12)
void testExportColumns() throws Exception {
this.requestGetWithOkAndReturn(String.format(BUG_EXPORT_COLUMNS, "default-project-for-bug"));
//校验权限
this.requestGetPermissionTest(PermissionConstants.BUG_EXPORT, String.format(BUG_EXPORT_COLUMNS, DEFAULT_PROJECT_ID));
}
@Test
@Order(13)
void testExportBugs() throws Exception {
BugExportRequest request = new BugExportRequest();
request.setProjectId("default-project-for-bug");
request.setSelectAll(true);
List<BugExportColumn> exportColumns = new ArrayList<>();
exportColumns.add(new BugExportColumn("name", "名称", "system"));
exportColumns.add(new BugExportColumn("id", "ID", "system"));
exportColumns.add(new BugExportColumn("content", "缺内容", "system"));
exportColumns.add(new BugExportColumn("status", "陷状态", "system"));
exportColumns.add(new BugExportColumn("handleUser", "处理人儿", "system"));
exportColumns.add(new BugExportColumn("createUser", "创建人儿", "other"));
exportColumns.add(new BugExportColumn("createTime", "搞定时间", "other"));
exportColumns.add(new BugExportColumn("caseCount", "用例量", "other"));
exportColumns.add(new BugExportColumn("comment", "评论", "other"));
exportColumns.add(new BugExportColumn("platform", "平台", "other"));
request.setExportColumns(exportColumns);
MvcResult result = this.requestPostDownloadFile(BUG_EXPORT, null, request);
byte[] bytes = result.getResponse().getContentAsByteArray();
Assertions.assertTrue(bytes.length > 0);
// 非Local的缺陷导出
request.setProjectId("default-project-for-bug-no-local");
result = this.requestPostDownloadFile(BUG_EXPORT, null, request);
bytes = result.getResponse().getContentAsByteArray();
Assertions.assertTrue(bytes.length > 0);
// 勾选部分
request.setSelectAll(false);
request.setIncludeBugIds(List.of("default-bug-id-single"));
result = this.requestPostDownloadFile(BUG_EXPORT, null, request);
bytes = result.getResponse().getContentAsByteArray();
Assertions.assertTrue(bytes.length > 0);
//不存在的ID
request.setIncludeBugIds(List.of(IDGenerator.nextStr()));
this.requestPost(BUG_EXPORT, request).andExpect(status().is5xxServerError());
//没有数据
request = new BugExportRequest();
request.setProjectId("default-project-for-bug");
request.setSelectAll(false);
request.setExportColumns(exportColumns);
this.requestPost(BUG_EXPORT, request).andExpect(status().is5xxServerError());
//测试权限
request = new BugExportRequest();
request.setProjectId(DEFAULT_PROJECT_ID);
request.setSelectAll(true);
request.setExportColumns(exportColumns);
this.requestPostPermissionTest(PermissionConstants.BUG_EXPORT, BUG_EXPORT, request);
}
@Test
@Order(90)
void testDeleteBugSuccess() throws Exception {
this.requestGet(BUG_DELETE + "/default-bug-id", status().isOk());
// 非Local缺陷
@ -347,13 +411,13 @@ public class BugControllerTests extends BaseTest {
}
@Test
@Order(13)
@Order(91)
void testDeleteBugError() throws Exception {
this.requestGet(BUG_DELETE + "/default-bug-id-not-exist", status().is5xxServerError());
}
@Test
@Order(14)
@Order(92)
void testBatchDeleteEmptyBugSuccess() throws Exception {
BugBatchRequest request = new BugBatchRequest();
request.setProjectId("default-project-for-bug");
@ -367,7 +431,7 @@ public class BugControllerTests extends BaseTest {
}
@Test
@Order(15)
@Order(93)
void testFollowBug() throws Exception {
// 关注的缺陷存在
this.requestGet(BUG_FOLLOW + "/default-bug-id-single", status().isOk());
@ -376,7 +440,7 @@ public class BugControllerTests extends BaseTest {
}
@Test
@Order(16)
@Order(94)
void testUnFollowBug() throws Exception {
// 取消关注的缺陷存在
this.requestGet(BUG_UN_FOLLOW + "/default-bug-id-single", status().isOk());
@ -385,7 +449,7 @@ public class BugControllerTests extends BaseTest {
}
@Test
@Order(20)
@Order(95)
void testBatchDeleteBugSuccess() throws Exception {
BugBatchRequest request = new BugBatchRequest();
request.setProjectId("default-project-for-bug");
@ -402,7 +466,7 @@ public class BugControllerTests extends BaseTest {
}
@Test
@Order(21)
@Order(96)
void coverUtilsTest() throws Exception {
CustomFieldUtils.appendToMultipleCustomField(null, "test");
}

View File

@ -692,7 +692,7 @@ public class FileManagementControllerTests extends BaseTest {
this.fileReUploadTestSuccess();
}
for (String fileMetadataId : FILE_ID_PATH.keySet()) {
MvcResult mvcResult = this.downloadFile(String.format(FileManagementRequestUtils.URL_FILE_DOWNLOAD, fileMetadataId));
MvcResult mvcResult = this.requestGetDownloadFile(String.format(FileManagementRequestUtils.URL_FILE_DOWNLOAD, fileMetadataId), null);
byte[] fileBytes = mvcResult.getResponse().getContentAsByteArray();
//通过MD5判断是否是同一个文件
@ -844,7 +844,7 @@ public class FileManagementControllerTests extends BaseTest {
batchProcessDTO.setSelectAll(false);
batchProcessDTO.setProjectId(project.getId());
batchProcessDTO.setSelectIds(new ArrayList<>(FILE_ID_PATH.keySet()));
MvcResult mvcResult = this.batchDownloadFile(FileManagementRequestUtils.URL_FILE_BATCH_DOWNLOAD, batchProcessDTO);
MvcResult mvcResult = this.requestPostDownloadFile(FileManagementRequestUtils.URL_FILE_BATCH_DOWNLOAD, null, batchProcessDTO);
byte[] fileBytes = mvcResult.getResponse().getContentAsByteArray();
Assertions.assertTrue(fileBytes.length > 0);
@ -852,7 +852,7 @@ public class FileManagementControllerTests extends BaseTest {
batchProcessDTO = new FileBatchProcessRequest();
batchProcessDTO.setSelectAll(true);
batchProcessDTO.setProjectId(project.getId());
mvcResult = this.batchDownloadFile(FileManagementRequestUtils.URL_FILE_BATCH_DOWNLOAD, batchProcessDTO);
mvcResult = this.requestPostDownloadFile(FileManagementRequestUtils.URL_FILE_BATCH_DOWNLOAD, null, batchProcessDTO);
fileBytes = mvcResult.getResponse().getContentAsByteArray();
Assertions.assertTrue(fileBytes.length > 0);
@ -930,13 +930,13 @@ public class FileManagementControllerTests extends BaseTest {
List<FileInformationResponse> fileList = JSON.parseArray(JSON.toJSONString(pageResult.getList()), FileInformationResponse.class);
for (FileInformationResponse fileDTO : fileList) {
MvcResult originalResult = this.downloadFile(String.format(FileManagementRequestUtils.URL_FILE_PREVIEW_ORIGINAL, "admin", fileDTO.getId()));
MvcResult originalResult = this.requestGetDownloadFile(String.format(FileManagementRequestUtils.URL_FILE_PREVIEW_ORIGINAL, "admin", fileDTO.getId()), null);
Assertions.assertTrue(originalResult.getResponse().getContentAsByteArray().length > 0);
MvcResult compressedResult;
if (StringUtils.equalsIgnoreCase(fileDTO.getFileType(), "svg")) {
compressedResult = this.downloadSvgFile(String.format(FileManagementRequestUtils.URL_FILE_PREVIEW_COMPRESSED, "admin", fileDTO.getId()));
compressedResult = this.requestGetDownloadFile(String.format(FileManagementRequestUtils.URL_FILE_PREVIEW_COMPRESSED, "admin", fileDTO.getId()), MediaType.valueOf("image/svg+xml"));
} else {
compressedResult = this.downloadFile(String.format(FileManagementRequestUtils.URL_FILE_PREVIEW_COMPRESSED, "admin", fileDTO.getId()));
compressedResult = this.requestGetDownloadFile(String.format(FileManagementRequestUtils.URL_FILE_PREVIEW_COMPRESSED, "admin", fileDTO.getId()), null);
}
byte[] fileBytes = compressedResult.getResponse().getContentAsByteArray();
@ -953,14 +953,14 @@ public class FileManagementControllerTests extends BaseTest {
}
//测试重复获取
for (FileInformationResponse fileDTO : fileList) {
MvcResult originalResult = this.downloadFile(String.format(FileManagementRequestUtils.URL_FILE_PREVIEW_ORIGINAL, "admin", fileDTO.getId()));
MvcResult originalResult = this.requestGetDownloadFile(String.format(FileManagementRequestUtils.URL_FILE_PREVIEW_ORIGINAL, "admin", fileDTO.getId()), null);
Assertions.assertTrue(originalResult.getResponse().getContentAsByteArray().length > 0);
MvcResult compressedResult;
if (StringUtils.equalsIgnoreCase(fileDTO.getFileType(), "svg")) {
compressedResult = this.downloadSvgFile(String.format(FileManagementRequestUtils.URL_FILE_PREVIEW_COMPRESSED, "admin", fileDTO.getId()));
compressedResult = this.requestGetDownloadFile(String.format(FileManagementRequestUtils.URL_FILE_PREVIEW_COMPRESSED, "admin", fileDTO.getId()), MediaType.valueOf("image/svg+xml"));
} else {
compressedResult = this.downloadFile(String.format(FileManagementRequestUtils.URL_FILE_PREVIEW_COMPRESSED, "admin", fileDTO.getId()));
compressedResult = this.requestGetDownloadFile(String.format(FileManagementRequestUtils.URL_FILE_PREVIEW_COMPRESSED, "admin", fileDTO.getId()), null);
}
byte[] fileBytes = compressedResult.getResponse().getContentAsByteArray();
if (TempFileUtils.isImage(fileDTO.getFileType())) {
@ -2180,24 +2180,6 @@ public class FileManagementControllerTests extends BaseTest {
.andReturn();
}
protected MvcResult downloadSvgFile(String url, Object... uriVariables) throws Exception {
return mockMvc.perform(getRequestBuilder(url, uriVariables))
.andExpect(content().contentType(MediaType.valueOf("image/svg+xml")))
.andExpect(status().isOk()).andReturn();
}
protected MvcResult downloadFile(String url, Object... uriVariables) throws Exception {
return mockMvc.perform(getRequestBuilder(url, uriVariables))
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM_VALUE))
.andExpect(status().isOk()).andReturn();
}
protected MvcResult batchDownloadFile(String url, Object param) throws Exception {
return mockMvc.perform(getPostRequestBuilder(url, param))
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM_VALUE))
.andExpect(status().isOk()).andReturn();
}
private List<BaseTreeNode> getFileModuleTreeNode() throws Exception {
MvcResult result = this.requestGetWithOkAndReturn(String.format(FileManagementRequestUtils.URL_MODULE_TREE, project.getId()));
String returnData = result.getResponse().getContentAsString(StandardCharsets.UTF_8);

View File

@ -146,6 +146,25 @@ public abstract class BaseTest {
.andExpect(content().contentType(MediaType.APPLICATION_JSON));
}
protected MvcResult requestGetDownloadFile(String url, MediaType contentType, Object... uriVariables) throws Exception {
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
return mockMvc.perform(getRequestBuilder(url, uriVariables))
.andExpect(content().contentType(contentType))
.andExpect(status().isOk()).andReturn();
}
protected MvcResult requestPostDownloadFile(String url, MediaType contentType, Object param) throws Exception {
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
return mockMvc.perform(getPostRequestBuilder(url, param))
.andExpect(content().contentType(contentType))
.andExpect(status().isOk()).andReturn();
}
protected MvcResult requestPostAndReturn(String url, Object param, Object... uriVariables) throws Exception {
return this.requestPost(url, param, uriVariables).andReturn();
}