Files
yunke/docs/superpowers/specs/2026-08-25-yunke-staff-chat-window-design.md
T

202 lines
16 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.
# 云客员工聊天窗口对齐设计(StaffChat 五视图)
- 日期:2026-08-25
- 状态:已批准(用户逐节确认)
- 范围:微信员工列表 → 员工信息列昵称点击进入的聊天窗口页,对齐云客全部 5 个视图(单聊/群聊/朋友圈/通讯录/统计分析),含功能与数据
## 1. 背景与目标
现有 `ChatTimeline.vue`(双栏:会话侧栏+消息流)与云客员工聊天窗口差距大。云客真机(2026-08-25 Playwright 实测,员工 `wxid_wo97u1u2x11w22`)该页面是**三栏五视图**结构:左侧员工资料卡+导航,中间会话列表,右侧内容区按导航切换五种视图。
目标:新建独立页面完整对齐(布局/交互/数据口径),并与云客保持一致。旧 `ChatTimeline` 保留给 WhatsApp 渠道,不动。
### 用户决策记录(2026-08-25 逐项确认)
| 决策点 | 结论 |
|---|---|
| 范围 | 全部 5 个视图(含用户未提的统计分析) |
| 渠道 | 只做微信版;WA 保持现有页面 |
| 删除聊天记录按钮 | **不复刻**(镜像数据删了只有重扫能找回) |
| 敏感词统计 | tab 占位空态(待 C1 敏感词功能接入,见 2026-08-24 数据分析方案) |
| 实现方案 | 方案 A:独立新页 `StaffChat.vue`,组件拆分,旧页不动 |
| 二次审计修订(同日) | 8 项全部修订:微信群表列改真机结构、统计明细按 tab 分型、语音通话复用 CallList 列、phone_call userId 转换写明、消息类型归一化+回填任务、通讯录批量操作、红包转账 msgid 定位、chat_group 补列(raw_json 提取)、左栏折叠 |
## 2. 云客真机事实(对齐依据)
页面 URL:`/cms/wechat/salewechat/message/wechat-window/{tab}/{wxId}/{friendId}`,tab 路径段 1=单聊 2=群聊;朋友圈/通讯录/统计分析是独立路由但共享左栏。
### 2.1 布局
```
┌─────────┬──────────────┬────────────────────────┐
│ 员工资料卡 │ 好友/群|聊天记录▼ │ 对话标题 │
│ 昵称 │ [搜索框] │ [近14天窗][关键词][类型▼]│
│ 真名·电话 │ ┌──────────┐ │ [导出聊天记录] │
│ 微信号 │ │聊天好友│已删除│ │ 消息流(加载更多+日期分隔) │
│─────────│ └──────────┘ │ │
│ 单聊/群聊/ │ 📌置顶区(可折叠)│ │
│ 朋友圈/ │ 会话卡列表 │ │
│ 通讯录/ │ (头像+今日N │ │
│ 统计分析 │ +相对时间) │ │
└─────────┴──────────────┴────────────────────────┘
```
### 2.2 接口事实(真机抓包)
| 视图 | 接口 | 关键参数/字段 |
|---|---|---|
| 员工资料卡 | `GET /wechatweb/wxFriends/getUserAndFriendInfo?wechatId=&friendId=` | `saleName` 昵称、`realName` 真名、`salePhone`、`alias`/`wechatId`、`HeadImageUrl` 头像 |
| 会话列表(单聊) | `POST /wechatweb/wxFriends/talkerList` form | `type=1, isDel=0|1, pageNumber, pageSize=20, wechatId`;行:`friendWechatId/nickName(脱敏)/remarkName/messageCount(今日)/headUrl/lastChatTime/showHead(-1=置顶)/memberCount`;`topCount` 置顶数、`hasMore` 翻页 |
| 会话列表(群聊) | 同上 | `type=2`(无 isDel);群名不脱敏 |
| 朋友圈 | `POST /wechatweb/wechatFriendster/getFriendsterInfo` form | `startTime/endTime(空), pageNum, pageSize=10, wechatId`;行:`id/snsId/friendsterName/friendsterWXIdImg/content/createTime(秒)/type(1图文/28纯文字)/praise[]{name,praiseImg}/comment[]/imgUrlsList[](OSS URL)/praiseNum`;头部 `saleNickName/headImageUrl/saleAlias/bgImg`;`hashNext` 翻页 |
| 通讯录 | `POST /wechatweb/wxFriends/friend_list/es` | 即逐员工客户列表(已抓过);表格列:客户微信(昵称+微信号)/数据来源/客户备注名/最近沟通时间/发送条数/接收条数/操作(查看聊天记录+导出HTML聊天记录) |
| 统计计数 | `POST /wechatweb/wechatcount/getSaleWechatIdInfoCount` | `{addFriend, addGroup, redAndTrans, sensitiveCount, delFriend, blackFriend, weChatVoiceCount}` |
| 统计明细 | `POST /wechatweb/wxFriends/getAddFriendOrGroup` form | `pageNum, pageSize, friendTypeYunke=1, wechatId, startdate, enddate`;**各 tab 明细列不同(真机逐 tab 验证)**:新客户=客户微信(可点)/客户备注/员工微信/时间;红包转账=操作类型/客户微信/员工微信/金额/发生时间/操作(查看聊天记录**带 msgid 定位消息**);新的群=微信群信息/员工微信/时间;删除客户=客户微信/员工微信/发生时间;语音通话=复选框/通话人/通话类型/来源/呼入呼出/接通状态/通话录音/通话时间/操作(9 列,与 CallList 同构) |
其他 UI 事实:
- 右列默认日期窗**近 14 天**(`今日00:00-14天 ~ 今日23:59:59`),统计视图默认**近 7 天**
- 类型下拉 10 类:文本/图片/语音消息/语音通话/视频通话/文件/小程序名片/链接/红包/转账
- 「好友/群|聊天记录」下拉:聊天记录模式=左侧按全量消息关键词搜索
- 置顶区含文件传输助手,可「折叠置顶聊天」
- 已删除好友 tab 存在(实测 3 条老好友,最近联系时间可显示到 2025 年)
- 资料卡 cursor=pointer 但点击无可见行为(不复刻点击)
- **通讯录·微信群 tab 列(真机验证)**:复选框/群信息/数据来源/群主信息(昵称+微信号)/群成员数量/群成员好友数量/消息数量/操作(查看聊天记录→群聊消息+导出HTML聊天记录);搜索框仅「请输入群名称」
- **通讯录批量操作**:客户表/群表都有全选复选框 + 顶部「查询/导出/导出聊天记录(勾选后可批量)」
- 左栏有「收起菜单」「收起侧边栏」折叠按钮
- 云客全站昵称脱敏(A**2),我们不脱敏(镜像惯例,有意差异)
## 3. 架构(方案 A)
```
路由: /staff-chat/:ownerId/:tab(single|group|moments|contacts|stats)/:talkerId?
默认 tab=single;talkerId 可空(账号级入口)
tab=group 时 talkerId 语义为 roomId;红包转账明细跳转带 ?msgid= 定位消息
StaffChat.vue (页面骨架)
├── StaffChatSide.vue 左栏: 员工资料卡 + 5 导航按钮 + 收起/展开(对齐云客「收起侧边栏」)
├── tab=single|group → SessionPane.vue (中列) + ChatMain.vue (右列)
├── tab=moments → MomentsPane.vue
├── tab=contacts → ContactsPane.vue
└── tab=stats → StatsPane.vue
```
- `SessionPane`/`ChatMain` 从 `ChatSidebar`/`ChatTimeline` 复制改造(非原地重构);`MessageBubble`、`buildMessageSections` 直接复用
- `ChatTimeline.vue`/`ChatSidebar.vue` 不动,WA 继续用。**当前未提交 WIP 直接还原(git checkout),不迁移**:`ChatTimeline.vue` 的 `sidebarProfile`/`ownerPhone`/`ownerAvatar` 三处、`api/index.ts` 的 `delete_flag` 参数——新页面自建,旧文件回到干净状态
- `talkerId` 为空(账号级入口,从员工列表昵称点入):右列显示空态「选择左侧会话查看消息」(与现有 ChatTimeline 行为一致),中列正常展示会话列表
- 入口改造:`StaffList.vue` 微信行昵称/主badge → `/staff-chat/{account_id}/single`;modal 内点客户微信带日期窗 query;`CustomerList.vue` 微信行「查看聊天记录」同步切新页
## 4. 数据层
### 4.1 新表 `moments_post`
| 列 | 类型 | 说明 |
|---|---|---|
| id | VARCHAR(64) PK | 云客 data.id |
| wechat_id | VARCHAR(128) idx | 归属员工 |
| sns_id | VARCHAR(64) | 云客 snsId |
| author_name / author_wxid / author_avatar | VARCHAR | 发布者 |
| content | TEXT | 文案 |
| type_raw | VARCHAR(16) | 1=图文 28=纯文字 |
| create_time | BIGINT idx | 发布时间(ms) |
| praise_json / comment_json / image_urls | JSON | 点赞/评论/图片 |
| raw_json + fetched_at/updated_at | | 惯例字段 |
### 4.2 新抓取实体 `moments`(页面 fetch 方式)
- 浏览器 `fetch POST getFriendsterInfo`,`pageSize=100` 逐员工翻页到 `hashNext=false`;**`createTime` 云客返回秒,入库前 ×1000 统一转 ms**(否则渲染 1970、水位逻辑错位)
- 接入 `scrape_via_page.py` 实体注册、`full_sync_all.sh` 加一步、调度 4h 增量(水位=本地 max(create_time),仅扫有新朋友圈的员工——云客接口无 per-员工增量标记,简化为全量浅扫第一页有新才继续)
- 图片接入现有 media 本地化批处理(OSS URL 型)
### 4.2b `chat_group` 补列(通讯录·微信群 tab 数据,从 raw_json 提取)
真机验证云客群表需要:群主信息/群成员好友数量/消息数量。`raw_json` 已有这些键(ownerName/ownerWchatId/ownerAlias/ownerImage/friendCount/createTime),加实体列避免每行解析 JSON:
| 新列 | 来源 raw_json 键 | 说明 |
|---|---|---|
| owner_name / owner_wxid / owner_alias / owner_avatar | ownerName/ownerWchatId/ownerAlias/ownerImage | 群主信息(上游脱敏,如实存) |
| friend_count | friendCount | 群成员中该员工好友数 |
| create_time | createTime | 建群时间(ms,统计·新的群 tab 用) |
一次性迁移脚本回填存量(幂等,复用 migrate 惯例),scraper upsert 持续写入。
### 4.3 API 改动
| API | 改动 |
|---|---|
| `GET /api/accounts` | `AccountOut` + `avatar`(从 `raw_json.headImageUrl` 提取,WA 侧无此键则 null) |
| `GET /api/sessions` | + `is_del` 参数(join `customer.delete_flag`,NULL 视为 0);行 + `pinned`(`talker_id='filehelper'`) |
| `GET /api/moments` 新 | `?wechat_id=&page=&page_size=` 帖子 desc + 头部 `{nickname, avatar, alias}`(头部取自 `wechat_account` 表/nick_name + `raw_json.headImageUrl`,不在 moments_post 存) |
| `GET /api/staff-stats` 新 | `?wechat_id=&start=&end=` 计数;`&kind=` 明细行(**除敏感词外全部 tab 都出明细行**,本地表聚合) |
| `GET /api/messages` | + `msg_type` 过滤 + `q` 全文关键词(聊天记录模式用)。**msg_type→本地 type_raw 映射需先补全归一化**(见 4.5) |
### 4.4 消息类型归一化补全 + 存量回填(10 类过滤的数据前提)
本地实测 wechat 渠道 `type` 仅 4 种(text/image/image_legacy/unknown),**unknown 约 13 万行**:type_raw 21=31859(链接)、15=18393、9=9023、10/4/18/8/6/22/13/14/5/11/20 各数千数百、49=202(红包转账);语音消息(34)未出现。10 类下拉中 语音/视频/文件/小程序名片 4 类现状过滤必为 0。
任务:扩展 `scripts/_normalize_msg_type` 映射(21→link、49 拆红包/转账按 paymsgid/appmsg type、15/18→视频/语音通话候选——**具体语义在 plan 第一阶段用 raw_json 样本逐一判别**),跑存量回填(2026-08-20 type=2 回填先例,幂等可重跑)。回填后 10 类过滤才有真实数据。
### 4.5 统计口径(现有表本地聚合)
| tab | 口径 |
|---|---|
| 新的客户 | `customer.add_time` 窗内(实测 62% 有值);缺失兜底=该对话首条消息时间 |
| 新的群 | `chat_group.create_time` 窗内(新列,从 raw_json.createTime 回填;无值行不计入) |
| 红包转账 | `message.type_raw=49 AND (raw_json.paymsgid 非空 OR text 含 appmsg type 2001/2000)`,合并一类;明细金额从 XML `<feedesc>` 提取,失败显 `—`(云客本条金额也显 `-`) |
| 删除客户 | `customer.delete_flag=1`(实测 53 行;无时间字段→计数=现存量,时间列显最近 updated) |
| 拉黑客户 | `customer.is_block=1`(实测 8 行,同上) |
| 语音通话 | **`phone_call.user_id` 存的是员工 userId(32位HEX) 非 wxId(实测确认)**——必须先 `wechat_account.user_id` join 转换再查,直接用 wxId 查会 0 行;`channel='voice_call' AND start_call_time 窗内` |
| 敏感词 | 占位空态「词库未配置,待 C1 敏感词功能接入」 |
## 5. 各视图细节(对齐云客)
### 单聊/群聊
- 中列顶部「好友/群 | 聊天记录」下拉双模式 + 搜索框(昵称/wxid 或全量消息关键词)
- 单聊有 `聊天好友|已删除好友` tab(群聊无)→ sessions `is_del`
- 置顶区:filehelper 固定置顶 + 可折叠;置顶不参与今日红点
- 会话卡:头像+昵称+`今日N`红点+相对时间(今天 `HH:mm`/昨天/今年 `M月D日`/往年全日期)
- 右列:默认近 14 天窗(清空=全部)、关键词、类型下拉 10 类、导出按钮(复用 `/export/chat-html`)
- 消息流:加载更多/上一页 + 日期分隔条;点会话切路由 talkerId
- 已删除好友会话照常显示历史消息
### 朋友圈
- feed 卡片:作者头像+名 → 内容 → 九宫格图片(点击预览)→ 时间 → 点赞人列表
- 滚动翻页;空态提示先跑 moments 抓取;缺图占位框
### 通讯录
- `微信客户|微信群` tab + 搜索框(客户 tab:昵称/微信号/备注/手机号;群 tab:仅群名称)
- 客户表 7 列:复选框/客户微信(头像+昵称+微信号)/数据来源/客户备注名/最近沟通时间/发送条数/接收条数/操作(查看聊天记录+导出HTML聊天记录)
- **微信群表 8 列(真机)**:复选框/群信息(群名)/数据来源/群主信息(昵称+微信号,raw_json 提取)/群成员数量/群成员好友数量(friend_count)/消息数量/操作(查看聊天记录→群聊消息+导出HTML聊天记录)
- **批量操作**:全选复选框 + 顶部「查询/导出(CSV)/导出聊天记录(勾选后批量导出 HTML,串行调 /export/chat-html)」
- 复用 `/api/customers`、`/api/groups`(owner 过滤已支持;groups 补 owner 精确+新列输出)
### 统计分析
- 7 tab 带计数徽标,默认近 7 天窗
- **明细列按 tab 分型(配置驱动 cols,真机验证)**:
- 新的客户:客户微信(可点→聊天)/客户备注/员工微信/时间
- 红包转账:操作类型(红包/转账)/客户微信(可点)/员工微信/金额/发生时间/操作(查看聊天记录**带 msgid 定位滚动到该消息**)
- 新的群:微信群信息(可点→群聊)/员工微信/时间
- 删除/拉黑客户:客户微信/员工微信/发生时间
- 语音通话:**复用 CallList 列定义**(通话人/通话类型/来源/呼入呼出/接通状态/通话录音/通话时间/操作)
- 敏感词空态占位
## 6. 错误处理
- 每视图独立 loading/error(`el-alert`),互不影响
- 翻页失败→显示已加载+重试;图片失效→占位框;缺字段→`—`
- 统计某类无数据→空表格+计数 0;非法 tab→重定向 single
- 与现有页面一致的后端未起/超时错误条
## 7. 测试
- 后端 pytest:`test_api_moments.py`(分页/排序/空库)、`test_api_staff_stats.py`(7 类口径+窗口边界+add_time 兜底+红包判定)、sessions 增补(is_del/置顶)、accounts 增补(avatar)、scraper 归一化单测(fixture=真机 JSON)
- 前端 vitest:路由跳转、tab 切换、相对时间格式化
- 验收:本地前后端 + Playwright 走查 5 视图,与云客真机对比
## 8. 实施顺序
1. 后端:moments 表+scraper → chat_group 补列+回填 → 消息类型归一化映射判别+存量回填 → API(accounts/sessions/messages/groups 改动,moments/staff-stats 新增)
2. 前端骨架:路由+左栏+单聊视图(含 msgid 定位消息能力)
3. 群聊/通讯录(客户表+群表+批量操作)/统计分析(分型明细)视图
4. 朋友圈视图
5. 入口改造 + Playwright 走查 5 视图对照云客 + 提交