refactor(project): use unified utcnow

This commit is contained in:
MingxuanGame
2025-08-22 11:27:45 +00:00
parent da66420eaa
commit 9b00dbda28
49 changed files with 201 additions and 167 deletions

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, datetime, timedelta from datetime import timedelta
import hashlib import hashlib
import re import re
import secrets import secrets
@@ -12,6 +12,7 @@ from app.database import (
User, User,
) )
from app.log import logger from app.log import logger
from app.utils import utcnow
import bcrypt import bcrypt
from jose import JWTError, jwt 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() to_encode = data.copy()
if expires_delta: if expires_delta:
expire = datetime.now(UTC) + expires_delta expire = utcnow() + expires_delta
else: 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)}) to_encode.update({"exp": expire, "random": secrets.token_hex(16)})
encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm) encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
@@ -206,7 +207,7 @@ async def store_token(
expires_in: int, expires_in: int,
) -> OAuthToken: ) -> 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) 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( statement = select(OAuthToken).where(
OAuthToken.access_token == access_token, OAuthToken.access_token == access_token,
OAuthToken.expires_at > datetime.utcnow(), OAuthToken.expires_at > utcnow(),
) )
return (await db.exec(statement)).first() 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( statement = select(OAuthToken).where(
OAuthToken.refresh_token == refresh_token, OAuthToken.refresh_token == refresh_token,
OAuthToken.expires_at > datetime.utcnow(), OAuthToken.expires_at > utcnow(),
) )
return (await db.exec(statement)).first() return (await db.exec(statement)).first()

View File

@@ -1,10 +1,11 @@
from datetime import UTC, datetime from datetime import datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from app.config import settings from app.config import settings
from app.models.achievement import MEDALS, Achievement from app.models.achievement import MEDALS, Achievement
from app.models.model import UTCBaseModel from app.models.model import UTCBaseModel
from app.models.notification import UserAchievementUnlock from app.models.notification import UserAchievementUnlock
from app.utils import utcnow
from .events import Event, EventType from .events import Event, EventType
@@ -28,7 +29,7 @@ if TYPE_CHECKING:
class UserAchievementBase(SQLModel, UTCBaseModel): class UserAchievementBase(SQLModel, UTCBaseModel):
achievement_id: int 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): class UserAchievement(UserAchievementBase, table=True):
@@ -56,7 +57,7 @@ async def process_achievements(session: AsyncSession, redis: Redis, score_id: in
).all() ).all()
not_achieved = {k: v for k, v in MEDALS.items() if k.id not in achieved} not_achieved = {k: v for k, v in MEDALS.items() if k.id not in achieved}
result: list[Achievement] = [] result: list[Achievement] = []
now = datetime.now(UTC) now = utcnow()
for k, v in not_achieved.items(): for k, v in not_achieved.items():
if await v(session, score, score.beatmap): if await v(session, score, score.beatmap):
result.append(k) result.append(k)

View File

@@ -3,6 +3,7 @@ import secrets
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from app.models.model import UTCBaseModel from app.models.model import UTCBaseModel
from app.utils import utcnow
from sqlalchemy import Column, DateTime from sqlalchemy import Column, DateTime
from sqlmodel import ( from sqlmodel import (
@@ -30,7 +31,7 @@ class OAuthToken(UTCBaseModel, SQLModel, table=True):
token_type: str = Field(default="Bearer", max_length=20) token_type: str = Field(default="Bearer", max_length=20)
scope: str = Field(default="*", max_length=100) scope: str = Field(default="*", max_length=100)
expires_at: datetime = Field(sa_column=Column(DateTime)) 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() user: "User" = Relationship()

View File

@@ -1,7 +1,7 @@
from datetime import UTC, datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from app.database.events import Event, EventType from app.database.events import Event, EventType
from app.utils import utcnow
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncAttrs 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 existing_playcount.playcount += 1
if existing_playcount.playcount % 100 == 0: if existing_playcount.playcount % 100 == 0:
playcount_event = Event( playcount_event = Event(
created_at=datetime.now(UTC), created_at=utcnow(),
type=EventType.BEATMAP_PLAYCOUNT, type=EventType.BEATMAP_PLAYCOUNT,
user_id=user_id, user_id=user_id,
) )

View File

@@ -1,9 +1,10 @@
from datetime import UTC, datetime from datetime import datetime
from enum import Enum from enum import Enum
from typing import Self from typing import Self
from app.database.lazer_user import RANKING_INCLUDES, User, UserResp from app.database.lazer_user import RANKING_INCLUDES, User, UserResp
from app.models.model import UTCBaseModel from app.models.model import UTCBaseModel
from app.utils import utcnow
from pydantic import BaseModel from pydantic import BaseModel
from redis.asyncio import Redis from redis.asyncio import Redis
@@ -170,7 +171,7 @@ class ChatMessageBase(UTCBaseModel, SQLModel):
content: str = Field(sa_column=Column(VARCHAR(1000))) content: str = Field(sa_column=Column(VARCHAR(1000)))
message_id: int = Field(index=True, primary_key=True, default=None) 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)) 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) type: MessageType = Field(default=MessageType.PLAIN, index=True, exclude=True)
uuid: str | None = Field(default=None) 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) channel_id: int = Field(foreign_key="chat_channels.channel_id", index=True)
until: datetime | None = Field(sa_column=Column(DateTime, index=True), default=None) 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)) 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): class UserSilenceResp(SQLModel):

View File

@@ -2,7 +2,7 @@ from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from app.models.model import UTCBaseModel from app.models.model import UTCBaseModel
from app.utils import are_adjacent_weeks from app.utils import are_adjacent_weeks, utcnow
from sqlmodel import ( from sqlmodel import (
BigInteger, BigInteger,
@@ -79,7 +79,7 @@ async def process_daily_challenge_score(session: AsyncSession, user_id: int, roo
session.add(stats) session.add(stats)
stats.playcount += 1 stats.playcount += 1
now = datetime.now(UTC) now = utcnow()
if stats.last_update is None: if stats.last_update is None:
stats.daily_streak_best = 1 stats.daily_streak_best = 1
stats.daily_streak_current = 1 stats.daily_streak_current = 1

View File

@@ -4,7 +4,9 @@
from __future__ import annotations 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 sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel 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)) user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), nullable=False, index=True))
email: str = Field(index=True) email: str = Field(index=True)
verification_code: str = Field(max_length=8) # 8位验证码 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() # 验证码过期时间 expires_at: datetime = Field() # 验证码过期时间
is_used: bool = Field(default=False) # 是否已使用 is_used: bool = Field(default=False) # 是否已使用
used_at: datetime | None = Field(default=None) 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) user_agent: str | None = Field(default=None, max_length=250)
country_code: str | None = Field(default=None) country_code: str | None = Field(default=None)
is_verified: bool = Field(default=False) # 是否已验证 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) verified_at: datetime | None = Field(default=None)
expires_at: datetime = Field() # 会话过期时间 expires_at: datetime = Field() # 会话过期时间
is_new_location: bool = Field(default=False) # 是否新位置登录 is_new_location: bool = Field(default=False) # 是否新位置登录

View File

@@ -3,6 +3,7 @@ from enum import Enum
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from app.models.model import UTCBaseModel from app.models.model import UTCBaseModel
from app.utils import utcnow
from pydantic import model_serializer from pydantic import model_serializer
from sqlmodel import ( from sqlmodel import (
@@ -40,7 +41,7 @@ class EventType(str, Enum):
class Event(UTCBaseModel, SQLModel, table=True): class Event(UTCBaseModel, SQLModel, table=True):
__tablename__: str = "user_events" __tablename__: str = "user_events"
id: int = Field(default=None, primary_key=True) 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 type: EventType
event_payload: dict = Field(exclude=True, default_factory=dict, sa_column=Column(JSON)) event_payload: dict = Field(exclude=True, default_factory=dict, sa_column=Column(JSON))
user_id: int | None = Field( user_id: int | None = Field(

View File

@@ -1,4 +1,4 @@
from datetime import UTC, datetime, timedelta from datetime import datetime, timedelta
import json import json
from typing import TYPE_CHECKING, NotRequired, TypedDict 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.score import GameMode
from app.models.user import Country, Page from app.models.user import Country, Page
from app.path import STATIC_DIR from app.path import STATIC_DIR
from app.utils import utcnow
from .achievement import UserAchievement, UserAchievementResp from .achievement import UserAchievement, UserAchievementResp
from .beatmap_playcounts import BeatmapPlaycounts from .beatmap_playcounts import BeatmapPlaycounts
@@ -75,7 +76,7 @@ class UserBase(UTCBaseModel, SQLModel):
is_active: bool = True is_active: bool = True
is_bot: bool = False is_bot: bool = False
is_supporter: 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 pm_friends_only: bool = False
profile_colour: str | None = None profile_colour: str | None = None
username: str = Field(max_length=32, unique=True, index=True) username: str = Field(max_length=32, unique=True, index=True)
@@ -99,7 +100,7 @@ class UserBase(UTCBaseModel, SQLModel):
discord: str | None = None discord: str | None = None
has_supported: bool = False has_supported: bool = False
interests: str | None = None interests: str | None = None
join_date: datetime = Field(default=datetime.now(UTC)) join_date: datetime = Field(default_factory=utcnow)
location: str | None = None location: str | None = None
max_blocks: int = 50 max_blocks: int = 50
max_friends: int = 500 max_friends: int = 500
@@ -408,7 +409,7 @@ class UserResp(UserBase):
Score.user_id == obj.id, Score.user_id == obj.id,
Score.gamemode == ruleset, Score.gamemode == ruleset,
col(Score.passed).is_(True), col(Score.passed).is_(True),
Score.ended_at > datetime.now(UTC) - timedelta(hours=24), Score.ended_at > utcnow() - timedelta(hours=24),
) )
) )
).one() ).one()
@@ -437,7 +438,7 @@ class UserResp(UserBase):
select(LoginSession).where( select(LoginSession).where(
LoginSession.user_id == obj.id, LoginSession.user_id == obj.id,
col(LoginSession.is_verified).is_(False), col(LoginSession.is_verified).is_(False),
LoginSession.expires_at > datetime.now(UTC), LoginSession.expires_at > utcnow(),
) )
) )
).first() ).first()

View File

@@ -1,7 +1,8 @@
from datetime import UTC, datetime from datetime import datetime
from typing import Any from typing import Any
from app.models.model import UTCBaseModel from app.models.model import UTCBaseModel
from app.utils import utcnow
from sqlmodel import ( from sqlmodel import (
JSON, JSON,
@@ -24,7 +25,7 @@ class MultiplayerEventBase(SQLModel, UTCBaseModel):
sa_column=Column( sa_column=Column(
DateTime(timezone=True), DateTime(timezone=True),
), ),
default=datetime.now(UTC), default_factory=utcnow,
) )
event_type: str = Field(index=True) event_type: str = Field(index=True)
@@ -40,7 +41,7 @@ class MultiplayerEvent(MultiplayerEventBase, table=True):
sa_column=Column( sa_column=Column(
DateTime(timezone=True), DateTime(timezone=True),
), ),
default=datetime.now(UTC), default_factory=utcnow,
) )
event_detail: dict[str, Any] | None = Field( event_detail: dict[str, Any] | None = Field(
sa_column=Column(JSON), sa_column=Column(JSON),

View File

@@ -1,7 +1,8 @@
from datetime import UTC, datetime from datetime import datetime
from typing import Any from typing import Any
from app.models.notification import NotificationDetail, NotificationName from app.models.notification import NotificationDetail, NotificationName
from app.utils import utcnow
from sqlmodel import ( from sqlmodel import (
JSON, JSON,
@@ -54,7 +55,7 @@ async def insert_notification(session: AsyncSession, detail: NotificationDetail)
object_id=detail.object_id, object_id=detail.object_id,
source_user_id=detail.source_user_id, source_user_id=detail.source_user_id,
details=detail.model_dump(), details=detail.model_dump(),
created_at=datetime.now(UTC), created_at=utcnow(),
) )
session.add(notification) session.add(notification)
await session.commit() await session.commit()

View File

@@ -4,7 +4,9 @@
from __future__ import annotations 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 sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel 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)) user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), nullable=False, index=True))
email: str = Field(index=True) email: str = Field(index=True)
reset_code: str = Field(max_length=8) # 8位重置验证码 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() # 验证码过期时间 expires_at: datetime = Field() # 验证码过期时间
is_used: bool = Field(default=False) # 是否已使用 is_used: bool = Field(default=False) # 是否已使用
used_at: datetime | None = Field(default=None) used_at: datetime | None = Field(default=None)

View File

@@ -1,11 +1,10 @@
from datetime import ( from datetime import (
UTC,
date as dt, date as dt,
datetime,
) )
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from app.models.score import GameMode from app.models.score import GameMode
from app.utils import utcnow
from pydantic import BaseModel from pydantic import BaseModel
from sqlmodel import ( from sqlmodel import (
@@ -33,7 +32,7 @@ class RankHistory(SQLModel, table=True):
mode: GameMode mode: GameMode
rank: int rank: int
date: dt = Field( date: dt = Field(
default_factory=lambda: datetime.now(UTC).date(), default_factory=lambda: utcnow().date(),
sa_column=Column(Date, index=True), sa_column=Column(Date, index=True),
) )
@@ -48,7 +47,7 @@ class RankTop(SQLModel, table=True):
mode: GameMode mode: GameMode
rank: int rank: int
date: dt = Field( date: dt = Field(
default_factory=lambda: datetime.now(UTC).date(), default_factory=lambda: utcnow().date(),
sa_column=Column(Date, index=True), sa_column=Column(Date, index=True),
) )

View File

@@ -1,4 +1,4 @@
from datetime import UTC, datetime from datetime import datetime
from app.database.playlist_attempts import PlaylistAggregateScore from app.database.playlist_attempts import PlaylistAggregateScore
from app.database.room_participated_user import RoomParticipatedUser from app.database.room_participated_user import RoomParticipatedUser
@@ -12,6 +12,7 @@ from app.models.room import (
RoomPlaylistItemStats, RoomPlaylistItemStats,
RoomStatus, RoomStatus,
) )
from app.utils import utcnow
from .lazer_user import User, UserResp from .lazer_user import User, UserResp
from .playlists import Playlist, PlaylistResp from .playlists import Playlist, PlaylistResp
@@ -39,7 +40,7 @@ class RoomBase(SQLModel, UTCBaseModel):
sa_column=Column( sa_column=Column(
DateTime(timezone=True), DateTime(timezone=True),
), ),
default=datetime.now(UTC), default_factory=utcnow,
) )
ends_at: datetime | None = Field( ends_at: datetime | None = Field(
sa_column=Column( sa_column=Column(

View File

@@ -1,6 +1,8 @@
from datetime import UTC, datetime from datetime import datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from app.utils import utcnow
from sqlalchemy.ext.asyncio import AsyncAttrs from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlmodel import ( from sqlmodel import (
BigInteger, 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)) user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), nullable=False))
joined_at: datetime = Field( joined_at: datetime = Field(
sa_column=Column(DateTime(timezone=True), nullable=False), 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) left_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True), nullable=True), default=None)

View File

@@ -1,5 +1,5 @@
from collections.abc import Sequence from collections.abc import Sequence
from datetime import UTC, date, datetime from datetime import date, datetime
import json import json
import math import math
import sys import sys
@@ -31,6 +31,7 @@ from app.models.score import (
ScoreStatistics, ScoreStatistics,
SoloScoreSubmissionInfo, SoloScoreSubmissionInfo,
) )
from app.utils import utcnow
from .beatmap import Beatmap, BeatmapResp from .beatmap import Beatmap, BeatmapResp
from .beatmap_playcounts import process_beatmap_playcount 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: if i < score_range and displaced_position > score_range and displaced_position is not None:
# Create rank lost event for the displaced user # Create rank lost event for the displaced user
rank_lost_event = Event( rank_lost_event = Event(
created_at=datetime.now(UTC), created_at=utcnow(),
type=EventType.RANK_LOST, type=EventType.RANK_LOST,
user_id=displaced_score.user_id, user_id=displaced_score.user_id,
) )
@@ -843,7 +844,7 @@ async def process_score(
total_score=info.total_score, total_score=info.total_score,
total_score_without_mods=info.total_score_without_mods, total_score_without_mods=info.total_score_without_mods,
beatmap_id=beatmap_id, beatmap_id=beatmap_id,
ended_at=datetime.now(UTC), ended_at=utcnow(),
gamemode=gamemode, gamemode=gamemode,
started_at=score_token.created_at, started_at=score_token.created_at,
user_id=user.id, user_id=user.id,

View File

@@ -2,6 +2,7 @@ from datetime import datetime
from app.models.model import UTCBaseModel from app.models.model import UTCBaseModel
from app.models.score import GameMode from app.models.score import GameMode
from app.utils import utcnow
from .beatmap import Beatmap from .beatmap import Beatmap
from .lazer_user import User from .lazer_user import User
@@ -15,8 +16,8 @@ class ScoreTokenBase(SQLModel, UTCBaseModel):
score_id: int | None = Field(sa_column=Column(BigInteger), default=None) score_id: int | None = Field(sa_column=Column(BigInteger), default=None)
ruleset_id: GameMode ruleset_id: GameMode
playlist_item_id: int | None = Field(default=None) # playlist playlist_item_id: int | None = Field(default=None) # playlist
created_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=datetime.utcnow, sa_column=Column(DateTime)) updated_at: datetime = Field(default_factory=utcnow, sa_column=Column(DateTime))
class ScoreToken(ScoreTokenBase, table=True): class ScoreToken(ScoreTokenBase, table=True):

View File

@@ -1,8 +1,9 @@
from datetime import UTC, datetime, timedelta from datetime import timedelta
import math import math
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from app.models.score import GameMode from app.models.score import GameMode
from app.utils import utcnow
from .rank_history import RankHistory from .rank_history import RankHistory
@@ -134,7 +135,7 @@ class UserStatisticsResp(UserStatisticsBase):
rank_best = ( rank_best = (
await session.exec( await session.exec(
select(func.max(RankHistory.rank)).where( 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, RankHistory.user_id == obj.user_id,
) )
) )
@@ -171,7 +172,7 @@ async def get_rank(session: AsyncSession, statistics: UserStatistics, country: s
return None return None
if country is None: if country is None:
today = datetime.now(UTC).date() today = utcnow().date()
rank_history = ( rank_history = (
await session.exec( await session.exec(
select(RankHistory).where( select(RankHistory).where(

View File

@@ -1,7 +1,8 @@
from datetime import UTC, datetime from datetime import datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from app.models.model import UTCBaseModel from app.models.model import UTCBaseModel
from app.utils import utcnow
from sqlalchemy import Column, DateTime from sqlalchemy import Column, DateTime
from sqlmodel import BigInteger, Field, ForeignKey, Relationship, SQLModel 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) short_name: str = Field(max_length=10)
flag_url: str | None = Field(default=None) flag_url: str | None = Field(default=None)
cover_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_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id")))
leader: "User" = Relationship() 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)) user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), primary_key=True))
team_id: int = Field(foreign_key="teams.id") 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"}) user: "User" = Relationship(back_populates="team_membership", sa_relationship_kwargs={"lazy": "joined"})
team: "Team" = Relationship(back_populates="members", 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)) 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) 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"}) user: "User" = Relationship(sa_relationship_kwargs={"lazy": "joined"})
team: "Team" = Relationship(sa_relationship_kwargs={"lazy": "joined"}) team: "Team" = Relationship(sa_relationship_kwargs={"lazy": "joined"})

View File

@@ -1,7 +1,8 @@
from datetime import UTC, datetime from datetime import datetime
from enum import Enum from enum import Enum
from app.models.model import UTCBaseModel from app.models.model import UTCBaseModel
from app.utils import utcnow
from sqlmodel import BigInteger, Column, Field, ForeignKey, Integer, SQLModel from sqlmodel import BigInteger, Column, Field, ForeignKey, Integer, SQLModel
@@ -17,7 +18,7 @@ class UserAccountHistoryBase(SQLModel, UTCBaseModel):
description: str | None = None description: str | None = None
length: int length: int
permanent: bool = False permanent: bool = False
timestamp: datetime = Field(default=datetime.now(UTC)) timestamp: datetime = Field(default_factory=utcnow)
type: UserAccountHistoryType type: UserAccountHistoryType

View File

@@ -4,6 +4,8 @@ User Login Log Database Model
from datetime import datetime from datetime import datetime
from app.utils import utcnow
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
@@ -16,7 +18,7 @@ class UserLoginLog(SQLModel, table=True):
user_id: int = Field(index=True, description="User ID") 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)") 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") 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 # GeoIP information
country_code: str | None = Field(default=None, max_length=2, description="Country code") country_code: str | None = Field(default=None, max_length=2, description="Country code")

View File

@@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
import asyncio import asyncio
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import UTC, datetime, timedelta from datetime import datetime, timedelta
from enum import IntEnum from enum import IntEnum
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
@@ -21,6 +21,7 @@ from app.database.beatmap import Beatmap
from app.dependencies.database import with_db from app.dependencies.database import with_db
from app.dependencies.fetcher import get_fetcher from app.dependencies.fetcher import get_fetcher
from app.exception import InvokeException from app.exception import InvokeException
from app.utils import utcnow
from .mods import API_MODS, APIMod from .mods import API_MODS, APIMod
from .room import ( from .room import (
@@ -558,7 +559,7 @@ class MultiplayerQueue:
from app.database import Playlist from app.database import Playlist
async with with_db() as session: async with with_db() as session:
played_at = datetime.now(UTC) played_at = utcnow()
await session.execute( await session.execute(
update(Playlist) update(Playlist)
.where( .where(

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, datetime, timedelta from datetime import timedelta
import re import re
from typing import Literal 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.login_log_service import LoginLogService
from app.service.password_reset_service import password_reset_service from app.service.password_reset_service import password_reset_service
from app.utils import utcnow
from fastapi import APIRouter, Depends, Form, Request from fastapi import APIRouter, Depends, Form, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
@@ -156,8 +157,8 @@ async def register_user(
pw_bcrypt=get_password_hash(user_password), pw_bcrypt=get_password_hash(user_password),
priv=1, # 普通用户权限 priv=1, # 普通用户权限
country_code=country_code, # 根据 IP 地理位置设置国家 country_code=country_code, # 根据 IP 地理位置设置国家
join_date=datetime.now(UTC), join_date=utcnow(),
last_visit=datetime.now(UTC), last_visit=utcnow(),
is_supporter=settings.enable_supporter_for_all_users, is_supporter=settings.enable_supporter_for_all_users,
support_level=int(settings.enable_supporter_for_all_users), support_level=int(settings.enable_supporter_for_all_users),
) )

View File

@@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, datetime
from app.config import settings from app.config import settings
from app.database.lazer_user import User from app.database.lazer_user import User
from app.database.notification import Notification, UserNotification 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.dependencies.user import get_client_user
from app.models.chat import ChatEvent from app.models.chat import ChatEvent
from app.router.v2 import api_v2_router as router from app.router.v2 import api_v2_router as router
from app.utils import utcnow
from . import channel, message # noqa: F401 from . import channel, message # noqa: F401
from .server import ( from .server import (
@@ -135,7 +134,7 @@ async def mark_notifications_as_read(
data={ data={
"notifications": [i.model_dump() for i in identities], "notifications": [i.model_dump() for i in identities],
"read_count": len(user_notifications), "read_count": len(user_notifications),
"timestamp": datetime.now(UTC).isoformat(), "timestamp": utcnow().isoformat(),
}, },
), ),
) )

View File

@@ -7,7 +7,7 @@ import json
from app.dependencies.database import get_redis, get_redis_message from app.dependencies.database import get_redis, get_redis_message
from app.log import logger from app.log import logger
from app.utils import bg_tasks from app.utils import bg_tasks, utcnow
from .router import router from .router import router
@@ -74,7 +74,7 @@ async def get_server_stats() -> ServerStats:
registered_users=registered_count, registered_users=registered_count,
online_users=online_count, online_users=online_count,
playing_users=playing_count, playing_users=playing_count,
timestamp=datetime.utcnow(), timestamp=utcnow(),
) )
except Exception as e: except Exception as e:
logger.error(f"Error getting server stats: {e}") logger.error(f"Error getting server stats: {e}")
@@ -83,7 +83,7 @@ async def get_server_stats() -> ServerStats:
registered_users=0, registered_users=0,
online_users=0, online_users=0,
playing_users=0, playing_users=0,
timestamp=datetime.utcnow(), timestamp=utcnow(),
) )
@@ -158,7 +158,7 @@ async def get_stats_debug_info():
try: try:
from app.service.enhanced_interval_stats import EnhancedIntervalStatsManager from app.service.enhanced_interval_stats import EnhancedIntervalStatsManager
current_time = datetime.utcnow() current_time = utcnow()
current_interval = await EnhancedIntervalStatsManager.get_current_interval_info() current_interval = await EnhancedIntervalStatsManager.get_current_interval_info()
interval_stats = await EnhancedIntervalStatsManager.get_current_interval_stats() 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) online_count = await _get_online_users_count(redis_async)
playing_count = await _get_playing_users_count(redis_async) playing_count = await _get_playing_users_count(redis_async)
current_time = datetime.utcnow() current_time = utcnow()
history_point = { history_point = {
"timestamp": current_time.isoformat(), "timestamp": current_time.isoformat(),
"online_count": online_count, "online_count": online_count,

View File

@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, datetime
import hashlib import hashlib
from app.database.lazer_user import BASE_INCLUDES, User, UserResp 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.router.notification import server
from app.storage.base import StorageService from app.storage.base import StorageService
from app.utils import check_image from app.utils import check_image, utcnow
from .router import router from .router import router
@@ -53,7 +52,7 @@ async def create_team(
check_image(flag, 2 * 1024 * 1024, 240, 120) check_image(flag, 2 * 1024 * 1024, 240, 120)
check_image(cover, 10 * 1024 * 1024, 3000, 2000) 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) team = Team(name=name, short_name=short_name, leader_id=user_id, created_at=now)
session.add(team) session.add(team)
await session.commit() await session.commit()
@@ -196,7 +195,7 @@ async def request_join_team(
) )
).first(): ).first():
raise HTTPException(status_code=409, detail="Join request already exists") 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) session.add(team_request)
await session.commit() await session.commit()
await session.refresh(team_request) 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(): 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") 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)) await server.new_private_notification(TeamApplicationAccept.init(team_request))
else: else:

View File

@@ -1,13 +1,12 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, datetime
from app.auth import validate_username from app.auth import validate_username
from app.config import settings from app.config import settings
from app.database.events import Event, EventType from app.database.events import Event, EventType
from app.database.lazer_user import User from app.database.lazer_user import User
from app.dependencies.database import Database from app.dependencies.database import Database
from app.dependencies.user import get_client_user from app.dependencies.user import get_client_user
from app.utils import utcnow
from .router import router from .router import router
@@ -47,7 +46,7 @@ async def user_rename(
current_user.username = new_name current_user.username = new_name
current_user.previous_usernames = previous_username current_user.previous_usernames = previous_username
rename_event = Event( rename_event = Event(
created_at=datetime.now(UTC), created_at=utcnow(),
type=EventType.USERNAME_CHANGE, type=EventType.USERNAME_CHANGE,
user_id=current_user.id, user_id=current_user.id,
user=current_user, user=current_user,

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, datetime, timedelta from datetime import datetime, timedelta
from typing import Literal from typing import Literal
from app.database.pp_best_score import PPBestScore 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.dependencies.database import Database
from app.models.mods import int_to_mods, mod_to_save, mods_to_int from app.models.mods import int_to_mods, mod_to_save, mods_to_int
from app.models.score import GameMode, LeaderboardType from app.models.score import GameMode, LeaderboardType
from app.utils import utcnow
from .router import AllStrModel, router from .router import AllStrModel, router
@@ -112,7 +113,7 @@ async def get_user_recent(
.where( .where(
Score.user_id == user if type == "id" or user.isdigit() else col(Score.user).has(username=user), 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.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()) .order_by(col(Score.pp).desc())
.options(joinedload(Score.beatmap)) .options(joinedload(Score.beatmap))

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, datetime from datetime import UTC
from typing import Literal from typing import Literal
from app.database.beatmap import Beatmap, BeatmapResp 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.models.room import RoomCategory, RoomStatus
from app.service.room import create_playlist_room_from_api from app.service.room import create_playlist_room_from_api
from app.signalr.hub import MultiplayerHubs from app.signalr.hub import MultiplayerHubs
from app.utils import utcnow
from .router import router from .router import router
@@ -50,7 +51,7 @@ async def get_all_rooms(
): ):
resp_list: list[RoomResp] = [] resp_list: list[RoomResp] = []
where_clauses: list[ColumnElement[bool]] = [col(Room.category) == category] where_clauses: list[ColumnElement[bool]] = [col(Room.category) == category]
now = datetime.now(UTC) now = utcnow()
if status is not None: if status is not None:
where_clauses.append(col(Room.status) == status) where_clauses.append(col(Room.status) == status)
if mode == "open": if mode == "open":
@@ -112,12 +113,12 @@ async def _participate_room(room_id: int, user_id: int, db_room: Room, session:
participated_user = RoomParticipatedUser( participated_user = RoomParticipatedUser(
room_id=room_id, room_id=room_id,
user_id=user_id, user_id=user_id,
joined_at=datetime.now(UTC), joined_at=utcnow(),
) )
session.add(participated_user) session.add(participated_user)
else: else:
participated_user.left_at = None participated_user.left_at = None
participated_user.joined_at = datetime.now(UTC) participated_user.joined_at = utcnow()
db_room.participant_count += 1 db_room.participant_count += 1
await redis.publish("chat:room:joined", f"{db_room.channel_id}:{user_id}") 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: if db_room is None:
raise HTTPException(404, "Room not found") raise HTTPException(404, "Room not found")
else: else:
db_room.ends_at = datetime.now(UTC) db_room.ends_at = utcnow()
await db.commit() await db.commit()
return None return None
@@ -238,7 +239,7 @@ async def remove_user_from_room(
) )
).first() ).first()
if participated_user is not None: if participated_user is not None:
participated_user.left_at = datetime.now(UTC) participated_user.left_at = utcnow()
if db_room.participant_count > 0: if db_room.participant_count > 0:
db_room.participant_count -= 1 db_room.participant_count -= 1
await redis.publish("chat:room:left", f"{db_room.channel_id}:{user_id}") await redis.publish("chat:room:left", f"{db_room.channel_id}:{user_id}")

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, date, datetime from datetime import UTC, date
import math import math
import time import time
@@ -50,6 +50,7 @@ from app.models.score import (
from app.service.user_cache_service import get_user_cache_service from app.service.user_cache_service import get_user_cache_service
from app.storage.base import StorageService from app.storage.base import StorageService
from app.storage.local import LocalStorageService from app.storage.local import LocalStorageService
from app.utils import utcnow
from .router import router from .router import router
@@ -166,7 +167,7 @@ async def submit_score(
total_users = (await db.exec(select(func.count()).select_from(User))).one() 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): if resp.rank_global is not None and resp.rank_global <= min(math.ceil(float(total_users) * 0.01), 50):
rank_event = Event( rank_event = Event(
created_at=datetime.now(UTC), created_at=utcnow(),
type=EventType.RANK, type=EventType.RANK,
user_id=score.user_id, user_id=score.user_id,
user=score.user, user=score.user,
@@ -451,7 +452,7 @@ async def create_playlist_score(
if not room: if not room:
raise HTTPException(status_code=404, detail="Room not found") raise HTTPException(status_code=404, detail="Room not found")
db_room_time = room.ends_at.replace(tzinfo=UTC) if room.ends_at else None 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") 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() item = (await session.exec(select(Playlist).where(Playlist.id == playlist_id, Playlist.room_id == room_id))).first()
if not item: if not item:

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, datetime, timedelta from datetime import timedelta
from typing import Literal from typing import Literal
from app.config import settings from app.config import settings
@@ -22,6 +22,7 @@ from app.log import logger
from app.models.score import GameMode from app.models.score import GameMode
from app.models.user import BeatmapsetType from app.models.user import BeatmapsetType
from app.service.user_cache_service import get_user_cache_service from app.service.user_cache_service import get_user_cache_service
from app.utils import utcnow
from .router import router from .router import router
@@ -338,7 +339,7 @@ async def get_user_scores(
where_clause &= exists().where(col(PPBestScore.score_id) == Score.id) where_clause &= exists().where(col(PPBestScore.score_id) == Score.id)
order_by = col(Score.pp).desc() order_by = col(Score.pp).desc()
elif type == "recent": 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() order_by = col(Score.ended_at).desc()
elif type == "firsts": elif type == "firsts":
# TODO # TODO

View File

@@ -5,13 +5,14 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from datetime import UTC, datetime, timedelta from datetime import timedelta
from app.config import settings from app.config import settings
from app.database.score import Score from app.database.score import Score
from app.dependencies.database import get_redis from app.dependencies.database import get_redis
from app.log import logger from app.log import logger
from app.service.user_cache_service import get_user_cache_service from app.service.user_cache_service import get_user_cache_service
from app.utils import utcnow
from sqlmodel import col, func, select from sqlmodel import col, func, select
@@ -34,7 +35,7 @@ async def schedule_user_cache_preload_task():
async with with_db() as session: async with with_db() as session:
# 获取最近24小时内活跃的用户提交过成绩的用户 # 获取最近24小时内活跃的用户提交过成绩的用户
recent_time = datetime.now(UTC) - timedelta(hours=24) recent_time = utcnow() - timedelta(hours=24)
score_count = func.count().label("score_count") score_count = func.count().label("score_count")
active_user_ids = ( active_user_ids = (

View File

@@ -6,11 +6,12 @@ Beatmap缓存预取服务
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from datetime import UTC, datetime, timedelta from datetime import timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from app.config import settings from app.config import settings
from app.log import logger from app.log import logger
from app.utils import utcnow
from redis.asyncio import Redis from redis.asyncio import Redis
from sqlmodel import col, func, select from sqlmodel import col, func, select
@@ -40,7 +41,7 @@ class BeatmapCacheService:
logger.info(f"Starting preload of top {limit} popular beatmaps") logger.info(f"Starting preload of top {limit} popular beatmaps")
# 获取过去24小时内最热门的beatmap # 获取过去24小时内最热门的beatmap
recent_time = datetime.now(UTC) - timedelta(hours=24) recent_time = utcnow() - timedelta(hours=24)
from app.database.score import Score from app.database.score import Score

View File

@@ -1,19 +1,20 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, datetime, timedelta from datetime import timedelta
from app.database import RankHistory, UserStatistics from app.database import RankHistory, UserStatistics
from app.database.rank_history import RankTop from app.database.rank_history import RankTop
from app.dependencies.database import with_db from app.dependencies.database import with_db
from app.dependencies.scheduler import get_scheduler from app.dependencies.scheduler import get_scheduler
from app.models.score import GameMode from app.models.score import GameMode
from app.utils import utcnow
from sqlmodel import col, exists, select, update from sqlmodel import col, exists, select, update
@get_scheduler().scheduled_job("cron", hour=0, minute=0, second=0, id="calculate_user_rank") @get_scheduler().scheduled_job("cron", hour=0, minute=0, second=0, id="calculate_user_rank")
async def calculate_user_rank(is_today: bool = False): 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) target_date = today if is_today else today - timedelta(days=1)
async with with_db() as session: async with with_db() as session:
for gamemode in GameMode: for gamemode in GameMode:

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, datetime, timedelta from datetime import UTC, timedelta
import json import json
from math import ceil from math import ceil
@@ -17,7 +17,7 @@ from app.log import logger
from app.models.metadata_hub import DailyChallengeInfo from app.models.metadata_hub import DailyChallengeInfo
from app.models.mods import APIMod, get_available_mods from app.models.mods import APIMod, get_available_mods
from app.models.room import RoomCategory 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 from .room import create_playlist_room
@@ -32,7 +32,7 @@ async def create_daily_challenge_room(
allowed_mods: list[APIMod] = [], allowed_mods: list[APIMod] = [],
) -> Room: ) -> Room:
async with with_db() as session: async with with_db() as session:
today = datetime.now(UTC).date() today = utcnow().date()
return await create_playlist_room( return await create_playlist_room(
session=session, session=session,
name=str(today), name=str(today),
@@ -57,7 +57,7 @@ async def create_daily_challenge_room(
async def daily_challenge_job(): async def daily_challenge_job():
from app.signalr.hub import MetadataHubs from app.signalr.hub import MetadataHubs
now = datetime.now(UTC) now = utcnow()
redis = get_redis() redis = get_redis()
key = f"daily_challenge:{now.date()}" key = f"daily_challenge:{now.date()}"
if not await redis.exists(key): if not await redis.exists(key):
@@ -67,7 +67,7 @@ async def daily_challenge_job():
await session.exec( await session.exec(
select(Room).where( select(Room).where(
Room.category == RoomCategory.DAILY_CHALLENGE, Room.category == RoomCategory.DAILY_CHALLENGE,
col(Room.ends_at) > datetime.now(UTC), col(Room.ends_at) > utcnow(),
) )
) )
).first() ).first()
@@ -87,7 +87,7 @@ async def daily_challenge_job():
get_scheduler().add_job( get_scheduler().add_job(
daily_challenge_job, daily_challenge_job,
"date", "date",
run_date=datetime.now(UTC) + timedelta(minutes=5), run_date=utcnow() + timedelta(minutes=5),
) )
return return
@@ -121,14 +121,14 @@ async def daily_challenge_job():
get_scheduler().add_job( get_scheduler().add_job(
daily_challenge_job, daily_challenge_job,
"date", "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") @get_scheduler().scheduled_job("cron", hour=0, minute=1, second=0, id="daily_challenge_last_top")
async def process_daily_challenge_top(): async def process_daily_challenge_top():
async with with_db() as session: async with with_db() as session:
now = datetime.now(UTC) now = utcnow()
room = ( room = (
await session.exec( await session.exec(
select(Room).where( select(Room).where(

View File

@@ -4,10 +4,11 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, datetime, timedelta from datetime import timedelta
from app.database.email_verification import EmailVerification, LoginSession from app.database.email_verification import EmailVerification, LoginSession
from app.log import logger from app.log import logger
from app.utils import utcnow
from sqlmodel import col, select from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
@@ -29,7 +30,7 @@ class DatabaseCleanupService:
""" """
try: try:
# 查找过期的验证码记录 # 查找过期的验证码记录
current_time = datetime.now(UTC) current_time = utcnow()
stmt = select(EmailVerification).where(EmailVerification.expires_at < current_time) stmt = select(EmailVerification).where(EmailVerification.expires_at < current_time)
result = await db.exec(stmt) result = await db.exec(stmt)
@@ -66,7 +67,7 @@ class DatabaseCleanupService:
""" """
try: try:
# 查找过期的登录会话记录 # 查找过期的登录会话记录
current_time = datetime.now(UTC) current_time = utcnow()
stmt = select(LoginSession).where(LoginSession.expires_at < current_time) stmt = select(LoginSession).where(LoginSession.expires_at < current_time)
result = await db.exec(stmt) result = await db.exec(stmt)
@@ -104,7 +105,7 @@ class DatabaseCleanupService:
""" """
try: 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)) stmt = select(EmailVerification).where(col(EmailVerification.is_used).is_(True))
result = await db.exec(stmt) result = await db.exec(stmt)
@@ -147,7 +148,7 @@ class DatabaseCleanupService:
""" """
try: 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)) stmt = select(LoginSession).where(col(LoginSession.is_verified).is_(True))
result = await db.exec(stmt) result = await db.exec(stmt)
@@ -225,7 +226,7 @@ class DatabaseCleanupService:
dict: 统计信息 dict: 统计信息
""" """
try: try:
current_time = datetime.now(UTC) current_time = utcnow()
cutoff_7_days = current_time - timedelta(days=7) cutoff_7_days = current_time - timedelta(days=7)
cutoff_30_days = current_time - timedelta(days=30) cutoff_30_days = current_time - timedelta(days=30)

View File

@@ -4,7 +4,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, datetime, timedelta from datetime import timedelta
import secrets import secrets
import string import string
@@ -12,6 +12,7 @@ from app.config import settings
from app.database.email_verification import EmailVerification, LoginSession from app.database.email_verification import EmailVerification, LoginSession
from app.log import logger from app.log import logger
from app.service.email_queue import email_queue # 导入邮件队列 from app.service.email_queue import email_queue # 导入邮件队列
from app.utils import utcnow
from redis.asyncio import Redis from redis.asyncio import Redis
from sqlmodel import col, select from sqlmodel import col, select
@@ -200,7 +201,7 @@ This email was sent automatically, please do not reply.
select(EmailVerification).where( select(EmailVerification).where(
EmailVerification.user_id == user_id, EmailVerification.user_id == user_id,
col(EmailVerification.is_used).is_(False), col(EmailVerification.is_used).is_(False),
EmailVerification.expires_at > datetime.now(UTC), EmailVerification.expires_at > utcnow(),
) )
) )
existing = existing_result.first() existing = existing_result.first()
@@ -217,7 +218,7 @@ This email was sent automatically, please do not reply.
user_id=user_id, user_id=user_id,
email=email, email=email,
verification_code=code, verification_code=code,
expires_at=datetime.now(UTC) + timedelta(minutes=10), # 10分钟过期 expires_at=utcnow() + timedelta(minutes=10), # 10分钟过期
ip_address=ip_address, ip_address=ip_address,
user_agent=user_agent, user_agent=user_agent,
) )
@@ -306,7 +307,7 @@ This email was sent automatically, please do not reply.
EmailVerification.user_id == user_id, EmailVerification.user_id == user_id,
EmailVerification.verification_code == code, EmailVerification.verification_code == code,
col(EmailVerification.is_used).is_(False), 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.is_used = True
verification.used_at = datetime.now(UTC) verification.used_at = utcnow()
# 同时更新对应的登录会话状态 # 同时更新对应的登录会话状态
await LoginSessionService.mark_session_verified(db, user_id) await LoginSessionService.mark_session_verified(db, user_id)
@@ -397,7 +398,7 @@ class LoginSessionService:
user_agent=None, user_agent=None,
country_code=country_code, country_code=country_code,
is_new_location=is_new_location, 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, # 新位置需要验证 is_verified=not is_new_location, # 新位置需要验证
) )
@@ -446,7 +447,7 @@ class LoginSessionService:
session = result.first() session = result.first()
if session: if session:
session.is_verified = True session.is_verified = True
session.verified_at = datetime.now(UTC) session.verified_at = utcnow()
await db.commit() await db.commit()
logger.info(f"[Login Session] User {user_id} session verification successful") logger.info(f"[Login Session] User {user_id} session verification successful")
@@ -463,7 +464,7 @@ class LoginSessionService:
"""检查是否为新位置登录""" """检查是否为新位置登录"""
try: try:
# 查看过去30天内是否有相同IP或相同国家的登录记录 # 查看过去30天内是否有相同IP或相同国家的登录记录
thirty_days_ago = datetime.now(UTC) - timedelta(days=30) thirty_days_ago = utcnow() - timedelta(days=30)
result = await db.exec( result = await db.exec(
select(LoginSession).where( select(LoginSession).where(
@@ -492,7 +493,7 @@ class LoginSessionService:
select(LoginSession).where( select(LoginSession).where(
LoginSession.user_id == user_id, LoginSession.user_id == user_id,
col(LoginSession.is_verified).is_(False), 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: for session in sessions:
session.is_verified = True session.is_verified = True
session.verified_at = datetime.now(UTC) session.verified_at = utcnow()
if sessions: if sessions:
logger.info(f"[Login Session] Marked {len(sessions)} session(s) as verified for user {user_id}") logger.info(f"[Login Session] Marked {len(sessions)} session(s) as verified for user {user_id}")

View File

@@ -16,6 +16,7 @@ from app.router.private.stats import (
_get_playing_users_count, _get_playing_users_count,
_redis_exec, _redis_exec,
) )
from app.utils import utcnow
# Redis keys for interval statistics # Redis keys for interval statistics
INTERVAL_STATS_BASE_KEY = "server:interval_stats" INTERVAL_STATS_BASE_KEY = "server:interval_stats"
@@ -34,7 +35,7 @@ class IntervalInfo:
def is_current(self) -> bool: def is_current(self) -> bool:
"""检查是否是当前区间""" """检查是否是当前区间"""
now = datetime.utcnow() now = utcnow()
return self.start_time <= now < self.end_time return self.start_time <= now < self.end_time
def to_dict(self) -> dict: def to_dict(self) -> dict:
@@ -101,7 +102,7 @@ class EnhancedIntervalStatsManager:
@staticmethod @staticmethod
def get_current_interval_boundaries() -> tuple[datetime, datetime]: def get_current_interval_boundaries() -> tuple[datetime, datetime]:
"""获取当前30分钟区间的边界""" """获取当前30分钟区间的边界"""
now = datetime.utcnow() now = utcnow()
# 计算区间开始时间向下取整到最近的30分钟 # 计算区间开始时间向下取整到最近的30分钟
minute = (now.minute // 30) * 30 minute = (now.minute // 30) * 30
start_time = now.replace(minute=minute, second=0, microsecond=0) start_time = now.replace(minute=minute, second=0, microsecond=0)
@@ -157,7 +158,7 @@ class EnhancedIntervalStatsManager:
peak_online_count=0, peak_online_count=0,
peak_playing_count=0, peak_playing_count=0,
total_samples=0, total_samples=0,
created_at=datetime.utcnow(), created_at=utcnow(),
) )
await _redis_exec( await _redis_exec(
@@ -195,7 +196,7 @@ class EnhancedIntervalStatsManager:
needed_points = 48 - history_length needed_points = 48 - history_length
# 从当前时间往前推创建缺失的时间点都填充为0 # 从当前时间往前推创建缺失的时间点都填充为0
current_time = datetime.utcnow() # noqa: F841 current_time = utcnow() # noqa: F841
current_interval_start, _ = EnhancedIntervalStatsManager.get_current_interval_boundaries() current_interval_start, _ = EnhancedIntervalStatsManager.get_current_interval_boundaries()
# 从当前区间开始往前推创建历史数据点确保时间对齐到30分钟边界 # 从当前区间开始往前推创建历史数据点确保时间对齐到30分钟边界
@@ -323,7 +324,7 @@ class EnhancedIntervalStatsManager:
peak_online_count=current_online, peak_online_count=current_online,
peak_playing_count=current_playing, peak_playing_count=current_playing,
total_samples=1, total_samples=1,
created_at=datetime.utcnow(), created_at=utcnow(),
) )
# 更新独特用户数 # 更新独特用户数
@@ -431,7 +432,7 @@ class EnhancedIntervalStatsManager:
try: try:
# 删除过期的区间统计数据超过2小时的 # 删除过期的区间统计数据超过2小时的
cutoff_time = datetime.utcnow() - timedelta(hours=2) cutoff_time = utcnow() - timedelta(hours=2)
pattern = f"{INTERVAL_STATS_BASE_KEY}:*" pattern = f"{INTERVAL_STATS_BASE_KEY}:*"
keys = await redis_async.keys(pattern) keys = await redis_async.keys(pattern)

View File

@@ -5,11 +5,11 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from datetime import datetime
from app.database.user_login_log import UserLoginLog from app.database.user_login_log import UserLoginLog
from app.dependencies.geoip import get_client_ip, get_geoip_helper, normalize_ip from app.dependencies.geoip import get_client_ip, get_geoip_helper, normalize_ip
from app.log import logger from app.log import logger
from app.utils import utcnow
from fastapi import Request from fastapi import Request
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
@@ -56,7 +56,7 @@ class LoginLogService:
user_id=user_id, user_id=user_id,
ip_address=ip_address, ip_address=ip_address,
user_agent=user_agent, user_agent=user_agent,
login_time=datetime.utcnow(), login_time=utcnow(),
login_success=login_success, login_success=login_success,
login_method=login_method, login_method=login_method,
notes=notes, notes=notes,

View File

@@ -6,11 +6,10 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime
from app.dependencies.database import get_redis from app.dependencies.database import get_redis
from app.log import logger from app.log import logger
from app.router.private.stats import add_online_user from app.router.private.stats import add_online_user
from app.utils import utcnow
class OnlineStatusManager: class OnlineStatusManager:
@@ -37,7 +36,7 @@ class OnlineStatusManager:
# 3. 设置最后活跃时间戳 # 3. 设置最后活跃时间戳
last_seen_key = f"user:last_seen:{user_id}" 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}") 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}" 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}") logger.debug(f"[OnlineStatusManager] Refreshed online status for user {user_id}")

View File

@@ -4,7 +4,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, datetime from datetime import datetime
import json import json
import secrets import secrets
import string import string
@@ -15,6 +15,7 @@ from app.dependencies.database import with_db
from app.log import logger from app.log import logger
from app.service.email_queue import email_queue # 导入邮件队列 from app.service.email_queue import email_queue # 导入邮件队列
from app.service.email_service import EmailService from app.service.email_service import EmailService
from app.utils import utcnow
from redis.asyncio import Redis from redis.asyncio import Redis
from sqlmodel import select from sqlmodel import select
@@ -88,7 +89,7 @@ class PasswordResetService:
"user_id": user.id, "user_id": user.id,
"email": email, "email": email,
"reset_code": reset_code, "reset_code": reset_code,
"created_at": datetime.now(UTC).isoformat(), "created_at": utcnow().isoformat(),
"ip_address": ip_address, "ip_address": ip_address,
"user_agent": user_agent, "user_agent": user_agent,
"used": False, "used": False,
@@ -346,7 +347,7 @@ class PasswordResetService:
try: try:
# 先标记验证码为已使用(在数据库操作之前) # 先标记验证码为已使用(在数据库操作之前)
reset_data["used"] = True reset_data["used"] = True
reset_data["used_at"] = datetime.now(UTC).isoformat() reset_data["used_at"] = utcnow().isoformat()
# 保存用户ID用于日志记录 # 保存用户ID用于日志记录
user_id = user.id user_id = user.id
@@ -391,7 +392,7 @@ class PasswordResetService:
# 计算剩余的TTL时间 # 计算剩余的TTL时间
created_at = datetime.fromisoformat(reset_data.get("created_at", "")) 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秒总过期时间 remaining_ttl = max(0, 600 - int(elapsed)) # 600秒总过期时间
if remaining_ttl > 0: if remaining_ttl > 0:

View File

@@ -6,7 +6,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from datetime import UTC, datetime from datetime import datetime
import json import json
from typing import TYPE_CHECKING, Literal from typing import TYPE_CHECKING, Literal
@@ -14,6 +14,7 @@ from app.config import settings
from app.database.statistics import UserStatistics, UserStatisticsResp from app.database.statistics import UserStatistics, UserStatisticsResp
from app.log import logger from app.log import logger
from app.models.score import GameMode from app.models.score import GameMode
from app.utils import utcnow
from redis.asyncio import Redis from redis.asyncio import Redis
from sqlmodel import col, select from sqlmodel import col, select
@@ -258,7 +259,7 @@ class RankingCacheService:
# 计算统计信息 # 计算统计信息
stats = { stats = {
"total_users": total_users, "total_users": total_users,
"last_updated": datetime.now(UTC).isoformat(), "last_updated": utcnow().isoformat(),
"type": type, "type": type,
"ruleset": ruleset, "ruleset": ruleset,
"country": country, "country": country,
@@ -370,7 +371,7 @@ class RankingCacheService:
# 计算统计信息 # 计算统计信息
stats = { stats = {
"total_countries": len(country_stats_list), "total_countries": len(country_stats_list),
"last_updated": datetime.now(UTC).isoformat(), "last_updated": utcnow().isoformat(),
"ruleset": ruleset, "ruleset": ruleset,
} }

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import UTC, datetime, timedelta from datetime import timedelta
from app.database.beatmap import Beatmap from app.database.beatmap import Beatmap
from app.database.chat import ChannelType, ChatChannel 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.database.room import APIUploadedRoom, Room
from app.dependencies.fetcher import get_fetcher from app.dependencies.fetcher import get_fetcher
from app.models.room import MatchType, QueueMode, RoomCategory, RoomStatus from app.models.room import MatchType, QueueMode, RoomCategory, RoomStatus
from app.utils import utcnow
from sqlalchemy import exists from sqlalchemy import exists
from sqlmodel import col, select 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: async def create_playlist_room_from_api(session: AsyncSession, room: APIUploadedRoom, host_id: int) -> Room:
db_room = room.to_room() db_room = room.to_room()
db_room.host_id = host_id 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) 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) session.add(db_room)
await session.commit() await session.commit()
@@ -52,8 +53,8 @@ async def create_playlist_room(
name=name, name=name,
category=category, category=category,
duration=duration, duration=duration,
starts_at=datetime.now(UTC), starts_at=utcnow(),
ends_at=datetime.now(UTC) + timedelta(minutes=duration), ends_at=utcnow() + timedelta(minutes=duration),
participant_count=0, participant_count=0,
max_attempts=max_attempts, max_attempts=max_attempts,
type=MatchType.PLAYLISTS, type=MatchType.PLAYLISTS,

View File

@@ -1,6 +1,6 @@
from __future__ import annotations 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.dependencies.database import get_redis, get_redis_message
from app.log import logger from app.log import logger
@@ -9,6 +9,7 @@ from app.router.private.stats import (
REDIS_PLAYING_USERS_KEY, REDIS_PLAYING_USERS_KEY,
_redis_exec, _redis_exec,
) )
from app.utils import utcnow
async def cleanup_stale_online_users() -> tuple[int, int]: 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) 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 stale_threshold = current_time - timedelta(hours=2) # 2小时无活动视为过期 # noqa: F841
# 对于在线用户我们检查metadata在线标记 # 对于在线用户我们检查metadata在线标记

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from datetime import datetime, timedelta from datetime import timedelta
from app.log import logger from app.log import logger
from app.router.private.stats import record_hourly_stats, update_registered_users_count 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, cleanup_stale_online_users,
refresh_redis_key_expiry, refresh_redis_key_expiry,
) )
from app.utils import utcnow
class StatsScheduler: class StatsScheduler:
@@ -60,7 +61,7 @@ class StatsScheduler:
while self._running: while self._running:
try: try:
# 计算下次区间结束时间 # 计算下次区间结束时间
now = datetime.utcnow() now = utcnow()
# 计算当前区间的结束时间 # 计算当前区间的结束时间
current_minute = (now.minute // 30) * 30 current_minute = (now.minute // 30) * 30
@@ -93,15 +94,11 @@ class StatsScheduler:
# 完成当前区间并记录到历史 # 完成当前区间并记录到历史
finalized_stats = await EnhancedIntervalStatsManager.finalize_interval() finalized_stats = await EnhancedIntervalStatsManager.finalize_interval()
if finalized_stats: if finalized_stats:
logger.info( logger.info(f"Finalized enhanced interval statistics at {utcnow().strftime('%Y-%m-%d %H:%M:%S')}")
f"Finalized enhanced interval statistics at {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')}"
)
else: else:
# 如果区间完成失败,使用原有方式记录 # 如果区间完成失败,使用原有方式记录
await record_hourly_stats() await record_hourly_stats()
logger.info( logger.info(f"Recorded hourly statistics (fallback) at {utcnow().strftime('%Y-%m-%d %H:%M:%S')}")
f"Recorded hourly statistics (fallback) at {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')}"
)
# 开始新的区间统计 # 开始新的区间统计
await EnhancedIntervalStatsManager.initialize_current_interval() await EnhancedIntervalStatsManager.initialize_current_interval()

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import asyncio import asyncio
from collections import defaultdict from collections import defaultdict
from collections.abc import Coroutine from collections.abc import Coroutine
from datetime import UTC, datetime
import math import math
from typing import override from typing import override
@@ -27,6 +26,7 @@ from app.models.metadata_hub import (
) )
from app.models.room import RoomCategory from app.models.room import RoomCategory
from app.service.subscribers.score_processed import ScoreSubscriber from app.service.subscribers.score_processed import ScoreSubscriber
from app.utils import utcnow
from .hub import Client, Hub from .hub import Client, Hub
@@ -41,11 +41,11 @@ class MetadataHub(Hub[MetadataClientState]):
self.subscriber = ScoreSubscriber() self.subscriber = ScoreSubscriber()
self.subscriber.metadata_hub = self self.subscriber.metadata_hub = self
self._daily_challenge_stats: MultiplayerRoomStats | None = None self._daily_challenge_stats: MultiplayerRoomStats | None = None
self._today = datetime.now(UTC).date() self._today = utcnow().date()
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
def get_daily_challenge_stats(self, daily_challenge_room: int) -> MultiplayerRoomStats: 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( self._daily_challenge_stats = MultiplayerRoomStats(
room_id=daily_challenge_room, room_id=daily_challenge_room,
playlist_item_stats={}, playlist_item_stats={},
@@ -98,7 +98,7 @@ class MetadataHub(Hub[MetadataClientState]):
async with with_db() as session: async with with_db() as session:
async with session.begin(): async with session.begin():
user = (await session.exec(select(User).where(User.id == int(state.connection_id)))).one() 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() await session.commit()
@override @override
@@ -149,7 +149,7 @@ class MetadataHub(Hub[MetadataClientState]):
daily_challenge_room = ( daily_challenge_room = (
await session.exec( await session.exec(
select(Room).where( select(Room).where(
col(Room.ends_at) > datetime.now(UTC), col(Room.ends_at) > utcnow(),
Room.category == RoomCategory.DAILY_CHALLENGE, Room.category == RoomCategory.DAILY_CHALLENGE,
) )
) )

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from datetime import UTC, datetime, timedelta from datetime import timedelta
from typing import override from typing import override
from app.database import Room from app.database import Room
@@ -44,6 +44,7 @@ from app.models.room import (
RoomStatus, RoomStatus,
) )
from app.models.score import GameMode from app.models.score import GameMode
from app.utils import utcnow
from .hub import Client, Hub from .hub import Client, Hub
@@ -233,7 +234,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
item = room.playlist[0] item = room.playlist[0]
item.owner_id = client.user_id item.owner_id = client.user_id
room.room_id = db_room.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))) beatmap_exists = await session.exec(select(exists().where(col(Beatmap.id) == item.beatmap_id)))
if not beatmap_exists.one(): if not beatmap_exists.one():
fetcher = await get_fetcher() fetcher = await get_fetcher()
@@ -306,7 +307,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
session.add(participated_user) session.add(participated_user)
else: else:
participated_user.left_at = None 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) db_room = await session.get(Room, room_id)
if db_room is None: if db_room is None:
@@ -1056,7 +1057,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
) )
).first() ).first()
if participated_user is not None: 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) db_room = await session.get(Room, room.room.room_id)
if db_room is None: if db_room is None:
@@ -1087,7 +1088,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
.where(col(Room.id) == room.room.room_id) .where(col(Room.id) == room.room.room_id)
.values( .values(
name=room.room.settings.name, name=room.room.settings.name,
ends_at=datetime.now(UTC), ends_at=utcnow(),
type=room.room.settings.match_type, type=room.room.settings.match_type,
queue_mode=room.room.settings.queue_mode, queue_mode=room.room.settings.queue_mode,
auto_skip=room.room.settings.auto_skip, auto_skip=room.room.settings.auto_skip,

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Awaitable, Callable, Sequence from collections.abc import Awaitable, Callable, Sequence
from datetime import datetime from datetime import UTC, datetime
import functools import functools
import inspect import inspect
from io import BytesIO from io import BytesIO
@@ -270,3 +270,7 @@ class BackgroundTasks:
bg_tasks = BackgroundTasks() bg_tasks = BackgroundTasks()
def utcnow() -> datetime:
return datetime.now(tz=UTC)

View File

@@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import datetime
from pathlib import Path from pathlib import Path
from app.config import settings 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.osu_rx_statistics import create_rx_statistics
from app.service.redis_message_system import redis_message_system from app.service.redis_message_system import redis_message_system
from app.service.stats_scheduler import start_stats_scheduler, stop_stats_scheduler 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 import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
@@ -157,7 +156,7 @@ async def root():
@app.get("/health", include_in_schema=False) @app.get("/health", include_in_schema=False)
async def health_check(): async def health_check():
"""健康检查端点""" """健康检查端点"""
return {"status": "ok", "timestamp": datetime.utcnow().isoformat()} return {"status": "ok", "timestamp": utcnow().isoformat()}
@app.exception_handler(RequestValidationError) @app.exception_handler(RequestValidationError)