From 3b2655fbc4e06a02d646a06603686362e4957b36 Mon Sep 17 00:00:00 2001 From: wuzhixiang <657873584@qq.com> Date: Mon, 17 Nov 2025 20:39:27 +0800 Subject: [PATCH 01/10] feat: notification --- contracts/binding/binding.go | 55 ++--- contracts/facades/facades.go | 110 ++++----- contracts/foundation/application.go | 3 + contracts/notification/notification.go | 20 ++ facades/facades.go | 5 + foundation/container.go | 12 + notification/application.go | 56 +++++ notification/application_test.go | 208 ++++++++++++++++++ notification/channel_manager.go | 40 ++++ notification/channels/database.go | 42 ++++ notification/channels/mail.go | 41 ++++ notification/job.go | 26 +++ notification/job_test.go | 49 +++++ notification/models/notification.go | 16 ++ notification/service_provider.go | 53 +++++ .../utils/notification_method_caller.go | 65 ++++++ 16 files changed, 720 insertions(+), 81 deletions(-) create mode 100644 contracts/notification/notification.go create mode 100644 notification/application.go create mode 100644 notification/application_test.go create mode 100644 notification/channel_manager.go create mode 100644 notification/channels/database.go create mode 100644 notification/channels/mail.go create mode 100644 notification/job.go create mode 100644 notification/job_test.go create mode 100644 notification/models/notification.go create mode 100644 notification/service_provider.go create mode 100644 notification/utils/notification_method_caller.go diff --git a/contracts/binding/binding.go b/contracts/binding/binding.go index 0a05604f0..d6c20f206 100644 --- a/contracts/binding/binding.go +++ b/contracts/binding/binding.go @@ -1,33 +1,34 @@ package binding const ( - Artisan = "goravel.artisan" - Auth = "goravel.auth" - Cache = "goravel.cache" - Config = "goravel.config" - Crypt = "goravel.crypt" - DB = "goravel.db" - Event = "goravel.event" - Gate = "goravel.gate" - Grpc = "goravel.grpc" - Hash = "goravel.hash" - Http = "goravel.http" - Lang = "goravel.lang" - Log = "goravel.log" - Mail = "goravel.mail" - Orm = "goravel.orm" - Process = "goravel.process" - Queue = "goravel.queue" - RateLimiter = "goravel.rate_limiter" - Route = "goravel.route" - Schedule = "goravel.schedule" - Schema = "goravel.schema" - Seeder = "goravel.seeder" - Session = "goravel.session" - Storage = "goravel.storage" - Testing = "goravel.testing" - Validation = "goravel.validation" - View = "goravel.view" + Artisan = "goravel.artisan" + Auth = "goravel.auth" + Cache = "goravel.cache" + Config = "goravel.config" + Crypt = "goravel.crypt" + DB = "goravel.db" + Event = "goravel.event" + Gate = "goravel.gate" + Grpc = "goravel.grpc" + Hash = "goravel.hash" + Http = "goravel.http" + Lang = "goravel.lang" + Log = "goravel.log" + Mail = "goravel.mail" + Orm = "goravel.orm" + Process = "goravel.process" + Queue = "goravel.queue" + RateLimiter = "goravel.rate_limiter" + Route = "goravel.route" + Schedule = "goravel.schedule" + Schema = "goravel.schema" + Seeder = "goravel.seeder" + Session = "goravel.session" + Storage = "goravel.storage" + Testing = "goravel.testing" + Validation = "goravel.validation" + View = "goravel.view" + Notification = "goravel.notification" ) type Relationship struct { diff --git a/contracts/facades/facades.go b/contracts/facades/facades.go index f2762666d..d61b260ff 100644 --- a/contracts/facades/facades.go +++ b/contracts/facades/facades.go @@ -3,61 +3,63 @@ package facades import "github.com/goravel/framework/contracts/binding" const ( - Artisan = "Artisan" - Auth = "Auth" - Cache = "Cache" - Config = "Config" - Crypt = "Crypt" - DB = "DB" - Event = "Event" - Gate = "Gate" - Grpc = "Grpc" - Hash = "Hash" - Http = "Http" - Lang = "Lang" - Log = "Log" - Mail = "Mail" - Process = "Process" - Orm = "Orm" - Queue = "Queue" - RateLimiter = "RateLimiter" - Route = "Route" - Schedule = "Schedule" - Schema = "Schema" - Seeder = "Seeder" - Session = "Session" - Storage = "Storage" - Testing = "Testing" - Validation = "Validation" - View = "View" + Artisan = "Artisan" + Auth = "Auth" + Cache = "Cache" + Config = "Config" + Crypt = "Crypt" + DB = "DB" + Event = "Event" + Gate = "Gate" + Grpc = "Grpc" + Hash = "Hash" + Http = "Http" + Lang = "Lang" + Log = "Log" + Mail = "Mail" + Process = "Process" + Orm = "Orm" + Queue = "Queue" + RateLimiter = "RateLimiter" + Route = "Route" + Schedule = "Schedule" + Schema = "Schema" + Seeder = "Seeder" + Session = "Session" + Storage = "Storage" + Testing = "Testing" + Validation = "Validation" + View = "View" + Notification = "Notification" ) var FacadeToBinding = map[string]string{ - Artisan: binding.Artisan, - Auth: binding.Auth, - Cache: binding.Cache, - Config: binding.Config, - Crypt: binding.Crypt, - DB: binding.DB, - Event: binding.Event, - Gate: binding.Gate, - Grpc: binding.Grpc, - Hash: binding.Hash, - Http: binding.Http, - Lang: binding.Lang, - Log: binding.Log, - Mail: binding.Mail, - Orm: binding.Orm, - Process: binding.Process, - Queue: binding.Queue, - RateLimiter: binding.RateLimiter, - Route: binding.Route, - Schedule: binding.Schedule, - Schema: binding.Schema, - Seeder: binding.Seeder, - Session: binding.Session, - Storage: binding.Storage, - Testing: binding.Testing, - Validation: binding.Validation, - View: binding.View, + Artisan: binding.Artisan, + Auth: binding.Auth, + Cache: binding.Cache, + Config: binding.Config, + Crypt: binding.Crypt, + DB: binding.DB, + Event: binding.Event, + Gate: binding.Gate, + Grpc: binding.Grpc, + Hash: binding.Hash, + Http: binding.Http, + Lang: binding.Lang, + Log: binding.Log, + Mail: binding.Mail, + Orm: binding.Orm, + Process: binding.Process, + Queue: binding.Queue, + RateLimiter: binding.RateLimiter, + Route: binding.Route, + Schedule: binding.Schedule, + Schema: binding.Schema, + Seeder: binding.Seeder, + Session: binding.Session, + Storage: binding.Storage, + Testing: binding.Testing, + Validation: binding.Validation, + View: binding.View, + Notification: binding.Notification, } diff --git a/contracts/foundation/application.go b/contracts/foundation/application.go index cd57329fd..5ccf9eb16 100644 --- a/contracts/foundation/application.go +++ b/contracts/foundation/application.go @@ -2,6 +2,7 @@ package foundation import ( "context" + "github.com/goravel/framework/contracts/notification" "github.com/goravel/framework/contracts/auth" "github.com/goravel/framework/contracts/auth/access" @@ -166,6 +167,8 @@ type Application interface { MakeValidation() validation.Validation // MakeView resolves the view instance. MakeView() view.View + // MakeNotification resolves the notification instance. + MakeNotification() notification.Notification // MakeSeeder resolves the seeder instance. MakeSeeder() seeder.Facade // MakeWith resolves the given type with the given parameters from the container. diff --git a/contracts/notification/notification.go b/contracts/notification/notification.go new file mode 100644 index 000000000..4da940924 --- /dev/null +++ b/contracts/notification/notification.go @@ -0,0 +1,20 @@ +package notification + +type Notification interface { + Send(notifiable Notifiable) error +} + +type Notif interface { + // Via Return to the list of channel names + Via(notifiable Notifiable) []string +} + +type Channel interface { + // Send sends the given notification to the given notifiable. + Send(notifiable Notifiable, notif interface{}) error +} + +type Notifiable interface { + // RouteNotificationFor returns the route notification for the given channel. + RouteNotificationFor(channel string) any +} diff --git a/facades/facades.go b/facades/facades.go index 6631ec767..b0d9dff7b 100644 --- a/facades/facades.go +++ b/facades/facades.go @@ -2,6 +2,7 @@ package facades import ( "context" + "github.com/goravel/framework/contracts/notification" "github.com/goravel/framework/contracts/auth" "github.com/goravel/framework/contracts/auth/access" @@ -150,3 +151,7 @@ func Validation() validation.Validation { func View() view.View { return App().MakeView() } + +func Notification() notification.Notification { + return App().MakeNotification() +} diff --git a/foundation/container.go b/foundation/container.go index c4fc84a4a..8c1e0e31c 100644 --- a/foundation/container.go +++ b/foundation/container.go @@ -26,6 +26,7 @@ import ( contractshttpclient "github.com/goravel/framework/contracts/http/client" contractslog "github.com/goravel/framework/contracts/log" contractsmail "github.com/goravel/framework/contracts/mail" + contractsotification "github.com/goravel/framework/contracts/notification" contractsprocess "github.com/goravel/framework/contracts/process" contractsqueue "github.com/goravel/framework/contracts/queue" contractsroute "github.com/goravel/framework/contracts/route" @@ -35,6 +36,7 @@ import ( contractstranslation "github.com/goravel/framework/contracts/translation" contractsvalidation "github.com/goravel/framework/contracts/validation" contractsview "github.com/goravel/framework/contracts/view" + "github.com/goravel/framework/support/color" ) @@ -386,6 +388,16 @@ func (r *Container) MakeView() contractsview.View { return instance.(contractsview.View) } +func (r *Container) MakeNotification() contractsotification.Notification { + instance, err := r.Make(facades.FacadeToBinding[facades.Notification]) + if err != nil { + color.Errorln(err) + return nil + } + + return instance.(contractsotification.Notification) +} + func (r *Container) MakeWith(key any, parameters map[string]any) (any, error) { return r.make(key, parameters) } diff --git a/notification/application.go b/notification/application.go new file mode 100644 index 000000000..b8ced8071 --- /dev/null +++ b/notification/application.go @@ -0,0 +1,56 @@ +package notification + +import ( + "fmt" + "github.com/goravel/framework/contracts/config" + contractsqueuedb "github.com/goravel/framework/contracts/database/db" + contractsmail "github.com/goravel/framework/contracts/mail" + "github.com/goravel/framework/contracts/notification" + contractsqueue "github.com/goravel/framework/contracts/queue" + "github.com/goravel/framework/errors" + "github.com/goravel/framework/notification/channels" +) + +type Application struct { + config config.Config + queue contractsqueue.Queue + db contractsqueuedb.DB + mail contractsmail.Mail +} + +func NewApplication(config config.Config, queue contractsqueue.Queue, db contractsqueuedb.DB, mail contractsmail.Mail) (*Application, error) { + return &Application{ + config: config, + queue: queue, + db: db, + mail: mail, + }, nil +} + +// Send a notification. +func (r *Application) Send(notifiable notification.Notifiable, notif notification.Notif) error { + vias := notif.Via(notifiable) + if len(vias) == 0 { + return errors.New("no channels defined for notification") + } + + for _, chName := range vias { + ch, ok := GetChannel(chName) + if !ok { + return fmt.Errorf("channel not registered: %s", chName) + } + if chName == "database" { + if databaseChannel, ok := ch.(*channels.DatabaseChannel); ok { + databaseChannel.SetDB(r.db) + } + } else if chName == "mail" { + if mailChannel, ok := ch.(*channels.MailChannel); ok { + mailChannel.SetMail(r.mail) + } + } + if err := ch.Send(notifiable, notif); err != nil { + return fmt.Errorf("channel %s send error: %w", chName, err) + } + } + return nil +} diff --git a/notification/application_test.go b/notification/application_test.go new file mode 100644 index 000000000..8e7ce0ed2 --- /dev/null +++ b/notification/application_test.go @@ -0,0 +1,208 @@ +package notification + +import ( + "fmt" + contractsqueuedb "github.com/goravel/framework/contracts/database/db" + "github.com/goravel/framework/contracts/notification" + contractsqueue "github.com/goravel/framework/contracts/queue" + + "github.com/goravel/framework/foundation/json" + "github.com/goravel/framework/mail" + mocksconfig "github.com/goravel/framework/mocks/config" + mocksdb "github.com/goravel/framework/mocks/database/db" + "github.com/goravel/framework/notification/channels" + "github.com/goravel/framework/queue" + "github.com/goravel/framework/support" + "github.com/goravel/framework/support/color" + "github.com/goravel/framework/support/file" + "github.com/spf13/viper" + "github.com/stretchr/testify/suite" + "os" + "testing" +) + +type User struct { + ID int `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + Phone string `json:"phone"` +} + +func (u User) RouteNotificationFor(channel string) any { + switch channel { + case "mail": + return u.Email + case "database": + return u.ID + default: + return "" + } +} + +type RegisterSuccessNotification struct { +} + +func (n RegisterSuccessNotification) Via(notifiable notification.Notifiable) []string { + return []string{ + "mail", + } +} +func (n RegisterSuccessNotification) ToMail(notifiable notification.Notifiable) map[string]string { + return map[string]string{ + "subject": "【sign】Register success", + "content": "Congratulations, your registration is successful!", + } +} + +type LoginSuccessNotification struct { +} + +func (n LoginSuccessNotification) Via(notifiable notification.Notifiable) []string { + return []string{ + "database", + } +} +func (n LoginSuccessNotification) ToDatabase(notifiable notification.Notifiable) map[string]string { + return map[string]string{ + "title": "Login success", + "content": "Congratulations, your login is successful!", + } +} + +type ApplicationTestSuite struct { + suite.Suite + mockConfig *mocksconfig.Config +} + +func TestApplicationTestSuite(t *testing.T) { + if !file.Exists(support.EnvFilePath) && os.Getenv("MAIL_HOST") == "" { + color.Errorln("No notification tests run, need create .env based on .env.example, then initialize it") + return + } + + suite.Run(t, new(ApplicationTestSuite)) +} + +func (s *ApplicationTestSuite) SetupTest() { +} + +func (s *ApplicationTestSuite) TestMailNotification() { + s.mockConfig = mockConfig(465) + + queueFacade := mockQueueFacade(s.mockConfig) + + mailFacade, err := mail.NewApplication(s.mockConfig, nil) + s.Nil(err) + + app, err := NewApplication(s.mockConfig, queueFacade, nil, mailFacade) + s.Nil(err) + + var user = User{ + ID: 1, + Email: "657873584@qq.com", + Name: "test", + } + + var registerSuccessNotification = RegisterSuccessNotification{} + + RegisterChannel("mail", &channels.MailChannel{}) + + err = app.Send(user, registerSuccessNotification) + s.Nil(err) +} + +func (s *ApplicationTestSuite) TestDatabaseNotification() { + s.mockConfig = mockConfig(465) + + var mockDB contractsqueuedb.DB + dbFacade := mocksdb.NewDB(s.T()) + dbFacade.EXPECT().Connection("mysql").Return(mockDB).Once() + dbFacade.EXPECT().Table("notifications").Return(nil).Once() + + fmt.Println(mockDB) + + app, err := NewApplication(s.mockConfig, nil, mockDB, nil) + s.Nil(err) + + var user = User{ + ID: 1, + Email: "657873584@qq.com", + Name: "test", + } + + var loginSuccessNotification = LoginSuccessNotification{} + + RegisterChannel("database", &channels.DatabaseChannel{}) + + err = app.Send(user, loginSuccessNotification) + s.Nil(err) +} + +func mockQueueFacade(mockConfig *mocksconfig.Config) contractsqueue.Queue { + mockConfig.EXPECT().GetString("queue.default").Return("redis").Once() + mockConfig.EXPECT().GetString("queue.connections.redis.queue", "default").Return("default").Once() + mockConfig.EXPECT().GetInt("queue.connections.redis.concurrent", 1).Return(2).Once() + mockConfig.EXPECT().GetString("app.name", "goravel").Return("goravel").Once() + mockConfig.EXPECT().GetBool("app.debug").Return(true).Once() + mockConfig.EXPECT().GetString("queue.failed.database").Return("mysql").Once() + mockConfig.EXPECT().GetString("queue.failed.table").Return("failed_jobs").Once() + + queueFacade := queue.NewApplication(queue.NewConfig(mockConfig), nil, queue.NewJobStorer(), json.New(), nil) + queueFacade.Register([]contractsqueue.Job{ + NewSendNotificationJob(mockConfig), + }) + return queueFacade +} + +func mockConfig(mailPort int) *mocksconfig.Config { + config := &mocksconfig.Config{} + config.On("GetString", "app.name").Return("goravel") + config.On("GetString", "queue.default").Return("sync") + config.On("GetString", "queue.connections.sync.queue", "default").Return("default") + config.On("GetString", "queue.connections.sync.driver").Return("sync") + config.On("GetInt", "queue.connections.sync.concurrent", 1).Return(1) + config.On("GetString", "queue.failed.database").Return("database") + config.On("GetString", "queue.failed.table").Return("failed_jobs") + + if file.Exists(support.EnvFilePath) { + vip := viper.New() + vip.SetConfigName(support.EnvFilePath) + vip.SetConfigType("env") + vip.AddConfigPath(".") + _ = vip.ReadInConfig() + vip.SetEnvPrefix("goravel") + vip.AutomaticEnv() + + config.On("GetString", "mail.host").Return(vip.Get("MAIL_HOST")) + config.On("GetInt", "mail.port").Return(mailPort) + config.On("GetString", "mail.from.address").Return(vip.Get("MAIL_FROM_ADDRESS")) + config.On("GetString", "mail.from.name").Return(vip.Get("MAIL_FROM_NAME")) + config.On("GetString", "mail.username").Return(vip.Get("MAIL_USERNAME")) + config.On("GetString", "mail.password").Return(vip.Get("MAIL_PASSWORD")) + config.On("GetString", "mail.to").Return(vip.Get("MAIL_TO")) + config.On("GetString", "mail.cc").Return(vip.Get("MAIL_CC")) + config.On("GetString", "mail.bcc").Return(vip.Get("MAIL_BCC")) + config.EXPECT().GetString("mail.template.default", "html").Return("html").Once() + config.EXPECT().GetString("mail.template.engines.html.driver", "html").Return("html").Once() + config.EXPECT().GetString("mail.template.engines.html.path", "resources/views/mail"). + Return("resources/views/mail").Once() + + } + if os.Getenv("MAIL_HOST") != "" { + config.On("GetString", "mail.host").Return(os.Getenv("MAIL_HOST")) + config.On("GetInt", "mail.port").Return(mailPort) + config.On("GetString", "mail.from.address").Return(os.Getenv("MAIL_FROM_ADDRESS")) + config.On("GetString", "mail.from.name").Return(os.Getenv("MAIL_FROM_NAME")) + config.On("GetString", "mail.username").Return(os.Getenv("MAIL_USERNAME")) + config.On("GetString", "mail.password").Return(os.Getenv("MAIL_PASSWORD")) + config.On("GetString", "mail.to").Return(os.Getenv("MAIL_TO")) + config.On("GetString", "mail.cc").Return(os.Getenv("MAIL_CC")) + config.On("GetString", "mail.bcc").Return(os.Getenv("MAIL_BCC")) + config.EXPECT().GetString("mail.template.default", "html").Return("html").Once() + config.EXPECT().GetString("mail.template.engines.html.driver", "html").Return("html").Once() + config.EXPECT().GetString("mail.template.engines.html.path", "resources/views/mail"). + Return("resources/views/mail").Once() + } + + return config +} diff --git a/notification/channel_manager.go b/notification/channel_manager.go new file mode 100644 index 000000000..666d8c1ff --- /dev/null +++ b/notification/channel_manager.go @@ -0,0 +1,40 @@ +package notification + +import ( + "github.com/goravel/framework/contracts/foundation" + "github.com/goravel/framework/contracts/notification" + "github.com/goravel/framework/notification/channels" + "strings" + "sync" +) + +// channelRegistry 管理已注册通道(线程安全) +type channelRegistry struct { + mu sync.RWMutex + channels map[string]notification.Channel +} + +var registry = &channelRegistry{ + channels: make(map[string]notification.Channel), +} + +// RegisterChannel 允许用户在应用启动时注册自定义通道(注册一次即可) +func RegisterChannel(name string, ch notification.Channel) { + registry.mu.Lock() + defer registry.mu.Unlock() + registry.channels[strings.ToLower(name)] = ch +} + +// GetChannel 获取已注册通道 +func GetChannel(name string) (notification.Channel, bool) { + registry.mu.RLock() + defer registry.mu.RUnlock() + ch, ok := registry.channels[strings.ToLower(name)] + return ch, ok +} + +// Boot: 注册内置默认通道(mail, database) +func RegisterDefaultChannels(app foundation.Application) { + RegisterChannel("mail", &channels.MailChannel{}) + RegisterChannel("database", &channels.DatabaseChannel{}) +} diff --git a/notification/channels/database.go b/notification/channels/database.go new file mode 100644 index 000000000..360e05615 --- /dev/null +++ b/notification/channels/database.go @@ -0,0 +1,42 @@ +package channels + +import ( + "fmt" + "github.com/google/uuid" + contractsqueuedb "github.com/goravel/framework/contracts/database/db" + "github.com/goravel/framework/contracts/notification" + "github.com/goravel/framework/notification/models" + "github.com/goravel/framework/notification/utils" + "github.com/goravel/framework/support/json" + "github.com/goravel/framework/support/str" +) + +// DatabaseChannel 默认数据库通道 +type DatabaseChannel struct { + db contractsqueuedb.DB +} + +func (c *DatabaseChannel) Send(notifiable notification.Notifiable, notif interface{}) error { + data, err := utils.CallToMethod(notif, "ToDatabase", notifiable) + if err != nil { + return fmt.Errorf("[DatabaseChannel] %s", err.Error()) + } + + jsonData, _ := json.MarshalString(data) + + var notificationModel models.Notification + notificationModel.ID = uuid.New().String() + notificationModel.Data = jsonData + notificationModel.NotifiableId = notifiable.RouteNotificationFor("id").(string) + notificationModel.NotifiableType = str.Of(fmt.Sprintf("%T", notifiable)).Replace("*", "").String() + notificationModel.Type = fmt.Sprintf("%T", notif) + + if _, err = c.db.Table("notifications").Insert(¬ificationModel); err != nil { + return err + } + return nil +} + +func (c *DatabaseChannel) SetDB(db contractsqueuedb.DB) { + c.db = db +} diff --git a/notification/channels/mail.go b/notification/channels/mail.go new file mode 100644 index 000000000..f49536980 --- /dev/null +++ b/notification/channels/mail.go @@ -0,0 +1,41 @@ +package channels + +import ( + "fmt" + contractsmail "github.com/goravel/framework/contracts/mail" + "github.com/goravel/framework/contracts/notification" + "github.com/goravel/framework/mail" + "github.com/goravel/framework/notification/utils" +) + +// MailChannel 默认邮件通道 +type MailChannel struct { + mail contractsmail.Mail +} + +func (c *MailChannel) Send(notifiable notification.Notifiable, notif interface{}) error { + data, err := utils.CallToMethod(notif, "ToMail", notifiable) + if err != nil { + return fmt.Errorf("[MailChannel] %s", err.Error()) + } + + email := notifiable.RouteNotificationFor("mail").(string) + if email == "" { + return fmt.Errorf("[MailChannel] notifiable has no mail") + } + + content := data["content"].(string) + subject := data["subject"].(string) + + if err := c.mail.To([]string{email}). + Content(mail.Html(content)). + Subject(subject).Send(); err != nil { + return err + } + + return nil +} + +func (c *MailChannel) SetMail(mail contractsmail.Mail) { + c.mail = mail +} diff --git a/notification/job.go b/notification/job.go new file mode 100644 index 000000000..697e32805 --- /dev/null +++ b/notification/job.go @@ -0,0 +1,26 @@ +package notification + +import ( + "github.com/goravel/framework/contracts/config" +) + +type SendNotificationJob struct { + config config.Config +} + +func NewSendNotificationJob(config config.Config) *SendNotificationJob { + return &SendNotificationJob{ + config: config, + } +} + +// Signature The name and signature of the job. +func (r *SendNotificationJob) Signature() string { + return "goravel_send_notification_job" +} + +// Handle Execute the job. +func (r *SendNotificationJob) Handle(args ...any) error { + + return nil +} diff --git a/notification/job_test.go b/notification/job_test.go new file mode 100644 index 000000000..11787827d --- /dev/null +++ b/notification/job_test.go @@ -0,0 +1,49 @@ +package notification + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + mocksconfig "github.com/goravel/framework/mocks/config" +) + +type SendNotificationJobTestSuite struct { + suite.Suite + job *SendNotificationJob + mockConfig *mocksconfig.Config +} + +func TestSendNotificationJobTestSuite(t *testing.T) { + suite.Run(t, new(SendNotificationJobTestSuite)) +} + +func (r *SendNotificationJobTestSuite) SetupTest() { + r.mockConfig = mocksconfig.NewConfig(r.T()) + r.job = NewSendNotificationJob(r.mockConfig) + r.NotNil(r.job) + r.Equal(r.mockConfig, r.job.config) +} + +func (r *SendNotificationJobTestSuite) TestSignature() { + r.Equal("goravel_send_notification_job", r.job.Signature()) +} + +func (r *SendNotificationJobTestSuite) TestHandle_WrongArgumentCount() { + tests := []struct { + name string + args []any + }{ + { + name: "too few arguments", + args: []any{"subject", "html"}, + }, + } + + for _, test := range tests { + r.Run(test.name, func() { + err := r.job.Handle(test.args...) + r.Contains(err.Error(), "expected 10 arguments") + }) + } +} diff --git a/notification/models/notification.go b/notification/models/notification.go new file mode 100644 index 000000000..f85b6bc0f --- /dev/null +++ b/notification/models/notification.go @@ -0,0 +1,16 @@ +package models + +import ( + "github.com/goravel/framework/database/orm" + "github.com/goravel/framework/support/carbon" +) + +type Notification struct { + ID string `json:"id"` + Type string `json:"type"` + NotifiableType string `json:"notifiable_type"` + NotifiableId string `json:"notifiable_id"` + Data string `json:"data"` + ReadAt *carbon.DateTime `json:"read_at"` + orm.Timestamps +} diff --git a/notification/service_provider.go b/notification/service_provider.go new file mode 100644 index 000000000..a9e3cd94e --- /dev/null +++ b/notification/service_provider.go @@ -0,0 +1,53 @@ +package notification + +import ( + "github.com/goravel/framework/contracts/binding" + "github.com/goravel/framework/contracts/foundation" + "github.com/goravel/framework/errors" +) + +type ServiceProvider struct { +} + +// Relationship returns the relationship of the service provider. +func (r *ServiceProvider) Relationship() binding.Relationship { + return binding.Relationship{ + Bindings: []string{ + binding.Notification, + }, + Dependencies: binding.Bindings[binding.Notification].Dependencies, + ProvideFor: []string{}, + } +} + +// Register registers the service provider. +func (r *ServiceProvider) Register(app foundation.Application) { + app.Bind(binding.Notification, func(app foundation.Application) (any, error) { + config := app.MakeConfig() + if config == nil { + return nil, errors.ConfigFacadeNotSet.SetModule(errors.ModuleMail) + } + + queue := app.MakeQueue() + if queue == nil { + return nil, errors.QueueFacadeNotSet.SetModule(errors.ModuleQueue) + } + + mail := app.MakeMail() + if mail == nil { + return nil, errors.New("Mail facade not set") + } + + db := app.MakeDB() + if db == nil { + return nil, errors.DBFacadeNotSet.SetModule(errors.ModuleDB) + } + + return NewApplication(config, queue, db, mail) + }) +} + +// Boot boots the service provider, will be called after all service providers are registered. +func (r *ServiceProvider) Boot(app foundation.Application) { + RegisterDefaultChannels(app) +} diff --git a/notification/utils/notification_method_caller.go b/notification/utils/notification_method_caller.go new file mode 100644 index 000000000..f8b507934 --- /dev/null +++ b/notification/utils/notification_method_caller.go @@ -0,0 +1,65 @@ +package utils + +import ( + "fmt" + "github.com/goravel/framework/contracts/notification" + "reflect" +) + +func CallToMethod(notification interface{}, methodName string, notifiable notification.Notifiable) (map[string]interface{}, error) { + v := reflect.ValueOf(notification) + if !v.IsValid() { + return nil, fmt.Errorf("invalid notification value") + } + + // 查找方法(优先指针接收者) + method := v.MethodByName(methodName) + if !method.IsValid() && v.CanAddr() { + method = v.Addr().MethodByName(methodName) + } + if !method.IsValid() { + return nil, fmt.Errorf("method %s not found", methodName) + } + + // 调用方法 + results := method.Call([]reflect.Value{reflect.ValueOf(notifiable)}) + if len(results) == 0 { + return nil, fmt.Errorf("method %s returned no values", methodName) + } + + // 处理错误返回 + if len(results) >= 2 && !results[1].IsNil() { + if err, ok := results[1].Interface().(error); ok { + return nil, err + } + return nil, fmt.Errorf("second return of %s is not error", methodName) + } + + // 转换第一个返回值为 map[string]interface{} + first := results[0].Interface() + switch data := first.(type) { + case map[string]interface{}: + return data, nil + case map[string]string: + out := make(map[string]interface{}, len(data)) + for k, v := range data { + out[k] = v + } + return out, nil + } + + // 处理结构体 + if rv := reflect.ValueOf(first); rv.Kind() == reflect.Struct { + out := make(map[string]interface{}) + rt := rv.Type() + for i := 0; i < rv.NumField(); i++ { + field := rt.Field(i) + if field.PkgPath == "" { // 仅导出字段 + out[field.Name] = rv.Field(i).Interface() + } + } + return out, nil + } + + return nil, fmt.Errorf("unsupported return type from %s", methodName) +} From ccd528ce0db2810d61940e4a71ea236feb06a649 Mon Sep 17 00:00:00 2001 From: wuzhixiang <657873584@qq.com> Date: Tue, 18 Nov 2025 09:08:46 +0800 Subject: [PATCH 02/10] feat: test mock db --- notification/application_test.go | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/notification/application_test.go b/notification/application_test.go index 8e7ce0ed2..d83f1bdf8 100644 --- a/notification/application_test.go +++ b/notification/application_test.go @@ -1,8 +1,6 @@ package notification import ( - "fmt" - contractsqueuedb "github.com/goravel/framework/contracts/database/db" "github.com/goravel/framework/contracts/notification" contractsqueue "github.com/goravel/framework/contracts/queue" @@ -114,12 +112,10 @@ func (s *ApplicationTestSuite) TestMailNotification() { func (s *ApplicationTestSuite) TestDatabaseNotification() { s.mockConfig = mockConfig(465) - var mockDB contractsqueuedb.DB - dbFacade := mocksdb.NewDB(s.T()) - dbFacade.EXPECT().Connection("mysql").Return(mockDB).Once() - dbFacade.EXPECT().Table("notifications").Return(nil).Once() - - fmt.Println(mockDB) + mockDB := mocksdb.NewDB(s.T()) + s.mockConfig.EXPECT().GetString("DB_CONNECTION").Return("mysql").Once() + mockDB.EXPECT().Connection("mysql").Return(mockDB).Once() + mockDB.EXPECT().Table("notifications").Return(nil).Once() app, err := NewApplication(s.mockConfig, nil, mockDB, nil) s.Nil(err) @@ -138,6 +134,18 @@ func (s *ApplicationTestSuite) TestDatabaseNotification() { s.Nil(err) } +//func mockDBFacade(mockConfig *mocksconfig.Config) contractsdb.DB { +// logger := db.NewLogger(mockConfig, utils.NewTestLog()) +// gorm, err := databasedriver.BuildGorm(mockConfig, logger.ToGorm(), pool, "mysql") +// if err != nil { +// return nil +// } +// +// driver := sqlite.NewSqlite(mockConfig, utils.NewTestLog(), connection) +// +// return db.NewDB(context.Background(), mocksconfig, nil, logger, gorm) +//} + func mockQueueFacade(mockConfig *mocksconfig.Config) contractsqueue.Queue { mockConfig.EXPECT().GetString("queue.default").Return("redis").Once() mockConfig.EXPECT().GetString("queue.connections.redis.queue", "default").Return("default").Once() @@ -204,5 +212,13 @@ func mockConfig(mailPort int) *mocksconfig.Config { Return("resources/views/mail").Once() } + //DB_CONNECTION + config.On("GetString", "database.default").Return(os.Getenv("DB_CONNECTION")).Once() + config.On("GetString", "database.connections.mysql.host").Return(os.Getenv("DB_HOST")).Once() + config.On("GetString", "database.connections.mysql.port").Return(os.Getenv("DB_PORT")).Once() + config.On("GetString", "database.connections.mysql.database").Return(os.Getenv("DB_DATABASE")).Once() + config.On("GetString", "database.connections.mysql.username").Return(os.Getenv("DB_USERNAME")).Once() + config.On("GetString", "database.connections.mysql.password").Return(os.Getenv("DB_PASSWORD")).Once() + return config } From 8ea259fc6aad310f9cb77f2adba4f619e1d86014 Mon Sep 17 00:00:00 2001 From: wuzhixiang <657873584@qq.com> Date: Tue, 18 Nov 2025 13:02:06 +0800 Subject: [PATCH 03/10] fix: databaseChanel test --- notification/application_test.go | 39 +++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/notification/application_test.go b/notification/application_test.go index d83f1bdf8..d6b14fdc1 100644 --- a/notification/application_test.go +++ b/notification/application_test.go @@ -1,18 +1,21 @@ package notification import ( + "fmt" + "github.com/google/uuid" "github.com/goravel/framework/contracts/notification" contractsqueue "github.com/goravel/framework/contracts/queue" - "github.com/goravel/framework/foundation/json" "github.com/goravel/framework/mail" mocksconfig "github.com/goravel/framework/mocks/config" mocksdb "github.com/goravel/framework/mocks/database/db" "github.com/goravel/framework/notification/channels" + "github.com/goravel/framework/notification/models" "github.com/goravel/framework/queue" "github.com/goravel/framework/support" "github.com/goravel/framework/support/color" "github.com/goravel/framework/support/file" + "github.com/goravel/framework/support/str" "github.com/spf13/viper" "github.com/stretchr/testify/suite" "os" @@ -20,7 +23,7 @@ import ( ) type User struct { - ID int `json:"id"` + ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` Phone string `json:"phone"` @@ -32,6 +35,8 @@ func (u User) RouteNotificationFor(channel string) any { return u.Email case "database": return u.ID + case "id": + return u.ID default: return "" } @@ -96,7 +101,7 @@ func (s *ApplicationTestSuite) TestMailNotification() { s.Nil(err) var user = User{ - ID: 1, + ID: "1", Email: "657873584@qq.com", Name: "test", } @@ -110,23 +115,31 @@ func (s *ApplicationTestSuite) TestMailNotification() { } func (s *ApplicationTestSuite) TestDatabaseNotification() { + var user = User{ + ID: "1", + Email: "657873584@qq.com", + Name: "test", + } + var loginSuccessNotification = LoginSuccessNotification{} + s.mockConfig = mockConfig(465) mockDB := mocksdb.NewDB(s.T()) s.mockConfig.EXPECT().GetString("DB_CONNECTION").Return("mysql").Once() - mockDB.EXPECT().Connection("mysql").Return(mockDB).Once() - mockDB.EXPECT().Table("notifications").Return(nil).Once() + mockQuery := mocksdb.NewQuery(s.T()) + mockDB.EXPECT().Table("notifications").Return(mockQuery).Once() - app, err := NewApplication(s.mockConfig, nil, mockDB, nil) - s.Nil(err) + var notificationModel models.Notification + notificationModel.ID = uuid.New().String() + notificationModel.Data = "{\"content\":\"Congratulations, your login is successful!\",\"title\":\"Login success\"}" + notificationModel.NotifiableId = user.ID + notificationModel.NotifiableType = str.Of(fmt.Sprintf("%T", user)).Replace("*", "").String() + notificationModel.Type = fmt.Sprintf("%T", loginSuccessNotification) - var user = User{ - ID: 1, - Email: "657873584@qq.com", - Name: "test", - } + mockQuery.EXPECT().Insert(¬ificationModel).Return(nil, nil).Once() - var loginSuccessNotification = LoginSuccessNotification{} + app, err := NewApplication(s.mockConfig, nil, mockDB, nil) + s.Nil(err) RegisterChannel("database", &channels.DatabaseChannel{}) From 5d255a4eaf8b74ecb8a1653a7b2d75a6d7f8faa2 Mon Sep 17 00:00:00 2001 From: wuzhixiang <657873584@qq.com> Date: Tue, 18 Nov 2025 21:25:08 +0800 Subject: [PATCH 04/10] feat: Try adding a notification queue. --- notification/application.go | 32 +++-------- notification/application_test.go | 60 ++++++++++++-------- notification/job.go | 4 +- notification/notification_sender.go | 87 +++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 48 deletions(-) create mode 100644 notification/notification_sender.go diff --git a/notification/application.go b/notification/application.go index b8ced8071..3dc6cd541 100644 --- a/notification/application.go +++ b/notification/application.go @@ -1,14 +1,11 @@ package notification import ( - "fmt" "github.com/goravel/framework/contracts/config" contractsqueuedb "github.com/goravel/framework/contracts/database/db" contractsmail "github.com/goravel/framework/contracts/mail" "github.com/goravel/framework/contracts/notification" contractsqueue "github.com/goravel/framework/contracts/queue" - "github.com/goravel/framework/errors" - "github.com/goravel/framework/notification/channels" ) type Application struct { @@ -28,29 +25,16 @@ func NewApplication(config config.Config, queue contractsqueue.Queue, db contrac } // Send a notification. -func (r *Application) Send(notifiable notification.Notifiable, notif notification.Notif) error { - vias := notif.Via(notifiable) - if len(vias) == 0 { - return errors.New("no channels defined for notification") +func (r *Application) Send(notifiables []notification.Notifiable, notif notification.Notif) error { + if err := (NewNotificationSender(r.db, r.mail, r.queue)).Send(notifiables, notif); err != nil { + return err } + return nil +} - for _, chName := range vias { - ch, ok := GetChannel(chName) - if !ok { - return fmt.Errorf("channel not registered: %s", chName) - } - if chName == "database" { - if databaseChannel, ok := ch.(*channels.DatabaseChannel); ok { - databaseChannel.SetDB(r.db) - } - } else if chName == "mail" { - if mailChannel, ok := ch.(*channels.MailChannel); ok { - mailChannel.SetMail(r.mail) - } - } - if err := ch.Send(notifiable, notif); err != nil { - return fmt.Errorf("channel %s send error: %w", chName, err) - } +func (r *Application) SendNow(notifiables []notification.Notifiable, notif notification.Notif) error { + if err := (NewNotificationSender(r.db, r.mail, nil)).SendNow(notifiables, notif); err != nil { + return err } return nil } diff --git a/notification/application_test.go b/notification/application_test.go index d6b14fdc1..6094cdeca 100644 --- a/notification/application_test.go +++ b/notification/application_test.go @@ -17,6 +17,7 @@ import ( "github.com/goravel/framework/support/file" "github.com/goravel/framework/support/str" "github.com/spf13/viper" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" "os" "testing" @@ -110,7 +111,34 @@ func (s *ApplicationTestSuite) TestMailNotification() { RegisterChannel("mail", &channels.MailChannel{}) - err = app.Send(user, registerSuccessNotification) + users := []notification.Notifiable{user} + err = app.SendNow(users, registerSuccessNotification) + s.Nil(err) +} + +func (s *ApplicationTestSuite) TestMailNotificationOnQueue() { + s.mockConfig = mockConfig(465) + + queueFacade := mockQueueFacade(s.mockConfig) + + mailFacade, err := mail.NewApplication(s.mockConfig, nil) + s.Nil(err) + + app, err := NewApplication(s.mockConfig, queueFacade, nil, mailFacade) + s.Nil(err) + + var user = User{ + ID: "1", + Email: "657873584@qq.com", + Name: "test", + } + + var registerSuccessNotification = RegisterSuccessNotification{} + + RegisterChannel("mail", &channels.MailChannel{}) + + users := []notification.Notifiable{user} + err = app.Send(users, registerSuccessNotification) s.Nil(err) } @@ -136,29 +164,23 @@ func (s *ApplicationTestSuite) TestDatabaseNotification() { notificationModel.NotifiableType = str.Of(fmt.Sprintf("%T", user)).Replace("*", "").String() notificationModel.Type = fmt.Sprintf("%T", loginSuccessNotification) - mockQuery.EXPECT().Insert(¬ificationModel).Return(nil, nil).Once() + mockQuery.EXPECT().Insert(mock.MatchedBy(func(model *models.Notification) bool { + return model.Data == "{\"content\":\"Congratulations, your login is successful!\",\"title\":\"Login success\"}" && + model.NotifiableId == user.ID && + model.NotifiableType == str.Of(fmt.Sprintf("%T", user)).Replace("*", "").String() && + model.Type == fmt.Sprintf("%T", loginSuccessNotification) + })).Return(nil, nil).Once() app, err := NewApplication(s.mockConfig, nil, mockDB, nil) s.Nil(err) RegisterChannel("database", &channels.DatabaseChannel{}) - err = app.Send(user, loginSuccessNotification) + users := []notification.Notifiable{user} + err = app.SendNow(users, loginSuccessNotification) s.Nil(err) } -//func mockDBFacade(mockConfig *mocksconfig.Config) contractsdb.DB { -// logger := db.NewLogger(mockConfig, utils.NewTestLog()) -// gorm, err := databasedriver.BuildGorm(mockConfig, logger.ToGorm(), pool, "mysql") -// if err != nil { -// return nil -// } -// -// driver := sqlite.NewSqlite(mockConfig, utils.NewTestLog(), connection) -// -// return db.NewDB(context.Background(), mocksconfig, nil, logger, gorm) -//} - func mockQueueFacade(mockConfig *mocksconfig.Config) contractsqueue.Queue { mockConfig.EXPECT().GetString("queue.default").Return("redis").Once() mockConfig.EXPECT().GetString("queue.connections.redis.queue", "default").Return("default").Once() @@ -225,13 +247,5 @@ func mockConfig(mailPort int) *mocksconfig.Config { Return("resources/views/mail").Once() } - //DB_CONNECTION - config.On("GetString", "database.default").Return(os.Getenv("DB_CONNECTION")).Once() - config.On("GetString", "database.connections.mysql.host").Return(os.Getenv("DB_HOST")).Once() - config.On("GetString", "database.connections.mysql.port").Return(os.Getenv("DB_PORT")).Once() - config.On("GetString", "database.connections.mysql.database").Return(os.Getenv("DB_DATABASE")).Once() - config.On("GetString", "database.connections.mysql.username").Return(os.Getenv("DB_USERNAME")).Once() - config.On("GetString", "database.connections.mysql.password").Return(os.Getenv("DB_PASSWORD")).Once() - return config } diff --git a/notification/job.go b/notification/job.go index 697e32805..8df567bb7 100644 --- a/notification/job.go +++ b/notification/job.go @@ -1,6 +1,7 @@ package notification import ( + "fmt" "github.com/goravel/framework/contracts/config" ) @@ -21,6 +22,7 @@ func (r *SendNotificationJob) Signature() string { // Handle Execute the job. func (r *SendNotificationJob) Handle(args ...any) error { - + fmt.Println(args[0], args[1]) + return nil } diff --git a/notification/notification_sender.go b/notification/notification_sender.go new file mode 100644 index 000000000..843167405 --- /dev/null +++ b/notification/notification_sender.go @@ -0,0 +1,87 @@ +package notification + +import ( + "fmt" + contractsqueuedb "github.com/goravel/framework/contracts/database/db" + contractsmail "github.com/goravel/framework/contracts/mail" + "github.com/goravel/framework/contracts/notification" + contractsqueue "github.com/goravel/framework/contracts/queue" + "github.com/goravel/framework/errors" + "github.com/goravel/framework/notification/channels" + "github.com/goravel/framework/support/json" +) + +type NotificationSender struct { + db contractsqueuedb.DB + mail contractsmail.Mail + queue contractsqueue.Queue +} + +func NewNotificationSender(db contractsqueuedb.DB, mail contractsmail.Mail, queue contractsqueue.Queue) *NotificationSender { + return &NotificationSender{ + db: db, + mail: mail, + queue: queue, + } +} + +// Send(notifiables []Notifiable, notification Notif) error +func (s *NotificationSender) Send(notifiables []notification.Notifiable, notification notification.Notif) error { + if err := s.queueNotification(notifiables, notification); err != nil { + return err + } + return nil +} + +func (s *NotificationSender) SendNow(notifiables []notification.Notifiable, notif notification.Notif) error { + for _, notifiable := range notifiables { + vias := notif.Via(notifiable) + if len(vias) == 0 { + return errors.New("no channels defined for notification") + } + + for _, chName := range vias { + ch, ok := GetChannel(chName) + if !ok { + return fmt.Errorf("channel not registered: %s", chName) + } + if chName == "database" { + if databaseChannel, ok := ch.(*channels.DatabaseChannel); ok { + databaseChannel.SetDB(s.db) + } + } else if chName == "mail" { + if mailChannel, ok := ch.(*channels.MailChannel); ok { + mailChannel.SetMail(s.mail) + } + } + if err := ch.Send(notifiable, notif); err != nil { + return fmt.Errorf("channel %s send error: %w", chName, err) + } + } + } + return nil +} + +// queueNotification +func (s *NotificationSender) queueNotification(notifiables []notification.Notifiable, notif notification.Notif) error { + var err error + var notifiablesJson string + if notifiablesJson, err = json.MarshalString(notifiables); err != nil { + return err + } + var notifJson string + if notifJson, err = json.MarshalString(notif); err != nil { + return err + } + pendingJob := s.queue.Job(&SendNotificationJob{}, []contractsqueue.Arg{ + { + Type: "string", + Value: notifiablesJson, + }, + { + Type: "string", + Value: notifJson, + }, + }) + return pendingJob.Dispatch() +} From 65167c3facc7528a2b9094d3377112d851930295 Mon Sep 17 00:00:00 2001 From: wuzhixiang <657873584@qq.com> Date: Wed, 19 Nov 2025 10:07:56 +0800 Subject: [PATCH 05/10] feat: queue job --- contracts/notification/notification.go | 9 ++- notification/application_test.go | 52 ++++++++++++- notification/channels/database.go | 9 ++- notification/job.go | 77 ++++++++++++++++++- notification/job_test.go | 16 ++-- notification/notification_sender.go | 58 +++++++++----- notification/payload_notification.go | 25 ++++++ notification/service_provider.go | 36 ++++++++- notification/simple_notifiable.go | 12 +++ notification/type_registry.go | 52 +++++++++++++ .../utils/notification_method_caller.go | 28 ++++--- 11 files changed, 322 insertions(+), 52 deletions(-) create mode 100644 notification/payload_notification.go create mode 100644 notification/simple_notifiable.go create mode 100644 notification/type_registry.go diff --git a/contracts/notification/notification.go b/contracts/notification/notification.go index 4da940924..d277c5351 100644 --- a/contracts/notification/notification.go +++ b/contracts/notification/notification.go @@ -15,6 +15,11 @@ type Channel interface { } type Notifiable interface { - // RouteNotificationFor returns the route notification for the given channel. - RouteNotificationFor(channel string) any + // RouteNotificationFor returns the route notification for the given channel. + RouteNotificationFor(channel string) any +} + +type PayloadProvider interface { + // PayloadFor returns prepared payload data for specific channel. + PayloadFor(channel string, notifiable Notifiable) (map[string]interface{}, error) } diff --git a/notification/application_test.go b/notification/application_test.go index 6094cdeca..469ca8c73 100644 --- a/notification/application_test.go +++ b/notification/application_test.go @@ -181,6 +181,56 @@ func (s *ApplicationTestSuite) TestDatabaseNotification() { s.Nil(err) } +func (s *ApplicationTestSuite) TestDatabaseNotificationOnQueue() { + var user = User{ + ID: "1", + Email: "657873584@qq.com", + Name: "test", + } + RegisterNotificationType("notification.LoginSuccessNotification", func() notification.Notif { + return &LoginSuccessNotification{} + }) + RegisterNotifiableType("notification.User", func(routes map[string]interface{}) notification.Notifiable { + user := &User{ + ID: routes["id"].(string), + } + return user + }) + + var loginSuccessNotification = LoginSuccessNotification{} + + s.mockConfig = mockConfig(465) + queueFacade := mockQueueFacade(s.mockConfig) + + mockDB := mocksdb.NewDB(s.T()) + s.mockConfig.EXPECT().GetString("DB_CONNECTION").Return("mysql").Once() + mockQuery := mocksdb.NewQuery(s.T()) + mockDB.EXPECT().Table("notifications").Return(mockQuery).Once() + + var notificationModel models.Notification + notificationModel.ID = uuid.New().String() + notificationModel.Data = "{\"content\":\"Congratulations, your login is successful!\",\"title\":\"Login success\"}" + notificationModel.NotifiableId = user.ID + notificationModel.NotifiableType = str.Of(fmt.Sprintf("%T", user)).Replace("*", "").String() + notificationModel.Type = fmt.Sprintf("%T", loginSuccessNotification) + + mockQuery.EXPECT().Insert(mock.MatchedBy(func(model *models.Notification) bool { + return model.Data == "{\"content\":\"Congratulations, your login is successful!\",\"title\":\"Login success\"}" && + model.NotifiableId == user.ID && + model.NotifiableType == str.Of(fmt.Sprintf("%T", user)).Replace("*", "").String() && + model.Type == fmt.Sprintf("%T", loginSuccessNotification) + })).Return(nil, nil).Once() + + app, err := NewApplication(s.mockConfig, queueFacade, mockDB, nil) + s.Nil(err) + + RegisterChannel("database", &channels.DatabaseChannel{}) + + users := []notification.Notifiable{user} + err = app.Send(users, loginSuccessNotification) + s.Nil(err) +} + func mockQueueFacade(mockConfig *mocksconfig.Config) contractsqueue.Queue { mockConfig.EXPECT().GetString("queue.default").Return("redis").Once() mockConfig.EXPECT().GetString("queue.connections.redis.queue", "default").Return("default").Once() @@ -192,7 +242,7 @@ func mockQueueFacade(mockConfig *mocksconfig.Config) contractsqueue.Queue { queueFacade := queue.NewApplication(queue.NewConfig(mockConfig), nil, queue.NewJobStorer(), json.New(), nil) queueFacade.Register([]contractsqueue.Job{ - NewSendNotificationJob(mockConfig), + NewSendNotificationJob(mockConfig, nil, nil), }) return queueFacade } diff --git a/notification/channels/database.go b/notification/channels/database.go index 360e05615..471f3a14a 100644 --- a/notification/channels/database.go +++ b/notification/channels/database.go @@ -22,14 +22,15 @@ func (c *DatabaseChannel) Send(notifiable notification.Notifiable, notif interfa return fmt.Errorf("[DatabaseChannel] %s", err.Error()) } - jsonData, _ := json.MarshalString(data) - var notificationModel models.Notification notificationModel.ID = uuid.New().String() - notificationModel.Data = jsonData notificationModel.NotifiableId = notifiable.RouteNotificationFor("id").(string) + notificationModel.NotifiableType = str.Of(fmt.Sprintf("%T", notifiable)).Replace("*", "").String() - notificationModel.Type = fmt.Sprintf("%T", notif) + notificationModel.Type = str.Of(fmt.Sprintf("%T", notif)).Replace("*", "").String() + + jsonData, _ := json.MarshalString(data) + notificationModel.Data = jsonData if _, err = c.db.Table("notifications").Insert(¬ificationModel); err != nil { return err diff --git a/notification/job.go b/notification/job.go index 8df567bb7..b0e7b53e8 100644 --- a/notification/job.go +++ b/notification/job.go @@ -3,15 +3,24 @@ package notification import ( "fmt" "github.com/goravel/framework/contracts/config" + contractsqueuedb "github.com/goravel/framework/contracts/database/db" + contractsmail "github.com/goravel/framework/contracts/mail" + contractsnotification "github.com/goravel/framework/contracts/notification" + "github.com/goravel/framework/notification/channels" + "github.com/goravel/framework/support/json" ) type SendNotificationJob struct { config config.Config + db contractsqueuedb.DB + mail contractsmail.Mail } -func NewSendNotificationJob(config config.Config) *SendNotificationJob { +func NewSendNotificationJob(config config.Config, db contractsqueuedb.DB, mail contractsmail.Mail) *SendNotificationJob { return &SendNotificationJob{ config: config, + db: db, + mail: mail, } } @@ -22,7 +31,69 @@ func (r *SendNotificationJob) Signature() string { // Handle Execute the job. func (r *SendNotificationJob) Handle(args ...any) error { - fmt.Println(args[0], args[1]) - + if len(args) != 3 { + return fmt.Errorf("expected 3 arguments, got %d", len(args)) + } + + channelsArg, ok := args[0].([]string) + if !ok { + return fmt.Errorf("channels should be of type []string") + } + routesJSON, ok := args[1].(string) + if !ok { + return fmt.Errorf("routes should be of type string") + } + payloadsJSON, ok := args[2].(string) + if !ok { + return fmt.Errorf("payloads should be of type string") + } + + var routes map[string]any + if routesJSON != "" { + if err := json.UnmarshalString(routesJSON, &routes); err != nil { + return err + } + } + var payloads map[string]map[string]interface{} + if payloadsJSON != "" { + if err := json.UnmarshalString(payloadsJSON, &payloads); err != nil { + return err + } + } + + var notifiable contractsnotification.Notifiable = MapNotifiable{Routes: routes} + if nt, ok := routes["_notifiable_type"].(string); ok && nt != "" && NotifiableHasWithRoutes(nt) { + if inst, ok := GetNotifiableInstance(nt, routes); ok { + notifiable = inst + } + } + + payloadNotif := PayloadNotification{Channels: channelsArg, Payloads: payloads} + notifObj := any(payloadNotif) + if t, ok := routes["_notif_type"].(string); ok && t != "" { + if inst, ok := GetNotificationInstance(t); ok { + notifObj = inst + } + } + + for _, chName := range channelsArg { + ch, ok := GetChannel(chName) + if !ok { + return fmt.Errorf("channel not registered: %s", chName) + } + if chName == "database" && r.db != nil { + if databaseChannel, ok := ch.(*channels.DatabaseChannel); ok { + databaseChannel.SetDB(r.db) + } + } else if chName == "mail" && r.mail != nil { + if mailChannel, ok := ch.(*channels.MailChannel); ok { + mailChannel.SetMail(r.mail) + } + } + if err := ch.Send(notifiable, notifObj); err != nil { + return fmt.Errorf("channel %s send error: %w", chName, err) + } + } + return nil } diff --git a/notification/job_test.go b/notification/job_test.go index 11787827d..181faf384 100644 --- a/notification/job_test.go +++ b/notification/job_test.go @@ -19,10 +19,10 @@ func TestSendNotificationJobTestSuite(t *testing.T) { } func (r *SendNotificationJobTestSuite) SetupTest() { - r.mockConfig = mocksconfig.NewConfig(r.T()) - r.job = NewSendNotificationJob(r.mockConfig) - r.NotNil(r.job) - r.Equal(r.mockConfig, r.job.config) + r.mockConfig = mocksconfig.NewConfig(r.T()) + r.job = NewSendNotificationJob(r.mockConfig, nil, nil) + r.NotNil(r.job) + r.Equal(r.mockConfig, r.job.config) } func (r *SendNotificationJobTestSuite) TestSignature() { @@ -42,8 +42,8 @@ func (r *SendNotificationJobTestSuite) TestHandle_WrongArgumentCount() { for _, test := range tests { r.Run(test.name, func() { - err := r.job.Handle(test.args...) - r.Contains(err.Error(), "expected 10 arguments") - }) - } + err := r.job.Handle(test.args...) + r.Contains(err.Error(), "expected 3 arguments") + }) + } } diff --git a/notification/notification_sender.go b/notification/notification_sender.go index 843167405..629162399 100644 --- a/notification/notification_sender.go +++ b/notification/notification_sender.go @@ -8,7 +8,10 @@ import ( contractsqueue "github.com/goravel/framework/contracts/queue" "github.com/goravel/framework/errors" "github.com/goravel/framework/notification/channels" + "github.com/goravel/framework/notification/utils" "github.com/goravel/framework/support/json" + "github.com/goravel/framework/support/str" + "strings" ) type NotificationSender struct { @@ -64,24 +67,41 @@ func (s *NotificationSender) SendNow(notifiables []notification.Notifiable, noti // queueNotification func (s *NotificationSender) queueNotification(notifiables []notification.Notifiable, notif notification.Notif) error { - var err error - var notifiablesJson string - if notifiablesJson, err = json.MarshalString(notifiables); err != nil { - return err - } - var notifJson string - if notifJson, err = json.MarshalString(notif); err != nil { - return err + for _, notifiable := range notifiables { + vias := notif.Via(notifiable) + payloads := map[string]map[string]any{} + for _, chName := range vias { + method := "To" + strings.Title(chName) + data, err := utils.CallToMethod(notif, method, notifiable) + if err != nil { + // allow empty payloads for channels that don't require data + data = map[string]any{} + } + payloads[chName] = data + } + + payloadsJSON, _ := json.MarshalString(payloads) + + routes := map[string]any{} + for _, chName := range vias { + routes[chName] = notifiable.RouteNotificationFor(chName) + } + // commonly used + routes["id"] = notifiable.RouteNotificationFor("id") + routes["_notifiable_type"] = str.Of(fmt.Sprintf("%T", notifiable)).Replace("*", "").String() + routes["_notif_type"] = str.Of(fmt.Sprintf("%T", notif)).Replace("*", "").String() + routesJSON, _ := json.MarshalString(routes) + + println(routesJSON) + + pendingJob := s.queue.Job(NewSendNotificationJob(nil, s.db, s.mail), []contractsqueue.Arg{ + {Type: "[]string", Value: vias}, + {Type: "string", Value: routesJSON}, + {Type: "string", Value: payloadsJSON}, + }) + if err := pendingJob.Dispatch(); err != nil { + return err + } } - pendingJob := s.queue.Job(&SendNotificationJob{}, []contractsqueue.Arg{ - { - Type: "string", - Value: notifiablesJson, - }, - { - Type: "string", - Value: notifJson, - }, - }) - return pendingJob.Dispatch() + return nil } diff --git a/notification/payload_notification.go b/notification/payload_notification.go new file mode 100644 index 000000000..1b4e24e23 --- /dev/null +++ b/notification/payload_notification.go @@ -0,0 +1,25 @@ +package notification + +import ( + contractsnotification "github.com/goravel/framework/contracts/notification" +) + +type PayloadNotification struct { + Channels []string + Payloads map[string]map[string]interface{} // channel -> payload map +} + +func (n PayloadNotification) Via(_ contractsnotification.Notifiable) []string { + return n.Channels +} + +func (n PayloadNotification) PayloadFor(channel string, _ contractsnotification.Notifiable) (map[string]interface{}, error) { + if n.Payloads == nil { + return map[string]interface{}{}, nil + } + m := n.Payloads[channel] + if m == nil { + return map[string]interface{}{}, nil + } + return m, nil +} \ No newline at end of file diff --git a/notification/service_provider.go b/notification/service_provider.go index a9e3cd94e..1671773a8 100644 --- a/notification/service_provider.go +++ b/notification/service_provider.go @@ -1,9 +1,10 @@ package notification import ( - "github.com/goravel/framework/contracts/binding" - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/errors" + "github.com/goravel/framework/contracts/binding" + "github.com/goravel/framework/contracts/foundation" + contractsqueue "github.com/goravel/framework/contracts/queue" + "github.com/goravel/framework/errors" ) type ServiceProvider struct { @@ -49,5 +50,32 @@ func (r *ServiceProvider) Register(app foundation.Application) { // Boot boots the service provider, will be called after all service providers are registered. func (r *ServiceProvider) Boot(app foundation.Application) { - RegisterDefaultChannels(app) + RegisterDefaultChannels(app) + r.registerJobs(app) +} + +func (r *ServiceProvider) registerJobs(app foundation.Application) { + queueFacade := app.MakeQueue() + if queueFacade == nil { + return + } + + configFacade := app.MakeConfig() + if configFacade == nil { + return + } + + mailFacade := app.MakeMail() + if mailFacade == nil { + return + } + + dbFacade := app.MakeDB() + if dbFacade == nil { + return + } + + queueFacade.Register([]contractsqueue.Job{ + NewSendNotificationJob(configFacade, dbFacade, mailFacade), + }) } diff --git a/notification/simple_notifiable.go b/notification/simple_notifiable.go new file mode 100644 index 000000000..1d2eeb37a --- /dev/null +++ b/notification/simple_notifiable.go @@ -0,0 +1,12 @@ +package notification + +type MapNotifiable struct { + Routes map[string]any +} + +func (n MapNotifiable) RouteNotificationFor(channel string) any { + if n.Routes == nil { + return nil + } + return n.Routes[channel] +} \ No newline at end of file diff --git a/notification/type_registry.go b/notification/type_registry.go new file mode 100644 index 000000000..439b28598 --- /dev/null +++ b/notification/type_registry.go @@ -0,0 +1,52 @@ +package notification + +import ( + contractsnotification "github.com/goravel/framework/contracts/notification" + "sync" +) + +var ( + notifMu sync.RWMutex + notifRegistry = map[string]func() contractsnotification.Notif{} + + notifiableMu sync.RWMutex + notifiableRegistry = map[string]func(map[string]interface{}) contractsnotification.Notifiable{} +) + +func RegisterNotificationType(name string, factory func() contractsnotification.Notif) { + notifMu.Lock() + defer notifMu.Unlock() + notifRegistry[name] = factory +} + +func GetNotificationInstance(name string) (contractsnotification.Notif, bool) { + notifMu.RLock() + defer notifMu.RUnlock() + if f, ok := notifRegistry[name]; ok { + return f(), true + } + return nil, false +} + +// RegisterNotifiableType keeps backward compatibility for tests/users +func RegisterNotifiableType(name string, factory func(map[string]interface{}) contractsnotification.Notifiable) { + notifiableMu.Lock() + defer notifiableMu.Unlock() + notifiableRegistry[name] = factory +} + +func GetNotifiableInstance(name string, routes map[string]any) (contractsnotification.Notifiable, bool) { + notifiableMu.RLock() + defer notifiableMu.RUnlock() + if f, ok := notifiableRegistry[name]; ok { + return f(routes), true + } + return nil, false +} + +func NotifiableHasWithRoutes(name string) bool { + notifiableMu.RLock() + defer notifiableMu.RUnlock() + _, ok := notifiableRegistry[name] + return ok +} diff --git a/notification/utils/notification_method_caller.go b/notification/utils/notification_method_caller.go index f8b507934..399577f37 100644 --- a/notification/utils/notification_method_caller.go +++ b/notification/utils/notification_method_caller.go @@ -1,25 +1,31 @@ package utils import ( - "fmt" - "github.com/goravel/framework/contracts/notification" - "reflect" + "fmt" + contractsnotification "github.com/goravel/framework/contracts/notification" + "reflect" + "strings" ) -func CallToMethod(notification interface{}, methodName string, notifiable notification.Notifiable) (map[string]interface{}, error) { - v := reflect.ValueOf(notification) - if !v.IsValid() { - return nil, fmt.Errorf("invalid notification value") - } +func CallToMethod(notification interface{}, methodName string, notifiable contractsnotification.Notifiable) (map[string]interface{}, error) { + v := reflect.ValueOf(notification) + if !v.IsValid() { + return nil, fmt.Errorf("invalid notification value") + } // 查找方法(优先指针接收者) method := v.MethodByName(methodName) if !method.IsValid() && v.CanAddr() { method = v.Addr().MethodByName(methodName) } - if !method.IsValid() { - return nil, fmt.Errorf("method %s not found", methodName) - } + if !method.IsValid() { + // fallback: support PayloadProvider + if provider, ok := v.Interface().(contractsnotification.PayloadProvider); ok { + channel := strings.ToLower(strings.TrimPrefix(methodName, "To")) + return provider.PayloadFor(channel, notifiable) + } + return nil, fmt.Errorf("method %s not found", methodName) + } // 调用方法 results := method.Call([]reflect.Value{reflect.ValueOf(notifiable)}) From 2f032aa558569f58645d35aad5f70b6a1362f32c Mon Sep 17 00:00:00 2001 From: wuzhixiang <657873584@qq.com> Date: Wed, 19 Nov 2025 14:02:50 +0800 Subject: [PATCH 06/10] fix --- contracts/notification/notification.go | 8 +-- notification/application_test.go | 67 +++++++++++++++++++------- notification/channels/database.go | 2 +- notification/channels/mail.go | 2 +- notification/notification_sender.go | 32 +++--------- notification/simple_notifiable.go | 14 +++--- notification/utils/serializer.go | 1 + 7 files changed, 71 insertions(+), 55 deletions(-) create mode 100644 notification/utils/serializer.go diff --git a/contracts/notification/notification.go b/contracts/notification/notification.go index d277c5351..99c6c1405 100644 --- a/contracts/notification/notification.go +++ b/contracts/notification/notification.go @@ -15,11 +15,11 @@ type Channel interface { } type Notifiable interface { - // RouteNotificationFor returns the route notification for the given channel. - RouteNotificationFor(channel string) any + // NotificationParams returns the parameters for the given key. + NotificationParams() map[string]interface{} } type PayloadProvider interface { - // PayloadFor returns prepared payload data for specific channel. - PayloadFor(channel string, notifiable Notifiable) (map[string]interface{}, error) + // PayloadFor returns prepared payload data for specific channel. + PayloadFor(channel string, notifiable Notifiable) (map[string]interface{}, error) } diff --git a/notification/application_test.go b/notification/application_test.go index 469ca8c73..3247756c2 100644 --- a/notification/application_test.go +++ b/notification/application_test.go @@ -1,6 +1,8 @@ package notification import ( + "bytes" + "encoding/gob" "fmt" "github.com/google/uuid" "github.com/goravel/framework/contracts/notification" @@ -30,20 +32,24 @@ type User struct { Phone string `json:"phone"` } -func (u User) RouteNotificationFor(channel string) any { - switch channel { - case "mail": - return u.Email - case "database": - return u.ID - case "id": - return u.ID - default: - return "" +func (u User) NotificationParams() map[string]interface{} { + return map[string]interface{}{ + "id": u.ID, + "email": u.Email, } } type RegisterSuccessNotification struct { + Title string + Content string +} + +// New +func NewRegisterSuccessNotification(title, content string) *RegisterSuccessNotification { + return &RegisterSuccessNotification{ + Title: title, + Content: content, + } } func (n RegisterSuccessNotification) Via(notifiable notification.Notifiable) []string { @@ -53,12 +59,21 @@ func (n RegisterSuccessNotification) Via(notifiable notification.Notifiable) []s } func (n RegisterSuccessNotification) ToMail(notifiable notification.Notifiable) map[string]string { return map[string]string{ - "subject": "【sign】Register success", - "content": "Congratulations, your registration is successful!", + "subject": n.Title, + "content": n.Content, } } type LoginSuccessNotification struct { + Title string + Content string +} + +func NewLoginSuccessNotification(title, content string) *LoginSuccessNotification { + return &LoginSuccessNotification{ + Title: title, + Content: content, + } } func (n LoginSuccessNotification) Via(notifiable notification.Notifiable) []string { @@ -68,8 +83,8 @@ func (n LoginSuccessNotification) Via(notifiable notification.Notifiable) []stri } func (n LoginSuccessNotification) ToDatabase(notifiable notification.Notifiable) map[string]string { return map[string]string{ - "title": "Login success", - "content": "Congratulations, your login is successful!", + "title": n.Title, + "content": n.Content, } } @@ -107,7 +122,7 @@ func (s *ApplicationTestSuite) TestMailNotification() { Name: "test", } - var registerSuccessNotification = RegisterSuccessNotification{} + var registerSuccessNotification = NewRegisterSuccessNotification("Registration successful!", "Congratulations, your registration is successful!") RegisterChannel("mail", &channels.MailChannel{}) @@ -133,7 +148,7 @@ func (s *ApplicationTestSuite) TestMailNotificationOnQueue() { Name: "test", } - var registerSuccessNotification = RegisterSuccessNotification{} + var registerSuccessNotification = NewRegisterSuccessNotification("Registration successful!", "Congratulations, your registration is successful!") RegisterChannel("mail", &channels.MailChannel{}) @@ -148,7 +163,7 @@ func (s *ApplicationTestSuite) TestDatabaseNotification() { Email: "657873584@qq.com", Name: "test", } - var loginSuccessNotification = LoginSuccessNotification{} + var loginSuccessNotification = NewLoginSuccessNotification("Login success", "Congratulations, your login is successful!") s.mockConfig = mockConfig(465) @@ -197,7 +212,7 @@ func (s *ApplicationTestSuite) TestDatabaseNotificationOnQueue() { return user }) - var loginSuccessNotification = LoginSuccessNotification{} + var loginSuccessNotification = NewLoginSuccessNotification("Login success", "Congratulations, your login is successful!") s.mockConfig = mockConfig(465) queueFacade := mockQueueFacade(s.mockConfig) @@ -231,6 +246,22 @@ func (s *ApplicationTestSuite) TestDatabaseNotificationOnQueue() { s.Nil(err) } +func (s *ApplicationTestSuite) TestNotifiableSerialize() { + var loginSuccessNotification = NewLoginSuccessNotification("Login success", "Congratulations, your login is successful!") + // 创建数据缓冲区 + var buf bytes.Buffer + // 创建编码器 + encoder := gob.NewEncoder(&buf) + err := encoder.Encode(loginSuccessNotification) + s.Nil(err) + var loginSuccessNotification2 LoginSuccessNotification + decoder := gob.NewDecoder(&buf) + err = decoder.Decode(&loginSuccessNotification2) + s.Nil(err) + s.Equal(loginSuccessNotification.Title, loginSuccessNotification2.Title) + s.Equal(loginSuccessNotification.Content, loginSuccessNotification2.Content) +} + func mockQueueFacade(mockConfig *mocksconfig.Config) contractsqueue.Queue { mockConfig.EXPECT().GetString("queue.default").Return("redis").Once() mockConfig.EXPECT().GetString("queue.connections.redis.queue", "default").Return("default").Once() diff --git a/notification/channels/database.go b/notification/channels/database.go index 471f3a14a..be3b9c86e 100644 --- a/notification/channels/database.go +++ b/notification/channels/database.go @@ -24,7 +24,7 @@ func (c *DatabaseChannel) Send(notifiable notification.Notifiable, notif interfa var notificationModel models.Notification notificationModel.ID = uuid.New().String() - notificationModel.NotifiableId = notifiable.RouteNotificationFor("id").(string) + notificationModel.NotifiableId = notifiable.NotificationParams()["id"].(string) notificationModel.NotifiableType = str.Of(fmt.Sprintf("%T", notifiable)).Replace("*", "").String() notificationModel.Type = str.Of(fmt.Sprintf("%T", notif)).Replace("*", "").String() diff --git a/notification/channels/mail.go b/notification/channels/mail.go index f49536980..409a1c9d8 100644 --- a/notification/channels/mail.go +++ b/notification/channels/mail.go @@ -19,7 +19,7 @@ func (c *MailChannel) Send(notifiable notification.Notifiable, notif interface{} return fmt.Errorf("[MailChannel] %s", err.Error()) } - email := notifiable.RouteNotificationFor("mail").(string) + email := notifiable.NotificationParams()["mail"].(string) if email == "" { return fmt.Errorf("[MailChannel] notifiable has no mail") } diff --git a/notification/notification_sender.go b/notification/notification_sender.go index 629162399..c860709c7 100644 --- a/notification/notification_sender.go +++ b/notification/notification_sender.go @@ -1,6 +1,8 @@ package notification import ( + "bytes" + "encoding/gob" "fmt" contractsqueuedb "github.com/goravel/framework/contracts/database/db" contractsmail "github.com/goravel/framework/contracts/mail" @@ -67,32 +69,14 @@ func (s *NotificationSender) SendNow(notifiables []notification.Notifiable, noti // queueNotification func (s *NotificationSender) queueNotification(notifiables []notification.Notifiable, notif notification.Notif) error { - for _, notifiable := range notifiables { - vias := notif.Via(notifiable) - payloads := map[string]map[string]any{} - for _, chName := range vias { - method := "To" + strings.Title(chName) - data, err := utils.CallToMethod(notif, method, notifiable) - if err != nil { - // allow empty payloads for channels that don't require data - data = map[string]any{} - } - payloads[chName] = data - } + // 创建数据缓冲区 + var buf bytes.Buffer - payloadsJSON, _ := json.MarshalString(payloads) - - routes := map[string]any{} - for _, chName := range vias { - routes[chName] = notifiable.RouteNotificationFor(chName) - } - // commonly used - routes["id"] = notifiable.RouteNotificationFor("id") - routes["_notifiable_type"] = str.Of(fmt.Sprintf("%T", notifiable)).Replace("*", "").String() - routes["_notif_type"] = str.Of(fmt.Sprintf("%T", notif)).Replace("*", "").String() - routesJSON, _ := json.MarshalString(routes) + // 创建编码器 + encoder := gob.NewEncoder(&buf) + for _, notifiable := range notifiables { - println(routesJSON) + notifiableSerialize := utils.Serialize(notifiable) pendingJob := s.queue.Job(NewSendNotificationJob(nil, s.db, s.mail), []contractsqueue.Arg{ {Type: "[]string", Value: vias}, diff --git a/notification/simple_notifiable.go b/notification/simple_notifiable.go index 1d2eeb37a..2dc4e7a53 100644 --- a/notification/simple_notifiable.go +++ b/notification/simple_notifiable.go @@ -1,12 +1,12 @@ package notification type MapNotifiable struct { - Routes map[string]any + Routes map[string]any } -func (n MapNotifiable) RouteNotificationFor(channel string) any { - if n.Routes == nil { - return nil - } - return n.Routes[channel] -} \ No newline at end of file +func (n MapNotifiable) NotificationParams() map[string]interface{} { + if n.Routes == nil { + return nil + } + return n.Routes +} diff --git a/notification/utils/serializer.go b/notification/utils/serializer.go new file mode 100644 index 000000000..d4b585bf7 --- /dev/null +++ b/notification/utils/serializer.go @@ -0,0 +1 @@ +package utils From 18c90156e0410fb353b80e86925165821926da6b Mon Sep 17 00:00:00 2001 From: wuzhixiang <657873584@qq.com> Date: Wed, 19 Nov 2025 14:44:18 +0800 Subject: [PATCH 07/10] refactor job --- notification/application_test.go | 13 +----- notification/channels/mail.go | 30 ++++++++---- notification/job.go | 69 +++++++++++++++------------- notification/job_test.go | 20 ++++---- notification/notification_sender.go | 61 ++++++++++++------------ notification/payload_notification.go | 25 ---------- notification/simple_notifiable.go | 12 ----- notification/type_registry.go | 52 --------------------- notification/utils/serializer.go | 1 - 9 files changed, 103 insertions(+), 180 deletions(-) delete mode 100644 notification/payload_notification.go delete mode 100644 notification/simple_notifiable.go delete mode 100644 notification/type_registry.go delete mode 100644 notification/utils/serializer.go diff --git a/notification/application_test.go b/notification/application_test.go index 3247756c2..25d569c5d 100644 --- a/notification/application_test.go +++ b/notification/application_test.go @@ -183,7 +183,7 @@ func (s *ApplicationTestSuite) TestDatabaseNotification() { return model.Data == "{\"content\":\"Congratulations, your login is successful!\",\"title\":\"Login success\"}" && model.NotifiableId == user.ID && model.NotifiableType == str.Of(fmt.Sprintf("%T", user)).Replace("*", "").String() && - model.Type == fmt.Sprintf("%T", loginSuccessNotification) + model.Type == str.Of(fmt.Sprintf("%T", loginSuccessNotification)).Replace("*", "").String() })).Return(nil, nil).Once() app, err := NewApplication(s.mockConfig, nil, mockDB, nil) @@ -202,15 +202,6 @@ func (s *ApplicationTestSuite) TestDatabaseNotificationOnQueue() { Email: "657873584@qq.com", Name: "test", } - RegisterNotificationType("notification.LoginSuccessNotification", func() notification.Notif { - return &LoginSuccessNotification{} - }) - RegisterNotifiableType("notification.User", func(routes map[string]interface{}) notification.Notifiable { - user := &User{ - ID: routes["id"].(string), - } - return user - }) var loginSuccessNotification = NewLoginSuccessNotification("Login success", "Congratulations, your login is successful!") @@ -233,7 +224,7 @@ func (s *ApplicationTestSuite) TestDatabaseNotificationOnQueue() { return model.Data == "{\"content\":\"Congratulations, your login is successful!\",\"title\":\"Login success\"}" && model.NotifiableId == user.ID && model.NotifiableType == str.Of(fmt.Sprintf("%T", user)).Replace("*", "").String() && - model.Type == fmt.Sprintf("%T", loginSuccessNotification) + model.Type == str.Of(fmt.Sprintf("%T", loginSuccessNotification)).Replace("*", "").String() })).Return(nil, nil).Once() app, err := NewApplication(s.mockConfig, queueFacade, mockDB, nil) diff --git a/notification/channels/mail.go b/notification/channels/mail.go index 409a1c9d8..f929c4565 100644 --- a/notification/channels/mail.go +++ b/notification/channels/mail.go @@ -14,15 +14,27 @@ type MailChannel struct { } func (c *MailChannel) Send(notifiable notification.Notifiable, notif interface{}) error { - data, err := utils.CallToMethod(notif, "ToMail", notifiable) - if err != nil { - return fmt.Errorf("[MailChannel] %s", err.Error()) - } - - email := notifiable.NotificationParams()["mail"].(string) - if email == "" { - return fmt.Errorf("[MailChannel] notifiable has no mail") - } + data, err := utils.CallToMethod(notif, "ToMail", notifiable) + if err != nil { + return fmt.Errorf("[MailChannel] %s", err.Error()) + } + params := notifiable.NotificationParams() + var email string + if v, ok := params["mail"]; ok { + if s, ok := v.(string); ok { + email = s + } + } + if email == "" { + if v, ok := params["email"]; ok { + if s, ok := v.(string); ok { + email = s + } + } + } + if email == "" { + return fmt.Errorf("[MailChannel] notifiable has no mail") + } content := data["content"].(string) subject := data["subject"].(string) diff --git a/notification/job.go b/notification/job.go index b0e7b53e8..286b96ca6 100644 --- a/notification/job.go +++ b/notification/job.go @@ -1,13 +1,14 @@ package notification import ( + "bytes" + "encoding/gob" "fmt" "github.com/goravel/framework/contracts/config" contractsqueuedb "github.com/goravel/framework/contracts/database/db" contractsmail "github.com/goravel/framework/contracts/mail" contractsnotification "github.com/goravel/framework/contracts/notification" "github.com/goravel/framework/notification/channels" - "github.com/goravel/framework/support/json" ) type SendNotificationJob struct { @@ -16,6 +17,11 @@ type SendNotificationJob struct { mail contractsmail.Mail } +type GobEnvelope struct { + Notifiable any + Notif any +} + func NewSendNotificationJob(config config.Config, db contractsqueuedb.DB, mail contractsmail.Mail) *SendNotificationJob { return &SendNotificationJob{ config: config, @@ -35,62 +41,59 @@ func (r *SendNotificationJob) Handle(args ...any) error { return fmt.Errorf("expected 3 arguments, got %d", len(args)) } - channelsArg, ok := args[0].([]string) - if !ok { - return fmt.Errorf("channels should be of type []string") - } - routesJSON, ok := args[1].(string) + notifiableBytes, _ := args[0].([]uint8) + notifBytes, _ := args[1].([]uint8) + vias, ok := args[2].([]string) if !ok { - return fmt.Errorf("routes should be of type string") - } - payloadsJSON, ok := args[2].(string) - if !ok { - return fmt.Errorf("payloads should be of type string") + return fmt.Errorf("invalid channels payload type: %T", args[2]) } - var routes map[string]any - if routesJSON != "" { - if err := json.UnmarshalString(routesJSON, &routes); err != nil { + var notifiable any + var notif any + // Try envelope decode first + if len(notifiableBytes) > 0 && len(notifBytes) == 0 { + var env GobEnvelope + if err := gob.NewDecoder(bytes.NewReader(notifiableBytes)).Decode(&env); err != nil { return err } - } - var payloads map[string]map[string]interface{} - if payloadsJSON != "" { - if err := json.UnmarshalString(payloadsJSON, &payloads); err != nil { + notifiable = env.Notifiable + notif = env.Notif + } else { + dec1 := gob.NewDecoder(bytes.NewReader(notifiableBytes)) + if err := dec1.Decode(¬ifiable); err != nil { return err } - } - var notifiable contractsnotification.Notifiable = MapNotifiable{Routes: routes} - if nt, ok := routes["_notifiable_type"].(string); ok && nt != "" && NotifiableHasWithRoutes(nt) { - if inst, ok := GetNotifiableInstance(nt, routes); ok { - notifiable = inst + dec2 := gob.NewDecoder(bytes.NewReader(notifBytes)) + if err := dec2.Decode(¬if); err != nil { + return err } } - payloadNotif := PayloadNotification{Channels: channelsArg, Payloads: payloads} - notifObj := any(payloadNotif) - if t, ok := routes["_notif_type"].(string); ok && t != "" { - if inst, ok := GetNotificationInstance(t); ok { - notifObj = inst - } + nbl, ok := notifiable.(contractsnotification.Notifiable) + if !ok { + return fmt.Errorf("decoded notifiable does not implement Notifiable: %T", notifiable) + } + nf, ok := notif.(contractsnotification.Notif) + if !ok { + return fmt.Errorf("decoded notif does not implement Notif: %T", notif) } - for _, chName := range channelsArg { + for _, chName := range vias { ch, ok := GetChannel(chName) if !ok { return fmt.Errorf("channel not registered: %s", chName) } - if chName == "database" && r.db != nil { + if chName == "database" { if databaseChannel, ok := ch.(*channels.DatabaseChannel); ok { databaseChannel.SetDB(r.db) } - } else if chName == "mail" && r.mail != nil { + } else if chName == "mail" { if mailChannel, ok := ch.(*channels.MailChannel); ok { mailChannel.SetMail(r.mail) } } - if err := ch.Send(notifiable, notifObj); err != nil { + if err := ch.Send(nbl, nf); err != nil { return fmt.Errorf("channel %s send error: %w", chName, err) } } diff --git a/notification/job_test.go b/notification/job_test.go index 181faf384..24e641afd 100644 --- a/notification/job_test.go +++ b/notification/job_test.go @@ -19,10 +19,10 @@ func TestSendNotificationJobTestSuite(t *testing.T) { } func (r *SendNotificationJobTestSuite) SetupTest() { - r.mockConfig = mocksconfig.NewConfig(r.T()) - r.job = NewSendNotificationJob(r.mockConfig, nil, nil) - r.NotNil(r.job) - r.Equal(r.mockConfig, r.job.config) + r.mockConfig = mocksconfig.NewConfig(r.T()) + r.job = NewSendNotificationJob(r.mockConfig, nil, nil) + r.NotNil(r.job) + r.Equal(r.mockConfig, r.job.config) } func (r *SendNotificationJobTestSuite) TestSignature() { @@ -42,8 +42,12 @@ func (r *SendNotificationJobTestSuite) TestHandle_WrongArgumentCount() { for _, test := range tests { r.Run(test.name, func() { - err := r.job.Handle(test.args...) - r.Contains(err.Error(), "expected 3 arguments") - }) - } + err := r.job.Handle(test.args...) + r.Contains(err.Error(), "expected 3 arguments") + }) + } +} + +func (r *SendNotificationJobTestSuite) TestHandle_WrongArgumentTypes() { + } diff --git a/notification/notification_sender.go b/notification/notification_sender.go index c860709c7..67c691606 100644 --- a/notification/notification_sender.go +++ b/notification/notification_sender.go @@ -1,19 +1,15 @@ package notification import ( - "bytes" - "encoding/gob" - "fmt" - contractsqueuedb "github.com/goravel/framework/contracts/database/db" - contractsmail "github.com/goravel/framework/contracts/mail" - "github.com/goravel/framework/contracts/notification" - contractsqueue "github.com/goravel/framework/contracts/queue" - "github.com/goravel/framework/errors" - "github.com/goravel/framework/notification/channels" - "github.com/goravel/framework/notification/utils" - "github.com/goravel/framework/support/json" - "github.com/goravel/framework/support/str" - "strings" + "bytes" + "encoding/gob" + "fmt" + contractsqueuedb "github.com/goravel/framework/contracts/database/db" + contractsmail "github.com/goravel/framework/contracts/mail" + "github.com/goravel/framework/contracts/notification" + contractsqueue "github.com/goravel/framework/contracts/queue" + "github.com/goravel/framework/errors" + "github.com/goravel/framework/notification/channels" ) type NotificationSender struct { @@ -69,23 +65,30 @@ func (s *NotificationSender) SendNow(notifiables []notification.Notifiable, noti // queueNotification func (s *NotificationSender) queueNotification(notifiables []notification.Notifiable, notif notification.Notif) error { - // 创建数据缓冲区 - var buf bytes.Buffer + for _, notifiable := range notifiables { + vias := notif.Via(notifiable) + if len(vias) == 0 { + return errors.New("no channels defined for notification") + } - // 创建编码器 - encoder := gob.NewEncoder(&buf) - for _, notifiable := range notifiables { + gob.Register(notifiable) + gob.Register(notif) + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + if err := enc.Encode(GobEnvelope{Notifiable: notifiable, Notif: notif}); err != nil { + return err + } - notifiableSerialize := utils.Serialize(notifiable) + args := []contractsqueue.Arg{ + {Type: "[]uint8", Value: buf.Bytes()}, + {Type: "[]uint8", Value: []byte{}}, + {Type: "[]string", Value: vias}, + } - pendingJob := s.queue.Job(NewSendNotificationJob(nil, s.db, s.mail), []contractsqueue.Arg{ - {Type: "[]string", Value: vias}, - {Type: "string", Value: routesJSON}, - {Type: "string", Value: payloadsJSON}, - }) - if err := pendingJob.Dispatch(); err != nil { - return err - } - } - return nil + pendingJob := s.queue.Job(NewSendNotificationJob(nil, s.db, s.mail), args) + if err := pendingJob.Dispatch(); err != nil { + return err + } + } + return nil } diff --git a/notification/payload_notification.go b/notification/payload_notification.go deleted file mode 100644 index 1b4e24e23..000000000 --- a/notification/payload_notification.go +++ /dev/null @@ -1,25 +0,0 @@ -package notification - -import ( - contractsnotification "github.com/goravel/framework/contracts/notification" -) - -type PayloadNotification struct { - Channels []string - Payloads map[string]map[string]interface{} // channel -> payload map -} - -func (n PayloadNotification) Via(_ contractsnotification.Notifiable) []string { - return n.Channels -} - -func (n PayloadNotification) PayloadFor(channel string, _ contractsnotification.Notifiable) (map[string]interface{}, error) { - if n.Payloads == nil { - return map[string]interface{}{}, nil - } - m := n.Payloads[channel] - if m == nil { - return map[string]interface{}{}, nil - } - return m, nil -} \ No newline at end of file diff --git a/notification/simple_notifiable.go b/notification/simple_notifiable.go deleted file mode 100644 index 2dc4e7a53..000000000 --- a/notification/simple_notifiable.go +++ /dev/null @@ -1,12 +0,0 @@ -package notification - -type MapNotifiable struct { - Routes map[string]any -} - -func (n MapNotifiable) NotificationParams() map[string]interface{} { - if n.Routes == nil { - return nil - } - return n.Routes -} diff --git a/notification/type_registry.go b/notification/type_registry.go deleted file mode 100644 index 439b28598..000000000 --- a/notification/type_registry.go +++ /dev/null @@ -1,52 +0,0 @@ -package notification - -import ( - contractsnotification "github.com/goravel/framework/contracts/notification" - "sync" -) - -var ( - notifMu sync.RWMutex - notifRegistry = map[string]func() contractsnotification.Notif{} - - notifiableMu sync.RWMutex - notifiableRegistry = map[string]func(map[string]interface{}) contractsnotification.Notifiable{} -) - -func RegisterNotificationType(name string, factory func() contractsnotification.Notif) { - notifMu.Lock() - defer notifMu.Unlock() - notifRegistry[name] = factory -} - -func GetNotificationInstance(name string) (contractsnotification.Notif, bool) { - notifMu.RLock() - defer notifMu.RUnlock() - if f, ok := notifRegistry[name]; ok { - return f(), true - } - return nil, false -} - -// RegisterNotifiableType keeps backward compatibility for tests/users -func RegisterNotifiableType(name string, factory func(map[string]interface{}) contractsnotification.Notifiable) { - notifiableMu.Lock() - defer notifiableMu.Unlock() - notifiableRegistry[name] = factory -} - -func GetNotifiableInstance(name string, routes map[string]any) (contractsnotification.Notifiable, bool) { - notifiableMu.RLock() - defer notifiableMu.RUnlock() - if f, ok := notifiableRegistry[name]; ok { - return f(routes), true - } - return nil, false -} - -func NotifiableHasWithRoutes(name string) bool { - notifiableMu.RLock() - defer notifiableMu.RUnlock() - _, ok := notifiableRegistry[name] - return ok -} diff --git a/notification/utils/serializer.go b/notification/utils/serializer.go deleted file mode 100644 index d4b585bf7..000000000 --- a/notification/utils/serializer.go +++ /dev/null @@ -1 +0,0 @@ -package utils From 794e87ba1f5fd7e75ebf0a73fc6c1d3abfac2e15 Mon Sep 17 00:00:00 2001 From: wuzhixiang <657873584@qq.com> Date: Wed, 19 Nov 2025 16:06:01 +0800 Subject: [PATCH 08/10] =?UTF-8?q?feat:=20=20=E5=AE=8C=E5=96=84=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- notification/application.go | 6 +- notification/channel_manager.go | 43 ++++----- notification/channels/database.go | 41 ++++---- notification/channels/mail.go | 25 ++--- notification/job.go | 94 ++++++++++--------- notification/models/notification.go | 20 ++-- notification/notification_sender.go | 85 +++++++++-------- notification/service_provider.go | 35 +++---- .../utils/notification_method_caller.go | 81 ++++++++-------- 9 files changed, 231 insertions(+), 199 deletions(-) diff --git a/notification/application.go b/notification/application.go index 3dc6cd541..3c89c03f2 100644 --- a/notification/application.go +++ b/notification/application.go @@ -8,6 +8,8 @@ import ( contractsqueue "github.com/goravel/framework/contracts/queue" ) +// Application provides a facade-backed entry point for sending notifications. +// It wires configuration, queue, database, and mail facades needed by channels. type Application struct { config config.Config queue contractsqueue.Queue @@ -15,6 +17,7 @@ type Application struct { mail contractsmail.Mail } +// NewApplication constructs an Application for the notification module. func NewApplication(config config.Config, queue contractsqueue.Queue, db contractsqueuedb.DB, mail contractsmail.Mail) (*Application, error) { return &Application{ config: config, @@ -24,7 +27,7 @@ func NewApplication(config config.Config, queue contractsqueue.Queue, db contrac }, nil } -// Send a notification. +// Send enqueues a notification to be processed asynchronously. func (r *Application) Send(notifiables []notification.Notifiable, notif notification.Notif) error { if err := (NewNotificationSender(r.db, r.mail, r.queue)).Send(notifiables, notif); err != nil { return err @@ -32,6 +35,7 @@ func (r *Application) Send(notifiables []notification.Notifiable, notif notifica return nil } +// SendNow sends a notification immediately without queueing. func (r *Application) SendNow(notifiables []notification.Notifiable, notif notification.Notif) error { if err := (NewNotificationSender(r.db, r.mail, nil)).SendNow(notifiables, notif); err != nil { return err diff --git a/notification/channel_manager.go b/notification/channel_manager.go index 666d8c1ff..e5a0e6a5e 100644 --- a/notification/channel_manager.go +++ b/notification/channel_manager.go @@ -1,40 +1,41 @@ package notification import ( - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/contracts/notification" - "github.com/goravel/framework/notification/channels" - "strings" - "sync" + "github.com/goravel/framework/contracts/foundation" + "github.com/goravel/framework/contracts/notification" + "github.com/goravel/framework/notification/channels" + "strings" + "sync" ) -// channelRegistry 管理已注册通道(线程安全) +// channelRegistry manages registered channels in a thread-safe manner. type channelRegistry struct { - mu sync.RWMutex - channels map[string]notification.Channel + mu sync.RWMutex + channels map[string]notification.Channel } var registry = &channelRegistry{ - channels: make(map[string]notification.Channel), + channels: make(map[string]notification.Channel), } -// RegisterChannel 允许用户在应用启动时注册自定义通道(注册一次即可) +// RegisterChannel allows registering a custom channel, typically during application boot. +// Channel names are normalized to lowercase. func RegisterChannel(name string, ch notification.Channel) { - registry.mu.Lock() - defer registry.mu.Unlock() - registry.channels[strings.ToLower(name)] = ch + registry.mu.Lock() + defer registry.mu.Unlock() + registry.channels[strings.ToLower(name)] = ch } -// GetChannel 获取已注册通道 +// GetChannel returns a previously registered channel by name. func GetChannel(name string) (notification.Channel, bool) { - registry.mu.RLock() - defer registry.mu.RUnlock() - ch, ok := registry.channels[strings.ToLower(name)] - return ch, ok + registry.mu.RLock() + defer registry.mu.RUnlock() + ch, ok := registry.channels[strings.ToLower(name)] + return ch, ok } -// Boot: 注册内置默认通道(mail, database) +// RegisterDefaultChannels registers built-in default channels: mail and database. func RegisterDefaultChannels(app foundation.Application) { - RegisterChannel("mail", &channels.MailChannel{}) - RegisterChannel("database", &channels.DatabaseChannel{}) + RegisterChannel("mail", &channels.MailChannel{}) + RegisterChannel("database", &channels.DatabaseChannel{}) } diff --git a/notification/channels/database.go b/notification/channels/database.go index be3b9c86e..e8fce16eb 100644 --- a/notification/channels/database.go +++ b/notification/channels/database.go @@ -1,26 +1,28 @@ package channels import ( - "fmt" - "github.com/google/uuid" - contractsqueuedb "github.com/goravel/framework/contracts/database/db" - "github.com/goravel/framework/contracts/notification" - "github.com/goravel/framework/notification/models" - "github.com/goravel/framework/notification/utils" - "github.com/goravel/framework/support/json" - "github.com/goravel/framework/support/str" + "fmt" + "github.com/google/uuid" + contractsqueuedb "github.com/goravel/framework/contracts/database/db" + "github.com/goravel/framework/contracts/notification" + "github.com/goravel/framework/notification/models" + "github.com/goravel/framework/notification/utils" + "github.com/goravel/framework/support/json" + "github.com/goravel/framework/support/str" ) -// DatabaseChannel 默认数据库通道 +// DatabaseChannel is the default database persistence channel. type DatabaseChannel struct { - db contractsqueuedb.DB + db contractsqueuedb.DB } +// Send persists the notification payload to the notifications table. +// It expects the notification to implement a ToDatabase(notifiable) method or PayloadProvider. func (c *DatabaseChannel) Send(notifiable notification.Notifiable, notif interface{}) error { - data, err := utils.CallToMethod(notif, "ToDatabase", notifiable) - if err != nil { - return fmt.Errorf("[DatabaseChannel] %s", err.Error()) - } + data, err := utils.CallToMethod(notif, "ToDatabase", notifiable) + if err != nil { + return fmt.Errorf("[DatabaseChannel] %s", err.Error()) + } var notificationModel models.Notification notificationModel.ID = uuid.New().String() @@ -32,12 +34,13 @@ func (c *DatabaseChannel) Send(notifiable notification.Notifiable, notif interfa jsonData, _ := json.MarshalString(data) notificationModel.Data = jsonData - if _, err = c.db.Table("notifications").Insert(¬ificationModel); err != nil { - return err - } - return nil + if _, err = c.db.Table("notifications").Insert(¬ificationModel); err != nil { + return err + } + return nil } +// SetDB injects the database facade into the channel. func (c *DatabaseChannel) SetDB(db contractsqueuedb.DB) { - c.db = db + c.db = db } diff --git a/notification/channels/mail.go b/notification/channels/mail.go index f929c4565..6c748416e 100644 --- a/notification/channels/mail.go +++ b/notification/channels/mail.go @@ -1,18 +1,20 @@ package channels import ( - "fmt" - contractsmail "github.com/goravel/framework/contracts/mail" - "github.com/goravel/framework/contracts/notification" - "github.com/goravel/framework/mail" - "github.com/goravel/framework/notification/utils" + "fmt" + contractsmail "github.com/goravel/framework/contracts/mail" + "github.com/goravel/framework/contracts/notification" + "github.com/goravel/framework/mail" + "github.com/goravel/framework/notification/utils" ) -// MailChannel 默认邮件通道 +// MailChannel is the default mail delivery channel. type MailChannel struct { - mail contractsmail.Mail + mail contractsmail.Mail } +// Send delivers a notification via email using the notifiable's params. +// It expects the notification to implement a ToMail(notifiable) method or PayloadProvider. func (c *MailChannel) Send(notifiable notification.Notifiable, notif interface{}) error { data, err := utils.CallToMethod(notif, "ToMail", notifiable) if err != nil { @@ -36,8 +38,8 @@ func (c *MailChannel) Send(notifiable notification.Notifiable, notif interface{} return fmt.Errorf("[MailChannel] notifiable has no mail") } - content := data["content"].(string) - subject := data["subject"].(string) + content := data["content"].(string) + subject := data["subject"].(string) if err := c.mail.To([]string{email}). Content(mail.Html(content)). @@ -45,9 +47,10 @@ func (c *MailChannel) Send(notifiable notification.Notifiable, notif interface{} return err } - return nil + return nil } +// SetMail injects the mail facade into the channel. func (c *MailChannel) SetMail(mail contractsmail.Mail) { - c.mail = mail + c.mail = mail } diff --git a/notification/job.go b/notification/job.go index 286b96ca6..3b9fcab38 100644 --- a/notification/job.go +++ b/notification/job.go @@ -1,45 +1,53 @@ package notification import ( - "bytes" - "encoding/gob" - "fmt" - "github.com/goravel/framework/contracts/config" - contractsqueuedb "github.com/goravel/framework/contracts/database/db" - contractsmail "github.com/goravel/framework/contracts/mail" - contractsnotification "github.com/goravel/framework/contracts/notification" - "github.com/goravel/framework/notification/channels" + "bytes" + "encoding/gob" + "fmt" + "github.com/goravel/framework/contracts/config" + contractsqueuedb "github.com/goravel/framework/contracts/database/db" + contractsmail "github.com/goravel/framework/contracts/mail" + contractsnotification "github.com/goravel/framework/contracts/notification" + "github.com/goravel/framework/notification/channels" ) +// SendNotificationJob is the queue job that delivers notifications to channels. +// It decodes the serialized payload and invokes the appropriate channel senders. type SendNotificationJob struct { - config config.Config - db contractsqueuedb.DB - mail contractsmail.Mail + config config.Config + db contractsqueuedb.DB + mail contractsmail.Mail } +// GobEnvelope wraps both notifiable and notification for simplified serialization. type GobEnvelope struct { - Notifiable any - Notif any + Notifiable any + Notif any } +// NewSendNotificationJob constructs a SendNotificationJob with required facades. func NewSendNotificationJob(config config.Config, db contractsqueuedb.DB, mail contractsmail.Mail) *SendNotificationJob { - return &SendNotificationJob{ - config: config, - db: db, - mail: mail, - } + return &SendNotificationJob{ + config: config, + db: db, + mail: mail, + } } -// Signature The name and signature of the job. +// Signature returns the unique name of the job. func (r *SendNotificationJob) Signature() string { - return "goravel_send_notification_job" + return "goravel_send_notification_job" } -// Handle Execute the job. +// Handle executes the job, decoding arguments and forwarding to registered channels. +// Expected arguments: +// 0: notifiable bytes (GobEnvelope when notif bytes empty) +// 1: notification bytes +// 2: []string of channel names func (r *SendNotificationJob) Handle(args ...any) error { - if len(args) != 3 { - return fmt.Errorf("expected 3 arguments, got %d", len(args)) - } + if len(args) != 3 { + return fmt.Errorf("expected 3 arguments, got %d", len(args)) + } notifiableBytes, _ := args[0].([]uint8) notifBytes, _ := args[1].([]uint8) @@ -79,24 +87,24 @@ func (r *SendNotificationJob) Handle(args ...any) error { return fmt.Errorf("decoded notif does not implement Notif: %T", notif) } - for _, chName := range vias { - ch, ok := GetChannel(chName) - if !ok { - return fmt.Errorf("channel not registered: %s", chName) - } - if chName == "database" { - if databaseChannel, ok := ch.(*channels.DatabaseChannel); ok { - databaseChannel.SetDB(r.db) - } - } else if chName == "mail" { - if mailChannel, ok := ch.(*channels.MailChannel); ok { - mailChannel.SetMail(r.mail) - } - } - if err := ch.Send(nbl, nf); err != nil { - return fmt.Errorf("channel %s send error: %w", chName, err) - } - } + for _, chName := range vias { + ch, ok := GetChannel(chName) + if !ok { + return fmt.Errorf("channel not registered: %s", chName) + } + if chName == "database" { + if databaseChannel, ok := ch.(*channels.DatabaseChannel); ok { + databaseChannel.SetDB(r.db) + } + } else if chName == "mail" { + if mailChannel, ok := ch.(*channels.MailChannel); ok { + mailChannel.SetMail(r.mail) + } + } + if err := ch.Send(nbl, nf); err != nil { + return fmt.Errorf("channel %s send error: %w", chName, err) + } + } - return nil + return nil } diff --git a/notification/models/notification.go b/notification/models/notification.go index f85b6bc0f..3b8dc5471 100644 --- a/notification/models/notification.go +++ b/notification/models/notification.go @@ -1,16 +1,18 @@ package models import ( - "github.com/goravel/framework/database/orm" - "github.com/goravel/framework/support/carbon" + "github.com/goravel/framework/database/orm" + "github.com/goravel/framework/support/carbon" ) +// Notification represents a stored notification record. +// Fields align with the `notifications` table schema. type Notification struct { - ID string `json:"id"` - Type string `json:"type"` - NotifiableType string `json:"notifiable_type"` - NotifiableId string `json:"notifiable_id"` - Data string `json:"data"` - ReadAt *carbon.DateTime `json:"read_at"` - orm.Timestamps + ID string `json:"id"` + Type string `json:"type"` + NotifiableType string `json:"notifiable_type"` + NotifiableId string `json:"notifiable_id"` + Data string `json:"data"` + ReadAt *carbon.DateTime `json:"read_at"` + orm.Timestamps } diff --git a/notification/notification_sender.go b/notification/notification_sender.go index 67c691606..ee05a00b1 100644 --- a/notification/notification_sender.go +++ b/notification/notification_sender.go @@ -12,58 +12,65 @@ import ( "github.com/goravel/framework/notification/channels" ) +// NotificationSender coordinates sending notifications via registered channels. +// It supports both immediate dispatch (SendNow) and queued dispatch (Send). type NotificationSender struct { - db contractsqueuedb.DB - mail contractsmail.Mail - queue contractsqueue.Queue + db contractsqueuedb.DB + mail contractsmail.Mail + queue contractsqueue.Queue } +// NewNotificationSender creates a new NotificationSender with the provided facades. func NewNotificationSender(db contractsqueuedb.DB, mail contractsmail.Mail, queue contractsqueue.Queue) *NotificationSender { - return &NotificationSender{ - db: db, - mail: mail, - queue: queue, - } + return &NotificationSender{ + db: db, + mail: mail, + queue: queue, + } } -// Send(notifiables []Notifiable, notification Notif) error +// Send enqueues notifications for asynchronous processing. +// For each notifiable, the notification payload is serialized and a job is dispatched. func (s *NotificationSender) Send(notifiables []notification.Notifiable, notification notification.Notif) error { - if err := s.queueNotification(notifiables, notification); err != nil { - return err - } - return nil + if err := s.queueNotification(notifiables, notification); err != nil { + return err + } + return nil } +// SendNow sends notifications immediately without queuing. +// Channels are resolved per notifiable using Notif.Via and configured with facades. func (s *NotificationSender) SendNow(notifiables []notification.Notifiable, notif notification.Notif) error { - for _, notifiable := range notifiables { - vias := notif.Via(notifiable) - if len(vias) == 0 { - return errors.New("no channels defined for notification") - } + for _, notifiable := range notifiables { + vias := notif.Via(notifiable) + if len(vias) == 0 { + return errors.New("no channels defined for notification") + } - for _, chName := range vias { - ch, ok := GetChannel(chName) - if !ok { - return fmt.Errorf("channel not registered: %s", chName) - } - if chName == "database" { - if databaseChannel, ok := ch.(*channels.DatabaseChannel); ok { - databaseChannel.SetDB(s.db) - } - } else if chName == "mail" { - if mailChannel, ok := ch.(*channels.MailChannel); ok { - mailChannel.SetMail(s.mail) - } - } - if err := ch.Send(notifiable, notif); err != nil { - return fmt.Errorf("channel %s send error: %w", chName, err) - } - } - } - return nil + for _, chName := range vias { + ch, ok := GetChannel(chName) + if !ok { + return fmt.Errorf("channel not registered: %s", chName) + } + if chName == "database" { + if databaseChannel, ok := ch.(*channels.DatabaseChannel); ok { + databaseChannel.SetDB(s.db) + } + } else if chName == "mail" { + if mailChannel, ok := ch.(*channels.MailChannel); ok { + mailChannel.SetMail(s.mail) + } + } + if err := ch.Send(notifiable, notif); err != nil { + return fmt.Errorf("channel %s send error: %w", chName, err) + } + } + } + return nil } -// queueNotification +// queueNotification serializes the notifiable and notification, then dispatches +// a SendNotificationJob with the target channels for asynchronous processing. func (s *NotificationSender) queueNotification(notifiables []notification.Notifiable, notif notification.Notif) error { for _, notifiable := range notifiables { vias := notif.Via(notifiable) diff --git a/notification/service_provider.go b/notification/service_provider.go index 1671773a8..1518d308b 100644 --- a/notification/service_provider.go +++ b/notification/service_provider.go @@ -10,24 +10,24 @@ import ( type ServiceProvider struct { } -// Relationship returns the relationship of the service provider. +// Relationship declares bindings and dependencies for the notification service provider. func (r *ServiceProvider) Relationship() binding.Relationship { - return binding.Relationship{ - Bindings: []string{ - binding.Notification, - }, - Dependencies: binding.Bindings[binding.Notification].Dependencies, - ProvideFor: []string{}, - } + return binding.Relationship{ + Bindings: []string{ + binding.Notification, + }, + Dependencies: binding.Bindings[binding.Notification].Dependencies, + ProvideFor: []string{}, + } } -// Register registers the service provider. +// Register binds the notification Application into the container using required facades. func (r *ServiceProvider) Register(app foundation.Application) { - app.Bind(binding.Notification, func(app foundation.Application) (any, error) { - config := app.MakeConfig() - if config == nil { - return nil, errors.ConfigFacadeNotSet.SetModule(errors.ModuleMail) - } + app.Bind(binding.Notification, func(app foundation.Application) (any, error) { + config := app.MakeConfig() + if config == nil { + return nil, errors.ConfigFacadeNotSet.SetModule(errors.ModuleMail) + } queue := app.MakeQueue() if queue == nil { @@ -44,16 +44,17 @@ func (r *ServiceProvider) Register(app foundation.Application) { return nil, errors.DBFacadeNotSet.SetModule(errors.ModuleDB) } - return NewApplication(config, queue, db, mail) - }) + return NewApplication(config, queue, db, mail) + }) } -// Boot boots the service provider, will be called after all service providers are registered. +// Boot initializes built-in channels and registers jobs once all providers are registered. func (r *ServiceProvider) Boot(app foundation.Application) { RegisterDefaultChannels(app) r.registerJobs(app) } +// registerJobs registers the SendNotificationJob with the queue facade if available. func (r *ServiceProvider) registerJobs(app foundation.Application) { queueFacade := app.MakeQueue() if queueFacade == nil { diff --git a/notification/utils/notification_method_caller.go b/notification/utils/notification_method_caller.go index 399577f37..44c0bedaa 100644 --- a/notification/utils/notification_method_caller.go +++ b/notification/utils/notification_method_caller.go @@ -7,19 +7,22 @@ import ( "strings" ) +// CallToMethod invokes a notification's channel-specific method via reflection. +// It supports both value and pointer receivers, and falls back to PayloadProvider. +// The returned value is normalized as map[string]interface{} for channel consumption. func CallToMethod(notification interface{}, methodName string, notifiable contractsnotification.Notifiable) (map[string]interface{}, error) { v := reflect.ValueOf(notification) if !v.IsValid() { return nil, fmt.Errorf("invalid notification value") } - // 查找方法(优先指针接收者) - method := v.MethodByName(methodName) - if !method.IsValid() && v.CanAddr() { - method = v.Addr().MethodByName(methodName) - } + // Locate method, preferring pointer receiver if available. + method := v.MethodByName(methodName) + if !method.IsValid() && v.CanAddr() { + method = v.Addr().MethodByName(methodName) + } if !method.IsValid() { - // fallback: support PayloadProvider + // Fallback: support PayloadProvider for dynamic channel payloads. if provider, ok := v.Interface().(contractsnotification.PayloadProvider); ok { channel := strings.ToLower(strings.TrimPrefix(methodName, "To")) return provider.PayloadFor(channel, notifiable) @@ -27,45 +30,45 @@ func CallToMethod(notification interface{}, methodName string, notifiable contra return nil, fmt.Errorf("method %s not found", methodName) } - // 调用方法 - results := method.Call([]reflect.Value{reflect.ValueOf(notifiable)}) - if len(results) == 0 { - return nil, fmt.Errorf("method %s returned no values", methodName) - } + // Invoke method with the notifiable. + results := method.Call([]reflect.Value{reflect.ValueOf(notifiable)}) + if len(results) == 0 { + return nil, fmt.Errorf("method %s returned no values", methodName) + } - // 处理错误返回 - if len(results) >= 2 && !results[1].IsNil() { - if err, ok := results[1].Interface().(error); ok { - return nil, err - } - return nil, fmt.Errorf("second return of %s is not error", methodName) - } + // Handle optional error return value. + if len(results) >= 2 && !results[1].IsNil() { + if err, ok := results[1].Interface().(error); ok { + return nil, err + } + return nil, fmt.Errorf("second return of %s is not error", methodName) + } - // 转换第一个返回值为 map[string]interface{} - first := results[0].Interface() - switch data := first.(type) { - case map[string]interface{}: - return data, nil - case map[string]string: + // Convert the first return value to map[string]interface{}. + first := results[0].Interface() + switch data := first.(type) { + case map[string]interface{}: + return data, nil + case map[string]string: out := make(map[string]interface{}, len(data)) for k, v := range data { out[k] = v } - return out, nil - } + return out, nil + } - // 处理结构体 - if rv := reflect.ValueOf(first); rv.Kind() == reflect.Struct { - out := make(map[string]interface{}) - rt := rv.Type() - for i := 0; i < rv.NumField(); i++ { - field := rt.Field(i) - if field.PkgPath == "" { // 仅导出字段 - out[field.Name] = rv.Field(i).Interface() - } - } - return out, nil - } + // Handle struct result by exporting fields. + if rv := reflect.ValueOf(first); rv.Kind() == reflect.Struct { + out := make(map[string]interface{}) + rt := rv.Type() + for i := 0; i < rv.NumField(); i++ { + field := rt.Field(i) + if field.PkgPath == "" { // only exported fields + out[field.Name] = rv.Field(i).Interface() + } + } + return out, nil + } - return nil, fmt.Errorf("unsupported return type from %s", methodName) + return nil, fmt.Errorf("unsupported return type from %s", methodName) } From 1fa57b09be8444b5383cd01ddd2d256b797b8f5f Mon Sep 17 00:00:00 2001 From: wuzhixiang <657873584@qq.com> Date: Wed, 19 Nov 2025 16:12:21 +0800 Subject: [PATCH 09/10] fix: Optimization and streamlining --- notification/application.go | 10 ++----- notification/channel_manager.go | 3 +- notification/channels/database.go | 13 +++++++-- notification/channels/mail.go | 43 +++++++++++++++++++---------- notification/job.go | 13 ++++----- notification/notification_sender.go | 18 ++++-------- notification/service_provider.go | 2 +- 7 files changed, 53 insertions(+), 49 deletions(-) diff --git a/notification/application.go b/notification/application.go index 3c89c03f2..1e8665b1c 100644 --- a/notification/application.go +++ b/notification/application.go @@ -29,16 +29,10 @@ func NewApplication(config config.Config, queue contractsqueue.Queue, db contrac // Send enqueues a notification to be processed asynchronously. func (r *Application) Send(notifiables []notification.Notifiable, notif notification.Notif) error { - if err := (NewNotificationSender(r.db, r.mail, r.queue)).Send(notifiables, notif); err != nil { - return err - } - return nil + return (NewNotificationSender(r.db, r.mail, r.queue)).Send(notifiables, notif) } // SendNow sends a notification immediately without queueing. func (r *Application) SendNow(notifiables []notification.Notifiable, notif notification.Notif) error { - if err := (NewNotificationSender(r.db, r.mail, nil)).SendNow(notifiables, notif); err != nil { - return err - } - return nil + return (NewNotificationSender(r.db, r.mail, nil)).SendNow(notifiables, notif) } diff --git a/notification/channel_manager.go b/notification/channel_manager.go index e5a0e6a5e..a53c4482d 100644 --- a/notification/channel_manager.go +++ b/notification/channel_manager.go @@ -1,7 +1,6 @@ package notification import ( - "github.com/goravel/framework/contracts/foundation" "github.com/goravel/framework/contracts/notification" "github.com/goravel/framework/notification/channels" "strings" @@ -35,7 +34,7 @@ func GetChannel(name string) (notification.Channel, bool) { } // RegisterDefaultChannels registers built-in default channels: mail and database. -func RegisterDefaultChannels(app foundation.Application) { +func RegisterDefaultChannels() { RegisterChannel("mail", &channels.MailChannel{}) RegisterChannel("database", &channels.DatabaseChannel{}) } diff --git a/notification/channels/database.go b/notification/channels/database.go index e8fce16eb..bb24acf88 100644 --- a/notification/channels/database.go +++ b/notification/channels/database.go @@ -24,9 +24,16 @@ func (c *DatabaseChannel) Send(notifiable notification.Notifiable, notif interfa return fmt.Errorf("[DatabaseChannel] %s", err.Error()) } - var notificationModel models.Notification - notificationModel.ID = uuid.New().String() - notificationModel.NotifiableId = notifiable.NotificationParams()["id"].(string) + var notificationModel models.Notification + notificationModel.ID = uuid.New().String() + if v, ok := notifiable.NotificationParams()["id"]; ok { + if s, ok := v.(string); ok { + notificationModel.NotifiableId = s + } + } + if notificationModel.NotifiableId == "" { + return fmt.Errorf("[DatabaseChannel] notifiable has no id") + } notificationModel.NotifiableType = str.Of(fmt.Sprintf("%T", notifiable)).Replace("*", "").String() notificationModel.Type = str.Of(fmt.Sprintf("%T", notif)).Replace("*", "").String() diff --git a/notification/channels/mail.go b/notification/channels/mail.go index 6c748416e..039b36eb5 100644 --- a/notification/channels/mail.go +++ b/notification/channels/mail.go @@ -21,25 +21,24 @@ func (c *MailChannel) Send(notifiable notification.Notifiable, notif interface{} return fmt.Errorf("[MailChannel] %s", err.Error()) } params := notifiable.NotificationParams() - var email string - if v, ok := params["mail"]; ok { - if s, ok := v.(string); ok { - email = s - } - } - if email == "" { - if v, ok := params["email"]; ok { - if s, ok := v.(string); ok { - email = s - } - } - } + email := getEmail(params) if email == "" { return fmt.Errorf("[MailChannel] notifiable has no mail") } - content := data["content"].(string) - subject := data["subject"].(string) + contentVal, ok := data["content"] + if !ok { + return fmt.Errorf("[MailChannel] content not provided") + } + subjectVal, ok := data["subject"] + if !ok { + return fmt.Errorf("[MailChannel] subject not provided") + } + content, _ := contentVal.(string) + subject, _ := subjectVal.(string) + if content == "" || subject == "" { + return fmt.Errorf("[MailChannel] invalid content or subject") + } if err := c.mail.To([]string{email}). Content(mail.Html(content)). @@ -54,3 +53,17 @@ func (c *MailChannel) Send(notifiable notification.Notifiable, notif interface{} func (c *MailChannel) SetMail(mail contractsmail.Mail) { c.mail = mail } + +func getEmail(params map[string]interface{}) string { + if v, ok := params["mail"]; ok { + if s, ok := v.(string); ok && s != "" { + return s + } + } + if v, ok := params["email"]; ok { + if s, ok := v.(string); ok && s != "" { + return s + } + } + return "" +} diff --git a/notification/job.go b/notification/job.go index 3b9fcab38..8e8f7caa3 100644 --- a/notification/job.go +++ b/notification/job.go @@ -92,14 +92,11 @@ func (r *SendNotificationJob) Handle(args ...any) error { if !ok { return fmt.Errorf("channel not registered: %s", chName) } - if chName == "database" { - if databaseChannel, ok := ch.(*channels.DatabaseChannel); ok { - databaseChannel.SetDB(r.db) - } - } else if chName == "mail" { - if mailChannel, ok := ch.(*channels.MailChannel); ok { - mailChannel.SetMail(r.mail) - } + switch chTyped := ch.(type) { + case *channels.DatabaseChannel: + chTyped.SetDB(r.db) + case *channels.MailChannel: + chTyped.SetMail(r.mail) } if err := ch.Send(nbl, nf); err != nil { return fmt.Errorf("channel %s send error: %w", chName, err) diff --git a/notification/notification_sender.go b/notification/notification_sender.go index ee05a00b1..9c6c6a148 100644 --- a/notification/notification_sender.go +++ b/notification/notification_sender.go @@ -32,10 +32,7 @@ func NewNotificationSender(db contractsqueuedb.DB, mail contractsmail.Mail, queu // Send enqueues notifications for asynchronous processing. // For each notifiable, the notification payload is serialized and a job is dispatched. func (s *NotificationSender) Send(notifiables []notification.Notifiable, notification notification.Notif) error { - if err := s.queueNotification(notifiables, notification); err != nil { - return err - } - return nil + return s.queueNotification(notifiables, notification) } // SendNow sends notifications immediately without queuing. @@ -52,14 +49,11 @@ func (s *NotificationSender) SendNow(notifiables []notification.Notifiable, noti if !ok { return fmt.Errorf("channel not registered: %s", chName) } - if chName == "database" { - if databaseChannel, ok := ch.(*channels.DatabaseChannel); ok { - databaseChannel.SetDB(s.db) - } - } else if chName == "mail" { - if mailChannel, ok := ch.(*channels.MailChannel); ok { - mailChannel.SetMail(s.mail) - } + switch chTyped := ch.(type) { + case *channels.DatabaseChannel: + chTyped.SetDB(s.db) + case *channels.MailChannel: + chTyped.SetMail(s.mail) } if err := ch.Send(notifiable, notif); err != nil { return fmt.Errorf("channel %s send error: %w", chName, err) diff --git a/notification/service_provider.go b/notification/service_provider.go index 1518d308b..70d113784 100644 --- a/notification/service_provider.go +++ b/notification/service_provider.go @@ -50,7 +50,7 @@ func (r *ServiceProvider) Register(app foundation.Application) { // Boot initializes built-in channels and registers jobs once all providers are registered. func (r *ServiceProvider) Boot(app foundation.Application) { - RegisterDefaultChannels(app) + RegisterDefaultChannels() r.registerJobs(app) } From 15ba236f14b88dc4f6f14a0c05f4cb711a2b1e72 Mon Sep 17 00:00:00 2001 From: wuzhixiang <657873584@qq.com> Date: Wed, 19 Nov 2025 16:15:56 +0800 Subject: [PATCH 10/10] chore: exec go run github.com/vektra/mockery/v2 --- mocks/foundation/Application.go | 49 ++++++++++++++ mocks/notification/Channel.go | 82 +++++++++++++++++++++++ mocks/notification/Notif.go | 83 +++++++++++++++++++++++ mocks/notification/Notifiable.go | 79 ++++++++++++++++++++++ mocks/notification/Notification.go | 81 +++++++++++++++++++++++ mocks/notification/PayloadProvider.go | 94 +++++++++++++++++++++++++++ 6 files changed, 468 insertions(+) create mode 100644 mocks/notification/Channel.go create mode 100644 mocks/notification/Notif.go create mode 100644 mocks/notification/Notifiable.go create mode 100644 mocks/notification/Notification.go create mode 100644 mocks/notification/PayloadProvider.go diff --git a/mocks/foundation/Application.go b/mocks/foundation/Application.go index 30d3fbdbd..377d149e6 100644 --- a/mocks/foundation/Application.go +++ b/mocks/foundation/Application.go @@ -38,6 +38,8 @@ import ( mock "github.com/stretchr/testify/mock" + notification "github.com/goravel/framework/contracts/notification" + orm "github.com/goravel/framework/contracts/database/orm" process "github.com/goravel/framework/contracts/process" @@ -1671,6 +1673,53 @@ func (_c *Application_MakeMail_Call) RunAndReturn(run func() mail.Mail) *Applica return _c } +// MakeNotification provides a mock function with no fields +func (_m *Application) MakeNotification() notification.Notification { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for MakeNotification") + } + + var r0 notification.Notification + if rf, ok := ret.Get(0).(func() notification.Notification); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(notification.Notification) + } + } + + return r0 +} + +// Application_MakeNotification_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MakeNotification' +type Application_MakeNotification_Call struct { + *mock.Call +} + +// MakeNotification is a helper method to define mock.On call +func (_e *Application_Expecter) MakeNotification() *Application_MakeNotification_Call { + return &Application_MakeNotification_Call{Call: _e.mock.On("MakeNotification")} +} + +func (_c *Application_MakeNotification_Call) Run(run func()) *Application_MakeNotification_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Application_MakeNotification_Call) Return(_a0 notification.Notification) *Application_MakeNotification_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Application_MakeNotification_Call) RunAndReturn(run func() notification.Notification) *Application_MakeNotification_Call { + _c.Call.Return(run) + return _c +} + // MakeOrm provides a mock function with no fields func (_m *Application) MakeOrm() orm.Orm { ret := _m.Called() diff --git a/mocks/notification/Channel.go b/mocks/notification/Channel.go new file mode 100644 index 000000000..d96eeed9f --- /dev/null +++ b/mocks/notification/Channel.go @@ -0,0 +1,82 @@ +// Code generated by mockery. DO NOT EDIT. + +package notification + +import ( + notification "github.com/goravel/framework/contracts/notification" + mock "github.com/stretchr/testify/mock" +) + +// Channel is an autogenerated mock type for the Channel type +type Channel struct { + mock.Mock +} + +type Channel_Expecter struct { + mock *mock.Mock +} + +func (_m *Channel) EXPECT() *Channel_Expecter { + return &Channel_Expecter{mock: &_m.Mock} +} + +// Send provides a mock function with given fields: notifiable, notif +func (_m *Channel) Send(notifiable notification.Notifiable, notif interface{}) error { + ret := _m.Called(notifiable, notif) + + if len(ret) == 0 { + panic("no return value specified for Send") + } + + var r0 error + if rf, ok := ret.Get(0).(func(notification.Notifiable, interface{}) error); ok { + r0 = rf(notifiable, notif) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Channel_Send_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Send' +type Channel_Send_Call struct { + *mock.Call +} + +// Send is a helper method to define mock.On call +// - notifiable notification.Notifiable +// - notif interface{} +func (_e *Channel_Expecter) Send(notifiable interface{}, notif interface{}) *Channel_Send_Call { + return &Channel_Send_Call{Call: _e.mock.On("Send", notifiable, notif)} +} + +func (_c *Channel_Send_Call) Run(run func(notifiable notification.Notifiable, notif interface{})) *Channel_Send_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(notification.Notifiable), args[1].(interface{})) + }) + return _c +} + +func (_c *Channel_Send_Call) Return(_a0 error) *Channel_Send_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Channel_Send_Call) RunAndReturn(run func(notification.Notifiable, interface{}) error) *Channel_Send_Call { + _c.Call.Return(run) + return _c +} + +// NewChannel creates a new instance of Channel. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewChannel(t interface { + mock.TestingT + Cleanup(func()) +}) *Channel { + mock := &Channel{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/notification/Notif.go b/mocks/notification/Notif.go new file mode 100644 index 000000000..5a45b833e --- /dev/null +++ b/mocks/notification/Notif.go @@ -0,0 +1,83 @@ +// Code generated by mockery. DO NOT EDIT. + +package notification + +import ( + notification "github.com/goravel/framework/contracts/notification" + mock "github.com/stretchr/testify/mock" +) + +// Notif is an autogenerated mock type for the Notif type +type Notif struct { + mock.Mock +} + +type Notif_Expecter struct { + mock *mock.Mock +} + +func (_m *Notif) EXPECT() *Notif_Expecter { + return &Notif_Expecter{mock: &_m.Mock} +} + +// Via provides a mock function with given fields: notifiable +func (_m *Notif) Via(notifiable notification.Notifiable) []string { + ret := _m.Called(notifiable) + + if len(ret) == 0 { + panic("no return value specified for Via") + } + + var r0 []string + if rf, ok := ret.Get(0).(func(notification.Notifiable) []string); ok { + r0 = rf(notifiable) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + return r0 +} + +// Notif_Via_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Via' +type Notif_Via_Call struct { + *mock.Call +} + +// Via is a helper method to define mock.On call +// - notifiable notification.Notifiable +func (_e *Notif_Expecter) Via(notifiable interface{}) *Notif_Via_Call { + return &Notif_Via_Call{Call: _e.mock.On("Via", notifiable)} +} + +func (_c *Notif_Via_Call) Run(run func(notifiable notification.Notifiable)) *Notif_Via_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(notification.Notifiable)) + }) + return _c +} + +func (_c *Notif_Via_Call) Return(_a0 []string) *Notif_Via_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Notif_Via_Call) RunAndReturn(run func(notification.Notifiable) []string) *Notif_Via_Call { + _c.Call.Return(run) + return _c +} + +// NewNotif creates a new instance of Notif. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewNotif(t interface { + mock.TestingT + Cleanup(func()) +}) *Notif { + mock := &Notif{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/notification/Notifiable.go b/mocks/notification/Notifiable.go new file mode 100644 index 000000000..3946f9b37 --- /dev/null +++ b/mocks/notification/Notifiable.go @@ -0,0 +1,79 @@ +// Code generated by mockery. DO NOT EDIT. + +package notification + +import mock "github.com/stretchr/testify/mock" + +// Notifiable is an autogenerated mock type for the Notifiable type +type Notifiable struct { + mock.Mock +} + +type Notifiable_Expecter struct { + mock *mock.Mock +} + +func (_m *Notifiable) EXPECT() *Notifiable_Expecter { + return &Notifiable_Expecter{mock: &_m.Mock} +} + +// NotificationParams provides a mock function with no fields +func (_m *Notifiable) NotificationParams() map[string]interface{} { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for NotificationParams") + } + + var r0 map[string]interface{} + if rf, ok := ret.Get(0).(func() map[string]interface{}); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]interface{}) + } + } + + return r0 +} + +// Notifiable_NotificationParams_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NotificationParams' +type Notifiable_NotificationParams_Call struct { + *mock.Call +} + +// NotificationParams is a helper method to define mock.On call +func (_e *Notifiable_Expecter) NotificationParams() *Notifiable_NotificationParams_Call { + return &Notifiable_NotificationParams_Call{Call: _e.mock.On("NotificationParams")} +} + +func (_c *Notifiable_NotificationParams_Call) Run(run func()) *Notifiable_NotificationParams_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Notifiable_NotificationParams_Call) Return(_a0 map[string]interface{}) *Notifiable_NotificationParams_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Notifiable_NotificationParams_Call) RunAndReturn(run func() map[string]interface{}) *Notifiable_NotificationParams_Call { + _c.Call.Return(run) + return _c +} + +// NewNotifiable creates a new instance of Notifiable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewNotifiable(t interface { + mock.TestingT + Cleanup(func()) +}) *Notifiable { + mock := &Notifiable{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/notification/Notification.go b/mocks/notification/Notification.go new file mode 100644 index 000000000..8fcf8041d --- /dev/null +++ b/mocks/notification/Notification.go @@ -0,0 +1,81 @@ +// Code generated by mockery. DO NOT EDIT. + +package notification + +import ( + notification "github.com/goravel/framework/contracts/notification" + mock "github.com/stretchr/testify/mock" +) + +// Notification is an autogenerated mock type for the Notification type +type Notification struct { + mock.Mock +} + +type Notification_Expecter struct { + mock *mock.Mock +} + +func (_m *Notification) EXPECT() *Notification_Expecter { + return &Notification_Expecter{mock: &_m.Mock} +} + +// Send provides a mock function with given fields: notifiable +func (_m *Notification) Send(notifiable notification.Notifiable) error { + ret := _m.Called(notifiable) + + if len(ret) == 0 { + panic("no return value specified for Send") + } + + var r0 error + if rf, ok := ret.Get(0).(func(notification.Notifiable) error); ok { + r0 = rf(notifiable) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Notification_Send_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Send' +type Notification_Send_Call struct { + *mock.Call +} + +// Send is a helper method to define mock.On call +// - notifiable notification.Notifiable +func (_e *Notification_Expecter) Send(notifiable interface{}) *Notification_Send_Call { + return &Notification_Send_Call{Call: _e.mock.On("Send", notifiable)} +} + +func (_c *Notification_Send_Call) Run(run func(notifiable notification.Notifiable)) *Notification_Send_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(notification.Notifiable)) + }) + return _c +} + +func (_c *Notification_Send_Call) Return(_a0 error) *Notification_Send_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Notification_Send_Call) RunAndReturn(run func(notification.Notifiable) error) *Notification_Send_Call { + _c.Call.Return(run) + return _c +} + +// NewNotification creates a new instance of Notification. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewNotification(t interface { + mock.TestingT + Cleanup(func()) +}) *Notification { + mock := &Notification{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/notification/PayloadProvider.go b/mocks/notification/PayloadProvider.go new file mode 100644 index 000000000..9ae23b596 --- /dev/null +++ b/mocks/notification/PayloadProvider.go @@ -0,0 +1,94 @@ +// Code generated by mockery. DO NOT EDIT. + +package notification + +import ( + notification "github.com/goravel/framework/contracts/notification" + mock "github.com/stretchr/testify/mock" +) + +// PayloadProvider is an autogenerated mock type for the PayloadProvider type +type PayloadProvider struct { + mock.Mock +} + +type PayloadProvider_Expecter struct { + mock *mock.Mock +} + +func (_m *PayloadProvider) EXPECT() *PayloadProvider_Expecter { + return &PayloadProvider_Expecter{mock: &_m.Mock} +} + +// PayloadFor provides a mock function with given fields: channel, notifiable +func (_m *PayloadProvider) PayloadFor(channel string, notifiable notification.Notifiable) (map[string]interface{}, error) { + ret := _m.Called(channel, notifiable) + + if len(ret) == 0 { + panic("no return value specified for PayloadFor") + } + + var r0 map[string]interface{} + var r1 error + if rf, ok := ret.Get(0).(func(string, notification.Notifiable) (map[string]interface{}, error)); ok { + return rf(channel, notifiable) + } + if rf, ok := ret.Get(0).(func(string, notification.Notifiable) map[string]interface{}); ok { + r0 = rf(channel, notifiable) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]interface{}) + } + } + + if rf, ok := ret.Get(1).(func(string, notification.Notifiable) error); ok { + r1 = rf(channel, notifiable) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PayloadProvider_PayloadFor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PayloadFor' +type PayloadProvider_PayloadFor_Call struct { + *mock.Call +} + +// PayloadFor is a helper method to define mock.On call +// - channel string +// - notifiable notification.Notifiable +func (_e *PayloadProvider_Expecter) PayloadFor(channel interface{}, notifiable interface{}) *PayloadProvider_PayloadFor_Call { + return &PayloadProvider_PayloadFor_Call{Call: _e.mock.On("PayloadFor", channel, notifiable)} +} + +func (_c *PayloadProvider_PayloadFor_Call) Run(run func(channel string, notifiable notification.Notifiable)) *PayloadProvider_PayloadFor_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(notification.Notifiable)) + }) + return _c +} + +func (_c *PayloadProvider_PayloadFor_Call) Return(_a0 map[string]interface{}, _a1 error) *PayloadProvider_PayloadFor_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *PayloadProvider_PayloadFor_Call) RunAndReturn(run func(string, notification.Notifiable) (map[string]interface{}, error)) *PayloadProvider_PayloadFor_Call { + _c.Call.Return(run) + return _c +} + +// NewPayloadProvider creates a new instance of PayloadProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPayloadProvider(t interface { + mock.TestingT + Cleanup(func()) +}) *PayloadProvider { + mock := &PayloadProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}