☰
开源在线客服系统源码部署与调优实践指南
2026/9/26 20:11:49 网站建设 项目流程

简介:这是一份开源版在线客服管理系统的完整源码压缩包,面向需要搭建企业级客服平台或学习客服系统二次开发的开发者,可基于它实现网页端、移动端的多渠道对话管理、工单处理与客户信息管理,并可通过插件方式接入聊天机器人及数据报表功能。压缩包大小约7.78MB,平台未提供包内文件明细,具体文件总数暂未列出,但源码包通常包含后端框架、前端界面、数据库脚本及部署配置文件,便于本地运行和二次修改。该系统采用开源授权,具备成本低、可灵活定制、社区维护持续更新等优点,尤其适合中小团队快速搭建专属客服平台,也为研究实时通信、消息队列、工单流转等模块提供直接可运行的参考实现。目前已有467人学习下载,适合有Java/PHP/Node.js等任一后端基础的开发者结合官方文档边读源码边实践,能够帮助深入理解在线客服系统的整体架构与常见业务逻辑。

1. 开源的在线客服管理系统,解决的不只是聊天

很多团队第一次见到“在线客服管理系统源码.zip”这个压缩包,场景高度雷同:SaaS 客服产品用了一两年,客户数据沉淀在第三方平台,想自建一个能掌控数据的客服入口;另一类是课程或毕业设计,需要一套能完整跑起来的前后端源码。这个 zip 里装的是一套完整的开源在线客服管理系统,包含访客端、客服工作台、管理后台三块,建库、配置、启动,就能真正接待会话。它适合有 Java 和前端基础的开发者,用来学习、二次开发或内部部署;不适合完全不懂技术的运营直接开箱即用。我按自己落地这类系统的顺序,把架构、启动、调参和排错一次讲透。

2. 先看清架构:会话、消息和客服工作台的核心设计

在解压之前先看目录结构和核心类,是我拿到任何源码包的第一个习惯。在线客服系统的代码规模通常在几万行上下,硬读一遍不现实,只要抓住“三条链路、一个状态机”就能快速定位问题。三条链路分别是访客端到后端的消息链路、客服工作台的实时同步链路、管理后端的配置链路;一个状态机就是会话从创建到关闭的流转。把这几块在源码里找出来,后面启动和排错都会顺手得多。

2.1 访客端、客服端与管理后台各自负责什么

访客端是一个嵌在业务页面里的 JavaScript 插件。它只做两件核心的事:建立 WebSocket 连接,以及把消息的发送和接收事件绑定到聊天窗口上。访客端不需要关心历史会话列表、客服在线状态这些信息,多数开源实现甚至不向访客显示客服是否在线,只给出“当前会话”和“排队中”这种粗粒度状态。所以访客端代码通常很小,二次开发的重心也不会在这里。

客服工作台才是整个系统最重的前端。它要实时展示排队会话数、当前接待会话、访客来源页、访客历史消息、快捷回复、内部备注和会话转接。这里最容易出问题的不是业务逻辑,而是实时状态的一致性——同一时刻两个客服看到的排队数不一致,或者消息被重复推送到界面上,几乎都出在这条实时同步链路上。

管理后台相对简单:账号管理、客服分组、欢迎语、会话统计,基本都是增删改查。但有一个细节值得单独确认:会话统计的口径。有的系统统计“创建会话数”,有的统计“有效会话数”(至少有一来一回才算),口径不同,报表数据能差出三成。正式看板做出来之前,先搞清楚这套源码的统计口径到底在哪里实现的,用哪个字段作为计数依据。

2.2 会话状态机:等待、接待、关闭与转接

所有客服系统都可以简化成一个会话状态机,最常见的四个状态:等待接待、接待中、已关闭、已转接。后端一般用一个小整数存状态,0 等待,1 接待中,2 已关闭,3 已转接,具体定义各套源码略有差异,但流转逻辑完全一样。

访客点击“开始聊天”时,后端会先做一次查重:按 visitor_id 找有没有未关闭的会话,有就直接复用,没有才创建新会话。这个查重逻辑是不少系统翻车的源头——如果不查重,访客每次刷新页面都会产生一条新会话,排队列表会被空白会话刷屏。

客服接入会话时,不是点进聊天窗口就算接待,而是要做一次“抢锁”。抢锁的 SQL 通常长这样:

UPDATE conversation SET agent_id = #{agentId}, status = 1 WHERE id = #{conversationId} AND (agent_id IS NULL OR agent_id = #{agentId})

这个条件更新是乐观锁的典型用法。条件里那句 agent_id 为空或等于自己,决定了同一会话不会被两个客服同时接待。如果影响行数为 0,说明别人已经接走,界面要立刻刷新排队列表并提示“该会话已被其他客服接入”。

我见过不少二次开发把这条 SQL 改成不带条件的直接 update,结果客服一多就出现两个客服同时回同一个访客的情况。这是客服系统最尴尬的生产事故,没有之一。

会话关闭以后,消息表不再写入。但超时自动关闭是另一个麻烦:后端定时任务每五分钟扫一次 last_msg_at 超过阈值的会话,改成已关闭状态并推送一条系统消息。超时时间通常写在后台配置项,有的写死在 application.yml。这个值不是越短越好——太短会把正在打字但停顿较久的会话强关掉,访客还没等到回复,会话就再也找不回来了。

还有一个容易忽视的边界是转接。客服把会话转给另一个客服时,状态通常不是直接改成已关闭,而是先走到转接中,等目标客服接入后再恢复为接待中。源码里搜 transfer 或 turnOver 能定位到这条逻辑;如果转接后状态没有回到接待中,排队列表会短暂把这条会话显示为关闭,访客端会觉得被强行挂断。

2.3 核心数据表:会话表、消息表与关键索引

数据表设计不需要全部看懂,但三张核心表必须定位到:会话表、消息表、客服账号表。我以 MySQL 里最常见的表结构为例,把关键字段列出来,方便你拿到源码后按字段名反查实体类。

CREATE TABLE `conversation` ( `id` varchar(32) NOT NULL COMMENT '会话ID', `visitor_id` varchar(64) NOT NULL COMMENT '访客唯一标识', `agent_id` varchar(32) DEFAULT NULL COMMENT '当前接待客服ID', `status` tinyint NOT NULL DEFAULT 0 COMMENT '0等待 1接待中 2已关闭 3已转接', `channel` varchar(16) DEFAULT 'web' COMMENT '来源渠道:web/app/wechat', `last_msg_at` datetime DEFAULT NULL COMMENT '最后一条消息时间', `created_at` datetime NOT NULL, PRIMARY KEY (`id`), KEY `idx_status_lastmsg` (`status`, `last_msg_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客服会话表'; CREATE TABLE `chat_message` ( `id` bigint NOT NULL AUTO_INCREMENT, `conversation_id` varchar(32) NOT NULL COMMENT '所属会话', `sender_type` tinyint NOT NULL COMMENT '1访客 2客服', `sender_id` varchar(64) NOT NULL COMMENT '发送者ID,需配合sender_type判断身份', `content` text NOT NULL COMMENT '消息内容', `msg_type` tinyint DEFAULT 0 COMMENT '0文本 1图片 2文件 3系统消息', `extra` json DEFAULT NULL COMMENT '图片、文件的URL信息', `created_at` datetime NOT NULL, PRIMARY KEY (`id`), KEY `idx_conversation_time` (`conversation_id`, `created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天消息表';

这个建表方案里有几个值得注意的决策。sender_id 用 varchar 而不是 int,是因为访客 ID 常常不是自增数字,而是会话 ID 或 UUID 截断;要同时兼容客服账号 ID,只能用字符串。所以查历史消息时必须同时带上 sender_type,否则无法判断发送方身份。

会话表上的联合索引 (status, last_msg_at) 是排队列表和超时扫描的命脉。没有这个索引,客服一多之后排队页会越查越慢,尤其是状态集中在 0 和 1 时,索引区分度不高,优化只能靠这个联合索引。拿到源码后先确认这张表和这个索引是否存在。如果一行都没有,说明这套系统把消息存到了 NoSQL 或消息中间件里,排查问题时思路要跟着换。

消息表是按会话维度查询最频繁的表,时间久了会膨胀得很快。比较省事的做法是定期把 30 天前的消息归档到历史表,或者直接按 created_at 做分区。源码里如果已经带了归档任务,上线前记得把开关打开;没有的话,至少每天半夜跑一次清理任务,否则三个月后 chat_message 会变成整个库最大的表,拖慢所有会话历史查询。

3. 把 zip 源码落地:验包、配库、启动前后端

从压缩包到能跑的系统,中间隔着一道坎。很多人下载 zip 之后直接双击解压,打开项目就启动,报错了再回头看。我的习惯是解压前先花几分钟做验包和目录检查,能筛掉一大半“怎么启动就报错”的问题。

3.1 验包先行:zip伪加密、文件编码与目录完整性

开源项目打包成 zip 分发时,国内二次转发的人偶尔会给压缩包加一层密码,或者只把文件头的加密标志位置位、内容并没有真正加密,也就是常说的 zip 伪加密。如果解压时提示输入密码,而发布说明里没写密码,先用 7-Zip 看压缩包里每个文件的加密状态:

7z l source-open-source-online-customer-service.zip

输出列表里每个文件名后面如果带“+”号,表示该文件被加密。整个包都带加密但没有密码,大概率是伪加密或转发方刻意为之。伪加密的处理不是暴力破解,而是把加密标志位修掉:用 ZipCenOp 这类工具能一键清除伪加密标志,之后正常解压。真加密就放弃这个包,去搜项目原出处,别在不透明的包上浪费时间。

正常解压时,我一般直接在 Linux 上完成,而不是 Windows 双击。Windows 自带解压对中文目录名的编码处理经常出问题,解完一堆乱码目录,项目直接打不开:

unzip -O gbk -d ./kefu source-open-source-online-customer-service.zip

-O gbk 让 unzip 按 GBK 解码文件名,这是从 Windows 压缩的 zip 最常见的编码方式。解压完先核对目录结构,看是否有 backend(或 server)、frontend(或 web)、doc(文档和 SQL 脚本)三块:

find . -maxdepth 2 -type d | sort

缺 backend 只有前端,说明发的是半包;缺 doc 也能跑,但数据库初始化脚本得去源码里找。如果解压出来文件名仍然乱码,用 convmv 批量转一次编码:

convmv -f GBK -t UTF-8 -r --notest ./kefu

提示:做完这一步,目录名和配置文件里的中文注释就正常了。很多 Linux 下解压 Windows zip 后打不开项目,根因就在文件名编码上,不是代码有问题。

3.2 后端启动:建库、导入脚本、改四连接参数

在线客服系统的后端,最常见的是 Java Spring Boot、PHP 或 Python 实现,这套落地思路三种技术栈通用。以 Java 版为例,启动前先做三件事:创建数据库、导入初始化 SQL、修改配置。

mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS kefu_system DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p kefu_system < ./doc/sql/init.sql

建库时显式指定字符集是必须的。如果沿用 MySQL 默认字符集(老版本默认 latin1),导入带表情符号的初始化数据会直接报错或丢字符。这不是玄学,是客服系统线上最常见的乱码来源。

后端主配置在 application.yml(Spring Boot 项目)或 .env(PHP 项目)里。端口通常不用改,需要改的是四个连接参数:

spring: datasource: url: jdbc:mysql://127.0.0.1:3306/kefu_system?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root1234 redis: host: 127.0.0.1 port: 6379 database: 0

连接串里的 characterEncoding 要和建库字符集一致,否则中文写入时乱码;serverTimezone 设成 Asia/Shanghai,避免日志时间差 8 小时。Redis 是消息推送的中间层,很多在线客服系统用 Redis 的发布订阅做消息路由:后端写入消息后 publish 到频道,WebSocket 服务订阅频道再推给前端。Redis 连不上时,表现往往不是启动报错,而是消息发不出去,这一点容易误判成前端问题。

启动后端:

cd kefu/backend mvn spring-boot:run

看到日志输出 Started Application 且没有异常堆栈,先用 curl 做接口探活:

curl -s http://127.0.0.1:8080/api/health

返回 JSON 里包含 status: UP 或类似结构,说明后端起来了。如果端口冲突,先查谁占用再处理,而不是不断改端口绕开:

ss -lntp | grep 8080

同机部署多个 Java 服务时,端口占用是第一个遇到的坑,杀掉旧进程比改端口更可靠。

3.3 前端联调:访客端和工作台怎么连上后端

前端一般用 Vue 或 React,本地启动前最关键的是代理配置。开发时前后端端口不同,必须把 /api 和 /ws 的请求代理到后端地址,其中 WebSocket 代理是最容易漏的:

// vite.config.js export default defineConfig({ server: { port: 3000, proxy: { '/api': { target: 'http://127.0.0.1:8080', changeOrigin: true }, '/ws': { target: 'ws://127.0.0.1:8080', ws: true, changeOrigin: true } } } })

/ws 前缀必须和后端 WebSocket 端点保持一致,否则握手请求到不了后端,浏览器控制台会持续报 WebSocket connection failed。启动前端:

cd kefu/frontend npm install npm run dev

打开 http://localhost:3000 能进入登录页,用初始化脚本里内置的管理员账号登录。登录后客服工作台里如果能看到 WebSocket 状态从 connecting 变成 online,前后端链路就算通了。

联调之后再做一次完整冒烟:开一个无痕窗口访问访客端页面,发起会话,客服工作台这边能立刻看到新会话进入排队;客服接入并回复,访客窗口能收到。这一来一回验证的是会话创建、消息推送、消息落库三条链路,比任何单接口测试都有说服力。冒烟过了,再继续做参数调整。

4. 把系统调成可用状态:分流、推送与知识库参数

能跑通只是第一步。真正投入使用,决定客服体验的是三组参数:会话怎么分给客服、消息怎么推给两端、高频回答怎么沉淀。这三组分别藏在配置文件和后台设置里,逐一调过一遍,这套系统才算真正“可用”。

4.1 客服分流策略:轮询、空闲优先还是老客优先

会话分配是客服系统里最影响使用感受的部分。在源码里搜 dispatch 或 assign 关键词,能看到默认实现。三种策略是主流:

策略实现方式适用场景注意点
轮询按客服ID顺序分配客服接待能力接近有个别空闲但轮不到的情况
空闲优先按当前接待会话数最少分配客服负载不均依赖实时计数,计数不准就分错
老客优先按访客历史关联客服分配复购型业务需要历史关联表,前期数据少时效果一般

开源版本默认多半是轮询加一个简单约束:客服在线且未达到最大接待数。最大接待数在配置里常见字段名是 max_concurrent 或 maxSession,默认值 5 左右。这个值不是越大越好——一线客服同时接待 5 个会话已经是上限,超过这个数回复质量会明显下降。

如果把分配改成空闲优先,核心逻辑往往在一个 Service 类里。改造思路不复杂:查所有在线客服的当前会话数,按数量升序取第一个。但要注意并发问题:两个访客同时进入,都查到客服 A 最空闲,同时分配给他,接待数会瞬时超限。解决方法是给分配逻辑加原子条件更新,比如分配时同时把接待数 +1 作为 UPDATE 的 WHERE 条件:

UPDATE agent SET current_count = current_count + 1 WHERE id = #{agentId} AND current_count < max_concurrent

影响行数为 1 才算分配成功。这是二次开发最值得改的一个点,访客体会到的“等待时间”在这里影响最大。

4.2 WebSocket 参数:心跳、超时与离线消息补偿

消息推送层出问题,表面上都是“消息收不到”,但根因通常是三类:心跳间隔太长被网关断开、重连策略太简单引发连接风暴、离线消息补偿没有实现。一套稳的配置组合如下:

websocket: heartbeat-interval: 30 # 客户端心跳间隔,单位秒 idle-timeout: 90 # 服务端闲置超时,超过该值断开 reconnect-max-retries: 5 # 重连最大次数 offline-message-limit: 50 # 离线消息拉取上限

与配置配套的前端心跳和重连逻辑:

const proto = location.protocol === 'https:' ? 'wss://' : 'ws://' let retries = 0 function initWs() { const ws = new WebSocket(`${proto}${location.host}/ws/agent`) ws.onopen = () => { retries = 0 setInterval(() => ws.send(JSON.stringify({ type: 'ping' })), 30000) // 重连成功后强制刷新一次排队列表,避免列表停留在旧状态 fetch('/api/queues/list').then(res => res.json()).then(renderQueue) } ws.onclose = () => { if (retries >= 5) return retries++ setTimeout(initWs, 1000 * retries) // 指数退避,1s/2s/3s/4s/5s } } initWs()

心跳 30 秒、服务端空闲超时给 90 秒,是相对可靠的搭配。心跳太短(5 秒)会让网关日志刷满 ping;心跳太长(60 秒以上)会遇到云负载均衡 60 秒空闲连接回收,直接断线。重连用指数退避而不是固定秒数,避免整批客服同时断线后重连请求把服务器打满。

离线消息补偿是另一个容易漏的点。客服工作台切到后台再切回来,浏览器可能已经换了 WebSocket,这段时间的消息会丢。标准做法是前端重连成功后带上 lastMessageId,后端查询比它更新的消息补推回来。源码里搜 pullOfflineMessages 或 loadHistoryAfter,确认前端有没有调用这步;没调用就接入重连逻辑里。

注意:后端如果同时支持访客端和客服端的 WebSocket,两边的主题或端点通常不一样,别把客服端的离线补偿接到访客通道上,那样会拉回一堆无关会话。

4.3 知识库与快捷回复:高频回答的沉淀

快捷回复是客服系统里最实用的功能。没有这个模块的话,二次开发的第一优先级就是它。快捷回复的数据结构很简单:一个分组、一个标题、一段内容。高频场景通常集中在发货时间、退换货政策、账号找回三块,做成带占位变量的模板,比纯文本通用得多:

CREATE TABLE `quick_reply` ( `id` int NOT NULL AUTO_INCREMENT, `group_id` int NOT NULL COMMENT '分组ID', `title` varchar(100) NOT NULL, `content` text NOT NULL, `sort` int DEFAULT 0 COMMENT '排序值', `created_at` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='快捷回复表';

如果这套系统把快捷回复缓存到了 Redis 而不是纯 MySQL,记得给重启后的缓存做预热,否则客服系统刚启动那几分钟快捷回复面板是空的,客服只能手动打字补。

另外要分清快捷回复和知识库不是同一件事:快捷回复是客服手动选的模板,知识库是访客自助查询的入口。有些入门实现把两者合并成一张“自动回复”表,访客端体验会打折扣——自动回复覆盖不了多少问题,反而把找人工客服的入口藏了起来。我的偏好是访客端保留尽量精简的自助问答,人工入口永远在第一位。

5. 源码部署避坑清单:五条实测踩坑记录

下面五条按出现频率排序,每一条都是实际部署里反复见到过的。现象、原因、解决三段式,方便直接对照。

5.1 现象:客服端收到的消息全是乱码

中文在访客端正常,客服端显示乱码,或数据库里存的直接是乱码。

原因:建库时用了默认字符集,老版本 MySQL 默认 latin1;或者连接串没带 characterEncoding=utf8mb4。客服系统必然要处理 emoji 和特殊符号,用 utf8 都不够,必须 utf8mb4。

解决:把库、表、连接串三层对齐成 utf8mb4。

mysql -uroot -p -e "ALTER DATABASE kefu_system CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p -e "ALTER TABLE conversation CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"

改完把连接串的 characterEncoding 也改成 utf8mb4,重启后端,发一条表情消息验证。已经产生的乱码数据无法自动恢复,只能清掉重测,所以初始化时就把字符集钉死,事后补救很被动。这也是免费开源源码包和商业发行版之间差异最明显的地方。

5.2 现象:客服工作台登录后一直显示连接中

访客端能发消息,客服端连接状态始终是 connecting,或者每隔几分钟断一次。

原因:WebSocket 握手被前置的 Nginx 拦住了。Nginx 默认代理配置不转发 Upgrade 头,WebSocket 升级请求到不了后端。最常见是只代理了 /api 而漏了 /ws。

解决:在 Nginx 里为 WebSocket 端点单独加 location:

location /ws { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 120s; }

关键在 proxy_set_header 两行。proxy_read_timeout 要大于服务端空闲超时,否则即使心跳正常,长连接也会被 Nginx 默认的 60 秒超时掐断。

5.3 现象:压测时一高并发就掉线,日志全是连接超时

小流量一切正常,客服一多或压测一上,WebSocket 批量断开,后端日志出现 Connection timed out。

原因:线上部署沿用开发态默认线程数。内嵌应用服务器默认最大线程数 200,每个 WebSocket 连接占用一个线程时,200 个客服几乎打满;再加消息推送线程池的消耗,必然超时。开源版本为方便本地启动会刻意调低这些值,直接拿去生产就是翻车现场。

解决:把线程池和连接数显式调大,并同步检查 Redis 连接池上限。

server: tomcat: max-threads: 800 max-connections: 2000 websocket: max-session-idle-timeout: 90000

推送链路依赖 Redis 订阅时,Redis 连接池太小会表现为消息延迟而不是报错,排查时注意。改完配置先压测观察线程和活跃连接数,别一开始就调得过大——线程太多反而上下文切换频繁,性能下降。

5.4 现象:访客消息偶尔丢一条,刷新后又出现

访客发送时显示失败,刷新后消息出现;或者客服收到的顺序和访客发送顺序不一致。

原因:Redis 与 MySQL 双写的时序问题。常见实现是先写 Redis 再异步落库 MySQL,Redis 写入成功就返回“已发送”,但异步落库一旦失败,消息虽然还在缓存里,服务一重启就消失在历史记录中。

解决:把 MySQL 落库作为主链路。第一优先是把落库做成同步事务,Redis 只做热数据缓存。改动成本高的话,至少增加一个定时对账任务,把超过 5 分钟仍在 Redis 但未落库的消息补写一遍。排查时先用 SQL 确认消息到底进没进库:

SELECT COUNT(*) FROM chat_message WHERE created_at > NOW() - INTERVAL 10 MINUTE;

如果库里消息数和实际会话数量对不上,链路断点在落库,不在前端。

5.5 现象:队列数字不动,客服空闲但新会话没分配

访客点击聊天显示排队中,客服工作台的待接入列表始终不刷新。

原因:队列列表通常同时依赖 WebSocket 推送和轮询接口两条通道。WebSocket 重连成功但列表没有重新拉取,界面留在旧状态;或者分配逻辑只在收到新消息时触发,访客进来没有发言,会话就一直在等待。

解决:在 WebSocket 的 onopen 回调里重新拉取一次队列接口,同时给分配逻辑补一个定时扫描:

ws.onopen = () => { fetch('/api/queues/list') .then(res => res.json()) .then(renderQueue) // 重连后强制刷新待接入列表 }

后端如果是事件驱动分配,加一个 1 分钟周期的定时扫描任务,扫所有 status=0 且超过 10 秒未分配的会话,尝试执行分配。这能覆盖访客不发消息直接等待的边界场景。

6. 生产上线前的十分钟自检:验证链路是否真的通了

正式上线前,我习惯把巡检写成一个可以直接执行的脚本,只验证三件事:HTTP 后端活着、WebSocket 握手能通、消息真的落库了。十分钟内跑完,比任何功能测试都直接。

# 1. 后端健康检查 curl -s http://127.0.0.1:8080/api/health | grep -q '"status":"UP"' && echo "backend ok" # 2. WebSocket 握手检查,期望返回 101 Switching Protocols curl -i -s http://127.0.0.1:8080/ws/agent \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \ -H "Sec-WebSocket-Version: 13" | head -5 # 3. 消息落库校验,统计最近 5 分钟入库消息数 mysql -uroot -p -e "SELECT COUNT(*) AS msg_count FROM chat_message WHERE created_at > NOW() - INTERVAL 5 MINUTE;"

前端挂在 https 下时,第 2 步协议要换成 wss 并且加 -k 跳过证书校验。如果握手没有返回 101,按第 5.2 的 Nginx 配置排查。

这三个检查点之外,我还会实际开一次访客窗口和客服工作台,发一条测试消息,然后去数据库里把这条消息查出来。跑过太多客服系统后,我最大的教训是:消息走得到数据库,才算真的走通。界面上看到“已发送”而没有落库的消息,在所有场景里都等于没有这条路。每次遇到解释不清的玄学现象,第一选择都是去库里查这一条消息在不在,而不是猜前端有没有 bug。

这套从验包、启动、调参到上线的流程,是我遇到这类开源在线客服系统时固定走的一条路。按这个顺序来,一次跑通的概率会高很多,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询