feat(analytics): A3 流失预警三分法——真流失/从未激活/待交接
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"""A3 三分法流失 + B2 该回未回。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from app.analytics import metrics
|
||||
from app.db.models import Customer, MsgDailyStats, Staff
|
||||
|
||||
_NOW = 1756950000000 # 相对时间基准(绝对日期无关紧要,断言全部相对此值自洽)
|
||||
|
||||
|
||||
def _cust(friend_id, *, last_chat=None, user_id="u1", delete=0, block=None):
|
||||
return Customer(friend_id=friend_id, channel="wechat",
|
||||
owner_account_id="wx1", user_id=user_id,
|
||||
last_chat_time=last_chat, delete_flag=delete, is_block=block)
|
||||
|
||||
|
||||
async def _seed_churn(session):
|
||||
now = _NOW
|
||||
day_ms = 86_400_000
|
||||
session.add(Staff(user_id="u9", name="离职的", is_on_job=False))
|
||||
session.add(Staff(user_id="u1", name="在职的", is_on_job=True))
|
||||
# 真流失:45 天没聊 + 历史消息 ≥5
|
||||
session.add(_cust("c_real", last_chat=now - 45 * day_ms))
|
||||
# 历史消息不足 5 → 不算真流失
|
||||
session.add(_cust("c_few", last_chat=now - 45 * day_ms))
|
||||
# 只沉默 10 天 → 不入 30 档
|
||||
session.add(_cust("c_recent", last_chat=now - 10 * day_ms))
|
||||
# 从未激活
|
||||
session.add(_cust("c_null", last_chat=None))
|
||||
# 待交接:归属离职员工
|
||||
session.add(_cust("c_hand", user_id="u9", last_chat=now - 3 * day_ms))
|
||||
# 已删好友 → 全部剔除
|
||||
session.add(_cust("c_del", last_chat=now - 45 * day_ms, delete=1))
|
||||
# 历史消息量:real=6(≥5),few=2
|
||||
for peer, n in (("c_real", 6), ("c_few", 2)):
|
||||
session.add(MsgDailyStats(channel="wechat", owner_account_id="wx1",
|
||||
peer_kind="friend", peer_id=peer, day=20260601,
|
||||
send_cnt=n, recv_cnt=0, last_ts=now - 50 * day_ms,
|
||||
last_is_send=True))
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def test_churn_real_tier30(make_client, monkeypatch):
|
||||
monkeypatch.setattr(metrics, "_now_ms", lambda: _NOW)
|
||||
async def seed(session):
|
||||
await _seed_churn(session)
|
||||
page = await metrics.churn(session, channel="wechat", kind="real",
|
||||
tier=30, min_msgs=5, page=1, page_size=20)
|
||||
ids = [r["friend_id"] for r in page["items"]]
|
||||
assert "c_real" in ids and "c_few" not in ids
|
||||
assert "c_recent" not in ids and "c_del" not in ids
|
||||
real = next(r for r in page["items"] if r["friend_id"] == "c_real")
|
||||
assert real["hist_msgs"] == 6 and real["active_days"] == 1
|
||||
|
||||
await make_client(seed)
|
||||
|
||||
|
||||
async def test_churn_inactive_and_handover(make_client, monkeypatch):
|
||||
monkeypatch.setattr(metrics, "_now_ms", lambda: _NOW)
|
||||
async def seed(session):
|
||||
await _seed_churn(session)
|
||||
p1 = await metrics.churn(session, channel="wechat", kind="inactive",
|
||||
tier=30, min_msgs=5, page=1, page_size=20)
|
||||
assert {r["friend_id"] for r in p1["items"]} == {"c_null"}
|
||||
p2 = await metrics.churn(session, channel="wechat", kind="handover",
|
||||
tier=30, min_msgs=5, page=1, page_size=20)
|
||||
assert {r["friend_id"] for r in p2["items"]} == {"c_hand"}
|
||||
|
||||
await make_client(seed)
|
||||
|
||||
|
||||
async def test_churn_endpoint_422(make_client):
|
||||
async def seed(session):
|
||||
...
|
||||
|
||||
client = await make_client(seed)
|
||||
r = await client.get("/api/analytics/churn", params={"tier": 45})
|
||||
assert r.status_code == 422
|
||||
r2 = await client.get("/api/analytics/churn", params={"channel": "sms"})
|
||||
assert r2.status_code == 422
|
||||
Reference in New Issue
Block a user