增加构建通知

This commit is contained in:
dengqichen 2025-11-12 14:23:57 +08:00
parent 8e860443d2
commit 0de08cb09b
17 changed files with 413 additions and 230 deletions

View File

@ -18,7 +18,9 @@ import com.qqchen.deploy.backend.deploy.service.IJenkinsSyncHistoryService;
import com.qqchen.deploy.backend.notification.entity.NotificationChannel; import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository; import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
import com.qqchen.deploy.backend.notification.service.INotificationSendService; import com.qqchen.deploy.backend.notification.service.INotificationSendService;
import com.qqchen.deploy.backend.notification.dto.NotificationRequest; import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest;
import com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum;
import com.qqchen.deploy.backend.deploy.dto.sync.JenkinsSyncContext; import com.qqchen.deploy.backend.deploy.dto.sync.JenkinsSyncContext;
import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException; import com.qqchen.deploy.backend.framework.exception.BusinessException;
@ -640,27 +642,15 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
// 构建失败时发送日志文件如果开启 // 构建失败时发送日志文件如果开启
if ("FAILURE".equals(status) && config.getBuildFailureFileEnabled() != null && config.getBuildFailureFileEnabled()) { if ("FAILURE".equals(status) && config.getBuildFailureFileEnabled() != null && config.getBuildFailureFileEnabled()) {
// 先发送文本通知 // 先发送文本通知
NotificationRequest textRequest = NotificationRequest.builder() sendNotificationByChannelType(channel, "Jenkins构建通知", content.toString());
.channelId(channel.getId())
.title("Jenkins构建通知")
.content(content.toString())
.build();
notificationSendService.send(textRequest);
// 然后发送日志文件 // 然后发送日志文件
sendBuildFailureLogFile(externalSystem, channel, job.getJobName(), build.getBuildNumber()); sendBuildFailureLogFile(externalSystem, channel, job.getJobName(), build.getBuildNumber());
return; // 已发送直接返回 return; // 已发送直接返回
} }
// 构建通知请求
NotificationRequest request = NotificationRequest.builder()
.channelId(channel.getId())
.title("Jenkins构建通知")
.content(content.toString())
.build();
// 发送通知 // 发送通知
notificationSendService.send(request); sendNotificationByChannelType(channel, "Jenkins构建通知", content.toString());
log.info("已发送构建通知: job={}, build={}, status={}", log.info("已发送构建通知: job={}, build={}, status={}",
job.getJobName(), build.getBuildNumber(), status); job.getJobName(), build.getBuildNumber(), status);
@ -670,6 +660,37 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
} }
} }
/**
* 根据渠道类型发送通知
*/
private void sendNotificationByChannelType(NotificationChannel channel, String title, String content) {
try {
switch (channel.getChannelType()) {
case WEWORK -> {
WeworkSendNotificationRequest weworkRequest = new WeworkSendNotificationRequest();
weworkRequest.setChannelId(channel.getId());
weworkRequest.setContent(title + "\n\n" + content);
weworkRequest.setMessageType(WeworkMessageTypeEnum.MARKDOWN); // 使用Markdown格式
notificationSendService.send(weworkRequest);
}
case EMAIL -> {
EmailSendNotificationRequest emailRequest = new EmailSendNotificationRequest();
emailRequest.setChannelId(channel.getId());
emailRequest.setSubject(title);
emailRequest.setContent(content);
// 这里需要设置收件人但Jenkins构建通知中没有提供
// 实际应该从团队环境配置或其他地方获取
emailRequest.setToReceivers(java.util.Arrays.asList("admin@company.com"));
notificationSendService.send(emailRequest);
}
default -> throw new RuntimeException("不支持的渠道类型: " + channel.getChannelType());
}
} catch (Exception e) {
log.error("发送通知失败: channelId={}, type={}", channel.getId(), channel.getChannelType(), e);
throw e;
}
}
/** /**
* 发送构建失败日志文件到企业微信 * 发送构建失败日志文件到企业微信
*/ */

View File

@ -1,27 +1,28 @@
package com.qqchen.deploy.backend.notification.adapter; package com.qqchen.deploy.backend.notification.adapter;
import com.qqchen.deploy.backend.notification.entity.config.BaseNotificationConfig; import com.qqchen.deploy.backend.notification.entity.config.BaseNotificationConfig;
import com.qqchen.deploy.backend.notification.dto.NotificationRequest; import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum; import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
/** /**
* 通知渠道适配器接口 * 通知渠道适配器接口
* 使用泛型约束配置类型避免类型转换 * 使用泛型约束配置类型和请求类型避免类型转换
* *
* @param <T> 配置类型必须继承 BaseNotificationConfig * @param <C> 配置类型必须继承 BaseNotificationConfig
* @param <R> 请求类型必须继承 BaseSendNotificationRequest
* @author qqchen * @author qqchen
* @since 2025-11-03 * @since 2025-11-03
*/ */
public interface INotificationChannelAdapter<T extends BaseNotificationConfig> { public interface INotificationChannelAdapter<C extends BaseNotificationConfig, R extends BaseSendNotificationRequest> {
/** /**
* 发送通知 * 发送通知
* *
* @param config 渠道配置 * @param config 渠道配置
* @param request 通知请求 * @param request 发送通知请求
* @throws Exception 发送失败时抛出异常 * @throws Exception 发送失败时抛出异常
*/ */
void send(T config, NotificationRequest request) throws Exception; void send(C config, R request) throws Exception;
/** /**
* 发送文件可选不是所有渠道都支持 * 发送文件可选不是所有渠道都支持
@ -31,7 +32,7 @@ public interface INotificationChannelAdapter<T extends BaseNotificationConfig> {
* @param fileName 文件名称 * @param fileName 文件名称
* @throws Exception 发送失败时抛出异常 * @throws Exception 发送失败时抛出异常
*/ */
default void sendFile(T config, String filePath, String fileName) throws Exception { default void sendFile(C config, String filePath, String fileName) throws Exception {
throw new UnsupportedOperationException("该渠道不支持发送文件"); throw new UnsupportedOperationException("该渠道不支持发送文件");
} }
@ -48,7 +49,7 @@ public interface INotificationChannelAdapter<T extends BaseNotificationConfig> {
* @param config 渠道配置 * @param config 渠道配置
* @return 校验结果消息 * @return 校验结果消息
*/ */
default String validateConfig(T config) { default String validateConfig(C config) {
return "配置有效"; return "配置有效";
} }
@ -59,6 +60,6 @@ public interface INotificationChannelAdapter<T extends BaseNotificationConfig> {
* @param config 渠道配置 * @param config 渠道配置
* @throws Exception 测试失败时抛出异常 * @throws Exception 测试失败时抛出异常
*/ */
void testConnection(T config) throws Exception; void testConnection(C config) throws Exception;
} }

View File

@ -3,7 +3,7 @@ package com.qqchen.deploy.backend.notification.adapter.impl;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.notification.adapter.INotificationChannelAdapter; import com.qqchen.deploy.backend.notification.adapter.INotificationChannelAdapter;
import com.qqchen.deploy.backend.notification.entity.config.EmailNotificationConfig; import com.qqchen.deploy.backend.notification.entity.config.EmailNotificationConfig;
import com.qqchen.deploy.backend.notification.dto.NotificationRequest; import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum; import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
@ -24,23 +24,21 @@ import java.util.Properties;
*/ */
@Slf4j @Slf4j
@Component @Component
public class EmailChannelAdapter implements INotificationChannelAdapter<EmailNotificationConfig> { public class EmailChannelAdapter implements INotificationChannelAdapter<EmailNotificationConfig, EmailSendNotificationRequest> {
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
@Override @Override
public void send(EmailNotificationConfig emailConfig, NotificationRequest request) throws Exception { public void send(EmailNotificationConfig emailConfig, EmailSendNotificationRequest request) throws Exception {
// 1. 校验配置 // 1. 校验配置
validateEmailConfig(emailConfig); validateEmailConfig(emailConfig);
// 2. 创建JavaMailSender // 2. 创建JavaMailSender
JavaMailSenderImpl mailSender = createMailSender(emailConfig); JavaMailSenderImpl mailSender = createMailSender(emailConfig);
// 3. 确定收件人 // 3. 校验收件人
List<String> receivers = determineReceivers(emailConfig, request); if (CollectionUtils.isEmpty(request.getToReceivers())) {
throw new IllegalArgumentException("收件人列表不能为空");
if (CollectionUtils.isEmpty(receivers)) {
throw new IllegalArgumentException("收件人列表为空,且未配置默认收件人");
} }
// 4. 构建邮件 // 4. 构建邮件
@ -55,24 +53,26 @@ public class EmailChannelAdapter implements INotificationChannelAdapter<EmailNot
} }
// 设置收件人 // 设置收件人
helper.setTo(receivers.toArray(new String[0])); helper.setTo(request.getToReceivers().toArray(new String[0]));
// 设置抄送 // 设置抄送
if (!CollectionUtils.isEmpty(request.getMentions())) { if (!CollectionUtils.isEmpty(request.getCcReceivers())) {
helper.setCc(request.getMentions().toArray(new String[0])); helper.setCc(request.getCcReceivers().toArray(new String[0]));
}
// 设置密送
if (!CollectionUtils.isEmpty(request.getBccReceivers())) {
helper.setBcc(request.getBccReceivers().toArray(new String[0]));
} }
// 设置主题 // 设置主题
String subject = request.getTitle() != null && !request.getTitle().isEmpty() helper.setSubject(request.getSubject());
? request.getTitle()
: "系统通知";
helper.setSubject(subject);
// 设置内容支持HTML // 设置内容支持HTML
helper.setText(request.getContent(), false); helper.setText(request.getContent(), request.getIsHtml() != null ? request.getIsHtml() : false);
// 5. 发送邮件 // 5. 发送邮件
log.info("发送邮件通知 - 收件人: {}, 主题: {}", receivers, subject); log.info("发送邮件通知 - 收件人: {}, 主题: {}", request.getToReceivers(), request.getSubject());
mailSender.send(mimeMessage); mailSender.send(mimeMessage);
log.info("邮件通知发送成功"); log.info("邮件通知发送成功");
} }
@ -112,40 +112,8 @@ public class EmailChannelAdapter implements INotificationChannelAdapter<EmailNot
throw new Exception("SMTP连接失败: " + e.getMessage(), e); throw new Exception("SMTP连接失败: " + e.getMessage(), e);
} }
// 5. 发送测试邮件可选如果配置了默认收件人 // 5. 测试连接成功不发送测试邮件因为没有默认收件人
if (!CollectionUtils.isEmpty(emailConfig.getDefaultReceivers())) { log.info("邮件通知渠道配置测试成功 - SMTP连接正常");
try {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
// 设置发件人
if (emailConfig.getFromName() != null && !emailConfig.getFromName().isEmpty()) {
helper.setFrom(new InternetAddress(emailConfig.getFrom(), emailConfig.getFromName(), "UTF-8"));
} else {
helper.setFrom(emailConfig.getFrom());
}
// 设置收件人使用配置的默认收件人
helper.setTo(emailConfig.getDefaultReceivers().toArray(new String[0]));
// 设置主题
helper.setSubject("【测试邮件】Deploy Ease Platform 邮件通知测试");
// 设置内容
String content = "这是一条来自 Deploy Ease Platform 的测试邮件。\n\n" +
"如果您收到此邮件,说明邮件通知渠道配置正常!\n\n" +
"发送时间:" + java.time.LocalDateTime.now();
helper.setText(content, false);
// 发送邮件
log.info("发送测试邮件到: {}", emailConfig.getDefaultReceivers());
mailSender.send(mimeMessage);
log.info("测试邮件发送成功");
} catch (Exception e) {
log.error("测试邮件发送失败: {}", e.getMessage());
throw new Exception("测试邮件发送失败: " + e.getMessage(), e);
}
}
} }
/** /**
@ -199,17 +167,5 @@ public class EmailChannelAdapter implements INotificationChannelAdapter<EmailNot
return mailSender; return mailSender;
} }
/**
* 确定收件人列表
*/
private List<String> determineReceivers(EmailNotificationConfig config, NotificationRequest request) {
// 优先使用请求中的收件人
if (!CollectionUtils.isEmpty(request.getReceivers())) {
return request.getReceivers();
}
// 使用配置中的默认收件人
return config.getDefaultReceivers();
}
} }

View File

@ -3,9 +3,10 @@ package com.qqchen.deploy.backend.notification.adapter.impl;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.utils.JsonUtils; import com.qqchen.deploy.backend.framework.utils.JsonUtils;
import com.qqchen.deploy.backend.notification.adapter.INotificationChannelAdapter; import com.qqchen.deploy.backend.notification.adapter.INotificationChannelAdapter;
import com.qqchen.deploy.backend.notification.dto.NotificationRequest; import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
import com.qqchen.deploy.backend.notification.entity.config.WeworkNotificationConfig; import com.qqchen.deploy.backend.notification.entity.config.WeworkNotificationConfig;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum; import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
import com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
@ -34,7 +35,7 @@ import java.util.Map;
*/ */
@Slf4j @Slf4j
@Component @Component
public class WeworkChannelAdapter implements INotificationChannelAdapter<WeworkNotificationConfig> { public class WeworkChannelAdapter implements INotificationChannelAdapter<WeworkNotificationConfig, WeworkSendNotificationRequest> {
private static final String BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook"; private static final String BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook";
@ -42,59 +43,61 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter<WeworkN
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
@Override @Override
public void send(WeworkNotificationConfig config, NotificationRequest request) throws Exception { public void send(WeworkNotificationConfig config, WeworkSendNotificationRequest request) throws Exception {
// 1. 校验配置 // 1. 校验配置
if (config.getKey() == null || config.getKey().isEmpty()) { if (config.getKey() == null || config.getKey().isEmpty()) {
throw new IllegalArgumentException("企业微信 Webhook Key 未配置"); throw new IllegalArgumentException("企业微信 Webhook Key 未配置");
} }
// 2. 构建发送消息 URL // 2. 根据消息类型处理
switch (request.getMessageType()) {
case FILE -> sendFileMessage(config, request);
case TEXT, MARKDOWN -> sendTextOrMarkdownMessage(config, request);
}
}
/**
* 发送文本或Markdown消息
*/
private void sendTextOrMarkdownMessage(WeworkNotificationConfig config, WeworkSendNotificationRequest request) throws Exception {
// 构建发送消息 URL
String webhookUrl = BASE_URL + "/send?key=" + config.getKey(); String webhookUrl = BASE_URL + "/send?key=" + config.getKey();
// 2. 构建消息内容 // 构建企业微信消息体
String message = buildMessage(request);
// 3. 构建@人列表
List<String> mentionedList = buildMentionedList(config, request);
List<String> mentionedMobileList = buildMentionedMobileList(config, request);
// 4. 构建企业微信消息体
Map<String, Object> messageBody = new HashMap<>(); Map<String, Object> messageBody = new HashMap<>();
String msgType = request.getMessageType().getApiType();
// 智能判断消息类型如果包含 Markdown 标签使用 markdown 类型
boolean isMarkdown = isMarkdownMessage(message);
String msgType = isMarkdown ? "markdown" : "text";
messageBody.put("msgtype", msgType); messageBody.put("msgtype", msgType);
if (isMarkdown) { if (request.getMessageType() == WeworkMessageTypeEnum.MARKDOWN) {
// Markdown 格式消息 // Markdown 格式消息
Map<String, Object> markdownContent = new HashMap<>(); Map<String, Object> markdownContent = new HashMap<>();
markdownContent.put("content", message); markdownContent.put("content", request.getContent());
messageBody.put("markdown", markdownContent); messageBody.put("markdown", markdownContent);
} else { } else {
// 纯文本消息 // 纯文本消息
Map<String, Object> textContent = new HashMap<>(); Map<String, Object> textContent = new HashMap<>();
textContent.put("content", message); textContent.put("content", request.getContent());
if (!CollectionUtils.isEmpty(mentionedList)) { // 添加@人列表
textContent.put("mentioned_list", mentionedList); if (!CollectionUtils.isEmpty(request.getMentionedUserList())) {
textContent.put("mentioned_list", request.getMentionedUserList());
} }
if (!CollectionUtils.isEmpty(mentionedMobileList)) { if (!CollectionUtils.isEmpty(request.getMentionedMobileList())) {
textContent.put("mentioned_mobile_list", mentionedMobileList); textContent.put("mentioned_mobile_list", request.getMentionedMobileList());
} }
messageBody.put("text", textContent); messageBody.put("text", textContent);
} }
// 5. 发送请求 // 发送请求
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON); headers.setContentType(MediaType.APPLICATION_JSON);
String jsonBody = objectMapper.writeValueAsString(messageBody); String jsonBody = objectMapper.writeValueAsString(messageBody);
HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers); HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers);
log.info("发送企业微信通知 - URL: {}, 类型: {}, 消息: {}", webhookUrl, msgType, message); log.info("发送企业微信通知 - URL: {}, 类型: {}, 消息: {}", webhookUrl, msgType, request.getContent());
String response = restTemplate.exchange( String response = restTemplate.exchange(
webhookUrl, webhookUrl,
@ -106,6 +109,44 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter<WeworkN
log.info("企业微信通知发送成功 - 响应: {}", response); log.info("企业微信通知发送成功 - 响应: {}", response);
} }
/**
* 发送文件消息
*/
private void sendFileMessage(WeworkNotificationConfig config, WeworkSendNotificationRequest request) throws Exception {
if (request.getFilePath() == null || request.getFilePath().isEmpty()) {
throw new IllegalArgumentException("文件路径不能为空");
}
// 1. 上传文件获取 media_id
String uploadUrl = BASE_URL + "/upload_media?key=" + config.getKey() + "&type=file";
String mediaId = uploadFile(uploadUrl, request.getFilePath());
if (mediaId == null) {
throw new RuntimeException("文件上传失败,未获取到 media_id");
}
// 2. 发送文件消息
String sendUrl = BASE_URL + "/send?key=" + config.getKey();
Map<String, Object> messageBody = buildFileMessage(mediaId);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String jsonBody = objectMapper.writeValueAsString(messageBody);
HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers);
log.info("发送企业微信文件消息 - 文件: {}", request.getFileName());
String response = restTemplate.exchange(
sendUrl,
HttpMethod.POST,
entity,
String.class
).getBody();
log.info("企业微信文件消息发送成功 - 响应: {}", response);
}
@Override @Override
public NotificationChannelTypeEnum supportedType() { public NotificationChannelTypeEnum supportedType() {
return NotificationChannelTypeEnum.WEWORK; return NotificationChannelTypeEnum.WEWORK;
@ -206,46 +247,6 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter<WeworkN
} }
} }
/**
* 构建消息内容
*/
private String buildMessage(NotificationRequest request) {
if (request.getTitle() != null && !request.getTitle().isEmpty()) {
return request.getTitle() + "\n" + request.getContent();
}
return request.getContent();
}
/**
* 构建@人列表userid
*/
private List<String> buildMentionedList(WeworkNotificationConfig config, NotificationRequest request) {
List<String> mentionedList = new ArrayList<>();
// 优先使用请求中的mentions
if (!CollectionUtils.isEmpty(request.getMentions())) {
mentionedList.addAll(request.getMentions());
} else if (!CollectionUtils.isEmpty(config.getMentionedList())) {
// 使用配置中的默认值
mentionedList.addAll(config.getMentionedList());
}
return mentionedList;
}
/**
* 构建@人列表手机号
*/
private List<String> buildMentionedMobileList(WeworkNotificationConfig config, NotificationRequest request) {
List<String> mentionedMobileList = new ArrayList<>();
// 使用配置中的默认手机号
if (!CollectionUtils.isEmpty(config.getMentionedMobileList())) {
mentionedMobileList.addAll(config.getMentionedMobileList());
}
return mentionedMobileList;
}
/** /**
* 判断是否是 Markdown 格式消息 * 判断是否是 Markdown 格式消息

View File

@ -4,7 +4,7 @@ import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.controller.BaseController; import com.qqchen.deploy.backend.framework.controller.BaseController;
import com.qqchen.deploy.backend.notification.dto.NotificationChannelDTO; import com.qqchen.deploy.backend.notification.dto.NotificationChannelDTO;
import com.qqchen.deploy.backend.notification.dto.NotificationChannelQuery; import com.qqchen.deploy.backend.notification.dto.NotificationChannelQuery;
import com.qqchen.deploy.backend.notification.dto.NotificationRequest; import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest;
import com.qqchen.deploy.backend.notification.entity.NotificationChannel; import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum; import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
import com.qqchen.deploy.backend.notification.service.INotificationChannelService; import com.qqchen.deploy.backend.notification.service.INotificationChannelService;
@ -41,7 +41,7 @@ public class NotificationChannelApiController extends BaseController<Notificatio
@Resource @Resource
private INotificationChannelService notificationChannelService; private INotificationChannelService notificationChannelService;
@Resource @Resource
private INotificationSendService notificationSendService; private INotificationSendService notificationSendService;
@ -51,7 +51,7 @@ public class NotificationChannelApiController extends BaseController<Notificatio
} }
@Override @Override
public Response<NotificationChannelDTO> update(@PathVariable Long id,@Validated @RequestBody NotificationChannelDTO dto) { public Response<NotificationChannelDTO> update(@PathVariable Long id, @Validated @RequestBody NotificationChannelDTO dto) {
return super.update(id, dto); return super.update(id, dto);
} }
@ -127,11 +127,11 @@ public class NotificationChannelApiController extends BaseController<Notificatio
notificationChannelService.disable(id); notificationChannelService.disable(id);
return Response.success(); return Response.success();
} }
@Operation(summary = "发送通知消息") @Operation(summary = "发送通知消息")
@PostMapping("/send") @PostMapping("/send")
public Response<Void> send( public Response<Void> send(
@Parameter(description = "通知请求", required = true) @RequestBody NotificationRequest request @Parameter(description = "通知请求", required = true) @RequestBody BaseSendNotificationRequest request
) { ) {
notificationSendService.send(request); notificationSendService.send(request);
return Response.success(); return Response.success();

View File

@ -8,6 +8,9 @@ import lombok.Data;
/** /**
* 通知配置DTO基类 * 通知配置DTO基类
* 用于数据传输层与Entity层的BaseNotificationConfig对应 * 用于数据传输层与Entity层的BaseNotificationConfig对应
*
* 使用 EXISTING_PROPERTY 方式利用外层 NotificationChannelDTO channelType 字段
* 进行多态反序列化无需在 config 内部添加额外的 type 字段
* *
* @author qqchen * @author qqchen
* @since 2025-11-12 * @since 2025-11-12
@ -15,7 +18,7 @@ import lombok.Data;
@Data @Data
@JsonTypeInfo( @JsonTypeInfo(
use = JsonTypeInfo.Id.NAME, use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY, include = JsonTypeInfo.As.PROPERTY,
property = "channelType" property = "channelType"
) )
@JsonSubTypes({ @JsonSubTypes({
@ -25,7 +28,7 @@ import lombok.Data;
public abstract class BaseNotificationConfigDTO { public abstract class BaseNotificationConfigDTO {
/** /**
* 获取渠道类型 * 获取渠道类型与外层 NotificationChannelDTO.channelType 保持一致
*/ */
public abstract NotificationChannelTypeEnum getChannelType(); public abstract NotificationChannelTypeEnum getChannelType();
} }

View File

@ -0,0 +1,45 @@
package com.qqchen.deploy.backend.notification.dto;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 发送通知请求基类
* 不同渠道有不同的发送参数使用多态设计
*
* @author qqchen
* @since 2025-11-12
*/
@Data
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "channelType"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = WeworkSendNotificationRequest.class, name = "WEWORK"),
@JsonSubTypes.Type(value = EmailSendNotificationRequest.class, name = "EMAIL")
})
public abstract class BaseSendNotificationRequest {
/**
* 通知渠道ID必填
*/
@NotNull(message = "渠道ID不能为空")
private Long channelId;
/**
* 消息内容必填
*/
@NotBlank(message = "消息内容不能为空")
private String content;
/**
* 获取渠道类型
*/
public abstract NotificationChannelTypeEnum getChannelType();
}

View File

@ -16,6 +16,11 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class EmailNotificationConfigDTO extends BaseNotificationConfigDTO { public class EmailNotificationConfigDTO extends BaseNotificationConfigDTO {
/**
* 渠道类型用于Jackson反序列化与外层保持一致
*/
private final NotificationChannelTypeEnum channelType = NotificationChannelTypeEnum.EMAIL;
/** /**
* SMTP服务器地址必填 * SMTP服务器地址必填
*/ */
@ -46,11 +51,6 @@ public class EmailNotificationConfigDTO extends BaseNotificationConfigDTO {
*/ */
private String fromName; private String fromName;
/**
* 默认收件人列表可选
*/
private List<String> defaultReceivers;
/** /**
* 是否使用SSL可选默认true * 是否使用SSL可选默认true
*/ */

View File

@ -0,0 +1,56 @@
package com.qqchen.deploy.backend.notification.dto;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 邮件发送通知请求
*
* @author qqchen
* @since 2025-11-12
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class EmailSendNotificationRequest extends BaseSendNotificationRequest {
/**
* 渠道类型用于Jackson反序列化
*/
private final NotificationChannelTypeEnum channelType = NotificationChannelTypeEnum.EMAIL;
/**
* 邮件主题必填
*/
@NotEmpty(message = "邮件主题不能为空")
private String subject;
/**
* 收件人列表必填
*/
@NotEmpty(message = "收件人不能为空")
private List<String> toReceivers;
/**
* 抄送列表可选
*/
private List<String> ccReceivers;
/**
* 密送列表可选
*/
private List<String> bccReceivers;
/**
* 是否HTML格式可选默认false
*/
private Boolean isHtml = false;
@Override
public NotificationChannelTypeEnum getChannelType() {
return NotificationChannelTypeEnum.EMAIL;
}
}

View File

@ -15,24 +15,18 @@ import java.util.List;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class WeworkNotificationConfigDTO extends BaseNotificationConfigDTO { public class WeworkNotificationConfigDTO extends BaseNotificationConfigDTO {
/**
* 渠道类型用于Jackson反序列化与外层保持一致
*/
private final NotificationChannelTypeEnum channelType = NotificationChannelTypeEnum.WEWORK;
/** /**
* 企业微信机器人 Webhook Key必填 * 企业微信机器人 Webhook Key必填
* 完整URL格式: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key} * 完整URL格式: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}
*/ */
private String key; private String key;
/**
* 默认@的手机号列表可选
*/
private List<String> mentionedMobileList;
/**
* 默认@的用户列表可选
* 例如["@all"] 表示@所有人
*/
private List<String> mentionedList;
@Override @Override
public NotificationChannelTypeEnum getChannelType() { public NotificationChannelTypeEnum getChannelType() {
return NotificationChannelTypeEnum.WEWORK; return NotificationChannelTypeEnum.WEWORK;

View File

@ -0,0 +1,59 @@
package com.qqchen.deploy.backend.notification.dto;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
import com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 企业微信发送通知请求
*
* @author qqchen
* @since 2025-11-12
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WeworkSendNotificationRequest extends BaseSendNotificationRequest {
/**
* 渠道类型用于Jackson反序列化
*/
private final NotificationChannelTypeEnum channelType = NotificationChannelTypeEnum.WEWORK;
/**
* 消息类型必填
* - TEXT: 普通文本消息
* - MARKDOWN: Markdown格式消息
* - FILE: 文件消息
*/
private WeworkMessageTypeEnum messageType = WeworkMessageTypeEnum.TEXT;
/**
* @的手机号列表可选 例如["13800138000", "13900139000"]
*/
private List<String> mentionedMobileList;
/**
* @的用户ID列表可选 例如["@all"] 表示@所有人或具体的userid
*/
private List<String> mentionedUserList;
/**
* 文件路径仅当messageType为FILE时使用
* 服务器本地文件路径用于文件上传
*/
private String filePath;
/**
* 文件名称仅当messageType为FILE时使用
* 显示给用户的文件名
*/
private String fileName;
@Override
public NotificationChannelTypeEnum getChannelType() {
return NotificationChannelTypeEnum.WEWORK;
}
}

View File

@ -46,11 +46,6 @@ public class EmailNotificationConfig extends BaseNotificationConfig {
*/ */
private String fromName; private String fromName;
/**
* 默认收件人列表可选
*/
private List<String> defaultReceivers;
/** /**
* 是否使用SSL可选默认true * 是否使用SSL可选默认true
*/ */

View File

@ -22,17 +22,6 @@ public class WeworkNotificationConfig extends BaseNotificationConfig {
*/ */
private String key; private String key;
/**
* 默认@的手机号列表可选
*/
private List<String> mentionedMobileList;
/**
* 默认@的用户列表可选
* 例如["@all"] 表示@所有人
*/
private List<String> mentionedList;
@Override @Override
public NotificationChannelTypeEnum getChannelType() { public NotificationChannelTypeEnum getChannelType() {
return NotificationChannelTypeEnum.WEWORK; return NotificationChannelTypeEnum.WEWORK;

View File

@ -0,0 +1,40 @@
package com.qqchen.deploy.backend.notification.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 企业微信消息类型枚举
*
* @author qqchen
* @since 2025-11-12
*/
@Getter
@AllArgsConstructor
public enum WeworkMessageTypeEnum {
/**
* 文本消息
*/
TEXT("text", "文本消息"),
/**
* Markdown消息
*/
MARKDOWN("markdown", "Markdown消息"),
/**
* 文件消息
*/
FILE("file", "文件消息");
/**
* 企业微信API中的消息类型标识
*/
private final String apiType;
/**
* 消息类型描述
*/
private final String description;
}

View File

@ -1,6 +1,6 @@
package com.qqchen.deploy.backend.notification.service; package com.qqchen.deploy.backend.notification.service;
import com.qqchen.deploy.backend.notification.dto.NotificationRequest; import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest;
/** /**
* 通知发送服务接口 * 通知发送服务接口
@ -16,36 +16,9 @@ public interface INotificationSendService {
* @param request 通知请求 * @param request 通知请求
* @throws com.qqchen.deploy.backend.framework.exception.BusinessException 渠道不存在渠道已禁用发送失败 * @throws com.qqchen.deploy.backend.framework.exception.BusinessException 渠道不存在渠道已禁用发送失败
*/ */
void send(NotificationRequest request); void send(BaseSendNotificationRequest request);
/** // TODO: 便捷方法需要重新设计因为现在需要指定具体的请求类型
* 便捷方法发送简单文本通知 // 暂时注释掉后续根据需要添加
*
* @param channelId 渠道ID
* @param content 消息内容
*/
default void sendSimple(Long channelId, String content) {
NotificationRequest request = NotificationRequest.builder()
.channelId(channelId)
.content(content)
.build();
send(request);
}
/**
* 便捷方法发送带标题的通知
*
* @param channelId 渠道ID
* @param title 标题
* @param content 内容
*/
default void send(Long channelId, String title, String content) {
NotificationRequest request = NotificationRequest.builder()
.channelId(channelId)
.title(title)
.content(content)
.build();
send(request);
}
} }

View File

@ -12,7 +12,7 @@ import com.qqchen.deploy.backend.notification.entity.config.BaseNotificationConf
import com.qqchen.deploy.backend.notification.entity.config.EmailNotificationConfig; import com.qqchen.deploy.backend.notification.entity.config.EmailNotificationConfig;
import com.qqchen.deploy.backend.notification.dto.NotificationChannelDTO; import com.qqchen.deploy.backend.notification.dto.NotificationChannelDTO;
import com.qqchen.deploy.backend.notification.dto.NotificationChannelQuery; import com.qqchen.deploy.backend.notification.dto.NotificationChannelQuery;
import com.qqchen.deploy.backend.notification.dto.NotificationRequest; import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest;
import com.qqchen.deploy.backend.notification.entity.config.WeworkNotificationConfig; import com.qqchen.deploy.backend.notification.entity.config.WeworkNotificationConfig;
import com.qqchen.deploy.backend.notification.entity.NotificationChannel; import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelStatusEnum; import com.qqchen.deploy.backend.notification.enums.NotificationChannelStatusEnum;
@ -115,8 +115,7 @@ public class NotificationChannelServiceImpl
log.info("禁用通知渠道: id={}, name={}", id, channel.getName()); log.info("禁用通知渠道: id={}, name={}", id, channel.getName());
} }
@Override public void send(BaseSendNotificationRequest request) {
public void send(NotificationRequest request) {
// 1. 参数校验 // 1. 参数校验
if (request == null || request.getChannelId() == null) { if (request == null || request.getChannelId() == null) {
throw new BusinessException(ResponseCode.INVALID_PARAM); throw new BusinessException(ResponseCode.INVALID_PARAM);
@ -149,8 +148,8 @@ public class NotificationChannelServiceImpl
// 6. 发送通知 // 6. 发送通知
try { try {
log.info("发送通知 - 渠道ID: {}, 渠道类型: {}, 标题: {}", log.info("发送通知 - 渠道ID: {}, 渠道类型: {}, 内容: {}",
channel.getId(), channel.getChannelType(), request.getTitle()); channel.getId(), channel.getChannelType(), request.getContent());
adapter.send(config, request); adapter.send(config, request);

View File

@ -1,5 +1,11 @@
package com.qqchen.deploy.backend.workflow.delegate; package com.qqchen.deploy.backend.workflow.delegate;
import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
import com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum;
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
import com.qqchen.deploy.backend.notification.service.INotificationSendService; import com.qqchen.deploy.backend.notification.service.INotificationSendService;
import com.qqchen.deploy.backend.workflow.dto.inputmapping.NotificationInputMapping; import com.qqchen.deploy.backend.workflow.dto.inputmapping.NotificationInputMapping;
import com.qqchen.deploy.backend.workflow.dto.outputs.NotificationOutputs; import com.qqchen.deploy.backend.workflow.dto.outputs.NotificationOutputs;
@ -9,6 +15,7 @@ import org.apache.commons.lang3.StringUtils;
import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Map; import java.util.Map;
/** /**
@ -23,6 +30,9 @@ public class NotificationNodeDelegate extends BaseNodeDelegate<NotificationInput
@Resource @Resource
private INotificationSendService notificationSendService; private INotificationSendService notificationSendService;
@Resource
private INotificationChannelRepository notificationChannelRepository;
@Override @Override
protected void executeInternal(DelegateExecution execution, Map<String, Object> configs, NotificationInputMapping input) { protected void executeInternal(DelegateExecution execution, Map<String, Object> configs, NotificationInputMapping input) {
@ -30,6 +40,47 @@ public class NotificationNodeDelegate extends BaseNodeDelegate<NotificationInput
logError(String.format("Notification delegate parameter verification failed %s %s %s", input.getChannelId(), input.getTitle(), input.getContent())); logError(String.format("Notification delegate parameter verification failed %s %s %s", input.getChannelId(), input.getTitle(), input.getContent()));
return; return;
} }
notificationSendService.send(input.getChannelId(), input.getTitle(), input.getContent()); try {
// 1. 查询渠道信息
NotificationChannel channel = notificationChannelRepository.findById(input.getChannelId())
.orElseThrow(() -> new RuntimeException("通知渠道不存在: " + input.getChannelId()));
// 2. 根据渠道类型构建对应的请求对象
switch (channel.getChannelType()) {
case WEWORK -> {
WeworkSendNotificationRequest weworkRequest = new WeworkSendNotificationRequest();
weworkRequest.setChannelId(input.getChannelId());
weworkRequest.setContent(buildWeworkContent(input.getTitle(), input.getContent()));
weworkRequest.setMessageType(WeworkMessageTypeEnum.TEXT);
notificationSendService.send(weworkRequest);
}
case EMAIL -> {
EmailSendNotificationRequest emailRequest = new EmailSendNotificationRequest();
emailRequest.setChannelId(input.getChannelId());
emailRequest.setSubject(input.getTitle());
emailRequest.setContent(input.getContent());
// 这里需要设置收件人但工作流中没有提供需要从其他地方获取
// 暂时使用一个默认值实际应该从工作流变量或配置中获取
emailRequest.setToReceivers(Arrays.asList("admin@company.com"));
notificationSendService.send(emailRequest);
}
default -> throw new RuntimeException("不支持的渠道类型: " + channel.getChannelType());
}
log.info("工作流通知发送成功 - 渠道ID: {}, 类型: {}", input.getChannelId(), channel.getChannelType());
} catch (Exception e) {
logError("工作流通知发送失败: " + e.getMessage());
throw new RuntimeException("通知发送失败", e);
}
}
/**
* 构建企业微信消息内容标题+内容
*/
private String buildWeworkContent(String title, String content) {
if (StringUtils.isNotEmpty(title)) {
return title + "\n\n" + content;
}
return content;
} }
} }