Files
yunke/docs/superpowers/specs/2026-08-01-yunke-data-mirror-design.md
T
2026-08-01 10:53:28 +08:00

327 lines
22 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.
# 云客数据本地化系统(yunke)设计文档
- **日期**:2026-08-01
- **状态**:草案(待评审 + 用户确认)
- **数据源**:云客 CRM(`https://crm2.yunkecn.com`),网页后台,无官方开放 API
- **仓库结构**:`yunke-server/`(后端:抓取 + 存储 + 调度 + API)、`yunkes/`(前端:查看后台)
- **配套数据字典**:[2026-08-01-yunke-data-dictionary.md](2026-08-01-yunke-data-dictionary.md)(全系统所有数据实体/接口/字段/规模的完整盘点)
---
## 1. 目标与范围
### 1.1 目标
把云客 CRM 后台的数据**持续抓取到本地**,提供**自有查看后台**随时查看,支持**增量同步**持续更新,后期可做**数据分析与报表**。
### 1.2 在范围内的数据
- **组织**:部门/公司分组树、人员(员工)
- **账号**:微信号(员工挂载)、WhatsApp 账号
- **客户**:微信好友 / WhatsApp 联系人(含完整字段)
- **聊天记录**:
- 渠道:**微信**、**WhatsApp**(外加短信、电话通话)
- 形态:**单聊** + **群聊**
- 消息类型:文字、语音、语音通话、图片、视频、文件、转账、红包、引用回复、系统消息等
- **多媒体**:以上所有语音/图片/视频/文件**真实文件全部下载到本地**
- **通话**:电话通话、语音通话(含录音)
### 1.3 渠道清单(已确认无其他)
微信、WhatsApp、短信、电话通话。
### 1.4 不在范围(本期)
- 双向同步/回写云客(**全程只读**,绝不写云客)
- 自动化营销/AI 应答(仅做数据沉淀与查看)
- 云客"设备/营销/AI分析"等非沟通类模块的数据(后期可选)
---
## 2. 侦察结论(基于真实接口抓取)
> 以下结论均通过浏览器实地登录云客后台、抓取真实网络请求得出,非推测。
### 2.1 鉴权机制
- 登录链路:`GET /newuc/login/getSecureKey` → `POST /usercenter/login/pcLogin`(账号密码登录,无验证码/短信/滑块,**可全自动**)
- 已登录后,数据接口通过**自定义请求头** + 会话 cookie 鉴权:
- `userid: <用户ID哈希>`(如 `BED5AE9E562C44C0AA920E9C8D139262`)
- `company: <公司码>`(如 `8m0atd`)
- `source: yunkepc`
- `channel: 4`
- `content-type: application/json`
- **结论**:接口极易重放——复用登录态并带上上述头即可。**方案 C(混合)的"快通道"完全可行**,无需全程开浏览器。
### 2.2 接口前缀(按模块)
| 模块 | 前缀 | 备注 |
|---|---|---|
| 通用(首页/统计/权限) | `/pc/...` | POST JSON |
| 用户/部门 | `/usercenter/...` | 部门树 `/usercenter/dept/tree` |
| 微信 | `/wechatweb/...` | 后端 Elasticsearch |
| WhatsApp | `/qqweb/WhatsAppUser/...` | 注意前缀与微信不同 |
### 2.3 已确认的关键接口与结构
#### 微信
| 用途 | 接口 | 关键参数/字段 |
|---|---|---|
| 客户/好友列表 | `POST /wechatweb/wxFriends/friend_list/es` | 返回 `total`、分页 `data[]`(实测 total=8152) |
| 会话列表 | `POST /wechatweb/wxFriends/talkerList` | 按员工微信拉会话,含 `messageCount`/`lastChatTime` |
| 消息列表 | `POST /wechatweb/msg/chats` | `wechatId`+`friendId`+`types`+`start/end`(毫秒时间范围);游标翻页 `end`/`hasLast` |
| **员工微信列表** | `POST /wechatweb/wxUser/list` | 实测 total=55;含 `wechatId`/`nickName`/`userId`/`userName`(员工真名)/`contactsNum`/`groupNum`/`isOnJob` |
| 语音通话列表 | `POST /wechatweb/callRecord/queryWeChatVoiceList` | 语音通话记录(字段待 Phase 0) |
| 通话统计 | `POST /wechatweb/callRecord/todayStatistics` | 通话统计 |
- **客户字段**(已完整确认):`friendWxId`、`wxId`(归属员工微信)、`friendWxNickname`/`friendWxRemark`、`realName`、`phone`/`wechatPhone`(完整)、`gender`、`region`、`description`、`saleName`、`userId`(归属员工)、`deptRoute`/`deptName`、`friendHeadImageUrl`/`headImg`(头像外链)、`lastChatTime`、`sendNum`/`receiveNum`/`messageCount`、`version`(更新戳)、`deleteFlag`/`isBlock`/`friendState` 等
- **消息字段**(已完整确认):
- `msgSvrId`(**消息唯一 ID,主键**)、`type`/`messageType`(`1`=文字、`-2`=日期分隔符、图片/语音/视频/文件/转账/红包各有码)、`text`、`file`/`fileTh`/`fileHd`(**媒体 URL**)、`md5`/`md5Th`/`md5Hd`(媒体哈希)、`duration`(语音/视频时长)、`transcriptionText`(**语音转文字**)、`imgPath`/`originalImage`/`picType`、`paymsgid`(转账/红包)、`referMsg`(引用回复)、`talker`/`oriTalker`(客户 wxid)、`wechatId`(员工微信)、`roomid`(群 ID,空=单聊)、`mine`/`isSend`(方向)、`timestamp`/`createTime`、`version`、`isDel`、`name`/`headUrl`
#### WhatsApp
| 用途 | 接口 | 关键参数/字段 |
|---|---|---|
| 客户/群信息 | `POST /qqweb/WhatsAppUser/getFriendOrTroopInfo` | — |
| **员工列表** | `POST /qqweb/WhatsAppUser/businessSalesList` | 实测 total=42;含 `userId`/`userName`(员工真名)/`uid`/`nickName`/`contactsNum`/`groupNum` |
| 好友列表 | `POST /qqweb/WhatsAppUser/businessFriendList` | WhatsApp 联系人列表 |
| 消息列表 | `POST /qqweb/WhatsAppUser/chatRecords` | `start/end` 时间游标;`hasNext`/`hasLast` |
| 会话统计 | `POST /qqweb/WhatsAppUser/getBusinessChatOverView` / `getBusinessChatDailyStatistics` / `getBusinessChatDetailList` | WhatsApp 会话概览/统计 |
- **消息字段**(已完整确认):`keyId`(**唯一 ID,主键**)、`type`/`msgType`(`0`=文字、`1`=图片/媒体、`-2`=日期符)、`content`、`mediaName`(**媒体文件名**,如 `{uid}_{keyId}.jpg`)、`nickName`、`uid`(员工 `手机号@s.whatsapp.net`)、`friendUid`(客户)、`isSend`、`createTime`/`dateTime`、`timezone`、`companyCode`、`quotedId`/`quotedMessage`(引用回复)
### 2.4 两渠道关键差异(影响设计)
| 维度 | 微信 | WhatsApp |
|---|---|---|
| 接口前缀 | `/wechatweb/` | `/qqweb/WhatsAppUser/` |
| ID 格式 | `wxid_*` | `手机号@s.whatsapp.net` |
| 消息类型码 | 文字=`1` | 文字=`0` |
| 消息唯一 ID | `msgSvrId` | `keyId` |
| **媒体获取** | 消息内带 `file`/`fileTh`/`fileHd` URL | 仅 `mediaName` 文件名,**需专门下载接口** |
### 2.5 规模(实测)
- 客户/好友:**8152** 条
- 微信号(员工挂载):**55** 个
- 消息量:6 天约 16 万条 → **日均约 2.7 万条** → 长期累积**百万级消息 + TB 级多媒体**
- **结论**:中大规模,必须可扩展(MySQL 分表/索引 + 对象存储去重 + 高效增量)
### 2.6 多媒体观察
- 头像/媒体多为**外链**(`wx.qlogo.cn`、`mmhead.c2c.wechat.com`、`yunke-pcfile.oss-cn-beijing.aliyuncs.com`)
- **风险**:外链可能过期/失效 → **必须抓取时趁会话有效立刻下载**,不能只存 URL
- WhatsApp 媒体在聊天视图**不内嵌 URL**(受保护/按需加载)→ 确切下载接口待 Phase 0
---
## 3. 总体架构
```
┌──────────────────────────────────────────────────────────────┐
│ yunke-server/ (Python:所有重活,一个代码库两个进程) │
│ │
│ ① 会话层 自动登录云客、拿登录态、侦测失效自动续期 │
│ ② 抓取层 混合引擎:HTTP 快通道(重放) + 浏览器兜底(方案C) │
│ └─ 按渠道:微信 scraper / WhatsApp scraper / 短信 / 通话 │
│ └─ 按对象:分组·人员·账号·客户·群·消息·媒体·通话 │
│ ③ 存储层 MySQL(结构化,带channel) + 对象存储(媒体,按哈希去重) │
│ ④ 调度层 定时/增量同步、任务队列、断点续抓、重试 │
│ ⑤ API 层 FastAPI:把存好的数据提供给前端 │
└──────────────────────────────────────────────────────────────┘
│ HTTP API
▼
┌──────────────────────────────────────────────────────────────┐
│ yunkes/ (Vue3 + TS:查看后台) │
│ 组织树下钻 · 客户/群列表 · 消息时间轴(跨渠道) · 全文搜索 · │
│ 媒体播放/下载 · 通话/短信 · 同步状态 · (后期)统计图表 │
└──────────────────────────────────────────────────────────────┘
```
### 两条数据流
- **抓取流**:调度层 → 会话层(拿登录态) → 抓取层(快通道/兜底) → 存储层(MySQL + 对象存储)
- **查看流**:浏览器(yunkes) → API 层(FastAPI) → MySQL + 对象存储 → 页面
### 关键设计原则
1. **各层职责单一、互不耦合**:抓取层只"拿数据",存储层只"存好",API 层只"对外提供"。
2. **抓取与查看彻底分离**:前端永远只读本地库,绝不直接碰云客。云客挂了/改版,本地数据照常查看。
3. **渠道维度贯穿**:所有数据带 `channel`,新增渠道只需加 scraper,不动主干。
4. **幂等写入**:消息按 `msgSvrId`/`keyId`、媒体按内容哈希去重,重复抓取只更新不重复。
5. **原始 JSON 留底**:每条记录存 `raw_json`,云客字段变了不丢信息,利于后期分析。
6. **全程只读**:绝不向云客写任何数据。
---
## 4. 数据模型
> 所有业务表带 `channel` 字段。**沟通类**表(customer/message/media/group):`wechat` / `whatsapp` / `sms`。**通话表**(call)的 `channel` 表示通话类型:`phone_call` / `voice_call`(通话不绑定聊天渠道)。每张表另含 `raw_json`(原始留底)、`version`(更新水位)、`fetched_at`(抓取时间)。
### 4.1 组织与账号
- **department(部门/分组)**:`dept_id` · `parent_id` · `name` · `path` · `version`
- 来源 `/usercenter/dept/tree`
- **staff(人员/员工)**:`user_id`(主键) · `name`(真实姓名) · `phone` · `sale_phone` · `dept_id` · `is_on_job` · `version`
- **数据来源**:员工信息嵌在账号列表里(微信 `wxUser/list` 的 `userId`/`userName`、WhatsApp `businessSalesList` 的 `userId`/`userName`),抓账号时顺带 upsert 员工。
- **wechat_account(员工微信号,共 55)**:`wx_id`(主键) · `owner_staff_id`(userId) · `nickname` · `alias` · `phone` · `head_image_url` · `contacts_num`(好友数) · `group_num`(群数) · `send_num`/`receive_num` · `is_on_job` · `db_size` · 在线状态 · `last_update` · `version`
- 来源 `POST /wechatweb/wxUser/list`(字段已确认)
- **whatsapp_account(员工 WhatsApp 账号,共 42)**:`wa_id`(主键, 手机号@s.whatsapp.net) · `owner_staff_id`(userId) · `nickname` · `phone` · `sale_phone` · `contacts_num` · `group_num` · `talker_count` · `friend_add_num` · `is_on_job` · `version`
- 来源 `POST /qqweb/WhatsAppUser/businessSalesList`(字段已确认)
### 4.2 客户与群
- **customer(客户/联系人)**:`channel` · `friend_id`(wxid / @s.whatsapp.net) · `owner_account_id` · `owner_staff_id` · `dept_id` · `nickname` · `remark` · `real_name` · `phone` · `gender` · `region` · `description` · `avatar_url` · `last_chat_time` · `send_num` · `receive_num` · `message_count` · `version` · 删除/拉黑/状态标记
- **chat_group(群聊)**:`channel` · `room_id` · `name` · `member_count` · `owner_account_id` · `announcement` · `version`
- **group_member(群成员)**:主键 `(channel, room_id, member_id)` · `display_name` · `role` · `join_time` · `version`
### 4.3 消息(核心)
- **message(聊天消息)**:按渠道/时间**分表**(如 `message_wechat_202608`)
- `msg_id`(归一主键:微信=`msgSvrId`、WhatsApp=`keyId`)· `channel`
- `owner_account_id`(员工微信/WhatsApp)· `talker_id`(客户/群成员)· `room_id`(群,空=单聊)
- `type_raw`(原始类型码)· `type`(**归一化枚举**:text/voice/voice_call/image/video/file/transfer/redpacket/system/quote/divider)
- `direction`(收/发)· `text`(文字/转录)· `media_hash`(关联媒体,可空)
- `duration` · `pay_msg_id` · `refer_msg`(JSON) · `transcription`(语音转文字)
- `timestamp` · `version` · `is_del` · `raw_json`
- 索引:主键 `msg_id`;联合 `(owner_account_id, talker_id, timestamp)`;`(channel, timestamp)`
### 4.4 媒体
- **media(多媒体文件)**:`media_hash`(主键,内容哈希去重) · `channel` · `type`(image/voice/video/file) · `source_url`/`source_name` · `local_path` · `size` · `mime` · `duration` · `width`/`height` · `fetched_at` · `status`(ok/failed/pending)
- 文件存对象存储,按哈希命名,同文件只存一份
### 4.5 通话与短信
- **call(通话)**:`channel`(取值 `phone_call`=电话通话 / `voice_call`=语音通话)· `call_id` · `owner_staff_id` · `customer_phone` · `type` · `direction` · `duration` · `start_time` · `result` · `recording_media_hash` · `version`
- **语音通话的存储归属(明确)**:通话的**完整记录**(时长/结果/录音)一律存 `call` 表(来自"通话""语音通话列表"模块)。若某次语音通话也以内联事件出现在聊天流里,`message` 表那条记录用 `type=voice_call` + `call_id` **引用** `call` 表,不重复存元数据。Phase 0 确认语音通话是否出现在消息流后据此实现。
- **sms(短信)**:`sms_id` · `owner_staff_id` · `customer_phone` · `direction` · `content` · `timestamp` · `version`
### 4.6 系统表
- **sync_state(同步水位)**:`entity_type` · `channel` · `scope`(部门/账号) · `last_version`/`last_timestamp` · `last_synced_at` · `status` · `last_error`
- **yk_account(云客登录账号)**:`username` · `password_encrypted`(加密存储) · `userid` · `company` · `session_token` · `session_expires`
- 密码用对称加密(如 Fernet/AES-GCM)加密入库,密钥放在**环境变量 / 配置**中,不入库、不进代码仓库。
- **sync_run(同步运行日志)**:`run_id` · `entity_type` · `scope` · `started_at` · `finished_at` · `fetched` · `failed` · `status` · `error`
---
## 5. 存储策略
- **MySQL(结构化)**:所有业务表。消息表按 `渠道 + 年月` 分表(百万级不能单表)。关键字段建索引(见 4.3)。字符集 utf8mb4(俄语等多语言 + emoji)。
- **对象存储(二进制媒体)**:
- 默认**本地磁盘目录**(按 `channel/type/哈希前两位/哈希` 组织)
- 封装 `ObjectStore` 接口(put/get/exists),后期可无缝换 MinIO/S3/OSS
- 按**内容哈希**去重,同图/同语音只存一份
- **Redis**:任务队列 + 会话缓存 + 限速计数 + 去重布隆(可选)
---
## 6. 抓取层(方案 C:混合)
### 6.1 会话层(SessionManager)
- 自动登录(账号密码 → `getSecureKey` → `pcLogin`),拿到 cookie + `userid`/`company` 等头
- 持久化会话,**侦测 401/失效自动重新登录续期**
- 对外提供"带鉴权的 HTTP 客户端",所有 scraper 复用
### 6.2 抓取引擎(混合)
- **默认快通道**:用会话层的 HTTP 客户端**直接重放**云客接口(POST + 鉴权头),解析 JSON。快、可并发。
- **浏览器兜底**:遇到动态签名/反爬/无法重放的接口,回退 Playwright 驱动真实浏览器取数。
- 每个**渠道**一个 scraper(`wechat_scraper` / `whatsapp_scraper` / `sms_scraper` / `call_scraper`),实现统一的 `fetch_X` 接口。
### 6.3 分页与全量首扫
- 消息接口是**时间游标向前翻页**(`end`/`hasLast` 或 `start`/`hasNext`)
- **全量首扫**:从当前向过去翻页,直到 `hasLast=false` 翻完历史
- 列表接口是**页码分页**(`pageNum`/`pageSize`),逐页拉满
### 6.4 媒体下载(每渠道独立)
- **微信**:消息含 `file`/`fileTh`/`fileHd` URL → 用会话下载,按 `md5` 去重存储
- **WhatsApp**:消息含 `mediaName` → 调专门下载接口(Phase 0 确认)→ 按哈希去重
- **抓到即下载**(外链会过期),下载失败入重试队列,不阻塞消息抓取
- 语音转文字(`transcriptionText`)一并入库,便于搜索
---
## 7. 增量同步策略
1. 每个 `(entity, channel, scope)` 在 `sync_state` 维护一个**时间/版本水位**。
2. 同步时查询 `[水位, 现在]` 的时间段(消息)或 `version > 水位`(客户/群),按主键幂等 upsert。
3. 推进水位到本批最大 `timestamp`/`version`。
4. **调度**:
- 每日定时:全实体增量
- 手动触发:单实体/单账号/单会话按需刷新
- 失败:指数退避重试,断点续抓(水位持久化,挂了重跑不重复)
5. **限速**:控制请求频率,避免触发云客风控/封号。
---
## 8. 查看后台(yunkes)
- **左侧组织树**:公司/分组 → 人员 → 微信/WhatsApp 账号(与云客一致下钻)
- **客户列表**:搜索/筛选/分页 → 点客户进**聊天时间轴**(单聊)
- **群聊列表** → 点群进**群聊时间轴 + 群成员**
- **消息时间轴**:按时间正序;按类型渲染(文字/语音播放+转文字/图片/视频/文件/转账红包);**跨渠道**聚合同一客户
- **全文搜索**:关键词 / 日期 / 人员 / 客户 / 群
- **跨分表搜索策略**:消息按 `渠道+年月` 分表,搜索需跨分表。MVP 用应用层**扇出查询**(对各分表发查询 + 合并排序,利用 `(owner_account_id, talker_id, timestamp)` 索引收敛);规模增大后引入 **Elasticsearch** 建搜索索引(云客后端本身用 ES,思路一致)。实现期在计划阶段定稿。
- **鉴权**:本期定位为**本地/内网工具**,采用简单鉴权(单一账号 + 密码 / 内网可信即可),不做复杂多租户权限。FastAPI 暴露按实体的查询/搜索/媒体读取接口(列表/详情/时间轴/搜索/媒体流)。
- **通话/语音通话/短信**:各自列表 + 详情
- **多媒体查看**:在线播放语音、看图/视频、下载文件
- **同步状态面板**:各实体上次同步时间、进度、错误
- **(后期)数据统计**:消息量、活跃度、趋势图表
---
## 9. 错误处理与稳健性
- 抓取**幂等**(主键/哈希去重)、**断点续抓**(水位持久化)
- 失败**重试队列**(指数退避),媒体下载失败不阻塞消息
- 登录态**自动侦测 + 续期**
- **限速**防封;异常监控与告警
- 每条**原始 JSON 留底**,字段变更不丢信息
- **全程只读**,绝不写云客
---
## 10. 测试策略
- **抓取层**:用录制的真实接口响应做**固定数据测试**(不依赖线上)
- **存储层**:幂等/去重/分表单元测试
- **同步层**:水位推进、增量正确性、断点续抓测试
- **API 层**:接口契约测试
- **端到端**:小范围真实账号冒烟测试(只读)
---
## 11. 技术栈
| 层 | 选型 | 理由 |
|---|---|---|
| 抓取 + 后端 | Python(httpx + Playwright + FastAPI) | 抓取生态最强;抓取与服务同语言 |
| 元数据库 | MySQL | 用户指定;可扩展(分表/索引) |
| 多媒体存储 | 本地磁盘 / 对象存储(MinIO 兼容) | 二进制不入库;按哈希去重 |
| 缓存/队列 | Redis + 任务队列(RQ/Dramatiq) | 可靠抓取任务 + 增量调度 |
| 前端 | Vue 3 + TypeScript | 查看后台 |
---
## 12. Phase 0 侦察任务(实现期首批,用浏览器抓包完成)
> 架构不依赖以下项;它们是 scraper 实现前的"接口确认"清单。用本次验证过的浏览器抓包方法逐一确认。
1. **媒体下载机制(最高优先)**
- ✅ OSS 桶已确认:`yunke-pcfile.oss-cn-beijing.aliyuncs.com`(由通话录音 `recordFile` 得知,路径 `/callFile/{company}/{userId}/YYYY/MMDD/{号}_{ts}.mp3`)
- 微信:抓一条含 `file` 非空的消息(图片/语音/文件),确认 `file`/`fileTh`/`fileHd` 是否也是该 OSS 直链
- WhatsApp:`mediaName` → 文件 的下载接口(确认是否同 OSS 桶)
2. **精确消息类型码**:枚举微信/WhatsApp 各渠道下 image/voice/video/file/transfer/redpacket/system 的 `type` 码
3. ~~**群聊**~~ ✅ 已确认:`/wechatweb/chatroom/getChatroomList`(1515 群),**成员 wxid 列表嵌在 `memberIds` 字段**(分号分隔);群消息用 `msg/chats` 传 `chatRoomId`;待确认:成员群昵称/头像是否需另调接口
4. **语音通话**:接口已确认 `/wechatweb/callRecord/queryWeChatVoiceList`;待确认其字段与录音下载方式
5. **电话通话 / 短信**:`通话` / `短信` 模块接口与字段
6. **登录自动化**:`getSecureKey`/`pcLogin` 的请求/响应(密码是否 RSA 加密、所需参数),确认全自动登录可行
7. ~~**人员/账号列表**~~ ✅ 已确认:微信 `wxUser/list`(55)、WhatsApp `businessSalesList`(42),员工信息(`userId`/`userName`)嵌在账号列表里
8. **WhatsApp 客户列表**:`businessFriendList`(接口已确认)/ `getFriendOrTroopInfo` 的完整响应字段
9. **限速/反爬阈值**:观察高频请求是否触发风控,确定安全速率
---
## 13. 风险
| 风险 | 应对 |
|---|---|
| 云客改版导致接口失效 | 原始 JSON 留底;scraper 集中封装,便于改;浏览器兜底 |
| 媒体外链过期 | 抓到即下载,不只存 URL |
| 登录态过期 | 会话层自动侦测 + 续期 |
| 触发风控/封号 | 限速 + 模拟正常节奏;只读不写降低风险 |
| 大规模性能 | 消息分表 + 对象存储去重 + 增量同步 |
| 多渠道字段差异 | 归一化枚举 + raw_json 留底 |
---
## 14. 后期扩展(本期不做)
- 数据分析与 BI 报表(消息量、活跃度、客户画像、销售漏斗)
- 跨渠道客户统一视图(同人微信+WhatsApp 合并)
- AI 分析(云客已有 `analyseResult` 字段,可对接)
- 增量实时化(Webhook/轮询频率提升)