From d1b636abdcfa550585de3bd513c9204dd3b746b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Fri, 4 Sep 2026 13:45:13 +0800 Subject: [PATCH] =?UTF-8?q?docs(spec):=20=E6=95=B0=E6=8D=AE=E5=88=86?= =?UTF-8?q?=E6=9E=90=E6=9D=BF=E5=9D=97=20v1=20=E8=AE=BE=E8=AE=A1=E5=AD=98?= =?UTF-8?q?=E6=A1=A3=E2=80=94=E2=80=94A1/A3/B2/C1=20=E9=AA=91=E6=97=A5?= =?UTF-8?q?=E6=B1=87=E6=80=BB=E5=B1=82+=E6=89=93=E5=BA=95=E6=9E=B6?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- ...026-09-04-yunke-analytics-module-design.md | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-04-yunke-analytics-module-design.md diff --git a/docs/superpowers/specs/2026-09-04-yunke-analytics-module-design.md b/docs/superpowers/specs/2026-09-04-yunke-analytics-module-design.md new file mode 100644 index 0000000..9068e72 --- /dev/null +++ b/docs/superpowers/specs/2026-09-04-yunke-analytics-module-design.md @@ -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 个工作日(估),测试随批写。