diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/INotificationChannelAdapter.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/INotificationChannelAdapter.java index 0d4b95e5..cc6b3d2b 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/INotificationChannelAdapter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/INotificationChannelAdapter.java @@ -1,26 +1,39 @@ 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.enums.NotificationChannelTypeEnum; -import java.util.Map; - /** * 通知渠道适配器接口 + * 使用泛型约束配置类型,避免类型转换 * + * @param 配置类型,必须继承 BaseNotificationConfig * @author qqchen * @since 2025-11-03 */ -public interface INotificationChannelAdapter { +public interface INotificationChannelAdapter { /** * 发送通知 * - * @param config 渠道配置(从数据库config字段解析) + * @param config 渠道配置 * @param request 通知请求 * @throws Exception 发送失败时抛出异常 */ - void send(Map 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 渠道配置 * @return 校验结果消息 */ - default String validateConfig(Map config) { + default String validateConfig(T config) { return "配置有效"; } @@ -46,6 +59,6 @@ public interface INotificationChannelAdapter { * @param config 渠道配置 * @throws Exception 测试失败时抛出异常 */ - void testConnection(Map config) throws Exception; + void testConnection(T config) throws Exception; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/impl/EmailChannelAdapter.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/impl/EmailChannelAdapter.java index f9711c69..933e2e69 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/impl/EmailChannelAdapter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/impl/EmailChannelAdapter.java @@ -1,12 +1,10 @@ package com.qqchen.deploy.backend.notification.adapter.impl; 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.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.enums.NotificationChannelTypeEnum; -import jakarta.annotation.Resource; import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.MimeMessage; import lombok.extern.slf4j.Slf4j; @@ -16,7 +14,6 @@ import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.List; -import java.util.Map; import java.util.Properties; /** @@ -27,15 +24,13 @@ import java.util.Properties; */ @Slf4j @Component -public class EmailChannelAdapter implements INotificationChannelAdapter { +public class EmailChannelAdapter implements INotificationChannelAdapter { private final ObjectMapper objectMapper = new ObjectMapper(); @Override - public void send(Map config, NotificationRequest request) throws Exception { - // 1. 解析配置 - EmailNotificationConfig emailConfig = JsonUtils.fromMap(config, EmailNotificationConfig.class); - + public void send(EmailNotificationConfig emailConfig, NotificationRequest request) throws Exception { + // 1. 校验配置 validateEmailConfig(emailConfig); // 2. 创建JavaMailSender @@ -88,9 +83,8 @@ public class EmailChannelAdapter implements INotificationChannelAdapter { } @Override - public String validateConfig(Map config) { + public String validateConfig(EmailNotificationConfig emailConfig) { try { - EmailNotificationConfig emailConfig = JsonUtils.fromMap(config, EmailNotificationConfig.class); validateEmailConfig(emailConfig); return "配置有效"; } catch (Exception e) { @@ -99,11 +93,8 @@ public class EmailChannelAdapter implements INotificationChannelAdapter { } @Override - public void testConnection(Map config) throws Exception { - // 1. 解析配置 - EmailNotificationConfig emailConfig = JsonUtils.fromMap(config, EmailNotificationConfig.class); - - // 2. 校验配置 + public void testConnection(EmailNotificationConfig emailConfig) throws Exception { + // 1. 校验配置 validateEmailConfig(emailConfig); // 3. 创建JavaMailSender diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/impl/WeworkChannelAdapter.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/impl/WeworkChannelAdapter.java index 0f4a7469..a92322b0 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/impl/WeworkChannelAdapter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/impl/WeworkChannelAdapter.java @@ -4,17 +4,23 @@ 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.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 lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; +import java.io.File; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -28,26 +34,29 @@ import java.util.Map; */ @Slf4j @Component -public class WeworkChannelAdapter implements INotificationChannelAdapter { +public class WeworkChannelAdapter implements INotificationChannelAdapter { + + private static final String BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook"; private final RestTemplate restTemplate = new RestTemplate(); private final ObjectMapper objectMapper = new ObjectMapper(); @Override - public void send(Map config, NotificationRequest request) throws Exception { - // 1. 解析配置 - WeworkNotificationConfig weworkConfig = JsonUtils.fromMap(config, WeworkNotificationConfig.class); - - if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) { - throw new IllegalArgumentException("企业微信Webhook URL未配置"); + public void send(WeworkNotificationConfig config, NotificationRequest request) 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. 构建消息内容 String message = buildMessage(request); // 3. 构建@人列表 - List mentionedList = buildMentionedList(weworkConfig, request); - List mentionedMobileList = buildMentionedMobileList(weworkConfig, request); + List mentionedList = buildMentionedList(config, request); + List mentionedMobileList = buildMentionedMobileList(config, request); // 4. 构建企业微信消息体 Map messageBody = new HashMap<>(); @@ -85,10 +94,10 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter { String jsonBody = objectMapper.writeValueAsString(messageBody); HttpEntity entity = new HttpEntity<>(jsonBody, headers); - log.info("发送企业微信通知 - URL: {}, 类型: {}, 消息: {}", weworkConfig.getWebhookUrl(), msgType, message); + log.info("发送企业微信通知 - URL: {}, 类型: {}, 消息: {}", webhookUrl, msgType, message); String response = restTemplate.exchange( - weworkConfig.getWebhookUrl(), + webhookUrl, HttpMethod.POST, entity, String.class @@ -103,32 +112,65 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter { } @Override - public String validateConfig(Map config) { + public void sendFile(WeworkNotificationConfig config, String filePath, String fileName) throws Exception { try { - WeworkNotificationConfig weworkConfig = JsonUtils.fromMap(config, WeworkNotificationConfig.class); - - if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) { - return "Webhook URL不能为空"; + // 1. 校验配置 + if (config.getKey() == null || config.getKey().isEmpty()) { + throw new IllegalArgumentException("企业微信 Webhook Key 未配置"); } - if (!weworkConfig.getWebhookUrl().startsWith("https://qyapi.weixin.qq.com")) { - return "Webhook URL格式不正确"; + // 2. 上传文件获取 media_id + 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 messageBody = buildFileMessage(mediaId); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + String jsonBody = objectMapper.writeValueAsString(messageBody); + HttpEntity 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) { - return "配置解析失败: " + e.getMessage(); + log.error("企业微信文件发送失败: filePath={}", filePath, e); + throw e; } } @Override - public void testConnection(Map config) throws Exception { - // 1. 解析配置 - WeworkNotificationConfig weworkConfig = JsonUtils.fromMap(config, WeworkNotificationConfig.class); - - if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) { - throw new IllegalArgumentException("企业微信Webhook URL未配置"); + public String validateConfig(WeworkNotificationConfig config) { + if (config.getKey() == null || config.getKey().isEmpty()) { + return "Webhook Key 不能为空"; } + 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. 构建测试消息 Map messageBody = new HashMap<>(); @@ -145,10 +187,10 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter { String jsonBody = objectMapper.writeValueAsString(messageBody); HttpEntity entity = new HttpEntity<>(jsonBody, headers); - log.info("测试企业微信连接 - URL: {}", weworkConfig.getWebhookUrl()); + log.info("测试企业微信连接 - URL: {}", webhookUrl); String response = restTemplate.exchange( - weworkConfig.getWebhookUrl(), + webhookUrl, HttpMethod.POST, entity, String.class @@ -227,5 +269,70 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter { message.contains("`") || 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 body = new LinkedMultiValueMap<>(); + body.add("media", new FileSystemResource(file)); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + // 发送请求 + ResponseEntity 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 buildFileMessage(String mediaId) { + Map message = new HashMap<>(); + message.put("msgtype", "file"); + + Map file = new HashMap<>(); + file.put("media_id", mediaId); + + message.put("file", file); + return message; + } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/converter/NotificationChannelConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/converter/NotificationChannelConverter.java index 6138b366..a096eb91 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/converter/NotificationChannelConverter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/converter/NotificationChannelConverter.java @@ -1,9 +1,17 @@ package com.qqchen.deploy.backend.notification.converter; 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.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.Mapping; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Map; /** * 通知渠道转换器 @@ -12,6 +20,83 @@ import org.mapstruct.Mapper; * @since 2025-10-22 */ @Mapper(config = BaseConverter.class) -public interface NotificationChannelConverter extends BaseConverter { +public abstract class NotificationChannelConverter implements BaseConverter { + + 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 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); + } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/converter/NotificationConfigConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/converter/NotificationConfigConverter.java new file mode 100644 index 00000000..f31dadc8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/converter/NotificationConfigConverter.java @@ -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()); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/BaseNotificationConfigDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/BaseNotificationConfigDTO.java new file mode 100644 index 00000000..37cad7f6 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/BaseNotificationConfigDTO.java @@ -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(); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/EmailNotificationConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/EmailNotificationConfigDTO.java similarity index 69% rename from backend/src/main/java/com/qqchen/deploy/backend/notification/dto/EmailNotificationConfig.java rename to backend/src/main/java/com/qqchen/deploy/backend/notification/dto/EmailNotificationConfigDTO.java index 5171f3df..6f856297 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/EmailNotificationConfig.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/EmailNotificationConfigDTO.java @@ -1,6 +1,8 @@ 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; @@ -8,10 +10,11 @@ import java.util.List; * 邮件通知配置DTO * * @author qqchen - * @since 2025-11-03 + * @since 2025-11-12 */ @Data -public class EmailNotificationConfig { +@EqualsAndHashCode(callSuper = true) +public class EmailNotificationConfigDTO extends BaseNotificationConfigDTO { /** * SMTP服务器地址(必填) @@ -52,5 +55,9 @@ public class EmailNotificationConfig { * 是否使用SSL(可选,默认true) */ private Boolean useSsl = true; + + @Override + public NotificationChannelTypeEnum getChannelType() { + return NotificationChannelTypeEnum.EMAIL; + } } - diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/NotificationChannelDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/NotificationChannelDTO.java index 468f2a9b..f8e6fb48 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/NotificationChannelDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/NotificationChannelDTO.java @@ -9,8 +9,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; -import java.util.Map; - /** * 通知渠道DTO * @@ -30,9 +28,9 @@ public class NotificationChannelDTO extends BaseDTO { @NotNull(message = "渠道类型不能为空") private NotificationChannelTypeEnum channelType; - @Schema(description = "渠道配置(JSON格式)", example = "{\"webhookUrl\":\"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx\"}") + @Schema(description = "渠道配置(根据渠道类型不同而不同)") @NotNull(message = "渠道配置不能为空") - private Map config; + private BaseNotificationConfigDTO config; @Schema(description = "状态", example = "ENABLED") private NotificationChannelStatusEnum status; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/WeworkNotificationConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/WeworkNotificationConfig.java deleted file mode 100644 index a76cc238..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/WeworkNotificationConfig.java +++ /dev/null @@ -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 mentionedMobileList; - - /** - * 默认@的用户列表(可选) - * 例如:["@all"] 表示@所有人 - */ - private List mentionedList; -} - diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/WeworkNotificationConfigDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/WeworkNotificationConfigDTO.java new file mode 100644 index 00000000..1f7b429b --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/WeworkNotificationConfigDTO.java @@ -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 mentionedMobileList; + + /** + * 默认@的用户列表(可选) + * 例如:["@all"] 表示@所有人 + */ + private List mentionedList; + + @Override + public NotificationChannelTypeEnum getChannelType() { + return NotificationChannelTypeEnum.WEWORK; + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/entity/config/BaseNotificationConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/entity/config/BaseNotificationConfig.java new file mode 100644 index 00000000..61d48d31 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/entity/config/BaseNotificationConfig.java @@ -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(); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/entity/config/EmailNotificationConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/entity/config/EmailNotificationConfig.java new file mode 100644 index 00000000..1cdd8e6d --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/entity/config/EmailNotificationConfig.java @@ -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 defaultReceivers; + + /** + * 是否使用SSL(可选,默认true) + */ + private Boolean useSsl = true; + + @Override + public NotificationChannelTypeEnum getChannelType() { + return NotificationChannelTypeEnum.EMAIL; + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/entity/config/WeworkNotificationConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/entity/config/WeworkNotificationConfig.java new file mode 100644 index 00000000..daf34ac7 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/entity/config/WeworkNotificationConfig.java @@ -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 mentionedMobileList; + + /** + * 默认@的用户列表(可选) + * 例如:["@all"] 表示@所有人 + */ + private List mentionedList; + + @Override + public NotificationChannelTypeEnum getChannelType() { + return NotificationChannelTypeEnum.WEWORK; + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationChannelServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationChannelServiceImpl.java index 62c2624d..d9badca9 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationChannelServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationChannelServiceImpl.java @@ -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.UniqueConstraintException; 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.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.NotificationChannelQuery; 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.enums.NotificationChannelStatusEnum; import com.qqchen.deploy.backend.notification.factory.NotificationChannelAdapterFactory; @@ -72,9 +76,12 @@ public class NotificationChannelServiceImpl throw new BusinessException(ResponseCode.ERROR, new Object[]{"不支持的渠道类型: " + channel.getChannelType()}); } - // 3. 执行连接测试 + // 3. 转换配置 + BaseNotificationConfig config = convertConfig(channel); + + // 4. 执行连接测试 try { - adapter.testConnection(channel.getConfig()); + adapter.testConnection(config); log.info("通知渠道连接测试成功: id={}, name={}", id, channel.getName()); return true; } catch (Exception e) { @@ -137,12 +144,15 @@ public class NotificationChannelServiceImpl throw new BusinessException(ResponseCode.ERROR); } - // 5. 发送通知 + // 5. 转换配置 + BaseNotificationConfig config = convertConfig(channel); + + // 6. 发送通知 try { log.info("发送通知 - 渠道ID: {}, 渠道类型: {}, 标题: {}", channel.getId(), channel.getChannelType(), request.getTitle()); - adapter.send(channel.getConfig(), request); + adapter.send(config, request); log.info("通知发送成功 - 渠道ID: {}", channel.getId()); } catch (Exception e) { @@ -150,5 +160,16 @@ public class NotificationChannelServiceImpl 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()); + }; + } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/WorkflowDefinitionApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/WorkflowDefinitionApiController.java index 55719163..77e551a7 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/WorkflowDefinitionApiController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/WorkflowDefinitionApiController.java @@ -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.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.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.dto.query.WorkflowDefinitionQuery; 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.servlet.http.HttpServletResponse; 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 java.util.HashMap; import java.util.List; -import java.util.Map; /** * 工作流定义控制器 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowDefinitionDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowDefinitionDTO.java index 21d12ae7..b8b03fd2 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowDefinitionDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowDefinitionDTO.java @@ -43,9 +43,9 @@ public class WorkflowDefinitionDTO extends BaseDTO { private Long formDefinitionId; /** - * 启动表单信息(用于展示) + * 启动表单名称(用于展示) */ - private FormDefinitionDTO formDefinition; + private String formDefinitionName; /** * 流程版本 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowDefinitionServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowDefinitionServiceImpl.java index 92ef5d4e..6e473acf 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowDefinitionServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowDefinitionServiceImpl.java @@ -38,11 +38,14 @@ import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; 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 new BusinessException(ResponseCode.WORKFLOW_NOT_FOUND)); - + // 校验:工作流必须绑定表单才能发布 if (definition.getFormDefinitionId() == null) { log.warn("工作流发布失败: 工作流 {} 未绑定表单", workflowDefinitionId); throw new BusinessException(ResponseCode.WORKFLOW_FORM_NOT_BOUND); } - + WorkflowDefinitionGraph graph = definition.getGraph(); definition.setBpmnXml(bpmnConverter.convertToXml(graph, definition.getKey())); Deployment deployment = this.deployWorkflow(definition); @@ -363,4 +363,62 @@ public class WorkflowDefinitionServiceImpl extends BaseServiceImpl page(WorkflowDefinitionQuery query) { + Page page = super.page(query); + fillFormDefinitionInfo(page.getContent()); + return new PageImpl<>(page.getContent(), page.getPageable(), page.getTotalElements()); + } + + /** + * 重写 findAll 方法,填充表单名称 + */ + @Override + public List findAll(WorkflowDefinitionQuery query) { + List list = super.findAll(query); + fillFormDefinitionInfo(list); + return list; + } + + /** + * 批量填充表单名称 + * 使用批量查询避免 N+1 问题 + */ + private void fillFormDefinitionInfo(List definitions) { + if (definitions == null || definitions.isEmpty()) { + return; + } + + // 收集所有表单ID + Set formDefinitionIds = definitions.stream() + .map(WorkflowDefinitionDTO::getFormDefinitionId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + if (formDefinitionIds.isEmpty()) { + return; + } + + // 批量查询表单定义,只获取ID和名称 + Map 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); + } + } + }); + } + } diff --git a/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql b/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql index a7e83044..fd5f141a 100644 --- a/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql +++ b/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql @@ -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), -- 工作流实例 (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), -- 表单数据详情(隐藏路由,用于查看表单数据) @@ -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), (315, NOW(), 102, 'workflow:instance:resume', '恢复实例', 'FUNCTION', 5), --- 表单管理 (menu_id=104) +-- 动态表单菜单 (menu_id=104) (321, NOW(), 104, 'workflow:form:list', '表单查询', 'FUNCTION', 1), (322, NOW(), 104, 'workflow:form:view', '表单详情', 'FUNCTION', 2), (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), -- (375, NOW(), NULL, 'workflow:category:delete', '工作流分类删除', 'FUNCTION', 25), -- --- -- 表单管理 (menu_id=104) +-- -- 动态表单菜单 (menu_id=104) -- (381, NOW(), 104, 'workflow:form:list', '表单查询', 'FUNCTION', 1), -- (382, NOW(), 104, 'workflow:form:view', '表单详情', 'FUNCTION', 2), -- (383, NOW(), 104, 'workflow:form:create', '表单创建', 'FUNCTION', 3),