
简介面向需要自建在线客服系统的开发者、中小企业技术团队与运维人员这份压缩包提供一套开源在线客服管理系统源码可用于网页端实时咨询、客服工作台、常见问题自动回复、服务记录与数据统计等场景并支持按业务需求二次开发。压缩包约7.78MB文件总数与主要文件类型明细上游暂未提供具体目录和源码构成需解压后确认。已有472人浏览学习说明其在开源客服系统选型与源码研究中受到一定关注。读者可了解客服系统前后端模块的组织方式围绕多渠道对话管理、客户信息查看、工单处理、报表分析、CRM/ERP对接、多语言与权限安全等方向做改造也可参考开源项目的定制、部署与维护思路用于课程实践、企业内部客服平台原型或功能验证适合具备一定开发基础、希望以较低成本自建客服系统并持续迭代的团队参考。1. 开源客服系统源码不是装完就能用先把三条链路摸清拿到「源码-开源版在线客服管理系统.zip」这类包很多人第一反应是找安装说明把它当普通插件往站点里一塞。真跑起来才发现它不是插件式小模块而是一套完整软件访客端、坐席工作台、会话分配、消息通道、权限和统计都在里面。适合已有 Web 业务、想自己掌控客服数据的人也适合要二次开发工单和渠道接入的团队。先别急着改界面。先把目录、数据表和长连接入口摸清这三处决定这套源码能不能接手、上线后会不会掉消息。后面按拆包选型、最小闭环、长连接可靠性、避坑、压测五步走每一步都能落到命令和参数上。2. 拆包与选型从目录结构判断这套源码能不能接手解压后的第一件事不是找 install而是确认它是独立软件还是插件式模块。独立软件一般有独立入口、独立数据库和常驻进程插件式模块往往只有几个控制器和一个挂载点消息通道依赖宿主系统。客服系统最怕的是“看着能装实际长连接没入口”所以先把目录翻一遍比看宣传页有用。2.1 先看入口和依赖三分钟判断这套源码能不能接手我一般先把压缩包解到独立目录避免和现有站点混在一起。然后找依赖清单、SQL 文件、启动脚本和 README这四样决定后面怎么配环境。没有依赖清单的包不一定不能用但维护成本会高没有 SQL 文件的包要重点看它是否依赖宿主库没有启动脚本却声称支持实时聊天就要怀疑消息通道是不是轮询。位置先看什么判断点composer.json / package.json / pom.xml依赖和脚本能否锁定依赖有没有长连接服务database 或 sql 目录建表脚本是否有 message、session、agent_statusbin 或 server 目录启动入口网关是不是独立进程config 目录数据库和缓存配置是否硬编码账号密码public 目录前端资源是源码还是编译产物mkdir -p /opt/chat-src cd /opt/chat-src unzip -q 源码-开源版在线客服管理系统.zip -d app find app -maxdepth 3 -type f \( -name composer.json -o -name package.json -o -name pom.xml -o -name *.sql -o -name README* \) -print du -sh app这段脚本做三件事把源码解到独立目录、找出依赖清单和 SQL、看压缩包解压后的大致体积。-d app指定目标目录-maxdepth 3防止在深层依赖里翻太久du -sh用来看前端资源是不是已经把编译产物打进来了。如果find只找到一个 README没有依赖清单也没有 SQL先别急着配环境优先确认它是不是半成品。接着看入口文件。客服系统通常有三类入口后台管理入口、访客端入口、长连接网关入口。后台和访客端一般是 HTTP 入口网关入口会在命令行启动脚本或独立端口中出现。把这三类入口对上后面排查“消息为什么不推”才有落点。cd /opt/chat-src/app grep -R listen\|websocket\|socket\|ws:// -n --include*.php --include*.js --include*.yaml --include*.conf . | head -n 60grep -R递归找关键字--include限定文件类型head -n 60只看前 60 行避免被前端依赖刷屏。这里不是让你改代码而是确认网关监听端口、握手路径和前端连接地址是否在同一个配置体系里。常见翻车点是前端写死ws://线上是 HTTPS浏览器直接拦掉混合内容或者网关监听0.0.0.0:9502反向代理却转发到/ws路径对不上。2.2 技术栈匹配PHP、Node、Java 三种常见形态的取舍客服系统的读写比例很偏会话和消息写得多坐席列表和统计读得多长连接还要维持大量空闲连接。PHP 常见形态是 Web 后台加一个常驻进程网关优点是部署简单、招人容易缺点是网关和业务代码容易分成两套Node 做长连接更自然事件循环适合大量 IO但 CPU 密集的统计任务要另拆Java 适合已有权限体系和组织架构的团队代价是启动和调优更重。判断技术形态不用读全量代码看依赖清单和启动脚本就够。下面这段命令用来找“谁在监听端口”和“谁在维护连接”。cd /opt/chat-src/app grep -R new Server\|createServer\|listen(\|bind(\|swoole\|eventloop -n --include*.php --include*.js --include*.java . | head -n 40 find . -maxdepth 2 -type f -name *.sh -o -name *.bat | sortgrep找服务端监听和事件循环关键字find找启动脚本。参数上不用追求全量匹配先看命中位置在bin/、server/还是普通控制器里。如果监听逻辑散落在多个控制器说明消息通道和业务耦合较重后面做多实例会麻烦如果只有一个入口脚本改造点相对集中。选型时别只看语言先看三个边界消息是否落库、坐席状态是否有过期机制、网关是否支持多实例共享会话。三个都满足技术栈顺眼就能用缺一个后面就要补。常见做法是 Web 层只管 HTTP 接口和后台页面网关层只做连接维护和消息转发消息落库由业务接口完成网关通过队列或 Redis 订阅拿推送内容。这样即使网关重启历史消息也不丢。2.3 数据模型先画清楚会话、消息、坐席状态三张核心表界面可以后改表结构一开始就要看明白。客服系统的最小数据模型绕不开三张表会话表记录访客和坐席的绑定关系消息表记录每条消息的归属和顺序坐席状态表记录在线、忙碌、最大接待数和最后心跳。原包表名可能不一样但字段职责跑不掉。CREATE TABLE cs_session ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, visitor_id BIGINT UNSIGNED NOT NULL, agent_id BIGINT UNSIGNED DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0排队 1进行中 2已关闭, created_at DATETIME NOT NULL, closed_at DATETIME DEFAULT NULL, KEY idx_status_created (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE cs_message ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, session_id BIGINT UNSIGNED NOT NULL, sender_type TINYINT NOT NULL COMMENT 1访客 2坐席 3系统, sender_id BIGINT UNSIGNED NOT NULL, client_msg_id VARCHAR(64) NOT NULL, seq BIGINT UNSIGNED NOT NULL, content TEXT NOT NULL, created_at DATETIME NOT NULL, UNIQUE KEY uk_client_msg (client_msg_id), KEY idx_session_seq (session_id, seq) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE cs_agent_status ( agent_id BIGINT UNSIGNED PRIMARY KEY, status TINYINT NOT NULL COMMENT 0离线 1在线 2忙碌, max_sessions INT NOT NULL DEFAULT 5, last_heartbeat DATETIME NOT NULL, version INT NOT NULL DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;cs_session.status用 0、1、2 表示排队、进行中、已关闭比字符串好索引cs_message.client_msg_id加唯一索引用来挡前端重发和网关重投seq是会话内递增序号客户端补拉时按它做游标不要拿全局自增 ID 跨会话排序。cs_agent_status.last_heartbeat和version一起用前者判断在线后者处理多端登录覆盖。如果原包表名不是这套别直接改表名先做映射层。常见误用是看到字段不够就加字段结果升级源码时冲突。我的做法是保留原表新增扩展表或视图把差异收在一处。后面第 3 章的最小闭环也按这三张表跑换包时只需替换映射。3. 跑通最小闭环数据库、坐席、会话、消息四条链路最小闭环不是“后台能登录”而是访客发消息、进入排队、分配到坐席、坐席回复、访客收到、会话关闭。这五步里任何一步只在前端模拟上线后都会变成“看起来能用实际掉消息”。先用命令行和接口把闭环跑通再开浏览器调界面排查范围会小很多。3.1 环境与配置把数据库和 Redis 连接先跑通先建库、导表、建应用账号。不要用 root 跑应用也不要把密码写进仓库。下面命令里的库名、用户名和密码都要替换成自己的导入前先看 SQL 文件里有没有DROP TABLE有的话确认是不是空库。mysql -uroot -p -e CREATE DATABASE chat DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p chat database/install.sql mysql -uroot -p -e CREATE USER chat_app127.0.0.1 IDENTIFIED BY change_me; GRANT SELECT,INSERT,UPDATE,DELETE ON chat.* TO chat_app127.0.0.1; FLUSH PRIVILEGES;DEFAULT CHARACTER SET utf8mb4是为了存表情和特殊字符应用账号只给增删改查不给建表权限127.0.0.1限制来源比%安全。导入后别急着开页面先用一段 PHP 脚本检查数据库和 Redis 是否真的连上。密码从环境变量读不写死在代码里。?php $config [ db [host 127.0.0.1, port 3306, name chat, user chat_app, pass getenv(CHAT_DB_PASS)], redis [host 127.0.0.1, port 6379, db 0], ws [listen 0.0.0.0:9502, heartbeat 30], ]; $pdo new PDO( mysql:host{$config[db][host]};port{$config[db][port]};dbname{$config[db][name]};charsetutf8mb4, $config[db][user], $config[db][pass], [PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION] ); echo $pdo-query(SELECT 1)-fetchColumn() 1 ? db ok\n : db fail\n; $redis new Redis(); $redis-connect($config[redis][host], $config[redis][port]); $redis-select($config[redis][db]); echo $redis-ping() ? redis ok\n : redis fail\n;这段脚本只做连通性检查PDO 用异常模式连不上会直接抛错Redis 用ping看返回。ws.heartbeat先设 30 秒后面第 4 章会按反向代理超时再调。如果这里就失败先看端口、防火墙和账号权限不要继续改业务代码。常见坑是 MySQL 只监听了127.0.0.1应用在容器里却连localhost两边看到的不是同一个地址。3.2 坐席与访客两端一个最小会话的创建、分配、关闭会话分配最容易写成“查一个在线坐席就更新”并发一上来就重复分配。下面这段 PHP 用事务和行锁把分配动作收在一个事务里SKIP LOCKED让多个请求不会卡在同一行。旧版数据库不支持这个语法时改用 Redis 原子队列或单独锁别硬套。?php function assignAgent(PDO $pdo, int $sessionId): ?int { $pdo-beginTransaction(); try { $stmt $pdo-prepare( SELECT agent_id FROM cs_agent_status WHERE status 1 AND last_heartbeat DATE_SUB(NOW(), INTERVAL 90 SECOND) ORDER BY max_sessions ASC, agent_id ASC LIMIT 1 FOR UPDATE SKIP LOCKED ); $stmt-execute(); $agentId $stmt-fetchColumn(); if (!$agentId) { $pdo-commit(); return null; } $pdo-prepare(UPDATE cs_session SET agent_id ?, status 1 WHERE id ? AND status 0) -execute([$agentId, $sessionId]); $pdo-commit(); return (int)$agentId; } catch (Throwable $e) { $pdo-rollBack(); throw $e; } }分配条件里status 1表示在线last_heartbeat90 秒内算有效max_sessions小的优先是为了让接待量低的坐席先接。FOR UPDATE SKIP LOCKED避免两个请求同时选中同一个坐席UPDATE ... AND status 0是第二道保险只有排队中的会话才能被分配。返回null表示没有可用坐席调用方要把会话留在排队状态别把状态改成进行中。创建会话和关闭会话可以先用 SQL 验证。创建时只写访客和排队状态分配成功后再更新坐席关闭时同时写closed_at方便后面统计会话时长。INSERT INTO cs_session (visitor_id, status, created_at) VALUES (1001, 0, NOW()); UPDATE cs_session SET status 2, closed_at NOW() WHERE id 1 AND status 1;第一条插入排队会话第二条只允许进行中的会话关闭重复关闭不会覆盖时间。参数上注意visitor_id要来自真实访客标识不要用时间戳凑关闭条件带status 1能挡住重复请求。跑完这两步再用后台接口查一次会话详情确认坐席、状态和时间都对得上。3.3 消息落库与推送先保证不丢再谈实时消息链路我一般按“先落库、再入队、后推送”的顺序写。先落库保证刷新页面能查到入队保证网关能异步推推送失败还能重试。下面这段函数把消息写入和 Redis 队列放在一个流程里client_msg_id唯一索引挡重复seq按会话递增。?php function appendMessage(PDO $pdo, Redis $redis, int $sessionId, int $senderType, int $senderId, string $clientMsgId, string $content): array { $pdo-beginTransaction(); try { $seqStmt $pdo-prepare(SELECT COALESCE(MAX(seq), 0) 1 FROM cs_message WHERE session_id ? FOR UPDATE); $seqStmt-execute([$sessionId]); $seq (int)$seqStmt-fetchColumn(); $ins $pdo-prepare( INSERT INTO cs_message(session_id, sender_type, sender_id, client_msg_id, seq, content, created_at) VALUES(?, ?, ?, ?, ?, ?, NOW()) ); $ins-execute([$sessionId, $senderType, $senderId, $clientMsgId, $seq, $content]); $id (int)$pdo-lastInsertId(); $pdo-commit(); $redis-rPush(chat:push:queue, json_encode([ session_id $sessionId, seq $seq, client_msg_id $clientMsgId, ])); return [id $id, seq $seq]; } catch (Throwable $e) { $pdo-rollBack(); throw $e; } }client_msg_id由前端生成同一消息重发时必须复用同一个值sender_type用 1、2、3 区分访客、坐席、系统FOR UPDATE锁住当前会话的序号计算避免两条消息拿到同一个seq。Redis 队列只放推送需要的轻量字段不放整段内容网关消费时再按 ID 查库。这样即使队列丢一条也能用补拉接口补回来。验证时别只看页面。用同一个client_msg_id连发两次第二次应该报唯一键冲突再查seq是否连续SELECT session_id, seq, client_msg_id, created_at FROM cs_message WHERE session_id 1 ORDER BY seq ASC;如果seq断了先看事务是不是提交失败再看是不是有别的写入路径没走这个函数。常见误用是后台“快捷回复”直接拼 SQL 插消息绕过序号计算最后补拉时顺序全乱。所有写消息的入口都收口到一个函数后面加审计和敏感词也方便。4. 长连接与消息可靠性WebSocket 网关的心跳、重连与补拉长连接是客服系统最容易变成黑匣子的部分页面显示在线消息却收不到网关日志没报错坐席状态却一直挂着。排查这类问题先把网关启动、心跳、重连、补拉四件事拆开每一件都用可观测的参数落下来别靠“刷新一下试试”。4.1 网关进程怎么起端口、心跳、鉴权三件事不同源码的网关入口不一样常见在bin/或server/下。先看 README 和启动脚本再用命令行起进程、查端口、看日志。下面命令里的入口文件要按实际包替换-d表示常驻端口 9502 只是示例。cd /opt/chat-src/app php bin/ws-gateway.php start -d ss -lntp | grep 9502 tail -f runtime/ws.logstart -d让进程在后台跑ss -lntp确认端口真的监听了tail -f看握手和断开日志。如果端口没起来先看入口文件是否存在、PHP 扩展是否装齐、日志目录有没有写权限。别在nohup和自带守护之间来回切先固定一种启动方式后面做进程守护才不混乱。反向代理要把 WebSocket 的升级头带过去。下面配置放在站点配置里路径/ws要和前端连接地址一致。location /ws { proxy_pass http://127.0.0.1:9502; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_read_timeout 120s; proxy_send_timeout 120s; }proxy_http_version 1.1和Upgrade、Connection是 WebSocket 握手必需proxy_read_timeout 120s要大于心跳间隔否则代理会先断开Host透传是为了网关做域名校验。鉴权别只放在前端握手时带 token网关校验后再允许订阅会话。token 放查询串容易进日志常见做法是放握手头或首帧认证消息。4.2 心跳与重连把“在线”判断从玄学变成可计算前端心跳负责告诉网关“我还活着”服务端心跳负责把坐席状态写成可查询的数据。两边间隔要配合心跳太密浪费连接太疏会被代理判死。我一般让客户端 25 秒发一次 ping服务端 TTL 设 90 秒反向代理读超时 120 秒三层依次放宽。const wsUrl (location.protocol https: ? wss:// : ws://) location.host /ws; let ws null; let heartbeatTimer null; let reconnectDelay 1000; function connect() { ws new WebSocket(wsUrl); ws.onopen () { reconnectDelay 1000; clearInterval(heartbeatTimer); heartbeatTimer setInterval(() { if (ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ type: ping, ts: Date.now() })); } }, 25000); }; ws.onmessage (ev) { const msg JSON.parse(ev.data); if (msg.type pong) return; if (msg.type message) renderMessage(msg.data); }; ws.onclose () { clearInterval(heartbeatTimer); setTimeout(connect, reconnectDelay); reconnectDelay Math.min(reconnectDelay * 2, 30000); }; } function renderMessage(data) { // 按 seq 去重后插入消息列表 const box document.getElementById(chat-list); if (!box || box.querySelector([data-seq${data.seq}])) return; const item document.createElement(div); item.dataset.seq data.seq; item.textContent data.content; box.appendChild(item); } connect();25000毫秒发一次 ping小于代理的 120 秒reconnectDelay从 1 秒开始翻倍最多 30 秒避免断网时疯狂重连onmessage里按seq去重防止补拉和实时推送重复渲染。重连成功后不要只等推送要主动调用补拉接口把断线期间的消息拿回来。服务端收到 ping 后要同时刷新 Redis 和数据库。只写内存进程重启就丢只写数据库高频心跳会把库压出热点。常见做法是 Redis 存短 TTL 在线状态数据库按较低频率落last_heartbeat。?php // 收到 ping 后刷新坐席在线状态TTL 要大于心跳间隔 $redis-setex(agent:online:{$agentId}, 90, json_encode([ts time(), status 1])); $pdo-prepare(UPDATE cs_agent_status SET last_heartbeat NOW(), version version 1 WHERE agent_id ?) -execute([$agentId]);setex的 90 秒是过期时间超过这个时间没有新心跳就自动离线数据库更新带version 1多端登录时可以比较版本决定谁生效。参数上别把 TTL 设得比心跳间隔还短否则坐席会反复上下线也别设几小时否则人走了状态还挂着。4.3 离线补偿消息 seq、ack、补拉接口实时推送只能保证“在线时收到”断线、切网、网关重启都会漏。补拉接口按会话和seq取增量客户端保存本地最大seq重连后带上它请求。不要用时间戳做游标同一秒多条消息会乱也不要用全局自增 ID 跨会话比较。-- 客户端带上本地最大 seq只取增量 SELECT id, session_id, seq, sender_type, sender_id, content, created_at FROM cs_message WHERE session_id ? AND seq ? ORDER BY seq ASC LIMIT 100;session_id和seq走联合索引idx_session_seqLIMIT 100防止一次拉太多。客户端收到后按seq排序去重再把本地最大seq更新到返回结果的最大值。如果返回空说明没有增量如果返回满 100 条继续用新的最大seq拉下一页。ack 机制用来标记“客户端已确认收到”。服务端可以记录每个会话的最后 ackseq超过一定时间没 ack 的消息进入重投队列。重投时仍然带原client_msg_id客户端靠唯一键和seq去重。常见坑是服务端只重投不记录次数网络差的时候同一条消息反复推前端又没去重用户看到满屏重复。补拉和重投共用一套seq体系问题就能收敛。5. 上线避坑五个只在真实会话里暴露的故障本地跑通不代表线上能用。客服系统的故障往往不在主流程而在状态、并发和超时这些边角。下面五条是我在模拟项目里反复见到的按“现象、原因、解决”拆开写方便对照日志排查。5.1 坐席状态永远在线心跳写了内存却没落库现象是后台显示某个坐席一直在线实际人已经下班新会话还往他身上分配。原因通常有两个心跳只更新了 Redis没有写数据库或者数据库有last_heartbeat但分配查询没带时间条件。还有一种情况是多端登录旧连接的关闭事件把新连接的状态覆盖了。解决分三层。写入层用 Redis TTL 加数据库低频落库TTL 取心跳间隔的三倍左右查询层分配坐席时必须带last_heartbeat时间条件多端登录用version或连接 ID 做比较旧连接关闭时先校验再更新。排查时直接查最近心跳和 Redis TTL别只看页面上的绿点。5.2 消息重复或乱序前端重发撞上网关重投现象是同一句话出现两次或者坐席先看到第二条再看到第一条。原因可能是前端超时重发但没复用client_msg_id也可能是网关推送失败后重投消息表却没有唯一约束。乱序则多半是seq计算没加锁两个请求拿到同一个序号或者客户端按到达时间排序而不是按seq排序。解决是把client_msg_id唯一索引作为最后一道闸所有写消息入口收口到一个函数seq在事务里按会话取最大值加一客户端渲染前按seq排序并去重。排查时查cs_message里同一client_msg_id的记录数再看seq是否有重复。重复不可怕怕的是没有幂等键。5.3 会话分配不均轮询没排除忙线和离线现象是某个坐席同时接十几个会话另一个坐席空闲。原因常见于分配查询只按status 1过滤没看当前进行中会话数或者用了轮询但没排除离线坐席还有一种是锁粒度太大并发时大家都抢到同一个坐席。解决是把坐席状态、最大接待数、当前会话数放在一次查询里按接待量升序选人分配更新带status 0条件失败就重试或进排队高并发下用 Redis 原子队列或数据库行锁别在应用层用文件锁。排查时看分配日志里的候选坐席列表确认忙线和离线有没有被排除。5.4 历史消息越翻越慢message 表缺联合索引现象是刚开始聊天很快历史消息到几千条后打开会话要等几秒。原因通常是cs_message只有主键索引查询按session_id过滤再按时间排序时走了全表扫描或者消息内容存在大字段里列表查询把整段内容都拉出来。解决是加(session_id, seq)联合索引列表查询只取需要的字段内容大的附件单独存表或对象存储分页用seq ?的游标不要用大偏移量LIMIT 10000, 20。排查时开慢查询日志看rows_examined是不是远大于返回行数。索引不是越多越好消息表写多索引多了插入也会慢。5.5 WebSocket 频繁断连反代超时和心跳间隔打架现象是页面每隔一两分钟重连一次日志里能看到 1006 关闭。原因可能是反向代理proxy_read_timeout小于心跳间隔代理先把空闲连接掐了也可能是负载均衡没有会话保持重连落到没有该连接状态的实例还有 HTTPS 页面连了ws://浏览器直接拦截。解决是让代理读超时大于心跳间隔心跳间隔小于超时的一半多实例时把连接状态和订阅关系放共享存储或者做一致性路由前端根据页面协议自动选ws://或wss://。排查时看代理错误日志和网关握手日志的时间差能快速判断是谁先断的。6. 压测与验收从 50 并发到 500 并发的扩容技巧压测我一般分两层先压消息写入接口再压长连接广播。两层混在一起压只能看到总 QPS看不出瓶颈在数据库还是在网关。写入层重点看唯一键冲突、事务等待和seq计算锁长连接层重点看握手耗时、心跳丢失和内存增长。# 200 并发压消息写入先看落库和幂等不经过网关 seq 1 200 | xargs -P 200 -I{} curl -s -o /dev/null -w %{http_code}\n \ -X POST http://127.0.0.1/api/message \ -H Content-Type: application/json \ -d {session_id:1,client_msg_id:bench-{},content:load-test}xargs -P 200控制并发client_msg_id每条不同避免把幂等冲突算成失败输出只看状态码200 是成功409 可能是重复500 要看应用日志。压完查一次seq是否连续、有没有重复client_msg_id再决定要不要加锁或改队列。并发量先看指标常见瓶颈处理方向50写入耗时、错误率单条 SQL 没索引加联合索引收口写入口200事务等待、连接数行锁竞争、连接池不足缩短事务调连接池500网关内存、心跳丢失广播放大、代理超时拆频道调心跳和超时验收时别只看平均值看 95 分位和错误率。我一般要求消息写入 95 分位在可接受范围内重复消息为 0断线重连后补拉不丢消息。压测数据要清库或打标记别把测试消息混进真实会话。从那以后我每次拿到客服系统源码都先把client_msg_id唯一索引和网关心跳参数单独验一遍再谈界面。希望帮到你。本文还有配套的精品资源点击获取