From a741feee760f74cc9315cd80d75db9f7920f681a Mon Sep 17 00:00:00 2001 From: guoyuqi Date: Tue, 12 Sep 2023 18:34:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=B6=88=E6=81=AF=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=B6=88=E6=81=AF=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sdk/constants/PermissionConstants.java | 2 + .../controller/NotificationController.java | 52 ++++ .../sdk/controller/handler/ResultHolder.java | 10 + .../handler/result/CommonResultCode.java | 1 + .../sdk/dto/request/MessageTaskRequest.java | 51 ++++ .../sdk/dto/request/NotificationRequest.java | 34 +++ .../sdk/mapper/BaseNotificationMapper.java | 16 + .../sdk/mapper/BaseNotificationMapper.xml | 30 ++ .../metersphere/sdk/notice/MessageDetail.java | 23 ++ .../metersphere/sdk/notice/NoticeModel.java | 59 ++++ .../io/metersphere/sdk/notice/Receiver.java | 13 + .../sdk/notice/annotation/SendNotice.java | 40 +++ .../sdk/notice/constants/NoticeConstants.java | 73 +++++ .../constants/NotificationConstants.java | 12 + .../notice/sender/AbstractNoticeSender.java | 280 +++++++++++++++++- .../AfterReturningNoticeSendService.java | 105 +++++++ .../sdk/notice/sender/NoticeSender.java | 9 + .../sdk/notice/sender/SendNoticeAspect.java | 137 +++++++++ .../sender/impl/DingCustomNoticeSender.java | 38 +++ .../impl/DingEnterPriseNoticeSender.java | 101 +++++++ .../sender/impl/InSiteNoticeSender.java | 66 +++++ .../notice/sender/impl/LarkNoticeSender.java | 39 +++ .../notice/sender/impl/MailNoticeSender.java | 153 +++++++++- .../notice/sender/impl/WeComNoticeSender.java | 37 +++ .../sender/impl/WebhookNoticeSender.java | 66 +++++ .../sdk/notice/utils/ClientPost.java | 37 +++ .../sdk/notice/utils/DingClient.java | 24 ++ .../sdk/notice/utils/LarkClient.java | 19 ++ .../sdk/notice/utils/WeComClient.java | 21 ++ .../sdk/service/MessageDetailService.java | 129 ++++++++ .../sdk/service/NoticeSendService.java | 133 +++++++++ .../sdk/service/NotificationService.java | 66 +++++ .../main/resources/i18n/project.properties | 2 + .../resources/i18n/project_en_US.properties | 2 + .../resources/i18n/project_zh_CN.properties | 2 + .../resources/i18n/project_zh_TW.properties | 2 + .../src/main/resources/i18n/system.properties | 3 + .../resources/i18n/system_en_US.properties | 4 +- .../resources/i18n/system_zh_CN.properties | 4 +- .../resources/i18n/system_zh_TW.properties | 5 +- .../NoticeMessageTaskController.java | 31 ++ .../enums/result/ProjectResultCode.java | 32 ++ .../service/CleanupRobotResourceService.java | 4 +- .../service/NoticeMessageTaskService.java | 116 ++++++++ .../controller/CleanupRobotResourceTests.java | 78 +++++ .../NoticeMessageTaskControllerTests.java | 113 +++++++ .../resources/dml/init_project_message.sql | 30 ++ .../test/resources/dml/init_project_robot.sql | 8 + 48 files changed, 2304 insertions(+), 8 deletions(-) create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/NotificationController.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/request/MessageTaskRequest.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/request/NotificationRequest.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/mapper/BaseNotificationMapper.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/mapper/BaseNotificationMapper.xml create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/MessageDetail.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/NoticeModel.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/Receiver.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/annotation/SendNotice.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/constants/NoticeConstants.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/constants/NotificationConstants.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/AfterReturningNoticeSendService.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/NoticeSender.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/SendNoticeAspect.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/DingCustomNoticeSender.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/DingEnterPriseNoticeSender.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/InSiteNoticeSender.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/LarkNoticeSender.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/WeComNoticeSender.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/WebhookNoticeSender.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/ClientPost.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/DingClient.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/LarkClient.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/WeComClient.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/service/MessageDetailService.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/service/NoticeSendService.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/service/NotificationService.java create mode 100644 backend/services/project-management/src/main/java/io/metersphere/project/controller/NoticeMessageTaskController.java create mode 100644 backend/services/project-management/src/main/java/io/metersphere/project/enums/result/ProjectResultCode.java create mode 100644 backend/services/project-management/src/main/java/io/metersphere/project/service/NoticeMessageTaskService.java create mode 100644 backend/services/project-management/src/test/java/io/metersphere/project/controller/CleanupRobotResourceTests.java create mode 100644 backend/services/project-management/src/test/java/io/metersphere/project/controller/NoticeMessageTaskControllerTests.java create mode 100644 backend/services/project-management/src/test/resources/dml/init_project_message.sql create mode 100644 backend/services/project-management/src/test/resources/dml/init_project_robot.sql diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PermissionConstants.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PermissionConstants.java index 5ac883bab7..bdbd7b33b0 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PermissionConstants.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PermissionConstants.java @@ -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"; diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/NotificationController.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/NotificationController.java new file mode 100644 index 0000000000..56c59ce9cd --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/NotificationController.java @@ -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> listNotification(@Validated @RequestBody NotificationRequest notificationRequest) { + Page 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()); + } +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/ResultHolder.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/ResultHolder.java index 6f16f0ee75..865dcb324f 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/ResultHolder.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/ResultHolder.java @@ -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); + } } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/result/CommonResultCode.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/result/CommonResultCode.java index 25ec90a0f1..46ca5d2bf2 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/result/CommonResultCode.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/controller/handler/result/CommonResultCode.java @@ -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; diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/request/MessageTaskRequest.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/request/MessageTaskRequest.java new file mode 100644 index 0000000000..1db42b06ca --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/request/MessageTaskRequest.java @@ -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 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; + +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/request/NotificationRequest.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/request/NotificationRequest.java new file mode 100644 index 0000000000..0917168916 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/request/NotificationRequest.java @@ -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; + +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/mapper/BaseNotificationMapper.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/mapper/BaseNotificationMapper.java new file mode 100644 index 0000000000..21c80ad02f --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/mapper/BaseNotificationMapper.java @@ -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 listNotification(@Param("notificationRequest") NotificationRequest notificationRequest); + + int countNotification(@Param("notification") Notification notification); + +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/mapper/BaseNotificationMapper.xml b/backend/framework/sdk/src/main/java/io/metersphere/sdk/mapper/BaseNotificationMapper.xml new file mode 100644 index 0000000000..95e89c1c7c --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/mapper/BaseNotificationMapper.xml @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/MessageDetail.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/MessageDetail.java new file mode 100644 index 0000000000..4213c4c8ba --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/MessageDetail.java @@ -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 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; + +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/NoticeModel.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/NoticeModel.java new file mode 100644 index 0000000000..ac3d368155 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/NoticeModel.java @@ -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 relatedUsers; + + /** + * 模版里的参数信息 + */ + private Map paramMap; + /** + * 接收人 + */ + private List receivers; + /** + * 抄送人 + */ + private List recipients; + /** + * 包括自己 + */ + private boolean excludeSelf; +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/Receiver.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/Receiver.java new file mode 100644 index 0000000000..d8a0044674 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/Receiver.java @@ -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; +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/annotation/SendNotice.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/annotation/SendNotice.java new file mode 100644 index 0000000000..6980fd3fc9 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/annotation/SendNotice.java @@ -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 ""; +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/constants/NoticeConstants.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/constants/NoticeConstants.java new file mode 100644 index 0000000000..51838d7c43 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/constants/NoticeConstants.java @@ -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";//处理人 + } +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/constants/NotificationConstants.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/constants/NotificationConstants.java new file mode 100644 index 0000000000..d617dc4b52 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/constants/NotificationConstants.java @@ -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 + } +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/AbstractNoticeSender.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/AbstractNoticeSender.java index 2ee38a0d47..66527cfa12 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/AbstractNoticeSender.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/AbstractNoticeSender.java @@ -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 fields; + if (customFields instanceof String) { + fields = JSON.parseArray((String) customFields, Object.class); + } else { + fields = (List) 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 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 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 getRealUserIds(MessageDetail messageDetail, NoticeModel noticeModel, String event) { + List toUsers = new ArrayList<>(); + Map 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 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 toUsers, Map paramMap) { + List relatedUsers = (List) paramMap.get("userIds"); + if (CollectionUtils.isNotEmpty(relatedUsers)) { + List receivers = relatedUsers.stream() + .map(u -> new Receiver(u, NotificationConstants.Type.SYSTEM_NOTICE.name())) + .toList(); + toUsers.addAll(receivers); + } + } + + private List handleFollows(MessageDetail messageDetail, NoticeModel noticeModel) { + List 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 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 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 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 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 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 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 getUsers(List userIds) { + UserExample userExample = new UserExample(); + userExample.createCriteria().andIdIn(userIds); + return userMapper.selectByExample(userExample); + } } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/AfterReturningNoticeSendService.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/AfterReturningNoticeSendService.java new file mode 100644 index 0000000000..cec9ec2dfb --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/AfterReturningNoticeSendService.java @@ -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 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 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; + } +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/NoticeSender.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/NoticeSender.java new file mode 100644 index 0000000000..7395121465 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/NoticeSender.java @@ -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); +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/SendNoticeAspect.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/SendNoticeAspect.java new file mode 100644 index 0000000000..4762d8f45f --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/SendNoticeAspect.java @@ -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 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 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(); + } + } +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/DingCustomNoticeSender.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/DingCustomNoticeSender.java new file mode 100644 index 0000000000..f280b7c45b --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/DingCustomNoticeSender.java @@ -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 userIds = noticeModel.getReceivers().stream() + .map(Receiver::getUserId) + .distinct() + .collect(Collectors.toList()); + List users = super.getUsers(userIds); + List 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); + } + +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/DingEnterPriseNoticeSender.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/DingEnterPriseNoticeSender.java new file mode 100644 index 0000000000..e2d2ab1984 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/DingEnterPriseNoticeSender.java @@ -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); + } + } +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/InSiteNoticeSender.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/InSiteNoticeSender.java new file mode 100644 index 0000000000..2d92526707 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/InSiteNoticeSender.java @@ -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 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 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); + } + +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/LarkNoticeSender.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/LarkNoticeSender.java new file mode 100644 index 0000000000..3905aa9a1f --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/LarkNoticeSender.java @@ -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 userIds = noticeModel.getReceivers().stream() + .map(Receiver::getUserId) + .distinct() + .collect(Collectors.toList()); + List users = super.getUsers(userIds); + List collect = users.stream() + .map(ud -> "" + ud.getName() + "") + .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); + } +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/MailNoticeSender.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/MailNoticeSender.java index 94a496898c..6e2ab319e6 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/MailNoticeSender.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/MailNoticeSender.java @@ -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 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 paramList = systemParameterMapper.selectByExample(example); + Map 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 userIds = noticeModel.getReceivers().stream() + .map(Receiver::getUserId) + .distinct() + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(userIds)) { + return; + } + + List 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 paramMap) { Properties props = new Properties(); @@ -42,4 +190,5 @@ public class MailNoticeSender extends AbstractNoticeSender { javaMailSender.setJavaMailProperties(props); return javaMailSender; } + } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/WeComNoticeSender.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/WeComNoticeSender.java new file mode 100644 index 0000000000..208c982cd9 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/WeComNoticeSender.java @@ -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 userIds = noticeModel.getReceivers().stream() + .map(Receiver::getUserId) + .distinct() + .collect(Collectors.toList()); + List users = super.getUsers(userIds); + List 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); + } + +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/WebhookNoticeSender.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/WebhookNoticeSender.java new file mode 100644 index 0000000000..2c4a518089 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/sender/impl/WebhookNoticeSender.java @@ -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 receivers = noticeModel.getReceivers(); + CloseableHttpClient httpClient = HttpClients.createDefault(); + CloseableHttpResponse response = null; + if (CollectionUtils.isNotEmpty(receivers)) { + List 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); + } + +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/ClientPost.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/ClientPost.java new file mode 100644 index 0000000000..61a8052207 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/ClientPost.java @@ -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 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); + } + } + } + +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/DingClient.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/DingClient.java new file mode 100644 index 0000000000..4b0a972457 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/DingClient.java @@ -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, ListmobileList) { + CloseableHttpClient httpClient = HttpClients.createDefault(); + Map mp = new LinkedHashMap<>(); + Map js = new HashMap<>(); + Map 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); + } + +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/LarkClient.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/LarkClient.java new file mode 100644 index 0000000000..b99918050c --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/LarkClient.java @@ -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 mp = new LinkedHashMap<>(); + Map js = new HashMap<>(); + js.put("text", context); + mp.put("msg_type", "text"); + mp.put("content", js); + ClientPost.executeClient(webhook, httpClient, mp); + } +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/WeComClient.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/WeComClient.java new file mode 100644 index 0000000000..a30e60298c --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/notice/utils/WeComClient.java @@ -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, ListmobileList) { + CloseableHttpClient httpClient = HttpClients.createDefault(); + Map mp = new LinkedHashMap<>(); + Map 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); + } + +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/MessageDetailService.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/MessageDetailService.java new file mode 100644 index 0000000000..3703daee13 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/MessageDetailService.java @@ -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 Listlist + */ + public List searchMessageByTypeAndProjectId(String taskType, String projectId) { + try { + return getMessageDetails(taskType, projectId); + } catch (Exception e) { + LogUtils.error(e.getMessage(), e); + return new ArrayList<>(); + } + } + + private List getMessageDetails(String type, String projectId) { + List messageDetails = new ArrayList<>(); + + //根据项目id找到所有开启的消息通知任务 以及模版配置,机器人配置 + MessageTaskExample example = new MessageTaskExample(); + + example.createCriteria() + .andTaskTypeEqualTo(type) + .andProjectIdEqualTo(projectId).andEnableEqualTo(true); + example.setOrderByClause("create_time asc"); + List 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 messageDetails, List messageTaskLists) { + List messageTaskIds = messageTaskLists.stream().map(MessageTask::getId).toList(); + MessageTaskBlobExample blobExample = new MessageTaskBlobExample(); + blobExample.createCriteria() + .andIdIn(messageTaskIds); + List messageTaskBlobs = messageTaskBlobMapper.selectByExample(blobExample); + Map messageTaskBlobMap = messageTaskBlobs.stream().collect(Collectors.toMap(MessageTaskBlob::getId, item -> item)); + + List robotIds = messageTaskLists.stream().map(MessageTask::getProjectRobotId).distinct().toList(); + ProjectRobotExample projectRobotExample = new ProjectRobotExample(); + projectRobotExample.createCriteria().andIdIn(robotIds).andEnableEqualTo(true); + List projectRobots = projectRobotMapper.selectByExample(projectRobotExample); + Map projectRobotMap = projectRobots.stream().collect(Collectors.toMap(ProjectRobot::getId, item -> item)); + + //消息通知任务以消息类型事件机器人唯一进行分组 + Map> messageTaskGroup = messageTaskLists.stream().collect(Collectors.groupingBy(t -> (t.getTaskType() + t.getEvent() + t.getProjectRobotId()))); + messageTaskGroup.forEach((messageTaskId, messageTaskList) -> { + //获取同一任务所有的接收人 + List 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 + */ + public List searchMessageByTestId(String testId) { + MessageTaskExample example = new MessageTaskExample(); + example.createCriteria().andTestIdEqualTo(testId); + List messageTaskLists = messageTaskMapper.selectByExample(example); + List scheduleMessageTask = new ArrayList<>(); + getMessageDetails(scheduleMessageTask, messageTaskLists); + scheduleMessageTask.sort(Comparator.comparing(MessageDetail::getCreateTime, Comparator.nullsLast(Long::compareTo)).reversed()); + return scheduleMessageTask; + } +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/NoticeSendService.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/NoticeSendService.java new file mode 100644 index 0000000000..4066499bd0 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/NoticeSendService.java @@ -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 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 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 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); + } + } +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/NotificationService.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/NotificationService.java new file mode 100644 index 0000000000..d0deb6c977 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/NotificationService.java @@ -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 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); + } + + + +} diff --git a/backend/framework/sdk/src/main/resources/i18n/project.properties b/backend/framework/sdk/src/main/resources/i18n/project.properties index 611b6b0820..ec5dc6b6ca 100644 --- a/backend/framework/sdk/src/main/resources/i18n/project.properties +++ b/backend/framework/sdk/src/main/resources/i18n/project.properties @@ -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=钉钉机器人的类型不能为空 diff --git a/backend/framework/sdk/src/main/resources/i18n/project_en_US.properties b/backend/framework/sdk/src/main/resources/i18n/project_en_US.properties index 8f81baa4ea..0585ca358c 100644 --- a/backend/framework/sdk/src/main/resources/i18n/project_en_US.properties +++ b/backend/framework/sdk/src/main/resources/i18n/project_en_US.properties @@ -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 diff --git a/backend/framework/sdk/src/main/resources/i18n/project_zh_CN.properties b/backend/framework/sdk/src/main/resources/i18n/project_zh_CN.properties index 40b0a79d8f..2c61f1a1da 100644 --- a/backend/framework/sdk/src/main/resources/i18n/project_zh_CN.properties +++ b/backend/framework/sdk/src/main/resources/i18n/project_zh_CN.properties @@ -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=钉钉机器人的类型不能为空 diff --git a/backend/framework/sdk/src/main/resources/i18n/project_zh_TW.properties b/backend/framework/sdk/src/main/resources/i18n/project_zh_TW.properties index e51553988c..1eff45e1bb 100644 --- a/backend/framework/sdk/src/main/resources/i18n/project_zh_TW.properties +++ b/backend/framework/sdk/src/main/resources/i18n/project_zh_TW.properties @@ -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=釘釘機器人的類型不能為空 diff --git a/backend/framework/sdk/src/main/resources/i18n/system.properties b/backend/framework/sdk/src/main/resources/i18n/system.properties index 018ef6941c..858da293b6 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system.properties @@ -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=通知人 diff --git a/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties b/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties index 67d490d4d7..7f71c4dcdc 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties @@ -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 diff --git a/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties b/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties index c1839cc405..f0ebc78c36 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties @@ -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=通知人 diff --git a/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties b/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties index 8b2684cb16..679d8fa0c2 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties @@ -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=模板 \ No newline at end of file +permission.organization_template.name=模板 +# message +user.remove=已被移除 +alert_others=通知人 \ No newline at end of file diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/controller/NoticeMessageTaskController.java b/backend/services/project-management/src/main/java/io/metersphere/project/controller/NoticeMessageTaskController.java new file mode 100644 index 0000000000..b16bfb124a --- /dev/null +++ b/backend/services/project-management/src/main/java/io/metersphere/project/controller/NoticeMessageTaskController.java @@ -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()); + } + +} + diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/enums/result/ProjectResultCode.java b/backend/services/project-management/src/main/java/io/metersphere/project/enums/result/ProjectResultCode.java new file mode 100644 index 0000000000..53649a51bb --- /dev/null +++ b/backend/services/project-management/src/main/java/io/metersphere/project/enums/result/ProjectResultCode.java @@ -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); + } +} diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/service/CleanupRobotResourceService.java b/backend/services/project-management/src/main/java/io/metersphere/project/service/CleanupRobotResourceService.java index 6e911d9aeb..dcf8cedd28 100644 --- a/backend/services/project-management/src/main/java/io/metersphere/project/service/CleanupRobotResourceService.java +++ b/backend/services/project-management/src/main/java/io/metersphere/project/service/CleanupRobotResourceService.java @@ -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 + "]相关消息机器人报告资源"); } } diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/service/NoticeMessageTaskService.java b/backend/services/project-management/src/main/java/io/metersphere/project/service/NoticeMessageTaskService.java new file mode 100644 index 0000000000..6f4f3b522c --- /dev/null +++ b/backend/services/project-management/src/main/java/io/metersphere/project/service/NoticeMessageTaskService.java @@ -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> 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> checkUserExistProject(List receiverIds, String projectId) { + UserRoleRelationExample userRoleRelationExample = new UserRoleRelationExample(); + userRoleRelationExample.createCriteria().andUserIdIn(receiverIds).andSourceIdEqualTo(projectId); + List userRoleRelations = userRoleRelationMapper.selectByExample(userRoleRelationExample); + List userIds = userRoleRelations.stream().map(UserRoleRelation::getUserId).distinct().toList(); + Map> map = new HashMap<>(); + if (CollectionUtils.isEmpty(userIds)) { + throw new MSException(Translator.get("user.not.exist")); + } + List 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; + } + +} diff --git a/backend/services/project-management/src/test/java/io/metersphere/project/controller/CleanupRobotResourceTests.java b/backend/services/project-management/src/test/java/io/metersphere/project/controller/CleanupRobotResourceTests.java new file mode 100644 index 0000000000..121956705d --- /dev/null +++ b/backend/services/project-management/src/test/java/io/metersphere/project/controller/CleanupRobotResourceTests.java @@ -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 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 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; + } +} diff --git a/backend/services/project-management/src/test/java/io/metersphere/project/controller/NoticeMessageTaskControllerTests.java b/backend/services/project-management/src/test/java/io/metersphere/project/controller/NoticeMessageTaskControllerTests.java new file mode 100644 index 0000000000..2603afdcb2 --- /dev/null +++ b/backend/services/project-management/src/test/java/io/metersphere/project/controller/NoticeMessageTaskControllerTests.java @@ -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 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 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 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()); + } + +} diff --git a/backend/services/project-management/src/test/resources/dml/init_project_message.sql b/backend/services/project-management/src/test/resources/dml/init_project_message.sql new file mode 100644 index 0000000000..f4ce466ef5 --- /dev/null +++ b/backend/services/project-management/src/test/resources/dml/init_project_message.sql @@ -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); + + + + diff --git a/backend/services/project-management/src/test/resources/dml/init_project_robot.sql b/backend/services/project-management/src/test/resources/dml/init_project_robot.sql new file mode 100644 index 0000000000..c9c4db9c36 --- /dev/null +++ b/backend/services/project-management/src/test/resources/dml/init_project_robot.sql @@ -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); + + + +