拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

企业IM客服系统PHP源码实战:ThinkPHP5+FastAdmin+Swoole安装与消息链路

企业IM客服系统PHP源码实战:ThinkPHP5+FastAdmin+Swoole安装与消息链路

简介:这份资源是一套基于ThinkPHP5、FastAdmin与Swoole构建的企业IM客服系统PHP源码,面向需要独立部署即时通讯与在线客服能力的开发者及中小企业技术团队,可用于搭建多站点统一管理的客服平台。系统覆盖会员、管理员与游客之间的即时通讯与群聊,支持多客服坐席无数量限制、按规则分配客服、一对一沟通、知识库智能匹配回复及转人工,并具备离线消息推送、历史消息、uni-app接入、群禁言与免打扰等能力,消息类型涵盖文字、表情、图片、音视频、链接、附件与转发引用,采用WSS加密传输并适配HTTPS。压缩包为zip格式,共约2000个文件,以1215个js、172个html、155个json、107个css及43个vue等前端资源为主,辅以md与txt说明文档、sql建表脚本和sh部署脚本,整体约25.56MB。目前已有298人学习关注,适合希望快速落地企业级IM客服系统的开发者参考与二次开发。

1. 企业IM客服系统PHP源码:ThinkPHP5+FastAdmin+Swoole 这套组合到底能跑出什么

很多做企业服务的 PHP 开发者都遇到过这个场景:客户要一套内部客服系统,要求能多坐席同时在线、消息实时推送、后台能管工单和客户资料,预算不高,交付周期还紧。这时候如果从零写 WebSocket 网关、自己搭后台权限、再手搓一套客服工作台,工期直接失控。标题里这套「ThinkPHP5 + FastAdmin + Swoole」的组合,本质上是把三件事拼在一起:FastAdmin 负责后台管理和 CRUD 快速生成,ThinkPHP5 作为底层 MVC 框架承载业务逻辑,Swoole 扛住长连接和实时消息推送。它解决的不是「做一个微信」这种量级的问题,而是中小企业内部 IM 客服、在线咨询、工单流转这类需求。适合谁?适合已经会 ThinkPHP、能看懂 FastAdmin 后台菜单和权限表、但没怎么碰过 Swoole 常驻内存模型的 PHP 后端。这套源码类项目的价值不在代码本身,而在于它把「Web 后台 + 长连接服务 + 安装部署」的完整链路摊开了,你照着跑通一遍,后面接类似需求就有底稿。

2. 环境准备与源码结构:把 ThinkPHP5、FastAdmin、Swoole 三者的边界划清楚

2.1 为什么是这三个东西拼在一起,而不是换别的

先讲选型逻辑,不然装到一半会怀疑人生。FastAdmin 是基于 ThinkPHP5 的后台框架,它的强项是 CRUD 一键生成、权限节点自动注册、菜单树管理,客服系统里「坐席管理、客户标签、工单分类、快捷回复」这些后台功能,用 FastAdmin 的 crud 命令几分钟就能出雏形。ThinkPHP5 在这里不是「最新版」,而是 FastAdmin 的依赖底座,你换 TP6 或 TP8,FastAdmin 的很多行为要重写,源码类项目基本不会这么干。Swoole 负责的是 ThinkPHP 不擅长的部分:常驻内存、WebSocket 握手、心跳检测、消息广播。三者边界要清楚:HTTP 请求走 ThinkPHP5 的控制器,后台页面走 FastAdmin 的视图和 JS,实时消息走 Swoole 的 WebSocket 服务,Swoole 里要查数据库时再引导 ThinkPHP 的 Db 类或直接 PDO。

提示:不要把 Swoole 服务写成 ThinkPHP 控制器的一个方法,常驻内存和每次请求销毁的生命周期完全不同,混在一起会出现连接状态丢失、内存泄漏。

2.2 安装前必须确认的版本与扩展清单

源码类项目最容易翻车的地方就是版本。FastAdmin 对 ThinkPHP5 的版本有要求,Swoole 扩展又和 PHP 版本强绑定。下面这张表是我一般会先核对的内容,缺一个后面就报错。

组件建议版本检查方式说明
PHP7.2 ~ 7.4php -vSwoole 4.x 对 PHP 8 支持有限,源码项目多按 7.x 写
Swoole 扩展4.4 ~ 4.8php --ri swoole必须确认swoole.use_shortname和 WebSocket 支持
MySQL5.7 或 8.0mysql -V注意sql_mode里的ONLY_FULL_GROUP_BY
Redis5.0+redis-cli ping坐席在线状态、消息队列常用
Composer1.x 或 2.xcomposer -VFastAdmin 依赖安装用
Nginx1.18+nginx -v反向代理 WebSocket 要加 Upgrade 头

安装 Swoole 扩展的命令按 PHP 版本走,常见做法是 pecl:

# 安装 Swoole 扩展,版本按 PHP 版本选 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 --ri swoole | grep -i version

这段命令的逻辑是:Swoole 是 PHP 扩展,不是 Composer 包,必须编译进 PHP。pecl install会拉源码编译,编译前确认gcc、make、php-dev已装。参数上,swoole-4.8.13是相对稳的版本,太新的 5.x 对旧代码兼容性差。验证时如果php --ri swoole没输出,说明 CLI 和 FPM 的 ini 没同步,WebSocket 服务用 CLI 跑,后台用 FPM,两边都要启用。

2.3 源码目录怎么摆,入口文件怎么分

拿到源码包后,先别急着访问安装页。目录结构一般是这样:application/是 ThinkPHP5 的应用目录,public/是 Web 入口,extend/放自定义类,swoole/或server/放 WebSocket 启动脚本。FastAdmin 的后台入口在public/index.php,API 入口可能是public/api.php,Swoole 启动文件通常是swoole/server.php或think swoole命令。你要做的是把 Web 入口和 Swoole 入口分开部署:Nginx 指向public/,Swoole 用 CLI 常驻。

# 典型目录结构确认 ls -la # application/ public/ extend/ runtime/ vendor/ swoole/ # 给 runtime 和 public/uploads 写权限 chmod -R 755 runtime public/uploads chown -R www-data:www-data runtime public/uploads

权限这步看着简单,但 FastAdmin 安装时写配置文件、Swoole 运行时写日志都依赖它。runtime目录如果不可写,安装页会卡在「写入配置」;public/uploads不可写,后台上传头像和附件会失败。参数上,755是目录权限,文件一般644,不要图省事给777,线上环境这是隐患。

3. 安装教程落地:从数据库导入到后台能登录的最小闭环

3.1 数据库导入与配置文件修改

源码包里的 SQL 文件一般在根目录或install/下,文件名可能是fastadmin.sql或im.sql。导入前先建库,字符集用utf8mb4,不然客服消息里的 emoji 会变问号。

# 创建数据库,字符集必须 utf8mb4 mysql -uroot -p -e "CREATE DATABASE im_customer DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 导入 SQL mysql -uroot -p im_customer < fastadmin.sql # 确认表数量 mysql -uroot -p im_customer -e "SHOW TABLES;" | wc -l

导入后改配置文件。FastAdmin 的数据库配置在application/database.php,也有项目放在application/extra/site.php。要改的是hostname、database、username、password、hostport。如果源码用了多数据库或 Redis,还要看application/config.php里的cache和redis段。

// application/database.php 关键段 return [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'database' => 'im_customer', 'username' => 'im_user', 'password' => 'your_password', 'hostport' => '3306', 'charset' => 'utf8mb4', 'prefix' => 'fa_', 'debug' => false, ];

参数说明:prefix是表前缀,FastAdmin 默认fa_,如果你导入的 SQL 用了别的前缀,这里必须一致,否则后台登录时查fa_admin表会报不存在。debug线上设false,本地调试设true,但注意debug=true时异常页会暴露路径。改完配置后访问http://你的域名/,如果出现 FastAdmin 的安装向导或登录页,说明数据库通了。

3.2 后台管理员创建与权限节点刷新

源码包通常自带一个默认管理员,但密码可能是加密的或需要重置。FastAdmin 的密码用md5加盐,盐在application/extra/site.php的salt字段。如果你不知道默认密码,最稳的方式是用命令行生成一个新密码,直接更新数据库。

// 生成 FastAdmin 管理员密码,在项目根目录用 php think 或临时脚本跑 <?php $password = 'admin123'; $salt = '你的site.php里的salt值'; $md5 = md5($password . $salt); echo $md5;

拿到$md5后更新fa_admin表:

UPDATE fa_admin SET password = '生成的md5值', status = 'normal' WHERE username = 'admin';

登录后台后,第一件事是去「权限管理 -> 权限节点」刷新节点,再给角色分配菜单。FastAdmin 的权限是「规则表 + 角色组 + 管理员」三层,源码导入后节点可能没同步,不刷新会出现菜单点了 403。刷新节点的入口在后台右上角或权限管理页,点一次会扫描所有控制器生成节点。这一步不做,后面客服工作台的菜单出不来。

3.3 Swoole WebSocket 服务启动与 Nginx 反代

后台能登录只完成一半,实时消息要靠 Swoole。启动脚本一般在swoole/server.php,用 CLI 跑:

# 启动 WebSocket 服务,监听 9501 php swoole/server.php start # 后台常驻,输出日志 nohup php swoole/server.php start > runtime/swoole.log 2>&1 & # 查看是否监听 netstat -tlnp | grep 9501

启动参数里,start是前台运行,-d是守护进程。我一般先用start看报错,确认没问题再-d。如果报Address already in use,说明 9501 被占,改端口或杀进程。Swoole 服务起来后,前端 WebSocket 不能直接连 9501,要用 Nginx 反代,因为浏览器页面是 HTTPS 的话,WS 要升级成 WSS。

# Nginx 反代 WebSocket 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"; proxy_set_header Host $host; proxy_read_timeout 3600s; }

proxy_set_header Upgrade和Connection这两行是 WebSocket 握手的关键,少了就变成普通 HTTP 请求,前端会报WebSocket connection failed。proxy_read_timeout设长一点,不然坐席挂机几分钟就被断开。前端连接地址写wss://你的域名/ws,对应 Nginx 的location /ws。

4. 客服工作台与消息链路:坐席分配、心跳、离线消息怎么串

4.1 坐席在线状态用 Redis 还是 MySQL

坐席在线状态是客服系统的核心。常见做法是用 Redis 的SET或Hash存agent_id -> fd映射,因为 Swoole 的fd是连接标识,坐席上线时绑定,下线时删除。MySQL 存状态会有写入频繁、查询慢的问题,不适合秒级变化。Redis 结构可以这样设计:

# 坐席在线集合 SADD online_agents 1001 # 坐席与 fd 映射 HSET agent_fd_map 1001 12 # 设置过期,防止异常下线残留 EXPIRE online_agents 3600

在 Swoole 的onOpen回调里,前端传agent_id和 token,服务端验证后写 Redis;onClose里删掉。参数上,EXPIRE是兜底,正常下线会主动删,但进程崩溃时靠过期清理。如果源码里用 MySQL 存online_status,你会看到坐席列表刷新延迟很大,这是选型问题,不是代码 bug。

4.2 消息发送、广播与离线消息落库

消息链路分三种:坐席发给客户、客户发给坐席、系统广播。Swoole 里用$server->push($fd, $data)发单点,用$server->connections遍历发广播。但要注意,push的fd必须是当前 worker 进程里的连接,跨进程要用sendMessage或 Redis 队列。

// Swoole onMessage 里处理消息 public function onMessage($server, $frame) { $data = json_decode($frame->data, true); $type = $data['type'] ?? 'chat'; if ($type === 'chat') { // 落库,用 ThinkPHP 的 Db 类 Db::name('chat_message')->insert([ 'from_id' => $data['from_id'], 'to_id' => $data['to_id'], 'content' => $data['content'], 'create_time' => time(), ]); // 查接收方 fd $toFd = Redis::hGet('agent_fd_map', $data['to_id']); if ($toFd && $server->exist($toFd)) { $server->push($toFd, json_encode($data)); } else { // 离线,标记未读 Db::name('chat_message')->where('id', $insertId)->update(['is_read' => 0]); } } }

逻辑说明:先落库再推送,保证消息不丢。$server->exist($toFd)判断连接是否还在,不在就走离线逻辑。参数上,from_id和to_id对应坐席或客户 ID,is_read默认 0,客户下次上线拉未读。如果源码里先推送再落库,断线时会丢消息,这是常见坑。

4.3 心跳检测与断线重连的前端配合

Swoole 默认不会主动检测死连接,需要开heartbeat_check_interval和heartbeat_idle_time。

// 在 server 配置里 $server->set([ 'heartbeat_check_interval' => 60, 'heartbeat_idle_time' => 120, ]);

含义是每 60 秒检查一次,120 秒没消息就断开。前端要配合发心跳包,一般 30 秒一次:

// 前端心跳 setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping', time: Date.now() })); } }, 30000);

服务端收到ping回pong,不落库。如果前端不发心跳,120 秒后连接被断,坐席会以为「消息发不出去」,其实是连接没了。断线重连用onclose里setTimeout重连,重连后要重新绑定agent_id到 Redis,不然消息推不到新 fd。

5. 避坑与排查:这套源码跑不起来时先看这五条

5.1 安装页 500,日志显示模板不存在

现象:访问安装页或后台,白屏或 500,runtime/log里报template not exists。原因:FastAdmin 的视图路径依赖application/admin/view/,源码包如果缺视图文件,或者view_replace_str配置指向了错误目录,就找不到模板。解决:确认application/admin/view/下有对应控制器名的目录,检查application/config.php里template段的view_path,一般不用改,但如果你把 admin 模块改名了就要同步。

5.2 Swoole 启动报「Cannot find PHP extension」

现象:php swoole/server.php start报扩展未加载。原因:CLI 和 FPM 用的 php.ini 不是同一个,你在 FPM 里装了 Swoole,CLI 没装。解决:php --ini看 CLI 加载哪个 ini,把extension=swoole.so加进去,重启 CLI 终端再验证。别只改 FPM 的 ini,Swoole 服务是 CLI 跑的。

5.3 WebSocket 连接 404 或 502

现象:前端连wss://域名/ws报 404 或 502。原因:Nginx 的location /ws没配,或者 Swoole 服务没启动,或者端口不对。解决:先netstat确认 9501 在听,再检查 Nginx 配置里proxy_pass的端口和 Swoole 一致,最后看 Nginx 错误日志error.log。502 一般是 Swoole 挂了,404 是路径没匹配。

5.4 后台登录提示「账号或密码错误」但密码是对的

现象:明明改了密码,还是登不进。原因:FastAdmin 密码校验用md5(密码 + salt),salt 在application/extra/site.php,如果你改密码时用的 salt 和系统里的不一致,就对不上。解决:直接看site.php里的salt,用它重新生成 md5,更新fa_admin表。另外确认status是normal,status=hidden也会登不进。

5.5 消息发送成功但对方收不到

现象:坐席 A 发消息,数据库有记录,坐席 B 没收到。原因:B 的 fd 在 Redis 里过期了,或者 B 连的是另一个 Swoole worker 进程,push跨进程失败。解决:检查agent_fd_map里 B 的 fd 是否存在,EXPIRE时间是否太短;如果用了多 worker,要用sendMessage跨进程通信或 Redis 订阅广播。单 worker 测试没问题、多 worker 出问题,基本就是这个。

6. 进阶:把客服系统从「能跑」推到「能扛」的几个具体技巧

第一件事是把 Swoole 的 worker 数和 task 进程调对。默认worker_num是 CPU 核数,但客服系统 IO 密集,可以设成核数的 2 到 4 倍。task_worker_num用来处理落库、发邮件这类耗时操作,不要放在onMessage里同步做,否则消息延迟肉眼可见。

$server->set([ 'worker_num' => 4, 'task_worker_num' => 2, 'daemonize' => true, 'log_file' => __DIR__ . '/../runtime/swoole.log', ]);

参数上,daemonize线上设true,本地调试设false方便看输出。log_file一定要写,Swoole 的报错不看日志根本找不到。落库操作丢到onTask里:

public function onTask($server, $taskId, $workerId, $data) { // 异步落库 Db::name('chat_message')->insert($data); return 'ok'; }

第二件事是消息表加索引。客服消息量上来后,chat_message表没索引,查历史消息会全表扫。至少加from_id、to_id、create_time的联合索引,按时间倒序查。

ALTER TABLE fa_chat_message ADD INDEX idx_from_to_time (from_id, to_id, create_time); ALTER TABLE fa_chat_message ADD INDEX idx_to_read (to_id, is_read);

第三件事是验证方法。别只看页面能发消息就完事,用wscat或浏览器控制台模拟多坐席并发,看 Redis 里的 fd 映射是否准确,看runtime/swoole.log有没有connection closed异常。我一般会开三个浏览器窗口,两个坐席一个客户,互发 50 条消息,然后杀掉一个坐席进程,看离线消息是否落库、重连后是否补推。这个测试能暴露 80% 的状态同步问题。

最后说个习惯:这套源码类项目,我从来不在生产环境直接改vendor/里的东西,FastAdmin 和 ThinkPHP 的升级会覆盖。要改就改application/和extend/,Swoole 脚本单独放swoole/,部署时用 git 管理,别用 FTP 覆盖。踩过一次坑,客户线上 FastAdmin 被覆盖后权限节点全丢,后台进不去,最后靠数据库备份才恢复。希望帮到你。

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

返回列表