无 Redis 实时客服系统实现思路(Fastify + MongoDB + Socket.IO)

无 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 排队与分配

  1. 顾客创建房间 → 自动加入 WAITING 队列。

  2. 客服后台实时获取 db.room.find({status:'WAITING'}).sort({createdAt:1})

  3. 分配策略:

    • 一对一:客服最大并发数 =1,接单后不再推送新房间。

    • 一对多:客服表加 maxConcurrent 字段,接单前校验 当前服务数 < max

    • 负载均衡:按客服当前服务房间数量排序,优先分配给空闲客服。

    • 技能匹配:根据房间类型和客服技能标签进行匹配(可选)。

  4. 并发冲突:使用 MongoDB 的 findOneAndUpdate 条件锁,只有一个客服成功。


5 实时聊天

阶段划分

| 阶段 | 连接策略 | 说明 |

| ------- | --------------------- | -------------------------- |

| WAITING | 轮询 或 SSE | 仅刷新排队信息,低资源占用 |

| SERVING | WebSocket / Socket.IO | 全双工实时收发消息 |

流程

  1. 房间状态变为 SERVING → 服务端向顾客推送 room:assigned

  2. 顾客端收到后建立 WebSocket 并 join(roomId)

  3. 客服端同时 join(roomId) 进入房间。

  4. 双方建立连接后,服务端推送历史消息。

  5. 双方消息:

    • 前端 emit('msg:send', {...})

    • 服务端 insertOnemessages 集合

    • 服务端 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 devnpm 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