Initial commit
This commit is contained in:
212
app/Jobs/RecalculateClaimedRanksJob.php
Normal file
212
app/Jobs/RecalculateClaimedRanksJob.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?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';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user