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) } // 内容监控配置 // Logan.Li task.ContentCheckMode = normalizeContentMode(getStr("content_check_mode")) task.ContentPaths = getStr("content_paths") task.ContentIgnoreRules = getStr("content_ignore_rules") task.ContentMinLength = getInt(0, "content_min_length") task.ContentConfirmTimes = getInt(2, "content_confirm_times") if task.ContentConfirmTimes <= 0 { task.ContentConfirmTimes = 2 } if v, ok := raw["content_sort_lines"].(bool); ok { task.ContentSortLines = v } return task } // normalizeContentMode 把前端传来的模式归一化,非法值一律回退为 off。 // Logan.Li func normalizeContentMode(mode string) string { switch strings.ToLower(strings.TrimSpace(mode)) { case "text": return siteprobe.ContentModeText case "json": return siteprobe.ContentModeJSON default: return siteprobe.ContentModeOff } }