feat: add template

This commit is contained in:
王性驊 2025-03-01 23:15:32 +08:00
parent 2921bc9869
commit 727f949f50
12 changed files with 309 additions and 4 deletions

View File

@ -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...)
}

View File

@ -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);
}

19
go.mod
View File

@ -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

View File

@ -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 &notification.TemplateResp{
Title: tmp.Title,
Body: tmp.Body,
}, nil
}

View File

@ -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)
}

View File

@ -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{}),
}
}

View File

@ -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. 版權所有"
)

View File

@ -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,
},
}

View File

@ -0,0 +1,11 @@
package template
// ==============================
// 語言與驗證碼類型
// ==============================
type Language string
const (
LanguageZhTW Language = "zh-tw"
)

View File

@ -0,0 +1,9 @@
package template
type Type string
const (
BindingEmail Type = "binding_email" // 驗證碼:綁定 Email
BindingPhone Type = "binding_phone" // 驗證碼:綁定 手機
ForgetPasswordVerify Type = "forget_password" // 驗證碼: 忘記密碼
)

View File

@ -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)
}

43
pkg/usecase/template.go Normal file
View File

@ -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
}