Files
ai-kefu-admin/admin/src/api/stats.ts
T
浅念 f640889f22 feat: 仪表盘(租户卡片墙/平台总览/时间切换)+ 顺手:409契约锁与死代码
- 仪表盘视图:超管=平台总览四指标卡(/api/platform/stats 合并视图)+
  租户卡片墙(逐租户 ?tenant= 下钻,含停用租户);租户管理员=自己
  指标卡(/tenants/{slug}/stats,slug 取登录态);口径文案照
  stats.py docstring(线索=留资三选一/成功率分母0显—等)
- 时间范围快捷切换(今天/近7天/近30天)→ from/to 本地日期串;
  utils/statsRange.ts 纯函数 + epoch 防快速切换过期响应竞写
- 空数据态:无租户 ElEmpty / 单租户零数据自然显 0 与 —
- api/stats.ts 补 fetchPlatformStats/fetchTenantStats;tenants.ts 补
  fetchTenantList(卡片墙枚举,其余仍留 Task 6)
- vitest +11(statsRange 6 + stats API 5,总 36)
- 顺手1:test_auth must_change 409 测试补 detail 断言,锁死前后端
  契约文案(client.ts MUST_CHANGE_DETAIL)
- 顺手2:删除 beforeEach.ts 的 isUnauthorizedError 死分支与 ApiStatus
  import(模板 isHttpError 判自建 ApiError 恒 false,分支不可达)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-13 16:59:12 +08:00

100 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 统计分析 API(Phase 3 Task 4 实装调用函数;类型与端点 Task 3 建立)
*
* 端点事实源:app/platform/routes/stats_routes.py + app/platform/stats.py
* (平台合并统计 / 单租户统计,?from/&to= 闭区间)。
*
* 指标口径事实源:app/platform/stats.py 模块 docstring 的口径表——
* 会话数不要求有消息 / 线索=留资三选一(phone/whatsapp/email)任一非空 /
* 高意向=评估 interested / 推送成功率 success/(success+failed)(分母 0 →
* null,前端展示"—")。Task 8(统计分析页)复用本组函数。
*/
import { http } from './client'
/** 指标卡(None 项为 null——如无推送时成功率无定义) */
export interface StatsCards {
conversations: number
leads: number
high_intent: number
push_success_rate: number | null
}
/** 漏斗计数 */
export interface StatsFunnel {
conversations: number
leads: number
evaluated: number
push_succeeded: number
}
/** 按日趋势行 */
export interface StatsTrendPoint {
date: string
conversations: number
leads: number
}
/** 分布视图(intent 按枚举键、source/site 计数表、country Top 数组) */
export interface StatsDistributions {
intent: Record<string, number>
source: Record<string, number>
site: Record<string, number>
country: Array<{ country: string; count: number }>
}
/** 推送健康计数 */
export interface PushHealth {
pending: number
success: number
failed: number
skipped: number
duplicated: number
}
/** 统计响应(平台合并与单租户同构) */
export interface StatsResponse {
cards: StatsCards
funnel: StatsFunnel
trend: StatsTrendPoint[]
distributions: StatsDistributions
push_health: PushHealth
}
/** 统计查询参数(from/to 闭区间,ISO 日期) */
export interface StatsParams {
from?: string
to?: string
}
/** 统计端点(platform 合并版仅超管;单租户版超管或本租户管理员) */
export const STATS_ENDPOINTS = {
platform: '/api/platform/stats',
tenant: (slug: string) => `/api/platform/tenants/${slug}/stats`
} as const
/** 平台合并统计查询参数(?tenant= 单租户下钻,未知 slug 404) */
export interface PlatformStatsParams extends StatsParams {
tenant?: string
}
/**
* 平台统计(仅超管):缺省全部租户合并(逐库查询应用层合并);
* tenant=slug 单租户下钻——仪表盘租户卡片墙逐租户调用即此形态。
* options.showErrorMessage: false 关全局错误提示(卡片墙 N 连发的失败
* toast 改由调用方逐卡内联展示)
*/
export function fetchPlatformStats(
params?: PlatformStatsParams,
options?: { showErrorMessage?: boolean }
): Promise<StatsResponse> {
return http.get<StatsResponse>(STATS_ENDPOINTS.platform, { params, ...options })
}
/**
* 单租户统计(本租户管理员或超管):租户管理员的"自己指标卡"走这里,
* slug 取登录态 tenant_slug(auth store)
*/
export function fetchTenantStats(slug: string, params?: StatsParams): Promise<StatsResponse> {
return http.get<StatsResponse>(STATS_ENDPOINTS.tenant(slug), { params })
}