
简介这是一份开源版在线客服管理系统的完整源码压缩包面向需要搭建企业级客服平台或学习客服系统二次开发的开发者可基于它实现网页端、移动端的多渠道对话管理、工单处理与客户信息管理并可通过插件方式接入聊天机器人及数据报表功能。压缩包大小约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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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.ymlSpring Boot 项目或 .envPHP 项目里。端口通常不用改需要改的是四个连接参数spring: datasource: url: jdbc:mysql://127.0.0.1:3306/kefu_system?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT快捷回复表;如果这套系统把快捷回复缓存到了 Redis 而不是纯 MySQL记得给重启后的缓存做预热否则客服系统刚启动那几分钟快捷回复面板是空的客服只能手动打字补。另外要分清快捷回复和知识库不是同一件事快捷回复是客服手动选的模板知识库是访客自助查询的入口。有些入门实现把两者合并成一张“自动回复”表访客端体验会打折扣——自动回复覆盖不了多少问题反而把找人工客服的入口藏了起来。我的偏好是访客端保留尽量精简的自助问答人工入口永远在第一位。5. 源码部署避坑清单五条实测踩坑记录下面五条按出现频率排序每一条都是实际部署里反复见到过的。现象、原因、解决三段式方便直接对照。5.1 现象客服端收到的消息全是乱码中文在访客端正常客服端显示乱码或数据库里存的直接是乱码。原因建库时用了默认字符集老版本 MySQL 默认 latin1或者连接串没带 characterEncodingutf8mb4。客服系统必然要处理 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 端点单独加 locationlocation /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 再异步落库 MySQLRedis 写入成功就返回“已发送”但异步落库一旦失败消息虽然还在缓存里服务一重启就消失在历史记录中。解决把 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 分钟周期的定时扫描任务扫所有 status0 且超过 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。这套从验包、启动、调参到上线的流程是我遇到这类开源在线客服系统时固定走的一条路。按这个顺序来一次跑通的概率会高很多希望帮到你。本文还有配套的精品资源点击获取