From 422a726ee8d29a221bac078d709a41d3b0e9bc23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Thu, 10 Sep 2026 12:20:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(management):=20=E9=A9=BE=E9=A9=B6=E8=88=B1?= =?UTF-8?q?KPI=E7=8E=AF=E6=AF=94(prev=5Fkpis+delta)+=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E5=8D=A1=E5=B8=A6=E5=91=98=E5=B7=A5=E5=90=8D=E8=B7=B3=E8=AF=A6?= =?UTF-8?q?=E6=83=85+=E5=88=97=E8=A1=A8=E8=BF=9B=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E5=B8=A6=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- yunke-server/app/api/routers/management.py | 30 +++++++++++++- yunke-server/tests/test_management_api.py | 40 +++++++++++++++++-- yunkes/src/api/management.ts | 5 +++ .../src/components/management/AnomalyList.vue | 38 +++++++++++++++--- .../management/ManagementKpiCard.vue | 35 +++++++++++++++- .../management/EmployeeAnalyticsView.vue | 12 +++++- .../management/ManagementDashboardView.vue | 13 ++++++ 7 files changed, 160 insertions(+), 13 deletions(-) diff --git a/yunke-server/app/api/routers/management.py b/yunke-server/app/api/routers/management.py index f642fdc..ef4ad86 100644 --- a/yunke-server/app/api/routers/management.py +++ b/yunke-server/app/api/routers/management.py @@ -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, diff --git a/yunke-server/tests/test_management_api.py b/yunke-server/tests/test_management_api.py index ffd83d0..f74b2f3 100644 --- a/yunke-server/tests/test_management_api.py +++ b/yunke-server/tests/test_management_api.py @@ -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) diff --git a/yunkes/src/api/management.ts b/yunkes/src/api/management.ts index d9b8d5a..07f2643 100644 --- a/yunkes/src/api/management.ts +++ b/yunkes/src/api/management.ts @@ -26,6 +26,8 @@ export interface SvcTrendPoint { day: number; fr_p50_ms: number | null; fr_p90_m export interface DimSummary { [dim: string]: { name: string; weight: number; scored_count: number; pending_count: number; insufficient_count: number; avg_score: number | null } } export interface AnomalyCard { rule_code: string; severity: 'critical' | 'warning'; employee_user_id: string + // dashboard 端点 enrich(批次D,跳员工详情用);员工详情端点不带 + employee_name?: string title: string; current_value: number; baseline_value: number | null; unit: string sample_size: number; affected_count: number; reason: string drilldown: { type: string; query: { metric_code?: string; user_id?: string } } @@ -34,6 +36,9 @@ export interface AnomalyCard { export interface DashboardResp { scope: MgmtScope; data_as_of: number | null; metric_version: string; config_source: 'db' | 'default' kpis: Kpis + // 前窗环比块(批次D):prev 窗同式聚合;可选防御旧缓存响应 + prev_kpis?: Pick + msg_delta?: number | null // (msg_total-prev)/prev,prev=0 → null;客户端三卡自算,此键备用 communication_trend: CommTrendPoint[] service_trend: SvcTrendPoint[] lifecycle: Kpis['lifecycle'] diff --git a/yunkes/src/components/management/AnomalyList.vue b/yunkes/src/components/management/AnomalyList.vue index 8ba0531..bdb01d7 100644 --- a/yunkes/src/components/management/AnomalyList.vue +++ b/yunkes/src/components/management/AnomalyList.vue @@ -14,6 +14,13 @@ @@ -43,16 +50,17 @@