Merge pull request #3546 from nslogx/fix/platform-quota-five-platforms

fix: allow five platform quota updates
This commit is contained in:
Wesley Liddick
2026-07-01 14:07:08 +08:00
committed by GitHub
7 changed files with 34 additions and 32 deletions
@@ -3812,7 +3812,7 @@ func slotOf(s *service.DefaultPlatformQuotaSetting, win string) *float64 {
return nil
}
// equalPlatformQuotaSettings reports whether two platform-quota maps are identical across all 12 slots.
// equalPlatformQuotaSettings reports whether two platform-quota maps are identical across all allowed slots.
func equalPlatformQuotaSettings(before, after map[string]*service.DefaultPlatformQuotaSetting) bool {
for _, platform := range service.AllowedQuotaPlatforms {
b := before[platform]
@@ -3,6 +3,7 @@ package admin
import (
"context"
"errors"
"fmt"
"log/slog"
"math"
"strconv"
@@ -629,8 +630,8 @@ func (h *UserHandler) UpdateUserPlatformQuotas(c *gin.Context) {
return
}
if len(req.Quotas) > 4 {
response.BadRequest(c, "quotas length must be <= 4")
if len(req.Quotas) > len(service.AllowedQuotaPlatforms) {
response.BadRequest(c, fmt.Sprintf("quotas length must be <= %d", len(service.AllowedQuotaPlatforms)))
return
}
seen := make(map[string]struct{}, len(req.Quotas))
@@ -749,7 +750,7 @@ func (h *UserHandler) UpdateUserPlatformQuotas(c *gin.Context) {
// 失效 cache:对全部允许的 platform 统一 invalidate。
// Trade-off:精确失效(仅 req 涉及平台 + 被软删平台)需 upsert 前额外 ListByUser
// 增加一次 DB 查询和逻辑复杂度。由于 AllowedQuotaPlatforms 只有 4 个元素
// 增加一次 DB 查询和逻辑复杂度。由于 AllowedQuotaPlatforms 数量很少
// 全量 invalidate 的额外开销可接受,且能可靠覆盖软删除场景。
if h.billingCache != nil {
for _, p := range service.AllowedQuotaPlatforms {
@@ -98,7 +98,10 @@ func TestUpdateUserPlatformQuotas_Success(t *testing.T) {
body := `{"quotas":[
{"platform":"anthropic","daily_limit_usd":10.0,"weekly_limit_usd":null,"monthly_limit_usd":100.0},
{"platform":"openai","daily_limit_usd":null,"weekly_limit_usd":null,"monthly_limit_usd":null}
{"platform":"openai","daily_limit_usd":80.0,"weekly_limit_usd":300.0,"monthly_limit_usd":null},
{"platform":"gemini","daily_limit_usd":null,"weekly_limit_usd":null,"monthly_limit_usd":null},
{"platform":"antigravity","daily_limit_usd":null,"weekly_limit_usd":null,"monthly_limit_usd":null},
{"platform":"grok","daily_limit_usd":null,"weekly_limit_usd":null,"monthly_limit_usd":null}
]}`
c, w := putReq(t, body)
h.UpdateUserPlatformQuotas(c)
@@ -109,10 +112,10 @@ func TestUpdateUserPlatformQuotas_Success(t *testing.T) {
if len(repo.upsertCalls) != 1 {
t.Fatalf("UpsertForUser should be called once, got %d", len(repo.upsertCalls))
}
if repo.upsertCalls[0].userID != 42 || len(repo.upsertCalls[0].records) != 2 {
if repo.upsertCalls[0].userID != 42 || len(repo.upsertCalls[0].records) != len(service.AllowedQuotaPlatforms) {
t.Errorf("unexpected upsert call: %+v", repo.upsertCalls[0])
}
// 缓存失效:请求中 2 个 platform + 软删除的 3 个 platformgemini, antigravity, grok= 5 次
// 缓存失效:按全部允许平台统一失效。
if len(cache.deleteCalls) != 5 {
t.Errorf("expected 5 cache delete calls, got %d: %+v", len(cache.deleteCalls), cache.deleteCalls)
}
@@ -154,7 +157,7 @@ func TestUpdateUserPlatformQuotas_RejectsNegativeLimit(t *testing.T) {
func TestUpdateUserPlatformQuotas_RejectsTooManyEntries(t *testing.T) {
h := buildTestHandler(&upsertCapturingQuotaRepo{}, &billingCacheStub{})
body := `{"quotas":[
{"platform":"anthropic"},{"platform":"openai"},{"platform":"gemini"},{"platform":"antigravity"},{"platform":"anthropic"}
{"platform":"anthropic"},{"platform":"openai"},{"platform":"gemini"},{"platform":"antigravity"},{"platform":"grok"},{"platform":"anthropic"}
]}`
c, w := putReq(t, body)
h.UpdateUserPlatformQuotas(c)
+2 -2
View File
@@ -802,7 +802,7 @@ func TestAPIContracts(t *testing.T) {
"force_email_on_third_party_signup": false,
"default_concurrency": 5,
"default_balance": 1.25,
"default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},"antigravity":{"daily":null,"weekly":null,"monthly":null},"gemini":{"daily":null,"weekly":null,"monthly":null},"openai":{"daily":null,"weekly":null,"monthly":null}},
"default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},"antigravity":{"daily":null,"weekly":null,"monthly":null},"gemini":{"daily":null,"weekly":null,"monthly":null},"grok":{"daily":null,"weekly":null,"monthly":null},"openai":{"daily":null,"weekly":null,"monthly":null}},
"auth_source_default_email_platform_quotas": null,
"auth_source_default_github_platform_quotas": null,
"auth_source_default_google_platform_quotas": null,
@@ -1050,7 +1050,7 @@ func TestAPIContracts(t *testing.T) {
"purchase_subscription_url": "",
"table_default_page_size": 20,
"table_page_size_options": [10, 20, 50],
"default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},"antigravity":{"daily":null,"weekly":null,"monthly":null},"gemini":{"daily":null,"weekly":null,"monthly":null},"openai":{"daily":null,"weekly":null,"monthly":null}},
"default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},"antigravity":{"daily":null,"weekly":null,"monthly":null},"gemini":{"daily":null,"weekly":null,"monthly":null},"grok":{"daily":null,"weekly":null,"monthly":null},"openai":{"daily":null,"weekly":null,"monthly":null}},
"auth_source_default_email_platform_quotas": null,
"auth_source_default_github_platform_quotas": null,
"auth_source_default_google_platform_quotas": null,
+1 -1
View File
@@ -1659,7 +1659,7 @@ func resolvedTokenVersion(user *User) int64 {
return user.TokenVersion ^ fingerprint
}
// snapshotPlatformQuotaDefaults 把 plan.PlatformQuotas4 platform × 3 window)以
// snapshotPlatformQuotaDefaults 把 plan.PlatformQuotasplatform × 3 window)以
// BulkInsertInitial 形式写入 user_platform_quotas 表。失败 fail-open(仅 warn log)。
func (s *AuthService) snapshotPlatformQuotaDefaults(ctx context.Context, userID int64, plan *signupGrantPlan) error {
if s.userPlatformQuotaRepo == nil || plan == nil || len(plan.PlatformQuotas) == 0 {
+7 -9
View File
@@ -5228,17 +5228,15 @@ func (s *SettingService) SetStreamTimeoutSettings(ctx context.Context, settings
return s.settingRepo.Set(ctx, SettingKeyStreamTimeoutSettings, string(data))
}
// GetDefaultPlatformQuotas 读取系统全局 platform quota JSON key,返回 4 platform x 3 window 的设置。
// 永远返回包含全部 4 platform key 的 map(值可能为零值/nil 字段,表示"上层未配置 = 不限制")。
// GetDefaultPlatformQuotas 读取系统全局 platform quota JSON key,返回全部允许平台 x 3 window 的设置。
// 永远返回包含全部允许 platform key 的 map(值可能为零值/nil 字段,表示"上层未配置 = 不限制")。
//
// 使用单个 JSON keydefault_platform_quotas),一次 DB roundtrip,消除旧 12-KV 格式的 N+1 问题。
// 容错语义:取值失败或 unmarshal 失败 → 返回补齐 4 key 的空 mapfail-open,注册不被阻断)。
// 容错语义:取值失败或 unmarshal 失败 → 返回补齐全部允许平台 key 的空 mapfail-open,注册不被阻断)。
func (s *SettingService) GetDefaultPlatformQuotas(ctx context.Context) (map[string]*DefaultPlatformQuotaSetting, error) {
out := map[string]*DefaultPlatformQuotaSetting{
"anthropic": {},
"openai": {},
"gemini": {},
"antigravity": {},
out := make(map[string]*DefaultPlatformQuotaSetting, len(AllowedQuotaPlatforms))
for _, platform := range AllowedQuotaPlatforms {
out[platform] = &DefaultPlatformQuotaSetting{}
}
raw, err := s.settingRepo.GetValue(ctx, SettingKeyDefaultPlatformQuotas)
if err != nil || raw == "" {
@@ -5254,7 +5252,7 @@ func (s *SettingService) GetDefaultPlatformQuotas(ctx context.Context) (map[stri
out[platform] = v
}
}
return out, nil // 补齐 4 platform key,保持与旧实现一致的下游契约
return out, nil // 补齐全部允许 platform key,保持与旧实现一致的下游契约
}
// GetAuthSourcePlatformQuotas 读取指定 auth source 的 platform quota 覆盖(仅返回有配置的平台,override 语义)。
@@ -60,18 +60,18 @@ func newSettingServiceForPlatformQuotaTest(seed map[string]string) *SettingServi
return NewSettingService(repo, &config.Config{})
}
func TestGetDefaultPlatformQuotas_ReturnsFourPlatforms(t *testing.T) {
func TestGetDefaultPlatformQuotas_ReturnsAllowedPlatforms(t *testing.T) {
zero := 0.0
svc := newSettingServiceForPlatformQuotaTest(map[string]string{
// 新 JSON 格式:anthropic daily=10.5, openai monthly=0, gemini/antigravity 无配置
// 新 JSON 格式:anthropic daily=10.5, openai monthly=0, 其他平台无配置
SettingKeyDefaultPlatformQuotas: `{"anthropic":{"daily":10.5},"openai":{"monthly":0}}`,
})
got, err := svc.GetDefaultPlatformQuotas(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// 必须包含全部 4 个 platform key(补齐契约)
for _, platform := range []string{"anthropic", "openai", "gemini", "antigravity"} {
// 必须包含全部允许 platform key(补齐契约)
for _, platform := range AllowedQuotaPlatforms {
if _, ok := got[platform]; !ok {
t.Errorf("missing platform key: %q", platform)
}
@@ -152,7 +152,7 @@ func TestGetAuthSourcePlatformQuotas_AllNegativeOrEmpty_NoEntry(t *testing.T) {
}
// TestSystemPlatformQuotas_WriteReadRoundTrip 验证系统层 platform quota 经 buildSystemSettingsUpdates(写)
// 再由 GetDefaultPlatformQuotas(读)正确往返——覆盖真实 write→read 路径锁住 4-key 补齐契约。
// 再由 GetDefaultPlatformQuotas(读)正确往返覆盖真实 write→read 路径锁住平台补齐契约。
func TestSystemPlatformQuotas_WriteReadRoundTrip(t *testing.T) {
svc := newSettingServiceForPlatformQuotaTest(nil)
ctx := context.Background()
@@ -171,10 +171,10 @@ func TestSystemPlatformQuotas_WriteReadRoundTrip(t *testing.T) {
if err != nil {
t.Fatal(err)
}
// 4-key 补齐契约:无论写了几个 platform,读回必须含全部 4 个
for _, p := range []string{"anthropic", "openai", "gemini", "antigravity"} {
// 平台补齐契约:无论写了几个 platform,读回必须含全部允许平台
for _, p := range AllowedQuotaPlatforms {
if _, ok := got[p]; !ok {
t.Errorf("4-key contract violated: missing platform %q", p)
t.Errorf("allowed-platform contract violated: missing platform %q", p)
}
}
// 写入值正确往返
@@ -188,7 +188,7 @@ func TestSystemPlatformQuotas_WriteReadRoundTrip(t *testing.T) {
}
// TestSystemPlatformQuotas_EmptyMapClearsAll 验证空 map 的整体替换语义:
// 写入 DefaultPlatformQuotas={} 后,GetDefaultPlatformQuotas 返回 4 个平台、所有字段均为 nil
// 写入 DefaultPlatformQuotas={} 后,GetDefaultPlatformQuotas 返回全部允许平台、所有字段均为 nil
// 明确文档化"空 map = 清空全部配额"是有意为之的 whole-replace 语义。
func TestSystemPlatformQuotas_EmptyMapClearsAll(t *testing.T) {
svc := newSettingServiceForPlatformQuotaTest(nil)
@@ -215,10 +215,10 @@ func TestSystemPlatformQuotas_EmptyMapClearsAll(t *testing.T) {
if err != nil {
t.Fatal(err)
}
// 4 个 key 仍然存在(补齐契约)
for _, p := range []string{"anthropic", "openai", "gemini", "antigravity"} {
// 全部允许平台 key 仍然存在(补齐契约)
for _, p := range AllowedQuotaPlatforms {
if _, ok := got[p]; !ok {
t.Errorf("4-key contract violated after empty write: missing %q", p)
t.Errorf("allowed-platform contract violated after empty write: missing %q", p)
}
}
// 所有字段 nil(全部已清空)