简介:WeLive客服系统V7.1是一套基于PHP与Workerman Socket通讯框架开发的在线客服源码,适用于需要在自有服务器部署私有化客服能力的企业、外贸站点及个人开发者。前端同时适配Web与移动端,可在微信、QQ浏览器中直接打开会话窗口,或嵌入其他App使用;系统内置中英文双语自动切换,便于对接跨境业务,坐席数量无限制且无需年费授权,部署后即可免费长期使用。资源包共313个文件,压缩后仅1.68MB,以116个PHP核心逻辑文件与106个PNG图形素材为主体,同时包含JS脚本、CSS样式、SQL数据表、GIF动图及MP3提示音等,结构精简,方便在Wamp Server等PHP+MySQL集成环境中快速搭建调试。已有91人下载学习,适合需要私有化客服部署、二次开发或学习Workerman长连接通讯的开发者作为轻量参考项目。
1. 自部署一套 WeLive 在线客服系统:开源 PHP 源码能省下什么
客服系统这件事,商业 SaaS 按坐席按月收费,数据还躺在别人服务器上;免费版客服工具又总在关键功能上做阉割。WeLive 作为一套开源 PHP 在线客服系统源码,正好卡在这两个选项之间:源码下载下来、部署在自己服务器上,访客聊天窗口、坐席工作台、管理后台全部自持,坐席数量不受平台限制。它最适合手里有 PHP+MySQL 基础、想快速搭起一套客服系统的开发者,也适合给客户做私有化交付的接单者。这篇笔记不聊泛泛的开源理念,就按部署顺序——解压、建库、改配置、读关键源码、处理报错——把这条链路完整走一遍。
2. 先拆 WeLive 的底子:三个端、一张会话表、一条消息管线
跑通之前,我习惯先把这类系统的骨架说清楚。在线客服系统不是一个“网页聊天框”,它是三个界面共享一套数据模型的应用,搞清楚数据怎么流动,后面排错才有方向。
2.1 一套聊天系统在源码包里长什么样
在线客服系统至少有三种使用界面同时存在:访客看到的聊天窗口、客服人员用的坐席工作台、管理员用的后台。源码分包通常也按入口分:根目录放访客端入口,坐席端和管理后台各自独立目录。下载压缩包解压后,第一件事是用ls看目录结构,而不是急着配环境。就算 WeLive 的目录和你下载到的压缩包不完全一致,你也能在这些目录里找到对应物:一个接口目录、一个模板目录、一个配置目录,以及若干 SQL 文件。
这类老牌 PHP 项目往往没有composer.json,不需要node_modules,整包就是index.php、api/、template/、config/、install/这类基础结构。它决定了部署门槛极低——一台能跑 PHP 的云主机就够。但也意味着代码大量使用过程式 PHP 和原生 SQL,读起来不像 Laravel 那样有框架约束,你得顺着入口文件去追一条消息的完整去向,这也是我后面专门用一章讲“读源码切入点”的原因。
2.2 会话模型:它跟留言板最本质的区别
留言板里一条留言就是一个完整记录,没有状态流转。客服系统里一次咨询从访客点开窗口开始,要经历排队、接待中、转接、结束这样的完整生命周期。核心表通常是一张会话表,上面挂消息表、访客表、坐席操作记录。会话表上至少有三个关键字段,排错时最先看的就是它们:
| 字段 | 作用 | 常见取值 |
|---|---|---|
| status | 会话当前处于哪个阶段 | waiting / chatting / closed |
| accept_agent | 当前接管的坐席 ID | 空表示还在排队 |
| create_time | 会话建立时间 | 排队时长统计全靠它 |
理解会话建模,才能明白为什么消息明明发出来了却“看不到”。很多问题不是消息丢,而是消息挂在一个没有坐席认领的会话里,status还是 waiting,accept_agent还是空。排查时先把这两个字段查出来,“客服看不到消息”的坑大多在这里。所以我在部署完成后,会顺手把这几张核心表的建表语句打印出来,先让自己知道每张表是干什么的。
2.3 环境地基:先选对 PHP 版本再下载源码
这类源码最麻烦的不是功能,而是运行环境有“时代感”。PHP 5.x 时代写的代码,直接拿 PHP 7.4 或 8.x 跑,经常首页白屏。V7.x 版本常见要求是 PHP 5.6 到 7.x 之间,MySQL 5.5 以上。我个人的经验是:先装 PHP 7.1 起步,如果安装向导或页面报函数错误,再降回 5.6,不要在最初就冲最新版。这也是我会反复跟接单人强调的一件事:老项目没有升级 PHP 的义务,能稳定跑起来才是重点。
环境选型我给一个相对省心的组合:Linux + Nginx + PHP-FPM 7.1 + MySQL 5.7。Nginx 比 Apache 的配置更直观,PHP-FPM 管理生命周期比 mod_php 干净。源码里如果带.htaccess而你用 Nginx,需要手动把 rewrite 规则翻成 Nginx 语法,这是很多人踩坑的第一站。装完 PHP 后先确认扩展是否齐全,老系统通常依赖mysql、mysqli、curl、gd:
php -m | grep -E 'mysql|mysqli|pdo_mysql|curl|gd'没有输出就先用包管理器补扩展,这一步比配 Nginx 更重要——很多页面加载异常其实是缺扩展,不是代码有问题。下载源码时也要看一眼文件名是否带v7.1之类版本标识,尽量走官方发布渠道,避免从不明转载站拿到被改过后门或删掉目录的半残包。
| 组件 | 我推荐 | 原因 |
|---|---|---|
| PHP | 7.1 或 5.6 | 老代码通常不兼容 PHP 8 的构造函数规则 |
| MySQL | 5.7 | utf8 支持最顺手,老 SQL 导入不易翻车 |
| Web 服务器 | Nginx 1.18+ | rewrite 规则好配,静态请求处理干净 |
| 操作系统 | CentOS 7 / Ubuntu 18.04 | 差异不大,按自己熟的来 |
3. 从压缩包到能聊天:LNMP 下的最小跑通流程
下载和解压只是开始,真正的工作从目录体检开始。这一章给的是我习惯的最小跑通流程,照着走一遍,本地能登录坐席端并用另一个浏览器发消息,就算成了。
3.1 解压之后先做“目录体检”,别急着配数据库
下载到的压缩包格式通常有三种:zip、tar.gz、带 install 向导的整合包。先解压到独立目录,然后做一次轻量体检:看有没有 SQL 文件、有没有数据库配置文件、有没有install/或install.php。
unzip welive_v7.1.zip -d ./welive cd ./welive ls -la du -sh . find . -name "*.sql" -o -name "config*.php" | head -20ls -la看根目录结构,du -sh看整个包体量是否合理,最后一条 find 直接定位 SQL 文件和配置文件。这一步的核心目的是确认你拿到的是完整源码还是残缺包——很多下载站上传时会漏掉目录,缺模板目录还能发现,缺了核心 include 目录就直接白屏。所以体检完成之前,别去碰数据库。
3.2 建库与导入:字符集选错就是中文变问号
用命令行建库导数据最稳,比在 phpMyAdmin 里点来点去更快。常见做法是先用 root 建库,再导入 SQL,最后确认表都建出来了:
# 建库,字符集以 SQL 文件头为准,拿不准先用 utf8 mysql -uroot -p -e "CREATE DATABASE welive DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;" # 导入表结构和初始数据 mysql -uroot -p welive < welive.sql # 确认表都建出来了 mysql -uroot -p -e "USE welive; SHOW TABLES;"如果 SQL 文件头注释里写的是utf8mb4,建库也跟随改成utf8mb4_unicode_ci,总之以 SQL 文件为基准。导入时报table already exists就删除重建;报Unknown collation,说明 SQL 里有当前 MySQL 不认识的字序,编辑文件把 COLLATE 换掉即可。
提示:字符集这里有个血泪经验——老系统 SQL 文件头写的是 utf8,你偏要建 utf8mb4 库,中文数据能写进去,但过长的索引键会报错,让你排查半天。先看 SQL 文件,再决定建库参数,顺序不要反。
3.3 改配置文件:三处必改和一处建议
多数老项目把数据库连接信息集中放在一个 config 文件里,比如config.php、config.inc.php、include/db.php。拿到源码后先全局搜一下db_host或mysql_connect,确定改动位置,再把连接参数逐项改掉:
<?php // 常见的数据库连接配置,按你的实际环境改这三行 $db_host = '127.0.0.1'; // 数据库地址,本地填 127.0.0.1,公网部署填内网 IP $db_user = 'welive_user'; // 数据库账号,建议单独建,别用 root $db_pass = 'your_password'; $db_name = 'welive'; $db_port = 3306; // 端口,本机改过端口要同步改这里 ?>另外建议单独建一个业务账号,而不是用 root 跑对话业务。授权 SQL 如下:
CREATE USER 'welive_user'@'localhost' IDENTIFIED BY 'your_password'; GRANT ALL PRIVILEGES ON welive.* TO 'welive_user'@'localhost'; FLUSH PRIVILEGES;如果页面仍报连接失败,去看 MySQL 的监听地址。本地填 127.0.0.1,云服务器填内网 IP。很多新手把db_host填成公网 IP,要么连不上,要么把数据库端口暴露出去惹上扫描爆破,这个习惯要改。
3.4 开发时用内置服务器,上线用 Nginx:两套启动方式都要会
PHP 内置服务器最适合快速验证代码有没有跑起来,但它只支持单线程,且对带 PATH_INFO 的路由支持不好,仅适合本地调试:
# 快速验证:进入源码目录,用 PHP 内置服务器跑起来 php -S 0.0.0.0:8080 -t /var/www/welive # 然后浏览器访问 http://服务器IP:8080能打开登录页或访客窗口,说明源码本身没问题,后续遇到的报错都在 Web 服务器配置上。上线时换 Nginx,注意老系统常见的index.php?action=xxx分发方式,配置要避免把所有静态请求都转发到前端控制器:
server { listen 80; server_name welive.test; root /var/www/welive; index index.php index.html; location / { # 静态文件直接走磁盘,动态请求再交给 PHP try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }try_files的先后顺序别乱:先找真实文件,找不到再交给入口文件。fastcgi_pass要和你的 PHP-FPM 监听地址一致,9000 端口或 unix socket 都行,但两边必须对上。这一步完成后,本地应该能用两个浏览器分别打开坐席端和访客端,发一条消息看到它出现在工作台里,到这里就算跑通了。
4. 读 WeLive 的关键源码:四个最值得改的位置
跑通只是起步,接单和二次开发才是真正花时间的地方。读这类老源码最忌讳从头读到尾,正确方式是顺着一次消息交互去追,追完一条链路,系统骨架就全清楚了。
4.1 从入口文件追一次“访客发消息”的完整链路
访客点击发送后,JS 会请求某个 URL,PHP 入口拿到参数后做路由分发。这类老系统的入口文件往往就是一个大型 switch:
<?php // 入口分发逻辑示意,不同版本文件位置不同,但模式类似 $action = isset($_GET['action']) ? $_GET['action'] : 'index'; switch ($action) { case 'send_msg': // 访客发消息 $visitor_id = $_COOKIE['welive_vid']; insert_message($visitor_id, $_POST['content']); break; case 'poll': // 坐席端轮询新消息 $agent_id = $_SESSION['agent_id']; echo json_encode(get_new_messages($agent_id)); break; } ?>顺着这条线看下去,你会看到消息插入到哪张表、访客 ID 从哪个 cookie 取、坐席端通过哪个参数认领会话。想加自定义字段(比如访客手动填手机号),就在send_msg分支里补一个字段,再去消息表加列,前后端同步改即可。
4.2 客服分配和排队逻辑:最常改的就是排序比较器
第二个值得改的是客服分配。常见策略有三种:轮流分配、最少接待量优先、按技能组匹配。老代码里很多默认是“先来先得”或轮询,改动通常集中在一个分配函数里。我最常把默认策略改成“在线坐席里挑正在接待数最少的”:
<?php // 分配策略示意:从在线坐席里选一个接待量最少的 $agents = get_online_agents(); usort($agents, function ($a, $b) { if ($a['active_chats'] == $b['active_chats']) return 0; return ($a['active_chats'] < $b['active_chats']) ? -1 : 1; }); $target = $agents[0]; assign_conversation($session_id, $target['id']); ?>想按技能组筛选,就在排序之前先按group_id把坐席列表过滤一遍;想控制每人的最大接待量,在排序后加一个判断,超过上限就跳到下一个坐席。这个函数就是分配策略的开关,改它比改页面上的按钮都值钱。
4.3 坐席端收消息:轮询间隔是个能调的产品参数
老系统通常没有 WebSocket,坐席工作台靠setInterval轮询后端接口拿新消息。这代码一般在坐席端页面的 JS 里,搜索setInterval或setTimeout就能定位:
// 坐席端轮询新消息,间隔 2 秒 setInterval(function () { fetch('index.php?action=poll&last_id=' + lastMsgId) .then(res => res.json()) .then(data => { if (data.messages.length) { renderMessages(data.messages); lastMsgId = data.messages[data.messages.length - 1].id; } }); }, 2000);轮询间隔 2 秒体验够用,放到 5 秒服务器压力小但消息感觉得延迟。如果同时在线坐席有几百个,轮询会把数据库查询放大几十倍,这种量级要么把 SQL 查询范围收紧(只查最近 N 条),要么整体改造接 WebSocket 网关。小项目不要一上来就上 WebSocket,轮询够用且好排查。
4.4 给源码做体检:日志、Xdebug 和直查数据库
很多“代码没问题但功能失灵”的现象,靠断点打不出结果,因为过程式代码的执行路径不好定位。我的习惯是直接查数据库:坐席端没收到消息,先在消息表看最新记录;分配不生效,先看坐席表的在线状态字段。然后再开慢查询日志,看有没有 SQL 在拖后腿:
# 开启慢查询日志,阈值 1 秒 SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; # 查看当前所有连接,看有没有堆积的 SEEP 连接 mysql -uroot -p -e "SHOW PROCESSLIST;"如果发现会话表查询慢得离谱,检查有没有索引。给status、accept_agent、create_time各加一个普通索引,通常是性价比最高的优化。改完后重新跑一次轮询接口,对比响应时间,数据比感觉更可靠。
5. 部署 WeLive 最常翻车的五个位置:现象、原因、解法
这一章是纯踩坑记录。前四条我都在不同项目里真实遇到过,第五条是接手老项目时最容易忽略的兼容性细节。
5.1 首页白屏:新老 PHP 版本之争
现象:解压配置完,访问首页一片白,F12 看到 HTTP 500 返回码。
原因:老代码使用类同名构造函数,PHP 7 开始废弃、PHP 8 直接报致命错误;或者还在用mysql_*函数,PHP 7 里已经被移除。
解决:先php -v看当前版本,切到 PHP 7.1 或 5.6 再试。如果还白屏,在入口文件顶部临时加上error_reporting(E_ALL)开启错误展示,把第一个 Warning 揪出来。老项目没有升级 PHP 的义务,能稳定跑起来最重要。
5.2 数据库连接失败,报错信息被吃掉
现象:页面显示“系统错误”或“数据库错误”,但不告诉你为什么。
原因:php.ini 里display_errors是 Off,代码里还可能用了@符号把错误吞掉。
解决:在入口文件最顶部临时打开错误输出,定位后必须删掉:
<?php // 放入口文件最顶部,定位问题用,上线前务必删除 error_reporting(E_ALL); ini_set('display_errors', 1); ?>看到具体报错后,按错误类型去查:连接失败看数据库地址端口,权限问题看授权账号,认证失败看密码是否包含被转义的字符。
5.3 访客能发消息,坐席能看到会话但看不到内容
现象:访客端提示发送成功,坐席工作台也出现了这个会话,点开却是空的。
原因:访客的 Cookie 在跨域或跨端口请求下没带上,服务器把消息挂到了“未知访客”上,甚至每次刷新都生成新访客 ID。
解决:用浏览器开发者工具看访客端发送消息的请求,确认请求头里有没有 Cookie 字段。若前端用不同端口调接口,需在接口层配跨域响应头,并让 fetch 带上credentials。老系统跨域处理能力弱,尽量把访客端和后端放同一域名下部署,能省掉这一整类问题。
5.4 客服工作台挂久了,发消息报连接失败
现象:客服早上打开工作台,午休回来后发消息失败,重启 PHP-FPM 又恢复正常。
原因:MySQL 默认wait_timeout是 8 小时,连接池或持久连接里的旧句柄已被服务端断开,老代码又没做自动重连。
解决:临时处理是重启 PHP-FPM 或者调大 MySQL 的wait_timeout。根治是在数据库封装里加 ping 重连,搜mysql_ping或mysqli_ping,没有就补一段:
// 在数据库连接封装里加一段,执行查询前检查连接是否还活着 if (!mysqli_ping($conn)) { mysqli_close($conn); $conn = mysqli_connect($db_host, $db_user, $db_pass, $db_name); }加了重连逻辑之后,哪怕 MySQL 重启过,页面也能自动恢复连接,不用整天去手动重启服务。
5.5 手机端访客窗口错位,按钮点不到
现象:手机浏览器打开聊天窗口,整个页面缩小得像网页缩略图,输入框被顶出屏幕。
原因:老模板的 head 区域没有 viewport 元标签,浏览器按 960px 固定宽度渲染再整体缩放。
解决:在访客端模板的<head>里加上 viewport 标签:
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">加完头标签后,用浏览器开发者工具的手机模拟模式验证,不需要借真机就能看出布局是否正常。若输入框仍被屏幕边缘截断,再给聊天框加一条针对小屏的媒体查询,把宽度从固定像素改成百分比或vw。
6. 跑通只是开始:两个小改造,一套上线前验证
系统能跑通,离能交付还差一步。我个人交付前的习惯是加一个体验细节,再走一遍验证清单,最后把源码包从 Web 目录里清掉。
6.1 小改造:给访客端加“正在输入”提示
老系统访客端一般只有收发消息,没有正在输入状态。加这个功能不用动表结构,访客输入时用 JS 定时上报一个标记,坐席端轮询时读取展示:
// 访客端输入时触发,1.5 秒上报一次 let typingTimer; input.addEventListener('input', function () { clearTimeout(typingTimer); typingTimer = setTimeout(function () { fetch('index.php?action=typing', { method: 'POST' }); }, 1500); });后端在会话表加一个visitor_typing字段,或者直接写一条系统消息,等下一个轮询周期带出去即可。注意设置超时,访客停止输入后自动清零,否则“正在输入”会一直挂着。
6.2 上线前验证:五步走一遍
| 验证项 | 通过标准 | 备注 |
|---|---|---|
| 消息收发 | 访客窗口发消息,坐席端 3 秒内收到 | 两个浏览器分别登录两端测 |
| 刷新不丢会话 | 刷新访客页后,历史消息还在,会话不重开 | 关注访客 Cookie 是否稳定 |
| 分配正确 | 两个坐席同时在线,新会话按预期分配 | 连续发起三次会话观察 |
| 断线恢复 | 重启 MySQL 后,再发消息能成功 | 验证 ping 重连逻辑 |
| 安全收紧 | 后台使用强口令,源码包移出 Web 目录 | 安装向导上线后要删或禁 |
这套清单我每次交付都会跑,尤其断线恢复这一项,不加重连逻辑的老系统几乎必挂。曾经有次交付给客户,对方客服第二天反馈“聊着聊着就发不出去了”,最后定位就是 MySQLwait_timeout断了连接,而我上线前漏了这步验证。从那以后,重启 MySQL 再发消息成了固定测试项。
希望这篇部署笔记能帮你少走一段弯路。WeLive 这类老牌 PHP 客服系统,功能不算新,但胜在结构简单、可控性强。把它部署起来、读透几条核心链路、改顺分配和轮询策略,就足以撑起大多数中小站点的客服需求。真到撑不住的那天,你也能清楚地知道瓶颈在哪——那时候再考虑引入消息队列或者 WebSocket 网关,就水到渠成了。
本文还有配套的精品资源,点击获取