From 8181746695bc0c6535a2606667f018ecfde56e39 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Thu, 1 Jan 2026 08:29:57 +0800 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20=E4=BC=98=E5=8C=96=20Gemi?= =?UTF-8?q?ni=20=E9=85=8D=E9=A2=9D=E6=98=BE=E7=A4=BA=EF=BC=8C=E5=8F=82?= =?UTF-8?q?=E8=80=83=20Antigravity=20=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 简化标签:将 "RPD Pro/Flash" 改为 "Pro/Flash",避免文字截断 - 添加账号类型徽章(Free/Pro/Ultra),带颜色区分 - 添加帮助图标(?),悬停显示限流政策和官方文档链接 - 重构显示布局:账号类型 + 两行配额(Pro/Flash) - 移除冗余的 AccountQuotaInfo 组件调用 --- .gitignore | 1 + .../internal/service/account_usage_service.go | 17 +-- backend/internal/service/ratelimit_service.go | 60 +++++++++- backend/migrations/017_add_gemini_tier_id.sql | 7 +- .../components/account/AccountQuotaInfo.vue | 9 +- .../components/account/AccountUsageCell.vue | 111 +++++++++++++++++- frontend/src/i18n/locales/en.ts | 9 +- frontend/src/i18n/locales/zh.ts | 9 +- 8 files changed, 201 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 390c8a03..6d636c8d 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,7 @@ temp/ *.temp *.log *.bak +.cache/ # =================== # 构建产物 diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index 0ea08763..dfceac07 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -202,22 +202,25 @@ func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64) (*U func (s *AccountUsageService) getGeminiUsage(ctx context.Context, account *Account) (*UsageInfo, error) { now := time.Now() - start := geminiDailyWindowStart(now) - - stats, err := s.usageLogRepo.GetModelStatsWithFilters(ctx, start, now, 0, 0, account.ID) - if err != nil { - return nil, fmt.Errorf("get gemini usage stats failed: %w", err) - } - usage := &UsageInfo{ UpdatedAt: &now, } + if s.geminiQuotaService == nil || s.usageLogRepo == nil { + return usage, nil + } + quota, ok := s.geminiQuotaService.QuotaForAccount(ctx, account) if !ok { return usage, nil } + start := geminiDailyWindowStart(now) + stats, err := s.usageLogRepo.GetModelStatsWithFilters(ctx, start, now, 0, 0, account.ID) + if err != nil { + return nil, fmt.Errorf("get gemini usage stats failed: %w", err) + } + totals := geminiAggregateUsage(stats) resetAt := geminiDailyResetTime(now) diff --git a/backend/internal/service/ratelimit_service.go b/backend/internal/service/ratelimit_service.go index ea83723d..0511d950 100644 --- a/backend/internal/service/ratelimit_service.go +++ b/backend/internal/service/ratelimit_service.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" "strings" + "sync" "time" "github.com/Wei-Shaw/sub2api/internal/config" @@ -17,8 +18,18 @@ type RateLimitService struct { usageRepo UsageLogRepository cfg *config.Config geminiQuotaService *GeminiQuotaService + usageCacheMu sync.Mutex + usageCache map[int64]*geminiUsageCacheEntry } +type geminiUsageCacheEntry struct { + windowStart time.Time + cachedAt time.Time + totals GeminiUsageTotals +} + +const geminiPrecheckCacheTTL = time.Minute + // NewRateLimitService 创建RateLimitService实例 func NewRateLimitService(accountRepo AccountRepository, usageRepo UsageLogRepository, cfg *config.Config, geminiQuotaService *GeminiQuotaService) *RateLimitService { return &RateLimitService{ @@ -26,6 +37,7 @@ func NewRateLimitService(accountRepo AccountRepository, usageRepo UsageLogReposi usageRepo: usageRepo, cfg: cfg, geminiQuotaService: geminiQuotaService, + usageCache: make(map[int64]*geminiUsageCacheEntry), } } @@ -73,7 +85,7 @@ func (s *RateLimitService) PreCheckUsage(ctx context.Context, account *Account, if account == nil || !account.IsGeminiCodeAssist() || strings.TrimSpace(requestedModel) == "" { return true, nil } - if s.usageRepo == nil { + if s.usageRepo == nil || s.geminiQuotaService == nil { return true, nil } @@ -95,11 +107,15 @@ func (s *RateLimitService) PreCheckUsage(ctx context.Context, account *Account, now := time.Now() start := geminiDailyWindowStart(now) - stats, err := s.usageRepo.GetModelStatsWithFilters(ctx, start, now, 0, 0, account.ID) - if err != nil { - return true, err + totals, ok := s.getGeminiUsageTotals(account.ID, start, now) + if !ok { + stats, err := s.usageRepo.GetModelStatsWithFilters(ctx, start, now, 0, 0, account.ID) + if err != nil { + return true, err + } + totals = geminiAggregateUsage(stats) + s.setGeminiUsageTotals(account.ID, start, now, totals) } - totals := geminiAggregateUsage(stats) var used int64 switch geminiModelClassFromName(requestedModel) { @@ -121,6 +137,40 @@ func (s *RateLimitService) PreCheckUsage(ctx context.Context, account *Account, return true, nil } +func (s *RateLimitService) getGeminiUsageTotals(accountID int64, windowStart, now time.Time) (GeminiUsageTotals, bool) { + s.usageCacheMu.Lock() + defer s.usageCacheMu.Unlock() + + if s.usageCache == nil { + return GeminiUsageTotals{}, false + } + + entry, ok := s.usageCache[accountID] + if !ok || entry == nil { + return GeminiUsageTotals{}, false + } + if !entry.windowStart.Equal(windowStart) { + return GeminiUsageTotals{}, false + } + if now.Sub(entry.cachedAt) >= geminiPrecheckCacheTTL { + return GeminiUsageTotals{}, false + } + return entry.totals, true +} + +func (s *RateLimitService) setGeminiUsageTotals(accountID int64, windowStart, now time.Time, totals GeminiUsageTotals) { + s.usageCacheMu.Lock() + defer s.usageCacheMu.Unlock() + if s.usageCache == nil { + s.usageCache = make(map[int64]*geminiUsageCacheEntry) + } + s.usageCache[accountID] = &geminiUsageCacheEntry{ + windowStart: windowStart, + cachedAt: now, + totals: totals, + } +} + // GeminiCooldown returns the fallback cooldown duration for Gemini 429s based on tier. func (s *RateLimitService) GeminiCooldown(ctx context.Context, account *Account) time.Duration { if account == nil { diff --git a/backend/migrations/017_add_gemini_tier_id.sql b/backend/migrations/017_add_gemini_tier_id.sql index 0388a412..17d9440d 100644 --- a/backend/migrations/017_add_gemini_tier_id.sql +++ b/backend/migrations/017_add_gemini_tier_id.sql @@ -26,5 +26,10 @@ UPDATE accounts SET credentials = credentials - 'tier_id' WHERE platform = 'gemini' AND type = 'oauth' - AND credentials->>'oauth_type' = 'code_assist'; + AND jsonb_typeof(credentials) = 'object' + AND credentials->>'tier_id' = 'LEGACY' + AND ( + credentials->>'oauth_type' = 'code_assist' + OR (credentials->>'oauth_type' IS NULL AND credentials->>'project_id' IS NOT NULL) + ); -- +goose StatementEnd diff --git a/frontend/src/components/account/AccountQuotaInfo.vue b/frontend/src/components/account/AccountQuotaInfo.vue index 52f1bae7..c20d685d 100644 --- a/frontend/src/components/account/AccountQuotaInfo.vue +++ b/frontend/src/components/account/AccountQuotaInfo.vue @@ -10,7 +10,7 @@ v-if="!isRateLimited" class="text-xs text-gray-400 dark:text-gray-500" > - 未限流 + {{ t('admin.accounts.gemini.rateLimit.ok') }} - 限流 {{ resetCountdown }} + {{ t('admin.accounts.gemini.rateLimit.limited', { time: resetCountdown }) }}