简介:这份资源是一套基于ThinkPHP5、FastAdmin与Swoole构建的企业IM客服系统PHP源码,面向需要独立部署即时通讯与在线客服能力的中小企业、开发者及运维人员,帮助解决多站点统一客服、会员与游客实时沟通等需求。压缩包共约2000个文件,整体25.56MB,以1215个js、172个html、155个json、107个css及43个vue等前端与配置资源为主,另含sql建库脚本、sh部署脚本与yaml配置,结构完整便于二次开发。系统支持会员、管理端与游客之间的即时通讯、群聊、多客服坐席无数量限制、按规则分配客服、知识库智能匹配回复及转人工,并具备离线消息推送、历史消息、群禁言、群免打扰、成员管理等功能,消息类型覆盖文字、表情、图片、音视频、链接、附件与收藏引用转发,采用WSS加密Socket传输,适配Https站点,支持CDN部署与第三方云存储上传。目前已有298人学习关注,适合希望快速搭建可跨站调用、统一管理的企业级IM客服平台的读者参考使用。
1. 企业IM客服系统源码落地:ThinkPHP5+FastAdmin+Swoole 到底能跑出什么
很多做企业服务的团队都遇到过这个场景:客户要求把在线客服嵌进自家后台,最好还能带工单、带坐席分配、带历史会话,预算却只够买一套源码自己改。这时候搜到「企业IM客服系统PHP源码带安装教程+基于ThinkPHP5+FastAdmin+Swoole」这类标题,第一反应往往是——这套东西到底能不能直接跑起来,还是装完就一堆报错。它本质上是把三样东西拼在一起:ThinkPHP5 负责后台业务逻辑和数据库读写,FastAdmin 提供现成的后台管理骨架和权限体系,Swoole 扛住 WebSocket 长连接和消息实时推送。适合谁?适合有一定 PHP 基础、想快速搭一套内部客服或工单系统的开发者,也适合接私活需要交付一套可维护后台的人。不适合完全没碰过命令行、指望双击安装包就完事的新手。下面按「先跑通、再改业务、最后调性能」的顺序拆开讲。
2. 环境准备与源码结构:把 ThinkPHP5+FastAdmin+Swoole 装进一台机器
2.1 运行环境怎么选:PHP 版本、扩展与 Swoole 编译
这套组合对版本比较敏感。ThinkPHP5 官方支持 PHP 5.6 到 7.x,FastAdmin 基于 ThinkPHP5 又额外依赖一些扩展,而 Swoole 4.x 之后对 PHP 7.2 以上支持最好。我一般会锁定 PHP 7.4 + Swoole 4.8 这个组合,兼容性最稳。需要装的扩展至少包括:swoole、pdo_mysql、mbstring、redis(如果会话和队列走 Redis)、fileinfo。Swoole 用 pecl 装最省事:
# 安装 Swoole 扩展,指定稳定版本 pecl install swoole-4.8.13 # 在 php.ini 中启用 echo "extension=swoole.so" >> /etc/php/7.4/cli/php.ini echo "extension=swoole.so" >> /etc/php/7.4/fpm/php.ini # 验证是否加载成功 php -m | grep swoole逻辑说明:Swoole 是 C 扩展,必须编译进 PHP 才能用。CLI 和 FPM 两个 ini 都要加,因为 WebSocket 服务通常用 CLI 启动,而后台页面走 FPM。参数上,swoole-4.8.13是 4.x 末期的稳定版,比 5.x 少一些协程行为变化,配合 ThinkPHP5 更省心。装完用php -m确认,如果没输出说明 ini 路径不对,用php --ini查实际加载文件。
提示:Swoole 编译时如果报
openssl相关错误,先装libssl-dev,再重新 pecl install。
2.2 源码目录怎么读:FastAdmin 的 application 与 Swoole 服务入口
拿到源码后别急着改,先看清结构。FastAdmin 的标准目录里,application/下按模块分admin、api、index,客服系统的业务代码通常放在application/api/controller/和application/admin/controller/。Swoole 的启动文件一般单独放在根目录或addons/下,常见命名是server.php或swoole.php。数据库配置在application/database.php,Redis 配置在application/config.php的redis段。先确认三件事:server.php里监听的端口、database.php里的库名账号、config.php里的 Redis 地址。这三处对不上,后面必翻车。
# 查看源码根目录结构 ls -la # 典型输出应包含 application/ public/ think server.php 等 # 查看 Swoole 启动文件监听配置 grep -n "listen\|port\|host" server.php逻辑说明:ls -la确认入口文件存在,grep快速定位监听端口。很多源码包把端口写成 9501 或 9502,如果服务器上已有其他服务占用,启动时会报Address already in use,这时候改端口比重启服务器快。
2.3 数据库与 Redis 初始化:导入 SQL 和改配置的顺序
导入数据库要在改配置之前做,否则后台登录会直接 500。常见做法是先用 phpMyAdmin 或命令行建库,再导入源码里的.sql文件。命令行方式:
# 建库并导入,注意字符集用 utf8mb4 mysql -u root -p -e "CREATE DATABASE im_kefu DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p im_kefu < install.sql # 改数据库配置 vim application/database.phpdatabase.php里重点改hostname、database、username、password、hostport五项。Redis 如果本机没装,先apt install redis-server并systemctl start redis。改完配置后访问后台域名,能出登录页说明 ThinkPHP5 和 FastAdmin 已经通了。这一步不通,后面 Swoole 不用看。
3. Swoole 长连接服务启动:WebSocket 握手与消息路由怎么配
3.1 启动 WebSocket 服务:命令行参数与常驻方式
Swoole 服务不能用浏览器访问,必须在命令行启动。常见启动命令是php server.php start,但生产环境要加守护进程参数:
# 开发调试:前台运行,看日志 php server.php start # 生产环境:守护进程模式 php server.php start -d # 查看是否在跑 ps aux | grep server.php逻辑说明:start是 Swoole 自定义的命令字,源码里通常用$argv[1]判断。-d让进程转入后台,日志会写到源码指定的log_file。如果启动后ps看不到进程,先看server.php里log_file路径有没有写权限,再看端口是否被占。我一般会在server.php里把worker_num设成 CPU 核数的 1 到 2 倍,max_request设 10000 防止内存泄漏。
注意:Swoole 服务重启必须用
php server.php reload或restart,直接 kill 会导致正在连接的客户端全部掉线且不重连。
3.2 WebSocket 握手与鉴权:token 怎么带、坐席怎么认
客户端连 WebSocket 时不能裸连,通常要在 URL 上带 token,服务端在onOpen回调里校验。常见写法:
// server.php 中 onOpen 回调片段 public function onOpen($server, $request) { // 从 URL 参数取 token,例如 ws://ip:9501?token=xxx $token = $request->get['token'] ?? ''; $uid = $this->auth->checkToken($token); // 自定义鉴权 if (!$uid) { $server->close($request->fd); return; } // 绑定 fd 与 uid,方便后续定向推送 $this->redis->hSet('im_online', $request->fd, $uid); }逻辑说明:$request->get拿 URL 参数,checkToken查数据库或 Redis 里的登录态。校验失败直接close,避免无效连接占资源。hSet把文件描述符和用户 ID 关联,后续坐席给客户发消息时,通过uid反查fd再push。参数上,im_online这个 hash 的 key 是 fd,value 是 uid,服务重启后要清空,否则会残留脏数据。
3.3 消息路由与离线消息:onMessage 里怎么分发和落库
onMessage是核心,收到消息后要判断是聊天、心跳还是系统指令。常见做法是用 JSON 里的type字段区分:
public function onMessage($server, $frame) { $data = json_decode($frame->data, true); switch ($data['type']) { case 'ping': $server->push($frame->fd, json_encode(['type' => 'pong'])); break; case 'chat': // 落库 $this->messageModel->save([ 'from_uid' => $data['from'], 'to_uid' => $data['to'], 'content' => $data['content'], 'add_time' => time(), ]); // 查对方 fd 并推送 $toFd = $this->redis->hGet('im_online', $data['to']); if ($toFd) { $server->push($toFd, json_encode($data)); } break; } }逻辑说明:ping用于保活,客户端每 30 秒发一次,服务端回pong,防止连接被中间层断开。chat先落库再推送,保证消息不丢。hGet查对方是否在线,不在线就只落库,等对方上线后拉历史。参数上,add_time用time()存时间戳,比存格式化字符串省空间且好排序。
4. 后台客服业务改造:FastAdmin 里加工单、坐席与快捷回复
4.1 用 FastAdmin 的 CRUD 生成工单表管理
FastAdmin 最大的价值是一键 CRUD。工单表建好后,在后台命令行执行:
# 生成工单表的 CRUD,表名 im_work_order php think crud -t im_work_order -c work_order逻辑说明:-t指定表名,-c指定生成的控制器名。执行完会在application/admin/controller/下生成Work_order.php,并在application/admin/view/下生成列表和表单视图。参数上,表里最好有status、create_time、admin_id三个字段,CRUD 会自动识别成筛选和排序条件。生成后要手动改work_order.js里的status渲染,否则列表里显示的是数字不是文字。
4.2 坐席分配逻辑:轮询还是按负载
坐席分配有两种常见做法:轮询和按当前会话数分配。轮询简单,用 Redis 的incr取模:
// 轮询分配坐席 $index = $this->redis->incr('seat_index') % $seatCount; $seatId = $seatList[$index];逻辑说明:incr是原子操作,多进程下不会冲突。seatCount是当前在线坐席数,从 Redis 的im_online里统计。按负载分配则要遍历坐席当前会话数,取最小的那个,代码复杂但更均衡。我一般先用轮询,等坐席超过 20 人再换负载算法。
4.3 快捷回复与消息模板:数据库表怎么设计
快捷回复表至少要有content、sort、admin_id三个字段。admin_id为 0 表示全局快捷语,非 0 表示个人。查询时用where('admin_id', 0)->whereOr('admin_id', $uid)。消息模板用于系统自动回复,比如「您好,客服已收到您的消息」,存在im_message_tpl表里,用code字段区分场景。这两张表都不大,但设计时要注意content用text类型,别用varchar(255),否则长回复会被截断。
5. 避坑与排查:这套源码最容易翻车的五个地方
5.1 现象:Swoole 启动报「Address already in use」
原因:端口被其他进程占用,或者上次没正常关闭残留进程。解决:lsof -i:9501查占用进程,kill -9掉,或者改server.php里的端口。改完记得同步改前端 WebSocket 连接地址。
5.2 现象:后台登录后一直跳回登录页
原因:FastAdmin 的 session 配置和 Swoole 的 Redis 冲突,或者application/config.php里session的type没改成 redis。解决:把 session 类型改成 redis,并确认 Redis 地址和 Swoole 用的是同一个库。如果还不行,清空runtime/目录再试。
5.3 现象:WebSocket 连上就断,日志显示 handshake 失败
原因:Nginx 反代 WebSocket 时没加Upgrade头。解决:在 Nginx 配置里加:
location /ws { proxy_pass http://127.0.0.1:9501; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }逻辑说明:Upgrade和Connection两个头是 WebSocket 握手必须的,缺一个就会 400。proxy_http_version必须写 1.1,1.0 不支持长连接。
5.4 现象:消息发出去了但对方收不到
原因:im_online里存的 fd 是旧的,对方重连后 fd 变了但没更新。解决:在onClose回调里删掉旧 fd,在onOpen里覆盖新 fd。另外检查hGet的 key 是不是字符串,Redis 里数字 key 和字符串 key 可能不匹配。
5.5 现象:Swoole 跑几小时后内存暴涨
原因:max_request没设,或者全局变量一直累积。解决:在server.php里设'max_request' => 10000,让 worker 处理完一定请求后自动重启。同时检查有没有把大数组存到$this->属性上,Swoole 的 worker 是常驻的,属性不会自动释放。
6. 压测与调优:用 wrk 和 Swoole 自带统计看真实承载
装完能跑只是第一步,能不能扛住才是关键。我一般用wrk压 HTTP 接口,用 Swoole 自带的stats看连接数。压测前先把worker_num调到 CPU 核数,task_worker_num设成worker_num的一半。命令:
# 压测后台登录接口,10 个连接,持续 30 秒 wrk -t4 -c10 -d30s http://your-domain/admin/index/login逻辑说明:-t4是 4 个线程,-c10是 10 个并发连接,-d30s持续 30 秒。看Requests/sec和Latency两个值。如果Latency超过 500ms,先查数据库慢查询,再查 Redis 连接数。Swoole 这边可以在server.php里加一个onWorkerStart定时输出$server->stats(),看connection_num和request_count。
调优参数上,buffer_output_size默认 2M,如果消息体大要调大;socket_buffer_size影响吞吐,一般设 2M 到 4M。heartbeat_check_interval设 30,heartbeat_idle_time设 60,配合客户端的ping一起用。最后说个血泪经验:别在 Swoole 的onMessage里做复杂数据库查询,能走 Redis 就走 Redis,否则一个慢查询会阻塞整个 worker。我习惯把耗时操作丢到task里异步处理,onMessage只做路由和推送。希望帮到你。
本文还有配套的精品资源,点击获取