fix(analytics): churn 排序决胜列+active_days去重日+边界测试
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -342,14 +342,20 @@ def _now_ms() -> int:
|
||||
|
||||
|
||||
def _next_tier(tier: int) -> int:
|
||||
return {30: 60, 60: 90, 90: 10_000}[tier]
|
||||
try:
|
||||
return {30: 60, 60: 90, 90: 10_000}[tier]
|
||||
except KeyError:
|
||||
raise ValueError(f"bad tier: {tier!r}") from None
|
||||
|
||||
|
||||
async def churn(
|
||||
session: AsyncSession, *, channel: str, kind: str, tier: int,
|
||||
min_msgs: int, page: int, page_size: int,
|
||||
) -> dict:
|
||||
"""A3 三分法(spec §3.3)。tier ∈ {30,60,90} 分层区间 [tier, next) ,90=≥90。"""
|
||||
"""A3 三分法(spec §3.3)。tier ∈ {30,60,90} 分层区间 [tier, next) ,90=≥90。
|
||||
|
||||
active_days=历史活跃天数(distinct 自然日;peer 跨账号转移同日多行只计一日)。
|
||||
"""
|
||||
day_ms = 86_400_000
|
||||
now = _now_ms()
|
||||
base = [
|
||||
@@ -379,16 +385,19 @@ async def churn(
|
||||
)
|
||||
)
|
||||
)
|
||||
order = Customer.last_chat_time.asc()
|
||||
# 决胜列 friend_id:count/分页两查询间同 last_chat_time 组内顺序稳定
|
||||
order = [Customer.last_chat_time.asc(), Customer.friend_id.asc()]
|
||||
elif kind == "inactive":
|
||||
base += [Customer.last_chat_time.is_(None)]
|
||||
order = Customer.friend_id.asc()
|
||||
order = [Customer.friend_id.asc()]
|
||||
elif kind == "handover":
|
||||
base += [Customer.user_id.in_(
|
||||
sa.select(Staff.user_id).where(Staff.is_on_job.is_(False))
|
||||
)]
|
||||
# MySQL 由 compat.py 编译补丁剥 NULLS LAST(DESC 下语义天然等价)
|
||||
order = Customer.last_chat_time.desc().nulls_last()
|
||||
# MySQL 由 compat.py 编译补丁剥 NULLS LAST(DESC 下语义天然等价);
|
||||
# NULL 尾部密集(离职员工的未激活客户),必须带 friend_id 决胜列防翻页重复
|
||||
order = [Customer.last_chat_time.desc().nulls_last(),
|
||||
Customer.friend_id.asc()]
|
||||
else:
|
||||
raise ValueError(f"bad kind: {kind!r}")
|
||||
|
||||
@@ -399,12 +408,13 @@ async def churn(
|
||||
).scalar_one()
|
||||
rows = (
|
||||
await session.execute(
|
||||
sa.select(Customer).where(*base).order_by(order)
|
||||
sa.select(Customer).where(*base).order_by(*order)
|
||||
.offset((page - 1) * page_size).limit(page_size)
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
# enrich:历史消息量/活跃天数(msg_daily_stats 按 peer 聚合)
|
||||
# enrich:历史消息量/活跃天数(msg_daily_stats 按 peer 聚合;
|
||||
# 活跃天数按 distinct 自然日计——peer 跨账号同日多行不重复计)
|
||||
items = []
|
||||
if rows:
|
||||
ids = [c.friend_id for c in rows]
|
||||
@@ -415,7 +425,7 @@ async def churn(
|
||||
sa.func.coalesce(
|
||||
sa.func.sum(MsgDailyStats.send_cnt + MsgDailyStats.recv_cnt), 0
|
||||
),
|
||||
sa.func.count(),
|
||||
sa.func.count(sa.distinct(MsgDailyStats.day)),
|
||||
)
|
||||
.where(
|
||||
MsgDailyStats.channel == channel,
|
||||
|
||||
Reference in New Issue
Block a user