baoxiang/backend/app/services/scheduler_service.py
2025-12-16 19:03:48 +08:00

262 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
定时任务服务
用于处理自动发放低保、自动封盘等定时任务
"""
import asyncio
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from ..core.database import get_db, SessionLocal
from ..models.user import User, Transaction
from ..models.game import Chest, ChestStatus
from ..services.user_service import UserService
from ..services.game_service import GameService
from ..core.config import settings
from ..services.balance_monitor_service import BalanceMonitorService
class SchedulerService:
"""定时任务服务"""
@staticmethod
async def start_scheduler():
"""
启动定时任务调度器
"""
# 创建后台任务运行定时器
asyncio.create_task(SchedulerService._run_daily_allowance_scheduler())
# 创建后台任务扫描过期宝箱
asyncio.create_task(SchedulerService._run_expired_chest_scanner())
# 创建后台任务扫描余额清零用户
asyncio.create_task(SchedulerService._run_zero_balance_scanner())
@staticmethod
async def _run_daily_allowance_scheduler():
"""
运行每日低保发放调度器
每天凌晨1点执行
"""
while True:
# 获取当前时间
now = datetime.now()
# 计算下次执行时间明天凌晨1点
next_run = now.replace(hour=1, minute=0, second=0, microsecond=0) + timedelta(days=1)
# 计算等待时间
sleep_seconds = (next_run - now).total_seconds()
# 等待到下次执行时间
await asyncio.sleep(sleep_seconds)
# 执行每日低保发放
try:
SchedulerService._distribute_daily_allowance()
except Exception as e:
print(f"每日低保发放失败: {e}")
@staticmethod
def _distribute_daily_allowance():
"""
发放每日低保
为所有余额低于低保门槛的用户发放低保
"""
db_gen = get_db()
db: Session = next(db_gen)
try:
# 获取所有余额低于低保门槛的活跃用户
users = db.query(User).filter(
User.balance < settings.game.ALLOWANCE_THRESHOLD,
User.status == "ACTIVE"
).all()
allowance_amount = settings.game.DAILY_ALLOWANCE
success_count = 0
for user in users:
try:
# 检查用户是否满足领取条件
if SchedulerService._can_claim_allowance(db, user):
# 发放低保
user.balance += allowance_amount
# 记录交易
transaction = Transaction(
user_id=user.id,
type="低保",
amount=allowance_amount,
balance_after=user.balance,
description="每日自动发放低保"
)
db.add(transaction)
success_count += 1
except Exception as e:
print(f"为用户 {user.username} 发放低保时出错: {e}")
continue
# 提交事务
db.commit()
print(f"每日低保发放完成,成功为 {success_count} 名用户发放低保")
except Exception as e:
db.rollback()
print(f"每日低保发放过程中出现错误: {e}")
finally:
# 关闭数据库连接
db.close()
@staticmethod
def _can_claim_allowance(db: Session, user: User) -> bool:
"""
检查用户是否可以领取低保
"""
# 检查是否已领取过低保
last_allowance = db.query(Transaction).filter(
Transaction.user_id == user.id,
Transaction.type == "低保"
).order_by(Transaction.created_at.desc()).first()
# 如果从未领取过低保,则可以领取
if not last_allowance:
return True
# 如果距离上次低保已经超过24小时则可以领取
time_since_last = datetime.now() - last_allowance.created_at
return time_since_last >= timedelta(hours=24)
@staticmethod
async def _run_expired_chest_scanner():
"""
运行过期宝箱扫描器
每10秒扫描一次自动锁定过期的宝箱
"""
while True:
try:
# 扫描并锁定过期宝箱
expired_count = SchedulerService._scan_and_lock_expired_chests()
if expired_count > 0:
print(f"自动锁定 {expired_count} 个过期宝箱")
except Exception as e:
print(f"扫描过期宝箱时出错: {e}")
# 每10秒扫描一次比1秒更高效
await asyncio.sleep(10)
@staticmethod
def _scan_and_lock_expired_chests() -> int:
"""
扫描并锁定所有过期的宝箱
Returns:
锁定宝箱的数量
"""
db = SessionLocal()
try:
# 获取所有状态为下注中的宝箱
chests = db.query(Chest).filter(
Chest.status == ChestStatus.BETTING
).all()
# 使用本地时间进行计算与数据库func.now()保持一致)
now = datetime.now()
expired_count = 0
expired_chest_ids = []
for chest in chests:
# 计算已过时间
elapsed = (now - chest.created_at).total_seconds()
# 检查是否过期增加0.5秒容差)
if elapsed >= chest.countdown_seconds - 0.5:
# 锁定宝箱
chest.status = ChestStatus.LOCKED
chest.locked_at = now
expired_count += 1
expired_chest_ids.append(chest.id)
if expired_count > 0:
db.commit()
print(f"已自动锁定 {expired_count} 个过期宝箱: {expired_chest_ids}")
return expired_count
except Exception as e:
db.rollback()
print(f"锁定过期宝箱时出错: {e}")
return 0
finally:
db.close()
@staticmethod
async def _run_zero_balance_scanner():
"""
运行余额清零用户扫描器
每天零点执行,扫描遗漏的余额清零用户并发放奖励
"""
while True:
try:
# 获取当前时间
now = datetime.now()
# 计算下次执行时间(每天零点)
next_run = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
# 如果是刚过零点00:00-00:10执行扫描
if now.hour == 0 and now.minute < 10:
print("执行余额清零用户扫描任务...")
SchedulerService._scan_zero_balance_users()
# 计算等待时间(到下一个整点)
sleep_seconds = (next_run - now).total_seconds()
# 等待到下次执行时间
await asyncio.sleep(sleep_seconds)
except Exception as e:
print(f"余额清零扫描任务执行出错: {e}")
# 出错后等待1小时再试
await asyncio.sleep(3600)
@staticmethod
def _scan_zero_balance_users():
"""
扫描余额清零用户并发放奖励
用于定时任务,确保没有遗漏
"""
db = SessionLocal()
try:
# 使用BalanceMonitorService扫描
processed_count = BalanceMonitorService.scan_zero_balance_users(db)
print(f"余额清零扫描完成,处理了 {processed_count} 个用户")
except Exception as e:
print(f"扫描余额清零用户时出错: {e}")
finally:
db.close()
@staticmethod
def reset_daily_allowance_status():
"""
重置每日低保领取状态(供手动调用)
"""
try:
from ..utils.redis import redis_client
# 获取所有低保领取状态的Redis键
keys = redis_client.keys("daily_allowance:*")
if keys:
# 删除所有低保领取状态
deleted_count = redis_client.delete(*keys)
print(f"已清理 {deleted_count} 个低保领取状态标记")
return deleted_count
else:
print("无需清理(暂无低保领取状态标记)")
return 0
except Exception as e:
print(f"重置低保状态时出错: {e}")
return 0