driverSupportsRealtime()) { return; } $nowUtc = Carbon::now()->utc(); $ms = (int) floor(microtime(true) * 1000); $fingerprint = $this->snapshot->hallTargetFingerprint($nowUtc); $lastFingerprint = Cache::get(self::COUNTDOWN_FP_CACHE_KEY); $stateChanged = $this->fingerprintChanged($lastFingerprint, $fingerprint); $shouldSync = $this->shouldBroadcastSyncPulse($nowUtc); Cache::put(self::COUNTDOWN_FP_CACHE_KEY, $fingerprint, now()->addMinutes(10)); if (! $stateChanged && ! $shouldSync) { return; } broadcast(new DrawCountdownBroadcast($this->snapshot->build($nowUtc), $ms)); } /** * Tick 首尾对比:**数据库**当期指纹({@see DrawHallSnapshotBuilder::hallTargetFingerprint})变了再发, * 载荷仍为 {@see DrawHallSnapshotBuilder::build()}(含未到 tick 时对 `open` 的展示规范化)。 */ public function notifyStatusChangeIfHallDbChanged(?array $fpBefore, ?array $fpAfter, ?array $snapshotPayload): void { if (! $this->driverSupportsRealtime()) { return; } if (($fpBefore['draw_no'] ?? null) === ($fpAfter['draw_no'] ?? null) && ($fpBefore['status'] ?? null) === ($fpAfter['status'] ?? null)) { return; } $this->notifyStatusChange($snapshotPayload); } /** `draw.status_change`(管理端发布后等不与 tick 同路径时使用)。 */ public function notifyStatusChange(?array $data): void { if (! $this->driverSupportsRealtime()) { return; } broadcast(new DrawStatusChangeBroadcast($data, (int) floor(microtime(true) * 1000))); } /** `result.published` */ public function notifyResultPublished(?array $data): void { if (! $this->driverSupportsRealtime()) { return; } 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), )); } /** * `play.catalog_updated` —— 玩法/赔率/封顶版本发布(全量目录变更)。 * * @param string $module play_config|odds|risk_cap */ public function notifyPlayCatalogUpdated( string $module, int $versionId, string $versionLabel, ?array $meta = null, ): void { if (! $this->driverSupportsRealtime()) { return; } broadcast(new PlayCatalogUpdatedBroadcast( $module, $versionId, $versionLabel, $meta, (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), )); } /** `jackpot.burst` —— Jackpot 爆池动画与浏览器通知 */ public function notifyJackpotBurst( int $drawId, string $drawNo, string $firstPrizeNumber, string $currencyCode, int $totalPayoutAmount, int $winnerCount, string $triggerType, int $poolAmountAfter, ): void { if (! $this->driverSupportsRealtime()) { return; } broadcast(new JackpotBurstBroadcast( $drawId, $drawNo, $firstPrizeNumber, strtoupper($currencyCode), $totalPayoutAmount, $winnerCount, $triggerType, $poolAmountAfter, (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); } private function shouldBroadcastSyncPulse(Carbon $nowUtc): bool { $interval = match ((int) config('lottery.realtime_hall_countdown_sync_interval_seconds', 5)) { 1, 2, 5, 10 => (int) config('lottery.realtime_hall_countdown_sync_interval_seconds', 5), default => 5, }; return $interval === 1 || ((int) $nowUtc->format('s')) % $interval === 0; } private function fingerprintChanged(mixed $before, ?array $after): bool { if (! is_array($before)) { return $after !== null; } return ($before['draw_no'] ?? null) !== ($after['draw_no'] ?? null) || ($before['status'] ?? null) !== ($after['status'] ?? null); } }