feat(消息管理): 增加消息通知

This commit is contained in:
guoyuqi 2023-09-12 18:34:52 +08:00 committed by fit2-zhao
parent 8baec397b8
commit a741feee76
48 changed files with 2304 additions and 8 deletions

View File

@ -135,10 +135,12 @@ public class PermissionConstants {
public static final String PROJECT_API_DEFINITION_READ_ADD_API = "PROJECT_API_DEFINITION:READ+ADD_API"; public static final String PROJECT_API_DEFINITION_READ_ADD_API = "PROJECT_API_DEFINITION:READ+ADD_API";
public static final String PROJECT_API_REPORT_READ_DELETE = "PROJECT_API_REPORT:READ+DELETE"; public static final String PROJECT_API_REPORT_READ_DELETE = "PROJECT_API_REPORT:READ+DELETE";
/*------ start: PROJECT_MESSAGE ------*/
public static final String PROJECT_MESSAGE_READ = "PROJECT_MESSAGE:READ"; public static final String PROJECT_MESSAGE_READ = "PROJECT_MESSAGE:READ";
public static final String PROJECT_MESSAGE_READ_UPDATE = "PROJECT_MESSAGE:READ+UPDATE"; public static final String PROJECT_MESSAGE_READ_UPDATE = "PROJECT_MESSAGE:READ+UPDATE";
public static final String PROJECT_MESSAGE_READ_ADD = "PROJECT_MESSAGE:READ+ADD"; public static final String PROJECT_MESSAGE_READ_ADD = "PROJECT_MESSAGE:READ+ADD";
public static final String PROJECT_MESSAGE_READ_DELETE = "PROJECT_MESSAGE:READ+DELETE"; public static final String PROJECT_MESSAGE_READ_DELETE = "PROJECT_MESSAGE:READ+DELETE";
/*------ end: PROJECT_MESSAGE ------*/
/*------ start: PROJECT_APPLICATION ------*/ /*------ start: PROJECT_APPLICATION ------*/
public static final String PROJECT_APPLICATION_TEST_PLAN_READ = "PROJECT_APPLICATION_TEST_PLAN:READ"; public static final String PROJECT_APPLICATION_TEST_PLAN_READ = "PROJECT_APPLICATION_TEST_PLAN:READ";

View File

@ -0,0 +1,52 @@
package io.metersphere.sdk.controller;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.project.domain.Notification;
import io.metersphere.sdk.dto.request.NotificationRequest;
import io.metersphere.sdk.service.NotificationService;
import io.metersphere.sdk.util.PageUtils;
import io.metersphere.sdk.util.Pager;
import io.metersphere.sdk.util.SessionUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "消息中心")
@RestController
@RequestMapping(value = "notification")
public class NotificationController {
@Resource
private NotificationService notificationService;
@PostMapping(value = "/list/all/page")
@Operation(summary = "消息中心-获取消息中心所有消息列表")
public Pager<List<Notification>> listNotification(@Validated @RequestBody NotificationRequest notificationRequest) {
Page<Object> page = PageHelper.startPage(notificationRequest.getCurrent(), notificationRequest.getPageSize(), true);
return PageUtils.setPageInfo(page, notificationService.listNotification(notificationRequest, SessionUtils.getUserId()));
}
@GetMapping(value = "/read/{id}")
@Operation(summary = "消息中心-将消息设置为已读")
public Integer read(@PathVariable int id) {
return notificationService.read(id, SessionUtils.getUserId());
}
@GetMapping(value = "/read/all")
@Operation(summary = "消息中心-获取消息中心所有已读消息")
public Integer readAll() {
return notificationService.readAll(SessionUtils.getUserId());
}
@PostMapping(value = "/count")
public Integer countNotification(@RequestBody Notification notification) {
return notificationService.countNotification(notification, SessionUtils.getUserId());
}
}

View File

@ -54,4 +54,14 @@ public class ResultHolder {
public static ResultHolder error(int code, String message, Object messageDetail) { public static ResultHolder error(int code, String message, Object messageDetail) {
return new ResultHolder(code, message, messageDetail, null); return new ResultHolder(code, message, messageDetail, null);
} }
/**
* 用于特殊情况比如接口可正常返回http状态码200但是需要页面提示错误信息的情况
* @param code 自定义 code
* @param message 给前端返回的 message
* @return
*/
public static ResultHolder successCodeErrorInfo(int code, String message) {
return new ResultHolder(code, message, null, null);
}
} }

View File

@ -23,6 +23,7 @@ public enum CommonResultCode implements IResultCode {
CUSTOM_FIELD_EXIST(101012, "custom_field.exist"), CUSTOM_FIELD_EXIST(101012, "custom_field.exist"),
TEMPLATE_EXIST(101013, "template.exist"); TEMPLATE_EXIST(101013, "template.exist");
private int code; private int code;
private String message; private String message;

View File

@ -0,0 +1,51 @@
package io.metersphere.sdk.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 jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Data
public class MessageTaskRequest {
@Schema(description = "消息配置所在项目ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{message_task.project_id.not_blank}", groups = {Created.class, Updated.class})
public String projectId;
@Schema(description = "消息配置ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{message_task.id.not_blank}", groups = {Updated.class})
public String id;
@Schema(description = "消息配置功能", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{message_task.taskType.not_blank}", groups = {Created.class, Updated.class})
public String taskType;
@Schema(description = "消息配置场景", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{message_task.event.not_blank}", groups = {Created.class, Updated.class})
public String event;
@Schema(description = "消息配置接收人", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "{message_task.receivers.not_empty}", groups = {Created.class, Updated.class})
private List<String> receiverIds;
@Schema(description = "具体测试的ID")
public String testId;
@Schema(description = "消息配置机器人id")
@NotBlank(message = "{message_task.robotId.not_blank}", groups = {Created.class, Updated.class})
public String robotId;
@Schema(description = "消息配置机器人是否开启")
@NotNull(message = "{message_task.enable.not_blank}", groups = {Created.class, Updated.class})
public Boolean enable;
@Schema(description = "消息配置消息模版")
@NotBlank(message = "{message_task.robotId.not_blank}", groups = {Created.class, Updated.class})
public String template;
}

View File

@ -0,0 +1,34 @@
package io.metersphere.sdk.dto.request;
import io.metersphere.sdk.dto.BasePageRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class NotificationRequest extends BasePageRequest {
@Schema(description = "ID")
private Long id;
@Schema(description = "通知类型")
private String type;
@Schema(description = "接收人")
private String receiver;
@Schema(description = "标题")
private String title;
@Schema(description = "状态")
private String status;
@Schema(description = "创建时间")
private Long createTime;
@Schema(description = "操作人")
private String operator;
@Schema(description = "操作")
private String operation;
}

View File

@ -0,0 +1,16 @@
package io.metersphere.sdk.mapper;
import io.metersphere.project.domain.Notification;
import io.metersphere.sdk.dto.request.NotificationRequest;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface BaseNotificationMapper {
List<Notification> listNotification(@Param("notificationRequest") NotificationRequest notificationRequest);
int countNotification(@Param("notification") Notification notification);
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="io.metersphere.sdk.mapper.BaseNotificationMapper">
<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} )
</if>
<if test='notification.type != null and notification.type != ""'>
AND type = #{notificationRequest.type}
</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

@ -0,0 +1,23 @@
package io.metersphere.sdk.notice;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Data
public class MessageDetail implements Serializable {
private List<String> receiverIds = new ArrayList<>();
private String id;
private String event;
private String taskType;
private String webhook;
private String type;
private String testId;
private Long createTime;
private String template;
private String appKey;
private String appSecret;
}

View File

@ -0,0 +1,59 @@
package io.metersphere.sdk.notice;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
@Data
@Builder
public class NoticeModel implements Serializable {
/**
* 保存 测试id
*/
private String testId;
/**
* 操作人
*/
private String operator;
/**
* 保存状态
*/
private String status;
/**
* Event
*/
private String event;
/**
* 消息主题
*/
private String subject;
/**
* 消息内容
*/
private String context;
/**
* 保存特殊的用户
*/
private List<String> relatedUsers;
/**
* 模版里的参数信息
*/
private Map<String, Object> paramMap;
/**
* 接收人
*/
private List<Receiver> receivers;
/**
* 抄送人
*/
private List<Receiver> recipients;
/**
* 包括自己
*/
private boolean excludeSelf;
}

View File

@ -0,0 +1,13 @@
package io.metersphere.sdk.notice;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@AllArgsConstructor
@EqualsAndHashCode
public class Receiver {
private String userId;
private String type;
}

View File

@ -0,0 +1,40 @@
package io.metersphere.sdk.notice.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SendNotice {
String taskType();
/**
* Event
*/
String event() default "";
/**
* 消息主题
*/
String subject() default "";
/**
* 获取实际值
*/
String target() default "";
/**
* 资源目标
*/
Class<?> targetClass() default Object.class;
/**
* 消息内容
*/
String context() default "";
String successContext() default "";
String failedContext() default "";
}

View File

@ -0,0 +1,73 @@
package io.metersphere.sdk.notice.constants;
public interface NoticeConstants {
interface TaskType {
String JENKINS_TASK = "JENKINS_TASK";
String TEST_PLAN_TASK = "TEST_PLAN_TASK";
String CASE_REVIEW_TASK = "CASE_REVIEW_TASK";
String FUNCTIONAL_CASE_TASK = "FUNCTIONAL_CASE_TASK";
String TRACK_HOME_TASK = "TRACK_HOME_TASK";
String TRACK_REPORT_TASK = "TRACK_REPORT_TASK";
String DEFECT_TASK = "DEFECT_TASK";
String SWAGGER_TASK = "SWAGGER_TASK";
String API_SCENARIO_TASK = "API_SCENARIO_TASK";
String API_DEFINITION_TASK = "API_DEFINITION_TASK";
String API_HOME_TASK = "API_HOME_TASK";
String API_REPORT_TASK = "API_REPORT_TASK";
String LOAD_REPORT_TASK = "LOAD_REPORT_TASK";
String LOAD_TEST_TASK = "LOAD_TEST_TASK";
String UI_SCENARIO_TASK = "UI_SCENARIO_TASK";
String UI_DEFINITION_TASK = "UI_DEFINITION_TASK";
String UI_HOME_TASK = "UI_HOME_TASK";
String UI_REPORT_TASK = "UI_REPORT_TASK";
String ENV_TASK = "ENV_TASK";
}
interface Mode {
String API = "API";
String SCHEDULE = "SCHEDULE";
}
interface Type {
String MAIL = "MAIL";
String IN_SITE = "IN_SITE";
String DING_CUSTOM_ROBOT = "DING_CUSTOM_ROBOT";
String DING_ENTERPRISE_ROBOT = "DING_ENTERPRISE_ROBOT";
String WECOM_ROBOT = "WECOM_ROBOT";
String LARK_ROBOT = "LARK_ROBOT";
String CUSTOM_WEBHOOK_ROBOT = "CUSTOM_WEBHOOK_ROBOT";
}
interface Event {
String EXECUTE_SUCCESSFUL = "EXECUTE_SUCCESSFUL";
String EXECUTE_FAILED = "EXECUTE_FAILED";
String EXECUTE_COMPLETED = "EXECUTE_COMPLETED";
String CREATE = "CREATE";
String UPDATE = "UPDATE";
String DELETE = "DELETE";
String COMPLETE = "COMPLETE";
String REVIEW = "REVIEW";
String CASE_CREATE = "CASE_CREATE";
String CASE_UPDATE = "CASE_UPDATE";
String CASE_DELETE = "CASE_DELETE";
String MOCK_CREATE = "MOCK_CREATE";
String MOCK_UPDATE = "MOCK_UPDATE";
String MOCK_DELETE = "MOCK_DELETE";
String COMMENT = "COMMENT";
String IMPORT = "IMPORT";
String CLOSE_SCHEDULE = "CLOSE_SCHEDULE";
}
interface RelatedUser {
String CREATE_USER = "CREATE_USER";//创建人
String EXECUTOR = "EXECUTOR";//负责人(评审人
String MAINTAINER = "MAINTAINER";//维护人
String FOLLOW_PEOPLE = "FOLLOW_PEOPLE";//关注人
String PROCESSOR = "PROCESSOR";//处理人
}
}

View File

@ -0,0 +1,12 @@
package io.metersphere.sdk.notice.constants;
public class NotificationConstants {
public enum Type {
MENTIONED_ME, SYSTEM_NOTICE
}
public enum Status {
READ, UNREAD
}
}

View File

@ -1,4 +1,282 @@
package io.metersphere.sdk.notice.sender; package io.metersphere.sdk.notice.sender;
public class AbstractNoticeSender {
import io.metersphere.api.domain.*;
import io.metersphere.api.mapper.ApiDefinitionFollowerMapper;
import io.metersphere.api.mapper.ApiScenarioFollowerMapper;
import io.metersphere.functional.domain.*;
import io.metersphere.functional.mapper.CaseReviewFollowerMapper;
import io.metersphere.functional.mapper.FunctionalCaseFollowerMapper;
import io.metersphere.load.domain.LoadTestFollower;
import io.metersphere.load.domain.LoadTestFollowerExample;
import io.metersphere.load.mapper.LoadTestFollowerMapper;
import io.metersphere.plan.domain.TestPlanFollower;
import io.metersphere.plan.domain.TestPlanFollowerExample;
import io.metersphere.plan.mapper.TestPlanFollowerMapper;
import io.metersphere.sdk.notice.MessageDetail;
import io.metersphere.sdk.notice.NoticeModel;
import io.metersphere.sdk.notice.Receiver;
import io.metersphere.sdk.notice.constants.NoticeConstants;
import io.metersphere.sdk.notice.constants.NotificationConstants;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.domain.CustomField;
import io.metersphere.system.domain.User;
import io.metersphere.system.domain.UserExample;
import io.metersphere.system.mapper.CustomFieldMapper;
import io.metersphere.system.mapper.UserMapper;
import jakarta.annotation.Resource;
import org.apache.commons.beanutils.BeanMap;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.text.StringSubstitutor;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public abstract class AbstractNoticeSender implements NoticeSender {
@Resource
private CustomFieldMapper customFieldMapper;
@Resource
private TestPlanFollowerMapper testPlanFollowerMapper;
@Resource
private FunctionalCaseFollowerMapper functionalCaseFollowerMapper;
@Resource
private ApiScenarioFollowerMapper apiScenarioFollowerMapper;
@Resource
private ApiDefinitionFollowerMapper apiDefinitionFollowerMapper;
@Resource
private LoadTestFollowerMapper loadTestFollowerMapper;
@Resource
private CaseReviewFollowerMapper caseReviewFollowerMapper;
@Resource
private UserMapper userMapper;
protected String getContext(MessageDetail messageDetail, NoticeModel noticeModel) {
//处理自定义字段的值
handleCustomFields(noticeModel);
// 处理 userIds 中包含的特殊值
noticeModel.setReceivers(getRealUserIds(messageDetail, noticeModel, messageDetail.getEvent()));
//apiReceiver特殊处理v2接口同步的通知v3这里待讨论
/*String apiSpecialType = (String) noticeModel.getParamMap().get("apiSpecialType");
if (apiSpecialType != null && apiSpecialType.equals("API_SPECIAL")) {
String specialReceivers = (String) noticeModel.getParamMap().get("specialReceivers");
List list = JSON.parseArray(specialReceivers);
if (CollectionUtils.isNotEmpty(list)) {
for (Object o : list) {
noticeModel.getReceivers().add(new Receiver(o.toString(), NotificationConstants.Type.MENTIONED_ME.name()));
}
}
}*/
// 如果配置了模版就直接使用模版
if (StringUtils.isNotBlank(messageDetail.getTemplate())) {
return getContent(messageDetail.getTemplate(), noticeModel.getParamMap());
}
String context = StringUtils.EMPTY;
if (StringUtils.isBlank(context)) {
context = noticeModel.getContext();
}
return getContent(context, noticeModel.getParamMap());
}
private void handleCustomFields(NoticeModel noticeModel) {
if (!noticeModel.getParamMap().containsKey("fields")) {
return;
}
try {
Object customFields = noticeModel.getParamMap().get("fields");
List<Object> fields;
if (customFields instanceof String) {
fields = JSON.parseArray((String) customFields, Object.class);
} else {
fields = (List<Object>) customFields;
}
if (CollectionUtils.isNotEmpty(fields)) {
for (Object o : fields) {
Map jsonObject = new BeanMap(o);
String id = (String) jsonObject.get("id");
CustomField customField = customFieldMapper.selectByPrimaryKey(id);
if (customField == null) {
continue;
}
Object value = jsonObject.get("value");
if (value instanceof String && StringUtils.isNotEmpty((String) value)) {
String v = StringUtils.unwrap((String) value, "\"");
noticeModel.getParamMap().put(customField.getName(), v); // 处理人
if (StringUtils.equals(customField.getName(), "处理人")) {
noticeModel.getParamMap().put(NoticeConstants.RelatedUser.PROCESSOR, v); // 处理人
}
}
}
}
} catch (Exception e) {
LogUtils.error(e);
}
}
protected String getContent(String template, Map<String, Object> context) {
// 处理 null
context.forEach((k, v) -> {
if (v == null) {
context.put(k, StringUtils.EMPTY);
}
});
// 处理时间格式的数据
handleTime(context);
StringSubstitutor sub = new StringSubstitutor(context);
return sub.replace(template);
}
private void handleTime(Map<String, Object> context) {
context.forEach((k, v) -> {
if (StringUtils.endsWithIgnoreCase(k, "Time")) {
try {
String value = v.toString();
long time = Long.parseLong(value);
v = DateFormatUtils.format(time, "yyyy-MM-dd HH:mm:ss");
context.put(k, v);
} catch (Exception ignore) {
}
}
});
}
private List<Receiver> getRealUserIds(MessageDetail messageDetail, NoticeModel noticeModel, String event) {
List<Receiver> toUsers = new ArrayList<>();
Map<String, Object> paramMap = noticeModel.getParamMap();
for (String userId : messageDetail.getReceiverIds()) {
switch (userId) {
case NoticeConstants.RelatedUser.EXECUTOR -> {
if (StringUtils.equals(NoticeConstants.Event.CREATE, event)) {
getRelateUsers(toUsers, paramMap);
}
if (paramMap.containsKey(NoticeConstants.RelatedUser.EXECUTOR)) {
toUsers.add(new Receiver((String) paramMap.get(NoticeConstants.RelatedUser.EXECUTOR), NotificationConstants.Type.SYSTEM_NOTICE.name()));
}
}
case NoticeConstants.RelatedUser.CREATE_USER -> {
String createUser = (String) paramMap.get(NoticeConstants.RelatedUser.CREATE_USER);
if (StringUtils.isNotBlank(createUser)) {
toUsers.add(new Receiver(createUser, NotificationConstants.Type.SYSTEM_NOTICE.name()));
}
}
case NoticeConstants.RelatedUser.MAINTAINER -> {
if (StringUtils.equals(NoticeConstants.Event.COMMENT, event)) {
getRelateUsers(toUsers, paramMap);
if (paramMap.containsKey(NoticeConstants.RelatedUser.MAINTAINER)) {
toUsers.add(new Receiver((String) paramMap.get(NoticeConstants.RelatedUser.MAINTAINER), NotificationConstants.Type.SYSTEM_NOTICE.name()));
}
}
}
case NoticeConstants.RelatedUser.FOLLOW_PEOPLE -> {
try {
List<Receiver> follows = handleFollows(messageDetail, noticeModel);
toUsers.addAll(follows);
} catch (Exception e) {
LogUtils.error("查询关注人失败: ", e);
}
}
case NoticeConstants.RelatedUser.PROCESSOR -> {
Object value = paramMap.get(NoticeConstants.RelatedUser.PROCESSOR); // 处理人
if (!Objects.isNull(value)) {
toUsers.add(new Receiver(value.toString(), NotificationConstants.Type.SYSTEM_NOTICE.name()));
}
}
default -> toUsers.add(new Receiver(userId, NotificationConstants.Type.MENTIONED_ME.name()));
}
}
// 去重复
return toUsers.stream()
.distinct()
.collect(Collectors.toList());
}
private void getRelateUsers(List<Receiver> toUsers, Map<String, Object> paramMap) {
List<String> relatedUsers = (List<String>) paramMap.get("userIds");
if (CollectionUtils.isNotEmpty(relatedUsers)) {
List<Receiver> receivers = relatedUsers.stream()
.map(u -> new Receiver(u, NotificationConstants.Type.SYSTEM_NOTICE.name()))
.toList();
toUsers.addAll(receivers);
}
}
private List<Receiver> handleFollows(MessageDetail messageDetail, NoticeModel noticeModel) {
List<Receiver> receivers = new ArrayList<>();
String id = (String) noticeModel.getParamMap().get("id");
String taskType = messageDetail.getTaskType();
switch (taskType) {
case NoticeConstants.TaskType.TEST_PLAN_TASK:
TestPlanFollowerExample testPlanFollowerExample = new TestPlanFollowerExample();
testPlanFollowerExample.createCriteria().andTestPlanIdEqualTo(id);
List<TestPlanFollower> testPlanFollowers = testPlanFollowerMapper.selectByExample(testPlanFollowerExample);
receivers = testPlanFollowers
.stream()
.map(t -> new Receiver(t.getUserId(), NotificationConstants.Type.SYSTEM_NOTICE.name()))
.collect(Collectors.toList());
break;
case NoticeConstants.TaskType.CASE_REVIEW_TASK:
CaseReviewFollowerExample caseReviewFollowerExample = new CaseReviewFollowerExample();
caseReviewFollowerExample.createCriteria().andReviewIdEqualTo(id);
List<CaseReviewFollower> caseReviewFollowers = caseReviewFollowerMapper.selectByExample(caseReviewFollowerExample);
receivers = caseReviewFollowers
.stream()
.map(t -> new Receiver(t.getUserId(), NotificationConstants.Type.SYSTEM_NOTICE.name()))
.collect(Collectors.toList());
break;
case NoticeConstants.TaskType.API_SCENARIO_TASK:
ApiScenarioFollowerExample apiScenarioFollowerExample = new ApiScenarioFollowerExample();
apiScenarioFollowerExample.createCriteria().andApiScenarioIdEqualTo(id);
List<ApiScenarioFollower> apiScenarioFollowers = apiScenarioFollowerMapper.selectByExample(apiScenarioFollowerExample);
receivers = apiScenarioFollowers
.stream()
.map(t -> new Receiver(t.getUserId(), NotificationConstants.Type.SYSTEM_NOTICE.name()))
.collect(Collectors.toList());
break;
case NoticeConstants.TaskType.API_DEFINITION_TASK:
ApiDefinitionFollowerExample apiDefinitionFollowerExample = new ApiDefinitionFollowerExample();
apiDefinitionFollowerExample.createCriteria().andApiDefinitionIdEqualTo(id);
List<ApiDefinitionFollower> apiDefinitionFollowers= apiDefinitionFollowerMapper.selectByExample(apiDefinitionFollowerExample);
receivers = apiDefinitionFollowers
.stream()
.map(t -> new Receiver(t.getUserId(), NotificationConstants.Type.SYSTEM_NOTICE.name()))
.collect(Collectors.toList());
break;
case NoticeConstants.TaskType.LOAD_TEST_TASK:
LoadTestFollowerExample loadTestFollowerExample = new LoadTestFollowerExample();
loadTestFollowerExample.createCriteria().andTestIdEqualTo(id);
List<LoadTestFollower> loadTestFollowers= loadTestFollowerMapper.selectByExample(loadTestFollowerExample);
receivers = loadTestFollowers
.stream()
.map(t -> new Receiver(t.getUserId(), NotificationConstants.Type.SYSTEM_NOTICE.name()))
.collect(Collectors.toList());
break;
case NoticeConstants.TaskType.FUNCTIONAL_CASE_TASK:
FunctionalCaseFollowerExample functionalCaseFollowerExample = new FunctionalCaseFollowerExample();
functionalCaseFollowerExample.createCriteria().andCaseIdEqualTo(id);
List<FunctionalCaseFollower> functionalCaseFollowers = functionalCaseFollowerMapper.selectByExample(functionalCaseFollowerExample);
receivers = functionalCaseFollowers
.stream()
.map(t -> new Receiver(t.getUserId(), NotificationConstants.Type.SYSTEM_NOTICE.name()))
.collect(Collectors.toList());
break;
default:
break;
}
LogUtils.info("FOLLOW_PEOPLE: {}", receivers);
return receivers;
}
protected List<User> getUsers(List<String> userIds) {
UserExample userExample = new UserExample();
userExample.createCriteria().andIdIn(userIds);
return userMapper.selectByExample(userExample);
}
} }

View File

@ -0,0 +1,105 @@
package io.metersphere.sdk.notice.sender;
import io.metersphere.sdk.dto.BaseSystemConfigDTO;
import io.metersphere.sdk.dto.SessionUser;
import io.metersphere.sdk.notice.annotation.SendNotice;
import io.metersphere.sdk.notice.constants.NoticeConstants;
import io.metersphere.sdk.notice.NoticeModel;
import io.metersphere.sdk.service.NoticeSendService;
import io.metersphere.sdk.service.SystemParameterService;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
public class AfterReturningNoticeSendService {
@Resource
private SystemParameterService systemParameterService;
@Resource
private NoticeSendService noticeSendService;
@Async
public void sendNotice(SendNotice sendNotice, List<Map> resources, SessionUser sessionUser, String currentProjectId) {
// 有批量操作发送多次
BaseSystemConfigDTO baseSystemConfigDTO = systemParameterService.getBaseInfo();
for (Map resource : resources) {
Map paramMap = new HashMap<>();
paramMap.put("url", baseSystemConfigDTO.getUrl());
paramMap.put("operator", sessionUser.getName());
paramMap.putAll(resource);
paramMap.putIfAbsent("projectId", currentProjectId);
// 占位符
handleDefaultValues(paramMap);
String context = getContext(sendNotice, paramMap);
NoticeModel noticeModel = NoticeModel.builder()
.operator(sessionUser.getId())
.context(context)
.subject(sendNotice.subject())
.paramMap(paramMap)
.event(sendNotice.event())
.status((String) paramMap.get("status"))
.excludeSelf(true)
.build();
noticeSendService.send(sendNotice.taskType(), noticeModel);
}
}
/**
* 有些默认的值避免通知里出现 ${key}
*/
private void handleDefaultValues(Map paramMap) {
paramMap.put("planShareUrl", StringUtils.EMPTY); // 占位符
}
private String getContext(SendNotice sendNotice, Map<String, Object> paramMap) {
String operation = "";
switch (sendNotice.event()) {
case NoticeConstants.Event.CREATE:
case NoticeConstants.Event.CASE_CREATE:
case NoticeConstants.Event.MOCK_CREATE:
operation = "创建了";
break;
case NoticeConstants.Event.UPDATE:
case NoticeConstants.Event.CASE_UPDATE:
case NoticeConstants.Event.MOCK_UPDATE:
operation = "更新了";
break;
case NoticeConstants.Event.DELETE:
case NoticeConstants.Event.CASE_DELETE:
case NoticeConstants.Event.MOCK_DELETE:
operation = "删除了";
break;
case NoticeConstants.Event.COMMENT:
operation = "评论了";
break;
case NoticeConstants.Event.COMPLETE:
operation = "完成了";
break;
case NoticeConstants.Event.CLOSE_SCHEDULE:
operation = "关闭了定时任务";
break;
default:
break;
}
String subject = sendNotice.subject();
String resource = StringUtils.removeEnd(subject, "通知");
String name = "";
if (paramMap.containsKey("name")) {
name = ": ${name}";
}
if (paramMap.containsKey("title")) {
name = ": ${title}";
}
return "${operator}" + operation + resource + name;
}
}

View File

@ -0,0 +1,9 @@
package io.metersphere.sdk.notice.sender;
import io.metersphere.sdk.notice.MessageDetail;
import io.metersphere.sdk.notice.NoticeModel;
public interface NoticeSender {
void send(MessageDetail messageDetail, NoticeModel noticeModel);
}

View File

@ -0,0 +1,137 @@
package io.metersphere.sdk.notice.sender;
import io.metersphere.sdk.dto.SessionUser;
import io.metersphere.sdk.notice.annotation.SendNotice;
import io.metersphere.sdk.util.CommonBeanFactory;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.SessionUtils;
import jakarta.annotation.Resource;
import org.apache.commons.beanutils.BeanMap;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Aspect
@Component
public class SendNoticeAspect {
@Resource
private AfterReturningNoticeSendService afterReturningNoticeSendService;
private ExpressionParser parser = new SpelExpressionParser();
private StandardReflectionParameterNameDiscoverer discoverer = new StandardReflectionParameterNameDiscoverer();
private ThreadLocal<String> source = new ThreadLocal<>();
@Pointcut("@annotation(io.metersphere.sdk.notice.annotation.SendNotice)")
public void pointcut() {
}
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
try {
//从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
//获取参数对象数组
Object[] args = joinPoint.getArgs();
SendNotice sendNotice = method.getAnnotation(SendNotice.class);
if (StringUtils.isNotEmpty(sendNotice.target())) {
// 操作内容
//获取方法参数名
String[] params = discoverer.getParameterNames(method);
//将参数纳入Spring管理
EvaluationContext context = new StandardEvaluationContext();
for (int len = 0; len < params.length; len++) {
context.setVariable(params[len], args[len]);
}
context.setVariable("targetClass", CommonBeanFactory.getBean(sendNotice.targetClass()));
String target = sendNotice.target();
Expression titleExp = parser.parseExpression(target);
Object v = titleExp.getValue(context, Object.class);
source.set(JSON.toJSONString(v));
}
} catch (Exception e) {
LogUtils.error(e.getMessage(), e);
}
}
@AfterReturning(value = "pointcut()", returning = "retValue")
public void sendNotice(JoinPoint joinPoint, Object retValue) {
try {
//从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
//获取参数对象数组
Object[] args = joinPoint.getArgs();
//获取方法参数名
String[] params = discoverer.getParameterNames(method);
//获取操作
SendNotice sendNotice = method.getAnnotation(SendNotice.class);
// 再次从数据库查询一次内容方便获取最新参数
if (StringUtils.isNotEmpty(sendNotice.target())) {
//将参数纳入Spring管理
EvaluationContext context = new StandardEvaluationContext();
for (int len = 0; len < params.length; len++) {
context.setVariable(params[len], args[len]);
}
context.setVariable("targetClass", CommonBeanFactory.getBean(sendNotice.targetClass()));
String target = sendNotice.target();
Expression titleExp = parser.parseExpression(target);
Object v = titleExp.getValue(context, Object.class);
// 查询结果如果是null或者是{}不使用这个值
String jsonObject = JSON.toJSONString(v);
if (v != null && !StringUtils.equals("{}", jsonObject) && !StringUtils.equals("[]", jsonObject)) {
source.set(JSON.toJSONString(v));
}
}
List<Map> resources = new ArrayList<>();
String v = source.get();
if (StringUtils.isNotBlank(v)) {
// array
if (StringUtils.startsWith(v, "[")) {
resources.addAll(JSON.parseArray(v, Map.class));
}
// map
else {
Map<?, ?> value = JSON.parseObject(v, Map.class);
resources.add(value);
}
} else {
resources.add(new BeanMap(retValue));
}
SessionUser sessionUser = SessionUtils.getUser();
String currentProjectId = SessionUtils.getCurrentProjectId();
afterReturningNoticeSendService.sendNotice(sendNotice, resources, sessionUser, currentProjectId);
} catch (Exception e) {
LogUtils.error(e.getMessage(), e);
} finally {
source.remove();
}
}
}

View File

@ -0,0 +1,38 @@
package io.metersphere.sdk.notice.sender.impl;
import io.metersphere.sdk.notice.MessageDetail;
import io.metersphere.sdk.notice.NoticeModel;
import io.metersphere.sdk.notice.Receiver;
import io.metersphere.sdk.notice.sender.AbstractNoticeSender;
import io.metersphere.sdk.notice.utils.DingClient;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.domain.User;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class DingCustomNoticeSender extends AbstractNoticeSender {
public void sendDingCustom(MessageDetail messageDetail, NoticeModel noticeModel, String context) {
List<String> userIds = noticeModel.getReceivers().stream()
.map(Receiver::getUserId)
.distinct()
.collect(Collectors.toList());
List<User> users = super.getUsers(userIds);
List<String> mobileList = users.stream().map(User::getPhone).toList();
LogUtils.info("钉钉自定义机器人收件人: {}", userIds);
context += StringUtils.join(mobileList, StringUtils.SPACE);
DingClient.send(messageDetail.getWebhook(), "消息通知: \n" + context, mobileList);
}
@Override
public void send(MessageDetail messageDetail, NoticeModel noticeModel) {
String context = super.getContext(messageDetail, noticeModel);
sendDingCustom(messageDetail, noticeModel, context);
}
}

View File

@ -0,0 +1,101 @@
package io.metersphere.sdk.notice.sender.impl;
import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenResponse;
import com.aliyun.dingtalkrobot_1_0.Client;
import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendHeaders;
import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendRequest;
import com.aliyun.tea.TeaException;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.Common;
import com.aliyun.teautil.models.RuntimeOptions;
import io.metersphere.sdk.notice.MessageDetail;
import io.metersphere.sdk.notice.NoticeModel;
import io.metersphere.sdk.notice.sender.AbstractNoticeSender;
import io.metersphere.sdk.util.LogUtils;
import org.springframework.stereotype.Component;
@Component
public class DingEnterPriseNoticeSender extends AbstractNoticeSender {
public void sendDing(MessageDetail messageDetail, NoticeModel noticeModel, String context) throws Exception {
Client client = DingEnterPriseNoticeSender.createClient();
GetAccessTokenResponse accessToken = getAccessToken(messageDetail.getAppKey(), messageDetail.getAppSecret());
OrgGroupSendHeaders orgGroupSendHeaders = new OrgGroupSendHeaders();
orgGroupSendHeaders.xAcsDingtalkAccessToken = accessToken.getBody().accessToken;
String tokenIndex = getTokenIndex(messageDetail.getWebhook());
OrgGroupSendRequest orgGroupSendRequest = new OrgGroupSendRequest()
.setMsgParam("{\"content\":\""+context+"\"}")
.setMsgKey("sampleText")
.setToken(tokenIndex);
try {
client.orgGroupSendWithOptions(orgGroupSendRequest, orgGroupSendHeaders, new RuntimeOptions());
} catch (TeaException err) {
if (!Common.empty(err.code) && !Common.empty(err.message)) {
LogUtils.error(err.message);
}
} catch (Exception error) {
TeaException err = new TeaException(error.getMessage(), error);
if (!Common.empty(err.code) && !Common.empty(err.message)) {
// err 中含有 code message 属性可帮助开发定位问题
LogUtils.error(err.message);
}
}
}
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
private static Client createClient() throws Exception {
Config config = new Config();
config.protocol = "https";
config.regionId = "central";
return new Client(config);
}
private static com.aliyun.dingtalkoauth2_1_0.Client createAuthClient() throws Exception {
Config config = new Config();
config.protocol = "https";
config.regionId = "central";
return new com.aliyun.dingtalkoauth2_1_0.Client(config);
}
private String getTokenIndex(String webhook) {
int tokenIndex = webhook.indexOf("=");
return webhook.substring(tokenIndex+ 1);
}
/**
* 通过机器人标识获取企业内部机器人的accessToken
* @param appKey
* @param appSecret
* @return
* @throws Exception
*/
private GetAccessTokenResponse getAccessToken(String appKey, String appSecret) throws Exception {
com.aliyun.dingtalkoauth2_1_0.Client authClient = DingEnterPriseNoticeSender.createAuthClient();
com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest getAccessTokenRequest = new com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest()
.setAppKey(appKey)
.setAppSecret(appSecret);
return authClient.getAccessToken(getAccessTokenRequest);
}
@Override
public void send(MessageDetail messageDetail, NoticeModel noticeModel) {
String context = super.getContext(messageDetail, noticeModel);
try {
sendDing(messageDetail, noticeModel, context);
LogUtils.debug("发送钉钉内部机器人结束");
} catch (Exception e) {
LogUtils.error(e);
}
}
}

View File

@ -0,0 +1,66 @@
package io.metersphere.sdk.notice.sender.impl;
import io.metersphere.project.domain.Notification;
import io.metersphere.sdk.notice.constants.NotificationConstants;
import io.metersphere.sdk.notice.MessageDetail;
import io.metersphere.sdk.notice.NoticeModel;
import io.metersphere.sdk.notice.Receiver;
import io.metersphere.sdk.notice.sender.AbstractNoticeSender;
import io.metersphere.sdk.service.NotificationService;
import io.metersphere.sdk.util.LogUtils;
import jakarta.annotation.Resource;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Component
public class InSiteNoticeSender extends AbstractNoticeSender {
@Resource
private NotificationService notificationService;
public void sendAnnouncement(MessageDetail messageDetail, NoticeModel noticeModel, String context) {
List<Receiver> receivers = noticeModel.getReceivers();
// 排除自己
if (noticeModel.isExcludeSelf()) {
receivers.removeIf(u -> StringUtils.equals(u.getUserId(), noticeModel.getOperator()));
}
if (CollectionUtils.isEmpty(receivers)) {
return;
}
LogUtils.info("发送站内通知: {}", receivers);
receivers.forEach(receiver -> {
Map<String, Object> paramMap = noticeModel.getParamMap();
Notification notification = new Notification();
notification.setTitle(noticeModel.getSubject());
notification.setOperator(noticeModel.getOperator());
notification.setOperation(noticeModel.getEvent());
notification.setResourceId((String) paramMap.get("id"));
notification.setResourceType(messageDetail.getTaskType());
if (paramMap.get("name") != null) {
notification.setResourceName((String) paramMap.get("name"));
}
if (paramMap.get("title") != null) {
notification.setResourceName((String) paramMap.get("title"));
}
notification.setType(receiver.getType());
notification.setStatus(NotificationConstants.Status.UNREAD.name());
notification.setCreateTime(System.currentTimeMillis());
notification.setReceiver(receiver.getUserId());
notificationService.sendAnnouncement(notification);
});
}
@Override
public void send(MessageDetail messageDetail, NoticeModel noticeModel) {
String context = super.getContext(messageDetail, noticeModel);
sendAnnouncement(messageDetail, noticeModel, context);
}
}

View File

@ -0,0 +1,39 @@
package io.metersphere.sdk.notice.sender.impl;
import io.metersphere.sdk.notice.Receiver;
import io.metersphere.sdk.notice.MessageDetail;
import io.metersphere.sdk.notice.NoticeModel;
import io.metersphere.sdk.notice.sender.AbstractNoticeSender;
import io.metersphere.sdk.notice.utils.LarkClient;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.domain.User;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class LarkNoticeSender extends AbstractNoticeSender {
public void sendLark(MessageDetail messageDetail, NoticeModel noticeModel, String context) {
List<String> userIds = noticeModel.getReceivers().stream()
.map(Receiver::getUserId)
.distinct()
.collect(Collectors.toList());
List<User> users = super.getUsers(userIds);
List<String> collect = users.stream()
.map(ud -> "<at email=\"" + ud.getEmail() + "\">" + ud.getName() + "</at>")
.toList();
LogUtils.info("飞书收件人: {}", userIds);
context += StringUtils.join(collect, StringUtils.SPACE);
LarkClient.send(messageDetail.getWebhook(), "消息通知: \n" + context);
}
@Override
public void send(MessageDetail messageDetail, NoticeModel noticeModel) {
String context = super.getContext(messageDetail, noticeModel);
sendLark(messageDetail, noticeModel, context);
}
}

View File

@ -2,18 +2,166 @@ package io.metersphere.sdk.notice.sender.impl;
import io.metersphere.sdk.constants.ParamConstants; import io.metersphere.sdk.constants.ParamConstants;
import io.metersphere.sdk.notice.MessageDetail;
import io.metersphere.sdk.notice.NoticeModel;
import io.metersphere.sdk.notice.sender.AbstractNoticeSender; import io.metersphere.sdk.notice.sender.AbstractNoticeSender;
import io.metersphere.sdk.util.EncryptUtils;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.domain.SystemParameter;
import io.metersphere.system.domain.SystemParameterExample;
import io.metersphere.system.domain.User;
import io.metersphere.system.mapper.SystemParameterMapper;
import jakarta.annotation.Resource;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import io.metersphere.sdk.notice.Receiver;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.*;
import java.util.Properties; import java.util.stream.Collectors;
@Component @Component
public class MailNoticeSender extends AbstractNoticeSender { public class MailNoticeSender extends AbstractNoticeSender {
@Resource
private SystemParameterMapper systemParameterMapper;
public void sendMail(String context, NoticeModel noticeModel) throws Exception {
List<String> userIds = noticeModel.getReceivers().stream()
.map(Receiver::getUserId)
.distinct()
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(userIds)) {
return;
}
String[] users = super.getUsers(userIds).stream()
.map(User::getEmail)
.distinct()
.toArray(String[]::new);
send(noticeModel.getSubject(), context, users, new String[0]);
}
private void send(String subject, String context, String[] users, String[] cc) throws Exception {
LogUtils.debug("发送邮件开始 ");
SystemParameterExample example = new SystemParameterExample();
example.createCriteria().andParamKeyLike(ParamConstants.Classify.MAIL.getValue() + "%");
List<SystemParameter> paramList = systemParameterMapper.selectByExample(example);
Map<String, String> paramMap = paramList.stream().collect(Collectors.toMap(SystemParameter::getParamKey, p -> {
if (StringUtils.equals(p.getParamKey(), ParamConstants.MAIL.PASSWORD.getValue())) {
return EncryptUtils.aesDecrypt(p.getParamValue()).toString();
}
if (StringUtils.isEmpty(p.getParamValue())) {
return "";
} else {
return p.getParamValue();
}
}));
JavaMailSenderImpl javaMailSender = getMailSender(paramMap);
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
String username = javaMailSender.getUsername();
String email;
if (username.contains("@")) {
email = username;
} else {
String mailHost = javaMailSender.getHost();
String domainName = mailHost.substring(mailHost.indexOf(".") + 1);
email = username + "@" + domainName;
}
InternetAddress from = new InternetAddress();
String smtpFrom = paramMap.get(ParamConstants.MAIL.FROM.getValue());
if (StringUtils.isBlank(smtpFrom)) {
from.setAddress(email);
from.setPersonal(username);
} else {
// 指定发件人后address 应该是邮件服务器验证过的发件人
if (smtpFrom.contains("@")) {
from.setAddress(smtpFrom);
} else {
from.setAddress(email);
}
from.setPersonal(smtpFrom);
}
helper.setFrom(from);
LogUtils.debug("发件人地址" + javaMailSender.getUsername());
LogUtils.debug("helper" + helper);
helper.setSubject("MeterSphere " + subject);
LogUtils.info("收件人地址: {}", Arrays.asList(users));
helper.setText(context, true);
// 有抄送
if (cc != null && cc.length > 0) {
//设置抄送人 CCCarbon Copy
helper.setCc(cc);
// to 参数表示收件人
helper.setTo(users);
javaMailSender.send(mimeMessage);
}
// 无抄送
else {
for (String u : users) {
helper.setTo(u);
try {
javaMailSender.send(mimeMessage);
} catch (Exception e) {
LogUtils.error("发送邮件失败: ", e);
}
}
}
}
public void sendExternalMail(String context, NoticeModel noticeModel) throws Exception {
List<String> userIds = noticeModel.getReceivers().stream()
.map(Receiver::getUserId)
.distinct()
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(userIds)) {
return;
}
List<String> recipients = new ArrayList<>();
if (CollectionUtils.isNotEmpty(noticeModel.getRecipients())) {
recipients = noticeModel.getRecipients().stream()
.map(Receiver::getUserId)
.distinct()
.collect(Collectors.toList());
}
String[] users = userIds.stream()
.distinct()
.toArray(String[]::new);
String[] ccArr = new String[0];
if (CollectionUtils.isNotEmpty(recipients)) {
ccArr = recipients.stream()
.distinct()
.toArray(String[]::new);
}
send(noticeModel.getSubject(), context, users, ccArr);
}
@Override
public void send(MessageDetail messageDetail, NoticeModel noticeModel) {
String context = super.getContext(messageDetail, noticeModel);
try {
sendMail(context, noticeModel);
LogUtils.debug("发送邮件结束");
} catch (Exception e) {
LogUtils.error(e);
}
}
public JavaMailSenderImpl getMailSender(Map<String, String> paramMap) { public JavaMailSenderImpl getMailSender(Map<String, String> paramMap) {
Properties props = new Properties(); Properties props = new Properties();
@ -42,4 +190,5 @@ public class MailNoticeSender extends AbstractNoticeSender {
javaMailSender.setJavaMailProperties(props); javaMailSender.setJavaMailProperties(props);
return javaMailSender; return javaMailSender;
} }
} }

View File

@ -0,0 +1,37 @@
package io.metersphere.sdk.notice.sender.impl;
import io.metersphere.sdk.notice.MessageDetail;
import io.metersphere.sdk.notice.NoticeModel;
import io.metersphere.sdk.notice.Receiver;
import io.metersphere.sdk.notice.sender.AbstractNoticeSender;
import io.metersphere.sdk.notice.utils.WeComClient;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.domain.User;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class WeComNoticeSender extends AbstractNoticeSender {
public void sendWeCom(MessageDetail messageDetail, NoticeModel noticeModel, String context) {
List<String> userIds = noticeModel.getReceivers().stream()
.map(Receiver::getUserId)
.distinct()
.collect(Collectors.toList());
List<User> users = super.getUsers(userIds);
List<String> mobileList = users.stream().map(User::getPhone).toList();
LogUtils.info("企业微信收件人: {}", userIds);
context += StringUtils.join(mobileList, StringUtils.SPACE);
WeComClient.send(messageDetail.getWebhook(), "消息通知: \n" + context, mobileList);
}
@Override
public void send(MessageDetail messageDetail, NoticeModel noticeModel) {
String context = super.getContext(messageDetail, noticeModel);
sendWeCom(messageDetail, noticeModel, context);
}
}

View File

@ -0,0 +1,66 @@
package io.metersphere.sdk.notice.sender.impl;
import io.metersphere.sdk.notice.MessageDetail;
import io.metersphere.sdk.notice.NoticeModel;
import io.metersphere.sdk.notice.Receiver;
import io.metersphere.sdk.notice.sender.AbstractNoticeSender;
import io.metersphere.sdk.util.LogUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class WebhookNoticeSender extends AbstractNoticeSender {
private void send(MessageDetail messageDetail, NoticeModel noticeModel, String context) {
List<Receiver> receivers = noticeModel.getReceivers();
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
if (CollectionUtils.isNotEmpty(receivers)) {
List<String> userIds = receivers.stream()
.map(Receiver::getUserId)
.distinct()
.collect(Collectors.toList());
LogUtils.info("Webhook收件人: {}", userIds);
}
try {
HttpPost httpPost = new HttpPost(messageDetail.getWebhook());
// 创建请求内容
StringEntity entity = new StringEntity(context, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);
// 执行http请求
response = httpClient.execute(httpPost);
} catch (Exception e) {
LogUtils.error(e.getMessage(), e);
} finally {
try {
if (response != null) {
response.close();
}
} catch (IOException e) {
LogUtils.error(e);
}
}
}
@Override
public void send(MessageDetail messageDetail, NoticeModel noticeModel) {
String context = super.getContext(messageDetail, noticeModel);
send(messageDetail, noticeModel, context);
}
}

View File

@ -0,0 +1,37 @@
package io.metersphere.sdk.notice.utils;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.LogUtils;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.io.entity.StringEntity;
import java.io.IOException;
import java.util.Map;
public class ClientPost {
public static void executeClient(String webhook, CloseableHttpClient httpClient, Map<String, Object> mp) {
CloseableHttpResponse response = null;
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(webhook);
// 创建请求内容
StringEntity entity = new StringEntity(JSON.toJSONString(mp), ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);
// 执行http请求
response = httpClient.execute(httpPost);
} catch (Exception e) {
LogUtils.error(e);
} finally {
try {
response.close();
} catch (IOException e) {
LogUtils.error(e);
}
}
}
}

View File

@ -0,0 +1,24 @@
package io.metersphere.sdk.notice.utils;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import java.util.*;
public class DingClient {
public static void send(String webhook, String context, List<String>mobileList) {
CloseableHttpClient httpClient = HttpClients.createDefault();
Map<String, Object> mp = new LinkedHashMap<>();
Map<String, Object> js = new HashMap<>();
Map<String, Object> at = new HashMap<>();
js.put("content", context);
at.put("atMobiles",mobileList);
at.put("isAtAll",false);
mp.put("msgtype", "text");
mp.put("text", js);
mp.put("at", at);
ClientPost.executeClient(webhook, httpClient, mp);
}
}

View File

@ -0,0 +1,19 @@
package io.metersphere.sdk.notice.utils;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import java.util.*;
public class LarkClient {
public static void send(String webhook, String context) {
CloseableHttpClient httpClient = HttpClients.createDefault();
Map<String, Object> mp = new LinkedHashMap<>();
Map<String, String> js = new HashMap<>();
js.put("text", context);
mp.put("msg_type", "text");
mp.put("content", js);
ClientPost.executeClient(webhook, httpClient, mp);
}
}

View File

@ -0,0 +1,21 @@
package io.metersphere.sdk.notice.utils;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import java.util.*;
public class WeComClient {
public static void send(String webhook, String context, List<String>mobileList) {
CloseableHttpClient httpClient = HttpClients.createDefault();
Map<String, Object> mp = new LinkedHashMap<>();
Map<String, Object> js = new HashMap<>();
js.put("content", context);
js.put("mentioned_mobile_list", mobileList);
mp.put("msgtype", "text");
mp.put("text", js);
ClientPost.executeClient(webhook, httpClient, mp);
}
}

View File

@ -0,0 +1,129 @@
package io.metersphere.sdk.service;
import io.metersphere.project.domain.*;
import io.metersphere.project.mapper.MessageTaskBlobMapper;
import io.metersphere.project.mapper.MessageTaskMapper;
import io.metersphere.project.mapper.ProjectRobotMapper;
import io.metersphere.sdk.notice.MessageDetail;
import io.metersphere.sdk.util.LogUtils;
import jakarta.annotation.Resource;
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;
/**
* @author guoyuqi
*/
@Service
@Transactional(rollbackFor = Exception.class)
public class MessageDetailService {
@Resource
private MessageTaskMapper messageTaskMapper;
@Resource
private MessageTaskBlobMapper messageTaskBlobMapper;
@Resource
private ProjectRobotMapper projectRobotMapper;
/**
* 获取唯一的消息任务
* @param taskType 任务类型
* @param projectId 项目ID
* @return List<MessageDetail>list
*/
public List<MessageDetail> searchMessageByTypeAndProjectId(String taskType, String projectId) {
try {
return getMessageDetails(taskType, projectId);
} catch (Exception e) {
LogUtils.error(e.getMessage(), e);
return new ArrayList<>();
}
}
private List<MessageDetail> getMessageDetails(String type, String projectId) {
List<MessageDetail> messageDetails = new ArrayList<>();
//根据项目id找到所有开启的消息通知任务 以及模版配置机器人配置
MessageTaskExample example = new MessageTaskExample();
example.createCriteria()
.andTaskTypeEqualTo(type)
.andProjectIdEqualTo(projectId).andEnableEqualTo(true);
example.setOrderByClause("create_time asc");
List<MessageTask> messageTaskLists = messageTaskMapper.selectByExample(example);
getMessageDetails(messageDetails, messageTaskLists);
return messageDetails.stream()
.sorted(Comparator.comparing(MessageDetail::getCreateTime, Comparator.nullsLast(Long::compareTo)).reversed())
.toList()
.stream()
.distinct()
.collect(Collectors.toList());
}
private void getMessageDetails(List<MessageDetail> messageDetails, List<MessageTask> messageTaskLists) {
List<String> messageTaskIds = messageTaskLists.stream().map(MessageTask::getId).toList();
MessageTaskBlobExample blobExample = new MessageTaskBlobExample();
blobExample.createCriteria()
.andIdIn(messageTaskIds);
List<MessageTaskBlob> messageTaskBlobs = messageTaskBlobMapper.selectByExample(blobExample);
Map<String, MessageTaskBlob> messageTaskBlobMap = messageTaskBlobs.stream().collect(Collectors.toMap(MessageTaskBlob::getId, item -> item));
List<String> robotIds = messageTaskLists.stream().map(MessageTask::getProjectRobotId).distinct().toList();
ProjectRobotExample projectRobotExample = new ProjectRobotExample();
projectRobotExample.createCriteria().andIdIn(robotIds).andEnableEqualTo(true);
List<ProjectRobot> projectRobots = projectRobotMapper.selectByExample(projectRobotExample);
Map<String, ProjectRobot> projectRobotMap = projectRobots.stream().collect(Collectors.toMap(ProjectRobot::getId, item -> item));
//消息通知任务以消息类型事件机器人唯一进行分组
Map<String, List<MessageTask>> messageTaskGroup = messageTaskLists.stream().collect(Collectors.groupingBy(t -> (t.getTaskType() + t.getEvent() + t.getProjectRobotId())));
messageTaskGroup.forEach((messageTaskId, messageTaskList) -> {
//获取同一任务所有的接收人
List<String> receivers = messageTaskList.stream().map(MessageTask::getReceiver).collect(Collectors.toList());
MessageDetail messageDetail = new MessageDetail();
MessageTask messageTask = messageTaskList.get(0);
messageDetail.setReceiverIds(receivers);
messageDetail.setTaskType(messageTask.getTaskType());
messageDetail.setEvent(messageTask.getEvent());
messageDetail.setCreateTime(messageTask.getCreateTime());
String projectRobotId = messageTask.getProjectRobotId();
ProjectRobot projectRobot = projectRobotMap.get(projectRobotId);
//如果当前机器人停止那么当前任务也失效
if (projectRobot == null) {
return;
}
messageDetail.setType(projectRobot.getPlatform());
messageDetail.setWebhook(projectRobot.getWebhook());
if (StringUtils.isNotBlank(messageTask.getTestId())) {
messageDetail.setTestId(messageTask.getTestId());
}
if (StringUtils.isNotBlank(projectRobot.getAppKey())) {
messageDetail.setAppKey(projectRobot.getAppKey());
}
if (StringUtils.isNotBlank(projectRobot.getAppSecret())) {
messageDetail.setAppSecret(projectRobot.getAppSecret());
}
MessageTaskBlob messageTaskBlob = messageTaskBlobMap.get(messageTask.getId());
messageDetail.setTemplate(messageTaskBlob.getTemplate());
messageDetails.add(messageDetail);
});
}
/**
* 根据用例ID获取所有该用例的定时任务的任务通知
* @param testId 用例id
* @return List<MessageDetail>
*/
public List<MessageDetail> searchMessageByTestId(String testId) {
MessageTaskExample example = new MessageTaskExample();
example.createCriteria().andTestIdEqualTo(testId);
List<MessageTask> messageTaskLists = messageTaskMapper.selectByExample(example);
List<MessageDetail> scheduleMessageTask = new ArrayList<>();
getMessageDetails(scheduleMessageTask, messageTaskLists);
scheduleMessageTask.sort(Comparator.comparing(MessageDetail::getCreateTime, Comparator.nullsLast(Long::compareTo)).reversed());
return scheduleMessageTask;
}
}

View File

@ -0,0 +1,133 @@
package io.metersphere.sdk.service;
import io.metersphere.project.domain.Project;
import io.metersphere.sdk.notice.constants.NoticeConstants;
import io.metersphere.sdk.notice.MessageDetail;
import io.metersphere.sdk.notice.sender.AbstractNoticeSender;
import io.metersphere.sdk.notice.sender.impl.*;
import io.metersphere.sdk.util.LogUtils;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import io.metersphere.sdk.notice.NoticeModel;
import java.util.ArrayList;
import java.util.List;
@Component
public class NoticeSendService {
@Resource
private MailNoticeSender mailNoticeSender;
@Resource
private WeComNoticeSender weComNoticeSender;
@Resource
private DingCustomNoticeSender dingCustomNoticeSender;
@Resource
private DingEnterPriseNoticeSender dingEnterPriseNoticeSender;
@Resource
private LarkNoticeSender larkNoticeSender;
@Resource
private InSiteNoticeSender inSiteNoticeSender;
@Resource
private WebhookNoticeSender webhookNoticeSender;
@Resource
private MessageDetailService messageDetailService;
private AbstractNoticeSender getNoticeSender(MessageDetail messageDetail) {
AbstractNoticeSender noticeSender = null;
switch (messageDetail.getType()) {
case NoticeConstants.Type.MAIL -> noticeSender = mailNoticeSender;
case NoticeConstants.Type.WECOM_ROBOT -> noticeSender = weComNoticeSender;
case NoticeConstants.Type.DING_CUSTOM_ROBOT -> noticeSender = dingCustomNoticeSender;
case NoticeConstants.Type.DING_ENTERPRISE_ROBOT -> noticeSender = dingEnterPriseNoticeSender;
case NoticeConstants.Type.LARK_ROBOT -> noticeSender = larkNoticeSender;
case NoticeConstants.Type.IN_SITE -> noticeSender = inSiteNoticeSender;
case NoticeConstants.Type.CUSTOM_WEBHOOK_ROBOT -> noticeSender = webhookNoticeSender;
default -> {
}
}
return noticeSender;
}
/**
* 在线操作发送通知
*/
@Async
public void send(String taskType, NoticeModel noticeModel) {
try {
String projectId = (String) noticeModel.getParamMap().get("projectId");
List<MessageDetail> messageDetails = messageDetailService.searchMessageByTypeAndProjectId(taskType, projectId);
// 异步发送通知
messageDetails.stream()
.filter(messageDetail -> StringUtils.equals(messageDetail.getEvent(), noticeModel.getEvent()))
.forEach(messageDetail -> {
MessageDetail m = SerializationUtils.clone(messageDetail);
NoticeModel n = SerializationUtils.clone(noticeModel);
this.getNoticeSender(m).send(m, n);
});
} catch (Exception e) {
LogUtils.error(e.getMessage(), e);
}
}
/**
* jenkins 和定时任务触发的发送
*/
@Async
public void sendJenkins(String triggerMode, NoticeModel noticeModel) {
// api和定时任务调用不排除自己
noticeModel.setExcludeSelf(false);
try {
List<MessageDetail> messageDetails = new ArrayList<>();
if (StringUtils.equals(triggerMode, NoticeConstants.Mode.SCHEDULE)) {
messageDetails = messageDetailService.searchMessageByTestId(noticeModel.getTestId());
}
if (StringUtils.equals(triggerMode, NoticeConstants.Mode.API)) {
String projectId = (String) noticeModel.getParamMap().get("projectId");
messageDetails = messageDetailService.searchMessageByTypeAndProjectId(NoticeConstants.TaskType.JENKINS_TASK, projectId);
}
// 异步发送通知
messageDetails.stream()
.filter(messageDetail -> StringUtils.equals(messageDetail.getEvent(), noticeModel.getEvent()))
.forEach(messageDetail -> {
MessageDetail m = SerializationUtils.clone(messageDetail);
NoticeModel n = SerializationUtils.clone(noticeModel);
this.getNoticeSender(m).send(m, n);
});
} catch (Exception e) {
LogUtils.error(e.getMessage(), e);
}
}
/**
* 后台触发的发送没有session
*/
@Async
public void send(Project project, String taskType, NoticeModel noticeModel) {
try {
List<MessageDetail> messageDetails = messageDetailService.searchMessageByTypeAndProjectId(taskType, project.getId());
// 异步发送通知
messageDetails.stream()
.filter(messageDetail -> StringUtils.equals(messageDetail.getEvent(), noticeModel.getEvent()))
.forEach(messageDetail -> {
MessageDetail m = SerializationUtils.clone(messageDetail);
NoticeModel n = SerializationUtils.clone(noticeModel);
this.getNoticeSender(m).send(m, n);
});
} catch (Exception e) {
LogUtils.error(e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,66 @@
package io.metersphere.sdk.service;
import io.metersphere.project.domain.Notification;
import io.metersphere.project.domain.NotificationExample;
import io.metersphere.project.mapper.NotificationMapper;
import io.metersphere.sdk.dto.request.NotificationRequest;
import io.metersphere.sdk.mapper.BaseNotificationMapper;
import io.metersphere.sdk.notice.constants.NotificationConstants;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(rollbackFor = Exception.class)
public class NotificationService {
@Resource
private NotificationMapper notificationMapper;
@Resource
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);
}
return baseNotificationMapper.listNotification(notificationRequest);
}
public int read(long id, String userId) {
Notification record = new Notification();
record.setStatus(NotificationConstants.Status.READ.name());
NotificationExample example = new NotificationExample();
example.createCriteria().andIdEqualTo(id).andReceiverEqualTo(userId);
return notificationMapper.updateByExampleSelective(record, example);
}
public int readAll(String userId) {
Notification record = new Notification();
record.setStatus(NotificationConstants.Status.READ.name());
NotificationExample example = new NotificationExample();
example.createCriteria().andReceiverEqualTo(userId);
return notificationMapper.updateByExampleSelective(record, example);
}
public int countNotification(Notification notification, String userId) {
if (StringUtils.isBlank(notification.getReceiver())) {
notification.setReceiver(userId);
}
return baseNotificationMapper.countNotification(notification);
}
public void sendAnnouncement(Notification notification) {
notificationMapper.insert(notification);
}
}

View File

@ -91,6 +91,8 @@ functional_case_template_extend.id.not_blank=ID不能为空
functional_case_template_extend.step_model.length_range=步骤模型长度必须在{min}-{max}之间 functional_case_template_extend.step_model.length_range=步骤模型长度必须在{min}-{max}之间
functional_case_template_extend.step_model.not_blank=步骤模型不能为空 functional_case_template_extend.step_model.not_blank=步骤模型不能为空
project_not_exist=项目不存在 project_not_exist=项目不存在
#消息管理
save_message_task_user_no_exist=所选用户部分不存在
# robot # robot
robot_is_null=当前机器人不存在 robot_is_null=当前机器人不存在
ding_type_is_null=钉钉机器人的类型不能为空 ding_type_is_null=钉钉机器人的类型不能为空

View File

@ -103,6 +103,8 @@ functional_case_template_extend.step_model.length_range=Step Model length must b
functional_case_template_extend.step_model.not_blank=Step Model is required functional_case_template_extend.step_model.not_blank=Step Model is required
project_is_null=Project does not exist project_is_null=Project does not exist
fake_error_name_exist=Fake error name already exists fake_error_name_exist=Fake error name already exists
#消息管理
save_message_task_user_no_exist=The selected user section does not exist
# robot # robot
robot_is_null=The current robot does not exist robot_is_null=The current robot does not exist
ding_type_is_null= DingTalk robot type is required ding_type_is_null= DingTalk robot type is required

View File

@ -103,6 +103,8 @@ functional_case_template_extend.step_model.length_range=步骤模型长度必须
functional_case_template_extend.step_model.not_blank=步骤模型不能为空 functional_case_template_extend.step_model.not_blank=步骤模型不能为空
project_not_exist=项目不存在 project_not_exist=项目不存在
fake_error_name_exist=误报名称已存在 fake_error_name_exist=误报名称已存在
#消息管理
save_message_task_user_no_exist=所选用户部分不存在
# robot # robot
robot_is_null=当前机器人不存在 robot_is_null=当前机器人不存在
ding_type_is_null=钉钉机器人的类型不能为空 ding_type_is_null=钉钉机器人的类型不能为空

View File

@ -103,6 +103,8 @@ functional_case_template_extend.step_model.length_range=步驟模型長度必須
functional_case_template_extend.step_model.not_blank=步驟模型不能為空 functional_case_template_extend.step_model.not_blank=步驟模型不能為空
project_is_null=項目不存在 project_is_null=項目不存在
fake_error_name_exist=誤報名稱已存在 fake_error_name_exist=誤報名稱已存在
#消息管理
save_message_task_user_no_exist=所選用戶部分不存在
# robot # robot
robot_is_null=當前機器人不存在 robot_is_null=當前機器人不存在
ding_type_is_null=釘釘機器人的類型不能為空 ding_type_is_null=釘釘機器人的類型不能為空

View File

@ -222,6 +222,9 @@ permission.system_operation_log.name=日志
permission.organization_operation_log.name=日志 permission.organization_operation_log.name=日志
permission.organization_custom_field.name=自定义字段 permission.organization_custom_field.name=自定义字段
permission.organization_template.name=模板 permission.organization_template.name=模板
# message
user.remove=已被移除
alert_others=通知人

View File

@ -223,7 +223,9 @@ permission.system_operation_log.name=Operation log
permission.organization_operation_log.name=Operation log permission.organization_operation_log.name=Operation log
permission.organization_custom_field.name=Custom Field permission.organization_custom_field.name=Custom Field
permission.organization_template.name=Template permission.organization_template.name=Template
# message
user.remove=has been removed
alert_others=Alert others

View File

@ -222,4 +222,6 @@ permission.system_operation_log.name=日志
permission.organization_operation_log.name=日志 permission.organization_operation_log.name=日志
permission.organization_custom_field.name=自定义字段 permission.organization_custom_field.name=自定义字段
permission.organization_template.name=模板 permission.organization_template.name=模板
# message
user.remove=已被移除
alert_others=通知人

View File

@ -222,3 +222,6 @@ permission.system_operation_log.name=日志
permission.organization_operation_log.name=日志 permission.organization_operation_log.name=日志
permission.organization_custom_field.name=自定義字段 permission.organization_custom_field.name=自定義字段
permission.organization_template.name=模板 permission.organization_template.name=模板
# message
user.remove=已被移除
alert_others=通知人

View File

@ -0,0 +1,31 @@
package io.metersphere.project.controller;
import io.metersphere.project.service.NoticeMessageTaskService;
import io.metersphere.sdk.controller.handler.ResultHolder;
import io.metersphere.sdk.dto.request.MessageTaskRequest;
import io.metersphere.sdk.util.SessionUtils;
import io.metersphere.validation.groups.Created;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@Tag(name = "项目管理-消息设置")
@RestController
@RequestMapping("notice/")
public class NoticeMessageTaskController {
@Resource
private NoticeMessageTaskService noticeMessageTaskService;
@PostMapping("message/task/add")
@Operation(summary = "项目管理-消息设置-保存消息设置")
public ResultHolder saveMessage(@Validated({Created.class}) @RequestBody MessageTaskRequest messageTaskRequest) {
return noticeMessageTaskService.saveMessageTask(messageTaskRequest, SessionUtils.getUserId());
}
}

View File

@ -0,0 +1,32 @@
package io.metersphere.project.enums.result;
import io.metersphere.sdk.controller.handler.result.IResultCode;
/**
* @author guoyuqi
*/
public enum ProjectResultCode implements IResultCode {
/**
* 项目管理-消息设置-保存消息设置-所选用户不在当前系统中会返回
*/
SAVE_MESSAGE_TASK_USER_NO_EXIST(102001, "save_message_task_user_no_exist");
private final int code;
private final String message;
ProjectResultCode(int code, String message) {
this.code = code;
this.message = message;
}
@Override
public int getCode() {
return code;
}
@Override
public String getMessage() {
return getTranslationMessage(this.message);
}
}

View File

@ -18,11 +18,11 @@ public class CleanupRobotResourceService implements CleanupProjectResourceServic
ProjectRobotExample projectExample = new ProjectRobotExample(); ProjectRobotExample projectExample = new ProjectRobotExample();
projectExample.createCriteria().andProjectIdEqualTo(projectId); projectExample.createCriteria().andProjectIdEqualTo(projectId);
robotMapper.deleteByExample(projectExample); robotMapper.deleteByExample(projectExample);
LogUtils.info("删除当前项目[" + projectId + "]相关接口测试资源"); LogUtils.info("删除当前项目[" + projectId + "]相关消息机器人资源");
} }
@Override @Override
public void cleanReportResources(String projectId) { public void cleanReportResources(String projectId) {
LogUtils.info("清理当前项目[" + projectId + "]相关接口测试报告资源"); LogUtils.info("清理当前项目[" + projectId + "]相关消息机器人报告资源");
} }
} }

View File

@ -0,0 +1,116 @@
package io.metersphere.project.service;
import io.metersphere.project.domain.MessageTask;
import io.metersphere.project.domain.MessageTaskBlob;
import io.metersphere.project.enums.result.ProjectResultCode;
import io.metersphere.project.mapper.MessageTaskBlobMapper;
import io.metersphere.project.mapper.MessageTaskMapper;
import io.metersphere.sdk.controller.handler.ResultHolder;
import io.metersphere.sdk.dto.request.MessageTaskRequest;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.uid.UUID;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.domain.User;
import io.metersphere.system.domain.UserRoleRelation;
import io.metersphere.system.domain.UserRoleRelationExample;
import io.metersphere.system.mapper.UserMapper;
import io.metersphere.system.mapper.UserRoleRelationMapper;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@Transactional(rollbackFor = Exception.class)
public class NoticeMessageTaskService {
@Resource
private UserMapper userMapper;
@Resource
private SqlSessionFactory sqlSessionFactory;
@Resource
private UserRoleRelationMapper userRoleRelationMapper;
public static final String USER_IDS = "user_ids";
public static final String NO_USER_NAMES = "no_user_names";
public static final String CREATOR = "CREATOR";
public static final String FOLLOW_PEOPLE = "FOLLOW_PEOPLE";
public ResultHolder saveMessageTask(MessageTaskRequest messageTaskRequest, String userId) {
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
MessageTaskMapper mapper = sqlSession.getMapper(MessageTaskMapper.class);
MessageTaskBlobMapper blobMapper = sqlSession.getMapper(MessageTaskBlobMapper.class);
//检查用户是否存在
Map<String, List<String>> stringListMap = checkUserExistProject(messageTaskRequest.getReceiverIds(), messageTaskRequest.getProjectId());
for (String receiverId : stringListMap.get(USER_IDS)) {
MessageTask messageTask = new MessageTask();
String insertId = UUID.randomUUID().toString();
messageTask.setId(insertId);
messageTask.setTaskType(messageTaskRequest.getTaskType());
messageTask.setEvent(messageTaskRequest.getEvent());
messageTask.setReceiver(receiverId);
messageTask.setProjectId(messageTaskRequest.getProjectId());
messageTask.setProjectRobotId(messageTaskRequest.getRobotId());
String testId = messageTaskRequest.getTestId() == null ? "NONE" : messageTaskRequest.getTestId();
messageTask.setTestId(testId);
messageTask.setCreateUser(userId);
messageTask.setCreateTime(System.currentTimeMillis());
messageTask.setUpdateUser(userId);
messageTask.setUpdateTime(System.currentTimeMillis());
messageTask.setEnable(messageTaskRequest.getEnable());
mapper.insert(messageTask);
MessageTaskBlob messageTaskBlob = new MessageTaskBlob();
messageTaskBlob.setId(messageTask.getId());
messageTaskBlob.setTemplate(messageTaskRequest.getTemplate());
blobMapper.insert(messageTaskBlob);
}
sqlSession.flushStatements();
SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
if (CollectionUtils.isNotEmpty(stringListMap.get(NO_USER_NAMES))) {
String message = Translator.get("alert_others") + stringListMap.get(NO_USER_NAMES).get(0) + Translator.get("user.remove");
return ResultHolder.successCodeErrorInfo(ProjectResultCode.SAVE_MESSAGE_TASK_USER_NO_EXIST.getCode(), message);
}
return ResultHolder.success("OK");
}
private Map<String, List<String>> checkUserExistProject(List<String> receiverIds, String projectId) {
UserRoleRelationExample userRoleRelationExample = new UserRoleRelationExample();
userRoleRelationExample.createCriteria().andUserIdIn(receiverIds).andSourceIdEqualTo(projectId);
List<UserRoleRelation> userRoleRelations = userRoleRelationMapper.selectByExample(userRoleRelationExample);
List<String> userIds = userRoleRelations.stream().map(UserRoleRelation::getUserId).distinct().toList();
Map<String, List<String>> map = new HashMap<>();
if (CollectionUtils.isEmpty(userIds)) {
throw new MSException(Translator.get("user.not.exist"));
}
List<String> noUserNames = new ArrayList<>();
if (userIds.size() < receiverIds.size()) {
for (String receiverId : receiverIds) {
if (!StringUtils.equalsIgnoreCase(receiverId, CREATOR) && !StringUtils.equalsIgnoreCase(receiverId, FOLLOW_PEOPLE) && !userIds.contains(receiverId)) {
User user = userMapper.selectByPrimaryKey(receiverId);
noUserNames.add(user.getName());
break;
}
}
}
map.put(NO_USER_NAMES, noUserNames);
map.put(USER_IDS, userIds);
return map;
}
}

View File

@ -0,0 +1,78 @@
package io.metersphere.project.controller;
import io.metersphere.project.domain.ProjectRobot;
import io.metersphere.project.request.ProjectRobotRequest;
import io.metersphere.project.service.CleanupRobotResourceService;
import io.metersphere.sdk.base.BaseTest;
import io.metersphere.sdk.constants.SessionConstants;
import io.metersphere.sdk.controller.handler.ResultHolder;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.Pager;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@AutoConfigureMockMvc
public class CleanupRobotResourceTests extends BaseTest {
@Resource
private CleanupRobotResourceService resourceService;
public static final String ROBOT_LIST = "/project/robot/list/page";
@Test
@Order(1)
@Sql(scripts = {"/dml/init_project_robot.sql"}, config = @SqlConfig(encoding = "utf-8", transactionMode = SqlConfig.TransactionMode.ISOLATED))
public void testCleanupResource() throws Exception {
ProjectRobotRequest request = new ProjectRobotRequest();
request.setCurrent(1);
request.setPageSize(5);
Pager<?> sortPageData = getPager(request);
List<ProjectRobot> projectRobots = JSON.parseArray(JSON.toJSONString(sortPageData.getList()), ProjectRobot.class);
if (CollectionUtils.isNotEmpty(projectRobots)) {
resourceService.deleteResources("test");
}
request = new ProjectRobotRequest();
request.setCurrent(1);
request.setPageSize(5);
request.setKeyword("测试机器人");
Pager<?> sortPageDataAfter = getPager(request);
List<ProjectRobot> projectRobotAfters = JSON.parseArray(JSON.toJSONString(sortPageDataAfter.getList()), ProjectRobot.class);
Assertions.assertTrue(CollectionUtils.isEmpty(projectRobotAfters));
}
@Test
@Order(2)
public void testCleanupReportResource() throws Exception {
resourceService.cleanReportResources("test");
}
private Pager<?> getPager(ProjectRobotRequest request) throws Exception {
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post(ROBOT_LIST)
.header(SessionConstants.HEADER_TOKEN, sessionId)
.header(SessionConstants.CSRF_TOKEN, csrfToken)
.content(JSON.toJSONString(request))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON)).andReturn();
String sortData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder sortHolder = JSON.parseObject(sortData, ResultHolder.class);
Pager<?> sortPageData = JSON.parseObject(JSON.toJSONString(sortHolder.getData()), Pager.class);
return sortPageData;
}
}

View File

@ -0,0 +1,113 @@
package io.metersphere.project.controller;
import io.metersphere.sdk.base.BaseTest;
import io.metersphere.sdk.constants.SessionConstants;
import io.metersphere.sdk.controller.handler.ResultHolder;
import io.metersphere.sdk.dto.request.MessageTaskRequest;
import io.metersphere.sdk.notice.constants.NoticeConstants;
import io.metersphere.sdk.util.JSON;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class NoticeMessageTaskControllerTests extends BaseTest {
@Test
@Order(1)
@Sql(scripts = {"/dml/init_project_message.sql"}, config = @SqlConfig(encoding = "utf-8", transactionMode = SqlConfig.TransactionMode.ISOLATED))
public void addMessageTaskHalfSuccess() throws Exception {
MessageTaskRequest messageTaskRequest = new MessageTaskRequest();
messageTaskRequest.setProjectId("project-message-test");
messageTaskRequest.setTaskType(NoticeConstants.TaskType.API_DEFINITION_TASK);
messageTaskRequest.setEvent(NoticeConstants.Event.CREATE);
List<String> userIds = new ArrayList<>();
userIds.add("project-message-user-1");
userIds.add("project-message-user-2");
userIds.add("project-message-user-del");
messageTaskRequest.setReceiverIds(userIds);
messageTaskRequest.setRobotId("test_message_robot2");
messageTaskRequest.setEnable(true);
messageTaskRequest.setTemplate("发送消息测试");
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/notice/message/task/add")
.header(SessionConstants.HEADER_TOKEN, sessionId)
.header(SessionConstants.CSRF_TOKEN, csrfToken)
.content(JSON.toJSONString(messageTaskRequest))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON)).andReturn();
String contentAsString = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder resultHolder = JSON.parseObject(contentAsString, ResultHolder.class);
Assertions.assertEquals(102001, resultHolder.getCode());
}
@Test
@Order(2)
public void addMessageTaskSuccess() throws Exception {
MessageTaskRequest messageTaskRequest = new MessageTaskRequest();
messageTaskRequest.setProjectId("project-message-test-1");
messageTaskRequest.setTaskType(NoticeConstants.TaskType.API_DEFINITION_TASK);
messageTaskRequest.setEvent(NoticeConstants.Event.CREATE);
List<String> userIds = new ArrayList<>();
userIds.add("project-message-user-3");
userIds.add("project-message-user-4");
messageTaskRequest.setReceiverIds(userIds);
messageTaskRequest.setRobotId("test_message_robot2");
messageTaskRequest.setEnable(true);
messageTaskRequest.setTemplate("发送消息测试");
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/notice/message/task/add")
.header(SessionConstants.HEADER_TOKEN, sessionId)
.header(SessionConstants.CSRF_TOKEN, csrfToken)
.content(JSON.toJSONString(messageTaskRequest))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON)).andReturn();
String contentAsString = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder resultHolder = JSON.parseObject(contentAsString, ResultHolder.class);
Assertions.assertEquals(100200, resultHolder.getCode());
}
@Test
@Order(3)
public void addMessageTaskFile() throws Exception {
MessageTaskRequest messageTaskRequest = new MessageTaskRequest();
messageTaskRequest.setProjectId("project-message-test-1");
messageTaskRequest.setTaskType(NoticeConstants.TaskType.API_DEFINITION_TASK);
messageTaskRequest.setEvent(NoticeConstants.Event.CREATE);
List<String> userIds = new ArrayList<>();
userIds.add("project-message-user-5");
userIds.add("project-message-user-6");
messageTaskRequest.setReceiverIds(userIds);
messageTaskRequest.setRobotId("test_message_robot2");
messageTaskRequest.setEnable(true);
messageTaskRequest.setTemplate("发送消息测试");
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/notice/message/task/add")
.header(SessionConstants.HEADER_TOKEN, sessionId)
.header(SessionConstants.CSRF_TOKEN, csrfToken)
.content(JSON.toJSONString(messageTaskRequest))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().is5xxServerError())
.andExpect(content().contentType(MediaType.APPLICATION_JSON)).andReturn();
String contentAsString = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder resultHolder = JSON.parseObject(contentAsString, ResultHolder.class);
Assertions.assertEquals(100500, resultHolder.getCode());
}
}

View File

@ -0,0 +1,30 @@
# 插入测试数据
INSERT INTO organization(id, num, name, description, create_time, update_time, create_user, update_user, deleted, delete_user, delete_time) VALUE
('organization-message-test', null, 'organization-message-test', 'organization-message-test', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 'admin', 'admin', 0, null, null);
INSERT INTO project (id, num, organization_id, name, description, create_user, update_user, create_time, update_time) VALUES
('project-message-test', null, 'organization-message-test', '默认项目', '系统默认创建的项目', 'admin', 'admin', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000),
('project-message-test-1', null, 'organization-message-test-1', '默认项目1', '系统默认创建的项目1', '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
('project-message-user-1', 'project-message-user-1', 'project-message-member1@metersphere.io', MD5('metersphere'), UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NUll, '', 'LOCAL', NULL, 'admin', 'admin', 0),
('project-message-user-2', 'project-message-user-2', 'project-message-member2@metersphere.io', MD5('metersphere'), UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NUll, '', 'LOCAL', NULL, 'admin', 'admin', 0),
('project-message-user-3', 'project-message-user-3', 'project-message-member3@metersphere.io', MD5('metersphere'), UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NUll, '', 'LOCAL', NULL, 'admin', 'admin', 0),
('project-message-user-4', 'project-message-user-4', 'project-message-member4@metersphere.io', MD5('metersphere'), UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NUll, '', 'LOCAL', NULL, 'admin', 'admin', 0),
('project-message-user-del', 'project-message-user-del', 'project-message-member-del@metersphere.io', MD5('metersphere'), UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NUll, '', 'LOCAL', NULL, 'admin', 'admin', 1);
INSERT INTO user_role_relation (id, user_id, role_id, source_id, organization_id, create_time, create_user) VALUES
(UUID(), 'project-message-user-1', 'project_member', 'project-message-test', 'organization-message-test', UNIX_TIMESTAMP() * 1000, 'admin'),
(UUID(), 'project-message-user-2', 'project_member', 'project-message-test', 'organization-message-test', UNIX_TIMESTAMP() * 1000, 'admin'),
(UUID(), 'project-message-user-3', 'project_member', 'project-message-test-1', 'organization-message-test', UNIX_TIMESTAMP() * 1000, 'admin'),
(UUID(), 'project-message-user-4', 'project_member', 'project-message-test-1', 'organization-message-test', UNIX_TIMESTAMP() * 1000, 'admin');
replace INTO project_robot(id, project_id, name, platform, webhook, type, app_key, app_secret, enable, create_user, create_time, update_user, update_time, description) VALUES ('test_message_robot1', 'test', '测试机器人1', 'IN_SITE', 'NONE', null, null, null, true, 'admin', unix_timestamp() * 1000,'admin', unix_timestamp() * 1000, null);
replace INTO project_robot(id, project_id, name, platform, webhook, type, app_key, app_secret, enable, create_user, create_time, update_user, update_time, description) VALUES ('test_message_robot2', 'test', '测试机器人2', 'MAIL', 'NONE', null, null, null, true, 'admin', unix_timestamp() * 1000,'admin', unix_timestamp() * 1000, null);

View File

@ -0,0 +1,8 @@
# 插入测试数据
replace INTO project_robot(id, project_id, name, platform, webhook, type, app_key, app_secret, enable, create_user, create_time, update_user, update_time, description) VALUES ('test_project_robot1', 'test', '测试机器人1', 'IN_SITE', 'NONE', null, null, null, true, 'admin', unix_timestamp() * 1000,'admin', unix_timestamp() * 1000, null);
replace INTO project_robot(id, project_id, name, platform, webhook, type, app_key, app_secret, enable, create_user, create_time, update_user, update_time, description) VALUES ('test_project_robot1', 'test', '测试机器人2', 'MAIL', 'NONE', null, null, null, true, 'admin', unix_timestamp() * 1000,'admin', unix_timestamp() * 1000, null);