Initial commit: TradeMate 外贸小助手 MVP
项目结构: - backend/ Python FastAPI 后端 - uni-app/ uni-app跨端前端 - docs/ 设计文档 - docker-compose.yml Docker编排 - nginx/scripts/systemd 运维配置 已完成功能: - 用户认证 (JWT) - 智能翻译 + 回复建议 - 营销素材生成 - 客户管理 + 沉默检测 - 报价单管理 - 产品库管理 - 汇率换算 - 推送通知 (uni-push) - WhatsApp Webhook框架 - Celery定时任务
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user