新增百家乐游戏:①数据库表+模型 ②OpenBaccaratRoundJob开局(广播+公屏) ③CloseBaccaratRoundJob结算(摇骰+赔付+CAS防并发) ④BaccaratController下注接口 ⑤前端弹窗(倒计时/骰子动画/历史趋势) ⑥调度器每分钟检查开局 ⑦GameConfig管控开关
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:百家乐结算队列任务
|
||||
*
|
||||
* 在押注截止时间到达后自动触发:
|
||||
* ① 摇三颗骰子 ② 判断大/小/豹子/庄家收割
|
||||
* ③ 遍历下注记录逐一发放赔付 ④ 广播结果 ⑤ 公屏公告
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Events\BaccaratRoundSettled;
|
||||
use App\Events\MessageSent;
|
||||
use App\Models\BaccaratBet;
|
||||
use App\Models\BaccaratRound;
|
||||
use App\Models\GameConfig;
|
||||
use App\Services\ChatStateService;
|
||||
use App\Services\UserCurrencyService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CloseBaccaratRoundJob implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* 最大重试次数。
|
||||
*/
|
||||
public int $tries = 3;
|
||||
|
||||
/**
|
||||
* @param BaccaratRound $round 要结算的局次
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly BaccaratRound $round,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 执行结算逻辑。
|
||||
*/
|
||||
public function handle(
|
||||
UserCurrencyService $currency,
|
||||
ChatStateService $chatState,
|
||||
): void {
|
||||
$round = $this->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;
|
||||
|
||||
DB::transaction(function () use ($bets, $result, $config, $currency, &$totalPayout) {
|
||||
foreach ($bets as $bet) {
|
||||
if ($result === 'kill') {
|
||||
// 庄家收割:全灭无退款
|
||||
$bet->update(['status' => 'lost', 'payout' => 0]);
|
||||
|
||||
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;
|
||||
} else {
|
||||
$bet->update(['status' => 'lost', 'payout' => 0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── 更新局次记录 ─────────────────────────────────────────────
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向公屏发送开奖结果系统消息。
|
||||
*/
|
||||
private function pushResultMessage(BaccaratRound $round, ChatStateService $chatState): void
|
||||
{
|
||||
$diceStr = "【{$round->dice1}】【{$round->dice2}】【{$round->dice3}】";
|
||||
|
||||
$resultText = match ($round->result) {
|
||||
'big' => "🔵 大({$round->total_points} 点)",
|
||||
'small' => "🟡 小({$round->total_points} 点)",
|
||||
'triple' => "💥 豹子!({$round->dice1}{$round->dice1}{$round->dice1})",
|
||||
'kill' => "☠️ 庄家收割!({$round->total_points} 点)全灭",
|
||||
default => '',
|
||||
};
|
||||
|
||||
$payoutText = $round->total_payout > 0
|
||||
? '共派发 🪙'.number_format($round->total_payout).' 金币'
|
||||
: '本局无人获奖';
|
||||
|
||||
$content = "🎲 【百家乐】第 #{$round->id} 局开奖!{$diceStr} 总点 {$round->total_points} → {$resultText}!{$payoutText}。";
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:百家乐开局队列任务
|
||||
*
|
||||
* 由调度器每 N 分钟触发一次(N 来自 game_configs.params.interval_minutes),
|
||||
* 游戏开启时创建新局次,广播开局事件,并在押注截止时间到期后自动调度结算任务。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Events\BaccaratRoundOpened;
|
||||
use App\Events\MessageSent;
|
||||
use App\Models\BaccaratRound;
|
||||
use App\Models\GameConfig;
|
||||
use App\Services\ChatStateService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
|
||||
class OpenBaccaratRoundJob implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* 最大重试次数。
|
||||
*/
|
||||
public int $tries = 1;
|
||||
|
||||
/**
|
||||
* 执行开局逻辑。
|
||||
*/
|
||||
public function handle(ChatStateService $chatState): void
|
||||
{
|
||||
// 检查游戏是否开启
|
||||
if (! GameConfig::isEnabled('baccarat')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config = GameConfig::forGame('baccarat')?->params ?? [];
|
||||
$betSeconds = (int) ($config['bet_window_seconds'] ?? 60);
|
||||
|
||||
// 防止重复开局(如果上一局还在押注中则跳过)
|
||||
if (BaccaratRound::currentRound()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = now();
|
||||
$closesAt = $now->copy()->addSeconds($betSeconds);
|
||||
|
||||
// 创建新局次
|
||||
$round = BaccaratRound::create([
|
||||
'status' => 'betting',
|
||||
'bet_opens_at' => $now,
|
||||
'bet_closes_at' => $closesAt,
|
||||
]);
|
||||
|
||||
// 广播开局事件
|
||||
broadcast(new BaccaratRoundOpened($round));
|
||||
|
||||
// 公屏系统公告
|
||||
$minBet = number_format($config['min_bet'] ?? 100);
|
||||
$maxBet = number_format($config['max_bet'] ?? 50000);
|
||||
|
||||
$content = "🎲 【百家乐】第 #{$round->id} 局开始!下注时间 {$betSeconds} 秒,可押「大/小/豹子」,押注范围 {$minBet}~{$maxBet} 金币。";
|
||||
$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);
|
||||
|
||||
// 在下注截止时安排结算任务
|
||||
CloseBaccaratRoundJob::dispatch($round)->delay($closesAt);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user