262 lines
9.0 KiB
Python
262 lines
9.0 KiB
Python
"""
|
||
定时任务服务
|
||
用于处理自动发放低保、自动封盘等定时任务
|
||
"""
|
||
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
|