企业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源码ThinkPHP5FastAdminSwoole 这套组合到底能跑出什么很多做企业服务的 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 或 TP8FastAdmin 的很多行为要重写源码类项目基本不会这么干。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_BYRedis5.0redis-cli ping坐席在线状态、消息队列常用Composer1.x 或 2.xcomposer -VFastAdmin 依赖安装用Nginx1.18nginx -v反向代理 WebSocket 要加 Upgrade 头安装 Swoole 扩展的命令按 PHP 版本走常见做法是 pecl# 安装 Swoole 扩展版本按 PHP 版本选 pecl install swoole-4.8.13 # 在 php.ini 里启用 echo extensionswoole.so /etc/php/7.4/cli/php.ini echo extensionswoole.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.phpAPI 入口可能是public/api.phpSwoole 启动文件通常是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但注意debugtrue时异常页会暴露路径。改完配置后访问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 21 # 查看是否监听 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服务端验证后写 RedisonClose里删掉。参数上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对应坐席或客户 IDis_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日志显示模板不存在现象访问安装页或后台白屏或 500runtime/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 里装了 SwooleCLI 没装。解决php --ini看 CLI 加载哪个 ini把extensionswoole.so加进去重启 CLI 终端再验证。别只改 FPM 的 iniSwoole 服务是 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是normalstatushidden也会登不进。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 被覆盖后权限节点全丢后台进不去最后靠数据库备份才恢复。希望帮到你。本文还有配套的精品资源点击获取