"""A1 总览指标:卡片/首沟通口径/环比同期对齐(仅周月)/异常警示。""" from __future__ import annotations import pytest from app.analytics import metrics from app.db.models import ( MsgDailyStats, PhoneCall, StatDailyStaff, WechatAccount, ) _TODAY = 20260904 # 测试统一用固定 day 键,monkeypatch today_key async def _seed(session): session.add(WechatAccount(wx_id="wx1", user_id="u1", user_name="张三")) # 昨日(20260903):u1 90 条;前 7 日均值(20260827-0902):每天 200 → 偏离 -55%>50% 触发 # (阈值是严格 >0.5——正好 -50% 不触发,seed 用 90) for day, sent in [(20260827, 200), (20260828, 200), (20260829, 200), (20260830, 200), (20260831, 200), (20260901, 200), (20260902, 200), (20260903, 90)]: session.add(StatDailyStaff(day=day, channel="wechat", user_id="u1", msg_sent=sent, msg_recv=0, active_friends=5, active_rooms=1)) # 当日 rollup 行(active_staff 卡片读 stat_daily_staff,必须有当日行) session.add(StatDailyStaff(day=20260904, channel="wechat", user_id="u1", msg_sent=15, msg_recv=7, active_friends=1, active_rooms=1)) # 当日 + 首沟通:t1 当日首现(首沟通=1),t2 早在 20260827 就有(不算) session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1", peer_kind="friend", peer_id="t1", day=20260904, send_cnt=10, recv_cnt=5, last_ts=1788500000000, last_is_send=True)) session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1", peer_kind="friend", peer_id="t2", day=20260827, send_cnt=3, recv_cnt=0, last_ts=1787800000000, last_is_send=True)) session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1", peer_kind="room", peer_id="r1", day=20260904, send_cnt=7, recv_cnt=0, last_ts=1788500000000, last_is_send=True)) # 通话时间戳必须是 2026-09-04 当日(1756950000000 是 2025 年——坑) session.add(PhoneCall(call_id="c1", channel="voice_call", start_call_time=1788500000000, call_duration=60)) await session.commit() async def test_overview_today(make_client, monkeypatch): monkeypatch.setattr(metrics, "today_key", lambda: _TODAY) async def seed(session): await _seed(session) out = await metrics.overview(session, channel="wechat", scope="today") assert out["cards"]["msg_total"] == 22 # (10+5)+(7+0) assert out["cards"]["active_customers"] == 1 # peer_kind=friend 去重,群不计 assert out["cards"]["active_staff"] == 1 assert out["cards"]["call_cnt"] == 1 assert out["cards"]["first_contact"] == 1 # t1 全史 MIN(day)=今日;t2 不是 assert "msg_total_delta" not in out["cards"] # 今日无环比 assert "prev_msg_total" not in out["cards"] await make_client(seed) async def test_anomaly_yesterday_vs_prev7(make_client, monkeypatch): monkeypatch.setattr(metrics, "today_key", lambda: _TODAY) async def seed(session): await _seed(session) a = await metrics.anomaly(session, channel="wechat") assert a is not None assert a["total_delta_pct"] <= -0.5 # 90 vs 均值 200 = -55%(严格 >50% 触发) staff = {s["user_id"]: s for s in a["staff"]} assert staff["u1"]["yesterday"] == 90 assert staff["u1"]["prev7_mean"] == 200.0 await make_client(seed) async def test_overview_fallback_when_rollup_empty(make_client, monkeypatch): """stat_daily_staff 空表 → 员工卡片回退实时聚合 msg_daily_stats(部署即用)。""" monkeypatch.setattr(metrics, "today_key", lambda: _TODAY) async def seed(session): session.add(WechatAccount(wx_id="wx1", user_id="u1")) session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1", peer_kind="friend", peer_id="t1", day=20260904, send_cnt=2, recv_cnt=1, last_ts=1788500000000, last_is_send=True)) await session.commit() out = await metrics.overview(session, channel="wechat", scope="today") assert out["cards"]["active_staff"] == 1 assert out["cards"]["msg_total"] == 3 await make_client(seed) async def test_anomaly_fallback_multi_account(make_client, monkeypatch): """回退路径同人多账号累加:wx1/wx2 同映射 u1,昨日量必须相加而非覆盖。""" monkeypatch.setattr(metrics, "today_key", lambda: _TODAY) async def seed(session): session.add(WechatAccount(wx_id="wx1", user_id="u1")) session.add(WechatAccount(wx_id="wx2", user_id="u1")) # 不 seed StatDailyStaff → 走 msg_daily_stats 回退路径。 # 昨日(20260903)各 100(合计 200);prev7 每天各 50(日均 100)→ 偏离 +100% 触发 for day in [20260827, 20260828, 20260829, 20260830, 20260831, 20260901, 20260902, 20260903]: sent = 100 if day == 20260903 else 50 for wx in ("wx1", "wx2"): session.add(MsgDailyStats(channel="wechat", owner_account_id=wx, peer_kind="friend", peer_id="t1", day=day, send_cnt=sent, recv_cnt=0, last_ts=1788500000000, last_is_send=True)) await session.commit() a = await metrics.anomaly(session, channel="wechat") assert a is not None staff = {s["user_id"]: s for s in a["staff"]} # 修复前:后写账号覆盖前写,u1 昨日只剩 100;修复后两账号累加 = 200 assert staff["u1"]["yesterday"] == 200 assert staff["u1"]["prev7_mean"] == 100.0 await make_client(seed) def test_prev_scope_days_aligned(): """环比窗=同期对齐(非滚动紧邻):周各日-7天;月=上月同日短月钳月末。""" week = metrics.scope_day_keys("week", 20260902) # 周三:本周一..三 assert week == [20260831, 20260901, 20260902] # 上周周一..上周三(各日-7)——不是跨周末的滚动紧邻窗 assert metrics._prev_scope_days("week", week) == [20260824, 20260825, 20260826] month = metrics.scope_day_keys("month", 20260902) assert month == [20260901, 20260902] assert metrics._prev_scope_days("month", month) == [20260801, 20260802] # 月末短月钳制:2026 非闰年 3/31→2/28;闰年 2024 3/31→2/29;跨年 1/1→上年 12/1 assert metrics._prev_scope_days("month", [20260331]) == [20260228] assert metrics._prev_scope_days("month", [20240331]) == [20240229] assert metrics._prev_scope_days("month", [20260101]) == [20251201] async def test_overview_trend_days_validation(make_client, monkeypatch): """trend_days 越界(1..90 外)直接 ValueError,不静默出奇怪窗口。""" monkeypatch.setattr(metrics, "today_key", lambda: _TODAY) async def seed(session): with pytest.raises(ValueError, match="trend_days"): await metrics.overview(session, channel="wechat", scope="today", trend_days=0) with pytest.raises(ValueError, match="trend_days"): await metrics.overview(session, channel="wechat", scope="today", trend_days=91) await make_client(seed) async def test_week_delta_none_when_prev_week_empty(make_client, monkeypatch): """上周边无数据(prev_total=0)→ delta=None,不除零不误报。""" monkeypatch.setattr(metrics, "today_key", lambda: _TODAY) async def seed(session): session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1", peer_kind="friend", peer_id="t1", day=20260904, send_cnt=5, recv_cnt=0, last_ts=1788500000000, last_is_send=True)) await session.commit() out = await metrics.overview(session, channel="wechat", scope="week") assert out["cards"]["prev_msg_total"] == 0 assert out["cards"]["msg_total_delta"] is None await make_client(seed) async def test_anomaly_none_when_no_prev_data(make_client, monkeypatch): """全员无前 7 日数据(totals_p<=0)→ 异常警示 None(无基数可算)。""" monkeypatch.setattr(metrics, "today_key", lambda: _TODAY) async def seed(session): session.add(StatDailyStaff(day=20260903, channel="wechat", user_id="u1", msg_sent=100, msg_recv=0, active_friends=1, active_rooms=0)) await session.commit() assert await metrics.anomaly(session, channel="wechat") is None await make_client(seed) async def test_anomaly_new_staff_detail_and_sort(make_client, monkeypatch): """新员工(前7日无量、昨日>0)进明细带 new 标记;明细按昨日量降序。""" monkeypatch.setattr(metrics, "today_key", lambda: _TODAY) async def seed(session): for day, sent in [(20260827, 100), (20260828, 100), (20260829, 100), (20260830, 100), (20260831, 100), (20260901, 100), (20260902, 100), (20260903, 250)]: session.add(StatDailyStaff(day=day, channel="wechat", user_id="u1", msg_sent=sent, msg_recv=0, active_friends=1, active_rooms=0)) # u2 昨日新出现(前 7 日无量)——修复前从明细里消失 session.add(StatDailyStaff(day=20260903, channel="wechat", user_id="u2", msg_sent=80, msg_recv=0, active_friends=1, active_rooms=0)) await session.commit() a = await metrics.anomaly(session, channel="wechat") assert a is not None assert [s["user_id"] for s in a["staff"]] == ["u1", "u2"] # 降序:250 > 80 staff = {s["user_id"]: s for s in a["staff"]} assert staff["u2"]["new"] is True assert staff["u2"]["yesterday"] == 80 assert "new" not in staff["u1"] # 老员工不带标记 await make_client(seed)