简介面向需要自建在线客服系统的企业站点或个人开发者WeLive是一款基于PHP与WebSocket开发的免费开源客服系统。它采用请求与推送全双工通信支持Web与移动端访问内置中英文双语切换、五套配色方案、AI机器人自动回复与访客文件上传等能力并能无限制添加坐席、独立部署在自有服务器适合外贸网站及多语言业务场景。资源包内共287个文件以PHP源码、JS脚本、CSS样式表为主辅以页面图片、提示音MP3和GIF动画总大小约1.6MB同时包含数据库SQL文件便于完整搭建与二次开发前端交互与后台管理模块均保留了可配置入口适度调整即可适配不同业务。已在CSDN被331人学习下载WeLive 5.9.0更新版还加入了客服端访客提示音可选、单双窗口切换、离线访客关闭等十余项优化对正在选型或维护客服系统的开发者有直接参考价值。1. WeLive是什么一套能直接上线的PHP客服系统而不是demo当你想给网站加一个在线咨询入口在开源社区里搜“WeLive PHP 在线客服系统”时会看到一堆演示截图和功能清单。把源码真正拉下来跑一遍你会发现入口、后台、访客端、数据库、会话记录一套齐全装完就是一个能用的在线客服平台而不是只丢给你一个留言表单的demo。WeLive的核心价值在于用PHP把“访客发起咨询—客服在线接待—会话存储与统计”这条链路完整串起来省掉从零写即时通讯和会话管理的力气。它解决的是两个很具体的场景。第一个是小网站、自媒体、独立站运营者想在页面角落挂一个浮动客服窗访客点开就能对话消息实时落到自己的客服后台而不是跳转到外部IM。第二个是外包项目交付时甲方要求“必须有一个在线客服功能”与其从零写消息表和会话分配器不如直接拿一套开源PHP客服系统来改把品牌、域名、业务逻辑换成自己的。适合谁用有PHP和MySQL基础、打算在源码上做二次开发的人或者做运维交付、只需要把客服系统部署到客户服务器上的工程师。如果你只想本地搭个环境体验一下按下面这套流程走一小时内也能跑起来。这篇我就按“部署环境—功能配置—二次开发—上线避坑—压测验证”的顺序把这套系统从源码变成可接待真实访客的服务讲清楚。2. 部署一套WeLive环境要求与最小安装步骤先别急着进后台点按钮客服系统是实时交互系统部署选型直接影响后面的会话延迟和连接稳定性。这个阶段把环境立住后面能少踩一半坑。2.1 PHP版本、扩展与Web服务器选型这类PHP客服系统的运行环境常见组合是Linux Nginx MySQL PHP-FPM也就是通常说的LNMP。PHP版本建议至少7.4如果源码较老直接跑在PHP 8.2上可能会报Deprecated: Dynamic properties这类弃用警告虽然不影响核心功能但日志会被刷爆所以生产环境我一般固定在7.4。扩展方面核心依赖是pdo_mysql、curl、mbstring、json、openssl。其中pdo_mysql负责数据库连接curl用于客服系统主动推送消息到第三方接口比如转接企业微信或邮件通知mbstring处理中文消息内容截断和编码转换。图片上传功能还需要fileinfo和gd访客发图片时服务端要做类型识别和缩略图生成缺了fileinfo会提示“非法文件类型”。Web服务器选型上我一般直接上Nginx。Apache也能跑但多数PHP项目要求AllowOverride All才能解析public目录下的伪静态规则Apache默认配置经常把这个关掉导致页面404。Nginx的try_files配置更直观而且静态资源处理能力强客服系统的访客端JS和CSS压力不大但胜在干净。2.2 用Nginx在服务器上跑通最小安装命令假设你已经通过GitHub Releases或Gitee下载到了WeLive源码包。下面这套命令兼容大多数Linux发行版用最小步骤把项目根目录立起来。# 1. 创建站点目录把源码包解压进去 mkdir -p /data/www/welive unzip welive-v1.0.zip -d /data/www/welive # 2. 进入项目根目录 cd /data/www/welive # 3. 给运行缓存、上传目录、配置目录写权限 chmod -R 775 runtime upload config chown -R www:www /data/www/welive # 4. 复制环境配置模板如果源码提供 .env.example if [ -f .env.example ]; then cp .env.example .env fi第一步的unzip是所有PHP项目通用操作解压后先看根目录有没有index.php还是public/index.php这决定了后面Nginx的root指向哪里。第三步的runtime和upload目录是客服系统的关键目录runtime存会话缓存和日志upload存访客聊天上传的附件。权限不足的表现不是直接报错而是“页面能打开、图片传不上、日志写不进”非常隐蔽。chown把目录归属给Nginx运行用户www如果你的PHP-FPM进程跑在nobody或自定义用户下这里要改成一致否则会出现PHP能读文件但写不进去的权限错位。接着配置Nginx站点。下面这段是客服系统最常见的伪静态配置把请求统一转发到入口文件。server { listen 80; server_name kf.example.com; root /data/www/welive/public; # 入口目录按源码实际情况调整 index index.php index.html; location / { try_files $uri $uri/ /index.php?s$uri; } location ~ \.php$ { fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~* \.(css|js|png|jpg|jpeg|gif|ico)$ { expires 7d; access_log off; } }这段配置里最容易出错的是fastcgi_pass后面的PHP-FPM socket路径。你用systemctl status php7.4-fpm或php -v确认实际版本路径/run/php/php7.4-fpm.sock与版本不匹配时请求会直接502。try_files的index.php?s$uri是PHP路由入口客服系统的访客端、客服后台、API接口都靠它分发不要把这一行删掉去写复杂的rewrite规则。配置完用nginx -t检查语法再systemctl reload nginx。2.3 安装完成后的目录结构与权限检查安装向导跑完后建议养成看目录结构的习惯。一套常规的PHP客服系统通常分成入口、应用、配置、资源四块。我习惯用下面这张表快速对账目录/文件作用需要检查的点public/Web入口放 index.php、CSS、JS站点root指向这里别指到项目根application/或app/控制器、模型、业务逻辑二次开发的主要阵地config/数据库、缓存、路由配置数据库密码存明文别提交到Gitruntime/或var/会话、日志、编译缓存必须可写否则页面白屏upload/聊天附件、客服头像注意PHP open_basedir限制目录检查可以用下面几条命令比用眼睛看可靠得多ls -l public/index.php php -m | grep -E pdo_mysql|curl|mbstring|json|openssl cat config/database.php 2/dev/null || cat .env 2/dev/null第一条确认入口文件存在第二条确认关键扩展已加载如果某个扩展没装安装向导会在环境检测页标红直接apt install php7.4-curl或yum install php74-php-mbstring补上第三条看数据库配置。注意安装向导里的表前缀默认通常是welive_如果你在同一台服务器的同一个库里做多客服系统测试前缀必须区分开否则表名冲突会互相覆盖数据。提示安装完成后先打开站点根地址确认能看到安装向导界面再启用上面的伪静态规则。很多人一上来就配伪静态结果安装向导页都是404排查半天发现是入口目录写错了。3. 让客服工作台跑起来坐席、会话分配与消息推送配置环境起来后开始配置真正的业务。这个阶段的目标是让客服能登录工作台、访客能发起会话、消息能实时往返。每一步都不复杂但参数选错了线上体验会很糟糕。3.1 创建坐席与客服组设置接待规则进入后台“客服管理”模块第一件事不是录入客服账号而是先建客服组。客服组对应业务线比如“售前咨询”“售后维权”“技术支持”。为什么先建组因为访客端嵌入代码里的group_id参数要把会话指定到某个组组不存在的话访客消息会进入“未分配”队列很多管理员看不到以为系统坏了。添加客服时注意客服账号和访客记录是两张完全不同的表。客服账号有密码哈希、分组、在线状态访客记录只有浏览器标识和访问历史。不要试图把客服也当访客插入访客表否则工作台拉取客服列表时会把密码字段暴露在JSON里。会话分配规则一般有两种轮流分配和空闲优先。轮流分配按“上次接待客服ID 1”的方式转适合体量均匀的团队空闲优先按当前进行中会话数升序排列会话数最少的客服优先接客。我建议把空闲优先的max_conversations参数设为3。设成1时客服还在敲字时新会话就会排队等待设成10时客服会同时面对10个窗口漏消息概率急剧上升。这个参数在客服组设置里改保存后对组内所有客服立即生效不需要重启PHP-FPM。3.2 消息实时推送的实现方式轮询间隔与长轮询客服系统体验上最关键的就是消息延迟。PHP默认没有常驻内存的WebSocket服务绝大多数开源PHP客服系统用的是轮询方案。短轮询是前端每2~3秒请求一次“拉取新消息”接口长轮询是前端发起请求后后端挂住连接等有新消息或30秒超时才返回。长轮询实时性更好但会长期占用PHP-FPM进程并发一高进程数就被占满。前端轮询代码一般长这样这是访客端核心逻辑// 访客端每3秒拉取一次新消息 async function pollNewMessages() { try { const resp await fetch(/api.php?actionpollvisitor_id visitorId, { headers: { X-Requested-With: XMLHttpRequest } }); const data await resp.json(); if (data.code 0 data.messages.length 0) { renderMessages(data.messages); } } catch (e) { // 网络错误或超时等下个周期再试 } finally { setTimeout(pollNewMessages, 3000); } } pollNewMessages();这里的visitor_id是访客唯一标识由后端在首次访问时生成并写入Cookie。setTimeout的3000是轮询间隔生产环境我一般设1到3秒。设太短1000个访客同时在线时每秒产生330个请求数据库压力陡增设太长客服回复后访客要等好几秒才看到体验像延迟对话。要注意后端接口必须做增量查询只看id last_message_id的消息千万别每3秒查一次这个访客的所有历史消息否则消息表三个月后数据量上来一次查询要几百毫秒轮询自然就卡了。前端还要判断跨域请求的CORS头如果客服系统在kf.example.com访客页在www.example.com接口返回JSON后浏览器会拦截响应需要在后端控制器里加上Access-Control-Allow-Origin头。3.3 访客端嵌入代码与自定义按钮访客端接入对运维来说是最常见的需求。把一段JS放进网页就会自动渲染浮动客服按钮和聊天窗口。嵌入代码通常提供一个配置对象script window.WELIVE_CONFIG { site_id: your_site_id, group_id: 1, theme_color: #1890ff, position: right-bottom, button_text: 在线咨询 }; /script script srchttps://kf.example.com/embed.js/scriptsite_id对应后台里的“站点编号”多域名共用一个客服系统时靠这个字段区分消息归属。group_id指定会话进入哪个客服组。theme_color直接控制悬浮按钮和聊天窗口的主色改配置对象比改CSS更方便。button_text是按钮上显示的文字如果你不想显示“在线咨询”改成“联系客服”就行。嵌入有个容易被忽略的点访客页如果用的是HTTPS而客服系统部署在HTTP浏览器会直接拦截http://kf.example.com/embed.js导致客服按钮根本不出现。所以客服系统生产环境必须上HTTPS。另外访客端域名和客服域名不一致时后台记录访客Session需要跨域处理。最省事的做法是把客服系统部署在.example.com的二级域名上再在配置里把Cookie的domain设为.example.com这样访客在同一个顶域名下的所有子站点客服系统都能识别为同一用户。4. 二次开发与数据落库看懂WeLive的PHP代码结构和API拿到源码后很多人习惯直接在文件里找业务字符串改完发现没生效。要做二次开发先花半小时把路由和数据表结构摸清楚比瞎改快得多。4.1 路由与控制器从index.php到业务逻辑的调用链这类PHP系统的入口文件通常很简洁它的职责是定义项目路径、加载框架、启动路由分发。下面这段是ThinkPHP风格的入口逻辑WeLive这类系统大多也是同样的套路?php // public/index.php 入口文件核心逻辑 define(APP_PATH, __DIR__ . /../application/); define(BIND_MODULE, home); // 加载框架引导文件 require __DIR__ . /../thinkphp/start.php;BIND_MODULE绑定默认模块。客服系统通常拆成几个模块home是访客端页面admin是客服后台api是访客端和客服端共用的Ajax接口。Nginx把URL重写到入口文件后框架根据路由规则解析成“模块/控制器/方法”的调用链。比如后台的客服列表地址是/admin/customer/index.html对应的控制器就是application/admin/controller/Customer.php方法index()里输出的就是列表数据。如果你改了控制器代码但刷新没变化先查runtime/目录里的编译缓存ThinkPHP会把控制器的模板编译结果缓存成php文件删掉runtime/temp目录再刷新。这个坑几乎是PHP框架二开的必修课。4.2 常用数据表与字段会话表、消息表、客服表在线客服系统的数据表比普通CMS复杂一点但核心就三张表。一张表记录会话主结构一张表记录每条消息一张表记录访客档案。下面这份表结构是我在实际项目里总结的常见设计表名前缀welive_关键字段用途说明welive_conversationid, visitor_id, staff_id, group_id, status, last_message_at会话主表status 0沟通中 1已结束 2留言welive_messageid, conversation_id, from_type, from_id, content, created_at消息明细from_type 0客服 1访客 2系统welive_visitorid, uuid, nickname, avatar, last_visit_at访客档案uuid为浏览器Cookie中的唯一标识welive_staffid, account, password_hash, group_id, online_status客服账号online_status控制上下线状态conversation表里的last_message_at字段非常关键。客服工作台会话列表的排序、未读角标的数量都依赖这个字段。如果你发现会话排序混乱优先查这个字段在消息写入时有没有同步更新。另外message表的检索通常按conversation_id和from_type过滤这两个字段一定要建联合索引否则客服点开会话查看历史消息时SQL会全表扫速度随数据量上升急剧恶化。4.3 扩展一个简单的“好评”接口示例二次开发最典型的场景是给会话扩展一个“满意度评价”功能。假设访客结束会话后可以给客服打1~5星我们就在API模块里加一个方法。?php namespace app\api\controller; use think\Db; class Evaluation extends Base { public function save() { $conversation_id intval(input(post.conversation_id)); $score intval(input(post.score)); if ($conversation_id 0 || $score 1 || $score 5) { return json([code 400, msg 参数不合法]); } $exists Db::name(conversation)-where(id, $conversation_id)-value(id); if (!$exists) { return json([code 404, msg 会话不存在]); } Db::name(conversation)-where(id, $conversation_id)-update([ score $score, evaluated_at time(), ]); return json([code 0, msg ok]); } }这段代码做了三件事参数校验、会话存在性检查、更新字段。注意intval(input(post.x))是ThinkPHP5风格的取值方式如果你的源码用的是原生PHP就换成$_POST[score] ?? 0再额外做一次过滤。我们给conversation表新增了score和evaluated_at两个字段执行前必须先ALTER TABLE加列否则update会静默失败连错误日志都不会有。二开时还有一个习惯建议所有Ajax接口用返回JSON的方式调试。客服系统前后端分离程度很高在浏览器F12的Network面板里看接口响应比看页面源码高效得多。接口报错500时顺手打开PHP错误日志多半是SQL字段不对或类名引入错误。5. WeLive运维避坑5个上线前必须处理的常见问题功能和二开都搞定后别急着上线。下面这五个坑都是我经手过的客服系统里最容易踩的有些问题排查了一天才定位到根因。按“现象—原因—解决”列出来你部署完可以直接对照检查。5.1 Session跨域丢失访客消息串人现象访客A打开www.example.com咨询客服回复后访客B也打开同一个页面竟然看到了A的会话内容或者两个访客被系统识别成了同一个人。原因访客端页面和客服系统不在同一个域名下。PHP默认Session的Cookie作用域是当前域名访客在www.example.com下拿到一个PHPSESSID但这个Cookie不会自动传给kf.example.com。跨域请求时后端识别不了Session就只能靠URL或请求头里重传的标识标识一旦没带上系统就为访客B新建了一个visitor_id把B的会话和A的会话混在一起。解决把客服系统部署在.example.com的二级域名下并把Session Cookie的domain设置为.example.com注意带点前缀。如果客服系统是Nginx反代到后端在站点配置里加一行proxy_cookie_domain kf.example.com .example.com;如果是Nginx直接转发给PHP-FPM不经过反代就改PHP配置在代码初始化处调用session_set_cookie_params([domain .example.com])。最稳妥的做法是在访客端嵌入代码里显式生成一个UUID并传给后端不依赖Cookie。5.2 消息延迟轮询被Nginx缓冲和PHP输出缓冲憋住现象客服在后台点回复访客端要等30秒以上才看到消息刷新页面后消息又全部出现。原因不一定是PHP代码慢。Nginx默认开启缓冲响应数据到达Nginx后不会立刻发给客户端而是攒够一定大小或等请求结束才输出。长轮询接口挂起30秒Nginx就把消息憋在缓冲区里访客端一直收不到直到超时或缓冲区满才一次性吐出。解决对轮询接口关闭Nginx缓冲。在Nginx的PHP location里加location ~ \.php$ { fastcgi_buffering off; proxy_buffering off; }同时后端代码在输出每条新消息后调用flush()强制PHP将数据立即推送到Nginx。检查PHP配置里output_buffering是否为关闭状态用php -i | grep output_buffering查看如果是4096之类的值在FPM配置里设为Off再重启。5.3 图片上传失败目录权限与open_basedir双重限制现象客服和访客在聊天窗口发送图片进度条走完提示“上传失败”或“服务器错误”后台日志一片空白。原因最初以为是upload/目录没有写权限执行chmod 777 upload/后还是失败。进一步查PHP-FPM配置发现open_basedir限制了PHP可访问的目录范围上传文件要写入的/data/www/welive/upload不在允许列表里PHP的move_uploaded_file函数直接返回false但没有抛出致命错误。解决修改PHP-FPM站点的php.ini配置把上传目录加进open_basediropen_basedir /data/www/welive:/tmp改完重启PHP-FPM。验证权限用一条命令sudo -u www touch /data/www/welive/upload/test.txt能创建文件说明权限到位然后删掉测试文件。注意不要整个关闭open_basedir那会让PHP访问服务器上的任意文件安全检查过不了。5.4 数据库连接数爆掉长轮询和慢查询是共犯现象上线半小时后客服后台卡死MySQL执行show processlist看到大量Sleep状态的连接连接数达到上限。原因长轮询接口在挂起30秒的期间代码里还保持着一个数据库连接每来一个访客轮询就占一个连接。100个访客同时在线连接数瞬间占满。即使换成短轮询如果查询条件没走索引每秒几十次全表扫也会把数据库拖垮。解决把长轮询接口里的数据库查询移除改用Redis缓存会话状态和未读消息数。增加Nginx对轮询接口的限流location /api.php { limit_req zonepolllimit burst20 nodelay; }给welive_message表加上(conversation_id, id)联合索引。改造后轮询接口只读Redis不再直接查询MySQL数据库连接数会大幅下降。5.5 客服全离线时访客留言丢消息现象客服都下线后访客发了一长串消息第二天客服上班发现会话列表里只有一条系统欢迎语访客发的消息内容一条都看不到。原因代码逻辑在访客发起会话时先查在线客服列表列表为空就走“离线留言”分支。但留言分支只把消息写进了welive_message表没有更新conversation表的状态。客服后台默认只查状态为“沟通中”的会话所以留言会话根本没出现在列表里。解决修改离线会话处理逻辑。当没有在线客服时创建会话的状态直接设为2留言并把last_message_at和留言内容写进会话表。同时给客服后台增加“留言列表”筛选默认展示status 2的会话客服点开后可以一键转成“沟通中”继续接待。这样即使客服不在线第二天也能在留言标签里找回所有内容。6. 从能用到好用压测验证与消息可靠性的几个进阶手法功能都正常后最后一步是上线前做一轮验证。我判断一套客服系统能不能上生产只看三个指标轮询接口扛不扛得住、消息会不会丢、请求慢了能不能定位。下面这几个手法是我每次部署都会做的。6.1 用ab打断轮询接口做最小压测上线前先对轮询接口做一次低压力测试用Apache自带的ab工具就够ab -n 2000 -c 50 -k http://kf.example.com/api.php?actionpollvisitor_idtest123-n 2000是总请求数-c 50是并发数-k开启KeepAlive。测试结束后看Requests per second和Failed requests两个指标。如果每秒请求数低于50或者失败率高于1%优先查接口内有没有睡眠函数、SQL有没有走索引、PHP-FPM进程数是不是被max_children限制住了。6.2 用SQL核对消息是否丢压测之后做消息可靠性验证。模拟两个访客各发10条消息然后用一条SQL把会话表和消息表关联起来看每个会话的消息数是否匹配SELECT c.id, COUNT(m.id) AS msg_count FROM welive_conversation c LEFT JOIN welive_message m ON m.conversation_id c.id WHERE c.created_at 2025-01-01 00:00:00 GROUP BY c.id HAVING msg_count 10;查出msg_count小于10的就是丢失消息的会话。常见原因有两个批量写入时用了insertAll但其中一条失败导致全部回滚或者长轮询接口的last_message_id更新条件写错导致消息被推给了错误的会话ID。6.3 开日志用时间戳对账最后把PHP错误日志打开线上排查靠这个救命。PHP-FPM默认可能只记录致命错误不够用改成log_errors On error_log /var/log/php-fpm/error.log另外在Nginx的location里加上$request_time变量日志格式里就可以看到每个接口的实际处理耗时。Nginx的access.log配置log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $request_time;上线后如果听说“客服回复总是慢”就翻access.log里request_time超过1秒的请求再对应到同时间段PHP错误日志很快能定位是慢SQL还是外部接口调用。干过几个客服系统之后我的习惯是每次上生产前必须做完三件事强制离线会话走留言状态、压测轮询接口、确认日志目录可写。否则半夜被“消息延迟”的告警吵醒再回头看代码痛苦会加倍。这套开源PHP客服系统本身不复杂复杂的是让它稳定地跑在真实流量里。希望这篇笔记帮到你少走我走过的弯路。本文还有配套的精品资源点击获取