Skip to content

Commit 93c7359

Browse files
alexp8claude
andauthored
Develop (#526)
* Add points field to daily stats API and optimize query performance (#523) * Add points field to daily stats API and optimize query performance Reduces database queries from 3 to 1 by replacing correlated subqueries with eager loading and PHP-based aggregation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * show streamer name --------- Co-authored-by: Claude <noreply@anthropic.com> * Refactor player stats to service layer and add monthly stats endpoint Extracted player time-range stats logic from controller to StatsService for better separation of concerns and reusability. Changes: - Add StatsService::getPlayerTimeRangeStats() method for flexible time-range queries - Refactor getPlayerDailyStats to use service (reduced from ~80 to ~45 lines) - Add new getPlayerMonthlyStats endpoint at /api/v1/ladder/{game}/player/{player}/month - Consistent caching strategy (5 min) for both daily and monthly stats 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1cd6d49 commit 93c7359

3 files changed

Lines changed: 118 additions & 42 deletions

File tree

cncnet-api/app/Http/Controllers/ApiLadderController.php

Lines changed: 62 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use App\Http\Services\LadderService;
1010
use App\Http\Services\PlayerService;
1111
use App\Http\Services\PointService;
12+
use App\Http\Services\StatsService;
1213
use App\Jobs\Qm\SaveLadderResultJob;
1314
use App\Models\Achievement;
1415
use App\Models\AchievementProgress;
@@ -40,6 +41,7 @@ class ApiLadderController extends Controller
4041
private $adminService;
4142
private $pointService;
4243
private $duneGameService;
44+
private $statsService;
4345

4446
public function __construct()
4547
{
@@ -50,6 +52,7 @@ public function __construct()
5052
$this->clanService = new ClanService();
5153
$this->adminService = new AdminService();
5254
$this->pointService = new PointService();
55+
$this->statsService = new StatsService();
5356
}
5457

5558
public function pingLadder(Request $request)
@@ -922,53 +925,70 @@ public function getPlayerDailyStats(Request $request, $game = null, $player = nu
922925
return response()->json(['error' => 'No active ladder history'], 404);
923926
}
924927

925-
// Get today's date range
926-
$startOfDay = Carbon::now()->startOfDay();
927-
$endOfDay = Carbon::now()->endOfDay();
928-
929-
// Single optimized query - fetch all player's games for today with teammate data
930-
$playerGames = PlayerGameReport::where('player_game_reports.player_id', '=', $playerModel->id)
931-
->where('player_game_reports.spectator', '=', false)
932-
->whereBetween('player_game_reports.created_at', [$startOfDay, $endOfDay])
933-
->join('game_reports', 'game_reports.id', '=', 'player_game_reports.game_report_id')
934-
->join('games', 'games.id', '=', 'game_reports.game_id')
935-
->where('games.ladder_history_id', '=', $history->id)
936-
->where('game_reports.valid', '=', true)
937-
->where('game_reports.best_report', '=', true)
938-
->select('player_game_reports.*')
939-
->with(['gameReport.playerGameReports' => function($q) {
940-
$q->where('spectator', false)->select('id', 'game_report_id', 'team', 'won', 'spectator');
941-
}])
942-
->get();
943-
944-
// Process results in PHP to calculate wins, losses, and points
945-
$wins = 0;
946-
$losses = 0;
947-
$points = 0;
948-
949-
foreach ($playerGames as $pgr) {
950-
$points += $pgr->points ?? 0;
951-
952-
// Check if any teammate on same team won
953-
$teammateWon = $pgr->gameReport->playerGameReports
954-
->where('team', $pgr->team)
955-
->where('won', true)
956-
->isNotEmpty();
957-
958-
if ($teammateWon) {
959-
$wins++;
960-
} else if (!$pgr->draw) {
961-
$losses++;
962-
}
963-
}
928+
// Get stats for today
929+
$stats = $this->statsService->getPlayerTimeRangeStats(
930+
$playerModel,
931+
$history,
932+
Carbon::now()->startOfDay(),
933+
Carbon::now()->endOfDay()
934+
);
964935

965936
return response()->json([
966937
'player' => $player,
967938
'ladder' => $game,
968939
'date' => Carbon::now()->format('Y-m-d'),
969-
'wins' => $wins,
970-
'losses' => $losses,
971-
'points' => $points
940+
'wins' => $stats['wins'],
941+
'losses' => $stats['losses'],
942+
'points' => $stats['points']
943+
], 200);
944+
});
945+
}
946+
947+
public function getPlayerMonthlyStats(Request $request, $game = null, $player = null)
948+
{
949+
return Cache::remember("getPlayerMonthlyStats/$game/$player/" . Carbon::now()->format('m-Y'), 5 * 60, function () use ($game, $player)
950+
{
951+
// Find the ladder by abbreviation
952+
$ladder = Ladder::where('abbreviation', '=', $game)->first();
953+
954+
if (!$ladder)
955+
{
956+
return response()->json(['error' => 'Ladder not found'], 404);
957+
}
958+
959+
// Find the player by username and ladder
960+
$playerModel = Player::where('username', '=', $player)
961+
->where('ladder_id', '=', $ladder->id)
962+
->first();
963+
964+
if (!$playerModel)
965+
{
966+
return response()->json(['error' => 'Player not found'], 404);
967+
}
968+
969+
// Get current ladder history
970+
$history = $ladder->currentHistory();
971+
972+
if (!$history)
973+
{
974+
return response()->json(['error' => 'No active ladder history'], 404);
975+
}
976+
977+
// Get stats for current month
978+
$stats = $this->statsService->getPlayerTimeRangeStats(
979+
$playerModel,
980+
$history,
981+
Carbon::now()->startOfMonth(),
982+
Carbon::now()->endOfMonth()
983+
);
984+
985+
return response()->json([
986+
'player' => $player,
987+
'ladder' => $game,
988+
'month' => Carbon::now()->format('m-Y'),
989+
'wins' => $stats['wins'],
990+
'losses' => $stats['losses'],
991+
'points' => $stats['points']
972992
], 200);
973993
});
974994
}

cncnet-api/app/Http/Services/StatsService.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use App\Models\GameReport;
1515
use App\Models\PlayerGameReport;
1616
use Carbon\Carbon;
17+
use DateTime;
1718
use Illuminate\Support\Facades\Cache;
1819
use Illuminate\Support\Facades\Log;
1920
use Illuminate\Support\Str;
@@ -304,6 +305,60 @@ public function checkPlayerIsPlayerOfTheDay($history, $player)
304305
return null;
305306
}
306307

308+
/**
309+
* Get player stats for a given time range (daily, monthly, or custom).
310+
*
311+
* @param Player $player
312+
* @param LadderHistory $history
313+
* @param DateTime $startTime
314+
* @param DateTime $endTime
315+
* @return array
316+
*/
317+
public function getPlayerTimeRangeStats(Player $player, LadderHistory $history, DateTime $startTime, DateTime $endTime): array
318+
{
319+
// Single optimized query - fetch all player's games for time range with teammate data
320+
$playerGames = PlayerGameReport::where('player_game_reports.player_id', '=', $player->id)
321+
->where('player_game_reports.spectator', '=', false)
322+
->whereBetween('player_game_reports.created_at', [$startTime, $endTime])
323+
->join('game_reports', 'game_reports.id', '=', 'player_game_reports.game_report_id')
324+
->join('games', 'games.id', '=', 'game_reports.game_id')
325+
->where('games.ladder_history_id', '=', $history->id)
326+
->where('game_reports.valid', '=', true)
327+
->where('game_reports.best_report', '=', true)
328+
->select('player_game_reports.*')
329+
->with(['gameReport.playerGameReports' => function($q) {
330+
$q->where('spectator', false)->select('id', 'game_report_id', 'team', 'won', 'spectator');
331+
}])
332+
->get();
333+
334+
// Process results to calculate wins, losses, and points
335+
$wins = 0;
336+
$losses = 0;
337+
$points = 0;
338+
339+
foreach ($playerGames as $pgr) {
340+
$points += $pgr->points ?? 0;
341+
342+
// Check if any teammate on same team won
343+
$teammateWon = $pgr->gameReport->playerGameReports
344+
->where('team', $pgr->team)
345+
->where('won', true)
346+
->isNotEmpty();
347+
348+
if ($teammateWon) {
349+
$wins++;
350+
} else if (!$pgr->draw) {
351+
$losses++;
352+
}
353+
}
354+
355+
return [
356+
'wins' => $wins,
357+
'losses' => $losses,
358+
'points' => $points,
359+
];
360+
}
361+
307362
public function getPlayerMatchups($player, $history)
308363
{
309364
$ladderType = $history->ladder->ladder_type;

cncnet-api/routes/api.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
Route::get('/{game}/player/{player}', [\App\Http\Controllers\ApiLadderController::class, 'getLadderPlayerFromPublicApi']);
104104
Route::get('/{game}/player/{player}/webview', [\App\Http\Controllers\ApiLadderStatsProfile::class, 'getWebview']);
105105
Route::get('/{game}/player/{player}/today', [\App\Http\Controllers\ApiLadderController::class, 'getPlayerDailyStats']);
106+
Route::get('/{game}/player/{player}/month', [\App\Http\Controllers\ApiLadderController::class, 'getPlayerMonthlyStats']);
106107
});
107108

108109
// Ultra short cache ladder endpoints

0 commit comments

Comments
 (0)