feat(消息中心): 新增消息中心功能

This commit is contained in:
guoyuqi 2024-03-01 19:12:16 +08:00 committed by 刘瑞斌
parent e9ddb78d89
commit 711d7ce005
10 changed files with 205 additions and 50 deletions

View File

@ -1,15 +1,12 @@
package io.metersphere.project.domain;
import io.metersphere.validation.groups.Created;
import io.metersphere.validation.groups.Updated;
import io.metersphere.validation.groups.*;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
import jakarta.validation.constraints.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import lombok.Data;
@Data
public class Notification implements Serializable {
@ -19,7 +16,7 @@ public class Notification implements Serializable {
@Schema(description = "通知类型", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{notification.type.not_blank}", groups = {Created.class})
@Size(min = 1, max = 30, message = "{notification.type.length_range}", groups = {Created.class, Updated.class})
@Size(min = 1, max = 64, message = "{notification.type.length_range}", groups = {Created.class, Updated.class})
private String type;
@Schema(description = "接收人", requiredMode = Schema.RequiredMode.REQUIRED)
@ -34,7 +31,7 @@ public class Notification implements Serializable {
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{notification.status.not_blank}", groups = {Created.class})
@Size(min = 1, max = 30, message = "{notification.status.length_range}", groups = {Created.class, Updated.class})
@Size(min = 1, max = 64, message = "{notification.status.length_range}", groups = {Created.class, Updated.class})
private String status;
@Schema(description = "创建时间")
@ -57,7 +54,7 @@ public class Notification implements Serializable {
@Schema(description = "资源类型", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{notification.resource_type.not_blank}", groups = {Created.class})
@Size(min = 1, max = 50, message = "{notification.resource_type.length_range}", groups = {Created.class, Updated.class})
@Size(min = 1, max = 64, message = "{notification.resource_type.length_range}", groups = {Created.class, Updated.class})
private String resourceType;
@Schema(description = "资源名称", requiredMode = Schema.RequiredMode.REQUIRED)

View File

@ -295,15 +295,15 @@ CREATE TABLE IF NOT EXISTS message_task_blob
CREATE TABLE IF NOT EXISTS notification(
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID' ,
`type` VARCHAR(30) NOT NULL COMMENT '通知类型' ,
`type` VARCHAR(64) NOT NULL COMMENT '通知类型' ,
`receiver` VARCHAR(50) NOT NULL COMMENT '接收人' ,
`subject` VARCHAR(255) NOT NULL COMMENT '标题' ,
`status` VARCHAR(30) NOT NULL COMMENT '状态' ,
`status` VARCHAR(64) NOT NULL COMMENT '状态' ,
`create_time` BIGINT NOT NULL COMMENT '创建时间' ,
`operator` VARCHAR(50) NOT NULL COMMENT '操作人' ,
`operation` VARCHAR(50) NOT NULL COMMENT '操作' ,
`resource_id` VARCHAR(50) NOT NULL COMMENT '资源ID' ,
`resource_type` VARCHAR(50) NOT NULL COMMENT '资源类型' ,
`resource_type` VARCHAR(64) NOT NULL COMMENT '资源类型' ,
`resource_name` VARCHAR(255) NOT NULL COMMENT '资源名称' ,
`content` TEXT NOT NULL COMMENT '通知内容' ,
PRIMARY KEY (id)
@ -314,11 +314,12 @@ CREATE TABLE IF NOT EXISTS notification(
CREATE INDEX idx_receiver ON notification(receiver);
CREATE INDEX idx_create_time ON notification(create_time desc);
CREATE INDEX idx_create_time ON notification(create_time);
CREATE INDEX idx_type ON notification(type);
CREATE INDEX idx_subject ON notification(subject);
CREATE INDEX idx_resource_id ON notification(resource_id);
CREATE INDEX idx_resource_type ON notification(resource_type);
CREATE INDEX idx_operator ON notification(operator);
CREATE INDEX idx_resource_id ON notification(resource_id);
CREATE TABLE IF NOT EXISTS project_robot(

View File

@ -5,6 +5,7 @@ import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.project.domain.Notification;
import io.metersphere.system.dto.sdk.OptionDTO;
import io.metersphere.system.dto.sdk.request.NotificationRequest;
import io.metersphere.system.service.NotificationService;
import io.metersphere.system.utils.PageUtils;
@ -35,19 +36,19 @@ public class NotificationController {
@GetMapping(value = "/read/{id}")
@Operation(summary = "消息中心-将消息设置为已读")
public Integer read(@PathVariable int id) {
public Integer read(@PathVariable long id) {
return notificationService.read(id, SessionUtils.getUserId());
}
@GetMapping(value = "/read/all")
@Operation(summary = "消息中心-获取消息中心所有已读消息")
@Operation(summary = "消息中心-将消息中心所有信息设置为已读消息")
public Integer readAll() {
return notificationService.readAll(SessionUtils.getUserId());
}
@PostMapping(value = "/count")
@Operation(summary = "消息中心-获取消息中心消息具体类型具体状态的数量")
public Integer countNotification(@RequestBody Notification notification) {
return notificationService.countNotification(notification, SessionUtils.getUserId());
public List<OptionDTO> countNotification(@RequestBody NotificationRequest notificationRequest) {
return notificationService.countNotification(notificationRequest, SessionUtils.getUserId());
}
}

View File

@ -22,6 +22,9 @@ public class NotificationRequest extends BasePageRequest {
@Schema(description = "状态")
private String status;
@Schema(description = "资源类型: TEST_PLAN/BUG/CASE/API/UI/LOAD/JENKINS")
private String resourceType;
@Schema(description = "创建时间")
private Long createTime;

View File

@ -9,8 +9,6 @@ import java.util.List;
public interface BaseNotificationMapper {
List<Notification> listNotification(@Param("notificationRequest") NotificationRequest notificationRequest);
int countNotification(@Param("notification") Notification notification);
List<Notification> listNotification(@Param("request") NotificationRequest notificationRequest);
}

View File

@ -4,27 +4,20 @@
<select id="listNotification" resultType="io.metersphere.project.domain.Notification">
SELECT * FROM notification
WHERE receiver = #{notificationRequest.receiver} AND create_time &gt; (unix_timestamp() - 90 * 24 * 3600) * 1000
<if test='notification.title != null and notification.title != ""'>
AND ( title LIKE #{notificationRequest.title} OR content LIKE #{notificationRequest.title} )
WHERE receiver = #{request.receiver} AND create_time &gt; (unix_timestamp() - 90 * 24 * 3600) * 1000
<if test='request.title != null and request.title != ""'>
AND ( title LIKE #{request.title} OR content LIKE #{request.title} )
</if>
<if test='notification.type != null and notification.type != ""'>
AND type = #{notificationRequest.type}
<if test='request.type != null and request.type != ""'>
AND type = #{request.type}
</if>
<if test='request.status != null and request.status != ""'>
AND status = #{request.status}
</if>
<if test='request.resourceType != null and notificationRequest.resourceType != ""'>
AND resource_type LIKE #{request.resourceType}
</if>
ORDER BY create_time DESC
</select>
<select id="countNotification" resultType="java.lang.Integer">
SELECT COUNT(*) FROM notification
WHERE receiver = #{notification.receiver} AND create_time &gt; (unix_timestamp() - 3600) * 1000
<if test="notification.type != null">
AND type = #{notification.type}
</if>
<if test="notification.status != null">
AND status = #{notification.status}
</if>
</select>
</mapper>

View File

@ -1,14 +1,15 @@
package io.metersphere.system.notice.sender;
import io.metersphere.system.dto.sdk.SessionUser;
import io.metersphere.system.notice.annotation.SendNotice;
import io.metersphere.sdk.util.CommonBeanFactory;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.dto.sdk.SessionUser;
import io.metersphere.system.notice.annotation.SendNotice;
import io.metersphere.system.utils.SessionUtils;
import jakarta.annotation.Resource;
import org.apache.commons.beanutils.BeanMap;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
@ -40,6 +41,8 @@ public class SendNoticeAspect {
private StandardReflectionParameterNameDiscoverer discoverer = new StandardReflectionParameterNameDiscoverer();
private ThreadLocal<String> source = new ThreadLocal<>();
private final static String ID = "id";
@Pointcut("@annotation(io.metersphere.system.notice.annotation.SendNotice)")
public void pointcut() {
}
@ -149,6 +152,11 @@ public class SendNoticeAspect {
SessionUser sessionUser = SessionUtils.getUser();
String currentProjectId = SessionUtils.getCurrentProjectId();
LogUtils.info("event:" + event);
String resultStr = JSON.toJSONString(retValue);
Map object = JSON.parseMap(resultStr);
if (MapUtils.isNotEmpty(object) && object.containsKey(ID)) {
resources.add(object);
}
afterReturningNoticeSendService.sendNotice(taskType, event, resources, sessionUser, currentProjectId);
} catch (Exception e) {
LogUtils.error(e.getMessage(), e);

View File

@ -9,7 +9,6 @@ import io.metersphere.system.notice.Receiver;
import io.metersphere.system.notice.constants.NotificationConstants;
import io.metersphere.system.notice.sender.AbstractNoticeSender;
import io.metersphere.system.service.NotificationService;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
@ -47,7 +46,6 @@ public class InSiteNoticeSender extends AbstractNoticeSender {
Map<String, Object> paramMap = noticeModel.getParamMap();
Notification notification = new Notification();
notification.setId(IDGenerator.nextNum());
notification.setSubject(noticeModel.getSubject());
notification.setOperator(noticeModel.getOperator());
notification.setOperation(noticeModel.getEvent());

View File

@ -5,6 +5,7 @@ package io.metersphere.system.service;
import io.metersphere.project.domain.Notification;
import io.metersphere.project.domain.NotificationExample;
import io.metersphere.project.mapper.NotificationMapper;
import io.metersphere.system.dto.sdk.OptionDTO;
import io.metersphere.system.dto.sdk.request.NotificationRequest;
import io.metersphere.system.mapper.BaseNotificationMapper;
import io.metersphere.system.notice.constants.NotificationConstants;
@ -13,7 +14,11 @@ 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;
@Service
@Transactional(rollbackFor = Exception.class)
@ -25,9 +30,6 @@ public class NotificationService {
private BaseNotificationMapper baseNotificationMapper;
public List<Notification> listNotification(NotificationRequest notificationRequest, String userId) {
if (StringUtils.isNotBlank(notificationRequest.getTitle())) {
notificationRequest.setTitle("%" + notificationRequest.getTitle() + "%");
}
if (StringUtils.isBlank(notificationRequest.getReceiver())) {
notificationRequest.setReceiver(userId);
}
@ -50,11 +52,52 @@ public class NotificationService {
return notificationMapper.updateByExampleSelective(record, example);
}
public int countNotification(Notification notification, String userId) {
if (StringUtils.isBlank(notification.getReceiver())) {
notification.setReceiver(userId);
public List<OptionDTO> countNotification(NotificationRequest notificationRequest, String userId) {
List<OptionDTO>optionDTOS = new ArrayList<>();
if (StringUtils.isBlank(notificationRequest.getReceiver())) {
notificationRequest.setReceiver(userId);
}
return baseNotificationMapper.countNotification(notification);
List<Notification> notifications = baseNotificationMapper.listNotification(notificationRequest);
OptionDTO totalOptionDTO = new OptionDTO();
totalOptionDTO.setId("total");
totalOptionDTO.setName(String.valueOf(notifications.size()));
optionDTOS.add(totalOptionDTO);
buildSourceCount(notifications, optionDTOS);
return optionDTOS;
}
private static void buildSourceCount(List<Notification> notifications, List<OptionDTO> optionDTOS) {
Map<String,Integer>countMap = new HashMap<>();
Map<String, List<Notification>> resourceMap = notifications.stream().collect(Collectors.groupingBy(Notification::getResourceType));
resourceMap.forEach((k,v)->{
if (k.contains("TEST_PLAN")) {
countMap.merge("TEST_PLAN", v.size(), Integer::sum);
}
if (k.contains("BUG")) {
countMap.merge("BUG", v.size(), Integer::sum);
}
if (k.contains("CASE")) {
countMap.merge("CASE", v.size(), Integer::sum);
}
if (k.contains("API")) {
countMap.merge("API", v.size(), Integer::sum);
}
if (k.contains("UI")) {
countMap.merge("UI", v.size(), Integer::sum);
}
if (k.contains("LOAD")) {
countMap.merge("LOAD", v.size(), Integer::sum);
}
if (k.contains("JENKINS")) {
countMap.merge("JENKINS", v.size(), Integer::sum);
}
});
countMap.forEach((k,v)->{
OptionDTO optionDTO = new OptionDTO();
optionDTO.setId("total");
optionDTO.setName(String.valueOf(notifications.size()));
optionDTOS.add(optionDTO);
});
}
public void sendAnnouncement(Notification notification) {

View File

@ -0,0 +1,113 @@
package io.metersphere.system.controller;
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.dto.sdk.OptionDTO;
import io.metersphere.system.dto.sdk.request.NotificationRequest;
import io.metersphere.system.notice.constants.NotificationConstants;
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.test.web.servlet.MvcResult;
import java.nio.charset.StandardCharsets;
import java.util.List;
@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class NotificationControllerTests extends BaseTest {
public static final String NOTIFICATION_LIST_PAGE = "/notification/list/all/page";
public static final String NOTIFICATION_READ = "/notification/read/";
public static final String NOTIFICATION_READ_ALL = "/notification/read/all";
public static final String NOTIFICATION_COUNT = "/notification/count";
@Resource
protected NotificationMapper notificationMapper;
void saveNotice() {
Notification notification = new Notification();
notification.setSubject("功能用例更新通知");
notification.setOperator("admin");
notification.setOperation("UPDATE");
notification.setResourceType("FUNCTIONAL_CASE_TASK");
notification.setResourceName("功能用例导入测4");
notification.setType("SYSTEM_NOTICE");
notification.setStatus(NotificationConstants.Status.UNREAD.name());
notification.setCreateTime(System.currentTimeMillis());
notification.setReceiver("admin");
notification.setResourceId("122334");
notification.setContent("nihao");
notificationMapper.insert(notification);
}
@Test
@Order(1)
void getNotificationSuccess() throws Exception {
saveNotice();
NotificationRequest notificationRequest = new NotificationRequest();
notificationRequest.setPageSize(10);
notificationRequest.setCurrent(1);
notificationRequest.setReceiver("admin");
notificationRequest.setType("SYSTEM_NOTICE");
MvcResult mvcResult = this.requestPostWithOkAndReturn(NOTIFICATION_LIST_PAGE, notificationRequest);
Pager<List<Notification>> tableData = JSON.parseObject(JSON.toJSONString(
JSON.parseObject(mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8), ResultHolder.class).getData()),
Pager.class);
//返回值的页码和当前页码相同
Assertions.assertEquals(tableData.getCurrent(), notificationRequest.getCurrent());
Assertions.assertFalse(tableData.getList().isEmpty());
}
@Test
@Order(2)
void setNotificationReadSuccess() throws Exception {
NotificationExample notificationExample = new NotificationExample();
notificationExample.createCriteria().andStatusEqualTo(NotificationConstants.Status.UNREAD.name());
List<Notification> notifications = notificationMapper.selectByExample(notificationExample);
this.requestGetWithOkAndReturn(NOTIFICATION_READ+notifications.get(0).getId());
notificationExample = new NotificationExample();
notificationExample.createCriteria().andStatusEqualTo(NotificationConstants.Status.READ.name());
List<Notification> readNotifications = notificationMapper.selectByExample(notificationExample);
Assertions.assertFalse(readNotifications.isEmpty());
}
@Test
@Order(3)
void setNotificationReadAll() throws Exception {
saveNotice();
this.requestGetWithOk(NOTIFICATION_READ_ALL);
NotificationExample notificationExample = new NotificationExample();
notificationExample.createCriteria().andStatusEqualTo(NotificationConstants.Status.READ.name());
List<Notification> notifications = notificationMapper.selectByExample(notificationExample);
Assertions.assertFalse(notifications.isEmpty());
}
@Test
@Order(4)
void getNotificationCount() throws Exception {
NotificationRequest notificationRequest = new NotificationRequest();
notificationRequest.setPageSize(10);
notificationRequest.setCurrent(1);
notificationRequest.setReceiver("admin");
notificationRequest.setType("SYSTEM_NOTICE");
MvcResult mvcResult = this.requestPostWithOkAndReturn(NOTIFICATION_COUNT, notificationRequest);
String updateReturnData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder resultHolder = JSON.parseObject(updateReturnData, ResultHolder.class);
List<OptionDTO> optionDTOS = JSON.parseArray(JSON.toJSONString(resultHolder.getData()), OptionDTO.class);
Assertions.assertFalse(optionDTOS.isEmpty());
}
}