功能更新与UI优化:游戏图标移除、用户名片修复、婚礼红包界面重设计
- 移除聊天室右下角浮动游戏图标(占卜、百家乐、赛马、老虎机) - 用户名片按钮区:修复已婚/已好友时按钮换行问题,统一单行显示 - 婚礼红包弹窗:重设计为喜庆鲜红背景,领取按钮改为圆形米黄样式 - 新增婚礼红包恢复接口(/wedding/pending-envelopes),刷新后自动恢复领取按钮 - 修复 Alpine :style 字符串覆盖静态 style 导致圆形按钮失效的问题 - 撤职后用户等级改为根据经验值重新计算,不再无条件重置为1 - 管理员修改用户经验值后自动重算等级,有职务用户等级锁定 - 娱乐大厅钓鱼游戏按钮直接调用 startFishing() 简化操作流程 - 新增赛马、占卜、百家乐游戏及相关后端逻辑
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
<?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);
|
||||
$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);
|
||||
$netPool = (int) ($totalPool * (1 - $houseTake / 100));
|
||||
|
||||
// 结算:遍历所有下注记录
|
||||
$bets = HorseBet::query()
|
||||
->where('race_id', $race->id)
|
||||
->where('status', 'pending')
|
||||
->with('user')
|
||||
->get();
|
||||
|
||||
$totalPayout = 0;
|
||||
|
||||
DB::transaction(function () use ($bets, $winnerId, $netPool, $winnerPool, $currency, &$totalPayout) {
|
||||
foreach ($bets as $bet) {
|
||||
if ((int) $bet->horse_id !== $winnerId) {
|
||||
// 未中奖(本金已在下注时扣除)
|
||||
$bet->update(['status' => 'lost', 'payout' => 0]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// 中奖:按注额比例分配净注池
|
||||
if ($winnerPool > 0) {
|
||||
$payout = (int) round($netPool * ($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}号马」中奖",
|
||||
);
|
||||
}
|
||||
|
||||
$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
|
||||
? '共派发 🪙'.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:赛马开赛队列任务
|
||||
*
|
||||
* 由调度器按配置间隔触发,游戏开启时创建新场次,
|
||||
* 生成参赛马匹,广播开赛事件并安排跑马任务。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Events\HorseRaceOpened;
|
||||
use App\Events\MessageSent;
|
||||
use App\Models\GameConfig;
|
||||
use App\Models\HorseRace;
|
||||
use App\Services\ChatStateService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
|
||||
class OpenHorseRaceJob implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* 最大重试次数。
|
||||
*/
|
||||
public int $tries = 1;
|
||||
|
||||
/**
|
||||
* 执行开赛逻辑。
|
||||
*/
|
||||
public function handle(ChatStateService $chatState): void
|
||||
{
|
||||
// 检查游戏是否开启
|
||||
if (! GameConfig::isEnabled('horse_racing')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止重复开赛(上一场还在进行中)
|
||||
if (HorseRace::currentRace()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config = GameConfig::forGame('horse_racing')?->params ?? [];
|
||||
$betSeconds = (int) ($config['bet_window_seconds'] ?? 90);
|
||||
$horseCount = (int) ($config['horse_count'] ?? 4);
|
||||
$minBet = (int) ($config['min_bet'] ?? 100);
|
||||
$maxBet = (int) ($config['max_bet'] ?? 100000);
|
||||
|
||||
$now = now();
|
||||
$closesAt = $now->copy()->addSeconds($betSeconds);
|
||||
|
||||
// 生成参赛马匹
|
||||
$horses = HorseRace::generateHorses($horseCount);
|
||||
|
||||
// 创建新场次
|
||||
$race = HorseRace::create([
|
||||
'status' => 'betting',
|
||||
'bet_opens_at' => $now,
|
||||
'bet_closes_at' => $closesAt,
|
||||
'horses' => $horses,
|
||||
]);
|
||||
|
||||
// 广播开赛事件
|
||||
broadcast(new HorseRaceOpened($race));
|
||||
|
||||
// 公屏系统公告
|
||||
$horseList = implode(' ', array_map(
|
||||
fn ($h) => "{$h['emoji']}{$h['name']}",
|
||||
$horses
|
||||
));
|
||||
$content = "🐎 【赛马】第 #{$race->id} 场开始!押注时间 {$betSeconds} 秒,参赛马匹:{$horseList}。押注范围 ".number_format($minBet).'~'.number_format($maxBet).' 金币!';
|
||||
|
||||
$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);
|
||||
|
||||
// 押注截止后触发跑马 & 结算任务
|
||||
RunHorseRaceJob::dispatch($race)->delay($closesAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:赛马跑马动画广播 + 结算队列任务
|
||||
*
|
||||
* 押注截止后触发,模拟跑马进度并实时广播,
|
||||
* 跑完后确定获胜者并调度结算任务。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Events\HorseRaceProgress;
|
||||
use App\Events\MessageSent;
|
||||
use App\Models\GameConfig;
|
||||
use App\Models\HorseBet;
|
||||
use App\Models\HorseRace;
|
||||
use App\Services\ChatStateService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
|
||||
class RunHorseRaceJob implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* 最大重试次数。
|
||||
*/
|
||||
public int $tries = 2;
|
||||
|
||||
/**
|
||||
* @param HorseRace $race 要运行的场次
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly HorseRace $race,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 执行跑马过程并广播进度,最后触发结算。
|
||||
*/
|
||||
public function handle(ChatStateService $chatState): void
|
||||
{
|
||||
$race = $this->race->fresh();
|
||||
|
||||
// 防止重复执行
|
||||
if (! $race || $race->status !== 'betting') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 乐观锁:CAS 改状态为 running
|
||||
$updated = HorseRace::query()
|
||||
->where('id', $race->id)
|
||||
->where('status', 'betting')
|
||||
->update([
|
||||
'status' => 'running',
|
||||
'race_starts_at' => now(),
|
||||
]);
|
||||
|
||||
if (! $updated) {
|
||||
return;
|
||||
}
|
||||
|
||||
$race->refresh();
|
||||
|
||||
// 公屏通知:跑马开始
|
||||
$horseList = implode(' ', array_map(
|
||||
fn ($h) => "{$h['emoji']}{$h['name']}",
|
||||
$race->horses ?? []
|
||||
));
|
||||
$startMsg = [
|
||||
'id' => $chatState->nextMessageId(1),
|
||||
'room_id' => 1,
|
||||
'from_user' => '系统传音',
|
||||
'to_user' => '大家',
|
||||
'content' => "🏇 【赛马】第 #{$race->id} 场押注截止!马匹已进入跑道,比赛开始!参赛阵容:{$horseList}",
|
||||
'is_secret' => false,
|
||||
'font_color' => '#336699',
|
||||
'action' => '大声宣告',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
$chatState->pushMessage(1, $startMsg);
|
||||
broadcast(new MessageSent(1, $startMsg));
|
||||
SaveMessageJob::dispatch($startMsg);
|
||||
|
||||
$config = GameConfig::forGame('horse_racing')?->params ?? [];
|
||||
$raceDuration = (int) ($config['race_duration'] ?? 30);
|
||||
$horses = $race->horses ?? [];
|
||||
$horseCount = count($horses);
|
||||
|
||||
// 初始化各马匹进度(0~100),每步随机增量
|
||||
$positions = array_fill_keys(array_column($horses, 'id'), 0);
|
||||
$speeds = [];
|
||||
foreach ($horses as $horse) {
|
||||
// 基础速度:随机值,确保比赛有悬念(均值接近 race_duration 步完成)
|
||||
$speeds[$horse['id']] = random_int(2, 5);
|
||||
}
|
||||
|
||||
// 跑马循环:模拟进度广播(此 job 为同步阻塞广播,每步 sleep 1 秒)
|
||||
$step = 0;
|
||||
$maxSteps = $raceDuration;
|
||||
$winnerId = null;
|
||||
|
||||
while ($step < $maxSteps && $winnerId === null) {
|
||||
$step++;
|
||||
|
||||
foreach ($horses as $horse) {
|
||||
$horseId = $horse['id'];
|
||||
// 随机冲刺(小概率加速)
|
||||
$boost = random_int(0, 10) >= 9 ? random_int(5, 15) : 0;
|
||||
$positions[$horseId] = min(100, $positions[$horseId] + $speeds[$horseId] + $boost);
|
||||
}
|
||||
|
||||
// 检查是否有马到达终点
|
||||
$finishedHorses = array_filter($positions, fn ($p) => $p >= 100);
|
||||
$finished = ! empty($finishedHorses);
|
||||
|
||||
if ($finished) {
|
||||
// 取进度最高的马为冠军(若并列取 id 最小的)
|
||||
arsort($finishedHorses);
|
||||
$winnerId = (int) array_key_first($finishedHorses);
|
||||
}
|
||||
|
||||
// 广播当前帧进度
|
||||
broadcast(new HorseRaceProgress($race->id, $positions, $finished, $winnerId ?? $this->leadingHorse($positions)));
|
||||
|
||||
if ($finished) {
|
||||
break;
|
||||
}
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
// 如果时间到还没分出胜负,取最高进度的马为赢家
|
||||
if ($winnerId === null) {
|
||||
arsort($positions);
|
||||
$winnerId = (int) array_key_first($positions);
|
||||
}
|
||||
|
||||
// 更新场次记录
|
||||
$race->update([
|
||||
'race_ends_at' => now(),
|
||||
'winner_horse_id' => $winnerId,
|
||||
]);
|
||||
|
||||
// 计算注池统计
|
||||
$totalBets = HorseBet::query()->where('race_id', $race->id)->count();
|
||||
$totalPool = HorseBet::query()->where('race_id', $race->id)->sum('amount');
|
||||
|
||||
$race->update([
|
||||
'total_bets' => $totalBets,
|
||||
'total_pool' => $totalPool,
|
||||
]);
|
||||
|
||||
// 触发结算任务
|
||||
CloseHorseRaceJob::dispatch($race->fresh());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前领跑马匹 ID(进度最高)。
|
||||
*
|
||||
* @param array<int, int> $positions
|
||||
*/
|
||||
private function leadingHorse(array $positions): int
|
||||
{
|
||||
arsort($positions);
|
||||
|
||||
return (int) array_key_first($positions);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user