2026-03-01 20:25:09 +08:00
|
|
|
|
<?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;
|
2026-04-11 23:27:29 +08:00
|
|
|
|
use App\Services\BaccaratLossCoverService;
|
2026-03-01 20:25:09 +08:00
|
|
|
|
use App\Services\ChatStateService;
|
|
|
|
|
|
use App\Services\UserCurrencyService;
|
|
|
|
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
|
|
|
|
use Illuminate\Foundation\Queue\Queueable;
|
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
2026-03-26 11:49:36 +08:00
|
|
|
|
use Illuminate\Support\Facades\Redis;
|
2026-03-01 20:25:09 +08:00
|
|
|
|
|
|
|
|
|
|
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,
|
2026-04-11 23:27:29 +08:00
|
|
|
|
BaccaratLossCoverService $lossCoverService,
|
2026-03-01 20:25:09 +08:00
|
|
|
|
): 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;
|
|
|
|
|
|
|
2026-03-04 14:41:07 +08:00
|
|
|
|
// 收集各用户输赢结果,用于公屏展示
|
|
|
|
|
|
$winners = [];
|
|
|
|
|
|
$losers = [];
|
2026-04-14 22:09:03 +08:00
|
|
|
|
$participantSettlements = [];
|
2026-03-04 14:41:07 +08:00
|
|
|
|
|
2026-04-14 22:09:03 +08:00
|
|
|
|
DB::transaction(function () use ($bets, $result, $config, $currency, $lossCoverService, &$totalPayout, &$winners, &$losers, &$participantSettlements) {
|
2026-03-01 20:25:09 +08:00
|
|
|
|
foreach ($bets as $bet) {
|
2026-04-02 14:56:51 +08:00
|
|
|
|
/** @var \App\Models\BaccaratBet $bet */
|
2026-03-04 14:41:07 +08:00
|
|
|
|
$username = $bet->user->username ?? '匿名';
|
|
|
|
|
|
|
2026-03-01 20:25:09 +08:00
|
|
|
|
if ($result === 'kill') {
|
|
|
|
|
|
// 庄家收割:全灭无退款
|
|
|
|
|
|
$bet->update(['status' => 'lost', 'payout' => 0]);
|
2026-04-11 23:27:29 +08:00
|
|
|
|
$lossCoverService->registerSettlement($bet->fresh());
|
2026-03-04 14:41:07 +08:00
|
|
|
|
$losers[] = "{$username}-{$bet->amount}";
|
2026-04-14 22:09:03 +08:00
|
|
|
|
$this->recordParticipantSettlement($participantSettlements, $bet, -$bet->amount, 0);
|
2026-03-01 20:25:09 +08:00
|
|
|
|
|
2026-03-26 11:49:36 +08:00
|
|
|
|
if ($username === 'AI小班长') {
|
|
|
|
|
|
$this->handleAiLoseStreak();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-01 20:25:09 +08:00
|
|
|
|
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;
|
2026-04-11 23:27:29 +08:00
|
|
|
|
$lossCoverService->registerSettlement($bet->fresh());
|
2026-03-04 14:41:07 +08:00
|
|
|
|
$winners[] = "{$username}+".number_format($payout);
|
2026-04-14 22:09:03 +08:00
|
|
|
|
// 结算提醒展示的是本局净输赢,因此要扣除下注时已经支付的本金。
|
|
|
|
|
|
$this->recordParticipantSettlement($participantSettlements, $bet, $payout - $bet->amount, $payout);
|
2026-03-26 11:49:36 +08:00
|
|
|
|
|
|
|
|
|
|
if ($username === 'AI小班长') {
|
|
|
|
|
|
Redis::del('ai_baccarat_lose_streak'); // 赢了清空连输
|
|
|
|
|
|
}
|
2026-03-01 20:25:09 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
$bet->update(['status' => 'lost', 'payout' => 0]);
|
2026-04-11 23:27:29 +08:00
|
|
|
|
$lossCoverService->registerSettlement($bet->fresh());
|
2026-03-04 14:41:07 +08:00
|
|
|
|
$losers[] = "{$username}-".number_format($bet->amount);
|
2026-04-14 22:09:03 +08:00
|
|
|
|
$this->recordParticipantSettlement($participantSettlements, $bet, -$bet->amount, 0);
|
2026-03-26 11:49:36 +08:00
|
|
|
|
|
|
|
|
|
|
if ($username === 'AI小班长') {
|
|
|
|
|
|
$this->handleAiLoseStreak();
|
|
|
|
|
|
}
|
2026-03-01 20:25:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 更新局次记录 ─────────────────────────────────────────────
|
|
|
|
|
|
$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));
|
|
|
|
|
|
|
|
|
|
|
|
// ── 公屏公告 ─────────────────────────────────────────────────
|
2026-03-04 14:41:07 +08:00
|
|
|
|
$this->pushResultMessage($round, $chatState, $winners, $losers);
|
2026-04-14 22:09:03 +08:00
|
|
|
|
|
|
|
|
|
|
// ── 参与者私聊提醒 ────────────────────────────────────────────
|
|
|
|
|
|
$this->pushParticipantToastNotifications($round, $chatState, $participantSettlements);
|
2026-03-01 20:25:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 11:49:36 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 AI 小班长连输逻辑
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function handleAiLoseStreak(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$streak = Redis::incr('ai_baccarat_lose_streak');
|
2026-03-28 21:50:03 +08:00
|
|
|
|
if ($streak >= 10) {
|
|
|
|
|
|
Redis::setex('ai_baccarat_timeout', 600, 'timeout'); // 连输十次,停赛10分钟
|
2026-03-26 11:49:36 +08:00
|
|
|
|
Redis::del('ai_baccarat_lose_streak');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 22:09:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 汇总单个参与者本局的下注、返还与净输赢金额。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array<int, array<string, mixed>> $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<int, array<string, mixed>> $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
|
|
|
|
|
|
? "本局结果:<b>{$roundResultLabel}</b><br>你本局净赢 <b>+{$absNetChange}</b> 金币!"
|
|
|
|
|
|
: ($netChange < 0
|
|
|
|
|
|
? "本局结果:<b>{$roundResultLabel}</b><br>你本局净输 <b>-{$absNetChange}</b> 金币。"
|
|
|
|
|
|
: "本局结果:<b>{$roundResultLabel}</b><br>你本局不输不赢。");
|
|
|
|
|
|
|
|
|
|
|
|
// 写入系统私聊,确保用户刷新聊天室后仍能看到本局输赢记录。
|
|
|
|
|
|
$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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-01 20:25:09 +08:00
|
|
|
|
/**
|
2026-03-04 14:41:07 +08:00
|
|
|
|
* 向公屏发送开奖结果系统消息(含各用户输赢情况)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array<string> $winners 中奖用户列表,格式 "用户名+金额"
|
|
|
|
|
|
* @param array<string> $losers 未中奖用户列表,格式 "用户名-金额"
|
2026-03-01 20:25:09 +08:00
|
|
|
|
*/
|
2026-03-04 14:41:07 +08:00
|
|
|
|
private function pushResultMessage(BaccaratRound $round, ChatStateService $chatState, array $winners = [], array $losers = []): void
|
2026-03-01 20:25:09 +08:00
|
|
|
|
{
|
2026-04-17 15:33:36 +08:00
|
|
|
|
$diceStr = "《{$round->dice1}》《{$round->dice2}》《{$round->dice3}》";
|
2026-03-01 20:25:09 +08:00
|
|
|
|
|
|
|
|
|
|
$resultText = match ($round->result) {
|
|
|
|
|
|
'big' => "🔵 大({$round->total_points} 点)",
|
|
|
|
|
|
'small' => "🟡 小({$round->total_points} 点)",
|
2026-03-04 14:41:07 +08:00
|
|
|
|
'triple' => "💥 豹子!({$round->dice1}{$round->dice2}{$round->dice3})",
|
2026-03-01 20:25:09 +08:00
|
|
|
|
'kill' => "☠️ 庄家收割!({$round->total_points} 点)全灭",
|
|
|
|
|
|
default => '',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
$payoutText = $round->total_payout > 0
|
2026-03-04 15:00:02 +08:00
|
|
|
|
? '共派发 💰'.number_format($round->total_payout).' 金币'
|
2026-03-01 20:25:09 +08:00
|
|
|
|
: '本局无人获奖';
|
|
|
|
|
|
|
2026-03-04 14:41:07 +08:00
|
|
|
|
// 拼接用户输赢明细(最多显示 10 人,防止消息过长)
|
|
|
|
|
|
$detailParts = [];
|
|
|
|
|
|
if ($winners) {
|
2026-03-04 15:00:02 +08:00
|
|
|
|
$detailParts[] = '🏆 中奖:'.implode('、', array_slice($winners, 0, 10)).' 💰';
|
2026-03-04 14:41:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
if ($losers) {
|
|
|
|
|
|
$detailParts[] = '😔 未中:'.implode('、', array_slice($losers, 0, 10));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$detail = $detailParts ? ' '.implode(' ', $detailParts) : '';
|
|
|
|
|
|
|
|
|
|
|
|
$content = "🎲 【百家乐】第 #{$round->id} 局开奖!{$diceStr} 总点 {$round->total_points} → {$resultText}!{$payoutText}。{$detail}";
|
2026-03-01 20:25:09 +08:00
|
|
|
|
|
|
|
|
|
|
$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);
|
2026-04-02 14:56:51 +08:00
|
|
|
|
|
2026-04-02 15:46:01 +08:00
|
|
|
|
// 触发微信机器人消息推送 (百家乐结果,无人参与时不推送微信群防止刷屏)
|
2026-04-02 14:56:51 +08:00
|
|
|
|
try {
|
2026-04-11 23:27:29 +08:00
|
|
|
|
if (! empty($winners) || ! empty($losers)) {
|
2026-04-02 15:46:01 +08:00
|
|
|
|
$wechatService = new \App\Services\WechatBot\WechatNotificationService;
|
|
|
|
|
|
$wechatService->notifyBaccaratResult($content);
|
|
|
|
|
|
}
|
2026-04-02 14:56:51 +08:00
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
|
\Illuminate\Support\Facades\Log::error('WechatBot baccarat notification failed', ['error' => $e->getMessage()]);
|
|
|
|
|
|
}
|
2026-03-01 20:25:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|