feat(analytics): 管理层指标呈现——首响回复率上墙/客户资产总览/自定义日期/排行五新列

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-07 14:41:08 +08:00
parent feb3b98665
commit 085a9b4fd3
6 changed files with 606 additions and 43 deletions
+27 -2
View File
@@ -1,6 +1,7 @@
"""GET /api/analytics/* —— 数据分析板块端组(前缀分组=未来角色权限落点)。"""
from __future__ import annotations
import datetime as dt
from typing import Annotated, Any, Literal
import sqlalchemy as sa
@@ -21,7 +22,11 @@ ChannelParam = Annotated[Literal["wechat", "whatsapp"], Query()]
# 看板 v2(2026-09-07):overview 默认全平台——"all" 在 router 层翻译成
# metrics 的 channel=None,不把哨兵字符串传进 SQL
AllChannelParam = Annotated[Literal["all", "wechat", "whatsapp"], Query()]
ScopeParam = Annotated[Literal["today", "week", "month", "last30"], Query()]
# C1(批2):custom=自定义日期范围,配合 start/end yyyymmdd
ScopeParam = Annotated[
Literal["today", "week", "month", "last30", "custom"], Query()
]
YmdParam = Annotated[int, Query(ge=20000101, le=29991231)]
@router.get("/health")
@@ -36,6 +41,8 @@ async def analytics_overview(
session: SessionDep,
channel: AllChannelParam = "all",
scope: ScopeParam = "today",
start: YmdParam | None = None,
end: YmdParam | None = None,
) -> dict[str, Any]:
"""A1 经营总览:卡片+趋势+异常警示一次返回(单往返)。
@@ -43,9 +50,27 @@ async def analytics_overview(
双渠道趋势序列;单渠道行为不变。
cards 不含 data_as_of——数据截至走 /health(data_as_of 为 ms-epoch int
与 entities[].last_seen 为 ISO 字符串的双格式)。
scope=custom(C1):start/end=yyyymmdd(缺一/日历非法/Start>End/跨度
>366 天皆 422);环比=等长紧邻前窗滚动(不做同期对齐)。
"""
s = e = None
if scope == "custom":
if start is None or end is None:
raise HTTPException(422, "scope=custom requires start and end")
try:
sd = dt.date(start // 10000, (start // 100) % 100, start % 100)
ed = dt.date(end // 10000, (end // 100) % 100, end % 100)
except ValueError as exc: # 如 20260230——Query 只拦范围不拦日历
raise HTTPException(422, "invalid start/end calendar date") from exc
if sd > ed:
raise HTTPException(422, "start must be <= end")
if (ed - sd).days > 365: # 含端点共 366 天
raise HTTPException(422, "custom span must be <= 366 days")
s, e = start, end
return await metrics_svc.overview(
session, channel=None if channel == "all" else channel, scope=scope
session, channel=None if channel == "all" else channel, scope=scope,
start=s, end=e,
)