diff --git a/app/Events/BalanceUpdateBroadcast.php b/app/Events/BalanceUpdateBroadcast.php new file mode 100644 index 0000000..c219caa --- /dev/null +++ b/app/Events/BalanceUpdateBroadcast.php @@ -0,0 +1,69 @@ + + */ + public function broadcastOn(): array + { + return [new Channel('player.'.$this->playerId)]; + } + + public function broadcastAs(): string + { + return 'balance.update'; + } + + /** + * @return array{player_id: int, currency_code: string, balance_minor: int, balance_formatted: string, change_minor: int, change_formatted: string, reason: string, emitted_at_ms: int} + */ + public function broadcastWith(): array + { + return [ + 'player_id' => $this->playerId, + 'currency_code' => $this->currencyCode, + 'balance_minor' => $this->balanceMinor, + 'balance_formatted' => number_format($this->balanceMinor / 100, 2), + 'change_minor' => $this->changeMinor, + 'change_formatted' => ($this->changeMinor > 0 ? '+' : '').number_format($this->changeMinor / 100, 2), + 'reason' => $this->reason, + 'emitted_at_ms' => $this->emittedAtMs, + ]; + } +} diff --git a/app/Events/OddsUpdateBroadcast.php b/app/Events/OddsUpdateBroadcast.php new file mode 100644 index 0000000..d0b691c --- /dev/null +++ b/app/Events/OddsUpdateBroadcast.php @@ -0,0 +1,62 @@ +|null $diff 差异数据(哪些玩法赔率变化了,可选) + * @param int $emittedAtMs 发送时间戳(毫秒) + */ + public function __construct( + public readonly int $versionId, + public readonly string $versionName, + public readonly ?array $diff, + public readonly int $emittedAtMs, + ) {} + + /** + * 公共频道,所有在大厅的玩家都能收到。 + * + * @return array + */ + public function broadcastOn(): array + { + return [new Channel('lottery-hall')]; + } + + public function broadcastAs(): string + { + return 'odds.update'; + } + + /** + * @return array{version_id: int, version_name: string, diff: array|null, message: string, emitted_at_ms: int} + */ + public function broadcastWith(): array + { + return [ + 'version_id' => $this->versionId, + 'version_name' => $this->versionName, + 'diff' => $this->diff, + 'message' => '赔率已更新,请重新预览注单', + 'emitted_at_ms' => $this->emittedAtMs, + ]; + } +} diff --git a/app/Events/PlayToggleBroadcast.php b/app/Events/PlayToggleBroadcast.php new file mode 100644 index 0000000..5d46aff --- /dev/null +++ b/app/Events/PlayToggleBroadcast.php @@ -0,0 +1,62 @@ + + */ + public function broadcastOn(): array + { + return [new Channel('lottery-hall')]; + } + + public function broadcastAs(): string + { + return 'play.toggle'; + } + + /** + * @return array{play_code: string, enabled: bool, reason: string|null, action: string, emitted_at_ms: int} + */ + public function broadcastWith(): array + { + return [ + 'play_code' => $this->playCode, + 'enabled' => $this->enabled, + 'reason' => $this->reason, + 'action' => $this->enabled ? 'enabled' : 'disabled', + 'emitted_at_ms' => $this->emittedAtMs, + ]; + } +} diff --git a/app/Events/RiskSoldOutBroadcast.php b/app/Events/RiskSoldOutBroadcast.php new file mode 100644 index 0000000..065c07e --- /dev/null +++ b/app/Events/RiskSoldOutBroadcast.php @@ -0,0 +1,61 @@ + + */ + public function broadcastOn(): array + { + return [new Channel('lottery-hall')]; + } + + public function broadcastAs(): string + { + return 'risk.sold_out'; + } + + /** + * @return array{draw_id: int, draw_no: string, normalized_number: string, emitted_at_ms: int} + */ + public function broadcastWith(): array + { + return [ + 'draw_id' => $this->drawId, + 'draw_no' => $this->drawNo, + 'normalized_number' => $this->normalizedNumber, + 'emitted_at_ms' => $this->emittedAtMs, + ]; + } +} diff --git a/app/Events/RiskWarningBroadcast.php b/app/Events/RiskWarningBroadcast.php new file mode 100644 index 0000000..927b437 --- /dev/null +++ b/app/Events/RiskWarningBroadcast.php @@ -0,0 +1,66 @@ + + */ + public function broadcastOn(): array + { + return [new Channel('lottery-hall')]; + } + + public function broadcastAs(): string + { + return 'risk.warning'; + } + + /** + * @return array{draw_id: int, draw_no: string, normalized_number: string, usage_ratio: float, usage_percent: int, warning_threshold: float, emitted_at_ms: int} + */ + public function broadcastWith(): array + { + return [ + 'draw_id' => $this->drawId, + 'draw_no' => $this->drawNo, + 'normalized_number' => $this->normalizedNumber, + 'usage_ratio' => round($this->usageRatio, 4), + 'usage_percent' => (int) round($this->usageRatio * 100), + 'warning_threshold' => 0.8, // 80% 阈值 + 'emitted_at_ms' => $this->emittedAtMs, + ]; + } +} diff --git a/app/Services/Draw/LotteryHallRealtimeBroadcaster.php b/app/Services/Draw/LotteryHallRealtimeBroadcaster.php index f636fe3..fd7d78c 100644 --- a/app/Services/Draw/LotteryHallRealtimeBroadcaster.php +++ b/app/Services/Draw/LotteryHallRealtimeBroadcaster.php @@ -2,12 +2,18 @@ namespace App\Services\Draw; +use App\Events\OddsUpdateBroadcast; +use App\Events\PlayToggleBroadcast; +use App\Events\RiskSoldOutBroadcast; +use App\Events\RiskWarningBroadcast; use App\Events\DrawCountdownBroadcast; use App\Events\DrawStatusChangeBroadcast; use App\Events\DrawResultPublishedBroadcast; /** - * 对齐界面文档 §2.1:`draw.countdown`、`draw.status_change`、`result.published`(频道 `lottery-hall`)。 + * 对齐界面文档 §2.1:大厅公共频道广播(`lottery-hall`)。 + * 包含:draw.countdown、draw.status_change、result.published、 + * risk.sold_out、risk.warning、play.toggle、odds.update */ final class LotteryHallRealtimeBroadcaster { @@ -66,6 +72,67 @@ final class LotteryHallRealtimeBroadcaster broadcast(new DrawResultPublishedBroadcast($data, (int) floor(microtime(true) * 1000))); } + /** `risk.sold_out` —— 号码赔付池耗尽 */ + public function notifyRiskSoldOut(int $drawId, string $drawNo, string $normalizedNumber): void + { + if (! $this->driverSupportsRealtime()) { + return; + } + + broadcast(new RiskSoldOutBroadcast( + $drawId, + $drawNo, + $normalizedNumber, + (int) floor(microtime(true) * 1000), + )); + } + + /** `risk.warning` —— 号码赔付池占用超 80% 预警 */ + public function notifyRiskWarning(int $drawId, string $drawNo, string $normalizedNumber, float $usageRatio): void + { + if (! $this->driverSupportsRealtime()) { + return; + } + + broadcast(new RiskWarningBroadcast( + $drawId, + $drawNo, + $normalizedNumber, + $usageRatio, + (int) floor(microtime(true) * 1000), + )); + } + + /** `play.toggle` —— 玩法开关变更 */ + public function notifyPlayToggle(string $playCode, bool $enabled, ?string $reason = null): void + { + if (! $this->driverSupportsRealtime()) { + return; + } + + broadcast(new PlayToggleBroadcast( + $playCode, + $enabled, + $reason, + (int) floor(microtime(true) * 1000), + )); + } + + /** `odds.update` —— 赔率变更 */ + public function notifyOddsUpdate(int $versionId, string $versionName, ?array $diff = null): void + { + if (! $this->driverSupportsRealtime()) { + return; + } + + broadcast(new OddsUpdateBroadcast( + $versionId, + $versionName, + $diff, + (int) floor(microtime(true) * 1000), + )); + } + private function driverSupportsRealtime(): bool { $default = config('broadcasting.default'); diff --git a/app/Services/PlayerRealtimeBroadcaster.php b/app/Services/PlayerRealtimeBroadcaster.php new file mode 100644 index 0000000..a61f89e --- /dev/null +++ b/app/Services/PlayerRealtimeBroadcaster.php @@ -0,0 +1,48 @@ +driverSupportsRealtime()) { + return; + } + + broadcast(new BalanceUpdateBroadcast( + $playerId, + $currencyCode, + $balanceMinor, + $changeMinor, + $reason, + (int) floor(microtime(true) * 1000), + )); + } + + private function driverSupportsRealtime(): bool + { + $default = config('broadcasting.default'); + if ($default === null || $default === 'null') { + return false; + } + + $driver = config("broadcasting.connections.{$default}.driver") ?? $default; + + return ! in_array($driver, ['null', 'log'], true); + } +}