""" 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'