Files
yunke/docs/superpowers/plans/2026-09-08-management-batch-a-verification.md
T
浅念 639edf56a6 docs(management): 批次A回填实测+对账记录+DEPLOY部署注记
- 核实文档追加Task8小节:回填实测汇总(fact 68,476行/72s,宽表12,371+
  通话1,163/71s,幂等,同源68,476≡68,476,孤儿0,通话314天)
- 对账抽查:王云青+田瑞志×3日期×三组,①②组12/12逐日全一致,③组宽表
  ≤日切(差值=跨夜串归属效应,全期12,055 vs 20,981折叠57.4%验证串未丢)
- 批次A验收清单11项全过(653测试本机复跑绿,写钩子零改动逐commit核查)
- DEPLOY.md §7 新增批次A附加步骤:b7c8d9e0f1a2手动alembic+两条回填
  命令及顺序依赖(employee-metrics必须在first-reply-facts之后)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-09 16:20:33 +08:00

17 KiB
Raw Blame History

批次A Task 0 核实记录:五项核实 + 回测名单

  • 日期:2026-09-09
  • 方式:docker exec -i yunke-mysql-1 mysql -uyunke -pyunke yunke -e "<SQL>"(容器名以 docker ps 实测为准,确认为 yunke-mysql-1)
  • 用途:本记录的全部结论直接约束批次A(数据底座)后续任务的实现细节;偏差处显式标注「影响 Task N」。任务编号引用 2026-09-08-management-console-batch-a-data-foundation.md。
  • 数据快照范围:stat_daily_staff / msg_daily_stats 覆盖 20251024 ~ 20260907;phone_call.start_call_time 覆盖 2025-10-18 ~ 2026-09-05。

① 员工主数据(staff 表)

指标 实测
staff 总数 41(与计划假设 ≈41 一致)
is_on_job=1 41(全部在职)
wechat_account DISTINCT user_id 41
whatsapp_account DISTINCT user_id 20
微信幽灵账号(user_id 不在 staff) 0
WA 幽灵账号(补充核实) 0
stat_daily_staff DISTINCT user_id 40(其 user_id 全部在 staff 内;员工 FD685A5620184156BE75A8D73FB72F64 无任何消息统计行)

结论:账号表 → staff 无幽灵映射,known_user_ids 过滤(v2 §1.3 #11)零损耗,不会意外丢行。两个渠道的 account_map 产出 user_id 均能落到 staff。无偏差,Task 5/6 按计划实现即可。


② phone_call 列与单位

表结构关键列:call_id(PK)、channel、start_call_time(bigint)、call_duration(int)、call_status(int)、incoming_call(int)、user_id、call_number、fetched_at、updated_at。

channel 分布

channel 行数
voice_call 2,504
wa_voice_call 686
合计 3,190(与已知 3,190 条一致)

call_duration 单位:秒(确认)

指标 实测
总和 263,651 秒 = 73.24 小时(对照已知约 73 小时,吻合)
均值 82.65 秒(对照预期均值 ≈82 秒,吻合)

call_status × duration 交叉

call_status 行数 duration=0 duration>0 语义判定
1 1,508 31 1,477 接通(含 31 条 0 秒即挂)
2 1,682 1,682 0 未接通(未接/拒接/取消,与方向无关)

incoming_call 分布

incoming_call 行数 语义
1 1,764 呼入
0 1,426 呼出

start_call_time:毫秒 epoch(确认)

13 位整数;FROM_UNIXTIME(x/1000) 解出 max = 2026-09-05 16:00:55(合理近期),按秒解释则超范围。min = 2025-10-18。与 message.timestamp 毫秒同口径。

call_number 形态(按 channel 完全可分)

  • voice_call → 微信 ID:wxid_* 或自定义微信号(csf7169708、yuanye751380、Maple_1988 等)
  • wa_voice_call → WA JID:2348182388888@s.whatsapp.net 等

与计划默认假设的偏差(重要)

  1. 计划假设 #4「PhoneCall.channel 是通话类型(phone_call/voice_call)非消息渠道」不完全成立:实测无 phone_call(蜂窝)值,且 wa_voice_call 能从 channel 列判别 WA。计划 Task 5 注释「voice_call 行无法从列判别微信/WA」不成立。影响 Task 5:_followup_exists 可按渠道收窄——voice_call 只查该员工 wechat 账号的 1:1 消息、wa_voice_call 只查 whatsapp 账号(当前双渠道任一匹配的写法也不会错——wxid 不会撞 WA JID,仅多一次无效查询;收窄属优化非必需)。
  2. call_duration 单位=秒、start_call_time=毫秒 epoch:与计划默认一致,Task 5 无需换算(day_bounds_ms 直接用)。
  3. 接通判定:计划默认 duration>0。实测 call_status=1 AND duration>0 = 1,477 行,duration>0 口径与之差 31 行(31 条接通但 0 秒,占 1.0%)。影响 Task 5:answered=dur>0 口径可用;若追求语义精确可改 call_status=1(含 0 秒接通),差异 ≤1%,实现时任选其一并在表注释写明。

③ customer 归属字段填充率

指标 实测
总行数 16,339
user_id 填充 16,339(100%)
owner_account_id 填充 16,339(100%)
delete_flag=1 53

分渠道:wechat 7,040(owner 填充 7,040)/ whatsapp 9,299(owner 填充 9,299)。

两条归属路径一致性(全表校验,非抽样)

customer.user_id vs owner_account_id → wechat_account.wx_id / whatsapp_account.uid → user_id:

渠道 校验行数 一致
whatsapp 9,299 9,299
wechat 7,040 7,040

结论:两条路径 100% 等价、零冲突。生命周期归属覆盖面完整。影响 Task 6:默认 owner_account_id → account_map → user_id 路径确认可用;customer.user_id 列可直接当冗余等价路径使用(如需省一次 join,实测安全)。

补充观察(影响 Task 3/6 的 peer 对齐):微信侧主体是无后缀裸 ID,后缀形态是少数。customer.friend_id 实测形态分布(全表):

渠道 形态 行数 占比
wechat 无后缀裸 ID(a_515855826、a053136a 等) 6,779 96.3%
wechat @openim 后缀 261 3.7%
whatsapp @s.whatsapp.net 9,266 99.6%
whatsapp @lid 后缀 33 0.4%

message.talker_id 侧同查(行数口径):wechat 裸 ID 844,287 / @openim 16,873 / @chatroom 17,003(群);whatsapp @s.whatsapp.net 72,357 / @lid 4,284 / @g.us 9,439(群)。即:微信 peer 主键以裸 ID 为准、@openim 为少数共存;WA 侧基本全带后缀。Task 3 实现 peer 匹配时按实测形态对齐(两边原值相等才命中),不做猜测性截断/补后缀。


④ 群占比(msg_daily_stats,量化混合口径偏差)

channel peer_kind 行数 消息量(send+recv) 会话线程(cust_threads)
wechat friend 38,343 269,074 82,879
wechat room 16,234 609,089 26,220
whatsapp friend 11,673 77,994 14,178
whatsapp room 866 8,086 1,402
合计 67,116 964,243 124,679

占比汇总:

维度 room 占比 说明
微信消息量 69.4%(609,089/878,163) 微信消息近七成来自群
WA 消息量 9.4%(8,086/86,080) WA 几乎纯 1:1
总消息量 64.0%(617,175/964,243)
总会话线程 22.2%(27,622/124,679)

结论:若现有看板首响/回复率不分 peer_kind,消息量维度被群污染 64%、线程维度 22%——污染主要来自微信侧(微信消息 69% 是群消息)。批次A first_reply_fact / employee_metric_daily 强制 peer_kind='friend' 过滤是必要的,不能沿用混合口径。


⑤ call_followup join 键(call_number ↔ message.talker_id,1:1 消息)

channel 分母(call_number 非空) 命中(talker_id 匹配且非群消息) 命中率
voice_call 2,504 2,378 95.0%
wa_voice_call(补充核实) 686 681 99.3%

结论:双渠道命中率均远超 50% 阈值,call_followup_count 一期高置信,无需降级标记、无需在口径中注记低置信。影响 Task 5:24h 跟进判定按计划实现。未命中成因已精确解释(复核补充):voice_call 的 126 条未命中恰好等于 call_number 带 @chatroom 后缀的 126 条群通话(实测这 126 条对 1:1 消息全部 0 命中——join 限定 room_id 为空,群通话本就不该匹配);剔除群通话后 1:1 可匹配通话命中率为 2,378/2,378 = 100%。wa_voice_call 侧后缀分布为 @s.whatsapp.net 676 / @g.us(群)9 / @lid 1,未命中 5 条全部为 @g.us 群通话(逐条列出验证)。


回测员工名单

选取依据:消息量/WA 量/活跃群排名 + 低样本端(见文末各候选分渠道数据)。覆盖:微信为主、WA 为主、群运营为主、高负载、低样本(前三类与高负载有合并)。

# user_id 名字 覆盖类型 关键数据(20251024~20260907)
1 369D257EEC9A4357935FDE19DDD99CAA 王云青 高负载 + 群运营为主 + 微信为主(合并) 消息量 #1(162,740:微信 162,371 / WA 369);活跃群 #1(4,036);通话 431+9
2 9C0B195A2C234C829FF507E011B4B460 田瑞志 WA 为主 WA 消息 9,986 / 总 11,597 = 86%;WA 通话 28
3 F99F6557C2904A7E8F4C5A3BD8A597A1 张士蓓 双通道混合(WA 量 #1,群 #2) 微信 91,936 + WA 12,529;活跃群 2,273;通话最多(471 微信 + 97 WA)
4 DC48798A5EA24DBB8B31A89CB858E08B 15639270965 纯微信无 WA 行(验证 WA 缺失路径) 微信 78,680,stat_daily_staff 无 whatsapp 行;通话 3
5 946C08639BF84DFEBBF5EC2A607AE1FA 18539430257 低样本(验证空数据路径) 消息 8 条,无群、无通话、无 WA

名单中的 #1/#3 建议作为 Task 8「本地全量回填实测」的 2 名回测员工(数据最厚、双通道+通话+群全齐)。


偏差汇总(对后续任务的约束清单)

# 偏差/确认 影响
1 phone_call.channel 实测仅 voice_call(微信)/wa_voice_call(WA)两值,无蜂窝 phone_call 值;channel 可判别渠道,计划「无法从列判别微信/WA」结论修正 Task 5(followup 可按渠道收窄,属优化非必需)
2 call_duration 单位=秒(263,651s=73.24h≈已知 73h);start_call_time=毫秒 epoch Task 5(默认口径正确,无需换算)
3 接通判定 dur>0 vs call_status=1 差 31 行(1.0%),前者少计 31 条 0 秒接通 Task 5(任选其一,注释写明)
4 customer.owner_account_id 填充 100%,与 user_id 列全表 100% 等价 Task 6(默认 owner 路径确认;可直接用 user_id 列省 join)
5 微信侧 peer ID 主体为无后缀裸 ID(customer.friend_id 6,779/7,040=96.3%;message.talker_id 行数口径 844,287 裸 vs 16,873 @openim),后缀形态(@openim 等)为少数;WA 侧基本全带后缀(@s.whatsapp.net/@lid) Task 3/6(peer 对齐按实测形态原值匹配,勿截断/勿补后缀)
6 群消息占总消息量 64%(微信侧 69%) Task 3/6(friend 过滤必须,佐证 spec 口径)
7 call_number↔talker_id 命中率 voice_call 95.0% / wa_voice_call 99.3% Task 5(call_followup_count 高置信,不降级)
8 staff 41 行无幽灵账号;stat_daily_staff 仅 40 名员工有行 Task 5/6(known_user_ids 过滤零损耗)

批次A回填实测与对账(Task 8,2026-09-09)

  • 环境:本地 Docker MySQL(yunke-mysql-1),数据快照同上(stat/msg 覆盖 20251024~20260907)。
  • 方式:Task 7 真机全量回填后本任务只读对账抽查(未重跑回填);对账前全库同源校验复验一次,确认库状态即 Task 7 收尾状态。

1. 回填实测汇总(Task 7 真机数字,Task 8 复验)

项目 实测
first-reply-facts 11,020 会话 → 68,476 fact 行,72s(冷)
employee-metrics 12,371 宽表行(day×channel×user)+ 1,163 通话行(314 天并集),71s 冷 / 36s 热
幂等复跑 两命令复跑结果一致(delete+insert 语义,无重复无漂移)
全库同源 SUM(employee_metric_daily.threads_answered) = 68,476 ≡ fact 行数(Task 8 复验通过)
孤儿检查 宽表/通话表 user_id 不在 staff = 0;fact 账号不在对应渠道账号表 = 0(Task 8 复验通过)
通话日并集修复 employee_call_daily 覆盖 314 天(修复前 306):8 个消息空窗日找回 27 通话 / 13 接通

2. 对账抽查(2 员工 × 3 日期 × 三组)

员工取自本文回测名单:王云青 369D257EEC9A4357935FDE19DDD99CAA(#1,高负载+群运营,混合口径差异最大场景;1 微信 + 2 WA 账号)、田瑞志 9C0B195A2C234C829FF507E011B4B460(#2,WA 为主;1 微信 + 2 WA 账号)。日期 20260815 / 20260801 / 20260601(近/中/远)。跨渠道员工按 channel 分别对账(①②组逐渠道逐日须一致)。

判定标准:①组(消息计数列 ↔ msg_daily_stats friend 聚合)与 ②组(串类列 ↔ fact 行数)必须逐日一致;③组(宽表串类 cust_threads ↔ 日切 stat_daily_staff.cust_threads)允许差异,差值=跨夜串归属效应。

王云青

渠道 日期 ① mds sent/recv ① 宽表 sent/recv ① 判定 ② fact 行 ② 宽表 answered ② 判定 ③ 宽表 threads ③ 日切 threads ③ 差值
wechat 20260601 186 / 177 186 / 177 ✅ 76 76 ✅ 76 121 45
wechat 20260801 0 / 38 0 / 38 ✅ 0 0 ✅ 0 16 16
wechat 20260815 0 / 23 0 / 23 ✅ 0 0 ✅ 0 17 17
whatsapp 20260601 (无行) 0 / 0 ✅ (无行) 0 ✅ 0 (无行) —
whatsapp 20260801 0 / 1 0 / 1 ✅ 1 1 ✅ 1 1 0
whatsapp 20260815 0 / 4 0 / 4 ✅ 1 1 ✅ 1 2 1

田瑞志

渠道 日期 ① mds sent/recv ① 宽表 sent/recv ① 判定 ② fact 行 ② 宽表 answered ② 判定 ③ 宽表 threads ③ 日切 threads ③ 差值
wechat 三日期均 (无行) 0 / 0 ✅ (无行) 0 ✅ 0 (无行) —
whatsapp 20260601 1 / 1 1 / 1 ✅ 1 1 ✅ 1 1 0
whatsapp 20260801 (无行) 0 / 0 ✅ (无行) 0 ✅ 0 2 2
whatsapp 20260815 (无行) 0 / 0 ✅ (无行) 0 ✅ 0 (无行) —

注:「无行」= 源表该 (渠道, 日) 无该员工数据,宽表记 0/0——一致。20260601 无 WA 数据未换日期:田瑞志 whatsapp 该日恰好有 1 条收发(对账有效),王云青 whatsapp 全期仅 369 条消息、无行属真实分布。

结论:①②组 12/12 逐日全部一致(含渠道拆分);③组宽表全部 ≤ 日切值。

3. 跨夜串差值观察(③组差异解释)

  • 归属语义:宽表串按「串末条客户消息所在上海自然日」归属(first_reply_facts.compute_conversation);日切表按「当日有客户消息的 peer 数」计。串末条必在归属日当天 ⇒ 结构上保证 ③组逐日 宽表 ≤ 日切,差值=归属到其他日终了的跨夜/多日串。
  • 差值形态:王云青 wechat 三日差 45/16/17——该员工深夜消息多(20260801/15 发送 0、仅收 38/23 条,客户消息跨零点后串归属顺延);whatsapp 差 0/1。田瑞志 whatsapp 20260801 差 2(2 个 peer 当日来消息但串末条在次日)。
  • 串未丢失验证:王云青 wechat 全期 宽表 12,055 threads(11,930 answered,314 天)vs 日切 20,981(14,647 answered,312 天)——串模型把多日串折叠为 1(比值 57.4%),且抽查 20260814~31 逐日,日切 17 线程的 20260815 对应串终落在真实结束日(0819=8、0821=10、0828=9),无凭空丢失。
  • 另:宽表 cust_threads ≥ threads_answered(如王云青 wechat 20260602 = 54/52)——未闭合尾串计入分母不产 fact 样本,与算法注释一致。

4. 批次A验收清单(Task 8 收尾核对)

# 项 状态 依据
1 6 张表本地存在,migration 幂等 + downgrade 干净 ✅ Task 2(迁移 b7c8d9e0f1a2,alembic head 实测一致;表实测在库:first_reply_fact / employee_metric_daily / employee_call_daily / employee_score_config / employee_score_snapshot / management_followup)
2 first-reply-facts 全量回填成功且复跑幂等 ✅ Task 7(本文 §1)
3 employee-metrics 全量回填成功且复跑幂等 ✅ Task 7(本文 §1)
4 跨夜串判别测试通过 ✅ Task 3(test_first_reply_facts.py 恰 12 例,Task 8 复跑全绿;含跨夜/多日串/同毫秒并列)
5 friend-only 口径判别测试通过 ✅ Task 6(test_all_friend_pairs_only_friend + 宽表 friend 过滤判别例)
6 同源判别测试通过 + 全库 SUM 验证 ✅ Task 3/6;全库 68,476 ≡ 68,476(本文 §1 复验)
7 幽灵员工过滤测试通过 ✅ Task 5/6;孤儿=0(本文 §1 复验)
8 对账抽查 ①②一致、③差异可解释 ✅ 本文 §2/§3
9 全量测试套件绿 ✅ 653 passed in 62s(Task 8 本机实跑,2026-09-09)
10 核实文档完整 ✅ 本文补齐
11 写钩子零改动 ✅ 批次A全部 17 个 commits(27f91e5→0fce21a)逐一核查,无一触碰 storage/summary.py、storage/repos.py(repos.py 最近一次改动 d428953 属 customer_account_link 工作线,先于批次A)