mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 23:16:37 +08:00
refactor(api): extract repository layer and thin HTTP handlers
Introduce internal/repository for data access and cache-backed system config reads. Move business logic into logics.go across admin push, user, template, cache, system_config, and upload/handler packages. Remove Gin from internal/util by relocating request-scoped helpers to oauth/gin_context.go. Propagate request context for config lookups in user flows. Slim model entities and delete model-level DB/cache helpers. Wire handlers to logics/repository so targeted packages no longer call db.DB directly. Update admin router tests to use ErrorHandlerMiddleware.
This commit is contained in:
@@ -4,15 +4,11 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -70,8 +66,6 @@ func (pc *PushChannel) Validate() error {
|
||||
return errors.New("request URL/address is required")
|
||||
}
|
||||
|
||||
// For custom and lark, we must enforce https:// URL prefix for security.
|
||||
// For email, it is an SMTP host:port, so no need for https:// prefix.
|
||||
if pc.Type != TypeEmail && !strings.HasPrefix(pc.URL, "https://") {
|
||||
return errors.New("request URL must use HTTPS protocol for security reasons")
|
||||
}
|
||||
@@ -102,58 +96,4 @@ func validateJSON(s string) error {
|
||||
return nil
|
||||
}
|
||||
return errors.New("payload schema must be a valid JSON format")
|
||||
}
|
||||
|
||||
// GetPushChannelByName 根据名称获取消息通道
|
||||
func GetPushChannelByName(ctx context.Context, name string) (*PushChannel, error) {
|
||||
var channel PushChannel
|
||||
err := db.DB(ctx).Where("name = ?", name).First(&channel).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
const activePushChannelCacheTTL = 24 * time.Hour
|
||||
|
||||
// GetActivePushChannelByName 根据名称获取启用的消息通道 (优先从 Redis 缓存获取)
|
||||
func GetActivePushChannelByName(ctx context.Context, name string) (*PushChannel, error) {
|
||||
cacheKey := "push:channel:active:" + name
|
||||
var channel PushChannel
|
||||
if db.Redis != nil {
|
||||
if err := db.GetJSON(ctx, cacheKey, &channel); err == nil {
|
||||
return &channel, nil
|
||||
}
|
||||
}
|
||||
|
||||
err := db.DB(ctx).Where("name = ? AND enabled = ?", name, true).First(&channel).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if db.Redis != nil {
|
||||
// 缓存有效时间设置为 24 小时
|
||||
_ = 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()
|
||||
}
|
||||
}
|
||||
|
||||
// AfterSave GORM 保存后钩子,用于自动清理缓存
|
||||
func (pc *PushChannel) AfterSave(tx *gorm.DB) error {
|
||||
DeleteActivePushChannelCache(tx.Statement.Context, pc.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AfterDelete GORM 删除后钩子,用于自动清理缓存
|
||||
func (pc *PushChannel) AfterDelete(tx *gorm.DB) error {
|
||||
DeleteActivePushChannelCache(tx.Statement.Context, pc.Name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,9 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PushEvent 系统通知事件模型
|
||||
@@ -51,48 +47,4 @@ func (pe *PushEvent) Validate() error {
|
||||
return errors.New("cannot enable event without any push channels configured")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const activePushEventCacheTTL = 24 * time.Hour
|
||||
|
||||
// GetActivePushEventByKey 获取启用的通知事件 (优先从 Redis 缓存获取)
|
||||
func GetActivePushEventByKey(ctx context.Context, key string) (*PushEvent, error) {
|
||||
cacheKey := "push:event:active:" + key
|
||||
var event PushEvent
|
||||
if db.Redis != nil {
|
||||
if err := db.GetJSON(ctx, cacheKey, &event); err == nil {
|
||||
return &event, nil
|
||||
}
|
||||
}
|
||||
|
||||
err := db.DB(ctx).Where("event_key = ? AND enabled = ?", key, true).First(&event).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if db.Redis != nil {
|
||||
// 缓存有效时间设置为 24 小时
|
||||
_ = 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()
|
||||
}
|
||||
}
|
||||
|
||||
// AfterSave GORM 保存后钩子,用于自动清理缓存
|
||||
func (pe *PushEvent) AfterSave(tx *gorm.DB) error {
|
||||
DeleteActivePushEventCache(tx.Statement.Context, pe.EventKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AfterDelete GORM 删除后钩子,用于自动清理缓存
|
||||
func (pe *PushEvent) AfterDelete(tx *gorm.DB) error {
|
||||
DeleteActivePushEventCache(tx.Statement.Context, pe.EventKey)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/pkg/cache/ram"
|
||||
)
|
||||
|
||||
const (
|
||||
// SystemConfigInvalidationChannel broadcasts RAM cache eviction across nodes.
|
||||
SystemConfigInvalidationChannel = "system:config_invalidation"
|
||||
systemConfigInvalidateAllToken = "*"
|
||||
systemConfigRAMMaximumSize = 512
|
||||
)
|
||||
|
||||
type systemConfigInvalidationMessage struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
var (
|
||||
systemConfigRAMCache = ram.MustNew[string, 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 SystemConfig) SystemConfig {
|
||||
return sc
|
||||
}
|
||||
|
||||
func populateSystemConfigCache(ctx context.Context, sc 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.
|
||||
// It also publishes cluster-wide RAM invalidation when Redis is available.
|
||||
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.
|
||||
// It also publishes cluster-wide RAM invalidation when Redis is available.
|
||||
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()
|
||||
}
|
||||
@@ -3,19 +3,7 @@
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
)
|
||||
import "time"
|
||||
|
||||
// 配置键常量 - 所有系统配置的 key 定义
|
||||
const (
|
||||
@@ -51,13 +39,6 @@ const (
|
||||
ConfigKeyStorageConfig = "storage_config" // 文件存储配置 (JSON)
|
||||
)
|
||||
|
||||
const (
|
||||
// SystemConfigRedisHashKey Redis Hash key,存储所有系统配置
|
||||
SystemConfigRedisHashKey = "system:system_configs"
|
||||
// SystemConfigVisibleListRedisKey Redis key,缓存所有 visibility=1 的公共配置列表
|
||||
SystemConfigVisibleListRedisKey = "system:visible_configs"
|
||||
)
|
||||
|
||||
const (
|
||||
// ConfigVisibilityHidden 表示配置不通过公共配置接口暴露
|
||||
ConfigVisibilityHidden = 0
|
||||
@@ -79,181 +60,4 @@ type SystemConfig struct {
|
||||
// TableName 表名
|
||||
func (SystemConfig) TableName() string {
|
||||
return "w_system_configs"
|
||||
}
|
||||
|
||||
// GetByKey 通过 key 查询配置(带 RAM + Redis 缓存)
|
||||
func (sc *SystemConfig) GetByKey(ctx context.Context, key string) error {
|
||||
ensureSystemConfigCacheListener()
|
||||
|
||||
if cached, ok := systemConfigRAMCache.GetIfPresent(key); ok {
|
||||
*sc = cloneSystemConfig(cached)
|
||||
return nil
|
||||
}
|
||||
|
||||
if db.Redis != nil {
|
||||
if err := db.HGetJSON(ctx, SystemConfigRedisHashKey, key, sc); err == nil {
|
||||
systemConfigRAMCache.Set(key, cloneSystemConfig(*sc))
|
||||
return nil
|
||||
} else if !errors.Is(err, redis.Nil) {
|
||||
// Redis 服务错误,返回错误
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 查数据库
|
||||
database := db.DB(ctx)
|
||||
if database == nil {
|
||||
return errors.New(errDatabaseNotInitialized)
|
||||
}
|
||||
|
||||
if err := database.Where("key = ?", key).First(sc).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
populateSystemConfigCache(ctx, *sc)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListSystemConfigsByKeys loads multiple config keys in one database round trip.
|
||||
// Keys already present in the process-local RAM cache are returned without querying PostgreSQL.
|
||||
func ListSystemConfigsByKeys(ctx context.Context, keys []string) (map[string]SystemConfig, error) {
|
||||
if len(keys) == 0 {
|
||||
return map[string]SystemConfig{}, nil
|
||||
}
|
||||
|
||||
ensureSystemConfigCacheListener()
|
||||
|
||||
result := make(map[string]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 []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) ([]SystemConfig, error) {
|
||||
if db.Redis != nil {
|
||||
var cached []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 []SystemConfig
|
||||
if err := database.Where("visibility = ?", 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) {
|
||||
var sc SystemConfig
|
||||
if err := sc.GetByKey(ctx, key); 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 类型
|
||||
// precision 指定保留的小数位数,多余的小数会被裁剪
|
||||
func GetDecimalByKey(ctx context.Context, key string, precision int32) (decimal.Decimal, error) {
|
||||
var sc SystemConfig
|
||||
if err := sc.GetByKey(ctx, key); 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) {
|
||||
var sc SystemConfig
|
||||
if err := sc.GetByKey(ctx, key); 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) {
|
||||
var sc SystemConfig
|
||||
if err := sc.GetByKey(ctx, ConfigKeyMenuDisplayConfig); 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
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,10 @@ package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
)
|
||||
|
||||
// Template 邮件/消息模板实体
|
||||
@@ -90,17 +86,3 @@ func (t *Template) Render(data any) (string, string, error) {
|
||||
|
||||
return subject, bodyBuf.String(), nil
|
||||
}
|
||||
|
||||
// RenderTemplate 渲染指定模板。模板不存在或渲染失败时返回错误,由调用方决定如何处理。
|
||||
func RenderTemplate(ctx context.Context, key string, data any) (string, string, error) {
|
||||
var t Template
|
||||
if err := db.DB(ctx).Where("key = ?", key).First(&t).Error; err != nil {
|
||||
return "", "", fmt.Errorf(errTemplateUnavailable, key, err)
|
||||
}
|
||||
|
||||
subject, body, err := t.Render(data)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf(errTemplateRenderFailed, key, err)
|
||||
}
|
||||
return subject, body, nil
|
||||
}
|
||||
|
||||
+2
-12
@@ -139,12 +139,7 @@ func (u *User) assignIDIfMissing() error {
|
||||
}
|
||||
|
||||
// CreateUser 创建新用户(用于 OAuth/OIDC 自动注册,含底层权限校验)
|
||||
func (u *User) CreateUser(ctx context.Context, tx *gorm.DB, oauthInfo *OAuthUserInfo) error {
|
||||
enabled, err := GetBoolByKey(ctx, ConfigKeyRegistrationEnabled)
|
||||
if err == nil && !enabled {
|
||||
return errors.New(errRegistrationDisabled)
|
||||
}
|
||||
|
||||
func (u *User) CreateUser(_ context.Context, tx *gorm.DB, oauthInfo *OAuthUserInfo) error {
|
||||
now := time.Now()
|
||||
userID := oauthInfo.GetID()
|
||||
newUser := User{
|
||||
@@ -169,12 +164,7 @@ func (u *User) CreateUser(ctx context.Context, tx *gorm.DB, oauthInfo *OAuthUser
|
||||
}
|
||||
|
||||
// RegisterUser 创建新用户并注册(用于本地密码注册,含全局开关和唯一性多重底层校验)
|
||||
func (u *User) RegisterUser(ctx context.Context, tx *gorm.DB) error {
|
||||
enabled, err := GetBoolByKey(ctx, ConfigKeyRegistrationEnabled)
|
||||
if err == nil && !enabled {
|
||||
return errors.New(errRegistrationDisabled)
|
||||
}
|
||||
|
||||
func (u *User) RegisterUser(_ context.Context, tx *gorm.DB) error {
|
||||
// 检查用户名冲突
|
||||
var count int64
|
||||
if err := tx.Model(&User{}).Where("username = ?", u.Username).Count(&count).Error; err != nil {
|
||||
|
||||
Reference in New Issue
Block a user