package controller import ( "bytes" "encoding/json" "fmt" "io" "net/http" "strings" "time" "ezops/model" "gorm.io/gorm" ) // dispatchCategoryAlert 按告警集合策略发送告警通知 // Logan.Li func dispatchCategoryAlert(db *gorm.DB, category string, ruleName, hostname, metric, severity, message string, value, threshold float64) { var policy model.AlertNotifyPolicy if err := db.Where("category = ? AND enabled = ?", category, true).First(&policy).Error; err != nil { fallbackNotifyChannels(db, ruleName, hostname, metric, severity, message, value, threshold) return } title := buildNotifyTitle(db, ruleName, hostname, metric, severity) content := buildAlertContent(ruleName, hostname, metric, severity, message, value, threshold) sent, _ := dispatchToPolicyContacts(db, &policy, title, content, severity, false) if sent == 0 { fallbackNotifyChannels(db, ruleName, hostname, metric, severity, message, value, threshold) } } // dispatchCategoryRecovery 按告警集合策略发送恢复通知 // Logan.Li func dispatchCategoryRecovery(db *gorm.DB, category, ruleName, hostname, metric, message string) { var policy model.AlertNotifyPolicy if err := db.Where("category = ? AND enabled = ?", category, true).First(&policy).Error; err != nil { fallbackNotifyRecovery(db, ruleName, hostname, metric, message) return } title := fmt.Sprintf("%s — 已恢复正常", ruleName) content := fmt.Sprintf("**告警已恢复**\\n\\n目标:%s\\n指标:%s\\n详情:%s\\n恢复时间:%s", hostname, metric, message, time.Now().Format("2006-01-02 15:04:05")) sent, _ := dispatchToPolicyContacts(db, &policy, title, content, "info", true) if sent == 0 { fallbackNotifyRecovery(db, ruleName, hostname, metric, message) } } // resolvePolicyContactIDs 展开策略中的联系组与直接联系人 // Logan.Li func resolvePolicyContactIDs(db *gorm.DB, policy *model.AlertNotifyPolicy) []uint { idSet := make(map[uint]struct{}) var groupIDs []uint if policy.ContactGroupIDs != "" && policy.ContactGroupIDs != "null" { _ = json.Unmarshal([]byte(policy.ContactGroupIDs), &groupIDs) } for _, gid := range groupIDs { var g model.AlertContactGroup if db.First(&g, gid).Error != nil { continue } var cids []uint if g.ContactIDs != "" && g.ContactIDs != "null" { _ = json.Unmarshal([]byte(g.ContactIDs), &cids) } for _, cid := range cids { idSet[cid] = struct{}{} } } var directIDs []uint if policy.ContactIDs != "" && policy.ContactIDs != "null" { _ = json.Unmarshal([]byte(policy.ContactIDs), &directIDs) } for _, cid := range directIDs { idSet[cid] = struct{}{} } ids := make([]uint, 0, len(idSet)) for id := range idSet { ids = append(ids, id) } return ids } // dispatchToPolicyContacts 向策略解析出的联系人发送通知 // Logan.Li func dispatchToPolicyContacts(db *gorm.DB, policy *model.AlertNotifyPolicy, title, content, severity string, isRecovery bool) (sent, skipped int) { if policy == nil || !policy.Enabled { return 0, 1 } contactIDs := resolvePolicyContactIDs(db, policy) if len(contactIDs) == 0 { return 0, 1 } for _, cid := range contactIDs { var contact model.AlertContact if db.First(&contact, cid).Error != nil || contact.WebhookURL == "" { skipped++ continue } var err error switch contact.ChannelType { case "feishu", "": if isRecovery { err = sendFeishuRecovery(db, feishuConfigFromURL(contact.WebhookURL), title, content) } else { err = sendFeishuAlert(db, feishuConfigFromURL(contact.WebhookURL), title, content, severity) } case "dingtalk", "wechat", "webhook": err = sendGenericWebhook(contact.WebhookURL, title, content) default: err = sendGenericWebhook(contact.WebhookURL, title, content) } if err != nil { fmt.Printf("[AlertNotify] 通知失败 (联系人:%s): %v\n", contact.Name, err) skipped++ } else { sent++ } } return sent, skipped } // feishuConfigFromURL 联系人 webhook 转飞书配置 JSON // Logan.Li func feishuConfigFromURL(url string) string { b, _ := json.Marshal(model.FeishuWebhookConfig{URL: url}) return string(b) } // sendGenericWebhook 钉钉/企微/通用 Webhook 文本通知 // Logan.Li func sendGenericWebhook(webhookURL, title, content string) error { body := map[string]interface{}{ "msgtype": "text", "text": map[string]string{ "content": title + "\n" + strings.ReplaceAll(content, "\\n", "\n"), }, } raw, _ := json.Marshal(body) req, err := http.NewRequest("POST", webhookURL, bytes.NewReader(raw)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { b, _ := io.ReadAll(resp.Body) return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(b)) } return nil } // fallbackNotifyChannels 无策略时回退到全局 NotifyChannel // Logan.Li func fallbackNotifyChannels(db *gorm.DB, ruleName, hostname, metric, severity, message string, value, threshold float64) { var channels []model.NotifyChannel db.Where("enabled = ? AND type = ?", true, "feishu").Find(&channels) title := buildNotifyTitle(db, ruleName, hostname, metric, severity) content := buildAlertContent(ruleName, hostname, metric, severity, message, value, threshold) for _, ch := range channels { if err := sendFeishuAlert(db, ch.Config, title, content, severity); err != nil { fmt.Printf("[AlertNotify] 飞书回退通知失败 (渠道:%s): %v\n", ch.Name, err) } } } func fallbackNotifyRecovery(db *gorm.DB, ruleName, hostname, metric, message string) { var channels []model.NotifyChannel db.Where("enabled = ? AND type = ?", true, "feishu").Find(&channels) title := fmt.Sprintf("%s — 已恢复正常", ruleName) content := fmt.Sprintf("**告警已恢复**\\n\\n主机:%s\\n指标:%s\\n详情:%s\\n恢复时间:%s", hostname, metric, message, time.Now().Format("2006-01-02 15:04:05")) for _, ch := range channels { if err := sendFeishuRecovery(db, ch.Config, title, content); err != nil { fmt.Printf("[AlertNotify] 飞书恢复回退失败 (渠道:%s): %v\n", ch.Name, err) } } }