1.ws优化bet.win订阅,修复中大奖没有推送

This commit is contained in:
2026-05-26 19:10:23 +08:00
parent 6d3711e1db
commit 4c69a8c77f
3 changed files with 90 additions and 17 deletions

View File

@@ -328,7 +328,7 @@ final class GameBetSettleService
if ($userId === false || $userId <= 0) { if ($userId === false || $userId <= 0) {
continue; continue;
} }
if ($periodId > 0 && !self::markBetWinNotifyOnce($periodId, $userId)) { if ($periodId > 0 && self::hasBetWinNotifyMarked($periodId, $userId)) {
continue; continue;
} }
$isJackpot = !empty($payload['is_jackpot']); $isJackpot = !empty($payload['is_jackpot']);
@@ -340,6 +340,15 @@ final class GameBetSettleService
'server_time' => $now, 'server_time' => $now,
]), $userId); ]), $userId);
GameWebSocketEventBus::publish(self::TOPIC_BET_WIN, $data); GameWebSocketEventBus::publish(self::TOPIC_BET_WIN, $data);
if ($periodId > 0) {
self::markBetWinNotifyOnce($periodId, $userId);
}
Log::info('bet.win published', [
'period_id' => $periodId,
'user_id' => $userId,
'total_win' => $payload['total_win'] ?? '',
'is_jackpot' => $isJackpot,
]);
} }
} }
@@ -489,9 +498,13 @@ final class GameBetSettleService
} }
} }
$effectiveBetWins = $betWins; $effectiveBetWins = $periodId > 0 && $resultNumber > 0
if ($effectiveBetWins === [] && $periodId > 0 && $resultNumber > 0) { ? self::buildBetWinPayloadsFromSettledOrders($periodId, $resultNumber)
$effectiveBetWins = self::buildBetWinPayloadsFromSettledOrders($periodId, $resultNumber); : [];
if ($effectiveBetWins === []) {
$effectiveBetWins = $betWins;
} else {
$effectiveBetWins = self::mergeBetWinPayloads($betWins, $effectiveBetWins);
} }
self::publishBetWinsAfterCommit($effectiveBetWins, $periodId); self::publishBetWinsAfterCommit($effectiveBetWins, $periodId);
if ($periodId > 0 && $resultNumber > 0) { if ($periodId > 0 && $resultNumber > 0) {
@@ -520,13 +533,8 @@ final class GameBetSettleService
if ($userId === false || $userId <= 0) { if ($userId === false || $userId <= 0) {
continue; continue;
} }
$key = self::BET_WIN_NOTIFY_DEDUP_PREFIX . $periodId . ':' . $userId; if (self::hasBetWinNotifyMarked($periodId, $userId)) {
try { continue;
$existing = Redis::get($key);
if ($existing !== false && $existing !== null && $existing !== '') {
continue;
}
} catch (Throwable) {
} }
$missing[] = $payload; $missing[] = $payload;
} }
@@ -540,21 +548,65 @@ final class GameBetSettleService
} }
} }
private static function markBetWinNotifyOnce(int $periodId, int $userId): bool private static function hasBetWinNotifyMarked(int $periodId, int $userId): bool
{ {
if ($periodId <= 0 || $userId <= 0) { if ($periodId <= 0 || $userId <= 0) {
return true; return false;
} }
$key = self::BET_WIN_NOTIFY_DEDUP_PREFIX . $periodId . ':' . $userId; $key = self::BET_WIN_NOTIFY_DEDUP_PREFIX . $periodId . ':' . $userId;
try { try {
$ok = Redis::set($key, '1', ['nx', 'ex' => 86400]); $existing = Redis::get($key);
return $ok === true || $ok === 'OK'; return $existing !== false && $existing !== null && $existing !== '';
} catch (Throwable) { } catch (Throwable) {
return true; return false;
} }
} }
private static function markBetWinNotifyOnce(int $periodId, int $userId): void
{
if ($periodId <= 0 || $userId <= 0) {
return;
}
$key = self::BET_WIN_NOTIFY_DEDUP_PREFIX . $periodId . ':' . $userId;
try {
Redis::setEx($key, 86400, '1');
} catch (Throwable) {
}
}
/**
* @param list<array<string, mixed>> $primary
* @param list<array<string, mixed>> $secondary
* @return list<array<string, mixed>>
*/
private static function mergeBetWinPayloads(array $primary, array $secondary): array
{
/** @var array<int, array<string, mixed>> $byUser */
$byUser = [];
foreach (array_merge($primary, $secondary) as $payload) {
if (!is_array($payload)) {
continue;
}
$userId = filter_var($payload['user_id'] ?? 0, FILTER_VALIDATE_INT);
if ($userId === false || $userId <= 0) {
continue;
}
if (!isset($byUser[$userId])) {
$byUser[$userId] = $payload;
continue;
}
if (!empty($payload['is_jackpot'])) {
$byUser[$userId]['is_jackpot'] = true;
}
if (!empty($payload['payout_pending_review'])) {
$byUser[$userId]['payout_pending_review'] = true;
}
}
return array_values($byUser);
}
private static function markSettlementNotifyOnce(int $periodId): bool private static function markSettlementNotifyOnce(int $periodId): bool
{ {
if ($periodId <= 0) { if ($periodId <= 0) {

View File

@@ -845,7 +845,7 @@
- **`data.payout_pending_review`**`bool``true` 表示已中奖但派彩待后台大奖审核,尚未入账(仍应展示中奖 UI - **`data.payout_pending_review`**`bool``true` 表示已中奖但派彩待后台大奖审核,尚未入账(仍应展示中奖 UI
- **合并赔率字段**(与 §7.1.2A 一致):`current_streak``streak_level``odds_factor``is_jackpot` - **合并赔率字段**(与 §7.1.2A 一致):`current_streak``streak_level``odds_factor``is_jackpot`
- `data.server_time`Unix 秒 - `data.server_time`Unix 秒
- **服务端去重**Redis Key `dfw:v1:ws:betwin:{period_id}:{user_id}`TTL 86400s**每期每用户至多推送一次**;与 `user.streak` / `wallet.changed` 的整期去重键 `dfw:v1:settle:notify:{period_id}` **分离**,避免后者先占位导致 `bet.win` 被吞 - **服务端去重**Redis Key `dfw:v1:ws:betwin:{period_id}:{user_id}`TTL 86400s**入队成功后再写入**每期每用户至多推送一次;与 `dfw:v1:settle:notify:{period_id}` **分离**。结算推送以库内已结算中奖注单为准重建载荷,避免内存聚合丢失
- **补偿**:若内存聚合 `bet_wins` 为空但库内已有本期已结算中奖注单,结算服务会从库重建载荷并补发(`buildBetWinPayloadsFromSettledOrders`)。 - **补偿**:若内存聚合 `bet_wins` 为空但库内已有本期已结算中奖注单,结算服务会从库重建载荷并补发(`buildBetWinPayloadsFromSettledOrders`)。
- **大奖审核通过后**:后台 `approveJackpot` 会再次向该用户推送 `bet.win`(入账后)。 - **大奖审核通过后**:后台 `approveJackpot` 会再次向该用户推送 `bet.win`(入账后)。
- **`jackpot.hit`(公共大奖广播,补充)**:在 **`bet.win` 之后**(同一结算批次内),若本期存在**大奖档命中**用户,**额外**向公共频道推送一帧,供全站公告/跑马灯;无大奖命中则不推送。**个人弹窗仍以 `bet.win` 为主**`jackpot.hit` 用于全站展示昵称与金额。 - **`jackpot.hit`(公共大奖广播,补充)**:在 **`bet.win` 之后**(同一结算批次内),若本期存在**大奖档命中**用户,**额外**向公共频道推送一帧,供全站公告/跑马灯;无大奖命中则不推送。**个人弹窗仍以 `bet.win` 为主**`jackpot.hit` 用于全站展示昵称与金额。

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../support/bootstrap.php';
use support\think\Db;
$periodNo = $argv[1] ?? '20260526-190442-c340448b';
$gr = Db::name('game_record')->where('period_no', $periodNo)->find();
if (!is_array($gr)) {
fwrite(STDERR, "period not found: {$periodNo}\n");
exit(1);
}
$pid = (int) $gr['id'];
$bets = Db::name('bet_order')->where('period_id', $pid)->select()->toArray();
echo "period_id={$pid} status={$gr['status']} result={$gr['result_number']}\n";
foreach ($bets as $b) {
echo "bet {$b['id']} user={$b['user_id']} status={$b['status']} win={$b['win_amount']}\n";
}