diff --git a/app/admin/controller/test/GameCurrentStatus.php b/app/admin/controller/test/GameCurrentStatus.php index 6af370f..7cb7afb 100644 --- a/app/admin/controller/test/GameCurrentStatus.php +++ b/app/admin/controller/test/GameCurrentStatus.php @@ -26,13 +26,18 @@ class GameCurrentStatus extends Backend $subscribeTopics = [ 'period.tick', - 'user.streak', - 'period.opened', 'period.locked', + 'period.opened', 'period.payout', - 'bet.accepted', + 'period.payout.tick', + 'bet.win', + 'user.streak', 'wallet.changed', + 'bet.accepted', + 'jackpot.hit', 'auto.spin.progress', + 'admin.live.snapshot', + 'admin.live.opened', ]; $oddsPushTopics = GameWebSocketPayloadHelper::ODDS_PUSH_TOPICS; diff --git a/app/common/service/GameBetSettleService.php b/app/common/service/GameBetSettleService.php index 7c75e76..8e2f89b 100644 --- a/app/common/service/GameBetSettleService.php +++ b/app/common/service/GameBetSettleService.php @@ -22,17 +22,23 @@ final class GameBetSettleService public const CONFIG_KEY_JACKPOT_MAX_AMOUNT = 'jackpot_max_amount'; + /** 小奖(非大奖档)中奖:移动端弹窗/通知专用 */ + public const TOPIC_BET_WIN = 'bet.win'; + /** * 对指定期次按开奖号码结算所有「待开奖」注单;同一注单幂等(仅 status=1 会更新)。 * - * @return array{jackpot_hits: list} + * @return array{ + * jackpot_hits: list, + * bet_wins: list> + * } * * @throws Throwable */ public static function settleBetsForDraw(int $recordId, int $resultNumber): array { if ($recordId <= 0 || $resultNumber < 1) { - return ['jackpot_hits' => []]; + return ['jackpot_hits' => [], 'bet_wins' => []]; } $now = time(); @@ -53,6 +59,9 @@ final class GameBetSettleService /** @var array */ $jackpotNotify = []; + /** @var array}> */ + $smallWinByUser = []; + foreach ($bets as $bet) { $betId = (int) ($bet['id'] ?? 0); if ($betId <= 0) { @@ -103,10 +112,30 @@ final class GameBetSettleService $balanceAfter = (string) (Db::name('user')->where('id', $userId)->value('coin') ?? '0'); if (!$needReview && bccomp($win, '0', 2) > 0) { - $paid = self::creditUserPayout($bet, $betId, $win, $now, null, '压注派彩'); + $paid = self::creditUserPayout($bet, $betId, $win, $now, null, '压注派彩', $resultNumber); if ($paid !== null) { $balanceAfter = $paid; } + $streakAtBet = (int) ($bet['streak_at_bet'] ?? 0); + if ($paid !== null && !StreakWinReward::isJackpotForStreakAtBet($streakAtBet)) { + if (!isset($smallWinByUser[$userId])) { + $smallWinByUser[$userId] = [ + 'user_id' => $userId, + 'period_id' => $recordId, + 'period_no' => (string) ($bet['period_no'] ?? ''), + 'result_number' => $resultNumber, + 'total_win' => '0.00', + 'balance_after' => $balanceAfter, + 'bets' => [], + ]; + } + $smallWinByUser[$userId]['total_win'] = bcadd($smallWinByUser[$userId]['total_win'], $win, 2); + $smallWinByUser[$userId]['balance_after'] = $balanceAfter; + $smallWinByUser[$userId]['bets'][] = [ + 'bet_id' => $betId, + 'win_amount' => $win, + ]; + } } $periodNo = (string) ($bet['period_no'] ?? ''); @@ -170,7 +199,32 @@ final class GameBetSettleService ]; } - return ['jackpot_hits' => $jackpotHits]; + $betWins = array_values($smallWinByUser); + + return ['jackpot_hits' => $jackpotHits, 'bet_wins' => $betWins]; + } + + /** + * 事务提交后推送小奖中奖(bet.win)。 + * + * @param list> $betWins + */ + public static function publishBetWinsAfterCommit(array $betWins): void + { + foreach ($betWins as $payload) { + if (!is_array($payload) || empty($payload['user_id'])) { + continue; + } + $userId = filter_var($payload['user_id'], FILTER_VALIDATE_INT); + if ($userId === false || $userId <= 0) { + continue; + } + $data = array_merge($payload, [ + 'is_jackpot' => false, + 'server_time' => time(), + ]); + GameWebSocketEventBus::publish(self::TOPIC_BET_WIN, $data); + } } /** @@ -408,7 +462,7 @@ final class GameBetSettleService /** * @return string|null 派彩后余额;已幂等入账过时返回当前余额;失败或未执行派彩返回 null */ - private static function creditUserPayout(array $bet, int $betId, string $winAmount, int $now, ?int $operatorAdminId, string $remark): ?string + private static function creditUserPayout(array $bet, int $betId, string $winAmount, int $now, ?int $operatorAdminId, string $remark, ?int $resultNumber = null): ?string { $userId = (int) ($bet['user_id'] ?? 0); if ($userId <= 0) { @@ -451,13 +505,20 @@ final class GameBetSettleService 'update_time' => $now, ]); GameHotDataCoordinator::afterUserCommitted($userId); - GameWebSocketEventBus::publish('wallet.changed', GameWebSocketPayloadHelper::mergeUserStreakInto([ + $walletPayload = [ 'user_id' => $userId, 'balance_after' => $after, 'biz_type' => 'payout', 'ref_id' => $betId, + 'amount' => $winAmount, + 'period_no' => (string) ($bet['period_no'] ?? ''), + 'period_id' => isset($bet['period_id']) && is_numeric($bet['period_id']) ? (int) $bet['period_id'] : 0, 'changed_at' => $now, - ], $userId)); + ]; + if ($resultNumber !== null && $resultNumber > 0) { + $walletPayload['result_number'] = $resultNumber; + } + GameWebSocketEventBus::publish('wallet.changed', GameWebSocketPayloadHelper::mergeUserStreakInto($walletPayload, $userId)); return $after; } diff --git a/app/common/service/GameLiveService.php b/app/common/service/GameLiveService.php index 15a4476..7e190d6 100644 --- a/app/common/service/GameLiveService.php +++ b/app/common/service/GameLiveService.php @@ -101,9 +101,10 @@ final class GameLiveService $now = time(); $payoutUntil = isset($row['payout_until']) ? (int) $row['payout_until'] : 0; + $settleOut = ['jackpot_hits' => [], 'bet_wins' => []]; Db::startTrans(); try { - GameBetSettleService::settleBetsForDraw($recordId, $resultNumber); + $settleOut = GameBetSettleService::settleBetsForDraw($recordId, $resultNumber); if ($status === 2) { if ($payoutUntil <= 0) { $payoutUntil = $now + self::getPayoutGraceSeconds(); @@ -139,6 +140,9 @@ final class GameLiveService return; } + $betWins = is_array($settleOut['bet_wins'] ?? null) ? $settleOut['bet_wins'] : []; + GameBetSettleService::publishBetWinsAfterCommit($betWins); + GameHotDataCoordinator::afterGameRecordCommitted($recordId); self::publishSnapshot(null); @@ -585,7 +589,7 @@ final class GameLiveService $now = time(); $payoutUntil = $now + self::getPayoutGraceSeconds(); - $settleOut = ['jackpot_hits' => []]; + $settleOut = ['jackpot_hits' => [], 'bet_wins' => []]; Db::startTrans(); try { Db::name('game_record')->where('id', (int) $record['id'])->update([ @@ -603,6 +607,9 @@ final class GameLiveService return ['ok' => false, 'msg' => __('Game live: settlement error') . ': ' . $e->getMessage()]; } + $betWins = is_array($settleOut['bet_wins'] ?? null) ? $settleOut['bet_wins'] : []; + GameBetSettleService::publishBetWinsAfterCommit($betWins); + GameHotDataCoordinator::afterGameRecordCommitted($rid); try { diff --git a/docs/36字花-移动端接口设计草案.md b/docs/36字花-移动端接口设计草案.md index 0ce1ccc..12e6882 100644 --- a/docs/36字花-移动端接口设计草案.md +++ b/docs/36字花-移动端接口设计草案.md @@ -791,7 +791,7 @@ - 订阅连胜/赔率(仅当前玩家):`{"action":"subscribe","topics":["user.streak","wallet.changed","bet.accepted"]}` - 订阅资金流:`{"action":"subscribe","topics":["bet.accepted","wallet.changed"]}` - 订阅托管流:`{"action":"subscribe","topics":["auto.spin.progress","wallet.changed"]}` - - 移动端推荐合并订阅:`period.tick`、`user.streak`、`wallet.changed`、`bet.accepted`、`period.opened` + - 移动端推荐合并订阅:`period.tick`、`bet.win`、`user.streak`、`wallet.changed`、`bet.accepted`、`period.opened`、`period.payout`、`jackpot.hit` #### 7.1.1 消息协议字段定义(联调口径) @@ -833,7 +833,13 @@ - `user.streak`:每期结算更新用户连胜后按用户推送(未中奖也会推送,`current_streak` 可能归零)。 - `admin.live.snapshot`:**每秒一次**(后台实时对局页全量快照;不受派彩静默期影响)。 - `period.opened` / `period.payout` / `admin.live.opened`:按开奖流程阶段触发(事件触发型,非固定频率)。 -- `wallet.changed`:仅在余额发生变更时推送(如下注扣款、充值入账、派彩入账)。 +- `wallet.changed`:仅在余额发生变更时推送(如下注扣款、充值入账、派彩入账)。派彩时 `biz_type=payout`,并带 `amount`(本次派彩金额)、`period_no`、`period_id`、`result_number`(若有)。 +- **`bet.win`(小奖)**:本期中奖且**非大奖档**(`streak_win_reward` 中 `is_jackpot=false`)时,按用户聚合推送一帧;用于弹窗/横幅,**比 `wallet.changed` 更适合做「你中了小奖」展示**。 + - `data.user_id` / `data.period_id` / `data.period_no` / `data.result_number` + - `data.total_win`:本期该用户小奖派彩合计 + - `data.balance_after`:派彩后余额 + - `data.bets[]`:`{ bet_id, win_amount }` 明细 + - `data.is_jackpot`:固定 `false` - `jackpot.hit`:**仅在本期存在中大奖命中用户时推送**;无命中不推送。 - **载荷字段**:`period_id` / `period_no` / `result_number` / `hits[]` / `server_time`。 - `hits[]` 数组每项字段: diff --git a/web/src/views/backend/test/components/WebSocketTestPage.vue b/web/src/views/backend/test/components/WebSocketTestPage.vue index 0ac6f5f..6745ec2 100644 --- a/web/src/views/backend/test/components/WebSocketTestPage.vue +++ b/web/src/views/backend/test/components/WebSocketTestPage.vue @@ -98,13 +98,18 @@ const logs = ref>([]) const defaultSubscribeTopics = [ 'period.tick', - 'user.streak', - 'period.opened', 'period.locked', + 'period.opened', 'period.payout', - 'bet.accepted', + 'period.payout.tick', + 'bet.win', + 'user.streak', 'wallet.changed', + 'bet.accepted', + 'jackpot.hit', 'auto.spin.progress', + 'admin.live.snapshot', + 'admin.live.opened', ] as const const testPlayerOddsSourceLabel = computed(() => {