完善跑马面板与控制器逻辑
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法功能:验证单个赢家至少拿回本金,并可获得种子池补贴。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user