# 云客数据本地化系统(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/轮询频率提升)