- 移除聊天室右下角浮动游戏图标(占卜、百家乐、赛马、老虎机) - 用户名片按钮区:修复已婚/已好友时按钮换行问题,统一单行显示 - 婚礼红包弹窗:重设计为喜庆鲜红背景,领取按钮改为圆形米黄样式 - 新增婚礼红包恢复接口(/wedding/pending-envelopes),刷新后自动恢复领取按钮 - 修复 Alpine :style 字符串覆盖静态 style 导致圆形按钮失效的问题 - 撤职后用户等级改为根据经验值重新计算,不再无条件重置为1 - 管理员修改用户经验值后自动重算等级,有职务用户等级锁定 - 娱乐大厅钓鱼游戏按钮直接调用 startFishing() 简化操作流程 - 新增赛马、占卜、百家乐游戏及相关后端逻辑
225 lines
7.5 KiB
PHP
225 lines
7.5 KiB
PHP
<?php
|
||
|
||
/**
|
||
* 文件功能:赛马竞猜前台控制器
|
||
*
|
||
* 提供用户在聊天室内参与赛马的 API 接口:
|
||
* - 查询当前场次信息(马匹、注池、赔率)
|
||
* - 提交下注(扣除金币 + 写入下注记录)
|
||
* - 查询本人下注状态
|
||
* - 查询最近历史记录
|
||
*
|
||
* @author ChatRoom Laravel
|
||
*
|
||
* @version 1.0.0
|
||
*/
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Enums\CurrencySource;
|
||
use App\Models\GameConfig;
|
||
use App\Models\HorseBet;
|
||
use App\Models\HorseRace;
|
||
use App\Services\UserCurrencyService;
|
||
use Illuminate\Http\JsonResponse;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
class HorseRaceController extends Controller
|
||
{
|
||
public function __construct(
|
||
private readonly UserCurrencyService $currency,
|
||
) {}
|
||
|
||
/**
|
||
* 获取当前进行中的场次信息(前端轮询或事件触发后调用)。
|
||
*/
|
||
public function currentRace(Request $request): JsonResponse
|
||
{
|
||
$race = HorseRace::currentRace();
|
||
|
||
if (! $race) {
|
||
return response()->json(['race' => null]);
|
||
}
|
||
|
||
$user = $request->user();
|
||
$myBet = HorseBet::query()
|
||
->where('race_id', $race->id)
|
||
->where('user_id', $user->id)
|
||
->first();
|
||
|
||
// 计算各马匹当前注额
|
||
$config = GameConfig::forGame('horse_racing')?->params ?? [];
|
||
$houseTake = (int) ($config['house_take_percent'] ?? 5);
|
||
|
||
$horsePools = HorseBet::query()
|
||
->where('race_id', $race->id)
|
||
->groupBy('horse_id')
|
||
->selectRaw('horse_id, SUM(amount) as pool')
|
||
->pluck('pool', 'horse_id')
|
||
->toArray();
|
||
|
||
// 计算实时赔率
|
||
$horses = $race->horses ?? [];
|
||
$horsesWithBets = array_map(function ($horse) use ($horsePools, $houseTake) {
|
||
$horsePool = (int) ($horsePools[$horse['id']] ?? 0);
|
||
$totalPool = array_sum(array_values($horsePools));
|
||
$netPool = $totalPool * (1 - $houseTake / 100);
|
||
$odds = $horsePool > 0 ? round($netPool / $horsePool, 2) : null;
|
||
|
||
return [
|
||
'id' => $horse['id'],
|
||
'name' => $horse['name'],
|
||
'emoji' => $horse['emoji'],
|
||
'pool' => $horsePool,
|
||
'odds' => $odds,
|
||
];
|
||
}, $horses);
|
||
|
||
return response()->json([
|
||
'race' => [
|
||
'id' => $race->id,
|
||
'status' => $race->status,
|
||
'bet_closes_at' => $race->bet_closes_at?->toIso8601String(),
|
||
'seconds_left' => $race->status === 'betting'
|
||
? max(0, (int) now()->diffInSeconds($race->bet_closes_at, false))
|
||
: 0,
|
||
'horses' => $horsesWithBets,
|
||
'total_pool' => $race->total_pool + array_sum(array_values($horsePools)),
|
||
'my_bet' => $myBet ? [
|
||
'horse_id' => $myBet->horse_id,
|
||
'amount' => $myBet->amount,
|
||
] : null,
|
||
],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 用户提交下注。
|
||
*
|
||
* 同一场每人限下一注,下注成功后立即扣除金币。
|
||
*/
|
||
public function bet(Request $request): JsonResponse
|
||
{
|
||
if (! GameConfig::isEnabled('horse_racing')) {
|
||
return response()->json(['ok' => false, 'message' => '赛马竞猜当前未开启。']);
|
||
}
|
||
|
||
$data = $request->validate([
|
||
'race_id' => 'required|integer|exists:horse_races,id',
|
||
'horse_id' => 'required|integer|min:1',
|
||
'amount' => 'required|integer|min:1',
|
||
]);
|
||
|
||
$config = GameConfig::forGame('horse_racing')?->params ?? [];
|
||
$minBet = (int) ($config['min_bet'] ?? 100);
|
||
$maxBet = (int) ($config['max_bet'] ?? 100000);
|
||
|
||
if ($data['amount'] < $minBet || $data['amount'] > $maxBet) {
|
||
return response()->json(['ok' => false, 'message' => "押注金额须在 {$minBet}~{$maxBet} 金币之间。"]);
|
||
}
|
||
|
||
$race = HorseRace::find($data['race_id']);
|
||
|
||
if (! $race || ! $race->isBettingOpen()) {
|
||
return response()->json(['ok' => false, 'message' => '当前不在下注时间内。']);
|
||
}
|
||
|
||
// 验证马匹 ID 是否有效
|
||
$horses = $race->horses ?? [];
|
||
$validIds = array_column($horses, 'id');
|
||
if (! in_array($data['horse_id'], $validIds, true)) {
|
||
return response()->json(['ok' => false, 'message' => '无效的马匹编号。']);
|
||
}
|
||
|
||
$user = $request->user();
|
||
$currency = $this->currency;
|
||
|
||
// 校验余额
|
||
if (($user->jjb ?? 0) < $data['amount']) {
|
||
return response()->json(['ok' => false, 'message' => '金币不足,无法下注。']);
|
||
}
|
||
|
||
return DB::transaction(function () use ($user, $race, $data, $currency, $horses): JsonResponse {
|
||
// 幂等:同一场只能下一注
|
||
$existing = HorseBet::query()
|
||
->where('race_id', $race->id)
|
||
->where('user_id', $user->id)
|
||
->lockForUpdate()
|
||
->exists();
|
||
|
||
if ($existing) {
|
||
return response()->json(['ok' => false, 'message' => '本场您已下注,请等待开奖。']);
|
||
}
|
||
|
||
// 找出马匹名称
|
||
$horseName = '';
|
||
foreach ($horses as $horse) {
|
||
if ((int) $horse['id'] === (int) $data['horse_id']) {
|
||
$horseName = ($horse['emoji'] ?? '').($horse['name'] ?? '');
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 扣除金币
|
||
$currency->change(
|
||
$user,
|
||
'gold',
|
||
-$data['amount'],
|
||
CurrencySource::HORSE_BET,
|
||
"赛马 #{$race->id} 押注 {$horseName}",
|
||
);
|
||
|
||
// 写入下注记录
|
||
HorseBet::create([
|
||
'race_id' => $race->id,
|
||
'user_id' => $user->id,
|
||
'horse_id' => $data['horse_id'],
|
||
'amount' => $data['amount'],
|
||
'status' => 'pending',
|
||
]);
|
||
|
||
return response()->json([
|
||
'ok' => true,
|
||
'message' => "✅ 已押注「{$horseName}」{$data['amount']} 金币,等待开跑!",
|
||
'amount' => $data['amount'],
|
||
'horse_id' => $data['horse_id'],
|
||
]);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 查询最近10场历史记录(前端展示胜负趋势)。
|
||
*/
|
||
public function history(): JsonResponse
|
||
{
|
||
$races = HorseRace::query()
|
||
->where('status', 'settled')
|
||
->orderByDesc('id')
|
||
->limit(10)
|
||
->get(['id', 'horses', 'winner_horse_id', 'total_pool', 'total_bets', 'settled_at']);
|
||
|
||
// 转换获胜马匹名称
|
||
$history = $races->map(function ($race) {
|
||
$winnerName = '未知';
|
||
foreach (($race->horses ?? []) as $horse) {
|
||
if (($horse['id'] ?? 0) === (int) $race->winner_horse_id) {
|
||
$winnerName = ($horse['emoji'] ?? '').($horse['name'] ?? '');
|
||
break;
|
||
}
|
||
}
|
||
|
||
return [
|
||
'id' => $race->id,
|
||
'winner_id' => $race->winner_horse_id,
|
||
'winner_name' => $winnerName,
|
||
'total_pool' => $race->total_pool,
|
||
'total_bets' => $race->total_bets,
|
||
'settled_at' => $race->settled_at?->toDateTimeString(),
|
||
];
|
||
});
|
||
|
||
return response()->json(['history' => $history]);
|
||
}
|
||
}
|