261 lines
9.0 KiB
PHP
261 lines
9.0 KiB
PHP
<?php
|
||
|
||
/**
|
||
* 文件功能:赛马竞猜前台控制器
|
||
*
|
||
* 提供用户在聊天室内参与赛马的 API 接口:
|
||
* - 查询当前场次信息(马匹、注池、赔率)
|
||
* - 提交下注(扣除金币 + 写入下注记录)
|
||
* - 查询本人下注状态
|
||
* - 查询最近历史记录
|
||
*
|
||
* @author ChatRoom Laravel
|
||
*
|
||
* @version 1.0.0
|
||
*/
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Enums\CurrencySource;
|
||
use App\Events\MessageSent;
|
||
use App\Jobs\SaveMessageJob;
|
||
use App\Models\GameConfig;
|
||
use App\Models\HorseBet;
|
||
use App\Models\HorseRace;
|
||
use App\Services\ChatStateService;
|
||
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);
|
||
$seedPool = (int) ($config['seed_pool'] ?? 0);
|
||
|
||
$horsePools = HorseBet::query()
|
||
->where('race_id', $race->id)
|
||
->groupBy('horse_id')
|
||
->selectRaw('horse_id, SUM(amount) as pool')
|
||
->pluck('pool', 'horse_id')
|
||
->toArray();
|
||
|
||
$oddsMap = HorseRace::calcOdds($horsePools, $houseTake, $seedPool);
|
||
|
||
// 计算实时赔率
|
||
$horses = $race->horses ?? [];
|
||
$horsesWithBets = array_map(function ($horse) use ($horsePools, $oddsMap) {
|
||
$horsePool = (int) ($horsePools[$horse['id']] ?? 0);
|
||
$odds = $horsePool > 0 ? ($oddsMap[$horse['id']] ?? null) : null;
|
||
|
||
return [
|
||
'id' => $horse['id'],
|
||
'name' => $horse['name'],
|
||
'emoji' => $horse['emoji'],
|
||
'pool' => $horsePool,
|
||
'odds' => $odds,
|
||
];
|
||
}, $horses);
|
||
|
||
// 押注阶段实时总池 = 当前记录的基础池(通常为种子池)+ 实时下注总额;
|
||
// 跑马/结算阶段 total_pool 已写回最终值,不能再重复叠加下注额。
|
||
$basePool = $race->status === 'betting'
|
||
? max((int) $race->total_pool, $seedPool)
|
||
: (int) $race->total_pool;
|
||
$displayTotalPool = $race->status === 'betting'
|
||
? $basePool + array_sum(array_values($horsePools))
|
||
: $basePool;
|
||
|
||
$minBet = (int) ($config['min_bet'] ?? 100);
|
||
$maxBet = (int) ($config['max_bet'] ?? 100000);
|
||
|
||
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' => $displayTotalPool,
|
||
'min_bet' => $minBet,
|
||
'max_bet' => $maxBet,
|
||
'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',
|
||
]);
|
||
|
||
$chatState = app(ChatStateService::class);
|
||
$formattedAmount = number_format($data['amount']);
|
||
$content = "🌟 🐎 <b>{$user->username}</b> 押注了 <b>{$formattedAmount}</b> 金币({$horseName})!✨";
|
||
$msg = [
|
||
'id' => $chatState->nextMessageId(1),
|
||
'room_id' => 1,
|
||
'from_user' => '系统传音',
|
||
'to_user' => '大家',
|
||
'content' => $content,
|
||
'is_secret' => false,
|
||
'font_color' => '#d97706',
|
||
'action' => '',
|
||
'sent_at' => now()->toDateTimeString(),
|
||
];
|
||
$chatState->pushMessage(1, $msg);
|
||
event(new MessageSent(1, $msg));
|
||
SaveMessageJob::dispatch($msg);
|
||
|
||
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]);
|
||
}
|
||
}
|