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,204 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user