feat(scheduler): implement scheduler service for background tasks and credit refills; add endpoints for admin control

This commit is contained in:
JSC
2025-07-02 13:39:17 +02:00
parent 703212656f
commit 1b597f4047
6 changed files with 304 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
"""Credit management service for handling daily credit refills."""
import logging
from datetime import datetime
from app.database import db
from app.models.user import User
logger = logging.getLogger(__name__)
class CreditService:
"""Service for managing user credits and daily refills."""
@staticmethod
def refill_all_users_credits() -> dict:
"""
Refill credits for all active users based on their plan.
This function:
1. Gets all active users
2. For each user, adds their plan's daily credit amount
3. Ensures credits never exceed the plan's max_credits limit
4. Updates all users in a single database transaction
Returns:
dict: Summary of the refill operation
"""
try:
# Get all active users with their plans
users = User.query.filter_by(is_active=True).all()
if not users:
logger.info("No active users found for credit refill")
return {
"success": True,
"users_processed": 0,
"credits_added": 0,
"message": "No active users found"
}
users_processed = 0
total_credits_added = 0
for user in users:
if not user.plan:
logger.warning(f"User {user.email} has no plan assigned, skipping")
continue
# Calculate new credit amount, capped at plan max
current_credits = user.credits or 0
plan_daily_credits = user.plan.credits
max_credits = user.plan.max_credits
# Add daily credits but don't exceed maximum
new_credits = min(current_credits + plan_daily_credits, max_credits)
credits_added = new_credits - current_credits
if credits_added > 0:
user.credits = new_credits
user.updated_at = datetime.utcnow()
total_credits_added += credits_added
logger.debug(
f"User {user.email}: {current_credits} -> {new_credits} "
f"(+{credits_added} credits, plan: {user.plan.code})"
)
else:
logger.debug(
f"User {user.email}: Already at max credits "
f"({current_credits}/{max_credits})"
)
users_processed += 1
# Commit all changes in a single transaction
db.session.commit()
logger.info(
f"Daily credit refill completed: {users_processed} users processed, "
f"{total_credits_added} total credits added"
)
return {
"success": True,
"users_processed": users_processed,
"credits_added": total_credits_added,
"message": f"Successfully refilled credits for {users_processed} users"
}
except Exception as e:
# Rollback transaction on error
db.session.rollback()
logger.error(f"Error during daily credit refill: {str(e)}")
return {
"success": False,
"users_processed": 0,
"credits_added": 0,
"error": str(e),
"message": "Credit refill failed"
}
@staticmethod
def get_user_credit_info(user_id: int) -> dict:
"""
Get detailed credit information for a specific user.
Args:
user_id: The user's ID
Returns:
dict: User's credit information
"""
user = User.query.get(user_id)
if not user:
return {"error": "User not found"}
if not user.plan:
return {"error": "User has no plan assigned"}
return {
"user_id": user.id,
"email": user.email,
"current_credits": user.credits,
"plan": {
"code": user.plan.code,
"name": user.plan.name,
"daily_credits": user.plan.credits,
"max_credits": user.plan.max_credits
},
"is_active": user.is_active
}

View File

@@ -0,0 +1,107 @@
"""Scheduler service for managing background tasks with APScheduler."""
import logging
from typing import Optional
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from app.services.credit_service import CreditService
logger = logging.getLogger(__name__)
class SchedulerService:
"""Service for managing scheduled background tasks."""
def __init__(self) -> None:
"""Initialize the scheduler service."""
self.scheduler: Optional[BackgroundScheduler] = None
def start(self) -> None:
"""Start the scheduler and add all scheduled jobs."""
if self.scheduler is not None:
logger.warning("Scheduler is already running")
return
self.scheduler = BackgroundScheduler()
# Add daily credit refill job
self._add_daily_credit_refill_job()
# Start the scheduler
self.scheduler.start()
logger.info("Scheduler started successfully")
def stop(self) -> None:
"""Stop the scheduler."""
if self.scheduler is not None:
self.scheduler.shutdown()
self.scheduler = None
logger.info("Scheduler stopped")
def _add_daily_credit_refill_job(self) -> None:
"""Add the daily credit refill job."""
if self.scheduler is None:
raise RuntimeError("Scheduler not initialized")
# Schedule daily at 00:00 UTC
trigger = CronTrigger(hour=0, minute=0)
self.scheduler.add_job(
func=self._run_daily_credit_refill,
trigger=trigger,
id="daily_credit_refill",
name="Daily Credit Refill",
replace_existing=True,
)
logger.info("Daily credit refill job scheduled for 00:00 UTC")
def _run_daily_credit_refill(self) -> None:
"""Execute the daily credit refill task."""
logger.info("Starting daily credit refill task")
try:
result = CreditService.refill_all_users_credits()
if result["success"]:
logger.info(
f"Daily credit refill completed successfully: "
f"{result['users_processed']} users processed, "
f"{result['credits_added']} credits added"
)
else:
logger.error(f"Daily credit refill failed: {result['message']}")
except Exception as e:
logger.exception(f"Error during daily credit refill: {e}")
def trigger_credit_refill_now(self) -> dict:
"""Manually trigger credit refill for testing purposes."""
logger.info("Manually triggering credit refill")
return CreditService.refill_all_users_credits()
def get_scheduler_status(self) -> dict:
"""Get the current status of the scheduler."""
if self.scheduler is None:
return {"running": False, "jobs": []}
jobs = []
for job in self.scheduler.get_jobs():
jobs.append({
"id": job.id,
"name": job.name,
"next_run": job.next_run_time.isoformat()
if job.next_run_time else None,
"trigger": str(job.trigger),
})
return {
"running": self.scheduler.running,
"jobs": jobs,
}
# Global scheduler instance
scheduler_service = SchedulerService()