diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/ServerAlertRuleApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/ServerAlertRuleApiController.java new file mode 100644 index 00000000..da471525 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/ServerAlertRuleApiController.java @@ -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 { + + @Resource + private IServerAlertRuleService alertRuleService; + + @Override + @Operation(summary = "创建告警规则", description = "创建新的服务器告警规则") + @PostMapping + public Response create(@Validated @RequestBody ServerAlertRuleDTO dto) { + return super.create(dto); + } + + @Override + @Operation(summary = "更新告警规则", description = "更新指定ID的告警规则") + @PutMapping("/{id}") + public Response 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 delete( + @Parameter(description = "告警规则ID", required = true) @PathVariable Long id + ) { + return super.delete(id); + } + + @Override + @Operation(summary = "查询告警规则详情", description = "根据ID查询告警规则详情") + @GetMapping("/{id}") + public Response findById( + @Parameter(description = "告警规则ID", required = true) @PathVariable Long id + ) { + return super.findById(id); + } + + @Override + @Operation(summary = "查询所有告警规则", description = "查询所有告警规则列表") + @GetMapping + public Response> findAll() { + return super.findAll(); + } + + @Override + @Operation(summary = "分页查询告警规则", description = "根据条件分页查询告警规则") + @GetMapping("/page") + public Response> page(ServerAlertRuleQuery query) { + return super.page(query); + } + + @Override + @Operation(summary = "条件查询告警规则列表", description = "根据条件查询告警规则列表") + @GetMapping("/list") + public Response> findAll(ServerAlertRuleQuery query) { + return super.findAll(query); + } + + @Override + @Operation(summary = "批量处理告警规则", description = "批量创建/更新/删除告警规则") + @PostMapping("/batch") + public CompletableFuture> batchProcess(@RequestBody List dtos) { + return super.batchProcess(dtos); + } + + @Override + protected void exportData(HttpServletResponse response, List data) { + log.info("导出告警规则数据,数据量:{}", data.size()); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/ServerAlertRuleConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/ServerAlertRuleConverter.java new file mode 100644 index 00000000..f11d1e4c --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/ServerAlertRuleConverter.java @@ -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 { +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerAlertRuleDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerAlertRuleDTO.java new file mode 100644 index 00000000..f528405d --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerAlertRuleDTO.java @@ -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 = "服务器ID(NULL表示全局规则)") + 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; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerDTO.java index c2dca37e..ab8a27cf 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerDTO.java @@ -1,5 +1,6 @@ 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.OsTypeEnum; 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.EqualsAndHashCode; import java.time.LocalDateTime; +import java.util.List; /** * 服务器 DTO @@ -96,10 +98,15 @@ public class ServerDTO extends BaseDTO { private Integer memorySize; /** - * 磁盘大小(GB) + * 磁盘总容量(GB) */ private Integer diskSize; + /** + * 磁盘详细信息列表 + */ + private List diskInfo; + /** * 标签(JSON格式) */ diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerInfoDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerInfoDTO.java index dfeee04b..aa5db8fe 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerInfoDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerInfoDTO.java @@ -1,8 +1,10 @@ package com.qqchen.deploy.backend.deploy.dto; +import com.qqchen.deploy.backend.framework.dto.DiskInfo; import lombok.Data; import java.time.LocalDateTime; +import java.util.List; /** * 服务器信息DTO @@ -56,7 +58,12 @@ public class ServerInfoDTO { private Integer memorySize; /** - * 磁盘大小(GB) + * 磁盘总容量(GB) */ private Integer diskSize; + + /** + * 磁盘详细信息列表 + */ + private List diskInfo; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerInitializeDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerInitializeDTO.java index af30dfcf..e55e30ac 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerInitializeDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerInitializeDTO.java @@ -1,9 +1,12 @@ package com.qqchen.deploy.backend.deploy.dto; +import com.qqchen.deploy.backend.framework.dto.DiskInfo; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotEmpty; import lombok.Data; +import java.util.List; /** * 服务器初始化 DTO @@ -27,12 +30,18 @@ public class ServerInitializeDTO { private Integer memorySize; /** - * 磁盘大小(GB) + * 磁盘总容量(GB) */ @NotNull(message = "磁盘大小不能为空") @Min(value = 1, message = "磁盘大小至少为1GB") private Integer diskSize; + /** + * 磁盘详细信息列表 + */ + @NotEmpty(message = "磁盘信息不能为空") + private List diskInfo; + /** * 操作系统版本 */ diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerMonitorDataDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerMonitorDataDTO.java new file mode 100644 index 00000000..4065dea8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerMonitorDataDTO.java @@ -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 diskUsage; + + @Schema(description = "采集时间") + private LocalDateTime collectTime; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerMonitorNotificationConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerMonitorNotificationConfig.java new file mode 100644 index 00000000..21ad8d0d --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ServerMonitorNotificationConfig.java @@ -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; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/Server.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/Server.java index b7285f31..ef1ee8ba 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/Server.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/Server.java @@ -1,5 +1,7 @@ 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.OsTypeEnum; import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum; @@ -8,6 +10,7 @@ import jakarta.persistence.*; import lombok.Data; import lombok.EqualsAndHashCode; import java.time.LocalDateTime; +import java.util.List; /** * 服务器实体 @@ -121,11 +124,20 @@ public class Server extends Entity { private Integer memorySize; /** - * 磁盘大小(GB) + * 磁盘总容量(GB) + * 所有磁盘分区容量的总和,方便排序和统计 */ @Column(name = "disk_size") private Integer diskSize; + /** + * 磁盘详细信息列表 + * 存储多个磁盘的详细信息,包括挂载点、容量 + */ + @Convert(converter = DiskInfoListConverter.class) + @Column(name = "disk_info", columnDefinition = "JSON") + private List diskInfo; + /** * 标签(JSON格式) */ diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ServerAlertLog.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ServerAlertLog.java new file mode 100644 index 00000000..a9a06886 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ServerAlertLog.java @@ -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; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ServerAlertRule.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ServerAlertRule.java new file mode 100644 index 00000000..11a32928 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ServerAlertRule.java @@ -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 { + + /** + * 服务器ID(NULL表示全局规则) + */ + @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; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ServerMonitor.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ServerMonitor.java new file mode 100644 index 00000000..f95b10f5 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ServerMonitor.java @@ -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; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/AlertLevelEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/AlertLevelEnum.java new file mode 100644 index 00000000..88a382df --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/AlertLevelEnum.java @@ -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; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/AlertTypeEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/AlertTypeEnum.java new file mode 100644 index 00000000..760c999a --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/AlertTypeEnum.java @@ -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; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/ServerAlertRuleQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/ServerAlertRuleQuery.java new file mode 100644 index 00000000..bc2fb070 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/ServerAlertRuleQuery.java @@ -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; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IServerAlertLogRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IServerAlertLogRepository.java new file mode 100644 index 00000000..2313fa96 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IServerAlertLogRepository.java @@ -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 { + + /** + * 查询指定服务器的活跃告警 + */ + List findByServerIdAndStatus(Long serverId, String status); + + /** + * 查询指定服务器在指定时间范围内的告警记录 + */ + List findByServerIdAndAlertTimeBetweenOrderByAlertTimeDesc( + Long serverId, LocalDateTime startTime, LocalDateTime endTime); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IServerAlertRuleRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IServerAlertRuleRepository.java new file mode 100644 index 00000000..d2d4896f --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IServerAlertRuleRepository.java @@ -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 { +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IServerMonitorRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IServerMonitorRepository.java new file mode 100644 index 00000000..b9731928 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IServerMonitorRepository.java @@ -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 { + + /** + * 查询指定服务器在指定时间范围内的监控记录 + */ + List findByServerIdAndCollectTimeBetweenOrderByCollectTimeDesc( + Long serverId, LocalDateTime startTime, LocalDateTime endTime); + + /** + * 删除指定时间之前的监控记录 + */ + @Modifying + @Query("DELETE FROM ServerMonitor m WHERE m.collectTime < :beforeTime") + int deleteByCollectTimeBefore(@Param("beforeTime") LocalDateTime beforeTime); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IServerRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IServerRepository.java index 464299a6..2af546d4 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IServerRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IServerRepository.java @@ -1,9 +1,12 @@ package com.qqchen.deploy.backend.deploy.repository; 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 org.springframework.stereotype.Repository; +import java.util.List; + /** * 服务器仓储接口 */ @@ -25,5 +28,14 @@ public interface IServerRepository extends IBaseRepository { * @return 是否存在 */ boolean existsByHostIpAndDeletedFalse(String hostIp); + + /** + * 根据状态和删除标记查询服务器列表 + * + * @param status 服务器状态 + * @param deleted 是否删除 + * @return 服务器列表 + */ + List findByStatusAndDeleted(ServerStatusEnum status, boolean deleted); } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/scheduler/ServerMonitorScheduler.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/scheduler/ServerMonitorScheduler.java new file mode 100644 index 00000000..fd12f977 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/scheduler/ServerMonitorScheduler.java @@ -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 shouldBeOnlineServers = serverRepository + .findByStatusAndDeleted(ServerStatusEnum.ONLINE, false); + + if (shouldBeOnlineServers.isEmpty()) { + log.debug("没有需要监控的服务器,跳过监控采集"); + return; + } + + log.info("发现 {} 台应在线服务器,开始检测状态", shouldBeOnlineServers.size()); + + // 2. 并发检测服务器连接状态并采集监控数据 + final ServerMonitorNotificationConfig finalConfig = config; + List> futures = shouldBeOnlineServers.stream() + .map(server -> CompletableFuture.supplyAsync(() -> + collectSingleServerWithStatusCheck(server, finalConfig))) + .collect(Collectors.toList()); + + // 3. 等待所有任务完成 + CompletableFuture allFutures = CompletableFuture.allOf( + futures.toArray(new CompletableFuture[0]) + ); + allFutures.join(); + + // 4. 收集结果 + List 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 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 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); + } + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IServerAlertRuleService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IServerAlertRuleService.java new file mode 100644 index 00000000..c2241927 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IServerAlertRuleService.java @@ -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 { +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IServerAlertService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IServerAlertService.java new file mode 100644 index 00000000..d60349c5 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IServerAlertService.java @@ -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); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IServerMonitorService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IServerMonitorService.java new file mode 100644 index 00000000..aad5c89a --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IServerMonitorService.java @@ -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 dataList); + + /** + * 删除指定时间之前的历史数据 + */ + int deleteOldData(LocalDateTime beforeTime); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsBuildServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsBuildServiceImpl.java index e9711a20..2f328dba 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsBuildServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsBuildServiceImpl.java @@ -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.service.IJenkinsBuildService; 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.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.INotificationTemplateRepository; import com.qqchen.deploy.backend.notification.service.INotificationService; import com.qqchen.deploy.backend.notification.service.INotificationSendService; 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.lock.SyncLockManager; import com.qqchen.deploy.backend.framework.enums.ResponseCode; @@ -763,21 +758,15 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl + 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); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/ServerAlertServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/ServerAlertServiceImpl.java new file mode 100644 index 00000000..e3666cb1 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/ServerAlertServiceImpl.java @@ -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 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 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 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; + } + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/ServerMonitorServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/ServerMonitorServiceImpl.java new file mode 100644 index 00000000..49ef8b2e --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/ServerMonitorServiceImpl.java @@ -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 dataList) { + if (dataList == null || dataList.isEmpty()) { + return; + } + + List 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(); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/ServerServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/ServerServiceImpl.java index 518f326c..da9ba08a 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/ServerServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/ServerServiceImpl.java @@ -74,6 +74,7 @@ public class ServerServiceImpl server.setCpuCores(dto.getCpuCores()); server.setMemorySize(dto.getMemorySize()); server.setDiskSize(dto.getDiskSize()); + server.setDiskInfo(dto.getDiskInfo()); server.setOsVersion(dto.getOsVersion()); server.setHostname(dto.getHostname()); server.setStatus(ServerStatusEnum.ONLINE); @@ -81,8 +82,9 @@ public class ServerServiceImpl // 3. 保存并返回 Server updated = serverRepository.save(server); - log.info("服务器初始化成功: serverId={}, cpuCores={}, memorySize={}GB, diskSize={}GB", - serverId, dto.getCpuCores(), dto.getMemorySize(), dto.getDiskSize()); + log.info("服务器初始化成功: serverId={}, cpuCores={}, memorySize={}GB, diskSize={}GB, diskCount={}", + serverId, dto.getCpuCores(), dto.getMemorySize(), dto.getDiskSize(), + dto.getDiskInfo() != null ? dto.getDiskInfo().size() : 0); return converter.toDto(updated); } @@ -169,10 +171,19 @@ public class ServerServiceImpl info.setOsVersion(sshService.getOsVersion(sshClient)); info.setCpuCores(sshService.getCpuCores(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. 更新服务器信息到数据库 server.setStatus(ServerStatusEnum.ONLINE); @@ -182,6 +193,7 @@ public class ServerServiceImpl server.setCpuCores(info.getCpuCores()); server.setMemorySize(info.getMemorySize()); server.setDiskSize(info.getDiskSize()); + server.setDiskInfo(info.getDiskInfo()); serverRepository.save(server); log.info("服务器状态已更新为ONLINE: serverId={}", serverId); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/converter/DiskInfoListConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/converter/DiskInfoListConverter.java new file mode 100644 index 00000000..6faebc71 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/converter/DiskInfoListConverter.java @@ -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, String> { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(List 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 convertToEntityAttribute(String dbData) { + if (dbData == null || dbData.trim().isEmpty()) { + return new ArrayList<>(); + } + try { + return objectMapper.readValue(dbData, new TypeReference>() {}); + } catch (JsonProcessingException e) { + log.error("解析磁盘信息JSON失败: {}", dbData, e); + return new ArrayList<>(); + } + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/dto/DiskInfo.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/dto/DiskInfo.java new file mode 100644 index 00000000..fabd8ba7 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/dto/DiskInfo.java @@ -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; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/dto/DiskUsageInfo.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/dto/DiskUsageInfo.java new file mode 100644 index 00000000..d70d9279 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/dto/DiskUsageInfo.java @@ -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; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/AbstractSSHCommandService.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/AbstractSSHCommandService.java index 93ae49b8..21beba9c 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/AbstractSSHCommandService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/AbstractSSHCommandService.java @@ -64,11 +64,37 @@ public abstract class AbstractSSHCommandService implements ISSHCommandService { 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) { - // 连接失败,关闭客户端 + // 其他未知异常 closeConnection(sshClient); 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; } } + + /** + * 解析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; + } + } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/ISSHCommandService.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/ISSHCommandService.java index 791e8ba0..e1de921c 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/ISSHCommandService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/ISSHCommandService.java @@ -1,8 +1,13 @@ 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 net.schmizz.sshj.SSHClient; +import java.math.BigDecimal; +import java.util.List; + /** * SSH命令服务接口(Framework层) * 封装了SSH连接和OS命令执行能力 @@ -74,12 +79,36 @@ public interface ISSHCommandService { Integer getMemorySize(SSHClient sshClient); /** - * 获取磁盘大小(GB) + * 获取磁盘信息列表 * * @param sshClient SSH客户端 - * @return 磁盘大小 + * @return 磁盘信息列表 */ - Integer getDiskSize(SSHClient sshClient); + List 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 getDiskUsage(SSHClient sshClient); /** * 获取支持的操作系统类型 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/impl/LinuxSSHCommandServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/impl/LinuxSSHCommandServiceImpl.java index 22799726..b3d554d0 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/impl/LinuxSSHCommandServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/impl/LinuxSSHCommandServiceImpl.java @@ -1,11 +1,19 @@ 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.ssh.AbstractSSHCommandService; import lombok.extern.slf4j.Slf4j; import net.schmizz.sshj.SSHClient; 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层) */ @@ -41,10 +49,112 @@ public class LinuxSSHCommandServiceImpl extends AbstractSSHCommandService { } @Override - public Integer getDiskSize(SSHClient sshClient) { - // 获取根分区的磁盘大小(GB) - String result = safeExecute(sshClient, "df -BG / | tail -1 | awk '{print $2}' | sed 's/G//'"); - return parseInteger(result); + public List getDiskInfo(SSHClient sshClient) { + // 使用df命令获取所有挂载点的磁盘信息 + // 格式: 设备|挂载点|文件系统类型|总大小(GB) + // 过滤条件:只保留 /dev/sd*, /dev/vd*, /dev/nvme*, /dev/mapper/* 等真实磁盘设备 + // 排除:loop设备、tmpfs、overlay等 + 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 diskList = new ArrayList<>(); + Set 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 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 diskUsageList = new ArrayList<>(); + Set 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 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/impl/MacOSSSHCommandServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/impl/MacOSSSHCommandServiceImpl.java index 441d9b75..5444ea02 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/impl/MacOSSSHCommandServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/impl/MacOSSSHCommandServiceImpl.java @@ -1,11 +1,19 @@ 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.ssh.AbstractSSHCommandService; import lombok.extern.slf4j.Slf4j; import net.schmizz.sshj.SSHClient; 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层) */ @@ -41,11 +49,114 @@ public class MacOSSSHCommandServiceImpl extends AbstractSSHCommandService { } @Override - public Integer getDiskSize(SSHClient sshClient) { - // 获取根分区磁盘大小(GB) - String command = "df -g / | tail -1 | awk '{print $2}'"; + public List getDiskInfo(SSHClient sshClient) { + // 使用df命令获取所有挂载点的磁盘信息 + // 格式: 设备|挂载点|总大小(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); - return parseInteger(result); + + List diskList = new ArrayList<>(); + Set 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 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 diskUsageList = new ArrayList<>(); + Set 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 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/impl/WindowsSSHCommandServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/impl/WindowsSSHCommandServiceImpl.java index f5e28cea..dc5db353 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/impl/WindowsSSHCommandServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/impl/WindowsSSHCommandServiceImpl.java @@ -1,11 +1,19 @@ 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.ssh.AbstractSSHCommandService; import lombok.extern.slf4j.Slf4j; import net.schmizz.sshj.SSHClient; 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服务器安装OpenSSH Server @@ -42,12 +50,132 @@ public class WindowsSSHCommandServiceImpl extends AbstractSSHCommandService { } @Override - public Integer getDiskSize(SSHClient sshClient) { - // 使用PowerShell获取C盘大小(GB) - String command = "powershell \"(Get-PSDrive C | " + - "Select-Object @{N='Size';E={[math]::Round($_.Free/1GB + $_.Used/1GB,0)}}).Size\""; + public List getDiskInfo(SSHClient sshClient) { + // 使用PowerShell获取所有硬盘分区信息,包含驱动器号、文件系统类型和总容量 + // 输出格式: 驱动器号|文件系统类型|总容量(GB) + 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); - return parseInteger(result); + + List diskList = new ArrayList<>(); + Set 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 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 diskUsageList = new ArrayList<>(); + Set 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 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/websocket/AbstractSSHWebSocketHandler.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/websocket/AbstractSSHWebSocketHandler.java index b4419a3f..e9974728 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/websocket/AbstractSSHWebSocketHandler.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/websocket/AbstractSSHWebSocketHandler.java @@ -16,6 +16,8 @@ import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import net.schmizz.sshj.SSHClient; 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.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; @@ -47,6 +49,8 @@ import java.util.concurrent.Future; @Slf4j public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler { + private static final Logger log = LoggerFactory.getLogger(AbstractSSHWebSocketHandler.class); + // ========== 会话存储 ========== protected final Map webSocketSessions = new ConcurrentHashMap<>(); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/BaseSendNotificationRequest.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/BaseSendNotificationRequest.java index a06f0e4d..37ed81fe 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/BaseSendNotificationRequest.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/BaseSendNotificationRequest.java @@ -26,12 +26,6 @@ import lombok.Data; }) public abstract class BaseSendNotificationRequest { - /** - * 通知渠道ID(必填) - */ - @NotNull(message = "渠道ID不能为空") - private Long channelId; - /** * 消息内容(必填) */ diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/SendNotificationRequest.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/SendNotificationRequest.java index 66adb9c9..1f5a5b9b 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/SendNotificationRequest.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/dto/SendNotificationRequest.java @@ -17,6 +17,10 @@ import java.util.Map; @Schema(description = "发送通知请求") public class SendNotificationRequest { + @Schema(description = "通知渠道ID", required = true, example = "1") + @NotNull(message = "通知渠道ID不能为空") + private Long channelId; + @Schema(description = "通知模板ID", required = true, example = "1") @NotNull(message = "通知模板ID不能为空") private Long notificationTemplateId; @@ -24,9 +28,4 @@ public class SendNotificationRequest { @Schema(description = "模板渲染参数(仅用于模板变量替换)", example = "{\"projectName\":\"测试项目\",\"buildNumber\":\"123\"}") private Map templateParams; - - @Schema(description = "具体的发送请求配置", required = true) - @NotNull(message = "发送请求配置不能为空") - @Valid - private BaseSendNotificationRequest sendRequest; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/service/INotificationSendService.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/service/INotificationSendService.java index e2686584..13ecc863 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/service/INotificationSendService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/service/INotificationSendService.java @@ -1,6 +1,7 @@ package com.qqchen.deploy.backend.notification.service; 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 通知请求 - * @throws com.qqchen.deploy.backend.framework.exception.BusinessException 渠道不存在、渠道已禁用、发送失败 + * @throws com.qqchen.deploy.backend.framework.exception.BusinessException 渠道已禁用、发送失败 */ - void send(BaseSendNotificationRequest request); - - // TODO: 便捷方法需要重新设计,因为现在需要指定具体的请求类型 - // 暂时注释掉,后续根据需要添加 + void send(NotificationChannel channel, BaseSendNotificationRequest request); } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationSendServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationSendServiceImpl.java index de662b9e..d33aa7d6 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationSendServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationSendServiceImpl.java @@ -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.WeworkNotificationConfig; 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.INotificationSendService; import jakarta.annotation.Resource; @@ -36,15 +35,12 @@ import static com.qqchen.deploy.backend.framework.annotation.ServiceType.Type.DA @ServiceType(DATABASE) public class NotificationSendServiceImpl implements INotificationSendService { - @Resource - private INotificationChannelRepository notificationChannelRepository; - @Resource private NotificationChannelAdapterFactory adapterFactory; - public void send(BaseSendNotificationRequest request) { + public void send(NotificationChannel channel, BaseSendNotificationRequest request) { // 1. 参数校验 - if (request == null || request.getChannelId() == null) { + if (channel == null || request == null) { throw new BusinessException(ResponseCode.INVALID_PARAM); } @@ -52,15 +48,12 @@ public class NotificationSendServiceImpl implements INotificationSendService { throw new BusinessException(ResponseCode.INVALID_PARAM); } - // 2. 查询渠道配置 - NotificationChannel channel = notificationChannelRepository.findById(request.getChannelId()).orElseThrow(() -> new BusinessException(ResponseCode.NOTIFICATION_CHANNEL_NOT_FOUND)); - - // 3. 校验渠道状态 + // 2. 校验渠道状态 if (!channel.getEnabled()) { throw new BusinessException(ResponseCode.NOTIFICATION_CHANNEL_DISABLED); } - // 4. 获取对应的适配器 + // 3. 获取对应的适配器 INotificationChannelAdapter adapter; try { adapter = adapterFactory.getAdapter(channel.getChannelType()); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationServiceImpl.java index 472609d6..e22c3d8d 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationServiceImpl.java @@ -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.exception.BusinessException; 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.SendNotificationRequest; import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest; @@ -49,32 +50,30 @@ public class NotificationServiceImpl implements INotificationService { @Override 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()) .orElseThrow(() -> new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_NOT_FOUND)); - // 2. 获取通知渠道 - NotificationChannel channel = notificationChannelRepository.findById(request.getSendRequest().getChannelId()) - .orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND)); - // 3. 验证模板和渠道类型是否匹配 if (!template.getChannelType().equals(channel.getChannelType())) { throw new BusinessException(ResponseCode.INVALID_PARAM); } - // 4. 验证SendRequest渠道类型和模板渠道类型是否匹配 - if (!request.getSendRequest().getChannelType().equals(template.getChannelType())) { - throw new BusinessException(ResponseCode.INVALID_PARAM); - } - - // 5. 验证模板和渠道是否启用 + // 4. 验证模板和渠道是否启用 if (!template.getEnabled()) { throw new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_DISABLED); } if (!channel.getEnabled()) { - throw new BusinessException(ResponseCode.DATA_NOT_FOUND); + throw new BusinessException(ResponseCode.NOTIFICATION_CHANNEL_DISABLED); } + // 5. 根据渠道类型自动创建对应的发送请求对象 + BaseSendNotificationRequest sendRequest = createSendRequestByChannel(channel, template); + // 6. 渲染模板内容 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()); // 8. 设置渲染后的内容和标题 - request.getSendRequest().setContent(content); - request.getSendRequest().setTitle(title); + sendRequest.setContent(content); + sendRequest.setTitle(title); // 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 getEmailReceivers(NotificationTemplate template) { + // 邮件收件人由调用方在 templateParams 中指定,或者在渠道配置中配置 + // 这里返回默认收件人,实际使用时由具体业务场景传递 + return java.util.Arrays.asList("admin@company.com"); } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/NotificationNodeDelegate.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/NotificationNodeDelegate.java index b150606f..9f900e47 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/NotificationNodeDelegate.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/NotificationNodeDelegate.java @@ -1,15 +1,6 @@ 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.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.workflow.dto.inputmapping.NotificationInputMapping; import com.qqchen.deploy.backend.workflow.dto.outputs.NotificationOutputs; @@ -35,12 +26,6 @@ public class NotificationNodeDelegate extends BaseNodeDelegate configs, NotificationInputMapping input) { // 1. 参数校验 @@ -48,80 +33,16 @@ public class NotificationNodeDelegate extends BaseNodeDelegate new RuntimeException("通知渠道不存在: " + input.getChannelId())); - NotificationTemplate template = notificationTemplateRepository.findById(input.getNotificationTemplateId()).orElseThrow(() -> new RuntimeException("通知模板不存在: " + input.getNotificationTemplateId())); - // 3. 构建SendNotificationRequest + // 2. 构建SendNotificationRequest SendNotificationRequest request = new SendNotificationRequest(); + request.setChannelId(input.getChannelId()); request.setNotificationTemplateId(input.getNotificationTemplateId()); request.setTemplateParams(execution.getVariables()); - // 4. 根据渠道类型创建sendRequest,并从模板配置中获取参数 - 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会处理模板渲染和详细配置) + // 3. 发送通知(NotificationService会自动根据渠道类型创建请求对象) notificationService.send(request); 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 getEmailReceivers(DelegateExecution execution, Map configs) { - // 1. 优先从工作流变量中获取 - Object receiversVar = execution.getVariable("emailReceivers"); - if (receiversVar instanceof java.util.List) { - return (java.util.List) receiversVar; - } - - // 2. 从配置中获取 - if (configs != null && configs.containsKey("emailReceivers")) { - Object receiversConfig = configs.get("emailReceivers"); - if (receiversConfig instanceof java.util.List) { - return (java.util.List) receiversConfig; - } - } - - // 3. 使用默认收件人 - return Arrays.asList("admin@company.com"); - } } diff --git a/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql b/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql index 3fc224b6..3f237fcf 100644 --- a/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql +++ b/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql @@ -1140,7 +1140,8 @@ CREATE TABLE deploy_server description VARCHAR(500) NULL COMMENT '服务器描述', cpu_cores INT NULL COMMENT 'CPU核心数', 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格式)', 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) ) 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 '服务器ID(NULL表示全局规则)', + 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 ( diff --git a/backend/src/main/resources/messages.properties b/backend/src/main/resources/messages.properties index 060a3529..49eb758c 100644 --- a/backend/src/main/resources/messages.properties +++ b/backend/src/main/resources/messages.properties @@ -33,8 +33,8 @@ dependency.injection.entitypath.failed=初始化实体 {0} 的EntityPath失败: # -------------------------------------------------------------------------------------- # SSH文件操作相关 (SSH File Operations) - 1200-1299 # -------------------------------------------------------------------------------------- -ssh.file.connection.failed=SSH连接失败 -ssh.file.authentication.failed=SSH认证失败 +ssh.file.connection.failed=SSH连接失败,请检查服务器地址、端口和网络连接 +ssh.file.authentication.failed=SSH认证失败,请检查用户名和密码/密钥是否正确 ssh.file.not.found=文件不存在 ssh.file.directory.not.found=目录不存在 ssh.file.permission.denied=权限不足 diff --git a/backend/src/main/resources/messages_en_US.properties b/backend/src/main/resources/messages_en_US.properties index cd8876c8..ad60f76d 100644 --- a/backend/src/main/resources/messages_en_US.properties +++ b/backend/src/main/resources/messages_en_US.properties @@ -33,8 +33,8 @@ dependency.injection.entitypath.failed=Failed to initialize EntityPath for entit # -------------------------------------------------------------------------------------- # SSH File Operations - 1200-1299 # -------------------------------------------------------------------------------------- -ssh.file.connection.failed=SSH connection failed -ssh.file.authentication.failed=SSH authentication failed +ssh.file.connection.failed=SSH connection failed, please check server address, port and network connection +ssh.file.authentication.failed=SSH authentication failed, please check username and password/key ssh.file.not.found=File not found ssh.file.directory.not.found=Directory not found ssh.file.permission.denied=Permission denied diff --git a/backend/src/main/resources/messages_zh_CN.properties b/backend/src/main/resources/messages_zh_CN.properties index d023ef8e..415e65ee 100644 --- a/backend/src/main/resources/messages_zh_CN.properties +++ b/backend/src/main/resources/messages_zh_CN.properties @@ -33,8 +33,8 @@ dependency.injection.entitypath.failed=获取实体 {0} 的QClass失败 # -------------------------------------------------------------------------------------- # SSH文件操作相关 (SSH File Operations) - 1200-1299 # -------------------------------------------------------------------------------------- -ssh.file.connection.failed=SSH连接失败 -ssh.file.authentication.failed=SSH认证失败 +ssh.file.connection.failed=SSH连接失败,请检查服务器地址、端口和网络连接 +ssh.file.authentication.failed=SSH认证失败,请检查用户名和密码/密钥是否正确 ssh.file.not.found=文件不存在 ssh.file.directory.not.found=目录不存在 ssh.file.permission.denied=权限不足