from datetime import datetime, timedelta from celery import shared_task from sqlalchemy import select, and_ import logging logger = logging.getLogger(__name__) @shared_task def check_silent_customers(): from app.database import AsyncSessionLocal from app.models.customer import Customer from app.models.user import User from app.services.push import PushService from app.services.notification import NotificationService async def _check(): async with AsyncSessionLocal() as db: now = datetime.utcnow() for days in [3, 7, 14]: cutoff = now - timedelta(days=days) result = await db.execute( select(Customer).where( and_( Customer.status.in_(["lead", "negotiating"]), Customer.last_contact_at.isnot(None), Customer.last_contact_at < cutoff, ) ) ) customers = result.scalars().all() for c in customers: messages = { 3: ("跟进提醒", f"客户 {c.name} 已沉默3天,建议发送跟进消息"), 7: ("跟进升级", f"客户 {c.name} 已沉默1周,建议发送优惠或新产品信息"), 14: ("跟进提示", f"客户 {c.name} 已沉默14天,建议换话题重新接触"), } title, content = messages.get(days, ("跟进提醒", f"客户 {c.name} 已沉默{days}天")) logger.info(f"Customer {c.name} silent for {days} days") user_result = await db.execute( select(User).where(User.id == c.user_id) ) user = user_result.scalar_one_or_none() if user: PushService.send_notification(c.user_id, title, content) await NotificationService.create_notification( db, c.user_id, title, content, notification_type="customer_silent", reference_type="customer", reference_id=str(c.id), ) import asyncio asyncio.run(_check()) return "Checked silent customers" @shared_task def batch_translate_texts(texts: list, target_lang: str, user_id: str): from app.services.translation import TranslationService async def _translate(): service = TranslationService() results = [] for text in texts: result = await service.translate(text, target_lang, user_id=user_id) results.append(result) return results import asyncio return asyncio.run(_translate()) @shared_task def generate_quotation_pdf(quotation_id: str): from app.database import AsyncSessionLocal from app.models.quotation import Quotation, QuotationItem from app.models.customer import Customer from app.services.pdf_generator import pdf_generator async def _generate(): async with AsyncSessionLocal() as db: result = await db.execute( select(Quotation).where(Quotation.id == quotation_id) ) q = result.scalar_one_or_none() if not q: return {"error": "Quotation not found"} items_result = await db.execute( select(QuotationItem).where(QuotationItem.quotation_id == q.id) ) items = items_result.scalars().all() customer = None if q.customer_id: cust_result = await db.execute( select(Customer).where(Customer.id == q.customer_id) ) customer = cust_result.scalar_one_or_none() data = { "quotation_number": f"{str(q.id)[:8].upper()}", "customer_name": customer.name if customer else "", "customer_company": customer.company if customer else "", "customer_country": customer.country if customer else "", "date": q.created_at.strftime("%Y-%m-%d") if q.created_at else "", "valid_until": q.valid_until or "", "currency": q.currency or "USD", "items": [ { "product_name": i.product_name, "description": i.description, "quantity": i.quantity, "unit_price": i.unit_price, "total_price": i.total_price, "unit": i.unit or "pcs", } for i in items ], "subtotal": q.subtotal or 0, "discount": q.discount or 0, "shipping": q.shipping or 0, "total": q.total or q.subtotal or 0, "payment_terms": q.payment_terms or "", "delivery_terms": q.delivery_terms or "", "lead_time": q.lead_time or "", "notes": q.notes or "", } pdf_bytes = pdf_generator.generate_quotation(data) if pdf_bytes: upload_dir = settings.UPLOAD_DIR pdf_path = os.path.join(upload_dir, f"quotation_{quotation_id}.pdf") os.makedirs(upload_dir, exist_ok=True) with open(pdf_path, "wb") as f: f.write(pdf_bytes) q.pdf_url = pdf_path await db.flush() return {"success": True, "pdf_path": pdf_path, "quotation_id": str(q.id)} else: return {"error": "PDF generation failed (weasyprint not available)", "quotation_id": str(q.id)} import asyncio return asyncio.run(_generate()) @shared_task def process_corpus_quality(): from app.database import AsyncSessionLocal from app.models.corpus import CorpusEntry async def _process(): async with AsyncSessionLocal() as db: result = await db.execute( select(CorpusEntry).where( and_( CorpusEntry.quality_score < 0.5, CorpusEntry.usage_count > 5, ) ).limit(100) ) entries = result.scalars().all() for e in entries: e.quality_score = min(1.0, e.quality_score + 0.1) await db.commit() return f"Processed {len(entries)} entries" import asyncio return asyncio.run(_process()) @shared_task(bind=True, max_retries=3, default_retry_delay=60) def process_customer_import(self, user_id: str, records: list): from app.database import AsyncSessionLocal from app.services.customer import CustomerService async def _import(): async with AsyncSessionLocal() as db: svc = CustomerService(db) imported = 0 errors = [] for i, record in enumerate(records): try: await svc.create_customer(user_id, record) imported += 1 except Exception as e: errors.append(f"Row {i+2}: {str(e)}") return {"imported": imported, "total": len(records), "errors": errors} import asyncio return asyncio.run(_import()) @shared_task def run_daily_corpus_training(): from app.database import AsyncSessionLocal from app.services.corpus_trainer import CorpusTrainer async def _train(): async with AsyncSessionLocal() as db: trainer = CorpusTrainer(db) result = await trainer.run_pipeline() logger.info(f"Daily corpus training complete: {result}") return result import asyncio return asyncio.run(_train()) @shared_task def update_customer_health_cache(): from app.database import AsyncSessionLocal from app.services.customer_health import CustomerHealthService from app.models.user import User from app.config import settings async def _update(): async with AsyncSessionLocal() as db: result = await db.execute(select(User.id)) user_ids = result.scalars().all() svc = CustomerHealthService(db) for uid in user_ids: try: overview = await svc.get_health_overview(uid) scores = await svc.get_all_health_scores(uid) except Exception as e: logger.error(f"Health cache failed for user {uid}: {e}") return f"Updated health cache for {len(user_ids)} users" import asyncio return asyncio.run(_update()) @shared_task def cleanup_old_sessions(): import redis.asyncio as aioredis async def _cleanup(): r = await aioredis.from_url(settings.REDIS_URL) keys = await r.keys("session:*") if keys: await r.delete(*keys) return f"Cleaned up {len(keys)} sessions" import asyncio return asyncio.run(_cleanup()) @shared_task def send_followup_reminder(customer_id: str, user_id: str): from app.database import AsyncSessionLocal from app.models.customer import Customer from app.services.customer import CustomerService async def _send(): async with AsyncSessionLocal() as db: result = await db.execute( select(Customer).where( and_(Customer.id == customer_id, Customer.user_id == user_id) ) ) c = result.scalar_one_or_none() if c: logger.info(f"Sending followup reminder for customer {c.name}") return {"customer_id": str(c.id), "customer_name": c.name} return {"error": "Customer not found"} import asyncio return asyncio.run(_send()) @shared_task def check_followup_engine(): from app.database import AsyncSessionLocal from app.services.followup_engine import FollowupEngine async def _check(): async with AsyncSessionLocal() as db: engine = FollowupEngine(db) result = await engine.scan_and_followup() logger.info(f"Followup engine check complete: {result}") return result import asyncio return asyncio.run(_check())