From 727f949f50c601eecc79fb3066e1d5265d35ca97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Sat, 1 Mar 2025 23:15:32 +0800 Subject: [PATCH] feat: add template --- client/senderservice/sender_service.go | 18 ++- generate/protobuf/notification.proto | 13 ++ go.mod | 19 +++ .../get_static_template_logic.go | 42 ++++++ .../senderservice/sender_service_server.go | 6 + internal/svc/service_context.go | 2 + pkg/domain/template/const.go | 12 ++ pkg/domain/template/email.go | 128 ++++++++++++++++++ pkg/domain/template/language.go | 11 ++ pkg/domain/template/type.go | 9 ++ pkg/domain/usecase/template.go | 10 ++ pkg/usecase/template.go | 43 ++++++ 12 files changed, 309 insertions(+), 4 deletions(-) create mode 100644 internal/logic/senderservice/get_static_template_logic.go create mode 100644 pkg/domain/template/const.go create mode 100644 pkg/domain/template/email.go create mode 100644 pkg/domain/template/language.go create mode 100644 pkg/domain/template/type.go create mode 100644 pkg/domain/usecase/template.go create mode 100644 pkg/usecase/template.go diff --git a/client/senderservice/sender_service.go b/client/senderservice/sender_service.go index 5540a1b..dc7ba3f 100644 --- a/client/senderservice/sender_service.go +++ b/client/senderservice/sender_service.go @@ -14,16 +14,20 @@ import ( ) type ( - NoneReq = notification.NoneReq - OKResp = notification.OKResp - SendMailReq = notification.SendMailReq - SendSMSReq = notification.SendSMSReq + NoneReq = notification.NoneReq + OKResp = notification.OKResp + SendMailReq = notification.SendMailReq + SendSMSReq = notification.SendSMSReq + TemplateReq = notification.TemplateReq + TemplateResp = notification.TemplateResp SenderService interface { // SendMail 寄信 SendMail(ctx context.Context, in *SendMailReq, opts ...grpc.CallOption) (*OKResp, error) // SendSms 寄簡訊 SendSms(ctx context.Context, in *SendSMSReq, opts ...grpc.CallOption) (*OKResp, error) + // 取得 Template + GetStaticTemplate(ctx context.Context, in *TemplateReq, opts ...grpc.CallOption) (*TemplateResp, error) } defaultSenderService struct { @@ -48,3 +52,9 @@ func (m *defaultSenderService) SendSms(ctx context.Context, in *SendSMSReq, opts client := notification.NewSenderServiceClient(m.cli.Conn()) return client.SendSms(ctx, in, opts...) } + +// 取得 Template +func (m *defaultSenderService) GetStaticTemplate(ctx context.Context, in *TemplateReq, opts ...grpc.CallOption) (*TemplateResp, error) { + client := notification.NewSenderServiceClient(m.cli.Conn()) + return client.GetStaticTemplate(ctx, in, opts...) +} diff --git a/generate/protobuf/notification.proto b/generate/protobuf/notification.proto index 8fc0ac9..25991f3 100644 --- a/generate/protobuf/notification.proto +++ b/generate/protobuf/notification.proto @@ -22,11 +22,24 @@ message SendSMSReq { string recipient_name=3; } +message TemplateReq { + string language = 1; + string template_id = 2; +} + +message TemplateResp { + string title = 1; + string body = 2; +} + + service SenderService { // SendMail 寄信 rpc SendMail(SendMailReq) returns(OKResp); // SendSms 寄簡訊 rpc SendSms(SendSMSReq) returns(OKResp); + // 取得 Template + rpc GetStaticTemplate(TemplateReq) returns(TemplateResp); } diff --git a/go.mod b/go.mod index 19bffd2..8e3fe52 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.36.3 github.com/aws/aws-sdk-go-v2/credentials v1.17.61 github.com/aws/aws-sdk-go-v2/service/ses v1.30.0 + github.com/matcornic/hermes/v2 v2.1.0 github.com/minchao/go-mitake v1.0.0 github.com/zeromicro/go-zero v1.7.0 google.golang.org/grpc v1.65.0 @@ -16,6 +17,11 @@ require ( ) require ( + github.com/Masterminds/semver v1.4.2 // indirect + github.com/Masterminds/sprig v2.16.0+incompatible // indirect + github.com/PuerkitoBio/goquery v1.5.0 // indirect + github.com/andybalholm/cascadia v1.0.0 // indirect + github.com/aokoli/goutils v1.0.1 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect github.com/aws/smithy-go v1.22.2 // indirect @@ -40,15 +46,21 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/huandu/xstrings v1.2.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/panjf2000/ants/v2 v2.10.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect @@ -57,7 +69,13 @@ require ( github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/redis/go-redis/v9 v9.6.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect + github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 // indirect + github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe // indirect go.etcd.io/etcd/api/v3 v3.5.15 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect go.etcd.io/etcd/client/v3 v3.5.15 // indirect @@ -76,6 +94,7 @@ require ( go.uber.org/automaxprocs v1.5.3 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect + golang.org/x/crypto v0.25.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.20.0 // indirect golang.org/x/sync v0.7.0 // indirect diff --git a/internal/logic/senderservice/get_static_template_logic.go b/internal/logic/senderservice/get_static_template_logic.go new file mode 100644 index 0000000..fecce88 --- /dev/null +++ b/internal/logic/senderservice/get_static_template_logic.go @@ -0,0 +1,42 @@ +package senderservicelogic + +import ( + "app-cloudep-notification-service/pkg/domain/template" + "context" + + "app-cloudep-notification-service/gen_result/pb/notification" + "app-cloudep-notification-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetStaticTemplateLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetStaticTemplateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetStaticTemplateLogic { + return &GetStaticTemplateLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// GetStaticTemplate 取得 Template +func (l *GetStaticTemplateLogic) GetStaticTemplate(in *notification.TemplateReq) (*notification.TemplateResp, error) { + tmp, err := l.svcCtx.TemplateUseCase.GetEmailTemplateByStatic( + l.ctx, + template.Language(in.GetLanguage()), + template.Type(in.GetTemplateId()), + ) + if err != nil { + return nil, err + } + + return ¬ification.TemplateResp{ + Title: tmp.Title, + Body: tmp.Body, + }, nil +} diff --git a/internal/server/senderservice/sender_service_server.go b/internal/server/senderservice/sender_service_server.go index d502a7f..f669d83 100644 --- a/internal/server/senderservice/sender_service_server.go +++ b/internal/server/senderservice/sender_service_server.go @@ -34,3 +34,9 @@ func (s *SenderServiceServer) SendSms(ctx context.Context, in *notification.Send l := senderservicelogic.NewSendSmsLogic(ctx, s.svcCtx) return l.SendSms(in) } + +// 取得 Template +func (s *SenderServiceServer) GetStaticTemplate(ctx context.Context, in *notification.TemplateReq) (*notification.TemplateResp, error) { + l := senderservicelogic.NewGetStaticTemplateLogic(ctx, s.svcCtx) + return l.GetStaticTemplate(in) +} diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 8674c25..746d849 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -14,6 +14,7 @@ import ( type ServiceContext struct { Config config.Config DeliveryUseCase useD.DeliveryUseCase + TemplateUseCase useD.TemplateUseCase } func NewServiceContext(c config.Config) *ServiceContext { @@ -80,5 +81,6 @@ func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, DeliveryUseCase: uc, + TemplateUseCase: usecase.MustTemplateUseCase(usecase.TemplateUseCaseParam{}), } } diff --git a/pkg/domain/template/const.go b/pkg/domain/template/const.go new file mode 100644 index 0000000..3e2d6a3 --- /dev/null +++ b/pkg/domain/template/const.go @@ -0,0 +1,12 @@ +package template + +// ============================== +// 產品資訊常數 +// ============================== + +const ( + ProductName = "TrueHeart 團隊" + ProductLink = "https://code.30cm.net" + ProductLogo = "https://true-heart-dev.s3.ap-northeast-3.amazonaws.com/f70904eb-1a29-40f7-8940-9a124f23793a.png" + ProductCopyright = "© 2025 TrueHeart Inc. 版權所有" +) diff --git a/pkg/domain/template/email.go b/pkg/domain/template/email.go new file mode 100644 index 0000000..652ff36 --- /dev/null +++ b/pkg/domain/template/email.go @@ -0,0 +1,128 @@ +package template + +import ( + "github.com/matcornic/hermes/v2" +) + +// ============================== +// Email 結構定義 +// ============================== + +// EmailTemplate 代表 Email 樣板 +type EmailTemplate struct { + Title string + Body string +} + +// ProductInfo 包含產品相關的資訊 +type ProductInfo struct { + Name string + Link string + Logo string + Copyright string +} + +// EmailBodyContent 包含郵件正文的資訊 +type EmailBodyContent struct { + RecipientName string + Intros []string + Actions []hermes.Action + Outros []string + Signature string +} + +// EmailContentParams 組成 Email 內容的參數 +type EmailContentParams struct { + Product ProductInfo + Content EmailBodyContent +} + +// ============================== +// Email 內容產生器 +// ============================== + +// GenerateEmailBody 根據參數產生 Email HTML 內容 +func GenerateEmailBody(params EmailContentParams) (string, error) { + product := hermes.Product{ + Name: params.Product.Name, + Link: params.Product.Link, + Logo: params.Product.Logo, + Copyright: params.Product.Copyright, + } + + body := hermes.Body{ + Name: params.Content.RecipientName, + Intros: params.Content.Intros, + Actions: params.Content.Actions, + Outros: params.Content.Outros, + Signature: params.Content.Signature, + } + + h := hermes.Hermes{Product: product} + email := hermes.Email{Body: body} + + return h.GenerateHTML(email) +} + +// GenerateEmailContent 生成 Email 內容(可用於不同驗證類型) +func GenerateEmailContent(title string, intros []string) (EmailTemplate, error) { + req := EmailContentParams{ + Product: ProductInfo{ + Name: ProductName, + Link: ProductLink, + Logo: ProductLogo, + Copyright: ProductCopyright, + }, + Content: EmailBodyContent{ + RecipientName: "{{.Username}}", + Intros: intros, + Actions: []hermes.Action{ + { + Instructions: "請複製您的驗證碼,到網頁重置", + InviteCode: "{{.VerifyCode}}", + }, + }, + Outros: []string{ + "如果您沒有請求此操作,請忽略此郵件。", + }, + Signature: "", + }, + } + + emailBody, err := GenerateEmailBody(req) + if err != nil { + return EmailTemplate{}, err + } + + return EmailTemplate{ + Title: title, + Body: emailBody, + }, nil +} + +// ============================== +// Email 產生函數 +// ============================== + +// GenerateForgetPasswordEmailZHTW 生成繁體中文的忘記密碼驗證信 +func GenerateForgetPasswordEmailZHTW() (EmailTemplate, error) { + return GenerateEmailContent("TrueHeart 重設密碼驗證信", + []string{"您收到此電子郵件是因為我們收到了針對帳戶的密碼重置請求。"}) +} + +// GenerateBindingEmailZHTW 生成綁定帳號驗證信 +func GenerateBindingEmailZHTW() (EmailTemplate, error) { + return GenerateEmailContent("TrueHeart 綁定信箱驗證信", + []string{"您收到此電子郵件是因為我們收到了針對帳戶的 Email 認證請求。"}) +} + +// ============================== +// Email 模板對應表 +// ============================== + +var EmailTemplateMap = map[Language]map[Type]func() (EmailTemplate, error){ + LanguageZhTW: { + ForgetPasswordVerify: GenerateForgetPasswordEmailZHTW, + BindingEmail: GenerateBindingEmailZHTW, + }, +} diff --git a/pkg/domain/template/language.go b/pkg/domain/template/language.go new file mode 100644 index 0000000..2be8fcb --- /dev/null +++ b/pkg/domain/template/language.go @@ -0,0 +1,11 @@ +package template + +// ============================== +// 語言與驗證碼類型 +// ============================== + +type Language string + +const ( + LanguageZhTW Language = "zh-tw" +) diff --git a/pkg/domain/template/type.go b/pkg/domain/template/type.go new file mode 100644 index 0000000..ea74771 --- /dev/null +++ b/pkg/domain/template/type.go @@ -0,0 +1,9 @@ +package template + +type Type string + +const ( + BindingEmail Type = "binding_email" // 驗證碼:綁定 Email + BindingPhone Type = "binding_phone" // 驗證碼:綁定 手機 + ForgetPasswordVerify Type = "forget_password" // 驗證碼: 忘記密碼 +) diff --git a/pkg/domain/usecase/template.go b/pkg/domain/usecase/template.go new file mode 100644 index 0000000..20e8c55 --- /dev/null +++ b/pkg/domain/usecase/template.go @@ -0,0 +1,10 @@ +package usecase + +import ( + "app-cloudep-notification-service/pkg/domain/template" + "context" +) + +type TemplateUseCase interface { + GetEmailTemplateByStatic(ctx context.Context, language template.Language, templateID template.Type) (template.EmailTemplate, error) +} diff --git a/pkg/usecase/template.go b/pkg/usecase/template.go new file mode 100644 index 0000000..965221c --- /dev/null +++ b/pkg/usecase/template.go @@ -0,0 +1,43 @@ +package usecase + +import ( + "app-cloudep-notification-service/pkg/domain/template" + "app-cloudep-notification-service/pkg/domain/usecase" + "context" + "fmt" +) + +type TemplateUseCaseParam struct{} + +type TemplateUseCase struct { + TemplateUseCaseParam +} + +func MustTemplateUseCase(param TemplateUseCaseParam) usecase.TemplateUseCase { + return &TemplateUseCase{ + param, + } +} + +func (use *TemplateUseCase) GetEmailTemplateByStatic(_ context.Context, language template.Language, templateID template.Type) (template.EmailTemplate, error) { + // 查找指定語言的模板映射 + templateByLang, exists := template.EmailTemplateMap[language] + if !exists { + return template.EmailTemplate{}, fmt.Errorf("email template not found for language: %s", language) + } + + // 查找指定類型的模板生成函數 + templateFunc, exists := templateByLang[templateID] + if !exists { + return template.EmailTemplate{}, fmt.Errorf("email template not found for type ID: %s", templateID) + } + + // 執行模板生成函數 + tmp, err := templateFunc() + if err != nil { + return template.EmailTemplate{}, fmt.Errorf("error generating email template: %w", err) + } + + // 返回構建好的響應 + return tmp, nil +}