1. 为什么要把 Workerman 塞进 BuildAdmin 里
1.1 BuildAdmin 确实是好东西,但它自己解决不了“实时”
先交代一下背景。我这两年接了不少后台管理类的项目,从内容发布系统到电商运营后台,再到企业内部 OA,基本离不开一整套“快速开发后台”的底座。BuildAdmin 在这个场景里是我用过最顺手的一套方案,因为它直接解决了传统后台开发里最磨人的几个环节:基于 ThinkPHP 8 的 PHP 端、基于 Vue3 + Element Plus 的前端、RBAC 权限、菜单管理、CRUD 生成、用户登录、操作日志全都有了。说是“开箱即用”一点不过分,装上之后不需要从零搭那种千篇一律的布局和管理逻辑,直接进入业务开发。
但用久了你会发现一个问题:BuildAdmin 再强,它的通讯模型还是传统的 HTTP 请求-响应模式。在这种模型下,服务器没办法主动把消息推给浏览器。比如后台收到一笔新订单,管理员想第一时间知道,只能靠刷新页面或者自己写一个轮询接口隔几秒去拉一次。轮询也不是不行,但请求一多,数据库和 Nginx 的压力都会上来,而且要保证及时性就得缩短轮询间隔,体验很别扭。
我最初是在给一个客户做“订单语音播报”功能时遇到这个瓶颈的。客户要求收银台在收到线上订单时,那个 Vue 后台页面不刷新也必须立刻弹出一条消息并播放一声提示音。这个需求用纯 BuildAdmin 是做不到的,必须引入长连接通道。我考察了一圈方案,最后选定的是 Workerman,并且把它整合进了 BuildAdmin,做了个可以直接拿来用的包。发布几个月,到现在已经有近 2000 次下载使用,中间踩了不少坑,也收到了不少用户反馈。这篇就系统讲一下这个整合方案的设计思路、核心实现和我在过程中趟出来的经验。
1.2 为什么选 Workerman 而不是 Swoole
提到 PHP 长连接,大家第一反应往往是 Swoole。Swoole 确实很强,性能上限更高,协程、进程模型都很成熟。但我最终没选它,原因很实际:Swoole 是一个 C 扩展,对服务器环境有要求,很多做传统 PHP 外包的同学,代码要交付到客户指定的服务器上,或者跑在一些不能随便装扩展的虚拟主机环境里,Swoole 根本装不上。为了一个实时消息功能去跟运维扯半天,不值得。
Workerman 是纯 PHP 写的常驻内存框架,不依赖任何扩展,只要 PHP 环境能跑起来,Composer 一装就能用。它底层用的是 stream_socket_server 和 pcntl_fork(Windows 上还有基于 select 的模拟实现),所以在开发机上也能跑,部署和调试的友好度比 Swoole 高一个量级。
另外,构建这个整合方案的时候,BuildAdmin 本身就是基于 ThinkPHP 8 的,TP8 的事件机制和容器管理比历史版本要更灵活。Workerman 对“在常规 PHP 框架内嵌入常驻服务”这件事的适配非常好,它不强行接管框架的整个生命周期,只是作为一组进程跑在业务旁边,需要的时候通过框架的容器去调用业务逻辑。这意味着整合之后,原来怎么用 BuildAdmin 写接口,现在还怎么用,我完全没有必要改变已有的开发习惯。
1.3 整合后到底能干什么
如果说 BuildAdmin 是“给你一台组装好的电脑”,那我做的这个整合方案就是“给这台电脑加了一张网卡”,让它能连上实时通讯的网络。具体来说,可以做到这几件事:
- WebSocket 实时消息推送:后台页面在线状态下的新订单提醒、待办任务推送、消息中心实时落地,不用刷新页面。
- 异步任务队列消费:把邮件发送、短信通知、批量导出这类耗时操作丢到 Workerman 进程里慢慢跑,HTTP 请求秒回,不再拖累接口响应。
- 定时任务:把以前挂在服务器 crontab 上的 PHP 脚本统一收编进 Workerman 的定时器里,还能直接复用 TP8 的模型和业务逻辑类。
- 进程内服务通讯:多个 Worker 进程之间可以用 Workerman 自带的 Channel 组件做数据订阅,这个在后续扩展业务的时候很好用。
这套方案适合的人群也很明确:已经在用或准备用 BuildAdmin 做项目的同学,尤其是独立开发者、外包团队、企业内部的二开人员。你们需要给后台加上实时能力,又不想把整套架构推倒重来,那这个整合包就是一个比较轻的增量方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整合方案的目录规划与启动链路
2.1 文件结构怎么摆,代码才不显得乱
做整合方案最怕的是把 Workerman 的文件和 TP8 的业务代码揉成一团。BuildAdmin 原本有它自己清晰的目录层级,如果为了装一个 Workerman 就把 app 目录搞得乌烟瘴气,那这套方案还没开始用就已经失败了。
我在设计时采用了“平行目录 + 独立启动入口”的方式,把跟 Workerman 相关的全部代码收拢在项目根目录的一个单独文件夹里,和 TP8 的核心目录互不打扰。这是我在实际项目中摸索出的目录方案:
text复制project-root/
├── app/ # TP8 应用目录,保持原样
├── config/ # TP8 配置目录,保持原样
├── route/ # 路由文件
├── vendor/ # Composer 依赖
├── workerman/
│ ├── start.php # Workerman 主启动文件
│ ├── server/
│ │ ├── WebSocket.php # WebSocket 服务类
│ │ ├── QueueWorker.php # 异步队列消费进程
│ │ └── CrontabWorker.php # 定时任务进程
│ ├── process/
│ │ ├── WebsocketProcess.php
│ │ ├── QueueProcess.php
│ │ └── CrontabProcess.php
│ └── config/
│ ├── server.php # Workerman 服务配置
│ └── depend.php # 容器依赖注入配置
├── buildadmin.sql # 数据库脚本(原样)
└── think # TP8 命令行入口
关键设计点有这些:
workerman/config/server.php统一放端口、进程数、运行模式等配置,不在代码里写死。workerman/server/下的类主要负责 Workerman 事件回调,比如onWorkerStart、onMessage、onClose。workerman/process/下的类则引用app/里的业务逻辑,比如调用 TP8 内的服务层类,实现业务复用。workerman/start.php是唯一启动入口,通过命令行执行php workerman/start.php start就可以拉起所有进程。
为什么这么分?核心是为了责任隔离。无论你是接别人的项目,还是自己后续回来维护,一看目录就知道哪部分是整合层的代码,哪部分是业务代码。以后升级 BuildAdmin 或者修改 Workerman 的版本,只动 workerman/ 目录基本就够了。
2.2 从 php think 到 Workerman 的启动链路改造
所有 TP8 开发者都熟悉 php think 这个命令行指令,BuildAdmin 的后台数据填充、管理员维护这些操作都依赖它。在做整合时,我没有另起炉灶,而是让 Workerman 的启动过程本身也复用 TP8 的 Console 内核。
说得具体一点:Workerman 的 start.php 里没有用传统的 require __DIR__ . '/../vendor/autoload.php' 就结束,而是把 TP8 的 bootstrap/app.php 完整拉起来,然后从中取到 Container 实例,再把这个实例传递给 Workerman 的事件回调类。这样在 Workerman 进程里调用 Db::name()、Cache::get()、event() 这些门面时,它们全部指向同一个 TP8 容器,不会出现两边各有一套配置、数据库连接各管各的尴尬。
这里有一段简化的示例代码,展示 start.php 里的关键逻辑:
php复制<?php
// workerman/start.php
use Workerman\Worker;
use think\App;
// 1. 加载 TP8 内核
require __DIR__ . '/../vendor/autoload.php';
$app = new App();
$app->initialize();
// 2. 从容器中取出配置
$serverConfig = $app->config->get('workerman.server');
// 3. 注册各类 Worker 进程
$wsWorker = new Worker('websocket://0.0.0.0:' . $serverConfig['websocket_port']);
$wsWorker->count = $serverConfig['websocket_process_count'];
$wsWorker->onWorkerStart = function ($worker) use ($app) {
// 每个子进程启动时,重新绑定容器到进程上下文
app()->instance('app', $app);
// 这里再实例化具体的业务处理类
};
// 其他进程类似...
// 4. 启动运行
Worker::runAll();
内部细节比这个复杂,但你要抓的核心链路就一条:TP8 容器先初始化,再创建 Workerman 进程,进程内的一切业务调用都从容器里取。
有几个细节必须提醒你:
.env配置文件里要增加WORKERMAN_WEBSOCKET_PORT等独立环境变量,不要把端口配置和 BuildAdmin 原本的 HTTP 端口搞混。- 如果你用了
php think做计划任务迁移,整合后可以逐步迁到CrontabWorker,但别急着删,先跑线上稳定两三天再切换。 - 每一台服务器只允许一条
start.php启动指令同时运行,否则会重复消费队列或者端口冲突,这在后面的平滑重启章节会专门讲。
2.3 WebSocket 握手阶段如何复用 BuildAdmin 的 JWT 认证
BuildAdmin 的用户认证是基于 JWT 的,前端登录后拿到 token,每次请求时带在请求头里。WebSocket 也是 TCP 连接,照样可以复用这一套认证体系。只不过 HTTP 请求的 Header 由浏览器自动携带,而 WebSocket 在握手阶段,浏览器会把 Sec-WebSocket-Protocol 或查询参数暴露给我们,我们就在这个环节把 token 解析一遍。
我的做法是:前端在创建 WebSocket 对象时,把 token 放在 URL 查询参数上,例如 ws://your-domain:8282?socket_token=xxxxx,然后在 onWebSocketConnect 回调里取到这个参数,调用 BuildAdmin 用户模块的 parseToken 方法做校验。校验通过则记录 connection->uid,让这个连接和用户 ID 绑定;校验失败就直接 close 掉连接,不给他任何通信机会。
示例代码大致是这样:
php复制<?php
namespace workerman\server;
use Workerman\Connection\TcpConnection;
use think\facade\Db;
class WebSocketServer
{
public function onConnect(TcpConnection $connection)
{
// 解析 URL 里的 token
$query = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY) ?? '';
parse_str($query, $params);
$token = $params['socket_token'] ?? '';
// 调用 BuildAdmin 的 JWT 校验服务
$user = $this->validateToken($token);
if (!$user) {
$connection->close(json_encode(['code' => 401, 'msg' => 'unauthorized']));
return;
}
// 绑定 uid(实际使用建议绑定连接 id 和 uid 的映射,存 Redis)
$connection->uid = $user['id'];
}
}
这里有个容易踩的坑:WebSocket 的 URL 查询参数在握手成功后会一直保留在请求 URI 里,所以你在 onMessage 阶段读取的是同一个 URI。如果你把 token 暴露出去了,等连接建立后,建议在 onWebSocketConnect 阶段把 token 从内存中清掉,避免后续日志或调试时泄露敏感信息。
另外,JWT 默认有有效期,后台管理场景下,用户如果长时间不操作,token 会过期。我的方案是引入“心跳 + 续签”机制:前端每 30 秒发送一个 Ping 消息,后端收到后检查 token 剩余有效时间,如果发现不足 10 分钟,就顺手签一个新的 token 下发给前端。这个机制避免了用户在后台挂一上午之后突然断线重连,结果发现 token 过期、还得重新登录的尴尬。
3. 让 Workerman 和 BuildAdmin 的容器真正变成一家人
3.1 解决数据库连接和 ORM 复用的问题
常驻内存和传统 PHP-FPM 的一个核心区别在于:FPM 模式下,每个请求处理完,PHP 进程就释放了所有资源,下一个请求是全新状态;而 Workerman 进程是永不退出的,所有静态变量、单例对象、数据库连接都在内存里长期存活。
这意味着你在 Workerman 里如果用 TP8 的 Db::name('user')->find(1),第一次请求会建立 MySQL 连接,第二次请求如果还复用同一个连接,而 MySQL 服务端因为 wait_timeout 已经把连接断开了,你直接查询就会报 MySQL server has gone away。
这个问题不解决,这套方案就根本没法在生产环境用。我提供的包里已经处理了这一层:
- 在
onWorkerStart时调用Db::clear()清理残留连接。 - 实现了一个
DatabaseHeartbeat类,在每次处理消息之前检查连接有效性,无效则重新建立连接。 - 通过
Container::pull()获取新的应用实例,避免门面模式下的静态缓存串号。
有一个更便捷的做法是用 TP8 提供的 think\db\PDOConnection 的 getPdo() 方法自己包一层重试机制,这个方法会在底层判断连接是否可用,不可用时它会自动重新连上。不过在 Workerman 中,我们不建议把所有查询都在线重试,那样如果数据库故障,进程会陷入死循环。稳妥的方案是捕获到 PDOException 后判断 errorInfo[1] 是否为 2006(MySQL server has gone away)或 2013(Lost connection),是就重连一次并重试当前查询,不是就直接抛出并记录日志。
3.2 多进程环境下,怎么处理进程间的数据共享
Workerman 的每个 Worker 进程是独立的内存空间,进程 A 里的一个普通变量,进程 B 是完全看不到的。这个特性在写常规业务时没影响,但一旦涉及“所有进程都要感知的数据”就会出问题。
举个例子:你的 WebSocket 服务有 4 个进程,用户 A 连接在进程 1 上,用户 B 连接在进程 2 上。当用户 A 发出一条消息想推送给用户 B 时,进程 1 不能直接遍历到进程 2 里的连接对象。这时候就需要一个中间层来协调。
我的解决方案分两个场景处理:
- 面向用户的连接映射:统一存到 Redis Hash,key 为
ws_user_connections,field 为用户 ID,value 为连接所属的 Worker 进程 ID + 连接 ID。当进程 1 需要推送消息给用户 B 时,先去 Redis 查出ws_user_connections里 B 的记录,如果 B 所在进程就是自己,直接推送;如果不是,通过 Workerman 的Channel\Client发广播给进程 2,让进程 2 去执行推送。 - 进程内共享的计数器或状态:不要用静态变量,统一放进 Redis 或者数据库。比如在线人数统计,每次连接握手成功后 Redis
incr一次,断开时decr一次,页面需要显示实时在线人数时直接读这个 key。
会有读者问,为什么不用 Workerman 自带的 GatewayWorker 方案?GatewayWorker 其实就是把 WebSocket 的分布式通信做了封装,连接网关相互独立,天然支持跨进程推送。但 GatewayWorker 需要额外部署 Gateway 和 BusinessWorker 两层,架构复杂度比纯 Workerman 高一点。在这个整合包里,我保留了纯 Workerman 的部分,同时也给出了一个基于 Channel 的轻量跨进程通信组件,让构建方自行选择。多数业务下用 Channel 就够了,不需要引入完整的 Gateway 体系。
3.3 定时任务和异步队列在常驻进程里的正确打开方式
BuildAdmin 自身的定时任务功能依赖 HTTP 请求触发或 crontab 定时访问某个 URL,这在传统模式下没问题,但在“需要秒级精度”或“不想让外部访问某个后台 URL”的场景下就不太方便了。整合 Workerman 之后,定时任务可以直接写进工作进程里。
常用的方式是利用 Workerman 自带的 Timer 类:
php复制<?php
use Workerman\Timer;
// 每 60 秒执行一次
Timer::add(60, function () {
// 在这个回调里直接调用 TP8 的业务类
$orderService = app(\app\service\OrderService::class);
$orderService->checkTimeoutOrders();
});
使用 Timer 时有三个经验要分享:
第一,Timer::add 默认是在当前 Worker 进程内执行,如果这个进程负责 WebSocket 连接,回调里出现了阻塞调用(比如同步发邮件、请求外部 API),会影响这个进程上所有连接的通讯。所以我在整合包里把定时任务独立成一个 CrontabWorker 进程,不和 WebSocket 进程混跑。
第二,定时任务回调里的异常一定要 catch 干净,绝对不能让它把 Worker 进程搞挂。常驻进程一旦挂掉,如果没有守护进程拉起,整个定时任务链就断了,而你很难第一时间发现。
第三,用 Timer 做队列消费时,注意并发控制。工作进程数设为 2 到 3 即可,如果你开 8 个进程同时消费同一个队列,数据库瞬时会收到大量相同类型的处理任务,可能造成锁冲突。我实际测试下来,2 个消费进程对于绝大多数中小项目已经够用,真正不够的时候该排查的是单条任务的执行耗时。
队列这块,我推荐直接把任务投递给 Redis 的 list,消费进程通过 BLPOP 阻塞获取。理由很简单:Workerman 进程本身不会退出,BLPOP 的阻塞等待不占 CPU,这是最适合常驻进程的消费模型。有的教程会让你用 while(true) 轮询,那是很笨的做法,不但空转 CPU,消息时效性也差。
4. 我已经踩平并在包内解决的坑
4.1 常驻内存里最大的坑:代码改了半天,就是不生效
在传统 PHP-FPM 模式下,改一个文件,下一次请求立刻就会用新代码。Workerman 常驻内存模式下,代码在进程启动时就加载进内存了,启动之后你再改文件,进程并不知道,它继续跑内存里的旧代码。
这是所有从 FPM 思维迁移到常驻内存开发的人都会撞的墙。我收到过好几个人反馈:“我改了数据库连接配置,重新 start 了也没用。”排查半天,原因是他们用的命令是 php workerman/start.php reload,而 reload 只是重启业务进程,不会重新加载已经被 opcache 或 PHP 缓存住的代码内容,甚至部分情况下是连 worker 进程都没真正退出。
我提供的包做了一件事:在开发模式下,启动命令里加了检测文件哈希的功能,检测到 app/ 或 workerman/ 目录里的 PHP 文件发生变化,就自动执行平滑重载。代码类似:
php复制// 开发模式下,比对最近修改时间
$lastHash = (string)cache('dev_file_hash');
$currentHash = md5(implode(',', $this->scanPhpFiles('app')));
if ($currentHash !== $lastHash) {
cache('dev_file_hash', $currentHash);
// 通知 Worker 进程 reload
POSIX::kill($masterPid, SIGUSR1);
}
这个机制只在开发环境开启,生产环境绝对不要开,因为生产环境在线用户多,频繁 reload 会导致连接中断。生产环境正确的姿势是:用代码发布流程,在服务器上 php workerman/start.php reload 手动执行平滑重载,或者干脆维护一个 systemd 服务,代码发布后执行 systemctl restart workerman(注意是 restart,不是 reload)。
顺带一提,opcache 也可能导致旧代码残留。生产环境如果开了 opcache.enable,重载进程前建议在 shell 里先清理一次:
bash复制php -r "opcache_reset();"
4.2 守护进程:没有它,你的服务挂了没人知道
Workerman 的 start 命令在前台运行,关闭终端进程就没了;start -d 可以在后台守护运行,但这还不够。服务器重启、进程被误杀、内存泄漏导致进程崩溃,这些都不是 Workerman 自己能解决的。
我整合包里给出了一个 systemd 服务文件,这是我在生产服务器上用了很久的方案:
ini复制[Unit]
Description=BuildAdmin Workerman Server
After=network.target
[Service]
Type=simple
User=www
Group=www
WorkingDirectory=/var/www/html/buildadmin
ExecStart=/usr/bin/php /var/www/html/buildadmin/workerman/start.php start
ExecReload=/bin/kill -USR1 $MAINPID
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
重点在 Restart=always,这个配置配合 systemd 的自动拉起能力,一旦进程崩溃,3 秒后自动恢复,不用人工干预。
还有一点经验之谈:User=www 这个用户权限要控制好。Workerman 启动后要能写日志、能连 MySQL 和 Redis,如果权限过低可能启动失败;但权限也不能给太高,否则一旦代码被注入恶意代码,攻击者获得的就是 www 甚至是 root 的权限,后果我不多说了。
4.3 多进程下数据库、Redis 连接数容易爆
单一 Workerman 进程只会维持一个 MySQL 连接、一个 Redis 连接,这听起来很美。但如果你开了 4 个 WebSocket 进程、2 个队列进程、1 个定时进程,那就是 7 个 MySQL 连接、7 个 Redis 连接起步。这还没算上 BuildAdmin 原本通过 FPM 处理 HTTP 请求时占用的连接。MySQL 默认 max_connections 是 151,Redis 默认没有连接数上限,但每个连接都有内存开销。
我在压测时发现,仅启动上述 7 个进程,MySQL 连接数就比纯 FPM 部署多出 7 个,而这 7 个连接是长期存活、不释放的。很多用户原有的 MySQL 实例连接数配置较小,一旦上线这套整合方案,数据库会出现间歇性的 Too many connections。
解决思路有两个方向:
- 给 Workerman 的数据库配置单独设置
break_reconnect = true(TP8 的database.php配置项),出现断线时自动重连。 - 生产环境如果并发确实高,可以给 MySQL 的
max_connections适当上调,或者在mysql配置里增加wait_timeout的缩短,让闲置连接尽快被回收。
Redis 方面倒是问题不大,但要注意 select 库位的问题。Workerman 进程连接 Redis 时,默认选中 0 号库。如果你在 BuildAdmin 的实现里使用了不同库位(比如 1 号库存 token、2 号库存队列),记得在 Workerman 的启动配置里把 Redis 库位参数对应设置好,否则数据读写在进程内外会不一致,排查起来特别隐蔽。
4.4 异常与日志:如何让常驻进程挂得明明白白
以前 FPM 模式,PHP 执行出错会输出到 Nginx 的错误日志里,一行一行很清晰。Workerman 常驻进程模式下,进程内的 echo 输出会直接打到终端或 nohup.out 里,而框架里用 Log::write() 记录的信息则是走 TP8 自己的日志通道。
我遇到过不少朋友说“进程崩了不知道去哪看错误”,其实只需要做好两件事:
第一,在 workerman/config/server.php 里指定 stdoutFile 和 pidFile 的路径,例如:
php复制'stdoutFile' => runtime_path() . '/workerman_stdout.log',
'pidFile' => runtime_path() . '/workerman.pid',
这样进程启动后的所有标准输出和错误输出都会写入指定日志文件,崩溃原因基本都能在里面看到。
第二,自己写一个全局异常监听,把 Worker 生命周期内抛出的异常统一捕获,写进 TP8 的日志系统。这样当线上出现问题,你不需要去服务器上翻 stdout 日志,直接通过 BuildAdmin 后台的操作日志或日志查看页面就能看到异常栈。
5. 近 2000 次下载,背后是一群什么人在用
5.1 从下载量里能看到的需求轮廓
这个包发布到现在有近 2000 次下载使用,说实话,这个数字不算夸张,但也没有水分。我作为发布者能看到的下载记录,从 IP 维度分析,大多来自国内独立开发者、中小型外包团队、以及个别企业内部 IT 部门的服务器。这个画像和 BuildAdmin 本身的使用群体高度重合,说明做后台开发的同学们卡在“实时能力缺失”这件事上的人不在少数。
为什么大家愿意用这个整合方案而不是自己去 Google 一片很零散的教程?我分析是这三点给到了省心的价值:
- 不用自己在 Workerman 官方文档和 TP8 文档之间来回翻,照着包里的示例就能把服务跑起来。
- 省掉了“WebSocket 鉴权和 BuildAdmin 用户体系打通”这个最繁琐的环节。
- 进程管理、queue、crontab 都有写好的可复用实现,不用每个项目重新造轮子。
5.2 三个真实使用场景,和各自的调优思路
我挑三个最典型的反馈场景,尽量还原他们的使用方式和调优方向,给你做个参考。
场景一:企业 OA 内部消息中心。用户的 BuildAdmin 后台里挂了一个 OA 系统,员工登录后能看到审批、公告、消息提醒。整合 Workerman 后,管理员在后台发公告,发完直接推送到所有在线员工的浏览器,新消息红点秒出现。他们的进程配置是 WebSocket 进程 2 个、队列进程 1 个、定时任务进程 1 个,线上稳定运行,几乎没有出过问题。这个场景核心是“通知推送”,对消息可靠性要求不算特别高,掉线重连后重新拉取未读列表即可。
场景二:餐饮门店收银订单推送。门店的收银页面是一个 BuildAdmin 管理的 Vue 应用,顾客在小程序下单后,订单数据进入数据库,同时触发一个队列任务,把订单信息实时推送到收银页面并语音播报。这个场景对延迟要求很高,从顾客支付成功到门店播报,他们的要求是 1 秒以内。实测下来,整个链路在局域网环境下能做到百毫秒级,语音文件通过 Base64 下发,前端用 Web Audio API 播放。优化点是把语音文件提前生成好放到 CDN,WebSocket 只下发一个 ID,前端自动拼 CDN 地址播放,避免大量数据走长连接通道。
场景三:客服在线会话。这个稍微复杂一些,因为客服会话需要有“已读”“正在输入”这类状态同步,而且消息要保存到数据库。方案是把消息先写入 MySQL,再通过队列推送给对方在线连接。这样即使对方不在线,下次打开页面也能从数据库里拉到历史消息。这个场景要重点处理的是连接稳定性,因为客服经常会关闭浏览器标签页或者切换网络,断线重连和未读消息补偿是刚需。
5.3 这套方案给你的建议和我的后续规划
如果你已经在用 BuildAdmin,打算上实时功能,我给你的建议是:先别急着把所有业务都迁到 Workerman 里,先用它解决最痛的点。比如你目前最需要的是订单提醒,那就只部署 WebSocket 服务,先把消息推送通道打通;等熟悉了整套机制,再逐步把定时任务、队列消费纳入。一口吃不成胖子,常驻进程里的 Bug 往往比 FPM 模式更隐蔽,控制好初次上线的范围,能大大降低排查问题的成本。
这个包现在的实现还是偏向“让 Workerman 成为 BuildAdmin 的补充”,后续我准备做几件事:
- 把整合包里的事件回调抽象得更通用,比如增加
onOrderPaid、onUserLogin这类业务事件的统一扩展点,让二开更简单。 - 增加更多的消息推送渠道示例,比如微信公众号模板消息、企业微信机器人这类常用渠道的接入示例,不用每个项目重新对接。
- 提供 Docker 部署的完整示例,省掉不少人在服务器环境配置上花的时间。
最后说点实在的:我见过有些开发者觉得“反正我项目不大,用不上长连接”,直接跳过这类方案。但实际是,哪怕你只是做个内部小工具,只要涉及“事件发生时用户没有主动发起请求”这种事,长连接都是最简单高效的解法。我现在做背景管理项目,几乎默认就会把这个整合方案装上,就算当前需求用不上,后面客户提“加个实时消息”的时候,不用慌,服务已经在跑了。
