round->fresh(); // 防止重复结算 if (! $round || $round->status !== 'betting') { return; } $config = GameConfig::forGame('baccarat')?->params ?? []; // 乐观锁:CAS 先把状态改为 settling $updated = BaccaratRound::query() ->where('id', $round->id) ->where('status', 'betting') ->update(['status' => 'settling']); if (! $updated) { return; // 已被其他进程处理 } // ── 摇骰子 ────────────────────────────────────────────────── $dice = [random_int(1, 6), random_int(1, 6), random_int(1, 6)]; $total = array_sum($dice); // ── 判断结果 ──────────────────────────────────────────────── $killPoints = $config['kill_points'] ?? [3, 18]; if (! is_array($killPoints)) { $killPoints = explode(',', (string) $killPoints); } $killPoints = array_map('intval', $killPoints); $result = match (true) { $dice[0] === $dice[1] && $dice[1] === $dice[2] => 'triple', // 豹子(优先判断) in_array($total, $killPoints, true) => 'kill', // 庄家收割 $total >= 11 && $total <= 17 => 'big', // 大 default => 'small', // 小 }; // ── 结算下注记录 ───────────────────────────────────────────── $bets = BaccaratBet::query()->where('round_id', $round->id)->where('status', 'pending')->with('user')->get(); $totalPayout = 0; // 收集各用户输赢结果,用于公屏展示 $winners = []; $losers = []; $participantSettlements = []; DB::transaction(function () use ($bets, $result, $config, $currency, $lossCoverService, &$totalPayout, &$winners, &$losers, &$participantSettlements) { foreach ($bets as $bet) { /** @var \App\Models\BaccaratBet $bet */ $username = $bet->user->username ?? '匿名'; if ($result === 'kill') { // 庄家收割:全灭无退款 $bet->update(['status' => 'lost', 'payout' => 0]); $lossCoverService->registerSettlement($bet->fresh()); $losers[] = "{$username}-{$bet->amount}"; $this->recordParticipantSettlement($participantSettlements, $bet, -$bet->amount, 0); if ($username === 'AI小班长') { $this->handleAiLoseStreak(); } continue; } if ($bet->bet_type === $result) { // 中奖:计算赔付(含本金返还) $payout = BaccaratRound::calcPayout($bet->bet_type, $bet->amount, $config); $bet->update(['status' => 'won', 'payout' => $payout]); // 金币入账 $currency->change( $bet->user, 'gold', $payout, CurrencySource::BACCARAT_WIN, "百家乐 #{$this->round->id} 押 {$bet->betTypeLabel()} 中奖", ); $totalPayout += $payout; $lossCoverService->registerSettlement($bet->fresh()); $winners[] = "{$username}+".number_format($payout); // 结算提醒展示的是本局净输赢,因此要扣除下注时已经支付的本金。 $this->recordParticipantSettlement($participantSettlements, $bet, $payout - $bet->amount, $payout); if ($username === 'AI小班长') { Redis::del('ai_baccarat_lose_streak'); // 赢了清空连输 } } else { $bet->update(['status' => 'lost', 'payout' => 0]); $lossCoverService->registerSettlement($bet->fresh()); $losers[] = "{$username}-".number_format($bet->amount); $this->recordParticipantSettlement($participantSettlements, $bet, -$bet->amount, 0); if ($username === 'AI小班长') { $this->handleAiLoseStreak(); } } } }); // ── 更新局次记录 ───────────────────────────────────────────── $round->update([ 'dice1' => $dice[0], 'dice2' => $dice[1], 'dice3' => $dice[2], 'total_points' => $total, 'result' => $result, 'status' => 'settled', 'settled_at' => now(), 'total_payout' => $totalPayout, ]); $round->refresh(); // ── 广播结算事件 ───────────────────────────────────────────── broadcast(new BaccaratRoundSettled($round)); // ── 公屏公告 ───────────────────────────────────────────────── $this->pushResultMessage($round, $chatState, $winners, $losers); // ── 参与者私聊提醒 ──────────────────────────────────────────── $this->pushParticipantToastNotifications($round, $chatState, $participantSettlements); } /** * 处理 AI 小班长连输逻辑 */ private function handleAiLoseStreak(): void { $streak = Redis::incr('ai_baccarat_lose_streak'); if ($streak >= 10) { Redis::setex('ai_baccarat_timeout', 600, 'timeout'); // 连输十次,停赛10分钟 Redis::del('ai_baccarat_lose_streak'); } } /** * 汇总单个参与者本局的下注、返还与净输赢金额。 * * @param array> $participantSettlements */ private function recordParticipantSettlement(array &$participantSettlements, BaccaratBet $bet, int $netChange, int $payout): void { if (! $bet->user) { return; } $userId = (int) $bet->user->id; $existing = $participantSettlements[$userId] ?? [ 'user' => $bet->user, 'username' => $bet->user->username, 'bet_amount' => 0, 'payout' => 0, 'net_change' => 0, ]; // 同一用户若存在多条下注记录,这里统一聚合成本局总输赢。 $existing['bet_amount'] += (int) $bet->amount; $existing['payout'] += $payout; $existing['net_change'] += $netChange; $participantSettlements[$userId] = $existing; } /** * 向参与本局的用户发送私聊结算提示,并复用右下角 toast 通知。 * * @param array> $participantSettlements */ private function pushParticipantToastNotifications(BaccaratRound $round, ChatStateService $chatState, array $participantSettlements): void { if ($participantSettlements === []) { return; } $roomId = 1; $roundResultLabel = $round->resultLabel(); foreach ($participantSettlements as $settlement) { $user = $settlement['user']; $username = (string) $settlement['username']; $betAmount = (int) $settlement['bet_amount']; $netChange = (int) $settlement['net_change']; $freshGold = (int) ($user->fresh()->jjb ?? 0); $absNetChange = number_format(abs($netChange)); $betAmountText = number_format($betAmount); $summaryText = $netChange > 0 ? "净赢 {$absNetChange} 金币" : ($netChange < 0 ? "净输 {$absNetChange} 金币" : '不输不赢'); $toastIcon = $netChange > 0 ? '🎉' : ($netChange < 0 ? '📉' : '🎲'); $toastColor = $netChange > 0 ? '#10b981' : ($netChange < 0 ? '#ef4444' : '#3b82f6'); $toastMessage = $netChange > 0 ? "本局结果:{$roundResultLabel}
你本局净赢 +{$absNetChange} 金币!" : ($netChange < 0 ? "本局结果:{$roundResultLabel}
你本局净输 -{$absNetChange} 金币。" : "本局结果:{$roundResultLabel}
你本局不输不赢。"); // 写入系统私聊,确保用户刷新聊天室后仍能看到本局输赢记录。 $msg = [ 'id' => $chatState->nextMessageId($roomId), 'room_id' => $roomId, 'from_user' => '系统', 'to_user' => $username, 'content' => "🎲 百家乐第 #{$round->id} 局已开奖,结果:{$roundResultLabel}。你本局下注 {$betAmountText} 金币,{$summaryText};当前金币:{$freshGold} 枚。", 'is_secret' => true, 'font_color' => '#8b5cf6', 'action' => '', 'sent_at' => now()->toDateTimeString(), 'toast_notification' => [ 'title' => '🎲 百家乐本局结算', 'message' => $toastMessage, 'icon' => $toastIcon, 'color' => $toastColor, 'duration' => 10000, ], ]; $chatState->pushMessage($roomId, $msg); broadcast(new MessageSent($roomId, $msg)); SaveMessageJob::dispatch($msg); } } /** * 向公屏发送开奖结果系统消息(含各用户输赢情况)。 * * @param array $winners 中奖用户列表,格式 "用户名+金额" * @param array $losers 未中奖用户列表,格式 "用户名-金额" */ private function pushResultMessage(BaccaratRound $round, ChatStateService $chatState, array $winners = [], array $losers = []): void { $diceStr = "《{$round->dice1}》《{$round->dice2}》《{$round->dice3}》"; $resultText = match ($round->result) { 'big' => "🔵 大({$round->total_points} 点)", 'small' => "🟡 小({$round->total_points} 点)", 'triple' => "💥 豹子!({$round->dice1}{$round->dice2}{$round->dice3})", 'kill' => "☠️ 庄家收割!({$round->total_points} 点)全灭", default => '', }; $payoutText = $round->total_payout > 0 ? '共派发 💰'.number_format($round->total_payout).' 金币' : '本局无人获奖'; // 拼接用户输赢明细(最多显示 10 人,防止消息过长) $detailParts = []; if ($winners) { $detailParts[] = '🏆 中奖:'.implode('、', array_slice($winners, 0, 10)).' 💰'; } if ($losers) { $detailParts[] = '😔 未中:'.implode('、', array_slice($losers, 0, 10)); } $detail = $detailParts ? ' '.implode(' ', $detailParts) : ''; $content = "🎲 【百家乐】第 #{$round->id} 局开奖!{$diceStr} 总点 {$round->total_points} → {$resultText}!{$payoutText}。{$detail}"; $msg = [ 'id' => $chatState->nextMessageId(1), 'room_id' => 1, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => $content, 'is_secret' => false, 'font_color' => '#8b5cf6', 'action' => '大声宣告', 'sent_at' => now()->toDateTimeString(), ]; $chatState->pushMessage(1, $msg); broadcast(new MessageSent(1, $msg)); SaveMessageJob::dispatch($msg); // 触发微信机器人消息推送 (百家乐结果,无人参与时不推送微信群防止刷屏) try { if (! empty($winners) || ! empty($losers)) { $wechatService = new \App\Services\WechatBot\WechatNotificationService; $wechatService->notifyBaccaratResult($content); } } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('WechatBot baccarat notification failed', ['error' => $e->getMessage()]); } } }