diff --git a/.gitignore b/.gitignore index 61a2d61..0a56a16 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ go.sum account/ gen_result/ etc/service.yaml -./client \ No newline at end of file +client/ \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..5518484 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,140 @@ +run: + timeout: 3m + # Exit code when at least one issue was found. + # Default: 1 + issues-exit-code: 2 + # Include test files or not. + # Default: true + tests: false + +# Reference URL: https://golangci-lint.run/usage/linters/ +linters: + # Disable everything by default so upgrades to not include new - default + # enabled- linters. + disable-all: true + # Specifically enable linters we want to use. + enable: + # - depguard + - errcheck + # - godot + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - misspell + - revive + # - staticcheck + - typecheck + - unused + # - wsl + - asasalint + - asciicheck + - bidichk + - bodyclose + # - containedctx + - contextcheck + # - cyclop + # - varnamelen + # - gci + - wastedassign + - whitespace + # - wrapcheck + - thelper + - tparallel + - unconvert + - unparam + - usestdlibvars + - tenv + - testableexamples + - stylecheck + - sqlclosecheck + - nosprintfhostport + - paralleltest + - prealloc + - predeclared + - promlinter + - reassign + - rowserrcheck + - nakedret + - nestif + - nilerr + - nilnil + - nlreturn + - noctx + - nolintlint + - nonamedreturns + - decorder + - dogsled + # - dupl + - dupword + - durationcheck + - errchkjson + - errname + - errorlint + # - execinquery + - exhaustive + - exportloopref + - forbidigo + - forcetypeassert + # - gochecknoglobals + - gochecknoinits + - gocognit + - goconst + - gocritic + - gocyclo + # - godox + # - goerr113 + # - gofumpt + - goheader + - gomoddirectives + # - gomodguard always failed + - goprintffuncname + - gosec + - grouper + - importas + - interfacebloat + # - ireturn + - lll + - loggercheck + - maintidx + - makezero + +issues: + exclude-rules: + - path: _test\.go + linters: + - funlen + - goconst + - interfacer + - dupl + - lll + - goerr113 + - errcheck + - gocritic + - cyclop + - wrapcheck + - gocognit + - contextcheck + +linters-settings: + gci: + sections: + - standard # Standard section: captures all standard packages. + - default # Default section: contains all imports that could not be matched to another section type. + gocognit: + # Minimal code complexity to report. + # Default: 30 (but we recommend 10-20) + min-complexity: 40 + nestif: + # Minimal complexity of if statements to report. + # Default: 5 + min-complexity: 10 + lll: + # Max line length, lines longer will be reported. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option. + # Default: 120. + line-length: 200 + # Tab width in spaces. + # Default: 1 + tab-width: 1 diff --git a/client/senderservice/sender_service.go b/client/senderservice/sender_service.go index a647688..33bcb18 100644 --- a/client/senderservice/sender_service.go +++ b/client/senderservice/sender_service.go @@ -22,11 +22,11 @@ type ( SenderService interface { // SendMail 寄信 SendMail(ctx context.Context, in *SendMailReq, opts ...grpc.CallOption) (*OKResp, error) - // SendSMS 寄簡訊 + // SendSms 寄簡訊 SendSms(ctx context.Context, in *SendSMSReq, opts ...grpc.CallOption) (*OKResp, error) - // SendMailByTemplateID 寄送模板信件 + // SendMailByTemplateId 寄送模板信件 SendMailByTemplateId(ctx context.Context, in *SendByTemplateIDReq, opts ...grpc.CallOption) (*OKResp, error) - // SendSMSByTemplateID 寄送模板簡訊 + // SendSmsByTemplateId 寄送模板簡訊 SendSmsByTemplateId(ctx context.Context, in *SendByTemplateIDReq, opts ...grpc.CallOption) (*OKResp, error) } @@ -47,19 +47,19 @@ func (m *defaultSenderService) SendMail(ctx context.Context, in *SendMailReq, op return client.SendMail(ctx, in, opts...) } -// SendSMS 寄簡訊 +// SendSms 寄簡訊 func (m *defaultSenderService) SendSms(ctx context.Context, in *SendSMSReq, opts ...grpc.CallOption) (*OKResp, error) { client := notification.NewSenderServiceClient(m.cli.Conn()) return client.SendSms(ctx, in, opts...) } -// SendMailByTemplateID 寄送模板信件 +// SendMailByTemplateId 寄送模板信件 func (m *defaultSenderService) SendMailByTemplateId(ctx context.Context, in *SendByTemplateIDReq, opts ...grpc.CallOption) (*OKResp, error) { client := notification.NewSenderServiceClient(m.cli.Conn()) return client.SendMailByTemplateId(ctx, in, opts...) } -// SendSMSByTemplateID 寄送模板簡訊 +// 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...) diff --git a/etc/service.example.yaml b/etc/service.example.yaml new file mode 100644 index 0000000..9474363 --- /dev/null +++ b/etc/service.example.yaml @@ -0,0 +1,16 @@ +Name: service.rpc +ListenOn: 0.0.0.0:8080 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: service.rpc +SMTP: + Host: smtp.mailgun.org + Port: 25 + User: xxx + Password: 000 + +SMSSender: + User: daniel@30cm.net + Password : test123 + diff --git a/etc/service.yaml b/etc/service.yaml deleted file mode 100644 index f9a189a..0000000 --- a/etc/service.yaml +++ /dev/null @@ -1,6 +0,0 @@ -Name: service.rpc -ListenOn: 0.0.0.0:8080 -Etcd: - Hosts: - - 127.0.0.1:2379 - Key: service.rpc diff --git a/generate/protobuf/service.proto b/generate/protobuf/service.proto index 2d84dce..e3d5304 100644 --- a/generate/protobuf/service.proto +++ b/generate/protobuf/service.proto @@ -13,11 +13,13 @@ message SendMailReq { string to = 1; string subject = 2; string body = 3; + string from =4; } message SendSMSReq { string to = 1; string body = 2; + string recipient_name=3; } message SendByTemplateIDReq { @@ -31,11 +33,11 @@ message SendByTemplateIDReq { service SenderService { // SendMail 寄信 rpc SendMail(SendMailReq) returns(OKResp); - // SendSMS 寄簡訊 + // SendSms 寄簡訊 rpc SendSms(SendSMSReq) returns(OKResp); - // SendMailByTemplateID 寄送模板信件 + // SendMailByTemplateId 寄送模板信件 rpc SendMailByTemplateId(SendByTemplateIDReq) returns(OKResp); - // SendSMSByTemplateID 寄送模板簡訊 + // SendSmsByTemplateId 寄送模板簡訊 rpc SendSmsByTemplateId(SendByTemplateIDReq) returns(OKResp); } diff --git a/go.mod b/go.mod index a339282..f983540 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,13 @@ go 1.22.3 require ( code.30cm.net/digimon/library-go/errors v1.0.0 + code.30cm.net/digimon/library-go/validator v1.0.0 + code.30cm.net/digimon/library-go/worker_pool v0.0.0-20240820153352-f9c90a90f5e2 + github.com/minchao/go-mitake v1.0.0 github.com/zeromicro/go-zero v1.7.0 google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df ) require ( @@ -19,11 +23,15 @@ 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 @@ -34,6 +42,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // 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 @@ -41,6 +50,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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 github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.5.0 // indirect @@ -66,14 +76,17 @@ 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 golang.org/x/sys v0.22.0 // indirect golang.org/x/term v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/internal/config/config.go b/internal/config/config.go index c1f85b9..3521805 100755 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,4 +4,16 @@ import "github.com/zeromicro/go-zero/zrpc" type Config struct { zrpc.RpcServerConf + + SMTP struct { + Host string + Port int + User string + Password string + } + + SMSSender struct { + User string + Password string + } } diff --git a/internal/domain/usecase/mail.go b/internal/domain/usecase/mail.go new file mode 100644 index 0000000..95fb400 --- /dev/null +++ b/internal/domain/usecase/mail.go @@ -0,0 +1,18 @@ +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 new file mode 100644 index 0000000..c4e6d66 --- /dev/null +++ b/internal/domain/usecase/sms.go @@ -0,0 +1,18 @@ +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/send_mail_by_template_id_logic.go b/internal/logic/senderservice/send_mail_by_template_id_logic.go index abf4b49..b9decc9 100644 --- a/internal/logic/senderservice/send_mail_by_template_id_logic.go +++ b/internal/logic/senderservice/send_mail_by_template_id_logic.go @@ -3,9 +3,10 @@ package senderservicelogic import ( "app-cloudep-notification-service/gen_result/pb/notification" "app-cloudep-notification-service/internal/svc" - ers "code.30cm.net/digimon/library-go/errors" "context" "fmt" + + ers "code.30cm.net/digimon/library-go/errors" "github.com/zeromicro/go-zero/core/logx" ) diff --git a/internal/logic/senderservice/send_mail_logic.go b/internal/logic/senderservice/send_mail_logic.go index 6af7fc1..c7aa930 100644 --- a/internal/logic/senderservice/send_mail_logic.go +++ b/internal/logic/senderservice/send_mail_logic.go @@ -1,8 +1,11 @@ package senderservicelogic import ( + "app-cloudep-notification-service/internal/domain/usecase" "context" + ers "code.30cm.net/digimon/library-go/errors" + "app-cloudep-notification-service/gen_result/pb/notification" "app-cloudep-notification-service/internal/svc" @@ -23,9 +26,43 @@ 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) { - // todo: add your logic here and delete this line + if err := l.svcCtx.Validate.ValidateAll(&sendMailReq{ + To: in.GetTo(), + Subject: in.GetSubject(), + Body: in.GetBody(), + From: in.GetFrom(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + // 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 { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "MailSender.SendMail"), + logx.Field("in", in), + logx.Field("err", err), + ).Error(err.Error()) + return nil, ers.ArkInternal("MailSender.SendMail failed to send mail") + } return ¬ification.OKResp{}, nil } diff --git a/internal/logic/senderservice/send_sms_logic.go b/internal/logic/senderservice/send_sms_logic.go index 7e5e4f9..83d79fa 100644 --- a/internal/logic/senderservice/send_sms_logic.go +++ b/internal/logic/senderservice/send_sms_logic.go @@ -1,8 +1,11 @@ package senderservicelogic import ( + "app-cloudep-notification-service/internal/domain/usecase" "context" + ers "code.30cm.net/digimon/library-go/errors" + "app-cloudep-notification-service/gen_result/pb/notification" "app-cloudep-notification-service/internal/svc" @@ -23,9 +26,39 @@ func NewSendSmsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsLo } } -// SendSMS 寄簡訊 +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) { - // todo: add your logic here and delete this line + if err := l.svcCtx.Validate.ValidateAll(&sendSMSReq{ + RecipientName: in.GetTo(), + Body: in.GetBody(), + RecipientAddress: in.GetTo(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + // TODO 以後可以做換線 + err := l.svcCtx.SMSSender.SendSMS(l.ctx, usecase.SMSReq{ + RecipientAddress: in.GetTo(), + RecipientName: in.GetRecipientName(), + Body: in.GetBody(), + }) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "SMSSender.SendSMS"), + logx.Field("in", in), + logx.Field("err", err), + ).Error(err.Error()) + return nil, ers.ArkInternal("SMSSender.SendSMS failed to send sms") + } return ¬ification.OKResp{}, nil } diff --git a/internal/server/senderservice/sender_service_server.go b/internal/server/senderservice/sender_service_server.go index 14d51b9..99f6d32 100644 --- a/internal/server/senderservice/sender_service_server.go +++ b/internal/server/senderservice/sender_service_server.go @@ -8,7 +8,6 @@ import ( "app-cloudep-notification-service/gen_result/pb/notification" senderservicelogic "app-cloudep-notification-service/internal/logic/senderservice" - "app-cloudep-notification-service/internal/logic/senderservice" "app-cloudep-notification-service/internal/svc" ) @@ -29,19 +28,19 @@ func (s *SenderServiceServer) SendMail(ctx context.Context, in *notification.Sen return l.SendMail(in) } -// SendSMS 寄簡訊 +// SendSms 寄簡訊 func (s *SenderServiceServer) SendSms(ctx context.Context, in *notification.SendSMSReq) (*notification.OKResp, error) { l := senderservicelogic.NewSendSmsLogic(ctx, s.svcCtx) return l.SendSms(in) } -// SendMailByTemplateID 寄送模板信件 +// 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 寄送模板簡訊 +// SendSmsByTemplateId 寄送模板簡訊 func (s *SenderServiceServer) SendSmsByTemplateId(ctx context.Context, in *notification.SendByTemplateIDReq) (*notification.OKResp, error) { l := senderservicelogic.NewSendSmsByTemplateIdLogic(ctx, s.svcCtx) return l.SendSmsByTemplateId(in) diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index d0f930f..7ce53b9 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -1,13 +1,26 @@ package svc -import "app-cloudep-notification-service/internal/config" +import ( + "app-cloudep-notification-service/internal/config" + domainUC "app-cloudep-notification-service/internal/domain/usecase" + "app-cloudep-notification-service/internal/usecase" + + v "code.30cm.net/digimon/library-go/validator" +) type ServiceContext struct { Config config.Config + + Validate v.Validate + MailSender domainUC.MailClientUseCase + SMSSender domainUC.SMSClientUseCase } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ - Config: c, + Config: c, + MailSender: usecase.MustMailgunUseCase(usecase.MailUseCaseParam{Conf: c}), + SMSSender: usecase.MustMitakeUseCase(usecase.SMSUseCaseParam{Conf: c}), + Validate: v.MustValidator(), } } diff --git a/internal/usecase/mitake.go b/internal/usecase/mitake.go new file mode 100644 index 0000000..ac332bc --- /dev/null +++ b/internal/usecase/mitake.go @@ -0,0 +1,36 @@ +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 new file mode 100644 index 0000000..f4d97e7 --- /dev/null +++ b/internal/usecase/smtp.go @@ -0,0 +1,56 @@ +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), + } +}