201 lines
5.9 KiB
Python
201 lines
5.9 KiB
Python
"""
|
||
倒计时服务 - 独立管理宝箱倒计时
|
||
"""
|
||
import asyncio
|
||
from typing import Dict, Optional, Callable
|
||
from datetime import datetime
|
||
from ..core.database import SessionLocal
|
||
from ..models.game import Chest
|
||
from ..utils.redis import get_pool_cache
|
||
|
||
|
||
class CountdownManager:
|
||
"""倒计时管理器 - 单例模式"""
|
||
|
||
_instance: Optional['CountdownManager'] = None
|
||
|
||
def __new__(cls) -> 'CountdownManager':
|
||
"""单例模式实现"""
|
||
if cls._instance is None:
|
||
cls._instance = super().__new__(cls)
|
||
cls._instance._initialized = False
|
||
return cls._instance
|
||
|
||
def __init__(self):
|
||
"""初始化倒计时管理器"""
|
||
if self._initialized:
|
||
return
|
||
|
||
self._initialized = True
|
||
self._chests: Dict[int, 'ChestCountdown'] = {} # 宝箱ID -> 倒计时实例
|
||
self._tasks: Dict[int, asyncio.Task] = {} # 宝箱ID -> 任务
|
||
self._lock = asyncio.Lock() # 线程锁
|
||
|
||
async def start_chest_countdown(
|
||
self,
|
||
chest: Chest,
|
||
on_update: Callable[[int, int], None],
|
||
on_expire: Callable[[int], None] = None
|
||
):
|
||
"""
|
||
为宝箱启动倒计时
|
||
|
||
Args:
|
||
chest: 宝箱对象
|
||
on_update: 倒计时更新回调函数 (chest_id, time_remaining)
|
||
on_expire: 倒计时结束回调函数 (chest_id)
|
||
"""
|
||
chest_id = chest.id
|
||
|
||
# 如果已存在,先停止
|
||
if chest_id in self._tasks:
|
||
await self.stop_chest_countdown(chest_id)
|
||
|
||
# 创建倒计时实例
|
||
countdown = ChestCountdown(chest, on_update, on_expire)
|
||
|
||
async with self._lock:
|
||
self._chests[chest_id] = countdown
|
||
|
||
# 启动倒计时任务
|
||
task = asyncio.create_task(self._run_countdown(chest_id))
|
||
async with self._lock:
|
||
self._tasks[chest_id] = task
|
||
|
||
async def stop_chest_countdown(self, chest_id: int):
|
||
"""停止宝箱倒计时"""
|
||
async with self._lock:
|
||
# 取消任务
|
||
if chest_id in self._tasks:
|
||
self._tasks[chest_id].cancel()
|
||
try:
|
||
await self._tasks[chest_id]
|
||
except asyncio.CancelledError:
|
||
pass
|
||
del self._tasks[chest_id]
|
||
|
||
# 移除倒计时实例
|
||
if chest_id in self._chests:
|
||
del self._chests[chest_id]
|
||
|
||
async def get_remaining_time(self, chest_id: int) -> int:
|
||
"""
|
||
获取宝箱剩余时间(秒)
|
||
|
||
Args:
|
||
chest_id: 宝箱ID
|
||
|
||
Returns:
|
||
剩余秒数,0表示已结束
|
||
"""
|
||
async with self._lock:
|
||
countdown = self._chests.get(chest_id)
|
||
if countdown:
|
||
return countdown.get_remaining_time()
|
||
return 0
|
||
|
||
async def _run_countdown(self, chest_id: int):
|
||
"""
|
||
运行倒计时任务
|
||
|
||
Args:
|
||
chest_id: 宝箱ID
|
||
"""
|
||
try:
|
||
while True:
|
||
async with self._lock:
|
||
countdown = self._chests.get(chest_id)
|
||
if not countdown:
|
||
break
|
||
|
||
# 计算剩余时间
|
||
remaining = countdown.get_remaining_time()
|
||
|
||
# 检查是否结束
|
||
if remaining <= 0:
|
||
# 调用过期回调
|
||
if countdown.on_expire:
|
||
countdown.on_expire(chest_id)
|
||
break
|
||
|
||
# 调用更新回调
|
||
if countdown.on_update:
|
||
countdown.on_update(chest_id, remaining)
|
||
|
||
# 每秒更新一次
|
||
await asyncio.sleep(1)
|
||
|
||
except asyncio.CancelledError:
|
||
# 任务被取消,正常退出
|
||
pass
|
||
except Exception as e:
|
||
print(f"Countdown task error for chest {chest_id}: {e}")
|
||
finally:
|
||
# 清理资源
|
||
await self.stop_chest_countdown(chest_id)
|
||
|
||
|
||
class ChestCountdown:
|
||
"""单个宝箱的倒计时实例"""
|
||
|
||
def __init__(
|
||
self,
|
||
chest: Chest,
|
||
on_update: Callable[[int, int], None],
|
||
on_expire: Optional[Callable[[int], None]] = None
|
||
):
|
||
"""
|
||
初始化宝箱倒计时
|
||
|
||
Args:
|
||
chest: 宝箱对象
|
||
on_update: 更新回调函数
|
||
on_expire: 过期回调函数
|
||
"""
|
||
self.chest = chest
|
||
self.on_update = on_update
|
||
self.on_expire = on_expire
|
||
self._start_time = datetime.now()
|
||
|
||
def get_remaining_time(self) -> int:
|
||
"""
|
||
计算剩余时间
|
||
|
||
Returns:
|
||
剩余秒数
|
||
"""
|
||
# 如果宝箱状态不是投注中,返回0
|
||
if self.chest.status != 0: # BETTING
|
||
return 0
|
||
|
||
# 获取宝箱创建时间
|
||
created_time = self.chest.created_at
|
||
if isinstance(created_time, str):
|
||
# 处理ISO格式字符串
|
||
if 'T' in created_time:
|
||
created_time = datetime.fromisoformat(
|
||
created_time.replace('Z', '')
|
||
)
|
||
else:
|
||
# 处理MySQL datetime格式 (YYYY-MM-DD HH:MM:SS)
|
||
created_time = datetime.strptime(
|
||
created_time,
|
||
'%Y-%m-%d %H:%M:%S'
|
||
)
|
||
else:
|
||
# 确保转换为naive datetime
|
||
created_time = created_time.replace(
|
||
tzinfo=None
|
||
) if created_time and created_time.tzinfo else created_time
|
||
|
||
# 使用本地时间计算(与数据库func.now()保持一致)
|
||
now = datetime.now()
|
||
elapsed = (now - created_time).total_seconds()
|
||
remaining = max(0, int(self.chest.countdown_seconds - elapsed))
|
||
|
||
return remaining
|
||
|
||
|
||
# 全局倒计时管理器实例
|
||
countdown_manager = CountdownManager()
|