Files
trade-assistant/backend/app/services/silent_pattern.py
T
TradeMate Dev 7b62c2f8b4 feat: 修复 H5 底部导航覆盖 + 更新项目进度文档
## H5 底部导航修复 (Bug #10)
- 精简 App.vue,移除重复 tabbar,仅保留全局样式
- uni-page 设置 height: calc(100% - 50px) + overflow-y: auto
- 内容区域精确停在底部导航上方,独立滚动不再叠加
- 恢复 custom-tab-bar 组件

## 项目进度文档
- PROGRESS.md 更新至 10 个 Bug 修复
- 新增 H5 底部导航修复记录
- 新增历史变更条目
2026-05-12 20:24:42 +08:00

169 lines
6.1 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 SilentPatternService:
def __init__(self, db: AsyncSession):
self.db = db
async def analyze_silent_risk(self, user_id: str) -> List[Dict[str, Any]]:
cutoff_3d = datetime.utcnow() - timedelta(days=3)
cutoff_7d = datetime.utcnow() - timedelta(days=7)
result = await self.db.execute(
select(Customer).where(
and_(
Customer.user_id == user_id,
Customer.status.in_(["lead", "negotiating"]),
)
)
)
customers = result.scalars().all()
risk_scores = []
for c in customers:
score, reasons = await self._calculate_risk_score(c, cutoff_3d, cutoff_7d)
if score > 0:
risk_scores.append({
"customer_id": str(c.id),
"name": c.name,
"company": c.company,
"country": c.country,
"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,
"risk_score": score,
"risk_level": self._risk_level(score),
"reasons": reasons,
})
risk_scores.sort(key=lambda x: x["risk_score"], reverse=True)
return risk_scores
async def _calculate_risk_score(
self, customer: Customer, cutoff_3d: datetime, cutoff_7d: datetime
) -> tuple:
score = 0
reasons = []
if not customer.last_contact_at:
return (0, [])
silence_days = (datetime.utcnow() - customer.last_contact_at).days
if silence_days >= 7:
score += 40
reasons.append(f"沉默超过7天")
elif silence_days >= 3:
score += 20
reasons.append(f"沉默超过3天")
conv_query = await self.db.execute(
select(Conversation).where(
and_(
Conversation.customer_id == customer.id,
Conversation.user_id == customer.user_id,
)
).order_by(Conversation.created_at.desc()).limit(1)
)
conv = conv_query.scalar_one_or_none()
if not conv:
return (score, reasons)
msg_count_query = await self.db.execute(
select(func.count(Message.id)).where(
and_(
Message.conversation_id == conv.id,
Message.direction == "inbound",
)
)
)
inbound_count = msg_count_query.scalar() or 0
if inbound_count >= 5 and silence_days >= 3:
score += 20
reasons.append(f"前期沟通频繁({inbound_count}条)后突然沉默")
if customer.status == "lead" and silence_days >= 3:
score += 15
reasons.append("潜在客户阶段需及时跟进")
if customer.status == "negotiating" and silence_days >= 2:
score += 25
reasons.append("谈判阶段客户需保持热度")
recent_query = await self.db.execute(
select(Message).where(
and_(
Message.conversation_id == conv.id,
Message.created_at >= cutoff_7d,
)
).order_by(Message.created_at.desc()).limit(3)
)
recent_msgs = recent_query.scalars().all()
if recent_msgs:
last_inbound = None
for m in recent_msgs:
if m.direction == "inbound":
last_inbound = m
break
if last_inbound and silence_days >= 1:
content_lower = last_inbound.content.lower()
closing_signals = ["i'll think", "let me check", "too expensive", "high price", "not now", "maybe later", "considering"]
for signal in closing_signals:
if signal in content_lower:
score += 15
reasons.append(f"客户回复含消极信号: \"{signal}\"")
break
return (min(score, 100), reasons)
def _risk_level(self, score: int) -> str:
if score >= 70:
return "high"
elif score >= 40:
return "medium"
elif score >= 20:
return "low"
return "minimal"
async def get_suggestions(self, user_id: str, customer_id: str) -> List[str]:
score_result = await self.analyze_silent_risk(user_id)
customer_scores = [s for s in score_result if s["customer_id"] == customer_id]
if not customer_scores:
return []
score = customer_scores[0]
suggestions = []
silence_days = score["silence_days"]
if silence_days >= 7:
suggestions.extend([
f"客户{score['name']}已沉默{silence_days}天,建议发送产品更新或行业资讯重新激活",
"考虑提供限时优惠或样品折扣打动客户",
])
elif silence_days >= 3:
suggestions.extend([
f"客户{score['name']}沉默{silence_days}天,建议发送跟进消息询问是否有进一步需求",
"可分享相关案例或成功故事保持客户兴趣",
])
if "negotiating" in score.get("status", ""):
suggestions.append("谈判阶段客户,建议主动提供更多产品细节或定制方案")
if "消极信号" in str(score.get("reasons", [])):
suggestions.append("客户曾表达价格顾虑,建议重新审视报价或提供增值服务")
if not suggestions:
suggestions.append("客户状态良好,建议保持定期跟进节奏")
return suggestions