From 5b36693c2eaec4ccd018c7034fb9ebbf6b114b15 Mon Sep 17 00:00:00 2001 From: jimi <1847930177@qq.com> Date: Fri, 13 Mar 2026 10:42:14 +0800 Subject: [PATCH] feat: alert wecom when no designer is available --- db/chat_log_db.py | 45 +++++++++++++++++- db/pending_transfer_db.py | 12 +++++ services/dispatch_service.py | 5 ++ services/service_designer_alert.py | 76 ++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 services/service_designer_alert.py diff --git a/db/chat_log_db.py b/db/chat_log_db.py index a7049f3..c10214e 100755 --- a/db/chat_log_db.py +++ b/db/chat_log_db.py @@ -8,7 +8,7 @@ import sqlite3 import os import threading from queue import Queue, Empty -from datetime import datetime +from datetime import datetime, timedelta from typing import List, Dict, Optional _DB_PATH = os.path.join(os.path.dirname(__file__), "chat_log_db", "chats.db") @@ -527,6 +527,49 @@ def get_latest_messages(limit: int = 20) -> List[Dict]: return [dict(r) for r in rows] +def get_waiting_customer_pool(window_minutes: int = 30) -> Dict: + """统计最近窗口内、最后一条消息仍来自客户的待接待客户池。""" + cutoff = (datetime.now() - timedelta(minutes=max(window_minutes, 1))).strftime("%Y-%m-%d %H:%M:%S") + with _get_conn() as conn: + rows = conn.execute(_sql(""" + SELECT id, customer_id, acc_id, direction, timestamp + FROM chat_logs + WHERE timestamp >= ? + AND customer_id <> '' + AND customer_id <> 'unknown' + AND acc_id <> '' + ORDER BY id DESC + """), (cutoff,)).fetchall() + + latest_by_session = {} + for row in rows: + item = dict(row) + key = (str(item.get("customer_id") or ""), str(item.get("acc_id") or "")) + if key not in latest_by_session: + latest_by_session[key] = item + + per_shop: Dict[str, int] = {} + waiting_sessions = 0 + for item in latest_by_session.values(): + if str(item.get("direction") or "") != "in": + continue + acc_id = str(item.get("acc_id") or "") + if not acc_id: + continue + per_shop[acc_id] = per_shop.get(acc_id, 0) + 1 + waiting_sessions += 1 + + shops = [ + {"acc_id": acc_id, "waiting_customers": count} + for acc_id, count in sorted(per_shop.items(), key=lambda kv: (-kv[1], kv[0])) + ] + return { + "total_waiting_customers": waiting_sessions, + "shops": shops, + "window_minutes": window_minutes, + } + + # ========== 订单相关 ========== @_retry_db_operation diff --git a/db/pending_transfer_db.py b/db/pending_transfer_db.py index 417ef98..dc95005 100644 --- a/db/pending_transfer_db.py +++ b/db/pending_transfer_db.py @@ -160,3 +160,15 @@ def retry_pending_transfer(row_id: int, delay_seconds: int = 60, error: str = "" (next_s, now_s, error[:500], row_id), ) conn.commit() + + +def count_open_pending_transfers() -> int: + with _get_conn() as conn: + row = conn.execute( + """ + SELECT COUNT(*) AS cnt + FROM pending_transfers + WHERE status IN ('pending', 'processing') + """ + ).fetchone() + return int(row["cnt"] or 0) if row else 0 diff --git a/services/dispatch_service.py b/services/dispatch_service.py index 542e8f6..5c2455a 100644 --- a/services/dispatch_service.py +++ b/services/dispatch_service.py @@ -3,6 +3,7 @@ import logging import httpx import asyncio from typing import Optional +from services.service_designer_alert import designer_alert_service logger = logging.getLogger("cs_agent") @@ -53,6 +54,10 @@ class DispatchService: return designer logger.warning(f"[Dispatch]{u_tag} 派单被拒: {data.get('reason')} body={body}") + await designer_alert_service.notify_if_needed( + trigger=f"dispatch_rejected:{data.get('reason') or 'unknown'}", + customer_id=user_id, + ) return None if response.status_code == 401: diff --git a/services/service_designer_alert.py b/services/service_designer_alert.py new file mode 100644 index 0000000..afd599d --- /dev/null +++ b/services/service_designer_alert.py @@ -0,0 +1,76 @@ +import os +import time +import logging +from datetime import datetime +from typing import Dict + +from db.chat_log_db import get_waiting_customer_pool +from db.pending_transfer_db import count_open_pending_transfers +from services.service_wecom_bot import wecom_bot_service + +logger = logging.getLogger("cs_agent") + +DESIGNER_ALERT_START_HOUR = int(os.getenv("DESIGNER_ALERT_START_HOUR", "8")) +DESIGNER_ALERT_END_HOUR = int(os.getenv("DESIGNER_ALERT_END_HOUR", "24")) +DESIGNER_ALERT_COOLDOWN_SECONDS = int(os.getenv("DESIGNER_ALERT_COOLDOWN_SECONDS", "300")) +DESIGNER_ALERT_POOL_WINDOW_MINUTES = int(os.getenv("DESIGNER_ALERT_POOL_WINDOW_MINUTES", "30")) + + +class DesignerAlertService: + def __init__(self): + self._last_alert_at = 0.0 + + @staticmethod + def _in_active_window(now: datetime) -> bool: + hour = now.hour + return DESIGNER_ALERT_START_HOUR <= hour < DESIGNER_ALERT_END_HOUR + + @staticmethod + def _render_shop_lines(pool: Dict) -> str: + shops = pool.get("shops") or [] + if not shops: + return "- 暂无店铺明细" + lines = [] + for item in shops[:10]: + acc_id = str(item.get("acc_id") or "") + waiting = int(item.get("waiting_customers") or 0) + lines.append(f"- {acc_id}:{waiting}人") + return "\n".join(lines) + + async def notify_if_needed(self, *, trigger: str = "", customer_id: str = "", acc_id: str = "") -> bool: + now = datetime.now() + if not self._in_active_window(now): + return False + + now_ts = time.time() + if now_ts - self._last_alert_at < max(DESIGNER_ALERT_COOLDOWN_SECONDS, 30): + return False + + pending_count = count_open_pending_transfers() + pool = get_waiting_customer_pool(DESIGNER_ALERT_POOL_WINDOW_MINUTES) + waiting_total = int(pool.get("total_waiting_customers") or 0) + if pending_count <= 0 and waiting_total <= 0: + return False + + content = ( + "【设计师在线提醒】\n" + f"当前时间:{now.strftime('%Y-%m-%d %H:%M:%S')}\n" + "8点到24点内检测到暂无设计师接单,有客户待转接。\n" + f"触发来源:{trigger or '-'}\n" + f"当前会话:{customer_id or '-'} / {acc_id or '-'}\n" + f"待转接池:{pending_count}人\n" + f"当前客户池:{waiting_total}人(近{pool.get('window_minutes') or DESIGNER_ALERT_POOL_WINDOW_MINUTES}分钟最后一条仍是客户消息)\n" + "店铺分布:\n" + f"{self._render_shop_lines(pool)}\n" + "群里如果有设计师在线,麻烦看一下。" + ) + ok = await wecom_bot_service.send_text(content) + if ok: + self._last_alert_at = now_ts + logger.info( + f"[DesignerAlert] 已发送企微提醒 trigger={trigger} pending={pending_count} waiting={waiting_total}" + ) + return ok + + +designer_alert_service = DesignerAlertService()