Files
yunke/docs/superpowers/specs/2026-09-04-yunke-analytics-module-design.md
T

224 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 云客数据镜像 · 数据分析板块 v1 设计(A1 总览 + A3 流失预警 + B2 该回未回 + C1 敏感词)
- **日期**:2026-09-04
- **状态**:设计定稿(已经三轮评审:架构权衡 → 修订复审 → 打磨级优化),待实施计划
- **上游方案**:`2026-08-24-yunke-analytics-portfolio-design.md`(23 项全景,本 spec 是其首批的落地设计,范围经用户确认有调整)
- **依据**:所有性能论断基于生产实测(1Panel MySQL buffer pool 128MB、msg_daily_stats 读路径 0.0009s、ngram 全文检索 0.4~0.7s);所有数据规模基于 2026-08-24 生产审计 + 2026-09-03 迁移实测(message 861,088 行)
- **用户定调**:不能只实现当前四个页面,要为后续路线图打底;价值论证基准=我们自己的数据。
---
## 1. 范围与背景
### 1.1 v1 范围(用户确认)
| 项 | 内容 | 系列 |
|---|---|---|
| A1 | 经营总览看板(卡片+趋势+异常警示) | 管理看板 |
| A3 | 客户流失预警(三分法) | 管理看板 |
| B2 | 该回未回清单 | 员工工具(v1 仅管理者用) |
| C1 | 敏感词扫描(词库+扫描+处置) | 合规风控 |
| E0-lite | 数据截至角标 + 实体新鲜度(完整 E0 二批) | 基建 |
| E1扩展 | stat_daily_staff 员工日汇总(新基建,见 §3.1) | 基建 |
### 1.2 相对 8-24 全景方案的关键变化(2026-09-04 现状)
1. **E1 已落地一半**:msg_daily_stats(渠道×账号×会话×上海自然日,send/recv/last_ts/last_is_send)2026-09-03 上线,写钩子挂 `repos.upsert_messages` 尾部,读路径生产实测 0.0009s。本 spec 全部读需求骑在此表上。
2. **E0 的 sync_run 账本失效**:页面抓取路径不写 sync_run。E0-lite 改用「表新鲜度」判定(§3.5)。
3. **性能约束被钉死**:生产 MySQL buffer pool 128MB,任何裸扫 message 的 GROUP BY 实测 18.9s——**全板块禁止裸扫**,明细只做索引点查。
### 1.3 明确不做(YAGNI)
stat_daily_customer/group 物化表(msg_daily_stats 按 peer_kind 分组即等价)、完整 E0(环比突变告警的完整版、调度日志解析)、跨渠道客户去重、报表订阅(E2)、多角色权限(仅留 API 前缀落点)、今日环比(误导)、D 系列 LLM。
---
## 2. 架构(分层 + 打底)
```
┌─ 前端板块「数据分析」(侧栏第四组)
│ yunkes/src/views/analytics/ 四页 + composables/useChart.ts
├─ API 层 /api/analytics/*(前缀分组 = 未来角色权限中间件落点)
├─ 分析服务层 yunke-server/app/analytics/(★v1 核心地基)
│ metrics.py 指标口径字典 + 查询函数(纯查询、无 HTTP 依赖)
│ health.py E0-lite 数据截至/新鲜度
│ sensitive/ C1 子系统(words / scanner / hits,按「内容源」抽象)
├─ 汇总底座
│ msg_daily_stats 原子事实层:会话×日(已有,6.2万行)
│ stat_daily_staff 员工×日 rollup(★新增,~2万行)
└─ 明细层 message / customer / phone_call(只许小表直查 + 索引点查)
```
### 2.1 打底承诺(后续路线图落点)
| 后续功能 | 落点 | v1 已铺 |
|---|---|---|
| A2 员工绩效深化 | stat_daily_staff 直读 | 员工口径表 + 账号→员工映射权威落点 |
| A4 时段热力图 | 新增 msg_hour_stats(复制 rollup 维护模式) | 模式已验证可复制 |
| B1 客户360 / B5 RFM | analytics 层加函数 | A3 已建 peer 聚合查询模式 |
| E2 报表订阅 | 复用 metrics.py 渲染 HTML | 指标函数纯查询无 HTTP 依赖 |
| E3 BI 直连 | MySQL 只读账号 | 零代码 |
| D4 ASR→内容分析 | sensitive scanner 加内容源 | scanner 按「内容源」抽象(text→refer_msg→ASR) |
| 多角色权限 | /api/analytics/* 挂中间件 | 路由前缀分组已隔离 |
---
## 3. 组件设计
### 3.1 stat_daily_staff(员工日汇总)
- **表**:`(day INT, channel VARCHAR(16), user_id VARCHAR(64))` 主键;`msg_sent, msg_recv, active_friends, active_rooms INT`(**活跃对话拆两列**——客户数与群运营数是两个绩效维度);`updated_at`。量级 ~2 万行。
- **派生**:`msg_daily_stats` 按 `owner_account_id → user_id` 映射聚合(微信查 wechat_account.user_id,WA 查 whatsapp_account)。
- **刷新(★不挂写钩子)**:4h 调度 job 刷当日 + 滚动 3 天窗(容常见迟到);夜间全量自愈(full_sync_all.sh 追加一步)。它是 rollup-of-rollup,滞后语义与看板"数据截至"角标天然一致,不值得付写路径代价。
- **空表回退**:表空/未回填时自动改为实时聚合 msg_daily_stats(同为毫秒级)——部署即用,不等回填。沿用 sessions 已验证的 has_rows 回退模式。
- **映射兜底**:映射不上的账号按 owner_account_id 原样展示(标"未映射"),夜间自愈随账号表更新纠正。
### 3.2 A1 经营总览(DashboardView)
- **卡片**(今日/本周/本月三档,channel tab:微信/WhatsApp):消息量、活跃客户、活跃员工、通话量、首次沟通客户数。**环比只显示在周/月卡片**(今日 vs 昨日全天会误导)。
- **异常警示条**(页顶,E0 核心能力提前):昨日消息总量 vs 前 7 日均值偏离 >50% → 黄条警示 + 点开按员工的偏离明细(某员工归零=掉同步或离职)。阈值 50% v1 固定。
- **趋势**:近 30 天折线(ECharts),指标可切(消息量/活跃客户/通话量)。
- **角标**:「截至今日 HH:mm」(读 health 端点)——镜像 ≤4h 滞后透明化。
- **通话卡片**:phone_call(3k 行小表)按 start_call_time 落窗直查,不进汇总表。
### 3.3 A3 流失预警(ChurnView)
- **三分法**(tab):
- **真流失**:历史消息 ≥5 条(默认,可调)且 last_chat_time 距今 >30/60/90 天分层;
- **从未激活**:last_chat_time IS NULL;
- **待交接**:归属员工已离职(is_on_job=0,**R1 风险**:数据源待 plan 核实,不可用则该 tab 降级隐藏)。
- 主体读 customer 表(12k 行直查:last_chat_time/user_id/delete_flag/is_block 全有);历史消息量与活跃天数从 msg_daily_stats 按 peer 聚合。全部剔除 delete_flag=1。
- **列**:客户/渠道/归属员工/最后沟通/历史消息量/历史活跃天数。
- **操作**:行点击跳员工聊天窗(复用 staff-chat);按员工分组 CSV 导出(复用 csvExport 惯例)。
### 3.4 B2 该回未回(UnrepliedView)
- **候选集**(msg_daily_stats 62k 行窗口查):每 (channel, owner, peer_kind=friend, peer) 取最新日行,`last_is_send=false` 且 `last_ts` 距今 > N 小时(默认 2h,可切 1/2/4/24)。
- **决赛圈处理**(仅分页后当页行,每页 ~20 行点查,毫秒级):
- message 点查(走 (channel, owner, timestamp) 索引)校验尾部消息**有效类型**(text/image/voice/video/file/link,排除 unknown/divider)+ 取预览文本;
- phone_call 点查通话豁免:`max(最后消息 ts, 该客户最后通话 ts)` 作为尾部时间——通话也算"已回应"(user_id 经账号映射 + call_number 匹配 customer.phone)。
- 剔除 delete_flag=1 / is_block=1。
- **列**:客户/员工/渠道/最后消息时间/等待时长/预览;行点击跳聊天窗。
- 工作时间感知放 v2。
### 3.5 E0-lite 数据健康(health.py)
- `GET /api/analytics/health` → `{data_as_of, entities: [{table, last_seen}]}`。
- data_as_of = MAX(msg_daily_stats.last_ts);实体新鲜度 = 各表 MAX(updated_at)/MAX(fetched_at)(message 走新索引)。据此可推断 4h job 是否活着(当日有无新行)。
- 完整 E0(调度日志解析、成功率账本)二批。
### 3.6 C1 敏感词子系统(app/analytics/sensitive/)
**表**:
- `sensitive_word(id PK, word UNIQUE, category, enabled, min_len DEFAULT 2, exclude_words JSON数组, created_at, updated_at)`
- `sensitive_hit(id PK, word_id FK, msg_id, channel, owner_account_id, talker_id, room_id /*★冗余跳转字段*/, ts, snippet, disposition DEFAULT 'pending', created_at; UNIQUE(word_id, msg_id) /*★防重扫膨胀*/; INDEX(disposition, ts))`
- `sensitive_scan_state(entity PK, last_fetched_at, last_run_at, scanned, hits)`
**扫描器**:
- **水位 = MAX(message.fetched_at)**(★非 timestamp——增量补扫拉回的历史消息 timestamp 早于水位但 fetched_at 是新的,水位按 timestamp 会永久漏扫)。message.fetched_at 需补索引(86 万行建索引十余秒,避开抓取窗口)。
- 增量:4h job(挂现有 asyncio.Lock),`fetched_at > 水位` 分批 1000 行;ngram MATCH 为主,**单字词 LIKE 回退**(ngram 最小 2 字,词库 UI 提示)。
- 首次全量回填:CLI 手动(65 万文本 × 词库,分批限速,分钟级),夜间窗口跑。
- v1 范围仅 message.text;scanner 按内容源抽象,预留 refer_msg / ASR 文本接入位。
**词库与处置 UX**:
- **加词预览**:保存前 COUNT 命中量,>500 警示收窄(加排除词/换长词)——防"词库一加就淹没"。
- **批量处置**:命中列表多选 + 批量置为 confirmed/false_positive/ignored;列表默认只显示 pending+confirmed。
- 词删除 = 级联删除其命中(UI 二次确认);禁用不删命中(保留历史证据)。
- 命中行点击跳聊天窗上下文(复用 staff-chat ?msgid= 定位)。
- **日志不打词库内容与命中正文**(词库是高敏数据,防 log 泄露面)。
- 内置起步词库(私单/飞单/加私人微信/红包转账/辱骂/离职倾向),上线后用户自补行业词。
---
## 4. API 设计(全部挂现有鉴权中间件)
| 端点 | 方法 | 说明 |
|---|---|---|
| `/api/analytics/overview?channel=&scope=today\|week\|month` | GET | 卡片+趋势+异常警示一次返回(单往返) |
| `/api/analytics/health` | GET | 数据截至 + 实体新鲜度 |
| `/api/analytics/churn?channel=&type=real\|inactive\|handover&tier=30\|60\|90&min_msgs=&page=` | GET | 流失名单(min_msgs=真流失历史消息阈值,默认 5,服务端兜底钳制 1~100) |
| `/api/analytics/unreplied?channel=&hours=&page=` | GET | 该回未回清单 |
| `/api/analytics/sensitive/words` | GET/POST | 词库列表/新增(新增带预览计数) |
| `/api/analytics/sensitive/words/{id}` | PATCH/DELETE | 改(enabled 等)/删(级联删命中) |
| `/api/analytics/sensitive/words/preview` | POST | 加词前命中量预览 |
| `/api/analytics/sensitive/hits?disposition=&word_id=&page=` | GET | 命中列表 |
| `/api/analytics/sensitive/hits` | PATCH | 单条/批量处置(body 带 ids 数组) |
响应沿用现有信封格式;路由文件归 analytics 前缀组(权限中间件落点)。
---
## 5. 前端设计
- **侧栏新组「数据分析」**(现三组:微信管理/WhatsApp管理/通用):数据看板 / 流失预警 / 该回未回 / 敏感词。路由 `/analytics/*` 懒加载(不拖累现有 bundle)。
- **useChart.ts composable**:echarts **按需引入**(echarts/core + 所用图表/组件,gzip ~100KB);统一注册、主题、ResizeObserver 自适应、卸载 dispose。后续所有图表页复用。
- **页面模板惯例**:筛选栏(channel tab + 参数选择)→ 卡片行 → 图表区 → 表格区;空态分语(有窗滤空提示 / 真无数据);seq 守卫防竞态(项目惯例)。
- 四页均带「数据截至 HH:mm」角标。
---
## 6. 数据库迁移与调度
- **alembic 一支迁移**:4 新表(stat_daily_staff / sensitive_word / sensitive_hit / sensitive_scan_state)+ message.fetched_at 索引。**本条取代上游方案 §4.2「create_all 建表」的过时说法**——以仓库近期实践(msg_daily_stats 迁移 a7d3e9f1c2b4、ngram 索引迁移 b41e8fa0c7d2)为准,一律走 alembic。
- **调度**:
- 4h job(挂现有调度器,asyncio.Lock 互斥):stat_daily_staff 刷当日+滚动 3 天 → C1 增量扫描。
- 夜间 full_sync_all.sh 追加一步:stat_daily_staff 全量重建(自愈)。
- 首次部署 CLI:staff-daily-stats 回填 + sensitive 全量回填(复用 daily-stats CLI 模式)。
- **服务器升级**:照 DEPLOY.md——pull/bundle → `up -d --build`(迁移自动)→ 首次回填 CLI → **restart frontend**(防 nginx DNS 缓存 502)。
---
## 7. 测试策略
- **方言防御(O4,踩过坑的红线)**:所有新 SQL 禁 bare column in GROUP BY(MySQL only_full_group_by 1055 坑)、禁 SQL 日期函数(时区坑)、日期一律 day 整数比较(Python 侧上海自然日)。测试跑内存 SQLite,写法双方言兼容。
- metrics 函数全覆盖单测;B2 判定矩阵(尾部方向×类型过滤×通话豁免×delete_flag 剔除)。
- **判别法测试**(9-03 先例):stat_daily_staff 回填后删光 message 明细,员工汇总仍出完整数据。
- scanner:水位幂等(重复扫描不产生重复命中,UNIQUE 兜底)、**迟到数据测试**(fetched_at 晚于水位入库的历史消息被扫到)。
- 空表回退路径测试(stat_daily_staff 空 → 实时聚合 fallback)。
---
## 8. 口径速查表(争议仲裁权威文本;analytics 层 docstring 同步维护)
| 指标 | 定义 |
|---|---|
| 消息量 | send+recv 计数,**含 unknown 类型**(13.7%),不含 timestamp NULL;角标 tooltip 注明 |
| 活跃客户 | distinct peer_id(peer_kind=friend,即 **1:1 对话客户**,群成员活动不计入),按渠道,跨员工去重 |
| 活跃员工 | distinct user_id(经账号→员工映射;未映射账号按账号计并标注) |
| 通话量 | phone_call 按 start_call_time 落窗,含全部 call-type |
| 首次沟通客户数 | 该 peer_id 按渠道在本库**全历史首条消息所在日=统计日**,跨员工归一,不跨渠道去重 |
| 真流失 | 历史消息 ≥5(默认可调)且 last_chat_time 距今 >30/60/90 天,剔除 delete_flag=1 |
| 从未激活 | last_chat_time IS NULL |
| 待交接 | 归属员工 is_on_job=0(依赖 R1) |
| 该回未回 | 1:1 对话尾部消息 is_send=false 且属有效类型,且 max(最后消息, 最后通话) 距今 > N 小时(默认 2h),剔除 delete_flag/is_block |
| 等待时长 | now − max(最后消息 ts, 最后通话 ts) |
| 环比 | 仅周/月卡片(本周 vs 上周、本月 vs 上月);**周起始=周一**(中国业务惯例,Python 侧计算) |
| 日汇总 day | Asia/Shanghai 自然日 yyyymmdd,纯 Python 计算 |
| 异常警示 | 昨日消息总量 vs 前 7 日均值偏离 >50%,明细到员工级 |
---
## 9. 风险与遗留
| # | 风险 | 处置 |
|---|---|---|
| R1 | is_on_job(员工在职状态)数据源未定位(可能在账号表 raw_json 或 staff 实体) | plan 阶段核实;不可用则 A3"待交接"tab 降级隐藏,不阻塞其余 |
| R2 | sensitive_hit 首次回填后行数量级未知(取决于词库命中密度) | 分批 + 唯一约束已防膨胀;回填后实测存储量级再评估 |
| — | 员工监控合规(A1 异常警示/C1 属监控类) | 上线前公司内部明示告知(管理动作,非代码) |
---
## 10. 实施批次预估(供 plan 参考)
| 批 | 内容 | 预估 |
|---|---|---|
| 1 | stat_daily_staff(表+刷新+回退)+ analytics 层骨架 + health 端点 | ~2 天 |
| 2 | A1 总览(含异常警示条)+ useChart + ECharts 接入 | ~2 天 |
| 3 | A3 流失 + B2 该回未回 | ~2 天 |
| 4 | C1 敏感词全链 + 首次回填 + 服务器部署 | ~2 天 |
总计 ~8 个工作日(估),测试随批写。