docs(spec): 数据分析板块 v1 设计存档——A1/A3/B2/C1 骑日汇总层+打底架构
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,223 @@
|
|||||||
|
# 云客数据镜像 · 数据分析板块 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&page=` | GET | 流失名单 |
|
||||||
|
| `/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 索引。
|
||||||
|
- **调度**:
|
||||||
|
- 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 上月) |
|
||||||
|
| 日汇总 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 个工作日(估),测试随批写。
|
||||||
Reference in New Issue
Block a user