feat(analytics): /overview 端点+unmapped_accounts 标注字段

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-04 18:04:05 +08:00
parent ad153aa2f7
commit b067d690f1
3 changed files with 139 additions and 18 deletions
+31 -14
View File
@@ -136,27 +136,39 @@ async def _first_contact(
async def _active_staff(
session: AsyncSession, channel: str, days: list[int]
) -> int:
"""活跃员工 distinct user_id;stat_daily_staff 空表回退 msg_daily_stats+映射。"""
) -> tuple[int, int]:
"""活跃员工 (distinct 数, 未映射账号数);stat_daily_staff 空表回退 msg_daily_stats+映射。
未映射口径(spec §8「未映射账号按账号计并标注」):
- rollup:当窗 distinct user_id 不在 account_map 值集(=在册员工 user_id
全集)——recompute 以 amap.get(owner, owner) 落表,映射不上的 owner
以原值混入 user_id 列,值集之外的即未映射;
- 回退:当窗 owner 中 amap.get(o, o)==o(映射不上,按账号原值计入
活跃数)的计未映射——与 rollup 口径一致(映射不上的账号原样出现)。
"""
if await _staff_rows(session):
val = (
await session.execute(
sa.select(sa.func.count(sa.func.distinct(StatDailyStaff.user_id))).where(
StatDailyStaff.channel == channel,
StatDailyStaff.day.in_(days),
uids = set(
(
await session.execute(
sa.select(StatDailyStaff.user_id).where(
StatDailyStaff.channel == channel,
StatDailyStaff.day.in_(days),
)
)
)
).scalar_one()
return int(val)
).scalars()
)
mapped = set((await account_map(session, channel)).values())
return len(uids), len(uids - mapped)
amap = await account_map(session, channel)
owners = {amap.get(o, o) for (o,) in (
owners = {o for (o,) in (
await session.execute(
sa.select(MsgDailyStats.owner_account_id).where(
MsgDailyStats.channel == channel, MsgDailyStats.day.in_(days)
)
)
).all()}
return len(owners)
unmapped = sum(1 for o in owners if o not in amap)
return len({amap.get(o, o) for o in owners}), unmapped
def anomaly_threshold() -> float:
@@ -248,7 +260,10 @@ async def anomaly(session: AsyncSession, channel: str) -> dict | None:
async def overview(
session: AsyncSession, *, channel: str, scope: str, trend_days: int = 30
) -> dict[str, Any]:
"""A1:卡片 + 环比(仅周/月,同期对齐) + 近 N 天趋势 + 异常警示。"""
"""A1:卡片 + 环比(仅周/月,同期对齐) + 近 N 天趋势 + 异常警示。
cards.unmapped_accounts=活跃数里映射不上的账号数(spec §8 按账号计并标注)。
"""
if trend_days < 1 or trend_days > 90:
raise ValueError("trend_days must be 1..90")
today = today_key()
@@ -257,10 +272,12 @@ async def overview(
msg_total = sum(int(s or 0) + int(r or 0) for _d, _k, _p, s, r in rows)
active_customers = len({p for _d, kind, p, _s, _r in rows if kind == "friend"})
active_staff, unmapped = await _active_staff(session, channel, days)
cards: dict[str, Any] = {
"msg_total": msg_total,
"active_customers": active_customers,
"active_staff": await _active_staff(session, channel, days),
"active_staff": active_staff,
"unmapped_accounts": unmapped,
"call_cnt": await _call_count(session, days),
"first_contact": await _first_contact(session, channel, days),
}