feat: 事件异常按工作日累计已过小时数

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-21 18:14:19 +08:00
co-authored by Cursor
parent 4381966c4e
commit ef0424a28f
2 changed files with 42 additions and 0 deletions
@@ -2,6 +2,7 @@
成交跟进纯规则:工作日顺延、线路对负责人。 成交跟进纯规则:工作日顺延、线路对负责人。
本文件职责:给私聊/群聊成交共用,不发企微、不写主账。 本文件职责:给私聊/群聊成交共用,不发企微、不写主账。
也给事件异常算工作日已过小时数(非工作日整天不算;工作日当天 0–24 都算)。
禁止:在回调线程算完还同步推负责人;把六态写进本文件。 禁止:在回调线程算完还同步推负责人;把六态写进本文件。
""" """
@@ -105,6 +106,30 @@ def next_followup_at(now: datetime | None = None) -> datetime:
return datetime.combine(day, time(9, 0), tzinfo=SHANGHAI) return datetime.combine(day, time(9, 0), tzinfo=SHANGHAI)
def workday_elapsed_hours(start: datetime, now: datetime) -> float:
"""
起止之间折合多少工作日小时。
非工作日整天不算;工作日当天 0–24 都算,不切上午晚上。
起点落在休息日:从下一个工作日 00:00 起算。
副作用:无。可在 Worker 巡检调用,禁止放进企微回调线程做 IO。
"""
a = start.astimezone(SHANGHAI) if start.tzinfo else start.replace(tzinfo=SHANGHAI)
b = now.astimezone(SHANGHAI) if now.tzinfo else now.replace(tzinfo=SHANGHAI)
if b <= a:
return 0.0
total = 0.0
cur = a
while cur.date() < b.date():
nxt = datetime.combine(cur.date() + timedelta(days=1), time(0, 0), tzinfo=SHANGHAI)
if is_workday(cur.date()):
total += (nxt - cur).total_seconds() / 3600.0
cur = nxt
if is_workday(b.date()):
total += (b - cur).total_seconds() / 3600.0
return total
def _split_routes(raw: str) -> list[str]: def _split_routes(raw: str) -> list[str]:
out: list[str] = [] out: list[str] = []
for part in (raw or "").replace(",", ",").replace("、", ",").split(","): for part in (raw or "").replace(",", ",").replace("、", ",").split(","):
+17
View File
@@ -14,6 +14,7 @@ from agent.policy.deal_followup import (
leader_matches, leader_matches,
next_followup_at, next_followup_at,
shift_workdays, shift_workdays,
workday_elapsed_hours,
) )
@@ -37,6 +38,22 @@ class TestWorkdays(unittest.TestCase):
self.assertEqual(when.tzinfo, SHANGHAI) self.assertEqual(when.tzinfo, SHANGHAI)
self.assertEqual(when.date(), date(2026, 9, 24)) self.assertEqual(when.date(), date(2026, 9, 24))
def test_skip_weekend_keep_weekday_night(self) -> None:
start = datetime(2026, 9, 18, 17, 0, tzinfo=SHANGHAI) # 周五
now = datetime(2026, 9, 21, 17, 0, tzinfo=SHANGHAI) # 周一
# 周五 17–24 = 7h,周末不算,周一 0–17 = 17h
self.assertAlmostEqual(workday_elapsed_hours(start, now), 24.0, places=4)
def test_rest_day_start_counts_from_next_workday(self) -> None:
start = datetime(2026, 9, 19, 10, 0, tzinfo=SHANGHAI) # 周六
now = datetime(2026, 9, 21, 10, 0, tzinfo=SHANGHAI) # 周一
self.assertAlmostEqual(workday_elapsed_hours(start, now), 10.0, places=4)
def test_fractional_quarter_hour(self) -> None:
start = datetime(2026, 9, 18, 9, 0, tzinfo=SHANGHAI)
now = datetime(2026, 9, 18, 9, 15, tzinfo=SHANGHAI)
self.assertAlmostEqual(workday_elapsed_hours(start, now), 0.25, places=4)
class TestAirRouteKeys(unittest.TestCase): class TestAirRouteKeys(unittest.TestCase):
def test_pair_and_dest(self) -> None: def test_pair_and_dest(self) -> None: