race->fresh(); // 防止重复结算 if (! $race || $race->status !== 'running') { return; } // CAS 改状态为 settled $updated = HorseRace::query() ->where('id', $race->id) ->where('status', 'running') ->update(['status' => 'settled', 'settled_at' => now()]); if (! $updated) { return; } $race->refresh(); $config = GameConfig::forGame('horse_racing')?->params ?? []; $houseTake = (int) ($config['house_take_percent'] ?? 5); $seedPool = (int) ($config['seed_pool'] ?? 0); $winnerId = (int) $race->winner_horse_id; // 按马匹统计各匹下注金额 $horsePools = HorseBet::query() ->where('race_id', $race->id) ->groupBy('horse_id') ->selectRaw('horse_id, SUM(amount) as pool') ->pluck('pool', 'horse_id') ->toArray(); $totalPool = array_sum($horsePools); $winnerPool = (int) ($horsePools[$winnerId] ?? 0); $distributablePool = HorseRace::calcDistributablePool($horsePools, $houseTake, $seedPool, $winnerPool); // 结算:遍历所有下注记录 $bets = HorseBet::query() ->where('race_id', $race->id) ->where('status', 'pending') ->with('user') ->get(); $totalPayout = 0; $participantSettlements = []; DB::transaction(function () use ($bets, $winnerId, $distributablePool, $winnerPool, $currency, &$totalPayout, &$participantSettlements) { foreach ($bets as $bet) { if ((int) $bet->horse_id !== $winnerId) { // 未中奖(本金已在下注时扣除) $bet->update(['status' => 'lost', 'payout' => 0]); $this->recordParticipantSettlement($participantSettlements, $bet, -$bet->amount, 0); continue; } // 中奖:按注额比例分配净注池 if ($winnerPool > 0) { $payout = (int) round($distributablePool * ($bet->amount / $winnerPool)); } else { $payout = 0; } $bet->update(['status' => 'won', 'payout' => $payout]); if ($payout > 0 && $bet->user) { $currency->change( $bet->user, 'gold', $payout, CurrencySource::HORSE_WIN, "赛马 #{$this->race->id} 「{$bet->horse_id}号马」中奖", ); } // 结算提示需要显示本场净输赢,因此要减去下注时已支付的本金。 $this->recordParticipantSettlement($participantSettlements, $bet, $payout - $bet->amount, $payout); $totalPayout += $payout; } }); // 公屏公告 $this->pushResultMessage($race, $chatState, $totalPayout); // 参与者私聊结算提醒 $this->pushParticipantToastNotifications($race, $chatState, $participantSettlements); // 广播结算事件 broadcast(new HorseRaceSettled($race)); } /** * 汇总单个参与者本场的下注、返还与净输赢金额。 * * @param array> $participantSettlements */ private function recordParticipantSettlement(array &$participantSettlements, HorseBet $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, 'horse_id' => (int) $bet->horse_id, ]; // 即使出现脏数据导致同一用户多笔下注,也统一汇总成本场总输赢。 $existing['bet_amount'] += (int) $bet->amount; $existing['payout'] += $payout; $existing['net_change'] += $netChange; $existing['horse_id'] = (int) $bet->horse_id; $participantSettlements[$userId] = $existing; } /** * 向参与本场的用户发送私聊结算提示,并复用右下角 toast 通知。 * * @param array> $participantSettlements */ private function pushParticipantToastNotifications(HorseRace $race, ChatStateService $chatState, array $participantSettlements): void { if ($participantSettlements === []) { return; } $roomId = 1; $winnerName = $this->resolveWinnerHorseName($race); 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); $horseId = (int) $settlement['horse_id']; $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 ? "冠军:{$winnerName}
你本场净赢 +{$absNetChange} 金币!" : ($netChange < 0 ? "冠军:{$winnerName}
你本场净输 -{$absNetChange} 金币。" : "冠军:{$winnerName}
你本场不输不赢。"); // 写入系统私聊,方便用户在聊天历史中回看本场结算结果。 $msg = [ 'id' => $chatState->nextMessageId($roomId), 'room_id' => $roomId, 'from_user' => '系统', 'to_user' => $username, 'content' => "🏇 赛马第 #{$race->id} 场已结束,冠军:{$winnerName}。你押注 {$horseId} 号马 {$betAmountText} 金币,{$summaryText};当前金币:{$freshGold} 枚。", 'is_secret' => true, 'font_color' => '#f59e0b', '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); } } /** * 向公屏发送赛果系统消息。 */ private function pushResultMessage(HorseRace $race, ChatStateService $chatState, int $totalPayout): void { // 找出胜利马匹名称 $winnerName = $this->resolveWinnerHorseName($race); $payoutText = $totalPayout > 0 ? '共派发 💰'.number_format($totalPayout).' 金币' : '本场无人获奖'; $content = "🏆 【赛马】第 #{$race->id} 场结束!冠军:{$winnerName}!{$payoutText}。"; $msg = [ 'id' => $chatState->nextMessageId(1), 'room_id' => 1, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => $content, 'is_secret' => false, 'font_color' => '#f59e0b', 'action' => '大声宣告', 'sent_at' => now()->toDateTimeString(), ]; $chatState->pushMessage(1, $msg); broadcast(new MessageSent(1, $msg)); SaveMessageJob::dispatch($msg); } /** * 解析冠军马匹的展示名称。 */ private function resolveWinnerHorseName(HorseRace $race): string { $horses = $race->horses ?? []; foreach ($horses as $horse) { if (($horse['id'] ?? 0) === (int) $race->winner_horse_id) { return ($horse['emoji'] ?? '').($horse['name'] ?? ''); } } return '未知'; } }