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
|