feat: add message tmplate #76

This commit is contained in:
duorameng
2026-04-07 21:34:36 +08:00
parent 691dda1bae
commit a85e253b45
9 changed files with 443 additions and 24 deletions
+32
View File
@@ -68,6 +68,21 @@ const (
KeyNotifyChannels = "channels"
KeyNotifyEvents = "events"
KeyNotifyToken = "notify_token"
KeyNotifyPrefix = "notify_prefix"
// Notify Templates Keys
KeyNotifyTemplateUserLoginTitle = "notify_template_user_login_title"
KeyNotifyTemplateUserLoginText = "notify_template_user_login_text"
KeyNotifyTemplateBruteForceLoginTitle = "notify_template_brute_force_login_title"
KeyNotifyTemplateBruteForceLoginText = "notify_template_brute_force_login_text"
KeyNotifyTemplatePasswordChangedTitle = "notify_template_password_changed_title"
KeyNotifyTemplatePasswordChangedText = "notify_template_password_changed_text"
KeyNotifyTemplateTaskSuccessTitle = "notify_template_task_success_title"
KeyNotifyTemplateTaskSuccessText = "notify_template_task_success_text"
KeyNotifyTemplateTaskFailedTitle = "notify_template_task_failed_title"
KeyNotifyTemplateTaskFailedText = "notify_template_task_failed_text"
KeyNotifyTemplateTaskTimeoutTitle = "notify_template_task_timeout_title"
KeyNotifyTemplateTaskTimeoutText = "notify_template_task_timeout_text"
// 事件绑定类型
BindingTypeSystem = "system"
@@ -186,4 +201,21 @@ var DefaultSettings = map[string]map[string]string{
KeyQueueSize: "100",
KeyRateInterval: "200",
},
SectionNotify: {
KeyNotifyPrefix: "[白虎面板]",
// Login
KeyNotifyTemplateUserLoginTitle: "用户登录(成功/失败)",
KeyNotifyTemplateUserLoginText: "用户 {{username}} 在 IP {{ip}} 登录{{status_label}}\n{{message}}",
KeyNotifyTemplateBruteForceLoginTitle: "系统安全警告",
KeyNotifyTemplateBruteForceLoginText: "检测到 IP {{ip}} 正在尝试暴力破解用户 {{username}}",
KeyNotifyTemplatePasswordChangedTitle: "账户安全通知",
KeyNotifyTemplatePasswordChangedText: "用户 {{username}} 刚刚修改了密码",
// Task
KeyNotifyTemplateTaskSuccessTitle: "任务[{{task_name}}] 成功",
KeyNotifyTemplateTaskSuccessText: "任务 #{{task_id}} {{task_name}}\n状态: 成功\n耗时: {{duration}}ms\n执行结果: {{output}}",
KeyNotifyTemplateTaskFailedTitle: "任务[{{task_name}}] 失败",
KeyNotifyTemplateTaskFailedText: "任务 #{{task_id}} {{task_name}}\n状态: 失败\n执行时间: {{start_time}}\n原因: {{error}}\n最后输出: {{output}}",
KeyNotifyTemplateTaskTimeoutTitle: "任务[{{task_name}}] 超时",
KeyNotifyTemplateTaskTimeoutText: "任务 #{{task_id}} {{task_name}}\n状态: 超时\n耗时: {{duration}}ms\n最后输出: {{output}}",
},
}
@@ -494,6 +494,39 @@ func (sc *SettingsController) RestoreBackup(c *gin.Context) {
utils.SuccessMsg(c, "恢复成功")
}
// GetSectionSettings 获取指定 section 的所有设置
func (sc *SettingsController) GetSectionSettings(c *gin.Context) {
section := c.Param("section")
if section == "" {
utils.BadRequest(c, "参数错误")
return
}
settings := sc.settingsService.GetSection(section)
utils.Success(c, settings)
}
// UpdateSectionSettings 批量更新指定 section 的设置
func (sc *SettingsController) UpdateSectionSettings(c *gin.Context) {
section := c.Param("section")
if section == "" {
utils.BadRequest(c, "参数错误")
return
}
var values map[string]string
if err := c.ShouldBindJSON(&values); err != nil {
utils.BadRequest(c, "参数错误")
return
}
if err := sc.settingsService.SetSection(section, values); err != nil {
utils.ServerError(c, "更新失败")
return
}
utils.SuccessMsg(c, "保存成功")
}
// GetSetting 获取单个设置值
func (sc *SettingsController) GetSetting(c *gin.Context) {
section := c.Param("section")
+2
View File
@@ -163,6 +163,8 @@ func registerSettingsRoutes(g *gin.RouterGroup, c *Controllers) {
settings.GET("/backup/download", c.Settings.DownloadBackup)
settings.POST("/restore", c.Settings.RestoreBackup)
// 通用设置接口
settings.GET("/:section", c.Settings.GetSectionSettings)
settings.PUT("/:section", c.Settings.UpdateSectionSettings)
settings.GET("/:section/:key", c.Settings.GetSetting)
settings.POST("/:section/:key/generate", c.Settings.GenerateSettingToken)
}
+107 -20
View File
@@ -15,6 +15,7 @@ import (
"github.com/engigu/baihu-panel/internal/sdk/messenger"
"gorm.io/gorm"
"regexp"
"strings"
)
// NotifyChannel 通知渠道配置
@@ -324,6 +325,54 @@ func stripAnsi(str string) string {
return ansiRegexp.ReplaceAllString(str, "")
}
// parseTemplate 简单的 {{key}} 模板替换
func (s *NotificationService) parseTemplate(tmpl string, payload map[string]interface{}) string {
result := tmpl
for k, v := range payload {
placeholder := fmt.Sprintf("{{%s}}", k)
valStr := fmt.Sprintf("%v", v)
result = strings.ReplaceAll(result, placeholder, valStr)
}
return result
}
// getDefaultMessage 兜底默认消息内容
func (s *NotificationService) getDefaultMessage(eventType string, payload map[string]interface{}) (string, string) {
var title, text string
switch eventType {
case constant.EventUserLogin:
status, _ := payload["status"].(string)
if status == "success" {
title = "用户登录成功"
text = fmt.Sprintf("用户 %v 在 IP %v 登录成功", payload["username"], payload["ip"])
} else {
title = "用户登录失败"
reason, _ := payload["message"].(string)
text = fmt.Sprintf("用户 %v 在 IP %v 登录失败\n原因: %v", payload["username"], payload["ip"], reason)
}
case constant.EventBruteForceLogin:
title = "系统安全警告"
text = fmt.Sprintf("检测到 IP %v 正在尝试暴力破解用户 %v", payload["ip"], payload["username"])
case constant.EventPasswordChanged:
title = "账户安全通知"
text = fmt.Sprintf("用户 %v 刚刚修改了密码", payload["username"])
case constant.EventTaskSuccess:
title = fmt.Sprintf("任务[%v] 成功", payload["task_name"])
text = fmt.Sprintf("任务 #%v %v\n状态: 成功\n执行时间: %v\n耗时: %vms", payload["task_id"], payload["task_name"], payload["start_time"], payload["duration"])
case constant.EventTaskFailed:
title = fmt.Sprintf("任务[%v] 失败", payload["task_name"])
if errStr, ok := payload["error"]; ok {
text = fmt.Sprintf("任务 #%v %v\n执行失败\n执行时间: %v\n错误: %v", payload["task_id"], payload["task_name"], payload["start_time"], errStr)
} else {
text = fmt.Sprintf("任务 #%v %v\n执行失败\n状态: %v\n执行时间: %v\n耗时: %vms", payload["task_id"], payload["task_name"], payload["status"], payload["start_time"], payload["duration"])
}
case constant.EventTaskTimeout:
title = fmt.Sprintf("任务[%v] 超时", payload["task_name"])
text = fmt.Sprintf("任务 #%v %v\n执行超时\n执行时间: %v\n耗时: %vms", payload["task_id"], payload["task_name"], payload["start_time"], payload["duration"])
}
return title, text
}
// handleEvent 处理事件订阅并发送通知
func (s *NotificationService) handleEvent(bindingType string) eventbus.Handler {
return func(e eventbus.Event) {
@@ -338,36 +387,52 @@ func (s *NotificationService) handleEvent(bindingType string) eventbus.Handler {
}
var title, text string
// 获取全局前缀和模板配置
prefix := s.settingsService.Get(constant.SectionNotify, constant.KeyNotifyPrefix)
var tmplTitleKey, tmplTextKey string
switch e.Type {
case constant.EventUserLogin:
tmplTitleKey = constant.KeyNotifyTemplateUserLoginTitle
tmplTextKey = constant.KeyNotifyTemplateUserLoginText
// 特殊处理登录状态
status, _ := payload["status"].(string)
if status == "success" {
title = "用户登录成功"
text = fmt.Sprintf("用户 %v 在 IP %v 登录成功", payload["username"], payload["ip"])
payload["status_label"] = "成功"
} else {
title = "用户登录失败"
reason, _ := payload["message"].(string)
text = fmt.Sprintf("用户 %v 在 IP %v 登录失败\n原因: %v", payload["username"], payload["ip"], reason)
payload["status_label"] = "失败"
}
case constant.EventBruteForceLogin:
title = "系统安全警告"
text = fmt.Sprintf("检测到 IP %v 正在尝试暴力破解用户 %v", payload["ip"], payload["username"])
tmplTitleKey = constant.KeyNotifyTemplateBruteForceLoginTitle
tmplTextKey = constant.KeyNotifyTemplateBruteForceLoginText
case constant.EventPasswordChanged:
title = "账户安全通知"
text = fmt.Sprintf("用户 %v 刚刚修改了密码", payload["username"])
case constant.EventTaskSuccess:
title = fmt.Sprintf("任务[%v] 成功", payload["task_name"])
text = fmt.Sprintf("任务 #%v %v\n状态: 成功\n执行时间: %v\n耗时: %vms", payload["task_id"], payload["task_name"], payload["start_time"], payload["duration"])
case constant.EventTaskFailed:
title = fmt.Sprintf("任务[%v] 失败", payload["task_name"])
if errStr, ok := payload["error"]; ok {
text = fmt.Sprintf("任务 #%v %v\n执行失败\n执行时间: %v\n错误: %v", payload["task_id"], payload["task_name"], payload["start_time"], errStr)
tmplTitleKey = constant.KeyNotifyTemplatePasswordChangedTitle
tmplTextKey = constant.KeyNotifyTemplatePasswordChangedText
case constant.EventTaskSuccess, constant.EventTaskFailed, constant.EventTaskTimeout:
if e.Type == constant.EventTaskSuccess {
tmplTitleKey = constant.KeyNotifyTemplateTaskSuccessTitle
tmplTextKey = constant.KeyNotifyTemplateTaskSuccessText
} else if e.Type == constant.EventTaskFailed {
tmplTitleKey = constant.KeyNotifyTemplateTaskFailedTitle
tmplTextKey = constant.KeyNotifyTemplateTaskFailedText
} else {
text = fmt.Sprintf("任务 #%v %v\n执行失败\n状态: %v\n执行时间: %v\n耗时: %vms", payload["task_id"], payload["task_name"], payload["status"], payload["start_time"], payload["duration"])
tmplTitleKey = constant.KeyNotifyTemplateTaskTimeoutTitle
tmplTextKey = constant.KeyNotifyTemplateTaskTimeoutText
}
case constant.EventTaskTimeout:
title = fmt.Sprintf("任务[%v] 超时", payload["task_name"])
text = fmt.Sprintf("任务 #%v %v\n执行超时\n执行时间: %v\n耗时: %vms", payload["task_id"], payload["task_name"], payload["start_time"], payload["duration"])
// 处理输出内容,避免过长
if output, ok := payload["output"].(string); ok {
// 如果输出包含了压缩后的 Base64 (以 "base64:" 开头),由于是推送到通知,我们尽量不发大段 Base64
// 这里简单处理:如果过长则截断,或者如果是压缩的则记录一下
if len(output) > 1000 {
payload["output"] = output[len(output)-1000:] + "\n...(截断)"
}
}
case constant.EventSystemNotice:
title, _ = payload["title"].(string)
text, _ = payload["content"].(string)
@@ -375,6 +440,28 @@ func (s *NotificationService) handleEvent(bindingType string) eventbus.Handler {
return
}
if tmplTitleKey != "" {
tmplTitle := s.settingsService.Get(constant.SectionNotify, tmplTitleKey)
tmplText := s.settingsService.Get(constant.SectionNotify, tmplTextKey)
if tmplTitle != "" {
title = s.parseTemplate(tmplTitle, payload)
}
if tmplText != "" {
text = s.parseTemplate(tmplText, payload)
}
// 如果模板为空,使用兜底默认逻辑(保持向上兼容)
if title == "" || text == "" {
title, text = s.getDefaultMessage(e.Type, payload)
}
}
// 添加全局前缀
if prefix != "" {
title = fmt.Sprintf("%s %s", prefix, title)
}
bindings := s.GetBindingsByEvent(bindingType, e.Type, dataID)
if len(bindings) == 0 {
return
+11
View File
@@ -87,6 +87,17 @@ func (s *SettingsService) InitSettings() error {
}
}
// 从 constant.DefaultSettings 初始化所有缺少的通知模板
if notifyDefaults, ok := constant.DefaultSettings[constant.SectionNotify]; ok {
for k, v := range notifyDefaults {
var count int64
database.DB.Model(&models.Setting{}).Where(&models.Setting{Section: constant.SectionNotify, Key: k}).Count(&count)
if count == 0 {
s.Set(constant.SectionNotify, k, v)
}
}
}
// 初始化或获取 JWT Secret 密码
var secCount int64
database.DB.Model(&models.Setting{}).Where(&models.Setting{Section: constant.SectionSecurity, Key: constant.KeySecret}).Count(&secCount)