增加ssh链接框架
This commit is contained in:
parent
ff149be46f
commit
460f237211
@ -0,0 +1,102 @@
|
|||||||
|
package com.qqchen.deploy.backend.deploy.api;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.deploy.dto.ServerAlertRuleDTO;
|
||||||
|
import com.qqchen.deploy.backend.deploy.entity.ServerAlertRule;
|
||||||
|
import com.qqchen.deploy.backend.deploy.query.ServerAlertRuleQuery;
|
||||||
|
import com.qqchen.deploy.backend.deploy.service.IServerAlertRuleService;
|
||||||
|
import com.qqchen.deploy.backend.framework.api.Response;
|
||||||
|
import com.qqchen.deploy.backend.framework.controller.BaseController;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器告警规则 Controller
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/server/alert-rule")
|
||||||
|
@Tag(name = "服务器告警规则管理", description = "服务器告警规则相关接口")
|
||||||
|
public class ServerAlertRuleApiController
|
||||||
|
extends BaseController<ServerAlertRule, ServerAlertRuleDTO, Long, ServerAlertRuleQuery> {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IServerAlertRuleService alertRuleService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Operation(summary = "创建告警规则", description = "创建新的服务器告警规则")
|
||||||
|
@PostMapping
|
||||||
|
public Response<ServerAlertRuleDTO> create(@Validated @RequestBody ServerAlertRuleDTO dto) {
|
||||||
|
return super.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Operation(summary = "更新告警规则", description = "更新指定ID的告警规则")
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public Response<ServerAlertRuleDTO> update(
|
||||||
|
@Parameter(description = "告警规则ID", required = true) @PathVariable Long id,
|
||||||
|
@Validated @RequestBody ServerAlertRuleDTO dto
|
||||||
|
) {
|
||||||
|
return super.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Operation(summary = "删除告警规则", description = "删除指定ID的告警规则(逻辑删除)")
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public Response<Void> delete(
|
||||||
|
@Parameter(description = "告警规则ID", required = true) @PathVariable Long id
|
||||||
|
) {
|
||||||
|
return super.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Operation(summary = "查询告警规则详情", description = "根据ID查询告警规则详情")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public Response<ServerAlertRuleDTO> findById(
|
||||||
|
@Parameter(description = "告警规则ID", required = true) @PathVariable Long id
|
||||||
|
) {
|
||||||
|
return super.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Operation(summary = "查询所有告警规则", description = "查询所有告警规则列表")
|
||||||
|
@GetMapping
|
||||||
|
public Response<List<ServerAlertRuleDTO>> findAll() {
|
||||||
|
return super.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Operation(summary = "分页查询告警规则", description = "根据条件分页查询告警规则")
|
||||||
|
@GetMapping("/page")
|
||||||
|
public Response<Page<ServerAlertRuleDTO>> page(ServerAlertRuleQuery query) {
|
||||||
|
return super.page(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Operation(summary = "条件查询告警规则列表", description = "根据条件查询告警规则列表")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Response<List<ServerAlertRuleDTO>> findAll(ServerAlertRuleQuery query) {
|
||||||
|
return super.findAll(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Operation(summary = "批量处理告警规则", description = "批量创建/更新/删除告警规则")
|
||||||
|
@PostMapping("/batch")
|
||||||
|
public CompletableFuture<Response<Void>> batchProcess(@RequestBody List<ServerAlertRuleDTO> dtos) {
|
||||||
|
return super.batchProcess(dtos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void exportData(HttpServletResponse response, List<ServerAlertRuleDTO> data) {
|
||||||
|
log.info("导出告警规则数据,数据量:{}", data.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.qqchen.deploy.backend.deploy.converter;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.deploy.dto.ServerAlertRuleDTO;
|
||||||
|
import com.qqchen.deploy.backend.deploy.entity.ServerAlertRule;
|
||||||
|
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器告警规则转换器
|
||||||
|
*/
|
||||||
|
@Mapper(config = BaseConverter.class)
|
||||||
|
public interface ServerAlertRuleConverter extends BaseConverter<ServerAlertRule, ServerAlertRuleDTO> {
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package com.qqchen.deploy.backend.deploy.dto;
|
package com.qqchen.deploy.backend.deploy.dto;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
|
||||||
import com.qqchen.deploy.backend.framework.enums.AuthTypeEnum;
|
import com.qqchen.deploy.backend.framework.enums.AuthTypeEnum;
|
||||||
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
|
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
|
||||||
import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum;
|
import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum;
|
||||||
@ -7,6 +8,7 @@ import com.qqchen.deploy.backend.framework.dto.BaseDTO;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务器 DTO
|
* 服务器 DTO
|
||||||
@ -96,10 +98,15 @@ public class ServerDTO extends BaseDTO {
|
|||||||
private Integer memorySize;
|
private Integer memorySize;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 磁盘大小(GB)
|
* 磁盘总容量(GB)
|
||||||
*/
|
*/
|
||||||
private Integer diskSize;
|
private Integer diskSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 磁盘详细信息列表
|
||||||
|
*/
|
||||||
|
private List<DiskInfo> diskInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 标签(JSON格式)
|
* 标签(JSON格式)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
package com.qqchen.deploy.backend.deploy.dto;
|
package com.qqchen.deploy.backend.deploy.dto;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务器信息DTO
|
* 服务器信息DTO
|
||||||
@ -56,7 +58,12 @@ public class ServerInfoDTO {
|
|||||||
private Integer memorySize;
|
private Integer memorySize;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 磁盘大小(GB)
|
* 磁盘总容量(GB)
|
||||||
*/
|
*/
|
||||||
private Integer diskSize;
|
private Integer diskSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 磁盘详细信息列表
|
||||||
|
*/
|
||||||
|
private List<DiskInfo> diskInfo;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
package com.qqchen.deploy.backend.deploy.dto;
|
package com.qqchen.deploy.backend.deploy.dto;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
|
||||||
import jakarta.validation.constraints.Min;
|
import jakarta.validation.constraints.Min;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务器初始化 DTO
|
* 服务器初始化 DTO
|
||||||
@ -27,12 +30,18 @@ public class ServerInitializeDTO {
|
|||||||
private Integer memorySize;
|
private Integer memorySize;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 磁盘大小(GB)
|
* 磁盘总容量(GB)
|
||||||
*/
|
*/
|
||||||
@NotNull(message = "磁盘大小不能为空")
|
@NotNull(message = "磁盘大小不能为空")
|
||||||
@Min(value = 1, message = "磁盘大小至少为1GB")
|
@Min(value = 1, message = "磁盘大小至少为1GB")
|
||||||
private Integer diskSize;
|
private Integer diskSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 磁盘详细信息列表
|
||||||
|
*/
|
||||||
|
@NotEmpty(message = "磁盘信息不能为空")
|
||||||
|
private List<DiskInfo> diskInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 操作系统版本
|
* 操作系统版本
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.qqchen.deploy.backend.deploy.dto;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskUsageInfo;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器监控数据DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "服务器监控数据")
|
||||||
|
public class ServerMonitorDataDTO {
|
||||||
|
|
||||||
|
@Schema(description = "服务器ID")
|
||||||
|
private Long serverId;
|
||||||
|
|
||||||
|
@Schema(description = "CPU使用率(%)")
|
||||||
|
private BigDecimal cpuUsage;
|
||||||
|
|
||||||
|
@Schema(description = "内存使用率(%)")
|
||||||
|
private BigDecimal memoryUsage;
|
||||||
|
|
||||||
|
@Schema(description = "已用内存(GB)")
|
||||||
|
private Integer memoryUsed;
|
||||||
|
|
||||||
|
@Schema(description = "各分区磁盘使用率")
|
||||||
|
private List<DiskUsageInfo> diskUsage;
|
||||||
|
|
||||||
|
@Schema(description = "采集时间")
|
||||||
|
private LocalDateTime collectTime;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
package com.qqchen.deploy.backend.deploy.entity;
|
package com.qqchen.deploy.backend.deploy.entity;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
|
||||||
|
import com.qqchen.deploy.backend.framework.converter.DiskInfoListConverter;
|
||||||
import com.qqchen.deploy.backend.framework.enums.AuthTypeEnum;
|
import com.qqchen.deploy.backend.framework.enums.AuthTypeEnum;
|
||||||
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
|
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
|
||||||
import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum;
|
import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum;
|
||||||
@ -8,6 +10,7 @@ import jakarta.persistence.*;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务器实体
|
* 服务器实体
|
||||||
@ -121,11 +124,20 @@ public class Server extends Entity<Long> {
|
|||||||
private Integer memorySize;
|
private Integer memorySize;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 磁盘大小(GB)
|
* 磁盘总容量(GB)
|
||||||
|
* 所有磁盘分区容量的总和,方便排序和统计
|
||||||
*/
|
*/
|
||||||
@Column(name = "disk_size")
|
@Column(name = "disk_size")
|
||||||
private Integer diskSize;
|
private Integer diskSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 磁盘详细信息列表
|
||||||
|
* 存储多个磁盘的详细信息,包括挂载点、容量
|
||||||
|
*/
|
||||||
|
@Convert(converter = DiskInfoListConverter.class)
|
||||||
|
@Column(name = "disk_info", columnDefinition = "JSON")
|
||||||
|
private List<DiskInfo> diskInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 标签(JSON格式)
|
* 标签(JSON格式)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
package com.qqchen.deploy.backend.deploy.entity;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.deploy.enums.AlertTypeEnum;
|
||||||
|
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
|
||||||
|
import com.qqchen.deploy.backend.framework.domain.Entity;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器告警规则实体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@jakarta.persistence.Entity
|
||||||
|
@Table(name = "deploy_server_alert_rule")
|
||||||
|
@LogicDelete(true)
|
||||||
|
public class ServerAlertRule extends Entity<Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package com.qqchen.deploy.backend.deploy.repository;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.deploy.entity.ServerAlertLog;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器告警记录Repository
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface IServerAlertLogRepository extends JpaRepository<ServerAlertLog, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定服务器的活跃告警
|
||||||
|
*/
|
||||||
|
List<ServerAlertLog> findByServerIdAndStatus(Long serverId, String status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定服务器在指定时间范围内的告警记录
|
||||||
|
*/
|
||||||
|
List<ServerAlertLog> findByServerIdAndAlertTimeBetweenOrderByAlertTimeDesc(
|
||||||
|
Long serverId, LocalDateTime startTime, LocalDateTime endTime);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.qqchen.deploy.backend.deploy.repository;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.deploy.entity.ServerAlertRule;
|
||||||
|
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器告警规则Repository
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface IServerAlertRuleRepository extends IBaseRepository<ServerAlertRule, Long> {
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package com.qqchen.deploy.backend.deploy.repository;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.deploy.entity.ServerMonitor;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器监控记录Repository
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface IServerMonitorRepository extends JpaRepository<ServerMonitor, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定服务器在指定时间范围内的监控记录
|
||||||
|
*/
|
||||||
|
List<ServerMonitor> findByServerIdAndCollectTimeBetweenOrderByCollectTimeDesc(
|
||||||
|
Long serverId, LocalDateTime startTime, LocalDateTime endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定时间之前的监控记录
|
||||||
|
*/
|
||||||
|
@Modifying
|
||||||
|
@Query("DELETE FROM ServerMonitor m WHERE m.collectTime < :beforeTime")
|
||||||
|
int deleteByCollectTimeBefore(@Param("beforeTime") LocalDateTime beforeTime);
|
||||||
|
}
|
||||||
@ -1,9 +1,12 @@
|
|||||||
package com.qqchen.deploy.backend.deploy.repository;
|
package com.qqchen.deploy.backend.deploy.repository;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.deploy.entity.Server;
|
import com.qqchen.deploy.backend.deploy.entity.Server;
|
||||||
|
import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum;
|
||||||
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
|
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务器仓储接口
|
* 服务器仓储接口
|
||||||
*/
|
*/
|
||||||
@ -25,5 +28,14 @@ public interface IServerRepository extends IBaseRepository<Server, Long> {
|
|||||||
* @return 是否存在
|
* @return 是否存在
|
||||||
*/
|
*/
|
||||||
boolean existsByHostIpAndDeletedFalse(String hostIp);
|
boolean existsByHostIpAndDeletedFalse(String hostIp);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据状态和删除标记查询服务器列表
|
||||||
|
*
|
||||||
|
* @param status 服务器状态
|
||||||
|
* @param deleted 是否删除
|
||||||
|
* @return 服务器列表
|
||||||
|
*/
|
||||||
|
List<Server> findByStatusAndDeleted(ServerStatusEnum status, boolean deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,267 @@
|
|||||||
|
package com.qqchen.deploy.backend.deploy.scheduler;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorDataDTO;
|
||||||
|
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorNotificationConfig;
|
||||||
|
import com.qqchen.deploy.backend.deploy.entity.Server;
|
||||||
|
import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum;
|
||||||
|
import com.qqchen.deploy.backend.deploy.repository.IServerRepository;
|
||||||
|
import com.qqchen.deploy.backend.deploy.service.IServerAlertService;
|
||||||
|
import com.qqchen.deploy.backend.deploy.service.IServerMonitorService;
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskUsageInfo;
|
||||||
|
import com.qqchen.deploy.backend.framework.ssh.ISSHCommandService;
|
||||||
|
import com.qqchen.deploy.backend.framework.ssh.SSHCommandServiceFactory;
|
||||||
|
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
|
||||||
|
import com.qqchen.deploy.backend.notification.service.INotificationService;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.schmizz.sshj.SSHClient;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器监控定时任务
|
||||||
|
* 由定时任务管理系统调用,不使用@Scheduled注解
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class ServerMonitorScheduler {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IServerRepository serverRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SSHCommandServiceFactory sshCommandServiceFactory;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IServerMonitorService monitorService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IServerAlertService alertService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private INotificationService notificationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 采集所有在线服务器的监控数据
|
||||||
|
* 此方法由定时任务管理系统调用
|
||||||
|
*
|
||||||
|
* @param config 通知配置(可选,为null则只记录数据不发通知)
|
||||||
|
*/
|
||||||
|
public void collectServerMetrics(ServerMonitorNotificationConfig config) {
|
||||||
|
if (config != null) {
|
||||||
|
log.info("========== 开始采集服务器监控数据 ========== channelId={}, offlineTemplateId={}, alertTemplateId={}",
|
||||||
|
config.getNotificationChannelId(), config.getServerOfflineTemplateId(), config.getResourceAlertTemplateId());
|
||||||
|
} else {
|
||||||
|
log.info("========== 开始采集服务器监控数据(不发送通知) ==========");
|
||||||
|
}
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 查询所有应该在线的服务器(状态为ONLINE)
|
||||||
|
List<Server> shouldBeOnlineServers = serverRepository
|
||||||
|
.findByStatusAndDeleted(ServerStatusEnum.ONLINE, false);
|
||||||
|
|
||||||
|
if (shouldBeOnlineServers.isEmpty()) {
|
||||||
|
log.debug("没有需要监控的服务器,跳过监控采集");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("发现 {} 台应在线服务器,开始检测状态", shouldBeOnlineServers.size());
|
||||||
|
|
||||||
|
// 2. 并发检测服务器连接状态并采集监控数据
|
||||||
|
final ServerMonitorNotificationConfig finalConfig = config;
|
||||||
|
List<CompletableFuture<ServerMonitorDataDTO>> futures = shouldBeOnlineServers.stream()
|
||||||
|
.map(server -> CompletableFuture.supplyAsync(() ->
|
||||||
|
collectSingleServerWithStatusCheck(server, finalConfig)))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 3. 等待所有任务完成
|
||||||
|
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
|
||||||
|
futures.toArray(new CompletableFuture[0])
|
||||||
|
);
|
||||||
|
allFutures.join();
|
||||||
|
|
||||||
|
// 4. 收集结果
|
||||||
|
List<ServerMonitorDataDTO> monitorDataList = futures.stream()
|
||||||
|
.map(CompletableFuture::join)
|
||||||
|
.filter(data -> data != null)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
log.info("========== 监控数据采集完成: 成功={}/{}, 耗时={}ms ==========",
|
||||||
|
monitorDataList.size(), shouldBeOnlineServers.size(), duration);
|
||||||
|
|
||||||
|
// 5. 批量保存监控数据到数据库
|
||||||
|
if (!monitorDataList.isEmpty()) {
|
||||||
|
monitorService.batchSaveMonitorData(monitorDataList);
|
||||||
|
log.info("监控数据已保存到数据库: count={}", monitorDataList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 检查告警规则
|
||||||
|
for (ServerMonitorDataDTO data : monitorDataList) {
|
||||||
|
try {
|
||||||
|
alertService.checkAlertRules(data.getServerId(), data, config);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("检查告警规则失败: serverId={}", data.getServerId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("服务器监控数据采集失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测服务器连接状态并采集监控数据
|
||||||
|
*/
|
||||||
|
private ServerMonitorDataDTO collectSingleServerWithStatusCheck(Server server, ServerMonitorNotificationConfig config) {
|
||||||
|
try {
|
||||||
|
// 尝试采集监控数据
|
||||||
|
return collectSingleServer(server);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 采集失败,说明服务器无法连接(离线)
|
||||||
|
log.error("服务器连接失败(离线): serverId={}, name={}, ip={}, error={}",
|
||||||
|
server.getId(), server.getServerName(), server.getHostIp(), e.getMessage());
|
||||||
|
|
||||||
|
// 发送离线通知
|
||||||
|
if (config != null && config.getNotificationChannelId() != null && config.getServerOfflineTemplateId() != null) {
|
||||||
|
try {
|
||||||
|
sendServerOfflineNotification(server, config);
|
||||||
|
} catch (Exception notifyError) {
|
||||||
|
log.error("发送服务器离线通知失败: serverId={}", server.getId(), notifyError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送服务器离线通知
|
||||||
|
*/
|
||||||
|
private void sendServerOfflineNotification(Server server, ServerMonitorNotificationConfig config) {
|
||||||
|
try {
|
||||||
|
// 1. 构建模板参数
|
||||||
|
Map<String, Object> templateParams = new HashMap<>();
|
||||||
|
templateParams.put("serverName", server.getServerName());
|
||||||
|
templateParams.put("serverIp", server.getHostIp());
|
||||||
|
templateParams.put("offlineTime", LocalDateTime.now().toString());
|
||||||
|
|
||||||
|
// 2. 构建SendNotificationRequest
|
||||||
|
SendNotificationRequest request = new SendNotificationRequest();
|
||||||
|
request.setChannelId(config.getNotificationChannelId());
|
||||||
|
request.setNotificationTemplateId(config.getServerOfflineTemplateId());
|
||||||
|
request.setTemplateParams(templateParams);
|
||||||
|
|
||||||
|
// 3. 发送通知(NotificationService会自动根据渠道类型创建请求对象)
|
||||||
|
notificationService.send(request);
|
||||||
|
|
||||||
|
log.info("✅ 服务器离线通知已发送: serverId={}, serverName={}, ip={}",
|
||||||
|
server.getId(), server.getServerName(), server.getHostIp());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("发送服务器离线通知异常: serverId={}", server.getId(), e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 采集单台服务器的监控数据
|
||||||
|
*/
|
||||||
|
private ServerMonitorDataDTO collectSingleServer(Server server) {
|
||||||
|
SSHClient sshClient = null;
|
||||||
|
ISSHCommandService sshService = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 获取对应OS的SSH服务
|
||||||
|
sshService = sshCommandServiceFactory.getService(server.getOsType());
|
||||||
|
|
||||||
|
// 2. 创建SSH连接
|
||||||
|
String password = null;
|
||||||
|
String privateKey = null;
|
||||||
|
String passphrase = null;
|
||||||
|
|
||||||
|
switch (server.getAuthType()) {
|
||||||
|
case PASSWORD:
|
||||||
|
password = server.getSshPassword();
|
||||||
|
break;
|
||||||
|
case KEY:
|
||||||
|
privateKey = server.getSshPrivateKey();
|
||||||
|
passphrase = server.getSshPassphrase();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
sshClient = sshService.createConnection(
|
||||||
|
server.getHostIp(),
|
||||||
|
server.getSshPort(),
|
||||||
|
server.getSshUser(),
|
||||||
|
password,
|
||||||
|
privateKey,
|
||||||
|
passphrase
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. 采集监控数据
|
||||||
|
BigDecimal cpuUsage = sshService.getCpuUsage(sshClient);
|
||||||
|
BigDecimal memoryUsage = sshService.getMemoryUsage(sshClient);
|
||||||
|
List<DiskUsageInfo> diskUsage = sshService.getDiskUsage(sshClient);
|
||||||
|
|
||||||
|
// 4. 计算已用内存(基于内存使用率和总内存)
|
||||||
|
Integer memoryUsed = null;
|
||||||
|
if (memoryUsage != null && server.getMemorySize() != null) {
|
||||||
|
memoryUsed = memoryUsage.multiply(new BigDecimal(server.getMemorySize()))
|
||||||
|
.divide(new BigDecimal(100), 0, BigDecimal.ROUND_HALF_UP)
|
||||||
|
.intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 构建监控数据
|
||||||
|
ServerMonitorDataDTO data = ServerMonitorDataDTO.builder()
|
||||||
|
.serverId(server.getId())
|
||||||
|
.cpuUsage(cpuUsage)
|
||||||
|
.memoryUsage(memoryUsage)
|
||||||
|
.memoryUsed(memoryUsed)
|
||||||
|
.diskUsage(diskUsage)
|
||||||
|
.collectTime(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
log.debug("服务器监控数据采集成功: serverId={}, cpu={}%, mem={}%, diskCount={}",
|
||||||
|
server.getId(), cpuUsage, memoryUsage,
|
||||||
|
diskUsage != null ? diskUsage.size() : 0);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("采集服务器监控数据失败: serverId={}, serverName={}, error={}",
|
||||||
|
server.getId(), server.getServerName(), e.getMessage());
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
// 6. 关闭SSH连接
|
||||||
|
if (sshService != null && sshClient != null) {
|
||||||
|
sshService.closeConnection(sshClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理历史监控数据
|
||||||
|
* 此方法由定时任务管理系统调用,建议每天凌晨执行
|
||||||
|
*/
|
||||||
|
public void cleanOldMonitorData() {
|
||||||
|
log.info("========== 开始清理历史监控数据 ==========");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 删除30天前的数据
|
||||||
|
LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30);
|
||||||
|
int deletedCount = monitorService.deleteOldData(thirtyDaysAgo);
|
||||||
|
|
||||||
|
log.info("========== 历史监控数据清理完成: count={} ==========", deletedCount);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("清理历史监控数据失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.qqchen.deploy.backend.deploy.service;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.deploy.dto.ServerAlertRuleDTO;
|
||||||
|
import com.qqchen.deploy.backend.deploy.entity.ServerAlertRule;
|
||||||
|
import com.qqchen.deploy.backend.deploy.query.ServerAlertRuleQuery;
|
||||||
|
import com.qqchen.deploy.backend.framework.service.IBaseService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器告警规则服务接口
|
||||||
|
*/
|
||||||
|
public interface IServerAlertRuleService extends IBaseService<ServerAlertRule, ServerAlertRuleDTO, ServerAlertRuleQuery, Long> {
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.qqchen.deploy.backend.deploy.service;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorDataDTO;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器监控服务接口
|
||||||
|
*/
|
||||||
|
public interface IServerMonitorService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量保存监控数据
|
||||||
|
*/
|
||||||
|
void batchSaveMonitorData(List<ServerMonitorDataDTO> dataList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定时间之前的历史数据
|
||||||
|
*/
|
||||||
|
int deleteOldData(LocalDateTime beforeTime);
|
||||||
|
}
|
||||||
@ -16,18 +16,13 @@ import com.qqchen.deploy.backend.deploy.integration.IJenkinsServiceIntegration;
|
|||||||
import com.qqchen.deploy.backend.deploy.repository.*;
|
import com.qqchen.deploy.backend.deploy.repository.*;
|
||||||
import com.qqchen.deploy.backend.deploy.service.IJenkinsBuildService;
|
import com.qqchen.deploy.backend.deploy.service.IJenkinsBuildService;
|
||||||
import com.qqchen.deploy.backend.deploy.service.IJenkinsSyncHistoryService;
|
import com.qqchen.deploy.backend.deploy.service.IJenkinsSyncHistoryService;
|
||||||
import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest;
|
|
||||||
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
|
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
|
||||||
import com.qqchen.deploy.backend.notification.entity.NotificationTemplate;
|
import com.qqchen.deploy.backend.notification.entity.NotificationTemplate;
|
||||||
import com.qqchen.deploy.backend.notification.entity.config.WeworkTemplateConfig;
|
|
||||||
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
|
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
|
||||||
import com.qqchen.deploy.backend.notification.repository.INotificationTemplateRepository;
|
import com.qqchen.deploy.backend.notification.repository.INotificationTemplateRepository;
|
||||||
import com.qqchen.deploy.backend.notification.service.INotificationService;
|
import com.qqchen.deploy.backend.notification.service.INotificationService;
|
||||||
import com.qqchen.deploy.backend.notification.service.INotificationSendService;
|
import com.qqchen.deploy.backend.notification.service.INotificationSendService;
|
||||||
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
|
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
|
||||||
import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
|
|
||||||
import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest;
|
|
||||||
import com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum;
|
|
||||||
import com.qqchen.deploy.backend.deploy.dto.sync.JenkinsSyncContext;
|
import com.qqchen.deploy.backend.deploy.dto.sync.JenkinsSyncContext;
|
||||||
import com.qqchen.deploy.backend.deploy.lock.SyncLockManager;
|
import com.qqchen.deploy.backend.deploy.lock.SyncLockManager;
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
@ -763,21 +758,15 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
// 从 actions JSON 中解析 commit 信息
|
// 从 actions JSON 中解析 commit 信息
|
||||||
parseCommitInfoFromActions(build.getActions(), templateParams);
|
parseCommitInfoFromActions(build.getActions(), templateParams);
|
||||||
|
|
||||||
// 4. 校验模板和渠道类型是否匹配
|
// 4. 构建 SendNotificationRequest
|
||||||
if (!template.getChannelType().equals(channel.getChannelType())) {
|
|
||||||
log.warn("模板渠道类型({})与通知渠道类型({})不匹配,跳过通知: templateId={}, channelId={}", template.getChannelType(), channel.getChannelType(), config.getBuildNotificationTemplateId(), channel.getId());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 构建 SendNotificationRequest
|
|
||||||
SendNotificationRequest request = new SendNotificationRequest();
|
SendNotificationRequest request = new SendNotificationRequest();
|
||||||
|
request.setChannelId(channel.getId());
|
||||||
request.setNotificationTemplateId(config.getBuildNotificationTemplateId());
|
request.setNotificationTemplateId(config.getBuildNotificationTemplateId());
|
||||||
request.setTemplateParams(templateParams);
|
request.setTemplateParams(templateParams);
|
||||||
request.setSendRequest(createSendRequestByChannel(channel, template));
|
|
||||||
|
|
||||||
log.debug("准备发送构建通知: job={}, build={}, templateId={}, channelId={}, channelType={}", job.getJobName(), build.getBuildNumber(), config.getBuildNotificationTemplateId(), channel.getId(), channel.getChannelType());
|
log.debug("准备发送构建通知: job={}, build={}, templateId={}, channelId={}", job.getJobName(), build.getBuildNumber(), config.getBuildNotificationTemplateId(), channel.getId());
|
||||||
|
|
||||||
// 6. 发送通知
|
// 5. 发送通知(NotificationService会自动根据渠道类型创建请求对象)
|
||||||
notificationService.send(request);
|
notificationService.send(request);
|
||||||
|
|
||||||
log.info("已发送构建通知: job={}, build={}, status={}, templateId={}", job.getJobName(), build.getBuildNumber(), status, config.getBuildNotificationTemplateId());
|
log.info("已发送构建通知: job={}, build={}, status={}, templateId={}", job.getJobName(), build.getBuildNumber(), status, config.getBuildNotificationTemplateId());
|
||||||
@ -792,44 +781,6 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据渠道类型创建对应的发送请求
|
|
||||||
*/
|
|
||||||
private BaseSendNotificationRequest createSendRequestByChannel(
|
|
||||||
NotificationChannel channel, NotificationTemplate template) {
|
|
||||||
switch (channel.getChannelType()) {
|
|
||||||
case WEWORK:
|
|
||||||
WeworkSendNotificationRequest weworkRequest = new WeworkSendNotificationRequest();
|
|
||||||
weworkRequest.setChannelId(channel.getId());
|
|
||||||
weworkRequest.setMessageType(getWeworkMessageType(template));
|
|
||||||
return weworkRequest;
|
|
||||||
case EMAIL:
|
|
||||||
EmailSendNotificationRequest emailRequest = new EmailSendNotificationRequest();
|
|
||||||
emailRequest.setChannelId(channel.getId());
|
|
||||||
emailRequest.setToReceivers(Arrays.asList("admin@company.com"));
|
|
||||||
return emailRequest;
|
|
||||||
default:
|
|
||||||
throw new RuntimeException("不支持的渠道类型: " + channel.getChannelType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从模板配置中获取企业微信消息类型
|
|
||||||
*/
|
|
||||||
private WeworkMessageTypeEnum getWeworkMessageType(NotificationTemplate template) {
|
|
||||||
try {
|
|
||||||
if (template.getTemplateConfig() != null) {
|
|
||||||
WeworkTemplateConfig weworkConfig = JsonUtils.fromMap(template.getTemplateConfig(), WeworkTemplateConfig.class);
|
|
||||||
if (weworkConfig != null && weworkConfig.getMessageType() != null) {
|
|
||||||
return weworkConfig.getMessageType();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("解析企业微信模板配置失败,使用默认消息类型: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
return WeworkMessageTypeEnum.TEXT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从 changeSets JSON 中解析 commit 信息
|
* 从 changeSets JSON 中解析 commit 信息
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
package com.qqchen.deploy.backend.deploy.service.impl;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.deploy.dto.ServerAlertRuleDTO;
|
||||||
|
import com.qqchen.deploy.backend.deploy.entity.ServerAlertRule;
|
||||||
|
import com.qqchen.deploy.backend.deploy.query.ServerAlertRuleQuery;
|
||||||
|
import com.qqchen.deploy.backend.deploy.repository.IServerAlertRuleRepository;
|
||||||
|
import com.qqchen.deploy.backend.deploy.service.IServerAlertRuleService;
|
||||||
|
import com.qqchen.deploy.backend.framework.annotation.ServiceType;
|
||||||
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
|
import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
||||||
|
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器告警规则服务实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@ServiceType(ServiceType.Type.DATABASE)
|
||||||
|
public class ServerAlertRuleServiceImpl
|
||||||
|
extends BaseServiceImpl<ServerAlertRule, ServerAlertRuleDTO, ServerAlertRuleQuery, Long>
|
||||||
|
implements IServerAlertRuleService {
|
||||||
|
|
||||||
|
private final IServerAlertRuleRepository alertRuleRepository;
|
||||||
|
|
||||||
|
public ServerAlertRuleServiceImpl(IServerAlertRuleRepository alertRuleRepository) {
|
||||||
|
this.alertRuleRepository = alertRuleRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void validateUniqueConstraints(ServerAlertRuleDTO dto) {
|
||||||
|
// 可以添加唯一性校验,比如同一个服务器的同一类型告警只能有一条规则
|
||||||
|
// 这里暂不添加约束
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServerAlertRuleDTO create(ServerAlertRuleDTO dto) {
|
||||||
|
// 校验阈值: 严重阈值必须大于警告阈值
|
||||||
|
if (dto.getCriticalThreshold().compareTo(dto.getWarningThreshold()) <= 0) {
|
||||||
|
throw new BusinessException(ResponseCode.INVALID_PARAM,
|
||||||
|
new Object[]{"严重阈值必须大于警告阈值"});
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServerAlertRuleDTO update(Long id, ServerAlertRuleDTO dto) {
|
||||||
|
// 校验阈值
|
||||||
|
if (dto.getCriticalThreshold().compareTo(dto.getWarningThreshold()) <= 0) {
|
||||||
|
throw new BusinessException(ResponseCode.INVALID_PARAM,
|
||||||
|
new Object[]{"严重阈值必须大于警告阈值"});
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.update(id, dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,242 @@
|
|||||||
|
package com.qqchen.deploy.backend.deploy.service.impl;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorDataDTO;
|
||||||
|
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorNotificationConfig;
|
||||||
|
import com.qqchen.deploy.backend.deploy.entity.Server;
|
||||||
|
import com.qqchen.deploy.backend.deploy.entity.ServerAlertLog;
|
||||||
|
import com.qqchen.deploy.backend.deploy.entity.ServerAlertRule;
|
||||||
|
import com.qqchen.deploy.backend.deploy.enums.AlertLevelEnum;
|
||||||
|
import com.qqchen.deploy.backend.deploy.enums.AlertTypeEnum;
|
||||||
|
import com.qqchen.deploy.backend.deploy.repository.IServerAlertLogRepository;
|
||||||
|
import com.qqchen.deploy.backend.deploy.repository.IServerAlertRuleRepository;
|
||||||
|
import com.qqchen.deploy.backend.deploy.repository.IServerRepository;
|
||||||
|
import com.qqchen.deploy.backend.deploy.service.IServerAlertService;
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskUsageInfo;
|
||||||
|
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
|
||||||
|
import com.qqchen.deploy.backend.notification.service.INotificationService;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器告警服务实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class ServerAlertServiceImpl implements IServerAlertService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IServerAlertRuleRepository alertRuleRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IServerAlertLogRepository alertLogRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IServerRepository serverRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private INotificationService notificationService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkAlertRules(Long serverId, ServerMonitorDataDTO monitorData,
|
||||||
|
ServerMonitorNotificationConfig config) {
|
||||||
|
if (monitorData == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询该服务器的所有启用的告警规则(包括全局规则)
|
||||||
|
List<ServerAlertRule> rules = alertRuleRepository.findAll();
|
||||||
|
|
||||||
|
for (ServerAlertRule rule : rules) {
|
||||||
|
// 过滤:只检查全局规则或匹配的服务器规则
|
||||||
|
if (rule.getServerId() != null && !rule.getServerId().equals(serverId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只检查启用的规则
|
||||||
|
if (!Boolean.TRUE.equals(rule.getEnabled())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据告警类型检查
|
||||||
|
checkSingleRule(serverId, monitorData, rule, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查单个告警规则
|
||||||
|
*/
|
||||||
|
private void checkSingleRule(Long serverId, ServerMonitorDataDTO monitorData, ServerAlertRule rule,
|
||||||
|
ServerMonitorNotificationConfig config) {
|
||||||
|
BigDecimal currentValue = null;
|
||||||
|
String resourceInfo = "";
|
||||||
|
|
||||||
|
// 根据告警类型获取当前值
|
||||||
|
switch (rule.getAlertType()) {
|
||||||
|
case CPU:
|
||||||
|
currentValue = monitorData.getCpuUsage();
|
||||||
|
resourceInfo = "CPU";
|
||||||
|
break;
|
||||||
|
case MEMORY:
|
||||||
|
currentValue = monitorData.getMemoryUsage();
|
||||||
|
resourceInfo = "内存";
|
||||||
|
break;
|
||||||
|
case DISK:
|
||||||
|
// 磁盘告警:检查所有分区,任意分区超过阈值就告警
|
||||||
|
checkDiskAlert(serverId, monitorData, rule, config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentValue == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断告警级别
|
||||||
|
AlertLevelEnum alertLevel = null;
|
||||||
|
BigDecimal threshold = null;
|
||||||
|
|
||||||
|
if (currentValue.compareTo(rule.getCriticalThreshold()) >= 0) {
|
||||||
|
alertLevel = AlertLevelEnum.CRITICAL;
|
||||||
|
threshold = rule.getCriticalThreshold();
|
||||||
|
} else if (currentValue.compareTo(rule.getWarningThreshold()) >= 0) {
|
||||||
|
alertLevel = AlertLevelEnum.WARNING;
|
||||||
|
threshold = rule.getWarningThreshold();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发告警
|
||||||
|
if (alertLevel != null) {
|
||||||
|
triggerAlert(serverId, rule, alertLevel, currentValue, threshold, resourceInfo, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查磁盘告警
|
||||||
|
*/
|
||||||
|
private void checkDiskAlert(Long serverId, ServerMonitorDataDTO monitorData, ServerAlertRule rule,
|
||||||
|
ServerMonitorNotificationConfig config) {
|
||||||
|
List<DiskUsageInfo> diskUsageList = monitorData.getDiskUsage();
|
||||||
|
if (diskUsageList == null || diskUsageList.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DiskUsageInfo diskUsage : diskUsageList) {
|
||||||
|
BigDecimal usagePercent = diskUsage.getUsagePercent();
|
||||||
|
if (usagePercent == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertLevelEnum alertLevel = null;
|
||||||
|
BigDecimal threshold = null;
|
||||||
|
|
||||||
|
if (usagePercent.compareTo(rule.getCriticalThreshold()) >= 0) {
|
||||||
|
alertLevel = AlertLevelEnum.CRITICAL;
|
||||||
|
threshold = rule.getCriticalThreshold();
|
||||||
|
} else if (usagePercent.compareTo(rule.getWarningThreshold()) >= 0) {
|
||||||
|
alertLevel = AlertLevelEnum.WARNING;
|
||||||
|
threshold = rule.getWarningThreshold();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alertLevel != null) {
|
||||||
|
String resourceInfo = "磁盘[" + diskUsage.getMountPoint() + "]";
|
||||||
|
triggerAlert(serverId, rule, alertLevel, usagePercent, threshold, resourceInfo, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发告警
|
||||||
|
*/
|
||||||
|
private void triggerAlert(Long serverId, ServerAlertRule rule, AlertLevelEnum level,
|
||||||
|
BigDecimal currentValue, BigDecimal threshold, String resourceInfo,
|
||||||
|
ServerMonitorNotificationConfig config) {
|
||||||
|
// 1. 记录告警日志到数据库
|
||||||
|
String alertMessage = String.format("%s使用率达到%s级别: 当前值=%.2f%%, 阈值=%.2f%%",
|
||||||
|
resourceInfo, level.getDescription(), currentValue, threshold);
|
||||||
|
|
||||||
|
ServerAlertLog alertLog = ServerAlertLog.builder()
|
||||||
|
.serverId(serverId)
|
||||||
|
.ruleId(rule.getId())
|
||||||
|
.alertType(rule.getAlertType())
|
||||||
|
.alertLevel(level)
|
||||||
|
.alertValue(currentValue)
|
||||||
|
.thresholdValue(threshold)
|
||||||
|
.alertMessage(alertMessage)
|
||||||
|
.status("ACTIVE")
|
||||||
|
.alertTime(LocalDateTime.now())
|
||||||
|
.notified(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
alertLogRepository.save(alertLog);
|
||||||
|
log.info("✅ 告警记录已保存: id={}, serverId={}, message={}",
|
||||||
|
alertLog.getId(), serverId, alertMessage);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("保存告警记录失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 记录日志
|
||||||
|
log.warn("⚠️ 服务器告警触发: serverId={}, ruleName={}, type={}, level={}, resource={}, " +
|
||||||
|
"current={}%, threshold={}%",
|
||||||
|
serverId, rule.getRuleName(), rule.getAlertType(), level,
|
||||||
|
resourceInfo, currentValue, threshold);
|
||||||
|
|
||||||
|
// 3. 发送告警通知
|
||||||
|
if (config != null && config.getNotificationChannelId() != null && config.getResourceAlertTemplateId() != null) {
|
||||||
|
try {
|
||||||
|
sendAlertNotification(serverId, rule, level, currentValue, threshold, resourceInfo, config);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("发送告警通知失败: serverId={}, error={}", serverId, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送告警通知
|
||||||
|
*/
|
||||||
|
private void sendAlertNotification(Long serverId, ServerAlertRule rule, AlertLevelEnum level,
|
||||||
|
BigDecimal currentValue, BigDecimal threshold, String resourceInfo,
|
||||||
|
ServerMonitorNotificationConfig config) {
|
||||||
|
try {
|
||||||
|
// 1. 获取服务器信息
|
||||||
|
Server server = serverRepository.findById(serverId).orElse(null);
|
||||||
|
if (server == null) {
|
||||||
|
log.warn("服务器不存在,跳过发送通知: serverId={}", serverId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 构建模板参数
|
||||||
|
Map<String, Object> templateParams = new HashMap<>();
|
||||||
|
templateParams.put("serverName", server.getServerName());
|
||||||
|
templateParams.put("serverIp", server.getHostIp());
|
||||||
|
templateParams.put("ruleName", rule.getRuleName());
|
||||||
|
templateParams.put("alertType", rule.getAlertType().getDescription());
|
||||||
|
templateParams.put("alertLevel", level.getDescription());
|
||||||
|
templateParams.put("resourceInfo", resourceInfo);
|
||||||
|
templateParams.put("currentValue", String.format("%.2f", currentValue));
|
||||||
|
templateParams.put("threshold", String.format("%.2f", threshold));
|
||||||
|
templateParams.put("alertTime", LocalDateTime.now().toString());
|
||||||
|
|
||||||
|
// 3. 构建SendNotificationRequest
|
||||||
|
SendNotificationRequest request = new SendNotificationRequest();
|
||||||
|
request.setChannelId(config.getNotificationChannelId());
|
||||||
|
request.setNotificationTemplateId(config.getResourceAlertTemplateId());
|
||||||
|
request.setTemplateParams(templateParams);
|
||||||
|
|
||||||
|
// 4. 发送通知(NotificationService会自动根据渠道类型创建请求对象)
|
||||||
|
notificationService.send(request);
|
||||||
|
|
||||||
|
log.info("✅ 告警通知已发送: serverId={}, channelId={}, templateId={}",
|
||||||
|
serverId, config.getNotificationChannelId(), config.getResourceAlertTemplateId());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("发送告警通知异常: serverId={}", serverId, e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
package com.qqchen.deploy.backend.deploy.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.qqchen.deploy.backend.deploy.dto.ServerMonitorDataDTO;
|
||||||
|
import com.qqchen.deploy.backend.deploy.entity.ServerMonitor;
|
||||||
|
import com.qqchen.deploy.backend.deploy.repository.IServerMonitorRepository;
|
||||||
|
import com.qqchen.deploy.backend.deploy.service.IServerMonitorService;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器监控服务实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class ServerMonitorServiceImpl implements IServerMonitorService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IServerMonitorRepository monitorRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void batchSaveMonitorData(List<ServerMonitorDataDTO> dataList) {
|
||||||
|
if (dataList == null || dataList.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ServerMonitor> monitors = dataList.stream()
|
||||||
|
.map(this::convertToEntity)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
monitorRepository.saveAll(monitors);
|
||||||
|
log.debug("批量保存监控数据成功: count={}", monitors.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public int deleteOldData(LocalDateTime beforeTime) {
|
||||||
|
int deletedCount = monitorRepository.deleteByCollectTimeBefore(beforeTime);
|
||||||
|
log.info("删除历史监控数据: beforeTime={}, count={}", beforeTime, deletedCount);
|
||||||
|
return deletedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换DTO为实体
|
||||||
|
*/
|
||||||
|
private ServerMonitor convertToEntity(ServerMonitorDataDTO dto) {
|
||||||
|
String diskUsageJson = null;
|
||||||
|
if (dto.getDiskUsage() != null && !dto.getDiskUsage().isEmpty()) {
|
||||||
|
try {
|
||||||
|
diskUsageJson = objectMapper.writeValueAsString(dto.getDiskUsage());
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("序列化磁盘使用率失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ServerMonitor.builder()
|
||||||
|
.serverId(dto.getServerId())
|
||||||
|
.cpuUsage(dto.getCpuUsage())
|
||||||
|
.memoryUsage(dto.getMemoryUsage())
|
||||||
|
.memoryUsed(dto.getMemoryUsed())
|
||||||
|
.diskUsage(diskUsageJson)
|
||||||
|
.collectTime(dto.getCollectTime())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -74,6 +74,7 @@ public class ServerServiceImpl
|
|||||||
server.setCpuCores(dto.getCpuCores());
|
server.setCpuCores(dto.getCpuCores());
|
||||||
server.setMemorySize(dto.getMemorySize());
|
server.setMemorySize(dto.getMemorySize());
|
||||||
server.setDiskSize(dto.getDiskSize());
|
server.setDiskSize(dto.getDiskSize());
|
||||||
|
server.setDiskInfo(dto.getDiskInfo());
|
||||||
server.setOsVersion(dto.getOsVersion());
|
server.setOsVersion(dto.getOsVersion());
|
||||||
server.setHostname(dto.getHostname());
|
server.setHostname(dto.getHostname());
|
||||||
server.setStatus(ServerStatusEnum.ONLINE);
|
server.setStatus(ServerStatusEnum.ONLINE);
|
||||||
@ -81,8 +82,9 @@ public class ServerServiceImpl
|
|||||||
|
|
||||||
// 3. 保存并返回
|
// 3. 保存并返回
|
||||||
Server updated = serverRepository.save(server);
|
Server updated = serverRepository.save(server);
|
||||||
log.info("服务器初始化成功: serverId={}, cpuCores={}, memorySize={}GB, diskSize={}GB",
|
log.info("服务器初始化成功: serverId={}, cpuCores={}, memorySize={}GB, diskSize={}GB, diskCount={}",
|
||||||
serverId, dto.getCpuCores(), dto.getMemorySize(), dto.getDiskSize());
|
serverId, dto.getCpuCores(), dto.getMemorySize(), dto.getDiskSize(),
|
||||||
|
dto.getDiskInfo() != null ? dto.getDiskInfo().size() : 0);
|
||||||
|
|
||||||
return converter.toDto(updated);
|
return converter.toDto(updated);
|
||||||
}
|
}
|
||||||
@ -169,10 +171,19 @@ public class ServerServiceImpl
|
|||||||
info.setOsVersion(sshService.getOsVersion(sshClient));
|
info.setOsVersion(sshService.getOsVersion(sshClient));
|
||||||
info.setCpuCores(sshService.getCpuCores(sshClient));
|
info.setCpuCores(sshService.getCpuCores(sshClient));
|
||||||
info.setMemorySize(sshService.getMemorySize(sshClient));
|
info.setMemorySize(sshService.getMemorySize(sshClient));
|
||||||
info.setDiskSize(sshService.getDiskSize(sshClient));
|
info.setDiskInfo(sshService.getDiskInfo(sshClient));
|
||||||
|
|
||||||
log.info("服务器信息采集成功: serverId={}, hostname={}, cpu={}核, mem={}GB, disk={}GB",
|
// 计算磁盘总容量
|
||||||
serverId, info.getHostname(), info.getCpuCores(), info.getMemorySize(), info.getDiskSize());
|
if (info.getDiskInfo() != null && !info.getDiskInfo().isEmpty()) {
|
||||||
|
int totalSize = info.getDiskInfo().stream()
|
||||||
|
.mapToInt(disk -> disk.getTotalSize().intValue())
|
||||||
|
.sum();
|
||||||
|
info.setDiskSize(totalSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("服务器信息采集成功: serverId={}, hostname={}, cpu={}核, mem={}GB, disk={}GB, diskCount={}",
|
||||||
|
serverId, info.getHostname(), info.getCpuCores(), info.getMemorySize(),
|
||||||
|
info.getDiskSize(), info.getDiskInfo() != null ? info.getDiskInfo().size() : 0);
|
||||||
|
|
||||||
// 6. 更新服务器信息到数据库
|
// 6. 更新服务器信息到数据库
|
||||||
server.setStatus(ServerStatusEnum.ONLINE);
|
server.setStatus(ServerStatusEnum.ONLINE);
|
||||||
@ -182,6 +193,7 @@ public class ServerServiceImpl
|
|||||||
server.setCpuCores(info.getCpuCores());
|
server.setCpuCores(info.getCpuCores());
|
||||||
server.setMemorySize(info.getMemorySize());
|
server.setMemorySize(info.getMemorySize());
|
||||||
server.setDiskSize(info.getDiskSize());
|
server.setDiskSize(info.getDiskSize());
|
||||||
|
server.setDiskInfo(info.getDiskInfo());
|
||||||
serverRepository.save(server);
|
serverRepository.save(server);
|
||||||
|
|
||||||
log.info("服务器状态已更新为ONLINE: serverId={}", serverId);
|
log.info("服务器状态已更新为ONLINE: serverId={}", serverId);
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
package com.qqchen.deploy.backend.framework.converter;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
|
||||||
|
import jakarta.persistence.AttributeConverter;
|
||||||
|
import jakarta.persistence.Converter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 磁盘信息列表 JSON 转换器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Converter
|
||||||
|
public class DiskInfoListConverter implements AttributeConverter<List<DiskInfo>, String> {
|
||||||
|
|
||||||
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String convertToDatabaseColumn(List<DiskInfo> attribute) {
|
||||||
|
if (attribute == null || attribute.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(attribute);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("转换磁盘信息为JSON失败", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DiskInfo> convertToEntityAttribute(String dbData) {
|
||||||
|
if (dbData == null || dbData.trim().isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(dbData, new TypeReference<List<DiskInfo>>() {});
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("解析磁盘信息JSON失败: {}", dbData, e);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -64,11 +64,37 @@ public abstract class AbstractSSHCommandService implements ISSHCommandService {
|
|||||||
|
|
||||||
return sshClient;
|
return sshClient;
|
||||||
|
|
||||||
|
} catch (net.schmizz.sshj.userauth.UserAuthException e) {
|
||||||
|
// 认证失败
|
||||||
|
closeConnection(sshClient);
|
||||||
|
log.error("SSH认证失败: {}@{}:{}", username, host, port);
|
||||||
|
throw new BusinessException(ResponseCode.SSH_FILE_AUTHENTICATION_FAILED);
|
||||||
|
} catch (net.schmizz.sshj.transport.TransportException e) {
|
||||||
|
// 连接失败(网络问题、主机不可达等)
|
||||||
|
closeConnection(sshClient);
|
||||||
|
log.error("SSH连接失败: {}@{}:{}, 原因: {}", username, host, port, e.getMessage());
|
||||||
|
throw new BusinessException(ResponseCode.SSH_FILE_CONNECTION_FAILED);
|
||||||
|
} catch (java.net.UnknownHostException e) {
|
||||||
|
// 主机不存在
|
||||||
|
closeConnection(sshClient);
|
||||||
|
log.error("SSH主机不存在: {}", host);
|
||||||
|
throw new BusinessException(ResponseCode.SSH_FILE_CONNECTION_FAILED,
|
||||||
|
new Object[]{"主机地址无效或不可达"});
|
||||||
|
} catch (java.net.SocketTimeoutException e) {
|
||||||
|
// 连接超时
|
||||||
|
closeConnection(sshClient);
|
||||||
|
log.error("SSH连接超时: {}@{}:{}", username, host, port);
|
||||||
|
throw new BusinessException(ResponseCode.SSH_FILE_TIMEOUT);
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
// 业务异常直接抛出
|
||||||
|
closeConnection(sshClient);
|
||||||
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 连接失败,关闭客户端
|
// 其他未知异常
|
||||||
closeConnection(sshClient);
|
closeConnection(sshClient);
|
||||||
log.error("SSH连接失败: {}@{}:{}", username, host, port, e);
|
log.error("SSH连接失败: {}@{}:{}", username, host, port, e);
|
||||||
throw e;
|
throw new BusinessException(ResponseCode.SSH_FILE_CONNECTION_FAILED,
|
||||||
|
new Object[]{e.getMessage()});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,4 +162,19 @@ public abstract class AbstractSSHCommandService implements ISSHCommandService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析BigDecimal结果
|
||||||
|
*/
|
||||||
|
protected java.math.BigDecimal parseBigDecimal(String value) {
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new java.math.BigDecimal(value.trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("解析BigDecimal失败: {}", value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
package com.qqchen.deploy.backend.framework.ssh;
|
package com.qqchen.deploy.backend.framework.ssh;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskUsageInfo;
|
||||||
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
|
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
|
||||||
import net.schmizz.sshj.SSHClient;
|
import net.schmizz.sshj.SSHClient;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SSH命令服务接口(Framework层)
|
* SSH命令服务接口(Framework层)
|
||||||
* 封装了SSH连接和OS命令执行能力
|
* 封装了SSH连接和OS命令执行能力
|
||||||
@ -74,12 +79,36 @@ public interface ISSHCommandService {
|
|||||||
Integer getMemorySize(SSHClient sshClient);
|
Integer getMemorySize(SSHClient sshClient);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取磁盘大小(GB)
|
* 获取磁盘信息列表
|
||||||
*
|
*
|
||||||
* @param sshClient SSH客户端
|
* @param sshClient SSH客户端
|
||||||
* @return 磁盘大小
|
* @return 磁盘信息列表
|
||||||
*/
|
*/
|
||||||
Integer getDiskSize(SSHClient sshClient);
|
List<DiskInfo> getDiskInfo(SSHClient sshClient);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取CPU使用率(%)
|
||||||
|
*
|
||||||
|
* @param sshClient SSH客户端
|
||||||
|
* @return CPU使用率
|
||||||
|
*/
|
||||||
|
BigDecimal getCpuUsage(SSHClient sshClient);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取内存使用率(%)
|
||||||
|
*
|
||||||
|
* @param sshClient SSH客户端
|
||||||
|
* @return 内存使用率
|
||||||
|
*/
|
||||||
|
BigDecimal getMemoryUsage(SSHClient sshClient);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取各分区磁盘使用率
|
||||||
|
*
|
||||||
|
* @param sshClient SSH客户端
|
||||||
|
* @return 磁盘使用率列表
|
||||||
|
*/
|
||||||
|
List<DiskUsageInfo> getDiskUsage(SSHClient sshClient);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取支持的操作系统类型
|
* 获取支持的操作系统类型
|
||||||
|
|||||||
@ -1,11 +1,19 @@
|
|||||||
package com.qqchen.deploy.backend.framework.ssh.impl;
|
package com.qqchen.deploy.backend.framework.ssh.impl;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskUsageInfo;
|
||||||
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
|
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
|
||||||
import com.qqchen.deploy.backend.framework.ssh.AbstractSSHCommandService;
|
import com.qqchen.deploy.backend.framework.ssh.AbstractSSHCommandService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.schmizz.sshj.SSHClient;
|
import net.schmizz.sshj.SSHClient;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Linux SSH命令服务实现(Framework层)
|
* Linux SSH命令服务实现(Framework层)
|
||||||
*/
|
*/
|
||||||
@ -41,10 +49,112 @@ public class LinuxSSHCommandServiceImpl extends AbstractSSHCommandService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer getDiskSize(SSHClient sshClient) {
|
public List<DiskInfo> getDiskInfo(SSHClient sshClient) {
|
||||||
// 获取根分区的磁盘大小(GB)
|
// 使用df命令获取所有挂载点的磁盘信息
|
||||||
String result = safeExecute(sshClient, "df -BG / | tail -1 | awk '{print $2}' | sed 's/G//'");
|
// 格式: 设备|挂载点|文件系统类型|总大小(GB)
|
||||||
return parseInteger(result);
|
// 过滤条件:只保留 /dev/sd*, /dev/vd*, /dev/nvme*, /dev/mapper/* 等真实磁盘设备
|
||||||
|
// 排除:loop设备、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<DiskInfo> diskList = new ArrayList<>();
|
||||||
|
Set<String> seenDevices = new HashSet<>(); // 用于去重:同一设备只记录一次
|
||||||
|
|
||||||
|
if (result != null && !result.trim().isEmpty()) {
|
||||||
|
String[] lines = result.trim().split("\\n");
|
||||||
|
for (String line : lines) {
|
||||||
|
String[] parts = line.split("\\|");
|
||||||
|
if (parts.length >= 4) {
|
||||||
|
try {
|
||||||
|
String device = parts[0].trim();
|
||||||
|
String mountPoint = parts[1].trim();
|
||||||
|
String fileSystem = parts[2].trim();
|
||||||
|
String sizeStr = parts[3].trim();
|
||||||
|
|
||||||
|
// 去重:如果同一个设备已经记录过,跳过(避免子卷、绑定挂载重复)
|
||||||
|
if (seenDevices.contains(device)) {
|
||||||
|
log.debug("跳过重复设备: {} -> {}", device, mountPoint);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenDevices.add(device);
|
||||||
|
|
||||||
|
DiskInfo diskInfo = DiskInfo.builder()
|
||||||
|
.mountPoint(mountPoint)
|
||||||
|
.fileSystem(fileSystem)
|
||||||
|
.totalSize(new BigDecimal(sizeStr))
|
||||||
|
.build();
|
||||||
|
diskList.add(diskInfo);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("解析磁盘信息失败: {}", line, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diskList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal getCpuUsage(SSHClient sshClient) {
|
||||||
|
// 使用 top 命令获取CPU使用率
|
||||||
|
String command = "top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | sed 's/%us,//'";
|
||||||
|
String result = safeExecute(sshClient, command);
|
||||||
|
return parseBigDecimal(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal getMemoryUsage(SSHClient sshClient) {
|
||||||
|
// 计算内存使用率:(总内存 - 可用内存) / 总内存 * 100
|
||||||
|
String command = "free | grep Mem | awk '{printf \"%.2f\", ($2-$7)/$2 * 100}'";
|
||||||
|
String result = safeExecute(sshClient, command);
|
||||||
|
return parseBigDecimal(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DiskUsageInfo> getDiskUsage(SSHClient sshClient) {
|
||||||
|
// 获取磁盘使用率
|
||||||
|
// 格式: 设备|挂载点|文件系统|总容量|已用|使用率
|
||||||
|
String command = "df -BG -T | grep -E '^/dev/(sd|vd|nvme|mapper|xvd)' | " +
|
||||||
|
"awk '{print $1 \"|\" $7 \"|\" $2 \"|\" $3 \"|\" $4 \"|\" $6}' | " +
|
||||||
|
"sed 's/G//g; s/%//'";
|
||||||
|
String result = safeExecute(sshClient, command);
|
||||||
|
|
||||||
|
List<DiskUsageInfo> diskUsageList = new ArrayList<>();
|
||||||
|
Set<String> seenDevices = new HashSet<>();
|
||||||
|
|
||||||
|
if (result != null && !result.trim().isEmpty()) {
|
||||||
|
String[] lines = result.trim().split("\\n");
|
||||||
|
for (String line : lines) {
|
||||||
|
String[] parts = line.split("\\|");
|
||||||
|
if (parts.length >= 6) {
|
||||||
|
try {
|
||||||
|
String device = parts[0].trim();
|
||||||
|
String mountPoint = parts[1].trim();
|
||||||
|
String fileSystem = parts[2].trim();
|
||||||
|
String totalStr = parts[3].trim();
|
||||||
|
String usedStr = parts[4].trim();
|
||||||
|
String usageStr = parts[5].trim();
|
||||||
|
|
||||||
|
// 去重
|
||||||
|
if (seenDevices.contains(device)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenDevices.add(device);
|
||||||
|
|
||||||
|
DiskUsageInfo diskUsage = DiskUsageInfo.builder()
|
||||||
|
.mountPoint(mountPoint)
|
||||||
|
.fileSystem(fileSystem)
|
||||||
|
.totalSize(new BigDecimal(totalStr))
|
||||||
|
.usedSize(new BigDecimal(usedStr))
|
||||||
|
.usagePercent(new BigDecimal(usageStr))
|
||||||
|
.build();
|
||||||
|
diskUsageList.add(diskUsage);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("解析磁盘使用率失败: {}", line, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diskUsageList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -1,11 +1,19 @@
|
|||||||
package com.qqchen.deploy.backend.framework.ssh.impl;
|
package com.qqchen.deploy.backend.framework.ssh.impl;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskUsageInfo;
|
||||||
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
|
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
|
||||||
import com.qqchen.deploy.backend.framework.ssh.AbstractSSHCommandService;
|
import com.qqchen.deploy.backend.framework.ssh.AbstractSSHCommandService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.schmizz.sshj.SSHClient;
|
import net.schmizz.sshj.SSHClient;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MacOS SSH命令服务实现(Framework层)
|
* MacOS SSH命令服务实现(Framework层)
|
||||||
*/
|
*/
|
||||||
@ -41,11 +49,114 @@ public class MacOSSSHCommandServiceImpl extends AbstractSSHCommandService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer getDiskSize(SSHClient sshClient) {
|
public List<DiskInfo> getDiskInfo(SSHClient sshClient) {
|
||||||
// 获取根分区磁盘大小(GB)
|
// 使用df命令获取所有挂载点的磁盘信息
|
||||||
String command = "df -g / | tail -1 | awk '{print $2}'";
|
// 格式: 设备|挂载点|总大小(GB)
|
||||||
|
// MacOS的df输出格式: Filesystem 512-blocks Used Available Capacity iused ifree %iused Mounted
|
||||||
|
String command = "df -g | grep -E '^/dev/disk' | awk '{print $1 \"| \" $9 \"| \" $2}'";
|
||||||
String result = safeExecute(sshClient, command);
|
String result = safeExecute(sshClient, command);
|
||||||
return parseInteger(result);
|
|
||||||
|
List<DiskInfo> diskList = new ArrayList<>();
|
||||||
|
Set<String> seenDevices = new HashSet<>();
|
||||||
|
|
||||||
|
if (result != null && !result.trim().isEmpty()) {
|
||||||
|
String[] lines = result.trim().split("\\n");
|
||||||
|
for (String line : lines) {
|
||||||
|
String[] parts = line.split("\\|");
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
try {
|
||||||
|
String device = parts[0].trim();
|
||||||
|
String mountPoint = parts[1].trim();
|
||||||
|
String sizeStr = parts[2].trim();
|
||||||
|
|
||||||
|
// 去重
|
||||||
|
if (seenDevices.contains(device)) {
|
||||||
|
log.debug("跳过重复设备: {} -> {}", device, mountPoint);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenDevices.add(device);
|
||||||
|
|
||||||
|
// MacOS通常使用APFS,但我们从设备名简化处理
|
||||||
|
String fileSystem = device.contains("disk") ? "APFS" : "HFS+";
|
||||||
|
|
||||||
|
DiskInfo diskInfo = DiskInfo.builder()
|
||||||
|
.mountPoint(mountPoint)
|
||||||
|
.fileSystem(fileSystem)
|
||||||
|
.totalSize(new BigDecimal(sizeStr))
|
||||||
|
.build();
|
||||||
|
diskList.add(diskInfo);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("解析磁盘信息失败: {}", line, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diskList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal getCpuUsage(SSHClient sshClient) {
|
||||||
|
// MacOS CPU使用率:使用top命令获取
|
||||||
|
String command = "top -l 1 -s 0 | grep 'CPU usage' | awk '{print $3}' | sed 's/%//'";
|
||||||
|
String result = safeExecute(sshClient, command);
|
||||||
|
return parseBigDecimal(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal getMemoryUsage(SSHClient sshClient) {
|
||||||
|
// MacOS内存使用率:使用vm_stat命令计算
|
||||||
|
// vm_stat输出的是页数,需要计算实际使用率
|
||||||
|
String command = "vm_stat | awk '/Pages active/ {active=$3} /Pages wired/ {wired=$4} /Pages free/ {free=$3} END {used=active+wired; total=used+free; printf \"%.2f\", (used/total)*100}'";
|
||||||
|
String result = safeExecute(sshClient, command);
|
||||||
|
return parseBigDecimal(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DiskUsageInfo> getDiskUsage(SSHClient sshClient) {
|
||||||
|
// MacOS磁盘使用率:使用df命令
|
||||||
|
// 格式: 设备|挂载点|容量|已用|可用|使用率
|
||||||
|
String command = "df -h | grep -E '^/dev/disk' | awk '{print $1 \"|\" $9 \"|\" $2 \"|\" $3 \"|\" $4 \"|\" $5}' | sed 's/Gi//g; s/%//'";
|
||||||
|
String result = safeExecute(sshClient, command);
|
||||||
|
|
||||||
|
List<DiskUsageInfo> diskUsageList = new ArrayList<>();
|
||||||
|
Set<String> seenDevices = new HashSet<>();
|
||||||
|
|
||||||
|
if (result != null && !result.trim().isEmpty()) {
|
||||||
|
String[] lines = result.trim().split("\\n");
|
||||||
|
for (String line : lines) {
|
||||||
|
String[] parts = line.split("\\|");
|
||||||
|
if (parts.length >= 6) {
|
||||||
|
try {
|
||||||
|
String device = parts[0].trim();
|
||||||
|
String mountPoint = parts[1].trim();
|
||||||
|
String totalStr = parts[2].trim();
|
||||||
|
String usedStr = parts[3].trim();
|
||||||
|
String usageStr = parts[5].trim();
|
||||||
|
|
||||||
|
// 去重
|
||||||
|
if (seenDevices.contains(device)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenDevices.add(device);
|
||||||
|
|
||||||
|
// 简化的文件系统类型
|
||||||
|
String fileSystem = "APFS";
|
||||||
|
|
||||||
|
DiskUsageInfo diskUsage = DiskUsageInfo.builder()
|
||||||
|
.mountPoint(mountPoint)
|
||||||
|
.fileSystem(fileSystem)
|
||||||
|
.totalSize(parseBigDecimal(totalStr))
|
||||||
|
.usedSize(parseBigDecimal(usedStr))
|
||||||
|
.usagePercent(parseBigDecimal(usageStr))
|
||||||
|
.build();
|
||||||
|
diskUsageList.add(diskUsage);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析MacOS磁盘使用率失败: {}", line, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diskUsageList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -1,11 +1,19 @@
|
|||||||
package com.qqchen.deploy.backend.framework.ssh.impl;
|
package com.qqchen.deploy.backend.framework.ssh.impl;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskInfo;
|
||||||
|
import com.qqchen.deploy.backend.framework.dto.DiskUsageInfo;
|
||||||
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
|
import com.qqchen.deploy.backend.framework.enums.OsTypeEnum;
|
||||||
import com.qqchen.deploy.backend.framework.ssh.AbstractSSHCommandService;
|
import com.qqchen.deploy.backend.framework.ssh.AbstractSSHCommandService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.schmizz.sshj.SSHClient;
|
import net.schmizz.sshj.SSHClient;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Windows SSH命令服务实现(Framework层)
|
* Windows SSH命令服务实现(Framework层)
|
||||||
* 需要Windows服务器安装OpenSSH Server
|
* 需要Windows服务器安装OpenSSH Server
|
||||||
@ -42,12 +50,132 @@ public class WindowsSSHCommandServiceImpl extends AbstractSSHCommandService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer getDiskSize(SSHClient sshClient) {
|
public List<DiskInfo> getDiskInfo(SSHClient sshClient) {
|
||||||
// 使用PowerShell获取C盘大小(GB)
|
// 使用PowerShell获取所有硬盘分区信息,包含驱动器号、文件系统类型和总容量
|
||||||
String command = "powershell \"(Get-PSDrive C | " +
|
// 输出格式: 驱动器号|文件系统类型|总容量(GB)
|
||||||
"Select-Object @{N='Size';E={[math]::Round($_.Free/1GB + $_.Used/1GB,0)}}).Size\"";
|
String command = "powershell \"Get-Volume | " +
|
||||||
|
"Where-Object {$_.DriveLetter -ne $null -and $_.Size -gt 0} | " +
|
||||||
|
"ForEach-Object {$_.DriveLetter + '|' + $_.FileSystemType + '|' + [math]::Round($_.Size/1GB, 2)}\"";
|
||||||
String result = safeExecute(sshClient, command);
|
String result = safeExecute(sshClient, command);
|
||||||
return parseInteger(result);
|
|
||||||
|
List<DiskInfo> diskList = new ArrayList<>();
|
||||||
|
Set<String> seenDrives = new HashSet<>();
|
||||||
|
|
||||||
|
if (result != null && !result.trim().isEmpty()) {
|
||||||
|
String[] lines = result.trim().split("\\n");
|
||||||
|
for (String line : lines) {
|
||||||
|
String[] parts = line.split("\\|");
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
try {
|
||||||
|
String driveLetter = parts[0].trim();
|
||||||
|
String fileSystem = parts[1].trim();
|
||||||
|
String sizeStr = parts[2].trim();
|
||||||
|
|
||||||
|
// 去重:同一驱动器号只记录一次
|
||||||
|
if (seenDrives.contains(driveLetter)) {
|
||||||
|
log.debug("跳过重复驱动器: {}", driveLetter);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenDrives.add(driveLetter);
|
||||||
|
|
||||||
|
// 只保留本地物理磁盘,排除网络驱动器等
|
||||||
|
// Windows常见文件系统:NTFS, ReFS, FAT32
|
||||||
|
if (fileSystem == null || fileSystem.isEmpty()) {
|
||||||
|
fileSystem = "NTFS"; // 默认NTFS
|
||||||
|
}
|
||||||
|
|
||||||
|
DiskInfo diskInfo = DiskInfo.builder()
|
||||||
|
.mountPoint(driveLetter + ":")
|
||||||
|
.fileSystem(fileSystem)
|
||||||
|
.totalSize(new BigDecimal(sizeStr))
|
||||||
|
.build();
|
||||||
|
diskList.add(diskInfo);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("解析磁盘信息失败: {}", line, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diskList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal getCpuUsage(SSHClient sshClient) {
|
||||||
|
// Windows CPU使用率:使用PowerShell获取
|
||||||
|
String command = "powershell \"Get-Counter '\\Processor(_Total)\\% Processor Time' | " +
|
||||||
|
"Select-Object -ExpandProperty CounterSamples | " +
|
||||||
|
"Select-Object -ExpandProperty CookedValue\"";
|
||||||
|
String result = safeExecute(sshClient, command);
|
||||||
|
return parseBigDecimal(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal getMemoryUsage(SSHClient sshClient) {
|
||||||
|
// Windows内存使用率:使用PowerShell计算
|
||||||
|
String command = "powershell \"$os = Get-CimInstance Win32_OperatingSystem; " +
|
||||||
|
"$total = $os.TotalVisibleMemorySize; " +
|
||||||
|
"$free = $os.FreePhysicalMemory; " +
|
||||||
|
"$used = $total - $free; " +
|
||||||
|
"[math]::Round(($used / $total) * 100, 2)\"";
|
||||||
|
String result = safeExecute(sshClient, command);
|
||||||
|
return parseBigDecimal(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DiskUsageInfo> getDiskUsage(SSHClient sshClient) {
|
||||||
|
// Windows磁盘使用率:使用PowerShell获取
|
||||||
|
// 输出格式: 驱动器号|文件系统|总容量|已用|使用率
|
||||||
|
String command = "powershell \"Get-Volume | " +
|
||||||
|
"Where-Object {$_.DriveLetter -ne $null -and $_.Size -gt 0} | " +
|
||||||
|
"ForEach-Object {" +
|
||||||
|
" $used = [math]::Round(($_.Size - $_.SizeRemaining)/1GB, 2); " +
|
||||||
|
" $total = [math]::Round($_.Size/1GB, 2); " +
|
||||||
|
" $percent = [math]::Round((($_.Size - $_.SizeRemaining)/$_.Size)*100, 2); " +
|
||||||
|
" $_.DriveLetter + '|' + $_.FileSystemType + '|' + $total + '|' + $used + '|' + $percent" +
|
||||||
|
"}\"";
|
||||||
|
String result = safeExecute(sshClient, command);
|
||||||
|
|
||||||
|
List<DiskUsageInfo> diskUsageList = new ArrayList<>();
|
||||||
|
Set<String> seenDrives = new HashSet<>();
|
||||||
|
|
||||||
|
if (result != null && !result.trim().isEmpty()) {
|
||||||
|
String[] lines = result.trim().split("\\n");
|
||||||
|
for (String line : lines) {
|
||||||
|
String[] parts = line.split("\\|");
|
||||||
|
if (parts.length >= 5) {
|
||||||
|
try {
|
||||||
|
String driveLetter = parts[0].trim();
|
||||||
|
String fileSystem = parts[1].trim();
|
||||||
|
String totalStr = parts[2].trim();
|
||||||
|
String usedStr = parts[3].trim();
|
||||||
|
String usageStr = parts[4].trim();
|
||||||
|
|
||||||
|
// 去重
|
||||||
|
if (seenDrives.contains(driveLetter)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenDrives.add(driveLetter);
|
||||||
|
|
||||||
|
// 处理空的文件系统类型
|
||||||
|
if (fileSystem == null || fileSystem.isEmpty()) {
|
||||||
|
fileSystem = "NTFS";
|
||||||
|
}
|
||||||
|
|
||||||
|
DiskUsageInfo diskUsage = DiskUsageInfo.builder()
|
||||||
|
.mountPoint(driveLetter + ":")
|
||||||
|
.fileSystem(fileSystem)
|
||||||
|
.totalSize(parseBigDecimal(totalStr))
|
||||||
|
.usedSize(parseBigDecimal(usedStr))
|
||||||
|
.usagePercent(parseBigDecimal(usageStr))
|
||||||
|
.build();
|
||||||
|
diskUsageList.add(diskUsage);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析Windows磁盘使用率失败: {}", line, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diskUsageList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import jakarta.annotation.Resource;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.schmizz.sshj.SSHClient;
|
import net.schmizz.sshj.SSHClient;
|
||||||
import net.schmizz.sshj.connection.channel.direct.Session;
|
import net.schmizz.sshj.connection.channel.direct.Session;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.core.task.AsyncTaskExecutor;
|
import org.springframework.core.task.AsyncTaskExecutor;
|
||||||
import org.springframework.web.socket.CloseStatus;
|
import org.springframework.web.socket.CloseStatus;
|
||||||
import org.springframework.web.socket.TextMessage;
|
import org.springframework.web.socket.TextMessage;
|
||||||
@ -47,6 +49,8 @@ import java.util.concurrent.Future;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler {
|
public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AbstractSSHWebSocketHandler.class);
|
||||||
|
|
||||||
// ========== 会话存储 ==========
|
// ========== 会话存储 ==========
|
||||||
|
|
||||||
protected final Map<String, WebSocketSession> webSocketSessions = new ConcurrentHashMap<>();
|
protected final Map<String, WebSocketSession> webSocketSessions = new ConcurrentHashMap<>();
|
||||||
|
|||||||
@ -26,12 +26,6 @@ import lombok.Data;
|
|||||||
})
|
})
|
||||||
public abstract class BaseSendNotificationRequest {
|
public abstract class BaseSendNotificationRequest {
|
||||||
|
|
||||||
/**
|
|
||||||
* 通知渠道ID(必填)
|
|
||||||
*/
|
|
||||||
@NotNull(message = "渠道ID不能为空")
|
|
||||||
private Long channelId;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息内容(必填)
|
* 消息内容(必填)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -17,6 +17,10 @@ import java.util.Map;
|
|||||||
@Schema(description = "发送通知请求")
|
@Schema(description = "发送通知请求")
|
||||||
public class SendNotificationRequest {
|
public class SendNotificationRequest {
|
||||||
|
|
||||||
|
@Schema(description = "通知渠道ID", required = true, example = "1")
|
||||||
|
@NotNull(message = "通知渠道ID不能为空")
|
||||||
|
private Long channelId;
|
||||||
|
|
||||||
@Schema(description = "通知模板ID", required = true, example = "1")
|
@Schema(description = "通知模板ID", required = true, example = "1")
|
||||||
@NotNull(message = "通知模板ID不能为空")
|
@NotNull(message = "通知模板ID不能为空")
|
||||||
private Long notificationTemplateId;
|
private Long notificationTemplateId;
|
||||||
@ -24,9 +28,4 @@ public class SendNotificationRequest {
|
|||||||
@Schema(description = "模板渲染参数(仅用于模板变量替换)",
|
@Schema(description = "模板渲染参数(仅用于模板变量替换)",
|
||||||
example = "{\"projectName\":\"测试项目\",\"buildNumber\":\"123\"}")
|
example = "{\"projectName\":\"测试项目\",\"buildNumber\":\"123\"}")
|
||||||
private Map<String, Object> templateParams;
|
private Map<String, Object> templateParams;
|
||||||
|
|
||||||
@Schema(description = "具体的发送请求配置", required = true)
|
|
||||||
@NotNull(message = "发送请求配置不能为空")
|
|
||||||
@Valid
|
|
||||||
private BaseSendNotificationRequest sendRequest;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.qqchen.deploy.backend.notification.service;
|
package com.qqchen.deploy.backend.notification.service;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest;
|
import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest;
|
||||||
|
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知发送服务接口
|
* 通知发送服务接口
|
||||||
@ -13,12 +14,10 @@ public interface INotificationSendService {
|
|||||||
/**
|
/**
|
||||||
* 发送通知(统一接口)
|
* 发送通知(统一接口)
|
||||||
*
|
*
|
||||||
|
* @param channel 通知渠道对象
|
||||||
* @param request 通知请求
|
* @param request 通知请求
|
||||||
* @throws com.qqchen.deploy.backend.framework.exception.BusinessException 渠道不存在、渠道已禁用、发送失败
|
* @throws com.qqchen.deploy.backend.framework.exception.BusinessException 渠道已禁用、发送失败
|
||||||
*/
|
*/
|
||||||
void send(BaseSendNotificationRequest request);
|
void send(NotificationChannel channel, BaseSendNotificationRequest request);
|
||||||
|
|
||||||
// TODO: 便捷方法需要重新设计,因为现在需要指定具体的请求类型
|
|
||||||
// 暂时注释掉,后续根据需要添加
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import com.qqchen.deploy.backend.notification.entity.config.BaseNotificationConf
|
|||||||
import com.qqchen.deploy.backend.notification.entity.config.EmailNotificationConfig;
|
import com.qqchen.deploy.backend.notification.entity.config.EmailNotificationConfig;
|
||||||
import com.qqchen.deploy.backend.notification.entity.config.WeworkNotificationConfig;
|
import com.qqchen.deploy.backend.notification.entity.config.WeworkNotificationConfig;
|
||||||
import com.qqchen.deploy.backend.notification.factory.NotificationChannelAdapterFactory;
|
import com.qqchen.deploy.backend.notification.factory.NotificationChannelAdapterFactory;
|
||||||
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
|
|
||||||
import com.qqchen.deploy.backend.notification.service.INotificationChannelService;
|
import com.qqchen.deploy.backend.notification.service.INotificationChannelService;
|
||||||
import com.qqchen.deploy.backend.notification.service.INotificationSendService;
|
import com.qqchen.deploy.backend.notification.service.INotificationSendService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@ -36,15 +35,12 @@ import static com.qqchen.deploy.backend.framework.annotation.ServiceType.Type.DA
|
|||||||
@ServiceType(DATABASE)
|
@ServiceType(DATABASE)
|
||||||
public class NotificationSendServiceImpl implements INotificationSendService {
|
public class NotificationSendServiceImpl implements INotificationSendService {
|
||||||
|
|
||||||
@Resource
|
|
||||||
private INotificationChannelRepository notificationChannelRepository;
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private NotificationChannelAdapterFactory adapterFactory;
|
private NotificationChannelAdapterFactory adapterFactory;
|
||||||
|
|
||||||
public void send(BaseSendNotificationRequest request) {
|
public void send(NotificationChannel channel, BaseSendNotificationRequest request) {
|
||||||
// 1. 参数校验
|
// 1. 参数校验
|
||||||
if (request == null || request.getChannelId() == null) {
|
if (channel == null || request == null) {
|
||||||
throw new BusinessException(ResponseCode.INVALID_PARAM);
|
throw new BusinessException(ResponseCode.INVALID_PARAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,15 +48,12 @@ public class NotificationSendServiceImpl implements INotificationSendService {
|
|||||||
throw new BusinessException(ResponseCode.INVALID_PARAM);
|
throw new BusinessException(ResponseCode.INVALID_PARAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 查询渠道配置
|
// 2. 校验渠道状态
|
||||||
NotificationChannel channel = notificationChannelRepository.findById(request.getChannelId()).orElseThrow(() -> new BusinessException(ResponseCode.NOTIFICATION_CHANNEL_NOT_FOUND));
|
|
||||||
|
|
||||||
// 3. 校验渠道状态
|
|
||||||
if (!channel.getEnabled()) {
|
if (!channel.getEnabled()) {
|
||||||
throw new BusinessException(ResponseCode.NOTIFICATION_CHANNEL_DISABLED);
|
throw new BusinessException(ResponseCode.NOTIFICATION_CHANNEL_DISABLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 获取对应的适配器
|
// 3. 获取对应的适配器
|
||||||
INotificationChannelAdapter adapter;
|
INotificationChannelAdapter adapter;
|
||||||
try {
|
try {
|
||||||
adapter = adapterFactory.getAdapter(channel.getChannelType());
|
adapter = adapterFactory.getAdapter(channel.getChannelType());
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.qqchen.deploy.backend.notification.service.impl;
|
|||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
||||||
import com.qqchen.deploy.backend.framework.utils.JsonUtils;
|
import com.qqchen.deploy.backend.framework.utils.JsonUtils;
|
||||||
|
import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest;
|
||||||
import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest;
|
import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest;
|
||||||
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
|
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
|
||||||
import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
|
import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
|
||||||
@ -49,32 +50,30 @@ public class NotificationServiceImpl implements INotificationService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void send(SendNotificationRequest request) {
|
public void send(SendNotificationRequest request) {
|
||||||
// 1. 获取通知模板
|
// 1. 获取通知渠道
|
||||||
|
NotificationChannel channel = notificationChannelRepository.findById(request.getChannelId())
|
||||||
|
.orElseThrow(() -> new BusinessException(ResponseCode.NOTIFICATION_CHANNEL_NOT_FOUND));
|
||||||
|
|
||||||
|
// 2. 获取通知模板
|
||||||
NotificationTemplate template = notificationTemplateRepository.findById(request.getNotificationTemplateId())
|
NotificationTemplate template = notificationTemplateRepository.findById(request.getNotificationTemplateId())
|
||||||
.orElseThrow(() -> new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_NOT_FOUND));
|
.orElseThrow(() -> new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_NOT_FOUND));
|
||||||
|
|
||||||
// 2. 获取通知渠道
|
|
||||||
NotificationChannel channel = notificationChannelRepository.findById(request.getSendRequest().getChannelId())
|
|
||||||
.orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND));
|
|
||||||
|
|
||||||
// 3. 验证模板和渠道类型是否匹配
|
// 3. 验证模板和渠道类型是否匹配
|
||||||
if (!template.getChannelType().equals(channel.getChannelType())) {
|
if (!template.getChannelType().equals(channel.getChannelType())) {
|
||||||
throw new BusinessException(ResponseCode.INVALID_PARAM);
|
throw new BusinessException(ResponseCode.INVALID_PARAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 验证SendRequest渠道类型和模板渠道类型是否匹配
|
// 4. 验证模板和渠道是否启用
|
||||||
if (!request.getSendRequest().getChannelType().equals(template.getChannelType())) {
|
|
||||||
throw new BusinessException(ResponseCode.INVALID_PARAM);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 验证模板和渠道是否启用
|
|
||||||
if (!template.getEnabled()) {
|
if (!template.getEnabled()) {
|
||||||
throw new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_DISABLED);
|
throw new BusinessException(ResponseCode.NOTIFICATION_TEMPLATE_DISABLED);
|
||||||
}
|
}
|
||||||
if (!channel.getEnabled()) {
|
if (!channel.getEnabled()) {
|
||||||
throw new BusinessException(ResponseCode.DATA_NOT_FOUND);
|
throw new BusinessException(ResponseCode.NOTIFICATION_CHANNEL_DISABLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. 根据渠道类型自动创建对应的发送请求对象
|
||||||
|
BaseSendNotificationRequest sendRequest = createSendRequestByChannel(channel, template);
|
||||||
|
|
||||||
// 6. 渲染模板内容
|
// 6. 渲染模板内容
|
||||||
String content = notificationTemplateService.renderTemplate(template.getContentTemplate(), request.getTemplateParams());
|
String content = notificationTemplateService.renderTemplate(template.getContentTemplate(), request.getTemplateParams());
|
||||||
|
|
||||||
@ -82,11 +81,65 @@ public class NotificationServiceImpl implements INotificationService {
|
|||||||
String title = notificationTemplateService.renderTitleById(request.getNotificationTemplateId(), request.getTemplateParams());
|
String title = notificationTemplateService.renderTitleById(request.getNotificationTemplateId(), request.getTemplateParams());
|
||||||
|
|
||||||
// 8. 设置渲染后的内容和标题
|
// 8. 设置渲染后的内容和标题
|
||||||
request.getSendRequest().setContent(content);
|
sendRequest.setContent(content);
|
||||||
request.getSendRequest().setTitle(title);
|
sendRequest.setTitle(title);
|
||||||
|
|
||||||
// 9. 发送通知
|
// 9. 发送通知
|
||||||
notificationSendService.send(request.getSendRequest());
|
notificationSendService.send(channel, sendRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据渠道类型自动创建对应的发送请求对象
|
||||||
|
* 业务方无需关心此逻辑,由通知服务内部处理
|
||||||
|
*/
|
||||||
|
private BaseSendNotificationRequest createSendRequestByChannel(
|
||||||
|
NotificationChannel channel,
|
||||||
|
NotificationTemplate template) {
|
||||||
|
|
||||||
|
switch (channel.getChannelType()) {
|
||||||
|
case WEWORK -> {
|
||||||
|
WeworkSendNotificationRequest request = new WeworkSendNotificationRequest();
|
||||||
|
// 从模板配置中获取消息类型
|
||||||
|
request.setMessageType(getWeworkMessageType(template));
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
case EMAIL -> {
|
||||||
|
EmailSendNotificationRequest request = new EmailSendNotificationRequest();
|
||||||
|
// 从模板配置中获取收件人(如果有的话)
|
||||||
|
request.setToReceivers(getEmailReceivers(template));
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
default -> throw new BusinessException(ResponseCode.ERROR,
|
||||||
|
new Object[]{"不支持的渠道类型: " + channel.getChannelType()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从模板配置中获取企业微信消息类型
|
||||||
|
*/
|
||||||
|
private WeworkMessageTypeEnum getWeworkMessageType(NotificationTemplate template) {
|
||||||
|
try {
|
||||||
|
if (template.getTemplateConfig() != null) {
|
||||||
|
WeworkTemplateConfig config = JsonUtils.fromMap(
|
||||||
|
template.getTemplateConfig(), WeworkTemplateConfig.class);
|
||||||
|
if (config != null && config.getMessageType() != null) {
|
||||||
|
return config.getMessageType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析企业微信模板配置失败,使用默认消息类型: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
// 默认使用TEXT类型
|
||||||
|
return WeworkMessageTypeEnum.TEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从模板配置中获取邮件收件人
|
||||||
|
*/
|
||||||
|
private List<String> getEmailReceivers(NotificationTemplate template) {
|
||||||
|
// 邮件收件人由调用方在 templateParams 中指定,或者在渠道配置中配置
|
||||||
|
// 这里返回默认收件人,实际使用时由具体业务场景传递
|
||||||
|
return java.util.Arrays.asList("admin@company.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,6 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.delegate;
|
package com.qqchen.deploy.backend.workflow.delegate;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.utils.JsonUtils;
|
|
||||||
import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest;
|
|
||||||
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
|
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
|
||||||
import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
|
|
||||||
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
|
|
||||||
import com.qqchen.deploy.backend.notification.entity.NotificationTemplate;
|
|
||||||
import com.qqchen.deploy.backend.notification.entity.config.WeworkTemplateConfig;
|
|
||||||
import com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum;
|
|
||||||
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
|
|
||||||
import com.qqchen.deploy.backend.notification.repository.INotificationTemplateRepository;
|
|
||||||
import com.qqchen.deploy.backend.notification.service.INotificationService;
|
import com.qqchen.deploy.backend.notification.service.INotificationService;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.inputmapping.NotificationInputMapping;
|
import com.qqchen.deploy.backend.workflow.dto.inputmapping.NotificationInputMapping;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.outputs.NotificationOutputs;
|
import com.qqchen.deploy.backend.workflow.dto.outputs.NotificationOutputs;
|
||||||
@ -35,12 +26,6 @@ public class NotificationNodeDelegate extends BaseNodeDelegate<NotificationInput
|
|||||||
@Resource
|
@Resource
|
||||||
private INotificationService notificationService;
|
private INotificationService notificationService;
|
||||||
|
|
||||||
@Resource
|
|
||||||
private INotificationChannelRepository notificationChannelRepository;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private INotificationTemplateRepository notificationTemplateRepository;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void executeInternal(DelegateExecution execution, Map<String, Object> configs, NotificationInputMapping input) {
|
protected void executeInternal(DelegateExecution execution, Map<String, Object> configs, NotificationInputMapping input) {
|
||||||
// 1. 参数校验
|
// 1. 参数校验
|
||||||
@ -48,80 +33,16 @@ public class NotificationNodeDelegate extends BaseNodeDelegate<NotificationInput
|
|||||||
logWarn(String.format("Notification delegate parameter verification failed - channelId: %s, templateId: %s", input.getChannelId(), input.getNotificationTemplateId()));
|
logWarn(String.format("Notification delegate parameter verification failed - channelId: %s, templateId: %s", input.getChannelId(), input.getNotificationTemplateId()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 2. 查询渠道和模板信息
|
|
||||||
NotificationChannel channel = notificationChannelRepository.findById(input.getChannelId()).orElseThrow(() -> new RuntimeException("通知渠道不存在: " + input.getChannelId()));
|
|
||||||
NotificationTemplate template = notificationTemplateRepository.findById(input.getNotificationTemplateId()).orElseThrow(() -> new RuntimeException("通知模板不存在: " + input.getNotificationTemplateId()));
|
|
||||||
|
|
||||||
// 3. 构建SendNotificationRequest
|
// 2. 构建SendNotificationRequest
|
||||||
SendNotificationRequest request = new SendNotificationRequest();
|
SendNotificationRequest request = new SendNotificationRequest();
|
||||||
|
request.setChannelId(input.getChannelId());
|
||||||
request.setNotificationTemplateId(input.getNotificationTemplateId());
|
request.setNotificationTemplateId(input.getNotificationTemplateId());
|
||||||
request.setTemplateParams(execution.getVariables());
|
request.setTemplateParams(execution.getVariables());
|
||||||
|
|
||||||
// 4. 根据渠道类型创建sendRequest,并从模板配置中获取参数
|
// 3. 发送通知(NotificationService会自动根据渠道类型创建请求对象)
|
||||||
switch (channel.getChannelType()) {
|
|
||||||
case WEWORK -> {
|
|
||||||
WeworkSendNotificationRequest weworkRequest = new WeworkSendNotificationRequest();
|
|
||||||
weworkRequest.setChannelId(input.getChannelId());
|
|
||||||
// 从模板配置中获取消息类型
|
|
||||||
weworkRequest.setMessageType(getWeworkMessageType(template));
|
|
||||||
request.setSendRequest(weworkRequest);
|
|
||||||
}
|
|
||||||
case EMAIL -> {
|
|
||||||
EmailSendNotificationRequest emailRequest = new EmailSendNotificationRequest();
|
|
||||||
emailRequest.setChannelId(input.getChannelId());
|
|
||||||
// 收件人从工作流变量获取
|
|
||||||
emailRequest.setToReceivers(getEmailReceivers(execution, configs));
|
|
||||||
// 其他配置(HTML格式等)由NotificationService根据模板配置自动设置
|
|
||||||
request.setSendRequest(emailRequest);
|
|
||||||
}
|
|
||||||
default -> throw new RuntimeException("不支持的渠道类型: " + channel.getChannelType());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 发送通知(NotificationService会处理模板渲染和详细配置)
|
|
||||||
notificationService.send(request);
|
notificationService.send(request);
|
||||||
|
|
||||||
log.info("工作流通知发送成功 - 渠道ID: {}, 模板ID: {}", input.getChannelId(), input.getNotificationTemplateId());
|
log.info("工作流通知发送成功 - 渠道ID: {}, 模板ID: {}", input.getChannelId(), input.getNotificationTemplateId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 从模板配置中获取企业微信消息类型
|
|
||||||
*/
|
|
||||||
private WeworkMessageTypeEnum getWeworkMessageType(NotificationTemplate template) {
|
|
||||||
try {
|
|
||||||
if (template.getTemplateConfig() != null) {
|
|
||||||
WeworkTemplateConfig weworkConfig = JsonUtils.fromMap(template.getTemplateConfig(), WeworkTemplateConfig.class);
|
|
||||||
if (weworkConfig != null && weworkConfig.getMessageType() != null) {
|
|
||||||
return weworkConfig.getMessageType();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("解析企业微信模板配置失败,使用默认消息类型: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认使用TEXT类型
|
|
||||||
return WeworkMessageTypeEnum.TEXT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取邮件收件人列表
|
|
||||||
* 从工作流变量或配置中获取,如果没有则使用默认值
|
|
||||||
*/
|
|
||||||
private java.util.List<String> getEmailReceivers(DelegateExecution execution, Map<String, Object> configs) {
|
|
||||||
// 1. 优先从工作流变量中获取
|
|
||||||
Object receiversVar = execution.getVariable("emailReceivers");
|
|
||||||
if (receiversVar instanceof java.util.List) {
|
|
||||||
return (java.util.List<String>) receiversVar;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 从配置中获取
|
|
||||||
if (configs != null && configs.containsKey("emailReceivers")) {
|
|
||||||
Object receiversConfig = configs.get("emailReceivers");
|
|
||||||
if (receiversConfig instanceof java.util.List) {
|
|
||||||
return (java.util.List<String>) receiversConfig;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 使用默认收件人
|
|
||||||
return Arrays.asList("admin@company.com");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1140,7 +1140,8 @@ CREATE TABLE deploy_server
|
|||||||
description VARCHAR(500) NULL COMMENT '服务器描述',
|
description VARCHAR(500) NULL COMMENT '服务器描述',
|
||||||
cpu_cores INT NULL COMMENT 'CPU核心数',
|
cpu_cores INT NULL COMMENT 'CPU核心数',
|
||||||
memory_size INT NULL COMMENT '内存大小(GB)',
|
memory_size INT NULL COMMENT '内存大小(GB)',
|
||||||
disk_size INT NULL COMMENT '磁盘大小(GB)',
|
disk_size INT NULL COMMENT '磁盘总容量(GB)',
|
||||||
|
disk_info JSON NULL COMMENT '磁盘详细信息(JSON格式,包含挂载点、文件系统、容量等)',
|
||||||
tags JSON NULL COMMENT '标签(JSON格式)',
|
tags JSON NULL COMMENT '标签(JSON格式)',
|
||||||
last_connect_time DATETIME NULL COMMENT '最后连接时间',
|
last_connect_time DATETIME NULL COMMENT '最后连接时间',
|
||||||
|
|
||||||
@ -1158,6 +1159,104 @@ CREATE TABLE deploy_server
|
|||||||
CONSTRAINT fk_server_category FOREIGN KEY (category_id) REFERENCES deploy_server_category (id)
|
CONSTRAINT fk_server_category FOREIGN KEY (category_id) REFERENCES deploy_server_category (id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='服务器管理表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='服务器管理表';
|
||||||
|
|
||||||
|
-- 服务器监控记录表
|
||||||
|
CREATE TABLE deploy_server_monitor
|
||||||
|
(
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
server_id BIGINT NOT NULL COMMENT '服务器ID',
|
||||||
|
|
||||||
|
-- 监控指标
|
||||||
|
cpu_usage DECIMAL(5,2) NULL COMMENT 'CPU使用率(%)',
|
||||||
|
memory_usage DECIMAL(5,2) NULL COMMENT '内存使用率(%)',
|
||||||
|
memory_used INT NULL COMMENT '已用内存(GB)',
|
||||||
|
|
||||||
|
-- 磁盘使用情况(JSON)
|
||||||
|
disk_usage JSON NULL COMMENT '各分区使用率',
|
||||||
|
|
||||||
|
-- 网络流量(可选)
|
||||||
|
network_rx BIGINT NULL COMMENT '网络接收(KB/s)',
|
||||||
|
network_tx BIGINT NULL COMMENT '网络发送(KB/s)',
|
||||||
|
|
||||||
|
-- 采集时间
|
||||||
|
collect_time DATETIME NOT NULL COMMENT '采集时间',
|
||||||
|
|
||||||
|
INDEX idx_server_time (server_id, collect_time),
|
||||||
|
INDEX idx_collect_time (collect_time),
|
||||||
|
CONSTRAINT fk_monitor_server FOREIGN KEY (server_id) REFERENCES deploy_server (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='服务器监控记录表';
|
||||||
|
|
||||||
|
-- 服务器告警规则表
|
||||||
|
CREATE TABLE deploy_server_alert_rule
|
||||||
|
(
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
create_by VARCHAR(100) NULL COMMENT '创建人',
|
||||||
|
create_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
update_by VARCHAR(100) NULL COMMENT '更新人',
|
||||||
|
update_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
version INT NOT NULL DEFAULT 1 COMMENT '版本号',
|
||||||
|
deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否删除',
|
||||||
|
|
||||||
|
server_id BIGINT NULL COMMENT '服务器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
|
CREATE TABLE deploy_record
|
||||||
(
|
(
|
||||||
|
|||||||
@ -33,8 +33,8 @@ dependency.injection.entitypath.failed=初始化实体 {0} 的EntityPath失败:
|
|||||||
# --------------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------------
|
||||||
# SSH文件操作相关 (SSH File Operations) - 1200-1299
|
# SSH文件操作相关 (SSH File Operations) - 1200-1299
|
||||||
# --------------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------------
|
||||||
ssh.file.connection.failed=SSH连接失败
|
ssh.file.connection.failed=SSH连接失败,请检查服务器地址、端口和网络连接
|
||||||
ssh.file.authentication.failed=SSH认证失败
|
ssh.file.authentication.failed=SSH认证失败,请检查用户名和密码/密钥是否正确
|
||||||
ssh.file.not.found=文件不存在
|
ssh.file.not.found=文件不存在
|
||||||
ssh.file.directory.not.found=目录不存在
|
ssh.file.directory.not.found=目录不存在
|
||||||
ssh.file.permission.denied=权限不足
|
ssh.file.permission.denied=权限不足
|
||||||
|
|||||||
@ -33,8 +33,8 @@ dependency.injection.entitypath.failed=Failed to initialize EntityPath for entit
|
|||||||
# --------------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------------
|
||||||
# SSH File Operations - 1200-1299
|
# SSH File Operations - 1200-1299
|
||||||
# --------------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------------
|
||||||
ssh.file.connection.failed=SSH connection failed
|
ssh.file.connection.failed=SSH connection failed, please check server address, port and network connection
|
||||||
ssh.file.authentication.failed=SSH authentication failed
|
ssh.file.authentication.failed=SSH authentication failed, please check username and password/key
|
||||||
ssh.file.not.found=File not found
|
ssh.file.not.found=File not found
|
||||||
ssh.file.directory.not.found=Directory not found
|
ssh.file.directory.not.found=Directory not found
|
||||||
ssh.file.permission.denied=Permission denied
|
ssh.file.permission.denied=Permission denied
|
||||||
|
|||||||
@ -33,8 +33,8 @@ dependency.injection.entitypath.failed=获取实体 {0} 的QClass失败
|
|||||||
# --------------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------------
|
||||||
# SSH文件操作相关 (SSH File Operations) - 1200-1299
|
# SSH文件操作相关 (SSH File Operations) - 1200-1299
|
||||||
# --------------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------------
|
||||||
ssh.file.connection.failed=SSH连接失败
|
ssh.file.connection.failed=SSH连接失败,请检查服务器地址、端口和网络连接
|
||||||
ssh.file.authentication.failed=SSH认证失败
|
ssh.file.authentication.failed=SSH认证失败,请检查用户名和密码/密钥是否正确
|
||||||
ssh.file.not.found=文件不存在
|
ssh.file.not.found=文件不存在
|
||||||
ssh.file.directory.not.found=目录不存在
|
ssh.file.directory.not.found=目录不存在
|
||||||
ssh.file.permission.denied=权限不足
|
ssh.file.permission.denied=权限不足
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user