admin-websocket.md 17 KB

后台实时推送:后端部署

更新日期:2026-11-01。适用于 upgrade/laravel-13 分支及本次审查修正后的完整代码。

状态:待首次上线。以下按首次部署流程执行。

本文面向后端和运维,包含迁移、环境变量、常驻进程、Supervisor、Nginx、调度和验收步骤。前端的接口、频道、事件字段及 Echo 参考实现统一在前端接入文档。

1. Reverb 与 relay 的职责

进程 命令 作用
Reverb php artisan reverb:start 维护 WebSocket 连接,把消息推给后台页面的 Laravel Echo 客户端
relay(中继) php artisan admin-realtime:relay 读取共享事件表,解析列表行和统计快照并发给 Reverb;失败重试,同时处理登录到期断连和在线状态扫描

两者各运行一个实例,并由 Supervisor 守护。此次推送链路同步发送,不需要额外的队列 worker;项目原有支付、Telegram 等队列任务及其 worker 继续保留。

2. 发布准备

本文示例项目目录为 /www/wwwroot/bot-28,WebSocket 公网域名为 admin.example.com,运行用户为 www,PHP CLI 为 /usr/bin/php。请替换成服务器实际值;网站 PHP-FPM、命令行、Supervisor 和 cron 均应使用 PHP 8.3+。若是宝塔等面板,PHP 路径可能是 /www/server/php/83/bin/php,以实际安装为准。

发布前备份业务数据库及现有后端发布目录(包括 composer.json、composer.lock、vendor 和 .env),再按项目原有发布方式部署完整代码、新迁移和配套 composer.lock。

现有未跟踪的 composer.lock 如阻止 Git 更新,先移到备份目录,使用新代码配套的 lock 文件。

3. 配置环境变量

编辑项目 .env,保留现有数据库、Redis 连接和前缀、APP_KEY 等配置。不要重新执行 key:generate。后台 JWT 密钥必须至少 32 字节,已有合格密钥可继续使用;更换后后台管理员需要重新登录。

先将推送设为关闭:

ADMIN_WS_ENABLED=false
ADMIN_WS_BROADCASTER=reverb
ADMIN_WS_SUMMARY_SCAN_SECONDS=5

# 前端 Echo 连接的公网地址:这里的 host 不带协议或路径
ADMIN_WS_PUBLIC_HOST=admin.example.com
ADMIN_WS_PUBLIC_PORT=443
ADMIN_WS_PUBLIC_SCHEME=https

REVERB_APP_ID=bot28-admin
REVERB_APP_KEY=填写第一条生成命令的结果
REVERB_APP_SECRET=填写第二条生成命令的结果

# Laravel、relay 调用 Reverb 的本机 HTTP 地址
REVERB_HOST=127.0.0.1
REVERB_PORT=8080
REVERB_SCHEME=http

# Reverb 只监听本机,公网连接由 Nginx 转发
REVERB_SERVER_HOST=127.0.0.1
REVERB_SERVER_PORT=8080
REVERB_ALLOWED_ORIGINS=admin.example.com
REVERB_SCALING_ENABLED=false

运行两次,各生成一个不同值,分别填入 KEY 和 SECRET:

openssl rand -hex 16
openssl rand -hex 16

REVERB_ALLOWED_ORIGINS 填后台页面所在的域名,不带协议;若页面和 API 域名不同,填页面域名。ADMIN_WS_PUBLIC_HOST 则填承接 WebSocket 的域名。KEY 会提供给前端,SECRET 只留在服务器。

PHP-FPM 与 relay 必须使用同一缓存存储,本项目通常使用 CACHE_DRIVER=redis;保留原有 Redis 配置即可。无需为此清空 Redis,也不要更改共享键前缀。

4. 安装依赖和执行迁移

以下命令在项目根目录执行,假定 php 已指向选定的 PHP 8.3+,Composer 也由该 PHP 运行:

cd /www/wwwroot/bot-28
php -v
composer validate --no-check-publish
composer install --no-dev --optimize-autoloader
composer check-platform-reqs --no-dev

php artisan optimize:clear --except=cache
php artisan migrate --force
php artisan migrate:status

迁移会自动跳过已经执行过的文件。本次推送相关迁移有三个:

  • 2026_10_31_160000_create_admin_realtime_events.php:共享事件表。
  • 2026_10_31_210000_add_seq_to_admin_realtime_events.php:补发用的 seq。
  • 2026_11_01_000000_add_delivery_state_to_admin_realtime_events.php:发送确认 published_at、待发送索引及持久状态表 admin_realtime_state。

migrate:status 中这三个应为已执行。不要手动清空 admin_realtime_state;它保存不会随事件历史删除的序号和发现游标。迁移及生产安装失败时先解决错误,不继续启动进程。生产使用 lock 文件安装依赖,不运行 composer update。

--except=cache 必须保留,避免 optimize:clear 连带清空共享 Redis 业务数据。

5. Nginx 配置

在已有 HTTPS 站点的 server 块内添加以下配置,保留原有 Laravel/PHP 配置:

location ^~ /app/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 120s;
    proxy_buffering off;
}

# 服务端发布消息、断开连接的接口只通过本机 8080 访问
location ^~ /apps/ {
    return 404;
}

这里必须使用 /app/,不要写成会同时匹配 /apps/ 的 /app 前缀。公网只使用站点的 HTTPS 443,8080 不需要对外开放。/admin/broadcasting/auth、/admin/realtime/config 和 /admin/realtime/replay 继续由 Laravel 处理,不反代到 Reverb。匹配规则见 Nginx 官方 location 说明。

确认 Nginx 检查通过后再重载:

nginx -t
nginx -s reload

6. 开启推送并建立缓存

把 .env 的开关改成:

ADMIN_WS_ENABLED=true

然后执行:

php artisan optimize

该命令重建配置、事件、路由和视图缓存;事件缓存必须更新,新的 Reverb 订阅检查才会加载。后续每次改 .env 或发布代码后都需重建缓存并重启常驻进程。

确保 storage、bootstrap/cache 可由进程运行用户写入。以 www 为例,由有权限的账号执行:

mkdir -p /www/wwwroot/bot-28/storage/logs/realtime
chown -R www:www /www/wwwroot/bot-28/storage /www/wwwroot/bot-28/bootstrap/cache

7. Supervisor 守护两个进程

在 Supervisor 实际使用的 include 目录创建配置,例如 /etc/supervisor/conf.d/bot28-realtime.conf。以下目录、PHP 绝对路径和用户都要与服务器一致:

[program:bot28-reverb]
directory=/www/wwwroot/bot-28
command=/usr/bin/php artisan reverb:start
user=www
numprocs=1
priority=10
autostart=true
autorestart=true
startsecs=3
startretries=10
stopsignal=TERM
stopwaitsecs=20
redirect_stderr=true
stdout_logfile=/www/wwwroot/bot-28/storage/logs/realtime/reverb.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=5

[program:bot28-admin-relay]
directory=/www/wwwroot/bot-28
command=/usr/bin/php artisan admin-realtime:relay
user=www
numprocs=1
priority=20
autostart=true
autorestart=true
startsecs=3
startretries=10
stopsignal=TERM
stopwaitsecs=30
redirect_stderr=true
stdout_logfile=/www/wwwroot/bot-28/storage/logs/realtime/relay.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=5

保存配置后执行,update 会按 autostart=true 启动两个进程:

supervisorctl reread
supervisorctl update
supervisorctl status bot28-reverb bot28-admin-relay

两个状态都应为 RUNNING。不要在启动 Supervisor 后再手动运行第二份 relay。relay 默认每小时正常退出一次,autorestart=true 会重新拉起它。

若需提高连接数,minfds=10000 属于 Supervisor 主配置的 [supervisord] 段,不属于上述 [program:...] 段;按服务器进程管理方式调整。Supervisor 官方配置说明

8. 保留调度和原有后台任务

若现有 cron 已经每分钟执行 schedule:run,保留它并确认使用 PHP 8.3+,不要重复新增。尚未配置时,以运行用户的 crontab 为例:

* * * * * cd /www/wwwroot/bot-28 && /usr/bin/php artisan schedule:run >> /dev/null 2>&1

调度中已经包含每小时的 admin-realtime:prune,不需要再单独新增清理任务。项目原有队列 worker 如在运行,需按原守护配置重启,以加载新代码和 PHP;此次推送自身不依赖它们。PHP-FPM 也应在切换 PHP 或发布时按原有运维方式重载。

9. 首次上线验收

先检查本机 Reverb 和进程:

curl -fsS http://127.0.0.1:8080/up
supervisorctl status bot28-reverb bot28-admin-relay
tail -n 50 storage/logs/realtime/relay.log
tail -n 50 storage/logs/realtime/reverb.log

/up 返回 200、两个进程 RUNNING 只说明进程和本机服务就绪。还需用后台页面完成以下联调:

可打开项目提供的 https://API域名/realtime-test.html 检查连接、授权、实时消息和补发;使用步骤见前端接入文档的快速验证页面。测试页所在域名同样需要被 REVERB_ALLOWED_ORIGINS 允许。

  1. 已登录管理员请求 GET /admin/realtime/config 成功,公网 host/443/https 正确。
  2. 浏览器连接 wss://域名/app/应用KEY?... 获得 101,POST /admin/broadcasting/auth 授权 200。
  3. 操作一条后台订单,列表在不手动刷新的情况下更新;断网重连后能补发或重新拉取列表。
  4. 无权限频道返回 403,登录过期后断开;新登录可以重新接入。

业务侧错误记录在 storage/logs/realtime/admin-日期.log,Supervisor 标准输出在上面两个 .log 文件。广播失败会保留批次重试;连续报错时应检查 Reverb 地址、凭据、数据和请求体大小。

前端须同步发布与服务端匹配的版本,前端说明见后台实时推送-前端接入。

10. 上线后暂停推送(按需)

先停止 relay,再把 .env 改为 ADMIN_WS_ENABLED=false,执行 php artisan config:cache;不再使用时也停止 Reverb,并禁用对应 Supervisor 配置。HTTP 列表继续可用。不要通过 cache:clear、Cache::flush() 或清空状态表来停用推送。

本节是统一的部署操作说明,未执行生产部署。Reverb 生产运行方式参见Laravel 官方文档,本项目验证范围见第 12 节。

11. 后端事件写入

平台在线人数与待处理角标由 relay 每 ADMIN_WS_SUMMARY_SCAN_SECONDS(默认 5)秒复用原 HTTP 控制器的统计查询,仅在结果变化时登记 online_platform_count / payment_unprocessed 事件,再经现有的持久发送、重试和补发流程投递。无需新增迁移或在 melbet 写入点加钩子;同一用户切换平台、订单跨过 24 小时窗口、分享领取待审核数量变化都能检测。统计扫描失败只记录日志,恢复后重试,不把失败当作已推送。

11.1 bot-28 已有写入覆盖

bot-28 内以下写入会产生事件:

  • 所有经 Eloquent 模型的新增、修改、删除(BaseModel 钩子,覆盖 sport_order、lhc_order、sport_game_order、jisu_game_order、third_game_orders、bets、recharges、payment_orders、withdraws、manual_audits):后台审核、退款、人工上下分、Telegram 机器人下注 / 充值 / 提现、各支付回调中以模型保存的部分等。只改 updated_at、is_send(通知标记)这类列时不推送。
  • 用户新增:User 模型创建(含 Telegram 注册 PublicService)。用户资料的其它修改不推送。
  • 不经模型的批量写入,已单独登记:
    • 六合彩开奖结算、开奖失败退分(BetService)
    • 足球游戏批量判定未中奖(SportGameOrderJob)
    • 三方游戏订单每分钟同步(只推新订单或状态、结算金额、更新时间、会员归属有变化的订单)及旧账号回填
    • USDT 充值链上确认、忽略无效充值、充值登记(RechargeService、RechargeBonusService)
    • 支付通道代付回调的状态更新(PaymentOrderService)
    • 后台批量设置打款信息、批量锁定 / 解锁提现与代付(PaymentOrder、Withdraw 控制器)
    • 会员合并时转移的投注与支付订单(SecretService)
  • 在线状态:不依赖写入点,由 relay 定期比对在线集合,bot-28 与 melbet_sport-api 的登录、活跃都能覆盖。

以后新增不经模型的批量写入时,在写入后调用:

\App\Services\AdminRealtime\AdminRealtime::record('payment_orders', $ids);            // 默认 updated
\App\Services\AdminRealtime\AdminRealtime::record('withdraws', $id, 'created');

它在事务内会等提交后再写,回滚则丢弃,失败只记日志。

11.2 melbet_sport-api 写入约定

会员前台 API(melbet_sport-api)也直接写这些表:体彩下单与结算(SportOrder)、六合彩 / 极速 / 足球游戏下注、投注、充值、代收代付、提现、会员注册。这些写入目前不会产生事件,在该仓库接入前,前台产生的新订单、新用户不会实时出现在后台列表(列表本身照常,刷新可见)。在线状态不受影响。

接入方式:在 melbet_sport-api 对应写入之后往同一张表插入一行即可,不需要新增依赖或配置,也不需要跟着换框架或接入 Reverb。seq 列由 relay 填写,写入方不要赋值。以 ThinkPHP 模型事件为例:

// app/common/service/AdminRealtimeEvent.php
namespace app\common\service;

use think\facade\Db;
use think\facade\Log;

class AdminRealtimeEvent
{
    /** $resource 为不带前缀的表名,如 sport_order、payment_orders、users */
    public static function push(string $resource, $id, string $action = 'updated'): void
    {
        try {
            Db::name('admin_realtime_events')->insert([
                'resource' => $resource, 'resource_id' => (string) $id, 'action' => $action,
                'source' => 'melbet_sport-api', 'created_at' => date('Y-m-d H:i:s'),
            ]);
        } catch (\Throwable $e) {
            Log::error('admin_realtime_event_failed: ' . $e->getMessage());
        }
    }
}

// app/common/model/SportOrder.php
public static function onAfterInsert($model) { AdminRealtimeEvent::push('sport_order', $model->id, 'created'); }
public static function onAfterUpdate($model) { AdminRealtimeEvent::push('sport_order', $model->id); }

注意:

  • resource 只能是本节上方列出的表名(users 只处理 created);action 用 created / updated / deleted。时间使用 Asia/Shanghai。
  • ThinkPHP 的模型事件只在模型 save() / create() 时触发,Db::name()->update() 这类批量写入需要在写入后手动调用 push()。
  • 在事务内插入也可以(回滚即不推送)。relay 会在 60 秒(ADMIN_WS_GAP_SECONDS)内补发暂未提交的事件;事务持续超过该时长的事件可能漏推,长事务建议提交后再插入。
  • 插入失败必须吞掉异常,不能影响下单、支付。

12. 后端验证范围

  • 2026-11-01 在隔离、关闭网络的本地 MySQL 9.6.0 上验证了新增迁移、旧 seq 高水位初始化、未确认批次重试、清空历史后序号继续递增,以及超期待发送批次不被清理。测试数据库已删除、临时服务已停止;这不代表生产 MySQL 或全部业务列表已完成联调。

  • 2026-11-01 修正后,本机真实 Reverb 验证:同一有效会话订阅多个频道保持连接;带有效签名但已过期的授权会被断开;同一 socket 混用不同会话会被断开,独立的新登录连接保持有效。后台实时测试 15 项通过;完整 Integration 为 661 项,仍有原来的 1 个错误、2 个失败、10 个跳过;Unit 为 90 项,仍有原来的 2 个错误。

  • tests/Integration/AdminRealtimeTest.php(隔离 SQLite):事务、模型和行格式、晚提交、补发与频道过滤、401/403、断开失败后重试且保留新登录、登记失败拒绝授权、到期后延迟使用旧频道授权、有效订阅保持连接、广播部分失败后新进程恢复并按序重试、清空历史后不重用序号、超期未发送批次保留。

  • 本机端到端(SQLite + reverb:start + admin-realtime:relay + Node 中的 laravel-echo@2.1.6/pusher-js@8.4.0):授权订阅成功、relay 广播实时送达且未订阅频道收不到;token 40 秒到期时 Reverb 断开连接、自动重连授权 401;Reverb 停止期间写入的事件广播失败记日志,Reverb 恢复后 Echo 自动重连、补发接口取回缺失事件并继续实时接收;relay 收到 SIGTERM 正常退出并释放锁。

  • 在线用户扫描与各订单列表的单行解析依赖 MySQL 语法(COLLATE 等),未在真实数据库、真实 Nginx / wss 环境下验证;上线后请按第 9 节的验收步骤实际下单核对。