Ship customer portal and marketing pages, user edit drawer with employee archive, IDE employee numbers, channel config refactor, and related nginx/route updates. Co-authored-by: Cursor <cursoragent@cursor.com>
680 lines
27 KiB
Markdown
680 lines
27 KiB
Markdown
# 在线客服模块设计方案
|
||
|
||
> **文档状态**:产品 + 架构 + 表结构 + 接口(**先评审,再开发**)
|
||
> **编写日期**:2026-07-26
|
||
> **关联页面**:H5「我的 / 联系客服」、AI 顾问(`/assistant`、`/h5/assistant`)、坐席工作台(新建)
|
||
> **关联能力**:专属顾问(`Customer.CustomerService` → `/customer/service-advisor`)、AI 对话(`/xhs/ai/chat`)、站内信(`/system/notice/*`)、微信注册绑定
|
||
> **关联文档**:`docs/H5客户中心设计方案.md`、`docs/前端页面设计规范.md`、`docs/rbac系统设计-060801.md`、`docs/数据权限设计.md`
|
||
> **目标读者**:产品、前端、后端、运营、测试
|
||
|
||
---
|
||
|
||
## 0 阅读指引
|
||
|
||
| 角色 | 优先阅读 |
|
||
| --- | --- |
|
||
| 产品 / 运营 | §1~§3、§5、§11 |
|
||
| 前端 | §4~§6、§8、§10 |
|
||
| 后端 / DBA | §4、§7~§9、§10 |
|
||
| 测试 | §5、§11、§12 |
|
||
|
||
**分期对照(务必先对齐)**
|
||
|
||
| 阶段 | 名称 | 是否改实时通道 | 交付物 |
|
||
| --- | --- | --- | --- |
|
||
| **P0** | AI + 微信兜底 | 否 | 入口打通、顾问二维码、复制单号 |
|
||
| **P1** | 站内轻量 IM(主推) | 是(SignalR) | 会话 / 消息 / 坐席工作台 / 转人工 |
|
||
| **P2** | 触达与渠道扩展 | 可选 | 离线通知、公众号模板、企微等 |
|
||
|
||
> 本文 **默认按 P0 → P1 落地**;P2 仅列边界,不进首版开发范围。
|
||
|
||
---
|
||
|
||
## 1 背景与目标
|
||
|
||
### 1.1 为什么要做
|
||
|
||
客户高频问题(多少钱、到哪了、出问题找谁)已在 H5 客户中心与 AI 顾问中部分覆盖,但「找人」仍停在:
|
||
|
||
| 现状 | 缺口 |
|
||
| --- | --- |
|
||
| AI 顾问 HTTP 一问一答(`/xhs/ai/chat`) | 无法转人工、无会话留痕给坐席 |
|
||
| `GET /customer/service-advisor` 展示微信二维码 | 系统外沟通,订单上下文靠客户口述 |
|
||
| H5「联系客服」规划为入口 | 尚未形成可运营的客服闭环 |
|
||
| 问题件站内信 / 邮件通知 | 非双向即时对话 |
|
||
|
||
物流售后场景需要:**带着运单上下文、可留痕、可考核、可回退到微信** 的客服能力,而不是官网浮窗式售前聊天。
|
||
|
||
### 1.2 建设目标
|
||
|
||
1. **三层服务**:AI 一线 → 站内人工 → 微信兜底,按能力成熟度渐进上线。
|
||
2. **上下文优先**:会话可挂 `OrderNo` / 订单状态摘要,减少「请发单号」。
|
||
3. **归属清晰**:优先路由到客户已绑定的 `CustomerService` 专属客服。
|
||
4. **双端一致**:客户侧(H5 / PC 顾问壳)与坐席侧(中后台)共用同一会话模型。
|
||
5. **可运营**:会话状态、未读、转接基础能力可统计;首版不做大屏 SLA。
|
||
|
||
### 1.3 设计原则
|
||
|
||
1. **不推翻现有顾问模型**:`Customer.CustomerService` + 顾问二维码继续作为兜底与身份来源。
|
||
2. **AI 与人工同壳**:复用 `AiAdvisorShell` / H5 AI Tab,增加「转人工」而非另起一套聊天 UI(客户侧)。
|
||
3. **实时用 SignalR**:与 .NET 8 栈一致;多实例用 Redis backplane(已有 FreeRedis)。
|
||
4. **身份隔离**:客户会话强制 `CustomerId` 隔离;坐席受 RBAC + 数据权限约束。
|
||
5. **先业务后推送**:P1 先保证在线双方实时;离线触达(站内信 / 公众号)可后置。
|
||
|
||
### 1.4 非目标(P0 / P1 不做)
|
||
|
||
- 语音 / 视频通话
|
||
- 全渠道聚合(企微、抖音、美洽等)作为主通道
|
||
- 机器人知识库编排、意图平台
|
||
- 坐席大屏、复杂 SLA / 质检评分体系
|
||
- 客户侧管理端 H5(坐席仅 PC 中后台)
|
||
- 改变订单状态机、计费、现有 AI Provider 配置模型
|
||
|
||
### 1.5 方案选型结论
|
||
|
||
| 方案 | 结论 |
|
||
| --- | --- |
|
||
| 仅 AI + 微信二维码 | **P0 必做**,成本最低 |
|
||
| 自研 SignalR 轻量 IM | **P1 主路径**,订单上下文与归属可深嵌 OMS |
|
||
| 第三方在线客服(美洽 / 环信等) | **不作为主路径**;仅适合官网匿名售前且不绑 OMS 时另议 |
|
||
|
||
---
|
||
|
||
## 2 角色与场景
|
||
|
||
### 2.1 角色
|
||
|
||
| 角色 | 登录形态 | 核心诉求 |
|
||
| --- | --- | --- |
|
||
| **客户**(含子客户) | `loginType=1` | 带着单号问人、尽快得到答复 |
|
||
| **专属客服** | `loginType=0`,对应 `Customer.CustomerService` | 只看自己客户、处理跟单问题 |
|
||
| **客服主管 / 值班** | `loginType=0` + 权限 | 认领公共队列、转接、查看范围更大(受数据权限) |
|
||
| **AI 顾问** | 系统机器人 | 一线解答;无法解决时引导转人工 |
|
||
|
||
### 2.2 高频场景(按紧迫度)
|
||
|
||
```text
|
||
1. 订单异常 / 理赔 → 要人立刻接
|
||
2. 查价 / 渠道选择搞不清 → 先问 AI,再转人工
|
||
3. 「货到哪了」→ AI / 自助轨迹优先;仍不懂再人工
|
||
4. 充值、账号问题 → 人工或微信
|
||
5. 坐席不在线 → 微信二维码 + 复制单号(绝不让客户卡死)
|
||
```
|
||
|
||
### 2.3 成功标准(客户感受)
|
||
|
||
| 场景 | 成功标准 |
|
||
| --- | --- |
|
||
| 联系客服 | 30 秒内看到「AI / 在线客服 / 微信」之一可用 |
|
||
| 转人工 | 单号与最近提问自动带入,无需重述 |
|
||
| 坐席侧 | 打开会话即见客户卡片 + 最近订单摘要 |
|
||
| 无人在线 | 明确提示并给出顾问微信,不出现空白等待 |
|
||
|
||
---
|
||
|
||
## 3 产品分期
|
||
|
||
### 3.1 P0 — AI + 微信兜底(约 1~2 周)
|
||
|
||
**目标**:H5 / AI 壳「找得到人」。
|
||
|
||
| 项 | 说明 |
|
||
| --- | --- |
|
||
| 入口 | H5「我的 → 联系客服」;订单异常「问顾问 / 联系客服」 |
|
||
| 能力 | 拉 `service-advisor`;展示姓名 / 微信号 / 二维码;一键复制单号 |
|
||
| 可选 | AI 页底部固定「联系专属客服」抽屉(复用现有 Advisor 抽屉) |
|
||
| 不做 | 会话表、SignalR、坐席工作台 |
|
||
|
||
### 3.2 P1 — 站内轻量 IM(主交付)
|
||
|
||
**目标**:客户 ↔ 坐席站内实时对话 + AI 转人工。
|
||
|
||
| 项 | 说明 |
|
||
| --- | --- |
|
||
| 客户侧 | 会话列表 + 聊天窗(嵌入 AI 壳旁或独立页);文本 + 图片 |
|
||
| 坐席侧 | 收件箱(待接入 / 进行中 / 已结束)+ 聊天 + 右侧客户/订单上下文 |
|
||
| 实时 | SignalR Hub;断线可 HTTP 补拉历史 |
|
||
| 分配 | 优先专属客服;离线进公共队列认领 |
|
||
| 兜底 | 无坐席在线 → 降级 P0 微信卡片 |
|
||
| 通知 | 未读角标;可选写站内信(`noticeType=message`) |
|
||
|
||
### 3.3 P2 — 触达与渠道(延后)
|
||
|
||
- 坐席离线时公众号 / 订阅消息提醒客户「有新回复」
|
||
- 客户离线时站内信 / 邮件提醒坐席(或反向)
|
||
- 企微 / 微信客服作为外部渠道,会话仍落本库
|
||
- 快捷回复库、会话评价、简易报表
|
||
- 转接多人、技能组、自动分配负载均衡
|
||
|
||
---
|
||
|
||
## 4 总体架构
|
||
|
||
### 4.1 逻辑架构
|
||
|
||
```text
|
||
┌─────────────┐ ┌──────────────┐ ┌────────────────────┐
|
||
│ H5 / PC 客户 │ │ 中后台坐席台 │ │ AI(现有 XhsAi) │
|
||
│ 聊天 + 入口 │ │ Inbox + 上下文│ │ HTTP /xhs/ai/chat │
|
||
└──────┬──────┘ └──────┬───────┘ └─────────┬──────────┘
|
||
│ REST + SignalR │ REST + SignalR │ 转人工时写入消息
|
||
▼ ▼ ▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ SimpleAdmin.Api │
|
||
│ CsConversationService(HTTP) + CsChatHub(SignalR) │
|
||
└─────────────┬───────────────────────────────┬───────────────┘
|
||
│ │
|
||
▼ ▼
|
||
MySQL(会话/消息) Redis(在线状态 + SignalR backplane)
|
||
│
|
||
▼(可选)
|
||
RabbitMQ / Notice(离线提醒,P1 末或 P2)
|
||
```
|
||
|
||
### 4.2 与现有模块关系
|
||
|
||
| 现有能力 | 关系 |
|
||
| --- | --- |
|
||
| `Customer.CustomerService` | 默认 Assignee 解析源 |
|
||
| `/customer/service-advisor` | P0 展示;P1 无人在线时降级 |
|
||
| `AiAdvisorShell` / `/xhs/ai/chat` | 一线;「转人工」创建会话并可选灌入最近 AI 对话 |
|
||
| `OmsFileUploadHelper` | 聊天图片上传(新 folder:`cs`) |
|
||
| `NoticeService` | 可选:新消息站内信 |
|
||
| 数据权限 | 坐席列表/认领范围按客服数据权限过滤 |
|
||
| nginx | P1 必须补 WebSocket Upgrade |
|
||
|
||
### 4.3 技术选型
|
||
|
||
| 点 | 选型 | 说明 |
|
||
| --- | --- | --- |
|
||
| 实时 | **ASP.NET Core SignalR** | 官方、易鉴权、易水平扩展 |
|
||
| 前端 SDK | `@microsoft/signalr` | 勿复用 hiprint 的 `socket.io-client` |
|
||
| Backplane | **Redis** | 多 Api 实例必开 |
|
||
| 落库 | SqlSugar + MySQL | 与 OMS 一致 |
|
||
| 在线状态 | Redis KV | `cs:agent:{userId}`,心跳续期 |
|
||
| 附件 | 本地 `wwwroot/uploads/cs/{yyyyMM}/` | 图片 ≤ 5MB,与现规则一致 |
|
||
|
||
### 4.4 服务端部署注意
|
||
|
||
1. `AddSignalR().AddStackExchangeRedis(...)` 或与现有 FreeRedis 兼容的 backplane 方案(实现阶段选定一种,避免双 Redis 客户端混乱)。
|
||
2. nginx 为 Hub 增加独立 location(建议 `/hubs/`),配置:
|
||
|
||
```nginx
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
proxy_read_timeout 3600s;
|
||
```
|
||
|
||
3. 前端 Docker / `nginx-docker.conf`、`nginx-default.conf` 同步修改;新坐席路由若与 API 前缀冲突,按规范 §十五做 SPA `try_files`。
|
||
|
||
---
|
||
|
||
## 5 核心流程
|
||
|
||
### 5.1 P0:联系专属客服
|
||
|
||
```text
|
||
客户点「联系客服」
|
||
→ GET /customer/service-advisor
|
||
→ 有微信:展示二维码 + 复制微信号
|
||
→ 无配置:提示「暂未分配专属客服,请联系管理员」
|
||
→ 若从订单进入:提供「复制单号」
|
||
```
|
||
|
||
### 5.2 P1:客户发起 / 继续会话
|
||
|
||
```text
|
||
客户进入客服页
|
||
→ 若有进行中会话:直接进入
|
||
→ 否则创建会话(可选带 orderNo、source、首条留言)
|
||
→ 分配:
|
||
1) 解析 Customer.CustomerService → 目标坐席
|
||
2) 坐席在线:状态=Open,推送给该坐席
|
||
3) 坐席离线:状态=Queued,进入公共队列
|
||
4) 无人可服务且队列过久(可配置,默认立即):展示微信降级卡片
|
||
→ 双方通过 Hub 收发;每条消息落库
|
||
```
|
||
|
||
### 5.3 AI → 转人工
|
||
|
||
```text
|
||
客户在 AI 顾问对话中点「转人工」
|
||
→ 创建(或复用进行中)会话
|
||
→ 将最近 N 条(建议 10)AI 对话写入 CsMessage
|
||
SenderType = Bot / Customer
|
||
→ 系统消息:「客户从 AI 顾问转入,关联运单:XXX」
|
||
→ 进入 §5.2 分配逻辑
|
||
→ UI 切换到人工会话视图(同一壳内 Tab 或抽屉)
|
||
```
|
||
|
||
### 5.4 坐席接单与结束
|
||
|
||
```text
|
||
坐席打开 Inbox
|
||
→ 我的进行中 + 公共待接入(有权限)
|
||
→ 认领 Queued 会话 → Assignee=自己,Status=Open
|
||
→ 回复文本/图片;可查看右侧订单摘要
|
||
→ 结束会话 → Status=Closed(客户可再开新会话)
|
||
→ 转接(P1 可做简化版):指定同事 → 改 Assignee + 系统消息
|
||
```
|
||
|
||
### 5.5 断线与补拉
|
||
|
||
```text
|
||
SignalR 断线 → 客户端自动重连
|
||
→ 重连成功后 HTTP 拉取 cursor 之后的消息(按 MessageId / CreateTime)
|
||
→ 不依赖「推送一定到达」作为唯一真相;数据库为真相源
|
||
```
|
||
|
||
---
|
||
|
||
## 6 页面、路由与权限
|
||
|
||
### 6.1 菜单与路由(坐席 / 管理端)
|
||
|
||
```text
|
||
客户服务(新建顶级或挂在「客户」下,评审定)
|
||
└── 在线客服 /customerService/inbox
|
||
```
|
||
|
||
前端:`src/router/routes/newModules/customerService.ts`
|
||
|
||
| path | name | component | permission |
|
||
| --- | --- | --- | --- |
|
||
| `inbox` | `CsInbox` | `views/customerService/inbox/index.vue` | `CustomerService.Inbox.Query` |
|
||
|
||
布局:左会话列表 + 中聊天区 + 右上下文(非表格主列表页;工具按钮仍遵循 32px / 主色变量)。列表类配置页(快捷回复等)P2 再按 `table-toolbar` 规范做。
|
||
|
||
### 6.2 客户侧入口
|
||
|
||
| 入口 | 路由 / 位置 | P0 | P1 |
|
||
| --- | --- | --- | --- |
|
||
| H5 我的 → 联系客服 | `/h5/me` 列表项 → 抽屉或 `/h5/cs` | 微信卡片 | 会话页 |
|
||
| H5 / PC AI 顾问 | `AiAdvisorShell` | 顾问微信抽屉 | 「转人工」 |
|
||
| 订单异常 | 订单卡片操作 | 复制单号 + 联系客服 | 带 `orderNo` 建会话 |
|
||
| PC 顶栏(可选) | 未读角标进 Inbox(仅坐席) | — | 有 |
|
||
|
||
建议客户会话页:
|
||
|
||
| path | 说明 |
|
||
| --- | --- |
|
||
| `/h5/cs` | H5 客服会话(P1) |
|
||
| `/assistant` 内嵌人工面板 | PC 与 AI 同壳切换(P1) |
|
||
|
||
### 6.3 RBAC 权限码
|
||
|
||
格式:`{Module}.{Resource}.{Action}`。
|
||
|
||
| 权限码 | 说明 |
|
||
| --- | --- |
|
||
| `CustomerService.Inbox` | 在线客服模块 |
|
||
| `CustomerService.Inbox.Query` | 查看收件箱 / 会话详情 / 历史消息 |
|
||
| `CustomerService.Inbox.Reply` | 发送消息、上传附件 |
|
||
| `CustomerService.Inbox.Claim` | 认领公共队列 |
|
||
| `CustomerService.Inbox.Close` | 结束会话 |
|
||
| `CustomerService.Inbox.Transfer` | 转接(P1 简化或 P1.1) |
|
||
| `CustomerService.Inbox.ViewAll` | 跨专属查看(主管;仍受数据权限) |
|
||
|
||
客户门户接口:**无上述 Permission**;仅 `loginType=1` + 当前 `CustomerId` 隔离。
|
||
|
||
### 6.4 后端 Service 与前缀
|
||
|
||
| 类 | 路由前缀 | 说明 |
|
||
| --- | --- | --- |
|
||
| `CsConversationService` | `/cs/conversation/*` | 会话 CRUD / 列表 / 认领 / 关闭 |
|
||
| `CsMessageService` | `/cs/message/*` | 历史分页、(可选)HTTP 发消息兜底 |
|
||
| `CsAgentService` | `/cs/agent/*` | 坐席心跳、在线状态 |
|
||
| `CsChatHub` | `/hubs/cs` | SignalR |
|
||
|
||
上传:`POST /cs/upload/image`(内部走 `OmsFileUploadHelper`,folder=`cs`)。
|
||
|
||
---
|
||
|
||
## 7 数据模型
|
||
|
||
> 表前缀建议 `t_cs_`;字段 PascalCase;审计字段对齐 OMS 公共规范(`Id`、`CreateTime`、`UpdateTime`、`IsDeleted` 等,以项目现有基类为准)。
|
||
|
||
### 7.1 `t_cs_conversation` 会话
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
| --- | --- | --- |
|
||
| Id | bigint PK | |
|
||
| CustomerId | int | 主客户 Id(子客户发起也归主客户,或记 SubUserId,见下) |
|
||
| SubUserId | int? | 子客户 RBAC 用户 Id(可空) |
|
||
| AssigneeUserId | int? | 当前坐席用户 Id |
|
||
| Status | varchar(20) | `Queued` / `Open` / `Closed` |
|
||
| Source | varchar(32) | `h5` / `assistant` / `order` / `ai_transfer` … |
|
||
| OrderNo | varchar(64)? | 关联运单号(展示用,不强绑订单表 FK) |
|
||
| Subject | varchar(200)? | 首条摘要 / 标题 |
|
||
| LastMessageAt | datetime? | 排序用 |
|
||
| LastMessagePreview | varchar(200)? | 列表预览 |
|
||
| CustomerUnread | int | 客户未读数 |
|
||
| AgentUnread | int | 坐席未读数 |
|
||
| ClosedAt | datetime? | |
|
||
| CloseReason | varchar(64)? | `agent` / `customer` / `system` |
|
||
| ExtJson | text? | 扩展(AI 会话 Id 等) |
|
||
|
||
索引建议:
|
||
|
||
- `(CustomerId, Status, LastMessageAt)`
|
||
- `(AssigneeUserId, Status, LastMessageAt)`
|
||
- `(Status, LastMessageAt)` — 公共队列
|
||
|
||
### 7.2 `t_cs_message` 消息
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
| --- | --- | --- |
|
||
| Id | bigint PK | |
|
||
| ConversationId | bigint | |
|
||
| SenderType | varchar(16) | `Customer` / `Agent` / `Bot` / `System` |
|
||
| SenderId | int? | 客户用户或坐席用户 Id |
|
||
| MsgType | varchar(16) | `text` / `image` / `system` |
|
||
| Content | text | 文本或图片 URL |
|
||
| PayloadJson | text? | 结构化扩展 |
|
||
| ClientMsgId | varchar(64)? | 客户端去重 |
|
||
| CreateTime | datetime | |
|
||
|
||
索引:`(ConversationId, Id)`、`(ConversationId, CreateTime)`;`ClientMsgId` 唯一(可按会话维度)。
|
||
|
||
### 7.3 坐席在线(可不建表)
|
||
|
||
Redis:
|
||
|
||
```text
|
||
cs:agent:online:{userId} = 1 TTL 60s(心跳 20s 续)
|
||
cs:agent:conn:{connectionId} → userId
|
||
```
|
||
|
||
### 7.4 快捷回复(P2)
|
||
|
||
`t_cs_quick_reply`:Title、Content、Sort、Enabled — 本期不建。
|
||
|
||
### 7.5 状态机
|
||
|
||
```text
|
||
创建
|
||
│
|
||
▼
|
||
Queued ──认领/专属在线自动接入──► Open ──结束──► Closed
|
||
│ │
|
||
└──────── 客户取消/超时 ────────┴──► Closed
|
||
```
|
||
|
||
规则:
|
||
|
||
- 同一 `CustomerId` **同时最多 1 个** `Queued|Open` 会话(再进线则复用)。
|
||
- `Closed` 后新开会话。
|
||
- 转接只改 `AssigneeUserId`,不改 `CustomerId`。
|
||
|
||
---
|
||
|
||
## 8 接口与 Hub 协议
|
||
|
||
### 8.1 HTTP — 客户侧(需客户登录)
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
| --- | --- | --- |
|
||
| GET | `/cs/conversation/my-active` | 当前进行中会话,无则 null |
|
||
| POST | `/cs/conversation/open` | 创建或返回进行中;body: `orderNo?`, `source`, `firstMessage?` |
|
||
| GET | `/cs/conversation/my-list` | 历史会话分页 |
|
||
| GET | `/cs/message/list` | `conversationId` + `beforeId?` + `pageSize` |
|
||
| POST | `/cs/message/send` | HTTP 兜底发消息(与 Hub 等价落库) |
|
||
| POST | `/cs/upload/image` | 上传图片 |
|
||
| POST | `/cs/conversation/close` | 客户结束(可选) |
|
||
|
||
`open` 响应需带:`conversation`、`assigneeOnline`、`fallbackAdvisor`(无人在线时附带原 `service-advisor` 结构)。
|
||
|
||
### 8.2 HTTP — 坐席侧(需内部登录 + Permission)
|
||
|
||
| 方法 | 路径 | 权限 |
|
||
| --- | --- | --- |
|
||
| GET | `/cs/conversation/inbox` | Query;query: `tab=mine\|queue\|closed` |
|
||
| GET | `/cs/conversation/detail` | Query;含客户摘要 |
|
||
| POST | `/cs/conversation/claim` | Claim |
|
||
| POST | `/cs/conversation/close` | Close |
|
||
| POST | `/cs/conversation/transfer` | Transfer |
|
||
| GET | `/cs/message/list` | Query |
|
||
| POST | `/cs/message/send` | Reply |
|
||
| POST | `/cs/upload/image` | Reply |
|
||
| POST | `/cs/agent/heartbeat` | Query(或连上 Hub 即心跳) |
|
||
| GET | `/cs/conversation/context` | Query;`customerId` + 最近订单摘要 |
|
||
|
||
**Inbox 数据范围**:
|
||
|
||
1. `mine`:`AssigneeUserId == 当前用户`
|
||
2. `queue`:`Status=Queued` 且当前用户数据权限覆盖该客户(或 `ViewAll`)
|
||
3. 无 `ViewAll` 时,默认还可看到 `Customer.CustomerService` 映射到自己的会话
|
||
|
||
### 8.3 上下文接口(右侧栏)
|
||
|
||
`GET /cs/conversation/context?customerId=&orderNo?` 返回:
|
||
|
||
```json
|
||
{
|
||
"customer": { "id": 0, "name": "", "code": "", "group": "", "balance": 0 },
|
||
"advisorKey": "",
|
||
"recentOrders": [
|
||
{ "orderNo": "", "status": "", "statusName": "", "updatedAt": "" }
|
||
],
|
||
"focusOrder": { "orderNo": "", "status": "", "trackSummary": "" }
|
||
}
|
||
```
|
||
|
||
复用现有订单查询能力,**只读摘要**,不在客服模块复制订单逻辑。
|
||
|
||
### 8.4 SignalR Hub `/hubs/cs`
|
||
|
||
**鉴权**:JWT(query `access_token` 或 Header,按现有前端封装统一)。
|
||
|
||
**客户端 → 服务端**
|
||
|
||
| 方法 | 参数 | 说明 |
|
||
| --- | --- | --- |
|
||
| `JoinConversation` | `conversationId` | 加入组 `cs:conv:{id}`;校验参与权 |
|
||
| `LeaveConversation` | `conversationId` | |
|
||
| `SendMessage` | `{ conversationId, msgType, content, clientMsgId }` | 落库并广播 |
|
||
| `Typing` | `conversationId` | 可选,P1.1 |
|
||
| `AgentHeartbeat` | — | 坐席续在线 |
|
||
|
||
**服务端 → 客户端**
|
||
|
||
| 事件 | 载荷 | 说明 |
|
||
| --- | --- | --- |
|
||
| `message` | 消息 DTO | 新消息 |
|
||
| `conversationUpdated` | 会话摘要 | 列表刷新 |
|
||
| `assigned` | 会话 Id + 坐席信息 | 客户侧提示已接入 |
|
||
| `closed` | 会话 Id | |
|
||
| `fallbackAdvisor` | 顾问 DTO | 无人在线降级 |
|
||
| `error` | `{ code, message }` | 业务错误 |
|
||
|
||
**组设计**:
|
||
|
||
- `cs:conv:{conversationId}` — 会话内消息
|
||
- `cs:agent:{userId}` — 定向推给坐席
|
||
- `cs:customer:{customerId}` — 定向推给该客户所有连接
|
||
- `cs:queue` — 有 `Claim` 权限的坐席收「新排队」事件(实现可用权限过滤代替大组广播)
|
||
|
||
### 8.5 消息发送统一语义
|
||
|
||
无论 HTTP 还是 Hub:
|
||
|
||
1. 校验会话状态为 `Open`(或 `Queued` 时允许客户继续留言)
|
||
2. `ClientMsgId` 去重
|
||
3. 写入 `t_cs_message`
|
||
4. 更新会话预览、未读、时间
|
||
5. 通过 Hub 推送;对方离线则累加未读,P1 可写一条站内信(开关配置)
|
||
|
||
---
|
||
|
||
## 9 分配与权限细节
|
||
|
||
### 9.1 专属客服解析
|
||
|
||
复用 `CustomerServiceAdvisorResolver` 思路:
|
||
|
||
1. 读 `t_customer.CustomerService`(工号 / 用户名 / 业务键,与现网一致)
|
||
2. 映射到 RBAC `UserId`
|
||
3. 若映射失败:会话进 `Queued`,不阻塞客户留言
|
||
|
||
### 9.2 在线判定
|
||
|
||
- 坐席页打开并成功连接 Hub 后开始心跳
|
||
- TTL 过期视为离线
|
||
- 创建会话时若专属坐席离线 → `Queued` + 返回 `fallbackAdvisor`
|
||
|
||
### 9.3 子客户
|
||
|
||
- 会话 `CustomerId` = 主客户 Id
|
||
- `SubUserId` = 发起人
|
||
- 主客户下其他子账号是否可见:P1 默认 **仅发起人 + 主账号** 可见(实现时用当前用户与主从关系判断);避免子账号互相窥私聊
|
||
|
||
### 9.4 数据权限
|
||
|
||
坐席 `queue` / `ViewAll` 必须走现有数据权限过滤器(`docs/数据权限设计.md`),禁止仅靠前端藏按钮。
|
||
|
||
---
|
||
|
||
## 10 前端交互要点
|
||
|
||
### 10.1 坐席 Inbox(PC)
|
||
|
||
```text
|
||
┌──────────────┬─────────────────────┬──────────────────┐
|
||
│ Tab:我的/ │ 聊天区 │ 客户卡片 │
|
||
│ 排队/已结束 │ 气泡 + 输入框 │ 最近订单 │
|
||
│ 会话列表 │ 系统消息/AI 记录 │ 聚焦运单摘要 │
|
||
│ 未读角标 │ 结束 / 转接 │ 打开订单详情链 │
|
||
└──────────────┴─────────────────────┴──────────────────┘
|
||
```
|
||
|
||
- 主色使用 `var(--app-primary-color)`
|
||
- 按钮高度 32px;危险操作(结束)二次确认
|
||
- 权限:`hasPermission('CustomerService.Inbox.Reply')` 等
|
||
|
||
### 10.2 客户侧(H5)
|
||
|
||
- P0:底部抽屉展示顾问二维码(与 AI 壳现抽屉一致)
|
||
- P1:独立 `/h5/cs` 全屏聊天气泡;顶栏显示接入状态(排队中 / 某某客服已接入 / 请添加微信)
|
||
- 与 AI Tab 关系:AI 页提供「转人工」;人工页提供「回 AI 顾问」
|
||
|
||
### 10.3 AI 同壳
|
||
|
||
- 保留现有 AI 消息流
|
||
- 转人工后:上方可折叠「AI 对话记录」,下方进入人工输入
|
||
- 或整页切换,但 **必须带上 order 上下文条**(与 `useAskAdvisor` 一致)
|
||
|
||
### 10.4 前端工程目录(建议)
|
||
|
||
```text
|
||
src/api/cs/
|
||
src/views/customerService/inbox/
|
||
src/views/h5/customer-center/cs/
|
||
src/components/CsChat/ # 气泡、输入框、连接状态(客户/坐席复用)
|
||
src/composables/useCsHub.ts
|
||
```
|
||
|
||
---
|
||
|
||
## 11 配置项与开关
|
||
|
||
| 配置键 | 默认 | 说明 |
|
||
| --- | --- | --- |
|
||
| `Cs:Enabled` | false | 总开关;false 时入口仅 P0 微信 |
|
||
| `Cs:SignalREnabled` | true | 可降级为纯 HTTP 轮询(应急) |
|
||
| `Cs:QueueFallbackSeconds` | 0 | 0=创建即若专属离线展示微信;>0 为等待秒数 |
|
||
| `Cs:AiTransferHistoryCount` | 10 | 转入时灌入 AI 消息条数 |
|
||
| `Cs:OfflineNoticeEnabled` | false | 新消息写站内信 |
|
||
| `Cs:MaxImageSizeMb` | 5 | 附件上限 |
|
||
|
||
配置可落 `t_system_other_config` 或 `appsettings`,实现阶段二选一,保持与现网配置风格一致。
|
||
|
||
---
|
||
|
||
## 12 验收标准
|
||
|
||
### 12.1 P0
|
||
|
||
- [ ] H5「联系客服」可展示专属顾问姓名 / 微信 / 二维码
|
||
- [ ] 从异常订单可复制单号
|
||
- [ ] 未分配顾问时有明确空态,不白屏
|
||
- [ ] AI 壳仍可打开顾问抽屉
|
||
|
||
### 12.2 P1
|
||
|
||
- [ ] 客户可创建会话、发文本/图片,刷新后历史仍在
|
||
- [ ] 专属客服在线时自动进入「我的」列表并实时收到消息
|
||
- [ ] 专属离线时进排队;有权限坐席可认领
|
||
- [ ] 无人可服务时返回微信降级卡片
|
||
- [ ] AI「转人工」带最近对话与 orderNo
|
||
- [ ] 坐席右侧能看到客户与最近订单摘要
|
||
- [ ] 结束会话后客户再进线为新会话
|
||
- [ ] 双实例部署下消息不丢(Redis backplane)
|
||
- [ ] nginx WebSocket 升级验证通过
|
||
- [ ] 权限码控制认领 / 回复 / 结束;客户无法调坐席接口
|
||
|
||
### 12.3 非功能
|
||
|
||
- [ ] 消息 `ClientMsgId` 重复提交不双写
|
||
- [ ] 客户只能读写本客户会话
|
||
- [ ] 图片上传超限有错误提示
|
||
- [ ] Hub 断线重连后消息可补齐
|
||
|
||
---
|
||
|
||
## 13 实施顺序(建议)
|
||
|
||
| 序号 | 任务 | 阶段 |
|
||
| --- | --- | --- |
|
||
| 1 | P0 入口与顾问卡片打通(H5 + 异常订单) | P0 |
|
||
| 2 | DDL:`t_cs_conversation` / `t_cs_message` + 仓储 | P1 |
|
||
| 3 | HTTP 会话/消息/上传 API + 权限码种子 | P1 |
|
||
| 4 | 坐席 Inbox 静态联调(可先轮询) | P1 |
|
||
| 5 | SignalR Hub + 前端 `useCsHub` + nginx | P1 |
|
||
| 6 | 分配逻辑 + 在线心跳 + 微信降级 | P1 |
|
||
| 7 | AI 转人工 + 上下文侧栏 | P1 |
|
||
| 8 | 未读角标 +(可选)站内信 | P1 |
|
||
| 9 | 转接 / 快捷回复 / 公众号触达 | P2 |
|
||
|
||
**DDL 脚本路径(落地时)**:`oms0414-back/sql/migrate_cs_module.sql`
|
||
**权限种子**:随 RBAC 菜单 SQL 或现有权限导入方式一并提交。
|
||
|
||
---
|
||
|
||
## 14 风险与对策
|
||
|
||
| 风险 | 对策 |
|
||
| --- | --- |
|
||
| 坐席不用系统、仍只回微信 | P0 微信永远保留;P1 强调「转人工带单号」提升系统内处理价值 |
|
||
| SignalR 经 nginx 失败 | 独立 `/hubs/` location + 验收清单;可临时 `SignalREnabled=false` HTTP 轮询 |
|
||
| 多实例串消息 | 强制 Redis backplane,禁止单机假设 |
|
||
| 客户把客服当即时 IM 闲聊 | 入口文案强调「物流咨询」;系统消息提示工作时间(可配置) |
|
||
| AI 灌入消息过长 | 限制条数与单条长度;仅转人工时写入 |
|
||
| `CustomerService` 键与 UserId 映射不稳定 | 收敛解析规则并在客户编辑页校验「可映射」 |
|
||
|
||
---
|
||
|
||
## 15 开放问题(评审需拍板)
|
||
|
||
1. **菜单挂载**:独立「客户服务」还是挂在现有「客户」菜单下?
|
||
2. **子客户会话可见性**:是否允许主账号查看所有子账号会话?
|
||
3. **工作时间**:是否首版就做「非工作时间仅微信」?
|
||
4. **P1 是否包含转接**:还是 P1 只做认领 + 结束?
|
||
5. **站内信提醒**:P1 默认开还是关?
|
||
6. **客户 PC 端**:是否必须与 H5 同步上线人工会话,还是 H5 优先?
|
||
|
||
---
|
||
|
||
## 16 附录:与 H5 设计方案的衔接
|
||
|
||
| H5 文档点 | 本文落点 |
|
||
| --- | --- |
|
||
| 我的 → 联系客服 | §6.2 / P0~P1 |
|
||
| 异常 → 问 AI / 联系客服 | §5.1、§5.3 |
|
||
| 复制单号给人工客服 | P0 保留;P1 升级为自动带 `orderNo` |
|
||
| AI 顾问为随身客服 | 仍为一线;人工为升级路径,不替换 AI Tab |
|
||
|
||
---
|
||
|
||
**文档维护**:评审结论请回写 §15;开发启动后将「文档状态」改为「开发中」,并补充实际 DDL 文件名与权限种子脚本路径。
|