Files
chatroom/app/Jobs/CloseHorseRaceJob.php
2026-04-14 22:14:10 +08:00

279 lines
9.7 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 文件功能:赛马结算队列任务
*
* 跑马结束后触发,按彩池赔率结算所有下注记录,
* 中奖者获得按注池比例计算的赔付,失败者金币已在下注时扣除。
* 结算完成后广播结果并发公屏公告。
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Jobs;
use App\Enums\CurrencySource;
use App\Events\HorseRaceSettled;
use App\Events\MessageSent;
use App\Models\GameConfig;
use App\Models\HorseBet;
use App\Models\HorseRace;
use App\Services\ChatStateService;
use App\Services\UserCurrencyService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\DB;
class CloseHorseRaceJob implements ShouldQueue
{
use Queueable;
/**
* 最大重试次数。
*/
public int $tries = 3;
/**
* @param HorseRace $race 要结算的场次
*/
public function __construct(
public readonly HorseRace $race,
) {}
/**
* 执行结算逻辑。
*/
public function handle(
UserCurrencyService $currency,
ChatStateService $chatState,
): void {
$race = $this->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<int, array<string, mixed>> $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<int, array<string, mixed>> $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
? "冠军:<b>{$winnerName}</b><br>你本场净赢 <b>+{$absNetChange}</b> 金币!"
: ($netChange < 0
? "冠军:<b>{$winnerName}</b><br>你本场净输 <b>-{$absNetChange}</b> 金币。"
: "冠军:<b>{$winnerName}</b><br>你本场不输不赢。");
// 写入系统私聊,方便用户在聊天历史中回看本场结算结果。
$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 '未知';
}
}