docs: initial design spec, data dictionary, implementation plan
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
# 云客数据本地化系统 实现计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 持续抓取云客 CRM(微信+WhatsApp+电话+短信)数据到本地 MySQL + 对象存储,提供自有查看后台与增量同步。
|
||||
|
||||
**Architecture:** `yunke-server/`(Python/FastAPI)含会话层、抓取层(每渠道一个 scraper,混合 HTTP 重放 + 浏览器兜底)、存储层(MySQL 分表 + 对象存储哈希去重)、调度层(水位增量同步)、API 层;`yunkes/`(Vue3+TS)查看后台。先做 WeChat 端到端骨架,再横向扩展渠道与纵向补全实体。
|
||||
|
||||
**Tech Stack:** Python 3.11, FastAPI, SQLAlchemy 2.0 + Alembic, MySQL(asyncmy)/Redis, httpx, Playwright, APScheduler, Pydantic v2, pytest(+asyncio/respx) | Vue3 + Vite + TS + Pinia + Vue Router + Element Plus + axios
|
||||
|
||||
**配套文档:** [设计文档](../specs/2026-08-01-yunke-data-mirror-design.md)、[数据字典](../specs/2026-08-01-yunke-data-dictionary.md)
|
||||
|
||||
**说明:** 项目非 git 仓库;每个任务结尾的"commit"步骤在初始化 git 后执行(Phase 0 之后)。
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
yunke-server/
|
||||
pyproject.toml .env.example alembic.ini README.md
|
||||
app/
|
||||
config.py # 环境配置(pydantic-settings)
|
||||
db/base.py # engine/session/Base
|
||||
db/models.py # 全部 ORM 模型
|
||||
storage/object_store.py # 对象存储抽象(本地磁盘/可换 MinIO)
|
||||
session/login.py # getSecureKey -> pcLogin
|
||||
session/yunke_client.py # 带鉴权头的 HTTP 客户端 + 自动续期
|
||||
scraper/base.py # 分页/游标/限速 基类
|
||||
scraper/parsers.py # 原始 type 码 -> 统一枚举
|
||||
scraper/wechat.py # 微信 scraper
|
||||
scraper/whatsapp.py # WhatsApp scraper
|
||||
scraper/call.py # 电话+短信 scraper
|
||||
scraper/media.py # 媒体下载器(每渠道策略)
|
||||
sync/watermarks.py # sync_state 读写
|
||||
sync/orchestrator.py # 全量+增量编排
|
||||
sync/scheduler.py # 定时任务
|
||||
api/main.py # FastAPI app
|
||||
api/deps.py
|
||||
api/routers/{customer,group,message,call,sms,media,search,sync}.py
|
||||
api/schemas.py # 响应模型
|
||||
migrations/ # alembic
|
||||
tests/ # 镜像结构, +fixtures/cassettes(录制的真实响应)
|
||||
yunkes/ # Vue3 前端(见 M5)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 0:侦察收尾(实现期首批,前置依赖)
|
||||
|
||||
用浏览器抓包方法(见数据字典 Phase 0)确认以下,结果写入 `docs/superpowers/specs/phase0-findings.md`:
|
||||
- [ ] 聊天媒体:微信 `file` URL 形态;WhatsApp `mediaName` 下载接口(OSS 桶已确认 `yunke-pcfile`)
|
||||
- [ ] 媒体类型码:微信/WhatsApp image/voice/video/file/transfer/redpacket/system 的 `type` 枚举
|
||||
- [ ] 语音通话字段:`callRecord/queryWeChatVoiceList`、WhatsApp `wavoicecalllist`
|
||||
- [ ] WhatsApp 联系人 `businessFriendList` 字段
|
||||
- [ ] 登录加密:`getSecureKey`/`pcLogin` 请求响应
|
||||
- [ ] 群成员昵称:`memberIds` 的 wxid 是否需另调接口取群昵称
|
||||
- [ ] 限速阈值观察
|
||||
- [ ] 初始化 git:`cd d:\refactor\yunke && git init`(此后所有 commit 步骤生效)
|
||||
|
||||
> Phase 0 产出 `phase0-findings.md` 后,scraper 任务据此填实精确字段;架构不依赖它。
|
||||
|
||||
---
|
||||
|
||||
## Milestone 1:地基 + 微信端到端骨架(Walking Skeleton)
|
||||
|
||||
**目标:** 登录 → 抓微信客户列表 + 单聊消息 → 存 MySQL → FastAPI 暴露 → 跑通。证明整条链路。
|
||||
|
||||
### Task 1.1:项目脚手架与依赖
|
||||
|
||||
**Files:** Create `yunke-server/pyproject.toml`, `yunke-server/.env.example`, `yunke-server/app/__init__.py`, `yunke-server/app/config.py`
|
||||
|
||||
- [ ] **Step 1:** 写 `pyproject.toml`(依赖:fastapi, uvicorn, sqlalchemy[asyncio], asyncmy, alembic, redis, httpx, playwright, apscheduler, pydantic, pydantic-settings, cryptography; dev: pytest, pytest-asyncio, respx, ruff)
|
||||
- [ ] **Step 2:** 写 `.env.example`(`YUNKE_BASE_URL`, `YUNKE_USERNAME`, `YUNKE_PASSWORD`, `DB_URL=mysql+asyncmy://...`, `REDIS_URL`, `MEDIA_DIR`, `ENCRYPTION_KEY`)
|
||||
- [ ] **Step 3:** 写 `app/config.py`(pydantic-settings `Settings`,从环境读取,单例 `get_settings()`)
|
||||
- [ ] **Step 4:** `pip install -e ".[dev]"` 并 `playwright install chromium`
|
||||
- [ ] **Step 5:** commit `chore: scaffold yunke-server`
|
||||
|
||||
### Task 1.2:数据库与配置(TDD)
|
||||
|
||||
**Files:** Create `app/db/base.py`, `tests/conftest.py`, `tests/test_config.py`
|
||||
|
||||
- [ ] **Step 1: 写失败测试** `tests/test_config.py`:断言 `get_settings().db_url` 能从环境读取(用 `monkeypatch.setenv`)
|
||||
- [ ] **Step 2: 跑测试确认失败** `pytest tests/test_config.py -v` → FAIL
|
||||
- [ ] **Step 3: 实现** `app/config.py` 的 Settings;`app/db/base.py`:`async_engine`、`async_session_maker`、`Base = DeclarativeBase`
|
||||
- [ ] **Step 4: 跑测试确认通过**
|
||||
- [ ] **Step 5:** commit
|
||||
|
||||
### Task 1.3:核心 ORM 模型
|
||||
|
||||
**Files:** Create `app/db/models.py`
|
||||
|
||||
实现(按数据字典):`Department`、`Staff`、`WechatAccount`、`WhatsappAccount`、`Customer`、`ChatGroup`、`GroupMember`、`Message`(按月分表命名规范预留)、`Media`、`PhoneCall`、`Sms`、`SyncState`、`YkAccount`、`SyncRun`。每表带 `channel`、`version`、`raw_json(JSON)`、`fetched_at`、`updated_at`。Message 主键 `msg_id`,联合索引 `(channel, owner_account_id, talker_id, timestamp)`。
|
||||
|
||||
- [ ] **Step 1: 写测试** `tests/test_models.py`:建内存 SQLite,插入/查询一条 Customer,断言字段映射与 `raw_json` 往返(测试用 SQLite,生产 MySQL)
|
||||
- [ ] **Step 2: 跑确认失败**
|
||||
- [ ] **Step 3: 实现 models.py**
|
||||
- [ ] **Step 4: 跑确认通过**
|
||||
- [ ] **Step 5:** `alembic init migrations`,生成首版迁移并 `alembic upgrade head`(对 MySQL);commit `feat: core ORM models`
|
||||
|
||||
### Task 1.4:会话层——登录与鉴权客户端(TDD + 录制响应)
|
||||
|
||||
**Files:** Create `app/session/login.py`, `app/session/yunke_client.py`, `tests/cassettes/login.json`, `tests/test_yunke_client.py`
|
||||
|
||||
- [ ] **Step 1:** 把 Phase 0 抓到的登录请求/响应存为 `tests/cassettes/login.json`
|
||||
- [ ] **Step 2: 写失败测试** `test_yunke_client.py`:用 respx mock `/usercenter/login/pcLogin` 返回 cassette,断言 `YunkeClient` 登录后 `userid`/`company` 头被注入;再 mock 一个数据接口,断言请求头含鉴权
|
||||
- [ ] **Step 3: 跑确认失败**
|
||||
- [ ] **Step 4: 实现** `login.py`(`getSecureKey`→`pcLogin`,按 Phase 0 加密密码;返回 cookie+userid+company);`yunke_client.py`(httpx.AsyncClient 封装,自动注入鉴权头,`post()`/`get()`,侦测 401→重新登录续期)
|
||||
- [ ] **Step 5: 跑确认通过**
|
||||
- [ ] **Step 6:** commit `feat: session login + authed client`
|
||||
|
||||
### Task 1.5:微信客户列表 scraper(TDD + 录制响应)
|
||||
|
||||
**Files:** Create `app/scraper/base.py`, `app/scraper/wechat.py`, `tests/cassettes/wxFriends_page1.json`, `tests/test_scrape_wechat_friends.py`
|
||||
|
||||
- [ ] **Step 1:** 存录制响应 `wxFriends_page1.json`(来自 `friend_list/es`)
|
||||
- [ ] **Step 2: 写失败测试**:respx mock 分页(2 页),断言 `scrape_wechat_friends(client)` 返回 N 条归一化 dict(含 friend_wx_id/real_name/phone/version),分页正确
|
||||
- [ ] **Step 3: 跑确认失败**
|
||||
- [ ] **Step 4: 实现** `base.py`(页码分页 helper、时间游标 helper、限速);`wechat.py` 的 `scrape_wechat_friends`(调 `/wechatweb/wxFriends/friend_list/es`,翻页,字段映射)
|
||||
- [ ] **Step 5: 跑确认通过**
|
||||
- [ ] **Step 6:** commit `feat: wechat friend scraper`
|
||||
|
||||
### Task 1.6:微信消息 scraper(TDD + 录制响应)
|
||||
|
||||
**Files:** Modify `app/scraper/wechat.py`; Create `tests/cassettes/msg_chats_*.json`, `tests/test_scrape_wechat_messages.py`
|
||||
|
||||
- [ ] **Step 1:** 存录制响应(含文字 + 一条带媒体占位 + 日期符 `-2`)
|
||||
- [ ] **Step 2: 写失败测试**:断言 `scrape_wechat_messages(client, wechat_id, friend_id, start, end)` 返回消息,`type=-2` 被过滤,字段映射(msgSvrId→msg_id、type 经 parsers 归一化、roomid 保留)
|
||||
- [ ] **Step 3: 跑确认失败**
|
||||
- [ ] **Step 4: 实现** `parsers.py`(`normalize_wechat_type(raw)->Enum`);`wechat.py` 的消息抓取(`/wechatweb/msg/chats`,时间游标 `end`/`hasLast` 向过去翻页)
|
||||
- [ ] **Step 5: 跑确认通过**
|
||||
- [ ] **Step 6:** commit `feat: wechat message scraper`
|
||||
|
||||
### Task 1.7:存储层——幂等 upsert(TDD)
|
||||
|
||||
**Files:** Create `app/storage/repos.py`, `tests/test_repos.py`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**:插入同一 customer 两次(不同 version),断言只 1 行且 version 更新;插入重复 media(同 hash) 两次,断言 1 行
|
||||
- [ ] **Step 2: 跑确认失败**
|
||||
- [ ] **Step 3: 实现** `repos.py`:`upsert_customer`、`upsert_messages`(按主键 `ON DUPLICATE KEY`/merge)、`ObjectStore.put_by_hash`(本地磁盘按 hash 存,exists 判重);实现**消息分表路由器** `message_table(channel, timestamp)` 选表(`message_<channel>_YYYYMM`),并预留跨分表查询 helper(按时间/会话扇出多表合并)
|
||||
- [ ] **Step 4: 跑确认通过**
|
||||
- [ ] **Step 5:** commit `feat: idempotent upsert repos + object store`
|
||||
|
||||
### Task 1.8:FastAPI 最小 API(TDD)
|
||||
|
||||
**Files:** Create `app/api/main.py`, `app/api/routers/customer.py`, `app/api/schemas.py`, `tests/test_api_customer.py`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**:`GET /api/customers?channel=wechat&page=1` 返回分页列表(seed 测试数据)
|
||||
- [ ] **Step 2: 跑确认失败**
|
||||
- [ ] **Step 3: 实现** FastAPI app、customer router(分页、筛选 channel/部门)、schemas
|
||||
- [ ] **Step 4: 跑确认通过**;`uvicorn app.api.main:app` 手测
|
||||
- [ ] **Step 5:** commit `feat: customer list API`
|
||||
|
||||
### Task 1.9:端到端冒烟脚本
|
||||
|
||||
**Files:** Create `scripts/smoke_wechat.py`
|
||||
|
||||
- [ ] **Step 1:** 写脚本:登录 → 抓 1 个微信号的好友(限 1 页) + 该号 1 个会话最近消息 → upsert → 打印统计。用真实账号小范围跑(只读)
|
||||
- [ ] **Step 2:** 跑通,确认数据落库
|
||||
- [ ] **Step 3:** commit `chore: wechat end-to-end smoke`;**Milestone 1 完成可演示**
|
||||
|
||||
---
|
||||
|
||||
## Milestone 2:微信全量补全
|
||||
|
||||
每个实体同 M1 模式(录制响应→TDD→实现→commit):
|
||||
|
||||
- [ ] **Task 2.0 部门树**:`scrape_departments`(`POST /usercenter/dept/tree`)→ upsert Department(含 `parent_id`/`path` 层级)。这是组织树的唯一来源,必须最先抓(Task 5.2 依赖它)
|
||||
- [ ] **Task 2.1 微信员工+账号**:`scrape_wechat_accounts`(`wxUser/list`),顺带 upsert Staff。**Staff 的 dept/phone 来源**:`wxUser/list` 仅给 `userId`/`userName`,无 dept;dept 关系从该账号下客户的 `deptRoute`/`userId` 反推(无客户账号的员工暂无 dept)。若需员工完整字段(手机/角色/离职)→ Phase 0 确认 `/pc/crm_t_portaluser/...` 后补一个 staff 详情 scraper
|
||||
- [ ] **Task 2.2 微信群+成员**:`scrape_wechat_groups`(`chatroom/getChatroomList`),解析 `memberIds` → 批量 upsert ChatGroup + GroupMember
|
||||
- [ ] **Task 2.3 群消息**:扩展消息 scraper 支持 `friendId=chatRoomId`(群聊),`talker` 为群内成员
|
||||
- [ ] **Task 2.4 微信语音通话**:`scrape_wechat_voice_calls`(`callRecord/queryWeChatVoiceList`,按 Phase 0 字段)
|
||||
- [ ] **Task 2.5 会话列表**:`scrape_wechat_talkers`(`talkerList`)用于发现哪些会话有消息
|
||||
- [ ] **Task 2.6 媒体下载(微信)**:`app/scraper/media.py` —— 抓到带 `file` 的消息即下载存对象存储(按 md5 去重);失败的入重试队列不阻塞。TDD:mock OSS 直链下载 + 去重
|
||||
- [ ] **Task 2.7 水位增量**:`sync/watermarks.py` + `sync/orchestrator.py`。**消息**按时间水位(每会话 `last_timestamp`,查 `[水位, now]`);**非消息实体**(客户/群/员工/电话/短信)按 `version` 水位推进(查 `version > 水位`)。推进水位、断点续抓。TDD:模拟两轮增量,断言只取新增
|
||||
- [ ] **Task 2.8 媒体读取 API**:`api/routers/media.py` —— 从对象存储流式返回媒体(Range 请求、Content-Type、inline/attachment)。与 JSON 列表模式不同,需独立实现。TDD:seed 一个 media 记录 + 本地文件,`GET /api/media/{hash}` 断言 200 + Content-Type + Range 支持
|
||||
- [ ] **Task 2.9 后端鉴权**:加一个简单共享 token 中间件(与 Task 5.10 前端配对);本期定位本地/内网工具,可默认信任网络边界,但显式写明信任模型,避免实现者猜测
|
||||
|
||||
---
|
||||
|
||||
## Milestone 3:WhatsApp 渠道
|
||||
|
||||
(与微信同模式;复用 base/storage/sync/api,加 `channel='whatsapp'`)
|
||||
|
||||
- [ ] **Task 3.1 WhatsApp 登录**:复用 YunkeClient(同会话),确认 `/qqweb/WhatsAppUser/` 也用同样鉴权头
|
||||
- [ ] **Task 3.2 员工+账号**:`businessSalesList`(42)
|
||||
- [ ] **Task 3.3 客户**:`businessFriendList`(按 Phase 0 字段)
|
||||
- [ ] **Task 3.4 消息**:`chatRecords`(keyId 主键、时间游标、`type` 归一化)
|
||||
- [ ] **Task 3.5 媒体下载(WhatsApp)**:按 Phase 0 找到的 `mediaName` 下载接口实现专属 downloader
|
||||
- [ ] **Task 3.6 语音通话**:`wavoicecalllist`(Phase 0)
|
||||
- [ ] **Task 3.7**:扩展 customer/message API 支持跨渠道 `channel` 过滤与聚合
|
||||
|
||||
---
|
||||
|
||||
## Milestone 4:电话 + 短信
|
||||
|
||||
- [ ] **Task 4.1 电话记录 scraper**:`/pc/callLog/list`(6047,含 `recordFile` OSS 录音 URL),归一化到 PhoneCall
|
||||
- [ ] **Task 4.2 录音下载**:复用对象存储,下载 `recordFile` mp3(直链 OSS),按内容哈希去重
|
||||
- [ ] **Task 4.3 短信 scraper**:`/pc/customer/smsLogsList`(53),归一化到 Sms
|
||||
- [ ] **Task 4.4 现场录音**:Phase 0 确认后补
|
||||
- [ ] **Task 4.5**:call/sms API + 时间范围增量同步
|
||||
|
||||
---
|
||||
|
||||
## Milestone 5:前端查看后台(yunkes)
|
||||
|
||||
**Files:** `yunkes/`(Vite 初始化 + Element Plus + Pinia + Router + axios)
|
||||
|
||||
- [ ] **Task 5.1 脚手架**:`npm create vite@latest yunkes -- --template vue-ts`;装 element-plus/pinia/vue-router/axios;配 axios 指向后端
|
||||
- [ ] **Task 5.2 布局**:左侧组织树(部门→人员→账号)+ 顶栏 + 主区(Element Plus 后台布局)
|
||||
- [ ] **Task 5.3 客户列表页**:分页表格 + 筛选 → 点昵称进时间轴
|
||||
- [ ] **Task 5.4 消息时间轴**:按时间正序渲染文字/语音(播放+转文字)/图片/视频/文件/转账红包;单聊+群聊(群显示发送成员)
|
||||
- [ ] **Task 5.5 群列表 + 群成员**
|
||||
- [ ] **Task 5.6 通话/短信页**
|
||||
- [ ] **Task 5.7 全文搜索**:跨渠道/日期/人员/客户/群(MVP 应用层扇出,见设计 §8)
|
||||
- [ ] **Task 5.8 媒体查看**:图片预览、语音/视频播放、文件下载
|
||||
- [ ] **Task 5.9 同步状态面板**:各实体上次同步时间/进度/错误
|
||||
- [ ] **Task 5.10 简单鉴权**:单一账号密码(本地工具)
|
||||
|
||||
---
|
||||
|
||||
## Milestone 6:调度与稳健化
|
||||
|
||||
- [ ] **Task 6.1 定时调度**:`sync/scheduler.py`(APScheduler + Redis):每日全实体增量 + 手动触发接口
|
||||
- [ ] **Task 6.2 重试与限速**:失败指数退避重试队列;请求限速器(避免风控)
|
||||
- [ ] **Task 6.3 登录态自动续期**:401 自动重新登录
|
||||
- [ ] **Task 6.4 监控日志**:SyncRun 记录每次运行统计与错误
|
||||
- [ ] **Task 6.5 数据脱敏/留底校验**:确认 raw_json 全留底
|
||||
- [ ] **Task 6.6 部署文档**:docker-compose(MySQL/Redis/app/yunkes)+ README
|
||||
|
||||
---
|
||||
|
||||
## 测试策略贯穿
|
||||
|
||||
- 所有 scraper 用 `tests/cassettes/` 录制响应 + respx,**不依赖线上**
|
||||
- storage/sync 用 SQLite 内存库做单测
|
||||
- API 用 FastAPI TestClient
|
||||
- 每个 Milestone 结束做一次真实账号小范围**只读冒烟**
|
||||
|
||||
## 风险锚点
|
||||
|
||||
- 媒体外链过期 → 抓到即下载(M2.6/M3.5/M4.2)
|
||||
- 云客改版 → scraper 集中封装 + raw_json 留底 + 浏览器兜底(base.py 预留 `BrowserFallback`)
|
||||
- 大规模 → 消息分表 + 增量水位 + 媒体哈希去重
|
||||
@@ -0,0 +1,154 @@
|
||||
# 云客 CRM 数据字典(全系统扫描结果)
|
||||
|
||||
- **日期**:2026-08-01
|
||||
- **来源**:登录云客 CRM(crm2.yunkecn.com)实地抓包,覆盖全部模块
|
||||
- **状态标记**:✅ 已确认字段 | ⏳ 接口已找到,字段待 Phase 0 | 🔭 仅知入口
|
||||
|
||||
> 本文档是[设计文档](2026-08-01-yunke-data-mirror-design.md)的配套数据字典,列出系统中**所有数据实体**及其接口、字段、规模、抓取优先级。
|
||||
|
||||
---
|
||||
|
||||
## 0. 鉴权与接口约定
|
||||
|
||||
- **鉴权**:自定义请求头 `userid` + `company` + `source:yunkepc` + `channel:4` + 会话 cookie;登录链 `getSecureKey → pcLogin`(账号密码,无验证码,可全自动)
|
||||
- **接口前缀**:
|
||||
- `/pc/...` — CRM 通用(客户/电话/短信/统计)
|
||||
- `/usercenter/...` — 用户/部门
|
||||
- `/wechatweb/...` — 微信
|
||||
- `/qqweb/WhatsAppUser/...` — WhatsApp
|
||||
- **通用响应**:`{success, code:"10000", message, data, traceId}`
|
||||
- **媒体存储**:OSS 桶 `yunke-pcfile.oss-cn-beijing.aliyuncs.com`(已由通话录音确认);头像在 `wx.qlogo.cn` / `mmhead.c2c.wechat.com` / `pc.oss.yunkecn.com`
|
||||
|
||||
---
|
||||
|
||||
## 1. 组织与人员
|
||||
|
||||
| 实体 | 接口 | 状态 | 关键字段 | 规模 |
|
||||
|---|---|---|---|---|
|
||||
| 部门/分组 department | `POST /usercenter/dept/tree` | ✅ | dept_id, parent_id, name, path | 6 个分组 |
|
||||
| 员工 staff | 嵌入账号列表(`userId`+`userName`) | ✅ | user_id, name, phone, sale_phone, dept_id, is_on_job | 与账号同源 |
|
||||
| 员工管理(完整) | `/pc/crm_t_portaluser/...` | 🔭 | 姓名/性别/手机/角色/离职 | 设置→部门管理 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 微信(prefix `/wechatweb/`)
|
||||
|
||||
| 实体 | 接口 | 状态 | 关键字段 | 规模 |
|
||||
|---|---|---|---|---|
|
||||
| 员工微信号+员工 `wxUser/list` | `POST /wechatweb/wxUser/list` | ✅ | wechatId, nickName, alias, phone, userId, userName(员工真名), contactsNum, groupNum, sendNum/receiveNum, isOnJob, dbSize, 在线状态, lastUpdate | **55** |
|
||||
| 客户/好友 customer | `POST /wechatweb/wxFriends/friend_list/es` | ✅ | friendWxId, wxId(归属), friendWxNickname/Remark, realName, phone/wechatPhone(完整), gender, region, description, saleName, userId, deptRoute/deptName, friendHeadImageUrl/headImg, lastChatTime, sendNum/receiveNum/messageCount, version, deleteFlag/isBlock/friendState | **8152** |
|
||||
| 会话列表 talker | `POST /wechatweb/wxFriends/talkerList` | ✅ | friendWechatId, nickName, remarkName, messageCount, lastChatTime, headUrl, memberCount, analyseResult | 按员工 |
|
||||
| **消息** message | `POST /wechatweb/msg/chats` | ✅ | msgSvrId(主键), type/messageType, text, **file/fileTh/fileHd(媒体URL)**, **md5/md5Th/md5Hd**, duration, **transcriptionText(语音转文字)**, imgPath/originalImage/picType, paymsgid, referMsg, talker/oriTalker, wechatId, **roomid(群,空=单聊)**, mine/isSend, timestamp/createTime, version, isDel, name/headUrl | 百万级 |
|
||||
| ↳ 参数 | — | ✅ | `wechatId`+`friendId`+`types`+`start/end`(ms 时间范围);游标 `end`/`hasLast` | — |
|
||||
| **群 chatroom** | `POST /wechatweb/chatroom/getChatroomList` | ✅ | chatRoomId(`@chatroom`), chatRoomName, memberCount, **memberIds(分号分隔全部成员wxid)**, headImageUrl, wechatId/wechatNickName(归属), userName/userPhone(群主员工), messageCount/ownMessageCount, lossMembers/addMembers, lastChatTime/updateTime | **1515** |
|
||||
| 语音通话 voice_call | `POST /wechatweb/callRecord/queryWeChatVoiceList` | ⏳ | 接口已确认,字段 Phase 0(含录音) | 待测 |
|
||||
| 通话统计 | `POST /wechatweb/callRecord/todayStatistics` | 🔭 | 统计聚合 | — |
|
||||
|
||||
**消息类型码(微信)**:`1`=文字、`-2`=日期分隔符(非真实消息);图片/语音/视频/文件/转账/红包/引用/系统 各有码(Phase 0 枚举)。
|
||||
|
||||
---
|
||||
|
||||
## 3. WhatsApp(prefix `/qqweb/WhatsAppUser/`)
|
||||
|
||||
| 实体 | 接口 | 状态 | 关键字段 | 规模 |
|
||||
|---|---|---|---|---|
|
||||
| 员工+账号 `businessSalesList` | `POST /qqweb/WhatsAppUser/businessSalesList` | ✅ | userId, userName(员工真名), uid(`手机号@s.whatsapp.net`), nickName, phone/salePhone/salesBindPhone, contactsNum, groupNum, talkerCount, friendAddNum, isOnJob, lastUpdate | **42** |
|
||||
| 客户/联系人 friend | `POST /qqweb/WhatsAppUser/businessFriendList` | ⏳ | 接口已确认,字段 Phase 0 | 待测 |
|
||||
| 客户/群信息 | `POST /qqweb/WhatsAppUser/getFriendOrTroopInfo` | ⏳ | 单个客户/群详情 | — |
|
||||
| **消息** message | `POST /qqweb/WhatsAppUser/chatRecords` | ✅ | keyId(主键), type/msgType, content, **mediaName(媒体文件名)**, nickName, uid(员工), friendUid(客户), isSend, createTime/dateTime, timezone, companyCode, quotedId/quotedMessage | 百万级 |
|
||||
| ↳ 参数 | — | ✅ | `start/end` 时间游标;`hasNext`/`hasLast` | — |
|
||||
| 会话统计 | `getBusinessChatOverView` / `getBusinessChatDailyStatistics` / `getBusinessChatDetailList` | 🔭 | 概览/日统计/明细 | — |
|
||||
| 语音通话 voice_call | 页面 `waBusiness/wavoicecalllist` | 🔭 | Phase 0 | — |
|
||||
|
||||
**消息类型码(WhatsApp)**:`0`=文字、`1`=图片/媒体、`-2`=日期符。
|
||||
**ID 格式**:`手机号@s.whatsapp.net`。
|
||||
**媒体差异**:仅给 `mediaName`(文件名),需专门下载接口(Phase 0)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 电话与短信(客户→沟通记录,prefix `/pc/`)
|
||||
|
||||
| 实体 | 接口 | 状态 | 关键字段 | 规模 |
|
||||
|---|---|---|---|---|
|
||||
| **电话记录** phone_call | `POST /pc/callLog/list` | ✅ | id, startCallTime, callDuration/callSeconds, ringTime/ringSecond, callStatus(0未接/2接通), incomingCall(0呼出/1呼入), userId/userIdName(员工), departmentId, callNumber, simPhone, simNumber, imei, **recordFile(录音OSS URL)**, isUpload, downloadCount, aiAsr, qcStatus, version, planCustomerName | **6047** |
|
||||
| 录音下载 | `/pc/customer/downloadMp3/1`、`/downloadMp3/2` | ✅ | MP3 下载(客户/通话两类) | — |
|
||||
| 通话导出 | `/pc/callLog/exportXls` | 🔭 | Excel 导出 | — |
|
||||
| **短信记录** sms | `POST /pc/customer/smsLogsList` | ✅ | id, customerPhone, **sms(全文)**/smsSub(预览), sender, direction(0收/1发), userId, senderDepartment/senderDepartmentId, receiveName, createTime, simPhone/simNumber, source | **53** |
|
||||
| 现场录音 sales_recording | `/cms/customer/salesRecording` | 🔭 | 工牌/手机设备录音 | 待测 |
|
||||
|
||||
**录音 URL 形态**(关键,媒体下载用):
|
||||
`https://yunke-pcfile.oss-cn-beijing.aliyuncs.com/callFile/{companyCode}/{userId}/YYYY/MMDD/{callNumber}_{timestamp}.mp3`
|
||||
|
||||
---
|
||||
|
||||
## 5. CRM 客户模块(prefix `/pc/newcustomer/`,独立于微信好友)
|
||||
|
||||
| 实体 | 接口 | 状态 | 备注 |
|
||||
|---|---|---|---|
|
||||
| CRM 客户 | `POST /pc/newcustomer/customers` | ✅(空) | **本账号 total=0**——该公司未用 CRM 客户,客户全在微信/WhatsApp 好友里 |
|
||||
| 客户分组/公海/统计/导出 | `/pc/newcustomer/...` | 🔭 | 接口存在,数据为空 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 其他模块数据(来自全系统权限地图 `GET /pc/authority/permissions`)
|
||||
|
||||
> 权限接口一次返回所有模块/子页/操作 URL,已据此盘点。下列多为**统计/管理/配置**类,非沟通主数据;标注是否建议抓取。
|
||||
|
||||
### 6.1 微信风控 / 设置(沟通模块下)
|
||||
- 微信财务统计、敏感操作统计、敏感词统计/设置、微信登录日志、**好友操作日志**(`/cms/wechat/friendDeletionLog`,含导出)、重复好友、微信验证消息、微信号解绑、聊天标签设置
|
||||
- **建议抓**:好友操作日志(好友增删变更,便于追溯)
|
||||
|
||||
### 6.2 首页统计
|
||||
- 数据统计/统计图表/通话/微信/短信/工作量统计/手机统计/打卡统计/自定义报表(均带导出)
|
||||
- 多为聚合统计,可后期按需抓
|
||||
|
||||
### 6.3 AI 分析(`/pc/ai/...`)
|
||||
- AI 客户洞察(客户分析/异议/问题/痛点/情绪/沟通关键词)、AI 员工赋能(优秀话术/竞品/战败)、AI 智能质检(敏感词质检/员工情绪/飞单预警/沟通质检)
|
||||
- **派生数据**(由聊天/通话经 AI 生成),可后期抓;消息里也有 `analyseResult`/`transcriptionText` 字段
|
||||
|
||||
### 6.4 微小助 / 营销(`/pc/wxTask/...`、`/cms/wechatAssistantModules/...`)
|
||||
- 微信营销任务(发圈/群发好友/群发群/批量加好友/视频号/客户唤醒)、企微任务、抖音/小红书营销、辅助营销(文章库/营销素材/快捷回复/话术库/微信活码)、拓客
|
||||
- 多为**任务/素材配置**,非客户沟通数据;按需
|
||||
|
||||
### 6.5 设备(`/cms/device/...`)
|
||||
- 设备统计、**手机截屏**(`/cms/device/phonescreenshot`)、**手机相册**(`/cms/settings/device/albumList`)、插拔卡统计、设备定位/地理围栏、设备管控
|
||||
- **含媒体数据**(截屏/相册图片),如需要可抓;默认不在沟通范围
|
||||
|
||||
### 6.6 设置(`/pc/crm_t_portaluser/...`、`/cms/settings/...`)
|
||||
- 部门管理/已离职用户/角色权限、自定义客户字段/标签、业务属性、系统设置(操作日志等)、聊天工作台
|
||||
- 配置类,按需
|
||||
|
||||
---
|
||||
|
||||
## 7. 规模总览(实测)
|
||||
|
||||
| 数据 | 规模 |
|
||||
|---|---|
|
||||
| 微信员工账号 | 55 |
|
||||
| WhatsApp 员工账号 | 42 |
|
||||
| 微信客户/好友 | 8152 |
|
||||
| 微信群 | 1515(含成员列表) |
|
||||
| 电话记录 | 6047 |
|
||||
| 短信记录 | 53 |
|
||||
| 消息量 | 日均 ~2.7 万条(长期百万级 + 多媒体 TB 级) |
|
||||
| CRM 客户 | 0(未使用) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 抓取优先级建议
|
||||
|
||||
- **P0 核心(必抓)**:员工/账号(微信+WA)、客户/好友(微信+WA)、群+群成员、消息(单聊+群聊)、电话记录+录音、短信、多媒体(图片/语音/视频/文件/语音通话)
|
||||
- **P1 建议**:语音通话(微信+WA)、WhatsApp 联系人明细、好友操作日志、现场录音、会话统计
|
||||
- **P2 后期/按需**:AI 分析结果、营销素材/话术、设备截屏/相册、各类统计报表、CRM 客户(空)
|
||||
|
||||
---
|
||||
|
||||
## 9. Phase 0 待确认(实现期首批,浏览器抓包)
|
||||
|
||||
1. **聊天媒体下载**:微信 `file/fileTh/fileHd` 的 URL 形态与鉴权;WhatsApp `mediaName` 的下载接口(OSS 桶已知为 `yunke-pcfile`)
|
||||
2. **媒体类型码**:微信/WhatsApp 各渠道 image/voice/video/file/transfer/redpacket/system 的 `type` 枚举
|
||||
3. **语音通话字段**:`callRecord/queryWeChatVoiceList`、WhatsApp `wavoicecalllist` 的响应结构 + 录音下载
|
||||
4. **WhatsApp 联系人**:`businessFriendList` 完整字段
|
||||
5. **登录自动化**:`getSecureKey`/`pcLogin` 请求响应(密码加密方式)
|
||||
6. **群成员昵称**:`memberIds` 给的是 wxid,群内显示名/头像是否需另调接口
|
||||
7. **限速/反爬阈值**:高频请求风控观察
|
||||
@@ -0,0 +1,326 @@
|
||||
# 云客数据本地化系统(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/轮询频率提升)
|
||||
Reference in New Issue
Block a user