package services import ( "encoding/json" "fmt" "sync" "github.com/engigu/baihu-panel/internal/constant" "github.com/engigu/baihu-panel/internal/database" "github.com/engigu/baihu-panel/internal/logger" "github.com/engigu/baihu-panel/internal/models" "github.com/engigu/baihu-panel/internal/utils" "github.com/engigu/baihu-panel/internal/sdk/messenger" ) // NotifyChannel 通知渠道配置 type NotifyChannel struct { ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` Enabled bool `json:"enabled"` Config map[string]string `json:"config"` } // NotifyMessage 通知消息 type NotifyMessage struct { Title string `json:"title"` Text string `json:"text"` } // NotifyResult 发送结果 type NotifyResult struct { Success bool `json:"success"` Error string `json:"error,omitempty"` } // SupportedChannelTypes 支持的渠道类型 var SupportedChannelTypes = []map[string]string{ {"type": messenger.ChannelTelegram, "label": "Telegram"}, {"type": messenger.ChannelBark, "label": "Bark"}, {"type": messenger.ChannelDtalk, "label": "钉钉"}, {"type": messenger.ChannelQyWeiXin, "label": "企业微信"}, {"type": messenger.ChannelFeishu, "label": "飞书"}, {"type": messenger.ChannelEmail, "label": "邮件"}, {"type": messenger.ChannelCustom, "label": "自定义Webhook"}, {"type": messenger.ChannelNtfy, "label": "Ntfy"}, {"type": messenger.ChannelGotify, "label": "Gotify"}, {"type": messenger.ChannelPushMe, "label": "PushMe"}, // {"type": messenger.ChannelWeChatOFAccount, "label": "微信公众号"}, {"type": messenger.ChannelAliyunSMS, "label": "阿里云短信"}, } // SupportedEvents 支持的事件类型 var SupportedEvents = []map[string]string{ {"type": constant.EventUserLogin, "label": "用户登录", "binding_type": constant.BindingTypeSystem}, {"type": constant.EventBruteForceLogin, "label": "密码多次错误", "binding_type": constant.BindingTypeSystem}, {"type": constant.EventPasswordChanged, "label": "密码修改", "binding_type": constant.BindingTypeSystem}, {"type": constant.EventTaskSuccess, "label": "任务成功", "binding_type": constant.BindingTypeTask}, {"type": constant.EventTaskFailed, "label": "任务失败", "binding_type": constant.BindingTypeTask}, {"type": constant.EventTaskTimeout, "label": "任务超时", "binding_type": constant.BindingTypeTask}, } type NotificationService struct { settingsService *SettingsService mu sync.RWMutex } func NewNotificationService() *NotificationService { return &NotificationService{ settingsService: NewSettingsService(), } } // GetChannels 获取所有渠道 func (s *NotificationService) GetChannels() []NotifyChannel { s.mu.RLock() defer s.mu.RUnlock() return s.getChannelsInternal() } // SaveChannel 保存/更新渠道 func (s *NotificationService) SaveChannel(channel NotifyChannel) error { s.mu.Lock() defer s.mu.Unlock() configJSON, err := json.Marshal(channel.Config) if err != nil { return err } if channel.ID == "" { // 新建 channel.ID = utils.GenerateID() notifyWay := &models.NotifyWay{ ID: channel.ID, Name: channel.Name, Type: channel.Type, Config: string(configJSON), Enabled: channel.Enabled, } return database.DB.Create(notifyWay).Error } // 更新 updates := map[string]interface{}{ "name": channel.Name, "type": channel.Type, "config": string(configJSON), "enabled": channel.Enabled, } return database.DB.Model(&models.NotifyWay{}).Where("id = ?", channel.ID).Updates(updates).Error } // DeleteChannel 删除渠道 func (s *NotificationService) DeleteChannel(id string) error { s.mu.Lock() defer s.mu.Unlock() // 检查渠道是否存在 var count int64 database.DB.Model(&models.NotifyWay{}).Where("id = ?", id).Count(&count) if count == 0 { return fmt.Errorf("渠道 %s 不存在", id) } // 删除渠道 if err := database.DB.Unscoped().Where("id = ?", id).Delete(&models.NotifyWay{}).Error; err != nil { return err } // 同时清理事件绑定中引用此渠道的配置 if err := database.DB.Unscoped().Where("way_id = ?", id).Delete(&models.NotifyBinding{}).Error; err != nil { logger.Errorf("[Notify] 清理事件绑定失败: %v", err) } return nil } // GetBindings 获取事件绑定列表(新接口,用于前端展示) func (s *NotificationService) GetBindings() []models.NotifyBinding { var bindings []models.NotifyBinding database.DB.Find(&bindings) return bindings } // SaveBinding 保存事件绑定 func (s *NotificationService) SaveBinding(binding *models.NotifyBinding) error { if binding.ID == "" { // 检查是否已经存在相同的绑定(避免重复点击导致多个记录) var existing models.NotifyBinding err := database.DB.Where("type = ? AND event = ? AND way_id = ? AND data_id = ?", binding.Type, binding.Event, binding.WayID, binding.DataID).First(&existing).Error if err == nil { // 如果已存在且未删除,直接返回(或者更新它) *binding = existing return nil } binding.ID = utils.GenerateID() return database.DB.Create(binding).Error } return database.DB.Save(binding).Error } // DeleteBinding 删除事件绑定 func (s *NotificationService) DeleteBinding(id string) error { return database.DB.Unscoped().Where("id = ?", id).Delete(&models.NotifyBinding{}).Error } // GetBindingsByEvent 根据事件类型和数据ID获取绑定 func (s *NotificationService) GetBindingsByEvent(bindingType, event, dataID string) []models.NotifyBinding { var bindings []models.NotifyBinding // 如果是任务事件且带有 dataID,只获取特定任务的绑定(禁用全局任务配置) if bindingType == constant.BindingTypeTask && dataID != "" { database.DB.Where("type = ? AND event = ? AND data_id = ?", constant.BindingTypeTask, event, dataID).Find(&bindings) return bindings } // 对于系统事件或其他情况 query := database.DB.Where("event = ?", event) if bindingType != "" { query = query.Where("type = ?", bindingType) } if dataID != "" { query = query.Where("data_id = ?", dataID) } else { query = query.Where("data_id = ? OR data_id IS NULL", "") } query.Find(&bindings) return bindings } // SendToChannel 使用 messenger SDK 发送通知到指定渠道 func (s *NotificationService) SendToChannel(channel NotifyChannel, msg *NotifyMessage) *NotifyResult { result, err := messenger.Send(channel.Type, messenger.ChannelConfig(channel.Config), &messenger.Message{ Title: msg.Title, Text: msg.Text, }) if err != nil { return &NotifyResult{Success: false, Error: err.Error()} } if !result.Success { return &NotifyResult{Success: false, Error: result.Error} } return &NotifyResult{Success: true} } // SendByChannelID 根据渠道ID发送通知 func (s *NotificationService) SendByChannelID(channelID string, msg *NotifyMessage) *NotifyResult { s.mu.RLock() defer s.mu.RUnlock() var notifyWay models.NotifyWay if err := database.DB.Where("id = ?", channelID).First(¬ifyWay).Error; err != nil { return &NotifyResult{Success: false, Error: "渠道不存在"} } if !notifyWay.Enabled { return &NotifyResult{Success: false, Error: "渠道已禁用"} } var config map[string]string if err := json.Unmarshal([]byte(notifyWay.Config), &config); err != nil { return &NotifyResult{Success: false, Error: "渠道配置解析失败"} } ch := NotifyChannel{ ID: notifyWay.ID, Name: notifyWay.Name, Type: notifyWay.Type, Enabled: notifyWay.Enabled, Config: config, } return s.SendToChannel(ch, msg) } // TriggerEvent 触发事件通知(实现 tasks.Notifier 接口) func (s *NotificationService) TriggerEvent(bindingType string, eventType string, dataID string, templateData map[string]interface{}) { var title, text string switch eventType { case constant.EventUserLogin: title = "用户登录通知" text = fmt.Sprintf("用户 %v 在 IP %v 登录成功", templateData["username"], templateData["ip"]) case constant.EventBruteForceLogin: title = "系统安全警告" text = fmt.Sprintf("检测到 IP %v 正在尝试暴力破解用户 %v", templateData["ip"], templateData["username"]) case constant.EventPasswordChanged: title = "账户安全通知" text = fmt.Sprintf("用户 %v 刚刚修改了密码", templateData["username"]) case constant.EventTaskSuccess: title = fmt.Sprintf("任务[%v] 成功", templateData["task_name"]) text = fmt.Sprintf("任务 #%v %v\n状态: 成功\n耗时: %vms", templateData["task_id"], templateData["task_name"], templateData["duration"]) case constant.EventTaskFailed: title = fmt.Sprintf("任务[%v] 失败", templateData["task_name"]) if errStr, ok := templateData["error"]; ok { text = fmt.Sprintf("任务 #%v %v\n执行失败\n错误: %v", templateData["task_id"], templateData["task_name"], errStr) } else { text = fmt.Sprintf("任务 #%v %v\n执行失败\n状态: %v\n耗时: %vms", templateData["task_id"], templateData["task_name"], templateData["status"], templateData["duration"]) } case constant.EventTaskTimeout: title = fmt.Sprintf("任务[%v] 超时", templateData["task_name"]) text = fmt.Sprintf("任务 #%v %v\n执行超时\n耗时: %vms", templateData["task_id"], templateData["task_name"], templateData["duration"]) default: title = "系统通知" text = "收到未知事件" } msg := &NotifyMessage{Title: title, Text: text} bindings := s.GetBindingsByEvent(bindingType, eventType, dataID) if len(bindings) == 0 { return } channels := s.GetChannels() channelMap := make(map[string]NotifyChannel) for _, ch := range channels { channelMap[ch.ID] = ch } for _, binding := range bindings { ch, ok := channelMap[binding.WayID] if !ok || !ch.Enabled { continue } go func(channel NotifyChannel) { result := s.SendToChannel(channel, msg) if !result.Success { logger.Warnf("[Notify] 发送事件 %s 到渠道 %s(%s) 失败: %s", eventType, channel.Name, channel.Type, result.Error) } }(ch) } } // --- 内部方法 --- // getChannelsInternal 从 notify_ways 表中读取所有渠道配置 func (s *NotificationService) getChannelsInternal() []NotifyChannel { var notifyWays []models.NotifyWay database.DB.Find(¬ifyWays) channels := make([]NotifyChannel, 0, len(notifyWays)) for _, nw := range notifyWays { var config map[string]string if err := json.Unmarshal([]byte(nw.Config), &config); err != nil { logger.Warnf("[Notify] 解析渠道 %s 配置失败: %v", nw.ID, err) continue } channels = append(channels, NotifyChannel{ ID: nw.ID, Name: nw.Name, Type: nw.Type, Enabled: nw.Enabled, Config: config, }) } return channels }