docs(spec): 5 数字 modal spec 修订——room_id 私聊过滤 + 分页适配 + handler 换源 + 无需 Alembic
spec review 反馈:群消息误算(加 room_id 过滤)、YkStaffDetailModal fetcher 分页适配 (合成分页)、5 handler 已存在(换数据源非接线)、索引已存在(不加 Alembic)、NULL 语义、 日期选择器放 StaffList、路径前缀。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -32,17 +32,21 @@
|
||||
|
||||
## 3. 数据模型 / 聚合
|
||||
|
||||
**无新表**。聚合在查询时算(`message` 表按 `owner_account_id` + 日期窗 GROUP BY `talker_id`)。
|
||||
**无新表、无新索引**。聚合在查询时算。现有索引 `ix_message_channel_owner_talker_time(channel, owner_account_id, talker_id, timestamp)` 已覆盖(**不需要 Alembic 迁移**)。
|
||||
|
||||
**每个客户(talker_id)的聚合字段**(窗口内):
|
||||
- `send_count` = `is_send=1` 消息数(员工发)
|
||||
- `receive_count` = `is_send=0` 消息数(客户发)
|
||||
- `interaction_days` = 不同 `timestamp` 日期数
|
||||
- `last_is_send` = 窗口内最后一条消息的 `is_send`(1=员工最后发,0=客户最后发)
|
||||
**只聚合 1:1 私聊**(对齐云客 `wxChatInfo` 是员工×客户对,不含群):聚合 SQL 必须 `WHERE channel='wechat' AND owner_account_id=:owner AND (room_id IS NULL OR room_id = '') AND timestamp BETWEEN :start AND :end`,再 `GROUP BY talker_id`。**漏 room_id 过滤会把群消息算进客户统计**(talker 在群消息里是群成员,非客户对)。
|
||||
|
||||
**NULL 语义**:`timestamp IS NULL` 的行排除(窗口/排序都依赖它);`is_send IS NULL` 的行不计入 send/receive(未知方向,skip)。
|
||||
|
||||
**每个客户(talker_id)的聚合字段**(窗口内私聊):
|
||||
- `send_count` = `is_send` True 的消息数(员工发)
|
||||
- `receive_count` = `is_send` False 的消息数(客户发)
|
||||
- `interaction_days` = 不同 `timestamp` 日期数(**跨库日期提取**:用 SQLAlchemy 在 epoch-秒 上算 `func.date(...)`,MySQL `DATE(FROM_UNIXTIME(ts/1000))` / SQLite `date(ts/1000,'unixepoch')`——不要写裸 SQL)
|
||||
- `last_is_send` = 窗口内最后一条(按 timestamp)消息的 `is_send`(True=员工最后发,False=客户最后发)
|
||||
- `last_time` = 最后一条时间
|
||||
- 客户身份:`talker_id` → 关联 `customer` 表取 `friend_nickname`/`friend_remark`/`real_name`(left join,缺失则用 talker_id)。
|
||||
- 客户身份:`talker_id` → left join `customer` 表取 `friend_nickname`/`friend_remark`/`real_name`(缺失则用 talker_id)。
|
||||
|
||||
**建议索引**:`message(owner_account_id, timestamp)` 加速(若不存在,plan 里加 Alembic 迁移;先确认现有索引)。
|
||||
> 注:`message.is_send` 是 `Boolean`(过滤用 `is_(True)/is_(False)`,不是 `==1`);`timestamp` 是 `BigInteger` ms-epoch,nullable。
|
||||
|
||||
## 4. 5 数字口径(近似,从聚合字段派生)
|
||||
|
||||
@@ -77,17 +81,16 @@
|
||||
|
||||
## 6. 前端(StaffList + modal)
|
||||
|
||||
- StaffList 每行(员工微信)「沟通数」「及时回复」复合列:把现有静态数字(若有)改/补成**可点** 5 个数字。每行加载时调 `getStaffChatStats({owner_account_id: row.<account>, start, end})`(默认近 3 天)填 summary。
|
||||
- 性能:每页 N 行 × 1 次聚合;首版顺序调用可接受(消息表有索引),或后续优化为批量 endpoint。
|
||||
- 点数字 → 复用 `YkStaffDetailModal`(Plan 1 通用件,支持自定义列 + 数据源)弹客户子集:
|
||||
- 沟通人数 → customers 全集
|
||||
- 有效沟通 → 过滤 `send≥1 && receive≥1`
|
||||
- 跟进 → 过滤 `interaction_days≥2`
|
||||
- 最后回复 → 过滤 `last_is_send=1`
|
||||
- 未回复 → 过滤 `last_is_send=0`
|
||||
- modal 列(对齐云客):序号 / 客户微信(昵称+号) / [有效沟通多 发送数/接收数 列] / 员工信息 / 员工微信。modal 内客户微信可点→(Phase B)。
|
||||
- modal 顶部日期选择器(el-date-picker,默认近 3 天)→ 改 window 重查。
|
||||
- modal 标题对齐云客(沟通人数(已去重) / 有效沟通数(已去重) / 跟进客户数 / 员工最后回复数 / 未回复客户数)+ 近似口径小字说明。
|
||||
> **事实(plan 核实)**:StaffList.vue 的 5 个数字 cell **点击 handler 已存在**(现都调 `openWechatCommModal`,数据源 `getCustomers`)。Phase A 是**换数据源**(→ `getStaffChatStats`)+ 换每行 summary 数字来源,**不是加点击接线**。
|
||||
|
||||
- **每行 5 个数字**:现来自上游 camelCase 字段(`chatCount`/`contactCount`/`followCount`/`saleSendLastNum`/`customerSendLastNum`)。Phase A 改为:每行(按 `row.account_id` = 员工微信号)调 `getStaffChatStats({owner_account_id: row.account_id, start, end})`(默认近 3 天)取 `summary`,5 个数字映射到 `chat_count/effective_count/follow_count/last_reply_count/no_reply_count`。
|
||||
- **员工最后回复率**(现 `saleSendLastNumRate`,line ~545)是比率非计数、无 Phase-A 本地等价 → **保持现状不动**(不可点),只有 5 个**计数**变本地可点。
|
||||
- **点数字 → 复用 `YkStaffDetailModal`**(Plan 1 通用件,`fetcher: (page,page_size)=>Page<T>` 服务端分页):
|
||||
- **分页适配(重要)**:本 endpoint 返回扁平 `{summary, customers[]}`(窗口内客户通常几十个,无需服务端分页)。StaffList 一次性取 `customers`,**在 fetcher 闭包里按 metric 过滤 + 切页包成合成 `Page<T>`** 传给 modal(不动 `YkStaffDetailModal` 的通用分页契约)。
|
||||
- metric 过滤:沟通人数=全集 / 有效沟通=`send≥1&&receive≥1` / 跟进=`interaction_days≥2` / 最后回复=`last_is_send=true` / 未回复=`last_is_send=false`。
|
||||
- modal 列(对齐云客):序号 / 客户微信(昵称+号) / [有效沟通 modal 多 发送数/接收数 列] / 员工信息 / 员工微信。
|
||||
- **日期选择器放 StaffList**(不放 `YkStaffDetailModal`,保持其通用):StaffList 持 `start/end` 状态,传进 fetcher 闭包;改日期 → 重查 `getStaffChatStats`。
|
||||
- modal 标题对齐云客(沟通人数(已去重) / 有效沟通数(已去重) / 跟进客户数 / 员工最后回复数 / 未回复客户数)+ 近似口径小字说明(跟进为近似口径)。
|
||||
|
||||
## 7. 关键决策(已与用户确认 2026-08-07)
|
||||
|
||||
@@ -106,17 +109,17 @@
|
||||
|
||||
## 9. 文件清单(预期)
|
||||
|
||||
**后端**:
|
||||
- 新 `app/api/routers/staff_chat_stats.py`(`/api/staff-chat-stats` 聚合查询)
|
||||
- 改 `app/api/main.py`(注册 router)
|
||||
- 改 `app/api/schemas.py`(`StaffChatStatsOut`/`StaffChatCustomerOut`)
|
||||
- (若需)Alembic 加 `message(owner_account_id, timestamp)` 索引
|
||||
- 新测试 `tests/test_api_staff_chat_stats.py`(种消息 → 聚合 5 数字 + 客户行正确)
|
||||
**后端**(路径前缀 `yunke-server/`):
|
||||
- 新 `yunke-server/app/api/routers/staff_chat_stats.py`(`/api/staff-chat-stats` 聚合查询,含 room_id 私聊过滤)
|
||||
- 改 `yunke-server/app/api/main.py`(注册 router)
|
||||
- 改 `yunke-server/app/api/schemas.py`(`StaffChatStatsOut`/`StaffChatCustomerOut`)
|
||||
- **不需要 Alembic**(复用现有 `ix_message_channel_owner_talker_time` 索引)
|
||||
- 新测试 `yunke-server/tests/test_api_staff_chat_stats.py`(种私聊+群消息 → 验证群被过滤、5 数字 + 客户行 + 日期窗正确)
|
||||
|
||||
**前端**:
|
||||
- 改 `yunkes/src/api/index.ts` + `types.ts`:`getStaffChatStats` / `StaffChatStats` 类型
|
||||
- 改 `yunkes/src/views/StaffList.vue`:5 数字可点 + 挂 modal
|
||||
- 改/复用 `yunkes/src/components/YkStaffDetailModal.vue`:支持日期选择器 + 5 套列配置 + 客户子集过滤
|
||||
- 改 `yunkes/src/api/index.ts` + `types.ts`:`getStaffChatStats` / `StaffChatStats` / `StaffChatCustomer` 类型
|
||||
- 改 `yunkes/src/views/StaffList.vue`:**换 5 数字数据源**(handler 已存在)→ `getStaffChatStats` summary;日期选择器状态 + fetcher 闭包(含 metric 过滤 + 合成分页);员工最后回复率保持不动
|
||||
- `YkStaffDetailModal.vue` **不改**(保持通用)—— StaffList 侧包装 fetcher 适配
|
||||
|
||||
## 10. 验收
|
||||
|
||||
|
||||
Reference in New Issue
Block a user