mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 23:16:37 +08:00
wavelet init
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
)
|
||||
|
||||
const activePushChannelCacheTTL = 24 * time.Hour
|
||||
|
||||
// ListPushChannels returns all push channels ordered by creation time descending.
|
||||
func ListPushChannels(ctx context.Context) ([]model.PushChannel, error) {
|
||||
var channels []model.PushChannel
|
||||
if err := db.DB(ctx).Order("created_at DESC").Find(&channels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
// GetPushChannelByID loads a push channel by primary key.
|
||||
func GetPushChannelByID(ctx context.Context, id uint64) (model.PushChannel, error) {
|
||||
var channel model.PushChannel
|
||||
if err := db.DB(ctx).Where("id = ?", id).First(&channel).Error; err != nil {
|
||||
return model.PushChannel{}, err
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
// GetPushChannelByName 根据名称获取消息通道。
|
||||
func GetPushChannelByName(ctx context.Context, name string) (*model.PushChannel, error) {
|
||||
var channel model.PushChannel
|
||||
if err := db.DB(ctx).Where("name = ?", name).First(&channel).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
// CountPushChannelsByName returns how many channels share the given name.
|
||||
func CountPushChannelsByName(ctx context.Context, name string) (int64, error) {
|
||||
var count int64
|
||||
if err := db.DB(ctx).Model(&model.PushChannel{}).Where("name = ?", name).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CreatePushChannel persists a new channel and invalidates cache.
|
||||
func CreatePushChannel(ctx context.Context, channel *model.PushChannel) error {
|
||||
if err := db.DB(ctx).Create(channel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
DeleteActivePushChannelCache(ctx, channel.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SavePushChannel updates a channel and invalidates cache.
|
||||
func SavePushChannel(ctx context.Context, channel *model.PushChannel) error {
|
||||
if err := db.DB(ctx).Save(channel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
DeleteActivePushChannelCache(ctx, channel.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePushChannel removes a channel and invalidates cache.
|
||||
func DeletePushChannel(ctx context.Context, channel *model.PushChannel) error {
|
||||
if err := db.DB(ctx).Delete(channel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
DeleteActivePushChannelCache(ctx, channel.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActivePushChannelByName 根据名称获取启用的消息通道 (优先从 Redis 缓存获取)。
|
||||
func GetActivePushChannelByName(ctx context.Context, name string) (*model.PushChannel, error) {
|
||||
cacheKey := "push:channel:active:" + name
|
||||
var channel model.PushChannel
|
||||
if db.Redis != nil {
|
||||
if err := db.GetJSON(ctx, cacheKey, &channel); err == nil {
|
||||
return &channel, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Where("name = ? AND enabled = ?", name, true).First(&channel).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if db.Redis != nil {
|
||||
_ = db.SetJSON(ctx, cacheKey, channel, activePushChannelCacheTTL)
|
||||
}
|
||||
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
// DeleteActivePushChannelCache 清理启用消息通道的缓存。
|
||||
func DeleteActivePushChannelCache(ctx context.Context, name string) {
|
||||
if db.Redis != nil {
|
||||
_ = db.Redis.Del(ctx, db.PrefixedKey("push:channel:active:"+name)).Err()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
)
|
||||
|
||||
const activePushEventCacheTTL = 24 * time.Hour
|
||||
|
||||
// ListPushEvents returns all push events ordered by creation time descending.
|
||||
func ListPushEvents(ctx context.Context) ([]model.PushEvent, error) {
|
||||
var events []model.PushEvent
|
||||
if err := db.DB(ctx).Order("created_at DESC").Find(&events).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// GetPushEventByID loads a push event by primary key.
|
||||
func GetPushEventByID(ctx context.Context, id uint64) (model.PushEvent, error) {
|
||||
var event model.PushEvent
|
||||
if err := db.DB(ctx).First(&event, id).Error; err != nil {
|
||||
return model.PushEvent{}, err
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// GetPushEventByKey loads a push event by event key.
|
||||
func GetPushEventByKey(ctx context.Context, key string) (model.PushEvent, error) {
|
||||
var event model.PushEvent
|
||||
if err := db.DB(ctx).Where("event_key = ?", key).First(&event).Error; err != nil {
|
||||
return model.PushEvent{}, err
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// CountPushEventsByKey returns how many events use the given event key.
|
||||
func CountPushEventsByKey(ctx context.Context, key string) (int64, error) {
|
||||
var count int64
|
||||
if err := db.DB(ctx).Model(&model.PushEvent{}).Where("event_key = ?", key).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CreatePushEvent persists a new push event and invalidates cache.
|
||||
func CreatePushEvent(ctx context.Context, event *model.PushEvent) error {
|
||||
if err := db.DB(ctx).Create(event).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
DeleteActivePushEventCache(ctx, event.EventKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SavePushEvent updates a push event and invalidates cache.
|
||||
func SavePushEvent(ctx context.Context, event *model.PushEvent) error {
|
||||
if err := db.DB(ctx).Save(event).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
DeleteActivePushEventCache(ctx, event.EventKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePushEventEnabled toggles the enabled flag for a push event.
|
||||
func UpdatePushEventEnabled(ctx context.Context, event *model.PushEvent, enabled bool) error {
|
||||
event.Enabled = enabled
|
||||
if err := db.DB(ctx).Model(event).Update("enabled", enabled).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
DeleteActivePushEventCache(ctx, event.EventKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePushEvent removes a push event and invalidates cache.
|
||||
func DeletePushEvent(ctx context.Context, event *model.PushEvent) error {
|
||||
if err := db.DB(ctx).Delete(event).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
DeleteActivePushEventCache(ctx, event.EventKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListActivePushEventsByTaskType returns enabled events bound to a task type.
|
||||
func ListActivePushEventsByTaskType(ctx context.Context, taskType string) ([]model.PushEvent, error) {
|
||||
var events []model.PushEvent
|
||||
if err := db.DB(ctx).Where("task_type = ? AND enabled = ?", taskType, true).Find(&events).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// GetActivePushEventByKey 获取启用的通知事件 (优先从 Redis 缓存获取)。
|
||||
func GetActivePushEventByKey(ctx context.Context, key string) (*model.PushEvent, error) {
|
||||
cacheKey := "push:event:active:" + key
|
||||
var event model.PushEvent
|
||||
if db.Redis != nil {
|
||||
if err := db.GetJSON(ctx, cacheKey, &event); err == nil {
|
||||
return &event, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Where("event_key = ? AND enabled = ?", key, true).First(&event).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if db.Redis != nil {
|
||||
_ = db.SetJSON(ctx, cacheKey, event, activePushEventCacheTTL)
|
||||
}
|
||||
|
||||
return &event, nil
|
||||
}
|
||||
|
||||
// DeleteActivePushEventCache 清理启用通知事件的缓存。
|
||||
func DeleteActivePushEventCache(ctx context.Context, key string) {
|
||||
if db.Redis != nil {
|
||||
_ = db.Redis.Del(ctx, db.PrefixedKey("push:event:active:"+key)).Err()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PushHistoryListFilter filters push history pagination queries.
|
||||
type PushHistoryListFilter struct {
|
||||
EventKey string
|
||||
Status string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// ListPushHistories returns paginated push history records.
|
||||
func ListPushHistories(ctx context.Context, filter PushHistoryListFilter) (int64, []model.PushHistory, error) {
|
||||
query := db.DB(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
|
||||
}
|
||||
|
||||
// CreatePushHistory persists a push history audit record.
|
||||
func CreatePushHistory(ctx context.Context, history *model.PushHistory) error {
|
||||
return db.DB(ctx).Create(history).Error
|
||||
}
|
||||
|
||||
// PushHistoryQuery returns a scoped query builder for push histories.
|
||||
func PushHistoryQuery(ctx context.Context) *gorm.DB {
|
||||
return db.DB(ctx).Model(&model.PushHistory{})
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package repository provides data access with caching and persistence boundaries.
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
)
|
||||
|
||||
const (
|
||||
errDatabaseNotInitialized = "database not initialized"
|
||||
errConfigIntParseFailed = "配置 %s 的值 '%s' 无法转换为整数: %w"
|
||||
errConfigDecimalParseFailed = "配置 %s 的值 '%s' 无法转换为decimal: %w"
|
||||
errConfigBoolParseFailed = "配置 %s 的值 '%s' 无法转换为布尔值: %w"
|
||||
errParseMenuDisplayConfigFailed = "解析目录显示配置失败: %w"
|
||||
)
|
||||
|
||||
// GetSystemConfigByKey 通过 key 查询配置(带 RAM + Redis 缓存)。
|
||||
func GetSystemConfigByKey(ctx context.Context, key string) (model.SystemConfig, error) {
|
||||
ensureSystemConfigCacheListener()
|
||||
|
||||
if cached, ok := systemConfigRAMCache.GetIfPresent(key); ok {
|
||||
return cloneSystemConfig(cached), nil
|
||||
}
|
||||
|
||||
var sc model.SystemConfig
|
||||
if db.Redis != nil {
|
||||
if err := db.HGetJSON(ctx, SystemConfigRedisHashKey, key, &sc); err == nil {
|
||||
systemConfigRAMCache.Set(key, cloneSystemConfig(sc))
|
||||
return sc, nil
|
||||
} else if !errors.Is(err, redis.Nil) {
|
||||
return model.SystemConfig{}, err
|
||||
}
|
||||
}
|
||||
|
||||
database := db.DB(ctx)
|
||||
if database == nil {
|
||||
return model.SystemConfig{}, errors.New(errDatabaseNotInitialized)
|
||||
}
|
||||
|
||||
if err := database.Where("key = ?", key).First(&sc).Error; err != nil {
|
||||
return model.SystemConfig{}, err
|
||||
}
|
||||
|
||||
populateSystemConfigCache(ctx, sc)
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// ListSystemConfigsByKeys loads multiple config keys in one database round trip.
|
||||
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 cached, ok := systemConfigRAMCache.GetIfPresent(key); ok {
|
||||
result[key] = cloneSystemConfig(cached)
|
||||
continue
|
||||
}
|
||||
missing = append(missing, key)
|
||||
}
|
||||
|
||||
if len(missing) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
database := db.DB(ctx)
|
||||
if database == nil {
|
||||
return nil, errors.New(errDatabaseNotInitialized)
|
||||
}
|
||||
|
||||
var configs []model.SystemConfig
|
||||
if err := database.Where("key IN ?", missing).Find(&configs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range configs {
|
||||
populateSystemConfigCache(ctx, configs[i])
|
||||
result[configs[i].Key] = cloneSystemConfig(configs[i])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// InvalidateVisibleSystemConfigsCache clears the cached public config list.
|
||||
func InvalidateVisibleSystemConfigsCache(ctx context.Context) error {
|
||||
if db.Redis == nil {
|
||||
return nil
|
||||
}
|
||||
return db.Redis.Del(ctx, db.PrefixedKey(SystemConfigVisibleListRedisKey)).Err()
|
||||
}
|
||||
|
||||
// ListVisibleSystemConfigs 查询所有可通过公共配置接口暴露的配置(带 Redis 列表缓存)。
|
||||
func ListVisibleSystemConfigs(ctx context.Context) ([]model.SystemConfig, error) {
|
||||
if db.Redis != nil {
|
||||
var cached []model.SystemConfig
|
||||
if err := db.GetJSON(ctx, SystemConfigVisibleListRedisKey, &cached); err == nil {
|
||||
return cached, nil
|
||||
} else if !errors.Is(err, redis.Nil) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
database := db.DB(ctx)
|
||||
if database == nil {
|
||||
return nil, errors.New(errDatabaseNotInitialized)
|
||||
}
|
||||
|
||||
var configs []model.SystemConfig
|
||||
if err := database.Where("visibility = ?", model.ConfigVisibilityVisible).Find(&configs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if db.Redis != nil {
|
||||
_ = db.SetJSON(ctx, SystemConfigVisibleListRedisKey, configs, 0)
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// GetIntByKey 通过 key 查询配置并转换为 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(errConfigIntParseFailed, key, sc.Value, err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// GetDecimalByKey 通过 key 查询配置并转换为 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(errConfigDecimalParseFailed, key, sc.Value, err)
|
||||
}
|
||||
|
||||
return value.Truncate(precision), nil
|
||||
}
|
||||
|
||||
// GetBoolByKey 通过 key 查询配置并转换为 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(errConfigBoolParseFailed, key, sc.Value, err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// GetMenuDisplayConfig 获取目录显示配置,解析为 map[string]bool。
|
||||
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(errParseMenuDisplayConfigFailed, err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ListAdminSystemConfigs returns all configs, optionally filtered by type.
|
||||
func ListAdminSystemConfigs(ctx context.Context, configType string) ([]model.SystemConfig, error) {
|
||||
query := db.DB(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 PostgreSQL.
|
||||
func GetAdminSystemConfigByKey(ctx context.Context, key string) (model.SystemConfig, error) {
|
||||
var config model.SystemConfig
|
||||
if err := db.DB(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 := db.DB(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
|
||||
}
|
||||
|
||||
// CreateSystemConfig persists a new system config row.
|
||||
func CreateSystemConfig(ctx context.Context, config *model.SystemConfig) error {
|
||||
return db.DB(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 db.DB(ctx).Model(config).Updates(updates).Error
|
||||
}
|
||||
|
||||
// SaveOrUpdateSystemConfig creates or updates a config row and invalidates cache.
|
||||
func SaveOrUpdateSystemConfig(ctx context.Context, key, value string) error {
|
||||
var sc model.SystemConfig
|
||||
err := db.DB(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: "system",
|
||||
Visibility: model.ConfigVisibilityHidden,
|
||||
}
|
||||
if err := db.DB(ctx).Create(&sc).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
sc.Value = value
|
||||
if err := db.DB(ctx).Save(&sc).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return InvalidateSystemConfigCache(ctx, key)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/pkg/cache/ram"
|
||||
)
|
||||
|
||||
const (
|
||||
// SystemConfigInvalidationChannel broadcasts RAM cache eviction across nodes.
|
||||
SystemConfigInvalidationChannel = "system:config_invalidation"
|
||||
// SystemConfigRedisHashKey Redis Hash key,存储所有系统配置。
|
||||
SystemConfigRedisHashKey = "system:system_configs"
|
||||
// SystemConfigVisibleListRedisKey Redis key,缓存所有 visibility=1 的公共配置列表。
|
||||
SystemConfigVisibleListRedisKey = "system:visible_configs"
|
||||
|
||||
systemConfigInvalidateAllToken = "*"
|
||||
systemConfigRAMMaximumSize = 512
|
||||
)
|
||||
|
||||
type systemConfigInvalidationMessage struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
var (
|
||||
systemConfigRAMCache = ram.MustNew[string, model.SystemConfig](ram.Options{MaximumSize: systemConfigRAMMaximumSize})
|
||||
systemConfigListenerOnce sync.Once
|
||||
)
|
||||
|
||||
func ensureSystemConfigCacheListener() {
|
||||
systemConfigListenerOnce.Do(startSystemConfigCacheInvalidationListener)
|
||||
}
|
||||
|
||||
func startSystemConfigCacheInvalidationListener() {
|
||||
if db.Redis == nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
pubsub := db.Redis.Subscribe(context.Background(), SystemConfigInvalidationChannel)
|
||||
defer func() {
|
||||
_ = pubsub.Close()
|
||||
}()
|
||||
|
||||
for msg := range pubsub.Channel() {
|
||||
var payload systemConfigInvalidationMessage
|
||||
if err := json.Unmarshal([]byte(msg.Payload), &payload); err != nil {
|
||||
systemConfigRAMCache.InvalidateAll()
|
||||
continue
|
||||
}
|
||||
if payload.Key == "" || payload.Key == systemConfigInvalidateAllToken {
|
||||
systemConfigRAMCache.InvalidateAll()
|
||||
continue
|
||||
}
|
||||
systemConfigRAMCache.Invalidate(payload.Key)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func cloneSystemConfig(sc model.SystemConfig) model.SystemConfig {
|
||||
return sc
|
||||
}
|
||||
|
||||
func populateSystemConfigCache(ctx context.Context, sc model.SystemConfig) {
|
||||
systemConfigRAMCache.Set(sc.Key, cloneSystemConfig(sc))
|
||||
if db.Redis != nil {
|
||||
_ = db.HSetJSON(ctx, SystemConfigRedisHashKey, sc.Key, &sc)
|
||||
}
|
||||
}
|
||||
|
||||
func publishSystemConfigRAMInvalidation(ctx context.Context, key string) {
|
||||
if db.Redis == nil {
|
||||
return
|
||||
}
|
||||
payload, err := json.Marshal(systemConfigInvalidationMessage{Key: key})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = db.Redis.Publish(ctx, SystemConfigInvalidationChannel, payload).Err()
|
||||
}
|
||||
|
||||
// InvalidateSystemConfigCache evicts one config key from local RAM and Redis.
|
||||
func InvalidateSystemConfigCache(ctx context.Context, key string) error {
|
||||
ensureSystemConfigCacheListener()
|
||||
|
||||
systemConfigRAMCache.Invalidate(key)
|
||||
if db.Redis != nil {
|
||||
if err := db.HDel(ctx, SystemConfigRedisHashKey, key); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
publishSystemConfigRAMInvalidation(ctx, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// InvalidateAllSystemConfigCaches evicts all config entries from local RAM and Redis.
|
||||
func InvalidateAllSystemConfigCaches(ctx context.Context) error {
|
||||
ensureSystemConfigCacheListener()
|
||||
|
||||
systemConfigRAMCache.InvalidateAll()
|
||||
if db.Redis != nil {
|
||||
if err := db.Redis.Del(ctx, db.PrefixedKey(SystemConfigRedisHashKey)).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
publishSystemConfigRAMInvalidation(ctx, systemConfigInvalidateAllToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetSystemConfigRAMCacheForTest clears only the process-local RAM cache.
|
||||
func ResetSystemConfigRAMCacheForTest() {
|
||||
systemConfigRAMCache.InvalidateAll()
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ListTemplates returns all templates ordered by system flag and creation time.
|
||||
func ListTemplates(ctx context.Context) ([]model.Template, error) {
|
||||
var templates []model.Template
|
||||
if err := db.DB(ctx).Order("is_system DESC, created_at DESC").Find(&templates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
// GetTemplateByKey loads a template by its key.
|
||||
func GetTemplateByKey(ctx context.Context, key string) (model.Template, error) {
|
||||
var tmpl model.Template
|
||||
if err := db.DB(ctx).Where("key = ?", key).First(&tmpl).Error; err != nil {
|
||||
return model.Template{}, err
|
||||
}
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
// TemplateExistsByKey reports whether a template key is already taken.
|
||||
func TemplateExistsByKey(ctx context.Context, key string) (bool, error) {
|
||||
var existing model.Template
|
||||
err := db.DB(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
|
||||
}
|
||||
|
||||
// CreateTemplate persists a new template.
|
||||
func CreateTemplate(ctx context.Context, tmpl *model.Template) error {
|
||||
return db.DB(ctx).Create(tmpl).Error
|
||||
}
|
||||
|
||||
// SaveTemplate updates an existing template.
|
||||
func SaveTemplate(ctx context.Context, tmpl *model.Template) error {
|
||||
return db.DB(ctx).Save(tmpl).Error
|
||||
}
|
||||
|
||||
// DeleteTemplate removes a template record.
|
||||
func DeleteTemplate(ctx context.Context, tmpl *model.Template) error {
|
||||
return db.DB(ctx).Delete(tmpl).Error
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UploadListFilter filters paginated upload queries.
|
||||
type UploadListFilter struct {
|
||||
UserID uint64
|
||||
Keyword string
|
||||
Type string
|
||||
Extension string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// ListUploads returns paginated upload records matching the filter.
|
||||
func ListUploads(ctx context.Context, filter UploadListFilter) (int64, []model.Upload, error) {
|
||||
query := db.DB(ctx).Model(&model.Upload{}).
|
||||
Where("status != ?", model.UploadStatusDeleted)
|
||||
|
||||
if filter.UserID != 0 {
|
||||
query = query.Where("user_id = ?", filter.UserID)
|
||||
}
|
||||
if filter.Keyword != "" {
|
||||
query = query.Where("LOWER(file_name) LIKE ?", "%"+strings.ToLower(filter.Keyword)+"%")
|
||||
}
|
||||
if filter.Type != "" {
|
||||
query = query.Where("type = ?", filter.Type)
|
||||
}
|
||||
if filter.Extension != "" {
|
||||
query = query.Where("extension = ?", strings.ToLower(filter.Extension))
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
var items []model.Upload
|
||||
offset := (filter.Page - 1) * filter.PageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(filter.PageSize).Find(&items).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return total, items, nil
|
||||
}
|
||||
|
||||
// GetActiveUploadByID loads a non-deleted upload by ID.
|
||||
func GetActiveUploadByID(ctx context.Context, id uint64) (model.Upload, error) {
|
||||
var upload model.Upload
|
||||
if err := db.DB(ctx).Where("id = ? AND status != ?", id, model.UploadStatusDeleted).First(&upload).Error; err != nil {
|
||||
return model.Upload{}, err
|
||||
}
|
||||
return upload, nil
|
||||
}
|
||||
|
||||
// SoftDeleteUpload marks an upload as deleted.
|
||||
// External modules must use upload.Remove or upload.RemoveOwned; only internal/apps/upload may call this.
|
||||
func SoftDeleteUpload(ctx context.Context, upload *model.Upload) error {
|
||||
return db.DB(ctx).Model(upload).Update("status", model.UploadStatusDeleted).Error
|
||||
}
|
||||
|
||||
// UpdateUpload applies partial field updates to an upload record.
|
||||
func UpdateUpload(ctx context.Context, upload *model.Upload, updates map[string]any) error {
|
||||
if len(updates) == 0 {
|
||||
return nil
|
||||
}
|
||||
return db.DB(ctx).Model(upload).Updates(updates).Error
|
||||
}
|
||||
|
||||
// ListDistinctUploadTypes returns all distinct non-empty upload business types.
|
||||
func ListDistinctUploadTypes(ctx context.Context) ([]string, error) {
|
||||
var types []string
|
||||
if err := db.DB(ctx).Model(&model.Upload{}).
|
||||
Where("type IS NOT NULL AND type != ''").
|
||||
Distinct().
|
||||
Pluck("type", &types).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return types, nil
|
||||
}
|
||||
|
||||
// FindReusableUploadByHash finds an existing upload with the same hash and size.
|
||||
func FindReusableUploadByHash(ctx context.Context, hash string, size int64) (model.Upload, error) {
|
||||
var existing model.Upload
|
||||
err := db.DB(ctx).
|
||||
Where("hash = ? AND file_size = ? AND status IN (?, ?)", hash, size, model.UploadStatusPending, model.UploadStatusUsed).
|
||||
First(&existing).Error
|
||||
return existing, err
|
||||
}
|
||||
|
||||
// CreateUpload persists a new upload record.
|
||||
// External modules must use upload.Ingest; only internal/apps/upload may call this.
|
||||
func CreateUpload(ctx context.Context, upload *model.Upload) error {
|
||||
return db.DB(ctx).Create(upload).Error
|
||||
}
|
||||
|
||||
// ListUploadsByIDs returns active uploads matching the given IDs.
|
||||
func ListUploadsByIDs(ctx context.Context, ids []uint64) ([]model.Upload, error) {
|
||||
var uploads []model.Upload
|
||||
if err := db.DB(ctx).
|
||||
Where("id IN ? AND status IN (?, ?)", ids, model.UploadStatusPending, model.UploadStatusUsed).
|
||||
Find(&uploads).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uploads, nil
|
||||
}
|
||||
|
||||
// UploadQuery returns a scoped GORM query for uploads.
|
||||
func UploadQuery(ctx context.Context) *gorm.DB {
|
||||
return db.DB(ctx).Model(&model.Upload{})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
)
|
||||
|
||||
// ListUploadStats returns all upload statistics rows.
|
||||
func ListUploadStats(ctx context.Context) ([]model.UploadStat, error) {
|
||||
var stats []model.UploadStat
|
||||
if err := db.DB(ctx).Find(&stats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GetUserByID loads an active user by ID.
|
||||
func GetUserByID(ctx context.Context, id uint64) (model.User, error) {
|
||||
var user model.User
|
||||
if err := db.DB(ctx).Where("id = ?", id).First(&user).Error; err != nil {
|
||||
return model.User{}, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetUserByUsername loads a user by username.
|
||||
func GetUserByUsername(ctx context.Context, username string) (model.User, error) {
|
||||
var user model.User
|
||||
if err := db.DB(ctx).Where("username = ?", username).First(&user).Error; err != nil {
|
||||
return model.User{}, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetSystemUser loads the built-in system user, or returns a synthetic fallback.
|
||||
func GetSystemUser(ctx context.Context) model.User {
|
||||
var user model.User
|
||||
if err := db.DB(ctx).Where("username = ?", "system").First(&user).Error; err == nil {
|
||||
return user
|
||||
}
|
||||
return model.User{
|
||||
ID: 999,
|
||||
Username: "system",
|
||||
Nickname: "系统",
|
||||
}
|
||||
}
|
||||
|
||||
// GetFirstAdminUser loads the earliest admin user.
|
||||
func GetFirstAdminUser(ctx context.Context) (model.User, error) {
|
||||
var user model.User
|
||||
if err := db.DB(ctx).Where("is_admin = ?", true).Order("id asc").First(&user).Error; err != nil {
|
||||
return model.User{}, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// AdminUserListFilter filters admin user list queries.
|
||||
type AdminUserListFilter struct {
|
||||
UserID *uint64
|
||||
Username string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// ListAdminUsers returns paginated users for the admin console.
|
||||
func ListAdminUsers(ctx context.Context, filter AdminUserListFilter) (int64, []model.User, error) {
|
||||
query := db.DB(ctx).Model(&model.User{})
|
||||
if filter.UserID != nil {
|
||||
query = query.Where("id = ?", *filter.UserID)
|
||||
}
|
||||
if filter.Username != "" {
|
||||
query = query.Where("username LIKE ?", filter.Username+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
var users []model.User
|
||||
offset := (filter.Page - 1) * filter.PageSize
|
||||
if err := query.
|
||||
Select("id, username, nickname, avatar_url, is_active, is_admin, last_login_at, created_at, updated_at").
|
||||
Order("id ASC").
|
||||
Offset(offset).
|
||||
Limit(filter.PageSize).
|
||||
Find(&users).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return total, users, nil
|
||||
}
|
||||
|
||||
// GetAdminUserDetail loads full user profile fields for admin detail view.
|
||||
func GetAdminUserDetail(ctx context.Context, id uint64) (model.User, error) {
|
||||
var user model.User
|
||||
if err := db.DB(ctx).
|
||||
Select("id, username, nickname, email, avatar_url, is_active, is_admin, bio, phone, gender, website, location, last_login_at, created_at, updated_at").
|
||||
Where("id = ?", id).
|
||||
First(&user).Error; err != nil {
|
||||
return model.User{}, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// UserAdminFlags stores minimal user authorization flags.
|
||||
type UserAdminFlags struct {
|
||||
ID uint64
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
// GetUserAdminFlags loads id and is_admin for authorization checks.
|
||||
func GetUserAdminFlags(ctx context.Context, id uint64) (UserAdminFlags, error) {
|
||||
var flags UserAdminFlags
|
||||
if err := db.DB(ctx).
|
||||
Model(&model.User{}).
|
||||
Select("id, is_admin").
|
||||
Where("id = ?", id).
|
||||
First(&flags).Error; err != nil {
|
||||
return UserAdminFlags{}, err
|
||||
}
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
// UpdateUserActive updates the is_active flag for a user.
|
||||
func UpdateUserActive(ctx context.Context, id uint64, active bool) error {
|
||||
return db.DB(ctx).Model(&model.User{}).Where("id = ?", id).Update("is_active", active).Error
|
||||
}
|
||||
|
||||
// DeleteUserWithRelations removes a user and related access tokens / external accounts.
|
||||
func DeleteUserWithRelations(ctx context.Context, id uint64) error {
|
||||
return db.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("user_id = ?", id).Delete(&model.AccessToken{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", id).Delete(&model.ExternalAccount{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Where("id = ?", id).Delete(&model.User{}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// CountUsersByUsername returns how many users share the username.
|
||||
func CountUsersByUsername(ctx context.Context, username string) (int64, error) {
|
||||
var count int64
|
||||
if err := db.DB(ctx).Model(&model.User{}).Where("username = ?", username).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountUsersByEmail returns how many users share the email.
|
||||
func CountUsersByEmail(ctx context.Context, email string) (int64, error) {
|
||||
var count int64
|
||||
if err := db.DB(ctx).Model(&model.User{}).Where("email = ?", email).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CreateUser persists a new user record.
|
||||
func CreateUser(ctx context.Context, user *model.User) error {
|
||||
return db.DB(ctx).Create(user).Error
|
||||
}
|
||||
|
||||
// ListUsersByIDs loads users matching the given IDs.
|
||||
func ListUsersByIDs(ctx context.Context, ids []uint64) ([]model.User, error) {
|
||||
if len(ids) == 0 {
|
||||
return []model.User{}, nil
|
||||
}
|
||||
var users []model.User
|
||||
if err := db.DB(ctx).Where("id IN ?", ids).Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
Reference in New Issue
Block a user