From dec681fea07a312f7c037aa9fce782bd72f11f74 Mon Sep 17 00:00:00 2001 From: fawney19 Date: Mon, 5 Jan 2026 02:23:24 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E6=97=B6=E5=8C=BA?= =?UTF-8?q?=E5=A4=84=E7=90=86=EF=BC=8C=E7=A1=AE=E4=BF=9D=E6=89=80=E6=9C=89?= =?UTF-8?q?=20datetime=20=E5=B8=A6=E6=97=B6=E5=8C=BA=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - token_bucket.py: get_reset_time 和 Redis 后端使用 timezone.utc - sliding_window.py: get_reset_time 和 retry_after 计算使用 timezone.utc - provider_strategy.py: dateutil.parser 解析后确保有时区信息 --- src/api/admin/provider_strategy.py | 13 ++++++++++--- src/plugins/rate_limit/sliding_window.py | 8 ++++---- src/plugins/rate_limit/token_bucket.py | 10 +++++----- 3 files changed, 19 insertions(+), 12 deletions(-) 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