2026-03-03 23:19:59 +08:00
|
|
|
<?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;
|
2026-04-30 15:41:50 +08:00
|
|
|
use App\Services\GameBetBroadcastService;
|
2026-04-29 14:37:28 +08:00
|
|
|
use App\Services\GameRoomScopeService;
|
2026-03-03 23:19:59 +08:00
|
|
|
use App\Services\UserCurrencyService;
|
|
|
|
|
use Illuminate\Http\JsonResponse;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
2026-04-29 11:42:49 +08:00
|
|
|
/**
|
|
|
|
|
* 类功能:赛马竞猜前台控制器
|
|
|
|
|
*
|
|
|
|
|
* 负责聊天室赛马玩法的当前场次查询、下注提交、历史记录读取,
|
|
|
|
|
* 并在发现线上遗留的超时 running 场次时执行最小范围的状态自愈。
|
|
|
|
|
*/
|
2026-03-03 23:19:59 +08:00
|
|
|
class HorseRaceController extends Controller
|
|
|
|
|
{
|
|
|
|
|
public function __construct(
|
|
|
|
|
private readonly UserCurrencyService $currency,
|
2026-04-29 14:37:28 +08:00
|
|
|
private readonly GameRoomScopeService $roomScopeService,
|
2026-04-30 15:41:50 +08:00
|
|
|
private readonly GameBetBroadcastService $betBroadcastService,
|
2026-03-03 23:19:59 +08:00
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取当前进行中的场次信息(前端轮询或事件触发后调用)。
|
|
|
|
|
*/
|
|
|
|
|
public function currentRace(Request $request): JsonResponse
|
|
|
|
|
{
|
2026-04-12 22:31:35 +08:00
|
|
|
$user = $request->user();
|
2026-04-24 21:18:09 +08:00
|
|
|
if (! $user) {
|
|
|
|
|
return response()->json(['message' => '未登录', 'status' => 'error'], 401);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:37:28 +08:00
|
|
|
$roomId = $this->roomScopeService->resolveRequestRoomId($request, $user);
|
|
|
|
|
if (! $this->roomScopeService->isRoomAllowedForGame('horse_racing', $roomId)) {
|
|
|
|
|
return response()->json(['race' => null, 'jjb' => (int) ($user->jjb ?? 0)]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$race = $this->resolveCurrentRaceState(HorseRace::currentRace($roomId));
|
2026-03-03 23:19:59 +08:00
|
|
|
|
|
|
|
|
if (! $race) {
|
2026-04-12 22:31:35 +08:00
|
|
|
return response()->json([
|
|
|
|
|
'race' => null,
|
|
|
|
|
// 即使当前无赛马场次,也返回最新金币余额,供前端打开弹窗时刷新显示。
|
|
|
|
|
'jjb' => (int) ($user->jjb ?? 0),
|
|
|
|
|
]);
|
2026-03-03 23:19:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$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);
|
2026-04-11 16:11:00 +08:00
|
|
|
$seedPool = (int) ($config['seed_pool'] ?? 0);
|
2026-03-03 23:19:59 +08:00
|
|
|
|
|
|
|
|
$horsePools = HorseBet::query()
|
|
|
|
|
->where('race_id', $race->id)
|
|
|
|
|
->groupBy('horse_id')
|
|
|
|
|
->selectRaw('horse_id, SUM(amount) as pool')
|
|
|
|
|
->pluck('pool', 'horse_id')
|
|
|
|
|
->toArray();
|
|
|
|
|
|
2026-04-11 16:11:00 +08:00
|
|
|
$oddsMap = HorseRace::calcOdds($horsePools, $houseTake, $seedPool);
|
|
|
|
|
|
2026-03-03 23:19:59 +08:00
|
|
|
// 计算实时赔率
|
2026-04-24 21:18:09 +08:00
|
|
|
$horses = $this->normalizeRaceHorses($race->horses);
|
|
|
|
|
$horsesWithBets = array_map(function (array $horse) use ($horsePools, $oddsMap) {
|
|
|
|
|
$horseId = (int) $horse['id'];
|
|
|
|
|
$horsePool = (int) ($horsePools[$horseId] ?? 0);
|
|
|
|
|
$odds = $horsePool > 0 ? ($oddsMap[$horseId] ?? null) : null;
|
2026-03-03 23:19:59 +08:00
|
|
|
|
|
|
|
|
return [
|
2026-04-24 21:18:09 +08:00
|
|
|
'id' => $horseId,
|
|
|
|
|
'name' => (string) $horse['name'],
|
|
|
|
|
'emoji' => (string) $horse['emoji'],
|
2026-03-03 23:19:59 +08:00
|
|
|
'pool' => $horsePool,
|
|
|
|
|
'odds' => $odds,
|
|
|
|
|
];
|
|
|
|
|
}, $horses);
|
|
|
|
|
|
2026-04-12 11:09:15 +08:00
|
|
|
// 押注阶段实时总池 = 当前记录的基础池(通常为种子池)+ 实时下注总额;
|
|
|
|
|
// 跑马/结算阶段 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;
|
|
|
|
|
|
2026-04-12 17:56:16 +08:00
|
|
|
$minBet = (int) ($config['min_bet'] ?? 100);
|
|
|
|
|
$maxBet = (int) ($config['max_bet'] ?? 100000);
|
|
|
|
|
|
2026-03-03 23:19:59 +08:00
|
|
|
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,
|
2026-04-12 11:09:15 +08:00
|
|
|
'total_pool' => $displayTotalPool,
|
2026-04-12 17:56:16 +08:00
|
|
|
'min_bet' => $minBet,
|
|
|
|
|
'max_bet' => $maxBet,
|
2026-03-03 23:19:59 +08:00
|
|
|
'my_bet' => $myBet ? [
|
|
|
|
|
'horse_id' => $myBet->horse_id,
|
|
|
|
|
'amount' => $myBet->amount,
|
|
|
|
|
] : null,
|
|
|
|
|
],
|
2026-04-12 22:31:35 +08:00
|
|
|
// 返回当前用户最新金币,确保弹窗右上角余额每次打开都以服务端最新值为准。
|
|
|
|
|
'jjb' => (int) ($user->jjb ?? 0),
|
2026-03-03 23:19:59 +08:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 用户提交下注。
|
|
|
|
|
*
|
|
|
|
|
* 同一场每人限下一注,下注成功后立即扣除金币。
|
|
|
|
|
*/
|
|
|
|
|
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',
|
|
|
|
|
]);
|
2026-04-29 14:37:28 +08:00
|
|
|
$roomId = $this->roomScopeService->resolveRequestRoomId($request, $request->user());
|
|
|
|
|
|
|
|
|
|
if (! $this->roomScopeService->isRoomAllowedForGame('horse_racing', $roomId)) {
|
|
|
|
|
return response()->json(['ok' => false, 'message' => '当前房间未开启赛马竞猜。'], 403);
|
|
|
|
|
}
|
2026-03-03 23:19:59 +08:00
|
|
|
|
|
|
|
|
$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']);
|
|
|
|
|
|
2026-04-29 14:37:28 +08:00
|
|
|
if (! $race || (int) $race->room_id !== $roomId || ! $race->isBettingOpen()) {
|
2026-03-03 23:19:59 +08:00
|
|
|
return response()->json(['ok' => false, 'message' => '当前不在下注时间内。']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证马匹 ID 是否有效
|
2026-04-24 21:18:09 +08:00
|
|
|
$horses = $this->normalizeRaceHorses($race->horses);
|
2026-03-03 23:19:59 +08:00
|
|
|
$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 = '';
|
2026-04-24 21:18:09 +08:00
|
|
|
$horseName = $this->resolveHorseDisplayName($horses, (int) $data['horse_id']);
|
2026-03-03 23:19:59 +08:00
|
|
|
|
|
|
|
|
// 扣除金币
|
|
|
|
|
$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',
|
|
|
|
|
]);
|
|
|
|
|
|
2026-04-30 15:41:50 +08:00
|
|
|
$this->betBroadcastService->horseRace((int) $race->room_id, $user->username, (int) $data['amount'], $horseName);
|
2026-04-11 16:58:28 +08:00
|
|
|
|
2026-03-03 23:19:59 +08:00
|
|
|
return response()->json([
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'message' => "✅ 已押注「{$horseName}」{$data['amount']} 金币,等待开跑!",
|
|
|
|
|
'amount' => $data['amount'],
|
|
|
|
|
'horse_id' => $data['horse_id'],
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 查询最近10场历史记录(前端展示胜负趋势)。
|
|
|
|
|
*/
|
|
|
|
|
public function history(): JsonResponse
|
|
|
|
|
{
|
2026-04-29 14:37:28 +08:00
|
|
|
$roomId = $this->roomScopeService->resolveUserRoomId(auth()->user());
|
2026-03-03 23:19:59 +08:00
|
|
|
$races = HorseRace::query()
|
2026-04-29 14:37:28 +08:00
|
|
|
->where('room_id', $roomId)
|
2026-03-03 23:19:59 +08:00
|
|
|
->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 = '未知';
|
2026-04-24 21:18:09 +08:00
|
|
|
foreach ($this->normalizeRaceHorses($race->horses) as $horse) {
|
|
|
|
|
if ((int) $horse['id'] === (int) $race->winner_horse_id) {
|
|
|
|
|
$winnerName = (string) $horse['emoji'].(string) $horse['name'];
|
2026-03-03 23:19:59 +08:00
|
|
|
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]);
|
|
|
|
|
}
|
2026-04-24 21:18:09 +08:00
|
|
|
|
2026-04-29 11:42:49 +08:00
|
|
|
/**
|
|
|
|
|
* 自愈当前场次状态,避免线上遗漏结算时长期卡在 running。
|
|
|
|
|
*/
|
|
|
|
|
private function resolveCurrentRaceState(?HorseRace $race): ?HorseRace
|
|
|
|
|
{
|
|
|
|
|
if (! $race || $race->status !== 'running') {
|
|
|
|
|
return $race;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! $this->shouldRecoverStaleRunningRace($race)) {
|
|
|
|
|
return $race;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$race = $this->prepareRunningRaceForSettlement($race);
|
|
|
|
|
if (! $race || $race->status !== 'running' || ! $race->winner_horse_id) {
|
|
|
|
|
return $race;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 线上若漏消费 CloseHorseRaceJob,这里同步补做一次结算,避免界面一直显示“跑马中”。
|
|
|
|
|
app()->call([new \App\Jobs\CloseHorseRaceJob($race), 'handle']);
|
|
|
|
|
|
2026-04-29 14:37:28 +08:00
|
|
|
return HorseRace::currentRace((int) $race->room_id);
|
2026-04-29 11:42:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 判断 running 场次是否已经超过合理比赛时长,需要请求侧补偿收尾。
|
|
|
|
|
*/
|
|
|
|
|
private function shouldRecoverStaleRunningRace(HorseRace $race): bool
|
|
|
|
|
{
|
|
|
|
|
if (! $race->race_starts_at) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$config = GameConfig::forGame('horse_racing')?->params ?? [];
|
|
|
|
|
$raceDuration = max(1, (int) ($config['race_duration'] ?? 30));
|
|
|
|
|
$recoveryGraceSeconds = 5;
|
|
|
|
|
|
|
|
|
|
return $race->race_starts_at->lte(now()->subSeconds($raceDuration + $recoveryGraceSeconds));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 为超时 running 场次补齐缺失赛果字段,确保后续结算任务可以安全执行。
|
|
|
|
|
*/
|
|
|
|
|
private function prepareRunningRaceForSettlement(HorseRace $race): ?HorseRace
|
|
|
|
|
{
|
|
|
|
|
if ($race->winner_horse_id && $race->race_ends_at) {
|
|
|
|
|
return $race->fresh();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$horses = $this->normalizeRaceHorses($race->horses);
|
|
|
|
|
$winnerHorseId = $race->winner_horse_id ?: $this->resolveStaleRunningWinnerId($race, $horses);
|
|
|
|
|
if (! $winnerHorseId) {
|
|
|
|
|
return $race;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$config = GameConfig::forGame('horse_racing')?->params ?? [];
|
|
|
|
|
$seedPool = (int) ($config['seed_pool'] ?? 0);
|
|
|
|
|
|
|
|
|
|
// 线上补偿场景下以当前下注快照补齐统计,确保本次请求内的结算口径与正常流程一致。
|
|
|
|
|
$totalBets = HorseBet::query()->where('race_id', $race->id)->count();
|
|
|
|
|
$totalPool = $seedPool + (int) HorseBet::query()->where('race_id', $race->id)->sum('amount');
|
|
|
|
|
|
|
|
|
|
HorseRace::query()
|
|
|
|
|
->where('id', $race->id)
|
|
|
|
|
->where('status', 'running')
|
|
|
|
|
->update([
|
|
|
|
|
'winner_horse_id' => $winnerHorseId,
|
|
|
|
|
'race_ends_at' => $race->race_ends_at ?? now(),
|
|
|
|
|
'total_bets' => $totalBets,
|
|
|
|
|
'total_pool' => $totalPool,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return $race->fresh();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 为异常滞留的 running 场次推导一个稳定冠军,避免多次请求得到不同结算结果。
|
|
|
|
|
*
|
|
|
|
|
* @param array<int, array{id:int,name:string,emoji:string}> $horses
|
|
|
|
|
*/
|
|
|
|
|
private function resolveStaleRunningWinnerId(HorseRace $race, array $horses): ?int
|
|
|
|
|
{
|
|
|
|
|
if ($horses === []) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$horsePools = HorseBet::query()
|
|
|
|
|
->where('race_id', $race->id)
|
|
|
|
|
->groupBy('horse_id')
|
|
|
|
|
->selectRaw('horse_id, SUM(amount) as pool')
|
|
|
|
|
->pluck('pool', 'horse_id')
|
|
|
|
|
->map(fn ($pool) => (int) $pool)
|
|
|
|
|
->toArray();
|
|
|
|
|
|
|
|
|
|
$candidateIds = array_map(
|
|
|
|
|
fn (array $horse): int => (int) $horse['id'],
|
|
|
|
|
$horses,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
usort($candidateIds, function (int $leftId, int $rightId) use ($horsePools): int {
|
|
|
|
|
$leftPool = (int) ($horsePools[$leftId] ?? 0);
|
|
|
|
|
$rightPool = (int) ($horsePools[$rightId] ?? 0);
|
|
|
|
|
|
|
|
|
|
if ($leftPool === $rightPool) {
|
|
|
|
|
return $leftId <=> $rightId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $rightPool <=> $leftPool;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return $candidateIds[0] ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 21:18:09 +08:00
|
|
|
/**
|
|
|
|
|
* 兼容旧赛马数据结构,统一清洗为前端可消费的马匹数组。
|
|
|
|
|
*
|
|
|
|
|
* @return array<int, array{id:int,name:string,emoji:string}>
|
|
|
|
|
*/
|
|
|
|
|
private function normalizeRaceHorses(mixed $horses): array
|
|
|
|
|
{
|
|
|
|
|
if (! is_array($horses)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$normalizedHorses = [];
|
|
|
|
|
|
|
|
|
|
foreach ($horses as $index => $horse) {
|
|
|
|
|
if (! is_array($horse)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$horseId = isset($horse['id']) && is_numeric($horse['id'])
|
|
|
|
|
? (int) $horse['id']
|
|
|
|
|
: $index + 1;
|
|
|
|
|
$horseName = trim((string) ($horse['name'] ?? ''));
|
|
|
|
|
if ($horseName === '') {
|
|
|
|
|
$horseName = '未知马匹';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$normalizedHorses[] = [
|
|
|
|
|
'id' => $horseId,
|
|
|
|
|
'name' => $horseName,
|
|
|
|
|
'emoji' => (string) ($horse['emoji'] ?? '🐎'),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $normalizedHorses;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 根据马匹编号返回展示名称,供系统播报与下注回执共用。
|
|
|
|
|
*
|
|
|
|
|
* @param array<int, array{id:int,name:string,emoji:string}> $horses
|
|
|
|
|
*/
|
|
|
|
|
private function resolveHorseDisplayName(array $horses, int $horseId): string
|
|
|
|
|
{
|
|
|
|
|
foreach ($horses as $horse) {
|
|
|
|
|
if ((int) ($horse['id'] ?? 0) === $horseId) {
|
|
|
|
|
return (string) ($horse['emoji'] ?? '🐎').(string) ($horse['name'] ?? '未知马匹');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return '🐎未知马匹';
|
|
|
|
|
}
|
2026-03-03 23:19:59 +08:00
|
|
|
}
|