213 lines
7.1 KiB
PHP
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';
|
|
}
|
|
|
|
|
|
}
|