Files
lotteryLaravel/app/Services/Draw/LotteryHallRealtimeBroadcaster.php

236 lines
7.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services\Draw;
use Carbon\Carbon;
use App\Events\OddsUpdateBroadcast;
use App\Events\PlayCatalogUpdatedBroadcast;
use App\Events\PlayToggleBroadcast;
use App\Events\RiskSoldOutBroadcast;
use App\Events\RiskWarningBroadcast;
use App\Events\JackpotBurstBroadcast;
use App\Events\DrawCountdownBroadcast;
use App\Events\DrawStatusChangeBroadcast;
use App\Events\DrawResultPublishedBroadcast;
use Illuminate\Support\Facades\Cache;
/**
* 对齐界面文档 §2.1:大厅公共频道广播(`lottery-hall`)。
* 包含draw.countdown、draw.status_change、result.published、
* risk.sold_out、risk.warning、play.toggle、odds.update、jackpot.burst
*/
final class LotteryHallRealtimeBroadcaster
{
private const COUNTDOWN_FP_CACHE_KEY = 'lottery:hall:countdown:last-fingerprint';
public function __construct(
private readonly DrawHallSnapshotBuilder $snapshot,
) {}
/** 每秒调度:边界变化立刻推送,完整快照按低频校准,避免仅本地倒计时无法切期。 */
public function countdownPulse(): void
{
if (! $this->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);
}
}