feat: 重构订单和物流信息展示格式
主要改动: - 订单列表:使用 order_list 格式,展示 5 个订单(全部状态) - 订单详情:使用 order_detail 格式,优化价格和时间显示 - 物流信息:使用 logistics 格式,根据 track id 动态生成步骤 - 商品图片:从 orderProduct.imageUrl 字段获取 - 时间格式:统一为 YYYY-MM-DD HH:MM:SS - 多语言支持:amountLabel、orderTime 支持中英文 - 配置管理:新增 FRONTEND_URL 环境变量 - API 集成:改进 Mall API tracks 数据解析 - 认证优化:account_id 从 webhook 动态获取 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,146 @@ from utils.logger import get_logger
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
# 订单字段多语言映射
|
||||
ORDER_FIELD_LABELS = {
|
||||
"zh": { # 中文
|
||||
"title": "订单详情",
|
||||
"order_id": "订单号",
|
||||
"status": "订单状态",
|
||||
"total_amount": "订单金额",
|
||||
"payment_method": "支付方式",
|
||||
"items_count": "商品总数量",
|
||||
"shipping_method": "运输方式",
|
||||
"tracking_number": "运单号",
|
||||
"shipping_address": "收货地址",
|
||||
"billing_address": "发票地址",
|
||||
"channel": "渠道",
|
||||
"created_at": "下单时间",
|
||||
"action": "操作",
|
||||
},
|
||||
"en": { # English
|
||||
"title": "Order Details",
|
||||
"order_id": "Order ID",
|
||||
"status": "Order Status",
|
||||
"total_amount": "Total Amount",
|
||||
"payment_method": "Payment Method",
|
||||
"items_count": "Total Items",
|
||||
"shipping_method": "Shipping Method",
|
||||
"tracking_number": "Tracking Number",
|
||||
"shipping_address": "Shipping Address",
|
||||
"billing_address": "Billing Address",
|
||||
"channel": "Channel",
|
||||
"created_at": "Order Date",
|
||||
"action": "Action",
|
||||
},
|
||||
"nl": { # Dutch (荷兰语)
|
||||
"title": "Orderdetails",
|
||||
"order_id": "Ordernummer",
|
||||
"status": "Orderstatus",
|
||||
"total_amount": "Totaalbedrag",
|
||||
"payment_method": "Betaalmethode",
|
||||
"items_count": "Totaal aantal artikelen",
|
||||
"shipping_method": "Verzendmethode",
|
||||
"tracking_number": "Trackingsnummer",
|
||||
"shipping_address": "Verzendadres",
|
||||
"billing_address": "Factuuradres",
|
||||
"channel": "Kanaal",
|
||||
"created_at": "Besteldatum",
|
||||
"action": "Actie",
|
||||
},
|
||||
"de": { # German (德语)
|
||||
"title": "Bestelldetails",
|
||||
"order_id": "Bestellnummer",
|
||||
"status": "Bestellstatus",
|
||||
"total_amount": "Gesamtbetrag",
|
||||
"payment_method": "Zahlungsmethode",
|
||||
"items_count": "Gesamtanzahl Artikel",
|
||||
"shipping_method": "Versandart",
|
||||
"tracking_number": "Sendungsnummer",
|
||||
"shipping_address": "Lieferadresse",
|
||||
"billing_address": "Rechnungsadresse",
|
||||
"channel": "Kanal",
|
||||
"created_at": "Bestelldatum",
|
||||
"action": "Aktion",
|
||||
},
|
||||
"es": { # Spanish (西班牙语)
|
||||
"title": "Detalles del pedido",
|
||||
"order_id": "Número de pedido",
|
||||
"status": "Estado del pedido",
|
||||
"total_amount": "Importe total",
|
||||
"payment_method": "Método de pago",
|
||||
"items_count": "Total de artículos",
|
||||
"shipping_method": "Método de envío",
|
||||
"tracking_number": "Número de seguimiento",
|
||||
"shipping_address": "Dirección de envío",
|
||||
"billing_address": "Dirección de facturación",
|
||||
"channel": "Canal",
|
||||
"created_at": "Fecha del pedido",
|
||||
"action": "Acción",
|
||||
},
|
||||
"fr": { # French (法语)
|
||||
"title": "Détails de la commande",
|
||||
"order_id": "Numéro de commande",
|
||||
"status": "Statut de la commande",
|
||||
"total_amount": "Montant total",
|
||||
"payment_method": "Méthode de paiement",
|
||||
"items_count": "Nombre total d'articles",
|
||||
"shipping_method": "Méthode d'expédition",
|
||||
"tracking_number": "Numéro de suivi",
|
||||
"shipping_address": "Adresse de livraison",
|
||||
"billing_address": "Adresse de facturation",
|
||||
"channel": "Canal",
|
||||
"created_at": "Date de commande",
|
||||
"action": "Action",
|
||||
},
|
||||
"it": { # Italian (意大利语)
|
||||
"title": "Detagli dell'ordine",
|
||||
"order_id": "Numero ordine",
|
||||
"status": "Stato ordine",
|
||||
"total_amount": "Importo totale",
|
||||
"payment_method": "Metodo di pagamento",
|
||||
"items_count": "Totale articoli",
|
||||
"shipping_method": "Metodo di spedizione",
|
||||
"tracking_number": "Numero di tracciamento",
|
||||
"shipping_address": "Indirizzo di spedizione",
|
||||
"billing_address": "Indirizzo di fatturazione",
|
||||
"channel": "Canale",
|
||||
"created_at": "Data ordine",
|
||||
"action": "Azione",
|
||||
},
|
||||
"tr": { # Turkish (土耳其语)
|
||||
"title": "Sipariş Detayları",
|
||||
"order_id": "Sipariş Numarası",
|
||||
"status": "Sipariş Durumu",
|
||||
"total_amount": "Toplam Tutar",
|
||||
"payment_method": "Ödeme Yöntemi",
|
||||
"items_count": "Toplam Ürün",
|
||||
"shipping_method": "Kargo Yöntemi",
|
||||
"tracking_number": "Takip Numarası",
|
||||
"shipping_address": "Teslimat Adresi",
|
||||
"billing_address": "Fatura Adresi",
|
||||
"channel": "Kanal",
|
||||
"created_at": "Sipariş Tarihi",
|
||||
"action": "İşlem",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_field_label(field_key: str, language: str = "en") -> str:
|
||||
"""获取指定语言的字段标签
|
||||
|
||||
Args:
|
||||
field_key: 字段键名(如 "order_id", "status" 等)
|
||||
language: 语言代码(默认 "en")
|
||||
|
||||
Returns:
|
||||
对应语言的字段标签
|
||||
"""
|
||||
if language not in ORDER_FIELD_LABELS:
|
||||
language = "en" # 默认使用英文
|
||||
return ORDER_FIELD_LABELS[language].get(field_key, ORDER_FIELD_LABELS["en"].get(field_key, field_key))
|
||||
|
||||
|
||||
class MessageType(str, Enum):
|
||||
"""Chatwoot message types"""
|
||||
INCOMING = "incoming"
|
||||
@@ -91,7 +231,49 @@ class ChatwootClient:
|
||||
if self._client:
|
||||
await self._client.aclose()
|
||||
self._client = None
|
||||
|
||||
|
||||
def _format_datetime(self, datetime_str: str) -> str:
|
||||
"""格式化日期时间为 YYYY-MM-DD HH:MM:SS 格式
|
||||
|
||||
Args:
|
||||
datetime_str: 输入的日期时间字符串
|
||||
|
||||
Returns:
|
||||
格式化后的日期时间字符串 (YYYY-MM-DD HH:MM:SS)
|
||||
"""
|
||||
if not datetime_str:
|
||||
return ""
|
||||
|
||||
# 如果已经是正确的格式(YYYY-MM-DD HH:MM:SS),直接返回
|
||||
if len(datetime_str) == 19 and datetime_str[10] == ' ' and datetime_str[13] == ':' and datetime_str[16] == ':':
|
||||
return datetime_str
|
||||
|
||||
# 尝试解析常见的时间格式
|
||||
from datetime import datetime
|
||||
|
||||
# 常见格式列表
|
||||
formats = [
|
||||
"%Y-%m-%d %H:%M:%S", # 2025-01-23 14:30:00
|
||||
"%Y-%m-%dT%H:%M:%S", # 2025-01-23T14:30:00
|
||||
"%Y-%m-%d %H:%M:%S.%f", # 2025-01-23 14:30:00.123456
|
||||
"%Y-%m-%dT%H:%M:%S.%f", # 2025-01-23T14:30:00.123456
|
||||
"%Y-%m-%dT%H:%M:%SZ", # 2025-01-23T14:30:00Z
|
||||
"%Y-%m-%dT%H:%M:%S.%fZ", # 2025-01-23T14:30:00.123456Z
|
||||
"%Y/%m/%d %H:%M:%S", # 2025/01/23 14:30:00
|
||||
"%d-%m-%Y %H:%M:%S", # 23-01-2025 14:30:00
|
||||
"%m/%d/%Y %H:%M:%S", # 01/23/2025 14:30:00
|
||||
]
|
||||
|
||||
for fmt in formats:
|
||||
try:
|
||||
dt = datetime.strptime(datetime_str, fmt)
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# 如果无法解析,返回原始字符串
|
||||
return datetime_str
|
||||
|
||||
# ============ Messages ============
|
||||
|
||||
async def send_message(
|
||||
@@ -283,28 +465,22 @@ class ChatwootClient:
|
||||
self,
|
||||
conversation_id: int,
|
||||
order_data: dict[str, Any],
|
||||
language: str = "en",
|
||||
actions: Optional[list[dict[str, Any]]] = None
|
||||
) -> dict[str, Any]:
|
||||
"""发送订单详情表单消息(使用 content_type=form)
|
||||
|
||||
根据 Chatwoot API 文档实现的 form 格式订单详情展示。
|
||||
form 类型支持的字段类型:text, text_area, email, select
|
||||
"""发送订单详情(使用 content_type=order_detail)
|
||||
|
||||
Args:
|
||||
conversation_id: 会话 ID
|
||||
order_data: 订单数据,包含:
|
||||
- order_id: 订单号
|
||||
- status: 订单状态
|
||||
- status: 订单状态码
|
||||
- status_text: 状态文本
|
||||
- created_at: 下单时间(可选)
|
||||
- items: 商品列表(可选)
|
||||
- created_at: 下单时间
|
||||
- total_amount: 总金额
|
||||
- shipping_fee: 运费(可选)
|
||||
- logistics: 物流信息(可选)
|
||||
- remark: 备注(可选)
|
||||
actions: 操作按钮配置列表(可选),每个按钮包含:
|
||||
- label: 按钮文字(用于 select 选项的显示)
|
||||
- value: 按钮值(用于 select 选项的值)
|
||||
- items: 商品列表
|
||||
language: 语言代码(en, nl, de, es, fr, it, tr, zh),默认 en
|
||||
actions: 操作按钮配置列表(可选,暂未使用)
|
||||
|
||||
Returns:
|
||||
发送结果
|
||||
@@ -312,131 +488,461 @@ class ChatwootClient:
|
||||
Example:
|
||||
>>> order_data = {
|
||||
... "order_id": "123456789",
|
||||
... "status": "shipped",
|
||||
... "status_text": "已发货",
|
||||
... "created_at": "2023-10-27 14:30",
|
||||
... "total_amount": "1058.00",
|
||||
... "items": [{"name": "商品A", "quantity": 2, "price": "100.00"}]
|
||||
... "status": "completed",
|
||||
... "status_text": "已完成",
|
||||
... "created_at": "2025-01-23 14:30:00",
|
||||
... "total_amount": "179.97",
|
||||
... "items": [{"name": "商品A", "image_url": "...", "quantity": 2, "price": "49.99"}]
|
||||
... }
|
||||
>>> actions = [
|
||||
... {"label": "查看详情", "value": "VIEW_DETAILS"},
|
||||
... {"label": "联系客服", "value": "CONTACT_SUPPORT"}
|
||||
... ]
|
||||
>>> await chatwoot.send_order_form(123, order_data, actions)
|
||||
>>> await chatwoot.send_order_form(123, order_data, language="zh")
|
||||
"""
|
||||
# 构建表单字段
|
||||
form_items = []
|
||||
order_id = order_data.get("order_id", "")
|
||||
status = order_data.get("status", "")
|
||||
status_text = order_data.get("status_text", "")
|
||||
|
||||
# 订单号(只读文本)
|
||||
form_items.append({
|
||||
"name": "order_id",
|
||||
"label": "订单号",
|
||||
"type": "text",
|
||||
"placeholder": "订单号",
|
||||
"default": order_data.get("order_id", "")
|
||||
})
|
||||
# 获取原始时间并格式化为 YYYY-MM-DD HH:MM:SS
|
||||
raw_created_at = order_data.get("created_at", order_data.get("date_added", ""))
|
||||
created_at = self._format_datetime(raw_created_at)
|
||||
|
||||
# 订单状态(只读文本)
|
||||
status_text = order_data.get("status_text", order_data.get("status", "unknown"))
|
||||
form_items.append({
|
||||
"name": "status",
|
||||
"label": "订单状态",
|
||||
"type": "text",
|
||||
"placeholder": "订单状态",
|
||||
"default": status_text
|
||||
})
|
||||
total_amount = order_data.get("total_amount", "0")
|
||||
|
||||
# 下单时间(只读文本)
|
||||
if order_data.get("created_at"):
|
||||
form_items.append({
|
||||
"name": "created_at",
|
||||
"label": "下单时间",
|
||||
"type": "text",
|
||||
"placeholder": "下单时间",
|
||||
"default": order_data["created_at"]
|
||||
})
|
||||
|
||||
# 商品列表(多行文本)
|
||||
items = order_data.get("items", [])
|
||||
if items:
|
||||
items_text = "\n".join([
|
||||
f"▫️ {item.get('name', '未知商品')} × {item.get('quantity', 1)} - ¥{item.get('price', '0.00')}"
|
||||
for item in items
|
||||
])
|
||||
form_items.append({
|
||||
"name": "items",
|
||||
"label": "商品详情",
|
||||
"type": "text_area",
|
||||
"placeholder": "商品列表",
|
||||
"default": items_text
|
||||
})
|
||||
|
||||
# 总金额(只读文本)
|
||||
form_items.append({
|
||||
"name": "total_amount",
|
||||
"label": "总金额",
|
||||
"type": "text",
|
||||
"placeholder": "总金额",
|
||||
"default": f"¥{order_data.get('total_amount', '0.00')}"
|
||||
})
|
||||
|
||||
# 运费(只读文本)
|
||||
if order_data.get("shipping_fee") is not None:
|
||||
form_items.append({
|
||||
"name": "shipping_fee",
|
||||
"label": "运费",
|
||||
"type": "text",
|
||||
"placeholder": "运费",
|
||||
"default": f"¥{order_data['shipping_fee']}"
|
||||
})
|
||||
|
||||
# 物流信息(多行文本)
|
||||
logistics = order_data.get("logistics")
|
||||
if logistics:
|
||||
logistics_text = (
|
||||
f"承运商: {logistics.get('carrier', '未知')}\n"
|
||||
f"单号: {logistics.get('tracking_number', '未知')}"
|
||||
)
|
||||
form_items.append({
|
||||
"name": "logistics",
|
||||
"label": "物流信息",
|
||||
"type": "text_area",
|
||||
"placeholder": "物流信息",
|
||||
"default": logistics_text
|
||||
})
|
||||
|
||||
# 备注(多行文本)
|
||||
if order_data.get("remark"):
|
||||
form_items.append({
|
||||
"name": "remark",
|
||||
"label": "备注",
|
||||
"type": "text_area",
|
||||
"placeholder": "备注",
|
||||
"default": order_data["remark"]
|
||||
})
|
||||
|
||||
# 操作选项(下拉选择,如果提供了 actions)
|
||||
if actions:
|
||||
form_items.append({
|
||||
"name": "action_select",
|
||||
"label": "操作",
|
||||
"type": "select",
|
||||
"options": actions
|
||||
})
|
||||
|
||||
# 构建 content_attributes
|
||||
content_attributes = {
|
||||
"items": form_items
|
||||
# 根据状态码映射状态和颜色
|
||||
status_mapping = {
|
||||
"0": {"status": "cancelled", "text": "已取消", "color": "text-red-600"},
|
||||
"1": {"status": "pending", "text": "待支付", "color": "text-yellow-600"},
|
||||
"2": {"status": "paid", "text": "已支付", "color": "text-blue-600"},
|
||||
"3": {"status": "shipped", "text": "已发货", "color": "text-purple-600"},
|
||||
"4": {"status": "signed", "text": "已签收", "color": "text-green-600"},
|
||||
"15": {"status": "completed", "text": "已完成", "color": "text-green-600"},
|
||||
"100": {"status": "cancelled", "text": "超时取消", "color": "text-red-600"},
|
||||
}
|
||||
|
||||
# 发送 form 类型消息
|
||||
return await self.send_rich_message(
|
||||
status_info = status_mapping.get(str(status), {"status": "unknown", "text": status_text or "未知", "color": "text-gray-600"})
|
||||
|
||||
# 构建商品列表
|
||||
items = order_data.get("items", [])
|
||||
formatted_items = []
|
||||
for item in items:
|
||||
# 获取价格并格式化为字符串(包含货币符号)
|
||||
price_value = item.get("price", "0")
|
||||
try:
|
||||
price_num = float(price_value)
|
||||
price_text = f"€{price_num:.2f}"
|
||||
except (ValueError, TypeError):
|
||||
price_text = str(price_value) if price_value else "€0.00"
|
||||
|
||||
formatted_items.append({
|
||||
"name": item.get("name", "未知商品"),
|
||||
"quantity": int(item.get("quantity", 1)),
|
||||
"price": price_text,
|
||||
"image": item.get("image_url", "")
|
||||
})
|
||||
|
||||
# 计算商品总数量
|
||||
total_quantity = sum(item.get("quantity", 1) for item in items)
|
||||
|
||||
# 计算总金额并格式化(多语言)
|
||||
try:
|
||||
total_amount_value = float(total_amount)
|
||||
except (ValueError, TypeError):
|
||||
total_amount_value = 0.0
|
||||
|
||||
# 构建总计标签
|
||||
if language == "zh":
|
||||
total_label = f"共 {total_quantity} 件商品"
|
||||
amount_text = f"总计: €{total_amount_value:.2f}"
|
||||
order_time_text = f"下单时间: {created_at}"
|
||||
else:
|
||||
total_label = f"Total: {total_quantity} items"
|
||||
amount_text = f"Total: €{total_amount_value:.2f}"
|
||||
order_time_text = f"Order time: {created_at}"
|
||||
|
||||
# 构建操作按钮
|
||||
actions_list = []
|
||||
# 获取前端 URL
|
||||
frontend_url = settings.frontend_url.rstrip('/')
|
||||
|
||||
if language == "zh":
|
||||
# 中文按钮
|
||||
actions_list.append({
|
||||
"text": "查看物流",
|
||||
"style": "default",
|
||||
"reply": f"查看订单物流信息:{order_id}"
|
||||
})
|
||||
actions_list.append({
|
||||
"text": "订单详情",
|
||||
"style": "primary",
|
||||
"url": f"{frontend_url}/customer/order/detail?orderId={order_id}",
|
||||
"target": "_self"
|
||||
})
|
||||
else:
|
||||
# 英文按钮
|
||||
actions_list.append({
|
||||
"text": "Logistics info",
|
||||
"style": "default",
|
||||
"reply": f"查看订单物流信息:{order_id}"
|
||||
})
|
||||
actions_list.append({
|
||||
"text": "Order details",
|
||||
"style": "primary",
|
||||
"url": f"{frontend_url}/customer/order/detail?orderId={order_id}",
|
||||
"target": "_self"
|
||||
})
|
||||
|
||||
# 构建 content_attributes(order_detail 格式)
|
||||
content_attributes = {
|
||||
"status": status_info["status"],
|
||||
"statusText": status_info["text"],
|
||||
"statusColor": status_info["color"],
|
||||
"orderId": str(order_id),
|
||||
"orderTime": order_time_text,
|
||||
"items": formatted_items,
|
||||
"showTotal": True,
|
||||
"totalLabel": total_label,
|
||||
"amountLabel": amount_text,
|
||||
"actions": actions_list
|
||||
}
|
||||
|
||||
# 记录发送的数据(用于调试)
|
||||
logger.info(
|
||||
"Sending order detail",
|
||||
conversation_id=conversation_id,
|
||||
content="订单详情",
|
||||
content_type="form",
|
||||
content_attributes=content_attributes
|
||||
order_id=order_id,
|
||||
items_count=len(formatted_items),
|
||||
total_quantity=total_quantity,
|
||||
language=language
|
||||
)
|
||||
|
||||
|
||||
# 发送 order_detail 类型消息
|
||||
client = await self._get_client()
|
||||
|
||||
payload = {
|
||||
"content_type": "order_detail",
|
||||
"content": "",
|
||||
"content_attributes": content_attributes
|
||||
}
|
||||
|
||||
response = await client.post(
|
||||
f"/conversations/{conversation_id}/messages",
|
||||
json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
|
||||
async def send_logistics_info(
|
||||
self,
|
||||
conversation_id: int,
|
||||
logistics_data: dict[str, Any],
|
||||
language: str = "en"
|
||||
) -> dict[str, Any]:
|
||||
"""发送物流信息(使用 content_type=logistics)
|
||||
|
||||
Args:
|
||||
conversation_id: 会话 ID
|
||||
logistics_data: 物流数据,包含:
|
||||
- carrier: 物流公司名称
|
||||
- tracking_number: 运单号
|
||||
- status: 当前状态
|
||||
- timeline: 物流轨迹列表
|
||||
- order_id: 订单号(可选,用于生成链接)
|
||||
language: 语言代码(en, nl, de, es, fr, it, tr, zh),默认 en
|
||||
|
||||
Returns:
|
||||
发送结果
|
||||
|
||||
Example:
|
||||
>>> logistics_data = {
|
||||
... "carrier": "顺丰速运",
|
||||
... "tracking_number": "SF154228901",
|
||||
... "status": "派送中",
|
||||
... "timeline": [...]
|
||||
... }
|
||||
>>> await chatwoot.send_logistics_info(123, logistics_data, language="zh")
|
||||
"""
|
||||
carrier = logistics_data.get("carrier", "")
|
||||
tracking_number = logistics_data.get("tracking_number", "")
|
||||
status = logistics_data.get("status", "")
|
||||
timeline = logistics_data.get("timeline", [])
|
||||
order_id = logistics_data.get("order_id", "")
|
||||
|
||||
# 获取最新物流信息(从 timeline 中提取 remark)
|
||||
latest_log = ""
|
||||
latest_time = ""
|
||||
current_step = 0
|
||||
|
||||
# Track ID 到步骤的映射
|
||||
# id = 1 -> Order Received(已接单)
|
||||
# id = 10 -> Picked Up(已揽收)
|
||||
# id = 20 -> In Transit(运输中)
|
||||
# id = 30 -> Delivered(已送达)
|
||||
track_id_to_step = {
|
||||
"1": 0, # Order Received
|
||||
"10": 1, # Picked Up
|
||||
"20": 2, # In Transit
|
||||
"30": 3 # Delivered
|
||||
}
|
||||
|
||||
if timeline and len(timeline) > 0:
|
||||
# 获取第一条(最新)物流信息
|
||||
first_event = timeline[0]
|
||||
if isinstance(first_event, dict):
|
||||
# 提取 remark 字段
|
||||
remark = first_event.get("remark", "")
|
||||
|
||||
# 提取时间
|
||||
time_str = (
|
||||
first_event.get("time") or
|
||||
first_event.get("date") or
|
||||
first_event.get("timestamp") or
|
||||
""
|
||||
)
|
||||
|
||||
# 提取位置
|
||||
location = first_event.get("location", "")
|
||||
|
||||
# 提取 track id
|
||||
track_id = str(first_event.get("id", ""))
|
||||
|
||||
# 构建最新物流描述:如果 remark 为空,则只显示 location;否则显示 location | remark
|
||||
if location and remark:
|
||||
latest_log = f"{location} | {remark}"
|
||||
elif remark:
|
||||
latest_log = remark
|
||||
elif location:
|
||||
latest_log = location
|
||||
else:
|
||||
latest_log = ""
|
||||
|
||||
# 格式化时间
|
||||
if time_str:
|
||||
latest_time = self._format_datetime(str(time_str))
|
||||
|
||||
# 根据 track id 判断当前步骤
|
||||
if track_id in track_id_to_step:
|
||||
current_step = track_id_to_step[track_id]
|
||||
else:
|
||||
# 如果无法识别 id,根据时间线长度判断
|
||||
current_step = len(timeline) if len(timeline) <= 4 else 2
|
||||
|
||||
# 构建步骤列表(固定4个步骤)
|
||||
if language == "zh":
|
||||
steps = [
|
||||
{"label": "已接单"},
|
||||
{"label": "已揽收"},
|
||||
{"label": "运输中"},
|
||||
{"label": "已送达"}
|
||||
]
|
||||
else:
|
||||
steps = [
|
||||
{"label": "Order Received"},
|
||||
{"label": "Picked Up"},
|
||||
{"label": "In Transit"},
|
||||
{"label": "Delivered"}
|
||||
]
|
||||
|
||||
# 构建操作按钮
|
||||
actions = []
|
||||
frontend_url = settings.frontend_url.rstrip('/')
|
||||
|
||||
if order_id:
|
||||
# 如果有订单号,生成物流追踪链接
|
||||
if language == "zh":
|
||||
actions.append({
|
||||
"text": "物流详情",
|
||||
"style": "primary",
|
||||
"url": f"{frontend_url}/logistic-tracking/{order_id}"
|
||||
})
|
||||
else:
|
||||
actions.append({
|
||||
"text": "Tracking Details",
|
||||
"style": "primary",
|
||||
"url": f"{frontend_url}/logistic-tracking/{order_id}"
|
||||
})
|
||||
|
||||
# 构建 content_attributes(logistics 格式)
|
||||
content_attributes = {
|
||||
"logisticsName": carrier,
|
||||
"trackingNumber": tracking_number,
|
||||
"currentStep": current_step,
|
||||
"isUrgent": False,
|
||||
"latestLog": latest_log,
|
||||
"latestTime": latest_time,
|
||||
"steps": steps,
|
||||
"actions": actions
|
||||
}
|
||||
|
||||
# 记录发送的数据(用于调试)
|
||||
logger.info(
|
||||
"Sending logistics info",
|
||||
conversation_id=conversation_id,
|
||||
carrier=carrier,
|
||||
tracking_number=tracking_number,
|
||||
current_step=current_step,
|
||||
timeline_count=len(timeline) if timeline else 0,
|
||||
language=language
|
||||
)
|
||||
|
||||
# 发送 logistics 类型消息
|
||||
client = await self._get_client()
|
||||
|
||||
payload = {
|
||||
"content_type": "logistics",
|
||||
"content": "",
|
||||
"content_attributes": content_attributes
|
||||
}
|
||||
|
||||
response = await client.post(
|
||||
f"/conversations/{conversation_id}/messages",
|
||||
json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
|
||||
async def send_order_list(
|
||||
self,
|
||||
conversation_id: int,
|
||||
orders: list[dict[str, Any]],
|
||||
language: str = "en"
|
||||
) -> dict[str, Any]:
|
||||
"""发送订单列表(使用自定义 content_type=order_list)
|
||||
|
||||
每个订单包含:
|
||||
- orderNumber: 订单号
|
||||
- date: 订单日期
|
||||
- status: 订单状态
|
||||
- items: 商品列表(图片和名称)
|
||||
- actions: 操作按钮
|
||||
|
||||
Args:
|
||||
conversation_id: 会话 ID
|
||||
orders: 订单列表,每个订单包含:
|
||||
- order_id: 订单号
|
||||
- status_text: 订单状态
|
||||
- created_at: 下单时间
|
||||
- items: 商品列表(可选,包含 name 和 image_url)
|
||||
- order_type: 订单类型(可选,用于判断渠道)
|
||||
language: 语言代码(en, nl, de, es, fr, it, tr, zh),默认 en
|
||||
|
||||
Returns:
|
||||
发送结果
|
||||
|
||||
Example:
|
||||
>>> orders = [
|
||||
... {
|
||||
... "order_id": "20250122001",
|
||||
... "created_at": "Jan 22, 2025",
|
||||
... "status_text": "Shipped",
|
||||
... "items": [
|
||||
... {"image_url": "url1", "name": "Product 1"},
|
||||
... {"image_url": "url2", "name": "Product 2"}
|
||||
... ]
|
||||
... }
|
||||
... ]
|
||||
>>> await chatwoot.send_order_list(123, orders, language="en")
|
||||
"""
|
||||
# 获取多语言按钮文本
|
||||
if language == "zh":
|
||||
details_text = "订单详情"
|
||||
logistics_text = "物流信息"
|
||||
details_reply_prefix = "查看订单详情:"
|
||||
logistics_reply_prefix = "查看订单物流信息:"
|
||||
else:
|
||||
details_text = "Order details"
|
||||
logistics_text = "Logistics info"
|
||||
details_reply_prefix = "查看订单详情:"
|
||||
logistics_reply_prefix = "查看订单物流信息:"
|
||||
|
||||
# 构建订单列表
|
||||
order_list_data = []
|
||||
|
||||
for order in orders:
|
||||
order_id = order.get("order_id", "")
|
||||
status_text = order.get("status_text", "")
|
||||
|
||||
# 获取原始时间并格式化为 YYYY-MM-DD HH:MM:SS
|
||||
raw_created_at = order.get("created_at", order.get("date_added", ""))
|
||||
created_at = self._format_datetime(raw_created_at)
|
||||
|
||||
# 构建商品列表
|
||||
items = order.get("items", [])
|
||||
formatted_items = []
|
||||
|
||||
logger.info(
|
||||
f"Processing order {order_id} for items",
|
||||
items_count=len(items) if isinstance(items, list) else 0,
|
||||
items_type=type(items).__name__,
|
||||
first_item_keys=list(items[0].keys()) if items and len(items) > 0 else None
|
||||
)
|
||||
|
||||
if items and isinstance(items, list):
|
||||
for item in items:
|
||||
formatted_items.append({
|
||||
"image": item.get("image_url", ""),
|
||||
"name": item.get("name", "Product")
|
||||
})
|
||||
|
||||
logger.info(
|
||||
f"Formatted items for order {order_id}",
|
||||
formatted_items_count=len(formatted_items),
|
||||
sample_items=str(formatted_items[:2]) if formatted_items else "[]"
|
||||
)
|
||||
|
||||
# 构建操作按钮
|
||||
actions = [
|
||||
{
|
||||
"text": details_text,
|
||||
"reply": f"{details_reply_prefix}{order_id}"
|
||||
},
|
||||
{
|
||||
"text": logistics_text,
|
||||
"reply": f"{logistics_reply_prefix}{order_id}"
|
||||
}
|
||||
]
|
||||
|
||||
# 构建单个订单
|
||||
order_data = {
|
||||
"orderNumber": order_id,
|
||||
"date": created_at,
|
||||
"status": status_text,
|
||||
"items": formatted_items,
|
||||
"actions": actions
|
||||
}
|
||||
|
||||
order_list_data.append(order_data)
|
||||
|
||||
# 构建 content_attributes(order_list 格式)
|
||||
content_attributes = {
|
||||
"orders": order_list_data
|
||||
}
|
||||
|
||||
# 记录发送的数据(用于调试)
|
||||
logger.info(
|
||||
"Sending order list",
|
||||
conversation_id=conversation_id,
|
||||
orders_count=len(orders),
|
||||
language=language,
|
||||
payload_preview=str(content_attributes)[:1000]
|
||||
)
|
||||
|
||||
# 发送 order_list 类型消息
|
||||
client = await self._get_client()
|
||||
|
||||
payload = {
|
||||
"content_type": "order_list",
|
||||
"content": "",
|
||||
"content_attributes": content_attributes
|
||||
}
|
||||
|
||||
response = await client.post(
|
||||
f"/conversations/{conversation_id}/messages",
|
||||
json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
|
||||
# ============ Conversations ============
|
||||
|
||||
async def get_conversation(self, conversation_id: int) -> dict[str, Any]:
|
||||
@@ -739,9 +1245,16 @@ def create_action_buttons(actions: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
chatwoot_client: Optional[ChatwootClient] = None
|
||||
|
||||
|
||||
def get_chatwoot_client() -> ChatwootClient:
|
||||
"""Get or create global Chatwoot client instance"""
|
||||
def get_chatwoot_client(account_id: Optional[int] = None) -> ChatwootClient:
|
||||
"""Get or create Chatwoot client instance
|
||||
|
||||
Args:
|
||||
account_id: Chatwoot account ID. If not provided, uses settings default.
|
||||
"""
|
||||
global chatwoot_client
|
||||
if account_id is not None:
|
||||
# 创建指定 account_id 的客户端实例
|
||||
return ChatwootClient(account_id=account_id)
|
||||
if chatwoot_client is None:
|
||||
chatwoot_client = ChatwootClient()
|
||||
return chatwoot_client
|
||||
|
||||
Reference in New Issue
Block a user