完善跑马面板与控制器逻辑

This commit is contained in:
2026-04-24 21:18:09 +08:00
parent 0f0bfef2a8
commit 34356a26ae
3 changed files with 145 additions and 27 deletions
+69 -17
View File
@@ -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<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 '🐎未知马匹';
}
}
@@ -419,6 +419,31 @@
// 历史记录
history: [],
/**
* 读取赛马接口 JSON;若后端返回了 HTML/警告页,则抛出可诊断错误。
*
* @param {string} url 请求地址
* @param {RequestInit} options fetch 选项
* @returns {Promise<any>}
*/
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);
}
+46
View File
@@ -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'));
}
/**
* 方法功能:验证单个赢家至少拿回本金,并可获得种子池补贴。
*/