This commit is contained in:
2026-03-20 21:17:26 +01:00
parent 1a62fcb81d
commit 29c3ff8572
229 changed files with 13147 additions and 2577 deletions

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Services\AchievementService;
use App\Services\XPService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class RecalculateUserXpCommand extends Command
{
protected $signature = 'skinbase:recalculate-user-xp
{user_id? : The ID of a single user to recompute}
{--all : Recompute XP and level for all non-deleted users}
{--chunk=1000 : Chunk size when --all is used}
{--dry-run : Show computed values without writing}
{--sync-achievements : Re-run achievement checks after a live recalculation}';
protected $description = 'Rebuild stored user XP, level, and rank from user_xp_logs';
public function handle(XPService $xp, AchievementService $achievements): int
{
$userId = $this->argument('user_id');
$all = (bool) $this->option('all');
$dryRun = (bool) $this->option('dry-run');
$syncAchievements = (bool) $this->option('sync-achievements');
$chunk = max(1, (int) $this->option('chunk'));
if ($userId !== null && $all) {
$this->error('Provide either a user_id or --all, not both.');
return self::FAILURE;
}
if ($userId !== null) {
return $this->recalculateSingle((int) $userId, $xp, $achievements, $dryRun, $syncAchievements);
}
if ($all) {
return $this->recalculateAll($xp, $achievements, $chunk, $dryRun, $syncAchievements);
}
$this->error('Provide a user_id or use --all.');
return self::FAILURE;
}
private function recalculateSingle(
int $userId,
XPService $xp,
AchievementService $achievements,
bool $dryRun,
bool $syncAchievements,
): int {
$exists = DB::table('users')->where('id', $userId)->exists();
if (! $exists) {
$this->error("User {$userId} not found.");
return self::FAILURE;
}
$label = $dryRun ? '[DRY-RUN]' : '[LIVE]';
$this->line("{$label} Recomputing XP for user #{$userId}...");
$result = $xp->recalculateStoredProgress($userId, ! $dryRun);
$this->table(
['Field', 'Stored', 'Computed'],
[
['xp', $result['previous']['xp'], $result['computed']['xp']],
['level', $result['previous']['level'], $result['computed']['level']],
['rank', $result['previous']['rank'], $result['computed']['rank']],
]
);
if ($dryRun) {
if ($syncAchievements) {
$pending = $achievements->previewUnlocks($userId);
$this->line('Achievements preview: ' . (empty($pending) ? 'no pending unlocks' : implode(', ', $pending)));
}
$this->warn('Dry-run: no changes written.');
return self::SUCCESS;
}
if ($syncAchievements) {
$unlocked = $achievements->checkAchievements($userId);
$this->line('Achievements checked: ' . (empty($unlocked) ? 'no new unlocks' : implode(', ', $unlocked)));
}
$this->info($result['changed'] ? "XP updated for user #{$userId}." : "User #{$userId} was already in sync.");
return self::SUCCESS;
}
private function recalculateAll(
XPService $xp,
AchievementService $achievements,
int $chunk,
bool $dryRun,
bool $syncAchievements,
): int {
$total = DB::table('users')->whereNull('deleted_at')->count();
$label = $dryRun ? '[DRY-RUN]' : '[LIVE]';
$this->info("{$label} Recomputing XP for {$total} users (chunk={$chunk})...");
$processed = 0;
$changed = 0;
$pendingAchievementUsers = 0;
$pendingAchievementUnlocks = 0;
$appliedAchievementUnlocks = 0;
$bar = $this->output->createProgressBar($total);
$bar->start();
DB::table('users')
->whereNull('deleted_at')
->orderBy('id')
->chunkById($chunk, function ($users) use ($xp, $achievements, $dryRun, $syncAchievements, &$processed, &$changed, &$pendingAchievementUsers, &$pendingAchievementUnlocks, &$appliedAchievementUnlocks, $bar): void {
foreach ($users as $user) {
$result = $xp->recalculateStoredProgress((int) $user->id, ! $dryRun);
if ($result['changed']) {
$changed++;
}
if ($syncAchievements) {
if ($dryRun) {
$pending = $achievements->previewUnlocks((int) $user->id);
if (! empty($pending)) {
$pendingAchievementUsers++;
$pendingAchievementUnlocks += count($pending);
}
} else {
$unlocked = $achievements->checkAchievements((int) $user->id);
$appliedAchievementUnlocks += count($unlocked);
}
}
$processed++;
$bar->advance();
}
});
$bar->finish();
$this->newLine();
$summary = "Done - {$processed} users processed, {$changed} " . ($dryRun ? 'would change.' : 'updated.');
if ($syncAchievements) {
if ($dryRun) {
$summary .= " Achievement preview: {$pendingAchievementUnlocks} pending unlock(s) across {$pendingAchievementUsers} user(s).";
} else {
$summary .= " Achievements re-checked: {$appliedAchievementUnlocks} unlock(s) applied.";
}
}
$this->info($summary);
return self::SUCCESS;
}
}