feat(analytics): 首响时长+回复率数据管道——消息串算法/日汇总扩展列/员工聚合
批1(A1+A2):管理层要看「每个人的响应质量」。 - 迁移 d9e0f1a2b3c4:msg_daily_stats/stat_daily_staff 各加 cust_threads/threads_answered/fr_sum_ms(NOT NULL server_default 0, 幂等守卫照 9b0d0c133035 惯例;models.py 双声明同步) - summary.py _thread_stats:串=客户连续消息不被员工消息打断的段, is_send=None 不参与串判定;fr=员工首条回复−串末条客户消息; 日切近似 v1(日末未闭合串计 threads 不计 answered,口径写进 docstring) - recompute_days/recompute_all 都先按 ts 排序再算串(乱序防护) - staff_summary 聚合三列按账号→员工累加(fr_cnt≡threads_answered 不单列) - 测试 +10:串算法矩阵 9 例+staff 聚合传递;全量 589 绿 - 本地 86 万行回填验证:daily-stats 60s/staff 10s,rollup 总和与 明细完全一致,随机 5 行 SQL 独立复算全对 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -205,3 +205,44 @@ async def test_recompute_all_and_rolling(make_client):
|
||||
await make_client(seed)
|
||||
days = staff_summary.rolling_days(20260904, window=3)
|
||||
assert days == [20260902, 20260903, 20260904]
|
||||
|
||||
|
||||
async def test_recompute_carries_thread_stats(make_client):
|
||||
"""首响/回复率三列(2026-09-07 批1)按账号→员工累加传递;未映射账号同样保留。"""
|
||||
|
||||
async def seed(session):
|
||||
session.add(WechatAccount(wx_id="wx1", user_id="u1", user_name="张三"))
|
||||
session.add(WechatAccount(wx_id="wx2", user_id="u1", user_name="张三"))
|
||||
rows = [
|
||||
MsgDailyStats(channel="wechat", owner_account_id="wx1", peer_kind="friend",
|
||||
peer_id="t1", day=20260904, send_cnt=10, recv_cnt=5,
|
||||
last_ts=1756900000000, last_is_send=True,
|
||||
cust_threads=4, threads_answered=3, fr_sum_ms=600_000),
|
||||
MsgDailyStats(channel="wechat", owner_account_id="wx2", peer_kind="room",
|
||||
peer_id="r1", day=20260904, send_cnt=2, recv_cnt=1,
|
||||
last_ts=1756900001000, last_is_send=False,
|
||||
cust_threads=2, threads_answered=1, fr_sum_ms=240_000),
|
||||
# 旧数据行(迁移前):三列 server_default 兜 0,聚合不炸
|
||||
MsgDailyStats(channel="wechat", owner_account_id="wx3", peer_kind="friend",
|
||||
peer_id="t9", day=20260904, send_cnt=1, recv_cnt=0,
|
||||
last_ts=1756900002000, last_is_send=True),
|
||||
]
|
||||
session.add_all(rows)
|
||||
await session.commit()
|
||||
await staff_summary.recompute_days(session, [20260904])
|
||||
await session.commit()
|
||||
from sqlalchemy import select
|
||||
|
||||
by_uid = {
|
||||
r.user_id: r
|
||||
for r in (await session.execute(select(StatDailyStaff))).scalars().all()
|
||||
}
|
||||
u1 = by_uid["u1"] # wx1+wx2 合并:threads=6, answered=4, fr_sum=840000
|
||||
assert u1.cust_threads == 6
|
||||
assert u1.threads_answered == 4
|
||||
assert u1.fr_sum_ms == 840_000
|
||||
assert by_uid["wx3"].cust_threads == 0 # 未传三列 → server_default 0
|
||||
assert by_uid["wx3"].threads_answered == 0
|
||||
assert by_uid["wx3"].fr_sum_ms == 0
|
||||
|
||||
await make_client(seed)
|
||||
|
||||
Reference in New Issue
Block a user