diff --git a/app/auth.py b/app/auth.py index 7f7c884..985a281 100644 --- a/app/auth.py +++ b/app/auth.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import UTC, datetime, timedelta +from datetime import timedelta import hashlib import re import secrets @@ -12,6 +12,7 @@ from app.database import ( User, ) from app.log import logger +from app.utils import utcnow import bcrypt from jose import JWTError, jwt @@ -150,9 +151,9 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None) -> s """创建访问令牌""" to_encode = data.copy() if expires_delta: - expire = datetime.now(UTC) + expires_delta + expire = utcnow() + expires_delta else: - expire = datetime.now(UTC) + timedelta(minutes=settings.access_token_expire_minutes) + expire = utcnow() + timedelta(minutes=settings.access_token_expire_minutes) to_encode.update({"exp": expire, "random": secrets.token_hex(16)}) encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm) @@ -206,7 +207,7 @@ async def store_token( expires_in: int, ) -> OAuthToken: """存储令牌到数据库""" - expires_at = datetime.utcnow() + timedelta(seconds=expires_in) + expires_at = utcnow() + timedelta(seconds=expires_in) # 删除用户的旧令牌 statement = select(OAuthToken).where(OAuthToken.user_id == user_id, OAuthToken.client_id == client_id) @@ -238,7 +239,7 @@ async def get_token_by_access_token(db: AsyncSession, access_token: str) -> OAut """根据访问令牌获取令牌记录""" statement = select(OAuthToken).where( OAuthToken.access_token == access_token, - OAuthToken.expires_at > datetime.utcnow(), + OAuthToken.expires_at > utcnow(), ) return (await db.exec(statement)).first() @@ -247,7 +248,7 @@ async def get_token_by_refresh_token(db: AsyncSession, refresh_token: str) -> OA """根据刷新令牌获取令牌记录""" statement = select(OAuthToken).where( OAuthToken.refresh_token == refresh_token, - OAuthToken.expires_at > datetime.utcnow(), + OAuthToken.expires_at > utcnow(), ) return (await db.exec(statement)).first() diff --git a/app/database/achievement.py b/app/database/achievement.py index 9c048ab..fcaacf4 100644 --- a/app/database/achievement.py +++ b/app/database/achievement.py @@ -1,10 +1,11 @@ -from datetime import UTC, datetime +from datetime import datetime from typing import TYPE_CHECKING from app.config import settings from app.models.achievement import MEDALS, Achievement from app.models.model import UTCBaseModel from app.models.notification import UserAchievementUnlock +from app.utils import utcnow from .events import Event, EventType @@ -28,7 +29,7 @@ if TYPE_CHECKING: class UserAchievementBase(SQLModel, UTCBaseModel): achievement_id: int - achieved_at: datetime = Field(default=datetime.now(UTC), sa_column=Column(DateTime(timezone=True))) + achieved_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime(timezone=True))) class UserAchievement(UserAchievementBase, table=True): @@ -56,7 +57,7 @@ async def process_achievements(session: AsyncSession, redis: Redis, score_id: in ).all() not_achieved = {k: v for k, v in MEDALS.items() if k.id not in achieved} result: list[Achievement] = [] - now = datetime.now(UTC) + now = utcnow() for k, v in not_achieved.items(): if await v(session, score, score.beatmap): result.append(k) diff --git a/app/database/auth.py b/app/database/auth.py index 74bee24..fecb3ce 100644 --- a/app/database/auth.py +++ b/app/database/auth.py @@ -3,6 +3,7 @@ import secrets from typing import TYPE_CHECKING from app.models.model import UTCBaseModel +from app.utils import utcnow from sqlalchemy import Column, DateTime from sqlmodel import ( @@ -30,7 +31,7 @@ class OAuthToken(UTCBaseModel, SQLModel, table=True): token_type: str = Field(default="Bearer", max_length=20) scope: str = Field(default="*", max_length=100) expires_at: datetime = Field(sa_column=Column(DateTime)) - created_at: datetime = Field(default_factory=datetime.utcnow, sa_column=Column(DateTime)) + created_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime)) user: "User" = Relationship() diff --git a/app/database/beatmap_playcounts.py b/app/database/beatmap_playcounts.py index 0917d5a..ffdb724 100644 --- a/app/database/beatmap_playcounts.py +++ b/app/database/beatmap_playcounts.py @@ -1,7 +1,7 @@ -from datetime import UTC, datetime from typing import TYPE_CHECKING from app.database.events import Event, EventType +from app.utils import utcnow from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncAttrs @@ -71,7 +71,7 @@ async def process_beatmap_playcount(session: AsyncSession, user_id: int, beatmap existing_playcount.playcount += 1 if existing_playcount.playcount % 100 == 0: playcount_event = Event( - created_at=datetime.now(UTC), + created_at=utcnow(), type=EventType.BEATMAP_PLAYCOUNT, user_id=user_id, ) diff --git a/app/database/chat.py b/app/database/chat.py index a1a66d8..a56c6e4 100644 --- a/app/database/chat.py +++ b/app/database/chat.py @@ -1,9 +1,10 @@ -from datetime import UTC, datetime +from datetime import datetime from enum import Enum from typing import Self from app.database.lazer_user import RANKING_INCLUDES, User, UserResp from app.models.model import UTCBaseModel +from app.utils import utcnow from pydantic import BaseModel from redis.asyncio import Redis @@ -170,7 +171,7 @@ class ChatMessageBase(UTCBaseModel, SQLModel): content: str = Field(sa_column=Column(VARCHAR(1000))) message_id: int = Field(index=True, primary_key=True, default=None) sender_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True)) - timestamp: datetime = Field(sa_column=Column(DateTime, index=True), default=datetime.now(UTC)) + timestamp: datetime = Field(sa_column=Column(DateTime, index=True), default_factory=utcnow) type: MessageType = Field(default=MessageType.PLAIN, index=True, exclude=True) uuid: str | None = Field(default=None) @@ -208,7 +209,7 @@ class SilenceUser(UTCBaseModel, SQLModel, table=True): channel_id: int = Field(foreign_key="chat_channels.channel_id", index=True) until: datetime | None = Field(sa_column=Column(DateTime, index=True), default=None) reason: str | None = Field(default=None, sa_column=Column(VARCHAR(255), index=True)) - banned_at: datetime = Field(sa_column=Column(DateTime, index=True), default=datetime.now(UTC)) + banned_at: datetime = Field(sa_column=Column(DateTime, index=True), default_factory=utcnow) class UserSilenceResp(SQLModel): diff --git a/app/database/daily_challenge.py b/app/database/daily_challenge.py index 071c606..35127fa 100644 --- a/app/database/daily_challenge.py +++ b/app/database/daily_challenge.py @@ -2,7 +2,7 @@ from datetime import UTC, datetime, timedelta from typing import TYPE_CHECKING from app.models.model import UTCBaseModel -from app.utils import are_adjacent_weeks +from app.utils import are_adjacent_weeks, utcnow from sqlmodel import ( BigInteger, @@ -79,7 +79,7 @@ async def process_daily_challenge_score(session: AsyncSession, user_id: int, roo session.add(stats) stats.playcount += 1 - now = datetime.now(UTC) + now = utcnow() if stats.last_update is None: stats.daily_streak_best = 1 stats.daily_streak_current = 1 diff --git a/app/database/email_verification.py b/app/database/email_verification.py index 07d57a5..2fd65a9 100644 --- a/app/database/email_verification.py +++ b/app/database/email_verification.py @@ -4,7 +4,9 @@ from __future__ import annotations -from datetime import UTC, datetime +from datetime import datetime + +from app.utils import utcnow from sqlalchemy import BigInteger, Column, ForeignKey from sqlmodel import Field, SQLModel @@ -19,7 +21,7 @@ class EmailVerification(SQLModel, table=True): user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), nullable=False, index=True)) email: str = Field(index=True) verification_code: str = Field(max_length=8) # 8位验证码 - created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) + created_at: datetime = Field(default_factory=utcnow) expires_at: datetime = Field() # 验证码过期时间 is_used: bool = Field(default=False) # 是否已使用 used_at: datetime | None = Field(default=None) @@ -39,7 +41,7 @@ class LoginSession(SQLModel, table=True): user_agent: str | None = Field(default=None, max_length=250) country_code: str | None = Field(default=None) is_verified: bool = Field(default=False) # 是否已验证 - created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) + created_at: datetime = Field(default_factory=lambda: utcnow()) verified_at: datetime | None = Field(default=None) expires_at: datetime = Field() # 会话过期时间 is_new_location: bool = Field(default=False) # 是否新位置登录 diff --git a/app/database/events.py b/app/database/events.py index 19ac878..fa9edfe 100644 --- a/app/database/events.py +++ b/app/database/events.py @@ -3,6 +3,7 @@ from enum import Enum from typing import TYPE_CHECKING from app.models.model import UTCBaseModel +from app.utils import utcnow from pydantic import model_serializer from sqlmodel import ( @@ -40,7 +41,7 @@ class EventType(str, Enum): class Event(UTCBaseModel, SQLModel, table=True): __tablename__: str = "user_events" id: int = Field(default=None, primary_key=True) - created_at: datetime = Field(sa_column=Column(DateTime(timezone=True), default=datetime.now(UTC))) + created_at: datetime = Field(sa_column=Column(DateTime(timezone=True), default_factory=utcnow)) type: EventType event_payload: dict = Field(exclude=True, default_factory=dict, sa_column=Column(JSON)) user_id: int | None = Field( diff --git a/app/database/lazer_user.py b/app/database/lazer_user.py index 8132137..c6e2b98 100644 --- a/app/database/lazer_user.py +++ b/app/database/lazer_user.py @@ -1,4 +1,4 @@ -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta import json from typing import TYPE_CHECKING, NotRequired, TypedDict @@ -6,6 +6,7 @@ from app.models.model import UTCBaseModel from app.models.score import GameMode from app.models.user import Country, Page from app.path import STATIC_DIR +from app.utils import utcnow from .achievement import UserAchievement, UserAchievementResp from .beatmap_playcounts import BeatmapPlaycounts @@ -75,7 +76,7 @@ class UserBase(UTCBaseModel, SQLModel): is_active: bool = True is_bot: bool = False is_supporter: bool = False - last_visit: datetime | None = Field(default=datetime.now(UTC), sa_column=Column(DateTime(timezone=True))) + last_visit: datetime | None = Field(default_factory=utcnow, sa_column=Column(DateTime(timezone=True))) pm_friends_only: bool = False profile_colour: str | None = None username: str = Field(max_length=32, unique=True, index=True) @@ -99,7 +100,7 @@ class UserBase(UTCBaseModel, SQLModel): discord: str | None = None has_supported: bool = False interests: str | None = None - join_date: datetime = Field(default=datetime.now(UTC)) + join_date: datetime = Field(default_factory=utcnow) location: str | None = None max_blocks: int = 50 max_friends: int = 500 @@ -408,7 +409,7 @@ class UserResp(UserBase): Score.user_id == obj.id, Score.gamemode == ruleset, col(Score.passed).is_(True), - Score.ended_at > datetime.now(UTC) - timedelta(hours=24), + Score.ended_at > utcnow() - timedelta(hours=24), ) ) ).one() @@ -437,7 +438,7 @@ class UserResp(UserBase): select(LoginSession).where( LoginSession.user_id == obj.id, col(LoginSession.is_verified).is_(False), - LoginSession.expires_at > datetime.now(UTC), + LoginSession.expires_at > utcnow(), ) ) ).first() diff --git a/app/database/multiplayer_event.py b/app/database/multiplayer_event.py index ce3f024..90d4fe5 100644 --- a/app/database/multiplayer_event.py +++ b/app/database/multiplayer_event.py @@ -1,7 +1,8 @@ -from datetime import UTC, datetime +from datetime import datetime from typing import Any from app.models.model import UTCBaseModel +from app.utils import utcnow from sqlmodel import ( JSON, @@ -24,7 +25,7 @@ class MultiplayerEventBase(SQLModel, UTCBaseModel): sa_column=Column( DateTime(timezone=True), ), - default=datetime.now(UTC), + default_factory=utcnow, ) event_type: str = Field(index=True) @@ -40,7 +41,7 @@ class MultiplayerEvent(MultiplayerEventBase, table=True): sa_column=Column( DateTime(timezone=True), ), - default=datetime.now(UTC), + default_factory=utcnow, ) event_detail: dict[str, Any] | None = Field( sa_column=Column(JSON), diff --git a/app/database/notification.py b/app/database/notification.py index 6cb44fb..b770ddd 100644 --- a/app/database/notification.py +++ b/app/database/notification.py @@ -1,7 +1,8 @@ -from datetime import UTC, datetime +from datetime import datetime from typing import Any from app.models.notification import NotificationDetail, NotificationName +from app.utils import utcnow from sqlmodel import ( JSON, @@ -54,7 +55,7 @@ async def insert_notification(session: AsyncSession, detail: NotificationDetail) object_id=detail.object_id, source_user_id=detail.source_user_id, details=detail.model_dump(), - created_at=datetime.now(UTC), + created_at=utcnow(), ) session.add(notification) await session.commit() diff --git a/app/database/password_reset.py b/app/database/password_reset.py index 51651ce..2e92f0a 100644 --- a/app/database/password_reset.py +++ b/app/database/password_reset.py @@ -4,7 +4,9 @@ from __future__ import annotations -from datetime import UTC, datetime +from datetime import datetime + +from app.utils import utcnow from sqlalchemy import BigInteger, Column, ForeignKey from sqlmodel import Field, SQLModel @@ -19,7 +21,7 @@ class PasswordReset(SQLModel, table=True): user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), nullable=False, index=True)) email: str = Field(index=True) reset_code: str = Field(max_length=8) # 8位重置验证码 - created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) + created_at: datetime = Field(default_factory=utcnow) expires_at: datetime = Field() # 验证码过期时间 is_used: bool = Field(default=False) # 是否已使用 used_at: datetime | None = Field(default=None) diff --git a/app/database/rank_history.py b/app/database/rank_history.py index 81caf22..8124501 100644 --- a/app/database/rank_history.py +++ b/app/database/rank_history.py @@ -1,11 +1,10 @@ from datetime import ( - UTC, date as dt, - datetime, ) from typing import TYPE_CHECKING, Optional from app.models.score import GameMode +from app.utils import utcnow from pydantic import BaseModel from sqlmodel import ( @@ -33,7 +32,7 @@ class RankHistory(SQLModel, table=True): mode: GameMode rank: int date: dt = Field( - default_factory=lambda: datetime.now(UTC).date(), + default_factory=lambda: utcnow().date(), sa_column=Column(Date, index=True), ) @@ -48,7 +47,7 @@ class RankTop(SQLModel, table=True): mode: GameMode rank: int date: dt = Field( - default_factory=lambda: datetime.now(UTC).date(), + default_factory=lambda: utcnow().date(), sa_column=Column(Date, index=True), ) diff --git a/app/database/room.py b/app/database/room.py index 491b55b..c5ed5ec 100644 --- a/app/database/room.py +++ b/app/database/room.py @@ -1,4 +1,4 @@ -from datetime import UTC, datetime +from datetime import datetime from app.database.playlist_attempts import PlaylistAggregateScore from app.database.room_participated_user import RoomParticipatedUser @@ -12,6 +12,7 @@ from app.models.room import ( RoomPlaylistItemStats, RoomStatus, ) +from app.utils import utcnow from .lazer_user import User, UserResp from .playlists import Playlist, PlaylistResp @@ -39,7 +40,7 @@ class RoomBase(SQLModel, UTCBaseModel): sa_column=Column( DateTime(timezone=True), ), - default=datetime.now(UTC), + default_factory=utcnow, ) ends_at: datetime | None = Field( sa_column=Column( diff --git a/app/database/room_participated_user.py b/app/database/room_participated_user.py index 3779516..4ef6526 100644 --- a/app/database/room_participated_user.py +++ b/app/database/room_participated_user.py @@ -1,6 +1,8 @@ -from datetime import UTC, datetime +from datetime import datetime from typing import TYPE_CHECKING +from app.utils import utcnow + from sqlalchemy.ext.asyncio import AsyncAttrs from sqlmodel import ( BigInteger, @@ -25,7 +27,7 @@ class RoomParticipatedUser(AsyncAttrs, SQLModel, table=True): user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), nullable=False)) joined_at: datetime = Field( sa_column=Column(DateTime(timezone=True), nullable=False), - default=datetime.now(UTC), + default_factory=utcnow, ) left_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True), nullable=True), default=None) diff --git a/app/database/score.py b/app/database/score.py index 66e1cc2..96351f1 100644 --- a/app/database/score.py +++ b/app/database/score.py @@ -1,5 +1,5 @@ from collections.abc import Sequence -from datetime import UTC, date, datetime +from datetime import date, datetime import json import math import sys @@ -31,6 +31,7 @@ from app.models.score import ( ScoreStatistics, SoloScoreSubmissionInfo, ) +from app.utils import utcnow from .beatmap import Beatmap, BeatmapResp from .beatmap_playcounts import process_beatmap_playcount @@ -733,7 +734,7 @@ async def process_user( if i < score_range and displaced_position > score_range and displaced_position is not None: # Create rank lost event for the displaced user rank_lost_event = Event( - created_at=datetime.now(UTC), + created_at=utcnow(), type=EventType.RANK_LOST, user_id=displaced_score.user_id, ) @@ -843,7 +844,7 @@ async def process_score( total_score=info.total_score, total_score_without_mods=info.total_score_without_mods, beatmap_id=beatmap_id, - ended_at=datetime.now(UTC), + ended_at=utcnow(), gamemode=gamemode, started_at=score_token.created_at, user_id=user.id, diff --git a/app/database/score_token.py b/app/database/score_token.py index ac6c9a9..9086ef3 100644 --- a/app/database/score_token.py +++ b/app/database/score_token.py @@ -2,6 +2,7 @@ from datetime import datetime from app.models.model import UTCBaseModel from app.models.score import GameMode +from app.utils import utcnow from .beatmap import Beatmap from .lazer_user import User @@ -15,8 +16,8 @@ class ScoreTokenBase(SQLModel, UTCBaseModel): score_id: int | None = Field(sa_column=Column(BigInteger), default=None) ruleset_id: GameMode playlist_item_id: int | None = Field(default=None) # playlist - created_at: datetime = Field(default_factory=datetime.utcnow, sa_column=Column(DateTime)) - updated_at: datetime = Field(default_factory=datetime.utcnow, sa_column=Column(DateTime)) + created_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime)) + updated_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime)) class ScoreToken(ScoreTokenBase, table=True): diff --git a/app/database/statistics.py b/app/database/statistics.py index 0422138..55a6200 100644 --- a/app/database/statistics.py +++ b/app/database/statistics.py @@ -1,8 +1,9 @@ -from datetime import UTC, datetime, timedelta +from datetime import timedelta import math from typing import TYPE_CHECKING from app.models.score import GameMode +from app.utils import utcnow from .rank_history import RankHistory @@ -134,7 +135,7 @@ class UserStatisticsResp(UserStatisticsBase): rank_best = ( await session.exec( select(func.max(RankHistory.rank)).where( - RankHistory.date > datetime.now(UTC) - timedelta(days=30), + RankHistory.date > utcnow() - timedelta(days=30), RankHistory.user_id == obj.user_id, ) ) @@ -171,7 +172,7 @@ async def get_rank(session: AsyncSession, statistics: UserStatistics, country: s return None if country is None: - today = datetime.now(UTC).date() + today = utcnow().date() rank_history = ( await session.exec( select(RankHistory).where( diff --git a/app/database/team.py b/app/database/team.py index e60b548..afdc42c 100644 --- a/app/database/team.py +++ b/app/database/team.py @@ -1,7 +1,8 @@ -from datetime import UTC, datetime +from datetime import datetime from typing import TYPE_CHECKING from app.models.model import UTCBaseModel +from app.utils import utcnow from sqlalchemy import Column, DateTime from sqlmodel import BigInteger, Field, ForeignKey, Relationship, SQLModel @@ -18,7 +19,7 @@ class Team(SQLModel, UTCBaseModel, table=True): short_name: str = Field(max_length=10) flag_url: str | None = Field(default=None) cover_url: str | None = Field(default=None) - created_at: datetime = Field(default=datetime.now(UTC), sa_column=Column(DateTime)) + created_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime)) leader_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"))) leader: "User" = Relationship() @@ -30,7 +31,7 @@ class TeamMember(SQLModel, UTCBaseModel, table=True): user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), primary_key=True)) team_id: int = Field(foreign_key="teams.id") - joined_at: datetime = Field(default_factory=datetime.utcnow, sa_column=Column(DateTime)) + joined_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime)) user: "User" = Relationship(back_populates="team_membership", sa_relationship_kwargs={"lazy": "joined"}) team: "Team" = Relationship(back_populates="members", sa_relationship_kwargs={"lazy": "joined"}) @@ -41,7 +42,7 @@ class TeamRequest(SQLModel, UTCBaseModel, table=True): user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), primary_key=True)) team_id: int = Field(foreign_key="teams.id", primary_key=True) - requested_at: datetime = Field(default=datetime.now(UTC), sa_column=Column(DateTime)) + requested_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime)) user: "User" = Relationship(sa_relationship_kwargs={"lazy": "joined"}) team: "Team" = Relationship(sa_relationship_kwargs={"lazy": "joined"}) diff --git a/app/database/user_account_history.py b/app/database/user_account_history.py index e09c209..c47d594 100644 --- a/app/database/user_account_history.py +++ b/app/database/user_account_history.py @@ -1,7 +1,8 @@ -from datetime import UTC, datetime +from datetime import datetime from enum import Enum from app.models.model import UTCBaseModel +from app.utils import utcnow from sqlmodel import BigInteger, Column, Field, ForeignKey, Integer, SQLModel @@ -17,7 +18,7 @@ class UserAccountHistoryBase(SQLModel, UTCBaseModel): description: str | None = None length: int permanent: bool = False - timestamp: datetime = Field(default=datetime.now(UTC)) + timestamp: datetime = Field(default_factory=utcnow) type: UserAccountHistoryType diff --git a/app/database/user_login_log.py b/app/database/user_login_log.py index 7ebe2c9..d789d5b 100644 --- a/app/database/user_login_log.py +++ b/app/database/user_login_log.py @@ -4,6 +4,8 @@ User Login Log Database Model from datetime import datetime +from app.utils import utcnow + from sqlmodel import Field, SQLModel @@ -16,7 +18,7 @@ class UserLoginLog(SQLModel, table=True): user_id: int = Field(index=True, description="User ID") ip_address: str = Field(max_length=45, index=True, description="IP address (supports IPv4 and IPv6)") user_agent: str | None = Field(default=None, max_length=500, description="User agent information") - login_time: datetime = Field(default_factory=datetime.utcnow, description="Login time") + login_time: datetime = Field(default_factory=utcnow, description="Login time") # GeoIP information country_code: str | None = Field(default=None, max_length=2, description="Country code") diff --git a/app/models/multiplayer_hub.py b/app/models/multiplayer_hub.py index 13294b8..7df4949 100644 --- a/app/models/multiplayer_hub.py +++ b/app/models/multiplayer_hub.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod import asyncio from collections.abc import Awaitable, Callable from dataclasses import dataclass -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta from enum import IntEnum from typing import ( TYPE_CHECKING, @@ -21,6 +21,7 @@ from app.database.beatmap import Beatmap from app.dependencies.database import with_db from app.dependencies.fetcher import get_fetcher from app.exception import InvokeException +from app.utils import utcnow from .mods import API_MODS, APIMod from .room import ( @@ -558,7 +559,7 @@ class MultiplayerQueue: from app.database import Playlist async with with_db() as session: - played_at = datetime.now(UTC) + played_at = utcnow() await session.execute( update(Playlist) .where( diff --git a/app/router/auth.py b/app/router/auth.py index c92ddd7..0a28af7 100644 --- a/app/router/auth.py +++ b/app/router/auth.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import UTC, datetime, timedelta +from datetime import timedelta import re from typing import Literal @@ -36,6 +36,7 @@ from app.service.email_verification_service import ( ) from app.service.login_log_service import LoginLogService from app.service.password_reset_service import password_reset_service +from app.utils import utcnow from fastapi import APIRouter, Depends, Form, Request from fastapi.responses import JSONResponse @@ -156,8 +157,8 @@ async def register_user( pw_bcrypt=get_password_hash(user_password), priv=1, # 普通用户权限 country_code=country_code, # 根据 IP 地理位置设置国家 - join_date=datetime.now(UTC), - last_visit=datetime.now(UTC), + join_date=utcnow(), + last_visit=utcnow(), is_supporter=settings.enable_supporter_for_all_users, support_level=int(settings.enable_supporter_for_all_users), ) diff --git a/app/router/notification/__init__.py b/app/router/notification/__init__.py index fef679f..d4c0b8b 100644 --- a/app/router/notification/__init__.py +++ b/app/router/notification/__init__.py @@ -1,7 +1,5 @@ from __future__ import annotations -from datetime import UTC, datetime - from app.config import settings from app.database.lazer_user import User from app.database.notification import Notification, UserNotification @@ -9,6 +7,7 @@ from app.dependencies.database import Database from app.dependencies.user import get_client_user from app.models.chat import ChatEvent from app.router.v2 import api_v2_router as router +from app.utils import utcnow from . import channel, message # noqa: F401 from .server import ( @@ -135,7 +134,7 @@ async def mark_notifications_as_read( data={ "notifications": [i.model_dump() for i in identities], "read_count": len(user_notifications), - "timestamp": datetime.now(UTC).isoformat(), + "timestamp": utcnow().isoformat(), }, ), ) diff --git a/app/router/private/stats.py b/app/router/private/stats.py index c4744fa..327d4e0 100644 --- a/app/router/private/stats.py +++ b/app/router/private/stats.py @@ -7,7 +7,7 @@ import json from app.dependencies.database import get_redis, get_redis_message from app.log import logger -from app.utils import bg_tasks +from app.utils import bg_tasks, utcnow from .router import router @@ -74,7 +74,7 @@ async def get_server_stats() -> ServerStats: registered_users=registered_count, online_users=online_count, playing_users=playing_count, - timestamp=datetime.utcnow(), + timestamp=utcnow(), ) except Exception as e: logger.error(f"Error getting server stats: {e}") @@ -83,7 +83,7 @@ async def get_server_stats() -> ServerStats: registered_users=0, online_users=0, playing_users=0, - timestamp=datetime.utcnow(), + timestamp=utcnow(), ) @@ -158,7 +158,7 @@ async def get_stats_debug_info(): try: from app.service.enhanced_interval_stats import EnhancedIntervalStatsManager - current_time = datetime.utcnow() + current_time = utcnow() current_interval = await EnhancedIntervalStatsManager.get_current_interval_info() interval_stats = await EnhancedIntervalStatsManager.get_current_interval_stats() @@ -334,7 +334,7 @@ async def record_hourly_stats() -> None: online_count = await _get_online_users_count(redis_async) playing_count = await _get_playing_users_count(redis_async) - current_time = datetime.utcnow() + current_time = utcnow() history_point = { "timestamp": current_time.isoformat(), "online_count": online_count, diff --git a/app/router/private/team.py b/app/router/private/team.py index 32584e6..563592d 100644 --- a/app/router/private/team.py +++ b/app/router/private/team.py @@ -1,6 +1,5 @@ from __future__ import annotations -from datetime import UTC, datetime import hashlib from app.database.lazer_user import BASE_INCLUDES, User, UserResp @@ -15,7 +14,7 @@ from app.models.notification import ( ) from app.router.notification import server from app.storage.base import StorageService -from app.utils import check_image +from app.utils import check_image, utcnow from .router import router @@ -53,7 +52,7 @@ async def create_team( check_image(flag, 2 * 1024 * 1024, 240, 120) check_image(cover, 10 * 1024 * 1024, 3000, 2000) - now = datetime.now(UTC) + now = utcnow() team = Team(name=name, short_name=short_name, leader_id=user_id, created_at=now) session.add(team) await session.commit() @@ -196,7 +195,7 @@ async def request_join_team( ) ).first(): raise HTTPException(status_code=409, detail="Join request already exists") - team_request = TeamRequest(user_id=current_user.id, team_id=team_id, requested_at=datetime.now(UTC)) + team_request = TeamRequest(user_id=current_user.id, team_id=team_id, requested_at=utcnow()) session.add(team_request) await session.commit() await session.refresh(team_request) @@ -233,7 +232,7 @@ async def handle_request( if (await session.exec(select(exists()).where(TeamMember.user_id == user_id))).first(): raise HTTPException(status_code=409, detail="User is already a member of the team") - session.add(TeamMember(user_id=user_id, team_id=team_id, joined_at=datetime.now(UTC))) + session.add(TeamMember(user_id=user_id, team_id=team_id, joined_at=utcnow())) await server.new_private_notification(TeamApplicationAccept.init(team_request)) else: diff --git a/app/router/private/username.py b/app/router/private/username.py index 0d94efe..312d257 100644 --- a/app/router/private/username.py +++ b/app/router/private/username.py @@ -1,13 +1,12 @@ from __future__ import annotations -from datetime import UTC, datetime - from app.auth import validate_username from app.config import settings from app.database.events import Event, EventType from app.database.lazer_user import User from app.dependencies.database import Database from app.dependencies.user import get_client_user +from app.utils import utcnow from .router import router @@ -47,7 +46,7 @@ async def user_rename( current_user.username = new_name current_user.previous_usernames = previous_username rename_event = Event( - created_at=datetime.now(UTC), + created_at=utcnow(), type=EventType.USERNAME_CHANGE, user_id=current_user.id, user=current_user, diff --git a/app/router/v1/score.py b/app/router/v1/score.py index fdedd59..27e8240 100644 --- a/app/router/v1/score.py +++ b/app/router/v1/score.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta from typing import Literal from app.database.pp_best_score import PPBestScore @@ -8,6 +8,7 @@ from app.database.score import Score, get_leaderboard from app.dependencies.database import Database from app.models.mods import int_to_mods, mod_to_save, mods_to_int from app.models.score import GameMode, LeaderboardType +from app.utils import utcnow from .router import AllStrModel, router @@ -112,7 +113,7 @@ async def get_user_recent( .where( Score.user_id == user if type == "id" or user.isdigit() else col(Score.user).has(username=user), Score.gamemode == GameMode.from_int_extra(ruleset_id), - Score.ended_at > datetime.now(UTC) - timedelta(hours=24), + Score.ended_at > utcnow() - timedelta(hours=24), ) .order_by(col(Score.pp).desc()) .options(joinedload(Score.beatmap)) diff --git a/app/router/v2/room.py b/app/router/v2/room.py index 03341d0..0afadb0 100644 --- a/app/router/v2/room.py +++ b/app/router/v2/room.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import UTC, datetime +from datetime import UTC from typing import Literal from app.database.beatmap import Beatmap, BeatmapResp @@ -17,6 +17,7 @@ from app.dependencies.user import get_client_user, get_current_user from app.models.room import RoomCategory, RoomStatus from app.service.room import create_playlist_room_from_api from app.signalr.hub import MultiplayerHubs +from app.utils import utcnow from .router import router @@ -50,7 +51,7 @@ async def get_all_rooms( ): resp_list: list[RoomResp] = [] where_clauses: list[ColumnElement[bool]] = [col(Room.category) == category] - now = datetime.now(UTC) + now = utcnow() if status is not None: where_clauses.append(col(Room.status) == status) if mode == "open": @@ -112,12 +113,12 @@ async def _participate_room(room_id: int, user_id: int, db_room: Room, session: participated_user = RoomParticipatedUser( room_id=room_id, user_id=user_id, - joined_at=datetime.now(UTC), + joined_at=utcnow(), ) session.add(participated_user) else: participated_user.left_at = None - participated_user.joined_at = datetime.now(UTC) + participated_user.joined_at = utcnow() db_room.participant_count += 1 await redis.publish("chat:room:joined", f"{db_room.channel_id}:{user_id}") @@ -185,7 +186,7 @@ async def delete_room( if db_room is None: raise HTTPException(404, "Room not found") else: - db_room.ends_at = datetime.now(UTC) + db_room.ends_at = utcnow() await db.commit() return None @@ -238,7 +239,7 @@ async def remove_user_from_room( ) ).first() if participated_user is not None: - participated_user.left_at = datetime.now(UTC) + participated_user.left_at = utcnow() if db_room.participant_count > 0: db_room.participant_count -= 1 await redis.publish("chat:room:left", f"{db_room.channel_id}:{user_id}") diff --git a/app/router/v2/score.py b/app/router/v2/score.py index 33fcf69..cf3aa32 100644 --- a/app/router/v2/score.py +++ b/app/router/v2/score.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import UTC, date, datetime +from datetime import UTC, date import math import time @@ -50,6 +50,7 @@ from app.models.score import ( from app.service.user_cache_service import get_user_cache_service from app.storage.base import StorageService from app.storage.local import LocalStorageService +from app.utils import utcnow from .router import router @@ -166,7 +167,7 @@ async def submit_score( total_users = (await db.exec(select(func.count()).select_from(User))).one() if resp.rank_global is not None and resp.rank_global <= min(math.ceil(float(total_users) * 0.01), 50): rank_event = Event( - created_at=datetime.now(UTC), + created_at=utcnow(), type=EventType.RANK, user_id=score.user_id, user=score.user, @@ -451,7 +452,7 @@ async def create_playlist_score( if not room: raise HTTPException(status_code=404, detail="Room not found") db_room_time = room.ends_at.replace(tzinfo=UTC) if room.ends_at else None - if db_room_time and db_room_time < datetime.now(UTC).replace(tzinfo=UTC): + if db_room_time and db_room_time < utcnow().replace(tzinfo=UTC): raise HTTPException(status_code=400, detail="Room has ended") item = (await session.exec(select(Playlist).where(Playlist.id == playlist_id, Playlist.room_id == room_id))).first() if not item: diff --git a/app/router/v2/user.py b/app/router/v2/user.py index 5f6ff8a..ec8bbd7 100644 --- a/app/router/v2/user.py +++ b/app/router/v2/user.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import UTC, datetime, timedelta +from datetime import timedelta from typing import Literal from app.config import settings @@ -22,6 +22,7 @@ from app.log import logger from app.models.score import GameMode from app.models.user import BeatmapsetType from app.service.user_cache_service import get_user_cache_service +from app.utils import utcnow from .router import router @@ -338,7 +339,7 @@ async def get_user_scores( where_clause &= exists().where(col(PPBestScore.score_id) == Score.id) order_by = col(Score.pp).desc() elif type == "recent": - where_clause &= Score.ended_at > datetime.now(UTC) - timedelta(hours=24) + where_clause &= Score.ended_at > utcnow() - timedelta(hours=24) order_by = col(Score.ended_at).desc() elif type == "firsts": # TODO diff --git a/app/scheduler/user_cache_scheduler.py b/app/scheduler/user_cache_scheduler.py index 1194046..0589daf 100644 --- a/app/scheduler/user_cache_scheduler.py +++ b/app/scheduler/user_cache_scheduler.py @@ -5,13 +5,14 @@ from __future__ import annotations import asyncio -from datetime import UTC, datetime, timedelta +from datetime import timedelta from app.config import settings from app.database.score import Score from app.dependencies.database import get_redis from app.log import logger from app.service.user_cache_service import get_user_cache_service +from app.utils import utcnow from sqlmodel import col, func, select @@ -34,7 +35,7 @@ async def schedule_user_cache_preload_task(): async with with_db() as session: # 获取最近24小时内活跃的用户(提交过成绩的用户) - recent_time = datetime.now(UTC) - timedelta(hours=24) + recent_time = utcnow() - timedelta(hours=24) score_count = func.count().label("score_count") active_user_ids = ( diff --git a/app/service/beatmap_cache_service.py b/app/service/beatmap_cache_service.py index 5351fd1..3a81686 100644 --- a/app/service/beatmap_cache_service.py +++ b/app/service/beatmap_cache_service.py @@ -6,11 +6,12 @@ Beatmap缓存预取服务 from __future__ import annotations import asyncio -from datetime import UTC, datetime, timedelta +from datetime import timedelta from typing import TYPE_CHECKING from app.config import settings from app.log import logger +from app.utils import utcnow from redis.asyncio import Redis from sqlmodel import col, func, select @@ -40,7 +41,7 @@ class BeatmapCacheService: logger.info(f"Starting preload of top {limit} popular beatmaps") # 获取过去24小时内最热门的beatmap - recent_time = datetime.now(UTC) - timedelta(hours=24) + recent_time = utcnow() - timedelta(hours=24) from app.database.score import Score diff --git a/app/service/calculate_all_user_rank.py b/app/service/calculate_all_user_rank.py index 3d089c5..1d1395a 100644 --- a/app/service/calculate_all_user_rank.py +++ b/app/service/calculate_all_user_rank.py @@ -1,19 +1,20 @@ from __future__ import annotations -from datetime import UTC, datetime, timedelta +from datetime import timedelta from app.database import RankHistory, UserStatistics from app.database.rank_history import RankTop from app.dependencies.database import with_db from app.dependencies.scheduler import get_scheduler from app.models.score import GameMode +from app.utils import utcnow from sqlmodel import col, exists, select, update @get_scheduler().scheduled_job("cron", hour=0, minute=0, second=0, id="calculate_user_rank") async def calculate_user_rank(is_today: bool = False): - today = datetime.now(UTC).date() + today = utcnow().date() target_date = today if is_today else today - timedelta(days=1) async with with_db() as session: for gamemode in GameMode: diff --git a/app/service/daily_challenge.py b/app/service/daily_challenge.py index 10117b3..13be1a4 100644 --- a/app/service/daily_challenge.py +++ b/app/service/daily_challenge.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import UTC, datetime, timedelta +from datetime import UTC, timedelta import json from math import ceil @@ -17,7 +17,7 @@ from app.log import logger from app.models.metadata_hub import DailyChallengeInfo from app.models.mods import APIMod, get_available_mods from app.models.room import RoomCategory -from app.utils import are_same_weeks +from app.utils import are_same_weeks, utcnow from .room import create_playlist_room @@ -32,7 +32,7 @@ async def create_daily_challenge_room( allowed_mods: list[APIMod] = [], ) -> Room: async with with_db() as session: - today = datetime.now(UTC).date() + today = utcnow().date() return await create_playlist_room( session=session, name=str(today), @@ -57,7 +57,7 @@ async def create_daily_challenge_room( async def daily_challenge_job(): from app.signalr.hub import MetadataHubs - now = datetime.now(UTC) + now = utcnow() redis = get_redis() key = f"daily_challenge:{now.date()}" if not await redis.exists(key): @@ -67,7 +67,7 @@ async def daily_challenge_job(): await session.exec( select(Room).where( Room.category == RoomCategory.DAILY_CHALLENGE, - col(Room.ends_at) > datetime.now(UTC), + col(Room.ends_at) > utcnow(), ) ) ).first() @@ -87,7 +87,7 @@ async def daily_challenge_job(): get_scheduler().add_job( daily_challenge_job, "date", - run_date=datetime.now(UTC) + timedelta(minutes=5), + run_date=utcnow() + timedelta(minutes=5), ) return @@ -121,14 +121,14 @@ async def daily_challenge_job(): get_scheduler().add_job( daily_challenge_job, "date", - run_date=datetime.now(UTC) + timedelta(minutes=5), + run_date=utcnow() + timedelta(minutes=5), ) @get_scheduler().scheduled_job("cron", hour=0, minute=1, second=0, id="daily_challenge_last_top") async def process_daily_challenge_top(): async with with_db() as session: - now = datetime.now(UTC) + now = utcnow() room = ( await session.exec( select(Room).where( diff --git a/app/service/database_cleanup_service.py b/app/service/database_cleanup_service.py index a024c9a..f17dc71 100644 --- a/app/service/database_cleanup_service.py +++ b/app/service/database_cleanup_service.py @@ -4,10 +4,11 @@ from __future__ import annotations -from datetime import UTC, datetime, timedelta +from datetime import timedelta from app.database.email_verification import EmailVerification, LoginSession from app.log import logger +from app.utils import utcnow from sqlmodel import col, select from sqlmodel.ext.asyncio.session import AsyncSession @@ -29,7 +30,7 @@ class DatabaseCleanupService: """ try: # 查找过期的验证码记录 - current_time = datetime.now(UTC) + current_time = utcnow() stmt = select(EmailVerification).where(EmailVerification.expires_at < current_time) result = await db.exec(stmt) @@ -66,7 +67,7 @@ class DatabaseCleanupService: """ try: # 查找过期的登录会话记录 - current_time = datetime.now(UTC) + current_time = utcnow() stmt = select(LoginSession).where(LoginSession.expires_at < current_time) result = await db.exec(stmt) @@ -104,7 +105,7 @@ class DatabaseCleanupService: """ try: # 查找指定天数前的已使用验证码记录 - cutoff_time = datetime.now(UTC) - timedelta(days=days_old) + cutoff_time = utcnow() - timedelta(days=days_old) stmt = select(EmailVerification).where(col(EmailVerification.is_used).is_(True)) result = await db.exec(stmt) @@ -147,7 +148,7 @@ class DatabaseCleanupService: """ try: # 查找指定天数前的已验证会话记录 - cutoff_time = datetime.now(UTC) - timedelta(days=days_old) + cutoff_time = utcnow() - timedelta(days=days_old) stmt = select(LoginSession).where(col(LoginSession.is_verified).is_(True)) result = await db.exec(stmt) @@ -225,7 +226,7 @@ class DatabaseCleanupService: dict: 统计信息 """ try: - current_time = datetime.now(UTC) + current_time = utcnow() cutoff_7_days = current_time - timedelta(days=7) cutoff_30_days = current_time - timedelta(days=30) diff --git a/app/service/email_verification_service.py b/app/service/email_verification_service.py index ef32fcb..1fe8d14 100644 --- a/app/service/email_verification_service.py +++ b/app/service/email_verification_service.py @@ -4,7 +4,7 @@ from __future__ import annotations -from datetime import UTC, datetime, timedelta +from datetime import timedelta import secrets import string @@ -12,6 +12,7 @@ from app.config import settings from app.database.email_verification import EmailVerification, LoginSession from app.log import logger from app.service.email_queue import email_queue # 导入邮件队列 +from app.utils import utcnow from redis.asyncio import Redis from sqlmodel import col, select @@ -200,7 +201,7 @@ This email was sent automatically, please do not reply. select(EmailVerification).where( EmailVerification.user_id == user_id, col(EmailVerification.is_used).is_(False), - EmailVerification.expires_at > datetime.now(UTC), + EmailVerification.expires_at > utcnow(), ) ) existing = existing_result.first() @@ -217,7 +218,7 @@ This email was sent automatically, please do not reply. user_id=user_id, email=email, verification_code=code, - expires_at=datetime.now(UTC) + timedelta(minutes=10), # 10分钟过期 + expires_at=utcnow() + timedelta(minutes=10), # 10分钟过期 ip_address=ip_address, user_agent=user_agent, ) @@ -306,7 +307,7 @@ This email was sent automatically, please do not reply. EmailVerification.user_id == user_id, EmailVerification.verification_code == code, col(EmailVerification.is_used).is_(False), - EmailVerification.expires_at > datetime.now(UTC), + EmailVerification.expires_at > utcnow(), ) ) @@ -316,7 +317,7 @@ This email was sent automatically, please do not reply. # 标记为已使用 verification.is_used = True - verification.used_at = datetime.now(UTC) + verification.used_at = utcnow() # 同时更新对应的登录会话状态 await LoginSessionService.mark_session_verified(db, user_id) @@ -397,7 +398,7 @@ class LoginSessionService: user_agent=None, country_code=country_code, is_new_location=is_new_location, - expires_at=datetime.now(UTC) + timedelta(hours=24), # 24小时过期 + expires_at=utcnow() + timedelta(hours=24), # 24小时过期 is_verified=not is_new_location, # 新位置需要验证 ) @@ -446,7 +447,7 @@ class LoginSessionService: session = result.first() if session: session.is_verified = True - session.verified_at = datetime.now(UTC) + session.verified_at = utcnow() await db.commit() logger.info(f"[Login Session] User {user_id} session verification successful") @@ -463,7 +464,7 @@ class LoginSessionService: """检查是否为新位置登录""" try: # 查看过去30天内是否有相同IP或相同国家的登录记录 - thirty_days_ago = datetime.now(UTC) - timedelta(days=30) + thirty_days_ago = utcnow() - timedelta(days=30) result = await db.exec( select(LoginSession).where( @@ -492,7 +493,7 @@ class LoginSessionService: select(LoginSession).where( LoginSession.user_id == user_id, col(LoginSession.is_verified).is_(False), - LoginSession.expires_at > datetime.now(UTC), + LoginSession.expires_at > utcnow(), ) ) @@ -501,7 +502,7 @@ class LoginSessionService: # 标记所有会话为已验证 for session in sessions: session.is_verified = True - session.verified_at = datetime.now(UTC) + session.verified_at = utcnow() if sessions: logger.info(f"[Login Session] Marked {len(sessions)} session(s) as verified for user {user_id}") diff --git a/app/service/enhanced_interval_stats.py b/app/service/enhanced_interval_stats.py index d8e3bcc..a160869 100644 --- a/app/service/enhanced_interval_stats.py +++ b/app/service/enhanced_interval_stats.py @@ -16,6 +16,7 @@ from app.router.private.stats import ( _get_playing_users_count, _redis_exec, ) +from app.utils import utcnow # Redis keys for interval statistics INTERVAL_STATS_BASE_KEY = "server:interval_stats" @@ -34,7 +35,7 @@ class IntervalInfo: def is_current(self) -> bool: """检查是否是当前区间""" - now = datetime.utcnow() + now = utcnow() return self.start_time <= now < self.end_time def to_dict(self) -> dict: @@ -101,7 +102,7 @@ class EnhancedIntervalStatsManager: @staticmethod def get_current_interval_boundaries() -> tuple[datetime, datetime]: """获取当前30分钟区间的边界""" - now = datetime.utcnow() + now = utcnow() # 计算区间开始时间(向下取整到最近的30分钟) minute = (now.minute // 30) * 30 start_time = now.replace(minute=minute, second=0, microsecond=0) @@ -157,7 +158,7 @@ class EnhancedIntervalStatsManager: peak_online_count=0, peak_playing_count=0, total_samples=0, - created_at=datetime.utcnow(), + created_at=utcnow(), ) await _redis_exec( @@ -195,7 +196,7 @@ class EnhancedIntervalStatsManager: needed_points = 48 - history_length # 从当前时间往前推,创建缺失的时间点(都填充为0) - current_time = datetime.utcnow() # noqa: F841 + current_time = utcnow() # noqa: F841 current_interval_start, _ = EnhancedIntervalStatsManager.get_current_interval_boundaries() # 从当前区间开始往前推,创建历史数据点(确保时间对齐到30分钟边界) @@ -323,7 +324,7 @@ class EnhancedIntervalStatsManager: peak_online_count=current_online, peak_playing_count=current_playing, total_samples=1, - created_at=datetime.utcnow(), + created_at=utcnow(), ) # 更新独特用户数 @@ -431,7 +432,7 @@ class EnhancedIntervalStatsManager: try: # 删除过期的区间统计数据(超过2小时的) - cutoff_time = datetime.utcnow() - timedelta(hours=2) + cutoff_time = utcnow() - timedelta(hours=2) pattern = f"{INTERVAL_STATS_BASE_KEY}:*" keys = await redis_async.keys(pattern) diff --git a/app/service/login_log_service.py b/app/service/login_log_service.py index b8b6766..87d3b7e 100644 --- a/app/service/login_log_service.py +++ b/app/service/login_log_service.py @@ -5,11 +5,11 @@ from __future__ import annotations import asyncio -from datetime import datetime from app.database.user_login_log import UserLoginLog from app.dependencies.geoip import get_client_ip, get_geoip_helper, normalize_ip from app.log import logger +from app.utils import utcnow from fastapi import Request from sqlmodel.ext.asyncio.session import AsyncSession @@ -56,7 +56,7 @@ class LoginLogService: user_id=user_id, ip_address=ip_address, user_agent=user_agent, - login_time=datetime.utcnow(), + login_time=utcnow(), login_success=login_success, login_method=login_method, notes=notes, diff --git a/app/service/online_status_manager.py b/app/service/online_status_manager.py index e01db1f..8200c43 100644 --- a/app/service/online_status_manager.py +++ b/app/service/online_status_manager.py @@ -6,11 +6,10 @@ from __future__ import annotations -from datetime import datetime - from app.dependencies.database import get_redis from app.log import logger from app.router.private.stats import add_online_user +from app.utils import utcnow class OnlineStatusManager: @@ -37,7 +36,7 @@ class OnlineStatusManager: # 3. 设置最后活跃时间戳 last_seen_key = f"user:last_seen:{user_id}" - await redis.set(last_seen_key, int(datetime.utcnow().timestamp()), ex=7200) + await redis.set(last_seen_key, int(utcnow().timestamp()), ex=7200) logger.debug(f"[OnlineStatusManager] User {user_id} set online via {hub_type}") @@ -62,7 +61,7 @@ class OnlineStatusManager: # 刷新最后活跃时间 last_seen_key = f"user:last_seen:{user_id}" - await redis.set(last_seen_key, int(datetime.utcnow().timestamp()), ex=7200) + await redis.set(last_seen_key, int(utcnow().timestamp()), ex=7200) logger.debug(f"[OnlineStatusManager] Refreshed online status for user {user_id}") diff --git a/app/service/password_reset_service.py b/app/service/password_reset_service.py index fc0c02e..de301de 100644 --- a/app/service/password_reset_service.py +++ b/app/service/password_reset_service.py @@ -4,7 +4,7 @@ from __future__ import annotations -from datetime import UTC, datetime +from datetime import datetime import json import secrets import string @@ -15,6 +15,7 @@ from app.dependencies.database import with_db from app.log import logger from app.service.email_queue import email_queue # 导入邮件队列 from app.service.email_service import EmailService +from app.utils import utcnow from redis.asyncio import Redis from sqlmodel import select @@ -88,7 +89,7 @@ class PasswordResetService: "user_id": user.id, "email": email, "reset_code": reset_code, - "created_at": datetime.now(UTC).isoformat(), + "created_at": utcnow().isoformat(), "ip_address": ip_address, "user_agent": user_agent, "used": False, @@ -346,7 +347,7 @@ class PasswordResetService: try: # 先标记验证码为已使用(在数据库操作之前) reset_data["used"] = True - reset_data["used_at"] = datetime.now(UTC).isoformat() + reset_data["used_at"] = utcnow().isoformat() # 保存用户ID用于日志记录 user_id = user.id @@ -391,7 +392,7 @@ class PasswordResetService: # 计算剩余的TTL时间 created_at = datetime.fromisoformat(reset_data.get("created_at", "")) - elapsed = (datetime.now(UTC) - created_at).total_seconds() + elapsed = (utcnow() - created_at).total_seconds() remaining_ttl = max(0, 600 - int(elapsed)) # 600秒总过期时间 if remaining_ttl > 0: diff --git a/app/service/ranking_cache_service.py b/app/service/ranking_cache_service.py index ea37306..89f1b37 100644 --- a/app/service/ranking_cache_service.py +++ b/app/service/ranking_cache_service.py @@ -6,7 +6,7 @@ from __future__ import annotations import asyncio -from datetime import UTC, datetime +from datetime import datetime import json from typing import TYPE_CHECKING, Literal @@ -14,6 +14,7 @@ from app.config import settings from app.database.statistics import UserStatistics, UserStatisticsResp from app.log import logger from app.models.score import GameMode +from app.utils import utcnow from redis.asyncio import Redis from sqlmodel import col, select @@ -258,7 +259,7 @@ class RankingCacheService: # 计算统计信息 stats = { "total_users": total_users, - "last_updated": datetime.now(UTC).isoformat(), + "last_updated": utcnow().isoformat(), "type": type, "ruleset": ruleset, "country": country, @@ -370,7 +371,7 @@ class RankingCacheService: # 计算统计信息 stats = { "total_countries": len(country_stats_list), - "last_updated": datetime.now(UTC).isoformat(), + "last_updated": utcnow().isoformat(), "ruleset": ruleset, } diff --git a/app/service/room.py b/app/service/room.py index 99ef917..03320ef 100644 --- a/app/service/room.py +++ b/app/service/room.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import UTC, datetime, timedelta +from datetime import timedelta from app.database.beatmap import Beatmap from app.database.chat import ChannelType, ChatChannel @@ -8,6 +8,7 @@ from app.database.playlists import Playlist from app.database.room import APIUploadedRoom, Room from app.dependencies.fetcher import get_fetcher from app.models.room import MatchType, QueueMode, RoomCategory, RoomStatus +from app.utils import utcnow from sqlalchemy import exists from sqlmodel import col, select @@ -17,7 +18,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession async def create_playlist_room_from_api(session: AsyncSession, room: APIUploadedRoom, host_id: int) -> Room: db_room = room.to_room() db_room.host_id = host_id - db_room.starts_at = datetime.now(UTC) + db_room.starts_at = utcnow() db_room.ends_at = db_room.starts_at + timedelta(minutes=db_room.duration if db_room.duration is not None else 0) session.add(db_room) await session.commit() @@ -52,8 +53,8 @@ async def create_playlist_room( name=name, category=category, duration=duration, - starts_at=datetime.now(UTC), - ends_at=datetime.now(UTC) + timedelta(minutes=duration), + starts_at=utcnow(), + ends_at=utcnow() + timedelta(minutes=duration), participant_count=0, max_attempts=max_attempts, type=MatchType.PLAYLISTS, diff --git a/app/service/stats_cleanup.py b/app/service/stats_cleanup.py index 1b93d7b..dab46e1 100644 --- a/app/service/stats_cleanup.py +++ b/app/service/stats_cleanup.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import timedelta from app.dependencies.database import get_redis, get_redis_message from app.log import logger @@ -9,6 +9,7 @@ from app.router.private.stats import ( REDIS_PLAYING_USERS_KEY, _redis_exec, ) +from app.utils import utcnow async def cleanup_stale_online_users() -> tuple[int, int]: @@ -25,7 +26,7 @@ async def cleanup_stale_online_users() -> tuple[int, int]: playing_users = await _redis_exec(redis_sync.smembers, REDIS_PLAYING_USERS_KEY) # 检查在线用户的最后活动时间 - current_time = datetime.utcnow() + current_time = utcnow() stale_threshold = current_time - timedelta(hours=2) # 2小时无活动视为过期 # noqa: F841 # 对于在线用户,我们检查metadata在线标记 diff --git a/app/service/stats_scheduler.py b/app/service/stats_scheduler.py index 960cb40..2ca3ea0 100644 --- a/app/service/stats_scheduler.py +++ b/app/service/stats_scheduler.py @@ -1,7 +1,7 @@ from __future__ import annotations import asyncio -from datetime import datetime, timedelta +from datetime import timedelta from app.log import logger from app.router.private.stats import record_hourly_stats, update_registered_users_count @@ -10,6 +10,7 @@ from app.service.stats_cleanup import ( cleanup_stale_online_users, refresh_redis_key_expiry, ) +from app.utils import utcnow class StatsScheduler: @@ -60,7 +61,7 @@ class StatsScheduler: while self._running: try: # 计算下次区间结束时间 - now = datetime.utcnow() + now = utcnow() # 计算当前区间的结束时间 current_minute = (now.minute // 30) * 30 @@ -93,15 +94,11 @@ class StatsScheduler: # 完成当前区间并记录到历史 finalized_stats = await EnhancedIntervalStatsManager.finalize_interval() if finalized_stats: - logger.info( - f"Finalized enhanced interval statistics at {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')}" - ) + logger.info(f"Finalized enhanced interval statistics at {utcnow().strftime('%Y-%m-%d %H:%M:%S')}") else: # 如果区间完成失败,使用原有方式记录 await record_hourly_stats() - logger.info( - f"Recorded hourly statistics (fallback) at {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')}" - ) + logger.info(f"Recorded hourly statistics (fallback) at {utcnow().strftime('%Y-%m-%d %H:%M:%S')}") # 开始新的区间统计 await EnhancedIntervalStatsManager.initialize_current_interval() diff --git a/app/signalr/hub/metadata.py b/app/signalr/hub/metadata.py index 34a283d..19215cb 100644 --- a/app/signalr/hub/metadata.py +++ b/app/signalr/hub/metadata.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from collections import defaultdict from collections.abc import Coroutine -from datetime import UTC, datetime import math from typing import override @@ -27,6 +26,7 @@ from app.models.metadata_hub import ( ) from app.models.room import RoomCategory from app.service.subscribers.score_processed import ScoreSubscriber +from app.utils import utcnow from .hub import Client, Hub @@ -41,11 +41,11 @@ class MetadataHub(Hub[MetadataClientState]): self.subscriber = ScoreSubscriber() self.subscriber.metadata_hub = self self._daily_challenge_stats: MultiplayerRoomStats | None = None - self._today = datetime.now(UTC).date() + self._today = utcnow().date() self._lock = asyncio.Lock() def get_daily_challenge_stats(self, daily_challenge_room: int) -> MultiplayerRoomStats: - if self._daily_challenge_stats is None or self._today != datetime.now(UTC).date(): + if self._daily_challenge_stats is None or self._today != utcnow().date(): self._daily_challenge_stats = MultiplayerRoomStats( room_id=daily_challenge_room, playlist_item_stats={}, @@ -98,7 +98,7 @@ class MetadataHub(Hub[MetadataClientState]): async with with_db() as session: async with session.begin(): user = (await session.exec(select(User).where(User.id == int(state.connection_id)))).one() - user.last_visit = datetime.now(UTC) + user.last_visit = utcnow() await session.commit() @override @@ -149,7 +149,7 @@ class MetadataHub(Hub[MetadataClientState]): daily_challenge_room = ( await session.exec( select(Room).where( - col(Room.ends_at) > datetime.now(UTC), + col(Room.ends_at) > utcnow(), Room.category == RoomCategory.DAILY_CHALLENGE, ) ) diff --git a/app/signalr/hub/multiplayer.py b/app/signalr/hub/multiplayer.py index ca850c8..a5eb244 100644 --- a/app/signalr/hub/multiplayer.py +++ b/app/signalr/hub/multiplayer.py @@ -1,7 +1,7 @@ from __future__ import annotations import asyncio -from datetime import UTC, datetime, timedelta +from datetime import timedelta from typing import override from app.database import Room @@ -44,6 +44,7 @@ from app.models.room import ( RoomStatus, ) from app.models.score import GameMode +from app.utils import utcnow from .hub import Client, Hub @@ -233,7 +234,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]): item = room.playlist[0] item.owner_id = client.user_id room.room_id = db_room.id - starts_at = db_room.starts_at or datetime.now(UTC) + starts_at = db_room.starts_at or utcnow() beatmap_exists = await session.exec(select(exists().where(col(Beatmap.id) == item.beatmap_id))) if not beatmap_exists.one(): fetcher = await get_fetcher() @@ -306,7 +307,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]): session.add(participated_user) else: participated_user.left_at = None - participated_user.joined_at = datetime.now(UTC) + participated_user.joined_at = utcnow() db_room = await session.get(Room, room_id) if db_room is None: @@ -1056,7 +1057,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]): ) ).first() if participated_user is not None: - participated_user.left_at = datetime.now(UTC) + participated_user.left_at = utcnow() db_room = await session.get(Room, room.room.room_id) if db_room is None: @@ -1087,7 +1088,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]): .where(col(Room.id) == room.room.room_id) .values( name=room.room.settings.name, - ends_at=datetime.now(UTC), + ends_at=utcnow(), type=room.room.settings.match_type, queue_mode=room.room.settings.queue_mode, auto_skip=room.room.settings.auto_skip, diff --git a/app/utils.py b/app/utils.py index ca64f23..6d8f8e9 100644 --- a/app/utils.py +++ b/app/utils.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable, Sequence -from datetime import datetime +from datetime import UTC, datetime import functools import inspect from io import BytesIO @@ -270,3 +270,7 @@ class BackgroundTasks: bg_tasks = BackgroundTasks() + + +def utcnow() -> datetime: + return datetime.now(tz=UTC) diff --git a/main.py b/main.py index cecbc36..5c3c97b 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,6 @@ from __future__ import annotations from contextlib import asynccontextmanager -from datetime import datetime from pathlib import Path from app.config import settings @@ -38,7 +37,7 @@ from app.service.online_status_maintenance import schedule_online_status_mainten from app.service.osu_rx_statistics import create_rx_statistics from app.service.redis_message_system import redis_message_system from app.service.stats_scheduler import start_stats_scheduler, stop_stats_scheduler -from app.utils import bg_tasks +from app.utils import bg_tasks, utcnow from fastapi import FastAPI, HTTPException, Request from fastapi.exceptions import RequestValidationError @@ -157,7 +156,7 @@ async def root(): @app.get("/health", include_in_schema=False) async def health_check(): """健康检查端点""" - return {"status": "ok", "timestamp": datetime.utcnow().isoformat()} + return {"status": "ok", "timestamp": utcnow().isoformat()} @app.exception_handler(RequestValidationError)