diff --git a/src/api/admin/provider_strategy.py b/src/api/admin/provider_strategy.py index 7b209e2..004f2f8 100644 --- a/src/api/admin/provider_strategy.py +++ b/src/api/admin/provider_strategy.py @@ -2,7 +2,7 @@ 提供商策略管理 API 端点 """ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request @@ -103,6 +103,9 @@ class AdminProviderBillingAdapter(AdminApiAdapter): if config.quota_last_reset_at: new_reset_at = parser.parse(config.quota_last_reset_at) + # 确保有时区信息,如果没有则假设为 UTC + if new_reset_at.tzinfo is None: + new_reset_at = new_reset_at.replace(tzinfo=timezone.utc) provider.quota_last_reset_at = new_reset_at # 自动同步该周期内的历史使用量 @@ -118,7 +121,11 @@ class AdminProviderBillingAdapter(AdminApiAdapter): logger.info(f"Synced usage for provider {provider.name}: ${period_usage:.4f} since {new_reset_at}") if config.quota_expires_at: - provider.quota_expires_at = parser.parse(config.quota_expires_at) + expires_at = parser.parse(config.quota_expires_at) + # 确保有时区信息,如果没有则假设为 UTC + if expires_at.tzinfo is None: + expires_at = expires_at.replace(tzinfo=timezone.utc) + provider.quota_expires_at = expires_at db.commit() db.refresh(provider) @@ -149,7 +156,7 @@ class AdminProviderStatsAdapter(AdminApiAdapter): if not provider: raise HTTPException(status_code=404, detail="Provider not found") - since = datetime.now() - timedelta(hours=self.hours) + since = datetime.now(timezone.utc) - timedelta(hours=self.hours) stats = ( db.query(ProviderUsageTracking) .filter( diff --git a/src/plugins/rate_limit/sliding_window.py b/src/plugins/rate_limit/sliding_window.py index bd76f08..3eca417 100644 --- a/src/plugins/rate_limit/sliding_window.py +++ b/src/plugins/rate_limit/sliding_window.py @@ -21,7 +21,7 @@ WARNING: 多进程环境注意事项 import asyncio import time from collections import deque -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Deque, Dict from src.core.logger import logger @@ -95,12 +95,12 @@ class SlidingWindow: """获取最早的重置时间""" self._cleanup() if not self.requests: - return datetime.now() + return datetime.now(timezone.utc) # 最早的请求将在window_size秒后过期 oldest_request = self.requests[0] reset_time = oldest_request + self.window_size - return datetime.fromtimestamp(reset_time) + return datetime.fromtimestamp(reset_time, tz=timezone.utc) class SlidingWindowStrategy(RateLimitStrategy): @@ -250,7 +250,7 @@ class SlidingWindowStrategy(RateLimitStrategy): retry_after = None if not allowed: # 计算需要等待的时间(最早请求过期的时间) - retry_after = int((reset_at - datetime.now()).total_seconds()) + 1 + retry_after = int((reset_at - datetime.now(timezone.utc)).total_seconds()) + 1 return RateLimitResult( allowed=allowed, diff --git a/src/plugins/rate_limit/token_bucket.py b/src/plugins/rate_limit/token_bucket.py index 664f3d6..b21d35d 100644 --- a/src/plugins/rate_limit/token_bucket.py +++ b/src/plugins/rate_limit/token_bucket.py @@ -3,7 +3,7 @@ import asyncio import os import time -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Any, Dict, Optional, Tuple from ...clients.redis_client import get_redis_client_sync @@ -63,11 +63,11 @@ class TokenBucket: def get_reset_time(self) -> datetime: """获取下次完全恢复的时间""" if self.tokens >= self.capacity: - return datetime.now() + return datetime.now(timezone.utc) tokens_needed = self.capacity - self.tokens seconds_to_full = tokens_needed / self.refill_rate - return datetime.now() + timedelta(seconds=seconds_to_full) + return datetime.now(timezone.utc) + timedelta(seconds=seconds_to_full) class TokenBucketStrategy(RateLimitStrategy): @@ -370,7 +370,7 @@ class RedisTokenBucketBackend: if tokens is None or last_refill is None: remaining = capacity - reset_at = datetime.now() + timedelta(seconds=capacity / refill_rate) + reset_at = datetime.now(timezone.utc) + timedelta(seconds=capacity / refill_rate) else: tokens_value = float(tokens) last_refill_value = float(last_refill) @@ -378,7 +378,7 @@ class RedisTokenBucketBackend: tokens_value = min(capacity, tokens_value + delta * refill_rate) remaining = int(tokens_value) reset_after = 0 if tokens_value >= capacity else (capacity - tokens_value) / refill_rate - reset_at = datetime.now() + timedelta(seconds=reset_after) + reset_at = datetime.now(timezone.utc) + timedelta(seconds=reset_after) allowed = remaining >= amount retry_after = None