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

22 KiB
Raw Permalink Blame History

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