feat: add notification events handling and MQTT integration

- Implemented notification event publishing with a new NotificationEventPublisher interface.
- Created a noopNotificationEventPublisher for testing purposes.
- Added functionality to publish notification created events via MQTT.
- Introduced a new stream event publisher for handling job logs and updates.
- Added database migration for popup_ads table.
- Created tests for notification events and popup ads functionality.
- Established MQTT connection and publishing helpers for event messages.
This commit is contained in:
2026-03-29 15:47:09 +00:00
parent a910e6c624
commit 863a0ea2f6
42 changed files with 4606 additions and 576 deletions

View File

@@ -24,7 +24,7 @@ type PlayerConfig struct {
Airplay *bool `gorm:"column:airplay;type:boolean;not null;default:true" json:"airplay"`
Chromecast *bool `gorm:"column:chromecast;type:boolean;not null;default:true" json:"chromecast"`
IsActive *bool `gorm:"column:is_active;type:boolean;not null;default:true" json:"is_active"`
IsDefault bool `gorm:"column:is_default;type:boolean;not null;index:idx_player_configs_user_default,priority:1;index:idx_player_configs_is_default,priority:1" json:"is_default"`
IsDefault bool `gorm:"column:is_default;type:boolean;not null;index:idx_player_configs_is_default,priority:1;index:idx_player_configs_user_default,priority:1" json:"is_default"`
CreatedAt *time.Time `gorm:"column:created_at;type:timestamp(3) without time zone;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(3) without time zone;not null" json:"updated_at"`
Version *int64 `gorm:"column:version;type:bigint;not null;default:1;version" json:"-"`

View File

@@ -0,0 +1,30 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNamePopupAd = "popup_ads"
// PopupAd mapped from table <popup_ads>
type PopupAd struct {
ID string `gorm:"column:id;type:uuid;primaryKey" json:"id"`
UserID string `gorm:"column:user_id;type:uuid;not null;index:idx_popup_ads_user_id,priority:1;index:idx_popup_ads_user_active,priority:2" json:"user_id"`
Type string `gorm:"column:type;type:character varying(20);not null" json:"type"`
Label string `gorm:"column:label;type:text;not null" json:"label"`
Value string `gorm:"column:value;type:text;not null" json:"value"`
IsActive *bool `gorm:"column:is_active;type:boolean;not null;index:idx_popup_ads_user_active,priority:1;default:true" json:"is_active"`
MaxTriggersPerSession *int32 `gorm:"column:max_triggers_per_session;type:integer;not null;default:3" json:"max_triggers_per_session"`
CreatedAt *time.Time `gorm:"column:created_at;type:timestamp with time zone;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt *time.Time `gorm:"column:updated_at;type:timestamp with time zone" json:"updated_at"`
Version *int64 `gorm:"column:version;type:bigint;not null;default:1;version" json:"-"`
}
// TableName PopupAd's table name
func (*PopupAd) TableName() string {
return TableNamePopupAd
}

View File

@@ -25,6 +25,7 @@ var (
Plan *plan
PlanSubscription *planSubscription
PlayerConfig *playerConfig
PopupAd *popupAd
User *user
UserPreference *userPreference
Video *video
@@ -41,6 +42,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
Plan = &Q.Plan
PlanSubscription = &Q.PlanSubscription
PlayerConfig = &Q.PlayerConfig
PopupAd = &Q.PopupAd
User = &Q.User
UserPreference = &Q.UserPreference
Video = &Q.Video
@@ -58,6 +60,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
Plan: newPlan(db, opts...),
PlanSubscription: newPlanSubscription(db, opts...),
PlayerConfig: newPlayerConfig(db, opts...),
PopupAd: newPopupAd(db, opts...),
User: newUser(db, opts...),
UserPreference: newUserPreference(db, opts...),
Video: newVideo(db, opts...),
@@ -76,6 +79,7 @@ type Query struct {
Plan plan
PlanSubscription planSubscription
PlayerConfig playerConfig
PopupAd popupAd
User user
UserPreference userPreference
Video video
@@ -95,6 +99,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
Plan: q.Plan.clone(db),
PlanSubscription: q.PlanSubscription.clone(db),
PlayerConfig: q.PlayerConfig.clone(db),
PopupAd: q.PopupAd.clone(db),
User: q.User.clone(db),
UserPreference: q.UserPreference.clone(db),
Video: q.Video.clone(db),
@@ -121,6 +126,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
Plan: q.Plan.replaceDB(db),
PlanSubscription: q.PlanSubscription.replaceDB(db),
PlayerConfig: q.PlayerConfig.replaceDB(db),
PopupAd: q.PopupAd.replaceDB(db),
User: q.User.replaceDB(db),
UserPreference: q.UserPreference.replaceDB(db),
Video: q.Video.replaceDB(db),
@@ -137,6 +143,7 @@ type queryCtx struct {
Plan IPlanDo
PlanSubscription IPlanSubscriptionDo
PlayerConfig IPlayerConfigDo
PopupAd IPopupAdDo
User IUserDo
UserPreference IUserPreferenceDo
Video IVideoDo
@@ -153,6 +160,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
Plan: q.Plan.WithContext(ctx),
PlanSubscription: q.PlanSubscription.WithContext(ctx),
PlayerConfig: q.PlayerConfig.WithContext(ctx),
PopupAd: q.PopupAd.WithContext(ctx),
User: q.User.WithContext(ctx),
UserPreference: q.UserPreference.WithContext(ctx),
Video: q.Video.WithContext(ctx),

View File

@@ -0,0 +1,427 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package query
import (
"context"
"database/sql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"stream.api/internal/database/model"
)
func newPopupAd(db *gorm.DB, opts ...gen.DOOption) popupAd {
_popupAd := popupAd{}
_popupAd.popupAdDo.UseDB(db, opts...)
_popupAd.popupAdDo.UseModel(&model.PopupAd{})
tableName := _popupAd.popupAdDo.TableName()
_popupAd.ALL = field.NewAsterisk(tableName)
_popupAd.ID = field.NewString(tableName, "id")
_popupAd.UserID = field.NewString(tableName, "user_id")
_popupAd.Type = field.NewString(tableName, "type")
_popupAd.Label = field.NewString(tableName, "label")
_popupAd.Value = field.NewString(tableName, "value")
_popupAd.IsActive = field.NewBool(tableName, "is_active")
_popupAd.MaxTriggersPerSession = field.NewInt32(tableName, "max_triggers_per_session")
_popupAd.CreatedAt = field.NewTime(tableName, "created_at")
_popupAd.UpdatedAt = field.NewTime(tableName, "updated_at")
_popupAd.Version = field.NewInt64(tableName, "version")
_popupAd.fillFieldMap()
return _popupAd
}
type popupAd struct {
popupAdDo popupAdDo
ALL field.Asterisk
ID field.String
UserID field.String
Type field.String
Label field.String
Value field.String
IsActive field.Bool
MaxTriggersPerSession field.Int32
CreatedAt field.Time
UpdatedAt field.Time
Version field.Int64
fieldMap map[string]field.Expr
}
func (p popupAd) Table(newTableName string) *popupAd {
p.popupAdDo.UseTable(newTableName)
return p.updateTableName(newTableName)
}
func (p popupAd) As(alias string) *popupAd {
p.popupAdDo.DO = *(p.popupAdDo.As(alias).(*gen.DO))
return p.updateTableName(alias)
}
func (p *popupAd) updateTableName(table string) *popupAd {
p.ALL = field.NewAsterisk(table)
p.ID = field.NewString(table, "id")
p.UserID = field.NewString(table, "user_id")
p.Type = field.NewString(table, "type")
p.Label = field.NewString(table, "label")
p.Value = field.NewString(table, "value")
p.IsActive = field.NewBool(table, "is_active")
p.MaxTriggersPerSession = field.NewInt32(table, "max_triggers_per_session")
p.CreatedAt = field.NewTime(table, "created_at")
p.UpdatedAt = field.NewTime(table, "updated_at")
p.Version = field.NewInt64(table, "version")
p.fillFieldMap()
return p
}
func (p *popupAd) WithContext(ctx context.Context) IPopupAdDo { return p.popupAdDo.WithContext(ctx) }
func (p popupAd) TableName() string { return p.popupAdDo.TableName() }
func (p popupAd) Alias() string { return p.popupAdDo.Alias() }
func (p popupAd) Columns(cols ...field.Expr) gen.Columns { return p.popupAdDo.Columns(cols...) }
func (p *popupAd) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := p.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (p *popupAd) fillFieldMap() {
p.fieldMap = make(map[string]field.Expr, 10)
p.fieldMap["id"] = p.ID
p.fieldMap["user_id"] = p.UserID
p.fieldMap["type"] = p.Type
p.fieldMap["label"] = p.Label
p.fieldMap["value"] = p.Value
p.fieldMap["is_active"] = p.IsActive
p.fieldMap["max_triggers_per_session"] = p.MaxTriggersPerSession
p.fieldMap["created_at"] = p.CreatedAt
p.fieldMap["updated_at"] = p.UpdatedAt
p.fieldMap["version"] = p.Version
}
func (p popupAd) clone(db *gorm.DB) popupAd {
p.popupAdDo.ReplaceConnPool(db.Statement.ConnPool)
return p
}
func (p popupAd) replaceDB(db *gorm.DB) popupAd {
p.popupAdDo.ReplaceDB(db)
return p
}
type popupAdDo struct{ gen.DO }
type IPopupAdDo interface {
gen.SubQuery
Debug() IPopupAdDo
WithContext(ctx context.Context) IPopupAdDo
WithResult(fc func(tx gen.Dao)) gen.ResultInfo
ReplaceDB(db *gorm.DB)
ReadDB() IPopupAdDo
WriteDB() IPopupAdDo
As(alias string) gen.Dao
Session(config *gorm.Session) IPopupAdDo
Columns(cols ...field.Expr) gen.Columns
Clauses(conds ...clause.Expression) IPopupAdDo
Not(conds ...gen.Condition) IPopupAdDo
Or(conds ...gen.Condition) IPopupAdDo
Select(conds ...field.Expr) IPopupAdDo
Where(conds ...gen.Condition) IPopupAdDo
Order(conds ...field.Expr) IPopupAdDo
Distinct(cols ...field.Expr) IPopupAdDo
Omit(cols ...field.Expr) IPopupAdDo
Join(table schema.Tabler, on ...field.Expr) IPopupAdDo
LeftJoin(table schema.Tabler, on ...field.Expr) IPopupAdDo
RightJoin(table schema.Tabler, on ...field.Expr) IPopupAdDo
Group(cols ...field.Expr) IPopupAdDo
Having(conds ...gen.Condition) IPopupAdDo
Limit(limit int) IPopupAdDo
Offset(offset int) IPopupAdDo
Count() (count int64, err error)
Scopes(funcs ...func(gen.Dao) gen.Dao) IPopupAdDo
Unscoped() IPopupAdDo
Create(values ...*model.PopupAd) error
CreateInBatches(values []*model.PopupAd, batchSize int) error
Save(values ...*model.PopupAd) error
First() (*model.PopupAd, error)
Take() (*model.PopupAd, error)
Last() (*model.PopupAd, error)
Find() ([]*model.PopupAd, error)
FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.PopupAd, err error)
FindInBatches(result *[]*model.PopupAd, batchSize int, fc func(tx gen.Dao, batch int) error) error
Pluck(column field.Expr, dest interface{}) error
Delete(...*model.PopupAd) (info gen.ResultInfo, err error)
Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
Updates(value interface{}) (info gen.ResultInfo, err error)
UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
UpdateColumns(value interface{}) (info gen.ResultInfo, err error)
UpdateFrom(q gen.SubQuery) gen.Dao
Attrs(attrs ...field.AssignExpr) IPopupAdDo
Assign(attrs ...field.AssignExpr) IPopupAdDo
Joins(fields ...field.RelationField) IPopupAdDo
Preload(fields ...field.RelationField) IPopupAdDo
FirstOrInit() (*model.PopupAd, error)
FirstOrCreate() (*model.PopupAd, error)
FindByPage(offset int, limit int) (result []*model.PopupAd, count int64, err error)
ScanByPage(result interface{}, offset int, limit int) (count int64, err error)
Rows() (*sql.Rows, error)
Row() *sql.Row
Scan(result interface{}) (err error)
Returning(value interface{}, columns ...string) IPopupAdDo
UnderlyingDB() *gorm.DB
schema.Tabler
}
func (p popupAdDo) Debug() IPopupAdDo {
return p.withDO(p.DO.Debug())
}
func (p popupAdDo) WithContext(ctx context.Context) IPopupAdDo {
return p.withDO(p.DO.WithContext(ctx))
}
func (p popupAdDo) ReadDB() IPopupAdDo {
return p.Clauses(dbresolver.Read)
}
func (p popupAdDo) WriteDB() IPopupAdDo {
return p.Clauses(dbresolver.Write)
}
func (p popupAdDo) Session(config *gorm.Session) IPopupAdDo {
return p.withDO(p.DO.Session(config))
}
func (p popupAdDo) Clauses(conds ...clause.Expression) IPopupAdDo {
return p.withDO(p.DO.Clauses(conds...))
}
func (p popupAdDo) Returning(value interface{}, columns ...string) IPopupAdDo {
return p.withDO(p.DO.Returning(value, columns...))
}
func (p popupAdDo) Not(conds ...gen.Condition) IPopupAdDo {
return p.withDO(p.DO.Not(conds...))
}
func (p popupAdDo) Or(conds ...gen.Condition) IPopupAdDo {
return p.withDO(p.DO.Or(conds...))
}
func (p popupAdDo) Select(conds ...field.Expr) IPopupAdDo {
return p.withDO(p.DO.Select(conds...))
}
func (p popupAdDo) Where(conds ...gen.Condition) IPopupAdDo {
return p.withDO(p.DO.Where(conds...))
}
func (p popupAdDo) Order(conds ...field.Expr) IPopupAdDo {
return p.withDO(p.DO.Order(conds...))
}
func (p popupAdDo) Distinct(cols ...field.Expr) IPopupAdDo {
return p.withDO(p.DO.Distinct(cols...))
}
func (p popupAdDo) Omit(cols ...field.Expr) IPopupAdDo {
return p.withDO(p.DO.Omit(cols...))
}
func (p popupAdDo) Join(table schema.Tabler, on ...field.Expr) IPopupAdDo {
return p.withDO(p.DO.Join(table, on...))
}
func (p popupAdDo) LeftJoin(table schema.Tabler, on ...field.Expr) IPopupAdDo {
return p.withDO(p.DO.LeftJoin(table, on...))
}
func (p popupAdDo) RightJoin(table schema.Tabler, on ...field.Expr) IPopupAdDo {
return p.withDO(p.DO.RightJoin(table, on...))
}
func (p popupAdDo) Group(cols ...field.Expr) IPopupAdDo {
return p.withDO(p.DO.Group(cols...))
}
func (p popupAdDo) Having(conds ...gen.Condition) IPopupAdDo {
return p.withDO(p.DO.Having(conds...))
}
func (p popupAdDo) Limit(limit int) IPopupAdDo {
return p.withDO(p.DO.Limit(limit))
}
func (p popupAdDo) Offset(offset int) IPopupAdDo {
return p.withDO(p.DO.Offset(offset))
}
func (p popupAdDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IPopupAdDo {
return p.withDO(p.DO.Scopes(funcs...))
}
func (p popupAdDo) Unscoped() IPopupAdDo {
return p.withDO(p.DO.Unscoped())
}
func (p popupAdDo) Create(values ...*model.PopupAd) error {
if len(values) == 0 {
return nil
}
return p.DO.Create(values)
}
func (p popupAdDo) CreateInBatches(values []*model.PopupAd, batchSize int) error {
return p.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (p popupAdDo) Save(values ...*model.PopupAd) error {
if len(values) == 0 {
return nil
}
return p.DO.Save(values)
}
func (p popupAdDo) First() (*model.PopupAd, error) {
if result, err := p.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.PopupAd), nil
}
}
func (p popupAdDo) Take() (*model.PopupAd, error) {
if result, err := p.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.PopupAd), nil
}
}
func (p popupAdDo) Last() (*model.PopupAd, error) {
if result, err := p.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.PopupAd), nil
}
}
func (p popupAdDo) Find() ([]*model.PopupAd, error) {
result, err := p.DO.Find()
return result.([]*model.PopupAd), err
}
func (p popupAdDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.PopupAd, err error) {
buf := make([]*model.PopupAd, 0, batchSize)
err = p.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (p popupAdDo) FindInBatches(result *[]*model.PopupAd, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return p.DO.FindInBatches(result, batchSize, fc)
}
func (p popupAdDo) Attrs(attrs ...field.AssignExpr) IPopupAdDo {
return p.withDO(p.DO.Attrs(attrs...))
}
func (p popupAdDo) Assign(attrs ...field.AssignExpr) IPopupAdDo {
return p.withDO(p.DO.Assign(attrs...))
}
func (p popupAdDo) Joins(fields ...field.RelationField) IPopupAdDo {
for _, _f := range fields {
p = *p.withDO(p.DO.Joins(_f))
}
return &p
}
func (p popupAdDo) Preload(fields ...field.RelationField) IPopupAdDo {
for _, _f := range fields {
p = *p.withDO(p.DO.Preload(_f))
}
return &p
}
func (p popupAdDo) FirstOrInit() (*model.PopupAd, error) {
if result, err := p.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.PopupAd), nil
}
}
func (p popupAdDo) FirstOrCreate() (*model.PopupAd, error) {
if result, err := p.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.PopupAd), nil
}
}
func (p popupAdDo) FindByPage(offset int, limit int) (result []*model.PopupAd, count int64, err error) {
result, err = p.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = p.Offset(-1).Limit(-1).Count()
return
}
func (p popupAdDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = p.Count()
if err != nil {
return
}
err = p.Offset(offset).Limit(limit).Scan(result)
return
}
func (p popupAdDo) Scan(result interface{}) (err error) {
return p.DO.Scan(result)
}
func (p popupAdDo) Delete(models ...*model.PopupAd) (result gen.ResultInfo, err error) {
return p.DO.Delete(models)
}
func (p *popupAdDo) withDO(do gen.Dao) *popupAdDo {
p.DO = *do.(*gen.DO)
return p
}