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:
TradeMate Dev
2026-05-08 18:17:12 +08:00
commit c6206787da
121 changed files with 11743 additions and 0 deletions
+193
View File
@@ -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())