Initial open-source release (MIT): image2api AI gateway
Full Go backend + Vue 3 frontend, OpenAI-compatible API, multi-provider account pools, billing/admin, Docker one-command deploy with auto HTTPS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"backend/internal/model"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const siteSettingCachePrefix = "setting:"
|
||||
|
||||
// siteSettingCacheTTL is a safety-net expiry; writes invalidate eagerly, so this
|
||||
// only bounds staleness if an invalidation is ever missed (e.g. Redis blip).
|
||||
const siteSettingCacheTTL = 5 * time.Minute
|
||||
|
||||
type SiteSettingRepository struct {
|
||||
db *gorm.DB
|
||||
cache *redis.Client
|
||||
}
|
||||
|
||||
// NewSiteSettingRepository wires the config KV store. cache may be nil, in which
|
||||
// case the repository transparently falls back to DB-only access.
|
||||
func NewSiteSettingRepository(db *gorm.DB, cache *redis.Client) *SiteSettingRepository {
|
||||
return &SiteSettingRepository{db: db, cache: cache}
|
||||
}
|
||||
|
||||
func (r *SiteSettingRepository) cacheKey(key string) string {
|
||||
return siteSettingCachePrefix + key
|
||||
}
|
||||
|
||||
func (r *SiteSettingRepository) GetValue(ctx context.Context, key string) (string, error) {
|
||||
if r.cache != nil {
|
||||
if v, err := r.cache.Get(ctx, r.cacheKey(key)).Result(); err == nil {
|
||||
return v, nil
|
||||
}
|
||||
// redis.Nil (miss) or any transient cache error -> fall through to DB.
|
||||
}
|
||||
|
||||
value := ""
|
||||
var setting model.SiteSetting
|
||||
if err := r.db.WithContext(ctx).First(&setting, "key = ?", key).Error; err != nil {
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return "", err
|
||||
}
|
||||
// Not found stays as "" — still cached below to absorb repeated misses.
|
||||
} else {
|
||||
value = setting.Value
|
||||
}
|
||||
|
||||
if r.cache != nil {
|
||||
_ = r.cache.Set(ctx, r.cacheKey(key), value, siteSettingCacheTTL).Err()
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (r *SiteSettingRepository) UpsertValue(ctx context.Context, key, value string) error {
|
||||
if err := r.db.WithContext(ctx).Save(&model.SiteSetting{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
r.invalidate(ctx, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SiteSettingRepository) UpsertValues(ctx context.Context, values map[string]string) error {
|
||||
if err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for key, value := range values {
|
||||
if err := tx.Save(&model.SiteSetting{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
for key := range values {
|
||||
r.invalidate(ctx, key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// invalidate drops the cached entry so the next read repopulates from the DB.
|
||||
// Deleting (rather than overwriting) keeps writes simple and race-tolerant.
|
||||
func (r *SiteSettingRepository) invalidate(ctx context.Context, key string) {
|
||||
if r.cache == nil {
|
||||
return
|
||||
}
|
||||
_ = r.cache.Del(ctx, r.cacheKey(key)).Err()
|
||||
}
|
||||
Reference in New Issue
Block a user