2026-03-03 23:19:59 +08:00
|
|
|
|
<?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);
|
2026-04-11 16:11:00 +08:00
|
|
|
|
$seedPool = (int) ($config['seed_pool'] ?? 0);
|
2026-03-03 23:19:59 +08:00
|
|
|
|
$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);
|
2026-04-11 16:11:00 +08:00
|
|
|
|
$distributablePool = HorseRace::calcDistributablePool($horsePools, $houseTake, $seedPool, $winnerPool);
|
2026-03-03 23:19:59 +08:00
|
|
|
|
|
|
|
|
|
|
// 结算:遍历所有下注记录
|
|
|
|
|
|
$bets = HorseBet::query()
|
|
|
|
|
|
->where('race_id', $race->id)
|
|
|
|
|
|
->where('status', 'pending')
|
|
|
|
|
|
->with('user')
|
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
|
|
$totalPayout = 0;
|
|
|
|
|
|
|
2026-04-11 16:11:00 +08:00
|
|
|
|
DB::transaction(function () use ($bets, $winnerId, $distributablePool, $winnerPool, $currency, &$totalPayout) {
|
2026-03-03 23:19:59 +08:00
|
|
|
|
foreach ($bets as $bet) {
|
|
|
|
|
|
if ((int) $bet->horse_id !== $winnerId) {
|
|
|
|
|
|
// 未中奖(本金已在下注时扣除)
|
|
|
|
|
|
$bet->update(['status' => 'lost', 'payout' => 0]);
|
|
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 中奖:按注额比例分配净注池
|
|
|
|
|
|
if ($winnerPool > 0) {
|
2026-04-11 16:11:00 +08:00
|
|
|
|
$payout = (int) round($distributablePool * ($bet->amount / $winnerPool));
|
2026-03-03 23:19:59 +08:00
|
|
|
|
} 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}号马」中奖",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$totalPayout += $payout;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 公屏公告
|
|
|
|
|
|
$this->pushResultMessage($race, $chatState, $totalPayout);
|
|
|
|
|
|
|
|
|
|
|
|
// 广播结算事件
|
|
|
|
|
|
broadcast(new HorseRaceSettled($race));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 向公屏发送赛果系统消息。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function pushResultMessage(HorseRace $race, ChatStateService $chatState, int $totalPayout): void
|
|
|
|
|
|
{
|
|
|
|
|
|
// 找出胜利马匹名称
|
|
|
|
|
|
$horses = $race->horses ?? [];
|
|
|
|
|
|
$winnerName = '未知';
|
|
|
|
|
|
foreach ($horses as $horse) {
|
|
|
|
|
|
if (($horse['id'] ?? 0) === (int) $race->winner_horse_id) {
|
|
|
|
|
|
$winnerName = ($horse['emoji'] ?? '').($horse['name'] ?? '');
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$payoutText = $totalPayout > 0
|
2026-03-04 15:00:02 +08:00
|
|
|
|
? '共派发 💰'.number_format($totalPayout).' 金币'
|
2026-03-03 23:19:59 +08:00
|
|
|
|
: '本场无人获奖';
|
|
|
|
|
|
|
|
|
|
|
|
$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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|