diff --git a/backend/internal/pkg/response/response.go b/backend/internal/pkg/response/response.go index a92ff9e8..43fe12d4 100644 --- a/backend/internal/pkg/response/response.go +++ b/backend/internal/pkg/response/response.go @@ -162,11 +162,11 @@ func ParsePagination(c *gin.Context) (page, pageSize int) { // 支持 page_size 和 limit 两种参数名 if ps := c.Query("page_size"); ps != "" { - if val, err := parseInt(ps); err == nil && val > 0 && val <= 100 { + if val, err := parseInt(ps); err == nil && val > 0 && val <= 1000 { pageSize = val } } else if l := c.Query("limit"); l != "" { - if val, err := parseInt(l); err == nil && val > 0 && val <= 100 { + if val, err := parseInt(l); err == nil && val > 0 && val <= 1000 { pageSize = val } } diff --git a/backend/internal/repository/api_key_cache.go b/backend/internal/repository/api_key_cache.go index 6d834b40..a1072057 100644 --- a/backend/internal/repository/api_key_cache.go +++ b/backend/internal/repository/api_key_cache.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log" "time" "github.com/Wei-Shaw/sub2api/internal/service" @@ -12,9 +13,10 @@ import ( ) const ( - apiKeyRateLimitKeyPrefix = "apikey:ratelimit:" - apiKeyRateLimitDuration = 24 * time.Hour - apiKeyAuthCachePrefix = "apikey:auth:" + apiKeyRateLimitKeyPrefix = "apikey:ratelimit:" + apiKeyRateLimitDuration = 24 * time.Hour + apiKeyAuthCachePrefix = "apikey:auth:" + authCacheInvalidateChannel = "auth:cache:invalidate" ) // apiKeyRateLimitKey generates the Redis key for API key creation rate limiting. @@ -91,3 +93,45 @@ func (c *apiKeyCache) SetAuthCache(ctx context.Context, key string, entry *servi func (c *apiKeyCache) DeleteAuthCache(ctx context.Context, key string) error { return c.rdb.Del(ctx, apiKeyAuthCacheKey(key)).Err() } + +// PublishAuthCacheInvalidation publishes a cache invalidation message to all instances +func (c *apiKeyCache) PublishAuthCacheInvalidation(ctx context.Context, cacheKey string) error { + return c.rdb.Publish(ctx, authCacheInvalidateChannel, cacheKey).Err() +} + +// SubscribeAuthCacheInvalidation subscribes to cache invalidation messages +func (c *apiKeyCache) SubscribeAuthCacheInvalidation(ctx context.Context, handler func(cacheKey string)) error { + pubsub := c.rdb.Subscribe(ctx, authCacheInvalidateChannel) + + // Verify subscription is working + _, err := pubsub.Receive(ctx) + if err != nil { + _ = pubsub.Close() + return fmt.Errorf("subscribe to auth cache invalidation: %w", err) + } + + go func() { + defer func() { + if err := pubsub.Close(); err != nil { + log.Printf("Warning: failed to close auth cache invalidation pubsub: %v", err) + } + }() + + ch := pubsub.Channel() + for { + select { + case <-ctx.Done(): + return + case msg, ok := <-ch: + if !ok { + return + } + if msg != nil { + handler(msg.Payload) + } + } + } + }() + + return nil +} diff --git a/backend/internal/service/api_key_auth_cache_impl.go b/backend/internal/service/api_key_auth_cache_impl.go index 521f1da5..eb5c7534 100644 --- a/backend/internal/service/api_key_auth_cache_impl.go +++ b/backend/internal/service/api_key_auth_cache_impl.go @@ -94,6 +94,20 @@ func (s *APIKeyService) initAuthCache(cfg *config.Config) { s.authCacheL1 = cache } +// StartAuthCacheInvalidationSubscriber starts the Pub/Sub subscriber for L1 cache invalidation. +// This should be called after the service is fully initialized. +func (s *APIKeyService) StartAuthCacheInvalidationSubscriber(ctx context.Context) { + if s.cache == nil || s.authCacheL1 == nil { + return + } + if err := s.cache.SubscribeAuthCacheInvalidation(ctx, func(cacheKey string) { + s.authCacheL1.Del(cacheKey) + }); err != nil { + // Log but don't fail - L1 cache will still work, just without cross-instance invalidation + println("[Service] Warning: failed to start auth cache invalidation subscriber:", err.Error()) + } +} + func (s *APIKeyService) authCacheKey(key string) string { sum := sha256.Sum256([]byte(key)) return hex.EncodeToString(sum[:]) @@ -149,6 +163,8 @@ func (s *APIKeyService) deleteAuthCache(ctx context.Context, cacheKey string) { return } _ = s.cache.DeleteAuthCache(ctx, cacheKey) + // Publish invalidation message to other instances + _ = s.cache.PublishAuthCacheInvalidation(ctx, cacheKey) } func (s *APIKeyService) loadAuthCacheEntry(ctx context.Context, key, cacheKey string) (*APIKeyAuthCacheEntry, error) { diff --git a/backend/internal/service/api_key_service.go b/backend/internal/service/api_key_service.go index ecc570c7..ef1ff990 100644 --- a/backend/internal/service/api_key_service.go +++ b/backend/internal/service/api_key_service.go @@ -65,6 +65,10 @@ type APIKeyCache interface { GetAuthCache(ctx context.Context, key string) (*APIKeyAuthCacheEntry, error) SetAuthCache(ctx context.Context, key string, entry *APIKeyAuthCacheEntry, ttl time.Duration) error DeleteAuthCache(ctx context.Context, key string) error + + // Pub/Sub for L1 cache invalidation across instances + PublishAuthCacheInvalidation(ctx context.Context, cacheKey string) error + SubscribeAuthCacheInvalidation(ctx context.Context, handler func(cacheKey string)) error } // APIKeyAuthCacheInvalidator 提供认证缓存失效能力 diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go index acc0a5fb..99e69594 100644 --- a/backend/internal/service/wire.go +++ b/backend/internal/service/wire.go @@ -1,6 +1,7 @@ package service import ( + "context" "database/sql" "time" @@ -189,6 +190,8 @@ func ProvideOpsScheduledReportService( // ProvideAPIKeyAuthCacheInvalidator 提供 API Key 认证缓存失效能力 func ProvideAPIKeyAuthCacheInvalidator(apiKeyService *APIKeyService) APIKeyAuthCacheInvalidator { + // Start Pub/Sub subscriber for L1 cache invalidation across instances + apiKeyService.StartAuthCacheInvalidationSubscriber(context.Background()) return apiKeyService } diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index f9c33be5..16295803 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -1371,7 +1371,7 @@
- +

{{ t('admin.accounts.billingRateMultiplierHint') }}

diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 94c7e115..59c20a73 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -566,7 +566,7 @@
- +

{{ t('admin.accounts.billingRateMultiplierHint') }}

diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index 96457172..47a15084 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -243,7 +243,7 @@ />

{{ t('admin.groups.platformHint') }}

-
+

{{ t('admin.groups.platformNotEditable') }}

-
+
{ } } -// 监听 subscription_type 变化,订阅模式时重置 rate_multiplier 为 1,is_exclusive 为 true +// 监听 subscription_type 变化,订阅模式时 is_exclusive 默认为 true watch( () => createForm.subscription_type, (newVal) => { if (newVal === 'subscription') { - createForm.rate_multiplier = 1.0 createForm.is_exclusive = true } }