增加ssh链接框架

This commit is contained in:
dengqichen 2025-12-07 21:12:08 +08:00
parent ff149be46f
commit 460f237211
47 changed files with 2185 additions and 211 deletions

View File

@ -0,0 +1,102 @@
package com.qqchen.deploy.backend.deploy.api;
import com.qqchen.deploy.backend.deploy.dto.ServerAlertRuleDTO;
import com.qqchen.deploy.backend.deploy.entity.ServerAlertRule;
import com.qqchen.deploy.backend.deploy.query.ServerAlertRuleQuery;
import com.qqchen.deploy.backend.deploy.service.IServerAlertRuleService;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.controller.BaseController;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* 服务器告警规则 Controller
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/server/alert-rule")
@Tag(name = "服务器告警规则管理", description = "服务器告警规则相关接口")
public class ServerAlertRuleApiController
extends BaseController<ServerAlertRule, ServerAlertRuleDTO, Long, ServerAlertRuleQuery> {
@Resource
private IServerAlertRuleService alertRuleService;
@Override
@Operation(summary = "创建告警规则", description = "创建新的服务器告警规则")
@PostMapping
public Response<ServerAlertRuleDTO> create(@Validated @RequestBody ServerAlertRuleDTO dto) {
return super.create(dto);
}
@Override
@Operation(summary = "更新告警规则", description = "更新指定ID的告警规则")
@PutMapping("/{id}")
public Response<ServerAlertRuleDTO> update(
@Parameter(description = "告警规则ID", required = true) @PathVariable Long id,
@Validated @RequestBody ServerAlertRuleDTO dto
) {
return super.update(id, dto);
}
@Override
@Operation(summary = "删除告警规则", description = "删除指定ID的告警规则逻辑删除")
@DeleteMapping("/{id}")
public Response<Void> delete(
@Parameter(description = "告警规则ID", required = true) @PathVariable Long id
) {
return super.delete(id);
}
@Override
@Operation(summary = "查询告警规则详情", description = "根据ID查询告警规则详情")
@GetMapping("/{id}")
public Response<ServerAlertRuleDTO> findById(
@Parameter(description = "告警规则ID", required = true) @PathVariable Long id
) {
return super.findById(id);
}
@Override
@Operation(summary = "查询所有告警规则", description = "查询所有告警规则列表")
@GetMapping
public Response<List<ServerAlertRuleDTO>> findAll() {
return super.findAll();
}
@Override
@Operation(summary = "分页查询告警规则", description = "根据条件分页查询告警规则")
@GetMapping("/page")
public Response<Page<ServerAlertRuleDTO>> page(ServerAlertRuleQuery query) {
return super.page(query);
}
@Override
@Operation(summary = "条件查询告警规则列表", description = "根据条件查询告警规则列表")
@GetMapping("/list")
public Response<List<ServerAlertRuleDTO>> findAll(ServerAlertRuleQuery query) {
return super.findAll(query);
}
@Override
@Operation(summary = "批量处理告警规则", description = "批量创建/更新/删除告警规则")
@PostMapping("/batch")
public CompletableFuture<Response<Void>> batchProcess(@RequestBody List<ServerAlertRuleDTO> dtos) {
return super.batchProcess(dtos);
}
@Override
protected void exportData(HttpServletResponse response, List<ServerAlertRuleDTO> data) {
log.info("导出告警规则数据,数据量:{}", data.size());
}
}

View File

@ -0,0 +1,13 @@
package com.qqchen.deploy.backend.deploy.converter;
import com.qqchen.deploy.backend.deploy.dto.ServerAlertRuleDTO;
import com.qqchen.deploy.backend.deploy.entity.ServerAlertRule;
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
import org.mapstruct.Mapper;
/**
* 服务器告警规则转换器
*/
@Mapper(config = BaseConverter.class)
public interface ServerAlertRuleConverter extends BaseConverter<ServerAlertRule, ServerAlertRuleDTO> {
}

View File

@ -0,0 +1,61 @@
package com.qqchen.deploy.backend.deploy.dto;
import com.qqchen.deploy.backend.deploy.enums.AlertTypeEnum;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 服务器告警规则DTO
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "服务器告警规则")
public class ServerAlertRuleDTO extends BaseDTO {
@Schema(description = "服务器IDNULL表示全局规则")
private Long serverId;
@Schema(description = "规则名称", required = true)
@NotBlank(message = "规则名称不能为空")
@Size(max = 100, message = "规则名称长度不能超过100个字符")
private String ruleName;
@Schema(description = "告警类型: CPU/MEMORY/DISK", required = true)
@NotNull(message = "告警类型不能为空")
private AlertTypeEnum alertType;
@Schema(description = "警告阈值(%)", required = true, example = "80.00")
@NotNull(message = "警告阈值不能为空")
@DecimalMin(value = "0.00", message = "警告阈值必须大于等于0")
@DecimalMax(value = "100.00", message = "警告阈值不能超过100")
private BigDecimal warningThreshold;
@Schema(description = "严重阈值(%)", required = true, example = "90.00")
@NotNull(message = "严重阈值不能为空")
@DecimalMin(value = "0.00", message = "严重阈值必须大于等于0")
@DecimalMax(value = "100.00", message = "严重阈值不能超过100")
private BigDecimal criticalThreshold;
@Schema(description = "持续时长(分钟)", example = "5")
@Min(value = 1, message = "持续时长至少为1分钟")
private Integer durationMinutes;
@Schema(description = "是否启用")
private Boolean enabled;
@Schema(description = "通知方式: EMAIL/SMS/WEBHOOK")
@Size(max = 50, message = "通知方式长度不能超过50个字符")
private String notifyMethod;
@Schema(description = "通知联系人JSON格式")
private String notifyContacts;
@Schema(description = "规则描述")
@Size(max = 500, message = "描述长度不能超过500个字符")
private String description;
}

View File

@ -1,5 +1,6 @@
package com.qqchen.deploy.backend.deploy.dto; package com.qqchen.deploy.backend.deploy.dto;
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
import com.qqchen.deploy.backend.framework.enums.AuthTypeEnum; import com.qqchen.deploy.backend.framework.enums.AuthTypeEnum;
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum; import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum; import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum;
@ -7,6 +8,7 @@ import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
/** /**
* 服务器 DTO * 服务器 DTO
@ -96,10 +98,15 @@ public class ServerDTO extends BaseDTO {
private Integer memorySize; private Integer memorySize;
/** /**
* 磁盘大小(GB) * 磁盘总容量(GB)
*/ */
private Integer diskSize; private Integer diskSize;
/**
* 磁盘详细信息列表
*/
private List<DiskInfo> diskInfo;
/** /**
* 标签JSON格式 * 标签JSON格式
*/ */

View File

@ -1,8 +1,10 @@
package com.qqchen.deploy.backend.deploy.dto; package com.qqchen.deploy.backend.deploy.dto;
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
/** /**
* 服务器信息DTO * 服务器信息DTO
@ -56,7 +58,12 @@ public class ServerInfoDTO {
private Integer memorySize; private Integer memorySize;
/** /**
* 磁盘大小(GB) * 磁盘总容量(GB)
*/ */
private Integer diskSize; private Integer diskSize;
/**
* 磁盘详细信息列表
*/
private List<DiskInfo> diskInfo;
} }

View File

@ -1,9 +1,12 @@
package com.qqchen.deploy.backend.deploy.dto; package com.qqchen.deploy.backend.deploy.dto;
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data; import lombok.Data;
import java.util.List;
/** /**
* 服务器初始化 DTO * 服务器初始化 DTO
@ -27,12 +30,18 @@ public class ServerInitializeDTO {
private Integer memorySize; private Integer memorySize;
/** /**
* 磁盘大小(GB) * 磁盘总容量(GB)
*/ */
@NotNull(message = "磁盘大小不能为空") @NotNull(message = "磁盘大小不能为空")
@Min(value = 1, message = "磁盘大小至少为1GB") @Min(value = 1, message = "磁盘大小至少为1GB")
private Integer diskSize; private Integer diskSize;
/**
* 磁盘详细信息列表
*/
@NotEmpty(message = "磁盘信息不能为空")
private List<DiskInfo> diskInfo;
/** /**
* 操作系统版本 * 操作系统版本
*/ */

View File

@ -0,0 +1,41 @@
package com.qqchen.deploy.backend.deploy.dto;
import com.qqchen.deploy.backend.framework.dto.DiskUsageInfo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 服务器监控数据DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "服务器监控数据")
public class ServerMonitorDataDTO {
@Schema(description = "服务器ID")
private Long serverId;
@Schema(description = "CPU使用率(%)")
private BigDecimal cpuUsage;
@Schema(description = "内存使用率(%)")
private BigDecimal memoryUsage;
@Schema(description = "已用内存(GB)")
private Integer memoryUsed;
@Schema(description = "各分区磁盘使用率")
private List<DiskUsageInfo> diskUsage;
@Schema(description = "采集时间")
private LocalDateTime collectTime;
}

View File

@ -0,0 +1,31 @@
package com.qqchen.deploy.backend.deploy.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 服务器监控通知配置
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ServerMonitorNotificationConfig {
/**
* 通知渠道ID
*/
private Long notificationChannelId;
/**
* 服务器离线通知模板ID
*/
private Long serverOfflineTemplateId;
/**
* 资源预警通知模板ID
*/
private Long resourceAlertTemplateId;
}

View File

@ -1,5 +1,7 @@
package com.qqchen.deploy.backend.deploy.entity; package com.qqchen.deploy.backend.deploy.entity;
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
import com.qqchen.deploy.backend.framework.converter.DiskInfoListConverter;
import com.qqchen.deploy.backend.framework.enums.AuthTypeEnum; import com.qqchen.deploy.backend.framework.enums.AuthTypeEnum;
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum; import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum; import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum;
@ -8,6 +10,7 @@ import jakarta.persistence.*;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
/** /**
* 服务器实体 * 服务器实体
@ -121,11 +124,20 @@ public class Server extends Entity<Long> {
private Integer memorySize; private Integer memorySize;
/** /**
* 磁盘大小(GB) * 磁盘总容量(GB)
* 所有磁盘分区容量的总和方便排序和统计
*/ */
@Column(name = "disk_size") @Column(name = "disk_size")
private Integer diskSize; private Integer diskSize;
/**
* 磁盘详细信息列表
* 存储多个磁盘的详细信息包括挂载点容量
*/
@Convert(converter = DiskInfoListConverter.class)
@Column(name = "disk_info", columnDefinition = "JSON")
private List<DiskInfo> diskInfo;
/** /**
* 标签JSON格式 * 标签JSON格式
*/ */

View File

@ -0,0 +1,102 @@
package com.qqchen.deploy.backend.deploy.entity;
import com.qqchen.deploy.backend.deploy.enums.AlertLevelEnum;
import com.qqchen.deploy.backend.deploy.enums.AlertTypeEnum;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 服务器告警记录实体
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "deploy_server_alert_log")
public class ServerAlertLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 服务器ID
*/
@Column(name = "server_id", nullable = false)
private Long serverId;
/**
* 规则ID
*/
@Column(name = "rule_id")
private Long ruleId;
/**
* 告警类型
*/
@Enumerated(EnumType.STRING)
@Column(name = "alert_type", nullable = false, length = 20)
private AlertTypeEnum alertType;
/**
* 告警级别
*/
@Enumerated(EnumType.STRING)
@Column(name = "alert_level", nullable = false, length = 20)
private AlertLevelEnum alertLevel;
/**
* 当前值
*/
@Column(name = "alert_value", nullable = false, precision = 5, scale = 2)
private BigDecimal alertValue;
/**
* 阈值
*/
@Column(name = "threshold_value", nullable = false, precision = 5, scale = 2)
private BigDecimal thresholdValue;
/**
* 告警消息
*/
@Column(name = "alert_message", length = 500)
private String alertMessage;
/**
* 状态: ACTIVE/RESOLVED
*/
@Column(name = "status", length = 20)
private String status = "ACTIVE";
/**
* 告警时间
*/
@Column(name = "alert_time", nullable = false)
private LocalDateTime alertTime;
/**
* 恢复时间
*/
@Column(name = "resolve_time")
private LocalDateTime resolveTime;
/**
* 是否已通知
*/
@Column(name = "notified")
private Boolean notified = false;
/**
* 通知时间
*/
@Column(name = "notify_time")
private LocalDateTime notifyTime;
}

View File

@ -0,0 +1,86 @@
package com.qqchen.deploy.backend.deploy.entity;
import com.qqchen.deploy.backend.deploy.enums.AlertTypeEnum;
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
import com.qqchen.deploy.backend.framework.domain.Entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* 服务器告警规则实体
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@jakarta.persistence.Entity
@Table(name = "deploy_server_alert_rule")
@LogicDelete(true)
public class ServerAlertRule extends Entity<Long> {
/**
* 服务器IDNULL表示全局规则
*/
@Column(name = "server_id")
private Long serverId;
/**
* 规则名称
*/
@Column(name = "rule_name", nullable = false, length = 100)
private String ruleName;
/**
* 告警类型
*/
@Enumerated(EnumType.STRING)
@Column(name = "alert_type", nullable = false, length = 20)
private AlertTypeEnum alertType;
/**
* 警告阈值(%)
*/
@Column(name = "warning_threshold", nullable = false, precision = 5, scale = 2)
private BigDecimal warningThreshold;
/**
* 严重阈值(%)
*/
@Column(name = "critical_threshold", nullable = false, precision = 5, scale = 2)
private BigDecimal criticalThreshold;
/**
* 持续时长(分钟)
*/
@Column(name = "duration_minutes")
private Integer durationMinutes = 5;
/**
* 是否启用
*/
@Column(name = "enabled")
private Boolean enabled = true;
/**
* 通知方式: EMAIL/SMS/WEBHOOK
*/
@Column(name = "notify_method", length = 50)
private String notifyMethod;
/**
* 通知联系人JSON格式
*/
@Column(name = "notify_contacts", columnDefinition = "JSON")
private String notifyContacts;
/**
* 规则描述
*/
@Column(name = "description", length = 500)
private String description;
}

View File

@ -0,0 +1,76 @@
package com.qqchen.deploy.backend.deploy.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 服务器监控记录实体
* 注意监控记录不需要审计字段和软删除使用简单实体
*/
@Data
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@lombok.Builder
@Entity
@Table(name = "deploy_server_monitor")
public class ServerMonitor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 服务器ID
*/
@Column(name = "server_id", nullable = false)
private Long serverId;
/**
* CPU使用率(%)
*/
@Column(name = "cpu_usage", precision = 5, scale = 2)
private BigDecimal cpuUsage;
/**
* 内存使用率(%)
*/
@Column(name = "memory_usage", precision = 5, scale = 2)
private BigDecimal memoryUsage;
/**
* 已用内存(GB)
*/
@Column(name = "memory_used")
private Integer memoryUsed;
/**
* 磁盘使用情况JSON格式
*/
@Column(name = "disk_usage", columnDefinition = "JSON")
private String diskUsage;
/**
* 网络接收(KB/s)
*/
@Column(name = "network_rx")
private Long networkRx;
/**
* 网络发送(KB/s)
*/
@Column(name = "network_tx")
private Long networkTx;
/**
* 采集时间
*/
@Column(name = "collect_time", nullable = false)
private LocalDateTime collectTime;
}

View File

@ -0,0 +1,24 @@
package com.qqchen.deploy.backend.deploy.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 告警级别枚举
*/
@Getter
@AllArgsConstructor
public enum AlertLevelEnum {
/**
* 警告
*/
WARNING("WARNING", "警告"),
/**
* 严重
*/
CRITICAL("CRITICAL", "严重");
private final String code;
private final String description;
}

View File

@ -0,0 +1,29 @@
package com.qqchen.deploy.backend.deploy.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 告警类型枚举
*/
@Getter
@AllArgsConstructor
public enum AlertTypeEnum {
/**
* CPU告警
*/
CPU("CPU", "CPU使用率"),
/**
* 内存告警
*/
MEMORY("MEMORY", "内存使用率"),
/**
* 磁盘告警
*/
DISK("DISK", "磁盘使用率");
private final String code;
private final String description;
}

View File

@ -0,0 +1,34 @@
package com.qqchen.deploy.backend.deploy.query;
import com.qqchen.deploy.backend.deploy.enums.AlertTypeEnum;
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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 服务器告警规则查询条件
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "服务器告警规则查询条件")
public class ServerAlertRuleQuery extends BaseQuery {
@Schema(description = "服务器ID")
@QueryField(field = "serverId")
private Long serverId;
@Schema(description = "规则名称(模糊匹配)")
@QueryField(field = "ruleName", type = QueryType.LIKE)
private String ruleName;
@Schema(description = "告警类型")
@QueryField(field = "alertType")
private AlertTypeEnum alertType;
@Schema(description = "是否启用")
@QueryField(field = "enabled")
private Boolean enabled;
}

View File

@ -0,0 +1,26 @@
package com.qqchen.deploy.backend.deploy.repository;
import com.qqchen.deploy.backend.deploy.entity.ServerAlertLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
/**
* 服务器告警记录Repository
*/
@Repository
public interface IServerAlertLogRepository extends JpaRepository<ServerAlertLog, Long> {
/**
* 查询指定服务器的活跃告警
*/
List<ServerAlertLog> findByServerIdAndStatus(Long serverId, String status);
/**
* 查询指定服务器在指定时间范围内的告警记录
*/
List<ServerAlertLog> findByServerIdAndAlertTimeBetweenOrderByAlertTimeDesc(
Long serverId, LocalDateTime startTime, LocalDateTime endTime);
}

View File

@ -0,0 +1,12 @@
package com.qqchen.deploy.backend.deploy.repository;
import com.qqchen.deploy.backend.deploy.entity.ServerAlertRule;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import org.springframework.stereotype.Repository;
/**
* 服务器告警规则Repository
*/
@Repository
public interface IServerAlertRuleRepository extends IBaseRepository<ServerAlertRule, Long> {
}

View File

@ -0,0 +1,31 @@
package com.qqchen.deploy.backend.deploy.repository;
import com.qqchen.deploy.backend.deploy.entity.ServerMonitor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
/**
* 服务器监控记录Repository
*/
@Repository
public interface IServerMonitorRepository extends JpaRepository<ServerMonitor, Long> {
/**
* 查询指定服务器在指定时间范围内的监控记录
*/
List<ServerMonitor> findByServerIdAndCollectTimeBetweenOrderByCollectTimeDesc(
Long serverId, LocalDateTime startTime, LocalDateTime endTime);
/**
* 删除指定时间之前的监控记录
*/
@Modifying
@Query("DELETE FROM ServerMonitor m WHERE m.collectTime < :beforeTime")
int deleteByCollectTimeBefore(@Param("beforeTime") LocalDateTime beforeTime);
}

View File

@ -1,9 +1,12 @@
package com.qqchen.deploy.backend.deploy.repository; package com.qqchen.deploy.backend.deploy.repository;
import com.qqchen.deploy.backend.deploy.entity.Server; import com.qqchen.deploy.backend.deploy.entity.Server;
import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository; import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List;
/** /**
* 服务器仓储接口 * 服务器仓储接口
*/ */
@ -25,5 +28,14 @@ public interface IServerRepository extends IBaseRepository<Server, Long> {
* @return 是否存在 * @return 是否存在
*/ */
boolean existsByHostIpAndDeletedFalse(String hostIp); boolean existsByHostIpAndDeletedFalse(String hostIp);
/**
* 根据状态和删除标记查询服务器列表
*
* @param status 服务器状态
* @param deleted 是否删除
* @return 服务器列表
*/
List<Server> findByStatusAndDeleted(ServerStatusEnum status, boolean deleted);
} }

View File

@ -0,0 +1,267 @@
package com.qqchen.deploy.backend.deploy.scheduler;
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorDataDTO;
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorNotificationConfig;
import com.qqchen.deploy.backend.deploy.entity.Server;
import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum;
import com.qqchen.deploy.backend.deploy.repository.IServerRepository;
import com.qqchen.deploy.backend.deploy.service.IServerAlertService;
import com.qqchen.deploy.backend.deploy.service.IServerMonitorService;
import com.qqchen.deploy.backend.framework.dto.DiskUsageInfo;
import com.qqchen.deploy.backend.framework.ssh.ISSHCommandService;
import com.qqchen.deploy.backend.framework.ssh.SSHCommandServiceFactory;
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
import com.qqchen.deploy.backend.notification.service.INotificationService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import net.schmizz.sshj.SSHClient;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/**
* 服务器监控定时任务
* 由定时任务管理系统调用不使用@Scheduled注解
*/
@Slf4j
@Component
public class ServerMonitorScheduler {
@Resource
private IServerRepository serverRepository;
@Resource
private SSHCommandServiceFactory sshCommandServiceFactory;
@Resource
private IServerMonitorService monitorService;
@Resource
private IServerAlertService alertService;
@Resource
private INotificationService notificationService;
/**
* 采集所有在线服务器的监控数据
* 此方法由定时任务管理系统调用
*
* @param config 通知配置可选为null则只记录数据不发通知
*/
public void collectServerMetrics(ServerMonitorNotificationConfig config) {
if (config != null) {
log.info("========== 开始采集服务器监控数据 ========== channelId={}, offlineTemplateId={}, alertTemplateId={}",
config.getNotificationChannelId(), config.getServerOfflineTemplateId(), config.getResourceAlertTemplateId());
} else {
log.info("========== 开始采集服务器监控数据(不发送通知) ==========");
}
long startTime = System.currentTimeMillis();
try {
// 1. 查询所有应该在线的服务器状态为ONLINE
List<Server> shouldBeOnlineServers = serverRepository
.findByStatusAndDeleted(ServerStatusEnum.ONLINE, false);
if (shouldBeOnlineServers.isEmpty()) {
log.debug("没有需要监控的服务器,跳过监控采集");
return;
}
log.info("发现 {} 台应在线服务器,开始检测状态", shouldBeOnlineServers.size());
// 2. 并发检测服务器连接状态并采集监控数据
final ServerMonitorNotificationConfig finalConfig = config;
List<CompletableFuture<ServerMonitorDataDTO>> futures = shouldBeOnlineServers.stream()
.map(server -> CompletableFuture.supplyAsync(() ->
collectSingleServerWithStatusCheck(server, finalConfig)))
.collect(Collectors.toList());
// 3. 等待所有任务完成
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
allFutures.join();
// 4. 收集结果
List<ServerMonitorDataDTO> monitorDataList = futures.stream()
.map(CompletableFuture::join)
.filter(data -> data != null)
.collect(Collectors.toList());
long duration = System.currentTimeMillis() - startTime;
log.info("========== 监控数据采集完成: 成功={}/{}, 耗时={}ms ==========",
monitorDataList.size(), shouldBeOnlineServers.size(), duration);
// 5. 批量保存监控数据到数据库
if (!monitorDataList.isEmpty()) {
monitorService.batchSaveMonitorData(monitorDataList);
log.info("监控数据已保存到数据库: count={}", monitorDataList.size());
}
// 6. 检查告警规则
for (ServerMonitorDataDTO data : monitorDataList) {
try {
alertService.checkAlertRules(data.getServerId(), data, config);
} catch (Exception e) {
log.error("检查告警规则失败: serverId={}", data.getServerId(), e);
}
}
} catch (Exception e) {
log.error("服务器监控数据采集失败", e);
}
}
/**
* 检测服务器连接状态并采集监控数据
*/
private ServerMonitorDataDTO collectSingleServerWithStatusCheck(Server server, ServerMonitorNotificationConfig config) {
try {
// 尝试采集监控数据
return collectSingleServer(server);
} catch (Exception e) {
// 采集失败说明服务器无法连接离线
log.error("服务器连接失败(离线): serverId={}, name={}, ip={}, error={}",
server.getId(), server.getServerName(), server.getHostIp(), e.getMessage());
// 发送离线通知
if (config != null && config.getNotificationChannelId() != null && config.getServerOfflineTemplateId() != null) {
try {
sendServerOfflineNotification(server, config);
} catch (Exception notifyError) {
log.error("发送服务器离线通知失败: serverId={}", server.getId(), notifyError);
}
}
return null;
}
}
/**
* 发送服务器离线通知
*/
private void sendServerOfflineNotification(Server server, ServerMonitorNotificationConfig config) {
try {
// 1. 构建模板参数
Map<String, Object> templateParams = new HashMap<>();
templateParams.put("serverName", server.getServerName());
templateParams.put("serverIp", server.getHostIp());
templateParams.put("offlineTime", LocalDateTime.now().toString());
// 2. 构建SendNotificationRequest
SendNotificationRequest request = new SendNotificationRequest();
request.setChannelId(config.getNotificationChannelId());
request.setNotificationTemplateId(config.getServerOfflineTemplateId());
request.setTemplateParams(templateParams);
// 3. 发送通知NotificationService会自动根据渠道类型创建请求对象
notificationService.send(request);
log.info("✅ 服务器离线通知已发送: serverId={}, serverName={}, ip={}",
server.getId(), server.getServerName(), server.getHostIp());
} catch (Exception e) {
log.error("发送服务器离线通知异常: serverId={}", server.getId(), e);
throw e;
}
}
/**
* 采集单台服务器的监控数据
*/
private ServerMonitorDataDTO collectSingleServer(Server server) {
SSHClient sshClient = null;
ISSHCommandService sshService = null;
try {
// 1. 获取对应OS的SSH服务
sshService = sshCommandServiceFactory.getService(server.getOsType());
// 2. 创建SSH连接
String password = null;
String privateKey = null;
String passphrase = null;
switch (server.getAuthType()) {
case PASSWORD:
password = server.getSshPassword();
break;
case KEY:
privateKey = server.getSshPrivateKey();
passphrase = server.getSshPassphrase();
break;
}
sshClient = sshService.createConnection(
server.getHostIp(),
server.getSshPort(),
server.getSshUser(),
password,
privateKey,
passphrase
);
// 3. 采集监控数据
BigDecimal cpuUsage = sshService.getCpuUsage(sshClient);
BigDecimal memoryUsage = sshService.getMemoryUsage(sshClient);
List<DiskUsageInfo> diskUsage = sshService.getDiskUsage(sshClient);
// 4. 计算已用内存基于内存使用率和总内存
Integer memoryUsed = null;
if (memoryUsage != null && server.getMemorySize() != null) {
memoryUsed = memoryUsage.multiply(new BigDecimal(server.getMemorySize()))
.divide(new BigDecimal(100), 0, BigDecimal.ROUND_HALF_UP)
.intValue();
}
// 5. 构建监控数据
ServerMonitorDataDTO data = ServerMonitorDataDTO.builder()
.serverId(server.getId())
.cpuUsage(cpuUsage)
.memoryUsage(memoryUsage)
.memoryUsed(memoryUsed)
.diskUsage(diskUsage)
.collectTime(LocalDateTime.now())
.build();
log.debug("服务器监控数据采集成功: serverId={}, cpu={}%, mem={}%, diskCount={}",
server.getId(), cpuUsage, memoryUsage,
diskUsage != null ? diskUsage.size() : 0);
return data;
} catch (Exception e) {
log.error("采集服务器监控数据失败: serverId={}, serverName={}, error={}",
server.getId(), server.getServerName(), e.getMessage());
return null;
} finally {
// 6. 关闭SSH连接
if (sshService != null && sshClient != null) {
sshService.closeConnection(sshClient);
}
}
}
/**
* 清理历史监控数据
* 此方法由定时任务管理系统调用建议每天凌晨执行
*/
public void cleanOldMonitorData() {
log.info("========== 开始清理历史监控数据 ==========");
try {
// 删除30天前的数据
LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30);
int deletedCount = monitorService.deleteOldData(thirtyDaysAgo);
log.info("========== 历史监控数据清理完成: count={} ==========", deletedCount);
} catch (Exception e) {
log.error("清理历史监控数据失败", e);
}
}
}

View File

@ -0,0 +1,12 @@
package com.qqchen.deploy.backend.deploy.service;
import com.qqchen.deploy.backend.deploy.dto.ServerAlertRuleDTO;
import com.qqchen.deploy.backend.deploy.entity.ServerAlertRule;
import com.qqchen.deploy.backend.deploy.query.ServerAlertRuleQuery;
import com.qqchen.deploy.backend.framework.service.IBaseService;
/**
* 服务器告警规则服务接口
*/
public interface IServerAlertRuleService extends IBaseService<ServerAlertRule, ServerAlertRuleDTO, ServerAlertRuleQuery, Long> {
}

View File

@ -0,0 +1,20 @@
package com.qqchen.deploy.backend.deploy.service;
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorDataDTO;
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorNotificationConfig;
/**
* 服务器告警服务接口
*/
public interface IServerAlertService {
/**
* 检查监控数据是否触发告警
*
* @param serverId 服务器ID
* @param monitorData 监控数据
* @param config 通知配置可选为null则不发送通知
*/
void checkAlertRules(Long serverId, ServerMonitorDataDTO monitorData,
ServerMonitorNotificationConfig config);
}

View File

@ -0,0 +1,22 @@
package com.qqchen.deploy.backend.deploy.service;
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorDataDTO;
import java.time.LocalDateTime;
import java.util.List;
/**
* 服务器监控服务接口
*/
public interface IServerMonitorService {
/**
* 批量保存监控数据
*/
void batchSaveMonitorData(List<ServerMonitorDataDTO> dataList);
/**
* 删除指定时间之前的历史数据
*/
int deleteOldData(LocalDateTime beforeTime);
}

View File

@ -16,18 +16,13 @@ import com.qqchen.deploy.backend.deploy.integration.IJenkinsServiceIntegration;
import com.qqchen.deploy.backend.deploy.repository.*; import com.qqchen.deploy.backend.deploy.repository.*;
import com.qqchen.deploy.backend.deploy.service.IJenkinsBuildService; import com.qqchen.deploy.backend.deploy.service.IJenkinsBuildService;
import com.qqchen.deploy.backend.deploy.service.IJenkinsSyncHistoryService; import com.qqchen.deploy.backend.deploy.service.IJenkinsSyncHistoryService;
import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest;
import com.qqchen.deploy.backend.notification.entity.NotificationChannel; import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
import com.qqchen.deploy.backend.notification.entity.NotificationTemplate; import com.qqchen.deploy.backend.notification.entity.NotificationTemplate;
import com.qqchen.deploy.backend.notification.entity.config.WeworkTemplateConfig;
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository; import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
import com.qqchen.deploy.backend.notification.repository.INotificationTemplateRepository; import com.qqchen.deploy.backend.notification.repository.INotificationTemplateRepository;
import com.qqchen.deploy.backend.notification.service.INotificationService; import com.qqchen.deploy.backend.notification.service.INotificationService;
import com.qqchen.deploy.backend.notification.service.INotificationSendService; import com.qqchen.deploy.backend.notification.service.INotificationSendService;
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest; import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest;
import com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum;
import com.qqchen.deploy.backend.deploy.dto.sync.JenkinsSyncContext; import com.qqchen.deploy.backend.deploy.dto.sync.JenkinsSyncContext;
import com.qqchen.deploy.backend.deploy.lock.SyncLockManager; import com.qqchen.deploy.backend.deploy.lock.SyncLockManager;
import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.enums.ResponseCode;
@ -763,21 +758,15 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
// actions JSON 中解析 commit 信息 // actions JSON 中解析 commit 信息
parseCommitInfoFromActions(build.getActions(), templateParams); parseCommitInfoFromActions(build.getActions(), templateParams);
// 4. 校验模板和渠道类型是否匹配 // 4. 构建 SendNotificationRequest
if (!template.getChannelType().equals(channel.getChannelType())) {
log.warn("模板渠道类型({})与通知渠道类型({})不匹配,跳过通知: templateId={}, channelId={}", template.getChannelType(), channel.getChannelType(), config.getBuildNotificationTemplateId(), channel.getId());
return;
}
// 5. 构建 SendNotificationRequest
SendNotificationRequest request = new SendNotificationRequest(); SendNotificationRequest request = new SendNotificationRequest();
request.setChannelId(channel.getId());
request.setNotificationTemplateId(config.getBuildNotificationTemplateId()); request.setNotificationTemplateId(config.getBuildNotificationTemplateId());
request.setTemplateParams(templateParams); request.setTemplateParams(templateParams);
request.setSendRequest(createSendRequestByChannel(channel, template));
log.debug("准备发送构建通知: job={}, build={}, templateId={}, channelId={}, channelType={}", job.getJobName(), build.getBuildNumber(), config.getBuildNotificationTemplateId(), channel.getId(), channel.getChannelType()); log.debug("准备发送构建通知: job={}, build={}, templateId={}, channelId={}", job.getJobName(), build.getBuildNumber(), config.getBuildNotificationTemplateId(), channel.getId());
// 6. 发送通知 // 5. 发送通知NotificationService会自动根据渠道类型创建请求对象
notificationService.send(request); notificationService.send(request);
log.info("已发送构建通知: job={}, build={}, status={}, templateId={}", job.getJobName(), build.getBuildNumber(), status, config.getBuildNotificationTemplateId()); log.info("已发送构建通知: job={}, build={}, status={}, templateId={}", job.getJobName(), build.getBuildNumber(), status, config.getBuildNotificationTemplateId());
@ -792,44 +781,6 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
} }
} }
/**
* 根据渠道类型创建对应的发送请求
*/
private BaseSendNotificationRequest createSendRequestByChannel(
NotificationChannel channel, NotificationTemplate template) {
switch (channel.getChannelType()) {
case WEWORK:
WeworkSendNotificationRequest weworkRequest = new WeworkSendNotificationRequest();
weworkRequest.setChannelId(channel.getId());
weworkRequest.setMessageType(getWeworkMessageType(template));
return weworkRequest;
case EMAIL:
EmailSendNotificationRequest emailRequest = new EmailSendNotificationRequest();
emailRequest.setChannelId(channel.getId());
emailRequest.setToReceivers(Arrays.asList("admin@company.com"));
return emailRequest;
default:
throw new RuntimeException("不支持的渠道类型: " + channel.getChannelType());
}
}
/**
* 从模板配置中获取企业微信消息类型
*/
private WeworkMessageTypeEnum getWeworkMessageType(NotificationTemplate template) {
try {
if (template.getTemplateConfig() != null) {
WeworkTemplateConfig weworkConfig = JsonUtils.fromMap(template.getTemplateConfig(), WeworkTemplateConfig.class);
if (weworkConfig != null && weworkConfig.getMessageType() != null) {
return weworkConfig.getMessageType();
}
}
} catch (Exception e) {
log.warn("解析企业微信模板配置失败,使用默认消息类型: {}", e.getMessage());
}
return WeworkMessageTypeEnum.TEXT;
}
/** /**
* changeSets JSON 中解析 commit 信息 * changeSets JSON 中解析 commit 信息
*/ */

View File

@ -0,0 +1,58 @@
package com.qqchen.deploy.backend.deploy.service.impl;
import com.qqchen.deploy.backend.deploy.dto.ServerAlertRuleDTO;
import com.qqchen.deploy.backend.deploy.entity.ServerAlertRule;
import com.qqchen.deploy.backend.deploy.query.ServerAlertRuleQuery;
import com.qqchen.deploy.backend.deploy.repository.IServerAlertRuleRepository;
import com.qqchen.deploy.backend.deploy.service.IServerAlertRuleService;
import com.qqchen.deploy.backend.framework.annotation.ServiceType;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 服务器告警规则服务实现
*/
@Slf4j
@Service
@ServiceType(ServiceType.Type.DATABASE)
public class ServerAlertRuleServiceImpl
extends BaseServiceImpl<ServerAlertRule, ServerAlertRuleDTO, ServerAlertRuleQuery, Long>
implements IServerAlertRuleService {
private final IServerAlertRuleRepository alertRuleRepository;
public ServerAlertRuleServiceImpl(IServerAlertRuleRepository alertRuleRepository) {
this.alertRuleRepository = alertRuleRepository;
}
@Override
protected void validateUniqueConstraints(ServerAlertRuleDTO dto) {
// 可以添加唯一性校验比如同一个服务器的同一类型告警只能有一条规则
// 这里暂不添加约束
}
@Override
public ServerAlertRuleDTO create(ServerAlertRuleDTO dto) {
// 校验阈值: 严重阈值必须大于警告阈值
if (dto.getCriticalThreshold().compareTo(dto.getWarningThreshold()) <= 0) {
throw new BusinessException(ResponseCode.INVALID_PARAM,
new Object[]{"严重阈值必须大于警告阈值"});
}
return super.create(dto);
}
@Override
public ServerAlertRuleDTO update(Long id, ServerAlertRuleDTO dto) {
// 校验阈值
if (dto.getCriticalThreshold().compareTo(dto.getWarningThreshold()) <= 0) {
throw new BusinessException(ResponseCode.INVALID_PARAM,
new Object[]{"严重阈值必须大于警告阈值"});
}
return super.update(id, dto);
}
}

View File

@ -0,0 +1,242 @@
package com.qqchen.deploy.backend.deploy.service.impl;
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorDataDTO;
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorNotificationConfig;
import com.qqchen.deploy.backend.deploy.entity.Server;
import com.qqchen.deploy.backend.deploy.entity.ServerAlertLog;
import com.qqchen.deploy.backend.deploy.entity.ServerAlertRule;
import com.qqchen.deploy.backend.deploy.enums.AlertLevelEnum;
import com.qqchen.deploy.backend.deploy.enums.AlertTypeEnum;
import com.qqchen.deploy.backend.deploy.repository.IServerAlertLogRepository;
import com.qqchen.deploy.backend.deploy.repository.IServerAlertRuleRepository;
import com.qqchen.deploy.backend.deploy.repository.IServerRepository;
import com.qqchen.deploy.backend.deploy.service.IServerAlertService;
import com.qqchen.deploy.backend.framework.dto.DiskUsageInfo;
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
import com.qqchen.deploy.backend.notification.service.INotificationService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 服务器告警服务实现
*/
@Slf4j
@Service
public class ServerAlertServiceImpl implements IServerAlertService {
@Resource
private IServerAlertRuleRepository alertRuleRepository;
@Resource
private IServerAlertLogRepository alertLogRepository;
@Resource
private IServerRepository serverRepository;
@Resource
private INotificationService notificationService;
@Override
public void checkAlertRules(Long serverId, ServerMonitorDataDTO monitorData,
ServerMonitorNotificationConfig config) {
if (monitorData == null) {
return;
}
// 查询该服务器的所有启用的告警规则包括全局规则
List<ServerAlertRule> rules = alertRuleRepository.findAll();
for (ServerAlertRule rule : rules) {
// 过滤只检查全局规则或匹配的服务器规则
if (rule.getServerId() != null && !rule.getServerId().equals(serverId)) {
continue;
}
// 只检查启用的规则
if (!Boolean.TRUE.equals(rule.getEnabled())) {
continue;
}
// 根据告警类型检查
checkSingleRule(serverId, monitorData, rule, config);
}
}
/**
* 检查单个告警规则
*/
private void checkSingleRule(Long serverId, ServerMonitorDataDTO monitorData, ServerAlertRule rule,
ServerMonitorNotificationConfig config) {
BigDecimal currentValue = null;
String resourceInfo = "";
// 根据告警类型获取当前值
switch (rule.getAlertType()) {
case CPU:
currentValue = monitorData.getCpuUsage();
resourceInfo = "CPU";
break;
case MEMORY:
currentValue = monitorData.getMemoryUsage();
resourceInfo = "内存";
break;
case DISK:
// 磁盘告警检查所有分区任意分区超过阈值就告警
checkDiskAlert(serverId, monitorData, rule, config);
return;
}
if (currentValue == null) {
return;
}
// 判断告警级别
AlertLevelEnum alertLevel = null;
BigDecimal threshold = null;
if (currentValue.compareTo(rule.getCriticalThreshold()) >= 0) {
alertLevel = AlertLevelEnum.CRITICAL;
threshold = rule.getCriticalThreshold();
} else if (currentValue.compareTo(rule.getWarningThreshold()) >= 0) {
alertLevel = AlertLevelEnum.WARNING;
threshold = rule.getWarningThreshold();
}
// 触发告警
if (alertLevel != null) {
triggerAlert(serverId, rule, alertLevel, currentValue, threshold, resourceInfo, config);
}
}
/**
* 检查磁盘告警
*/
private void checkDiskAlert(Long serverId, ServerMonitorDataDTO monitorData, ServerAlertRule rule,
ServerMonitorNotificationConfig config) {
List<DiskUsageInfo> diskUsageList = monitorData.getDiskUsage();
if (diskUsageList == null || diskUsageList.isEmpty()) {
return;
}
for (DiskUsageInfo diskUsage : diskUsageList) {
BigDecimal usagePercent = diskUsage.getUsagePercent();
if (usagePercent == null) {
continue;
}
AlertLevelEnum alertLevel = null;
BigDecimal threshold = null;
if (usagePercent.compareTo(rule.getCriticalThreshold()) >= 0) {
alertLevel = AlertLevelEnum.CRITICAL;
threshold = rule.getCriticalThreshold();
} else if (usagePercent.compareTo(rule.getWarningThreshold()) >= 0) {
alertLevel = AlertLevelEnum.WARNING;
threshold = rule.getWarningThreshold();
}
if (alertLevel != null) {
String resourceInfo = "磁盘[" + diskUsage.getMountPoint() + "]";
triggerAlert(serverId, rule, alertLevel, usagePercent, threshold, resourceInfo, config);
}
}
}
/**
* 触发告警
*/
private void triggerAlert(Long serverId, ServerAlertRule rule, AlertLevelEnum level,
BigDecimal currentValue, BigDecimal threshold, String resourceInfo,
ServerMonitorNotificationConfig config) {
// 1. 记录告警日志到数据库
String alertMessage = String.format("%s使用率达到%s级别: 当前值=%.2f%%, 阈值=%.2f%%",
resourceInfo, level.getDescription(), currentValue, threshold);
ServerAlertLog alertLog = ServerAlertLog.builder()
.serverId(serverId)
.ruleId(rule.getId())
.alertType(rule.getAlertType())
.alertLevel(level)
.alertValue(currentValue)
.thresholdValue(threshold)
.alertMessage(alertMessage)
.status("ACTIVE")
.alertTime(LocalDateTime.now())
.notified(false)
.build();
try {
alertLogRepository.save(alertLog);
log.info("✅ 告警记录已保存: id={}, serverId={}, message={}",
alertLog.getId(), serverId, alertMessage);
} catch (Exception e) {
log.error("保存告警记录失败", e);
}
// 2. 记录日志
log.warn("⚠️ 服务器告警触发: serverId={}, ruleName={}, type={}, level={}, resource={}, " +
"current={}%, threshold={}%",
serverId, rule.getRuleName(), rule.getAlertType(), level,
resourceInfo, currentValue, threshold);
// 3. 发送告警通知
if (config != null && config.getNotificationChannelId() != null && config.getResourceAlertTemplateId() != null) {
try {
sendAlertNotification(serverId, rule, level, currentValue, threshold, resourceInfo, config);
} catch (Exception e) {
log.error("发送告警通知失败: serverId={}, error={}", serverId, e.getMessage(), e);
}
}
}
/**
* 发送告警通知
*/
private void sendAlertNotification(Long serverId, ServerAlertRule rule, AlertLevelEnum level,
BigDecimal currentValue, BigDecimal threshold, String resourceInfo,
ServerMonitorNotificationConfig config) {
try {
// 1. 获取服务器信息
Server server = serverRepository.findById(serverId).orElse(null);
if (server == null) {
log.warn("服务器不存在,跳过发送通知: serverId={}", serverId);
return;
}
// 2. 构建模板参数
Map<String, Object> templateParams = new HashMap<>();
templateParams.put("serverName", server.getServerName());
templateParams.put("serverIp", server.getHostIp());
templateParams.put("ruleName", rule.getRuleName());
templateParams.put("alertType", rule.getAlertType().getDescription());
templateParams.put("alertLevel", level.getDescription());
templateParams.put("resourceInfo", resourceInfo);
templateParams.put("currentValue", String.format("%.2f", currentValue));
templateParams.put("threshold", String.format("%.2f", threshold));
templateParams.put("alertTime", LocalDateTime.now().toString());
// 3. 构建SendNotificationRequest
SendNotificationRequest request = new SendNotificationRequest();
request.setChannelId(config.getNotificationChannelId());
request.setNotificationTemplateId(config.getResourceAlertTemplateId());
request.setTemplateParams(templateParams);
// 4. 发送通知NotificationService会自动根据渠道类型创建请求对象
notificationService.send(request);
log.info("✅ 告警通知已发送: serverId={}, channelId={}, templateId={}",
serverId, config.getNotificationChannelId(), config.getResourceAlertTemplateId());
} catch (Exception e) {
log.error("发送告警通知异常: serverId={}", serverId, e);
throw e;
}
}
}

View File

@ -0,0 +1,76 @@
package com.qqchen.deploy.backend.deploy.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorDataDTO;
import com.qqchen.deploy.backend.deploy.entity.ServerMonitor;
import com.qqchen.deploy.backend.deploy.repository.IServerMonitorRepository;
import com.qqchen.deploy.backend.deploy.service.IServerMonitorService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* 服务器监控服务实现
*/
@Slf4j
@Service
public class ServerMonitorServiceImpl implements IServerMonitorService {
@Resource
private IServerMonitorRepository monitorRepository;
@Resource
private ObjectMapper objectMapper;
@Override
@Transactional
public void batchSaveMonitorData(List<ServerMonitorDataDTO> dataList) {
if (dataList == null || dataList.isEmpty()) {
return;
}
List<ServerMonitor> monitors = dataList.stream()
.map(this::convertToEntity)
.collect(Collectors.toList());
monitorRepository.saveAll(monitors);
log.debug("批量保存监控数据成功: count={}", monitors.size());
}
@Override
@Transactional
public int deleteOldData(LocalDateTime beforeTime) {
int deletedCount = monitorRepository.deleteByCollectTimeBefore(beforeTime);
log.info("删除历史监控数据: beforeTime={}, count={}", beforeTime, deletedCount);
return deletedCount;
}
/**
* 转换DTO为实体
*/
private ServerMonitor convertToEntity(ServerMonitorDataDTO dto) {
String diskUsageJson = null;
if (dto.getDiskUsage() != null && !dto.getDiskUsage().isEmpty()) {
try {
diskUsageJson = objectMapper.writeValueAsString(dto.getDiskUsage());
} catch (JsonProcessingException e) {
log.error("序列化磁盘使用率失败", e);
}
}
return ServerMonitor.builder()
.serverId(dto.getServerId())
.cpuUsage(dto.getCpuUsage())
.memoryUsage(dto.getMemoryUsage())
.memoryUsed(dto.getMemoryUsed())
.diskUsage(diskUsageJson)
.collectTime(dto.getCollectTime())
.build();
}
}

View File

@ -74,6 +74,7 @@ public class ServerServiceImpl
server.setCpuCores(dto.getCpuCores()); server.setCpuCores(dto.getCpuCores());
server.setMemorySize(dto.getMemorySize()); server.setMemorySize(dto.getMemorySize());
server.setDiskSize(dto.getDiskSize()); server.setDiskSize(dto.getDiskSize());
server.setDiskInfo(dto.getDiskInfo());
server.setOsVersion(dto.getOsVersion()); server.setOsVersion(dto.getOsVersion());
server.setHostname(dto.getHostname()); server.setHostname(dto.getHostname());
server.setStatus(ServerStatusEnum.ONLINE); server.setStatus(ServerStatusEnum.ONLINE);
@ -81,8 +82,9 @@ public class ServerServiceImpl
// 3. 保存并返回 // 3. 保存并返回
Server updated = serverRepository.save(server); Server updated = serverRepository.save(server);
log.info("服务器初始化成功: serverId={}, cpuCores={}, memorySize={}GB, diskSize={}GB", log.info("服务器初始化成功: serverId={}, cpuCores={}, memorySize={}GB, diskSize={}GB, diskCount={}",
serverId, dto.getCpuCores(), dto.getMemorySize(), dto.getDiskSize()); serverId, dto.getCpuCores(), dto.getMemorySize(), dto.getDiskSize(),
dto.getDiskInfo() != null ? dto.getDiskInfo().size() : 0);
return converter.toDto(updated); return converter.toDto(updated);
} }
@ -169,10 +171,19 @@ public class ServerServiceImpl
info.setOsVersion(sshService.getOsVersion(sshClient)); info.setOsVersion(sshService.getOsVersion(sshClient));
info.setCpuCores(sshService.getCpuCores(sshClient)); info.setCpuCores(sshService.getCpuCores(sshClient));
info.setMemorySize(sshService.getMemorySize(sshClient)); info.setMemorySize(sshService.getMemorySize(sshClient));
info.setDiskSize(sshService.getDiskSize(sshClient)); info.setDiskInfo(sshService.getDiskInfo(sshClient));
log.info("服务器信息采集成功: serverId={}, hostname={}, cpu={}核, mem={}GB, disk={}GB", // 计算磁盘总容量
serverId, info.getHostname(), info.getCpuCores(), info.getMemorySize(), info.getDiskSize()); if (info.getDiskInfo() != null && !info.getDiskInfo().isEmpty()) {
int totalSize = info.getDiskInfo().stream()
.mapToInt(disk -> disk.getTotalSize().intValue())
.sum();
info.setDiskSize(totalSize);
}
log.info("服务器信息采集成功: serverId={}, hostname={}, cpu={}核, mem={}GB, disk={}GB, diskCount={}",
serverId, info.getHostname(), info.getCpuCores(), info.getMemorySize(),
info.getDiskSize(), info.getDiskInfo() != null ? info.getDiskInfo().size() : 0);
// 6. 更新服务器信息到数据库 // 6. 更新服务器信息到数据库
server.setStatus(ServerStatusEnum.ONLINE); server.setStatus(ServerStatusEnum.ONLINE);
@ -182,6 +193,7 @@ public class ServerServiceImpl
server.setCpuCores(info.getCpuCores()); server.setCpuCores(info.getCpuCores());
server.setMemorySize(info.getMemorySize()); server.setMemorySize(info.getMemorySize());
server.setDiskSize(info.getDiskSize()); server.setDiskSize(info.getDiskSize());
server.setDiskInfo(info.getDiskInfo());
serverRepository.save(server); serverRepository.save(server);
log.info("服务器状态已更新为ONLINE: serverId={}", serverId); log.info("服务器状态已更新为ONLINE: serverId={}", serverId);

View File

@ -0,0 +1,48 @@
package com.qqchen.deploy.backend.framework.converter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
* 磁盘信息列表 JSON 转换器
*/
@Slf4j
@Converter
public class DiskInfoListConverter implements AttributeConverter<List<DiskInfo>, String> {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public String convertToDatabaseColumn(List<DiskInfo> attribute) {
if (attribute == null || attribute.isEmpty()) {
return null;
}
try {
return objectMapper.writeValueAsString(attribute);
} catch (JsonProcessingException e) {
log.error("转换磁盘信息为JSON失败", e);
return null;
}
}
@Override
public List<DiskInfo> convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.trim().isEmpty()) {
return new ArrayList<>();
}
try {
return objectMapper.readValue(dbData, new TypeReference<List<DiskInfo>>() {});
} catch (JsonProcessingException e) {
log.error("解析磁盘信息JSON失败: {}", dbData, e);
return new ArrayList<>();
}
}
}

View File

@ -0,0 +1,35 @@
package com.qqchen.deploy.backend.framework.dto;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* 磁盘信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonPropertyOrder({"mountPoint", "fileSystem", "totalSize"})
public class DiskInfo {
/**
* 挂载点
*/
private String mountPoint;
/**
* 文件系统类型
*/
private String fileSystem;
/**
* 总容量(GB)
*/
private BigDecimal totalSize;
}

View File

@ -0,0 +1,35 @@
package com.qqchen.deploy.backend.framework.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* 磁盘使用率信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "磁盘使用率信息")
public class DiskUsageInfo {
@Schema(description = "挂载点")
private String mountPoint;
@Schema(description = "文件系统类型")
private String fileSystem;
@Schema(description = "总容量(GB)")
private BigDecimal totalSize;
@Schema(description = "已用容量(GB)")
private BigDecimal usedSize;
@Schema(description = "使用率(%)")
private BigDecimal usagePercent;
}

View File

@ -64,11 +64,37 @@ public abstract class AbstractSSHCommandService implements ISSHCommandService {
return sshClient; return sshClient;
} catch (net.schmizz.sshj.userauth.UserAuthException e) {
// 认证失败
closeConnection(sshClient);
log.error("SSH认证失败: {}@{}:{}", username, host, port);
throw new BusinessException(ResponseCode.SSH_FILE_AUTHENTICATION_FAILED);
} catch (net.schmizz.sshj.transport.TransportException e) {
// 连接失败网络问题主机不可达等
closeConnection(sshClient);
log.error("SSH连接失败: {}@{}:{}, 原因: {}", username, host, port, e.getMessage());
throw new BusinessException(ResponseCode.SSH_FILE_CONNECTION_FAILED);
} catch (java.net.UnknownHostException e) {
// 主机不存在
closeConnection(sshClient);
log.error("SSH主机不存在: {}", host);
throw new BusinessException(ResponseCode.SSH_FILE_CONNECTION_FAILED,
new Object[]{"主机地址无效或不可达"});
} catch (java.net.SocketTimeoutException e) {
// 连接超时
closeConnection(sshClient);
log.error("SSH连接超时: {}@{}:{}", username, host, port);
throw new BusinessException(ResponseCode.SSH_FILE_TIMEOUT);
} catch (BusinessException e) {
// 业务异常直接抛出
closeConnection(sshClient);
throw e;
} catch (Exception e) { } catch (Exception e) {
// 连接失败关闭客户端 // 其他未知异常
closeConnection(sshClient); closeConnection(sshClient);
log.error("SSH连接失败: {}@{}:{}", username, host, port, e); log.error("SSH连接失败: {}@{}:{}", username, host, port, e);
throw e; throw new BusinessException(ResponseCode.SSH_FILE_CONNECTION_FAILED,
new Object[]{e.getMessage()});
} }
} }
@ -136,4 +162,19 @@ public abstract class AbstractSSHCommandService implements ISSHCommandService {
return null; return null;
} }
} }
/**
* 解析BigDecimal结果
*/
protected java.math.BigDecimal parseBigDecimal(String value) {
if (value == null || value.isEmpty()) {
return null;
}
try {
return new java.math.BigDecimal(value.trim());
} catch (NumberFormatException e) {
log.warn("解析BigDecimal失败: {}", value);
return null;
}
}
} }

View File

@ -1,8 +1,13 @@
package com.qqchen.deploy.backend.framework.ssh; package com.qqchen.deploy.backend.framework.ssh;
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
import com.qqchen.deploy.backend.framework.dto.DiskUsageInfo;
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum; import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.SSHClient;
import java.math.BigDecimal;
import java.util.List;
/** /**
* SSH命令服务接口Framework层 * SSH命令服务接口Framework层
* 封装了SSH连接和OS命令执行能力 * 封装了SSH连接和OS命令执行能力
@ -74,12 +79,36 @@ public interface ISSHCommandService {
Integer getMemorySize(SSHClient sshClient); Integer getMemorySize(SSHClient sshClient);
/** /**
* 获取磁盘大小(GB) * 获取磁盘信息列表
* *
* @param sshClient SSH客户端 * @param sshClient SSH客户端
* @return 磁盘大小 * @return 磁盘信息列表
*/ */
Integer getDiskSize(SSHClient sshClient); List<DiskInfo> getDiskInfo(SSHClient sshClient);
/**
* 获取CPU使用率(%)
*
* @param sshClient SSH客户端
* @return CPU使用率
*/
BigDecimal getCpuUsage(SSHClient sshClient);
/**
* 获取内存使用率(%)
*
* @param sshClient SSH客户端
* @return 内存使用率
*/
BigDecimal getMemoryUsage(SSHClient sshClient);
/**
* 获取各分区磁盘使用率
*
* @param sshClient SSH客户端
* @return 磁盘使用率列表
*/
List<DiskUsageInfo> getDiskUsage(SSHClient sshClient);
/** /**
* 获取支持的操作系统类型 * 获取支持的操作系统类型

View File

@ -1,11 +1,19 @@
package com.qqchen.deploy.backend.framework.ssh.impl; package com.qqchen.deploy.backend.framework.ssh.impl;
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
import com.qqchen.deploy.backend.framework.dto.DiskUsageInfo;
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum; import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
import com.qqchen.deploy.backend.framework.ssh.AbstractSSHCommandService; import com.qqchen.deploy.backend.framework.ssh.AbstractSSHCommandService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.SSHClient;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** /**
* Linux SSH命令服务实现Framework层 * Linux SSH命令服务实现Framework层
*/ */
@ -41,10 +49,112 @@ public class LinuxSSHCommandServiceImpl extends AbstractSSHCommandService {
} }
@Override @Override
public Integer getDiskSize(SSHClient sshClient) { public List<DiskInfo> getDiskInfo(SSHClient sshClient) {
// 获取根分区的磁盘大小GB // 使用df命令获取所有挂载点的磁盘信息
String result = safeExecute(sshClient, "df -BG / | tail -1 | awk '{print $2}' | sed 's/G//'"); // 格式: 设备|挂载点|文件系统类型|总大小(GB)
return parseInteger(result); // 过滤条件只保留 /dev/sd*, /dev/vd*, /dev/nvme*, /dev/mapper/* 等真实磁盘设备
// 排除loop设备tmpfsoverlay等
String command = "df -BG -T | grep -E '^/dev/(sd|vd|nvme|mapper|xvd)' | awk '{print $1 \"|\" $7 \"|\" $2 \"|\" $3}' | sed 's/G//g'";
String result = safeExecute(sshClient, command);
List<DiskInfo> diskList = new ArrayList<>();
Set<String> seenDevices = new HashSet<>(); // 用于去重同一设备只记录一次
if (result != null && !result.trim().isEmpty()) {
String[] lines = result.trim().split("\\n");
for (String line : lines) {
String[] parts = line.split("\\|");
if (parts.length >= 4) {
try {
String device = parts[0].trim();
String mountPoint = parts[1].trim();
String fileSystem = parts[2].trim();
String sizeStr = parts[3].trim();
// 去重如果同一个设备已经记录过跳过避免子卷绑定挂载重复
if (seenDevices.contains(device)) {
log.debug("跳过重复设备: {} -> {}", device, mountPoint);
continue;
}
seenDevices.add(device);
DiskInfo diskInfo = DiskInfo.builder()
.mountPoint(mountPoint)
.fileSystem(fileSystem)
.totalSize(new BigDecimal(sizeStr))
.build();
diskList.add(diskInfo);
} catch (NumberFormatException e) {
log.warn("解析磁盘信息失败: {}", line, e);
}
}
}
}
return diskList;
}
@Override
public BigDecimal getCpuUsage(SSHClient sshClient) {
// 使用 top 命令获取CPU使用率
String command = "top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | sed 's/%us,//'";
String result = safeExecute(sshClient, command);
return parseBigDecimal(result);
}
@Override
public BigDecimal getMemoryUsage(SSHClient sshClient) {
// 计算内存使用率(总内存 - 可用内存) / 总内存 * 100
String command = "free | grep Mem | awk '{printf \"%.2f\", ($2-$7)/$2 * 100}'";
String result = safeExecute(sshClient, command);
return parseBigDecimal(result);
}
@Override
public List<DiskUsageInfo> getDiskUsage(SSHClient sshClient) {
// 获取磁盘使用率
// 格式: 设备|挂载点|文件系统|总容量|已用|使用率
String command = "df -BG -T | grep -E '^/dev/(sd|vd|nvme|mapper|xvd)' | " +
"awk '{print $1 \"|\" $7 \"|\" $2 \"|\" $3 \"|\" $4 \"|\" $6}' | " +
"sed 's/G//g; s/%//'";
String result = safeExecute(sshClient, command);
List<DiskUsageInfo> diskUsageList = new ArrayList<>();
Set<String> seenDevices = new HashSet<>();
if (result != null && !result.trim().isEmpty()) {
String[] lines = result.trim().split("\\n");
for (String line : lines) {
String[] parts = line.split("\\|");
if (parts.length >= 6) {
try {
String device = parts[0].trim();
String mountPoint = parts[1].trim();
String fileSystem = parts[2].trim();
String totalStr = parts[3].trim();
String usedStr = parts[4].trim();
String usageStr = parts[5].trim();
// 去重
if (seenDevices.contains(device)) {
continue;
}
seenDevices.add(device);
DiskUsageInfo diskUsage = DiskUsageInfo.builder()
.mountPoint(mountPoint)
.fileSystem(fileSystem)
.totalSize(new BigDecimal(totalStr))
.usedSize(new BigDecimal(usedStr))
.usagePercent(new BigDecimal(usageStr))
.build();
diskUsageList.add(diskUsage);
} catch (NumberFormatException e) {
log.warn("解析磁盘使用率失败: {}", line, e);
}
}
}
}
return diskUsageList;
} }
@Override @Override

View File

@ -1,11 +1,19 @@
package com.qqchen.deploy.backend.framework.ssh.impl; package com.qqchen.deploy.backend.framework.ssh.impl;
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
import com.qqchen.deploy.backend.framework.dto.DiskUsageInfo;
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum; import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
import com.qqchen.deploy.backend.framework.ssh.AbstractSSHCommandService; import com.qqchen.deploy.backend.framework.ssh.AbstractSSHCommandService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.SSHClient;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** /**
* MacOS SSH命令服务实现Framework层 * MacOS SSH命令服务实现Framework层
*/ */
@ -41,11 +49,114 @@ public class MacOSSSHCommandServiceImpl extends AbstractSSHCommandService {
} }
@Override @Override
public Integer getDiskSize(SSHClient sshClient) { public List<DiskInfo> getDiskInfo(SSHClient sshClient) {
// 获取根分区磁盘大小GB // 使用df命令获取所有挂载点的磁盘信息
String command = "df -g / | tail -1 | awk '{print $2}'"; // 格式: 设备|挂载点|总大小(GB)
// MacOS的df输出格式: Filesystem 512-blocks Used Available Capacity iused ifree %iused Mounted
String command = "df -g | grep -E '^/dev/disk' | awk '{print $1 \"| \" $9 \"| \" $2}'";
String result = safeExecute(sshClient, command); String result = safeExecute(sshClient, command);
return parseInteger(result);
List<DiskInfo> diskList = new ArrayList<>();
Set<String> seenDevices = new HashSet<>();
if (result != null && !result.trim().isEmpty()) {
String[] lines = result.trim().split("\\n");
for (String line : lines) {
String[] parts = line.split("\\|");
if (parts.length >= 3) {
try {
String device = parts[0].trim();
String mountPoint = parts[1].trim();
String sizeStr = parts[2].trim();
// 去重
if (seenDevices.contains(device)) {
log.debug("跳过重复设备: {} -> {}", device, mountPoint);
continue;
}
seenDevices.add(device);
// MacOS通常使用APFS但我们从设备名简化处理
String fileSystem = device.contains("disk") ? "APFS" : "HFS+";
DiskInfo diskInfo = DiskInfo.builder()
.mountPoint(mountPoint)
.fileSystem(fileSystem)
.totalSize(new BigDecimal(sizeStr))
.build();
diskList.add(diskInfo);
} catch (NumberFormatException e) {
log.warn("解析磁盘信息失败: {}", line, e);
}
}
}
}
return diskList;
}
@Override
public BigDecimal getCpuUsage(SSHClient sshClient) {
// MacOS CPU使用率使用top命令获取
String command = "top -l 1 -s 0 | grep 'CPU usage' | awk '{print $3}' | sed 's/%//'";
String result = safeExecute(sshClient, command);
return parseBigDecimal(result);
}
@Override
public BigDecimal getMemoryUsage(SSHClient sshClient) {
// MacOS内存使用率使用vm_stat命令计算
// vm_stat输出的是页数需要计算实际使用率
String command = "vm_stat | awk '/Pages active/ {active=$3} /Pages wired/ {wired=$4} /Pages free/ {free=$3} END {used=active+wired; total=used+free; printf \"%.2f\", (used/total)*100}'";
String result = safeExecute(sshClient, command);
return parseBigDecimal(result);
}
@Override
public List<DiskUsageInfo> getDiskUsage(SSHClient sshClient) {
// MacOS磁盘使用率使用df命令
// 格式: 设备|挂载点|容量|已用|可用|使用率
String command = "df -h | grep -E '^/dev/disk' | awk '{print $1 \"|\" $9 \"|\" $2 \"|\" $3 \"|\" $4 \"|\" $5}' | sed 's/Gi//g; s/%//'";
String result = safeExecute(sshClient, command);
List<DiskUsageInfo> diskUsageList = new ArrayList<>();
Set<String> seenDevices = new HashSet<>();
if (result != null && !result.trim().isEmpty()) {
String[] lines = result.trim().split("\\n");
for (String line : lines) {
String[] parts = line.split("\\|");
if (parts.length >= 6) {
try {
String device = parts[0].trim();
String mountPoint = parts[1].trim();
String totalStr = parts[2].trim();
String usedStr = parts[3].trim();
String usageStr = parts[5].trim();
// 去重
if (seenDevices.contains(device)) {
continue;
}
seenDevices.add(device);
// 简化的文件系统类型
String fileSystem = "APFS";
DiskUsageInfo diskUsage = DiskUsageInfo.builder()
.mountPoint(mountPoint)
.fileSystem(fileSystem)
.totalSize(parseBigDecimal(totalStr))
.usedSize(parseBigDecimal(usedStr))
.usagePercent(parseBigDecimal(usageStr))
.build();
diskUsageList.add(diskUsage);
} catch (Exception e) {
log.warn("解析MacOS磁盘使用率失败: {}", line, e);
}
}
}
}
return diskUsageList;
} }
@Override @Override

View File

@ -1,11 +1,19 @@
package com.qqchen.deploy.backend.framework.ssh.impl; package com.qqchen.deploy.backend.framework.ssh.impl;
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
import com.qqchen.deploy.backend.framework.dto.DiskUsageInfo;
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum; import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
import com.qqchen.deploy.backend.framework.ssh.AbstractSSHCommandService; import com.qqchen.deploy.backend.framework.ssh.AbstractSSHCommandService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.SSHClient;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** /**
* Windows SSH命令服务实现Framework层 * Windows SSH命令服务实现Framework层
* 需要Windows服务器安装OpenSSH Server * 需要Windows服务器安装OpenSSH Server
@ -42,12 +50,132 @@ public class WindowsSSHCommandServiceImpl extends AbstractSSHCommandService {
} }
@Override @Override
public Integer getDiskSize(SSHClient sshClient) { public List<DiskInfo> getDiskInfo(SSHClient sshClient) {
// 使用PowerShell获取C盘大小GB // 使用PowerShell获取所有硬盘分区信息包含驱动器号文件系统类型和总容量
String command = "powershell \"(Get-PSDrive C | " + // 输出格式: 驱动器号|文件系统类型|总容量(GB)
"Select-Object @{N='Size';E={[math]::Round($_.Free/1GB + $_.Used/1GB,0)}}).Size\""; String command = "powershell \"Get-Volume | " +
"Where-Object {$_.DriveLetter -ne $null -and $_.Size -gt 0} | " +
"ForEach-Object {$_.DriveLetter + '|' + $_.FileSystemType + '|' + [math]::Round($_.Size/1GB, 2)}\"";
String result = safeExecute(sshClient, command); String result = safeExecute(sshClient, command);
return parseInteger(result);
List<DiskInfo> diskList = new ArrayList<>();
Set<String> seenDrives = new HashSet<>();
if (result != null && !result.trim().isEmpty()) {
String[] lines = result.trim().split("\\n");
for (String line : lines) {
String[] parts = line.split("\\|");
if (parts.length >= 3) {
try {
String driveLetter = parts[0].trim();
String fileSystem = parts[1].trim();
String sizeStr = parts[2].trim();
// 去重同一驱动器号只记录一次
if (seenDrives.contains(driveLetter)) {
log.debug("跳过重复驱动器: {}", driveLetter);
continue;
}
seenDrives.add(driveLetter);
// 只保留本地物理磁盘排除网络驱动器等
// Windows常见文件系统NTFS, ReFS, FAT32
if (fileSystem == null || fileSystem.isEmpty()) {
fileSystem = "NTFS"; // 默认NTFS
}
DiskInfo diskInfo = DiskInfo.builder()
.mountPoint(driveLetter + ":")
.fileSystem(fileSystem)
.totalSize(new BigDecimal(sizeStr))
.build();
diskList.add(diskInfo);
} catch (NumberFormatException e) {
log.warn("解析磁盘信息失败: {}", line, e);
}
}
}
}
return diskList;
}
@Override
public BigDecimal getCpuUsage(SSHClient sshClient) {
// Windows CPU使用率使用PowerShell获取
String command = "powershell \"Get-Counter '\\Processor(_Total)\\% Processor Time' | " +
"Select-Object -ExpandProperty CounterSamples | " +
"Select-Object -ExpandProperty CookedValue\"";
String result = safeExecute(sshClient, command);
return parseBigDecimal(result);
}
@Override
public BigDecimal getMemoryUsage(SSHClient sshClient) {
// Windows内存使用率使用PowerShell计算
String command = "powershell \"$os = Get-CimInstance Win32_OperatingSystem; " +
"$total = $os.TotalVisibleMemorySize; " +
"$free = $os.FreePhysicalMemory; " +
"$used = $total - $free; " +
"[math]::Round(($used / $total) * 100, 2)\"";
String result = safeExecute(sshClient, command);
return parseBigDecimal(result);
}
@Override
public List<DiskUsageInfo> getDiskUsage(SSHClient sshClient) {
// Windows磁盘使用率使用PowerShell获取
// 输出格式: 驱动器号|文件系统|总容量|已用|使用率
String command = "powershell \"Get-Volume | " +
"Where-Object {$_.DriveLetter -ne $null -and $_.Size -gt 0} | " +
"ForEach-Object {" +
" $used = [math]::Round(($_.Size - $_.SizeRemaining)/1GB, 2); " +
" $total = [math]::Round($_.Size/1GB, 2); " +
" $percent = [math]::Round((($_.Size - $_.SizeRemaining)/$_.Size)*100, 2); " +
" $_.DriveLetter + '|' + $_.FileSystemType + '|' + $total + '|' + $used + '|' + $percent" +
"}\"";
String result = safeExecute(sshClient, command);
List<DiskUsageInfo> diskUsageList = new ArrayList<>();
Set<String> seenDrives = new HashSet<>();
if (result != null && !result.trim().isEmpty()) {
String[] lines = result.trim().split("\\n");
for (String line : lines) {
String[] parts = line.split("\\|");
if (parts.length >= 5) {
try {
String driveLetter = parts[0].trim();
String fileSystem = parts[1].trim();
String totalStr = parts[2].trim();
String usedStr = parts[3].trim();
String usageStr = parts[4].trim();
// 去重
if (seenDrives.contains(driveLetter)) {
continue;
}
seenDrives.add(driveLetter);
// 处理空的文件系统类型
if (fileSystem == null || fileSystem.isEmpty()) {
fileSystem = "NTFS";
}
DiskUsageInfo diskUsage = DiskUsageInfo.builder()
.mountPoint(driveLetter + ":")
.fileSystem(fileSystem)
.totalSize(parseBigDecimal(totalStr))
.usedSize(parseBigDecimal(usedStr))
.usagePercent(parseBigDecimal(usageStr))
.build();
diskUsageList.add(diskUsage);
} catch (Exception e) {
log.warn("解析Windows磁盘使用率失败: {}", line, e);
}
}
}
}
return diskUsageList;
} }
@Override @Override

View File

@ -16,6 +16,8 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.connection.channel.direct.Session; import net.schmizz.sshj.connection.channel.direct.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.TextMessage;
@ -47,6 +49,8 @@ import java.util.concurrent.Future;
@Slf4j @Slf4j
public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler { public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler {
private static final Logger log = LoggerFactory.getLogger(AbstractSSHWebSocketHandler.class);
// ========== 会话存储 ========== // ========== 会话存储 ==========
protected final Map<String, WebSocketSession> webSocketSessions = new ConcurrentHashMap<>(); protected final Map<String, WebSocketSession> webSocketSessions = new ConcurrentHashMap<>();

View File

@ -26,12 +26,6 @@ import lombok.Data;
}) })
public abstract class BaseSendNotificationRequest { public abstract class BaseSendNotificationRequest {
/**
* 通知渠道ID必填
*/
@NotNull(message = "渠道ID不能为空")
private Long channelId;
/** /**
* 消息内容必填 * 消息内容必填
*/ */

View File

@ -17,6 +17,10 @@ import java.util.Map;
@Schema(description = "发送通知请求") @Schema(description = "发送通知请求")
public class SendNotificationRequest { public class SendNotificationRequest {
@Schema(description = "通知渠道ID", required = true, example = "1")
@NotNull(message = "通知渠道ID不能为空")
private Long channelId;
@Schema(description = "通知模板ID", required = true, example = "1") @Schema(description = "通知模板ID", required = true, example = "1")
@NotNull(message = "通知模板ID不能为空") @NotNull(message = "通知模板ID不能为空")
private Long notificationTemplateId; private Long notificationTemplateId;
@ -24,9 +28,4 @@ public class SendNotificationRequest {
@Schema(description = "模板渲染参数(仅用于模板变量替换)", @Schema(description = "模板渲染参数(仅用于模板变量替换)",
example = "{\"projectName\":\"测试项目\",\"buildNumber\":\"123\"}") example = "{\"projectName\":\"测试项目\",\"buildNumber\":\"123\"}")
private Map<String, Object> templateParams; private Map<String, Object> templateParams;
@Schema(description = "具体的发送请求配置", required = true)
@NotNull(message = "发送请求配置不能为空")
@Valid
private BaseSendNotificationRequest sendRequest;
} }

View File

@ -1,6 +1,7 @@
package com.qqchen.deploy.backend.notification.service; package com.qqchen.deploy.backend.notification.service;
import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest; import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest;
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
/** /**
* 通知发送服务接口 * 通知发送服务接口
@ -13,12 +14,10 @@ public interface INotificationSendService {
/** /**
* 发送通知统一接口 * 发送通知统一接口
* *
* @param channel 通知渠道对象
* @param request 通知请求 * @param request 通知请求
* @throws com.qqchen.deploy.backend.framework.exception.BusinessException 渠道不存在渠道已禁用发送失败 * @throws com.qqchen.deploy.backend.framework.exception.BusinessException 渠道已禁用发送失败
*/ */
void send(BaseSendNotificationRequest request); void send(NotificationChannel channel, BaseSendNotificationRequest request);
// TODO: 便捷方法需要重新设计因为现在需要指定具体的请求类型
// 暂时注释掉后续根据需要添加
} }

View File

@ -15,7 +15,6 @@ import com.qqchen.deploy.backend.notification.entity.config.BaseNotificationConf
import com.qqchen.deploy.backend.notification.entity.config.EmailNotificationConfig; import com.qqchen.deploy.backend.notification.entity.config.EmailNotificationConfig;
import com.qqchen.deploy.backend.notification.entity.config.WeworkNotificationConfig; import com.qqchen.deploy.backend.notification.entity.config.WeworkNotificationConfig;
import com.qqchen.deploy.backend.notification.factory.NotificationChannelAdapterFactory; import com.qqchen.deploy.backend.notification.factory.NotificationChannelAdapterFactory;
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
import com.qqchen.deploy.backend.notification.service.INotificationChannelService; import com.qqchen.deploy.backend.notification.service.INotificationChannelService;
import com.qqchen.deploy.backend.notification.service.INotificationSendService; import com.qqchen.deploy.backend.notification.service.INotificationSendService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@ -36,15 +35,12 @@ import static com.qqchen.deploy.backend.framework.annotation.ServiceType.Type.DA
@ServiceType(DATABASE) @ServiceType(DATABASE)
public class NotificationSendServiceImpl implements INotificationSendService { public class NotificationSendServiceImpl implements INotificationSendService {
@Resource
private INotificationChannelRepository notificationChannelRepository;
@Resource @Resource
private NotificationChannelAdapterFactory adapterFactory; private NotificationChannelAdapterFactory adapterFactory;
public void send(BaseSendNotificationRequest request) { public void send(NotificationChannel channel, BaseSendNotificationRequest request) {
// 1. 参数校验 // 1. 参数校验
if (request == null || request.getChannelId() == null) { if (channel == null || request == null) {
throw new BusinessException(ResponseCode.INVALID_PARAM); throw new BusinessException(ResponseCode.INVALID_PARAM);
} }
@ -52,15 +48,12 @@ public class NotificationSendServiceImpl implements INotificationSendService {
throw new BusinessException(ResponseCode.INVALID_PARAM); throw new BusinessException(ResponseCode.INVALID_PARAM);
} }
// 2. 查询渠道配置 // 2. 校验渠道状态
NotificationChannel channel = notificationChannelRepository.findById(request.getChannelId()).orElseThrow(() -> new BusinessException(ResponseCode.NOTIFICATION_CHANNEL_NOT_FOUND));
// 3. 校验渠道状态
if (!channel.getEnabled()) { if (!channel.getEnabled()) {
throw new BusinessException(ResponseCode.NOTIFICATION_CHANNEL_DISABLED); throw new BusinessException(ResponseCode.NOTIFICATION_CHANNEL_DISABLED);
} }
// 4. 获取对应的适配器 // 3. 获取对应的适配器
INotificationChannelAdapter adapter; INotificationChannelAdapter adapter;
try { try {
adapter = adapterFactory.getAdapter(channel.getChannelType()); adapter = adapterFactory.getAdapter(channel.getChannelType());

View File

@ -3,6 +3,7 @@ package com.qqchen.deploy.backend.notification.service.impl;
import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException; import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.utils.JsonUtils; import com.qqchen.deploy.backend.framework.utils.JsonUtils;
import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest; import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest; import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest; import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
@ -49,32 +50,30 @@ public class NotificationServiceImpl implements INotificationService {
@Override @Override
public void send(SendNotificationRequest request) { public void send(SendNotificationRequest request) {
// 1. 获取通知模板 // 1. 获取通知渠道
NotificationChannel channel = notificationChannelRepository.findById(request.getChannelId())
.orElseThrow(() -> new BusinessException(ResponseCode.NOTIFICATION_CHANNEL_NOT_FOUND));
// 2. 获取通知模板
NotificationTemplate template = notificationTemplateRepository.findById(request.getNotificationTemplateId()) NotificationTemplate template = notificationTemplateRepository.findById(request.getNotificationTemplateId())
.orElseThrow(() -> new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_NOT_FOUND)); .orElseThrow(() -> new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_NOT_FOUND));
// 2. 获取通知渠道
NotificationChannel channel = notificationChannelRepository.findById(request.getSendRequest().getChannelId())
.orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND));
// 3. 验证模板和渠道类型是否匹配 // 3. 验证模板和渠道类型是否匹配
if (!template.getChannelType().equals(channel.getChannelType())) { if (!template.getChannelType().equals(channel.getChannelType())) {
throw new BusinessException(ResponseCode.INVALID_PARAM); throw new BusinessException(ResponseCode.INVALID_PARAM);
} }
// 4. 验证SendRequest渠道类型和模板渠道类型是否匹配 // 4. 验证模板和渠道是否启用
if (!request.getSendRequest().getChannelType().equals(template.getChannelType())) {
throw new BusinessException(ResponseCode.INVALID_PARAM);
}
// 5. 验证模板和渠道是否启用
if (!template.getEnabled()) { if (!template.getEnabled()) {
throw new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_DISABLED); throw new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_DISABLED);
} }
if (!channel.getEnabled()) { if (!channel.getEnabled()) {
throw new BusinessException(ResponseCode.DATA_NOT_FOUND); throw new BusinessException(ResponseCode.NOTIFICATION_CHANNEL_DISABLED);
} }
// 5. 根据渠道类型自动创建对应的发送请求对象
BaseSendNotificationRequest sendRequest = createSendRequestByChannel(channel, template);
// 6. 渲染模板内容 // 6. 渲染模板内容
String content = notificationTemplateService.renderTemplate(template.getContentTemplate(), request.getTemplateParams()); String content = notificationTemplateService.renderTemplate(template.getContentTemplate(), request.getTemplateParams());
@ -82,11 +81,65 @@ public class NotificationServiceImpl implements INotificationService {
String title = notificationTemplateService.renderTitleById(request.getNotificationTemplateId(), request.getTemplateParams()); String title = notificationTemplateService.renderTitleById(request.getNotificationTemplateId(), request.getTemplateParams());
// 8. 设置渲染后的内容和标题 // 8. 设置渲染后的内容和标题
request.getSendRequest().setContent(content); sendRequest.setContent(content);
request.getSendRequest().setTitle(title); sendRequest.setTitle(title);
// 9. 发送通知 // 9. 发送通知
notificationSendService.send(request.getSendRequest()); notificationSendService.send(channel, sendRequest);
}
/**
* 根据渠道类型自动创建对应的发送请求对象
* 业务方无需关心此逻辑由通知服务内部处理
*/
private BaseSendNotificationRequest createSendRequestByChannel(
NotificationChannel channel,
NotificationTemplate template) {
switch (channel.getChannelType()) {
case WEWORK -> {
WeworkSendNotificationRequest request = new WeworkSendNotificationRequest();
// 从模板配置中获取消息类型
request.setMessageType(getWeworkMessageType(template));
return request;
}
case EMAIL -> {
EmailSendNotificationRequest request = new EmailSendNotificationRequest();
// 从模板配置中获取收件人如果有的话
request.setToReceivers(getEmailReceivers(template));
return request;
}
default -> throw new BusinessException(ResponseCode.ERROR,
new Object[]{"不支持的渠道类型: " + channel.getChannelType()});
}
}
/**
* 从模板配置中获取企业微信消息类型
*/
private WeworkMessageTypeEnum getWeworkMessageType(NotificationTemplate template) {
try {
if (template.getTemplateConfig() != null) {
WeworkTemplateConfig config = JsonUtils.fromMap(
template.getTemplateConfig(), WeworkTemplateConfig.class);
if (config != null && config.getMessageType() != null) {
return config.getMessageType();
}
}
} catch (Exception e) {
log.warn("解析企业微信模板配置失败,使用默认消息类型: {}", e.getMessage());
}
// 默认使用TEXT类型
return WeworkMessageTypeEnum.TEXT;
}
/**
* 从模板配置中获取邮件收件人
*/
private List<String> getEmailReceivers(NotificationTemplate template) {
// 邮件收件人由调用方在 templateParams 中指定或者在渠道配置中配置
// 这里返回默认收件人实际使用时由具体业务场景传递
return java.util.Arrays.asList("admin@company.com");
} }
} }

View File

@ -1,15 +1,6 @@
package com.qqchen.deploy.backend.workflow.delegate; package com.qqchen.deploy.backend.workflow.delegate;
import com.qqchen.deploy.backend.framework.utils.JsonUtils;
import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest; 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.entity.config.WeworkTemplateConfig;
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.INotificationService; import com.qqchen.deploy.backend.notification.service.INotificationService;
import com.qqchen.deploy.backend.workflow.dto.inputmapping.NotificationInputMapping; import com.qqchen.deploy.backend.workflow.dto.inputmapping.NotificationInputMapping;
import com.qqchen.deploy.backend.workflow.dto.outputs.NotificationOutputs; import com.qqchen.deploy.backend.workflow.dto.outputs.NotificationOutputs;
@ -35,12 +26,6 @@ public class NotificationNodeDelegate extends BaseNodeDelegate<NotificationInput
@Resource @Resource
private INotificationService notificationService; private INotificationService notificationService;
@Resource
private INotificationChannelRepository notificationChannelRepository;
@Resource
private INotificationTemplateRepository notificationTemplateRepository;
@Override @Override
protected void executeInternal(DelegateExecution execution, Map<String, Object> configs, NotificationInputMapping input) { protected void executeInternal(DelegateExecution execution, Map<String, Object> configs, NotificationInputMapping input) {
// 1. 参数校验 // 1. 参数校验
@ -48,80 +33,16 @@ public class NotificationNodeDelegate extends BaseNodeDelegate<NotificationInput
logWarn(String.format("Notification delegate parameter verification failed - channelId: %s, templateId: %s", input.getChannelId(), input.getNotificationTemplateId())); logWarn(String.format("Notification delegate parameter verification failed - channelId: %s, templateId: %s", input.getChannelId(), input.getNotificationTemplateId()));
return; return;
} }
// 2. 查询渠道和模板信息
NotificationChannel channel = notificationChannelRepository.findById(input.getChannelId()).orElseThrow(() -> new RuntimeException("通知渠道不存在: " + input.getChannelId()));
NotificationTemplate template = notificationTemplateRepository.findById(input.getNotificationTemplateId()).orElseThrow(() -> new RuntimeException("通知模板不存在: " + input.getNotificationTemplateId()));
// 3. 构建SendNotificationRequest // 2. 构建SendNotificationRequest
SendNotificationRequest request = new SendNotificationRequest(); SendNotificationRequest request = new SendNotificationRequest();
request.setChannelId(input.getChannelId());
request.setNotificationTemplateId(input.getNotificationTemplateId()); request.setNotificationTemplateId(input.getNotificationTemplateId());
request.setTemplateParams(execution.getVariables()); request.setTemplateParams(execution.getVariables());
// 4. 根据渠道类型创建sendRequest并从模板配置中获取参数 // 3. 发送通知NotificationService会自动根据渠道类型创建请求对象
switch (channel.getChannelType()) {
case WEWORK -> {
WeworkSendNotificationRequest weworkRequest = new WeworkSendNotificationRequest();
weworkRequest.setChannelId(input.getChannelId());
// 从模板配置中获取消息类型
weworkRequest.setMessageType(getWeworkMessageType(template));
request.setSendRequest(weworkRequest);
}
case EMAIL -> {
EmailSendNotificationRequest emailRequest = new EmailSendNotificationRequest();
emailRequest.setChannelId(input.getChannelId());
// 收件人从工作流变量获取
emailRequest.setToReceivers(getEmailReceivers(execution, configs));
// 其他配置HTML格式等由NotificationService根据模板配置自动设置
request.setSendRequest(emailRequest);
}
default -> throw new RuntimeException("不支持的渠道类型: " + channel.getChannelType());
}
// 5. 发送通知NotificationService会处理模板渲染和详细配置
notificationService.send(request); notificationService.send(request);
log.info("工作流通知发送成功 - 渠道ID: {}, 模板ID: {}", input.getChannelId(), input.getNotificationTemplateId()); log.info("工作流通知发送成功 - 渠道ID: {}, 模板ID: {}", input.getChannelId(), input.getNotificationTemplateId());
} }
/**
* 从模板配置中获取企业微信消息类型
*/
private WeworkMessageTypeEnum getWeworkMessageType(NotificationTemplate template) {
try {
if (template.getTemplateConfig() != null) {
WeworkTemplateConfig weworkConfig = JsonUtils.fromMap(template.getTemplateConfig(), WeworkTemplateConfig.class);
if (weworkConfig != null && weworkConfig.getMessageType() != null) {
return weworkConfig.getMessageType();
}
}
} catch (Exception e) {
log.warn("解析企业微信模板配置失败,使用默认消息类型: {}", e.getMessage());
}
// 默认使用TEXT类型
return WeworkMessageTypeEnum.TEXT;
}
/**
* 获取邮件收件人列表
* 从工作流变量或配置中获取如果没有则使用默认值
*/
private java.util.List<String> getEmailReceivers(DelegateExecution execution, Map<String, Object> configs) {
// 1. 优先从工作流变量中获取
Object receiversVar = execution.getVariable("emailReceivers");
if (receiversVar instanceof java.util.List) {
return (java.util.List<String>) receiversVar;
}
// 2. 从配置中获取
if (configs != null && configs.containsKey("emailReceivers")) {
Object receiversConfig = configs.get("emailReceivers");
if (receiversConfig instanceof java.util.List) {
return (java.util.List<String>) receiversConfig;
}
}
// 3. 使用默认收件人
return Arrays.asList("admin@company.com");
}
} }

View File

@ -1140,7 +1140,8 @@ CREATE TABLE deploy_server
description VARCHAR(500) NULL COMMENT '服务器描述', description VARCHAR(500) NULL COMMENT '服务器描述',
cpu_cores INT NULL COMMENT 'CPU核心数', cpu_cores INT NULL COMMENT 'CPU核心数',
memory_size INT NULL COMMENT '内存大小(GB)', memory_size INT NULL COMMENT '内存大小(GB)',
disk_size INT NULL COMMENT '磁盘大小(GB)', disk_size INT NULL COMMENT '磁盘总容量(GB)',
disk_info JSON NULL COMMENT '磁盘详细信息JSON格式包含挂载点、文件系统、容量等',
tags JSON NULL COMMENT '标签JSON格式', tags JSON NULL COMMENT '标签JSON格式',
last_connect_time DATETIME NULL COMMENT '最后连接时间', last_connect_time DATETIME NULL COMMENT '最后连接时间',
@ -1158,6 +1159,104 @@ CREATE TABLE deploy_server
CONSTRAINT fk_server_category FOREIGN KEY (category_id) REFERENCES deploy_server_category (id) CONSTRAINT fk_server_category FOREIGN KEY (category_id) REFERENCES deploy_server_category (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='服务器管理表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='服务器管理表';
-- 服务器监控记录表
CREATE TABLE deploy_server_monitor
(
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
server_id BIGINT NOT NULL COMMENT '服务器ID',
-- 监控指标
cpu_usage DECIMAL(5,2) NULL COMMENT 'CPU使用率(%)',
memory_usage DECIMAL(5,2) NULL COMMENT '内存使用率(%)',
memory_used INT NULL COMMENT '已用内存(GB)',
-- 磁盘使用情况JSON
disk_usage JSON NULL COMMENT '各分区使用率',
-- 网络流量(可选)
network_rx BIGINT NULL COMMENT '网络接收(KB/s)',
network_tx BIGINT NULL COMMENT '网络发送(KB/s)',
-- 采集时间
collect_time DATETIME NOT NULL COMMENT '采集时间',
INDEX idx_server_time (server_id, collect_time),
INDEX idx_collect_time (collect_time),
CONSTRAINT fk_monitor_server FOREIGN KEY (server_id) REFERENCES deploy_server (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='服务器监控记录表';
-- 服务器告警规则表
CREATE TABLE deploy_server_alert_rule
(
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
create_by VARCHAR(100) NULL COMMENT '创建人',
create_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_by VARCHAR(100) NULL COMMENT '更新人',
update_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
version INT NOT NULL DEFAULT 1 COMMENT '版本号',
deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否删除',
server_id BIGINT NULL COMMENT '服务器IDNULL表示全局规则',
rule_name VARCHAR(100) NOT NULL COMMENT '规则名称',
-- 告警类型
alert_type VARCHAR(20) NOT NULL COMMENT '告警类型: CPU/MEMORY/DISK',
-- 阈值
warning_threshold DECIMAL(5,2) NOT NULL COMMENT '警告阈值(%)',
critical_threshold DECIMAL(5,2) NOT NULL COMMENT '严重阈值(%)',
-- 持续时间(避免误报)
duration_minutes INT DEFAULT 5 COMMENT '持续时长(分钟)',
-- 是否启用
enabled BOOLEAN DEFAULT TRUE COMMENT '是否启用',
-- 通知方式
notify_method VARCHAR(50) NULL COMMENT '通知方式: EMAIL/SMS/WEBHOOK',
notify_contacts JSON NULL COMMENT '通知联系人',
-- 描述
description VARCHAR(500) NULL COMMENT '规则描述',
INDEX idx_server (server_id),
INDEX idx_type (alert_type),
INDEX idx_enabled (enabled),
CONSTRAINT fk_alert_rule_server FOREIGN KEY (server_id) REFERENCES deploy_server (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='服务器告警规则表';
-- 服务器告警记录表
CREATE TABLE deploy_server_alert_log
(
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
server_id BIGINT NOT NULL COMMENT '服务器ID',
rule_id BIGINT NULL COMMENT '规则ID',
-- 告警信息
alert_type VARCHAR(20) NOT NULL COMMENT '告警类型: CPU/MEMORY/DISK',
alert_level VARCHAR(20) NOT NULL COMMENT '告警级别: WARNING/CRITICAL',
alert_value DECIMAL(5,2) NOT NULL COMMENT '当前值',
threshold_value DECIMAL(5,2) NOT NULL COMMENT '阈值',
alert_message VARCHAR(500) NULL COMMENT '告警消息',
-- 告警状态
status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态: ACTIVE/RESOLVED',
-- 时间
alert_time DATETIME NOT NULL COMMENT '告警时间',
resolve_time DATETIME NULL COMMENT '恢复时间',
-- 通知状态
notified BOOLEAN DEFAULT FALSE COMMENT '是否已通知',
notify_time DATETIME NULL COMMENT '通知时间',
INDEX idx_server_time (server_id, alert_time),
INDEX idx_status (status),
INDEX idx_rule (rule_id),
CONSTRAINT fk_alert_log_server FOREIGN KEY (server_id) REFERENCES deploy_server (id),
CONSTRAINT fk_alert_log_rule FOREIGN KEY (rule_id) REFERENCES deploy_server_alert_rule (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='服务器告警记录表';
-- 部署记录表 -- 部署记录表
CREATE TABLE deploy_record CREATE TABLE deploy_record
( (

View File

@ -33,8 +33,8 @@ dependency.injection.entitypath.failed=初始化实体 {0} 的EntityPath失败:
# -------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------
# SSH文件操作相关 (SSH File Operations) - 1200-1299 # SSH文件操作相关 (SSH File Operations) - 1200-1299
# -------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------
ssh.file.connection.failed=SSH连接失败 ssh.file.connection.failed=SSH连接失败,请检查服务器地址、端口和网络连接
ssh.file.authentication.failed=SSH认证失败 ssh.file.authentication.failed=SSH认证失败,请检查用户名和密码/密钥是否正确
ssh.file.not.found=文件不存在 ssh.file.not.found=文件不存在
ssh.file.directory.not.found=目录不存在 ssh.file.directory.not.found=目录不存在
ssh.file.permission.denied=权限不足 ssh.file.permission.denied=权限不足

View File

@ -33,8 +33,8 @@ dependency.injection.entitypath.failed=Failed to initialize EntityPath for entit
# -------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------
# SSH File Operations - 1200-1299 # SSH File Operations - 1200-1299
# -------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------
ssh.file.connection.failed=SSH connection failed ssh.file.connection.failed=SSH connection failed, please check server address, port and network connection
ssh.file.authentication.failed=SSH authentication failed ssh.file.authentication.failed=SSH authentication failed, please check username and password/key
ssh.file.not.found=File not found ssh.file.not.found=File not found
ssh.file.directory.not.found=Directory not found ssh.file.directory.not.found=Directory not found
ssh.file.permission.denied=Permission denied ssh.file.permission.denied=Permission denied

View File

@ -33,8 +33,8 @@ dependency.injection.entitypath.failed=获取实体 {0} 的QClass失败
# -------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------
# SSH文件操作相关 (SSH File Operations) - 1200-1299 # SSH文件操作相关 (SSH File Operations) - 1200-1299
# -------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------
ssh.file.connection.failed=SSH连接失败 ssh.file.connection.failed=SSH连接失败,请检查服务器地址、端口和网络连接
ssh.file.authentication.failed=SSH认证失败 ssh.file.authentication.failed=SSH认证失败,请检查用户名和密码/密钥是否正确
ssh.file.not.found=文件不存在 ssh.file.not.found=文件不存在
ssh.file.directory.not.found=目录不存在 ssh.file.directory.not.found=目录不存在
ssh.file.permission.denied=权限不足 ssh.file.permission.denied=权限不足