diff --git a/cmd/dashboard/controller/member_api.go b/cmd/dashboard/controller/member_api.go index 126f0d1..cf5a03f 100644 --- a/cmd/dashboard/controller/member_api.go +++ b/cmd/dashboard/controller/member_api.go @@ -259,13 +259,14 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) { } type cronForm struct { - ID uint64 - Name string - Scheduler string - Command string - ServersRaw string - Cover uint8 - PushSuccessful string + ID uint64 + Name string + Scheduler string + Command string + ServersRaw string + Cover uint8 + PushSuccessful string + NotificationTag string } func (ma *memberAPI) addOrEditCron(c *gin.Context) { @@ -278,6 +279,7 @@ func (ma *memberAPI) addOrEditCron(c *gin.Context) { cr.Command = cf.Command cr.ServersRaw = cf.ServersRaw cr.PushSuccessful = cf.PushSuccessful == "on" + cr.NotificationTag = cf.NotificationTag cr.ID = cf.ID cr.Cover = cf.Cover err = utils.Json.Unmarshal([]byte(cf.ServersRaw), &cr.Servers) @@ -376,6 +378,7 @@ func (ma *memberAPI) forceUpdate(c *gin.Context) { type notificationForm struct { ID uint64 Name string + Tag string // 分组名 URL string RequestMethod int RequestType int @@ -390,6 +393,7 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) { err := c.ShouldBindJSON(&nf) if err == nil { n.Name = nf.Name + n.Tag = nf.Tag n.RequestMethod = nf.RequestMethod n.RequestType = nf.RequestType n.RequestHeader = nf.RequestHeader @@ -401,6 +405,10 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) { err = n.Send("这是测试消息") } if err == nil { + // 保证Tag不为空 + if n.Tag == "" { + n.Tag = "default" + } if n.ID == 0 { err = singleton.DB.Create(&n).Error } else { @@ -414,7 +422,7 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) { }) return } - singleton.OnRefreshOrAddNotification(n) + singleton.OnRefreshOrAddNotification(&n) c.JSON(http.StatusOK, model.Response{ Code: http.StatusOK, }) diff --git a/model/config.go b/model/config.go index e7cf6b7..35cdd3d 100644 --- a/model/config.go +++ b/model/config.go @@ -72,6 +72,7 @@ type Config struct { TLS bool EnableIPChangeNotification bool + IPChangeNotificationTag string EnablePlainIPInNotification bool // IP变更提醒 @@ -102,6 +103,9 @@ func (c *Config) Read(path string) error { if c.GRPCPort == 0 { c.GRPCPort = 5555 } + if c.EnableIPChangeNotification && c.IPChangeNotificationTag == "" { + c.IPChangeNotificationTag = "default" + } c.updateIgnoredIPNotificationID() return nil diff --git a/model/cron.go b/model/cron.go index fbdf75a..a8e756c 100644 --- a/model/cron.go +++ b/model/cron.go @@ -15,14 +15,15 @@ const ( type Cron struct { Common - Name string - Scheduler string //分钟 小时 天 月 星期 - Command string - Servers []uint64 `gorm:"-"` - PushSuccessful bool // 推送成功的通知 - LastExecutedAt time.Time // 最后一次执行时间 - LastResult bool // 最后一次执行结果 - Cover uint8 // 计划任务覆盖范围 (0:仅覆盖特定服务器 1:仅忽略特定服务器) + Name string + Scheduler string //分钟 小时 天 月 星期 + Command string + Servers []uint64 `gorm:"-"` + PushSuccessful bool // 推送成功的通知 + NotificationTag string // 指定通知方式的分组 + LastExecutedAt time.Time // 最后一次执行时间 + LastResult bool // 最后一次执行结果 + Cover uint8 // 计划任务覆盖范围 (0:仅覆盖特定服务器 1:仅忽略特定服务器) CronJobID cron.EntryID `gorm:"-"` ServersRaw string diff --git a/model/notification.go b/model/notification.go index 3ea2a9e..fcbfb56 100644 --- a/model/notification.go +++ b/model/notification.go @@ -28,6 +28,7 @@ const ( type Notification struct { Common Name string + Tag string // 分组名 URL string RequestMethod int RequestType int diff --git a/resource/template/component/cron.html b/resource/template/component/cron.html index 3ee3da8..2d3ba9d 100644 --- a/resource/template/component/cron.html +++ b/resource/template/component/cron.html @@ -38,6 +38,10 @@ +
+ + +

diff --git a/resource/template/component/notification.html b/resource/template/component/notification.html index 9ba5a09..408fe78 100644 --- a/resource/template/component/notification.html +++ b/resource/template/component/notification.html @@ -8,6 +8,10 @@

+
+ + +
diff --git a/resource/template/dashboard/cron.html b/resource/template/dashboard/cron.html index 92b862d..78ab201 100644 --- a/resource/template/dashboard/cron.html +++ b/resource/template/dashboard/cron.html @@ -18,6 +18,7 @@ 计划 命令 成功推送 + 通知方式组 覆盖范围 特定服务器 最后执行 @@ -33,6 +34,7 @@ {{$cron.Scheduler}} {{$cron.Command}} {{$cron.PushSuccessful}} + {$cron.NotificationTag} {{if eq $cron.Cover 0}}忽略所有{{else}}覆盖所有{{end}} {{$cron.ServersRaw}} {{$cron.LastExecutedAt|tf}} diff --git a/resource/template/dashboard/notification.html b/resource/template/dashboard/notification.html index 8ebcc84..c48f94d 100644 --- a/resource/template/dashboard/notification.html +++ b/resource/template/dashboard/notification.html @@ -15,6 +15,7 @@ ID 名称 + 分组 URL 验证SSL 管理 @@ -25,6 +26,7 @@ {{$notification.ID}} {{$notification.Name}} + {{$notification.Tag}} {{$notification.URL}} {{$notification.VerifySSL}} diff --git a/service/rpc/nezha.go b/service/rpc/nezha.go index e48a854..2bca2c5 100644 --- a/service/rpc/nezha.go +++ b/service/rpc/nezha.go @@ -29,10 +29,10 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece singleton.ServerLock.RLock() defer singleton.ServerLock.RUnlock() if cr.PushSuccessful && r.GetSuccessful() { - singleton.SendNotification(fmt.Sprintf("[任务成功] %s ,服务器:%s,日志:\n%s", cr.Name, singleton.ServerList[clientID].Name, r.GetData()), false) + singleton.SendNotification(cr.NotificationTag, fmt.Sprintf("[任务成功] %s ,服务器:%s,日志:\n%s", cr.Name, singleton.ServerList[clientID].Name, r.GetData()), false) } if !r.GetSuccessful() { - singleton.SendNotification(fmt.Sprintf("[任务失败] %s ,服务器:%s,日志:\n%s", cr.Name, singleton.ServerList[clientID].Name, r.GetData()), false) + singleton.SendNotification(cr.NotificationTag, fmt.Sprintf("[任务失败] %s ,服务器:%s,日志:\n%s", cr.Name, singleton.ServerList[clientID].Name, r.GetData()), false) } singleton.DB.Model(cr).Updates(model.Cron{ LastExecutedAt: time.Now().Add(time.Second * -1 * time.Duration(r.GetDelay())), @@ -103,7 +103,7 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece singleton.ServerList[clientID].Host.IP != "" && host.IP != "" && singleton.ServerList[clientID].Host.IP != host.IP { - singleton.SendNotification(fmt.Sprintf( + singleton.SendNotification(singleton.Conf.IPChangeNotificationTag, fmt.Sprintf( "[IP变更] %s ,旧IP:%s,新IP:%s。", singleton.ServerList[clientID].Name, singleton.IPDesensitize(singleton.ServerList[clientID].Host.IP), singleton.IPDesensitize(host.IP)), true) } diff --git a/service/singleton/crontask.go b/service/singleton/crontask.go index b4aafe1..eccc940 100644 --- a/service/singleton/crontask.go +++ b/service/singleton/crontask.go @@ -11,7 +11,7 @@ import ( var ( Cron *cron.Cron - Crons map[uint64]*model.Cron + Crons map[uint64]*model.Cron // [CrondID] -> *model.Cron CronLock sync.RWMutex ) @@ -27,9 +27,12 @@ func LoadCronTasks() { DB.Find(&crons) var err error errMsg := new(bytes.Buffer) - for i := 0; i < len(crons); i++ { - cr := crons[i] - + var notificationTagList []string + for _, cr := range crons { + // 旧版本计划任务可能不存在通知组 为其添加默认通知组 + if cr.NotificationTag == "" { + AddDefaultCronNotificationTag(&cr) + } // 注册计划任务 cr.CronJobID, err = Cron.AddFunc(cr.Scheduler, CronTrigger(cr)) if err == nil { @@ -39,15 +42,30 @@ func LoadCronTasks() { errMsg.WriteString("调度失败的计划任务:[") } errMsg.WriteString(fmt.Sprintf("%d,", cr.ID)) + notificationTagList = append(notificationTagList, cr.NotificationTag) } } if errMsg.Len() > 0 { - msg := errMsg.String() - SendNotification(msg[:len(msg)-1]+"] 这些任务将无法正常执行,请进入后点重新修改保存。", false) + msg := errMsg.String() + "] 这些任务将无法正常执行,请进入后点重新修改保存。" + for _, tag := range notificationTagList { + // 向调度错误的计划任务所包含的所有通知组发送通知 + SendNotification(tag, msg, false) + } } Cron.Start() } +// AddDefaultCronNotificationTag 添加默认的计划任务通知组 +func AddDefaultCronNotificationTag(c *model.Cron) { + CronLock.Lock() + defer CronLock.Unlock() + + if c.NotificationTag == "" { + c.NotificationTag = "default" + } + DB.Save(c) +} + func ManualTrigger(c model.Cron) { CronTrigger(c)() } @@ -74,7 +92,7 @@ func CronTrigger(cr model.Cron) func() { Type: model.TaskTypeCommand, }) } else { - SendNotification(fmt.Sprintf("[任务失败] %s,服务器 %s 离线,无法执行。", cr.Name, s.Name), false) + SendNotification(cr.NotificationTag, fmt.Sprintf("[任务失败] %s,服务器 %s 离线,无法执行。", cr.Name, s.Name), false) } } } diff --git a/service/singleton/notification.go b/service/singleton/notification.go index 476403f..caf3e6d 100644 --- a/service/singleton/notification.go +++ b/service/singleton/notification.go @@ -13,46 +13,95 @@ import ( const firstNotificationDelay = time.Minute * 15 // 通知方式 -var notifications []model.Notification -var notificationsLock sync.RWMutex +var ( + NotificationList map[string]map[uint64]*model.Notification // [NotificationMethodTag][NotificationID] -> model.Notification + NotificationIDToTag map[uint64]string // [NotificationID] -> NotificationTag + notificationsLock sync.RWMutex +) -// LoadNotifications 从 DB 加载通知方式到 singleton.notifications 变量 +// InitNotification 初始化 Tag <-> ID <-> Notification 的映射 +func InitNotification() { + NotificationList = make(map[string]map[uint64]*model.Notification) + NotificationIDToTag = make(map[uint64]string) +} + +// LoadNotifications 从 DB 初始化通知方式相关参数 func LoadNotifications() { + InitNotification() notificationsLock.Lock() + defer notificationsLock.Unlock() + + var notifications []model.Notification if err := DB.Find(¬ifications).Error; err != nil { panic(err) } - notificationsLock.Unlock() + for _, n := range notifications { + // 旧版本的Tag可能不存在 自动设置为默认值 + if n.Tag == "" { + SetDefaultNotificationTagInDB(&n) + } + AddNotificationToList(&n) + } } -func OnRefreshOrAddNotification(n model.Notification) { +// SetDefaultNotificationTagInDB 设置默认通知方式的 Tag +func SetDefaultNotificationTagInDB(n *model.Notification) { + n.Tag = "default" + if err := DB.Save(n).Error; err != nil { + log.Println("[ERROR]", err) + } +} + +// OnRefreshOrAddNotification 刷新通知方式相关参数 +func OnRefreshOrAddNotification(n *model.Notification) { notificationsLock.Lock() defer notificationsLock.Unlock() + var isEdit bool - for i := 0; i < len(notifications); i++ { - if notifications[i].ID == n.ID { - notifications[i] = n - isEdit = true - } + if _, ok := NotificationList[n.Tag][n.ID]; ok { + isEdit = true } if !isEdit { - notifications = append(notifications, n) + AddNotificationToList(n) + } else { + UpdateNotificationInList(n) } } +// AddNotificationToList 添加通知方式到map中 +func AddNotificationToList(n *model.Notification) { + notificationsLock.Lock() + defer notificationsLock.Unlock() + + // 当前 Tag 不存在,创建对应该 Tag 的 子 map 后再添加 + if _, ok := NotificationList[n.Tag]; !ok { + NotificationList[n.Tag] = make(map[uint64]*model.Notification) + } + NotificationList[n.Tag][n.ID] = n + NotificationIDToTag[n.ID] = n.Tag + +} + +// UpdateNotificationInList 在 map 中更新通知方式 +func UpdateNotificationInList(n *model.Notification) { + notificationsLock.Lock() + defer notificationsLock.Unlock() + + NotificationList[n.Tag][n.ID] = n +} + +// OnDeleteNotification 在map中删除通知方式 func OnDeleteNotification(id uint64) { notificationsLock.Lock() defer notificationsLock.Unlock() - for i := 0; i < len(notifications); i++ { - if notifications[i].ID == id { - notifications = append(notifications[:i], notifications[i+1:]...) - i-- - } - } + + delete(NotificationList[NotificationIDToTag[id]], id) + delete(NotificationIDToTag, id) } -func SendNotification(desc string, muteable bool) { - if muteable { +// SendNotification 向指定的通知方式组的所有通知方式发送通知 +func SendNotification(notificationTag string, desc string, mutable bool) { + if mutable { // 通知防骚扰策略 nID := hex.EncodeToString(md5.New().Sum([]byte(desc))) // #nosec var flag bool @@ -80,16 +129,17 @@ func SendNotification(desc string, muteable bool) { if !flag { if Conf.Debug { - log.Println("NEZHA>> 静音的重复通知:", desc, muteable) + log.Println("NEZHA>> 静音的重复通知:", desc, mutable) } return } } - // 发出通知 + // 向该通知方式组的所有通知方式发出通知 notificationsLock.RLock() defer notificationsLock.RUnlock() - for i := 0; i < len(notifications); i++ { - if err := notifications[i].Send(desc); err != nil { + + for _, n := range NotificationList[notificationTag] { + if err := n.Send(desc); err != nil { log.Println("NEZHA>> 发送通知失败:", err) } }