236 lines
7.2 KiB
PHP
236 lines
7.2 KiB
PHP
<?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);
|
||
}
|
||
}
|