增加构建通知
This commit is contained in:
parent
5a924f7aa5
commit
adf83458a5
@ -1,26 +1,39 @@
|
|||||||
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.dto.NotificationRequest;
|
import com.qqchen.deploy.backend.notification.dto.NotificationRequest;
|
||||||
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
|
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知渠道适配器接口
|
* 通知渠道适配器接口
|
||||||
|
* 使用泛型约束配置类型,避免类型转换
|
||||||
*
|
*
|
||||||
|
* @param <T> 配置类型,必须继承 BaseNotificationConfig
|
||||||
* @author qqchen
|
* @author qqchen
|
||||||
* @since 2025-11-03
|
* @since 2025-11-03
|
||||||
*/
|
*/
|
||||||
public interface INotificationChannelAdapter {
|
public interface INotificationChannelAdapter<T extends BaseNotificationConfig> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送通知
|
* 发送通知
|
||||||
*
|
*
|
||||||
* @param config 渠道配置(从数据库config字段解析)
|
* @param config 渠道配置
|
||||||
* @param request 通知请求
|
* @param request 通知请求
|
||||||
* @throws Exception 发送失败时抛出异常
|
* @throws Exception 发送失败时抛出异常
|
||||||
*/
|
*/
|
||||||
void send(Map<String, Object> config, NotificationRequest request) throws Exception;
|
void send(T config, NotificationRequest request) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送文件(可选,不是所有渠道都支持)
|
||||||
|
*
|
||||||
|
* @param config 渠道配置
|
||||||
|
* @param filePath 文件路径
|
||||||
|
* @param fileName 文件名称
|
||||||
|
* @throws Exception 发送失败时抛出异常
|
||||||
|
*/
|
||||||
|
default void sendFile(T config, String filePath, String fileName) throws Exception {
|
||||||
|
throw new UnsupportedOperationException("该渠道不支持发送文件");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支持的渠道类型
|
* 支持的渠道类型
|
||||||
@ -35,7 +48,7 @@ public interface INotificationChannelAdapter {
|
|||||||
* @param config 渠道配置
|
* @param config 渠道配置
|
||||||
* @return 校验结果消息
|
* @return 校验结果消息
|
||||||
*/
|
*/
|
||||||
default String validateConfig(Map<String, Object> config) {
|
default String validateConfig(T config) {
|
||||||
return "配置有效";
|
return "配置有效";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +59,6 @@ public interface INotificationChannelAdapter {
|
|||||||
* @param config 渠道配置
|
* @param config 渠道配置
|
||||||
* @throws Exception 测试失败时抛出异常
|
* @throws Exception 测试失败时抛出异常
|
||||||
*/
|
*/
|
||||||
void testConnection(Map<String, Object> config) throws Exception;
|
void testConnection(T config) throws Exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
package com.qqchen.deploy.backend.notification.adapter.impl;
|
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.notification.adapter.INotificationChannelAdapter;
|
import com.qqchen.deploy.backend.notification.adapter.INotificationChannelAdapter;
|
||||||
import com.qqchen.deploy.backend.notification.dto.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.NotificationRequest;
|
||||||
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
|
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import jakarta.mail.internet.InternetAddress;
|
import jakarta.mail.internet.InternetAddress;
|
||||||
import jakarta.mail.internet.MimeMessage;
|
import jakarta.mail.internet.MimeMessage;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -16,7 +14,6 @@ import org.springframework.stereotype.Component;
|
|||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,15 +24,13 @@ import java.util.Properties;
|
|||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class EmailChannelAdapter implements INotificationChannelAdapter {
|
public class EmailChannelAdapter implements INotificationChannelAdapter<EmailNotificationConfig> {
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void send(Map<String, Object> config, NotificationRequest request) throws Exception {
|
public void send(EmailNotificationConfig emailConfig, NotificationRequest request) throws Exception {
|
||||||
// 1. 解析配置
|
// 1. 校验配置
|
||||||
EmailNotificationConfig emailConfig = JsonUtils.fromMap(config, EmailNotificationConfig.class);
|
|
||||||
|
|
||||||
validateEmailConfig(emailConfig);
|
validateEmailConfig(emailConfig);
|
||||||
|
|
||||||
// 2. 创建JavaMailSender
|
// 2. 创建JavaMailSender
|
||||||
@ -88,9 +83,8 @@ public class EmailChannelAdapter implements INotificationChannelAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String validateConfig(Map<String, Object> config) {
|
public String validateConfig(EmailNotificationConfig emailConfig) {
|
||||||
try {
|
try {
|
||||||
EmailNotificationConfig emailConfig = JsonUtils.fromMap(config, EmailNotificationConfig.class);
|
|
||||||
validateEmailConfig(emailConfig);
|
validateEmailConfig(emailConfig);
|
||||||
return "配置有效";
|
return "配置有效";
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -99,11 +93,8 @@ public class EmailChannelAdapter implements INotificationChannelAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void testConnection(Map<String, Object> config) throws Exception {
|
public void testConnection(EmailNotificationConfig emailConfig) throws Exception {
|
||||||
// 1. 解析配置
|
// 1. 校验配置
|
||||||
EmailNotificationConfig emailConfig = JsonUtils.fromMap(config, EmailNotificationConfig.class);
|
|
||||||
|
|
||||||
// 2. 校验配置
|
|
||||||
validateEmailConfig(emailConfig);
|
validateEmailConfig(emailConfig);
|
||||||
|
|
||||||
// 3. 创建JavaMailSender
|
// 3. 创建JavaMailSender
|
||||||
|
|||||||
@ -4,17 +4,23 @@ 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.NotificationRequest;
|
||||||
import com.qqchen.deploy.backend.notification.dto.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 lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
import org.springframework.http.HttpEntity;
|
import org.springframework.http.HttpEntity;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -28,26 +34,29 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class WeworkChannelAdapter implements INotificationChannelAdapter {
|
public class WeworkChannelAdapter implements INotificationChannelAdapter<WeworkNotificationConfig> {
|
||||||
|
|
||||||
|
private static final String BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook";
|
||||||
|
|
||||||
private final RestTemplate restTemplate = new RestTemplate();
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void send(Map<String, Object> config, NotificationRequest request) throws Exception {
|
public void send(WeworkNotificationConfig config, NotificationRequest request) throws Exception {
|
||||||
// 1. 解析配置
|
// 1. 校验配置
|
||||||
WeworkNotificationConfig weworkConfig = JsonUtils.fromMap(config, WeworkNotificationConfig.class);
|
if (config.getKey() == null || config.getKey().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("企业微信 Webhook Key 未配置");
|
||||||
if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("企业微信Webhook URL未配置");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 构建发送消息 URL
|
||||||
|
String webhookUrl = BASE_URL + "/send?key=" + config.getKey();
|
||||||
|
|
||||||
// 2. 构建消息内容
|
// 2. 构建消息内容
|
||||||
String message = buildMessage(request);
|
String message = buildMessage(request);
|
||||||
|
|
||||||
// 3. 构建@人列表
|
// 3. 构建@人列表
|
||||||
List<String> mentionedList = buildMentionedList(weworkConfig, request);
|
List<String> mentionedList = buildMentionedList(config, request);
|
||||||
List<String> mentionedMobileList = buildMentionedMobileList(weworkConfig, request);
|
List<String> mentionedMobileList = buildMentionedMobileList(config, request);
|
||||||
|
|
||||||
// 4. 构建企业微信消息体
|
// 4. 构建企业微信消息体
|
||||||
Map<String, Object> messageBody = new HashMap<>();
|
Map<String, Object> messageBody = new HashMap<>();
|
||||||
@ -85,10 +94,10 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
|
|||||||
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: {}, 类型: {}, 消息: {}", weworkConfig.getWebhookUrl(), msgType, message);
|
log.info("发送企业微信通知 - URL: {}, 类型: {}, 消息: {}", webhookUrl, msgType, message);
|
||||||
|
|
||||||
String response = restTemplate.exchange(
|
String response = restTemplate.exchange(
|
||||||
weworkConfig.getWebhookUrl(),
|
webhookUrl,
|
||||||
HttpMethod.POST,
|
HttpMethod.POST,
|
||||||
entity,
|
entity,
|
||||||
String.class
|
String.class
|
||||||
@ -103,32 +112,65 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String validateConfig(Map<String, Object> config) {
|
public void sendFile(WeworkNotificationConfig config, String filePath, String fileName) throws Exception {
|
||||||
try {
|
try {
|
||||||
WeworkNotificationConfig weworkConfig = JsonUtils.fromMap(config, WeworkNotificationConfig.class);
|
// 1. 校验配置
|
||||||
|
if (config.getKey() == null || config.getKey().isEmpty()) {
|
||||||
if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) {
|
throw new IllegalArgumentException("企业微信 Webhook Key 未配置");
|
||||||
return "Webhook URL不能为空";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!weworkConfig.getWebhookUrl().startsWith("https://qyapi.weixin.qq.com")) {
|
// 2. 上传文件获取 media_id
|
||||||
return "Webhook URL格式不正确";
|
String uploadUrl = BASE_URL + "/upload_media?key=" + config.getKey() + "&type=file";
|
||||||
|
String mediaId = uploadFile(uploadUrl, filePath);
|
||||||
|
|
||||||
|
if (mediaId == null) {
|
||||||
|
throw new RuntimeException("文件上传失败,未获取到 media_id");
|
||||||
}
|
}
|
||||||
|
|
||||||
return "配置有效";
|
// 3. 发送文件消息
|
||||||
|
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("发送企业微信文件 - fileName: {}, mediaId: {}", fileName, mediaId);
|
||||||
|
|
||||||
|
String response = restTemplate.exchange(
|
||||||
|
sendUrl,
|
||||||
|
HttpMethod.POST,
|
||||||
|
entity,
|
||||||
|
String.class
|
||||||
|
).getBody();
|
||||||
|
|
||||||
|
log.info("企业微信文件发送成功 - 响应: {}", response);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return "配置解析失败: " + e.getMessage();
|
log.error("企业微信文件发送失败: filePath={}", filePath, e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void testConnection(Map<String, Object> config) throws Exception {
|
public String validateConfig(WeworkNotificationConfig config) {
|
||||||
// 1. 解析配置
|
if (config.getKey() == null || config.getKey().isEmpty()) {
|
||||||
WeworkNotificationConfig weworkConfig = JsonUtils.fromMap(config, WeworkNotificationConfig.class);
|
return "Webhook Key 不能为空";
|
||||||
|
|
||||||
if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("企业微信Webhook URL未配置");
|
|
||||||
}
|
}
|
||||||
|
return "配置有效";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void testConnection(WeworkNotificationConfig config) throws Exception {
|
||||||
|
// 1. 校验配置
|
||||||
|
if (config.getKey() == null || config.getKey().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("企业微信 Webhook Key 未配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 构建测试消息 URL
|
||||||
|
String webhookUrl = BASE_URL + "/send?key=" + config.getKey();
|
||||||
|
|
||||||
// 2. 构建测试消息
|
// 2. 构建测试消息
|
||||||
Map<String, Object> messageBody = new HashMap<>();
|
Map<String, Object> messageBody = new HashMap<>();
|
||||||
@ -145,10 +187,10 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
|
|||||||
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: {}", weworkConfig.getWebhookUrl());
|
log.info("测试企业微信连接 - URL: {}", webhookUrl);
|
||||||
|
|
||||||
String response = restTemplate.exchange(
|
String response = restTemplate.exchange(
|
||||||
weworkConfig.getWebhookUrl(),
|
webhookUrl,
|
||||||
HttpMethod.POST,
|
HttpMethod.POST,
|
||||||
entity,
|
entity,
|
||||||
String.class
|
String.class
|
||||||
@ -227,5 +269,70 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
|
|||||||
message.contains("`") ||
|
message.contains("`") ||
|
||||||
message.matches(".*\\[.*\\]\\(.*\\).*"); // [text](url) 格式
|
message.matches(".*\\[.*\\]\\(.*\\).*"); // [text](url) 格式
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件到企业微信
|
||||||
|
*/
|
||||||
|
private String uploadFile(String uploadUrl, String filePath) {
|
||||||
|
try {
|
||||||
|
File file = new File(filePath);
|
||||||
|
if (!file.exists()) {
|
||||||
|
log.error("文件不存在: {}", filePath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 multipart 请求
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||||
|
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("media", new FileSystemResource(file));
|
||||||
|
|
||||||
|
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
ResponseEntity<String> response = restTemplate.postForEntity(uploadUrl, requestEntity, String.class);
|
||||||
|
|
||||||
|
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
||||||
|
// 解析响应获取 media_id
|
||||||
|
com.fasterxml.jackson.databind.JsonNode jsonNode = JsonUtils.parseJson(response.getBody());
|
||||||
|
if (jsonNode == null) {
|
||||||
|
log.error("文件上传响应解析失败");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int errcode = jsonNode.get("errcode").asInt();
|
||||||
|
if (errcode == 0) {
|
||||||
|
String mediaId = jsonNode.get("media_id").asText();
|
||||||
|
log.info("文件上传成功, media_id: {}", mediaId);
|
||||||
|
return mediaId;
|
||||||
|
} else {
|
||||||
|
log.error("文件上传失败, errcode: {}, errmsg: {}",
|
||||||
|
errcode, jsonNode.get("errmsg").asText());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("上传文件到企业微信失败: filePath={}", filePath, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建文件消息体
|
||||||
|
*/
|
||||||
|
private Map<String, Object> buildFileMessage(String mediaId) {
|
||||||
|
Map<String, Object> message = new HashMap<>();
|
||||||
|
message.put("msgtype", "file");
|
||||||
|
|
||||||
|
Map<String, String> file = new HashMap<>();
|
||||||
|
file.put("media_id", mediaId);
|
||||||
|
|
||||||
|
message.put("file", file);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
package com.qqchen.deploy.backend.notification.converter;
|
package com.qqchen.deploy.backend.notification.converter;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
|
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
|
||||||
import com.qqchen.deploy.backend.notification.dto.NotificationChannelDTO;
|
import com.qqchen.deploy.backend.framework.utils.JsonUtils;
|
||||||
|
import com.qqchen.deploy.backend.notification.dto.*;
|
||||||
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
|
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
|
||||||
|
import com.qqchen.deploy.backend.notification.entity.config.BaseNotificationConfig;
|
||||||
|
import com.qqchen.deploy.backend.notification.entity.config.EmailNotificationConfig;
|
||||||
|
import com.qqchen.deploy.backend.notification.entity.config.WeworkNotificationConfig;
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知渠道转换器
|
* 通知渠道转换器
|
||||||
@ -12,6 +20,83 @@ import org.mapstruct.Mapper;
|
|||||||
* @since 2025-10-22
|
* @since 2025-10-22
|
||||||
*/
|
*/
|
||||||
@Mapper(config = BaseConverter.class)
|
@Mapper(config = BaseConverter.class)
|
||||||
public interface NotificationChannelConverter extends BaseConverter<NotificationChannel, NotificationChannelDTO> {
|
public abstract class NotificationChannelConverter implements BaseConverter<NotificationChannel, NotificationChannelDTO> {
|
||||||
|
|
||||||
|
protected NotificationConfigConverter notificationConfigConverter;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setNotificationConfigConverter(NotificationConfigConverter notificationConfigConverter) {
|
||||||
|
this.notificationConfigConverter = notificationConfigConverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity -> DTO
|
||||||
|
* config: Map -> BaseNotificationConfigDTO
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Mapping(target = "config", expression = "java(mapToConfigDTO(entity))")
|
||||||
|
public abstract NotificationChannelDTO toDto(NotificationChannel entity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO -> Entity
|
||||||
|
* config: BaseNotificationConfigDTO -> Map
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Mapping(target = "config", expression = "java(mapToConfigMap(dto))")
|
||||||
|
public abstract NotificationChannel toEntity(NotificationChannelDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新Entity(手动实现)
|
||||||
|
* config: BaseNotificationConfigDTO -> Map
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void updateEntity(NotificationChannel entity, NotificationChannelDTO dto) {
|
||||||
|
if (entity == null || dto == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新基本字段
|
||||||
|
entity.setName(dto.getName());
|
||||||
|
entity.setChannelType(dto.getChannelType());
|
||||||
|
entity.setStatus(dto.getStatus());
|
||||||
|
entity.setDescription(dto.getDescription());
|
||||||
|
|
||||||
|
// 更新config字段
|
||||||
|
entity.setConfig(mapToConfigMap(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将Entity的Map配置转换为DTO
|
||||||
|
*/
|
||||||
|
protected BaseNotificationConfigDTO mapToConfigDTO(NotificationChannel entity) {
|
||||||
|
if (entity == null || entity.getConfig() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Map -> BaseNotificationConfig Entity
|
||||||
|
BaseNotificationConfig configEntity = switch (entity.getChannelType()) {
|
||||||
|
case WEWORK -> JsonUtils.fromMap(entity.getConfig(), WeworkNotificationConfig.class);
|
||||||
|
case EMAIL -> JsonUtils.fromMap(entity.getConfig(), EmailNotificationConfig.class);
|
||||||
|
default -> throw new IllegalArgumentException("不支持的渠道类型: " + entity.getChannelType());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. BaseNotificationConfig Entity -> BaseNotificationConfigDTO
|
||||||
|
return notificationConfigConverter.toDto(configEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将DTO配置转换为Entity的Map
|
||||||
|
*/
|
||||||
|
protected Map<String, Object> mapToConfigMap(NotificationChannelDTO dto) {
|
||||||
|
if (dto == null || dto.getConfig() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. BaseNotificationConfigDTO -> BaseNotificationConfig Entity
|
||||||
|
BaseNotificationConfig configEntity = notificationConfigConverter.toEntity(dto.getConfig());
|
||||||
|
|
||||||
|
// 2. BaseNotificationConfig Entity -> Map
|
||||||
|
return JsonUtils.toMap(configEntity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.qqchen.deploy.backend.notification.converter;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.notification.dto.*;
|
||||||
|
import com.qqchen.deploy.backend.notification.entity.config.BaseNotificationConfig;
|
||||||
|
import com.qqchen.deploy.backend.notification.entity.config.EmailNotificationConfig;
|
||||||
|
import com.qqchen.deploy.backend.notification.entity.config.WeworkNotificationConfig;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知配置转换器
|
||||||
|
* 负责Entity配置和DTO配置之间的转换
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @since 2025-11-12
|
||||||
|
*/
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
public interface NotificationConfigConverter {
|
||||||
|
|
||||||
|
// Entity -> DTO
|
||||||
|
WeworkNotificationConfigDTO toDto(WeworkNotificationConfig entity);
|
||||||
|
EmailNotificationConfigDTO toDto(EmailNotificationConfig entity);
|
||||||
|
|
||||||
|
// DTO -> Entity
|
||||||
|
WeworkNotificationConfig toEntity(WeworkNotificationConfigDTO dto);
|
||||||
|
EmailNotificationConfig toEntity(EmailNotificationConfigDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型自动转换 Entity -> DTO
|
||||||
|
*/
|
||||||
|
default BaseNotificationConfigDTO toDto(BaseNotificationConfig entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity instanceof WeworkNotificationConfig) {
|
||||||
|
return toDto((WeworkNotificationConfig) entity);
|
||||||
|
} else if (entity instanceof EmailNotificationConfig) {
|
||||||
|
return toDto((EmailNotificationConfig) entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("不支持的配置类型: " + entity.getClass().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型自动转换 DTO -> Entity
|
||||||
|
*/
|
||||||
|
default BaseNotificationConfig toEntity(BaseNotificationConfigDTO dto) {
|
||||||
|
if (dto == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto instanceof WeworkNotificationConfigDTO) {
|
||||||
|
return toEntity((WeworkNotificationConfigDTO) dto);
|
||||||
|
} else if (dto instanceof EmailNotificationConfigDTO) {
|
||||||
|
return toEntity((EmailNotificationConfigDTO) dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("不支持的配置类型: " + dto.getClass().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
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 lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知配置DTO基类
|
||||||
|
* 用于数据传输层,与Entity层的BaseNotificationConfig对应
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @since 2025-11-12
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonTypeInfo(
|
||||||
|
use = JsonTypeInfo.Id.NAME,
|
||||||
|
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
|
||||||
|
property = "channelType"
|
||||||
|
)
|
||||||
|
@JsonSubTypes({
|
||||||
|
@JsonSubTypes.Type(value = WeworkNotificationConfigDTO.class, name = "WEWORK"),
|
||||||
|
@JsonSubTypes.Type(value = EmailNotificationConfigDTO.class, name = "EMAIL")
|
||||||
|
})
|
||||||
|
public abstract class BaseNotificationConfigDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取渠道类型
|
||||||
|
*/
|
||||||
|
public abstract NotificationChannelTypeEnum getChannelType();
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
package com.qqchen.deploy.backend.notification.dto;
|
package com.qqchen.deploy.backend.notification.dto;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -8,10 +10,11 @@ import java.util.List;
|
|||||||
* 邮件通知配置DTO
|
* 邮件通知配置DTO
|
||||||
*
|
*
|
||||||
* @author qqchen
|
* @author qqchen
|
||||||
* @since 2025-11-03
|
* @since 2025-11-12
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class EmailNotificationConfig {
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class EmailNotificationConfigDTO extends BaseNotificationConfigDTO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SMTP服务器地址(必填)
|
* SMTP服务器地址(必填)
|
||||||
@ -52,5 +55,9 @@ public class EmailNotificationConfig {
|
|||||||
* 是否使用SSL(可选,默认true)
|
* 是否使用SSL(可选,默认true)
|
||||||
*/
|
*/
|
||||||
private Boolean useSsl = true;
|
private Boolean useSsl = true;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NotificationChannelTypeEnum getChannelType() {
|
||||||
|
return NotificationChannelTypeEnum.EMAIL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9,8 +9,6 @@ import jakarta.validation.constraints.NotNull;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知渠道DTO
|
* 通知渠道DTO
|
||||||
*
|
*
|
||||||
@ -30,9 +28,9 @@ public class NotificationChannelDTO extends BaseDTO {
|
|||||||
@NotNull(message = "渠道类型不能为空")
|
@NotNull(message = "渠道类型不能为空")
|
||||||
private NotificationChannelTypeEnum channelType;
|
private NotificationChannelTypeEnum channelType;
|
||||||
|
|
||||||
@Schema(description = "渠道配置(JSON格式)", example = "{\"webhookUrl\":\"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx\"}")
|
@Schema(description = "渠道配置(根据渠道类型不同而不同)")
|
||||||
@NotNull(message = "渠道配置不能为空")
|
@NotNull(message = "渠道配置不能为空")
|
||||||
private Map<String, Object> config;
|
private BaseNotificationConfigDTO config;
|
||||||
|
|
||||||
@Schema(description = "状态", example = "ENABLED")
|
@Schema(description = "状态", example = "ENABLED")
|
||||||
private NotificationChannelStatusEnum status;
|
private NotificationChannelStatusEnum status;
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.notification.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 企业微信通知配置DTO
|
|
||||||
*
|
|
||||||
* @author qqchen
|
|
||||||
* @since 2025-11-03
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class WeworkNotificationConfig {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Webhook URL(必填)
|
|
||||||
*/
|
|
||||||
private String webhookUrl;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认@的手机号列表(可选)
|
|
||||||
*/
|
|
||||||
private List<String> mentionedMobileList;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认@的用户列表(可选)
|
|
||||||
* 例如:["@all"] 表示@所有人
|
|
||||||
*/
|
|
||||||
private List<String> mentionedList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
package com.qqchen.deploy.backend.notification.dto;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企业微信通知配置DTO
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @since 2025-11-12
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class WeworkNotificationConfigDTO extends BaseNotificationConfigDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企业微信机器人 Webhook Key(必填)
|
||||||
|
* 完整URL格式: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}
|
||||||
|
*/
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认@的手机号列表(可选)
|
||||||
|
*/
|
||||||
|
private List<String> mentionedMobileList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认@的用户列表(可选)
|
||||||
|
* 例如:["@all"] 表示@所有人
|
||||||
|
*/
|
||||||
|
private List<String> mentionedList;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NotificationChannelTypeEnum getChannelType() {
|
||||||
|
return NotificationChannelTypeEnum.WEWORK;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package com.qqchen.deploy.backend.notification.entity.config;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知配置基类
|
||||||
|
* 提取各渠道配置的共用字段
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @since 2025-11-12
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public abstract class BaseNotificationConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取渠道类型
|
||||||
|
*/
|
||||||
|
public abstract NotificationChannelTypeEnum getChannelType();
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
package com.qqchen.deploy.backend.notification.entity.config;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件通知配置DTO
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @since 2025-11-03
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class EmailNotificationConfig extends BaseNotificationConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP服务器地址(必填)
|
||||||
|
*/
|
||||||
|
private String smtpHost;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP服务器端口(必填)
|
||||||
|
*/
|
||||||
|
private Integer smtpPort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP用户名(必填)
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP密码(必填)
|
||||||
|
*/
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发件人邮箱(必填)
|
||||||
|
*/
|
||||||
|
private String from;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发件人名称(可选)
|
||||||
|
*/
|
||||||
|
private String fromName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认收件人列表(可选)
|
||||||
|
*/
|
||||||
|
private List<String> defaultReceivers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否使用SSL(可选,默认true)
|
||||||
|
*/
|
||||||
|
private Boolean useSsl = true;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NotificationChannelTypeEnum getChannelType() {
|
||||||
|
return NotificationChannelTypeEnum.EMAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.qqchen.deploy.backend.notification.entity.config;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企业微信通知配置DTO
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @since 2025-11-03
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class WeworkNotificationConfig extends BaseNotificationConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企业微信机器人 Webhook Key(必填)
|
||||||
|
* 完整URL格式: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}
|
||||||
|
*/
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认@的手机号列表(可选)
|
||||||
|
*/
|
||||||
|
private List<String> mentionedMobileList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认@的用户列表(可选)
|
||||||
|
* 例如:["@all"] 表示@所有人
|
||||||
|
*/
|
||||||
|
private List<String> mentionedList;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NotificationChannelTypeEnum getChannelType() {
|
||||||
|
return NotificationChannelTypeEnum.WEWORK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -5,11 +5,15 @@ import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|||||||
import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
||||||
import com.qqchen.deploy.backend.framework.exception.UniqueConstraintException;
|
import com.qqchen.deploy.backend.framework.exception.UniqueConstraintException;
|
||||||
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
|
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
|
||||||
|
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.converter.NotificationChannelConverter;
|
import com.qqchen.deploy.backend.notification.converter.NotificationChannelConverter;
|
||||||
|
import com.qqchen.deploy.backend.notification.entity.config.BaseNotificationConfig;
|
||||||
|
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.NotificationRequest;
|
||||||
|
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;
|
||||||
import com.qqchen.deploy.backend.notification.factory.NotificationChannelAdapterFactory;
|
import com.qqchen.deploy.backend.notification.factory.NotificationChannelAdapterFactory;
|
||||||
@ -72,9 +76,12 @@ public class NotificationChannelServiceImpl
|
|||||||
throw new BusinessException(ResponseCode.ERROR, new Object[]{"不支持的渠道类型: " + channel.getChannelType()});
|
throw new BusinessException(ResponseCode.ERROR, new Object[]{"不支持的渠道类型: " + channel.getChannelType()});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 执行连接测试
|
// 3. 转换配置
|
||||||
|
BaseNotificationConfig config = convertConfig(channel);
|
||||||
|
|
||||||
|
// 4. 执行连接测试
|
||||||
try {
|
try {
|
||||||
adapter.testConnection(channel.getConfig());
|
adapter.testConnection(config);
|
||||||
log.info("通知渠道连接测试成功: id={}, name={}", id, channel.getName());
|
log.info("通知渠道连接测试成功: id={}, name={}", id, channel.getName());
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -137,12 +144,15 @@ public class NotificationChannelServiceImpl
|
|||||||
throw new BusinessException(ResponseCode.ERROR);
|
throw new BusinessException(ResponseCode.ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 发送通知
|
// 5. 转换配置
|
||||||
|
BaseNotificationConfig config = convertConfig(channel);
|
||||||
|
|
||||||
|
// 6. 发送通知
|
||||||
try {
|
try {
|
||||||
log.info("发送通知 - 渠道ID: {}, 渠道类型: {}, 标题: {}",
|
log.info("发送通知 - 渠道ID: {}, 渠道类型: {}, 标题: {}",
|
||||||
channel.getId(), channel.getChannelType(), request.getTitle());
|
channel.getId(), channel.getChannelType(), request.getTitle());
|
||||||
|
|
||||||
adapter.send(channel.getConfig(), request);
|
adapter.send(config, request);
|
||||||
|
|
||||||
log.info("通知发送成功 - 渠道ID: {}", channel.getId());
|
log.info("通知发送成功 - 渠道ID: {}", channel.getId());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -150,5 +160,16 @@ public class NotificationChannelServiceImpl
|
|||||||
throw new BusinessException(ResponseCode.ERROR, new Object[]{e.getMessage()});
|
throw new BusinessException(ResponseCode.ERROR, new Object[]{e.getMessage()});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将Map配置转换为具体的配置类型
|
||||||
|
*/
|
||||||
|
private BaseNotificationConfig convertConfig(NotificationChannel channel) {
|
||||||
|
return switch (channel.getChannelType()) {
|
||||||
|
case WEWORK -> JsonUtils.fromMap(channel.getConfig(), WeworkNotificationConfig.class);
|
||||||
|
case EMAIL -> JsonUtils.fromMap(channel.getConfig(), EmailNotificationConfig.class);
|
||||||
|
default -> throw new IllegalArgumentException("不支持的渠道类型: " + channel.getChannelType());
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,11 +2,8 @@ package com.qqchen.deploy.backend.workflow.api;
|
|||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.api.Response;
|
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.framework.enums.ResponseCode;
|
|
||||||
import com.qqchen.deploy.backend.workflow.dto.WorkflowCategoryDTO;
|
import com.qqchen.deploy.backend.workflow.dto.WorkflowCategoryDTO;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO;
|
import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.WorkflowExecutionDTO;
|
|
||||||
import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.query.WorkflowDefinitionQuery;
|
import com.qqchen.deploy.backend.workflow.dto.query.WorkflowDefinitionQuery;
|
||||||
import com.qqchen.deploy.backend.workflow.service.IWorkflowDefinitionService;
|
import com.qqchen.deploy.backend.workflow.service.IWorkflowDefinitionService;
|
||||||
@ -16,17 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.flowable.engine.HistoryService;
|
|
||||||
import org.flowable.engine.RuntimeService;
|
|
||||||
import org.flowable.engine.history.HistoricActivityInstance;
|
|
||||||
import org.flowable.engine.history.HistoricProcessInstance;
|
|
||||||
import org.flowable.variable.api.history.HistoricVariableInstance;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流定义控制器
|
* 工作流定义控制器
|
||||||
|
|||||||
@ -43,9 +43,9 @@ public class WorkflowDefinitionDTO extends BaseDTO {
|
|||||||
private Long formDefinitionId;
|
private Long formDefinitionId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动表单信息(用于展示)
|
* 启动表单名称(用于展示)
|
||||||
*/
|
*/
|
||||||
private FormDefinitionDTO formDefinition;
|
private String formDefinitionName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流程版本
|
* 流程版本
|
||||||
|
|||||||
@ -38,11 +38,14 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageImpl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流定义服务实现
|
* 工作流定义服务实现
|
||||||
@ -57,12 +60,6 @@ public class WorkflowDefinitionServiceImpl extends BaseServiceImpl<WorkflowDefin
|
|||||||
@Resource
|
@Resource
|
||||||
private RuntimeService runtimeService;
|
private RuntimeService runtimeService;
|
||||||
|
|
||||||
@Resource
|
|
||||||
private ManagementService managementService;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private TaskService taskService;
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private HistoryService historyService;
|
private HistoryService historyService;
|
||||||
|
|
||||||
@ -78,6 +75,9 @@ public class WorkflowDefinitionServiceImpl extends BaseServiceImpl<WorkflowDefin
|
|||||||
@Resource
|
@Resource
|
||||||
private BpmnConverter bpmnConverter;
|
private BpmnConverter bpmnConverter;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.qqchen.deploy.backend.workflow.repository.IFormDefinitionRepository formDefinitionRepository;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@ -330,13 +330,13 @@ public class WorkflowDefinitionServiceImpl extends BaseServiceImpl<WorkflowDefin
|
|||||||
public void publishedWorkflowDesign(Long workflowDefinitionId) {
|
public void publishedWorkflowDesign(Long workflowDefinitionId) {
|
||||||
WorkflowDefinition definition = workflowDefinitionRepository.findById(workflowDefinitionId)
|
WorkflowDefinition definition = workflowDefinitionRepository.findById(workflowDefinitionId)
|
||||||
.orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NOT_FOUND));
|
.orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NOT_FOUND));
|
||||||
|
|
||||||
// 校验:工作流必须绑定表单才能发布
|
// 校验:工作流必须绑定表单才能发布
|
||||||
if (definition.getFormDefinitionId() == null) {
|
if (definition.getFormDefinitionId() == null) {
|
||||||
log.warn("工作流发布失败: 工作流 {} 未绑定表单", workflowDefinitionId);
|
log.warn("工作流发布失败: 工作流 {} 未绑定表单", workflowDefinitionId);
|
||||||
throw new BusinessException(ResponseCode.WORKFLOW_FORM_NOT_BOUND);
|
throw new BusinessException(ResponseCode.WORKFLOW_FORM_NOT_BOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
WorkflowDefinitionGraph graph = definition.getGraph();
|
WorkflowDefinitionGraph graph = definition.getGraph();
|
||||||
definition.setBpmnXml(bpmnConverter.convertToXml(graph, definition.getKey()));
|
definition.setBpmnXml(bpmnConverter.convertToXml(graph, definition.getKey()));
|
||||||
Deployment deployment = this.deployWorkflow(definition);
|
Deployment deployment = this.deployWorkflow(definition);
|
||||||
@ -363,4 +363,62 @@ public class WorkflowDefinitionServiceImpl extends BaseServiceImpl<WorkflowDefin
|
|||||||
return this.converter.toDtoList(workflowDefinitionRepository.findByStatus(WorkflowDefinitionStatusEnums.PUBLISHED));
|
return this.converter.toDtoList(workflowDefinitionRepository.findByStatus(WorkflowDefinitionStatusEnums.PUBLISHED));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重写 page 方法,填充表单名称
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Page<WorkflowDefinitionDTO> page(WorkflowDefinitionQuery query) {
|
||||||
|
Page<WorkflowDefinitionDTO> page = super.page(query);
|
||||||
|
fillFormDefinitionInfo(page.getContent());
|
||||||
|
return new PageImpl<>(page.getContent(), page.getPageable(), page.getTotalElements());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重写 findAll 方法,填充表单名称
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<WorkflowDefinitionDTO> findAll(WorkflowDefinitionQuery query) {
|
||||||
|
List<WorkflowDefinitionDTO> list = super.findAll(query);
|
||||||
|
fillFormDefinitionInfo(list);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量填充表单名称
|
||||||
|
* 使用批量查询避免 N+1 问题
|
||||||
|
*/
|
||||||
|
private void fillFormDefinitionInfo(List<WorkflowDefinitionDTO> definitions) {
|
||||||
|
if (definitions == null || definitions.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集所有表单ID
|
||||||
|
Set<Long> formDefinitionIds = definitions.stream()
|
||||||
|
.map(WorkflowDefinitionDTO::getFormDefinitionId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
if (formDefinitionIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量查询表单定义,只获取ID和名称
|
||||||
|
Map<Long, String> formDefinitionNameMap =
|
||||||
|
formDefinitionRepository.findAllById(formDefinitionIds).stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
com.qqchen.deploy.backend.workflow.entity.FormDefinition::getId,
|
||||||
|
com.qqchen.deploy.backend.workflow.entity.FormDefinition::getName
|
||||||
|
));
|
||||||
|
|
||||||
|
// 填充表单名称
|
||||||
|
definitions.forEach(definition -> {
|
||||||
|
if (definition.getFormDefinitionId() != null) {
|
||||||
|
String formDefinitionName = formDefinitionNameMap.get(definition.getFormDefinitionId());
|
||||||
|
if (formDefinitionName != null) {
|
||||||
|
definition.setFormDefinitionName(formDefinitionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,8 +59,8 @@ VALUES
|
|||||||
(1011, '工作流设计器', '/workflow/design/:id', 'Workflow/Design', 'EditOutlined', NULL, 2, 100, 11, TRUE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
(1011, '工作流设计器', '/workflow/design/:id', 'Workflow/Design', 'EditOutlined', NULL, 2, 100, 11, TRUE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||||
-- 工作流实例
|
-- 工作流实例
|
||||||
(102, '工作流实例', '/workflow/instances', 'Workflow/Instance/List', 'BranchesOutlined', 'workflow:instance', 2, 100, 20, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
(102, '工作流实例', '/workflow/instances', 'Workflow/Instance/List', 'BranchesOutlined', 'workflow:instance', 2, 100, 20, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||||
-- 表单管理
|
-- 动态表单菜单
|
||||||
(104, '表单管理', '/workflow/form', 'Form/Definition/List', 'FormOutlined', 'workflow:form', 2, 100, 30, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
(104, '动态表单菜单', '/workflow/form', 'Form/Definition/List', 'FormOutlined', 'workflow:form', 2, 100, 30, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||||
-- 表单设计器(隐藏路由,用于设计表单)
|
-- 表单设计器(隐藏路由,用于设计表单)
|
||||||
(1041, '表单设计器', '/workflow/form/:id/design', 'Form/Definition/Designer', 'FormOutlined', NULL, 2, 100, 31, TRUE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
(1041, '表单设计器', '/workflow/form/:id/design', 'Form/Definition/Designer', 'FormOutlined', NULL, 2, 100, 31, TRUE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||||
-- 表单数据详情(隐藏路由,用于查看表单数据)
|
-- 表单数据详情(隐藏路由,用于查看表单数据)
|
||||||
@ -295,7 +295,7 @@ INSERT INTO sys_permission (id, create_time, menu_id, code, name, type, sort) VA
|
|||||||
(314, NOW(), 102, 'workflow:instance:suspend', '挂起实例', 'FUNCTION', 4),
|
(314, NOW(), 102, 'workflow:instance:suspend', '挂起实例', 'FUNCTION', 4),
|
||||||
(315, NOW(), 102, 'workflow:instance:resume', '恢复实例', 'FUNCTION', 5),
|
(315, NOW(), 102, 'workflow:instance:resume', '恢复实例', 'FUNCTION', 5),
|
||||||
|
|
||||||
-- 表单管理 (menu_id=104)
|
-- 动态表单菜单 (menu_id=104)
|
||||||
(321, NOW(), 104, 'workflow:form:list', '表单查询', 'FUNCTION', 1),
|
(321, NOW(), 104, 'workflow:form:list', '表单查询', 'FUNCTION', 1),
|
||||||
(322, NOW(), 104, 'workflow:form:view', '表单详情', 'FUNCTION', 2),
|
(322, NOW(), 104, 'workflow:form:view', '表单详情', 'FUNCTION', 2),
|
||||||
(323, NOW(), 104, 'workflow:form:create', '表单创建', 'FUNCTION', 3),
|
(323, NOW(), 104, 'workflow:form:create', '表单创建', 'FUNCTION', 3),
|
||||||
@ -455,7 +455,7 @@ INSERT INTO sys_permission (id, create_time, menu_id, code, name, type, sort) VA
|
|||||||
-- (374, NOW(), NULL, 'workflow:category:update', '工作流分类修改', 'FUNCTION', 24),
|
-- (374, NOW(), NULL, 'workflow:category:update', '工作流分类修改', 'FUNCTION', 24),
|
||||||
-- (375, NOW(), NULL, 'workflow:category:delete', '工作流分类删除', 'FUNCTION', 25),
|
-- (375, NOW(), NULL, 'workflow:category:delete', '工作流分类删除', 'FUNCTION', 25),
|
||||||
--
|
--
|
||||||
-- -- 表单管理 (menu_id=104)
|
-- -- 动态表单菜单 (menu_id=104)
|
||||||
-- (381, NOW(), 104, 'workflow:form:list', '表单查询', 'FUNCTION', 1),
|
-- (381, NOW(), 104, 'workflow:form:list', '表单查询', 'FUNCTION', 1),
|
||||||
-- (382, NOW(), 104, 'workflow:form:view', '表单详情', 'FUNCTION', 2),
|
-- (382, NOW(), 104, 'workflow:form:view', '表单详情', 'FUNCTION', 2),
|
||||||
-- (383, NOW(), 104, 'workflow:form:create', '表单创建', 'FUNCTION', 3),
|
-- (383, NOW(), 104, 'workflow:form:create', '表单创建', 'FUNCTION', 3),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user