feat(缺陷管理): 缺陷评论功能

This commit is contained in:
song-cc-rock 2023-11-03 18:40:07 +08:00 committed by f2c-ci-robot[bot]
parent 0bde349e40
commit 8de0d13870
15 changed files with 924 additions and 5 deletions

View File

@ -89,4 +89,9 @@ third_party_not_config=项目未配置第三方平台
bug_attachment_upload_error=缺陷附件上传失败 bug_attachment_upload_error=缺陷附件上传失败
bug_attachment_delete_error=缺陷附件删除失败 bug_attachment_delete_error=缺陷附件删除失败
no_bug_select=未勾选缺陷 no_bug_select=未勾选缺陷
bug_select_not_found=未查询到勾选的缺陷 bug_select_not_found=未查询到勾选的缺陷
bug_comment.event.not_blank=缺陷评论事件类型不能为空
bug_comment.parent_id.not_blank=缺陷评论父级ID不能为空
bug_comment.parent.not_exist=父级评论不存在
bug_comment.reply_user.not_blank=缺陷回复人不能为空
bug_comment_not_exist=缺陷评论不存在

View File

@ -90,3 +90,8 @@ bug_attachment_upload_error=Bug attachment upload error
bug_attachment_delete_error=Bug attachment delete error bug_attachment_delete_error=Bug attachment delete error
no_bug_select=No bug selected no_bug_select=No bug selected
bug_select_not_found=Selected bug not found bug_select_not_found=Selected bug not found
bug_comment.event.not_blank=Bug comment event cannot be empty
bug_comment.parent_id.not_blank=Bug comment parent-id cannot be empty
bug_comment.parent.not_exist=Bug comment parent does not exist
bug_comment.reply_user.not_blank=Bug comment reply-user cannot be empty
bug_comment_not_exist=Bug comment does not exist

View File

@ -89,4 +89,9 @@ third_party_not_config=项目未配置第三方平台
bug_attachment_upload_error=缺陷附件上传失败 bug_attachment_upload_error=缺陷附件上传失败
bug_attachment_delete_error=缺陷附件删除失败 bug_attachment_delete_error=缺陷附件删除失败
no_bug_select=未勾选缺陷 no_bug_select=未勾选缺陷
bug_select_not_found=未查询到勾选的缺陷 bug_select_not_found=未查询到勾选的缺陷
bug_comment.event.not_blank=缺陷评论事件类型不能为空
bug_comment.parent_id.not_blank=缺陷评论父级ID不能为空
bug_comment.parent.not_exist=父级评论不存在
bug_comment.reply_user.not_blank=缺陷回复人不能为空
bug_comment_not_exist=缺陷评论不存在

View File

@ -90,4 +90,9 @@ bug_attachment_upload_error=缺陷附件上傳失敗
bug_attachment_delete_error=缺陷附件刪除失敗 bug_attachment_delete_error=缺陷附件刪除失敗
no_bug_select=未勾選缺陷 no_bug_select=未勾選缺陷
bug_select_not_found=未查詢到勾選的缺陷 bug_select_not_found=未查詢到勾選的缺陷
bug_comment.event.not_blank=評論事件類型不能為空
bug_comment.parent_id.not_blank=缺陷評論父級ID不能為空
bug_comment.parent.not_exist=父級評論不存在
bug_comment.reply_user.not_blank=缺陷回復人不能為空
bug_comment_not_exist=缺陷評論不存在

View File

@ -0,0 +1,47 @@
package io.metersphere.bug.controller;
import io.metersphere.bug.domain.BugComment;
import io.metersphere.bug.dto.BugCommentDTO;
import io.metersphere.bug.dto.BugCommentUserInfo;
import io.metersphere.bug.dto.request.BugCommentEditRequest;
import io.metersphere.bug.service.BugCommentService;
import io.metersphere.system.utils.SessionUtils;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "缺陷管理-评论")
@RestController
@RequestMapping("/bug/comment")
public class BugCommentController {
@Resource
private BugCommentService bugCommentService;
@GetMapping("/get/{bugId}")
public List<BugCommentDTO> get(@PathVariable String bugId) {
return bugCommentService.getComments(bugId);
}
@GetMapping("/user-extra/{bugId}")
public List<BugCommentUserInfo> getUserExtra(@PathVariable String bugId) {
return bugCommentService.getUserExtra(bugId);
}
@PostMapping("/add")
public BugComment add(@RequestBody BugCommentEditRequest request) {
return bugCommentService.addComment(request, SessionUtils.getUserId());
}
@PostMapping("/update")
public BugComment update(@RequestBody BugCommentEditRequest request) {
return bugCommentService.updateComment(request, SessionUtils.getUserId());
}
@GetMapping("/delete/{commentId}")
public void delete(@PathVariable String commentId) {
bugCommentService.deleteComment(commentId);
}
}

View File

@ -0,0 +1,22 @@
package io.metersphere.bug.dto;
import io.metersphere.bug.domain.BugComment;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = false)
public class BugCommentDTO extends BugComment {
@Schema(description = "创建人名称")
private String createUserName;
@Schema(description = "回复人名称")
private String replyUserName;
@Schema(description = "子评论")
private List<BugCommentDTO> childComments;
}

View File

@ -0,0 +1,19 @@
package io.metersphere.bug.dto;
import io.metersphere.system.dto.sdk.OptionDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = false)
public class BugCommentNoticeDTO extends BugDTO{
@Schema(description = "通知人集合, @用户, 使用';'隔开! ")
private String notifier;
@Schema(description = "自定义字段值集合")
private List<OptionDTO> fields;
}

View File

@ -0,0 +1,14 @@
package io.metersphere.bug.dto;
import io.metersphere.system.domain.User;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
public class BugCommentUserInfo extends User {
@Schema(description = "用户头像")
private String createUserAvatar;
}

View File

@ -0,0 +1,36 @@
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.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
public class BugCommentEditRequest {
@Schema(description = "评论ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{bug_comment.id.not_blank}", groups = {Updated.class})
private String id;
@Schema(description = "缺陷ID", requiredMode = Schema.RequiredMode.REQUIRED)
private String bugId;
@Schema(description = "回复人")
private String replyUser;
@Schema(description = "通知人, @名称展示, 以用户ID';'分隔")
private String notifier;
@Schema(description = "父评论ID")
private String parentId;
@Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED)
private String content;
@Schema(description = "任务事件(仅评论: COMMENT; 评论并@: AT; 回复评论/回复并@: REPLAY;)", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{bug_comment.event.not_blank}", groups = {Created.class})
private String event;
}

View File

@ -0,0 +1,132 @@
package io.metersphere.bug.service;
import io.metersphere.bug.domain.Bug;
import io.metersphere.bug.dto.BugCommentNoticeDTO;
import io.metersphere.bug.dto.BugCustomFieldDTO;
import io.metersphere.bug.dto.request.BugCommentEditRequest;
import io.metersphere.bug.mapper.BugMapper;
import io.metersphere.bug.mapper.ExtBugCustomFieldMapper;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.system.domain.User;
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.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@Service
@Transactional(rollbackFor = Exception.class)
public class BugCommentNoticeService {
@Resource
private BugMapper bugMapper;
@Resource
private UserMapper userMapper;
@Resource
private NoticeSendService noticeSendService;
@Resource
private ExtBugCustomFieldMapper extBugCustomFieldMapper;
/**
* 获取缺陷通知参数
* @param request 页面请求参数
* @return 缺陷通知参数
*/
public BugCommentNoticeDTO getBugCommentNotice(BugCommentEditRequest request) {
Bug bug = bugMapper.selectByPrimaryKey(request.getBugId());
BugCommentNoticeDTO bugCommentNoticeDTO = new BugCommentNoticeDTO();
BeanUtils.copyBean(bugCommentNoticeDTO, bug);
setNotifier(request, bugCommentNoticeDTO);
setCustomField(bugCommentNoticeDTO);
return bugCommentNoticeDTO;
}
/**
* 发送缺陷通知
*/
@Async
public void sendNotice(BugCommentEditRequest request, BugCommentNoticeDTO noticeDTO, String currentUser) {
User user = userMapper.selectByPrimaryKey(currentUser);
Map<String, String> defaultTemplateMap = MessageTemplateUtils.getDefaultTemplateMap();
String template = defaultTemplateMap.get(NoticeConstants.TaskType.BUG_TASK + "_" + request.getEvent());
Map<String, String> defaultSubjectMap = MessageTemplateUtils.getDefaultTemplateSubjectMap();
String subject = defaultSubjectMap.get(NoticeConstants.TaskType.BUG_TASK + "_" + request.getEvent());
BeanMap beanMap = new BeanMap(noticeDTO);
Map paramMap = new HashMap<>(beanMap);
paramMap.put(NoticeConstants.RelatedUser.OPERATOR, user.getName());
NoticeModel noticeModel = NoticeModel.builder()
.operator(currentUser)
.context(template)
.subject(subject)
.paramMap(paramMap)
.event(request.getEvent())
.status((String) paramMap.get("status"))
.excludeSelf(true)
.relatedUsers(getRelateUser(noticeDTO.getNotifier()))
.build();
noticeSendService.send(NoticeConstants.TaskType.BUG_TASK, noticeModel);
}
/**
* 设置缺陷通知的自定义字段值
* @param bugCommentNoticeDTO 缺陷通知参数
*/
private void setCustomField(BugCommentNoticeDTO bugCommentNoticeDTO) {
List<BugCustomFieldDTO> bugCustomFields = extBugCustomFieldMapper.getBugCustomFields(List.of(bugCommentNoticeDTO.getId()), bugCommentNoticeDTO.getProjectId());
if (CollectionUtils.isNotEmpty(bugCustomFields)) {
List<OptionDTO> fields = bugCustomFields.stream().map(field -> new OptionDTO(field.getName(), field.getValue())).toList();
bugCommentNoticeDTO.setFields(fields);
}
}
/**
* 评论通知@用户处理与功能用例保持一致即可, 根据事件类型设置通知人
* 如果是REPLAY事件需要判断有无@的人如果有@的人且和当前被回复的人不是同一人这里只被回复的人,如果是同一人这里通知人为空走AT事件
* 如果不是REPLAY事件需要判断有无被回复的人如果被回复的人不在被@人里则用页面参数传递的通知人如果在则排除这个人,如果没有被回复的人用页面数据
*
* @param request 页面请求参数
* @param bugCommentNoticeDTO 缺陷通知参数
*/
private void setNotifier(BugCommentEditRequest request, BugCommentNoticeDTO bugCommentNoticeDTO) {
String notifier = request.getNotifier();
String replyUser = request.getReplyUser();
if (StringUtils.equals(request.getEvent(), NoticeConstants.Event.REPLY)) {
// REPLAY事件, 只通知回复人(且不为空);
bugCommentNoticeDTO.setNotifier(replyUser);
} else {
// AT事件, 如果有回复人, 直接排除;
if (StringUtils.isNotBlank(replyUser) && StringUtils.isNotBlank(notifier)) {
List<String> notifierList = new ArrayList<>(Arrays.asList(notifier.split(";")));
if (notifierList.contains(replyUser)) {
notifierList.remove(replyUser);
bugCommentNoticeDTO.setNotifier(String.join(";", notifierList));
} else {
bugCommentNoticeDTO.setNotifier(notifier);
}
} else {
bugCommentNoticeDTO.setNotifier(notifier);
}
}
}
/**
* 根据通知人分隔的字符串获取参数
* @return 通知人列表
*/
private List<String> getRelateUser(String notifier) {
if (StringUtils.isBlank(notifier)) {
return null;
}
return Arrays.asList(notifier.split(";"));
}
}

View File

@ -0,0 +1,291 @@
package io.metersphere.bug.service;
import io.metersphere.bug.domain.Bug;
import io.metersphere.bug.domain.BugComment;
import io.metersphere.bug.domain.BugCommentExample;
import io.metersphere.bug.dto.BugCommentDTO;
import io.metersphere.bug.dto.BugCommentNoticeDTO;
import io.metersphere.bug.dto.BugCommentUserInfo;
import io.metersphere.bug.dto.request.BugCommentEditRequest;
import io.metersphere.bug.mapper.BugCommentMapper;
import io.metersphere.bug.mapper.BugMapper;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.domain.User;
import io.metersphere.system.domain.UserExample;
import io.metersphere.system.dto.sdk.OptionDTO;
import io.metersphere.system.mapper.BaseUserMapper;
import io.metersphere.system.mapper.UserMapper;
import io.metersphere.system.notice.constants.NoticeConstants;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
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.*;
import java.util.stream.Collectors;
@Service
@Transactional(rollbackFor = Exception.class)
public class BugCommentService {
@Resource
private BugMapper bugMapper;
@Resource
private UserMapper userMapper;
@Resource
private BaseUserMapper baseUserMapper;
@Resource
private BugCommentMapper bugCommentMapper;
@Resource
private BugCommentNoticeService bugCommentNoticeService;
/**
* 获取缺陷评论
* @param bugId 缺陷ID
* @return 缺陷评论
*/
public List<BugCommentDTO> getComments(String bugId) {
BugCommentExample example = new BugCommentExample();
example.createCriteria().andBugIdEqualTo(bugId);
List<BugComment> bugComments = bugCommentMapper.selectByExample(example);
if (CollectionUtils.isEmpty(bugComments)) {
return new ArrayList<>();
}
// BugComment -> BugCommentDTO
Map<String, String> userMap = getUserMap(bugComments);
List<BugCommentDTO> bugCommentDTOList = bugComments.stream().map(bugComment -> {
BugCommentDTO commentDTO = new BugCommentDTO();
BeanUtils.copyBean(commentDTO, bugComment);
commentDTO.setCreateUserName(userMap.get(bugComment.getCreateUser()));
commentDTO.setReplyUserName(userMap.get(bugComment.getReplyUser()));
return commentDTO;
}).toList();
// 父级评论
List<BugCommentDTO> parentComments = bugCommentDTOList.stream().filter(bugCommentDTO -> StringUtils.isEmpty(bugCommentDTO.getParentId()))
.sorted(Comparator.comparing(BugCommentDTO::getCreateTime, Comparator.reverseOrder())).toList();
parentComments.forEach(parentComment -> {
// 子级评论
List<BugCommentDTO> childComments = bugCommentDTOList.stream().filter(bugCommentDTO -> StringUtils.equals(bugCommentDTO.getParentId(), parentComment.getId()))
.sorted(Comparator.comparing(BugCommentDTO::getCreateTime, Comparator.reverseOrder())).toList();
parentComment.setChildComments(childComments);
});
return parentComments;
}
/**
* 获取用户全部信息
* @param bugId 缺陷ID
* @return 用户集合
*/
public List<BugCommentUserInfo> getUserExtra(String bugId) {
BugCommentExample example = new BugCommentExample();
example.createCriteria().andBugIdEqualTo(bugId);
List<BugComment> bugComments = bugCommentMapper.selectByExample(example);
if (CollectionUtils.isEmpty(bugComments)) {
return new ArrayList<>();
}
List<String> userIds = new ArrayList<>(bugComments.stream().map(BugComment::getCreateUser).toList());
bugComments.forEach(bugComment -> {
if (StringUtils.isNotEmpty(bugComment.getNotifier())) {
// 通知人@内容以';'分隔
userIds.addAll(Arrays.asList(bugComment.getNotifier().split(";")));
}
});
UserExample userExample = new UserExample();
userExample.createCriteria().andIdIn(userIds.stream().distinct().toList());
List<User> users = userMapper.selectByExample(userExample);
return users.stream().map(user -> {
BugCommentUserInfo userInfo = new BugCommentUserInfo();
BeanUtils.copyBean(userInfo, user);
return userInfo;
}).toList();
}
/**
* 添加评论
* @param request 评论请求参数
* @param currentUser 当前用户ID
* @return 缺陷评论
*/
public BugComment addComment(BugCommentEditRequest request, String currentUser) {
checkBug(request.getBugId());
BugComment bugComment = getBugComment(request, currentUser, false);
if (StringUtils.equals(request.getEvent(), NoticeConstants.Event.REPLY)) {
return addBugCommentAndReplyNotice(request, bugComment, currentUser);
} else {
return addBugCommentAndCommentNotice(request, bugComment, currentUser);
}
}
/**
* 修改评论
* @param request 评论请求参数
* @param currentUser 当前用户ID
* @return 缺陷评论
*/
public BugComment updateComment(BugCommentEditRequest request, String currentUser) {
checkComment(request.getId());
BugComment bugComment = getBugComment(request, currentUser, true);
return updateBugCommentAndNotice(request, bugComment, currentUser);
}
/**
* 删除评论
* @param commentId 评论ID
*/
public void deleteComment(String commentId) {
checkComment(commentId);
BugComment bugComment = bugCommentMapper.selectByPrimaryKey(commentId);
if (StringUtils.isEmpty(bugComment.getParentId())) {
// 如果是父评论, 先删除子评论
BugCommentExample example = new BugCommentExample();
example.createCriteria().andParentIdEqualTo(commentId);
bugCommentMapper.deleteByExample(example);
}
// 删除当条评论
bugCommentMapper.deleteByPrimaryKey(commentId);
}
/**
* 新建评论并发送REPLY通知(需处理@提醒人)
* @param request 缺陷评论请求参数
* @param bugComment 缺陷评论
* @return 缺陷评论
*/
public BugComment addBugCommentAndReplyNotice(BugCommentEditRequest request, BugComment bugComment, String currentUser) {
checkAndSetReplyComment(request, bugComment);
bugCommentMapper.insertSelective(bugComment);
// 回复通知
BugCommentNoticeDTO bugCommentNotice = bugCommentNoticeService.getBugCommentNotice(request);
bugCommentNoticeService.sendNotice(request, bugCommentNotice, currentUser);
// @通知
request.setEvent(NoticeConstants.Event.AT);
bugCommentNotice = bugCommentNoticeService.getBugCommentNotice(request);
bugCommentNoticeService.sendNotice(request, bugCommentNotice, currentUser);
return bugComment;
}
/**
* 新建评论并发送COMMENT通知(需处理@提醒人)
* @param request 缺陷评论请求参数
* @param bugComment 缺陷评论
* @return 缺陷评论
*/
public BugComment addBugCommentAndCommentNotice(BugCommentEditRequest request, BugComment bugComment, String currentUser) {
bugComment.setNotifier(request.getNotifier());
bugCommentMapper.insertSelective(bugComment);
/*
* 通知;
* 如果通知@人不为空, 先发送@通知, 再发送评论通知.
* 如果通知@人为空, 只发送评论通知.
*/
BugCommentNoticeDTO bugCommentNotice = bugCommentNoticeService.getBugCommentNotice(request);
bugCommentNoticeService.sendNotice(request, bugCommentNotice, currentUser);
if (StringUtils.isEmpty(request.getParentId()) && StringUtils.equals(request.getEvent(), NoticeConstants.Event.AT)) {
request.setEvent(NoticeConstants.Event.COMMENT);
bugCommentNoticeService.sendNotice(request, bugCommentNotice, currentUser);
}
return bugComment;
}
public BugComment updateBugCommentAndNotice(BugCommentEditRequest request, BugComment bugComment, String currentUser) {
bugComment.setNotifier(request.getNotifier());
bugCommentMapper.updateByPrimaryKeySelective(bugComment);
if (StringUtils.equals(request.getEvent(), NoticeConstants.Event.COMMENT)) {
// COMMENT事件, 无需通知
return bugComment;
}
// REPLY及AT通知, 都只处理@通知人
if (StringUtils.equals(request.getEvent(), NoticeConstants.Event.REPLY)) {
// REPLY事件需保留回复人
request.setReplyUser(null);
request.setEvent(NoticeConstants.Event.AT);
}
BugCommentNoticeDTO bugCommentNotice = bugCommentNoticeService.getBugCommentNotice(request);
bugCommentNoticeService.sendNotice(request, bugCommentNotice, currentUser);
return bugComment;
}
/**
* 获取评论
* @param request 缺陷评论请求参数
* @param currentUser 当前用户ID
* @return 缺陷评论
*/
public static BugComment getBugComment(BugCommentEditRequest request, String currentUser, boolean isUpdate) {
BugComment bugComment = new BugComment();
bugComment.setId(isUpdate ? request.getId() : IDGenerator.nextStr());
bugComment.setBugId(request.getBugId());
bugComment.setContent(request.getContent());
if (!isUpdate) {
bugComment.setCreateUser(currentUser);
bugComment.setCreateTime(System.currentTimeMillis());
}
bugComment.setUpdateUser(currentUser);
bugComment.setUpdateTime(System.currentTimeMillis());
return bugComment;
}
/**
* 校验并设置评论信息
* @param request 缺陷评论请求参数
* @param bugComment 缺陷评论
*/
private void checkAndSetReplyComment(BugCommentEditRequest request, BugComment bugComment) {
if (StringUtils.isEmpty(request.getParentId())) {
throw new MSException(Translator.get("bug_comment.parent_id.not_blank"));
}
BugComment parentComment = bugCommentMapper.selectByPrimaryKey(request.getParentId());
if (parentComment == null) {
throw new MSException(Translator.get("bug_comment.parent.not_exist"));
}
bugComment.setParentId(request.getParentId());
if (StringUtils.isEmpty(request.getReplyUser())) {
throw new MSException(Translator.get("bug_comment.reply_user.not_blank"));
}
bugComment.setReplyUser(request.getReplyUser());
bugComment.setNotifier(request.getNotifier());
}
/**
* 校验缺陷是否存在
* @param bugId 缺陷ID
*/
private void checkBug(String bugId) {
Bug bug = bugMapper.selectByPrimaryKey(bugId);
if (bug == null) {
throw new IllegalArgumentException(Translator.get("bug_not_exist"));
}
}
/**
* 校验评论是否存在
* @param commentId 评论ID
*/
private void checkComment(String commentId) {
BugComment bugComment = bugCommentMapper.selectByPrimaryKey(commentId);
if (bugComment == null) {
throw new IllegalArgumentException(Translator.get("bug_comment_not_exist"));
}
}
/**
* 获取用户Map
* @param bugComments 缺陷评论集合
* @return 用户信息Map
*/
private Map<String, String> getUserMap(List<BugComment> bugComments) {
List<String> userIds = new ArrayList<>();
userIds.addAll(bugComments.stream().map(BugComment::getCreateUser).toList());
userIds.addAll(bugComments.stream().map(BugComment::getReplyUser).toList());
List<OptionDTO> options = baseUserMapper.selectUserOptionByIds(userIds.stream().distinct().toList());
return options.stream().collect(Collectors.toMap(OptionDTO::getId, OptionDTO::getName));
}
}

View File

@ -552,8 +552,7 @@ public class BugService {
*/ */
private List<BugDTO> buildExtraInfo(List<BugDTO> bugs) { private List<BugDTO> buildExtraInfo(List<BugDTO> bugs) {
// 获取用户集合 // 获取用户集合
List<String> userIds = new ArrayList<>(); List<String> userIds = new ArrayList<>(bugs.stream().map(BugDTO::getCreateUser).toList());
userIds.addAll(bugs.stream().map(BugDTO::getCreateUser).toList());
userIds.addAll(bugs.stream().map(BugDTO::getUpdateUser).toList()); userIds.addAll(bugs.stream().map(BugDTO::getUpdateUser).toList());
userIds.addAll(bugs.stream().map(BugDTO::getDeleteUser).toList()); userIds.addAll(bugs.stream().map(BugDTO::getDeleteUser).toList());
userIds.addAll(bugs.stream().map(BugDTO::getHandleUser).toList()); userIds.addAll(bugs.stream().map(BugDTO::getHandleUser).toList());

View File

@ -0,0 +1,312 @@
package io.metersphere.bug.controller;
import io.metersphere.bug.domain.BugComment;
import io.metersphere.bug.dto.BugCommentDTO;
import io.metersphere.bug.dto.BugCommentUserInfo;
import io.metersphere.bug.dto.request.BugCommentEditRequest;
import io.metersphere.bug.mapper.BugCommentMapper;
import io.metersphere.project.domain.Notification;
import io.metersphere.project.domain.NotificationExample;
import io.metersphere.project.mapper.NotificationMapper;
import io.metersphere.sdk.util.JSON;
import io.metersphere.system.base.BaseTest;
import io.metersphere.system.controller.handler.ResultHolder;
import io.metersphere.system.notice.constants.NoticeConstants;
import jakarta.annotation.Resource;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.web.servlet.MvcResult;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BugCommentTests extends BaseTest {
@Resource
private BugCommentMapper bugCommentMapper;
@Resource
private NotificationMapper notificationMapper;
public static final String BUG_COMMENT_GET = "/bug/comment/get";
public static final String BUG_COMMENT_USER_GET = "/bug/comment/user-extra";
public static final String BUG_COMMENT_ADD = "/bug/comment/add";
public static final String BUG_COMMENT_UPDATE = "/bug/comment/update";
public static final String BUG_COMMENT_DELETE = "/bug/comment/delete";
@Test
@Order(0)
@Sql(scripts = {"/dml/init_bug_comment.sql"}, config = @SqlConfig(encoding = "utf-8", transactionMode = SqlConfig.TransactionMode.ISOLATED))
public void testGetBugComment() throws Exception {
MvcResult mvcResult = this.requestGetWithOkAndReturn(BUG_COMMENT_GET + "/default-bug-id-for-comment");
String sortData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder resultHolder = JSON.parseObject(sortData, ResultHolder.class);
List<BugCommentDTO> comments = JSON.parseArray(JSON.toJSONString(resultHolder.getData()), BugCommentDTO.class);
Assertions.assertTrue(CollectionUtils.isNotEmpty(comments));
Assertions.assertTrue(StringUtils.equals("default-bug-comment-id-3", comments.get(0).getId()));
// 第二条评论的子评论不为空且ID为default-bug-comment-id-2
Assertions.assertTrue(CollectionUtils.isNotEmpty(comments.get(1).getChildComments()) &&
StringUtils.equals("default-bug-comment-id-2", comments.get(1).getChildComments().get(0).getId()));
}
@Test
@Order(1)
public void testGetBugEmptyComment() throws Exception {
MvcResult mvcResult = this.requestGetWithOkAndReturn(BUG_COMMENT_GET + "/default-bug-id-for-comment1");
String sortData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder resultHolder = JSON.parseObject(sortData, ResultHolder.class);
List<BugCommentDTO> comments = JSON.parseArray(JSON.toJSONString(resultHolder.getData()), BugCommentDTO.class);
// 该缺陷没有评论
Assertions.assertTrue(CollectionUtils.isEmpty(comments));
}
@Test
@Order(2)
public void testGetBugUserExtra() throws Exception {
MvcResult mvcResult = this.requestGetWithOkAndReturn(BUG_COMMENT_USER_GET + "/default-bug-id-for-comment");
String sortData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder resultHolder = JSON.parseObject(sortData, ResultHolder.class);
List<BugCommentUserInfo> userInfos = JSON.parseArray(JSON.toJSONString(resultHolder.getData()), BugCommentUserInfo.class);
Assertions.assertTrue(CollectionUtils.isNotEmpty(userInfos));
long count = userInfos.stream().filter(userInfo -> StringUtils.contains(userInfo.getId(), "oasis-user-id")).count();
// 该缺陷评论存在两个通知人
Assertions.assertEquals(3, count);
}
@Test
@Order(3)
public void testGetBugEmptyUserExtra() throws Exception {
MvcResult mvcResult = this.requestGetWithOkAndReturn(BUG_COMMENT_USER_GET + "/default-bug-id-for-comment1");
String sortData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder resultHolder = JSON.parseObject(sortData, ResultHolder.class);
List<BugCommentUserInfo> userInfos = JSON.parseArray(JSON.toJSONString(resultHolder.getData()), BugCommentUserInfo.class);
Assertions.assertTrue(CollectionUtils.isEmpty(userInfos));
}
@Test
@Order(4)
public void testAddBugCommentOnlySuccess() throws Exception {
// 只评论, @用户, 也不是回复
BugCommentEditRequest request = new BugCommentEditRequest();
request.setBugId("default-bug-id-for-comment1");
request.setContent("This is a comment test!");
request.setEvent(NoticeConstants.Event.COMMENT);
BugComment bugComment = saveOrUpdateComment(request, false);
// 查询通知表中记录
NotificationExample example = new NotificationExample();
example.createCriteria().andResourceIdEqualTo("default-bug-id-for-comment1").andResourceTypeEqualTo(NoticeConstants.TaskType.BUG_TASK);
List<Notification> notifications = notificationMapper.selectByExample(example);
Assertions.assertTrue(CollectionUtils.isNotEmpty(notifications));
Assertions.assertTrue(StringUtils.equals(notifications.get(0).getReceiver(), "oasis-user-id"));
BugComment comment = bugCommentMapper.selectByPrimaryKey(bugComment.getId());
Assertions.assertTrue(StringUtils.equals(comment.getContent(), "This is a comment test!"));
}
@Test
@Order(5)
public void testAddBugCommentOnlyError() throws Exception {
// 只评论, @用户, 也不是回复 (缺陷不存在)
BugCommentEditRequest request = new BugCommentEditRequest();
request.setBugId("default-bug-id-for-comment-x");
request.setContent("This is a comment test!");
request.setEvent(NoticeConstants.Event.COMMENT);
this.requestPost(BUG_COMMENT_ADD, request, status().is5xxServerError());
// 回复评论, 没有父级评论ID
request.setReplyUser("oasis-user-id1");
request.setBugId("default-bug-id-for-comment");
request.setContent("This is a comment test!");
request.setEvent(NoticeConstants.Event.REPLY);
this.requestPost(BUG_COMMENT_ADD, request, status().is5xxServerError());
// 回复评论, 父级评论不存在
request.setReplyUser("oasis-user-id1");
request.setBugId("default-bug-id-for-comment");
request.setContent("This is a comment test!");
request.setParentId("default-bug-comment-id-3-x");
request.setEvent(NoticeConstants.Event.REPLY);
this.requestPost(BUG_COMMENT_ADD, request, status().is5xxServerError());
// 回复评论, 但回复人为空
request.setReplyUser(null);
request.setBugId("default-bug-id-for-comment");
request.setContent("This is a comment test!");
request.setParentId("default-bug-comment-id-3");
request.setEvent(NoticeConstants.Event.REPLY);
this.requestPost(BUG_COMMENT_ADD, request, status().is5xxServerError());
}
@Test
@Order(6)
public void testAddBugCommentWithAtEventSuccess() throws Exception {
// 评论并@用户, 不是回复
BugCommentEditRequest request = new BugCommentEditRequest();
request.setBugId("default-bug-id-for-comment");
request.setContent("This is a at comment test!");
request.setNotifier(String.join(";", "oasis-user-id4"));
request.setEvent(NoticeConstants.Event.AT);
BugComment bugComment = saveOrUpdateComment(request, false);
// 查询通知表中记录
NotificationExample example = new NotificationExample();
example.createCriteria().andResourceIdEqualTo("default-bug-id-for-comment").andResourceTypeEqualTo(NoticeConstants.TaskType.BUG_TASK);
List<Notification> notifications = notificationMapper.selectByExample(example);
// 存在2条通知
Assertions.assertEquals(2, notifications.size());
long atCount = notifications.stream().filter(notification -> StringUtils.equals(notification.getOperation(), NoticeConstants.Event.AT)).count();
// 仅有1条AT方法通知
Assertions.assertEquals(atCount, 1);
BugComment comment = bugCommentMapper.selectByPrimaryKey(bugComment.getId());
Assertions.assertTrue(StringUtils.equals(comment.getContent(), "This is a at comment test!"));
}
@Test
@Order(7)
public void testAddBugCommentWithReplyEventSuccess() throws Exception {
// 评论并回复用户, 但不@
BugCommentEditRequest request = new BugCommentEditRequest();
request.setBugId("default-bug-id-for-comment");
request.setContent("This is a reply comment test!");
request.setReplyUser("oasis-user-id1");
request.setParentId("default-bug-comment-id-3");
request.setEvent(NoticeConstants.Event.REPLY);
BugComment bugComment = saveOrUpdateComment(request, false);
// 查询通知表中记录
NotificationExample example = new NotificationExample();
example.createCriteria().andResourceIdEqualTo("default-bug-id-for-comment").andResourceTypeEqualTo(NoticeConstants.TaskType.BUG_TASK);
List<Notification> notifications = notificationMapper.selectByExample(example);
// 存在3条通知
Assertions.assertEquals(3, notifications.size());
long reCount = notifications.stream().filter(notification -> StringUtils.equals(notification.getOperation(), NoticeConstants.Event.REPLY)).count();
// 仅有1条REPLY方法通知
Assertions.assertEquals(reCount, 1);
BugComment comment = bugCommentMapper.selectByPrimaryKey(bugComment.getId());
Assertions.assertTrue(StringUtils.equals(comment.getContent(), "This is a reply comment test!"));
}
@Test
@Order(8)
public void testAddBugCommentWithReplyAndAtEventSuccess() throws Exception {
// 评论并回复用户, 但不@当前回复人
BugCommentEditRequest request = new BugCommentEditRequest();
request.setBugId("default-bug-id-for-comment");
request.setContent("This is a reply && at comment test!");
request.setReplyUser("oasis-user-id");
request.setNotifier(String.join(";", "oasis-user-id3", "oasis-user-id4"));
request.setParentId("default-bug-comment-id-2");
request.setEvent(NoticeConstants.Event.REPLY);
BugComment bugComment = saveOrUpdateComment(request, false);
// 查询通知表中记录
NotificationExample example = new NotificationExample();
example.createCriteria().andResourceIdEqualTo("default-bug-id-for-comment").andResourceTypeEqualTo(NoticeConstants.TaskType.BUG_TASK);
List<Notification> notifications = notificationMapper.selectByExample(example);
// 存在7条通知
Assertions.assertEquals(6, notifications.size());
long reCount = notifications.stream().filter(notification -> StringUtils.equals(notification.getOperation(), NoticeConstants.Event.REPLY)).count();
// 仅有2条REPLY方法通知
Assertions.assertEquals(reCount, 2);
BugComment comment = bugCommentMapper.selectByPrimaryKey(bugComment.getId());
Assertions.assertTrue(StringUtils.equals(comment.getContent(), "This is a reply && at comment test!"));
// 评论并回复用户, @当前回复人
request.setNotifier(String.join(";", "oasis-user-id", "oasis-user-id2"));
saveOrUpdateComment(request, false);
List<Notification> notificationsList = notificationMapper.selectByExample(example);
// 存在8条通知
Assertions.assertEquals(8, notificationsList.size());
long reCount1 = notificationsList.stream().filter(notification -> StringUtils.equals(notification.getOperation(), NoticeConstants.Event.REPLY)).count();
// 仅有3条REPLY方法通知
Assertions.assertEquals(reCount1, 3);
}
@Test
@Order(9)
public void testUpdateBugCommentOnlySuccess() throws Exception {
// 只编辑第一级评论
BugCommentEditRequest request = new BugCommentEditRequest();
request.setId("default-bug-comment-id-3");
request.setBugId("default-bug-id-for-comment");
request.setContent("This is a comment test!");
request.setEvent(NoticeConstants.Event.COMMENT);
BugComment bugComment = saveOrUpdateComment(request, true);
NotificationExample example = new NotificationExample();
example.createCriteria().andResourceIdEqualTo("default-bug-id-for-comment").andResourceTypeEqualTo(NoticeConstants.TaskType.BUG_TASK);
List<Notification> notifications = notificationMapper.selectByExample(example);
// 存在8条通知
Assertions.assertEquals(8, notifications.size());
BugComment comment = bugCommentMapper.selectByPrimaryKey(bugComment.getId());
Assertions.assertTrue(StringUtils.equals(comment.getContent(), "This is a comment test!"));
}
@Test
@Order(10)
public void testUpdateBugCommentWithReplyEventSuccess() throws Exception {
// 只编辑回复的评论并且带有@用户
BugCommentEditRequest request = new BugCommentEditRequest();
request.setId("default-bug-comment-id-2");
request.setBugId("default-bug-id-for-comment");
request.setContent("This is a comment && reply test!");
request.setReplyUser("admin");
request.setNotifier(String.join(";", "oasis-user-id1", "oasis-user-id2", "admin"));
request.setEvent(NoticeConstants.Event.REPLY);
BugComment bugComment = saveOrUpdateComment(request, true);
NotificationExample example = new NotificationExample();
example.createCriteria().andResourceIdEqualTo("default-bug-id-for-comment").andResourceTypeEqualTo(NoticeConstants.TaskType.BUG_TASK);
List<Notification> notifications = notificationMapper.selectByExample(example);
// 存在10条通知, 会排除掉当前登录用户
Assertions.assertEquals(10, notifications.size());
BugComment comment = bugCommentMapper.selectByPrimaryKey(bugComment.getId());
Assertions.assertTrue(StringUtils.equals(comment.getContent(), "This is a comment && reply test!"));
}
@Test
@Order(11)
public void testUpdateBugCommentWithAtEventSuccess() throws Exception {
// 编辑第一级并带有@的评论
BugCommentEditRequest request = new BugCommentEditRequest();
request.setId("default-bug-comment-id-3");
request.setBugId("default-bug-id-for-comment");
request.setContent("This is a comment && at test!");
request.setNotifier(String.join(";", "oasis-user-id3", "oasis-user-id4"));
request.setEvent(NoticeConstants.Event.AT);
BugComment bugComment = saveOrUpdateComment(request, true);
NotificationExample example = new NotificationExample();
example.createCriteria().andResourceIdEqualTo("default-bug-id-for-comment").andResourceTypeEqualTo(NoticeConstants.TaskType.BUG_TASK);
List<Notification> notifications = notificationMapper.selectByExample(example);
// 存在12条通知
Assertions.assertEquals(12, notifications.size());
BugComment comment = bugCommentMapper.selectByPrimaryKey(bugComment.getId());
Assertions.assertTrue(StringUtils.equals(comment.getContent(), "This is a comment && at test!"));
}
@Test
@Order(12)
public void testDeleteBugCommentSuccess() throws Exception {
// 删除第一级评论
this.requestGet(BUG_COMMENT_DELETE + "/default-bug-comment-id-1");
BugComment comment = bugCommentMapper.selectByPrimaryKey("default-bug-comment-id-2");
Assertions.assertNull(comment);
// 删除第二级评论
this.requestGet(BUG_COMMENT_DELETE + "/default-bug-comment-id-4");
BugComment comment1 = bugCommentMapper.selectByPrimaryKey("default-bug-comment-id-4");
Assertions.assertNull(comment1);
}
@Test
@Order(13)
public void testDeleteBugCommentError() throws Exception {
// 评论不存在
this.requestGet(BUG_COMMENT_DELETE + "/default-bug-comment-id-x");
}
private BugComment saveOrUpdateComment(BugCommentEditRequest request, boolean isUpdated) throws Exception{
MvcResult mvcResult = this.requestPostWithOkAndReturn(isUpdated ? BUG_COMMENT_UPDATE : BUG_COMMENT_ADD, request);
String sortData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder resultHolder = JSON.parseObject(sortData, ResultHolder.class);
return JSON.parseObject(JSON.toJSONString(resultHolder.getData()), BugComment.class);
}
}

View File

@ -0,0 +1,25 @@
INSERT INTO project (id, num, organization_id, name, description, create_user, update_user, create_time, update_time) VALUE
('default-project-for-bug-comment', null, '100001', '测试项目(缺陷)', '系统默认创建的项目(缺陷)', 'admin', 'admin', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000);
INSERT INTO user(id, name, email, password, create_time, update_time, language, last_organization_id, phone, source, last_project_id, create_user, update_user, deleted) VALUES
('oasis-user-id', 'oasis', 'oasis@test.com', MD5('metersphere'), UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NUll, '', 'LOCAL', NULL, 'admin', 'admin', false),
('oasis-user-id1', 'oasis1', 'oasis1@test.com', MD5('metersphere'), UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NUll, '', 'LOCAL', NULL, 'admin', 'admin', false),
('oasis-user-id2', 'oasis2', 'oasis2@test.com', MD5('metersphere'), UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NUll, '', 'LOCAL', NULL, 'admin', 'admin', false),
('oasis-user-id3', 'oasis3', 'oasis3@test.com', MD5('metersphere'), UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NUll, '', 'LOCAL', NULL, 'admin', 'admin', false),
('oasis-user-id4', 'oasis4', 'oasis4@test.com', MD5('metersphere'), UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NUll, '', 'LOCAL', NULL, 'admin', 'admin', false);
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, tag, platform_bug_id, deleted) VALUE
('default-bug-id-for-comment', 100099, 'default-bug-for-comment', 'oasis', 'oasis', 'oasis-user-id', UNIX_TIMESTAMP() * 1000, 'admin', UNIX_TIMESTAMP() * 1000, 'admin', UNIX_TIMESTAMP() * 1000, '100001100001', 'bug-template-id', 'Local', 'open', 'default-tag', null, 0),
('default-bug-id-for-comment1', 100099, 'default-bug-for-comment', 'oasis', 'oasis', 'oasis-user-id', UNIX_TIMESTAMP() * 1000, 'admin', UNIX_TIMESTAMP() * 1000, 'admin', UNIX_TIMESTAMP() * 1000, '100001100001', 'bug-template-id', 'Local', 'open', 'default-tag', null, 0);
INSERT INTO bug_comment (id, bug_id, reply_user, notifier, parent_id, content, create_user, create_time, update_user, update_time) VALUES
('default-bug-comment-id-1', 'default-bug-id-for-comment', null, null, null, 'This is a test comment!', 'admin', UNIX_TIMESTAMP() * 1000, 'admin', UNIX_TIMESTAMP() * 1000),
('default-bug-comment-id-2', 'default-bug-id-for-comment', 'admin', 'oasis-user-id1;oasis-user-id2', 'default-bug-comment-id-1', 'This is a test comment!', 'oasis-user-id', UNIX_TIMESTAMP() * 1000, 'admin', UNIX_TIMESTAMP() * 1000),
('default-bug-comment-id-3', 'default-bug-id-for-comment', null, null, null, 'This is a test comment!', 'oasis-user-id1', UNIX_TIMESTAMP() * 1000 + 1000, 'admin', UNIX_TIMESTAMP() * 1000),
('default-bug-comment-id-4', 'default-bug-id-for-comment', 'oasis-user-id1', null, 'default-bug-comment-id-3', 'This is a test comment!', 'oasis-user-id2', UNIX_TIMESTAMP() * 1000 + 1000, 'admin', UNIX_TIMESTAMP() * 1000);
INSERT INTO bug_custom_field (bug_id, field_id, value) VALUE ('default-bug-id-for-comment1', 'comment_test_field', '["default", "default-1"]');
INSERT INTO custom_field (id, name, scene, type, remark, internal, scope_type, create_time, update_time, create_user, scope_id) VALUE
('comment_test_field', '测试字段', 'BUG', 'MULTIPLE_SELECT', '', 0, 'PROJECT', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 'admin', '100001100001');

View File

@ -5,10 +5,12 @@ import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
public class OptionDTO { public class OptionDTO implements Serializable {
@Schema(description = "选项ID") @Schema(description = "选项ID")
private String id; private String id;
@Schema(description = "选项名称") @Schema(description = "选项名称")