# 在线客服模块设计方案 > **文档状态**:产品 + 架构 + 表结构 + 接口(**先评审,再开发**) > **编写日期**: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 文件名与权限种子脚本路径。