diff --git a/app/Http/Controllers/HorseRaceController.php b/app/Http/Controllers/HorseRaceController.php index 786d44a..55a0454 100644 --- a/app/Http/Controllers/HorseRaceController.php +++ b/app/Http/Controllers/HorseRaceController.php @@ -40,6 +40,10 @@ class HorseRaceController extends Controller public function currentRace(Request $request): JsonResponse { $user = $request->user(); + if (! $user) { + return response()->json(['message' => '未登录', 'status' => 'error'], 401); + } + $race = HorseRace::currentRace(); if (! $race) { @@ -70,15 +74,16 @@ class HorseRaceController extends Controller $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; + $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; return [ - 'id' => $horse['id'], - 'name' => $horse['name'], - 'emoji' => $horse['emoji'], + 'id' => $horseId, + 'name' => (string) $horse['name'], + 'emoji' => (string) $horse['emoji'], 'pool' => $horsePool, 'odds' => $odds, ]; @@ -150,7 +155,7 @@ class HorseRaceController extends Controller } // 验证马匹 ID 是否有效 - $horses = $race->horses ?? []; + $horses = $this->normalizeRaceHorses($race->horses); $validIds = array_column($horses, 'id'); if (! in_array($data['horse_id'], $validIds, true)) { return response()->json(['ok' => false, 'message' => '无效的马匹编号。']); @@ -178,12 +183,7 @@ class HorseRaceController extends Controller // 找出马匹名称 $horseName = ''; - foreach ($horses as $horse) { - if ((int) $horse['id'] === (int) $data['horse_id']) { - $horseName = ($horse['emoji'] ?? '').($horse['name'] ?? ''); - break; - } - } + $horseName = $this->resolveHorseDisplayName($horses, (int) $data['horse_id']); // 扣除金币 $currency->change( @@ -244,9 +244,9 @@ class HorseRaceController extends Controller // 转换获胜马匹名称 $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'] ?? ''); + foreach ($this->normalizeRaceHorses($race->horses) as $horse) { + if ((int) $horse['id'] === (int) $race->winner_horse_id) { + $winnerName = (string) $horse['emoji'].(string) $horse['name']; break; } } @@ -263,4 +263,56 @@ class HorseRaceController extends Controller return response()->json(['history' => $history]); } + + /** + * 兼容旧赛马数据结构,统一清洗为前端可消费的马匹数组。 + * + * @return array + */ + 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 $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 '🐎未知马匹'; + } } diff --git a/resources/views/chat/partials/games/horse-race-panel.blade.php b/resources/views/chat/partials/games/horse-race-panel.blade.php index 4fb2381..47e482d 100644 --- a/resources/views/chat/partials/games/horse-race-panel.blade.php +++ b/resources/views/chat/partials/games/horse-race-panel.blade.php @@ -419,6 +419,31 @@ // 历史记录 history: [], + /** + * 读取赛马接口 JSON;若后端返回了 HTML/警告页,则抛出可诊断错误。 + * + * @param {string} url 请求地址 + * @param {RequestInit} options fetch 选项 + * @returns {Promise} + */ + async requestJson(url, options = {}) { + const response = await fetch(url, { + headers: { + 'Accept': 'application/json', + ...(options.headers || {}), + }, + ...options, + }); + const rawText = await response.text(); + + try { + return JSON.parse(rawText); + } catch (error) { + const preview = rawText.slice(0, 160).replace(/\s+/g, ' ').trim(); + throw new Error(`赛马接口未返回 JSON(${response.status}): ${preview}`); + } + }, + /** * 同步全局聊天上下文中的金币余额,供弹窗右上角与其他面板共用。 */ @@ -510,8 +535,7 @@ */ async loadCurrentRace() { try { - const res = await fetch('/horse-race/current'); - const data = await res.json(); + const data = await this.requestJson('/horse-race/current'); // 每次打开或刷新当前场次时,都先同步右上角金币余额。 this.syncUserGold(data.jjb); if (data.race) { @@ -653,8 +677,7 @@ */ async loadHistory() { try { - const res = await fetch('/horse-race/history'); - const data = await res.json(); + const data = await this.requestJson('/horse-race/history'); this.history = (data.history || []).reverse(); } catch {} }, @@ -684,8 +707,7 @@ */ async openFromHall() { try { - const res = await fetch('/horse-race/current'); - const data = await res.json(); + const data = await this.requestJson('/horse-race/current'); this.syncUserGold(data.jjb); if (data.race) { const race = data.race; @@ -765,17 +787,15 @@ /** 页面加载时恢复进行中的场次 */ document.addEventListener('DOMContentLoaded', async () => { try { - const histRes = await fetch('/horse-race/history'); - const histData = await histRes.json(); const panel = document.getElementById('horse-race-panel'); + const histData = panel ? await Alpine.$data(panel).requestJson('/horse-race/history') : { history: [] }; const fab = document.getElementById('horse-race-fab'); if (panel) { Alpine.$data(panel).history = (histData.history || []).reverse(); } - const curRes = await fetch('/horse-race/current'); - const curData = await curRes.json(); + const curData = panel ? await Alpine.$data(panel).requestJson('/horse-race/current') : { race: null }; if (panel) { Alpine.$data(panel).syncUserGold(curData.jjb); } diff --git a/tests/Feature/HorseRaceControllerTest.php b/tests/Feature/HorseRaceControllerTest.php index 2aacb0c..609904f 100644 --- a/tests/Feature/HorseRaceControllerTest.php +++ b/tests/Feature/HorseRaceControllerTest.php @@ -307,6 +307,52 @@ class HorseRaceControllerTest extends TestCase $this->assertCount(1, $response->json('history')); } + /** + * 方法功能:验证赛马接口可兼容旧场次中的异常马匹结构,不返回 PHP 警告页。 + */ + public function test_horse_race_endpoints_tolerate_legacy_horse_payloads(): void + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 3456]); + + HorseRace::create([ + 'status' => 'settled', + 'bet_opens_at' => now()->subMinutes(3), + 'bet_closes_at' => now()->subMinutes(2), + 'settled_at' => now()->subMinute(), + 'winner_horse_id' => 2, + 'horses' => [ + ['name' => '旧赤兔'], + 'legacy-string-entry', + ['id' => 2, 'emoji' => '⚡'], + ], + 'total_bets' => 1, + 'total_pool' => 200, + ]); + + HorseRace::create([ + 'status' => 'betting', + 'bet_opens_at' => now()->subSeconds(10), + 'bet_closes_at' => now()->addMinute(), + 'horses' => [ + ['name' => '缺编号旧马'], + ['id' => 2, 'emoji' => '🏇'], + null, + ], + 'total_bets' => 0, + 'total_pool' => 0, + ]); + + $currentResponse = $this->actingAs($user)->getJson(route('horse-race.current')); + $historyResponse = $this->actingAs($user)->getJson(route('horse-race.history')); + + $currentResponse->assertOk(); + $historyResponse->assertOk(); + $this->assertSame('缺编号旧马', $currentResponse->json('race.horses.0.name')); + $this->assertSame('🐎', $currentResponse->json('race.horses.0.emoji')); + $this->assertSame('⚡未知马匹', $historyResponse->json('history.0.winner_name')); + } + /** * 方法功能:验证单个赢家至少拿回本金,并可获得种子池补贴。 */