完善跑马面板与控制器逻辑
This commit is contained in:
@@ -40,6 +40,10 @@ class HorseRaceController extends Controller
|
|||||||
public function currentRace(Request $request): JsonResponse
|
public function currentRace(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
if (! $user) {
|
||||||
|
return response()->json(['message' => '未登录', 'status' => 'error'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
$race = HorseRace::currentRace();
|
$race = HorseRace::currentRace();
|
||||||
|
|
||||||
if (! $race) {
|
if (! $race) {
|
||||||
@@ -70,15 +74,16 @@ class HorseRaceController extends Controller
|
|||||||
$oddsMap = HorseRace::calcOdds($horsePools, $houseTake, $seedPool);
|
$oddsMap = HorseRace::calcOdds($horsePools, $houseTake, $seedPool);
|
||||||
|
|
||||||
// 计算实时赔率
|
// 计算实时赔率
|
||||||
$horses = $race->horses ?? [];
|
$horses = $this->normalizeRaceHorses($race->horses);
|
||||||
$horsesWithBets = array_map(function ($horse) use ($horsePools, $oddsMap) {
|
$horsesWithBets = array_map(function (array $horse) use ($horsePools, $oddsMap) {
|
||||||
$horsePool = (int) ($horsePools[$horse['id']] ?? 0);
|
$horseId = (int) $horse['id'];
|
||||||
$odds = $horsePool > 0 ? ($oddsMap[$horse['id']] ?? null) : null;
|
$horsePool = (int) ($horsePools[$horseId] ?? 0);
|
||||||
|
$odds = $horsePool > 0 ? ($oddsMap[$horseId] ?? null) : null;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $horse['id'],
|
'id' => $horseId,
|
||||||
'name' => $horse['name'],
|
'name' => (string) $horse['name'],
|
||||||
'emoji' => $horse['emoji'],
|
'emoji' => (string) $horse['emoji'],
|
||||||
'pool' => $horsePool,
|
'pool' => $horsePool,
|
||||||
'odds' => $odds,
|
'odds' => $odds,
|
||||||
];
|
];
|
||||||
@@ -150,7 +155,7 @@ class HorseRaceController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证马匹 ID 是否有效
|
// 验证马匹 ID 是否有效
|
||||||
$horses = $race->horses ?? [];
|
$horses = $this->normalizeRaceHorses($race->horses);
|
||||||
$validIds = array_column($horses, 'id');
|
$validIds = array_column($horses, 'id');
|
||||||
if (! in_array($data['horse_id'], $validIds, true)) {
|
if (! in_array($data['horse_id'], $validIds, true)) {
|
||||||
return response()->json(['ok' => false, 'message' => '无效的马匹编号。']);
|
return response()->json(['ok' => false, 'message' => '无效的马匹编号。']);
|
||||||
@@ -178,12 +183,7 @@ class HorseRaceController extends Controller
|
|||||||
|
|
||||||
// 找出马匹名称
|
// 找出马匹名称
|
||||||
$horseName = '';
|
$horseName = '';
|
||||||
foreach ($horses as $horse) {
|
$horseName = $this->resolveHorseDisplayName($horses, (int) $data['horse_id']);
|
||||||
if ((int) $horse['id'] === (int) $data['horse_id']) {
|
|
||||||
$horseName = ($horse['emoji'] ?? '').($horse['name'] ?? '');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扣除金币
|
// 扣除金币
|
||||||
$currency->change(
|
$currency->change(
|
||||||
@@ -244,9 +244,9 @@ class HorseRaceController extends Controller
|
|||||||
// 转换获胜马匹名称
|
// 转换获胜马匹名称
|
||||||
$history = $races->map(function ($race) {
|
$history = $races->map(function ($race) {
|
||||||
$winnerName = '未知';
|
$winnerName = '未知';
|
||||||
foreach (($race->horses ?? []) as $horse) {
|
foreach ($this->normalizeRaceHorses($race->horses) as $horse) {
|
||||||
if (($horse['id'] ?? 0) === (int) $race->winner_horse_id) {
|
if ((int) $horse['id'] === (int) $race->winner_horse_id) {
|
||||||
$winnerName = ($horse['emoji'] ?? '').($horse['name'] ?? '');
|
$winnerName = (string) $horse['emoji'].(string) $horse['name'];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,4 +263,56 @@ class HorseRaceController extends Controller
|
|||||||
|
|
||||||
return response()->json(['history' => $history]);
|
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: [],
|
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() {
|
async loadCurrentRace() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/horse-race/current');
|
const data = await this.requestJson('/horse-race/current');
|
||||||
const data = await res.json();
|
|
||||||
// 每次打开或刷新当前场次时,都先同步右上角金币余额。
|
// 每次打开或刷新当前场次时,都先同步右上角金币余额。
|
||||||
this.syncUserGold(data.jjb);
|
this.syncUserGold(data.jjb);
|
||||||
if (data.race) {
|
if (data.race) {
|
||||||
@@ -653,8 +677,7 @@
|
|||||||
*/
|
*/
|
||||||
async loadHistory() {
|
async loadHistory() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/horse-race/history');
|
const data = await this.requestJson('/horse-race/history');
|
||||||
const data = await res.json();
|
|
||||||
this.history = (data.history || []).reverse();
|
this.history = (data.history || []).reverse();
|
||||||
} catch {}
|
} catch {}
|
||||||
},
|
},
|
||||||
@@ -684,8 +707,7 @@
|
|||||||
*/
|
*/
|
||||||
async openFromHall() {
|
async openFromHall() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/horse-race/current');
|
const data = await this.requestJson('/horse-race/current');
|
||||||
const data = await res.json();
|
|
||||||
this.syncUserGold(data.jjb);
|
this.syncUserGold(data.jjb);
|
||||||
if (data.race) {
|
if (data.race) {
|
||||||
const race = data.race;
|
const race = data.race;
|
||||||
@@ -765,17 +787,15 @@
|
|||||||
/** 页面加载时恢复进行中的场次 */
|
/** 页面加载时恢复进行中的场次 */
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
try {
|
try {
|
||||||
const histRes = await fetch('/horse-race/history');
|
|
||||||
const histData = await histRes.json();
|
|
||||||
const panel = document.getElementById('horse-race-panel');
|
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');
|
const fab = document.getElementById('horse-race-fab');
|
||||||
|
|
||||||
if (panel) {
|
if (panel) {
|
||||||
Alpine.$data(panel).history = (histData.history || []).reverse();
|
Alpine.$data(panel).history = (histData.history || []).reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
const curRes = await fetch('/horse-race/current');
|
const curData = panel ? await Alpine.$data(panel).requestJson('/horse-race/current') : { race: null };
|
||||||
const curData = await curRes.json();
|
|
||||||
if (panel) {
|
if (panel) {
|
||||||
Alpine.$data(panel).syncUserGold(curData.jjb);
|
Alpine.$data(panel).syncUserGold(curData.jjb);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,6 +307,52 @@ class HorseRaceControllerTest extends TestCase
|
|||||||
$this->assertCount(1, $response->json('history'));
|
$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