feat(消息管理): 增加消息通知
This commit is contained in:
parent
8baec397b8
commit
a741feee76
|
@ -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_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_UPDATE = "PROJECT_MESSAGE:READ+UPDATE";
|
||||
public static final String PROJECT_MESSAGE_READ_ADD = "PROJECT_MESSAGE:READ+ADD";
|
||||
public static final String PROJECT_MESSAGE_READ_DELETE = "PROJECT_MESSAGE:READ+DELETE";
|
||||
/*------ end: PROJECT_MESSAGE ------*/
|
||||
|
||||
/*------ start: PROJECT_APPLICATION ------*/
|
||||
public static final String PROJECT_APPLICATION_TEST_PLAN_READ = "PROJECT_APPLICATION_TEST_PLAN:READ";
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -54,4 +54,14 @@ public class ResultHolder {
|
|||
public static ResultHolder error(int code, String message, Object messageDetail) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ public enum CommonResultCode implements IResultCode {
|
|||
CUSTOM_FIELD_EXIST(101012, "custom_field.exist"),
|
||||
TEMPLATE_EXIST(101013, "template.exist");
|
||||
|
||||
|
||||
private int code;
|
||||
private String message;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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 > (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 > (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>
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 "";
|
||||
}
|
|
@ -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";//处理人
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,4 +1,282 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -2,18 +2,166 @@ package io.metersphere.sdk.notice.sender.impl;
|
|||
|
||||
|
||||
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.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.StringUtils;
|
||||
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import io.metersphere.sdk.notice.Receiver;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
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) {
|
||||
//设置抄送人 CC(Carbon 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) {
|
||||
Properties props = new Properties();
|
||||
|
@ -42,4 +190,5 @@ public class MailNoticeSender extends AbstractNoticeSender {
|
|||
javaMailSender.setJavaMailProperties(props);
|
||||
return javaMailSender;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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.not_blank=步骤模型不能为空
|
||||
project_not_exist=项目不存在
|
||||
#消息管理
|
||||
save_message_task_user_no_exist=所选用户部分不存在
|
||||
# robot
|
||||
robot_is_null=当前机器人不存在
|
||||
ding_type_is_null=钉钉机器人的类型不能为空
|
||||
|
|
|
@ -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
|
||||
project_is_null=Project does not exist
|
||||
fake_error_name_exist=Fake error name already exists
|
||||
#消息管理
|
||||
save_message_task_user_no_exist=The selected user section does not exist
|
||||
# robot
|
||||
robot_is_null=The current robot does not exist
|
||||
ding_type_is_null= DingTalk robot type is required
|
||||
|
|
|
@ -103,6 +103,8 @@ functional_case_template_extend.step_model.length_range=步骤模型长度必须
|
|||
functional_case_template_extend.step_model.not_blank=步骤模型不能为空
|
||||
project_not_exist=项目不存在
|
||||
fake_error_name_exist=误报名称已存在
|
||||
#消息管理
|
||||
save_message_task_user_no_exist=所选用户部分不存在
|
||||
# robot
|
||||
robot_is_null=当前机器人不存在
|
||||
ding_type_is_null=钉钉机器人的类型不能为空
|
||||
|
|
|
@ -103,6 +103,8 @@ functional_case_template_extend.step_model.length_range=步驟模型長度必須
|
|||
functional_case_template_extend.step_model.not_blank=步驟模型不能為空
|
||||
project_is_null=項目不存在
|
||||
fake_error_name_exist=誤報名稱已存在
|
||||
#消息管理
|
||||
save_message_task_user_no_exist=所選用戶部分不存在
|
||||
# robot
|
||||
robot_is_null=當前機器人不存在
|
||||
ding_type_is_null=釘釘機器人的類型不能為空
|
||||
|
|
|
@ -222,6 +222,9 @@ permission.system_operation_log.name=日志
|
|||
permission.organization_operation_log.name=日志
|
||||
permission.organization_custom_field.name=自定义字段
|
||||
permission.organization_template.name=模板
|
||||
# message
|
||||
user.remove=已被移除
|
||||
alert_others=通知人
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -223,7 +223,9 @@ permission.system_operation_log.name=Operation log
|
|||
permission.organization_operation_log.name=Operation log
|
||||
permission.organization_custom_field.name=Custom Field
|
||||
permission.organization_template.name=Template
|
||||
|
||||
# message
|
||||
user.remove=has been removed
|
||||
alert_others=Alert others
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -222,4 +222,6 @@ permission.system_operation_log.name=日志
|
|||
permission.organization_operation_log.name=日志
|
||||
permission.organization_custom_field.name=自定义字段
|
||||
permission.organization_template.name=模板
|
||||
|
||||
# message
|
||||
user.remove=已被移除
|
||||
alert_others=通知人
|
||||
|
|
|
@ -221,4 +221,7 @@ permission.system_organization_project_member.delete=删除成员
|
|||
permission.system_operation_log.name=日志
|
||||
permission.organization_operation_log.name=日志
|
||||
permission.organization_custom_field.name=自定義字段
|
||||
permission.organization_template.name=模板
|
||||
permission.organization_template.name=模板
|
||||
# message
|
||||
user.remove=已被移除
|
||||
alert_others=通知人
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -18,11 +18,11 @@ public class CleanupRobotResourceService implements CleanupProjectResourceServic
|
|||
ProjectRobotExample projectExample = new ProjectRobotExample();
|
||||
projectExample.createCriteria().andProjectIdEqualTo(projectId);
|
||||
robotMapper.deleteByExample(projectExample);
|
||||
LogUtils.info("删除当前项目[" + projectId + "]相关接口测试资源");
|
||||
LogUtils.info("删除当前项目[" + projectId + "]相关消息机器人资源");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanReportResources(String projectId) {
|
||||
LogUtils.info("清理当前项目[" + projectId + "]相关接口测试报告资源");
|
||||
LogUtils.info("清理当前项目[" + projectId + "]相关消息机器人报告资源");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
||||
|
||||
|
|
@ -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);
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue