增加构建通知

This commit is contained in:
dengqichen 2025-11-12 16:53:53 +08:00
parent 62c1a8b0d1
commit 13f8be9146
24 changed files with 999 additions and 55 deletions

View File

@ -229,7 +229,13 @@ public enum ResponseCode {
DEPLOY_PERMISSION_DENIED(3009, "deploy.permission.denied"),
DEPLOY_ENVIRONMENT_LOCKED(3010, "deploy.environment.locked"),
DEPLOY_APPROVAL_REQUIRED(3011, "deploy.approval.required"),
DEPLOY_RECORD_NOT_FOUND(3012, "deploy.record.not.found");
DEPLOY_RECORD_NOT_FOUND(3012, "deploy.record.not.found"),
// 通知模板相关错误码 (3100-3119)
NOTIFICATION_TEMPLATE_NOT_FOUND(3100, "notification.template.not.found"),
NOTIFICATION_TEMPLATE_CODE_EXISTS(3101, "notification.template.code.exists"),
NOTIFICATION_TEMPLATE_DISABLED(3102, "notification.template.disabled"),
NOTIFICATION_TEMPLATE_RENDER_ERROR(3103, "notification.template.render.error");
private final int code;
private final String messageKey; // 国际化消息key

View File

@ -0,0 +1,36 @@
package com.qqchen.deploy.backend.notification.api;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
import com.qqchen.deploy.backend.notification.service.INotificationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 通知发送API控制器
*
* @author qqchen
* @since 2025-11-12
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/notification")
@Tag(name = "通知发送", description = "通知发送相关接口")
@Validated
public class NotificationApiController {
@Resource
private INotificationService notificationService;
@Operation(summary = "发送通知消息")
@PostMapping("/send")
public Response<Void> send(@Valid @RequestBody SendNotificationRequest request) {
notificationService.send(request);
return Response.success();
}
}

View File

@ -4,11 +4,9 @@ import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.controller.BaseController;
import com.qqchen.deploy.backend.notification.dto.NotificationChannelDTO;
import com.qqchen.deploy.backend.notification.dto.NotificationChannelQuery;
import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest;
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
import com.qqchen.deploy.backend.notification.service.INotificationChannelService;
import com.qqchen.deploy.backend.notification.service.INotificationSendService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -42,9 +40,6 @@ public class NotificationChannelApiController extends BaseController<Notificatio
@Resource
private INotificationChannelService notificationChannelService;
@Resource
private INotificationSendService notificationSendService;
@Override
public Response<NotificationChannelDTO> create(@Validated @RequestBody NotificationChannelDTO dto) {
return super.create(dto);
@ -128,14 +123,6 @@ public class NotificationChannelApiController extends BaseController<Notificatio
return Response.success();
}
@Operation(summary = "发送通知消息")
@PostMapping("/send")
public Response<Void> send(
@Parameter(description = "通知请求", required = true) @RequestBody BaseSendNotificationRequest request
) {
notificationSendService.send(request);
return Response.success();
}
@Override
protected void exportData(HttpServletResponse response, List<NotificationChannelDTO> data) {

View File

@ -0,0 +1,73 @@
package com.qqchen.deploy.backend.notification.api;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.controller.BaseController;
import com.qqchen.deploy.backend.notification.dto.NotificationTemplateDTO;
import com.qqchen.deploy.backend.notification.dto.template.NotificationTemplateQuery;
import com.qqchen.deploy.backend.notification.dto.template.TemplateRenderRequest;
import com.qqchen.deploy.backend.notification.entity.NotificationTemplate;
import com.qqchen.deploy.backend.notification.service.INotificationTemplateService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* 通知模板API控制器
*
* @author qqchen
* @since 2025-11-12
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/notification-template")
@Tag(name = "通知模板管理", description = "通知模板的增删改查、渲染预览、发送测试等功能")
@Validated
public class NotificationTemplateApiController extends BaseController<NotificationTemplate, NotificationTemplateDTO, Long, NotificationTemplateQuery> {
@Resource
private INotificationTemplateService notificationTemplateService;
@Operation(summary = "根据编码获取模板")
@GetMapping("/code/{code}")
public Response<NotificationTemplateDTO> getByCode(@Parameter(description = "模板编码", required = true) @PathVariable String code) {
NotificationTemplateDTO template = notificationTemplateService.getByCode(code);
return Response.success(template);
}
@Operation(summary = "渲染模板预览")
@PostMapping("/render")
public Response<String> renderTemplate(@Valid @RequestBody TemplateRenderRequest request) {
String result = notificationTemplateService.renderTemplate(request.getTemplateCode(), request.getParams());
return Response.success(result);
}
@Operation(summary = "启用通知模板")
@PutMapping("/{id}/enable")
public Response<Void> enable(@Parameter(description = "模板ID", required = true) @PathVariable Long id) {
notificationTemplateService.enable(id);
return Response.success();
}
@Operation(summary = "禁用通知模板")
@PutMapping("/{id}/disable")
public Response<Void> disable(@Parameter(description = "模板ID", required = true) @PathVariable Long id) {
notificationTemplateService.disable(id);
return Response.success();
}
@Override
protected void exportData(HttpServletResponse response, List<NotificationTemplateDTO> data) {
// TODO: 实现导出功能
}
}

View File

@ -0,0 +1,45 @@
package com.qqchen.deploy.backend.notification.config;
import freemarker.template.Configuration;
import freemarker.template.TemplateExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
* FreeMarker配置
*
* @author qqchen
* @since 2025-11-12
*/
@Component
public class FreeMarkerConfig {
@Bean
public Configuration freemarkerConfig() {
Configuration config = new Configuration(Configuration.VERSION_2_3_32);
// 设置默认编码
config.setDefaultEncoding(StandardCharsets.UTF_8.name());
// 设置异常处理器
config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
// 设置日期格式
config.setDateFormat("yyyy-MM-dd");
config.setTimeFormat("HH:mm:ss");
config.setDateTimeFormat("yyyy-MM-dd HH:mm:ss");
// 设置数字格式避免科学计数法
config.setNumberFormat("0.######");
// 设置布尔值格式
config.setBooleanFormat("true,false");
// 禁用自动转义因为我们可能需要输出HTML
config.setAutoEscapingPolicy(Configuration.DISABLE_AUTO_ESCAPING_POLICY);
return config;
}
}

View File

@ -58,7 +58,7 @@ public abstract class NotificationChannelConverter implements BaseConverter<Noti
// 更新基本字段
entity.setName(dto.getName());
entity.setChannelType(dto.getChannelType());
entity.setStatus(dto.getStatus());
entity.setEnabled(dto.getEnabled());
entity.setDescription(dto.getDescription());
// 更新config字段

View File

@ -0,0 +1,16 @@
package com.qqchen.deploy.backend.notification.converter;
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
import com.qqchen.deploy.backend.notification.dto.NotificationTemplateDTO;
import com.qqchen.deploy.backend.notification.entity.NotificationTemplate;
import org.mapstruct.Mapper;
/**
* 通知模板转换器
*
* @author qqchen
* @since 2025-11-12
*/
@Mapper(componentModel = "spring")
public interface NotificationTemplateConverter extends BaseConverter<NotificationTemplate, NotificationTemplateDTO> {
}

View File

@ -1,7 +1,6 @@
package com.qqchen.deploy.backend.notification.dto;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelStatusEnum;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@ -32,8 +31,8 @@ public class NotificationChannelDTO extends BaseDTO {
@NotNull(message = "渠道配置不能为空")
private BaseNotificationConfigDTO config;
@Schema(description = "状态", example = "ENABLED")
private NotificationChannelStatusEnum status;
@Schema(description = "是否启用", example = "true")
private Boolean enabled;
@Schema(description = "描述", example = "研发部通知群,用于部署通知")
private String description;

View File

@ -0,0 +1,47 @@
package com.qqchen.deploy.backend.notification.dto;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 通知模板DTO
*
* @author qqchen
* @since 2025-11-12
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "通知模板DTO")
public class NotificationTemplateDTO extends BaseDTO {
@Schema(description = "模板名称", example = "Jenkins构建通知")
@NotBlank(message = "模板名称不能为空")
@Size(max = 100, message = "模板名称长度不能超过100个字符")
private String name;
@Schema(description = "模板编码", example = "jenkins_build_wework")
@NotBlank(message = "模板编码不能为空")
@Size(max = 50, message = "模板编码长度不能超过50个字符")
private String code;
@Schema(description = "模板描述", example = "用于Jenkins构建结果通知的模板")
@Size(max = 500, message = "模板描述长度不能超过500个字符")
private String description;
@Schema(description = "渠道类型", example = "WEWORK")
@NotNull(message = "渠道类型不能为空")
private NotificationChannelTypeEnum channelType;
@Schema(description = "内容模板", example = "### 构建通知\\n**项目**: ${projectName}")
@NotBlank(message = "内容模板不能为空")
private String contentTemplate;
@Schema(description = "是否启用", example = "true")
private Boolean enabled = true;
}

View File

@ -0,0 +1,29 @@
package com.qqchen.deploy.backend.notification.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Map;
/**
* 发送通知请求
*
* @author qqchen
* @since 2025-11-12
*/
@Data
@Schema(description = "发送通知请求")
public class SendNotificationRequest {
@Schema(description = "渠道ID", required = true, example = "1")
@NotNull(message = "渠道ID不能为空")
private Long channelId;
@Schema(description = "通知模板ID", required = true, example = "1")
@NotNull(message = "通知模板ID不能为空")
private Long notificationTemplateId;
@Schema(description = "模板参数", example = "{\"projectName\":\"测试项目\",\"buildNumber\":\"123\"}")
private Map<String, Object> params;
}

View File

@ -0,0 +1,37 @@
package com.qqchen.deploy.backend.notification.dto.template;
import com.qqchen.deploy.backend.framework.annotation.QueryField;
import com.qqchen.deploy.backend.framework.enums.QueryType;
import com.qqchen.deploy.backend.framework.query.BaseQuery;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 通知模板查询条件
*
* @author qqchen
* @since 2025-11-12
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "通知模板查询条件")
public class NotificationTemplateQuery extends BaseQuery {
@QueryField(type = QueryType.LIKE)
@Schema(description = "模板名称(模糊查询)", example = "Jenkins")
private String name;
@QueryField(type = QueryType.LIKE)
@Schema(description = "模板编码(模糊查询)", example = "jenkins")
private String code;
@QueryField(type = QueryType.EQUAL)
@Schema(description = "渠道类型", example = "WEWORK")
private NotificationChannelTypeEnum channelType;
@QueryField(type = QueryType.EQUAL)
@Schema(description = "是否启用", example = "true")
private Boolean enabled;
}

View File

@ -0,0 +1,25 @@
package com.qqchen.deploy.backend.notification.dto.template;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.Map;
/**
* 模板渲染请求
*
* @author qqchen
* @since 2025-11-12
*/
@Data
@Schema(description = "模板渲染请求")
public class TemplateRenderRequest {
@Schema(description = "模板编码", example = "jenkins_build_wework")
@NotBlank(message = "模板编码不能为空")
private String templateCode;
@Schema(description = "模板参数", example = "{\"projectName\": \"deploy-ease-platform\", \"buildNumber\": 123}")
private Map<String, Object> params;
}

View File

@ -0,0 +1,30 @@
package com.qqchen.deploy.backend.notification.dto.template;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Map;
/**
* 模板发送请求
*
* @author qqchen
* @since 2025-11-12
*/
@Data
@Schema(description = "模板发送请求")
public class TemplateSendRequest {
@Schema(description = "模板编码", example = "jenkins_build_wework")
@NotBlank(message = "模板编码不能为空")
private String templateCode;
@Schema(description = "渠道ID", example = "1")
@NotNull(message = "渠道ID不能为空")
private Long channelId;
@Schema(description = "模板参数", example = "{\"projectName\": \"deploy-ease-platform\", \"buildNumber\": 123}")
private Map<String, Object> params;
}

View File

@ -2,7 +2,6 @@ package com.qqchen.deploy.backend.notification.entity;
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
import com.qqchen.deploy.backend.framework.domain.Entity;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelStatusEnum;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
import com.vladmihalcea.hibernate.type.json.JsonType;
import jakarta.persistence.*;
@ -46,11 +45,10 @@ public class NotificationChannel extends Entity<Long> {
private Map<String, Object> config;
/**
* 状态
* 是否启用
*/
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private NotificationChannelStatusEnum status = NotificationChannelStatusEnum.ENABLED;
@Column(nullable = false)
private Boolean enabled = true;
/**
* 描述

View File

@ -0,0 +1,59 @@
package com.qqchen.deploy.backend.notification.entity;
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
import com.qqchen.deploy.backend.framework.domain.Entity;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 通知模板实体
*
* @author qqchen
* @since 2025-11-12
*/
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "sys_notification_template")
@LogicDelete
public class NotificationTemplate extends Entity<Long> {
/**
* 模板名称
*/
@Column(nullable = false, length = 100)
private String name;
/**
* 模板编码唯一标识
*/
@Column(nullable = false, length = 50, unique = true)
private String code;
/**
* 模板描述
*/
@Column(length = 500)
private String description;
/**
* 渠道类型
*/
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private NotificationChannelTypeEnum channelType;
/**
* 内容模板FreeMarker格式
*/
@Column(nullable = false, columnDefinition = "TEXT")
private String contentTemplate;
/**
* 是否启用
*/
@Column(nullable = false)
private Boolean enabled = true;
}

View File

@ -16,7 +16,7 @@ import java.util.List;
*/
@Repository
public interface INotificationChannelRepository extends IBaseRepository<NotificationChannel, Long> {
/**
* 根据名称和删除标记查询是否存在
*
@ -24,25 +24,6 @@ public interface INotificationChannelRepository extends IBaseRepository<Notifica
* @return 是否存在
*/
boolean existsByNameAndDeletedFalse(String name);
/**
* 根据渠道类型和状态查询渠道列表
*
* @param channelType 渠道类型
* @param status 状态
* @return 渠道列表
*/
List<NotificationChannel> findByChannelTypeAndStatusAndDeletedFalse(
NotificationChannelTypeEnum channelType,
NotificationChannelStatusEnum status
);
/**
* 根据状态查询渠道列表
*
* @param status 状态
* @return 渠道列表
*/
List<NotificationChannel> findByStatusAndDeletedFalse(NotificationChannelStatusEnum status);
}

View File

@ -0,0 +1,60 @@
package com.qqchen.deploy.backend.notification.repository;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import com.qqchen.deploy.backend.notification.entity.NotificationTemplate;
import com.qqchen.deploy.backend.notification.enums.NotificationChannelTypeEnum;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* 通知模板Repository接口
*
* @author qqchen
* @since 2025-11-12
*/
@Repository
public interface INotificationTemplateRepository extends IBaseRepository<NotificationTemplate, Long> {
/**
* 根据编码查找模板
*/
Optional<NotificationTemplate> findByCode(String code);
/**
* 根据编码和启用状态查找模板
*/
Optional<NotificationTemplate> findByCodeAndEnabled(String code, Boolean enabled);
/**
* 根据渠道类型查找模板
*/
List<NotificationTemplate> findByChannelTypeAndEnabled(NotificationChannelTypeEnum channelType, Boolean enabled);
/**
* 检查编码是否存在排除指定ID
*/
@Query("SELECT COUNT(t) > 0 FROM NotificationTemplate t WHERE t.code = :code AND t.id != :id")
boolean existsByCodeAndIdNot(@Param("code") String code, @Param("id") Long id);
/**
* 分页查询模板
*/
@Query("SELECT t FROM NotificationTemplate t WHERE " +
"(:name IS NULL OR t.name LIKE %:name%) AND " +
"(:code IS NULL OR t.code LIKE %:code%) AND " +
"(:channelType IS NULL OR t.channelType = :channelType) AND " +
"(:enabled IS NULL OR t.enabled = :enabled)")
Page<NotificationTemplate> findByConditions(
@Param("name") String name,
@Param("code") String code,
@Param("channelType") NotificationChannelTypeEnum channelType,
@Param("enabled") Boolean enabled,
Pageable pageable
);
}

View File

@ -0,0 +1,19 @@
package com.qqchen.deploy.backend.notification.service;
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
/**
* 通知服务接口
*
* @author qqchen
* @since 2025-11-12
*/
public interface INotificationService {
/**
* 发送通知消息
*
* @param request 发送请求包含渠道ID模板ID和参数
*/
void send(SendNotificationRequest request);
}

View File

@ -0,0 +1,58 @@
package com.qqchen.deploy.backend.notification.service;
import com.qqchen.deploy.backend.framework.service.IBaseService;
import com.qqchen.deploy.backend.notification.dto.NotificationTemplateDTO;
import com.qqchen.deploy.backend.notification.dto.template.NotificationTemplateQuery;
import com.qqchen.deploy.backend.notification.entity.NotificationTemplate;
import java.util.Map;
/**
* 通知模板Service接口
*
* @author qqchen
* @since 2025-11-12
*/
public interface INotificationTemplateService extends IBaseService<NotificationTemplate, NotificationTemplateDTO, NotificationTemplateQuery, Long> {
/**
* 渲染模板
*
* @param templateCode 模板编码
* @param params 模板参数
* @return 渲染后的内容
*/
String renderTemplate(String templateCode, Map<String, Object> params);
/**
* 根据编码获取模板
*
* @param code 模板编码
* @return 模板DTO
*/
NotificationTemplateDTO getByCode(String code);
/**
* 检查编码是否存在
*
* @param code 模板编码
* @param id 排除的ID编辑时使用
* @return 是否存在
*/
boolean existsByCode(String code, Long id);
/**
* 启用通知模板
*
* @param id 模板ID
*/
void enable(Long id);
/**
* 禁用通知模板
*
* @param id 模板ID
*/
void disable(Long id);
}

View File

@ -15,7 +15,6 @@ import com.qqchen.deploy.backend.notification.dto.NotificationChannelQuery;
import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest;
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;
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
import com.qqchen.deploy.backend.notification.service.INotificationChannelService;
@ -97,7 +96,7 @@ public class NotificationChannelServiceImpl
NotificationChannel channel = notificationChannelRepository.findById(id)
.orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND));
channel.setStatus(NotificationChannelStatusEnum.ENABLED);
channel.setEnabled(true);
notificationChannelRepository.save(channel);
log.info("启用通知渠道: id={}, name={}", id, channel.getName());
@ -109,7 +108,7 @@ public class NotificationChannelServiceImpl
NotificationChannel channel = notificationChannelRepository.findById(id)
.orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND));
channel.setStatus(NotificationChannelStatusEnum.DISABLED);
channel.setEnabled(false);
notificationChannelRepository.save(channel);
log.info("禁用通知渠道: id={}, name={}", id, channel.getName());
@ -130,7 +129,7 @@ public class NotificationChannelServiceImpl
.orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND));
// 3. 校验渠道状态
if (channel.getStatus() != NotificationChannelStatusEnum.ENABLED) {
if (!channel.getEnabled()) {
throw new BusinessException(ResponseCode.DATA_NOT_FOUND);
}

View File

@ -0,0 +1,147 @@
package com.qqchen.deploy.backend.notification.service.impl;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
import com.qqchen.deploy.backend.notification.entity.NotificationTemplate;
import com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum;
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
import com.qqchen.deploy.backend.notification.repository.INotificationTemplateRepository;
import com.qqchen.deploy.backend.notification.service.INotificationSendService;
import com.qqchen.deploy.backend.notification.service.INotificationService;
import com.qqchen.deploy.backend.notification.service.INotificationTemplateService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* 通知服务实现类
*
* @author qqchen
* @since 2025-11-12
*/
@Slf4j
@Service
public class NotificationServiceImpl implements INotificationService {
@Resource
private INotificationTemplateService notificationTemplateService;
@Resource
private INotificationSendService notificationSendService;
@Resource
private INotificationChannelRepository notificationChannelRepository;
@Resource
private INotificationTemplateRepository notificationTemplateRepository;
@Override
public void send(SendNotificationRequest request) {
// 1. 获取通知模板
NotificationTemplate template = notificationTemplateRepository.findById(request.getNotificationTemplateId())
.orElseThrow(() -> new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_NOT_FOUND));
// 2. 获取通知渠道
NotificationChannel channel = notificationChannelRepository.findById(request.getChannelId())
.orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND));
// 3. 验证模板和渠道类型是否匹配
if (!template.getChannelType().equals(channel.getChannelType())) {
throw new BusinessException(ResponseCode.INVALID_PARAM);
}
// 4. 验证模板和渠道是否启用
if (!template.getEnabled()) {
throw new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_DISABLED);
}
if (!channel.getEnabled()) {
throw new BusinessException(ResponseCode.DATA_NOT_FOUND);
}
// 5. 渲染模板内容
String content = notificationTemplateService.renderTemplate(template.getCode(), request.getParams());
// 6. 根据渠道类型发送通知
switch (template.getChannelType()) {
case WEWORK -> sendWeworkNotification(request.getChannelId(), content, request.getParams());
case EMAIL -> sendEmailNotification(request.getChannelId(), content, request.getParams());
default -> throw new BusinessException(ResponseCode.INVALID_PARAM);
}
}
/**
* 发送企业微信通知
*/
private void sendWeworkNotification(Long channelId, String content, Map<String, Object> params) {
WeworkSendNotificationRequest request = new WeworkSendNotificationRequest();
request.setChannelId(channelId);
request.setContent(content);
// 根据内容判断消息类型或从参数中获取
WeworkMessageTypeEnum messageType = WeworkMessageTypeEnum.MARKDOWN; // 默认使用 MARKDOWN
if (params != null && params.containsKey("messageType")) {
messageType = WeworkMessageTypeEnum.valueOf(params.get("messageType").toString());
}
request.setMessageType(messageType);
// 从参数中获取@人员信息
if (params != null) {
if (params.containsKey("mentionedMobileList")) {
request.setMentionedMobileList((List<String>) params.get("mentionedMobileList"));
}
if (params.containsKey("mentionedUserList")) {
request.setMentionedUserList((List<String>) params.get("mentionedUserList"));
}
}
notificationSendService.send(request);
}
/**
* 发送邮件通知
*/
private void sendEmailNotification(Long channelId, String content, Map<String, Object> params) {
EmailSendNotificationRequest request = new EmailSendNotificationRequest();
request.setChannelId(channelId);
request.setContent(content);
// 邮件标题从参数中获取或使用默认值
String subject = "系统通知";
if (params != null && params.containsKey("subject")) {
subject = params.get("subject").toString();
}
request.setSubject(subject);
// 是否HTML格式从参数中获取
Boolean isHtml = false;
if (params != null && params.containsKey("isHtml")) {
isHtml = (Boolean) params.get("isHtml");
}
request.setIsHtml(isHtml);
// 收件人信息必须
if (params == null || !params.containsKey("toReceivers")) {
throw new BusinessException(ResponseCode.INVALID_PARAM);
}
request.setToReceivers((List<String>) params.get("toReceivers"));
// 可选参数
if (params != null) {
if (params.containsKey("ccReceivers")) {
request.setCcReceivers((List<String>) params.get("ccReceivers"));
}
if (params.containsKey("bccReceivers")) {
request.setBccReceivers((List<String>) params.get("bccReceivers"));
}
}
notificationSendService.send(request);
}
}

View File

@ -0,0 +1,118 @@
package com.qqchen.deploy.backend.notification.service.impl;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.notification.converter.NotificationTemplateConverter;
import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.NotificationTemplateDTO;
import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.template.NotificationTemplateQuery;
import com.qqchen.deploy.backend.notification.entity.NotificationTemplate;
import com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum;
import com.qqchen.deploy.backend.notification.repository.INotificationTemplateRepository;
import com.qqchen.deploy.backend.notification.service.INotificationSendService;
import com.qqchen.deploy.backend.notification.service.INotificationTemplateService;
import freemarker.template.Configuration;
import freemarker.template.Template;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.List;
import java.util.Map;
/**
* 通知模板Service实现类
*
* @author qqchen
* @since 2025-11-12
*/
@Slf4j
@Service
public class NotificationTemplateServiceImpl extends BaseServiceImpl<NotificationTemplate, NotificationTemplateDTO, NotificationTemplateQuery, Long> implements INotificationTemplateService {
@Resource
private INotificationTemplateRepository notificationTemplateRepository;
@Resource
private NotificationTemplateConverter notificationTemplateConverter;
@Resource
private Configuration freemarkerConfig;
@Override
protected void validateUniqueConstraints(NotificationTemplateDTO dto) {
// 检查编码是否重复
if (existsByCode(dto.getCode(), dto.getId())) {
throw new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_CODE_EXISTS);
}
}
@Override
public String renderTemplate(String templateCode, Map<String, Object> params) {
NotificationTemplate template = getTemplateByCode(templateCode);
return processTemplate(template.getContentTemplate(), params);
}
@Override
public NotificationTemplateDTO getByCode(String code) {
NotificationTemplate template = getTemplateByCode(code);
return notificationTemplateConverter.toDto(template);
}
@Override
public boolean existsByCode(String code, Long id) {
if (id == null) {
return notificationTemplateRepository.findByCode(code).isPresent();
}
return notificationTemplateRepository.existsByCodeAndIdNot(code, id);
}
/**
* 根据编码获取模板实体
*/
private NotificationTemplate getTemplateByCode(String code) {
return notificationTemplateRepository.findByCodeAndEnabled(code, true)
.orElseThrow(() -> new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_NOT_FOUND));
}
/**
* FreeMarker模板处理
*/
private String processTemplate(String templateContent, Map<String, Object> params) {
try {
Template template = new Template("notification", new StringReader(templateContent), freemarkerConfig);
StringWriter writer = new StringWriter();
template.process(params, writer);
return writer.toString();
} catch (Exception e) {
log.error("模板渲染失败: {}", e.getMessage(), e);
throw new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_RENDER_ERROR);
}
}
@Override
@Transactional
public void enable(Long id) {
NotificationTemplate template = notificationTemplateRepository.findById(id)
.orElseThrow(() -> new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_NOT_FOUND));
template.setEnabled(true);
notificationTemplateRepository.save(template);
}
@Override
@Transactional
public void disable(Long id) {
NotificationTemplate template = notificationTemplateRepository.findById(id)
.orElseThrow(() -> new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_NOT_FOUND));
template.setEnabled(false);
notificationTemplateRepository.save(template);
}
}

View File

@ -75,8 +75,10 @@ VALUES
(203, '定时任务管理', '/deploy/schedule-jobs', 'Deploy/ScheduleJob/List', 'ClockCircleOutlined', 'deploy:schedule-job', 2, 200, 3, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
-- 环境管理
(204, '环境管理', '/deploy/environments', 'Deploy/Environment/List', 'CloudOutlined', 'deploy:environment', 2, 200, 4, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
-- 消息中心
(205, '消息中心', '/deploy/notification-channels', 'Deploy/NotificationChannel/List', 'BellOutlined', 'deploy:notification-channel', 2, 200, 5, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
-- 消息渠道管理
(205, '消息渠道管理', '/deploy/notification-channels', 'Deploy/NotificationChannel/List', 'BellOutlined', 'deploy:notification-channel', 2, 200, 5, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
-- 通知模板
(206, '通知模板', '/deploy/notification-templates', 'Deploy/NotificationTemplate/List', 'FileTextOutlined', 'deploy:notification-template', 2, 200, 6, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
-- 资源管理
(300, '资源管理', '/resource', NULL, 'DatabaseOutlined', NULL, 1, NULL, 3, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
@ -301,7 +303,16 @@ INSERT INTO sys_permission (id, create_time, menu_id, code, name, type, sort) VA
(323, NOW(), 104, 'workflow:form:create', '表单创建', 'FUNCTION', 3),
(324, NOW(), 104, 'workflow:form:update', '表单修改', 'FUNCTION', 4),
(325, NOW(), 104, 'workflow:form:delete', '表单删除', 'FUNCTION', 5),
(326, NOW(), 104, 'workflow:form:publish', '发布表单', 'FUNCTION', 6);
(326, NOW(), 104, 'workflow:form:publish', '发布表单', 'FUNCTION', 6),
-- 通知模板管理 (menu_id=206)
(2061, NOW(), 206, 'notification:template:view', '查看通知模板', 'FUNCTION', 1),
(2062, NOW(), 206, 'notification:template:create', '新增通知模板', 'FUNCTION', 2),
(2063, NOW(), 206, 'notification:template:update', '编辑通知模板', 'FUNCTION', 3),
(2064, NOW(), 206, 'notification:template:delete', '删除通知模板', 'FUNCTION', 4),
(2065, NOW(), 206, 'notification:template:toggle', '启用/禁用通知模板', 'FUNCTION', 5),
(2066, NOW(), 206, 'notification:template:copy', '复制通知模板', 'FUNCTION', 6),
(2067, NOW(), 206, 'notification:template:preview', '预览通知模板', 'FUNCTION', 7);
--
-- -- 团队配置管理 (无对应菜单menu_id=NULL)
@ -1195,11 +1206,11 @@ INSERT INTO workflow_node_definition (
-- 企业微信通知渠道示例
INSERT INTO sys_notification_channel
(name, channel_type, config, status, description, create_by, create_time, update_by, update_time, version, deleted)
(name, channel_type, config, enabled, description, create_by, create_time, update_by, update_time, version, deleted)
VALUES
('研发部企业微信群', 'WEWORK',
'{"webhookUrl":"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=example-key-please-replace"}',
'DISABLED',
0,
'研发部通知群用于部署通知示例数据请修改为实际的Webhook地址',
'admin', NOW(), 'admin', NOW(), 0, 0);
@ -1308,3 +1319,144 @@ INSERT INTO deploy_server_category (id, name, code, icon, description, sort, ena
(3, '中间件服务器', 'MIDDLEWARE_SERVER', 'cluster', '消息队列、搜索引擎等中间件', 3, 1, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 1, 0),
(4, '应用服务器', 'APP_SERVER', 'cloud-server', '业务应用服务器', 4, 1, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 1, 0),
(5, '其他', 'OTHER', 'hdd', '其他类型服务器', 99, 1, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 1, 0);
-- =====================================================
-- 通知模板初始数据
-- =====================================================
-- 插入通知模板初始数据
INSERT INTO sys_notification_template (id, create_by, create_time, update_by, update_time, version, deleted,
name, code, description, channel_type, content_template, enabled) VALUES
-- Jenkins构建通知模板
(1, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 1, 0,
'Jenkins构建通知-企业微信', 'jenkins_build_wework', 'Jenkins构建结果通知模板企业微信', 'WEWORK',
'### 🚀 Jenkins构建通知
****: ${projectName}
****: #${buildNumber}
****: <#if buildStatus == "SUCCESS">✅ 成功<#elseif buildStatus == "FAILURE">❌ 失败<#else>🔄 构建中</#if>
<#if buildTime??>**构建时间**: ${buildTime}</#if>
<#if duration??>**构建耗时**: ${duration}</#if>
<#if buildStatus == "FAILURE">
****
</#if>
---
*Deploy Ease Platform *', 1),
(2, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 1, 0,
'Jenkins构建通知-邮件', 'jenkins_build_email', 'Jenkins构建结果通知模板邮件', 'EMAIL',
'<h2>🚀 Jenkins构建通知</h2>
<table style="border-collapse: collapse; width: 100%;">
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong></strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${projectName}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong></strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">#${buildNumber}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong></strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">
<#if buildStatus == "SUCCESS">
<span style="color: green;"> </span>
<#elseif buildStatus == "FAILURE">
<span style="color: red;"> </span>
<#else>
<span style="color: orange;">🔄 </span>
</#if>
</td>
</tr>
<#if buildTime??>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong></strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${buildTime}</td>
</tr>
</#if>
<#if duration??>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong></strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${duration}</td>
</tr>
</#if>
</table>
<#if buildStatus == "FAILURE">
<p style="color: red; font-weight: bold;"> </p>
</#if>
<hr>
<p style="color: #666; font-size: 12px;">Deploy Ease Platform </p>', 1),
-- 部署通知模板
(3, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 1, 0,
'部署通知-企业微信', 'deploy_notification_wework', '应用部署结果通知模板(企业微信)', 'WEWORK',
'### 📦 应用部署通知
****: ${appName}
****: ${environment}
****: <#if deployStatus == "SUCCESS">✅ 部署成功<#else>❌ 部署失败</#if>
****: ${version}
****: ${deployTime}
<#if deployUrl??>
**访**: ${deployUrl}
</#if>
<#if deployStatus == "SUCCESS">
🎉 **线**
<#else>
****
</#if>
---
*Deploy Ease Platform *', 1),
(4, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 1, 0,
'部署通知-邮件', 'deploy_notification_email', '应用部署结果通知模板(邮件)', 'EMAIL',
'<h2>📦 应用部署通知</h2>
<table style="border-collapse: collapse; width: 100%;">
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong></strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${appName}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong></strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${environment}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong></strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">
<#if deployStatus == "SUCCESS">
<span style="color: green;"> </span>
<#else>
<span style="color: red;"> </span>
</#if>
</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong></strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${version}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong></strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${deployTime}</td>
</tr>
<#if deployUrl??>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>访</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;"><a href="${deployUrl}">${deployUrl}</a></td>
</tr>
</#if>
</table>
<#if deployStatus == "SUCCESS">
<p style="color: green; font-weight: bold;">🎉 线</p>
<#else>
<p style="color: red; font-weight: bold;"> </p>
</#if>
<hr>
<p style="color: #666; font-size: 12px;">Deploy Ease Platform </p>', 1);

View File

@ -926,11 +926,11 @@ CREATE TABLE sys_notification_channel
name VARCHAR(100) NOT NULL COMMENT '渠道名称(如:研发部企业微信群)',
channel_type VARCHAR(50) NOT NULL COMMENT '渠道类型WEWORK, FEISHU, DINGTALK, SMS, EMAIL, SLACK',
config JSON NOT NULL COMMENT '渠道配置JSON格式不同渠道存储不同字段',
status VARCHAR(20) NOT NULL DEFAULT 'ENABLED' COMMENT '状态ENABLED-启用, DISABLED-禁用)',
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用',
description VARCHAR(500) NULL COMMENT '描述说明',
INDEX idx_channel_type (channel_type),
INDEX idx_status (status),
INDEX idx_enabled (enabled),
INDEX idx_deleted (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通知渠道配置表';
@ -1144,3 +1144,26 @@ CREATE TABLE deploy_record
CONSTRAINT fk_deploy_record_team_app FOREIGN KEY (team_application_id) REFERENCES deploy_team_application (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部署记录表';
-- 通知模板表
CREATE TABLE sys_notification_template
(
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(100) NULL COMMENT '创建人',
create_time DATETIME(6) NULL COMMENT '创建时间',
update_by VARCHAR(100) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 1 COMMENT '版本号',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
name VARCHAR(100) NOT NULL COMMENT '模板名称',
code VARCHAR(50) NOT NULL COMMENT '模板编码',
description VARCHAR(500) NULL COMMENT '模板描述',
channel_type VARCHAR(20) NOT NULL COMMENT '渠道类型',
content_template TEXT NOT NULL COMMENT '内容模板',
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用',
UNIQUE KEY uk_code (code),
KEY idx_channel_type (channel_type),
KEY idx_enabled (enabled)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通知模板表';