a60aac4638
Centralizes all hardcoded page paths, storage keys, external URLs, and branding into a single uni-app/src/config.js. Fixes trackMarketingEffect sending wrong field names (action/content_preview -> event_type/content) that silently dropped tracking data. Adds notes, estimated_value, next_followup_at to Customer response. Removes '翻译' from bottom tab nav (5 tabs now), adds quick translate card on home page. Makes profile page header color consistent with app theme (#1890ff).
208 lines
7.4 KiB
Python
208 lines
7.4 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,
|
|
"notes": c.notes,
|
|
"status": c.status,
|
|
"estimated_value": c.estimated_value,
|
|
"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,
|
|
"next_followup_at": c.next_followup_at.isoformat() if c.next_followup_at else None,
|
|
"created_at": c.created_at.isoformat() if c.created_at else None,
|
|
}
|