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:
2026-06-25 22:59:04 +08:00
co-authored by Claude Opus 4.8
commit 606caaf047
142 changed files with 33648 additions and 0 deletions
@@ -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()
}