1.重构实时消息WebSocket连接
2.MySQL备份
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
namespace app\admin\controller\game;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\library\admin\PushChannelConfigHelper;
|
||||
use app\common\service\GameLiveService;
|
||||
use app\common\service\GameRecordService;
|
||||
use support\Response;
|
||||
@@ -39,21 +38,6 @@ class Live extends Backend
|
||||
return $this->success('', GameLiveService::buildSnapshot($recordId));
|
||||
}
|
||||
|
||||
public function pushConfig(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'url' => PushChannelConfigHelper::wsBaseUrl(),
|
||||
'app_key' => (string) config('plugin.webman.push.app.app_key'),
|
||||
'channel' => 'game-live',
|
||||
'event' => 'bet-updated',
|
||||
]);
|
||||
}
|
||||
|
||||
public function calculate(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
|
||||
49
app/admin/controller/test/GameCurrentStatus.php
Normal file
49
app/admin/controller/test/GameCurrentStatus.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\test;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\library\admin\WebSocketConfigHelper;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* WebSocket 测试:状态流(period.tick / period.opened)
|
||||
*/
|
||||
class GameCurrentStatus extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
public function wsConfig(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$subscribeTopics = [
|
||||
'period.tick',
|
||||
'period.opened',
|
||||
'period.locked',
|
||||
'period.payout',
|
||||
'bet.accepted',
|
||||
'wallet.changed',
|
||||
'auto.spin.progress',
|
||||
];
|
||||
|
||||
return $this->success('', [
|
||||
'name' => 'ws.period',
|
||||
'ws_url' => WebSocketConfigHelper::wsUrl(),
|
||||
'connect_tip' => '连接成功后会自动订阅下列主题;也可在「发送消息」中手动改订阅。未订阅时不会收到业务推送。',
|
||||
'subscribe_topics' => $subscribeTopics,
|
||||
'sample_messages' => [
|
||||
'{"action":"ping"}',
|
||||
'{"action":"subscribe","topics":["period.tick","period.opened"]}',
|
||||
'{"action":"subscribe","topics":["bet.accepted","wallet.changed","auto.spin.progress"]}',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\test;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\library\admin\PushChannelConfigHelper;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 推送测试:public-game-period(对局公共频道)
|
||||
*/
|
||||
class PushGamePeriod extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
public function pushConfig(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'url' => PushChannelConfigHelper::wsBaseUrl(),
|
||||
'app_key' => PushChannelConfigHelper::appKey(),
|
||||
'channel' => 'public-game-period',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\test;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\library\admin\PushChannelConfigHelper;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 推送测试:public-operation-notice(公告广播频道)
|
||||
*/
|
||||
class PushOperationNotice extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
public function pushConfig(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'url' => PushChannelConfigHelper::wsBaseUrl(),
|
||||
'app_key' => PushChannelConfigHelper::appKey(),
|
||||
'channel' => 'public-operation-notice',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\test;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\library\admin\PushChannelConfigHelper;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 推送测试:private-user-{uuid}(用户私有频道)
|
||||
*/
|
||||
class PushPrivateUser extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
public function pushConfig(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($request->get('uuid') ?? $request->post('uuid') ?? ''));
|
||||
if ($uuid === '') {
|
||||
return $this->error(__('Parameter %s can not be empty', ['uuid']));
|
||||
}
|
||||
if (strlen($uuid) > 64 || !preg_match('/^[0-9a-zA-Z_-]+$/', $uuid)) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'url' => PushChannelConfigHelper::wsBaseUrl(),
|
||||
'app_key' => PushChannelConfigHelper::appKey(),
|
||||
'channel' => 'private-user-' . $uuid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ use app\common\model\DepositOrder;
|
||||
use app\common\model\GameConfig;
|
||||
use app\common\model\WithdrawOrder;
|
||||
use app\common\service\DepositOrderExpireService;
|
||||
use app\common\service\UserPushService;
|
||||
use support\Response;
|
||||
use support\think\Db;
|
||||
use Throwable;
|
||||
@@ -332,7 +331,7 @@ class Finance extends MobileBase
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟第三方异步通知:验签后调用 DepositSettlement::settle 入账,并推送 wallet.changed。
|
||||
* 模拟第三方异步通知:验签后调用 DepositSettlement::settle 入账。
|
||||
*/
|
||||
public function depositMockNotify(Request $request): Response
|
||||
{
|
||||
@@ -373,19 +372,6 @@ class Finance extends MobileBase
|
||||
null,
|
||||
'channel_code=' . $pc
|
||||
);
|
||||
$uid = intval(strval($order->user_id));
|
||||
if ($uid > 0) {
|
||||
$coinAfter = is_string($result['balance_after'] ?? null) ? $result['balance_after'] : strval($result['balance_after'] ?? '0');
|
||||
$credit = is_string($result['credit'] ?? null) ? $result['credit'] : strval($result['credit'] ?? '0');
|
||||
UserPushService::publish($uid, UserPushService::EVT_WALLET_CHANGED, [
|
||||
'reason' => 'deposit',
|
||||
'ref_type' => 'deposit_order',
|
||||
'ref_id' => (string) $orderId,
|
||||
'order_no' => $orderNo,
|
||||
'delta' => $credit,
|
||||
'balance_after' => $coinAfter,
|
||||
]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
return $this->mobileError(2000, $e->getMessage());
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use app\common\model\UserWalletRecord;
|
||||
use app\common\service\GameHotDataCoordinator;
|
||||
use app\common\service\GameHotDataRedis;
|
||||
use app\common\service\GameRecordService;
|
||||
use app\common\service\UserPushService;
|
||||
use app\common\service\GameWebSocketEventBus;
|
||||
use support\think\Db;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
@@ -139,12 +139,27 @@ class Game extends MobileBase
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交下注:入参极简——period_no + numbers + bet_amount(整笔总金额) + idempotency_key。
|
||||
*
|
||||
* 下注判定:开奖号码 ∈ pick_numbers 即算中奖,赔付按整笔 total_amount × odds 计算
|
||||
* (派彩 = 压注总额 × 连胜奖励表 odds_factor;streak_at_bet 为下注时快照)。
|
||||
* 与前端文档对齐:/api/game/current_status
|
||||
*/
|
||||
public function currentStatus(Request $request): Response
|
||||
{
|
||||
return $this->periodCurrent($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧路由:/api/game/betPlace
|
||||
* 新语义与 place_bet 一致:bet_amount 作为“单注金额”。
|
||||
*/
|
||||
public function betPlace(Request $request): Response
|
||||
{
|
||||
return $this->placeBet($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交下注:入参为 period_no + numbers + single_bet_amount + idempotency_key。
|
||||
* 兼容前端传参 bet_amount(作为 single_bet_amount 同义字段)。
|
||||
*/
|
||||
public function placeBet(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeMobile($request);
|
||||
if ($response !== null) {
|
||||
@@ -152,16 +167,14 @@ class Game extends MobileBase
|
||||
}
|
||||
$periodNo = trim((string) $request->post('period_no', ''));
|
||||
$numbersRaw = $request->post('numbers', '');
|
||||
$betAmount = trim((string) $request->post('bet_amount', ''));
|
||||
$singleBetAmount = trim((string) ($request->post('single_bet_amount', $request->post('bet_amount', ''))));
|
||||
$idempotencyKey = trim((string) $request->post('idempotency_key', ''));
|
||||
if ($periodNo === '' || $betAmount === '' || $idempotencyKey === '') {
|
||||
if ($periodNo === '' || $singleBetAmount === '' || $idempotencyKey === '') {
|
||||
return $this->mobileError(1001, 'Missing parameters');
|
||||
}
|
||||
if (!is_numeric($betAmount) || bccomp($betAmount, '0', 2) <= 0) {
|
||||
if (!is_numeric($singleBetAmount) || bccomp($singleBetAmount, '0', 2) <= 0) {
|
||||
return $this->mobileError(1003, 'Invalid parameter value');
|
||||
}
|
||||
$totalAmount = bcadd($betAmount, '0', 2);
|
||||
|
||||
$numbers = $this->parseBetNumbersFromRequest($numbersRaw);
|
||||
if ($numbers === []) {
|
||||
return $this->mobileError(1003, 'Invalid parameter value');
|
||||
@@ -170,6 +183,9 @@ class Game extends MobileBase
|
||||
if (count($numbers) > $maxSelect) {
|
||||
return $this->mobileError(1003, 'Invalid parameter value');
|
||||
}
|
||||
$singleAmount = bcadd($singleBetAmount, '0', 2);
|
||||
$numberCount = (string) count($numbers);
|
||||
$totalAmount = bcmul($singleAmount, $numberCount, 2);
|
||||
|
||||
if (!GameRecordService::isLiveRuntimeEnabled()) {
|
||||
return $this->mobileError(3001, 'Game is paused');
|
||||
@@ -273,19 +289,28 @@ class Game extends MobileBase
|
||||
}
|
||||
|
||||
GameHotDataCoordinator::afterUserCommitted($userId);
|
||||
UserPushService::publish($userId, UserPushService::EVT_BET_ACCEPTED, [
|
||||
'order_no' => $orderNo,
|
||||
'period_no' => (string) $period->period_no,
|
||||
'status' => 'accepted',
|
||||
'balance_after' => $after,
|
||||
'total_amount' => $totalAmount,
|
||||
'current_streak' => $streakAtBet,
|
||||
GameWebSocketEventBus::publish('bet.accepted', [
|
||||
'user_id' => $userId,
|
||||
'period_no' => $period->period_no,
|
||||
'numbers' => $numbers,
|
||||
'single_bet_amount' => $singleAmount,
|
||||
'numbers_count' => count($numbers),
|
||||
'total_amount' => $totalAmount,
|
||||
'balance_after' => $after,
|
||||
'accepted_at' => time(),
|
||||
]);
|
||||
GameWebSocketEventBus::publish('wallet.changed', [
|
||||
'user_id' => $userId,
|
||||
'balance_after' => $after,
|
||||
'biz_type' => 'bet',
|
||||
'changed_at' => time(),
|
||||
]);
|
||||
|
||||
return $this->mobileSuccess([
|
||||
'order_no' => $orderNo,
|
||||
'period_no' => $period->period_no,
|
||||
'status' => 'accepted',
|
||||
'single_bet_amount' => $singleAmount,
|
||||
'numbers_count' => count($numbers),
|
||||
'locked_balance' => '0.00',
|
||||
'balance_after' => $after,
|
||||
'current_streak' => $streakAtBet,
|
||||
@@ -295,6 +320,61 @@ class Game extends MobileBase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动托管(无推送模式):先落托管配置,实际执行仍由客户端轮询 current_status 驱动。
|
||||
*/
|
||||
public function autoSpin(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeMobile($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
$action = trim((string) $request->post('action', 'start'));
|
||||
if ($action === 'stop') {
|
||||
return $this->mobileSuccess([
|
||||
'status' => 'stopped',
|
||||
'auto_mode' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
$periodNo = trim((string) $request->post('period_no', ''));
|
||||
$numbersRaw = $request->post('numbers', '');
|
||||
$singleBetAmount = trim((string) ($request->post('single_bet_amount', $request->post('bet_amount', ''))));
|
||||
$rounds = $this->intValue($request->post('rounds', 1));
|
||||
if ($periodNo === '' || $singleBetAmount === '' || $rounds < 1) {
|
||||
return $this->mobileError(1001, 'Missing parameters');
|
||||
}
|
||||
if (!is_numeric($singleBetAmount) || bccomp($singleBetAmount, '0', 2) <= 0) {
|
||||
return $this->mobileError(1003, 'Invalid parameter value');
|
||||
}
|
||||
$numbers = $this->parseBetNumbersFromRequest($numbersRaw);
|
||||
if ($numbers === []) {
|
||||
return $this->mobileError(1003, 'Invalid parameter value');
|
||||
}
|
||||
$userIdValue = filter_var($this->auth->id ?? null, FILTER_VALIDATE_INT);
|
||||
if ($userIdValue === false) {
|
||||
$userIdValue = 0;
|
||||
}
|
||||
GameWebSocketEventBus::publish('auto.spin.progress', [
|
||||
'user_id' => $userIdValue,
|
||||
'period_no' => $periodNo,
|
||||
'rounds' => $rounds,
|
||||
'remaining_rounds' => $rounds,
|
||||
'completed_rounds' => 0,
|
||||
'status' => 'scheduled',
|
||||
'server_time' => time(),
|
||||
]);
|
||||
return $this->mobileSuccess([
|
||||
'status' => 'scheduled',
|
||||
'auto_mode' => true,
|
||||
'period_no' => $periodNo,
|
||||
'numbers' => $numbers,
|
||||
'single_bet_amount' => bcadd($singleBetAmount, '0', 2),
|
||||
'rounds' => $rounds,
|
||||
'remaining_rounds' => $rounds,
|
||||
]);
|
||||
}
|
||||
|
||||
public function betMyOrders(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeMobile($request);
|
||||
|
||||
@@ -81,10 +81,10 @@ return [
|
||||
'连胜奖励' => 'Win streak rewards',
|
||||
'连胜降低档位' => 'Streak reduction tiers',
|
||||
'钱包加减点' => 'Wallet adjust',
|
||||
'测试频道监听' => 'Test channel monitoring',
|
||||
'推送-对局公共频道' => 'Push: public game period',
|
||||
'推送-公告广播频道' => 'Push: operation notices',
|
||||
'推送-用户私有频道' => 'Push: user private',
|
||||
'测试频道监听' => 'WebSocket channel test',
|
||||
'接口-当前状态' => 'API: current status',
|
||||
'接口-下注' => 'API: place bet',
|
||||
'接口-自动托管' => 'API: auto spin',
|
||||
'渠道管理' => 'Channel management',
|
||||
'管理员提现记录' => 'Admin withdraw records',
|
||||
'管理员钱包' => 'Admin wallets',
|
||||
@@ -100,7 +100,7 @@ return [
|
||||
// 部分按钮 title 为动作名(game/live 等迁移写入)
|
||||
'index' => 'View',
|
||||
'snapshot' => 'Snapshot',
|
||||
'pushConfig' => 'Push config',
|
||||
'wsConfig' => 'WebSocket config',
|
||||
'recordSettings' => 'Period settings',
|
||||
'createNextManual' => 'Create next period manually',
|
||||
'periodSettings' => 'Period settings',
|
||||
|
||||
@@ -47,7 +47,7 @@ return [
|
||||
'approve' => '审核通过',
|
||||
'reject' => '审核驳回',
|
||||
'snapshot' => '快照',
|
||||
'pushConfig' => '推送配置',
|
||||
'wsConfig' => '连接配置',
|
||||
'recordSettings' => '期次设置',
|
||||
'createNextManual' => '手动创建下一期',
|
||||
'periodSettings' => '期号设置',
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\admin;
|
||||
|
||||
/**
|
||||
* 后台推送测试页:读取 webman/push 配置(与 game/Live::pushConfig 口径一致)
|
||||
*/
|
||||
final class PushChannelConfigHelper
|
||||
{
|
||||
public static function wsBaseUrl(): string
|
||||
{
|
||||
$client = trim((string) config('plugin.webman.push.app.websocket_client'));
|
||||
if ($client !== '') {
|
||||
return self::normalizeClientWsBase($client);
|
||||
}
|
||||
|
||||
$ws = (string) config('plugin.webman.push.app.websocket');
|
||||
$ws = str_replace('websocket://', 'ws://', $ws);
|
||||
|
||||
return str_replace('0.0.0.0', '127.0.0.1', $ws);
|
||||
}
|
||||
|
||||
private static function normalizeClientWsBase(string $url): string
|
||||
{
|
||||
$url = rtrim(trim($url), '/');
|
||||
if (str_starts_with($url, 'websocket://')) {
|
||||
return str_replace('websocket://', 'ws://', $url);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public static function appKey(): string
|
||||
{
|
||||
return (string) config('plugin.webman.push.app.app_key');
|
||||
}
|
||||
}
|
||||
18
app/common/library/admin/WebSocketConfigHelper.php
Normal file
18
app/common/library/admin/WebSocketConfigHelper.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\admin;
|
||||
|
||||
final class WebSocketConfigHelper
|
||||
{
|
||||
public static function wsUrl(): string
|
||||
{
|
||||
$url = trim((string) env('H5_WEBSOCKET_URL', ''));
|
||||
if ($url !== '') {
|
||||
return $url;
|
||||
}
|
||||
return 'ws://127.0.0.1:3131';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\finance;
|
||||
|
||||
use app\common\service\GameWebSocketEventBus;
|
||||
use RuntimeException;
|
||||
use support\think\Db;
|
||||
use Throwable;
|
||||
@@ -183,6 +184,14 @@ final class DepositSettlement
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
GameWebSocketEventBus::publish('wallet.changed', [
|
||||
'user_id' => $userId,
|
||||
'balance_after' => $balanceAfter,
|
||||
'biz_type' => 'deposit',
|
||||
'order_no' => $orderNo,
|
||||
'changed_at' => $now,
|
||||
]);
|
||||
|
||||
return [
|
||||
'order_id' => $orderId,
|
||||
'order_no' => $orderNo,
|
||||
|
||||
@@ -134,33 +134,6 @@ final class GameBetSettleService
|
||||
GameHotDataCoordinator::afterUserCommitted($userId);
|
||||
}
|
||||
|
||||
foreach ($aggregateByUser as $userId => $agg) {
|
||||
$hitOrderCount = 0;
|
||||
foreach ($agg['orders'] as $o) {
|
||||
if (($o['hit'] ?? false) === true) {
|
||||
$hitOrderCount++;
|
||||
}
|
||||
}
|
||||
UserPushService::publish((int) $userId, UserPushService::EVT_BET_SETTLED, [
|
||||
'period_no' => $agg['period_no'],
|
||||
'result_number' => $resultNumber,
|
||||
'total_win_amount' => $agg['total_win'],
|
||||
'order_count' => count($agg['orders']),
|
||||
'hit_order_count' => $hitOrderCount,
|
||||
'balance_after' => $agg['balance_after'],
|
||||
]);
|
||||
|
||||
if (bccomp($agg['total_win'], '0', 2) > 0) {
|
||||
UserPushService::publish((int) $userId, UserPushService::EVT_WALLET_CHANGED, [
|
||||
'reason' => 'payout',
|
||||
'ref_type' => 'game_period',
|
||||
'ref_id' => (string) $recordId,
|
||||
'delta' => $agg['total_win'],
|
||||
'balance_after' => $agg['balance_after'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$jackpotHits = [];
|
||||
foreach ($jackpotNotify as $uid => $_) {
|
||||
if (!isset($aggregateByUser[$uid])) {
|
||||
@@ -210,9 +183,8 @@ final class GameBetSettleService
|
||||
}
|
||||
Db::startTrans();
|
||||
try {
|
||||
$out = self::settleBetsForDraw($rid, $rn);
|
||||
self::settleBetsForDraw($rid, $rn);
|
||||
Db::commit();
|
||||
JackpotPushService::publishHits($out['jackpot_hits'] ?? []);
|
||||
$count++;
|
||||
} catch (Throwable $e) {
|
||||
Db::rollback();
|
||||
@@ -322,6 +294,13 @@ final class GameBetSettleService
|
||||
'update_time' => $now,
|
||||
]);
|
||||
GameHotDataCoordinator::afterUserCommitted($userId);
|
||||
GameWebSocketEventBus::publish('wallet.changed', [
|
||||
'user_id' => $userId,
|
||||
'balance_after' => $after,
|
||||
'biz_type' => 'payout',
|
||||
'ref_id' => $betId,
|
||||
'changed_at' => $now,
|
||||
]);
|
||||
|
||||
return $after;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use app\common\service\GameHotDataCoordinator;
|
||||
use app\common\service\GameHotDataLock;
|
||||
use support\think\Db;
|
||||
use Throwable;
|
||||
use Webman\Push\Api;
|
||||
|
||||
final class GameLiveService
|
||||
{
|
||||
@@ -384,8 +383,6 @@ final class GameLiveService
|
||||
GameRecordStatService::refreshForRecordId($rid);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
JackpotPushService::publishHits($settleOut['jackpot_hits'] ?? []);
|
||||
|
||||
self::publishPublicPeriodOpened((string) $record['period_no'], $finalNumber, $now);
|
||||
self::publishPublicPeriodPayout((string) $record['period_no'], $finalNumber, $payoutUntil);
|
||||
self::publishSnapshot(null);
|
||||
@@ -611,72 +608,60 @@ final class GameLiveService
|
||||
|
||||
public static function publishSnapshot(?int $recordId = null): void
|
||||
{
|
||||
try {
|
||||
$payload = self::buildSnapshot($recordId);
|
||||
$api = self::createPushApi();
|
||||
$api->trigger(self::CHANNEL, self::EVENT, $payload);
|
||||
self::publishPublicPeriodTick($payload, $api);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
private static function createPushApi(): Api
|
||||
{
|
||||
return new Api(
|
||||
str_replace('0.0.0.0', '127.0.0.1', (string) config('plugin.webman.push.app.api')),
|
||||
(string) config('plugin.webman.push.app.app_key'),
|
||||
(string) config('plugin.webman.push.app.app_secret')
|
||||
);
|
||||
$snapshot = self::buildSnapshot($recordId);
|
||||
self::publishPublicPeriodTick($snapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动端公共频道:每秒心跳,含期号、倒计时、阶段(对齐 lobbyInit/periodCurrent 语义)
|
||||
*/
|
||||
private static function publishPublicPeriodTick(array $snapshot, Api $api): void
|
||||
private static function publishPublicPeriodTick(array $snapshot): void
|
||||
{
|
||||
$record = $snapshot['record'] ?? null;
|
||||
$serverTime = (int) ($snapshot['server_time'] ?? time());
|
||||
$remaining = (int) ($snapshot['remaining_seconds'] ?? 0);
|
||||
$betCloseIn = (int) ($snapshot['bet_remaining_seconds'] ?? 0);
|
||||
$payoutRem = (int) ($snapshot['payout_remaining_seconds'] ?? 0);
|
||||
$isPayout = !empty($snapshot['is_payout_phase']);
|
||||
$periodNo = '';
|
||||
$dbStatus = 0;
|
||||
$periodId = 0;
|
||||
$status = 'finished';
|
||||
$resultNumber = null;
|
||||
if (is_array($record)) {
|
||||
$periodNo = (string) ($record['period_no'] ?? '');
|
||||
$dbStatus = (int) ($record['status'] ?? 0);
|
||||
$rn = $record['result_number'] ?? null;
|
||||
$resultNumber = is_numeric((string) $rn) ? (int) $rn : null;
|
||||
}
|
||||
if ($record === null || $periodNo === '') {
|
||||
$status = 'idle';
|
||||
} else {
|
||||
$status = self::mapPublicPeriodStatus($dbStatus, $betCloseIn);
|
||||
}
|
||||
$payload = [
|
||||
'server_time' => $serverTime,
|
||||
'period_no' => $periodNo,
|
||||
'status' => $status,
|
||||
'countdown' => $remaining,
|
||||
'bet_close_in'=> $betCloseIn,
|
||||
'payout_remaining_seconds' => $payoutRem,
|
||||
'is_payout_phase' => $isPayout,
|
||||
'payout_message' => $isPayout ? '派彩中,请稍候' : '',
|
||||
];
|
||||
if ($periodNo !== '' && $record !== null) {
|
||||
$start = (int) ($record['period_start_at'] ?? 0);
|
||||
$betSeconds = (int) ($snapshot['bet_seconds'] ?? 20);
|
||||
$periodSeconds = (int) ($snapshot['period_seconds'] ?? 30);
|
||||
if ($start > 0) {
|
||||
$payload['lock_at'] = $start + $betSeconds;
|
||||
$payload['open_at'] = $start + $periodSeconds;
|
||||
$periodNoRaw = $record['period_no'] ?? '';
|
||||
if (is_string($periodNoRaw)) {
|
||||
$periodNo = $periodNoRaw;
|
||||
}
|
||||
$periodIdRaw = $record['id'] ?? 0;
|
||||
$periodIdParsed = filter_var($periodIdRaw, FILTER_VALIDATE_INT);
|
||||
if ($periodIdParsed !== false && $periodIdParsed > 0) {
|
||||
$periodId = $periodIdParsed;
|
||||
}
|
||||
$dbStatusRaw = $record['status'] ?? 4;
|
||||
$dbStatus = filter_var($dbStatusRaw, FILTER_VALIDATE_INT);
|
||||
if ($dbStatus === false) {
|
||||
$dbStatus = 4;
|
||||
}
|
||||
$betRemainingRaw = $snapshot['bet_remaining_seconds'] ?? 0;
|
||||
$betRemaining = filter_var($betRemainingRaw, FILTER_VALIDATE_INT);
|
||||
if ($betRemaining === false || $betRemaining < 0) {
|
||||
$betRemaining = 0;
|
||||
}
|
||||
$status = self::mapPublicPeriodStatus($dbStatus, $betRemaining);
|
||||
$resultRaw = $record['result_number'] ?? null;
|
||||
$resultParsed = filter_var($resultRaw, FILTER_VALIDATE_INT);
|
||||
if ($resultParsed !== false && $resultParsed > 0) {
|
||||
$resultNumber = $resultParsed;
|
||||
}
|
||||
}
|
||||
if ($resultNumber !== null) {
|
||||
$payload['result_number'] = $resultNumber;
|
||||
}
|
||||
$api->trigger(self::CHANNEL_PUBLIC_GAME_PERIOD, self::EVT_PERIOD_TICK, $payload);
|
||||
|
||||
$payload = [
|
||||
'period_id' => $periodId,
|
||||
'period_no' => $periodNo,
|
||||
'status' => $status,
|
||||
'countdown' => max(0, self::safeInt($snapshot['remaining_seconds'] ?? 0)),
|
||||
'bet_close_in' => max(0, self::safeInt($snapshot['bet_remaining_seconds'] ?? 0)),
|
||||
'result_number' => $resultNumber,
|
||||
'runtime_enabled' => !empty($snapshot['runtime_enabled']),
|
||||
'server_time' => time(),
|
||||
];
|
||||
|
||||
GameWebSocketEventBus::publish(self::EVT_PERIOD_TICK, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -684,32 +669,26 @@ final class GameLiveService
|
||||
*/
|
||||
private static function publishPublicPeriodLocked(array $record): void
|
||||
{
|
||||
try {
|
||||
$start = (int) ($record['period_start_at'] ?? 0);
|
||||
$betSeconds = self::getConfigInt(self::KEY_BET_SECONDS, 20);
|
||||
$periodNo = (string) ($record['period_no'] ?? '');
|
||||
$payload = [
|
||||
'period_no' => $periodNo,
|
||||
'lock_at' => $start > 0 ? $start + $betSeconds : time(),
|
||||
];
|
||||
$api = self::createPushApi();
|
||||
$api->trigger(self::CHANNEL_PUBLIC_GAME_PERIOD, self::EVT_PERIOD_LOCKED, $payload);
|
||||
} catch (Throwable) {
|
||||
$periodNo = is_string($record['period_no'] ?? null) ? $record['period_no'] : '';
|
||||
$periodId = filter_var($record['id'] ?? 0, FILTER_VALIDATE_INT);
|
||||
if ($periodId === false) {
|
||||
$periodId = 0;
|
||||
}
|
||||
GameWebSocketEventBus::publish(self::EVT_PERIOD_LOCKED, [
|
||||
'period_id' => $periodId,
|
||||
'period_no' => $periodNo,
|
||||
'status' => 'locked',
|
||||
'server_time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
private static function publishPublicPeriodOpened(string $periodNo, int $resultNumber, int $openTime): void
|
||||
{
|
||||
try {
|
||||
$payload = [
|
||||
'period_no' => $periodNo,
|
||||
'result_number' => $resultNumber,
|
||||
'open_time' => $openTime,
|
||||
];
|
||||
$api = self::createPushApi();
|
||||
$api->trigger(self::CHANNEL_PUBLIC_GAME_PERIOD, self::EVT_PERIOD_OPENED, $payload);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
GameWebSocketEventBus::publish(self::EVT_PERIOD_OPENED, [
|
||||
'period_no' => $periodNo,
|
||||
'result_number' => $resultNumber,
|
||||
'open_time' => $openTime,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -717,17 +696,12 @@ final class GameLiveService
|
||||
*/
|
||||
private static function publishPublicPeriodPayout(string $periodNo, int $resultNumber, int $payoutUntil): void
|
||||
{
|
||||
try {
|
||||
$payload = [
|
||||
'period_no' => $periodNo,
|
||||
'result_number' => $resultNumber,
|
||||
'payout_until' => $payoutUntil,
|
||||
'message' => '派彩中,请稍候',
|
||||
];
|
||||
$api = self::createPushApi();
|
||||
$api->trigger(self::CHANNEL_PUBLIC_GAME_PERIOD, self::EVT_PERIOD_PAYOUT, $payload);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
GameWebSocketEventBus::publish(self::EVT_PERIOD_PAYOUT, [
|
||||
'period_no' => $periodNo,
|
||||
'result_number' => $resultNumber,
|
||||
'payout_until' => $payoutUntil,
|
||||
'server_time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -911,4 +885,13 @@ final class GameLiveService
|
||||
$index = random_int(0, count($numbers) - 1);
|
||||
return $numbers[$index];
|
||||
}
|
||||
|
||||
private static function safeInt($value): int
|
||||
{
|
||||
$parsed = filter_var($value, FILTER_VALIDATE_INT);
|
||||
if ($parsed === false) {
|
||||
return 0;
|
||||
}
|
||||
return $parsed;
|
||||
}
|
||||
}
|
||||
|
||||
93
app/common/service/GameWebSocketEventBus.php
Normal file
93
app/common/service/GameWebSocketEventBus.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use support\Redis;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 通过 Redis 列表在不同进程间投递 WebSocket 事件。
|
||||
*/
|
||||
final class GameWebSocketEventBus
|
||||
{
|
||||
private const KEY_QUEUE = 'dfw:v1:ws:event:queue';
|
||||
private const MAX_BATCH = 100;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function publish(string $topic, array $data): void
|
||||
{
|
||||
$topic = trim($topic);
|
||||
if ($topic === '') {
|
||||
return;
|
||||
}
|
||||
$payload = [
|
||||
'topic' => $topic,
|
||||
'event' => $topic,
|
||||
'data' => $data,
|
||||
'server_time' => time(),
|
||||
];
|
||||
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($json) || $json === '') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Redis::lPush(self::KEY_QUEUE, $json);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{topic:string,event:string,data:array<string,mixed>,server_time:int}>
|
||||
*/
|
||||
public static function popBatch(int $limit = self::MAX_BATCH): array
|
||||
{
|
||||
if ($limit <= 0) {
|
||||
return [];
|
||||
}
|
||||
if ($limit > self::MAX_BATCH) {
|
||||
$limit = self::MAX_BATCH;
|
||||
}
|
||||
|
||||
$out = [];
|
||||
try {
|
||||
for ($i = 0; $i < $limit; $i++) {
|
||||
$raw = Redis::rPop(self::KEY_QUEUE);
|
||||
if (!is_string($raw) || $raw === '') {
|
||||
break;
|
||||
}
|
||||
$decoded = json_decode($raw, true);
|
||||
if (!is_array($decoded)) {
|
||||
continue;
|
||||
}
|
||||
$topicRaw = $decoded['topic'] ?? '';
|
||||
$eventRaw = $decoded['event'] ?? '';
|
||||
$dataRaw = $decoded['data'] ?? [];
|
||||
$serverTimeRaw = $decoded['server_time'] ?? time();
|
||||
if (!is_string($topicRaw) || trim($topicRaw) === '') {
|
||||
continue;
|
||||
}
|
||||
$topic = trim($topicRaw);
|
||||
$event = is_string($eventRaw) && trim($eventRaw) !== '' ? trim($eventRaw) : $topic;
|
||||
$data = is_array($dataRaw) ? $dataRaw : [];
|
||||
$serverTime = filter_var($serverTimeRaw, FILTER_VALIDATE_INT);
|
||||
if ($serverTime === false || $serverTime <= 0) {
|
||||
$serverTime = time();
|
||||
}
|
||||
$out[] = [
|
||||
'topic' => $topic,
|
||||
'event' => $event,
|
||||
'data' => $data,
|
||||
'server_time' => $serverTime,
|
||||
];
|
||||
}
|
||||
} catch (Throwable) {
|
||||
return $out;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use Throwable;
|
||||
use Webman\Push\Api;
|
||||
|
||||
/**
|
||||
* 大奖派彩:玩家私有频道 + 公共频道(对局频道 + 公告频道,便于大厅与公告测试页均能收到)
|
||||
*/
|
||||
final class JackpotPushService
|
||||
{
|
||||
private const CHANNEL_GAME_PERIOD = 'public-game-period';
|
||||
|
||||
private const CHANNEL_OPERATION_NOTICE = 'public-operation-notice';
|
||||
|
||||
private const EVT_JACKPOT_HIT = 'jackpot.hit';
|
||||
|
||||
/**
|
||||
* @param list<array{user_id: int, period_no: string, total_win: string, result_number: int}> $hits
|
||||
*/
|
||||
public static function publishHits(array $hits): void
|
||||
{
|
||||
foreach ($hits as $h) {
|
||||
$uid = (int) ($h['user_id'] ?? 0);
|
||||
if ($uid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$periodNo = (string) ($h['period_no'] ?? '');
|
||||
$totalWin = (string) ($h['total_win'] ?? '0');
|
||||
$rn = (int) ($h['result_number'] ?? 0);
|
||||
UserPushService::publish($uid, UserPushService::EVT_JACKPOT_HIT, [
|
||||
'period_no' => $periodNo,
|
||||
'total_win_amount' => $totalWin,
|
||||
'result_number' => $rn,
|
||||
'is_jackpot' => true,
|
||||
]);
|
||||
self::publishPublicChannels($periodNo, $uid, $totalWin, $rn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private static function triggerChannel(Api $api, string $channel, array $payload): void
|
||||
{
|
||||
$api->trigger($channel, self::EVT_JACKPOT_HIT, $payload);
|
||||
}
|
||||
|
||||
private static function publishPublicChannels(string $periodNo, int $userId, string $totalWin, int $resultNumber): void
|
||||
{
|
||||
try {
|
||||
$api = new Api(
|
||||
str_replace('0.0.0.0', '127.0.0.1', (string) config('plugin.webman.push.app.api')),
|
||||
(string) config('plugin.webman.push.app.app_key'),
|
||||
(string) config('plugin.webman.push.app.app_secret')
|
||||
);
|
||||
$payload = [
|
||||
'period_no' => $periodNo,
|
||||
'user_id' => $userId,
|
||||
'total_win_amount' => $totalWin,
|
||||
'result_number' => $resultNumber,
|
||||
'message' => '恭喜玩家命中大奖派彩',
|
||||
];
|
||||
self::triggerChannel($api, self::CHANNEL_GAME_PERIOD, $payload);
|
||||
self::triggerChannel($api, self::CHANNEL_OPERATION_NOTICE, $payload);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use support\think\Db;
|
||||
use Throwable;
|
||||
use Webman\Push\Api;
|
||||
|
||||
/**
|
||||
* 用户私有频道推送:private-user-{uuid}(与移动端接口设计草案 7.1 一致)
|
||||
*/
|
||||
final class UserPushService
|
||||
{
|
||||
public const EVT_BET_ACCEPTED = 'bet.accepted';
|
||||
|
||||
/** 单注开奖结果(含未中奖 win_amount=0) */
|
||||
public const EVT_BET_SETTLED = 'bet.settled';
|
||||
|
||||
public const EVT_WALLET_CHANGED = 'wallet.changed';
|
||||
|
||||
/** 命中配置为「大奖」的连胜档派彩(私有频道) */
|
||||
public const EVT_JACKPOT_HIT = 'jackpot.hit';
|
||||
|
||||
private static function channelName(string $uuid): string
|
||||
{
|
||||
return 'private-user-' . $uuid;
|
||||
}
|
||||
|
||||
private static function createApi(): Api
|
||||
{
|
||||
return new Api(
|
||||
str_replace('0.0.0.0', '127.0.0.1', (string) config('plugin.webman.push.app.api')),
|
||||
(string) config('plugin.webman.push.app.app_key'),
|
||||
(string) config('plugin.webman.push.app.app_secret')
|
||||
);
|
||||
}
|
||||
|
||||
public static function uuidForUserId(int $userId): ?string
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return null;
|
||||
}
|
||||
$u = Db::name('user')->where('id', $userId)->value('uuid');
|
||||
|
||||
return is_string($u) && $u !== '' ? $u : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function publish(int $userId, string $event, array $data): void
|
||||
{
|
||||
$uuid = self::uuidForUserId($userId);
|
||||
if ($uuid === null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
self::createApi()->trigger(self::channelName($uuid), $event, $data);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
151
app/process/GameWebSocketServer.php
Normal file
151
app/process/GameWebSocketServer.php
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use app\common\service\GameWebSocketEventBus;
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Timer;
|
||||
|
||||
/**
|
||||
* 后台测试页 WebSocket 服务(仅用于连接联调)
|
||||
*/
|
||||
class GameWebSocketServer
|
||||
{
|
||||
/** @var array<int, TcpConnection> */
|
||||
private static array $connections = [];
|
||||
|
||||
private static bool $eventBusConsumerStarted = false;
|
||||
|
||||
/**
|
||||
* 从 Redis 队列拉取事件并推送给已订阅连接。
|
||||
* 部分环境下 WebSocket 进程的 onWorkerStart 可能未触发,因此在首帧握手处也会兜底启动一次(全局仅注册一个 Timer)。
|
||||
*/
|
||||
private static function ensureEventBusConsumer(): void
|
||||
{
|
||||
if (self::$eventBusConsumerStarted) {
|
||||
return;
|
||||
}
|
||||
self::$eventBusConsumerStarted = true;
|
||||
Timer::add(1, static function (): void {
|
||||
$events = GameWebSocketEventBus::popBatch();
|
||||
if ($events === []) {
|
||||
return;
|
||||
}
|
||||
foreach ($events as $event) {
|
||||
$topic = $event['topic'] ?? '';
|
||||
if (!is_string($topic) || $topic === '') {
|
||||
continue;
|
||||
}
|
||||
$eventName = $event['event'] ?? $topic;
|
||||
$data = $event['data'] ?? [];
|
||||
if (!is_array($data)) {
|
||||
$data = [];
|
||||
}
|
||||
$serverTime = $event['server_time'] ?? time();
|
||||
foreach (self::$connections as $connection) {
|
||||
$topics = $connection->topics ?? [];
|
||||
if (!is_array($topics) || !in_array($topic, $topics, true)) {
|
||||
continue;
|
||||
}
|
||||
$connection->send(json_encode([
|
||||
'event' => $eventName,
|
||||
'topic' => $topic,
|
||||
'data' => $data,
|
||||
'server_time' => $serverTime,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function onWorkerStart(): void
|
||||
{
|
||||
self::ensureEventBusConsumer();
|
||||
}
|
||||
|
||||
public function onConnect(TcpConnection $connection): void
|
||||
{
|
||||
$connection->topics = [];
|
||||
self::$connections[$connection->id] = $connection;
|
||||
}
|
||||
|
||||
public function onWebSocketConnect(TcpConnection $connection): void
|
||||
{
|
||||
self::ensureEventBusConsumer();
|
||||
$connection->send(json_encode([
|
||||
'event' => 'ws.connected',
|
||||
'message' => 'WebSocket connected',
|
||||
'connection_id' => $connection->id,
|
||||
'server_time' => time(),
|
||||
'heartbeat_interval' => 30,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
public function onMessage(TcpConnection $connection, string $payload): void
|
||||
{
|
||||
$decoded = json_decode($payload, true);
|
||||
if (!is_array($decoded)) {
|
||||
$connection->send(json_encode([
|
||||
'event' => 'ws.error',
|
||||
'message' => 'Invalid JSON payload',
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
return;
|
||||
}
|
||||
|
||||
$action = $decoded['action'] ?? '';
|
||||
if ($action === 'ping') {
|
||||
$connection->send(json_encode([
|
||||
'event' => 'pong',
|
||||
'server_time' => date('Y-m-d H:i:s'),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
return;
|
||||
}
|
||||
|
||||
if ($action === 'subscribe') {
|
||||
$topics = $decoded['topics'] ?? [];
|
||||
$sanitized = [];
|
||||
if (is_array($topics)) {
|
||||
foreach ($topics as $topic) {
|
||||
if (!is_string($topic)) {
|
||||
continue;
|
||||
}
|
||||
$value = trim($topic);
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
$sanitized[] = $value;
|
||||
}
|
||||
}
|
||||
$connection->topics = array_values(array_unique($sanitized));
|
||||
$connection->send(json_encode([
|
||||
'event' => 'ws.subscribed',
|
||||
'topics' => $connection->topics,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
return;
|
||||
}
|
||||
|
||||
$connection->send(json_encode([
|
||||
'event' => 'ws.error',
|
||||
'message' => 'Unsupported action',
|
||||
'received_action' => $action,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
public function onError(TcpConnection $connection, int $code, string $msg): void
|
||||
{
|
||||
$connection->send(json_encode([
|
||||
'event' => 'ws.error',
|
||||
'message' => 'Server internal error',
|
||||
'code' => $code,
|
||||
'detail' => $msg,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
public function onClose(TcpConnection $connection): void
|
||||
{
|
||||
$connection->topics = [];
|
||||
unset(self::$connections[$connection->id]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user