feat(daily-challenge): simple implement
This commit is contained in:
@@ -25,7 +25,7 @@ from .playlist_best_score import PlaylistBestScore
|
|||||||
from .playlists import Playlist, PlaylistResp
|
from .playlists import Playlist, PlaylistResp
|
||||||
from .pp_best_score import PPBestScore
|
from .pp_best_score import PPBestScore
|
||||||
from .relationship import Relationship, RelationshipResp, RelationshipType
|
from .relationship import Relationship, RelationshipResp, RelationshipType
|
||||||
from .room import Room, RoomResp
|
from .room import APIUploadedRoom, Room, RoomResp
|
||||||
from .room_participated_user import RoomParticipatedUser
|
from .room_participated_user import RoomParticipatedUser
|
||||||
from .score import (
|
from .score import (
|
||||||
MultiplayerScores,
|
MultiplayerScores,
|
||||||
@@ -48,6 +48,7 @@ from .user_account_history import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"APIUploadedRoom",
|
||||||
"Beatmap",
|
"Beatmap",
|
||||||
"Beatmapset",
|
"Beatmapset",
|
||||||
"BeatmapsetResp",
|
"BeatmapsetResp",
|
||||||
|
|||||||
@@ -138,6 +138,6 @@ class PlaylistResp(PlaylistBase):
|
|||||||
) -> "PlaylistResp":
|
) -> "PlaylistResp":
|
||||||
data = playlist.model_dump()
|
data = playlist.model_dump()
|
||||||
if "beatmap" in include:
|
if "beatmap" in include:
|
||||||
data["beatmap"] = await BeatmapResp.from_db(playlist.beatmap, from_set=True)
|
data["beatmap"] = await BeatmapResp.from_db(playlist.beatmap)
|
||||||
resp = cls.model_validate(data)
|
resp = cls.model_validate(data)
|
||||||
return resp
|
return resp
|
||||||
|
|||||||
@@ -160,3 +160,18 @@ class RoomResp(RoomBase):
|
|||||||
participant_count=len(room.users),
|
participant_count=len(room.users),
|
||||||
)
|
)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
class APIUploadedRoom(RoomBase):
|
||||||
|
def to_room(self) -> Room:
|
||||||
|
"""
|
||||||
|
将 APIUploadedRoom 转换为 Room 对象,playlist 字段需单独处理。
|
||||||
|
"""
|
||||||
|
room_dict = self.model_dump()
|
||||||
|
room_dict.pop("playlist", None)
|
||||||
|
# host_id 已在字段中
|
||||||
|
return Room(**room_dict)
|
||||||
|
|
||||||
|
id: int | None
|
||||||
|
host_id: int | None = None
|
||||||
|
playlist: list[Playlist] = Field(default_factory=list)
|
||||||
|
|||||||
26
app/dependencies/scheduler.py
Normal file
26
app/dependencies/scheduler.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import UTC
|
||||||
|
|
||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
|
||||||
|
scheduler: AsyncIOScheduler | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def init_scheduler():
|
||||||
|
global scheduler
|
||||||
|
scheduler = AsyncIOScheduler(timezone=UTC)
|
||||||
|
scheduler.start()
|
||||||
|
|
||||||
|
|
||||||
|
def get_scheduler() -> AsyncIOScheduler:
|
||||||
|
global scheduler
|
||||||
|
if scheduler is None:
|
||||||
|
init_scheduler()
|
||||||
|
return scheduler # pyright: ignore[reportReturnType]
|
||||||
|
|
||||||
|
|
||||||
|
def stop_scheduler():
|
||||||
|
global scheduler
|
||||||
|
if scheduler:
|
||||||
|
scheduler.shutdown()
|
||||||
@@ -123,3 +123,7 @@ class OnlineStatus(IntEnum):
|
|||||||
OFFLINE = 0 # 隐身
|
OFFLINE = 0 # 隐身
|
||||||
DO_NOT_DISTURB = 1
|
DO_NOT_DISTURB = 1
|
||||||
ONLINE = 2
|
ONLINE = 2
|
||||||
|
|
||||||
|
|
||||||
|
class DailyChallengeInfo(BaseModel):
|
||||||
|
room_id: int
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from app.database.beatmap import Beatmap, BeatmapResp
|
from app.database.beatmap import Beatmap, BeatmapResp
|
||||||
@@ -8,13 +8,14 @@ from app.database.beatmapset import BeatmapsetResp
|
|||||||
from app.database.lazer_user import User, UserResp
|
from app.database.lazer_user import User, UserResp
|
||||||
from app.database.multiplayer_event import MultiplayerEvent, MultiplayerEventResp
|
from app.database.multiplayer_event import MultiplayerEvent, MultiplayerEventResp
|
||||||
from app.database.playlist_attempts import ItemAttemptsCount, ItemAttemptsResp
|
from app.database.playlist_attempts import ItemAttemptsCount, ItemAttemptsResp
|
||||||
from app.database.playlists import Playlist, PlaylistBase, PlaylistResp
|
from app.database.playlists import Playlist, PlaylistResp
|
||||||
from app.database.room import Room, RoomBase, RoomResp
|
from app.database.room import APIUploadedRoom, Room, RoomResp
|
||||||
from app.database.room_participated_user import RoomParticipatedUser
|
from app.database.room_participated_user import RoomParticipatedUser
|
||||||
from app.database.score import Score
|
from app.database.score import Score
|
||||||
from app.dependencies.database import get_db, get_redis
|
from app.dependencies.database import get_db, get_redis
|
||||||
from app.dependencies.user import get_current_user
|
from app.dependencies.user import 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.signalr.hub import MultiplayerHubs
|
from app.signalr.hub import MultiplayerHubs
|
||||||
|
|
||||||
from .api_router import router
|
from .api_router import router
|
||||||
@@ -92,21 +93,6 @@ class APICreatedRoom(RoomResp):
|
|||||||
error: str = ""
|
error: str = ""
|
||||||
|
|
||||||
|
|
||||||
class APIUploadedRoom(RoomBase):
|
|
||||||
def to_room(self) -> Room:
|
|
||||||
"""
|
|
||||||
将 APIUploadedRoom 转换为 Room 对象,playlist 字段需单独处理。
|
|
||||||
"""
|
|
||||||
room_dict = self.model_dump()
|
|
||||||
room_dict.pop("playlist", None)
|
|
||||||
# host_id 已在字段中
|
|
||||||
return Room(**room_dict)
|
|
||||||
|
|
||||||
id: int | None
|
|
||||||
host_id: int | None = None
|
|
||||||
playlist: list[PlaylistBase] = Field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
async def _participate_room(
|
async def _participate_room(
|
||||||
room_id: int, user_id: int, db_room: Room, session: AsyncSession
|
room_id: int, user_id: int, db_room: Room, session: AsyncSession
|
||||||
):
|
):
|
||||||
@@ -137,27 +123,11 @@ async def create_room(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
# db_room = Room.from_resp(room)
|
|
||||||
await db.refresh(current_user)
|
|
||||||
user_id = current_user.id
|
user_id = current_user.id
|
||||||
db_room = room.to_room()
|
db_room = await create_playlist_room_from_api(db, room, user_id)
|
||||||
db_room.host_id = current_user.id if current_user.id else 1
|
|
||||||
db_room.starts_at = datetime.now(UTC)
|
|
||||||
db_room.ends_at = db_room.starts_at + timedelta(
|
|
||||||
minutes=db_room.duration if db_room.duration is not None else 0
|
|
||||||
)
|
|
||||||
db.add(db_room)
|
|
||||||
await db.commit()
|
|
||||||
await db.refresh(db_room)
|
|
||||||
await _participate_room(db_room.id, user_id, db_room, db)
|
await _participate_room(db_room.id, user_id, db_room, db)
|
||||||
|
# await db.commit()
|
||||||
for item in room.playlist:
|
# await db.refresh(db_room)
|
||||||
item.id = await Playlist.get_next_id_for_room(db_room.id, db)
|
|
||||||
item.room_id = db_room.id
|
|
||||||
item.owner_id = user_id if user_id else 1
|
|
||||||
db.add(item)
|
|
||||||
await db.commit()
|
|
||||||
await db.refresh(db_room)
|
|
||||||
created_room = APICreatedRoom.model_validate(await RoomResp.from_db(db_room, db))
|
created_room = APICreatedRoom.model_validate(await RoomResp.from_db(db_room, db))
|
||||||
created_room.error = ""
|
created_room.error = ""
|
||||||
return created_room
|
return created_room
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from app.dependencies.fetcher import get_fetcher
|
|||||||
from app.dependencies.user import get_current_user
|
from app.dependencies.user import get_current_user
|
||||||
from app.fetcher import Fetcher
|
from app.fetcher import Fetcher
|
||||||
from app.models.beatmap import BeatmapRankStatus
|
from app.models.beatmap import BeatmapRankStatus
|
||||||
|
from app.models.room import RoomCategory
|
||||||
from app.models.score import (
|
from app.models.score import (
|
||||||
INT_TO_MODE,
|
INT_TO_MODE,
|
||||||
GameMode,
|
GameMode,
|
||||||
@@ -402,6 +403,10 @@ async def index_playlist_scores(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
session: AsyncSession = Depends(get_db),
|
session: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
|
room = await session.get(Room, room_id)
|
||||||
|
if not room:
|
||||||
|
raise HTTPException(status_code=404, detail="Room not found")
|
||||||
|
|
||||||
limit = clamp(limit, 1, 50)
|
limit = clamp(limit, 1, 50)
|
||||||
|
|
||||||
scores = (
|
scores = (
|
||||||
@@ -426,6 +431,12 @@ async def index_playlist_scores(
|
|||||||
score.position = await get_position(room_id, playlist_id, score.id, session)
|
score.position = await get_position(room_id, playlist_id, score.id, session)
|
||||||
if score.user_id == current_user.id:
|
if score.user_id == current_user.id:
|
||||||
user_score = score
|
user_score = score
|
||||||
|
|
||||||
|
if room.category == RoomCategory.DAILY_CHALLENGE:
|
||||||
|
score_resp = [s for s in score_resp if s.passed]
|
||||||
|
if user_score and not user_score.passed:
|
||||||
|
user_score = None
|
||||||
|
|
||||||
resp = IndexedScoreResp(
|
resp = IndexedScoreResp(
|
||||||
scores=score_resp,
|
scores=score_resp,
|
||||||
user_score=user_score,
|
user_score=user_score,
|
||||||
|
|||||||
10
app/service/__init__.py
Normal file
10
app/service/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .daily_challenge import create_daily_challenge_room
|
||||||
|
from .room import create_playlist_room, create_playlist_room_from_api
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"create_daily_challenge_room",
|
||||||
|
"create_playlist_room",
|
||||||
|
"create_playlist_room_from_api",
|
||||||
|
]
|
||||||
103
app/service/daily_challenge.py
Normal file
103
app/service/daily_challenge.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
import json
|
||||||
|
|
||||||
|
from app.database.playlists import Playlist
|
||||||
|
from app.database.room import Room
|
||||||
|
from app.dependencies.database import engine, get_redis
|
||||||
|
from app.dependencies.scheduler import get_scheduler
|
||||||
|
from app.log import logger
|
||||||
|
from app.models.metadata_hub import DailyChallengeInfo
|
||||||
|
from app.models.mods import APIMod
|
||||||
|
from app.models.room import RoomCategory
|
||||||
|
from app.signalr.hub import MetadataHubs
|
||||||
|
|
||||||
|
from .room import create_playlist_room
|
||||||
|
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
|
async def create_daily_challenge_room(
|
||||||
|
beatmap: int, ruleset_id: int, required_mods: list[APIMod] = []
|
||||||
|
) -> Room:
|
||||||
|
async with AsyncSession(engine) as session:
|
||||||
|
today = datetime.now(UTC).date()
|
||||||
|
return await create_playlist_room(
|
||||||
|
session=session,
|
||||||
|
name=str(today),
|
||||||
|
host_id=3,
|
||||||
|
playlist=[
|
||||||
|
Playlist(
|
||||||
|
id=0,
|
||||||
|
room_id=0,
|
||||||
|
owner_id=3,
|
||||||
|
ruleset_id=ruleset_id,
|
||||||
|
beatmap_id=beatmap,
|
||||||
|
required_mods=required_mods,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
category=RoomCategory.DAILY_CHALLENGE,
|
||||||
|
duration=24 * 60 - 2, # remain 2 minute to apply new daily challenge
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@get_scheduler().scheduled_job("cron", hour=0, minute=0, second=0, id="daily_challenge")
|
||||||
|
async def daily_challenge_job():
|
||||||
|
today = datetime.now(UTC).date()
|
||||||
|
redis = get_redis()
|
||||||
|
key = f"daily_challenge:{today.year}-{today.month}-{today.day}"
|
||||||
|
if not await redis.exists(key):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
beatmap = await redis.hget(key, "beatmap") # pyright: ignore[reportGeneralTypeIssues]
|
||||||
|
ruleset_id = await redis.hget(key, "ruleset_id") # pyright: ignore[reportGeneralTypeIssues]
|
||||||
|
required_mods = await redis.hget(key, "required_mods") # pyright: ignore[reportGeneralTypeIssues]
|
||||||
|
|
||||||
|
if beatmap is None or ruleset_id is None:
|
||||||
|
logger.warning(
|
||||||
|
f"[DailyChallenge] Missing required data for daily challenge {today}."
|
||||||
|
" Will try again in 5 minutes."
|
||||||
|
)
|
||||||
|
get_scheduler().add_job(
|
||||||
|
daily_challenge_job,
|
||||||
|
"date",
|
||||||
|
run_date=datetime.now(UTC) + timedelta(minutes=5),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
beatmap_int = int(beatmap)
|
||||||
|
ruleset_id_int = int(ruleset_id)
|
||||||
|
|
||||||
|
mods_list = []
|
||||||
|
if required_mods:
|
||||||
|
mods_list = json.loads(required_mods)
|
||||||
|
|
||||||
|
room = await create_daily_challenge_room(
|
||||||
|
beatmap=beatmap_int,
|
||||||
|
ruleset_id=ruleset_id_int,
|
||||||
|
required_mods=mods_list,
|
||||||
|
)
|
||||||
|
await MetadataHubs.broadcast_call(
|
||||||
|
"DailyChallengeUpdated", DailyChallengeInfo(room_id=room.id)
|
||||||
|
)
|
||||||
|
logger.success(
|
||||||
|
"[DailyChallenge] Added today's daily challenge: "
|
||||||
|
f"{beatmap=}, {ruleset_id=}, {required_mods=}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except (ValueError, json.JSONDecodeError) as e:
|
||||||
|
logger.warning(
|
||||||
|
f"[DailyChallenge] Error processing daily challenge data: {e}"
|
||||||
|
" Will try again in 5 minutes."
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
f"[DailyChallenge] Unexpected error in daily challenge job: {e}"
|
||||||
|
" Will try again in 5 minutes."
|
||||||
|
)
|
||||||
|
get_scheduler().add_job(
|
||||||
|
daily_challenge_job,
|
||||||
|
"date",
|
||||||
|
run_date=datetime.now(UTC) + timedelta(minutes=5),
|
||||||
|
)
|
||||||
78
app/service/room.py
Normal file
78
app/service/room.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
|
from app.database.beatmap import Beatmap
|
||||||
|
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 sqlalchemy import exists
|
||||||
|
from sqlmodel import col, select
|
||||||
|
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.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()
|
||||||
|
await session.refresh(db_room)
|
||||||
|
await add_playlists_to_room(session, db_room.id, room.playlist, host_id)
|
||||||
|
await session.refresh(db_room)
|
||||||
|
return db_room
|
||||||
|
|
||||||
|
|
||||||
|
async def create_playlist_room(
|
||||||
|
session: AsyncSession,
|
||||||
|
name: str,
|
||||||
|
host_id: int,
|
||||||
|
category: RoomCategory = RoomCategory.NORMAL,
|
||||||
|
duration: int = 30,
|
||||||
|
max_attempts: int | None = None,
|
||||||
|
playlist: list[Playlist] = [],
|
||||||
|
) -> Room:
|
||||||
|
db_room = Room(
|
||||||
|
name=name,
|
||||||
|
category=category,
|
||||||
|
duration=duration,
|
||||||
|
starts_at=datetime.now(UTC),
|
||||||
|
ends_at=datetime.now(UTC) + timedelta(minutes=duration),
|
||||||
|
participant_count=0,
|
||||||
|
max_attempts=max_attempts,
|
||||||
|
type=MatchType.PLAYLISTS,
|
||||||
|
queue_mode=QueueMode.HOST_ONLY,
|
||||||
|
auto_skip=False,
|
||||||
|
auto_start_duration=0,
|
||||||
|
status=RoomStatus.IDLE,
|
||||||
|
host_id=host_id,
|
||||||
|
)
|
||||||
|
session.add(db_room)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(db_room)
|
||||||
|
await add_playlists_to_room(session, db_room.id, playlist, host_id)
|
||||||
|
await session.refresh(db_room)
|
||||||
|
return db_room
|
||||||
|
|
||||||
|
|
||||||
|
async def add_playlists_to_room(
|
||||||
|
session: AsyncSession, room_id: int, playlist: list[Playlist], owner_id: int
|
||||||
|
):
|
||||||
|
for item in playlist:
|
||||||
|
if not (
|
||||||
|
await session.exec(select(exists().where(col(Beatmap.id) == item.beatmap)))
|
||||||
|
).first():
|
||||||
|
fetcher = await get_fetcher()
|
||||||
|
await Beatmap.get_or_fetch(session, fetcher, item.beatmap_id)
|
||||||
|
item.id = await Playlist.get_next_id_for_room(room_id, session)
|
||||||
|
item.room_id = room_id
|
||||||
|
item.owner_id = owner_id
|
||||||
|
session.add(item)
|
||||||
|
await session.commit()
|
||||||
@@ -6,12 +6,19 @@ from datetime import UTC, datetime
|
|||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from app.database import Relationship, RelationshipType, User
|
from app.database import Relationship, RelationshipType, User
|
||||||
|
from app.database.room import Room
|
||||||
from app.dependencies.database import engine, get_redis
|
from app.dependencies.database import engine, get_redis
|
||||||
from app.models.metadata_hub import MetadataClientState, OnlineStatus, UserActivity
|
from app.models.metadata_hub import (
|
||||||
|
DailyChallengeInfo,
|
||||||
|
MetadataClientState,
|
||||||
|
OnlineStatus,
|
||||||
|
UserActivity,
|
||||||
|
)
|
||||||
|
from app.models.room import RoomCategory
|
||||||
|
|
||||||
from .hub import Client, Hub
|
from .hub import Client, Hub
|
||||||
|
|
||||||
from sqlmodel import select
|
from sqlmodel import col, select
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
ONLINE_PRESENCE_WATCHERS_GROUP = "metadata:online-presence-watchers"
|
ONLINE_PRESENCE_WATCHERS_GROUP = "metadata:online-presence-watchers"
|
||||||
@@ -107,6 +114,23 @@ class MetadataHub(Hub[MetadataClientState]):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
await asyncio.gather(*tasks)
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
daily_challenge_room = (
|
||||||
|
await session.exec(
|
||||||
|
select(Room).where(
|
||||||
|
col(Room.ends_at) > datetime.now(UTC),
|
||||||
|
Room.category == RoomCategory.DAILY_CHALLENGE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
if daily_challenge_room:
|
||||||
|
await self.call_noblock(
|
||||||
|
client,
|
||||||
|
"DailyChallengeUpdated",
|
||||||
|
DailyChallengeInfo(
|
||||||
|
room_id=daily_challenge_room.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
redis = get_redis()
|
redis = get_redis()
|
||||||
await redis.set(f"metadata:online:{user_id}", "")
|
await redis.set(f"metadata:online:{user_id}", "")
|
||||||
|
|
||||||
|
|||||||
3
main.py
3
main.py
@@ -6,6 +6,7 @@ from datetime import datetime
|
|||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.dependencies.database import create_tables, engine, redis_client
|
from app.dependencies.database import create_tables, engine, redis_client
|
||||||
from app.dependencies.fetcher import get_fetcher
|
from app.dependencies.fetcher import get_fetcher
|
||||||
|
from app.dependencies.scheduler import init_scheduler, stop_scheduler
|
||||||
from app.router import (
|
from app.router import (
|
||||||
api_router,
|
api_router,
|
||||||
auth_router,
|
auth_router,
|
||||||
@@ -21,8 +22,10 @@ async def lifespan(app: FastAPI):
|
|||||||
# on startup
|
# on startup
|
||||||
await create_tables()
|
await create_tables()
|
||||||
await get_fetcher() # 初始化 fetcher
|
await get_fetcher() # 初始化 fetcher
|
||||||
|
init_scheduler()
|
||||||
# on shutdown
|
# on shutdown
|
||||||
yield
|
yield
|
||||||
|
stop_scheduler()
|
||||||
await engine.dispose()
|
await engine.dispose()
|
||||||
await redis_client.aclose()
|
await redis_client.aclose()
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ requires-python = ">=3.12"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aiomysql>=0.2.0",
|
"aiomysql>=0.2.0",
|
||||||
"alembic>=1.12.1",
|
"alembic>=1.12.1",
|
||||||
|
"apscheduler>=3.11.0",
|
||||||
"bcrypt>=4.1.2",
|
"bcrypt>=4.1.2",
|
||||||
"cryptography>=41.0.7",
|
"cryptography>=41.0.7",
|
||||||
"fastapi>=0.104.1",
|
"fastapi>=0.104.1",
|
||||||
|
|||||||
37
uv.lock
generated
37
uv.lock
generated
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 2
|
revision = 3
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
[manifest]
|
[manifest]
|
||||||
@@ -57,6 +57,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "apscheduler"
|
||||||
|
version = "3.11.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "tzlocal" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4e/00/6d6814ddc19be2df62c8c898c4df6b5b1914f3bd024b780028caa392d186/apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133", size = 107347, upload-time = "2024-11-24T19:39:26.463Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/ae/9a053dd9229c0fde6b1f1f33f609ccff1ee79ddda364c756a924c6d8563b/APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da", size = 64004, upload-time = "2024-11-24T19:39:24.442Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bcrypt"
|
name = "bcrypt"
|
||||||
version = "4.3.0"
|
version = "4.3.0"
|
||||||
@@ -493,6 +505,7 @@ source = { virtual = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiomysql" },
|
{ name = "aiomysql" },
|
||||||
{ name = "alembic" },
|
{ name = "alembic" },
|
||||||
|
{ name = "apscheduler" },
|
||||||
{ name = "bcrypt" },
|
{ name = "bcrypt" },
|
||||||
{ name = "cryptography" },
|
{ name = "cryptography" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
@@ -522,6 +535,7 @@ dev = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "aiomysql", specifier = ">=0.2.0" },
|
{ name = "aiomysql", specifier = ">=0.2.0" },
|
||||||
{ name = "alembic", specifier = ">=1.12.1" },
|
{ name = "alembic", specifier = ">=1.12.1" },
|
||||||
|
{ name = "apscheduler", specifier = ">=3.11.0" },
|
||||||
{ name = "bcrypt", specifier = ">=4.1.2" },
|
{ name = "bcrypt", specifier = ">=4.1.2" },
|
||||||
{ name = "cryptography", specifier = ">=41.0.7" },
|
{ name = "cryptography", specifier = ">=41.0.7" },
|
||||||
{ name = "fastapi", specifier = ">=0.104.1" },
|
{ name = "fastapi", specifier = ">=0.104.1" },
|
||||||
@@ -904,6 +918,27 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2025.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzlocal"
|
||||||
|
version = "5.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.35.0"
|
version = "0.35.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user