Files
aiops/backend/controller/site_view.go
T
2026-07-09 14:16:56 +08:00

224 lines
6.0 KiB
Go

package controller
import (
"strings"
"time"
"ezops/model"
"ezops/pkg/siteprobe"
"gorm.io/gorm"
)
// SiteTaskView 前端拨测任务视图(字段与前端对齐)// Logan.Li
type SiteTaskView struct {
ID uint `json:"id"`
Name string `json:"name"`
URL string `json:"url"`
Type string `json:"type"`
FrequencySec int `json:"frequency_sec"`
TimeoutSec int `json:"timeout_sec"`
ExpectedStatusCode int `json:"expected_status_code"`
Enabled bool `json:"enabled"`
LastStatus string `json:"last_status,omitempty"`
LastResponseTimeMs float64 `json:"last_response_time_ms,omitempty"`
LastCheckedAt *string `json:"last_checked_at,omitempty"`
SuccessRate float64 `json:"success_rate"`
AlertStatus string `json:"alert_status"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
SSLDaysRemaining *int `json:"ssl_days_remaining,omitempty"`
SSLCertExpiry *string `json:"ssl_cert_expiry,omitempty"`
}
func toSiteTaskView(task model.SiteTask) SiteTaskView {
taskType := normalizeTaskType(task)
enabled := task.Status == model.SiteTaskStatusRunning
lastStatus := "unknown"
switch task.LastResult {
case "success":
lastStatus = "up"
case "fail", "timeout":
lastStatus = "down"
}
var lastChecked *string
if task.LastCheckAt != nil {
s := task.LastCheckAt.Format("2006-01-02T15:04:05Z07:00")
lastChecked = &s
}
return SiteTaskView{
ID: task.ID,
Name: task.Name,
URL: task.URL,
Type: taskType,
FrequencySec: task.Frequency,
TimeoutSec: task.Timeout,
ExpectedStatusCode: task.ExpectedStatus,
Enabled: enabled,
LastStatus: lastStatus,
LastResponseTimeMs: task.AvgResponse,
LastCheckedAt: lastChecked,
SuccessRate: task.SuccessRate,
AlertStatus: "normal",
Status: string(task.Status),
CreatedAt: task.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}
// enrichSiteTaskViewsAlert 批量附加活跃告警状态 // Logan.Li
func enrichSiteTaskViewsAlert(db *gorm.DB, views []SiteTaskView) {
if len(views) == 0 {
return
}
// 列表加载时清理已恢复任务的陈旧 active 告警(与 down_count 对齐)// Logan.Li
for _, v := range views {
if v.LastStatus == "up" {
siteprobe.ResolveRecoveredProbeAlerts(db, v.ID, "success")
}
}
ids := make([]uint, 0, len(views))
for _, v := range views {
ids = append(ids, v.ID)
}
type row struct {
TaskID uint
}
var rows []row
db.Model(&model.SiteAlert{}).
Select("DISTINCT task_id").
Where("task_id IN ? AND status = ?", ids, model.SiteAlertStatusActive).
Find(&rows)
alerting := make(map[uint]bool, len(rows))
for _, r := range rows {
alerting[r.TaskID] = true
}
for i := range views {
if !alerting[views[i].ID] {
continue
}
// 最近一次拨测成功则显示正常(与 down_count 一致;SSL 证书预警走行内徽章)// Logan.Li
if views[i].LastStatus == "up" {
views[i].AlertStatus = "normal"
continue
}
views[i].AlertStatus = "alerting"
}
}
func normalizeTaskType(task model.SiteTask) string {
t := strings.ToLower(string(task.TaskType))
if t == "https" {
return "HTTP"
}
if t != "" {
return strings.ToUpper(t)
}
return strings.ToUpper(siteprobe.InferTaskTypePublic(task))
}
type siteSSLInfo struct {
DaysRemaining int
CertExpiry *time.Time
}
// enrichSiteTaskViewsSSL 批量附加 SSL 拨测任务的证书到期信息 // Logan.Li
func enrichSiteTaskViewsSSL(db *gorm.DB, views []SiteTaskView) {
if len(views) == 0 {
return
}
ids := make([]uint, 0, len(views))
for _, v := range views {
if strings.ToUpper(v.Type) != "SSL" {
continue
}
ids = append(ids, v.ID)
}
if len(ids) == 0 {
return
}
type row struct {
TaskID uint
SSLDaysRemaining int
SSLCertExpiry *time.Time
}
var rows []row
db.Model(&model.SiteResult{}).
Select("task_id, ssl_days_remaining, ssl_cert_expiry").
Where("task_id IN ? AND ssl_cert_expiry IS NOT NULL", ids).
Order("checked_at DESC").
Find(&rows)
seen := map[uint]bool{}
sslMap := make(map[uint]siteSSLInfo, len(rows))
for _, r := range rows {
if seen[r.TaskID] {
continue
}
seen[r.TaskID] = true
sslMap[r.TaskID] = siteSSLInfo{DaysRemaining: r.SSLDaysRemaining, CertExpiry: r.SSLCertExpiry}
}
for i := range views {
if ssl, ok := sslMap[views[i].ID]; ok {
d := ssl.DaysRemaining
views[i].SSLDaysRemaining = &d
if ssl.CertExpiry != nil {
s := ssl.CertExpiry.Format("2006-01-02T15:04:05Z07:00")
views[i].SSLCertExpiry = &s
}
}
}
}
func parseCreateTaskRequest(raw map[string]interface{}) model.SiteTask {
getStr := func(keys ...string) string {
for _, k := range keys {
if v, ok := raw[k]; ok {
if s, ok := v.(string); ok {
return strings.TrimSpace(s)
}
}
}
return ""
}
getInt := func(def int, keys ...string) int {
for _, k := range keys {
if v, ok := raw[k]; ok {
switch n := v.(type) {
case float64:
return int(n)
case int:
return n
}
}
}
return def
}
taskType := strings.ToLower(getStr("type", "task_type"))
switch taskType {
case "https":
taskType = "http"
case "":
taskType = "http"
}
task := model.SiteTask{
Name: getStr("name"),
URL: getStr("url"),
TaskType: model.SiteTaskType(taskType),
Method: model.SiteTaskMethodGET,
Timeout: getInt(10, "timeout_sec", "timeout"),
Frequency: siteprobe.NormalizeProbeFrequency(taskType, getInt(0, "frequency_sec", "frequency")),
ExpectedStatus: getInt(200, "expected_status_code", "expected_status"),
FollowRedirect: true,
VerifySSL: true,
Status: model.SiteTaskStatusRunning,
}
if v, ok := raw["enabled"].(bool); ok && !v {
task.Status = model.SiteTaskStatusPaused
}
if v, ok := raw["dns_resolver"].(string); ok {
task.DNSResolver = strings.TrimSpace(v)
}
return task
}