From dce5a9007b5fe4452e15771d12662add3e06b199 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 15:24:56 +0000 Subject: [PATCH] feat/refactor (#10) Reviewed-on: https://code.30cm.net/digimon/app-cloudep-notification-service/pulls/10 --- Makefile | 4 +- build/Dockerfile | 6 +- client/senderservice/sender_service.go | 30 ++-- etc/service.example.yaml | 34 +++-- generate/database/mongodb/readme.md | 1 - ...816014305_create_permission_table.down.sql | 1 - ...40816014305_create_permission_table.up.sql | 15 -- .../20230529020000_create_schema.down.sql | 1 - .../20230529020000_create_schema.up.sql | 1 - generate/database/readme.md | 39 ------ .../seeder/20230620025708_init_role.down.sql | 1 - .../seeder/20230620025708_init_role.up.sql | 3 - generate/protobuf/notification.proto | 19 +-- go.mod | 34 +++-- internal/config/config.go | 26 +++- internal/domain/errors.go | 58 -------- internal/domain/usecase/mail.go | 18 --- internal/domain/usecase/sms.go | 18 --- .../get_static_template_logic.go | 42 ++++++ .../send_mail_by_template_id_logic.go | 29 ---- .../logic/senderservice/send_mail_logic.go | 40 +----- .../send_sms_by_template_id_logic.go | 31 ----- .../logic/senderservice/send_sms_logic.go | 41 +----- .../senderservice/sender_service_server.go | 16 +-- internal/svc/service_context.go | 82 +++++++++-- internal/usecase/mitake.go | 36 ----- internal/usecase/smtp.go | 56 -------- notification.go | 5 +- pkg/config/config.go | 34 +++++ pkg/domain/error.go | 28 ++++ pkg/domain/notification/language.go | 9 ++ pkg/domain/notification/type.go | 16 +++ pkg/domain/repository/mail.go | 15 ++ pkg/domain/repository/sms.go | 16 +++ 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/delivary.go | 43 ++++++ pkg/domain/usecase/template.go | 10 ++ pkg/repository/aws_ses_mailer.go | 109 +++++++++++++++ pkg/repository/mitake_sms_sender.go | 63 +++++++++ pkg/repository/smtp_mailer.go | 52 +++++++ pkg/usecase/delivery.go | 75 ++++++++++ pkg/usecase/template.go | 43 ++++++ 45 files changed, 904 insertions(+), 456 deletions(-) delete mode 100644 generate/database/mongodb/readme.md delete mode 100644 generate/database/mysql/20240816014305_create_permission_table.down.sql delete mode 100644 generate/database/mysql/20240816014305_create_permission_table.up.sql delete mode 100644 generate/database/mysql/create/20230529020000_create_schema.down.sql delete mode 100644 generate/database/mysql/create/20230529020000_create_schema.up.sql delete mode 100644 generate/database/readme.md delete mode 100644 generate/database/seeder/20230620025708_init_role.down.sql delete mode 100644 generate/database/seeder/20230620025708_init_role.up.sql delete mode 100644 internal/domain/errors.go delete mode 100644 internal/domain/usecase/mail.go delete mode 100644 internal/domain/usecase/sms.go create mode 100644 internal/logic/senderservice/get_static_template_logic.go delete mode 100644 internal/logic/senderservice/send_mail_by_template_id_logic.go delete mode 100644 internal/logic/senderservice/send_sms_by_template_id_logic.go delete mode 100644 internal/usecase/mitake.go delete mode 100644 internal/usecase/smtp.go create mode 100644 pkg/config/config.go create mode 100644 pkg/domain/error.go create mode 100644 pkg/domain/notification/language.go create mode 100644 pkg/domain/notification/type.go create mode 100644 pkg/domain/repository/mail.go create mode 100644 pkg/domain/repository/sms.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/delivary.go create mode 100644 pkg/domain/usecase/template.go create mode 100644 pkg/repository/aws_ses_mailer.go create mode 100644 pkg/repository/mitake_sms_sender.go create mode 100644 pkg/repository/smtp_mailer.go create mode 100644 pkg/usecase/delivery.go create mode 100644 pkg/usecase/template.go diff --git a/Makefile b/Makefile index 785dc15..6c7922a 100644 --- a/Makefile +++ b/Makefile @@ -18,11 +18,11 @@ test: # 進行測試 fmt: # 格式優化 $(GOFMT) -w $(GOFILES) goimports -w ./ + golangci-lint run .PHONY: gen-rpc gen-rpc: # 建立 rpc code $(GO_CTL_NAME) rpc protoc ./generate/protobuf/notification.proto -m --style=$(GO_ZERO_STYLE) --go_out=./gen_result/pb --go-grpc_out=./gen_result/pb --zrpc_out=. - copy ./etc/service.yaml ./etc/service.example.yaml go mod tidy @echo "Generate core-api files successfully" @@ -44,6 +44,6 @@ run-docker: # 建立 rpc code .PHONY: build-docker build-docker: cp ./build/Dockerfile Dockerfile - docker buildx build -t $(DOCKER_REPO):$(VERSION) --build-arg SSH_PRIVATE_KEY="$(cat ~/.ssh/ed_25519)" . + docker buildx build -t $(DOCKER_REPO):$(VERSION) --build-arg SSH_PRIVATE_KEY="$(cat ~/.ssh/id_ed25519)" . rm -rf Dockerfile @echo "Generate core-api files successfully" diff --git a/build/Dockerfile b/build/Dockerfile index c5e8f4b..38ea0e9 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -2,7 +2,7 @@ # BUILDER # ########### -FROM golang:1.22.3 as builder +FROM golang:1.24.0 as builder ARG VERSION ARG BUILT @@ -41,7 +41,7 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ FROM gcr.io/distroless/static-debian11 WORKDIR /app -COPY --from=builder /app/service /app/service -COPY --from=builder /app/etc/service.yaml /app/etc/service.yaml +COPY --from=builder /app /app +COPY --from=builder /app/etc/notification.yaml /app/etc/notification.yaml EXPOSE 8080 CMD ["/app/notification"] \ No newline at end of file diff --git a/client/senderservice/sender_service.go b/client/senderservice/sender_service.go index bbc3d18..dc7ba3f 100644 --- a/client/senderservice/sender_service.go +++ b/client/senderservice/sender_service.go @@ -1,4 +1,5 @@ // Code generated by goctl. DO NOT EDIT. +// goctl 1.7.3 // Source: notification.proto package senderservice @@ -13,21 +14,20 @@ import ( ) type ( - NoneReq = notification.NoneReq - OKResp = notification.OKResp - SendByTemplateIDReq = notification.SendByTemplateIDReq - 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) - // SendMailByTemplateId 寄送模板信件 - SendMailByTemplateId(ctx context.Context, in *SendByTemplateIDReq, opts ...grpc.CallOption) (*OKResp, error) - // SendSmsByTemplateId 寄送模板簡訊 - SendSmsByTemplateId(ctx context.Context, in *SendByTemplateIDReq, opts ...grpc.CallOption) (*OKResp, error) + // 取得 Template + GetStaticTemplate(ctx context.Context, in *TemplateReq, opts ...grpc.CallOption) (*TemplateResp, error) } defaultSenderService struct { @@ -53,14 +53,8 @@ func (m *defaultSenderService) SendSms(ctx context.Context, in *SendSMSReq, opts return client.SendSms(ctx, in, opts...) } -// SendMailByTemplateId 寄送模板信件 -func (m *defaultSenderService) SendMailByTemplateId(ctx context.Context, in *SendByTemplateIDReq, opts ...grpc.CallOption) (*OKResp, error) { +// 取得 Template +func (m *defaultSenderService) GetStaticTemplate(ctx context.Context, in *TemplateReq, opts ...grpc.CallOption) (*TemplateResp, error) { client := notification.NewSenderServiceClient(m.cli.Conn()) - return client.SendMailByTemplateId(ctx, in, opts...) -} - -// SendSmsByTemplateId 寄送模板簡訊 -func (m *defaultSenderService) SendSmsByTemplateId(ctx context.Context, in *SendByTemplateIDReq, opts ...grpc.CallOption) (*OKResp, error) { - client := notification.NewSenderServiceClient(m.cli.Conn()) - return client.SendSmsByTemplateId(ctx, in, opts...) + return client.GetStaticTemplate(ctx, in, opts...) } diff --git a/etc/service.example.yaml b/etc/service.example.yaml index 07047e8..7a1edeb 100644 --- a/etc/service.example.yaml +++ b/etc/service.example.yaml @@ -4,13 +4,31 @@ Etcd: Hosts: - 127.0.0.1:2379 Key: notification.rpc -SMTP: - Host: smtp.mail.host - Port: 25 - User: smtp@user.net - Password: smtp_password -SMSSender: - User: sms@user.net - Password : sms_password +SMTPConfig: + Enable: false + Sort: 1 + GoroutinePoolNum: 10 + Host: xxxxxx + Port: xxxxxx + Username: xxxxxx + Password: xxxxxx + +AmazonSesSettings: + Enable: false + Sort: 2 + PoolSize: 2000 + Region : ap-northeast-3 + Sender : xxxxxx + Charset : xxxxxx + AccessKey : xxxxxx + SecretKey : xxxxxx + Token : xxxxxx + +MitakeSMSSender: + Enable: false + Sort: 1 + PoolSize: 10 + User: xxxxxx + Password : xxxxxx diff --git a/generate/database/mongodb/readme.md b/generate/database/mongodb/readme.md deleted file mode 100644 index 5cbb27e..0000000 --- a/generate/database/mongodb/readme.md +++ /dev/null @@ -1 +0,0 @@ -如果有需要可以把 mongo 放這邊 \ No newline at end of file diff --git a/generate/database/mysql/20240816014305_create_permission_table.down.sql b/generate/database/mysql/20240816014305_create_permission_table.down.sql deleted file mode 100644 index 6dc8750..0000000 --- a/generate/database/mysql/20240816014305_create_permission_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS `permission`; diff --git a/generate/database/mysql/20240816014305_create_permission_table.up.sql b/generate/database/mysql/20240816014305_create_permission_table.up.sql deleted file mode 100644 index cbe4d31..0000000 --- a/generate/database/mysql/20240816014305_create_permission_table.up.sql +++ /dev/null @@ -1,15 +0,0 @@ --- 通常會把整個表都放到記憶體當中,不常搜尋,不需要加其他搜尋的 index -CREATE TABLE `permission` -( - `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', - `parent` bigint unsigned DEFAULT NULL, - `name` varchar(255) NOT NULL, - `http_method` varchar(255) NOT NULL, - `http_path` text NOT NULL, - `status` tinyint NOT NULL DEFAULT '1' COMMENT '狀態 1: 啟用, 2: 關閉', - `type` tinyint NOT NULL DEFAULT '1' COMMENT '狀態 1: 後台, 2: 前台', - `create_time` bigint DEFAULT 0 NOT NULL COMMENT '創建時間', - `update_time` bigint DEFAULT 0 NOT NULL COMMENT '更新時間', - PRIMARY KEY (`id`), - UNIQUE KEY `name_unique_key` (`name`) -) ENGINE = InnoDB COMMENT ='權限表'; \ No newline at end of file diff --git a/generate/database/mysql/create/20230529020000_create_schema.down.sql b/generate/database/mysql/create/20230529020000_create_schema.down.sql deleted file mode 100644 index 766ea05..0000000 --- a/generate/database/mysql/create/20230529020000_create_schema.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP DATABASE IF EXISTS `example`; \ No newline at end of file diff --git a/generate/database/mysql/create/20230529020000_create_schema.up.sql b/generate/database/mysql/create/20230529020000_create_schema.up.sql deleted file mode 100644 index 07648b3..0000000 --- a/generate/database/mysql/create/20230529020000_create_schema.up.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE DATABASE IF NOT EXISTS `example`; \ No newline at end of file diff --git a/generate/database/readme.md b/generate/database/readme.md deleted file mode 100644 index 4db6e02..0000000 --- a/generate/database/readme.md +++ /dev/null @@ -1,39 +0,0 @@ -# migrate -數據庫遷移工具 - -[golang-migrate](https://github.com/golang-migrate/migrate) - -## 安裝 make -```shell -brew install Makefile -``` - -## 安裝 golang-migrate -```shell -brew install golang-migrate -``` - -## 執行刪除 mysql schema - -```shell -migrate -source file://database/migrations/mysql -database 'mysql://account:password@tcp(127.0.0.1:3306)/esc_c2c' down -``` - -## 執行安裝 mysql schema - -```shell -migrate -source file://database/migrations/mysql -database 'mysql://account:password@tcp(127.0.0.1:3306)/esc_c2c' up -``` - -## 執行刪除 mongo schema - -```shell -migrate -source file://database/migrations/mongodb -database 'mongodb://127.0.0.1:27017/esc_c2c' down -``` - -## 執行安裝 mongo schema - -```shell -migrate -source file://database/migrations/mongodb -database 'mongodb://127.0.0.1:27017/esc_c2c' up -``` - diff --git a/generate/database/seeder/20230620025708_init_role.down.sql b/generate/database/seeder/20230620025708_init_role.down.sql deleted file mode 100644 index 36a1962..0000000 --- a/generate/database/seeder/20230620025708_init_role.down.sql +++ /dev/null @@ -1 +0,0 @@ -DELETE FROM `role` WHERE (`role_id` = 'AM000000'); diff --git a/generate/database/seeder/20230620025708_init_role.up.sql b/generate/database/seeder/20230620025708_init_role.up.sql deleted file mode 100644 index d4c158e..0000000 --- a/generate/database/seeder/20230620025708_init_role.up.sql +++ /dev/null @@ -1,3 +0,0 @@ -INSERT INTO `role` (`role_id`, `display_name`, `status`, `create_time`, `update_time`) -VALUES ('AM000000', 'admin', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), - ('AM000001', 'visitor', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); \ No newline at end of file diff --git a/generate/protobuf/notification.proto b/generate/protobuf/notification.proto index e3d5304..25991f3 100644 --- a/generate/protobuf/notification.proto +++ b/generate/protobuf/notification.proto @@ -22,11 +22,14 @@ message SendSMSReq { string recipient_name=3; } -message SendByTemplateIDReq { - string to = 1; - string template_id =2; - string lang=3; - map content_data = 4; +message TemplateReq { + string language = 1; + string template_id = 2; +} + +message TemplateResp { + string title = 1; + string body = 2; } @@ -35,10 +38,8 @@ service SenderService { rpc SendMail(SendMailReq) returns(OKResp); // SendSms 寄簡訊 rpc SendSms(SendSMSReq) returns(OKResp); - // SendMailByTemplateId 寄送模板信件 - rpc SendMailByTemplateId(SendByTemplateIDReq) returns(OKResp); - // SendSmsByTemplateId 寄送模板簡訊 - rpc SendSmsByTemplateId(SendByTemplateIDReq) returns(OKResp); + // 取得 Template + rpc GetStaticTemplate(TemplateReq) returns(TemplateResp); } diff --git a/go.mod b/go.mod index 9ea4180..8e3fe52 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,14 @@ module app-cloudep-notification-service -go 1.22.3 +go 1.24.0 require ( - code.30cm.net/digimon/library-go/errs v1.2.3 - code.30cm.net/digimon/library-go/validator v1.0.0 + code.30cm.net/digimon/library-go/errs v1.2.14 code.30cm.net/digimon/library-go/worker_pool v0.0.0-20240820153352-f9c90a90f5e2 + 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 @@ -14,6 +17,14 @@ 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 github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -23,15 +34,11 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/color v1.17.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.22.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -39,16 +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/leodido/go-urn v1.4.0 // 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 diff --git a/internal/config/config.go b/internal/config/config.go index 3521805..65212ac 100755 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,14 +5,34 @@ import "github.com/zeromicro/go-zero/zrpc" type Config struct { zrpc.RpcServerConf - SMTP struct { + SMTPConfig struct { + Enable bool + Sort int + GoroutinePoolNum int + Host string Port int - User string + Username string Password string } - SMSSender struct { + AmazonSesSettings struct { + Enable bool + Sort int + PoolSize int + + Region string + Sender string + Charset string + AccessKey string + SecretKey string + Token string + } + MitakeSMSSender struct { + Enable bool + Sort int + PoolSize int + User string Password string } diff --git a/internal/domain/errors.go b/internal/domain/errors.go deleted file mode 100644 index b6d54f0..0000000 --- a/internal/domain/errors.go +++ /dev/null @@ -1,58 +0,0 @@ -package domain - -import ( - "fmt" - "strings" - - "code.30cm.net/digimon/library-go/errs" - "code.30cm.net/digimon/library-go/errs/code" - "github.com/zeromicro/go-zero/core/logx" -) - -type ErrorCode uint32 - -func (e ErrorCode) ToUint32() uint32 { - return uint32(e) -} - -const ( - _ = iota - SendMailErrorCode ErrorCode = iota - SendSMSErrorCode -) - -// SendMailError ... -func SendMailError(s ...string) *errs.LibError { - return errs.NewError(code.CloudEPNotification, code.ThirdParty, - SendMailErrorCode.ToUint32(), - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// SendMailErrorL logs error message and returns Err -func SendMailErrorL(l logx.Logger, filed []logx.LogField, s ...string) *errs.LibError { - e := SendMailError(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} - -// SendSMSError ... -func SendSMSError(s ...string) *errs.LibError { - return errs.NewError(code.CloudEPNotification, code.ThirdParty, - SendSMSErrorCode.ToUint32(), - fmt.Sprintf("%s", strings.Join(s, " "))) -} - -// SendSMSErrorL logs error message and returns Err -func SendSMSErrorL(l logx.Logger, filed []logx.LogField, s ...string) *errs.LibError { - e := SendSMSError(s...) - if filed != nil || len(filed) >= 0 { - l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) - } - l.WithCallerSkip(1).Error(e.Error()) - - return e -} diff --git a/internal/domain/usecase/mail.go b/internal/domain/usecase/mail.go deleted file mode 100644 index 95fb400..0000000 --- a/internal/domain/usecase/mail.go +++ /dev/null @@ -1,18 +0,0 @@ -package usecase - -import "context" - -type MailClientUseCase interface { - SendMail(ctx context.Context, req MailReq) error -} - -type MailReq struct { - To string - From string - Subject string - Body string -} - -type MailUseCase interface { - GetMailTemplateByID(ctx context.Context, tid int64) ([]rune, error) -} diff --git a/internal/domain/usecase/sms.go b/internal/domain/usecase/sms.go deleted file mode 100644 index c4e6d66..0000000 --- a/internal/domain/usecase/sms.go +++ /dev/null @@ -1,18 +0,0 @@ -package usecase - -import ( - "context" -) - -type SMSClientUseCase interface { - SendSMS(ctx context.Context, req SMSReq) error -} - -type SMSReq struct { - // RecipientAddress 接收者號碼 - RecipientAddress string - // RecipientName 接收者姓名 - RecipientName string - // Body 要傳送的訊息 - Body string -} 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/logic/senderservice/send_mail_by_template_id_logic.go b/internal/logic/senderservice/send_mail_by_template_id_logic.go deleted file mode 100644 index 08f53ae..0000000 --- a/internal/logic/senderservice/send_mail_by_template_id_logic.go +++ /dev/null @@ -1,29 +0,0 @@ -package senderservicelogic - -import ( - "app-cloudep-notification-service/gen_result/pb/notification" - "app-cloudep-notification-service/internal/svc" - "context" - - "github.com/zeromicro/go-zero/core/logx" -) - -type SendMailByTemplateIdLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewSendMailByTemplateIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendMailByTemplateIdLogic { - return &SendMailByTemplateIdLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// SendMailByTemplateId 寄送模板信件 -func (l *SendMailByTemplateIdLogic) SendMailByTemplateId(in *notification.SendByTemplateIDReq) (*notification.OKResp, error) { - - return ¬ification.OKResp{}, nil -} diff --git a/internal/logic/senderservice/send_mail_logic.go b/internal/logic/senderservice/send_mail_logic.go index 7b16399..e2b1b7b 100644 --- a/internal/logic/senderservice/send_mail_logic.go +++ b/internal/logic/senderservice/send_mail_logic.go @@ -1,12 +1,9 @@ package senderservicelogic import ( - "app-cloudep-notification-service/internal/domain" - "app-cloudep-notification-service/internal/domain/usecase" + "app-cloudep-notification-service/pkg/domain/usecase" "context" - ers "code.30cm.net/digimon/library-go/errs" - "app-cloudep-notification-service/gen_result/pb/notification" "app-cloudep-notification-service/internal/svc" @@ -27,45 +24,16 @@ func NewSendMailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendMail } } -type sendMailReq struct { - // TO 收件者 - To string `validate:"required,email"` - // Subject 信件主旨 - Subject string `validate:"required,max=128"` - // Body 內容 - Body string `validate:"required"` - // From 寄件者 - From string `validate:"required"` -} - // SendMail 寄信 func (l *SendMailLogic) SendMail(in *notification.SendMailReq) (*notification.OKResp, error) { - if err := l.svcCtx.Validate.ValidateAll(&sendMailReq{ - To: in.GetTo(), + err := l.svcCtx.DeliveryUseCase.SendEmail(l.ctx, usecase.MailReq{ + To: []string{in.GetTo()}, Subject: in.GetSubject(), Body: in.GetBody(), From: in.GetFrom(), - }); err != nil { - return nil, ers.InvalidFormat("invalid format") - } - - // TODO 以後可以做換線 - err := l.svcCtx.MailSender.SendMail(l.ctx, usecase.MailReq{ - To: in.GetTo(), - Subject: in.GetSubject(), - From: in.GetFrom(), - Body: in.GetBody(), }) if err != nil { - return nil, domain.SendMailErrorL( - logx.WithContext(l.ctx), - []logx.LogField{ - logx.Field("func", "MailSender.SendMail"), - logx.Field("in", in), - logx.Field("err", err), - }, - "MailSender.SendMail failed to send mail", - ) + return nil, err } return ¬ification.OKResp{}, nil diff --git a/internal/logic/senderservice/send_sms_by_template_id_logic.go b/internal/logic/senderservice/send_sms_by_template_id_logic.go deleted file mode 100644 index 0ec19d2..0000000 --- a/internal/logic/senderservice/send_sms_by_template_id_logic.go +++ /dev/null @@ -1,31 +0,0 @@ -package senderservicelogic - -import ( - "context" - - "app-cloudep-notification-service/gen_result/pb/notification" - "app-cloudep-notification-service/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type SendSmsByTemplateIdLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewSendSmsByTemplateIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsByTemplateIdLogic { - return &SendSmsByTemplateIdLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// SendSmsByTemplateId 寄送模板簡訊 -func (l *SendSmsByTemplateIdLogic) SendSmsByTemplateId(in *notification.SendByTemplateIDReq) (*notification.OKResp, error) { - // todo: add your logic here and delete this line - - return ¬ification.OKResp{}, nil -} diff --git a/internal/logic/senderservice/send_sms_logic.go b/internal/logic/senderservice/send_sms_logic.go index 28e30a9..85b3ff6 100644 --- a/internal/logic/senderservice/send_sms_logic.go +++ b/internal/logic/senderservice/send_sms_logic.go @@ -1,12 +1,9 @@ package senderservicelogic import ( - "app-cloudep-notification-service/internal/domain" - "app-cloudep-notification-service/internal/domain/usecase" + "app-cloudep-notification-service/pkg/domain/usecase" "context" - "code.30cm.net/digimon/library-go/errs" - "app-cloudep-notification-service/gen_result/pb/notification" "app-cloudep-notification-service/internal/svc" @@ -27,41 +24,15 @@ func NewSendSmsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsLo } } -type sendSMSReq struct { - // RecipientAddress 收件者 - RecipientAddress string `validate:"required,phone"` - // Body 內容 - Body string `validate:"required"` - // RecipientName 收件者信名 - RecipientName string `validate:"required"` -} - // SendSms 寄簡訊 func (l *SendSmsLogic) SendSms(in *notification.SendSMSReq) (*notification.OKResp, error) { - if err := l.svcCtx.Validate.ValidateAll(&sendSMSReq{ - RecipientName: in.GetTo(), - Body: in.GetBody(), - RecipientAddress: in.GetTo(), - }); err != nil { - return nil, errs.InvalidFormat(err.Error()) - } - - // TODO 以後可以做換線 - err := l.svcCtx.SMSSender.SendSMS(l.ctx, usecase.SMSReq{ - RecipientAddress: in.GetTo(), - RecipientName: in.GetRecipientName(), - Body: in.GetBody(), + err := l.svcCtx.DeliveryUseCase.SendMessage(l.ctx, usecase.SMSMessageRequest{ + PhoneNumber: in.GetTo(), + RecipientName: in.GetRecipientName(), + MessageContent: in.GetBody(), }) if err != nil { - return nil, domain.SendSMSErrorL( - logx.WithContext(l.ctx), - []logx.LogField{ - logx.Field("func", "SMSSender.SendSMS"), - logx.Field("in", in), - logx.Field("err", err), - }, - "SMSSender.SendSMS failed to send sms", - ) + return nil, err } return ¬ification.OKResp{}, nil diff --git a/internal/server/senderservice/sender_service_server.go b/internal/server/senderservice/sender_service_server.go index 6c1ead4..f669d83 100644 --- a/internal/server/senderservice/sender_service_server.go +++ b/internal/server/senderservice/sender_service_server.go @@ -1,4 +1,5 @@ // Code generated by goctl. DO NOT EDIT. +// goctl 1.7.3 // Source: notification.proto package server @@ -8,7 +9,6 @@ import ( "app-cloudep-notification-service/gen_result/pb/notification" senderservicelogic "app-cloudep-notification-service/internal/logic/senderservice" - "app-cloudep-notification-service/internal/svc" ) @@ -35,14 +35,8 @@ func (s *SenderServiceServer) SendSms(ctx context.Context, in *notification.Send return l.SendSms(in) } -// SendMailByTemplateId 寄送模板信件 -func (s *SenderServiceServer) SendMailByTemplateId(ctx context.Context, in *notification.SendByTemplateIDReq) (*notification.OKResp, error) { - l := senderservicelogic.NewSendMailByTemplateIdLogic(ctx, s.svcCtx) - return l.SendMailByTemplateId(in) -} - -// SendSmsByTemplateId 寄送模板簡訊 -func (s *SenderServiceServer) SendSmsByTemplateId(ctx context.Context, in *notification.SendByTemplateIDReq) (*notification.OKResp, error) { - l := senderservicelogic.NewSendSmsByTemplateIdLogic(ctx, s.svcCtx) - return l.SendSmsByTemplateId(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 d5a59dc..746d849 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -2,29 +2,85 @@ package svc import ( "app-cloudep-notification-service/internal/config" - domainUC "app-cloudep-notification-service/internal/domain/usecase" - "app-cloudep-notification-service/internal/usecase" + cfg "app-cloudep-notification-service/pkg/config" + useD "app-cloudep-notification-service/pkg/domain/usecase" + "app-cloudep-notification-service/pkg/repository" + "app-cloudep-notification-service/pkg/usecase" "code.30cm.net/digimon/library-go/errs" "code.30cm.net/digimon/library-go/errs/code" - - v "code.30cm.net/digimon/library-go/validator" ) type ServiceContext struct { - Config config.Config - - Validate v.Validate - MailSender domainUC.MailClientUseCase - SMSSender domainUC.SMSClientUseCase + Config config.Config + DeliveryUseCase useD.DeliveryUseCase + TemplateUseCase useD.TemplateUseCase } func NewServiceContext(c config.Config) *ServiceContext { errs.Scope = code.CloudEPNotification + + param := usecase.DeliveryUseCaseParam{} + if c.AmazonSesSettings.Enable { + sesRepo := repository.MustAwsSesMailRepository(repository.AwsEmailDeliveryParam{ + Conf: &cfg.AmazonSesSettings{ + Enable: c.AmazonSesSettings.Enable, + Sort: c.AmazonSesSettings.Sort, + PoolSize: c.AmazonSesSettings.PoolSize, + Region: c.AmazonSesSettings.Region, + Sender: c.AmazonSesSettings.Sender, + Charset: c.AmazonSesSettings.Charset, + AccessKey: c.AmazonSesSettings.AccessKey, + SecretKey: c.AmazonSesSettings.SecretKey, + Token: c.AmazonSesSettings.Token, + }, + }) + + param.EmailProviders = append(param.EmailProviders, useD.EmailProvider{ + Sort: int64(c.AmazonSesSettings.Sort), + Repo: sesRepo, + }) + } + + if c.SMTPConfig.Enable { + smtpRepo := repository.MustSMTPUseCase(repository.SMTPMailUseCaseParam{ + Conf: cfg.SMTPConfig{ + Enable: c.SMTPConfig.Enable, + Sort: c.SMTPConfig.Sort, + GoroutinePoolNum: c.SMTPConfig.GoroutinePoolNum, + Host: c.SMTPConfig.Host, + Port: c.SMTPConfig.Port, + Username: c.SMTPConfig.Username, + Password: c.SMTPConfig.Password, + }, + }) + + param.EmailProviders = append(param.EmailProviders, useD.EmailProvider{ + Sort: int64(c.SMTPConfig.Sort), + Repo: smtpRepo, + }) + } + + if c.MitakeSMSSender.Enable { + param.SMSProviders = append(param.SMSProviders, useD.SMSProvider{ + Sort: int64(c.MitakeSMSSender.Sort), + Repo: repository.MustMitakeRepository(repository.MitakeSMSDeliveryParam{ + Conf: &cfg.MitakeSMSSender{ + Enable: c.MitakeSMSSender.Enable, + Sort: c.MitakeSMSSender.Sort, + User: c.MitakeSMSSender.User, + Password: c.MitakeSMSSender.Password, + PoolSize: c.MitakeSMSSender.PoolSize, + }, + }), + }) + } + + uc := usecase.MustDeliveryUseCase(param) + return &ServiceContext{ - Config: c, - MailSender: usecase.MustMailgunUseCase(usecase.MailUseCaseParam{Conf: c}), - SMSSender: usecase.MustMitakeUseCase(usecase.SMSUseCaseParam{Conf: c}), - Validate: v.MustValidator(), + Config: c, + DeliveryUseCase: uc, + TemplateUseCase: usecase.MustTemplateUseCase(usecase.TemplateUseCaseParam{}), } } diff --git a/internal/usecase/mitake.go b/internal/usecase/mitake.go deleted file mode 100644 index ac332bc..0000000 --- a/internal/usecase/mitake.go +++ /dev/null @@ -1,36 +0,0 @@ -package usecase - -import ( - "app-cloudep-notification-service/internal/config" - "app-cloudep-notification-service/internal/domain/usecase" - "context" - - "github.com/minchao/go-mitake" -) - -type SMSUseCaseParam struct { - Conf config.Config -} - -type SMSUseCase struct { - Client *mitake.Client -} - -func (s *SMSUseCase) SendSMS(_ context.Context, req usecase.SMSReq) error { - message := mitake.Message{ - Dstaddr: req.RecipientAddress, - Destname: req.RecipientName, - Smbody: req.Body, - } - _, err := s.Client.Send(message) - if err != nil { - return err - } - return nil -} - -func MustMitakeUseCase(param SMSUseCaseParam) usecase.SMSClientUseCase { - return &SMSUseCase{ - Client: mitake.NewClient(param.Conf.SMSSender.User, param.Conf.SMSSender.Password, nil), - } -} diff --git a/internal/usecase/smtp.go b/internal/usecase/smtp.go deleted file mode 100644 index f4d97e7..0000000 --- a/internal/usecase/smtp.go +++ /dev/null @@ -1,56 +0,0 @@ -package usecase - -import ( - "app-cloudep-notification-service/internal/config" - "app-cloudep-notification-service/internal/domain/usecase" - - pool "code.30cm.net/digimon/library-go/worker_pool" - "github.com/zeromicro/go-zero/core/logx" - - "context" - - "gopkg.in/gomail.v2" -) - -type MailUseCaseParam struct { - Conf config.Config -} - -type MailUseCase struct { - Host string - Port int - User string - Password string - Pool pool.WorkerPool -} - -func (mu *MailUseCase) SendMail(_ context.Context, req usecase.MailReq) error { - // 用 goroutine pool 送,否則會超時 - err := mu.Pool.Submit(func() { - m := gomail.NewMessage() - m.SetHeader("From", req.From) - m.SetHeader("To", req.To) - m.SetHeader("Subject", req.Subject) - m.SetBody("text/html", req.Body) - d := gomail.NewDialer(mu.Host, mu.Port, mu.User, mu.Password) - if err := d.DialAndSend(m); err != nil { - logx.WithCallerSkip(1).WithFields( - logx.Field("func", "MailUseCase.SendMail"), - logx.Field("req", req), - logx.Field("err", err), - ).Error("failed to send mail by mailgun") - } - }) - - return err -} - -func MustMailgunUseCase(param MailUseCaseParam) usecase.MailClientUseCase { - return &MailUseCase{ - Host: param.Conf.SMTP.Host, - Port: param.Conf.SMTP.Port, - User: param.Conf.SMTP.User, - Password: param.Conf.SMTP.Password, - Pool: pool.NewWorkerPool(2000), - } -} diff --git a/notification.go b/notification.go index b35a92c..95fe1ec 100644 --- a/notification.go +++ b/notification.go @@ -2,7 +2,8 @@ package main import ( "flag" - "fmt" + + "github.com/zeromicro/go-zero/core/logx" "app-cloudep-notification-service/gen_result/pb/notification" "app-cloudep-notification-service/internal/config" @@ -34,6 +35,6 @@ func main() { }) defer s.Stop() - fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + logx.Infof("Starting rpc server at %s...\n", c.ListenOn) s.Start() } diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..054b66a --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,34 @@ +package config + +type SMTPConfig struct { + Enable bool + Sort int + GoroutinePoolNum int + + Host string + Port int + Username string + Password string +} + +type AmazonSesSettings struct { + Enable bool + Sort int + PoolSize int + + Region string + Sender string + Charset string + AccessKey string + SecretKey string + Token string +} + +type MitakeSMSSender struct { + Enable bool + Sort int + PoolSize int + + User string + Password string +} diff --git a/pkg/domain/error.go b/pkg/domain/error.go new file mode 100644 index 0000000..0db475c --- /dev/null +++ b/pkg/domain/error.go @@ -0,0 +1,28 @@ +package domain + +import ( + "fmt" + "strings" + + ers "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + "github.com/zeromicro/go-zero/core/logx" +) + +func ThirdPartyError(scope uint32, ec ers.ErrorCode, s ...string) *ers.LibError { + return ers.NewError(scope, code.ThirdParty, ec.ToUint32(), fmt.Sprintf("thirty error: %s", strings.Join(s, " "))) +} + +func ThirdPartyErrorL(scope uint32, ec ers.ErrorCode, + l logx.Logger, filed []logx.LogField, s ...string) *ers.LibError { + e := ThirdPartyError(scope, ec, s...) + l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) + + return e +} + +const ( + NotificationErrorCode = 1 + iota + FailedToSendEmailErrorCode + FailedToSendSMSErrorCode +) diff --git a/pkg/domain/notification/language.go b/pkg/domain/notification/language.go new file mode 100644 index 0000000..693fb68 --- /dev/null +++ b/pkg/domain/notification/language.go @@ -0,0 +1,9 @@ +package notification + +// Language 定義模板請求 +type Language string + +const ( + LanguageZhTW Language = "zh-tw" + LanguageEnUS Language = "en-us" +) diff --git a/pkg/domain/notification/type.go b/pkg/domain/notification/type.go new file mode 100644 index 0000000..fc0810a --- /dev/null +++ b/pkg/domain/notification/type.go @@ -0,0 +1,16 @@ +package notification + +import "fmt" + +type TypeID int64 + +func (id TypeID) String() string { + return fmt.Sprintf("%4d", id) +} + +// 驗證碼通知類 0 ~ 100 +const ( + BindingEmail TypeID = 1 // 驗證碼:綁定 Email + BindingPhone TypeID = 2 // 驗證碼:綁定 手機 + ForgetPasswordVerify TypeID = 3 // 驗證碼: 忘記密碼 +) diff --git a/pkg/domain/repository/mail.go b/pkg/domain/repository/mail.go new file mode 100644 index 0000000..f95f764 --- /dev/null +++ b/pkg/domain/repository/mail.go @@ -0,0 +1,15 @@ +package repository + +import "context" + +type MailRepository interface { + // SendMail 送出 Email + SendMail(ctx context.Context, req MailReq) error +} + +type MailReq struct { + To []string + From string + Subject string + Body string +} diff --git a/pkg/domain/repository/sms.go b/pkg/domain/repository/sms.go new file mode 100644 index 0000000..b9b230e --- /dev/null +++ b/pkg/domain/repository/sms.go @@ -0,0 +1,16 @@ +package repository + +import ( + "context" +) + +type SMSClientRepository interface { + SendSMS(ctx context.Context, req SMSMessageRequest) error +} + +// SMSMessageRequest SMS 訊息請求結構 +type SMSMessageRequest struct { + PhoneNumber string `json:"phone_number" validate:"required,e164"` // 接收者號碼 (e164 格式用於驗證國際號碼) + RecipientName string `json:"recipient_name" validate:"required"` // 接收者姓名 + MessageContent string `json:"message_content" validate:"required"` // 要傳送的訊息 +} 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/delivary.go b/pkg/domain/usecase/delivary.go new file mode 100644 index 0000000..5bd3957 --- /dev/null +++ b/pkg/domain/usecase/delivary.go @@ -0,0 +1,43 @@ +package usecase + +import ( + "app-cloudep-notification-service/pkg/domain/repository" + "context" +) + +type DeliveryUseCase interface { + SendMessage(ctx context.Context, req SMSMessageRequest) error + SendEmail(ctx context.Context, req MailReq) error +} + +type MailReq struct { + To []string + From string + Subject string + Body string +} + +type SMSMessageRequest struct { + PhoneNumber string `json:"phone_number" validate:"required,e164"` // 接收者號碼 (e164 格式用於驗證國際號碼) + RecipientName string `json:"recipient_name" validate:"required"` // 接收者姓名 + MessageContent string `json:"message_content" validate:"required"` // 要傳送的訊息 +} + +type EmailTemplateResp struct { + Subject string `json:"subject"` // 郵件主題 + Body string `json:"body"` // 郵件內容 +} + +type SMSTemplateResp struct { + Body string `json:"body"` +} + +type SMSProvider struct { + Sort int64 + Repo repository.SMSClientRepository +} + +type EmailProvider struct { + Sort int64 + Repo repository.MailRepository +} 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/repository/aws_ses_mailer.go b/pkg/repository/aws_ses_mailer.go new file mode 100644 index 0000000..229cb4a --- /dev/null +++ b/pkg/repository/aws_ses_mailer.go @@ -0,0 +1,109 @@ +package repository + +import ( + "app-cloudep-notification-service/pkg/config" + "app-cloudep-notification-service/pkg/domain" + "app-cloudep-notification-service/pkg/domain/repository" + "context" + "time" + + "code.30cm.net/digimon/library-go/errs/code" + pool "code.30cm.net/digimon/library-go/worker_pool" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/ses/types" + "github.com/zeromicro/go-zero/core/logx" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ses" +) + +// AwsEmailDeliveryParam 傳送參數配置 +type AwsEmailDeliveryParam struct { + Conf *config.AmazonSesSettings +} + +type AwsEmailDeliveryRepository struct { + Client *ses.Client + Pool pool.WorkerPool +} + +func MustAwsSesMailRepository(param AwsEmailDeliveryParam) repository.MailRepository { + // 手動指定 AWS 配置,不使用默認配置 + cfg := aws.Config{ + Region: param.Conf.Region, // 自定義的 AWS 區域 + Credentials: credentials.NewStaticCredentialsProvider( + param.Conf.AccessKey, // AWS Access Key + param.Conf.SecretKey, // AWS Secret Key + "", + ), + } + // 創建 SES 客戶端 + sesClient := ses.NewFromConfig(cfg) + + return &AwsEmailDeliveryRepository{ + Client: sesClient, + Pool: pool.NewWorkerPool(param.Conf.PoolSize), + } +} + +func (use *AwsEmailDeliveryRepository) SendMail(ctx context.Context, req repository.MailReq) error { + err := use.Pool.Submit(func() { + // 設置郵件參數 + to := make([]string, 0, len(req.To)) + to = append(to, req.To...) + + input := &ses.SendEmailInput{ + Destination: &types.Destination{ + ToAddresses: to, + }, + Message: &types.Message{ + Body: &types.Body{ + Html: &types.Content{ + Charset: aws.String("UTF-8"), + Data: aws.String(req.Body), + }, + }, + Subject: &types.Content{ + Charset: aws.String("UTF-8"), + Data: aws.String(req.Subject), + }, + }, + Source: aws.String(req.From), + } + + // 發送郵件 + // TODO 不明原因送不出去,會被 context cancel 這裡先把它手動加到100sec + newCtx, cancel := context.WithTimeout(context.Background(), 100*time.Second) + defer cancel() + + //nolint:contextcheck + if _, err := use.Client.SendEmail(newCtx, input); err != nil { + _ = domain.ThirdPartyErrorL( + code.CloudEPNotification, + domain.FailedToSendEmailErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: req}, + {Key: "func", Value: "AwsEmailDeliveryU.SendEmail"}, + {Key: "err", Value: err.Error()}, + }, + "failed to send mail by aws ses") + } + }) + if err != nil { + e := domain.ThirdPartyErrorL( + code.CloudEPNotification, + domain.FailedToSendEmailErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: req}, + {Key: "func", Value: "AwsEmailDeliveryU.SendEmail"}, + {Key: "err", Value: err.Error()}, + }, + "failed to send mail by aws ses") + + return e + } + + return nil +} diff --git a/pkg/repository/mitake_sms_sender.go b/pkg/repository/mitake_sms_sender.go new file mode 100644 index 0000000..fc2ec23 --- /dev/null +++ b/pkg/repository/mitake_sms_sender.go @@ -0,0 +1,63 @@ +package repository + +import ( + "app-cloudep-notification-service/pkg/config" + "app-cloudep-notification-service/pkg/domain" + "app-cloudep-notification-service/pkg/domain/repository" + "context" + + "code.30cm.net/digimon/library-go/errs/code" + pool "code.30cm.net/digimon/library-go/worker_pool" + "github.com/minchao/go-mitake" + "github.com/zeromicro/go-zero/core/logx" +) + +// MitakeSMSDeliveryParam 三竹傳送參數配置 +type MitakeSMSDeliveryParam struct { + Conf *config.MitakeSMSSender +} + +type MitakeSMSDeliveryRepository struct { + Client *mitake.Client + Pool pool.WorkerPool +} + +func (use *MitakeSMSDeliveryRepository) SendSMS(ctx context.Context, req repository.SMSMessageRequest) error { + // 用 goroutine pool 送,否則會超時 + err := use.Pool.Submit(func() { + message := mitake.Message{ + Dstaddr: req.PhoneNumber, + Destname: req.RecipientName, + Smbody: req.MessageContent, + } + _, err := use.Client.Send(message) + if err != nil { + logx.Error("failed to send sms via mitake") + } + }) + + if err != nil { + // 錯誤代碼 20-201-04 + e := domain.ThirdPartyErrorL( + code.CloudEPNotification, + domain.FailedToSendSMSErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: req}, + {Key: "func", Value: "MitakeSMSDeliveryRepository.Client.Send"}, + {Key: "err", Value: err.Error()}, + }, + "failed to send sns by mitake").Wrap(err) + + return e + } + + return nil +} + +func MustMitakeRepository(param MitakeSMSDeliveryParam) repository.SMSClientRepository { + return &MitakeSMSDeliveryRepository{ + Client: mitake.NewClient(param.Conf.User, param.Conf.Password, nil), + Pool: pool.NewWorkerPool(param.Conf.PoolSize), + } +} diff --git a/pkg/repository/smtp_mailer.go b/pkg/repository/smtp_mailer.go new file mode 100644 index 0000000..0c13a24 --- /dev/null +++ b/pkg/repository/smtp_mailer.go @@ -0,0 +1,52 @@ +package repository + +import ( + "app-cloudep-notification-service/pkg/config" + "app-cloudep-notification-service/pkg/domain/repository" + "context" + + pool "code.30cm.net/digimon/library-go/worker_pool" + "github.com/zeromicro/go-zero/core/logx" + "gopkg.in/gomail.v2" +) + +type SMTPMailUseCaseParam struct { + Conf config.SMTPConfig +} + +type SMTPMailRepository struct { + Client *gomail.Dialer + Pool pool.WorkerPool +} + +func MustSMTPUseCase(param SMTPMailUseCaseParam) repository.MailRepository { + return &SMTPMailRepository{ + Client: gomail.NewDialer( + param.Conf.Host, + param.Conf.Port, + param.Conf.Username, + param.Conf.Password, + ), + Pool: pool.NewWorkerPool(param.Conf.GoroutinePoolNum), + } +} + +func (repo *SMTPMailRepository) SendMail(_ context.Context, req repository.MailReq) error { + // 用 goroutine pool 送,否則會超時 + err := repo.Pool.Submit(func() { + m := gomail.NewMessage() + m.SetHeader("From", req.From) + m.SetHeader("To", req.To...) + m.SetHeader("Subject", req.Subject) + m.SetBody("text/html", req.Body) + if err := repo.Client.DialAndSend(m); err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "MailUseCase.SendMail"), + logx.Field("req", req), + logx.Field("err", err), + ).Error("failed to send mail by mailgun") + } + }) + + return err +} diff --git a/pkg/usecase/delivery.go b/pkg/usecase/delivery.go new file mode 100644 index 0000000..1c05c72 --- /dev/null +++ b/pkg/usecase/delivery.go @@ -0,0 +1,75 @@ +package usecase + +import ( + "app-cloudep-notification-service/pkg/domain/repository" + "app-cloudep-notification-service/pkg/domain/usecase" + "context" + "sort" + "time" +) + +// DeliveryUseCaseParam 傳送參數配置 +type DeliveryUseCaseParam struct { + SMSProviders []usecase.SMSProvider + EmailProviders []usecase.EmailProvider +} + +// DeliveryUseCase 通知 +type DeliveryUseCase struct { + param DeliveryUseCaseParam +} + +func MustDeliveryUseCase(param DeliveryUseCaseParam) usecase.DeliveryUseCase { + return &DeliveryUseCase{ + param: param, + } +} + +func (use *DeliveryUseCase) SendMessage(ctx context.Context, req usecase.SMSMessageRequest) error { + var err error + // 根據 Sort 欄位對 SMSProviders 進行排序 + sort.Slice(use.param.SMSProviders, func(i, j int) bool { + return use.param.SMSProviders[i].Sort < use.param.SMSProviders[j].Sort + }) + + newCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + // 依序嘗試發送 + for _, provider := range use.param.SMSProviders { + if err = provider.Repo.SendSMS(newCtx, repository.SMSMessageRequest{ + PhoneNumber: req.PhoneNumber, + RecipientName: req.RecipientName, + MessageContent: req.MessageContent, + }); err == nil { + return nil // 發送成功 + } + } + + return err +} + +func (use *DeliveryUseCase) SendEmail(ctx context.Context, req usecase.MailReq) error { + var err error + // 根據 Sort 欄位對 SMSProviders 進行排序 + sort.Slice(use.param.EmailProviders, func(i, j int) bool { + return use.param.EmailProviders[i].Sort < use.param.EmailProviders[j].Sort + }) + + newCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + // 依序嘗試發送 dreq + for _, provider := range use.param.EmailProviders { + if err = provider.Repo.SendMail(newCtx, repository.MailReq{ + From: req.From, + To: req.To, + Subject: req.Subject, + Body: req.Body, + }); err == nil { + return nil // 發送成功 + } + } + + return err +} 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 +}