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:
@@ -776,6 +776,10 @@ class MsgDailyStats(Base):
|
||||
- ``send_cnt``/``recv_cnt`` 仅计 is_send 真/假;NULL 两边都不计(对齐
|
||||
旧聚合的 case 语义)。
|
||||
- ``last_ts``/``last_is_send`` 为该日最后一条消息的时间与方向。
|
||||
- 首响/回复率三列(2026-09-07 批1)按「消息串」口径,见
|
||||
:func:`app.storage.summary._thread_stats`:串=客户连续消息不被员工
|
||||
消息打断的段(is_send=NULL 不参与串判定);日切近似——日末未闭合
|
||||
串计 threads 不计 answered,跨夜的串在次日段重新起算。
|
||||
"""
|
||||
|
||||
__tablename__ = "msg_daily_stats"
|
||||
@@ -796,6 +800,18 @@ class MsgDailyStats(Base):
|
||||
# 当日最后一条消息(ms);当天无有效方向计数的消息也存在(is_send=NULL)
|
||||
last_ts: Mapped[int | None] = mapped_column(sa.BigInteger, nullable=True)
|
||||
last_is_send: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
||||
# 当日客户串数(串=客户连续消息不被员工消息打断的段;NULL 方向不参与)
|
||||
cust_threads: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0, server_default=sa.text("0")
|
||||
)
|
||||
# 被员工回复的串数 ≡ 首响样本数 fr_cnt(fr_cnt=threads_answered 不单列)
|
||||
threads_answered: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0, server_default=sa.text("0")
|
||||
)
|
||||
# 首响样本时长和(ms):展示用首响时长 = fr_sum_ms / threads_answered
|
||||
fr_sum_ms: Mapped[int] = mapped_column(
|
||||
sa.BigInteger, nullable=False, default=0, server_default=sa.text("0")
|
||||
)
|
||||
|
||||
updated_at: Mapped[dt.datetime] = _updated_at()
|
||||
|
||||
@@ -829,6 +845,17 @@ class StatDailyStaff(Base):
|
||||
active_rooms: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0, server_default=sa.text("0")
|
||||
)
|
||||
# 首响/回复率三列(2026-09-07 批1):msg_daily_stats 同义列按账号→员工
|
||||
# 累加。回复率=Σanswered/Σthreads;首响时长=Σfr_sum_ms/Σanswered。
|
||||
cust_threads: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0, server_default=sa.text("0")
|
||||
)
|
||||
threads_answered: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0, server_default=sa.text("0")
|
||||
)
|
||||
fr_sum_ms: Mapped[int] = mapped_column(
|
||||
sa.BigInteger, nullable=False, default=0, server_default=sa.text("0")
|
||||
)
|
||||
updated_at: Mapped[dt.datetime] = _updated_at()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user