1ST
This commit is contained in:
226
backend/tasks.py
Normal file
226
backend/tasks.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
Celery Tasks for Background Jobs
|
||||
處理排程任務,包括提醒郵件和摘要報告
|
||||
"""
|
||||
|
||||
from celery import Celery
|
||||
from datetime import datetime, date, timedelta
|
||||
from sqlalchemy import and_, or_
|
||||
from models import (
|
||||
db, TodoItem, TodoItemResponsible, TodoItemFollower,
|
||||
TodoUserPref, TodoAuditLog
|
||||
)
|
||||
from utils.email_service import EmailService
|
||||
from utils.notification_service import NotificationService
|
||||
from utils.logger import get_logger
|
||||
import os
|
||||
|
||||
# 建立 Celery 實例
|
||||
def make_celery(app):
|
||||
celery = Celery(
|
||||
app.import_name,
|
||||
backend=app.config['CELERY_RESULT_BACKEND'],
|
||||
broker=app.config['CELERY_BROKER_URL']
|
||||
)
|
||||
celery.conf.update(app.config)
|
||||
|
||||
class ContextTask(celery.Task):
|
||||
"""Make celery tasks work with Flask app context"""
|
||||
def __call__(self, *args, **kwargs):
|
||||
with app.app_context():
|
||||
return self.run(*args, **kwargs)
|
||||
|
||||
celery.Task = ContextTask
|
||||
return celery
|
||||
|
||||
# 建立 Flask 應用程式和 Celery
|
||||
def create_celery_app():
|
||||
"""建立 Celery 應用程式,延遲導入避免循環依賴"""
|
||||
from app import create_app
|
||||
flask_app = create_app()
|
||||
return make_celery(flask_app), flask_app
|
||||
|
||||
# 全局變數,延遲初始化
|
||||
celery = None
|
||||
flask_app = None
|
||||
|
||||
def get_celery():
|
||||
"""獲取 Celery 實例"""
|
||||
global celery, flask_app
|
||||
if celery is None:
|
||||
celery, flask_app = create_celery_app()
|
||||
return celery
|
||||
logger = get_logger(__name__)
|
||||
|
||||
def send_daily_reminders():
|
||||
"""發送每日提醒郵件"""
|
||||
try:
|
||||
celery_app = get_celery()
|
||||
from app import create_app
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
today = date.today()
|
||||
tomorrow = today + timedelta(days=1)
|
||||
|
||||
# 查找明日到期的待辦事項
|
||||
due_tomorrow = db.session.query(TodoItem).filter(
|
||||
and_(
|
||||
TodoItem.due_date == tomorrow,
|
||||
TodoItem.status != 'DONE'
|
||||
)
|
||||
).all()
|
||||
|
||||
# 查找已逾期的待辦事項
|
||||
overdue = db.session.query(TodoItem).filter(
|
||||
and_(
|
||||
TodoItem.due_date < today,
|
||||
TodoItem.status != 'DONE'
|
||||
)
|
||||
).all()
|
||||
|
||||
email_service = EmailService()
|
||||
notification_service = NotificationService()
|
||||
sent_count = 0
|
||||
|
||||
# 處理明日到期提醒
|
||||
for todo in due_tomorrow:
|
||||
recipients = notification_service.get_notification_recipients(todo)
|
||||
for recipient in recipients:
|
||||
try:
|
||||
# 檢查用戶是否啟用郵件提醒
|
||||
user_pref = TodoUserPref.query.filter_by(ad_account=recipient).first()
|
||||
if not user_pref or not user_pref.email_reminder_enabled:
|
||||
continue
|
||||
|
||||
if email_service.send_reminder_email(todo, recipient, 'due_tomorrow'):
|
||||
sent_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send due tomorrow reminder to {recipient}: {str(e)}")
|
||||
|
||||
# 處理逾期提醒
|
||||
for todo in overdue:
|
||||
recipients = notification_service.get_notification_recipients(todo)
|
||||
for recipient in recipients:
|
||||
try:
|
||||
# 檢查用戶是否啟用郵件提醒
|
||||
user_pref = TodoUserPref.query.filter_by(ad_account=recipient).first()
|
||||
if not user_pref or not user_pref.email_reminder_enabled:
|
||||
continue
|
||||
|
||||
if email_service.send_reminder_email(todo, recipient, 'overdue'):
|
||||
sent_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send overdue reminder to {recipient}: {str(e)}")
|
||||
|
||||
# 記錄稽核日誌
|
||||
audit = TodoAuditLog(
|
||||
actor_ad='system',
|
||||
todo_id=None,
|
||||
action='DAILY_REMINDER',
|
||||
detail={
|
||||
'due_tomorrow_count': len(due_tomorrow),
|
||||
'overdue_count': len(overdue),
|
||||
'emails_sent': sent_count
|
||||
}
|
||||
)
|
||||
db.session.add(audit)
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Daily reminders sent: {sent_count} emails for {len(due_tomorrow + overdue)} todos")
|
||||
return {
|
||||
'sent_count': sent_count,
|
||||
'due_tomorrow': len(due_tomorrow),
|
||||
'overdue': len(overdue)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Daily reminders task failed: {str(e)}")
|
||||
raise
|
||||
|
||||
@celery.task
|
||||
def send_weekly_summary():
|
||||
"""發送每週摘要報告"""
|
||||
try:
|
||||
with flask_app.app_context():
|
||||
# 取得所有啟用週報的用戶
|
||||
users = TodoUserPref.query.filter_by(weekly_summary_enabled=True).all()
|
||||
|
||||
email_service = EmailService()
|
||||
notification_service = NotificationService()
|
||||
sent_count = 0
|
||||
|
||||
for user in users:
|
||||
try:
|
||||
# 準備週報資料
|
||||
digest_data = notification_service.prepare_digest(user.ad_account, 'weekly')
|
||||
|
||||
if email_service.send_digest_email(user.ad_account, digest_data):
|
||||
sent_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send weekly summary to {user.ad_account}: {str(e)}")
|
||||
|
||||
# 記錄稽核日誌
|
||||
audit = TodoAuditLog(
|
||||
actor_ad='system',
|
||||
todo_id=None,
|
||||
action='WEEKLY_SUMMARY',
|
||||
detail={
|
||||
'users_count': len(users),
|
||||
'emails_sent': sent_count
|
||||
}
|
||||
)
|
||||
db.session.add(audit)
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Weekly summary sent: {sent_count} emails to {len(users)} users")
|
||||
return {
|
||||
'sent_count': sent_count,
|
||||
'total_users': len(users)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Weekly summary task failed: {str(e)}")
|
||||
raise
|
||||
|
||||
@celery.task
|
||||
def cleanup_old_logs():
|
||||
"""清理舊的日誌記錄"""
|
||||
try:
|
||||
with flask_app.app_context():
|
||||
# 清理30天前的稽核日誌
|
||||
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
|
||||
deleted_count = TodoAuditLog.query.filter(
|
||||
TodoAuditLog.created_at < thirty_days_ago
|
||||
).delete()
|
||||
|
||||
db.session.commit()
|
||||
logger.info(f"Cleaned up {deleted_count} old audit logs")
|
||||
return {'deleted_count': deleted_count}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cleanup logs task failed: {str(e)}")
|
||||
raise
|
||||
|
||||
# Celery Beat 排程配置
|
||||
celery.conf.beat_schedule = {
|
||||
# 每日早上9點發送提醒
|
||||
'daily-reminders': {
|
||||
'task': 'tasks.send_daily_reminders',
|
||||
'schedule': 60.0 * 60.0 * 24.0, # 24小時
|
||||
'options': {'expires': 3600}
|
||||
},
|
||||
# 每週一早上9點發送週報
|
||||
'weekly-summary': {
|
||||
'task': 'tasks.send_weekly_summary',
|
||||
'schedule': 60.0 * 60.0 * 24.0 * 7.0, # 7天
|
||||
'options': {'expires': 3600}
|
||||
},
|
||||
# 每週清理一次舊日誌
|
||||
'cleanup-logs': {
|
||||
'task': 'tasks.cleanup_old_logs',
|
||||
'schedule': 60.0 * 60.0 * 24.0 * 7.0, # 7天
|
||||
'options': {'expires': 3600}
|
||||
}
|
||||
}
|
||||
|
||||
celery.conf.timezone = 'Asia/Taipei'
|
Reference in New Issue
Block a user