Files
trade-assistant/backend/app/services/customer.py
T
TradeMate Dev c6206787da 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定时任务
2026-05-08 18:17:12 +08:00

205 lines
7.3 KiB
Python

from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_
from app.models.customer import Customer, Conversation, Message
import logging
logger = logging.getLogger(__name__)
class CustomerService:
def __init__(self, db: AsyncSession):
self.db = db
async def list_customers(self, user_id: str, status: Optional[str] = None, page: int = 1, size: int = 20) -> Dict[str, Any]:
query = select(Customer).where(Customer.user_id == user_id)
count_query = select(func.count()).select_from(Customer).where(Customer.user_id == user_id)
if status:
query = query.where(Customer.status == status)
count_query = count_query.where(Customer.status == status)
query = query.order_by(Customer.updated_at.desc()).offset((page - 1) * size).limit(size)
total = await self.db.execute(count_query)
result = await self.db.execute(query)
customers = result.scalars().all()
return {
"items": [self._to_dict(c) for c in customers],
"total": total.scalar(),
"page": page,
"size": size,
}
async def get_customer(self, user_id: str, customer_id: str) -> Optional[Dict]:
result = await self.db.execute(
select(Customer).where(
and_(Customer.id == customer_id, Customer.user_id == user_id)
)
)
c = result.scalar_one_or_none()
return self._to_dict(c) if c else None
async def create_customer(self, user_id: str, data: Dict[str, Any]) -> Dict:
c = Customer(user_id=user_id, **data)
self.db.add(c)
await self.db.flush()
return self._to_dict(c)
async def update_customer(self, user_id: str, customer_id: str, data: Dict[str, Any]) -> Optional[Dict]:
result = await self.db.execute(
select(Customer).where(
and_(Customer.id == customer_id, Customer.user_id == user_id)
)
)
c = result.scalar_one_or_none()
if not c:
return None
for k, v in data.items():
if hasattr(c, k):
setattr(c, k, v)
await self.db.flush()
return self._to_dict(c)
async def delete_customer(self, user_id: str, customer_id: str) -> bool:
result = await self.db.execute(
select(Customer).where(
and_(Customer.id == customer_id, Customer.user_id == user_id)
)
)
c = result.scalar_one_or_none()
if not c:
return False
await self.db.delete(c)
return True
async def get_silent_customers(self, user_id: str, days: int = 3) -> List[Dict]:
cutoff = datetime.utcnow() - timedelta(days=days)
result = await self.db.execute(
select(Customer)
.where(
and_(
Customer.user_id == user_id,
Customer.status.in_(["lead", "negotiating"]),
Customer.last_contact_at.isnot(None),
Customer.last_contact_at < cutoff,
)
)
.order_by(Customer.last_contact_at.asc())
)
return [self._to_dict(c) for c in result.scalars().all()]
async def record_contact(self, user_id: str, customer_id: str):
now = datetime.utcnow()
result = await self.db.execute(
select(Customer).where(
and_(Customer.id == customer_id, Customer.user_id == user_id)
)
)
c = result.scalar_one_or_none()
if c:
c.last_contact_at = now
c.silence_started_at = None
c.next_followup_at = now + timedelta(days=3)
await self.db.flush()
async def get_conversation(self, user_id: str, customer_id: str, page: int = 1, size: int = 50) -> Dict[str, Any]:
conv_query = select(Conversation).where(
and_(Conversation.user_id == user_id, Conversation.customer_id == customer_id)
).order_by(Conversation.created_at.desc()).limit(1)
conv_result = await self.db.execute(conv_query)
conv = conv_result.scalar_one_or_none()
if not conv:
return {"messages": [], "total": 0, "conversation_id": None}
msg_query = (
select(Message)
.where(Message.conversation_id == conv.id)
.order_by(Message.created_at.asc())
.offset((page - 1) * size)
.limit(size)
)
msg_result = await self.db.execute(msg_query)
messages = msg_result.scalars().all()
return {
"conversation_id": str(conv.id),
"messages": [
{
"id": str(m.id),
"direction": m.direction,
"content": m.content,
"content_translated": m.content_translated,
"ai_suggestions": m.ai_suggestions,
"selected_suggestion": m.selected_suggestion,
"created_at": m.created_at.isoformat() if m.created_at else None,
}
for m in messages
],
"total": conv.message_count,
}
async def save_message(
self, user_id: str, customer_id: str, direction: str, content: str,
translation: Optional[str] = None, suggestions: Optional[List] = None,
) -> Dict:
conv_result = await self.db.execute(
select(Conversation).where(
and_(Conversation.user_id == user_id, Conversation.customer_id == customer_id)
).order_by(Conversation.created_at.desc()).limit(1)
)
conv = conv_result.scalar_one_or_none()
if not conv:
conv = Conversation(
user_id=user_id,
customer_id=customer_id,
channel="whatsapp",
status="active",
)
self.db.add(conv)
await self.db.flush()
msg = Message(
conversation_id=conv.id,
direction=direction,
content=content,
content_translated=translation,
ai_suggestions=suggestions,
)
self.db.add(msg)
conv.message_count = (conv.message_count or 0) + 1
conv.last_message_at = datetime.utcnow()
await self.db.flush()
await self.record_contact(user_id, customer_id)
return {
"message_id": str(msg.id),
"conversation_id": str(conv.id),
"direction": direction,
"content": content,
}
def _to_dict(self, c: Customer) -> Dict:
if not c:
return {}
return {
"id": str(c.id),
"name": c.name,
"company": c.company,
"country": c.country,
"phone": c.phone,
"email": c.email,
"whatsapp_id": c.whatsapp_id,
"source": c.source,
"tags": c.tags,
"status": c.status,
"last_contact_at": c.last_contact_at.isoformat() if c.last_contact_at else None,
"silence_days": (datetime.utcnow() - c.last_contact_at).days if c.last_contact_at else 0,
"created_at": c.created_at.isoformat() if c.created_at else None,
}