baoxiang/backend/app/services/scheduler_service.py

262 lines
9.0 KiB
Python
Raw Normal View History

2025-12-16 18:06:50 +08:00
"""
定时任务服务
用于处理自动发放低保自动封盘等定时任务
"""
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
2025-12-16 19:03:48 +08:00
from ..services.balance_monitor_service import BalanceMonitorService
2025-12-16 18:06:50 +08:00
class SchedulerService:
"""定时任务服务"""
2025-12-16 19:03:48 +08:00
2025-12-16 18:06:50 +08:00
@staticmethod
async def start_scheduler():
"""
启动定时任务调度器
"""
# 创建后台任务运行定时器
asyncio.create_task(SchedulerService._run_daily_allowance_scheduler())
# 创建后台任务扫描过期宝箱
asyncio.create_task(SchedulerService._run_expired_chest_scanner())
2025-12-16 19:03:48 +08:00
# 创建后台任务扫描余额清零用户
asyncio.create_task(SchedulerService._run_zero_balance_scanner())
2025-12-16 18:06:50 +08:00
@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:
2025-12-16 19:03:48 +08:00
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