diff --git a/backend/internal/handler/sora_client_handler_test.go b/backend/internal/handler/sora_client_handler_test.go
index d2a849b1cb..248df208aa 100644
--- a/backend/internal/handler/sora_client_handler_test.go
+++ b/backend/internal/handler/sora_client_handler_test.go
@@ -2132,7 +2132,7 @@ func (r *stubAccountRepoForHandler) BulkUpdate(context.Context, []int64, service
return 0, nil
}
-func (r *stubAccountRepoForHandler) IncrementQuotaUsed(context.Context, int64, float64) error {
+func (r *stubAccountRepoForHandler) IncrementQuotaUsed(context.Context, int64, float64, string) error {
return nil
}
diff --git a/backend/internal/handler/sora_gateway_handler_test.go b/backend/internal/handler/sora_gateway_handler_test.go
index 637462ad50..2d1fb0a97f 100644
--- a/backend/internal/handler/sora_gateway_handler_test.go
+++ b/backend/internal/handler/sora_gateway_handler_test.go
@@ -216,7 +216,7 @@ func (r *stubAccountRepo) BulkUpdate(ctx context.Context, ids []int64, updates s
return 0, nil
}
-func (r *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
+func (r *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error {
return nil
}
diff --git a/backend/internal/repository/account_repo.go b/backend/internal/repository/account_repo.go
index ffbfd46655..fdda231a87 100644
--- a/backend/internal/repository/account_repo.go
+++ b/backend/internal/repository/account_repo.go
@@ -1677,18 +1677,57 @@ func (r *accountRepository) FindByExtraField(ctx context.Context, key string, va
}
// IncrementQuotaUsed 原子递增账号的 extra.quota_used 字段
-func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
- rows, err := r.sql.QueryContext(ctx,
- `UPDATE accounts SET extra = jsonb_set(
- COALESCE(extra, '{}'::jsonb),
- '{quota_used}',
- to_jsonb(COALESCE((extra->>'quota_used')::numeric, 0) + $1)
- ), updated_at = NOW()
- WHERE id = $2 AND deleted_at IS NULL
- RETURNING
- COALESCE((extra->>'quota_used')::numeric, 0),
- COALESCE((extra->>'quota_limit')::numeric, 0)`,
- amount, id)
+// period 为 "daily"/"weekly" 时,使用 CTE 原子重置+递增;为空时保持累计制。
+func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, period string) error {
+ var rows *sql.Rows
+ var err error
+
+ interval := periodToInterval(period)
+ if interval == "" {
+ // 累计制:保持原有逻辑
+ rows, err = r.sql.QueryContext(ctx,
+ `UPDATE accounts SET extra = jsonb_set(
+ COALESCE(extra, '{}'::jsonb),
+ '{quota_used}',
+ to_jsonb(COALESCE((extra->>'quota_used')::numeric, 0) + $1)
+ ), updated_at = NOW()
+ WHERE id = $2 AND deleted_at IS NULL
+ RETURNING
+ COALESCE((extra->>'quota_used')::numeric, 0),
+ COALESCE((extra->>'quota_limit')::numeric, 0)`,
+ amount, id)
+ } else {
+ // 周期制:CTE 原子检查过期 → 重置 → 递增
+ rows, err = r.sql.QueryContext(ctx,
+ `WITH reset_check AS (
+ SELECT id, extra,
+ CASE WHEN COALESCE((extra->>'quota_period_start')::timestamptz, '1970-01-01'::timestamptz)
+ + $3::interval <= NOW()
+ THEN true ELSE false END AS needs_reset
+ FROM accounts WHERE id = $2 AND deleted_at IS NULL
+ )
+ UPDATE accounts SET extra = jsonb_set(
+ jsonb_set(
+ COALESCE(extra, '{}'::jsonb),
+ '{quota_used}',
+ to_jsonb(
+ CASE WHEN (SELECT needs_reset FROM reset_check) THEN $1
+ ELSE COALESCE((extra->>'quota_used')::numeric, 0) + $1 END
+ )
+ ),
+ '{quota_period_start}',
+ to_jsonb(
+ CASE WHEN (SELECT needs_reset FROM reset_check)
+ THEN to_char(NOW(), 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')
+ ELSE COALESCE(extra->>'quota_period_start', to_char(NOW(), 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')) END
+ )
+ ), updated_at = NOW()
+ WHERE id = $2 AND deleted_at IS NULL
+ RETURNING
+ COALESCE((extra->>'quota_used')::numeric, 0),
+ COALESCE((extra->>'quota_limit')::numeric, 0)`,
+ amount, id, interval)
+ }
if err != nil {
return err
}
@@ -1713,6 +1752,18 @@ func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, am
return nil
}
+// periodToInterval 将周期类型转为 PostgreSQL interval 字符串
+func periodToInterval(period string) string {
+ switch period {
+ case "daily":
+ return "24 hours"
+ case "weekly":
+ return "168 hours"
+ default:
+ return ""
+ }
+}
+
// ResetQuotaUsed 重置账号的 extra.quota_used 为 0
func (r *accountRepository) ResetQuotaUsed(ctx context.Context, id int64) error {
_, err := r.sql.ExecContext(ctx,
diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go
index aafbbe21cb..b6915943fc 100644
--- a/backend/internal/server/api_contract_test.go
+++ b/backend/internal/server/api_contract_test.go
@@ -1096,7 +1096,7 @@ func (s *stubAccountRepo) UpdateExtra(ctx context.Context, id int64, updates map
return errors.New("not implemented")
}
-func (s *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
+func (s *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error {
return errors.New("not implemented")
}
diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go
index 07459d8246..ce1b5c309d 100644
--- a/backend/internal/service/account.go
+++ b/backend/internal/service/account.go
@@ -1172,12 +1172,68 @@ func (a *Account) GetQuotaUsed() float64 {
return 0
}
+// GetQuotaPeriod 获取配额周期类型("daily"/"weekly",空字符串表示累计制)
+func (a *Account) GetQuotaPeriod() string {
+ if a.Extra == nil {
+ return ""
+ }
+ if v, ok := a.Extra["quota_period"]; ok {
+ if s, ok := v.(string); ok {
+ return s
+ }
+ }
+ return ""
+}
+
+// GetQuotaPeriodStart 获取当前周期的起始时间
+func (a *Account) GetQuotaPeriodStart() time.Time {
+ if a.Extra == nil {
+ return time.Time{}
+ }
+ if v, ok := a.Extra["quota_period_start"]; ok {
+ if s, ok := v.(string); ok {
+ if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
+ return t
+ }
+ if t, err := time.Parse(time.RFC3339, s); err == nil {
+ return t
+ }
+ }
+ }
+ return time.Time{}
+}
+
+// quotaPeriodDuration 返回周期长度
+func quotaPeriodDuration(period string) time.Duration {
+ switch period {
+ case "daily":
+ return 24 * time.Hour
+ case "weekly":
+ return 7 * 24 * time.Hour
+ default:
+ return 0
+ }
+}
+
+// isQuotaPeriodExpired 检查配额周期是否已过期
+func isQuotaPeriodExpired(period string, periodStart time.Time) bool {
+ dur := quotaPeriodDuration(period)
+ if dur == 0 || periodStart.IsZero() {
+ return false
+ }
+ return time.Since(periodStart) >= dur
+}
+
// IsQuotaExceeded 检查 API Key 账号配额是否已超限
func (a *Account) IsQuotaExceeded() bool {
limit := a.GetQuotaLimit()
if limit <= 0 {
return false
}
+ // 周期性配额:如果当前周期已过期,视为未超限(下次 increment 时会自动重置)
+ if isQuotaPeriodExpired(a.GetQuotaPeriod(), a.GetQuotaPeriodStart()) {
+ return false
+ }
return a.GetQuotaUsed() >= limit
}
diff --git a/backend/internal/service/account_service.go b/backend/internal/service/account_service.go
index 26c0b1c29a..85c1398cf3 100644
--- a/backend/internal/service/account_service.go
+++ b/backend/internal/service/account_service.go
@@ -69,7 +69,7 @@ type AccountRepository interface {
UpdateExtra(ctx context.Context, id int64, updates map[string]any) error
BulkUpdate(ctx context.Context, ids []int64, updates AccountBulkUpdate) (int64, error)
// IncrementQuotaUsed 原子递增 API Key 账号的配额用量
- IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error
+ IncrementQuotaUsed(ctx context.Context, id int64, amount float64, period string) error
// ResetQuotaUsed 重置 API Key 账号的配额用量为 0
ResetQuotaUsed(ctx context.Context, id int64) error
}
diff --git a/backend/internal/service/account_service_delete_test.go b/backend/internal/service/account_service_delete_test.go
index c96b436f02..bfb57bdb44 100644
--- a/backend/internal/service/account_service_delete_test.go
+++ b/backend/internal/service/account_service_delete_test.go
@@ -199,7 +199,7 @@ func (s *accountRepoStub) BulkUpdate(ctx context.Context, ids []int64, updates A
panic("unexpected BulkUpdate call")
}
-func (s *accountRepoStub) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
+func (s *accountRepoStub) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error {
return nil
}
diff --git a/backend/internal/service/gateway_multiplatform_test.go b/backend/internal/service/gateway_multiplatform_test.go
index 466ff294bb..6f800f3387 100644
--- a/backend/internal/service/gateway_multiplatform_test.go
+++ b/backend/internal/service/gateway_multiplatform_test.go
@@ -187,7 +187,7 @@ func (m *mockAccountRepoForPlatform) BulkUpdate(ctx context.Context, ids []int64
return 0, nil
}
-func (m *mockAccountRepoForPlatform) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
+func (m *mockAccountRepoForPlatform) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error {
return nil
}
diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go
index 0fe2545ec0..73b726c843 100644
--- a/backend/internal/service/gateway_service.go
+++ b/backend/internal/service/gateway_service.go
@@ -6585,7 +6585,8 @@ func postUsageBilling(ctx context.Context, p *postUsageBillingParams, deps *bill
// 4. 账号配额用量(账号口径:TotalCost × 账号计费倍率)
if cost.TotalCost > 0 && p.Account.Type == AccountTypeAPIKey && p.Account.GetQuotaLimit() > 0 {
accountCost := cost.TotalCost * p.AccountRateMultiplier
- if err := deps.accountRepo.IncrementQuotaUsed(ctx, p.Account.ID, accountCost); err != nil {
+ period := p.Account.GetQuotaPeriod()
+ if err := deps.accountRepo.IncrementQuotaUsed(ctx, p.Account.ID, accountCost, period); err != nil {
slog.Error("increment account quota used failed", "account_id", p.Account.ID, "cost", accountCost, "error", err)
}
}
diff --git a/backend/internal/service/gemini_multiplatform_test.go b/backend/internal/service/gemini_multiplatform_test.go
index 56f94d5f1f..5a9dc69a14 100644
--- a/backend/internal/service/gemini_multiplatform_test.go
+++ b/backend/internal/service/gemini_multiplatform_test.go
@@ -176,7 +176,7 @@ func (m *mockAccountRepoForGemini) BulkUpdate(ctx context.Context, ids []int64,
return 0, nil
}
-func (m *mockAccountRepoForGemini) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
+func (m *mockAccountRepoForGemini) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error {
return nil
}
diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue
index 92725c0788..c50712ff10 100644
--- a/frontend/src/components/account/CreateAccountModal.vue
+++ b/frontend/src/components/account/CreateAccountModal.vue
@@ -1228,7 +1228,7 @@
-
{{ t('admin.accounts.quotaLimitAmountHint') }}
+ +{{ t('admin.accounts.quotaPeriodHint') }}
+