From 6d3711e1db7ff6a551ae1a2f4a8fc8e782ad3cc2 Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Tue, 26 May 2026 19:01:28 +0800 Subject: [PATCH] =?UTF-8?q?1.ws=E4=BC=98=E5=8C=96bet.win=E8=AE=A2=E9=98=85?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E4=B8=AD=E5=A4=A7=E5=A5=96=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/GameBetSettleService.php | 44 +++++++++++ app/common/service/GameLiveService.php | 1 + scripts/debug_bet_win_push.php | 79 +++++++++++++++++++ scripts/republish_bet_win.php | 84 +++++++++++++++++++++ 4 files changed, 208 insertions(+) create mode 100644 scripts/debug_bet_win_push.php create mode 100644 scripts/republish_bet_win.php diff --git a/app/common/service/GameBetSettleService.php b/app/common/service/GameBetSettleService.php index 6e217ba..c88b73f 100644 --- a/app/common/service/GameBetSettleService.php +++ b/app/common/service/GameBetSettleService.php @@ -494,6 +494,50 @@ final class GameBetSettleService $effectiveBetWins = self::buildBetWinPayloadsFromSettledOrders($periodId, $resultNumber); } self::publishBetWinsAfterCommit($effectiveBetWins, $periodId); + if ($periodId > 0 && $resultNumber > 0) { + self::ensurePeriodBetWinNotifications($periodId, $resultNumber); + } + } + + /** + * 开奖后兜底:库内已有中奖但 Redis 无 bet.win 去重键时补推(避免 streak/wallet 整期 dedup 或旧版逻辑漏推)。 + */ + public static function ensurePeriodBetWinNotifications(int $periodId, int $resultNumber): void + { + if ($periodId <= 0 || $resultNumber < 1) { + return; + } + $payloads = self::buildBetWinPayloadsFromSettledOrders($periodId, $resultNumber); + if ($payloads === []) { + return; + } + $missing = []; + foreach ($payloads as $payload) { + if (!is_array($payload)) { + continue; + } + $userId = filter_var($payload['user_id'] ?? 0, FILTER_VALIDATE_INT); + if ($userId === false || $userId <= 0) { + continue; + } + $key = self::BET_WIN_NOTIFY_DEDUP_PREFIX . $periodId . ':' . $userId; + try { + $existing = Redis::get($key); + if ($existing !== false && $existing !== null && $existing !== '') { + continue; + } + } catch (Throwable) { + } + $missing[] = $payload; + } + if ($missing !== []) { + Log::warning('bet.win ensurePeriodBetWinNotifications republish', [ + 'period_id' => $periodId, + 'result_number' => $resultNumber, + 'user_ids' => array_map(static fn (array $p): int => (int) ($p['user_id'] ?? 0), $missing), + ]); + self::publishBetWinsAfterCommit($missing, $periodId); + } } private static function markBetWinNotifyOnce(int $periodId, int $userId): bool diff --git a/app/common/service/GameLiveService.php b/app/common/service/GameLiveService.php index 4010f00..1fc2874 100644 --- a/app/common/service/GameLiveService.php +++ b/app/common/service/GameLiveService.php @@ -162,6 +162,7 @@ final class GameLiveService is_string($row['period_no'] ?? null) ? (string) $row['period_no'] : '', (int) $resultNumber ); + GameBetSettleService::ensurePeriodBetWinNotifications($recordId, (int) $resultNumber); GameHotDataCoordinator::afterGameRecordCommitted($recordId); self::publishSnapshot(null); diff --git a/scripts/debug_bet_win_push.php b/scripts/debug_bet_win_push.php new file mode 100644 index 0000000..3a9f2c9 --- /dev/null +++ b/scripts/debug_bet_win_push.php @@ -0,0 +1,79 @@ +where('id', $playId)->find(); +if (!is_array($row)) { + $row = Db::name('bet_order')->where('id', $playId)->find(); +} +if (!is_array($row)) { + fwrite(STDERR, "play record not found: {$playId}\n"); + exit(1); +} + +$periodId = (int) ($row['period_id'] ?? 0); +$periodNo = (string) ($row['period_no'] ?? ''); +$userId = (int) ($row['user_id'] ?? 0); +$status = (int) ($row['status'] ?? 0); +$winAmount = (string) ($row['win_amount'] ?? '0'); + +echo "=== play_record id={$playId} ===\n"; +echo "period_id={$periodId} period_no={$periodNo}\n"; +echo "user_id={$userId} status={$status} win_amount={$winAmount}\n"; +echo "streak_at_bet=" . ($row['streak_at_bet'] ?? '') . "\n"; + +$user = Db::name('user')->where('id', $userId)->field('id,username,phone')->find(); +if (is_array($user)) { + echo "user: id={$user['id']} username={$user['username']} phone={$user['phone']}\n"; +} + +$gr = Db::name('game_record')->where('id', $periodId)->find(); +if (is_array($gr)) { + echo "game_record: status={$gr['status']} result_number={$gr['result_number']} period_start_at={$gr['period_start_at']}\n"; +} + +$settleKey = 'dfw:v1:settle:notify:' . $periodId; +$betWinKey = 'dfw:v1:ws:betwin:' . $periodId . ':' . $userId; +echo "\n=== Redis dedup keys ===\n"; +try { + echo "settle_notify={$settleKey} => " . var_export(Redis::get($settleKey), true) . "\n"; + echo "bet_win={$betWinKey} => " . var_export(Redis::get($betWinKey), true) . "\n"; +} catch (Throwable $e) { + echo 'redis err: ' . $e->getMessage() . "\n"; +} + +$queueLen = 0; +try { + $queueLen = (int) Redis::lLen('dfw:v1:ws:event:queue'); +} catch (Throwable $e) { +} +echo "ws queue length={$queueLen}\n"; + +$resultNumber = is_array($gr) ? (int) ($gr['result_number'] ?? 0) : 0; +$payloads = GameBetSettleService::buildBetWinPayloadsFromSettledOrders($periodId, $resultNumber); +echo "\n=== buildBetWinPayloadsFromSettledOrders ===\n"; +echo 'winner_count=' . count($payloads) . "\n"; +foreach ($payloads as $p) { + echo json_encode($p, JSON_UNESCAPED_UNICODE) . "\n"; +} + +$allWinners = Db::name('bet_order') + ->where('period_id', $periodId) + ->whereIn('status', [2, 5]) + ->whereRaw('CAST(win_amount AS DECIMAL(20,2)) > 0') + ->field('id,user_id,win_amount,status,streak_at_bet') + ->select() + ->toArray(); +echo "\n=== all winning orders in period ===\n"; +foreach ($allWinners as $w) { + echo "bet_id={$w['id']} user_id={$w['user_id']} win={$w['win_amount']} status={$w['status']} streak_at_bet={$w['streak_at_bet']}\n"; +} diff --git a/scripts/republish_bet_win.php b/scripts/republish_bet_win.php new file mode 100644 index 0000000..a656aff --- /dev/null +++ b/scripts/republish_bet_win.php @@ -0,0 +1,84 @@ + 0) { + $pid = Db::name('game_play_record')->where('id', $playId)->value('period_id'); + $periodId = is_numeric((string) $pid) ? (int) $pid : 0; + echo "play_record_id={$playId} => period_id={$periodId}" . PHP_EOL; + } +} +if ($periodId <= 0 && isset($opts['period-no'])) { + $periodNo = trim((string) $opts['period-no']); + if ($periodNo !== '') { + $pid = Db::name('game_record')->where('period_no', $periodNo)->value('id'); + $periodId = is_numeric((string) $pid) ? (int) $pid : 0; + echo "period_no={$periodNo} => period_id={$periodId}" . PHP_EOL; + } +} + +if ($periodId <= 0) { + fwrite(STDERR, "请指定 --period-id、--play-record-id 或 --period-no\n"); + exit(1); +} + +$row = Db::name('game_record')->where('id', $periodId)->find(); +if (!is_array($row)) { + fwrite(STDERR, "对局不存在: period_id={$periodId}\n"); + exit(1); +} + +$resultNumber = filter_var($row['result_number'] ?? 0, FILTER_VALIDATE_INT); +if ($resultNumber === false || $resultNumber < 1) { + fwrite(STDERR, "对局尚未开奖,无法补发 bet.win\n"); + exit(1); +} + +$payloads = GameBetSettleService::buildBetWinPayloadsFromSettledOrders($periodId, $resultNumber); +if ($payloads === []) { + echo "本期无已结算中奖注单,无需补发。\n"; + exit(0); +} + +if ($force) { + foreach ($payloads as $payload) { + $uid = (int) ($payload['user_id'] ?? 0); + if ($uid <= 0) { + continue; + } + try { + Redis::del('dfw:v1:ws:betwin:' . $periodId . ':' . $uid); + } catch (Throwable) { + } + } + echo "已清除 dedup 键(--force)\n"; +} + +GameBetSettleService::publishBetWinsAfterCommit($payloads, $periodId); +echo '已补发 bet.win 用户数: ' . count($payloads) . PHP_EOL; +foreach ($payloads as $p) { + echo ' user_id=' . ($p['user_id'] ?? '') . ' total_win=' . ($p['total_win'] ?? '') . ' period_no=' . ($p['period_no'] ?? '') . PHP_EOL; +}