fix: 修复多个管理后台问题
- 分页接口 page_size 最大限制从 100 改为 1000 - 通过 Redis Pub/Sub 实现跨实例认证缓存失效 - 允许订阅类型分组编辑计费倍率 - 账号计费倍率支持 3 位小数
This commit is contained in:
@@ -162,11 +162,11 @@ func ParsePagination(c *gin.Context) (page, pageSize int) {
|
|||||||
|
|
||||||
// 支持 page_size 和 limit 两种参数名
|
// 支持 page_size 和 limit 两种参数名
|
||||||
if ps := c.Query("page_size"); ps != "" {
|
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
|
pageSize = val
|
||||||
}
|
}
|
||||||
} else if l := c.Query("limit"); l != "" {
|
} 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
|
pageSize = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
@@ -12,9 +13,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
apiKeyRateLimitKeyPrefix = "apikey:ratelimit:"
|
apiKeyRateLimitKeyPrefix = "apikey:ratelimit:"
|
||||||
apiKeyRateLimitDuration = 24 * time.Hour
|
apiKeyRateLimitDuration = 24 * time.Hour
|
||||||
apiKeyAuthCachePrefix = "apikey:auth:"
|
apiKeyAuthCachePrefix = "apikey:auth:"
|
||||||
|
authCacheInvalidateChannel = "auth:cache:invalidate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// apiKeyRateLimitKey generates the Redis key for API key creation rate limiting.
|
// 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 {
|
func (c *apiKeyCache) DeleteAuthCache(ctx context.Context, key string) error {
|
||||||
return c.rdb.Del(ctx, apiKeyAuthCacheKey(key)).Err()
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -94,6 +94,20 @@ func (s *APIKeyService) initAuthCache(cfg *config.Config) {
|
|||||||
s.authCacheL1 = cache
|
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 {
|
func (s *APIKeyService) authCacheKey(key string) string {
|
||||||
sum := sha256.Sum256([]byte(key))
|
sum := sha256.Sum256([]byte(key))
|
||||||
return hex.EncodeToString(sum[:])
|
return hex.EncodeToString(sum[:])
|
||||||
@@ -149,6 +163,8 @@ func (s *APIKeyService) deleteAuthCache(ctx context.Context, cacheKey string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = s.cache.DeleteAuthCache(ctx, cacheKey)
|
_ = 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) {
|
func (s *APIKeyService) loadAuthCacheEntry(ctx context.Context, key, cacheKey string) (*APIKeyAuthCacheEntry, error) {
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ type APIKeyCache interface {
|
|||||||
GetAuthCache(ctx context.Context, key string) (*APIKeyAuthCacheEntry, error)
|
GetAuthCache(ctx context.Context, key string) (*APIKeyAuthCacheEntry, error)
|
||||||
SetAuthCache(ctx context.Context, key string, entry *APIKeyAuthCacheEntry, ttl time.Duration) error
|
SetAuthCache(ctx context.Context, key string, entry *APIKeyAuthCacheEntry, ttl time.Duration) error
|
||||||
DeleteAuthCache(ctx context.Context, key string) 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 提供认证缓存失效能力
|
// APIKeyAuthCacheInvalidator 提供认证缓存失效能力
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -189,6 +190,8 @@ func ProvideOpsScheduledReportService(
|
|||||||
|
|
||||||
// ProvideAPIKeyAuthCacheInvalidator 提供 API Key 认证缓存失效能力
|
// ProvideAPIKeyAuthCacheInvalidator 提供 API Key 认证缓存失效能力
|
||||||
func ProvideAPIKeyAuthCacheInvalidator(apiKeyService *APIKeyService) APIKeyAuthCacheInvalidator {
|
func ProvideAPIKeyAuthCacheInvalidator(apiKeyService *APIKeyService) APIKeyAuthCacheInvalidator {
|
||||||
|
// Start Pub/Sub subscriber for L1 cache invalidation across instances
|
||||||
|
apiKeyService.StartAuthCacheInvalidationSubscriber(context.Background())
|
||||||
return apiKeyService
|
return apiKeyService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1371,7 +1371,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label">{{ t('admin.accounts.billingRateMultiplier') }}</label>
|
<label class="input-label">{{ t('admin.accounts.billingRateMultiplier') }}</label>
|
||||||
<input v-model.number="form.rate_multiplier" type="number" min="0" step="0.01" class="input" />
|
<input v-model.number="form.rate_multiplier" type="number" min="0" step="0.001" class="input" />
|
||||||
<p class="input-hint">{{ t('admin.accounts.billingRateMultiplierHint') }}</p>
|
<p class="input-hint">{{ t('admin.accounts.billingRateMultiplierHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -566,7 +566,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label">{{ t('admin.accounts.billingRateMultiplier') }}</label>
|
<label class="input-label">{{ t('admin.accounts.billingRateMultiplier') }}</label>
|
||||||
<input v-model.number="form.rate_multiplier" type="number" min="0" step="0.01" class="input" />
|
<input v-model.number="form.rate_multiplier" type="number" min="0" step="0.001" class="input" />
|
||||||
<p class="input-hint">{{ t('admin.accounts.billingRateMultiplierHint') }}</p>
|
<p class="input-hint">{{ t('admin.accounts.billingRateMultiplierHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -243,7 +243,7 @@
|
|||||||
/>
|
/>
|
||||||
<p class="input-hint">{{ t('admin.groups.platformHint') }}</p>
|
<p class="input-hint">{{ t('admin.groups.platformHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="createForm.subscription_type !== 'subscription'">
|
<div>
|
||||||
<label class="input-label">{{ t('admin.groups.form.rateMultiplier') }}</label>
|
<label class="input-label">{{ t('admin.groups.form.rateMultiplier') }}</label>
|
||||||
<input
|
<input
|
||||||
v-model.number="createForm.rate_multiplier"
|
v-model.number="createForm.rate_multiplier"
|
||||||
@@ -680,7 +680,7 @@
|
|||||||
/>
|
/>
|
||||||
<p class="input-hint">{{ t('admin.groups.platformNotEditable') }}</p>
|
<p class="input-hint">{{ t('admin.groups.platformNotEditable') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="editForm.subscription_type !== 'subscription'">
|
<div>
|
||||||
<label class="input-label">{{ t('admin.groups.form.rateMultiplier') }}</label>
|
<label class="input-label">{{ t('admin.groups.form.rateMultiplier') }}</label>
|
||||||
<input
|
<input
|
||||||
v-model.number="editForm.rate_multiplier"
|
v-model.number="editForm.rate_multiplier"
|
||||||
@@ -1605,12 +1605,11 @@ const confirmDelete = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听 subscription_type 变化,订阅模式时重置 rate_multiplier 为 1,is_exclusive 为 true
|
// 监听 subscription_type 变化,订阅模式时 is_exclusive 默认为 true
|
||||||
watch(
|
watch(
|
||||||
() => createForm.subscription_type,
|
() => createForm.subscription_type,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
if (newVal === 'subscription') {
|
if (newVal === 'subscription') {
|
||||||
createForm.rate_multiplier = 1.0
|
|
||||||
createForm.is_exclusive = true
|
createForm.is_exclusive = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user