mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-01 06:36:38 +08:00
4f50f6a8f9
Wire plugin services through Bind/InjectFrom and AppContext so HTTP and workers resolve dependencies after Apply. Register TaskHandler objects with persisted results, and implement send_email_code, mail:send, cleanup_inactive_users, and dispatch_bot_msg.
429 lines
11 KiB
Go
429 lines
11 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Package repository provides persistence operations for the admin domain.
|
|
package repository
|
|
|
|
import (
|
|
"Wavelet/core"
|
|
"Wavelet/core/contracts"
|
|
"Wavelet/pkg/cache/ram"
|
|
"Wavelet/pkg/logger"
|
|
"Wavelet/plugins/domain/admin/errs"
|
|
"Wavelet/plugins/domain/admin/model"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
configTypeSystem = "system"
|
|
)
|
|
|
|
var (
|
|
repoMu sync.RWMutex
|
|
dbService contracts.DBService
|
|
cacheService contracts.CacheService
|
|
)
|
|
|
|
// SetDBService injects the DBService contract.
|
|
func SetDBService(s contracts.DBService) {
|
|
repoMu.Lock()
|
|
defer repoMu.Unlock()
|
|
dbService = s
|
|
}
|
|
|
|
// SetCacheService injects the CacheService contract.
|
|
func SetCacheService(s contracts.CacheService) {
|
|
repoMu.Lock()
|
|
defer repoMu.Unlock()
|
|
cacheService = s
|
|
}
|
|
|
|
// ResetServices clears injected persistence services.
|
|
func ResetServices() {
|
|
repoMu.Lock()
|
|
defer repoMu.Unlock()
|
|
dbService = nil
|
|
cacheService = nil
|
|
}
|
|
|
|
// GetDB returns the GORM DB instance bound to the context if available.
|
|
func GetDB(ctx context.Context) *gorm.DB {
|
|
if s, err := core.InjectFrom[contracts.DBService](ctx); err == nil && s != nil {
|
|
return s.DB(ctx)
|
|
}
|
|
repoMu.RLock()
|
|
defer repoMu.RUnlock()
|
|
if dbService == nil {
|
|
return nil
|
|
}
|
|
return dbService.DB(ctx)
|
|
}
|
|
|
|
// GetCache returns the unified CacheService instance.
|
|
func GetCache(ctx context.Context) contracts.CacheService {
|
|
if s, err := core.InjectFrom[contracts.CacheService](ctx); err == nil && s != nil {
|
|
return s
|
|
}
|
|
repoMu.RLock()
|
|
defer repoMu.RUnlock()
|
|
return cacheService
|
|
}
|
|
|
|
// PreheatSystemConfigs loads all system configs from database.
|
|
func PreheatSystemConfigs(ctx context.Context) ([]model.SystemConfig, error) {
|
|
database := GetDB(ctx)
|
|
if database == nil {
|
|
return nil, errors.New(errs.ErrDatabaseNotInitialized)
|
|
}
|
|
|
|
var configs []model.SystemConfig
|
|
if err := database.Find(&configs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return configs, nil
|
|
}
|
|
|
|
// PreheatSystemConfigByKey loads a single config key from database.
|
|
func PreheatSystemConfigByKey(ctx context.Context, key string) (model.SystemConfig, error) {
|
|
database := GetDB(ctx)
|
|
if database == nil {
|
|
return model.SystemConfig{}, errors.New(errs.ErrDatabaseNotInitialized)
|
|
}
|
|
|
|
var sc model.SystemConfig
|
|
if err := database.Where("key = ?", key).First(&sc).Error; err != nil {
|
|
return model.SystemConfig{}, err
|
|
}
|
|
return sc, nil
|
|
}
|
|
|
|
// GetSystemConfigByGroup queries a configuration by Type and Key.
|
|
func GetSystemConfigByGroup(ctx context.Context, configType, key string) (model.SystemConfig, error) {
|
|
ensureSystemConfigCacheListener()
|
|
|
|
if item, ok := ram.Get(configType, key); ok {
|
|
var sc model.SystemConfig
|
|
if err := json.Unmarshal([]byte(item.Value), &sc); err == nil {
|
|
return sc, nil
|
|
}
|
|
}
|
|
|
|
database := GetDB(ctx)
|
|
if database == nil {
|
|
return model.SystemConfig{}, errors.New(errs.ErrDatabaseNotInitialized)
|
|
}
|
|
|
|
var sc model.SystemConfig
|
|
if err := database.Where("key = ?", key).First(&sc).Error; err != nil {
|
|
return model.SystemConfig{}, err
|
|
}
|
|
|
|
valBytes, err := json.Marshal(sc)
|
|
if err == nil {
|
|
ram.Set(ram.CacheItem{
|
|
Key: sc.Key,
|
|
Value: string(valBytes),
|
|
Type: configType,
|
|
TTL: determineTTL(sc.Key),
|
|
})
|
|
}
|
|
|
|
return sc, nil
|
|
}
|
|
|
|
// GetSystemConfigByKey queries config by key.
|
|
func GetSystemConfigByKey(ctx context.Context, key string) (model.SystemConfig, error) {
|
|
return GetSystemConfigByGroup(ctx, ConfigCacheType, key)
|
|
}
|
|
|
|
// ListSystemConfigsByKeys loads multiple config keys.
|
|
func ListSystemConfigsByKeys(ctx context.Context, keys []string) (map[string]model.SystemConfig, error) {
|
|
if len(keys) == 0 {
|
|
return map[string]model.SystemConfig{}, nil
|
|
}
|
|
|
|
ensureSystemConfigCacheListener()
|
|
|
|
result := make(map[string]model.SystemConfig, len(keys))
|
|
missing := make([]string, 0, len(keys))
|
|
|
|
for _, key := range keys {
|
|
if item, ok := ram.Get(ConfigCacheType, key); ok {
|
|
var sc model.SystemConfig
|
|
if err := json.Unmarshal([]byte(item.Value), &sc); err == nil {
|
|
result[key] = sc
|
|
continue
|
|
}
|
|
}
|
|
missing = append(missing, key)
|
|
}
|
|
|
|
if len(missing) == 0 {
|
|
return result, nil
|
|
}
|
|
|
|
database := GetDB(ctx)
|
|
if database == nil {
|
|
return nil, errors.New(errs.ErrDatabaseNotInitialized)
|
|
}
|
|
|
|
var configs []model.SystemConfig
|
|
if err := database.Where("key IN ?", missing).Find(&configs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := range configs {
|
|
valBytes, err := json.Marshal(configs[i])
|
|
if err == nil {
|
|
ram.Set(ram.CacheItem{
|
|
Key: configs[i].Key,
|
|
Value: string(valBytes),
|
|
Type: ConfigCacheType,
|
|
TTL: determineTTL(configs[i].Key),
|
|
})
|
|
}
|
|
result[configs[i].Key] = configs[i]
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// InvalidateVisibleSystemConfigsCache clears the cached public config list.
|
|
func InvalidateVisibleSystemConfigsCache(ctx context.Context) error {
|
|
return InvalidateAllSystemConfigCaches(ctx)
|
|
}
|
|
|
|
// ListVisibleSystemConfigs queries visible configs using local cache store.
|
|
func ListVisibleSystemConfigs(ctx context.Context) ([]model.SystemConfig, error) {
|
|
ensureSystemConfigCacheListener()
|
|
|
|
items := ram.GetTypeItems(ConfigCacheType)
|
|
if len(items) > 0 {
|
|
var list []model.SystemConfig
|
|
for _, item := range items {
|
|
var sc model.SystemConfig
|
|
if err := json.Unmarshal([]byte(item.Value), &sc); err == nil {
|
|
if sc.Visibility == model.ConfigVisibilityVisible {
|
|
list = append(list, sc)
|
|
}
|
|
}
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
database := GetDB(ctx)
|
|
if database == nil {
|
|
return nil, errors.New(errs.ErrDatabaseNotInitialized)
|
|
}
|
|
|
|
var configs []model.SystemConfig
|
|
if err := database.Where("visibility = ?", model.ConfigVisibilityVisible).Find(&configs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, cfg := range configs {
|
|
valBytes, err := json.Marshal(cfg)
|
|
if err == nil {
|
|
ram.Set(ram.CacheItem{
|
|
Key: cfg.Key,
|
|
Value: string(valBytes),
|
|
Type: ConfigCacheType,
|
|
TTL: determineTTL(cfg.Key),
|
|
})
|
|
}
|
|
}
|
|
|
|
return configs, nil
|
|
}
|
|
|
|
// GetIntByKey queries config and converts to int.
|
|
func GetIntByKey(ctx context.Context, key string) (int, error) {
|
|
sc, err := GetSystemConfigByKey(ctx, key)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
value, err := strconv.Atoi(sc.Value)
|
|
if err != nil {
|
|
return 0, fmt.Errorf(errs.ErrConfigIntParseFailed, key, sc.Value, err)
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
// GetDecimalByKey queries config and converts to decimal.Decimal.
|
|
func GetDecimalByKey(ctx context.Context, key string, precision int32) (decimal.Decimal, error) {
|
|
sc, err := GetSystemConfigByKey(ctx, key)
|
|
if err != nil {
|
|
return decimal.Zero, err
|
|
}
|
|
|
|
value, err := decimal.NewFromString(sc.Value)
|
|
if err != nil {
|
|
return decimal.Zero, fmt.Errorf(errs.ErrConfigDecimalParseFailed, key, sc.Value, err)
|
|
}
|
|
|
|
return value.Truncate(precision), nil
|
|
}
|
|
|
|
// GetBoolByKey queries config and converts to bool.
|
|
func GetBoolByKey(ctx context.Context, key string) (bool, error) {
|
|
sc, err := GetSystemConfigByKey(ctx, key)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
value, err := strconv.ParseBool(sc.Value)
|
|
if err != nil {
|
|
return false, fmt.Errorf(errs.ErrConfigBoolParseFailed, key, sc.Value, err)
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
// GetMenuDisplayConfig queries and parses menu config.
|
|
func GetMenuDisplayConfig(ctx context.Context) (map[string]bool, error) {
|
|
sc, err := GetSystemConfigByKey(ctx, model.ConfigKeyMenuDisplayConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config := make(map[string]bool)
|
|
if sc.Value == "" || sc.Value == "{}" {
|
|
return config, nil
|
|
}
|
|
|
|
if err := json.Unmarshal([]byte(sc.Value), &config); err != nil {
|
|
return nil, fmt.Errorf(errs.ErrParseMenuDisplayConfigFailed, err)
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// ListAdminSystemConfigs returns all configs, optionally filtered by type.
|
|
func ListAdminSystemConfigs(ctx context.Context, configType string) ([]model.SystemConfig, error) {
|
|
query := GetDB(ctx).Order("created_at DESC")
|
|
if configType != "" {
|
|
query = query.Where("type = ?", configType)
|
|
}
|
|
var configs []model.SystemConfig
|
|
if err := query.Find(&configs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return configs, nil
|
|
}
|
|
|
|
// GetAdminSystemConfigByKey loads a config directly from DB.
|
|
func GetAdminSystemConfigByKey(ctx context.Context, key string) (model.SystemConfig, error) {
|
|
var config model.SystemConfig
|
|
if err := GetDB(ctx).Where("key = ?", key).First(&config).Error; err != nil {
|
|
return model.SystemConfig{}, err
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
// SystemConfigExists reports whether a config key already exists.
|
|
func SystemConfigExists(ctx context.Context, key string) (bool, error) {
|
|
var existing model.SystemConfig
|
|
err := GetDB(ctx).Where("key = ?", key).First(&existing).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// CreateSystemConfigRecord persists a new system config row.
|
|
func CreateSystemConfigRecord(ctx context.Context, config *model.SystemConfig) error {
|
|
return GetDB(ctx).Create(config).Error
|
|
}
|
|
|
|
// UpdateSystemConfigFields applies partial updates to a system config row.
|
|
func UpdateSystemConfigFields(ctx context.Context, config *model.SystemConfig, updates map[string]any) error {
|
|
return GetDB(ctx).Model(config).Updates(updates).Error
|
|
}
|
|
|
|
// UpdateSystemConfigTx applies the config row updates inside a transaction and, when
|
|
// resolveTaskType is not empty, marks that task type's failed executions as succeeded
|
|
// within the same transaction.
|
|
func UpdateSystemConfigTx(
|
|
ctx context.Context,
|
|
config *model.SystemConfig,
|
|
updates map[string]any,
|
|
resolveTaskType string,
|
|
resolveResult string,
|
|
) error {
|
|
database := GetDB(ctx)
|
|
if database == nil {
|
|
return errors.New(errs.ErrDatabaseServiceNotAvailable)
|
|
}
|
|
|
|
return database.Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Model(config).Updates(updates).Error; err != nil {
|
|
return err
|
|
}
|
|
if resolveTaskType == "" {
|
|
return nil
|
|
}
|
|
if err := MarkFailedTaskExecutionsSucceededTx(tx, resolveTaskType, resolveResult, time.Now()); err != nil {
|
|
logger.ErrorF(ctx, errs.ErrAutoResolveMigrationTaskFailed, err)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// SaveOrUpdateSystemConfig creates or updates a config row and invalidates cache.
|
|
func SaveOrUpdateSystemConfig(ctx context.Context, key, value string) error {
|
|
var sc model.SystemConfig
|
|
err := GetDB(ctx).Where("key = ?", key).First(&sc).Error
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
sc = model.SystemConfig{
|
|
Key: key,
|
|
Value: value,
|
|
Type: configTypeSystem,
|
|
Visibility: model.ConfigVisibilityHidden,
|
|
}
|
|
if err := GetDB(ctx).Create(&sc).Error; err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
sc.Value = value
|
|
if err := GetDB(ctx).Save(&sc).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return InvalidateSystemConfigCache(ctx, key)
|
|
}
|
|
|
|
// CountActiveUploads counts non-deleted rows of the storage upload table. A missing
|
|
// database handle yields zero, matching the pre-refactor guard behaviour.
|
|
func CountActiveUploads(ctx context.Context) (int64, error) {
|
|
gormDB := GetDB(ctx)
|
|
if gormDB == nil {
|
|
return 0, nil
|
|
}
|
|
|
|
var uploadCount int64
|
|
if err := gormDB.Table("w_uploads").
|
|
Where("status != ?", "deleted").
|
|
Count(&uploadCount).Error; err != nil {
|
|
return 0, fmt.Errorf(errs.ErrCheckExistingUploadsFailed, err)
|
|
}
|
|
return uploadCount, nil
|
|
}
|