7b62c2f8b4
## H5 底部导航修复 (Bug #10) - 精简 App.vue,移除重复 tabbar,仅保留全局样式 - uni-page 设置 height: calc(100% - 50px) + overflow-y: auto - 内容区域精确停在底部导航上方,独立滚动不再叠加 - 恢复 custom-tab-bar 组件 ## 项目进度文档 - PROGRESS.md 更新至 10 个 Bug 修复 - 新增 H5 底部导航修复记录 - 新增历史变更条目
291 lines
10 KiB
Python
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()) |