Files
vkv/app/Jobs/RecalculateClaimedRanksJob.php
Zdeněk Burda 41e3ce6f25 Initial commit
2026-01-09 21:26:40 +01:00

213 lines
7.1 KiB
PHP

<?php
namespace App\Jobs;
use App\Models\EvaluationLock;
use App\Models\EvaluationRun;
use App\Models\LogResult;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
/**
* Job: RecalculateClaimedRanksJob
*
* Přepočítá pořadí deklarovaných výsledků (CLAIMED) pro daný evaluation run.
* - rank_overall: pořadí podle band + (SINGLE|MULTI)
* - rank_in_category: pořadí podle band + (SINGLE|MULTI) + power (LP|QRP|N)
* - OK/OL pořadí: stejné výpočty pouze pro české účastníky (pcall začíná OK/OL)
* - CHECK logy se nepočítají
*/
class RecalculateClaimedRanksJob implements ShouldQueue, ShouldBeUnique
{
use Queueable;
public int $uniqueFor = 30;
public int $tries = 2;
public array $backoff = [60];
public function __construct(
protected int $evaluationRunId
) {
}
public function uniqueId(): string
{
return (string) $this->evaluationRunId;
}
public function handle(): void
{
$run = EvaluationRun::find($this->evaluationRunId);
if (! $run) {
return;
}
// Zabraňuje souběžnému přepočtu pro stejné kolo (claimed scoreboard).
$lock = EvaluationLock::acquire(
key: "evaluation:claimed-ranks:round:{$run->round_id}",
run: $run,
ttl: 300
);
if (! $lock) {
return;
}
try {
// Vynuluje pořadí, aby staré hodnoty neovlivnily nový přepočet.
LogResult::where('evaluation_run_id', $run->id)
->update([
'rank_overall' => null,
'rank_in_category' => null,
'rank_overall_ok' => null,
'rank_in_category_ok' => null,
]);
// Načte všechny deklarované výsledky včetně vazeb pro kategorii a výkon.
$results = LogResult::with(['log', 'category', 'powerCategory'])
->where('evaluation_run_id', $run->id)
->get();
// Do pořadí vstupují jen logy se statusem OK a kategorií SINGLE/MULTI.
$eligible = $results->filter(function (LogResult $r) {
return $r->status === 'OK' && $this->getCategoryType($r) !== null;
});
// Celkové pořadí: podle pásma + SINGLE/MULTI + 6H/standard.
$allOverall = $eligible->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getSixHourBucket($r));
foreach ($allOverall as $items) {
$this->applyRanking($items, 'rank_overall');
}
// Pořadí výkonových kategorií: pásmo + SINGLE/MULTI + výkon (jen LP/QRP/N) + 6H/standard.
$allPower = $eligible->filter(fn (LogResult $r) => $this->getPowerClass($r) !== null)
->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getPowerClass($r) . '|' . $this->getSixHourBucket($r));
foreach ($allPower as $items) {
$this->applyRanking($items, 'rank_in_category');
}
// Česká podmnožina (OK/OL) pro národní pořadí.
$okOnly = $eligible->filter(fn (LogResult $r) => $this->isOkCall($r));
$okOverall = $okOnly->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getSixHourBucket($r));
foreach ($okOverall as $items) {
$this->applyRanking($items, 'rank_overall_ok');
}
// České pořadí výkonových kategorií: stejné jako power, ale jen OK/OL a 6H/standard.
$okPower = $okOnly->filter(fn (LogResult $r) => $this->getPowerClass($r) !== null)
->groupBy(fn (LogResult $r) => $r->band_id . '|' . $this->getCategoryBucket($r) . '|' . $this->getPowerClass($r) . '|' . $this->getSixHourBucket($r));
foreach ($okPower as $items) {
$this->applyRanking($items, 'rank_in_category_ok');
}
} finally {
EvaluationLock::release("evaluation:claimed-ranks:round:{$run->round_id}", $run);
}
}
protected function applyRanking(Collection $items, string $rankField): void
{
// Řazení podle claimed_score (desc), pak QSO (desc), pak log_id (asc) kvůli stabilitě.
$sorted = $items->sort(function (LogResult $a, LogResult $b) {
$scoreA = $a->claimed_score ?? 0;
$scoreB = $b->claimed_score ?? 0;
if ($scoreA !== $scoreB) {
return $scoreB <=> $scoreA;
}
$qsoA = $a->claimed_qso_count ?? 0;
$qsoB = $b->claimed_qso_count ?? 0;
if ($qsoA !== $qsoB) {
return $qsoB <=> $qsoA;
}
return $a->log_id <=> $b->log_id;
})->values();
$lastScore = null;
$lastQso = null;
$lastRank = 0;
foreach ($sorted as $index => $result) {
$score = $result->claimed_score ?? 0;
$qso = $result->claimed_qso_count ?? 0;
// Shodný výsledek (stejné skóre + QSO) = stejné pořadí.
if ($score === $lastScore && $qso === $lastQso) {
$rank = $lastRank;
} else {
$rank = $index + 1;
}
$result->{$rankField} = $rank;
$result->save();
$lastScore = $score;
$lastQso = $qso;
$lastRank = $rank;
}
}
protected function getCategoryType(LogResult $r): ?string
{
$name = $r->category?->name;
if (! $name) {
return null;
}
$lower = mb_strtolower($name);
if (str_contains($lower, 'single')) {
return 'SINGLE';
}
if (str_contains($lower, 'multi')) {
return 'MULTI';
}
return null;
}
protected function getCategoryBucket(LogResult $r): ?string
{
$type = $this->getCategoryType($r);
if ($type === null) {
return null;
}
return $this->getSixHourBucket($r) === '6H' ? 'ALL' : $type;
}
protected function getPowerClass(LogResult $r): ?string
{
$name = $r->powerCategory?->name;
if (! $name) {
return null;
}
$upper = mb_strtoupper($name);
if (in_array($upper, ['LP', 'QRP', 'N'], true)) {
return $upper;
}
return null;
}
protected function isOkCall(LogResult $r): bool
{
$call = $this->normalizeCallsign($r->log?->pcall ?? '');
return Str::startsWith($call, ['OK', 'OL']);
}
protected function normalizeCallsign(string $call): string
{
$value = mb_strtoupper(trim($call));
$value = preg_replace('/\s+/', '', $value);
return $value ?? '';
}
protected function getSixHourBucket(LogResult $r): string
{
$sixh = $r->sixhr_category;
if ($sixh === null) {
$sixh = $r->log?->sixhr_category;
}
return $sixh ? '6H' : 'STD';
}
}