164 lines
5.8 KiB
PHP
164 lines
5.8 KiB
PHP
<?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;
|
|
}
|
|
}
|