c6206787da
项目结构: - backend/ Python FastAPI 后端 - uni-app/ uni-app跨端前端 - docs/ 设计文档 - docker-compose.yml Docker编排 - nginx/scripts/systemd 运维配置 已完成功能: - 用户认证 (JWT) - 智能翻译 + 回复建议 - 营销素材生成 - 客户管理 + 沉默检测 - 报价单管理 - 产品库管理 - 汇率换算 - 推送通知 (uni-push) - WhatsApp Webhook框架 - Celery定时任务
193 lines
6.1 KiB
Python
193 lines
6.1 KiB
Python
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
|
|
|
|
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:
|
|
if days == 3:
|
|
logger.info(f"Customer {c.name} silent for 3 days")
|
|
elif days == 7:
|
|
logger.info(f"Customer {c.name} silent for 7 days - upgrade")
|
|
else:
|
|
logger.info(f"Customer {c.name} silent for 14 days - recommend new approach")
|
|
|
|
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
|
|
|
|
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()
|
|
|
|
pdf_content = generate_pdf_text(q, items)
|
|
|
|
return {"pdf_content": pdf_content, "quotation_id": str(q.id)}
|
|
|
|
import asyncio
|
|
return asyncio.run(_generate())
|
|
|
|
|
|
def generate_pdf_text(quotation, items):
|
|
from datetime import datetime
|
|
|
|
lines = [
|
|
"=" * 60,
|
|
f"QUOTATION",
|
|
f"#{str(quotation.id)[:8].upper()}",
|
|
"=" * 60,
|
|
f"Date: {datetime.utcnow().strftime('%Y-%m-%d')}",
|
|
]
|
|
|
|
if quotation.valid_until:
|
|
lines.append(f"Valid Until: {quotation.valid_until}")
|
|
|
|
lines.append("")
|
|
lines.append(f"{'Item':<30} {'Qty':<8} {'Unit Price':<12} {'Total':<12}")
|
|
lines.append("-" * 62)
|
|
|
|
for item in items:
|
|
lines.append(
|
|
f"{item.product_name:<30} {item.quantity:<8} ${item.unit_price:<10.2f} ${item.total_price:<10.2f}"
|
|
)
|
|
|
|
lines.append("-" * 62)
|
|
if quotation.subtotal:
|
|
lines.append(f"{'Subtotal':>48} ${quotation.subtotal:<10.2f}")
|
|
if quotation.discount:
|
|
lines.append(f"{'Discount':>48} -${quotation.discount:<10.2f}")
|
|
if quotation.shipping:
|
|
lines.append(f"{'Shipping':>48} ${quotation.shipping:<10.2f}")
|
|
lines.append(f"{'TOTAL':>48} ${quotation.total or quotation.subtotal or 0:<10.2f}")
|
|
|
|
lines.append("")
|
|
if quotation.payment_terms:
|
|
lines.append(f"Payment Terms: {quotation.payment_terms}")
|
|
if quotation.delivery_terms:
|
|
lines.append(f"Delivery Terms: {quotation.delivery_terms}")
|
|
if quotation.lead_time:
|
|
lines.append(f"Lead Time: {quotation.lead_time}")
|
|
if quotation.notes:
|
|
lines.append(f"Notes: {quotation.notes}")
|
|
|
|
lines.append("=" * 60)
|
|
lines.append("Generated by TradeMate")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
@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
|
|
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()) |