From 8fa9df5b3c02a44082bc75eba8dc733f22f58ad1 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 2 Feb 2025 16:39:30 +0800 Subject: [PATCH] feat: enhance email notifications with HTML templates and improved content --- common/message/template.go | 34 ++++++++++++++++++++++++++++++++++ controller/misc.go | 35 ++++++++++++++++++++++++++--------- model/token.go | 29 +++++++++++++++++++++++------ monitor/channel.go | 37 ++++++++++++++++++++++++++++++------- 4 files changed, 113 insertions(+), 22 deletions(-) create mode 100644 common/message/template.go diff --git a/common/message/template.go b/common/message/template.go new file mode 100644 index 00000000..55733725 --- /dev/null +++ b/common/message/template.go @@ -0,0 +1,34 @@ +package message + +import ( + "fmt" + + "github.com/songquanpeng/one-api/common/config" +) + +// EmailTemplate 生成美观的 HTML 邮件内容 +func EmailTemplate(title, content string) string { + return fmt.Sprintf(` + + + + + + + +
+
+

%s

+
+
+ %s +
+
+

此邮件由系统自动发送,请勿直接回复

+

%s

+
+
+ + +`, title, content, config.SystemName) +} diff --git a/controller/misc.go b/controller/misc.go index d3981dc2..75fec8f9 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -116,10 +116,17 @@ func SendEmailVerification(c *gin.Context) { } code := common.GenerateVerificationCode(6) common.RegisterVerificationCodeWithKey(email, code, common.EmailVerificationPurpose) - subject := fmt.Sprintf("%s邮箱验证邮件", config.SystemName) - content := fmt.Sprintf("

您好,你正在进行%s邮箱验证。

"+ - "

您的验证码为: %s

"+ - "

验证码 %d 分钟内有效,如果不是本人操作,请忽略。

", config.SystemName, code, common.VerificationValidMinutes) + subject := fmt.Sprintf("%s 邮箱验证邮件", config.SystemName) + content := message.EmailTemplate( + subject, + fmt.Sprintf(` +

您好!

+

您正在进行 %s 邮箱验证。

+

您的验证码为:

+

%s

+

验证码 %d 分钟内有效,如果不是本人操作,请忽略。

+ `, config.SystemName, code, common.VerificationValidMinutes), + ) err := message.SendEmail(subject, email, content) if err != nil { c.JSON(http.StatusOK, gin.H{ @@ -154,11 +161,21 @@ func SendPasswordResetEmail(c *gin.Context) { code := common.GenerateVerificationCode(0) common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose) link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", config.ServerAddress, email, code) - subject := fmt.Sprintf("%s密码重置", config.SystemName) - content := fmt.Sprintf("

您好,你正在进行%s密码重置。

"+ - "

点击 此处 进行密码重置。

"+ - "

如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:
%s

"+ - "

重置链接 %d 分钟内有效,如果不是本人操作,请忽略。

", config.SystemName, link, link, common.VerificationValidMinutes) + subject := fmt.Sprintf("%s 密码重置", config.SystemName) + content := message.EmailTemplate( + subject, + fmt.Sprintf(` +

您好!

+

您正在进行 %s 密码重置。

+

请点击下面的按钮进行密码重置:

+

+ 重置密码 +

+

如果按钮无法点击,请复制以下链接到浏览器中打开:

+

%s

+

重置链接 %d 分钟内有效,如果不是本人操作,请忽略。

+ `, config.SystemName, link, link, common.VerificationValidMinutes), + ) err := message.SendEmail(subject, email, content) if err != nil { c.JSON(http.StatusOK, gin.H{ diff --git a/model/token.go b/model/token.go index 91e72a82..52ee63ef 100644 --- a/model/token.go +++ b/model/token.go @@ -3,12 +3,14 @@ package model import ( "errors" "fmt" + + "gorm.io/gorm" + "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/common/message" - "gorm.io/gorm" ) const ( @@ -238,16 +240,31 @@ func PreConsumeTokenQuota(tokenId int, quota int64) (err error) { if err != nil { logger.SysError("failed to fetch user email: " + err.Error()) } - prompt := "您的额度即将用尽" + prompt := "额度提醒" + var contentText string if noMoreQuota { - prompt = "您的额度已用尽" + contentText = "您的额度已用尽" + } else { + contentText = "您的额度即将用尽" } if email != "" { topUpLink := fmt.Sprintf("%s/topup", config.ServerAddress) - err = message.SendEmail(prompt, email, - fmt.Sprintf("%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。
充值链接:%s", prompt, userQuota, topUpLink, topUpLink)) + content := message.EmailTemplate( + prompt, + fmt.Sprintf(` +

您好!

+

%s,当前剩余额度为 %d

+

为了不影响您的使用,请及时充值。

+

+ 立即充值 +

+

如果按钮无法点击,请复制以下链接到浏览器中打开:

+

%s

+ `, contentText, userQuota, topUpLink, topUpLink), + ) + err = message.SendEmail(prompt, email, content) if err != nil { - logger.SysError("failed to send email" + err.Error()) + logger.SysError("failed to send email: " + err.Error()) } } }() diff --git a/monitor/channel.go b/monitor/channel.go index 7e5dc58a..a375c8db 100644 --- a/monitor/channel.go +++ b/monitor/channel.go @@ -2,6 +2,7 @@ package monitor import ( "fmt" + "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/common/message" @@ -30,17 +31,32 @@ func notifyRootUser(subject string, content string) { func DisableChannel(channelId int, channelName string, reason string) { model.UpdateChannelStatusById(channelId, model.ChannelStatusAutoDisabled) logger.SysLog(fmt.Sprintf("channel #%d has been disabled: %s", channelId, reason)) - subject := fmt.Sprintf("渠道「%s」(#%d)已被禁用", channelName, channelId) - content := fmt.Sprintf("渠道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason) + subject := fmt.Sprintf("渠道状态变更提醒") + content := message.EmailTemplate( + subject, + fmt.Sprintf(` +

您好!

+

渠道「%s」(#%d)已被禁用。

+

禁用原因:

+

%s

+ `, channelName, channelId, reason), + ) notifyRootUser(subject, content) } func MetricDisableChannel(channelId int, successRate float64) { model.UpdateChannelStatusById(channelId, model.ChannelStatusAutoDisabled) logger.SysLog(fmt.Sprintf("channel #%d has been disabled due to low success rate: %.2f", channelId, successRate*100)) - subject := fmt.Sprintf("渠道 #%d 已被禁用", channelId) - content := fmt.Sprintf("该渠道(#%d)在最近 %d 次调用中成功率为 %.2f%%,低于阈值 %.2f%%,因此被系统自动禁用。", - channelId, config.MetricQueueSize, successRate*100, config.MetricSuccessRateThreshold*100) + subject := fmt.Sprintf("渠道状态变更提醒") + content := message.EmailTemplate( + subject, + fmt.Sprintf(` +

您好!

+

渠道 #%d 已被系统自动禁用。

+

禁用原因:

+

该渠道在最近 %d 次调用中成功率为 %.2f%%,低于系统阈值 %.2f%%

+ `, channelId, config.MetricQueueSize, successRate*100, config.MetricSuccessRateThreshold*100), + ) notifyRootUser(subject, content) } @@ -48,7 +64,14 @@ func MetricDisableChannel(channelId int, successRate float64) { func EnableChannel(channelId int, channelName string) { model.UpdateChannelStatusById(channelId, model.ChannelStatusEnabled) logger.SysLog(fmt.Sprintf("channel #%d has been enabled", channelId)) - subject := fmt.Sprintf("渠道「%s」(#%d)已被启用", channelName, channelId) - content := fmt.Sprintf("渠道「%s」(#%d)已被启用", channelName, channelId) + subject := fmt.Sprintf("渠道状态变更提醒") + content := message.EmailTemplate( + subject, + fmt.Sprintf(` +

您好!

+

渠道「%s」(#%d)已被重新启用。

+

您现在可以继续使用该渠道了。

+ `, channelName, channelId), + ) notifyRootUser(subject, content) }