|
|
@@ -0,0 +1,116 @@
|
|
|
+package service
|
|
|
+
|
|
|
+import (
|
|
|
+ "fmt"
|
|
|
+ "one-api/common"
|
|
|
+ "one-api/constant"
|
|
|
+ "strconv"
|
|
|
+ "sync"
|
|
|
+ "time"
|
|
|
+)
|
|
|
+
|
|
|
+// notifyLimitStore is used for in-memory rate limiting when Redis is disabled
|
|
|
+var (
|
|
|
+ notifyLimitStore sync.Map
|
|
|
+ cleanupOnce sync.Once
|
|
|
+)
|
|
|
+
|
|
|
+type limitCount struct {
|
|
|
+ Count int
|
|
|
+ Timestamp time.Time
|
|
|
+}
|
|
|
+
|
|
|
+func getDuration() time.Duration {
|
|
|
+ minute := constant.NotificationLimitDurationMinute
|
|
|
+ return time.Duration(minute) * time.Minute
|
|
|
+}
|
|
|
+
|
|
|
+// startCleanupTask starts a background task to clean up expired entries
|
|
|
+func startCleanupTask() {
|
|
|
+ go func() {
|
|
|
+ for {
|
|
|
+ time.Sleep(time.Hour)
|
|
|
+ now := time.Now()
|
|
|
+ notifyLimitStore.Range(func(key, value interface{}) bool {
|
|
|
+ if limit, ok := value.(limitCount); ok {
|
|
|
+ if now.Sub(limit.Timestamp) >= getDuration() {
|
|
|
+ notifyLimitStore.Delete(key)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }()
|
|
|
+}
|
|
|
+
|
|
|
+// CheckNotificationLimit checks if the user has exceeded their notification limit
|
|
|
+// Returns true if the user can send notification, false if limit exceeded
|
|
|
+func CheckNotificationLimit(userId int, notifyType string) (bool, error) {
|
|
|
+ if common.RedisEnabled {
|
|
|
+ return checkRedisLimit(userId, notifyType)
|
|
|
+ }
|
|
|
+ return checkMemoryLimit(userId, notifyType)
|
|
|
+}
|
|
|
+
|
|
|
+func checkRedisLimit(userId int, notifyType string) (bool, error) {
|
|
|
+ key := fmt.Sprintf("notify_limit:%d:%s:%s", userId, notifyType, time.Now().Format("2006010215"))
|
|
|
+
|
|
|
+ // Get current count
|
|
|
+ count, err := common.RedisGet(key)
|
|
|
+ if err != nil && err.Error() != "redis: nil" {
|
|
|
+ return false, fmt.Errorf("failed to get notification count: %w", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // If key doesn't exist, initialize it
|
|
|
+ if count == "" {
|
|
|
+ err = common.RedisSet(key, "1", getDuration())
|
|
|
+ return true, err
|
|
|
+ }
|
|
|
+
|
|
|
+ currentCount, _ := strconv.Atoi(count)
|
|
|
+ limit := constant.DefaultNotifyHourlyLimit
|
|
|
+
|
|
|
+ // Check if limit is already reached
|
|
|
+ if currentCount >= limit {
|
|
|
+ return false, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // Only increment if under limit
|
|
|
+ err = common.RedisIncr(key, 1)
|
|
|
+ if err != nil {
|
|
|
+ return false, fmt.Errorf("failed to increment notification count: %w", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ return true, nil
|
|
|
+}
|
|
|
+
|
|
|
+func checkMemoryLimit(userId int, notifyType string) (bool, error) {
|
|
|
+ // Ensure cleanup task is started
|
|
|
+ cleanupOnce.Do(startCleanupTask)
|
|
|
+
|
|
|
+ key := fmt.Sprintf("%d:%s:%s", userId, notifyType, time.Now().Format("2006010215"))
|
|
|
+ now := time.Now()
|
|
|
+
|
|
|
+ // Get current limit count or initialize new one
|
|
|
+ var currentLimit limitCount
|
|
|
+ if value, ok := notifyLimitStore.Load(key); ok {
|
|
|
+ currentLimit = value.(limitCount)
|
|
|
+ // Check if the entry has expired
|
|
|
+ if now.Sub(currentLimit.Timestamp) >= getDuration() {
|
|
|
+ currentLimit = limitCount{Count: 0, Timestamp: now}
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ currentLimit = limitCount{Count: 0, Timestamp: now}
|
|
|
+ }
|
|
|
+
|
|
|
+ // Increment count
|
|
|
+ currentLimit.Count++
|
|
|
+
|
|
|
+ // Check against limits
|
|
|
+ limit := constant.DefaultNotifyHourlyLimit
|
|
|
+
|
|
|
+ // Store updated count
|
|
|
+ notifyLimitStore.Store(key, currentLimit)
|
|
|
+
|
|
|
+ return currentLimit.Count <= limit, nil
|
|
|
+}
|