mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 14:26:36 +08:00
365 lines
11 KiB
Go
365 lines
11 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package repository
|
|
|
|
import (
|
|
"Wavelet/core"
|
|
"Wavelet/core/contracts"
|
|
"Wavelet/plugins/domain/message_gateway/errs"
|
|
"Wavelet/plugins/domain/message_gateway/model"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
activePushChannelCacheTTL = 24 * time.Hour
|
|
activePushEventCacheTTL = 24 * time.Hour
|
|
)
|
|
|
|
var (
|
|
cacheMu sync.RWMutex
|
|
cacheSvc contracts.CacheService
|
|
)
|
|
|
|
// SetCacheService sets the cache service singleton.
|
|
func SetCacheService(s contracts.CacheService) {
|
|
cacheMu.Lock()
|
|
defer cacheMu.Unlock()
|
|
cacheSvc = s
|
|
}
|
|
|
|
// GetCache resolves the cache service for the current call.
|
|
func GetCache(ctx context.Context) contracts.CacheService {
|
|
if c, ok := ctx.(*core.Context); ok && c != nil {
|
|
if s, err := core.Inject[contracts.CacheService](c); err == nil && s != nil {
|
|
return s
|
|
}
|
|
}
|
|
cacheMu.RLock()
|
|
s := cacheSvc
|
|
cacheMu.RUnlock()
|
|
return s
|
|
}
|
|
|
|
// ListPushChannelsRecord returns all push channels ordered by creation time descending.
|
|
func ListPushChannelsRecord(ctx context.Context) ([]model.PushChannel, error) {
|
|
var channels []model.PushChannel
|
|
if err := GetDB(ctx).Order("created_at DESC").Find(&channels).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return channels, nil
|
|
}
|
|
|
|
// GetPushChannelByIDRecord loads a push channel by primary key.
|
|
func GetPushChannelByIDRecord(ctx context.Context, id uint64) (model.PushChannel, error) {
|
|
var channel model.PushChannel
|
|
if err := GetDB(ctx).Where("id = ?", id).First(&channel).Error; err != nil {
|
|
return model.PushChannel{}, mapNotFound(err)
|
|
}
|
|
return channel, nil
|
|
}
|
|
|
|
// GetPushChannelByNameRecord loads a push channel by its unique name.
|
|
func GetPushChannelByNameRecord(ctx context.Context, name string) (*model.PushChannel, error) {
|
|
var channel model.PushChannel
|
|
if err := GetDB(ctx).Where("name = ?", name).First(&channel).Error; err != nil {
|
|
return nil, mapNotFound(err)
|
|
}
|
|
return &channel, nil
|
|
}
|
|
|
|
// CountPushChannelsByNameRecord returns how many channels share the given name.
|
|
func CountPushChannelsByNameRecord(ctx context.Context, name string) (int64, error) {
|
|
var count int64
|
|
if err := GetDB(ctx).Model(&model.PushChannel{}).Where("name = ?", name).Count(&count).Error; err != nil {
|
|
return 0, err
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
// CreatePushChannelRecord persists a new channel and invalidates cache.
|
|
func CreatePushChannelRecord(ctx context.Context, channel *model.PushChannel) error {
|
|
if err := GetDB(ctx).Create(channel).Error; err != nil {
|
|
return err
|
|
}
|
|
DeleteActivePushChannelCache(ctx, channel.Name)
|
|
return nil
|
|
}
|
|
|
|
// SavePushChannelRecord updates a channel and invalidates cache.
|
|
func SavePushChannelRecord(ctx context.Context, channel *model.PushChannel) error {
|
|
if err := GetDB(ctx).Save(channel).Error; err != nil {
|
|
return err
|
|
}
|
|
DeleteActivePushChannelCache(ctx, channel.Name)
|
|
return nil
|
|
}
|
|
|
|
// DeletePushChannelRecord removes a channel and invalidates cache.
|
|
func DeletePushChannelRecord(ctx context.Context, channel *model.PushChannel) error {
|
|
if err := GetDB(ctx).Delete(channel).Error; err != nil {
|
|
return err
|
|
}
|
|
DeleteActivePushChannelCache(ctx, channel.Name)
|
|
return nil
|
|
}
|
|
|
|
func getCachedOrQuery[T any](ctx context.Context, cacheKey string, ttl time.Duration, query func(db *gorm.DB, dest *T) error) (*T, error) {
|
|
var val T
|
|
if cache := GetCache(ctx); cache != nil {
|
|
if err := cache.Get(ctx, cacheKey, &val); err == nil {
|
|
return &val, nil
|
|
}
|
|
}
|
|
|
|
db := GetDB(ctx)
|
|
if err := query(db, &val); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if cache := GetCache(ctx); cache != nil {
|
|
_ = cache.Set(ctx, cacheKey, val, ttl)
|
|
}
|
|
|
|
return &val, nil
|
|
}
|
|
|
|
// GetActivePushChannelByName loads an enabled push channel, preferring the cache layer.
|
|
func GetActivePushChannelByName(ctx context.Context, name string) (*model.PushChannel, error) {
|
|
channel, err := getCachedOrQuery(ctx, "push:channel:active:"+name, activePushChannelCacheTTL, func(db *gorm.DB, dest *model.PushChannel) error {
|
|
return db.Where("name = ? AND enabled = ?", name, true).First(dest).Error
|
|
})
|
|
if err != nil {
|
|
return nil, mapNotFound(err)
|
|
}
|
|
return channel, nil
|
|
}
|
|
|
|
// DeleteActivePushChannelCache drops the cached enabled-channel entry.
|
|
func DeleteActivePushChannelCache(ctx context.Context, name string) {
|
|
if cache := GetCache(ctx); cache != nil {
|
|
_ = cache.Delete(ctx, "push:channel:active:"+name)
|
|
}
|
|
}
|
|
|
|
// ListPushEventsRecord returns all push events ordered by creation time descending.
|
|
func ListPushEventsRecord(ctx context.Context) ([]model.PushEvent, error) {
|
|
var events []model.PushEvent
|
|
if err := GetDB(ctx).Order("created_at DESC").Find(&events).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return events, nil
|
|
}
|
|
|
|
// GetPushEventByIDRecord loads a push event by primary key.
|
|
func GetPushEventByIDRecord(ctx context.Context, id uint64) (model.PushEvent, error) {
|
|
var event model.PushEvent
|
|
if err := GetDB(ctx).First(&event, id).Error; err != nil {
|
|
return model.PushEvent{}, mapNotFound(err)
|
|
}
|
|
return event, nil
|
|
}
|
|
|
|
// GetPushEventByKeyRecord loads a push event by event key.
|
|
func GetPushEventByKeyRecord(ctx context.Context, key string) (model.PushEvent, error) {
|
|
var event model.PushEvent
|
|
if err := GetDB(ctx).Where("event_key = ?", key).First(&event).Error; err != nil {
|
|
return model.PushEvent{}, mapNotFound(err)
|
|
}
|
|
return event, nil
|
|
}
|
|
|
|
// CountPushEventsByKeyRecord returns how many events use the given event key.
|
|
func CountPushEventsByKeyRecord(ctx context.Context, key string) (int64, error) {
|
|
var count int64
|
|
if err := GetDB(ctx).Model(&model.PushEvent{}).Where("event_key = ?", key).Count(&count).Error; err != nil {
|
|
return 0, err
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
// CreatePushEventRecord persists a new push event and invalidates cache.
|
|
func CreatePushEventRecord(ctx context.Context, event *model.PushEvent) error {
|
|
if err := GetDB(ctx).Create(event).Error; err != nil {
|
|
return err
|
|
}
|
|
DeleteActivePushEventCache(ctx, event.EventKey)
|
|
return nil
|
|
}
|
|
|
|
// SavePushEventRecord updates a push event and invalidates cache.
|
|
func SavePushEventRecord(ctx context.Context, event *model.PushEvent) error {
|
|
if err := GetDB(ctx).Save(event).Error; err != nil {
|
|
return err
|
|
}
|
|
DeleteActivePushEventCache(ctx, event.EventKey)
|
|
return nil
|
|
}
|
|
|
|
// UpdatePushEventEnabledRecord toggles the enabled flag for a push event.
|
|
func UpdatePushEventEnabledRecord(ctx context.Context, event *model.PushEvent, enabled bool) error {
|
|
event.Enabled = enabled
|
|
if err := GetDB(ctx).Model(event).Update("enabled", enabled).Error; err != nil {
|
|
return err
|
|
}
|
|
DeleteActivePushEventCache(ctx, event.EventKey)
|
|
return nil
|
|
}
|
|
|
|
// DeletePushEventRecord removes a push event and invalidates cache.
|
|
func DeletePushEventRecord(ctx context.Context, event *model.PushEvent) error {
|
|
if err := GetDB(ctx).Delete(event).Error; err != nil {
|
|
return err
|
|
}
|
|
DeleteActivePushEventCache(ctx, event.EventKey)
|
|
return nil
|
|
}
|
|
|
|
// ListActivePushEventsByTaskTypeRecord returns enabled events bound to a task type.
|
|
func ListActivePushEventsByTaskTypeRecord(ctx context.Context, taskType string) ([]model.PushEvent, error) {
|
|
var events []model.PushEvent
|
|
if err := GetDB(ctx).Where("task_type = ? AND enabled = ?", taskType, true).Find(&events).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return events, nil
|
|
}
|
|
|
|
// GetActivePushEventByKey loads an enabled push event, preferring the cache layer.
|
|
func GetActivePushEventByKey(ctx context.Context, key string) (*model.PushEvent, error) {
|
|
event, err := getCachedOrQuery(ctx, "push:event:active:"+key, activePushEventCacheTTL, func(db *gorm.DB, dest *model.PushEvent) error {
|
|
return db.Where("event_key = ? AND enabled = ?", key, true).First(dest).Error
|
|
})
|
|
if err != nil {
|
|
return nil, mapNotFound(err)
|
|
}
|
|
return event, nil
|
|
}
|
|
|
|
// DeleteActivePushEventCache drops the cached enabled-event entry.
|
|
func DeleteActivePushEventCache(ctx context.Context, key string) {
|
|
if cache := GetCache(ctx); cache != nil {
|
|
_ = cache.Delete(ctx, "push:event:active:"+key)
|
|
}
|
|
}
|
|
|
|
// ListPushHistoriesRecord returns paginated push history records.
|
|
func ListPushHistoriesRecord(ctx context.Context, filter model.PushHistoryListFilter) (int64, []model.PushHistory, error) {
|
|
query := GetDB(ctx).Model(&model.PushHistory{}).Order("created_at DESC")
|
|
if filter.EventKey != "" {
|
|
query = query.Where("event_key = ?", filter.EventKey)
|
|
}
|
|
if filter.Status != "" {
|
|
query = query.Where("status = ?", filter.Status)
|
|
}
|
|
|
|
var total int64
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
var results []model.PushHistory
|
|
offset := (filter.Page - 1) * filter.PageSize
|
|
if err := query.Offset(offset).Limit(filter.PageSize).Find(&results).Error; err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
return total, results, nil
|
|
}
|
|
|
|
// CreatePushHistoryRecord persists a push history audit record.
|
|
func CreatePushHistoryRecord(ctx context.Context, history *model.PushHistory) error {
|
|
return GetDB(ctx).Create(history).Error
|
|
}
|
|
|
|
// PushHistoryQuery returns a scoped query builder for push histories.
|
|
func PushHistoryQuery(ctx context.Context) *gorm.DB {
|
|
return GetDB(ctx).Model(&model.PushHistory{})
|
|
}
|
|
|
|
// smtpConfigKeys are the system-config rows backing the built-in email channel.
|
|
var smtpConfigKeys = []string{"smtp_host", "smtp_port", "smtp_username", "smtp_password"}
|
|
|
|
// LoadSMTPConfigRecord reads the SMTP settings in one query.
|
|
//
|
|
// A key that is simply absent leaves its field empty, which is how an unconfigured
|
|
// mailer is represented. A read that fails is returned as an error, so callers
|
|
// cannot mistake an unhealthy database for "no SMTP configured" and silently drop
|
|
// the notification.
|
|
func LoadSMTPConfigRecord(ctx context.Context) (model.SMTPConfig, error) {
|
|
db := GetDB(ctx)
|
|
if db == nil {
|
|
return model.SMTPConfig{}, errors.New("database not available")
|
|
}
|
|
|
|
var rows []struct {
|
|
Key string
|
|
Value string
|
|
}
|
|
if err := db.Table("w_system_configs").
|
|
Select("key", "value").
|
|
Where("key IN ?", smtpConfigKeys).
|
|
Find(&rows).Error; err != nil {
|
|
return model.SMTPConfig{}, fmt.Errorf("read smtp system configs: %w", err)
|
|
}
|
|
|
|
var cfg model.SMTPConfig
|
|
for _, row := range rows {
|
|
switch row.Key {
|
|
case "smtp_host":
|
|
cfg.Host = row.Value
|
|
case "smtp_port":
|
|
cfg.Port = row.Value
|
|
case "smtp_username":
|
|
cfg.Username = row.Value
|
|
case "smtp_password":
|
|
cfg.Password = row.Value
|
|
}
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
// userLookupColumns allow-lists the columns FindUserByFieldRecord may filter on.
|
|
// The column name is concatenated into the WHERE clause, so anything not listed
|
|
// here must never reach the database.
|
|
var userLookupColumns = map[string]struct{}{
|
|
"id": {},
|
|
"username": {},
|
|
}
|
|
|
|
// FindUserByFieldRecord is the user lookup fallback for when the UserService
|
|
// contract is not wired yet. field must be one of userLookupColumns.
|
|
func FindUserByFieldRecord(ctx context.Context, field string, value any) (*contracts.UserDTO, error) {
|
|
if _, ok := userLookupColumns[field]; !ok {
|
|
return nil, errs.ErrUnsupportedUserLookupField
|
|
}
|
|
db := GetDB(ctx)
|
|
if db == nil {
|
|
return nil, errs.ErrRecordNotFound
|
|
}
|
|
var user contracts.UserDTO
|
|
if err := db.Table("w_users").Where(field+" = ?", value).First(&user).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
// FindFirstAdminUserRecord is the admin lookup fallback for when the UserService
|
|
// contract is not wired yet.
|
|
func FindFirstAdminUserRecord(ctx context.Context) (*contracts.UserDTO, error) {
|
|
db := GetDB(ctx)
|
|
if db == nil {
|
|
return nil, errs.ErrRecordNotFound
|
|
}
|
|
var adminUser contracts.UserDTO
|
|
if err := db.Table("w_users").Where("is_admin = ?", true).Order("id ASC").First(&adminUser).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &adminUser, nil
|
|
}
|