增加构建通知

This commit is contained in:
dengqichen 2025-11-12 13:17:48 +08:00
parent 5a924f7aa5
commit adf83458a5
18 changed files with 615 additions and 122 deletions

View File

@ -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;
} }

View File

@ -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

View File

@ -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;
}
} }

View File

@ -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);
}
} }

View File

@ -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());
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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());
};
}
} }

View File

@ -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;
/** /**
* 工作流定义控制器 * 工作流定义控制器

View File

@ -43,9 +43,9 @@ public class WorkflowDefinitionDTO extends BaseDTO {
private Long formDefinitionId; private Long formDefinitionId;
/** /**
* 启动表单信息用于展示 * 启动表单名称用于展示
*/ */
private FormDefinitionDTO formDefinition; private String formDefinitionName;
/** /**
* 流程版本 * 流程版本

View File

@ -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)
@ -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);
}
}
});
}
} }

View File

@ -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),