cd8d36fd00
Co-Authored-By: Claude <noreply@anthropic.com>
22 KiB
22 KiB
云客数据本地化系统(yunke)设计文档
- 日期:2026-08-01
- 状态:草案(待评审 + 用户确认)
- 数据源:云客 CRM(
https://crm2.yunkecn.com),网页后台,无官方开放 API - 仓库结构:
yunke-server/(后端:抓取 + 存储 + 调度 + API)、yunkes/(前端:查看后台) - 配套数据字典: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: yunkepcchannel: 4content-type: application/json
- 结论:接口极易重放——复用登录态并带上上述头即可。方案 C(混合)的"快通道"完全可行,无需全程开浏览器。
2.2 接口前缀(按模块)
| 模块 | 前缀 | 备注 |
|---|---|---|
| 通用(首页/统计/权限) | /pc/... |
POST JSON |
| 用户/部门 | /usercenter/... |
部门树 /usercenter/dept/tree |
| 微信 | /wechatweb/... |
后端 Elasticsearch |
/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
| 用途 | 接口 | 关键参数/字段 |
|---|---|---|
| 客户/群信息 | 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 两渠道关键差异(影响设计)
| 维度 | 微信 | |
|---|---|---|
| 接口前缀 | /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 + 对象存储 → 页面
关键设计原则
- 各层职责单一、互不耦合:抓取层只"拿数据",存储层只"存好",API 层只"对外提供"。
- 抓取与查看彻底分离:前端永远只读本地库,绝不直接碰云客。云客挂了/改版,本地数据照常查看。
- 渠道维度贯穿:所有数据带
channel,新增渠道只需加 scraper,不动主干。 - 幂等写入:消息按
msgSvrId/keyId、媒体按内容哈希去重,重复抓取只更新不重复。 - 原始 JSON 留底:每条记录存
raw_json,云客字段变了不丢信息,利于后期分析。 - 全程只读:绝不向云客写任何数据。
4. 数据模型
所有业务表带
channel字段。沟通类表(customer/message/media/group):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、WhatsAppbusinessSalesList的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)·channelowner_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/fileHdURL → 用会话下载,按md5去重存储 - WhatsApp:消息含
mediaName→ 调专门下载接口(Phase 0 确认)→ 按哈希去重 - 抓到即下载(外链会过期),下载失败入重试队列,不阻塞消息抓取
- 语音转文字(
transcriptionText)一并入库,便于搜索
7. 增量同步策略
- 每个
(entity, channel, scope)在sync_state维护一个时间/版本水位。 - 同步时查询
[水位, 现在]的时间段(消息)或version > 水位(客户/群),按主键幂等 upsert。 - 推进水位到本批最大
timestamp/version。 - 调度:
- 每日定时:全实体增量
- 手动触发:单实体/单账号/单会话按需刷新
- 失败:指数退避重试,断点续抓(水位持久化,挂了重跑不重复)
- 限速:控制请求频率,避免触发云客风控/封号。
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 实现前的"接口确认"清单。用本次验证过的浏览器抓包方法逐一确认。
- 媒体下载机制(最高优先)
- ✅ OSS 桶已确认:
yunke-pcfile.oss-cn-beijing.aliyuncs.com(由通话录音recordFile得知,路径/callFile/{company}/{userId}/YYYY/MMDD/{号}_{ts}.mp3) - 微信:抓一条含
file非空的消息(图片/语音/文件),确认file/fileTh/fileHd是否也是该 OSS 直链 - WhatsApp:
mediaName→ 文件 的下载接口(确认是否同 OSS 桶)
- ✅ OSS 桶已确认:
- 精确消息类型码:枚举微信/WhatsApp 各渠道下 image/voice/video/file/transfer/redpacket/system 的
type码 群聊✅ 已确认:/wechatweb/chatroom/getChatroomList(1515 群),成员 wxid 列表嵌在memberIds字段(分号分隔);群消息用msg/chats传chatRoomId;待确认:成员群昵称/头像是否需另调接口- 语音通话:接口已确认
/wechatweb/callRecord/queryWeChatVoiceList;待确认其字段与录音下载方式 - 电话通话 / 短信:
通话/短信模块接口与字段 - 登录自动化:
getSecureKey/pcLogin的请求/响应(密码是否 RSA 加密、所需参数),确认全自动登录可行 人员/账号列表✅ 已确认:微信wxUser/list(55)、WhatsAppbusinessSalesList(42),员工信息(userId/userName)嵌在账号列表里- WhatsApp 客户列表:
businessFriendList(接口已确认)/getFriendOrTroopInfo的完整响应字段 - 限速/反爬阈值:观察高频请求是否触发风控,确定安全速率
13. 风险
| 风险 | 应对 |
|---|---|
| 云客改版导致接口失效 | 原始 JSON 留底;scraper 集中封装,便于改;浏览器兜底 |
| 媒体外链过期 | 抓到即下载,不只存 URL |
| 登录态过期 | 会话层自动侦测 + 续期 |
| 触发风控/封号 | 限速 + 模拟正常节奏;只读不写降低风险 |
| 大规模性能 | 消息分表 + 对象存储去重 + 增量同步 |
| 多渠道字段差异 | 归一化枚举 + raw_json 留底 |
14. 后期扩展(本期不做)
- 数据分析与 BI 报表(消息量、活跃度、客户画像、销售漏斗)
- 跨渠道客户统一视图(同人微信+WhatsApp 合并)
- AI 分析(云客已有
analyseResult字段,可对接) - 增量实时化(Webhook/轮询频率提升)