✨ graph api发送邮件
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
module likedream-goframe
|
||||
|
||||
go 1.24.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.5
|
||||
@@ -28,10 +28,12 @@ require (
|
||||
github.com/wenlng/go-captcha-assets v1.0.7
|
||||
github.com/wenlng/go-captcha/v2 v2.0.4
|
||||
github.com/wneessen/go-mail v0.6.2
|
||||
github.com/xuri/excelize/v2 v2.10.0
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/net v0.46.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
resty.dev/v3 v3.0.0-beta.3
|
||||
@@ -100,7 +102,6 @@ require (
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
github.com/xuri/excelize/v2 v2.10.0 // indirect
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
||||
|
||||
@@ -200,8 +200,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -281,10 +281,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
+15
-17
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/controller/agent"
|
||||
"likedream-goframe/internal/controller/balance"
|
||||
@@ -26,56 +27,53 @@ import (
|
||||
|
||||
func StartServer(ctx context.Context) {
|
||||
s := g.Server()
|
||||
// 设置端口
|
||||
|
||||
port, err := g.Cfg().Get(ctx, cst.ConfigServerPort)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
s.SetNameToUriType(3)
|
||||
// 设置中间件
|
||||
s.Use(
|
||||
middleware.ExceptionHandler,
|
||||
)
|
||||
s.Use(middleware.ExceptionHandler)
|
||||
s.BindStatusHandler(404, common.NewBaseV1().NotFound)
|
||||
// 业务路由
|
||||
|
||||
s.Group("/v1", func(group *ghttp.RouterGroup) {
|
||||
group.Bind(user_login.NewUserLoginV1())
|
||||
// 系统设置路由
|
||||
group.Bind(config.NewMailOAuthCtrlV1())
|
||||
|
||||
systemGroup := group.Group("/system").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
systemGroup.Bind(role.NewV1())
|
||||
systemGroup.Bind(menu.NewV1())
|
||||
systemGroup.Bind(user.NewUserCtrlV1())
|
||||
systemGroup.Bind(config.NewConfigCtrlV1())
|
||||
systemGroup.Bind(config.NewAIConfigCtrlV1())
|
||||
//systemGroup.Bind(agent.NewV1())
|
||||
// 用户信息路由
|
||||
|
||||
userGroup := group.Group("/user").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
userGroup.Bind(user_login.NewUserInfoV1())
|
||||
userGroup.Bind(balance.NewBalanceCtrlV1())
|
||||
// 代理信息路由
|
||||
|
||||
agentGroup := group.Group("/agent").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
agentGroup.Bind(agent.NewV1())
|
||||
agentGroup.Bind(card.NewCardCtrlV1())
|
||||
// 今日校园
|
||||
|
||||
cpdailyGroup := group.Group("/cpdaily").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
cpdailyGroup.Bind(cpdaily.NewCpUserCtrlV1())
|
||||
cpdailyGroup.Bind(cpdaily.NewCpTaskCtrlV1())
|
||||
cpdailyGroup.Bind(cpdaily.NewCpImageCtrlV1())
|
||||
cpdailyGroup.Bind(cpdaily.NewCpTaskLogCtrlV1())
|
||||
cpdailyGroup.Bind(cpdaily.NewCpLeaveCtrlV1())
|
||||
// 娱乐
|
||||
|
||||
musicGroup := group.Group("/music").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
musicGroup.Bind(music.NewMusicCtrlV1())
|
||||
// 网课
|
||||
|
||||
courseGroup := group.Group("/course").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
|
||||
courseGroup.Bind(course.NewXxtCtrlV1())
|
||||
courseGroup.Bind(course.NewSmartEduSCCtrlV1())
|
||||
})
|
||||
|
||||
s.SetPort(port.Int())
|
||||
|
||||
s.GetOpenApi().Config.IgnorePkgPath = true
|
||||
// 设置全局异常捕获
|
||||
e := s.Start()
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
err = s.Start()
|
||||
utils.OkOrPanic(err, oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
func StopServer() {
|
||||
|
||||
@@ -24,26 +24,62 @@ type ConfigListReq struct {
|
||||
}
|
||||
|
||||
type ConfigPublicGetReq struct {
|
||||
g.Meta `path:"/config/public/{key}" method:"get" tags:"Config" summary:"获取公开配置(无需认证)" auth:"false"`
|
||||
g.Meta `path:"/config/public/{key}" method:"get" tags:"Config" summary:"获取公开配置" auth:"false"`
|
||||
Key string `json:"key" v:"required#配置键不能为空"`
|
||||
}
|
||||
|
||||
// SMTP 配置
|
||||
type SmtpConfigGetReq struct {
|
||||
g.Meta `path:"/config/smtp" method:"get" tags:"Config" summary:"获取SMTP配置" permission:"sys:config:edit"`
|
||||
g.Meta `path:"/config/smtp" method:"get" tags:"Config" summary:"获取邮件配置" permission:"sys:config:edit"`
|
||||
}
|
||||
|
||||
type SmtpConfigUpdateReq struct {
|
||||
g.Meta `path:"/config/smtp" method:"put" tags:"Config" summary:"更新SMTP配置" permission:"sys:config:edit"`
|
||||
SmtpHost string `json:"smtpHost" v:"required#SMTP服务器地址不能为空"`
|
||||
SmtpPort int `json:"smtpPort" v:"required#SMTP端口不能为空"`
|
||||
SmtpUsername string `json:"smtpUsername" v:"required#SMTP用户名不能为空"`
|
||||
SmtpPassword string `json:"smtpPassword" v:"required#SMTP密码不能为空"`
|
||||
FromEmail string `json:"fromEmail" v:"required#发件人邮箱不能为空"`
|
||||
g.Meta `path:"/config/smtp" method:"put" tags:"Config" summary:"更新邮件配置" permission:"sys:config:edit"`
|
||||
|
||||
AuthMode string `json:"authMode" v:"required#请选择邮件认证模式"`
|
||||
|
||||
SmtpHost string `json:"smtpHost"`
|
||||
SmtpPort int `json:"smtpPort"`
|
||||
SmtpUsername string `json:"smtpUsername"`
|
||||
SmtpPassword string `json:"smtpPassword"`
|
||||
|
||||
FromEmail string `json:"fromEmail" v:"required|email#发件人邮箱不能为空|请输入正确的发件人邮箱"`
|
||||
FromName string `json:"fromName" v:"required#发件人名称不能为空"`
|
||||
|
||||
OAuthTenant string `json:"oauthTenant"`
|
||||
OAuthClientID string `json:"oauthClientId"`
|
||||
OAuthClientSecret string `json:"oauthClientSecret"`
|
||||
OAuthRedirectURL string `json:"oauthRedirectUrl"`
|
||||
OAuthRefreshToken string `json:"oauthRefreshToken"`
|
||||
OAuthScope string `json:"oauthScope"`
|
||||
}
|
||||
|
||||
type SmtpTestReq struct {
|
||||
g.Meta `path:"/config/smtp/test" method:"post" tags:"Config" summary:"发送测试邮件" permission:"sys:config:edit"`
|
||||
ToEmail string `json:"toEmail" v:"required|email#收件人邮箱不能为空|请输入有效邮箱"`
|
||||
ToEmail string `json:"toEmail" v:"required|email#收件人邮箱不能为空|请输入正确的收件人邮箱"`
|
||||
}
|
||||
|
||||
type OutlookOAuthAuthorizeURLReq struct {
|
||||
g.Meta `path:"/config/smtp/oauth/outlook/authorize-url" method:"post" tags:"Config" summary:"获取 Outlook OAuth2 授权地址" permission:"sys:config:edit"`
|
||||
|
||||
OAuthTenant string `json:"oauthTenant"`
|
||||
OAuthClientID string `json:"oauthClientId" v:"required#Client ID 不能为空"`
|
||||
OAuthClientSecret string `json:"oauthClientSecret" v:"required#Client Secret 不能为空"`
|
||||
OAuthScope string `json:"oauthScope"`
|
||||
FromEmail string `json:"fromEmail" v:"required|email#发件人邮箱不能为空|请输入正确的发件人邮箱"`
|
||||
FrontendOrigin string `json:"frontendOrigin" v:"required#前端来源地址不能为空"`
|
||||
CallbackBaseURL string `json:"callbackBaseUrl" v:"required#API 基础地址不能为空"`
|
||||
}
|
||||
|
||||
type OutlookOAuthAuthorizeURLResp struct {
|
||||
AuthURL string `json:"authUrl"`
|
||||
CallbackURL string `json:"callbackUrl"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type OutlookOAuthCallbackReq struct {
|
||||
g.Meta `path:"/mail/oauth/outlook/callback" method:"get" tags:"MailOAuth" summary:"Outlook OAuth2 回调" auth:"false"`
|
||||
Code string `json:"code"`
|
||||
State string `json:"state"`
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
@@ -28,22 +28,26 @@ const (
|
||||
|
||||
ConfigTaskConcurrency = "task.concurrency"
|
||||
|
||||
// Email配置常量
|
||||
ConfigEmailAuthMode = "email.authMode"
|
||||
ConfigEmailSMTPHost = "email.smtpHost"
|
||||
ConfigEmailSMTPPort = "email.smtpPort"
|
||||
ConfigEmailSMTPUsername = "email.smtpUsername"
|
||||
ConfigEmailSMTPPassword = "email.smtpPassword"
|
||||
ConfigEmailFromEmail = "email.fromEmail"
|
||||
ConfigEmailFromName = "email.fromName"
|
||||
ConfigEmailOAuthTenant = "email.oauthTenant"
|
||||
ConfigEmailOAuthClientID = "email.oauthClientId"
|
||||
ConfigEmailOAuthClientSecret = "email.oauthClientSecret"
|
||||
ConfigEmailOAuthRedirectURL = "email.oauthRedirectUrl"
|
||||
ConfigEmailOAuthRefreshToken = "email.oauthRefreshToken"
|
||||
ConfigEmailOAuthScope = "email.oauthScope"
|
||||
|
||||
// 基础积分配置
|
||||
ConfigBasePointCpdailyTask = "basePoint.cpdailyTask"
|
||||
ConfigBasePointCpdailyLeave = "basePoint.cpdailyLeave"
|
||||
ConfigBasePointCpdailyPhone = "basePoint.cpdailyPhone"
|
||||
|
||||
// 系统配置
|
||||
ConfigSystemAllowRegister = "system.allowRegister" // 是否允许注册
|
||||
ConfigSystemRequireInviteCode = "system.requireInviteCode" // 是否必须邀请码
|
||||
ConfigSystemAllowRegister = "system.allowRegister"
|
||||
ConfigSystemRequireInviteCode = "system.requireInviteCode"
|
||||
|
||||
ConfigRedisCpdailyTask = "price.cpdaily_task"
|
||||
ConfigRedisCpdailyLeave = "price.cpdaily_leave"
|
||||
@@ -51,15 +55,13 @@ const (
|
||||
ConfigRedisXxt = "price.xxt"
|
||||
ConfigRedisSmarteduSc = "price.smartedu_sc"
|
||||
|
||||
// 第三方题库配置
|
||||
ConfigQuesBankUrl = "quesbank.url"
|
||||
ConfigQuesBankKey = "quesbank.key"
|
||||
)
|
||||
|
||||
const (
|
||||
// 应用程序常量
|
||||
AppName = "若梦系统"
|
||||
AppCopyright = "© 2025 若梦系统. 保留所有权利."
|
||||
AppCopyright = "© 2025 若梦系统. 保留所有权利"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -2,14 +2,14 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/service"
|
||||
"likedream-goframe/internal/utils"
|
||||
)
|
||||
|
||||
type ConfigCtrlV1 struct {
|
||||
}
|
||||
type ConfigCtrlV1 struct{}
|
||||
|
||||
func (c ConfigCtrlV1) Get(ctx context.Context, req *v1.ConfigGetReq) (res *utils.Response[*ent.SysConfig], err error) {
|
||||
defer utils.Pass(&err)
|
||||
@@ -31,13 +31,11 @@ func (c ConfigCtrlV1) Update(ctx context.Context, req *v1.ConfigUpdateReq) (res
|
||||
return service.Global.Config.Update(ctx, req), nil
|
||||
}
|
||||
|
||||
// GetPriceConfig 获取费率配置
|
||||
func (c ConfigCtrlV1) GetPriceConfig(ctx context.Context, req *v1.PriceConfigGetReq) (res *utils.Response[*v1.PriceConfigResp], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.Config.GetPriceConfig(ctx, req), nil
|
||||
}
|
||||
|
||||
// UpdatePriceConfig 更新费率配置
|
||||
func (c ConfigCtrlV1) UpdatePriceConfig(ctx context.Context, req *v1.PriceConfigUpdateReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.Config.UpdatePriceConfig(ctx, req), nil
|
||||
@@ -47,20 +45,22 @@ func NewConfigCtrlV1() IConfigCtrlV1 {
|
||||
return &ConfigCtrlV1{}
|
||||
}
|
||||
|
||||
// GetSmtpConfig 获取SMTP配置
|
||||
func (c ConfigCtrlV1) GetSmtpConfig(ctx context.Context, req *v1.SmtpConfigGetReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.Config.GetSmtpConfig(ctx, req), nil
|
||||
}
|
||||
|
||||
// UpdateSmtpConfig 更新SMTP配置
|
||||
func (c ConfigCtrlV1) UpdateSmtpConfig(ctx context.Context, req *v1.SmtpConfigUpdateReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.Config.UpdateSmtpConfig(ctx, req), nil
|
||||
}
|
||||
|
||||
// TestSmtp 发送测试邮件
|
||||
func (c ConfigCtrlV1) TestSmtp(ctx context.Context, req *v1.SmtpTestReq) (res *utils.Response[any], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.Config.TestSmtp(ctx, req), nil
|
||||
}
|
||||
|
||||
func (c ConfigCtrlV1) GetOutlookOAuthAuthorizeURL(ctx context.Context, req *v1.OutlookOAuthAuthorizeURLReq) (res *utils.Response[*v1.OutlookOAuthAuthorizeURLResp], err error) {
|
||||
defer utils.Pass(&err)
|
||||
return service.Global.Config.GetOutlookOAuthAuthorizeURL(ctx, req), nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/utils"
|
||||
@@ -17,4 +18,5 @@ type IConfigCtrlV1 interface {
|
||||
GetSmtpConfig(ctx context.Context, req *v1.SmtpConfigGetReq) (*utils.Response[any], error)
|
||||
UpdateSmtpConfig(ctx context.Context, req *v1.SmtpConfigUpdateReq) (*utils.Response[any], error)
|
||||
TestSmtp(ctx context.Context, req *v1.SmtpTestReq) (*utils.Response[any], error)
|
||||
GetOutlookOAuthAuthorizeURL(ctx context.Context, req *v1.OutlookOAuthAuthorizeURLReq) (*utils.Response[*v1.OutlookOAuthAuthorizeURLResp], error)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
mailservice "likedream-goframe/internal/service/email"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
type MailOAuthCtrlV1 struct{}
|
||||
|
||||
func NewMailOAuthCtrlV1() *MailOAuthCtrlV1 {
|
||||
return &MailOAuthCtrlV1{}
|
||||
}
|
||||
|
||||
func (c *MailOAuthCtrlV1) OutlookCallback(ctx context.Context, req *v1.OutlookOAuthCallbackReq) (res *utils.Response[any], err error) {
|
||||
r := ghttp.RequestFromCtx(ctx)
|
||||
r.Response.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
if req.Error != "" {
|
||||
r.Response.Write(mailservice.BuildOutlookOAuthErrorHTML(fmt.Sprintf("%s: %s", req.Error, req.ErrorDescription)))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
refreshToken, frontendOrigin, exchangeErr := mailservice.NewV1().ExchangeOutlookAuthorizeCode(ctx, req.State, req.Code)
|
||||
if exchangeErr != nil {
|
||||
r.Response.Write(mailservice.BuildOutlookOAuthPopupHTML("*", false, exchangeErr.Error(), ""))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
r.Response.Write(mailservice.BuildOutlookOAuthPopupHTML(frontendOrigin, true, "授权成功", refreshToken))
|
||||
return nil, nil
|
||||
}
|
||||
@@ -4,10 +4,21 @@ type SendPayload struct {
|
||||
To string
|
||||
Subject string
|
||||
Body string
|
||||
|
||||
AuthMode string
|
||||
|
||||
FromName string
|
||||
FromEmail string
|
||||
|
||||
SMTPHost string
|
||||
SMTPPort int
|
||||
SMTPUsername string
|
||||
SMTPPassword string
|
||||
|
||||
OAuthTenant string
|
||||
OAuthClientID string
|
||||
OAuthClientSecret string
|
||||
OAuthRedirectURL string
|
||||
OAuthRefreshToken string
|
||||
OAuthScope string
|
||||
}
|
||||
|
||||
@@ -4,58 +4,41 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/framework/model/email"
|
||||
payloadmodel "likedream-goframe/internal/framework/model/email"
|
||||
mailservice "likedream-goframe/internal/service/email"
|
||||
"likedream-goframe/internal/utils"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/samber/oops"
|
||||
"github.com/wneessen/go-mail"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type EmailTaskHandler struct {
|
||||
}
|
||||
type EmailTaskHandler struct{}
|
||||
|
||||
func (h *EmailTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task) (err error) {
|
||||
defer utils.Pass(&err)
|
||||
var payload email.SendPayload
|
||||
e := gob.NewDecoder(bytes.NewReader(task.Payload())).Decode(&payload)
|
||||
|
||||
var payload payloadmodel.SendPayload
|
||||
if e := gob.NewDecoder(bytes.NewReader(task.Payload())).Decode(&payload); e != nil {
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
logger.Logger.Info("开始处理邮件发送", zap.String("邮箱", payload.To))
|
||||
}
|
||||
|
||||
logger.Logger.Info("start sending email", zap.String("to", payload.To))
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// 打印日志
|
||||
logger.Logger.Error("邮件发送失败", zap.String("邮箱", payload.To), zap.Error(r.(error)))
|
||||
// 重新抛出相同的 panic
|
||||
logger.Logger.Error("send email failed", zap.String("to", payload.To), zap.Error(r.(error)))
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
|
||||
message := mail.NewMsg()
|
||||
|
||||
e = message.FromFormat(payload.FromName, payload.FromEmail)
|
||||
svc := mailservice.NewV1()
|
||||
cfg := mailservice.ConfigFromPayload(payload)
|
||||
if e := svc.SendMessageNow(ctx, cfg, payload.To, payload.Subject, payload.Body); e != nil {
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
e = message.AddTo(payload.To)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
message.Subject(payload.Subject)
|
||||
|
||||
message.SetBodyString(mail.TypeTextHTML, payload.Body)
|
||||
|
||||
client, e := mail.NewClient(payload.SMTPHost, mail.WithPort(payload.SMTPPort),
|
||||
mail.WithSMTPAuth(mail.SMTPAuthLogin), // Outlook 使用 LOGIN 认证
|
||||
mail.WithTLSPortPolicy(mail.TLSMandatory),
|
||||
mail.WithTimeout(30*time.Second), // 连接超时 30 秒
|
||||
mail.WithUsername(payload.SMTPUsername), mail.WithPassword(payload.SMTPPassword))
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
defer utils.LogError(ctx, "关闭邮件客户端失败", client.Close())
|
||||
|
||||
e = client.DialAndSendWithContext(ctx, message)
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
logger.Logger.Info("邮件发送完成", zap.String("邮箱", payload.To))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Logger.Info("email sent", zap.String("to", payload.To))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,45 +3,37 @@ package config
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
v1 "likedream-goframe/internal/api/v1"
|
||||
cst2 "likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/common/db"
|
||||
errors2 "likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/ent"
|
||||
"likedream-goframe/internal/ent/sysconfig"
|
||||
"likedream-goframe/internal/service/email"
|
||||
mailservice "likedream-goframe/internal/service/email"
|
||||
"likedream-goframe/internal/utils"
|
||||
"time"
|
||||
|
||||
mail "github.com/wneessen/go-mail"
|
||||
)
|
||||
|
||||
type ConfigServiceV1 struct {
|
||||
}
|
||||
type ConfigServiceV1 struct{}
|
||||
|
||||
func NewV1() *ConfigServiceV1 {
|
||||
return &ConfigServiceV1{}
|
||||
}
|
||||
|
||||
// Get 获取配置
|
||||
func (s *ConfigServiceV1) Get(ctx context.Context, req *v1.ConfigGetReq) *utils.Response[*ent.SysConfig] {
|
||||
config, err := db.Global.SysConfig.Query().
|
||||
Where(sysconfig.KeyEQ(req.Key)).
|
||||
Only(ctx)
|
||||
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(404, "配置不存在"))
|
||||
}
|
||||
utils.OkOrPanic(err)
|
||||
}
|
||||
|
||||
return utils.SuccessWithData[*ent.SysConfig](config)
|
||||
}
|
||||
|
||||
// PublicGet 获取公开配置(无需认证)
|
||||
func (s *ConfigServiceV1) PublicGet(ctx context.Context, req *v1.ConfigPublicGetReq) *utils.Response[*ent.SysConfig] {
|
||||
// 只允许获取特定的公开配置
|
||||
allowedKeys := []string{"system.announcement", "system.allowRegister", "system.requireInviteCode"}
|
||||
|
||||
allowed := false
|
||||
@@ -51,7 +43,6 @@ func (s *ConfigServiceV1) PublicGet(ctx context.Context, req *v1.ConfigPublicGet
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(403, "该配置不允许公开访问"))
|
||||
}
|
||||
@@ -59,10 +50,8 @@ func (s *ConfigServiceV1) PublicGet(ctx context.Context, req *v1.ConfigPublicGet
|
||||
config, err := db.Global.SysConfig.Query().
|
||||
Where(sysconfig.KeyEQ(req.Key)).
|
||||
Only(ctx)
|
||||
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
// 如果配置不存在,返回空值
|
||||
return utils.SuccessWithData[*ent.SysConfig](nil)
|
||||
}
|
||||
utils.OkOrPanic(err)
|
||||
@@ -71,7 +60,6 @@ func (s *ConfigServiceV1) PublicGet(ctx context.Context, req *v1.ConfigPublicGet
|
||||
return utils.SuccessWithData[*ent.SysConfig](config)
|
||||
}
|
||||
|
||||
// List 获取配置列表
|
||||
func (s *ConfigServiceV1) List(ctx context.Context, req *v1.ConfigListReq) *utils.Response[[]*ent.SysConfig] {
|
||||
query := db.Global.SysConfig.Query().
|
||||
Order(ent.Asc(sysconfig.FieldKey))
|
||||
@@ -89,37 +77,30 @@ func (s *ConfigServiceV1) List(ctx context.Context, req *v1.ConfigListReq) *util
|
||||
return utils.SuccessWithTotal[[]*ent.SysConfig](data, total)
|
||||
}
|
||||
|
||||
// Update 更新配置
|
||||
func (s *ConfigServiceV1) Update(ctx context.Context, req *v1.ConfigUpdateReq) *utils.Response[any] {
|
||||
// 检查配置是否存在
|
||||
config, err := db.Global.SysConfig.Query().
|
||||
Where(sysconfig.KeyEQ(req.Key)).
|
||||
Only(ctx)
|
||||
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
// 如果不存在,创建新配置
|
||||
db.Global.SysConfig.Create().
|
||||
SetKey(req.Key).
|
||||
SetValue(req.Value).
|
||||
SetDescription(req.Description).
|
||||
SetType("markdown"). // 默认为 markdown 类型
|
||||
SetType("markdown").
|
||||
SaveX(ctx)
|
||||
return utils.SuccessWithMsg("配置创建成功")
|
||||
}
|
||||
utils.OkOrPanic(err)
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
db.Global.SysConfig.UpdateOne(config).
|
||||
SetValue(req.Value).
|
||||
SetDescription(req.Description).
|
||||
ExecX(ctx)
|
||||
|
||||
return utils.SuccessWithMsg("配置更新成功")
|
||||
}
|
||||
|
||||
// 费率配置键常量
|
||||
const (
|
||||
PriceKeyCpdailyTask = "price.cpdaily_task"
|
||||
PriceKeyCpdailyLeave = "price.cpdaily_leave"
|
||||
@@ -128,7 +109,6 @@ const (
|
||||
PriceKeySmarteduSc = "price.smartedu_sc"
|
||||
)
|
||||
|
||||
// GetPriceConfig 获取费率配置
|
||||
func (s *ConfigServiceV1) GetPriceConfig(ctx context.Context, req *v1.PriceConfigGetReq) *utils.Response[*v1.PriceConfigResp] {
|
||||
resp := &v1.PriceConfigResp{
|
||||
CpdailyTask: s.getConfigValue(ctx, PriceKeyCpdailyTask, "0.5"),
|
||||
@@ -140,7 +120,6 @@ func (s *ConfigServiceV1) GetPriceConfig(ctx context.Context, req *v1.PriceConfi
|
||||
return utils.SuccessWithData[*v1.PriceConfigResp](resp)
|
||||
}
|
||||
|
||||
// getConfigValue 获取配置值,不存在则返回默认值
|
||||
func (s *ConfigServiceV1) getConfigValue(ctx context.Context, key, defaultValue string) string {
|
||||
config, err := db.Global.SysConfig.Query().
|
||||
Where(sysconfig.KeyEQ(key)).
|
||||
@@ -151,9 +130,7 @@ func (s *ConfigServiceV1) getConfigValue(ctx context.Context, key, defaultValue
|
||||
return config.Value
|
||||
}
|
||||
|
||||
// UpdatePriceConfig 更新费率配置
|
||||
func (s *ConfigServiceV1) UpdatePriceConfig(ctx context.Context, req *v1.PriceConfigUpdateReq) *utils.Response[any] {
|
||||
// 更新各项费率配置
|
||||
if req.CpdailyTask != "" {
|
||||
s.upsertConfig(ctx, PriceKeyCpdailyTask, req.CpdailyTask, "今日校园签到单价")
|
||||
}
|
||||
@@ -172,12 +149,10 @@ func (s *ConfigServiceV1) UpdatePriceConfig(ctx context.Context, req *v1.PriceCo
|
||||
return utils.SuccessWithMsg("费率配置更新成功")
|
||||
}
|
||||
|
||||
// upsertConfig 更新或创建配置
|
||||
func (s *ConfigServiceV1) upsertConfig(ctx context.Context, key, value, description string) {
|
||||
config, err := db.Global.SysConfig.Query().
|
||||
Where(sysconfig.KeyEQ(key)).
|
||||
Only(ctx)
|
||||
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
db.Global.SysConfig.Create().
|
||||
@@ -196,76 +171,154 @@ func (s *ConfigServiceV1) upsertConfig(ctx context.Context, key, value, descript
|
||||
ExecX(ctx)
|
||||
}
|
||||
|
||||
// GetSmtpConfig 获取SMTP配置
|
||||
func (s *ConfigServiceV1) GetSmtpConfig(ctx context.Context, req *v1.SmtpConfigGetReq) *utils.Response[any] {
|
||||
keys := []string{
|
||||
cst2.ConfigEmailAuthMode,
|
||||
cst2.ConfigEmailSMTPHost,
|
||||
cst2.ConfigEmailSMTPPort,
|
||||
cst2.ConfigEmailSMTPUsername,
|
||||
cst2.ConfigEmailSMTPPassword,
|
||||
cst2.ConfigEmailFromEmail,
|
||||
cst2.ConfigEmailFromName,
|
||||
cst2.ConfigEmailOAuthTenant,
|
||||
cst2.ConfigEmailOAuthClientID,
|
||||
cst2.ConfigEmailOAuthClientSecret,
|
||||
cst2.ConfigEmailOAuthRedirectURL,
|
||||
cst2.ConfigEmailOAuthRefreshToken,
|
||||
cst2.ConfigEmailOAuthScope,
|
||||
}
|
||||
result := make(map[string]string)
|
||||
|
||||
result := make(map[string]string, len(keys))
|
||||
for _, key := range keys {
|
||||
config, err := db.Global.SysConfig.Query().Where(sysconfig.KeyEQ(key)).Only(ctx)
|
||||
if err == nil {
|
||||
result[key] = config.Value
|
||||
}
|
||||
}
|
||||
return utils.SuccessWithData[any](map[string]string{
|
||||
"smtpHost": result[cst2.ConfigEmailSMTPHost],
|
||||
"smtpPort": result[cst2.ConfigEmailSMTPPort],
|
||||
"smtpUsername": result[cst2.ConfigEmailSMTPUsername],
|
||||
"smtpPassword": result[cst2.ConfigEmailSMTPPassword],
|
||||
"fromEmail": result[cst2.ConfigEmailFromEmail],
|
||||
"fromName": result[cst2.ConfigEmailFromName],
|
||||
|
||||
cfg := mailservice.NormalizeConfig(mailservice.EmailConfig{
|
||||
AuthMode: result[cst2.ConfigEmailAuthMode],
|
||||
SMTPHost: result[cst2.ConfigEmailSMTPHost],
|
||||
SMTPPort: atoiOrZero(result[cst2.ConfigEmailSMTPPort]),
|
||||
SMTPUsername: result[cst2.ConfigEmailSMTPUsername],
|
||||
SMTPPassword: result[cst2.ConfigEmailSMTPPassword],
|
||||
FromEmail: result[cst2.ConfigEmailFromEmail],
|
||||
FromName: result[cst2.ConfigEmailFromName],
|
||||
OAuthTenant: result[cst2.ConfigEmailOAuthTenant],
|
||||
OAuthClientID: result[cst2.ConfigEmailOAuthClientID],
|
||||
OAuthClientSecret: result[cst2.ConfigEmailOAuthClientSecret],
|
||||
OAuthRedirectURL: result[cst2.ConfigEmailOAuthRedirectURL],
|
||||
OAuthRefreshToken: result[cst2.ConfigEmailOAuthRefreshToken],
|
||||
OAuthScope: result[cst2.ConfigEmailOAuthScope],
|
||||
})
|
||||
|
||||
return utils.SuccessWithData[any](map[string]any{
|
||||
"authMode": cfg.AuthMode,
|
||||
"smtpHost": cfg.SMTPHost,
|
||||
"smtpPort": cfg.SMTPPort,
|
||||
"smtpUsername": cfg.SMTPUsername,
|
||||
"smtpPassword": cfg.SMTPPassword,
|
||||
"fromEmail": cfg.FromEmail,
|
||||
"fromName": cfg.FromName,
|
||||
"oauthTenant": cfg.OAuthTenant,
|
||||
"oauthClientId": cfg.OAuthClientID,
|
||||
"oauthClientSecret": cfg.OAuthClientSecret,
|
||||
"oauthRedirectUrl": cfg.OAuthRedirectURL,
|
||||
"oauthRefreshToken": cfg.OAuthRefreshToken,
|
||||
"oauthScope": cfg.OAuthScope,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateSmtpConfig 更新SMTP配置
|
||||
func (s *ConfigServiceV1) UpdateSmtpConfig(ctx context.Context, req *v1.SmtpConfigUpdateReq) *utils.Response[any] {
|
||||
configs := map[string]string{
|
||||
cst2.ConfigEmailSMTPHost: req.SmtpHost,
|
||||
cst2.ConfigEmailSMTPPort: fmt.Sprintf("%d", req.SmtpPort),
|
||||
cst2.ConfigEmailSMTPUsername: req.SmtpUsername,
|
||||
cst2.ConfigEmailSMTPPassword: req.SmtpPassword,
|
||||
cst2.ConfigEmailFromEmail: req.FromEmail,
|
||||
cst2.ConfigEmailFromName: req.FromName,
|
||||
cfg := mailservice.NormalizeConfig(mailservice.EmailConfig{
|
||||
AuthMode: req.AuthMode,
|
||||
SMTPHost: req.SmtpHost,
|
||||
SMTPPort: req.SmtpPort,
|
||||
SMTPUsername: req.SmtpUsername,
|
||||
SMTPPassword: req.SmtpPassword,
|
||||
FromEmail: req.FromEmail,
|
||||
FromName: req.FromName,
|
||||
OAuthTenant: req.OAuthTenant,
|
||||
OAuthClientID: req.OAuthClientID,
|
||||
OAuthClientSecret: req.OAuthClientSecret,
|
||||
OAuthRedirectURL: req.OAuthRedirectURL,
|
||||
OAuthRefreshToken: req.OAuthRefreshToken,
|
||||
OAuthScope: req.OAuthScope,
|
||||
})
|
||||
if err := mailservice.NewV1().ValidateConfig(cfg); err != nil {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, err.Error()))
|
||||
}
|
||||
|
||||
configs := map[string]string{
|
||||
cst2.ConfigEmailAuthMode: cfg.AuthMode,
|
||||
cst2.ConfigEmailSMTPHost: cfg.SMTPHost,
|
||||
cst2.ConfigEmailSMTPPort: fmt.Sprintf("%d", cfg.SMTPPort),
|
||||
cst2.ConfigEmailSMTPUsername: cfg.SMTPUsername,
|
||||
cst2.ConfigEmailSMTPPassword: cfg.SMTPPassword,
|
||||
cst2.ConfigEmailFromEmail: cfg.FromEmail,
|
||||
cst2.ConfigEmailFromName: cfg.FromName,
|
||||
cst2.ConfigEmailOAuthTenant: cfg.OAuthTenant,
|
||||
cst2.ConfigEmailOAuthClientID: cfg.OAuthClientID,
|
||||
cst2.ConfigEmailOAuthClientSecret: cfg.OAuthClientSecret,
|
||||
cst2.ConfigEmailOAuthRedirectURL: cfg.OAuthRedirectURL,
|
||||
cst2.ConfigEmailOAuthRefreshToken: cfg.OAuthRefreshToken,
|
||||
cst2.ConfigEmailOAuthScope: cfg.OAuthScope,
|
||||
}
|
||||
|
||||
for key, value := range configs {
|
||||
exists, _ := db.Global.SysConfig.Query().Where(sysconfig.KeyEQ(key)).Exist(ctx)
|
||||
if exists {
|
||||
db.Global.SysConfig.Update().Where(sysconfig.KeyEQ(key)).SetValue(value).ExecX(ctx)
|
||||
} else {
|
||||
db.Global.SysConfig.Create().SetKey(key).SetValue(value).SetDescription("SMTP邮件配置").ExecX(ctx)
|
||||
db.Global.SysConfig.Create().
|
||||
SetKey(key).
|
||||
SetValue(value).
|
||||
SetDescription("邮件配置").
|
||||
ExecX(ctx)
|
||||
}
|
||||
}
|
||||
return utils.SuccessWithMsg("SMTP配置保存成功")
|
||||
}
|
||||
|
||||
// TestSmtp 发送测试邮件
|
||||
return utils.SuccessWithMsg("邮件配置保存成功")
|
||||
}
|
||||
|
||||
func (s *ConfigServiceV1) TestSmtp(ctx context.Context, req *v1.SmtpTestReq) *utils.Response[any] {
|
||||
emailService := email.NewV1()
|
||||
// 直接同步发送测试邮件,不走队列
|
||||
msg := mail.NewMsg()
|
||||
e := msg.FromFormat(emailService.GetConfig().FromName, emailService.GetConfig().FromEmail)
|
||||
utils.OkOrPanic(e)
|
||||
e = msg.AddTo(req.ToEmail)
|
||||
utils.OkOrPanic(e)
|
||||
msg.Subject("若梦系统 - SMTP测试邮件")
|
||||
msg.SetBodyString(mail.TypeTextHTML, "<h2>SMTP配置测试成功</h2><p>如果您收到此邮件,说明SMTP配置正确。</p>")
|
||||
|
||||
emailService := mailservice.NewV1()
|
||||
cfg := emailService.GetConfig()
|
||||
client, e := mail.NewClient(cfg.SMTPHost, mail.WithPort(cfg.SMTPPort),
|
||||
mail.WithSMTPAuth(mail.SMTPAuthLogin),
|
||||
mail.WithTLSPortPolicy(mail.TLSMandatory),
|
||||
mail.WithTimeout(30*time.Second),
|
||||
mail.WithUsername(cfg.SMTPUsername), mail.WithPassword(cfg.SMTPPassword))
|
||||
utils.OkOrPanic(e)
|
||||
defer client.Close()
|
||||
if err := emailService.ValidateConfig(cfg); err != nil {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, err.Error()))
|
||||
}
|
||||
|
||||
e = client.DialAndSendWithContext(ctx, msg)
|
||||
utils.OkOrPanic(e)
|
||||
subject := "邮件配置测试"
|
||||
body := "<h2>邮件配置测试成功</h2><p>如果您收到此邮件,说明当前邮件配置可以正常发信。</p>"
|
||||
if err := emailService.SendMessageNow(ctx, cfg, req.ToEmail, subject, body); err != nil {
|
||||
utils.OkOrPanic(err)
|
||||
}
|
||||
return utils.SuccessWithMsg("测试邮件发送成功")
|
||||
}
|
||||
|
||||
func (s *ConfigServiceV1) GetOutlookOAuthAuthorizeURL(ctx context.Context, req *v1.OutlookOAuthAuthorizeURLReq) *utils.Response[*v1.OutlookOAuthAuthorizeURLResp] {
|
||||
cfg := mailservice.NormalizeConfig(mailservice.EmailConfig{
|
||||
AuthMode: mailservice.AuthModeOutlookOAuth2,
|
||||
FromEmail: req.FromEmail,
|
||||
FromName: "Microsoft Graph OAuth2",
|
||||
OAuthTenant: req.OAuthTenant,
|
||||
OAuthClientID: req.OAuthClientID,
|
||||
OAuthClientSecret: req.OAuthClientSecret,
|
||||
OAuthScope: req.OAuthScope,
|
||||
})
|
||||
|
||||
resp, err := mailservice.NewV1().BuildOutlookAuthorizeURL(cfg, req.CallbackBaseURL, req.FrontendOrigin)
|
||||
if err != nil {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, err.Error()))
|
||||
}
|
||||
return utils.SuccessWithData(resp)
|
||||
}
|
||||
|
||||
func atoiOrZero(value string) int {
|
||||
if value == "" {
|
||||
return 0
|
||||
}
|
||||
var result int
|
||||
_, _ = fmt.Sscanf(value, "%d", &result)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -5,45 +5,70 @@ import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
db "likedream-goframe/internal/common/db"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cst2 "likedream-goframe/internal/common/cst"
|
||||
db "likedream-goframe/internal/common/db"
|
||||
"likedream-goframe/internal/ent/sysconfig"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
email2 "likedream-goframe/internal/framework/model/email"
|
||||
"likedream-goframe/internal/framework/queue"
|
||||
"likedream-goframe/internal/utils"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/samber/oops"
|
||||
mail "github.com/wneessen/go-mail"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
//go:embed templates/*.gohtml
|
||||
var templateFiles embed.FS
|
||||
|
||||
type EmailServiceV1 struct {
|
||||
}
|
||||
const (
|
||||
AuthModeSMTP = "smtp"
|
||||
AuthModeOutlookOAuth2 = "outlook_oauth2"
|
||||
|
||||
DefaultOutlookSMTPHost = "smtp-mail.outlook.com"
|
||||
DefaultOutlookSMTPPort = 587
|
||||
DefaultOutlookTenant = "consumers"
|
||||
DefaultOutlookOAuthScope = "offline_access https://graph.microsoft.com/Mail.Send"
|
||||
DefaultOutlookRedirectURL = "http://localhost:3200/api/v1/mail/oauth/outlook/callback"
|
||||
DefaultGraphSendMailURL = "https://graph.microsoft.com/v1.0/me/sendMail"
|
||||
)
|
||||
|
||||
type EmailServiceV1 struct{}
|
||||
|
||||
type EmailConfig struct {
|
||||
AuthMode string
|
||||
|
||||
SMTPHost string
|
||||
SMTPPort int
|
||||
SMTPUsername string
|
||||
SMTPPassword string
|
||||
|
||||
FromEmail string
|
||||
FromName string
|
||||
|
||||
OAuthTenant string
|
||||
OAuthClientID string
|
||||
OAuthClientSecret string
|
||||
OAuthRedirectURL string
|
||||
OAuthRefreshToken string
|
||||
OAuthScope string
|
||||
}
|
||||
|
||||
func NewV1() *EmailServiceV1 {
|
||||
return &EmailServiceV1{}
|
||||
}
|
||||
|
||||
// GetConfig 从数据库读取当前邮件配置
|
||||
func (s *EmailServiceV1) GetConfig() EmailConfig {
|
||||
ctx := gctx.New()
|
||||
|
||||
@@ -59,17 +84,301 @@ func (s *EmailServiceV1) GetConfig() EmailConfig {
|
||||
|
||||
port, _ := strconv.Atoi(getVal(cst2.ConfigEmailSMTPPort))
|
||||
|
||||
return EmailConfig{
|
||||
cfg := EmailConfig{
|
||||
AuthMode: getVal(cst2.ConfigEmailAuthMode),
|
||||
SMTPHost: getVal(cst2.ConfigEmailSMTPHost),
|
||||
SMTPPort: port,
|
||||
SMTPUsername: getVal(cst2.ConfigEmailSMTPUsername),
|
||||
SMTPPassword: getVal(cst2.ConfigEmailSMTPPassword),
|
||||
FromEmail: getVal(cst2.ConfigEmailFromEmail),
|
||||
FromName: getVal(cst2.ConfigEmailFromName),
|
||||
OAuthTenant: getVal(cst2.ConfigEmailOAuthTenant),
|
||||
OAuthClientID: getVal(cst2.ConfigEmailOAuthClientID),
|
||||
OAuthClientSecret: getVal(cst2.ConfigEmailOAuthClientSecret),
|
||||
OAuthRedirectURL: getVal(cst2.ConfigEmailOAuthRedirectURL),
|
||||
OAuthRefreshToken: getVal(cst2.ConfigEmailOAuthRefreshToken),
|
||||
OAuthScope: getVal(cst2.ConfigEmailOAuthScope),
|
||||
}
|
||||
|
||||
return NormalizeConfig(cfg)
|
||||
}
|
||||
|
||||
func NormalizeConfig(cfg EmailConfig) EmailConfig {
|
||||
if cfg.AuthMode == "" {
|
||||
cfg.AuthMode = AuthModeSMTP
|
||||
}
|
||||
if cfg.FromEmail != "" && cfg.SMTPUsername == "" {
|
||||
cfg.SMTPUsername = cfg.FromEmail
|
||||
}
|
||||
if cfg.AuthMode == AuthModeOutlookOAuth2 {
|
||||
cfg.OAuthTenant = DefaultOutlookTenant
|
||||
cfg.OAuthScope = DefaultOutlookOAuthScope
|
||||
cfg.OAuthRedirectURL = DefaultOutlookRedirectURL
|
||||
} else if cfg.SMTPPort == 0 {
|
||||
cfg.SMTPPort = 587
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (s *EmailServiceV1) ValidateConfig(cfg EmailConfig) error {
|
||||
cfg = NormalizeConfig(cfg)
|
||||
|
||||
if cfg.FromEmail == "" {
|
||||
return fmt.Errorf("fromEmail is required")
|
||||
}
|
||||
if cfg.FromName == "" {
|
||||
return fmt.Errorf("fromName is required")
|
||||
}
|
||||
|
||||
switch cfg.AuthMode {
|
||||
case AuthModeSMTP:
|
||||
if cfg.SMTPHost == "" {
|
||||
return fmt.Errorf("smtpHost is required")
|
||||
}
|
||||
if cfg.SMTPPort <= 0 {
|
||||
return fmt.Errorf("smtpPort is required")
|
||||
}
|
||||
if cfg.SMTPUsername == "" {
|
||||
return fmt.Errorf("smtpUsername is required")
|
||||
}
|
||||
if cfg.SMTPPassword == "" {
|
||||
return fmt.Errorf("smtpPassword is required")
|
||||
}
|
||||
case AuthModeOutlookOAuth2:
|
||||
if cfg.OAuthClientID == "" {
|
||||
return fmt.Errorf("oauthClientId is required")
|
||||
}
|
||||
if cfg.OAuthClientSecret == "" {
|
||||
return fmt.Errorf("oauthClientSecret is required")
|
||||
}
|
||||
if cfg.OAuthRefreshToken == "" {
|
||||
return fmt.Errorf("oauthRefreshToken is required")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported authMode: %s", cfg.AuthMode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *EmailServiceV1) AccessToken(ctx context.Context, cfg EmailConfig) (string, error) {
|
||||
cfg = NormalizeConfig(cfg)
|
||||
if cfg.AuthMode != AuthModeOutlookOAuth2 {
|
||||
return "", fmt.Errorf("access token is only available in oauth2 mode")
|
||||
}
|
||||
|
||||
form := url.Values{
|
||||
"client_id": {cfg.OAuthClientID},
|
||||
"client_secret": {cfg.OAuthClientSecret},
|
||||
"grant_type": {"refresh_token"},
|
||||
"refresh_token": {cfg.OAuthRefreshToken},
|
||||
"scope": {cfg.OAuthScope},
|
||||
}
|
||||
if cfg.OAuthRedirectURL != "" {
|
||||
form.Set("redirect_uri", cfg.OAuthRedirectURL)
|
||||
}
|
||||
|
||||
tokenURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", cfg.OAuthTenant)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var tokenResp struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
Scope string `json:"scope"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
if err := json.Unmarshal(bodyBytes, &tokenResp); err != nil {
|
||||
return "", fmt.Errorf("decode oauth token response failed: %w", err)
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
if tokenResp.ErrorDescription != "" {
|
||||
return "", fmt.Errorf("oauth token refresh failed: %s (%s)", tokenResp.ErrorDescription, tokenResp.Error)
|
||||
}
|
||||
return "", fmt.Errorf("oauth token refresh failed with status %d", resp.StatusCode)
|
||||
}
|
||||
if tokenResp.AccessToken == "" {
|
||||
return "", fmt.Errorf("empty access token returned by oauth2 token endpoint")
|
||||
}
|
||||
return strings.TrimSpace(tokenResp.AccessToken), nil
|
||||
}
|
||||
|
||||
func (s *EmailServiceV1) newClient(ctx context.Context, cfg EmailConfig) (*mail.Client, error) {
|
||||
cfg = NormalizeConfig(cfg)
|
||||
if err := s.ValidateConfig(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.AuthMode != AuthModeSMTP {
|
||||
return nil, fmt.Errorf("smtp client is only available in smtp mode")
|
||||
}
|
||||
|
||||
options := []mail.Option{
|
||||
mail.WithPort(cfg.SMTPPort),
|
||||
mail.WithTLSPortPolicy(mail.TLSMandatory),
|
||||
mail.WithTimeout(30 * time.Second),
|
||||
mail.WithUsername(cfg.SMTPUsername),
|
||||
}
|
||||
|
||||
options = append(options,
|
||||
mail.WithSMTPAuth(mail.SMTPAuthLogin),
|
||||
mail.WithPassword(cfg.SMTPPassword),
|
||||
)
|
||||
|
||||
return mail.NewClient(cfg.SMTPHost, options...)
|
||||
}
|
||||
|
||||
func (s *EmailServiceV1) newMessage(fromName, fromEmail, to, subject, body string) (*mail.Msg, error) {
|
||||
msg := mail.NewMsg()
|
||||
if err := msg.FromFormat(fromName, fromEmail); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := msg.AddTo(to); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg.Subject(subject)
|
||||
msg.SetBodyString(mail.TypeTextHTML, body)
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func (s *EmailServiceV1) SendMessageNow(ctx context.Context, cfg EmailConfig, to, subject, body string) error {
|
||||
cfg = NormalizeConfig(cfg)
|
||||
if err := s.ValidateConfig(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.AuthMode == AuthModeOutlookOAuth2 {
|
||||
return s.sendGraphMessageNow(ctx, cfg, to, subject, body)
|
||||
}
|
||||
|
||||
msg, err := s.newMessage(cfg.FromName, cfg.FromEmail, to, subject, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := s.newClient(ctx, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
return client.DialAndSendWithContext(ctx, msg)
|
||||
}
|
||||
|
||||
func (s *EmailServiceV1) sendGraphMessageNow(ctx context.Context, cfg EmailConfig, to, subject, body string) error {
|
||||
accessToken, err := s.AccessToken(ctx, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := map[string]any{
|
||||
"message": map[string]any{
|
||||
"subject": subject,
|
||||
"body": map[string]any{
|
||||
"contentType": "HTML",
|
||||
"content": body,
|
||||
},
|
||||
"toRecipients": []map[string]any{
|
||||
{
|
||||
"emailAddress": map[string]any{
|
||||
"address": to,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"saveToSentItems": true,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, DefaultGraphSendMailURL, bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
var graphErr struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
_ = json.NewDecoder(resp.Body).Decode(&graphErr)
|
||||
if graphErr.Error.Message != "" {
|
||||
return fmt.Errorf("graph sendMail failed: %s (%s)", graphErr.Error.Message, graphErr.Error.Code)
|
||||
}
|
||||
return fmt.Errorf("graph sendMail failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ConfigFromPayload(payload email2.SendPayload) EmailConfig {
|
||||
return NormalizeConfig(EmailConfig{
|
||||
AuthMode: payload.AuthMode,
|
||||
SMTPHost: payload.SMTPHost,
|
||||
SMTPPort: payload.SMTPPort,
|
||||
SMTPUsername: payload.SMTPUsername,
|
||||
SMTPPassword: payload.SMTPPassword,
|
||||
FromEmail: payload.FromEmail,
|
||||
FromName: payload.FromName,
|
||||
OAuthTenant: payload.OAuthTenant,
|
||||
OAuthClientID: payload.OAuthClientID,
|
||||
OAuthClientSecret: payload.OAuthClientSecret,
|
||||
OAuthRedirectURL: payload.OAuthRedirectURL,
|
||||
OAuthRefreshToken: payload.OAuthRefreshToken,
|
||||
OAuthScope: payload.OAuthScope,
|
||||
})
|
||||
}
|
||||
|
||||
func PayloadFromConfig(cfg EmailConfig, to, subject, body string) *email2.SendPayload {
|
||||
cfg = NormalizeConfig(cfg)
|
||||
return &email2.SendPayload{
|
||||
To: to,
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
AuthMode: cfg.AuthMode,
|
||||
FromName: cfg.FromName,
|
||||
FromEmail: cfg.FromEmail,
|
||||
SMTPHost: cfg.SMTPHost,
|
||||
SMTPPort: cfg.SMTPPort,
|
||||
SMTPUsername: cfg.SMTPUsername,
|
||||
SMTPPassword: cfg.SMTPPassword,
|
||||
OAuthTenant: cfg.OAuthTenant,
|
||||
OAuthClientID: cfg.OAuthClientID,
|
||||
OAuthClientSecret: cfg.OAuthClientSecret,
|
||||
OAuthRedirectURL: cfg.OAuthRedirectURL,
|
||||
OAuthRefreshToken: cfg.OAuthRefreshToken,
|
||||
OAuthScope: cfg.OAuthScope,
|
||||
}
|
||||
}
|
||||
|
||||
// templatePrefix 根据当前品牌返回模板文件前缀
|
||||
func templatePrefix() string {
|
||||
if IsNewBrand() {
|
||||
return "new_"
|
||||
@@ -77,45 +386,39 @@ func templatePrefix() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// brandAppName 获取当前品牌的应用名称
|
||||
func brandAppName() string {
|
||||
return GetBrandConfig().AppName
|
||||
}
|
||||
|
||||
// brandCopyright 获取当前品牌的版权信息
|
||||
func brandCopyright() string {
|
||||
return GetBrandConfig().AppCopyright
|
||||
}
|
||||
|
||||
// SendSignupEmail 发送注册成功邮件
|
||||
func (s *EmailServiceV1) SendSignupEmail(ctx context.Context, toEmail, username string, token string) *utils.Response[any] {
|
||||
cfg := s.GetConfig()
|
||||
if cfg.SMTPUsername == "" || cfg.SMTPPassword == "" {
|
||||
logger.Logger.Error("邮件服务未配置")
|
||||
if err := s.ValidateConfig(cfg); err != nil {
|
||||
logger.Logger.Error("mail config is invalid", zap.Error(err))
|
||||
return utils.FailBusiness("邮件服务未配置")
|
||||
}
|
||||
|
||||
appName := brandAppName()
|
||||
subject := fmt.Sprintf("欢迎注册%s", appName)
|
||||
subject := fmt.Sprintf("欢迎注册%s", brandAppName())
|
||||
body, err := s.renderSignupEmailTemplate(username, token)
|
||||
if err != nil {
|
||||
logger.Logger.Error("邮件模板渲染失败")
|
||||
logger.Logger.Error("render signup email failed", zap.Error(err))
|
||||
return utils.FailBusiness(fmt.Sprintf("邮件模板渲染失败: %v", err))
|
||||
}
|
||||
|
||||
err = s.sendEmail(ctx, toEmail, subject, body)
|
||||
if err != nil {
|
||||
logger.Logger.Error("邮件发送失败")
|
||||
if err = s.sendEmail(ctx, toEmail, subject, body); err != nil {
|
||||
logger.Logger.Error("send signup email failed", zap.Error(err))
|
||||
return utils.FailBusiness(fmt.Sprintf("邮件发送失败: %v", err))
|
||||
}
|
||||
|
||||
return utils.SuccessWithMsg("注册邮件发送成功")
|
||||
}
|
||||
|
||||
// SendTaskNotificationEmail 发送任务状态通知邮件
|
||||
func (s *EmailServiceV1) SendTaskNotificationEmail(ctx context.Context, toEmail, taskName string, status email2.TaskStatus, details map[string]string) *utils.Response[any] {
|
||||
cfg := s.GetConfig()
|
||||
if cfg.SMTPUsername == "" || cfg.SMTPPassword == "" {
|
||||
if err := s.ValidateConfig(cfg); err != nil {
|
||||
return utils.FailBusiness("邮件服务未配置")
|
||||
}
|
||||
|
||||
@@ -125,91 +428,79 @@ func (s *EmailServiceV1) SendTaskNotificationEmail(ctx context.Context, toEmail,
|
||||
return utils.FailBusiness(fmt.Sprintf("邮件模板渲染失败: %v", err))
|
||||
}
|
||||
|
||||
err = s.sendEmail(ctx, toEmail, subject, body)
|
||||
if err != nil {
|
||||
if err = s.sendEmail(ctx, toEmail, subject, body); err != nil {
|
||||
return utils.FailBusiness(fmt.Sprintf("邮件发送失败: %v", err))
|
||||
}
|
||||
|
||||
return utils.SuccessWithMsg("通知邮件发送成功")
|
||||
}
|
||||
|
||||
// SendResetPasswordEmail 发送重置密码邮件
|
||||
func (s *EmailServiceV1) SendResetPasswordEmail(ctx context.Context, toEmail, username string, token string) *utils.Response[any] {
|
||||
cfg := s.GetConfig()
|
||||
if cfg.SMTPUsername == "" || cfg.SMTPPassword == "" {
|
||||
logger.Logger.Error("邮件服务未配置")
|
||||
if err := s.ValidateConfig(cfg); err != nil {
|
||||
logger.Logger.Error("mail config is invalid", zap.Error(err))
|
||||
return utils.FailBusiness("邮件服务未配置")
|
||||
}
|
||||
|
||||
appName := brandAppName()
|
||||
subject := fmt.Sprintf("重置%s账户密码", appName)
|
||||
subject := fmt.Sprintf("重置%s账户密码", brandAppName())
|
||||
body, err := s.renderResetPasswordEmailTemplate(username, token)
|
||||
if err != nil {
|
||||
logger.Logger.Error("邮件模板渲染失败")
|
||||
logger.Logger.Error("render reset password email failed", zap.Error(err))
|
||||
return utils.FailBusiness(fmt.Sprintf("邮件模板渲染失败: %v", err))
|
||||
}
|
||||
|
||||
err = s.sendEmail(ctx, toEmail, subject, body)
|
||||
if err != nil {
|
||||
logger.Logger.Error("邮件发送失败")
|
||||
if err = s.sendEmail(ctx, toEmail, subject, body); err != nil {
|
||||
logger.Logger.Error("send reset password email failed", zap.Error(err))
|
||||
return utils.FailBusiness(fmt.Sprintf("邮件发送失败: %v", err))
|
||||
}
|
||||
|
||||
return utils.SuccessWithMsg("重置密码邮件发送成功")
|
||||
}
|
||||
|
||||
// SendBalanceMigrateEmail 发送积分转移邮件
|
||||
func (s *EmailServiceV1) SendBalanceMigrateEmail(ctx context.Context, toEmail, username string, balance int, token string) *utils.Response[any] {
|
||||
cfg := s.GetConfig()
|
||||
if cfg.SMTPUsername == "" || cfg.SMTPPassword == "" {
|
||||
logger.Logger.Error("邮件服务未配置")
|
||||
if err := s.ValidateConfig(cfg); err != nil {
|
||||
logger.Logger.Error("mail config is invalid", zap.Error(err))
|
||||
return utils.FailBusiness("邮件服务未配置")
|
||||
}
|
||||
|
||||
appName := brandAppName()
|
||||
subject := fmt.Sprintf("%s积分转移通知", appName)
|
||||
subject := fmt.Sprintf("%s积分转移通知", brandAppName())
|
||||
body, err := s.renderBalanceMigrateEmailTemplate(username, balance, token)
|
||||
if err != nil {
|
||||
logger.Logger.Error("邮件模板渲染失败")
|
||||
logger.Logger.Error("render balance migrate email failed", zap.Error(err))
|
||||
return utils.FailBusiness(fmt.Sprintf("邮件模板渲染失败: %v", err))
|
||||
}
|
||||
|
||||
err = s.sendEmail(ctx, toEmail, subject, body)
|
||||
if err != nil {
|
||||
logger.Logger.Error("邮件发送失败")
|
||||
if err = s.sendEmail(ctx, toEmail, subject, body); err != nil {
|
||||
logger.Logger.Error("send balance migrate email failed", zap.Error(err))
|
||||
return utils.FailBusiness(fmt.Sprintf("邮件发送失败: %v", err))
|
||||
}
|
||||
|
||||
return utils.SuccessWithMsg("积分转移邮件发送成功")
|
||||
}
|
||||
|
||||
// sendEmail 发送邮件的底层方法
|
||||
func (s *EmailServiceV1) sendEmail(ctx context.Context, to, subject, body string) error {
|
||||
cfg := s.GetConfig()
|
||||
if err := s.ValidateConfig(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := bytes.NewBuffer(nil)
|
||||
if err := gob.NewEncoder(payload).Encode(PayloadFromConfig(cfg, to, subject, body)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e := gob.NewEncoder(payload).Encode(&email2.SendPayload{
|
||||
To: to,
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
FromName: cfg.FromName,
|
||||
FromEmail: cfg.FromEmail,
|
||||
SMTPHost: cfg.SMTPHost,
|
||||
SMTPPort: cfg.SMTPPort,
|
||||
SMTPUsername: cfg.SMTPUsername,
|
||||
SMTPPassword: cfg.SMTPPassword,
|
||||
})
|
||||
utils.OkOrPanic(e, oops.WithContext(ctx).Wrap)
|
||||
|
||||
task := queue.AddTask(ctx, asynq.NewTask(cst2.TaskEmail, payload.Bytes()), asynq.Queue(cst2.TaskQueueEmail),
|
||||
task := queue.AddTask(
|
||||
ctx,
|
||||
asynq.NewTask(cst2.TaskEmail, payload.Bytes()),
|
||||
asynq.Queue(cst2.TaskQueueEmail),
|
||||
asynq.MaxRetry(1),
|
||||
asynq.Timeout(5*time.Minute),
|
||||
)
|
||||
logger.Logger.Info("添加任务: ", zap.String("task_id", task.ID), zap.String("邮箱", to))
|
||||
logger.Logger.Info("add email task", zap.String("task_id", task.ID), zap.String("to", to))
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseTemplates 根据当前品牌解析 layout + content 模板
|
||||
func parseTemplates(contentTemplate string) (*template.Template, error) {
|
||||
prefix := templatePrefix()
|
||||
layoutFile := fmt.Sprintf("templates/%slayout.gohtml", prefix)
|
||||
@@ -217,7 +508,6 @@ func parseTemplates(contentTemplate string) (*template.Template, error) {
|
||||
return template.ParseFS(templateFiles, layoutFile, contentFile)
|
||||
}
|
||||
|
||||
// renderSignupEmailTemplate 渲染注册邮件模板
|
||||
func (s *EmailServiceV1) renderSignupEmailTemplate(username string, token string) (string, error) {
|
||||
tmpl, err := parseTemplates("signup_confirm.gohtml")
|
||||
if err != nil {
|
||||
@@ -229,15 +519,12 @@ func (s *EmailServiceV1) renderSignupEmailTemplate(username string, token string
|
||||
|
||||
prefix := templatePrefix()
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.ExecuteTemplate(&buf, prefix+"signup_confirm.gohtml", data)
|
||||
if err != nil {
|
||||
if err = tmpl.ExecuteTemplate(&buf, prefix+"signup_confirm.gohtml", data); err != nil {
|
||||
return "", fmt.Errorf("渲染注册邮件模板失败: %v", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// renderTaskNotificationTemplate 渲染任务通知邮件模板
|
||||
func (s *EmailServiceV1) renderTaskNotificationTemplate(taskName string, status email2.TaskStatus, details map[string]string) (string, error) {
|
||||
tmpl, err := parseTemplates("task_notification.gohtml")
|
||||
if err != nil {
|
||||
@@ -249,15 +536,12 @@ func (s *EmailServiceV1) renderTaskNotificationTemplate(taskName string, status
|
||||
|
||||
prefix := templatePrefix()
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.ExecuteTemplate(&buf, prefix+"task_notification.gohtml", data)
|
||||
if err != nil {
|
||||
if err = tmpl.ExecuteTemplate(&buf, prefix+"task_notification.gohtml", data); err != nil {
|
||||
return "", fmt.Errorf("渲染任务通知邮件模板失败: %v", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// renderResetPasswordEmailTemplate 渲染重置密码邮件模板
|
||||
func (s *EmailServiceV1) renderResetPasswordEmailTemplate(username string, token string) (string, error) {
|
||||
tmpl, err := parseTemplates("reset_password.gohtml")
|
||||
if err != nil {
|
||||
@@ -269,15 +553,12 @@ func (s *EmailServiceV1) renderResetPasswordEmailTemplate(username string, token
|
||||
|
||||
prefix := templatePrefix()
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.ExecuteTemplate(&buf, prefix+"reset_password.gohtml", data)
|
||||
if err != nil {
|
||||
if err = tmpl.ExecuteTemplate(&buf, prefix+"reset_password.gohtml", data); err != nil {
|
||||
return "", fmt.Errorf("渲染重置密码邮件模板失败: %v", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// renderBalanceMigrateEmailTemplate 渲染积分转移邮件模板
|
||||
func (s *EmailServiceV1) renderBalanceMigrateEmailTemplate(username string, balance int, token string) (string, error) {
|
||||
tmpl, err := parseTemplates("balance_migrate.gohtml")
|
||||
if err != nil {
|
||||
@@ -289,10 +570,8 @@ func (s *EmailServiceV1) renderBalanceMigrateEmailTemplate(username string, bala
|
||||
|
||||
prefix := templatePrefix()
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.ExecuteTemplate(&buf, prefix+"balance_migrate.gohtml", data)
|
||||
if err != nil {
|
||||
if err = tmpl.ExecuteTemplate(&buf, prefix+"balance_migrate.gohtml", data); err != nil {
|
||||
return "", fmt.Errorf("渲染积分转移邮件模板失败: %v", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user