Files
trade-assistant/backend/app/workers/tasks.py
T
TradeMate Dev 7b62c2f8b4 feat: 修复 H5 底部导航覆盖 + 更新项目进度文档
## H5 底部导航修复 (Bug #10)
- 精简 App.vue,移除重复 tabbar,仅保留全局样式
- uni-page 设置 height: calc(100% - 50px) + overflow-y: auto
- 内容区域精确停在底部导航上方,独立滚动不再叠加
- 恢复 custom-tab-bar 组件

## 项目进度文档
- PROGRESS.md 更新至 10 个 Bug 修复
- 新增 H5 底部导航修复记录
- 新增历史变更条目
2026-05-12 20:24:42 +08:00

291 lines
10 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
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())