补齐成交跟进:按线路推成交通知、收下未成交原因、协商中按工作日再问。
成交走询价消息服务;陆运/海运私聊下一句能记下原因;协商中回执写明顺延三个工作日和下次 9:00。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
成交跟进纯规则:工作日顺延、线路对负责人。
|
||||
|
||||
本文件职责:给私聊/群聊成交共用,不发企微、不写主账。
|
||||
禁止:在回调线程算完还同步推负责人;把六态写进本文件。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timedelta, time
|
||||
from typing import Any, Iterable
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from agent.schema.tms_air_query import resolve_airport_code
|
||||
|
||||
SHANGHAI = ZoneInfo("Asia/Shanghai")
|
||||
|
||||
# 法定休息日(不含调休上班)。调休日放进 MAKEUP_WORKDAYS,周末也算工作日。
|
||||
_HOLIDAYS = frozenset(
|
||||
{
|
||||
# 2026
|
||||
date(2026, 1, 1),
|
||||
date(2026, 1, 2),
|
||||
date(2026, 1, 3),
|
||||
date(2026, 2, 15),
|
||||
date(2026, 2, 16),
|
||||
date(2026, 2, 17),
|
||||
date(2026, 2, 18),
|
||||
date(2026, 2, 19),
|
||||
date(2026, 2, 20),
|
||||
date(2026, 2, 21),
|
||||
date(2026, 2, 22),
|
||||
date(2026, 2, 23),
|
||||
date(2026, 4, 4),
|
||||
date(2026, 4, 5),
|
||||
date(2026, 4, 6),
|
||||
date(2026, 5, 1),
|
||||
date(2026, 5, 2),
|
||||
date(2026, 5, 3),
|
||||
date(2026, 5, 4),
|
||||
date(2026, 5, 5),
|
||||
date(2026, 6, 19),
|
||||
date(2026, 6, 20),
|
||||
date(2026, 6, 21),
|
||||
date(2026, 9, 25),
|
||||
date(2026, 9, 26),
|
||||
date(2026, 9, 27),
|
||||
date(2026, 10, 1),
|
||||
date(2026, 10, 2),
|
||||
date(2026, 10, 3),
|
||||
date(2026, 10, 4),
|
||||
date(2026, 10, 5),
|
||||
date(2026, 10, 6),
|
||||
date(2026, 10, 7),
|
||||
date(2026, 10, 8),
|
||||
# 2027 元旦预留
|
||||
date(2027, 1, 1),
|
||||
date(2027, 1, 2),
|
||||
date(2027, 1, 3),
|
||||
}
|
||||
)
|
||||
|
||||
# 周末调休上班:算工作日。
|
||||
_MAKEUP_WORKDAYS = frozenset(
|
||||
{
|
||||
date(2026, 1, 4),
|
||||
date(2026, 2, 14),
|
||||
date(2026, 2, 28),
|
||||
date(2026, 5, 9),
|
||||
date(2026, 10, 10),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def is_workday(day: date) -> bool:
|
||||
"""周六日默认休息;法定假休息;调休上班算工作日。"""
|
||||
if day in _HOLIDAYS:
|
||||
return False
|
||||
if day in _MAKEUP_WORKDAYS:
|
||||
return True
|
||||
return day.weekday() < 5
|
||||
|
||||
|
||||
def shift_workdays(start: date, count: int) -> date:
|
||||
"""
|
||||
从 start 当天之后数 count 个工作日。
|
||||
|
||||
成交选协商中:顺延 3 个工作日后,第四个工作日再问,故 count=4。
|
||||
"""
|
||||
if count < 1:
|
||||
return start
|
||||
cur = start
|
||||
seen = 0
|
||||
while seen < count:
|
||||
cur = cur + timedelta(days=1)
|
||||
if is_workday(cur):
|
||||
seen += 1
|
||||
return cur
|
||||
|
||||
|
||||
def next_followup_at(now: datetime | None = None) -> datetime:
|
||||
"""第四个工作日 Asia/Shanghai 09:00。"""
|
||||
base = now.astimezone(SHANGHAI) if now else datetime.now(SHANGHAI)
|
||||
day = shift_workdays(base.date(), 4)
|
||||
return datetime.combine(day, time(9, 0), tzinfo=SHANGHAI)
|
||||
|
||||
|
||||
def _split_routes(raw: str) -> list[str]:
|
||||
out: list[str] = []
|
||||
for part in (raw or "").replace(",", ",").replace("、", ",").split(","):
|
||||
t = part.strip()
|
||||
if t:
|
||||
out.append(t)
|
||||
return out
|
||||
|
||||
|
||||
def air_route_keys(facts: dict[str, str] | None) -> set[str]:
|
||||
"""本单可对上的空运线路名:三字码对 + 目的港。"""
|
||||
src = dict(facts or {})
|
||||
origin = resolve_airport_code(str(src.get("起运港") or src.get("originCode") or ""))
|
||||
dest = resolve_airport_code(str(src.get("目的港") or src.get("destinationCode") or ""))
|
||||
keys: set[str] = set()
|
||||
if dest:
|
||||
keys.add(dest)
|
||||
if origin and dest:
|
||||
keys.add(f"{origin}-{dest}")
|
||||
return keys
|
||||
|
||||
|
||||
def _is_waicai(facts: dict[str, str] | None) -> bool:
|
||||
src = dict(facts or {})
|
||||
for key in ("外采", "是否外采", "采购方式"):
|
||||
val = str(src.get(key) or "").strip().lower()
|
||||
if val in {"1", "是", "true", "yes", "外采"}:
|
||||
return True
|
||||
blob = "".join(str(v) for v in src.values())
|
||||
return "外采" in blob
|
||||
|
||||
|
||||
def leader_matches(
|
||||
*,
|
||||
role_code: str,
|
||||
routes: str,
|
||||
is_leader: bool,
|
||||
business_line: str,
|
||||
facts: dict[str, str] | None,
|
||||
) -> bool:
|
||||
"""
|
||||
员工管理「是负责人 + 负责线路对得上」。
|
||||
|
||||
海运:海运全线。陆运:线路类别整词。空运:三字码对 / 目的港 / 全线;外采须工单标明。
|
||||
"""
|
||||
if not is_leader:
|
||||
return False
|
||||
line = (business_line or "").strip().upper()
|
||||
code = (role_code or "").strip().lower()
|
||||
tokens = _split_routes(routes)
|
||||
if line == "SEA":
|
||||
return code == "sea" and "海运全线" in tokens
|
||||
if line == "LAND":
|
||||
want = str((facts or {}).get("线路类别") or "").strip()
|
||||
return code == "land" and bool(want) and want in tokens
|
||||
if line == "AIR":
|
||||
if code != "air":
|
||||
return False
|
||||
if "全线" in tokens:
|
||||
return True
|
||||
if "外采-空运" in tokens and _is_waicai(facts):
|
||||
return True
|
||||
keys = air_route_keys(facts)
|
||||
return any(t in keys for t in tokens)
|
||||
return False
|
||||
|
||||
|
||||
def pick_leaders(
|
||||
staff: Iterable[dict[str, Any]],
|
||||
*,
|
||||
business_line: str,
|
||||
facts: dict[str, str] | None,
|
||||
) -> list[dict[str, str]]:
|
||||
"""从员工行筛出该推的负责人(启用且有企微 id)。"""
|
||||
out: list[dict[str, str]] = []
|
||||
seen: set[str] = set()
|
||||
for row in staff:
|
||||
if str(row.get("status") or "active").lower() != "active":
|
||||
continue
|
||||
wecom = str(row.get("wecomId") or row.get("wecom_id") or "").strip()
|
||||
if not wecom or wecom in seen:
|
||||
continue
|
||||
leader_flag = row.get("isLeader")
|
||||
if leader_flag is None:
|
||||
leader_flag = row.get("is_leader")
|
||||
is_leader = leader_flag in {True, 1, "1", "true", "True"}
|
||||
if not leader_matches(
|
||||
role_code=str(row.get("roleCode") or row.get("role_code") or ""),
|
||||
routes=str(row.get("routes") or ""),
|
||||
is_leader=bool(is_leader),
|
||||
business_line=business_line,
|
||||
facts=facts,
|
||||
):
|
||||
continue
|
||||
seen.add(wecom)
|
||||
out.append(
|
||||
{
|
||||
"name": str(row.get("name") or "").strip(),
|
||||
"wecomId": wecom,
|
||||
"roleCode": str(row.get("roleCode") or row.get("role_code") or ""),
|
||||
}
|
||||
)
|
||||
return out
|
||||
Reference in New Issue
Block a user