From da810c3b43bf218b2a03fd32073b6139515f151f Mon Sep 17 00:00:00 2001 From: Pluviobyte Date: Sun, 28 Jun 2026 09:05:28 +0800 Subject: [PATCH] fix(keys): reactivate exhausted keys set to unlimited --- backend/internal/service/api_key_service.go | 4 ++-- .../service/api_key_service_delete_test.go | 7 +++++- .../service/api_key_service_quota_test.go | 24 +++++++++++++++++++ frontend/src/views/user/KeysView.vue | 18 ++++++++++---- 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/backend/internal/service/api_key_service.go b/backend/internal/service/api_key_service.go index f749b35a24..de9b908dc7 100644 --- a/backend/internal/service/api_key_service.go +++ b/backend/internal/service/api_key_service.go @@ -574,8 +574,8 @@ func (s *APIKeyService) Update(ctx context.Context, id int64, userID int64, req // Update quota fields if req.Quota != nil { apiKey.Quota = *req.Quota - // If quota is increased and status was quota_exhausted, reactivate - if apiKey.Status == StatusAPIKeyQuotaExhausted && *req.Quota > apiKey.QuotaUsed { + // If quota now has room, or is changed to unlimited, reactivate exhausted keys. + if apiKey.Status == StatusAPIKeyQuotaExhausted && (*req.Quota <= 0 || *req.Quota > apiKey.QuotaUsed) { apiKey.Status = StatusActive } } diff --git a/backend/internal/service/api_key_service_delete_test.go b/backend/internal/service/api_key_service_delete_test.go index 753752e8bc..8664c03bd7 100644 --- a/backend/internal/service/api_key_service_delete_test.go +++ b/backend/internal/service/api_key_service_delete_test.go @@ -27,7 +27,9 @@ type apiKeyRepoStub struct { apiKey *APIKey // GetKeyAndOwnerID 的返回值 getByIDErr error // GetKeyAndOwnerID 的错误返回值 deleteErr error // Delete 的错误返回值 + updateErr error // Update 的错误返回值 deletedIDs []int64 // 记录已删除的 API Key ID 列表 + updatedKeys []APIKey allowListByUserID bool listByUserIDKeys []APIKey listByUserIDErr error @@ -74,7 +76,10 @@ func (s *apiKeyRepoStub) GetByKeyForAuth(ctx context.Context, key string) (*APIK } func (s *apiKeyRepoStub) Update(ctx context.Context, key *APIKey) error { - panic("unexpected Update call") + if key != nil { + s.updatedKeys = append(s.updatedKeys, *key) + } + return s.updateErr } // Delete 记录被删除的 API Key ID 并返回预设的错误。 diff --git a/backend/internal/service/api_key_service_quota_test.go b/backend/internal/service/api_key_service_quota_test.go index 4d1d6f0060..45d1940146 100644 --- a/backend/internal/service/api_key_service_quota_test.go +++ b/backend/internal/service/api_key_service_quota_test.go @@ -174,3 +174,27 @@ func TestAPIKeyService_UpdateQuotaUsed_UsesAtomicStatePath(t *testing.T) { require.Equal(t, 0, repo.getByIDCalls, "fast path should not re-read API key by id") require.Equal(t, []string{svc.authCacheKey("sk-test-quota")}, cache.deleteAuthKeys) } + +func TestAPIKeyService_Update_ReactivatesQuotaExhaustedWhenQuotaUnlimited(t *testing.T) { + repo := &apiKeyRepoStub{ + apiKey: &APIKey{ + ID: 10, + UserID: 7, + Key: "sk-test-unlimited", + Status: StatusAPIKeyQuotaExhausted, + Quota: 10, + QuotaUsed: 12, + }, + } + svc := &APIKeyService{apiKeyRepo: repo} + quota := 0.0 + + updated, err := svc.Update(context.Background(), 10, 7, UpdateAPIKeyRequest{Quota: "a}) + + require.NoError(t, err) + require.Equal(t, StatusActive, updated.Status) + require.Equal(t, 0.0, updated.Quota) + require.Len(t, repo.updatedKeys, 1) + require.Equal(t, StatusActive, repo.updatedKeys[0].Status) + require.Equal(t, 0.0, repo.updatedKeys[0].Quota) +} diff --git a/frontend/src/views/user/KeysView.vue b/frontend/src/views/user/KeysView.vue index 7ca0a1c787..707a10c8ba 100644 --- a/frontend/src/views/user/KeysView.vue +++ b/frontend/src/views/user/KeysView.vue @@ -1068,7 +1068,7 @@ import TablePageLayout from '@/components/layout/TablePageLayout.vue' import EndpointPopover from '@/components/keys/EndpointPopover.vue' import GroupBadge from '@/components/common/GroupBadge.vue' import GroupOptionItem from '@/components/common/GroupOptionItem.vue' - import type { ApiKey, Group, PublicSettings, SubscriptionType, GroupPlatform } from '@/types' + import type { ApiKey, Group, PublicSettings, SubscriptionType, GroupPlatform, UpdateApiKeyRequest } from '@/types' import type { Column } from '@/components/common/types' import type { BatchApiKeyUsageStats } from '@/api/usage' import { formatDateTime } from '@/utils/format' @@ -1211,6 +1211,13 @@ const statusOptions = computed(() => [ { value: 'inactive', label: t('common.inactive') } ]) +const shouldSubmitEditStatus = (key: ApiKey, status: 'active' | 'inactive') => { + if (key.status === 'quota_exhausted' || key.status === 'expired') { + return status === 'active' + } + return true +} + // Filter dropdown options const groupFilterOptions = computed(() => [ { value: '', label: t('keys.allGroups') }, @@ -1542,10 +1549,9 @@ const handleSubmit = async () => { submitting.value = true try { if (showEditModal.value && selectedKey.value) { - await keysAPI.update(selectedKey.value.id, { + const updates: UpdateApiKeyRequest = { name: formData.value.name, group_id: formData.value.group_id, - status: formData.value.status, ip_whitelist: ipWhitelist, ip_blacklist: ipBlacklist, quota: quota, @@ -1553,7 +1559,11 @@ const handleSubmit = async () => { rate_limit_5h: rateLimitData.rate_limit_5h, rate_limit_1d: rateLimitData.rate_limit_1d, rate_limit_7d: rateLimitData.rate_limit_7d, - }) + } + if (shouldSubmitEditStatus(selectedKey.value, formData.value.status)) { + updates.status = formData.value.status + } + await keysAPI.update(selectedKey.value.id, updates) appStore.showSuccess(t('keys.keyUpdatedSuccess')) } else { const customKey = formData.value.use_custom_key ? formData.value.custom_key : undefined