diff --git a/app/common/service/GameBetSettleService.php b/app/common/service/GameBetSettleService.php index 5a1f5a2..d841a48 100644 --- a/app/common/service/GameBetSettleService.php +++ b/app/common/service/GameBetSettleService.php @@ -35,6 +35,9 @@ final class GameBetSettleService /** 每期每用户 bet.win 去重(与 streak/wallet 分离,避免整期 dedup 吞掉中奖推送) */ private const BET_WIN_NOTIFY_DEDUP_PREFIX = 'dfw:v1:ws:betwin:'; + /** jackpot.hit 去重(独立于 settleNotify,避免 recover/补偿路径漏广播) */ + private const JACKPOT_HIT_DEDUP_PREFIX = 'dfw:v1:ws:jackpot_hit:'; + /** * 对指定期次按开奖号码结算所有「待开奖」注单;同一注单幂等(仅 status=1 会更新)。 * @@ -435,13 +438,40 @@ final class GameBetSettleService if ($jackpotHits === [] || $periodId <= 0 || $resultNumber < 1) { return; } - GameWebSocketEventBus::publish(self::TOPIC_JACKPOT_HIT, [ + $dedupKey = self::JACKPOT_HIT_DEDUP_PREFIX . $periodId; + try { + $already = Redis::get($dedupKey); + if ($already !== false && $already !== null && $already !== '') { + return; + } + } catch (Throwable) { + // ignore + } + + $ok = GameWebSocketEventBus::publish(self::TOPIC_JACKPOT_HIT, [ 'period_id' => $periodId, 'period_no' => $periodNo, 'result_number' => $resultNumber, 'hits' => $jackpotHits, 'server_time' => time(), ]); + if ($ok) { + try { + Redis::setEx($dedupKey, 86400, '1'); + } catch (Throwable) { + } + Log::channel('ws')->info('jackpot.hit published', [ + 'period_id' => $periodId, + 'period_no' => $periodNo, + 'result_number' => $resultNumber, + 'hit_count' => count($jackpotHits), + ]); + } else { + Log::channel('ws')->warning('jackpot.hit publish failed', [ + 'period_id' => $periodId, + 'period_no' => $periodNo, + ]); + } } /** @@ -473,6 +503,10 @@ final class GameBetSettleService } } + // jackpot.hit 独立去重:即使 settleNotify 已被占位(recover/补偿路径)也允许补广播一次 + $jackpotHits = is_array($settleOut['jackpot_hits'] ?? null) ? $settleOut['jackpot_hits'] : []; + self::publishJackpotHitsAfterCommit($jackpotHits, $periodId, $periodNo, $resultNumber); + if (($settledCount !== false && $settledCount > 0) || $hasStreak || $hasWallet) { if (self::markSettlementNotifyOnce($periodId)) { $streakEvents = is_array($settleOut['user_streak_events'] ?? null) ? $settleOut['user_streak_events'] : []; @@ -500,9 +534,6 @@ final class GameBetSettleService } GameWebSocketEventBus::publish('wallet.changed', GameWebSocketPayloadHelper::mergeUserStreakInto($payload, $userId)); } - - $jackpotHits = is_array($settleOut['jackpot_hits'] ?? null) ? $settleOut['jackpot_hits'] : []; - self::publishJackpotHitsAfterCommit($jackpotHits, $periodId, $periodNo, $resultNumber); } } diff --git a/app/common/service/GameWebSocketPayloadHelper.php b/app/common/service/GameWebSocketPayloadHelper.php index da21e05..1d60af1 100644 --- a/app/common/service/GameWebSocketPayloadHelper.php +++ b/app/common/service/GameWebSocketPayloadHelper.php @@ -125,8 +125,17 @@ final class GameWebSocketPayloadHelper if ($userId <= 0) { return $payload; } + // 注意:部分业务事件(尤其 bet.win)的 is_jackpot 语义为“本期中奖是否大奖”, + // 而赔率字段 is_jackpot 语义为“下一注下注是否处于大奖档”。二者字段名相同但语义不同, + // 这里以 payload 里已有字段为准,避免被 userStreakData 覆盖导致“1胜被误判为中大奖”。 + $streak = self::userStreakData($userId, $currentStreak); + foreach ($streak as $k => $v) { + if (!array_key_exists($k, $payload)) { + $payload[$k] = $v; + } + } - return array_merge($payload, self::userStreakData($userId, $currentStreak)); + return $payload; } /**