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,109 @@
|
||||
from typing import Dict, Any, Optional
|
||||
import httpx
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WhatsAppService:
|
||||
def __init__(self):
|
||||
self.api_token = settings.WHATSAPP_API_TOKEN
|
||||
self.phone_number_id = settings.WHATSAPP_PHONE_NUMBER_ID
|
||||
self.api_base = f"https://graph.facebook.com/v18.0/{self.phone_number_id}"
|
||||
|
||||
def verify_webhook(self, mode: str, token: str, challenge: str) -> Optional[str]:
|
||||
if mode == "subscribe" and token == settings.WHATSAPP_WEBHOOK_VERIFY_TOKEN:
|
||||
return challenge
|
||||
return None
|
||||
|
||||
def verify_signature(self, body: bytes, signature: str) -> bool:
|
||||
if not signature:
|
||||
return False
|
||||
expected = hmac.new(
|
||||
settings.WHATSAPP_API_TOKEN.encode(),
|
||||
body,
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
return hmac.compare_digest(f"sha256={expected}", signature)
|
||||
|
||||
async def send_text(self, to: str, text: str) -> bool:
|
||||
if not self.api_token or not self.phone_number_id:
|
||||
logger.warning("WhatsApp not configured")
|
||||
return False
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
f"{self.api_base}/messages",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"messaging_product": "whatsapp",
|
||||
"to": to,
|
||||
"type": "text",
|
||||
"text": {"body": text},
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
logger.error(f"WhatsApp send failed: {resp.text}")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def send_template(self, to: str, template_name: str, params: Dict[str, str]) -> bool:
|
||||
if not self.api_token or not self.phone_number_id:
|
||||
return False
|
||||
|
||||
components = [
|
||||
{
|
||||
"type": "body",
|
||||
"parameters": [
|
||||
{"type": "text", "text": v} for v in params.values()
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
f"{self.api_base}/messages",
|
||||
headers={"Authorization": f"Bearer {self.api_token}", "Content-Type": "application/json"},
|
||||
json={
|
||||
"messaging_product": "whatsapp",
|
||||
"to": to,
|
||||
"type": "template",
|
||||
"template": {
|
||||
"name": template_name,
|
||||
"language": {"code": "en"},
|
||||
"components": components,
|
||||
},
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
return resp.status_code == 200
|
||||
|
||||
def parse_webhook(self, body: Dict) -> Optional[Dict]:
|
||||
try:
|
||||
entry = body.get("entry", [{}])[0]
|
||||
change = entry.get("changes", [{}])[0]
|
||||
value = change.get("value", {})
|
||||
messages = value.get("messages", [])
|
||||
|
||||
if not messages:
|
||||
return None
|
||||
|
||||
msg = messages[0]
|
||||
return {
|
||||
"from": msg.get("from"),
|
||||
"text": msg.get("text", {}).get("body", ""),
|
||||
"msg_id": msg.get("id"),
|
||||
"timestamp": msg.get("timestamp"),
|
||||
"type": msg.get("type", "text"),
|
||||
"profile_name": value.get("contacts", [{}])[0].get("profile", {}).get("name"),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to parse WhatsApp webhook: {e}")
|
||||
return None
|
||||
Reference in New Issue
Block a user