无 Redis 实时客服系统实现思路(Fastify + MongoDB + Socket.IO)
无 Redis 实时客服系统实现思路(Fastify + MongoDB + Socket.IO)
面向:
- 后端:Fastify + MongoDB + Socket.IO
- 无 Redis、单机多进程可扩展
- 功能:顾客排队、客服接单、实时聊天、聊天记录持久化
1 数据库表结构设计
1.1 客服用户表(web_chat_agent)
| 字段 | 类型 | 说明 |
| ------------ | -------- | -------------------------- |
| _id | ObjectId | 客服ID |
| username | string | 客服用户名 |
| password | string | 密码(加密存储) |
| email | string | 邮箱 |
| phone | string | 电话 |
| realName | string | 真实姓名 |
| avatar | string | 头像URL(可选) |
| skills | string[] | 技能标签数组 |
| maxConcurrent| number | 最大并发数(默认3) |
| status | string | 在线状态:ONLINE/OFFLINE/BUSY |
| isActive | boolean | 是否启用(默认true) |
| add_time | number | 创建时间戳 |
| last_login_time | number | 最后登录时间戳(可选) |
| login_ip | string | 登录IP(可选) |
1.2 聊天房间表(web_chat_room)
| 字段 | 类型 | 说明 |
| ------------ | -------- | -------------------------- |
| _id | ObjectId | 房间ID |
| customerId | ObjectId | 顾客用户ID |
| customerName | string | 顾客昵称 |
| agentId | ObjectId | 客服ID(分配后才有) |
| status | string | 状态:WAITING/SERVING/CLOSED |
| createdAt | number | 创建时间戳 |
| lastMessageAt| number | 最后消息时间戳 |
| closedAt | number | 关闭时间戳 |
| category | string | 问题分类(可选) |
| priority | string | 优先级:LOW/MEDIUM/HIGH |
1.3 聊天消息表(web_chat_message)
| 字段 | 类型 | 说明 |
| ------------ | -------- | -------------------------- |
| _id | ObjectId | 消息ID |
| roomId | ObjectId | 房间ID |
| senderId | ObjectId | 发送者ID |
| senderType | string | 发送者类型:CUSTOMER/AGENT |
| content | string | 消息内容 |
| msgType | string | 消息类型:text/image/file |
| createdAt | number | 发送时间戳 |
| readAt | number | 读取时间戳(可选) |
| isSystem | boolean | 是否系统消息(默认false) |
1.4 客服工作统计表(web_chat_agent_stats)
| 字段 | 类型 | 说明 |
| ------------ | -------- | -------------------------- |
| _id | ObjectId | 统计ID |
| agentId | ObjectId | 客服ID |
| date | string | 统计日期(YYYY-MM-DD) |
| totalRooms | number | 总处理房间数 |
| avgResponseTime | number | 平均响应时间(秒) |
| customerSatisfaction | number | 客户满意度评分(1-5) |
| onlineTime | number | 在线时长(分钟) |
| createdAt | number | 创建时间戳 |
2 会话房间(Room)
| 字段 | 说明 |
| ------------ | -------------------------- |
| _id | roomId (ObjectId) |
| customerId | 顾客用户ID |
| customerName | 顾客昵称 |
| status | WAITING / SERVING / CLOSED |
| agentId | 为空表示未分配 |
| createdAt | 创建时间 |
| lastMessageAt| 最后消息时间 |
| closedAt | 关闭时间 |
-
顾客进入页面 → 立即
insertOne生成房间,状态 = WAITING。 -
客服接单 →
findOneAndUpdate({status:'WAITING'}, {$set:{status:'SERVING', agentId}})。 -
房间关闭 →
updateOne({status:'SERVING'}, {$set:{status:'CLOSED', closedAt: Date.now()}})。
3 客服在线状态
-
存储位置:进程内存
Map<agentId, {socketId, lastPing, currentRooms: string[]}>。 -
上线:客服登录后台 → 前端主动发
agent:login,服务端set(agentId, ...)。 -
心跳:WebSocket/Socket.IO 自带
ping/pong,或自定义setInterval每 25s。 -
掉线检测:
-
正常关闭页面/退出:前端
disconnect→ 服务端delete(agentId)。 -
网络异常:心跳超时 60s → 服务端
delete(agentId)。
-
只有"下线"才删除 ID;进入房间、房间结束都不影响在线集合。
4 排队与分配
-
顾客创建房间 → 自动加入 WAITING 队列。
-
客服后台实时获取
db.room.find({status:'WAITING'}).sort({createdAt:1})。 -
分配策略:
-
一对一:客服最大并发数 =1,接单后不再推送新房间。
-
一对多:客服表加
maxConcurrent字段,接单前校验当前服务数 < max。 -
负载均衡:按客服当前服务房间数量排序,优先分配给空闲客服。
-
技能匹配:根据房间类型和客服技能标签进行匹配(可选)。
-
-
并发冲突:使用 MongoDB 的
findOneAndUpdate条件锁,只有一个客服成功。
5 实时聊天
阶段划分
| 阶段 | 连接策略 | 说明 |
| ------- | --------------------- | -------------------------- |
| WAITING | 轮询 或 SSE | 仅刷新排队信息,低资源占用 |
| SERVING | WebSocket / Socket.IO | 全双工实时收发消息 |
流程
-
房间状态变为 SERVING → 服务端向顾客推送
room:assigned。 -
顾客端收到后建立 WebSocket 并
join(roomId)。 -
客服端同时
join(roomId)进入房间。 -
双方建立连接后,服务端推送历史消息。
-
双方消息:
-
前端
emit('msg:send', {...}) -
服务端
insertOne到messages集合 -
服务端
io.to(roomId).emit('msg:new', ...)→ 对面实时渲染
-
消息模型
| 字段 | 说明 |
| --------- | ----------------------- |
| _id | 消息ID (ObjectId) |
| roomId | 外键 |
| sender | CUSTOMER / AGENT |
| senderId | 发送者ID |
| content | 文本 |
| msgType | text / image / file |
| createdAt | 时间戳 |
| readAt | 读取时间 |
6 房间关闭与数据生命周期
-
关闭触发:
-
客服点击"关闭"
-
顾客超时无响应(定时任务:最后消息 >5 分钟)
-
-
动作:
-
room.status = CLOSED -
通知双方
room:closed
-
-
归档:
- 7 天后把 CLOSED 房间迁移到冷存,或加 TTL 索引。
7 错误处理与异常情况
网络异常处理
-
客服掉线:自动释放房间,重新进入WAITING状态
-
顾客掉线:保持房间状态,等待重连
-
消息发送失败:重试机制,最多3次
数据一致性
-
使用MongoDB事务确保房间状态和消息的一致性
-
定期清理僵尸房间(超过30分钟无活动的WAITING房间)
性能优化
-
消息分页加载,避免一次性加载过多历史消息
-
房间列表分页,避免大量数据影响性能
8 部署与横向扩展
| 场景 | 方案 |
| ----------- | ------------------------------------------------------------ |
| 单机/低并发 | 直接 npm run dev 或 npm start |
| 多实例 | 在线状态换成 Redis Set;消息队列可选 Redis Stream / RabbitMQ |
| 容器化部署 | Docker + PM2 进程管理 |
| Serverless | Vercel 不支持常驻 WS,需独立容器部署 chat-service |
9 关键索引(MongoDB)
-
room.status+createdAt(排队查询) -
message.roomId+createdAt(拉历史消息) -
room.agentId+status(客服工作台查询) -
room.customerId+status(顾客历史会话) -
message.senderId+createdAt(用户消息统计)
10 一句话总结
顾客进线即建房间排队 → 客服登录后心跳保活 → 接单时房间变 SERVING → 双方建立WebSocket连接 → 每句话直接落 MongoDB 并广播 → 关闭房间即结束。
本文由萧兮的博客原创发布,欢迎转载,转载务必保留原文链接。
萧兮的博客:https://www.20010515.xyz · 原文:https://www.20010515.xyz/posts/32909ced-d321-436c-9ba7-f62341bfffbc