feat(management): 驾驶舱KPI环比(prev_kpis+delta)+异常卡带员工名跳详情+列表进详情带筛选

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-10 12:20:28 +08:00
parent be0d6f844e
commit 422a726ee8
7 changed files with 160 additions and 13 deletions
+29 -1
View File
@@ -184,6 +184,26 @@ async def management_dashboard(
"lifecycle": lifecycle,
}
# ---- 前窗环比基线(批次D):prev 窗全员一次 period_metrics,聚合式与
# kpis 完全同式(活跃客户=Σactive_friends、活跃员工=天数>0 计数、
# msg_total=Σ(sent+recv)、cust_threads=Σ)。lifecycle(期末存量)与
# 通话(全渠道口径,表无渠道维)不加环比——期末/混合口径的环比意义弱。
ps, pe = prev_window(start, end)
prev_pms = list((await period_metrics(session, ps, pe, ch)).values())
prev_kpis = {
# 全员求和口径同 kpis(跨员工共享客户可能重复计)
"active_customers": sum(p.active_friends for p in prev_pms),
"active_employees": sum(1 for p in prev_pms if p.days_with_data > 0),
"msg_total": sum(p.msg_sent + p.msg_recv for p in prev_pms),
"cust_threads": sum(p.cust_threads for p in prev_pms),
}
# 前窗无消息(prev=0)不算环比——None 而非 0/负∞
prev_msg = prev_kpis["msg_total"]
msg_delta = (
round((kpis["msg_total"] - prev_msg) / prev_msg, 3)
if prev_msg > 0 else None
)
# ---- 维度分布(全员 score_employee 的维度状态计数) ----
dim_summary: dict[str, dict] = {}
for a in assembled.values():
@@ -207,7 +227,13 @@ async def management_dashboard(
)
# ---- 异常汇总:critical 先、warning 后,同 severity 按 affected desc ----
anomalies = [a for data in assembled.values() for a in data["anomalies"]]
# dashboard 卡片 enrich employee_name(staff 名,assemble 已在手)供前端
# 跳员工详情;员工详情端点的 anomalies 不带(自指无意义)
anomalies = [
{**a, "employee_name": data["name"]}
for data in assembled.values()
for a in data["anomalies"]
]
sev_rank = {"critical": 0, "warning": 1}
anomalies.sort(key=lambda a: (
sev_rank.get(a["severity"], 9),
@@ -222,6 +248,8 @@ async def management_dashboard(
"metric_version": METRIC_VERSION,
"config_source": config_source,
"kpis": kpis,
"prev_kpis": prev_kpis,
"msg_delta": msg_delta,
"communication_trend": comm_trend,
"service_trend": service_trend,
"lifecycle": lifecycle,
+37 -3
View File
@@ -5,7 +5,8 @@
page_size>100)、404(employees/{ghost}、drilldown 员工不存在);
- dashboard 各段齐全与关键数值(kpis/communication_trend/service_trend/
lifecycle/dimension_summary/anomalies/action_summary/data_as_of/
metric_version);
metric_version);批次D 补:prev_kpis 环比块+msg_delta(含前窗零数据
→None)与 anomalies employee_name enrich(详情端点不带);
- employees:默认分页 20/total/critical 优先默认排序/channel=wechat 时
whatsapp-only 员工消失/keyword/status/min_days 不足→insufficient_sample;
- detail:账号清单+metrics 全字段+score 完整明细+anomalies+prev/baseline;
@@ -280,6 +281,15 @@ async def test_dashboard_structure_and_kpis(make_client):
"reactivated": 5}
assert k["lifecycle"] == body["lifecycle"]
# 前窗环比(批次D):prev 窗 0827-0831 与基线窗种子重叠——
# u1(14+14)×5 + u2(5+5)×5 = 190;宽表无 active_friends/串列 → 0
pk = body["prev_kpis"]
assert set(pk) == {"active_customers", "active_employees",
"msg_total", "cust_threads"}
assert pk == {"active_customers": 0, "active_employees": 2,
"msg_total": 190, "cust_threads": 0}
assert body["msg_delta"] == round((155 - 190) / 190, 3) # -0.184
# 趋势逐日 5 点;0901 = u1(1)+u2(20)+u3(10) 消息,宽表 active 合计 8,通话 2
trend = body["communication_trend"]
assert [t["day"] for t in trend] == [20260901 + i for i in range(5)]
@@ -306,17 +316,39 @@ async def test_dashboard_structure_and_kpis(make_client):
assert d["scored_count"] + d["insufficient_count"] + d["pending_count"] == 3
assert "avg_score" in d
# 异常:critical 在前(u2),warning 其后按 affected_count desc
# 异常:critical 在前(u2),warning 其后按 affected_count desc;
# dashboard 卡片 enrich employee_name(批次D,前端跳员工详情用)
anoms = body["anomalies"]
assert anoms[0]["severity"] == "critical"
assert anoms[0]["employee_user_id"] == "u2"
assert anoms[0]["employee_name"] == "李四"
assert all(a["severity"] == "critical" for a in anoms[:1])
assert all(a["severity"] == "warning" for a in anoms[1:])
assert all(a["employee_name"] == "张三" for a in anoms[1:])
assert body["action_summary"] == {"unreplied_2h": 1, "churn_real_30d": 0,
"sensitive_pending": 0}
async def test_dashboard_prev_zero_no_delta(make_client):
"""前窗无数据(whatsapp 视图前窗零宽表)→ prev_kpis 四键全 0、
msg_delta None(prev=0 不算环比,None 而非 0/负∞)。"""
async def seed(s):
await _seed_common(s)
client = await make_client(seed)
r = await client.get("/api/management/dashboard",
params={"channel": "whatsapp", "start": START,
"end": END})
assert r.status_code == 200
body = r.json()
assert body["kpis"]["msg_total"] == 50 # 仅 u3 的 whatsapp 宽表
assert body["prev_kpis"] == {"active_customers": 0,
"active_employees": 0, "msg_total": 0,
"cust_threads": 0}
assert body["msg_delta"] is None
# ---------------------------------------------------------------- employees
@@ -501,9 +533,11 @@ async def test_employee_detail_full(make_client):
assert body["score"]["metrics_detail"]["reply_2h_rate"]["value"] == 0.2
assert body["score"]["metrics_detail"]["fr_p90_ms"]["baseline"] == 60_000
# anomalies + prev/baseline 原始值(prev 窗 0827-0831 与基线窗重叠:
# 5 天 × msg_sent 14 = 70)
# 5 天 × msg_sent 14 = 70);详情端点 anomalies 不带 employee_name
# (自指无意义,仅 dashboard enrich)
assert len(body["anomalies"]) == 2
assert all(a["severity"] == "warning" for a in body["anomalies"])
assert all("employee_name" not in a for a in body["anomalies"])
assert body["prev"]["msg_sent"] == 70
assert body["baseline"]["msg_sent"] == 70
assert body["data_as_of"] == _ts(2026, 9, 3, 9, 0)