""" 定时任务服务 用于处理自动发放低保、自动封盘等定时任务 """ 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 ..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: # 从数据库动态读取低保配置 from ..services.system_service import ConfigManager allowance_threshold = ConfigManager.get_allowance_threshold(db) allowance_amount = ConfigManager.get_daily_allowance(db) # 获取所有余额低于低保门槛的活跃用户 users = db.query(User).filter( User.balance < allowance_threshold, User.status == "ACTIVE" ).all() 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